UNPKG

prisma-zod-generator

Version:

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

357 lines 16.4 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ConfigurationValidator = void 0; exports.validateConfiguration = validateConfiguration; exports.validateConfigurationWithModels = validateConfigurationWithModels; exports.formatValidationErrors = formatValidationErrors; exports.formatValidationWarnings = formatValidationWarnings; const ajv_1 = __importDefault(require("ajv")); const ajv_formats_1 = __importDefault(require("ajv-formats")); const schema_1 = require("./schema"); /** * Configuration validator class */ class ConfigurationValidator { constructor() { // Initialize AJV with strict mode and additional formats this.ajv = new ajv_1.default({ strict: true, allErrors: true, verbose: true, discriminator: true, removeAdditional: false, }); // Add format validation (email, uri, etc.) (0, ajv_formats_1.default)(this.ajv); // Compile the configuration schema this.validateConfig = this.ajv.compile(schema_1.ConfigurationSchema); } /** * Validate a configuration object */ validate(config, modelNames) { const result = { valid: false, errors: [], warnings: [], }; // First, validate against JSON Schema const schemaValid = this.validateConfig(config); if (!schemaValid && this.validateConfig.errors) { result.errors.push(...this.convertAjvErrors(this.validateConfig.errors)); } // If basic schema validation fails, return early if (!schemaValid) { return result; } // Cast to GeneratorConfig for further validation const typedConfig = config; // Create validation context const context = { config: typedConfig, errors: [...result.errors], warnings: [...result.warnings], modelNames, }; // Perform cross-field and business logic validation this.validateCrossFieldConstraints(context); this.validateModelReferences(context); this.validateVariantConsistency(context); this.validateOperationConsistency(context); // Update result result.errors = context.errors; result.warnings = context.warnings; result.valid = result.errors.length === 0; if (result.valid) { result.config = this.applyDefaults(typedConfig); } return result; } /** * Validate configuration with Prisma model names */ validateWithModels(config, modelNames) { return this.validate(config, modelNames); } /** * Convert AJV validation errors to our error format */ convertAjvErrors(ajvErrors) { return ajvErrors.map((error) => this.convertSingleAjvError(error)); } /** * Convert a single AJV error to our error format */ convertSingleAjvError(error) { var _a, _b, _c, _d; const path = error.instancePath || 'root'; switch (error.keyword) { case 'enum': return { type: schema_1.ValidationErrorType.INVALID_JSON_SCHEMA, message: `Invalid value at ${path}. Expected one of: ${error.schema}`, path, value: error.data, allowedValues: Array.isArray(error.schema) ? error.schema : [error.schema], }; case 'pattern': return { type: schema_1.ValidationErrorType.INVALID_JSON_SCHEMA, message: `Invalid format at ${path}. ${this.getPatternErrorMessage(path, error.schema)}`, path, value: error.data, }; case 'required': return { type: schema_1.ValidationErrorType.MISSING_REQUIRED, message: `Missing required property: ${(_a = error.params) === null || _a === void 0 ? void 0 : _a.missingProperty}`, path: `${path}/${(_b = error.params) === null || _b === void 0 ? void 0 : _b.missingProperty}`, value: error.data, }; case 'additionalProperties': return { type: schema_1.ValidationErrorType.INVALID_JSON_SCHEMA, message: `Unknown property: ${(_c = error.params) === null || _c === void 0 ? void 0 : _c.additionalProperty}`, path: `${path}/${(_d = error.params) === null || _d === void 0 ? void 0 : _d.additionalProperty}`, value: error.data, }; default: return { type: schema_1.ValidationErrorType.INVALID_JSON_SCHEMA, message: error.message || 'Unknown validation error', path, value: error.data, }; } } /** * Get human-readable error message for pattern validation failures */ getPatternErrorMessage(_path, pattern) { if (pattern === '^[a-zA-Z_][a-zA-Z0-9_]*$') { return 'Must be a valid identifier (letters, numbers, underscores, cannot start with number)'; } if (pattern === '^[A-Z][a-zA-Z0-9_]*$') { return 'Must be a valid model name (PascalCase, letters, numbers, underscores)'; } if (pattern === '^\\.[a-zA-Z][a-zA-Z0-9_]*$') { return 'Must be a valid file suffix (start with dot, followed by identifier)'; } if (pattern === '^[^<>:"|?*\\x00-\\x1f]+$') { return 'Must be a valid file path (no invalid characters)'; } return `Must match pattern: ${pattern}`; } /** * Validate cross-field constraints */ validateCrossFieldConstraints(context) { var _a, _b; const { config } = context; // Validate mode-specific constraints if (config.mode === 'minimal') { // In minimal mode, certain configurations don't make sense if ((_b = (_a = config.variants) === null || _a === void 0 ? void 0 : _a.result) === null || _b === void 0 ? void 0 : _b.enabled) { context.warnings.push('Result variants are not typically used in minimal mode'); } if (config.models && Object.keys(config.models).length > 0) { // Check if any model has non-minimal operations Object.entries(config.models).forEach(([modelName, modelConfig]) => { if (modelConfig.operations) { const nonMinimalOps = modelConfig.operations.filter((op) => !['findMany', 'findUnique', 'create', 'update', 'delete', 'upsert'].includes(op)); if (nonMinimalOps.length > 0) { context.warnings.push(`Model ${modelName} has non-minimal operations in minimal mode: ${nonMinimalOps.join(', ')}`); } } }); } } // Validate variant suffix uniqueness if (config.variants) { const suffixes = new Set(); Object.entries(config.variants).forEach(([variantName, variantConfig]) => { if (variantConfig === null || variantConfig === void 0 ? void 0 : variantConfig.suffix) { if (suffixes.has(variantConfig.suffix)) { context.errors.push({ type: schema_1.ValidationErrorType.DUPLICATE_VALUES, message: `Duplicate suffix "${variantConfig.suffix}" found in variants`, path: `variants.${variantName}.suffix`, value: variantConfig.suffix, }); } suffixes.add(variantConfig.suffix); } }); } } /** * Validate model references against actual Prisma models */ validateModelReferences(context) { const { config, modelNames } = context; if (!modelNames || !config.models) { return; } // Check if configured models exist in Prisma schema Object.keys(config.models).forEach((configuredModel) => { if (!modelNames.includes(configuredModel)) { context.errors.push({ type: schema_1.ValidationErrorType.INVALID_MODEL_NAME, message: `Model "${configuredModel}" not found in Prisma schema`, path: `models.${configuredModel}`, value: configuredModel, allowedValues: modelNames, }); } }); // Warn about Prisma models not explicitly configured const unconfiguredModels = modelNames.filter((modelName) => !config.models || !config.models[modelName]); if (unconfiguredModels.length > 0 && config.mode === 'custom') { context.warnings.push(`The following models are not configured and will use defaults: ${unconfiguredModels.join(', ')}`); } } /** * Validate variant consistency */ validateVariantConsistency(context) { const { config } = context; if (!config.variants && !config.models) { return; } // Check for variant-specific field exclusions that reference non-existent variants if (config.models) { Object.entries(config.models).forEach(([modelName, modelConfig]) => { if (modelConfig.variants) { Object.keys(modelConfig.variants).forEach((variantName) => { if (!(0, schema_1.isValidVariant)(variantName)) { context.errors.push({ type: schema_1.ValidationErrorType.INVALID_VARIANT, message: `Invalid variant name "${variantName}" in model ${modelName}`, path: `models.${modelName}.variants.${variantName}`, value: variantName, allowedValues: [...schema_1.SCHEMA_VARIANTS], }); } }); } }); } } /** * Validate operation consistency */ validateOperationConsistency(context) { const { config } = context; if (!config.models) { return; } Object.entries(config.models).forEach(([modelName, modelConfig]) => { if (modelConfig.operations) { // Check for invalid operations modelConfig.operations.forEach((operation) => { if (!(0, schema_1.isValidOperation)(operation)) { context.errors.push({ type: schema_1.ValidationErrorType.INVALID_OPERATION, message: `Invalid operation "${operation}" for model ${modelName}`, path: `models.${modelName}.operations`, value: operation, allowedValues: [...schema_1.PRISMA_OPERATIONS], }); } }); // Check for duplicate operations const uniqueOperations = new Set(modelConfig.operations); if (uniqueOperations.size !== modelConfig.operations.length) { context.errors.push({ type: schema_1.ValidationErrorType.DUPLICATE_VALUES, message: `Duplicate operations found for model ${modelName}`, path: `models.${modelName}.operations`, value: modelConfig.operations, }); } } }); } /** * Apply default values to configuration */ applyDefaults(config) { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w; return { mode: config.mode || schema_1.DEFAULT_CONFIG.mode, output: config.output || schema_1.DEFAULT_CONFIG.output, globalExclusions: config.globalExclusions || schema_1.DEFAULT_CONFIG.globalExclusions, variants: { pure: { enabled: (_c = (_b = (_a = config.variants) === null || _a === void 0 ? void 0 : _a.pure) === null || _b === void 0 ? void 0 : _b.enabled) !== null && _c !== void 0 ? _c : schema_1.DEFAULT_CONFIG.variants.pure.enabled, suffix: ((_e = (_d = config.variants) === null || _d === void 0 ? void 0 : _d.pure) === null || _e === void 0 ? void 0 : _e.suffix) || schema_1.DEFAULT_CONFIG.variants.pure.suffix, excludeFields: ((_g = (_f = config.variants) === null || _f === void 0 ? void 0 : _f.pure) === null || _g === void 0 ? void 0 : _g.excludeFields) || [], }, input: { enabled: (_k = (_j = (_h = config.variants) === null || _h === void 0 ? void 0 : _h.input) === null || _j === void 0 ? void 0 : _j.enabled) !== null && _k !== void 0 ? _k : schema_1.DEFAULT_CONFIG.variants.input.enabled, suffix: ((_m = (_l = config.variants) === null || _l === void 0 ? void 0 : _l.input) === null || _m === void 0 ? void 0 : _m.suffix) || schema_1.DEFAULT_CONFIG.variants.input.suffix, excludeFields: ((_p = (_o = config.variants) === null || _o === void 0 ? void 0 : _o.input) === null || _p === void 0 ? void 0 : _p.excludeFields) || [], }, result: { enabled: (_s = (_r = (_q = config.variants) === null || _q === void 0 ? void 0 : _q.result) === null || _r === void 0 ? void 0 : _r.enabled) !== null && _s !== void 0 ? _s : schema_1.DEFAULT_CONFIG.variants.result.enabled, suffix: ((_u = (_t = config.variants) === null || _t === void 0 ? void 0 : _t.result) === null || _u === void 0 ? void 0 : _u.suffix) || schema_1.DEFAULT_CONFIG.variants.result.suffix, excludeFields: ((_w = (_v = config.variants) === null || _v === void 0 ? void 0 : _v.result) === null || _w === void 0 ? void 0 : _w.excludeFields) || [], }, }, models: config.models || schema_1.DEFAULT_CONFIG.models, }; } } exports.ConfigurationValidator = ConfigurationValidator; /** * Convenience function to validate configuration */ function validateConfiguration(config, modelNames) { const validator = new ConfigurationValidator(); return validator.validate(config, modelNames); } /** * Convenience function to validate configuration with model names */ function validateConfigurationWithModels(config, modelNames) { const validator = new ConfigurationValidator(); return validator.validateWithModels(config, modelNames); } /** * Create human-readable validation error message */ function formatValidationErrors(errors) { if (errors.length === 0) { return 'No validation errors'; } let message = `Configuration validation failed with ${errors.length} error(s):\n\n`; errors.forEach((error, index) => { message += `${index + 1}. ${error.message}\n`; message += ` Path: ${error.path}\n`; if (error.value !== undefined) { message += ` Value: ${JSON.stringify(error.value)}\n`; } if (error.allowedValues && error.allowedValues.length > 0) { message += ` Allowed values: ${error.allowedValues.join(', ')}\n`; } message += '\n'; }); return message.trim(); } /** * Create human-readable validation warnings message */ function formatValidationWarnings(warnings) { if (warnings.length === 0) { return ''; } let message = `Configuration warnings (${warnings.length}):\n\n`; warnings.forEach((warning, index) => { message += `${index + 1}. ${warning}\n`; }); return message.trim(); } //# sourceMappingURL=validator.js.map