mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-14 16:17:25 +00:00
refactor(store): add QueryScope abstraction for composable multi-level isolation
Introduce store.QueryScope struct that generates WHERE clauses for tenant + optional project scoping. Add scopeClause/scopeClauseAlias wrappers in pg/helpers.go as drop-in replacements for tenantClauseN. Old tenant-only helpers remain for existing callers (76 occurrences). Migrate providers.go (4 calls) as proof-of-concept. Future callers should use scopeClause for project-scope readiness.
This commit is contained in:
1 parent
379da91a68
commit
deb821063a
3 files changed
+107
-4
No files matched your search
@@ -257,3 +257,28 @@ func requireTenantID(ctx context.Context) (uuid.UUID, error) {
|
||||
}
|
||||
return tid, nil
|
||||
}
|
||||
|
||||
// --- Scope-based helpers (composable alternative to tenantClauseN) ---
|
||||
// Use these for new code. Old tenantClauseN/tenantClauseNAlias remain for existing callers.
|
||||
|
||||
// scopeClause extracts QueryScope from context and generates WHERE conditions.
|
||||
// Drop-in replacement for tenantClauseN that supports future project-level scoping.
|
||||
func scopeClause(ctx context.Context, startParam int) (clause string, args []any, nextParam int, err error) {
|
||||
scope, err := store.ScopeFromContext(ctx)
|
||||
if err != nil {
|
||||
return "", nil, startParam, err
|
||||
}
|
||||
clause, args, nextParam = scope.WhereClause(startParam)
|
||||
return clause, args, nextParam, nil
|
||||
}
|
||||
|
||||
// scopeClauseAlias is like scopeClause but qualifies columns with a table alias.
|
||||
// SECURITY: alias is interpolated into SQL — callers MUST pass hardcoded string literals only.
|
||||
func scopeClauseAlias(ctx context.Context, startParam int, alias string) (clause string, args []any, nextParam int, err error) {
|
||||
scope, err := store.ScopeFromContext(ctx)
|
||||
if err != nil {
|
||||
return "", nil, startParam, err
|
||||
}
|
||||
clause, args, nextParam = scope.WhereClauseAlias(startParam, alias)
|
||||
return clause, args, nextParam, nil
|
||||
}
|
||||
@@ -61,7 +61,7 @@ func (s *PGProviderStore) CreateProvider(ctx context.Context, p *store.LLMProvid
|
||||
}
|
||||
|
||||
func (s *PGProviderStore) GetProvider(ctx context.Context, id uuid.UUID) (*store.LLMProviderData, error) {
|
||||
tClause, tArgs, err := tenantClauseN(ctx, 2)
|
||||
tClause, tArgs, _, err := scopeClause(ctx, 2)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -80,7 +80,7 @@ func (s *PGProviderStore) GetProvider(ctx context.Context, id uuid.UUID) (*store
|
||||
}
|
||||
|
||||
func (s *PGProviderStore) GetProviderByName(ctx context.Context, name string) (*store.LLMProviderData, error) {
|
||||
tClause, tArgs, err := tenantClauseN(ctx, 2)
|
||||
tClause, tArgs, _, err := scopeClause(ctx, 2)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -99,7 +99,7 @@ func (s *PGProviderStore) GetProviderByName(ctx context.Context, name string) (*
|
||||
}
|
||||
|
||||
func (s *PGProviderStore) ListProviders(ctx context.Context) ([]store.LLMProviderData, error) {
|
||||
tClause, tArgs, err := tenantClauseN(ctx, 1)
|
||||
tClause, tArgs, _, err := scopeClause(ctx, 1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -168,7 +168,7 @@ func (s *PGProviderStore) UpdateProvider(ctx context.Context, id uuid.UUID, upda
|
||||
}
|
||||
|
||||
func (s *PGProviderStore) DeleteProvider(ctx context.Context, id uuid.UUID) error {
|
||||
tClause, tArgs, err := tenantClauseN(ctx, 2)
|
||||
tClause, tArgs, _, err := scopeClause(ctx, 2)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// QueryScope represents the multi-level isolation scope for database queries.
|
||||
// Currently supports tenant-level scoping; designed to be extended with
|
||||
// project-level scoping when the Project feature is implemented.
|
||||
type QueryScope struct {
|
||||
TenantID uuid.UUID
|
||||
ProjectID *uuid.UUID // nil = no project filter (tenant-only)
|
||||
}
|
||||
|
||||
// ScopeFromContext extracts the query scope from context.
|
||||
// Returns error if tenant ID is missing (fail-closed).
|
||||
func ScopeFromContext(ctx context.Context) (QueryScope, error) {
|
||||
tid := TenantIDFromContext(ctx)
|
||||
if tid == uuid.Nil {
|
||||
return QueryScope{}, fmt.Errorf("tenant_id required")
|
||||
}
|
||||
return QueryScope{TenantID: tid}, nil
|
||||
}
|
||||
|
||||
// WhereClause generates SQL WHERE conditions for the scope.
|
||||
// Returns the clause string (e.g. " AND tenant_id = $3"), args, and the next
|
||||
// available parameter index. Callers chain: startParam → nextParam.
|
||||
func (s QueryScope) WhereClause(startParam int) (clause string, args []any, nextParam int) {
|
||||
clause = fmt.Sprintf(" AND tenant_id = $%d", startParam)
|
||||
args = []any{s.TenantID}
|
||||
nextParam = startParam + 1
|
||||
|
||||
if s.ProjectID != nil {
|
||||
clause += fmt.Sprintf(" AND project_id = $%d", nextParam)
|
||||
args = append(args, *s.ProjectID)
|
||||
nextParam++
|
||||
}
|
||||
|
||||
return clause, args, nextParam
|
||||
}
|
||||
|
||||
// WhereClauseAlias generates SQL WHERE conditions qualified with a table alias.
|
||||
// Used in JOIN queries to avoid column ambiguity.
|
||||
// SECURITY: alias is interpolated into SQL — callers MUST pass hardcoded string literals only.
|
||||
func (s QueryScope) WhereClauseAlias(startParam int, alias string) (clause string, args []any, nextParam int) {
|
||||
clause = fmt.Sprintf(" AND %s.tenant_id = $%d", alias, startParam)
|
||||
args = []any{s.TenantID}
|
||||
nextParam = startParam + 1
|
||||
|
||||
if s.ProjectID != nil {
|
||||
clause += fmt.Sprintf(" AND %s.project_id = $%d", alias, nextParam)
|
||||
args = append(args, *s.ProjectID)
|
||||
nextParam++
|
||||
}
|
||||
|
||||
return clause, args, nextParam
|
||||
}
|
||||
|
||||
// InsertValues returns column names and values for INSERT operations.
|
||||
// Falls back to MasterTenantID when TenantID is nil.
|
||||
func (s QueryScope) InsertValues() (columns []string, values []any) {
|
||||
tid := s.TenantID
|
||||
if tid == uuid.Nil {
|
||||
tid = MasterTenantID
|
||||
}
|
||||
columns = []string{"tenant_id"}
|
||||
values = []any{tid}
|
||||
|
||||
if s.ProjectID != nil {
|
||||
columns = append(columns, "project_id")
|
||||
values = append(values, *s.ProjectID)
|
||||
}
|
||||
|
||||
return columns, values
|
||||
}
|
||||
Reference in new issue
Block a user