turbine-orm 0.16.0 → 0.18.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 +180 -12
- package/dist/adapters/cockroachdb.js +4 -2
- package/dist/adapters/index.js +4 -1
- package/dist/adapters/yugabytedb.js +4 -2
- package/dist/cjs/adapters/cockroachdb.js +4 -2
- package/dist/cjs/adapters/index.js +4 -1
- package/dist/cjs/adapters/yugabytedb.js +4 -2
- package/dist/cjs/cli/studio.js +5 -1
- package/dist/cjs/client.js +164 -0
- package/dist/cjs/errors.js +35 -5
- package/dist/cjs/generate.js +14 -3
- package/dist/cjs/index.js +10 -2
- package/dist/cjs/introspect.js +81 -0
- package/dist/cjs/nested-write.js +70 -6
- package/dist/cjs/query/builder.js +538 -12
- package/dist/cjs/realtime.js +147 -0
- package/dist/cjs/schema-builder.js +86 -0
- package/dist/cjs/schema.js +10 -0
- package/dist/cjs/typed-sql.js +149 -0
- package/dist/cli/studio.js +5 -1
- package/dist/client.d.ts +120 -0
- package/dist/client.js +165 -1
- package/dist/errors.js +35 -5
- package/dist/generate.js +14 -3
- package/dist/index.d.ts +4 -2
- package/dist/index.js +5 -1
- package/dist/introspect.js +81 -0
- package/dist/nested-write.js +70 -6
- package/dist/query/builder.d.ts +104 -1
- package/dist/query/builder.js +539 -13
- package/dist/query/index.d.ts +1 -1
- package/dist/query/types.d.ts +126 -2
- package/dist/realtime.d.ts +71 -0
- package/dist/realtime.js +144 -0
- package/dist/schema-builder.d.ts +68 -1
- package/dist/schema-builder.js +85 -0
- package/dist/schema.d.ts +18 -1
- package/dist/schema.js +10 -0
- package/dist/typed-sql.d.ts +101 -0
- package/dist/typed-sql.js +145 -0
- package/package.json +17 -15
package/dist/cjs/errors.js
CHANGED
|
@@ -211,7 +211,13 @@ class UniqueConstraintError extends TurbineError {
|
|
|
211
211
|
const constraintPart = constraint ? ` on ${constraint}` : '';
|
|
212
212
|
const columnsPart = columns && columns.length > 0 ? ` (${columns.join(', ')})` : '';
|
|
213
213
|
message = `[turbine] Unique constraint violation${constraintPart}${columnsPart}`;
|
|
214
|
-
|
|
214
|
+
// PII-safe by default: the raw pg `detail` string contains the
|
|
215
|
+
// conflicting row VALUES (e.g. `Key (email)=(alice@x.com) already
|
|
216
|
+
// exists.`). Only append it in 'verbose' mode. In 'safe' mode the
|
|
217
|
+
// message carries keys/constraint/column names only — the structured
|
|
218
|
+
// `.columns`/`.constraint`/`.column` fields and `.cause` still expose
|
|
219
|
+
// the full detail for programmatic use.
|
|
220
|
+
const detail = errorMessageMode === 'verbose' ? detailFromCause(cause) : undefined;
|
|
215
221
|
if (detail)
|
|
216
222
|
message += `: ${detail}`;
|
|
217
223
|
}
|
|
@@ -233,7 +239,13 @@ class ForeignKeyError extends TurbineError {
|
|
|
233
239
|
if (!message) {
|
|
234
240
|
const constraintPart = constraint ? ` on ${constraint}` : '';
|
|
235
241
|
message = `[turbine] Foreign key constraint violation${constraintPart}`;
|
|
236
|
-
|
|
242
|
+
// PII-safe by default: the raw pg `detail` string contains the
|
|
243
|
+
// conflicting row VALUES (e.g. `Key (email)=(alice@x.com) already
|
|
244
|
+
// exists.`). Only append it in 'verbose' mode. In 'safe' mode the
|
|
245
|
+
// message carries keys/constraint/column names only — the structured
|
|
246
|
+
// `.columns`/`.constraint`/`.column` fields and `.cause` still expose
|
|
247
|
+
// the full detail for programmatic use.
|
|
248
|
+
const detail = errorMessageMode === 'verbose' ? detailFromCause(cause) : undefined;
|
|
237
249
|
if (detail)
|
|
238
250
|
message += `: ${detail}`;
|
|
239
251
|
}
|
|
@@ -254,7 +266,13 @@ class NotNullViolationError extends TurbineError {
|
|
|
254
266
|
if (!message) {
|
|
255
267
|
const columnPart = column ? ` on column "${column}"` : '';
|
|
256
268
|
message = `[turbine] NOT NULL constraint violation${columnPart}`;
|
|
257
|
-
|
|
269
|
+
// PII-safe by default: the raw pg `detail` string contains the
|
|
270
|
+
// conflicting row VALUES (e.g. `Key (email)=(alice@x.com) already
|
|
271
|
+
// exists.`). Only append it in 'verbose' mode. In 'safe' mode the
|
|
272
|
+
// message carries keys/constraint/column names only — the structured
|
|
273
|
+
// `.columns`/`.constraint`/`.column` fields and `.cause` still expose
|
|
274
|
+
// the full detail for programmatic use.
|
|
275
|
+
const detail = errorMessageMode === 'verbose' ? detailFromCause(cause) : undefined;
|
|
258
276
|
if (detail)
|
|
259
277
|
message += `: ${detail}`;
|
|
260
278
|
}
|
|
@@ -342,7 +360,13 @@ class CheckConstraintError extends TurbineError {
|
|
|
342
360
|
if (!message) {
|
|
343
361
|
const constraintPart = constraint ? ` on ${constraint}` : '';
|
|
344
362
|
message = `[turbine] Check constraint violation${constraintPart}`;
|
|
345
|
-
|
|
363
|
+
// PII-safe by default: the raw pg `detail` string contains the
|
|
364
|
+
// conflicting row VALUES (e.g. `Key (email)=(alice@x.com) already
|
|
365
|
+
// exists.`). Only append it in 'verbose' mode. In 'safe' mode the
|
|
366
|
+
// message carries keys/constraint/column names only — the structured
|
|
367
|
+
// `.columns`/`.constraint`/`.column` fields and `.cause` still expose
|
|
368
|
+
// the full detail for programmatic use.
|
|
369
|
+
const detail = errorMessageMode === 'verbose' ? detailFromCause(cause) : undefined;
|
|
346
370
|
if (detail)
|
|
347
371
|
message += `: ${detail}`;
|
|
348
372
|
}
|
|
@@ -362,7 +386,13 @@ class ExclusionConstraintError extends TurbineError {
|
|
|
362
386
|
if (!message) {
|
|
363
387
|
const constraintPart = constraint ? ` on ${constraint}` : '';
|
|
364
388
|
message = `[turbine] Exclusion constraint violation${constraintPart}`;
|
|
365
|
-
|
|
389
|
+
// PII-safe by default: the raw pg `detail` string contains the
|
|
390
|
+
// conflicting row VALUES (e.g. `Key (email)=(alice@x.com) already
|
|
391
|
+
// exists.`). Only append it in 'verbose' mode. In 'safe' mode the
|
|
392
|
+
// message carries keys/constraint/column names only — the structured
|
|
393
|
+
// `.columns`/`.constraint`/`.column` fields and `.cause` still expose
|
|
394
|
+
// the full detail for programmatic use.
|
|
395
|
+
const detail = errorMessageMode === 'verbose' ? detailFromCause(cause) : undefined;
|
|
366
396
|
if (detail)
|
|
367
397
|
message += `: ${detail}`;
|
|
368
398
|
}
|
package/dist/cjs/generate.js
CHANGED
|
@@ -156,7 +156,8 @@ function generateTypes(schema) {
|
|
|
156
156
|
lines.push(`export interface ${typeName}Relations {`);
|
|
157
157
|
for (const [relName, rel] of Object.entries(table.relations)) {
|
|
158
158
|
const targetType = entityName(rel.to);
|
|
159
|
-
|
|
159
|
+
// manyToMany is a collection too → 'many' cardinality (same as hasMany).
|
|
160
|
+
const cardinality = rel.type === 'hasMany' || rel.type === 'manyToMany' ? "'many'" : "'one'";
|
|
160
161
|
const targetRelations = tablesWithRelations.has(rel.to) ? `${targetType}Relations` : '{}';
|
|
161
162
|
lines.push(` ${relName}: RelationDescriptor<${targetType}, ${cardinality}, ${targetRelations}>;`);
|
|
162
163
|
}
|
|
@@ -165,7 +166,7 @@ function generateTypes(schema) {
|
|
|
165
166
|
// --- Legacy per-relation interfaces (kept for backward compatibility) ---
|
|
166
167
|
for (const [relName, rel] of Object.entries(table.relations)) {
|
|
167
168
|
const targetType = entityName(rel.to);
|
|
168
|
-
if (rel.type === 'hasMany') {
|
|
169
|
+
if (rel.type === 'hasMany' || rel.type === 'manyToMany') {
|
|
169
170
|
lines.push(`/** ${typeName} with \`${relName}\` relation loaded (${rel.type}: ${rel.to}) */`);
|
|
170
171
|
lines.push(`export interface ${typeName}With${(0, schema_js_1.snakeToPascal)(relName)} extends ${typeName} {`);
|
|
171
172
|
lines.push(` ${relName}: ${targetType}[];`);
|
|
@@ -332,7 +333,17 @@ function generateMetadata(schema) {
|
|
|
332
333
|
const refLiteral = Array.isArray(rel.referenceKey)
|
|
333
334
|
? `[${rel.referenceKey.map((c) => `'${escSQ(c)}'`).join(', ')}]`
|
|
334
335
|
: `'${escSQ(rel.referenceKey)}'`;
|
|
335
|
-
|
|
336
|
+
// manyToMany relations carry a `through` junction descriptor — emit it so
|
|
337
|
+
// the runtime query builder can JOIN through the junction table.
|
|
338
|
+
let throughLiteral = '';
|
|
339
|
+
if (rel.through) {
|
|
340
|
+
const keyLiteral = (k) => Array.isArray(k) ? `[${k.map((c) => `'${escSQ(c)}'`).join(', ')}]` : `'${escSQ(k)}'`;
|
|
341
|
+
throughLiteral =
|
|
342
|
+
`, through: { table: '${escSQ(rel.through.table)}', ` +
|
|
343
|
+
`sourceKey: ${keyLiteral(rel.through.sourceKey)}, ` +
|
|
344
|
+
`targetKey: ${keyLiteral(rel.through.targetKey)} }`;
|
|
345
|
+
}
|
|
346
|
+
lines.push(` ${relName}: { type: '${escSQ(rel.type)}', name: '${escSQ(rel.name)}', from: '${escSQ(rel.from)}', to: '${escSQ(rel.to)}', foreignKey: ${fkLiteral}, referenceKey: ${refLiteral}${throughLiteral} },`);
|
|
336
347
|
}
|
|
337
348
|
lines.push(' },');
|
|
338
349
|
// indexes
|
package/dist/cjs/index.js
CHANGED
|
@@ -34,8 +34,8 @@
|
|
|
34
34
|
* ```
|
|
35
35
|
*/
|
|
36
36
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
-
exports.
|
|
38
|
-
exports.turbineHttp = exports.schemaToSQLString = exports.schemaToSQL = exports.schemaPush = exports.schemaDiff = void 0;
|
|
37
|
+
exports.column = exports.ColumnBuilder = exports.applyManyToManyRelations = exports.snakeToPascal = exports.snakeToCamel = exports.singularize = exports.pgTypeToTs = exports.pgArrayType = exports.normalizeKeyColumns = exports.isDateType = exports.camelToSnake = exports.validateChannel = exports.QueryInterface = exports.pipelineSupported = exports.executePipeline = exports.hasRelationFields = exports.executeNestedUpdate = exports.executeNestedCreate = exports.introspect = exports.generate = exports.wrapPgError = exports.ValidationError = exports.UniqueConstraintError = exports.TurbineErrorCode = exports.TurbineError = exports.TimeoutError = exports.setErrorMessageMode = exports.SerializationFailureError = exports.RelationError = exports.PipelineError = exports.OptimisticLockError = exports.NotNullViolationError = exports.NotFoundError = exports.MigrationError = exports.getErrorMessageMode = exports.ForeignKeyError = exports.ExclusionConstraintError = exports.DeadlockError = exports.ConnectionError = exports.CircularRelationError = exports.CheckConstraintError = exports.postgresDialect = exports.withRetry = exports.TurbineClient = exports.TransactionClient = exports.yugabytedb = exports.timescale = exports.postgresql = exports.cockroachdb = exports.alloydb = void 0;
|
|
38
|
+
exports.TypedSqlQuery = exports.buildTypedSql = exports.turbineHttp = exports.schemaToSQLString = exports.schemaToSQL = exports.schemaPush = exports.schemaDiff = exports.table = exports.defineSchema = void 0;
|
|
39
39
|
var index_js_1 = require("./adapters/index.js");
|
|
40
40
|
Object.defineProperty(exports, "alloydb", { enumerable: true, get: function () { return index_js_1.alloydb; } });
|
|
41
41
|
Object.defineProperty(exports, "cockroachdb", { enumerable: true, get: function () { return index_js_1.cockroachdb; } });
|
|
@@ -90,6 +90,9 @@ Object.defineProperty(exports, "pipelineSupported", { enumerable: true, get: fun
|
|
|
90
90
|
// Query builder
|
|
91
91
|
var index_js_2 = require("./query/index.js");
|
|
92
92
|
Object.defineProperty(exports, "QueryInterface", { enumerable: true, get: function () { return index_js_2.QueryInterface; } });
|
|
93
|
+
// Realtime — LISTEN/NOTIFY pub/sub
|
|
94
|
+
var realtime_js_1 = require("./realtime.js");
|
|
95
|
+
Object.defineProperty(exports, "validateChannel", { enumerable: true, get: function () { return realtime_js_1.validateChannel; } });
|
|
93
96
|
// Schema utilities
|
|
94
97
|
var schema_js_1 = require("./schema.js");
|
|
95
98
|
Object.defineProperty(exports, "camelToSnake", { enumerable: true, get: function () { return schema_js_1.camelToSnake; } });
|
|
@@ -102,6 +105,7 @@ Object.defineProperty(exports, "snakeToCamel", { enumerable: true, get: function
|
|
|
102
105
|
Object.defineProperty(exports, "snakeToPascal", { enumerable: true, get: function () { return schema_js_1.snakeToPascal; } });
|
|
103
106
|
// Schema builder — define schemas in TypeScript
|
|
104
107
|
var schema_builder_js_1 = require("./schema-builder.js");
|
|
108
|
+
Object.defineProperty(exports, "applyManyToManyRelations", { enumerable: true, get: function () { return schema_builder_js_1.applyManyToManyRelations; } });
|
|
105
109
|
Object.defineProperty(exports, "ColumnBuilder", { enumerable: true, get: function () { return schema_builder_js_1.ColumnBuilder; } });
|
|
106
110
|
Object.defineProperty(exports, "column", { enumerable: true, get: function () { return schema_builder_js_1.column; } });
|
|
107
111
|
Object.defineProperty(exports, "defineSchema", { enumerable: true, get: function () { return schema_builder_js_1.defineSchema; } });
|
|
@@ -116,3 +120,7 @@ Object.defineProperty(exports, "schemaToSQLString", { enumerable: true, get: fun
|
|
|
116
120
|
// Serverless / edge factory
|
|
117
121
|
var serverless_js_1 = require("./serverless.js");
|
|
118
122
|
Object.defineProperty(exports, "turbineHttp", { enumerable: true, get: function () { return serverless_js_1.turbineHttp; } });
|
|
123
|
+
// Typed raw SQL — Turbine's TypedSQL escape hatch
|
|
124
|
+
var typed_sql_js_1 = require("./typed-sql.js");
|
|
125
|
+
Object.defineProperty(exports, "buildTypedSql", { enumerable: true, get: function () { return typed_sql_js_1.buildTypedSql; } });
|
|
126
|
+
Object.defineProperty(exports, "TypedSqlQuery", { enumerable: true, get: function () { return typed_sql_js_1.TypedSqlQuery; } });
|
package/dist/cjs/introspect.js
CHANGED
|
@@ -281,6 +281,87 @@ async function introspect(options) {
|
|
|
281
281
|
referenceKey,
|
|
282
282
|
};
|
|
283
283
|
}
|
|
284
|
+
// ----- Conservative many-to-many auto-detection (PURELY ADDITIVE) -----
|
|
285
|
+
//
|
|
286
|
+
// Auto-detecting m2m is a footgun: any table with two FKs *looks* like a
|
|
287
|
+
// junction, but a `enrollments(student_id, course_id, grade, enrolled_at)`
|
|
288
|
+
// table is a first-class entity, not a join table. Prisma and Drizzle both
|
|
289
|
+
// require explicit m2m declaration for exactly this reason.
|
|
290
|
+
//
|
|
291
|
+
// We only treat a table J as a PURE junction when ALL of these hold:
|
|
292
|
+
// 1. J's primary key is exactly two columns.
|
|
293
|
+
// 2. J has exactly two FKs, each single-column.
|
|
294
|
+
// 3. Each FK's source column is one of J's two PK columns (the PK *is* the
|
|
295
|
+
// two FK columns — no surrogate PK, no extra identity).
|
|
296
|
+
// 4. The two FKs target two DISTINCT tables (A and B).
|
|
297
|
+
// 5. J has no columns beyond those two FK/PK columns (no payload columns
|
|
298
|
+
// like `grade` or `created_at`).
|
|
299
|
+
//
|
|
300
|
+
// For such a J linking A and B we ADD a `manyToMany` relation on A → B and
|
|
301
|
+
// symmetrically on B → A, both routed `through` J. The existing belongsTo /
|
|
302
|
+
// hasMany relations derived from J's FKs are left untouched — this block
|
|
303
|
+
// never removes or renames anything. If the chosen relation name already
|
|
304
|
+
// exists on the source table (e.g. another relation grabbed it), we SKIP to
|
|
305
|
+
// stay additive.
|
|
306
|
+
for (const tableName of tableNames) {
|
|
307
|
+
const pk = pkByTable.get(tableName) ?? [];
|
|
308
|
+
if (pk.length !== 2)
|
|
309
|
+
continue;
|
|
310
|
+
// FKs whose source is this table.
|
|
311
|
+
const tableFks = foreignKeys.filter((fk) => fk.sourceTable === tableName);
|
|
312
|
+
if (tableFks.length !== 2)
|
|
313
|
+
continue;
|
|
314
|
+
// Both FKs must be single-column.
|
|
315
|
+
if (tableFks.some((fk) => fk.sourceColumns.length !== 1))
|
|
316
|
+
continue;
|
|
317
|
+
const fkCols = tableFks.map((fk) => fk.sourceColumns[0]);
|
|
318
|
+
const pkSet = new Set(pk);
|
|
319
|
+
// Both FK columns must be the PK columns (and vice-versa).
|
|
320
|
+
if (!fkCols.every((c) => pkSet.has(c)))
|
|
321
|
+
continue;
|
|
322
|
+
if (new Set(fkCols).size !== 2)
|
|
323
|
+
continue;
|
|
324
|
+
// Two DISTINCT target tables.
|
|
325
|
+
const [fkA, fkB] = tableFks;
|
|
326
|
+
if (fkA.targetTable === fkB.targetTable)
|
|
327
|
+
continue;
|
|
328
|
+
// No payload columns: J's columns are exactly the two FK/PK columns.
|
|
329
|
+
const jCols = (columnsByTable.get(tableName) ?? []).map((c) => c.name);
|
|
330
|
+
if (jCols.length !== 2)
|
|
331
|
+
continue;
|
|
332
|
+
// For each direction, the m2m `referenceKey` is the *targeted* table's
|
|
333
|
+
// referenced column(s); the junction's sourceKey is the FK column pointing
|
|
334
|
+
// to that table; the targetKey is the FK column pointing to the OTHER table.
|
|
335
|
+
const addM2M = (self, other) => {
|
|
336
|
+
const sourceTbl = self.targetTable; // A
|
|
337
|
+
const targetTbl = other.targetTable; // B
|
|
338
|
+
const relName = (0, schema_js_1.snakeToCamel)(targetTbl); // plural table name → e.g. "tags"
|
|
339
|
+
if (!relationsByTable.has(sourceTbl))
|
|
340
|
+
relationsByTable.set(sourceTbl, {});
|
|
341
|
+
const existing = relationsByTable.get(sourceTbl);
|
|
342
|
+
// Additive-only: never clobber an existing relation name.
|
|
343
|
+
if (existing[relName])
|
|
344
|
+
return;
|
|
345
|
+
existing[relName] = {
|
|
346
|
+
type: 'manyToMany',
|
|
347
|
+
name: relName,
|
|
348
|
+
from: sourceTbl,
|
|
349
|
+
to: targetTbl,
|
|
350
|
+
// referenceKey = A's referenced column(s) that J's sourceKey points at.
|
|
351
|
+
referenceKey: self.targetColumns.length === 1 ? self.targetColumns[0] : self.targetColumns,
|
|
352
|
+
// foreignKey is unused for m2m correlation but kept for shape parity
|
|
353
|
+
// (mirrors the source-side reference for back-compat consumers).
|
|
354
|
+
foreignKey: self.targetColumns.length === 1 ? self.targetColumns[0] : self.targetColumns,
|
|
355
|
+
through: {
|
|
356
|
+
table: tableName,
|
|
357
|
+
sourceKey: self.sourceColumns[0], // J col → A
|
|
358
|
+
targetKey: other.sourceColumns[0], // J col → B
|
|
359
|
+
},
|
|
360
|
+
};
|
|
361
|
+
};
|
|
362
|
+
addM2M(fkA, fkB); // A → B
|
|
363
|
+
addM2M(fkB, fkA); // B → A
|
|
364
|
+
}
|
|
284
365
|
// ----- Assemble TableMetadata for each table -----
|
|
285
366
|
const tables = {};
|
|
286
367
|
for (const tableName of tableNames) {
|
package/dist/cjs/nested-write.js
CHANGED
|
@@ -144,17 +144,29 @@ async function executeNestedCreate(ctx, tableName, data, depth = 0, path = []) {
|
|
|
144
144
|
}
|
|
145
145
|
validateOps(relName, ops, false);
|
|
146
146
|
}
|
|
147
|
-
//
|
|
148
|
-
|
|
149
|
-
//
|
|
147
|
+
// belongsTo relations put the foreign key on the PARENT row, so they must be
|
|
148
|
+
// resolved BEFORE the parent is inserted — otherwise a NOT NULL FK column
|
|
149
|
+
// fails on the initial INSERT. We resolve each belongsTo op (create/connect/
|
|
150
|
+
// connectOrCreate) to its referenced row and fold the FK values into the
|
|
151
|
+
// parent's own INSERT.
|
|
152
|
+
const belongsToFks = {};
|
|
153
|
+
for (const [relName, ops] of Object.entries(relations)) {
|
|
154
|
+
const rel = tableMeta.relations[relName];
|
|
155
|
+
if (rel.type === 'belongsTo') {
|
|
156
|
+
Object.assign(belongsToFks, await resolveBelongsToForCreate(ctx, rel, ops, tableName, depth, path, relName));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// Insert the parent row (scalars + resolved belongsTo foreign keys)
|
|
160
|
+
const parentRow = (await ctx.tx.table(tableName).create({
|
|
161
|
+
data: { ...scalars, ...belongsToFks },
|
|
162
|
+
}));
|
|
163
|
+
// Process hasMany / hasOne relations — their FK lives on the CHILD, so they
|
|
164
|
+
// need the parent row to exist first.
|
|
150
165
|
for (const [relName, ops] of Object.entries(relations)) {
|
|
151
166
|
const rel = tableMeta.relations[relName];
|
|
152
167
|
if (rel.type === 'hasMany' || rel.type === 'hasOne') {
|
|
153
168
|
await processHasManyCreate(ctx, rel, ops, parentRow, depth, path, relName);
|
|
154
169
|
}
|
|
155
|
-
else if (rel.type === 'belongsTo') {
|
|
156
|
-
await processBelongsToCreate(ctx, rel, ops, parentRow, tableName, depth, path, relName);
|
|
157
|
-
}
|
|
158
170
|
}
|
|
159
171
|
// Build the `with` clause for the final read to return the full tree
|
|
160
172
|
const withClause = {};
|
|
@@ -319,6 +331,58 @@ async function processHasManyCreate(ctx, rel, ops, parentRow, depth, path, relNa
|
|
|
319
331
|
// ---------------------------------------------------------------------------
|
|
320
332
|
// belongsTo create operations
|
|
321
333
|
// ---------------------------------------------------------------------------
|
|
334
|
+
/**
|
|
335
|
+
* Resolve a belongsTo relation's create/connect/connectOrCreate op to the
|
|
336
|
+
* foreign-key value(s) that belong on the PARENT row, returning them keyed by
|
|
337
|
+
* the parent's own field names so they can be merged into the parent INSERT.
|
|
338
|
+
*
|
|
339
|
+
* Used by the create path only. (The update path uses processBelongsToCreate,
|
|
340
|
+
* which UPDATEs the FK after the parent already exists.)
|
|
341
|
+
*/
|
|
342
|
+
async function resolveBelongsToForCreate(ctx, rel, ops, parentTable, depth, path, relName) {
|
|
343
|
+
const fks = (0, schema_js_1.normalizeKeyColumns)(rel.foreignKey);
|
|
344
|
+
const refs = (0, schema_js_1.normalizeKeyColumns)(rel.referenceKey);
|
|
345
|
+
const parentMeta = ctx.schema.tables[parentTable];
|
|
346
|
+
const relatedTable = ctx.schema.tables[rel.to];
|
|
347
|
+
let relatedRow = null;
|
|
348
|
+
if (ops.create !== undefined) {
|
|
349
|
+
const items = toArray(ops.create);
|
|
350
|
+
if (items.length > 0) {
|
|
351
|
+
relatedRow = (await executeNestedCreate(ctx, rel.to, items[0], depth + 1, [...path, relName]));
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
else if (ops.connect !== undefined) {
|
|
355
|
+
const items = toArray(ops.connect);
|
|
356
|
+
if (items.length > 0) {
|
|
357
|
+
const target = items[0];
|
|
358
|
+
relatedRow = (await ctx.tx.table(rel.to).findUnique({ where: target }));
|
|
359
|
+
if (!relatedRow) {
|
|
360
|
+
throw new errors_js_1.ValidationError(`[turbine] connect on "${relName}": no ${rel.to} row found matching ${JSON.stringify(target)}.`);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
else if (ops.connectOrCreate !== undefined) {
|
|
365
|
+
const items = toArray(ops.connectOrCreate);
|
|
366
|
+
if (items.length > 0) {
|
|
367
|
+
const op = items[0];
|
|
368
|
+
relatedRow = (await ctx.tx.table(rel.to).findUnique({ where: op.where }));
|
|
369
|
+
if (!relatedRow) {
|
|
370
|
+
// For belongsTo the FK lives on the parent, so the related row is
|
|
371
|
+
// created plainly (no FK injection) and we read its reference key.
|
|
372
|
+
relatedRow = (await ctx.tx.table(rel.to).create({ data: op.create }));
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
const fkScalars = {};
|
|
377
|
+
if (relatedRow) {
|
|
378
|
+
for (let i = 0; i < fks.length; i++) {
|
|
379
|
+
const fkField = parentMeta.reverseColumnMap[fks[i]] ?? fks[i];
|
|
380
|
+
const refField = relatedTable?.reverseColumnMap[refs[i]] ?? refs[i];
|
|
381
|
+
fkScalars[fkField] = relatedRow[refField];
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return fkScalars;
|
|
385
|
+
}
|
|
322
386
|
async function processBelongsToCreate(ctx, rel, ops, parentRow, parentTable, depth, path, relName) {
|
|
323
387
|
const fks = (0, schema_js_1.normalizeKeyColumns)(rel.foreignKey);
|
|
324
388
|
const refs = (0, schema_js_1.normalizeKeyColumns)(rel.referenceKey);
|