diff --git a/packages/schema/package.json b/packages/schema/package.json index 89058bef6..a8a6d1425 100644 --- a/packages/schema/package.json +++ b/packages/schema/package.json @@ -98,6 +98,7 @@ "semver": "^7.3.8", "sleep-promise": "^9.1.0", "strip-color": "^0.1.0", + "tiny-invariant": "^1.3.1", "ts-morph": "^16.0.0", "ts-pattern": "^4.3.0", "upper-case-first": "^2.0.2", diff --git a/packages/schema/src/cli/actions/generate.ts b/packages/schema/src/cli/actions/generate.ts index 25553c094..ead57803f 100644 --- a/packages/schema/src/cli/actions/generate.ts +++ b/packages/schema/src/cli/actions/generate.ts @@ -1,8 +1,6 @@ import { PluginError } from '@zenstackhq/sdk'; import colors from 'colors'; import path from 'path'; -import { Context } from '../../types'; -import { PackageManagers } from '../../utils/pkg-utils'; import { CliError } from '../cli-error'; import { checkNewVersion, @@ -11,13 +9,15 @@ import { loadDocument, requiredPrismaVersion, } from '../cli-util'; -import { PluginRunner } from '../plugin-runner'; +import { PluginRunner, PluginRunnerOptions } from '../plugin-runner'; type Options = { schema: string; - packageManager: PackageManagers | undefined; + output?: string; dependencyCheck: boolean; versionCheck: boolean; + compile: boolean; + defaultPlugins: boolean; }; /** @@ -53,14 +53,17 @@ export async function generate(projectPath: string, options: Options) { async function runPlugins(options: Options) { const model = await loadDocument(options.schema); - const context: Context = { + + const runnerOpts: PluginRunnerOptions = { schema: model, schemaPath: path.resolve(options.schema), - outDir: path.dirname(options.schema), + defaultPlugins: options.defaultPlugins, + output: options.output, + compile: options.compile, }; try { - await new PluginRunner().run(context); + await new PluginRunner().run(runnerOpts); } catch (err) { if (err instanceof PluginError) { console.error(colors.red(`${err.plugin}: ${err.message}`)); diff --git a/packages/schema/src/cli/index.ts b/packages/schema/src/cli/index.ts index 8ea6fa28c..ae8f901cf 100644 --- a/packages/schema/src/cli/index.ts +++ b/packages/schema/src/cli/index.ts @@ -81,7 +81,7 @@ export function createProgram() { .addOption(configOption) .addOption(pmOption) .addOption(new Option('--prisma ', 'location of Prisma schema file to bootstrap from')) - .addOption(new Option('--tag [tag]', 'the NPM package tag to use when installing dependencies')) + .addOption(new Option('--tag ', 'the NPM package tag to use when installing dependencies')) .addOption(noVersionCheckOption) .argument('[path]', 'project path', '.') .action(initAction); @@ -90,8 +90,10 @@ export function createProgram() { .command('generate') .description('Run code generation.') .addOption(schemaOption) + .addOption(new Option('-o, --output ', 'default output directory for built-in plugins')) .addOption(configOption) - .addOption(pmOption) + .addOption(new Option('--no-default-plugins', 'do not run default plugins')) + .addOption(new Option('--no-compile', 'do not compile the output of built-in plugins')) .addOption(noVersionCheckOption) .addOption(noDependencyCheck) .action(generateAction); diff --git a/packages/schema/src/cli/plugin-runner.ts b/packages/schema/src/cli/plugin-runner.ts index 96819cdfb..b963e3da8 100644 --- a/packages/schema/src/cli/plugin-runner.ts +++ b/packages/schema/src/cli/plugin-runner.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-var-requires */ import type { DMMF } from '@prisma/generator-helper'; -import { isPlugin, Plugin } from '@zenstackhq/language/ast'; +import { isPlugin, Model, Plugin } from '@zenstackhq/language/ast'; import { getDataModels, getDMMF, @@ -19,9 +19,7 @@ import ora from 'ora'; import path from 'path'; import { ensureDefaultOutputFolder } from '../plugins/plugin-utils'; import telemetry from '../telemetry'; -import type { Context } from '../types'; import { getVersion } from '../utils/version-utils'; -import { config } from './config'; type PluginInfo = { name: string; @@ -32,6 +30,14 @@ type PluginInfo = { module: any; }; +export type PluginRunnerOptions = { + schema: Model; + schemaPath: string; + output?: string; + defaultPlugins: boolean; + compile: boolean; +}; + /** * ZenStack plugin runner */ @@ -39,16 +45,16 @@ export class PluginRunner { /** * Runs a series of nested generators */ - async run(context: Context): Promise { + async run(options: PluginRunnerOptions): Promise { const version = getVersion(); console.log(colors.bold(`āŒ›ļø ZenStack CLI v${version}, running plugins`)); - ensureDefaultOutputFolder(); + ensureDefaultOutputFolder(options); const plugins: PluginInfo[] = []; - const pluginDecls = context.schema.declarations.filter((d): d is Plugin => isPlugin(d)); + const pluginDecls = options.schema.declarations.filter((d): d is Plugin => isPlugin(d)); - let prismaOutput = resolvePath('./prisma/schema.prisma', { schemaPath: context.schemaPath, name: '' }); + let prismaOutput = resolvePath('./prisma/schema.prisma', { schemaPath: options.schemaPath, name: '' }); for (const pluginDecl of pluginDecls) { const pluginProvider = this.getPluginProvider(pluginDecl); @@ -73,59 +79,35 @@ export class PluginRunner { const dependencies = this.getPluginDependencies(pluginModule); const pluginName = this.getPluginName(pluginModule, pluginProvider); - const options: PluginOptions = { schemaPath: context.schemaPath, name: pluginName }; + const pluginOptions: PluginOptions = { schemaPath: options.schemaPath, name: pluginName }; pluginDecl.fields.forEach((f) => { const value = getLiteral(f.value) ?? getLiteralArray(f.value); if (value === undefined) { throw new PluginError(pluginName, `Invalid option value for ${f.name}`); } - options[f.name] = value; + pluginOptions[f.name] = value; }); plugins.push({ name: pluginName, provider: pluginProvider, dependencies, - options, + options: pluginOptions, run: pluginModule.default as PluginFunction, module: pluginModule, }); - if (pluginProvider === '@core/prisma' && typeof options.output === 'string') { + if (pluginProvider === '@core/prisma' && typeof pluginOptions.output === 'string') { // record custom prisma output path - prismaOutput = resolvePath(options.output, options); + prismaOutput = resolvePath(pluginOptions.output, pluginOptions); } } - // make sure prerequisites are included - const corePlugins: Array<{ provider: string; options?: Record }> = [ - { provider: '@core/prisma' }, - { provider: '@core/model-meta' }, - { provider: '@core/access-policy' }, - ]; - - if (getDataModels(context.schema).some((model) => hasValidationAttributes(model))) { - // '@core/zod' plugin is auto-enabled if there're validation rules - corePlugins.push({ provider: '@core/zod', options: { modelOnly: true } }); - } - - // core plugins introduced by dependencies - plugins - .flatMap((p) => p.dependencies) - .forEach((dep) => { - if (dep.startsWith('@core/')) { - const existing = corePlugins.find((p) => p.provider === dep); - if (existing) { - // reset options to default - existing.options = undefined; - } else { - // add core dependency - corePlugins.push({ provider: dep }); - } - } - }); + // get core plugins that need to be enabled + const corePlugins = this.calculateCorePlugins(options, plugins); + // shift/insert core plugins to the front for (const corePlugin of corePlugins.reverse()) { const existingIdx = plugins.findIndex((p) => p.provider === corePlugin.provider); if (existingIdx >= 0) { @@ -141,7 +123,7 @@ export class PluginRunner { name: pluginName, provider: corePlugin.provider, dependencies: [], - options: { schemaPath: context.schemaPath, name: pluginName, ...corePlugin.options }, + options: { schemaPath: options.schemaPath, name: pluginName, ...corePlugin.options }, run: pluginModule.default, module: pluginModule, }); @@ -161,12 +143,17 @@ export class PluginRunner { } } + if (plugins.length === 0) { + console.log(colors.yellow('No plugins configured.')); + return; + } + const warnings: string[] = []; let dmmf: DMMF.Document | undefined = undefined; - for (const { name, provider, run, options } of plugins) { + for (const { name, provider, run, options: pluginOptions } of plugins) { // const start = Date.now(); - await this.runPlugin(name, run, context, options, dmmf, warnings); + await this.runPlugin(name, run, options, pluginOptions, dmmf, warnings); // console.log(`āœ… Plugin ${colors.bold(name)} (${provider}) completed in ${Date.now() - start}ms`); if (provider === '@core/prisma') { // load prisma DMMF @@ -175,7 +162,6 @@ export class PluginRunner { }); } } - console.log(colors.green(colors.bold('\nšŸ‘» All plugins completed successfully!'))); warnings.forEach((w) => console.warn(colors.yellow(w))); @@ -183,6 +169,57 @@ export class PluginRunner { console.log(`Don't forget to restart your dev server to let the changes take effect.`); } + private calculateCorePlugins(options: PluginRunnerOptions, plugins: PluginInfo[]) { + const corePlugins: Array<{ provider: string; options?: Record }> = []; + + if (options.defaultPlugins) { + corePlugins.push( + { provider: '@core/prisma' }, + { provider: '@core/model-meta' }, + { provider: '@core/access-policy' } + ); + } else if (plugins.length > 0) { + // "@core/prisma" plugin is always enabled if any plugin is configured + corePlugins.push({ provider: '@core/prisma' }); + } + + // "@core/access-policy" has implicit requirements + if ([...plugins, ...corePlugins].find((p) => p.provider === '@core/access-policy')) { + // make sure "@core/model-meta" is enabled + if (!corePlugins.find((p) => p.provider === '@core/model-meta')) { + corePlugins.push({ provider: '@core/model-meta' }); + } + + // '@core/zod' plugin is auto-enabled by "@core/access-policy" + // if there're validation rules + if (!corePlugins.find((p) => p.provider === '@core/zod') && this.hasValidation(options.schema)) { + corePlugins.push({ provider: '@core/zod', options: { modelOnly: true } }); + } + } + + // core plugins introduced by dependencies + plugins + .flatMap((p) => p.dependencies) + .forEach((dep) => { + if (dep.startsWith('@core/')) { + const existing = corePlugins.find((p) => p.provider === dep); + if (existing) { + // reset options to default + existing.options = undefined; + } else { + // add core dependency + corePlugins.push({ provider: dep }); + } + } + }); + + return corePlugins; + } + + private hasValidation(schema: Model) { + return getDataModels(schema).some((model) => hasValidationAttributes(model)); + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any private getPluginName(pluginModule: any, pluginProvider: string): string { return typeof pluginModule.name === 'string' ? (pluginModule.name as string) : pluginProvider; @@ -200,7 +237,7 @@ export class PluginRunner { private async runPlugin( name: string, run: PluginFunction, - context: Context, + runnerOptions: PluginRunnerOptions, options: PluginOptions, dmmf: DMMF.Document | undefined, warnings: string[] @@ -216,7 +253,10 @@ export class PluginRunner { options, }, async () => { - let result = run(context.schema, options, dmmf, config); + let result = run(runnerOptions.schema, options, dmmf, { + output: runnerOptions.output, + compile: runnerOptions.compile, + }); if (result instanceof Promise) { result = await result; } diff --git a/packages/schema/src/plugins/access-policy/index.ts b/packages/schema/src/plugins/access-policy/index.ts index c47f6e11d..cbdcbd64f 100644 --- a/packages/schema/src/plugins/access-policy/index.ts +++ b/packages/schema/src/plugins/access-policy/index.ts @@ -1,9 +1,10 @@ -import { Model } from '@zenstackhq/language/ast'; -import { PluginOptions } from '@zenstackhq/sdk'; +import { PluginFunction } from '@zenstackhq/sdk'; import PolicyGenerator from './policy-guard-generator'; export const name = 'Access Policy'; -export default async function run(model: Model, options: PluginOptions) { - return new PolicyGenerator().generate(model, options); -} +const run: PluginFunction = async (model, options, _dmmf, globalOptions) => { + return new PolicyGenerator().generate(model, options, globalOptions); +}; + +export default run; diff --git a/packages/schema/src/plugins/access-policy/policy-guard-generator.ts b/packages/schema/src/plugins/access-policy/policy-guard-generator.ts index 9acb3f37c..b6ac99576 100644 --- a/packages/schema/src/plugins/access-policy/policy-guard-generator.ts +++ b/packages/schema/src/plugins/access-policy/policy-guard-generator.ts @@ -30,6 +30,7 @@ import { import { ExpressionContext, PluginError, + PluginGlobalOptions, PluginOptions, RUNTIME_PACKAGE, analyzePolicies, @@ -65,8 +66,8 @@ import { ExpressionWriter, FALSE, TRUE } from './expression-writer'; * Generates source file that contains Prisma query guard objects used for injecting database queries */ export default class PolicyGenerator { - async generate(model: Model, options: PluginOptions) { - let output = options.output ? (options.output as string) : getDefaultOutputFolder(); + async generate(model: Model, options: PluginOptions, globalOptions?: PluginGlobalOptions) { + let output = options.output ? (options.output as string) : getDefaultOutputFolder(globalOptions); if (!output) { throw new PluginError(options.name, `Unable to determine output path, not running plugin`); } @@ -147,7 +148,14 @@ export default class PolicyGenerator { sf.addStatements('export default policy'); - const shouldCompile = options.compile !== false; + let shouldCompile = true; + if (typeof options.compile === 'boolean') { + // explicit override + shouldCompile = options.compile; + } else if (globalOptions) { + shouldCompile = globalOptions.compile; + } + if (!shouldCompile || options.preserveTsFiles === true) { // save ts files await saveProject(project); diff --git a/packages/schema/src/plugins/model-meta/index.ts b/packages/schema/src/plugins/model-meta/index.ts index 7049c9957..8c4432db5 100644 --- a/packages/schema/src/plugins/model-meta/index.ts +++ b/packages/schema/src/plugins/model-meta/index.ts @@ -8,7 +8,6 @@ import { isNumberLiteral, isReferenceExpr, isStringLiteral, - Model, ReferenceExpr, } from '@zenstackhq/language/ast'; import type { RuntimeAttribute } from '@zenstackhq/runtime'; @@ -22,7 +21,7 @@ import { hasAttribute, isIdField, PluginError, - PluginOptions, + PluginFunction, resolved, resolvePath, saveProject, @@ -34,8 +33,8 @@ import { getDefaultOutputFolder } from '../plugin-utils'; export const name = 'Model Metadata'; -export default async function run(model: Model, options: PluginOptions) { - let output = options.output ? (options.output as string) : getDefaultOutputFolder(); +const run: PluginFunction = async (model, options, _dmmf, globalOptions) => { + let output = options.output ? (options.output as string) : getDefaultOutputFolder(globalOptions); if (!output) { throw new PluginError(options.name, `Unable to determine output path, not running plugin`); } @@ -53,7 +52,15 @@ export default async function run(model: Model, options: PluginOptions) { }); sf.addStatements('export default metadata;'); - const shouldCompile = options.compile !== false; + let shouldCompile = true; + if (typeof options.compile === 'boolean') { + // explicit override + shouldCompile = options.compile; + } else if (globalOptions) { + // from CLI or config file + shouldCompile = globalOptions.compile; + } + if (!shouldCompile || options.preserveTsFiles === true) { // save ts files await saveProject(project); @@ -61,7 +68,7 @@ export default async function run(model: Model, options: PluginOptions) { if (shouldCompile) { await emitProject(project); } -} +}; function generateModelMetadata(dataModels: DataModel[], writer: CodeBlockWriter) { writer.block(() => { @@ -256,3 +263,5 @@ function generateForeignKeyMapping(field: DataModelField) { }); return result; } + +export default run; diff --git a/packages/schema/src/plugins/plugin-utils.ts b/packages/schema/src/plugins/plugin-utils.ts index 8371d67a0..dfbb2334f 100644 --- a/packages/schema/src/plugins/plugin-utils.ts +++ b/packages/schema/src/plugins/plugin-utils.ts @@ -1,6 +1,8 @@ import type { PolicyOperationKind } from '@zenstackhq/runtime'; +import { PluginGlobalOptions } from '@zenstackhq/sdk'; import fs from 'fs'; import path from 'path'; +import { PluginRunnerOptions } from '../cli/plugin-runner'; export const ALL_OPERATION_KINDS: PolicyOperationKind[] = ['create', 'update', 'postUpdate', 'read', 'delete']; @@ -24,29 +26,37 @@ export function getNodeModulesFolder(startPath?: string): string | undefined { /** * Ensure the default output folder is initialized. */ -export function ensureDefaultOutputFolder() { - const output = getDefaultOutputFolder(); +export function ensureDefaultOutputFolder(options: PluginRunnerOptions) { + const output = options.output ? path.resolve(options.output) : getDefaultOutputFolder(); if (output && !fs.existsSync(output)) { - const pkgJson = { - name: '.zenstack', - version: '1.0.0', - exports: { - './zod': { - default: './zod/index.js', - types: './zod/index.d.ts', - }, - }, - }; fs.mkdirSync(output, { recursive: true }); - fs.writeFileSync(path.join(output, 'package.json'), JSON.stringify(pkgJson, undefined, 4)); + if (!options.output) { + const pkgJson = { + name: '.zenstack', + version: '1.0.0', + exports: { + './zod': { + default: './zod/index.js', + types: './zod/index.d.ts', + }, + }, + }; + fs.writeFileSync(path.join(output, 'package.json'), JSON.stringify(pkgJson, undefined, 4)); + } } + + return output; } /** * Gets the default node_modules/.zenstack output folder for plugins. * @returns */ -export function getDefaultOutputFolder() { +export function getDefaultOutputFolder(globalOptions?: PluginGlobalOptions) { + if (typeof globalOptions?.output === 'string') { + return path.resolve(globalOptions.output); + } + // Find the real runtime module path, it might be a symlink in pnpm let runtimeModulePath = require.resolve('@zenstackhq/runtime'); diff --git a/packages/schema/src/plugins/prisma/index.ts b/packages/schema/src/plugins/prisma/index.ts index 6ffc0a6a8..3a96cf40f 100644 --- a/packages/schema/src/plugins/prisma/index.ts +++ b/packages/schema/src/plugins/prisma/index.ts @@ -1,15 +1,10 @@ -import type { DMMF } from '@prisma/generator-helper'; -import { Model } from '@zenstackhq/language/ast'; -import { PluginOptions } from '@zenstackhq/sdk'; +import { PluginFunction } from '@zenstackhq/sdk'; import PrismaSchemaGenerator from './schema-generator'; export const name = 'Prisma'; -export default async function run( - model: Model, - options: PluginOptions, - _dmmf?: DMMF.Document, - config?: Record -) { - return new PrismaSchemaGenerator().generate(model, options, config); -} +const run: PluginFunction = async (model, options, _dmmf, _globalOptions) => { + return new PrismaSchemaGenerator().generate(model, options); +}; + +export default run; diff --git a/packages/schema/src/plugins/prisma/schema-generator.ts b/packages/schema/src/plugins/prisma/schema-generator.ts index ea7a1436c..7406bff3e 100644 --- a/packages/schema/src/plugins/prisma/schema-generator.ts +++ b/packages/schema/src/plugins/prisma/schema-generator.ts @@ -81,7 +81,7 @@ export default class PrismaSchemaGenerator { `; - async generate(model: Model, options: PluginOptions, _config?: Record) { + async generate(model: Model, options: PluginOptions) { const warnings: string[] = []; const prismaVersion = getPrismaVersion(); diff --git a/packages/schema/src/plugins/zod/generator.ts b/packages/schema/src/plugins/zod/generator.ts index 9caefd62e..854fa91b7 100644 --- a/packages/schema/src/plugins/zod/generator.ts +++ b/packages/schema/src/plugins/zod/generator.ts @@ -1,5 +1,6 @@ import { ConnectorType, DMMF } from '@prisma/generator-helper'; import { + PluginGlobalOptions, PluginOptions, createProject, emitProject, @@ -25,10 +26,15 @@ import Transformer from './transformer'; import removeDir from './utils/removeDir'; import { makeFieldSchema, makeValidationRefinements } from './utils/schema-gen'; -export async function generate(model: Model, options: PluginOptions, dmmf: DMMF.Document) { +export async function generate( + model: Model, + options: PluginOptions, + dmmf: DMMF.Document, + globalOptions?: PluginGlobalOptions +) { let output = options.output as string; if (!output) { - const defaultOutputFolder = getDefaultOutputFolder(); + const defaultOutputFolder = getDefaultOutputFolder(globalOptions); if (defaultOutputFolder) { output = path.join(defaultOutputFolder, 'zod'); } else { @@ -96,7 +102,15 @@ export async function generate(model: Model, options: PluginOptions, dmmf: DMMF. project.createSourceFile(path.join(output, 'index.ts'), exports.join(';\n'), { overwrite: true }); // emit - const shouldCompile = options.compile !== false; + let shouldCompile = true; + if (typeof options.compile === 'boolean') { + // explicit override + shouldCompile = options.compile; + } else if (globalOptions) { + // from CLI or config file + shouldCompile = globalOptions.compile; + } + if (!shouldCompile || options.preserveTsFiles === true) { // save ts files await saveProject(project); diff --git a/packages/schema/src/plugins/zod/index.ts b/packages/schema/src/plugins/zod/index.ts index 80d454533..b2b43cb40 100644 --- a/packages/schema/src/plugins/zod/index.ts +++ b/packages/schema/src/plugins/zod/index.ts @@ -1,10 +1,12 @@ -import type { DMMF } from '@prisma/generator-helper'; -import { PluginOptions } from '@zenstackhq/sdk'; -import { Model } from '@zenstackhq/sdk/ast'; +import { PluginFunction } from '@zenstackhq/sdk'; +import invariant from 'tiny-invariant'; import { generate } from './generator'; export const name = 'Zod'; -export default async function run(model: Model, options: PluginOptions, dmmf: DMMF.Document) { - return generate(model, options, dmmf); -} +const run: PluginFunction = async (model, options, dmmf, globalOptions) => { + invariant(dmmf); + return generate(model, options, dmmf, globalOptions); +}; + +export default run; diff --git a/packages/schema/src/types.ts b/packages/schema/src/types.ts deleted file mode 100644 index 436b2ec8b..000000000 --- a/packages/schema/src/types.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Model } from '@zenstackhq/language/ast'; - -export interface Context { - schema: Model; - schemaPath: string; - outDir: string; -} - -export interface Generator { - get name(): string; - get startMessage(): string; - get successMessage(): string; - generate(context: Context): Promise; -} diff --git a/packages/sdk/src/types.ts b/packages/sdk/src/types.ts index b79829900..c19fdfc42 100644 --- a/packages/sdk/src/types.ts +++ b/packages/sdk/src/types.ts @@ -7,12 +7,39 @@ import { Model } from '@zenstackhq/language/ast'; export type OptionValue = string | number | boolean; /** - * Plugin configuration oiptions + * Plugin configuration options */ -export type PluginOptions = { provider?: string; schemaPath: string; name: string } & Record< - string, - OptionValue | OptionValue[] ->; +export type PluginOptions = { + /*** + * The provider package + */ + provider?: string; + + /** + * The path of the ZModel schema + */ + schemaPath: string; + + /** + * The name of the plugin + */ + name: string; +} & Record; + +/** + * Global options that apply to all plugins + */ +export type PluginGlobalOptions = { + /** + * Default output directory + */ + output?: string; + + /** + * Whether to compile the generated code + */ + compile: boolean; +}; /** * Plugin entry point function definition @@ -21,7 +48,7 @@ export type PluginFunction = ( model: Model, options: PluginOptions, dmmf?: DMMF.Document, - config?: Record + globalOptions?: PluginGlobalOptions ) => Promise | string[] | Promise | void; /** diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9d8560b7d..5379c7236 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -124,7 +124,7 @@ importers: version: 0.2.1 ts-jest: specifier: ^29.0.5 - version: 29.0.5(@babel/core@7.22.5)(esbuild@0.18.13)(jest@29.5.0)(typescript@4.9.5) + version: 29.0.5(@babel/core@7.22.9)(esbuild@0.18.13)(jest@29.5.0)(typescript@4.9.5) typescript: specifier: ^4.9.5 version: 4.9.5 @@ -488,6 +488,9 @@ importers: strip-color: specifier: ^0.1.0 version: 0.1.0 + tiny-invariant: + specifier: ^1.3.1 + version: 1.3.1 ts-morph: specifier: ^16.0.0 version: 16.0.0 @@ -593,7 +596,7 @@ importers: version: 0.2.1 ts-jest: specifier: ^29.0.3 - version: 29.0.3(@babel/core@7.22.9)(esbuild@0.15.12)(jest@29.5.0)(typescript@4.8.4) + version: 29.0.3(@babel/core@7.22.5)(esbuild@0.15.12)(jest@29.5.0)(typescript@4.8.4) ts-node: specifier: ^10.9.1 version: 10.9.1(@types/node@18.0.0)(typescript@4.8.4) @@ -10561,7 +10564,7 @@ packages: yargs-parser: 21.1.1 dev: true - /ts-jest@29.0.3(@babel/core@7.22.9)(esbuild@0.15.12)(jest@29.5.0)(typescript@4.8.4): + /ts-jest@29.0.3(@babel/core@7.22.5)(esbuild@0.15.12)(jest@29.5.0)(typescript@4.8.4): resolution: {integrity: sha512-Ibygvmuyq1qp/z3yTh9QTwVVAbFdDy/+4BtIQR2sp6baF2SJU/8CKK/hhnGIDY2L90Az2jIqTwZPnN2p+BweiQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true @@ -10582,7 +10585,7 @@ packages: esbuild: optional: true dependencies: - '@babel/core': 7.22.9 + '@babel/core': 7.22.5 bs-logger: 0.2.6 esbuild: 0.15.12 fast-json-stable-stringify: 2.1.0 @@ -10596,7 +10599,7 @@ packages: yargs-parser: 21.1.1 dev: true - /ts-jest@29.0.5(@babel/core@7.22.5)(esbuild@0.18.13)(jest@29.5.0)(typescript@4.9.5): + /ts-jest@29.0.5(@babel/core@7.22.9)(esbuild@0.18.13)(jest@29.5.0)(typescript@4.9.4): resolution: {integrity: sha512-PL3UciSgIpQ7f6XjVOmbi96vmDHUqAyqDr8YxzopDqX3kfgYtX1cuNeBjP+L9sFXi6nzsGGA6R3fP3DDDJyrxA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true @@ -10617,7 +10620,7 @@ packages: esbuild: optional: true dependencies: - '@babel/core': 7.22.5 + '@babel/core': 7.22.9 bs-logger: 0.2.6 esbuild: 0.18.13 fast-json-stable-stringify: 2.1.0 @@ -10627,11 +10630,11 @@ packages: lodash.memoize: 4.1.2 make-error: 1.3.6 semver: 7.5.3 - typescript: 4.9.5 + typescript: 4.9.4 yargs-parser: 21.1.1 dev: true - /ts-jest@29.0.5(@babel/core@7.22.9)(esbuild@0.18.13)(jest@29.5.0)(typescript@4.9.4): + /ts-jest@29.0.5(@babel/core@7.22.9)(esbuild@0.18.13)(jest@29.5.0)(typescript@4.9.5): resolution: {integrity: sha512-PL3UciSgIpQ7f6XjVOmbi96vmDHUqAyqDr8YxzopDqX3kfgYtX1cuNeBjP+L9sFXi6nzsGGA6R3fP3DDDJyrxA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true @@ -10662,7 +10665,7 @@ packages: lodash.memoize: 4.1.2 make-error: 1.3.6 semver: 7.5.3 - typescript: 4.9.4 + typescript: 4.9.5 yargs-parser: 21.1.1 dev: true diff --git a/tests/integration/tests/cli/generate.test.ts b/tests/integration/tests/cli/generate.test.ts new file mode 100644 index 000000000..40a7981c8 --- /dev/null +++ b/tests/integration/tests/cli/generate.test.ts @@ -0,0 +1,183 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +/// + +import * as fs from 'fs'; +import path from 'path'; +import * as tmp from 'tmp'; +import { createProgram } from '../../../../packages/schema/src/cli'; +import { execSync } from '../../../../packages/schema/src/utils/exec-utils'; +import { createNpmrc } from './share'; + +describe('CLI generate command tests', () => { + let origDir: string; + const MODEL = ` +datasource db { + provider = "sqlite" + url = "file:./dev.db" +} + +generator js { + provider = "prisma-client-js" +} + +model User { + id Int @id @default(autoincrement()) + email String @unique @email + posts Post[] +} + +model Post { + id Int @id @default(autoincrement()) + title String + author User @relation(fields: [authorId], references: [id]) + authorId Int +} + `; + + beforeEach(() => { + origDir = process.cwd(); + const r = tmp.dirSync({ unsafeCleanup: true }); + console.log(`Project dir: ${r.name}`); + process.chdir(r.name); + + // set up project + fs.writeFileSync('package.json', JSON.stringify({ name: 'my app', version: '1.0.0' })); + createNpmrc(); + execSync('npm install prisma @prisma/client zod'); + execSync(`npm install ${path.join(__dirname, '../../../../packages/runtime/dist')}`); + + // set up schema + fs.writeFileSync('schema.zmodel', MODEL, 'utf-8'); + }); + + afterEach(() => { + process.chdir(origDir); + }); + + it('generate standard', async () => { + const program = createProgram(); + await program.parseAsync(['generate', '--no-dependency-check'], { from: 'user' }); + expect(fs.existsSync('./node_modules/.zenstack/policy.js')).toBeTruthy(); + expect(fs.existsSync('./node_modules/.zenstack/model-meta.js')).toBeTruthy(); + expect(fs.existsSync('./node_modules/.zenstack/zod/index.js')).toBeTruthy(); + }); + + it('generate custom output default', async () => { + const program = createProgram(); + await program.parseAsync(['generate', '--no-dependency-check', '-o', 'out'], { from: 'user' }); + expect(fs.existsSync('./node_modules/.zenstack')).toBeFalsy(); + expect(fs.existsSync('./out/policy.js')).toBeTruthy(); + expect(fs.existsSync('./out/model-meta.js')).toBeTruthy(); + expect(fs.existsSync('./out/zod')).toBeTruthy(); + }); + + it('generate custom output non-std schema location', async () => { + fs.mkdirSync('./schema'); + fs.cpSync('schema.zmodel', './schema/my.zmodel'); + fs.rmSync('schema.zmodel'); + + const program = createProgram(); + await program.parseAsync(['generate', '--no-dependency-check', '--schema', './schema/my.zmodel', '-o', 'out'], { + from: 'user', + }); + expect(fs.existsSync('./node_modules/.zenstack')).toBeFalsy(); + expect(fs.existsSync('./out/policy.js')).toBeTruthy(); + expect(fs.existsSync('./out/model-meta.js')).toBeTruthy(); + expect(fs.existsSync('./out/zod')).toBeTruthy(); + }); + + it('generate custom output override', async () => { + fs.appendFileSync( + 'schema.zmodel', + ` + plugin policy { + provider = '@core/access-policy' + output = 'policy-out' + } + ` + ); + + const program = createProgram(); + await program.parseAsync(['generate', '--no-dependency-check', '-o', 'out'], { from: 'user' }); + expect(fs.existsSync('./node_modules/.zenstack')).toBeFalsy(); + expect(fs.existsSync('./out/model-meta.js')).toBeTruthy(); + expect(fs.existsSync('./out/zod')).toBeTruthy(); + expect(fs.existsSync('./out/policy.js')).toBeFalsy(); + expect(fs.existsSync('./policy-out/policy.js')).toBeTruthy(); + }); + + it('generate no default plugins run nothing', async () => { + const program = createProgram(); + await program.parseAsync(['generate', '--no-dependency-check', '--no-default-plugins'], { from: 'user' }); + expect(fs.existsSync('./node_modules/.zenstack/policy.js')).toBeFalsy(); + expect(fs.existsSync('./node_modules/.zenstack/model-meta.js')).toBeFalsy(); + expect(fs.existsSync('./node_modules/.zenstack/zod')).toBeFalsy(); + expect(fs.existsSync('./prisma/schema.prisma')).toBeFalsy(); + }); + + it('generate no default plugins with prisma only', async () => { + fs.appendFileSync( + 'schema.zmodel', + ` + plugin prisma { + provider = '@core/prisma' + } + ` + ); + const program = createProgram(); + await program.parseAsync(['generate', '--no-dependency-check', '--no-default-plugins'], { from: 'user' }); + expect(fs.existsSync('./node_modules/.zenstack/policy.js')).toBeFalsy(); + expect(fs.existsSync('./node_modules/.zenstack/model-meta.js')).toBeFalsy(); + expect(fs.existsSync('./node_modules/.zenstack/zod')).toBeFalsy(); + expect(fs.existsSync('./prisma/schema.prisma')).toBeTruthy(); + }); + + it('generate no default plugins with access-policy with zod', async () => { + fs.appendFileSync( + 'schema.zmodel', + ` + plugin policy { + provider = '@core/access-policy' + } + ` + ); + const program = createProgram(); + await program.parseAsync(['generate', '--no-dependency-check', '--no-default-plugins'], { from: 'user' }); + expect(fs.existsSync('./node_modules/.zenstack/policy.js')).toBeTruthy(); + expect(fs.existsSync('./node_modules/.zenstack/model-meta.js')).toBeTruthy(); + expect(fs.existsSync('./node_modules/.zenstack/zod')).toBeTruthy(); + expect(fs.existsSync('./prisma/schema.prisma')).toBeTruthy(); + }); + + it('generate no default plugins with access-policy without zod', async () => { + fs.appendFileSync( + 'schema.zmodel', + ` + plugin policy { + provider = '@core/access-policy' + } + ` + ); + let content = fs.readFileSync('schema.zmodel', 'utf-8'); + content = content.replace('@email', ''); + fs.writeFileSync('schema.zmodel', content, 'utf-8'); + + const program = createProgram(); + await program.parseAsync(['generate', '--no-dependency-check', '--no-default-plugins'], { from: 'user' }); + expect(fs.existsSync('./node_modules/.zenstack/policy.js')).toBeTruthy(); + expect(fs.existsSync('./node_modules/.zenstack/model-meta.js')).toBeTruthy(); + expect(fs.existsSync('./prisma/schema.prisma')).toBeTruthy(); + expect(fs.existsSync('./node_modules/.zenstack/zod')).toBeFalsy(); + }); + + it('generate no compile', async () => { + const program = createProgram(); + await program.parseAsync(['generate', '--no-dependency-check', '--no-compile'], { from: 'user' }); + expect(fs.existsSync('./node_modules/.zenstack/policy.js')).toBeFalsy(); + expect(fs.existsSync('./node_modules/.zenstack/policy.ts')).toBeTruthy(); + expect(fs.existsSync('./node_modules/.zenstack/model-meta.js')).toBeFalsy(); + expect(fs.existsSync('./node_modules/.zenstack/model-meta.ts')).toBeTruthy(); + expect(fs.existsSync('./node_modules/.zenstack/zod/index.js')).toBeFalsy(); + expect(fs.existsSync('./node_modules/.zenstack/zod/index.ts')).toBeTruthy(); + }); +}); diff --git a/tests/integration/tests/cli/command.test.ts b/tests/integration/tests/cli/init.test.ts similarity index 91% rename from tests/integration/tests/cli/command.test.ts rename to tests/integration/tests/cli/init.test.ts index 47671a69d..96492b286 100644 --- a/tests/integration/tests/cli/command.test.ts +++ b/tests/integration/tests/cli/init.test.ts @@ -7,8 +7,9 @@ import * as path from 'path'; import * as tmp from 'tmp'; import { createProgram } from '../../../../packages/schema/src/cli'; import { execSync } from '../../../../packages/schema/src/utils/exec-utils'; +import { createNpmrc } from './share'; -describe('CLI Command Tests', () => { +describe('CLI init command tests', () => { let origDir: string; beforeEach(() => { @@ -22,10 +23,6 @@ describe('CLI Command Tests', () => { process.chdir(origDir); }); - function createNpmrc() { - fs.writeFileSync('.npmrc', `cache=${getWorkspaceNpmCacheFolder(__dirname)}`); - } - it('init project t3 npm std', async () => { execSync('npx --yes create-t3-app@latest --prisma --CI --noGit .', 'inherit', { npm_config_user_agent: 'npm', @@ -98,6 +95,14 @@ describe('CLI Command Tests', () => { expect(fs.readFileSync('schema.zmodel', 'utf-8')).toBeTruthy(); }); + it('init project no version check', async () => { + fs.writeFileSync('package.json', JSON.stringify({ name: 'my app', version: '1.0.0' })); + createNpmrc(); + const program = createProgram(); + await program.parseAsync(['init', '--tag', 'latest', '--no-version-check'], { from: 'user' }); + expect(fs.readFileSync('schema.zmodel', 'utf-8')).toBeTruthy(); + }); + it('init project existing zmodel', async () => { fs.writeFileSync('package.json', JSON.stringify({ name: 'my app', version: '1.0.0' })); const origZModelContent = ` diff --git a/tests/integration/tests/cli/share.ts b/tests/integration/tests/cli/share.ts new file mode 100644 index 000000000..7d4f8805d --- /dev/null +++ b/tests/integration/tests/cli/share.ts @@ -0,0 +1,6 @@ +import { getWorkspaceNpmCacheFolder } from '@zenstackhq/testtools'; +import fs from 'fs'; + +export function createNpmrc() { + fs.writeFileSync('.npmrc', `cache=${getWorkspaceNpmCacheFolder(__dirname)}`); +}