Documentation
¶
Index ¶
- Variables
- func BackfillSchemaHistory(ctx context.Context, pool *pgxpool.Pool) (int, error)
- func NewControlPlaneRouter(svc Service, adminKey string) *chi.Mux
- func SuggestTenantID(raw string) string
- type RegisterRequest
- type RollbackResult
- type Service
- type ServiceOption
- type Tenant
- type TenantProvisionHook
Constants ¶
This section is empty.
Variables ¶
var ( ErrAlreadyExists = errors.New("already exists") ErrNotFound = errors.New("not found") ErrInvalidInput = errors.New("invalid input") )
Sentinel errors used by the service layer and mapped to HTTP status codes by the control plane HTTP handlers.
Functions ¶
func BackfillSchemaHistory ¶
BackfillSchemaHistory captures the CURRENT schema of every pre-versioning tenant (json_schema set, history empty) as its v1 — run once at boot, so the history is immediately useful on an existing install. Each schema is re-marshaled through schema.APISchema so its hash is canonical (raw jsonb text would hash differently than the engine's own marshaling and break the unchanged-schema dedup). Best-effort per tenant; returns the first error after attempting all.
func NewControlPlaneRouter ¶
NewControlPlaneRouter builds the chi.Mux for the control plane API (port 9090). adminKey is compared against the X-Admin-Key request header on all routes except /health.
func SuggestTenantID ¶
SuggestTenantID converts a rejected id into the closest VALID one: lowercased, with every separator and any other character DROPPED (mi-clinica → miclinica), leading non-letters trimmed, capped at 30. Separators are dropped rather than replaced because the replacement used to be '_', which the id rule no longer accepts — the suggestion has to be something that actually works. Returns "" when nothing salvageable remains; callers append it to the validation error so the user gets an actionable fix, not just a rule.
Types ¶
type RegisterRequest ¶
type RegisterRequest struct {
TenantID string `json:"tenant_id"`
DisplayName string `json:"display_name"`
Email string `json:"email"`
Plan string `json:"plan"`
Schema *schema.APISchema `json:"schema"`
}
RegisterRequest carries all data needed to onboard a new tenant.
type RollbackResult ¶
type RollbackResult struct {
Outcome *migration.ApplyOutcome
TargetVersion int
NewVersion int
Schema *schema.APISchema
}
RollbackResult reports an applied rollback: the migration outcome (what was applied/gated, same shape as a deploy), the version rolled back TO, the NEW history version the rollback appended (0 if the history write failed — logged, never blocks the applied migration), and the schema now live.
type Service ¶
type Service interface {
Register(ctx context.Context, req RegisterRequest) (*Tenant, error)
GetByID(ctx context.Context, id string) (*Tenant, error)
// UpdateSchema applies a schema change with NO destructive approval (additive:
// every drop is gated). Equivalent to UpdateSchemaApproved with no approved drops.
UpdateSchema(ctx context.Context, id string, s *schema.APISchema) error
// UpdateSchemaApproved applies a schema change, executing ONLY the destructive
// drops whose approval key is in approvedDrops (DropTable "<table>" / DropColumn
// "<table>.<column>"). With an empty slice it is identical to UpdateSchema (fail-
// safe: nothing is dropped). It returns what was applied/gated for the response.
UpdateSchemaApproved(ctx context.Context, id string, s *schema.APISchema, approvedDrops []string) (*migration.ApplyOutcome, error)
// PreviewSchema computes a dry-run of the schema change WITHOUT applying it: the
// classified plan plus the impact (rows lost) of each data-losing drop, evaluated
// against the given approval set. It is the informed-consent surface.
PreviewSchema(ctx context.Context, id string, s *schema.APISchema, approvedDrops []string) (*migration.Preview, error)
GetSchema(ctx context.Context, id string) (*schema.APISchema, error)
// ListSchemaHistory returns one page of the tenant's deployed-schema history,
// newest first (append-only; the latest version is the current schema).
ListSchemaHistory(ctx context.Context, id string, page, perPage int) (*schemahistory.Page, error)
// GetSchemaVersion returns one recorded version WITH its full schema.
GetSchemaVersion(ctx context.Context, id string, version int) (*schemahistory.Version, error)
// RollbackSchema re-applies the stored schema of history version v — the SAME
// diff→gate→apply migration path as UpdateSchemaApproved (NOT a second engine),
// so what later versions added is reverted as gated destructive drops, and data
// already lost by an approved forward drop is NOT recovered. Append-only: the
// rollback records a NEW version whose content is v's.
RollbackSchema(ctx context.Context, id string, version int, approvedDrops []string) (*RollbackResult, error)
}
Service is the control plane dependency injected into the HTTP handlers. All methods are safe to mock in unit tests.
func NewService ¶
NewService creates a production Service. redisClient may be nil — in that case schema updates are written to the DB only and pg_notify handles cache invalidation.
type ServiceOption ¶
type ServiceOption func(*pgService)
ServiceOption configures NewService (variadic so existing call sites are untouched).
func WithProvisionHook ¶
func WithProvisionHook(h TenantProvisionHook) ServiceOption
WithProvisionHook wires the consumer's per-tenant provisioning seam (ENG-8) into every registration this Service performs. See TenantProvisionHook.
type Tenant ¶
type Tenant struct {
ID string `json:"id"`
PGSchema string `json:"pg_schema"`
DisplayName string `json:"display_name"`
Email string `json:"email"`
Plan string `json:"plan"`
CreatedAt time.Time `json:"created_at"`
}
Tenant is the created tenant record returned to the caller.
func RegisterTenant ¶
RegisterTenant onboards a new tenant in 10 atomic steps:
- Validate tenantID format.
- Verify no duplicate in public.tenants. 3-7. Transaction: INSERT tenant + CREATE SCHEMA + INSERT policy → COMMIT.
- ApplyTenantMigration: CREATE TABLE for each resource (+ the optional TenantProvisionHook — consumer DDL — via RegisterTenantWithHook).
- pg_notify('schema_updated', tenantID).
- Return the created Tenant.
func RegisterTenantWithHook ¶
func RegisterTenantWithHook(ctx context.Context, pool *pgxpool.Pool, req RegisterRequest, hook TenantProvisionHook) (*Tenant, error)
RegisterTenantWithHook is RegisterTenant with a consumer provisioning hook (nil = identical to RegisterTenant). See TenantProvisionHook.
type TenantProvisionHook ¶
type TenantProvisionHook func(ctx context.Context, pool *pgxpool.Pool, tenantID, pgSchema string) error
TenantProvisionHook is a consumer's per-tenant provisioning seam (ENG-8, CONSUMER-PATH-S1): it runs INSIDE tenant registration, after the engine has provisioned the tenant's tables, so consumer-owned DDL (generated columns, CHECK constraints, partial indexes — the things Config.BeforeStart applies at boot) reaches tenants created while the app is LIVE. Before this seam existed, the normal SaaS flow — install → boot → register tenant — produced a tenant missing the consumer's DDL, and a core endpoint answered 500 until a manual restart re-ran BeforeStart (measured on the 58, commerce GAPS 3-6).
The hook is part of the registration's all-or-nothing contract: an error rolls the whole registration back (no tenant is left half-provisioned) and is returned to the caller. It MUST be idempotent — BeforeStart typically re-runs the same DDL over all tenants at every boot.