From 892c2b55000a4f179d36a57d2c4c46d886a2838e Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Wed, 1 May 2024 15:16:46 +0800 Subject: [PATCH] fix: compatibility with pnpm monorepo - Use `@zenstackhq/runtime/models` to import logical prisma client types - Except for zod plugin, when generating into default location, still use ".zenstack/models" to avoid cyclic dependencies - Only run prisma client generator when generating for logical schema --- .../src/plugins/enhancer/enhance/index.ts | 99 +++++++++++++------ packages/schema/src/plugins/enhancer/index.ts | 4 +- packages/schema/src/plugins/zod/generator.ts | 14 ++- .../schema/src/plugins/zod/transformer.ts | 5 +- 4 files changed, 88 insertions(+), 34 deletions(-) diff --git a/packages/schema/src/plugins/enhancer/enhance/index.ts b/packages/schema/src/plugins/enhancer/enhance/index.ts index 680bf11dd..0425b76d0 100644 --- a/packages/schema/src/plugins/enhancer/enhance/index.ts +++ b/packages/schema/src/plugins/enhancer/enhance/index.ts @@ -5,6 +5,7 @@ import { getAttributeArg, getAuthModel, getDataModels, + getLiteral, isDelegateModel, type PluginOptions, } from '@zenstackhq/sdk'; @@ -14,12 +15,14 @@ import { ReferenceExpr, isArrayExpr, isDataModel, + isGeneratorDecl, isReferenceExpr, type Model, } from '@zenstackhq/sdk/ast'; -import { getDMMF, getPrismaClientImportSpec, type DMMF } from '@zenstackhq/sdk/prisma'; +import { getDMMF, getPrismaClientImportSpec, getPrismaVersion, type DMMF } from '@zenstackhq/sdk/prisma'; import fs from 'fs'; import path from 'path'; +import semver from 'semver'; import { FunctionDeclarationStructure, InterfaceDeclaration, @@ -42,6 +45,8 @@ import { generateAuthType } from './auth-type-generator'; // information of delegate models and their sub models type DelegateInfo = [DataModel, DataModel[]][]; +const LOGICAL_CLIENT_GENERATION_PATH = './.logical-prisma-client'; + export class EnhancerGenerator { constructor( private readonly model: Model, @@ -60,7 +65,7 @@ export class EnhancerGenerator { // schema contains delegate models, need to generate a logical prisma schema const result = await this.generateLogicalPrisma(); - logicalPrismaClientDir = './.logical-prisma-client'; + logicalPrismaClientDir = LOGICAL_CLIENT_GENERATION_PATH; dmmf = result.dmmf; // create a reexport of the logical prisma client @@ -190,40 +195,76 @@ export function enhance(prisma: any, context?: EnhancementContext<${authTypePara private async generateLogicalPrisma() { const prismaGenerator = new PrismaSchemaGenerator(this.model); - const prismaClientOutDir = './.logical-prisma-client'; - const logicalPrismaFile = path.join(this.outDir, 'logical.prisma'); - await prismaGenerator.generate({ - provider: '@internal', // doesn't matter - schemaPath: this.options.schemaPath, - output: logicalPrismaFile, - overrideClientGenerationPath: prismaClientOutDir, - mode: 'logical', - }); - // generate the prisma client - const generateCmd = `prisma generate --schema "${logicalPrismaFile}" --no-engine`; + // dir of the zmodel file + const zmodelDir = path.dirname(this.options.schemaPath); + + // generate a temp logical prisma schema in zmodel's dir + const logicalPrismaFile = path.join(zmodelDir, `logical-${Date.now()}.prisma`); + + // calculate a relative output path to output the logical prisma client into enhancer's output dir + const prismaClientOutDir = path.join(path.relative(zmodelDir, this.outDir), LOGICAL_CLIENT_GENERATION_PATH); try { - // run 'prisma generate' - await execPackage(generateCmd, { stdio: 'ignore' }); - } catch { - await trackPrismaSchemaError(logicalPrismaFile); + await prismaGenerator.generate({ + provider: '@internal', // doesn't matter + schemaPath: this.options.schemaPath, + output: logicalPrismaFile, + overrideClientGenerationPath: prismaClientOutDir, + mode: 'logical', + }); + + // generate the prisma client + + // only run prisma client generator for the logical schema + const prismaClientGeneratorName = this.getPrismaClientGeneratorName(this.model); + let generateCmd = `prisma generate --schema "${logicalPrismaFile}" --generator=${prismaClientGeneratorName}`; + + const prismaVersion = getPrismaVersion(); + if (!prismaVersion || semver.gte(prismaVersion, '5.2.0')) { + // add --no-engine to reduce generation size if the prisma version supports + generateCmd += ' --no-engine'; + } + try { - // run 'prisma generate' again with output to the console - await execPackage(generateCmd); + // run 'prisma generate' + await execPackage(generateCmd, { stdio: 'ignore' }); } catch { - // noop + await trackPrismaSchemaError(logicalPrismaFile); + try { + // run 'prisma generate' again with output to the console + await execPackage(generateCmd); + } catch { + // noop + } + throw new PluginError(name, `Failed to run "prisma generate" on logical schema: ${logicalPrismaFile}`); } - throw new PluginError(name, `Failed to run "prisma generate" on logical schema: ${logicalPrismaFile}`); - } - // make a bunch of typing fixes to the generated prisma client - await this.processClientTypes(path.join(this.outDir, prismaClientOutDir)); + // make a bunch of typing fixes to the generated prisma client + await this.processClientTypes(path.join(this.outDir, LOGICAL_CLIENT_GENERATION_PATH)); + + return { + prismaSchema: logicalPrismaFile, + // load the dmmf of the logical prisma schema + dmmf: await getDMMF({ datamodel: fs.readFileSync(logicalPrismaFile, { encoding: 'utf-8' }) }), + }; + } finally { + if (fs.existsSync(logicalPrismaFile)) { + fs.rmSync(logicalPrismaFile); + } + } + } - return { - prismaSchema: logicalPrismaFile, - // load the dmmf of the logical prisma schema - dmmf: await getDMMF({ datamodel: fs.readFileSync(logicalPrismaFile, { encoding: 'utf-8' }) }), - }; + private getPrismaClientGeneratorName(model: Model) { + for (const generator of model.declarations.filter(isGeneratorDecl)) { + if ( + generator.fields.some( + (f) => f.name === 'provider' && getLiteral(f.value) === 'prisma-client-js' + ) + ) { + return generator.name; + } + } + throw new PluginError(name, `Cannot find prisma-client-js generator in the schema`); } private async processClientTypes(prismaClientDir: string) { diff --git a/packages/schema/src/plugins/enhancer/index.ts b/packages/schema/src/plugins/enhancer/index.ts index 0c82acfba..79e8fd6e6 100644 --- a/packages/schema/src/plugins/enhancer/index.ts +++ b/packages/schema/src/plugins/enhancer/index.ts @@ -1,4 +1,4 @@ -import { PluginError, createProject, resolvePath, type PluginFunction } from '@zenstackhq/sdk'; +import { PluginError, RUNTIME_PACKAGE, createProject, resolvePath, type PluginFunction } from '@zenstackhq/sdk'; import path from 'path'; import { getDefaultOutputFolder } from '../plugin-utils'; import { EnhancerGenerator } from './enhance'; @@ -31,7 +31,7 @@ const run: PluginFunction = async (model, options, _dmmf, globalOptions) => { // resolve it relative to the schema path prismaClientPath = path.relative(path.dirname(options.schemaPath), prismaClientPathAbs); } else { - prismaClientPath = `.zenstack/models`; + prismaClientPath = `${RUNTIME_PACKAGE}/models`; } } diff --git a/packages/schema/src/plugins/zod/generator.ts b/packages/schema/src/plugins/zod/generator.ts index f2f628b30..a9b963d30 100644 --- a/packages/schema/src/plugins/zod/generator.ts +++ b/packages/schema/src/plugins/zod/generator.ts @@ -1,6 +1,7 @@ import { PluginGlobalOptions, PluginOptions, + RUNTIME_PACKAGE, ensureEmptyDir, getDataModels, hasAttribute, @@ -279,7 +280,7 @@ export class ZodSchemaGenerator { } } if (importEnums.size > 0) { - const prismaImport = getPrismaClientImportSpec(path.join(output, 'models'), this.options); + const prismaImport = computePrismaClientImport(path.join(output, 'models'), this.options); writer.writeLine(`import { ${[...importEnums].join(', ')} } from '${prismaImport}';`); } @@ -581,3 +582,14 @@ export const ${upperCaseFirst(model.name)}UpdateSchema = ${updateSchema}; return `${schema}.passthrough()`; } } + +export function computePrismaClientImport(importingFrom: string, options: PluginOptions) { + let importPath = getPrismaClientImportSpec(importingFrom, options); + if (importPath.startsWith(RUNTIME_PACKAGE) && !options.output) { + // default import from `@zenstackhq/runtime` and this plugin is generating + // into default location, we should correct the prisma client import into a + // importing from `.zenstack` to avoid cyclic dependencies with runtime + importPath = importPath.replace(RUNTIME_PACKAGE, '.zenstack'); + } + return importPath; +} diff --git a/packages/schema/src/plugins/zod/transformer.ts b/packages/schema/src/plugins/zod/transformer.ts index 86829f1ca..4fb04cdcc 100644 --- a/packages/schema/src/plugins/zod/transformer.ts +++ b/packages/schema/src/plugins/zod/transformer.ts @@ -1,10 +1,11 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ import { indentString, type PluginOptions } from '@zenstackhq/sdk'; import { checkModelHasModelRelation, findModelByName, isAggregateInputType } from '@zenstackhq/sdk/dmmf-helpers'; -import { getPrismaClientImportSpec, type DMMF as PrismaDMMF } from '@zenstackhq/sdk/prisma'; +import { type DMMF as PrismaDMMF } from '@zenstackhq/sdk/prisma'; import path from 'path'; import type { Project, SourceFile } from 'ts-morph'; import { upperCaseFirst } from 'upper-case-first'; +import { computePrismaClientImport } from './generator'; import { AggregateOperationSupport, TransformerParams } from './types'; export default class Transformer { @@ -290,7 +291,7 @@ export const ${this.name}ObjectSchema: SchemaType = ${schema} as SchemaType;`; } generateImportPrismaStatement(options: PluginOptions) { - const prismaClientImportPath = getPrismaClientImportSpec( + const prismaClientImportPath = computePrismaClientImport( path.resolve(Transformer.outputPath, './objects'), options );