Skip to content

Commit c5660c5

Browse files
authored
Merge pull request #185 from zenstackhq/dev
merge dev to main (v3.0.0-alpha.27)
2 parents 6262b76 + 2ad8857 commit c5660c5

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

59 files changed

+808
-438
lines changed

.vscode/launch.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
"request": "launch",
1111
"skipFiles": ["<node_internals>/**"],
1212
"type": "node",
13-
"args": ["generate", "--schema", "${workspaceFolder}/samples/blog/zenstack/schema.zmodel"]
13+
"args": ["generate"],
14+
"cwd": "${workspaceFolder}/samples/blog/zenstack"
1415
},
1516
{
1617
"name": "Debug with TSX",

TODO.md

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,14 @@
99
- [x] init
1010
- [x] validate
1111
- [ ] format
12-
- [ ] db seed
13-
- [ ] plugin mechanism
14-
- [ ] built-in plugins
15-
- [ ] ts
16-
- [ ] prisma
12+
- [x] plugin mechanism
13+
- [x] built-in plugins
14+
- [x] ts
15+
- [x] prisma
1716
- [ ] ZModel
18-
- [ ] Import
17+
- [x] Import
1918
- [ ] View support
19+
- [ ] Datasource provider-scoped attributes
2020
- [ ] ORM
2121
- [x] Create
2222
- [x] Input validation
@@ -85,11 +85,12 @@
8585
- [x] Custom field name
8686
- [ ] Strict undefined checks
8787
- [ ] DbNull vs JsonNull
88+
- [ ] Migrate to tsdown
8889
- [ ] Benchmark
8990
- [x] Plugin
9091
- [x] Post-mutation hooks should be called after transaction is committed
9192
- [x] TypeDef and mixin
92-
- [ ] Strongly typed JSON
93+
- [x] Strongly typed JSON
9394
- [x] Polymorphism
9495
- [x] ZModel
9596
- [x] Runtime
@@ -105,3 +106,4 @@
105106
- [x] SQLite
106107
- [x] PostgreSQL
107108
- [ ] Multi-schema
109+
- [ ] MySQL

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "zenstack-v3",
3-
"version": "3.0.0-alpha.26",
3+
"version": "3.0.0-alpha.27",
44
"description": "ZenStack",
55
"packageManager": "[email protected]",
66
"scripts": {
@@ -21,7 +21,7 @@
2121
"license": "MIT",
2222
"devDependencies": {
2323
"@eslint/js": "^9.29.0",
24-
"@types/node": "^20.17.24",
24+
"@types/node": "catalog:",
2525
"eslint": "~9.29.0",
2626
"glob": "^11.0.2",
2727
"prettier": "^3.5.3",

packages/cli/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"publisher": "zenstack",
44
"displayName": "ZenStack CLI",
55
"description": "FullStack database toolkit with built-in access control and automatic API generation.",
6-
"version": "3.0.0-alpha.26",
6+
"version": "3.0.0-alpha.27",
77
"type": "module",
88
"author": {
99
"name": "ZenStack Team"
Lines changed: 84 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
import { invariant } from '@zenstackhq/common-helpers';
2-
import { isPlugin, LiteralExpr, type Model } from '@zenstackhq/language/ast';
3-
import { PrismaSchemaGenerator, TsSchemaGenerator, type CliGenerator } from '@zenstackhq/sdk';
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';
45
import colors from 'colors';
5-
import fs from 'node:fs';
66
import path from 'node:path';
7+
import ora from 'ora';
8+
import { CliError } from '../cli-error';
9+
import * as corePlugins from '../plugins';
710
import { getPkgJsonConfig, getSchemaFile, loadSchemaDocument } from './action-utils';
811

912
type Options = {
1013
schema?: string;
1114
output?: string;
1215
silent?: boolean;
13-
savePrismaSchema?: string | boolean;
1416
};
1517

1618
/**
@@ -24,25 +26,10 @@ export async function run(options: Options) {
2426
const model = await loadSchemaDocument(schemaFile);
2527
const outputPath = getOutputPath(options, schemaFile);
2628

27-
// generate TS schema
28-
const tsSchemaFile = path.join(outputPath, 'schema.ts');
29-
await new TsSchemaGenerator().generate(schemaFile, [], outputPath);
30-
31-
await runPlugins(model, outputPath, tsSchemaFile);
32-
33-
// generate Prisma schema
34-
if (options.savePrismaSchema) {
35-
const prismaSchema = await new PrismaSchemaGenerator(model).generate();
36-
let prismaSchemaFile = path.join(outputPath, 'schema.prisma');
37-
if (typeof options.savePrismaSchema === 'string') {
38-
prismaSchemaFile = path.resolve(outputPath, options.savePrismaSchema);
39-
fs.mkdirSync(path.dirname(prismaSchemaFile), { recursive: true });
40-
}
41-
fs.writeFileSync(prismaSchemaFile, prismaSchema);
42-
}
29+
await runPlugins(schemaFile, model, outputPath);
4330

4431
if (!options.silent) {
45-
console.log(colors.green(`Generation completed successfully in ${Date.now() - start}ms.`));
32+
console.log(colors.green(`Generation completed successfully in ${Date.now() - start}ms.\n`));
4633
console.log(`You can now create a ZenStack client with it.
4734
4835
\`\`\`ts
@@ -68,18 +55,84 @@ function getOutputPath(options: Options, schemaFile: string) {
6855
}
6956
}
7057

71-
async function runPlugins(model: Model, outputPath: string, tsSchemaFile: string) {
58+
async function runPlugins(schemaFile: string, model: Model, outputPath: string) {
7259
const plugins = model.declarations.filter(isPlugin);
60+
const processedPlugins: { cliPlugin: CliPlugin; pluginOptions: Record<string, unknown> }[] = [];
61+
7362
for (const plugin of plugins) {
74-
const providerField = plugin.fields.find((f) => f.name === 'provider');
75-
invariant(providerField, `Plugin ${plugin.name} does not have a provider field`);
76-
const provider = (providerField.value as LiteralExpr).value as string;
77-
let useProvider = provider;
78-
if (useProvider.startsWith('@core/')) {
79-
useProvider = `@zenstackhq/runtime/plugins/${useProvider.slice(6)}`;
63+
const provider = getPluginProvider(plugin);
64+
65+
let cliPlugin: CliPlugin;
66+
if (provider.startsWith('@core/')) {
67+
cliPlugin = (corePlugins as any)[provider.slice('@core/'.length)];
68+
if (!cliPlugin) {
69+
throw new CliError(`Unknown core plugin: ${provider}`);
70+
}
71+
} else {
72+
let moduleSpec = provider;
73+
if (moduleSpec.startsWith('.')) {
74+
// relative to schema's path
75+
moduleSpec = path.resolve(path.dirname(schemaFile), moduleSpec);
76+
}
77+
try {
78+
cliPlugin = (await import(moduleSpec)).default as CliPlugin;
79+
} catch (error) {
80+
throw new CliError(`Failed to load plugin ${provider}: ${error}`);
81+
}
82+
}
83+
84+
processedPlugins.push({ cliPlugin, pluginOptions: getPluginOptions(plugin) });
85+
}
86+
87+
const defaultPlugins = [corePlugins['typescript']].reverse();
88+
defaultPlugins.forEach((d) => {
89+
if (!processedPlugins.some((p) => p.cliPlugin === d)) {
90+
processedPlugins.push({ cliPlugin: d, pluginOptions: {} });
91+
}
92+
});
93+
94+
for (const { cliPlugin, pluginOptions } of processedPlugins) {
95+
invariant(
96+
typeof cliPlugin.generate === 'function',
97+
`Plugin ${cliPlugin.name} does not have a generate function`,
98+
);
99+
100+
// run plugin generator
101+
const spinner = ora(cliPlugin.statusText ?? `Running plugin ${cliPlugin.name}`).start();
102+
try {
103+
await cliPlugin.generate({
104+
schemaFile,
105+
model,
106+
defaultOutputPath: outputPath,
107+
pluginOptions,
108+
});
109+
spinner.succeed();
110+
} catch (err) {
111+
spinner.fail();
112+
console.error(err);
113+
}
114+
}
115+
}
116+
117+
function getPluginProvider(plugin: Plugin) {
118+
const providerField = plugin.fields.find((f) => f.name === 'provider');
119+
invariant(providerField, `Plugin ${plugin.name} does not have a provider field`);
120+
const provider = (providerField.value as LiteralExpr).value as string;
121+
return provider;
122+
}
123+
124+
function getPluginOptions(plugin: Plugin): Record<string, unknown> {
125+
const result: Record<string, unknown> = {};
126+
for (const field of plugin.fields) {
127+
if (field.name === 'provider') {
128+
continue; // skip provider
129+
}
130+
const value = getLiteral(field.value) ?? getLiteralArray(field.value);
131+
if (value === undefined) {
132+
console.warn(`Plugin "${plugin.name}" option "${field.name}" has unsupported value, skipping`);
133+
continue;
80134
}
81-
const generator = (await import(useProvider)).default as CliGenerator;
82-
console.log('Running generator:', provider);
83-
await generator({ model, outputPath, tsSchemaFile });
135+
result[field.name] = value;
84136
}
137+
return result;
85138
}

packages/cli/src/index.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,6 @@ export function createProgram() {
5555
.description('Run code generation.')
5656
.addOption(schemaOption)
5757
.addOption(new Option('--silent', 'do not print any output'))
58-
.addOption(
59-
new Option(
60-
'--save-prisma-schema [path]',
61-
'save a Prisma schema file, by default into the output directory',
62-
),
63-
)
6458
.addOption(new Option('-o, --output <path>', 'default output directory for core plugins'))
6559
.action(generateAction);
6660

packages/cli/src/plugins/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { default as prisma } from './prisma';
2+
export { default as typescript } from './typescript';

packages/cli/src/plugins/prisma.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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, defaultOutputPath, pluginOptions }) {
9+
let outDir = defaultOutputPath;
10+
if (typeof pluginOptions['output'] === 'string') {
11+
outDir = path.resolve(defaultOutputPath, pluginOptions['output']);
12+
if (!fs.existsSync(outDir)) {
13+
fs.mkdirSync(outDir, { recursive: true });
14+
}
15+
}
16+
const prismaSchema = await new PrismaSchemaGenerator(model).generate();
17+
fs.writeFileSync(path.join(outDir, 'schema.prisma'), prismaSchema);
18+
},
19+
};
20+
21+
export default plugin;
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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;

packages/cli/test/generate.test.ts

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -30,18 +30,6 @@ describe('CLI generate command test', () => {
3030
expect(fs.existsSync(path.join(workDir, 'zenstack/schema.ts'))).toBe(true);
3131
});
3232

33-
it('should respect save prisma schema option', () => {
34-
const workDir = createProject(model);
35-
runCli('generate --save-prisma-schema', workDir);
36-
expect(fs.existsSync(path.join(workDir, 'zenstack/schema.prisma'))).toBe(true);
37-
});
38-
39-
it('should respect save prisma schema custom path option', () => {
40-
const workDir = createProject(model);
41-
runCli('generate --save-prisma-schema "../prisma/schema.prisma"', workDir);
42-
expect(fs.existsSync(path.join(workDir, 'prisma/schema.prisma'))).toBe(true);
43-
});
44-
4533
it('should respect package.json config', () => {
4634
const workDir = createProject(model);
4735
fs.mkdirSync(path.join(workDir, 'foo'));

0 commit comments

Comments
 (0)