prisma-zod-generator
Version:
Prisma 2+ generator to emit Zod schemas from your Prisma schema
357 lines • 16.4 kB
JavaScript
;
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