@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
JavaScript
"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