@casekit/orm2-config 0.0.0-20250331202540 → 1.0.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 (73) hide show
  1. package/package.json +19 -19
  2. package/{build/index.d.ts → src/index.ts} +7 -1
  3. package/{build/normalize/defaultZodSchema.test.js → src/normalize/defaultZodSchema.test.ts} +13 -5
  4. package/{build/normalize/defaultZodSchema.js → src/normalize/defaultZodSchema.ts} +22 -30
  5. package/{build/normalize/getColumns.test.js → src/normalize/getColumns.test.ts} +17 -4
  6. package/{build/normalize/getColumns.js → src/normalize/getColumns.ts} +6 -1
  7. package/{build/normalize/normalizeConfig.test.js → src/normalize/normalizeConfig.test.ts} +70 -24
  8. package/{build/normalize/normalizeConfig.js → src/normalize/normalizeConfig.ts} +7 -2
  9. package/{build/normalize/normalizeField.test.js → src/normalize/normalizeField.test.ts} +12 -7
  10. package/src/normalize/normalizeField.ts +16 -0
  11. package/{build/normalize/normalizeForeignKeys.test.js → src/normalize/normalizeForeignKeys.test.ts} +26 -7
  12. package/src/normalize/normalizeForeignKeys.ts +89 -0
  13. package/{build/normalize/normalizeModel.test.js → src/normalize/normalizeModel.test.ts} +42 -18
  14. package/{build/normalize/normalizeModel.js → src/normalize/normalizeModel.ts} +8 -1
  15. package/{build/normalize/normalizePrimaryKey.test.js → src/normalize/normalizePrimaryKey.test.ts} +31 -7
  16. package/src/normalize/normalizePrimaryKey.ts +30 -0
  17. package/{build/normalize/normalizeRelations.test.js → src/normalize/normalizeRelations.test.ts} +38 -9
  18. package/{build/normalize/normalizeRelations.js → src/normalize/normalizeRelations.ts} +34 -12
  19. package/{build/normalize/normalizeUniqueConstraints.test.js → src/normalize/normalizeUniqueConstraints.test.ts} +46 -15
  20. package/src/normalize/normalizeUniqueConstraints.ts +58 -0
  21. package/src/normalize/populateField.test.ts +253 -0
  22. package/{build/normalize/populateField.js → src/normalize/populateField.ts} +9 -1
  23. package/{build/normalize/populateModels.test.js → src/normalize/populateModels.test.ts} +35 -14
  24. package/{build/normalize/populateModels.js → src/normalize/populateModels.ts} +11 -2
  25. package/{build/types/NormalizedConfig.d.ts → src/types/NormalizedConfig.ts} +8 -1
  26. package/{build/types/NormalizedFieldDefinition.d.ts → src/types/NormalizedFieldDefinition.ts} +2 -1
  27. package/{build/types/NormalizedModelDefinition.d.ts → src/types/NormalizedModelDefinition.ts} +1 -0
  28. package/{build/types/NormalizedRelationDefinition.d.ts → src/types/NormalizedRelationDefinition.ts} +7 -1
  29. package/{build/types/NormalizedUniqueConstraintDefinition.d.ts → src/types/NormalizedUniqueConstraintDefinition.ts} +1 -0
  30. package/{build/types/PopulatedFieldDefinition.d.ts → src/types/PopulatedFieldDefinition.ts} +1 -0
  31. package/{build/types/PopulatedModelDefinition.d.ts → src/types/PopulatedModelDefinition.ts} +2 -0
  32. package/src/util.ts +38 -0
  33. package/build/index.js +0 -4
  34. package/build/normalize/defaultZodSchema.d.ts +0 -8
  35. package/build/normalize/defaultZodSchema.test.d.ts +0 -1
  36. package/build/normalize/getColumns.d.ts +0 -2
  37. package/build/normalize/getColumns.test.d.ts +0 -1
  38. package/build/normalize/normalizeConfig.d.ts +0 -3
  39. package/build/normalize/normalizeConfig.test.d.ts +0 -1
  40. package/build/normalize/normalizeField.d.ts +0 -3
  41. package/build/normalize/normalizeField.js +0 -11
  42. package/build/normalize/normalizeField.test.d.ts +0 -1
  43. package/build/normalize/normalizeForeignKeys.d.ts +0 -5
  44. package/build/normalize/normalizeForeignKeys.js +0 -50
  45. package/build/normalize/normalizeForeignKeys.test.d.ts +0 -1
  46. package/build/normalize/normalizeModel.d.ts +0 -3
  47. package/build/normalize/normalizeModel.test.d.ts +0 -1
  48. package/build/normalize/normalizePrimaryKey.d.ts +0 -3
  49. package/build/normalize/normalizePrimaryKey.js +0 -18
  50. package/build/normalize/normalizePrimaryKey.test.d.ts +0 -1
  51. package/build/normalize/normalizeRelations.d.ts +0 -3
  52. package/build/normalize/normalizeRelations.test.d.ts +0 -1
  53. package/build/normalize/normalizeUniqueConstraints.d.ts +0 -5
  54. package/build/normalize/normalizeUniqueConstraints.js +0 -29
  55. package/build/normalize/normalizeUniqueConstraints.test.d.ts +0 -1
  56. package/build/normalize/populateField.d.ts +0 -3
  57. package/build/normalize/populateField.test.d.ts +0 -1
  58. package/build/normalize/populateField.test.js +0 -198
  59. package/build/normalize/populateModels.d.ts +0 -3
  60. package/build/normalize/populateModels.test.d.ts +0 -1
  61. package/build/types/NormalizedConfig.js +0 -1
  62. package/build/types/NormalizedFieldDefinition.js +0 -1
  63. package/build/types/NormalizedForeignKeyDefinition.js +0 -1
  64. package/build/types/NormalizedModelDefinition.js +0 -1
  65. package/build/types/NormalizedPrimaryKey.js +0 -1
  66. package/build/types/NormalizedRelationDefinition.js +0 -1
  67. package/build/types/NormalizedUniqueConstraintDefinition.js +0 -1
  68. package/build/types/PopulatedFieldDefinition.js +0 -1
  69. package/build/types/PopulatedModelDefinition.js +0 -1
  70. package/build/util.d.ts +0 -6
  71. package/build/util.js +0 -21
  72. /package/{build/types/NormalizedForeignKeyDefinition.d.ts → src/types/NormalizedForeignKeyDefinition.ts} +0 -0
  73. /package/{build/types/NormalizedPrimaryKey.d.ts → src/types/NormalizedPrimaryKey.ts} +0 -0
@@ -1,7 +1,9 @@
1
1
  import { snakeCase } from "es-toolkit";
2
2
  import { describe, expect, test } from "vitest";
3
+
3
4
  import { normalizeForeignKeys } from "./normalizeForeignKeys.js";
4
5
  import { populateModels } from "./populateModels.js";
6
+
5
7
  describe("normalizeForeignKeys", () => {
6
8
  test("normalizes foreign keys defined at the top level", () => {
7
9
  const models = populateModels({
@@ -23,7 +25,8 @@ describe("normalizeForeignKeys", () => {
23
25
  },
24
26
  },
25
27
  });
26
- expect(normalizeForeignKeys(models, models["post"])).toEqual([
28
+
29
+ expect(normalizeForeignKeys(models, models["post"]!)).toEqual([
27
30
  {
28
31
  name: "post_user_id_fkey",
29
32
  fields: ["userId"],
@@ -40,6 +43,7 @@ describe("normalizeForeignKeys", () => {
40
43
  },
41
44
  ]);
42
45
  });
46
+
43
47
  test("normalizes foreign keys defined in foreignKeys array", () => {
44
48
  const models = populateModels({
45
49
  naming: { column: snakeCase },
@@ -66,7 +70,8 @@ describe("normalizeForeignKeys", () => {
66
70
  },
67
71
  },
68
72
  });
69
- expect(normalizeForeignKeys(models, models["post"])).toEqual([
73
+
74
+ expect(normalizeForeignKeys(models, models["post"]!)).toEqual([
70
75
  {
71
76
  name: "post_user_id_fkey",
72
77
  fields: ["userId"],
@@ -83,6 +88,7 @@ describe("normalizeForeignKeys", () => {
83
88
  },
84
89
  ]);
85
90
  });
91
+
86
92
  test("throws error when referenced model doesn't exist", () => {
87
93
  const models = populateModels({
88
94
  naming: { column: snakeCase },
@@ -98,8 +104,12 @@ describe("normalizeForeignKeys", () => {
98
104
  },
99
105
  },
100
106
  });
101
- expect(() => normalizeForeignKeys(models, models["post"])).toThrow('Referenced model "nonexistent" not found in models');
107
+
108
+ expect(() => normalizeForeignKeys(models, models["post"]!)).toThrow(
109
+ 'Referenced model "nonexistent" not found in models',
110
+ );
102
111
  });
112
+
103
113
  test("respects custom onDelete and onUpdate actions", () => {
104
114
  const models = populateModels({
105
115
  naming: { column: snakeCase },
@@ -125,7 +135,8 @@ describe("normalizeForeignKeys", () => {
125
135
  },
126
136
  },
127
137
  });
128
- expect(normalizeForeignKeys(models, models["post"])).toEqual([
138
+
139
+ expect(normalizeForeignKeys(models, models["post"]!)).toEqual([
129
140
  {
130
141
  name: "post_user_id_fkey",
131
142
  fields: ["userId"],
@@ -142,6 +153,7 @@ describe("normalizeForeignKeys", () => {
142
153
  },
143
154
  ]);
144
155
  });
156
+
145
157
  test("throws error on duplicate foreign keys", () => {
146
158
  const models = populateModels({
147
159
  naming: { column: snakeCase },
@@ -171,8 +183,12 @@ describe("normalizeForeignKeys", () => {
171
183
  },
172
184
  },
173
185
  });
174
- expect(() => normalizeForeignKeys(models, models["post"])).toThrow('Duplicate foreign key defined in model "post"');
186
+
187
+ expect(() => normalizeForeignKeys(models, models["post"]!)).toThrow(
188
+ 'Duplicate foreign key defined in model "post"',
189
+ );
175
190
  });
191
+
176
192
  test("handles custom foreign key names", () => {
177
193
  const models = populateModels({
178
194
  naming: { column: snakeCase },
@@ -200,7 +216,8 @@ describe("normalizeForeignKeys", () => {
200
216
  },
201
217
  },
202
218
  });
203
- expect(normalizeForeignKeys(models, models["post"])).toEqual([
219
+
220
+ expect(normalizeForeignKeys(models, models["post"]!)).toEqual([
204
221
  {
205
222
  name: "custom_fk_name",
206
223
  fields: ["userId"],
@@ -217,6 +234,7 @@ describe("normalizeForeignKeys", () => {
217
234
  },
218
235
  ]);
219
236
  });
237
+
220
238
  test("handles foreign keys in custom schema", () => {
221
239
  const models = populateModels({
222
240
  naming: { column: snakeCase },
@@ -238,7 +256,8 @@ describe("normalizeForeignKeys", () => {
238
256
  },
239
257
  },
240
258
  });
241
- expect(normalizeForeignKeys(models, models["post"])).toEqual([
259
+
260
+ expect(normalizeForeignKeys(models, models["post"]!)).toEqual([
242
261
  {
243
262
  name: "post_user_id_fkey",
244
263
  fields: ["userId"],
@@ -0,0 +1,89 @@
1
+ import { isEqual } from "es-toolkit";
2
+
3
+ import { FieldDefinition, ForeignKeyDefinition } from "@casekit/orm2-schema";
4
+ import { MarkNonNullable } from "@casekit/toolbox";
5
+
6
+ import { NormalizedForeignKeyDefinition } from "#types/NormalizedForeignKeyDefinition.js";
7
+ import { PopulatedFieldDefinition } from "#types/PopulatedFieldDefinition.js";
8
+ import { PopulatedModelDefinition } from "#types/PopulatedModelDefinition.js";
9
+ import { getColumns } from "./getColumns.js";
10
+
11
+ export const normalizeForeignKeys = (
12
+ models: Record<string, PopulatedModelDefinition>,
13
+ model: PopulatedModelDefinition,
14
+ ): NormalizedForeignKeyDefinition[] => {
15
+ const columnLevelForeignKeys = Object.values(model.fields)
16
+ .filter(hasReference)
17
+ .map(referenceToForeignKey)
18
+ .map((fk) => normalizeForeignKey(models, model, fk));
19
+
20
+ const modelLevelForeignKeys = model.foreignKeys.map((fk) =>
21
+ normalizeForeignKey(models, model, fk),
22
+ );
23
+
24
+ for (const fk of columnLevelForeignKeys) {
25
+ if (
26
+ modelLevelForeignKeys.some((other) =>
27
+ isEqual(fk.columns, other.columns),
28
+ )
29
+ ) {
30
+ throw new Error(
31
+ `Duplicate foreign key defined in model "${model.name}"`,
32
+ );
33
+ }
34
+ }
35
+
36
+ return [...columnLevelForeignKeys, ...modelLevelForeignKeys];
37
+ };
38
+
39
+ export const normalizeForeignKey = (
40
+ models: Record<string, PopulatedModelDefinition>,
41
+ model: PopulatedModelDefinition,
42
+ fk: ForeignKeyDefinition,
43
+ ): NormalizedForeignKeyDefinition => {
44
+ const referencedModel = models[fk.references.model];
45
+ if (!referencedModel) {
46
+ throw new Error(
47
+ `Referenced model "${fk.references.model}" not found in models`,
48
+ );
49
+ }
50
+
51
+ const columns = getColumns(model, fk.fields);
52
+
53
+ return {
54
+ name: fk.name ?? [model.table, ...columns, "fkey"].join("_"),
55
+ fields: fk.fields,
56
+ columns: columns,
57
+ references: {
58
+ model: fk.references.model,
59
+ fields: fk.references.fields,
60
+ schema: referencedModel.schema,
61
+ table: referencedModel.table,
62
+ columns: getColumns(referencedModel, fk.references.fields),
63
+ },
64
+ onUpdate: fk.onUpdate ?? null,
65
+ onDelete: fk.onDelete ?? null,
66
+ };
67
+ };
68
+
69
+ const referenceToForeignKey = (
70
+ field: MarkNonNullable<PopulatedFieldDefinition, "references">,
71
+ ): ForeignKeyDefinition => {
72
+ return {
73
+ fields: [field.name],
74
+ references: {
75
+ model: field.references.model,
76
+ fields: [field.references.field],
77
+ },
78
+ onUpdate: field.references.onUpdate,
79
+ onDelete: field.references.onDelete,
80
+ };
81
+ };
82
+
83
+ const hasReference = (
84
+ field: FieldDefinition,
85
+ ): field is PopulatedFieldDefinition & {
86
+ references: MarkNonNullable<PopulatedFieldDefinition, "references">;
87
+ } => {
88
+ return !!field.references;
89
+ };
@@ -1,9 +1,12 @@
1
1
  import { snakeCase } from "es-toolkit";
2
2
  import { describe, expect, test } from "vitest";
3
- import { ZodSchema, z } from "zod";
3
+ import { ZodType, z } from "zod";
4
+
4
5
  import { sql } from "@casekit/sql";
6
+
5
7
  import { normalizeModel } from "./normalizeModel.js";
6
8
  import { populateModels } from "./populateModels.js";
9
+
7
10
  describe("normalizeModel", () => {
8
11
  test("normalizes complete model definition", () => {
9
12
  const models = populateModels({
@@ -19,7 +22,7 @@ describe("normalizeModel", () => {
19
22
  email: {
20
23
  type: "text",
21
24
  unique: true,
22
- zodSchema: z.string().email(),
25
+ zodSchema: z.email(),
23
26
  },
24
27
  name: {
25
28
  type: "text",
@@ -27,7 +30,7 @@ describe("normalizeModel", () => {
27
30
  },
28
31
  createdAt: {
29
32
  type: "timestamp",
30
- default: sql `NOW()`,
33
+ default: sql`NOW()`,
31
34
  provided: true,
32
35
  },
33
36
  },
@@ -63,32 +66,38 @@ describe("normalizeModel", () => {
63
66
  },
64
67
  },
65
68
  });
66
- const result = normalizeModel(models, models["user"]);
69
+
70
+ const result = normalizeModel(models, models["user"]!);
71
+
67
72
  // Test basic model properties
68
73
  expect(result.name).toBe("user");
69
74
  expect(result.schema).toBe("auth");
70
75
  expect(result.table).toBe("user");
76
+
71
77
  // Test fields
72
- expect(result.fields["id"]).toEqual({
78
+ expect(result.fields["id"]!).toEqual({
73
79
  name: "id",
74
80
  column: "id",
75
81
  type: "serial",
76
- zodSchema: expect.any(ZodSchema),
82
+ zodSchema: expect.any(ZodType),
77
83
  nullable: false,
78
84
  default: null,
79
85
  provided: false,
80
86
  });
81
- expect(result.fields["email"]).toEqual({
87
+
88
+ expect(result.fields["email"]!).toEqual({
82
89
  name: "email",
83
90
  column: "email",
84
91
  type: "text",
85
- zodSchema: expect.any(ZodSchema),
92
+ zodSchema: expect.any(ZodType),
86
93
  nullable: false,
87
94
  default: null,
88
95
  provided: false,
89
96
  });
97
+
90
98
  // Test primary key
91
99
  expect(result.primaryKey).toEqual([{ field: "id", column: "id" }]);
100
+
92
101
  // Test unique constraints
93
102
  expect(result.uniqueConstraints).toEqual([
94
103
  {
@@ -99,8 +108,9 @@ describe("normalizeModel", () => {
99
108
  nullsNotDistinct: false,
100
109
  },
101
110
  ]);
111
+
102
112
  // Test relations
103
- expect(result.relations["posts"]).toEqual({
113
+ expect(result.relations["posts"]!).toEqual({
104
114
  name: "posts",
105
115
  type: "1:N",
106
116
  model: "post",
@@ -114,7 +124,8 @@ describe("normalizeModel", () => {
114
124
  columns: ["author_id"],
115
125
  },
116
126
  });
117
- expect(result.relations["likedPosts"]).toEqual({
127
+
128
+ expect(result.relations["likedPosts"]!).toEqual({
118
129
  name: "likedPosts",
119
130
  type: "N:N",
120
131
  model: "post",
@@ -127,6 +138,7 @@ describe("normalizeModel", () => {
127
138
  },
128
139
  });
129
140
  });
141
+
130
142
  test("normalizes model with column name transformations", () => {
131
143
  const models = populateModels({
132
144
  naming: { column: snakeCase },
@@ -141,12 +153,16 @@ describe("normalizeModel", () => {
141
153
  },
142
154
  },
143
155
  });
144
- const result = normalizeModel(models, models["userProfile"]);
145
- expect(result.fields["firstName"].column).toBe("first_name");
146
- expect(result.fields["lastName"].column).toBe("last_name");
147
- expect(result.fields["emailAddress"].column).toBe("email_address");
148
- expect(result.uniqueConstraints[0].columns).toEqual(["email_address"]);
156
+
157
+ const result = normalizeModel(models, models["userProfile"]!);
158
+
159
+ expect(result.fields["firstName"]!.column).toBe("first_name");
160
+ expect(result.fields["lastName"]!.column).toBe("last_name");
161
+ expect(result.fields["emailAddress"]!.column).toBe("email_address");
162
+
163
+ expect(result.uniqueConstraints[0]!.columns).toEqual(["email_address"]);
149
164
  });
165
+
150
166
  test("normalizes model with custom schema and table names", () => {
151
167
  const models = populateModels({
152
168
  models: {
@@ -159,10 +175,13 @@ describe("normalizeModel", () => {
159
175
  },
160
176
  },
161
177
  });
162
- const result = normalizeModel(models, models["user"]);
178
+
179
+ const result = normalizeModel(models, models["user"]!);
180
+
163
181
  expect(result.schema).toBe("custom_schema");
164
182
  expect(result.table).toBe("custom_table");
165
183
  });
184
+
166
185
  test("normalizes model with composite primary key", () => {
167
186
  const models = populateModels({
168
187
  naming: { column: snakeCase },
@@ -176,12 +195,15 @@ describe("normalizeModel", () => {
176
195
  },
177
196
  },
178
197
  });
179
- const result = normalizeModel(models, models["orderLine"]);
198
+
199
+ const result = normalizeModel(models, models["orderLine"]!);
200
+
180
201
  expect(result.primaryKey).toEqual([
181
202
  { field: "orderId", column: "order_id" },
182
203
  { field: "lineNumber", column: "line_number" },
183
204
  ]);
184
205
  });
206
+
185
207
  test("normalizes model with foreign keys", () => {
186
208
  const models = populateModels({
187
209
  naming: { column: snakeCase },
@@ -206,7 +228,9 @@ describe("normalizeModel", () => {
206
228
  },
207
229
  },
208
230
  });
209
- const result = normalizeModel(models, models["order"]);
231
+
232
+ const result = normalizeModel(models, models["order"]!);
233
+
210
234
  expect(result.foreignKeys).toEqual([
211
235
  {
212
236
  name: "order_customer_id_fkey",
@@ -1,10 +1,17 @@
1
1
  import { mapValues } from "es-toolkit";
2
+
3
+ import { NormalizedModelDefinition } from "#types/NormalizedModelDefinition.js";
4
+ import { PopulatedModelDefinition } from "#types/PopulatedModelDefinition.js";
2
5
  import { normalizeField } from "./normalizeField.js";
3
6
  import { normalizeForeignKeys } from "./normalizeForeignKeys.js";
4
7
  import { normalizePrimaryKey } from "./normalizePrimaryKey.js";
5
8
  import { normalizeRelations } from "./normalizeRelations.js";
6
9
  import { normalizeUniqueConstraints } from "./normalizeUniqueConstraints.js";
7
- export const normalizeModel = (models, model) => {
10
+
11
+ export const normalizeModel = (
12
+ models: Record<string, PopulatedModelDefinition>,
13
+ model: PopulatedModelDefinition,
14
+ ): NormalizedModelDefinition => {
8
15
  return {
9
16
  name: model.name,
10
17
  schema: model.schema,
@@ -1,7 +1,9 @@
1
1
  import { snakeCase } from "es-toolkit";
2
2
  import { describe, expect, test } from "vitest";
3
+
3
4
  import { normalizePrimaryKey } from "./normalizePrimaryKey.js";
4
5
  import { populateModels } from "./populateModels.js";
6
+
5
7
  describe("normalizePrimaryKey", () => {
6
8
  test("normalizes field-level primary key", () => {
7
9
  const models = populateModels({
@@ -14,7 +16,9 @@ describe("normalizePrimaryKey", () => {
14
16
  },
15
17
  },
16
18
  });
17
- const result = normalizePrimaryKey(models["user"]);
19
+
20
+ const result = normalizePrimaryKey(models["user"]!);
21
+
18
22
  expect(result).toEqual([
19
23
  {
20
24
  field: "id",
@@ -22,6 +26,7 @@ describe("normalizePrimaryKey", () => {
22
26
  },
23
27
  ]);
24
28
  });
29
+
25
30
  test("normalizes model-level primary key", () => {
26
31
  const models = populateModels({
27
32
  models: {
@@ -35,7 +40,9 @@ describe("normalizePrimaryKey", () => {
35
40
  },
36
41
  },
37
42
  });
38
- const result = normalizePrimaryKey(models["userRole"]);
43
+
44
+ const result = normalizePrimaryKey(models["userRole"]!);
45
+
39
46
  expect(result).toEqual([
40
47
  {
41
48
  field: "userId",
@@ -47,6 +54,7 @@ describe("normalizePrimaryKey", () => {
47
54
  },
48
55
  ]);
49
56
  });
57
+
50
58
  test("throws error when primary key is defined at both levels", () => {
51
59
  const models = populateModels({
52
60
  models: {
@@ -59,8 +67,12 @@ describe("normalizePrimaryKey", () => {
59
67
  },
60
68
  },
61
69
  });
62
- expect(() => normalizePrimaryKey(models["user"])).toThrow('Model "user" has primary key fields defined at both the model and field levels.');
70
+
71
+ expect(() => normalizePrimaryKey(models["user"]!)).toThrow(
72
+ 'Model "user" has primary key fields defined at both the model and field levels.',
73
+ );
63
74
  });
75
+
64
76
  test("handles column name transformations", () => {
65
77
  const models = populateModels({
66
78
  naming: { column: snakeCase },
@@ -73,7 +85,9 @@ describe("normalizePrimaryKey", () => {
73
85
  },
74
86
  },
75
87
  });
76
- const result = normalizePrimaryKey(models["user"]);
88
+
89
+ const result = normalizePrimaryKey(models["user"]!);
90
+
77
91
  expect(result).toEqual([
78
92
  {
79
93
  field: "userId",
@@ -81,6 +95,7 @@ describe("normalizePrimaryKey", () => {
81
95
  },
82
96
  ]);
83
97
  });
98
+
84
99
  test("handles custom column names", () => {
85
100
  const models = populateModels({
86
101
  models: {
@@ -95,7 +110,9 @@ describe("normalizePrimaryKey", () => {
95
110
  },
96
111
  },
97
112
  });
98
- const result = normalizePrimaryKey(models["user"]);
113
+
114
+ const result = normalizePrimaryKey(models["user"]!);
115
+
99
116
  expect(result).toEqual([
100
117
  {
101
118
  field: "id",
@@ -103,6 +120,7 @@ describe("normalizePrimaryKey", () => {
103
120
  },
104
121
  ]);
105
122
  });
123
+
106
124
  test("throws error for non-existent primary key field in model-level definition", () => {
107
125
  const models = populateModels({
108
126
  models: {
@@ -115,8 +133,12 @@ describe("normalizePrimaryKey", () => {
115
133
  },
116
134
  },
117
135
  });
118
- expect(() => normalizePrimaryKey(models["user"])).toThrow('Primary key field "nonexistent" does not exist in model "user".');
136
+
137
+ expect(() => normalizePrimaryKey(models["user"]!)).toThrow(
138
+ 'Primary key field "nonexistent" does not exist in model "user".',
139
+ );
119
140
  });
141
+
120
142
  test("handles multiple field-level primary keys", () => {
121
143
  const models = populateModels({
122
144
  models: {
@@ -129,7 +151,9 @@ describe("normalizePrimaryKey", () => {
129
151
  },
130
152
  },
131
153
  });
132
- const result = normalizePrimaryKey(models["userRole"]);
154
+
155
+ const result = normalizePrimaryKey(models["userRole"]!);
156
+
133
157
  expect(result).toEqual([
134
158
  {
135
159
  field: "userId",
@@ -0,0 +1,30 @@
1
+ import { NormalizedPrimaryKey } from "#types/NormalizedPrimaryKey.js";
2
+ import { PopulatedModelDefinition } from "#types/PopulatedModelDefinition.js";
3
+
4
+ export const normalizePrimaryKey = (
5
+ model: PopulatedModelDefinition,
6
+ ): NormalizedPrimaryKey[] => {
7
+ const fieldLevelPrimaryKey = Object.entries(model.fields)
8
+ .filter(([, field]) => field.primaryKey)
9
+ .map(([name]) => name);
10
+
11
+ if (model.primaryKey && fieldLevelPrimaryKey.length > 0) {
12
+ throw new Error(
13
+ `Model "${model.name}" has primary key fields defined at both the model and field levels.`,
14
+ );
15
+ }
16
+
17
+ const fields = model.primaryKey ?? fieldLevelPrimaryKey;
18
+
19
+ return fields.map((name) => {
20
+ if (!model.fields[name]) {
21
+ throw new Error(
22
+ `Primary key field "${name}" does not exist in model "${model.name}".`,
23
+ );
24
+ }
25
+ return {
26
+ field: name,
27
+ column: model.fields[name].column,
28
+ };
29
+ });
30
+ };
@@ -1,7 +1,10 @@
1
1
  import { snakeCase } from "es-toolkit";
2
2
  import { describe, expect, test } from "vitest";
3
+
4
+ import { NormalizedOneToManyRelationDefinition } from "../types/NormalizedRelationDefinition.js";
3
5
  import { normalizeRelations } from "./normalizeRelations.js";
4
6
  import { populateModels } from "./populateModels.js";
7
+
5
8
  describe("normalizeRelations", () => {
6
9
  test("normalizes one-to-many relation", () => {
7
10
  const models = populateModels({
@@ -28,7 +31,9 @@ describe("normalizeRelations", () => {
28
31
  },
29
32
  },
30
33
  });
31
- const result = normalizeRelations(models, models["user"]);
34
+
35
+ const result = normalizeRelations(models, models["user"]!);
36
+
32
37
  expect(result).toEqual({
33
38
  posts: {
34
39
  name: "posts",
@@ -46,6 +51,7 @@ describe("normalizeRelations", () => {
46
51
  },
47
52
  });
48
53
  });
54
+
49
55
  test("normalizes many-to-one relation", () => {
50
56
  const models = populateModels({
51
57
  naming: { column: snakeCase },
@@ -71,7 +77,9 @@ describe("normalizeRelations", () => {
71
77
  },
72
78
  },
73
79
  });
74
- const result = normalizeRelations(models, models["post"]);
80
+
81
+ const result = normalizeRelations(models, models["post"]!);
82
+
75
83
  expect(result).toEqual({
76
84
  author: {
77
85
  name: "author",
@@ -90,6 +98,7 @@ describe("normalizeRelations", () => {
90
98
  },
91
99
  });
92
100
  });
101
+
93
102
  test("normalizes many-to-many relation", () => {
94
103
  const models = populateModels({
95
104
  naming: { column: snakeCase },
@@ -123,7 +132,9 @@ describe("normalizeRelations", () => {
123
132
  },
124
133
  },
125
134
  });
126
- const result = normalizeRelations(models, models["user"]);
135
+
136
+ const result = normalizeRelations(models, models["user"]!);
137
+
127
138
  expect(result).toEqual({
128
139
  likedPosts: {
129
140
  model: "post",
@@ -139,6 +150,7 @@ describe("normalizeRelations", () => {
139
150
  },
140
151
  });
141
152
  });
153
+
142
154
  test("handles composite keys in relations", () => {
143
155
  const models = populateModels({
144
156
  naming: { column: snakeCase },
@@ -167,7 +179,9 @@ describe("normalizeRelations", () => {
167
179
  },
168
180
  },
169
181
  });
170
- const result = normalizeRelations(models, models["order"]);
182
+
183
+ const result = normalizeRelations(models, models["order"]!);
184
+
171
185
  expect(result).toEqual({
172
186
  product: {
173
187
  name: "product",
@@ -186,6 +200,7 @@ describe("normalizeRelations", () => {
186
200
  },
187
201
  });
188
202
  });
203
+
189
204
  test("throws error for non-existent related model", () => {
190
205
  const models = populateModels({
191
206
  models: {
@@ -204,8 +219,12 @@ describe("normalizeRelations", () => {
204
219
  },
205
220
  },
206
221
  });
207
- expect(() => normalizeRelations(models, models["user"])).toThrow('Model "user" has relation "posts" that references non-existent model "nonexistent".');
222
+
223
+ expect(() => normalizeRelations(models, models["user"]!)).toThrow(
224
+ 'Model "user" has relation "posts" that references non-existent model "nonexistent".',
225
+ );
208
226
  });
227
+
209
228
  test("throws error for non-existent join model", () => {
210
229
  const models = populateModels({
211
230
  models: {
@@ -232,8 +251,12 @@ describe("normalizeRelations", () => {
232
251
  },
233
252
  },
234
253
  });
235
- expect(() => normalizeRelations(models, models["user"])).toThrow('Model "user" has relation "likedPosts" with join model "nonexistent" that does not exist.');
254
+
255
+ expect(() => normalizeRelations(models, models["user"]!)).toThrow(
256
+ 'Model "user" has relation "likedPosts" with join model "nonexistent" that does not exist.',
257
+ );
236
258
  });
259
+
237
260
  test("throws error for non-existent field in relation", () => {
238
261
  const models = populateModels({
239
262
  models: {
@@ -258,8 +281,12 @@ describe("normalizeRelations", () => {
258
281
  },
259
282
  },
260
283
  });
261
- expect(() => normalizeRelations(models, models["user"])).toThrow('Model "user" has relation with non-existent field "nonexistent".');
284
+
285
+ expect(() => normalizeRelations(models, models["user"]!)).toThrow(
286
+ 'Model "user" has relation with non-existent field "nonexistent".',
287
+ );
262
288
  });
289
+
263
290
  test("handles custom column names", () => {
264
291
  const models = populateModels({
265
292
  models: {
@@ -290,8 +317,10 @@ describe("normalizeRelations", () => {
290
317
  },
291
318
  },
292
319
  });
293
- const result = normalizeRelations(models, models["user"]);
294
- const posts = result["posts"];
320
+
321
+ const result = normalizeRelations(models, models["user"]!);
322
+
323
+ const posts = result["posts"]! as NormalizedOneToManyRelationDefinition;
295
324
  expect(posts.from.columns).toEqual(["user_identifier"]);
296
325
  expect(posts.to.columns).toEqual(["created_by_user"]);
297
326
  });