Skip to content
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
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/feature_request.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
title: '[Feature Request] '
labels: ''
assignees: ''
---
Expand Down
21 changes: 16 additions & 5 deletions packages/schema/src/plugins/zod/generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ export async function generate(model: Model, options: PluginOptions, dmmf: DMMF.
const outputObjectTypes = prismaClientDmmf.schema.outputObjectTypes.prisma;
const models: DMMF.Model[] = prismaClientDmmf.datamodel.models;

// whether Prisma's Unchecked* series of input types should be generated
const generateUnchecked = options.noUncheckedInput !== true;

const project = createProject();

// common schemas
Expand All @@ -71,7 +74,7 @@ export async function generate(model: Model, options: PluginOptions, dmmf: DMMF.
Transformer.provider = dataSourceProvider;
addMissingInputObjectTypes(inputObjectTypes, outputObjectTypes, models);
const aggregateOperationSupport = resolveAggregateOperationSupport(inputObjectTypes);
await generateObjectSchemas(inputObjectTypes, project, output, model);
await generateObjectSchemas(inputObjectTypes, project, output, model, generateUnchecked);

// input schemas
const transformer = new Transformer({
Expand All @@ -82,7 +85,7 @@ export async function generate(model: Model, options: PluginOptions, dmmf: DMMF.
zmodel: model,
inputObjectTypes,
});
await transformer.generateInputSchemas();
await transformer.generateInputSchemas(generateUnchecked);
}

// create barrel file
Expand Down Expand Up @@ -146,14 +149,18 @@ async function generateObjectSchemas(
inputObjectTypes: DMMF.InputType[],
project: Project,
output: string,
zmodel: Model
zmodel: Model,
generateUnchecked: boolean
) {
const moduleNames: string[] = [];
for (let i = 0; i < inputObjectTypes.length; i += 1) {
const fields = inputObjectTypes[i]?.fields;
const name = inputObjectTypes[i]?.name;
if (!generateUnchecked && name.includes('Unchecked')) {
continue;
}
const transformer = new Transformer({ name, fields, project, zmodel, inputObjectTypes });
const moduleName = transformer.generateObjectSchema();
const moduleName = transformer.generateObjectSchema(generateUnchecked);
moduleNames.push(moduleName);
}
project.createSourceFile(
Expand Down Expand Up @@ -236,7 +243,11 @@ async function generateModelSchema(model: DataModel, project: Project, output: s
// compile "@@validate" to ".refine"
const refinements = makeValidationRefinements(model);
if (refinements.length > 0) {
writer.writeLine(`function refine<T, D extends z.ZodTypeDef>(schema: z.ZodType<T, D, T>) { return schema${refinements.join('\n')}; }`);
writer.writeLine(
`function refine<T, D extends z.ZodTypeDef>(schema: z.ZodType<T, D, T>) { return schema${refinements.join(
'\n'
)}; }`
);
}

// model schema
Expand Down
72 changes: 55 additions & 17 deletions packages/schema/src/plugins/zod/transformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,8 @@ export default class Transformer {
return `export const ${name}Schema = ${schema}`;
}

generateObjectSchema() {
const zodObjectSchemaFields = this.generateObjectSchemaFields();
generateObjectSchema(generateUnchecked: boolean) {
const zodObjectSchemaFields = this.generateObjectSchemaFields(generateUnchecked);
const objectSchema = this.prepareObjectSchema(zodObjectSchemaFields);

const filePath = path.join(Transformer.outputPath, `objects/${this.name}.schema.ts`);
Expand All @@ -86,9 +86,9 @@ export default class Transformer {
return `${this.name}.schema`;
}

generateObjectSchemaFields() {
generateObjectSchemaFields(generateUnchecked: boolean) {
const zodObjectSchemaFields = this.fields
.map((field) => this.generateObjectSchemaField(field))
.map((field) => this.generateObjectSchemaField(field, generateUnchecked))
.flatMap((item) => item)
.map((item) => {
const [zodStringWithMainType, field, skipValidators] = item;
Expand All @@ -102,14 +102,21 @@ export default class Transformer {
return zodObjectSchemaFields;
}

generateObjectSchemaField(field: PrismaDMMF.SchemaArg): [string, PrismaDMMF.SchemaArg, boolean][] {
generateObjectSchemaField(
field: PrismaDMMF.SchemaArg,
generateUnchecked: boolean
): [string, PrismaDMMF.SchemaArg, boolean][] {
const lines = field.inputTypes;

if (lines.length === 0) {
return [];
}

let alternatives = lines.reduce<string[]>((result, inputType) => {
if (!generateUnchecked && typeof inputType.type === 'string' && inputType.type.includes('Unchecked')) {
return result;
}

if (inputType.type === 'String') {
result.push(this.wrapWithZodValidators('z.string()', field, inputType));
} else if (inputType.type === 'Int' || inputType.type === 'Float') {
Expand Down Expand Up @@ -377,7 +384,7 @@ export const ${this.name}ObjectSchema: SchemaType = ${schema} as SchemaType;`;
return wrapped;
}

async generateInputSchemas() {
async generateInputSchemas(generateUnchecked: boolean) {
const globalExports: string[] = [];

for (const modelOperation of this.modelOperations) {
Expand Down Expand Up @@ -460,10 +467,17 @@ export const ${this.name}ObjectSchema: SchemaType = ${schema} as SchemaType;`;

if (createOne) {
imports.push(
`import { ${modelName}CreateInputObjectSchema } from '../objects/${modelName}CreateInput.schema'`,
`import { ${modelName}UncheckedCreateInputObjectSchema } from '../objects/${modelName}UncheckedCreateInput.schema'`
`import { ${modelName}CreateInputObjectSchema } from '../objects/${modelName}CreateInput.schema'`
);
codeBody += `create: z.object({ ${selectZodSchemaLineLazy} ${includeZodSchemaLineLazy} data: z.union([${modelName}CreateInputObjectSchema, ${modelName}UncheckedCreateInputObjectSchema]) }),`;
if (generateUnchecked) {
imports.push(
`import { ${modelName}UncheckedCreateInputObjectSchema } from '../objects/${modelName}UncheckedCreateInput.schema'`
);
}
const dataSchema = generateUnchecked
? `z.union([${modelName}CreateInputObjectSchema, ${modelName}UncheckedCreateInputObjectSchema])`
: `${modelName}CreateInputObjectSchema`;
codeBody += `create: z.object({ ${selectZodSchemaLineLazy} ${includeZodSchemaLineLazy} data: ${dataSchema} }),`;
operations.push(['create', origModelName]);
}

Expand Down Expand Up @@ -494,32 +508,56 @@ export const ${this.name}ObjectSchema: SchemaType = ${schema} as SchemaType;`;
if (updateOne) {
imports.push(
`import { ${modelName}UpdateInputObjectSchema } from '../objects/${modelName}UpdateInput.schema'`,
`import { ${modelName}UncheckedUpdateInputObjectSchema } from '../objects/${modelName}UncheckedUpdateInput.schema'`,
`import { ${modelName}WhereUniqueInputObjectSchema } from '../objects/${modelName}WhereUniqueInput.schema'`
);
codeBody += `update: z.object({ ${selectZodSchemaLineLazy} ${includeZodSchemaLineLazy} data: z.union([${modelName}UpdateInputObjectSchema, ${modelName}UncheckedUpdateInputObjectSchema]), where: ${modelName}WhereUniqueInputObjectSchema }),`;
if (generateUnchecked) {
imports.push(
`import { ${modelName}UncheckedUpdateInputObjectSchema } from '../objects/${modelName}UncheckedUpdateInput.schema'`
);
}
const dataSchema = generateUnchecked
? `z.union([${modelName}UpdateInputObjectSchema, ${modelName}UncheckedUpdateInputObjectSchema])`
: `${modelName}UpdateInputObjectSchema`;
codeBody += `update: z.object({ ${selectZodSchemaLineLazy} ${includeZodSchemaLineLazy} data: ${dataSchema}, where: ${modelName}WhereUniqueInputObjectSchema }),`;
operations.push(['update', origModelName]);
}

if (updateMany) {
imports.push(
`import { ${modelName}UpdateManyMutationInputObjectSchema } from '../objects/${modelName}UpdateManyMutationInput.schema'`,
`import { ${modelName}UncheckedUpdateManyInputObjectSchema } from '../objects/${modelName}UncheckedUpdateManyInput.schema'`,
`import { ${modelName}WhereInputObjectSchema } from '../objects/${modelName}WhereInput.schema'`
);
codeBody += `updateMany: z.object({ data: z.union([${modelName}UpdateManyMutationInputObjectSchema, ${modelName}UncheckedUpdateManyInputObjectSchema]), where: ${modelName}WhereInputObjectSchema.optional() }),`;
if (generateUnchecked) {
imports.push(
`import { ${modelName}UncheckedUpdateManyInputObjectSchema } from '../objects/${modelName}UncheckedUpdateManyInput.schema'`
);
}
const dataSchema = generateUnchecked
? `z.union([${modelName}UpdateManyMutationInputObjectSchema, ${modelName}UncheckedUpdateManyInputObjectSchema])`
: `${modelName}UpdateManyMutationInputObjectSchema`;
codeBody += `updateMany: z.object({ data: ${dataSchema}, where: ${modelName}WhereInputObjectSchema.optional() }),`;
operations.push(['updateMany', origModelName]);
}

if (upsertOne) {
imports.push(
`import { ${modelName}WhereUniqueInputObjectSchema } from '../objects/${modelName}WhereUniqueInput.schema'`,
`import { ${modelName}CreateInputObjectSchema } from '../objects/${modelName}CreateInput.schema'`,
`import { ${modelName}UncheckedCreateInputObjectSchema } from '../objects/${modelName}UncheckedCreateInput.schema'`,
`import { ${modelName}UpdateInputObjectSchema } from '../objects/${modelName}UpdateInput.schema'`,
`import { ${modelName}UncheckedUpdateInputObjectSchema } from '../objects/${modelName}UncheckedUpdateInput.schema'`
`import { ${modelName}UpdateInputObjectSchema } from '../objects/${modelName}UpdateInput.schema'`
);
codeBody += `upsert: z.object({ ${selectZodSchemaLineLazy} ${includeZodSchemaLineLazy} where: ${modelName}WhereUniqueInputObjectSchema, create: z.union([${modelName}CreateInputObjectSchema, ${modelName}UncheckedCreateInputObjectSchema]), update: z.union([${modelName}UpdateInputObjectSchema, ${modelName}UncheckedUpdateInputObjectSchema]) }),`;
if (generateUnchecked) {
imports.push(
`import { ${modelName}UncheckedCreateInputObjectSchema } from '../objects/${modelName}UncheckedCreateInput.schema'`,
`import { ${modelName}UncheckedUpdateInputObjectSchema } from '../objects/${modelName}UncheckedUpdateInput.schema'`
);
}
const createSchema = generateUnchecked
? `z.union([${modelName}CreateInputObjectSchema, ${modelName}UncheckedCreateInputObjectSchema])`
: `${modelName}CreateInputObjectSchema`;
const updateSchema = generateUnchecked
? `z.union([${modelName}UpdateInputObjectSchema, ${modelName}UncheckedUpdateInputObjectSchema])`
: `${modelName}UpdateInputObjectSchema`;
codeBody += `upsert: z.object({ ${selectZodSchemaLineLazy} ${includeZodSchemaLineLazy} where: ${modelName}WhereUniqueInputObjectSchema, create: ${createSchema}, update: ${updateSchema} }),`;
operations.push(['upsert', origModelName]);
}

Expand Down
1 change: 0 additions & 1 deletion packages/server/src/api/rpc/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,6 @@ class RequestHandler extends APIHandlerBase {
return { status: resCode, body: response };
} catch (err) {
if (isPrismaClientKnownRequestError(err)) {
logError(logger, err.code, err.message);
const status = ERROR_STATUS_MAPPING[err.code] ?? 400;

const { error } = this.makeError(
Expand Down
88 changes: 81 additions & 7 deletions tests/integration/tests/plugins/zod.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ describe('Zod plugin tests', () => {
});

it('basic generation', async () => {
const model = `
const { zodSchemas } = await loadSchema(
`
datasource db {
provider = 'postgresql'
url = env('DATABASE_URL')
Expand All @@ -24,16 +25,16 @@ describe('Zod plugin tests', () => {
generator js {
provider = 'prisma-client-js'
}

plugin zod {
provider = "@core/zod"
}

enum Role {
USER
ADMIN
}

model User {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
Expand All @@ -54,9 +55,9 @@ describe('Zod plugin tests', () => {
published Boolean @default(false)
viewCount Int @default(0)
}
`;

const { zodSchemas } = await loadSchema(model, { addPrelude: false, pushDb: false });
`,
{ addPrelude: false, pushDb: false }
);
const schemas = zodSchemas.models;
expect(schemas.UserSchema).toBeTruthy();
expect(schemas.UserCreateSchema).toBeTruthy();
Expand All @@ -74,12 +75,25 @@ describe('Zod plugin tests', () => {
schemas.UserCreateSchema.safeParse({ email: 'abc@zenstack.dev', role: 'ADMIN', password: 'abc123' }).success
).toBeTruthy();

// create unchecked
// create unchecked
expect(
zodSchemas.input.UserInputSchema.create.safeParse({
data: { id: 1, email: 'abc@zenstack.dev', password: 'abc123' },
}).success
).toBeTruthy();

// update
expect(schemas.UserUpdateSchema.safeParse({}).success).toBeTruthy();
expect(schemas.UserUpdateSchema.safeParse({ email: 'abc@def.com' }).success).toBeFalsy();
expect(schemas.UserUpdateSchema.safeParse({ email: 'def@zenstack.dev' }).success).toBeTruthy();
expect(schemas.UserUpdateSchema.safeParse({ password: 'password456' }).success).toBeTruthy();

// update unchecked
expect(
zodSchemas.input.UserInputSchema.update.safeParse({ where: { id: 1 }, data: { id: 2 } }).success
).toBeTruthy();

// model schema
expect(schemas.UserSchema.safeParse({ email: 'abc@zenstack.dev', role: 'ADMIN' }).success).toBeFalsy();
// without omitted field
Expand Down Expand Up @@ -413,4 +427,64 @@ describe('Zod plugin tests', () => {

await loadSchema(model, { addPrelude: false, pushDb: false });
});

it('no unchecked input', async () => {
const { zodSchemas } = await loadSchema(
`
datasource db {
provider = 'postgresql'
url = env('DATABASE_URL')
}

generator js {
provider = 'prisma-client-js'
}

plugin zod {
provider = "@core/zod"
noUncheckedInput = true
}

enum Role {
USER
ADMIN
}

model User {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
email String @unique @email @endsWith('@zenstack.dev')
password String @omit
role Role @default(USER)
posts Post[]
}

model Post {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
title String @length(5, 10)
author User? @relation(fields: [authorId], references: [id])
authorId Int?
published Boolean @default(false)
viewCount Int @default(0)
}
`,
{ addPrelude: false, pushDb: false }
);
const schemas = zodSchemas.models;

// create unchecked
expect(
zodSchemas.input.UserInputSchema.create.safeParse({
data: { id: 1, email: 'abc@zenstack.dev', password: 'abc123' },
}).success
).toBeFalsy();

// update unchecked
expect(
zodSchemas.input.UserInputSchema.update.safeParse({ where: { id: 1 }, data: { id: 2 } }).success
).toBeFalsy();
});
});