Initial commit

This commit is contained in:
MaKarin
2026-03-25 21:01:31 +03:00
commit a46a860f4e
111 changed files with 21805 additions and 0 deletions

357
tools/dsl-summary.mjs Normal file
View File

@@ -0,0 +1,357 @@
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,
};
}