From 577304971e79a79949f98a7fa53031186f6ce72d Mon Sep 17 00:00:00 2001 From: Eugen Istoc Date: Fri, 4 Jul 2025 20:43:41 -0400 Subject: [PATCH 1/7] fix: improve support for new `prisma-client` generator --- .../src/plugins/enhancer/enhance/index.ts | 58 ++++++++++++++++++- 1 file changed, 57 insertions(+), 1 deletion(-) diff --git a/packages/schema/src/plugins/enhancer/enhance/index.ts b/packages/schema/src/plugins/enhancer/enhance/index.ts index f30d756cc..c1e79bdab 100644 --- a/packages/schema/src/plugins/enhancer/enhance/index.ts +++ b/packages/schema/src/plugins/enhancer/enhance/index.ts @@ -121,9 +121,13 @@ export class EnhancerGenerator { } // reexport PrismaClient types (original or fixed) + const modelsTsContent = `export * from '${resultPrismaTypeImport}';${ + this.isNewPrismaClientGenerator ? '\nexport * from \'./json-fields\';' : '' + }`; + const modelsTs = this.project.createSourceFile( path.join(this.outDir, 'models.ts'), - `export * from '${resultPrismaTypeImport}';`, + modelsTsContent, { overwrite: true } ); this.saveSourceFile(modelsTs); @@ -490,6 +494,20 @@ export type Enhanced = private async processClientTypesNewPrismaGenerator(prismaClientDir: string, delegateInfo: DelegateInfo) { const project = new Project(); + // Create a shared json-fields.ts file for all type definitions + const jsonFieldsFile = project.createSourceFile(path.join(this.outDir, 'json-fields.ts'), undefined, { + overwrite: true, + }); + + // Generate all type definitions in the shared file + for (const decl of this.model.declarations) { + if (isTypeDef(decl)) { + generateTypeDefType(jsonFieldsFile, decl); + } + } + + await jsonFieldsFile.save(); + for (const d of this.model.declarations.filter(isDataModel)) { const fileName = `${prismaClientDir}/models/${d.name}.ts`; const sf = project.addSourceFileAtPath(fileName); @@ -502,6 +520,44 @@ export type Enhanced = throw new PluginError(name, `Unexpected syntax list structure in ${fileName}`); } + // Check if $Types is used in any type aliases (before and after transformation) + let needsTypesImport = false; + syntaxList.getChildren().forEach((node) => { + if (Node.isTypeAliasDeclaration(node)) { + // Check original type + const typeText = node.getType().getText(); + if (typeText.includes('$Types')) { + needsTypesImport = true; + } + + // Check if transformation would add $Types + const structure = this.transformTypeAlias(node, delegateInfo); + if (structure.type && typeof structure.type === 'string' && structure.type.includes('$Types')) { + needsTypesImport = true; + } + } + }); + + // Add $Types import if needed + if (needsTypesImport) { + sfNew.addStatements('import $Types = runtime.Types;'); + } + + // Add import for json-fields if this model has JSON type fields + const modelWithJsonFields = this.modelsWithJsonTypeFields.find((m) => m.name === d.name); + if (modelWithJsonFields) { + // Get the specific types that are used in this model + const getTypedJsonFields = (model: DataModel) => { + return model.fields.filter((f) => isTypeDef(f.type.reference?.ref)); + }; + const jsonFieldTypes = getTypedJsonFields(modelWithJsonFields); + const typeNames = jsonFieldTypes.map(field => field.type.reference!.$refText); + + if (typeNames.length > 0) { + sfNew.addStatements(`import type { ${typeNames.join(', ')} } from "../../json-fields";`); + } + } + syntaxList.getChildren().forEach((node) => { if (Node.isInterfaceDeclaration(node)) { sfNew.addInterface(this.transformInterface(node, delegateInfo)); From 3436488e59133937d27791271db9dbbeafe18c0e Mon Sep 17 00:00:00 2001 From: Eugen Istoc Date: Fri, 4 Jul 2025 22:54:46 -0400 Subject: [PATCH 2/7] refactor: always add $Types --- .../src/plugins/enhancer/enhance/index.ts | 39 ++++--------------- 1 file changed, 8 insertions(+), 31 deletions(-) diff --git a/packages/schema/src/plugins/enhancer/enhance/index.ts b/packages/schema/src/plugins/enhancer/enhance/index.ts index c1e79bdab..644ece690 100644 --- a/packages/schema/src/plugins/enhancer/enhance/index.ts +++ b/packages/schema/src/plugins/enhancer/enhance/index.ts @@ -122,14 +122,12 @@ export class EnhancerGenerator { // reexport PrismaClient types (original or fixed) const modelsTsContent = `export * from '${resultPrismaTypeImport}';${ - this.isNewPrismaClientGenerator ? '\nexport * from \'./json-fields\';' : '' + this.isNewPrismaClientGenerator ? "\nexport * from './json-fields';" : '' }`; - - const modelsTs = this.project.createSourceFile( - path.join(this.outDir, 'models.ts'), - modelsTsContent, - { overwrite: true } - ); + + const modelsTs = this.project.createSourceFile(path.join(this.outDir, 'models.ts'), modelsTsContent, { + overwrite: true, + }); this.saveSourceFile(modelsTs); const authDecl = getAuthDecl(getDataModelAndTypeDefs(this.model)); @@ -520,28 +518,7 @@ export type Enhanced = throw new PluginError(name, `Unexpected syntax list structure in ${fileName}`); } - // Check if $Types is used in any type aliases (before and after transformation) - let needsTypesImport = false; - syntaxList.getChildren().forEach((node) => { - if (Node.isTypeAliasDeclaration(node)) { - // Check original type - const typeText = node.getType().getText(); - if (typeText.includes('$Types')) { - needsTypesImport = true; - } - - // Check if transformation would add $Types - const structure = this.transformTypeAlias(node, delegateInfo); - if (structure.type && typeof structure.type === 'string' && structure.type.includes('$Types')) { - needsTypesImport = true; - } - } - }); - - // Add $Types import if needed - if (needsTypesImport) { - sfNew.addStatements('import $Types = runtime.Types;'); - } + sfNew.addStatements('import $Types = runtime.Types;'); // Add import for json-fields if this model has JSON type fields const modelWithJsonFields = this.modelsWithJsonTypeFields.find((m) => m.name === d.name); @@ -551,8 +528,8 @@ export type Enhanced = return model.fields.filter((f) => isTypeDef(f.type.reference?.ref)); }; const jsonFieldTypes = getTypedJsonFields(modelWithJsonFields); - const typeNames = jsonFieldTypes.map(field => field.type.reference!.$refText); - + const typeNames = jsonFieldTypes.map((field) => field.type.reference!.$refText); + if (typeNames.length > 0) { sfNew.addStatements(`import type { ${typeNames.join(', ')} } from "../../json-fields";`); } From 3e7e85da8d21f9302dd198bd8cc6d4383c3bbe02 Mon Sep 17 00:00:00 2001 From: Eugen Istoc Date: Fri, 4 Jul 2025 22:57:42 -0400 Subject: [PATCH 3/7] fix: ensure unique type names for JSON fields in enhance function --- packages/schema/src/plugins/enhancer/enhance/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/schema/src/plugins/enhancer/enhance/index.ts b/packages/schema/src/plugins/enhancer/enhance/index.ts index 644ece690..8be743e3b 100644 --- a/packages/schema/src/plugins/enhancer/enhance/index.ts +++ b/packages/schema/src/plugins/enhancer/enhance/index.ts @@ -528,7 +528,7 @@ export type Enhanced = return model.fields.filter((f) => isTypeDef(f.type.reference?.ref)); }; const jsonFieldTypes = getTypedJsonFields(modelWithJsonFields); - const typeNames = jsonFieldTypes.map((field) => field.type.reference!.$refText); + const typeNames = [...new Set(jsonFieldTypes.map((field) => field.type.reference!.$refText))]; if (typeNames.length > 0) { sfNew.addStatements(`import type { ${typeNames.join(', ')} } from "../../json-fields";`); From 9c650e6b71c16932b7381d1786d2011cbc151098 Mon Sep 17 00:00:00 2001 From: Eugen Istoc Date: Fri, 4 Jul 2025 23:07:23 -0400 Subject: [PATCH 4/7] chore: enhance comments for clarity on JSON fields type definitions --- packages/schema/src/plugins/enhancer/enhance/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/schema/src/plugins/enhancer/enhance/index.ts b/packages/schema/src/plugins/enhancer/enhance/index.ts index 8be743e3b..ba39c45e4 100644 --- a/packages/schema/src/plugins/enhancer/enhance/index.ts +++ b/packages/schema/src/plugins/enhancer/enhance/index.ts @@ -492,12 +492,12 @@ export type Enhanced = private async processClientTypesNewPrismaGenerator(prismaClientDir: string, delegateInfo: DelegateInfo) { const project = new Project(); - // Create a shared json-fields.ts file for all type definitions + // Create a shared json-fields.ts file for all JSON fields type definitions const jsonFieldsFile = project.createSourceFile(path.join(this.outDir, 'json-fields.ts'), undefined, { overwrite: true, }); - // Generate all type definitions in the shared file + // Generate all JSON fields type definitions in the shared file for (const decl of this.model.declarations) { if (isTypeDef(decl)) { generateTypeDefType(jsonFieldsFile, decl); From 146fa96e080f20bde1250853c610e27d67e40e9c Mon Sep 17 00:00:00 2001 From: Eugen Istoc Date: Fri, 4 Jul 2025 23:15:13 -0400 Subject: [PATCH 5/7] refactor: update JSON fields type definitions handling in enhance function --- .../schema/src/plugins/enhancer/enhance/index.ts | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/packages/schema/src/plugins/enhancer/enhance/index.ts b/packages/schema/src/plugins/enhancer/enhance/index.ts index ba39c45e4..84c44bede 100644 --- a/packages/schema/src/plugins/enhancer/enhance/index.ts +++ b/packages/schema/src/plugins/enhancer/enhance/index.ts @@ -492,18 +492,12 @@ export type Enhanced = private async processClientTypesNewPrismaGenerator(prismaClientDir: string, delegateInfo: DelegateInfo) { const project = new Project(); - // Create a shared json-fields.ts file for all JSON fields type definitions - const jsonFieldsFile = project.createSourceFile(path.join(this.outDir, 'json-fields.ts'), undefined, { + // Create a shared file for all JSON fields type definitions + const jsonFieldsFile = project.createSourceFile(path.join(this.outDir, 'json-fields.d.ts'), undefined, { overwrite: true, }); - // Generate all JSON fields type definitions in the shared file - for (const decl of this.model.declarations) { - if (isTypeDef(decl)) { - generateTypeDefType(jsonFieldsFile, decl); - } - } - + await this.generateExtraTypes(jsonFieldsFile); await jsonFieldsFile.save(); for (const d of this.model.declarations.filter(isDataModel)) { From e3421bcd1ecdff7c704ade0978551d70cf396e46 Mon Sep 17 00:00:00 2001 From: Eugen Istoc Date: Fri, 4 Jul 2025 23:17:31 -0400 Subject: [PATCH 6/7] refactor: change JSON fields type definitions file extension from .d.ts to .ts --- packages/schema/src/plugins/enhancer/enhance/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/schema/src/plugins/enhancer/enhance/index.ts b/packages/schema/src/plugins/enhancer/enhance/index.ts index 84c44bede..e4e35bd42 100644 --- a/packages/schema/src/plugins/enhancer/enhance/index.ts +++ b/packages/schema/src/plugins/enhancer/enhance/index.ts @@ -493,7 +493,7 @@ export type Enhanced = const project = new Project(); // Create a shared file for all JSON fields type definitions - const jsonFieldsFile = project.createSourceFile(path.join(this.outDir, 'json-fields.d.ts'), undefined, { + const jsonFieldsFile = project.createSourceFile(path.join(this.outDir, 'json-fields.ts'), undefined, { overwrite: true, }); From 42e5dfe43cc792a7216e4609d6c543be5081deef Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Mon, 7 Jul 2025 11:04:37 +0800 Subject: [PATCH 7/7] Minor tweaks - Change the file name containing json types from "json-fields.ts" to "json-types.ts" - Fixed the import path of `DynamicClientExtensionThis` etc. to "@prisma/client/runtime/library" - Removed an unnecessary async modifier - Added a regression test case --- .../src/plugins/enhancer/enhance/index.ts | 16 +++---- tests/regression/tests/issue-2168.test.ts | 46 +++++++++++++++++++ 2 files changed, 54 insertions(+), 8 deletions(-) create mode 100644 tests/regression/tests/issue-2168.test.ts diff --git a/packages/schema/src/plugins/enhancer/enhance/index.ts b/packages/schema/src/plugins/enhancer/enhance/index.ts index e4e35bd42..b439a61fb 100644 --- a/packages/schema/src/plugins/enhancer/enhance/index.ts +++ b/packages/schema/src/plugins/enhancer/enhance/index.ts @@ -122,7 +122,7 @@ export class EnhancerGenerator { // reexport PrismaClient types (original or fixed) const modelsTsContent = `export * from '${resultPrismaTypeImport}';${ - this.isNewPrismaClientGenerator ? "\nexport * from './json-fields';" : '' + this.isNewPrismaClientGenerator ? "\nexport * from './json-types';" : '' }`; const modelsTs = this.project.createSourceFile(path.join(this.outDir, 'models.ts'), modelsTsContent, { @@ -243,7 +243,7 @@ export function enhance(prisma: DbClient, context?: Enh private createLogicalPrismaImports(prismaImport: string, prismaClientImport: string, target: string) { const prismaTargetImport = target === 'edge' ? `${prismaImport}/edge` : prismaImport; return `import { Prisma as _Prisma, PrismaClient as _PrismaClient } from '${prismaTargetImport}'; -import type { InternalArgs, DynamicClientExtensionThis } from '${prismaImport}/runtime/library'; +import type { InternalArgs, DynamicClientExtensionThis } from '@prisma/client/runtime/library'; import type * as _P from '${prismaClientImport}'; import type { Prisma, PrismaClient } from '${prismaClientImport}'; export type { PrismaClient }; @@ -493,12 +493,12 @@ export type Enhanced = const project = new Project(); // Create a shared file for all JSON fields type definitions - const jsonFieldsFile = project.createSourceFile(path.join(this.outDir, 'json-fields.ts'), undefined, { + const jsonFieldsFile = project.createSourceFile(path.join(this.outDir, 'json-types.ts'), undefined, { overwrite: true, }); - await this.generateExtraTypes(jsonFieldsFile); - await jsonFieldsFile.save(); + this.generateExtraTypes(jsonFieldsFile); + await saveSourceFile(jsonFieldsFile); for (const d of this.model.declarations.filter(isDataModel)) { const fileName = `${prismaClientDir}/models/${d.name}.ts`; @@ -514,7 +514,7 @@ export type Enhanced = sfNew.addStatements('import $Types = runtime.Types;'); - // Add import for json-fields if this model has JSON type fields + // Add import for json-types if this model has JSON type fields const modelWithJsonFields = this.modelsWithJsonTypeFields.find((m) => m.name === d.name); if (modelWithJsonFields) { // Get the specific types that are used in this model @@ -525,7 +525,7 @@ export type Enhanced = const typeNames = [...new Set(jsonFieldTypes.map((field) => field.type.reference!.$refText))]; if (typeNames.length > 0) { - sfNew.addStatements(`import type { ${typeNames.join(', ')} } from "../../json-fields";`); + sfNew.addStatements(`import type { ${typeNames.join(', ')} } from "../../json-types";`); } } @@ -940,7 +940,7 @@ export type Enhanced = return source; } - private async generateExtraTypes(sf: SourceFile) { + private generateExtraTypes(sf: SourceFile) { for (const decl of this.model.declarations) { if (isTypeDef(decl)) { generateTypeDefType(sf, decl); diff --git a/tests/regression/tests/issue-2168.test.ts b/tests/regression/tests/issue-2168.test.ts new file mode 100644 index 000000000..b588e1a60 --- /dev/null +++ b/tests/regression/tests/issue-2168.test.ts @@ -0,0 +1,46 @@ +import { loadSchema } from '@zenstackhq/testtools'; + +describe('issue 2168', () => { + it('regression', async () => { + await loadSchema( + ` + datasource db { + provider = "sqlite" + url = "file:./test.db" + + } + + generator client { + provider = "prisma-client" + output = "../generated/prisma" + moduleFormat = "cjs" + } + + model User { + id Int @id + profile Profile @json + } + + type Profile { + age Int + } + `, + { + compile: true, + addPrelude: false, + output: './generated/zenstack', + prismaLoadPath: './generated/prisma/client', + extraSourceFiles: [ + { + name: 'main.ts', + content: ` +import type { Profile } from './generated/zenstack/models'; +const profile: Profile = { age: 18 }; +console.log(profile); +`, + }, + ], + } + ); + }); +});