@casekit/orm2-cli 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/build/cli.js +2 -0
  2. package/build/commands/db-drop/handler.js +0 -1
  3. package/build/commands/db-pull/handler.js +2 -3
  4. package/build/commands/db-pull.test.js +8 -0
  5. package/build/commands/db-push/handler.js +0 -1
  6. package/build/commands/generate-model/handler.d.ts +3 -0
  7. package/build/commands/generate-model/handler.js +19 -0
  8. package/build/commands/generate-model/options.d.ts +18 -0
  9. package/build/commands/generate-model/options.js +18 -0
  10. package/build/commands/generate-model.d.ts +40 -0
  11. package/build/commands/generate-model.js +8 -0
  12. package/build/commands/generate-model.test.js +63 -0
  13. package/build/commands/init/handler.js +4 -15
  14. package/build/commands/init.test.js +48 -23
  15. package/build/{commands/init/util → generators}/generateConfigFile.js +6 -1
  16. package/build/generators/generateDbFile.js +37 -0
  17. package/build/generators/generateModelFile.d.ts +3 -0
  18. package/build/generators/generateModelFile.js +105 -0
  19. package/build/generators/generateModelFile.test.d.ts +1 -0
  20. package/build/generators/generateModelFile.test.js +69 -0
  21. package/build/test/setup.js +4 -0
  22. package/build/util/loadConfig.js +3 -0
  23. package/package.json +8 -8
  24. package/build/commands/init/util/generateDbFile.js +0 -50
  25. package/build/util/usesDevServer.d.ts +0 -1
  26. package/build/util/usesDevServer.js +0 -10
  27. package/build/util/usesDevServer.test.js +0 -54
  28. /package/build/{util/usesDevServer.test.d.ts → commands/generate-model.test.d.ts} +0 -0
  29. /package/build/{commands/init/util → generators}/generateConfigFile.d.ts +0 -0
  30. /package/build/{commands/init/util → generators}/generateDbFile.d.ts +0 -0
  31. /package/build/{commands/init/util → generators}/generateModelsFile.d.ts +0 -0
  32. /package/build/{commands/init/util → generators}/generateModelsFile.js +0 -0
package/build/cli.js CHANGED
@@ -3,6 +3,7 @@ import { hideBin } from "yargs/helpers";
3
3
  import { dbDrop } from "#commands/db-drop.js";
4
4
  import { dbPush } from "#commands/db-push.js";
5
5
  import { dbPull } from "./commands/db-pull.js";
6
+ import { generateModel } from "./commands/generate-model.js";
6
7
  import { init } from "./commands/init.js";
7
8
  import { globalOptions } from "./options.js";
8
9
  await yargs(hideBin(process.argv))
@@ -10,6 +11,7 @@ await yargs(hideBin(process.argv))
10
11
  .scriptName("orm")
11
12
  .options(globalOptions)
12
13
  .command("db", "Commands for managing your database", (yargs) => yargs.command(dbDrop).command(dbPush).command(dbPull))
14
+ .command("generate", "Commands for generating files", (yargs) => yargs.command(generateModel))
13
15
  .command(init)
14
16
  .help()
15
17
  .showHelpOnFail(true)
@@ -4,7 +4,6 @@ export const handler = async (opts) => {
4
4
  const { db } = await loadConfig(opts);
5
5
  try {
6
6
  console.log("Dropping database schemas");
7
- await db.connect();
8
7
  await migrate.drop(db);
9
8
  console.log("✅ Done");
10
9
  }
@@ -4,14 +4,13 @@ import path from "path";
4
4
  import { migrate } from "@casekit/orm2-migrate";
5
5
  import { createOrOverwriteFile } from "#util/createOrOverwriteFile.js";
6
6
  import { loadConfig } from "#util/loadConfig.js";
7
- import { prettify } from "../../util/prettify.js";
8
- import { generateModelsFile } from "../init/util/generateModelsFile.js";
7
+ import { prettify } from "#util/prettify.js";
8
+ import { generateModelsFile } from "../../generators/generateModelsFile.js";
9
9
  import { renderModel } from "./util/renderModel.js";
10
10
  export const handler = async (opts) => {
11
11
  const config = await loadConfig(opts);
12
12
  const schema = opts.schema.length > 0 ? opts.schema : [config.db.config.schema];
13
13
  try {
14
- await config.db.connect();
15
14
  console.log("Pulling schema from database");
16
15
  const tables = await migrate.pull(config.db, schema);
17
16
  console.log(`Found ${tables.length} tables`);
@@ -70,6 +70,10 @@ describe("db pull", () => {
70
70
  // Import the mocked config
71
71
  const path = "/project/orm.config.ts";
72
72
  const { default: config } = await import(path);
73
+ await config.db.connect();
74
+ process.on("exit", async function () {
75
+ await config.db.close();
76
+ });
73
77
  vi.spyOn(loadConfig, "loadConfig").mockResolvedValue(config);
74
78
  });
75
79
  afterEach(async () => {
@@ -503,6 +507,10 @@ describe("db pull", () => {
503
507
  // Import the mocked config
504
508
  const path = "/project/orm.config.ts";
505
509
  const { default: config } = await import(path);
510
+ await config.db.connect();
511
+ process.on("exit", async function () {
512
+ await config.db.close();
513
+ });
506
514
  vi.spyOn(loadConfig, "loadConfig").mockResolvedValue(config);
507
515
  // Push the kitchen sink model to the database
508
516
  await yargs()
@@ -3,7 +3,6 @@ import { loadConfig } from "#util/loadConfig.js";
3
3
  export const handler = async (opts) => {
4
4
  const { db } = await loadConfig(opts);
5
5
  try {
6
- await db.connect();
7
6
  await migrate.push(db);
8
7
  console.log("✅ Done");
9
8
  }
@@ -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,19 @@
1
+ import { dir } from "console";
2
+ import { camelCase } from "es-toolkit";
3
+ import fs from "fs";
4
+ import path from "path";
5
+ import { generateModelFile } from "#generators/generateModelFile.js";
6
+ import { generateModelsFile } from "#generators/generateModelsFile.js";
7
+ import { createOrOverwriteFile } from "#util/createOrOverwriteFile.js";
8
+ import { loadConfig } from "#util/loadConfig.js";
9
+ import { prettify } from "#util/prettify.js";
10
+ export const handler = async (opts) => {
11
+ const config = await loadConfig(opts);
12
+ const modelFile = await generateModelFile(opts, config);
13
+ await createOrOverwriteFile(`${config.directory}/models/${opts.name}.ts`, modelFile, opts.force);
14
+ const models = fs
15
+ .readdirSync(`${config.directory}/models`)
16
+ .filter((f) => f !== "index.ts")
17
+ .map((f) => camelCase(f.replace(/\.ts$/, "")));
18
+ await createOrOverwriteFile(`${config.directory}/models/index.ts`, await prettify(path.join(process.cwd(), `${dir}/models/index.ts`), generateModelsFile(models)), opts.force);
19
+ };
@@ -0,0 +1,18 @@
1
+ export declare const builder: {
2
+ readonly name: {
3
+ readonly type: "string";
4
+ readonly desc: "Name of the model";
5
+ readonly demandOption: true;
6
+ };
7
+ readonly template: {
8
+ readonly type: "string";
9
+ readonly desc: "Template to use to generate the model. Pass --template none if you have a default template defined but don't want to use it.";
10
+ readonly default: "default";
11
+ };
12
+ readonly fields: {
13
+ readonly type: "string";
14
+ readonly desc: "Fields to generate for the model, in the format 'name:type name:type'";
15
+ readonly array: true;
16
+ readonly default: readonly [];
17
+ };
18
+ };
@@ -0,0 +1,18 @@
1
+ export const builder = {
2
+ name: {
3
+ type: "string",
4
+ desc: "Name of the model",
5
+ demandOption: true,
6
+ },
7
+ template: {
8
+ type: "string",
9
+ desc: "Template to use to generate the model. Pass --template none if you have a default template defined but don't want to use it.",
10
+ default: "default",
11
+ },
12
+ fields: {
13
+ type: "string",
14
+ desc: "Fields to generate for the model, in the format 'name:type name:type'",
15
+ array: true,
16
+ default: [],
17
+ },
18
+ };
@@ -0,0 +1,40 @@
1
+ export declare const generateModel: {
2
+ command: string;
3
+ desc: string;
4
+ builder: {
5
+ readonly name: {
6
+ readonly type: "string";
7
+ readonly desc: "Name of the model";
8
+ readonly demandOption: true;
9
+ };
10
+ readonly template: {
11
+ readonly type: "string";
12
+ readonly desc: "Template to use to generate the model. Pass --template none if you have a default template defined but don't want to use it.";
13
+ readonly default: "default";
14
+ };
15
+ readonly fields: {
16
+ readonly type: "string";
17
+ readonly desc: "Fields to generate for the model, in the format 'name:type name:type'";
18
+ readonly array: true;
19
+ readonly default: readonly [];
20
+ };
21
+ };
22
+ handler: import("../types.js").Handler<{
23
+ readonly name: {
24
+ readonly type: "string";
25
+ readonly desc: "Name of the model";
26
+ readonly demandOption: true;
27
+ };
28
+ readonly template: {
29
+ readonly type: "string";
30
+ readonly desc: "Template to use to generate the model. Pass --template none if you have a default template defined but don't want to use it.";
31
+ readonly default: "default";
32
+ };
33
+ readonly fields: {
34
+ readonly type: "string";
35
+ readonly desc: "Fields to generate for the model, in the format 'name:type name:type'";
36
+ readonly array: true;
37
+ readonly default: readonly [];
38
+ };
39
+ }>;
40
+ };
@@ -0,0 +1,8 @@
1
+ import { handler } from "./generate-model/handler.js";
2
+ import { builder } from "./generate-model/options.js";
3
+ export const generateModel = {
4
+ command: "model",
5
+ desc: "Generate a skeleton model file",
6
+ builder,
7
+ handler,
8
+ };
@@ -0,0 +1,63 @@
1
+ import * as prompts from "@inquirer/prompts";
2
+ import { vol } from "memfs";
3
+ import { beforeEach, describe, expect, test, vi } from "vitest";
4
+ import yargs from "yargs";
5
+ import { unindent } from "@casekit/unindent";
6
+ import { globalOptions } from "#options.js";
7
+ import { generateModel } from "./generate-model.js";
8
+ describe("orm generate model", () => {
9
+ beforeEach(() => {
10
+ vi.spyOn(process, "cwd").mockReturnValue(".");
11
+ vol.fromJSON({
12
+ "app/db.server/models/index.ts": unindent `
13
+ import { user } from "./user";
14
+
15
+ export const models = {
16
+ user
17
+ };
18
+ `,
19
+ "app/db.server/models/user.ts": unindent `
20
+ import { ModelDefinition, sql } from "@casekit/orm2";
21
+
22
+ export const user = {
23
+ fields: {
24
+ id: { type: "uuid", primaryKey: true },
25
+ createdAt: { type: "timestamp", default: sql\`now()\` },
26
+ name: { type: "text" },
27
+ },
28
+ } as const satisfies ModelDefinition;
29
+ `,
30
+ }, "/project");
31
+ });
32
+ test("it generates a model file merging the template and provided fields", async () => {
33
+ vi.spyOn(prompts, "confirm").mockResolvedValue(true);
34
+ vi.spyOn(process, "cwd").mockReturnValue("/project");
35
+ await yargs()
36
+ .options(globalOptions)
37
+ .command(generateModel)
38
+ .parseAsync("model --name post --fields title:text content:text");
39
+ const modelFile = vol.readFileSync("./app/db.server/models/post.ts", "utf8");
40
+ expect(modelFile.trim()).toEqual(unindent `
41
+ import { ModelDefinition, sql } from "@casekit/orm2";
42
+
43
+ export const post = {
44
+ fields: {
45
+ id: { type: "uuid", primaryKey: true },
46
+ createdAt: { type: "timestamp", default: sql\`now()\` },
47
+ title: { type: "text" },
48
+ content: { type: "text" },
49
+ },
50
+ } as const satisfies ModelDefinition;
51
+ `);
52
+ const indexFile = vol.readFileSync("./app/db.server/models/index.ts", "utf8");
53
+ expect(indexFile.trim()).toEqual(unindent `
54
+ import { post } from "./post";
55
+ import { user } from "./user";
56
+
57
+ export const models = {
58
+ post,
59
+ user,
60
+ };
61
+ `);
62
+ });
63
+ });
@@ -1,12 +1,11 @@
1
1
  import { input } from "@inquirer/prompts";
2
2
  import fs from "fs";
3
3
  import path from "path";
4
- import { unindent } from "@casekit/unindent";
4
+ import { generateConfigFile } from "#generators/generateConfigFile.js";
5
+ import { generateDbFile } from "#generators/generateDbFile.js";
6
+ import { generateModelsFile } from "#generators/generateModelsFile.js";
5
7
  import { createOrOverwriteFile } from "#util/createOrOverwriteFile.js";
6
- import { prettify } from "../../util/prettify.js";
7
- import { generateConfigFile } from "./util/generateConfigFile.js";
8
- import { generateDbFile } from "./util/generateDbFile.js";
9
- import { generateModelsFile } from "./util/generateModelsFile.js";
8
+ import { prettify } from "#util/prettify.js";
10
9
  export const handler = async (opts) => {
11
10
  const srcDir = ["src", "app", "lib"].find(fs.existsSync) ?? "src";
12
11
  const dir = opts.directory ??
@@ -16,16 +15,6 @@ export const handler = async (opts) => {
16
15
  }));
17
16
  const dbFile = generateDbFile();
18
17
  await createOrOverwriteFile(`${dir}/index.ts`, dbFile, opts.force);
19
- await createOrOverwriteFile(`${dir}/config.ts`, unindent `
20
- import { type Config } from "@casekit/orm2";
21
- import { models } from "./models";
22
-
23
- export const config = {
24
- models,
25
- schema: "public",
26
- connection: { connectionString: process.env.DATABASE_URL },
27
- } as const satisfies Config;
28
- `, opts.force);
29
18
  const modelsFile = generateModelsFile([]);
30
19
  await createOrOverwriteFile(`${dir}/models/index.ts`, await prettify(path.join(process.cwd(), `${dir}/models/index.ts`), modelsFile), opts.force);
31
20
  const configFile = generateConfigFile(dir);
@@ -5,14 +5,7 @@ import yargs from "yargs";
5
5
  import { unindent } from "@casekit/unindent";
6
6
  import { init } from "./init.js";
7
7
  describe("init", () => {
8
- test("creating config files for a react-router app", async () => {
9
- vol.fromJSON({
10
- "package.json": JSON.stringify({
11
- dependencies: {
12
- "react-router": "^6.0.0",
13
- },
14
- }),
15
- }, "/project");
8
+ test("scaffolding config files", async () => {
16
9
  vi.spyOn(prompts, "input").mockResolvedValueOnce("./app/db.server");
17
10
  vi.spyOn(prompts, "confirm").mockResolvedValueOnce(true);
18
11
  await yargs().command(init).parseAsync("init");
@@ -21,32 +14,36 @@ describe("init", () => {
21
14
  const configFile = vol.readFileSync("./orm.config.ts", "utf-8");
22
15
  expect(dbFile).toEqual(unindent `
23
16
  import { type Config, type ModelType, type Orm, orm } from "@casekit/orm2";
17
+ import { models } from "./models";
24
18
 
25
- import { type Models, config } from "./config";
19
+ const config = {
20
+ models,
21
+ } as const satisfies Config;
26
22
 
27
23
  let db: Orm<typeof config>;
28
24
 
29
25
  declare global {
30
- // eslint-disable-next-line no-var
31
- var __db: Orm<typeof config>;
26
+ // eslint-disable-next-line no-var
27
+ var __db: Orm<typeof config>;
32
28
  }
33
29
 
34
30
  // we do this because in development we don't want to restart
35
31
  // the server with every change, but we want to make sure we don't
36
32
  // create a new connection to the DB with every change either.
37
33
  if (process.env.NODE_ENV === "production") {
38
- db = orm(config);
39
- await db.connect();
34
+ db = orm(config);
35
+ await db.connect();
40
36
  } else {
41
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
42
- if (!global.__db) {
43
- global.__db = orm(config);
44
- await global.__db.connect();
45
- }
46
- db = global.__db;
37
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
38
+ if (!global.__db) {
39
+ global.__db = orm(config);
40
+ await global.__db.connect();
41
+ }
42
+ db = global.__db;
47
43
  }
48
44
 
49
45
  export type DB = Orm<typeof config>;
46
+ export type Models = typeof models;
50
47
  export type Model<M extends keyof Models> = ModelType<Models[M]>;
51
48
 
52
49
  export { db };
@@ -55,9 +52,14 @@ describe("init", () => {
55
52
  export const models = {};
56
53
  `);
57
54
  expect(configFile).toEqual(unindent `
55
+ import { type Config, orm } from "@casekit/orm2";
58
56
  import type { OrmCLIConfig } from "@casekit/orm2-cli";
59
57
 
60
- import { config } from "./app/db.server/config";
58
+ import { models } from "./app/db.server/models";
59
+
60
+ const config = {
61
+ models,
62
+ } as const satisfies Config;
61
63
 
62
64
  export default {
63
65
  db: orm(config),
@@ -83,13 +85,36 @@ describe("init", () => {
83
85
  const configFile = vol.readFileSync("./orm.config.ts", "utf-8");
84
86
  expect(dbFile).toEqual(unindent `
85
87
  import { type Config, type ModelType, type Orm, orm } from "@casekit/orm2";
88
+ import { models } from "./models";
89
+
90
+ const config = {
91
+ models,
92
+ } as const satisfies Config;
86
93
 
87
- import { type Models, config } from "./config";
94
+ let db: Orm<typeof config>;
95
+
96
+ declare global {
97
+ // eslint-disable-next-line no-var
98
+ var __db: Orm<typeof config>;
99
+ }
88
100
 
89
- const db: Orm<typeof config> = orm(config);
90
- await db.connect();
101
+ // we do this because in development we don't want to restart
102
+ // the server with every change, but we want to make sure we don't
103
+ // create a new connection to the DB with every change either.
104
+ if (process.env.NODE_ENV === "production") {
105
+ db = orm(config);
106
+ await db.connect();
107
+ } else {
108
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
109
+ if (!global.__db) {
110
+ global.__db = orm(config);
111
+ await global.__db.connect();
112
+ }
113
+ db = global.__db;
114
+ }
91
115
 
92
116
  export type DB = Orm<typeof config>;
117
+ export type Models = typeof models;
93
118
  export type Model<M extends keyof Models> = ModelType<Models[M]>;
94
119
 
95
120
  export { db };
@@ -1,8 +1,13 @@
1
1
  import { unindent } from "@casekit/unindent";
2
2
  export const generateConfigFile = (directory) => unindent `
3
+ import { type Config, orm } from "@casekit/orm2";
3
4
  import type { OrmCLIConfig } from "@casekit/orm2-cli";
4
5
 
5
- import { config } from "./${directory.replace(/^\.\//, "")}/config";
6
+ import { models } from "./${directory.replace(/^\.\//, "")}/models";
7
+
8
+ const config = {
9
+ models,
10
+ } as const satisfies Config;
6
11
 
7
12
  export default {
8
13
  db: orm(config),
@@ -0,0 +1,37 @@
1
+ import { unindent } from "@casekit/unindent";
2
+ export const generateDbFile = () => unindent `
3
+ import { type Config, type ModelType, type Orm, orm } from "@casekit/orm2";
4
+ import { models } from "./models";
5
+
6
+ const config = {
7
+ models,
8
+ } as const satisfies Config;
9
+
10
+ let db: Orm<typeof config>;
11
+
12
+ declare global {
13
+ // eslint-disable-next-line no-var
14
+ var __db: Orm<typeof config>;
15
+ }
16
+
17
+ // we do this because in development we don't want to restart
18
+ // the server with every change, but we want to make sure we don't
19
+ // create a new connection to the DB with every change either.
20
+ if (process.env.NODE_ENV === "production") {
21
+ db = orm(config);
22
+ await db.connect();
23
+ } else {
24
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
25
+ if (!global.__db) {
26
+ global.__db = orm(config);
27
+ await global.__db.connect();
28
+ }
29
+ db = global.__db;
30
+ }
31
+
32
+ export type DB = Orm<typeof config>;
33
+ export type Models = typeof models;
34
+ export type Model<M extends keyof Models> = ModelType<Models[M]>;
35
+
36
+ export { db };
37
+ `;
@@ -0,0 +1,3 @@
1
+ import type { CommandOptions, OrmCLIConfig } from "#types.js";
2
+ import type { builder } from "../commands/generate-model/options.js";
3
+ export declare const generateModelFile: (opts: Pick<CommandOptions<typeof builder>, "name" | "config" | "template" | "fields">, { directory }: Pick<OrmCLIConfig, "directory">) => Promise<string>;
@@ -0,0 +1,105 @@
1
+ import { camelCase } from "es-toolkit";
2
+ import jscodeshift from "jscodeshift";
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import { prettify } from "#util/prettify.js";
6
+ export const generateModelFile = async (opts, { directory }) => {
7
+ const config = fs.readFileSync(opts.config, "utf-8");
8
+ const j = jscodeshift.withParser("ts");
9
+ const root = j(config);
10
+ // Find the template in the config
11
+ const templatePath = root
12
+ .find(j.ExportDefaultDeclaration)
13
+ .find(j.ObjectExpression)
14
+ .find(j.ObjectProperty, {
15
+ key: { type: "Identifier", name: "generate" },
16
+ })
17
+ .find(j.ObjectProperty, {
18
+ key: { type: "Identifier", name: "templates" },
19
+ })
20
+ .find(j.ObjectProperty, {
21
+ key: { type: "Identifier", name: opts.template },
22
+ });
23
+ let modelDefinition;
24
+ if (templatePath.length > 0) {
25
+ // Get the template node directly
26
+ modelDefinition = templatePath.find(j.ObjectExpression).get().node;
27
+ }
28
+ else {
29
+ console.warn(`Template "${opts.template}" not found.`);
30
+ // Create a minimal model definition with just fields
31
+ modelDefinition = j.objectExpression([
32
+ j.objectProperty(j.identifier("fields"), j.objectExpression([])),
33
+ ]);
34
+ }
35
+ // Find or create the fields property
36
+ let fieldsProperty = modelDefinition.properties.find((prop) => prop.type === "ObjectProperty" &&
37
+ prop.key.type === "Identifier" &&
38
+ prop.key.name === "fields");
39
+ if (!fieldsProperty) {
40
+ fieldsProperty = j.objectProperty(j.identifier("fields"), j.objectExpression([]));
41
+ modelDefinition.properties.push(fieldsProperty);
42
+ }
43
+ const fieldsObject = fieldsProperty.value;
44
+ // Collect existing fields from template by converting them to source code
45
+ const templateFieldsCode = new Map();
46
+ for (const prop of fieldsObject.properties) {
47
+ if (prop.type === "ObjectProperty" && prop.key.type === "Identifier") {
48
+ // Extract the field code by converting the property to source
49
+ const fieldCode = j(prop.value).toSource({ quote: "double" });
50
+ templateFieldsCode.set(prop.key.name, fieldCode);
51
+ }
52
+ }
53
+ // Process command-line fields - they override template fields but should be added at the end
54
+ const newFields = new Map();
55
+ for (const field of opts.fields) {
56
+ const [originalName, type] = field.split(":");
57
+ if (!originalName || !type) {
58
+ console.error("Invalid field definition:", field);
59
+ process.exit(1);
60
+ }
61
+ const camelCasedName = camelCase(originalName);
62
+ // Build a code string for the new field
63
+ let fieldCode = `{ type: "${type}" }`;
64
+ if (camelCasedName !== originalName) {
65
+ fieldCode = `{ column: "${originalName}", type: "${type}" }`;
66
+ }
67
+ // Remove from template if it exists, add to new fields
68
+ templateFieldsCode.delete(camelCasedName);
69
+ newFields.set(camelCasedName, fieldCode);
70
+ }
71
+ // Merge: template fields first (excluding overridden ones), then new fields
72
+ const allFieldsCode = new Map([...templateFieldsCode, ...newFields]);
73
+ // Rebuild the fields object with all properties
74
+ const newFieldsProps = [];
75
+ for (const [name, code] of allFieldsCode.entries()) {
76
+ // Parse the field code back into an AST node
77
+ const parsed = j(`const x = ${code}`);
78
+ const value = parsed.find(j.VariableDeclarator).get("init")
79
+ .node;
80
+ newFieldsProps.push(j.objectProperty(j.identifier(name), value));
81
+ }
82
+ fieldsObject.properties = newFieldsProps;
83
+ // Check if sql import is needed by traversing the AST
84
+ const hasSqlLiteral = j(modelDefinition).find(j.TaggedTemplateExpression, {
85
+ tag: { type: "Identifier", name: "sql" },
86
+ }).length > 0;
87
+ // Build the import statement
88
+ const importSpecifiers = [
89
+ j.importSpecifier(j.identifier("ModelDefinition")),
90
+ ];
91
+ if (hasSqlLiteral) {
92
+ importSpecifiers.push(j.importSpecifier(j.identifier("sql")));
93
+ }
94
+ const importDeclaration = j.importDeclaration(importSpecifiers, j.literal("@casekit/orm2"));
95
+ // Build the export statement with type assertion
96
+ const exportDeclaration = j.exportNamedDeclaration(j.variableDeclaration("const", [
97
+ j.variableDeclarator(j.identifier(opts.name), j.tsSatisfiesExpression(j.tsAsExpression(modelDefinition, j.tsTypeReference(j.identifier("const"))), j.tsTypeReference(j.identifier("ModelDefinition")))),
98
+ ]));
99
+ const program = j.program([importDeclaration, exportDeclaration]);
100
+ const result = j(program).toSource({
101
+ quote: "double",
102
+ objectCurlySpacing: false,
103
+ });
104
+ return await prettify(path.join(process.cwd(), directory), result);
105
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,69 @@
1
+ import { vol } from "memfs";
2
+ import { beforeAll, describe, expect, test } from "vitest";
3
+ import { unindent } from "@casekit/unindent";
4
+ import { generateModelFile } from "./generateModelFile.js";
5
+ describe("generateModelFile", () => {
6
+ beforeAll(() => {
7
+ vol.fromJSON({
8
+ "orm.config.ts": unindent `
9
+ import { sql } from "@casekit/orm2";
10
+ import { type OrmCLIConfig } from "@casekit/orm2-cli";
11
+
12
+ export default {
13
+ directory: "./app/db.server",
14
+ generate: {
15
+ templates: {
16
+ default: {
17
+ fields: {
18
+ id: { type: "uuid", primaryKey: true },
19
+ createdAt: { type: "timestamp", default: sql\`now()\` },
20
+ },
21
+ },
22
+ foo: {
23
+ fields: {
24
+ name: { type: "text" },
25
+ },
26
+ },
27
+ },
28
+ },
29
+ } satisfies OrmCLIConfig;
30
+ `,
31
+ }, "/project");
32
+ });
33
+ test("merges fields from the passed `fields` argument, overwriting same-named fields in the template", async () => {
34
+ const result = await generateModelFile({
35
+ config: "orm.config.ts",
36
+ name: "bar",
37
+ template: "default",
38
+ fields: ["id:serial", "name:text"],
39
+ }, { directory: "./app/db.server" });
40
+ expect(result.trim()).toBe(unindent `
41
+ import { ModelDefinition, sql } from "@casekit/orm2";
42
+
43
+ export const bar = {
44
+ fields: {
45
+ createdAt: { type: "timestamp", default: sql\`now()\` },
46
+ id: { type: "serial" },
47
+ name: { type: "text" },
48
+ },
49
+ } as const satisfies ModelDefinition;
50
+ `);
51
+ });
52
+ test("works if the template does not exist", async () => {
53
+ const result = await generateModelFile({
54
+ config: "orm.config.ts",
55
+ name: "foo",
56
+ template: "wrong",
57
+ fields: [],
58
+ }, {
59
+ directory: "./app/db.server",
60
+ });
61
+ expect(result.trim()).toBe(unindent `
62
+ import { ModelDefinition } from "@casekit/orm2";
63
+
64
+ export const foo = {
65
+ fields: {},
66
+ } as const satisfies ModelDefinition;
67
+ `);
68
+ });
69
+ });
@@ -19,6 +19,10 @@ afterAll(async () => {
19
19
  beforeEach(async () => {
20
20
  const path = "./orm.config.ts";
21
21
  const { default: config } = await import(path);
22
+ await config.db.connect();
23
+ process.on("exit", async function () {
24
+ await config.db.close();
25
+ });
22
26
  vi.spyOn(loadConfig, "loadConfig").mockResolvedValue(config);
23
27
  const originalFs = await vi.importActual("fs");
24
28
  const ormConfig = originalFs.readFileSync("./src/test/orm.config.ts", "utf8");
@@ -1,11 +1,14 @@
1
+ import dotenv from "dotenv";
1
2
  import path from "path";
2
3
  import { register } from "tsx/esm/api";
3
4
  export const loadConfig = async (options) => {
4
5
  try {
6
+ dotenv.config();
5
7
  const unregister = register();
6
8
  const { default: config } = await import(path.join(process.cwd(), options.config));
7
9
  await unregister();
8
10
  const c = "default" in config ? config.default : config;
11
+ await c.db.connect();
9
12
  process.on("exit", async function () {
10
13
  await c.db.close();
11
14
  });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@casekit/orm2-cli",
3
3
  "description": "",
4
- "version": "1.0.0",
4
+ "version": "1.0.2",
5
5
  "author": "",
6
6
  "bin": {
7
7
  "orm": "./build/cli.js"
@@ -17,10 +17,10 @@
17
17
  "pluralize": "^8.0.0",
18
18
  "tsx": "^4.20.3",
19
19
  "yargs": "^18.0.0",
20
- "@casekit/orm2": "1.0.0",
21
- "@casekit/orm2-migrate": "1.0.0",
22
- "@casekit/sql": "1.0.0",
23
- "@casekit/toolbox": "1.0.0"
20
+ "@casekit/orm2": "1.0.2",
21
+ "@casekit/sql": "1.0.2",
22
+ "@casekit/toolbox": "1.0.2",
23
+ "@casekit/orm2-migrate": "1.0.2"
24
24
  },
25
25
  "devDependencies": {
26
26
  "@trivago/prettier-plugin-sort-imports": "^5.2.2",
@@ -39,9 +39,9 @@
39
39
  "vite-tsconfig-paths": "^5.1.4",
40
40
  "vitest": "^3.2.4",
41
41
  "zod": "^4.0.17",
42
- "@casekit/tsconfig": "1.0.0",
43
- "@casekit/orm2-fixtures": "1.0.0",
44
- "@casekit/prettier-config": "1.0.0"
42
+ "@casekit/orm2-fixtures": "1.0.2",
43
+ "@casekit/tsconfig": "1.0.2",
44
+ "@casekit/prettier-config": "1.0.2"
45
45
  },
46
46
  "exports": {
47
47
  ".": "./build/index.js"
@@ -1,50 +0,0 @@
1
- import { unindent } from "@casekit/unindent";
2
- import { usesDevServer } from "#util/usesDevServer.js";
3
- export const generateDbFile = () => {
4
- return usesDevServer()
5
- ? unindent `
6
- import { type Config, type ModelType, type Orm, orm } from "@casekit/orm2";
7
-
8
- import { type Models, config } from "./config";
9
-
10
- let db: Orm<typeof config>;
11
-
12
- declare global {
13
- // eslint-disable-next-line no-var
14
- var __db: Orm<typeof config>;
15
- }
16
-
17
- // we do this because in development we don't want to restart
18
- // the server with every change, but we want to make sure we don't
19
- // create a new connection to the DB with every change either.
20
- if (process.env.NODE_ENV === "production") {
21
- db = orm(config);
22
- await db.connect();
23
- } else {
24
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
25
- if (!global.__db) {
26
- global.__db = orm(config);
27
- await global.__db.connect();
28
- }
29
- db = global.__db;
30
- }
31
-
32
- export type DB = Orm<typeof config>;
33
- export type Model<M extends keyof Models> = ModelType<Models[M]>;
34
-
35
- export { db };
36
- `
37
- : unindent `
38
- import { type Config, type ModelType, type Orm, orm } from "@casekit/orm2";
39
-
40
- import { type Models, config } from "./config";
41
-
42
- const db: Orm<typeof config> = orm(config);
43
- await db.connect();
44
-
45
- export type DB = Orm<typeof config>;
46
- export type Model<M extends keyof Models> = ModelType<Models[M]>;
47
-
48
- export { db };
49
- `;
50
- };
@@ -1 +0,0 @@
1
- export declare const usesDevServer: () => boolean;
@@ -1,10 +0,0 @@
1
- import fs from "fs";
2
- export const usesDevServer = () => {
3
- try {
4
- const manifest = JSON.parse(fs.readFileSync("./package.json", "utf-8"));
5
- return !!Object.keys(manifest.dependencies).find((dep) => /(^react-router$|^@remix-run|^next$)/.test(dep));
6
- }
7
- catch {
8
- return false;
9
- }
10
- };
@@ -1,54 +0,0 @@
1
- import { vol } from "memfs";
2
- import { describe, expect, test } from "vitest";
3
- import { usesDevServer } from "./usesDevServer.js";
4
- describe("usesDevServer", () => {
5
- test("returns true when package.json has react-router dependency", () => {
6
- vol.fromJSON({
7
- "package.json": JSON.stringify({
8
- dependencies: {
9
- "react-router": "^6.0.0",
10
- },
11
- }),
12
- }, "/project");
13
- expect(usesDevServer()).toBe(true);
14
- });
15
- test("returns true when package.json has @remix-run dependency", () => {
16
- vol.fromJSON({
17
- "package.json": JSON.stringify({
18
- dependencies: {
19
- "@remix-run/react": "^1.0.0",
20
- },
21
- }),
22
- }, "/project");
23
- expect(usesDevServer()).toBe(true);
24
- });
25
- test("returns true when package.json has next.js dependency", () => {
26
- vol.fromJSON({
27
- "package.json": JSON.stringify({
28
- dependencies: {
29
- next: "^12.0.0",
30
- },
31
- }),
32
- }, "/project");
33
- expect(usesDevServer()).toBe(true);
34
- });
35
- test("returns false when package.json has no next, remix, or react-router dependencies", () => {
36
- vol.fromJSON({
37
- "package.json": JSON.stringify({
38
- dependencies: {
39
- lodash: "^4.0.0",
40
- },
41
- }),
42
- }, "/project");
43
- expect(usesDevServer()).toBe(false);
44
- });
45
- test("returns false when package.json is missing", () => {
46
- expect(usesDevServer()).toBe(false);
47
- });
48
- test("returns false when package.json is invalid JSON", () => {
49
- vol.fromJSON({
50
- "package.json": "invalid json",
51
- }, "/project");
52
- expect(usesDevServer()).toBe(false);
53
- });
54
- });