@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,241 @@
1
+ import { snakeCase } from "es-toolkit";
2
+ import { describe, expect, test } from "vitest";
3
+ import { sql } from "@casekit/sql";
4
+ import { normalizeUniqueConstraints } from "./normalizeUniqueConstraints.js";
5
+ import { populateModels } from "./populateModels.js";
6
+ describe("normalizeUniqueConstraints", () => {
7
+ test("handles both column-level and model-level unique constraints", () => {
8
+ const models = populateModels({
9
+ naming: { column: snakeCase },
10
+ models: {
11
+ user: {
12
+ fields: {
13
+ id: { type: "serial", primaryKey: true },
14
+ email: { type: "text", unique: true },
15
+ username: { type: "text" },
16
+ },
17
+ uniqueConstraints: [
18
+ {
19
+ fields: ["username"],
20
+ },
21
+ ],
22
+ },
23
+ },
24
+ });
25
+ const result = normalizeUniqueConstraints(models["user"]);
26
+ expect(result).toEqual([
27
+ {
28
+ name: "user_email_ukey",
29
+ fields: ["email"],
30
+ columns: ["email"],
31
+ where: null,
32
+ nullsNotDistinct: false,
33
+ },
34
+ {
35
+ name: "user_username_ukey",
36
+ fields: ["username"],
37
+ columns: ["username"],
38
+ where: null,
39
+ nullsNotDistinct: false,
40
+ },
41
+ ]);
42
+ });
43
+ test("handles complex column-level unique constraints", () => {
44
+ const models = populateModels({
45
+ naming: { column: snakeCase },
46
+ models: {
47
+ user: {
48
+ fields: {
49
+ id: { type: "serial", primaryKey: true },
50
+ email: {
51
+ type: "text",
52
+ unique: {
53
+ where: sql `deleted_at IS NULL`,
54
+ nullsNotDistinct: true,
55
+ },
56
+ },
57
+ },
58
+ },
59
+ },
60
+ });
61
+ const result = normalizeUniqueConstraints(models["user"]);
62
+ expect(result).toEqual([
63
+ {
64
+ name: "user_email_ukey",
65
+ fields: ["email"],
66
+ columns: ["email"],
67
+ where: sql `deleted_at IS NULL`,
68
+ nullsNotDistinct: true,
69
+ },
70
+ ]);
71
+ });
72
+ test("handles fields with unique: false", () => {
73
+ const models = populateModels({
74
+ models: {
75
+ user: {
76
+ fields: {
77
+ id: { type: "serial", primaryKey: true },
78
+ email: { type: "text", unique: false },
79
+ },
80
+ },
81
+ },
82
+ });
83
+ const result = normalizeUniqueConstraints(models["user"]);
84
+ expect(result).toEqual([]);
85
+ });
86
+ test("handles multiple model-level unique constraints", () => {
87
+ const models = populateModels({
88
+ naming: { column: snakeCase },
89
+ models: {
90
+ user: {
91
+ fields: {
92
+ id: { type: "serial", primaryKey: true },
93
+ firstName: { type: "text" },
94
+ lastName: { type: "text" },
95
+ email: { type: "text" },
96
+ },
97
+ uniqueConstraints: [
98
+ {
99
+ fields: ["firstName", "lastName"],
100
+ nullsNotDistinct: true,
101
+ },
102
+ {
103
+ name: "unique_email",
104
+ fields: ["email"],
105
+ where: sql `deleted_at IS NULL`,
106
+ },
107
+ ],
108
+ },
109
+ },
110
+ });
111
+ const result = normalizeUniqueConstraints(models["user"]);
112
+ expect(result).toEqual([
113
+ {
114
+ name: "user_first_name_last_name_ukey",
115
+ fields: ["firstName", "lastName"],
116
+ columns: ["first_name", "last_name"],
117
+ where: null,
118
+ nullsNotDistinct: true,
119
+ },
120
+ {
121
+ name: "unique_email",
122
+ fields: ["email"],
123
+ columns: ["email"],
124
+ where: sql `deleted_at IS NULL`,
125
+ nullsNotDistinct: false,
126
+ },
127
+ ]);
128
+ });
129
+ test("handles custom column names", () => {
130
+ const models = populateModels({
131
+ models: {
132
+ user: {
133
+ fields: {
134
+ id: { type: "serial", primaryKey: true },
135
+ email: {
136
+ type: "text",
137
+ column: "user_email",
138
+ unique: true,
139
+ },
140
+ },
141
+ },
142
+ },
143
+ });
144
+ const result = normalizeUniqueConstraints(models["user"]);
145
+ expect(result).toEqual([
146
+ {
147
+ name: "user_user_email_ukey",
148
+ fields: ["email"],
149
+ columns: ["user_email"],
150
+ where: null,
151
+ nullsNotDistinct: false,
152
+ },
153
+ ]);
154
+ });
155
+ test("returns empty array when no unique constraints exist", () => {
156
+ const models = populateModels({
157
+ models: {
158
+ user: {
159
+ fields: {
160
+ id: { type: "serial", primaryKey: true },
161
+ email: { type: "text" },
162
+ },
163
+ },
164
+ },
165
+ });
166
+ const result = normalizeUniqueConstraints(models["user"]);
167
+ expect(result).toEqual([]);
168
+ });
169
+ test("throws error for non-existent fields in model-level constraints", () => {
170
+ const models = populateModels({
171
+ models: {
172
+ user: {
173
+ fields: {
174
+ id: { type: "serial", primaryKey: true },
175
+ },
176
+ uniqueConstraints: [
177
+ {
178
+ fields: ["nonexistent"],
179
+ },
180
+ ],
181
+ },
182
+ },
183
+ });
184
+ expect(() => normalizeUniqueConstraints(models["user"])).toThrow('Field "nonexistent" not found in model "user"');
185
+ });
186
+ test("handles mix of boolean and object unique constraints at column level", () => {
187
+ const models = populateModels({
188
+ naming: { column: snakeCase },
189
+ models: {
190
+ user: {
191
+ fields: {
192
+ id: { type: "serial", primaryKey: true },
193
+ email: { type: "text", unique: true },
194
+ username: {
195
+ type: "text",
196
+ unique: {
197
+ where: sql `active = true`,
198
+ nullsNotDistinct: true,
199
+ },
200
+ },
201
+ },
202
+ },
203
+ },
204
+ });
205
+ const result = normalizeUniqueConstraints(models["user"]);
206
+ expect(result).toEqual([
207
+ {
208
+ name: "user_email_ukey",
209
+ fields: ["email"],
210
+ columns: ["email"],
211
+ where: null,
212
+ nullsNotDistinct: false,
213
+ },
214
+ {
215
+ name: "user_username_ukey",
216
+ fields: ["username"],
217
+ columns: ["username"],
218
+ where: sql `active = true`,
219
+ nullsNotDistinct: true,
220
+ },
221
+ ]);
222
+ });
223
+ test("throws error for duplicate unique constraints", () => {
224
+ const models = populateModels({
225
+ models: {
226
+ user: {
227
+ fields: {
228
+ id: { type: "serial", primaryKey: true },
229
+ email: { type: "text", unique: true },
230
+ },
231
+ uniqueConstraints: [
232
+ {
233
+ fields: ["email"], // This duplicates the column-level unique constraint
234
+ },
235
+ ],
236
+ },
237
+ },
238
+ });
239
+ expect(() => normalizeUniqueConstraints(models["user"])).toThrow('Duplicate unique constraint defined in model "user"');
240
+ });
241
+ });
@@ -0,0 +1,3 @@
1
+ import { Config, FieldDefinition } from "@casekit/orm2-schema";
2
+ import { PopulatedFieldDefinition } from "#types/PopulatedFieldDefinition.js";
3
+ export declare const populateField: (config: Config, definition: FieldDefinition, name: string) => PopulatedFieldDefinition;
@@ -0,0 +1,15 @@
1
+ import { defaultZodSchema } from "./defaultZodSchema.js";
2
+ export const populateField = (config, definition, name) => {
3
+ return {
4
+ name,
5
+ column: definition.column ?? config.naming?.column?.(name) ?? name,
6
+ type: definition.type,
7
+ zodSchema: definition.zodSchema ?? defaultZodSchema(definition.type),
8
+ default: definition.default ?? null,
9
+ references: definition.references ?? null,
10
+ nullable: definition.nullable ?? false,
11
+ unique: definition.unique ?? false,
12
+ provided: definition.provided ?? false,
13
+ primaryKey: definition.primaryKey ?? false,
14
+ };
15
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,198 @@
1
+ import { snakeCase } from "es-toolkit";
2
+ import { describe, expect, test } from "vitest";
3
+ import { ZodSchema, z } from "zod";
4
+ import { populateField } from "./populateField.js";
5
+ describe("populateField", () => {
6
+ test("populates minimal field definition with defaults", () => {
7
+ const result = populateField({
8
+ models: {
9
+ user: {
10
+ fields: {
11
+ name: { type: "text" },
12
+ },
13
+ },
14
+ },
15
+ }, { type: "text" }, "name");
16
+ expect(result).toEqual({
17
+ name: "name",
18
+ column: "name",
19
+ type: "text",
20
+ zodSchema: expect.any(ZodSchema),
21
+ default: null,
22
+ references: null,
23
+ nullable: false,
24
+ unique: false,
25
+ primaryKey: false,
26
+ provided: false,
27
+ });
28
+ });
29
+ test("applies naming function to column name when provided", () => {
30
+ const result = populateField({
31
+ naming: { column: snakeCase },
32
+ models: {
33
+ user: {
34
+ fields: {
35
+ fullName: { type: "text" },
36
+ },
37
+ },
38
+ },
39
+ }, { type: "text" }, "fullName");
40
+ expect(result.column).toBe("full_name");
41
+ });
42
+ test("uses explicit column name over naming function", () => {
43
+ const result = populateField({
44
+ naming: { column: snakeCase },
45
+ models: {
46
+ user: {
47
+ fields: {
48
+ fullName: { type: "text", column: "explicit_name" },
49
+ },
50
+ },
51
+ },
52
+ }, { type: "text", column: "explicit_name" }, "fullName");
53
+ expect(result.column).toBe("explicit_name");
54
+ });
55
+ test("preserves custom zodSchema", () => {
56
+ const customSchema = z.string().email();
57
+ const result = populateField({
58
+ models: {
59
+ user: {
60
+ fields: {
61
+ email: { type: "text", zodSchema: customSchema },
62
+ },
63
+ },
64
+ },
65
+ }, { type: "text", zodSchema: customSchema }, "email");
66
+ expect(result.zodSchema).toEqual(customSchema);
67
+ });
68
+ test("preserves all provided values", () => {
69
+ const result = populateField({
70
+ models: {
71
+ user: {
72
+ fields: {
73
+ id: {
74
+ type: "integer",
75
+ column: "custom_id",
76
+ zodSchema: z.number().min(1),
77
+ default: 42,
78
+ references: { model: "other", field: "id" },
79
+ nullable: true,
80
+ unique: true,
81
+ primaryKey: true,
82
+ provided: true,
83
+ },
84
+ },
85
+ },
86
+ other: {
87
+ fields: {
88
+ id: { type: "serial", primaryKey: true },
89
+ },
90
+ },
91
+ },
92
+ }, {
93
+ type: "integer",
94
+ column: "custom_id",
95
+ zodSchema: z.number().min(1),
96
+ default: 42,
97
+ references: { model: "other", field: "id" },
98
+ nullable: true,
99
+ unique: true,
100
+ primaryKey: true,
101
+ provided: true,
102
+ }, "id");
103
+ expect(result).toEqual({
104
+ name: "id",
105
+ type: "integer",
106
+ column: "custom_id",
107
+ zodSchema: expect.any(ZodSchema),
108
+ default: 42,
109
+ references: { model: "other", field: "id" },
110
+ nullable: true,
111
+ unique: true,
112
+ primaryKey: true,
113
+ provided: true,
114
+ });
115
+ });
116
+ test("handles array types", () => {
117
+ const result = populateField({
118
+ models: {
119
+ post: {
120
+ fields: {
121
+ tags: { type: "text[]" },
122
+ },
123
+ },
124
+ },
125
+ }, { type: "text[]" }, "tags");
126
+ expect(result).toEqual({
127
+ name: "tags",
128
+ column: "tags",
129
+ type: "text[]",
130
+ zodSchema: expect.any(ZodSchema),
131
+ default: null,
132
+ references: null,
133
+ nullable: false,
134
+ unique: false,
135
+ primaryKey: false,
136
+ provided: false,
137
+ });
138
+ });
139
+ test("handles nullable fields", () => {
140
+ const result = populateField({
141
+ models: {
142
+ post: {
143
+ fields: {
144
+ description: { type: "text", nullable: true },
145
+ },
146
+ },
147
+ },
148
+ }, { type: "text", nullable: true }, "description");
149
+ expect(result.nullable).toBe(true);
150
+ });
151
+ test("handles fields with defaults", () => {
152
+ const result = populateField({
153
+ models: {
154
+ user: {
155
+ fields: {
156
+ active: { type: "boolean", default: false },
157
+ },
158
+ },
159
+ },
160
+ }, { type: "boolean", default: false }, "active");
161
+ expect(result.default).toBe(false);
162
+ });
163
+ test("handles fields with foreign keys", () => {
164
+ const result = populateField({
165
+ models: {
166
+ user: {
167
+ fields: {
168
+ id: { type: "serial", primaryKey: true },
169
+ },
170
+ },
171
+ post: {
172
+ fields: {
173
+ userId: {
174
+ type: "integer",
175
+ references: {
176
+ model: "user",
177
+ field: "id",
178
+ onDelete: "CASCADE",
179
+ },
180
+ },
181
+ },
182
+ },
183
+ },
184
+ }, {
185
+ type: "integer",
186
+ references: {
187
+ model: "user",
188
+ field: "id",
189
+ onDelete: "CASCADE",
190
+ },
191
+ }, "userId");
192
+ expect(result.references).toEqual({
193
+ model: "user",
194
+ field: "id",
195
+ onDelete: "CASCADE",
196
+ });
197
+ });
198
+ });
@@ -0,0 +1,3 @@
1
+ import { Config } from "@casekit/orm2-schema";
2
+ import { PopulatedModelDefinition } from "#types/PopulatedModelDefinition.js";
3
+ export declare const populateModels: (config: Config) => Record<string, PopulatedModelDefinition>;
@@ -0,0 +1,16 @@
1
+ import { mapValues } from "es-toolkit";
2
+ import { populateField } from "./populateField.js";
3
+ export const populateModels = (config) => {
4
+ return mapValues(config.models, (model, name) => {
5
+ return {
6
+ name,
7
+ schema: model.schema ?? config.schema ?? "public",
8
+ table: model.table ?? config.naming?.table?.(name) ?? name,
9
+ primaryKey: model.primaryKey ?? null,
10
+ uniqueConstraints: model.uniqueConstraints ?? [],
11
+ foreignKeys: model.foreignKeys ?? [],
12
+ relations: model.relations ?? {},
13
+ fields: mapValues(model.fields, (field, name) => populateField(config, field, name)),
14
+ };
15
+ });
16
+ };
@@ -0,0 +1 @@
1
+ export {};