From 0b2aa33b121629a939d105a3af5739e6d284d096 Mon Sep 17 00:00:00 2001 From: Krystof Date: Thu, 5 Dec 2024 19:40:51 +0100 Subject: [PATCH 1/4] chore(be): csv and zip deps --- apps/backend/package.json | 3 ++ pnpm-lock.yaml | 91 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+) diff --git a/apps/backend/package.json b/apps/backend/package.json index 289c23e7..99f429eb 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -25,6 +25,7 @@ }, "dependencies": { "@apollo/server": "^4.11.2", + "@fast-csv/parse": "^5.0.2", "@nestjs/apollo": "^12.2.1", "@nestjs/cache-manager": "^2.3.0", "@nestjs/common": "^10.4.13", @@ -41,6 +42,7 @@ "radash": "^12.1.0", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", + "unzipper": "^0.12.3", "zod": "^3.23.8" }, "devDependencies": { @@ -51,6 +53,7 @@ "@types/jest": "^29.5.14", "@types/node": "^20.17.9", "@types/supertest": "^6.0.2", + "@types/unzipper": "^0.10.10", "@typescript-eslint/eslint-plugin": "^6.21.0", "@typescript-eslint/parser": "^6.21.0", "eslint": "^8.57.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c9c4af59..05399fa4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,9 @@ importers: '@apollo/server': specifier: ^4.11.2 version: 4.11.2(graphql@16.9.0) + '@fast-csv/parse': + specifier: ^5.0.2 + version: 5.0.2 '@nestjs/apollo': specifier: ^12.2.1 version: 12.2.1(@apollo/server@4.11.2(graphql@16.9.0))(@nestjs/common@10.4.13(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.13(@nestjs/common@10.4.13(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.13)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/graphql@12.2.1(@nestjs/common@10.4.13(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.13(@nestjs/common@10.4.13(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.13)(reflect-metadata@0.2.2)(rxjs@7.8.1))(graphql@16.9.0)(reflect-metadata@0.2.2))(graphql@16.9.0) @@ -65,6 +68,9 @@ importers: rxjs: specifier: ^7.8.1 version: 7.8.1 + unzipper: + specifier: ^0.12.3 + version: 0.12.3 zod: specifier: ^3.23.8 version: 3.23.8 @@ -97,6 +103,9 @@ importers: '@types/supertest': specifier: ^6.0.2 version: 6.0.2 + '@types/unzipper': + specifier: ^0.10.10 + version: 0.10.10 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1)(typescript@5.7.2) @@ -533,6 +542,9 @@ packages: resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@fast-csv/parse@5.0.2': + resolution: {integrity: sha512-gMu1Btmm99TP+wc0tZnlH30E/F1Gw1Tah3oMDBHNPe9W8S68ixVHjt89Wg5lh7d9RuQMtwN+sGl5kxR891+fzw==} + '@graphql-tools/merge@8.4.2': resolution: {integrity: sha512-XbrHAaj8yDuINph+sAfuq3QCZ/tKblrTLOpirK0+CAgNlZUCHs0Fa+xtMUURgwCVThLle1AF7svJCxFizygLsw==} peerDependencies: @@ -1465,6 +1477,9 @@ packages: '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@types/unzipper@0.10.10': + resolution: {integrity: sha512-jKJdNxhmCHTZsaKW5x0qjn6rB+gHk0w5VFbEKsw84i+RJqXZyfTmGnpjDcKqzMpjz7VVLsUBMtO5T3mVidpt0g==} + '@types/yargs-parser@21.0.3': resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} @@ -1835,6 +1850,9 @@ packages: bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + bluebird@3.7.2: + resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} + body-parser@1.20.3: resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} @@ -2454,6 +2472,9 @@ packages: resolution: {integrity: sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==} engines: {node: '>=4'} + duplexer2@0.1.4: + resolution: {integrity: sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==} + eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} @@ -2885,6 +2906,10 @@ packages: resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} engines: {node: '>=12'} + fs-extra@11.2.0: + resolution: {integrity: sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==} + engines: {node: '>=14.14'} + fs-monkey@1.0.6: resolution: {integrity: sha512-b1FMfwetIKymC0eioW7mTywihSQE4oLzQn1dB6rZB5fx/3NpNEdAWeCSMB+60/AeT0TCXsxzAlcYVEFCTAksWg==} @@ -3708,9 +3733,24 @@ packages: lodash.clonedeep@4.5.0: resolution: {integrity: sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==} + lodash.escaperegexp@4.1.2: + resolution: {integrity: sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==} + lodash.get@4.4.2: resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==} + lodash.groupby@4.6.0: + resolution: {integrity: sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==} + + lodash.isfunction@3.0.9: + resolution: {integrity: sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==} + + lodash.isnil@4.0.0: + resolution: {integrity: sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng==} + + lodash.isundefined@3.0.1: + resolution: {integrity: sha512-MXB1is3s899/cD8jheYYE2V9qTHwKvt+npCwpD+1Sxm3Q3cECXCiYHjeHWXNwr6Q0SOBPrYUDxendrO6goVTEA==} + lodash.memoize@4.1.2: resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} @@ -3723,6 +3763,9 @@ packages: lodash.sortby@4.7.0: resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} + lodash.uniq@4.5.0: + resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==} + lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} @@ -5286,6 +5329,9 @@ packages: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} + unzipper@0.12.3: + resolution: {integrity: sha512-PZ8hTS+AqcGxsaQntl3IRBw65QrBI6lxzqDEL7IAo/XCEqRTKGfOX56Vea5TH9SZczRVxuzk1re04z/YjuYCJA==} + update-browserslist-db@1.1.1: resolution: {integrity: sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==} hasBin: true @@ -5915,6 +5961,15 @@ snapshots: '@eslint/js@8.57.1': {} + '@fast-csv/parse@5.0.2': + dependencies: + lodash.escaperegexp: 4.1.2 + lodash.groupby: 4.6.0 + lodash.isfunction: 3.0.9 + lodash.isnil: 4.0.0 + lodash.isundefined: 3.0.1 + lodash.uniq: 4.5.0 + '@graphql-tools/merge@8.4.2(graphql@16.9.0)': dependencies: '@graphql-tools/utils': 9.2.1(graphql@16.9.0) @@ -6917,6 +6972,10 @@ snapshots: '@types/unist@3.0.3': {} + '@types/unzipper@0.10.10': + dependencies: + '@types/node': 20.17.9 + '@types/yargs-parser@21.0.3': {} '@types/yargs@17.0.33': @@ -7381,6 +7440,8 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 + bluebird@3.7.2: {} + body-parser@1.20.3: dependencies: bytes: 3.1.2 @@ -8005,6 +8066,10 @@ snapshots: dset@3.1.4: {} + duplexer2@0.1.4: + dependencies: + readable-stream: 2.3.8 + eastasianwidth@0.2.0: {} ee-first@1.1.1: {} @@ -8708,6 +8773,12 @@ snapshots: jsonfile: 6.1.0 universalify: 2.0.1 + fs-extra@11.2.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 + fs-monkey@1.0.6: {} fs.realpath@1.0.0: {} @@ -9772,8 +9843,18 @@ snapshots: lodash.clonedeep@4.5.0: {} + lodash.escaperegexp@4.1.2: {} + lodash.get@4.4.2: {} + lodash.groupby@4.6.0: {} + + lodash.isfunction@3.0.9: {} + + lodash.isnil@4.0.0: {} + + lodash.isundefined@3.0.1: {} + lodash.memoize@4.1.2: {} lodash.merge@4.6.2: {} @@ -9782,6 +9863,8 @@ snapshots: lodash.sortby@4.7.0: {} + lodash.uniq@4.5.0: {} + lodash@4.17.21: {} log-symbols@4.1.0: @@ -11804,6 +11887,14 @@ snapshots: unpipe@1.0.0: {} + unzipper@0.12.3: + dependencies: + bluebird: 3.7.2 + duplexer2: 0.1.4 + fs-extra: 11.2.0 + graceful-fs: 4.2.11 + node-int64: 0.4.0 + update-browserslist-db@1.1.1(browserslist@4.24.2): dependencies: browserslist: 4.24.2 From a9a8912a47b5bf748fd1511281c27b2b892a03e7 Mon Sep 17 00:00:00 2001 From: Krystof Date: Thu, 5 Dec 2024 19:43:21 +0100 Subject: [PATCH 2/4] feat(be): gtfs tables --- .../20241205183812_gtfs_routes/migration.sql | 33 +++++++++++++++++++ apps/backend/prisma/schema.prisma | 30 +++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 apps/backend/prisma/migrations/20241205183812_gtfs_routes/migration.sql diff --git a/apps/backend/prisma/migrations/20241205183812_gtfs_routes/migration.sql b/apps/backend/prisma/migrations/20241205183812_gtfs_routes/migration.sql new file mode 100644 index 00000000..c86881cf --- /dev/null +++ b/apps/backend/prisma/migrations/20241205183812_gtfs_routes/migration.sql @@ -0,0 +1,33 @@ +-- CreateTable +CREATE TABLE "GtfsRoute" ( + "id" TEXT NOT NULL, + "type" TEXT NOT NULL, + "shortName" TEXT NOT NULL, + "longName" TEXT, + "url" TEXT, + "color" TEXT, + "isNight" BOOLEAN, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "GtfsRoute_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "GtfsRouteStop" ( + "id" TEXT NOT NULL, + "routeId" TEXT NOT NULL, + "directionId" TEXT NOT NULL, + "stopId" TEXT NOT NULL, + "stopSequence" INTEGER NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "GtfsRouteStop_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "GtfsRouteStop_routeId_directionId_stopId_stopSequence_key" ON "GtfsRouteStop"("routeId", "directionId", "stopId", "stopSequence"); + +-- AddForeignKey +ALTER TABLE "GtfsRouteStop" ADD CONSTRAINT "GtfsRouteStop_routeId_fkey" FOREIGN KEY ("routeId") REFERENCES "GtfsRoute"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/apps/backend/prisma/schema.prisma b/apps/backend/prisma/schema.prisma index 93934949..595a2dea 100644 --- a/apps/backend/prisma/schema.prisma +++ b/apps/backend/prisma/schema.prisma @@ -71,6 +71,36 @@ model PlatformsOnRoutes { @@unique([platformId, routeId]) } +model GtfsRoute { + id String @id + type String + shortName String + longName String? + url String? + color String? + isNight Boolean? + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + GtfsRouteStop GtfsRouteStop[] +} + +model GtfsRouteStop { + id String @id @default(uuid()) + + routeId String + route GtfsRoute @relation(fields: [routeId], references: [id]) + + directionId String + stopId String + stopSequence Int + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([routeId, directionId, stopId, stopSequence]) +} + model Log { id BigInt @id @default(autoincrement()) level LogLevel From 4d562e5260d43dbe96dc5c9c691408ed4223ffb4 Mon Sep 17 00:00:00 2001 From: Krystof Date: Thu, 5 Dec 2024 19:45:27 +0100 Subject: [PATCH 3/4] feat(be): gtfs sync data --- apps/backend/src/app.module.ts | 2 + .../src/modules/gtfs/gtfs.controller.ts | 18 ++++ apps/backend/src/modules/gtfs/gtfs.module.ts | 11 +++ apps/backend/src/modules/gtfs/gtfs.service.ts | 86 +++++++++++++++++++ apps/backend/src/utils/csv.utils.ts | 14 +++ 5 files changed, 131 insertions(+) create mode 100644 apps/backend/src/modules/gtfs/gtfs.controller.ts create mode 100644 apps/backend/src/modules/gtfs/gtfs.module.ts create mode 100644 apps/backend/src/modules/gtfs/gtfs.service.ts create mode 100644 apps/backend/src/utils/csv.utils.ts diff --git a/apps/backend/src/app.module.ts b/apps/backend/src/app.module.ts index bce2de1f..f026cee3 100644 --- a/apps/backend/src/app.module.ts +++ b/apps/backend/src/app.module.ts @@ -9,6 +9,7 @@ import { cacheModuleConfig } from "src/config/cache-module.config"; import { configModuleConfig } from "src/config/config-module.config"; import { GRAPHQL_PATH } from "src/constants/api"; import { DepartureModule } from "src/modules/departure/departure.module"; +import { GtfsModule } from "src/modules/gtfs/gtfs.module"; import { ImportModule } from "src/modules/import/import.module"; import { LoggerModule } from "src/modules/logger/logger.module"; import { PlatformModule } from "src/modules/platform/platform.module"; @@ -25,6 +26,7 @@ import { StopModule } from "src/modules/stop/stop.module"; PrismaModule, LoggerModule, StatusModule, + GtfsModule, ConfigModule.forRoot(configModuleConfig), ScheduleModule.forRoot(), CacheModule.registerAsync(cacheModuleConfig), diff --git a/apps/backend/src/modules/gtfs/gtfs.controller.ts b/apps/backend/src/modules/gtfs/gtfs.controller.ts new file mode 100644 index 00000000..bc7d97e2 --- /dev/null +++ b/apps/backend/src/modules/gtfs/gtfs.controller.ts @@ -0,0 +1,18 @@ +import { Controller, OnModuleInit } from "@nestjs/common"; +import { Cron, CronExpression } from "@nestjs/schedule"; + +import { GtfsService } from "src/modules/gtfs/gtfs.service"; + +@Controller("gtfs") +export class GtfsController implements OnModuleInit { + constructor(private readonly gtfsService: GtfsService) {} + + async onModuleInit(): Promise { + await this.gtfsService.syncGtfsData(); + } + + @Cron(CronExpression.EVERY_7_HOURS) + async cronSyncStops(): Promise { + await this.gtfsService.syncGtfsData(); + } +} diff --git a/apps/backend/src/modules/gtfs/gtfs.module.ts b/apps/backend/src/modules/gtfs/gtfs.module.ts new file mode 100644 index 00000000..10892c6d --- /dev/null +++ b/apps/backend/src/modules/gtfs/gtfs.module.ts @@ -0,0 +1,11 @@ +import { Module } from "@nestjs/common"; + +import { GtfsController } from "src/modules/gtfs/gtfs.controller"; +import { GtfsService } from "src/modules/gtfs/gtfs.service"; + +@Module({ + controllers: [GtfsController], + providers: [GtfsService], + imports: [], +}) +export class GtfsModule {} diff --git a/apps/backend/src/modules/gtfs/gtfs.service.ts b/apps/backend/src/modules/gtfs/gtfs.service.ts new file mode 100644 index 00000000..422b2ee4 --- /dev/null +++ b/apps/backend/src/modules/gtfs/gtfs.service.ts @@ -0,0 +1,86 @@ +import { Injectable } from "@nestjs/common"; +import { Open as unzipperOpen } from "unzipper"; + +import { PrismaService } from "src/modules/prisma/prisma.service"; +import { parseCsvString } from "src/utils/csv.utils"; + +@Injectable() +export class GtfsService { + constructor(private readonly prisma: PrismaService) {} + + async syncGtfsData() { + const response = await fetch("https://data.pid.cz/PID_GTFS.zip"); + const arrayBuffer = await response.arrayBuffer(); + const buffer = Buffer.from(arrayBuffer); + const directory = await unzipperOpen.buffer(buffer); + + const routes = directory.files.find( + (file) => file.path === "routes.txt", + ); + if (!routes) { + console.log("routes.txt not found"); + return; + } + const routeStops = directory.files.find( + (file) => file.path === "route_stops.txt", + ); + if (!routeStops) { + console.log("route_stops.txt not found"); + return; + } + + const routesBuffer = await routes.buffer(); + const routeStopsBuffer = await routeStops.buffer(); + + type RouteRecord = { + route_id: string; + route_short_name: string; + route_long_name: string; + route_type: string; + route_color?: string | undefined; + is_night: string; + route_url?: string | undefined; + }; + // FIXME: validate with zod + const routesData = await parseCsvString( + routesBuffer.toString(), + ); + + type RouteStopRecord = { + route_id: string; + direction_id: string; + stop_id: string; + stop_sequence: string; + }; + // FIXME: validate with zod + const routeStopsData = await parseCsvString( + routeStopsBuffer.toString(), + ); + + await this.prisma.$transaction(async (transaction) => { + await transaction.gtfsRouteStop.deleteMany(); + await transaction.gtfsRoute.deleteMany(); + + await transaction.gtfsRoute.createMany({ + data: routesData.map((route) => ({ + id: route.route_id, + shortName: route.route_short_name, + longName: route.route_long_name ?? null, + type: route.route_type, + isNight: Boolean(route.is_night), + color: route.route_color ?? null, + url: route.route_url ?? null, + })), + }); + + await transaction.gtfsRouteStop.createMany({ + data: routeStopsData.map((routeStop) => ({ + routeId: routeStop.route_id, + directionId: routeStop.direction_id, + stopId: routeStop.stop_id, + stopSequence: Number(routeStop.stop_sequence), + })), + }); + }); + } +} diff --git a/apps/backend/src/utils/csv.utils.ts b/apps/backend/src/utils/csv.utils.ts new file mode 100644 index 00000000..22c29c8d --- /dev/null +++ b/apps/backend/src/utils/csv.utils.ts @@ -0,0 +1,14 @@ +import { parseString } from "@fast-csv/parse"; + +export async function parseCsvString(csvString: string): Promise { + return new Promise((resolve) => { + const rows: T[] = []; + + parseString(csvString, { headers: true }) + .on("error", (error) => console.error(error)) + .on("data", (row) => rows.push(row)) + .on("end", () => { + resolve(rows); + }); + }); +} From 4bfce2001521dd2ed84b9f15dae48e112dd7a1e0 Mon Sep 17 00:00:00 2001 From: Krystof Date: Thu, 5 Dec 2024 20:30:55 +0100 Subject: [PATCH 4/4] feat(be): route endpoint --- apps/backend/src/app.module.ts | 2 + .../src/modules/gtfs/gtfs.controller.ts | 12 +++- .../src/modules/route/route.controller.ts | 38 +++++++++++ .../backend/src/modules/route/route.module.ts | 11 ++++ .../src/modules/route/route.service.ts | 65 +++++++++++++++++++ 5 files changed, 126 insertions(+), 2 deletions(-) create mode 100644 apps/backend/src/modules/route/route.controller.ts create mode 100644 apps/backend/src/modules/route/route.module.ts create mode 100644 apps/backend/src/modules/route/route.service.ts diff --git a/apps/backend/src/app.module.ts b/apps/backend/src/app.module.ts index f026cee3..8ebc2e0a 100644 --- a/apps/backend/src/app.module.ts +++ b/apps/backend/src/app.module.ts @@ -14,6 +14,7 @@ import { ImportModule } from "src/modules/import/import.module"; import { LoggerModule } from "src/modules/logger/logger.module"; import { PlatformModule } from "src/modules/platform/platform.module"; import { PrismaModule } from "src/modules/prisma/prisma.module"; +import { RouteModule } from "src/modules/route/route.module"; import { StatusModule } from "src/modules/status/status.module"; import { StopModule } from "src/modules/stop/stop.module"; @@ -27,6 +28,7 @@ import { StopModule } from "src/modules/stop/stop.module"; LoggerModule, StatusModule, GtfsModule, + RouteModule, ConfigModule.forRoot(configModuleConfig), ScheduleModule.forRoot(), CacheModule.registerAsync(cacheModuleConfig), diff --git a/apps/backend/src/modules/gtfs/gtfs.controller.ts b/apps/backend/src/modules/gtfs/gtfs.controller.ts index bc7d97e2..803dbd4f 100644 --- a/apps/backend/src/modules/gtfs/gtfs.controller.ts +++ b/apps/backend/src/modules/gtfs/gtfs.controller.ts @@ -8,11 +8,19 @@ export class GtfsController implements OnModuleInit { constructor(private readonly gtfsService: GtfsService) {} async onModuleInit(): Promise { - await this.gtfsService.syncGtfsData(); + try { + this.gtfsService.syncGtfsData(); + } catch (error) { + console.error(error); + } } @Cron(CronExpression.EVERY_7_HOURS) async cronSyncStops(): Promise { - await this.gtfsService.syncGtfsData(); + try { + await this.gtfsService.syncGtfsData(); + } catch (error) { + console.error(error); + } } } diff --git a/apps/backend/src/modules/route/route.controller.ts b/apps/backend/src/modules/route/route.controller.ts new file mode 100644 index 00000000..df48cac5 --- /dev/null +++ b/apps/backend/src/modules/route/route.controller.ts @@ -0,0 +1,38 @@ +import { CacheInterceptor } from "@nestjs/cache-manager"; +import { + Controller, + Get, + HttpException, + HttpStatus, + Param, + UseInterceptors, + Version, +} from "@nestjs/common"; +import { ApiParam, ApiTags } from "@nestjs/swagger"; + +import { EndpointVersion } from "src/enums/endpoint-version"; +import { RouteService } from "src/modules/route/route.service"; + +@ApiTags("route") +@Controller("route") +@UseInterceptors(CacheInterceptor) +export class RouteController { + constructor(private readonly routeService: RouteService) {} + + @Get(":id") + @Version([EndpointVersion.v1]) + @ApiParam({ + name: "id", + description: "Route ID", + required: true, + example: "L991", + type: "string", + }) + async getRoute(@Param("id") id: unknown) { + if (typeof id !== "string") { + throw new HttpException("Missing route ID", HttpStatus.BAD_REQUEST); + } + + return this.routeService.getRoute(id); + } +} diff --git a/apps/backend/src/modules/route/route.module.ts b/apps/backend/src/modules/route/route.module.ts new file mode 100644 index 00000000..8f9cae0d --- /dev/null +++ b/apps/backend/src/modules/route/route.module.ts @@ -0,0 +1,11 @@ +import { Module } from "@nestjs/common"; + +import { RouteController } from "src/modules/route/route.controller"; +import { RouteService } from "src/modules/route/route.service"; + +@Module({ + controllers: [RouteController], + providers: [RouteService], + imports: [], +}) +export class RouteModule {} diff --git a/apps/backend/src/modules/route/route.service.ts b/apps/backend/src/modules/route/route.service.ts new file mode 100644 index 00000000..2d97fd29 --- /dev/null +++ b/apps/backend/src/modules/route/route.service.ts @@ -0,0 +1,65 @@ +import { Injectable } from "@nestjs/common"; +import { group, unique } from "radash"; + +import { platformSelect } from "src/modules/platform/platform.service"; +import { PrismaService } from "src/modules/prisma/prisma.service"; + +const gtfsRouteSelect = { + id: true, + shortName: true, + longName: true, + isNight: true, + color: true, + url: true, + type: true, +}; + +const gtfsRouteStopSelect = { + directionId: true, + stopId: true, + stopSequence: true, +}; + +@Injectable() +export class RouteService { + constructor(private prisma: PrismaService) {} + + async getRoute(id: string) { + const route = await this.prisma.gtfsRoute.findFirst({ + select: gtfsRouteSelect, + where: { + id, + }, + }); + + const routeStops = await this.prisma.gtfsRouteStop.findMany({ + select: gtfsRouteStopSelect, + where: { + routeId: id, + }, + orderBy: { + stopSequence: "asc", + }, + }); + + const stops = await this.prisma.platform.findMany({ + select: platformSelect, + where: { + id: { + in: unique(routeStops.map((item) => item.stopId)), + }, + }, + }); + + return { + ...route, + directions: group( + routeStops.map((routeStop) => ({ + ...routeStop, + stop: stops.find((stop) => stop.id === routeStop.stopId), + })), + (item) => item.directionId, + ), + }; + } +}