Skip to content

fix: Update re-exports for new prisma-client generator #2184

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jul 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 109 additions & 39 deletions packages/schema/src/plugins/enhancer/enhance/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { DELEGATE_AUX_RELATION_PREFIX } from '@zenstackhq/runtime';
import { invariant, upperCaseFirst } from '@zenstackhq/runtime/local-helpers';
import {
PluginError,
getAttribute,
Expand Down Expand Up @@ -26,7 +27,6 @@ import {
type Model,
} from '@zenstackhq/sdk/ast';
import { getDMMF, getPrismaClientImportSpec, getPrismaVersion, type DMMF } from '@zenstackhq/sdk/prisma';
import { invariant, upperCaseFirst } from '@zenstackhq/runtime/local-helpers';
import fs from 'fs';
import path from 'path';
import semver from 'semver';
Expand Down Expand Up @@ -105,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';
Expand All @@ -154,8 +232,8 @@ ${
}

${
prismaTypesFixed
? this.createLogicalPrismaImports(prismaImport, resultPrismaTypeImport, target)
needsLogicalClient
? this.createLogicalPrismaImports(prismaImport, prismaClientImport, target)
: this.createSimplePrismaImports(prismaImport, target)
}

Expand All @@ -164,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() {
Expand Down Expand Up @@ -210,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}';
Expand Down Expand Up @@ -241,10 +311,10 @@ export function enhance<DbClient extends object>(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 typed only in "@prisma/client"
? // 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`;
Expand Down
158 changes: 80 additions & 78 deletions packages/testtools/src/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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');
Expand All @@ -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 };
}

/**
Expand Down
Loading
Loading