@rebasepro/server-postgres 0.12.1-canary.gf5f1d39 → 0.13.0
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 +1 -1
- package/dist/PostgresBootstrapper.d.ts +25 -1
- package/dist/auth/services.d.ts +21 -0
- package/dist/backup/backup-service.d.ts +10 -1
- package/dist/backup/pg-tools.d.ts +47 -0
- package/dist/backup-service-CD8o_1Sl.js +8999 -0
- package/dist/backup-service-CD8o_1Sl.js.map +1 -0
- package/dist/cli-helpers.d.ts +39 -0
- 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-ViG8XiPn.js +57 -0
- package/dist/ensure-collection-policies-ViG8XiPn.js.map +1 -0
- package/dist/ensure-collection-tables-CBQdOETu.js +650 -0
- package/dist/ensure-collection-tables-CBQdOETu.js.map +1 -0
- package/dist/index.es.js +1231 -10031
- package/dist/index.es.js.map +1 -1
- package/dist/policy-CeA1JcxP.js +105 -0
- package/dist/policy-CeA1JcxP.js.map +1 -0
- package/dist/schema/auth-schema.d.ts +83 -144
- package/dist/schema/ensure-collection-policies.d.ts +60 -0
- package/dist/schema/ensure-collection-tables.d.ts +44 -2
- package/dist/schema/generate-postgres-ddl-logic.d.ts +135 -1
- 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/services/FetchService.d.ts +4 -1
- 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 +3 -1
- package/dist/services/row-pipeline.d.ts +1 -1
- package/dist/{src-BbFOPJ1S.js → src-DlPBctw_.js} +299 -173
- package/dist/src-DlPBctw_.js.map +1 -0
- package/dist/{src-Zqwaw3P5.js → src-DoU9yPqq.js} +3 -159
- package/dist/src-DoU9yPqq.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-B2LsrINK.js +530 -0
- package/dist/websocket-B2LsrINK.js.map +1 -0
- package/package.json +14 -14
- package/src/PostgresAdapter.ts +21 -2
- package/src/PostgresBackendDriver.ts +4 -0
- package/src/PostgresBootstrapper.ts +192 -33
- package/src/auth/ensure-tables.ts +164 -9
- 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 +70 -0
- package/src/cli.ts +44 -26
- package/src/collections/validate-relations.ts +15 -0
- package/src/connection.ts +73 -0
- package/src/data-transformer.ts +9 -3
- package/src/databasePoolManager.ts +5 -2
- package/src/schema/auth-schema.ts +30 -19
- 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 +220 -32
- package/src/schema/generate-drizzle-schema-logic.ts +42 -6
- package/src/schema/generate-postgres-ddl-logic.ts +382 -19
- 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/security/policy-drift.test.ts +11 -3
- package/src/services/BranchService.ts +66 -28
- package/src/services/FetchService.ts +90 -14
- package/src/services/PersistService.ts +20 -6
- package/src/services/RelationService.ts +249 -48
- package/src/services/channel-bus/index.ts +0 -9
- package/src/services/collection-helpers.ts +69 -3
- package/src/services/dataService.ts +3 -1
- package/src/services/realtimeService.ts +3 -3
- 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/src-BbFOPJ1S.js.map +0 -1
- package/dist/src-Zqwaw3P5.js.map +0 -1
|
@@ -27,10 +27,14 @@
|
|
|
27
27
|
*/
|
|
28
28
|
import { type CollectionConfig, type Property, isPostgresCollectionConfig } from "@rebasepro/types";
|
|
29
29
|
import { getTableName } from "@rebasepro/common";
|
|
30
|
+
import { logger } from "@rebasepro/server";
|
|
30
31
|
import {
|
|
31
32
|
getSqlColumnType,
|
|
32
33
|
resolveColumnName,
|
|
33
|
-
isIdProperty
|
|
34
|
+
isIdProperty,
|
|
35
|
+
planRelationalColumns,
|
|
36
|
+
planJunctionTables,
|
|
37
|
+
quoteSqlLiteral
|
|
34
38
|
} from "./generate-postgres-ddl-logic";
|
|
35
39
|
|
|
36
40
|
/**
|
|
@@ -62,10 +66,18 @@ export interface ExistingSchema {
|
|
|
62
66
|
tables: Map<string, Set<string>>;
|
|
63
67
|
/** `schema.typename` of every enum type that already exists. */
|
|
64
68
|
enums: Set<string>;
|
|
69
|
+
/**
|
|
70
|
+
* `schema.table.constraint` of every constraint that already exists.
|
|
71
|
+
*
|
|
72
|
+
* Optional so a caller that only cares about tables can still build one by
|
|
73
|
+
* hand; absent is read as "none known", which at worst re-attempts a
|
|
74
|
+
* constraint that then fails harmlessly as a duplicate.
|
|
75
|
+
*/
|
|
76
|
+
constraints?: Set<string>;
|
|
65
77
|
}
|
|
66
78
|
|
|
67
79
|
export interface EnsureAction {
|
|
68
|
-
kind: "create-enum" | "create-table" | "add-column";
|
|
80
|
+
kind: "create-enum" | "create-table" | "add-column" | "add-constraint" | "rename-column";
|
|
69
81
|
/** Qualified target, for logging: `public.posts` or `public.posts.title`. */
|
|
70
82
|
target: string;
|
|
71
83
|
sql: string;
|
|
@@ -75,6 +87,39 @@ export interface EnsurePlan {
|
|
|
75
87
|
actions: EnsureAction[];
|
|
76
88
|
/** Every statement, in dependency order. Empty when the schema is current. */
|
|
77
89
|
statements: string[];
|
|
90
|
+
/**
|
|
91
|
+
* Relation columns this plan is about to create where the table already
|
|
92
|
+
* carries the same column under its pre-singularization name.
|
|
93
|
+
*
|
|
94
|
+
* The reason this is reported rather than silently handled: the ensure is
|
|
95
|
+
* additive, so it would add `category_id` beside a populated
|
|
96
|
+
* `categorie_id` and the relation would then read the new, empty one. No
|
|
97
|
+
* statement fails, no table is missing, and the only symptom is relations
|
|
98
|
+
* resolving to nothing — which is indistinguishable from having no data.
|
|
99
|
+
*/
|
|
100
|
+
legacyForeignKeys: LegacyForeignKey[];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** A relation column whose old and new spellings both plausibly apply. */
|
|
104
|
+
export interface LegacyForeignKey {
|
|
105
|
+
/** `schema.table`. */
|
|
106
|
+
table: string;
|
|
107
|
+
/** The name the current rule derives, and what this plan would create. */
|
|
108
|
+
expected: string;
|
|
109
|
+
/** The name the old rule derived, which the table already has. */
|
|
110
|
+
legacy: string;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface EnsureOutcome extends EnsurePlan {
|
|
114
|
+
/**
|
|
115
|
+
* Constraints that could not be added — always non-fatal.
|
|
116
|
+
*
|
|
117
|
+
* A foreign key can only fail on data that already violates it, and the
|
|
118
|
+
* column it would police exists either way, so the collection still serves.
|
|
119
|
+
* Refusing to boot over one would turn a pre-existing data problem into an
|
|
120
|
+
* outage. Reported loudly instead.
|
|
121
|
+
*/
|
|
122
|
+
failures: { target: string; error: string }[];
|
|
78
123
|
}
|
|
79
124
|
|
|
80
125
|
function schemaOf(collection: CollectionConfig): string {
|
|
@@ -114,11 +159,6 @@ function requiredEnums(collection: CollectionConfig): { name: string; values: st
|
|
|
114
159
|
return out;
|
|
115
160
|
}
|
|
116
161
|
|
|
117
|
-
/** Single-quote escaping for an enum label. */
|
|
118
|
-
function quoteLiteral(value: string): string {
|
|
119
|
-
return `'${value.replace(/'/g, "''")}'`;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
162
|
/**
|
|
123
163
|
* Decide what to add. Pure — the caller supplies what exists and runs the result.
|
|
124
164
|
*
|
|
@@ -143,7 +183,7 @@ export function planCollectionSchemaEnsure(
|
|
|
143
183
|
actions.push({
|
|
144
184
|
kind: "create-enum",
|
|
145
185
|
target: name,
|
|
146
|
-
sql: `CREATE TYPE "${schema}"."${typeName}" AS ENUM (${values.map(
|
|
186
|
+
sql: `CREATE TYPE "${schema}"."${typeName}" AS ENUM (${values.map(quoteSqlLiteral).join(", ")});`
|
|
147
187
|
});
|
|
148
188
|
}
|
|
149
189
|
}
|
|
@@ -183,35 +223,137 @@ export function planCollectionSchemaEnsure(
|
|
|
183
223
|
});
|
|
184
224
|
}
|
|
185
225
|
|
|
226
|
+
// 2b. Junction tables behind many-to-many relations. No collection declares
|
|
227
|
+
// them, so the walk above never sees them — and until they existed, an
|
|
228
|
+
// m2m write had nowhere to land and the junction's derived RLS had
|
|
229
|
+
// nothing to attach to.
|
|
230
|
+
const junctions = planJunctionTables(collections);
|
|
231
|
+
for (const junction of junctions) {
|
|
232
|
+
const key = `${junction.schema}.${junction.table}`;
|
|
233
|
+
if (existing.tables.has(key) || created.has(key)) continue;
|
|
234
|
+
created.add(key);
|
|
235
|
+
actions.push({ kind: "create-table", target: key, sql: junction.createTable });
|
|
236
|
+
}
|
|
237
|
+
|
|
186
238
|
// 3. Missing columns, on both brand-new and pre-existing tables.
|
|
239
|
+
const legacyForeignKeys: LegacyForeignKey[] = [];
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Move a relation column that is only missing because it was renamed.
|
|
243
|
+
*
|
|
244
|
+
* Returns true when it handled the column, so the caller skips the ordinary
|
|
245
|
+
* ADD. Adding here would be the wrong move and a quiet one: the data is in
|
|
246
|
+
* the old column, `ADD COLUMN` creates the new one empty beside it, every
|
|
247
|
+
* statement succeeds, and the relation reads the empty one. A rename is
|
|
248
|
+
* metadata-only in Postgres, keeps the values, and carries the column's
|
|
249
|
+
* indexes and constraints with it.
|
|
250
|
+
*
|
|
251
|
+
* Only ever reached when the new name is absent and the old name is
|
|
252
|
+
* present, so there is nothing to overwrite and nothing to choose between.
|
|
253
|
+
*/
|
|
254
|
+
const renameLegacyColumn = (
|
|
255
|
+
key: string,
|
|
256
|
+
schema: string,
|
|
257
|
+
table: string,
|
|
258
|
+
column: string,
|
|
259
|
+
legacyName: string | undefined
|
|
260
|
+
): boolean => {
|
|
261
|
+
const present = existing.tables.get(key);
|
|
262
|
+
if (!legacyName || !present) return false;
|
|
263
|
+
if (present.has(column) || !present.has(legacyName)) return false;
|
|
264
|
+
|
|
265
|
+
legacyForeignKeys.push({ table: key, expected: column, legacy: legacyName });
|
|
266
|
+
actions.push({
|
|
267
|
+
kind: "rename-column",
|
|
268
|
+
target: `${key}.${column}`,
|
|
269
|
+
sql: `ALTER TABLE "${schema}"."${table}" RENAME COLUMN "${legacyName}" TO "${column}";`
|
|
270
|
+
});
|
|
271
|
+
return true;
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
const addColumn = (
|
|
275
|
+
key: string,
|
|
276
|
+
schema: string,
|
|
277
|
+
table: string,
|
|
278
|
+
column: string,
|
|
279
|
+
type: string
|
|
280
|
+
): void => {
|
|
281
|
+
const present = existing.tables.get(key);
|
|
282
|
+
if (present?.has(column)) return;
|
|
283
|
+
actions.push({
|
|
284
|
+
kind: "add-column",
|
|
285
|
+
target: `${key}.${column}`,
|
|
286
|
+
// Never NOT NULL: an existing table with rows cannot take a
|
|
287
|
+
// non-null column without a default, and inventing one would be
|
|
288
|
+
// guessing at the customer's data.
|
|
289
|
+
sql: `ALTER TABLE "${schema}"."${table}" ADD COLUMN IF NOT EXISTS "${column}" ${type};`
|
|
290
|
+
});
|
|
291
|
+
};
|
|
292
|
+
|
|
187
293
|
for (const collection of collections) {
|
|
188
294
|
const key = qualified(collection);
|
|
189
295
|
const schema = schemaOf(collection);
|
|
190
296
|
const table = getTableName(collection);
|
|
191
|
-
const present = existing.tables.get(key) ?? new Set<string>();
|
|
192
297
|
for (const [propName, prop] of Object.entries(collection.properties ?? {})) {
|
|
193
298
|
const p = prop as Property;
|
|
194
299
|
if (isIdProperty(propName, p, collection)) continue;
|
|
195
|
-
//
|
|
196
|
-
//
|
|
197
|
-
//
|
|
198
|
-
//
|
|
300
|
+
// Relation and reference columns are planned from the shared
|
|
301
|
+
// relational planner below, which derives the column name, type and
|
|
302
|
+
// foreign key the same way `db push` does. Deriving them here as
|
|
303
|
+
// plain columns is what once produced a column with no constraint.
|
|
199
304
|
if (p.type === "reference" || p.type === "relation") continue;
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
305
|
+
addColumn(key, schema, table, resolveColumnName(propName, p), getSqlColumnType(propName, p, collection, collections));
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// 3b. A junction that already existed, but is short a column. One created
|
|
310
|
+
// above already carries both — unlike a collection table, whose CREATE
|
|
311
|
+
// declares only the identity column — so re-listing them would log two
|
|
312
|
+
// no-op statements and inflate the count of changes applied.
|
|
313
|
+
for (const junction of junctions) {
|
|
314
|
+
const key = `${junction.schema}.${junction.table}`;
|
|
315
|
+
if (created.has(key)) continue;
|
|
316
|
+
for (const column of junction.columns) {
|
|
317
|
+
if (renameLegacyColumn(key, junction.schema, junction.table, column.name, column.legacyName)) continue;
|
|
318
|
+
addColumn(key, junction.schema, junction.table, column.name, column.type);
|
|
211
319
|
}
|
|
212
320
|
}
|
|
213
321
|
|
|
214
|
-
|
|
322
|
+
// 3c. The columns relation and reference properties own.
|
|
323
|
+
for (const relational of planRelationalColumns(collections)) {
|
|
324
|
+
const relKey = `${relational.schema}.${relational.table}`;
|
|
325
|
+
if (renameLegacyColumn(relKey, relational.schema, relational.table, relational.column, relational.legacyColumn)) continue;
|
|
326
|
+
addColumn(
|
|
327
|
+
relKey,
|
|
328
|
+
relational.schema,
|
|
329
|
+
relational.table,
|
|
330
|
+
relational.column,
|
|
331
|
+
relational.type
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// 4. Foreign keys, last: the tables and columns on both ends have to exist
|
|
336
|
+
// first, and a constraint is the one thing here that can fail on data
|
|
337
|
+
// rather than on schema, so nothing else depends on it.
|
|
338
|
+
const knownConstraints = existing.constraints ?? new Set<string>();
|
|
339
|
+
const plannedConstraints = new Set<string>();
|
|
340
|
+
const foreignKeys = [
|
|
341
|
+
...planRelationalColumns(collections).map(r => r.foreignKey),
|
|
342
|
+
...junctions.flatMap(j => j.foreignKeys)
|
|
343
|
+
];
|
|
344
|
+
for (const fk of foreignKeys) {
|
|
345
|
+
if (!fk) continue;
|
|
346
|
+
const name = `${fk.schema}.${fk.table}.${fk.constraintName}`;
|
|
347
|
+
if (knownConstraints.has(name) || plannedConstraints.has(name)) continue;
|
|
348
|
+
plannedConstraints.add(name);
|
|
349
|
+
actions.push({
|
|
350
|
+
kind: "add-constraint",
|
|
351
|
+
target: `${fk.schema}.${fk.table}.${fk.constraintName}`,
|
|
352
|
+
sql: fk.sql
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return { actions, statements: actions.map(a => a.sql), legacyForeignKeys };
|
|
215
357
|
}
|
|
216
358
|
|
|
217
359
|
/** Read what the database has, for the schemas the collections live in. */
|
|
@@ -250,7 +392,23 @@ export async function readExistingSchema(
|
|
|
250
392
|
);
|
|
251
393
|
for (const row of enumRows) enums.add(`${row.schema}.${row.name}`);
|
|
252
394
|
|
|
253
|
-
|
|
395
|
+
// `ADD CONSTRAINT` has no IF NOT EXISTS, so an existing foreign key is
|
|
396
|
+
// skipped by name rather than guarded in SQL.
|
|
397
|
+
const constraints = new Set<string>();
|
|
398
|
+
const { rows: constraintRows } = await client.query<{
|
|
399
|
+
schema: string;
|
|
400
|
+
table: string;
|
|
401
|
+
name: string;
|
|
402
|
+
}>(
|
|
403
|
+
`SELECT n.nspname AS schema, c.relname AS table, con.conname AS name
|
|
404
|
+
FROM pg_constraint con
|
|
405
|
+
JOIN pg_class c ON con.conrelid = c.oid
|
|
406
|
+
JOIN pg_namespace n ON c.relnamespace = n.oid
|
|
407
|
+
WHERE n.nspname IN (${inList})`
|
|
408
|
+
);
|
|
409
|
+
for (const row of constraintRows) constraints.add(`${row.schema}.${row.table}.${row.name}`);
|
|
410
|
+
|
|
411
|
+
return { tables, enums, constraints };
|
|
254
412
|
}
|
|
255
413
|
|
|
256
414
|
/**
|
|
@@ -265,8 +423,14 @@ export async function ensureCollectionTables(
|
|
|
265
423
|
client: Queryable,
|
|
266
424
|
collections: CollectionConfig[],
|
|
267
425
|
log?: (message: string) => void
|
|
268
|
-
): Promise<
|
|
269
|
-
|
|
426
|
+
): Promise<EnsureOutcome> {
|
|
427
|
+
// Junctions live alongside the collections that declare them, so their
|
|
428
|
+
// schema has to be read too — otherwise an existing junction reads as
|
|
429
|
+
// missing and its constraints as unplanned.
|
|
430
|
+
const schemas = Array.from(new Set([
|
|
431
|
+
...collections.map(schemaOf),
|
|
432
|
+
...planJunctionTables(collections).map(j => j.schema)
|
|
433
|
+
]));
|
|
270
434
|
for (const schema of schemas) {
|
|
271
435
|
assertSafeIdentifier(schema, "schema name");
|
|
272
436
|
if (schema !== "public") {
|
|
@@ -276,10 +440,26 @@ export async function ensureCollectionTables(
|
|
|
276
440
|
|
|
277
441
|
const existing = await readExistingSchema(client, schemas);
|
|
278
442
|
const plan = planCollectionSchemaEnsure(collections, existing);
|
|
443
|
+
const failures: { target: string; error: string }[] = [];
|
|
444
|
+
|
|
445
|
+
// Reported, not warned: this is a rename the ensure is about to perform, and
|
|
446
|
+
// the operator should be able to see in the log why a column changed name.
|
|
447
|
+
// The interesting case is the one that no longer happens — before this,
|
|
448
|
+
// ensure added the new column empty beside the populated old one, every
|
|
449
|
+
// statement succeeded, and the relation read the empty one.
|
|
450
|
+
for (const legacy of plan.legacyForeignKeys) {
|
|
451
|
+
const message =
|
|
452
|
+
`Renaming "${legacy.table}"."${legacy.legacy}" to "${legacy.expected}". The old name is ` +
|
|
453
|
+
"the one Rebase derived for this relation before it singularized properly; the column " +
|
|
454
|
+
"keeps its data, indexes and constraints. To keep the old name instead, set " +
|
|
455
|
+
`\`localKey: "${legacy.legacy}"\` on the relation and this will stop.`;
|
|
456
|
+
logger.info(`[schema] ${message}`);
|
|
457
|
+
log?.(message);
|
|
458
|
+
}
|
|
279
459
|
|
|
280
460
|
if (plan.actions.length === 0) {
|
|
281
461
|
log?.("Schema is up to date; nothing to create.");
|
|
282
|
-
return plan;
|
|
462
|
+
return { ...plan, failures };
|
|
283
463
|
}
|
|
284
464
|
|
|
285
465
|
for (const action of plan.actions) {
|
|
@@ -287,11 +467,19 @@ export async function ensureCollectionTables(
|
|
|
287
467
|
await client.query(action.sql);
|
|
288
468
|
log?.(`${action.kind}: ${action.target}`);
|
|
289
469
|
} catch (err) {
|
|
470
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
471
|
+
// A foreign key is the only action that can fail on the customer's
|
|
472
|
+
// data rather than on the schema. The column it polices is already
|
|
473
|
+
// there, so the collection serves either way — record it and carry
|
|
474
|
+
// on rather than crash-looping the deployment.
|
|
475
|
+
if (action.kind === "add-constraint") {
|
|
476
|
+
failures.push({ target: action.target, error: message });
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
290
479
|
throw new Error(
|
|
291
|
-
`Failed to ${action.kind} ${action.target}: `
|
|
292
|
-
`${err instanceof Error ? err.message : String(err)}\n ${action.sql}`
|
|
480
|
+
`Failed to ${action.kind} ${action.target}: ${message}\n ${action.sql}`
|
|
293
481
|
);
|
|
294
482
|
}
|
|
295
483
|
}
|
|
296
|
-
return plan;
|
|
484
|
+
return { ...plan, failures };
|
|
297
485
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { CollectionConfig, NumberProperty, Property, ResolvedRelation, RelationProperty, SecurityOperation, SecurityRule, StringProperty, isPostgresCollectionConfig, DateProperty, ArrayProperty, MapProperty, ReferenceProperty, VectorProperty, BinaryProperty, isManyToMany, type ResolvedManyToMany, type ResolvedBelongsTo, type ResolvedForeignKeyOnTarget, hasForeignKeyOnTarget } from "@rebasepro/types";
|
|
2
2
|
import { getPrimaryKeys } from "../services/collection-helpers";
|
|
3
|
-
import { getEnumVarName, getTableName, getTableVarName, resolveCollectionRelations, findRelation, securityRuleToConditions, policyToPostgres, getEffectiveSecurityRules, resolveJunctionSpecs, getJunctionSecurityRules, getJunctionCollectionConfig } from "@rebasepro/common";
|
|
3
|
+
import { getEnumVarName, getTableName, getTableVarName, resolveCollectionRelations, findRelation, securityRuleToConditions, policyToPostgres, getEffectiveSecurityRules, resolveJunctionSpecs, getJunctionSecurityRules, getJunctionCollectionConfig, resolveStringColumnLength } from "@rebasepro/common";
|
|
4
4
|
import { toSnakeCase, getPolicyNamesForRule } from "@rebasepro/utils";
|
|
5
5
|
import { logger } from "@rebasepro/server";
|
|
6
6
|
// --- Helper Functions ---
|
|
@@ -98,9 +98,13 @@ const getDrizzleColumn = (propName: string, prop: Property, collection: Collecti
|
|
|
98
98
|
} else if (stringProp.columnType === "uuid") {
|
|
99
99
|
columnDefinition = `uuid("${colName}")`;
|
|
100
100
|
} else if (stringProp.columnType === "char") {
|
|
101
|
-
columnDefinition = `char("${colName}")`;
|
|
101
|
+
columnDefinition = `char("${colName}", { length: ${resolveStringColumnLength(stringProp)} })`;
|
|
102
102
|
} else if (stringProp.columnType === "varchar") {
|
|
103
|
-
|
|
103
|
+
// The length is not optional decoration: `varchar("col")` with
|
|
104
|
+
// no length is an UNBOUNDED varchar in Postgres, which is what
|
|
105
|
+
// this emitted while the DDL generator emitted VARCHAR(255) for
|
|
106
|
+
// the very same property.
|
|
107
|
+
columnDefinition = `varchar("${colName}", { length: ${resolveStringColumnLength(stringProp)} })`;
|
|
104
108
|
} else {
|
|
105
109
|
// `text` is the default, and the only length-unbounded choice.
|
|
106
110
|
// Ask for `varchar` explicitly if you want the length constraint.
|
|
@@ -133,6 +137,25 @@ const getDrizzleColumn = (propName: string, prop: Property, collection: Collecti
|
|
|
133
137
|
let baseType = (numProp.validation?.integer || isId) ? `integer("${colName}")` : `numeric("${colName}")`;
|
|
134
138
|
if (numProp.columnType) {
|
|
135
139
|
if (numProp.columnType === "double precision") baseType = `doublePrecision("${colName}")`;
|
|
140
|
+
// `bigint` and `bigserial` are the only pg-core builders that
|
|
141
|
+
// *require* a config argument: without `mode`, drizzle cannot
|
|
142
|
+
// know whether to hand back a `number` or a `bigint`, and the
|
|
143
|
+
// emitted call does not typecheck.
|
|
144
|
+
//
|
|
145
|
+
// This is why `schema.generated.ts` drifted. Regenerating it
|
|
146
|
+
// produced a file that would not compile, so the bigint lines
|
|
147
|
+
// were hand-patched — and every regeneration after that looked
|
|
148
|
+
// like a large, alarming diff nobody wanted to ship. The file
|
|
149
|
+
// then sat stale for long enough that a security fix to two RLS
|
|
150
|
+
// policies never reached production.
|
|
151
|
+
//
|
|
152
|
+
// `number` rather than `bigint`: these are counters and byte
|
|
153
|
+
// totals that every caller already treats as numbers, and
|
|
154
|
+
// switching the runtime type would be a breaking change to
|
|
155
|
+
// every consumer of the generated schema.
|
|
156
|
+
else if (numProp.columnType === "bigint" || numProp.columnType === "bigserial") {
|
|
157
|
+
baseType = `${numProp.columnType}("${colName}", { mode: "number" })`;
|
|
158
|
+
}
|
|
136
159
|
else baseType = `${numProp.columnType}("${colName}")`;
|
|
137
160
|
}
|
|
138
161
|
|
|
@@ -547,8 +570,15 @@ export const generateSchema = async (collections: CollectionConfig[], stripPolic
|
|
|
547
570
|
const tableVarName = getTableVarName(tableName);
|
|
548
571
|
if (isJunction && relation && sourceCollection && isManyToMany(relation)) {
|
|
549
572
|
const targetCollection = relation.target();
|
|
550
|
-
|
|
551
|
-
|
|
573
|
+
// Junctions live in `public`, full stop — `resolveJunctionSpecs`
|
|
574
|
+
// hardcodes that, so it is where `planJunctionTables` CREATEs them
|
|
575
|
+
// and where the derived RLS policies are applied. Inheriting the
|
|
576
|
+
// endpoint's schema here (as this used to) put the junction of any
|
|
577
|
+
// m2m onto `users` in `rebase`, while its policies were still
|
|
578
|
+
// created against `public.<junction>` — RLS enabled on one table,
|
|
579
|
+
// rows written to another. The three generators have to agree, and
|
|
580
|
+
// the other two already did.
|
|
581
|
+
const tableCreator = "pgTable";
|
|
552
582
|
const baseTableName = tableName.includes(".") ? tableName.split(".").pop()! : tableName;
|
|
553
583
|
const {
|
|
554
584
|
sourceColumn,
|
|
@@ -774,8 +804,14 @@ export const generateSchema = async (collections: CollectionConfig[], stripPolic
|
|
|
774
804
|
// name (e.g. "client_id") may differ from the property key
|
|
775
805
|
// (e.g. "clientId") when `columnName` is set.
|
|
776
806
|
const drizzleFieldKey = resolvePropertyKeyForColumn(collection, otherRel.foreignKeyOnTarget);
|
|
807
|
+
// The column the far side points at: its
|
|
808
|
+
// primary key, unless the link names another
|
|
809
|
+
// one with `sourceKey`.
|
|
810
|
+
const referencedKey = otherRel.sourceKey
|
|
811
|
+
? resolvePropertyKeyForColumn(otherCollection, otherRel.sourceKey)
|
|
812
|
+
: getPrimaryKeyName(otherCollection);
|
|
777
813
|
const synthKey = `_synth_${otherTableVar}_${drizzleFieldKey}`;
|
|
778
|
-
tableRelations.push(` "${synthKey}": one(${otherTableVar}, {\n fields: [${tableVarName}.${drizzleFieldKey}],\n references: [${otherTableVar}.${
|
|
814
|
+
tableRelations.push(` "${synthKey}": one(${otherTableVar}, {\n fields: [${tableVarName}.${drizzleFieldKey}],\n references: [${otherTableVar}.${referencedKey}],\n relationName: \"${drizzleRelationName}\"\n })`);
|
|
779
815
|
emittedRelationNames.add(deduplicationKey);
|
|
780
816
|
}
|
|
781
817
|
}
|