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.
Files changed (41) hide show
  1. package/README.md +180 -12
  2. package/dist/adapters/cockroachdb.js +4 -2
  3. package/dist/adapters/index.js +4 -1
  4. package/dist/adapters/yugabytedb.js +4 -2
  5. package/dist/cjs/adapters/cockroachdb.js +4 -2
  6. package/dist/cjs/adapters/index.js +4 -1
  7. package/dist/cjs/adapters/yugabytedb.js +4 -2
  8. package/dist/cjs/cli/studio.js +5 -1
  9. package/dist/cjs/client.js +164 -0
  10. package/dist/cjs/errors.js +35 -5
  11. package/dist/cjs/generate.js +14 -3
  12. package/dist/cjs/index.js +10 -2
  13. package/dist/cjs/introspect.js +81 -0
  14. package/dist/cjs/nested-write.js +70 -6
  15. package/dist/cjs/query/builder.js +538 -12
  16. package/dist/cjs/realtime.js +147 -0
  17. package/dist/cjs/schema-builder.js +86 -0
  18. package/dist/cjs/schema.js +10 -0
  19. package/dist/cjs/typed-sql.js +149 -0
  20. package/dist/cli/studio.js +5 -1
  21. package/dist/client.d.ts +120 -0
  22. package/dist/client.js +165 -1
  23. package/dist/errors.js +35 -5
  24. package/dist/generate.js +14 -3
  25. package/dist/index.d.ts +4 -2
  26. package/dist/index.js +5 -1
  27. package/dist/introspect.js +81 -0
  28. package/dist/nested-write.js +70 -6
  29. package/dist/query/builder.d.ts +104 -1
  30. package/dist/query/builder.js +539 -13
  31. package/dist/query/index.d.ts +1 -1
  32. package/dist/query/types.d.ts +126 -2
  33. package/dist/realtime.d.ts +71 -0
  34. package/dist/realtime.js +144 -0
  35. package/dist/schema-builder.d.ts +68 -1
  36. package/dist/schema-builder.js +85 -0
  37. package/dist/schema.d.ts +18 -1
  38. package/dist/schema.js +10 -0
  39. package/dist/typed-sql.d.ts +101 -0
  40. package/dist/typed-sql.js +145 -0
  41. package/package.json +17 -15
@@ -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
- const detail = detailFromCause(cause);
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
- const detail = detailFromCause(cause);
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
- const detail = detailFromCause(cause);
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
- const detail = detailFromCause(cause);
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
- const detail = detailFromCause(cause);
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
  }
@@ -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
- const cardinality = rel.type === 'hasMany' ? "'many'" : "'one'";
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
- lines.push(` ${relName}: { type: '${escSQ(rel.type)}', name: '${escSQ(rel.name)}', from: '${escSQ(rel.from)}', to: '${escSQ(rel.to)}', foreignKey: ${fkLiteral}, referenceKey: ${refLiteral} },`);
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.table = exports.defineSchema = exports.column = exports.ColumnBuilder = exports.snakeToPascal = exports.snakeToCamel = exports.singularize = exports.pgTypeToTs = exports.pgArrayType = exports.normalizeKeyColumns = exports.isDateType = exports.camelToSnake = 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.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; } });
@@ -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) {
@@ -144,17 +144,29 @@ async function executeNestedCreate(ctx, tableName, data, depth = 0, path = []) {
144
144
  }
145
145
  validateOps(relName, ops, false);
146
146
  }
147
- // Insert the parent row
148
- const parentRow = (await ctx.tx.table(tableName).create({ data: scalars }));
149
- // Process each relation
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);