From ad48fcd80e56cc96285fd86ee62847eac4c69f69 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Sat, 20 May 2023 20:21:38 +0900 Subject: [PATCH 1/3] fix: canonicalize plugin's output folder detection; don't generate aux field unnecessarily --- .../plugins/openapi/src/generator-base.ts | 7 ++- .../plugins/openapi/src/rest-generator.ts | 9 ++- packages/plugins/openapi/src/rpc-generator.ts | 10 ++-- packages/plugins/react/src/generator/index.ts | 2 +- .../react/src/generator/react-query.ts | 14 ++--- packages/plugins/react/src/generator/swr.ts | 14 ++--- packages/plugins/swr/src/generator.ts | 14 ++--- .../plugins/tanstack-query/src/generator.ts | 44 ++++++++------- packages/plugins/trpc/src/generator.ts | 22 ++++---- packages/plugins/trpc/src/zod/generator.ts | 6 +- .../src/enhancements/policy/handler.ts | 25 +++++++++ packages/schema/src/cli/cli-util.ts | 2 +- packages/schema/src/cli/plugin-runner.ts | 39 +++++++------ .../access-policy/expression-writer.ts | 21 +++---- .../access-policy/policy-guard-generator.ts | 13 +++-- .../typescript-expression-transformer.ts | 17 +++--- .../schema/src/plugins/model-meta/index.ts | 14 ++--- packages/schema/src/plugins/plugin-utils.ts | 18 ------ .../src/plugins/prisma/schema-generator.ts | 55 ++++++++++--------- packages/schema/src/plugins/zod/generator.ts | 3 +- .../tests/generator/prisma-generator.test.ts | 12 +++- packages/sdk/src/code-gen.ts | 4 +- packages/sdk/src/types.ts | 7 ++- packages/sdk/src/utils.ts | 18 ++++++ packages/testtools/src/schema.ts | 2 +- 25 files changed, 213 insertions(+), 179 deletions(-) diff --git a/packages/plugins/openapi/src/generator-base.ts b/packages/plugins/openapi/src/generator-base.ts index 6d50a6a29..e348af0aa 100644 --- a/packages/plugins/openapi/src/generator-base.ts +++ b/packages/plugins/openapi/src/generator-base.ts @@ -2,8 +2,8 @@ import { 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 { SecuritySchemesSchema } from './schema'; import { fromZodError } from 'zod-validation-error'; +import { SecuritySchemesSchema } from './schema'; export abstract class OpenAPIGeneratorBase { constructor(protected model: Model, protected options: PluginOptions, protected dmmf: DMMF.Document) {} @@ -51,7 +51,10 @@ export abstract class OpenAPIGeneratorBase { if (securitySchemes) { const parsed = SecuritySchemesSchema.safeParse(securitySchemes); if (!parsed.success) { - throw new PluginError(`"securitySchemes" option is invalid: ${fromZodError(parsed.error)}`); + throw new PluginError( + this.options.name, + `"securitySchemes" option is invalid: ${fromZodError(parsed.error)}` + ); } return parsed.data; } diff --git a/packages/plugins/openapi/src/rest-generator.ts b/packages/plugins/openapi/src/rest-generator.ts index 9c4dc3ba0..1b1140705 100644 --- a/packages/plugins/openapi/src/rest-generator.ts +++ b/packages/plugins/openapi/src/rest-generator.ts @@ -3,12 +3,13 @@ import { DMMF } from '@prisma/generator-helper'; import { AUXILIARY_FIELDS, - PluginError, analyzePolicies, getDataModels, isForeignKeyField, isIdField, isRelationshipField, + requireOption, + resolvePath, } from '@zenstackhq/sdk'; import { DataModel, DataModelField, DataModelFieldType, Enum, isDataModel, isEnum } from '@zenstackhq/sdk/ast'; import * as fs from 'fs'; @@ -30,10 +31,8 @@ export class RESTfulOpenAPIGenerator extends OpenAPIGeneratorBase { private warnings: string[] = []; generate() { - const output = this.getOption('output', ''); - if (!output) { - throw new PluginError('"output" option is required'); - } + let output = requireOption(this.options, 'output'); + output = resolvePath(output, this.options); const components = this.generateComponents(); const paths = this.generatePaths(); diff --git a/packages/plugins/openapi/src/rpc-generator.ts b/packages/plugins/openapi/src/rpc-generator.ts index 1084f8059..b3e63d690 100644 --- a/packages/plugins/openapi/src/rpc-generator.ts +++ b/packages/plugins/openapi/src/rpc-generator.ts @@ -1,7 +1,7 @@ // Inspired by: https://github.com/omar-dulaimi/prisma-trpc-generator import { DMMF } from '@prisma/generator-helper'; -import { analyzePolicies, AUXILIARY_FIELDS, PluginError } from '@zenstackhq/sdk'; +import { analyzePolicies, AUXILIARY_FIELDS, PluginError, requireOption, resolvePath } from '@zenstackhq/sdk'; import { DataModel, isDataModel } from '@zenstackhq/sdk/ast'; import { addMissingInputObjectTypesForAggregate, @@ -31,10 +31,8 @@ export class RPCOpenAPIGenerator extends OpenAPIGeneratorBase { private warnings: string[] = []; generate() { - const output = this.getOption('output', ''); - if (!output) { - throw new PluginError('"output" option is required'); - } + let output = requireOption(this.options, 'output'); + output = resolvePath(output, this.options); // input types this.inputObjectTypes.push(...this.dmmf.schema.inputObjectTypes.prisma); @@ -663,7 +661,7 @@ export class RPCOpenAPIGenerator extends OpenAPIGeneratorBase { return this.wrapArray(this.ref(def.type, false), def.isList); default: - throw new PluginError(`Unsupported field kind: ${def.kind}`); + throw new PluginError(this.options.name, `Unsupported field kind: ${def.kind}`); } } diff --git a/packages/plugins/react/src/generator/index.ts b/packages/plugins/react/src/generator/index.ts index c16cfaafd..2b4389756 100644 --- a/packages/plugins/react/src/generator/index.ts +++ b/packages/plugins/react/src/generator/index.ts @@ -12,6 +12,6 @@ export async function generate(model: Model, options: PluginOptions, dmmf: DMMF. case 'react-query': return reactQueryGenerate(model, options, dmmf); default: - throw new PluginError(`Unknown "fetcher" option: ${fetcher}, use "swr" or "react-query"`); + throw new PluginError(options.name, `Unknown "fetcher" option: ${fetcher}, use "swr" or "react-query"`); } } diff --git a/packages/plugins/react/src/generator/react-query.ts b/packages/plugins/react/src/generator/react-query.ts index c065e75d8..0a9d08481 100644 --- a/packages/plugins/react/src/generator/react-query.ts +++ b/packages/plugins/react/src/generator/react-query.ts @@ -1,6 +1,7 @@ import { DMMF } from '@prisma/generator-helper'; -import { PluginError, PluginOptions, createProject, getDataModels, saveProject } from '@zenstackhq/sdk'; +import { PluginOptions, createProject, getDataModels, saveProject } from '@zenstackhq/sdk'; import { DataModel, Model } from '@zenstackhq/sdk/ast'; +import { requireOption, resolvePath } from '@zenstackhq/sdk/utils'; import { paramCase } from 'change-case'; import { lowerCaseFirst } from 'lower-case-first'; import * as path from 'path'; @@ -8,15 +9,8 @@ import { Project, SourceFile, VariableDeclarationKind } from 'ts-morph'; import { upperCaseFirst } from 'upper-case-first'; export async function generate(model: Model, options: PluginOptions, dmmf: DMMF.Document) { - let outDir = options.output as string; - if (!outDir) { - throw new PluginError('"output" option is required'); - } - - if (!path.isAbsolute(outDir)) { - // output dir is resolved relative to the schema file path - outDir = path.join(path.dirname(options.schemaPath), outDir); - } + let outDir = requireOption(options, 'output'); + outDir = resolvePath(outDir, options); const project = createProject(); const warnings: string[] = []; diff --git a/packages/plugins/react/src/generator/swr.ts b/packages/plugins/react/src/generator/swr.ts index cd503b18f..766b3e853 100644 --- a/packages/plugins/react/src/generator/swr.ts +++ b/packages/plugins/react/src/generator/swr.ts @@ -1,10 +1,11 @@ import { DMMF } from '@prisma/generator-helper'; import { CrudFailureReason, - PluginError, PluginOptions, createProject, getDataModels, + requireOption, + resolvePath, saveProject, } from '@zenstackhq/sdk'; import { DataModel, Model } from '@zenstackhq/sdk/ast'; @@ -14,15 +15,8 @@ import * as path from 'path'; import { Project } from 'ts-morph'; export async function generate(model: Model, options: PluginOptions, dmmf: DMMF.Document) { - let outDir = options.output as string; - if (!outDir) { - throw new PluginError('"output" option is required'); - } - - if (!path.isAbsolute(outDir)) { - // output dir is resolved relative to the schema file path - outDir = path.join(path.dirname(options.schemaPath), outDir); - } + let outDir = requireOption(options, 'output'); + outDir = resolvePath(outDir, options); const project = createProject(); const warnings: string[] = []; diff --git a/packages/plugins/swr/src/generator.ts b/packages/plugins/swr/src/generator.ts index bf96f47c4..335bdd1f7 100644 --- a/packages/plugins/swr/src/generator.ts +++ b/packages/plugins/swr/src/generator.ts @@ -1,10 +1,11 @@ import { DMMF } from '@prisma/generator-helper'; import { CrudFailureReason, - PluginError, PluginOptions, createProject, getDataModels, + requireOption, + resolvePath, saveProject, } from '@zenstackhq/sdk'; import { DataModel, Model } from '@zenstackhq/sdk/ast'; @@ -15,15 +16,8 @@ import path from 'path'; import { Project } from 'ts-morph'; export async function generate(model: Model, options: PluginOptions, dmmf: DMMF.Document) { - let outDir = options.output as string; - if (!outDir) { - throw new PluginError('"output" option is required'); - } - - if (!path.isAbsolute(outDir)) { - // output dir is resolved relative to the schema file path - outDir = path.join(path.dirname(options.schemaPath), outDir); - } + let outDir = requireOption(options, 'output'); + outDir = resolvePath(outDir, options); const project = createProject(); const warnings: string[] = []; diff --git a/packages/plugins/tanstack-query/src/generator.ts b/packages/plugins/tanstack-query/src/generator.ts index 9ef1cafc2..49301061d 100644 --- a/packages/plugins/tanstack-query/src/generator.ts +++ b/packages/plugins/tanstack-query/src/generator.ts @@ -1,5 +1,13 @@ import { DMMF } from '@prisma/generator-helper'; -import { PluginError, PluginOptions, createProject, getDataModels, saveProject } from '@zenstackhq/sdk'; +import { + PluginError, + PluginOptions, + createProject, + getDataModels, + requireOption, + resolvePath, + saveProject, +} from '@zenstackhq/sdk'; import { DataModel, Model } from '@zenstackhq/sdk/ast'; import { paramCase } from 'change-case'; import fs from 'fs'; @@ -7,31 +15,25 @@ import { lowerCaseFirst } from 'lower-case-first'; import path from 'path'; import { Project, SourceFile, VariableDeclarationKind } from 'ts-morph'; import { upperCaseFirst } from 'upper-case-first'; +import { name } from '.'; const supportedTargets = ['react', 'svelte']; type TargetFramework = (typeof supportedTargets)[number]; export async function generate(model: Model, options: PluginOptions, dmmf: DMMF.Document) { - let outDir = options.output as string; - if (!outDir) { - throw new PluginError('"output" option is required'); - } - - if (!path.isAbsolute(outDir)) { - // output dir is resolved relative to the schema file path - outDir = path.join(path.dirname(options.schemaPath), outDir); - } + let outDir = requireOption(options, 'output'); + outDir = resolvePath(outDir, options); const project = createProject(); const warnings: string[] = []; const models = getDataModels(model); - const target = options.target as string; - if (!target) { - throw new PluginError(`"target" option is required, supported values: ${supportedTargets.join(', ')}`); - } + const target = requireOption(options, 'target'); if (!supportedTargets.includes(target)) { - throw new PluginError(`Unsupported target "${target}", supported values: ${supportedTargets.join(', ')}`); + throw new PluginError( + options.name, + `Unsupported target "${target}", supported values: ${supportedTargets.join(', ')}` + ); } generateIndex(project, outDir, models); @@ -198,7 +200,7 @@ function generateMutationHook( break; default: - throw new PluginError(`Unsupported target "${target}"`); + throw new PluginError(name, `Unsupported target "${target}"`); } func.addStatements('return mutation;'); @@ -395,7 +397,7 @@ function generateHelper(target: TargetFramework, project: Project, outDir: strin srcFile = path.join(__dirname, './res/svelte/helper.ts'); break; default: - throw new PluginError(`Unsupported target: ${target}`); + throw new PluginError(name, `Unsupported target: ${target}`); } // merge content of `shared.ts`, `helper.ts` and `marshal-?.ts` @@ -418,7 +420,7 @@ function makeGetContext(target: TargetFramework) { case 'svelte': return `const { endpoint } = getContext(SvelteQueryContextKey);`; default: - throw new PluginError(`Unsupported target "${target}"`); + throw new PluginError(name, `Unsupported target "${target}"`); } } @@ -441,7 +443,7 @@ function makeBaseImports(target: TargetFramework) { ...shared, ]; default: - throw new PluginError(`Unsupported target: ${target}`); + throw new PluginError(name, `Unsupported target: ${target}`); } } @@ -452,7 +454,7 @@ function makeQueryOptions(target: string, returnType: string) { case 'svelte': return `QueryOptions<${returnType}>`; default: - throw new PluginError(`Unsupported target: ${target}`); + throw new PluginError(name, `Unsupported target: ${target}`); } } @@ -463,6 +465,6 @@ function makeMutationOptions(target: string, returnType: string, argsType: strin case 'svelte': return `MutationOptions<${returnType}, unknown, ${argsType}>`; default: - throw new PluginError(`Unsupported target: ${target}`); + throw new PluginError(name, `Unsupported target: ${target}`); } } diff --git a/packages/plugins/trpc/src/generator.ts b/packages/plugins/trpc/src/generator.ts index 95c4d6f56..2f0d6cd48 100644 --- a/packages/plugins/trpc/src/generator.ts +++ b/packages/plugins/trpc/src/generator.ts @@ -1,8 +1,15 @@ import { DMMF } from '@prisma/generator-helper'; -import { CrudFailureReason, PluginError, PluginOptions, RUNTIME_PACKAGE, saveProject } from '@zenstackhq/sdk'; +import { + CrudFailureReason, + PluginOptions, + RUNTIME_PACKAGE, + requireOption, + resolvePath, + saveProject, +} from '@zenstackhq/sdk'; import { Model } from '@zenstackhq/sdk/ast'; -import { lowerCaseFirst } from 'lower-case-first'; import { promises as fs } from 'fs'; +import { lowerCaseFirst } from 'lower-case-first'; import path from 'path'; import { Project } from 'ts-morph'; import { @@ -17,15 +24,8 @@ import removeDir from './utils/removeDir'; import { generate as PrismaZodGenerator } from './zod/generator'; export async function generate(model: Model, options: PluginOptions, dmmf: DMMF.Document) { - let outDir = options.output as string; - if (!outDir) { - throw new PluginError('"output" option is required'); - } - - if (!path.isAbsolute(outDir)) { - // output dir is resolved relative to the schema file path - outDir = path.join(path.dirname(options.schemaPath), outDir); - } + let outDir = requireOption(options, 'output'); + outDir = resolvePath(outDir, options); await fs.mkdir(outDir, { recursive: true }); await removeDir(outDir, true); diff --git a/packages/plugins/trpc/src/zod/generator.ts b/packages/plugins/trpc/src/zod/generator.ts index d480ac646..daa5064a9 100644 --- a/packages/plugins/trpc/src/zod/generator.ts +++ b/packages/plugins/trpc/src/zod/generator.ts @@ -1,6 +1,6 @@ import { ConnectorType, DMMF } from '@prisma/generator-helper'; import { Dictionary } from '@prisma/internals'; -import { PluginOptions, getLiteral } from '@zenstackhq/sdk'; +import { PluginOptions, getLiteral, resolvePath } from '@zenstackhq/sdk'; import { DataSource, Model, isDataSource } from '@zenstackhq/sdk/ast'; import { AggregateOperationSupport, @@ -14,7 +14,9 @@ import removeDir from './utils/removeDir'; import { writeFileSafely } from './utils/writeFileSafely'; export async function generate(model: Model, options: PluginOptions, dmmf: DMMF.Document) { - const output = (options.output as string) ?? './generated'; + let output = (options.output as string) ?? './generated'; + output = resolvePath(output, options); + await handleGeneratorOutputValue(output); const prismaClientDmmf = dmmf; diff --git a/packages/runtime/src/enhancements/policy/handler.ts b/packages/runtime/src/enhancements/policy/handler.ts index 60ba6638a..69000179e 100644 --- a/packages/runtime/src/enhancements/policy/handler.ts +++ b/packages/runtime/src/enhancements/policy/handler.ts @@ -38,11 +38,21 @@ export class PolicyProxyHandler implements Pr throw prismaClientValidationError(this.prisma, 'where field is required in query argument'); } + const guard = await this.utils.getAuthGuard(this.model, 'read'); + if (guard === false) { + return null; + } + const entities = await this.utils.readWithCheck(this.model, args); return entities[0] ?? null; } async findUniqueOrThrow(args: any) { + const guard = await this.utils.getAuthGuard(this.model, 'read'); + if (guard === false) { + throw this.utils.notFound(this.model); + } + const entity = await this.findUnique(args); if (!entity) { throw this.utils.notFound(this.model); @@ -51,11 +61,21 @@ export class PolicyProxyHandler implements Pr } async findFirst(args: any) { + const guard = await this.utils.getAuthGuard(this.model, 'read'); + if (guard === false) { + return null; + } + const entities = await this.utils.readWithCheck(this.model, args); return entities[0] ?? null; } async findFirstOrThrow(args: any) { + const guard = await this.utils.getAuthGuard(this.model, 'read'); + if (guard === false) { + throw this.utils.notFound(this.model); + } + const entity = await this.findFirst(args); if (!entity) { throw this.utils.notFound(this.model); @@ -64,6 +84,11 @@ export class PolicyProxyHandler implements Pr } async findMany(args: any) { + const guard = await this.utils.getAuthGuard(this.model, 'read'); + if (guard === false) { + return []; + } + return this.utils.readWithCheck(this.model, args); } diff --git a/packages/schema/src/cli/cli-util.ts b/packages/schema/src/cli/cli-util.ts index 30ad3da62..0540ad9ba 100644 --- a/packages/schema/src/cli/cli-util.ts +++ b/packages/schema/src/cli/cli-util.ts @@ -242,7 +242,7 @@ export async function runPlugins(options: { schema: string; packageManager: Pack await new PluginRunner().run(context); } catch (err) { if (err instanceof PluginError) { - console.error(colors.red(err.message)); + console.error(colors.red(`${err.plugin}: ${err.message}`)); throw new CliError(err.message); } else { throw err; diff --git a/packages/schema/src/cli/plugin-runner.ts b/packages/schema/src/cli/plugin-runner.ts index 909dce993..63b15e48b 100644 --- a/packages/schema/src/cli/plugin-runner.ts +++ b/packages/schema/src/cli/plugin-runner.ts @@ -2,7 +2,7 @@ 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'; +import { getLiteral, getLiteralArray, PluginError, PluginFunction, PluginOptions, resolvePath } from '@zenstackhq/sdk'; import colors from 'colors'; import fs from 'fs'; import ora from 'ora'; @@ -42,16 +42,6 @@ export class PluginRunner { for (const pluginProvider of allPluginProviders) { const plugin = pluginDecls.find((p) => this.getPluginProvider(p) === pluginProvider); if (plugin) { - const options: PluginOptions = { schemaPath: context.schemaPath }; - - plugin.fields.forEach((f) => { - const value = getLiteral(f.value) ?? getLiteralArray(f.value); - if (value === undefined) { - throw new PluginError(`Invalid plugin value for ${f.name}`); - } - options[f.name] = value; - }); - const pluginModulePath = this.getPluginModulePath(pluginProvider); // eslint-disable-next-line @typescript-eslint/no-explicit-any let pluginModule: any; @@ -59,32 +49,45 @@ export class PluginRunner { pluginModule = require(pluginModulePath); } catch (err) { console.error(`Unable to load plugin module ${pluginProvider}: ${pluginModulePath}, ${err}`); - throw new PluginError(`Unable to load plugin module ${pluginProvider}`); + throw new PluginError('', `Unable to load plugin module ${pluginProvider}`); } if (!pluginModule.default || typeof pluginModule.default !== 'function') { console.error(`Plugin provider ${pluginProvider} is missing a default function export`); - throw new PluginError(`Plugin provider ${pluginProvider} is missing a default function export`); + throw new PluginError('', `Plugin provider ${pluginProvider} is missing a default function export`); } + + const pluginName = this.getPluginName(pluginModule, pluginProvider); + const options: PluginOptions = { schemaPath: context.schemaPath, name: pluginName }; + + plugin.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; + }); + plugins.push({ - name: this.getPluginName(pluginModule, pluginProvider), + name: pluginName, provider: pluginProvider, run: pluginModule.default as PluginFunction, options, }); - if (pluginProvider === '@core/prisma' && options.output) { + if (pluginProvider === '@core/prisma' && typeof options.output === 'string') { // record custom prisma output path - prismaOutput = options.output as string; + prismaOutput = resolvePath(options.output, options); } } else { // synthesize a plugin const pluginModule = require(this.getPluginModulePath(pluginProvider)); + const pluginName = this.getPluginName(pluginModule, pluginProvider); plugins.push({ - name: this.getPluginName(pluginModule, pluginProvider), + name: pluginName, provider: pluginProvider, run: pluginModule.default, - options: { schemaPath: context.schemaPath }, + options: { schemaPath: context.schemaPath, name: pluginName }, }); } } diff --git a/packages/schema/src/plugins/access-policy/expression-writer.ts b/packages/schema/src/plugins/access-policy/expression-writer.ts index 238eed6c1..c5d0e47d3 100644 --- a/packages/schema/src/plugins/access-policy/expression-writer.ts +++ b/packages/schema/src/plugins/access-policy/expression-writer.ts @@ -16,6 +16,7 @@ import { } from '@zenstackhq/language/ast'; import { getLiteral, GUARD_FIELD_NAME, PluginError } from '@zenstackhq/sdk'; import { CodeBlockWriter } from 'ts-morph'; +import { name } from '.'; import { FILTER_OPERATOR_FUNCTIONS } from '../../language-server/constants'; import { getIdFields, isAuthInvocation } from '../../utils/ast-utils'; import TypeScriptExpressionTransformer from './typescript-expression-transformer'; @@ -180,7 +181,7 @@ export class ExpressionWriter { 'has' ); } else { - throw new PluginError('"in" operator cannot be used with field references on both sides'); + throw new PluginError(name, '"in" operator cannot be used with field references on both sides'); } }); } @@ -235,7 +236,7 @@ export class ExpressionWriter { const rightIsFieldAccess = this.isFieldAccess(expr.right); if (leftIsFieldAccess && rightIsFieldAccess) { - throw new PluginError(`Comparison between fields are not supported yet`); + throw new PluginError(name, `Comparison between fields are not supported yet`); } if (!leftIsFieldAccess && !rightIsFieldAccess) { @@ -292,11 +293,11 @@ export class ExpressionWriter { const idFields = getIdFields(dataModel); if (!idFields || idFields.length === 0) { - throw new PluginError(`Data model ${dataModel.name} does not have an id field`); + throw new PluginError(name, `Data model ${dataModel.name} does not have an id field`); } if (operator !== '==' && operator !== '!=') { - throw new PluginError('Only == and != operators are allowed'); + throw new PluginError(name, 'Only == and != operators are allowed'); } if (!isThisExpr(fieldAccess)) { @@ -354,7 +355,7 @@ export class ExpressionWriter { } else if (operator === '!=') { this.writer.write('isNot: '); } else { - throw new PluginError('Only == and != operators are allowed for data model comparison'); + throw new PluginError(name, 'Only == and != operators are allowed for data model comparison'); } writeOperand(); } else { @@ -396,11 +397,11 @@ export class ExpressionWriter { operand = fieldAccess.operand; } } else { - throw new PluginError(`Unsupported expression type: ${fieldAccess.$type}`); + throw new PluginError(name, `Unsupported expression type: ${fieldAccess.$type}`); } if (!selector) { - throw new PluginError(`Failed to write FieldAccess expression`); + throw new PluginError(name, `Failed to write FieldAccess expression`); } const writerFilterOutput = () => { @@ -517,7 +518,7 @@ export class ExpressionWriter { private writeUnary(expr: UnaryExpr) { if (expr.operator !== '!') { - throw new PluginError(`Unary operator "${expr.operator}" is not supported`); + throw new PluginError(name, `Unary operator "${expr.operator}" is not supported`); } this.block(() => { @@ -537,7 +538,7 @@ export class ExpressionWriter { private writeInvocation(expr: InvocationExpr) { const funcDecl = expr.function.ref; if (!funcDecl) { - throw new PluginError(`Failed to resolve function declaration`); + throw new PluginError(name, `Failed to resolve function declaration`); } if (FILTER_OPERATOR_FUNCTIONS.includes(funcDecl.name)) { @@ -573,7 +574,7 @@ export class ExpressionWriter { ); }); } else { - throw new PluginError(`Unsupported function ${funcDecl.name}`); + throw new PluginError(name, `Unsupported function ${funcDecl.name}`); } } } 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 b77da8fed..6a080cd06 100644 --- a/packages/schema/src/plugins/access-policy/policy-guard-generator.ts +++ b/packages/schema/src/plugins/access-policy/policy-guard-generator.ts @@ -25,11 +25,12 @@ import { PluginError, PluginOptions, resolved, + resolvePath, RUNTIME_PACKAGE, saveProject, } from '@zenstackhq/sdk'; -import { lowerCaseFirst } from 'lower-case-first'; import { streamAllContents } from 'langium'; +import { lowerCaseFirst } from 'lower-case-first'; import path from 'path'; import { FunctionDeclaration, SourceFile, VariableDeclarationKind } from 'ts-morph'; import { name } from '.'; @@ -46,11 +47,11 @@ import { ZodSchemaGenerator } from './zod-schema-generator'; */ export default class PolicyGenerator { async generate(model: Model, options: PluginOptions) { - const output = options.output ? (options.output as string) : getDefaultOutputFolder(); + let output = options.output ? (options.output as string) : getDefaultOutputFolder(); if (!output) { - console.error(`Unable to determine output path, not running plugin ${name}`); - return; + throw new PluginError(options.name, `Unable to determine output path, not running plugin`); } + output = resolvePath(output, options); const project = createProject(); const sf = project.createSourceFile(path.join(output, 'policy.ts'), undefined, { overwrite: true }); @@ -343,11 +344,11 @@ export default class PolicyGenerator { (decl): decl is DataModel => isDataModel(decl) && decl.name === 'User' ); if (!userModel) { - throw new PluginError('User model not found'); + throw new PluginError(name, 'User model not found'); } const userIdFields = getIdFields(userModel); if (!userIdFields || userIdFields.length === 0) { - throw new PluginError('User model does not have an id field'); + throw new PluginError(name, 'User model does not have an id field'); } // normalize user to null to avoid accidentally use undefined in filter diff --git a/packages/schema/src/plugins/access-policy/typescript-expression-transformer.ts b/packages/schema/src/plugins/access-policy/typescript-expression-transformer.ts index 61d4b6f9c..d5acda4b1 100644 --- a/packages/schema/src/plugins/access-policy/typescript-expression-transformer.ts +++ b/packages/schema/src/plugins/access-policy/typescript-expression-transformer.ts @@ -13,6 +13,7 @@ import { UnaryExpr, } from '@zenstackhq/language/ast'; import { getLiteral, PluginError } from '@zenstackhq/sdk'; +import { name } from '.'; import { FILTER_OPERATOR_FUNCTIONS } from '../../language-server/constants'; import { isAuthInvocation } from '../../utils/ast-utils'; import { isFutureExpr } from './utils'; @@ -63,7 +64,7 @@ export default class TypeScriptExpressionTransformer { return this.binary(expr as BinaryExpr, normalizeUndefined); default: - throw new PluginError(`Unsupported expression type: ${expr.$type}`); + throw new PluginError(name, `Unsupported expression type: ${expr.$type}`); } } @@ -75,14 +76,14 @@ export default class TypeScriptExpressionTransformer { private memberAccess(expr: MemberAccessExpr, normalizeUndefined: boolean) { if (!expr.member.ref) { - throw new PluginError(`Unresolved MemberAccessExpr`); + throw new PluginError(name, `Unresolved MemberAccessExpr`); } if (isThisExpr(expr.operand)) { return expr.member.ref.name; } else if (isFutureExpr(expr.operand)) { if (!this.isPostGuard) { - throw new PluginError(`future() is only supported in postUpdate rules`); + throw new PluginError(name, `future() is only supported in postUpdate rules`); } return expr.member.ref.name; } else { @@ -97,7 +98,7 @@ export default class TypeScriptExpressionTransformer { private invocation(expr: InvocationExpr, normalizeUndefined: boolean) { if (!expr.function.ref) { - throw new PluginError(`Unresolved InvocationExpr`); + throw new PluginError(name, `Unresolved InvocationExpr`); } if (isAuthInvocation(expr)) { @@ -121,7 +122,7 @@ export default class TypeScriptExpressionTransformer { break; } case 'search': - throw new PluginError('"search" function must be used against a field'); + throw new PluginError(name, '"search" function must be used against a field'); case 'startsWith': result = `${arg0}?.startsWith(${this.transform(expr.args[1].value, normalizeUndefined)})`; break; @@ -147,18 +148,18 @@ export default class TypeScriptExpressionTransformer { result = `${arg0}?.length === 0`; break; default: - throw new PluginError(`Function invocation is not supported: ${expr.function.ref?.name}`); + throw new PluginError(name, `Function invocation is not supported: ${expr.function.ref?.name}`); } return `(${result} ?? false)`; } else { - throw new PluginError(`Function invocation is not supported: ${expr.function.ref?.name}`); + throw new PluginError(name, `Function invocation is not supported: ${expr.function.ref?.name}`); } } private reference(expr: ReferenceExpr) { if (!expr.target.ref) { - throw new PluginError(`Unresolved ReferenceExpr`); + throw new PluginError(name, `Unresolved ReferenceExpr`); } if (isEnumField(expr.target.ref)) { diff --git a/packages/schema/src/plugins/model-meta/index.ts b/packages/schema/src/plugins/model-meta/index.ts index 25c373173..2198c4b43 100644 --- a/packages/schema/src/plugins/model-meta/index.ts +++ b/packages/schema/src/plugins/model-meta/index.ts @@ -16,32 +16,30 @@ import { getLiteral, hasAttribute, isIdField, + PluginError, PluginOptions, resolved, + resolvePath, saveProject, } from '@zenstackhq/sdk'; import { lowerCaseFirst } from 'lower-case-first'; import path from 'path'; import { CodeBlockWriter, VariableDeclarationKind } from 'ts-morph'; -import { ensureNodeModuleFolder, getDefaultOutputFolder } from '../plugin-utils'; +import { getDefaultOutputFolder } from '../plugin-utils'; export const name = 'Model Metadata'; export default async function run(model: Model, options: PluginOptions) { - const output = options.output ? (options.output as string) : getDefaultOutputFolder(); + let output = options.output ? (options.output as string) : getDefaultOutputFolder(); if (!output) { - console.error(`Unable to determine output path, not running plugin ${name}`); - return; + throw new PluginError(options.name, `Unable to determine output path, not running plugin`); } + output = resolvePath(output, options); const dataModels = getDataModels(model); const project = createProject(); - if (!options.output) { - ensureNodeModuleFolder(output); - } - const sf = project.createSourceFile(path.join(output, 'model-meta.ts'), undefined, { overwrite: true }); sf.addStatements('/* eslint-disable */'); sf.addVariableStatement({ diff --git a/packages/schema/src/plugins/plugin-utils.ts b/packages/schema/src/plugins/plugin-utils.ts index d10be7c94..546166892 100644 --- a/packages/schema/src/plugins/plugin-utils.ts +++ b/packages/schema/src/plugins/plugin-utils.ts @@ -29,21 +29,3 @@ export function getDefaultOutputFolder() { const modulesFolder = getNodeModulesFolder(runtimeModulePath); return modulesFolder ? path.join(modulesFolder, '.zenstack') : undefined; } - -/** - * Ensure a folder exists and has a package.json in it. - */ -export function ensureNodeModuleFolder(folder: string) { - if (!fs.existsSync(folder)) { - fs.mkdirSync(folder, { recursive: true }); - } - if (!fs.existsSync(path.join(folder, 'package.json'))) { - fs.writeFileSync( - path.join(folder, 'package.json'), - JSON.stringify({ - name: '.zenstack', - version: '1.0.0', - }) - ); - } -} diff --git a/packages/schema/src/plugins/prisma/schema-generator.ts b/packages/schema/src/plugins/prisma/schema-generator.ts index 17a594377..fed88dad3 100644 --- a/packages/schema/src/plugins/prisma/schema-generator.ts +++ b/packages/schema/src/plugins/prisma/schema-generator.ts @@ -28,11 +28,13 @@ import { PluginError, PluginOptions, resolved, + resolvePath, TRANSACTION_FIELD_NAME, } from '@zenstackhq/sdk'; import fs from 'fs'; import { writeFile } from 'fs/promises'; import path from 'path'; +import { name } from '.'; import { getStringLiteral } from '../../language-server/validator/utils'; import { execSync } from '../../utils/exec-utils'; import { @@ -94,7 +96,9 @@ export default class PrismaSchemaGenerator { } } - const outFile = (options.output as string) ?? './prisma/schema.prisma'; + let outFile = (options.output as string) ?? './prisma/schema.prisma'; + outFile = resolvePath(outFile, options); + if (!fs.existsSync(path.dirname(outFile))) { fs.mkdirSync(path.dirname(outFile), { recursive: true }); } @@ -125,7 +129,7 @@ export default class PrismaSchemaGenerator { if (this.isStringLiteral(f.value)) { provider = f.value.value as string; } else { - throw new PluginError('Datasource provider must be set to a string'); + throw new PluginError(name, 'Datasource provider must be set to a string'); } break; } @@ -133,7 +137,7 @@ export default class PrismaSchemaGenerator { case 'url': { const r = this.extractDataSourceUrl(f.value); if (!r) { - throw new PluginError('Invalid value for datasource url'); + throw new PluginError(name, 'Invalid value for datasource url'); } url = r; break; @@ -142,7 +146,7 @@ export default class PrismaSchemaGenerator { case 'shadowDatabaseUrl': { const r = this.extractDataSourceUrl(f.value); if (!r) { - throw new PluginError('Invalid value for datasource url'); + throw new PluginError(name, 'Invalid value for datasource url'); } shadowDatabaseUrl = r; break; @@ -153,6 +157,7 @@ export default class PrismaSchemaGenerator { const value = isArrayExpr(f.value) ? getLiteralArray(f.value) : getLiteral(f.value); if (value === undefined) { throw new PluginError( + name, `Invalid value for datasource field ${f.name}: value must be a string or an array of strings` ); } else { @@ -164,10 +169,10 @@ export default class PrismaSchemaGenerator { } if (!provider) { - throw new PluginError('Datasource is missing "provider" field'); + throw new PluginError(name, 'Datasource is missing "provider" field'); } if (!url) { - throw new PluginError('Datasource is missing "url" field'); + throw new PluginError(name, 'Datasource is missing "url" field'); } prisma.addDataSource(dataSource.name, provider, url, shadowDatabaseUrl, restFields); @@ -204,25 +209,25 @@ export default class PrismaSchemaGenerator { this.generateModelField(model, field); } - // add an "zenstack_guard" field for dealing with boolean conditions - 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_guard" field for dealing with boolean conditions + 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)), + ]); + } + // add an "zenstack_transaction" field for tracking records created/updated with nested writes const transactionField = model.addField(TRANSACTION_FIELD_NAME, 'String?'); @@ -289,7 +294,7 @@ export default class PrismaSchemaGenerator { const fieldType = field.type.type || field.type.reference?.ref?.name || this.getUnsupportedFieldType(field.type); if (!fieldType) { - throw new PluginError(`Field type is not resolved: ${field.$container.name}.${field.name}`); + throw new PluginError(name, `Field type is not resolved: ${field.$container.name}.${field.name}`); } const type = new ModelFieldType(fieldType, field.type.array, field.type.optional); @@ -315,7 +320,7 @@ export default class PrismaSchemaGenerator { if (text) { return new PrismaPassThroughAttribute(text); } else { - throw new PluginError(`Invalid arguments for ${FIELD_PASSTHROUGH_ATTR} attribute`); + throw new PluginError(name, `Invalid arguments for ${FIELD_PASSTHROUGH_ATTR} attribute`); } } else { return new PrismaFieldAttribute( @@ -339,7 +344,7 @@ export default class PrismaSchemaGenerator { case 'boolean': return new PrismaAttributeArgValue('Boolean', node.value); default: - throw new PluginError(`Unexpected literal type: ${typeof node.value}`); + throw new PluginError(name, `Unexpected literal type: ${typeof node.value}`); } } else if (isArrayExpr(node)) { return new PrismaAttributeArgValue( @@ -358,7 +363,7 @@ export default class PrismaSchemaGenerator { // invocation return new PrismaAttributeArgValue('FunctionCall', this.makeFunctionCall(node)); } else { - throw new PluginError(`Unsupported attribute argument expression type: ${node.$type}`); + throw new PluginError(name, `Unsupported attribute argument expression type: ${node.$type}`); } } @@ -367,7 +372,7 @@ export default class PrismaSchemaGenerator { resolved(node.function).name, node.args.map((arg) => { if (!isLiteralExpr(arg.value)) { - throw new PluginError('Function call argument must be literal'); + throw new PluginError(name, 'Function call argument must be literal'); } return new PrismaFunctionCallArg(arg.name, arg.value.value); }) diff --git a/packages/schema/src/plugins/zod/generator.ts b/packages/schema/src/plugins/zod/generator.ts index 0f8ba70a8..d723533eb 100644 --- a/packages/schema/src/plugins/zod/generator.ts +++ b/packages/schema/src/plugins/zod/generator.ts @@ -1,6 +1,6 @@ import { ConnectorType, DMMF } from '@prisma/generator-helper'; import { Dictionary } from '@prisma/internals'; -import { PluginOptions, createProject, emitProject, getLiteral, saveProject } from '@zenstackhq/sdk'; +import { PluginOptions, createProject, emitProject, getLiteral, resolvePath, saveProject } from '@zenstackhq/sdk'; import { DataSource, Model, isDataSource } from '@zenstackhq/sdk/ast'; import { AggregateOperationSupport, @@ -24,6 +24,7 @@ export async function generate(model: Model, options: PluginOptions, dmmf: DMMF. output = './generated/zod'; } } + output = resolvePath(output, options); await handleGeneratorOutputValue(output); const prismaClientDmmf = dmmf; diff --git a/packages/schema/tests/generator/prisma-generator.test.ts b/packages/schema/tests/generator/prisma-generator.test.ts index 890fdf519..12a8b59da 100644 --- a/packages/schema/tests/generator/prisma-generator.test.ts +++ b/packages/schema/tests/generator/prisma-generator.test.ts @@ -32,6 +32,7 @@ describe('Prisma generator test', () => { const { name } = tmp.fileSync({ postfix: '.prisma' }); await new PrismaSchemaGenerator().generate(model, { + name: 'Prisma', provider: '@core/prisma', schemaPath: 'schema.zmodel', output: name, @@ -69,6 +70,7 @@ describe('Prisma generator test', () => { const { name } = tmp.fileSync({ postfix: '.prisma' }); await new PrismaSchemaGenerator().generate(model, { + name: 'Prisma', provider: '@core/prisma', schemaPath: 'schema.zmodel', output: name, @@ -100,6 +102,7 @@ describe('Prisma generator test', () => { const { name } = tmp.fileSync({ postfix: '.prisma' }); await new PrismaSchemaGenerator().generate(model, { + name: 'Prisma', provider: '@core/prisma', schemaPath: 'schema.zmodel', output: name, @@ -135,6 +138,7 @@ describe('Prisma generator test', () => { const { name } = tmp.fileSync({ postfix: '.prisma' }); await new PrismaSchemaGenerator().generate(model, { + name: 'Prisma', provider: '@core/prisma', schemaPath: 'schema.zmodel', output: name, @@ -174,6 +178,7 @@ describe('Prisma generator test', () => { const { name } = tmp.fileSync({ postfix: '.prisma' }); await new PrismaSchemaGenerator().generate(model, { + name: 'Prisma', provider: '@core/prisma', schemaPath: 'schema.zmodel', output: name, @@ -224,6 +229,7 @@ describe('Prisma generator test', () => { const { name } = tmp.fileSync({ postfix: '.prisma' }); await new PrismaSchemaGenerator().generate(model, { + name: 'Prisma', provider: '@core/prisma', schemaPath: 'schema.zmodel', output: name, @@ -256,6 +262,7 @@ describe('Prisma generator test', () => { `); const { name } = tmp.fileSync({ postfix: '.prisma' }); await new PrismaSchemaGenerator().generate(model, { + name: 'Prisma', provider: '@core/prisma', schemaPath: 'schema.zmodel', output: name, @@ -268,7 +275,7 @@ describe('Prisma generator test', () => { expect(dmmf.datamodel.models.length).toBe(1); const post = dmmf.datamodel.models[0]; expect(post.name).toBe('Post'); - expect(post.fields.length).toBe(6); + expect(post.fields.length).toBe(5); }); it('custom aux field names', async () => { @@ -289,6 +296,7 @@ describe('Prisma generator test', () => { await new PrismaSchemaGenerator().generate( model, { + name: 'Prisma', provider: '@core/prisma', schemaPath: 'schema.zmodel', output: name, @@ -311,6 +319,7 @@ describe('Prisma generator test', () => { const { name } = tmp.fileSync({ postfix: '.prisma' }); await new PrismaSchemaGenerator().generate(model, { + name: 'Prisma', provider: '@core/prisma', schemaPath: 'schema.zmodel', output: name, @@ -351,6 +360,7 @@ describe('Prisma generator test', () => { const { name } = tmp.fileSync({ postfix: '.prisma' }); await new PrismaSchemaGenerator().generate(model, { + name: 'Prisma', provider: '@core/prisma', schemaPath: 'schema.zmodel', output: name, diff --git a/packages/sdk/src/code-gen.ts b/packages/sdk/src/code-gen.ts index 02dd348b1..602abdc02 100644 --- a/packages/sdk/src/code-gen.ts +++ b/packages/sdk/src/code-gen.ts @@ -63,7 +63,7 @@ export async function emitProject(project: Project) { console.error('Error compiling generated code:'); console.error(project.formatDiagnosticsWithColorAndContext(errors.slice(0, 10))); await project.save(); - throw new PluginError(`Error compiling generated code`); + throw new PluginError('', `Error compiling generated code`); } const result = await project.emit(); @@ -73,6 +73,6 @@ export async function emitProject(project: Project) { console.error('Some generated code is not emitted:'); console.error(project.formatDiagnosticsWithColorAndContext(emitErrors.slice(0, 10))); await project.save(); - throw new PluginError(`Error emitting generated code`); + throw new PluginError('', `Error emitting generated code`); } } diff --git a/packages/sdk/src/types.ts b/packages/sdk/src/types.ts index bb967d234..b050d2023 100644 --- a/packages/sdk/src/types.ts +++ b/packages/sdk/src/types.ts @@ -9,7 +9,10 @@ export type OptionValue = string | number | boolean; /** * Plugin configuration oiptions */ -export type PluginOptions = { provider?: string; schemaPath: string } & Record; +export type PluginOptions = { provider?: string; schemaPath: string; name: string } & Record< + string, + OptionValue | OptionValue[] +>; /** * Plugin entry point function definition @@ -25,7 +28,7 @@ export type PluginFunction = ( * Plugin error */ export class PluginError extends Error { - constructor(message: string) { + constructor(public plugin: string, message: string) { super(message); } } diff --git a/packages/sdk/src/utils.ts b/packages/sdk/src/utils.ts index 974148b4d..31bb2aa10 100644 --- a/packages/sdk/src/utils.ts +++ b/packages/sdk/src/utils.ts @@ -16,6 +16,8 @@ import { Reference, ReferenceExpr, } from '@zenstackhq/language/ast'; +import path from 'path'; +import { PluginOptions } from './types'; /** * Gets data models that are not ignored @@ -194,3 +196,19 @@ export function isForeignKeyField(field: DataModelField) { return false; }); } + +export function resolvePath(_path: string, options: PluginOptions) { + if (path.isAbsolute(_path)) { + return _path; + } else { + return path.join(path.dirname(options.schemaPath), _path); + } +} + +export function requireOption(options: PluginOptions, name: string): T { + const value = options[name]; + if (value === undefined) { + throw new Error(`Plugin "${options.name}" is missing required option: ${name}`); + } + return value as T; +} diff --git a/packages/testtools/src/schema.ts b/packages/testtools/src/schema.ts index ed8bc5ce9..2ea8e1856 100644 --- a/packages/testtools/src/schema.ts +++ b/packages/testtools/src/schema.ts @@ -174,7 +174,7 @@ export async function loadZModelAndDmmf( const model = await loadDocument(modelFile); const { name: prismaFile } = tmp.fileSync({ postfix: '.prisma' }); - await prismaPlugin(model, { schemaPath: modelFile, output: prismaFile, generateClient: false }); + await prismaPlugin(model, { schemaPath: modelFile, name: 'Prisma', output: prismaFile, generateClient: false }); const prismaContent = fs.readFileSync(prismaFile, { encoding: 'utf-8' }); From 06f216f82341f18ffe1671c928ddbbd43432af81 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Sat, 20 May 2023 21:39:53 +0900 Subject: [PATCH 2/3] fix path --- packages/plugins/trpc/tests/trpc.test.ts | 74 +++++++++++++++++++- packages/schema/src/cli/plugin-runner.ts | 2 +- packages/schema/tests/plugins/prisma.test.ts | 57 +++++++++++++++ packages/testtools/src/schema.ts | 27 +++++-- 4 files changed, 152 insertions(+), 8 deletions(-) create mode 100644 packages/schema/tests/plugins/prisma.test.ts diff --git a/packages/plugins/trpc/tests/trpc.test.ts b/packages/plugins/trpc/tests/trpc.test.ts index 2ca105c7e..d37bceee2 100644 --- a/packages/plugins/trpc/tests/trpc.test.ts +++ b/packages/plugins/trpc/tests/trpc.test.ts @@ -1,6 +1,8 @@ /// import { loadSchema } from '@zenstackhq/testtools'; +import fs from 'fs'; +import path from 'path'; describe('tRPC Plugin Tests', () => { let origDir: string; @@ -13,7 +15,7 @@ describe('tRPC Plugin Tests', () => { process.chdir(origDir); }); - it('run plugin', async () => { + it('run plugin absolute output', async () => { await loadSchema( ` plugin trpc { @@ -52,4 +54,74 @@ model Foo { true ); }); + + it('run plugin relative output', async () => { + const { projectDir } = await loadSchema( + ` +plugin trpc { + provider = '${process.cwd()}/dist' + output = './trpc' +} + +model User { + id String @id + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + email String @unique + role String @default('USER') + posts Post[] +} + +model Post { + id String @id + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + title String + author User? @relation(fields: [authorId], references: [id]) + authorId String? + published Boolean @default(false) + viewCount Int @default(0) +} + +model Foo { + id String @id + @@ignore +} + `, + true, + false, + [`${origDir}/dist`, '@trpc/client', '@trpc/server'], + true + ); + expect(fs.existsSync(path.join(projectDir, 'trpc'))).toBe(true); + }); + + it('run plugin non-standard zmodel location', async () => { + const { projectDir } = await loadSchema( + ` +plugin trpc { + provider = '${process.cwd()}/dist' + output = './trpc' +} + +model User { + id String @id + posts Post[] +} + +model Post { + id String @id + title String + author User? @relation(fields: [authorId], references: [id]) + authorId String? +} + `, + true, + false, + [`${origDir}/dist`, '@trpc/client', '@trpc/server'], + true, + 'zenstack/schema.zmodel' + ); + expect(fs.existsSync(path.join(projectDir, 'zenstack/trpc'))).toBe(true); + }); }); diff --git a/packages/schema/src/cli/plugin-runner.ts b/packages/schema/src/cli/plugin-runner.ts index 63b15e48b..2f6b008e6 100644 --- a/packages/schema/src/cli/plugin-runner.ts +++ b/packages/schema/src/cli/plugin-runner.ts @@ -37,7 +37,7 @@ export class PluginRunner { .map((p) => this.getPluginProvider(p)) .filter((p): p is string => !!p && !prereqPlugins.includes(p)) ); - let prismaOutput = './prisma/schema.prisma'; + let prismaOutput = resolvePath('./prisma/schema.prisma', { schemaPath: context.schemaPath }); for (const pluginProvider of allPluginProviders) { const plugin = pluginDecls.find((p) => this.getPluginProvider(p) === pluginProvider); diff --git a/packages/schema/tests/plugins/prisma.test.ts b/packages/schema/tests/plugins/prisma.test.ts new file mode 100644 index 000000000..c3c8478fc --- /dev/null +++ b/packages/schema/tests/plugins/prisma.test.ts @@ -0,0 +1,57 @@ +import { loadSchema } from '@zenstackhq/testtools'; +import fs from 'fs'; +import path from 'path'; +import tmp from 'tmp'; + +describe('Prisma plugin tests', () => { + let origDir: string; + + beforeEach(() => { + origDir = process.cwd(); + }); + + afterEach(() => { + process.chdir(origDir); + }); + + it('standard output location', async () => { + const model = ` +model User { + id String @id @default(cuid()) +} + `; + const { projectDir } = await loadSchema(model); + expect(fs.existsSync(path.join(projectDir, './prisma/schema.prisma'))).toEqual(true); + }); + + it('relative output location', async () => { + const model = ` +model User { + id String @id @default(cuid()) +} + +plugin prisma { + provider = '@core/prisma' + output = './db/schema.prisma' +} + `; + const { projectDir } = await loadSchema(model, true, false); + expect(fs.existsSync(path.join(projectDir, './db/schema.prisma'))).toEqual(true); + }); + + it('relative absolute location', async () => { + const { name: outDir } = tmp.dirSync({ unsafeCleanup: true }); + const model = ` +model User { + id String @id @default(cuid()) +} + +plugin prisma { + provider = '@core/prisma' + output = '${outDir}/db/schema.prisma' +} + `; + await loadSchema(model, true, false); + expect(fs.existsSync(path.join(outDir, './db/schema.prisma'))).toEqual(true); + }); +}); diff --git a/packages/testtools/src/schema.ts b/packages/testtools/src/schema.ts index 2ea8e1856..5ee96fe4e 100644 --- a/packages/testtools/src/schema.ts +++ b/packages/testtools/src/schema.ts @@ -92,7 +92,8 @@ export async function loadSchema( addPrelude = true, pushDb = true, extraDependencies: string[] = [], - compile = false + compile = false, + customSchemaFilePath?: string ) { const { name: projectRoot } = tmp.dirSync({ unsafeCleanup: true }); @@ -113,10 +114,24 @@ export async function loadSchema( schema = schema.replaceAll('$projectRoot', projectRoot); + let zmodelPath = path.join(projectRoot, 'schema.zmodel'); const content = addPrelude ? `${MODEL_PRELUDE}\n${schema}` : schema; - fs.writeFileSync('schema.zmodel', content); + if (customSchemaFilePath) { + zmodelPath = path.join(projectRoot, customSchemaFilePath); + fs.mkdirSync(path.dirname(zmodelPath), { recursive: true }); + fs.writeFileSync(zmodelPath, content); + } else { + fs.writeFileSync('schema.zmodel', content); + } run('npm install'); - run('npx zenstack generate --no-dependency-check', { NODE_PATH: './node_modules' }); + + if (customSchemaFilePath) { + run(`npx zenstack generate --schema ${zmodelPath} --no-dependency-check`, { + NODE_PATH: './node_modules', + }); + } else { + run('npx zenstack generate --no-dependency-check', { NODE_PATH: './node_modules' }); + } if (pushDb) { run('npx prisma db push'); @@ -136,9 +151,9 @@ export async function loadSchema( run('npx tsc --project tsconfig.json'); } - const policy = require(path.join(projectRoot, '.zenstack/policy')).default; - const modelMeta = require(path.join(projectRoot, '.zenstack/model-meta')).default; - const zodSchemas = require(path.join(projectRoot, '.zenstack/zod')); + const policy = require(path.join(path.dirname(zmodelPath), '.zenstack/policy')).default; + const modelMeta = require(path.join(path.dirname(zmodelPath), '.zenstack/model-meta')).default; + const zodSchemas = require(path.join(path.dirname(zmodelPath), '.zenstack/zod')); return { projectDir: projectRoot, From 94573a72e7ece2c0178288e1e84e1c94a7cc81d2 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Sat, 20 May 2023 21:42:34 +0900 Subject: [PATCH 3/3] fixes --- packages/schema/src/cli/plugin-runner.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/schema/src/cli/plugin-runner.ts b/packages/schema/src/cli/plugin-runner.ts index 2f6b008e6..2fff152d9 100644 --- a/packages/schema/src/cli/plugin-runner.ts +++ b/packages/schema/src/cli/plugin-runner.ts @@ -37,7 +37,7 @@ export class PluginRunner { .map((p) => this.getPluginProvider(p)) .filter((p): p is string => !!p && !prereqPlugins.includes(p)) ); - let prismaOutput = resolvePath('./prisma/schema.prisma', { schemaPath: context.schemaPath }); + let prismaOutput = resolvePath('./prisma/schema.prisma', { schemaPath: context.schemaPath, name: '' }); for (const pluginProvider of allPluginProviders) { const plugin = pluginDecls.find((p) => this.getPluginProvider(p) === pluginProvider);