update generated validation UX and AID integration flow

Improve generated validation behavior and backend error mapping so UI shows user-friendly Russian messages, while keeping filtering/sorting and exporter updates aligned with current app generation flow.
This commit is contained in:
time_
2026-03-27 11:07:01 +03:00
parent 420ca0348c
commit 9a1a700efa
12 changed files with 539 additions and 21 deletions

View File

@@ -1,7 +1,96 @@
import { NestFactory } from '@nestjs/core';
import { ConfigService } from '@nestjs/config';
import {
BadRequestException,
ValidationError,
ValidationPipe,
} from '@nestjs/common';
import { AppModule } from './app.module';
import { RuntimeEnvironment } from './config/env.validation';
import { ApiExceptionFilter } from './common/filters/api-exception.filter';
const FIELD_LABELS: Record<string, string> = {
code: 'Код',
name: 'Название',
manufacturer: 'Производитель',
maintenanceIntervalHours: 'Интервал ТО (часы)',
overhaulIntervalHours: 'Интервал капремонта (часы)',
inventoryNumber: 'Инвентарный номер',
serialNumber: 'Серийный номер',
equipmentTypeCode: 'Тип оборудования',
equipmentId: 'Оборудование',
status: 'Статус',
location: 'Местоположение',
commissionedAt: 'Дата ввода в эксплуатацию',
totalEngineHours: 'Наработка общая',
engineHoursSinceLastRepair: 'Наработка после ремонта',
lastRepairAt: 'Дата последнего ремонта',
notes: 'Примечание',
number: 'Номер',
repairKind: 'Вид ремонта',
plannedAt: 'Плановая дата',
startedAt: 'Дата начала',
completedAt: 'Дата завершения',
contractor: 'Подрядчик',
engineHoursAtRepair: 'Наработка на момент ремонта',
description: 'Описание',
id: 'Идентификатор',
};
function prettifyFieldName(field: string): string {
if (FIELD_LABELS[field]) return FIELD_LABELS[field];
const withSpaces = field
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
.replace(/_/g, ' ')
.trim();
if (!withSpaces) return field;
return withSpaces[0].toUpperCase() + withSpaces.slice(1);
}
function constraintToRuMessage(field: string, constraint: string): string {
const label = prettifyFieldName(field);
switch (constraint) {
case 'isNotEmpty':
return `Поле "${label}" обязательно`;
case 'isString':
return `Поле "${label}" должно быть строкой`;
case 'isInt':
return `Поле "${label}" должно быть целым числом`;
case 'isUUID':
return `Поле "${label}" должно быть UUID`;
case 'isNumberString':
return `Поле "${label}" должно быть числом`;
case 'isIso8601':
return `Поле "${label}" должно содержать корректную дату`;
case 'isEnum':
return `Поле "${label}" содержит недопустимое значение`;
default:
return `Поле "${label}" заполнено некорректно`;
}
}
function buildValidationMessages(errors: ValidationError[]): string[] {
const messages: string[] = [];
const walk = (errorList: ValidationError[]) => {
for (const error of errorList) {
if (error.constraints) {
const constraints = Object.keys(error.constraints);
// If field is empty, "required" is enough; skip type noise.
const filtered = constraints.includes('isNotEmpty')
? ['isNotEmpty']
: constraints;
filtered.forEach((constraint) =>
messages.push(constraintToRuMessage(error.property, constraint)),
);
}
if (error.children?.length) walk(error.children);
}
};
walk(errors);
return Array.from(new Set(messages));
}
async function bootstrap() {
const app = await NestFactory.create(AppModule);
@@ -29,6 +118,17 @@ async function bootstrap() {
credentials: false,
});
app.useGlobalPipes(
new ValidationPipe({
transform: true,
whitelist: true,
forbidUnknownValues: false,
exceptionFactory: (errors) =>
new BadRequestException(buildValidationMessages(errors)),
}),
);
app.useGlobalFilters(new ApiExceptionFilter());
const port = configService.get('PORT', 3000);
await app.listen(port);
}