import { readdirSync, readFileSync } from 'node:fs'; import path from 'node:path'; function stripInlineComment(line) { let inString = false; let result = ''; for (let index = 0; index < line.length; index += 1) { const current = line[index]; const next = line[index + 1]; if (current === '"' && line[index - 1] !== '\\') { inString = !inString; result += current; continue; } if (!inString && current === '/' && next === '/') { break; } result += current; } return result.trim(); } function parseDefaultValue(rawValue) { const trimmed = rawValue.trim(); if (trimmed.startsWith('"') && trimmed.endsWith('"')) { return trimmed.slice(1, -1); } if (/^-?\d+$/.test(trimmed)) { return Number(trimmed); } return trimmed; } export function getDslFiles(rootDir) { const domainDir = path.join(rootDir, 'domain'); return readdirSync(domainDir, { withFileTypes: true }) .filter((entry) => entry.isFile() && entry.name.toLowerCase().endsWith('.dsl')) .map((entry) => path.join(domainDir, entry.name)) .sort((left, right) => left.localeCompare(right)); } function getOverrideFiles(rootDir) { const overridesDir = path.join(rootDir, 'overrides'); const files = ['api-overrides.dsl', 'ui-overrides.dsl'] .map((name) => path.join(overridesDir, name)) .filter((filePath) => { try { return readdirSync(path.dirname(filePath)).includes(path.basename(filePath)); } catch { return false; } }); return files; } export function parseDslFiles(rootDir) { const dslFiles = getDslFiles(rootDir); const enums = []; const entities = []; const stack = []; for (const filePath of dslFiles) { const contents = readFileSync(filePath, 'utf8'); const lines = contents.split(/\r?\n/); for (const rawLine of lines) { const line = stripInlineComment(rawLine); if (!line) { continue; } const top = stack.at(-1); const enumMatch = line.match(/^enum\s+([A-Za-z][A-Za-z0-9_]*)\s*\{$/); if (!top && enumMatch) { const enumDefinition = { name: enumMatch[1], values: [] }; enums.push(enumDefinition); stack.push({ type: 'enum', ref: enumDefinition }); continue; } const entityMatch = line.match(/^entity\s+([A-Za-z][A-Za-z0-9_]*)\s*\{$/); if (!top && entityMatch) { const entityDefinition = { name: entityMatch[1], primaryKey: null, fields: [], foreignKeys: [], }; entities.push(entityDefinition); stack.push({ type: 'entity', ref: entityDefinition }); continue; } const valueMatch = line.match(/^value\s+([A-Za-z][A-Za-z0-9_]*)\s*\{$/); if (top?.type === 'enum' && valueMatch) { const enumValue = { name: valueMatch[1] }; top.ref.values.push(enumValue); stack.push({ type: 'enumValue', ref: enumValue }); continue; } const attributeMatch = line.match(/^attribute\s+([A-Za-z][A-Za-z0-9_]*)\s*\{$/); if (top?.type === 'entity' && attributeMatch) { const field = { name: attributeMatch[1], type: null, required: false, unique: false, default: null, }; top.ref.fields.push(field); stack.push({ type: 'field', ref: field, entity: top.ref }); continue; } if (top?.type === 'field' && /^key\s+foreign\s*\{$/.test(line)) { stack.push({ type: 'foreignKey', ref: top.ref, entity: top.entity }); continue; } if (top?.type === 'enumValue') { const labelMatch = line.match(/^label\s+"(.*)"\s*;$/); if (labelMatch) { top.ref.label = labelMatch[1]; continue; } } if (top?.type === 'field') { const typeMatch = line.match(/^type\s+([A-Za-z][A-Za-z0-9_]*)\s*;$/); if (typeMatch) { top.ref.type = typeMatch[1]; continue; } if (/^key\s+primary\s*;$/.test(line)) { top.ref.primary = true; top.entity.primaryKey = top.ref.name; continue; } const defaultMatch = line.match(/^default\s+(.+)\s*;$/); if (defaultMatch) { top.ref.default = parseDefaultValue(defaultMatch[1]); continue; } if (/^is\s+required\s*;$/.test(line)) { top.ref.required = true; continue; } if (/^is\s+unique\s*;$/.test(line)) { top.ref.unique = true; continue; } } if (top?.type === 'foreignKey') { const relatesMatch = line.match( /^relates\s+([A-Za-z][A-Za-z0-9_]*)\.([A-Za-z][A-Za-z0-9_]*)\s*;$/, ); if (relatesMatch) { const foreignKey = { field: top.ref.name, references: { entity: relatesMatch[1], field: relatesMatch[2], }, }; top.ref.foreignKey = foreignKey.references; top.entity.foreignKeys.push(foreignKey); continue; } } if (/^}\s*;?$/.test(line)) { stack.pop(); } } } return { dslFiles, enums, entities }; } function assertNoDomainRedefinition(line, filePath) { const blocked = [/^\s*entity\s+/i, /^\s*enum\s+/i, /^\s*attribute\s+/i, /^\s*key\s+primary/i, /^\s*key\s+foreign/i]; for (const pattern of blocked) { if (pattern.test(line)) { throw new Error( `Override file ${path.basename(filePath)} attempts to redefine domain structure: ${line.trim()}`, ); } } } export function parseOverrides(rootDir, entities) { const overrideFiles = getOverrideFiles(rootDir); const entityNames = new Set(entities.map((entity) => entity.name)); const fieldNames = new Set( entities.flatMap((entity) => entity.fields.map((field) => `${entity.name}.${field.name}`)), ); const api = { resources: {} }; const ui = { fields: {} }; for (const filePath of overrideFiles) { const isApi = filePath.endsWith('api-overrides.dsl'); const isUi = filePath.endsWith('ui-overrides.dsl'); const lines = readFileSync(filePath, 'utf8').split(/\r?\n/); for (const rawLine of lines) { const line = stripInlineComment(rawLine); if (!line) { continue; } assertNoDomainRedefinition(line, filePath); if (isApi) { const match = line.match(/^resource\s+([A-Za-z][A-Za-z0-9_]*)\s+path\s+"([^"]+)"\s*;$/); if (!match) { throw new Error( `Unsupported API override syntax in ${path.basename(filePath)}: ${line}`, ); } const [, entityName, resourcePath] = match; if (!entityNames.has(entityName)) { throw new Error(`API override references unknown entity ${entityName}`); } api.resources[entityName] = { path: resourcePath }; } if (isUi) { const match = line.match( /^field\s+([A-Za-z][A-Za-z0-9_]*)\.([A-Za-z][A-Za-z0-9_]*)\s+widget\s+"([^"]+)"\s*;$/, ); if (!match) { throw new Error( `Unsupported UI override syntax in ${path.basename(filePath)}: ${line}`, ); } const [, entityName, fieldName, widget] = match; const key = `${entityName}.${fieldName}`; if (!fieldNames.has(key)) { throw new Error(`UI override references unknown field ${key}`); } ui.fields[key] = { widget }; } } } return { api, ui, sourceFiles: overrideFiles.map((filePath) => path.relative(rootDir, filePath).replaceAll('\\', '/')) }; } export function buildDomainSummary(rootDir) { const { dslFiles, enums, entities } = parseDslFiles(rootDir); parseOverrides(rootDir, entities); const entityByName = new Map(entities.map((entity) => [entity.name, entity])); const enumByName = new Map(enums.map((entry) => [entry.name, entry])); for (const entity of entities) { if (!entity.primaryKey) { throw new Error(`Entity ${entity.name} is missing a primary key`); } const primaryKeyField = entity.fields.find((field) => field.name === entity.primaryKey); if (!primaryKeyField) { throw new Error( `Entity ${entity.name} declares primary key ${entity.primaryKey}, but the field is missing`, ); } if (entity.fields.some((field) => !field.type)) { throw new Error(`Entity ${entity.name} has attributes with missing type`); } for (const foreignKey of entity.foreignKeys) { const targetEntity = entityByName.get(foreignKey.references.entity); if (!targetEntity) { throw new Error( `Foreign key ${entity.name}.${foreignKey.field} references missing entity ${foreignKey.references.entity}`, ); } const targetField = targetEntity.fields.find( (field) => field.name === foreignKey.references.field, ); if (!targetField) { throw new Error( `Foreign key ${entity.name}.${foreignKey.field} references missing field ${foreignKey.references.entity}.${foreignKey.references.field}`, ); } const sourceField = entity.fields.find((field) => field.name === foreignKey.field); if (sourceField && sourceField.type !== targetField.type) { throw new Error( `Foreign key ${entity.name}.${foreignKey.field} type ${sourceField.type} does not match ${foreignKey.references.entity}.${foreignKey.references.field} type ${targetField.type}`, ); } } const fieldNames = new Set(); for (const field of entity.fields) { if (fieldNames.has(field.name)) { throw new Error(`Entity ${entity.name} has duplicate field ${field.name}`); } fieldNames.add(field.name); } } const entityNames = new Set(); for (const entity of entities) { if (entityNames.has(entity.name)) { throw new Error(`Duplicate entity definition: ${entity.name}`); } entityNames.add(entity.name); } for (const [enumName, enumDefinition] of enumByName.entries()) { const valueNames = new Set(); for (const value of enumDefinition.values) { if (valueNames.has(value.name)) { throw new Error(`Enum ${enumName} has duplicate value ${value.name}`); } valueNames.add(value.name); } } return { sourceFiles: dslFiles.map((filePath) => path.relative(rootDir, filePath).replaceAll('\\', '/')), entities: entities.map((entity) => ({ name: entity.name, primaryKey: entity.primaryKey, fields: entity.fields.map((field) => ({ name: field.name, type: field.type, required: field.required, unique: field.unique, default: field.default, })), foreignKeys: entity.foreignKeys, })), enums, }; }