Skip to content

Commit 4668ee9

Browse files
authored
feat: add CLI config file support (#328)
1 parent 15ab420 commit 4668ee9

File tree

11 files changed

+242
-19
lines changed

11 files changed

+242
-19
lines changed

packages/schema/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@
9292
"colors": "1.4.0",
9393
"commander": "^8.3.0",
9494
"cuid": "^2.1.8",
95+
"get-latest-version": "^5.0.1",
9596
"langium": "1.1.0",
9697
"mixpanel": "^0.17.0",
9798
"node-machine-id": "^1.1.12",
@@ -108,7 +109,7 @@
108109
"vscode-languageserver-textdocument": "^1.0.7",
109110
"vscode-uri": "^3.0.6",
110111
"zod": "^3.19.1",
111-
"get-latest-version": "^5.0.1"
112+
"zod-validation-error": "^0.2.1"
112113
},
113114
"devDependencies": {
114115
"@types/async-exit-hook": "^2.0.0",

packages/schema/src/cli/config.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { GUARD_FIELD_NAME, TRANSACTION_FIELD_NAME } from '@zenstackhq/sdk';
2+
import fs from 'fs';
3+
import z from 'zod';
4+
import { fromZodError } from 'zod-validation-error';
5+
import { CliError } from './cli-error';
6+
7+
const schema = z
8+
.object({
9+
guardFieldName: z.string().default(GUARD_FIELD_NAME),
10+
transactionFieldName: z.string().default(TRANSACTION_FIELD_NAME),
11+
})
12+
.strict();
13+
14+
export type ConfigType = z.infer<typeof schema>;
15+
16+
export let config: ConfigType = schema.parse({});
17+
18+
/**
19+
* Loads and validates CLI configuration file.
20+
* @returns
21+
*/
22+
export function loadConfig(filename: string) {
23+
if (!fs.existsSync(filename)) {
24+
return;
25+
}
26+
27+
let content: unknown;
28+
try {
29+
content = JSON.parse(fs.readFileSync(filename, 'utf-8'));
30+
} catch {
31+
throw new CliError(`Config is not a valid JSON file: ${filename}`);
32+
}
33+
34+
const parsed = schema.safeParse(content);
35+
if (!parsed.success) {
36+
throw new CliError(`Config file ${filename} is not valid: ${fromZodError(parsed.error)}`);
37+
}
38+
39+
config = parsed.data;
40+
}

packages/schema/src/cli/index.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,20 @@
22
import { ZModelLanguageMetaData } from '@zenstackhq/language/module';
33
import colors from 'colors';
44
import { Command, Option } from 'commander';
5+
import fs from 'fs';
56
import * as semver from 'semver';
67
import telemetry from '../telemetry';
78
import { PackageManagers } from '../utils/pkg-utils';
89
import { getVersion } from '../utils/version-utils';
910
import { CliError } from './cli-error';
1011
import { dumpInfo, initProject, runPlugins } from './cli-util';
12+
import { loadConfig } from './config';
1113

1214
// required minimal version of Prisma
1315
export const requiredPrismaVersion = '4.0.0';
1416

17+
const DEFAULT_CONFIG_FILE = 'zenstack.config.json';
18+
1519
export const initAction = async (
1620
projectPath: string,
1721
options: {
@@ -97,6 +101,8 @@ export function createProgram() {
97101
'./schema.zmodel'
98102
);
99103

104+
const configOption = new Option('-c, --config [file]', 'config file');
105+
100106
const pmOption = new Option('-p, --package-manager <pm>', 'package manager to use').choices([
101107
'npm',
102108
'yarn',
@@ -114,6 +120,7 @@ export function createProgram() {
114120
program
115121
.command('init')
116122
.description('Initialize an existing project for ZenStack.')
123+
.addOption(configOption)
117124
.addOption(pmOption)
118125
.addOption(new Option('--prisma <file>', 'location of Prisma schema file to bootstrap from'))
119126
.addOption(new Option('--tag [tag]', 'the NPM package tag to use when installing dependencies'))
@@ -124,9 +131,27 @@ export function createProgram() {
124131
.command('generate')
125132
.description('Run code generation.')
126133
.addOption(schemaOption)
134+
.addOption(configOption)
127135
.addOption(pmOption)
128136
.addOption(noDependencyCheck)
129137
.action(generateAction);
138+
139+
// make sure config is loaded before actions run
140+
program.hook('preAction', async (_, actionCommand) => {
141+
let configFile: string | undefined = actionCommand.opts().config;
142+
if (!configFile && fs.existsSync(DEFAULT_CONFIG_FILE)) {
143+
configFile = DEFAULT_CONFIG_FILE;
144+
}
145+
146+
if (configFile) {
147+
if (fs.existsSync(configFile)) {
148+
loadConfig(configFile);
149+
} else {
150+
throw new CliError(`Config file ${configFile} not found`);
151+
}
152+
}
153+
});
154+
130155
return program;
131156
}
132157

packages/schema/src/cli/plugin-runner.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/* eslint-disable @typescript-eslint/no-var-requires */
2-
import { DMMF } from '@prisma/generator-helper';
2+
import type { DMMF } from '@prisma/generator-helper';
33
import { getDMMF } from '@prisma/internals';
44
import { isPlugin, Plugin } from '@zenstackhq/language/ast';
55
import { getLiteral, getLiteralArray, PluginError, PluginFunction, PluginOptions } from '@zenstackhq/sdk';
@@ -8,7 +8,8 @@ import fs from 'fs';
88
import ora from 'ora';
99
import path from 'path';
1010
import telemetry from '../telemetry';
11-
import { Context } from '../types';
11+
import type { Context } from '../types';
12+
import { config } from './config';
1213

1314
/**
1415
* ZenStack code generator
@@ -133,7 +134,7 @@ export class PluginRunner {
133134
plugin: name,
134135
},
135136
async () => {
136-
let result = run(context.schema, options, dmmf);
137+
let result = run(context.schema, options, dmmf, config);
137138
if (result instanceof Promise) {
138139
result = await result;
139140
}
Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
1+
import type { DMMF } from '@prisma/generator-helper';
12
import { Model } from '@zenstackhq/language/ast';
23
import { PluginOptions } from '@zenstackhq/sdk';
34
import PrismaSchemaGenerator from './schema-generator';
45

56
export const name = 'Prisma';
67

7-
export default async function run(model: Model, options: PluginOptions) {
8-
return new PrismaSchemaGenerator().generate(model, options);
8+
export default async function run(
9+
model: Model,
10+
options: PluginOptions,
11+
_dmmf?: DMMF.Document,
12+
config?: Record<string, string>
13+
) {
14+
return new PrismaSchemaGenerator().generate(model, options, config);
915
}

packages/schema/src/plugins/prisma/schema-generator.ts

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ export default class PrismaSchemaGenerator {
6969
7070
`;
7171

72-
async generate(model: Model, options: PluginOptions) {
72+
async generate(model: Model, options: PluginOptions, config?: Record<string, string>) {
7373
const prisma = new PrismaModel();
7474

7575
for (const decl of model.declarations) {
@@ -83,7 +83,7 @@ export default class PrismaSchemaGenerator {
8383
break;
8484

8585
case DataModel:
86-
this.generateModel(prisma, decl as DataModel);
86+
this.generateModel(prisma, decl as DataModel, config);
8787
break;
8888

8989
case GeneratorDecl:
@@ -191,26 +191,33 @@ export default class PrismaSchemaGenerator {
191191
);
192192
}
193193

194-
private generateModel(prisma: PrismaModel, decl: DataModel) {
194+
private generateModel(prisma: PrismaModel, decl: DataModel, config?: Record<string, string>) {
195195
const model = prisma.addModel(decl.name);
196196
for (const field of decl.fields) {
197197
this.generateModelField(model, field);
198198
}
199199

200200
// add an "zenstack_guard" field for dealing with boolean conditions
201-
model.addField(GUARD_FIELD_NAME, 'Boolean', [
201+
const guardField = model.addField(GUARD_FIELD_NAME, 'Boolean', [
202202
new PrismaFieldAttribute('@default', [
203203
new PrismaAttributeArg(undefined, new PrismaAttributeArgValue('Boolean', true)),
204204
]),
205205
]);
206206

207+
if (config?.guardFieldName && config?.guardFieldName !== GUARD_FIELD_NAME) {
208+
// generate a @map to rename field in the database
209+
guardField.addAttribute('@map', [
210+
new PrismaAttributeArg(undefined, new PrismaAttributeArgValue('String', config.guardFieldName)),
211+
]);
212+
}
213+
207214
const { allowAll, denyAll, hasFieldValidation } = analyzePolicies(decl);
208215

209216
if ((!allowAll && !denyAll) || hasFieldValidation) {
210217
// generate auxiliary fields for policy check
211218

212219
// add an "zenstack_transaction" field for tracking records created/updated with nested writes
213-
model.addField(TRANSACTION_FIELD_NAME, 'String?');
220+
const transactionField = model.addField(TRANSACTION_FIELD_NAME, 'String?');
214221

215222
// create an index for "zenstack_transaction" field
216223
model.addAttribute('@@index', [
@@ -221,6 +228,16 @@ export default class PrismaSchemaGenerator {
221228
])
222229
),
223230
]);
231+
232+
if (config?.transactionFieldName && config?.transactionFieldName !== TRANSACTION_FIELD_NAME) {
233+
// generate a @map to rename field in the database
234+
transactionField.addAttribute('@map', [
235+
new PrismaAttributeArg(
236+
undefined,
237+
new PrismaAttributeArgValue('String', config.transactionFieldName)
238+
),
239+
]);
240+
}
224241
}
225242

226243
for (const attr of decl.attributes.filter((attr) => this.isPrismaAttribute(attr))) {

packages/schema/tests/cli/cli.test.ts renamed to packages/schema/tests/cli/command.test.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import * as tmp from 'tmp';
88
import { createProgram } from '../../src/cli';
99
import { execSync } from '../../src/utils/exec-utils';
1010

11-
describe('CLI Tests', () => {
11+
describe('CLI Command Tests', () => {
1212
let projDir: string;
1313
let origDir: string;
1414

@@ -37,7 +37,7 @@ describe('CLI Tests', () => {
3737
createNpmrc();
3838

3939
const program = createProgram();
40-
program.parse(['init', '--tag', 'latest'], { from: 'user' });
40+
await program.parseAsync(['init', '--tag', 'latest'], { from: 'user' });
4141

4242
expect(fs.readFileSync('schema.zmodel', 'utf-8')).toEqual(fs.readFileSync('prisma/schema.prisma', 'utf-8'));
4343

@@ -53,7 +53,7 @@ describe('CLI Tests', () => {
5353
createNpmrc();
5454

5555
const program = createProgram();
56-
program.parse(['init', '--tag', 'latest'], { from: 'user' });
56+
await program.parseAsync(['init', '--tag', 'latest'], { from: 'user' });
5757

5858
expect(fs.readFileSync('schema.zmodel', 'utf-8')).toEqual(fs.readFileSync('prisma/schema.prisma', 'utf-8'));
5959

@@ -69,7 +69,7 @@ describe('CLI Tests', () => {
6969
createNpmrc();
7070

7171
const program = createProgram();
72-
program.parse(['init', '--tag', 'latest'], { from: 'user' });
72+
await program.parseAsync(['init', '--tag', 'latest'], { from: 'user' });
7373

7474
expect(fs.readFileSync('schema.zmodel', 'utf-8')).toEqual(fs.readFileSync('prisma/schema.prisma', 'utf-8'));
7575

@@ -86,7 +86,7 @@ describe('CLI Tests', () => {
8686
fs.renameSync('prisma/schema.prisma', 'prisma/my.prisma');
8787

8888
const program = createProgram();
89-
program.parse(['init', '--tag', 'latest', '--prisma', 'prisma/my.prisma'], { from: 'user' });
89+
await program.parseAsync(['init', '--tag', 'latest', '--prisma', 'prisma/my.prisma'], { from: 'user' });
9090

9191
expect(fs.readFileSync('schema.zmodel', 'utf-8')).toEqual(fs.readFileSync('prisma/my.prisma', 'utf-8'));
9292
});
@@ -96,7 +96,7 @@ describe('CLI Tests', () => {
9696
fs.writeFileSync('package.json', JSON.stringify({ name: 'my app', version: '1.0.0' }));
9797
createNpmrc();
9898
const program = createProgram();
99-
program.parse(['init', '--tag', 'latest'], { from: 'user' });
99+
await program.parseAsync(['init', '--tag', 'latest'], { from: 'user' });
100100
expect(fs.readFileSync('schema.zmodel', 'utf-8')).toBeTruthy();
101101
});
102102

@@ -111,7 +111,7 @@ describe('CLI Tests', () => {
111111
fs.writeFileSync('schema.zmodel', origZModelContent);
112112
createNpmrc();
113113
const program = createProgram();
114-
program.parse(['init', '--tag', 'latest'], { from: 'user' });
114+
await program.parseAsync(['init', '--tag', 'latest'], { from: 'user' });
115115
expect(fs.readFileSync('schema.zmodel', 'utf-8')).toEqual(origZModelContent);
116116
});
117117
});

0 commit comments

Comments
 (0)