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

528 lines 19.3 kB
"use strict"; 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