From f44fce2a0e8686bb58a71f4c150e6de65edcffef Mon Sep 17 00:00:00 2001 From: N08I40K Date: Thu, 20 Mar 2025 02:55:35 +0400 Subject: [PATCH] =?UTF-8?q?=D0=9F=D0=B5=D1=80=D0=B5=D1=85=D0=BE=D0=B4=20?= =?UTF-8?q?=D0=BD=D0=B0=20postgres?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- prisma/schema.prisma | 15 ++- .../firebase-admin.controller.ts | 7 +- src/firebase-admin/firebase-admin.service.ts | 63 ++++++----- .../schedule-parser/schedule-parser.ts | 21 +--- src/schedule/schedule-replacer.controller.ts | 100 ------------------ src/schedule/schedule-replacer.service.ts | 62 ----------- src/schedule/schedule.module.ts | 6 +- src/schedule/schedule.service.spec.ts0 | 49 --------- src/schedule/schedule.service.ts | 15 +-- src/users/entity/fcm-user.entity.ts | 6 ++ src/users/users.service.ts | 18 ++-- start.sh | 4 +- 12 files changed, 73 insertions(+), 293 deletions(-) delete mode 100644 src/schedule/schedule-replacer.controller.ts delete mode 100644 src/schedule/schedule-replacer.service.ts delete mode 100644 src/schedule/schedule.service.spec.ts0 diff --git a/prisma/schema.prisma b/prisma/schema.prisma index db49829..cdcebd9 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -9,29 +9,26 @@ generator client { } datasource db { - provider = "mongodb" + provider = "postgresql" url = env("DATABASE_URL") } -model ScheduleReplace { - id String @id @default(auto()) @map("_id") @db.ObjectId - etag String @unique - data Bytes -} - enum UserRole { STUDENT TEACHER ADMIN } -type FCM { +model FCM { token String topics String[] + + user User @relation(fields: [userId], references: [id]) + userId String @unique } model User { - id String @id @map("_id") @db.ObjectId + id String @id // username String @unique // diff --git a/src/firebase-admin/firebase-admin.controller.ts b/src/firebase-admin/firebase-admin.controller.ts index 6f044f7..e766ecf 100644 --- a/src/firebase-admin/firebase-admin.controller.ts +++ b/src/firebase-admin/firebase-admin.controller.ts @@ -65,9 +65,10 @@ export class FirebaseAdminController { ): Promise { if (user.fcm?.token === token) return; - const updatedUser = ( - await this.firebaseAdminService.updateToken(user, token) - ).userDto; + const updatedUser = await this.firebaseAdminService.updateToken( + user, + token, + ); await this.firebaseAdminService .subscribe(updatedUser, new Set(), true) diff --git a/src/firebase-admin/firebase-admin.service.ts b/src/firebase-admin/firebase-admin.service.ts index 3a5c60f..2a170a4 100644 --- a/src/firebase-admin/firebase-admin.service.ts +++ b/src/firebase-admin/firebase-admin.service.ts @@ -15,7 +15,6 @@ import { UsersService } from "../users/users.service"; import User from "../users/entity/user.entity"; import { TokenMessage } from "firebase-admin/lib/messaging/messaging-api"; import FCM from "../users/entity/fcm-user.entity"; -import { plainToInstance } from "class-transformer"; @Injectable() export class FirebaseAdminService implements OnModuleInit { @@ -47,39 +46,47 @@ export class FirebaseAdminService implements OnModuleInit { await this.messaging.send(message); } - private getFcmOrDefault(user: User, token: string): FCM { - if (!user.fcm) { - return plainToInstance(FCM, { - token: token, - topics: [], - } as FCM); - } + private async getFcm(user: User, token: string): Promise { + const userToFCM = (user: User) => + user.fcm ? FCM.fromObject(user.fcm) : null; - return user.fcm; + const fcm = await this.usersService + .findUnique({ + where: { id: user.id }, + include: { fcm: true }, + }) + .then(userToFCM); + + if (fcm) return FCM.fromObject(fcm); + + return await this.usersService + .update({ + where: { id: user.id }, + data: { fcm: { create: { token: token, topics: [] } } }, + include: { fcm: true }, + }) + .then(userToFCM); } - async updateToken( - user: User, - token: string, - ): Promise<{ userDto: User; isNew: boolean }> { - const isNew = user.fcm === null; - const fcm = this.getFcmOrDefault(user, token); + async updateToken(user: User, token: string): Promise { + const fcm = await this.getFcm(user, token); - if (!isNew) { - if (fcm.token === token) return { userDto: user, isNew: false }; + if (user.fcm !== null) { + if (fcm.token === token) { + user.fcm = fcm; + return user; + } for (const topic of fcm.topics) await this.messaging.subscribeToTopic(token, topic); fcm.token = token; } - return { - userDto: await this.usersService.update({ - where: { id: user.id }, - data: { fcm: fcm }, - }), - isNew: isNew, - }; + return await this.usersService.update({ + where: { id: user.id }, + data: { fcm: { update: { token: fcm.token } } }, + include: { fcm: true }, + }); } async unsubscribe(user: User, topics: Set): Promise { @@ -100,7 +107,8 @@ export class FirebaseAdminService implements OnModuleInit { return await this.usersService.update({ where: { id: user.id }, - data: { fcm: fcm }, + data: { fcm: { update: { topics: fcm.topics } } }, + include: { fcm: true }, }); } @@ -126,11 +134,10 @@ export class FirebaseAdminService implements OnModuleInit { if (newTopics.size === fcm.topics.length) return user; - fcm.topics = Array.from(newTopics); - return await this.usersService.update({ where: { id: user.id }, - data: { fcm: fcm }, + data: { fcm: { update: { topics: Array.from(newTopics) } } }, + include: { fcm: true }, }); } diff --git a/src/schedule/internal/schedule-parser/schedule-parser.ts b/src/schedule/internal/schedule-parser/schedule-parser.ts index 6aea14d..b4d598d 100644 --- a/src/schedule/internal/schedule-parser/schedule-parser.ts +++ b/src/schedule/internal/schedule-parser/schedule-parser.ts @@ -12,7 +12,6 @@ import Lesson from "../../entities/lesson.entity"; import Day from "../../entities/day.entity"; import Group from "../../entities/group.entity"; import * as assert from "node:assert"; -import { ScheduleReplacerService } from "../../schedule-replacer.service"; import Teacher from "../../entities/teacher.entity"; import TeacherDay from "../../entities/teacher-day.entity"; import TeacherLesson from "../../entities/teacher-lesson.entity"; @@ -23,8 +22,8 @@ import { IsString, ValidateNested, } from "class-validator"; -import { ToMap } from "create-map-transform-fn"; import { ClassProperties } from "../../../utility/class-trasformer/class-transformer-ctor"; +import { ToMap } from "create-map-transform-fn"; type InternalId = { /** @@ -132,11 +131,9 @@ export class ScheduleParser { /** * @param xlsDownloader - класс для загрузки расписания с сайта политехникума - * @param scheduleReplacerService - сервис для подмены расписания */ public constructor( private readonly xlsDownloader: XlsDownloaderInterface, - private readonly scheduleReplacerService?: ScheduleReplacerService, ) {} /** @@ -437,20 +434,10 @@ export class ScheduleParser { assert(headData.type === "success"); - const replacer = this.scheduleReplacerService - ? await this.scheduleReplacerService.getByEtag(headData.etag) - : null; - - if (this.lastResult && this.lastResult.etag === headData.etag) { - if (!replacer) return this.lastResult; - - if (this.lastResult.replacerId === replacer.id) - return this.lastResult; - } + if (this.lastResult && this.lastResult.etag === headData.etag) + return this.lastResult; const buffer = async () => { - if (replacer) return replacer.data; - const downloadData = await this.xlsDownloader.fetch(false); this.xlsDownloader.verifyFetchResult(downloadData); @@ -605,7 +592,7 @@ export class ScheduleParser { uploadedAt: headData.uploadedAt, etag: headData.etag, - replacerId: replacer?.id, + replacerId: null, groups: groups, teachers: teachers, diff --git a/src/schedule/schedule-replacer.controller.ts b/src/schedule/schedule-replacer.controller.ts deleted file mode 100644 index 0f520e9..0000000 --- a/src/schedule/schedule-replacer.controller.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { - BadRequestException, - Controller, - Get, - HttpCode, - HttpStatus, - Post, - UploadedFile, - UseGuards, - UseInterceptors, -} from "@nestjs/common"; -import { AuthGuard } from "src/auth/auth.guard"; -import { AuthRoles } from "../auth/auth-role.decorator"; -import { ScheduleReplacerService } from "./schedule-replacer.service"; -import { FileInterceptor } from "@nestjs/platform-express"; -import { - ApiBearerAuth, - ApiOperation, - ApiResponse, - ApiTags, -} from "@nestjs/swagger"; -import { ResultDto } from "src/utility/validation/class-validator.interceptor"; -import { plainToInstance } from "class-transformer"; -import { ScheduleService } from "./schedule.service"; -import UserRole from "../users/user-role.enum"; -import ReplacerDto from "./dto/replacer.dto"; -import ClearReplacerDto from "./dto/clear-replacer.dto"; - -@ApiTags("v1/schedule-replacer") -@ApiBearerAuth() -@Controller({ path: "schedule-replacer", version: "1" }) -@UseGuards(AuthGuard) -export class ScheduleReplacerController { - constructor( - private readonly scheduleService: ScheduleService, - private readonly scheduleReplaceService: ScheduleReplacerService, - ) {} - - @ApiOperation({ description: "Замена текущего расписание на новое" }) - @ApiResponse({ - status: HttpStatus.OK, - description: "Замена прошла успешно", - }) - @Post("set") - @HttpCode(HttpStatus.OK) - @AuthRoles([UserRole.ADMIN]) - @ResultDto(null) - @UseInterceptors( - FileInterceptor("file", { limits: { fileSize: 1024 * 1024 } }), - ) - async setSchedule( - @UploadedFile() file: Express.Multer.File, - ): Promise { - if (!file) throw new BadRequestException("Файл отсутствует"); - if (file.mimetype !== "application/vnd.ms-excel") - throw new BadRequestException("Некорректный тип файла"); - - const etag = (await this.scheduleService.getSourceSchedule()).etag; - await this.scheduleReplaceService.setByEtag(etag, file.buffer); - await this.scheduleService.refreshCache(); - } - - @ApiOperation({ description: "Получение списка заменителей расписания" }) - @ApiResponse({ - status: HttpStatus.OK, - description: "Список получен успешно", - }) - @Get("get") - @HttpCode(HttpStatus.OK) - @AuthRoles([UserRole.ADMIN]) - @ResultDto(null) // TODO: Как нибудь сделать проверку в таких случаях - async getReplacers(): Promise { - return await this.scheduleReplaceService.getAll().then((result) => { - return result.map((replacer) => { - return plainToInstance(ReplacerDto, { - etag: replacer.etag, - size: replacer.data.byteLength, - } as ReplacerDto); - }); - }); - } - - @ApiOperation({ description: "Удаление всех замен расписаний" }) - @ApiResponse({ - status: HttpStatus.OK, - description: "Отчистка прошла успешно", - type: ClearReplacerDto, - }) - @Post("clear") - @HttpCode(HttpStatus.OK) - @AuthRoles([UserRole.ADMIN]) - @ResultDto(ClearReplacerDto) - async clear(): Promise { - const response = { count: await this.scheduleReplaceService.clear() }; - - await this.scheduleService.refreshCache(); - - return response; - } -} diff --git a/src/schedule/schedule-replacer.service.ts b/src/schedule/schedule-replacer.service.ts deleted file mode 100644 index fe7340a..0000000 --- a/src/schedule/schedule-replacer.service.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { Injectable } from "@nestjs/common"; -import { PrismaService } from "../prisma/prisma.service"; -import SetScheduleReplacerDto from "./dto/set-schedule-replacer.dto"; -import { plainToInstance } from "class-transformer"; - -@Injectable() -export class ScheduleReplacerService { - constructor(private readonly prismaService: PrismaService) {} - - async hasByEtag(etag: string): Promise { - return ( - (await this.prismaService.scheduleReplace.count({ - where: { etag: etag }, - })) > 0 - ); - } - - async getByEtag(etag: string): Promise { - const response = await this.prismaService.scheduleReplace.findUnique({ - where: { etag: etag }, - }); - if (response == null) return null; - - return plainToInstance(SetScheduleReplacerDto, response); - } - - async getAll(): Promise> { - const response = await this.prismaService.scheduleReplace.findMany(); - - return plainToInstance(SetScheduleReplacerDto, response); - } - - async clear(): Promise { - const count = await this.prismaService.scheduleReplace.count(); - await this.prismaService.scheduleReplace.deleteMany({}); - - return count; - } - - async setByEtag(etag: string, buffer: Buffer): Promise { - if ( - (await this.prismaService.scheduleReplace.count({ - where: { etag: etag }, - })) > 0 - ) { - await this.prismaService.scheduleReplace.update({ - where: { etag: etag }, - data: { - data: buffer, - }, - }); - return; - } - - await this.prismaService.scheduleReplace.create({ - data: { - etag: etag, - data: buffer, - }, - }); - } -} diff --git a/src/schedule/schedule.module.ts b/src/schedule/schedule.module.ts index 3d910b4..ab7440c 100644 --- a/src/schedule/schedule.module.ts +++ b/src/schedule/schedule.module.ts @@ -2,15 +2,13 @@ import { forwardRef, Module } from "@nestjs/common"; import { PrismaService } from "../prisma/prisma.service"; import { FirebaseAdminModule } from "../firebase-admin/firebase-admin.module"; import { UsersModule } from "src/users/users.module"; -import { ScheduleReplacerService } from "./schedule-replacer.service"; -import { ScheduleReplacerController } from "./schedule-replacer.controller"; import { ScheduleService } from "./schedule.service"; import { ScheduleController } from "./schedule.controller"; @Module({ imports: [forwardRef(() => UsersModule), FirebaseAdminModule], - providers: [PrismaService, ScheduleService, ScheduleReplacerService], - controllers: [ScheduleController, ScheduleReplacerController], + providers: [PrismaService, ScheduleService], + controllers: [ScheduleController], exports: [ScheduleService], }) export class ScheduleModule {} diff --git a/src/schedule/schedule.service.spec.ts0 b/src/schedule/schedule.service.spec.ts0 deleted file mode 100644 index 07f8e74..0000000 --- a/src/schedule/schedule.service.spec.ts0 +++ /dev/null @@ -1,49 +0,0 @@ -import { Test, TestingModule } from "@nestjs/testing"; -import { V1ScheduleService } from "./schedule.service"; -import * as fs from "node:fs"; -import { CacheModule } from "@nestjs/cache-manager"; -import { FirebaseAdminService } from "../firebase-admin/firebase-admin.service"; -import { UsersService } from "../users/users.service"; -import { PrismaService } from "../prisma/prisma.service"; -import { ScheduleReplacerService } from "./schedule-replacer.service"; - -describe("V1ScheduleService", () => { - let service: V1ScheduleService; - - beforeEach(async () => { - return; - - const module: TestingModule = await Test.createTestingModule({ - imports: [CacheModule.register()], - providers: [ - V1ScheduleService, - CacheModule, - FirebaseAdminService, - UsersService, - PrismaService, - ScheduleReplacerService, - ], - }).compile(); - - service = module.get(V1ScheduleService); - }); - - describe("get group schedule", () => { - it("should return group schedule", async () => { - return; - - const mainPage = fs.readFileSync("./test/mainPage").toString(); - await service.updateSiteMainPage({ mainPage: mainPage }); - - const groupName = "ИС-214/23"; - - const schedule = await service.getGroup(groupName); - expect(schedule.group.name).toBe(groupName); - - console.log(schedule.group.days[2].lessons[0].teacherNames); - expect(schedule.group.days[2].lessons[0].teacherNames.length).toBe( - 2, - ); - }); - }); -}); diff --git a/src/schedule/schedule.service.ts b/src/schedule/schedule.service.ts index b2281c7..df3bf5a 100644 --- a/src/schedule/schedule.service.ts +++ b/src/schedule/schedule.service.ts @@ -2,7 +2,6 @@ import { Inject, Injectable, NotFoundException } from "@nestjs/common"; import { BasicXlsDownloader } from "./internal/xls-downloader/basic-xls-downloader"; import { Cache, CACHE_MANAGER } from "@nestjs/cache-manager"; import { plainToInstance } from "class-transformer"; -import { ScheduleReplacerService } from "./schedule-replacer.service"; import { FirebaseAdminService } from "../firebase-admin/firebase-admin.service"; import { scheduleConstants } from "../contants"; import { @@ -32,12 +31,10 @@ export class ScheduleService { /** * Конструктор сервиса * @param cacheManager Менеджер кэша - * @param scheduleReplacerService Сервис замены расписания * @param firebaseAdminService Сервис работы с Firebase */ constructor( @Inject(CACHE_MANAGER) private readonly cacheManager: Cache, - private readonly scheduleReplacerService: ScheduleReplacerService, private readonly firebaseAdminService: FirebaseAdminService, ) { setInterval(() => { @@ -57,10 +54,7 @@ export class ScheduleService { .then(); }, 60000); - this.scheduleParser = new ScheduleParser( - new BasicXlsDownloader(), - this.scheduleReplacerService, - ); + this.scheduleParser = new ScheduleParser(new BasicXlsDownloader()); } /** @@ -92,18 +86,15 @@ export class ScheduleService { if (this.cacheHash !== oldHash) { if (this.scheduleUpdatedAt.valueOf() !== 0) { - const isReplaced = await this.scheduleReplacerService.hasByEtag( - schedule.etag, - ); - await this.firebaseAdminService.sendByTopic("common", { data: { type: "schedule-update", - replaced: isReplaced.toString(), + replaced: "false", etag: schedule.etag, }, }); } + this.scheduleUpdatedAt = new Date(); } diff --git a/src/users/entity/fcm-user.entity.ts b/src/users/entity/fcm-user.entity.ts index c8283a5..4387138 100644 --- a/src/users/entity/fcm-user.entity.ts +++ b/src/users/entity/fcm-user.entity.ts @@ -1,4 +1,6 @@ import { IsArray, IsString, ValidateNested } from "class-validator"; +import { plainToInstance } from "class-transformer"; +import { ClassProperties } from "../../utility/class-trasformer/class-transformer-ctor"; // noinspection JSClassNamingConvention export default class FCM { @@ -17,4 +19,8 @@ export default class FCM { @ValidateNested({ each: true }) @IsString() topics: Array; + + static fromObject(object: ClassProperties): FCM { + return plainToInstance(FCM, object); + } } diff --git a/src/users/users.service.ts b/src/users/users.service.ts index 617527d..627ddeb 100644 --- a/src/users/users.service.ts +++ b/src/users/users.service.ts @@ -7,9 +7,16 @@ import User from "./entity/user.entity"; export class UsersService { constructor(private readonly prismaService: PrismaService) {} - async findUnique(where: Prisma.UserWhereUniqueInput): Promise { + async findUnique(where: Prisma.UserWhereUniqueInput): Promise; + async findUnique(args: Prisma.UserFindUniqueArgs): Promise; + + async findUnique( + args: Prisma.UserWhereUniqueInput | Prisma.UserFindUniqueArgs, + ): Promise { return User.fromPlain( - await this.prismaService.user.findUnique({ where: where }), + await this.prismaService.user.findUnique( + "where" in args ? args : { where: args }, + ), ); } @@ -19,11 +26,8 @@ export class UsersService { ); } - async update(params: { - where: Prisma.UserWhereUniqueInput; - data: Prisma.UserUpdateInput; - }): Promise { - return User.fromPlain(await this.prismaService.user.update(params)); + async update(args: Prisma.UserUpdateArgs): Promise { + return User.fromPlain(await this.prismaService.user.update(args)); } async create(data: Prisma.UserCreateInput): Promise { diff --git a/start.sh b/start.sh index e52a1a1..1da3d4d 100644 --- a/start.sh +++ b/start.sh @@ -1,11 +1,11 @@ #!/bin/bash -DATABASE_URL="mongodb://$(cat "$MONGO_PROJECTDB_USERNAME_FILE"):$(cat "$MONGO_PROJECTDB_PASSWORD_FILE")@$MONGO_DOMAIN:$MONGO_PORT/$MONGO_INITDB_DATABASE?authMechanism=SCRAM-SHA-1" +DATABASE_URL="postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@$POSTGRES_HOST:$POSTGRES_PORT/$POSTGRES_DB" export DATABASE_URL JWT_SECRET=$(cat "$JWT_TOKEN_FILE") export JWT_SECRET -npx prisma db push +npx prisma db push --skip-generate exec "$@" \ No newline at end of file