Skip to content

fix: zod and openapi generation error when "fullTextSearch" is enabled #658

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 1 commit into from
Aug 30, 2023
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
11 changes: 9 additions & 2 deletions packages/plugins/openapi/src/rpc-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,13 @@ export class RPCOpenAPIGenerator extends OpenAPIGeneratorBase {
security: read === true ? [] : undefined,
});

// OrderByWithRelationInput's name is different when "fullTextSearch" is enabled
const orderByWithRelationInput = this.inputObjectTypes
.map((o) => upperCaseFirst(o.name))
.includes(`${modelName}OrderByWithRelationInput`)
? `${modelName}OrderByWithRelationInput`
: `${modelName}OrderByWithRelationAndSearchRelevanceInput`;

if (ops['aggregate']) {
definitions.push({
method: 'get',
Expand All @@ -409,7 +416,7 @@ export class RPCOpenAPIGenerator extends OpenAPIGeneratorBase {
type: 'object',
properties: {
where: this.ref(`${modelName}WhereInput`),
orderBy: this.ref(`${modelName}OrderByWithRelationInput`),
orderBy: this.ref(orderByWithRelationInput),
cursor: this.ref(`${modelName}WhereUniqueInput`),
take: { type: 'integer' },
skip: { type: 'integer' },
Expand All @@ -435,7 +442,7 @@ export class RPCOpenAPIGenerator extends OpenAPIGeneratorBase {
type: 'object',
properties: {
where: this.ref(`${modelName}WhereInput`),
orderBy: this.ref(`${modelName}OrderByWithRelationInput`),
orderBy: this.ref(orderByWithRelationInput),
by: this.ref(`${modelName}ScalarFieldEnum`),
having: this.ref(`${modelName}ScalarWhereWithAggregatesInput`),
take: { type: 'integer' },
Expand Down
47 changes: 47 additions & 0 deletions packages/plugins/openapi/tests/openapi-rpc.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,53 @@ model Foo {
const baseline = YAML.parse(fs.readFileSync(`${__dirname}/baseline/rpc-type-coverage.baseline.yaml`, 'utf-8'));
expect(parsed).toMatchObject(baseline);
});

it('full-text search', async () => {
const { model, dmmf, modelFile } = await loadZModelAndDmmf(`
generator js {
provider = 'prisma-client-js'
previewFeatures = ['fullTextSearch']
}

plugin openapi {
provider = '${process.cwd()}/dist'
}

enum role {
USER
ADMIN
}

model User {
id String @id
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
email String @unique
role role @default(USER)
posts post_Item[]
}

model post_Item {
id String @id
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
title String
author User? @relation(fields: [authorId], references: [id])
authorId String?
published Boolean @default(false)
viewCount Int @default(0)
}
`);

const { name: output } = tmp.fileSync({ postfix: '.yaml' });

const options = buildOptions(model, modelFile, output);
await generate(model, options, dmmf);

console.log('OpenAPI specification generated:', output);

await OpenAPIParser.validate(output);
});
});

function buildOptions(model: Model, modelFile: string, output: string) {
Expand Down
4 changes: 3 additions & 1 deletion packages/schema/src/plugins/zod/generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ export async function generate(model: Model, options: PluginOptions, dmmf: DMMF.
aggregateOperationSupport,
project,
zmodel: model,
inputObjectTypes,
});
await transformer.generateInputSchemas();
}
Expand Down Expand Up @@ -149,6 +150,7 @@ async function generateEnumSchemas(
enumTypes,
project,
zmodel,
inputObjectTypes: [],
});
await transformer.generateEnumSchemas();
}
Expand All @@ -163,7 +165,7 @@ async function generateObjectSchemas(
for (let i = 0; i < inputObjectTypes.length; i += 1) {
const fields = inputObjectTypes[i]?.fields;
const name = inputObjectTypes[i]?.name;
const transformer = new Transformer({ name, fields, project, zmodel });
const transformer = new Transformer({ name, fields, project, zmodel, inputObjectTypes });
const moduleName = transformer.generateObjectSchema();
moduleNames.push(moduleName);
}
Expand Down
25 changes: 17 additions & 8 deletions packages/schema/src/plugins/zod/transformer.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
import type { DMMF as PrismaDMMF } from '@prisma/generator-helper';
import type { DMMF, DMMF as PrismaDMMF } from '@prisma/generator-helper';
import { Model } from '@zenstackhq/language/ast';
import { AUXILIARY_FIELDS, getPrismaClientImportSpec, getPrismaVersion } from '@zenstackhq/sdk';
import { checkModelHasModelRelation, findModelByName, isAggregateInputType } from '@zenstackhq/sdk/dmmf-helpers';
import indentString from '@zenstackhq/sdk/utils';
import { indentString } from '@zenstackhq/sdk/utils';
import path from 'path';
import * as semver from 'semver';
import { Project } from 'ts-morph';
Expand All @@ -28,6 +28,7 @@ export default class Transformer {
private hasDecimal = false;
private project: Project;
private zmodel: Model;
private inputObjectTypes: DMMF.InputType[];

constructor(params: TransformerParams) {
this.originalName = params.name ?? '';
Expand All @@ -39,6 +40,7 @@ export default class Transformer {
this.enumTypes = params.enumTypes ?? [];
this.project = params.project;
this.zmodel = params.zmodel;
this.inputObjectTypes = params.inputObjectTypes;
}

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

// OrderByWithRelationInput's name is different when "fullTextSearch" is enabled
const orderByWithRelationInput = this.inputObjectTypes
.map((o) => upperCaseFirst(o.name))
.includes(`${modelName}OrderByWithRelationInput`)
? `${modelName}OrderByWithRelationInput`
: `${modelName}OrderByWithRelationAndSearchRelevanceInput`;

if (findUnique) {
imports.push(
`import { ${modelName}WhereUniqueInputObjectSchema } from '../objects/${modelName}WhereUniqueInput.schema'`
Expand All @@ -431,22 +440,22 @@ export const ${this.name}ObjectSchema: SchemaType = ${schema} as SchemaType;`;
if (findFirst) {
imports.push(
`import { ${modelName}WhereInputObjectSchema } from '../objects/${modelName}WhereInput.schema'`,
`import { ${modelName}OrderByWithRelationInputObjectSchema } from '../objects/${modelName}OrderByWithRelationInput.schema'`,
`import { ${orderByWithRelationInput}ObjectSchema } from '../objects/${orderByWithRelationInput}.schema'`,
`import { ${modelName}WhereUniqueInputObjectSchema } from '../objects/${modelName}WhereUniqueInput.schema'`,
`import { ${modelName}ScalarFieldEnumSchema } from '../enums/${modelName}ScalarFieldEnum.schema'`
);
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() }),`;
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() }),`;
operations.push(['findFirst', origModelName]);
}

if (findMany) {
imports.push(
`import { ${modelName}WhereInputObjectSchema } from '../objects/${modelName}WhereInput.schema'`,
`import { ${modelName}OrderByWithRelationInputObjectSchema } from '../objects/${modelName}OrderByWithRelationInput.schema'`,
`import { ${orderByWithRelationInput}ObjectSchema } from '../objects/${orderByWithRelationInput}.schema'`,
`import { ${modelName}WhereUniqueInputObjectSchema } from '../objects/${modelName}WhereUniqueInput.schema'`,
`import { ${modelName}ScalarFieldEnumSchema } from '../enums/${modelName}ScalarFieldEnum.schema'`
);
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() }),`;
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() }),`;
operations.push(['findMany', origModelName]);
}

Expand Down Expand Up @@ -557,11 +566,11 @@ export const ${this.name}ObjectSchema: SchemaType = ${schema} as SchemaType;`;
if (aggregate) {
imports.push(
`import { ${modelName}WhereInputObjectSchema } from '../objects/${modelName}WhereInput.schema'`,
`import { ${modelName}OrderByWithRelationInputObjectSchema } from '../objects/${modelName}OrderByWithRelationInput.schema'`,
`import { ${orderByWithRelationInput}ObjectSchema } from '../objects/${orderByWithRelationInput}.schema'`,
`import { ${modelName}WhereUniqueInputObjectSchema } from '../objects/${modelName}WhereUniqueInput.schema'`
);

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(
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(
', '
)} }),`;
operations.push(['aggregate', modelName]);
Expand Down
3 changes: 2 additions & 1 deletion packages/schema/src/plugins/zod/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { DMMF as PrismaDMMF } from '@prisma/generator-helper';
import { DMMF, DMMF as PrismaDMMF } from '@prisma/generator-helper';
import { Model } from '@zenstackhq/language/ast';
import { Project } from 'ts-morph';

Expand All @@ -13,6 +13,7 @@ export type TransformerParams = {
prismaClientOutputPath?: string;
project: Project;
zmodel: Model;
inputObjectTypes: DMMF.InputType[];
};

export type AggregateOperationSupport = {
Expand Down
14 changes: 10 additions & 4 deletions packages/schema/src/res/stdlib.zmodel
Original file line number Diff line number Diff line change
Expand Up @@ -239,12 +239,12 @@ attribute @map(_ name: String) @@@prisma
attribute @@map(_ name: String) @@@prisma

/**
* Exclude a field from the Prisma Client (for example, a field that you do not want Prisma users to update).
* Exclude a field from the Prisma Client (for example, a field that you do not want Prisma users to update).
*/
attribute @ignore() @@@prisma

/**
* Exclude a model from the Prisma Client (for example, a model that you do not want Prisma users to update).
* Exclude a model from the Prisma Client (for example, a model that you do not want Prisma users to update).
*/
attribute @@ignore() @@@prisma

Expand All @@ -253,6 +253,12 @@ attribute @@ignore() @@@prisma
*/
attribute @updatedAt() @@@targetField([DateTimeField]) @@@prisma

/**
* Add full text index (MySQL only).
*/
attribute @@fulltext(_ fields: FieldReference[]) @@@prisma


// String type modifiers

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

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

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

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

Expand Down
21 changes: 20 additions & 1 deletion packages/sdk/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ import {
EnumField,
Expression,
FunctionDecl,
GeneratorDecl,
InternalAttribute,
isArrayExpr,
isDataModel,
isDataModelField,
isEnumField,
isGeneratorDecl,
isInvocationExpr,
isLiteralExpr,
isModel,
Expand Down Expand Up @@ -88,7 +90,7 @@ export function getObjectLiteral<T>(expr: Expression | undefined): T | undefined
return result as T;
}

export default function indentString(string: string, count = 4): string {
export function indentString(string: string, count = 4): string {
const indent = ' ';
return string.replace(/^(?!\s*$)/gm, indent.repeat(count));
}
Expand Down Expand Up @@ -298,3 +300,20 @@ export function getContainingModel(node: AstNode | undefined): Model | null {
}
return isModel(node) ? node : getContainingModel(node.$container);
}

export function getPreviewFeatures(model: Model) {
const jsGenerator = model.declarations.find(
(d) =>
isGeneratorDecl(d) &&
d.fields.some((f) => f.name === 'provider' && getLiteral<string>(f.value) === 'prisma-client-js')
) as GeneratorDecl | undefined;

if (jsGenerator) {
const previewFeaturesField = jsGenerator.fields.find((f) => f.name === 'previewFeatures');
if (previewFeaturesField) {
return getLiteralArray<string>(previewFeaturesField.value);
}
}

return [] as string[];
}
43 changes: 39 additions & 4 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading