@rebasepro/server-postgres 0.12.1-canary.gf5f1d39 → 0.13.1-canary.g18cfeb7
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/PostgresBackendDriver.d.ts +49 -2
- package/dist/PostgresBootstrapper.d.ts +25 -1
- package/dist/auth/services.d.ts +21 -0
- package/dist/{src-BbFOPJ1S.js → auth-users-columns-Do9mw5Y5.js} +602 -200
- package/dist/auth-users-columns-Do9mw5Y5.js.map +1 -0
- package/dist/backup/backup-service.d.ts +10 -1
- package/dist/backup/pg-tools.d.ts +47 -0
- package/dist/backup-service-Bww-Lg0s.js +8999 -0
- package/dist/backup-service-Bww-Lg0s.js.map +1 -0
- package/dist/cli-helpers.d.ts +40 -1
- package/dist/connection-BuZ97wsr.js +250 -0
- package/dist/connection-BuZ97wsr.js.map +1 -0
- package/dist/connection.d.ts +42 -0
- package/dist/ensure-collection-policies-DMUdRQdM.js +57 -0
- package/dist/ensure-collection-policies-DMUdRQdM.js.map +1 -0
- package/dist/ensure-collection-tables-DSIxvLLD.js +694 -0
- package/dist/ensure-collection-tables-DSIxvLLD.js.map +1 -0
- package/dist/index.es.js +1701 -10167
- package/dist/index.es.js.map +1 -1
- package/dist/policy-CPkCqVTz.js +105 -0
- package/dist/policy-CPkCqVTz.js.map +1 -0
- package/dist/rls-bootstrap-sql-Bpv3nUZo.js +244 -0
- package/dist/rls-bootstrap-sql-Bpv3nUZo.js.map +1 -0
- package/dist/schema/auth-schema.d.ts +83 -144
- package/dist/schema/auth-users-columns.d.ts +97 -0
- package/dist/schema/ensure-collection-policies.d.ts +60 -0
- package/dist/schema/ensure-collection-tables.d.ts +45 -3
- package/dist/schema/generate-drizzle-schema-logic.d.ts +1 -1
- package/dist/schema/generate-postgres-ddl-logic.d.ts +137 -3
- package/dist/schema/generated-schema-staleness.d.ts +39 -0
- package/dist/schema/introspect-db-constraints.d.ts +57 -0
- package/dist/schema/introspect-db-logic.d.ts +94 -5
- package/dist/schema/introspect-db-queries.d.ts +119 -0
- package/dist/schema/introspect-db-structure.d.ts +263 -0
- package/dist/schema/introspect-db-types.d.ts +11 -0
- package/dist/schema/rls-bootstrap-sql.d.ts +135 -0
- package/dist/security/rls-enforcement.d.ts +53 -2
- package/dist/services/FetchService.d.ts +14 -8
- package/dist/services/RelationService.d.ts +24 -1
- package/dist/services/channel-bus/index.d.ts +1 -7
- package/dist/services/collection-helpers.d.ts +36 -2
- package/dist/services/dataService.d.ts +5 -1
- package/dist/services/realtimeService.d.ts +25 -21
- package/dist/services/row-pipeline.d.ts +1 -1
- package/dist/{src-Zqwaw3P5.js → src-C_wvdMnl.js} +78 -145
- package/dist/src-C_wvdMnl.js.map +1 -0
- package/dist/utils/connection-string.d.ts +29 -0
- package/dist/utils/drizzle-conditions.d.ts +5 -4
- package/dist/utils/pg-error-utils.d.ts +35 -0
- package/dist/websocket-D0TBU3ia.js +530 -0
- package/dist/websocket-D0TBU3ia.js.map +1 -0
- package/package.json +17 -16
- package/src/PostgresAdapter.ts +21 -2
- package/src/PostgresBackendDriver.ts +169 -3
- package/src/PostgresBootstrapper.ts +233 -35
- package/src/auth/ensure-tables.ts +335 -81
- package/src/auth/services.ts +24 -2
- package/src/backup/backup-cli.ts +41 -2
- package/src/backup/backup-service.ts +38 -5
- package/src/backup/pg-tools.ts +96 -3
- package/src/cli-helpers.ts +92 -9
- package/src/cli.ts +219 -56
- package/src/collections/validate-relations.ts +136 -14
- package/src/connection.ts +73 -0
- package/src/data-transformer.ts +22 -6
- package/src/databasePoolManager.ts +5 -2
- package/src/history/ensure-history-table.ts +7 -0
- package/src/schema/auth-schema.ts +30 -19
- package/src/schema/auth-users-columns.ts +131 -0
- package/src/schema/doctor.ts +7 -5
- package/src/schema/ensure-collection-policies.ts +105 -0
- package/src/schema/ensure-collection-tables.test.ts +105 -9
- package/src/schema/ensure-collection-tables.ts +301 -44
- package/src/schema/generate-drizzle-schema-logic.ts +49 -7
- package/src/schema/generate-postgres-ddl-logic.ts +474 -27
- package/src/schema/generated-schema-staleness.ts +169 -0
- package/src/schema/introspect-db-constraints.ts +385 -0
- package/src/schema/introspect-db-inference.ts +18 -8
- package/src/schema/introspect-db-logic.ts +385 -71
- package/src/schema/introspect-db-queries.ts +326 -0
- package/src/schema/introspect-db-structure.ts +670 -0
- package/src/schema/introspect-db-types.ts +56 -0
- package/src/schema/introspect-db.ts +37 -80
- package/src/schema/introspect-runtime.test.ts +56 -8
- package/src/schema/introspect-runtime.ts +31 -9
- package/src/schema/non-sql-collections.test.ts +131 -0
- package/src/schema/rls-bootstrap-sql.ts +288 -0
- package/src/security/anonymous-grants.test.ts +4 -2
- package/src/security/policy-drift.test.ts +11 -3
- package/src/security/rls-enforcement.ts +141 -3
- package/src/services/BranchService.ts +71 -28
- package/src/services/FetchService.ts +100 -107
- package/src/services/PersistService.ts +34 -7
- package/src/services/RelationService.ts +249 -48
- package/src/services/channel-bus/index.ts +0 -9
- package/src/services/channel-history.ts +8 -0
- package/src/services/channel-presence.ts +6 -0
- package/src/services/collection-helpers.ts +69 -3
- package/src/services/dataService.ts +5 -1
- package/src/services/realtimeService.ts +38 -35
- package/src/services/row-pipeline.ts +1 -1
- package/src/utils/connection-string.ts +58 -0
- package/src/utils/drizzle-conditions.ts +31 -6
- package/src/utils/pg-error-utils.ts +65 -0
- package/src/websocket.ts +18 -9
- package/dist/chunk-DSJWtz9O.js +0 -40
- package/dist/ensure-collection-tables-CNTcZGvn.js +0 -304
- package/dist/ensure-collection-tables-CNTcZGvn.js.map +0 -1
- package/dist/schema/auth-bootstrap-sql.d.ts +0 -24
- package/dist/src-BbFOPJ1S.js.map +0 -1
- package/dist/src-Zqwaw3P5.js.map +0 -1
- package/src/schema/auth-bootstrap-sql.ts +0 -47
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { CollectionConfig, NumberProperty, Property, ResolvedRelation, RelationProperty, SecurityOperation, SecurityRule, StringProperty, isPostgresCollectionConfig, DateProperty, ArrayProperty, MapProperty, ReferenceProperty, VectorProperty, BinaryProperty, isManyToMany, type ResolvedManyToMany, type ResolvedBelongsTo } from "@rebasepro/types";
|
|
2
|
-
import { getEnumVarName, getTableName, resolveCollectionRelations, findRelation, securityRuleToConditions, policyToPostgres, getEffectiveSecurityRules, getInjectedSecurityRules, resolveJunctionSpecs, getJunctionSecurityRules, getJunctionCollectionConfig } from "@rebasepro/common";
|
|
3
|
-
import { toSnakeCase, getPolicyNamesForRule } from "@rebasepro/utils";
|
|
2
|
+
import { getEnumVarName, getTableName, resolveCollectionRelations, findRelation, securityRuleToConditions, policyToPostgres, getEffectiveSecurityRules, getInjectedSecurityRules, resolveJunctionSpecs, getJunctionSecurityRules, getJunctionCollectionConfig, resolveStringColumnLength, relationalCollections } from "@rebasepro/common";
|
|
3
|
+
import { toSnakeCase, getPolicyNamesForRule, generateForeignKeyName, legacyForeignKeyName } from "@rebasepro/utils";
|
|
4
|
+
import { AUTH_USERS_COLUMNS, authUsersColumnDefinition, authUsersColumnSql, isAuthCollection } from "./auth-users-columns";
|
|
5
|
+
import { REBASE_SCHEMA } from "@rebasepro/types";
|
|
4
6
|
|
|
5
7
|
// --- Helper Functions ---
|
|
6
8
|
|
|
@@ -11,7 +13,7 @@ export const resolveColumnName = (propName: string, prop?: Property | null): str
|
|
|
11
13
|
return toSnakeCase(propName);
|
|
12
14
|
};
|
|
13
15
|
|
|
14
|
-
const getPrimaryKeyProp = (collection: CollectionConfig): { name: string, type: "string" | "number", isUuid: boolean } => {
|
|
16
|
+
export const getPrimaryKeyProp = (collection: CollectionConfig): { name: string, type: "string" | "number", isUuid: boolean } => {
|
|
15
17
|
if (collection.properties) {
|
|
16
18
|
const idPropEntry = Object.entries(collection.properties).find(([_, prop]) => "isId" in (prop as unknown as object) && Boolean((prop as unknown as Record<string, unknown>).isId));
|
|
17
19
|
if (idPropEntry) {
|
|
@@ -28,14 +30,18 @@ const getPrimaryKeyProp = (collection: CollectionConfig): { name: string, type:
|
|
|
28
30
|
return { name: "id", type: "string", isUuid: isUuid ?? false };
|
|
29
31
|
};
|
|
30
32
|
|
|
31
|
-
const isNumericId = (collection: CollectionConfig): boolean => {
|
|
33
|
+
export const isNumericId = (collection: CollectionConfig): boolean => {
|
|
32
34
|
return getPrimaryKeyProp(collection).type === "number";
|
|
33
35
|
};
|
|
34
36
|
|
|
35
|
-
const getPrimaryKeyName = (collection: CollectionConfig): string => {
|
|
37
|
+
export const getPrimaryKeyName = (collection: CollectionConfig): string => {
|
|
36
38
|
return getPrimaryKeyProp(collection).name;
|
|
37
39
|
};
|
|
38
40
|
|
|
41
|
+
/** The column type a junction holds for one endpoint's primary key. */
|
|
42
|
+
const junctionKeyType = (collection: CollectionConfig): string =>
|
|
43
|
+
isNumericId(collection) ? "INTEGER" : (getPrimaryKeyProp(collection).isUuid ? "UUID" : "TEXT");
|
|
44
|
+
|
|
39
45
|
export const isIdProperty = (propName: string, prop: Property, collection: CollectionConfig): boolean => {
|
|
40
46
|
if ("isId" in prop && Boolean(prop.isId)) return true;
|
|
41
47
|
const hasExplicitId = Object.values(collection.properties ?? {}).some(p => "isId" in (p as unknown as object) && Boolean((p as unknown as Record<string, unknown>).isId));
|
|
@@ -45,7 +51,28 @@ export const isIdProperty = (propName: string, prop: Property, collection: Colle
|
|
|
45
51
|
|
|
46
52
|
type ResolveCollection = (slug: string) => CollectionConfig | undefined;
|
|
47
53
|
|
|
48
|
-
|
|
54
|
+
/**
|
|
55
|
+
* Render statements produced by {@link generatePolicyStatements} back into the
|
|
56
|
+
* exact string the DDL/policies files have always carried: each statement on
|
|
57
|
+
* its own line, terminated by a newline. Keeping the string form derived from
|
|
58
|
+
* the statement array means the two can never drift — the boot-time applier and
|
|
59
|
+
* the generated `policies.sql` emit the same SQL, from the same source.
|
|
60
|
+
*/
|
|
61
|
+
const statementsToDdl = (statements: string[]): string => statements.map(s => `${s}\n`).join("");
|
|
62
|
+
|
|
63
|
+
const generatePolicyDdl = (collection: CollectionConfig, rule: SecurityRule, resolveCollection: ResolveCollection): string =>
|
|
64
|
+
statementsToDdl(generatePolicyStatements(collection, rule, resolveCollection));
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The individual SQL statements a single security rule compiles to: a
|
|
68
|
+
* `DROP POLICY IF EXISTS` / `CREATE POLICY` pair per operation, each a complete
|
|
69
|
+
* statement (terminated by `;`, no trailing newline).
|
|
70
|
+
*
|
|
71
|
+
* This is the primitive the boot-time RLS applier runs one statement at a time
|
|
72
|
+
* (the runtime's DB handle speaks the extended query protocol, which forbids
|
|
73
|
+
* multiple commands in one execute), while `db push` writes the joined string.
|
|
74
|
+
*/
|
|
75
|
+
export const generatePolicyStatements = (collection: CollectionConfig, rule: SecurityRule, resolveCollection: ResolveCollection): string[] => {
|
|
49
76
|
const tableName = getTableName(collection);
|
|
50
77
|
const ops: readonly SecurityOperation[] = rule.operations && rule.operations.length > 0
|
|
51
78
|
? rule.operations
|
|
@@ -53,12 +80,12 @@ const generatePolicyDdl = (collection: CollectionConfig, rule: SecurityRule, res
|
|
|
53
80
|
|
|
54
81
|
const policyNames = getPolicyNamesForRule(rule, tableName);
|
|
55
82
|
|
|
56
|
-
return ops.
|
|
57
|
-
return
|
|
58
|
-
})
|
|
83
|
+
return ops.flatMap((op, opIdx) => {
|
|
84
|
+
return generateSinglePolicyStatements(collection, rule, op, policyNames[opIdx], resolveCollection);
|
|
85
|
+
});
|
|
59
86
|
};
|
|
60
87
|
|
|
61
|
-
const
|
|
88
|
+
const generateSinglePolicyStatements = (collection: CollectionConfig, rule: SecurityRule, operation: SecurityOperation, policyName: string, resolveCollection: ResolveCollection): string[] => {
|
|
62
89
|
const schema = isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : "public";
|
|
63
90
|
const tableName = getTableName(collection);
|
|
64
91
|
const mode = (rule.mode ?? "permissive").toUpperCase();
|
|
@@ -83,13 +110,24 @@ const generateSinglePolicyDdl = (collection: CollectionConfig, rule: SecurityRul
|
|
|
83
110
|
withCheckClause = "false";
|
|
84
111
|
}
|
|
85
112
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
if (usingClause)
|
|
89
|
-
if (withCheckClause)
|
|
90
|
-
|
|
113
|
+
const drop = `DROP POLICY IF EXISTS "${policyName}" ON "${schema}"."${tableName}";`;
|
|
114
|
+
let create = `CREATE POLICY "${policyName}" ON "${schema}"."${tableName}" AS ${mode} FOR ${operationUpper} TO ${pgRoles.map(r => `"${r}"`).join(", ")}`;
|
|
115
|
+
if (usingClause) create += ` USING (${usingClause})`;
|
|
116
|
+
if (withCheckClause) create += ` WITH CHECK (${withCheckClause})`;
|
|
117
|
+
create += ";";
|
|
118
|
+
return [drop, create];
|
|
91
119
|
};
|
|
92
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Single-quote escaping for a SQL string literal (PostgreSQL doubles the
|
|
123
|
+
* quote). Enum labels come straight from user-authored collection config, so a
|
|
124
|
+
* label like `it's` closes the literal early and the whole generated file stops
|
|
125
|
+
* parsing at the `CREATE TYPE`. Lives here rather than next to its other caller
|
|
126
|
+
* because ensure-collection-tables already imports from this module — the
|
|
127
|
+
* reverse would be a cycle.
|
|
128
|
+
*/
|
|
129
|
+
export const quoteSqlLiteral = (value: string): string => `'${value.replace(/'/g, "''")}'`;
|
|
130
|
+
|
|
93
131
|
export const getSqlColumnType = (propName: string, prop: Property, collection: CollectionConfig, collections: CollectionConfig[]): string => {
|
|
94
132
|
switch (prop.type) {
|
|
95
133
|
case "string": {
|
|
@@ -103,11 +141,15 @@ export const getSqlColumnType = (propName: string, prop: Property, collection: C
|
|
|
103
141
|
if (stringProp.isId === "uuid" || stringProp.columnType === "uuid") {
|
|
104
142
|
return "UUID";
|
|
105
143
|
}
|
|
144
|
+
// Width comes from `validation.max` when the property states one.
|
|
145
|
+
// It used to be a hardcoded 255 here and *absent* on the Drizzle
|
|
146
|
+
// path, so the same property produced a bounded column down one
|
|
147
|
+
// generator and an unbounded one down the other.
|
|
106
148
|
if (stringProp.columnType === "char") {
|
|
107
|
-
return
|
|
149
|
+
return `CHAR(${resolveStringColumnLength(stringProp)})`;
|
|
108
150
|
}
|
|
109
151
|
if (stringProp.columnType === "varchar") {
|
|
110
|
-
return
|
|
152
|
+
return `VARCHAR(${resolveStringColumnLength(stringProp)})`;
|
|
111
153
|
}
|
|
112
154
|
// `text` is the default. The two generators disagreed here before:
|
|
113
155
|
// this one emitted VARCHAR(255) while the drizzle path emitted a bare
|
|
@@ -196,14 +238,24 @@ export const getSqlColumnType = (propName: string, prop: Property, collection: C
|
|
|
196
238
|
};
|
|
197
239
|
|
|
198
240
|
export const generatePostgresDdl = async (
|
|
199
|
-
|
|
241
|
+
allCollections: CollectionConfig[],
|
|
200
242
|
options: { includePolicies?: boolean } = { includePolicies: true }
|
|
201
243
|
): Promise<string> => {
|
|
244
|
+
// Only the collections this engine stores. See `relationalCollections`.
|
|
245
|
+
const collections = relationalCollections(allCollections);
|
|
202
246
|
let ddl = "-- This file is auto-generated by the Rebase DDL generator. Do not edit manually.\n\n";
|
|
203
247
|
|
|
204
|
-
// 1. Create custom schemas
|
|
248
|
+
// 1. Create custom schemas.
|
|
249
|
+
//
|
|
250
|
+
// `rebase` is unconditional and load-bearing. The RLS helper functions live
|
|
251
|
+
// in it, so the migration preamble creates it — which puts it in Atlas's
|
|
252
|
+
// replayed state, and anything in that state but absent from this desired
|
|
253
|
+
// schema gets a `DROP SCHEMA … CASCADE` planned against it. That would take
|
|
254
|
+
// the auth tables. It used to appear here only when some collection
|
|
255
|
+
// happened to declare `schema: "rebase"`; the scaffold's users collection
|
|
256
|
+
// does, which is why nobody hit it, but nothing guaranteed it.
|
|
205
257
|
const uniqueSchemas = Array.from(new Set([
|
|
206
|
-
|
|
258
|
+
REBASE_SCHEMA,
|
|
207
259
|
...collections.map(c => isPostgresCollectionConfig(c) ? c.schema : undefined).filter(Boolean)
|
|
208
260
|
]));
|
|
209
261
|
uniqueSchemas.forEach(schema => {
|
|
@@ -212,6 +264,13 @@ export const generatePostgresDdl = async (
|
|
|
212
264
|
if (uniqueSchemas.length > 0) ddl += "\n";
|
|
213
265
|
|
|
214
266
|
// 2. Generate Enums
|
|
267
|
+
//
|
|
268
|
+
// The enum type name is derived from table + column, so two collections
|
|
269
|
+
// mapped onto the same table — or two properties whose `columnName`
|
|
270
|
+
// resolves to the same column — land on the same name. `CREATE TYPE` has no
|
|
271
|
+
// IF NOT EXISTS, so emitting it twice aborts the whole file on the second
|
|
272
|
+
// statement. Dedupe by name, matching planCollectionSchemaEnsure.
|
|
273
|
+
const emittedEnums = new Set<string>();
|
|
215
274
|
collections.forEach(collection => {
|
|
216
275
|
const collectionTable = getTableName(collection);
|
|
217
276
|
const schema = isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : "public";
|
|
@@ -223,8 +282,9 @@ export const generatePostgresDdl = async (
|
|
|
223
282
|
String(typeof v === "object" && v !== null && "id" in v ? v.id : v)
|
|
224
283
|
)
|
|
225
284
|
: Object.keys(prop.enum);
|
|
226
|
-
if (values.length > 0) {
|
|
227
|
-
|
|
285
|
+
if (values.length > 0 && !emittedEnums.has(`${schema}.${enumDbName}`)) {
|
|
286
|
+
emittedEnums.add(`${schema}.${enumDbName}`);
|
|
287
|
+
ddl += `CREATE TYPE "${schema}"."${enumDbName}" AS ENUM (${values.map(quoteSqlLiteral).join(", ")});\n`;
|
|
228
288
|
}
|
|
229
289
|
}
|
|
230
290
|
});
|
|
@@ -307,8 +367,8 @@ export const generatePostgresDdl = async (
|
|
|
307
367
|
ddl += ` PRIMARY KEY ("${sourceColumn}", "${targetColumn}")\n`;
|
|
308
368
|
ddl += `);\n\n`;
|
|
309
369
|
|
|
310
|
-
fkStatements.push(`ALTER TABLE "${schema}"."${baseTableName}" ADD CONSTRAINT "${baseTableName}_${sourceColumn}_fkey" FOREIGN KEY ("${sourceColumn}") REFERENCES "${sourceSchema}"."${sourceTable}" ("${sourceId}") ON DELETE ${onDelete.toUpperCase()};`);
|
|
311
|
-
fkStatements.push(`ALTER TABLE "${schema}"."${baseTableName}" ADD CONSTRAINT "${baseTableName}_${targetColumn}_fkey" FOREIGN KEY ("${targetColumn}") REFERENCES "${targetSchema}"."${targetTable}" ("${targetId}") ON DELETE ${onDelete.toUpperCase()};`);
|
|
370
|
+
fkStatements.push(`ALTER TABLE "${schema}"."${baseTableName}" ADD CONSTRAINT "${toPostgresIdentifier(`${baseTableName}_${sourceColumn}_fkey`)}" FOREIGN KEY ("${sourceColumn}") REFERENCES "${sourceSchema}"."${sourceTable}" ("${sourceId}") ON DELETE ${onDelete.toUpperCase()};`);
|
|
371
|
+
fkStatements.push(`ALTER TABLE "${schema}"."${baseTableName}" ADD CONSTRAINT "${toPostgresIdentifier(`${baseTableName}_${targetColumn}_fkey`)}" FOREIGN KEY ("${targetColumn}") REFERENCES "${targetSchema}"."${targetTable}" ("${targetId}") ON DELETE ${onDelete.toUpperCase()};`);
|
|
312
372
|
|
|
313
373
|
if (options.includePolicies) {
|
|
314
374
|
// Junction tables are generated tables like any other: locked by
|
|
@@ -366,7 +426,7 @@ export const generatePostgresDdl = async (
|
|
|
366
426
|
if (required) colDef += " NOT NULL";
|
|
367
427
|
columns.push(colDef);
|
|
368
428
|
|
|
369
|
-
fkStatements.push(`ALTER TABLE "${schema}"."${baseTableName}" ADD CONSTRAINT "${baseTableName}_${relInfo.localKey}_fkey" FOREIGN KEY ("${relInfo.localKey}") REFERENCES "${targetSchema}"."${targetTable}" ("${targetId}") ON DELETE ${onDeleteVal.toUpperCase()}${onUpdate};`);
|
|
429
|
+
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};`);
|
|
370
430
|
} else if (prop.type === "reference") {
|
|
371
431
|
const refProp = prop as ReferenceProperty;
|
|
372
432
|
const targetCollection = collections.find(c => c.slug === refProp.path || getTableName(c) === refProp.path);
|
|
@@ -388,10 +448,27 @@ export const generatePostgresDdl = async (
|
|
|
388
448
|
if (required) colDef += " NOT NULL";
|
|
389
449
|
columns.push(colDef);
|
|
390
450
|
|
|
391
|
-
fkStatements.push(`ALTER TABLE "${schema}"."${baseTableName}" ADD CONSTRAINT "${baseTableName}_${colName}_fkey" FOREIGN KEY ("${colName}") REFERENCES "${targetSchema}"."${targetTable}" ("${targetId}") ON DELETE ${onDelete.toUpperCase()};`);
|
|
451
|
+
fkStatements.push(`ALTER TABLE "${schema}"."${baseTableName}" ADD CONSTRAINT "${toPostgresIdentifier(`${baseTableName}_${colName}_fkey`)}" FOREIGN KEY ("${colName}") REFERENCES "${targetSchema}"."${targetTable}" ("${targetId}") ON DELETE ${onDelete.toUpperCase()};`);
|
|
392
452
|
}
|
|
393
453
|
} else {
|
|
394
454
|
const colName = resolveColumnName(propName, prop);
|
|
455
|
+
|
|
456
|
+
// On an auth collection, the columns auth reads and writes
|
|
457
|
+
// are defined once, in `auth-users-columns`, and every
|
|
458
|
+
// creator of this table emits that definition. Before this,
|
|
459
|
+
// the desired state here was built from the collection file
|
|
460
|
+
// alone — which knows nothing of `is_anonymous` or
|
|
461
|
+
// `tokens_valid_after` — so `db push` and the runtime
|
|
462
|
+
// disagreed about `email`'s nullability and about which
|
|
463
|
+
// columns exist at all.
|
|
464
|
+
const authDefinition = isAuthCollection(collection)
|
|
465
|
+
? authUsersColumnDefinition(colName)
|
|
466
|
+
: undefined;
|
|
467
|
+
if (authDefinition && !isIdProperty(propName, prop, collection)) {
|
|
468
|
+
columns.push(` "${colName}" ${authDefinition}`);
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
|
|
395
472
|
const colType = getSqlColumnType(propName, prop, collection, collections);
|
|
396
473
|
let colDef = ` "${colName}" ${colType}`;
|
|
397
474
|
|
|
@@ -428,6 +505,27 @@ export const generatePostgresDdl = async (
|
|
|
428
505
|
}
|
|
429
506
|
});
|
|
430
507
|
|
|
508
|
+
// ── Auth columns the collection file never mentions ──────────────
|
|
509
|
+
// `db push` is declarative: Atlas diffs the database against exactly
|
|
510
|
+
// what is emitted here and drops anything else. The scaffold's users
|
|
511
|
+
// collection describes 12 columns while auth needs 14, so
|
|
512
|
+
// `is_anonymous` and `tokens_valid_after` — created at boot by
|
|
513
|
+
// `ensureAuthTablesExist` — read as unmanaged drift, and a push run
|
|
514
|
+
// after the server had started once planned to DROP them. The
|
|
515
|
+
// destructive gate catches it, which makes it a data-loss prompt on
|
|
516
|
+
// the auth table rather than silent damage; either way the desired
|
|
517
|
+
// state was wrong. Emit the full contract so the two agree.
|
|
518
|
+
if (isAuthCollection(collection)) {
|
|
519
|
+
const declared = new Set(
|
|
520
|
+
Object.entries(collection.properties ?? {})
|
|
521
|
+
.map(([name, prop]) => resolveColumnName(name, prop))
|
|
522
|
+
);
|
|
523
|
+
for (const spec of AUTH_USERS_COLUMNS) {
|
|
524
|
+
if (declared.has(spec.column)) continue;
|
|
525
|
+
columns.push(` "${spec.column}" ${authUsersColumnSql(spec)}`);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
431
529
|
// Backwards compatibility: add default id primary key if missing
|
|
432
530
|
const hasPk = columns.some(c => c.includes("PRIMARY KEY"));
|
|
433
531
|
if (!hasPk) {
|
|
@@ -470,7 +568,356 @@ export const generatePostgresDdl = async (
|
|
|
470
568
|
return ddl;
|
|
471
569
|
};
|
|
472
570
|
|
|
473
|
-
|
|
571
|
+
/** The RLS statements one declared collection's table needs, ready to run. */
|
|
572
|
+
/**
|
|
573
|
+
* A foreign key, as both its parts and the statement that creates it.
|
|
574
|
+
*
|
|
575
|
+
* `ALTER TABLE … ADD CONSTRAINT` has no `IF NOT EXISTS`, so a caller applying
|
|
576
|
+
* these has to skip by name — hence the name is a field and not only a substring
|
|
577
|
+
* of the SQL.
|
|
578
|
+
*/
|
|
579
|
+
export interface ForeignKeyPlan {
|
|
580
|
+
constraintName: string;
|
|
581
|
+
schema: string;
|
|
582
|
+
/** Bare table name, no schema prefix. */
|
|
583
|
+
table: string;
|
|
584
|
+
column: string;
|
|
585
|
+
targetSchema: string;
|
|
586
|
+
targetTable: string;
|
|
587
|
+
targetColumn: string;
|
|
588
|
+
sql: string;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/** A column a `relation` or `reference` property owns on its own table. */
|
|
592
|
+
export interface RelationalColumnPlan {
|
|
593
|
+
schema: string;
|
|
594
|
+
/** Bare table name, no schema prefix. */
|
|
595
|
+
table: string;
|
|
596
|
+
column: string;
|
|
597
|
+
/** Postgres type, exactly as the DDL generator declares it. */
|
|
598
|
+
type: string;
|
|
599
|
+
/** Absent when the target collection is not part of this bundle. */
|
|
600
|
+
foreignKey?: ForeignKeyPlan;
|
|
601
|
+
/**
|
|
602
|
+
* What this column would have been called before `generateForeignKeyName`
|
|
603
|
+
* learned to singularize — set only when the two differ and the column name
|
|
604
|
+
* is the derived default rather than one the author wrote.
|
|
605
|
+
*
|
|
606
|
+
* Carried so the boot-time ensure can notice a database provisioned under
|
|
607
|
+
* the old rule. It is never used to name anything.
|
|
608
|
+
*/
|
|
609
|
+
legacyColumn?: string;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/** The table behind a many-to-many `through` relation. */
|
|
613
|
+
export interface JunctionTablePlan {
|
|
614
|
+
schema: string;
|
|
615
|
+
/** Bare table name, no schema prefix. */
|
|
616
|
+
table: string;
|
|
617
|
+
columns: { name: string; type: string; legacyName?: string }[];
|
|
618
|
+
/** Both endpoint columns plus the composite primary key. */
|
|
619
|
+
createTable: string;
|
|
620
|
+
foreignKeys: ForeignKeyPlan[];
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
const schemaOfCollection = (collection: CollectionConfig): string =>
|
|
624
|
+
isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : "public";
|
|
625
|
+
|
|
626
|
+
const bareTableName = (name: string): string => (name.includes(".") ? name.split(".").pop()! : name);
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Truncate a derived identifier the way Postgres does: to 63 bytes, silently.
|
|
630
|
+
*
|
|
631
|
+
* This is not cosmetic, and not really a naming choice at all — it is agreeing
|
|
632
|
+
* with the name the database ALREADY stored. `ADD CONSTRAINT` on a longer name
|
|
633
|
+
* succeeds and records the truncated form, so the untruncated name this used to
|
|
634
|
+
* derive matched nothing in the catalogue. Boot-ensure compares its planned
|
|
635
|
+
* constraints against `readExistingSchema`, which reads catalogue names, so the
|
|
636
|
+
* comparison could never hit: every boot re-issued `ADD CONSTRAINT` for the same
|
|
637
|
+
* constraint, forever, and got "already exists" every time. Non-fatal (foreign
|
|
638
|
+
* keys are the one action allowed to fail) and therefore permanent — an error in
|
|
639
|
+
* the log on every restart of a project whose table and column names happened to
|
|
640
|
+
* be long.
|
|
641
|
+
*
|
|
642
|
+
* Byte length, not string length: NAMEDATALEN is 64 bytes, and a multi-byte
|
|
643
|
+
* character straddling the boundary would be cut mid-sequence by `slice(0, 63)`.
|
|
644
|
+
*/
|
|
645
|
+
const toPostgresIdentifier = (name: string): string => {
|
|
646
|
+
const bytes = Buffer.from(name, "utf8");
|
|
647
|
+
if (bytes.byteLength <= 63) return name;
|
|
648
|
+
// `toString` on a slice that ends mid-character yields U+FFFD; dropping it
|
|
649
|
+
// lands on the last whole character that fits, which is what Postgres does.
|
|
650
|
+
return bytes.subarray(0, 63).toString("utf8").replace(/�+$/, "");
|
|
651
|
+
};
|
|
652
|
+
|
|
653
|
+
const foreignKeyPlan = (
|
|
654
|
+
args: Omit<ForeignKeyPlan, "constraintName" | "sql"> & { onDelete: string; onUpdate?: string }
|
|
655
|
+
): ForeignKeyPlan => {
|
|
656
|
+
const constraintName = toPostgresIdentifier(`${args.table}_${args.column}_fkey`);
|
|
657
|
+
const onUpdate = args.onUpdate ? ` ON UPDATE ${args.onUpdate.toUpperCase()}` : "";
|
|
658
|
+
return {
|
|
659
|
+
constraintName,
|
|
660
|
+
schema: args.schema,
|
|
661
|
+
table: args.table,
|
|
662
|
+
column: args.column,
|
|
663
|
+
targetSchema: args.targetSchema,
|
|
664
|
+
targetTable: args.targetTable,
|
|
665
|
+
targetColumn: args.targetColumn,
|
|
666
|
+
sql:
|
|
667
|
+
`ALTER TABLE "${args.schema}"."${args.table}" ADD CONSTRAINT "${constraintName}" ` +
|
|
668
|
+
`FOREIGN KEY ("${args.column}") REFERENCES "${args.targetSchema}"."${args.targetTable}" ` +
|
|
669
|
+
`("${args.targetColumn}") ON DELETE ${args.onDelete.toUpperCase()}${onUpdate};`
|
|
670
|
+
};
|
|
671
|
+
};
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* The FK columns the declared collections own — one entry per `relation`
|
|
675
|
+
* (`belongsTo` side) or `reference` property.
|
|
676
|
+
*
|
|
677
|
+
* Split out of {@link generatePostgresDdl} so the boot-time schema ensure can
|
|
678
|
+
* create the same columns with the same names, types and constraints. Before
|
|
679
|
+
* this it skipped them outright, which was survivable only because `db push`
|
|
680
|
+
* always followed; on a managed tenant nothing follows, so a table arrived
|
|
681
|
+
* without the column its own collection reads and wrote 400 on every insert.
|
|
682
|
+
*
|
|
683
|
+
* A relation whose target is not in the bundle yields no column at all (the
|
|
684
|
+
* generator returns early on an unresolvable target); a `reference` whose target
|
|
685
|
+
* is unknown yields the column without a constraint. Both mirror the generator
|
|
686
|
+
* exactly — a divergence here is a schema fork between boot and `db push`.
|
|
687
|
+
*/
|
|
688
|
+
export const planRelationalColumns = (allCollections: CollectionConfig[]): RelationalColumnPlan[] => {
|
|
689
|
+
const collections = relationalCollections(allCollections);
|
|
690
|
+
const plans: RelationalColumnPlan[] = [];
|
|
691
|
+
|
|
692
|
+
for (const collection of collections) {
|
|
693
|
+
const tableName = getTableName(collection);
|
|
694
|
+
if (!tableName) continue;
|
|
695
|
+
const schema = schemaOfCollection(collection);
|
|
696
|
+
const table = bareTableName(tableName);
|
|
697
|
+
|
|
698
|
+
for (const [propName, rawProp] of Object.entries(collection.properties ?? {})) {
|
|
699
|
+
const prop = rawProp as Property;
|
|
700
|
+
|
|
701
|
+
if (prop.type === "relation") {
|
|
702
|
+
const refProp = prop as RelationProperty;
|
|
703
|
+
const resolvedRelations = resolveCollectionRelations(collection);
|
|
704
|
+
const relInfo = findRelation(resolvedRelations, refProp.relation?.relationName ?? propName);
|
|
705
|
+
if (relInfo?.kind !== "belongsTo") continue;
|
|
706
|
+
// The relation and an explicit FK property can both be declared;
|
|
707
|
+
// the explicit one owns the column.
|
|
708
|
+
if (collection.properties[relInfo.localKey] && propName !== relInfo.localKey) continue;
|
|
709
|
+
|
|
710
|
+
let targetCollection: CollectionConfig;
|
|
711
|
+
try {
|
|
712
|
+
targetCollection = relInfo.target();
|
|
713
|
+
} catch {
|
|
714
|
+
continue;
|
|
715
|
+
}
|
|
716
|
+
if (!targetCollection) continue;
|
|
717
|
+
|
|
718
|
+
const required = prop.validation?.required;
|
|
719
|
+
// Only a *derived* column name can be affected by the change to
|
|
720
|
+
// `generateForeignKeyName`; one the author wrote is theirs.
|
|
721
|
+
const relationName = refProp.relation?.relationName ?? propName;
|
|
722
|
+
const legacyKey = legacyForeignKeyName(relationName);
|
|
723
|
+
const derived = relInfo.localKey === generateForeignKeyName(relationName);
|
|
724
|
+
plans.push({
|
|
725
|
+
schema,
|
|
726
|
+
table,
|
|
727
|
+
column: relInfo.localKey,
|
|
728
|
+
legacyColumn: derived && legacyKey !== relInfo.localKey ? legacyKey : undefined,
|
|
729
|
+
type: getSqlColumnType(propName, prop, collection, collections),
|
|
730
|
+
foreignKey: foreignKeyPlan({
|
|
731
|
+
schema,
|
|
732
|
+
table,
|
|
733
|
+
column: relInfo.localKey,
|
|
734
|
+
targetSchema: schemaOfCollection(targetCollection),
|
|
735
|
+
targetTable: bareTableName(getTableName(targetCollection)),
|
|
736
|
+
targetColumn: getPrimaryKeyName(targetCollection),
|
|
737
|
+
onDelete: relInfo.onDelete ?? (required ? "CASCADE" : "SET NULL"),
|
|
738
|
+
onUpdate: relInfo.onUpdate
|
|
739
|
+
})
|
|
740
|
+
});
|
|
741
|
+
} else if (prop.type === "reference") {
|
|
742
|
+
const refProp = prop as ReferenceProperty;
|
|
743
|
+
const targetCollection = collections.find(
|
|
744
|
+
c => c.slug === refProp.path || getTableName(c) === refProp.path
|
|
745
|
+
);
|
|
746
|
+
const column = resolveColumnName(propName, prop);
|
|
747
|
+
const type = getSqlColumnType(propName, prop, collection, collections);
|
|
748
|
+
const required = prop.validation?.required;
|
|
749
|
+
|
|
750
|
+
plans.push({
|
|
751
|
+
schema,
|
|
752
|
+
table,
|
|
753
|
+
column,
|
|
754
|
+
type,
|
|
755
|
+
foreignKey: targetCollection
|
|
756
|
+
? foreignKeyPlan({
|
|
757
|
+
schema,
|
|
758
|
+
table,
|
|
759
|
+
column,
|
|
760
|
+
targetSchema: schemaOfCollection(targetCollection),
|
|
761
|
+
targetTable: bareTableName(getTableName(targetCollection)),
|
|
762
|
+
targetColumn: getPrimaryKeyName(targetCollection),
|
|
763
|
+
onDelete: required ? "CASCADE" : "SET NULL"
|
|
764
|
+
})
|
|
765
|
+
: undefined
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
return plans;
|
|
772
|
+
};
|
|
773
|
+
|
|
774
|
+
/**
|
|
775
|
+
* The junction tables a bundle's many-to-many relations imply.
|
|
776
|
+
*
|
|
777
|
+
* Derived from {@link resolveJunctionSpecs}, the same source the junction RLS
|
|
778
|
+
* comes from, so a table created here always has policies planned for it — a
|
|
779
|
+
* junction with row-level security left off is readable and writable by every
|
|
780
|
+
* signed-in user, which is why the two must ship together.
|
|
781
|
+
*/
|
|
782
|
+
export const planJunctionTables = (allCollections: CollectionConfig[]): JunctionTablePlan[] => {
|
|
783
|
+
const collections = relationalCollections(allCollections);
|
|
784
|
+
const plans: JunctionTablePlan[] = [];
|
|
785
|
+
|
|
786
|
+
for (const spec of resolveJunctionSpecs(collections).values()) {
|
|
787
|
+
const [source, target] = spec.endpoints;
|
|
788
|
+
// A junction column's default name is derived from the endpoint
|
|
789
|
+
// collection's slug — which is normally plural, so this is where the
|
|
790
|
+
// singularization change actually lands: a `categories` endpoint used
|
|
791
|
+
// to give `categorie_id` and now gives `category_id`. Recorded, not
|
|
792
|
+
// used, so boot-ensure can recognise a table built under the old rule.
|
|
793
|
+
const legacyFor = (endpoint: typeof source): string | undefined => {
|
|
794
|
+
const slug = toSnakeCase(endpoint.collection.slug ?? endpoint.collection.name ?? "");
|
|
795
|
+
const legacy = legacyForeignKeyName(slug);
|
|
796
|
+
const derived = endpoint.junctionColumn === generateForeignKeyName(slug);
|
|
797
|
+
return derived && legacy !== endpoint.junctionColumn ? legacy : undefined;
|
|
798
|
+
};
|
|
799
|
+
const columns = [
|
|
800
|
+
{ name: source.junctionColumn, type: junctionKeyType(source.collection), legacyName: legacyFor(source) },
|
|
801
|
+
{ name: target.junctionColumn, type: junctionKeyType(target.collection), legacyName: legacyFor(target) }
|
|
802
|
+
];
|
|
803
|
+
// Every declaring side agrees on the edge's lifetime; the first one wins,
|
|
804
|
+
// as it does in the generator's walk.
|
|
805
|
+
const onDelete = spec.declaringSides[0]?.relation.onDelete ?? "CASCADE";
|
|
806
|
+
|
|
807
|
+
plans.push({
|
|
808
|
+
schema: spec.schema,
|
|
809
|
+
table: spec.table,
|
|
810
|
+
columns,
|
|
811
|
+
createTable:
|
|
812
|
+
`CREATE TABLE IF NOT EXISTS "${spec.schema}"."${spec.table}" (` +
|
|
813
|
+
columns.map(c => `"${c.name}" ${c.type} NOT NULL`).join(", ") +
|
|
814
|
+
`, PRIMARY KEY (${columns.map(c => `"${c.name}"`).join(", ")}));`,
|
|
815
|
+
foreignKeys: [source, target].map((endpoint, i) =>
|
|
816
|
+
foreignKeyPlan({
|
|
817
|
+
schema: spec.schema,
|
|
818
|
+
table: spec.table,
|
|
819
|
+
column: columns[i].name,
|
|
820
|
+
targetSchema: schemaOfCollection(endpoint.collection),
|
|
821
|
+
targetTable: bareTableName(getTableName(endpoint.collection)),
|
|
822
|
+
targetColumn: getPrimaryKeyName(endpoint.collection),
|
|
823
|
+
onDelete
|
|
824
|
+
})
|
|
825
|
+
)
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
return plans;
|
|
830
|
+
};
|
|
831
|
+
|
|
832
|
+
export interface CollectionPolicyPlan {
|
|
833
|
+
/** The table's schema (e.g. `public`, `rebase`). */
|
|
834
|
+
schema: string;
|
|
835
|
+
/** The bare table name, no schema prefix. */
|
|
836
|
+
table: string;
|
|
837
|
+
/** `schema.table` — matches the keys `readExistingSchema` returns. */
|
|
838
|
+
qualified: string;
|
|
839
|
+
/** `ALTER TABLE … ENABLE ROW LEVEL SECURITY;` — locked by default. */
|
|
840
|
+
enableRls: string;
|
|
841
|
+
/** `DROP POLICY IF EXISTS` / `CREATE POLICY` statements, in order. */
|
|
842
|
+
policyStatements: string[];
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
/**
|
|
846
|
+
* The per-table RLS plan for the *declared* collections, as executable
|
|
847
|
+
* statements — what the managed runtime applies at boot so a freshly
|
|
848
|
+
* provisioned tenant database serves data instead of 401ing every read.
|
|
849
|
+
*
|
|
850
|
+
* Mirrors {@link generatePostgresPoliciesDdl} exactly (same
|
|
851
|
+
* `generatePolicyStatements`, same enable-RLS, same effective rules, same
|
|
852
|
+
* derived junction rules), so boot and `db push` produce identical policies from
|
|
853
|
+
* identical collections.
|
|
854
|
+
*
|
|
855
|
+
* Junction tables are included, and have to be: boot creates them now
|
|
856
|
+
* ({@link planJunctionTables}), and a junction with RLS left off is readable and
|
|
857
|
+
* writable by every signed-in user. A junction whose table is still absent is
|
|
858
|
+
* skipped by the applier, not planned away here.
|
|
859
|
+
*/
|
|
860
|
+
export const planCollectionPolicies = (allCollections: CollectionConfig[]): CollectionPolicyPlan[] => {
|
|
861
|
+
// A store with no RLS gets no policies planned for it — `supportsRLS` is
|
|
862
|
+
// false for exactly the engines `relationalCollections` filters out.
|
|
863
|
+
const collections = relationalCollections(allCollections);
|
|
864
|
+
const resolveCollection: ResolveCollection = (slug) => collections.find(c => c.slug === slug || getTableName(c) === slug);
|
|
865
|
+
const plans: CollectionPolicyPlan[] = [];
|
|
866
|
+
const seen = new Set<string>();
|
|
867
|
+
|
|
868
|
+
for (const collection of collections) {
|
|
869
|
+
const tableName = getTableName(collection);
|
|
870
|
+
if (!tableName) continue;
|
|
871
|
+
const schema = isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : "public";
|
|
872
|
+
const baseTableName = tableName.includes(".") ? tableName.split(".").pop()! : tableName;
|
|
873
|
+
const qualified = `${schema}.${baseTableName}`;
|
|
874
|
+
if (seen.has(qualified)) continue;
|
|
875
|
+
seen.add(qualified);
|
|
876
|
+
|
|
877
|
+
const policyStatements: string[] = [];
|
|
878
|
+
for (const rule of getEffectiveSecurityRules(collection)) {
|
|
879
|
+
policyStatements.push(...generatePolicyStatements(collection, rule, resolveCollection));
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
plans.push({
|
|
883
|
+
schema,
|
|
884
|
+
table: baseTableName,
|
|
885
|
+
qualified,
|
|
886
|
+
enableRls: `ALTER TABLE "${schema}"."${baseTableName}" ENABLE ROW LEVEL SECURITY;`,
|
|
887
|
+
policyStatements
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
// Junctions are derived from `through` relations rather than declared, so
|
|
892
|
+
// the walk above never sees them.
|
|
893
|
+
for (const spec of resolveJunctionSpecs(collections).values()) {
|
|
894
|
+
const qualified = `${spec.schema}.${spec.table}`;
|
|
895
|
+
if (seen.has(qualified)) continue;
|
|
896
|
+
seen.add(qualified);
|
|
897
|
+
|
|
898
|
+
const junctionCollection = getJunctionCollectionConfig(spec);
|
|
899
|
+
const policyStatements: string[] = [];
|
|
900
|
+
for (const rule of getJunctionSecurityRules(spec)) {
|
|
901
|
+
policyStatements.push(...generatePolicyStatements(junctionCollection, rule, resolveCollection));
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
plans.push({
|
|
905
|
+
schema: spec.schema,
|
|
906
|
+
table: spec.table,
|
|
907
|
+
qualified,
|
|
908
|
+
enableRls: `ALTER TABLE "${spec.schema}"."${spec.table}" ENABLE ROW LEVEL SECURITY;`,
|
|
909
|
+
policyStatements
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
return plans;
|
|
914
|
+
};
|
|
915
|
+
|
|
916
|
+
export const generatePostgresPoliciesDdl = (allCollections: CollectionConfig[]): string => {
|
|
917
|
+
// Also the expectation `checkPolicyDrift` reconciles the database against,
|
|
918
|
+
// so a non-SQL collection filtered out here is one the drift report cannot
|
|
919
|
+
// invent a missing policy for.
|
|
920
|
+
const collections = relationalCollections(allCollections);
|
|
474
921
|
let ddl = "-- This file contains RLS policies generated by Rebase. Applied separately from migrations.\n\n";
|
|
475
922
|
|
|
476
923
|
const allTablesToGenerate = new Map<string, {
|