@casekit/orm2-cli 1.0.1 → 1.0.3
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.
- package/build/cli.js +2 -0
- package/build/commands/db-drop/handler.js +0 -1
- package/build/commands/db-pull/handler.js +2 -3
- package/build/commands/db-pull.test.js +8 -0
- package/build/commands/db-push/handler.js +0 -1
- package/build/commands/generate-model/handler.d.ts +3 -0
- package/build/commands/generate-model/handler.js +19 -0
- package/build/commands/generate-model/options.d.ts +18 -0
- package/build/commands/generate-model/options.js +18 -0
- package/build/commands/generate-model.d.ts +40 -0
- package/build/commands/generate-model.js +8 -0
- package/build/commands/generate-model.test.js +63 -0
- package/build/commands/init/handler.js +4 -15
- package/build/commands/init.test.js +48 -23
- package/build/{commands/init/util → generators}/generateConfigFile.js +6 -1
- package/build/generators/generateDbFile.js +37 -0
- package/build/generators/generateModelFile.d.ts +3 -0
- package/build/generators/generateModelFile.js +112 -0
- package/build/generators/generateModelFile.test.d.ts +1 -0
- package/build/generators/generateModelFile.test.js +69 -0
- package/build/test/setup.js +4 -0
- package/build/util/loadConfig.js +3 -0
- package/package.json +8 -8
- package/build/commands/init/util/generateDbFile.js +0 -50
- package/build/util/usesDevServer.d.ts +0 -1
- package/build/util/usesDevServer.js +0 -10
- package/build/util/usesDevServer.test.js +0 -54
- /package/build/{util/usesDevServer.test.d.ts → commands/generate-model.test.d.ts} +0 -0
- /package/build/{commands/init/util → generators}/generateConfigFile.d.ts +0 -0
- /package/build/{commands/init/util → generators}/generateDbFile.d.ts +0 -0
- /package/build/{commands/init/util → generators}/generateModelsFile.d.ts +0 -0
- /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,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 "
|
|
8
|
-
import { generateModelsFile } from "
|
|
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()
|
|
@@ -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,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 { type 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 { type 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 {
|
|
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 "
|
|
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("
|
|
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
|
-
|
|
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
|
-
|
|
31
|
-
|
|
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
|
-
|
|
39
|
-
|
|
34
|
+
db = orm(config);
|
|
35
|
+
await db.connect();
|
|
40
36
|
} else {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
90
|
-
|
|
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 {
|
|
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,112 @@
|
|
|
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 by parsing the import code string
|
|
88
|
+
// to ensure proper TypeScript type-only import syntax
|
|
89
|
+
let importCode;
|
|
90
|
+
if (hasSqlLiteral) {
|
|
91
|
+
// import { type ModelDefinition, sql }
|
|
92
|
+
importCode =
|
|
93
|
+
'import { type ModelDefinition, sql } from "@casekit/orm2";';
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
// import type { ModelDefinition }
|
|
97
|
+
importCode = 'import type { ModelDefinition } from "@casekit/orm2";';
|
|
98
|
+
}
|
|
99
|
+
const importDeclaration = j(importCode)
|
|
100
|
+
.find(j.ImportDeclaration)
|
|
101
|
+
.get().node;
|
|
102
|
+
// Build the export statement with type assertion
|
|
103
|
+
const exportDeclaration = j.exportNamedDeclaration(j.variableDeclaration("const", [
|
|
104
|
+
j.variableDeclarator(j.identifier(opts.name), j.tsSatisfiesExpression(j.tsAsExpression(modelDefinition, j.tsTypeReference(j.identifier("const"))), j.tsTypeReference(j.identifier("ModelDefinition")))),
|
|
105
|
+
]));
|
|
106
|
+
const program = j.program([importDeclaration, exportDeclaration]);
|
|
107
|
+
const result = j(program).toSource({
|
|
108
|
+
quote: "double",
|
|
109
|
+
objectCurlySpacing: false,
|
|
110
|
+
});
|
|
111
|
+
return await prettify(path.join(process.cwd(), directory), result);
|
|
112
|
+
};
|
|
@@ -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 { type 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 type { ModelDefinition } from "@casekit/orm2";
|
|
63
|
+
|
|
64
|
+
export const foo = {
|
|
65
|
+
fields: {},
|
|
66
|
+
} as const satisfies ModelDefinition;
|
|
67
|
+
`);
|
|
68
|
+
});
|
|
69
|
+
});
|
package/build/test/setup.js
CHANGED
|
@@ -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");
|
package/build/util/loadConfig.js
CHANGED
|
@@ -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.
|
|
4
|
+
"version": "1.0.3",
|
|
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.
|
|
21
|
-
"@casekit/orm2-migrate": "1.0.
|
|
22
|
-
"@casekit/sql": "1.0.
|
|
23
|
-
"@casekit/toolbox": "1.0.
|
|
20
|
+
"@casekit/orm2": "1.0.3",
|
|
21
|
+
"@casekit/orm2-migrate": "1.0.3",
|
|
22
|
+
"@casekit/sql": "1.0.3",
|
|
23
|
+
"@casekit/toolbox": "1.0.3"
|
|
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.
|
|
43
|
-
"@casekit/orm2-fixtures": "1.0.
|
|
44
|
-
"@casekit/prettier-config": "1.0.
|
|
42
|
+
"@casekit/tsconfig": "1.0.3",
|
|
43
|
+
"@casekit/orm2-fixtures": "1.0.3",
|
|
44
|
+
"@casekit/prettier-config": "1.0.3"
|
|
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
|
-
});
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|