@casekit/orm2 1.0.1 → 1.0.2

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.
@@ -131,29 +131,83 @@ export const buildFind = (config, middleware, modelName, query, lateralBy = [],
131
131
  throw new Error(`Unexpected relation type: ${relation.type}. Should be N:1`);
132
132
  }
133
133
  const joinBuilder = buildFind(config, middleware, relation.model, subquery, [], [...path, r], builder.tableIndex);
134
- builder.joins.push({
135
- relation: r,
136
- path: [...path, r],
137
- type: relation.optional && !subquery.where ? "LEFT" : "INNER",
138
- table: joinBuilder.table,
139
- where: joinBuilder.where,
140
- orderBy: joinBuilder.orderBy,
141
- columns: relation.from.columns.map((fk, i) => ({
142
- from: {
143
- table: builder.table.alias,
144
- name: fk,
145
- },
146
- to: {
147
- table: joinBuilder.table.alias,
148
- name: relation.to.columns[i],
134
+ const joinType = relation.optional && !subquery.where ? "LEFT" : "INNER";
135
+ const needsSubquery = joinType === "LEFT" && joinBuilder.joins.length > 0;
136
+ if (needsSubquery) {
137
+ // When we have a LEFT JOIN with nested joins, we need to wrap
138
+ // everything in a subquery to preserve LEFT JOIN semantics.
139
+ // Otherwise, INNER JOINs in the nested relations would filter
140
+ // out rows where the optional relation is NULL.
141
+ const subqueryAlias = `${joinBuilder.table.alias}_subq`;
142
+ // The join columns need to reference the subquery output columns
143
+ // by their aliases rather than the original table
144
+ const joinColumns = relation.from.columns.map((fk, i) => {
145
+ const toColumnName = relation.to.columns[i];
146
+ // Find the column in the subquery that matches the join key
147
+ const toColumn = joinBuilder.columns.find(col => col.table === joinBuilder.table.alias &&
148
+ col.name === toColumnName);
149
+ if (!toColumn) {
150
+ throw new Error(`Join key column ${toColumnName} not found in select for relation ${r}`);
151
+ }
152
+ return {
153
+ from: {
154
+ table: builder.table.alias,
155
+ name: fk,
156
+ },
157
+ to: {
158
+ table: subqueryAlias,
159
+ name: toColumn.alias, // Use the column alias from subquery
160
+ },
161
+ };
162
+ });
163
+ builder.joins.push({
164
+ relation: r,
165
+ path: [...path, r],
166
+ type: joinType,
167
+ table: joinBuilder.table,
168
+ where: joinBuilder.where,
169
+ orderBy: joinBuilder.orderBy,
170
+ columns: joinColumns,
171
+ subquery: {
172
+ alias: subqueryAlias,
173
+ joins: joinBuilder.joins,
174
+ columns: joinBuilder.columns,
149
175
  },
150
- })),
151
- });
176
+ });
177
+ // Add columns but update their table reference to the subquery alias
178
+ // and use the column's alias as the name since that's what the subquery outputs
179
+ builder.columns.push(...joinBuilder.columns.map(col => ({
180
+ ...col,
181
+ table: subqueryAlias,
182
+ name: col.alias, // The subquery outputs columns with their aliases
183
+ })));
184
+ }
185
+ else {
186
+ // Original flattening logic for INNER joins or LEFT joins without nesting
187
+ builder.joins.push({
188
+ relation: r,
189
+ path: [...path, r],
190
+ type: joinType,
191
+ table: joinBuilder.table,
192
+ where: joinBuilder.where,
193
+ orderBy: joinBuilder.orderBy,
194
+ columns: relation.from.columns.map((fk, i) => ({
195
+ from: {
196
+ table: builder.table.alias,
197
+ name: fk,
198
+ },
199
+ to: {
200
+ table: joinBuilder.table.alias,
201
+ name: relation.to.columns[i],
202
+ },
203
+ })),
204
+ });
205
+ builder.columns.push(...joinBuilder.columns);
206
+ builder.joins.push(...joinBuilder.joins);
207
+ }
152
208
  // update the parent builder's tableIndex with the one from the
153
209
  // joinBuilder, so that we don't have overlapping table indices
154
210
  builder.tableIndex = joinBuilder.tableIndex;
155
- builder.columns.push(...joinBuilder.columns);
156
- builder.joins.push(...joinBuilder.joins);
157
211
  // this is admittedly a bit weird,
158
212
  // we wouldn't expect N:1 relations to specify
159
213
  // ordering, skipping, and limiting, and typescript
@@ -260,4 +260,33 @@ describe("buildFind", () => {
260
260
  },
261
261
  ]);
262
262
  });
263
+ test("uses subquery for nested relations when parent relation is optional", () => {
264
+ const { db } = createTestDB();
265
+ const result = buildFind(db.config, [], "task", {
266
+ select: ["id", "title"],
267
+ include: {
268
+ assignee: {
269
+ select: ["name"],
270
+ include: {
271
+ department: {
272
+ select: ["name"],
273
+ },
274
+ },
275
+ },
276
+ },
277
+ });
278
+ // The task.assignee relation is optional, so it should LEFT JOIN
279
+ expect(result.joins[0]?.type).toBe("LEFT");
280
+ // Because assignee is optional and has nested joins, it should
281
+ // use a subquery to preserve LEFT JOIN semantics
282
+ expect(result.joins[0]?.subquery).toBeDefined();
283
+ expect(result.joins[0]?.subquery?.alias).toBe("b_subq");
284
+ // The nested join (employee.department) should be inside the subquery
285
+ expect(result.joins[0]?.subquery?.joins).toHaveLength(1);
286
+ expect(result.joins[0]?.subquery?.joins[0]?.relation).toBe("department");
287
+ expect(result.joins[0]?.subquery?.joins[0]?.type).toBe("INNER");
288
+ // The columns should reference the subquery alias
289
+ const assigneeColumns = result.columns.filter((col) => col.path[0] === "assignee");
290
+ expect(assigneeColumns.every((col) => col.table === "b_subq")).toBe(true);
291
+ });
263
292
  });
@@ -67,6 +67,11 @@ export type Join = {
67
67
  where?: SQLStatement | null;
68
68
  orderBy?: OrderBy[];
69
69
  path: string[];
70
+ subquery?: {
71
+ alias: string;
72
+ joins: Join[];
73
+ columns: SelectedColumn[];
74
+ };
70
75
  };
71
76
  export type LateralJoin = {
72
77
  outerAlias: string;
@@ -76,7 +76,7 @@ export class Connection {
76
76
  }
77
77
  }
78
78
  catch (e) {
79
- this.config.logger.error("Error running query");
79
+ this.config.logger.error(`Error running query: ${sql.pretty}`);
80
80
  throw e;
81
81
  }
82
82
  }
@@ -406,4 +406,132 @@ describe("findToSql", () => {
406
406
  ) "b" ON TRUE
407
407
  `);
408
408
  });
409
+ test("generates query with subquery join for optional relations with nested joins", () => {
410
+ const statement = findToSql({
411
+ table: {
412
+ schema: "public",
413
+ name: "tasks",
414
+ alias: "a",
415
+ model: "task",
416
+ },
417
+ columns: [
418
+ { table: "a", name: "id", alias: "a_0", path: ["id"] },
419
+ { table: "a", name: "title", alias: "a_1", path: ["title"] },
420
+ {
421
+ table: "b_subq",
422
+ name: "b_0",
423
+ alias: "b_0",
424
+ path: ["assignee", "name"],
425
+ },
426
+ {
427
+ table: "b_subq",
428
+ name: "b_1",
429
+ alias: "b_1",
430
+ path: ["assignee", "id"],
431
+ },
432
+ {
433
+ table: "b_subq",
434
+ name: "c_0",
435
+ alias: "c_0",
436
+ path: ["assignee", "department", "name"],
437
+ },
438
+ {
439
+ table: "b_subq",
440
+ name: "c_1",
441
+ alias: "c_1",
442
+ path: ["assignee", "department", "id"],
443
+ },
444
+ ],
445
+ joins: [
446
+ {
447
+ type: "LEFT",
448
+ relation: "assignee",
449
+ path: ["assignee"],
450
+ table: {
451
+ schema: "public",
452
+ name: "employees",
453
+ alias: "b",
454
+ model: "employee",
455
+ },
456
+ columns: [
457
+ {
458
+ from: { table: "a", name: "assignee_id" },
459
+ to: { table: "b_subq", name: "b_1" },
460
+ },
461
+ ],
462
+ subquery: {
463
+ alias: "b_subq",
464
+ columns: [
465
+ {
466
+ table: "b",
467
+ name: "name",
468
+ alias: "b_0",
469
+ path: ["assignee", "name"],
470
+ },
471
+ {
472
+ table: "b",
473
+ name: "id",
474
+ alias: "b_1",
475
+ path: ["assignee", "id"],
476
+ },
477
+ {
478
+ table: "c",
479
+ name: "name",
480
+ alias: "c_0",
481
+ path: ["assignee", "department", "name"],
482
+ },
483
+ {
484
+ table: "c",
485
+ name: "id",
486
+ alias: "c_1",
487
+ path: ["assignee", "department", "id"],
488
+ },
489
+ ],
490
+ joins: [
491
+ {
492
+ type: "INNER",
493
+ relation: "department",
494
+ path: ["assignee", "department"],
495
+ table: {
496
+ schema: "public",
497
+ name: "departments",
498
+ alias: "c",
499
+ model: "department",
500
+ },
501
+ columns: [
502
+ {
503
+ from: { table: "b", name: "department_id" },
504
+ to: { table: "c", name: "id" },
505
+ },
506
+ ],
507
+ },
508
+ ],
509
+ },
510
+ },
511
+ ],
512
+ orderBy: [],
513
+ tableIndex: 0,
514
+ });
515
+ expect(statement.pretty).toBe(unindent `
516
+ SELECT
517
+ "a"."id" AS "a_0",
518
+ "a"."title" AS "a_1",
519
+ "b_subq"."b_0" AS "b_0",
520
+ "b_subq"."b_1" AS "b_1",
521
+ "b_subq"."c_0" AS "c_0",
522
+ "b_subq"."c_1" AS "c_1"
523
+ FROM
524
+ "public"."tasks" AS "a"
525
+ LEFT JOIN (
526
+ SELECT
527
+ "b"."name" AS "b_0",
528
+ "b"."id" AS "b_1",
529
+ "c"."name" AS "c_0",
530
+ "c"."id" AS "c_1"
531
+ FROM
532
+ "public"."employees" AS "b"
533
+ JOIN "public"."departments" AS "c" ON "b"."department_id" = "c"."id"
534
+ ) AS "b_subq" ON "a"."assignee_id" = "b_subq"."b_1"
535
+ `);
536
+ });
409
537
  });
@@ -6,5 +6,5 @@ export declare const selectColumn: ({ table, name, alias }: SelectedColumn) => S
6
6
  export declare const returnedColumn: ({ name, alias }: ReturnedColumn) => SQLStatement<import("pg").QueryResultRow>;
7
7
  export declare const unnestPk: (pk: LateralJoin["primaryKeys"][number]) => SQLStatement<import("pg").QueryResultRow>;
8
8
  export declare const setClause: ([column, value]: [string, unknown]) => SQLStatement<import("pg").QueryResultRow>;
9
- export declare const joinClause: (join: Join) => SQLStatement<import("pg").QueryResultRow>;
9
+ export declare const joinClause: (join: Join) => SQLStatement;
10
10
  export declare const orderByColumn: (orderByColumn: OrderBy) => SQLStatement<import("pg").QueryResultRow>;
package/build/sql/util.js CHANGED
@@ -18,12 +18,29 @@ export const setClause = ([column, value]) => {
18
18
  return sql `${sql.ident(column)} = ${sql.value(value)}`;
19
19
  };
20
20
  export const joinClause = (join) => {
21
- const pkClauses = join.columns.map(({ from, to }) => sql `${columnName(from)} = ${columnName(to)}`);
22
- const clauses = join.where ? [...pkClauses, join.where] : pkClauses;
23
- return sql `
24
- ${join.type === "LEFT" ? sql `LEFT JOIN` : sql `JOIN`}
25
- ${tableName(join.table)} ON ${sql.join(clauses, " AND ")}
26
- `;
21
+ if (join.subquery) {
22
+ // Build subquery with nested joins
23
+ const subquerySql = sql `
24
+ SELECT ${sql.join(join.subquery.columns.map(selectColumn))}
25
+ FROM ${tableName(join.table)}
26
+ ${sql.join(join.subquery.joins.map(joinClause), "\n")}
27
+ ${join.where ? sql `WHERE ${join.where}` : sql ``}
28
+ `;
29
+ const pkClauses = join.columns.map(({ from, to }) => sql `${columnName(from)} = ${sql.ident(to.table)}.${sql.ident(to.name)}`);
30
+ return sql `
31
+ ${join.type === "LEFT" ? sql `LEFT JOIN` : sql `JOIN`}
32
+ (${subquerySql}) AS ${sql.ident(join.subquery.alias)}
33
+ ON ${sql.join(pkClauses, " AND ")}
34
+ `;
35
+ }
36
+ else {
37
+ const pkClauses = join.columns.map(({ from, to }) => sql `${columnName(from)} = ${columnName(to)}`);
38
+ const clauses = join.where ? [...pkClauses, join.where] : pkClauses;
39
+ return sql `
40
+ ${join.type === "LEFT" ? sql `LEFT JOIN` : sql `JOIN`}
41
+ ${tableName(join.table)} ON ${sql.join(clauses, " AND ")}
42
+ `;
43
+ }
27
44
  };
28
45
  export const orderByColumn = (orderByColumn) => {
29
46
  return sql `
@@ -203,7 +203,7 @@ describe("Transaction", () => {
203
203
  test("rolling back on error", async () => {
204
204
  await transaction.query(sql `INSERT INTO transaction_test_table VALUES (2)`);
205
205
  await expect(transaction.query(sql `SELECT * FROM non_existent_table`)).rejects.toThrowError('relation "non_existent_table" does not exist');
206
- expect(logger.logs.error[0]?.message).toEqual("Error running query");
206
+ expect(logger.logs.error[0]?.message).toContain("Error running query");
207
207
  expect(logger.logs.error[1]?.message).toEqual("Rolling back transaction due to error");
208
208
  const result = await connection.query(sql `SELECT * FROM transaction_test_table`);
209
209
  expect(result.rows.length).toBe(0);
@@ -285,7 +285,7 @@ describe("Transaction", () => {
285
285
  test("rolling back on error", async () => {
286
286
  await transaction.query(sql `INSERT INTO transaction_test_table VALUES (2)`);
287
287
  await expect(transaction.query(sql `SELECT * FROM non_existent_table`)).rejects.toThrowError('relation "non_existent_table" does not exist');
288
- expect(logger.logs.error[0]?.message).toEqual("Error running query");
288
+ expect(logger.logs.error[0]?.message).toContain("Error running query");
289
289
  const result = await connection.query(sql `SELECT * FROM transaction_test_table`);
290
290
  expect(result.rows.length).toBe(0);
291
291
  });
@@ -293,7 +293,7 @@ describe("Transaction", () => {
293
293
  const nestedTx = await transaction.startTransaction();
294
294
  await nestedTx.query(sql `INSERT INTO transaction_test_table VALUES (2)`);
295
295
  await expect(nestedTx.query(sql `SELECT * FROM non_existent_table`)).rejects.toThrowError('relation "non_existent_table" does not exist');
296
- expect(logger.logs.error[0]?.message).toEqual("Error running query");
296
+ expect(logger.logs.error[0]?.message).toContain("Error running query");
297
297
  expect(logger.logs.error[1]?.message).toEqual("Rolling back transaction due to error");
298
298
  await transaction.query(sql `INSERT INTO transaction_test_table VALUES (7)`);
299
299
  const result = await transaction.query(sql `SELECT * FROM transaction_test_table`);
@@ -277,4 +277,241 @@ describe("findOne: N:1 relations", () => {
277
277
  });
278
278
  }, { rollback: true });
279
279
  });
280
+ test("handles optional N:1 relation with nested non-optional relation using subquery", async () => {
281
+ await db.transact(async (db) => {
282
+ // Create departments
283
+ await db.createOne("department", {
284
+ values: factory.department({
285
+ id: 1,
286
+ name: "Engineering",
287
+ }),
288
+ });
289
+ await db.createOne("department", {
290
+ values: factory.department({
291
+ id: 2,
292
+ name: "Sales",
293
+ }),
294
+ });
295
+ // Create employees
296
+ await db.createOne("employee", {
297
+ values: factory.employee({
298
+ id: 1,
299
+ name: "Alice",
300
+ departmentId: 1,
301
+ }),
302
+ });
303
+ await db.createOne("employee", {
304
+ values: factory.employee({
305
+ id: 2,
306
+ name: "Bob",
307
+ departmentId: 2,
308
+ }),
309
+ });
310
+ // Create tasks - some with assignees, some without
311
+ await db.createMany("task", {
312
+ values: [
313
+ factory.task({
314
+ id: 1,
315
+ title: "Task with Alice",
316
+ assigneeId: 1,
317
+ }),
318
+ factory.task({
319
+ id: 2,
320
+ title: "Unassigned task",
321
+ assigneeId: null,
322
+ }),
323
+ factory.task({
324
+ id: 3,
325
+ title: "Task with Bob",
326
+ assigneeId: 2,
327
+ }),
328
+ ],
329
+ });
330
+ // Test 1: Task with assignee and department
331
+ const taskWithAssignee = await db.findOne("task", {
332
+ select: ["id", "title"],
333
+ where: { id: 1 },
334
+ include: {
335
+ assignee: {
336
+ select: ["name"],
337
+ include: {
338
+ department: {
339
+ select: ["name"],
340
+ },
341
+ },
342
+ },
343
+ },
344
+ });
345
+ expect(taskWithAssignee).toEqual({
346
+ id: 1,
347
+ title: "Task with Alice",
348
+ assignee: {
349
+ name: "Alice",
350
+ department: {
351
+ name: "Engineering",
352
+ },
353
+ },
354
+ });
355
+ // Test 2: Task without assignee should return with null assignee
356
+ // This is the critical test - the LEFT JOIN with nested INNER JOIN
357
+ // should be wrapped in a subquery to preserve the NULL behavior
358
+ const unassignedTask = await db.findOne("task", {
359
+ select: ["id", "title"],
360
+ where: { id: 2 },
361
+ include: {
362
+ assignee: {
363
+ select: ["name"],
364
+ include: {
365
+ department: {
366
+ select: ["name"],
367
+ },
368
+ },
369
+ },
370
+ },
371
+ });
372
+ expect(unassignedTask).toEqual({
373
+ id: 2,
374
+ title: "Unassigned task",
375
+ assignee: null,
376
+ });
377
+ // Test 3: Verify Bob's task also works correctly
378
+ const bobsTask = await db.findOne("task", {
379
+ select: ["id", "title"],
380
+ where: { id: 3 },
381
+ include: {
382
+ assignee: {
383
+ select: ["name"],
384
+ include: {
385
+ department: {
386
+ select: ["name"],
387
+ },
388
+ },
389
+ },
390
+ },
391
+ });
392
+ expect(bobsTask).toEqual({
393
+ id: 3,
394
+ title: "Task with Bob",
395
+ assignee: {
396
+ name: "Bob",
397
+ department: {
398
+ name: "Sales",
399
+ },
400
+ },
401
+ });
402
+ }, { rollback: true });
403
+ });
404
+ test("handles where clauses on nested relations of optional N:1 relations", async () => {
405
+ await db.transact(async (db) => {
406
+ // Create departments
407
+ const engineering = await db.createOne("department", {
408
+ values: factory.department({
409
+ id: 1,
410
+ name: "Engineering",
411
+ }),
412
+ returning: ["id", "name"],
413
+ });
414
+ const sales = await db.createOne("department", {
415
+ values: factory.department({
416
+ id: 2,
417
+ name: "Sales",
418
+ }),
419
+ returning: ["id", "name"],
420
+ });
421
+ // Create employees in different departments
422
+ const alice = await db.createOne("employee", {
423
+ values: factory.employee({
424
+ id: 1,
425
+ name: "Alice",
426
+ departmentId: 1,
427
+ }),
428
+ returning: ["id", "name"],
429
+ });
430
+ const bob = await db.createOne("employee", {
431
+ values: factory.employee({
432
+ id: 2,
433
+ name: "Bob",
434
+ departmentId: 2,
435
+ }),
436
+ returning: ["id", "name"],
437
+ });
438
+ // Create tasks
439
+ const [task1, task2, task3] = await db.createMany("task", {
440
+ values: [
441
+ factory.task({
442
+ id: 1,
443
+ title: "Engineering task",
444
+ assigneeId: alice.id,
445
+ }),
446
+ factory.task({
447
+ id: 2,
448
+ title: "Sales task",
449
+ assigneeId: bob.id,
450
+ }),
451
+ factory.task({
452
+ id: 3,
453
+ title: "Unassigned task",
454
+ assigneeId: null,
455
+ }),
456
+ ],
457
+ returning: ["id", "title"],
458
+ });
459
+ // Test 1: optional N:1 relation (assignee) includes non-optional department
460
+ const engineeringTasks = await db.findMany("task", {
461
+ select: ["id", "title"],
462
+ include: {
463
+ assignee: {
464
+ select: ["id", "name"],
465
+ include: {
466
+ department: {
467
+ select: ["id", "name"],
468
+ where: { name: "Engineering" },
469
+ },
470
+ },
471
+ },
472
+ },
473
+ orderBy: ["id"],
474
+ });
475
+ // All tasks are returned (because assignee is optional)
476
+ // Only assignees in Engineering dept are returned
477
+ // Task 3 is filtered out (no assignee)
478
+ expect(engineeringTasks).toEqual([
479
+ {
480
+ ...task1,
481
+ assignee: {
482
+ ...alice,
483
+ department: { ...engineering },
484
+ },
485
+ },
486
+ { ...task2, assignee: null },
487
+ { ...task3, assignee: null },
488
+ ]);
489
+ // Test 2: Where clause on the assignee itself (not nested department)
490
+ const aliceTasks = await db.findMany("task", {
491
+ select: ["id", "title"],
492
+ include: {
493
+ assignee: {
494
+ select: ["id", "name"],
495
+ where: { name: "Alice" },
496
+ include: {
497
+ department: {
498
+ select: ["id", "name"],
499
+ },
500
+ },
501
+ },
502
+ },
503
+ orderBy: ["id"],
504
+ });
505
+ // Only task 1 should be returned (assigned to Alice)
506
+ expect(aliceTasks).toEqual([
507
+ {
508
+ ...task1,
509
+ assignee: {
510
+ ...alice,
511
+ department: { ...engineering },
512
+ },
513
+ },
514
+ ]);
515
+ }, { rollback: true });
516
+ });
280
517
  });
@@ -522,6 +522,86 @@ export declare const createTestDB: (overrides?: Partial<Omit<Config, "models">>)
522
522
  };
523
523
  };
524
524
  };
525
+ department: {
526
+ readonly fields: {
527
+ readonly id: {
528
+ readonly type: "serial";
529
+ readonly primaryKey: true;
530
+ };
531
+ readonly name: {
532
+ readonly type: "text";
533
+ };
534
+ };
535
+ readonly relations: {
536
+ readonly employees: {
537
+ readonly type: "1:N";
538
+ readonly model: "employee";
539
+ readonly fromField: "id";
540
+ readonly toField: "departmentId";
541
+ };
542
+ };
543
+ };
544
+ employee: {
545
+ readonly fields: {
546
+ readonly id: {
547
+ readonly type: "serial";
548
+ readonly primaryKey: true;
549
+ };
550
+ readonly name: {
551
+ readonly type: "text";
552
+ };
553
+ readonly departmentId: {
554
+ readonly type: "integer";
555
+ readonly references: {
556
+ readonly model: "department";
557
+ readonly field: "id";
558
+ };
559
+ };
560
+ };
561
+ readonly relations: {
562
+ readonly department: {
563
+ readonly type: "N:1";
564
+ readonly model: "department";
565
+ readonly fromField: "departmentId";
566
+ readonly toField: "id";
567
+ };
568
+ readonly tasks: {
569
+ readonly type: "1:N";
570
+ readonly model: "task";
571
+ readonly fromField: "id";
572
+ readonly toField: "assigneeId";
573
+ };
574
+ };
575
+ };
576
+ task: {
577
+ readonly fields: {
578
+ readonly id: {
579
+ readonly type: "serial";
580
+ readonly primaryKey: true;
581
+ };
582
+ readonly title: {
583
+ readonly type: "text";
584
+ };
585
+ readonly assigneeId: {
586
+ readonly type: "integer";
587
+ readonly nullable: true;
588
+ readonly references: {
589
+ readonly model: "employee";
590
+ readonly field: "id";
591
+ readonly onDelete: "SET NULL";
592
+ };
593
+ };
594
+ };
595
+ readonly relations: {
596
+ readonly assignee: {
597
+ readonly type: "N:1";
598
+ readonly model: "employee";
599
+ readonly fromField: "assigneeId";
600
+ readonly toField: "id";
601
+ readonly optional: true;
602
+ };
603
+ };
604
+ };
525
605
  };
526
606
  }, {
527
607
  audit: {
@@ -1034,6 +1114,86 @@ export declare const createTestDB: (overrides?: Partial<Omit<Config, "models">>)
1034
1114
  };
1035
1115
  };
1036
1116
  };
1117
+ department: {
1118
+ readonly fields: {
1119
+ readonly id: {
1120
+ readonly type: "serial";
1121
+ readonly primaryKey: true;
1122
+ };
1123
+ readonly name: {
1124
+ readonly type: "text";
1125
+ };
1126
+ };
1127
+ readonly relations: {
1128
+ readonly employees: {
1129
+ readonly type: "1:N";
1130
+ readonly model: "employee";
1131
+ readonly fromField: "id";
1132
+ readonly toField: "departmentId";
1133
+ };
1134
+ };
1135
+ };
1136
+ employee: {
1137
+ readonly fields: {
1138
+ readonly id: {
1139
+ readonly type: "serial";
1140
+ readonly primaryKey: true;
1141
+ };
1142
+ readonly name: {
1143
+ readonly type: "text";
1144
+ };
1145
+ readonly departmentId: {
1146
+ readonly type: "integer";
1147
+ readonly references: {
1148
+ readonly model: "department";
1149
+ readonly field: "id";
1150
+ };
1151
+ };
1152
+ };
1153
+ readonly relations: {
1154
+ readonly department: {
1155
+ readonly type: "N:1";
1156
+ readonly model: "department";
1157
+ readonly fromField: "departmentId";
1158
+ readonly toField: "id";
1159
+ };
1160
+ readonly tasks: {
1161
+ readonly type: "1:N";
1162
+ readonly model: "task";
1163
+ readonly fromField: "id";
1164
+ readonly toField: "assigneeId";
1165
+ };
1166
+ };
1167
+ };
1168
+ task: {
1169
+ readonly fields: {
1170
+ readonly id: {
1171
+ readonly type: "serial";
1172
+ readonly primaryKey: true;
1173
+ };
1174
+ readonly title: {
1175
+ readonly type: "text";
1176
+ };
1177
+ readonly assigneeId: {
1178
+ readonly type: "integer";
1179
+ readonly nullable: true;
1180
+ readonly references: {
1181
+ readonly model: "employee";
1182
+ readonly field: "id";
1183
+ readonly onDelete: "SET NULL";
1184
+ };
1185
+ };
1186
+ };
1187
+ readonly relations: {
1188
+ readonly assignee: {
1189
+ readonly type: "N:1";
1190
+ readonly model: "employee";
1191
+ readonly fromField: "assigneeId";
1192
+ readonly toField: "id";
1193
+ readonly optional: true;
1194
+ };
1195
+ };
1196
+ };
1037
1197
  }, {
1038
1198
  where: never;
1039
1199
  }>;
@@ -1549,5 +1709,85 @@ export declare const createTestDB: (overrides?: Partial<Omit<Config, "models">>)
1549
1709
  };
1550
1710
  };
1551
1711
  };
1712
+ department: {
1713
+ readonly fields: {
1714
+ readonly id: {
1715
+ readonly type: "serial";
1716
+ readonly primaryKey: true;
1717
+ };
1718
+ readonly name: {
1719
+ readonly type: "text";
1720
+ };
1721
+ };
1722
+ readonly relations: {
1723
+ readonly employees: {
1724
+ readonly type: "1:N";
1725
+ readonly model: "employee";
1726
+ readonly fromField: "id";
1727
+ readonly toField: "departmentId";
1728
+ };
1729
+ };
1730
+ };
1731
+ employee: {
1732
+ readonly fields: {
1733
+ readonly id: {
1734
+ readonly type: "serial";
1735
+ readonly primaryKey: true;
1736
+ };
1737
+ readonly name: {
1738
+ readonly type: "text";
1739
+ };
1740
+ readonly departmentId: {
1741
+ readonly type: "integer";
1742
+ readonly references: {
1743
+ readonly model: "department";
1744
+ readonly field: "id";
1745
+ };
1746
+ };
1747
+ };
1748
+ readonly relations: {
1749
+ readonly department: {
1750
+ readonly type: "N:1";
1751
+ readonly model: "department";
1752
+ readonly fromField: "departmentId";
1753
+ readonly toField: "id";
1754
+ };
1755
+ readonly tasks: {
1756
+ readonly type: "1:N";
1757
+ readonly model: "task";
1758
+ readonly fromField: "id";
1759
+ readonly toField: "assigneeId";
1760
+ };
1761
+ };
1762
+ };
1763
+ task: {
1764
+ readonly fields: {
1765
+ readonly id: {
1766
+ readonly type: "serial";
1767
+ readonly primaryKey: true;
1768
+ };
1769
+ readonly title: {
1770
+ readonly type: "text";
1771
+ };
1772
+ readonly assigneeId: {
1773
+ readonly type: "integer";
1774
+ readonly nullable: true;
1775
+ readonly references: {
1776
+ readonly model: "employee";
1777
+ readonly field: "id";
1778
+ readonly onDelete: "SET NULL";
1779
+ };
1780
+ };
1781
+ };
1782
+ readonly relations: {
1783
+ readonly assignee: {
1784
+ readonly type: "N:1";
1785
+ readonly model: "employee";
1786
+ readonly fromField: "assigneeId";
1787
+ readonly toField: "id";
1788
+ readonly optional: true;
1789
+ };
1790
+ };
1791
+ };
1552
1792
  }>;
1553
1793
  };
package/package.json CHANGED
@@ -1,17 +1,17 @@
1
1
  {
2
2
  "name": "@casekit/orm2",
3
3
  "description": "",
4
- "version": "1.0.1",
4
+ "version": "1.0.2",
5
5
  "author": "",
6
6
  "dependencies": {
7
7
  "es-toolkit": "^1.39.3",
8
8
  "object-hash": "^3.0.0",
9
9
  "pino": "^9.7.0",
10
10
  "uuid": "^11.1.0",
11
- "@casekit/orm2-config": "1.0.1",
12
- "@casekit/orm2-schema": "1.0.1",
13
- "@casekit/sql": "1.0.1",
14
- "@casekit/toolbox": "1.0.1"
11
+ "@casekit/orm2-config": "1.0.2",
12
+ "@casekit/orm2-schema": "1.0.2",
13
+ "@casekit/toolbox": "1.0.2",
14
+ "@casekit/sql": "1.0.2"
15
15
  },
16
16
  "devDependencies": {
17
17
  "@casekit/unindent": "^1.0.5",
@@ -31,10 +31,10 @@
31
31
  "typescript-eslint": "^8.34.1",
32
32
  "vite-tsconfig-paths": "^5.1.4",
33
33
  "vitest": "^3.2.4",
34
- "@casekit/orm2-fixtures": "1.0.1",
35
- "@casekit/orm2-testing": "1.0.1",
36
- "@casekit/prettier-config": "1.0.1",
37
- "@casekit/tsconfig": "1.0.1"
34
+ "@casekit/orm2-fixtures": "1.0.2",
35
+ "@casekit/orm2-testing": "1.0.2",
36
+ "@casekit/tsconfig": "1.0.2",
37
+ "@casekit/prettier-config": "1.0.2"
38
38
  },
39
39
  "exports": {
40
40
  ".": "./build/index.js"