Фикс невозможности парсинга субботы.

class-validator.interceptor.ts
- Добавлена возможность возвращать клиенту любой DTO из списка.

Добавлен разный ответ клиенту в зависимости от его версии.
This commit is contained in:
2024-09-28 01:40:19 +04:00
parent 99dc3c86e7
commit d18a6764c9
8 changed files with 172 additions and 44 deletions

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "schedule-parser-next", "name": "schedule-parser-next",
"version": "1.1.0", "version": "1.1.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "schedule-parser-next", "name": "schedule-parser-next",
"version": "1.1.0", "version": "1.1.1",
"license": "UNLICENSED", "license": "UNLICENSED",
"dependencies": { "dependencies": {
"@nestjs/cache-manager": "^2.2.2", "@nestjs/cache-manager": "^2.2.2",

View File

@@ -1,6 +1,6 @@
{ {
"name": "schedule-parser-next", "name": "schedule-parser-next",
"version": "1.1.0", "version": "1.1.1",
"description": "", "description": "",
"author": "", "author": "",
"private": true, "private": true,

View File

@@ -12,7 +12,13 @@ import {
ValidateNested, ValidateNested,
} from "class-validator"; } from "class-validator";
import { ApiProperty, OmitType, PickType } from "@nestjs/swagger"; 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 { export class LessonTimeDto {
@ApiProperty({ @ApiProperty({
@@ -205,12 +211,13 @@ export class GroupDto {
} }
} }
export class CacheStatusDto { export class CacheStatusV0Dto {
@ApiProperty({ @ApiProperty({
example: true, example: true,
description: "Нужно ли обновить ссылку для скачивания xls?", description: "Нужно ли обновить ссылку для скачивания xls?",
}) })
@IsBoolean() @IsBoolean()
@Expose()
cacheUpdateRequired: boolean; cacheUpdateRequired: boolean;
@ApiProperty({ @ApiProperty({
@@ -218,9 +225,44 @@ export class CacheStatusDto {
description: "Хеш последних полученных данных", description: "Хеш последних полученных данных",
}) })
@IsHash("sha1") @IsHash("sha1")
@Expose()
cacheHash: string; 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 { export class ScheduleDto {
@ApiProperty({ @ApiProperty({
example: new Date(), example: new Date(),

View File

@@ -107,11 +107,16 @@ export class ScheduleParser {
++row; ++row;
} }
if (
days.length == 0 ||
!days[days.length - 1].name.startsWith("Суббота")
) {
const dayMonthIdx = /[А-Яа-я]+\s(\d+)\.\d+\.\d+/.exec( const dayMonthIdx = /[А-Яа-я]+\s(\d+)\.\d+\.\d+/.exec(
trimAll(dayName), trimAll(dayName),
); );
if (dayMonthIdx === null) continue; if (dayMonthIdx === null) continue;
}
days.push({ days.push({
row: row, row: row,

View File

@@ -11,6 +11,8 @@ import { AuthGuard } from "../auth/auth.guard";
import { ScheduleService } from "./schedule.service"; import { ScheduleService } from "./schedule.service";
import { import {
CacheStatusDto, CacheStatusDto,
CacheStatusV0Dto,
CacheStatusV1Dto,
GroupScheduleDto, GroupScheduleDto,
GroupScheduleRequestDto, GroupScheduleRequestDto,
ScheduleDto, ScheduleDto,
@@ -26,6 +28,7 @@ import {
ApiOperation, ApiOperation,
refs, refs,
} from "@nestjs/swagger"; } from "@nestjs/swagger";
import { ClientVersion } from "../version/client-version.decorator";
@Controller("api/v1/schedule") @Controller("api/v1/schedule")
@UseGuards(AuthGuard) @UseGuards(AuthGuard)
@@ -82,34 +85,59 @@ export class ScheduleController {
} }
@ApiExtraModels(SiteMainPageDto) @ApiExtraModels(SiteMainPageDto)
@ApiExtraModels(CacheStatusDto) @ApiExtraModels(CacheStatusV0Dto)
@ApiExtraModels(CacheStatusV1Dto)
@ApiOperation({ @ApiOperation({
summary: "Обновление данных основной страницы политехникума", summary: "Обновление данных основной страницы политехникума",
tags: ["schedule"], tags: ["schedule"],
}) })
@ApiOkResponse({ description: "Данные обновлены успешно" }) @ApiOkResponse({
description: "Данные обновлены успешно",
schema: refs(CacheStatusV0Dto)[0],
})
@ApiOkResponse({
description: "Данные обновлены успешно",
schema: refs(CacheStatusV0Dto)[1],
})
@ApiNotAcceptableResponse({ @ApiNotAcceptableResponse({
description: "Передан некорректный код страницы", description: "Передан некорректный код страницы",
}) })
@ResultDto(CacheStatusDto) @ResultDto([CacheStatusV0Dto, CacheStatusV1Dto])
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@Post("update-site-main-page") @Post("update-site-main-page")
async updateSiteMainPage( async updateSiteMainPage(
@Body() siteMainPageDto: SiteMainPageDto, @Body() siteMainPageDto: SiteMainPageDto,
): Promise<CacheStatusDto> { @ClientVersion() version: number,
return await this.scheduleService.updateSiteMainPage(siteMainPageDto); ): Promise<CacheStatusV0Dto> {
return CacheStatusDto.stripVersion(
await this.scheduleService.updateSiteMainPage(siteMainPageDto),
version,
);
} }
@ApiExtraModels(CacheStatusDto) @ApiExtraModels(CacheStatusV0Dto)
@ApiExtraModels(CacheStatusV1Dto)
@ApiOperation({ @ApiOperation({
summary: "Получение информации о кеше", summary: "Получение информации о кеше",
tags: ["schedule", "cache"], tags: ["schedule", "cache"],
}) })
@ApiOkResponse({ description: "Получение данных прошло успешно" }) @ApiOkResponse({
@ResultDto(CacheStatusDto) description: "Получение данных прошло успешно",
schema: refs(CacheStatusV0Dto)[0],
})
@ApiOkResponse({
description: "Получение данных прошло успешно",
schema: refs(CacheStatusV1Dto)[0],
})
@ResultDto([CacheStatusV0Dto, CacheStatusV1Dto])
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@Get("cache-status") @Get("cache-status")
getCacheStatus(): CacheStatusDto { getCacheStatus(
return this.scheduleService.getCacheStatus(); @ClientVersion() version: number,
): CacheStatusV0Dto | CacheStatusV1Dto {
return CacheStatusDto.stripVersion(
this.scheduleService.getCacheStatus(),
version,
);
} }
} }

View File

@@ -31,6 +31,7 @@ export class ScheduleService {
private cacheHash: string = "0000000000000000000000000000000000000000"; private cacheHash: string = "0000000000000000000000000000000000000000";
private lastChangedDays: Array<Array<number>> = []; private lastChangedDays: Array<Array<number>> = [];
private scheduleUpdatedAt: Date = new Date(0);
constructor(@Inject(CACHE_MANAGER) private readonly cacheManager: Cache) {} constructor(@Inject(CACHE_MANAGER) private readonly cacheManager: Cache) {}
@@ -39,6 +40,8 @@ export class ScheduleService {
cacheHash: this.cacheHash, cacheHash: this.cacheHash,
cacheUpdateRequired: cacheUpdateRequired:
(Date.now() - this.cacheUpdatedAt.valueOf()) / 1000 / 60 >= 5, (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<GroupDto>; ) as Array<GroupDto>;
this.cacheUpdatedAt = new Date(); this.cacheUpdatedAt = new Date();
const oldHash = this.cacheHash;
this.cacheHash = crypto this.cacheHash = crypto
.createHash("sha1") .createHash("sha1")
.update(schedule.etag) .update(
JSON.stringify(schedule.groups, null, 0) + schedule.etag,
)
.digest("hex"); .digest("hex");
if (
this.scheduleUpdatedAt.valueOf() === 0 ||
this.cacheHash !== oldHash
)
this.scheduleUpdatedAt = new Date();
return schedule; return schedule;
}); });
} }

View File

@@ -32,6 +32,12 @@ export class ClassValidatorInterceptor implements NestInterceptor {
handler.name, handler.name,
); );
const isArrayOfDto = Reflect.getMetadata(
"design:result-dto-array",
cls.prototype,
handler.name,
);
if (classDto === null) return returnValue; if (classDto === null) return returnValue;
if (classDto === undefined) { if (classDto === undefined) {
@@ -41,8 +47,13 @@ export class ClassValidatorInterceptor implements NestInterceptor {
return returnValue; return returnValue;
} }
const dtoArray: Array<any> = isArrayOfDto
? classDto
: [classDto];
for (let idx = 0; idx < dtoArray.length; idx++) {
const returnValueDto = plainToInstance( const returnValueDto = plainToInstance(
classDto, dtoArray[idx],
instanceToPlain(returnValue), instanceToPlain(returnValue),
); );
@@ -58,9 +69,13 @@ export class ClassValidatorInterceptor implements NestInterceptor {
); );
if (validationErrors.length > 0) { if (validationErrors.length > 0) {
if (idx !== dtoArray.length - 1) continue;
throw new UnprocessableEntityException({ throw new UnprocessableEntityException({
message: validationErrors message: validationErrors
.map((value) => Object.values(value.constraints)) .map((value) =>
Object.values(value.constraints),
)
.flat(), .flat(),
object: returnValue, object: returnValue,
error: "Response Validation Failed", error: "Response Validation Failed",
@@ -68,14 +83,26 @@ export class ClassValidatorInterceptor implements NestInterceptor {
}); });
} }
return returnValue; return returnValue;
}
}), }),
); );
} }
} }
// noinspection FunctionNamingConventionJS // noinspection FunctionNamingConventionJS
export function ResultDto(type: any) { export function ResultDto(dtoType: any) {
return (target: NonNullable<unknown>, propertyKey: string | symbol) => { return (target: NonNullable<unknown>, 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,
);
}; };
} }

View File

@@ -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;
},
);