Skip to content

Commit 485be72

Browse files
authored
feat: support for the new "prisma-client" generator (#2151)
1 parent 2d63bff commit 485be72

File tree

10 files changed

+275
-63
lines changed

10 files changed

+275
-63
lines changed

packages/plugins/tanstack-query/src/generator.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
ensureEmptyDir,
77
generateModelMeta,
88
getDataModels,
9+
getPrismaClientGenerator,
910
isDelegateModel,
1011
requireOption,
1112
resolvePath,
@@ -52,7 +53,6 @@ export async function generate(model: Model, options: PluginOptions, dmmf: DMMF.
5253
`Invalid value for "portable" option: ${options.portable}, a boolean value is expected`
5354
);
5455
}
55-
const portable = options.portable ?? false;
5656

5757
await generateModelMeta(project, models, typeDefs, {
5858
output: path.join(outDir, '__model_meta.ts'),
@@ -70,8 +70,13 @@ export async function generate(model: Model, options: PluginOptions, dmmf: DMMF.
7070
generateModelHooks(target, version, project, outDir, dataModel, mapping, options);
7171
});
7272

73-
if (portable) {
74-
generateBundledTypes(project, outDir, options);
73+
if (options.portable) {
74+
const gen = getPrismaClientGenerator(model);
75+
if (gen?.isNewGenerator) {
76+
warnings.push(`The "portable" option is not supported with the "prisma-client" generator and is ignored.`);
77+
} else {
78+
generateBundledTypes(project, outDir, options);
79+
}
7580
}
7681

7782
await saveProject(project);

packages/schema/src/cli/actions/generate.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { PluginError } from '@zenstackhq/sdk';
1+
import { getPrismaClientGenerator, PluginError } from '@zenstackhq/sdk';
22
import { isPlugin } from '@zenstackhq/sdk/ast';
33
import colors from 'colors';
44
import path from 'path';
@@ -70,6 +70,18 @@ async function runPlugins(options: Options) {
7070

7171
const model = await loadDocument(schema);
7272

73+
const gen = getPrismaClientGenerator(model);
74+
if (gen?.isNewGenerator && !options.output) {
75+
console.error(
76+
colors.red(
77+
'When using the "prisma-client" generator, you must provide an explicit output path with the "--output" CLI parameter.'
78+
)
79+
);
80+
throw new CliError(
81+
'When using with the "prisma-client" generator, you must provide an explicit output path with the "--output" CLI parameter.'
82+
);
83+
}
84+
7385
for (const name of [...(options.withPlugins ?? []), ...(options.withoutPlugins ?? [])]) {
7486
const pluginDecl = model.declarations.find((d) => isPlugin(d) && d.name === name);
7587
if (!pluginDecl) {

packages/schema/src/cli/plugin-runner.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -112,10 +112,7 @@ export class PluginRunner {
112112
const otherPlugins = plugins.filter((p) => !p.options.preprocessor);
113113

114114
// calculate all plugins (including core plugins implicitly enabled)
115-
const { corePlugins, userPlugins } = this.calculateAllPlugins(
116-
runnerOptions,
117-
otherPlugins,
118-
);
115+
const { corePlugins, userPlugins } = this.calculateAllPlugins(runnerOptions, otherPlugins);
119116
const allPlugins = [...corePlugins, ...userPlugins];
120117

121118
// check dependencies
@@ -448,7 +445,7 @@ export class PluginRunner {
448445
}
449446

450447
async function compileProject(project: Project, runnerOptions: PluginRunnerOptions) {
451-
if (runnerOptions.compile !== false) {
448+
if (!runnerOptions.output && runnerOptions.compile !== false) {
452449
// emit
453450
await emitProject(project);
454451
} else {

packages/schema/src/plugins/enhancer/enhance/index.ts

Lines changed: 55 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
getDataModelAndTypeDefs,
88
getDataModels,
99
getForeignKeyFields,
10-
getLiteral,
10+
getPrismaClientGenerator,
1111
getRelationField,
1212
hasAttribute,
1313
isDelegateModel,
@@ -22,7 +22,6 @@ import {
2222
ReferenceExpr,
2323
isArrayExpr,
2424
isDataModel,
25-
isGeneratorDecl,
2625
isTypeDef,
2726
type Model,
2827
} from '@zenstackhq/sdk/ast';
@@ -56,7 +55,7 @@ import { generateTypeDefType } from './model-typedef-generator';
5655
// information of delegate models and their sub models
5756
type DelegateInfo = [DataModel, DataModel[]][];
5857

59-
const LOGICAL_CLIENT_GENERATION_PATH = './.logical-prisma-client';
58+
const LOGICAL_CLIENT_GENERATION_PATH = './logical-prisma-client';
6059

6160
export class EnhancerGenerator {
6261
// regex for matching "ModelCreateXXXInput" and "ModelUncheckedCreateXXXInput" type
@@ -114,6 +113,9 @@ export class EnhancerGenerator {
114113
if (this.needsLogicalClient) {
115114
prismaTypesFixed = true;
116115
resultPrismaTypeImport = LOGICAL_CLIENT_GENERATION_PATH;
116+
if (this.isNewPrismaClientGenerator) {
117+
resultPrismaTypeImport += '/client';
118+
}
117119
const result = await this.generateLogicalPrisma();
118120
dmmf = result.dmmf;
119121
}
@@ -440,23 +442,14 @@ export type Enhanced<Client> =
440442
}
441443

442444
private getPrismaClientGeneratorName(model: Model) {
443-
for (const generator of model.declarations.filter(isGeneratorDecl)) {
444-
if (
445-
generator.fields.some(
446-
(f) => f.name === 'provider' && getLiteral<string>(f.value) === 'prisma-client-js'
447-
)
448-
) {
449-
return generator.name;
450-
}
445+
const gen = getPrismaClientGenerator(model);
446+
if (!gen) {
447+
throw new PluginError(name, `Cannot find "prisma-client-js" or "prisma-client" generator in the schema`);
451448
}
452-
throw new PluginError(name, `Cannot find prisma-client-js generator in the schema`);
449+
return gen.name;
453450
}
454451

455452
private async processClientTypes(prismaClientDir: string) {
456-
// make necessary updates to the generated `index.d.ts` file and overwrite it
457-
const project = new Project();
458-
const sf = project.addSourceFileAtPath(path.join(prismaClientDir, 'index.d.ts'));
459-
460453
// build a map of delegate models and their sub models
461454
const delegateInfo: DelegateInfo = [];
462455
this.model.declarations
@@ -468,6 +461,16 @@ export type Enhanced<Client> =
468461
}
469462
});
470463

464+
if (this.isNewPrismaClientGenerator) {
465+
await this.processClientTypesNewPrismaGenerator(prismaClientDir, delegateInfo);
466+
} else {
467+
await this.processClientTypesLegacyPrismaGenerator(prismaClientDir, delegateInfo);
468+
}
469+
}
470+
private async processClientTypesLegacyPrismaGenerator(prismaClientDir: string, delegateInfo: DelegateInfo) {
471+
const project = new Project();
472+
const sf = project.addSourceFileAtPath(path.join(prismaClientDir, 'index.d.ts'));
473+
471474
// transform index.d.ts and write it into a new file (better perf than in-line editing)
472475
const sfNew = project.createSourceFile(path.join(prismaClientDir, 'index-fixed.d.ts'), undefined, {
473476
overwrite: true,
@@ -484,6 +487,36 @@ export type Enhanced<Client> =
484487
await sfNew.save();
485488
}
486489

490+
private async processClientTypesNewPrismaGenerator(prismaClientDir: string, delegateInfo: DelegateInfo) {
491+
const project = new Project();
492+
493+
for (const d of this.model.declarations.filter(isDataModel)) {
494+
const fileName = `${prismaClientDir}/models/${d.name}.ts`;
495+
const sf = project.addSourceFileAtPath(fileName);
496+
const sfNew = project.createSourceFile(`${prismaClientDir}/models/${d.name}-fixed.ts`, undefined, {
497+
overwrite: true,
498+
});
499+
500+
const syntaxList = sf.getChildren()[0];
501+
if (!Node.isSyntaxList(syntaxList)) {
502+
throw new PluginError(name, `Unexpected syntax list structure in ${fileName}`);
503+
}
504+
505+
syntaxList.getChildren().forEach((node) => {
506+
if (Node.isInterfaceDeclaration(node)) {
507+
sfNew.addInterface(this.transformInterface(node, delegateInfo));
508+
} else if (Node.isTypeAliasDeclaration(node)) {
509+
sfNew.addTypeAlias(this.transformTypeAlias(node, delegateInfo));
510+
} else {
511+
sfNew.addStatements(node.getText());
512+
}
513+
});
514+
515+
await sfNew.move(sf.getFilePath(), { overwrite: true });
516+
await sfNew.save();
517+
}
518+
}
519+
487520
private transformPrismaTypes(sf: SourceFile, sfNew: SourceFile, delegateInfo: DelegateInfo) {
488521
// copy toplevel imports
489522
sfNew.addImportDeclarations(sf.getImportDeclarations().map((n) => n.getStructure()));
@@ -639,7 +672,7 @@ export type Enhanced<Client> =
639672
source = `${payloadRecord[1]
640673
.map(
641674
(concrete) =>
642-
`($${concrete.name}Payload<ExtArgs> & { scalars: { ${discriminatorDecl.name}: '${concrete.name}' } })`
675+
`(Prisma.$${concrete.name}Payload<ExtArgs> & { scalars: { ${discriminatorDecl.name}: '${concrete.name}' } })`
643676
)
644677
.join(' | ')}`;
645678
}
@@ -916,4 +949,9 @@ export type Enhanced<Client> =
916949
private trimEmptyLines(source: string): string {
917950
return source.replace(/^\s*[\r\n]/gm, '');
918951
}
952+
953+
private get isNewPrismaClientGenerator() {
954+
const gen = getPrismaClientGenerator(this.model);
955+
return !!gen?.isNewGenerator;
956+
}
919957
}

packages/schema/src/plugins/prisma/index.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,10 @@ import {
22
PluginError,
33
type PluginFunction,
44
type PluginOptions,
5-
getLiteral,
5+
getPrismaClientGenerator,
66
normalizedRelative,
77
resolvePath,
88
} from '@zenstackhq/sdk';
9-
import { GeneratorDecl, isGeneratorDecl } from '@zenstackhq/sdk/ast';
109
import { getDMMF } from '@zenstackhq/sdk/prisma';
1110
import colors from 'colors';
1211
import fs from 'fs';
@@ -58,13 +57,9 @@ const run: PluginFunction = async (model, options, _dmmf, _globalOptions) => {
5857
}
5958

6059
// extract user-provided prisma client output path
61-
const generator = model.declarations.find(
62-
(d): d is GeneratorDecl =>
63-
isGeneratorDecl(d) &&
64-
d.fields.some((f) => f.name === 'provider' && getLiteral(f.value) === 'prisma-client-js')
65-
);
66-
const clientOutputField = generator?.fields.find((f) => f.name === 'output');
67-
const clientOutput = getLiteral<string>(clientOutputField?.value);
60+
const gen = getPrismaClientGenerator(model);
61+
const clientOutput = gen?.output;
62+
const newGenerator = !!gen?.isNewGenerator;
6863

6964
if (clientOutput) {
7065
if (path.isAbsolute(clientOutput)) {
@@ -81,6 +76,11 @@ const run: PluginFunction = async (model, options, _dmmf, _globalOptions) => {
8176
clientOutputDir = prismaClientPath;
8277
}
8378

79+
if (newGenerator) {
80+
// "prisma-client" generator requires an extra "/client" import suffix
81+
prismaClientPath = `${prismaClientPath}/client`;
82+
}
83+
8484
// get PrismaClient dts path
8585

8686
if (clientOutput) {
@@ -89,7 +89,7 @@ const run: PluginFunction = async (model, options, _dmmf, _globalOptions) => {
8989
prismaClientDtsPath = path.resolve(path.dirname(options.schemaPath), clientOutputDir, 'index.d.ts');
9090
}
9191

92-
if (!prismaClientDtsPath || !fs.existsSync(prismaClientDtsPath)) {
92+
if (!newGenerator && (!prismaClientDtsPath || !fs.existsSync(prismaClientDtsPath))) {
9393
// if the file does not exist, try node module resolution
9494
try {
9595
// the resolution is relative to the schema path by default

packages/schema/src/plugins/prisma/schema-generator.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,10 @@ export class PrismaSchemaGenerator {
234234

235235
// deal with configuring PrismaClient preview features
236236
const provider = generator.fields.find((f) => f.name === 'provider');
237-
if (provider?.text === JSON.stringify('prisma-client-js')) {
237+
if (
238+
provider?.text === JSON.stringify('prisma-client-js') ||
239+
provider?.text === JSON.stringify('prisma-client')
240+
) {
238241
const prismaVersion = getPrismaVersion();
239242
if (prismaVersion) {
240243
const previewFeatures = JSON.parse(

packages/sdk/src/utils.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -466,7 +466,11 @@ export function getPreviewFeatures(model: Model) {
466466
const jsGenerator = model.declarations.find(
467467
(d) =>
468468
isGeneratorDecl(d) &&
469-
d.fields.some((f) => f.name === 'provider' && getLiteral<string>(f.value) === 'prisma-client-js')
469+
d.fields.some(
470+
(f) =>
471+
(f.name === 'provider' && getLiteral<string>(f.value) === 'prisma-client-js') ||
472+
getLiteral<string>(f.value) === 'prisma-client'
473+
)
470474
) as GeneratorDecl | undefined;
471475

472476
if (jsGenerator) {
@@ -683,3 +687,28 @@ export function getRelationName(field: DataModelField) {
683687
}
684688
return getAttributeArgLiteral(relAttr, 'name');
685689
}
690+
691+
export function getPrismaClientGenerator(model: Model) {
692+
const decl = model.declarations.find(
693+
(d): d is GeneratorDecl =>
694+
isGeneratorDecl(d) &&
695+
d.fields.some(
696+
(f) =>
697+
f.name === 'provider' &&
698+
(getLiteral<string>(f.value) === 'prisma-client-js' ||
699+
getLiteral<string>(f.value) === 'prisma-client')
700+
)
701+
);
702+
if (!decl) {
703+
return undefined;
704+
}
705+
706+
const provider = getLiteral<string>(decl.fields.find((f) => f.name === 'provider')?.value);
707+
return {
708+
name: decl.name,
709+
output: getLiteral<string>(decl.fields.find((f) => f.name === 'output')?.value),
710+
previewFeatures: getLiteralArray<string>(decl.fields.find((f) => f.name === 'previewFeatures')?.value),
711+
provider,
712+
isNewGenerator: provider === 'prisma-client',
713+
};
714+
}

packages/testtools/src/schema.ts

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -278,27 +278,6 @@ export async function loadSchema(schema: string, options?: SchemaLoadOptions) {
278278
fs.cpSync(dep, path.join(projectDir, 'node_modules', pkgJson.name), { recursive: true, force: true });
279279
});
280280

281-
const prismaLoadPath = options?.prismaLoadPath
282-
? path.isAbsolute(options.prismaLoadPath)
283-
? options.prismaLoadPath
284-
: path.join(projectDir, options.prismaLoadPath)
285-
: path.join(projectDir, 'node_modules/.prisma/client');
286-
const prismaModule = require(prismaLoadPath);
287-
const PrismaClient = prismaModule.PrismaClient;
288-
289-
let clientOptions: object = { log: ['info', 'warn', 'error'] };
290-
if (options?.prismaClientOptions) {
291-
clientOptions = { ...clientOptions, ...options.prismaClientOptions };
292-
}
293-
let prisma = new PrismaClient(clientOptions);
294-
// https://github.com/prisma/prisma/issues/18292
295-
prisma[Symbol.for('nodejs.util.inspect.custom')] = 'PrismaClient';
296-
297-
if (opt.pulseApiKey) {
298-
const withPulse = loadModule('@prisma/extension-pulse/node', projectDir).withPulse;
299-
prisma = prisma.$extends(withPulse({ apiKey: opt.pulseApiKey }));
300-
}
301-
302281
opt.extraSourceFiles?.forEach(({ name, content }) => {
303282
fs.writeFileSync(path.join(projectDir, name), content);
304283
});
@@ -325,6 +304,27 @@ export async function loadSchema(schema: string, options?: SchemaLoadOptions) {
325304
run('npx tsc --project tsconfig.json');
326305
}
327306

307+
const prismaLoadPath = options?.prismaLoadPath
308+
? path.isAbsolute(options.prismaLoadPath)
309+
? options.prismaLoadPath
310+
: path.join(projectDir, options.prismaLoadPath)
311+
: path.join(projectDir, 'node_modules/.prisma/client');
312+
const prismaModule = require(prismaLoadPath);
313+
const PrismaClient = prismaModule.PrismaClient;
314+
315+
let clientOptions: object = { log: ['info', 'warn', 'error'] };
316+
if (options?.prismaClientOptions) {
317+
clientOptions = { ...clientOptions, ...options.prismaClientOptions };
318+
}
319+
let prisma = new PrismaClient(clientOptions);
320+
// https://github.com/prisma/prisma/issues/18292
321+
prisma[Symbol.for('nodejs.util.inspect.custom')] = 'PrismaClient';
322+
323+
if (opt.pulseApiKey) {
324+
const withPulse = loadModule('@prisma/extension-pulse/node', projectDir).withPulse;
325+
prisma = prisma.$extends(withPulse({ apiKey: opt.pulseApiKey }));
326+
}
327+
328328
if (options?.getPrismaOnly) {
329329
return {
330330
prisma,

0 commit comments

Comments
 (0)