/** * Supabase service layer for the Schema Builder tables. All functions take a * pre-constructed client so the package does not couple to any one app's * Supabase wiring. * * PostgREST note: `.or()` chained twice generates two root-level `or=` query * params which PostgREST interprets as "pick one", returning HTTP 400. Build * a single nested `and(or(...),or(...))` expression so both filters AND. */ import type { AppScope, TenantEntity, TenantEntityRelation, TenantFieldDefinition } from './types.js'; export type LooseChannel = any; export type LooseSupabaseClient = any; export declare function getSchemaEntities(client: LooseSupabaseClient, tenantId: string | null, appScope?: AppScope): Promise; export declare function getSchemaRelations(client: LooseSupabaseClient, tenantId: string | null, appScope?: AppScope): Promise; export declare function createSchemaEntity(client: LooseSupabaseClient, entity: Omit): Promise; export declare function updateSchemaEntity(client: LooseSupabaseClient, id: string, updates: Partial): Promise; export declare function deleteSchemaEntity(client: LooseSupabaseClient, id: string): Promise; export declare function createSchemaRelation(client: LooseSupabaseClient, relation: Omit): Promise; export declare function updateSchemaRelation(client: LooseSupabaseClient, id: string, updates: Partial): Promise; export declare function deleteSchemaRelation(client: LooseSupabaseClient, id: string): Promise; /** * Fetch the active fields for a single entity, sorted by sort_order then * creation time. Used by the per-entity properties panel. */ export declare function getEntityFields(client: LooseSupabaseClient, entityId: string): Promise; /** * Fetch all active fields for the given tenant. Used by * `useSchemaController` to populate `fields` on every entity node at once * without N+1 round-trips. NULL tenant fields (platform / system fields) * are included so they appear on shared entities too. */ export declare function getTenantFields(client: LooseSupabaseClient, tenantId: string | null): Promise; export declare function createEntityField(client: LooseSupabaseClient, field: Omit): Promise; export declare function updateEntityField(client: LooseSupabaseClient, id: string, updates: Partial): Promise; export declare function deleteEntityField(client: LooseSupabaseClient, id: string): Promise; /** * Atomically reorder the active fields of an entity via the * `reorder_entity_fields` SECURITY DEFINER RPC. `orderedFieldIds` MUST be * the complete set of active field ids for `entityId` (no missing, extra, * or duplicate ids); the RPC rejects partial arrays with * `invalid_parameter_value` to prevent accidental data loss. * * Phase 3A — see `docs/20260504-schema-builder-phase-3-plan-v1.00W.md` §3.A * and migration `20260505000000_field_sort_order_and_reorder_rpc.sql`. */ export declare function reorderEntityFields(client: LooseSupabaseClient, entityId: string, orderedFieldIds: string[]): Promise; /** * Result of `rename_physical_column`. On a dry-run `executed` is always * `false` and `would_execute` / `affected_views` / `affected_policies` are * populated so the UI can ask the user to confirm. On a wet-run `executed` * flips to `true` when the ALTER TABLE succeeded. * * REFUSALS carry a `reason`, and since migration 20261101000000 they are * distinguished — because the caller can act on one and not the other: * * 'no_physical_table' there is genuinely no `public.` table * 'table_not_registered' the table exists but is not in * `schema_builder_physical_tables` for this tenant, so a * platform developer can register it * * Both were previously `no_physical_table`, and because the allowlist ships EMPTY * that was the answer for every entity — which the dialog rendered as "No physical * table exists for this entity", usually false. Neither refusal writes an audit row. * * Phase 3B — see `docs/20260504-schema-builder-phase-3-plan-v1.00W.md` §3.B * and migration `20260506000000_rename_physical_column_rpc.sql`. */ export interface RenamePhysicalColumnResult { executed: boolean; /** See the note above: 'no_physical_table' | 'table_not_registered' on refusal. */ reason?: string; audit_id?: string; would_execute?: string; affected_views?: string[]; affected_policies?: string[]; old_field_name?: string; new_field_name?: string; } /** * Invoke the `rename_physical_column` RPC. Callers MUST run a dry-run first * (opts.dryRun = true) and show the returned `would_execute` + affected * objects to the user before calling again with `dryRun: false`. The RPC * writes one audit row per call (dry-run and wet-run both) so every attempt * — successful or not — is observable in `schema_mutations_audit`. */ /** * How many physical tables this tenant has registered for Schema-Builder renames. * * WHY THE UI NEEDS THIS. `rename_physical_column` refuses unless the entity's * table appears in `schema_builder_physical_tables`, and that allowlist ships * EMPTY by design — it is the fix for a privilege escalation where the ALTER * TABLE target came from caller-controlled `tenant_entities.name`. Measured on * production 2026-09-01: the allowlist holds 0 rows, and 0 of 45 `tenant_entities` * resolve to a real `public.` table at all. * * So the "also rename the database column" checkbox was being offered on every * field of every entity while being unable to succeed for any of them — an inert * control that charged the user a confirm step to reach a refusal. When this * returns 0, the caller hides the disclosure and the rename takes the metadata * path, which is what the user wanted and what works. * * Counted per TENANT rather than per entity because the allowlist is keyed on * `(table_name, tenant_id)` with no entity column — there is no cheap client-side * entity -> table mapping, and the RPC still enforces the per-entity check. Zero * is the answer that matters: it proves no entity can succeed. */ export declare function countPhysicalTableRegistrations(client: LooseSupabaseClient, tenantId: string): Promise; export declare function renamePhysicalColumn(client: LooseSupabaseClient, entityId: string, fieldId: string, newName: string, opts: { dryRun: boolean; }): Promise; export interface TenantSchemaLayoutRow { id: string; tenant_id: string; user_id: string | null; entity_id: string; app_scope: string; pos_x: number; pos_y: number; } /** * Every layout row visible to the caller for this tenant/scope — both the * tenant defaults (`user_id IS NULL`) and the caller's own overrides. RLS does * the filtering; resolution between the two happens in `resolveLayout`. */ export declare function getSchemaLayout(client: LooseSupabaseClient, tenantId: string | null, appScope?: AppScope): Promise; /** * Collapse the rows into one position per entity. A personal override beats the * tenant default; that ordering is the whole point of allowing both. */ export declare function resolveLayout(rows: TenantSchemaLayoutRow[], userId: string | null): Map; /** * Persist one entity position. Goes through the `save_schema_layout_position` * RPC rather than a bare upsert so the "personal override vs tenant default" * decision is made in exactly one place — three consumer apps render this * canvas and would each re-derive it slightly differently. */ export declare function saveSchemaLayoutPosition(client: LooseSupabaseClient, args: { tenantId: string; entityId: string; x: number; y: number; personal?: boolean; appScope?: AppScope; }): Promise; /** * Developer-only rename of a PLATFORM-owned entity. Every other entity edit * goes through `updateSchemaEntity`; this exists because platform rows are * deliberately unwritable through ordinary RLS, and the RPC is the single * audited exception. It refuses tenant-owned entities server-side. */ export declare function updatePlatformEntityLabel(client: LooseSupabaseClient, entityId: string, label: string, description?: string | null): Promise; //# sourceMappingURL=service.d.ts.map