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/query/writes.js
CHANGED
|
@@ -21,10 +21,10 @@ import * as whereMod from './where.js';
|
|
|
21
21
|
*
|
|
22
22
|
* Two value shapes need rewriting, both a JS `Date` on a temporal column:
|
|
23
23
|
*
|
|
24
|
-
* 1. A time-of-day column (`time` / `timetz`)
|
|
24
|
+
* 1. A time-of-day column (`time` / `timetz`), the driver serializes a `Date`
|
|
25
25
|
* as a full ISO timestamp with the process offset and Postgres answers
|
|
26
26
|
* `22007 invalid input syntax for type time`. Rewritten on every engine.
|
|
27
|
-
* 2. A zone-less `date` / `timestamp` column on PostgreSQL
|
|
27
|
+
* 2. A zone-less `date` / `timestamp` column on PostgreSQL, the driver's
|
|
28
28
|
* local-offset serialization stores the PROCESS's calendar fields, so in a
|
|
29
29
|
* non-UTC process the stored value is wrong and (because the read path
|
|
30
30
|
* interprets an offset-less value as UTC) does not round-trip. Rewritten to
|
|
@@ -59,7 +59,7 @@ export function coerceWriteValue(qi, key, value) {
|
|
|
59
59
|
* PostgreSQL only: MySQL's `TIMESTAMP`/`DATETIME` and SQL Server's
|
|
60
60
|
* `datetime2` are converted or bound by their own drivers, and MySQL in
|
|
61
61
|
* particular reads a zone-less literal in the SESSION time zone, so a UTC
|
|
62
|
-
* literal would be misread there. `utcTimestamps: false` opts out
|
|
62
|
+
* literal would be misread there. `utcTimestamps: false` opts out, it is the
|
|
63
63
|
* same switch that turns off the UTC READ parsing, so the two stay symmetric.
|
|
64
64
|
*/
|
|
65
65
|
function utcDateTimeWrites(qi) {
|
|
@@ -158,7 +158,7 @@ export function buildCreateMany(qi, args) {
|
|
|
158
158
|
return keys.map((key) => coerceWriteValue(qi, key, record[key]));
|
|
159
159
|
});
|
|
160
160
|
// Use actual Postgres types for array casts in the default PostgreSQL dialect.
|
|
161
|
-
// Enum columns cast to `"EnumName"[]
|
|
161
|
+
// Enum columns cast to `"EnumName"[]`, the generic text[] fallback would
|
|
162
162
|
// type the UNNEST output as text, which Postgres refuses to coerce to the
|
|
163
163
|
// enum ("column X is of type Y but expression is of type text").
|
|
164
164
|
const typeCasts = columns.map((col) => {
|
|
@@ -166,6 +166,18 @@ export function buildCreateMany(qi, args) {
|
|
|
166
166
|
return enumType ? `${qi.q(enumType)}[]` : whereMod.getColumnArrayType(qi, col);
|
|
167
167
|
});
|
|
168
168
|
const quotedColumns = columns.map((c) => qi.q(c));
|
|
169
|
+
// ARRAY-TYPED COLUMNS force the row-at-a-time VALUES form.
|
|
170
|
+
//
|
|
171
|
+
// The default PostgreSQL shape is `SELECT * FROM UNNEST($1::text[], …)`,
|
|
172
|
+
// one bound array per COLUMN. That is a column-major transpose, and it
|
|
173
|
+
// cannot express an array-valued column: `unnest` flattens, so N rows each
|
|
174
|
+
// holding a `text[]` arrive as one flat `text[]` and PostgreSQL rejects the
|
|
175
|
+
// insert with 42804 ("column is of type text[] but expression is of type
|
|
176
|
+
// text"). Single-row `create` never hit it because it binds each value
|
|
177
|
+
// directly. Detected from the column's own declared type rather than from
|
|
178
|
+
// the shape of the first row's value, so a row whose array column happens to
|
|
179
|
+
// be null or absent still takes the correct form.
|
|
180
|
+
const hasArrayColumn = columns.some((col) => (qi.columnPgTypeMap.get(col) ?? '').startsWith('_'));
|
|
169
181
|
const built = qi.dialect.buildBulkInsertStatement({
|
|
170
182
|
table: qt,
|
|
171
183
|
columns: quotedColumns,
|
|
@@ -173,6 +185,7 @@ export function buildCreateMany(qi, args) {
|
|
|
173
185
|
columnArrayTypes: typeCasts,
|
|
174
186
|
skipDuplicates: args.skipDuplicates,
|
|
175
187
|
returning: writeReturningColumns(qi),
|
|
188
|
+
requireRowValues: hasArrayColumn,
|
|
176
189
|
});
|
|
177
190
|
return {
|
|
178
191
|
sql: built.sql,
|
|
@@ -184,20 +197,51 @@ export function buildCreateMany(qi, args) {
|
|
|
184
197
|
export function buildUpdate(qi, args) {
|
|
185
198
|
assertWritable(qi, 'update');
|
|
186
199
|
qi.currentSkip = args.skipGlobalFilters;
|
|
187
|
-
|
|
200
|
+
// `updatedAt`-tagged columns are filled in before anything reads `data`, so
|
|
201
|
+
// the SET list, the fingerprint and the param collector all see one object.
|
|
202
|
+
const dataObj = applyUpdatedAtColumns(qi, args.data);
|
|
188
203
|
assertNoGeneratedColumns(qi, dataObj, 'update');
|
|
189
204
|
// Prisma compound-unique selector (e.g. `{ orgId_userId: { orgId, userId } }`)
|
|
190
205
|
// → the column conjunction, before the empty-`where` guard so the expanded
|
|
191
206
|
// members count as a real predicate.
|
|
192
207
|
const userWhere = expandCompoundUniqueWhere(qi.tableMeta, args.where);
|
|
193
208
|
const lock = args.optimisticLock;
|
|
194
|
-
// The empty-`where` guard checks the USER predicate only
|
|
209
|
+
// The empty-`where` guard checks the USER predicate only, a global filter
|
|
195
210
|
// must never turn an unguarded mass update into an allowed one.
|
|
196
211
|
const userHasPredicate = !whereMod.userPredicateIsEmpty(qi, userWhere) || !!lock;
|
|
197
212
|
whereMod.assertMutationHasPredicate(qi, 'update', userHasPredicate ? ' WHERE x' : '', args.allowFullTableScan);
|
|
198
213
|
// The SQL is built from the global-filter-merged where (soft-delete keeps an
|
|
199
214
|
// update from touching already-deleted rows).
|
|
200
215
|
const whereObj = (whereMod.mergeGlobalFilter(qi, userWhere) ?? {});
|
|
216
|
+
// An update with nothing to set is a NO-OP that returns the current row.
|
|
217
|
+
//
|
|
218
|
+
// It used to render `UPDATE t SET WHERE …` and fail with a raw PostgreSQL
|
|
219
|
+
// 42601 syntax error. That is easy to hit honestly: any handler that builds
|
|
220
|
+
// its payload from optional request fields produces `{}` on a request that
|
|
221
|
+
// supplied none of them, so a legitimate request turned into a 500. Prisma
|
|
222
|
+
// treats the same call as a no-op and returns the row, so a ported handler
|
|
223
|
+
// inherited a crash where the original returned 200.
|
|
224
|
+
//
|
|
225
|
+
// The row is re-selected through the same projection a real update would
|
|
226
|
+
// return (PII columns excluded on tagged tables), and a where matching no
|
|
227
|
+
// row still raises NotFoundError, so only the SQL differs, not the contract.
|
|
228
|
+
// `optimisticLock` is excluded: it always has a version column to SET and a
|
|
229
|
+
// version check that must still run.
|
|
230
|
+
const hasSetData = Object.values(dataObj).some((v) => v !== undefined);
|
|
231
|
+
if (!hasSetData && !lock) {
|
|
232
|
+
const sel = buildReselectByWhere(qi, whereObj);
|
|
233
|
+
return {
|
|
234
|
+
sql: sel.sql,
|
|
235
|
+
params: sel.params,
|
|
236
|
+
transform: (result) => {
|
|
237
|
+
const row = result.rows[0];
|
|
238
|
+
if (!row)
|
|
239
|
+
throw new NotFoundError({ table: qi.table, where: args.where, operation: 'update' });
|
|
240
|
+
return parseWriteRow(qi, row);
|
|
241
|
+
},
|
|
242
|
+
tag: `${qi.table}.update`,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
201
245
|
const setFp = fingerprintSet(qi, dataObj);
|
|
202
246
|
const whereFp = whereMod.fingerprintWhere(qi, whereObj);
|
|
203
247
|
const ck = lock ? null : `u:${setFp}|${whereFp}${whereMod.globalFilterCacheSegment(qi)}`;
|
|
@@ -277,7 +321,7 @@ export function buildUpdate(qi, args) {
|
|
|
277
321
|
// Optimistic-lock conflict: the version-checked UPDATE matched no
|
|
278
322
|
// row. The re-fetch below uses `where` WITHOUT the version
|
|
279
323
|
// predicate, so it would return the stale row and silently mask
|
|
280
|
-
// the conflict
|
|
324
|
+
// the conflict, detect it from affected-rows here instead, to
|
|
281
325
|
// match the OptimisticLockError thrown on RETURNING/OUTPUT engines.
|
|
282
326
|
if (lock && (writeResult.rowCount ?? 0) === 0) {
|
|
283
327
|
throw new OptimisticLockError({
|
|
@@ -358,7 +402,7 @@ export function buildUpsert(qi, args) {
|
|
|
358
402
|
const createParams = createEntries.map(([k, v]) => coerceWriteValue(qi, k, v));
|
|
359
403
|
// Enum columns get an explicit `::"EnumName"` cast (see enumTypeForColumn).
|
|
360
404
|
const placeholders = createEntries.map(([k], i) => `${qi.p(i + 1)}${whereMod.enumCastSuffix(qi, qi.toColumn(k))}`);
|
|
361
|
-
// The conflict target comes from `where` keys
|
|
405
|
+
// The conflict target comes from `where` keys, must be unique/PK columns
|
|
362
406
|
const conflictKeys = Object.keys(upsertWhere).filter((k) => upsertWhere[k] !== undefined);
|
|
363
407
|
const conflictColumns = conflictKeys.map((k) => qi.toSqlColumn(k));
|
|
364
408
|
// Build the UPDATE SET part
|
|
@@ -419,10 +463,22 @@ export function buildUpsert(qi, args) {
|
|
|
419
463
|
export function buildUpdateMany(qi, args) {
|
|
420
464
|
assertWritable(qi, 'updateMany');
|
|
421
465
|
qi.currentSkip = args.skipGlobalFilters;
|
|
422
|
-
const dataObj = args.data;
|
|
466
|
+
const dataObj = applyUpdatedAtColumns(qi, args.data);
|
|
423
467
|
assertNoGeneratedColumns(qi, dataObj, 'updateMany');
|
|
424
468
|
whereMod.assertMutationHasPredicate(qi, 'updateMany', whereMod.userPredicateIsEmpty(qi, args.where) ? '' : ' WHERE x', args.allowFullTableScan);
|
|
425
469
|
const whereObj = (whereMod.mergeGlobalFilter(qi, args.where) ?? {});
|
|
470
|
+
// Nothing to SET: a no-op, for the same reason as `update` above (that path
|
|
471
|
+
// has the full rationale). Reports `count: 0` because zero rows were
|
|
472
|
+
// modified, no statement is issued at all, so this is not the count of rows
|
|
473
|
+
// the predicate MATCHED.
|
|
474
|
+
if (!Object.values(dataObj).some((v) => v !== undefined)) {
|
|
475
|
+
return {
|
|
476
|
+
sql: `SELECT ${qi.p(1)} AS count`,
|
|
477
|
+
params: [0],
|
|
478
|
+
transform: () => ({ count: 0 }),
|
|
479
|
+
tag: `${qi.table}.updateMany`,
|
|
480
|
+
};
|
|
481
|
+
}
|
|
426
482
|
const setFp = fingerprintSet(qi, dataObj);
|
|
427
483
|
const whereFp = whereMod.fingerprintWhere(qi, whereObj);
|
|
428
484
|
const ck = `um:${setFp}|${whereFp}${whereMod.globalFilterCacheSegment(qi)}`;
|
|
@@ -503,17 +559,49 @@ export function piiFields(_qi, meta) {
|
|
|
503
559
|
}
|
|
504
560
|
/**
|
|
505
561
|
* The `RETURNING` / `OUTPUT` selection for a write on this table. A table with
|
|
506
|
-
* no PII column returns `'*'` (every column
|
|
562
|
+
* no PII column returns `'*'` (every column, byte-identical SQL to before);
|
|
507
563
|
* a table WITH PII columns returns an explicit quoted list of every non-PII
|
|
508
564
|
* column so the PII values never leave the database on a write. A PII-tagged
|
|
509
565
|
* PRIMARY KEY column is kept in the projection regardless (the returned row
|
|
510
|
-
* must stay addressable): tag sensitive data, not keys
|
|
566
|
+
* must stay addressable): tag sensitive data, not keys, a PII PK is
|
|
511
567
|
* documented out of scope for stripping. Writes accept no `select`/`includePii`
|
|
512
568
|
* (unlike reads), so this is the whole write-return policy at the SQL level;
|
|
513
569
|
* {@link parseWriteRow} remains as a defense-in-depth strip (a no-op once the
|
|
514
570
|
* SQL already excludes the columns). Derived purely from static per-table
|
|
515
571
|
* schema metadata, so the write SQL cache needs no extra key segment.
|
|
516
572
|
*/
|
|
573
|
+
/**
|
|
574
|
+
* Return `data` with every `updatedAt`-tagged column the caller did not name
|
|
575
|
+
* set to `now`, or the original object when the table has none.
|
|
576
|
+
*
|
|
577
|
+
* Prisma's `@updatedAt` has no turbine equivalent, so a migrated application
|
|
578
|
+
* had to remember the field on every single update, and the value is usually
|
|
579
|
+
* load-bearing in the response body, so forgetting it is a silent staleness
|
|
580
|
+
* bug rather than a crash. The tag is opt-in per column and never inferred
|
|
581
|
+
* from a column's name, so a schema that does not use it emits byte-identical
|
|
582
|
+
* SQL and an application already managing its own timestamp is untouched.
|
|
583
|
+
*
|
|
584
|
+
* The timestamp is generated CLIENT-side (like Prisma) rather than as a SQL
|
|
585
|
+
* `now()`, so it flows through the same temporal coercion as any other bound
|
|
586
|
+
* `Date` and lands in UTC on every engine.
|
|
587
|
+
*
|
|
588
|
+
* An explicit value always wins, including an explicit `null`: naming the
|
|
589
|
+
* column is a statement of intent.
|
|
590
|
+
*/
|
|
591
|
+
export function applyUpdatedAtColumns(qi, data) {
|
|
592
|
+
const tagged = qi.tableMeta.columns.filter((c) => c.updatedAt);
|
|
593
|
+
if (tagged.length === 0)
|
|
594
|
+
return data;
|
|
595
|
+
let out = null;
|
|
596
|
+
const now = new Date();
|
|
597
|
+
for (const col of tagged) {
|
|
598
|
+
if (Object.hasOwn(data, col.field) && data[col.field] !== undefined)
|
|
599
|
+
continue;
|
|
600
|
+
out ??= { ...data };
|
|
601
|
+
out[col.field] = now;
|
|
602
|
+
}
|
|
603
|
+
return out ?? data;
|
|
604
|
+
}
|
|
517
605
|
export function writeReturningColumns(qi) {
|
|
518
606
|
const piiCols = piiColumns(qi, qi.tableMeta);
|
|
519
607
|
if (piiCols.size === 0)
|
|
@@ -571,7 +659,7 @@ export function assertNoGeneratedColumns(qi, data, operation) {
|
|
|
571
659
|
const col = qi.tableMeta.columns.find((c) => c.field === key || c.name === key || c.name === camelToSnake(key));
|
|
572
660
|
if (col?.isGeneratedStored) {
|
|
573
661
|
throw new ValidationError(`[turbine] Cannot ${operation} "${qi.table}": column "${key}" is a GENERATED ALWAYS AS (…) STORED ` +
|
|
574
|
-
'column whose value the database computes
|
|
662
|
+
'column whose value the database computes, remove it from your data.');
|
|
575
663
|
}
|
|
576
664
|
}
|
|
577
665
|
}
|
|
@@ -580,7 +668,7 @@ export function assertNoGeneratedColumns(qi, data, operation) {
|
|
|
580
668
|
*
|
|
581
669
|
* Supports plain values and atomic operator objects ({ set, increment,
|
|
582
670
|
* decrement, multiply, divide }). An operator object is detected ONLY when
|
|
583
|
-
* it has EXACTLY one key that is one of the 5 operator keys
|
|
671
|
+
* it has EXACTLY one key that is one of the 5 operator keys, this avoids
|
|
584
672
|
* misinterpreting JSON column values like `{ set: 'x' }` as operators
|
|
585
673
|
* (real operator objects always have exactly one key, and a plain JSON
|
|
586
674
|
* payload that happens to have a single `set` key is extremely unusual).
|
package/dist/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/realtime.js
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,7 +23,7 @@
|
|
|
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 { ConnectionError, ValidationError, wrapPgError } from './errors.js';
|
|
29
29
|
// ---------------------------------------------------------------------------
|
|
@@ -33,7 +33,7 @@ import { ConnectionError, ValidationError, wrapPgError } from './errors.js';
|
|
|
33
33
|
* Strict Postgres identifier: a letter or underscore followed by letters,
|
|
34
34
|
* digits, or underscores. Channel names CANNOT be parameterized in
|
|
35
35
|
* LISTEN/UNLISTEN (`LISTEN $1` is a syntax error), so the channel is the one
|
|
36
|
-
* place an identifier is interpolated into SQL
|
|
36
|
+
* place an identifier is interpolated into SQL, it MUST pass this regex AND
|
|
37
37
|
* go through `quoteIdent` before reaching the SQL string.
|
|
38
38
|
*/
|
|
39
39
|
const CHANNEL_REGEX = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
@@ -43,7 +43,7 @@ const MAX_CHANNEL_LEN = 63;
|
|
|
43
43
|
* Validate a LISTEN/NOTIFY channel name. Throws ValidationError on anything
|
|
44
44
|
* that isn't a plain, reasonable-length SQL identifier. This is enforced for
|
|
45
45
|
* BOTH `$listen` (where the channel is interpolated) and `$notify` (where the
|
|
46
|
-
* channel is a bound param)
|
|
46
|
+
* channel is a bound param), defensive parity, and it catches user typos
|
|
47
47
|
* loudly.
|
|
48
48
|
*/
|
|
49
49
|
export function validateChannel(channel) {
|
|
@@ -54,7 +54,7 @@ export function validateChannel(channel) {
|
|
|
54
54
|
throw new ValidationError(`[turbine] $listen/$notify channel "${channel}" exceeds the ${MAX_CHANNEL_LEN}-character Postgres identifier limit`);
|
|
55
55
|
}
|
|
56
56
|
if (!CHANNEL_REGEX.test(channel)) {
|
|
57
|
-
throw new ValidationError(`[turbine] Invalid $listen/$notify channel "${channel}"
|
|
57
|
+
throw new ValidationError(`[turbine] Invalid $listen/$notify channel "${channel}", must match /^[A-Za-z_][A-Za-z0-9_]*$/ ` +
|
|
58
58
|
'(letters, digits, underscores; cannot start with a digit)');
|
|
59
59
|
}
|
|
60
60
|
}
|
|
@@ -62,7 +62,7 @@ export function validateChannel(channel) {
|
|
|
62
62
|
* Acquire a dedicated connection, run `LISTEN "channel"`, and wire the handler.
|
|
63
63
|
*
|
|
64
64
|
* @param pool the pg-compatible pool to check a long-lived client out of
|
|
65
|
-
* @param channel channel name
|
|
65
|
+
* @param channel channel name, MUST already be validated by the caller
|
|
66
66
|
* @param quotedChannel the channel run through quoteIdent (interpolated into SQL)
|
|
67
67
|
* @param handler called with each notification's payload
|
|
68
68
|
* @param onClosed invoked when the subscription releases, so the client can
|
|
@@ -77,7 +77,7 @@ export async function createSubscription(pool, channel, quotedChannel, handler,
|
|
|
77
77
|
throw wrapPgError(err);
|
|
78
78
|
}
|
|
79
79
|
// Verify the checked-out client can actually receive async notifications.
|
|
80
|
-
// Stateless HTTP drivers return a client with no `.on
|
|
80
|
+
// Stateless HTTP drivers return a client with no `.on`, LISTEN would hang
|
|
81
81
|
// forever waiting for messages that can never arrive, so fail loudly now and
|
|
82
82
|
// give the connection straight back.
|
|
83
83
|
if (typeof client.on !== 'function') {
|
package/dist/schema-builder.d.ts
CHANGED
|
@@ -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
|
*
|
package/dist/schema-builder.js
CHANGED
|
@@ -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
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
/** Maps shorthand names to actual Postgres type strings */
|
|
26
26
|
const TYPE_MAP = {
|
|
27
27
|
// `serial` maps to SERIAL (int4). Its values fit in a JS `number` and pg
|
|
28
|
-
// returns them as numbers
|
|
28
|
+
// returns them as numbers, so the generated `number` type is accurate.
|
|
29
29
|
// For 64-bit auto-increment keys use `bigserial` (int8), noting that values
|
|
30
30
|
// above Number.MAX_SAFE_INTEGER read back as string. (Changed in 0.24.0;
|
|
31
31
|
// `serial` previously emitted BIGSERIAL.)
|
|
@@ -37,7 +37,7 @@ const TYPE_MAP = {
|
|
|
37
37
|
text: 'TEXT',
|
|
38
38
|
varchar: 'VARCHAR',
|
|
39
39
|
boolean: 'BOOLEAN',
|
|
40
|
-
// `timestamp` is an honest alias for TIMESTAMPTZ (timezone-aware)
|
|
40
|
+
// `timestamp` is an honest alias for TIMESTAMPTZ (timezone-aware), Turbine
|
|
41
41
|
// has always emitted TIMESTAMPTZ for it. `timestamptz` is the explicit spelling.
|
|
42
42
|
timestamp: 'TIMESTAMPTZ',
|
|
43
43
|
timestamptz: 'TIMESTAMPTZ',
|
|
@@ -51,7 +51,7 @@ const TYPE_MAP = {
|
|
|
51
51
|
double: 'DOUBLE PRECISION',
|
|
52
52
|
numeric: 'NUMERIC',
|
|
53
53
|
bytea: 'BYTEA',
|
|
54
|
-
// Sentinels
|
|
54
|
+
// Sentinels, the real DDL type is derived from `enumName` / `dimensions`
|
|
55
55
|
// in schema-sql.ts, never from these placeholders.
|
|
56
56
|
enum: 'ENUM',
|
|
57
57
|
vector: 'VECTOR',
|
|
@@ -95,6 +95,7 @@ function resolveColumn(def) {
|
|
|
95
95
|
isArray: def.array ?? false,
|
|
96
96
|
check: def.check ?? null,
|
|
97
97
|
pii: def.pii ?? false,
|
|
98
|
+
updatedAt: def.updatedAt ?? false,
|
|
98
99
|
};
|
|
99
100
|
}
|
|
100
101
|
/** Type guard: is this index declaration a doc-field expression index? */
|
|
@@ -188,7 +189,7 @@ export function defineSchema(input, options) {
|
|
|
188
189
|
}
|
|
189
190
|
// Validate composite PK references real columns and clear column-level PKs
|
|
190
191
|
// for those columns so we don't double-emit `PRIMARY KEY` clauses.
|
|
191
|
-
// Composite PK members are implicitly NOT NULL
|
|
192
|
+
// Composite PK members are implicitly NOT NULL, preserve that even
|
|
192
193
|
// when the user clears the column-level `primaryKey: true` flag.
|
|
193
194
|
if (pk && pk.length > 0) {
|
|
194
195
|
for (const colName of pk) {
|
|
@@ -197,7 +198,7 @@ export function defineSchema(input, options) {
|
|
|
197
198
|
`Known columns: ${Object.keys(columns).join(', ') || '(none)'}`);
|
|
198
199
|
}
|
|
199
200
|
// A composite PK at the table level supersedes any column-level
|
|
200
|
-
// `primaryKey: true` flag
|
|
201
|
+
// `primaryKey: true` flag, silently clear it so DDL emission
|
|
201
202
|
// produces a single, valid table-level PRIMARY KEY constraint.
|
|
202
203
|
// Force NOT NULL since PK columns can never be nullable.
|
|
203
204
|
const c = columns[colName];
|
|
@@ -228,7 +229,7 @@ function camelToSnakeLocal(s) {
|
|
|
228
229
|
return s.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);
|
|
229
230
|
}
|
|
230
231
|
// ---------------------------------------------------------------------------
|
|
231
|
-
// Legacy compat
|
|
232
|
+
// Legacy compat, ColumnBuilder still works for existing code
|
|
232
233
|
// ---------------------------------------------------------------------------
|
|
233
234
|
export class ColumnBuilder {
|
|
234
235
|
_config;
|
|
@@ -249,6 +250,7 @@ export class ColumnBuilder {
|
|
|
249
250
|
isArray: false,
|
|
250
251
|
check: null,
|
|
251
252
|
pii: false,
|
|
253
|
+
updatedAt: false,
|
|
252
254
|
};
|
|
253
255
|
}
|
|
254
256
|
serial() {
|
|
@@ -359,6 +361,11 @@ export class ColumnBuilder {
|
|
|
359
361
|
this._config.pii = true;
|
|
360
362
|
return this;
|
|
361
363
|
}
|
|
364
|
+
/** Auto-set this column to the current time on every update. Prisma's `@updatedAt`. */
|
|
365
|
+
updatedAt() {
|
|
366
|
+
this._config.updatedAt = true;
|
|
367
|
+
return this;
|
|
368
|
+
}
|
|
362
369
|
array() {
|
|
363
370
|
this._config.isArray = true;
|
|
364
371
|
return this;
|
|
@@ -421,7 +428,7 @@ export function table(columns) {
|
|
|
421
428
|
*
|
|
422
429
|
* This is the runtime bridge for the code-first m2m API: `defineSchema` only
|
|
423
430
|
* produces DDL, so after `introspect()`ing the live database you call this to
|
|
424
|
-
* attach the m2m relations you declared. It is PURELY ADDITIVE
|
|
431
|
+
* attach the m2m relations you declared. It is PURELY ADDITIVE, existing
|
|
425
432
|
* belongsTo/hasMany/hasOne relations are preserved, and a declared relation is
|
|
426
433
|
* skipped (not overwritten) if its name already exists on the source table.
|
|
427
434
|
*
|
|
@@ -460,7 +467,7 @@ export function applyManyToManyRelations(meta, def) {
|
|
|
460
467
|
const sourceTable = tableDef.name;
|
|
461
468
|
const sourceMeta = tables[sourceTable];
|
|
462
469
|
if (!sourceMeta)
|
|
463
|
-
continue; // table not present in introspected metadata
|
|
470
|
+
continue; // table not present in introspected metadata, skip
|
|
464
471
|
const relations = { ...sourceMeta.relations };
|
|
465
472
|
for (const m of tableDef.manyToMany) {
|
|
466
473
|
// 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)`,
|
package/dist/schema-metadata.js
CHANGED
|
@@ -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()` +
|
|
@@ -77,13 +77,13 @@ function udtName(config) {
|
|
|
77
77
|
return 'vector';
|
|
78
78
|
return DDL_TO_UDT[config.type];
|
|
79
79
|
}
|
|
80
|
-
/** Server-generated (sequence-backed) types
|
|
80
|
+
/** Server-generated (sequence-backed) types, pg's `nextval(...)` default. */
|
|
81
81
|
function isSerialType(type) {
|
|
82
82
|
return type === 'SERIAL' || type === 'BIGSERIAL';
|
|
83
83
|
}
|
|
84
84
|
/**
|
|
85
85
|
* Resolve the raw column part of a `references: 'table.column'` target to a
|
|
86
|
-
* snake_case column name
|
|
86
|
+
* snake_case column name, accepting either the camelCase field name or the
|
|
87
87
|
* snake_case DDL name, mirroring how schema-sql.ts accepts both table forms.
|
|
88
88
|
*/
|
|
89
89
|
function resolveColumnName(raw, target) {
|
|
@@ -163,7 +163,7 @@ function mapIndexes(tableDef, declared) {
|
|
|
163
163
|
/**
|
|
164
164
|
* Convert a code-first {@link SchemaDef} into runtime {@link SchemaMetadata}.
|
|
165
165
|
*
|
|
166
|
-
* Pure function
|
|
166
|
+
* Pure function, no database connection, no side effects, input untouched.
|
|
167
167
|
* The output is shaped identically to the `SCHEMA` constant `turbine generate`
|
|
168
168
|
* emits from introspection, so it can be handed to any consumer that expects
|
|
169
169
|
* introspected metadata: `new TurbineClient(config, metadata)`,
|
|
@@ -197,7 +197,7 @@ function mapIndexes(tableDef, declared) {
|
|
|
197
197
|
export function schemaDefToMetadata(def) {
|
|
198
198
|
// ----- Pass 1: resolve every table's snake_case names for FK lookups -----
|
|
199
199
|
// Lookup accepts the accessor key (camelCase), the DDL name (snake_case),
|
|
200
|
-
// and the explicit `accessor` field
|
|
200
|
+
// and the explicit `accessor` field, same tolerance as schema-sql.ts.
|
|
201
201
|
const lookup = new Map();
|
|
202
202
|
for (const [key, tableDef] of Object.entries(def.tables)) {
|
|
203
203
|
const fieldToColumn = new Map();
|
|
@@ -224,7 +224,7 @@ export function schemaDefToMetadata(def) {
|
|
|
224
224
|
if (parts.length !== 2)
|
|
225
225
|
continue;
|
|
226
226
|
const target = lookup.get(parts[0]);
|
|
227
|
-
// Reference to a table outside this SchemaDef
|
|
227
|
+
// Reference to a table outside this SchemaDef, skip, exactly like
|
|
228
228
|
// introspection skips FKs whose target is excluded from the table set.
|
|
229
229
|
if (!target)
|
|
230
230
|
continue;
|
|
@@ -246,14 +246,14 @@ export function schemaDefToMetadata(def) {
|
|
|
246
246
|
// catalog: legacy-first naming, per-column disambiguation for several FKs to
|
|
247
247
|
// the same target, and collision resolution against scalar column fields
|
|
248
248
|
// (json/jsonb `unknown`-typed shadows keep the historical name). The old
|
|
249
|
-
// local reimplementation had NO collision guard
|
|
249
|
+
// local reimplementation had NO collision guard, `posts.user` (text) +
|
|
250
250
|
// `userId references users.id` produced a relation `user` that shadowed the
|
|
251
251
|
// scalar, and two FKs deriving the same name silently clobbered each other
|
|
252
252
|
// (N-4).
|
|
253
253
|
//
|
|
254
254
|
// Constraint names are synthesized in pg's default `<table>_<column>_fkey`
|
|
255
255
|
// form; they only feed the referential-action lookup and composite-FK
|
|
256
|
-
// naming (never hit here
|
|
256
|
+
// naming (never hit here, `references:` is single-column by design).
|
|
257
257
|
const fkEntries = [];
|
|
258
258
|
const fkActions = new Map();
|
|
259
259
|
for (const fk of foreignKeys) {
|