Skip to content

Commit 1d4011b

Browse files
authored
feat(cli): db seed command (#428)
* feat(cli): db seed command * add tests * improve help text
1 parent 127393e commit 1d4011b

File tree

8 files changed

+138
-8
lines changed

8 files changed

+138
-8
lines changed

packages/cli/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"@zenstackhq/sdk": "workspace:*",
3535
"colors": "1.4.0",
3636
"commander": "^8.3.0",
37+
"execa": "^9.6.0",
3738
"langium": "catalog:",
3839
"mixpanel": "^0.18.1",
3940
"ora": "^5.4.1",

packages/cli/src/actions/action-utils.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,11 @@ export async function generateTempPrismaSchema(zmodelPath: string, folder?: stri
7878
}
7979

8080
export function getPkgJsonConfig(startPath: string) {
81-
const result: { schema: string | undefined; output: string | undefined } = { schema: undefined, output: undefined };
81+
const result: { schema: string | undefined; output: string | undefined; seed: string | undefined } = {
82+
schema: undefined,
83+
output: undefined,
84+
seed: undefined,
85+
};
8286
const pkgJsonFile = findUp(['package.json'], startPath, false);
8387

8488
if (!pkgJsonFile) {
@@ -93,8 +97,15 @@ export function getPkgJsonConfig(startPath: string) {
9397
}
9498

9599
if (pkgJson.zenstack && typeof pkgJson.zenstack === 'object') {
96-
result.schema = pkgJson.zenstack.schema && path.resolve(path.dirname(pkgJsonFile), pkgJson.zenstack.schema);
97-
result.output = pkgJson.zenstack.output && path.resolve(path.dirname(pkgJsonFile), pkgJson.zenstack.output);
100+
result.schema =
101+
pkgJson.zenstack.schema &&
102+
typeof pkgJson.zenstack.schema === 'string' &&
103+
path.resolve(path.dirname(pkgJsonFile), pkgJson.zenstack.schema);
104+
result.output =
105+
pkgJson.zenstack.output &&
106+
typeof pkgJson.zenstack.output === 'string' &&
107+
path.resolve(path.dirname(pkgJsonFile), pkgJson.zenstack.output);
108+
result.seed = typeof pkgJson.zenstack.seed === 'string' && pkgJson.zenstack.seed;
98109
}
99110

100111
return result;

packages/cli/src/actions/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@ import { run as generate } from './generate';
55
import { run as info } from './info';
66
import { run as init } from './init';
77
import { run as migrate } from './migrate';
8+
import { run as seed } from './seed';
89

9-
export { check, db, format, generate, info, init, migrate };
10+
export { check, db, format, generate, info, init, migrate, seed };

packages/cli/src/actions/migrate.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ import path from 'node:path';
33
import { CliError } from '../cli-error';
44
import { execPrisma } from '../utils/exec-utils';
55
import { generateTempPrismaSchema, getSchemaFile } from './action-utils';
6+
import { run as runSeed } from './seed';
67

78
type CommonOptions = {
89
schema?: string;
910
migrations?: string;
11+
skipSeed?: boolean;
1012
};
1113

1214
type DevOptions = CommonOptions & {
@@ -70,6 +72,7 @@ function runDev(prismaSchemaFile: string, options: DevOptions) {
7072
'migrate dev',
7173
` --schema "${prismaSchemaFile}"`,
7274
' --skip-generate',
75+
' --skip-seed',
7376
options.name ? ` --name "${options.name}"` : '',
7477
options.createOnly ? ' --create-only' : '',
7578
].join('');
@@ -79,18 +82,23 @@ function runDev(prismaSchemaFile: string, options: DevOptions) {
7982
}
8083
}
8184

82-
function runReset(prismaSchemaFile: string, options: ResetOptions) {
85+
async function runReset(prismaSchemaFile: string, options: ResetOptions) {
8386
try {
8487
const cmd = [
8588
'migrate reset',
8689
` --schema "${prismaSchemaFile}"`,
8790
' --skip-generate',
91+
' --skip-seed',
8892
options.force ? ' --force' : '',
8993
].join('');
9094
execPrisma(cmd);
9195
} catch (err) {
9296
handleSubProcessError(err);
9397
}
98+
99+
if (!options.skipSeed) {
100+
await runSeed({ noWarnings: true, printStatus: true }, []);
101+
}
94102
}
95103

96104
function runDeploy(prismaSchemaFile: string, _options: DeployOptions) {

packages/cli/src/actions/seed.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
}

packages/cli/src/index.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ const formatAction = async (options: Parameters<typeof actions.format>[0]): Prom
3434
await telemetry.trackCommand('format', () => actions.format(options));
3535
};
3636

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+
3741
function createProgram() {
3842
const program = new Command('zen')
3943
.alias('zenstack')
@@ -87,6 +91,7 @@ function createProgram() {
8791
.addOption(schemaOption)
8892
.addOption(new Option('--force', 'skip the confirmation prompt'))
8993
.addOption(migrationsOption)
94+
.addOption(new Option('--skip-seed', 'skip seeding the database after reset'))
9095
.addOption(noVersionCheckOption)
9196
.description('Reset your database and apply all migrations, all data will be lost')
9297
.action((options) => migrateAction('reset', options));
@@ -128,6 +133,26 @@ function createProgram() {
128133
.addOption(new Option('--force-reset', 'force a reset of the database before push'))
129134
.action((options) => dbAction('push', options));
130135

136+
dbCommand
137+
.command('seed')
138+
.description('Seed the database')
139+
.allowExcessArguments(true)
140+
.addHelpText(
141+
'after',
142+
`
143+
Seed script is configured under the "zenstack.seed" field in package.json.
144+
E.g.:
145+
{
146+
"zenstack": {
147+
"seed": "ts-node ./zenstack/seed.ts"
148+
}
149+
}
150+
151+
Arguments following -- are passed to the seed script. E.g.: "zen db seed -- --users 10"`,
152+
)
153+
.addOption(noVersionCheckOption)
154+
.action((options, command) => seedAction(options, command.args));
155+
131156
program
132157
.command('info')
133158
.description('Get information of installed ZenStack packages')

packages/cli/test/db.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,47 @@ describe('CLI db commands test', () => {
1515
runCli('db push', workDir);
1616
expect(fs.existsSync(path.join(workDir, 'zenstack/dev.db'))).toBe(true);
1717
});
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+
});
1861
});

pnpm-lock.yaml

Lines changed: 6 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)