prisma-zod-generator
Version:
Prisma 2+ generator to emit Zod schemas from your Prisma schema
554 lines • 23.4 kB
JavaScript
;
/**
* Result Schema Generator
* Generates Zod schemas for Prisma operation return values to enable validation of API responses and operation results
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.ResultSchemaGenerator = exports.OperationType = void 0;
const defaults_1 = require("../config/defaults");
/**
* Prisma operation types that return results
*/
var OperationType;
(function (OperationType) {
OperationType["FIND_UNIQUE"] = "findUnique";
OperationType["FIND_FIRST"] = "findFirst";
OperationType["FIND_MANY"] = "findMany";
OperationType["CREATE"] = "create";
OperationType["CREATE_MANY"] = "createMany";
OperationType["UPDATE"] = "update";
OperationType["UPDATE_MANY"] = "updateMany";
OperationType["UPSERT"] = "upsert";
OperationType["DELETE"] = "delete";
OperationType["DELETE_MANY"] = "deleteMany";
OperationType["AGGREGATE"] = "aggregate";
OperationType["GROUP_BY"] = "groupBy";
OperationType["COUNT"] = "count";
})(OperationType || (exports.OperationType = OperationType = {}));
/**
* Result Schema Generator
* Main class for generating Zod schemas for Prisma operation results
*/
class ResultSchemaGenerator {
// Safe accessors for JSON Schema compatibility flags/options to avoid strict type coupling
isJsonSchemaModeEnabled() {
const cfg = this.config;
return !!(cfg === null || cfg === void 0 ? void 0 : cfg.jsonSchemaCompatible);
}
getJsonSchemaOptions() {
var _a;
const cfg = this.config;
return ((_a = cfg === null || cfg === void 0 ? void 0 : cfg.jsonSchemaOptions) !== null && _a !== void 0 ? _a : {});
}
constructor(config) {
this.generatedSchemas = new Map();
this.baseModelSchemas = new Map();
this.config = config !== null && config !== void 0 ? config : (0, defaults_1.getDefaultConfiguration)();
}
/**
* Generate result schema for a specific operation
*/
generateResultSchema(model, options) {
const context = this.buildGenerationContext(model, options);
const cacheKey = this.generateCacheKey(options);
// Check cache first
if (this.generatedSchemas.has(cacheKey)) {
const cachedSchema = this.generatedSchemas.get(cacheKey);
if (cachedSchema) {
return cachedSchema;
}
}
let result;
switch (options.operationType) {
case OperationType.FIND_UNIQUE:
case OperationType.FIND_FIRST:
case OperationType.CREATE:
case OperationType.UPDATE:
case OperationType.UPSERT:
case OperationType.DELETE:
result = this.generateSingleResultSchema(context);
break;
case OperationType.FIND_MANY:
result = this.generateArrayResultSchema(context);
break;
case OperationType.CREATE_MANY:
case OperationType.UPDATE_MANY:
case OperationType.DELETE_MANY:
result = this.generateBatchResultSchema(context);
break;
case OperationType.AGGREGATE:
result = this.generateAggregateResultSchema(context);
break;
case OperationType.GROUP_BY:
result = this.generateGroupByResultSchema(context);
break;
case OperationType.COUNT:
result = this.generateCountResultSchema(context);
break;
default:
throw new Error(`Unsupported operation type: ${options.operationType}`);
}
// Cache the result
this.generatedSchemas.set(cacheKey, result);
return result;
}
/**
* Generate schemas for all operations of a model
*/
generateAllResultSchemas(model, operationTypes = Object.values(OperationType)) {
const results = [];
operationTypes.forEach((operationType) => {
const options = {
modelName: model.name,
operationType,
paginationSupport: operationType === OperationType.FIND_MANY,
nullableResult: this.isNullableOperation(operationType),
};
try {
const result = this.generateResultSchema(model, options);
results.push(result);
}
catch (error) {
console.warn(`Failed to generate ${operationType} result schema for ${model.name}:`, error);
}
});
return results;
}
/**
* Build generation context
*/
buildGenerationContext(model, options) {
const relatedModels = new Map();
const fieldTypeMap = new Map();
// Build field type mapping
model.fields.forEach((field) => {
fieldTypeMap.set(field.name, this.mapPrismaTypeToZod(field));
// Collect related models for relation fields
if (field.kind === 'object' && field.type !== model.name) {
// In a real implementation, you'd get this from the DMMF
// For now, we'll create a placeholder
relatedModels.set(field.type, {
name: field.type,
fields: [],
dbName: null,
schema: null,
primaryKey: null,
uniqueFields: [],
uniqueIndexes: [],
isGenerated: false,
});
}
});
return {
model,
options,
baseModelSchema: this.getBaseModelSchema(model),
relatedModels,
fieldTypeMap,
};
}
/**
* Generate single result schema (for operations returning one model or null)
*/
generateSingleResultSchema(context) {
const { options } = context;
const schemaName = this.generateSchemaName(options);
const baseSchema = this.buildBaseResultSchema(context);
let zodSchema;
let typeDefinition;
if (options.nullableResult || this.isNullableOperation(options.operationType)) {
zodSchema = `z.nullable(${baseSchema})`;
typeDefinition = `z.infer<typeof ${schemaName}> | null`;
}
else {
zodSchema = baseSchema;
typeDefinition = `z.infer<typeof ${schemaName}>`;
}
const documentation = this.generateDocumentation(options, 'Single model result');
const examples = this.generateExamples(context, 'single');
return {
operationType: options.operationType,
schemaName,
zodSchema: `export const ${schemaName} = ${zodSchema};`,
typeDefinition: `export type ${schemaName}Type = ${typeDefinition};`,
imports: new Set(['z']),
exports: new Set([schemaName, `${schemaName}Type`]),
dependencies: this.extractDependencies(context),
documentation,
examples,
};
}
/**
* Generate array result schema (for findMany operations)
*/
generateArrayResultSchema(context) {
const { options } = context;
const schemaName = this.generateSchemaName(options);
const baseSchema = this.buildBaseResultSchema(context);
let zodSchema;
let typeDefinition;
if (options.paginationSupport) {
const paginationSchema = this.generatePaginationSchema();
zodSchema = `z.object({\n data: z.array(${baseSchema}),\n pagination: ${paginationSchema}\n})`;
typeDefinition = `z.infer<typeof ${schemaName}>`;
}
else {
zodSchema = `z.array(${baseSchema})`;
typeDefinition = `z.infer<typeof ${schemaName}>`;
}
const documentation = this.generateDocumentation(options, 'Array of model results');
const examples = this.generateExamples(context, 'array');
return {
operationType: options.operationType,
schemaName,
zodSchema: `export const ${schemaName} = ${zodSchema};`,
typeDefinition: `export type ${schemaName}Type = ${typeDefinition};`,
imports: new Set(['z']),
exports: new Set([schemaName, `${schemaName}Type`]),
dependencies: this.extractDependencies(context),
documentation,
examples,
};
}
/**
* Generate batch operation result schema
*/
generateBatchResultSchema(context) {
const { options } = context;
const schemaName = this.generateSchemaName(options);
const zodSchema = `z.object({
count: z.number()
})`;
const documentation = this.generateDocumentation(options, 'Batch operation result');
const examples = this.generateExamples(context, 'batch');
return {
operationType: options.operationType,
schemaName,
zodSchema: `export const ${schemaName} = ${zodSchema};`,
typeDefinition: `export type ${schemaName}Type = z.infer<typeof ${schemaName}>;`,
imports: new Set(['z']),
exports: new Set([schemaName, `${schemaName}Type`]),
dependencies: [],
documentation,
examples,
};
}
/**
* Generate aggregate result schema
*/
generateAggregateResultSchema(context) {
const { model, options } = context;
const schemaName = this.generateSchemaName(options);
const aggregateFields = this.buildAggregateFields(model);
const zodSchema = `z.object({${aggregateFields}})`;
const documentation = this.generateDocumentation(options, 'Aggregate operation result');
const examples = this.generateExamples(context, 'aggregate');
return {
operationType: options.operationType,
schemaName,
zodSchema: `export const ${schemaName} = ${zodSchema};`,
typeDefinition: `export type ${schemaName}Type = z.infer<typeof ${schemaName}>;`,
imports: new Set(['z']),
exports: new Set([schemaName, `${schemaName}Type`]),
dependencies: [],
documentation,
examples,
};
}
/**
* Generate groupBy result schema
*/
generateGroupByResultSchema(context) {
const { model, options } = context;
const schemaName = this.generateSchemaName(options);
const groupByFields = this.buildGroupByFields(model);
const aggregateFields = this.buildAggregateFields(model);
const allFields = [groupByFields, aggregateFields].filter((fields) => fields.trim().length > 0);
const zodSchema = `z.array(z.object({
${allFields.join(',\n')}
}))`;
const documentation = this.generateDocumentation(options, 'GroupBy operation result');
const examples = this.generateExamples(context, 'groupBy');
return {
operationType: options.operationType,
schemaName,
zodSchema: `export const ${schemaName} = ${zodSchema};`,
typeDefinition: `export type ${schemaName}Type = z.infer<typeof ${schemaName}>;`,
imports: new Set(['z']),
exports: new Set([schemaName, `${schemaName}Type`]),
dependencies: [],
documentation,
examples,
};
}
/**
* Generate count result schema
*/
generateCountResultSchema(context) {
const { options } = context;
const schemaName = this.generateSchemaName(options);
// Simple count should be a number schema
const zodSchema = 'z.number()';
const documentation = this.generateDocumentation(options, 'Count operation result');
const examples = this.generateExamples(context, 'count');
return {
operationType: options.operationType,
schemaName,
zodSchema: `export const ${schemaName} = ${zodSchema};`,
typeDefinition: `export type ${schemaName}Type = z.infer<typeof ${schemaName}>;`,
imports: new Set(['z']),
exports: new Set([schemaName, `${schemaName}Type`]),
dependencies: [],
documentation,
examples,
};
}
/**
* Helper methods
*/
generateSchemaName(options) {
const operationSuffix = this.operationTypeToSuffix(options.operationType);
return `${options.modelName}${operationSuffix}ResultSchema`;
}
operationTypeToSuffix(operationType) {
const suffixMap = {
[OperationType.FIND_UNIQUE]: 'FindUnique',
[OperationType.FIND_FIRST]: 'FindFirst',
[OperationType.FIND_MANY]: 'FindMany',
[OperationType.CREATE]: 'Create',
[OperationType.CREATE_MANY]: 'CreateMany',
[OperationType.UPDATE]: 'Update',
[OperationType.UPDATE_MANY]: 'UpdateMany',
[OperationType.UPSERT]: 'Upsert',
[OperationType.DELETE]: 'Delete',
[OperationType.DELETE_MANY]: 'DeleteMany',
[OperationType.AGGREGATE]: 'Aggregate',
[OperationType.GROUP_BY]: 'GroupBy',
[OperationType.COUNT]: 'Count',
};
return suffixMap[operationType];
}
isNullableOperation(operationType) {
return [
OperationType.FIND_UNIQUE,
OperationType.FIND_FIRST,
OperationType.UPDATE,
OperationType.DELETE,
].includes(operationType);
}
buildBaseResultSchema(context) {
const { model, options } = context;
// Start with base model schema
const fields = model.fields.filter((field) => {
var _a;
if ((_a = options.excludeFields) === null || _a === void 0 ? void 0 : _a.includes(field.name))
return false;
return true;
});
const fieldSchemas = fields.map((field) => {
const zodType = this.mapPrismaTypeToZod(field);
const optionalMarker = !field.isRequired ? '.optional()' : '';
return ` ${field.name}: ${zodType}${optionalMarker}`;
});
// Add included relations
if (options.includeRelations) {
options.includeRelations.forEach((relationName) => {
const relationField = model.fields.find((f) => f.name === relationName);
if (relationField) {
const relationSchema = this.buildRelationSchema(relationField);
fieldSchemas.push(` ${relationName}: ${relationSchema}`);
}
});
}
const baseSchemaStr = `z.object({\n${fieldSchemas.join(',\n')}\n})`;
return baseSchemaStr;
}
buildRelationSchema(field) {
if (field.isList) {
return `z.array(z.object({ /* ${field.type} fields */ }))`;
}
return `z.object({ /* ${field.type} fields */ }).optional()`;
}
buildAggregateFields(model) {
const numericFields = model.fields.filter((f) => ['Int', 'Float', 'Decimal', 'BigInt'].includes(f.type));
// _count: object with per-field counts (including booleans and relations), optional
const countObjectFields = model.fields.map((field) => ` ${field.name}: z.number()`);
const aggregateFields = [
` _count: z.object({\n${countObjectFields.join(',\n')}\n }).optional()`,
];
if (numericFields.length > 0) {
// Sum: numbers for numeric fields; BigInt as bigint
const sumFields = numericFields.map((field) => ` ${field.name}: ${field.type === 'BigInt' ? 'z.bigint()' : 'z.number()'}.nullable()`);
aggregateFields.push(` _sum: z.object({\n${sumFields.join(',\n')}\n }).nullable().optional()`);
// Avg: average results are numbers
const avgFields = numericFields.map((field) => ` ${field.name}: z.number().nullable()`);
aggregateFields.push(` _avg: z.object({\n${avgFields.join(',\n')}\n }).nullable().optional()`);
}
// Min/max for comparable fields (include BigInt)
const comparableFields = model.fields.filter((field) => ['Int', 'Float', 'Decimal', 'DateTime', 'String', 'BigInt'].includes(field.type));
if (comparableFields.length > 0) {
const minMaxFields = comparableFields.map((field) => {
const zodType = this.mapPrismaTypeToZod(field);
return ` ${field.name}: ${zodType}.nullable()`;
});
aggregateFields.push(` _min: z.object({\n${minMaxFields.join(',\n')}\n }).nullable().optional()`);
aggregateFields.push(` _max: z.object({\n${minMaxFields.join(',\n')}\n }).nullable().optional()`);
}
return aggregateFields.join(',\n');
}
buildGroupByFields(model) {
// For groupBy, we include the actual field values that can be grouped by
// Arrays can be grouped by in databases like PostgreSQL, so include them
const groupableFields = model.fields.filter((f) => f.kind === 'scalar');
return groupableFields
.map((field) => {
const zodType = this.mapPrismaTypeToZod(field);
return ` ${field.name}: ${zodType}`;
})
.join(',\n');
}
generatePaginationSchema() {
return `z.object({
page: z.number().int().min(1),
pageSize: z.number().int().min(1),
total: z.number().int().min(0),
totalPages: z.number().int().min(0),
hasNext: z.boolean(),
hasPrev: z.boolean()
})`;
}
mapPrismaTypeToZod(field) {
const isJsonSchemaCompatible = this.isJsonSchemaModeEnabled();
// Handle JSON Schema compatibility mapping
if (isJsonSchemaCompatible) {
switch (field.type) {
case 'DateTime':
const { dateTimeFormat } = this.getJsonSchemaOptions();
const dtFormat = dateTimeFormat || 'isoString';
if (dtFormat === 'isoDate') {
return field.isList
? 'z.array(z.string().regex(/^\\d{4}-\\d{2}-\\d{2}$/, "Invalid ISO date"))'
: 'z.string().regex(/^\\d{4}-\\d{2}-\\d{2}$/, "Invalid ISO date")';
}
else {
return field.isList
? 'z.array(z.string().regex(/^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$/, "Invalid ISO datetime"))'
: 'z.string().regex(/^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$/, "Invalid ISO datetime")';
}
case 'BigInt':
const { bigIntFormat } = this.getJsonSchemaOptions();
const biFormat = bigIntFormat || 'string';
if (biFormat === 'string') {
return field.isList
? 'z.array(z.string().regex(/^\\d+$/, "Invalid bigint string"))'
: 'z.string().regex(/^\\d+$/, "Invalid bigint string")';
}
else {
return field.isList ? 'z.array(z.number().int())' : 'z.number().int()';
}
case 'Bytes':
const { bytesFormat } = this.getJsonSchemaOptions();
const bFormat = bytesFormat || 'base64String';
if (bFormat === 'base64String') {
return field.isList
? 'z.array(z.string().regex(/^[A-Za-z0-9+/]*={0,2}$/, "Invalid base64 string"))'
: 'z.string().regex(/^[A-Za-z0-9+/]*={0,2}$/, "Invalid base64 string")';
}
else {
return field.isList
? 'z.array(z.string().regex(/^[0-9a-fA-F]*$/, "Invalid hex string"))'
: 'z.string().regex(/^[0-9a-fA-F]*$/, "Invalid hex string")';
}
}
}
const typeMap = {
String: 'z.string()',
Int: 'z.number().int()',
Float: 'z.number()',
Boolean: 'z.boolean()',
DateTime: 'z.date()',
Json: isJsonSchemaCompatible ? 'z.any()' : 'z.unknown()',
Bytes: 'z.instanceof(Uint8Array)',
Decimal: 'z.number()', // or z.string() depending on configuration
BigInt: 'z.bigint()',
};
const baseType = typeMap[field.type] || (isJsonSchemaCompatible ? 'z.any()' : 'z.unknown()');
// Handle arrays
if (field.isList) {
return `z.array(${baseType})`;
}
return baseType;
}
getBaseModelSchema(model) {
// This would typically reference the generated model schema
return `${model.name}Schema`;
}
extractDependencies(context) {
const dependencies = [];
if (context.options.includeRelations) {
context.options.includeRelations.forEach((relation) => {
const relationField = context.model.fields.find((f) => f.name === relation);
if (relationField && relationField.type !== context.model.name) {
dependencies.push(`${relationField.type}Schema`);
}
});
}
return dependencies;
}
generateDocumentation(options, description) {
return `/**
* ${description} for ${options.modelName} ${options.operationType} operation
* Generated at: ${new Date().toISOString()}
*/`;
}
generateExamples(context, type) {
// Generate example usage based on the result type
const examples = [];
const schemaName = this.generateSchemaName(context.options);
switch (type) {
case 'single':
examples.push(`const result = ${schemaName}.parse(apiResponse);`);
break;
case 'array':
examples.push(`const results = ${schemaName}.parse(apiResponse);`);
break;
case 'batch':
examples.push(`const batchResult = ${schemaName}.parse({ count: 5 });`);
break;
}
return examples;
}
generateCacheKey(options) {
var _a, _b;
return `${options.modelName}:${options.operationType}:${JSON.stringify({
includeRelations: (_a = options.includeRelations) === null || _a === void 0 ? void 0 : _a.sort(),
excludeFields: (_b = options.excludeFields) === null || _b === void 0 ? void 0 : _b.sort(),
paginationSupport: options.paginationSupport,
nullableResult: options.nullableResult,
})}`;
}
/**
* Public utility methods
*/
/**
* Clear generated schema cache
*/
clearCache() {
this.generatedSchemas.clear();
}
/**
* Get all generated schemas
*/
getAllGeneratedSchemas() {
return Array.from(this.generatedSchemas.values());
}
/**
* Register base model schema
*/
registerBaseModelSchema(modelName, schema) {
this.baseModelSchemas.set(modelName, schema);
}
}
exports.ResultSchemaGenerator = ResultSchemaGenerator;
exports.default = ResultSchemaGenerator;
//# sourceMappingURL=results.js.map