@rebasepro/server-postgres 0.12.0 → 0.12.1-canary.g009ed95

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.
Files changed (91) hide show
  1. package/dist/PostgresBackendDriver.d.ts +1 -1
  2. package/dist/PostgresBootstrapper.d.ts +25 -1
  3. package/dist/auth/services.d.ts +21 -0
  4. package/dist/backup/backup-service.d.ts +10 -1
  5. package/dist/backup/pg-tools.d.ts +47 -0
  6. package/dist/backup-service-CD8o_1Sl.js +8999 -0
  7. package/dist/backup-service-CD8o_1Sl.js.map +1 -0
  8. package/dist/cli-helpers.d.ts +39 -0
  9. package/dist/connection-BuZ97wsr.js +250 -0
  10. package/dist/connection-BuZ97wsr.js.map +1 -0
  11. package/dist/connection.d.ts +42 -0
  12. package/dist/ensure-collection-policies-BrUVgjz3.js +57 -0
  13. package/dist/ensure-collection-policies-BrUVgjz3.js.map +1 -0
  14. package/dist/ensure-collection-tables-Da2oGkX2.js +650 -0
  15. package/dist/ensure-collection-tables-Da2oGkX2.js.map +1 -0
  16. package/dist/index.es.js +816 -9679
  17. package/dist/index.es.js.map +1 -1
  18. package/dist/policy-CeA1JcxP.js +105 -0
  19. package/dist/policy-CeA1JcxP.js.map +1 -0
  20. package/dist/schema/auth-schema.d.ts +83 -144
  21. package/dist/schema/ensure-collection-policies.d.ts +60 -0
  22. package/dist/schema/ensure-collection-tables.d.ts +44 -2
  23. package/dist/schema/generate-postgres-ddl-logic.d.ts +135 -1
  24. package/dist/schema/introspect-db-constraints.d.ts +57 -0
  25. package/dist/schema/introspect-db-logic.d.ts +94 -5
  26. package/dist/schema/introspect-db-queries.d.ts +119 -0
  27. package/dist/schema/introspect-db-structure.d.ts +263 -0
  28. package/dist/schema/introspect-db-types.d.ts +11 -0
  29. package/dist/services/FetchService.d.ts +4 -1
  30. package/dist/services/RelationService.d.ts +24 -1
  31. package/dist/services/channel-bus/index.d.ts +1 -7
  32. package/dist/services/collection-helpers.d.ts +24 -1
  33. package/dist/services/dataService.d.ts +3 -1
  34. package/dist/services/row-pipeline.d.ts +1 -1
  35. package/dist/{src-BbFOPJ1S.js → src-CzbghKwf.js} +271 -173
  36. package/dist/src-CzbghKwf.js.map +1 -0
  37. package/dist/{src-Zqwaw3P5.js → src-DoU9yPqq.js} +3 -159
  38. package/dist/src-DoU9yPqq.js.map +1 -0
  39. package/dist/utils/connection-string.d.ts +29 -0
  40. package/dist/utils/drizzle-conditions.d.ts +5 -4
  41. package/dist/utils/pg-error-utils.d.ts +19 -0
  42. package/dist/websocket-B2LsrINK.js +530 -0
  43. package/dist/websocket-B2LsrINK.js.map +1 -0
  44. package/package.json +14 -14
  45. package/src/PostgresAdapter.ts +21 -2
  46. package/src/PostgresBackendDriver.ts +4 -0
  47. package/src/PostgresBootstrapper.ts +192 -33
  48. package/src/auth/ensure-tables.ts +164 -9
  49. package/src/auth/services.ts +24 -2
  50. package/src/backup/backup-cli.ts +41 -2
  51. package/src/backup/backup-service.ts +38 -5
  52. package/src/backup/pg-tools.ts +96 -3
  53. package/src/cli-helpers.ts +70 -0
  54. package/src/cli.ts +44 -26
  55. package/src/collections/validate-relations.ts +15 -0
  56. package/src/connection.ts +73 -0
  57. package/src/data-transformer.ts +9 -3
  58. package/src/databasePoolManager.ts +5 -2
  59. package/src/schema/auth-schema.ts +30 -19
  60. package/src/schema/ensure-collection-policies.ts +105 -0
  61. package/src/schema/ensure-collection-tables.test.ts +105 -9
  62. package/src/schema/ensure-collection-tables.ts +220 -32
  63. package/src/schema/generate-drizzle-schema-logic.ts +23 -6
  64. package/src/schema/generate-postgres-ddl-logic.ts +382 -19
  65. package/src/schema/introspect-db-constraints.ts +385 -0
  66. package/src/schema/introspect-db-inference.ts +18 -8
  67. package/src/schema/introspect-db-logic.ts +385 -71
  68. package/src/schema/introspect-db-queries.ts +326 -0
  69. package/src/schema/introspect-db-structure.ts +670 -0
  70. package/src/schema/introspect-db-types.ts +56 -0
  71. package/src/schema/introspect-db.ts +37 -80
  72. package/src/schema/introspect-runtime.test.ts +56 -8
  73. package/src/schema/introspect-runtime.ts +31 -9
  74. package/src/security/policy-drift.test.ts +11 -3
  75. package/src/services/FetchService.ts +76 -14
  76. package/src/services/PersistService.ts +20 -6
  77. package/src/services/RelationService.ts +249 -48
  78. package/src/services/channel-bus/index.ts +0 -9
  79. package/src/services/collection-helpers.ts +40 -1
  80. package/src/services/dataService.ts +3 -1
  81. package/src/services/realtimeService.ts +3 -3
  82. package/src/services/row-pipeline.ts +1 -1
  83. package/src/utils/connection-string.ts +58 -0
  84. package/src/utils/drizzle-conditions.ts +31 -6
  85. package/src/utils/pg-error-utils.ts +46 -0
  86. package/src/websocket.ts +18 -9
  87. package/dist/chunk-DSJWtz9O.js +0 -40
  88. package/dist/ensure-collection-tables-CNTcZGvn.js +0 -304
  89. package/dist/ensure-collection-tables-CNTcZGvn.js.map +0 -1
  90. package/dist/src-BbFOPJ1S.js.map +0 -1
  91. 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(quoteLiteral).join(", ")});`
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
- // A relation's own column is emitted by the DDL generator with a
196
- // foreign key; adding a bare column here would create the column
197
- // without the constraint and make the generator's later output
198
- // disagree with the database. Left to a real migration.
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
- const column = resolveColumnName(propName, p);
201
- if (present.has(column)) continue;
202
- const type = getSqlColumnType(propName, p, collection, collections);
203
- actions.push({
204
- kind: "add-column",
205
- target: `${key}.${column}`,
206
- // Never NOT NULL: an existing table with rows cannot take a
207
- // non-null column without a default, and inventing one would be
208
- // guessing at the customer's data.
209
- sql: `ALTER TABLE "${schema}"."${table}" ADD COLUMN IF NOT EXISTS "${column}" ${type};`
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
- return { actions, statements: actions.map(a => a.sql) };
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
- return { tables, enums };
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<EnsurePlan> {
269
- const schemas = Array.from(new Set(collections.map(schemaOf)));
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
- columnDefinition = `varchar("${colName}")`;
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.
@@ -547,8 +551,15 @@ export const generateSchema = async (collections: CollectionConfig[], stripPolic
547
551
  const tableVarName = getTableVarName(tableName);
548
552
  if (isJunction && relation && sourceCollection && isManyToMany(relation)) {
549
553
  const targetCollection = relation.target();
550
- const schema = (isPostgresCollectionConfig(targetCollection) ? targetCollection.schema : undefined) || (isPostgresCollectionConfig(sourceCollection) ? sourceCollection.schema : undefined);
551
- const tableCreator = schema ? `${schema}Schema.table` : "pgTable";
554
+ // Junctions live in `public`, full stop `resolveJunctionSpecs`
555
+ // hardcodes that, so it is where `planJunctionTables` CREATEs them
556
+ // and where the derived RLS policies are applied. Inheriting the
557
+ // endpoint's schema here (as this used to) put the junction of any
558
+ // m2m onto `users` in `rebase`, while its policies were still
559
+ // created against `public.<junction>` — RLS enabled on one table,
560
+ // rows written to another. The three generators have to agree, and
561
+ // the other two already did.
562
+ const tableCreator = "pgTable";
552
563
  const baseTableName = tableName.includes(".") ? tableName.split(".").pop()! : tableName;
553
564
  const {
554
565
  sourceColumn,
@@ -774,8 +785,14 @@ export const generateSchema = async (collections: CollectionConfig[], stripPolic
774
785
  // name (e.g. "client_id") may differ from the property key
775
786
  // (e.g. "clientId") when `columnName` is set.
776
787
  const drizzleFieldKey = resolvePropertyKeyForColumn(collection, otherRel.foreignKeyOnTarget);
788
+ // The column the far side points at: its
789
+ // primary key, unless the link names another
790
+ // one with `sourceKey`.
791
+ const referencedKey = otherRel.sourceKey
792
+ ? resolvePropertyKeyForColumn(otherCollection, otherRel.sourceKey)
793
+ : getPrimaryKeyName(otherCollection);
777
794
  const synthKey = `_synth_${otherTableVar}_${drizzleFieldKey}`;
778
- tableRelations.push(` "${synthKey}": one(${otherTableVar}, {\n fields: [${tableVarName}.${drizzleFieldKey}],\n references: [${otherTableVar}.${getPrimaryKeyName(otherCollection)}],\n relationName: \"${drizzleRelationName}\"\n })`);
795
+ tableRelations.push(` "${synthKey}": one(${otherTableVar}, {\n fields: [${tableVarName}.${drizzleFieldKey}],\n references: [${otherTableVar}.${referencedKey}],\n relationName: \"${drizzleRelationName}\"\n })`);
779
796
  emittedRelationNames.add(deduplicationKey);
780
797
  }
781
798
  }