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:
28
server/src/common/field-labels.generated.ts
Normal file
28
server/src/common/field-labels.generated.ts
Normal 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": "Общая наработка, моточасов",
|
||||
};
|
||||
@@ -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)
|
||||
|
||||
@@ -8,34 +8,7 @@ import {
|
||||
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: 'Идентификатор',
|
||||
};
|
||||
import { FIELD_LABELS } from './common/field-labels.generated';
|
||||
|
||||
function prettifyFieldName(field: string): string {
|
||||
if (FIELD_LABELS[field]) return FIELD_LABELS[field];
|
||||
@@ -60,9 +33,12 @@ function constraintToRuMessage(field: string, constraint: string): string {
|
||||
return `Поле "${label}" должно быть UUID`;
|
||||
case 'isNumberString':
|
||||
return `Поле "${label}" должно быть числом`;
|
||||
case 'isNumber':
|
||||
return `Поле "${label}" должно быть числом`;
|
||||
case 'isIso8601':
|
||||
return `Поле "${label}" должно содержать корректную дату`;
|
||||
case 'isEnum':
|
||||
case 'isIn':
|
||||
return `Поле "${label}" содержит недопустимое значение`;
|
||||
default:
|
||||
return `Поле "${label}" заполнено некорректно`;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { IsISO8601, IsNotEmpty, IsNumberString, IsString } from 'class-validator';
|
||||
import { IsISO8601, IsIn, IsNotEmpty, IsNumber, IsString } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
export class CreateEquipmentDto {
|
||||
@IsString({ message: 'inventoryNumber: должно быть строкой' })
|
||||
@@ -12,17 +13,19 @@ export class CreateEquipmentDto {
|
||||
@IsString({ message: 'equipmentTypeCode: должно быть строкой' })
|
||||
@IsNotEmpty({ message: 'equipmentTypeCode: обязательное поле' })
|
||||
equipmentTypeCode!: string;
|
||||
@IsString({ message: 'status: должно быть строкой' })
|
||||
@IsIn(['Active', 'Repair', 'Reserve', 'WriteOff'], { message: 'status: недопустимое значение' })
|
||||
@IsNotEmpty({ message: 'status: обязательное поле' })
|
||||
status!: string;
|
||||
@IsString({ message: 'location: должно быть строкой' })
|
||||
location?: string;
|
||||
@IsISO8601({}, { message: 'commissionedAt: должно содержать корректную дату' })
|
||||
commissionedAt?: string;
|
||||
@IsNumberString({}, { message: 'totalEngineHours: должно быть числом' })
|
||||
totalEngineHours?: string;
|
||||
@IsNumberString({}, { message: 'engineHoursSinceLastRepair: должно быть числом' })
|
||||
engineHoursSinceLastRepair?: string;
|
||||
@Type(() => Number)
|
||||
@IsNumber({ allowNaN: false, allowInfinity: false }, { message: 'totalEngineHours: должно быть числом' })
|
||||
totalEngineHours?: number;
|
||||
@Type(() => Number)
|
||||
@IsNumber({ allowNaN: false, allowInfinity: false }, { message: 'engineHoursSinceLastRepair: должно быть числом' })
|
||||
engineHoursSinceLastRepair?: number;
|
||||
@IsISO8601({}, { message: 'lastRepairAt: должно содержать корректную дату' })
|
||||
lastRepairAt?: string;
|
||||
@IsString({ message: 'notes: должно быть строкой' })
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { IsISO8601, IsNumberString, IsOptional, IsString, IsUUID } from 'class-validator';
|
||||
import { IsISO8601, IsIn, IsNumber, IsOptional, IsString, IsUUID } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
export class UpdateEquipmentDto {
|
||||
@IsOptional()
|
||||
@@ -17,7 +18,7 @@ export class UpdateEquipmentDto {
|
||||
@IsString({ message: 'equipmentTypeCode: должно быть строкой' })
|
||||
equipmentTypeCode?: string;
|
||||
@IsOptional()
|
||||
@IsString({ message: 'status: должно быть строкой' })
|
||||
@IsIn(['Active', 'Repair', 'Reserve', 'WriteOff'], { message: 'status: недопустимое значение' })
|
||||
status?: string;
|
||||
@IsOptional()
|
||||
@IsString({ message: 'location: должно быть строкой' })
|
||||
@@ -26,11 +27,13 @@ export class UpdateEquipmentDto {
|
||||
@IsISO8601({}, { message: 'commissionedAt: должно содержать корректную дату' })
|
||||
commissionedAt?: string;
|
||||
@IsOptional()
|
||||
@IsNumberString({}, { message: 'totalEngineHours: должно быть числом' })
|
||||
totalEngineHours?: string;
|
||||
@Type(() => Number)
|
||||
@IsNumber({ allowNaN: false, allowInfinity: false }, { message: 'totalEngineHours: должно быть числом' })
|
||||
totalEngineHours?: number;
|
||||
@IsOptional()
|
||||
@IsNumberString({}, { message: 'engineHoursSinceLastRepair: должно быть числом' })
|
||||
engineHoursSinceLastRepair?: string;
|
||||
@Type(() => Number)
|
||||
@IsNumber({ allowNaN: false, allowInfinity: false }, { message: 'engineHoursSinceLastRepair: должно быть числом' })
|
||||
engineHoursSinceLastRepair?: number;
|
||||
@IsOptional()
|
||||
@IsISO8601({}, { message: 'lastRepairAt: должно содержать корректную дату' })
|
||||
lastRepairAt?: string;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { IsISO8601, IsNotEmpty, IsNumberString, IsString, IsUUID } from 'class-validator';
|
||||
import { IsISO8601, IsIn, IsNotEmpty, IsNumber, IsString, IsUUID } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
export class CreateRepairOrderDto {
|
||||
@IsString({ message: 'number: должно быть строкой' })
|
||||
@@ -7,10 +8,10 @@ export class CreateRepairOrderDto {
|
||||
@IsUUID(undefined, { message: 'equipmentId: должно быть UUID' })
|
||||
@IsNotEmpty({ message: 'equipmentId: обязательное поле' })
|
||||
equipmentId!: string;
|
||||
@IsString({ message: 'repairKind: должно быть строкой' })
|
||||
@IsIn(['TO', 'TR', 'TRE', 'KR', 'AR', 'MP'], { message: 'repairKind: недопустимое значение' })
|
||||
@IsNotEmpty({ message: 'repairKind: обязательное поле' })
|
||||
repairKind!: string;
|
||||
@IsString({ message: 'status: должно быть строкой' })
|
||||
@IsIn(['Draft', 'Approved', 'InWork', 'Done', 'Cancelled'], { message: 'status: недопустимое значение' })
|
||||
@IsNotEmpty({ message: 'status: обязательное поле' })
|
||||
status!: string;
|
||||
@IsISO8601({}, { message: 'plannedAt: должно содержать корректную дату' })
|
||||
@@ -22,8 +23,9 @@ export class CreateRepairOrderDto {
|
||||
completedAt?: string;
|
||||
@IsString({ message: 'contractor: должно быть строкой' })
|
||||
contractor?: string;
|
||||
@IsNumberString({}, { message: 'engineHoursAtRepair: должно быть числом' })
|
||||
engineHoursAtRepair?: string;
|
||||
@Type(() => Number)
|
||||
@IsNumber({ allowNaN: false, allowInfinity: false }, { message: 'engineHoursAtRepair: должно быть числом' })
|
||||
engineHoursAtRepair?: number;
|
||||
@IsString({ message: 'description: должно быть строкой' })
|
||||
description?: string;
|
||||
@IsString({ message: 'notes: должно быть строкой' })
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { IsISO8601, IsNumberString, IsOptional, IsString, IsUUID } from 'class-validator';
|
||||
import { IsISO8601, IsIn, IsNumber, IsOptional, IsString, IsUUID } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
export class UpdateRepairOrderDto {
|
||||
@IsOptional()
|
||||
@@ -11,10 +12,10 @@ export class UpdateRepairOrderDto {
|
||||
@IsUUID(undefined, { message: 'equipmentId: должно быть UUID' })
|
||||
equipmentId?: string;
|
||||
@IsOptional()
|
||||
@IsString({ message: 'repairKind: должно быть строкой' })
|
||||
@IsIn(['TO', 'TR', 'TRE', 'KR', 'AR', 'MP'], { message: 'repairKind: недопустимое значение' })
|
||||
repairKind?: string;
|
||||
@IsOptional()
|
||||
@IsString({ message: 'status: должно быть строкой' })
|
||||
@IsIn(['Draft', 'Approved', 'InWork', 'Done', 'Cancelled'], { message: 'status: недопустимое значение' })
|
||||
status?: string;
|
||||
@IsOptional()
|
||||
@IsISO8601({}, { message: 'plannedAt: должно содержать корректную дату' })
|
||||
@@ -29,8 +30,9 @@ export class UpdateRepairOrderDto {
|
||||
@IsString({ message: 'contractor: должно быть строкой' })
|
||||
contractor?: string;
|
||||
@IsOptional()
|
||||
@IsNumberString({}, { message: 'engineHoursAtRepair: должно быть числом' })
|
||||
engineHoursAtRepair?: string;
|
||||
@Type(() => Number)
|
||||
@IsNumber({ allowNaN: false, allowInfinity: false }, { message: 'engineHoursAtRepair: должно быть числом' })
|
||||
engineHoursAtRepair?: number;
|
||||
@IsOptional()
|
||||
@IsString({ message: 'description: должно быть строкой' })
|
||||
description?: string;
|
||||
|
||||
Reference in New Issue
Block a user