@rebasepro/server-postgres 0.16.0 → 0.16.1-canary.g0d7af95
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/PostgresAdapter.d.ts +1 -1
- package/dist/PostgresBackendDriver.d.ts +75 -12
- package/dist/PostgresBootstrapper.d.ts +6 -6
- package/dist/auth/services.d.ts +1 -1
- package/dist/backup/backup-cron.d.ts +1 -1
- package/dist/backup/backup-service.d.ts +2 -2
- package/dist/backup/index.d.ts +4 -4
- package/dist/{backup-service-BZoixhVl.js → backup-service-BtgHxfFm.js} +5 -4
- package/dist/{backup-service-BZoixhVl.js.map → backup-service-BtgHxfFm.js.map} +1 -1
- package/dist/cli-helpers.d.ts +41 -0
- package/dist/{auth-users-columns-CgyPWQ18.js → collection-index-DxJBvVTH.js} +486 -503
- package/dist/collection-index-DxJBvVTH.js.map +1 -0
- package/dist/collections/PostgresCollectionRegistry.d.ts +1 -1
- package/dist/collections/buildRegistry.d.ts +1 -1
- package/dist/collections/validate-relations.d.ts +1 -1
- package/dist/{connection-BuZ97wsr.js → connection-GOKU3Hu5.js} +34 -7
- package/dist/connection-GOKU3Hu5.js.map +1 -0
- package/dist/connection.d.ts +16 -0
- package/dist/data-transformer.d.ts +1 -1
- package/dist/{ensure-collection-policies-BVFb2olB.js → ensure-collection-policies-DFpOl8SM.js} +4 -4
- package/dist/{ensure-collection-policies-BVFb2olB.js.map → ensure-collection-policies-DFpOl8SM.js.map} +1 -1
- package/dist/ensure-collection-tables-DMjOkeRy.js +1952 -0
- package/dist/ensure-collection-tables-DMjOkeRy.js.map +1 -0
- package/dist/index.d.ts +16 -16
- package/dist/index.es.js +19 -7128
- package/dist/index.es.js.map +1 -1
- package/dist/{rls-bootstrap-sql-B5Sajku6.js → rls-bootstrap-sql-DNzaWd4C.js} +3 -3
- package/dist/{rls-bootstrap-sql-B5Sajku6.js.map → rls-bootstrap-sql-DNzaWd4C.js.map} +1 -1
- package/dist/{rls-enforcement-Ch0T6OwW.js → rls-enforcement-CInuYj1-.js} +14 -4
- package/dist/rls-enforcement-CInuYj1-.js.map +1 -0
- package/dist/schema/classify-change.d.ts +82 -0
- package/dist/schema/collection-index.d.ts +182 -0
- package/dist/schema/dynamic-tables.d.ts +1 -1
- package/dist/schema/ensure-collection-policies.d.ts +1 -1
- package/dist/schema/ensure-collection-tables.d.ts +93 -2
- package/dist/schema/generate-schema-commit.d.ts +136 -0
- package/dist/schema/generated-schema-staleness.d.ts +19 -0
- package/dist/schema/introspect-db-constraints.d.ts +1 -1
- package/dist/schema/introspect-db-logic.d.ts +3 -3
- package/dist/schema/introspect-db-project.d.ts +1 -1
- package/dist/schema/introspect-db-queries.d.ts +1 -1
- package/dist/schema/introspect-db-structure.d.ts +2 -2
- package/dist/schema/introspect-runtime.d.ts +1 -1
- package/dist/schema/vector-index.d.ts +88 -0
- package/dist/services/BranchService.d.ts +2 -2
- package/dist/services/FetchService.d.ts +4 -4
- package/dist/services/PersistService.d.ts +5 -5
- package/dist/services/RelationService.d.ts +3 -3
- package/dist/services/RelationWriteService.d.ts +3 -3
- package/dist/services/cdc/junction-tables.d.ts +1 -1
- package/dist/services/cdc/trigger-cdc.d.ts +1 -1
- package/dist/services/channel-bus/PostgresChannelBus.d.ts +1 -1
- package/dist/services/channel-bus/index.d.ts +2 -2
- package/dist/services/collection-helpers.d.ts +1 -1
- package/dist/services/dataService.d.ts +10 -10
- package/dist/services/index.d.ts +4 -4
- package/dist/services/junction-writes.d.ts +2 -2
- package/dist/services/nested-path.d.ts +1 -1
- package/dist/services/realtimeService.d.ts +3 -3
- package/dist/services/row-pipeline.d.ts +1 -1
- package/dist/services/write-denial.d.ts +1 -1
- package/dist/{src-BBFsDaeA.js → src-DiDgtX8P.js} +97 -1
- package/dist/src-DiDgtX8P.js.map +1 -0
- package/dist/utils/drizzle-conditions.d.ts +2 -2
- package/dist/websocket-CUnpSe8v.js +8188 -0
- package/dist/websocket-CUnpSe8v.js.map +1 -0
- package/dist/websocket.d.ts +29 -2
- package/package.json +7 -7
- package/src/PostgresBackendDriver.ts +190 -59
- package/src/backup/backup-service.ts +1 -1
- package/src/cli-helpers.ts +117 -2
- package/src/cli.ts +22 -0
- package/src/connection.ts +37 -3
- package/src/databasePoolManager.ts +5 -2
- package/src/schema/classify-change.ts +436 -0
- package/src/schema/collection-index.ts +427 -0
- package/src/schema/ensure-collection-tables.test.ts +168 -1
- package/src/schema/ensure-collection-tables.ts +365 -14
- package/src/schema/generate-drizzle-schema-logic.ts +23 -11
- package/src/schema/generate-drizzle-schema.ts +13 -2
- package/src/schema/generate-postgres-ddl-logic.ts +33 -6
- package/src/schema/generate-postgres-ddl.ts +13 -2
- package/src/schema/generate-schema-commit.ts +242 -0
- package/src/schema/generated-schema-staleness.ts +114 -1
- package/src/schema/vector-index.ts +278 -0
- package/src/services/collection-helpers.ts +3 -2
- package/src/websocket.ts +47 -3
- package/dist/auth-users-columns-CgyPWQ18.js.map +0 -1
- package/dist/connection-BuZ97wsr.js.map +0 -1
- package/dist/data_driver-ULAyJEi9.js +0 -193
- package/dist/data_driver-ULAyJEi9.js.map +0 -1
- package/dist/ensure-collection-tables-BY1pHRD_.js +0 -840
- package/dist/ensure-collection-tables-BY1pHRD_.js.map +0 -1
- package/dist/rls-enforcement-Ch0T6OwW.js.map +0 -1
- package/dist/src-BBFsDaeA.js.map +0 -1
- package/dist/utils/table-classification.d.ts +0 -8
- package/dist/websocket-BVgDVO-V.js +0 -614
- package/dist/websocket-BVgDVO-V.js.map +0 -1
- package/src/utils/table-classification.ts +0 -16
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ensure-collection-tables-BY1pHRD_.js","names":[],"sources":["../src/schema/generate-postgres-ddl-logic.ts","../src/schema/ensure-collection-tables.ts"],"sourcesContent":["import { CollectionConfig, NumberProperty, Property, ResolvedRelation, RelationProperty, SecurityOperation, SecurityRule, StringProperty, isPostgresCollectionConfig, DateProperty, ArrayProperty, MapProperty, ReferenceProperty, VectorProperty, BinaryProperty, isManyToMany, type ResolvedManyToMany, type ResolvedBelongsTo } from \"@rebasepro/types\";\nimport { getEnumVarName, getTableName, resolveCollectionRelations, findRelation, securityRuleToConditions, policyToPostgres, getEffectiveSecurityRules, getInjectedSecurityRules, resolveJunctionSpecs, getJunctionSecurityRules, getJunctionCollectionConfig, resolveStringColumnLength, relationalCollections } from \"@rebasepro/common\";\nimport { toSnakeCase, getPolicyNamesForRule, generateForeignKeyName, legacyForeignKeyName, toPostgresIdentifier } from \"@rebasepro/utils\";\nimport { AUTH_USERS_COLUMNS, authUsersColumnDefinition, authUsersColumnSql, isAuthCollection } from \"./auth-users-columns\";\nimport {\n buildSearchColumnSpec,\n searchColumnDefinition,\n fuzzyColumnDefinition,\n searchIndexStatements,\n searchHelperFunctions,\n searchExtensionStatements,\n searchColumnNames,\n searchColumnStamps,\n searchStampGuards,\n searchIndexNames,\n type SearchColumnSpec\n} from \"./search-column\";\nimport { REBASE_SCHEMA } from \"@rebasepro/types\";\n\n// --- Helper Functions ---\n\nexport const resolveColumnName = (propName: string, prop?: Property | null): string => {\n if (prop && \"columnName\" in prop && typeof prop.columnName === \"string\") {\n return prop.columnName;\n }\n return toSnakeCase(propName);\n};\n\nexport const getPrimaryKeyProp = (collection: CollectionConfig): { name: string, type: \"string\" | \"number\", isUuid: boolean } => {\n if (collection.properties) {\n const idPropEntry = Object.entries(collection.properties).find(([_, prop]) => \"isId\" in (prop as unknown as object) && Boolean((prop as unknown as Record<string, unknown>).isId));\n if (idPropEntry) {\n const prop = idPropEntry[1] as unknown as Property;\n const isUuid = prop.type === \"string\" && \"isId\" in prop && (prop as unknown as StringProperty).isId === \"uuid\";\n return { name: idPropEntry[0], type: prop.type === \"number\" ? \"number\" : \"string\", isUuid };\n }\n }\n const idProp = collection.properties?.[\"id\"] as unknown as Property | undefined;\n if (idProp?.type === \"number\") {\n return { name: \"id\", type: \"number\", isUuid: false };\n }\n const isUuid = idProp?.type === \"string\" && \"isId\" in idProp && (idProp as unknown as StringProperty).isId === \"uuid\";\n return { name: \"id\", type: \"string\", isUuid: isUuid ?? false };\n};\n\nexport const isNumericId = (collection: CollectionConfig): boolean => {\n return getPrimaryKeyProp(collection).type === \"number\";\n};\n\nexport const getPrimaryKeyName = (collection: CollectionConfig): string => {\n return getPrimaryKeyProp(collection).name;\n};\n\n/** The column type a junction holds for one endpoint's primary key. */\nconst junctionKeyType = (collection: CollectionConfig): string =>\n isNumericId(collection) ? \"INTEGER\" : (getPrimaryKeyProp(collection).isUuid ? \"UUID\" : \"TEXT\");\n\nexport const isIdProperty = (propName: string, prop: Property, collection: CollectionConfig): boolean => {\n if (\"isId\" in prop && Boolean(prop.isId)) return true;\n const hasExplicitId = Object.values(collection.properties ?? {}).some(p => \"isId\" in (p as unknown as object) && Boolean((p as unknown as Record<string, unknown>).isId));\n return !hasExplicitId && propName === \"id\";\n};\n\n\ntype ResolveCollection = (slug: string) => CollectionConfig | undefined;\n\n/**\n * Render statements produced by {@link generatePolicyStatements} back into the\n * exact string the DDL/policies files have always carried: each statement on\n * its own line, terminated by a newline. Keeping the string form derived from\n * the statement array means the two can never drift — the boot-time applier and\n * the generated `policies.sql` emit the same SQL, from the same source.\n */\nconst statementsToDdl = (statements: string[]): string => statements.map(s => `${s}\\n`).join(\"\");\n\nconst generatePolicyDdl = (collection: CollectionConfig, rule: SecurityRule, resolveCollection: ResolveCollection): string =>\n statementsToDdl(generatePolicyStatements(collection, rule, resolveCollection));\n\n/**\n * The individual SQL statements a single security rule compiles to: a\n * `DROP POLICY IF EXISTS` / `CREATE POLICY` pair per operation, each a complete\n * statement (terminated by `;`, no trailing newline).\n *\n * This is the primitive the boot-time RLS applier runs one statement at a time\n * (the runtime's DB handle speaks the extended query protocol, which forbids\n * multiple commands in one execute), while `db push` writes the joined string.\n */\nexport const generatePolicyStatements = (collection: CollectionConfig, rule: SecurityRule, resolveCollection: ResolveCollection): string[] => {\n const tableName = getTableName(collection);\n const ops: readonly SecurityOperation[] = rule.operations && rule.operations.length > 0\n ? rule.operations\n : [rule.operation ?? \"all\"];\n\n const policyNames = getPolicyNamesForRule(rule, tableName);\n\n return ops.flatMap((op, opIdx) => {\n return generateSinglePolicyStatements(collection, rule, op, policyNames[opIdx], resolveCollection);\n });\n};\n\nconst generateSinglePolicyStatements = (collection: CollectionConfig, rule: SecurityRule, operation: SecurityOperation, policyName: string, resolveCollection: ResolveCollection): string[] => {\n const schema = isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : \"public\";\n const tableName = getTableName(collection);\n const mode = (rule.mode ?? \"permissive\").toUpperCase();\n const operationUpper = operation.toUpperCase();\n const pgRoles = rule.pgRoles ? [...rule.pgRoles].sort() : [\"public\"];\n\n const needsUsing = operation !== \"insert\";\n const needsWithCheck = operation !== \"select\" && operation !== \"delete\";\n\n // Desugar the rule (access / ownerField / roles / structured condition / raw\n // SQL) into the shared PolicyExpression model, then compile to SQL. This is\n // the same normalization the client-side evaluator uses, so DDL and UI agree.\n const { usingExpr, withCheckExpr } = securityRuleToConditions(rule);\n\n let usingClause = needsUsing && usingExpr ? policyToPostgres(usingExpr, collection, { resolveCollection }) : null;\n let withCheckClause = needsWithCheck && withCheckExpr ? policyToPostgres(withCheckExpr, collection, { resolveCollection }) : null;\n\n if (!usingClause && needsUsing) {\n usingClause = \"false\";\n }\n if (!withCheckClause && needsWithCheck) {\n withCheckClause = \"false\";\n }\n\n const drop = `DROP POLICY IF EXISTS \"${policyName}\" ON \"${schema}\".\"${tableName}\";`;\n let create = `CREATE POLICY \"${policyName}\" ON \"${schema}\".\"${tableName}\" AS ${mode} FOR ${operationUpper} TO ${pgRoles.map(r => `\"${r}\"`).join(\", \")}`;\n if (usingClause) create += ` USING (${usingClause})`;\n if (withCheckClause) create += ` WITH CHECK (${withCheckClause})`;\n create += \";\";\n return [drop, create];\n};\n\n/**\n * Single-quote escaping for a SQL string literal (PostgreSQL doubles the\n * quote). Enum labels come straight from user-authored collection config, so a\n * label like `it's` closes the literal early and the whole generated file stops\n * parsing at the `CREATE TYPE`. Lives here rather than next to its other caller\n * because ensure-collection-tables already imports from this module — the\n * reverse would be a cycle.\n */\nexport const quoteSqlLiteral = (value: string): string => `'${value.replace(/'/g, \"''\")}'`;\n\nexport const getSqlColumnType = (propName: string, prop: Property, collection: CollectionConfig, collections: CollectionConfig[]): string => {\n switch (prop.type) {\n case \"string\": {\n const stringProp = prop as StringProperty;\n if (stringProp.enum) {\n const tableName = getTableName(collection);\n const colName = resolveColumnName(propName, prop);\n const schema = isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : \"public\";\n return `\"${schema}\".\"${tableName}_${colName}\"`;\n }\n if (stringProp.isId === \"uuid\" || stringProp.columnType === \"uuid\") {\n return \"UUID\";\n }\n // Width comes from `validation.max` when the property states one.\n // It used to be a hardcoded 255 here and *absent* on the Drizzle\n // path, so the same property produced a bounded column down one\n // generator and an unbounded one down the other.\n if (stringProp.columnType === \"char\") {\n return `CHAR(${resolveStringColumnLength(stringProp)})`;\n }\n if (stringProp.columnType === \"varchar\") {\n return `VARCHAR(${resolveStringColumnLength(stringProp)})`;\n }\n // `text` is the default. The two generators disagreed here before:\n // this one emitted VARCHAR(255) while the drizzle path emitted a bare\n // `varchar()`, which Postgres treats as unbounded — so the same\n // property produced a capped column down one path and an uncapped one\n // down the other.\n return \"TEXT\";\n }\n case \"number\": {\n const numProp = prop as NumberProperty;\n const isId = isIdProperty(propName, prop, collection);\n if (\"isId\" in numProp && numProp.isId === \"increment\") {\n return \"INTEGER GENERATED BY DEFAULT AS IDENTITY\";\n }\n if (numProp.columnType) {\n if (numProp.columnType === \"double precision\") return \"DOUBLE PRECISION\";\n return numProp.columnType.toUpperCase();\n }\n return (numProp.validation?.integer || isId) ? \"INTEGER\" : \"NUMERIC\";\n }\n case \"boolean\":\n return \"BOOLEAN\";\n case \"date\": {\n const dateProp = prop as DateProperty;\n if (dateProp.columnType === \"date\") return \"DATE\";\n if (dateProp.columnType === \"time\") return \"TIME\";\n return \"TIMESTAMP WITH TIME ZONE\";\n }\n case \"map\": {\n const mapProp = prop as MapProperty;\n return mapProp.columnType === \"json\" ? \"JSON\" : \"JSONB\";\n }\n // `{ latitude, longitude }` — a document, like `map`. It used to fall to\n // the `TEXT` default below while the Drizzle generator emitted no column\n // at all, which is the divergence that made the type unpersistable.\n case \"geopoint\":\n return \"JSONB\";\n case \"array\": {\n const arrayProp = prop as ArrayProperty;\n let colType = arrayProp.columnType;\n if (!colType && arrayProp.of && !Array.isArray(arrayProp.of)) {\n const ofProp = arrayProp.of as Property;\n if (ofProp.type === \"string\") {\n colType = \"text[]\";\n } else if (ofProp.type === \"number\") {\n colType = ofProp.validation?.integer ? \"integer[]\" : \"numeric[]\";\n } else if (ofProp.type === \"boolean\") {\n colType = \"boolean[]\";\n }\n }\n if (colType === \"json\") return \"JSON\";\n if (colType === \"text[]\") return \"TEXT[]\";\n if (colType === \"integer[]\") return \"INTEGER[]\";\n if (colType === \"boolean[]\") return \"BOOLEAN[]\";\n if (colType === \"numeric[]\") return \"NUMERIC[]\";\n return \"JSONB\";\n }\n case \"vector\": {\n const vp = prop as VectorProperty;\n return `VECTOR(${vp.dimensions})`;\n }\n case \"binary\": {\n return \"BYTEA\";\n }\n case \"relation\": {\n const refProp = prop as RelationProperty;\n const resolvedRelations = resolveCollectionRelations(collection);\n const relation = findRelation(resolvedRelations, refProp.relation?.relationName ?? propName);\n if (relation?.kind !== \"belongsTo\") {\n throw new Error(`Relation ${propName} does not put a column on this table (only \\`belongsTo\\` does)`);\n }\n let targetCollection: CollectionConfig;\n try {\n targetCollection = relation.target();\n } catch {\n return \"TEXT\";\n }\n const pkProp = getPrimaryKeyProp(targetCollection);\n return pkProp.type === \"number\" ? \"INTEGER\" : (pkProp.isUuid ? \"UUID\" : \"TEXT\");\n }\n case \"reference\": {\n const refProp = prop as ReferenceProperty;\n const targetCollection = collections.find(c => c.slug === refProp.path || getTableName(c) === refProp.path);\n if (!targetCollection) return \"TEXT\";\n const pkProp = getPrimaryKeyProp(targetCollection);\n return pkProp.type === \"number\" ? \"INTEGER\" : (pkProp.isUuid ? \"UUID\" : \"TEXT\");\n }\n default:\n // A silent `TEXT` here is how the two generators drifted apart: the\n // database got a column for `geopoint` and the Drizzle table did\n // not, so the type looked supported and persisted nothing. Every\n // member of `DataType` is handled above; anything else is a\n // generator that has not been taught the type yet, and it should\n // say so rather than invent a column.\n throw new Error(\n `No Postgres column type for property '${propName}' of type ` +\n `'${(prop as Property).type}' in collection '${collection.slug}'. ` +\n \"Add a case to `getSqlColumnType` (and to `getDrizzleColumn`, which must agree).\"\n );\n }\n};\n\n/**\n * Everything a `search` block needs, as a file Rebase applies itself.\n *\n * Search is the one part of the schema Atlas does not own. Two independent\n * reasons, and either alone would be enough:\n *\n * 1. Its free tier refuses to *parse* a desired-state file that so much as\n * contains a function — \"functions and procedures are available to\n * logged-in users only\". A generated `tsvector` column cannot avoid one:\n * `unaccent` is STABLE and jsonb flattening needs a set-returning function,\n * so both have to be wrapped in an IMMUTABLE helper to be legal in a\n * generated column at all.\n * 2. Even with the file accepted, Atlas *wipes* the dev database it diffs\n * against, so a helper seeded there beforehand is gone by the time the plan\n * is analysed. There is no hook to reinstate it.\n *\n * So the column, its index and its helpers are excluded from Atlas's view\n * (`searchExcludePatterns`) and applied from here — the same arrangement the\n * RLS policies already use, and for the same underlying reason.\n *\n * Ordered as it must run: extensions, then helpers, then the column whose\n * expression calls them, then the index over that column. Every statement is\n * `IF NOT EXISTS` / `OR REPLACE`, because this is replayed on every push and\n * appended to migrations that run against databases at any stage of their\n * life. Empty when nothing opted in — the caller writes no file then.\n *\n * The leading half — extensions and helpers, without the table-shaped\n * statements — is available on its own as {@link searchPrerequisiteStatements},\n * for the dev database Atlas analyses plans against. That database has none of\n * the project's tables, so it wants the functions and nothing else.\n */\nexport const searchPrerequisiteStatements = (allCollections: CollectionConfig[]): string[] => {\n const specs = relationalCollections(allCollections)\n .map(c => buildSearchColumnSpec(c))\n .filter((s): s is SearchColumnSpec => s !== undefined);\n if (specs.length === 0) return [];\n return [\n ...new Set(specs.flatMap(searchExtensionStatements)),\n ...new Set(specs.flatMap(searchHelperFunctions))\n ];\n};\n\nexport const generatePostgresSearchDdl = (allCollections: CollectionConfig[]): string => {\n const collections = relationalCollections(allCollections);\n const specs = collections\n .map(c => buildSearchColumnSpec(c))\n .filter((s): s is SearchColumnSpec => s !== undefined);\n if (specs.length === 0) return \"\";\n\n const extensions = Array.from(new Set(specs.flatMap(searchExtensionStatements)));\n const helpers = Array.from(new Set(specs.flatMap(searchHelperFunctions)));\n\n let ddl = \"-- This file is auto-generated by the Rebase DDL generator. Do not edit manually.\\n\";\n ddl += \"--\\n\";\n ddl += \"-- Full-text search for the collections declaring a `search` block.\\n\";\n ddl += \"-- Applied by Rebase, not by Atlas — see generatePostgresSearchDdl.\\n\\n\";\n\n extensions.forEach(s => { ddl += `${s}\\n`; });\n if (extensions.length > 0) ddl += \"\\n\";\n helpers.forEach(s => { ddl += `${s}\\n\\n`; });\n\n for (const collection of collections) {\n const spec = buildSearchColumnSpec(collection);\n if (!spec) continue;\n const schema = isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : \"public\";\n const table = `\"${schema}\".\"${getTableName(collection)}\"`;\n\n // ADD COLUMN IF NOT EXISTS rather than the inline definition the\n // CREATE TABLE would use: by the time this runs the table exists,\n // whether Atlas just created it or it has been live for a year.\n // Refuse before altering anything if the column in the database was\n // generated from a different `search` block: `ADD COLUMN IF NOT EXISTS`\n // is a no-op against an existing column, so without this the file would\n // report success and leave the old expression in place — and then stamp\n // it as current.\n searchStampGuards(spec).forEach(s => { ddl += `${s}\\n`; });\n ddl += `ALTER TABLE ${table} ADD COLUMN IF NOT EXISTS ${searchColumnDefinition(spec)};\\n`;\n const fuzzyDef = fuzzyColumnDefinition(spec);\n if (fuzzyDef) ddl += `ALTER TABLE ${table} ADD COLUMN IF NOT EXISTS ${fuzzyDef};\\n`;\n // The fingerprint of the expression each column was built from — the\n // only record of *which* block a generated column came from, and what\n // the guard above and the boot-time ensure both compare against.\n searchColumnStamps(spec).forEach(s => { ddl += `${s.sql}\\n`; });\n searchIndexStatements(spec).forEach(s => { ddl += `${s}\\n`; });\n ddl += \"\\n\";\n }\n\n return ddl;\n};\n\n/**\n * Glob patterns telling Atlas to leave the search column and its index alone.\n *\n * Without these, a desired state that omits search reads to Atlas as an\n * instruction to drop the column — taking the index and the whole search\n * feature with it on the next push.\n *\n * Fully qualified, `schema.table.object`, matching the include list. The\n * two-part form is what Atlas wants when the connection URL scopes it to one\n * schema and is *silently ignored* otherwise: it reads `posts.search_vector`\n * as a table named `search_vector` in a schema named `posts`, matches nothing,\n * and reports no error for the pattern that never fired.\n */\nexport const searchExcludePatterns = (allCollections: CollectionConfig[]): string[] => {\n const patterns: string[] = [];\n for (const collection of relationalCollections(allCollections)) {\n const spec = buildSearchColumnSpec(collection);\n if (!spec) continue;\n const schema = isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : \"public\";\n const table = getTableName(collection);\n for (const name of searchColumnNames(collection)) {\n patterns.push(`${schema}.${table}.${name}`);\n }\n for (const index of searchIndexNames(spec)) {\n patterns.push(`${schema}.${table}.${index}`);\n }\n }\n return patterns;\n};\n\nexport const generatePostgresDdl = async (\n allCollections: CollectionConfig[],\n options: { includePolicies?: boolean; includeSearch?: boolean } = {\n includePolicies: true,\n includeSearch: true\n }\n): Promise<string> => {\n // Only the collections this engine stores. See `relationalCollections`.\n const collections = relationalCollections(allCollections);\n let ddl = \"-- This file is auto-generated by the Rebase DDL generator. Do not edit manually.\\n\\n\";\n\n // 1. Create custom schemas.\n //\n // `rebase` is unconditional and load-bearing. The RLS helper functions live\n // in it, so the migration preamble creates it — which puts it in Atlas's\n // replayed state, and anything in that state but absent from this desired\n // schema gets a `DROP SCHEMA … CASCADE` planned against it. That would take\n // the auth tables. It used to appear here only when some collection\n // happened to declare `schema: \"rebase\"`; the scaffold's users collection\n // does, which is why nobody hit it, but nothing guaranteed it.\n const uniqueSchemas = Array.from(new Set([\n REBASE_SCHEMA,\n ...collections.map(c => isPostgresCollectionConfig(c) ? c.schema : undefined).filter(Boolean)\n ]));\n uniqueSchemas.forEach(schema => {\n if (schema) ddl += `CREATE SCHEMA IF NOT EXISTS \"${schema}\";\\n`;\n });\n if (uniqueSchemas.length > 0) ddl += \"\\n\";\n\n // 1b. Search support, for collections that opted in.\n //\n // Extensions and helper functions come before every CREATE TABLE because a\n // generated column's expression is resolved at creation time: a table whose\n // search column calls `rebase_search_text` cannot be created before that\n // function exists. Both are `IF NOT EXISTS` / `OR REPLACE`, so a file\n // replayed against a live database is a no-op here.\n const searchSpecs = collections\n .map(c => buildSearchColumnSpec(c))\n .filter((s): s is SearchColumnSpec => s !== undefined);\n\n if (searchSpecs.length > 0) {\n if (options.includeSearch === false) {\n // Everything search-related lives in `search.sql` — see\n // `generatePostgresSearchDdl` for why Atlas is not shown any of it.\n ddl += \"-- Full-text search support lives in `search.sql`, applied separately.\\n\\n\";\n } else {\n const extensions = Array.from(new Set(searchSpecs.flatMap(searchExtensionStatements)));\n const helpers = Array.from(new Set(searchSpecs.flatMap(searchHelperFunctions)));\n ddl += \"-- Full-text search support (collections declaring a `search` block)\\n\";\n extensions.forEach(s => { ddl += `${s}\\n`; });\n if (extensions.length > 0) ddl += \"\\n\";\n helpers.forEach(s => { ddl += `${s}\\n\\n`; });\n }\n }\n\n // 2. Generate Enums\n //\n // The enum type name is derived from table + column, so two collections\n // mapped onto the same table — or two properties whose `columnName`\n // resolves to the same column — land on the same name. `CREATE TYPE` has no\n // IF NOT EXISTS, so emitting it twice aborts the whole file on the second\n // statement. Dedupe by name, matching planCollectionSchemaEnsure.\n const emittedEnums = new Set<string>();\n collections.forEach(collection => {\n const collectionTable = getTableName(collection);\n const schema = isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : \"public\";\n Object.entries(collection.properties ?? {}).forEach(([propName, prop]) => {\n if ((\"enum\" in prop) && (prop.type === \"string\" || prop.type === \"number\") && prop.enum) {\n const enumDbName = `${collectionTable}_${resolveColumnName(propName, prop)}`;\n const values = Array.isArray(prop.enum)\n ? (prop.enum as (string | number | { id: string | number })[]).map((v: string | number | { id: string | number }) =>\n String(typeof v === \"object\" && v !== null && \"id\" in v ? v.id : v)\n )\n : Object.keys(prop.enum);\n if (values.length > 0 && !emittedEnums.has(`${schema}.${enumDbName}`)) {\n emittedEnums.add(`${schema}.${enumDbName}`);\n ddl += `CREATE TYPE \"${schema}\".\"${enumDbName}\" AS ENUM (${values.map(quoteSqlLiteral).join(\", \")});\\n`;\n }\n }\n });\n });\n if (ddl.endsWith(\";\\n\")) ddl += \"\\n\";\n\n // Junction policy derivation needs every declaring side of each junction,\n // not just the first relation that reached it in the walk below.\n const junctionSpecs = resolveJunctionSpecs(collections);\n\n const allTablesToGenerate = new Map<string, {\n collection: CollectionConfig,\n isJunction?: boolean,\n relation?: ResolvedRelation,\n sourceCollection?: CollectionConfig\n }>();\n\n // Identify all tables\n for (const collection of collections) {\n const tableName = getTableName(collection);\n if (tableName) {\n allTablesToGenerate.set(tableName, { collection });\n }\n\n const resolvedRelations = resolveCollectionRelations(collection);\n for (const relation of Object.values(resolvedRelations)) {\n if (isManyToMany(relation)) {\n const junctionTableName = relation.through.table;\n if (!allTablesToGenerate.has(junctionTableName)) {\n allTablesToGenerate.set(junctionTableName, {\n collection: {\n table: junctionTableName,\n properties: {}\n } as CollectionConfig,\n isJunction: true,\n relation: relation,\n sourceCollection: collection\n });\n }\n }\n }\n }\n\n // 3. Generate tables\n const fkStatements: string[] = [];\n // Indexes follow the tables for the same reason the FK constraints do: the\n // table has to exist first.\n const indexStatements: string[] = [];\n // Policies are emitted after every CREATE TABLE, like the FK constraints:\n // a policy may reference other tables (a junction's derived policies always\n // reference both endpoints; `policy.existsIn` references a join table), and\n // CREATE POLICY validates those relations at creation time.\n const policyStatements: string[] = [];\n for (const [tableName, {\n collection,\n isJunction,\n relation,\n sourceCollection\n }] of allTablesToGenerate.entries()) {\n const schema = isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : \"public\";\n const baseTableName = tableName.includes(\".\") ? tableName.split(\".\").pop()! : tableName;\n\n if (isJunction && relation && sourceCollection && isManyToMany(relation)) {\n const targetCollection = relation.target();\n const sourceTable = getTableName(sourceCollection);\n const targetTable = getTableName(targetCollection);\n const sourceSchema = isPostgresCollectionConfig(sourceCollection) && sourceCollection.schema ? sourceCollection.schema : \"public\";\n const targetSchema = isPostgresCollectionConfig(targetCollection) && targetCollection.schema ? targetCollection.schema : \"public\";\n const { sourceColumn, targetColumn } = relation.through;\n\n // TEXT, matching the string default: a junction column has to have the\n // same type as the primary key it references.\n const sourceColType = isNumericId(sourceCollection) ? \"INTEGER\" : (getPrimaryKeyProp(sourceCollection).isUuid ? \"UUID\" : \"TEXT\");\n const targetColType = isNumericId(targetCollection) ? \"INTEGER\" : (getPrimaryKeyProp(targetCollection).isUuid ? \"UUID\" : \"TEXT\");\n const sourceId = getPrimaryKeyName(sourceCollection);\n const targetId = getPrimaryKeyName(targetCollection);\n\n const onDelete = relation.onDelete ?? \"CASCADE\";\n\n ddl += `CREATE TABLE \"${schema}\".\"${baseTableName}\" (\\n`;\n ddl += ` \"${sourceColumn}\" ${sourceColType} NOT NULL,\\n`;\n ddl += ` \"${targetColumn}\" ${targetColType} NOT NULL,\\n`;\n ddl += ` PRIMARY KEY (\"${sourceColumn}\", \"${targetColumn}\")\\n`;\n ddl += `);\\n\\n`;\n\n fkStatements.push(`ALTER TABLE \"${schema}\".\"${baseTableName}\" ADD CONSTRAINT \"${toPostgresIdentifier(`${baseTableName}_${sourceColumn}_fkey`)}\" FOREIGN KEY (\"${sourceColumn}\") REFERENCES \"${sourceSchema}\".\"${sourceTable}\" (\"${sourceId}\") ON DELETE ${onDelete.toUpperCase()};`);\n fkStatements.push(`ALTER TABLE \"${schema}\".\"${baseTableName}\" ADD CONSTRAINT \"${toPostgresIdentifier(`${baseTableName}_${targetColumn}_fkey`)}\" FOREIGN KEY (\"${targetColumn}\") REFERENCES \"${targetSchema}\".\"${targetTable}\" (\"${targetId}\") ON DELETE ${onDelete.toUpperCase()};`);\n\n if (options.includePolicies) {\n // Junction tables are generated tables like any other: locked by\n // default, with derived policies — reads follow the endpoints'\n // visibility, writes follow the declaring side's update rules.\n // Without this they were the one kind of generated table with no\n // RLS at all, readable and writable by every signed-in user.\n ddl += `ALTER TABLE \"${schema}\".\"${baseTableName}\" ENABLE ROW LEVEL SECURITY;\\n`;\n ddl += `\\n`;\n\n const spec = junctionSpecs.get(baseTableName);\n if (spec) {\n const junctionCollection = getJunctionCollectionConfig(spec);\n const resolveCollection: ResolveCollection = (slug) => collections.find(c => c.slug === slug || getTableName(c) === slug);\n getJunctionSecurityRules(spec).forEach((rule: SecurityRule) => {\n policyStatements.push(generatePolicyDdl(junctionCollection, rule, resolveCollection));\n });\n }\n }\n } else if (!isJunction) {\n ddl += `CREATE TABLE \"${schema}\".\"${baseTableName}\" (\\n`;\n const columns: string[] = [];\n\n Object.entries(collection.properties ?? {}).forEach(([propName, prop]) => {\n if (prop.type === \"relation\") {\n const refProp = prop as RelationProperty;\n const resolvedRelations = resolveCollectionRelations(collection);\n const relInfo = findRelation(resolvedRelations, refProp.relation?.relationName ?? propName);\n\n if (relInfo?.kind !== \"belongsTo\") {\n return;\n }\n\n if (collection.properties[relInfo.localKey] && propName !== relInfo.localKey) {\n return;\n }\n\n let targetCollection: CollectionConfig;\n try {\n targetCollection = relInfo.target();\n } catch {\n return;\n }\n\n const targetTable = getTableName(targetCollection);\n const targetSchema = isPostgresCollectionConfig(targetCollection) && targetCollection.schema ? targetCollection.schema : \"public\";\n const targetId = getPrimaryKeyName(targetCollection);\n const fkColType = getSqlColumnType(propName, prop, collection, collections);\n \n const onUpdate = relInfo.onUpdate ? ` ON UPDATE ${relInfo.onUpdate.toUpperCase()}` : \"\";\n const required = prop.validation?.required;\n const onDeleteVal = relInfo.onDelete ?? (required ? \"CASCADE\" : \"SET NULL\");\n \n let colDef = ` \"${relInfo.localKey}\" ${fkColType}`;\n if (required) colDef += \" NOT NULL\";\n columns.push(colDef);\n\n fkStatements.push(`ALTER TABLE \"${schema}\".\"${baseTableName}\" ADD CONSTRAINT \"${toPostgresIdentifier(`${baseTableName}_${relInfo.localKey}_fkey`)}\" FOREIGN KEY (\"${relInfo.localKey}\") REFERENCES \"${targetSchema}\".\"${targetTable}\" (\"${targetId}\") ON DELETE ${onDeleteVal.toUpperCase()}${onUpdate};`);\n } else if (prop.type === \"reference\") {\n const refProp = prop as ReferenceProperty;\n const targetCollection = collections.find(c => c.slug === refProp.path || getTableName(c) === refProp.path);\n const colName = resolveColumnName(propName, prop);\n const colType = getSqlColumnType(propName, prop, collection, collections);\n const required = prop.validation?.required;\n\n if (!targetCollection) {\n let colDef = ` \"${colName}\" ${colType}`;\n if (required) colDef += \" NOT NULL\";\n columns.push(colDef);\n } else {\n const targetTable = getTableName(targetCollection);\n const targetSchema = isPostgresCollectionConfig(targetCollection) && targetCollection.schema ? targetCollection.schema : \"public\";\n const targetId = getPrimaryKeyName(targetCollection);\n const onDelete = required ? \"CASCADE\" : \"SET NULL\";\n\n let colDef = ` \"${colName}\" ${colType}`;\n if (required) colDef += \" NOT NULL\";\n columns.push(colDef);\n\n fkStatements.push(`ALTER TABLE \"${schema}\".\"${baseTableName}\" ADD CONSTRAINT \"${toPostgresIdentifier(`${baseTableName}_${colName}_fkey`)}\" FOREIGN KEY (\"${colName}\") REFERENCES \"${targetSchema}\".\"${targetTable}\" (\"${targetId}\") ON DELETE ${onDelete.toUpperCase()};`);\n }\n } else {\n const colName = resolveColumnName(propName, prop);\n\n // On an auth collection, the columns auth reads and writes\n // are defined once, in `auth-users-columns`, and every\n // creator of this table emits that definition. Before this,\n // the desired state here was built from the collection file\n // alone — which knows nothing of `is_anonymous` or\n // `tokens_valid_after` — so `db push` and the runtime\n // disagreed about `email`'s nullability and about which\n // columns exist at all.\n const authDefinition = isAuthCollection(collection)\n ? authUsersColumnDefinition(colName)\n : undefined;\n if (authDefinition && !isIdProperty(propName, prop, collection)) {\n columns.push(` \"${colName}\" ${authDefinition}`);\n return;\n }\n\n const colType = getSqlColumnType(propName, prop, collection, collections);\n let colDef = ` \"${colName}\" ${colType}`;\n\n if (isIdProperty(propName, prop, collection)) {\n colDef += \" PRIMARY KEY\";\n }\n\n if (\"isId\" in prop && prop.isId !== \"manual\" && prop.isId !== true && prop.isId !== \"increment\") {\n if (prop.isId === \"uuid\") {\n colDef += \" DEFAULT gen_random_uuid()\";\n } else if (prop.isId === \"cuid\") {\n colDef += \" DEFAULT cuid()\";\n } else if (typeof prop.isId === \"string\") {\n colDef += ` DEFAULT ${prop.isId}`;\n }\n }\n\n if (!isIdProperty(propName, prop, collection) && prop.validation?.unique) {\n colDef += \" UNIQUE\";\n }\n\n if (prop.type === \"date\") {\n const dateProp = prop as DateProperty;\n if (dateProp.autoValue === \"on_create\" || dateProp.autoValue === \"on_update\") {\n colDef += \" DEFAULT now()\";\n }\n }\n\n if (prop.validation?.required && !colDef.includes(\"PRIMARY KEY\")) {\n colDef += \" NOT NULL\";\n }\n\n columns.push(colDef);\n }\n });\n\n // ── Auth columns the collection file never mentions ──────────────\n // `db push` is declarative: Atlas diffs the database against exactly\n // what is emitted here and drops anything else. The scaffold's users\n // collection describes 12 columns while auth needs 14, so\n // `is_anonymous` and `tokens_valid_after` — created at boot by\n // `ensureAuthTablesExist` — read as unmanaged drift, and a push run\n // after the server had started once planned to DROP them. The\n // destructive gate catches it, which makes it a data-loss prompt on\n // the auth table rather than silent damage; either way the desired\n // state was wrong. Emit the full contract so the two agree.\n if (isAuthCollection(collection)) {\n const declared = new Set(\n Object.entries(collection.properties ?? {})\n .map(([name, prop]) => resolveColumnName(name, prop))\n );\n for (const spec of AUTH_USERS_COLUMNS) {\n if (declared.has(spec.column)) continue;\n columns.push(` \"${spec.column}\" ${authUsersColumnSql(spec)}`);\n }\n }\n\n // ── The opt-in search column ─────────────────────────────────────\n // Last, so it reads as what it is: derived from the columns above\n // it. Postgres recomputes it on every write of a source column and\n // rejects any attempt to write it directly, which is the property\n // that makes it impossible for the index to drift from the row.\n const searchSpec = options.includeSearch === false ? undefined : buildSearchColumnSpec(collection);\n if (searchSpec) {\n columns.push(` ${searchColumnDefinition(searchSpec)}`);\n const fuzzyDef = fuzzyColumnDefinition(searchSpec);\n if (fuzzyDef) columns.push(` ${fuzzyDef}`);\n indexStatements.push(...searchIndexStatements(searchSpec));\n }\n\n // Backwards compatibility: add default id primary key if missing\n const hasPk = columns.some(c => c.includes(\"PRIMARY KEY\"));\n if (!hasPk) {\n columns.unshift(' \"id\" TEXT PRIMARY KEY');\n }\n\n ddl += columns.join(\",\\n\");\n ddl += `\\n);\\n\\n`;\n\n if (options.includePolicies) {\n // Enable RLS and add Policies. No FORCE: authenticated requests\n // run as the non-owner `rebase_user` role, which plain ENABLE\n // already binds. The owner (server context) must bypass — it is\n // the trusted plane (auth flows, dataAsAdmin).\n ddl += `ALTER TABLE \"${schema}\".\"${baseTableName}\" ENABLE ROW LEVEL SECURITY;\\n`;\n ddl += `\\n`;\n\n const securityRules = getEffectiveSecurityRules(collection);\n if (securityRules.length > 0) {\n const resolveCollection: ResolveCollection = (slug) => collections.find(c => c.slug === slug || getTableName(c) === slug);\n securityRules.forEach((rule: SecurityRule) => {\n policyStatements.push(generatePolicyDdl(collection, rule, resolveCollection));\n });\n }\n }\n }\n }\n\n if (fkStatements.length > 0) {\n ddl += \"-- Foreign Key Constraints\\n\";\n ddl += fkStatements.join(\"\\n\") + \"\\n\\n\";\n }\n\n if (indexStatements.length > 0) {\n ddl += \"-- Indexes\\n\";\n ddl += indexStatements.join(\"\\n\") + \"\\n\\n\";\n }\n\n if (policyStatements.length > 0) {\n ddl += \"-- Row Level Security Policies\\n\";\n ddl += policyStatements.join(\"\");\n ddl += \"\\n\";\n }\n\n return ddl;\n};\n\n/** The RLS statements one declared collection's table needs, ready to run. */\n/**\n * A foreign key, as both its parts and the statement that creates it.\n *\n * `ALTER TABLE … ADD CONSTRAINT` has no `IF NOT EXISTS`, so a caller applying\n * these has to skip by name — hence the name is a field and not only a substring\n * of the SQL.\n */\nexport interface ForeignKeyPlan {\n constraintName: string;\n schema: string;\n /** Bare table name, no schema prefix. */\n table: string;\n column: string;\n targetSchema: string;\n targetTable: string;\n targetColumn: string;\n sql: string;\n}\n\n/** A column a `relation` or `reference` property owns on its own table. */\nexport interface RelationalColumnPlan {\n schema: string;\n /** Bare table name, no schema prefix. */\n table: string;\n column: string;\n /** Postgres type, exactly as the DDL generator declares it. */\n type: string;\n /** Absent when the target collection is not part of this bundle. */\n foreignKey?: ForeignKeyPlan;\n /**\n * What this column would have been called before `generateForeignKeyName`\n * learned to singularize — set only when the two differ and the column name\n * is the derived default rather than one the author wrote.\n *\n * Carried so the boot-time ensure can notice a database provisioned under\n * the old rule. It is never used to name anything.\n */\n legacyColumn?: string;\n}\n\n/** The table behind a many-to-many `through` relation. */\nexport interface JunctionTablePlan {\n schema: string;\n /** Bare table name, no schema prefix. */\n table: string;\n columns: { name: string; type: string; legacyName?: string }[];\n /** Both endpoint columns plus the composite primary key. */\n createTable: string;\n foreignKeys: ForeignKeyPlan[];\n}\n\nconst schemaOfCollection = (collection: CollectionConfig): string =>\n isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : \"public\";\n\nconst bareTableName = (name: string): string => (name.includes(\".\") ? name.split(\".\").pop()! : name);\n\n/**\n * Truncate a derived identifier the way Postgres does: to 63 bytes, silently.\n *\n * This is not cosmetic, and not really a naming choice at all — it is agreeing\n * with the name the database ALREADY stored. `ADD CONSTRAINT` on a longer name\n * succeeds and records the truncated form, so the untruncated name this used to\n * derive matched nothing in the catalogue. Boot-ensure compares its planned\n * constraints against `readExistingSchema`, which reads catalogue names, so the\n * comparison could never hit: every boot re-issued `ADD CONSTRAINT` for the same\n * constraint, forever, and got \"already exists\" every time. Non-fatal (foreign\n * keys are the one action allowed to fail) and therefore permanent — an error in\n * the log on every restart of a project whose table and column names happened to\n * be long.\n *\n * Byte length, not string length: NAMEDATALEN is 64 bytes, and a multi-byte\n * character straddling the boundary would be cut mid-sequence by `slice(0, 63)`.\n */\n// Moved to `@rebasepro/utils` so the search-column builder derives index names\n// under the same 63-byte rule this file derives constraint names under. Two\n// copies of a truncation rule is two rules the moment one of them is edited.\n\nconst foreignKeyPlan = (\n args: Omit<ForeignKeyPlan, \"constraintName\" | \"sql\"> & { onDelete: string; onUpdate?: string }\n): ForeignKeyPlan => {\n const constraintName = toPostgresIdentifier(`${args.table}_${args.column}_fkey`);\n const onUpdate = args.onUpdate ? ` ON UPDATE ${args.onUpdate.toUpperCase()}` : \"\";\n return {\n constraintName,\n schema: args.schema,\n table: args.table,\n column: args.column,\n targetSchema: args.targetSchema,\n targetTable: args.targetTable,\n targetColumn: args.targetColumn,\n sql:\n `ALTER TABLE \"${args.schema}\".\"${args.table}\" ADD CONSTRAINT \"${constraintName}\" ` +\n `FOREIGN KEY (\"${args.column}\") REFERENCES \"${args.targetSchema}\".\"${args.targetTable}\" ` +\n `(\"${args.targetColumn}\") ON DELETE ${args.onDelete.toUpperCase()}${onUpdate};`\n };\n};\n\n/**\n * The FK columns the declared collections own — one entry per `relation`\n * (`belongsTo` side) or `reference` property.\n *\n * Split out of {@link generatePostgresDdl} so the boot-time schema ensure can\n * create the same columns with the same names, types and constraints. Before\n * this it skipped them outright, which was survivable only because `db push`\n * always followed; on a managed tenant nothing follows, so a table arrived\n * without the column its own collection reads and wrote 400 on every insert.\n *\n * A relation whose target is not in the bundle yields no column at all (the\n * generator returns early on an unresolvable target); a `reference` whose target\n * is unknown yields the column without a constraint. Both mirror the generator\n * exactly — a divergence here is a schema fork between boot and `db push`.\n */\nexport const planRelationalColumns = (allCollections: CollectionConfig[]): RelationalColumnPlan[] => {\n const collections = relationalCollections(allCollections);\n const plans: RelationalColumnPlan[] = [];\n\n for (const collection of collections) {\n const tableName = getTableName(collection);\n if (!tableName) continue;\n const schema = schemaOfCollection(collection);\n const table = bareTableName(tableName);\n\n for (const [propName, rawProp] of Object.entries(collection.properties ?? {})) {\n const prop = rawProp as Property;\n\n if (prop.type === \"relation\") {\n const refProp = prop as RelationProperty;\n const resolvedRelations = resolveCollectionRelations(collection);\n const relInfo = findRelation(resolvedRelations, refProp.relation?.relationName ?? propName);\n if (relInfo?.kind !== \"belongsTo\") continue;\n // The relation and an explicit FK property can both be declared;\n // the explicit one owns the column.\n if (collection.properties[relInfo.localKey] && propName !== relInfo.localKey) continue;\n\n let targetCollection: CollectionConfig;\n try {\n targetCollection = relInfo.target();\n } catch {\n continue;\n }\n if (!targetCollection) continue;\n\n const required = prop.validation?.required;\n // Only a *derived* column name can be affected by the change to\n // `generateForeignKeyName`; one the author wrote is theirs.\n const relationName = refProp.relation?.relationName ?? propName;\n const legacyKey = legacyForeignKeyName(relationName);\n const derived = relInfo.localKey === generateForeignKeyName(relationName);\n plans.push({\n schema,\n table,\n column: relInfo.localKey,\n legacyColumn: derived && legacyKey !== relInfo.localKey ? legacyKey : undefined,\n type: getSqlColumnType(propName, prop, collection, collections),\n foreignKey: foreignKeyPlan({\n schema,\n table,\n column: relInfo.localKey,\n targetSchema: schemaOfCollection(targetCollection),\n targetTable: bareTableName(getTableName(targetCollection)),\n targetColumn: getPrimaryKeyName(targetCollection),\n onDelete: relInfo.onDelete ?? (required ? \"CASCADE\" : \"SET NULL\"),\n onUpdate: relInfo.onUpdate\n })\n });\n } else if (prop.type === \"reference\") {\n const refProp = prop as ReferenceProperty;\n const targetCollection = collections.find(\n c => c.slug === refProp.path || getTableName(c) === refProp.path\n );\n const column = resolveColumnName(propName, prop);\n const type = getSqlColumnType(propName, prop, collection, collections);\n const required = prop.validation?.required;\n\n plans.push({\n schema,\n table,\n column,\n type,\n foreignKey: targetCollection\n ? foreignKeyPlan({\n schema,\n table,\n column,\n targetSchema: schemaOfCollection(targetCollection),\n targetTable: bareTableName(getTableName(targetCollection)),\n targetColumn: getPrimaryKeyName(targetCollection),\n onDelete: required ? \"CASCADE\" : \"SET NULL\"\n })\n : undefined\n });\n }\n }\n }\n\n return plans;\n};\n\n/**\n * The junction tables a bundle's many-to-many relations imply.\n *\n * Derived from {@link resolveJunctionSpecs}, the same source the junction RLS\n * comes from, so a table created here always has policies planned for it — a\n * junction with row-level security left off is readable and writable by every\n * signed-in user, which is why the two must ship together.\n */\nexport const planJunctionTables = (allCollections: CollectionConfig[]): JunctionTablePlan[] => {\n const collections = relationalCollections(allCollections);\n const plans: JunctionTablePlan[] = [];\n\n for (const spec of resolveJunctionSpecs(collections).values()) {\n const [source, target] = spec.endpoints;\n // A junction column's default name is derived from the endpoint\n // collection's slug — which is normally plural, so this is where the\n // singularization change actually lands: a `categories` endpoint used\n // to give `categorie_id` and now gives `category_id`. Recorded, not\n // used, so boot-ensure can recognise a table built under the old rule.\n const legacyFor = (endpoint: typeof source): string | undefined => {\n const slug = toSnakeCase(endpoint.collection.slug ?? endpoint.collection.name ?? \"\");\n const legacy = legacyForeignKeyName(slug);\n const derived = endpoint.junctionColumn === generateForeignKeyName(slug);\n return derived && legacy !== endpoint.junctionColumn ? legacy : undefined;\n };\n const columns = [\n { name: source.junctionColumn, type: junctionKeyType(source.collection), legacyName: legacyFor(source) },\n { name: target.junctionColumn, type: junctionKeyType(target.collection), legacyName: legacyFor(target) }\n ];\n // Every declaring side agrees on the edge's lifetime; the first one wins,\n // as it does in the generator's walk.\n const onDelete = spec.declaringSides[0]?.relation.onDelete ?? \"CASCADE\";\n\n plans.push({\n schema: spec.schema,\n table: spec.table,\n columns,\n createTable:\n `CREATE TABLE IF NOT EXISTS \"${spec.schema}\".\"${spec.table}\" (` +\n columns.map(c => `\"${c.name}\" ${c.type} NOT NULL`).join(\", \") +\n `, PRIMARY KEY (${columns.map(c => `\"${c.name}\"`).join(\", \")}));`,\n foreignKeys: [source, target].map((endpoint, i) =>\n foreignKeyPlan({\n schema: spec.schema,\n table: spec.table,\n column: columns[i].name,\n targetSchema: schemaOfCollection(endpoint.collection),\n targetTable: bareTableName(getTableName(endpoint.collection)),\n targetColumn: getPrimaryKeyName(endpoint.collection),\n onDelete\n })\n )\n });\n }\n\n return plans;\n};\n\nexport interface CollectionPolicyPlan {\n /** The table's schema (e.g. `public`, `rebase`). */\n schema: string;\n /** The bare table name, no schema prefix. */\n table: string;\n /** `schema.table` — matches the keys `readExistingSchema` returns. */\n qualified: string;\n /** `ALTER TABLE … ENABLE ROW LEVEL SECURITY;` — locked by default. */\n enableRls: string;\n /** `DROP POLICY IF EXISTS` / `CREATE POLICY` statements, in order. */\n policyStatements: string[];\n}\n\n/**\n * The per-table RLS plan for the *declared* collections, as executable\n * statements — what the managed runtime applies at boot so a freshly\n * provisioned tenant database serves data instead of 401ing every read.\n *\n * Mirrors {@link generatePostgresPoliciesDdl} exactly (same\n * `generatePolicyStatements`, same enable-RLS, same effective rules, same\n * derived junction rules), so boot and `db push` produce identical policies from\n * identical collections.\n *\n * Junction tables are included, and have to be: boot creates them now\n * ({@link planJunctionTables}), and a junction with RLS left off is readable and\n * writable by every signed-in user. A junction whose table is still absent is\n * skipped by the applier, not planned away here.\n */\nexport const planCollectionPolicies = (allCollections: CollectionConfig[]): CollectionPolicyPlan[] => {\n // A store with no RLS gets no policies planned for it — `supportsRLS` is\n // false for exactly the engines `relationalCollections` filters out.\n const collections = relationalCollections(allCollections);\n const resolveCollection: ResolveCollection = (slug) => collections.find(c => c.slug === slug || getTableName(c) === slug);\n const plans: CollectionPolicyPlan[] = [];\n const seen = new Set<string>();\n\n for (const collection of collections) {\n const tableName = getTableName(collection);\n if (!tableName) continue;\n const schema = isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : \"public\";\n const baseTableName = tableName.includes(\".\") ? tableName.split(\".\").pop()! : tableName;\n const qualified = `${schema}.${baseTableName}`;\n if (seen.has(qualified)) continue;\n seen.add(qualified);\n\n const policyStatements: string[] = [];\n for (const rule of getEffectiveSecurityRules(collection)) {\n policyStatements.push(...generatePolicyStatements(collection, rule, resolveCollection));\n }\n\n plans.push({\n schema,\n table: baseTableName,\n qualified,\n enableRls: `ALTER TABLE \"${schema}\".\"${baseTableName}\" ENABLE ROW LEVEL SECURITY;`,\n policyStatements\n });\n }\n\n // Junctions are derived from `through` relations rather than declared, so\n // the walk above never sees them.\n for (const spec of resolveJunctionSpecs(collections).values()) {\n const qualified = `${spec.schema}.${spec.table}`;\n if (seen.has(qualified)) continue;\n seen.add(qualified);\n\n const junctionCollection = getJunctionCollectionConfig(spec);\n const policyStatements: string[] = [];\n for (const rule of getJunctionSecurityRules(spec)) {\n policyStatements.push(...generatePolicyStatements(junctionCollection, rule, resolveCollection));\n }\n\n plans.push({\n schema: spec.schema,\n table: spec.table,\n qualified,\n enableRls: `ALTER TABLE \"${spec.schema}\".\"${spec.table}\" ENABLE ROW LEVEL SECURITY;`,\n policyStatements\n });\n }\n\n return plans;\n};\n\nexport const generatePostgresPoliciesDdl = (allCollections: CollectionConfig[]): string => {\n // Also the expectation `checkPolicyDrift` reconciles the database against,\n // so a non-SQL collection filtered out here is one the drift report cannot\n // invent a missing policy for.\n const collections = relationalCollections(allCollections);\n let ddl = \"-- This file contains RLS policies generated by Rebase. Applied separately from migrations.\\n\\n\";\n\n const allTablesToGenerate = new Map<string, {\n collection: CollectionConfig\n }>();\n\n for (const collection of collections) {\n const tableName = getTableName(collection);\n if (tableName) {\n allTablesToGenerate.set(tableName, { collection });\n }\n }\n\n for (const [tableName, { collection }] of allTablesToGenerate.entries()) {\n const schema = isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : \"public\";\n const baseTableName = tableName.includes(\".\") ? tableName.split(\".\").pop()! : tableName;\n\n // No FORCE: user requests run as the non-owner `rebase_user` role\n // (plain ENABLE binds them); the owner is the trusted server context.\n ddl += `ALTER TABLE \"${schema}\".\"${baseTableName}\" ENABLE ROW LEVEL SECURITY;\\n`;\n ddl += `\\n`;\n\n const securityRules = getEffectiveSecurityRules(collection);\n if (securityRules.length > 0) {\n const resolveCollection: ResolveCollection = (slug) => collections.find(c => c.slug === slug || getTableName(c) === slug);\n const injectedNames = new Set(getInjectedSecurityRules(collection).map((rule) => rule.name));\n\n securityRules.forEach((rule: SecurityRule) => {\n // Say which policies the author did not write. They are permissive,\n // so they OR with the declared rules and widen the final ACL beyond\n // what `securityRules` reads like — and re-appear after any manual\n // DROP, because a push asserts the declared state.\n if (rule.name && injectedNames.has(rule.name)) {\n ddl += `-- Injected by Rebase (not from this collection's securityRules).\\n`;\n ddl += `-- Set \\`disableDefaultPolicies: true\\` on \"${collection.slug}\" to drop these and own its RLS outright.\\n`;\n }\n ddl += generatePolicyDdl(collection, rule, resolveCollection);\n });\n ddl += \"\\n\";\n }\n }\n\n // Junction tables are generated from `through` relations, not declared as\n // collections, so the walk above never sees them. They get the same\n // treatment as any generated table: locked by default, with derived\n // policies — reads follow the endpoints, writes follow the declaring\n // side's update rules.\n const junctionSpecs = resolveJunctionSpecs(collections);\n for (const spec of junctionSpecs.values()) {\n ddl += `ALTER TABLE \"${spec.schema}\".\"${spec.table}\" ENABLE ROW LEVEL SECURITY;\\n`;\n ddl += `\\n`;\n\n const junctionRules = getJunctionSecurityRules(spec);\n if (junctionRules.length === 0) continue;\n\n const junctionCollection = getJunctionCollectionConfig(spec);\n const resolveCollection: ResolveCollection = (slug) => collections.find(c => c.slug === slug || getTableName(c) === slug);\n const declaringSlugs = spec.declaringSides.map(s => s.collection.slug).join('\", \"');\n\n ddl += `-- Derived by Rebase for the junction \"${spec.table}\" (no collection declares it).\\n`;\n ddl += `-- Reads require both endpoint rows to be visible; writes follow the update\\n`;\n ddl += `-- rules of \"${declaringSlugs}\". Set \\`disableDefaultPolicies: true\\` on the\\n`;\n ddl += `-- declaring collection(s) to drop these and police the junction yourself.\\n`;\n junctionRules.forEach((rule: SecurityRule) => {\n ddl += generatePolicyDdl(junctionCollection, rule, resolveCollection);\n });\n ddl += \"\\n\";\n }\n\n return ddl;\n};\n\n","/**\n * Bringing a database up to date with a bundle's collections, additively.\n *\n * ## Why this exists\n *\n * A managed runtime boots someone else's compiled project against a database it\n * has never seen. Auth tables are ensured at boot already, but collection tables\n * were not created by anything: the platform ran the app and every `/api/data/*`\n * request answered 500 on a missing relation. `rebase db push` cannot help — it\n * is an Atlas-driven CLI command, and the runtime image ships no CLI.\n *\n * ## Why additive-only, forever\n *\n * This runs unattended, against a database with customers' data in it, with no\n * human reading a diff. So it may only ever do things that cannot lose data:\n * create a missing table, add a missing column, create a missing enum type.\n *\n * It will **never** drop a table or a column, narrow a type, or alter a\n * constraint. A removed field leaves its column behind; a renamed field looks\n * like an addition and the old column stays. That is the correct trade for an\n * automated path — the alternative is an unattended process that can silently\n * destroy a column, which is precisely the failure `db push` was hardened\n * against. Destructive changes stay a deliberate, human-reviewed migration.\n *\n * Because of that, this is safe to run on every boot, and re-running it is a\n * no-op.\n */\nimport { type CollectionConfig, type Property, isPostgresCollectionConfig } from \"@rebasepro/types\";\nimport { getTableName, relationalCollections } from \"@rebasepro/common\";\nimport { logger, isConcurrentDdlRace, isDuplicateObjectRace } from \"@rebasepro/server\";\nimport {\n assertSearchIsPostgresOnly,\n buildSearchColumnSpec,\n searchExtensionStatements,\n searchHelperFunctions,\n searchIndexStatements,\n searchColumnStamps,\n SEARCH_STAMP_PREFIX,\n SEARCH_TEXT_FN,\n SEARCH_UNACCENT_FN,\n type SearchColumnSpec\n} from \"./search-column\";\nimport {\n getSqlColumnType,\n resolveColumnName,\n isIdProperty,\n planRelationalColumns,\n planJunctionTables,\n quoteSqlLiteral\n} from \"./generate-postgres-ddl-logic\";\nimport {\n AUTH_USERS_COLUMNS,\n authUsersColumnDefinition,\n authUsersColumnSql,\n isAuthCollection\n} from \"./auth-users-columns\";\n\n/**\n * The subset of a database handle this needs: run a statement, get rows back.\n *\n * Deliberately parameterless. Everything here is DDL or catalogue reads keyed by\n * schema name, and schema names are identifiers — they cannot be bound as\n * parameters anyway. They are validated against {@link SAFE_IDENTIFIER} before\n * they reach a statement, so a config that somehow carried a quote is refused\n * rather than concatenated.\n */\nexport interface Queryable {\n query<T = unknown>(sql: string): Promise<{ rows: T[] }>;\n}\n\n/** Postgres identifiers this module is willing to interpolate. */\nconst SAFE_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_$]*$/;\n\nfunction assertSafeIdentifier(value: string, what: string): string {\n if (!SAFE_IDENTIFIER.test(value)) {\n throw new Error(`Refusing to build SQL with an unsafe ${what}: ${JSON.stringify(value)}`);\n }\n return value;\n}\n\n/** What the database currently has, as the planner needs it. */\nexport interface ExistingSchema {\n /** `schema.table` → set of column names. */\n tables: Map<string, Set<string>>;\n /** `schema.typename` of every enum type that already exists. */\n enums: Set<string>;\n /**\n * `schema.table.constraint` of every constraint that already exists.\n *\n * Optional so a caller that only cares about tables can still build one by\n * hand; absent is read as \"none known\", which at worst re-attempts a\n * constraint that then fails harmlessly as a duplicate.\n */\n constraints?: Set<string>;\n /**\n * `schema.table.column` → that column's comment, for the columns that have\n * one. This is where a generated search column's fingerprint lives, so it\n * is the only evidence that a `search` block has changed since the column\n * was built. Absent is read as \"no column is stamped\", which plans a stamp\n * and reports nothing as drifted.\n */\n columnComments?: Map<string, string>;\n}\n\nexport interface EnsureAction {\n kind: \"create-enum\" | \"create-table\" | \"add-column\" | \"add-constraint\" | \"rename-column\"\n | \"create-extension\" | \"create-function\" | \"create-index\" | \"comment-column\";\n /** Qualified target, for logging: `public.posts` or `public.posts.title`. */\n target: string;\n sql: string;\n}\n\nexport interface EnsurePlan {\n actions: EnsureAction[];\n /** Every statement, in dependency order. Empty when the schema is current. */\n statements: string[];\n /**\n * Relation columns this plan is about to create where the table already\n * carries the same column under its pre-singularization name.\n *\n * The reason this is reported rather than silently handled: the ensure is\n * additive, so it would add `category_id` beside a populated\n * `categorie_id` and the relation would then read the new, empty one. No\n * statement fails, no table is missing, and the only symptom is relations\n * resolving to nothing — which is indistinguishable from having no data.\n */\n legacyForeignKeys: LegacyForeignKey[];\n /**\n * Generated search columns whose `search` block has changed since they were\n * built. Reported, never planned into `actions` — see\n * {@link SearchColumnDrift} for why applying it is not this path's call.\n */\n searchDrift: SearchColumnDrift[];\n /**\n * Generated search columns that exist but carry no fingerprint — created\n * before this check existed, or by `search.sql` on an older CLI. The plan\n * stamps them so the *next* change is detectable; whether they match the\n * current block cannot be known, which is what the caller reports.\n */\n searchAdopted: { table: string; column: string }[];\n}\n\n/**\n * A generated search column built from a `search` block that has since changed.\n *\n * Reported instead of applied because the two ways to apply it are both worse\n * than stopping. `ALTER COLUMN … SET EXPRESSION` exists only on PG17+ and\n * rewrites the table either way; `DROP COLUMN` + `ADD COLUMN` rewrites it under\n * an ACCESS EXCLUSIVE lock and rebuilds the GIN index. This module runs\n * unattended against live customer data with nobody reading a diff — the same\n * reason it withholds `SET NOT NULL` from an adopted table — so a multi-minute\n * outage is not a decision it may take on its own.\n *\n * Not applying it silently is not an option either: that is the bug this\n * detection exists for. A collection that added a field, flipped `unaccent` or\n * raised a weight kept indexing the *old* set forever, and the only symptom was\n * searches returning nothing for content plainly in the row.\n */\nexport interface SearchColumnDrift {\n /** `schema.table`. */\n table: string;\n column: string;\n /** The fingerprint recorded on the column. */\n found: string;\n /** The fingerprint the current `search` block computes. */\n expected: string;\n /** The statements that would rebuild the column, for the operator to run. */\n rebuild: string[];\n}\n\n/** A relation column whose old and new spellings both plausibly apply. */\nexport interface LegacyForeignKey {\n /** `schema.table`. */\n table: string;\n /** The name the current rule derives, and what this plan would create. */\n expected: string;\n /** The name the old rule derived, which the table already has. */\n legacy: string;\n}\n\nexport interface EnsureOutcome extends EnsurePlan {\n /**\n * Actions that could not be applied and are non-fatal by nature.\n *\n * Two kinds qualify. A foreign key can only fail on data that already\n * violates it, and the column it would police exists either way, so the\n * collection still serves; refusing to boot over one would turn a\n * pre-existing data problem into an outage. A column comment is the search\n * fingerprint, which needs table ownership — losing it costs drift\n * detection on the next boot, not the deployment. Both are reported loudly.\n */\n failures: { kind: EnsureAction[\"kind\"]; target: string; error: string }[];\n}\n\nfunction schemaOf(collection: CollectionConfig): string {\n return isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : \"public\";\n}\n\nfunction qualified(collection: CollectionConfig): string {\n return `${schemaOf(collection)}.${getTableName(collection)}`;\n}\n\n/**\n * Enum types a collection's properties require, as `schema.typename`.\n *\n * Named exactly as the DDL generator names them (`<table>_<column>`), because\n * a column added here has to reference the same type the generator would have\n * created — a second, differently-named type for the same field would be a\n * silent schema fork.\n */\nfunction requiredEnums(collection: CollectionConfig): { name: string; values: string[] }[] {\n const table = getTableName(collection);\n const schema = schemaOf(collection);\n const out: { name: string; values: string[] }[] = [];\n for (const [propName, prop] of Object.entries(collection.properties ?? {})) {\n const p = prop as Property;\n if (!(\"enum\" in p) || !p.enum) continue;\n if (p.type !== \"string\" && p.type !== \"number\") continue;\n const values = (p.enum as unknown[])\n .map(entry =>\n entry && typeof entry === \"object\" && \"id\" in (entry as Record<string, unknown>)\n ? String((entry as Record<string, unknown>).id)\n : String(entry)\n )\n .filter(v => v.length > 0);\n if (values.length === 0) continue;\n out.push({ name: `${schema}.${table}_${resolveColumnName(propName, p)}`, values });\n }\n return out;\n}\n\n/**\n * Decide what to add. Pure — the caller supplies what exists and runs the result.\n *\n * Ordering matters and is deliberate: enum types before the tables and columns\n * that reference them, tables before the columns added to other tables (a new\n * table may be the target of a relation), and nothing is emitted twice.\n */\nexport function planCollectionSchemaEnsure(\n allCollections: CollectionConfig[],\n existing: ExistingSchema\n): EnsurePlan {\n // Boot receives every collection the bundle declares, including the ones\n // served by another engine entirely. Creating a Postgres table for a\n // Firestore collection is not a harmless extra: the app keeps reading\n // documents from Firestore while an empty table with the same name accretes\n // policies and shows up in every drift report.\n // Before the filter, deliberately: a `search` block on a collection this\n // engine does not store would otherwise be dropped here without a word.\n assertSearchIsPostgresOnly(allCollections);\n\n const collections = relationalCollections(allCollections);\n const actions: EnsureAction[] = [];\n const plannedEnums = new Set<string>();\n\n // 1. Enum types. `CREATE TYPE` has no IF NOT EXISTS, so an existing type is\n // skipped by name rather than guarded in SQL.\n for (const collection of collections) {\n for (const { name, values } of requiredEnums(collection)) {\n if (existing.enums.has(name) || plannedEnums.has(name)) continue;\n plannedEnums.add(name);\n const [schema, typeName] = name.split(\".\");\n actions.push({\n kind: \"create-enum\",\n target: name,\n sql: `CREATE TYPE \"${schema}\".\"${typeName}\" AS ENUM (${values.map(quoteSqlLiteral).join(\", \")});`\n });\n }\n }\n\n // 1b. Search support, for collections that declared a `search` block.\n //\n // Before the tables, because a generated column's expression is\n // resolved when the column is created: a table whose search column\n // calls `rebase_search_text` cannot be added before that function\n // exists. Both forms are idempotent, so a boot against a database that\n // already has them plans nothing.\n const searchSpecs = collections\n .map(c => buildSearchColumnSpec(c))\n .filter((spec): spec is SearchColumnSpec => spec !== undefined);\n\n const plannedExtensions = new Set<string>();\n for (const spec of searchSpecs) {\n for (const statement of searchExtensionStatements(spec)) {\n if (plannedExtensions.has(statement)) continue;\n plannedExtensions.add(statement);\n actions.push({ kind: \"create-extension\", target: statement.replace(/^CREATE EXTENSION IF NOT EXISTS |;$/g, \"\"), sql: statement });\n }\n }\n const plannedFunctions = new Set<string>();\n for (const spec of searchSpecs) {\n for (const statement of searchHelperFunctions(spec)) {\n if (plannedFunctions.has(statement)) continue;\n plannedFunctions.add(statement);\n actions.push({ kind: \"create-function\", target: statement.includes(\"unaccent\") ? SEARCH_UNACCENT_FN : SEARCH_TEXT_FN, sql: statement });\n }\n }\n\n // 2. Missing tables. Only the identity column is created here; every other\n // column is added by step 3, so a new table and an existing table that\n // gained a field travel the exact same code path. One way to build a\n // column means one way for it to be wrong.\n const created = new Set<string>();\n for (const collection of collections) {\n const key = qualified(collection);\n if (existing.tables.has(key) || created.has(key)) continue;\n created.add(key);\n const schema = schemaOf(collection);\n const table = getTableName(collection);\n const idEntry = Object.entries(collection.properties ?? {}).find(([n, p]) =>\n isIdProperty(n, p as Property, collection)\n );\n const idName = idEntry ? resolveColumnName(idEntry[0], idEntry[1] as Property) : \"id\";\n const idProp = idEntry?.[1] as Property | undefined;\n // Derived through the generator's own type mapping, not re-decided here.\n // This branch used to emit BIGSERIAL for a numeric id while `db push`\n // emitted INTEGER GENERATED BY DEFAULT AS IDENTITY, so the same project\n // got an int8 key when the runtime brought the schema up and an int4 key\n // when a human pushed it. Two consequences, both real: node-postgres\n // hands back int8 as a *string*, so a collection declaring\n // `type: \"number\"` served `\"1\"` instead of `1` on the managed path only;\n // and every foreign key and junction column pointing at it stayed\n // INTEGER on both paths, which is a truncation waiting for the sequence\n // to pass 2^31. The agreement test now pins this.\n const idType = idProp\n ? getSqlColumnType(idEntry![0], idProp, collection, collections)\n : \"TEXT\";\n let idDef = `\"${idName}\" ${idType} PRIMARY KEY`;\n if (idProp?.type === \"string\" && (idProp as { isId?: unknown }).isId === \"uuid\") {\n idDef += \" DEFAULT gen_random_uuid()\";\n }\n actions.push({\n kind: \"create-table\",\n target: key,\n sql: `CREATE TABLE IF NOT EXISTS \"${schema}\".\"${table}\" (${idDef});`\n });\n }\n\n // 2b. Junction tables behind many-to-many relations. No collection declares\n // them, so the walk above never sees them — and until they existed, an\n // m2m write had nowhere to land and the junction's derived RLS had\n // nothing to attach to.\n const junctions = planJunctionTables(collections);\n for (const junction of junctions) {\n const key = `${junction.schema}.${junction.table}`;\n if (existing.tables.has(key) || created.has(key)) continue;\n created.add(key);\n actions.push({ kind: \"create-table\", target: key, sql: junction.createTable });\n }\n\n // 3. Missing columns, on both brand-new and pre-existing tables.\n const legacyForeignKeys: LegacyForeignKey[] = [];\n\n /**\n * Move a relation column that is only missing because it was renamed.\n *\n * Returns true when it handled the column, so the caller skips the ordinary\n * ADD. Adding here would be the wrong move and a quiet one: the data is in\n * the old column, `ADD COLUMN` creates the new one empty beside it, every\n * statement succeeds, and the relation reads the empty one. A rename is\n * metadata-only in Postgres, keeps the values, and carries the column's\n * indexes and constraints with it.\n *\n * Only ever reached when the new name is absent and the old name is\n * present, so there is nothing to overwrite and nothing to choose between.\n */\n const renameLegacyColumn = (\n key: string,\n schema: string,\n table: string,\n column: string,\n legacyName: string | undefined\n ): boolean => {\n const present = existing.tables.get(key);\n if (!legacyName || !present) return false;\n if (present.has(column) || !present.has(legacyName)) return false;\n\n legacyForeignKeys.push({ table: key, expected: column, legacy: legacyName });\n actions.push({\n kind: \"rename-column\",\n target: `${key}.${column}`,\n sql: `ALTER TABLE \"${schema}\".\"${table}\" RENAME COLUMN \"${legacyName}\" TO \"${column}\";`\n });\n return true;\n };\n\n const addColumn = (\n key: string,\n schema: string,\n table: string,\n column: string,\n definition: string\n ): void => {\n const present = existing.tables.get(key);\n if (present?.has(column)) return;\n actions.push({\n kind: \"add-column\",\n target: `${key}.${column}`,\n sql: `ALTER TABLE \"${schema}\".\"${table}\" ADD COLUMN IF NOT EXISTS \"${column}\" ${definition};`\n });\n };\n\n for (const collection of collections) {\n const key = qualified(collection);\n const schema = schemaOf(collection);\n const table = getTableName(collection);\n // A table this run is creating has no rows yet, so the constraints\n // `db push` writes are free to apply. On a table that already exists\n // they are not: `SET NOT NULL` is checked against live rows and a UNIQUE\n // would fail on existing duplicates, and this module runs unattended\n // against customer data with nobody reading a diff. So the constraints\n // are emitted for the fresh case — which is the whole managed-runtime\n // path, and the one that diverged from `db push` — and withheld for the\n // adopted one. `rebase db push` remains how an existing table gets them.\n const fresh = created.has(key);\n const auth = isAuthCollection(collection);\n for (const [propName, prop] of Object.entries(collection.properties ?? {})) {\n const p = prop as Property;\n if (isIdProperty(propName, p, collection)) continue;\n // Relation and reference columns are planned from the shared\n // relational planner below, which derives the column name, type and\n // foreign key the same way `db push` does. Deriving them here as\n // plain columns is what once produced a column with no constraint.\n if (p.type === \"reference\" || p.type === \"relation\") continue;\n\n const column = resolveColumnName(propName, p);\n // On an auth collection, the columns auth itself reads and writes\n // have exactly one definition, wherever the table is created from —\n // see `auth-users-columns`. Anything else on that collection is an\n // ordinary user-declared field and is generated like any other.\n const authDefinition = auth ? authUsersColumnDefinition(column) : undefined;\n if (authDefinition) {\n addColumn(key, schema, table, column, authDefinition);\n continue;\n }\n\n // Assembled in the generator's order — type, UNIQUE, DEFAULT,\n // NOT NULL — so the two produce byte-identical column definitions\n // and the agreement test can compare them directly instead of\n // checking that a column merely exists, which is how BIGSERIAL-vs-\n // INTEGER and every missing constraint went unnoticed.\n let definition = getSqlColumnType(propName, p, collection, collections);\n if (fresh && p.validation?.unique) definition += \" UNIQUE\";\n // Not gated on `fresh`: a default binds future writes only, so it is\n // safe on a live table, and a column added without it would take the\n // value the application forgot to send rather than `now()`.\n const autoValue = (p as { autoValue?: string }).autoValue;\n if (p.type === \"date\" && (autoValue === \"on_create\" || autoValue === \"on_update\")) {\n definition += \" DEFAULT now()\";\n }\n if (fresh && p.validation?.required) definition += \" NOT NULL\";\n addColumn(key, schema, table, column, definition);\n }\n\n // The auth columns the collection never mentions. The scaffold's users\n // collection describes 12 of the 14 auth reads and writes, so planning\n // only from properties left `is_anonymous` and `tokens_valid_after` to\n // `ensureAuthTablesExist` — which does create them, but only because\n // that function happens to run later in the same boot. Planning them\n // here makes this path self-contained and identical to `db push`, so\n // neither depends on the other having run.\n if (auth) {\n const declared = new Set(\n Object.entries(collection.properties ?? {})\n .map(([name, prop]) => resolveColumnName(name, prop as Property))\n );\n for (const spec of AUTH_USERS_COLUMNS) {\n if (declared.has(spec.column)) continue;\n addColumn(key, schema, table, spec.column, authUsersColumnSql(spec));\n }\n }\n }\n\n // 3aa. The generated search columns.\n //\n // Adding a STORED generated column rewrites the table, which on a large\n // one is not free — but it is the same additive shape as every other\n // column here, and the alternative (leaving it out until someone runs a\n // migration) is a declared `search` block that silently does nothing.\n //\n // Changing one is not additive, and `ADD COLUMN IF NOT EXISTS` is a\n // no-op against a column that is already there — which is why a `search`\n // block that gained a field, flipped `unaccent` or moved a weight used\n // to be inert forever, on every path, with nothing logged. Each column\n // therefore carries a fingerprint of the expression it was built from\n // (in its comment), and a mismatch is reported rather than applied.\n const searchDrift: SearchColumnDrift[] = [];\n const searchAdopted: { table: string; column: string }[] = [];\n for (const spec of searchSpecs) {\n const key = `${spec.schema}.${spec.table}`;\n const definitions: Record<string, string> = {\n [spec.column]: `tsvector GENERATED ALWAYS AS (${spec.expression}) STORED`\n };\n if (spec.fuzzy) {\n definitions[spec.fuzzy.column] = `text GENERATED ALWAYS AS (${spec.fuzzy.expression}) STORED`;\n }\n\n for (const stamp of searchColumnStamps(spec)) {\n const definition = definitions[stamp.column];\n const exists = existing.tables.get(key)?.has(stamp.column) === true;\n const recorded = existing.columnComments?.get(`${key}.${stamp.column}`);\n\n if (exists && recorded?.startsWith(SEARCH_STAMP_PREFIX) && recorded !== stamp.fingerprint) {\n searchDrift.push({\n table: key,\n column: stamp.column,\n found: recorded,\n expected: stamp.fingerprint,\n rebuild: [\n `ALTER TABLE \"${spec.schema}\".\"${spec.table}\" DROP COLUMN \"${stamp.column}\";`,\n `ALTER TABLE \"${spec.schema}\".\"${spec.table}\" ADD COLUMN \"${stamp.column}\" ${definition};`,\n stamp.sql\n ]\n });\n // The old stamp is the only evidence of what the column holds;\n // overwriting it here would erase the drift instead of fixing it.\n continue;\n }\n\n addColumn(key, spec.schema, spec.table, stamp.column, definition);\n if (exists && recorded === undefined) {\n searchAdopted.push({ table: key, column: stamp.column });\n }\n if (recorded !== stamp.fingerprint) {\n actions.push({\n kind: \"comment-column\",\n target: `${key}.${stamp.column}`,\n sql: stamp.sql\n });\n }\n }\n }\n\n // 3b. A junction that already existed, but is short a column. One created\n // above already carries both — unlike a collection table, whose CREATE\n // declares only the identity column — so re-listing them would log two\n // no-op statements and inflate the count of changes applied.\n for (const junction of junctions) {\n const key = `${junction.schema}.${junction.table}`;\n if (created.has(key)) continue;\n for (const column of junction.columns) {\n if (renameLegacyColumn(key, junction.schema, junction.table, column.name, column.legacyName)) continue;\n addColumn(key, junction.schema, junction.table, column.name, column.type);\n }\n }\n\n // 3c. The columns relation and reference properties own.\n for (const relational of planRelationalColumns(collections)) {\n const relKey = `${relational.schema}.${relational.table}`;\n if (renameLegacyColumn(relKey, relational.schema, relational.table, relational.column, relational.legacyColumn)) continue;\n addColumn(\n relKey,\n relational.schema,\n relational.table,\n relational.column,\n relational.type\n );\n }\n\n // 4. Foreign keys, last: the tables and columns on both ends have to exist\n // first, and a constraint is the one thing here that can fail on data\n // rather than on schema, so nothing else depends on it.\n const knownConstraints = existing.constraints ?? new Set<string>();\n const plannedConstraints = new Set<string>();\n const foreignKeys = [\n ...planRelationalColumns(collections).map(r => r.foreignKey),\n ...junctions.flatMap(j => j.foreignKeys)\n ];\n for (const fk of foreignKeys) {\n if (!fk) continue;\n const name = `${fk.schema}.${fk.table}.${fk.constraintName}`;\n if (knownConstraints.has(name) || plannedConstraints.has(name)) continue;\n plannedConstraints.add(name);\n actions.push({\n kind: \"add-constraint\",\n target: `${fk.schema}.${fk.table}.${fk.constraintName}`,\n sql: fk.sql\n });\n }\n\n // 5. Search indexes, after everything — the column has to exist, and this is\n // the one step that runs against a populated table for real work.\n //\n // CONCURRENTLY: a plain CREATE INDEX takes a lock that blocks writes for\n // the duration of the build, which on a live table is an outage. Each\n // statement here is issued on its own, outside any transaction, which is\n // the condition CONCURRENTLY requires.\n for (const spec of searchSpecs) {\n for (const statement of searchIndexStatements(spec)) {\n actions.push({\n kind: \"create-index\",\n target: `${spec.schema}.${spec.table}`,\n sql: statement.replace(\"CREATE INDEX IF NOT EXISTS\", \"CREATE INDEX CONCURRENTLY IF NOT EXISTS\")\n });\n }\n }\n\n return { actions, statements: actions.map(a => a.sql), legacyForeignKeys, searchDrift, searchAdopted };\n}\n\n/** Read what the database has, for the schemas the collections live in. */\nexport async function readExistingSchema(\n client: Queryable,\n schemas: string[]\n): Promise<ExistingSchema> {\n const tables = new Map<string, Set<string>>();\n const enums = new Set<string>();\n if (schemas.length === 0) return { tables, enums };\n\n const inList = schemas\n .map(schema => `'${assertSafeIdentifier(schema, \"schema name\")}'`)\n .join(\", \");\n\n const { rows: columns } = await client.query<{\n table_schema: string;\n table_name: string;\n column_name: string;\n }>(\n `SELECT table_schema, table_name, column_name\n FROM information_schema.columns\n WHERE table_schema IN (${inList})`\n );\n for (const row of columns) {\n const key = `${row.table_schema}.${row.table_name}`;\n if (!tables.has(key)) tables.set(key, new Set());\n tables.get(key)!.add(row.column_name);\n }\n\n const { rows: enumRows } = await client.query<{ schema: string; name: string }>(\n `SELECT n.nspname AS schema, t.typname AS name\n FROM pg_type t\n JOIN pg_namespace n ON t.typnamespace = n.oid\n WHERE t.typtype = 'e' AND n.nspname IN (${inList})`\n );\n for (const row of enumRows) enums.add(`${row.schema}.${row.name}`);\n\n // `ADD CONSTRAINT` has no IF NOT EXISTS, so an existing foreign key is\n // skipped by name rather than guarded in SQL.\n const constraints = new Set<string>();\n const { rows: constraintRows } = await client.query<{\n schema: string;\n table: string;\n name: string;\n }>(\n `SELECT n.nspname AS schema, c.relname AS table, con.conname AS name\n FROM pg_constraint con\n JOIN pg_class c ON con.conrelid = c.oid\n JOIN pg_namespace n ON c.relnamespace = n.oid\n WHERE n.nspname IN (${inList})`\n );\n for (const row of constraintRows) constraints.add(`${row.schema}.${row.table}.${row.name}`);\n\n // Column comments, which is where a generated search column records the\n // expression it was built from. `objsubid > 0` is what makes a row a\n // *column* comment rather than the table's own.\n const columnComments = new Map<string, string>();\n const { rows: commentRows } = await client.query<{\n schema: string;\n table: string;\n column: string;\n comment: string | null;\n }>(\n `SELECT n.nspname AS schema, c.relname AS table, a.attname AS column, d.description AS comment\n FROM pg_description d\n JOIN pg_class c ON d.objoid = c.oid\n JOIN pg_namespace n ON c.relnamespace = n.oid\n JOIN pg_attribute a ON a.attrelid = c.oid AND a.attnum = d.objsubid\n WHERE d.objsubid > 0 AND n.nspname IN (${inList})`\n );\n for (const row of commentRows) {\n if (row.comment == null) continue;\n columnComments.set(`${row.schema}.${row.table}.${row.column}`, row.comment);\n }\n\n return { tables, enums, constraints, columnComments };\n}\n\n/**\n * What to tell an operator whose `search` block no longer matches its column.\n *\n * Every line here is doing work: naming the collection is not enough, because\n * the symptom (a search that finds nothing) points at the data, not the schema;\n * and the remediation has to be exact, because it is a table rewrite the\n * operator is being asked to schedule rather than discover.\n */\nfunction searchDriftMessage(drift: SearchColumnDrift[]): string {\n const blocks = drift.map(d =>\n ` \"${d.table}\".\"${d.column}\" was generated from a different \\`search\\` block ` +\n `(recorded ${d.found}, current ${d.expected}).\\n` +\n d.rebuild.map(s => ` ${s}`).join(\"\\n\")\n );\n return (\n \"The `search` block changed after its generated column was created, and Postgres cannot alter a \" +\n \"generated expression in place.\\n\" +\n \"Rebase will not rebuild it for you: dropping and re-adding a STORED generated column rewrites the whole \" +\n \"table under an ACCESS EXCLUSIVE lock and rebuilds its GIN index, which is an outage this unattended path \" +\n \"may not schedule on your behalf.\\n\" +\n \"Until it is rebuilt the column keeps indexing the previous fields, weights and language — searches for \" +\n \"anything added since return nothing, which reads from outside as \\\"no such row\\\".\\n\" +\n \"Run these (or revert the block to what the column was built from), then boot again:\\n\" +\n blocks.join(\"\\n\") +\n \"\\n The GIN index is dropped with the column and recreated concurrently on the next boot.\"\n );\n}\n\n/**\n * The missing-pgvector explanation, appended to the error that reveals it.\n *\n * A `{ type: \"vector\" }` property compiles to `VECTOR(n)`, and nothing in the\n * OSS pipeline installs pgvector — not this ensure, not `db push`, not the\n * scaffold's `postgres:18-alpine`, which does not ship it. Installing an\n * extension on someone's database is a decision with a deployment behind it\n * (image, superuser, cloud allow-list), so this path stays a refusal; what it\n * must not stay is a bare `type \"vector\" does not exist` on a crash-looping\n * pod, which names nothing the reader can act on.\n */\nfunction vectorExtensionHint(message: string): string {\n if (!/type \"(vector|halfvec|sparsevec)\" does not exist/i.test(message)) return \"\";\n return (\n \"\\n pgvector is not installed on this database, and Rebase does not install it: it is a server extension, \" +\n \"so it needs an image that ships it (e.g. `pgvector/pgvector:pg18` — the scaffold's `postgres:18-alpine` \" +\n \"does not) and a role allowed to run `CREATE EXTENSION vector;`. Install it once, then boot again. \" +\n \"Note also that Rebase creates no ANN index for a vector column, so `vectorSearch` is an exact scan.\"\n );\n}\n\n/**\n * Bring the database up to date. Returns what it did.\n *\n * Each statement runs on its own rather than in one transaction: they are all\n * independently safe and idempotent, and a single failure (an enum label that\n * cannot be added, say) should not roll back the tables that were created fine.\n * The error is surfaced with the statement that caused it.\n */\nexport async function ensureCollectionTables(\n client: Queryable,\n collections: CollectionConfig[],\n log?: (message: string) => void\n): Promise<EnsureOutcome> {\n // Junctions live alongside the collections that declare them, so their\n // schema has to be read too — otherwise an existing junction reads as\n // missing and its constraints as unplanned.\n const schemas = Array.from(new Set([\n ...collections.map(schemaOf),\n ...planJunctionTables(collections).map(j => j.schema)\n ]));\n for (const schema of schemas) {\n assertSafeIdentifier(schema, \"schema name\");\n if (schema !== \"public\") {\n await client.query(`CREATE SCHEMA IF NOT EXISTS \"${schema}\";`);\n }\n }\n\n const existing = await readExistingSchema(client, schemas);\n const plan = planCollectionSchemaEnsure(collections, existing);\n const failures: EnsureOutcome[\"failures\"] = [];\n\n // Reported, not warned: this is a rename the ensure is about to perform, and\n // the operator should be able to see in the log why a column changed name.\n // The interesting case is the one that no longer happens — before this,\n // ensure added the new column empty beside the populated old one, every\n // statement succeeded, and the relation read the empty one.\n for (const legacy of plan.legacyForeignKeys) {\n const message =\n `Renaming \"${legacy.table}\".\"${legacy.legacy}\" to \"${legacy.expected}\". The old name is ` +\n \"the one Rebase derived for this relation before it singularized properly; the column \" +\n \"keeps its data, indexes and constraints. To keep the old name instead, set \" +\n `\\`localKey: \"${legacy.legacy}\"\\` on the relation and this will stop.`;\n logger.info(`[schema] ${message}`);\n log?.(message);\n }\n\n // Before anything is applied: a `search` block that changed after its column\n // was generated cannot be honoured by an additive plan, and serving the old\n // index while the config describes a new one is the silent failure this\n // check exists to end. Refusing is the loud half — boot is fatal on purpose\n // (see `ensureCollectionSchema` in the server's boot) and the message\n // carries the exact statements that resolve it.\n if (plan.searchDrift.length > 0) {\n throw new Error(searchDriftMessage(plan.searchDrift));\n }\n\n // Said once per column, at the moment the stamp is applied: from here on a\n // change is detected, but whether *this* column matches the block it is\n // being stamped with is not knowable — it predates the stamp.\n for (const adopted of plan.searchAdopted) {\n const message =\n `Adopting the existing generated column \"${adopted.table}\".\"${adopted.column}\" and recording what the ` +\n \"current `search` block would generate. Any later change to that block will be detected and refused; a \" +\n \"change made *before* this version was deployed cannot be, so if search has been missing content, \" +\n `rebuild the column once: ALTER TABLE \"${adopted.table.split(\".\").join('\".\"')}\" DROP COLUMN \"${adopted.column}\"; and boot again.`;\n logger.info(`[schema] ${message}`);\n log?.(message);\n }\n\n if (plan.actions.length === 0) {\n log?.(\"Schema is up to date; nothing to create.\");\n return { ...plan, failures };\n }\n\n for (const action of plan.actions) {\n try {\n if (await applyAction(client, action)) {\n log?.(`${action.kind}: ${action.target}`);\n } else {\n log?.(`${action.kind}: ${action.target} (already created by a peer)`);\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n // A foreign key is the only action that can fail on the customer's\n // data rather than on the schema. The column it polices is already\n // there, so the collection serves either way — record it and carry\n // on rather than crash-looping the deployment.\n //\n // A comment is metadata about a column that was just created\n // successfully, and it can only fail on ownership (COMMENT requires\n // owning the table, which an adopted table may not grant). Losing\n // the stamp costs drift detection on the next boot; it must not cost\n // the deployment.\n if (action.kind === \"add-constraint\" || action.kind === \"comment-column\") {\n failures.push({ kind: action.kind, target: action.target, error: message });\n continue;\n }\n throw new Error(\n `Failed to ${action.kind} ${action.target}: ${message}${vectorExtensionHint(message)}\\n ${action.sql}`\n );\n }\n }\n return { ...plan, failures };\n}\n\n/** Attempts per action, including the first. Matches the server's bootstraps. */\nconst DDL_ATTEMPTS = 4;\n\n/**\n * Run one planned statement, surviving a simultaneous boot.\n *\n * Every statement in a plan is written to be idempotent, and that is not the\n * same as being safe to run concurrently: `CREATE … IF NOT EXISTS` reads the\n * catalog and then writes to it as two steps, so peers starting together both\n * see \"absent\" and the loser gets a duplicate key on a *catalog* index. Measured\n * against Postgres 18: five instances, 8 of 10 calls lost. `CREATE TYPE` is\n * worse, because Postgres has no `IF NOT EXISTS` for it at all.\n *\n * What made that fatal here rather than merely noisy is the loop this sits in.\n * A losing statement threw, and the throw abandoned **every remaining action in\n * the plan** — so a replica that lost one race came up missing tables it never\n * attempted, and the boot log blamed the one statement that failed.\n *\n * @returns `true` if this process applied the statement, `false` if a peer had\n * already created the object. The distinction is only for the log; both mean\n * the object is now there.\n * @throws the original error for anything that is not a race — a syntax error, a\n * permission failure, a unique constraint the customer's own rows violate.\n */\nasync function applyAction(\n client: Queryable,\n action: EnsureAction\n): Promise<boolean> {\n for (let attempt = 1; ; attempt++) {\n try {\n await client.query(action.sql);\n return true;\n } catch (err) {\n // Already there. Not \"retry\" — the end state this statement wanted\n // is the end state the database is in, so carry on to the next\n // action rather than spending three more attempts proving it.\n if (isDuplicateObjectRace(err)) {\n logger.debug(\n `[schema] ${action.kind} ${action.target}: already created by another instance`\n );\n return false;\n }\n // Retryable but not yet satisfied — a deadlock between two boots\n // taking catalog locks in step. The statement did nothing; run it\n // again after a jittered pause so peers that collided once do not\n // collide again in lockstep.\n if (isConcurrentDdlRace(err) && attempt < DDL_ATTEMPTS) {\n logger.debug(\n `[schema] ${action.kind} ${action.target}: lost a race with another instance ` +\n `(attempt ${attempt}/${DDL_ATTEMPTS}) — retrying`\n );\n await new Promise(resolve => setTimeout(resolve, 40 * attempt * (1 + Math.random())));\n continue;\n }\n throw err;\n }\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"rls-enforcement-Ch0T6OwW.js","names":[],"sources":["../../common/src/util/internal-tables.ts","../src/security/rls-enforcement.ts"],"sourcesContent":["/**\n * The tables Rebase creates for its own bookkeeping, and the SQL that keeps the\n * end-user role away from them.\n *\n * ## Why this exists\n *\n * Authenticated requests run as {@link REBASE_USER_ROLE}, and the boot-time role\n * provisioning grants that role `SELECT, INSERT, UPDATE, DELETE` on every table\n * in the schemas a project uses — including `rebase`, because a project's own\n * collections are allowed to live there (the scaffold puts `users` there). It\n * also sets `ALTER DEFAULT PRIVILEGES`, so a table created *later* by the\n * migrating role inherits the same grant.\n *\n * Every framework-internal table is created later: auth's tables come up during\n * `initializeAuth`, `api_keys` during route mounting, `cron_logs` when the first\n * job registers, `idempotency_keys` on the first request that carries a key. So\n * they all inherited full DML for the end-user role — and none of them enables\n * row-level security, because none of them is a collection with\n * `securityRules`. Measured on a freshly provisioned database, `SET ROLE\n * rebase_user` could read `rebase.refresh_tokens` (session token hashes),\n * `rebase.mfa_factors` (`secret_encrypted`), `rebase.recovery_codes`, and\n * `rebase.api_keys` (including its `admin` flag), and insert into\n * `rebase.app_config`.\n *\n * Nothing routes a user-context query at those tables today, so this was not\n * reachable over the API. That is the wrong thing to depend on: the documented\n * model is that RLS is the authorization boundary, and these tables sat outside\n * it. The boundary is now a privilege boundary instead — the role simply cannot\n * address them.\n *\n * ## Why REVOKE rather than ENABLE ROW LEVEL SECURITY\n *\n * RLS with no policy denies every row, which is the same outcome, but it is the\n * *weaker* statement: it leaves the grant in place, so a later policy — or a\n * `FORCE` flag cleared by some future migration — reopens the table. There is no\n * row of `refresh_tokens` any end user should ever reach, so the honest encoding\n * is \"this role has no privilege here at all\". It also keeps the owner\n * connection (which auth actually runs on) completely unaffected.\n *\n * ## Keeping it true\n *\n * `packages/rls-check` scans the `rebase` schema — it used to skip it as a\n * \"platform\" schema — and its `rls-disabled` check fires on exactly the\n * condition this module removes: RLS off *and* a DML grant to a reachable role.\n * So a table added here without a revoke is caught by `pnpm rls:check`, not by\n * someone re-reading this file.\n */\n\n/**\n * The Postgres role authenticated requests run as.\n *\n * Defined here rather than in the Postgres driver because both the driver (which\n * provisions the role) and this module (which revokes on its behalf) need it,\n * and a second spelling of a role name is a silent no-op waiting to happen.\n */\nexport const REBASE_USER_ROLE = \"rebase_user\";\n\n/**\n * Framework-internal table names, unqualified.\n *\n * Deliberately NOT including `users`: the auth user table is also a collection,\n * with `securityRules`, RLS enabled and policies applied. Users read their own\n * row through it — revoking there would break sign-in.\n *\n * `atlas_schema_revisions` is Atlas's migration ledger, which lands in `rebase`\n * because `db migrate apply` passes `--revisions-schema rebase`.\n */\nexport const REBASE_INTERNAL_TABLES: readonly string[] = [\n // auth\n \"user_identities\",\n \"refresh_tokens\",\n \"password_reset_tokens\",\n \"magic_link_tokens\",\n \"mfa_factors\",\n \"mfa_challenges\",\n \"recovery_codes\",\n \"app_config\",\n \"schema_meta\",\n // platform services\n \"api_keys\",\n \"cron_logs\",\n \"cron_claims\",\n \"rate_limit_hits\",\n \"idempotency_keys\",\n \"entity_history\",\n \"branches\",\n // realtime channels — authorization for these lives in the channel rules the\n // server evaluates before it reads or writes, never in a row policy\n \"channel_messages\",\n \"channel_cursors\",\n \"channel_presence\",\n // migration bookkeeping\n \"atlas_schema_revisions\"\n];\n\n/** Postgres identifiers this module is willing to interpolate. */\nconst SAFE_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_$]*$/;\n\n/**\n * A single statement that takes every privilege on `schema.table` away from the\n * end-user role.\n *\n * Wrapped in a `DO` block guarded on `pg_roles` for two reasons, both of which\n * happen in practice:\n *\n * - the role does not exist when the connection is unprivileged (Rebase then\n * relies on native RLS rather than a role switch), and a bare `REVOKE` on a\n * missing role is an error, not a no-op;\n * - the table may not exist yet — `cron_logs` never appears in a project with\n * no cron jobs — and `to_regclass` returning NULL has to be tolerated too.\n *\n * One command, so it is safe on handles that speak the extended query protocol\n * and reject multi-statement strings.\n */\nexport function revokeInternalTableSql(schema: string, table: string): string {\n if (!SAFE_IDENTIFIER.test(schema)) {\n throw new Error(`Refusing to build SQL with an unsafe schema name: ${JSON.stringify(schema)}`);\n }\n if (!SAFE_IDENTIFIER.test(table)) {\n throw new Error(`Refusing to build SQL with an unsafe table name: ${JSON.stringify(table)}`);\n }\n const qualified = `\"${schema}\".\"${table}\"`;\n return `\n DO $rebase_revoke$\n BEGIN\n IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = '${REBASE_USER_ROLE}')\n AND to_regclass('${qualified}') IS NOT NULL THEN\n EXECUTE 'REVOKE ALL ON ${qualified} FROM ${REBASE_USER_ROLE}';\n END IF;\n END\n $rebase_revoke$;\n `.trim();\n}\n\n/**\n * Revoke on every internal table in `schema`, one statement at a time.\n *\n * Best-effort per table: a connection that does not own one of them (a\n * pre-provisioned database, a platform-managed ledger) cannot revoke on it, and\n * that must not take down a boot. The caller decides how loud to be — `onError`\n * exists so the driver can warn without this module importing a logger.\n */\nexport async function revokeInternalTableAccess(\n execute: (sql: string) => Promise<unknown>,\n schema: string,\n options?: { tables?: readonly string[]; onError?: (table: string, error: unknown) => void }\n): Promise<void> {\n for (const table of options?.tables ?? REBASE_INTERNAL_TABLES) {\n try {\n await execute(revokeInternalTableSql(schema, table));\n } catch (error) {\n options?.onError?.(table, error);\n }\n }\n}\n","import { sql as drizzleSql, SQL } from \"drizzle-orm\";\nimport { ANONYMOUS_USER_ID, PolicyExpression, SecurityRule } from \"@rebasepro/types\";\nimport {\n AnonymousGrantRisk,\n findAnonymousGrants,\n REBASE_USER_ROLE,\n revokeInternalTableAccess,\n securityRuleToConditions\n} from \"@rebasepro/common\";\nimport { REBASE_SCHEMA, usesLegacyRlsFunctions } from \"@rebasepro/types\";\nimport { logger } from \"@rebasepro/server\";\n\n/**\n * Unified RLS enforcement — the \"user context vs server context\" model.\n *\n * Every operation runs in one of two contexts:\n *\n * - **User context** — a request authenticated (or anonymous) via\n * `driver.withAuth(user)`. Runs as the restricted `rebase_user` role: a\n * non-owner, NOSUPERUSER, NOBYPASSRLS role, so Postgres RLS binds *every*\n * statement (SELECT, INSERT, UPDATE, DELETE). The collection's\n * `securityRules` are the whole authorization model; app-layer callbacks\n * are validation/side-effects, not a security boundary.\n *\n * - **Server context** — the base (owner) connection: auth flows, migrations,\n * and raw `rebase.sql`. As table owner it bypasses RLS. This is the trusted\n * plane, equivalent to Supabase's `service_role`.\n *\n * `rebase.dataAsAdmin` is **not** in it, despite the name. `init.ts` scopes\n * that driver with `withAuth(SERVICE_IDENTITY)`, so it arrives as user\n * context above — `rebase_user`, `app.uid = 'service'`, policies evaluated —\n * and clears the default policies through their admin arm rather than the\n * `rebase.uid() IS NULL` one.\n *\n * This module provides the three pieces:\n *\n * 1. {@link detectConnectionPosture} — is the connection subject to RLS at\n * all? (superuser / BYPASSRLS / table owner ⇒ no)\n * 2. {@link ensureAppRole} — idempotently provision `rebase_user` with\n * SELECT/INSERT/UPDATE/DELETE grants (+ default privileges so future\n * tables stay covered).\n * 3. {@link applyAuthContext} — per-transaction: set the `app.*` GUCs the\n * policies read (`rebase.uid()` etc.) and `SET LOCAL ROLE rebase_user` so\n * RLS binds. Transaction-scoped, so it composes with poolers.\n *\n * Provisioning runs from the framework's own bootstrap/migrate (which already\n * self-creates the `auth` schema and functions) — enforcement is default-on,\n * not an operator opt-in.\n */\n\n/**\n * The restricted role every authenticated (user-context) request runs as.\n *\n * Re-exported, not re-declared: the same name is needed by\n * `@rebasepro/common`'s internal-table revokes, and two spellings of a role name\n * fail as a silent no-op rather than an error.\n */\nexport { REBASE_USER_ROLE };\n\n/** Minimal SQL runner so callers can adapt drizzle or pg.Client. */\nexport type RawSqlRunner = (sqlText: string) => Promise<Record<string, unknown>[]>;\n\n/** Minimal transaction surface needed by {@link applyAuthContext}. */\nexport interface SqlTx {\n execute(query: SQL): Promise<unknown>;\n}\n\nexport interface ConnectionPosture {\n /** The connection's `current_user`. */\n role: string;\n superuser: boolean;\n bypassRLS: boolean;\n /** Owns at least one user table — owners bypass non-FORCE RLS. */\n ownsTables: boolean;\n /** True when RLS would NOT constrain this connection. */\n privileged: boolean;\n}\n\nexport interface AuthContext {\n uid: string;\n /** Raw roles as carried on the user (strings or `{ id }` objects). */\n roles: unknown[];\n}\n\nconst quoteIdent = (name: string): string => `\"${name.replace(/\"/g, \"\\\"\\\"\")}\"`;\n\n/** DML the user role holds on managed tables (RLS still filters per row). */\nconst USER_TABLE_PRIVILEGES = \"SELECT, INSERT, UPDATE, DELETE\";\n\n/**\n * Warn when the connection role shares its name with an existing schema.\n *\n * Postgres resolves unqualified names through `search_path`, which defaults to\n * `\"$user\", public` — and `$user` is the connection ROLE. When a schema of that\n * name exists it sits ahead of `public`, so every unqualified statement\n * silently operates on it instead:\n *\n * CREATE TABLE posts (...); -- you meant public.posts; you got <role>.posts\n *\n * Nothing errors. You get a second table of the same name in the wrong schema,\n * and reads that pin `public` cannot see it — which reads as \"missing table\" and\n * sends people to re-run a push that creates a *third* copy. The bootstrapper\n * has a whole branch dedicated to recognising the symptom after the fact.\n *\n * Rebase shipped straight into this: it creates a schema named `rebase` while\n * every template named the database role `rebase` too. The scaffold uses\n * `rebase_app` now, and every pool Rebase opens pins `search_path=public`\n * (`pinSearchPath`), which covers the paths the framework controls. This covers\n * the ones it does not — `psql`, `pg_dump`, drizzle-kit, a colleague's script,\n * a hand-written migration — because the hazard is a property of the two NAMES,\n * not of any one connection.\n *\n * A warning rather than a boot failure: the database works, the framework's own\n * traffic is pinned, and refusing to start over a naming choice a user may have\n * inherited would be worse than the risk.\n */\nexport async function warnOnRoleSchemaCollision(run: RawSqlRunner): Promise<void> {\n try {\n const rows = await run(`\n SELECT current_user AS role,\n EXISTS (\n SELECT 1 FROM pg_namespace n WHERE n.nspname = current_user\n ) AS collides\n `);\n if (rows[0]?.collides !== true) return;\n const role = String(rows[0]?.role ?? \"the connection role\");\n logger.warn(\n `⚠️ The database role \"${role}\" has the same name as a schema. Postgres resolves unqualified ` +\n `names through \\`search_path\\`, which defaults to \\`\"$user\", public\\` — so \"${role}\" is searched ` +\n `BEFORE public, and any unqualified \\`CREATE TABLE\\`/\\`SELECT\\` from a tool that does not pin the ` +\n `path (psql, pg_dump, drizzle-kit, a hand-written migration) silently lands in \"${role}\" instead. ` +\n `Rebase's own connections pin \\`search_path=public\\`, so the server is unaffected. To remove the ` +\n `hazard entirely, connect as a role whose name is not also a schema — the scaffold uses ` +\n `\"rebase_app\".`\n );\n } catch {\n // A diagnostic must never be the reason a boot fails.\n }\n}\n\nexport async function detectConnectionPosture(run: RawSqlRunner): Promise<ConnectionPosture> {\n const rows = await run(`\n SELECT current_user AS role,\n r.rolsuper AS superuser,\n r.rolbypassrls AS bypassrls,\n EXISTS (\n SELECT 1 FROM pg_tables t\n WHERE t.tableowner = current_user\n AND t.schemaname NOT IN ('pg_catalog', 'information_schema')\n ) AS owns_tables\n FROM pg_roles r\n WHERE r.rolname = current_user\n `);\n const row = rows[0] ?? {};\n const superuser = row.superuser === true;\n const bypassRLS = row.bypassrls === true;\n const ownsTables = row.owns_tables === true;\n return {\n role: String(row.role ?? \"unknown\"),\n superuser,\n bypassRLS,\n ownsTables,\n privileged: superuser || bypassRLS || ownsTables\n };\n}\n\n/**\n * Human-actionable instructions for when the connection cannot provision the\n * user role itself (no CREATEROLE and role not pre-created by the platform).\n */\nexport function appRoleSetupInstructions(connectionRole: string, schemas: string[]): string {\n const grants = schemas.map((s) =>\n `GRANT USAGE ON SCHEMA ${quoteIdent(s)} TO ${REBASE_USER_ROLE};\\n` +\n `GRANT ${USER_TABLE_PRIVILEGES} ON ALL TABLES IN SCHEMA ${quoteIdent(s)} TO ${REBASE_USER_ROLE};\\n` +\n `GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA ${quoteIdent(s)} TO ${REBASE_USER_ROLE};`\n ).join(\"\\n\");\n return (\n `Rebase enforces row-level security by running authenticated requests as ` +\n `the restricted role \"${REBASE_USER_ROLE}\", but the connection role ` +\n `\"${connectionRole}\" bypasses RLS and cannot create that role itself.\\n` +\n `Run the following as a database administrator, then restart:\\n\\n` +\n `CREATE ROLE ${REBASE_USER_ROLE} NOLOGIN NOSUPERUSER NOBYPASSRLS NOINHERIT;\\n` +\n `GRANT ${REBASE_USER_ROLE} TO ${quoteIdent(connectionRole)};\\n` +\n grants\n );\n}\n\n/**\n * Idempotently provision the `rebase_user` role, membership for the current\n * connection role, and DML grants (+ default privileges for future tables)\n * on every existing schema in `schemas`.\n *\n * Split into privilege tiers so it works both when the connection is a\n * superuser (creates everything) and when the platform pre-created the role\n * and membership (e.g. CNPG `postInitApplicationSQL`) and the connection is\n * merely the table owner — owners can always run the grant tier themselves.\n *\n * RLS still filters every row: these grants only make the tables *reachable*\n * by the role; the policies decide which rows/commands actually pass.\n *\n * Throws with precise setup instructions when the role is missing and the\n * connection cannot create it.\n */\nexport async function ensureAppRole(run: RawSqlRunner, schemas: string[]): Promise<void> {\n const uniqueSchemas = Array.from(new Set(schemas.filter(Boolean)));\n\n // Tier 1 — role existence.\n const roleRows = await run(`SELECT 1 FROM pg_roles WHERE rolname = '${REBASE_USER_ROLE}'`);\n if (roleRows.length === 0) {\n try {\n await run(`CREATE ROLE ${REBASE_USER_ROLE} NOLOGIN NOSUPERUSER NOBYPASSRLS NOINHERIT`);\n } catch (err) {\n throw new Error(\n `Failed to create the \"${REBASE_USER_ROLE}\" role: ${err instanceof Error ? err.message : String(err)}\\n\\n` +\n appRoleSetupInstructions(\"current connection role\", uniqueSchemas)\n );\n }\n }\n\n // Tier 2 — membership, so a non-superuser connection may SET ROLE to it.\n const memberRows = await run(`\n SELECT (pg_has_role(current_user, '${REBASE_USER_ROLE}', 'MEMBER')\n OR (SELECT rolsuper FROM pg_roles WHERE rolname = current_user)) AS can_set,\n current_user AS role\n `);\n if (memberRows[0]?.can_set !== true) {\n try {\n await run(`GRANT ${REBASE_USER_ROLE} TO CURRENT_USER`);\n } catch (err) {\n throw new Error(\n `The connection role is not a member of \"${REBASE_USER_ROLE}\" and cannot grant itself membership: ` +\n `${err instanceof Error ? err.message : String(err)}\\n\\n` +\n appRoleSetupInstructions(String(memberRows[0]?.role ?? \"current connection role\"), uniqueSchemas)\n );\n }\n }\n\n // Tier 3 — grants. Table owners (the expected non-superuser posture) can\n // always grant on their own objects, so this tier needs no extra privilege.\n const nspRows = await run(\"SELECT nspname FROM pg_namespace\");\n const existing = new Set(nspRows.map((r) => String(r.nspname)));\n for (const schema of uniqueSchemas) {\n if (!existing.has(schema)) continue;\n const s = quoteIdent(schema);\n await run(`GRANT USAGE ON SCHEMA ${s} TO ${REBASE_USER_ROLE}`);\n await run(`GRANT ${USER_TABLE_PRIVILEGES} ON ALL TABLES IN SCHEMA ${s} TO ${REBASE_USER_ROLE}`);\n await run(`GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA ${s} TO ${REBASE_USER_ROLE}`);\n // Cover objects created later by the CURRENT role (the role that runs\n // migrations), so a migrate can never strand the user role.\n await run(`ALTER DEFAULT PRIVILEGES IN SCHEMA ${s} GRANT ${USER_TABLE_PRIVILEGES} ON TABLES TO ${REBASE_USER_ROLE}`);\n await run(`ALTER DEFAULT PRIVILEGES IN SCHEMA ${s} GRANT USAGE, SELECT ON SEQUENCES TO ${REBASE_USER_ROLE}`);\n\n // The grants above are deliberately schema-wide — a project's own\n // collections may live in `rebase`, and future tables must be reachable\n // or a migration strands the role. Rebase's OWN tables are the exception:\n // refresh tokens, MFA secrets, API keys and the rest carry no RLS and no\n // row an end user should ever address. Taking the privilege back here\n // covers every table that already exists; each creator revokes on the\n // table it just made, for the boot that creates them for the first time.\n await revokeInternalTableAccess(async (text) => { await run(text); }, schema, {\n onError: (table, error) => logger.warn(\n `🔐 [rls] Could not revoke \"${REBASE_USER_ROLE}\" access to \"${schema}\".\"${table}\" — ` +\n \"it stays reachable by authenticated requests: \" +\n (error instanceof Error ? error.message : String(error))\n )\n });\n }\n\n logger.debug(`🔐 [rls] User role \"${REBASE_USER_ROLE}\" provisioned (schemas: ${uniqueSchemas.join(\", \")})`);\n}\n\n/**\n * Apply the authenticated context to a transaction: the `app.*` GUCs that RLS\n * policies read via `rebase.uid()` / `rebase.roles()` / `rebase.jwt()`, and — when\n * `userRole` is set — `SET LOCAL ROLE` so RLS binds every statement in this\n * transaction (reads *and* writes).\n *\n * GUCs are set with `is_local = true` and the role switch is `LOCAL`: both\n * reset at commit/rollback, so pooled connections are never polluted.\n *\n * Fails closed by construction: if the role switch errors, the transaction\n * aborts instead of proceeding privileged.\n *\n * SECURITY: this function is only ever called on the **user** path (the server\n * context uses the base/owner driver and never calls it). The default policies\n * treat `rebase.uid() IS NULL` as the trusted server context, and `rebase.uid()`\n * is `NULLIF(current_setting('app.uid'), '')` — so an EMPTY user id would\n * be read as NULL and silently escalate a user request to server privileges.\n * Coerce empty/blank ids to `ANONYMOUS_USER_ID` here, at the single chokepoint,\n * rather than trusting every caller (e.g. realtime subscription auth) to do it.\n * That sentinel is exported from `@rebasepro/types` because it leaks into rule\n * semantics: it is why `rebase.uid() IS NOT NULL` is true for anonymous requests.\n */\nexport async function applyAuthContext(tx: SqlTx, auth: AuthContext, userRole?: string): Promise<void> {\n const uid = typeof auth.uid === \"string\" && auth.uid.trim() !== \"\" ? auth.uid : ANONYMOUS_USER_ID;\n const normalizedRoles = auth.roles.map((r: unknown) =>\n typeof r === \"string\" ? r : (r as Record<string, unknown>)?.id ?? String(r)\n );\n // `app.user_id` is the pre-rename spelling, still written because policies\n // are data: a database provisioned before the rename holds rules compiled\n // to `current_setting('app.user_id')`, and those predicates would evaluate\n // to NULL — failing open or locking out — if we stopped setting it. Drop\n // the alias only once no live database carries a legacy policy.\n await tx.execute(drizzleSql`\n SELECT\n set_config('app.uid', ${uid}, true),\n set_config('app.user_id', ${uid}, true),\n set_config('app.user_roles', ${normalizedRoles.join(\",\")}, true),\n set_config('app.jwt', ${JSON.stringify({ sub: uid, roles: auth.roles })}, true)\n `);\n if (userRole) {\n await tx.execute(drizzleSql.raw(`SET LOCAL ROLE ${quoteIdent(userRole)}`));\n }\n}\n\n/** Role names from other BaaS platforms that people reach for out of habit. */\nconst FOREIGN_CONVENTION_ROLES: Record<string, string> = {\n authenticated: \"Supabase\",\n anon: \"Supabase\",\n service_role: \"Supabase\"\n};\n\n/**\n * Warn about rules that read as \"signed-in users only\" but admit anonymous\n * callers — `rebase.uid() IS NOT NULL`, or a comparison against another\n * platform's magic user id such as `'anon'`.\n *\n * The sibling of {@link validatePolicyPgRoles}, for the more dangerous spelling\n * of the same habit. A foreign `pgRoles` value makes a policy unreachable and\n * the table reads empty — loud, and that guard throws. These do the opposite:\n * the rule compiles to a grant, and nothing looks wrong until the data is\n * already public.\n *\n * Warns rather than throws. Unlike an unreachable `pgRoles`, these rules are\n * serving traffic today: refusing to boot would take an app offline to report a\n * problem it already has, and on the read path it would take it offline\n * *because* its data was exposed. Rewriting the author's SQL is not an option\n * either — this is the escape hatch whose whole promise is that it means what it\n * says. So: say so, loudly, and leave the rule alone.\n */\nexport function warnOnAnonymousGrants(\n collections: { slug?: string; securityRules?: readonly SecurityRule[] }[]\n): void {\n // Grouped by the mistake, not by the rule: one habit typically repeats\n // across every collection an author wrote, and a per-rule list would repeat\n // the same paragraph dozens of times and get skimmed.\n const byRisk = new Map<string, { risk: AnonymousGrantRisk; sites: string[] }>();\n\n for (const collection of collections) {\n for (const rule of collection.securityRules ?? []) {\n const { usingExpr, withCheckExpr } = securityRuleToConditions(rule);\n const risks = [usingExpr, withCheckExpr]\n .filter((e): e is PolicyExpression => e !== null)\n .flatMap(findAnonymousGrants);\n\n for (const risk of risks) {\n const key = `${risk.pattern}:${risk.detail}`;\n const site = `${collection.slug ?? \"(unnamed)\"} → \"${rule.name ?? \"(unnamed rule)\"}\"`;\n const entry = byRisk.get(key) ?? { risk, sites: [] };\n if (!entry.sites.includes(site)) entry.sites.push(site);\n byRisk.set(key, entry);\n }\n }\n }\n\n if (byRisk.size === 0) return;\n\n const problems = [...byRisk.values()].map(({ risk, sites }) =>\n ` • ${risk.explanation}\\n ${sites.length} rule(s): ${sites.join(\", \")}`\n );\n\n logger.warn(\n `Security rules that read as a lockdown but grant access to anonymous requests. Every caller from a ` +\n `client carries a user id ('${ANONYMOUS_USER_ID}' when nobody is signed in), so these clauses are ` +\n `true for everyone:\\n\\n` +\n problems.join(\"\\n\\n\") + \"\\n\"\n );\n}\n\n/**\n * Name the collections whose raw policy SQL still calls the pre-1.0 helpers.\n *\n * The compiler rewrites `auth.uid()` to `rebase.uid()` on the way into the\n * database, so nothing is broken and no policy is wrong — which is exactly why\n * this has to be said out loud. A silent rewrite that works forever is not a\n * migration, it is a second supported spelling nobody wrote down, and the next\n * person to read those rules will copy the old one.\n *\n * Only `raw` expressions can carry it. Structured rules (`policy.authUid()`,\n * `policy.rolesOverlap(...)`) compile from the model and were never affected.\n */\nexport function warnOnLegacyRlsFunctions(\n collections: { slug?: string; securityRules?: readonly SecurityRule[] }[]\n): void {\n const sites: string[] = [];\n\n for (const collection of collections) {\n for (const rule of collection.securityRules ?? []) {\n const { usingExpr, withCheckExpr } = securityRuleToConditions(rule);\n const carriesLegacy = [usingExpr, withCheckExpr]\n .filter((e): e is PolicyExpression => e !== null)\n .some(containsLegacyRlsCall);\n if (!carriesLegacy) continue;\n\n const site = `${collection.slug ?? \"(unnamed)\"} → \"${rule.name ?? \"(unnamed rule)\"}\"`;\n if (!sites.includes(site)) sites.push(site);\n }\n }\n\n if (sites.length === 0) return;\n\n logger.warn(\n `These security rules call the pre-1.0 RLS helpers (\\`auth.uid()\\`, \\`auth.roles()\\`, \\`auth.jwt()\\`). ` +\n `They still work — the compiler rewrites them — but the functions now live in the \\`rebase\\` schema, ` +\n `and the \\`auth\\` one is Supabase's. Update the raw SQL in these rules to \\`${REBASE_SCHEMA}.uid()\\` ` +\n `and friends, or switch them to the structured helpers (\\`policy.authUid()\\`, \\`policy.rolesOverlap()\\`), ` +\n `which never had to be spelled by hand:\\n\\n` +\n sites.map(s => ` • ${s}`).join(\"\\n\") + \"\\n\"\n );\n}\n\n/** Whether any `raw` expression in the tree calls a pre-1.0 helper. */\nfunction containsLegacyRlsCall(expr: PolicyExpression): boolean {\n switch (expr.kind) {\n case \"raw\":\n return usesLegacyRlsFunctions(expr.sql);\n case \"and\":\n case \"or\":\n return expr.operands.some(containsLegacyRlsCall);\n case \"not\":\n return containsLegacyRlsCall(expr.operand);\n case \"existsIn\":\n return containsLegacyRlsCall(expr.where);\n default:\n return false;\n }\n}\n\n/**\n * Reject `pgRoles` that this server can never satisfy.\n *\n * `pgRoles` sets the `TO` clause of a generated policy, so a policy naming a\n * role the request never runs as simply never applies — and RLS then filters\n * every row. The table reads as empty, which is indistinguishable from having\n * no data, so the mistake survives review and ships.\n *\n * Requests run as `rebase_user`, so a policy is only reachable if it targets\n * `public` or a role `rebase_user` holds. Anything else is a configuration\n * error worth failing the boot for.\n */\nexport async function validatePolicyPgRoles(\n run: RawSqlRunner,\n collections: { slug?: string; securityRules?: readonly { name?: string; pgRoles?: readonly string[] }[] }[],\n /** The role requests actually run as: `rebase_user` when the connection is\n * privileged enough to switch, otherwise the connection role itself. */\n requestRole: string = REBASE_USER_ROLE\n): Promise<void> {\n const wanted = new Map<string, string[]>();\n for (const collection of collections) {\n for (const rule of collection.securityRules ?? []) {\n for (const role of rule.pgRoles ?? []) {\n if (role === \"public\") continue;\n wanted.set(role, [...(wanted.get(role) ?? []), collection.slug ?? \"(unnamed)\"]);\n }\n }\n }\n if (wanted.size === 0) return;\n\n const names = [...wanted.keys()].map((r) => `'${r.replace(/'/g, \"''\")}'`).join(\",\");\n const escapedRequestRole = requestRole.replace(/'/g, \"''\");\n const rows = await run(`\n SELECT r.rolname AS role,\n COALESCE(pg_has_role(to_regrole('${escapedRequestRole}'), r.oid, 'MEMBER'), false) AS reachable\n FROM pg_roles r\n WHERE r.rolname IN (${names})\n `);\n\n const reachable = new Map(rows.map((row) => [String(row.role), row.reachable === true]));\n const problems: string[] = [];\n\n for (const [role, slugs] of wanted) {\n if (reachable.get(role) === true) continue;\n\n const why = reachable.has(role)\n ? `\"${requestRole}\" is not a member of it`\n : \"no such role exists in this database\";\n const platform = FOREIGN_CONVENTION_ROLES[role];\n const hint = platform\n ? `\"${role}\" is a ${platform} convention, not a PostgreSQL role. Application roles belong in \\`roles: [\"${role === \"service_role\" ? \"admin\" : role}\"]\\`, which is checked inside the policy via rebase.roles().`\n : `Either grant it (GRANT ${role} TO ${requestRole}) or drop \\`pgRoles\\` so the policy targets \\`public\\`.`;\n\n problems.push(\n ` • pgRoles: [\"${role}\"] on ${slugs.join(\", \")} — ${why}.\\n ${hint}`\n );\n }\n\n if (problems.length > 0) {\n throw new Error(\n `Security rules target PostgreSQL roles this server cannot use. Requests run as ` +\n `\"${requestRole}\", so these policies would never apply and every row would be ` +\n `filtered out — the collections would look empty rather than error.\\n\\n` +\n problems.join(\"\\n\\n\") + \"\\n\"\n );\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuDA,IAAa,mBAAmB;;;;;;;;;;;AAYhC,IAAa,yBAA4C;CAErD;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAEA;CACA;CACA;CACA;CACA;CACA;CACA;CAGA;CACA;CACA;CAEA;AACJ;;AAGA,IAAM,kBAAkB;;;;;;;;;;;;;;;;;AAkBxB,SAAgB,uBAAuB,QAAgB,OAAuB;CAC1E,IAAI,CAAC,gBAAgB,KAAK,MAAM,GAC5B,MAAM,IAAI,MAAM,qDAAqD,KAAK,UAAU,MAAM,GAAG;CAEjG,IAAI,CAAC,gBAAgB,KAAK,KAAK,GAC3B,MAAM,IAAI,MAAM,oDAAoD,KAAK,UAAU,KAAK,GAAG;CAE/F,MAAM,YAAY,IAAI,OAAO,KAAK,MAAM;CACxC,OAAO;;;iEAGsD,iBAAiB;kCAChD,UAAU;yCACH,UAAU,QAAQ,iBAAiB;;;;MAItE,KAAK;AACX;;;;;;;;;AAUA,eAAsB,0BAClB,SACA,QACA,SACa;CACb,KAAK,MAAM,SAAS,SAAS,UAAU,wBACnC,IAAI;EACA,MAAM,QAAQ,uBAAuB,QAAQ,KAAK,CAAC;CACvD,SAAS,OAAO;EACZ,SAAS,UAAU,OAAO,KAAK;CACnC;AAER;;;ACtEA,IAAM,cAAc,SAAyB,IAAI,KAAK,QAAQ,MAAM,MAAM,EAAE;;AAG5E,IAAM,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6B9B,eAAsB,0BAA0B,KAAkC;CAC9E,IAAI;EACA,MAAM,OAAO,MAAM,IAAI;;;;;SAKtB;EACD,IAAI,KAAK,EAAE,EAAE,aAAa,MAAM;EAChC,MAAM,OAAO,OAAO,KAAK,EAAE,EAAE,QAAQ,qBAAqB;EAC1D,OAAO,KACH,0BAA0B,KAAK,4IAC+C,KAAK,gMAED,KAAK,gNAI3F;CACJ,QAAQ,CAER;AACJ;AAEA,eAAsB,wBAAwB,KAA+C;CAazF,MAAM,OAAM,MAZO,IAAI;;;;;;;;;;;KAWtB,EAAA,CACgB,MAAM,CAAC;CACxB,MAAM,YAAY,IAAI,cAAc;CACpC,MAAM,YAAY,IAAI,cAAc;CACpC,MAAM,aAAa,IAAI,gBAAgB;CACvC,OAAO;EACH,MAAM,OAAO,IAAI,QAAQ,SAAS;EAClC;EACA;EACA;EACA,YAAY,aAAa,aAAa;CAC1C;AACJ;;;;;AAMA,SAAgB,yBAAyB,gBAAwB,SAA2B;CACxF,MAAM,SAAS,QAAQ,KAAK,MACxB,yBAAyB,WAAW,CAAC,EAAE,MAAM,iBAAiB,WACrD,sBAAsB,2BAA2B,WAAW,CAAC,EAAE,MAAM,iBAAiB,oDAC7C,WAAW,CAAC,EAAE,MAAM,iBAAiB,EAC3F,CAAC,CAAC,KAAK,IAAI;CACX,OACI,gGACwB,iBAAiB,8BACrC,eAAe,kIAEJ,iBAAiB,qDACvB,iBAAiB,MAAM,WAAW,cAAc,EAAE,OAC3D;AAER;;;;;;;;;;;;;;;;;AAkBA,eAAsB,cAAc,KAAmB,SAAkC;CACrF,MAAM,gBAAgB,MAAM,KAAK,IAAI,IAAI,QAAQ,OAAO,OAAO,CAAC,CAAC;CAIjE,KAAI,MADmB,IAAI,sDAA8D,EAAA,CAC5E,WAAW,GACpB,IAAI;EACA,MAAM,IAAI,eAAe,iBAAiB,2CAA2C;CACzF,SAAS,KAAK;EACV,MAAM,IAAI,MACN,yBAAyB,iBAAiB,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,EAAE,QACrG,yBAAyB,2BAA2B,aAAa,CACrE;CACJ;CAIJ,MAAM,aAAa,MAAM,IAAI;6CACY,iBAAiB;;;KAGzD;CACD,IAAI,WAAW,EAAE,EAAE,YAAY,MAC3B,IAAI;EACA,MAAM,IAAI,SAAS,iBAAiB,iBAAiB;CACzD,SAAS,KAAK;EACV,MAAM,IAAI,MACN,2CAA2C,iBAAiB,wCACzD,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,EAAE,QACpD,yBAAyB,OAAO,WAAW,EAAE,EAAE,QAAQ,yBAAyB,GAAG,aAAa,CACpG;CACJ;CAKJ,MAAM,UAAU,MAAM,IAAI,kCAAkC;CAC5D,MAAM,WAAW,IAAI,IAAI,QAAQ,KAAK,MAAM,OAAO,EAAE,OAAO,CAAC,CAAC;CAC9D,KAAK,MAAM,UAAU,eAAe;EAChC,IAAI,CAAC,SAAS,IAAI,MAAM,GAAG;EAC3B,MAAM,IAAI,WAAW,MAAM;EAC3B,MAAM,IAAI,yBAAyB,EAAE,MAAM,kBAAkB;EAC7D,MAAM,IAAI,SAAS,sBAAsB,2BAA2B,EAAE,MAAM,kBAAkB;EAC9F,MAAM,IAAI,kDAAkD,EAAE,MAAM,kBAAkB;EAGtF,MAAM,IAAI,sCAAsC,EAAE,SAAS,sBAAsB,gBAAgB,kBAAkB;EACnH,MAAM,IAAI,sCAAsC,EAAE,uCAAuC,kBAAkB;EAS3G,MAAM,0BAA0B,OAAO,SAAS;GAAE,MAAM,IAAI,IAAI;EAAG,GAAG,QAAQ,EAC1E,UAAU,OAAO,UAAU,OAAO,KAC9B,8BAA8B,iBAAiB,eAAe,OAAO,KAAK,MAAM,uDAE/E,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,EAC1D,EACJ,CAAC;CACL;CAEA,OAAO,MAAM,uBAAuB,iBAAiB,0BAA0B,cAAc,KAAK,IAAI,EAAE,EAAE;AAC9G;;;;;;;;;;;;;;;;;;;;;;;AAwBA,eAAsB,iBAAiB,IAAW,MAAmB,UAAkC;CACnG,MAAM,MAAM,OAAO,KAAK,QAAQ,YAAY,KAAK,IAAI,KAAK,MAAM,KAAK,KAAK,MAAM;CAChF,MAAM,kBAAkB,KAAK,MAAM,KAAK,MACpC,OAAO,MAAM,WAAW,IAAK,GAA+B,MAAM,OAAO,CAAC,CAC9E;CAMA,MAAM,GAAG,QAAQ,GAAU;;oCAEK,IAAI;wCACA,IAAI;2CACD,gBAAgB,KAAK,GAAG,EAAE;oCACjC,KAAK,UAAU;EAAE,KAAK;EAAK,OAAO,KAAK;CAAM,CAAC,EAAE;KAC/E;CACD,IAAI,UACA,MAAM,GAAG,QAAQ,IAAW,IAAI,kBAAkB,WAAW,QAAQ,GAAG,CAAC;AAEjF;;AAGA,IAAM,2BAAmD;CACrD,eAAe;CACf,MAAM;CACN,cAAc;AAClB;;;;;;;;;;;;;;;;;;;AAoBA,SAAgB,sBACZ,aACI;CAIJ,MAAM,yBAAS,IAAI,IAA2D;CAE9E,KAAK,MAAM,cAAc,aACrB,KAAK,MAAM,QAAQ,WAAW,iBAAiB,CAAC,GAAG;EAC/C,MAAM,EAAE,WAAW,kBAAkB,yBAAyB,IAAI;EAClE,MAAM,QAAQ,CAAC,WAAW,aAAa,CAAC,CACnC,QAAQ,MAA6B,MAAM,IAAI,CAAC,CAChD,QAAQ,mBAAmB;EAEhC,KAAK,MAAM,QAAQ,OAAO;GACtB,MAAM,MAAM,GAAG,KAAK,QAAQ,GAAG,KAAK;GACpC,MAAM,OAAO,GAAG,WAAW,QAAQ,YAAY,MAAM,KAAK,QAAQ,iBAAiB;GACnF,MAAM,QAAQ,OAAO,IAAI,GAAG,KAAK;IAAE;IAAM,OAAO,CAAC;GAAE;GACnD,IAAI,CAAC,MAAM,MAAM,SAAS,IAAI,GAAG,MAAM,MAAM,KAAK,IAAI;GACtD,OAAO,IAAI,KAAK,KAAK;EACzB;CACJ;CAGJ,IAAI,OAAO,SAAS,GAAG;CAEvB,MAAM,WAAW,CAAC,GAAG,OAAO,OAAO,CAAC,CAAC,CAAC,KAAK,EAAE,MAAM,YAC/C,OAAO,KAAK,YAAY,QAAQ,MAAM,OAAO,YAAY,MAAM,KAAK,IAAI,GAC5E;CAEA,OAAO,KACH,iIAC8B,kBAAkB,4EAEhD,SAAS,KAAK,MAAM,IAAI,IAC5B;AACJ;;;;;;;;;;;;;AAcA,SAAgB,yBACZ,aACI;CACJ,MAAM,QAAkB,CAAC;CAEzB,KAAK,MAAM,cAAc,aACrB,KAAK,MAAM,QAAQ,WAAW,iBAAiB,CAAC,GAAG;EAC/C,MAAM,EAAE,WAAW,kBAAkB,yBAAyB,IAAI;EAIlE,IAAI,CAHkB,CAAC,WAAW,aAAa,CAAC,CAC3C,QAAQ,MAA6B,MAAM,IAAI,CAAC,CAChD,KAAK,qBACL,GAAe;EAEpB,MAAM,OAAO,GAAG,WAAW,QAAQ,YAAY,MAAM,KAAK,QAAQ,iBAAiB;EACnF,IAAI,CAAC,MAAM,SAAS,IAAI,GAAG,MAAM,KAAK,IAAI;CAC9C;CAGJ,IAAI,MAAM,WAAW,GAAG;CAExB,OAAO,KACH,wRAE8E,cAAc,gKAG5F,MAAM,KAAI,MAAK,OAAO,GAAG,CAAC,CAAC,KAAK,IAAI,IAAI,IAC5C;AACJ;;AAGA,SAAS,sBAAsB,MAAiC;CAC5D,QAAQ,KAAK,MAAb;EACI,KAAK,OACD,OAAO,uBAAuB,KAAK,GAAG;EAC1C,KAAK;EACL,KAAK,MACD,OAAO,KAAK,SAAS,KAAK,qBAAqB;EACnD,KAAK,OACD,OAAO,sBAAsB,KAAK,OAAO;EAC7C,KAAK,YACD,OAAO,sBAAsB,KAAK,KAAK;EAC3C,SACI,OAAO;CACf;AACJ;;;;;;;;;;;;;AAcA,eAAsB,sBAClB,KACA,aAGA,cAAsB,kBACT;CACb,MAAM,yBAAS,IAAI,IAAsB;CACzC,KAAK,MAAM,cAAc,aACrB,KAAK,MAAM,QAAQ,WAAW,iBAAiB,CAAC,GAC5C,KAAK,MAAM,QAAQ,KAAK,WAAW,CAAC,GAAG;EACnC,IAAI,SAAS,UAAU;EACvB,OAAO,IAAI,MAAM,CAAC,GAAI,OAAO,IAAI,IAAI,KAAK,CAAC,GAAI,WAAW,QAAQ,WAAW,CAAC;CAClF;CAGR,IAAI,OAAO,SAAS,GAAG;CAEvB,MAAM,QAAQ,CAAC,GAAG,OAAO,KAAK,CAAC,CAAC,CAAC,KAAK,MAAM,IAAI,EAAE,QAAQ,MAAM,IAAI,EAAE,EAAE,CAAC,CAAC,KAAK,GAAG;CAElF,MAAM,OAAO,MAAM,IAAI;;kDADI,YAAY,QAAQ,MAAM,IAGP,EAAmB;;8BAEvC,MAAM;KAC/B;CAED,MAAM,YAAY,IAAI,IAAI,KAAK,KAAK,QAAQ,CAAC,OAAO,IAAI,IAAI,GAAG,IAAI,cAAc,IAAI,CAAC,CAAC;CACvF,MAAM,WAAqB,CAAC;CAE5B,KAAK,MAAM,CAAC,MAAM,UAAU,QAAQ;EAChC,IAAI,UAAU,IAAI,IAAI,MAAM,MAAM;EAElC,MAAM,MAAM,UAAU,IAAI,IAAI,IACxB,IAAI,YAAY,2BAChB;EACN,MAAM,WAAW,yBAAyB;EAC1C,MAAM,OAAO,WACP,IAAI,KAAK,SAAS,SAAS,6EAA6E,SAAS,iBAAiB,UAAU,KAAK,gEACjJ,0BAA0B,KAAK,MAAM,YAAY;EAEvD,SAAS,KACL,kBAAkB,KAAK,QAAQ,MAAM,KAAK,IAAI,EAAE,KAAK,IAAI,SAAS,MACtE;CACJ;CAEA,IAAI,SAAS,SAAS,GAClB,MAAM,IAAI,MACN,mFACI,YAAY,wIAEhB,SAAS,KAAK,MAAM,IAAI,IAC5B;AAER"}
|
package/dist/src-BBFsDaeA.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"src-BBFsDaeA.js","names":[],"sources":["../../types/src/types/filter-operators.ts","../../types/src/types/data_source.ts","../../types/src/types/collections.ts","../../types/src/types/rls-functions.ts"],"sourcesContent":["/**\n * Canonical filter operators and REST wire-format mappings.\n *\n * `WhereFilterOp` is THE operator type used at every layer — from React\n * components through the SDK, server, and down to the database driver.\n *\n * PostgREST short-codes (`eq`, `gt`, `cs`, …) exist **only** at the\n * HTTP wire boundary, handled by `serializeFilter` / `deserializeFilter`\n * in `@rebasepro/common`.\n *\n * ┌──────────────────────┬───────────────┬──────────────────────────────┐\n * │ Canonical │ REST short │ Meaning │\n * ├──────────────────────┼───────────────┼──────────────────────────────┤\n * │ \"==\" │ \"eq\" │ Equal │\n * │ \"!=\" │ \"neq\" │ Not equal │\n * │ \">\" │ \"gt\" │ Greater than │\n * │ \">=\" │ \"gte\" │ Greater than or equal │\n * │ \"<\" │ \"lt\" │ Less than │\n * │ \"<=\" │ \"lte\" │ Less than or equal │\n * │ \"in\" │ \"in\" │ Value in list │\n * │ \"not-in\" │ \"nin\" │ Value not in list │\n * │ \"array-contains\" │ \"cs\" │ Array contains element │\n * │ \"array-contains-any\" │ \"csa\" │ Array contains any of │\n * │ \"like\" │ \"like\" │ SQL LIKE (case-sensitive) │\n * │ \"ilike\" │ \"ilike\" │ SQL ILIKE (case-insensitive) │\n * │ \"not-like\" │ \"nlike\" │ NOT LIKE (case-sensitive) │\n * │ \"not-ilike\" │ \"nilike\" │ NOT ILIKE (case-insensitive) │\n * │ \"is-null\" │ \"isnull\" │ Field IS NULL │\n * │ \"is-not-null\" │ \"notnull\" │ Field IS NOT NULL │\n * └──────────────────────┴───────────────┴──────────────────────────────┘\n *\n * Pattern matching (`like`/`ilike`) uses SQL wildcard syntax: `%` matches any\n * sequence of characters, `_` matches a single character. On MongoDB these are\n * translated to anchored regular expressions; Firestore has no native pattern\n * matching and rejects these operators (use `searchString` instead).\n *\n * @module\n */\n\n/**\n * Canonical sort representation: `[fieldName, direction]`.\n *\n * Used in `FindParams.orderBy`, `collection.sort`, and `FilterPreset.sort`.\n * The colon-string form (`\"field:direction\"`) exists only at the HTTP wire\n * boundary, handled by `serializeOrderBy` / `deserializeOrderBy` in\n * `@rebasepro/common`.\n *\n * @group Models\n */\nexport type OrderByTuple<Key extends string = string> = [Key, \"asc\" | \"desc\"];\n\n/**\n * One sort key, or several applied in order of significance.\n *\n * ```ts\n * orderBy: [\"created_at\", \"desc\"] // one key\n * orderBy: [[\"roles\", \"asc\"], [\"created_at\", \"desc\"]] // roles, then newest first\n * ```\n *\n * The two forms are told apart by whether the first element is itself an\n * array, so a single tuple never needs wrapping and every existing caller\n * keeps working unchanged. `normalizeOrderBy` in `@rebasepro/common` collapses\n * both to the list form, which is what every layer below the call site speaks.\n *\n * Ties on the last key are broken by the row id, so a multi-key sort is a\n * total order and pages over it neither repeat nor skip rows.\n *\n * @group Models\n */\nexport type OrderBySpec<Key extends string = string> =\n | OrderBySortTuple<Key>\n | OrderBySortTuple<Key>[];\n\n/**\n * A sort key: a field name, or an aggregate over a to-many relation.\n *\n * @group Models\n */\nexport type SortKey<Key extends string = string> = Key | RelationAggregateSort;\n\n/**\n * `[sortKey, direction]` — the authoring form of {@link OrderByTuple}, which\n * additionally accepts a {@link RelationAggregateSort} object.\n *\n * The object never reaches a driver: `normalizeOrderBy` in `@rebasepro/common`\n * encodes it to its string spelling on the way down, and everything below that\n * point speaks plain `OrderByTuple`. See {@link RelationAggregateSort} for why\n * the wire form is a string.\n *\n * @group Models\n */\nexport type OrderBySortTuple<Key extends string = string> = [SortKey<Key>, \"asc\" | \"desc\"];\n\n/**\n * The aggregate functions a relation sort can apply.\n *\n * Five, and no `array_agg`/`string_agg`: an aggregate used as a sort key has to\n * produce something with an order, and these are the ones that do.\n *\n * @group Models\n */\nexport type RelationAggregateFn = \"min\" | \"max\" | \"count\" | \"sum\" | \"avg\";\n\n/**\n * Order rows by an aggregate over the rows a to-many relation reaches —\n * \"candidates, oldest waiting first\", \"clients, busiest first\".\n *\n * ```ts\n * // The date of each candidate's earliest open application.\n * orderBy: [[{ relation: \"applications\", field: \"created_at\", agg: \"min\" }, \"asc\"]]\n *\n * // How many applications each candidate has.\n * orderBy: [[{ relation: \"applications\", agg: \"count\" }, \"desc\"]]\n * ```\n *\n * This is the half of a queue that cannot be worked around client-side. A\n * *filter* over a relation can be approximated by denormalising a flag onto the\n * row; an *ordering* cannot be approximated at all once the result set is\n * paged, because the client only ever holds one page and the page was chosen by\n * the wrong order.\n *\n * Rows the relation reaches nothing from sort last ascending and first\n * descending — the placement Postgres gives a `NULL`, stated rather than\n * inherited, because the keyset comparison behind cursor paging has to agree\n * with it exactly. Ties are broken by the row id, so the order is total and\n * paging over it neither repeats nor skips.\n *\n * Compiled by the driver into a correlated subquery, so it is subject to the\n * reader's own row-level security on the target table: a related row the reader\n * cannot see does not contribute to the aggregate. Offered only where\n * {@link DataSourceCapabilities.relationAggregateSorts} says the driver can\n * compile it.\n *\n * @group Models\n */\nexport interface RelationAggregateSort {\n /** The to-many relation to aggregate over, by its name on this collection. */\n relation: string;\n\n /** The aggregate to apply. */\n agg: RelationAggregateFn;\n\n /**\n * The column of the *target* to aggregate. Required by every function\n * except `count`, which counts the related rows themselves when it is\n * omitted — and counts the rows whose column is non-null when it is not.\n */\n field?: string;\n}\n\n/** The wire spelling of a {@link RelationAggregateSort}: `min(applications.created_at)`. */\nconst RELATION_AGGREGATE_SORT_PATTERN = /^(min|max|count|sum|avg)\\(([^().]+)(?:\\.([^()]+))?\\)$/;\n\n/**\n * A {@link RelationAggregateSort} as a single string — `min(applications.created_at)`,\n * `count(applications)`.\n *\n * The wire form is a string because every layer below the call site already is\n * one: `OrderByTuple` is `[string, direction]`, the REST parameter is\n * `?orderBy=key:direction`, the driver contract takes `orderBy?: string |\n * OrderByTuple[]`, and a cursor names its keys by string. `_score` established\n * the same pattern — a sort key that is not a column, spelled as one — and this\n * reuses it rather than widening five signatures to carry an object that would\n * be flattened at the end anyway.\n *\n * SQL's own spelling, so the key reads as what it compiles to. Neither `:` nor\n * `,` appears in it, which is what keeps it safe in the colon-delimited wire\n * shorthand.\n *\n * @group Models\n */\nexport function encodeRelationAggregateSort(sort: RelationAggregateSort): string {\n return `${sort.agg}(${sort.relation}${sort.field ? `.${sort.field}` : \"\"})`;\n}\n\n/**\n * Read the string spelling back, or `undefined` if it is not one.\n *\n * `undefined` rather than a throw: this is asked of *every* sort key to find\n * out which kind it is, and an ordinary column name is not an error.\n *\n * @group Models\n */\nexport function parseRelationAggregateSort(key: string): RelationAggregateSort | undefined {\n const match = RELATION_AGGREGATE_SORT_PATTERN.exec(key);\n if (!match) return undefined;\n const [, agg, relation, field] = match;\n // `min()` and friends have nothing to aggregate without a column, and a\n // key that parses to a half-built sort would resolve to no expression and\n // be dropped — leaving the rows unsorted while the caller believes\n // otherwise. `count` is the one function that means something on its own.\n if (!field && agg !== \"count\") return undefined;\n return { agg: agg as RelationAggregateFn, relation, ...(field && { field }) };\n}\n\n/** Is this sort key the object form rather than a field name? */\nexport function isRelationAggregateSort(key: unknown): key is RelationAggregateSort {\n return typeof key === \"object\" && key !== null &&\n typeof (key as RelationAggregateSort).relation === \"string\" &&\n typeof (key as RelationAggregateSort).agg === \"string\";\n}\n\n/** A sort key in the single-string form every layer below the call site speaks. */\nexport function sortKeyToString(key: SortKey): string {\n return isRelationAggregateSort(key) ? encodeRelationAggregateSort(key) : key;\n}\n\n/**\n * Canonical filter operators supported across all database backends.\n * Each DB driver translates these to its native query format.\n *\n * @group Models\n */\nexport type WhereFilterOp =\n | \"<\"\n | \"<=\"\n | \"==\"\n | \"!=\"\n | \">=\"\n | \">\"\n | \"array-contains\"\n | \"in\"\n | \"not-in\"\n | \"array-contains-any\"\n | \"like\"\n | \"ilike\"\n | \"not-like\"\n | \"not-ilike\"\n | \"is-null\"\n | \"is-not-null\";\n\n/**\n * Used to define filters applied in collections.\n *\n * A single condition is a tuple `[operator, value]`.\n * Multiple conditions on the same field use an array of tuples.\n *\n * @example\n * // Single condition per field\n * { status: [\"==\", \"active\"], price: [\">=\", 9.99] }\n *\n * // Multiple conditions on one field\n * { age: [[\">=\", 18], [\"<\", 65]] }\n *\n * // Array operators\n * { role: [\"in\", [\"admin\", \"editor\"]] }\n * { tags: [\"array-contains\", \"featured\"] }\n *\n * // Pattern matching (SQL wildcards: % and _)\n * { name: [\"ilike\", \"%john%\"] }\n * { slug: [\"like\", \"post-%\"] }\n *\n * // Null checks (the value is ignored; `null` is conventional)\n * { deleted_at: [\"is-null\", null] }\n * { published_at: [\"is-not-null\", null] }\n *\n * @group Models\n */\nexport type FilterValues<Key extends string> =\n Partial<Record<Key, [WhereFilterOp, unknown] | [WhereFilterOp, unknown][]>>;\n\n/**\n * The field names a query may address on a row type: every column, plus a\n * dotted path reaching inside one — or *through a relation* to a column of the\n * related row.\n *\n * A dotted path is not checked at all, in either direction. That is a\n * deliberate loosening, and it is worth being exact about what it costs. The\n * root used to be checked: `\"meta.tag\"` required a `meta` column. It cannot\n * stay checked, because the other thing a dotted path now means is\n * `\"applications.status\"` — and `applications` is a *relation*, which comes\n * from the collection's `relations` and is not a column of `M` at all. There is\n * nothing in a generated row type that could validate one. `FindParams.include`\n * is `string[]` for exactly this reason and says so.\n *\n * So the guarantee moves rather than disappears: an unresolvable path is a 400\n * from the driver, not a silently dropped condition. See\n * `UnknownFilterFieldsMode` in `@rebasepro/server-postgres` — dropping a filter\n * key *widens* the read to every row, which is why that resolution fails\n * closed. A typo'd relation path is refused at runtime with the target\n * collection's real column list in the message.\n *\n * Undotted keys are unaffected and still checked against `keyof M`.\n *\n * When `M` is left at its default `Record<string, unknown>`, `keyof M` is\n * `string` and this collapses to `string`, so every query stays permissive.\n * That is what keeps an untyped `createRebaseClient()` behaving exactly as it\n * did before the row type was threaded through.\n *\n * @group Models\n */\nexport type FieldPath<M extends Record<string, unknown> = Record<string, unknown>> =\n | Extract<keyof M, string>\n | `${string}.${string}`;\n\n/**\n * Relaxed filter type that also accepts pre-serialized PostgREST strings.\n * **Internal only** — used at the wire-format boundary\n * (`serializeFilter` / `deserializeFilter` in `@rebasepro/common`).\n *\n * Application code, UI components, and SDK consumers should use\n * {@link FilterValues} instead.\n *\n * @internal\n */\nexport type WireFilterValues<Key extends string> =\n Partial<Record<Key, [WhereFilterOp, unknown] | [WhereFilterOp, unknown][] | string>>;\n\n/**\n * A pre-defined filter preset for quick access in the collection toolbar.\n * Users can select a preset to instantly apply a set of filters and\n * optionally a sort order.\n *\n * @group Models\n */\nexport interface FilterPreset<Key extends string = string> {\n /**\n * Display label shown in the preset menu.\n * If omitted, a summary is auto-generated from the filter keys.\n */\n label?: string;\n\n /**\n * The filter values to apply when this preset is selected.\n */\n filterValues: FilterValues<Key>;\n\n /**\n * Optional sort override to apply alongside the filter values.\n * One key, or several in order of significance.\n */\n sort?: OrderBySpec<Key>;\n}\n\n/**\n * PostgREST short-code operators. Wire format only — these never appear\n * in application code. Used by `serializeFilter`/`deserializeFilter`\n * in `@rebasepro/common`.\n */\nexport type RestFilterOp =\n | \"eq\" | \"neq\"\n | \"gt\" | \"gte\"\n | \"lt\" | \"lte\"\n | \"in\" | \"nin\"\n | \"cs\" | \"csa\"\n | \"like\" | \"ilike\"\n | \"nlike\" | \"nilike\"\n | \"isnull\" | \"notnull\";\n\n/** Maps canonical operators to their REST short-code equivalents. */\nexport const CANONICAL_TO_REST: Readonly<Record<WhereFilterOp, RestFilterOp>> = {\n \"==\": \"eq\",\n \"!=\": \"neq\",\n \">\": \"gt\",\n \">=\": \"gte\",\n \"<\": \"lt\",\n \"<=\": \"lte\",\n \"in\": \"in\",\n \"not-in\": \"nin\",\n \"array-contains\": \"cs\",\n \"array-contains-any\": \"csa\",\n \"like\": \"like\",\n \"ilike\": \"ilike\",\n \"not-like\": \"nlike\",\n \"not-ilike\": \"nilike\",\n \"is-null\": \"isnull\",\n \"is-not-null\": \"notnull\"\n};\n\n/** Maps REST short-code operators to their canonical equivalents. */\nexport const REST_TO_CANONICAL: Readonly<Record<RestFilterOp, WhereFilterOp>> = {\n \"eq\": \"==\",\n \"neq\": \"!=\",\n \"gt\": \">\",\n \"gte\": \">=\",\n \"lt\": \"<\",\n \"lte\": \"<=\",\n \"in\": \"in\",\n \"nin\": \"not-in\",\n \"cs\": \"array-contains\",\n \"csa\": \"array-contains-any\",\n \"like\": \"like\",\n \"ilike\": \"ilike\",\n \"nlike\": \"not-like\",\n \"nilike\": \"not-ilike\",\n \"isnull\": \"is-null\",\n \"notnull\": \"is-not-null\"\n};\n\n/**\n * Operators that test for null/not-null and therefore ignore their value.\n * Codecs normalize the value of these conditions to `null`.\n */\nexport const NULL_OPS: ReadonlySet<WhereFilterOp> = new Set<WhereFilterOp>([\n \"is-null\", \"is-not-null\"\n]);\n\n/**\n * Every canonical operator, in a stable order. Useful for engine capability\n * declarations ({@link DataSourceCapabilities.filterOperators}) and for\n * building operator subsets.\n * @group Models\n */\nexport const ALL_WHERE_FILTER_OPS: readonly WhereFilterOp[] = [\n \"<\", \"<=\", \"==\", \"!=\", \">=\", \">\",\n \"in\", \"not-in\",\n \"array-contains\", \"array-contains-any\",\n \"like\", \"ilike\", \"not-like\", \"not-ilike\",\n \"is-null\", \"is-not-null\"\n];\n\n/** All canonical operator strings for runtime validation. */\nconst CANONICAL_OPS: ReadonlySet<string> = new Set<WhereFilterOp>(ALL_WHERE_FILTER_OPS);\n\n/**\n * The REST table as a `Map`, because the key `toCanonicalOp` is handed comes\n * off the wire.\n *\n * Indexed as a plain object, every `Object.prototype` member answered:\n * `toCanonicalOp(\"valueOf\")` returned the inherited *function* as though it\n * were a `WhereFilterOp`, and every caller here treats a defined result as\n * \"known operator\". Same defect the REST codec's own lookup tables were\n * converted away from in `filter-dialect.ts`; this is the copy that survived\n * one package over, and it now sits under the operator validation the REST\n * parser does, which would otherwise have admitted `[\"constructor\", x]`.\n */\nconst REST_OP_LOOKUP: ReadonlyMap<string, WhereFilterOp> = new Map<string, WhereFilterOp>(\n Object.entries(REST_TO_CANONICAL) as [string, WhereFilterOp][]\n);\n\n/**\n * Resolve any operator string (canonical or REST short-code) to its\n * canonical `WhereFilterOp` form. Returns `undefined` for unknown operators.\n *\n * @example\n * toCanonicalOp(\"==\") // \"==\"\n * toCanonicalOp(\"eq\") // \"==\"\n * toCanonicalOp(\"cs\") // \"array-contains\"\n * toCanonicalOp(\"xyz\") // undefined\n */\nexport function toCanonicalOp(op: string): WhereFilterOp | undefined {\n if (CANONICAL_OPS.has(op)) return op as WhereFilterOp;\n return REST_OP_LOOKUP.get(op);\n}\n","import { ALL_WHERE_FILTER_OPS, WhereFilterOp } from \"./filter-operators\";\n\n/**\n * Describes the capabilities and features supported by a data source (driver).\n *\n * Each driver (Postgres, Firebase, MongoDB, etc.) declares which features it\n * supports. The admin uses this descriptor to:\n * - Show/hide editor tabs (e.g. Relations for SQL, Subcollections for Firebase)\n * - Filter the property type picker (e.g. `relation` for SQL, `reference` for Firebase)\n * - Toggle driver-specific form controls (e.g. `columnType` for SQL)\n *\n * @group Models\n */\nexport interface DataSourceCapabilities {\n /** Unique driver key (e.g. \"postgres\", \"firestore\", \"mongodb\") */\n key: string;\n\n /** Human-readable label for the UI (e.g. \"PostgreSQL\", \"Firebase / Firestore\") */\n label: string;\n\n // ── Feature flags ─────────────────────────────────────────────────\n /** Does this source support SQL-style relations (JOINs)? */\n supportsRelations: boolean;\n\n /** Does this source support nested subcollections? */\n supportsSubcollections: boolean;\n\n /** Does this source support Row Level Security policies? */\n supportsRLS: boolean;\n\n /** Does this source support document references (Firebase-style)? */\n supportsReferences: boolean;\n\n /** Does this source support SQL column type annotations? */\n supportsColumnTypes: boolean;\n\n /** Does this source support real-time listeners? */\n supportsRealtime: boolean;\n\n /**\n * Does this source store vectors natively?\n *\n * `VectorProperty` carries a `dimensions` and is pgvector-shaped. It was\n * the one driver-specific property kind with no flag to gate it, so unlike\n * every other field in this descriptor there was not even a runtime answer\n * to appeal to — a Firestore collection could declare an embedding column\n * and no driver would do anything with it.\n */\n supportsVectors: boolean;\n\n /**\n * Canonical filter operators this engine can execute.\n *\n * The admin UI intersects this set with the property-type defaults and\n * any per-property narrowing (`property.ui.filterOperators`) to decide\n * which operators to offer in filter fields — so an engine that cannot\n * run `ilike` (e.g. Firestore) never shows a \"Contains\" filter that\n * would throw at query time.\n */\n filterOperators: readonly WhereFilterOp[];\n\n /**\n * Relation kinds this engine's driver can compile into a filter.\n *\n * Only `belongsTo` puts a column on the row being filtered; the others are\n * answered with a correlated subquery over the junction or the target\n * table, which not every driver can build. An engine with no relations at\n * all declares none.\n *\n * The admin uses this to decide whether a relation column offers a filter\n * control. Offering one an engine cannot answer is not cosmetic: a driver\n * that drops the key it cannot resolve *widens* the read to every row, and\n * one that fails closed answers a control the admin itself put on screen\n * with a 400.\n *\n * Optional, so a third-party driver registered before this existed still\n * compiles. Omitted means {@link DEFAULT_FILTERABLE_RELATION_KINDS} — the\n * one kind that is a plain column comparison, which every relational\n * driver can do. The subquery kinds are a real capability and have to be\n * claimed rather than assumed: assuming them wrongly is the widening.\n */\n filterableRelationKinds?: readonly string[];\n\n /**\n * Can a filter address a *column of the related row* — `applications.status`\n * — rather than only the related row's id?\n *\n * A separate capability from {@link filterableRelationKinds} because it is\n * a separate subquery: the id filter stops at the junction, one of these\n * reaches the target table and compares one of its columns. A driver can\n * do the first and not the second.\n *\n * Optional and defaulting to **false**, for the reason the relation kinds\n * default narrow: an unclaimed capability that the admin assumes is there\n * produces a control whose query the driver answers by dropping the key —\n * and a dropped filter key widens the read to every row.\n *\n * Meaningless without {@link supportsRelations}; a driver with no relations\n * has nothing to reach through.\n */\n supportsRelationFieldFilters?: boolean;\n\n /**\n * Can a sort key be an aggregate over a to-many relation — \"oldest waiting\n * first\", \"busiest first\"?\n *\n * Compiled as a correlated scalar subquery in `ORDER BY`, which a document\n * store cannot express at all. Optional and defaulting to **false**.\n *\n * A wrongly claimed sort capability fails differently from a wrongly\n * claimed filter one, and worse in one respect: a driver that cannot\n * resolve the key drops the `ORDER BY` and answers 200 with rows in\n * whatever order the database pleased, which reads as a sorted list. Paging\n * over that repeats and skips rows.\n */\n relationAggregateSorts?: boolean;\n\n // ── Admin capability flags ───────────────────────────────────────\n /** Does this source support SQL admin operations (SQL editor, EXPLAIN, etc.)? */\n supportsSQLAdmin: boolean;\n\n /** Does this source support document admin operations (aggregation, stats)? */\n supportsDocumentAdmin: boolean;\n\n /** Does this source support schema admin (unmapped tables, table metadata)? */\n supportsSchemaAdmin: boolean;\n}\n\n/**\n * Subset of DataSourceCapabilities containing only feature flags.\n * Useful when you only need to check capabilities without UI metadata.\n * @group Models\n */\nexport type DataSourceFeatures = Omit<DataSourceCapabilities, \"key\" | \"label\">;\n\n/**\n * The default data-source key, used when a collection does not name a\n * `dataSource`. Shared by the frontend router and the backend driver\n * registry so both agree on \"the default database\".\n * @group Models\n */\nexport const DEFAULT_DATA_SOURCE_KEY = \"(default)\";\n\n/**\n * How the *frontend* reaches a data source.\n *\n * - `\"server\"` — through the Rebase backend (the `RebaseClient`). The backend\n * holds the actual database adapter and routes by data-source key. This is\n * the default and covers Postgres, MongoDB, and any other server-mediated\n * engine.\n * - `\"direct\"` — straight from the client to the external backend via its own\n * SDK driver (e.g. Firestore). The Rebase backend is not in the data path.\n * - `\"custom\"` — a developer-supplied {@link DataDriver}, transport unspecified.\n *\n * @group Models\n */\nexport type DataSourceTransport = \"server\" | \"direct\" | \"custom\";\n\n/**\n * Declarative definition of a data source — a named place data lives.\n *\n * Declared once and shared front and back: the frontend uses it to decide\n * transport (client vs direct driver), the backend uses the same `key` to\n * resolve a database adapter, and the editor derives capabilities from\n * `engine`. Collections reference a definition by its `key` via\n * `collection.dataSource`.\n *\n * @group Models\n */\nexport interface DataSourceDefinition {\n /**\n * Unique identifier for this data source. Collections point at it via\n * `dataSource`. Defaults to {@link DEFAULT_DATA_SOURCE_KEY}.\n */\n key: string;\n\n /**\n * The engine backing this data source (e.g. `\"postgres\"`, `\"mongodb\"`,\n * `\"firestore\"`, or a custom id). Determines the\n * {@link DataSourceCapabilities} surfaced in the editor.\n */\n engine: string;\n\n /**\n * How the frontend reaches this source. Optional — when omitted it is\n * inferred: `\"direct\"` if the definition carries a client-side driver,\n * `\"server\"` otherwise.\n */\n transport?: DataSourceTransport;\n\n /**\n * The physical database/schema/Firestore-database within the engine.\n * Threaded to drivers/adapters as the existing `databaseId` runtime\n * parameter. Defaults to the engine's own default.\n */\n databaseId?: string;\n\n /** Human-readable label for the UI. */\n label?: string;\n}\n\n/**\n * The resolved data source for a collection: the single source of truth that\n * the frontend router, backend registry, and editor all derive from.\n * Produced by `resolveDataSource(collection, registry)`.\n *\n * @group Models\n */\nexport interface ResolvedDataSource {\n /** Data-source key (routing key, shared front + back). */\n key: string;\n /** Engine backing the source (drives capabilities). */\n engine: string;\n /** Frontend transport. */\n transport: DataSourceTransport;\n /** Within-engine instance, if any (the `databaseId` runtime param). */\n databaseId?: string;\n /** Capabilities derived from {@link engine}. */\n capabilities: DataSourceCapabilities;\n}\n\n/**\n * Relation kinds assumed filterable when a driver does not say.\n *\n * `belongsTo` alone: its filter is a comparison on a column of the row being\n * filtered, the one shape that needs no query construction a driver might not\n * have. Everything else is a correlated subquery over another table.\n *\n * @group Models\n */\nexport const DEFAULT_FILTERABLE_RELATION_KINDS: readonly string[] = [\"belongsTo\"];\n\n// ── Built-in driver capabilities ─────────────────────────────────────\n\n/** @group Models */\nexport const POSTGRES_CAPABILITIES: DataSourceCapabilities = {\n key: \"postgres\",\n label: \"PostgreSQL\",\n supportsRelations: true,\n supportsSubcollections: false,\n supportsRLS: true,\n supportsReferences: false,\n supportsColumnTypes: true,\n supportsRealtime: true,\n supportsVectors: true,\n filterOperators: ALL_WHERE_FILTER_OPS,\n // `via` is absent: its join path is authored source → target with no\n // stated inverse, so the driver has nothing to reverse into a filter.\n filterableRelationKinds: [\"belongsTo\", \"manyToMany\", \"hasMany\", \"hasOne\"],\n supportsRelationFieldFilters: true,\n relationAggregateSorts: true,\n supportsSQLAdmin: true,\n supportsDocumentAdmin: false,\n supportsSchemaAdmin: true\n};\n\n/** @group Models */\nexport const FIREBASE_CAPABILITIES: DataSourceCapabilities = {\n key: \"firestore\",\n label: \"Firebase / Firestore\",\n supportsRelations: false,\n supportsSubcollections: true,\n supportsRLS: false,\n supportsReferences: true,\n supportsColumnTypes: false,\n supportsRealtime: true,\n supportsVectors: false,\n // Firestore has no SQL pattern matching — the driver throws on the LIKE\n // family, so the UI must never offer it.\n filterOperators: ALL_WHERE_FILTER_OPS.filter(op =>\n op !== \"like\" && op !== \"ilike\" && op !== \"not-like\" && op !== \"not-ilike\"),\n // No relations at all — a document store links by reference. Nothing to\n // reach through, so neither of the two relation-reaching features either.\n filterableRelationKinds: [],\n supportsRelationFieldFilters: false,\n relationAggregateSorts: false,\n supportsSQLAdmin: false,\n supportsDocumentAdmin: false,\n supportsSchemaAdmin: false\n};\n\n/** @group Models */\nexport const MONGODB_CAPABILITIES: DataSourceCapabilities = {\n key: \"mongodb\",\n label: \"MongoDB\",\n supportsRelations: false,\n supportsSubcollections: true,\n supportsRLS: false,\n supportsReferences: true,\n supportsColumnTypes: false,\n supportsRealtime: false,\n supportsVectors: false,\n filterOperators: ALL_WHERE_FILTER_OPS,\n filterableRelationKinds: [],\n supportsRelationFieldFilters: false,\n relationAggregateSorts: false,\n supportsSQLAdmin: false,\n supportsDocumentAdmin: true,\n supportsSchemaAdmin: true\n};\n\n/**\n * Fallback capabilities when the driver is unknown.\n * Enables everything so nothing is hidden unexpectedly.\n * @group Models\n */\nexport const DEFAULT_CAPABILITIES: DataSourceCapabilities = {\n key: \"(default)\",\n label: \"Default\",\n supportsRelations: true,\n supportsSubcollections: true,\n supportsRLS: true,\n supportsReferences: true,\n supportsColumnTypes: true,\n supportsRealtime: true,\n supportsVectors: true,\n filterOperators: ALL_WHERE_FILTER_OPS,\n // The exception to this descriptor's \"enable everything\" rule. The other\n // flags hide a tab or a picker when they are wrong; this one decides\n // whether a query is sent that an unknown driver may answer by dropping\n // the condition — which returns every row rather than none.\n filterableRelationKinds: DEFAULT_FILTERABLE_RELATION_KINDS,\n // Narrow for the same reason, and more sharply. An unknown driver that is\n // assumed to compile these answers by dropping the key: the filter widens\n // the read to every row, and the sort comes back unordered while looking\n // sorted. Both have to be claimed.\n supportsRelationFieldFilters: false,\n relationAggregateSorts: false,\n supportsSQLAdmin: true,\n supportsDocumentAdmin: true,\n supportsSchemaAdmin: true\n};\n\nconst CAPABILITIES_REGISTRY: Record<string, DataSourceCapabilities> = {\n postgres: POSTGRES_CAPABILITIES,\n firestore: FIREBASE_CAPABILITIES,\n mongodb: MONGODB_CAPABILITIES,\n \"(default)\": DEFAULT_CAPABILITIES\n};\n\n/**\n * Look up capabilities for a given engine key.\n * If `engine` is undefined or not found, returns `DEFAULT_CAPABILITIES`.\n * @group Models\n */\nexport function getDataSourceCapabilities(engine?: string): DataSourceCapabilities {\n if (!engine) return POSTGRES_CAPABILITIES; // postgres is the default engine\n return CAPABILITIES_REGISTRY[engine] ?? DEFAULT_CAPABILITIES;\n}\n\n/**\n * Register custom capabilities for a third-party driver.\n * @group Models\n */\nexport function registerDataSourceCapabilities(capabilities: DataSourceCapabilities): void {\n CAPABILITIES_REGISTRY[capabilities.key] = capabilities;\n}\n","import type { CollectionCallbacks } from \"./entity_callbacks\";\n\nimport type { EnumValues, Properties, PostgresProperties, FirebaseProperties, MongoProperties } from \"./properties\";\n\nimport type { User } from \"../users\";\nimport type { Relation } from \"./relations\";\nimport type { SecurityRule } from \"./security_rules\";\nimport { getDataSourceCapabilities } from \"./data_source\";\nimport type { WhereFilterOp, FilterValues, FilterPreset } from \"./filter-operators\";\nimport type { SearchConfig } from \"./search\";\n\n/**\n * Base interface containing all driver-agnostic collection properties.\n * Use {@link PostgresCollectionConfig} or {@link FirebaseCollectionConfig} for\n * driver-specific type safety, or {@link CollectionConfig} when you\n * need to handle any collection regardless of backend.\n *\n * @group Models\n */\nexport interface BaseCollectionConfig<M extends Record<string, unknown> = Record<string, unknown>, USER extends User = User> {\n\n /**\n * The collection's identity. Required, and the value nearly everything else\n * keys on:\n *\n * - the REST path — `/api/data/<slug>`\n * - the SDK accessor — `client.data.<slug>` / `client.data.collection(\"<slug>\")`\n * - the admin panel's URL\n * - the target of a `reference` or `relation` property\n *\n * Conventionally kebab-case and plural (`blog-posts`). It is independent of\n * {@link table}: the slug is what callers say, the table is where the rows\n * live, and renaming one does not rename the other.\n *\n * Treat it as frozen once anything has shipped against it — changing a slug\n * changes every URL and every generated accessor at once.\n *\n * @example\n * defineCollection({\n * slug: \"blog-posts\", // /api/data/blog-posts, client.data.blogPosts\n * table: \"posts\",\n * properties: { … }\n * })\n */\n slug: string;\n\n /**\n * Name of the collection, typically plural.\n * E.g. `Products`, `Blog`\n */\n name: string;\n\n /**\n * Singular name of an entry in this collection\n * E.g. `Product`, `Blog entry`\n */\n singularName?: string;\n\n /**\n * Optional description of this view. You can use Markdown.\n */\n description?: string;\n\n /**\n * Child collections nested under entities of this collection.\n * Populated automatically during normalization from driver-specific fields\n * (e.g. Firebase `subcollections`, Postgres `relations` with many-cardinality).\n *\n * Custom drivers can set this directly to expose child collections to the UI.\n */\n childCollections?: () => CollectionConfig<Record<string, unknown>>[];\n\n\n /**\n * The data source this collection belongs to — the routing key shared by\n * the frontend router and the backend driver registry. It points at a\n * {@link DataSourceDefinition} registered on `<Rebase dataSources>` (front)\n * and `initializeRebaseBackend({ dataSources })` (back).\n *\n * If not specified, the default data source `\"(default)\"` is used, which\n * for a standard Rebase app is the server-mediated Postgres backend.\n *\n * @example\n * // Default data source (server-mediated Postgres)\n * { slug: \"products\" }\n *\n * // A direct-transport Firestore data source registered as \"analytics\"\n * { slug: \"events\", dataSource: \"analytics\" }\n */\n dataSource?: string;\n\n /**\n * The database engine backing this collection (`\"postgres\"`, `\"firestore\"`,\n * `\"mongodb\"`, or a custom id).\n *\n * On concrete collection types ({@link PostgresCollectionConfig},\n * {@link FirebaseCollectionConfig}, {@link MongoDBCollectionConfig}) this is a literal\n * discriminant. On the base type it is optional and gets stamped\n * automatically during collection normalization from the registered\n * {@link DataSourceDefinition}.\n *\n * Prefer setting {@link dataSource} and letting the engine be resolved.\n */\n engine?: string;\n\n /**\n * Which database within the engine.\n * - For Firestore: The Firestore database ID (e.g., for multi-database projects)\n * - For PostgreSQL: Schema or database name\n * - For MongoDB: Database name\n *\n * If not specified, the default database of the engine is used. Resolved\n * from the collection's {@link DataSourceDefinition} when omitted here.\n */\n databaseId?: string;\n\n /**\n * Set of properties that compose a entity\n */\n properties: Properties;\n\n\n\n\n\n\n\n\n\n\n\n\n /**\n * Mark this collection as an authentication collection.\n * When true, this collection is used for user management, login, password hashing, and invitation flows.\n */\n auth?: boolean | AuthCollectionConfig;\n\n\n\n\n\n\n\n /**\n * Row-level authorization rules for this collection.\n *\n * Driver-agnostic on purpose, unlike `disableDefaultPolicies`, `table` and\n * `relations`, which are declared on {@link PostgresCollectionConfig} only.\n * The rules are a *contract* — who may read or write which rows — and each\n * engine enforces it its own way:\n *\n * - **Postgres** compiles them to real `CREATE POLICY` statements and lets\n * the database enforce them (see {@link PostgresCollectionConfig.securityRules},\n * which narrows this with the raw-SQL details).\n * - **MongoDB** translates them into a query filter it AND-s into every\n * read and write, honouring `access`, `ownerField`, `roles`, `mode` and\n * the `operation`/`operations` selectors, and making a best effort at raw\n * `using`/`withCheck` SQL.\n * - **Firestore** does not implement them at all; its own rules language is\n * evaluated by Google, not from here. `supportsRLS` on\n * {@link DataSourceCapabilities} reports which engines generate policies,\n * which is not the same question as whether an engine honours a rule.\n */\n securityRules?: readonly SecurityRule[];\n\n /**\n * This interface defines all the callbacks that can be used when a entity\n * is being created, updated or deleted.\n * Useful for adding your own logic or blocking the execution of the operation.\n */\n readonly callbacks?: CollectionCallbacks<M, USER>;\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n /**\n * User id of the owner of this collection. This is used only by plugins, or if you\n * are writing custom code\n */\n ownerId?: string;\n\n /**\n * Arbitrary key-value metadata for external consumers.\n * Not interpreted by Rebase — passed through serialization unchanged.\n * Used by domain apps to store custom per-collection config.\n */\n metadata?: Record<string, unknown>;\n\n\n\n\n /**\n * If set to true, changes to the entity will be saved in a subcollection.\n * This prop has no effect if the history plugin is not enabled\n */\n history?: boolean;\n\n /**\n * Whether a write naming a field this collection does not declare is\n * rejected with a 400. Defaults to `true`.\n *\n * Set to `false` where a column really does exist that the config never\n * declared — populated by a trigger, or introspected rather than declared —\n * and callers need to write it. The column still has to exist: the driver\n * checks the key against the table's own columns whatever this is set to,\n * because a key with no column behind it is not passed to the database and\n * refused, it is dropped from the statement and answered 201.\n *\n * It does not let a typo through to Postgres for Postgres to judge. That is\n * what this flag was documented as doing, and no such judgment ever\n * happened.\n */\n strictWrites?: boolean;\n\n\n\n\n\n\n\n\n}\n\n// ── Driver-specific collection types ──────────────────────────────────\n\n/**\n * A collection backed by PostgreSQL (or any SQL database).\n * Adds support for SQL-style relations (JOINs) and Row Level Security.\n *\n * Use this type instead of {@link CollectionConfig} when you want\n * compile-time safety that only SQL-relevant fields appear.\n *\n * @group Models\n */\nexport interface PostgresCollectionConfig<M extends Record<string, unknown> = Record<string, unknown>, USER extends User = User>\n extends BaseCollectionConfig<M, USER> {\n properties: PostgresProperties;\n\n /**\n * The database engine for this collection. For Postgres collections this\n * can be omitted (Postgres is the default) or set to `\"postgres\"`.\n */\n engine?: \"postgres\" | undefined;\n\n /**\n * The PostgreSQL table name for this collection.\n */\n table: string;\n\n /**\n * The PostgreSQL schema name for this table.\n * E.g. \"public\", \"rebase\", \"auth\".\n * If not specified, \"public\" is used (or the default search path).\n */\n schema?: string;\n\n /**\n * For SQL databases, you can define the relations between collections here.\n * Relations describe JOINs, foreign keys, and junction tables.\n */\n relations?: Relation[];\n\n /**\n * Security rules for this collection (PostgreSQL Row Level Security).\n * When defined, the schema generator will enable RLS on the table and\n * create the corresponding PostgreSQL policies.\n *\n * Supports three levels of expressiveness:\n * 1. **Convenience shortcuts** — `ownerField`, `access`, `roles`\n * 2. **Raw SQL** — `using` and `withCheck` for full PostgreSQL power\n * 3. **Combined** — mix shortcuts with `roles` for common patterns\n *\n * The authenticated user context is available in raw SQL via:\n * - `rebase.uid()` — the current user's ID\n * - `rebase.roles()` — comma-separated app role IDs\n * - `rebase.jwt()` — full JWT claims as JSONB\n */\n securityRules?: readonly SecurityRule[];\n\n /**\n * Opt out of the framework's default Row Level Security policies.\n *\n * The schema generator automatically injects, for every collection, a\n * baseline SELECT policy granting the trusted server context and the\n * `admin` role read access (reads run under a restricted role, so RLS\n * default-denies without it). For auth collections it additionally injects\n * a self-read policy (`id = rebase.uid()`) and an admin-only write gate\n * (INSERT/UPDATE/DELETE require the `admin` role or the trusted server\n * context), making privileged columns such as `roles` safe by default.\n *\n * Author-defined `securityRules` are permissive and broaden access on top\n * of these defaults. Set this flag to `true` to remove the defaults\n * entirely and take full responsibility for the collection's RLS.\n *\n * @default false\n */\n disableDefaultPolicies?: boolean;\n\n /**\n * Opt in to Postgres full-text search for this collection.\n *\n * Omit it and `.search()` keeps its existing behaviour exactly — an\n * `ILIKE '%term%'` across top-level string properties. Declare it and the\n * collection gains one generated `tsvector` column and a GIN index, and\n * `.search()` compiles to a ranked `@@ websearch_to_tsquery` against them.\n *\n * Postgres-only, like {@link VectorProperty}: the block is rejected at boot\n * on other engines rather than silently ignored.\n *\n * @see SearchConfig\n */\n search?: SearchConfig;\n}\n\n/**\n * A collection backed by Firebase / Firestore.\n * Adds support for subcollections (nested document collections).\n *\n * Use this type instead of {@link CollectionConfig} when you want\n * compile-time safety that only Firestore-relevant fields appear.\n *\n * @group Models\n */\nexport interface FirebaseCollectionConfig<M extends Record<string, unknown> = Record<string, unknown>, USER extends User = User>\n extends BaseCollectionConfig<M, USER> {\n /**\n * The database engine for this collection. Must be set to `\"firestore\"`.\n */\n engine: \"firestore\";\n\n /**\n * Set of properties that compose a entity.\n * Firestore collections support `reference` properties but not `relation`.\n */\n properties: FirebaseProperties;\n\n /**\n * The Firestore collection path to query. Defaults to `slug` if not set.\n * Use this when the Firestore path differs from the slug\n * (e.g., when a PostgreSQL collection already uses the same slug).\n *\n * @example\n * ```typescript\n * const fsCustomer: FirebaseCollectionConfig = {\n * slug: \"fs_customer\", // URL: /c/fs_customer\n * path: \"customer\", // Firestore path: customer\n * name: \"Customers (Firestore)\",\n * engine: \"firestore\",\n * properties: { ... }\n * };\n * ```\n */\n path?: string;\n\n /**\n * You can add subcollections to your entity in the same way you define the root\n * collections. The collections added here will be displayed when opening\n * the side dialog of a entity.\n */\n subcollections?: () => CollectionConfig<Record<string, unknown>>[];\n}\n\n/**\n * A collection backed by MongoDB.\n *\n * Use this type instead of {@link CollectionConfig} when you want\n * compile-time safety that only MongoDB-relevant fields appear.\n *\n * @group Models\n */\nexport interface MongoDBCollectionConfig<M extends Record<string, unknown> = Record<string, unknown>, USER extends User = User>\n extends BaseCollectionConfig<M, USER> {\n\n /**\n * The database engine for this collection. Must be set to `\"mongodb\"`.\n */\n engine: \"mongodb\";\n\n /**\n * Set of properties that compose a entity.\n * MongoDB collections support `reference` properties but not `relation`.\n */\n properties: MongoProperties;\n\n /**\n * The MongoDB collection name to use. Defaults to `slug` if not set.\n * Use this when the MongoDB collection name differs from the slug\n * (e.g., when a PostgreSQL collection already uses the same slug).\n *\n * @example\n * ```typescript\n * const mongoCustomer: MongoDBCollectionConfig = {\n * slug: \"mongo_customer\", // URL: /c/mongo_customer\n * path: \"customer\", // MongoDB collection: customer\n * name: \"Customers (MongoDB)\",\n * engine: \"mongodb\",\n * properties: { ... }\n * };\n * ```\n */\n path?: string;\n}\n\n/**\n * A collection backed by any data source.\n * This is a discriminated union — use {@link PostgresCollectionConfig},\n * {@link FirebaseCollectionConfig}, or {@link MongoDBCollectionConfig} for\n * driver-specific type safety.\n *\n * @group Models\n */\nexport type CollectionConfig<M extends Record<string, unknown> = Record<string, unknown>, USER extends User = User> =\n | PostgresCollectionConfig<M, USER>\n | FirebaseCollectionConfig<M, USER>\n | MongoDBCollectionConfig<M, USER>;\n\n/**\n * A collection of *any* row type.\n *\n * `CollectionConfig` is **invariant** in `M`: `callbacks` both consumes `M`\n * (`AfterReadProps<M>`) and produces it, so neither direction of assignment\n * holds. `CollectionConfig<SomeRow>` is therefore not assignable to a bare\n * `CollectionConfig`, whose `M` defaults to `Record<string, unknown>`.\n *\n * That matters wherever a collection is merely *referred to* rather than read\n * from. `defineCollection` returns a config whose `M` is inferred from the\n * properties — the whole point of it — so a field typed `() => CollectionConfig`\n * rejects every collection the builder produces, and `target: () => otherCollection`\n * (the documented way to point a relation at its other end) does not compile in\n * any project that uses the builder.\n *\n * `any` is deliberate and is what it is for here: these positions never read the\n * target's rows, they only identify which collection is meant, so there is no\n * type safety to preserve and invariance is pure obstruction.\n *\n * @group Models\n */\nexport type AnyCollectionConfig = CollectionConfig<any, any>;\n\n/**\n * Type guard for PostgreSQL collections.\n * Returns true if the collection uses the Postgres engine (or the default engine).\n *\n * Generic over the *input* type, and narrows by intersection rather than\n * replacement. Narrowing to a bare `PostgresCollectionConfig` discarded whatever\n * the caller actually had — most visibly the admin panel's view model, whose\n * flattened presentation fields vanished the moment a collection passed through\n * one of these guards.\n *\n * @group Models\n */\nexport function isPostgresCollectionConfig<C extends CollectionConfig<any, any>>(\n collection: C\n): collection is C & PostgresCollectionConfig<any, any> {\n return !collection.engine || collection.engine === \"postgres\";\n}\n\n/**\n * Narrows to the SQL collection fields — `table`, `relations`,\n * `disableDefaultPolicies` — by asking the engine's declared capabilities\n * rather than by naming Postgres.\n *\n * The two halves of this already existed and were never joined. The engine\n * split (`PostgresCollectionConfig` / `FirebaseCollectionConfig` /\n * `MongoDBCollectionConfig`) said which fields belong to which engine at the\n * type level; {@link DataSourceCapabilities} said the same thing at runtime,\n * down to a `supportsRelations` flag. So call sites guarded on the capability\n * and then read a field the base type had to declare for them — which is why\n * those fields were on the base, and why a MongoDB collection could be written\n * with a `table`.\n *\n * Prefer this over {@link isPostgresCollectionConfig} wherever the question is\n * \"does this collection live in a SQL table\", so a custom SQL engine\n * registered through `registerDataSourceCapabilities` is included.\n *\n * @group Models\n */\nexport function isRelationalCollectionConfig<C extends CollectionConfig<any, any>>(\n collection: C\n): collection is C & PostgresCollectionConfig<any, any> {\n return getDataSourceCapabilities(collection.engine).supportsRelations;\n}\n\n/**\n * Type guard for Firebase / Firestore collections.\n * @group Models\n */\nexport function isFirebaseCollectionConfig<C extends CollectionConfig<any, any>>(\n collection: C\n): collection is C & FirebaseCollectionConfig<any, any> {\n return collection.engine === \"firestore\";\n}\n\n/**\n * Type guard for MongoDB collections.\n * @group Models\n */\nexport function isMongoDBCollectionConfig<C extends CollectionConfig<any, any>>(\n collection: C\n): collection is C & MongoDBCollectionConfig<any, any> {\n return collection.engine === \"mongodb\";\n}\n\n/**\n * Returns the data path for a collection.\n * For Firestore or MongoDB collections with a `path`, returns that value;\n * otherwise falls back to `slug`.\n */\nexport function getCollectionDataPath<M extends Record<string, unknown> = Record<string, unknown>, USER extends User = User>(\n collection: CollectionConfig<M, USER>\n): string {\n if (isFirebaseCollectionConfig(collection) && collection.path) {\n return collection.path;\n }\n if (isMongoDBCollectionConfig(collection) && collection.path) {\n return collection.path;\n }\n return collection.slug;\n}\n\n/**\n * Reads a collection's driver-declared subcollections thunk (the `subcollections`\n * field) independent of engine identity, so engine-agnostic code doesn't have to\n * type-guard against a specific driver. Returns `undefined` when the collection\n * declares none.\n *\n * Pair with `getDataSourceCapabilities(engine).supportsSubcollections` to decide\n * whether the engine honours subcollections at all before reading them.\n * @group Models\n */\nexport function getDeclaredSubcollections<M extends Record<string, unknown> = Record<string, unknown>, USER extends User = User>(\n collection: CollectionConfig<M, USER>\n): (() => CollectionConfig<Record<string, unknown>>[]) | undefined {\n return (collection as FirebaseCollectionConfig<M, USER>).subcollections;\n}\n\n/**\n * Where the rows in an {@link EntityChildView} come from.\n *\n * The two are not the same thing, and conflating them is what made a Postgres\n * relation borrow Firestore's addressing:\n *\n * - `subcollection` is **containment**. The rows live under the parent; the\n * path is their identity, and they cannot exist without it. This is what\n * Firestore has natively.\n * - `relation` is a **link**. The rows are an ordinary collection, narrowed to\n * those the parent reaches. `owned` means the child carries the parent's\n * foreign key and belongs to it alone; `linked` means the row is shared\n * through a junction, so what the parent controls is the link, not the row.\n *\n * @group Models\n */\nexport type ChildViewSource =\n | { kind: \"subcollection\" }\n | {\n kind: \"relation\";\n relationKey: string;\n mode: \"owned\" | \"linked\";\n /**\n * Slug of the collection the rows actually live in.\n *\n * Distinct from the view's `key`, which is the relation. A `linked` view\n * needs both: the key addresses the parent's set, and this addresses the\n * whole collection to pick an existing row out of.\n */\n targetSlug: string;\n };\n\n/**\n * A list of rows rendered inside an entity view — the tab under a record.\n *\n * This is a *presentation* descriptor, which is the whole point: rendering a\n * related list as a tab used to require minting a child `CollectionConfig` with\n * its own slug, which dragged a URL grammar, a path resolver and a second\n * read/write pipeline along with it. A tab needs a key, a collection to list,\n * and to know where its rows come from.\n *\n * @group Models\n */\nexport interface EntityChildView<M extends Record<string, unknown> = Record<string, unknown>> {\n /**\n * Stable identifier for this view: the tab id and the path segment.\n *\n * For a relation this is the **relation key** — the name the backend\n * resolves a nested path segment by — not the target collection's slug.\n * Those differ whenever a relation is named, which is every inline relation\n * property, and the mismatch is why such a tab used to open onto an error.\n */\n key: string;\n\n /** The collection whose rows this view lists, with any overrides applied. */\n collection: CollectionConfig<M>;\n\n source: ChildViewSource;\n}\n\n\nexport type { WhereFilterOp, FilterValues, WireFilterValues, FilterPreset } from \"./filter-operators\";\n\n\nexport type InferCollectionConfigType<S extends CollectionConfig> = S extends CollectionConfig<infer M> ? M : never;\n\n/**\n * Configuration for authentication collections.\n *\n * Controls what happens when admins create users, reset passwords,\n * and which entity actions are auto-injected.\n *\n * Use `auth: true` as sugar for `{ enabled: true }` with all defaults.\n *\n * @example Override user creation\n * ```ts\n * auth: {\n * enabled: true,\n * onCreateUser: async (values, ctx) => {\n * const hash = await ctx.hashPassword(\"welcome123\");\n * return {\n * values: { ...values, passwordHash: hash, emailVerified: true },\n * temporaryPassword: \"welcome123\",\n * };\n * },\n * }\n * ```\n *\n * @example Disable the reset-password entity action\n * ```ts\n * auth: {\n * enabled: true,\n * actions: { resetPassword: false },\n * }\n * ```\n *\n * @group Models\n */\nexport interface AuthCollectionConfig {\n /** Set to true to mark this collection as the authentication collection. */\n enabled: boolean;\n\n /**\n * Called when an admin creates a user via the collection REST API.\n *\n * Default: generate password → hash → normalize email → save →\n * send invitation email (or return temp password if no email configured).\n *\n * Override to implement custom invitation flows, LDAP sync, etc.\n */\n onCreateUser?: (\n values: Record<string, unknown>,\n ctx: AuthCollectionContext\n ) => Promise<AuthCollectionCreateResult>;\n\n /**\n * Called when an admin resets a user's password via the admin panel.\n *\n * Default: generate reset token → send email (or generate + return temp password).\n * Override for custom reset flows.\n */\n onResetPassword?: (\n uid: string,\n ctx: AuthCollectionContext\n ) => Promise<AuthCollectionResetResult>;\n\n /**\n * Control which auth-specific entity actions are auto-injected.\n *\n * Default: `{ resetPassword: true }` — the framework auto-injects\n * the built-in `resetPasswordAction` into the collection's entity actions.\n *\n * Set to `false` to disable, or pass a custom `EntityAction` to replace the UI.\n *\n * The object form is an `EntityAction` from `@rebasepro/admin-types`, typed\n * here as `object` because it is a React component with admin controllers in\n * its props and nothing on the server reads it — only whether the built-in\n * action is injected, which is the boolean.\n */\n actions?: {\n resetPassword?: boolean | object;\n };\n}\n\n/**\n * Context provided to collection-level auth hooks.\n *\n * This is a simplified facade over the server internals —\n * it exposes only what's needed for custom auth flows without\n * coupling collection config to internal interfaces.\n *\n * @group Models\n */\nexport interface AuthCollectionContext {\n /** Hash a password using the configured algorithm (scrypt by default). */\n hashPassword: (password: string) => Promise<string>;\n /** Send an email. Only available when email service is configured. */\n sendEmail?: (options: { to: string; subject: string; html: string; text?: string }) => Promise<void>;\n /** Whether the email service is configured and available. */\n emailConfigured: boolean;\n /** The app name from email config (for templates). */\n appName: string;\n /** The base URL for password reset links. */\n resetPasswordUrl: string;\n}\n\n/**\n * Result of a collection-level `onCreateUser` hook.\n * @group Models\n */\nexport interface AuthCollectionCreateResult {\n /** Processed values to persist (must include passwordHash, NOT raw password). */\n values: Record<string, unknown>;\n /** If set, shown to the admin in the creation result dialog. */\n temporaryPassword?: string;\n /** Whether an invitation email was sent. */\n invitationSent?: boolean;\n}\n\n/**\n * Result of a collection-level `onResetPassword` hook.\n * @group Models\n */\nexport interface AuthCollectionResetResult {\n /** If set, shown to the admin. */\n temporaryPassword?: string;\n /** Whether a reset email was sent. */\n invitationSent?: boolean;\n}\n","/**\n * The SQL helper functions RLS policies call, and the schema they live in.\n *\n * ## One schema, and it is ours\n *\n * Rebase creates exactly one schema in a project's database: `rebase`. These\n * three functions live in it alongside the framework's own tables, and that is\n * the whole contract — a reader can look at a database and know precisely which\n * namespace belongs to the framework and that nothing else was touched.\n *\n * It used to be two. `uid()`, `jwt()` and `roles()` sat in a schema called\n * `auth`, which is Supabase's name, chosen so that a developer who had written\n * Supabase RLS would recognise `auth.uid()`. The familiarity was real but the\n * name was not Rebase's to take, and taking it had a concrete cost: pointing\n * Rebase at a database that already had a Supabase `auth` schema meant\n * `CREATE OR REPLACE FUNCTION auth.uid() RETURNS text` against Supabase's\n * `RETURNS uuid`, which Postgres rejects outright —\n *\n * ERROR: cannot change return type of existing function\n * HINT: Use DROP FUNCTION auth.uid() first.\n *\n * — and the failure landed inside a catch-all that logged a warning and carried\n * on, leaving a database with auth tables, no helper functions, and policies\n * calling functions that did not exist. Under `rebase db migrate` the same\n * statements aborted the migration instead.\n *\n * `rebase.uid()` collides with nobody. A Supabase database keeps its `auth`\n * schema untouched and gains a `rebase` one, which is what a gradual migration\n * needs.\n *\n * ## Why functions at all, rather than inlining `current_setting`\n *\n * Because the indirection has already been spent once. `uid()` resolves\n * `app.uid` and falls back to the pre-rename `app.user_id`, so that during a\n * rolling deploy — old and new pods serving one database — both eras resolve\n * the principal. That was a single `CREATE OR REPLACE`. Inlined into policy\n * bodies it would have been a rewrite of every policy on every table.\n *\n * ## Why the name is not configurable\n *\n * A policy body is stored SQL: Postgres parses `USING (…)` once and keeps it, so\n * these strings are written into every policy in every database Rebase has\n * provisioned. Everything that reads policies back — the SQL-to-policy parser\n * behind the admin UI, the drift checker, `rls-check` — would have to know the\n * configured value to recognise its own output. One frozen name is the feature.\n */\n\n/** The schema Rebase owns. The only schema Rebase creates. */\nexport const REBASE_SCHEMA = \"rebase\";\n\n/**\n * The principal of the current request, as text, or NULL in the server context.\n *\n * Never NULL for a user request — an anonymous one carries\n * {@link ANONYMOUS_USER_ID} — which is what makes `IS NULL` a reliable test for\n * the trusted server plane and `IS NOT NULL` a tautology.\n */\nexport const RLS_UID_SQL = `${REBASE_SCHEMA}.uid()`;\n\n/** The request's roles as a comma-separated string, for `string_to_array`. */\nexport const RLS_ROLES_SQL = `${REBASE_SCHEMA}.roles()`;\n\n/** The request's JWT claims as `jsonb`, or `{}`. */\nexport const RLS_JWT_SQL = `${REBASE_SCHEMA}.jwt()`;\n\n/**\n * The pre-1.0 spellings, for recognising policies and hand-written SQL that\n * predate the move.\n *\n * Kept because policies outlive the server that wrote them: a database migrated\n * by an older release still holds `auth.uid()` in its policy bodies until the\n * next push or boot recompiles them, and anything that reads policies back has\n * to recognise both eras or report the framework's own output as foreign drift.\n * Also used to give a project whose `securityRules` contain raw `auth.uid()` a\n * message naming the replacement, instead of a parse failure.\n */\nexport const LEGACY_RLS_SCHEMA = \"auth\";\nexport const LEGACY_RLS_UID_SQL = `${LEGACY_RLS_SCHEMA}.uid()`;\nexport const LEGACY_RLS_ROLES_SQL = `${LEGACY_RLS_SCHEMA}.roles()`;\nexport const LEGACY_RLS_JWT_SQL = `${LEGACY_RLS_SCHEMA}.jwt()`;\n\n/**\n * Rewrites the pre-1.0 function calls in a fragment of policy SQL.\n *\n * Deliberately anchored on a word boundary and the schema qualifier, so a column\n * called `auth_uid` or a table named `auth` is left alone.\n */\nexport function rewriteLegacyRlsFunctions(sql: string): string {\n return sql.replace(\n /\\bauth\\.(uid|jwt|roles)\\s*\\(\\s*\\)/gi,\n (_match, fn: string) => `${REBASE_SCHEMA}.${fn.toLowerCase()}()`\n );\n}\n\n/** Whether a fragment of SQL still calls the pre-1.0 functions. */\nexport function usesLegacyRlsFunctions(sql: string): boolean {\n return /\\bauth\\.(uid|jwt|roles)\\s*\\(\\s*\\)/i.test(sql);\n}\n"],"mappings":"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"}
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Table Classification Utility
|
|
3
|
-
*
|
|
4
|
-
* Re-exports shared classification logic from @rebasepro/common.
|
|
5
|
-
* This module exists for backward compatibility — prefer importing directly
|
|
6
|
-
* from @rebasepro/common in new code.
|
|
7
|
-
*/
|
|
8
|
-
export { type TableCategory, REBASE_INTERNAL_SCHEMAS, REBASE_INTERNAL_PREFIXES, classifyTable, isRebaseInternalTable, detectJunctionTables, JUNCTION_TABLES_SQL, } from "@rebasepro/common";
|