@rebasepro/server-postgres 0.22.0 → 0.24.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/{BranchService-CucnFcSE.js → BranchService-BRt78gfa.js} +35 -18
- package/dist/BranchService-BRt78gfa.js.map +1 -0
- package/dist/PostgresBackendDriver.d.ts +116 -4
- package/dist/auth/services.d.ts +99 -22
- package/dist/{auth-users-columns-D2LBFrMH.js → auth-users-columns-C72EMoDJ.js} +17 -1
- package/dist/{auth-users-columns-D2LBFrMH.js.map → auth-users-columns-C72EMoDJ.js.map} +1 -1
- package/dist/backup/backup-cli.d.ts +22 -0
- package/dist/{backup-cli-DqBakiMO.js → backup-cli-DW5p9_zv.js} +17 -14
- package/dist/backup-cli-DW5p9_zv.js.map +1 -0
- package/dist/{backup-service-CaMOS76G.js → backup-service-EwcDVG-8.js} +7 -9
- package/dist/{backup-service-CaMOS76G.js.map → backup-service-EwcDVG-8.js.map} +1 -1
- package/dist/{cli-errors-Dka89exj.js → cli-errors-DsA-K9uP.js} +101 -1
- package/dist/cli-errors-DsA-K9uP.js.map +1 -0
- package/dist/cli-errors.d.ts +42 -0
- package/dist/cli-flags-BglvpjHv.js +138 -0
- package/dist/cli-flags-BglvpjHv.js.map +1 -0
- package/dist/cli-flags.d.ts +28 -0
- package/dist/cli-helpers.d.ts +18 -12
- package/dist/cli-scratch-database.d.ts +34 -0
- package/dist/cli.js +177 -287
- package/dist/cli.js.map +1 -1
- package/dist/{column-plan-helpers-CpILzHJS.js → column-plan-helpers-1-LQD0yI.js} +44 -38
- package/dist/column-plan-helpers-1-LQD0yI.js.map +1 -0
- package/dist/data-transformer.d.ts +0 -8
- package/dist/{doctor-CU9IogdL.js → doctor-C-sYWbmt.js} +228 -46
- package/dist/doctor-C-sYWbmt.js.map +1 -0
- package/dist/{ensure-collection-policies-Dzd-2S81.js → ensure-collection-policies-B1ureSIV.js} +73 -18
- package/dist/ensure-collection-policies-B1ureSIV.js.map +1 -0
- package/dist/{ensure-collection-tables-CpAgy51F.js → ensure-collection-tables-kkkHk8oo.js} +124 -37
- package/dist/ensure-collection-tables-kkkHk8oo.js.map +1 -0
- package/dist/{ensure-tables-Cr5B4UmH.js → ensure-tables-BmI_tRxc.js} +10 -3
- package/dist/ensure-tables-BmI_tRxc.js.map +1 -0
- package/dist/{generate-drizzle-schema-B537GIvz.js → generate-drizzle-schema-93M0lUxK.js} +2 -2
- package/dist/{generate-drizzle-schema-B537GIvz.js.map → generate-drizzle-schema-93M0lUxK.js.map} +1 -1
- package/dist/{generate-drizzle-schema-logic-BcMl7VSy.js → generate-drizzle-schema-logic-sSFDp6LR.js} +26 -8
- package/dist/generate-drizzle-schema-logic-sSFDp6LR.js.map +1 -0
- package/dist/generate-postgres-ddl-logic-BJsLaVNX.js +152 -0
- package/dist/generate-postgres-ddl-logic-BJsLaVNX.js.map +1 -0
- package/dist/generated-sql.d.ts +28 -0
- package/dist/index.es.js +4768 -1117
- package/dist/index.es.js.map +1 -1
- package/dist/{introspect-db-logic-C6LQdTxj.js → introspect-db-logic-kCETE8TY.js} +532 -39
- package/dist/introspect-db-logic-kCETE8TY.js.map +1 -0
- package/dist/introspect-db-queries-C_Q5VgQw.js +317 -0
- package/dist/introspect-db-queries-C_Q5VgQw.js.map +1 -0
- package/dist/{plan-schema-CboAIwLN.js → plan-schema-DU9exq6C.js} +291 -529
- package/dist/plan-schema-DU9exq6C.js.map +1 -0
- package/dist/{policy-drift-xJfy9xG7.js → policy-drift-B-J2hhm0.js} +3 -3
- package/dist/policy-drift-B-J2hhm0.js.map +1 -0
- package/dist/{generate-postgres-ddl-logic-D7imhYV8.js → render-ddl-Ds2t_d9V.js} +11 -149
- package/dist/render-ddl-Ds2t_d9V.js.map +1 -0
- package/dist/{rls-bootstrap-sql-H3rCFi3F.js → rls-bootstrap-sql-_KNnjanK.js} +562 -35
- package/dist/rls-bootstrap-sql-_KNnjanK.js.map +1 -0
- package/dist/{rls-enforcement-C6Xk0lA6.js → rls-enforcement-CfXOJJaW.js} +10 -2
- package/dist/rls-enforcement-CfXOJJaW.js.map +1 -0
- package/dist/schema/atlas-argv.d.ts +15 -0
- package/dist/schema/auth-schema.d.ts +170 -0
- package/dist/schema/classify-change.d.ts +29 -1
- package/dist/schema/column-plan-helpers.d.ts +51 -20
- package/dist/schema/destructive-sql.d.ts +71 -1
- package/dist/schema/doctor-cli.js +4 -4
- package/dist/schema/doctor.d.ts +34 -1
- package/dist/schema/ensure-collection-policies.d.ts +22 -0
- package/dist/schema/generate-drizzle-schema.js +1 -1
- package/dist/schema/generate-postgres-ddl-logic.d.ts +5 -5
- package/dist/schema/generate-postgres-ddl.js +1 -1
- package/dist/schema/generate-schema-commit.d.ts +12 -0
- package/dist/schema/introspect-db-logic.d.ts +31 -0
- package/dist/schema/introspect-db-queries.d.ts +1 -1
- package/dist/schema/introspect-db-search.d.ts +22 -0
- package/dist/schema/introspect-db-storage.d.ts +83 -0
- package/dist/schema/introspect-db.js +15 -314
- package/dist/schema/introspect-db.js.map +1 -1
- package/dist/schema/plan/diff-plan.d.ts +4 -3
- package/dist/schema/plan/plan-schema.d.ts +26 -11
- package/dist/schema/plan/render-ddl.d.ts +6 -0
- package/dist/schema/plan/types.d.ts +43 -12
- package/dist/search-column-BM-GV6vH.js +442 -0
- package/dist/search-column-BM-GV6vH.js.map +1 -0
- package/dist/security/policy-drift.d.ts +1 -1
- package/dist/security/rls-enforcement.d.ts +7 -0
- package/dist/services/BranchService.d.ts +22 -1
- package/dist/services/FetchService.d.ts +102 -32
- package/dist/services/PersistService.d.ts +41 -5
- package/dist/services/RelationService.d.ts +29 -0
- package/dist/services/RelationWriteService.d.ts +6 -0
- package/dist/services/cdc/CdcListener.d.ts +15 -5
- package/dist/services/cdc/identity-columns.d.ts +19 -0
- package/dist/services/cdc/trigger-cdc.d.ts +45 -7
- package/dist/services/channel-bus/PostgresChannelBus.d.ts +12 -7
- package/dist/services/channel-history.d.ts +17 -1
- package/dist/services/collection-helpers.d.ts +21 -0
- package/dist/services/dataService.d.ts +3 -20
- package/dist/services/field-op-sql.d.ts +71 -0
- package/dist/services/junction-writes.d.ts +19 -3
- package/dist/services/pg-notify-listener.d.ts +95 -6
- package/dist/services/read-field-access.d.ts +19 -0
- package/dist/services/realtimeService.d.ts +232 -44
- package/dist/services/row-pipeline.d.ts +5 -0
- package/dist/services/socket-liveness.d.ts +45 -0
- package/dist/services/soft-delete.d.ts +12 -2
- package/dist/services/sql-script.d.ts +71 -0
- package/dist/services/write-depth.d.ts +16 -0
- package/dist/services/write-transaction-scope.d.ts +42 -0
- package/dist/utils/drizzle-conditions.d.ts +49 -4
- package/dist/utils/sql-redaction.d.ts +21 -0
- package/dist/websocket.d.ts +57 -18
- package/package.json +9 -9
- package/dist/BranchService-CucnFcSE.js.map +0 -1
- package/dist/backup-cli-DqBakiMO.js.map +0 -1
- package/dist/cli-errors-Dka89exj.js.map +0 -1
- package/dist/column-plan-helpers-CpILzHJS.js.map +0 -1
- package/dist/doctor-CU9IogdL.js.map +0 -1
- package/dist/ensure-collection-policies-Dzd-2S81.js.map +0 -1
- package/dist/ensure-collection-tables-CpAgy51F.js.map +0 -1
- package/dist/ensure-tables-Cr5B4UmH.js.map +0 -1
- package/dist/generate-drizzle-schema-logic-BcMl7VSy.js.map +0 -1
- package/dist/generate-postgres-ddl-logic-D7imhYV8.js.map +0 -1
- package/dist/introspect-db-logic-C6LQdTxj.js.map +0 -1
- package/dist/plan-schema-CboAIwLN.js.map +0 -1
- package/dist/policy-drift-xJfy9xG7.js.map +0 -1
- package/dist/rls-bootstrap-sql-H3rCFi3F.js.map +0 -1
- package/dist/rls-enforcement-C6Xk0lA6.js.map +0 -1
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { createRequire as __createRequire } from "module";
|
|
2
2
|
__createRequire(import.meta.url);
|
|
3
|
-
import {
|
|
3
|
+
import { a as buildSearchColumnSpec, u as searchColumnStamps } from "./search-column-BM-GV6vH.js";
|
|
4
|
+
import { DEFAULT_SEARCH_COLUMN, DEFAULT_SEARCH_LANGUAGE } from "@rebasepro/types";
|
|
5
|
+
import { firstFreeKey, toPostgresIdentifier, toWireKey } from "@rebasepro/utils";
|
|
4
6
|
//#region src/schema/introspect-db-naming.ts
|
|
5
7
|
/**
|
|
6
8
|
* Naming helpers shared by the introspection modules. These live apart from
|
|
@@ -184,6 +186,359 @@ function mapPgType(dataType) {
|
|
|
184
186
|
if (dt === "uuid") return "string";
|
|
185
187
|
return "string";
|
|
186
188
|
}
|
|
189
|
+
//#endregion
|
|
190
|
+
//#region src/schema/introspect-db-storage.ts
|
|
191
|
+
var literal = (value) => JSON.stringify(value);
|
|
192
|
+
/** `character varying` → `varchar`, the spellings the planner's `columnType` uses. */
|
|
193
|
+
var NUMBER_COLUMN_TYPE = {
|
|
194
|
+
"integer": "integer",
|
|
195
|
+
"bigint": "bigint",
|
|
196
|
+
"real": "real",
|
|
197
|
+
"double precision": "double precision"
|
|
198
|
+
};
|
|
199
|
+
/** A sequence default — `nextval('orders_id_seq'::regclass)`. */
|
|
200
|
+
var SEQUENCE_DEFAULT = /^nextval\('([^']+)'::regclass\)$/;
|
|
201
|
+
/**
|
|
202
|
+
* Whether a `nextval` default names the sequence `SERIAL` would have made for
|
|
203
|
+
* this column — the only one the planner can declare, because it declares the
|
|
204
|
+
* column as `SERIAL` and lets Postgres name the sequence.
|
|
205
|
+
*/
|
|
206
|
+
var isConventionalSequence = (col, sequence) => {
|
|
207
|
+
return sequence.replace(/^.*\./, "").replace(/^"|"$/g, "") === serialSequenceName(col.table_name, col.column_name);
|
|
208
|
+
};
|
|
209
|
+
/**
|
|
210
|
+
* The name Postgres gives a `SERIAL` column's sequence — `ChooseRelationName`
|
|
211
|
+
* with the `seq` label: `<table>_<column>_seq`, and when that is over 63 bytes
|
|
212
|
+
* the longer of the two names is shortened, one character at a time, until it
|
|
213
|
+
* fits.
|
|
214
|
+
*/
|
|
215
|
+
function serialSequenceName(table, column) {
|
|
216
|
+
const available = 58;
|
|
217
|
+
let tableChars = table.length;
|
|
218
|
+
let columnChars = column.length;
|
|
219
|
+
while (tableChars + columnChars > available) if (tableChars > columnChars) tableChars--;
|
|
220
|
+
else columnChars--;
|
|
221
|
+
return `${table.slice(0, tableChars)}_${column.slice(0, columnChars)}_seq`;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* The `isId` (and `columnType`) a single-column primary key needs.
|
|
225
|
+
*
|
|
226
|
+
* `isId: "increment"` is an identity column, and only an INTEGER one. A serial
|
|
227
|
+
* key — an integer with a `nextval` default — is `SERIAL`/`BIGSERIAL`, declared
|
|
228
|
+
* through `columnType` beside `isId: "manual"`. A uuid key is `"uuid"` only when
|
|
229
|
+
* the database generates it; a key with no default is `"manual"`, which keeps
|
|
230
|
+
* the planner from inventing one.
|
|
231
|
+
*/
|
|
232
|
+
function primaryKeyStorage(col, propType) {
|
|
233
|
+
const dataType = col.data_type.toLowerCase();
|
|
234
|
+
const fallback = col.column_default?.trim();
|
|
235
|
+
if (propType === "number") {
|
|
236
|
+
if (col.is_identity === "YES") {
|
|
237
|
+
if (dataType === "integer") return { keys: [`isId: ${literal("increment")}`] };
|
|
238
|
+
return {
|
|
239
|
+
keys: [`isId: ${literal("increment")}`],
|
|
240
|
+
note: `"${col.column_name}" is a ${dataType} identity key; \`isId: "increment"\` is always INTEGER, so a push would plan a narrowing type change (and ask before applying it).`
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
const sequence = fallback ? SEQUENCE_DEFAULT.exec(fallback) : null;
|
|
244
|
+
if (sequence) {
|
|
245
|
+
const serial = dataType === "bigint" ? "bigserial" : dataType === "integer" ? "serial" : void 0;
|
|
246
|
+
if (serial && isConventionalSequence(col, sequence[1])) return { keys: [`isId: ${literal("manual")}`, `columnType: ${literal(serial)}`] };
|
|
247
|
+
return {
|
|
248
|
+
keys: [`isId: ${literal("manual")}`, ...numberColumnType(dataType)],
|
|
249
|
+
note: `"${col.column_name}" takes its value from the sequence ${sequence[1]}, which no property can declare; a push would plan dropping that default.`
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
if (fallback) return { keys: [`isId: ${literal(`sql\`${fallback}\``)}`, ...numberColumnType(dataType)] };
|
|
253
|
+
return { keys: [`isId: ${literal("manual")}`, ...numberColumnType(dataType)] };
|
|
254
|
+
}
|
|
255
|
+
if (dataType === "uuid") {
|
|
256
|
+
if (fallback === "gen_random_uuid()") return { keys: [`isId: ${literal("uuid")}`] };
|
|
257
|
+
if (fallback) return { keys: [`isId: ${literal(`sql\`${fallback}\``)}`, `columnType: ${literal("uuid")}`] };
|
|
258
|
+
return { keys: [`isId: ${literal("manual")}`, `columnType: ${literal("uuid")}`] };
|
|
259
|
+
}
|
|
260
|
+
const strategy = fallback ? `sql\`${fallback}\`` : "manual";
|
|
261
|
+
const stored = stringStorage(col);
|
|
262
|
+
return {
|
|
263
|
+
keys: [`isId: ${literal(strategy)}`, ...stored.keys],
|
|
264
|
+
note: stored.note
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
/** `columnType` for a number column the planner would otherwise make NUMERIC. */
|
|
268
|
+
function numberColumnType(dataType) {
|
|
269
|
+
const mapped = NUMBER_COLUMN_TYPE[dataType];
|
|
270
|
+
return mapped ? [`columnType: ${literal(mapped)}`] : [];
|
|
271
|
+
}
|
|
272
|
+
/** The keys a `string` column needs; the bound itself is emitted as `validation.max`. */
|
|
273
|
+
function stringStorage(col) {
|
|
274
|
+
switch (col.data_type.toLowerCase()) {
|
|
275
|
+
case "text": return { keys: [] };
|
|
276
|
+
case "character varying":
|
|
277
|
+
if (typeof col.character_maximum_length === "number" && col.character_maximum_length > 0) return { keys: [`columnType: ${literal("varchar")}`] };
|
|
278
|
+
return {
|
|
279
|
+
keys: [`columnType: ${literal("varchar")}`],
|
|
280
|
+
note: `"${col.column_name}" is a varchar with no length; the planner always writes one (255 unless \`validation.max\` says otherwise), so a push would plan a narrowing type change.`
|
|
281
|
+
};
|
|
282
|
+
case "character": return { keys: [`columnType: ${literal("char")}`] };
|
|
283
|
+
case "uuid": return { keys: [`columnType: ${literal("uuid")}`] };
|
|
284
|
+
default: return {
|
|
285
|
+
keys: [],
|
|
286
|
+
note: `"${col.column_name}" is ${col.data_type === "USER-DEFINED" ? col.udt_name : col.data_type}, which no property type stores; it is read as text, and a push would plan converting the column to text.`
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* The keys a non-key column needs, for the property type introspection chose.
|
|
292
|
+
*
|
|
293
|
+
* `enumTypeName` is the type the planner would name for this column
|
|
294
|
+
* (`<table>_<column>`, cut to 63 bytes); an enum column whose type is called
|
|
295
|
+
* anything else cannot be declared, and the note says how to rename it.
|
|
296
|
+
*/
|
|
297
|
+
function columnStorage(col, propType, context) {
|
|
298
|
+
const dataType = col.data_type.toLowerCase();
|
|
299
|
+
if (context.isEnumColumn) {
|
|
300
|
+
if (col.udt_name === context.enumTypeName) return { keys: [] };
|
|
301
|
+
return {
|
|
302
|
+
keys: [],
|
|
303
|
+
note: context.enumTypeShared ? `"${col.column_name}" uses the enum type "${col.udt_name}", which other columns share; Rebase gives each column its own type (<table>_<column>), so a push would plan "${context.enumTypeName}" and a conversion Postgres cannot cast.` : `"${col.column_name}" uses the enum type "${col.udt_name}", and Rebase names a column's enum type <table>_<column>. Rename it before pushing: ALTER TYPE "${col.udt_name}" RENAME TO "${context.enumTypeName}";`
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
switch (propType) {
|
|
307
|
+
case "number": {
|
|
308
|
+
if (dataType === "numeric") {
|
|
309
|
+
const keys = [];
|
|
310
|
+
if (typeof col.numeric_precision === "number") keys.push(`precision: ${col.numeric_precision}`);
|
|
311
|
+
if (typeof col.numeric_precision === "number" && typeof col.numeric_scale === "number") keys.push(`scale: ${col.numeric_scale}`);
|
|
312
|
+
return { keys };
|
|
313
|
+
}
|
|
314
|
+
if (dataType === "smallint") return {
|
|
315
|
+
keys: [`columnType: ${literal("integer")}`],
|
|
316
|
+
note: `"${col.column_name}" is smallint, which no \`columnType\` names; it is declared integer, so a push widens it (every value survives).`
|
|
317
|
+
};
|
|
318
|
+
const mapped = numberColumnType(dataType);
|
|
319
|
+
if (mapped.length > 0) return { keys: mapped };
|
|
320
|
+
return {
|
|
321
|
+
keys: [],
|
|
322
|
+
note: `"${col.column_name}" is ${col.data_type}, which no \`columnType\` names; it is declared numeric, and a push would plan converting it.`
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
case "string": return stringStorage(col);
|
|
326
|
+
case "date": switch (dataType) {
|
|
327
|
+
case "timestamp with time zone": return { keys: [] };
|
|
328
|
+
case "date": return { keys: [`columnType: ${literal("date")}`] };
|
|
329
|
+
case "time without time zone": return { keys: [`columnType: ${literal("time")}`] };
|
|
330
|
+
default: return {
|
|
331
|
+
keys: [],
|
|
332
|
+
note: `"${col.column_name}" is ${col.data_type}; a date property stores timestamptz, date or time, so a push would plan converting it to timestamptz (and ask before applying it).`
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
case "map": return dataType === "json" ? { keys: [`columnType: ${literal("json")}`] } : { keys: [] };
|
|
336
|
+
default: return { keys: [] };
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* The `columnType` an array column needs, or a note when its element type is
|
|
341
|
+
* not one the planner stores natively.
|
|
342
|
+
*
|
|
343
|
+
* Only `text[]`, `integer[]`, `boolean[]` and `numeric[]` are. Reading
|
|
344
|
+
* `bigint[]` as `integer[]` — what this used to do — planned a narrowing.
|
|
345
|
+
*/
|
|
346
|
+
function arrayStorage(col) {
|
|
347
|
+
switch (col.udt_name) {
|
|
348
|
+
case "_text": return {
|
|
349
|
+
columnType: "text[]",
|
|
350
|
+
innerType: "string"
|
|
351
|
+
};
|
|
352
|
+
case "_int4": return {
|
|
353
|
+
columnType: "integer[]",
|
|
354
|
+
innerType: "number"
|
|
355
|
+
};
|
|
356
|
+
case "_bool": return {
|
|
357
|
+
columnType: "boolean[]",
|
|
358
|
+
innerType: "boolean"
|
|
359
|
+
};
|
|
360
|
+
case "_numeric": return {
|
|
361
|
+
columnType: "numeric[]",
|
|
362
|
+
innerType: "number"
|
|
363
|
+
};
|
|
364
|
+
default: {
|
|
365
|
+
const element = col.udt_name.replace(/^_/, "");
|
|
366
|
+
return {
|
|
367
|
+
innerType: "string",
|
|
368
|
+
note: `"${col.column_name}" is an array of ${element}; arrays are stored as text[], integer[], boolean[] or numeric[] (anything else as jsonb), so a push would plan converting it.`
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* A column default as a `defaultValue`, when it is a literal a property can carry.
|
|
375
|
+
*
|
|
376
|
+
* `undefined` when there is no default to carry, or when the default is one the
|
|
377
|
+
* generator expresses another way (`now()` is `autoValue`, a key's default is
|
|
378
|
+
* its `isId`). `{ note }` when there is a default and no property can say it —
|
|
379
|
+
* a push would plan dropping it.
|
|
380
|
+
*/
|
|
381
|
+
function literalDefaultOf(col, propType) {
|
|
382
|
+
const raw = col.column_default?.trim();
|
|
383
|
+
if (!raw) return void 0;
|
|
384
|
+
if (/^(now\(\)|CURRENT_TIMESTAMP)$/i.test(raw)) return void 0;
|
|
385
|
+
const quoted = /^'((?:[^']|'')*)'(?:::[\w\s."[\]]+)?$/.exec(raw);
|
|
386
|
+
const unquoted = quoted ? quoted[1].replace(/''/g, "'") : void 0;
|
|
387
|
+
switch (propType) {
|
|
388
|
+
case "string":
|
|
389
|
+
if (unquoted !== void 0) return { source: literal(unquoted) };
|
|
390
|
+
break;
|
|
391
|
+
case "number": {
|
|
392
|
+
const text = unquoted ?? raw.replace(/^\((.*)\)$/, "$1").replace(/::[\w\s]+$/, "");
|
|
393
|
+
if (/^-?\d+(\.\d+)?$/.test(text) && Number.isFinite(Number(text))) return { source: String(Number(text)) };
|
|
394
|
+
break;
|
|
395
|
+
}
|
|
396
|
+
case "boolean":
|
|
397
|
+
if (/^true$/i.test(raw)) return { source: "true" };
|
|
398
|
+
if (/^false$/i.test(raw)) return { source: "false" };
|
|
399
|
+
break;
|
|
400
|
+
case "map":
|
|
401
|
+
if (unquoted !== void 0) try {
|
|
402
|
+
const parsed = JSON.parse(unquoted);
|
|
403
|
+
if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) return { source: JSON.stringify(parsed) };
|
|
404
|
+
} catch {}
|
|
405
|
+
break;
|
|
406
|
+
default: break;
|
|
407
|
+
}
|
|
408
|
+
return { note: `"${col.column_name}" defaults to ${raw}, which no \`defaultValue\` can express; a push would plan dropping that default.` };
|
|
409
|
+
}
|
|
410
|
+
/** `ON DELETE` as the relation's `onDelete`, when it is not the default the planner would choose. */
|
|
411
|
+
function onDeleteOf(fk, required) {
|
|
412
|
+
const rule = fk.delete_rule?.toUpperCase();
|
|
413
|
+
if (!rule) return void 0;
|
|
414
|
+
if (rule === (required ? "RESTRICT" : "SET NULL")) return void 0;
|
|
415
|
+
return rule.toLowerCase();
|
|
416
|
+
}
|
|
417
|
+
//#endregion
|
|
418
|
+
//#region src/schema/introspect-db-search.ts
|
|
419
|
+
/**
|
|
420
|
+
* A collection's `search` block, read back out of the column it built.
|
|
421
|
+
*
|
|
422
|
+
* A Rebase search column is `tsvector GENERATED ALWAYS AS (…) STORED`, stamped
|
|
423
|
+
* with a fingerprint of the expression in its `COMMENT`. Introspection used to
|
|
424
|
+
* emit it as an ordinary read-only string property, which the planner makes a
|
|
425
|
+
* `text` column — so the first push after adopting a Rebase database planned
|
|
426
|
+
* `ALTER COLUMN "search_vector" TYPE text … DROP EXPRESSION`, and search was
|
|
427
|
+
* gone for whoever approved it.
|
|
428
|
+
*
|
|
429
|
+
* Reading the block back is a guess checked by the stamp: the deparsed
|
|
430
|
+
* expression names each field's column, weight and language, which is enough
|
|
431
|
+
* to write a candidate block; the candidate is then compiled by the very code
|
|
432
|
+
* that built the column, and kept only if its fingerprint is the one the column
|
|
433
|
+
* carries. A block that does not reproduce the stamp is not emitted at all —
|
|
434
|
+
* the note says so — because a wrong one would rebuild the column differently.
|
|
435
|
+
*
|
|
436
|
+
* `mode` and `fuzzyThreshold` act at query time and are stored nowhere, so they
|
|
437
|
+
* come back as their defaults.
|
|
438
|
+
*/
|
|
439
|
+
/** One `setweight(to_tsvector('<lang>'::regconfig, <text>), '<W>'::"char")`, deparsed. */
|
|
440
|
+
var SEGMENT = /setweight\(to_tsvector\('((?:[^']|'')+)'::regconfig, ([\s\S]+?)\), '([A-D])'::"char"\)/g;
|
|
441
|
+
/** A property with the column's shape, for the candidate block to resolve against. */
|
|
442
|
+
function propertyFor(col) {
|
|
443
|
+
if (col.data_type === "text" || col.data_type === "character varying" || col.data_type === "character") return {
|
|
444
|
+
name: col.column_name,
|
|
445
|
+
type: "string",
|
|
446
|
+
columnName: col.column_name
|
|
447
|
+
};
|
|
448
|
+
if (col.udt_name === "_text") return {
|
|
449
|
+
name: col.column_name,
|
|
450
|
+
type: "array",
|
|
451
|
+
columnName: col.column_name,
|
|
452
|
+
of: {
|
|
453
|
+
name: col.column_name,
|
|
454
|
+
type: "string"
|
|
455
|
+
}
|
|
456
|
+
};
|
|
457
|
+
if (col.data_type === "jsonb") return {
|
|
458
|
+
name: col.column_name,
|
|
459
|
+
type: "map",
|
|
460
|
+
columnName: col.column_name,
|
|
461
|
+
keyValue: true
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* The search block a table's stamped generated columns were built from, or a
|
|
466
|
+
* note saying why it could not be read back.
|
|
467
|
+
*/
|
|
468
|
+
function reconstructSearchBlock(input) {
|
|
469
|
+
const stamped = input.columns.filter((col) => col.is_generated === "ALWAYS" && (input.comments.get(col.column_name) ?? "").startsWith("rebase:search:v1:"));
|
|
470
|
+
const main = stamped.find((col) => col.udt_name === "tsvector");
|
|
471
|
+
if (!main) return { columns: /* @__PURE__ */ new Set() };
|
|
472
|
+
const fuzzy = stamped.find((col) => col.column_name === `${main.column_name}_text`);
|
|
473
|
+
const columns = new Set(stamped.map((col) => col.column_name));
|
|
474
|
+
const unreadable = (why) => ({
|
|
475
|
+
columns,
|
|
476
|
+
note: `"${main.column_name}" is a Rebase search column whose \`search\` block could not be read back (${why}). Declare the block again before pushing — without it a push plans dropping the column.`
|
|
477
|
+
});
|
|
478
|
+
const expression = main.generation_expression ?? "";
|
|
479
|
+
const byColumn = new Map(input.columns.map((col) => [col.column_name, col]));
|
|
480
|
+
const fields = [];
|
|
481
|
+
let language;
|
|
482
|
+
let unaccent = false;
|
|
483
|
+
for (const [, lang, inner, weight] of expression.matchAll(SEGMENT)) {
|
|
484
|
+
language ??= lang.replace(/''/g, "'");
|
|
485
|
+
if (lang.replace(/''/g, "'") !== language) return unreadable("its fields use different languages");
|
|
486
|
+
if (inner.includes("rebase_search_unaccent")) unaccent = true;
|
|
487
|
+
const column = [...inner.matchAll(/"?([A-Za-z_][A-Za-z0-9_$]*)"?/g)].map((m) => m[1]).find((name) => byColumn.has(name) && !columns.has(name));
|
|
488
|
+
if (!column) return unreadable("a field reads no column of this table");
|
|
489
|
+
const key = input.keyByColumn.get(column);
|
|
490
|
+
if (!key) return unreadable(`the column "${column}" is not a property`);
|
|
491
|
+
const jsonPath = [...inner.matchAll(/(?:->|#>)\s*'([^']+)'/g)].flatMap((m) => m[1].replace(/^\{|\}$/g, "").split(","));
|
|
492
|
+
fields.push({
|
|
493
|
+
path: [key, ...jsonPath].join("."),
|
|
494
|
+
weight
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
if (fields.length === 0 || !language) return unreadable("its expression is not one Rebase writes");
|
|
498
|
+
const search = {
|
|
499
|
+
...main.column_name !== DEFAULT_SEARCH_COLUMN ? { column: main.column_name } : {},
|
|
500
|
+
...language !== DEFAULT_SEARCH_LANGUAGE ? { language } : {},
|
|
501
|
+
...unaccent ? { unaccent: true } : {},
|
|
502
|
+
...fuzzy ? { fuzzy: true } : {},
|
|
503
|
+
fields: fields.map((f) => ({
|
|
504
|
+
path: f.path,
|
|
505
|
+
weight: f.weight
|
|
506
|
+
}))
|
|
507
|
+
};
|
|
508
|
+
const properties = {};
|
|
509
|
+
for (const [column, key] of input.keyByColumn) {
|
|
510
|
+
const col = byColumn.get(column);
|
|
511
|
+
const prop = col ? propertyFor(col) : void 0;
|
|
512
|
+
if (prop) properties[key] = prop;
|
|
513
|
+
}
|
|
514
|
+
const candidate = {
|
|
515
|
+
slug: input.table,
|
|
516
|
+
name: input.table,
|
|
517
|
+
table: input.table,
|
|
518
|
+
...input.schema !== "public" ? { schema: input.schema } : {},
|
|
519
|
+
properties,
|
|
520
|
+
search
|
|
521
|
+
};
|
|
522
|
+
let fingerprints;
|
|
523
|
+
try {
|
|
524
|
+
const spec = buildSearchColumnSpec(candidate);
|
|
525
|
+
if (!spec) return unreadable("no block compiles from it");
|
|
526
|
+
fingerprints = new Map(searchColumnStamps(spec).map((stamp) => [stamp.column, stamp.fingerprint]));
|
|
527
|
+
} catch (err) {
|
|
528
|
+
return unreadable(err instanceof Error ? err.message : String(err));
|
|
529
|
+
}
|
|
530
|
+
for (const col of stamped) if (fingerprints.get(col.column_name) !== input.comments.get(col.column_name)) return unreadable("the block read from its expression does not reproduce the column's stamp");
|
|
531
|
+
return {
|
|
532
|
+
columns,
|
|
533
|
+
block: `\n search: {\n ${[
|
|
534
|
+
...search.column ? [`column: ${JSON.stringify(search.column)}`] : [],
|
|
535
|
+
...search.language ? [`language: ${JSON.stringify(search.language)}`] : [],
|
|
536
|
+
...search.unaccent ? ["unaccent: true"] : [],
|
|
537
|
+
...search.fuzzy ? ["fuzzy: true"] : [],
|
|
538
|
+
`fields: [\n${fields.map((f) => ` { path: ${JSON.stringify(f.path)}, weight: ${JSON.stringify(f.weight)} }`).join(",\n")}\n ]`
|
|
539
|
+
].join(",\n ")}\n },`
|
|
540
|
+
};
|
|
541
|
+
}
|
|
187
542
|
/**
|
|
188
543
|
* A timestamp the database maintains: a temporal column defaulting to the
|
|
189
544
|
* transaction clock.
|
|
@@ -300,6 +655,7 @@ function buildContext(metadata, tables) {
|
|
|
300
655
|
inboundByTable.set(constraint.foreignTable, inbound);
|
|
301
656
|
}
|
|
302
657
|
return {
|
|
658
|
+
schema: metadata.schema,
|
|
303
659
|
tables,
|
|
304
660
|
constraintsByTable,
|
|
305
661
|
inboundByTable,
|
|
@@ -319,9 +675,14 @@ function buildContext(metadata, tables) {
|
|
|
319
675
|
* with attributes, and collapsing it into a many-to-many silently drops those
|
|
320
676
|
* attributes from the UI entirely;
|
|
321
677
|
* - nothing references it. A junction with its own dependents is something rows
|
|
322
|
-
* point *at*, so it needs an identity of its own
|
|
678
|
+
* point *at*, so it needs an identity of its own;
|
|
679
|
+
* - it is in `public`. That is the only schema Rebase reads and creates
|
|
680
|
+
* junction tables in (`resolveJunctionSpecs`), so a `manyToMany` folded from
|
|
681
|
+
* `crm.contacts_tags` would read and write an empty `public.contacts_tags`
|
|
682
|
+
* instead. Kept as a table, its rows stay where they are and are served.
|
|
323
683
|
*/
|
|
324
684
|
function classifyJunction(table, context) {
|
|
685
|
+
if (context.schema !== "public") return null;
|
|
325
686
|
const meta = context.tables.get(table);
|
|
326
687
|
if (!meta) return null;
|
|
327
688
|
const constraints = context.constraintsByTable.get(table) ?? [];
|
|
@@ -591,6 +952,34 @@ function deriveListProperties(propertiesOrder, hidden = /* @__PURE__ */ new Set(
|
|
|
591
952
|
* no process.exit. It is imported by introspect-db.ts (the CLI entry-point)
|
|
592
953
|
* and consumed directly by tests.
|
|
593
954
|
*/
|
|
955
|
+
/**
|
|
956
|
+
* Why a table keyed on several columns is not generated as a collection, or
|
|
957
|
+
* `undefined` when it is.
|
|
958
|
+
*
|
|
959
|
+
* Several `isId` properties are one composite key, and the planner builds that
|
|
960
|
+
* table under the same `PRIMARY KEY (a, b)` — so such a table round-trips when
|
|
961
|
+
* two things hold:
|
|
962
|
+
*
|
|
963
|
+
* - **Every key column can carry `isId`,** which exists on string and number
|
|
964
|
+
* properties only. Read from the column's SQL type, not a data sample: a
|
|
965
|
+
* `line_no` holding only 1s is not a boolean. A key with a column of another
|
|
966
|
+
* type — pagila's `payment` is keyed on a timestamp too — can be marked on
|
|
967
|
+
* some of its columns only, and those would be read as the whole key.
|
|
968
|
+
* - **Nothing points at it.** A foreign key into it, or a junction, is a link
|
|
969
|
+
* into a composite key, which the planner refuses: the relation has one
|
|
970
|
+
* column to point with.
|
|
971
|
+
*/
|
|
972
|
+
function compositeKeySkipReason(meta, allFks) {
|
|
973
|
+
if (meta.pks.length < 2) return void 0;
|
|
974
|
+
const unmarkable = meta.pks.filter((pk) => {
|
|
975
|
+
const column = meta.columns.find((c) => c.column_name === pk);
|
|
976
|
+
const type = column ? mapPgType(column.data_type) : void 0;
|
|
977
|
+
return type !== "string" && type !== "number";
|
|
978
|
+
});
|
|
979
|
+
if (unmarkable.length > 0) return `its key column${unmarkable.length > 1 ? "s" : ""} ${unmarkable.map((c) => `"${c}"`).join(", ")} cannot carry \`isId\`, which a string or number property takes`;
|
|
980
|
+
const referencing = [...new Set(allFks.filter((fk) => fk.foreign_table_name === meta.name && fk.table_name !== meta.name).map((fk) => fk.table_name))];
|
|
981
|
+
if (referencing.length > 0) return `${referencing.map((t) => `"${t}"`).join(", ")} point${referencing.length > 1 ? "" : "s"} at it, and a link cannot point at a composite key`;
|
|
982
|
+
}
|
|
594
983
|
var IRREGULAR_SINGULARS = {
|
|
595
984
|
people: "person",
|
|
596
985
|
children: "child",
|
|
@@ -948,6 +1337,8 @@ function withAdminOptions(extra, entries, emitAdmin = true) {
|
|
|
948
1337
|
if (extra.includes("admin: {")) return extra.replace("admin: {", `admin: {\n${block},`);
|
|
949
1338
|
return `${extra}\n admin: {\n${block}\n },`;
|
|
950
1339
|
}
|
|
1340
|
+
/** A SQL string literal. */
|
|
1341
|
+
var quoteSqlString = (value) => `'${value.replace(/'/g, "''")}'`;
|
|
951
1342
|
/** A TypeScript string literal, escaped. */
|
|
952
1343
|
function quote(value) {
|
|
953
1344
|
return JSON.stringify(value);
|
|
@@ -1052,6 +1443,9 @@ function generateCollectionFile(tableName, meta, allFks, joinTables, tablesMap,
|
|
|
1052
1443
|
};
|
|
1053
1444
|
let propsOutput = "";
|
|
1054
1445
|
let relationsOutput = "";
|
|
1446
|
+
/** What the planner will not reproduce exactly, column by column. */
|
|
1447
|
+
const fileNotes = [];
|
|
1448
|
+
const skippedTables = context.skippedTables ?? /* @__PURE__ */ new Set();
|
|
1055
1449
|
const orderEntries = [];
|
|
1056
1450
|
const propertyBlocks = /* @__PURE__ */ new Map();
|
|
1057
1451
|
/**
|
|
@@ -1069,25 +1463,45 @@ function generateCollectionFile(tableName, meta, allFks, joinTables, tablesMap,
|
|
|
1069
1463
|
const hiddenFromCollection = /* @__PURE__ */ new Set();
|
|
1070
1464
|
let columnIndex = 0;
|
|
1071
1465
|
const isCompositePk = meta.pks.length > 1;
|
|
1466
|
+
/**
|
|
1467
|
+
* The property type a column is generated as, with what the data sample
|
|
1468
|
+
* adds to it. A column of a composite key keeps its SQL type's reading:
|
|
1469
|
+
* it carries `isId`, which only a string or number property takes, and a
|
|
1470
|
+
* key column holding only 1s and 0s is still a key, not a boolean.
|
|
1471
|
+
*/
|
|
1472
|
+
const propertyTypeOf = (col) => {
|
|
1473
|
+
const isEnumColumn = col.data_type === "USER-DEFINED" && enumMap.get(col.udt_name) !== void 0;
|
|
1474
|
+
const propType = isEnumColumn ? "string" : col.udt_name === "vector" ? "vector" : mapPgType(col.data_type);
|
|
1475
|
+
if (isEnumColumn || !sampleData || sampleData.length === 0 || isCompositePk && meta.pks.includes(col.column_name)) return {
|
|
1476
|
+
propType,
|
|
1477
|
+
finalPropType: propType,
|
|
1478
|
+
inferenceExtra: ""
|
|
1479
|
+
};
|
|
1480
|
+
const values = sampleData.map((r) => r[col.column_name]);
|
|
1481
|
+
const inferred = inferPropertyFromData(col.column_name, col.data_type, propType, values, meta.pks.includes(col.column_name), emitAdmin);
|
|
1482
|
+
return {
|
|
1483
|
+
propType,
|
|
1484
|
+
finalPropType: inferred.propType || propType,
|
|
1485
|
+
inferenceExtra: inferred.extra || ""
|
|
1486
|
+
};
|
|
1487
|
+
};
|
|
1488
|
+
const compositeKeyMarkable = isCompositePk && meta.pks.every((pk) => {
|
|
1489
|
+
const keyColumn = meta.columns.find((c) => c.column_name === pk);
|
|
1490
|
+
if (!keyColumn) return false;
|
|
1491
|
+
const { finalPropType } = propertyTypeOf(keyColumn);
|
|
1492
|
+
return finalPropType === "string" || finalPropType === "number";
|
|
1493
|
+
});
|
|
1072
1494
|
for (const col of meta.columns) {
|
|
1495
|
+
if (col.is_generated === "ALWAYS" && (columnComments.get(col.column_name) ?? "").startsWith("rebase:search:v1:")) continue;
|
|
1073
1496
|
if (meta.fks.some((fk) => fk.column_name === col.column_name) && !meta.pks.includes(col.column_name)) continue;
|
|
1074
1497
|
const currentIndex = columnIndex++;
|
|
1075
1498
|
const propertyKey = firstFreeKey([toWireKey(col.column_name), col.column_name], propertyBlocks);
|
|
1076
1499
|
keyByColumn.set(col.column_name, propertyKey);
|
|
1077
1500
|
const colEnumValues = enumMap.get(col.udt_name);
|
|
1078
1501
|
const isEnumColumn = col.data_type === "USER-DEFINED" && colEnumValues !== void 0;
|
|
1079
|
-
const isVectorColumn = col.udt_name === "vector";
|
|
1080
|
-
const propType = isEnumColumn ? "string" : isVectorColumn ? "vector" : mapPgType(col.data_type);
|
|
1081
1502
|
let extra = "";
|
|
1082
1503
|
const colNameLower = col.column_name.toLowerCase();
|
|
1083
|
-
|
|
1084
|
-
let inferenceExtra = "";
|
|
1085
|
-
if (!isEnumColumn && sampleData && sampleData.length > 0) {
|
|
1086
|
-
const values = sampleData.map((r) => r[col.column_name]);
|
|
1087
|
-
const inferred = inferPropertyFromData(col.column_name, col.data_type, propType, values, meta.pks.includes(col.column_name), emitAdmin);
|
|
1088
|
-
if (inferred.propType) finalPropType = inferred.propType;
|
|
1089
|
-
if (inferred.extra) inferenceExtra = inferred.extra;
|
|
1090
|
-
}
|
|
1504
|
+
const { propType, finalPropType, inferenceExtra } = propertyTypeOf(col);
|
|
1091
1505
|
const columnChecks = tableChecks?.get(col.column_name);
|
|
1092
1506
|
if (isEnumColumn && colEnumValues) {
|
|
1093
1507
|
const enumEntries = colEnumValues.map((v) => `{ id: ${quote(v)}, label: ${quote(humanize(v))} }`).join(", ");
|
|
@@ -1095,6 +1509,13 @@ function generateCollectionFile(tableName, meta, allFks, joinTables, tablesMap,
|
|
|
1095
1509
|
} else if (columnChecks?.enumValues && !inferenceExtra.includes("enum:") && propType === "string") {
|
|
1096
1510
|
const enumEntries = columnChecks.enumValues.map((v) => `{ id: ${quote(v)}, label: ${quote(humanize(v))} }`).join(", ");
|
|
1097
1511
|
extra += `\n enum: [${enumEntries}],`;
|
|
1512
|
+
const enumType = toPostgresIdentifier(`${tableName}_${col.column_name}`);
|
|
1513
|
+
const labels = columnChecks.enumValues.map((v) => `'${v.replace(/'/g, "''")}'`).join(", ");
|
|
1514
|
+
const check = (context.metadata?.checks ?? []).find((c) => c.table_name === tableName && c.definition.includes(col.column_name));
|
|
1515
|
+
const table = `"${tableName}"`;
|
|
1516
|
+
const column = `"${col.column_name}"`;
|
|
1517
|
+
const restoreDefault = literalDefaultOf(col, "string");
|
|
1518
|
+
fileNotes.push(`"${col.column_name}" is text with CHECK (… IN …), declared here as an \`enum\`, which Rebase stores as the enum type "${enumType}". Convert the column before pushing: CREATE TYPE "${enumType}" AS ENUM (${labels}); ALTER TABLE ${table} ${check ? `DROP CONSTRAINT "${check.constraint_name}", ` : ""}ALTER COLUMN ${column} DROP DEFAULT, ALTER COLUMN ${column} TYPE "${enumType}" USING ${column}::"${enumType}"` + (restoreDefault && "source" in restoreDefault ? `, ALTER COLUMN ${column} SET DEFAULT ${quoteSqlString(JSON.parse(restoreDefault.source))}` : "") + "; — or delete the `enum` to keep the column as it is.");
|
|
1098
1519
|
}
|
|
1099
1520
|
if (finalPropType === "date") {
|
|
1100
1521
|
if (colNameLower === "created_at" || colNameLower === "createdat") {
|
|
@@ -1111,16 +1532,10 @@ function generateCollectionFile(tableName, meta, allFks, joinTables, tablesMap,
|
|
|
1111
1532
|
}
|
|
1112
1533
|
}
|
|
1113
1534
|
if (finalPropType === "array" && !inferenceExtra.includes("of: {")) {
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
if (
|
|
1117
|
-
|
|
1118
|
-
if (innerType === "string") colType = "text[]";
|
|
1119
|
-
else if (innerType === "number") colType = col.udt_name === "_numeric" ? "numeric[]" : "integer[]";
|
|
1120
|
-
else if (innerType === "boolean") colType = "boolean[]";
|
|
1121
|
-
}
|
|
1122
|
-
if (colType) extra += `\n columnType: ${quote(colType)},`;
|
|
1123
|
-
extra += `\n of: { name: ${quote(`${humanize(col.column_name)} Item`)}, type: ${quote(innerType)} },`;
|
|
1535
|
+
const stored = arrayStorage(col);
|
|
1536
|
+
if (stored.columnType) extra += `\n columnType: ${quote(stored.columnType)},`;
|
|
1537
|
+
if (stored.note) fileNotes.push(stored.note);
|
|
1538
|
+
extra += `\n of: { name: ${quote(`${humanize(col.column_name)} Item`)}, type: ${quote(stored.innerType)} },`;
|
|
1124
1539
|
} else if (finalPropType === "map" && !inferenceExtra.includes("keyValue: true") && !inferenceExtra.includes("properties: {")) extra += "\n keyValue: true,";
|
|
1125
1540
|
if (finalPropType === "string" && !isEnumColumn && !inferenceExtra) {
|
|
1126
1541
|
const isUrl = colNameLower.endsWith("_url") || colNameLower.endsWith("_uri") || colNameLower.endsWith("_link");
|
|
@@ -1134,6 +1549,25 @@ function generateCollectionFile(tableName, meta, allFks, joinTables, tablesMap,
|
|
|
1134
1549
|
extra += inferenceExtra;
|
|
1135
1550
|
if (!extra.endsWith(",")) extra += ",";
|
|
1136
1551
|
}
|
|
1552
|
+
if (!meta.pks.includes(col.column_name) && !isReadOnlyColumn(col)) {
|
|
1553
|
+
const enumTypeName = toPostgresIdentifier(`${tableName}_${col.column_name}`);
|
|
1554
|
+
const enumTypeShared = isEnumColumn && (context.metadata?.columns ?? meta.columns).filter((other) => other.udt_name === col.udt_name && other.data_type === "USER-DEFINED").length > 1;
|
|
1555
|
+
const stored = finalPropType === "array" ? { keys: [] } : columnStorage(col, finalPropType, {
|
|
1556
|
+
isEnumColumn,
|
|
1557
|
+
enumTypeName,
|
|
1558
|
+
enumTypeShared
|
|
1559
|
+
});
|
|
1560
|
+
for (const key of stored.keys) {
|
|
1561
|
+
const name = key.slice(0, key.indexOf(":"));
|
|
1562
|
+
if (!hasGeneratedKey(extra, name) && !hasGeneratedKey(inferenceExtra, name)) extra += `\n ${key},`;
|
|
1563
|
+
}
|
|
1564
|
+
if (stored.note) fileNotes.push(stored.note);
|
|
1565
|
+
if (!hasGeneratedKey(extra, "autoValue") && !hasGeneratedKey(extra, "defaultValue")) {
|
|
1566
|
+
const fallback = literalDefaultOf(col, finalPropType);
|
|
1567
|
+
if (fallback && "source" in fallback) extra += `\n defaultValue: ${fallback.source},`;
|
|
1568
|
+
else if (fallback) fileNotes.push(fallback.note);
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1137
1571
|
const declaredValidation = [];
|
|
1138
1572
|
if (finalPropType === "string" && typeof col.character_maximum_length === "number" && col.character_maximum_length > 0 && !hasGeneratedKey(extra, "max")) declaredValidation.push(`max: ${col.character_maximum_length}`);
|
|
1139
1573
|
if (columnChecks) {
|
|
@@ -1161,16 +1595,30 @@ function generateCollectionFile(tableName, meta, allFks, joinTables, tablesMap,
|
|
|
1161
1595
|
const columnComment = columnComments.get(col.column_name);
|
|
1162
1596
|
if (columnComment) extra = `\n description: ${quote(columnComment)},${extra}`;
|
|
1163
1597
|
if (meta.pks.includes(col.column_name)) {
|
|
1164
|
-
if (isCompositePk)
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1598
|
+
if (isCompositePk) {
|
|
1599
|
+
extra += `\n // Part of composite primary key (${commentText(meta.pks.join(", "))})`;
|
|
1600
|
+
if (compositeKeyMarkable && !inferenceExtra.includes("isId:")) {
|
|
1601
|
+
const stored = primaryKeyStorage(col, finalPropType);
|
|
1602
|
+
for (const key of stored.keys) {
|
|
1603
|
+
const name = key.slice(0, key.indexOf(":"));
|
|
1604
|
+
if (!hasGeneratedKey(extra, name)) extra += `\n ${key},`;
|
|
1605
|
+
}
|
|
1606
|
+
if (stored.note) fileNotes.push(stored.note);
|
|
1607
|
+
}
|
|
1608
|
+
} else if (!inferenceExtra.includes("isId:")) {
|
|
1609
|
+
const stored = primaryKeyStorage(col, finalPropType);
|
|
1610
|
+
for (const key of stored.keys) {
|
|
1611
|
+
const name = key.slice(0, key.indexOf(":"));
|
|
1612
|
+
if (!hasGeneratedKey(extra, name)) extra += `\n ${key},`;
|
|
1613
|
+
}
|
|
1614
|
+
if (stored.note) fileNotes.push(stored.note);
|
|
1615
|
+
}
|
|
1168
1616
|
}
|
|
1169
1617
|
if (finalPropType === "vector") {
|
|
1170
1618
|
const dims = col.atttypmod && col.atttypmod > 0 ? col.atttypmod : 1536;
|
|
1171
1619
|
extra += `\n dimensions: ${dims},`;
|
|
1172
1620
|
}
|
|
1173
|
-
if (col.is_nullable === "NO" && !meta.pks.includes(col.column_name) && !
|
|
1621
|
+
if (col.is_nullable === "NO" && !meta.pks.includes(col.column_name) && !isReadOnlyColumn(col)) if (extra.includes("validation: {")) extra = extra.replace("validation: {", "validation: {\n required: true,");
|
|
1174
1622
|
else extra += "\n validation: {\n required: true\n },";
|
|
1175
1623
|
const humanName = humanize(col.column_name);
|
|
1176
1624
|
orderEntries.push({
|
|
@@ -1193,6 +1641,7 @@ function generateCollectionFile(tableName, meta, allFks, joinTables, tablesMap,
|
|
|
1193
1641
|
}
|
|
1194
1642
|
for (const fk of meta.fks) {
|
|
1195
1643
|
const targetTableName = fk.foreign_table_name;
|
|
1644
|
+
if (skippedTables.has(targetTableName)) continue;
|
|
1196
1645
|
if (!joinTables.has(targetTableName)) {
|
|
1197
1646
|
const stripped = toWireKey(fk.column_name.replace(/_id$/, ""));
|
|
1198
1647
|
const relName = firstFreeKey([
|
|
@@ -1213,27 +1662,47 @@ function generateCollectionFile(tableName, meta, allFks, joinTables, tablesMap,
|
|
|
1213
1662
|
});
|
|
1214
1663
|
const targetCollectionCamel = importCollection(targetTableName);
|
|
1215
1664
|
const relHumanName = humanize(relName);
|
|
1665
|
+
const required = meta.columns.find((c) => c.column_name === fk.column_name)?.is_nullable === "NO";
|
|
1666
|
+
const onDelete = onDeleteOf(fk, required);
|
|
1667
|
+
const requiredLine = required ? "\n validation: { required: true }," : "";
|
|
1668
|
+
const onDeleteLine = onDelete ? `,\n onDelete: ${quote(onDelete)}` : "";
|
|
1216
1669
|
propertyBlocks.set(relName, `
|
|
1217
1670
|
${propKey(relName)}: {
|
|
1218
1671
|
name: ${quote(relHumanName)},
|
|
1219
|
-
type: "relation"
|
|
1672
|
+
type: "relation",${requiredLine}
|
|
1220
1673
|
// mapped from foreign key: ${commentText(fk.column_name)} -> ${commentText(targetTableName)}(${commentText(fk.foreign_column_name)})
|
|
1221
1674
|
relation: {
|
|
1222
1675
|
kind: "belongsTo",
|
|
1223
1676
|
target: ${relationTarget(targetCollectionCamel)},
|
|
1224
|
-
localKey: ${quote(fk.column_name)}
|
|
1677
|
+
localKey: ${quote(fk.column_name)}${onDeleteLine}
|
|
1225
1678
|
}
|
|
1226
1679
|
},`);
|
|
1227
1680
|
}
|
|
1228
1681
|
}
|
|
1229
|
-
|
|
1682
|
+
/**
|
|
1683
|
+
* A name for an entry in the `relations` array that no property and no
|
|
1684
|
+
* earlier entry has taken.
|
|
1685
|
+
*
|
|
1686
|
+
* Relations and properties share one namespace — a relation is found by
|
|
1687
|
+
* name — so a `belongsTo` property called `series` beside a many-to-many
|
|
1688
|
+
* also called `series` left one of them unreachable: the planner resolved
|
|
1689
|
+
* the property's name to the many-to-many, planned no `series_id` column,
|
|
1690
|
+
* and the first push dropped it.
|
|
1691
|
+
*/
|
|
1692
|
+
const usedRelationNames = /* @__PURE__ */ new Set();
|
|
1693
|
+
const relationNameFor = (preferred, fallback) => {
|
|
1694
|
+
const name = firstFreeKey([preferred, fallback], { has: (key) => propertyBlocks.has(key) || usedRelationNames.has(key) });
|
|
1695
|
+
usedRelationNames.add(name);
|
|
1696
|
+
return name;
|
|
1697
|
+
};
|
|
1698
|
+
const inverseFks = allFks.filter((fk) => fk.foreign_table_name === tableName && !joinTables.has(fk.table_name) && !skippedTables.has(fk.table_name));
|
|
1230
1699
|
for (const fk of inverseFks) {
|
|
1231
1700
|
const sourceTableName = fk.table_name;
|
|
1232
1701
|
const targetCollectionCamel = importCollection(sourceTableName);
|
|
1233
1702
|
relationsOutput += `
|
|
1234
1703
|
{
|
|
1235
1704
|
kind: "hasMany",
|
|
1236
|
-
relationName: ${quote(sourceTableName)},
|
|
1705
|
+
relationName: ${quote(relationNameFor(sourceTableName, `${sourceTableName}_by_${fk.column_name}`))},
|
|
1237
1706
|
target: ${relationTarget(targetCollectionCamel)},
|
|
1238
1707
|
// the ${commentText(sourceTableName)}.${commentText(fk.column_name)} FK points back here
|
|
1239
1708
|
foreignKeyOnTarget: ${quote(fk.column_name)}
|
|
@@ -1247,7 +1716,11 @@ function generateCollectionFile(tableName, meta, allFks, joinTables, tablesMap,
|
|
|
1247
1716
|
const jtMeta = tablesMap.get(jt);
|
|
1248
1717
|
if (!jtMeta) continue;
|
|
1249
1718
|
const joinFks = jtMeta.fks;
|
|
1250
|
-
const
|
|
1719
|
+
const keyPosition = (fk) => {
|
|
1720
|
+
const position = jtMeta.pks.indexOf(fk.column_name);
|
|
1721
|
+
return position === -1 ? Number.MAX_SAFE_INTEGER : position;
|
|
1722
|
+
};
|
|
1723
|
+
const selfRefFks = joinFks.filter((fk) => fk.foreign_table_name === tableName).sort((a, b) => keyPosition(a) - keyPosition(b));
|
|
1251
1724
|
if (selfRefFks.length === 2) {
|
|
1252
1725
|
const thisFk = selfRefFks[0];
|
|
1253
1726
|
const otherFk = selfRefFks[1];
|
|
@@ -1255,7 +1728,7 @@ function generateCollectionFile(tableName, meta, allFks, joinTables, tablesMap,
|
|
|
1255
1728
|
relationsOutput += `
|
|
1256
1729
|
{
|
|
1257
1730
|
kind: "manyToMany",
|
|
1258
|
-
relationName: ${quote(relPropName)},
|
|
1731
|
+
relationName: ${quote(relationNameFor(relPropName, `${relPropName}_${jt}`))},
|
|
1259
1732
|
target: ${relationTarget(toCollectionVarName(tableName))},
|
|
1260
1733
|
through: {
|
|
1261
1734
|
table: ${quote(jt)},
|
|
@@ -1266,7 +1739,14 @@ function generateCollectionFile(tableName, meta, allFks, joinTables, tablesMap,
|
|
|
1266
1739
|
continue;
|
|
1267
1740
|
}
|
|
1268
1741
|
const otherFk = joinFks.find((fk) => fk.foreign_table_name !== tableName);
|
|
1269
|
-
|
|
1742
|
+
const leadingTable = joinFks.find((fk) => fk.column_name === jtMeta.pks[0])?.foreign_table_name;
|
|
1743
|
+
const sortsFirst = otherFk ? [tableName, otherFk.foreign_table_name].sort()[0] : tableName;
|
|
1744
|
+
const declareHere = !leadingTable || leadingTable === sortsFirst || leadingTable === tableName;
|
|
1745
|
+
if (otherFk && !declareHere) relationsOutput += `
|
|
1746
|
+
// The many-to-many with ${commentText(otherFk.foreign_table_name)} through ${commentText(jt)} is declared on
|
|
1747
|
+
// ${commentText(otherFk.foreign_table_name)} only: declaring it here too would re-key ${commentText(jt)}
|
|
1748
|
+
// from (${commentText(jtMeta.pks.join(", "))}) on the next push.`;
|
|
1749
|
+
if (otherFk && declareHere && !skippedTables.has(otherFk.foreign_table_name)) {
|
|
1270
1750
|
const targetTableName = otherFk.foreign_table_name;
|
|
1271
1751
|
const targetCollectionCamel = importCollection(targetTableName);
|
|
1272
1752
|
const thisFk = joinFks.find((fk) => fk.foreign_table_name === tableName);
|
|
@@ -1274,11 +1754,20 @@ function generateCollectionFile(tableName, meta, allFks, joinTables, tablesMap,
|
|
|
1274
1754
|
relationsOutput += `
|
|
1275
1755
|
{
|
|
1276
1756
|
kind: "manyToMany",
|
|
1277
|
-
relationName: ${quote(targetTableName)},
|
|
1757
|
+
relationName: ${quote(relationNameFor(targetTableName, `${targetTableName}_via_${jt}`))},
|
|
1278
1758
|
target: ${relationTarget(targetCollectionCamel)},${throughCode}
|
|
1279
1759
|
},`;
|
|
1280
1760
|
}
|
|
1281
1761
|
}
|
|
1762
|
+
const search = reconstructSearchBlock({
|
|
1763
|
+
schema: context.metadata?.schema ?? "public",
|
|
1764
|
+
table: tableName,
|
|
1765
|
+
columns: meta.columns,
|
|
1766
|
+
comments: columnComments,
|
|
1767
|
+
keyByColumn
|
|
1768
|
+
});
|
|
1769
|
+
if (search.note) fileNotes.push(search.note);
|
|
1770
|
+
const searchBlock = search.block ?? "";
|
|
1282
1771
|
const relationsBlock = relationsOutput ? `\n relations: [${relationsOutput}\n ],` : "";
|
|
1283
1772
|
const sortedPropertiesOrder = sortPropertiesOrder(orderEntries);
|
|
1284
1773
|
for (const key of sortedPropertiesOrder) propsOutput += propertyBlocks.get(key) || "";
|
|
@@ -1302,17 +1791,21 @@ function generateCollectionFile(tableName, meta, allFks, joinTables, tablesMap,
|
|
|
1302
1791
|
adminEntries.push(`propertiesOrder: ${formatKeyList(sortedPropertiesOrder)}`);
|
|
1303
1792
|
const adminBlock = emitAdmin ? `\n admin: {\n ${adminEntries.join(",\n ")}\n }` : "";
|
|
1304
1793
|
const descriptionBlock = tableComment ? `\n description: ${quote(tableComment)},` : "";
|
|
1794
|
+
const pgSchema = context.metadata?.schema;
|
|
1795
|
+
const schemaBlock = pgSchema && pgSchema !== "public" ? `\n schema: ${quote(pgSchema)},` : "";
|
|
1305
1796
|
const classificationNote = classification && classification.role !== "entity" ? `\n// Introspected as a ${commentText(classification.role)}: ${commentText(classification.reason)}.\n` : "";
|
|
1797
|
+
for (const note of fileNotes) context.notes?.push(`${tableName}: ${note}`);
|
|
1798
|
+
const storageNote = fileNotes.length > 0 ? `\n// ⚠ Not reproduced exactly by \`rebase db push\` — read before pushing:\n${fileNotes.map((n) => `// - ${commentText(n)}`).join("\n")}\n` : "";
|
|
1306
1799
|
const collectionVarName = toCollectionVarName(tableName);
|
|
1307
1800
|
const [open, close] = builder === "annotation" ? [`const ${collectionVarName}: PostgresCollectionConfig = {`, "};"] : [`const ${collectionVarName} = defineCollection({`, "});"];
|
|
1308
1801
|
const importLines = Array.from(imports);
|
|
1309
1802
|
return `${[...importLines.filter((line) => !line.includes("from \"./")), ...importLines.filter((line) => line.includes("from \"./"))].join("\n")}
|
|
1310
|
-
${classificationNote}
|
|
1803
|
+
${classificationNote}${storageNote}
|
|
1311
1804
|
${open}
|
|
1312
1805
|
name: ${quote(collectionName)},
|
|
1313
1806
|
singularName: ${quote(singular)},
|
|
1314
1807
|
slug: ${quote(tableName)},
|
|
1315
|
-
table: ${quote(tableName)},${descriptionBlock}
|
|
1808
|
+
table: ${quote(tableName)},${schemaBlock}${descriptionBlock}${searchBlock}
|
|
1316
1809
|
properties: {${propsOutput}
|
|
1317
1810
|
},${relationsBlock}${adminBlock}
|
|
1318
1811
|
${close}
|
|
@@ -1373,6 +1866,6 @@ function safeHostFromUrl(url) {
|
|
|
1373
1866
|
return url.includes("@") ? url.split("@")[1] : "(local connection)";
|
|
1374
1867
|
}
|
|
1375
1868
|
//#endregion
|
|
1376
|
-
export {
|
|
1869
|
+
export { compositeKeySkipReason as a, getIconForTable as c, safeHostFromUrl as d, singularize as f, humanize as g, mapPgType as h, buildTablesMap as i, identifyJoinTables as l, lookupCandidates as m, COMMON_PACKAGE as n, generateCollectionFile as o, classifyTables as p, buildEnumMap as r, generateIndexContent as s, ADMIN_TYPES_PACKAGE as t, mergeIndexContent as u };
|
|
1377
1870
|
|
|
1378
|
-
//# sourceMappingURL=introspect-db-logic-
|
|
1871
|
+
//# sourceMappingURL=introspect-db-logic-kCETE8TY.js.map
|