@hackapulco/prisma-zod-generator
Version:
Prisma 2+ generator to emit Zod schemas from your Prisma schema
531 lines (454 loc) • 18.7 kB
text/typescript
import type { DMMF as PrismaDMMF } from '@prisma/generator-helper';
import path from 'path';
import { TransformerParams } from './types';
import { writeFileSafely } from './utils/writeFileSafely';
export default class Transformer {
name: string;
fields: PrismaDMMF.SchemaArg[];
schemaImports = new Set<string>();
modelOperations: PrismaDMMF.ModelMapping[];
enumTypes: PrismaDMMF.SchemaEnum[];
static enumNames: string[] = [];
private static outputPath: string = './generated';
private hasJson = false;
static isDefaultPrismaClientOutput?: boolean;
static prismaClientOutputPath?: string;
constructor(params: TransformerParams) {
this.name = params.name ?? '';
this.fields = params.fields ?? [];
this.modelOperations = params.modelOperations ?? [];
this.enumTypes = params.enumTypes ?? [];
}
static setOutputPath(outPath: string) {
this.outputPath = outPath;
}
static getOutputPath() {
return this.outputPath;
}
addSchemaImport(name: string) {
this.schemaImports.add(name);
}
getAllSchemaImports() {
return [...this.schemaImports]
.map((name) =>
Transformer.enumNames.includes(name)
? `import { ${name}Schema } from '../enums/${name}.schema';`
: `import { ${name}ObjectSchema } from './${name}.schema';`,
)
.join(';\r\n');
}
getPrismaStringLine(
field: PrismaDMMF.SchemaArg,
inputType: PrismaDMMF.SchemaArgInputType,
inputsLength: number,
) {
const isEnum = inputType.location === 'enumTypes';
let objectSchemaLine = `${inputType.type}ObjectSchema`;
let enumSchemaLine = `${inputType.type}Schema`;
const schema =
inputType.type === this.name
? objectSchemaLine
: isEnum
? enumSchemaLine
: objectSchemaLine;
const arr = inputType.isList ? '.array()' : '';
const opt = !field.isRequired ? '.optional()' : '';
return inputsLength === 1
? ` ${field.name}: z.lazy(() => ${schema})${arr}${opt}`
: `z.lazy(() => ${schema})${arr}${opt}`;
}
wrapWithZodValidators(
mainValidator: string,
field: PrismaDMMF.SchemaArg,
inputType: PrismaDMMF.SchemaArgInputType,
) {
let line: string = '';
line = mainValidator;
if (inputType.isList) {
line += '.array()';
}
if (!field.isRequired) {
line += '.optional()';
}
return line;
}
getObjectSchemaLine(
field: PrismaDMMF.SchemaArg,
): [string, PrismaDMMF.SchemaArg, boolean][] {
let lines = field.inputTypes;
if (lines.length === 0) {
return [];
}
let alternatives = lines.reduce<string[]>((result, inputType) => {
if (inputType.type === 'String') {
result.push(this.wrapWithZodValidators('z.string()', field, inputType));
} else if (
inputType.type === 'Int' ||
inputType.type === 'Float' ||
inputType.type === 'Decimal'
) {
result.push(this.wrapWithZodValidators('z.number()', field, inputType));
} else if (inputType.type === 'BigInt') {
result.push(
this.wrapWithZodValidators('z.bigint()', field, inputType),
);
} else if (inputType.type === 'Boolean') {
result.push(
this.wrapWithZodValidators('z.boolean()', field, inputType),
);
} else if (inputType.type === 'DateTime') {
result.push(this.wrapWithZodValidators('z.date()', field, inputType));
} else if (inputType.type === 'Json') {
this.hasJson = true;
result.push(this.wrapWithZodValidators('jsonSchema', field, inputType));
} else {
const isEnum = inputType.location === 'enumTypes';
if (inputType.namespace === 'prisma' || isEnum) {
if (
inputType.type !== this.name &&
typeof inputType.type === 'string'
) {
this.addSchemaImport(inputType.type);
}
result.push(this.getPrismaStringLine(field, inputType, lines.length));
}
}
return result;
}, []);
if (alternatives.length === 0) {
return [];
}
if (alternatives.length > 1) {
alternatives = alternatives.map((alter) =>
alter.replace('.optional()', ''),
);
}
const fieldName = alternatives.some((alt) => alt.includes(':'))
? ''
: ` ${field.name}:`;
const opt = !field.isRequired ? '.optional()' : '';
let resString =
alternatives.length === 1
? alternatives.join(',\r\n')
: `z.union([${alternatives.join(',\r\n')}])${opt}`;
if (field.isNullable) {
resString += '.nullable()';
}
return [[` ${fieldName} ${resString} `, field, true]];
}
getFieldValidators(
zodStringWithMainType: string,
field: PrismaDMMF.SchemaArg,
) {
const { isRequired, isNullable } = field;
if (!isRequired) {
zodStringWithMainType += '.optional()';
}
if (isNullable) {
zodStringWithMainType += '.nullable()';
}
return zodStringWithMainType;
}
getImportZod() {
let zodImportStatement = "import { z } from 'zod';";
zodImportStatement += '\n';
return zodImportStatement;
}
getImportPrisma() {
let prismaClientPath = '@prisma/client';
if (Transformer.isDefaultPrismaClientOutput) {
prismaClientPath = Transformer.prismaClientOutputPath ?? '';
prismaClientPath = path
.relative(
path.join(Transformer.outputPath, 'schemas', 'objects'),
prismaClientPath,
)
.split(path.sep)
.join(path.posix.sep);
}
return `import type { Prisma } from '${prismaClientPath}';\n\n`;
}
getJsonSchemaImplementation() {
let jsonShemaImplementation = '';
if (this.hasJson) {
jsonShemaImplementation += `\n`;
jsonShemaImplementation += `const literalSchema = z.union([z.string(), z.number(), z.boolean()]);\n`;
jsonShemaImplementation += `const jsonSchema: z.ZodType<Prisma.InputJsonValue> = z.lazy(() =>\n`;
jsonShemaImplementation += ` z.union([literalSchema, z.array(jsonSchema.nullable()), z.record(jsonSchema.nullable())])\n`;
jsonShemaImplementation += `);\n\n`;
}
return jsonShemaImplementation;
}
getImportsForObjectSchemas() {
let imports = this.getImportZod();
imports += this.getAllSchemaImports();
imports += '\n\n';
return imports;
}
getImportsForSchemas(additionalImports: string[]) {
let imports = this.getImportZod();
imports += [...additionalImports].join(';\r\n');
imports += '\n\n';
return imports;
}
addExportObjectSchema(schema: string) {
const end = `export const ${this.name}ObjectSchema = Schema`;
return `const Schema: z.ZodType<Prisma.${this.name}> = ${schema};\n\n ${end}`;
}
addExportSchema(schema: string, name: string) {
return `export const ${name}Schema = ${schema}`;
}
wrapWithZodObject(zodStringFields: string | string[]) {
let wrapped = '';
wrapped += 'z.object({';
wrapped += '\n';
wrapped += ' ' + zodStringFields;
wrapped += '\n';
wrapped += '})';
return wrapped;
}
wrapWithZodOUnion(zodStringFields: string[]) {
let wrapped = '';
wrapped += 'z.union([';
wrapped += '\n';
wrapped += ' ' + zodStringFields.join(',');
wrapped += '\n';
wrapped += '])';
return wrapped;
}
addFinalWrappers({ zodStringFields }: { zodStringFields: string[] }) {
const fields = [...zodStringFields];
const shouldWrapWithUnion = fields.some(
(field) =>
// TODO handle other cases if any
// field.includes('create:') ||
field.includes('connectOrCreate:') || field.includes('connect:'),
);
if (!shouldWrapWithUnion) {
return this.wrapWithZodObject(fields) + '.strict()';
}
const wrapped = fields.map((field) => this.wrapWithZodObject(field));
return this.wrapWithZodOUnion(wrapped);
}
getFinalForm(zodStringFields: string[]) {
const objectSchema = `${this.addExportObjectSchema(
this.addFinalWrappers({ zodStringFields }),
)}\n`;
const prismaImport = this.getImportPrisma();
const json = this.getJsonSchemaImplementation();
return `${this.getImportsForObjectSchemas()}${prismaImport}${json}${objectSchema}`;
}
async printObjectSchemas() {
const zodStringFields = this.fields
.map((field) => this.getObjectSchemaLine(field))
.flatMap((item) => item)
.map((item) => {
const [zodStringWithMainType, field, skipValidators] = item;
const value = skipValidators
? zodStringWithMainType
: this.getFieldValidators(zodStringWithMainType, field);
return value.trim();
});
await writeFileSafely(
path.join(
Transformer.outputPath,
`schemas/objects/${this.name}.schema.ts`,
),
this.getFinalForm(zodStringFields),
);
}
async printModelSchemas() {
for (const model of this.modelOperations) {
const {
model: modelName,
findUnique,
findFirst,
findMany,
// @ts-ignore
createOne,
createMany,
// @ts-ignore
deleteOne,
// @ts-ignore
updateOne,
deleteMany,
updateMany,
// @ts-ignore
upsertOne,
aggregate,
groupBy,
} = model;
if (findUnique) {
const imports = [
`import { ${modelName}WhereUniqueInputObjectSchema } from './objects/${modelName}WhereUniqueInput.schema'`,
];
await writeFileSafely(
path.join(Transformer.outputPath, `schemas/${findUnique}.schema.ts`),
`${this.getImportsForSchemas(imports)}${this.addExportSchema(
`z.object({ where: ${modelName}WhereUniqueInputObjectSchema })`,
`${modelName}FindUnique`,
)}`,
);
}
if (findFirst) {
const imports = [
`import { ${modelName}WhereInputObjectSchema } from './objects/${modelName}WhereInput.schema'`,
`import { ${modelName}OrderByWithRelationInputObjectSchema } from './objects/${modelName}OrderByWithRelationInput.schema'`,
`import { ${modelName}WhereUniqueInputObjectSchema } from './objects/${modelName}WhereUniqueInput.schema'`,
`import { ${modelName}ScalarFieldEnumSchema } from './enums/${modelName}ScalarFieldEnum.schema'`,
];
await writeFileSafely(
path.join(Transformer.outputPath, `schemas/${findFirst}.schema.ts`),
`${this.getImportsForSchemas(imports)}${this.addExportSchema(
`z.object({ where: ${modelName}WhereInputObjectSchema.optional(), orderBy: ${modelName}OrderByWithRelationInputObjectSchema.optional(), cursor: ${modelName}WhereUniqueInputObjectSchema.optional(), take: z.number().optional(), skip: z.number().optional(), distinct: z.array(${modelName}ScalarFieldEnumSchema).optional() })`,
`${modelName}FindFirst`,
)}`,
);
}
if (findMany) {
const imports = [
`import { ${modelName}WhereInputObjectSchema } from './objects/${modelName}WhereInput.schema'`,
`import { ${modelName}OrderByWithRelationInputObjectSchema } from './objects/${modelName}OrderByWithRelationInput.schema'`,
`import { ${modelName}WhereUniqueInputObjectSchema } from './objects/${modelName}WhereUniqueInput.schema'`,
`import { ${modelName}ScalarFieldEnumSchema } from './enums/${modelName}ScalarFieldEnum.schema'`,
];
await writeFileSafely(
path.join(Transformer.outputPath, `schemas/${findMany}.schema.ts`),
`${this.getImportsForSchemas(imports)}${this.addExportSchema(
`z.object({ where: ${modelName}WhereInputObjectSchema.optional(), orderBy: ${modelName}OrderByWithRelationInputObjectSchema.optional(), cursor: ${modelName}WhereUniqueInputObjectSchema.optional(), take: z.number().optional(), skip: z.number().optional(), distinct: z.array(${modelName}ScalarFieldEnumSchema).optional() })`,
`${modelName}FindMany`,
)}`,
);
}
if (createOne) {
const imports = [
`import { ${modelName}CreateInputObjectSchema } from './objects/${modelName}CreateInput.schema'`,
];
await writeFileSafely(
path.join(Transformer.outputPath, `schemas/${createOne}.schema.ts`),
`${this.getImportsForSchemas(imports)}${this.addExportSchema(
`z.object({ data: ${modelName}CreateInputObjectSchema })`,
`${modelName}CreateOne`,
)}`,
);
}
if (createMany) {
const imports = [
`import { ${modelName}CreateManyInputObjectSchema } from './objects/${modelName}CreateManyInput.schema'`,
];
await writeFileSafely(
path.join(Transformer.outputPath, `schemas/${createMany}.schema.ts`),
`${this.getImportsForSchemas(imports)}${this.addExportSchema(
`z.object({ data: ${modelName}CreateManyInputObjectSchema })`,
`${modelName}CreateMany`,
)}`,
);
}
if (deleteOne) {
const imports = [
`import { ${modelName}WhereUniqueInputObjectSchema } from './objects/${modelName}WhereUniqueInput.schema'`,
];
await writeFileSafely(
path.join(Transformer.outputPath, `schemas/${deleteOne}.schema.ts`),
`${this.getImportsForSchemas(imports)}${this.addExportSchema(
`z.object({ where: ${modelName}WhereUniqueInputObjectSchema })`,
`${modelName}DeleteOne`,
)}`,
);
}
if (deleteMany) {
const imports = [
`import { ${modelName}WhereInputObjectSchema } from './objects/${modelName}WhereInput.schema'`,
];
await writeFileSafely(
path.join(Transformer.outputPath, `schemas/${deleteMany}.schema.ts`),
`${this.getImportsForSchemas(imports)}${this.addExportSchema(
`z.object({ where: ${modelName}WhereInputObjectSchema.optional() })`,
`${modelName}DeleteMany`,
)}`,
);
}
if (updateOne) {
const imports = [
`import { ${modelName}UpdateInputObjectSchema } from './objects/${modelName}UpdateInput.schema'`,
`import { ${modelName}WhereUniqueInputObjectSchema } from './objects/${modelName}WhereUniqueInput.schema'`,
];
await writeFileSafely(
path.join(Transformer.outputPath, `schemas/${updateOne}.schema.ts`),
`${this.getImportsForSchemas(imports)}${this.addExportSchema(
`z.object({ data: ${modelName}UpdateInputObjectSchema, where: ${modelName}WhereUniqueInputObjectSchema })`,
`${modelName}UpdateOne`,
)}`,
);
}
if (updateMany) {
const imports = [
`import { ${modelName}UpdateManyMutationInputObjectSchema } from './objects/${modelName}UpdateManyMutationInput.schema'`,
`import { ${modelName}WhereInputObjectSchema } from './objects/${modelName}WhereInput.schema'`,
];
await writeFileSafely(
path.join(Transformer.outputPath, `schemas/${updateMany}.schema.ts`),
`${this.getImportsForSchemas(imports)}${this.addExportSchema(
`z.object({ data: ${modelName}UpdateManyMutationInputObjectSchema, where: ${modelName}WhereInputObjectSchema.optional() })`,
`${modelName}UpdateMany`,
)}`,
);
}
if (upsertOne) {
const imports = [
`import { ${modelName}WhereUniqueInputObjectSchema } from './objects/${modelName}WhereUniqueInput.schema'`,
`import { ${modelName}CreateInputObjectSchema } from './objects/${modelName}CreateInput.schema'`,
`import { ${modelName}UpdateInputObjectSchema } from './objects/${modelName}UpdateInput.schema'`,
];
await writeFileSafely(
path.join(Transformer.outputPath, `schemas/${upsertOne}.schema.ts`),
`${this.getImportsForSchemas(imports)}${this.addExportSchema(
`z.object({ where: ${modelName}WhereUniqueInputObjectSchema, create: ${modelName}CreateInputObjectSchema, update: ${modelName}UpdateInputObjectSchema })`,
`${modelName}Upsert`,
)}`,
);
}
if (aggregate) {
const imports = [
`import { ${modelName}WhereInputObjectSchema } from './objects/${modelName}WhereInput.schema'`,
`import { ${modelName}OrderByWithRelationInputObjectSchema } from './objects/${modelName}OrderByWithRelationInput.schema'`,
`import { ${modelName}WhereUniqueInputObjectSchema } from './objects/${modelName}WhereUniqueInput.schema'`,
];
await writeFileSafely(
path.join(Transformer.outputPath, `schemas/${aggregate}.schema.ts`),
`${this.getImportsForSchemas(imports)}${this.addExportSchema(
`z.object({ where: ${modelName}WhereInputObjectSchema.optional(), orderBy: ${modelName}OrderByWithRelationInputObjectSchema.optional(), cursor: ${modelName}WhereUniqueInputObjectSchema.optional(), take: z.number().optional(), skip: z.number().optional() })`,
`${modelName}Aggregate`,
)}`,
);
}
if (groupBy) {
const imports = [
`import { ${modelName}WhereInputObjectSchema } from './objects/${modelName}WhereInput.schema'`,
`import { ${modelName}OrderByWithAggregationInputObjectSchema } from './objects/${modelName}OrderByWithAggregationInput.schema'`,
`import { ${modelName}ScalarWhereWithAggregatesInputObjectSchema } from './objects/${modelName}ScalarWhereWithAggregatesInput.schema'`,
`import { ${modelName}ScalarFieldEnumSchema } from './enums/${modelName}ScalarFieldEnum.schema'`,
];
await writeFileSafely(
path.join(Transformer.outputPath, `schemas/${groupBy}.schema.ts`),
`${this.getImportsForSchemas(imports)}${this.addExportSchema(
`z.object({ where: ${modelName}WhereInputObjectSchema.optional(), orderBy: ${modelName}OrderByWithAggregationInputObjectSchema, having: ${modelName}ScalarWhereWithAggregatesInputObjectSchema.optional(), take: z.number().optional(), skip: z.number().optional(), by: z.array(${modelName}ScalarFieldEnumSchema) })`,
`${modelName}GroupBy`,
)}`,
);
}
}
}
async printEnumSchemas() {
for (const enumType of this.enumTypes) {
const { name, values } = enumType;
await writeFileSafely(
path.join(Transformer.outputPath, `schemas/enums/${name}.schema.ts`),
`${this.getImportZod()}\n${this.addExportSchema(
`z.enum(${JSON.stringify(values)})`,
`${name}`,
)}`,
);
}
}
}