@casekit/orm2-config 0.0.0-20250322230249

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 (67) hide show
  1. package/build/index.d.ts +11 -0
  2. package/build/index.js +4 -0
  3. package/build/normalize/defaultZodSchema.d.ts +8 -0
  4. package/build/normalize/defaultZodSchema.js +127 -0
  5. package/build/normalize/defaultZodSchema.test.d.ts +1 -0
  6. package/build/normalize/defaultZodSchema.test.js +153 -0
  7. package/build/normalize/getColumns.d.ts +2 -0
  8. package/build/normalize/getColumns.js +8 -0
  9. package/build/normalize/getColumns.test.d.ts +1 -0
  10. package/build/normalize/getColumns.test.js +63 -0
  11. package/build/normalize/normalizeConfig.d.ts +3 -0
  12. package/build/normalize/normalizeConfig.js +20 -0
  13. package/build/normalize/normalizeConfig.test.d.ts +1 -0
  14. package/build/normalize/normalizeConfig.test.js +217 -0
  15. package/build/normalize/normalizeField.d.ts +3 -0
  16. package/build/normalize/normalizeField.js +11 -0
  17. package/build/normalize/normalizeField.test.d.ts +1 -0
  18. package/build/normalize/normalizeField.test.js +59 -0
  19. package/build/normalize/normalizeForeignKeys.d.ts +5 -0
  20. package/build/normalize/normalizeForeignKeys.js +50 -0
  21. package/build/normalize/normalizeForeignKeys.test.d.ts +1 -0
  22. package/build/normalize/normalizeForeignKeys.test.js +258 -0
  23. package/build/normalize/normalizeModel.d.ts +3 -0
  24. package/build/normalize/normalizeModel.js +18 -0
  25. package/build/normalize/normalizeModel.test.d.ts +1 -0
  26. package/build/normalize/normalizeModel.test.js +227 -0
  27. package/build/normalize/normalizePrimaryKey.d.ts +3 -0
  28. package/build/normalize/normalizePrimaryKey.js +18 -0
  29. package/build/normalize/normalizePrimaryKey.test.d.ts +1 -0
  30. package/build/normalize/normalizePrimaryKey.test.js +144 -0
  31. package/build/normalize/normalizeRelations.d.ts +3 -0
  32. package/build/normalize/normalizeRelations.js +58 -0
  33. package/build/normalize/normalizeRelations.test.d.ts +1 -0
  34. package/build/normalize/normalizeRelations.test.js +298 -0
  35. package/build/normalize/normalizeUniqueConstraints.d.ts +5 -0
  36. package/build/normalize/normalizeUniqueConstraints.js +29 -0
  37. package/build/normalize/normalizeUniqueConstraints.test.d.ts +1 -0
  38. package/build/normalize/normalizeUniqueConstraints.test.js +241 -0
  39. package/build/normalize/populateField.d.ts +3 -0
  40. package/build/normalize/populateField.js +15 -0
  41. package/build/normalize/populateField.test.d.ts +1 -0
  42. package/build/normalize/populateField.test.js +198 -0
  43. package/build/normalize/populateModels.d.ts +3 -0
  44. package/build/normalize/populateModels.js +16 -0
  45. package/build/normalize/populateModels.test.d.ts +1 -0
  46. package/build/normalize/populateModels.test.js +240 -0
  47. package/build/types/NormalizedConfig.d.ts +16 -0
  48. package/build/types/NormalizedConfig.js +1 -0
  49. package/build/types/NormalizedFieldDefinition.d.ts +10 -0
  50. package/build/types/NormalizedFieldDefinition.js +1 -0
  51. package/build/types/NormalizedForeignKeyDefinition.d.ts +14 -0
  52. package/build/types/NormalizedForeignKeyDefinition.js +1 -0
  53. package/build/types/NormalizedModelDefinition.d.ts +15 -0
  54. package/build/types/NormalizedModelDefinition.js +1 -0
  55. package/build/types/NormalizedPrimaryKey.d.ts +4 -0
  56. package/build/types/NormalizedPrimaryKey.js +1 -0
  57. package/build/types/NormalizedRelationDefinition.d.ts +42 -0
  58. package/build/types/NormalizedRelationDefinition.js +1 -0
  59. package/build/types/NormalizedUniqueConstraintDefinition.d.ts +8 -0
  60. package/build/types/NormalizedUniqueConstraintDefinition.js +1 -0
  61. package/build/types/PopulatedFieldDefinition.d.ts +4 -0
  62. package/build/types/PopulatedFieldDefinition.js +1 -0
  63. package/build/types/PopulatedModelDefinition.d.ts +6 -0
  64. package/build/types/PopulatedModelDefinition.js +1 -0
  65. package/build/util.d.ts +6 -0
  66. package/build/util.js +21 -0
  67. package/package.json +54 -0
@@ -0,0 +1,144 @@
1
+ import { snakeCase } from "es-toolkit";
2
+ import { describe, expect, test } from "vitest";
3
+ import { normalizePrimaryKey } from "./normalizePrimaryKey.js";
4
+ import { populateModels } from "./populateModels.js";
5
+ describe("normalizePrimaryKey", () => {
6
+ test("normalizes field-level primary key", () => {
7
+ const models = populateModels({
8
+ models: {
9
+ user: {
10
+ fields: {
11
+ id: { type: "serial", primaryKey: true },
12
+ name: { type: "text" },
13
+ },
14
+ },
15
+ },
16
+ });
17
+ const result = normalizePrimaryKey(models["user"]);
18
+ expect(result).toEqual([
19
+ {
20
+ field: "id",
21
+ column: "id",
22
+ },
23
+ ]);
24
+ });
25
+ test("normalizes model-level primary key", () => {
26
+ const models = populateModels({
27
+ models: {
28
+ userRole: {
29
+ primaryKey: ["userId", "roleId"],
30
+ fields: {
31
+ userId: { type: "integer" },
32
+ roleId: { type: "integer" },
33
+ assignedAt: { type: "timestamp" },
34
+ },
35
+ },
36
+ },
37
+ });
38
+ const result = normalizePrimaryKey(models["userRole"]);
39
+ expect(result).toEqual([
40
+ {
41
+ field: "userId",
42
+ column: "userId",
43
+ },
44
+ {
45
+ field: "roleId",
46
+ column: "roleId",
47
+ },
48
+ ]);
49
+ });
50
+ test("throws error when primary key is defined at both levels", () => {
51
+ const models = populateModels({
52
+ models: {
53
+ user: {
54
+ primaryKey: ["id"],
55
+ fields: {
56
+ id: { type: "serial", primaryKey: true },
57
+ name: { type: "text" },
58
+ },
59
+ },
60
+ },
61
+ });
62
+ expect(() => normalizePrimaryKey(models["user"])).toThrow('Model "user" has primary key fields defined at both the model and field levels.');
63
+ });
64
+ test("handles column name transformations", () => {
65
+ const models = populateModels({
66
+ naming: { column: snakeCase },
67
+ models: {
68
+ user: {
69
+ fields: {
70
+ userId: { type: "serial", primaryKey: true },
71
+ fullName: { type: "text" },
72
+ },
73
+ },
74
+ },
75
+ });
76
+ const result = normalizePrimaryKey(models["user"]);
77
+ expect(result).toEqual([
78
+ {
79
+ field: "userId",
80
+ column: "user_id",
81
+ },
82
+ ]);
83
+ });
84
+ test("handles custom column names", () => {
85
+ const models = populateModels({
86
+ models: {
87
+ user: {
88
+ fields: {
89
+ id: {
90
+ type: "serial",
91
+ primaryKey: true,
92
+ column: "user_identifier",
93
+ },
94
+ },
95
+ },
96
+ },
97
+ });
98
+ const result = normalizePrimaryKey(models["user"]);
99
+ expect(result).toEqual([
100
+ {
101
+ field: "id",
102
+ column: "user_identifier",
103
+ },
104
+ ]);
105
+ });
106
+ test("throws error for non-existent primary key field in model-level definition", () => {
107
+ const models = populateModels({
108
+ models: {
109
+ user: {
110
+ primaryKey: ["nonexistent"],
111
+ fields: {
112
+ id: { type: "serial" },
113
+ name: { type: "text" },
114
+ },
115
+ },
116
+ },
117
+ });
118
+ expect(() => normalizePrimaryKey(models["user"])).toThrow('Primary key field "nonexistent" does not exist in model "user".');
119
+ });
120
+ test("handles multiple field-level primary keys", () => {
121
+ const models = populateModels({
122
+ models: {
123
+ userRole: {
124
+ fields: {
125
+ userId: { type: "integer", primaryKey: true },
126
+ roleId: { type: "integer", primaryKey: true },
127
+ assignedAt: { type: "timestamp" },
128
+ },
129
+ },
130
+ },
131
+ });
132
+ const result = normalizePrimaryKey(models["userRole"]);
133
+ expect(result).toEqual([
134
+ {
135
+ field: "userId",
136
+ column: "userId",
137
+ },
138
+ {
139
+ field: "roleId",
140
+ column: "roleId",
141
+ },
142
+ ]);
143
+ });
144
+ });
@@ -0,0 +1,3 @@
1
+ import { NormalizedRelationDefinition } from "#types/NormalizedRelationDefinition.js";
2
+ import { PopulatedModelDefinition } from "#types/PopulatedModelDefinition.js";
3
+ export declare const normalizeRelations: (models: Record<string, PopulatedModelDefinition>, model: PopulatedModelDefinition) => Record<string, NormalizedRelationDefinition>;
@@ -0,0 +1,58 @@
1
+ import { mapValues } from "es-toolkit";
2
+ import { castArray } from "es-toolkit/compat";
3
+ const normalizeRelationKeys = (model, fields) => {
4
+ const columns = castArray(fields).map((field) => {
5
+ if (!model.fields[field]) {
6
+ throw new Error(`Model "${model.name}" has relation with non-existent field "${field}".`);
7
+ }
8
+ return model.fields[field].column;
9
+ });
10
+ return { fields: castArray(fields), columns };
11
+ };
12
+ export const normalizeRelations = (models, model) => {
13
+ return mapValues(model.relations, (relation, name) => {
14
+ const relatedModel = models[relation.model];
15
+ if (!relatedModel) {
16
+ throw new Error(`Model "${model.name}" has relation "${name}" that references non-existent model "${relation.model}".`);
17
+ }
18
+ if (relation.type === "N:N") {
19
+ const joinModel = models[relation.through.model];
20
+ if (!joinModel) {
21
+ throw new Error(`Model "${model.name}" has relation "${name}" with join model "${relation.through.model}" that does not exist.`);
22
+ }
23
+ return {
24
+ name,
25
+ type: relation.type,
26
+ model: relation.model,
27
+ table: relatedModel.table,
28
+ through: {
29
+ model: joinModel.name,
30
+ table: joinModel.table,
31
+ fromRelation: relation.through.fromRelation,
32
+ toRelation: relation.through.toRelation,
33
+ },
34
+ };
35
+ }
36
+ else if (relation.type === "1:N") {
37
+ return {
38
+ name,
39
+ type: relation.type,
40
+ model: relation.model,
41
+ table: relatedModel.table,
42
+ from: normalizeRelationKeys(model, relation.fromField),
43
+ to: normalizeRelationKeys(relatedModel, relation.toField),
44
+ };
45
+ }
46
+ else {
47
+ return {
48
+ name,
49
+ type: relation.type,
50
+ model: relation.model,
51
+ table: relatedModel.table,
52
+ optional: relation.optional ?? false,
53
+ from: normalizeRelationKeys(model, relation.fromField),
54
+ to: normalizeRelationKeys(relatedModel, relation.toField),
55
+ };
56
+ }
57
+ });
58
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,298 @@
1
+ import { snakeCase } from "es-toolkit";
2
+ import { describe, expect, test } from "vitest";
3
+ import { normalizeRelations } from "./normalizeRelations.js";
4
+ import { populateModels } from "./populateModels.js";
5
+ describe("normalizeRelations", () => {
6
+ test("normalizes one-to-many relation", () => {
7
+ const models = populateModels({
8
+ naming: { column: snakeCase },
9
+ models: {
10
+ user: {
11
+ fields: {
12
+ id: { type: "serial" },
13
+ },
14
+ relations: {
15
+ posts: {
16
+ type: "1:N",
17
+ model: "post",
18
+ fromField: "id",
19
+ toField: "authorId",
20
+ },
21
+ },
22
+ },
23
+ post: {
24
+ fields: {
25
+ id: { type: "serial" },
26
+ authorId: { type: "integer" },
27
+ },
28
+ },
29
+ },
30
+ });
31
+ const result = normalizeRelations(models, models["user"]);
32
+ expect(result).toEqual({
33
+ posts: {
34
+ name: "posts",
35
+ type: "1:N",
36
+ model: "post",
37
+ table: "post",
38
+ from: {
39
+ fields: ["id"],
40
+ columns: ["id"],
41
+ },
42
+ to: {
43
+ fields: ["authorId"],
44
+ columns: ["author_id"],
45
+ },
46
+ },
47
+ });
48
+ });
49
+ test("normalizes many-to-one relation", () => {
50
+ const models = populateModels({
51
+ naming: { column: snakeCase },
52
+ models: {
53
+ post: {
54
+ fields: {
55
+ id: { type: "serial" },
56
+ authorId: { type: "integer" },
57
+ },
58
+ relations: {
59
+ author: {
60
+ type: "N:1",
61
+ model: "user",
62
+ fromField: "authorId",
63
+ toField: "id",
64
+ },
65
+ },
66
+ },
67
+ user: {
68
+ fields: {
69
+ id: { type: "serial" },
70
+ },
71
+ },
72
+ },
73
+ });
74
+ const result = normalizeRelations(models, models["post"]);
75
+ expect(result).toEqual({
76
+ author: {
77
+ name: "author",
78
+ type: "N:1",
79
+ model: "user",
80
+ table: "user",
81
+ from: {
82
+ fields: ["authorId"],
83
+ columns: ["author_id"],
84
+ },
85
+ to: {
86
+ fields: ["id"],
87
+ columns: ["id"],
88
+ },
89
+ optional: false,
90
+ },
91
+ });
92
+ });
93
+ test("normalizes many-to-many relation", () => {
94
+ const models = populateModels({
95
+ naming: { column: snakeCase },
96
+ models: {
97
+ user: {
98
+ fields: {
99
+ id: { type: "serial" },
100
+ },
101
+ relations: {
102
+ likedPosts: {
103
+ type: "N:N",
104
+ model: "post",
105
+ through: {
106
+ model: "like",
107
+ fromRelation: "user",
108
+ toRelation: "post",
109
+ },
110
+ },
111
+ },
112
+ },
113
+ post: {
114
+ fields: {
115
+ id: { type: "serial" },
116
+ },
117
+ },
118
+ like: {
119
+ fields: {
120
+ userId: { type: "integer" },
121
+ postId: { type: "integer" },
122
+ },
123
+ },
124
+ },
125
+ });
126
+ const result = normalizeRelations(models, models["user"]);
127
+ expect(result).toEqual({
128
+ likedPosts: {
129
+ model: "post",
130
+ name: "likedPosts",
131
+ table: "post",
132
+ through: {
133
+ model: "like",
134
+ table: "like",
135
+ fromRelation: "user",
136
+ toRelation: "post",
137
+ },
138
+ type: "N:N",
139
+ },
140
+ });
141
+ });
142
+ test("handles composite keys in relations", () => {
143
+ const models = populateModels({
144
+ naming: { column: snakeCase },
145
+ models: {
146
+ order: {
147
+ fields: {
148
+ orderId: { type: "integer" },
149
+ productId: { type: "integer" },
150
+ variantId: { type: "integer" },
151
+ lineNumber: { type: "integer" },
152
+ },
153
+ relations: {
154
+ product: {
155
+ type: "N:1",
156
+ model: "product",
157
+ fromField: ["productId", "variantId"],
158
+ toField: ["id", "variantId"],
159
+ },
160
+ },
161
+ },
162
+ product: {
163
+ fields: {
164
+ id: { type: "serial" },
165
+ variantId: { type: "integer" },
166
+ },
167
+ },
168
+ },
169
+ });
170
+ const result = normalizeRelations(models, models["order"]);
171
+ expect(result).toEqual({
172
+ product: {
173
+ name: "product",
174
+ type: "N:1",
175
+ model: "product",
176
+ table: "product",
177
+ from: {
178
+ fields: ["productId", "variantId"],
179
+ columns: ["product_id", "variant_id"],
180
+ },
181
+ to: {
182
+ fields: ["id", "variantId"],
183
+ columns: ["id", "variant_id"],
184
+ },
185
+ optional: false,
186
+ },
187
+ });
188
+ });
189
+ test("throws error for non-existent related model", () => {
190
+ const models = populateModels({
191
+ models: {
192
+ user: {
193
+ fields: {
194
+ id: { type: "serial" },
195
+ },
196
+ relations: {
197
+ posts: {
198
+ type: "1:N",
199
+ model: "nonexistent",
200
+ fromField: "id",
201
+ toField: "authorId",
202
+ },
203
+ },
204
+ },
205
+ },
206
+ });
207
+ expect(() => normalizeRelations(models, models["user"])).toThrow('Model "user" has relation "posts" that references non-existent model "nonexistent".');
208
+ });
209
+ test("throws error for non-existent join model", () => {
210
+ const models = populateModels({
211
+ models: {
212
+ user: {
213
+ fields: {
214
+ id: { type: "serial" },
215
+ },
216
+ relations: {
217
+ likedPosts: {
218
+ type: "N:N",
219
+ model: "post",
220
+ through: {
221
+ model: "nonexistent",
222
+ fromRelation: "user",
223
+ toRelation: "post",
224
+ },
225
+ },
226
+ },
227
+ },
228
+ post: {
229
+ fields: {
230
+ id: { type: "serial" },
231
+ },
232
+ },
233
+ },
234
+ });
235
+ expect(() => normalizeRelations(models, models["user"])).toThrow('Model "user" has relation "likedPosts" with join model "nonexistent" that does not exist.');
236
+ });
237
+ test("throws error for non-existent field in relation", () => {
238
+ const models = populateModels({
239
+ models: {
240
+ user: {
241
+ fields: {
242
+ id: { type: "serial" },
243
+ },
244
+ relations: {
245
+ posts: {
246
+ type: "1:N",
247
+ model: "post",
248
+ fromField: "nonexistent",
249
+ toField: "authorId",
250
+ },
251
+ },
252
+ },
253
+ post: {
254
+ fields: {
255
+ id: { type: "serial" },
256
+ authorId: { type: "integer" },
257
+ },
258
+ },
259
+ },
260
+ });
261
+ expect(() => normalizeRelations(models, models["user"])).toThrow('Model "user" has relation with non-existent field "nonexistent".');
262
+ });
263
+ test("handles custom column names", () => {
264
+ const models = populateModels({
265
+ models: {
266
+ user: {
267
+ fields: {
268
+ id: {
269
+ type: "serial",
270
+ column: "user_identifier",
271
+ },
272
+ },
273
+ relations: {
274
+ posts: {
275
+ type: "1:N",
276
+ model: "post",
277
+ fromField: "id",
278
+ toField: "authorId",
279
+ },
280
+ },
281
+ },
282
+ post: {
283
+ fields: {
284
+ id: { type: "serial" },
285
+ authorId: {
286
+ type: "integer",
287
+ column: "created_by_user",
288
+ },
289
+ },
290
+ },
291
+ },
292
+ });
293
+ const result = normalizeRelations(models, models["user"]);
294
+ const posts = result["posts"];
295
+ expect(posts.from.columns).toEqual(["user_identifier"]);
296
+ expect(posts.to.columns).toEqual(["created_by_user"]);
297
+ });
298
+ });
@@ -0,0 +1,5 @@
1
+ import { UniqueConstraintDefinition } from "@casekit/orm2-schema";
2
+ import { NormalizedUniqueConstraintDefinition } from "#types/NormalizedUniqueConstraintDefinition.js";
3
+ import { PopulatedModelDefinition } from "#types/PopulatedModelDefinition.js";
4
+ export declare const normalizeUniqueConstraint: (model: PopulatedModelDefinition, constraint: UniqueConstraintDefinition) => NormalizedUniqueConstraintDefinition;
5
+ export declare const normalizeUniqueConstraints: (model: PopulatedModelDefinition) => NormalizedUniqueConstraintDefinition[];
@@ -0,0 +1,29 @@
1
+ import { getColumns } from "./getColumns.js";
2
+ export const normalizeUniqueConstraint = (model, constraint) => {
3
+ const columns = getColumns(model, constraint.fields);
4
+ return {
5
+ name: constraint.name ?? [model.table, ...columns, "ukey"].join("_"),
6
+ fields: constraint.fields,
7
+ columns,
8
+ where: constraint.where ?? null,
9
+ nullsNotDistinct: constraint.nullsNotDistinct ?? false,
10
+ };
11
+ };
12
+ export const normalizeUniqueConstraints = (model) => {
13
+ const columnLevelUniqueConstraints = Object.values(model.fields)
14
+ .filter((field) => field.unique)
15
+ .map((field) => normalizeUniqueConstraint(model, typeof field.unique === "boolean"
16
+ ? {
17
+ fields: [field.name],
18
+ where: null,
19
+ nullsNotDistinct: false,
20
+ }
21
+ : { fields: [field.name], ...field.unique }));
22
+ const modelLevelUniqueConstraints = model.uniqueConstraints.map((constraint) => normalizeUniqueConstraint(model, constraint));
23
+ for (const constraint of columnLevelUniqueConstraints) {
24
+ if (modelLevelUniqueConstraints.some((other) => constraint.fields.every((field) => other.fields.includes(field)))) {
25
+ throw new Error(`Duplicate unique constraint defined in model "${model.name}"`);
26
+ }
27
+ }
28
+ return [...columnLevelUniqueConstraints, ...modelLevelUniqueConstraints];
29
+ };