@rebasepro/server-postgres 0.12.1-canary.gcfa333c → 0.12.1-canary.gddbd489
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 +6 -1
- package/dist/backup/backup-service.d.ts +10 -1
- package/dist/backup/pg-tools.d.ts +47 -0
- package/dist/{backup-service-DH9kPg-E.js → backup-service-CD8o_1Sl.js} +144 -11
- package/dist/backup-service-CD8o_1Sl.js.map +1 -0
- package/dist/cli-helpers.d.ts +39 -0
- package/dist/{connection-B5Wndbr1.js → connection-BuZ97wsr.js} +57 -3
- package/dist/connection-BuZ97wsr.js.map +1 -0
- package/dist/connection.d.ts +42 -0
- package/dist/{ensure-collection-policies-Vl3Cv1Q9.js → ensure-collection-policies-ViG8XiPn.js} +2 -2
- package/dist/{ensure-collection-policies-Vl3Cv1Q9.js.map → ensure-collection-policies-ViG8XiPn.js.map} +1 -1
- package/dist/{ensure-collection-tables-DsDsNl6o.js → ensure-collection-tables-CBQdOETu.js} +72 -12
- package/dist/ensure-collection-tables-CBQdOETu.js.map +1 -0
- package/dist/index.es.js +990 -677
- 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/ensure-collection-tables.d.ts +21 -1
- package/dist/schema/generate-postgres-ddl-logic.d.ts +19 -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/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-DihrDFuP.js → src-DlPBctw_.js} +217 -101
- package/dist/src-DlPBctw_.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 +16 -0
- package/dist/{websocket-BKcGvILX.js → websocket-B2LsrINK.js} +8 -6
- package/dist/websocket-B2LsrINK.js.map +1 -0
- package/package.json +6 -6
- package/src/PostgresAdapter.ts +7 -2
- package/src/PostgresBackendDriver.ts +4 -0
- package/src/PostgresBootstrapper.ts +83 -22
- package/src/auth/services.ts +6 -3
- 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/ensure-collection-tables.ts +81 -10
- package/src/schema/generate-drizzle-schema-logic.ts +35 -3
- package/src/schema/generate-postgres-ddl-logic.ts +50 -6
- 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/security/policy-drift.test.ts +11 -3
- package/src/services/FetchService.ts +90 -14
- package/src/services/PersistService.ts +20 -6
- package/src/services/RelationService.ts +211 -45
- 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 +19 -0
- package/src/websocket.ts +18 -9
- package/dist/backup-service-DH9kPg-E.js.map +0 -1
- package/dist/connection-B5Wndbr1.js.map +0 -1
- package/dist/ensure-collection-tables-DsDsNl6o.js.map +0 -1
- package/dist/src-DihrDFuP.js.map +0 -1
- package/dist/websocket-BKcGvILX.js.map +0 -1
|
@@ -27,12 +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
34
|
isIdProperty,
|
|
34
35
|
planRelationalColumns,
|
|
35
|
-
planJunctionTables
|
|
36
|
+
planJunctionTables,
|
|
37
|
+
quoteSqlLiteral
|
|
36
38
|
} from "./generate-postgres-ddl-logic";
|
|
37
39
|
|
|
38
40
|
/**
|
|
@@ -75,7 +77,7 @@ export interface ExistingSchema {
|
|
|
75
77
|
}
|
|
76
78
|
|
|
77
79
|
export interface EnsureAction {
|
|
78
|
-
kind: "create-enum" | "create-table" | "add-column" | "add-constraint";
|
|
80
|
+
kind: "create-enum" | "create-table" | "add-column" | "add-constraint" | "rename-column";
|
|
79
81
|
/** Qualified target, for logging: `public.posts` or `public.posts.title`. */
|
|
80
82
|
target: string;
|
|
81
83
|
sql: string;
|
|
@@ -85,6 +87,27 @@ export interface EnsurePlan {
|
|
|
85
87
|
actions: EnsureAction[];
|
|
86
88
|
/** Every statement, in dependency order. Empty when the schema is current. */
|
|
87
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;
|
|
88
111
|
}
|
|
89
112
|
|
|
90
113
|
export interface EnsureOutcome extends EnsurePlan {
|
|
@@ -136,11 +159,6 @@ function requiredEnums(collection: CollectionConfig): { name: string; values: st
|
|
|
136
159
|
return out;
|
|
137
160
|
}
|
|
138
161
|
|
|
139
|
-
/** Single-quote escaping for an enum label. */
|
|
140
|
-
function quoteLiteral(value: string): string {
|
|
141
|
-
return `'${value.replace(/'/g, "''")}'`;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
162
|
/**
|
|
145
163
|
* Decide what to add. Pure — the caller supplies what exists and runs the result.
|
|
146
164
|
*
|
|
@@ -165,7 +183,7 @@ export function planCollectionSchemaEnsure(
|
|
|
165
183
|
actions.push({
|
|
166
184
|
kind: "create-enum",
|
|
167
185
|
target: name,
|
|
168
|
-
sql: `CREATE TYPE "${schema}"."${typeName}" AS ENUM (${values.map(
|
|
186
|
+
sql: `CREATE TYPE "${schema}"."${typeName}" AS ENUM (${values.map(quoteSqlLiteral).join(", ")});`
|
|
169
187
|
});
|
|
170
188
|
}
|
|
171
189
|
}
|
|
@@ -218,6 +236,41 @@ export function planCollectionSchemaEnsure(
|
|
|
218
236
|
}
|
|
219
237
|
|
|
220
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
|
+
|
|
221
274
|
const addColumn = (
|
|
222
275
|
key: string,
|
|
223
276
|
schema: string,
|
|
@@ -261,14 +314,17 @@ export function planCollectionSchemaEnsure(
|
|
|
261
314
|
const key = `${junction.schema}.${junction.table}`;
|
|
262
315
|
if (created.has(key)) continue;
|
|
263
316
|
for (const column of junction.columns) {
|
|
317
|
+
if (renameLegacyColumn(key, junction.schema, junction.table, column.name, column.legacyName)) continue;
|
|
264
318
|
addColumn(key, junction.schema, junction.table, column.name, column.type);
|
|
265
319
|
}
|
|
266
320
|
}
|
|
267
321
|
|
|
268
322
|
// 3c. The columns relation and reference properties own.
|
|
269
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;
|
|
270
326
|
addColumn(
|
|
271
|
-
|
|
327
|
+
relKey,
|
|
272
328
|
relational.schema,
|
|
273
329
|
relational.table,
|
|
274
330
|
relational.column,
|
|
@@ -297,7 +353,7 @@ export function planCollectionSchemaEnsure(
|
|
|
297
353
|
});
|
|
298
354
|
}
|
|
299
355
|
|
|
300
|
-
return { actions, statements: actions.map(a => a.sql) };
|
|
356
|
+
return { actions, statements: actions.map(a => a.sql), legacyForeignKeys };
|
|
301
357
|
}
|
|
302
358
|
|
|
303
359
|
/** Read what the database has, for the schemas the collections live in. */
|
|
@@ -386,6 +442,21 @@ export async function ensureCollectionTables(
|
|
|
386
442
|
const plan = planCollectionSchemaEnsure(collections, existing);
|
|
387
443
|
const failures: { target: string; error: string }[] = [];
|
|
388
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
|
+
}
|
|
459
|
+
|
|
389
460
|
if (plan.actions.length === 0) {
|
|
390
461
|
log?.("Schema is up to date; nothing to create.");
|
|
391
462
|
return { ...plan, failures };
|
|
@@ -137,6 +137,25 @@ const getDrizzleColumn = (propName: string, prop: Property, collection: Collecti
|
|
|
137
137
|
let baseType = (numProp.validation?.integer || isId) ? `integer("${colName}")` : `numeric("${colName}")`;
|
|
138
138
|
if (numProp.columnType) {
|
|
139
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
|
+
}
|
|
140
159
|
else baseType = `${numProp.columnType}("${colName}")`;
|
|
141
160
|
}
|
|
142
161
|
|
|
@@ -551,8 +570,15 @@ export const generateSchema = async (collections: CollectionConfig[], stripPolic
|
|
|
551
570
|
const tableVarName = getTableVarName(tableName);
|
|
552
571
|
if (isJunction && relation && sourceCollection && isManyToMany(relation)) {
|
|
553
572
|
const targetCollection = relation.target();
|
|
554
|
-
|
|
555
|
-
|
|
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";
|
|
556
582
|
const baseTableName = tableName.includes(".") ? tableName.split(".").pop()! : tableName;
|
|
557
583
|
const {
|
|
558
584
|
sourceColumn,
|
|
@@ -778,8 +804,14 @@ export const generateSchema = async (collections: CollectionConfig[], stripPolic
|
|
|
778
804
|
// name (e.g. "client_id") may differ from the property key
|
|
779
805
|
// (e.g. "clientId") when `columnName` is set.
|
|
780
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);
|
|
781
813
|
const synthKey = `_synth_${otherTableVar}_${drizzleFieldKey}`;
|
|
782
|
-
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 })`);
|
|
783
815
|
emittedRelationNames.add(deduplicationKey);
|
|
784
816
|
}
|
|
785
817
|
}
|
|
@@ -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 } from "@rebasepro/types";
|
|
2
2
|
import { getEnumVarName, getTableName, resolveCollectionRelations, findRelation, securityRuleToConditions, policyToPostgres, getEffectiveSecurityRules, getInjectedSecurityRules, resolveJunctionSpecs, getJunctionSecurityRules, getJunctionCollectionConfig, resolveStringColumnLength } from "@rebasepro/common";
|
|
3
|
-
import { toSnakeCase, getPolicyNamesForRule } from "@rebasepro/utils";
|
|
3
|
+
import { toSnakeCase, getPolicyNamesForRule, generateForeignKeyName, legacyForeignKeyName } from "@rebasepro/utils";
|
|
4
4
|
|
|
5
5
|
// --- Helper Functions ---
|
|
6
6
|
|
|
@@ -116,6 +116,16 @@ const generateSinglePolicyStatements = (collection: CollectionConfig, rule: Secu
|
|
|
116
116
|
return [drop, create];
|
|
117
117
|
};
|
|
118
118
|
|
|
119
|
+
/**
|
|
120
|
+
* Single-quote escaping for a SQL string literal (PostgreSQL doubles the
|
|
121
|
+
* quote). Enum labels come straight from user-authored collection config, so a
|
|
122
|
+
* label like `it's` closes the literal early and the whole generated file stops
|
|
123
|
+
* parsing at the `CREATE TYPE`. Lives here rather than next to its other caller
|
|
124
|
+
* because ensure-collection-tables already imports from this module — the
|
|
125
|
+
* reverse would be a cycle.
|
|
126
|
+
*/
|
|
127
|
+
export const quoteSqlLiteral = (value: string): string => `'${value.replace(/'/g, "''")}'`;
|
|
128
|
+
|
|
119
129
|
export const getSqlColumnType = (propName: string, prop: Property, collection: CollectionConfig, collections: CollectionConfig[]): string => {
|
|
120
130
|
switch (prop.type) {
|
|
121
131
|
case "string": {
|
|
@@ -242,6 +252,13 @@ export const generatePostgresDdl = async (
|
|
|
242
252
|
if (uniqueSchemas.length > 0) ddl += "\n";
|
|
243
253
|
|
|
244
254
|
// 2. Generate Enums
|
|
255
|
+
//
|
|
256
|
+
// The enum type name is derived from table + column, so two collections
|
|
257
|
+
// mapped onto the same table — or two properties whose `columnName`
|
|
258
|
+
// resolves to the same column — land on the same name. `CREATE TYPE` has no
|
|
259
|
+
// IF NOT EXISTS, so emitting it twice aborts the whole file on the second
|
|
260
|
+
// statement. Dedupe by name, matching planCollectionSchemaEnsure.
|
|
261
|
+
const emittedEnums = new Set<string>();
|
|
245
262
|
collections.forEach(collection => {
|
|
246
263
|
const collectionTable = getTableName(collection);
|
|
247
264
|
const schema = isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : "public";
|
|
@@ -253,8 +270,9 @@ export const generatePostgresDdl = async (
|
|
|
253
270
|
String(typeof v === "object" && v !== null && "id" in v ? v.id : v)
|
|
254
271
|
)
|
|
255
272
|
: Object.keys(prop.enum);
|
|
256
|
-
if (values.length > 0) {
|
|
257
|
-
|
|
273
|
+
if (values.length > 0 && !emittedEnums.has(`${schema}.${enumDbName}`)) {
|
|
274
|
+
emittedEnums.add(`${schema}.${enumDbName}`);
|
|
275
|
+
ddl += `CREATE TYPE "${schema}"."${enumDbName}" AS ENUM (${values.map(quoteSqlLiteral).join(", ")});\n`;
|
|
258
276
|
}
|
|
259
277
|
}
|
|
260
278
|
});
|
|
@@ -530,6 +548,15 @@ export interface RelationalColumnPlan {
|
|
|
530
548
|
type: string;
|
|
531
549
|
/** Absent when the target collection is not part of this bundle. */
|
|
532
550
|
foreignKey?: ForeignKeyPlan;
|
|
551
|
+
/**
|
|
552
|
+
* What this column would have been called before `generateForeignKeyName`
|
|
553
|
+
* learned to singularize — set only when the two differ and the column name
|
|
554
|
+
* is the derived default rather than one the author wrote.
|
|
555
|
+
*
|
|
556
|
+
* Carried so the boot-time ensure can notice a database provisioned under
|
|
557
|
+
* the old rule. It is never used to name anything.
|
|
558
|
+
*/
|
|
559
|
+
legacyColumn?: string;
|
|
533
560
|
}
|
|
534
561
|
|
|
535
562
|
/** The table behind a many-to-many `through` relation. */
|
|
@@ -537,7 +564,7 @@ export interface JunctionTablePlan {
|
|
|
537
564
|
schema: string;
|
|
538
565
|
/** Bare table name, no schema prefix. */
|
|
539
566
|
table: string;
|
|
540
|
-
columns: { name: string; type: string }[];
|
|
567
|
+
columns: { name: string; type: string; legacyName?: string }[];
|
|
541
568
|
/** Both endpoint columns plus the composite primary key. */
|
|
542
569
|
createTable: string;
|
|
543
570
|
foreignKeys: ForeignKeyPlan[];
|
|
@@ -613,10 +640,16 @@ export const planRelationalColumns = (collections: CollectionConfig[]): Relation
|
|
|
613
640
|
if (!targetCollection) continue;
|
|
614
641
|
|
|
615
642
|
const required = prop.validation?.required;
|
|
643
|
+
// Only a *derived* column name can be affected by the change to
|
|
644
|
+
// `generateForeignKeyName`; one the author wrote is theirs.
|
|
645
|
+
const relationName = refProp.relation?.relationName ?? propName;
|
|
646
|
+
const legacyKey = legacyForeignKeyName(relationName);
|
|
647
|
+
const derived = relInfo.localKey === generateForeignKeyName(relationName);
|
|
616
648
|
plans.push({
|
|
617
649
|
schema,
|
|
618
650
|
table,
|
|
619
651
|
column: relInfo.localKey,
|
|
652
|
+
legacyColumn: derived && legacyKey !== relInfo.localKey ? legacyKey : undefined,
|
|
620
653
|
type: getSqlColumnType(propName, prop, collection, collections),
|
|
621
654
|
foreignKey: foreignKeyPlan({
|
|
622
655
|
schema,
|
|
@@ -675,9 +708,20 @@ export const planJunctionTables = (collections: CollectionConfig[]): JunctionTab
|
|
|
675
708
|
|
|
676
709
|
for (const spec of resolveJunctionSpecs(collections).values()) {
|
|
677
710
|
const [source, target] = spec.endpoints;
|
|
711
|
+
// A junction column's default name is derived from the endpoint
|
|
712
|
+
// collection's slug — which is normally plural, so this is where the
|
|
713
|
+
// singularization change actually lands: a `categories` endpoint used
|
|
714
|
+
// to give `categorie_id` and now gives `category_id`. Recorded, not
|
|
715
|
+
// used, so boot-ensure can recognise a table built under the old rule.
|
|
716
|
+
const legacyFor = (endpoint: typeof source): string | undefined => {
|
|
717
|
+
const slug = toSnakeCase(endpoint.collection.slug ?? endpoint.collection.name ?? "");
|
|
718
|
+
const legacy = legacyForeignKeyName(slug);
|
|
719
|
+
const derived = endpoint.junctionColumn === generateForeignKeyName(slug);
|
|
720
|
+
return derived && legacy !== endpoint.junctionColumn ? legacy : undefined;
|
|
721
|
+
};
|
|
678
722
|
const columns = [
|
|
679
|
-
{ name: source.junctionColumn, type: junctionKeyType(source.collection) },
|
|
680
|
-
{ name: target.junctionColumn, type: junctionKeyType(target.collection) }
|
|
723
|
+
{ name: source.junctionColumn, type: junctionKeyType(source.collection), legacyName: legacyFor(source) },
|
|
724
|
+
{ name: target.junctionColumn, type: junctionKeyType(target.collection), legacyName: legacyFor(target) }
|
|
681
725
|
];
|
|
682
726
|
// Every declaring side agrees on the edge's lifetime; the first one wins,
|
|
683
727
|
// as it does in the generator's walk.
|