feat: align RU validation, error contract, and generator runtime templates

Wire DSL-derived field labels, safe API error JSON (string|string[]), decimal/enum DTO fixes, and client dataProvider without comma-splitting. Add generation/templates/runtime as canonical source copied on generate; extend AID bundle, prompts, validation gate, and docs.
This commit is contained in:
time_
2026-03-29 10:39:54 +03:00
parent 9a1a700efa
commit f6cdeec918
25 changed files with 801 additions and 93 deletions

View File

@@ -0,0 +1,28 @@
/** AUTO-GENERATED from domain DSL (generation/generate.mjs). Do not edit by hand. */
export const FIELD_LABELS: Record<string, string> = {
"code": "Код вида оборудования",
"commissionedAt": "Дата ввода в эксплуатацию",
"completedAt": "Фактическая дата завершения",
"contractor": "Подрядная организация (если внешний ремонт)",
"description": "Описание работ / дефекта",
"engineHoursAtRepair": "Наработка на момент ремонта, моточасов",
"engineHoursSinceLastRepair": "Наработка с последнего ремонта, моточасов",
"equipmentId": "Оборудование",
"equipmentTypeCode": "Вид оборудования",
"id": "Идентификатор",
"inventoryNumber": "Инвентарный номер",
"lastRepairAt": "Дата последнего ремонта",
"location": "Место эксплуатации / скважина / куст",
"maintenanceIntervalHours": "Периодичность ТО, моточасов",
"manufacturer": "Производитель",
"name": "Наименование единицы оборудования",
"notes": "Примечания",
"number": "Номер заявки",
"overhaulIntervalHours": "Периодичность КР, моточасов",
"plannedAt": "Плановая дата начала",
"repairKind": "Вид ремонта",
"serialNumber": "Заводской (серийный) номер",
"startedAt": "Фактическая дата начала",
"status": "Текущий статус",
"totalEngineHours": "Общая наработка, моточасов",
};

View File

@@ -12,7 +12,7 @@ import { Request, Response } from 'express';
type ErrorResponseBody = {
statusCode: number;
message: string;
message: string | string[];
code: string;
details?: unknown;
path: string;
@@ -39,8 +39,10 @@ export class ApiExceptionFilter implements ExceptionFilter {
};
if (mapped.statusCode >= 500) {
const logDetail =
exception instanceof Error ? exception.message : String(mapped.message);
this.logger.error(
`Unhandled error on ${request.method} ${request.url}: ${mapped.message}`,
`Unhandled error on ${request.method} ${request.url}: ${logDetail}`,
exception instanceof Error ? exception.stack : undefined,
);
}
@@ -50,7 +52,7 @@ export class ApiExceptionFilter implements ExceptionFilter {
private mapException(exception: unknown): {
statusCode: number;
message: string;
message: string | string[];
code: string;
details?: unknown;
} {
@@ -74,9 +76,11 @@ export class ApiExceptionFilter implements ExceptionFilter {
}
const rawMessage = payload?.message ?? exception.message;
const message = Array.isArray(rawMessage)
? rawMessage.join(', ')
: rawMessage || exception.message;
const message: string | string[] = Array.isArray(rawMessage)
? rawMessage.map((m) => String(m))
: typeof rawMessage === 'string' && rawMessage.length > 0
? rawMessage
: String(exception.message ?? 'Bad Request');
return {
statusCode,
@@ -118,21 +122,26 @@ export class ApiExceptionFilter implements ExceptionFilter {
if (exception instanceof Error) {
return {
statusCode: HttpStatus.INTERNAL_SERVER_ERROR,
message: exception.message || 'Internal server error',
message: 'Внутренняя ошибка сервера.',
code: 'INTERNAL_ERROR',
};
}
return {
statusCode: HttpStatus.INTERNAL_SERVER_ERROR,
message: 'Internal server error',
message: 'Внутренняя ошибка сервера.',
code: 'INTERNAL_ERROR',
};
}
private mapPrismaKnownRequestError(
exception: Prisma.PrismaClientKnownRequestError,
) {
): {
statusCode: number;
message: string;
code: string;
details?: unknown;
} {
switch (exception.code) {
case 'P2002': {
const target = Array.isArray(exception.meta?.target)