From d18a6764c93b3ed85455408a0a68740043ebd907 Mon Sep 17 00:00:00 2001 From: n08i40k Date: Sat, 28 Sep 2024 01:40:19 +0400 Subject: [PATCH] 1.1.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Фикс невозможности парсинга субботы. class-validator.interceptor.ts - Добавлена возможность возвращать клиенту любой DTO из списка. Добавлен разный ответ клиенту в зависимости от его версии. --- package-lock.json | 4 +- package.json | 2 +- src/dto/schedule.dto.ts | 46 +++++++++++- .../schedule-parser/schedule-parser.ts | 13 +++- src/schedule/schedule.controller.ts | 48 +++++++++--- src/schedule/schedule.service.ts | 15 +++- .../validation/class-validator.interceptor.ts | 75 +++++++++++++------ src/version/client-version.decorator.ts | 13 ++++ 8 files changed, 172 insertions(+), 44 deletions(-) create mode 100644 src/version/client-version.decorator.ts diff --git a/package-lock.json b/package-lock.json index 6c5e9da..4981d1f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "schedule-parser-next", - "version": "1.1.0", + "version": "1.1.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "schedule-parser-next", - "version": "1.1.0", + "version": "1.1.1", "license": "UNLICENSED", "dependencies": { "@nestjs/cache-manager": "^2.2.2", diff --git a/package.json b/package.json index 915e1b5..ac57686 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "schedule-parser-next", - "version": "1.1.0", + "version": "1.1.1", "description": "", "author": "", "private": true, diff --git a/src/dto/schedule.dto.ts b/src/dto/schedule.dto.ts index 36faa94..b5a4b8d 100644 --- a/src/dto/schedule.dto.ts +++ b/src/dto/schedule.dto.ts @@ -12,7 +12,13 @@ import { ValidateNested, } from "class-validator"; import { ApiProperty, OmitType, PickType } from "@nestjs/swagger"; -import { Transform, Type } from "class-transformer"; +import { + Expose, + instanceToPlain, + plainToClass, + Transform, + Type, +} from "class-transformer"; export class LessonTimeDto { @ApiProperty({ @@ -205,12 +211,13 @@ export class GroupDto { } } -export class CacheStatusDto { +export class CacheStatusV0Dto { @ApiProperty({ example: true, description: "Нужно ли обновить ссылку для скачивания xls?", }) @IsBoolean() + @Expose() cacheUpdateRequired: boolean; @ApiProperty({ @@ -218,9 +225,44 @@ export class CacheStatusDto { description: "Хеш последних полученных данных", }) @IsHash("sha1") + @Expose() cacheHash: string; } +export class CacheStatusV1Dto extends CacheStatusV0Dto { + @ApiProperty({ + example: new Date().valueOf(), + description: "Дата обновления кеша", + }) + @IsNumber() + @Expose() + lastCacheUpdate: number; + + @ApiProperty({ + example: new Date().valueOf(), + description: "Дата обновления расписания", + }) + @IsNumber() + @Expose() + lastScheduleUpdate: number; +} + +export class CacheStatusDto extends CacheStatusV1Dto { + public static stripVersion(instance: CacheStatusDto, version: number) { + switch (version) { + default: + return instance; + case 0: { + return plainToClass( + CacheStatusV0Dto, + instanceToPlain(instance), + { excludeExtraneousValues: true }, + ); + } + } + } +} + export class ScheduleDto { @ApiProperty({ example: new Date(), diff --git a/src/schedule/internal/schedule-parser/schedule-parser.ts b/src/schedule/internal/schedule-parser/schedule-parser.ts index 83bd244..167c031 100644 --- a/src/schedule/internal/schedule-parser/schedule-parser.ts +++ b/src/schedule/internal/schedule-parser/schedule-parser.ts @@ -107,11 +107,16 @@ export class ScheduleParser { ++row; } - const dayMonthIdx = /[А-Яа-я]+\s(\d+)\.\d+\.\d+/.exec( - trimAll(dayName), - ); + if ( + days.length == 0 || + !days[days.length - 1].name.startsWith("Суббота") + ) { + const dayMonthIdx = /[А-Яа-я]+\s(\d+)\.\d+\.\d+/.exec( + trimAll(dayName), + ); - if (dayMonthIdx === null) continue; + if (dayMonthIdx === null) continue; + } days.push({ row: row, diff --git a/src/schedule/schedule.controller.ts b/src/schedule/schedule.controller.ts index a077331..11dc8ea 100644 --- a/src/schedule/schedule.controller.ts +++ b/src/schedule/schedule.controller.ts @@ -11,6 +11,8 @@ import { AuthGuard } from "../auth/auth.guard"; import { ScheduleService } from "./schedule.service"; import { CacheStatusDto, + CacheStatusV0Dto, + CacheStatusV1Dto, GroupScheduleDto, GroupScheduleRequestDto, ScheduleDto, @@ -26,6 +28,7 @@ import { ApiOperation, refs, } from "@nestjs/swagger"; +import { ClientVersion } from "../version/client-version.decorator"; @Controller("api/v1/schedule") @UseGuards(AuthGuard) @@ -82,34 +85,59 @@ export class ScheduleController { } @ApiExtraModels(SiteMainPageDto) - @ApiExtraModels(CacheStatusDto) + @ApiExtraModels(CacheStatusV0Dto) + @ApiExtraModels(CacheStatusV1Dto) @ApiOperation({ summary: "Обновление данных основной страницы политехникума", tags: ["schedule"], }) - @ApiOkResponse({ description: "Данные обновлены успешно" }) + @ApiOkResponse({ + description: "Данные обновлены успешно", + schema: refs(CacheStatusV0Dto)[0], + }) + @ApiOkResponse({ + description: "Данные обновлены успешно", + schema: refs(CacheStatusV0Dto)[1], + }) @ApiNotAcceptableResponse({ description: "Передан некорректный код страницы", }) - @ResultDto(CacheStatusDto) + @ResultDto([CacheStatusV0Dto, CacheStatusV1Dto]) @HttpCode(HttpStatus.OK) @Post("update-site-main-page") async updateSiteMainPage( @Body() siteMainPageDto: SiteMainPageDto, - ): Promise { - return await this.scheduleService.updateSiteMainPage(siteMainPageDto); + @ClientVersion() version: number, + ): Promise { + return CacheStatusDto.stripVersion( + await this.scheduleService.updateSiteMainPage(siteMainPageDto), + version, + ); } - @ApiExtraModels(CacheStatusDto) + @ApiExtraModels(CacheStatusV0Dto) + @ApiExtraModels(CacheStatusV1Dto) @ApiOperation({ summary: "Получение информации о кеше", tags: ["schedule", "cache"], }) - @ApiOkResponse({ description: "Получение данных прошло успешно" }) - @ResultDto(CacheStatusDto) + @ApiOkResponse({ + description: "Получение данных прошло успешно", + schema: refs(CacheStatusV0Dto)[0], + }) + @ApiOkResponse({ + description: "Получение данных прошло успешно", + schema: refs(CacheStatusV1Dto)[0], + }) + @ResultDto([CacheStatusV0Dto, CacheStatusV1Dto]) @HttpCode(HttpStatus.OK) @Get("cache-status") - getCacheStatus(): CacheStatusDto { - return this.scheduleService.getCacheStatus(); + getCacheStatus( + @ClientVersion() version: number, + ): CacheStatusV0Dto | CacheStatusV1Dto { + return CacheStatusDto.stripVersion( + this.scheduleService.getCacheStatus(), + version, + ); } } diff --git a/src/schedule/schedule.service.ts b/src/schedule/schedule.service.ts index 060d937..af28120 100644 --- a/src/schedule/schedule.service.ts +++ b/src/schedule/schedule.service.ts @@ -31,6 +31,7 @@ export class ScheduleService { private cacheHash: string = "0000000000000000000000000000000000000000"; private lastChangedDays: Array> = []; + private scheduleUpdatedAt: Date = new Date(0); constructor(@Inject(CACHE_MANAGER) private readonly cacheManager: Cache) {} @@ -39,6 +40,8 @@ export class ScheduleService { cacheHash: this.cacheHash, cacheUpdateRequired: (Date.now() - this.cacheUpdatedAt.valueOf()) / 1000 / 60 >= 5, + lastCacheUpdate: this.cacheUpdatedAt.valueOf(), + lastScheduleUpdate: this.scheduleUpdatedAt.valueOf(), }; } @@ -50,11 +53,21 @@ export class ScheduleService { ) as Array; this.cacheUpdatedAt = new Date(); + + const oldHash = this.cacheHash; this.cacheHash = crypto .createHash("sha1") - .update(schedule.etag) + .update( + JSON.stringify(schedule.groups, null, 0) + schedule.etag, + ) .digest("hex"); + if ( + this.scheduleUpdatedAt.valueOf() === 0 || + this.cacheHash !== oldHash + ) + this.scheduleUpdatedAt = new Date(); + return schedule; }); } diff --git a/src/utility/validation/class-validator.interceptor.ts b/src/utility/validation/class-validator.interceptor.ts index d948d27..849747e 100644 --- a/src/utility/validation/class-validator.interceptor.ts +++ b/src/utility/validation/class-validator.interceptor.ts @@ -32,6 +32,12 @@ export class ClassValidatorInterceptor implements NestInterceptor { handler.name, ); + const isArrayOfDto = Reflect.getMetadata( + "design:result-dto-array", + cls.prototype, + handler.name, + ); + if (classDto === null) return returnValue; if (classDto === undefined) { @@ -41,41 +47,62 @@ export class ClassValidatorInterceptor implements NestInterceptor { return returnValue; } - const returnValueDto = plainToInstance( - classDto, - instanceToPlain(returnValue), - ); + const dtoArray: Array = isArrayOfDto + ? classDto + : [classDto]; - if (!(returnValueDto instanceof Object)) - throw new InternalServerErrorException( - returnValueDto, - "Return value is not object!", + for (let idx = 0; idx < dtoArray.length; idx++) { + const returnValueDto = plainToInstance( + dtoArray[idx], + instanceToPlain(returnValue), ); - const validationErrors = await validate( - returnValueDto, - this.validatorOptions, - ); + if (!(returnValueDto instanceof Object)) + throw new InternalServerErrorException( + returnValueDto, + "Return value is not object!", + ); - if (validationErrors.length > 0) { - throw new UnprocessableEntityException({ - message: validationErrors - .map((value) => Object.values(value.constraints)) - .flat(), - object: returnValue, - error: "Response Validation Failed", - statusCode: HttpStatus.UNPROCESSABLE_ENTITY, - }); + const validationErrors = await validate( + returnValueDto, + this.validatorOptions, + ); + + if (validationErrors.length > 0) { + if (idx !== dtoArray.length - 1) continue; + + throw new UnprocessableEntityException({ + message: validationErrors + .map((value) => + Object.values(value.constraints), + ) + .flat(), + object: returnValue, + error: "Response Validation Failed", + statusCode: HttpStatus.UNPROCESSABLE_ENTITY, + }); + } + return returnValue; } - return returnValue; }), ); } } // noinspection FunctionNamingConventionJS -export function ResultDto(type: any) { +export function ResultDto(dtoType: any) { return (target: NonNullable, propertyKey: string | symbol) => { - Reflect.defineMetadata("design:result-dto", type, target, propertyKey); + Reflect.defineMetadata( + "design:result-dto", + dtoType, + target, + propertyKey, + ); + Reflect.defineMetadata( + "design:result-dto-array", + dtoType !== null && dtoType.constructor === Array, + target, + propertyKey, + ); }; } diff --git a/src/version/client-version.decorator.ts b/src/version/client-version.decorator.ts new file mode 100644 index 0000000..6162bdc --- /dev/null +++ b/src/version/client-version.decorator.ts @@ -0,0 +1,13 @@ +import { createParamDecorator, ExecutionContext } from "@nestjs/common"; + +export const ClientVersion = createParamDecorator( + (_, context: ExecutionContext) => { + const sourceVersion: string | null = context.switchToHttp().getRequest() + .headers.version; + const parsedVersion = Number.parseInt(sourceVersion); + + if (Number.isNaN(parsedVersion) || parsedVersion < 0) return 0; + + return parsedVersion; + }, +);