UNPKG

@aradox/multi-orm

Version:

Type-safe ORM with multi-datasource support, row-level security, and Prisma-like API for PostgreSQL, SQL Server, and HTTP APIs

408 lines 13.9 kB
"use strict"; /** * IR Validation Module * * Validates the Intermediate Representation (IR) structure before code generation. * Ensures IR integrity and catches structural errors early. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.validateIR = validateIR; exports.formatValidationErrors = formatValidationErrors; /** * Validates the entire IR structure */ function validateIR(ir) { const errors = []; const warnings = []; // Check if IR exists if (!ir) { errors.push({ path: 'root', message: 'IR is null or undefined', severity: 'error' }); return { valid: false, errors, warnings }; } // Validate top-level structure if (typeof ir !== 'object') { errors.push({ path: 'root', message: 'IR must be an object', severity: 'error' }); return { valid: false, errors, warnings }; } // Validate datasources if (!ir.datasources || typeof ir.datasources !== 'object') { errors.push({ path: 'datasources', message: 'IR must have a datasources object', severity: 'error' }); } else { errors.push(...validateDatasources(ir.datasources)); } // Validate models if (!ir.models || typeof ir.models !== 'object') { errors.push({ path: 'models', message: 'IR must have a models object', severity: 'error' }); } else { errors.push(...validateModels(ir.models, ir.datasources || {}, ir.enums || {})); } // Validate config (optional) if (ir.config) { warnings.push(...validateConfig(ir.config)); } return { valid: errors.length === 0, errors, warnings }; } /** * Validates datasources structure */ function validateDatasources(datasources) { const errors = []; for (const [name, ds] of Object.entries(datasources)) { const path = `datasources.${name}`; // Check datasource has required fields if (!ds.provider) { errors.push({ path: `${path}.provider`, message: `Datasource '${name}' missing required 'provider' field`, severity: 'error', suggestion: "Add provider: 'mssql' | 'postgres' | 'http'" }); } // Validate provider value if (ds.provider && !['mssql', 'postgres', 'http', 'mysql', 'mongodb', 'mongo', 'sqlserver', 'postgresql'].includes(ds.provider)) { errors.push({ path: `${path}.provider`, message: `Invalid provider '${ds.provider}' for datasource '${name}'`, severity: 'error', suggestion: "Use one of: 'mssql', 'postgres', 'http', 'mysql'" }); } // Validate URL/baseUrl if (ds.provider === 'http') { if (!ds.baseUrl) { errors.push({ path: `${path}.baseUrl`, message: `HTTP datasource '${name}' missing required 'baseUrl' field`, severity: 'error' }); } } else { if (!ds.url) { errors.push({ path: `${path}.url`, message: `Datasource '${name}' missing required 'url' field`, severity: 'error' }); } } } return errors; } /** * Validates models structure */ function validateModels(models, datasources, enums) { const errors = []; const modelNames = Object.keys(models); for (const [name, model] of Object.entries(models)) { const path = `models.${name}`; // Validate model name (PascalCase) if (!/^[A-Z][a-zA-Z0-9]*$/.test(name)) { errors.push({ path, message: `Model name '${name}' must be PascalCase`, severity: 'error', suggestion: `Rename to '${toPascalCase(name)}'` }); } // Check model has required fields if (!model.name) { errors.push({ path: `${path}.name`, message: `Model missing 'name' field`, severity: 'error' }); } if (!model.datasource) { errors.push({ path: `${path}.datasource`, message: `Model '${name}' missing 'datasource' field`, severity: 'error' }); } // Validate datasource reference if (model.datasource && !datasources[model.datasource]) { errors.push({ path: `${path}.datasource`, message: `Model '${name}' references unknown datasource '${model.datasource}'`, severity: 'error', suggestion: `Available datasources: ${Object.keys(datasources).join(', ')}` }); } // Validate fields (can be array or object) if (!model.fields) { errors.push({ path: `${path}.fields`, message: `Model '${name}' must have fields`, severity: 'error' }); } else { // Convert fields object to array if needed const fieldsArray = Array.isArray(model.fields) ? model.fields : Object.values(model.fields); if (fieldsArray.length === 0) { errors.push({ path: `${path}.fields`, message: `Model '${name}' has no fields`, severity: 'error' }); } else { errors.push(...validateFields(fieldsArray, name, modelNames, Object.keys(enums || {}))); } // Check for at least one @id field const hasIdField = fieldsArray.some((f) => f.isId === true || (f.attributes && (f.attributes.id === true || f.attributes.id))); if (!hasIdField) { errors.push({ path: `${path}.fields`, message: `Model '${name}' must have at least one @id field`, severity: 'error', suggestion: 'Add @id attribute to a field' }); } } } return errors; } /** * Validates fields within a model */ function validateFields(fields, modelName, allModelNames, allEnumNames) { const errors = []; const fieldNames = new Set(); for (const field of fields) { const path = `models.${modelName}.fields.${field.name || 'unknown'}`; // Check field has name if (!field.name) { errors.push({ path, message: 'Field missing name', severity: 'error' }); continue; } // Check for duplicate field names if (fieldNames.has(field.name)) { errors.push({ path, message: `Duplicate field name '${field.name}' in model '${modelName}'`, severity: 'error' }); } fieldNames.add(field.name); // Check field has type if (!field.type) { errors.push({ path: `${path}.type`, message: `Field '${field.name}' missing type`, severity: 'error' }); } // Validate scalar types, models, or enums const scalarTypes = ['String', 'Int', 'Float', 'Boolean', 'DateTime', 'Json']; if (field.type && !scalarTypes.includes(field.type) && !allModelNames.includes(field.type) && !allEnumNames.includes(field.type)) { errors.push({ path: `${path}.type`, message: `Invalid type '${field.type}' for field '${field.name}'`, severity: 'error', suggestion: `Use scalar type (${scalarTypes.join(', ')}) or model/enum name` }); } // Validate relation fields if (field.relation) { errors.push(...validateRelation(field, modelName, allModelNames)); } // Validate optional/array flags if (field.optional !== undefined && typeof field.optional !== 'boolean') { errors.push({ path: `${path}.optional`, message: `Field '${field.name}' optional flag must be boolean`, severity: 'error' }); } if (field.array !== undefined && typeof field.array !== 'boolean') { errors.push({ path: `${path}.array`, message: `Field '${field.name}' array flag must be boolean`, severity: 'error' }); } } return errors; } /** * Validates relation field structure */ function validateRelation(field, modelName, allModelNames) { const errors = []; const path = `models.${modelName}.fields.${field.name}.relation`; const rel = field.relation; // Check relation has required fields if (!rel.model) { errors.push({ path: `${path}.model`, message: `Relation on '${field.name}' missing 'model' field`, severity: 'error' }); } // Validate relation model reference if (rel.model && !allModelNames.includes(rel.model)) { errors.push({ path: `${path}.model`, message: `Relation references unknown model '${rel.model}'`, severity: 'error', suggestion: `Available models: ${allModelNames.join(', ')}` }); } // Check for fields array if (!rel.fields || !Array.isArray(rel.fields)) { errors.push({ path: `${path}.fields`, message: `Relation on '${field.name}' must have 'fields' array`, severity: 'error' }); } else if (rel.fields.length === 0) { errors.push({ path: `${path}.fields`, message: `Relation on '${field.name}' has empty 'fields' array`, severity: 'error' }); } // Check for references array if (!rel.references || !Array.isArray(rel.references)) { errors.push({ path: `${path}.references`, message: `Relation on '${field.name}' must have 'references' array`, severity: 'error' }); } else if (rel.references.length === 0) { errors.push({ path: `${path}.references`, message: `Relation on '${field.name}' has empty 'references' array`, severity: 'error' }); } // Check fields and references have same length if (rel.fields && rel.references && rel.fields.length !== rel.references.length) { errors.push({ path: `${path}.fields`, message: `Relation 'fields' and 'references' arrays must have same length`, severity: 'error' }); } // Validate strategy if (!rel.strategy) { errors.push({ path: `${path}.strategy`, message: `Relation on '${field.name}' missing 'strategy' field`, severity: 'error', suggestion: "Add strategy: 'lookup' or strategy: 'pushdown'" }); } else if (!['lookup', 'pushdown'].includes(rel.strategy)) { errors.push({ path: `${path}.strategy`, message: `Invalid strategy '${rel.strategy}'`, severity: 'error', suggestion: "Use 'lookup' or 'pushdown'" }); } return errors; } /** * Validates config structure */ function validateConfig(config) { const warnings = []; if (config.limits) { const limits = config.limits; // Validate numeric limits const numericLimits = [ 'maxIncludeDepth', 'maxFanOut', 'maxConcurrentRequests', 'requestTimeoutMs', 'postFilterRowLimit' ]; for (const key of numericLimits) { if (limits[key] !== undefined) { if (typeof limits[key] !== 'number') { warnings.push({ path: `config.limits.${key}`, message: `Limit '${key}' must be a number`, severity: 'warning' }); } else if (limits[key] < 0) { warnings.push({ path: `config.limits.${key}`, message: `Limit '${key}' cannot be negative`, severity: 'warning' }); } } } } return warnings; } /** * Helper: Convert string to PascalCase */ function toPascalCase(str) { return str .replace(/[^a-zA-Z0-9]+(.)/g, (_, chr) => chr.toUpperCase()) .replace(/^[a-z]/, (chr) => chr.toUpperCase()); } /** * Format validation errors for display */ function formatValidationErrors(result) { const lines = []; if (result.errors.length > 0) { lines.push('❌ IR Validation Errors:'); for (const error of result.errors) { lines.push(` ${error.path}: ${error.message}`); if (error.suggestion) { lines.push(` Suggestion: ${error.suggestion}`); } } } if (result.warnings.length > 0) { lines.push('⚠️ IR Validation Warnings:'); for (const warning of result.warnings) { lines.push(` ${warning.path}: ${warning.message}`); if (warning.suggestion) { lines.push(` Suggestion: ${warning.suggestion}`); } } } return lines.join('\n'); } //# sourceMappingURL=ir-validator.js.map