UNPKG

prisma-zod-generator

Version:

Prisma 2+ generator to emit Zod schemas from your Prisma schema

554 lines 23.4 kB
"use strict"; /** * 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