diff --git a/package.json b/package.json index c8e5a4e11..d10d39da9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zenstack-monorepo", - "version": "2.16.1", + "version": "2.17.0", "description": "", "scripts": { "build": "pnpm -r --filter=\"!./packages/ide/*\" build", diff --git a/packages/ide/jetbrains/build.gradle.kts b/packages/ide/jetbrains/build.gradle.kts index 929cfa728..f234ffb46 100644 --- a/packages/ide/jetbrains/build.gradle.kts +++ b/packages/ide/jetbrains/build.gradle.kts @@ -9,7 +9,7 @@ plugins { } group = "dev.zenstack" -version = "2.16.1" +version = "2.17.0" repositories { mavenCentral() diff --git a/packages/ide/jetbrains/package.json b/packages/ide/jetbrains/package.json index 8a37457f7..21b2deb68 100644 --- a/packages/ide/jetbrains/package.json +++ b/packages/ide/jetbrains/package.json @@ -1,6 +1,6 @@ { "name": "jetbrains", - "version": "2.16.1", + "version": "2.17.0", "displayName": "ZenStack JetBrains IDE Plugin", "description": "ZenStack JetBrains IDE plugin", "homepage": "https://zenstack.dev", diff --git a/packages/language/package.json b/packages/language/package.json index 30d056063..343d9cad3 100644 --- a/packages/language/package.json +++ b/packages/language/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/language", - "version": "2.16.1", + "version": "2.17.0", "displayName": "ZenStack modeling language compiler", "description": "ZenStack modeling language compiler", "homepage": "https://zenstack.dev", diff --git a/packages/misc/redwood/package.json b/packages/misc/redwood/package.json index a7a3d2415..a8fc42e98 100644 --- a/packages/misc/redwood/package.json +++ b/packages/misc/redwood/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/redwood", "displayName": "ZenStack RedwoodJS Integration", - "version": "2.16.1", + "version": "2.17.0", "description": "CLI and runtime for integrating ZenStack with RedwoodJS projects.", "repository": { "type": "git", diff --git a/packages/plugins/openapi/package.json b/packages/plugins/openapi/package.json index 90a2985c0..7428083c9 100644 --- a/packages/plugins/openapi/package.json +++ b/packages/plugins/openapi/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/openapi", "displayName": "ZenStack Plugin and Runtime for OpenAPI", - "version": "2.16.1", + "version": "2.17.0", "description": "ZenStack plugin and runtime supporting OpenAPI", "main": "index.js", "repository": { diff --git a/packages/plugins/openapi/src/rest-generator.ts b/packages/plugins/openapi/src/rest-generator.ts index 5a1344c2b..30e2607ae 100644 --- a/packages/plugins/openapi/src/rest-generator.ts +++ b/packages/plugins/openapi/src/rest-generator.ts @@ -44,6 +44,7 @@ type Policies = ReturnType; */ export class RESTfulOpenAPIGenerator extends OpenAPIGeneratorBase { private warnings: string[] = []; + private modelNameMapping: Record; constructor(protected model: Model, protected options: PluginOptions, protected dmmf: DMMF.Document) { super(model, options, dmmf); @@ -51,6 +52,8 @@ export class RESTfulOpenAPIGenerator extends OpenAPIGeneratorBase { if (this.options.omitInputDetails !== undefined) { throw new PluginError(name, '"omitInputDetails" option is not supported for "rest" flavor'); } + + this.modelNameMapping = this.getOption('modelNameMapping', {} as Record); } generate() { @@ -126,6 +129,10 @@ export class RESTfulOpenAPIGenerator extends OpenAPIGeneratorBase { return result; } + private mapModelName(modelName: string): string { + return this.modelNameMapping[modelName] ?? modelName; + } + private generatePathsForModel(model: DMMF.Model, zmodel: DataModel): OAPI.PathItemObject | undefined { const result: Record = {}; @@ -139,9 +146,11 @@ export class RESTfulOpenAPIGenerator extends OpenAPIGeneratorBase { const resourceMeta = getModelResourceMeta(zmodel); + const modelName = this.mapModelName(model.name); + // GET /resource // POST /resource - result[`${prefix}/${lowerCaseFirst(model.name)}`] = { + result[`${prefix}/${lowerCaseFirst(modelName)}`] = { get: this.makeResourceList(zmodel, policies, resourceMeta), post: this.makeResourceCreate(zmodel, policies, resourceMeta), }; @@ -150,10 +159,10 @@ export class RESTfulOpenAPIGenerator extends OpenAPIGeneratorBase { // PUT /resource/{id} // PATCH /resource/{id} // DELETE /resource/{id} - result[`${prefix}/${lowerCaseFirst(model.name)}/{id}`] = { + result[`${prefix}/${lowerCaseFirst(modelName)}/{id}`] = { get: this.makeResourceFetch(zmodel, policies, resourceMeta), - put: this.makeResourceUpdate(zmodel, policies, `update-${model.name}-put`, resourceMeta), - patch: this.makeResourceUpdate(zmodel, policies, `update-${model.name}-patch`, resourceMeta), + put: this.makeResourceUpdate(zmodel, policies, `update-${modelName}-put`, resourceMeta), + patch: this.makeResourceUpdate(zmodel, policies, `update-${modelName}-patch`, resourceMeta), delete: this.makeResourceDelete(zmodel, policies, resourceMeta), }; @@ -165,14 +174,14 @@ export class RESTfulOpenAPIGenerator extends OpenAPIGeneratorBase { } // GET /resource/{id}/{relationship} - const relatedDataPath = `${prefix}/${lowerCaseFirst(model.name)}/{id}/${field.name}`; + const relatedDataPath = `${prefix}/${lowerCaseFirst(modelName)}/{id}/${field.name}`; let container = result[relatedDataPath]; if (!container) { container = result[relatedDataPath] = {}; } container.get = this.makeRelatedFetch(zmodel, field, relationDecl, resourceMeta); - const relationshipPath = `${prefix}/${lowerCaseFirst(model.name)}/{id}/relationships/${field.name}`; + const relationshipPath = `${prefix}/${lowerCaseFirst(modelName)}/{id}/relationships/${field.name}`; container = result[relationshipPath]; if (!container) { container = result[relationshipPath] = {}; diff --git a/packages/plugins/openapi/tests/openapi-restful.test.ts b/packages/plugins/openapi/tests/openapi-restful.test.ts index a66f45941..bde8ea53c 100644 --- a/packages/plugins/openapi/tests/openapi-restful.test.ts +++ b/packages/plugins/openapi/tests/openapi-restful.test.ts @@ -129,7 +129,7 @@ model Bar { } }); - it('options', async () => { + it('common options', async () => { const { model, dmmf, modelFile } = await loadZModelAndDmmf(` plugin openapi { provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' @@ -396,6 +396,39 @@ model User { expect.arrayContaining(['role', 'company']) ); }); + + it('works with mapped model name', async () => { + const { model, dmmf, modelFile } = await loadZModelAndDmmf(` +plugin openapi { + provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' + title = 'My Awesome API' + prefix = '/api' + modelNameMapping = { + User: 'myUser' + } +} + +model User { + id String @id + posts Post[] +} + +model Post { + id String @id + author User @relation(fields: [authorId], references: [id]) + authorId String +} + `); + + const { name: output } = tmp.fileSync({ postfix: '.yaml' }); + const options = buildOptions(model, modelFile, output); + await generate(model, options, dmmf); + console.log('OpenAPI specification generated:', output); + const api = await OpenAPIParser.validate(output); + expect(api.paths?.['/api/myUser']).toBeTruthy(); + expect(api.paths?.['/api/user']).toBeFalsy(); + expect(api.paths?.['/api/post']).toBeTruthy(); + }); }); function buildOptions(model: Model, modelFile: string, output: string, specVersion = '3.0.0') { diff --git a/packages/plugins/swr/package.json b/packages/plugins/swr/package.json index 2030a3b63..fa87079e2 100644 --- a/packages/plugins/swr/package.json +++ b/packages/plugins/swr/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/swr", "displayName": "ZenStack plugin for generating SWR hooks", - "version": "2.16.1", + "version": "2.17.0", "description": "ZenStack plugin for generating SWR hooks", "main": "index.js", "repository": { diff --git a/packages/plugins/tanstack-query/package.json b/packages/plugins/tanstack-query/package.json index 0d56d7ad8..8ca523bf6 100644 --- a/packages/plugins/tanstack-query/package.json +++ b/packages/plugins/tanstack-query/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/tanstack-query", "displayName": "ZenStack plugin for generating tanstack-query hooks", - "version": "2.16.1", + "version": "2.17.0", "description": "ZenStack plugin for generating tanstack-query hooks", "main": "index.js", "exports": { diff --git a/packages/plugins/trpc/package.json b/packages/plugins/trpc/package.json index da211d057..dbc709268 100644 --- a/packages/plugins/trpc/package.json +++ b/packages/plugins/trpc/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/trpc", "displayName": "ZenStack plugin for tRPC", - "version": "2.16.1", + "version": "2.17.0", "description": "ZenStack plugin for tRPC", "main": "index.js", "repository": { diff --git a/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/package.json b/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/package.json index 45b49215d..a1dab5873 100644 --- a/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/package.json +++ b/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/package.json @@ -10,7 +10,7 @@ "postinstall": "nuxt prepare" }, "dependencies": { - "@prisma/client": "6.10.x", + "@prisma/client": "6.11.x", "@trpc/client": "^10.45.2", "@trpc/server": "^10.45.2", "nuxt": "^3.14.1592", @@ -21,7 +21,7 @@ }, "devDependencies": { "esbuild": "^0.24.0", - "prisma": "6.10.x", + "prisma": "6.11.x", "typescript": "^5.6.2", "vue-tsc": "^2.1.10" } diff --git a/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/package.json b/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/package.json index e8be9a741..09909ffc3 100644 --- a/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/package.json +++ b/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/package.json @@ -10,7 +10,7 @@ "postinstall": "nuxt prepare" }, "dependencies": { - "@prisma/client": "6.10.x", + "@prisma/client": "6.11.x", "@trpc/client": "^11.0.0-rc.563", "@trpc/server": "^11.0.0-rc.563", "nuxt": "^3.14.1592", @@ -21,7 +21,7 @@ }, "devDependencies": { "esbuild": "^0.24.0", - "prisma": "6.10.x", + "prisma": "6.11.x", "typescript": "^5.6.2", "vue-tsc": "^2.1.10" } diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v11/package.json b/packages/plugins/trpc/tests/projects/t3-trpc-v11/package.json index 3b3d17c27..87acfbce8 100644 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v11/package.json +++ b/packages/plugins/trpc/tests/projects/t3-trpc-v11/package.json @@ -15,7 +15,7 @@ "start": "next start" }, "dependencies": { - "@prisma/client": "6.10.x", + "@prisma/client": "6.11.x", "@t3-oss/env-nextjs": "^0.10.1", "@tanstack/react-query": "^5.50.0", "@trpc/client": "^11.0.0-rc.446", @@ -39,7 +39,7 @@ "@typescript-eslint/parser": "^8.1.0", "eslint": "^8.57.0", "eslint-config-next": "^14.2.4", - "prisma": "6.10.x", + "prisma": "6.11.x", "typescript": "^5.5.3" }, "ct3aMetadata": { diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 1819994dd..367aaa856 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/runtime", "displayName": "ZenStack Runtime Library", - "version": "2.16.1", + "version": "2.17.0", "description": "Runtime of ZenStack for both client-side and server-side environments.", "repository": { "type": "git", @@ -114,7 +114,7 @@ "zod-validation-error": "^1.5.0" }, "peerDependencies": { - "@prisma/client": "5.0.0 - 6.10.x" + "@prisma/client": "5.0.0 - 6.11.x" }, "author": { "name": "ZenStack Team" diff --git a/packages/schema/package.json b/packages/schema/package.json index 22990b8d5..82dc110d2 100644 --- a/packages/schema/package.json +++ b/packages/schema/package.json @@ -3,7 +3,7 @@ "publisher": "zenstack", "displayName": "ZenStack Language Tools", "description": "FullStack enhancement for Prisma ORM: seamless integration from database to UI", - "version": "2.16.1", + "version": "2.17.0", "author": { "name": "ZenStack Team" }, @@ -118,10 +118,10 @@ "zod-validation-error": "^1.5.0" }, "peerDependencies": { - "prisma": "5.0.0 - 6.10.x" + "prisma": "5.0.0 - 6.11.x" }, "devDependencies": { - "@prisma/client": "6.10.x", + "@prisma/client": "6.11.x", "@types/async-exit-hook": "^2.0.0", "@types/pluralize": "^0.0.29", "@types/semver": "^7.3.13", diff --git a/packages/schema/src/plugins/enhancer/enhance/index.ts b/packages/schema/src/plugins/enhancer/enhance/index.ts index b439a61fb..8e6e3719d 100644 --- a/packages/schema/src/plugins/enhancer/enhance/index.ts +++ b/packages/schema/src/plugins/enhancer/enhance/index.ts @@ -1,4 +1,5 @@ import { DELEGATE_AUX_RELATION_PREFIX } from '@zenstackhq/runtime'; +import { invariant, upperCaseFirst } from '@zenstackhq/runtime/local-helpers'; import { PluginError, getAttribute, @@ -26,7 +27,6 @@ import { type Model, } from '@zenstackhq/sdk/ast'; import { getDMMF, getPrismaClientImportSpec, getPrismaVersion, type DMMF } from '@zenstackhq/sdk/prisma'; -import { upperCaseFirst } from '@zenstackhq/runtime/local-helpers'; import fs from 'fs'; import path from 'path'; import semver from 'semver'; @@ -40,6 +40,7 @@ import { SyntaxKind, TypeAliasDeclaration, VariableStatement, + type StatementStructures, } from 'ts-morph'; import { name } from '..'; import { getConcreteModels, getDiscriminatorField } from '../../../utils/ast-utils'; @@ -104,44 +105,122 @@ export class EnhancerGenerator { } async generate(): Promise<{ dmmf: DMMF.Document | undefined; newPrismaClientDtsPath: string | undefined }> { - let dmmf: DMMF.Document | undefined; + if (this.isNewPrismaClientGenerator) { + // "prisma-client" generator + return this.generateForNewClientGenerator(); + } else { + // "prisma-client-js" generator + return this.generateForOldClientGenerator(); + } + } + // logic for "prisma-client" generator + private async generateForNewClientGenerator() { + const needsLogicalClient = this.needsLogicalClient; const prismaImport = getPrismaClientImportSpec(this.outDir, this.options); - let prismaTypesFixed = false; - let resultPrismaTypeImport = prismaImport; - - if (this.needsLogicalClient) { - prismaTypesFixed = true; - resultPrismaTypeImport = LOGICAL_CLIENT_GENERATION_PATH; - if (this.isNewPrismaClientGenerator) { - resultPrismaTypeImport += '/client'; - } + let resultPrismaBaseImport = path.dirname(prismaImport); // get to the parent folder of "client" + let dmmf: DMMF.Document | undefined; + + if (needsLogicalClient) { + // use logical client, note we use the parent of "client" folder here too + resultPrismaBaseImport = LOGICAL_CLIENT_GENERATION_PATH; const result = await this.generateLogicalPrisma(); dmmf = result.dmmf; } - // reexport PrismaClient types (original or fixed) - const modelsTsContent = `export * from '${resultPrismaTypeImport}';${ - this.isNewPrismaClientGenerator ? "\nexport * from './json-types';" : '' - }`; + // `models.ts` for exporting model types + const modelsTsContent = [ + `export * from '${resultPrismaBaseImport}/models';`, + `export * from './json-types';`, + ].join('\n'); + const modelsTs = this.project.createSourceFile(path.join(this.outDir, 'models.ts'), modelsTsContent, { + overwrite: true, + }); + this.saveSourceFile(modelsTs); + + // `enums.ts` for exporting enums + const enumsTs = this.project.createSourceFile( + path.join(this.outDir, 'enums.ts'), + `export * from '${resultPrismaBaseImport}/enums';`, + { + overwrite: true, + } + ); + this.saveSourceFile(enumsTs); + + // `client.ts` for exporting `PrismaClient` and `Prisma` namespace + const clientTs = this.project.createSourceFile( + path.join(this.outDir, 'client.ts'), + `export * from '${resultPrismaBaseImport}/client';`, + { + overwrite: true, + } + ); + this.saveSourceFile(clientTs); + + // `enhance.ts` and `enhance-edge.ts` + for (const target of ['node', 'edge'] as const) { + this.generateEnhance(prismaImport, `${resultPrismaBaseImport}/client`, needsLogicalClient, target); + } + + return { + // logical dmmf if there is one + dmmf, + // new client generator doesn't have a barrel .d.ts file + newPrismaClientDtsPath: undefined, + }; + } + // logic for "prisma-client-js" generator + private async generateForOldClientGenerator() { + const needsLogicalClient = this.needsLogicalClient; + const prismaImport = getPrismaClientImportSpec(this.outDir, this.options); + let resultPrismaClientImport = prismaImport; + let dmmf: DMMF.Document | undefined; + + if (needsLogicalClient) { + // redirect `PrismaClient` import to the logical client + resultPrismaClientImport = LOGICAL_CLIENT_GENERATION_PATH; + const result = await this.generateLogicalPrisma(); + dmmf = result.dmmf; + } + + // `models.ts` for exporting model types + const modelsTsContent = `export * from '${resultPrismaClientImport}';`; const modelsTs = this.project.createSourceFile(path.join(this.outDir, 'models.ts'), modelsTsContent, { overwrite: true, }); this.saveSourceFile(modelsTs); + // `enhance.ts` and `enhance-edge.ts` + for (const target of ['node', 'edge'] as const) { + this.generateEnhance(prismaImport, resultPrismaClientImport, needsLogicalClient, target); + } + + return { + // logical dmmf if there is one + dmmf, + newPrismaClientDtsPath: needsLogicalClient + ? path.resolve(this.outDir, LOGICAL_CLIENT_GENERATION_PATH, 'index.d.ts') + : undefined, + }; + } + + private generateEnhance( + prismaImport: string, + prismaClientImport: string, + needsLogicalClient: boolean, + target: 'node' | 'edge' + ) { const authDecl = getAuthDecl(getDataModelAndTypeDefs(this.model)); const authTypes = authDecl ? generateAuthType(this.model, authDecl) : ''; const authTypeParam = authDecl ? `auth.${authDecl.name}` : 'AuthUser'; - const checkerTypes = this.generatePermissionChecker ? generateCheckerType(this.model) : ''; - for (const target of ['node', 'edge']) { - // generate separate `enhance()` for node and edge runtime - const outFile = target === 'node' ? 'enhance.ts' : 'enhance-edge.ts'; - const enhanceTs = this.project.createSourceFile( - path.join(this.outDir, outFile), - `/* eslint-disable */ + const outFile = target === 'node' ? 'enhance.ts' : 'enhance-edge.ts'; + const enhanceTs = this.project.createSourceFile( + path.join(this.outDir, outFile), + `/* eslint-disable */ import { type EnhancementContext, type EnhancementOptions, type ZodSchemas, type AuthUser } from '@zenstackhq/runtime'; import { createEnhancement } from '@zenstackhq/runtime/enhancements/${target}'; import modelMeta from './model-meta'; @@ -153,8 +232,8 @@ ${ } ${ - prismaTypesFixed - ? this.createLogicalPrismaImports(prismaImport, resultPrismaTypeImport, target) + needsLogicalClient + ? this.createLogicalPrismaImports(prismaImport, prismaClientImport, target) : this.createSimplePrismaImports(prismaImport, target) } @@ -163,23 +242,15 @@ ${authTypes} ${checkerTypes} ${ - prismaTypesFixed + needsLogicalClient ? this.createLogicalPrismaEnhanceFunction(authTypeParam) : this.createSimplePrismaEnhanceFunction(authTypeParam) } `, - { overwrite: true } - ); - - this.saveSourceFile(enhanceTs); - } + { overwrite: true } + ); - return { - dmmf, - newPrismaClientDtsPath: prismaTypesFixed - ? path.resolve(this.outDir, LOGICAL_CLIENT_GENERATION_PATH, 'index.d.ts') - : undefined, - }; + this.saveSourceFile(enhanceTs); } private getZodImport() { @@ -209,7 +280,7 @@ ${ return normalizedRelative(this.outDir, zodAbsPath); } - private createSimplePrismaImports(prismaImport: string, target: string) { + private createSimplePrismaImports(prismaImport: string, target: string | undefined) { const prismaTargetImport = target === 'edge' ? `${prismaImport}/edge` : prismaImport; return `import { Prisma, type PrismaClient } from '${prismaTargetImport}'; @@ -240,10 +311,16 @@ export function enhance(prisma: DbClient, context?: Enh `; } - private createLogicalPrismaImports(prismaImport: string, prismaClientImport: string, target: string) { + private createLogicalPrismaImports(prismaImport: string, prismaClientImport: string, target: string | undefined) { const prismaTargetImport = target === 'edge' ? `${prismaImport}/edge` : prismaImport; + const runtimeLibraryImport = this.isNewPrismaClientGenerator + ? // new generator has these types only in "@prisma/client" + '@prisma/client/runtime/library' + : // old generator has these types generated with the client + `${prismaImport}/runtime/library`; + return `import { Prisma as _Prisma, PrismaClient as _PrismaClient } from '${prismaTargetImport}'; -import type { InternalArgs, DynamicClientExtensionThis } from '@prisma/client/runtime/library'; +import type { InternalArgs, DynamicClientExtensionThis } from '${runtimeLibraryImport}'; import type * as _P from '${prismaClientImport}'; import type { Prisma, PrismaClient } from '${prismaClientImport}'; export type { PrismaClient }; @@ -503,16 +580,13 @@ export type Enhanced = for (const d of this.model.declarations.filter(isDataModel)) { const fileName = `${prismaClientDir}/models/${d.name}.ts`; const sf = project.addSourceFileAtPath(fileName); - const sfNew = project.createSourceFile(`${prismaClientDir}/models/${d.name}-fixed.ts`, undefined, { - overwrite: true, - }); const syntaxList = sf.getChildren()[0]; if (!Node.isSyntaxList(syntaxList)) { throw new PluginError(name, `Unexpected syntax list structure in ${fileName}`); } - sfNew.addStatements('import $Types = runtime.Types;'); + const statements: (string | StatementStructures)[] = ['import $Types = runtime.Types;']; // Add import for json-types if this model has JSON type fields const modelWithJsonFields = this.modelsWithJsonTypeFields.find((m) => m.name === d.name); @@ -525,23 +599,35 @@ 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-types";`); + statements.push(`import type { ${typeNames.join(', ')} } from "../../json-types";`); } } syntaxList.getChildren().forEach((node) => { if (Node.isInterfaceDeclaration(node)) { - sfNew.addInterface(this.transformInterface(node, delegateInfo)); + statements.push(this.transformInterface(node, delegateInfo)); } else if (Node.isTypeAliasDeclaration(node)) { - sfNew.addTypeAlias(this.transformTypeAlias(node, delegateInfo)); + statements.push(this.transformTypeAlias(node, delegateInfo)); } else { - sfNew.addStatements(node.getText()); + statements.push(node.getText()); } }); - await sfNew.move(sf.getFilePath(), { overwrite: true }); + const structure = sf.getStructure(); + structure.statements = statements; + + const sfNew = project.createSourceFile(`${prismaClientDir}/models/${d.name}-fixed.ts`, structure, { + overwrite: true, + }); await sfNew.save(); } + + for (const d of this.model.declarations.filter(isDataModel)) { + const fixedFileName = `${prismaClientDir}/models/${d.name}-fixed.ts`; + const fileName = `${prismaClientDir}/models/${d.name}.ts`; + + fs.renameSync(fixedFileName, fileName); + } } private transformPrismaTypes(sf: SourceFile, sfNew: SourceFile, delegateInfo: DelegateInfo) { @@ -641,6 +727,27 @@ export type Enhanced = return structure; } + private transformVariableStatementProps(variable: VariableStatement) { + const structure = variable.getStructure(); + + // remove `delegate_aux_*` fields from the variable's initializer + const auxFields = this.findAuxProps(variable); + if (auxFields.length > 0) { + structure.declarations.forEach((variable) => { + if (variable.initializer) { + let source = variable.initializer; + auxFields.forEach((f) => { + invariant(typeof source === 'string'); + source = this.removeFromSource(source, f.getText()); + }); + variable.initializer = source; + } + }); + } + + return structure; + } + private transformInterface(iface: InterfaceDeclaration, delegateInfo: DelegateInfo) { const structure = iface.getStructure(); @@ -958,6 +1065,12 @@ export type Enhanced = .filter((n) => n.getName().includes(DELEGATE_AUX_RELATION_PREFIX)); } + private findAuxProps(node: Node) { + return node + .getDescendantsOfKind(SyntaxKind.PropertyAssignment) + .filter((n) => n.getName().includes(DELEGATE_AUX_RELATION_PREFIX)); + } + private saveSourceFile(sf: SourceFile) { if (this.options.preserveTsFiles) { saveSourceFile(sf); @@ -974,7 +1087,7 @@ export type Enhanced = } private trimEmptyLines(source: string): string { - return source.replace(/^\s*[\r\n]/gm, ''); + return source.replace(/^\s*,?[\r\n]/gm, ''); } private get isNewPrismaClientGenerator() { diff --git a/packages/schema/src/res/stdlib.zmodel b/packages/schema/src/res/stdlib.zmodel index 222765a29..1fde1fc57 100644 --- a/packages/schema/src/res/stdlib.zmodel +++ b/packages/schema/src/res/stdlib.zmodel @@ -505,8 +505,8 @@ attribute @db.Timetz(_ x: Int?) @@@targetField([DateTimeField]) @@@prisma // Json type modifiers -attribute @db.Json() @@@targetField([JsonField]) @@@prisma -attribute @db.JsonB() @@@targetField([JsonField]) @@@prisma +attribute @db.Json() @@@targetField([JsonField, TypeDefField]) @@@prisma +attribute @db.JsonB() @@@targetField([JsonField, TypeDefField]) @@@prisma // Bytes type modifiers @@ -748,3 +748,14 @@ function raw(value: String): Any { * Marks a field to be strong-typed JSON. */ attribute @json() @@@targetField([TypeDefField]) + + +/** + * Attaches arbitrary metadata to a model or type def. + */ +attribute @@meta(_ name: String, _ value: Any) + +/** + * Attaches arbitrary metadata to a field. + */ +attribute @meta(_ name: String, _ value: Any) diff --git a/packages/schema/src/telemetry.ts b/packages/schema/src/telemetry.ts index e00557347..9e9a764d7 100644 --- a/packages/schema/src/telemetry.ts +++ b/packages/schema/src/telemetry.ts @@ -11,6 +11,8 @@ import isDocker from './utils/is-docker'; import { isWsl } from './utils/is-wsl'; import { getMachineId } from './utils/machine-id-utils'; import { getVersion } from './utils/version-utils'; +import { isInCi } from './utils/is-ci'; +import { isInContainer } from './utils/is-container'; /** * Telemetry events @@ -44,6 +46,8 @@ export class Telemetry { private readonly prismaVersion = getPrismaVersion(); private readonly isDocker = isDocker(); private readonly isWsl = isWsl(); + private readonly isContainer = isInContainer(); + private readonly isCi = isInCi; private exitWait = 200; constructor() { @@ -108,6 +112,8 @@ export class Telemetry { prismaVersion: this.prismaVersion, isDocker: this.isDocker, isWsl: this.isWsl, + isContainer: this.isContainer, + isCi: this.isCi, ...properties, }; this.mixpanel.track(event, payload); diff --git a/packages/schema/src/utils/is-ci.ts b/packages/schema/src/utils/is-ci.ts new file mode 100644 index 000000000..4950c331e --- /dev/null +++ b/packages/schema/src/utils/is-ci.ts @@ -0,0 +1,8 @@ +import {env} from 'node:process'; +export const isInCi = env.CI !== '0' + && env.CI !== 'false' + && ( + 'CI' in env + || 'CONTINUOUS_INTEGRATION' in env + || Object.keys(env).some(key => key.startsWith('CI_')) + ); \ No newline at end of file diff --git a/packages/schema/src/utils/is-container.ts b/packages/schema/src/utils/is-container.ts new file mode 100644 index 000000000..23c94a29e --- /dev/null +++ b/packages/schema/src/utils/is-container.ts @@ -0,0 +1,23 @@ +import fs from 'node:fs'; +import isDocker from './is-docker'; + +let cachedResult: boolean | undefined; + +// Podman detection +const hasContainerEnv = () => { + try { + fs.statSync('/run/.containerenv'); + return true; + } catch { + return false; + } +}; + +export function isInContainer() { + // TODO: Use `??=` when targeting Node.js 16. + if (cachedResult === undefined) { + cachedResult = hasContainerEnv() || isDocker(); + } + + return cachedResult; +} \ No newline at end of file diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 0a50decd7..8b293b494 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/sdk", - "version": "2.16.1", + "version": "2.17.0", "description": "ZenStack plugin development SDK", "main": "index.js", "scripts": { @@ -18,8 +18,8 @@ "author": "", "license": "MIT", "dependencies": { - "@prisma/generator-helper": "6.10.x", - "@prisma/internals": "6.10.x", + "@prisma/generator-helper": "6.11.x", + "@prisma/internals": "6.11.x", "@zenstackhq/language": "workspace:*", "@zenstackhq/runtime": "workspace:*", "langium": "1.3.1", diff --git a/packages/sdk/src/model-meta-generator.ts b/packages/sdk/src/model-meta-generator.ts index ad34fcffa..84ea5ff2e 100644 --- a/packages/sdk/src/model-meta-generator.ts +++ b/packages/sdk/src/model-meta-generator.ts @@ -3,15 +3,18 @@ import { DataModel, DataModelAttribute, DataModelField, + Expression, isArrayExpr, isBooleanLiteral, isDataModel, isDataModelField, isInvocationExpr, isNumberLiteral, + isObjectExpr, isReferenceExpr, isStringLiteral, isTypeDef, + ObjectExpr, ReferenceExpr, TypeDef, TypeDefField, @@ -20,6 +23,7 @@ import type { RuntimeAttribute } from '@zenstackhq/runtime'; import { lowerCaseFirst } from '@zenstackhq/runtime/local-helpers'; import { streamAst } from 'langium'; import { FunctionDeclarationStructure, OptionalKind, Project, VariableDeclarationKind } from 'ts-morph'; +import { match } from 'ts-pattern'; import { CodeWriter, ExpressionContext, @@ -362,20 +366,9 @@ function getAttributes(target: DataModelField | DataModel | TypeDefField): Runti .map((attr) => { const args: Array<{ name?: string; value: unknown }> = []; for (const arg of attr.args) { - if (isNumberLiteral(arg.value)) { - let v = parseInt(arg.value.value); - if (isNaN(v)) { - v = parseFloat(arg.value.value); - } - if (isNaN(v)) { - throw new Error(`Invalid number literal: ${arg.value.value}`); - } - args.push({ name: arg.name, value: v }); - } else if (isStringLiteral(arg.value) || isBooleanLiteral(arg.value)) { - args.push({ name: arg.name, value: arg.value.value }); - } else { - // non-literal args are ignored - } + const argName = arg.$resolvedParam?.name ?? arg.name; + const argValue = exprToValue(arg.value); + args.push({ name: argName, value: argValue }); } return { name: resolved(attr.decl).name, args }; }) @@ -602,3 +595,26 @@ function getOnUpdateAction(fieldInfo: DataModelField) { } return undefined; } + +function exprToValue(value: Expression): unknown { + return match(value) + .when(isStringLiteral, (v) => v.value) + .when(isBooleanLiteral, (v) => v.value) + .when(isNumberLiteral, (v) => { + let num = parseInt(v.value); + if (isNaN(num)) { + num = parseFloat(v.value); + } + if (isNaN(num)) { + return undefined; + } + return num; + }) + .when(isArrayExpr, (v) => v.items.map((item) => exprToValue(item))) + .when(isObjectExpr, (v) => exprToObject(v)) + .otherwise(() => undefined); +} + +function exprToObject(value: ObjectExpr): unknown { + return Object.fromEntries(value.fields.map((field) => [field.name, exprToValue(field.value)])); +} diff --git a/packages/server/package.json b/packages/server/package.json index 25bd4d57d..69adb73ca 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/server", - "version": "2.16.1", + "version": "2.17.0", "displayName": "ZenStack Server-side Adapters", "description": "ZenStack server-side adapters", "homepage": "https://zenstack.dev", diff --git a/packages/server/src/api/rest/index.ts b/packages/server/src/api/rest/index.ts index 59558be98..75196b1ac 100644 --- a/packages/server/src/api/rest/index.ts +++ b/packages/server/src/api/rest/index.ts @@ -50,6 +50,8 @@ export type Options = { * it should be included in the charset. */ urlSegmentCharset?: string; + + modelNameMapping?: Record; }; type RelationshipInfo = { @@ -65,6 +67,19 @@ type ModelInfo = { relationships: Record; }; +type Match = { + type: string; + id: string; + relationship: string; +}; + +enum UrlPatterns { + SINGLE = 'single', + FETCH_RELATIONSHIP = 'fetchRelationship', + RELATIONSHIP = 'relationship', + COLLECTION = 'collection', +} + class InvalidValueError extends Error { constructor(public readonly message: string) { super(message); @@ -220,29 +235,71 @@ class RequestHandler extends APIHandlerBase { // divider used to separate compound ID fields private idDivider; - private urlPatterns; + private urlPatternMap: Record; + private modelNameMapping: Record; + private reverseModelNameMapping: Record; constructor(private readonly options: Options) { super(); this.idDivider = options.idDivider ?? prismaIdDivider; const segmentCharset = options.urlSegmentCharset ?? 'a-zA-Z0-9-_~ %'; - this.urlPatterns = this.buildUrlPatterns(this.idDivider, segmentCharset); + + this.modelNameMapping = options.modelNameMapping ?? {}; + this.modelNameMapping = Object.fromEntries( + Object.entries(this.modelNameMapping).map(([k, v]) => [lowerCaseFirst(k), v]) + ); + this.reverseModelNameMapping = Object.fromEntries( + Object.entries(this.modelNameMapping).map(([k, v]) => [v, k]) + ); + this.urlPatternMap = this.buildUrlPatternMap(segmentCharset); } - buildUrlPatterns(idDivider: string, urlSegmentNameCharset: string) { + private buildUrlPatternMap(urlSegmentNameCharset: string): Record { const options = { segmentValueCharset: urlSegmentNameCharset }; + + const buildPath = (segments: string[]) => { + return '/' + segments.join('/'); + }; + return { - // collection operations - collection: new UrlPattern('/:type', options), - // single resource operations - single: new UrlPattern('/:type/:id', options), - // related entity fetching - fetchRelationship: new UrlPattern('/:type/:id/:relationship', options), - // relationship operations - relationship: new UrlPattern('/:type/:id/relationships/:relationship', options), + [UrlPatterns.SINGLE]: new UrlPattern(buildPath([':type', ':id']), options), + [UrlPatterns.FETCH_RELATIONSHIP]: new UrlPattern(buildPath([':type', ':id', ':relationship']), options), + [UrlPatterns.RELATIONSHIP]: new UrlPattern( + buildPath([':type', ':id', 'relationships', ':relationship']), + options + ), + [UrlPatterns.COLLECTION]: new UrlPattern(buildPath([':type']), options), }; } + private mapModelName(modelName: string): string { + return this.modelNameMapping[modelName] ?? modelName; + } + + private matchUrlPattern(path: string, routeType: UrlPatterns): Match | undefined { + const pattern = this.urlPatternMap[routeType]; + if (!pattern) { + throw new InvalidValueError(`Unknown route type: ${routeType}`); + } + + const match = pattern.match(path); + if (!match) { + return; + } + + if (match.type in this.modelNameMapping) { + throw new InvalidValueError( + `use the mapped model name: ${this.modelNameMapping[match.type]} and not ${match.type}` + ); + } + + if (match.type in this.reverseModelNameMapping) { + match.type = this.reverseModelNameMapping[match.type]; + } + + return match; + } + async handleRequest({ prisma, method, @@ -274,19 +331,18 @@ class RequestHandler extends APIHandlerBase { try { switch (method) { case 'GET': { - let match = this.urlPatterns.single.match(path); + let match = this.matchUrlPattern(path, UrlPatterns.SINGLE); if (match) { // single resource read return await this.processSingleRead(prisma, match.type, match.id, query); } - - match = this.urlPatterns.fetchRelationship.match(path); + match = this.matchUrlPattern(path, UrlPatterns.FETCH_RELATIONSHIP); if (match) { // fetch related resource(s) return await this.processFetchRelated(prisma, match.type, match.id, match.relationship, query); } - match = this.urlPatterns.relationship.match(path); + match = this.matchUrlPattern(path, UrlPatterns.RELATIONSHIP); if (match) { // read relationship return await this.processReadRelationship( @@ -298,7 +354,7 @@ class RequestHandler extends APIHandlerBase { ); } - match = this.urlPatterns.collection.match(path); + match = this.matchUrlPattern(path, UrlPatterns.COLLECTION); if (match) { // collection read return await this.processCollectionRead(prisma, match.type, query); @@ -311,8 +367,7 @@ class RequestHandler extends APIHandlerBase { if (!requestBody) { return this.makeError('invalidPayload'); } - - let match = this.urlPatterns.collection.match(path); + let match = this.matchUrlPattern(path, UrlPatterns.COLLECTION); if (match) { const body = requestBody as any; const upsertMeta = this.upsertMetaSchema.safeParse(body); @@ -338,8 +393,7 @@ class RequestHandler extends APIHandlerBase { ); } } - - match = this.urlPatterns.relationship.match(path); + match = this.matchUrlPattern(path, UrlPatterns.RELATIONSHIP); if (match) { // relationship creation (collection relationship only) return await this.processRelationshipCRUD( @@ -362,8 +416,7 @@ class RequestHandler extends APIHandlerBase { if (!requestBody) { return this.makeError('invalidPayload'); } - - let match = this.urlPatterns.single.match(path); + let match = this.matchUrlPattern(path, UrlPatterns.SINGLE); if (match) { // resource update return await this.processUpdate( @@ -376,8 +429,7 @@ class RequestHandler extends APIHandlerBase { zodSchemas ); } - - match = this.urlPatterns.relationship.match(path); + match = this.matchUrlPattern(path, UrlPatterns.RELATIONSHIP); if (match) { // relationship update return await this.processRelationshipCRUD( @@ -395,13 +447,13 @@ class RequestHandler extends APIHandlerBase { } case 'DELETE': { - let match = this.urlPatterns.single.match(path); + let match = this.matchUrlPattern(path, UrlPatterns.SINGLE); if (match) { // resource deletion return await this.processDelete(prisma, match.type, match.id); } - match = this.urlPatterns.relationship.match(path); + match = this.matchUrlPattern(path, UrlPatterns.RELATIONSHIP); if (match) { // relationship deletion (collection relationship only) return await this.processRelationshipCRUD( @@ -531,11 +583,12 @@ class RequestHandler extends APIHandlerBase { } if (entity?.[relationship]) { + const mappedType = this.mapModelName(type); return { status: 200, body: await this.serializeItems(relationInfo.type, entity[relationship], { linkers: { - document: new Linker(() => this.makeLinkUrl(`/${type}/${resourceId}/${relationship}`)), + document: new Linker(() => this.makeLinkUrl(`/${mappedType}/${resourceId}/${relationship}`)), paginator, }, include, @@ -582,11 +635,12 @@ class RequestHandler extends APIHandlerBase { } const entity: any = await prisma[type].findUnique(args); + const mappedType = this.mapModelName(type); if (entity?._count?.[relationship] !== undefined) { // build up paginator const total = entity?._count?.[relationship] as number; - const url = this.makeNormalizedUrl(`/${type}/${resourceId}/relationships/${relationship}`, query); + const url = this.makeNormalizedUrl(`/${mappedType}/${resourceId}/relationships/${relationship}`, query); const { offset, limit } = this.getPagination(query); paginator = this.makePaginator(url, offset, limit, total); } @@ -595,7 +649,7 @@ class RequestHandler extends APIHandlerBase { const serialized: any = await this.serializeItems(relationInfo.type, entity[relationship], { linkers: { document: new Linker(() => - this.makeLinkUrl(`/${type}/${resourceId}/relationships/${relationship}`) + this.makeLinkUrl(`/${mappedType}/${resourceId}/relationships/${relationship}`) ), paginator, }, @@ -680,7 +734,8 @@ class RequestHandler extends APIHandlerBase { ]); const total = count as number; - const url = this.makeNormalizedUrl(`/${type}`, query); + const mappedType = this.mapModelName(type); + const url = this.makeNormalizedUrl(`/${mappedType}`, query); const options: Partial = { include, linkers: { @@ -1009,9 +1064,13 @@ class RequestHandler extends APIHandlerBase { const entity: any = await prisma[type].update(updateArgs); + const mappedType = this.mapModelName(type); + const serialized: any = await this.serializeItems(relationInfo.type, entity[relationship], { linkers: { - document: new Linker(() => this.makeLinkUrl(`/${type}/${resourceId}/relationships/${relationship}`)), + document: new Linker(() => + this.makeLinkUrl(`/${mappedType}/${resourceId}/relationships/${relationship}`) + ), }, onlyIdentifier: true, }); @@ -1156,6 +1215,7 @@ class RequestHandler extends APIHandlerBase { for (const model of Object.keys(modelMeta.models)) { const ids = getIdFields(modelMeta, model); + const mappedModel = this.mapModelName(model); if (ids.length < 1) { continue; @@ -1163,8 +1223,8 @@ class RequestHandler extends APIHandlerBase { const linker = new Linker((items) => Array.isArray(items) - ? this.makeLinkUrl(`/${model}`) - : this.makeLinkUrl(`/${model}/${this.getId(model, items, modelMeta)}`) + ? this.makeLinkUrl(`/${mappedModel}`) + : this.makeLinkUrl(`/${mappedModel}/${this.getId(model, items, modelMeta)}`) ); linkers[model] = linker; @@ -1208,6 +1268,8 @@ class RequestHandler extends APIHandlerBase { } const fieldIds = getIdFields(modelMeta, fieldMeta.type); if (fieldIds.length > 0) { + const mappedModel = this.mapModelName(model); + const relator = new Relator( async (data) => { return (data as any)[field]; @@ -1223,7 +1285,7 @@ class RequestHandler extends APIHandlerBase { ), relationship: new Linker((primary) => this.makeLinkUrl( - `/${lowerCaseFirst(model)}/${this.getId( + `/${lowerCaseFirst(mappedModel)}/${this.getId( model, primary, modelMeta diff --git a/packages/server/tests/api/rest.test.ts b/packages/server/tests/api/rest.test.ts index 642b3fcf8..c0023873a 100644 --- a/packages/server/tests/api/rest.test.ts +++ b/packages/server/tests/api/rest.test.ts @@ -3013,4 +3013,102 @@ describe('REST server tests', () => { expect(r.body.data.attributes.enabled).toBe(false); }); }); + + describe('REST server tests - model name mapping', () => { + const schema = ` + model User { + id String @id @default(cuid()) + name String + posts Post[] + } + + model Post { + id String @id @default(cuid()) + title String + author User? @relation(fields: [authorId], references: [id]) + authorId String? + } + `; + beforeAll(async () => { + const params = await loadSchema(schema); + prisma = params.prisma; + zodSchemas = params.zodSchemas; + modelMeta = params.modelMeta; + + const _handler = makeHandler({ + endpoint: 'http://localhost/api', + modelNameMapping: { + User: 'myUser', + }, + }); + handler = (args) => + _handler({ ...args, zodSchemas, modelMeta, url: new URL(`http://localhost/${args.path}`) }); + }); + + it('works with name mapping', async () => { + // using original model name + await expect( + handler({ + method: 'post', + path: '/user', + query: {}, + requestBody: { data: { type: 'user', attributes: { id: '1', name: 'User1' } } }, + prisma, + }) + ).resolves.toMatchObject({ + status: 400, + }); + + // using mapped model name + await expect( + handler({ + method: 'post', + path: '/myUser', + query: {}, + requestBody: { data: { type: 'user', attributes: { id: '1', name: 'User1' } } }, + prisma, + }) + ).resolves.toMatchObject({ + status: 201, + body: { + links: { self: 'http://localhost/api/myUser/1' }, + }, + }); + + await expect( + handler({ + method: 'get', + path: '/myUser/1', + query: {}, + prisma, + }) + ).resolves.toMatchObject({ + status: 200, + body: { + links: { self: 'http://localhost/api/myUser/1' }, + }, + }); + + // works with unmapped model name + await expect( + handler({ + method: 'post', + path: '/post', + query: {}, + requestBody: { + data: { + type: 'post', + attributes: { id: '1', title: 'Post1' }, + relationships: { + author: { data: { type: 'user', id: '1' } }, + }, + }, + }, + prisma, + }) + ).resolves.toMatchObject({ + status: 201, + }); + }); + }); }); diff --git a/packages/testtools/package.json b/packages/testtools/package.json index 198d5d76a..96f2f266f 100644 --- a/packages/testtools/package.json +++ b/packages/testtools/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/testtools", - "version": "2.16.1", + "version": "2.17.0", "description": "ZenStack Test Tools", "main": "index.js", "private": true, diff --git a/packages/testtools/src/schema.ts b/packages/testtools/src/schema.ts index c1b8c6aeb..152b05ada 100644 --- a/packages/testtools/src/schema.ts +++ b/packages/testtools/src/schema.ts @@ -171,6 +171,84 @@ export async function loadSchemaFromFile(schemaFile: string, options?: SchemaLoa } export async function loadSchema(schema: string, options?: SchemaLoadOptions) { + const { projectDir, options: mergedOptions } = createProjectAndCompile(schema, options); + + const prismaLoadPath = + mergedOptions?.prismaLoadPath && mergedOptions.prismaLoadPath !== '@prisma/client' + ? path.isAbsolute(mergedOptions.prismaLoadPath) + ? mergedOptions.prismaLoadPath + : path.join(projectDir, mergedOptions.prismaLoadPath) + : path.join(projectDir, 'node_modules/.prisma/client'); + const prismaModule = require(prismaLoadPath); + const PrismaClient = prismaModule.PrismaClient; + + let clientOptions: object = { log: ['info', 'warn', 'error'] }; + if (mergedOptions?.prismaClientOptions) { + clientOptions = { ...clientOptions, ...mergedOptions.prismaClientOptions }; + } + let prisma = new PrismaClient(clientOptions); + // https://github.com/prisma/prisma/issues/18292 + prisma[Symbol.for('nodejs.util.inspect.custom')] = 'PrismaClient'; + + if (mergedOptions.pulseApiKey) { + const withPulse = loadModule('@prisma/extension-pulse/node', projectDir).withPulse; + prisma = prisma.$extends(withPulse({ apiKey: mergedOptions.pulseApiKey })); + } + + if (mergedOptions?.getPrismaOnly) { + return { + prisma, + prismaModule, + projectDir, + enhance: undefined as any, + enhanceRaw: undefined as any, + policy: undefined as unknown as PolicyDef, + modelMeta: undefined as any, + zodSchemas: undefined as any, + }; + } + + const outputPath = mergedOptions.output + ? path.isAbsolute(mergedOptions.output) + ? mergedOptions.output + : path.join(projectDir, mergedOptions.output) + : path.join(projectDir, 'node_modules', DEFAULT_RUNTIME_LOAD_PATH); + + const policy: PolicyDef = require(path.join(outputPath, 'policy')).default; + const modelMeta = require(path.join(outputPath, 'model-meta')).default; + + let zodSchemas: any; + try { + zodSchemas = require(path.join(outputPath, 'zod')); + } catch { + /* noop */ + } + + const enhance = require(path.join(outputPath, 'enhance')).enhance; + + return { + projectDir: projectDir, + prisma, + enhance: (user?: AuthUser, options?: EnhancementOptions): FullDbClientContract => + enhance( + prisma, + { user }, + { + logPrismaQuery: mergedOptions.logPrismaQuery, + transactionTimeout: 1000000, + kinds: mergedOptions.enhancements, + ...(options ?? mergedOptions.enhanceOptions), + } + ), + enhanceRaw: enhance, + policy, + modelMeta, + zodSchemas, + prismaModule, + }; +} + +export function createProjectAndCompile(schema: string, options: SchemaLoadOptions | undefined) { const opt = { ...defaultOptions, ...options }; let projectDir = opt.projectDir; @@ -282,11 +360,7 @@ export async function loadSchema(schema: string, options?: SchemaLoadOptions) { fs.writeFileSync(path.join(projectDir, name), content); }); - if (opt.extraSourceFiles && opt.extraSourceFiles.length > 0 && !opt.compile) { - console.warn('`extraSourceFiles` is true but `compile` is false.'); - } - - if (opt.compile) { + if (opt.compile || opt.extraSourceFiles) { console.log('Compiling...'); run('npx tsc --init'); @@ -303,79 +377,7 @@ export async function loadSchema(schema: string, options?: SchemaLoadOptions) { fs.writeFileSync(path.join(projectDir, './tsconfig.json'), JSON.stringify(tsconfig, null, 2)); run('npx tsc --project tsconfig.json'); } - - const prismaLoadPath = options?.prismaLoadPath - ? path.isAbsolute(options.prismaLoadPath) - ? options.prismaLoadPath - : path.join(projectDir, options.prismaLoadPath) - : path.join(projectDir, 'node_modules/.prisma/client'); - const prismaModule = require(prismaLoadPath); - const PrismaClient = prismaModule.PrismaClient; - - let clientOptions: object = { log: ['info', 'warn', 'error'] }; - if (options?.prismaClientOptions) { - clientOptions = { ...clientOptions, ...options.prismaClientOptions }; - } - let prisma = new PrismaClient(clientOptions); - // https://github.com/prisma/prisma/issues/18292 - prisma[Symbol.for('nodejs.util.inspect.custom')] = 'PrismaClient'; - - if (opt.pulseApiKey) { - const withPulse = loadModule('@prisma/extension-pulse/node', projectDir).withPulse; - prisma = prisma.$extends(withPulse({ apiKey: opt.pulseApiKey })); - } - - if (options?.getPrismaOnly) { - return { - prisma, - prismaModule, - projectDir, - enhance: undefined as any, - enhanceRaw: undefined as any, - policy: undefined as unknown as PolicyDef, - modelMeta: undefined as any, - zodSchemas: undefined as any, - }; - } - - const outputPath = opt.output - ? path.isAbsolute(opt.output) - ? opt.output - : path.join(projectDir, opt.output) - : path.join(projectDir, 'node_modules', DEFAULT_RUNTIME_LOAD_PATH); - - const policy: PolicyDef = require(path.join(outputPath, 'policy')).default; - const modelMeta = require(path.join(outputPath, 'model-meta')).default; - - let zodSchemas: any; - try { - zodSchemas = require(path.join(outputPath, 'zod')); - } catch { - /* noop */ - } - - const enhance = require(path.join(outputPath, 'enhance')).enhance; - - return { - projectDir: projectDir, - prisma, - enhance: (user?: AuthUser, options?: EnhancementOptions): FullDbClientContract => - enhance( - prisma, - { user }, - { - logPrismaQuery: opt.logPrismaQuery, - transactionTimeout: 1000000, - kinds: opt.enhancements, - ...(options ?? opt.enhanceOptions), - } - ), - enhanceRaw: enhance, - policy, - modelMeta, - zodSchemas, - prismaModule, - }; + return { projectDir, options: opt }; } /** diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f46c528ca..d0c0d7f99 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -350,8 +350,8 @@ importers: packages/runtime: dependencies: '@prisma/client': - specifier: 5.0.0 - 6.10.x - version: 6.10.0(prisma@6.0.0)(typescript@5.5.2) + specifier: 5.0.0 - 6.11.x + version: 6.11.1(prisma@6.0.0)(typescript@5.5.2) bcryptjs: specifier: ^2.4.3 version: 2.4.3 @@ -454,7 +454,7 @@ importers: specifier: ^4.0.0 version: 4.0.1 prisma: - specifier: 5.0.0 - 6.10.x + specifier: 5.0.0 - 6.11.x version: 6.0.0 semver: specifier: ^7.5.2 @@ -497,8 +497,8 @@ importers: version: 1.5.0(zod@3.23.8) devDependencies: '@prisma/client': - specifier: 6.10.x - version: 6.10.0(prisma@6.0.0)(typescript@5.5.2) + specifier: 6.11.x + version: 6.11.1(prisma@6.0.0)(typescript@5.5.2) '@types/async-exit-hook': specifier: ^2.0.0 version: 2.0.2 @@ -549,11 +549,11 @@ importers: packages/sdk: dependencies: '@prisma/generator-helper': - specifier: 6.10.x - version: 6.10.0 + specifier: 6.11.x + version: 6.11.1 '@prisma/internals': - specifier: 6.10.x - version: 6.10.0(typescript@5.5.2) + specifier: 6.11.x + version: 6.11.1(typescript@5.5.2) '@zenstackhq/language': specifier: workspace:* version: link:../language/dist @@ -2486,8 +2486,8 @@ packages: prisma: optional: true - '@prisma/client@6.10.0': - resolution: {integrity: sha512-C+3A6sPt8EwUlNwsbT22IoUq0O+wXXA4Sw39UmCATlfa8HVP5r0X/l9xGyELhfSmmO0sjgSAl7qmlCHS6Dkekw==} + '@prisma/client@6.11.1': + resolution: {integrity: sha512-5CLFh8QP6KxRm83pJ84jaVCeSVPQr8k0L2SEtOJHwdkS57/VQDcI/wQpGmdyOZi+D9gdNabdo8tj1Uk+w+upsQ==} engines: {node: '>=18.18'} peerDependencies: prisma: '*' @@ -2498,8 +2498,8 @@ packages: typescript: optional: true - '@prisma/config@6.10.0': - resolution: {integrity: sha512-9aA88Vub9O7zdb52PuJg88cN2GCjfY2I45CIttJe7fS5EyvTRRGE/PDQlbjTG9ij9+leD47fGLQCqYDpyCE5Iw==} + '@prisma/config@6.11.1': + resolution: {integrity: sha512-z6rCTQN741wxDq82cpdzx2uVykpnQIXalLhrWQSR0jlBVOxCIkz3HZnd8ern3uYTcWKfB3IpVAF7K2FU8t/8AQ==} '@prisma/debug@5.14.0': resolution: {integrity: sha512-iq56qBZuFfX3fCxoxT8gBX33lQzomBU0qIUaEj1RebsKVz1ob/BVH1XSBwwwvRVtZEV1b7Fxx2eVu34Ge/mg3w==} @@ -2507,14 +2507,14 @@ packages: '@prisma/debug@6.0.0': resolution: {integrity: sha512-eUjoNThlDXdyJ1iQ2d7U6aTVwm59EwvODb5zFVNJEokNoSiQmiYWNzZIwZyDmZ+j51j42/0iTaHIJ4/aZPKFRg==} - '@prisma/debug@6.10.0': - resolution: {integrity: sha512-vzVu0Z3DfCzyx0m7LPZgdA/M7opv7B2R7agNLjh1PpIapCqHo/dwoXoj3Kl25A6TkmhexJzOZKedmhpXsMBwGA==} + '@prisma/debug@6.11.1': + resolution: {integrity: sha512-lWRb/YSWu8l4Yum1UXfGLtqFzZkVS2ygkWYpgkbgMHn9XJlMITIgeMvJyX5GepChzhmxuSuiq/MY/kGFweOpGw==} - '@prisma/dmmf@6.10.0': - resolution: {integrity: sha512-wLrw4Ro88jLQ7e3UfyAmktE6oNcIdPN7MNZQGX30KFvHOu6INz9SeMfVJ4kBgQkiVgq4jSEZ6Sfde54cra23tQ==} + '@prisma/dmmf@6.11.1': + resolution: {integrity: sha512-EwPSlhz7Qt5msepoxlKIGjtGP6fUEFQyxrYx6f0IdvQ8l0rLSJEqBrTiMpNhgKQFeR8kuiQTv+xzHk/l8mV+Xw==} - '@prisma/driver-adapter-utils@6.10.0': - resolution: {integrity: sha512-+nApo/19QbfPy6smSwZValpNYbHrcI3ew6vhogYgJEijsB09Mxu2Wo+JDZIGF21DQq6RdRWeBFnJtUHLISlw4Q==} + '@prisma/driver-adapter-utils@6.11.1': + resolution: {integrity: sha512-y09aIY3XTJIgPI1ZMfybn5kndbhu11Rjg81cH6pSi/Rg/PnbxomWVHhu4NnKmCGioidUqJ5KeQ7i75dIOVsSXQ==} '@prisma/engines-version@5.14.0-25.e9771e62de70f79a5e1c604a2d7c8e2a0a874b48': resolution: {integrity: sha512-ip6pNkRo1UxWv+6toxNcYvItNYaqQjXdFNGJ+Nuk2eYtRoEdoF13wxo7/jsClJFFenMPVNVqXQDV0oveXnR1cA==} @@ -2522,8 +2522,8 @@ packages: '@prisma/engines-version@5.23.0-27.5dbef10bdbfb579e07d35cc85fb1518d357cb99e': resolution: {integrity: sha512-JmIds0Q2/vsOmnuTJYxY4LE+sajqjYKhLtdOT6y4imojqv5d/aeVEfbBGC74t8Be1uSp0OP8lxIj2OqoKbLsfQ==} - '@prisma/engines-version@6.10.0-43.aee10d5a411e4360c6d3445ce4810ca65adbf3e8': - resolution: {integrity: sha512-Dy7cS5Sz/kzdj2nrYTiPfycf/ZeQXFoIcXgTLmYHpuDX0rGITEGe7JSTSNnLYRUnjTHerDTPGPJCiDeyb6lPBg==} + '@prisma/engines-version@6.11.1-1.f40f79ec31188888a2e33acda0ecc8fd10a853a9': + resolution: {integrity: sha512-swFJTOOg4tHyOM1zB/pHb3MeH0i6t7jFKn5l+ZsB23d9AQACuIRo9MouvuKGvnDogzkcjbWnXi/NvOZ0+n5Jfw==} '@prisma/engines@5.14.0': resolution: {integrity: sha512-lgxkKZ6IEygVcw6IZZUlPIfLQ9hjSYAtHjZ5r64sCLDgVzsPFCi2XBBJgzPMkOQ5RHzUD4E/dVdpn9+ez8tk1A==} @@ -2531,8 +2531,8 @@ packages: '@prisma/engines@6.0.0': resolution: {integrity: sha512-ZZCVP3q22ifN6Ex6C8RIcTDBlRtMJS2H1ljV0knCiWNGArvvkEbE88W3uDdq/l4+UvyvHpGzdf9ZsCWSQR7ZQQ==} - '@prisma/engines@6.10.0': - resolution: {integrity: sha512-g/VL/J+b1rjzvPLZWSjOt/iWX/As44IF65x0XrsvwjD1UI0hLHzDAVx3AJz4k4cNsFzEQqVl/rLa6ICsLy8v5w==} + '@prisma/engines@6.11.1': + resolution: {integrity: sha512-6eKEcV6V8W2eZAUwX2xTktxqPM4vnx3sxz3SDtpZwjHKpC6lhOtc4vtAtFUuf5+eEqBk+dbJ9Dcaj6uQU+FNNg==} '@prisma/fetch-engine@5.14.0': resolution: {integrity: sha512-VrheA9y9DMURK5vu8OJoOgQpxOhas3qF0IBHJ8G/0X44k82kc8E0w98HCn2nhnbOOMwbWsJWXfLC2/F8n5u0gQ==} @@ -2540,17 +2540,17 @@ packages: '@prisma/fetch-engine@6.0.0': resolution: {integrity: sha512-j2m+iO5RDPRI7SUc7sHo8wX7SA4iTkJ+18Sxch8KinQM46YiCQD1iXKN6qU79C1Fliw5Bw/qDyTHaTsa3JMerA==} - '@prisma/fetch-engine@6.10.0': - resolution: {integrity: sha512-7An09F6Xe886gSwcj1HEY/0LBuD4IR0ZnKbNv4d0kMnmNzGCz+IK4XRnd/yOkiptIks0nF+igLEin5MEoBejfA==} + '@prisma/fetch-engine@6.11.1': + resolution: {integrity: sha512-NBYzmkXTkj9+LxNPRSndaAeALOL1Gr3tjvgRYNqruIPlZ6/ixLeuE/5boYOewant58tnaYFZ5Ne0jFBPfGXHpQ==} '@prisma/generator-helper@5.14.0': resolution: {integrity: sha512-xVc71cmTnPZ0lnSs4FAY6Ta72vFJ3webrQwKMQ2ujr6hDG1VPIEf820T1TOS3ZZQd/OKigNKXnq3co8biz9/qw==} - '@prisma/generator-helper@6.10.0': - resolution: {integrity: sha512-oztrNwQ70ridJ4qAGRUyAqoxU9lei7NkiTDg9HfRd1OCj8P2eEjEpUL2RAVeoCJJlTK4FAbTdo0vvp3APG5M9w==} + '@prisma/generator-helper@6.11.1': + resolution: {integrity: sha512-mxdeBqW8inKkuE2nHLo0SsdzsjY51cyRQYgD1uEshvy1/n5eXJJOhEA9T948qA0ZBYUb8SppO2HZtQjwkGwzjQ==} - '@prisma/generator@6.10.0': - resolution: {integrity: sha512-lQFnS2l/t/lzY5k8E3MTWI72d/1pNxmPOSkHpJXvRfBQi54pAcnyaZwMa+BC/MqP+Xl3INc+7xCPBCkPxjo5nA==} + '@prisma/generator@6.11.1': + resolution: {integrity: sha512-LNpsWyVjebWbVIHVw0bTTQUIlnK+92naGB5fWnjEJqJcxXuDnNQ0XomZAQ1BrqwbMrYL6RYS4whkTpKtu1ijLQ==} '@prisma/get-platform@5.14.0': resolution: {integrity: sha512-/yAyBvcEjRv41ynZrhdrPtHgk47xLRRq/o5eWGcUpBJ1YrUZTYB8EoPiopnP7iQrMATK8stXQdPOoVlrzuTQZw==} @@ -2558,14 +2558,14 @@ packages: '@prisma/get-platform@6.0.0': resolution: {integrity: sha512-PS6nYyIm9g8C03E4y7LknOfdCw/t2KyEJxntMPQHQZCOUgOpF82Ma60mdlOD08w90I3fjLiZZ0+MadenR3naDQ==} - '@prisma/get-platform@6.10.0': - resolution: {integrity: sha512-6xqX2cxC2l0JHySyyFlXZ4QIESeEmkvSJfGy2r/NsQG+vjxBNDrlwDOgh+aQI1ivbgqwFRjSXuUjl/yd2Za2HQ==} + '@prisma/get-platform@6.11.1': + resolution: {integrity: sha512-b2Z8oV2gwvdCkFemBTFd0x4lsL4O2jLSx8lB7D+XqoFALOQZPa7eAPE1NU0Mj1V8gPHRxIsHnyUNtw2i92psUw==} '@prisma/internals@5.14.0': resolution: {integrity: sha512-s0JRNDmR2bvcyy0toz89jy7SbbjANAs4e9KCReNvSm5czctIaZzDf68tcOXdtH0G7m9mKhVhNPdS9lMky0DhWA==} - '@prisma/internals@6.10.0': - resolution: {integrity: sha512-bUZZhfVL2xMZcIDm/g7G6J2Pkc3jDRZ3sRkw6JIA6ufxGhbmCh+KjeeECrvtSCWs/wIiInGz/4DXhibgaLiscQ==} + '@prisma/internals@6.11.1': + resolution: {integrity: sha512-lJDepOgdG/qCFX+q1ZIBNM3NVJE1C9nakaPVl+USVrYS6wDSUbZqEGeiUsWH/TUyQUQNovYzypEf1TbW4eMeOQ==} peerDependencies: typescript: '>=5.1.0' peerDependenciesMeta: @@ -2578,17 +2578,17 @@ packages: '@prisma/prisma-schema-wasm@5.14.0-25.e9771e62de70f79a5e1c604a2d7c8e2a0a874b48': resolution: {integrity: sha512-WeTmJ0mK8ALoKJUQFO+465k9lm1JWS4ODUg7akJq1wjgyDU1RTAzDFli8ESmNJlMVgJgoAd6jXmzcnoA0HT9Lg==} - '@prisma/prisma-schema-wasm@6.10.0-43.aee10d5a411e4360c6d3445ce4810ca65adbf3e8': - resolution: {integrity: sha512-AZO22bMnTa9fzlzDumgoHloQw5INU6/9kR7SlbZr7qMH11pyr0+cZE5r4yoi6JWlUXWsUA04XeGfsjXFoQx3WA==} + '@prisma/prisma-schema-wasm@6.11.1-1.f40f79ec31188888a2e33acda0ecc8fd10a853a9': + resolution: {integrity: sha512-6JOVGkouivLshm++Gl089nIf4TN7A0O5wHZkwkSczDuTAJkJQ3vtXrxH1Rl2jbwdizSRP/eOiky0bCiSdATuGw==} - '@prisma/schema-engine-wasm@6.10.0-43.aee10d5a411e4360c6d3445ce4810ca65adbf3e8': - resolution: {integrity: sha512-p1gYMKVY+AhrcG4ct/bBTKZzjud+17lzpUS1iDCg4L4xYSK9/PLr0f4BqoUdM0AYzJ3bTAvZ3ukYbpBHQfkVlw==} + '@prisma/schema-engine-wasm@6.11.1-1.f40f79ec31188888a2e33acda0ecc8fd10a853a9': + resolution: {integrity: sha512-ds0a6rXtMATRH8dqq8TaHCmuYoMKgEfMu6bZ943vws+9pjfHcttusCKczvYR+siWM1J63yIH7uVyUMCoqWUAdA==} '@prisma/schema-files-loader@5.14.0': resolution: {integrity: sha512-n1QHR2C63dARKPZe0WPn7biybcBHzXe+BEmiHC5Drq9KPWnpmQtIfGpqm1ZKdvCZfcA5FF3wgpSMPK4LnB0obQ==} - '@prisma/schema-files-loader@6.10.0': - resolution: {integrity: sha512-R1z5g97Jc3UU+ptcwDlVDTkpDNfRQKv/tRrYXwygvrI4XWneEIJHYM3SXbTFcu2arlUzDHJBXAylXkcD+RM5zQ==} + '@prisma/schema-files-loader@6.11.1': + resolution: {integrity: sha512-yLuqR0X2zYhG0SNjd+8mDg4qCBWy6G7qtu1gYWLQhChahOnm2mJicHF6Wcg+C+G4vzQOZgc5yEv4vzLLZj9UqA==} '@protobufjs/aspromise@1.1.2': resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} @@ -10756,12 +10756,12 @@ snapshots: optionalDependencies: prisma: 6.0.0 - '@prisma/client@6.10.0(prisma@6.0.0)(typescript@5.5.2)': + '@prisma/client@6.11.1(prisma@6.0.0)(typescript@5.5.2)': optionalDependencies: prisma: 6.0.0 typescript: 5.5.2 - '@prisma/config@6.10.0': + '@prisma/config@6.11.1': dependencies: jiti: 2.4.2 @@ -10769,19 +10769,19 @@ snapshots: '@prisma/debug@6.0.0': {} - '@prisma/debug@6.10.0': {} + '@prisma/debug@6.11.1': {} - '@prisma/dmmf@6.10.0': {} + '@prisma/dmmf@6.11.1': {} - '@prisma/driver-adapter-utils@6.10.0': + '@prisma/driver-adapter-utils@6.11.1': dependencies: - '@prisma/debug': 6.10.0 + '@prisma/debug': 6.11.1 '@prisma/engines-version@5.14.0-25.e9771e62de70f79a5e1c604a2d7c8e2a0a874b48': {} '@prisma/engines-version@5.23.0-27.5dbef10bdbfb579e07d35cc85fb1518d357cb99e': {} - '@prisma/engines-version@6.10.0-43.aee10d5a411e4360c6d3445ce4810ca65adbf3e8': {} + '@prisma/engines-version@6.11.1-1.f40f79ec31188888a2e33acda0ecc8fd10a853a9': {} '@prisma/engines@5.14.0': dependencies: @@ -10797,12 +10797,12 @@ snapshots: '@prisma/fetch-engine': 6.0.0 '@prisma/get-platform': 6.0.0 - '@prisma/engines@6.10.0': + '@prisma/engines@6.11.1': dependencies: - '@prisma/debug': 6.10.0 - '@prisma/engines-version': 6.10.0-43.aee10d5a411e4360c6d3445ce4810ca65adbf3e8 - '@prisma/fetch-engine': 6.10.0 - '@prisma/get-platform': 6.10.0 + '@prisma/debug': 6.11.1 + '@prisma/engines-version': 6.11.1-1.f40f79ec31188888a2e33acda0ecc8fd10a853a9 + '@prisma/fetch-engine': 6.11.1 + '@prisma/get-platform': 6.11.1 '@prisma/fetch-engine@5.14.0': dependencies: @@ -10816,23 +10816,23 @@ snapshots: '@prisma/engines-version': 5.23.0-27.5dbef10bdbfb579e07d35cc85fb1518d357cb99e '@prisma/get-platform': 6.0.0 - '@prisma/fetch-engine@6.10.0': + '@prisma/fetch-engine@6.11.1': dependencies: - '@prisma/debug': 6.10.0 - '@prisma/engines-version': 6.10.0-43.aee10d5a411e4360c6d3445ce4810ca65adbf3e8 - '@prisma/get-platform': 6.10.0 + '@prisma/debug': 6.11.1 + '@prisma/engines-version': 6.11.1-1.f40f79ec31188888a2e33acda0ecc8fd10a853a9 + '@prisma/get-platform': 6.11.1 '@prisma/generator-helper@5.14.0': dependencies: '@prisma/debug': 5.14.0 - '@prisma/generator-helper@6.10.0': + '@prisma/generator-helper@6.11.1': dependencies: - '@prisma/debug': 6.10.0 - '@prisma/dmmf': 6.10.0 - '@prisma/generator': 6.10.0 + '@prisma/debug': 6.11.1 + '@prisma/dmmf': 6.11.1 + '@prisma/generator': 6.11.1 - '@prisma/generator@6.10.0': {} + '@prisma/generator@6.11.1': {} '@prisma/get-platform@5.14.0': dependencies: @@ -10842,9 +10842,9 @@ snapshots: dependencies: '@prisma/debug': 6.0.0 - '@prisma/get-platform@6.10.0': + '@prisma/get-platform@6.11.1': dependencies: - '@prisma/debug': 6.10.0 + '@prisma/debug': 6.11.1 '@prisma/internals@5.14.0': dependencies: @@ -10858,20 +10858,20 @@ snapshots: arg: 5.0.2 prompts: 2.4.2 - '@prisma/internals@6.10.0(typescript@5.5.2)': - dependencies: - '@prisma/config': 6.10.0 - '@prisma/debug': 6.10.0 - '@prisma/dmmf': 6.10.0 - '@prisma/driver-adapter-utils': 6.10.0 - '@prisma/engines': 6.10.0 - '@prisma/fetch-engine': 6.10.0 - '@prisma/generator': 6.10.0 - '@prisma/generator-helper': 6.10.0 - '@prisma/get-platform': 6.10.0 - '@prisma/prisma-schema-wasm': 6.10.0-43.aee10d5a411e4360c6d3445ce4810ca65adbf3e8 - '@prisma/schema-engine-wasm': 6.10.0-43.aee10d5a411e4360c6d3445ce4810ca65adbf3e8 - '@prisma/schema-files-loader': 6.10.0 + '@prisma/internals@6.11.1(typescript@5.5.2)': + dependencies: + '@prisma/config': 6.11.1 + '@prisma/debug': 6.11.1 + '@prisma/dmmf': 6.11.1 + '@prisma/driver-adapter-utils': 6.11.1 + '@prisma/engines': 6.11.1 + '@prisma/fetch-engine': 6.11.1 + '@prisma/generator': 6.11.1 + '@prisma/generator-helper': 6.11.1 + '@prisma/get-platform': 6.11.1 + '@prisma/prisma-schema-wasm': 6.11.1-1.f40f79ec31188888a2e33acda0ecc8fd10a853a9 + '@prisma/schema-engine-wasm': 6.11.1-1.f40f79ec31188888a2e33acda0ecc8fd10a853a9 + '@prisma/schema-files-loader': 6.11.1 arg: 5.0.2 prompts: 2.4.2 optionalDependencies: @@ -10881,18 +10881,18 @@ snapshots: '@prisma/prisma-schema-wasm@5.14.0-25.e9771e62de70f79a5e1c604a2d7c8e2a0a874b48': {} - '@prisma/prisma-schema-wasm@6.10.0-43.aee10d5a411e4360c6d3445ce4810ca65adbf3e8': {} + '@prisma/prisma-schema-wasm@6.11.1-1.f40f79ec31188888a2e33acda0ecc8fd10a853a9': {} - '@prisma/schema-engine-wasm@6.10.0-43.aee10d5a411e4360c6d3445ce4810ca65adbf3e8': {} + '@prisma/schema-engine-wasm@6.11.1-1.f40f79ec31188888a2e33acda0ecc8fd10a853a9': {} '@prisma/schema-files-loader@5.14.0': dependencies: '@prisma/prisma-schema-wasm': 5.14.0-17.56ca112d5a19c9925b53af75c3c6b7ada97f9f85 fs-extra: 11.1.1 - '@prisma/schema-files-loader@6.10.0': + '@prisma/schema-files-loader@6.11.1': dependencies: - '@prisma/prisma-schema-wasm': 6.10.0-43.aee10d5a411e4360c6d3445ce4810ca65adbf3e8 + '@prisma/prisma-schema-wasm': 6.11.1-1.f40f79ec31188888a2e33acda0ecc8fd10a853a9 fs-extra: 11.3.0 '@protobufjs/aspromise@1.1.2': {} diff --git a/script/test-scaffold.ts b/script/test-scaffold.ts index 0773666fb..8406b575b 100644 --- a/script/test-scaffold.ts +++ b/script/test-scaffold.ts @@ -19,6 +19,6 @@ function run(cmd: string) { } run('npm init -y'); -run('npm i --no-audit --no-fund typescript prisma@6.10.x @prisma/client@6.10.x zod@^3.22.4 decimal.js @types/node'); +run('npm i --no-audit --no-fund typescript prisma@6.11.x @prisma/client@6.11.x zod@^3.22.4 decimal.js @types/node'); console.log('Test scaffold setup complete.'); diff --git a/tests/integration/test-run/package.json b/tests/integration/test-run/package.json index 79a72aff6..117c8b061 100644 --- a/tests/integration/test-run/package.json +++ b/tests/integration/test-run/package.json @@ -10,9 +10,9 @@ "author": "", "license": "ISC", "dependencies": { - "@prisma/client": "6.10.x", + "@prisma/client": "6.11.x", "@zenstackhq/runtime": "file:../../../packages/runtime/dist", - "prisma": "6.10.x", + "prisma": "6.11.x", "react": "^18.2.0", "swr": "^1.3.0", "typescript": "^4.9.3", diff --git a/tests/integration/tests/cli/generate.test.ts b/tests/integration/tests/cli/generate.test.ts index 857c4fed1..39e1611d5 100644 --- a/tests/integration/tests/cli/generate.test.ts +++ b/tests/integration/tests/cli/generate.test.ts @@ -45,7 +45,7 @@ model Post { // set up project fs.writeFileSync('package.json', JSON.stringify({ name: 'my app', version: '1.0.0' })); createNpmrc(); - installPackage('prisma @prisma/client zod'); + installPackage('prisma @prisma/client zod@3'); installPackage(path.join(__dirname, '../../../../packages/runtime/dist')); // set up schema diff --git a/tests/integration/tests/cli/plugins.test.ts b/tests/integration/tests/cli/plugins.test.ts index 3d1f32536..483add807 100644 --- a/tests/integration/tests/cli/plugins.test.ts +++ b/tests/integration/tests/cli/plugins.test.ts @@ -75,7 +75,7 @@ describe('CLI Plugins Tests', () => { 'swr', '@tanstack/react-query@5.56.x', '@trpc/server', - '@prisma/client@6.10.x', + '@prisma/client@6.11.x', `${path.join(__dirname, '../../../../.build/zenstackhq-language-' + ver + '.tgz')}`, `${path.join(__dirname, '../../../../.build/zenstackhq-sdk-' + ver + '.tgz')}`, `${path.join(__dirname, '../../../../.build/zenstackhq-runtime-' + ver + '.tgz')}`, @@ -85,7 +85,7 @@ describe('CLI Plugins Tests', () => { const devDepPkgs = [ 'typescript', '@types/react', - 'prisma@6.10.x', + 'prisma@6.11.x', `${path.join(__dirname, '../../../../.build/zenstack-' + ver + '.tgz')}`, `${path.join(__dirname, '../../../../.build/zenstackhq-tanstack-query-' + ver + '.tgz')}`, `${path.join(__dirname, '../../../../.build/zenstackhq-swr-' + ver + '.tgz')}`, diff --git a/tests/integration/tests/enhancements/json/typing.test.ts b/tests/integration/tests/enhancements/json/typing.test.ts index 99869accd..607daa54f 100644 --- a/tests/integration/tests/enhancements/json/typing.test.ts +++ b/tests/integration/tests/enhancements/json/typing.test.ts @@ -385,4 +385,37 @@ async function main() { } ); }); + + it('supports @db.Json and @db.JsonB', async () => { + await loadSchema( + ` + type Profile { + age Int @gt(0) + } + + model User { + id Int @id @default(autoincrement()) + profile Profile @json @db.Json + posts Post[] + @@allow('all', true) + } + + type Meta { + description String + } + + model Post { + id Int @id @default(autoincrement()) + title String + user User @relation(fields: [userId], references: [id]) + userId Int + meta Meta @json @db.JsonB + } + `, + { + provider: 'postgresql', + pushDb: false, + } + ); + }); }); diff --git a/tests/integration/tests/frameworks/nextjs/test-project/package.json b/tests/integration/tests/frameworks/nextjs/test-project/package.json index 149e98f23..c071d5759 100644 --- a/tests/integration/tests/frameworks/nextjs/test-project/package.json +++ b/tests/integration/tests/frameworks/nextjs/test-project/package.json @@ -9,7 +9,7 @@ "lint": "next lint" }, "dependencies": { - "@prisma/client": "6.10.x", + "@prisma/client": "6.11.x", "@types/node": "18.11.18", "@types/react": "18.0.27", "@types/react-dom": "18.0.10", @@ -26,6 +26,6 @@ "@zenstackhq/swr": "../../../../../../../packages/plugins/swr/dist" }, "devDependencies": { - "prisma": "6.10.x" + "prisma": "6.11.x" } } diff --git a/tests/integration/tests/frameworks/trpc/test-project/package.json b/tests/integration/tests/frameworks/trpc/test-project/package.json index 5cc6318c8..a7fc82443 100644 --- a/tests/integration/tests/frameworks/trpc/test-project/package.json +++ b/tests/integration/tests/frameworks/trpc/test-project/package.json @@ -9,7 +9,7 @@ "lint": "next lint" }, "dependencies": { - "@prisma/client": "6.10.x", + "@prisma/client": "6.11.x", "@tanstack/react-query": "^4.22.4", "@trpc/client": "^10.34.0", "@trpc/next": "^10.34.0", @@ -31,6 +31,6 @@ "@zenstackhq/trpc": "../../../../../../../packages/plugins/trpc/dist" }, "devDependencies": { - "prisma": "6.10.x" + "prisma": "6.11.x" } } diff --git a/tests/integration/tests/misc/meta-annotation.test.ts b/tests/integration/tests/misc/meta-annotation.test.ts new file mode 100644 index 000000000..1c3988258 --- /dev/null +++ b/tests/integration/tests/misc/meta-annotation.test.ts @@ -0,0 +1,59 @@ +import { loadSchema } from '@zenstackhq/testtools'; + +describe('`@@meta` attribute tests', () => { + it('generates generated into model-meta', async () => { + const { modelMeta } = await loadSchema( + ` + model User { + id Int @id + name String @meta('private', true) @meta('level', 1) + + @@meta(name: 'doc', value: 'It is user model') + @@meta('info', { description: 'This is a user model', geo: { country: 'US' } }) + } + ` + ); + + const model = modelMeta.models.user; + const userModelMeta = model.attributes.filter((attr: any) => attr.name === '@@meta'); + expect(userModelMeta).toEqual( + expect.arrayContaining([ + { + name: '@@meta', + args: expect.arrayContaining([ + { name: 'name', value: 'doc' }, + { name: 'value', value: 'It is user model' }, + ]), + }, + { + name: '@@meta', + args: expect.arrayContaining([ + { name: 'name', value: 'info' }, + { name: 'value', value: { description: 'This is a user model', geo: { country: 'US' } } }, + ]), + }, + ]) + ); + + const field = model.fields.name; + const fieldMeta = field.attributes.filter((attr: any) => attr.name === '@meta'); + expect(fieldMeta).toEqual( + expect.arrayContaining([ + { + name: '@meta', + args: expect.arrayContaining([ + { name: 'name', value: 'private' }, + { name: 'value', value: true }, + ]), + }, + { + name: '@meta', + args: expect.arrayContaining([ + { name: 'name', value: 'level' }, + { name: 'value', value: 1 }, + ]), + }, + ]) + ); + }); +}); diff --git a/tests/integration/tests/misc/prisma-client-generator.test.ts b/tests/integration/tests/misc/prisma-client-generator.test.ts index fbb2813c9..4b0d48151 100644 --- a/tests/integration/tests/misc/prisma-client-generator.test.ts +++ b/tests/integration/tests/misc/prisma-client-generator.test.ts @@ -1,20 +1,240 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('New prisma-client generator tests', () => { - it('works with `auth` in `@default`', async () => { - const { enhance, prisma } = await loadSchema( - ` - datasource db { - provider = "sqlite" - url = "file:./dev.db" +import { createProjectAndCompile, loadSchema } from '@zenstackhq/testtools'; + +const PRISMA_CLIENT_JS_GENERATOR = ` +datasource db { + provider = "sqlite" + url = "file:./dev.db" +} + +generator client { + provider = "prisma-client-js" +} +`; + +const PRISMA_CLIENT_GENERATOR = ` +datasource db { + provider = "sqlite" + url = "file:./dev.db" +} + +generator client { + provider = "prisma-client" + output = "../generated/prisma" + moduleFormat = "cjs" +} +`; + +describe.each([ + { + isNewGenerator: false, + generator: PRISMA_CLIENT_JS_GENERATOR, + prismaLoadPath: '@prisma/client', + clientLoadPath: '@prisma/client', + modelsLoadPath: '@prisma/client', + enumsLoadPath: '@prisma/client', + enhanceLoadPath: '.zenstack/enhance', + output: undefined, + }, + { + isNewGenerator: true, + generator: PRISMA_CLIENT_GENERATOR, + prismaLoadPath: './generated/prisma/client', + clientLoadPath: './generated/zenstack/client', + modelsLoadPath: './generated/zenstack/models', + enumsLoadPath: './generated/zenstack/enums', + enhanceLoadPath: './generated/zenstack/enhance', + output: './generated/zenstack', + }, +])('Prisma-client generator tests', (config) => { + describe('regular client', () => { + it('exports expected modules', async () => { + await createProjectAndCompile( + ` + ${config.generator} + + enum Role { + USER + ADMIN + } + + model User { + id Int @id + name String + role Role + } + `, + { + addPrelude: false, + output: config.output, + prismaLoadPath: config.prismaLoadPath, + extraSourceFiles: [ + { + name: 'main.ts', + content: ` +import { Prisma, type User } from '${config.clientLoadPath}'; +${config.isNewGenerator ? "import type { UserModel } from '" + config.modelsLoadPath + "';" : ''} +import { Role } from '${config.enumsLoadPath}'; + +const user: User = { id: 1, name: 'Alice', role: Role.USER }; +console.log(user); + +${config.isNewGenerator ? "const user1: UserModel = { id: 1, name: 'Alice', role: 'USER' };\nconsole.log(user1);" : ''} + +const role = Role.USER; +console.log(role); + `, + }, + ], + } + ); + }); + + it('works with client extension', async () => { + await createProjectAndCompile( + ` + ${config.generator} + model User { + id Int @id @default(autoincrement()) + email String } + `, + { + addPrelude: false, + output: config.output, + prismaLoadPath: config.prismaLoadPath, + extraSourceFiles: [ + { + name: 'main.ts', + content: ` +import { Prisma } from '${config.clientLoadPath}'; +import { PrismaClient } from '${config.prismaLoadPath}'; +import { enhance } from '${config.enhanceLoadPath}'; + +const prisma = new PrismaClient().$extends({ + model: { + user: { + async signUp(email: string) { + return prisma.user.create({ data: { email } }); + }, + }, + }, +}); + +async function main() { + const db = enhance(prisma); + const newUser = await db.user.signUp('user@test.com') +} + +main() + `, + }, + ], + } + ); + }); + }); - generator client { - provider = "prisma-client" - output = "./prisma-generated" - moduleFormat = "cjs" + describe('logical client', () => { + it('exports expected modules', async () => { + await createProjectAndCompile( + ` + ${config.generator} + + enum Role { + USER + ADMIN } + model User { + id Int @id + name String + role Role + } + + model Post { + id Int @id + authorId Int @default(auth().id) + } + `, + { + addPrelude: false, + output: config.output, + prismaLoadPath: config.prismaLoadPath, + extraSourceFiles: [ + { + name: 'main.ts', + content: ` +import { Prisma, type User } from '${config.clientLoadPath}'; +${config.isNewGenerator ? "import type { UserModel } from '" + config.modelsLoadPath + "';" : ''} +import { Role } from '${config.enumsLoadPath}'; + +const user: User = { id: 1, name: 'Alice', role: Role.USER }; +console.log(user); + +${config.isNewGenerator ? "const user1: UserModel = { id: 1, name: 'Alice', role: 'USER' };\nconsole.log(user1);" : ''} + +const role = Role.USER; +console.log(role); + `, + }, + ], + } + ); + }); + + it('works with client extension', async () => { + await createProjectAndCompile( + ` + ${config.generator} + model User { + id Int @id @default(autoincrement()) + email String + } + + model Post { + id Int @id @default(autoincrement()) + authorId Int @default(auth().id) + } + `, + { + addPrelude: false, + output: config.output, + prismaLoadPath: config.prismaLoadPath, + extraSourceFiles: [ + { + name: 'main.ts', + content: ` +import { PrismaClient } from '${config.prismaLoadPath}'; +import { enhance } from '${config.enhanceLoadPath}'; + +const prisma = new PrismaClient().$extends({ + model: { + user: { + async signUp(email: string) { + return prisma.user.create({ data: { email } }); + }, + }, + }, +}); + +async function main() { + const db = enhance(prisma); + const newUser = await db.user.signUp('user@test.com') +} + +main() + `, + }, + ], + } + ); + }); + + it('works with `auth` in `@default`', async () => { + const { enhance, prisma } = await loadSchema( + ` + ${config.generator} + model User { id Int @id posts Post[] @@ -29,17 +249,17 @@ describe('New prisma-client generator tests', () => { @@allow('all', true) } `, - { - addPrelude: false, - output: './zenstack', - compile: true, - prismaLoadPath: './prisma/prisma-generated/client', - extraSourceFiles: [ - { - name: 'main.ts', - content: ` -import { PrismaClient } from './prisma/prisma-generated/client'; -import { enhance } from './zenstack/enhance'; + { + addPrelude: false, + output: config.output, + compile: true, + prismaLoadPath: config.prismaLoadPath, + extraSourceFiles: [ + { + name: 'main.ts', + content: ` +import { PrismaClient } from '${config.prismaLoadPath}'; +import { enhance } from '${config.enhanceLoadPath}'; const prisma = new PrismaClient(); const db = enhance(prisma); @@ -51,31 +271,22 @@ async function main() { main(); `, - }, - ], - } - ); + }, + ], + } + ); - const user = await prisma.user.create({ data: { id: 1 } }); - const db = enhance({ id: user.id }); - await expect(db.post.create({ data: { id: 1, title: 'Hello World' } })).resolves.toMatchObject({ - authorId: user.id, + const user = await prisma.user.create({ data: { id: 1 } }); + const db = enhance({ id: user.id }); + await expect(db.post.create({ data: { id: 1, title: 'Hello World' } })).resolves.toMatchObject({ + authorId: user.id, + }); }); - }); - - it('works with delegate models', async () => { - const { enhance } = await loadSchema( - ` - datasource db { - provider = "sqlite" - url = "file:./dev.db" - } - generator client { - provider = "prisma-client" - output = "./prisma-generated" - moduleFormat = "cjs" - } + it('works with delegate models', async () => { + const { enhance } = await loadSchema( + ` + ${config.generator} model Asset { id Int @id @@ -88,18 +299,18 @@ main(); title String } `, - { - enhancements: ['delegate'], - addPrelude: false, - output: './zenstack', - compile: true, - prismaLoadPath: './prisma/prisma-generated/client', - extraSourceFiles: [ - { - name: 'main.ts', - content: ` -import { PrismaClient } from './prisma/prisma-generated/client'; -import { enhance } from './zenstack/enhance'; + { + enhancements: ['delegate'], + addPrelude: false, + output: config.output, + compile: true, + prismaLoadPath: config.prismaLoadPath, + extraSourceFiles: [ + { + name: 'main.ts', + content: ` +import { PrismaClient } from '${config.prismaLoadPath}'; +import { enhance } from '${config.enhanceLoadPath}'; const prisma = new PrismaClient(); const db = enhance(prisma); @@ -111,14 +322,15 @@ async function main() { main(); `, - }, - ], - } - ); + }, + ], + } + ); - const db = enhance(); - await expect( - db.post.create({ data: { id: 1, name: 'Test Post', title: 'Hello World' } }) - ).resolves.toMatchObject({ id: 1, name: 'Test Post', type: 'Post', title: 'Hello World' }); + const db = enhance(); + await expect( + db.post.create({ data: { id: 1, name: 'Test Post', title: 'Hello World' } }) + ).resolves.toMatchObject({ id: 1, name: 'Test Post', type: 'Post', title: 'Hello World' }); + }); }); }); diff --git a/tests/regression/tests/issue-2175.test.ts b/tests/regression/tests/issue-2175.test.ts new file mode 100644 index 000000000..7b31aa989 --- /dev/null +++ b/tests/regression/tests/issue-2175.test.ts @@ -0,0 +1,121 @@ +import { loadSchema } from '@zenstackhq/testtools'; + +describe('issue 2175', () => { + it('regression standard generator', async () => { + await loadSchema( + ` + model User { + id Int @id @default(autoincrement()) + email String @unique + posts Post[] + } + + model Post { + id Int @id @default(autoincrement()) + title String + author User? @relation(fields: [authorId], references: [id]) + authorId Int? @default(auth().id) + } + + `, + { + compile: true, + extraSourceFiles: [ + { + name: 'main.ts', + content: ` +import { PrismaClient } from "@prisma/client"; +import { enhance } from ".zenstack/enhance"; + +const prisma = new PrismaClient(); +const prismaExtended = prisma.$extends({ + model: { + user: { + async signUp(email: string) { + return prisma.user.create({ data: { email } }); + }, + }, + }, +}); + +const dbExtended = enhance(prismaExtended); + +async function main() { + const newUser = await dbExtended.user.signUp("a@b.com"); + console.log(newUser); +} + +main(); +`, + }, + ], + } + ); + }); + + it('regression new generator', 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 @default(autoincrement()) + email String @unique + posts Post[] + } + + model Post { + id Int @id @default(autoincrement()) + title String + author User? @relation(fields: [authorId], references: [id]) + authorId Int? @default(auth().id) + } + + `, + { + addPrelude: false, + compile: true, + extraSourceFiles: [ + { + name: 'main.ts', + content: ` +import { PrismaClient } from "./generated/prisma/client"; +import { enhance } from "./generated/zenstack/enhance"; + +const prisma = new PrismaClient(); +const prismaExtended = prisma.$extends({ + model: { + user: { + async signUp(email: string) { + return prisma.user.create({ data: { email } }); + }, + }, + }, +}); + +const dbExtended = enhance(prismaExtended); + +async function main() { + const newUser = await dbExtended.user.signUp("a@b.com"); + console.log(newUser); +} + +main(); +`, + }, + ], + output: './generated/zenstack', + prismaLoadPath: './generated/prisma/client', + } + ); + }); +});