mirror of
https://github.com/n08i40k/schedule-parser-next.git
synced 2025-12-06 09:47:46 +03:00
1.1.1
Фикс невозможности парсинга субботы. class-validator.interceptor.ts - Добавлена возможность возвращать клиенту любой DTO из списка. Добавлен разный ответ клиенту в зависимости от его версии.
This commit is contained in:
4
package-lock.json
generated
4
package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -107,11 +107,16 @@ export class ScheduleParser {
|
|||||||
++row;
|
++row;
|
||||||
}
|
}
|
||||||
|
|
||||||
const dayMonthIdx = /[А-Яа-я]+\s(\d+)\.\d+\.\d+/.exec(
|
if (
|
||||||
trimAll(dayName),
|
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({
|
days.push({
|
||||||
row: row,
|
row: row,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,41 +47,62 @@ export class ClassValidatorInterceptor implements NestInterceptor {
|
|||||||
return returnValue;
|
return returnValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const returnValueDto = plainToInstance(
|
const dtoArray: Array<any> = isArrayOfDto
|
||||||
classDto,
|
? classDto
|
||||||
instanceToPlain(returnValue),
|
: [classDto];
|
||||||
);
|
|
||||||
|
|
||||||
if (!(returnValueDto instanceof Object))
|
for (let idx = 0; idx < dtoArray.length; idx++) {
|
||||||
throw new InternalServerErrorException(
|
const returnValueDto = plainToInstance(
|
||||||
returnValueDto,
|
dtoArray[idx],
|
||||||
"Return value is not object!",
|
instanceToPlain(returnValue),
|
||||||
);
|
);
|
||||||
|
|
||||||
const validationErrors = await validate(
|
if (!(returnValueDto instanceof Object))
|
||||||
returnValueDto,
|
throw new InternalServerErrorException(
|
||||||
this.validatorOptions,
|
returnValueDto,
|
||||||
);
|
"Return value is not object!",
|
||||||
|
);
|
||||||
|
|
||||||
if (validationErrors.length > 0) {
|
const validationErrors = await validate(
|
||||||
throw new UnprocessableEntityException({
|
returnValueDto,
|
||||||
message: validationErrors
|
this.validatorOptions,
|
||||||
.map((value) => Object.values(value.constraints))
|
);
|
||||||
.flat(),
|
|
||||||
object: returnValue,
|
if (validationErrors.length > 0) {
|
||||||
error: "Response Validation Failed",
|
if (idx !== dtoArray.length - 1) continue;
|
||||||
statusCode: HttpStatus.UNPROCESSABLE_ENTITY,
|
|
||||||
});
|
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
|
// 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,
|
||||||
|
);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
13
src/version/client-version.decorator.ts
Normal file
13
src/version/client-version.decorator.ts
Normal 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;
|
||||||
|
},
|
||||||
|
);
|
||||||
Reference in New Issue
Block a user