@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
528 lines • 19.3 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.DSLParser = void 0;
const errors_1 = require("./errors");
const schema_validator_1 = require("../validation/schema-validator");
class DSLParser {
lines = [];
lineNumbers = []; // Track original line numbers
currentLine = 0;
filePath = 'schema.qts';
// Schema context for validation
schemaModels = new Map();
schemaDatasources = new Map();
schemaEnums = new Map();
parse(schema, filePath = 'schema.qts') {
this.filePath = filePath;
// Reset schema context
this.schemaModels.clear();
this.schemaDatasources.clear();
// Parse lines and track original line numbers
const rawLines = schema.split('\n');
this.lines = [];
this.lineNumbers = [];
rawLines.forEach((line, index) => {
const trimmed = line.trim();
if (trimmed && !trimmed.startsWith('//')) {
this.lines.push(trimmed);
this.lineNumbers.push(index + 1); // Store 1-indexed line number
}
});
this.currentLine = 0;
const ir = {
config: this.parseConfig(),
datasources: {},
models: {},
enums: {}
};
while (this.currentLine < this.lines.length) {
const line = this.lines[this.currentLine];
if (line.startsWith('datasource ')) {
const ds = this.parseDatasource();
ir.datasources[ds.name] = ds;
}
else if (line.startsWith('model ')) {
const model = this.parseModel();
ir.models[model.name] = model;
}
else if (line.startsWith('enum ')) {
const enm = this.parseEnum();
ir.enums = ir.enums || {};
ir.enums[enm.name] = enm;
}
else {
this.currentLine++;
}
}
// Run schema validation BEFORE IR validation
this.validateSchema();
// Run basic IR validation
this.validate(ir);
return ir;
}
/**
* Run schema validation and throw on errors
*/
validateSchema() {
const context = {
filePath: this.filePath,
models: this.schemaModels,
datasources: this.schemaDatasources,
enums: this.schemaEnums
};
const result = (0, schema_validator_1.validateSchema)(context);
if (!result.valid || result.warnings.length > 0) {
const formatted = (0, schema_validator_1.formatSchemaValidationErrors)(result);
if (!result.valid) {
// Throw on errors
throw new Error(`Schema validation failed:\n${formatted}`);
}
else {
// Just log warnings
console.warn(formatted);
}
}
}
parseEnum() {
const line = this.lines[this.currentLine];
const lineNumber = this.lineNumbers[this.currentLine];
const match = line.match(/enum\s+(\w+)/);
if (!match) {
throw new errors_1.DSLParserError(`Invalid enum declaration: ${line}`, {
file: this.filePath,
line: lineNumber,
column: 0,
length: line.length
}, { suggestion: 'Expected: enum <Name> { ... }', code: 'E010' });
}
const name = match[1];
const values = [];
this.currentLine++; // move past enum line
// Skip opening brace if present on its own line
if (this.lines[this.currentLine] === '{') {
this.currentLine++;
}
while (!this.lines[this.currentLine].startsWith('}')) {
const valLine = this.lines[this.currentLine];
// Allow comma separated or single token per line
const tokens = valLine.split(',').map(s => s.trim()).filter(s => s);
for (const t of tokens) {
// strip possible trailing commas or comments
const cleaned = t.replace(/,$/, '').trim();
if (cleaned)
values.push(cleaned);
}
this.currentLine++;
}
this.currentLine++; // skip closing brace
// Track in schema enums for validation
this.schemaEnums.set(name, { name, values, line: lineNumber });
return { name, values };
}
parseConfig() {
const configIdx = this.lines.findIndex(l => l.startsWith('config {'));
if (configIdx === -1) {
return {
strict: false,
limits: this.getDefaultLimits()
};
}
this.currentLine = configIdx + 1;
const config = {
strict: false,
limits: this.getDefaultLimits()
};
while (!this.lines[this.currentLine].startsWith('}')) {
const line = this.lines[this.currentLine];
if (line.startsWith('strict')) {
config.strict = line.includes('true');
}
else if (line.startsWith('limits')) {
config.limits = this.parseLimits();
}
this.currentLine++;
}
this.currentLine++; // Skip closing }
return config;
}
parseLimits() {
const limits = this.getDefaultLimits();
this.currentLine++; // Skip 'limits = {'
while (!this.lines[this.currentLine].startsWith('}')) {
const line = this.lines[this.currentLine];
const [key, value] = line.split('=').map((s) => s.trim());
if (key in limits) {
limits[key] = parseInt(value);
}
this.currentLine++;
}
return limits;
}
parseDatasource() {
const line = this.lines[this.currentLine];
const lineNumber = this.lineNumbers[this.currentLine];
const nameMatch = line.match(/datasource\s+(\w+)/);
if (!nameMatch) {
throw new errors_1.DSLParserError('Invalid datasource declaration', {
file: this.filePath,
line: lineNumber,
column: 0,
length: line.length
}, {
suggestion: 'Expected: datasource <name> { ... }',
code: 'E001'
});
}
const ds = {
name: nameMatch[1],
provider: 'http'
};
// Track for schema validation
this.schemaDatasources.set(ds.name, {
name: ds.name,
provider: ds.provider,
line: lineNumber
});
this.currentLine++; // Move to next line
// Skip lines until we find the opening brace or the first property
while (this.currentLine < this.lines.length && !this.lines[this.currentLine].startsWith('}')) {
const nextLine = this.lines[this.currentLine];
if (nextLine === '{') {
this.currentLine++;
break;
}
else if (nextLine.includes('=')) {
// This is a property line, don't skip
break;
}
else {
this.currentLine++;
}
}
while (!this.lines[this.currentLine].startsWith('}')) {
const line = this.lines[this.currentLine];
if (line.startsWith('provider')) {
const value = this.extractValue(line);
ds.provider = value;
}
else if (line.startsWith('url')) {
ds.url = this.extractValue(line);
}
else if (line.startsWith('baseUrl')) {
ds.baseUrl = this.extractValue(line);
}
else if (line.startsWith('oauth')) {
ds.oauth = this.parseOAuthConfig(line);
}
this.currentLine++;
}
this.currentLine++; // Skip }
return ds;
}
parseOAuthConfig(line) {
const match = line.match(/oauth\s*=\s*{(.+)}/);
if (!match)
return undefined;
const parts = match[1].split(',').map(p => p.trim());
const config = {};
for (const part of parts) {
const [key, val] = part.split(':').map(s => s.trim());
config[key] = this.cleanValue(val);
}
return config;
}
parseModel() {
const line = this.lines[this.currentLine];
const lineNumber = this.lineNumbers[this.currentLine];
const match = line.match(/model\s+(\w+)(?:\s+@datasource\(([^)]+)\))?/);
if (!match) {
throw new errors_1.DSLParserError(`Invalid model declaration: ${line}`, {
file: this.filePath,
line: lineNumber,
column: 0,
length: line.length
}, {
suggestion: 'Expected: model <Name> @datasource(datasourceName) { ... }',
code: 'E002'
});
}
const model = {
name: match[1],
datasource: match[2] || '',
fields: {}
};
// Track schema model for validation
const schemaFields = [];
this.schemaModels.set(model.name, {
name: model.name,
datasource: match[2],
line: lineNumber,
fields: schemaFields
});
this.currentLine++; // Move past model line
// Skip opening brace if it's on its own line
if (this.lines[this.currentLine] === '{') {
this.currentLine++;
}
// Note: If the brace is on the same line as "model Name {", we're already past it
while (!this.lines[this.currentLine].startsWith('}')) {
const line = this.lines[this.currentLine];
const fieldLineNumber = this.lineNumbers[this.currentLine];
if (line.startsWith('@endpoint')) {
if (!model.endpoints)
model.endpoints = {};
const endpoint = this.parseEndpoint(line);
Object.assign(model.endpoints, endpoint);
}
else if (line.startsWith('@limits')) {
model.limits = this.parseLimitsAttribute(line);
}
else if (!line.startsWith('@')) {
const field = this.parseField(line);
if (field) {
model.fields[field.name] = field;
// Track schema field for validation
schemaFields.push({
name: field.name,
type: field.type,
line: fieldLineNumber,
isOptional: field.isOptional || false,
isList: field.isList || false,
isId: field.isId,
map: field.map,
relation: field.relation ? {
model: field.relation.model || field.type,
fields: field.relation.fields || [],
references: field.relation.references || [],
strategy: field.relation.strategy
} : undefined
});
}
}
this.currentLine++;
}
this.currentLine++; // Skip }
return model;
}
parseField(line) {
const parts = line.split(/\s+/);
if (parts.length < 2) {
return null;
}
const field = {
name: parts[0],
type: parts[1].replace('?', '').replace('[]', ''),
isOptional: parts[1].includes('?'),
isList: parts[1].includes('[]')
};
// Parse attributes - need to reconstruct multi-part attributes like @relation(...)
let i = 2;
while (i < parts.length) {
let attr = parts[i];
// If attribute starts with @ and contains ( but not ), collect until we find )
if (attr.startsWith('@') && attr.includes('(') && !attr.includes(')')) {
i++;
while (i < parts.length && !parts[i - 1].includes(')')) {
attr += ' ' + parts[i];
i++;
}
i--; // Back up one since we'll increment at the end of loop
}
if (attr === '@id') {
field.isId = true;
}
else if (attr === '@unique') {
field.isUnique = true;
}
else if (attr === '@index') {
field.index = true;
}
else if (attr.startsWith('@default')) {
field.default = this.parseDefaultValue(attr);
}
else if (attr.startsWith('@map')) {
field.map = this.parseMapValue(attr);
}
else if (attr.startsWith('@relation')) {
field.relation = this.parseRelation(attr);
// Add the model name from the field type
if (field.relation) {
field.relation.model = field.type;
}
}
else if (attr.startsWith('@computed')) {
field.computed = this.parseComputed(attr);
}
i++;
}
return field;
}
parseMapValue(attr) {
const match = attr.match(/@map\(["']([^"']+)["']\)/);
if (!match)
return undefined;
return match[1];
}
parseDefaultValue(attr) {
const match = attr.match(/@default\(([^)]+)\)/);
if (!match)
return undefined;
const value = match[1];
if (value === 'autoincrement()' || value === 'now()') {
return { type: 'function', value };
}
return { type: 'literal', value: this.cleanValue(value) };
}
parseRelation(attr) {
const match = attr.match(/@relation\(([^)]+)\)/);
if (!match)
return undefined;
// Split by comma, but be careful with arrays containing commas
const content = match[1];
const parts = [];
let current = '';
let depth = 0;
for (let i = 0; i < content.length; i++) {
const char = content[i];
if (char === '[')
depth++;
if (char === ']')
depth--;
if (char === ',' && depth === 0) {
parts.push(current.trim());
current = '';
}
else {
current += char;
}
}
if (current.trim()) {
parts.push(current.trim());
}
const relation = {};
for (const part of parts) {
const colonIdx = part.indexOf(':');
if (colonIdx === -1)
continue;
const key = part.substring(0, colonIdx).trim();
const val = part.substring(colonIdx + 1).trim();
if (key === 'fields' || key === 'references') {
// Handle array format [item1, item2] or [item1,item2]
const arrayMatch = val.match(/\[([^\]]*)\]/);
if (arrayMatch) {
relation[key] = arrayMatch[1].split(',').map(s => s.trim()).filter(s => s);
}
}
else {
relation[key] = this.cleanValue(val);
}
}
return relation;
}
parseComputed(attr) {
const match = attr.match(/@computed\(([^)]+)\)/);
if (!match)
return undefined;
const parts = match[1].split(',').map(p => p.trim());
const computed = { async: false, io: false };
for (const part of parts) {
const [key, val] = part.split(':').map(s => s.trim());
if (key === 'async' || key === 'io') {
computed[key] = val === 'true';
}
else {
computed[key] = this.cleanValue(val);
}
}
return computed;
}
parseEndpoint(line) {
// Match @endpoint(name: { ... }) with proper brace counting
const nameMatch = line.match(/@endpoint\((\w+):\s*{/);
if (!nameMatch)
return {};
const name = nameMatch[1];
const startIdx = line.indexOf('{', line.indexOf('@endpoint'));
// Count braces to find the matching closing brace
let braceCount = 0;
let endIdx = startIdx;
for (let i = startIdx; i < line.length; i++) {
if (line[i] === '{')
braceCount++;
if (line[i] === '}')
braceCount--;
if (braceCount === 0) {
endIdx = i;
break;
}
}
const body = line.substring(startIdx + 1, endIdx);
const parts = this.parseObject(body);
return { [name]: parts };
}
parseLimitsAttribute(line) {
const match = line.match(/@limits\(([^)]+)\)/);
if (!match)
return undefined;
const parts = match[1].split(',').map(p => p.trim());
const limits = {};
for (const part of parts) {
const [key, val] = part.split(':').map(s => s.trim());
limits[key] = parseInt(val);
}
return limits;
}
parseObject(str) {
const obj = {};
const parts = str.split(',').map(p => p.trim());
for (const part of parts) {
const colonIdx = part.indexOf(':');
if (colonIdx === -1)
continue;
const key = part.slice(0, colonIdx).trim();
const val = part.slice(colonIdx + 1).trim();
if (val.startsWith('{')) {
obj[key] = this.parseObject(val.slice(1, -1));
}
else {
obj[key] = this.cleanValue(val);
}
}
return obj;
}
extractValue(line) {
const match = line.match(/=\s*(.+)/);
if (!match)
return '';
return this.cleanValue(match[1]);
}
cleanValue(val) {
// Remove outer quotes but keep env() wrapper for runtime resolution
return val.replace(/^["']|["']$/g, '').trim();
}
getDefaultLimits() {
return {
maxIncludeDepth: 2,
maxFanOut: 2000,
maxConcurrentRequests: 10,
requestTimeoutMs: 10000,
postFilterRowLimit: 10000
};
}
validate(ir) {
// Validate datasource references
for (const model of Object.values(ir.models)) {
if (model.datasource && !ir.datasources[model.datasource]) {
throw new Error(`Model ${model.name} references unknown datasource: ${model.datasource}`);
}
// Validate relation references
for (const field of Object.values(model.fields)) {
if (field.relation && !ir.models[field.relation.model]) {
throw new Error(`Field ${model.name}.${field.name} references unknown model: ${field.relation.model}`);
}
}
}
}
}
exports.DSLParser = DSLParser;
//# sourceMappingURL=index.js.map