@zenstackhq/cli 3.0.0-beta.9 → 3.1.0
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/.turbo/turbo-build.log +8 -8
- package/dist/index.cjs +375 -172
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +375 -167
- package/dist/index.js.map +1 -1
- package/package.json +16 -13
- package/src/actions/action-utils.ts +36 -8
- package/src/actions/db.ts +9 -5
- package/src/actions/format.ts +27 -0
- package/src/actions/generate.ts +101 -19
- package/src/actions/index.ts +4 -2
- package/src/actions/init.ts +4 -4
- package/src/actions/migrate.ts +35 -24
- package/src/actions/seed.ts +38 -0
- package/src/actions/templates.ts +5 -5
- package/src/constants.ts +3 -0
- package/src/index.ts +60 -15
- package/src/plugins/typescript.ts +20 -1
- package/src/utils/exec-utils.ts +35 -0
- package/test/db.test.ts +43 -0
- package/test/format.test.ts +33 -0
- package/test/generate.test.ts +29 -0
- package/test/migrate.test.ts +1 -2
- package/test/ts-schema-gen.test.ts +23 -1
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { formatDocument } from '@zenstackhq/language';
|
|
2
|
+
import colors from 'colors';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import { getSchemaFile } from './action-utils';
|
|
5
|
+
|
|
6
|
+
type Options = {
|
|
7
|
+
schema?: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* CLI action for formatting a ZModel schema file.
|
|
12
|
+
*/
|
|
13
|
+
export async function run(options: Options) {
|
|
14
|
+
const schemaFile = getSchemaFile(options.schema);
|
|
15
|
+
let formattedContent: string;
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
formattedContent = await formatDocument(fs.readFileSync(schemaFile, 'utf-8'));
|
|
19
|
+
} catch (error) {
|
|
20
|
+
console.error(colors.red('✗ Schema formatting failed.'));
|
|
21
|
+
// Re-throw to maintain CLI exit code behavior
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
fs.writeFileSync(schemaFile, formattedContent, 'utf-8');
|
|
26
|
+
console.log(colors.green('✓ Schema formatting completed successfully.'));
|
|
27
|
+
}
|
package/src/actions/generate.ts
CHANGED
|
@@ -3,7 +3,10 @@ import { isPlugin, LiteralExpr, Plugin, type Model } from '@zenstackhq/language/
|
|
|
3
3
|
import { getLiteral, getLiteralArray } from '@zenstackhq/language/utils';
|
|
4
4
|
import { type CliPlugin } from '@zenstackhq/sdk';
|
|
5
5
|
import colors from 'colors';
|
|
6
|
+
import { createJiti } from 'jiti';
|
|
7
|
+
import fs from 'node:fs';
|
|
6
8
|
import path from 'node:path';
|
|
9
|
+
import { pathToFileURL } from 'node:url';
|
|
7
10
|
import ora, { type Ora } from 'ora';
|
|
8
11
|
import { CliError } from '../cli-error';
|
|
9
12
|
import * as corePlugins from '../plugins';
|
|
@@ -13,6 +16,8 @@ type Options = {
|
|
|
13
16
|
schema?: string;
|
|
14
17
|
output?: string;
|
|
15
18
|
silent: boolean;
|
|
19
|
+
lite: boolean;
|
|
20
|
+
liteOnly: boolean;
|
|
16
21
|
};
|
|
17
22
|
|
|
18
23
|
/**
|
|
@@ -33,15 +38,15 @@ export async function run(options: Options) {
|
|
|
33
38
|
console.log(`You can now create a ZenStack client with it.
|
|
34
39
|
|
|
35
40
|
\`\`\`ts
|
|
36
|
-
import { ZenStackClient } from '@zenstackhq/
|
|
37
|
-
import { schema } from '${outputPath}/schema';
|
|
41
|
+
import { ZenStackClient } from '@zenstackhq/orm';
|
|
42
|
+
import { schema } from '${path.relative('.', outputPath)}/schema';
|
|
38
43
|
|
|
39
44
|
const client = new ZenStackClient(schema, {
|
|
40
45
|
dialect: { ... }
|
|
41
46
|
});
|
|
42
47
|
\`\`\`
|
|
43
48
|
|
|
44
|
-
Check documentation: https://zenstack.dev/docs
|
|
49
|
+
Check documentation: https://zenstack.dev/docs/`);
|
|
45
50
|
}
|
|
46
51
|
}
|
|
47
52
|
|
|
@@ -64,32 +69,43 @@ async function runPlugins(schemaFile: string, model: Model, outputPath: string,
|
|
|
64
69
|
for (const plugin of plugins) {
|
|
65
70
|
const provider = getPluginProvider(plugin);
|
|
66
71
|
|
|
67
|
-
let cliPlugin: CliPlugin;
|
|
72
|
+
let cliPlugin: CliPlugin | undefined;
|
|
68
73
|
if (provider.startsWith('@core/')) {
|
|
69
74
|
cliPlugin = (corePlugins as any)[provider.slice('@core/'.length)];
|
|
70
75
|
if (!cliPlugin) {
|
|
71
76
|
throw new CliError(`Unknown core plugin: ${provider}`);
|
|
72
77
|
}
|
|
73
78
|
} else {
|
|
74
|
-
|
|
75
|
-
if (moduleSpec.startsWith('.')) {
|
|
76
|
-
// relative to schema's path
|
|
77
|
-
moduleSpec = path.resolve(path.dirname(schemaFile), moduleSpec);
|
|
78
|
-
}
|
|
79
|
-
try {
|
|
80
|
-
cliPlugin = (await import(moduleSpec)).default as CliPlugin;
|
|
81
|
-
} catch (error) {
|
|
82
|
-
throw new CliError(`Failed to load plugin ${provider}: ${error}`);
|
|
83
|
-
}
|
|
79
|
+
cliPlugin = await loadPluginModule(provider, path.dirname(schemaFile));
|
|
84
80
|
}
|
|
85
81
|
|
|
86
|
-
|
|
82
|
+
if (cliPlugin) {
|
|
83
|
+
const pluginOptions = getPluginOptions(plugin);
|
|
84
|
+
|
|
85
|
+
// merge CLI options
|
|
86
|
+
if (provider === '@core/typescript') {
|
|
87
|
+
if (pluginOptions['lite'] === undefined) {
|
|
88
|
+
pluginOptions['lite'] = options.lite;
|
|
89
|
+
}
|
|
90
|
+
if (pluginOptions['liteOnly'] === undefined) {
|
|
91
|
+
pluginOptions['liteOnly'] = options.liteOnly;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
processedPlugins.push({ cliPlugin, pluginOptions });
|
|
96
|
+
}
|
|
87
97
|
}
|
|
88
98
|
|
|
89
|
-
const defaultPlugins = [
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
99
|
+
const defaultPlugins = [
|
|
100
|
+
{
|
|
101
|
+
plugin: corePlugins['typescript'],
|
|
102
|
+
options: { lite: options.lite, liteOnly: options.liteOnly },
|
|
103
|
+
},
|
|
104
|
+
];
|
|
105
|
+
defaultPlugins.forEach(({ plugin, options }) => {
|
|
106
|
+
if (!processedPlugins.some((p) => p.cliPlugin === plugin)) {
|
|
107
|
+
// default plugins are run before user plugins
|
|
108
|
+
processedPlugins.unshift({ cliPlugin: plugin, pluginOptions: options });
|
|
93
109
|
}
|
|
94
110
|
});
|
|
95
111
|
|
|
@@ -142,3 +158,69 @@ function getPluginOptions(plugin: Plugin): Record<string, unknown> {
|
|
|
142
158
|
}
|
|
143
159
|
return result;
|
|
144
160
|
}
|
|
161
|
+
|
|
162
|
+
async function loadPluginModule(provider: string, basePath: string) {
|
|
163
|
+
let moduleSpec = provider;
|
|
164
|
+
if (moduleSpec.startsWith('.')) {
|
|
165
|
+
// relative to schema's path
|
|
166
|
+
moduleSpec = path.resolve(basePath, moduleSpec);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const importAsEsm = async (spec: string) => {
|
|
170
|
+
try {
|
|
171
|
+
const result = (await import(spec)).default as CliPlugin;
|
|
172
|
+
return result;
|
|
173
|
+
} catch (err) {
|
|
174
|
+
throw new CliError(`Failed to load plugin module from ${spec}: ${(err as Error).message}`);
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const jiti = createJiti(pathToFileURL(basePath).toString());
|
|
179
|
+
const importAsTs = async (spec: string) => {
|
|
180
|
+
try {
|
|
181
|
+
const result = (await jiti.import(spec, { default: true })) as CliPlugin;
|
|
182
|
+
return result;
|
|
183
|
+
} catch (err) {
|
|
184
|
+
throw new CliError(`Failed to load plugin module from ${spec}: ${(err as Error).message}`);
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const esmSuffixes = ['.js', '.mjs'];
|
|
189
|
+
const tsSuffixes = ['.ts', '.mts'];
|
|
190
|
+
|
|
191
|
+
if (fs.existsSync(moduleSpec) && fs.statSync(moduleSpec).isFile()) {
|
|
192
|
+
// try provider as ESM file
|
|
193
|
+
if (esmSuffixes.some((suffix) => moduleSpec.endsWith(suffix))) {
|
|
194
|
+
return await importAsEsm(pathToFileURL(moduleSpec).toString());
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// try provider as TS file
|
|
198
|
+
if (tsSuffixes.some((suffix) => moduleSpec.endsWith(suffix))) {
|
|
199
|
+
return await importAsTs(moduleSpec);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// try ESM index files in provider directory
|
|
204
|
+
for (const suffix of esmSuffixes) {
|
|
205
|
+
const indexPath = path.join(moduleSpec, `index${suffix}`);
|
|
206
|
+
if (fs.existsSync(indexPath)) {
|
|
207
|
+
return await importAsEsm(pathToFileURL(indexPath).toString());
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// try TS index files in provider directory
|
|
212
|
+
for (const suffix of tsSuffixes) {
|
|
213
|
+
const indexPath = path.join(moduleSpec, `index${suffix}`);
|
|
214
|
+
if (fs.existsSync(indexPath)) {
|
|
215
|
+
return await importAsTs(indexPath);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// last resort, try to import as esm directly
|
|
220
|
+
try {
|
|
221
|
+
return (await import(moduleSpec)).default as CliPlugin;
|
|
222
|
+
} catch {
|
|
223
|
+
// plugin may not export a generator so we simply ignore the error here
|
|
224
|
+
return undefined;
|
|
225
|
+
}
|
|
226
|
+
}
|
package/src/actions/index.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { run as check } from './check';
|
|
1
2
|
import { run as db } from './db';
|
|
3
|
+
import { run as format } from './format';
|
|
2
4
|
import { run as generate } from './generate';
|
|
3
5
|
import { run as info } from './info';
|
|
4
6
|
import { run as init } from './init';
|
|
5
7
|
import { run as migrate } from './migrate';
|
|
6
|
-
import { run as
|
|
8
|
+
import { run as seed } from './seed';
|
|
7
9
|
|
|
8
|
-
export { db, generate, info, init, migrate,
|
|
10
|
+
export { check, db, format, generate, info, init, migrate, seed };
|
package/src/actions/init.ts
CHANGED
|
@@ -12,8 +12,8 @@ import { STARTER_ZMODEL } from './templates';
|
|
|
12
12
|
*/
|
|
13
13
|
export async function run(projectPath: string) {
|
|
14
14
|
const packages = [
|
|
15
|
-
{ name: '@zenstackhq/cli@
|
|
16
|
-
{ name: '@zenstackhq/
|
|
15
|
+
{ name: '@zenstackhq/cli@latest', dev: true },
|
|
16
|
+
{ name: '@zenstackhq/orm@latest', dev: false },
|
|
17
17
|
];
|
|
18
18
|
let pm = await detect();
|
|
19
19
|
if (!pm) {
|
|
@@ -23,9 +23,9 @@ export async function run(projectPath: string) {
|
|
|
23
23
|
console.log(colors.gray(`Using package manager: ${pm.agent}`));
|
|
24
24
|
|
|
25
25
|
for (const pkg of packages) {
|
|
26
|
-
const resolved = resolveCommand(pm.agent, '
|
|
26
|
+
const resolved = resolveCommand(pm.agent, 'add', [
|
|
27
27
|
pkg.name,
|
|
28
|
-
...(pkg.dev ? [pm.agent === '
|
|
28
|
+
...(pkg.dev ? [pm.agent.startsWith('yarn') || pm.agent === 'bun' ? '--dev' : '--save-dev'] : []),
|
|
29
29
|
]);
|
|
30
30
|
if (!resolved) {
|
|
31
31
|
throw new CliError(`Unable to determine how to install package "${pkg.name}". Please install it manually.`);
|
package/src/actions/migrate.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { CliError } from '../cli-error';
|
|
4
|
-
import {
|
|
5
|
-
import { generateTempPrismaSchema, getSchemaFile } from './action-utils';
|
|
4
|
+
import { execPrisma } from '../utils/exec-utils';
|
|
5
|
+
import { generateTempPrismaSchema, getSchemaFile, requireDataSourceUrl } from './action-utils';
|
|
6
|
+
import { run as runSeed } from './seed';
|
|
6
7
|
|
|
7
8
|
type CommonOptions = {
|
|
8
9
|
schema?: string;
|
|
9
10
|
migrations?: string;
|
|
11
|
+
skipSeed?: boolean;
|
|
10
12
|
};
|
|
11
13
|
|
|
12
14
|
type DevOptions = CommonOptions & {
|
|
@@ -32,6 +34,10 @@ type ResolveOptions = CommonOptions & {
|
|
|
32
34
|
*/
|
|
33
35
|
export async function run(command: string, options: CommonOptions) {
|
|
34
36
|
const schemaFile = getSchemaFile(options.schema);
|
|
37
|
+
|
|
38
|
+
// validate datasource url exists
|
|
39
|
+
await requireDataSourceUrl(schemaFile);
|
|
40
|
+
|
|
35
41
|
const prismaSchemaDir = options.migrations ? path.dirname(options.migrations) : undefined;
|
|
36
42
|
const prismaSchemaFile = await generateTempPrismaSchema(schemaFile, prismaSchemaDir);
|
|
37
43
|
|
|
@@ -64,17 +70,17 @@ export async function run(command: string, options: CommonOptions) {
|
|
|
64
70
|
}
|
|
65
71
|
}
|
|
66
72
|
|
|
67
|
-
|
|
73
|
+
function runDev(prismaSchemaFile: string, options: DevOptions) {
|
|
68
74
|
try {
|
|
69
75
|
const cmd = [
|
|
70
|
-
'
|
|
76
|
+
'migrate dev',
|
|
71
77
|
` --schema "${prismaSchemaFile}"`,
|
|
72
78
|
' --skip-generate',
|
|
73
|
-
|
|
79
|
+
' --skip-seed',
|
|
80
|
+
options.name ? ` --name "${options.name}"` : '',
|
|
74
81
|
options.createOnly ? ' --create-only' : '',
|
|
75
82
|
].join('');
|
|
76
|
-
|
|
77
|
-
await execPackage(cmd);
|
|
83
|
+
execPrisma(cmd);
|
|
78
84
|
} catch (err) {
|
|
79
85
|
handleSubProcessError(err);
|
|
80
86
|
}
|
|
@@ -82,48 +88,53 @@ async function runDev(prismaSchemaFile: string, options: DevOptions) {
|
|
|
82
88
|
|
|
83
89
|
async function runReset(prismaSchemaFile: string, options: ResetOptions) {
|
|
84
90
|
try {
|
|
85
|
-
const cmd = [
|
|
86
|
-
'',
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
91
|
+
const cmd = [
|
|
92
|
+
'migrate reset',
|
|
93
|
+
` --schema "${prismaSchemaFile}"`,
|
|
94
|
+
' --skip-generate',
|
|
95
|
+
' --skip-seed',
|
|
96
|
+
options.force ? ' --force' : '',
|
|
97
|
+
].join('');
|
|
98
|
+
execPrisma(cmd);
|
|
90
99
|
} catch (err) {
|
|
91
100
|
handleSubProcessError(err);
|
|
92
101
|
}
|
|
102
|
+
|
|
103
|
+
if (!options.skipSeed) {
|
|
104
|
+
await runSeed({ noWarnings: true, printStatus: true }, []);
|
|
105
|
+
}
|
|
93
106
|
}
|
|
94
107
|
|
|
95
|
-
|
|
108
|
+
function runDeploy(prismaSchemaFile: string, _options: DeployOptions) {
|
|
96
109
|
try {
|
|
97
|
-
const cmd = ['
|
|
98
|
-
|
|
99
|
-
await execPackage(cmd);
|
|
110
|
+
const cmd = ['migrate deploy', ` --schema "${prismaSchemaFile}"`].join('');
|
|
111
|
+
execPrisma(cmd);
|
|
100
112
|
} catch (err) {
|
|
101
113
|
handleSubProcessError(err);
|
|
102
114
|
}
|
|
103
115
|
}
|
|
104
116
|
|
|
105
|
-
|
|
117
|
+
function runStatus(prismaSchemaFile: string, _options: StatusOptions) {
|
|
106
118
|
try {
|
|
107
|
-
|
|
119
|
+
execPrisma(`migrate status --schema "${prismaSchemaFile}"`);
|
|
108
120
|
} catch (err) {
|
|
109
121
|
handleSubProcessError(err);
|
|
110
122
|
}
|
|
111
123
|
}
|
|
112
124
|
|
|
113
|
-
|
|
125
|
+
function runResolve(prismaSchemaFile: string, options: ResolveOptions) {
|
|
114
126
|
if (!options.applied && !options.rolledBack) {
|
|
115
127
|
throw new CliError('Either --applied or --rolled-back option must be provided');
|
|
116
128
|
}
|
|
117
129
|
|
|
118
130
|
try {
|
|
119
131
|
const cmd = [
|
|
120
|
-
'
|
|
132
|
+
'migrate resolve',
|
|
121
133
|
` --schema "${prismaSchemaFile}"`,
|
|
122
|
-
options.applied ? ` --applied ${options.applied}` : '',
|
|
123
|
-
options.rolledBack ? ` --rolled-back ${options.rolledBack}` : '',
|
|
134
|
+
options.applied ? ` --applied "${options.applied}"` : '',
|
|
135
|
+
options.rolledBack ? ` --rolled-back "${options.rolledBack}"` : '',
|
|
124
136
|
].join('');
|
|
125
|
-
|
|
126
|
-
await execPackage(cmd);
|
|
137
|
+
execPrisma(cmd);
|
|
127
138
|
} catch (err) {
|
|
128
139
|
handleSubProcessError(err);
|
|
129
140
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import colors from 'colors';
|
|
2
|
+
import { execaCommand } from 'execa';
|
|
3
|
+
import { CliError } from '../cli-error';
|
|
4
|
+
import { getPkgJsonConfig } from './action-utils';
|
|
5
|
+
|
|
6
|
+
type Options = {
|
|
7
|
+
noWarnings?: boolean;
|
|
8
|
+
printStatus?: boolean;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* CLI action for seeding the database.
|
|
13
|
+
*/
|
|
14
|
+
export async function run(options: Options, args: string[]) {
|
|
15
|
+
const pkgJsonConfig = getPkgJsonConfig(process.cwd());
|
|
16
|
+
if (!pkgJsonConfig.seed) {
|
|
17
|
+
if (!options.noWarnings) {
|
|
18
|
+
console.warn(colors.yellow('No seed script defined in package.json. Skipping seeding.'));
|
|
19
|
+
}
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const command = `${pkgJsonConfig.seed}${args.length > 0 ? ' ' + args.join(' ') : ''}`;
|
|
24
|
+
|
|
25
|
+
if (options.printStatus) {
|
|
26
|
+
console.log(colors.gray(`Running seed script "${command}"...`));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
await execaCommand(command, {
|
|
31
|
+
stdout: 'inherit',
|
|
32
|
+
stderr: 'inherit',
|
|
33
|
+
});
|
|
34
|
+
} catch (err) {
|
|
35
|
+
console.error(colors.red(err instanceof Error ? err.message : String(err)));
|
|
36
|
+
throw new CliError('Failed to seed the database. Please check the error message above for details.');
|
|
37
|
+
}
|
|
38
|
+
}
|
package/src/actions/templates.ts
CHANGED
|
@@ -21,23 +21,23 @@ model Post {
|
|
|
21
21
|
title String @length(1, 256)
|
|
22
22
|
content String
|
|
23
23
|
published Boolean @default(false)
|
|
24
|
-
author User @relation(fields: [authorId], references: [id])
|
|
24
|
+
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
|
|
25
25
|
authorId String
|
|
26
26
|
}
|
|
27
27
|
`;
|
|
28
28
|
|
|
29
|
-
export const STARTER_MAIN_TS = `import { ZenStackClient } from '@zenstackhq/
|
|
29
|
+
export const STARTER_MAIN_TS = `import { ZenStackClient } from '@zenstackhq/orm';
|
|
30
|
+
import { SqliteDialect } from '@zenstackhq/orm/dialects/sqlite';
|
|
30
31
|
import SQLite from 'better-sqlite3';
|
|
31
|
-
import { SqliteDialect } from 'kysely';
|
|
32
32
|
import { schema } from './zenstack/schema';
|
|
33
33
|
|
|
34
34
|
async function main() {
|
|
35
|
-
const
|
|
35
|
+
const db = new ZenStackClient(schema, {
|
|
36
36
|
dialect: new SqliteDialect({
|
|
37
37
|
database: new SQLite('./zenstack/dev.db'),
|
|
38
38
|
}),
|
|
39
39
|
});
|
|
40
|
-
const user = await
|
|
40
|
+
const user = await db.user.create({
|
|
41
41
|
data: {
|
|
42
42
|
email: 'test@zenstack.dev',
|
|
43
43
|
posts: {
|
package/src/constants.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -30,10 +30,19 @@ const checkAction = async (options: Parameters<typeof actions.check>[0]): Promis
|
|
|
30
30
|
await telemetry.trackCommand('check', () => actions.check(options));
|
|
31
31
|
};
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
const formatAction = async (options: Parameters<typeof actions.format>[0]): Promise<void> => {
|
|
34
|
+
await telemetry.trackCommand('format', () => actions.format(options));
|
|
35
|
+
};
|
|
35
36
|
|
|
36
|
-
|
|
37
|
+
const seedAction = async (options: Parameters<typeof actions.seed>[0], args: string[]): Promise<void> => {
|
|
38
|
+
await telemetry.trackCommand('db seed', () => actions.seed(options, args));
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
function createProgram() {
|
|
42
|
+
const program = new Command('zen')
|
|
43
|
+
.alias('zenstack')
|
|
44
|
+
.helpOption('-h, --help', 'Show this help message')
|
|
45
|
+
.version(getVersion()!, '-v --version', 'Show CLI version');
|
|
37
46
|
|
|
38
47
|
const schemaExtensions = ZModelLanguageMetaData.fileExtensions.join(', ');
|
|
39
48
|
|
|
@@ -41,7 +50,7 @@ function createProgram() {
|
|
|
41
50
|
.description(
|
|
42
51
|
`${colors.bold.blue(
|
|
43
52
|
'ζ',
|
|
44
|
-
)} ZenStack is the data layer for
|
|
53
|
+
)} ZenStack is the modern data layer for TypeScript apps.\n\nDocumentation: https://zenstack.dev/docs`,
|
|
45
54
|
)
|
|
46
55
|
.showHelpAfterError()
|
|
47
56
|
.showSuggestionAfterError();
|
|
@@ -55,10 +64,12 @@ function createProgram() {
|
|
|
55
64
|
|
|
56
65
|
program
|
|
57
66
|
.command('generate')
|
|
58
|
-
.description('Run code generation plugins
|
|
67
|
+
.description('Run code generation plugins')
|
|
59
68
|
.addOption(schemaOption)
|
|
60
69
|
.addOption(noVersionCheckOption)
|
|
61
70
|
.addOption(new Option('-o, --output <path>', 'default output directory for code generation'))
|
|
71
|
+
.addOption(new Option('--lite', 'also generate a lite version of schema without attributes').default(false))
|
|
72
|
+
.addOption(new Option('--lite-only', 'only generate lite version of schema without attributes').default(false))
|
|
62
73
|
.addOption(new Option('--silent', 'suppress all output except errors').default(false))
|
|
63
74
|
.action(generateAction);
|
|
64
75
|
|
|
@@ -72,7 +83,7 @@ function createProgram() {
|
|
|
72
83
|
.addOption(new Option('-n, --name <name>', 'migration name'))
|
|
73
84
|
.addOption(new Option('--create-only', 'only create migration, do not apply'))
|
|
74
85
|
.addOption(migrationsOption)
|
|
75
|
-
.description('Create a migration from changes in schema and apply it to the database
|
|
86
|
+
.description('Create a migration from changes in schema and apply it to the database')
|
|
76
87
|
.action((options) => migrateAction('dev', options));
|
|
77
88
|
|
|
78
89
|
migrateCommand
|
|
@@ -80,8 +91,13 @@ function createProgram() {
|
|
|
80
91
|
.addOption(schemaOption)
|
|
81
92
|
.addOption(new Option('--force', 'skip the confirmation prompt'))
|
|
82
93
|
.addOption(migrationsOption)
|
|
94
|
+
.addOption(new Option('--skip-seed', 'skip seeding the database after reset'))
|
|
83
95
|
.addOption(noVersionCheckOption)
|
|
84
|
-
.description('Reset your database and apply all migrations, all data will be lost
|
|
96
|
+
.description('Reset your database and apply all migrations, all data will be lost')
|
|
97
|
+
.addHelpText(
|
|
98
|
+
'after',
|
|
99
|
+
'\nIf there is a seed script defined in package.json, it will be run after the reset. Use --skip-seed to skip it.',
|
|
100
|
+
)
|
|
85
101
|
.action((options) => migrateAction('reset', options));
|
|
86
102
|
|
|
87
103
|
migrateCommand
|
|
@@ -89,7 +105,7 @@ function createProgram() {
|
|
|
89
105
|
.addOption(schemaOption)
|
|
90
106
|
.addOption(noVersionCheckOption)
|
|
91
107
|
.addOption(migrationsOption)
|
|
92
|
-
.description('Deploy your pending migrations to your production/staging database
|
|
108
|
+
.description('Deploy your pending migrations to your production/staging database')
|
|
93
109
|
.action((options) => migrateAction('deploy', options));
|
|
94
110
|
|
|
95
111
|
migrateCommand
|
|
@@ -97,7 +113,7 @@ function createProgram() {
|
|
|
97
113
|
.addOption(schemaOption)
|
|
98
114
|
.addOption(noVersionCheckOption)
|
|
99
115
|
.addOption(migrationsOption)
|
|
100
|
-
.description('Check the status of your database migrations
|
|
116
|
+
.description('Check the status of your database migrations')
|
|
101
117
|
.action((options) => migrateAction('status', options));
|
|
102
118
|
|
|
103
119
|
migrateCommand
|
|
@@ -107,41 +123,70 @@ function createProgram() {
|
|
|
107
123
|
.addOption(migrationsOption)
|
|
108
124
|
.addOption(new Option('--applied <migration>', 'record a specific migration as applied'))
|
|
109
125
|
.addOption(new Option('--rolled-back <migration>', 'record a specific migration as rolled back'))
|
|
110
|
-
.description('Resolve issues with database migrations in deployment databases
|
|
126
|
+
.description('Resolve issues with database migrations in deployment databases')
|
|
111
127
|
.action((options) => migrateAction('resolve', options));
|
|
112
128
|
|
|
113
|
-
const dbCommand = program.command('db').description('Manage your database schema during development
|
|
129
|
+
const dbCommand = program.command('db').description('Manage your database schema during development');
|
|
114
130
|
|
|
115
131
|
dbCommand
|
|
116
132
|
.command('push')
|
|
117
|
-
.description('Push the state from your schema to your database
|
|
133
|
+
.description('Push the state from your schema to your database')
|
|
118
134
|
.addOption(schemaOption)
|
|
119
135
|
.addOption(noVersionCheckOption)
|
|
120
136
|
.addOption(new Option('--accept-data-loss', 'ignore data loss warnings'))
|
|
121
137
|
.addOption(new Option('--force-reset', 'force a reset of the database before push'))
|
|
122
138
|
.action((options) => dbAction('push', options));
|
|
123
139
|
|
|
140
|
+
dbCommand
|
|
141
|
+
.command('seed')
|
|
142
|
+
.description('Seed the database')
|
|
143
|
+
.allowExcessArguments(true)
|
|
144
|
+
.addHelpText(
|
|
145
|
+
'after',
|
|
146
|
+
`
|
|
147
|
+
Seed script is configured under the "zenstack.seed" field in package.json.
|
|
148
|
+
E.g.:
|
|
149
|
+
{
|
|
150
|
+
"zenstack": {
|
|
151
|
+
"seed": "ts-node ./zenstack/seed.ts"
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
Arguments following -- are passed to the seed script. E.g.: "zen db seed -- --users 10"`,
|
|
156
|
+
)
|
|
157
|
+
.addOption(noVersionCheckOption)
|
|
158
|
+
.action((options, command) => seedAction(options, command.args));
|
|
159
|
+
|
|
124
160
|
program
|
|
125
161
|
.command('info')
|
|
126
|
-
.description('Get information of installed ZenStack packages
|
|
162
|
+
.description('Get information of installed ZenStack packages')
|
|
127
163
|
.argument('[path]', 'project path', '.')
|
|
128
164
|
.addOption(noVersionCheckOption)
|
|
129
165
|
.action(infoAction);
|
|
130
166
|
|
|
131
167
|
program
|
|
132
168
|
.command('init')
|
|
133
|
-
.description('Initialize an existing project for ZenStack
|
|
169
|
+
.description('Initialize an existing project for ZenStack')
|
|
134
170
|
.argument('[path]', 'project path', '.')
|
|
135
171
|
.addOption(noVersionCheckOption)
|
|
136
172
|
.action(initAction);
|
|
137
173
|
|
|
138
174
|
program
|
|
139
175
|
.command('check')
|
|
140
|
-
.description('Check a ZModel schema for syntax or semantic errors
|
|
176
|
+
.description('Check a ZModel schema for syntax or semantic errors')
|
|
141
177
|
.addOption(schemaOption)
|
|
142
178
|
.addOption(noVersionCheckOption)
|
|
143
179
|
.action(checkAction);
|
|
144
180
|
|
|
181
|
+
program
|
|
182
|
+
.command('format')
|
|
183
|
+
.description('Format a ZModel schema file')
|
|
184
|
+
.addOption(schemaOption)
|
|
185
|
+
.addOption(noVersionCheckOption)
|
|
186
|
+
.action(formatAction);
|
|
187
|
+
|
|
188
|
+
program.addHelpCommand('help [command]', 'Display help for a command');
|
|
189
|
+
|
|
145
190
|
program.hook('preAction', async (_thisCommand, actionCommand) => {
|
|
146
191
|
if (actionCommand.getOptionValue('versionCheck') !== false) {
|
|
147
192
|
await checkNewVersion();
|
|
@@ -7,6 +7,7 @@ const plugin: CliPlugin = {
|
|
|
7
7
|
name: 'TypeScript Schema Generator',
|
|
8
8
|
statusText: 'Generating TypeScript schema',
|
|
9
9
|
async generate({ model, defaultOutputPath, pluginOptions }) {
|
|
10
|
+
// output path
|
|
10
11
|
let outDir = defaultOutputPath;
|
|
11
12
|
if (typeof pluginOptions['output'] === 'string') {
|
|
12
13
|
outDir = path.resolve(defaultOutputPath, pluginOptions['output']);
|
|
@@ -14,7 +15,25 @@ const plugin: CliPlugin = {
|
|
|
14
15
|
fs.mkdirSync(outDir, { recursive: true });
|
|
15
16
|
}
|
|
16
17
|
}
|
|
17
|
-
|
|
18
|
+
|
|
19
|
+
// lite mode
|
|
20
|
+
const lite = pluginOptions['lite'] === true;
|
|
21
|
+
|
|
22
|
+
// liteOnly mode
|
|
23
|
+
const liteOnly = pluginOptions['liteOnly'] === true;
|
|
24
|
+
|
|
25
|
+
// add .js extension when importing
|
|
26
|
+
const importWithFileExtension = pluginOptions['importWithFileExtension'];
|
|
27
|
+
if (importWithFileExtension && typeof importWithFileExtension !== 'string') {
|
|
28
|
+
throw new Error('The "importWithFileExtension" option must be a string if specified.');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
await new TsSchemaGenerator().generate(model, {
|
|
32
|
+
outDir,
|
|
33
|
+
lite,
|
|
34
|
+
liteOnly,
|
|
35
|
+
importWithFileExtension: importWithFileExtension as string | undefined,
|
|
36
|
+
});
|
|
18
37
|
},
|
|
19
38
|
};
|
|
20
39
|
|
package/src/utils/exec-utils.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { execSync as _exec, type ExecSyncOptions } from 'child_process';
|
|
2
|
+
import { fileURLToPath } from 'url';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Utility for executing command synchronously and prints outputs on current console
|
|
@@ -24,3 +25,37 @@ export function execPackage(
|
|
|
24
25
|
const packageManager = process?.versions?.['bun'] ? 'bunx' : 'npx';
|
|
25
26
|
execSync(`${packageManager} ${cmd}`, options);
|
|
26
27
|
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Utility for running prisma commands
|
|
31
|
+
*/
|
|
32
|
+
export function execPrisma(args: string, options?: Omit<ExecSyncOptions, 'env'> & { env?: Record<string, string> }) {
|
|
33
|
+
let prismaPath: string | undefined;
|
|
34
|
+
try {
|
|
35
|
+
if (typeof import.meta.resolve === 'function') {
|
|
36
|
+
// esm
|
|
37
|
+
prismaPath = fileURLToPath(import.meta.resolve('prisma/build/index.js'));
|
|
38
|
+
} else {
|
|
39
|
+
// cjs
|
|
40
|
+
prismaPath = require.resolve('prisma/build/index.js');
|
|
41
|
+
}
|
|
42
|
+
} catch {
|
|
43
|
+
// ignore and fallback
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const _options = {
|
|
47
|
+
...options,
|
|
48
|
+
env: {
|
|
49
|
+
...options?.env,
|
|
50
|
+
PRISMA_HIDE_UPDATE_MESSAGE: '1',
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
if (!prismaPath) {
|
|
55
|
+
// fallback to npx/bunx execute
|
|
56
|
+
execPackage(`prisma ${args}`, _options);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
execSync(`node ${prismaPath} ${args}`, _options);
|
|
61
|
+
}
|