Skip to content

feat: add CLI config file support #328

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Apr 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/schema/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@
"colors": "1.4.0",
"commander": "^8.3.0",
"cuid": "^2.1.8",
"get-latest-version": "^5.0.1",
"langium": "1.1.0",
"mixpanel": "^0.17.0",
"node-machine-id": "^1.1.12",
Expand All @@ -108,7 +109,7 @@
"vscode-languageserver-textdocument": "^1.0.7",
"vscode-uri": "^3.0.6",
"zod": "^3.19.1",
"get-latest-version": "^5.0.1"
"zod-validation-error": "^0.2.1"
},
"devDependencies": {
"@types/async-exit-hook": "^2.0.0",
Expand Down
40 changes: 40 additions & 0 deletions packages/schema/src/cli/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { GUARD_FIELD_NAME, TRANSACTION_FIELD_NAME } from '@zenstackhq/sdk';
import fs from 'fs';
import z from 'zod';
import { fromZodError } from 'zod-validation-error';
import { CliError } from './cli-error';

const schema = z
.object({
guardFieldName: z.string().default(GUARD_FIELD_NAME),
transactionFieldName: z.string().default(TRANSACTION_FIELD_NAME),
})
.strict();

export type ConfigType = z.infer<typeof schema>;

export let config: ConfigType = schema.parse({});

/**
* Loads and validates CLI configuration file.
* @returns
*/
export function loadConfig(filename: string) {
if (!fs.existsSync(filename)) {
return;
}

let content: unknown;
try {
content = JSON.parse(fs.readFileSync(filename, 'utf-8'));
} catch {
throw new CliError(`Config is not a valid JSON file: ${filename}`);
}

const parsed = schema.safeParse(content);
if (!parsed.success) {
throw new CliError(`Config file ${filename} is not valid: ${fromZodError(parsed.error)}`);
}

config = parsed.data;
}
25 changes: 25 additions & 0 deletions packages/schema/src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,20 @@
import { ZModelLanguageMetaData } from '@zenstackhq/language/module';
import colors from 'colors';
import { Command, Option } from 'commander';
import fs from 'fs';
import * as semver from 'semver';
import telemetry from '../telemetry';
import { PackageManagers } from '../utils/pkg-utils';
import { getVersion } from '../utils/version-utils';
import { CliError } from './cli-error';
import { dumpInfo, initProject, runPlugins } from './cli-util';
import { loadConfig } from './config';

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

const DEFAULT_CONFIG_FILE = 'zenstack.config.json';

export const initAction = async (
projectPath: string,
options: {
Expand Down Expand Up @@ -97,6 +101,8 @@ export function createProgram() {
'./schema.zmodel'
);

const configOption = new Option('-c, --config [file]', 'config file');

const pmOption = new Option('-p, --package-manager <pm>', 'package manager to use').choices([
'npm',
'yarn',
Expand All @@ -114,6 +120,7 @@ export function createProgram() {
program
.command('init')
.description('Initialize an existing project for ZenStack.')
.addOption(configOption)
.addOption(pmOption)
.addOption(new Option('--prisma <file>', 'location of Prisma schema file to bootstrap from'))
.addOption(new Option('--tag [tag]', 'the NPM package tag to use when installing dependencies'))
Expand All @@ -124,9 +131,27 @@ export function createProgram() {
.command('generate')
.description('Run code generation.')
.addOption(schemaOption)
.addOption(configOption)
.addOption(pmOption)
.addOption(noDependencyCheck)
.action(generateAction);

// make sure config is loaded before actions run
program.hook('preAction', async (_, actionCommand) => {
let configFile: string | undefined = actionCommand.opts().config;
if (!configFile && fs.existsSync(DEFAULT_CONFIG_FILE)) {
configFile = DEFAULT_CONFIG_FILE;
}

if (configFile) {
if (fs.existsSync(configFile)) {
loadConfig(configFile);
} else {
throw new CliError(`Config file ${configFile} not found`);
}
}
});

return program;
}

Expand Down
7 changes: 4 additions & 3 deletions packages/schema/src/cli/plugin-runner.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* eslint-disable @typescript-eslint/no-var-requires */
import { DMMF } from '@prisma/generator-helper';
import type { DMMF } from '@prisma/generator-helper';
import { getDMMF } from '@prisma/internals';
import { isPlugin, Plugin } from '@zenstackhq/language/ast';
import { getLiteral, getLiteralArray, PluginError, PluginFunction, PluginOptions } from '@zenstackhq/sdk';
Expand All @@ -8,7 +8,8 @@ import fs from 'fs';
import ora from 'ora';
import path from 'path';
import telemetry from '../telemetry';
import { Context } from '../types';
import type { Context } from '../types';
import { config } from './config';

/**
* ZenStack code generator
Expand Down Expand Up @@ -133,7 +134,7 @@ export class PluginRunner {
plugin: name,
},
async () => {
let result = run(context.schema, options, dmmf);
let result = run(context.schema, options, dmmf, config);
if (result instanceof Promise) {
result = await result;
}
Expand Down
10 changes: 8 additions & 2 deletions packages/schema/src/plugins/prisma/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import type { DMMF } from '@prisma/generator-helper';
import { Model } from '@zenstackhq/language/ast';
import { PluginOptions } from '@zenstackhq/sdk';
import PrismaSchemaGenerator from './schema-generator';

export const name = 'Prisma';

export default async function run(model: Model, options: PluginOptions) {
return new PrismaSchemaGenerator().generate(model, options);
export default async function run(
model: Model,
options: PluginOptions,
_dmmf?: DMMF.Document,
config?: Record<string, string>
) {
return new PrismaSchemaGenerator().generate(model, options, config);
}
27 changes: 22 additions & 5 deletions packages/schema/src/plugins/prisma/schema-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export default class PrismaSchemaGenerator {

`;

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

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

case DataModel:
this.generateModel(prisma, decl as DataModel);
this.generateModel(prisma, decl as DataModel, config);
break;

case GeneratorDecl:
Expand Down Expand Up @@ -191,26 +191,33 @@ export default class PrismaSchemaGenerator {
);
}

private generateModel(prisma: PrismaModel, decl: DataModel) {
private generateModel(prisma: PrismaModel, decl: DataModel, config?: Record<string, string>) {
const model = prisma.addModel(decl.name);
for (const field of decl.fields) {
this.generateModelField(model, field);
}

// add an "zenstack_guard" field for dealing with boolean conditions
model.addField(GUARD_FIELD_NAME, 'Boolean', [
const guardField = model.addField(GUARD_FIELD_NAME, 'Boolean', [
new PrismaFieldAttribute('@default', [
new PrismaAttributeArg(undefined, new PrismaAttributeArgValue('Boolean', true)),
]),
]);

if (config?.guardFieldName && config?.guardFieldName !== GUARD_FIELD_NAME) {
// generate a @map to rename field in the database
guardField.addAttribute('@map', [
new PrismaAttributeArg(undefined, new PrismaAttributeArgValue('String', config.guardFieldName)),
]);
}

const { allowAll, denyAll, hasFieldValidation } = analyzePolicies(decl);

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

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

// create an index for "zenstack_transaction" field
model.addAttribute('@@index', [
Expand All @@ -221,6 +228,16 @@ export default class PrismaSchemaGenerator {
])
),
]);

if (config?.transactionFieldName && config?.transactionFieldName !== TRANSACTION_FIELD_NAME) {
// generate a @map to rename field in the database
transactionField.addAttribute('@map', [
new PrismaAttributeArg(
undefined,
new PrismaAttributeArgValue('String', config.transactionFieldName)
),
]);
}
}

for (const attr of decl.attributes.filter((attr) => this.isPrismaAttribute(attr))) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import * as tmp from 'tmp';
import { createProgram } from '../../src/cli';
import { execSync } from '../../src/utils/exec-utils';

describe('CLI Tests', () => {
describe('CLI Command Tests', () => {
let projDir: string;
let origDir: string;

Expand Down Expand Up @@ -37,7 +37,7 @@ describe('CLI Tests', () => {
createNpmrc();

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

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

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

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

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

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

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

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

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

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

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

Expand All @@ -111,7 +111,7 @@ describe('CLI Tests', () => {
fs.writeFileSync('schema.zmodel', origZModelContent);
createNpmrc();
const program = createProgram();
program.parse(['init', '--tag', 'latest'], { from: 'user' });
await program.parseAsync(['init', '--tag', 'latest'], { from: 'user' });
expect(fs.readFileSync('schema.zmodel', 'utf-8')).toEqual(origZModelContent);
});
});
Expand Down
Loading