@zenstackhq/cli 3.0.0-alpha.3 → 3.0.0-alpha.30

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/src/index.ts CHANGED
@@ -1,12 +1,11 @@
1
1
  import { ZModelLanguageMetaData } from '@zenstackhq/language';
2
2
  import colors from 'colors';
3
- import { Command, Option } from 'commander';
3
+ import { Command, CommanderError, Option } from 'commander';
4
4
  import * as actions from './actions';
5
+ import { CliError } from './cli-error';
5
6
  import { getVersion } from './utils/version-utils';
6
7
 
7
- const generateAction = async (
8
- options: Parameters<typeof actions.generate>[0]
9
- ): Promise<void> => {
8
+ const generateAction = async (options: Parameters<typeof actions.generate>[0]): Promise<void> => {
10
9
  await actions.generate(options);
11
10
  };
12
11
 
@@ -26,6 +25,10 @@ const initAction = async (projectPath: string): Promise<void> => {
26
25
  await actions.init(projectPath);
27
26
  };
28
27
 
28
+ const checkAction = async (options: Parameters<typeof actions.check>[0]): Promise<void> => {
29
+ await actions.check(options);
30
+ };
31
+
29
32
  export function createProgram() {
30
33
  const program = new Command('zenstack');
31
34
 
@@ -36,92 +39,80 @@ export function createProgram() {
36
39
  program
37
40
  .description(
38
41
  `${colors.bold.blue(
39
- 'ζ'
40
- )} ZenStack is a Prisma power pack for building full-stack apps.\n\nDocumentation: https://zenstack.dev.`
42
+ 'ζ',
43
+ )} ZenStack is the data layer for modern TypeScript apps.\n\nDocumentation: https://zenstack.dev.`,
41
44
  )
42
45
  .showHelpAfterError()
43
46
  .showSuggestionAfterError();
44
47
 
45
48
  const schemaOption = new Option(
46
49
  '--schema <file>',
47
- `schema file (with extension ${schemaExtensions}). Defaults to "schema.zmodel" unless specified in package.json.`
50
+ `schema file (with extension ${schemaExtensions}). Defaults to "zenstack/schema.zmodel" unless specified in package.json.`,
48
51
  );
49
52
 
50
53
  program
51
54
  .command('generate')
52
- .description('Run code generation.')
55
+ .description('Run code generation plugins.')
53
56
  .addOption(schemaOption)
54
- .addOption(
55
- new Option(
56
- '-o, --output <path>',
57
- 'default output directory for core plugins'
58
- )
59
- )
57
+ .addOption(new Option('-o, --output <path>', 'default output directory for code generation'))
60
58
  .action(generateAction);
61
59
 
62
- const migrateCommand = program
63
- .command('migrate')
64
- .description('Update the database schema with migrations.');
60
+ const migrateCommand = program.command('migrate').description('Run database schema migration related tasks.');
61
+ const migrationsOption = new Option('--migrations <path>', 'path that contains the "migrations" directory');
65
62
 
66
63
  migrateCommand
67
64
  .command('dev')
68
65
  .addOption(schemaOption)
69
66
  .addOption(new Option('-n, --name <name>', 'migration name'))
70
- .addOption(
71
- new Option('--create-only', 'only create migration, do not apply')
72
- )
73
- .description(
74
- 'Create a migration from changes in schema and apply it to the database.'
75
- )
67
+ .addOption(new Option('--create-only', 'only create migration, do not apply'))
68
+ .addOption(migrationsOption)
69
+ .description('Create a migration from changes in schema and apply it to the database.')
76
70
  .action((options) => migrateAction('dev', options));
77
71
 
78
72
  migrateCommand
79
73
  .command('reset')
80
74
  .addOption(schemaOption)
81
75
  .addOption(new Option('--force', 'skip the confirmation prompt'))
82
- .description(
83
- 'Reset your database and apply all migrations, all data will be lost.'
84
- )
76
+ .addOption(migrationsOption)
77
+ .description('Reset your database and apply all migrations, all data will be lost.')
85
78
  .action((options) => migrateAction('reset', options));
86
79
 
87
80
  migrateCommand
88
81
  .command('deploy')
89
82
  .addOption(schemaOption)
90
- .description(
91
- 'Deploy your pending migrations to your production/staging database.'
92
- )
83
+ .addOption(migrationsOption)
84
+ .description('Deploy your pending migrations to your production/staging database.')
93
85
  .action((options) => migrateAction('deploy', options));
94
86
 
95
87
  migrateCommand
96
88
  .command('status')
97
89
  .addOption(schemaOption)
98
- .description('check the status of your database migrations.')
90
+ .addOption(migrationsOption)
91
+ .description('Check the status of your database migrations.')
99
92
  .action((options) => migrateAction('status', options));
100
93
 
101
- const dbCommand = program
102
- .command('db')
103
- .description('Manage your database schema during development.');
94
+ migrateCommand
95
+ .command('resolve')
96
+ .addOption(schemaOption)
97
+ .addOption(migrationsOption)
98
+ .addOption(new Option('--applied <migration>', 'record a specific migration as applied'))
99
+ .addOption(new Option('--rolled-back <migration>', 'record a specific migration as rolled back'))
100
+ .description('Resolve issues with database migrations in deployment databases.')
101
+ .action((options) => migrateAction('resolve', options));
102
+
103
+ const dbCommand = program.command('db').description('Manage your database schema during development.');
104
104
 
105
105
  dbCommand
106
106
  .command('push')
107
- .description('Push the state from your schema to your database')
107
+ .description('Push the state from your schema to your database.')
108
108
  .addOption(schemaOption)
109
- .addOption(
110
- new Option('--accept-data-loss', 'ignore data loss warnings')
111
- )
112
- .addOption(
113
- new Option(
114
- '--force-reset',
115
- 'force a reset of the database before push'
116
- )
117
- )
109
+ .addOption(new Option('--accept-data-loss', 'ignore data loss warnings'))
110
+ .addOption(new Option('--force-reset', 'force a reset of the database before push'))
118
111
  .action((options) => dbAction('push', options));
119
112
 
120
113
  program
121
114
  .command('info')
122
- .description(
123
- 'Get information of installed ZenStack and related packages.'
124
- )
115
+ .description('Get information of installed ZenStack packages.')
125
116
  .argument('[path]', 'project path', '.')
126
117
  .action(infoAction);
127
118
 
@@ -131,8 +122,27 @@ export function createProgram() {
131
122
  .argument('[path]', 'project path', '.')
132
123
  .action(initAction);
133
124
 
125
+ program
126
+ .command('check')
127
+ .description('Check a ZModel schema for syntax or semantic errors.')
128
+ .addOption(schemaOption)
129
+ .action(checkAction);
130
+
134
131
  return program;
135
132
  }
136
133
 
137
134
  const program = createProgram();
138
- program.parse(process.argv);
135
+
136
+ program.parseAsync().catch((err) => {
137
+ if (err instanceof CliError) {
138
+ console.error(colors.red(err.message));
139
+ process.exit(1);
140
+ } else if (err instanceof CommanderError) {
141
+ // errors are already reported, just exit
142
+ process.exit(err.exitCode);
143
+ } else {
144
+ console.error(colors.red('An unexpected error occurred:'));
145
+ console.error(err);
146
+ process.exit(1);
147
+ }
148
+ });
@@ -0,0 +1,2 @@
1
+ export { default as prisma } from './prisma';
2
+ export { default as typescript } from './typescript';
@@ -0,0 +1,21 @@
1
+ import { PrismaSchemaGenerator, type CliPlugin } from '@zenstackhq/sdk';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+
5
+ const plugin: CliPlugin = {
6
+ name: 'Prisma Schema Generator',
7
+ statusText: 'Generating Prisma schema',
8
+ async generate({ model, schemaFile, defaultOutputPath, pluginOptions }) {
9
+ let outFile = path.join(defaultOutputPath, 'schema.prisma');
10
+ if (typeof pluginOptions['output'] === 'string') {
11
+ outFile = path.resolve(path.dirname(schemaFile), pluginOptions['output']);
12
+ if (!fs.existsSync(path.dirname(outFile))) {
13
+ fs.mkdirSync(path.dirname(outFile), { recursive: true });
14
+ }
15
+ }
16
+ const prismaSchema = await new PrismaSchemaGenerator(model).generate();
17
+ fs.writeFileSync(outFile, prismaSchema);
18
+ },
19
+ };
20
+
21
+ export default plugin;
@@ -0,0 +1,21 @@
1
+ import type { CliPlugin } from '@zenstackhq/sdk';
2
+ import { TsSchemaGenerator } from '@zenstackhq/sdk';
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+
6
+ const plugin: CliPlugin = {
7
+ name: 'TypeScript Schema Generator',
8
+ statusText: 'Generating TypeScript schema',
9
+ async generate({ model, defaultOutputPath, pluginOptions }) {
10
+ let outDir = defaultOutputPath;
11
+ if (typeof pluginOptions['output'] === 'string') {
12
+ outDir = path.resolve(defaultOutputPath, pluginOptions['output']);
13
+ if (!fs.existsSync(outDir)) {
14
+ fs.mkdirSync(outDir, { recursive: true });
15
+ }
16
+ }
17
+ await new TsSchemaGenerator().generate(model, outDir);
18
+ },
19
+ };
20
+
21
+ export default plugin;
@@ -3,10 +3,7 @@ import { execSync as _exec, type ExecSyncOptions } from 'child_process';
3
3
  /**
4
4
  * Utility for executing command synchronously and prints outputs on current console
5
5
  */
6
- export function execSync(
7
- cmd: string,
8
- options?: Omit<ExecSyncOptions, 'env'> & { env?: Record<string, string> }
9
- ): void {
6
+ export function execSync(cmd: string, options?: Omit<ExecSyncOptions, 'env'> & { env?: Record<string, string> }): void {
10
7
  const { env, ...restOptions } = options ?? {};
11
8
  const mergedEnv = env ? { ...process.env, ...env } : undefined;
12
9
  _exec(cmd, {
@@ -22,7 +19,7 @@ export function execSync(
22
19
  */
23
20
  export function execPackage(
24
21
  cmd: string,
25
- options?: Omit<ExecSyncOptions, 'env'> & { env?: Record<string, string> }
22
+ options?: Omit<ExecSyncOptions, 'env'> & { env?: Record<string, string> },
26
23
  ): void {
27
24
  const packageManager = process?.versions?.['bun'] ? 'bunx' : 'npx';
28
25
  execSync(`${packageManager} ${cmd}`, options);
@@ -1,13 +1,13 @@
1
- /* eslint-disable @typescript-eslint/no-var-requires */
2
- export function getVersion(): string | undefined {
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ export function getVersion() {
3
6
  try {
4
- return require('../package.json').version;
7
+ // isomorphic __dirname
8
+ const _dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url));
9
+ return JSON.parse(fs.readFileSync(path.join(_dirname, '../package.json'), 'utf8')).version;
5
10
  } catch {
6
- try {
7
- // dev environment
8
- return require('../../package.json').version;
9
- } catch {
10
- return undefined;
11
- }
11
+ return undefined;
12
12
  }
13
13
  }
@@ -0,0 +1,101 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { describe, expect, it } from 'vitest';
4
+ import { createProject, runCli } from './utils';
5
+
6
+ const validModel = `
7
+ model User {
8
+ id String @id @default(cuid())
9
+ email String @unique
10
+ name String?
11
+ posts Post[]
12
+ }
13
+
14
+ model Post {
15
+ id String @id @default(cuid())
16
+ title String
17
+ content String?
18
+ author User @relation(fields: [authorId], references: [id])
19
+ authorId String
20
+ }
21
+ `;
22
+
23
+ const invalidModel = `
24
+ model User {
25
+ id String @id @default(cuid())
26
+ email String @unique
27
+ posts Post[]
28
+ }
29
+
30
+ model Post {
31
+ id String @id @default(cuid())
32
+ title String
33
+ author User @relation(fields: [authorId], references: [id])
34
+ // Missing authorId field - should cause validation error
35
+ }
36
+ `;
37
+
38
+ describe('CLI validate command test', () => {
39
+ it('should validate a valid schema successfully', () => {
40
+ const workDir = createProject(validModel);
41
+
42
+ // Should not throw an error
43
+ expect(() => runCli('check', workDir)).not.toThrow();
44
+ });
45
+
46
+ it('should fail validation for invalid schema', () => {
47
+ const workDir = createProject(invalidModel);
48
+
49
+ // Should throw an error due to validation failure
50
+ expect(() => runCli('check', workDir)).toThrow();
51
+ });
52
+
53
+ it('should respect custom schema location', () => {
54
+ const workDir = createProject(validModel);
55
+ fs.renameSync(path.join(workDir, 'zenstack/schema.zmodel'), path.join(workDir, 'zenstack/custom.zmodel'));
56
+
57
+ // Should not throw an error when using custom schema path
58
+ expect(() => runCli('check --schema ./zenstack/custom.zmodel', workDir)).not.toThrow();
59
+ });
60
+
61
+ it('should fail when schema file does not exist', () => {
62
+ const workDir = createProject(validModel);
63
+
64
+ // Should throw an error when schema file doesn't exist
65
+ expect(() => runCli('check --schema ./nonexistent.zmodel', workDir)).toThrow();
66
+ });
67
+
68
+ it('should respect package.json config', () => {
69
+ const workDir = createProject(validModel);
70
+ fs.mkdirSync(path.join(workDir, 'foo'));
71
+ fs.renameSync(path.join(workDir, 'zenstack/schema.zmodel'), path.join(workDir, 'foo/schema.zmodel'));
72
+ fs.rmdirSync(path.join(workDir, 'zenstack'));
73
+
74
+ const pkgJson = JSON.parse(fs.readFileSync(path.join(workDir, 'package.json'), 'utf8'));
75
+ pkgJson.zenstack = {
76
+ schema: './foo/schema.zmodel',
77
+ };
78
+ fs.writeFileSync(path.join(workDir, 'package.json'), JSON.stringify(pkgJson, null, 2));
79
+
80
+ // Should not throw an error when using package.json config
81
+ expect(() => runCli('check', workDir)).not.toThrow();
82
+ });
83
+
84
+ it('should validate schema with syntax errors', () => {
85
+ const modelWithSyntaxError = `
86
+ datasource db {
87
+ provider = "sqlite"
88
+ url = "file:./dev.db"
89
+ }
90
+
91
+ model User {
92
+ id String @id @default(cuid())
93
+ email String @unique
94
+ // Missing closing brace - syntax error
95
+ `;
96
+ const workDir = createProject(modelWithSyntaxError, false);
97
+
98
+ // Should throw an error due to syntax error
99
+ expect(() => runCli('check', workDir)).toThrow();
100
+ });
101
+ });
@@ -0,0 +1,18 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { describe, expect, it } from 'vitest';
4
+ import { createProject, runCli } from './utils';
5
+
6
+ const model = `
7
+ model User {
8
+ id String @id @default(cuid())
9
+ }
10
+ `;
11
+
12
+ describe('CLI db commands test', () => {
13
+ it('should generate a database with db push', () => {
14
+ const workDir = createProject(model);
15
+ runCli('db push', workDir);
16
+ expect(fs.existsSync(path.join(workDir, 'zenstack/dev.db'))).toBe(true);
17
+ });
18
+ });
@@ -0,0 +1,47 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { describe, expect, it } from 'vitest';
4
+ import { createProject, runCli } from './utils';
5
+
6
+ const model = `
7
+ model User {
8
+ id String @id @default(cuid())
9
+ }
10
+ `;
11
+
12
+ describe('CLI generate command test', () => {
13
+ it('should generate a TypeScript schema', () => {
14
+ const workDir = createProject(model);
15
+ runCli('generate', workDir);
16
+ expect(fs.existsSync(path.join(workDir, 'zenstack/schema.ts'))).toBe(true);
17
+ expect(fs.existsSync(path.join(workDir, 'zenstack/schema.prisma'))).toBe(false);
18
+ });
19
+
20
+ it('should respect custom output directory', () => {
21
+ const workDir = createProject(model);
22
+ runCli('generate --output ./zen', workDir);
23
+ expect(fs.existsSync(path.join(workDir, 'zen/schema.ts'))).toBe(true);
24
+ });
25
+
26
+ it('should respect custom schema location', () => {
27
+ const workDir = createProject(model);
28
+ fs.renameSync(path.join(workDir, 'zenstack/schema.zmodel'), path.join(workDir, 'zenstack/foo.zmodel'));
29
+ runCli('generate --schema ./zenstack/foo.zmodel', workDir);
30
+ expect(fs.existsSync(path.join(workDir, 'zenstack/schema.ts'))).toBe(true);
31
+ });
32
+
33
+ it('should respect package.json config', () => {
34
+ const workDir = createProject(model);
35
+ fs.mkdirSync(path.join(workDir, 'foo'));
36
+ fs.renameSync(path.join(workDir, 'zenstack/schema.zmodel'), path.join(workDir, 'foo/schema.zmodel'));
37
+ fs.rmdirSync(path.join(workDir, 'zenstack'));
38
+ const pkgJson = JSON.parse(fs.readFileSync(path.join(workDir, 'package.json'), 'utf8'));
39
+ pkgJson.zenstack = {
40
+ schema: './foo/schema.zmodel',
41
+ output: './bar',
42
+ };
43
+ fs.writeFileSync(path.join(workDir, 'package.json'), JSON.stringify(pkgJson, null, 2));
44
+ runCli('generate', workDir);
45
+ expect(fs.existsSync(path.join(workDir, 'bar/schema.ts'))).toBe(true);
46
+ });
47
+ });
@@ -0,0 +1,13 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import tmp from 'tmp';
4
+ import { describe, expect, it } from 'vitest';
5
+ import { runCli } from './utils';
6
+
7
+ describe('Cli init command tests', () => {
8
+ it('should create a new project', () => {
9
+ const { name: workDir } = tmp.dirSync({ unsafeCleanup: true });
10
+ runCli('init', workDir);
11
+ expect(fs.existsSync(path.join(workDir, 'zenstack/schema.zmodel'))).toBe(true);
12
+ });
13
+ });
@@ -0,0 +1,72 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { describe, expect, it } from 'vitest';
4
+ import { createProject, runCli } from './utils';
5
+
6
+ const model = `
7
+ model User {
8
+ id String @id @default(cuid())
9
+ }
10
+ `;
11
+
12
+ describe('CLI migrate commands test', () => {
13
+ it('should generate a database with migrate dev', () => {
14
+ const workDir = createProject(model);
15
+ runCli('migrate dev --name init', workDir);
16
+ expect(fs.existsSync(path.join(workDir, 'zenstack/dev.db'))).toBe(true);
17
+ expect(fs.existsSync(path.join(workDir, 'zenstack/migrations'))).toBe(true);
18
+ });
19
+
20
+ it('should reset the database with migrate reset', () => {
21
+ const workDir = createProject(model);
22
+ runCli('db push', workDir);
23
+ expect(fs.existsSync(path.join(workDir, 'zenstack/dev.db'))).toBe(true);
24
+ runCli('migrate reset --force', workDir);
25
+ expect(fs.existsSync(path.join(workDir, 'zenstack/dev.db'))).toBe(true);
26
+ });
27
+
28
+ it('should reset the database with migrate deploy', () => {
29
+ const workDir = createProject(model);
30
+ runCli('migrate dev --name init', workDir);
31
+ fs.rmSync(path.join(workDir, 'zenstack/dev.db'));
32
+ runCli('migrate deploy', workDir);
33
+ expect(fs.existsSync(path.join(workDir, 'zenstack/dev.db'))).toBe(true);
34
+ });
35
+
36
+ it('supports migrate status', () => {
37
+ const workDir = createProject(model);
38
+ runCli('migrate dev --name init', workDir);
39
+ runCli('migrate status', workDir);
40
+ });
41
+
42
+ it('supports migrate resolve', () => {
43
+ const workDir = createProject(model);
44
+ runCli('migrate dev --name init', workDir);
45
+
46
+ // find the migration record "timestamp_init"
47
+ const migrationRecords = fs.readdirSync(path.join(workDir, 'zenstack/migrations'));
48
+ const migration = migrationRecords.find((f) => f.endsWith('_init'));
49
+
50
+ // force a migration failure
51
+ fs.writeFileSync(path.join(workDir, 'zenstack/migrations', migration!, 'migration.sql'), 'invalid content');
52
+
53
+ // redeploy the migration, which will fail
54
+ fs.rmSync(path.join(workDir, 'zenstack/dev.db'), { force: true });
55
+ try {
56
+ runCli('migrate deploy', workDir);
57
+ } catch {
58
+ // noop
59
+ }
60
+
61
+ // --rolled-back
62
+ runCli(`migrate resolve --rolled-back ${migration}`, workDir);
63
+
64
+ // --applied
65
+ runCli(`migrate resolve --applied ${migration}`, workDir);
66
+ });
67
+
68
+ it('should throw error when neither applied nor rolled-back is provided', () => {
69
+ const workDir = createProject(model);
70
+ expect(() => runCli('migrate resolve', workDir)).toThrow();
71
+ });
72
+ });
@@ -0,0 +1,50 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { describe, expect, it } from 'vitest';
4
+ import { createProject, runCli } from '../utils';
5
+ import { execSync } from 'node:child_process';
6
+
7
+ describe('Custom plugins tests', () => {
8
+ it('runs custom plugin generator', () => {
9
+ const workDir = createProject(`
10
+ plugin custom {
11
+ provider = '../my-plugin.js'
12
+ output = '../custom-output'
13
+ }
14
+
15
+ model User {
16
+ id String @id @default(cuid())
17
+ }
18
+ `);
19
+
20
+ fs.writeFileSync(
21
+ path.join(workDir, 'my-plugin.ts'),
22
+ `
23
+ import type { CliPlugin } from '@zenstackhq/sdk';
24
+ import fs from 'node:fs';
25
+ import path from 'node:path';
26
+
27
+ const plugin: CliPlugin = {
28
+ name: 'Custom Generator',
29
+ statusText: 'Generating foo.txt',
30
+ async generate({ model, defaultOutputPath, pluginOptions }) {
31
+ let outDir = defaultOutputPath;
32
+ if (typeof pluginOptions['output'] === 'string') {
33
+ outDir = path.resolve(defaultOutputPath, pluginOptions['output']);
34
+ if (!fs.existsSync(outDir)) {
35
+ fs.mkdirSync(outDir, { recursive: true });
36
+ }
37
+ }
38
+ fs.writeFileSync(path.join(outDir, 'foo.txt'), 'from my plugin');
39
+ },
40
+ };
41
+
42
+ export default plugin;
43
+ `,
44
+ );
45
+
46
+ execSync('npx tsc', { cwd: workDir });
47
+ runCli('generate', workDir);
48
+ expect(fs.existsSync(path.join(workDir, 'custom-output/foo.txt'))).toBe(true);
49
+ });
50
+ });
@@ -0,0 +1,60 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { describe, expect, it } from 'vitest';
4
+ import { createProject, runCli } from '../utils';
5
+
6
+ describe('Core plugins tests', () => {
7
+ it('can automatically generate a TypeScript schema with default output', () => {
8
+ const workDir = createProject(`
9
+ model User {
10
+ id String @id @default(cuid())
11
+ }
12
+ `);
13
+ runCli('generate', workDir);
14
+ expect(fs.existsSync(path.join(workDir, 'zenstack/schema.ts'))).toBe(true);
15
+ });
16
+
17
+ it('can automatically generate a TypeScript schema with custom output', () => {
18
+ const workDir = createProject(`
19
+ plugin typescript {
20
+ provider = '@core/typescript'
21
+ output = '../generated-schema'
22
+ }
23
+
24
+ model User {
25
+ id String @id @default(cuid())
26
+ }
27
+ `);
28
+ runCli('generate', workDir);
29
+ expect(fs.existsSync(path.join(workDir, 'generated-schema/schema.ts'))).toBe(true);
30
+ });
31
+
32
+ it('can generate a Prisma schema with default output', () => {
33
+ const workDir = createProject(`
34
+ plugin prisma {
35
+ provider = '@core/prisma'
36
+ }
37
+
38
+ model User {
39
+ id String @id @default(cuid())
40
+ }
41
+ `);
42
+ runCli('generate', workDir);
43
+ expect(fs.existsSync(path.join(workDir, 'zenstack/schema.prisma'))).toBe(true);
44
+ });
45
+
46
+ it('can generate a Prisma schema with custom output', () => {
47
+ const workDir = createProject(`
48
+ plugin prisma {
49
+ provider = '@core/prisma'
50
+ output = '../prisma/schema.prisma'
51
+ }
52
+
53
+ model User {
54
+ id String @id @default(cuid())
55
+ }
56
+ `);
57
+ runCli('generate', workDir);
58
+ expect(fs.existsSync(path.join(workDir, 'prisma/schema.prisma'))).toBe(true);
59
+ });
60
+ });