turbine-orm 0.50.0 → 0.51.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/README.md +66 -66
- package/dist/adapters/cockroachdb.d.ts +5 -5
- package/dist/adapters/cockroachdb.js +10 -10
- package/dist/adapters/index.d.ts +5 -5
- package/dist/adapters/index.js +7 -7
- package/dist/adapters/yugabytedb.d.ts +7 -7
- package/dist/adapters/yugabytedb.js +10 -10
- package/dist/cjs/adapters/cockroachdb.d.ts +5 -5
- package/dist/cjs/adapters/cockroachdb.js +10 -10
- package/dist/cjs/adapters/index.d.ts +5 -5
- package/dist/cjs/adapters/index.js +7 -7
- package/dist/cjs/adapters/yugabytedb.d.ts +7 -7
- package/dist/cjs/adapters/yugabytedb.js +10 -10
- package/dist/cjs/cli/config.d.ts +13 -2
- package/dist/cjs/cli/config.js +3 -2
- package/dist/cjs/cli/destructive.d.ts +1 -1
- package/dist/cjs/cli/destructive.js +1 -1
- package/dist/cjs/cli/index.d.ts +10 -10
- package/dist/cjs/cli/index.js +49 -45
- package/dist/cjs/cli/loader.d.ts +7 -7
- package/dist/cjs/cli/loader.js +9 -9
- package/dist/cjs/cli/mcp.js +4 -4
- package/dist/cjs/cli/migrate.d.ts +5 -5
- package/dist/cjs/cli/migrate.js +11 -11
- package/dist/cjs/cli/studio-ui.generated.js +1 -1
- package/dist/cjs/cli/ui.d.ts +2 -2
- package/dist/cjs/cli/ui.js +2 -2
- package/dist/cjs/client.d.ts +49 -38
- package/dist/cjs/client.js +57 -56
- package/dist/cjs/dialect.d.ts +62 -18
- package/dist/cjs/dialect.js +40 -2
- package/dist/cjs/errors.d.ts +5 -5
- package/dist/cjs/errors.js +11 -11
- package/dist/cjs/generate.d.ts +6 -6
- package/dist/cjs/generate.js +31 -29
- package/dist/cjs/index-advisor.d.ts +5 -5
- package/dist/cjs/index-advisor.js +0 -0
- package/dist/cjs/index.d.ts +1 -1
- package/dist/cjs/index.js +7 -7
- package/dist/cjs/introspect.d.ts +35 -9
- package/dist/cjs/introspect.js +83 -32
- package/dist/cjs/mssql.d.ts +11 -11
- package/dist/cjs/mssql.js +64 -29
- package/dist/cjs/mysql.d.ts +8 -8
- package/dist/cjs/mysql.js +61 -23
- package/dist/cjs/nested-write.d.ts +21 -2
- package/dist/cjs/nested-write.js +51 -14
- package/dist/cjs/optional-peer-import.cjs +7 -7
- package/dist/cjs/optional-peer-import.d.cts +7 -7
- package/dist/cjs/pipeline-submittable.d.ts +2 -2
- package/dist/cjs/pipeline-submittable.js +6 -6
- package/dist/cjs/pipeline.d.ts +1 -1
- package/dist/cjs/pipeline.js +4 -4
- package/dist/cjs/powdb-introspect.d.ts +1 -1
- package/dist/cjs/powdb-introspect.js +1 -1
- package/dist/cjs/powdb.d.ts +28 -28
- package/dist/cjs/powdb.js +66 -66
- package/dist/cjs/powql.d.ts +27 -27
- package/dist/cjs/powql.js +73 -52
- package/dist/cjs/query/aggregates.d.ts +1 -1
- package/dist/cjs/query/aggregates.js +5 -5
- package/dist/cjs/query/batched-loader.d.ts +11 -11
- package/dist/cjs/query/batched-loader.js +24 -24
- package/dist/cjs/query/builder.d.ts +39 -21
- package/dist/cjs/query/builder.js +99 -57
- package/dist/cjs/query/compound-unique.d.ts +1 -1
- package/dist/cjs/query/compound-unique.js +0 -0
- package/dist/cjs/query/deferred.d.ts +12 -6
- package/dist/cjs/query/deferred.js +1 -1
- package/dist/cjs/query/filters.d.ts +31 -11
- package/dist/cjs/query/filters.js +67 -14
- package/dist/cjs/query/index.d.ts +1 -1
- package/dist/cjs/query/index.js +1 -1
- package/dist/cjs/query/relations.d.ts +9 -9
- package/dist/cjs/query/relations.js +164 -57
- package/dist/cjs/query/types.d.ts +86 -35
- package/dist/cjs/query/types.js +1 -1
- package/dist/cjs/query/utils.d.ts +27 -10
- package/dist/cjs/query/utils.js +86 -14
- package/dist/cjs/query/where.d.ts +47 -28
- package/dist/cjs/query/where.js +130 -31
- package/dist/cjs/query/writes.d.ts +24 -5
- package/dist/cjs/query/writes.js +102 -13
- package/dist/cjs/realtime.d.ts +7 -7
- package/dist/cjs/realtime.js +9 -9
- package/dist/cjs/schema-builder.d.ts +18 -7
- package/dist/cjs/schema-builder.js +17 -10
- package/dist/cjs/schema-metadata.d.ts +3 -3
- package/dist/cjs/schema-metadata.js +9 -9
- package/dist/cjs/schema-sql.d.ts +9 -9
- package/dist/cjs/schema-sql.js +20 -20
- package/dist/cjs/schema.d.ts +19 -9
- package/dist/cjs/schema.js +6 -6
- package/dist/cjs/serverless.d.ts +15 -15
- package/dist/cjs/serverless.js +16 -16
- package/dist/cjs/sqlite.d.ts +8 -8
- package/dist/cjs/sqlite.js +53 -22
- package/dist/cjs/typed-sql.d.ts +4 -4
- package/dist/cjs/typed-sql.js +5 -5
- package/dist/cli/config.d.ts +13 -2
- package/dist/cli/config.js +3 -2
- package/dist/cli/destructive.d.ts +1 -1
- package/dist/cli/destructive.js +1 -1
- package/dist/cli/index.d.ts +10 -10
- package/dist/cli/index.js +49 -45
- package/dist/cli/loader.d.ts +7 -7
- package/dist/cli/loader.js +9 -9
- package/dist/cli/mcp.js +4 -4
- package/dist/cli/migrate.d.ts +5 -5
- package/dist/cli/migrate.js +11 -11
- package/dist/cli/studio-ui.generated.js +1 -1
- package/dist/cli/ui.d.ts +2 -2
- package/dist/cli/ui.js +2 -2
- package/dist/client.d.ts +49 -38
- package/dist/client.js +57 -56
- package/dist/dialect.d.ts +62 -18
- package/dist/dialect.js +40 -2
- package/dist/errors.d.ts +5 -5
- package/dist/errors.js +11 -11
- package/dist/generate.d.ts +6 -6
- package/dist/generate.js +31 -29
- package/dist/index-advisor.d.ts +5 -5
- package/dist/index-advisor.js +0 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +7 -7
- package/dist/introspect.d.ts +35 -9
- package/dist/introspect.js +82 -32
- package/dist/mssql.d.ts +11 -11
- package/dist/mssql.js +64 -29
- package/dist/mysql.d.ts +8 -8
- package/dist/mysql.js +61 -23
- package/dist/nested-write.d.ts +21 -2
- package/dist/nested-write.js +51 -14
- package/dist/optional-peer-import.cjs +7 -7
- package/dist/optional-peer-import.d.cts +7 -7
- package/dist/pipeline-submittable.d.ts +2 -2
- package/dist/pipeline-submittable.js +6 -6
- package/dist/pipeline.d.ts +1 -1
- package/dist/pipeline.js +4 -4
- package/dist/powdb-introspect.d.ts +1 -1
- package/dist/powdb-introspect.js +1 -1
- package/dist/powdb.d.ts +28 -28
- package/dist/powdb.js +66 -66
- package/dist/powql.d.ts +27 -27
- package/dist/powql.js +73 -52
- package/dist/query/aggregates.d.ts +1 -1
- package/dist/query/aggregates.js +5 -5
- package/dist/query/batched-loader.d.ts +11 -11
- package/dist/query/batched-loader.js +24 -24
- package/dist/query/builder.d.ts +39 -21
- package/dist/query/builder.js +100 -58
- package/dist/query/compound-unique.d.ts +1 -1
- package/dist/query/compound-unique.js +0 -0
- package/dist/query/deferred.d.ts +12 -6
- package/dist/query/deferred.js +1 -1
- package/dist/query/filters.d.ts +31 -11
- package/dist/query/filters.js +66 -13
- package/dist/query/index.d.ts +1 -1
- package/dist/query/index.js +1 -1
- package/dist/query/relations.d.ts +9 -9
- package/dist/query/relations.js +165 -58
- package/dist/query/types.d.ts +86 -35
- package/dist/query/types.js +1 -1
- package/dist/query/utils.d.ts +27 -10
- package/dist/query/utils.js +84 -14
- package/dist/query/where.d.ts +47 -28
- package/dist/query/where.js +129 -32
- package/dist/query/writes.d.ts +24 -5
- package/dist/query/writes.js +101 -13
- package/dist/realtime.d.ts +7 -7
- package/dist/realtime.js +9 -9
- package/dist/schema-builder.d.ts +18 -7
- package/dist/schema-builder.js +17 -10
- package/dist/schema-metadata.d.ts +3 -3
- package/dist/schema-metadata.js +9 -9
- package/dist/schema-sql.d.ts +9 -9
- package/dist/schema-sql.js +20 -20
- package/dist/schema.d.ts +19 -9
- package/dist/schema.js +6 -6
- package/dist/serverless.d.ts +15 -15
- package/dist/serverless.js +16 -16
- package/dist/sqlite.d.ts +8 -8
- package/dist/sqlite.js +53 -22
- package/dist/typed-sql.d.ts +4 -4
- package/dist/typed-sql.js +5 -5
- package/package.json +2 -2
package/dist/cjs/query/writes.js
CHANGED
|
@@ -57,6 +57,7 @@ exports.buildUpdateMany = buildUpdateMany;
|
|
|
57
57
|
exports.buildDeleteMany = buildDeleteMany;
|
|
58
58
|
exports.piiColumns = piiColumns;
|
|
59
59
|
exports.piiFields = piiFields;
|
|
60
|
+
exports.applyUpdatedAtColumns = applyUpdatedAtColumns;
|
|
60
61
|
exports.writeReturningColumns = writeReturningColumns;
|
|
61
62
|
exports.writeReselectSelection = writeReselectSelection;
|
|
62
63
|
exports.parseWriteRow = parseWriteRow;
|
|
@@ -77,10 +78,10 @@ const whereMod = __importStar(require("./where.js"));
|
|
|
77
78
|
*
|
|
78
79
|
* Two value shapes need rewriting, both a JS `Date` on a temporal column:
|
|
79
80
|
*
|
|
80
|
-
* 1. A time-of-day column (`time` / `timetz`)
|
|
81
|
+
* 1. A time-of-day column (`time` / `timetz`), the driver serializes a `Date`
|
|
81
82
|
* as a full ISO timestamp with the process offset and Postgres answers
|
|
82
83
|
* `22007 invalid input syntax for type time`. Rewritten on every engine.
|
|
83
|
-
* 2. A zone-less `date` / `timestamp` column on PostgreSQL
|
|
84
|
+
* 2. A zone-less `date` / `timestamp` column on PostgreSQL, the driver's
|
|
84
85
|
* local-offset serialization stores the PROCESS's calendar fields, so in a
|
|
85
86
|
* non-UTC process the stored value is wrong and (because the read path
|
|
86
87
|
* interprets an offset-less value as UTC) does not round-trip. Rewritten to
|
|
@@ -115,7 +116,7 @@ function coerceWriteValue(qi, key, value) {
|
|
|
115
116
|
* PostgreSQL only: MySQL's `TIMESTAMP`/`DATETIME` and SQL Server's
|
|
116
117
|
* `datetime2` are converted or bound by their own drivers, and MySQL in
|
|
117
118
|
* particular reads a zone-less literal in the SESSION time zone, so a UTC
|
|
118
|
-
* literal would be misread there. `utcTimestamps: false` opts out
|
|
119
|
+
* literal would be misread there. `utcTimestamps: false` opts out, it is the
|
|
119
120
|
* same switch that turns off the UTC READ parsing, so the two stay symmetric.
|
|
120
121
|
*/
|
|
121
122
|
function utcDateTimeWrites(qi) {
|
|
@@ -214,7 +215,7 @@ function buildCreateMany(qi, args) {
|
|
|
214
215
|
return keys.map((key) => coerceWriteValue(qi, key, record[key]));
|
|
215
216
|
});
|
|
216
217
|
// Use actual Postgres types for array casts in the default PostgreSQL dialect.
|
|
217
|
-
// Enum columns cast to `"EnumName"[]
|
|
218
|
+
// Enum columns cast to `"EnumName"[]`, the generic text[] fallback would
|
|
218
219
|
// type the UNNEST output as text, which Postgres refuses to coerce to the
|
|
219
220
|
// enum ("column X is of type Y but expression is of type text").
|
|
220
221
|
const typeCasts = columns.map((col) => {
|
|
@@ -222,6 +223,18 @@ function buildCreateMany(qi, args) {
|
|
|
222
223
|
return enumType ? `${qi.q(enumType)}[]` : whereMod.getColumnArrayType(qi, col);
|
|
223
224
|
});
|
|
224
225
|
const quotedColumns = columns.map((c) => qi.q(c));
|
|
226
|
+
// ARRAY-TYPED COLUMNS force the row-at-a-time VALUES form.
|
|
227
|
+
//
|
|
228
|
+
// The default PostgreSQL shape is `SELECT * FROM UNNEST($1::text[], …)`,
|
|
229
|
+
// one bound array per COLUMN. That is a column-major transpose, and it
|
|
230
|
+
// cannot express an array-valued column: `unnest` flattens, so N rows each
|
|
231
|
+
// holding a `text[]` arrive as one flat `text[]` and PostgreSQL rejects the
|
|
232
|
+
// insert with 42804 ("column is of type text[] but expression is of type
|
|
233
|
+
// text"). Single-row `create` never hit it because it binds each value
|
|
234
|
+
// directly. Detected from the column's own declared type rather than from
|
|
235
|
+
// the shape of the first row's value, so a row whose array column happens to
|
|
236
|
+
// be null or absent still takes the correct form.
|
|
237
|
+
const hasArrayColumn = columns.some((col) => (qi.columnPgTypeMap.get(col) ?? '').startsWith('_'));
|
|
225
238
|
const built = qi.dialect.buildBulkInsertStatement({
|
|
226
239
|
table: qt,
|
|
227
240
|
columns: quotedColumns,
|
|
@@ -229,6 +242,7 @@ function buildCreateMany(qi, args) {
|
|
|
229
242
|
columnArrayTypes: typeCasts,
|
|
230
243
|
skipDuplicates: args.skipDuplicates,
|
|
231
244
|
returning: writeReturningColumns(qi),
|
|
245
|
+
requireRowValues: hasArrayColumn,
|
|
232
246
|
});
|
|
233
247
|
return {
|
|
234
248
|
sql: built.sql,
|
|
@@ -240,20 +254,51 @@ function buildCreateMany(qi, args) {
|
|
|
240
254
|
function buildUpdate(qi, args) {
|
|
241
255
|
assertWritable(qi, 'update');
|
|
242
256
|
qi.currentSkip = args.skipGlobalFilters;
|
|
243
|
-
|
|
257
|
+
// `updatedAt`-tagged columns are filled in before anything reads `data`, so
|
|
258
|
+
// the SET list, the fingerprint and the param collector all see one object.
|
|
259
|
+
const dataObj = applyUpdatedAtColumns(qi, args.data);
|
|
244
260
|
assertNoGeneratedColumns(qi, dataObj, 'update');
|
|
245
261
|
// Prisma compound-unique selector (e.g. `{ orgId_userId: { orgId, userId } }`)
|
|
246
262
|
// → the column conjunction, before the empty-`where` guard so the expanded
|
|
247
263
|
// members count as a real predicate.
|
|
248
264
|
const userWhere = (0, compound_unique_js_1.expandCompoundUniqueWhere)(qi.tableMeta, args.where);
|
|
249
265
|
const lock = args.optimisticLock;
|
|
250
|
-
// The empty-`where` guard checks the USER predicate only
|
|
266
|
+
// The empty-`where` guard checks the USER predicate only, a global filter
|
|
251
267
|
// must never turn an unguarded mass update into an allowed one.
|
|
252
268
|
const userHasPredicate = !whereMod.userPredicateIsEmpty(qi, userWhere) || !!lock;
|
|
253
269
|
whereMod.assertMutationHasPredicate(qi, 'update', userHasPredicate ? ' WHERE x' : '', args.allowFullTableScan);
|
|
254
270
|
// The SQL is built from the global-filter-merged where (soft-delete keeps an
|
|
255
271
|
// update from touching already-deleted rows).
|
|
256
272
|
const whereObj = (whereMod.mergeGlobalFilter(qi, userWhere) ?? {});
|
|
273
|
+
// An update with nothing to set is a NO-OP that returns the current row.
|
|
274
|
+
//
|
|
275
|
+
// It used to render `UPDATE t SET WHERE …` and fail with a raw PostgreSQL
|
|
276
|
+
// 42601 syntax error. That is easy to hit honestly: any handler that builds
|
|
277
|
+
// its payload from optional request fields produces `{}` on a request that
|
|
278
|
+
// supplied none of them, so a legitimate request turned into a 500. Prisma
|
|
279
|
+
// treats the same call as a no-op and returns the row, so a ported handler
|
|
280
|
+
// inherited a crash where the original returned 200.
|
|
281
|
+
//
|
|
282
|
+
// The row is re-selected through the same projection a real update would
|
|
283
|
+
// return (PII columns excluded on tagged tables), and a where matching no
|
|
284
|
+
// row still raises NotFoundError, so only the SQL differs, not the contract.
|
|
285
|
+
// `optimisticLock` is excluded: it always has a version column to SET and a
|
|
286
|
+
// version check that must still run.
|
|
287
|
+
const hasSetData = Object.values(dataObj).some((v) => v !== undefined);
|
|
288
|
+
if (!hasSetData && !lock) {
|
|
289
|
+
const sel = buildReselectByWhere(qi, whereObj);
|
|
290
|
+
return {
|
|
291
|
+
sql: sel.sql,
|
|
292
|
+
params: sel.params,
|
|
293
|
+
transform: (result) => {
|
|
294
|
+
const row = result.rows[0];
|
|
295
|
+
if (!row)
|
|
296
|
+
throw new errors_js_1.NotFoundError({ table: qi.table, where: args.where, operation: 'update' });
|
|
297
|
+
return parseWriteRow(qi, row);
|
|
298
|
+
},
|
|
299
|
+
tag: `${qi.table}.update`,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
257
302
|
const setFp = fingerprintSet(qi, dataObj);
|
|
258
303
|
const whereFp = whereMod.fingerprintWhere(qi, whereObj);
|
|
259
304
|
const ck = lock ? null : `u:${setFp}|${whereFp}${whereMod.globalFilterCacheSegment(qi)}`;
|
|
@@ -333,7 +378,7 @@ function buildUpdate(qi, args) {
|
|
|
333
378
|
// Optimistic-lock conflict: the version-checked UPDATE matched no
|
|
334
379
|
// row. The re-fetch below uses `where` WITHOUT the version
|
|
335
380
|
// predicate, so it would return the stale row and silently mask
|
|
336
|
-
// the conflict
|
|
381
|
+
// the conflict, detect it from affected-rows here instead, to
|
|
337
382
|
// match the OptimisticLockError thrown on RETURNING/OUTPUT engines.
|
|
338
383
|
if (lock && (writeResult.rowCount ?? 0) === 0) {
|
|
339
384
|
throw new errors_js_1.OptimisticLockError({
|
|
@@ -414,7 +459,7 @@ function buildUpsert(qi, args) {
|
|
|
414
459
|
const createParams = createEntries.map(([k, v]) => coerceWriteValue(qi, k, v));
|
|
415
460
|
// Enum columns get an explicit `::"EnumName"` cast (see enumTypeForColumn).
|
|
416
461
|
const placeholders = createEntries.map(([k], i) => `${qi.p(i + 1)}${whereMod.enumCastSuffix(qi, qi.toColumn(k))}`);
|
|
417
|
-
// The conflict target comes from `where` keys
|
|
462
|
+
// The conflict target comes from `where` keys, must be unique/PK columns
|
|
418
463
|
const conflictKeys = Object.keys(upsertWhere).filter((k) => upsertWhere[k] !== undefined);
|
|
419
464
|
const conflictColumns = conflictKeys.map((k) => qi.toSqlColumn(k));
|
|
420
465
|
// Build the UPDATE SET part
|
|
@@ -475,10 +520,22 @@ function buildUpsert(qi, args) {
|
|
|
475
520
|
function buildUpdateMany(qi, args) {
|
|
476
521
|
assertWritable(qi, 'updateMany');
|
|
477
522
|
qi.currentSkip = args.skipGlobalFilters;
|
|
478
|
-
const dataObj = args.data;
|
|
523
|
+
const dataObj = applyUpdatedAtColumns(qi, args.data);
|
|
479
524
|
assertNoGeneratedColumns(qi, dataObj, 'updateMany');
|
|
480
525
|
whereMod.assertMutationHasPredicate(qi, 'updateMany', whereMod.userPredicateIsEmpty(qi, args.where) ? '' : ' WHERE x', args.allowFullTableScan);
|
|
481
526
|
const whereObj = (whereMod.mergeGlobalFilter(qi, args.where) ?? {});
|
|
527
|
+
// Nothing to SET: a no-op, for the same reason as `update` above (that path
|
|
528
|
+
// has the full rationale). Reports `count: 0` because zero rows were
|
|
529
|
+
// modified, no statement is issued at all, so this is not the count of rows
|
|
530
|
+
// the predicate MATCHED.
|
|
531
|
+
if (!Object.values(dataObj).some((v) => v !== undefined)) {
|
|
532
|
+
return {
|
|
533
|
+
sql: `SELECT ${qi.p(1)} AS count`,
|
|
534
|
+
params: [0],
|
|
535
|
+
transform: () => ({ count: 0 }),
|
|
536
|
+
tag: `${qi.table}.updateMany`,
|
|
537
|
+
};
|
|
538
|
+
}
|
|
482
539
|
const setFp = fingerprintSet(qi, dataObj);
|
|
483
540
|
const whereFp = whereMod.fingerprintWhere(qi, whereObj);
|
|
484
541
|
const ck = `um:${setFp}|${whereFp}${whereMod.globalFilterCacheSegment(qi)}`;
|
|
@@ -559,17 +616,49 @@ function piiFields(_qi, meta) {
|
|
|
559
616
|
}
|
|
560
617
|
/**
|
|
561
618
|
* The `RETURNING` / `OUTPUT` selection for a write on this table. A table with
|
|
562
|
-
* no PII column returns `'*'` (every column
|
|
619
|
+
* no PII column returns `'*'` (every column, byte-identical SQL to before);
|
|
563
620
|
* a table WITH PII columns returns an explicit quoted list of every non-PII
|
|
564
621
|
* column so the PII values never leave the database on a write. A PII-tagged
|
|
565
622
|
* PRIMARY KEY column is kept in the projection regardless (the returned row
|
|
566
|
-
* must stay addressable): tag sensitive data, not keys
|
|
623
|
+
* must stay addressable): tag sensitive data, not keys, a PII PK is
|
|
567
624
|
* documented out of scope for stripping. Writes accept no `select`/`includePii`
|
|
568
625
|
* (unlike reads), so this is the whole write-return policy at the SQL level;
|
|
569
626
|
* {@link parseWriteRow} remains as a defense-in-depth strip (a no-op once the
|
|
570
627
|
* SQL already excludes the columns). Derived purely from static per-table
|
|
571
628
|
* schema metadata, so the write SQL cache needs no extra key segment.
|
|
572
629
|
*/
|
|
630
|
+
/**
|
|
631
|
+
* Return `data` with every `updatedAt`-tagged column the caller did not name
|
|
632
|
+
* set to `now`, or the original object when the table has none.
|
|
633
|
+
*
|
|
634
|
+
* Prisma's `@updatedAt` has no turbine equivalent, so a migrated application
|
|
635
|
+
* had to remember the field on every single update, and the value is usually
|
|
636
|
+
* load-bearing in the response body, so forgetting it is a silent staleness
|
|
637
|
+
* bug rather than a crash. The tag is opt-in per column and never inferred
|
|
638
|
+
* from a column's name, so a schema that does not use it emits byte-identical
|
|
639
|
+
* SQL and an application already managing its own timestamp is untouched.
|
|
640
|
+
*
|
|
641
|
+
* The timestamp is generated CLIENT-side (like Prisma) rather than as a SQL
|
|
642
|
+
* `now()`, so it flows through the same temporal coercion as any other bound
|
|
643
|
+
* `Date` and lands in UTC on every engine.
|
|
644
|
+
*
|
|
645
|
+
* An explicit value always wins, including an explicit `null`: naming the
|
|
646
|
+
* column is a statement of intent.
|
|
647
|
+
*/
|
|
648
|
+
function applyUpdatedAtColumns(qi, data) {
|
|
649
|
+
const tagged = qi.tableMeta.columns.filter((c) => c.updatedAt);
|
|
650
|
+
if (tagged.length === 0)
|
|
651
|
+
return data;
|
|
652
|
+
let out = null;
|
|
653
|
+
const now = new Date();
|
|
654
|
+
for (const col of tagged) {
|
|
655
|
+
if (Object.hasOwn(data, col.field) && data[col.field] !== undefined)
|
|
656
|
+
continue;
|
|
657
|
+
out ??= { ...data };
|
|
658
|
+
out[col.field] = now;
|
|
659
|
+
}
|
|
660
|
+
return out ?? data;
|
|
661
|
+
}
|
|
573
662
|
function writeReturningColumns(qi) {
|
|
574
663
|
const piiCols = piiColumns(qi, qi.tableMeta);
|
|
575
664
|
if (piiCols.size === 0)
|
|
@@ -627,7 +716,7 @@ function assertNoGeneratedColumns(qi, data, operation) {
|
|
|
627
716
|
const col = qi.tableMeta.columns.find((c) => c.field === key || c.name === key || c.name === (0, schema_js_1.camelToSnake)(key));
|
|
628
717
|
if (col?.isGeneratedStored) {
|
|
629
718
|
throw new errors_js_1.ValidationError(`[turbine] Cannot ${operation} "${qi.table}": column "${key}" is a GENERATED ALWAYS AS (…) STORED ` +
|
|
630
|
-
'column whose value the database computes
|
|
719
|
+
'column whose value the database computes, remove it from your data.');
|
|
631
720
|
}
|
|
632
721
|
}
|
|
633
722
|
}
|
|
@@ -636,7 +725,7 @@ function assertNoGeneratedColumns(qi, data, operation) {
|
|
|
636
725
|
*
|
|
637
726
|
* Supports plain values and atomic operator objects ({ set, increment,
|
|
638
727
|
* decrement, multiply, divide }). An operator object is detected ONLY when
|
|
639
|
-
* it has EXACTLY one key that is one of the 5 operator keys
|
|
728
|
+
* it has EXACTLY one key that is one of the 5 operator keys, this avoids
|
|
640
729
|
* misinterpreting JSON column values like `{ set: 'x' }` as operators
|
|
641
730
|
* (real operator objects always have exactly one key, and a plain JSON
|
|
642
731
|
* payload that happens to have a single `set` key is extremely unusual).
|
package/dist/cjs/realtime.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* turbine-orm
|
|
2
|
+
* turbine-orm, LISTEN/NOTIFY realtime pub/sub
|
|
3
3
|
*
|
|
4
4
|
* Postgres LISTEN/NOTIFY is a first-class realtime primitive that neither
|
|
5
5
|
* Prisma nor Drizzle expose ergonomically. This module backs the thin
|
|
6
6
|
* `$listen` / `$notify` methods on TurbineClient.
|
|
7
7
|
*
|
|
8
|
-
* Design
|
|
8
|
+
* Design, **one dedicated connection per subscription**:
|
|
9
9
|
*
|
|
10
10
|
* Each `$listen(channel, handler)` acquires its OWN long-lived client from
|
|
11
11
|
* the pool, runs `LISTEN "chan"`, and keeps that connection checked out for
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* subscription owns its lifecycle, `unsubscribe()` cleanly UNLISTENs and
|
|
14
14
|
* releases exactly one connection, and there is no shared multiplexing
|
|
15
15
|
* state to reason about. The trade-off is one pool slot per active channel
|
|
16
|
-
*
|
|
16
|
+
* - for the handful of channels a typical app listens on, that's a fine
|
|
17
17
|
* price for clarity. (A future optimization could multiplex many channels
|
|
18
18
|
* over a single shared notification connection.)
|
|
19
19
|
*
|
|
@@ -23,14 +23,14 @@
|
|
|
23
23
|
* notification messages back to the client. Stateless HTTP drivers
|
|
24
24
|
* (Neon HTTP, Vercel Postgres over fetch) cannot hold such a connection, so
|
|
25
25
|
* `$listen` will surface a clear error rather than hang. `$notify` works
|
|
26
|
-
* everywhere
|
|
26
|
+
* everywhere, it's a single round-trip `SELECT pg_notify(...)`.
|
|
27
27
|
*/
|
|
28
28
|
import type { PgCompatPool } from './client.js';
|
|
29
29
|
/**
|
|
30
30
|
* Validate a LISTEN/NOTIFY channel name. Throws ValidationError on anything
|
|
31
31
|
* that isn't a plain, reasonable-length SQL identifier. This is enforced for
|
|
32
32
|
* BOTH `$listen` (where the channel is interpolated) and `$notify` (where the
|
|
33
|
-
* channel is a bound param)
|
|
33
|
+
* channel is a bound param), defensive parity, and it catches user typos
|
|
34
34
|
* loudly.
|
|
35
35
|
*/
|
|
36
36
|
export declare function validateChannel(channel: string): void;
|
|
@@ -45,7 +45,7 @@ export interface Subscription {
|
|
|
45
45
|
readonly channel: string;
|
|
46
46
|
/**
|
|
47
47
|
* Stop listening: runs `UNLISTEN "chan"`, removes the notification listener,
|
|
48
|
-
* and releases the dedicated connection. Idempotent
|
|
48
|
+
* and releases the dedicated connection. Idempotent, safe to call twice.
|
|
49
49
|
*/
|
|
50
50
|
unsubscribe(): Promise<void>;
|
|
51
51
|
}
|
|
@@ -61,7 +61,7 @@ export interface ActiveSubscription extends Subscription {
|
|
|
61
61
|
* Acquire a dedicated connection, run `LISTEN "channel"`, and wire the handler.
|
|
62
62
|
*
|
|
63
63
|
* @param pool the pg-compatible pool to check a long-lived client out of
|
|
64
|
-
* @param channel channel name
|
|
64
|
+
* @param channel channel name, MUST already be validated by the caller
|
|
65
65
|
* @param quotedChannel the channel run through quoteIdent (interpolated into SQL)
|
|
66
66
|
* @param handler called with each notification's payload
|
|
67
67
|
* @param onClosed invoked when the subscription releases, so the client can
|
package/dist/cjs/realtime.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* turbine-orm
|
|
3
|
+
* turbine-orm, LISTEN/NOTIFY realtime pub/sub
|
|
4
4
|
*
|
|
5
5
|
* Postgres LISTEN/NOTIFY is a first-class realtime primitive that neither
|
|
6
6
|
* Prisma nor Drizzle expose ergonomically. This module backs the thin
|
|
7
7
|
* `$listen` / `$notify` methods on TurbineClient.
|
|
8
8
|
*
|
|
9
|
-
* Design
|
|
9
|
+
* Design, **one dedicated connection per subscription**:
|
|
10
10
|
*
|
|
11
11
|
* Each `$listen(channel, handler)` acquires its OWN long-lived client from
|
|
12
12
|
* the pool, runs `LISTEN "chan"`, and keeps that connection checked out for
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* subscription owns its lifecycle, `unsubscribe()` cleanly UNLISTENs and
|
|
15
15
|
* releases exactly one connection, and there is no shared multiplexing
|
|
16
16
|
* state to reason about. The trade-off is one pool slot per active channel
|
|
17
|
-
*
|
|
17
|
+
* - for the handful of channels a typical app listens on, that's a fine
|
|
18
18
|
* price for clarity. (A future optimization could multiplex many channels
|
|
19
19
|
* over a single shared notification connection.)
|
|
20
20
|
*
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* notification messages back to the client. Stateless HTTP drivers
|
|
25
25
|
* (Neon HTTP, Vercel Postgres over fetch) cannot hold such a connection, so
|
|
26
26
|
* `$listen` will surface a clear error rather than hang. `$notify` works
|
|
27
|
-
* everywhere
|
|
27
|
+
* everywhere, it's a single round-trip `SELECT pg_notify(...)`.
|
|
28
28
|
*/
|
|
29
29
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
30
30
|
exports.validateChannel = validateChannel;
|
|
@@ -37,7 +37,7 @@ const errors_js_1 = require("./errors.js");
|
|
|
37
37
|
* Strict Postgres identifier: a letter or underscore followed by letters,
|
|
38
38
|
* digits, or underscores. Channel names CANNOT be parameterized in
|
|
39
39
|
* LISTEN/UNLISTEN (`LISTEN $1` is a syntax error), so the channel is the one
|
|
40
|
-
* place an identifier is interpolated into SQL
|
|
40
|
+
* place an identifier is interpolated into SQL, it MUST pass this regex AND
|
|
41
41
|
* go through `quoteIdent` before reaching the SQL string.
|
|
42
42
|
*/
|
|
43
43
|
const CHANNEL_REGEX = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
@@ -47,7 +47,7 @@ const MAX_CHANNEL_LEN = 63;
|
|
|
47
47
|
* Validate a LISTEN/NOTIFY channel name. Throws ValidationError on anything
|
|
48
48
|
* that isn't a plain, reasonable-length SQL identifier. This is enforced for
|
|
49
49
|
* BOTH `$listen` (where the channel is interpolated) and `$notify` (where the
|
|
50
|
-
* channel is a bound param)
|
|
50
|
+
* channel is a bound param), defensive parity, and it catches user typos
|
|
51
51
|
* loudly.
|
|
52
52
|
*/
|
|
53
53
|
function validateChannel(channel) {
|
|
@@ -58,7 +58,7 @@ function validateChannel(channel) {
|
|
|
58
58
|
throw new errors_js_1.ValidationError(`[turbine] $listen/$notify channel "${channel}" exceeds the ${MAX_CHANNEL_LEN}-character Postgres identifier limit`);
|
|
59
59
|
}
|
|
60
60
|
if (!CHANNEL_REGEX.test(channel)) {
|
|
61
|
-
throw new errors_js_1.ValidationError(`[turbine] Invalid $listen/$notify channel "${channel}"
|
|
61
|
+
throw new errors_js_1.ValidationError(`[turbine] Invalid $listen/$notify channel "${channel}", must match /^[A-Za-z_][A-Za-z0-9_]*$/ ` +
|
|
62
62
|
'(letters, digits, underscores; cannot start with a digit)');
|
|
63
63
|
}
|
|
64
64
|
}
|
|
@@ -66,7 +66,7 @@ function validateChannel(channel) {
|
|
|
66
66
|
* Acquire a dedicated connection, run `LISTEN "channel"`, and wire the handler.
|
|
67
67
|
*
|
|
68
68
|
* @param pool the pg-compatible pool to check a long-lived client out of
|
|
69
|
-
* @param channel channel name
|
|
69
|
+
* @param channel channel name, MUST already be validated by the caller
|
|
70
70
|
* @param quotedChannel the channel run through quoteIdent (interpolated into SQL)
|
|
71
71
|
* @param handler called with each notification's payload
|
|
72
72
|
* @param onClosed invoked when the subscription releases, so the client can
|
|
@@ -81,7 +81,7 @@ async function createSubscription(pool, channel, quotedChannel, handler, onClose
|
|
|
81
81
|
throw (0, errors_js_1.wrapPgError)(err);
|
|
82
82
|
}
|
|
83
83
|
// Verify the checked-out client can actually receive async notifications.
|
|
84
|
-
// Stateless HTTP drivers return a client with no `.on
|
|
84
|
+
// Stateless HTTP drivers return a client with no `.on`, LISTEN would hang
|
|
85
85
|
// forever waiting for messages that can never arrive, so fail loudly now and
|
|
86
86
|
// give the connection straight back.
|
|
87
87
|
if (typeof client.on !== 'function') {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* turbine-orm
|
|
2
|
+
* turbine-orm, Schema Builder
|
|
3
3
|
*
|
|
4
4
|
* TypeScript-first schema definition API. Define your database schema
|
|
5
|
-
* as plain objects
|
|
5
|
+
* as plain objects, no method chaining, no DSL. Fully type-checked,
|
|
6
6
|
* JSON-serializable, and easy to read.
|
|
7
7
|
*
|
|
8
8
|
* @example
|
|
@@ -56,9 +56,9 @@ export interface ColumnDef {
|
|
|
56
56
|
references?: string | ReferenceDef;
|
|
57
57
|
/** Max length for varchar columns */
|
|
58
58
|
maxLength?: number;
|
|
59
|
-
/** Enum type name
|
|
59
|
+
/** Enum type name, required when `type: 'enum'`. */
|
|
60
60
|
enumName?: string;
|
|
61
|
-
/** pgvector dimension count
|
|
61
|
+
/** pgvector dimension count, required when `type: 'vector'`. */
|
|
62
62
|
dimensions?: number;
|
|
63
63
|
/** When true, the column is an array of `type` (e.g. `text[]`). */
|
|
64
64
|
array?: boolean;
|
|
@@ -73,6 +73,13 @@ export interface ColumnDef {
|
|
|
73
73
|
* `includePii: true`) and redacted by Studio. Introspection never auto-tags PII.
|
|
74
74
|
*/
|
|
75
75
|
pii?: boolean;
|
|
76
|
+
/**
|
|
77
|
+
* Set this column to the current time on every `update` that does not name
|
|
78
|
+
* it explicitly (Prisma's `@updatedAt`). Opt-in per column and never
|
|
79
|
+
* inferred from the column name: see
|
|
80
|
+
* {@link import('./schema.js').ColumnMetadata.updatedAt}.
|
|
81
|
+
*/
|
|
82
|
+
updatedAt?: boolean;
|
|
76
83
|
}
|
|
77
84
|
/** Postgres-level column type (uppercase, as used in DDL) */
|
|
78
85
|
export type ColumnType = 'SERIAL' | 'BIGSERIAL' | 'BIGINT' | 'INTEGER' | 'SMALLINT' | 'TEXT' | 'BOOLEAN' | 'TIMESTAMPTZ' | 'JSONB' | 'UUID' | 'REAL' | 'DOUBLE PRECISION' | 'NUMERIC' | 'BYTEA' | 'DATE' | 'VARCHAR' | 'ENUM' | 'VECTOR';
|
|
@@ -99,13 +106,15 @@ export interface ColumnConfig {
|
|
|
99
106
|
check: string | null;
|
|
100
107
|
/** Whether this column is tagged as PII (personally identifiable information). */
|
|
101
108
|
pii: boolean;
|
|
109
|
+
/** Whether this column is auto-set to the current time on update. */
|
|
110
|
+
updatedAt: boolean;
|
|
102
111
|
}
|
|
103
112
|
/**
|
|
104
113
|
* Explicit many-to-many relation declaration for the code-first schema.
|
|
105
114
|
*
|
|
106
115
|
* Auto-detecting m2m from a junction table is intentionally conservative (a
|
|
107
116
|
* junction with payload columns is treated as a first-class entity, not a join
|
|
108
|
-
* table
|
|
117
|
+
* table, see `introspect.ts`). This declaration lets users opt in to an m2m
|
|
109
118
|
* relation explicitly, mirroring how Prisma/Drizzle require an explicit
|
|
110
119
|
* `@relation` / `relation()` for join tables.
|
|
111
120
|
*
|
|
@@ -196,7 +205,7 @@ export interface TableDef {
|
|
|
196
205
|
/**
|
|
197
206
|
* Optional composite primary key. When present, takes precedence over any
|
|
198
207
|
* column-level `primaryKey: true` flags. Column names listed here are the
|
|
199
|
-
* camelCase JS-facing field names
|
|
208
|
+
* camelCase JS-facing field names, they will be converted to snake_case
|
|
200
209
|
* when emitted as a `PRIMARY KEY (...)` table constraint.
|
|
201
210
|
*/
|
|
202
211
|
primaryKey?: readonly string[];
|
|
@@ -311,6 +320,8 @@ export declare class ColumnBuilder {
|
|
|
311
320
|
}): this;
|
|
312
321
|
check(expression: string): this;
|
|
313
322
|
pii(): this;
|
|
323
|
+
/** Auto-set this column to the current time on every update. Prisma's `@updatedAt`. */
|
|
324
|
+
updatedAt(): this;
|
|
314
325
|
array(): this;
|
|
315
326
|
build(): ColumnConfig;
|
|
316
327
|
}
|
|
@@ -331,7 +342,7 @@ export declare function table(columns: Record<string, ColumnBuilder>): TableDef;
|
|
|
331
342
|
*
|
|
332
343
|
* This is the runtime bridge for the code-first m2m API: `defineSchema` only
|
|
333
344
|
* produces DDL, so after `introspect()`ing the live database you call this to
|
|
334
|
-
* attach the m2m relations you declared. It is PURELY ADDITIVE
|
|
345
|
+
* attach the m2m relations you declared. It is PURELY ADDITIVE, existing
|
|
335
346
|
* belongsTo/hasMany/hasOne relations are preserved, and a declared relation is
|
|
336
347
|
* skipped (not overwritten) if its name already exists on the source table.
|
|
337
348
|
*
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* turbine-orm
|
|
3
|
+
* turbine-orm, Schema Builder
|
|
4
4
|
*
|
|
5
5
|
* TypeScript-first schema definition API. Define your database schema
|
|
6
|
-
* as plain objects
|
|
6
|
+
* as plain objects, no method chaining, no DSL. Fully type-checked,
|
|
7
7
|
* JSON-serializable, and easy to read.
|
|
8
8
|
*
|
|
9
9
|
* @example
|
|
@@ -32,7 +32,7 @@ exports.applyManyToManyRelations = applyManyToManyRelations;
|
|
|
32
32
|
/** Maps shorthand names to actual Postgres type strings */
|
|
33
33
|
const TYPE_MAP = {
|
|
34
34
|
// `serial` maps to SERIAL (int4). Its values fit in a JS `number` and pg
|
|
35
|
-
// returns them as numbers
|
|
35
|
+
// returns them as numbers, so the generated `number` type is accurate.
|
|
36
36
|
// For 64-bit auto-increment keys use `bigserial` (int8), noting that values
|
|
37
37
|
// above Number.MAX_SAFE_INTEGER read back as string. (Changed in 0.24.0;
|
|
38
38
|
// `serial` previously emitted BIGSERIAL.)
|
|
@@ -44,7 +44,7 @@ const TYPE_MAP = {
|
|
|
44
44
|
text: 'TEXT',
|
|
45
45
|
varchar: 'VARCHAR',
|
|
46
46
|
boolean: 'BOOLEAN',
|
|
47
|
-
// `timestamp` is an honest alias for TIMESTAMPTZ (timezone-aware)
|
|
47
|
+
// `timestamp` is an honest alias for TIMESTAMPTZ (timezone-aware), Turbine
|
|
48
48
|
// has always emitted TIMESTAMPTZ for it. `timestamptz` is the explicit spelling.
|
|
49
49
|
timestamp: 'TIMESTAMPTZ',
|
|
50
50
|
timestamptz: 'TIMESTAMPTZ',
|
|
@@ -58,7 +58,7 @@ const TYPE_MAP = {
|
|
|
58
58
|
double: 'DOUBLE PRECISION',
|
|
59
59
|
numeric: 'NUMERIC',
|
|
60
60
|
bytea: 'BYTEA',
|
|
61
|
-
// Sentinels
|
|
61
|
+
// Sentinels, the real DDL type is derived from `enumName` / `dimensions`
|
|
62
62
|
// in schema-sql.ts, never from these placeholders.
|
|
63
63
|
enum: 'ENUM',
|
|
64
64
|
vector: 'VECTOR',
|
|
@@ -102,6 +102,7 @@ function resolveColumn(def) {
|
|
|
102
102
|
isArray: def.array ?? false,
|
|
103
103
|
check: def.check ?? null,
|
|
104
104
|
pii: def.pii ?? false,
|
|
105
|
+
updatedAt: def.updatedAt ?? false,
|
|
105
106
|
};
|
|
106
107
|
}
|
|
107
108
|
/** Type guard: is this index declaration a doc-field expression index? */
|
|
@@ -195,7 +196,7 @@ function defineSchema(input, options) {
|
|
|
195
196
|
}
|
|
196
197
|
// Validate composite PK references real columns and clear column-level PKs
|
|
197
198
|
// for those columns so we don't double-emit `PRIMARY KEY` clauses.
|
|
198
|
-
// Composite PK members are implicitly NOT NULL
|
|
199
|
+
// Composite PK members are implicitly NOT NULL, preserve that even
|
|
199
200
|
// when the user clears the column-level `primaryKey: true` flag.
|
|
200
201
|
if (pk && pk.length > 0) {
|
|
201
202
|
for (const colName of pk) {
|
|
@@ -204,7 +205,7 @@ function defineSchema(input, options) {
|
|
|
204
205
|
`Known columns: ${Object.keys(columns).join(', ') || '(none)'}`);
|
|
205
206
|
}
|
|
206
207
|
// A composite PK at the table level supersedes any column-level
|
|
207
|
-
// `primaryKey: true` flag
|
|
208
|
+
// `primaryKey: true` flag, silently clear it so DDL emission
|
|
208
209
|
// produces a single, valid table-level PRIMARY KEY constraint.
|
|
209
210
|
// Force NOT NULL since PK columns can never be nullable.
|
|
210
211
|
const c = columns[colName];
|
|
@@ -235,7 +236,7 @@ function camelToSnakeLocal(s) {
|
|
|
235
236
|
return s.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);
|
|
236
237
|
}
|
|
237
238
|
// ---------------------------------------------------------------------------
|
|
238
|
-
// Legacy compat
|
|
239
|
+
// Legacy compat, ColumnBuilder still works for existing code
|
|
239
240
|
// ---------------------------------------------------------------------------
|
|
240
241
|
class ColumnBuilder {
|
|
241
242
|
_config;
|
|
@@ -256,6 +257,7 @@ class ColumnBuilder {
|
|
|
256
257
|
isArray: false,
|
|
257
258
|
check: null,
|
|
258
259
|
pii: false,
|
|
260
|
+
updatedAt: false,
|
|
259
261
|
};
|
|
260
262
|
}
|
|
261
263
|
serial() {
|
|
@@ -366,6 +368,11 @@ class ColumnBuilder {
|
|
|
366
368
|
this._config.pii = true;
|
|
367
369
|
return this;
|
|
368
370
|
}
|
|
371
|
+
/** Auto-set this column to the current time on every update. Prisma's `@updatedAt`. */
|
|
372
|
+
updatedAt() {
|
|
373
|
+
this._config.updatedAt = true;
|
|
374
|
+
return this;
|
|
375
|
+
}
|
|
369
376
|
array() {
|
|
370
377
|
this._config.isArray = true;
|
|
371
378
|
return this;
|
|
@@ -429,7 +436,7 @@ function table(columns) {
|
|
|
429
436
|
*
|
|
430
437
|
* This is the runtime bridge for the code-first m2m API: `defineSchema` only
|
|
431
438
|
* produces DDL, so after `introspect()`ing the live database you call this to
|
|
432
|
-
* attach the m2m relations you declared. It is PURELY ADDITIVE
|
|
439
|
+
* attach the m2m relations you declared. It is PURELY ADDITIVE, existing
|
|
433
440
|
* belongsTo/hasMany/hasOne relations are preserved, and a declared relation is
|
|
434
441
|
* skipped (not overwritten) if its name already exists on the source table.
|
|
435
442
|
*
|
|
@@ -468,7 +475,7 @@ function applyManyToManyRelations(meta, def) {
|
|
|
468
475
|
const sourceTable = tableDef.name;
|
|
469
476
|
const sourceMeta = tables[sourceTable];
|
|
470
477
|
if (!sourceMeta)
|
|
471
|
-
continue; // table not present in introspected metadata
|
|
478
|
+
continue; // table not present in introspected metadata, skip
|
|
472
479
|
const relations = { ...sourceMeta.relations };
|
|
473
480
|
for (const m of tableDef.manyToMany) {
|
|
474
481
|
// Additive-only: never clobber an existing relation name.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* turbine-orm
|
|
2
|
+
* turbine-orm, defineSchema() → SchemaMetadata bridge
|
|
3
3
|
*
|
|
4
4
|
* Converts a code-first {@link SchemaDef} (the output of `defineSchema()`)
|
|
5
5
|
* into the runtime {@link SchemaMetadata} shape that the query builder,
|
|
6
|
-
* `TurbineClient`, and the non-SQL engines consume
|
|
6
|
+
* `TurbineClient`, and the non-SQL engines consume, without touching a
|
|
7
7
|
* live database.
|
|
8
8
|
*
|
|
9
9
|
* Why this exists: the historical converter path (`introspect()` +
|
|
@@ -49,7 +49,7 @@ import { type SchemaDef } from './schema-builder.js';
|
|
|
49
49
|
/**
|
|
50
50
|
* Convert a code-first {@link SchemaDef} into runtime {@link SchemaMetadata}.
|
|
51
51
|
*
|
|
52
|
-
* Pure function
|
|
52
|
+
* Pure function, no database connection, no side effects, input untouched.
|
|
53
53
|
* The output is shaped identically to the `SCHEMA` constant `turbine generate`
|
|
54
54
|
* emits from introspection, so it can be handed to any consumer that expects
|
|
55
55
|
* introspected metadata: `new TurbineClient(config, metadata)`,
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* turbine-orm
|
|
3
|
+
* turbine-orm, defineSchema() → SchemaMetadata bridge
|
|
4
4
|
*
|
|
5
5
|
* Converts a code-first {@link SchemaDef} (the output of `defineSchema()`)
|
|
6
6
|
* into the runtime {@link SchemaMetadata} shape that the query builder,
|
|
7
|
-
* `TurbineClient`, and the non-SQL engines consume
|
|
7
|
+
* `TurbineClient`, and the non-SQL engines consume, without touching a
|
|
8
8
|
* live database.
|
|
9
9
|
*
|
|
10
10
|
* Why this exists: the historical converter path (`introspect()` +
|
|
@@ -80,13 +80,13 @@ function udtName(config) {
|
|
|
80
80
|
return 'vector';
|
|
81
81
|
return DDL_TO_UDT[config.type];
|
|
82
82
|
}
|
|
83
|
-
/** Server-generated (sequence-backed) types
|
|
83
|
+
/** Server-generated (sequence-backed) types, pg's `nextval(...)` default. */
|
|
84
84
|
function isSerialType(type) {
|
|
85
85
|
return type === 'SERIAL' || type === 'BIGSERIAL';
|
|
86
86
|
}
|
|
87
87
|
/**
|
|
88
88
|
* Resolve the raw column part of a `references: 'table.column'` target to a
|
|
89
|
-
* snake_case column name
|
|
89
|
+
* snake_case column name, accepting either the camelCase field name or the
|
|
90
90
|
* snake_case DDL name, mirroring how schema-sql.ts accepts both table forms.
|
|
91
91
|
*/
|
|
92
92
|
function resolveColumnName(raw, target) {
|
|
@@ -166,7 +166,7 @@ function mapIndexes(tableDef, declared) {
|
|
|
166
166
|
/**
|
|
167
167
|
* Convert a code-first {@link SchemaDef} into runtime {@link SchemaMetadata}.
|
|
168
168
|
*
|
|
169
|
-
* Pure function
|
|
169
|
+
* Pure function, no database connection, no side effects, input untouched.
|
|
170
170
|
* The output is shaped identically to the `SCHEMA` constant `turbine generate`
|
|
171
171
|
* emits from introspection, so it can be handed to any consumer that expects
|
|
172
172
|
* introspected metadata: `new TurbineClient(config, metadata)`,
|
|
@@ -200,7 +200,7 @@ function mapIndexes(tableDef, declared) {
|
|
|
200
200
|
function schemaDefToMetadata(def) {
|
|
201
201
|
// ----- Pass 1: resolve every table's snake_case names for FK lookups -----
|
|
202
202
|
// Lookup accepts the accessor key (camelCase), the DDL name (snake_case),
|
|
203
|
-
// and the explicit `accessor` field
|
|
203
|
+
// and the explicit `accessor` field, same tolerance as schema-sql.ts.
|
|
204
204
|
const lookup = new Map();
|
|
205
205
|
for (const [key, tableDef] of Object.entries(def.tables)) {
|
|
206
206
|
const fieldToColumn = new Map();
|
|
@@ -227,7 +227,7 @@ function schemaDefToMetadata(def) {
|
|
|
227
227
|
if (parts.length !== 2)
|
|
228
228
|
continue;
|
|
229
229
|
const target = lookup.get(parts[0]);
|
|
230
|
-
// Reference to a table outside this SchemaDef
|
|
230
|
+
// Reference to a table outside this SchemaDef, skip, exactly like
|
|
231
231
|
// introspection skips FKs whose target is excluded from the table set.
|
|
232
232
|
if (!target)
|
|
233
233
|
continue;
|
|
@@ -249,14 +249,14 @@ function schemaDefToMetadata(def) {
|
|
|
249
249
|
// catalog: legacy-first naming, per-column disambiguation for several FKs to
|
|
250
250
|
// the same target, and collision resolution against scalar column fields
|
|
251
251
|
// (json/jsonb `unknown`-typed shadows keep the historical name). The old
|
|
252
|
-
// local reimplementation had NO collision guard
|
|
252
|
+
// local reimplementation had NO collision guard, `posts.user` (text) +
|
|
253
253
|
// `userId references users.id` produced a relation `user` that shadowed the
|
|
254
254
|
// scalar, and two FKs deriving the same name silently clobbered each other
|
|
255
255
|
// (N-4).
|
|
256
256
|
//
|
|
257
257
|
// Constraint names are synthesized in pg's default `<table>_<column>_fkey`
|
|
258
258
|
// form; they only feed the referential-action lookup and composite-FK
|
|
259
|
-
// naming (never hit here
|
|
259
|
+
// naming (never hit here, `references:` is single-column by design).
|
|
260
260
|
const fkEntries = [];
|
|
261
261
|
const fkActions = new Map();
|
|
262
262
|
for (const fk of foreignKeys) {
|