From 90918c23528c1107b4a513139450ed27cb6f766c Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Tue, 9 Jan 2024 14:36:33 +0800 Subject: [PATCH 1/5] refactor: merge core plugins --- .../plugins/openapi/src/generator-base.ts | 8 +- packages/plugins/openapi/src/rpc-generator.ts | 2 +- packages/plugins/swr/src/generator.ts | 2 - .../plugins/tanstack-query/src/generator.ts | 9 +- packages/schema/src/cli/plugin-runner.ts | 167 +++++++++--------- .../schema/src/plugins/access-policy/index.ts | 10 -- .../schema/src/plugins/enhancer/enhancer.ts | 29 +++ packages/schema/src/plugins/enhancer/index.ts | 42 ++--- .../schema/src/plugins/enhancer/model-meta.ts | 13 ++ .../policy}/expression-writer.ts | 6 +- .../src/plugins/enhancer/policy/index.ts | 8 + .../policy}/policy-guard-generator.ts | 42 +---- .../schema/src/plugins/model-meta/index.ts | 42 ----- packages/schema/src/plugins/plugin-utils.ts | 9 + packages/schema/src/plugins/zod/generator.ts | 2 +- packages/sdk/src/model-meta-generator.ts | 13 +- packages/sdk/src/types.ts | 19 +- packages/sdk/src/utils.ts | 10 +- packages/testtools/src/schema.ts | 7 +- 19 files changed, 194 insertions(+), 246 deletions(-) delete mode 100644 packages/schema/src/plugins/access-policy/index.ts create mode 100644 packages/schema/src/plugins/enhancer/enhancer.ts create mode 100644 packages/schema/src/plugins/enhancer/model-meta.ts rename packages/schema/src/plugins/{access-policy => enhancer/policy}/expression-writer.ts (99%) create mode 100644 packages/schema/src/plugins/enhancer/policy/index.ts rename packages/schema/src/plugins/{access-policy => enhancer/policy}/policy-guard-generator.ts (96%) delete mode 100644 packages/schema/src/plugins/model-meta/index.ts diff --git a/packages/plugins/openapi/src/generator-base.ts b/packages/plugins/openapi/src/generator-base.ts index d00c081fc..1a46fa528 100644 --- a/packages/plugins/openapi/src/generator-base.ts +++ b/packages/plugins/openapi/src/generator-base.ts @@ -2,9 +2,10 @@ import type { DMMF } from '@prisma/generator-helper'; import { PluginError, PluginOptions, getDataModels, hasAttribute } from '@zenstackhq/sdk'; import { Model } from '@zenstackhq/sdk/ast'; import type { OpenAPIV3_1 as OAPI } from 'openapi-types'; +import semver from 'semver'; import { fromZodError } from 'zod-validation-error'; +import { name } from '.'; import { SecuritySchemesSchema } from './schema'; -import semver from 'semver'; export abstract class OpenAPIGeneratorBase { protected readonly DEFAULT_SPEC_VERSION = '3.1.0'; @@ -91,10 +92,7 @@ export abstract class OpenAPIGeneratorBase { if (securitySchemes) { const parsed = SecuritySchemesSchema.safeParse(securitySchemes); if (!parsed.success) { - throw new PluginError( - this.options.name, - `"securitySchemes" option is invalid: ${fromZodError(parsed.error)}` - ); + throw new PluginError(name, `"securitySchemes" option is invalid: ${fromZodError(parsed.error)}`); } return parsed.data; } diff --git a/packages/plugins/openapi/src/rpc-generator.ts b/packages/plugins/openapi/src/rpc-generator.ts index 13bb91272..c551a8aef 100644 --- a/packages/plugins/openapi/src/rpc-generator.ts +++ b/packages/plugins/openapi/src/rpc-generator.ts @@ -721,7 +721,7 @@ export class RPCOpenAPIGenerator extends OpenAPIGeneratorBase { return this.wrapArray(this.wrapNullable(this.ref(def.type, false), !def.isRequired), def.isList); default: - throw new PluginError(this.options.name, `Unsupported field kind: ${def.kind}`); + throw new PluginError(name, `Unsupported field kind: ${def.kind}`); } } diff --git a/packages/plugins/swr/src/generator.ts b/packages/plugins/swr/src/generator.ts index e074b603c..3a47a1c87 100644 --- a/packages/plugins/swr/src/generator.ts +++ b/packages/plugins/swr/src/generator.ts @@ -38,8 +38,6 @@ export async function generate(model: Model, options: PluginOptions, dmmf: DMMF. await generateModelMeta(project, models, { output: path.join(outDir, '__model_meta.ts'), - compile: false, - preserveTsFiles: true, generateAttributes: false, }); diff --git a/packages/plugins/tanstack-query/src/generator.ts b/packages/plugins/tanstack-query/src/generator.ts index 1e39253fc..9ad261c21 100644 --- a/packages/plugins/tanstack-query/src/generator.ts +++ b/packages/plugins/tanstack-query/src/generator.ts @@ -34,21 +34,16 @@ export async function generate(model: Model, options: PluginOptions, dmmf: DMMF. const target = requireOption(options, 'target', name); if (!supportedTargets.includes(target)) { - throw new PluginError( - options.name, - `Unsupported target "${target}", supported values: ${supportedTargets.join(', ')}` - ); + throw new PluginError(name, `Unsupported target "${target}", supported values: ${supportedTargets.join(', ')}`); } const version = typeof options.version === 'string' ? options.version : 'v4'; if (version !== 'v4' && version !== 'v5') { - throw new PluginError(options.name, `Unsupported version "${version}": use "v4" or "v5"`); + throw new PluginError(name, `Unsupported version "${version}": use "v4" or "v5"`); } await generateModelMeta(project, models, { output: path.join(outDir, '__model_meta.ts'), - compile: false, - preserveTsFiles: true, generateAttributes: false, }); diff --git a/packages/schema/src/cli/plugin-runner.ts b/packages/schema/src/cli/plugin-runner.ts index 2914ae43b..df0aa91fe 100644 --- a/packages/schema/src/cli/plugin-runner.ts +++ b/packages/schema/src/cli/plugin-runner.ts @@ -8,16 +8,17 @@ import { getLiteral, getLiteralArray, hasValidationAttributes, + OptionValue, + PluginDeclaredOptions, PluginError, PluginFunction, - PluginOptions, resolvePath, } from '@zenstackhq/sdk'; import colors from 'colors'; import fs from 'fs'; import ora from 'ora'; import path from 'path'; -import { ensureDefaultOutputFolder } from '../plugins/plugin-utils'; +import { CorePlugins, ensureDefaultOutputFolder } from '../plugins/plugin-utils'; import { getDefaultPrismaOutputFile } from '../plugins/prisma/schema-generator'; import telemetry from '../telemetry'; import { getVersion } from '../utils/version-utils'; @@ -26,7 +27,7 @@ type PluginInfo = { name: string; description?: string; provider: string; - options: PluginOptions; + options: PluginDeclaredOptions; run: PluginFunction; dependencies: string[]; module: any; @@ -47,16 +48,16 @@ export class PluginRunner { /** * Runs a series of nested generators */ - async run(options: PluginRunnerOptions): Promise { + async run(runnerOptions: PluginRunnerOptions): Promise { const version = getVersion(); console.log(colors.bold(`⌛️ ZenStack CLI v${version}, running plugins`)); - ensureDefaultOutputFolder(options); + ensureDefaultOutputFolder(runnerOptions); const plugins: PluginInfo[] = []; - const pluginDecls = options.schema.declarations.filter((d): d is Plugin => isPlugin(d)); + const pluginDecls = runnerOptions.schema.declarations.filter((d): d is Plugin => isPlugin(d)); - let prismaOutput = getDefaultPrismaOutputFile(options.schemaPath); + let prismaOutput = getDefaultPrismaOutputFile(runnerOptions.schemaPath); for (const pluginDecl of pluginDecls) { const pluginProvider = this.getPluginProvider(pluginDecl); @@ -69,7 +70,7 @@ export class PluginRunner { let pluginModule: any; try { - pluginModule = this.loadPluginModule(pluginProvider, options); + pluginModule = this.loadPluginModule(pluginProvider, runnerOptions.schemaPath); } catch (err) { console.error(`Unable to load plugin module ${pluginProvider}: ${err}`); throw new PluginError('', `Unable to load plugin module ${pluginProvider}`); @@ -82,7 +83,9 @@ export class PluginRunner { const dependencies = this.getPluginDependencies(pluginModule); const pluginName = this.getPluginName(pluginModule, pluginProvider); - const pluginOptions: PluginOptions = { schemaPath: options.schemaPath, name: pluginName }; + const pluginOptions: PluginDeclaredOptions = { + provider: pluginProvider, + }; pluginDecl.fields.forEach((f) => { const value = getLiteral(f.value) ?? getLiteralArray(f.value); @@ -104,39 +107,15 @@ export class PluginRunner { if (pluginProvider === '@core/prisma' && typeof pluginOptions.output === 'string') { // record custom prisma output path - prismaOutput = resolvePath(pluginOptions.output, pluginOptions); + prismaOutput = resolvePath(pluginOptions.output, { schemaPath: runnerOptions.schemaPath }); } } - // 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) { - // shift the plugin to the front - const existing = plugins[existingIdx]; - plugins.splice(existingIdx, 1); - plugins.unshift(existing); - } else { - // synthesize a plugin and insert front - const pluginModule = require(this.getPluginModulePath(corePlugin.provider, options)); - const pluginName = this.getPluginName(pluginModule, corePlugin.provider); - plugins.unshift({ - name: pluginName, - description: this.getPluginDescription(pluginModule), - provider: corePlugin.provider, - dependencies: [], - options: { schemaPath: options.schemaPath, name: pluginName, ...corePlugin.options }, - run: pluginModule.default, - module: pluginModule, - }); - } - } + // calculate all plugins (including core plugins implicitly enabled) + const allPlugins = this.calculateAllPlugins(runnerOptions, plugins); // check dependencies - for (const plugin of plugins) { + for (const plugin of allPlugins) { for (const dep of plugin.dependencies) { if (!plugins.find((p) => p.provider === dep)) { console.error(`Plugin ${plugin.provider} depends on "${dep}" but it's not declared`); @@ -148,7 +127,7 @@ export class PluginRunner { } } - if (plugins.length === 0) { + if (allPlugins.length === 0) { console.log(colors.yellow('No plugins configured.')); return; } @@ -156,9 +135,9 @@ export class PluginRunner { const warnings: string[] = []; let dmmf: DMMF.Document | undefined = undefined; - for (const { name, description, provider, run, options: pluginOptions } of plugins) { + for (const { name, description, provider, run, options: pluginOptions } of allPlugins) { // const start = Date.now(); - await this.runPlugin(name, description, run, options, pluginOptions, dmmf, warnings); + await this.runPlugin(name, description, run, runnerOptions, pluginOptions, dmmf, warnings); // console.log(`✅ Plugin ${colors.bold(name)} (${provider}) completed in ${Date.now() - start}ms`); if (provider === '@core/prisma') { // load prisma DMMF @@ -174,41 +153,47 @@ 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 }> = []; + private calculateAllPlugins(options: PluginRunnerOptions, plugins: PluginInfo[]) { + const corePlugins: PluginInfo[] = []; + let zodImplicitlyAdded = false; - 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' }); + // 1. @core/prisma + const existingPrisma = plugins.find((p) => p.provider === CorePlugins.Prisma); + if (existingPrisma) { + corePlugins.push(existingPrisma); + plugins.splice(plugins.indexOf(existingPrisma), 1); + } else if (options.defaultPlugins || plugins.some((p) => p.provider !== CorePlugins.Prisma)) { + // "@core/prisma" is enabled as default or if any other plugin is configured + corePlugins.push(this.makeCorePlugin(CorePlugins.Prisma, options.schemaPath, {})); } - // "@core/access-policy" has implicit requirements - let zodImplicitlyAdded = false; - 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)) { - zodImplicitlyAdded = true; - corePlugins.push({ provider: '@core/zod', options: { modelOnly: true } }); - } + // 2. @core/zod + const existingZod = plugins.find((p) => p.provider === CorePlugins.Zod); + if (existingZod) { + corePlugins.push(existingZod); + plugins.splice(plugins.indexOf(existingZod), 1); + } else if ( + (options.defaultPlugins || plugins.some((p) => p.provider === CorePlugins.Enhancer)) && + this.hasValidation(options.schema) + ) { + // "@core/zod" is enabled if "@core/enhancer" is enabled and there're validation rules + zodImplicitlyAdded = true; + corePlugins.push(this.makeCorePlugin(CorePlugins.Zod, options.schemaPath, { modelOnly: true })); } - if (options.defaultPlugins) { - corePlugins.push({ - provider: '@core/enhancer', - options: { withZodSchemas: corePlugins.some((p) => p.provider === '@core/zod') }, - }); + // 3. @core/enhancer + const existingEnhancer = plugins.find((p) => p.provider === CorePlugins.Enhancer); + if (existingEnhancer) { + corePlugins.push(existingEnhancer); + plugins.splice(plugins.indexOf(existingEnhancer), 1); + } else { + if (options.defaultPlugins) { + corePlugins.push( + this.makeCorePlugin(CorePlugins.Enhancer, options.schemaPath, { + withZodSchemas: corePlugins.some((p) => p.provider === CorePlugins.Zod), + }) + ); + } } // core plugins introduced by dependencies @@ -227,7 +212,9 @@ export class PluginRunner { if (existing.provider === '@core/zod') { // Zod plugin can be automatically enabled in `modelOnly` mode, however // other plugin (tRPC) for now requires it to run in full mode - existing.options = {}; + if (existing.options.modelOnly) { + delete existing.options.modelOnly; + } if ( isTrpcPlugin && @@ -239,21 +226,39 @@ export class PluginRunner { } } else { // add core dependency - const toAdd = { provider: dep, options: {} as Record }; + const depOptions: Record = {}; // TODO: generalize this if (dep === '@core/zod' && isTrpcPlugin) { // pass trpc plugin's `generateModels` option down to zod plugin - toAdd.options.generateModels = plugin.options.generateModels; + depOptions.generateModels = plugin.options.generateModels; } - corePlugins.push(toAdd); + corePlugins.push(this.makeCorePlugin(dep, options.schemaPath, depOptions)); } } } }); - return corePlugins; + return [...corePlugins, ...plugins]; + } + + private makeCorePlugin( + provider: string, + schemaPath: string, + options: Record + ): PluginInfo { + const pluginModule = require(this.getPluginModulePath(provider, schemaPath)); + const pluginName = this.getPluginName(pluginModule, provider); + return { + name: pluginName, + description: this.getPluginDescription(pluginModule), + provider: provider, + dependencies: [], + options: { ...options, provider }, + run: pluginModule.default, + module: pluginModule, + }; } private hasValidation(schema: Model) { @@ -284,7 +289,7 @@ export class PluginRunner { description: string | undefined, run: PluginFunction, runnerOptions: PluginRunnerOptions, - options: PluginOptions, + options: PluginDeclaredOptions, dmmf: DMMF.Document | undefined, warnings: string[] ) { @@ -300,7 +305,7 @@ export class PluginRunner { options, }, async () => { - let result = run(runnerOptions.schema, options, dmmf, { + let result = run(runnerOptions.schema, { ...options, schemaPath: runnerOptions.schemaPath }, dmmf, { output: runnerOptions.output, compile: runnerOptions.compile, }); @@ -319,7 +324,7 @@ export class PluginRunner { } } - private getPluginModulePath(provider: string, options: Pick) { + private getPluginModulePath(provider: string, schemaPath: string) { let pluginModulePath = provider; if (provider.startsWith('@core/')) { pluginModulePath = provider.replace(/^@core/, path.join(__dirname, '../plugins')); @@ -329,14 +334,14 @@ export class PluginRunner { require.resolve(pluginModulePath); } catch { // relative - pluginModulePath = resolvePath(provider, options); + pluginModulePath = resolvePath(provider, { schemaPath }); } } return pluginModulePath; } - private loadPluginModule(provider: string, options: Pick) { - const pluginModulePath = this.getPluginModulePath(provider, options); + private loadPluginModule(provider: string, schemaPath: string) { + const pluginModulePath = this.getPluginModulePath(provider, schemaPath); return require(pluginModulePath); } } diff --git a/packages/schema/src/plugins/access-policy/index.ts b/packages/schema/src/plugins/access-policy/index.ts deleted file mode 100644 index cbdcbd64f..000000000 --- a/packages/schema/src/plugins/access-policy/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { PluginFunction } from '@zenstackhq/sdk'; -import PolicyGenerator from './policy-guard-generator'; - -export const name = 'Access Policy'; - -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/enhancer/enhancer.ts b/packages/schema/src/plugins/enhancer/enhancer.ts new file mode 100644 index 000000000..09360254b --- /dev/null +++ b/packages/schema/src/plugins/enhancer/enhancer.ts @@ -0,0 +1,29 @@ +import { getPrismaClientImportSpec, type PluginOptions } from '@zenstackhq/sdk'; +import type { Model } from '@zenstackhq/sdk/ast'; +import path from 'path'; +import type { Project } from 'ts-morph'; + +export async function generate(model: Model, options: PluginOptions, project: Project, outDir: string) { + const outFile = path.join(outDir, 'enhance.ts'); + + project.createSourceFile( + outFile, + `import { createEnhancement, type WithPolicyContext, type EnhancementOptions, type ZodSchemas } from '@zenstackhq/runtime'; +import modelMeta from './model-meta'; +import policy from './policy'; +${options.withZodSchemas ? "import * as zodSchemas from './zod';" : 'const zodSchemas = undefined;'} +import { Prisma } from '${getPrismaClientImportSpec(model, outDir)}'; + +export function enhance(prisma: DbClient, context?: WithPolicyContext, options?: EnhancementOptions): DbClient { + return createEnhancement(prisma, { + modelMeta, + policy, + zodSchemas: zodSchemas as unknown as (ZodSchemas | undefined), + prismaModule: Prisma, + ...options + }, context); +} +`, + { overwrite: true } + ); +} diff --git a/packages/schema/src/plugins/enhancer/index.ts b/packages/schema/src/plugins/enhancer/index.ts index f18418f9c..45f3ceb35 100644 --- a/packages/schema/src/plugins/enhancer/index.ts +++ b/packages/schema/src/plugins/enhancer/index.ts @@ -2,26 +2,31 @@ import { PluginError, createProject, emitProject, - getPrismaClientImportSpec, resolvePath, saveProject, type PluginFunction, } from '@zenstackhq/sdk'; -import path from 'path'; import { getDefaultOutputFolder } from '../plugin-utils'; +import { generate as generateEnhancer } from './enhancer'; +import { generate as generateModelMeta } from './model-meta'; +import { generate as generatePolicy } from './policy'; export const name = 'Prisma Enhancer'; +export const description = 'Generating PrismaClient enhancer'; -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`); +const run: PluginFunction = async (model, options, _dmmf, globalOptions) => { + let ourDir = options.output ? (options.output as string) : getDefaultOutputFolder(globalOptions); + if (!ourDir) { + throw new PluginError(name, `Unable to determine output path, not running plugin`); } + ourDir = resolvePath(ourDir, options); - output = resolvePath(output, options); - const outFile = path.join(output, 'enhance.ts'); const project = createProject(); + await generateModelMeta(model, options, project, ourDir); + await generatePolicy(model, options, project, ourDir); + await generateEnhancer(model, options, project, ourDir); + let shouldCompile = true; if (typeof options.compile === 'boolean') { // explicit override @@ -31,27 +36,6 @@ const run: PluginFunction = async (_model, options, _dmmf, globalOptions) => { shouldCompile = globalOptions.compile; } - project.createSourceFile( - outFile, - `import { createEnhancement, type WithPolicyContext, type EnhancementOptions, type ZodSchemas } from '@zenstackhq/runtime'; -import modelMeta from './model-meta'; -import policy from './policy'; -${options.withZodSchemas ? "import * as zodSchemas from './zod';" : 'const zodSchemas = undefined;'} -import { Prisma } from '${getPrismaClientImportSpec(_model, output)}'; - -export function enhance(prisma: DbClient, context?: WithPolicyContext, options?: EnhancementOptions): DbClient { - return createEnhancement(prisma, { - modelMeta, - policy, - zodSchemas: zodSchemas as unknown as (ZodSchemas | undefined), - prismaModule: Prisma, - ...options - }, context); -} -`, - { overwrite: true } - ); - if (!shouldCompile || options.preserveTsFiles === true) { await saveProject(project); } diff --git a/packages/schema/src/plugins/enhancer/model-meta.ts b/packages/schema/src/plugins/enhancer/model-meta.ts new file mode 100644 index 000000000..541106e24 --- /dev/null +++ b/packages/schema/src/plugins/enhancer/model-meta.ts @@ -0,0 +1,13 @@ +import { generateModelMeta, getDataModels, type PluginOptions } from '@zenstackhq/sdk'; +import type { Model } from '@zenstackhq/sdk/ast'; +import path from 'path'; +import type { Project } from 'ts-morph'; + +export async function generate(model: Model, options: PluginOptions, project: Project, outDir: string) { + const outFile = path.join(outDir, 'model-meta.ts'); + const dataModels = getDataModels(model); + await generateModelMeta(project, dataModels, { + output: outFile, + generateAttributes: true, + }); +} diff --git a/packages/schema/src/plugins/access-policy/expression-writer.ts b/packages/schema/src/plugins/enhancer/policy/expression-writer.ts similarity index 99% rename from packages/schema/src/plugins/access-policy/expression-writer.ts rename to packages/schema/src/plugins/enhancer/policy/expression-writer.ts index 2ab3e2bdd..0cc80c7ea 100644 --- a/packages/schema/src/plugins/access-policy/expression-writer.ts +++ b/packages/schema/src/plugins/enhancer/policy/expression-writer.ts @@ -26,12 +26,12 @@ import { } from '@zenstackhq/sdk'; import { lowerCaseFirst } from 'lower-case-first'; import { CodeBlockWriter } from 'ts-morph'; -import { name } from '.'; -import { getIdFields, isAuthInvocation } from '../../utils/ast-utils'; +import { name } from '..'; +import { getIdFields, isAuthInvocation } from '../../../utils/ast-utils'; import { TypeScriptExpressionTransformer, TypeScriptExpressionTransformerError, -} from '../../utils/typescript-expression-transformer'; +} from '../../../utils/typescript-expression-transformer'; type ComparisonOperator = '==' | '!=' | '>' | '>=' | '<' | '<='; diff --git a/packages/schema/src/plugins/enhancer/policy/index.ts b/packages/schema/src/plugins/enhancer/policy/index.ts new file mode 100644 index 000000000..8eaf1d00b --- /dev/null +++ b/packages/schema/src/plugins/enhancer/policy/index.ts @@ -0,0 +1,8 @@ +import { type PluginOptions } from '@zenstackhq/sdk'; +import type { Model } from '@zenstackhq/sdk/ast'; +import type { Project } from 'ts-morph'; +import { PolicyGenerator } from './policy-guard-generator'; + +export async function generate(model: Model, options: PluginOptions, project: Project, outDir: string) { + return new PolicyGenerator().generate(project, model, options, outDir); +} diff --git a/packages/schema/src/plugins/access-policy/policy-guard-generator.ts b/packages/schema/src/plugins/enhancer/policy/policy-guard-generator.ts similarity index 96% rename from packages/schema/src/plugins/access-policy/policy-guard-generator.ts rename to packages/schema/src/plugins/enhancer/policy/policy-guard-generator.ts index 2025c3d5c..e5017383d 100644 --- a/packages/schema/src/plugins/access-policy/policy-guard-generator.ts +++ b/packages/schema/src/plugins/enhancer/policy/policy-guard-generator.ts @@ -31,12 +31,9 @@ import { import { ExpressionContext, PluginError, - PluginGlobalOptions, PluginOptions, RUNTIME_PACKAGE, analyzePolicies, - createProject, - emitProject, getAttributeArg, getAuthModel, getDataModels, @@ -48,35 +45,26 @@ import { isForeignKeyField, isFromStdlib, isFutureExpr, - resolvePath, resolved, - saveProject, } from '@zenstackhq/sdk'; import { streamAllContents, streamAst, streamContents } from 'langium'; import { lowerCaseFirst } from 'lower-case-first'; import path from 'path'; -import { FunctionDeclaration, SourceFile, VariableDeclarationKind, WriterFunction } from 'ts-morph'; -import { name } from '.'; -import { getIdFields, isAuthInvocation, isCollectionPredicate } from '../../utils/ast-utils'; +import { FunctionDeclaration, Project, SourceFile, VariableDeclarationKind, WriterFunction } from 'ts-morph'; +import { name } from '..'; +import { getIdFields, isAuthInvocation, isCollectionPredicate } from '../../../utils/ast-utils'; import { TypeScriptExpressionTransformer, TypeScriptExpressionTransformerError, -} from '../../utils/typescript-expression-transformer'; -import { ALL_OPERATION_KINDS, getDefaultOutputFolder } from '../plugin-utils'; +} from '../../../utils/typescript-expression-transformer'; +import { ALL_OPERATION_KINDS } from '../../plugin-utils'; 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, 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`); - } - output = resolvePath(output, options); - - const project = createProject(); +export class PolicyGenerator { + async generate(project: Project, model: Model, _options: PluginOptions, output: string) { const sf = project.createSourceFile(path.join(output, 'policy.ts'), undefined, { overwrite: true }); sf.addStatements('/* eslint-disable */'); @@ -156,22 +144,6 @@ export default class PolicyGenerator { }); sf.addStatements('export default policy'); - - 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); - } - if (shouldCompile) { - await emitProject(project); - } } // Generates a { select: ... } object to select `auth()` fields used in policy rules diff --git a/packages/schema/src/plugins/model-meta/index.ts b/packages/schema/src/plugins/model-meta/index.ts deleted file mode 100644 index 8d7454674..000000000 --- a/packages/schema/src/plugins/model-meta/index.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { - createProject, - generateModelMeta, - getDataModels, - PluginError, - PluginFunction, - resolvePath, -} from '@zenstackhq/sdk'; -import path from 'path'; -import { getDefaultOutputFolder } from '../plugin-utils'; - -export const name = 'Model Metadata'; - -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`); - } - - output = resolvePath(output, options); - const outFile = path.join(output, 'model-meta.ts'); - const dataModels = getDataModels(model); - const project = createProject(); - - let shouldCompile = true; - if (typeof options.compile === 'boolean') { - // explicit override - shouldCompile = options.compile; - } else if (globalOptions) { - // from CLI or config file - shouldCompile = globalOptions.compile; - } - - await generateModelMeta(project, dataModels, { - output: outFile, - compile: shouldCompile, - preserveTsFiles: options.preserveTsFiles === true, - generateAttributes: true, - }); -}; - -export default run; diff --git a/packages/schema/src/plugins/plugin-utils.ts b/packages/schema/src/plugins/plugin-utils.ts index 4b1be34ff..f4f521fdc 100644 --- a/packages/schema/src/plugins/plugin-utils.ts +++ b/packages/schema/src/plugins/plugin-utils.ts @@ -95,3 +95,12 @@ export function getDefaultOutputFolder(globalOptions?: PluginGlobalOptions) { const modulesFolder = getNodeModulesFolder(runtimeModulePath); return modulesFolder ? path.join(modulesFolder, DEFAULT_RUNTIME_LOAD_PATH) : undefined; } + +/** + * Core plugin providers + */ +export enum CorePlugins { + Prisma = '@core/prisma', + Zod = '@core/zod', + Enhancer = '@core/enhancer', +} diff --git a/packages/schema/src/plugins/zod/generator.ts b/packages/schema/src/plugins/zod/generator.ts index 2727a781f..19ce18a3b 100644 --- a/packages/schema/src/plugins/zod/generator.ts +++ b/packages/schema/src/plugins/zod/generator.ts @@ -26,7 +26,7 @@ import { name } from '.'; import { getDefaultOutputFolder } from '../plugin-utils'; import Transformer from './transformer'; import removeDir from './utils/removeDir'; -import { makeFieldSchema, makeValidationRefinements, getFieldSchemaDefault } from './utils/schema-gen'; +import { getFieldSchemaDefault, makeFieldSchema, makeValidationRefinements } from './utils/schema-gen'; export async function generate( model: Model, diff --git a/packages/sdk/src/model-meta-generator.ts b/packages/sdk/src/model-meta-generator.ts index 41a0ea0c9..da0ba96dd 100644 --- a/packages/sdk/src/model-meta-generator.ts +++ b/packages/sdk/src/model-meta-generator.ts @@ -14,7 +14,6 @@ import type { RuntimeAttribute } from '@zenstackhq/runtime'; import { lowerCaseFirst } from 'lower-case-first'; import { CodeBlockWriter, Project, VariableDeclarationKind } from 'ts-morph'; import { - emitProject, getAttribute, getAttributeArg, getAttributeArgs, @@ -26,13 +25,10 @@ import { isForeignKeyField, isIdField, resolved, - saveProject, } from '.'; export type ModelMetaGeneratorOptions = { output: string; - compile: boolean; - preserveTsFiles: boolean; generateAttributes: boolean; }; @@ -44,14 +40,7 @@ export async function generate(project: Project, models: DataModel[], options: M declarations: [{ name: 'metadata', initializer: (writer) => generateModelMetadata(models, writer, options) }], }); sf.addStatements('export default metadata;'); - - if (!options.compile || options.preserveTsFiles) { - // save ts files - await saveProject(project); - } - if (options.compile) { - await emitProject(project); - } + return sf; } function generateModelMetadata(dataModels: DataModel[], writer: CodeBlockWriter, options: ModelMetaGeneratorOptions) { diff --git a/packages/sdk/src/types.ts b/packages/sdk/src/types.ts index c19fdfc42..9fbbd5553 100644 --- a/packages/sdk/src/types.ts +++ b/packages/sdk/src/types.ts @@ -9,23 +9,18 @@ export type OptionValue = string | number | boolean; /** * Plugin configuration options */ -export type PluginOptions = { +export type PluginDeclaredOptions = { /*** * The provider package */ - provider?: string; - - /** - * The path of the ZModel schema - */ - schemaPath: string; - - /** - * The name of the plugin - */ - name: string; + provider: string; } & Record; +/** + * Plugin configuration options for execution + */ +export type PluginOptions = { schemaPath: string } & PluginDeclaredOptions; + /** * Global options that apply to all plugins */ diff --git a/packages/sdk/src/utils.ts b/packages/sdk/src/utils.ts index abec6092b..a4b5039b5 100644 --- a/packages/sdk/src/utils.ts +++ b/packages/sdk/src/utils.ts @@ -29,7 +29,7 @@ import { } from '@zenstackhq/language/ast'; import path from 'path'; import { ExpressionContext, STD_LIB_MODULE_NAME } from './constants'; -import { PluginError, PluginOptions } from './types'; +import { PluginDeclaredOptions, PluginError, PluginOptions } from './types'; /** * Gets data models that are not ignored @@ -280,7 +280,7 @@ export function resolvePath(_path: string, options: Pick(options: PluginOptions, name: string, pluginName: string): T { +export function requireOption(options: PluginDeclaredOptions, name: string, pluginName: string): T { const value = options[name]; if (value === undefined) { throw new PluginError(pluginName, `Plugin "${options.name}" is missing required option: ${name}`); @@ -288,8 +288,8 @@ export function requireOption(options: PluginOptions, name: string, pluginNam return value as T; } -export function parseOptionAsStrings(options: PluginOptions, optionaName: string, pluginName: string) { - const value = options[optionaName]; +export function parseOptionAsStrings(options: PluginDeclaredOptions, optionName: string, pluginName: string) { + const value = options[optionName]; if (value === undefined) { return undefined; } else if (typeof value === 'string') { @@ -304,7 +304,7 @@ export function parseOptionAsStrings(options: PluginOptions, optionaName: string } else { throw new PluginError( pluginName, - `Invalid "${optionaName}" option: must be a comma-separated string or an array of strings` + `Invalid "${optionName}" option: must be a comma-separated string or an array of strings` ); } } diff --git a/packages/testtools/src/schema.ts b/packages/testtools/src/schema.ts index 52d0e2376..f152d6e57 100644 --- a/packages/testtools/src/schema.ts +++ b/packages/testtools/src/schema.ts @@ -321,7 +321,12 @@ export async function loadZModelAndDmmf( const model = await loadDocument(modelFile); const { name: prismaFile } = tmp.fileSync({ postfix: '.prisma' }); - await prismaPlugin(model, { schemaPath: modelFile, name: 'Prisma', output: prismaFile, generateClient: false }); + await prismaPlugin(model, { + provider: '@core/plugin', + schemaPath: modelFile, + output: prismaFile, + generateClient: false, + }); const prismaContent = fs.readFileSync(prismaFile, { encoding: 'utf-8' }); From 59413ad6ffb2a7e4f318490ef3c982ba3a409a26 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Tue, 9 Jan 2024 14:56:50 +0800 Subject: [PATCH 2/5] fix test --- packages/schema/tests/generator/expression-writer.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/schema/tests/generator/expression-writer.test.ts b/packages/schema/tests/generator/expression-writer.test.ts index f9baa0de9..a4cc6ae5f 100644 --- a/packages/schema/tests/generator/expression-writer.test.ts +++ b/packages/schema/tests/generator/expression-writer.test.ts @@ -3,7 +3,7 @@ import { DataModel, Enum, Expression, isDataModel, isEnum } from '@zenstackhq/language/ast'; import * as tmp from 'tmp'; import { Project, VariableDeclarationKind } from 'ts-morph'; -import { ExpressionWriter } from '../../src/plugins/access-policy/expression-writer'; +import { ExpressionWriter } from '../../src/plugins/enhancer/policy/expression-writer'; import { loadModel } from '../utils'; describe('Expression Writer Tests', () => { From 5a3f66d6f3fffd7a90a900cc12352a380e0fc67f Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Tue, 9 Jan 2024 15:12:20 +0800 Subject: [PATCH 3/5] fix tests --- CONTRIBUTING.md | 2 +- packages/schema/tests/schema/cal-com.zmodel | 9 ++------- packages/testtools/src/schema.ts | 9 ++------- tests/integration/tests/cli/generate.test.ts | 8 ++++---- tests/integration/tests/cli/plugins.test.ts | 8 ++------ 5 files changed, 11 insertions(+), 25 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f1f733983..2b16adebf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -63,7 +63,7 @@ The ZModel language's definition, including its syntax definition and parser/lin ### `schema` -The `zenstack` CLI and ZModel VSCode extension implementation. The package also contains several built-in plugins: `@core/prisma`, `@core/model-meta`, `@core/access-policy`, and `core/zod`. +The `zenstack` CLI and ZModel VSCode extension implementation. The package also contains several built-in plugins: `@core/prisma`, `@core/enhancer`, and `core/zod`. ### `runtime` diff --git a/packages/schema/tests/schema/cal-com.zmodel b/packages/schema/tests/schema/cal-com.zmodel index c6e874304..a32bd45a6 100644 --- a/packages/schema/tests/schema/cal-com.zmodel +++ b/packages/schema/tests/schema/cal-com.zmodel @@ -11,13 +11,8 @@ generator client { previewFeatures = [] } -plugin meta { - provider = '@core/model-meta' - output = '.zenstack' -} - -plugin policy { - provider = '@core/access-policy' +plugin enhancer { + provider = '@core/enhancer' output = '.zenstack' } diff --git a/packages/testtools/src/schema.ts b/packages/testtools/src/schema.ts index f152d6e57..fd7df30b7 100644 --- a/packages/testtools/src/schema.ts +++ b/packages/testtools/src/schema.ts @@ -84,13 +84,8 @@ generator js { previewFeatures = ['clientExtensions'] } -plugin meta { - provider = '@core/model-meta' - preserveTsFiles = true -} - -plugin policy { - provider = '@core/access-policy' +plugin enhancer { + provider = '@core/enhancer' preserveTsFiles = true } diff --git a/tests/integration/tests/cli/generate.test.ts b/tests/integration/tests/cli/generate.test.ts index c8e1b0e6b..85fe76a53 100644 --- a/tests/integration/tests/cli/generate.test.ts +++ b/tests/integration/tests/cli/generate.test.ts @@ -116,8 +116,8 @@ model Post { fs.appendFileSync( 'schema.zmodel', ` - plugin policy { - provider = '@core/access-policy' + plugin enhancer { + provider = '@core/enhancer' } ` ); @@ -133,8 +133,8 @@ model Post { fs.appendFileSync( 'schema.zmodel', ` - plugin policy { - provider = '@core/access-policy' + plugin enhancer { + provider = '@core/enhancer' } ` ); diff --git a/tests/integration/tests/cli/plugins.test.ts b/tests/integration/tests/cli/plugins.test.ts index 4f93106e8..19dfb4dce 100644 --- a/tests/integration/tests/cli/plugins.test.ts +++ b/tests/integration/tests/cli/plugins.test.ts @@ -129,12 +129,8 @@ describe('CLI Plugins Tests', () => { output = 'prisma/my.prisma' generateClient = true }`, - `plugin meta { - provider = '@core/model-meta' - } - `, - `plugin policy { - provider = '@core/access-policy' + `plugin enhancer { + provider = '@core/enhancer' }`, `plugin tanstack { provider = '@zenstackhq/tanstack-query' From c6a224cf523669d376f0143821169b9cd6669039 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Tue, 9 Jan 2024 16:09:23 +0800 Subject: [PATCH 4/5] fix tests --- packages/plugins/swr/tests/swr.test.ts | 7 +++++- .../tanstack-query/tests/plugin.test.ts | 12 ++++----- packages/plugins/trpc/tests/trpc.test.ts | 25 +++++++++++-------- packages/schema/src/cli/plugin-runner.ts | 4 +-- 4 files changed, 29 insertions(+), 19 deletions(-) diff --git a/packages/plugins/swr/tests/swr.test.ts b/packages/plugins/swr/tests/swr.test.ts index 76db29b49..9759aee2d 100644 --- a/packages/plugins/swr/tests/swr.test.ts +++ b/packages/plugins/swr/tests/swr.test.ts @@ -59,7 +59,12 @@ ${sharedModel} { provider: 'postgresql', pushDb: false, - extraDependencies: [`${origDir}/dist`, 'react@18.2.0', '@types/react@18.2.0', 'swr@^2'], + extraDependencies: [ + path.resolve(__dirname, '../dist'), + 'react@18.2.0', + '@types/react@18.2.0', + 'swr@^2', + ], compile: true, } ); diff --git a/packages/plugins/tanstack-query/tests/plugin.test.ts b/packages/plugins/tanstack-query/tests/plugin.test.ts index 49a99df94..c87e2a38f 100644 --- a/packages/plugins/tanstack-query/tests/plugin.test.ts +++ b/packages/plugins/tanstack-query/tests/plugin.test.ts @@ -61,7 +61,7 @@ ${sharedModel} provider: 'postgresql', pushDb: false, extraDependencies: ['react@18.2.0', '@types/react@18.2.0', '@tanstack/react-query@4.29.7'], - copyDependencies: [`${origDir}/dist`], + copyDependencies: [path.resolve(__dirname, '../dist')], compile: true, } ); @@ -83,7 +83,7 @@ ${sharedModel} provider: 'postgresql', pushDb: false, extraDependencies: ['react@18.2.0', '@types/react@18.2.0', '@tanstack/react-query@^5.0.0'], - copyDependencies: [`${origDir}/dist`], + copyDependencies: [path.resolve(__dirname, '../dist')], compile: true, } ); @@ -104,7 +104,7 @@ ${sharedModel} provider: 'postgresql', pushDb: false, extraDependencies: ['vue@^3.3.4', '@tanstack/vue-query@4.37.0'], - copyDependencies: [`${origDir}/dist`], + copyDependencies: [path.resolve(__dirname, '../dist')], compile: true, } ); @@ -126,7 +126,7 @@ ${sharedModel} provider: 'postgresql', pushDb: false, extraDependencies: ['vue@^3.3.4', '@tanstack/vue-query@latest'], - copyDependencies: [`${origDir}/dist`], + copyDependencies: [path.resolve(__dirname, '../dist')], compile: true, } ); @@ -147,7 +147,7 @@ ${sharedModel} provider: 'postgresql', pushDb: false, extraDependencies: ['svelte@^3.0.0', '@tanstack/svelte-query@4.29.7'], - copyDependencies: [`${origDir}/dist`], + copyDependencies: [path.resolve(__dirname, '../dist')], compile: true, } ); @@ -169,7 +169,7 @@ ${sharedModel} provider: 'postgresql', pushDb: false, extraDependencies: ['svelte@^3.0.0', '@tanstack/svelte-query@^5.0.0'], - copyDependencies: [`${origDir}/dist`], + copyDependencies: [path.resolve(__dirname, '../dist')], compile: true, } ); diff --git a/packages/plugins/trpc/tests/trpc.test.ts b/packages/plugins/trpc/tests/trpc.test.ts index cf43c9a49..757e7e182 100644 --- a/packages/plugins/trpc/tests/trpc.test.ts +++ b/packages/plugins/trpc/tests/trpc.test.ts @@ -56,7 +56,7 @@ model Foo { { provider: 'postgresql', pushDb: false, - extraDependencies: [`${origDir}/dist`, '@trpc/client', '@trpc/server'], + extraDependencies: [path.resolve(__dirname, '../dist'), '@trpc/client', '@trpc/server'], compile: true, fullZod: true, } @@ -98,7 +98,7 @@ model Foo { `, { pushDb: false, - extraDependencies: [`${origDir}/dist`, '@trpc/client', '@trpc/server'], + extraDependencies: [path.resolve(__dirname, '../dist'), '@trpc/client', '@trpc/server'], compile: true, fullZod: true, } @@ -128,7 +128,7 @@ model Post { `, { pushDb: false, - extraDependencies: [`${origDir}/dist`, '@trpc/client', '@trpc/server'], + extraDependencies: [path.resolve(__dirname, '../dist'), '@trpc/client', '@trpc/server'], compile: true, fullZod: true, customSchemaFilePath: 'zenstack/schema.zmodel', @@ -153,7 +153,7 @@ model Post { `, { pushDb: false, - extraDependencies: [`${origDir}/dist`, '@trpc/client', '@trpc/server'], + extraDependencies: [path.resolve(__dirname, '../dist'), '@trpc/client', '@trpc/server'], compile: true, fullZod: true, customSchemaFilePath: 'zenstack/schema.zmodel', @@ -183,7 +183,7 @@ model Post { `, { pushDb: false, - extraDependencies: [`${origDir}/dist`, '@trpc/client', '@trpc/server'], + extraDependencies: [path.resolve(__dirname, '../dist'), '@trpc/client', '@trpc/server'], compile: true, fullZod: true, customSchemaFilePath: 'zenstack/schema.zmodel', @@ -229,7 +229,12 @@ model Post { `, { pushDb: false, - extraDependencies: [`${origDir}/dist`, '@trpc/client', '@trpc/server', '@trpc/react-query'], + extraDependencies: [ + path.resolve(__dirname, '../dist'), + '@trpc/client', + '@trpc/server', + '@trpc/react-query', + ], compile: true, fullZod: true, } @@ -249,7 +254,7 @@ model Post { `, { pushDb: false, - extraDependencies: [`${origDir}/dist`, '@trpc/client', '@trpc/server', '@trpc/next'], + extraDependencies: [path.resolve(__dirname, '../dist'), '@trpc/client', '@trpc/server', '@trpc/next'], compile: true, fullZod: true, } @@ -279,7 +284,7 @@ model post_item { `, { pushDb: false, - extraDependencies: [`${origDir}/dist`, '@trpc/client', '@trpc/server'], + extraDependencies: [path.resolve(__dirname, '../dist'), '@trpc/client', '@trpc/server'], compile: true, fullZod: true, } @@ -326,7 +331,7 @@ model Foo { { addPrelude: false, pushDb: false, - extraDependencies: [`${origDir}/dist`, '@trpc/client', '@trpc/server'], + extraDependencies: [path.resolve(__dirname, '../dist'), '@trpc/client', '@trpc/server'], compile: true, } ); @@ -397,7 +402,7 @@ model Foo { { addPrelude: false, pushDb: false, - extraDependencies: [`${origDir}/dist`, '@trpc/client', '@trpc/server'], + extraDependencies: [path.resolve(__dirname, '../dist'), '@trpc/client', '@trpc/server'], compile: true, } ); diff --git a/packages/schema/src/cli/plugin-runner.ts b/packages/schema/src/cli/plugin-runner.ts index df0aa91fe..d2f7dcd18 100644 --- a/packages/schema/src/cli/plugin-runner.ts +++ b/packages/schema/src/cli/plugin-runner.ts @@ -117,7 +117,7 @@ export class PluginRunner { // check dependencies for (const plugin of allPlugins) { for (const dep of plugin.dependencies) { - if (!plugins.find((p) => p.provider === dep)) { + if (!allPlugins.find((p) => p.provider === dep)) { console.error(`Plugin ${plugin.provider} depends on "${dep}" but it's not declared`); throw new PluginError( plugin.name, @@ -196,7 +196,7 @@ export class PluginRunner { } } - // core plugins introduced by dependencies + // collect core plugins introduced by dependencies plugins.forEach((plugin) => { // TODO: generalize this const isTrpcPlugin = From e287798d054da1bf2e48e1e6630b115a95689286 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Tue, 9 Jan 2024 17:40:37 +0800 Subject: [PATCH 5/5] fix zod plugin issue --- packages/schema/src/cli/plugin-runner.ts | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/packages/schema/src/cli/plugin-runner.ts b/packages/schema/src/cli/plugin-runner.ts index d2f7dcd18..3e73932a1 100644 --- a/packages/schema/src/cli/plugin-runner.ts +++ b/packages/schema/src/cli/plugin-runner.ts @@ -82,7 +82,6 @@ export class PluginRunner { } const dependencies = this.getPluginDependencies(pluginModule); - const pluginName = this.getPluginName(pluginModule, pluginProvider); const pluginOptions: PluginDeclaredOptions = { provider: pluginProvider, }; @@ -90,13 +89,13 @@ export class PluginRunner { 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}`); + throw new PluginError(pluginDecl.name, `Invalid option value for ${f.name}`); } pluginOptions[f.name] = value; }); plugins.push({ - name: pluginName, + name: pluginDecl.name, description: this.getPluginDescription(pluginModule), provider: pluginProvider, dependencies, @@ -167,16 +166,22 @@ export class PluginRunner { corePlugins.push(this.makeCorePlugin(CorePlugins.Prisma, options.schemaPath, {})); } + const hasValidation = this.hasValidation(options.schema); + // 2. @core/zod const existingZod = plugins.find((p) => p.provider === CorePlugins.Zod); - if (existingZod) { - corePlugins.push(existingZod); + if (existingZod && !existingZod.options.output) { + // we can reuse the user-provided zod plugin if it didn't specify a custom output path plugins.splice(plugins.indexOf(existingZod), 1); - } else if ( + corePlugins.push(existingZod); + } + + if ( + !corePlugins.some((p) => p.provider === CorePlugins.Zod) && (options.defaultPlugins || plugins.some((p) => p.provider === CorePlugins.Enhancer)) && - this.hasValidation(options.schema) + hasValidation ) { - // "@core/zod" is enabled if "@core/enhancer" is enabled and there're validation rules + // ensure "@core/zod" is enabled if "@core/enhancer" is enabled and there're validation rules zodImplicitlyAdded = true; corePlugins.push(this.makeCorePlugin(CorePlugins.Zod, options.schemaPath, { modelOnly: true })); } @@ -190,7 +195,7 @@ export class PluginRunner { if (options.defaultPlugins) { corePlugins.push( this.makeCorePlugin(CorePlugins.Enhancer, options.schemaPath, { - withZodSchemas: corePlugins.some((p) => p.provider === CorePlugins.Zod), + withZodSchemas: hasValidation, }) ); }