@casekit/orm2-cli 1.0.6 → 1.0.7
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/commands/generate-model/handler.js +2 -3
- package/build/commands/generate-model/options.d.ts +0 -11
- package/build/commands/generate-model/options.js +0 -11
- package/build/commands/generate-model.d.ts +0 -22
- package/build/commands/generate-model.test.js +4 -9
- package/build/generators/generateModelFile.d.ts +1 -3
- package/build/generators/generateModelFile.js +8 -109
- package/build/generators/generateModelFile.test.js +4 -54
- package/package.json +8 -10
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { dir } from "console";
|
|
2
1
|
import { camelCase } from "es-toolkit";
|
|
3
2
|
import fs from "fs";
|
|
4
3
|
import path from "path";
|
|
@@ -9,11 +8,11 @@ import { loadConfig } from "#util/loadConfig.js";
|
|
|
9
8
|
import { prettify } from "#util/prettify.js";
|
|
10
9
|
export const handler = async (opts) => {
|
|
11
10
|
const config = await loadConfig(opts);
|
|
12
|
-
const modelFile = await generateModelFile(opts, config);
|
|
11
|
+
const modelFile = await generateModelFile(opts.name, config.directory);
|
|
13
12
|
await createOrOverwriteFile(`${config.directory}/models/${opts.name}.ts`, modelFile, opts.force);
|
|
14
13
|
const models = fs
|
|
15
14
|
.readdirSync(`${config.directory}/models`)
|
|
16
15
|
.filter((f) => f !== "index.ts")
|
|
17
16
|
.map((f) => camelCase(f.replace(/\.ts$/, "")));
|
|
18
|
-
await createOrOverwriteFile(`${config.directory}/models/index.ts`, await prettify(path.join(process.cwd(), `${
|
|
17
|
+
await createOrOverwriteFile(`${config.directory}/models/index.ts`, await prettify(path.join(process.cwd(), `${config.directory}/models/index.ts`), generateModelsFile(models)), opts.force);
|
|
19
18
|
};
|
|
@@ -4,15 +4,4 @@ export declare const builder: {
|
|
|
4
4
|
readonly desc: "Name of the model";
|
|
5
5
|
readonly demandOption: true;
|
|
6
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
7
|
};
|
|
@@ -4,15 +4,4 @@ export const builder = {
|
|
|
4
4
|
desc: "Name of the model",
|
|
5
5
|
demandOption: true,
|
|
6
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
7
|
};
|
|
@@ -7,17 +7,6 @@ export declare const generateModel: {
|
|
|
7
7
|
readonly desc: "Name of the model";
|
|
8
8
|
readonly demandOption: true;
|
|
9
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
10
|
};
|
|
22
11
|
handler: import("../types.js").Handler<{
|
|
23
12
|
readonly name: {
|
|
@@ -25,16 +14,5 @@ export declare const generateModel: {
|
|
|
25
14
|
readonly desc: "Name of the model";
|
|
26
15
|
readonly demandOption: true;
|
|
27
16
|
};
|
|
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
17
|
}>;
|
|
40
18
|
};
|
|
@@ -29,24 +29,19 @@ describe("orm generate model", () => {
|
|
|
29
29
|
`,
|
|
30
30
|
}, "/project");
|
|
31
31
|
});
|
|
32
|
-
test("it generates a model file
|
|
32
|
+
test("it generates a model file and updates the index", async () => {
|
|
33
33
|
vi.spyOn(prompts, "confirm").mockResolvedValue(true);
|
|
34
34
|
vi.spyOn(process, "cwd").mockReturnValue("/project");
|
|
35
35
|
await yargs()
|
|
36
36
|
.options(globalOptions)
|
|
37
37
|
.command(generateModel)
|
|
38
|
-
.parseAsync("model --name post
|
|
38
|
+
.parseAsync("model --name post");
|
|
39
39
|
const modelFile = vol.readFileSync("./app/db.server/models/post.ts", "utf8");
|
|
40
40
|
expect(modelFile.trim()).toEqual(unindent `
|
|
41
|
-
import {
|
|
41
|
+
import type { ModelDefinition } from "@casekit/orm2";
|
|
42
42
|
|
|
43
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
|
-
},
|
|
44
|
+
fields: {},
|
|
50
45
|
} as const satisfies ModelDefinition;
|
|
51
46
|
`);
|
|
52
47
|
const indexFile = vol.readFileSync("./app/db.server/models/index.ts", "utf8");
|
|
@@ -1,3 +1 @@
|
|
|
1
|
-
|
|
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>;
|
|
1
|
+
export declare const generateModelFile: (name: string, directory: string) => Promise<string>;
|
|
@@ -1,112 +1,11 @@
|
|
|
1
|
-
import { camelCase } from "es-toolkit";
|
|
2
|
-
import jscodeshift from "jscodeshift";
|
|
3
|
-
import fs from "node:fs";
|
|
4
1
|
import path from "node:path";
|
|
5
2
|
import { prettify } from "#util/prettify.js";
|
|
6
|
-
export const generateModelFile = async (
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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);
|
|
3
|
+
export const generateModelFile = async (name, directory) => {
|
|
4
|
+
const code = `import type { ModelDefinition } from "@casekit/orm2";
|
|
5
|
+
|
|
6
|
+
export const ${name} = {
|
|
7
|
+
fields: {},
|
|
8
|
+
} as const satisfies ModelDefinition;
|
|
9
|
+
`;
|
|
10
|
+
return await prettify(path.join(process.cwd(), directory), code);
|
|
112
11
|
};
|
|
@@ -4,64 +4,14 @@ import { unindent } from "@casekit/unindent";
|
|
|
4
4
|
import { generateModelFile } from "./generateModelFile.js";
|
|
5
5
|
describe("generateModelFile", () => {
|
|
6
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
|
-
`);
|
|
7
|
+
vol.fromJSON({}, "/project");
|
|
51
8
|
});
|
|
52
|
-
test("
|
|
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
|
-
});
|
|
9
|
+
test("generates an empty model definition", async () => {
|
|
10
|
+
const result = await generateModelFile("book", "./app/db.server");
|
|
61
11
|
expect(result.trim()).toBe(unindent `
|
|
62
12
|
import type { ModelDefinition } from "@casekit/orm2";
|
|
63
13
|
|
|
64
|
-
export const
|
|
14
|
+
export const book = {
|
|
65
15
|
fields: {},
|
|
66
16
|
} as const satisfies ModelDefinition;
|
|
67
17
|
`);
|
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.7",
|
|
5
5
|
"author": "",
|
|
6
6
|
"bin": {
|
|
7
7
|
"orm": "./build/cli.js"
|
|
@@ -14,19 +14,17 @@
|
|
|
14
14
|
"camelcase": "^8.0.0",
|
|
15
15
|
"dotenv": "^16.5.0",
|
|
16
16
|
"es-toolkit": "^1.39.3",
|
|
17
|
-
"jscodeshift": "^17.3.0",
|
|
18
17
|
"pluralize": "^8.0.0",
|
|
19
18
|
"tsx": "^4.20.3",
|
|
20
19
|
"yargs": "^18.0.0",
|
|
21
|
-
"@casekit/orm2": "1.0.
|
|
22
|
-
"@casekit/sql": "1.0.
|
|
23
|
-
"@casekit/
|
|
24
|
-
"@casekit/
|
|
20
|
+
"@casekit/orm2-migrate": "1.0.7",
|
|
21
|
+
"@casekit/sql": "1.0.7",
|
|
22
|
+
"@casekit/orm2": "1.0.7",
|
|
23
|
+
"@casekit/toolbox": "1.0.7"
|
|
25
24
|
},
|
|
26
25
|
"devDependencies": {
|
|
27
26
|
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
|
|
28
27
|
"@types/byline": "^4.2.36",
|
|
29
|
-
"@types/jscodeshift": "^17.3.0",
|
|
30
28
|
"@types/node": "^24.0.3",
|
|
31
29
|
"@types/pg": "^8.15.4",
|
|
32
30
|
"@types/pluralize": "^0.0.33",
|
|
@@ -39,9 +37,9 @@
|
|
|
39
37
|
"vite-tsconfig-paths": "^5.1.4",
|
|
40
38
|
"vitest": "^3.2.4",
|
|
41
39
|
"zod": "^4.0.17",
|
|
42
|
-
"@casekit/
|
|
43
|
-
"@casekit/
|
|
44
|
-
"@casekit/
|
|
40
|
+
"@casekit/orm2-fixtures": "1.0.7",
|
|
41
|
+
"@casekit/tsconfig": "1.0.7",
|
|
42
|
+
"@casekit/prettier-config": "1.0.7"
|
|
45
43
|
},
|
|
46
44
|
"exports": {
|
|
47
45
|
".": "./build/index.js"
|