@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/.turbo/turbo-build.log +22 -23
- package/bin/cli +1 -1
- package/dist/index.cjs +385 -193
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +387 -199
- package/dist/index.js.map +1 -1
- package/eslint.config.js +4 -0
- package/package.json +16 -17
- package/src/actions/action-utils.ts +81 -10
- package/src/actions/check.ts +22 -0
- package/src/actions/db.ts +30 -29
- package/src/actions/generate.ts +101 -38
- package/src/actions/index.ts +2 -1
- package/src/actions/info.ts +9 -18
- package/src/actions/init.ts +6 -27
- package/src/actions/migrate.ts +90 -63
- package/src/actions/templates.ts +4 -3
- package/src/index.ts +57 -47
- package/src/plugins/index.ts +2 -0
- package/src/plugins/prisma.ts +21 -0
- package/src/plugins/typescript.ts +21 -0
- package/src/utils/exec-utils.ts +2 -5
- package/src/utils/version-utils.ts +9 -9
- package/test/check.test.ts +101 -0
- package/test/db.test.ts +18 -0
- package/test/generate.test.ts +47 -0
- package/test/init.test.ts +13 -0
- package/test/migrate.test.ts +72 -0
- package/test/plugins/custom-plugin.test.ts +50 -0
- package/test/plugins/prisma-plugin.test.ts +60 -0
- package/test/ts-schema-gen.test.ts +180 -1
- package/test/utils.ts +23 -0
- package/tsconfig.json +1 -4
- package/vitest.config.ts +1 -1
- package/.turbo/turbo-lint.log +0 -18
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
.
|
|
83
|
-
|
|
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
|
-
.
|
|
91
|
-
|
|
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
|
-
.
|
|
90
|
+
.addOption(migrationsOption)
|
|
91
|
+
.description('Check the status of your database migrations.')
|
|
99
92
|
.action((options) => migrateAction('status', options));
|
|
100
93
|
|
|
101
|
-
|
|
102
|
-
.command('
|
|
103
|
-
.
|
|
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
|
-
|
|
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
|
-
|
|
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,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;
|
package/src/utils/exec-utils.ts
CHANGED
|
@@ -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
|
-
|
|
2
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
});
|
package/test/db.test.ts
ADDED
|
@@ -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
|
+
});
|