@zenstackhq/cli 3.0.0-beta.23 → 3.0.0-beta.25
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 -26
- package/dist/index.cjs +241 -129
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +238 -126
- package/dist/index.js.map +1 -1
- package/package.json +11 -12
- package/src/actions/action-utils.ts +27 -7
- package/src/actions/db.ts +6 -2
- package/src/actions/format.ts +27 -0
- package/src/actions/generate.ts +13 -1
- package/src/actions/index.ts +4 -2
- package/src/actions/migrate.ts +14 -2
- package/src/actions/seed.ts +38 -0
- package/src/index.ts +57 -14
- package/src/plugins/typescript.ts +12 -1
- package/src/utils/exec-utils.ts +18 -7
- package/test/db.test.ts +43 -0
- package/test/format.test.ts +33 -0
package/src/actions/migrate.ts
CHANGED
|
@@ -2,11 +2,13 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { CliError } from '../cli-error';
|
|
4
4
|
import { execPrisma } from '../utils/exec-utils';
|
|
5
|
-
import { generateTempPrismaSchema, getSchemaFile } from './action-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
|
|
|
@@ -70,6 +76,7 @@ function runDev(prismaSchemaFile: string, options: DevOptions) {
|
|
|
70
76
|
'migrate dev',
|
|
71
77
|
` --schema "${prismaSchemaFile}"`,
|
|
72
78
|
' --skip-generate',
|
|
79
|
+
' --skip-seed',
|
|
73
80
|
options.name ? ` --name "${options.name}"` : '',
|
|
74
81
|
options.createOnly ? ' --create-only' : '',
|
|
75
82
|
].join('');
|
|
@@ -79,18 +86,23 @@ function runDev(prismaSchemaFile: string, options: DevOptions) {
|
|
|
79
86
|
}
|
|
80
87
|
}
|
|
81
88
|
|
|
82
|
-
function runReset(prismaSchemaFile: string, options: ResetOptions) {
|
|
89
|
+
async function runReset(prismaSchemaFile: string, options: ResetOptions) {
|
|
83
90
|
try {
|
|
84
91
|
const cmd = [
|
|
85
92
|
'migrate reset',
|
|
86
93
|
` --schema "${prismaSchemaFile}"`,
|
|
87
94
|
' --skip-generate',
|
|
95
|
+
' --skip-seed',
|
|
88
96
|
options.force ? ' --force' : '',
|
|
89
97
|
].join('');
|
|
90
98
|
execPrisma(cmd);
|
|
91
99
|
} catch (err) {
|
|
92
100
|
handleSubProcessError(err);
|
|
93
101
|
}
|
|
102
|
+
|
|
103
|
+
if (!options.skipSeed) {
|
|
104
|
+
await runSeed({ noWarnings: true, printStatus: true }, []);
|
|
105
|
+
}
|
|
94
106
|
}
|
|
95
107
|
|
|
96
108
|
function runDeploy(prismaSchemaFile: string, _options: DeployOptions) {
|
|
@@ -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/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
|
|
|
@@ -55,7 +64,7 @@ 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'))
|
|
@@ -74,7 +83,7 @@ function createProgram() {
|
|
|
74
83
|
.addOption(new Option('-n, --name <name>', 'migration name'))
|
|
75
84
|
.addOption(new Option('--create-only', 'only create migration, do not apply'))
|
|
76
85
|
.addOption(migrationsOption)
|
|
77
|
-
.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')
|
|
78
87
|
.action((options) => migrateAction('dev', options));
|
|
79
88
|
|
|
80
89
|
migrateCommand
|
|
@@ -82,8 +91,13 @@ function createProgram() {
|
|
|
82
91
|
.addOption(schemaOption)
|
|
83
92
|
.addOption(new Option('--force', 'skip the confirmation prompt'))
|
|
84
93
|
.addOption(migrationsOption)
|
|
94
|
+
.addOption(new Option('--skip-seed', 'skip seeding the database after reset'))
|
|
85
95
|
.addOption(noVersionCheckOption)
|
|
86
|
-
.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
|
+
)
|
|
87
101
|
.action((options) => migrateAction('reset', options));
|
|
88
102
|
|
|
89
103
|
migrateCommand
|
|
@@ -91,7 +105,7 @@ function createProgram() {
|
|
|
91
105
|
.addOption(schemaOption)
|
|
92
106
|
.addOption(noVersionCheckOption)
|
|
93
107
|
.addOption(migrationsOption)
|
|
94
|
-
.description('Deploy your pending migrations to your production/staging database
|
|
108
|
+
.description('Deploy your pending migrations to your production/staging database')
|
|
95
109
|
.action((options) => migrateAction('deploy', options));
|
|
96
110
|
|
|
97
111
|
migrateCommand
|
|
@@ -99,7 +113,7 @@ function createProgram() {
|
|
|
99
113
|
.addOption(schemaOption)
|
|
100
114
|
.addOption(noVersionCheckOption)
|
|
101
115
|
.addOption(migrationsOption)
|
|
102
|
-
.description('Check the status of your database migrations
|
|
116
|
+
.description('Check the status of your database migrations')
|
|
103
117
|
.action((options) => migrateAction('status', options));
|
|
104
118
|
|
|
105
119
|
migrateCommand
|
|
@@ -109,41 +123,70 @@ function createProgram() {
|
|
|
109
123
|
.addOption(migrationsOption)
|
|
110
124
|
.addOption(new Option('--applied <migration>', 'record a specific migration as applied'))
|
|
111
125
|
.addOption(new Option('--rolled-back <migration>', 'record a specific migration as rolled back'))
|
|
112
|
-
.description('Resolve issues with database migrations in deployment databases
|
|
126
|
+
.description('Resolve issues with database migrations in deployment databases')
|
|
113
127
|
.action((options) => migrateAction('resolve', options));
|
|
114
128
|
|
|
115
|
-
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');
|
|
116
130
|
|
|
117
131
|
dbCommand
|
|
118
132
|
.command('push')
|
|
119
|
-
.description('Push the state from your schema to your database
|
|
133
|
+
.description('Push the state from your schema to your database')
|
|
120
134
|
.addOption(schemaOption)
|
|
121
135
|
.addOption(noVersionCheckOption)
|
|
122
136
|
.addOption(new Option('--accept-data-loss', 'ignore data loss warnings'))
|
|
123
137
|
.addOption(new Option('--force-reset', 'force a reset of the database before push'))
|
|
124
138
|
.action((options) => dbAction('push', options));
|
|
125
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
|
+
|
|
126
160
|
program
|
|
127
161
|
.command('info')
|
|
128
|
-
.description('Get information of installed ZenStack packages
|
|
162
|
+
.description('Get information of installed ZenStack packages')
|
|
129
163
|
.argument('[path]', 'project path', '.')
|
|
130
164
|
.addOption(noVersionCheckOption)
|
|
131
165
|
.action(infoAction);
|
|
132
166
|
|
|
133
167
|
program
|
|
134
168
|
.command('init')
|
|
135
|
-
.description('Initialize an existing project for ZenStack
|
|
169
|
+
.description('Initialize an existing project for ZenStack')
|
|
136
170
|
.argument('[path]', 'project path', '.')
|
|
137
171
|
.addOption(noVersionCheckOption)
|
|
138
172
|
.action(initAction);
|
|
139
173
|
|
|
140
174
|
program
|
|
141
175
|
.command('check')
|
|
142
|
-
.description('Check a ZModel schema for syntax or semantic errors
|
|
176
|
+
.description('Check a ZModel schema for syntax or semantic errors')
|
|
143
177
|
.addOption(schemaOption)
|
|
144
178
|
.addOption(noVersionCheckOption)
|
|
145
179
|
.action(checkAction);
|
|
146
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
|
+
|
|
147
190
|
program.hook('preAction', async (_thisCommand, actionCommand) => {
|
|
148
191
|
if (actionCommand.getOptionValue('versionCheck') !== false) {
|
|
149
192
|
await checkNewVersion();
|
|
@@ -22,7 +22,18 @@ const plugin: CliPlugin = {
|
|
|
22
22
|
// liteOnly mode
|
|
23
23
|
const liteOnly = pluginOptions['liteOnly'] === true;
|
|
24
24
|
|
|
25
|
-
|
|
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
|
+
});
|
|
26
37
|
},
|
|
27
38
|
};
|
|
28
39
|
|
package/src/utils/exec-utils.ts
CHANGED
|
@@ -30,13 +30,24 @@ export function execPackage(
|
|
|
30
30
|
* Utility for running prisma commands
|
|
31
31
|
*/
|
|
32
32
|
export function execPrisma(args: string, options?: Omit<ExecSyncOptions, 'env'> & { env?: Record<string, string> }) {
|
|
33
|
-
let prismaPath: string;
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
|
40
44
|
}
|
|
45
|
+
|
|
46
|
+
if (!prismaPath) {
|
|
47
|
+
// fallback to npx/bunx execute
|
|
48
|
+
execPackage(`prisma ${args}`, options);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
41
52
|
execSync(`node ${prismaPath} ${args}`, options);
|
|
42
53
|
}
|
package/test/db.test.ts
CHANGED
|
@@ -15,4 +15,47 @@ describe('CLI db commands test', () => {
|
|
|
15
15
|
runCli('db push', workDir);
|
|
16
16
|
expect(fs.existsSync(path.join(workDir, 'zenstack/dev.db'))).toBe(true);
|
|
17
17
|
});
|
|
18
|
+
|
|
19
|
+
it('should seed the database with db seed with seed script', () => {
|
|
20
|
+
const workDir = createProject(model);
|
|
21
|
+
const pkgJson = JSON.parse(fs.readFileSync(path.join(workDir, 'package.json'), 'utf8'));
|
|
22
|
+
pkgJson.zenstack = {
|
|
23
|
+
seed: 'node seed.js',
|
|
24
|
+
};
|
|
25
|
+
fs.writeFileSync(path.join(workDir, 'package.json'), JSON.stringify(pkgJson, null, 2));
|
|
26
|
+
fs.writeFileSync(
|
|
27
|
+
path.join(workDir, 'seed.js'),
|
|
28
|
+
`
|
|
29
|
+
import fs from 'node:fs';
|
|
30
|
+
fs.writeFileSync('seed.txt', 'success');
|
|
31
|
+
`,
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
runCli('db seed', workDir);
|
|
35
|
+
expect(fs.readFileSync(path.join(workDir, 'seed.txt'), 'utf8')).toBe('success');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should seed the database after migrate reset', () => {
|
|
39
|
+
const workDir = createProject(model);
|
|
40
|
+
const pkgJson = JSON.parse(fs.readFileSync(path.join(workDir, 'package.json'), 'utf8'));
|
|
41
|
+
pkgJson.zenstack = {
|
|
42
|
+
seed: 'node seed.js',
|
|
43
|
+
};
|
|
44
|
+
fs.writeFileSync(path.join(workDir, 'package.json'), JSON.stringify(pkgJson, null, 2));
|
|
45
|
+
fs.writeFileSync(
|
|
46
|
+
path.join(workDir, 'seed.js'),
|
|
47
|
+
`
|
|
48
|
+
import fs from 'node:fs';
|
|
49
|
+
fs.writeFileSync('seed.txt', 'success');
|
|
50
|
+
`,
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
runCli('migrate reset --force', workDir);
|
|
54
|
+
expect(fs.readFileSync(path.join(workDir, 'seed.txt'), 'utf8')).toBe('success');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should skip seeding the database without seed script', () => {
|
|
58
|
+
const workDir = createProject(model);
|
|
59
|
+
runCli('db seed', workDir);
|
|
60
|
+
});
|
|
18
61
|
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { createProject, runCli } from './utils';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
|
|
5
|
+
const model = `
|
|
6
|
+
model User {
|
|
7
|
+
id String @id @default(cuid())
|
|
8
|
+
email String @unique
|
|
9
|
+
}
|
|
10
|
+
`;
|
|
11
|
+
|
|
12
|
+
describe('CLI format command test', () => {
|
|
13
|
+
it('should format a valid schema successfully', () => {
|
|
14
|
+
const workDir = createProject(model);
|
|
15
|
+
expect(() => runCli('format', workDir)).not.toThrow();
|
|
16
|
+
const updatedContent = fs.readFileSync(`${workDir}/zenstack/schema.zmodel`, 'utf-8');
|
|
17
|
+
expect(
|
|
18
|
+
updatedContent.includes(`model User {
|
|
19
|
+
id String @id @default(cuid())
|
|
20
|
+
email String @unique
|
|
21
|
+
}`),
|
|
22
|
+
).toBeTruthy();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should silently ignore invalid schema', () => {
|
|
26
|
+
const invalidModel = `
|
|
27
|
+
model User {
|
|
28
|
+
id String @id @default(cuid())
|
|
29
|
+
`;
|
|
30
|
+
const workDir = createProject(invalidModel);
|
|
31
|
+
expect(() => runCli('format', workDir)).not.toThrow();
|
|
32
|
+
});
|
|
33
|
+
});
|