Skip to content

Commit 0cb7cd1

Browse files
authored
fix: zod and openapi generation error when "fullTextSearch" is enabled (#658)
1 parent e147412 commit 0cb7cd1

File tree

9 files changed

+193
-21
lines changed

9 files changed

+193
-21
lines changed

packages/plugins/openapi/src/rpc-generator.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,13 @@ export class RPCOpenAPIGenerator extends OpenAPIGeneratorBase {
399399
security: read === true ? [] : undefined,
400400
});
401401

402+
// OrderByWithRelationInput's name is different when "fullTextSearch" is enabled
403+
const orderByWithRelationInput = this.inputObjectTypes
404+
.map((o) => upperCaseFirst(o.name))
405+
.includes(`${modelName}OrderByWithRelationInput`)
406+
? `${modelName}OrderByWithRelationInput`
407+
: `${modelName}OrderByWithRelationAndSearchRelevanceInput`;
408+
402409
if (ops['aggregate']) {
403410
definitions.push({
404411
method: 'get',
@@ -409,7 +416,7 @@ export class RPCOpenAPIGenerator extends OpenAPIGeneratorBase {
409416
type: 'object',
410417
properties: {
411418
where: this.ref(`${modelName}WhereInput`),
412-
orderBy: this.ref(`${modelName}OrderByWithRelationInput`),
419+
orderBy: this.ref(orderByWithRelationInput),
413420
cursor: this.ref(`${modelName}WhereUniqueInput`),
414421
take: { type: 'integer' },
415422
skip: { type: 'integer' },
@@ -435,7 +442,7 @@ export class RPCOpenAPIGenerator extends OpenAPIGeneratorBase {
435442
type: 'object',
436443
properties: {
437444
where: this.ref(`${modelName}WhereInput`),
438-
orderBy: this.ref(`${modelName}OrderByWithRelationInput`),
445+
orderBy: this.ref(orderByWithRelationInput),
439446
by: this.ref(`${modelName}ScalarFieldEnum`),
440447
having: this.ref(`${modelName}ScalarWhereWithAggregatesInput`),
441448
take: { type: 'integer' },

packages/plugins/openapi/tests/openapi-rpc.test.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,53 @@ model Foo {
376376
const baseline = YAML.parse(fs.readFileSync(`${__dirname}/baseline/rpc-type-coverage.baseline.yaml`, 'utf-8'));
377377
expect(parsed).toMatchObject(baseline);
378378
});
379+
380+
it('full-text search', async () => {
381+
const { model, dmmf, modelFile } = await loadZModelAndDmmf(`
382+
generator js {
383+
provider = 'prisma-client-js'
384+
previewFeatures = ['fullTextSearch']
385+
}
386+
387+
plugin openapi {
388+
provider = '${process.cwd()}/dist'
389+
}
390+
391+
enum role {
392+
USER
393+
ADMIN
394+
}
395+
396+
model User {
397+
id String @id
398+
createdAt DateTime @default(now())
399+
updatedAt DateTime @updatedAt
400+
email String @unique
401+
role role @default(USER)
402+
posts post_Item[]
403+
}
404+
405+
model post_Item {
406+
id String @id
407+
createdAt DateTime @default(now())
408+
updatedAt DateTime @updatedAt
409+
title String
410+
author User? @relation(fields: [authorId], references: [id])
411+
authorId String?
412+
published Boolean @default(false)
413+
viewCount Int @default(0)
414+
}
415+
`);
416+
417+
const { name: output } = tmp.fileSync({ postfix: '.yaml' });
418+
419+
const options = buildOptions(model, modelFile, output);
420+
await generate(model, options, dmmf);
421+
422+
console.log('OpenAPI specification generated:', output);
423+
424+
await OpenAPIParser.validate(output);
425+
});
379426
});
380427

381428
function buildOptions(model: Model, modelFile: string, output: string) {

packages/schema/src/plugins/zod/generator.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ export async function generate(model: Model, options: PluginOptions, dmmf: DMMF.
8181
aggregateOperationSupport,
8282
project,
8383
zmodel: model,
84+
inputObjectTypes,
8485
});
8586
await transformer.generateInputSchemas();
8687
}
@@ -149,6 +150,7 @@ async function generateEnumSchemas(
149150
enumTypes,
150151
project,
151152
zmodel,
153+
inputObjectTypes: [],
152154
});
153155
await transformer.generateEnumSchemas();
154156
}
@@ -163,7 +165,7 @@ async function generateObjectSchemas(
163165
for (let i = 0; i < inputObjectTypes.length; i += 1) {
164166
const fields = inputObjectTypes[i]?.fields;
165167
const name = inputObjectTypes[i]?.name;
166-
const transformer = new Transformer({ name, fields, project, zmodel });
168+
const transformer = new Transformer({ name, fields, project, zmodel, inputObjectTypes });
167169
const moduleName = transformer.generateObjectSchema();
168170
moduleNames.push(moduleName);
169171
}

packages/schema/src/plugins/zod/transformer.ts

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
/* eslint-disable @typescript-eslint/ban-ts-comment */
2-
import type { DMMF as PrismaDMMF } from '@prisma/generator-helper';
2+
import type { DMMF, DMMF as PrismaDMMF } from '@prisma/generator-helper';
33
import { Model } from '@zenstackhq/language/ast';
44
import { AUXILIARY_FIELDS, getPrismaClientImportSpec, getPrismaVersion } from '@zenstackhq/sdk';
55
import { checkModelHasModelRelation, findModelByName, isAggregateInputType } from '@zenstackhq/sdk/dmmf-helpers';
6-
import indentString from '@zenstackhq/sdk/utils';
6+
import { indentString } from '@zenstackhq/sdk/utils';
77
import path from 'path';
88
import * as semver from 'semver';
99
import { Project } from 'ts-morph';
@@ -28,6 +28,7 @@ export default class Transformer {
2828
private hasDecimal = false;
2929
private project: Project;
3030
private zmodel: Model;
31+
private inputObjectTypes: DMMF.InputType[];
3132

3233
constructor(params: TransformerParams) {
3334
this.originalName = params.name ?? '';
@@ -39,6 +40,7 @@ export default class Transformer {
3940
this.enumTypes = params.enumTypes ?? [];
4041
this.project = params.project;
4142
this.zmodel = params.zmodel;
43+
this.inputObjectTypes = params.inputObjectTypes;
4244
}
4345

4446
static setOutputPath(outPath: string) {
@@ -420,6 +422,13 @@ export const ${this.name}ObjectSchema: SchemaType = ${schema} as SchemaType;`;
420422
let codeBody = '';
421423
const operations: [string, string][] = [];
422424

425+
// OrderByWithRelationInput's name is different when "fullTextSearch" is enabled
426+
const orderByWithRelationInput = this.inputObjectTypes
427+
.map((o) => upperCaseFirst(o.name))
428+
.includes(`${modelName}OrderByWithRelationInput`)
429+
? `${modelName}OrderByWithRelationInput`
430+
: `${modelName}OrderByWithRelationAndSearchRelevanceInput`;
431+
423432
if (findUnique) {
424433
imports.push(
425434
`import { ${modelName}WhereUniqueInputObjectSchema } from '../objects/${modelName}WhereUniqueInput.schema'`
@@ -431,22 +440,22 @@ export const ${this.name}ObjectSchema: SchemaType = ${schema} as SchemaType;`;
431440
if (findFirst) {
432441
imports.push(
433442
`import { ${modelName}WhereInputObjectSchema } from '../objects/${modelName}WhereInput.schema'`,
434-
`import { ${modelName}OrderByWithRelationInputObjectSchema } from '../objects/${modelName}OrderByWithRelationInput.schema'`,
443+
`import { ${orderByWithRelationInput}ObjectSchema } from '../objects/${orderByWithRelationInput}.schema'`,
435444
`import { ${modelName}WhereUniqueInputObjectSchema } from '../objects/${modelName}WhereUniqueInput.schema'`,
436445
`import { ${modelName}ScalarFieldEnumSchema } from '../enums/${modelName}ScalarFieldEnum.schema'`
437446
);
438-
codeBody += `findFirst: z.object({ ${selectZodSchemaLineLazy} ${includeZodSchemaLineLazy} where: ${modelName}WhereInputObjectSchema.optional(), orderBy: z.union([${modelName}OrderByWithRelationInputObjectSchema, ${modelName}OrderByWithRelationInputObjectSchema.array()]).optional(), cursor: ${modelName}WhereUniqueInputObjectSchema.optional(), take: z.number().optional(), skip: z.number().optional(), distinct: z.array(${modelName}ScalarFieldEnumSchema).optional() }),`;
447+
codeBody += `findFirst: z.object({ ${selectZodSchemaLineLazy} ${includeZodSchemaLineLazy} where: ${modelName}WhereInputObjectSchema.optional(), orderBy: z.union([${orderByWithRelationInput}ObjectSchema, ${orderByWithRelationInput}ObjectSchema.array()]).optional(), cursor: ${modelName}WhereUniqueInputObjectSchema.optional(), take: z.number().optional(), skip: z.number().optional(), distinct: z.array(${modelName}ScalarFieldEnumSchema).optional() }),`;
439448
operations.push(['findFirst', origModelName]);
440449
}
441450

442451
if (findMany) {
443452
imports.push(
444453
`import { ${modelName}WhereInputObjectSchema } from '../objects/${modelName}WhereInput.schema'`,
445-
`import { ${modelName}OrderByWithRelationInputObjectSchema } from '../objects/${modelName}OrderByWithRelationInput.schema'`,
454+
`import { ${orderByWithRelationInput}ObjectSchema } from '../objects/${orderByWithRelationInput}.schema'`,
446455
`import { ${modelName}WhereUniqueInputObjectSchema } from '../objects/${modelName}WhereUniqueInput.schema'`,
447456
`import { ${modelName}ScalarFieldEnumSchema } from '../enums/${modelName}ScalarFieldEnum.schema'`
448457
);
449-
codeBody += `findMany: z.object({ ${selectZodSchemaLineLazy} ${includeZodSchemaLineLazy} where: ${modelName}WhereInputObjectSchema.optional(), orderBy: z.union([${modelName}OrderByWithRelationInputObjectSchema, ${modelName}OrderByWithRelationInputObjectSchema.array()]).optional(), cursor: ${modelName}WhereUniqueInputObjectSchema.optional(), take: z.number().optional(), skip: z.number().optional(), distinct: z.array(${modelName}ScalarFieldEnumSchema).optional() }),`;
458+
codeBody += `findMany: z.object({ ${selectZodSchemaLineLazy} ${includeZodSchemaLineLazy} where: ${modelName}WhereInputObjectSchema.optional(), orderBy: z.union([${orderByWithRelationInput}ObjectSchema, ${orderByWithRelationInput}ObjectSchema.array()]).optional(), cursor: ${modelName}WhereUniqueInputObjectSchema.optional(), take: z.number().optional(), skip: z.number().optional(), distinct: z.array(${modelName}ScalarFieldEnumSchema).optional() }),`;
450459
operations.push(['findMany', origModelName]);
451460
}
452461

@@ -557,11 +566,11 @@ export const ${this.name}ObjectSchema: SchemaType = ${schema} as SchemaType;`;
557566
if (aggregate) {
558567
imports.push(
559568
`import { ${modelName}WhereInputObjectSchema } from '../objects/${modelName}WhereInput.schema'`,
560-
`import { ${modelName}OrderByWithRelationInputObjectSchema } from '../objects/${modelName}OrderByWithRelationInput.schema'`,
569+
`import { ${orderByWithRelationInput}ObjectSchema } from '../objects/${orderByWithRelationInput}.schema'`,
561570
`import { ${modelName}WhereUniqueInputObjectSchema } from '../objects/${modelName}WhereUniqueInput.schema'`
562571
);
563572

564-
codeBody += `aggregate: z.object({ where: ${modelName}WhereInputObjectSchema.optional(), orderBy: z.union([${modelName}OrderByWithRelationInputObjectSchema, ${modelName}OrderByWithRelationInputObjectSchema.array()]).optional(), cursor: ${modelName}WhereUniqueInputObjectSchema.optional(), take: z.number().optional(), skip: z.number().optional(), ${aggregateOperations.join(
573+
codeBody += `aggregate: z.object({ where: ${modelName}WhereInputObjectSchema.optional(), orderBy: z.union([${orderByWithRelationInput}ObjectSchema, ${orderByWithRelationInput}ObjectSchema.array()]).optional(), cursor: ${modelName}WhereUniqueInputObjectSchema.optional(), take: z.number().optional(), skip: z.number().optional(), ${aggregateOperations.join(
565574
', '
566575
)} }),`;
567576
operations.push(['aggregate', modelName]);

packages/schema/src/plugins/zod/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { DMMF as PrismaDMMF } from '@prisma/generator-helper';
1+
import { DMMF, DMMF as PrismaDMMF } from '@prisma/generator-helper';
22
import { Model } from '@zenstackhq/language/ast';
33
import { Project } from 'ts-morph';
44

@@ -13,6 +13,7 @@ export type TransformerParams = {
1313
prismaClientOutputPath?: string;
1414
project: Project;
1515
zmodel: Model;
16+
inputObjectTypes: DMMF.InputType[];
1617
};
1718

1819
export type AggregateOperationSupport = {

packages/schema/src/res/stdlib.zmodel

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -239,12 +239,12 @@ attribute @map(_ name: String) @@@prisma
239239
attribute @@map(_ name: String) @@@prisma
240240

241241
/**
242-
* Exclude a field from the Prisma Client (for example, a field that you do not want Prisma users to update).
242+
* Exclude a field from the Prisma Client (for example, a field that you do not want Prisma users to update).
243243
*/
244244
attribute @ignore() @@@prisma
245245

246246
/**
247-
* Exclude a model from the Prisma Client (for example, a model that you do not want Prisma users to update).
247+
* Exclude a model from the Prisma Client (for example, a model that you do not want Prisma users to update).
248248
*/
249249
attribute @@ignore() @@@prisma
250250

@@ -253,6 +253,12 @@ attribute @@ignore() @@@prisma
253253
*/
254254
attribute @updatedAt() @@@targetField([DateTimeField]) @@@prisma
255255

256+
/**
257+
* Add full text index (MySQL only).
258+
*/
259+
attribute @@fulltext(_ fields: FieldReference[]) @@@prisma
260+
261+
256262
// String type modifiers
257263

258264
attribute @db.String(_ x: Int?) @@@targetField([StringField]) @@@prisma
@@ -352,7 +358,7 @@ attribute @@schema(_ name: String) @@@prisma
352358
attribute @@allow(_ operation: String, _ condition: Boolean)
353359

354360
/**
355-
* Defines an access policy that allows a set of operations when the given condition is true.
361+
* Defines an access policy that allows the annotated field to be read or updated.
356362
*/
357363
attribute @allow(_ operation: String, _ condition: Boolean)
358364

@@ -362,7 +368,7 @@ attribute @allow(_ operation: String, _ condition: Boolean)
362368
attribute @@deny(_ operation: String, _ condition: Boolean)
363369

364370
/**
365-
* Defines an access policy that denies a set of operations when the given condition is true.
371+
* Defines an access policy that denies the annotated field to be read or updated.
366372
*/
367373
attribute @deny(_ operation: String, _ condition: Boolean)
368374

packages/sdk/src/utils.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@ import {
88
EnumField,
99
Expression,
1010
FunctionDecl,
11+
GeneratorDecl,
1112
InternalAttribute,
1213
isArrayExpr,
1314
isDataModel,
1415
isDataModelField,
1516
isEnumField,
17+
isGeneratorDecl,
1618
isInvocationExpr,
1719
isLiteralExpr,
1820
isModel,
@@ -88,7 +90,7 @@ export function getObjectLiteral<T>(expr: Expression | undefined): T | undefined
8890
return result as T;
8991
}
9092

91-
export default function indentString(string: string, count = 4): string {
93+
export function indentString(string: string, count = 4): string {
9294
const indent = ' ';
9395
return string.replace(/^(?!\s*$)/gm, indent.repeat(count));
9496
}
@@ -298,3 +300,20 @@ export function getContainingModel(node: AstNode | undefined): Model | null {
298300
}
299301
return isModel(node) ? node : getContainingModel(node.$container);
300302
}
303+
304+
export function getPreviewFeatures(model: Model) {
305+
const jsGenerator = model.declarations.find(
306+
(d) =>
307+
isGeneratorDecl(d) &&
308+
d.fields.some((f) => f.name === 'provider' && getLiteral<string>(f.value) === 'prisma-client-js')
309+
) as GeneratorDecl | undefined;
310+
311+
if (jsGenerator) {
312+
const previewFeaturesField = jsGenerator.fields.find((f) => f.name === 'previewFeatures');
313+
if (previewFeaturesField) {
314+
return getLiteralArray<string>(previewFeaturesField.value);
315+
}
316+
}
317+
318+
return [] as string[];
319+
}

pnpm-lock.yaml

Lines changed: 39 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)