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:
@@ -1,8 +0,0 @@
|
||||
node_modules
|
||||
dist
|
||||
coverage
|
||||
.git
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
npm-debug.log*
|
||||
@@ -1,37 +0,0 @@
|
||||
FROM node:20-bookworm-slim AS build
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends openssl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY package*.json ./
|
||||
|
||||
COPY prisma ./prisma
|
||||
|
||||
RUN npm ci
|
||||
|
||||
COPY nest-cli.json tsconfig*.json ./
|
||||
COPY src ./src
|
||||
|
||||
RUN npm run build
|
||||
|
||||
FROM node:20-bookworm-slim AS runtime
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
ENV NODE_ENV=production
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends openssl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY --from=build /app/package*.json ./
|
||||
COPY --from=build /app/node_modules ./node_modules
|
||||
COPY --from=build /app/prisma ./prisma
|
||||
COPY --from=build /app/dist ./dist
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["sh", "-c", "npx prisma migrate deploy && node dist/src/main.js"]
|
||||
40
server/package-lock.json
generated
40
server/package-lock.json
generated
@@ -15,6 +15,8 @@
|
||||
"@nestjs/core": "^10.0.0",
|
||||
"@nestjs/platform-express": "^10.0.0",
|
||||
"@prisma/client": "^5.22.0",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.1",
|
||||
"jose": "^6.2.2",
|
||||
"reflect-metadata": "^0.2.0",
|
||||
"rxjs": "^7.8.1"
|
||||
@@ -2426,6 +2428,12 @@
|
||||
"@types/superagent": "^8.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/validator": {
|
||||
"version": "13.15.10",
|
||||
"resolved": "https://registry.npmmirror.com/@types/validator/-/validator-13.15.10.tgz",
|
||||
"integrity": "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/yargs": {
|
||||
"version": "17.0.35",
|
||||
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz",
|
||||
@@ -3655,6 +3663,23 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/class-transformer": {
|
||||
"version": "0.5.1",
|
||||
"resolved": "https://registry.npmmirror.com/class-transformer/-/class-transformer-0.5.1.tgz",
|
||||
"integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/class-validator": {
|
||||
"version": "0.14.4",
|
||||
"resolved": "https://registry.npmmirror.com/class-validator/-/class-validator-0.14.4.tgz",
|
||||
"integrity": "sha512-AwNusCCam51q703dW82x95tOqQp6oC9HNUl724KxJJOfnKscI8dOloXFgyez7LbTTKWuRBA37FScqVbJEoq8Yw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/validator": "^13.15.3",
|
||||
"libphonenumber-js": "^1.11.1",
|
||||
"validator": "^13.15.22"
|
||||
}
|
||||
},
|
||||
"node_modules/cli-cursor": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
|
||||
@@ -6823,6 +6848,12 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/libphonenumber-js": {
|
||||
"version": "1.12.40",
|
||||
"resolved": "https://registry.npmmirror.com/libphonenumber-js/-/libphonenumber-js-1.12.40.tgz",
|
||||
"integrity": "sha512-HKGs7GowShNls3Zh+7DTr6wYpPk5jC78l508yQQY3e8ZgJChM3A9JZghmMJZuK+5bogSfuTafpjksGSR3aMIEg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lines-and-columns": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
|
||||
@@ -9462,6 +9493,15 @@
|
||||
"node": ">=10.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/validator": {
|
||||
"version": "13.15.26",
|
||||
"resolved": "https://registry.npmmirror.com/validator/-/validator-13.15.26.tgz",
|
||||
"integrity": "sha512-spH26xU080ydGggxRyR1Yhcbgx+j3y5jbNXk/8L+iRvdIEQ4uTRH2Sgf2dokud6Q4oAtsbNvJ1Ft+9xmm6IZcA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/vary": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"start": "nest start",
|
||||
"start:dev": "nest start --watch",
|
||||
"start:debug": "nest start --debug --watch",
|
||||
"start:prod": "node dist/src/main.js",
|
||||
"start:prod": "node dist/main",
|
||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
@@ -31,6 +31,8 @@
|
||||
"@nestjs/core": "^10.0.0",
|
||||
"@nestjs/platform-express": "^10.0.0",
|
||||
"@prisma/client": "^5.22.0",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.1",
|
||||
"jose": "^6.2.2",
|
||||
"reflect-metadata": "^0.2.0",
|
||||
"rxjs": "^7.8.1"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
-- CreateEnum
|
||||
-- CreateEnum
|
||||
CREATE TYPE "EquipmentStatus" AS ENUM ('Active', 'Repair', 'Reserve', 'WriteOff');
|
||||
|
||||
-- CreateEnum
|
||||
|
||||
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": "Общая наработка, моточасов",
|
||||
};
|
||||
184
server/src/common/filters/api-exception.filter.ts
Normal file
184
server/src/common/filters/api-exception.filter.ts
Normal file
@@ -0,0 +1,184 @@
|
||||
import {
|
||||
ArgumentsHost,
|
||||
BadRequestException,
|
||||
Catch,
|
||||
ExceptionFilter,
|
||||
HttpException,
|
||||
HttpStatus,
|
||||
Logger,
|
||||
} from '@nestjs/common';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { Request, Response } from 'express';
|
||||
|
||||
type ErrorResponseBody = {
|
||||
statusCode: number;
|
||||
message: string | string[];
|
||||
code: string;
|
||||
details?: unknown;
|
||||
path: string;
|
||||
timestamp: string;
|
||||
};
|
||||
|
||||
@Catch()
|
||||
export class ApiExceptionFilter implements ExceptionFilter {
|
||||
private readonly logger = new Logger(ApiExceptionFilter.name);
|
||||
|
||||
catch(exception: unknown, host: ArgumentsHost) {
|
||||
const ctx = host.switchToHttp();
|
||||
const response = ctx.getResponse<Response>();
|
||||
const request = ctx.getRequest<Request>();
|
||||
|
||||
const mapped = this.mapException(exception);
|
||||
const body: ErrorResponseBody = {
|
||||
statusCode: mapped.statusCode,
|
||||
message: mapped.message,
|
||||
code: mapped.code,
|
||||
...(mapped.details !== undefined ? { details: mapped.details } : {}),
|
||||
path: request.url,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
if (mapped.statusCode >= 500) {
|
||||
const logDetail =
|
||||
exception instanceof Error ? exception.message : String(mapped.message);
|
||||
this.logger.error(
|
||||
`Unhandled error on ${request.method} ${request.url}: ${logDetail}`,
|
||||
exception instanceof Error ? exception.stack : undefined,
|
||||
);
|
||||
}
|
||||
|
||||
response.status(mapped.statusCode).json(body);
|
||||
}
|
||||
|
||||
private mapException(exception: unknown): {
|
||||
statusCode: number;
|
||||
message: string | string[];
|
||||
code: string;
|
||||
details?: unknown;
|
||||
} {
|
||||
if (exception instanceof HttpException) {
|
||||
const statusCode = exception.getStatus();
|
||||
const payload = exception.getResponse() as
|
||||
| string
|
||||
| {
|
||||
message?: string | string[];
|
||||
error?: string;
|
||||
code?: string;
|
||||
details?: unknown;
|
||||
};
|
||||
|
||||
if (typeof payload === 'string') {
|
||||
return {
|
||||
statusCode,
|
||||
message: payload,
|
||||
code: `HTTP_${statusCode}`,
|
||||
};
|
||||
}
|
||||
|
||||
const rawMessage = payload?.message ?? 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,
|
||||
message,
|
||||
code: payload?.code ?? payload?.error ?? `HTTP_${statusCode}`,
|
||||
details: payload?.details,
|
||||
};
|
||||
}
|
||||
|
||||
if (exception instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
return this.mapPrismaKnownRequestError(exception);
|
||||
}
|
||||
|
||||
if (exception instanceof Prisma.PrismaClientValidationError) {
|
||||
return {
|
||||
statusCode: HttpStatus.BAD_REQUEST,
|
||||
message:
|
||||
'Некорректные данные запроса. Проверьте обязательные поля и форматы значений.',
|
||||
code: 'PRISMA_VALIDATION_ERROR',
|
||||
};
|
||||
}
|
||||
|
||||
if (exception instanceof Prisma.PrismaClientInitializationError) {
|
||||
return {
|
||||
statusCode: HttpStatus.SERVICE_UNAVAILABLE,
|
||||
message: 'Сервис базы данных временно недоступен.',
|
||||
code: 'DATABASE_UNAVAILABLE',
|
||||
};
|
||||
}
|
||||
|
||||
if (exception instanceof Prisma.PrismaClientRustPanicError) {
|
||||
return {
|
||||
statusCode: HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
message: 'Внутренняя ошибка сервера.',
|
||||
code: 'DATABASE_ENGINE_PANIC',
|
||||
};
|
||||
}
|
||||
|
||||
if (exception instanceof Error) {
|
||||
return {
|
||||
statusCode: HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
message: 'Внутренняя ошибка сервера.',
|
||||
code: 'INTERNAL_ERROR',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
statusCode: HttpStatus.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)
|
||||
? exception.meta?.target.join(', ')
|
||||
: String(exception.meta?.target ?? '');
|
||||
return {
|
||||
statusCode: HttpStatus.CONFLICT,
|
||||
message: target
|
||||
? `Запись с таким значением уже существует (${target}).`
|
||||
: 'Запись с таким значением уже существует.',
|
||||
code: 'UNIQUE_CONSTRAINT_VIOLATION',
|
||||
details: exception.meta,
|
||||
};
|
||||
}
|
||||
case 'P2003':
|
||||
return {
|
||||
statusCode: HttpStatus.CONFLICT,
|
||||
message:
|
||||
'Операцию нельзя выполнить из-за связанных данных или некорректной ссылки.',
|
||||
code: 'FOREIGN_KEY_CONSTRAINT_VIOLATION',
|
||||
details: exception.meta,
|
||||
};
|
||||
case 'P2025':
|
||||
return {
|
||||
statusCode: HttpStatus.NOT_FOUND,
|
||||
message: 'Запись не найдена.',
|
||||
code: 'RECORD_NOT_FOUND',
|
||||
details: exception.meta,
|
||||
};
|
||||
default:
|
||||
return {
|
||||
statusCode: HttpStatus.BAD_REQUEST,
|
||||
message: 'Ошибка при обработке данных в базе.',
|
||||
code: exception.code,
|
||||
details: exception.meta,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,72 @@
|
||||
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';
|
||||
import { FIELD_LABELS } from './common/field-labels.generated';
|
||||
|
||||
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 'isNumber':
|
||||
return `Поле "${label}" должно быть числом`;
|
||||
case 'isIso8601':
|
||||
return `Поле "${label}" должно содержать корректную дату`;
|
||||
case 'isEnum':
|
||||
case 'isIn':
|
||||
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 +94,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);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,19 @@
|
||||
import { IsInt, IsNotEmpty, IsString } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
export class CreateEquipmentTypeDto {
|
||||
@IsString({ message: 'code: должно быть строкой' })
|
||||
@IsNotEmpty({ message: 'code: обязательное поле' })
|
||||
code?: string;
|
||||
@IsString({ message: 'name: должно быть строкой' })
|
||||
@IsNotEmpty({ message: 'name: обязательное поле' })
|
||||
name!: string;
|
||||
@IsString({ message: 'manufacturer: должно быть строкой' })
|
||||
manufacturer?: string;
|
||||
@Type(() => Number)
|
||||
@IsInt({ message: 'maintenanceIntervalHours: должно быть целым числом' })
|
||||
maintenanceIntervalHours?: number;
|
||||
@Type(() => Number)
|
||||
@IsInt({ message: 'overhaulIntervalHours: должно быть целым числом' })
|
||||
overhaulIntervalHours?: number;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,25 @@
|
||||
import { IsInt, IsOptional, IsString } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
export class UpdateEquipmentTypeDto {
|
||||
@IsOptional()
|
||||
@IsString({ message: 'id: должно быть строкой' })
|
||||
id?: string;
|
||||
@IsOptional()
|
||||
@IsString({ message: 'code: должно быть строкой' })
|
||||
code?: string;
|
||||
@IsOptional()
|
||||
@IsString({ message: 'name: должно быть строкой' })
|
||||
name?: string;
|
||||
@IsOptional()
|
||||
@IsString({ message: 'manufacturer: должно быть строкой' })
|
||||
manufacturer?: string;
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsInt({ message: 'maintenanceIntervalHours: должно быть целым числом' })
|
||||
maintenanceIntervalHours?: number;
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsInt({ message: 'overhaulIntervalHours: должно быть целым числом' })
|
||||
overhaulIntervalHours?: number;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,33 @@
|
||||
import { IsISO8601, IsIn, IsNotEmpty, IsNumber, IsString } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
export class CreateEquipmentDto {
|
||||
@IsString({ message: 'inventoryNumber: должно быть строкой' })
|
||||
@IsNotEmpty({ message: 'inventoryNumber: обязательное поле' })
|
||||
inventoryNumber!: string;
|
||||
@IsString({ message: 'serialNumber: должно быть строкой' })
|
||||
serialNumber?: string;
|
||||
@IsString({ message: 'name: должно быть строкой' })
|
||||
@IsNotEmpty({ message: 'name: обязательное поле' })
|
||||
name!: string;
|
||||
@IsString({ message: 'equipmentTypeCode: должно быть строкой' })
|
||||
@IsNotEmpty({ message: 'equipmentTypeCode: обязательное поле' })
|
||||
equipmentTypeCode!: string;
|
||||
@IsIn(['Active', 'Repair', 'Reserve', 'WriteOff'], { message: 'status: недопустимое значение' })
|
||||
@IsNotEmpty({ message: 'status: обязательное поле' })
|
||||
status!: string;
|
||||
@IsString({ message: 'location: должно быть строкой' })
|
||||
location?: string;
|
||||
@IsISO8601({}, { message: 'commissionedAt: должно содержать корректную дату' })
|
||||
commissionedAt?: string;
|
||||
totalEngineHours?: string;
|
||||
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: должно быть строкой' })
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
@@ -1,14 +1,43 @@
|
||||
import { IsISO8601, IsIn, IsNumber, IsOptional, IsString, IsUUID } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
export class UpdateEquipmentDto {
|
||||
@IsOptional()
|
||||
@IsUUID(undefined, { message: 'id: должно быть UUID' })
|
||||
id?: string;
|
||||
@IsOptional()
|
||||
@IsString({ message: 'inventoryNumber: должно быть строкой' })
|
||||
inventoryNumber?: string;
|
||||
@IsOptional()
|
||||
@IsString({ message: 'serialNumber: должно быть строкой' })
|
||||
serialNumber?: string;
|
||||
@IsOptional()
|
||||
@IsString({ message: 'name: должно быть строкой' })
|
||||
name?: string;
|
||||
@IsOptional()
|
||||
@IsString({ message: 'equipmentTypeCode: должно быть строкой' })
|
||||
equipmentTypeCode?: string;
|
||||
@IsOptional()
|
||||
@IsIn(['Active', 'Repair', 'Reserve', 'WriteOff'], { message: 'status: недопустимое значение' })
|
||||
status?: string;
|
||||
@IsOptional()
|
||||
@IsString({ message: 'location: должно быть строкой' })
|
||||
location?: string;
|
||||
@IsOptional()
|
||||
@IsISO8601({}, { message: 'commissionedAt: должно содержать корректную дату' })
|
||||
commissionedAt?: string;
|
||||
totalEngineHours?: string;
|
||||
engineHoursSinceLastRepair?: string;
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsNumber({ allowNaN: false, allowInfinity: false }, { message: 'totalEngineHours: должно быть числом' })
|
||||
totalEngineHours?: number;
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsNumber({ allowNaN: false, allowInfinity: false }, { message: 'engineHoursSinceLastRepair: должно быть числом' })
|
||||
engineHoursSinceLastRepair?: number;
|
||||
@IsOptional()
|
||||
@IsISO8601({}, { message: 'lastRepairAt: должно содержать корректную дату' })
|
||||
lastRepairAt?: string;
|
||||
@IsOptional()
|
||||
@IsString({ message: 'notes: должно быть строкой' })
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,33 @@
|
||||
import { IsISO8601, IsIn, IsNotEmpty, IsNumber, IsString, IsUUID } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
export class CreateRepairOrderDto {
|
||||
@IsString({ message: 'number: должно быть строкой' })
|
||||
@IsNotEmpty({ message: 'number: обязательное поле' })
|
||||
number!: string;
|
||||
@IsUUID(undefined, { message: 'equipmentId: должно быть UUID' })
|
||||
@IsNotEmpty({ message: 'equipmentId: обязательное поле' })
|
||||
equipmentId!: string;
|
||||
@IsIn(['TO', 'TR', 'TRE', 'KR', 'AR', 'MP'], { message: 'repairKind: недопустимое значение' })
|
||||
@IsNotEmpty({ message: 'repairKind: обязательное поле' })
|
||||
repairKind!: string;
|
||||
@IsIn(['Draft', 'Approved', 'InWork', 'Done', 'Cancelled'], { message: 'status: недопустимое значение' })
|
||||
@IsNotEmpty({ message: 'status: обязательное поле' })
|
||||
status!: string;
|
||||
@IsISO8601({}, { message: 'plannedAt: должно содержать корректную дату' })
|
||||
@IsNotEmpty({ message: 'plannedAt: обязательное поле' })
|
||||
plannedAt!: string;
|
||||
@IsISO8601({}, { message: 'startedAt: должно содержать корректную дату' })
|
||||
startedAt?: string;
|
||||
@IsISO8601({}, { message: 'completedAt: должно содержать корректную дату' })
|
||||
completedAt?: string;
|
||||
@IsString({ message: 'contractor: должно быть строкой' })
|
||||
contractor?: string;
|
||||
engineHoursAtRepair?: string;
|
||||
@Type(() => Number)
|
||||
@IsNumber({ allowNaN: false, allowInfinity: false }, { message: 'engineHoursAtRepair: должно быть числом' })
|
||||
engineHoursAtRepair?: number;
|
||||
@IsString({ message: 'description: должно быть строкой' })
|
||||
description?: string;
|
||||
@IsString({ message: 'notes: должно быть строкой' })
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
@@ -1,14 +1,42 @@
|
||||
import { IsISO8601, IsIn, IsNumber, IsOptional, IsString, IsUUID } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
export class UpdateRepairOrderDto {
|
||||
@IsOptional()
|
||||
@IsUUID(undefined, { message: 'id: должно быть UUID' })
|
||||
id?: string;
|
||||
@IsOptional()
|
||||
@IsString({ message: 'number: должно быть строкой' })
|
||||
number?: string;
|
||||
@IsOptional()
|
||||
@IsUUID(undefined, { message: 'equipmentId: должно быть UUID' })
|
||||
equipmentId?: string;
|
||||
@IsOptional()
|
||||
@IsIn(['TO', 'TR', 'TRE', 'KR', 'AR', 'MP'], { message: 'repairKind: недопустимое значение' })
|
||||
repairKind?: string;
|
||||
@IsOptional()
|
||||
@IsIn(['Draft', 'Approved', 'InWork', 'Done', 'Cancelled'], { message: 'status: недопустимое значение' })
|
||||
status?: string;
|
||||
@IsOptional()
|
||||
@IsISO8601({}, { message: 'plannedAt: должно содержать корректную дату' })
|
||||
plannedAt?: string;
|
||||
@IsOptional()
|
||||
@IsISO8601({}, { message: 'startedAt: должно содержать корректную дату' })
|
||||
startedAt?: string;
|
||||
@IsOptional()
|
||||
@IsISO8601({}, { message: 'completedAt: должно содержать корректную дату' })
|
||||
completedAt?: string;
|
||||
@IsOptional()
|
||||
@IsString({ message: 'contractor: должно быть строкой' })
|
||||
contractor?: string;
|
||||
engineHoursAtRepair?: string;
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsNumber({ allowNaN: false, allowInfinity: false }, { message: 'engineHoursAtRepair: должно быть числом' })
|
||||
engineHoursAtRepair?: number;
|
||||
@IsOptional()
|
||||
@IsString({ message: 'description: должно быть строкой' })
|
||||
description?: string;
|
||||
@IsOptional()
|
||||
@IsString({ message: 'notes: должно быть строкой' })
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user