@casekit/orm-cli 0.0.1-release-candidate

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 (99) hide show
  1. package/build/cli.d.ts +1 -0
  2. package/build/cli.js +21 -0
  3. package/build/commands/db-drop/handler.d.ts +3 -0
  4. package/build/commands/db-drop/handler.js +15 -0
  5. package/build/commands/db-drop/options.d.ts +1 -0
  6. package/build/commands/db-drop/options.js +1 -0
  7. package/build/commands/db-drop.d.ts +6 -0
  8. package/build/commands/db-drop.js +8 -0
  9. package/build/commands/db-drop.test.d.ts +1 -0
  10. package/build/commands/db-drop.test.js +22 -0
  11. package/build/commands/db-pull/handler.d.ts +3 -0
  12. package/build/commands/db-pull/handler.js +32 -0
  13. package/build/commands/db-pull/options.d.ts +13 -0
  14. package/build/commands/db-pull/options.js +13 -0
  15. package/build/commands/db-pull/util/relationNames.d.ts +3 -0
  16. package/build/commands/db-pull/util/relationNames.js +14 -0
  17. package/build/commands/db-pull/util/relationNames.test.d.ts +1 -0
  18. package/build/commands/db-pull/util/relationNames.test.js +61 -0
  19. package/build/commands/db-pull/util/renderDefault.d.ts +3 -0
  20. package/build/commands/db-pull/util/renderDefault.js +46 -0
  21. package/build/commands/db-pull/util/renderDefault.test.d.ts +1 -0
  22. package/build/commands/db-pull/util/renderDefault.test.js +67 -0
  23. package/build/commands/db-pull/util/renderFieldDefinition.d.ts +2 -0
  24. package/build/commands/db-pull/util/renderFieldDefinition.js +50 -0
  25. package/build/commands/db-pull/util/renderFieldDefinition.test.d.ts +1 -0
  26. package/build/commands/db-pull/util/renderFieldDefinition.test.js +182 -0
  27. package/build/commands/db-pull/util/renderModel.basic.test.d.ts +1 -0
  28. package/build/commands/db-pull/util/renderModel.basic.test.js +169 -0
  29. package/build/commands/db-pull/util/renderModel.constraints.test.d.ts +1 -0
  30. package/build/commands/db-pull/util/renderModel.constraints.test.js +677 -0
  31. package/build/commands/db-pull/util/renderModel.d.ts +2 -0
  32. package/build/commands/db-pull/util/renderModel.defaultValues.test.d.ts +1 -0
  33. package/build/commands/db-pull/util/renderModel.defaultValues.test.js +518 -0
  34. package/build/commands/db-pull/util/renderModel.js +88 -0
  35. package/build/commands/db-pull/util/renderModel.relations.test.d.ts +1 -0
  36. package/build/commands/db-pull/util/renderModel.relations.test.js +880 -0
  37. package/build/commands/db-pull/util/renderModel.types.test.d.ts +1 -0
  38. package/build/commands/db-pull/util/renderModel.types.test.js +703 -0
  39. package/build/commands/db-pull/util/renderRelations.d.ts +2 -0
  40. package/build/commands/db-pull/util/renderRelations.js +55 -0
  41. package/build/commands/db-pull/util/renderRelations.test.d.ts +1 -0
  42. package/build/commands/db-pull/util/renderRelations.test.js +165 -0
  43. package/build/commands/db-pull/util/renderType.d.ts +6 -0
  44. package/build/commands/db-pull/util/renderType.js +55 -0
  45. package/build/commands/db-pull/util/renderType.test.d.ts +1 -0
  46. package/build/commands/db-pull/util/renderType.test.js +46 -0
  47. package/build/commands/db-pull.d.ts +30 -0
  48. package/build/commands/db-pull.js +8 -0
  49. package/build/commands/db-pull.test.d.ts +1 -0
  50. package/build/commands/db-pull.test.js +633 -0
  51. package/build/commands/db-push/handler.d.ts +3 -0
  52. package/build/commands/db-push/handler.js +14 -0
  53. package/build/commands/db-push/options.d.ts +2 -0
  54. package/build/commands/db-push/options.js +1 -0
  55. package/build/commands/db-push.d.ts +6 -0
  56. package/build/commands/db-push.js +8 -0
  57. package/build/commands/db-push.test.d.ts +1 -0
  58. package/build/commands/db-push.test.js +21 -0
  59. package/build/commands/generate-model/handler.d.ts +3 -0
  60. package/build/commands/generate-model/handler.js +18 -0
  61. package/build/commands/generate-model/options.d.ts +7 -0
  62. package/build/commands/generate-model/options.js +7 -0
  63. package/build/commands/generate-model.d.ts +18 -0
  64. package/build/commands/generate-model.js +8 -0
  65. package/build/commands/generate-model.test.d.ts +1 -0
  66. package/build/commands/generate-model.test.js +58 -0
  67. package/build/commands/init/handler.d.ts +3 -0
  68. package/build/commands/init/handler.js +22 -0
  69. package/build/commands/init/options.d.ts +12 -0
  70. package/build/commands/init/options.js +12 -0
  71. package/build/commands/init.d.ts +28 -0
  72. package/build/commands/init.js +8 -0
  73. package/build/commands/init.test.d.ts +1 -0
  74. package/build/commands/init.test.js +125 -0
  75. package/build/generators/generateConfigFile.d.ts +1 -0
  76. package/build/generators/generateConfigFile.js +16 -0
  77. package/build/generators/generateDbFile.d.ts +1 -0
  78. package/build/generators/generateDbFile.js +37 -0
  79. package/build/generators/generateModelFile.d.ts +1 -0
  80. package/build/generators/generateModelFile.js +11 -0
  81. package/build/generators/generateModelFile.test.d.ts +1 -0
  82. package/build/generators/generateModelFile.test.js +19 -0
  83. package/build/generators/generateModelsFile.d.ts +1 -0
  84. package/build/generators/generateModelsFile.js +8 -0
  85. package/build/index.d.ts +1 -0
  86. package/build/index.js +1 -0
  87. package/build/options.d.ts +12 -0
  88. package/build/options.js +12 -0
  89. package/build/test/setup.d.ts +1 -0
  90. package/build/test/setup.js +35 -0
  91. package/build/types.d.ts +14 -0
  92. package/build/types.js +1 -0
  93. package/build/util/createOrOverwriteFile.d.ts +1 -0
  94. package/build/util/createOrOverwriteFile.js +17 -0
  95. package/build/util/loadConfig.d.ts +3 -0
  96. package/build/util/loadConfig.js +21 -0
  97. package/build/util/prettify.d.ts +1 -0
  98. package/build/util/prettify.js +8 -0
  99. package/package.json +70 -0
@@ -0,0 +1,633 @@
1
+ import * as prompts from "@inquirer/prompts";
2
+ import { fs, vol } from "memfs";
3
+ import { randomUUID } from "node:crypto";
4
+ import pg from "pg";
5
+ import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
6
+ import yargs from "yargs";
7
+ import { orm, sql } from "@casekit/orm";
8
+ import { unindent } from "@casekit/unindent";
9
+ import { globalOptions } from "#options.js";
10
+ import * as loadConfig from "#util/loadConfig.js";
11
+ import { dbPull } from "./db-pull.js";
12
+ import { dbPush } from "./db-push.js";
13
+ describe("db pull", () => {
14
+ const schema = `orm_${randomUUID()}`;
15
+ let db;
16
+ beforeEach(async () => {
17
+ db = new pg.Client();
18
+ await db.connect();
19
+ await db.query(`CREATE SCHEMA IF NOT EXISTS "${schema}"`);
20
+ // Mock filesystem
21
+ vi.spyOn(process, "cwd").mockReturnValue("/project");
22
+ const userModelContent = unindent `
23
+ import { type ModelDefinition, sql } from "@casekit/orm";
24
+
25
+ export const user = {
26
+ fields: {
27
+ id: { type: "uuid", primaryKey: true },
28
+ createdAt: { type: "timestamp", default: sql\`now()\` },
29
+ email: { type: "text", unique: true },
30
+ name: { type: "text", nullable: true },
31
+ },
32
+ } as const satisfies ModelDefinition;
33
+ `;
34
+ vol.fromJSON({
35
+ "orm.config.ts": unindent `
36
+ import { orm } from "@casekit/orm";
37
+ import { user } from "./app/db.server/models/user.ts";
38
+
39
+ export default {
40
+ db: orm({
41
+ schema: "${schema}",
42
+ models: { user },
43
+ }),
44
+ directory: "./app/db.server",
45
+ };
46
+ `,
47
+ "app/db.server/models/user.ts": userModelContent,
48
+ }, "/project");
49
+ // Mock the specific config file path to return the memfs content
50
+ vi.doMock("/project/orm.config.ts", () => {
51
+ const userContent = fs.readFileSync("/project/app/db.server/models/user.ts", "utf8");
52
+ // Extract the user object from the file content
53
+ const userMatch = /export const user = ({[\s\S]*}) as const satisfies ModelDefinition;/.exec(userContent);
54
+ if (!userMatch) {
55
+ throw new Error("Could not extract user from model file");
56
+ }
57
+ // Dynamic evaluation to get the user object
58
+ // eslint-disable-next-line @typescript-eslint/no-implied-eval
59
+ const user = new Function("sql", "orm", `return ${userMatch[1]}`)(sql, orm);
60
+ return {
61
+ default: {
62
+ db: orm({
63
+ schema,
64
+ models: { user },
65
+ }),
66
+ directory: "./app/db.server",
67
+ },
68
+ };
69
+ });
70
+ // Import the mocked config
71
+ const path = "/project/orm.config.ts";
72
+ const { default: config } = await import(path);
73
+ await config.db.connect();
74
+ process.on("exit", async function () {
75
+ await config.db.close();
76
+ });
77
+ vi.spyOn(loadConfig, "loadConfig").mockResolvedValue(config);
78
+ });
79
+ afterEach(async () => {
80
+ await db.query(`DROP SCHEMA IF EXISTS "${schema}" CASCADE`);
81
+ await db.end();
82
+ vol.reset();
83
+ vi.restoreAllMocks();
84
+ });
85
+ test("creates model files for tables in the specified schema", async () => {
86
+ // Set up database schema
87
+ // await yargs().options(globalOptions).command(dbDrop).parseAsync("drop");
88
+ await yargs().options(globalOptions).command(dbPush).parseAsync("push");
89
+ await db.query(`
90
+ CREATE TABLE "${schema}".post (
91
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
92
+ title TEXT NOT NULL,
93
+ content TEXT,
94
+ created_at TIMESTAMP DEFAULT now()
95
+ );
96
+ `);
97
+ // Mock prompts to auto-confirm file writes
98
+ vi.spyOn(prompts, "confirm").mockResolvedValue(true);
99
+ // Run db-pull
100
+ await yargs()
101
+ .options(globalOptions)
102
+ .command(dbPull)
103
+ .parseAsync(`pull --schema ${schema}`);
104
+ // Check that post model was created
105
+ const postModel = fs.readFileSync("/project/app/db.server/models/post.ts", "utf8");
106
+ expect(postModel.trim()).toEqual(unindent `
107
+ import { type ModelDefinition, sql } from "@casekit/orm";
108
+
109
+ export const post = {
110
+ schema: "${schema}",
111
+ fields: {
112
+ id: { type: "uuid", primaryKey: true, default: sql\`gen_random_uuid()\` },
113
+ title: { type: "text" },
114
+ content: { type: "text", nullable: true },
115
+ createdAt: {
116
+ column: "created_at",
117
+ type: "timestamp without time zone",
118
+ nullable: true,
119
+ default: sql\`now()\`,
120
+ },
121
+ },
122
+ } as const satisfies ModelDefinition;
123
+ `);
124
+ });
125
+ test("handles multiple schemas", async () => {
126
+ // Set up database with multiple schemas
127
+ const schema1 = `schema1_${randomUUID()}`;
128
+ const schema2 = `schema2_${randomUUID()}`;
129
+ await db.query(`CREATE SCHEMA IF NOT EXISTS "${schema1}"`);
130
+ await db.query(`CREATE SCHEMA IF NOT EXISTS "${schema2}"`);
131
+ await db.query(`
132
+ CREATE TABLE "${schema1}".table1 (
133
+ id SERIAL PRIMARY KEY,
134
+ name TEXT NOT NULL
135
+ );
136
+ `);
137
+ await db.query(`
138
+ CREATE TABLE "${schema2}".table2 (
139
+ id SERIAL PRIMARY KEY,
140
+ description TEXT
141
+ );
142
+ `);
143
+ vi.spyOn(prompts, "confirm").mockResolvedValue(true);
144
+ // Run db-pull with multiple schemas
145
+ await yargs()
146
+ .options(globalOptions)
147
+ .command(dbPull)
148
+ .parseAsync(`pull --schema ${schema1} --schema ${schema2}`);
149
+ // Check that both models were created
150
+ const table1Model = fs.readFileSync("/project/app/db.server/models/table1.ts", "utf8");
151
+ const table2Model = fs.readFileSync("/project/app/db.server/models/table2.ts", "utf8");
152
+ expect(table1Model.trim()).toEqual(unindent `
153
+ import { type ModelDefinition } from "@casekit/orm";
154
+
155
+ export const table1 = {
156
+ schema: "${schema1}",
157
+ fields: {
158
+ id: { type: "serial", primaryKey: true },
159
+ name: { type: "text" },
160
+ },
161
+ } as const satisfies ModelDefinition;
162
+ `);
163
+ expect(table2Model.trim()).toEqual(unindent `
164
+ import { type ModelDefinition } from "@casekit/orm";
165
+
166
+ export const table2 = {
167
+ schema: "${schema2}",
168
+ fields: {
169
+ id: { type: "serial", primaryKey: true },
170
+ description: { type: "text", nullable: true },
171
+ },
172
+ } as const satisfies ModelDefinition;
173
+ `);
174
+ // Clean up
175
+ await db.query(`DROP SCHEMA "${schema1}" CASCADE`);
176
+ await db.query(`DROP SCHEMA "${schema2}" CASCADE`);
177
+ });
178
+ test("uses default schema from config when no schema specified", async () => {
179
+ // Set up database schema
180
+ await yargs().options(globalOptions).command(dbPush).parseAsync("push");
181
+ await db.query(`
182
+ CREATE TABLE "${schema}".product (
183
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
184
+ name TEXT NOT NULL,
185
+ price NUMERIC(10,2)
186
+ );
187
+ `);
188
+ vi.spyOn(prompts, "confirm").mockResolvedValue(true);
189
+ // Run db-pull without specifying schema
190
+ await yargs().options(globalOptions).command(dbPull).parseAsync("pull");
191
+ // Check that product model was created
192
+ const productModel = fs.readFileSync("/project/app/db.server/models/product.ts", "utf8");
193
+ expect(productModel.trim()).toEqual(unindent `
194
+ import { type ModelDefinition, sql } from "@casekit/orm";
195
+
196
+ export const product = {
197
+ schema: "${schema}",
198
+ fields: {
199
+ id: { type: "uuid", primaryKey: true, default: sql\`gen_random_uuid()\` },
200
+ name: { type: "text" },
201
+ price: { type: "numeric", nullable: true },
202
+ },
203
+ } as const satisfies ModelDefinition;
204
+ `);
205
+ });
206
+ test("overwrites existing files with --force flag", async () => {
207
+ // Set up database schema
208
+ await yargs().options(globalOptions).command(dbPush).parseAsync("push");
209
+ // Create existing file with different content
210
+ vol.fromJSON({
211
+ "app/db.server/models/user.ts": unindent `
212
+ // This is the old user model that should be overwritten
213
+ export const user = { old: true };
214
+ `,
215
+ }, "/project");
216
+ // Run db-pull with --force
217
+ await yargs()
218
+ .options(globalOptions)
219
+ .command(dbPull)
220
+ .parseAsync(`pull --schema ${schema} --force`);
221
+ // Check that user model was overwritten
222
+ const userModel = fs.readFileSync("/project/app/db.server/models/user.ts", "utf8");
223
+ // Verify it doesn't contain the old content
224
+ expect(userModel).not.toContain("old: true");
225
+ expect(userModel.trim()).toEqual(unindent `
226
+ import { type ModelDefinition, sql } from "@casekit/orm";
227
+
228
+ export const user = {
229
+ schema: "${schema}",
230
+ fields: {
231
+ id: { type: "uuid", primaryKey: true },
232
+ createdAt: { type: "timestamp without time zone", default: sql\`now()\` },
233
+ email: { type: "text", unique: true },
234
+ name: { type: "text", nullable: true },
235
+ },
236
+ } as const satisfies ModelDefinition;
237
+ `);
238
+ });
239
+ test("handles tables with multi-column primary keys", async () => {
240
+ const testSchema = `testschema_${randomUUID()}`;
241
+ await db.query(`CREATE SCHEMA IF NOT EXISTS "${testSchema}"`);
242
+ await db.query(`
243
+ CREATE TABLE "${testSchema}".user_role (
244
+ user_id UUID NOT NULL,
245
+ role_id UUID NOT NULL,
246
+ assigned_at TIMESTAMP DEFAULT now(),
247
+ PRIMARY KEY (user_id, role_id)
248
+ );
249
+ `);
250
+ vi.spyOn(prompts, "confirm").mockResolvedValue(true);
251
+ await yargs()
252
+ .options(globalOptions)
253
+ .command(dbPull)
254
+ .parseAsync(`pull --schema ${testSchema}`);
255
+ const model = fs.readFileSync("/project/app/db.server/models/userRole.ts", "utf8");
256
+ expect(model.trim()).toEqual(unindent `
257
+ import { type ModelDefinition, sql } from "@casekit/orm";
258
+
259
+ export const userRole = {
260
+ schema: "${testSchema}",
261
+ table: "user_role",
262
+ fields: {
263
+ userId: { column: "user_id", type: "uuid" },
264
+ roleId: { column: "role_id", type: "uuid" },
265
+ assignedAt: {
266
+ column: "assigned_at",
267
+ type: "timestamp without time zone",
268
+ nullable: true,
269
+ default: sql\`now()\`,
270
+ },
271
+ },
272
+ primaryKey: ["userId", "roleId"],
273
+ } as const satisfies ModelDefinition;
274
+ `);
275
+ await db.query(`DROP SCHEMA "${testSchema}" CASCADE`);
276
+ });
277
+ test("handles empty schema gracefully", async () => {
278
+ const emptySchema = `emptyschema_${randomUUID()}`;
279
+ await db.query(`CREATE SCHEMA IF NOT EXISTS "${emptySchema}"`);
280
+ const result = await yargs()
281
+ .options(globalOptions)
282
+ .command(dbPull)
283
+ .parseAsync(`pull --schema ${emptySchema}`);
284
+ // Should complete without error
285
+ expect(result).toBeDefined();
286
+ await db.query(`DROP SCHEMA "${emptySchema}"`);
287
+ });
288
+ test("handles all supported data types (kitchen sink test)", async () => {
289
+ const testSchema = `kitchensink_${randomUUID()}`;
290
+ const modelFileContent = unindent `
291
+ // This file will be overwritten by the pull command
292
+ import { type ModelDefinition, sql } from "@casekit/orm";
293
+
294
+ export const kitchenSink = {
295
+ schema: "${testSchema}",
296
+ fields: {
297
+ id: { type: "serial", primaryKey: true },
298
+ charField: { type: "character", default: "A" },
299
+ characterField: { type: "character", default: "B" },
300
+ characterNField: { type: "character", default: "Test Character" },
301
+ varcharField: {
302
+ type: "character varying",
303
+ default: sql\`'varchar test'::character varying\`,
304
+ },
305
+ varcharNField: {
306
+ type: "character varying",
307
+ default: sql\`'varchar24 test'::character varying\`,
308
+ },
309
+ textField: { type: "text", default: "This is a text field" },
310
+ bitField: { type: "bit", default: sql\`'1'::bit(1)\` },
311
+ bitNField: { type: "bit", default: sql\`'101'::bit(3)\` },
312
+ bitVaryingField: {
313
+ type: "bit varying",
314
+ default: sql\`'11010'::bit varying\`,
315
+ },
316
+ bitVaryingNField: {
317
+ type: "bit varying",
318
+ default: sql\`'110'::bit varying(3)\`,
319
+ },
320
+ numericField: { type: "numeric", default: 123.45 },
321
+ numericPrecisionField: { type: "numeric", default: 999.99 },
322
+ integerField: { type: "integer", default: 42 },
323
+ bigintField: { type: "bigint", default: "9223372036854775807" },
324
+ smallintField: { type: "smallint", default: 32767 },
325
+ serialField: { type: "serial" },
326
+ bigserialField: { type: "bigserial" },
327
+ smallserialField: { type: "smallserial" },
328
+ doublePrecisionField: {
329
+ type: "double precision",
330
+ default: 3.141592653589793,
331
+ },
332
+ realField: { type: "real", default: 2.71828 },
333
+ moneyField: { type: "money", default: sql\`'$1,234.56'::money\` },
334
+ booleanField: { type: "boolean", default: true },
335
+ uuidField: { type: "uuid", default: sql\`gen_random_uuid()\` },
336
+ dateField: { type: "date", default: sql\`CURRENT_DATE\` },
337
+ timeField: {
338
+ type: "time without time zone",
339
+ default: sql\`CURRENT_TIME\`,
340
+ },
341
+ timeWithPrecisionField: {
342
+ type: "time without time zone",
343
+ default: sql\`CURRENT_TIME\`,
344
+ },
345
+ timetzField: {
346
+ type: "time with time zone",
347
+ default: sql\`CURRENT_TIME\`,
348
+ },
349
+ timeWithTzField: {
350
+ type: "time with time zone",
351
+ default: sql\`CURRENT_TIME\`,
352
+ },
353
+ timestampField: {
354
+ type: "timestamp without time zone",
355
+ default: sql\`CURRENT_TIMESTAMP\`,
356
+ },
357
+ timestampWithPrecisionField: {
358
+ type: "timestamp without time zone",
359
+ default: sql\`CURRENT_TIMESTAMP\`,
360
+ },
361
+ timestamptzField: {
362
+ type: "timestamp with time zone",
363
+ default: sql\`CURRENT_TIMESTAMP\`,
364
+ },
365
+ timestampWithTzField: {
366
+ type: "timestamp with time zone",
367
+ default: sql\`CURRENT_TIMESTAMP\`,
368
+ },
369
+ jsonField: { type: "json", default: sql\`'{"key": "value"}'::json\` },
370
+ jsonbField: { type: "jsonb", default: sql\`'{"key": "value"}'::jsonb\` },
371
+ lineField: { type: "line", default: sql\`'{1,2,3}'::line\` },
372
+ lsegField: { type: "lseg", default: sql\`'[(0,0),(1,1)]'::lseg\` },
373
+ boxField: { type: "box", default: sql\`'(1,1),(0,0)'::box\` },
374
+ polygonField: {
375
+ type: "polygon",
376
+ default: sql\`'((0,0),(1,0),(1,1),(0,1))'::polygon\`,
377
+ },
378
+ cidrField: { type: "cidr", default: sql\`'192.168.1.0/24'::cidr\` },
379
+ inetField: { type: "inet", default: sql\`'192.168.1.1'::inet\` },
380
+ macaddrField: {
381
+ type: "macaddr",
382
+ default: sql\`'08:00:2b:01:02:03'::macaddr\`,
383
+ },
384
+ macaddr8Field: {
385
+ type: "macaddr8",
386
+ default: sql\`'08:00:2b:01:02:03:04:05'::macaddr8\`,
387
+ },
388
+ byteaField: { type: "bytea", default: sql\`'\\x41'::bytea\` },
389
+ xmlField: {
390
+ type: "xml",
391
+ default: sql\`'<root><element>value</element></root>'::xml\`,
392
+ },
393
+ pathField: { type: "path", default: sql\`'((0,0),(1,1),(2,0))'::path\` },
394
+ tsqueryField: {
395
+ type: "tsquery",
396
+ default: sql\`'''fat'' & ''rat'''::tsquery\`,
397
+ },
398
+ tsvectorField: {
399
+ type: "tsvector",
400
+ default: sql\`'''cat'':3 ''fat'':2,4 ''rat'':5A,6B'::tsvector\`,
401
+ },
402
+ int4RangeField: {
403
+ column: "int4rangeField",
404
+ type: "int4range",
405
+ default: sql\`'[1,10)'::int4range\`,
406
+ },
407
+ int8RangeField: {
408
+ column: "int8rangeField",
409
+ type: "int8range",
410
+ default: sql\`'[100,200)'::int8range\`,
411
+ },
412
+ numrangeField: {
413
+ type: "numrange",
414
+ default: sql\`'[1.1,2.2)'::numrange\`,
415
+ },
416
+ tsrangeField: {
417
+ type: "tsrange",
418
+ default: sql\`'["2023-01-01 00:00:00","2023-01-02 00:00:00")'::tsrange\`,
419
+ },
420
+ tstzrangeField: {
421
+ type: "tstzrange",
422
+ default: sql\`'["2023-01-01 00:00:00+00","2023-01-02 00:00:00+00")'::tstzrange\`,
423
+ },
424
+ daterangeField: {
425
+ type: "daterange",
426
+ default: sql\`'[2023-01-01,2023-01-02)'::daterange\`,
427
+ },
428
+ int2VectorField: {
429
+ column: "int2vectorField",
430
+ type: "int2vector",
431
+ default: sql\`'1 2 3'::int2vector\`,
432
+ },
433
+ oidField: { type: "oid", default: sql\`'12345'::oid\` },
434
+ pglsnField: { type: "pg_lsn", default: sql\`'16/B374D848'::pg_lsn\` },
435
+ regclassField: { type: "regclass", default: sql\`'pg_class'::regclass\` },
436
+ regnamespaceField: {
437
+ type: "regnamespace",
438
+ default: sql\`'public'::regnamespace\`,
439
+ },
440
+ regprocField: { type: "regproc", default: sql\`'now'::regproc\` },
441
+ regprocedureField: {
442
+ type: "regprocedure",
443
+ default: sql\`'abs(integer)'::regprocedure\`,
444
+ },
445
+ regroleField: {
446
+ type: "regrole",
447
+ default: sql\`(CURRENT_USER)::regrole\`,
448
+ },
449
+ regtypeField: { type: "regtype", default: sql\`'integer'::regtype\` },
450
+ tidField: { type: "tid", default: sql\`'(0,1)'::tid\` },
451
+ xidField: { type: "xid", default: sql\`'123'::xid\` },
452
+ txidSnapshotField: {
453
+ type: "txid_snapshot",
454
+ default: sql\`'10:20:10,14,15'::txid_snapshot\`,
455
+ },
456
+ arrayField: {
457
+ type: "text[]",
458
+ default: sql\`'{item1,item2,item3}'::text[]\`,
459
+ },
460
+ multiArrayField: {
461
+ type: "text[][][]",
462
+ default: sql\`ARRAY[ARRAY[ARRAY['a'::text, 'b'::text]], ARRAY[ARRAY['c'::text, 'd'::text]]]\`,
463
+ },
464
+ },
465
+ } as const satisfies ModelDefinition;
466
+ `;
467
+ vol.fromJSON({
468
+ "orm.config.ts": unindent `
469
+ import { orm } from "@casekit/orm";
470
+ import { kitchenSink } from "./app/db.server/models/kitchenSink.ts";
471
+
472
+ export default {
473
+ db: orm({
474
+ schema: "${testSchema}",
475
+ models: { kitchenSink },
476
+ }),
477
+ directory: "./app/db.server",
478
+ };
479
+ `,
480
+ "app/db.server/models/kitchenSink.ts": modelFileContent,
481
+ }, "/project");
482
+ // Mock the specific config file path to return the memfs content
483
+ vi.doMock("/project/orm.config.ts", () => {
484
+ const kitchenSinkContent = fs.readFileSync("/project/app/db.server/models/kitchenSink.ts", "utf8");
485
+ // Extract the kitchenSink object from the file content
486
+ const kitchenSinkMatch = /export const kitchenSink = ({[\s\S]*}) as const satisfies ModelDefinition;/.exec(kitchenSinkContent);
487
+ if (!kitchenSinkMatch) {
488
+ throw new Error("Could not extract kitchenSink from model file");
489
+ }
490
+ // doing this is really annoying but it's the only way i've figured out
491
+ // to get the kitchenSink object into the config without having to redefine it,
492
+ // as i haven't been able to figure out how to make `import` use files from the memfs
493
+ // mocked filesystem.
494
+ // eslint-disable-next-line @typescript-eslint/no-implied-eval
495
+ const kitchenSink = new Function("sql", "orm", `return ${kitchenSinkMatch[1]}`)(sql, orm);
496
+ return {
497
+ default: {
498
+ db: orm({
499
+ schema: "${testSchema}",
500
+ models: { kitchenSink },
501
+ }),
502
+ directory: "./app/db.server",
503
+ },
504
+ };
505
+ });
506
+ await db.query(`CREATE SCHEMA IF NOT EXISTS "${testSchema}"`);
507
+ // Import the mocked config
508
+ const path = "/project/orm.config.ts";
509
+ const { default: config } = await import(path);
510
+ await config.db.connect();
511
+ process.on("exit", async function () {
512
+ await config.db.close();
513
+ });
514
+ vi.spyOn(loadConfig, "loadConfig").mockResolvedValue(config);
515
+ // Push the kitchen sink model to the database
516
+ await yargs()
517
+ .options(globalOptions)
518
+ .command(dbPush)
519
+ .parseAsync(`push --schema ${testSchema}`);
520
+ // Mock prompts to auto-confirm file writes
521
+ vi.spyOn(prompts, "confirm").mockResolvedValue(true);
522
+ // Pull the schema back
523
+ await yargs()
524
+ .options(globalOptions)
525
+ .command(dbPull)
526
+ .parseAsync(`pull --schema ${testSchema} --force`);
527
+ // Read the generated model file
528
+ const pulledModel = fs.readFileSync("/project/app/db.server/models/kitchenSink.ts", { encoding: "utf8" });
529
+ // Verify the pulled model has actually been overwritten
530
+ expect(pulledModel).not.toMatch(/This file will be overwritten by the pull command/);
531
+ expect(pulledModel.trim()).toEqual(modelFileContent
532
+ .trim()
533
+ .replace("// This file will be overwritten by the pull command\n", ""));
534
+ // Push the pulled model back to the database
535
+ await db.query(`DROP SCHEMA "${testSchema}" CASCADE`);
536
+ await db.query(`CREATE SCHEMA IF NOT EXISTS "${testSchema}"`);
537
+ await yargs()
538
+ .options(globalOptions)
539
+ .command(dbPush)
540
+ .parseAsync(`push --schema ${testSchema}`);
541
+ // Verify the table was created successfully by querying it
542
+ const result = await db.query(`SELECT column_name, data_type, column_default
543
+ FROM information_schema.columns
544
+ WHERE table_schema = $1 AND table_name = $2
545
+ ORDER BY ordinal_position`, [testSchema, "kitchenSink"]);
546
+ expect(result.rows.length).toEqual(68);
547
+ await db.query(`DROP SCHEMA "${testSchema}" CASCADE`);
548
+ });
549
+ test("handles foreign key relations (many-to-one and one-to-many)", async () => {
550
+ const testSchema = `relations_${randomUUID()}`;
551
+ await db.query(`CREATE SCHEMA IF NOT EXISTS "${testSchema}"`);
552
+ // Create tables with foreign key relationships directly in the database
553
+ await db.query(`
554
+ CREATE TABLE "${testSchema}".user (
555
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
556
+ name TEXT NOT NULL,
557
+ email TEXT UNIQUE NOT NULL
558
+ );
559
+ `);
560
+ await db.query(`
561
+ CREATE TABLE "${testSchema}".post (
562
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
563
+ title TEXT NOT NULL,
564
+ content TEXT,
565
+ author_id UUID NOT NULL,
566
+ created_at TIMESTAMP DEFAULT now(),
567
+ CONSTRAINT post_author_id_fkey
568
+ FOREIGN KEY (author_id) REFERENCES "${testSchema}".user(id)
569
+ );
570
+ `);
571
+ vi.spyOn(prompts, "confirm").mockResolvedValue(true);
572
+ // Run db-pull to generate model files
573
+ await yargs()
574
+ .options(globalOptions)
575
+ .command(dbPull)
576
+ .parseAsync(`pull --schema ${testSchema}`);
577
+ // Check that both model files were created
578
+ const userModel = fs.readFileSync("/project/app/db.server/models/user.ts", "utf8");
579
+ const postModel = fs.readFileSync("/project/app/db.server/models/post.ts", "utf8");
580
+ expect(userModel.trim()).toEqual(unindent `
581
+ import { type ModelDefinition, sql } from "@casekit/orm";
582
+
583
+ export const user = {
584
+ schema: "${testSchema}",
585
+ fields: {
586
+ id: { type: "uuid", primaryKey: true, default: sql\`gen_random_uuid()\` },
587
+ name: { type: "text" },
588
+ email: { type: "text", unique: true },
589
+ },
590
+ relations: {
591
+ authorPosts: {
592
+ type: "1:N",
593
+ model: "post",
594
+ fromField: "id",
595
+ toField: "authorId",
596
+ },
597
+ },
598
+ } as const satisfies ModelDefinition;
599
+ `);
600
+ expect(postModel.trim()).toEqual(unindent `
601
+ import { type ModelDefinition, sql } from "@casekit/orm";
602
+
603
+ export const post = {
604
+ schema: "${testSchema}",
605
+ fields: {
606
+ id: { type: "uuid", primaryKey: true, default: sql\`gen_random_uuid()\` },
607
+ title: { type: "text" },
608
+ content: { type: "text", nullable: true },
609
+ authorId: {
610
+ column: "author_id",
611
+ type: "uuid",
612
+ references: { model: "user", field: "id" },
613
+ },
614
+ createdAt: {
615
+ column: "created_at",
616
+ type: "timestamp without time zone",
617
+ nullable: true,
618
+ default: sql\`now()\`,
619
+ },
620
+ },
621
+ relations: {
622
+ author: {
623
+ type: "N:1",
624
+ model: "user",
625
+ fromField: "authorId",
626
+ toField: "id",
627
+ },
628
+ },
629
+ } as const satisfies ModelDefinition;
630
+ `);
631
+ await db.query(`DROP SCHEMA "${testSchema}" CASCADE`);
632
+ });
633
+ });
@@ -0,0 +1,3 @@
1
+ import { Handler } from "#types.js";
2
+ import { builder } from "./options.js";
3
+ export declare const handler: Handler<typeof builder>;
@@ -0,0 +1,14 @@
1
+ import { migrate } from "@casekit/orm-migrate";
2
+ import { loadConfig } from "#util/loadConfig.js";
3
+ export const handler = async (opts) => {
4
+ const { db } = await loadConfig(opts);
5
+ try {
6
+ await migrate.push(db);
7
+ console.log("✅ Done");
8
+ }
9
+ catch (e) {
10
+ console.error("Error pushing schema to database", e);
11
+ process.exitCode = 1;
12
+ throw e;
13
+ }
14
+ };
@@ -0,0 +1,2 @@
1
+ import { Builder } from "#types.js";
2
+ export declare const builder: Builder;
@@ -0,0 +1 @@
1
+ export const builder = {};
@@ -0,0 +1,6 @@
1
+ export declare const dbPush: {
2
+ command: string;
3
+ desc: string;
4
+ builder: import("../types.js").Builder;
5
+ handler: import("../types.js").Handler<import("../types.js").Builder>;
6
+ };
@@ -0,0 +1,8 @@
1
+ import { handler } from "./db-push/handler.js";
2
+ import { builder } from "./db-push/options.js";
3
+ export const dbPush = {
4
+ command: "push",
5
+ desc: "Forcibly push the current schema to the database",
6
+ builder,
7
+ handler,
8
+ };