@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.
@@ -1,72 +1,135 @@
1
- import { isPlugin, LiteralExpr, type Model } from '@zenstackhq/language/ast';
2
- import type { CliGenerator } from '@zenstackhq/runtime/client';
3
- import { PrismaSchemaGenerator, TsSchemaGenerator } from '@zenstackhq/sdk';
1
+ import { invariant } from '@zenstackhq/common-helpers';
2
+ import { isPlugin, LiteralExpr, Plugin, type Model } from '@zenstackhq/language/ast';
3
+ import { getLiteral, getLiteralArray } from '@zenstackhq/language/utils';
4
+ import { type CliPlugin } from '@zenstackhq/sdk';
4
5
  import colors from 'colors';
5
- import fs from 'node:fs';
6
6
  import path from 'node:path';
7
- import invariant from 'tiny-invariant';
8
- import { getSchemaFile, loadSchemaDocument } from './action-utils';
7
+ import ora from 'ora';
8
+ import { CliError } from '../cli-error';
9
+ import * as corePlugins from '../plugins';
10
+ import { getPkgJsonConfig, getSchemaFile, loadSchemaDocument } from './action-utils';
9
11
 
10
12
  type Options = {
11
13
  schema?: string;
12
14
  output?: string;
13
- silent?: boolean;
14
15
  };
15
16
 
16
17
  /**
17
18
  * CLI action for generating code from schema
18
19
  */
19
20
  export async function run(options: Options) {
21
+ const start = Date.now();
22
+
20
23
  const schemaFile = getSchemaFile(options.schema);
21
24
 
22
25
  const model = await loadSchemaDocument(schemaFile);
23
- const outputPath = options.output ?? path.dirname(schemaFile);
24
-
25
- // generate TS schema
26
- const tsSchemaFile = path.join(outputPath, 'schema.ts');
27
- await new TsSchemaGenerator().generate(schemaFile, [], tsSchemaFile);
28
-
29
- await runPlugins(model, outputPath, tsSchemaFile);
26
+ const outputPath = getOutputPath(options, schemaFile);
30
27
 
31
- // generate Prisma schema
32
- const prismaSchema = await new PrismaSchemaGenerator(model).generate();
33
- fs.writeFileSync(path.join(outputPath, 'schema.prisma'), prismaSchema);
28
+ await runPlugins(schemaFile, model, outputPath);
34
29
 
35
- if (!options.silent) {
36
- console.log(colors.green('Generation completed successfully.'));
37
- console.log(`You can now create a ZenStack client with it.
30
+ console.log(colors.green(`Generation completed successfully in ${Date.now() - start}ms.\n`));
31
+ console.log(`You can now create a ZenStack client with it.
38
32
 
39
33
  \`\`\`ts
40
34
  import { ZenStackClient } from '@zenstackhq/runtime';
41
35
  import { schema } from '${outputPath}/schema';
42
36
 
43
37
  const client = new ZenStackClient(schema, {
44
- dialectConfig: { ... }
38
+ dialect: { ... }
45
39
  });
46
- \`\`\`
47
- `);
40
+ \`\`\``);
41
+ }
42
+
43
+ function getOutputPath(options: Options, schemaFile: string) {
44
+ if (options.output) {
45
+ return options.output;
46
+ }
47
+ const pkgJsonConfig = getPkgJsonConfig(process.cwd());
48
+ if (pkgJsonConfig.output) {
49
+ return pkgJsonConfig.output;
50
+ } else {
51
+ return path.dirname(schemaFile);
48
52
  }
49
53
  }
50
54
 
51
- async function runPlugins(
52
- model: Model,
53
- outputPath: string,
54
- tsSchemaFile: string
55
- ) {
55
+ async function runPlugins(schemaFile: string, model: Model, outputPath: string) {
56
56
  const plugins = model.declarations.filter(isPlugin);
57
+ const processedPlugins: { cliPlugin: CliPlugin; pluginOptions: Record<string, unknown> }[] = [];
58
+
57
59
  for (const plugin of plugins) {
58
- const providerField = plugin.fields.find((f) => f.name === 'provider');
60
+ const provider = getPluginProvider(plugin);
61
+
62
+ let cliPlugin: CliPlugin;
63
+ if (provider.startsWith('@core/')) {
64
+ cliPlugin = (corePlugins as any)[provider.slice('@core/'.length)];
65
+ if (!cliPlugin) {
66
+ throw new CliError(`Unknown core plugin: ${provider}`);
67
+ }
68
+ } else {
69
+ let moduleSpec = provider;
70
+ if (moduleSpec.startsWith('.')) {
71
+ // relative to schema's path
72
+ moduleSpec = path.resolve(path.dirname(schemaFile), moduleSpec);
73
+ }
74
+ try {
75
+ cliPlugin = (await import(moduleSpec)).default as CliPlugin;
76
+ } catch (error) {
77
+ throw new CliError(`Failed to load plugin ${provider}: ${error}`);
78
+ }
79
+ }
80
+
81
+ processedPlugins.push({ cliPlugin, pluginOptions: getPluginOptions(plugin) });
82
+ }
83
+
84
+ const defaultPlugins = [corePlugins['typescript']].reverse();
85
+ defaultPlugins.forEach((d) => {
86
+ if (!processedPlugins.some((p) => p.cliPlugin === d)) {
87
+ processedPlugins.push({ cliPlugin: d, pluginOptions: {} });
88
+ }
89
+ });
90
+
91
+ for (const { cliPlugin, pluginOptions } of processedPlugins) {
59
92
  invariant(
60
- providerField,
61
- `Plugin ${plugin.name} does not have a provider field`
93
+ typeof cliPlugin.generate === 'function',
94
+ `Plugin ${cliPlugin.name} does not have a generate function`,
62
95
  );
63
- const provider = (providerField.value as LiteralExpr).value as string;
64
- let useProvider = provider;
65
- if (useProvider.startsWith('@core/')) {
66
- useProvider = `@zenstackhq/runtime/plugins/${useProvider.slice(6)}`;
96
+
97
+ // run plugin generator
98
+ const spinner = ora(cliPlugin.statusText ?? `Running plugin ${cliPlugin.name}`).start();
99
+ try {
100
+ await cliPlugin.generate({
101
+ schemaFile,
102
+ model,
103
+ defaultOutputPath: outputPath,
104
+ pluginOptions,
105
+ });
106
+ spinner.succeed();
107
+ } catch (err) {
108
+ spinner.fail();
109
+ console.error(err);
110
+ }
111
+ }
112
+ }
113
+
114
+ function getPluginProvider(plugin: Plugin) {
115
+ const providerField = plugin.fields.find((f) => f.name === 'provider');
116
+ invariant(providerField, `Plugin ${plugin.name} does not have a provider field`);
117
+ const provider = (providerField.value as LiteralExpr).value as string;
118
+ return provider;
119
+ }
120
+
121
+ function getPluginOptions(plugin: Plugin): Record<string, unknown> {
122
+ const result: Record<string, unknown> = {};
123
+ for (const field of plugin.fields) {
124
+ if (field.name === 'provider') {
125
+ continue; // skip provider
126
+ }
127
+ const value = getLiteral(field.value) ?? getLiteralArray(field.value);
128
+ if (value === undefined) {
129
+ console.warn(`Plugin "${plugin.name}" option "${field.name}" has unsupported value, skipping`);
130
+ continue;
67
131
  }
68
- const generator = (await import(useProvider)).default as CliGenerator;
69
- console.log('Running generator:', provider);
70
- await generator({ model, outputPath, tsSchemaFile });
132
+ result[field.name] = value;
71
133
  }
134
+ return result;
72
135
  }
@@ -3,5 +3,6 @@ import { run as generate } from './generate';
3
3
  import { run as info } from './info';
4
4
  import { run as init } from './init';
5
5
  import { run as migrate } from './migrate';
6
+ import { run as check } from './check';
6
7
 
7
- export { db, generate, info, init, migrate };
8
+ export { db, generate, info, init, migrate, check };
@@ -7,9 +7,7 @@ import path from 'node:path';
7
7
  export async function run(projectPath: string) {
8
8
  const packages = await getZenStackPackages(projectPath);
9
9
  if (!packages) {
10
- console.error(
11
- 'Unable to locate package.json. Are you in a valid project directory?'
12
- );
10
+ console.error('Unable to locate package.json. Are you in a valid project directory?');
13
11
  return;
14
12
  }
15
13
 
@@ -23,17 +21,11 @@ export async function run(projectPath: string) {
23
21
  }
24
22
 
25
23
  if (versions.size > 1) {
26
- console.warn(
27
- colors.yellow(
28
- 'WARNING: Multiple versions of Zenstack packages detected. This may cause issues.'
29
- )
30
- );
24
+ console.warn(colors.yellow('WARNING: Multiple versions of Zenstack packages detected. This may cause issues.'));
31
25
  }
32
26
  }
33
27
 
34
- async function getZenStackPackages(
35
- projectPath: string
36
- ): Promise<Array<{ pkg: string; version: string | undefined }>> {
28
+ async function getZenStackPackages(projectPath: string): Promise<Array<{ pkg: string; version: string | undefined }>> {
37
29
  let pkgJson: {
38
30
  dependencies: Record<string, unknown>;
39
31
  devDependencies: Record<string, unknown>;
@@ -45,17 +37,16 @@ async function getZenStackPackages(
45
37
  with: { type: 'json' },
46
38
  })
47
39
  ).default;
48
- } catch (err) {
40
+ } catch {
49
41
  return [];
50
42
  }
51
43
 
52
44
  const packages = Array.from(
53
45
  new Set(
54
- [
55
- ...Object.keys(pkgJson.dependencies ?? {}),
56
- ...Object.keys(pkgJson.devDependencies ?? {}),
57
- ].filter((p) => p.startsWith('@zenstackhq/') || p === 'zenstack')
58
- )
46
+ [...Object.keys(pkgJson.dependencies ?? {}), ...Object.keys(pkgJson.devDependencies ?? {})].filter(
47
+ (p) => p.startsWith('@zenstackhq/') || p === 'zenstack',
48
+ ),
49
+ ),
59
50
  ).sort();
60
51
 
61
52
  const result = await Promise.all(
@@ -70,7 +61,7 @@ async function getZenStackPackages(
70
61
  } catch {
71
62
  return { pkg, version: undefined };
72
63
  }
73
- })
64
+ }),
74
65
  );
75
66
 
76
67
  return result;
@@ -28,9 +28,7 @@ export async function run(projectPath: string) {
28
28
  ...(pkg.dev ? [pm.agent === 'yarn' ? '--dev' : '--save-dev'] : []),
29
29
  ]);
30
30
  if (!resolved) {
31
- throw new CliError(
32
- `Unable to determine how to install package "${pkg.name}". Please install it manually.`
33
- );
31
+ throw new CliError(`Unable to determine how to install package "${pkg.name}". Please install it manually.`);
34
32
  }
35
33
 
36
34
  const spinner = ora(`Installing "${pkg.name}"`).start();
@@ -51,32 +49,13 @@ export async function run(projectPath: string) {
51
49
  fs.mkdirSync(path.join(projectPath, generationFolder));
52
50
  }
53
51
 
54
- if (
55
- !fs.existsSync(
56
- path.join(projectPath, generationFolder, 'schema.zmodel')
57
- )
58
- ) {
59
- fs.writeFileSync(
60
- path.join(projectPath, generationFolder, 'schema.zmodel'),
61
- STARTER_ZMODEL
62
- );
52
+ if (!fs.existsSync(path.join(projectPath, generationFolder, 'schema.zmodel'))) {
53
+ fs.writeFileSync(path.join(projectPath, generationFolder, 'schema.zmodel'), STARTER_ZMODEL);
63
54
  } else {
64
- console.log(
65
- colors.yellow(
66
- 'Schema file already exists. Skipping generation of sample.'
67
- )
68
- );
55
+ console.log(colors.yellow('Schema file already exists. Skipping generation of sample.'));
69
56
  }
70
57
 
71
58
  console.log(colors.green('ZenStack project initialized successfully!'));
72
- console.log(
73
- colors.gray(
74
- `See "${generationFolder}/schema.zmodel" for your database schema.`
75
- )
76
- );
77
- console.log(
78
- colors.gray(
79
- 'Run `zenstack generate` to compile the the schema into a TypeScript file.'
80
- )
81
- );
59
+ console.log(colors.gray(`See "${generationFolder}/schema.zmodel" for your database schema.`));
60
+ console.log(colors.gray('Run `zenstack generate` to compile the the schema into a TypeScript file.'));
82
61
  }
@@ -1,11 +1,30 @@
1
+ import fs from 'node:fs';
1
2
  import path from 'node:path';
3
+ import { CliError } from '../cli-error';
2
4
  import { execPackage } from '../utils/exec-utils';
3
- import { getSchemaFile } from './action-utils';
4
- import { run as runGenerate } from './generate';
5
+ import { generateTempPrismaSchema, getSchemaFile } from './action-utils';
5
6
 
6
7
  type CommonOptions = {
7
8
  schema?: string;
9
+ migrations?: string;
10
+ };
11
+
12
+ type DevOptions = CommonOptions & {
8
13
  name?: string;
14
+ createOnly?: boolean;
15
+ };
16
+
17
+ type ResetOptions = CommonOptions & {
18
+ force?: boolean;
19
+ };
20
+
21
+ type DeployOptions = CommonOptions;
22
+
23
+ type StatusOptions = CommonOptions;
24
+
25
+ type ResolveOptions = CommonOptions & {
26
+ applied?: string;
27
+ rolledBack?: string;
9
28
  };
10
29
 
11
30
  /**
@@ -13,97 +32,105 @@ type CommonOptions = {
13
32
  */
14
33
  export async function run(command: string, options: CommonOptions) {
15
34
  const schemaFile = getSchemaFile(options.schema);
35
+ const prismaSchemaDir = options.migrations ? path.dirname(options.migrations) : undefined;
36
+ const prismaSchemaFile = await generateTempPrismaSchema(schemaFile, prismaSchemaDir);
37
+
38
+ try {
39
+ switch (command) {
40
+ case 'dev':
41
+ await runDev(prismaSchemaFile, options as DevOptions);
42
+ break;
43
+
44
+ case 'reset':
45
+ await runReset(prismaSchemaFile, options as ResetOptions);
46
+ break;
16
47
 
17
- // run generate first
18
- await runGenerate({
19
- schema: schemaFile,
20
- silent: true,
21
- });
22
-
23
- const prismaSchemaFile = path.join(
24
- path.dirname(schemaFile),
25
- 'schema.prisma'
26
- );
27
-
28
- switch (command) {
29
- case 'dev':
30
- await runDev(prismaSchemaFile, options);
31
- break;
32
-
33
- case 'reset':
34
- await runReset(prismaSchemaFile, options as any);
35
- break;
36
-
37
- case 'deploy':
38
- await runDeploy(prismaSchemaFile, options);
39
- break;
40
-
41
- case 'status':
42
- await runStatus(prismaSchemaFile, options);
43
- break;
48
+ case 'deploy':
49
+ await runDeploy(prismaSchemaFile, options as DeployOptions);
50
+ break;
51
+
52
+ case 'status':
53
+ await runStatus(prismaSchemaFile, options as StatusOptions);
54
+ break;
55
+
56
+ case 'resolve':
57
+ await runResolve(prismaSchemaFile, options as ResolveOptions);
58
+ break;
59
+ }
60
+ } finally {
61
+ if (fs.existsSync(prismaSchemaFile)) {
62
+ fs.unlinkSync(prismaSchemaFile);
63
+ }
44
64
  }
45
65
  }
46
66
 
47
- async function runDev(prismaSchemaFile: string, _options: unknown) {
67
+ async function runDev(prismaSchemaFile: string, options: DevOptions) {
48
68
  try {
49
- await execPackage(
50
- `prisma migrate dev --schema "${prismaSchemaFile}" --skip-generate`,
51
- {
52
- stdio: 'inherit',
53
- }
54
- );
69
+ const cmd = [
70
+ 'prisma migrate dev',
71
+ ` --schema "${prismaSchemaFile}"`,
72
+ ' --skip-generate',
73
+ options.name ? ` --name ${options.name}` : '',
74
+ options.createOnly ? ' --create-only' : '',
75
+ ].join('');
76
+
77
+ await execPackage(cmd);
55
78
  } catch (err) {
56
79
  handleSubProcessError(err);
57
80
  }
58
81
  }
59
82
 
60
- async function runReset(prismaSchemaFile: string, options: { force: boolean }) {
83
+ async function runReset(prismaSchemaFile: string, options: ResetOptions) {
61
84
  try {
62
- await execPackage(
63
- `prisma migrate reset --schema "${prismaSchemaFile}"${
64
- options.force ? ' --force' : ''
65
- }`,
66
- {
67
- stdio: 'inherit',
68
- }
85
+ const cmd = ['prisma migrate reset', ` --schema "${prismaSchemaFile}"`, options.force ? ' --force' : ''].join(
86
+ '',
69
87
  );
88
+
89
+ await execPackage(cmd);
70
90
  } catch (err) {
71
91
  handleSubProcessError(err);
72
92
  }
73
93
  }
74
94
 
75
- async function runDeploy(prismaSchemaFile: string, _options: unknown) {
95
+ async function runDeploy(prismaSchemaFile: string, _options: DeployOptions) {
76
96
  try {
77
- await execPackage(
78
- `prisma migrate deploy --schema "${prismaSchemaFile}"`,
79
- {
80
- stdio: 'inherit',
81
- }
82
- );
97
+ const cmd = ['prisma migrate deploy', ` --schema "${prismaSchemaFile}"`].join('');
98
+
99
+ await execPackage(cmd);
83
100
  } catch (err) {
84
101
  handleSubProcessError(err);
85
102
  }
86
103
  }
87
104
 
88
- async function runStatus(prismaSchemaFile: string, _options: unknown) {
105
+ async function runStatus(prismaSchemaFile: string, _options: StatusOptions) {
89
106
  try {
90
- await execPackage(
91
- `prisma migrate status --schema "${prismaSchemaFile}"`,
92
- {
93
- stdio: 'inherit',
94
- }
95
- );
107
+ await execPackage(`prisma migrate status --schema "${prismaSchemaFile}"`);
108
+ } catch (err) {
109
+ handleSubProcessError(err);
110
+ }
111
+ }
112
+
113
+ async function runResolve(prismaSchemaFile: string, options: ResolveOptions) {
114
+ if (!options.applied && !options.rolledBack) {
115
+ throw new CliError('Either --applied or --rolled-back option must be provided');
116
+ }
117
+
118
+ try {
119
+ const cmd = [
120
+ 'prisma migrate resolve',
121
+ ` --schema "${prismaSchemaFile}"`,
122
+ options.applied ? ` --applied ${options.applied}` : '',
123
+ options.rolledBack ? ` --rolled-back ${options.rolledBack}` : '',
124
+ ].join('');
125
+
126
+ await execPackage(cmd);
96
127
  } catch (err) {
97
128
  handleSubProcessError(err);
98
129
  }
99
130
  }
100
131
 
101
132
  function handleSubProcessError(err: unknown) {
102
- if (
103
- err instanceof Error &&
104
- 'status' in err &&
105
- typeof err.status === 'number'
106
- ) {
133
+ if (err instanceof Error && 'status' in err && typeof err.status === 'number') {
107
134
  process.exit(err.status);
108
135
  } else {
109
136
  process.exit(1);
@@ -27,14 +27,15 @@ model Post {
27
27
  `;
28
28
 
29
29
  export const STARTER_MAIN_TS = `import { ZenStackClient } from '@zenstackhq/runtime';
30
- import { schema } from './zenstack/schema';
31
30
  import SQLite from 'better-sqlite3';
31
+ import { SqliteDialect } from 'kysely';
32
+ import { schema } from './zenstack/schema';
32
33
 
33
34
  async function main() {
34
35
  const client = new ZenStackClient(schema, {
35
- dialectConfig: {
36
+ dialect: new SqliteDialect({
36
37
  database: new SQLite('./zenstack/dev.db'),
37
- },
38
+ }),
38
39
  });
39
40
  const user = await client.user.create({
40
41
  data: {