From 4de721dfdf292596cca46e89d2536c26fde5853f Mon Sep 17 00:00:00 2001 From: Webster Dave Bontilao Date: Tue, 17 Sep 2024 16:11:35 +0800 Subject: [PATCH 1/2] Updated routes endpoint name to follow api standards --- .env.template | 5 + .gitignore | 6 +- package-lock.json | 96 +++++++++++++++++-- package.json | 3 + src/app.module.ts | 8 +- src/route/route.controller.ts | 15 --- src/route/route.module.ts | 7 -- .../routes.controller.spec.ts} | 10 +- src/routes/routes.controller.ts | 4 + src/routes/routes.module.ts | 7 ++ .../routes.service.spec.ts} | 10 +- .../routes.service.ts} | 4 +- 12 files changed, 126 insertions(+), 49 deletions(-) create mode 100644 .env.template delete mode 100644 src/route/route.controller.ts delete mode 100644 src/route/route.module.ts rename src/{route/route.controller.spec.ts => routes/routes.controller.spec.ts} (52%) create mode 100644 src/routes/routes.controller.ts create mode 100644 src/routes/routes.module.ts rename src/{route/route.service.spec.ts => routes/routes.service.spec.ts} (55%) rename src/{route/route.service.ts => routes/routes.service.ts} (64%) diff --git a/.env.template b/.env.template new file mode 100644 index 0000000..2009b31 --- /dev/null +++ b/.env.template @@ -0,0 +1,5 @@ +# ============================= +# OTP API +# ============================= + +OTP_API_URL=http://localhost:8080 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 4b56acf..190be4d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ # compiled output -/dist -/node_modules -/build +dist/ +node_modules/ +build/ # Logs logs diff --git a/package-lock.json b/package-lock.json index 0aa98a1..b211e77 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,9 +9,12 @@ "version": "0.0.1", "license": "UNLICENSED", "dependencies": { + "@nestjs/axios": "^3.0.3", "@nestjs/common": "^10.0.0", + "@nestjs/config": "^3.2.3", "@nestjs/core": "^10.0.0", "@nestjs/platform-express": "^10.0.0", + "axios": "^1.7.7", "express": "^4.19.2", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1" @@ -1562,6 +1565,17 @@ "integrity": "sha512-HZpPoABogPvjeJOdzCOSJsXeL/SMCBgBZMVC3X3d7YYp2gf31MfxhUoYUNwf1ERPJOnQc0wkFn9trqI6ZEdZuA==", "dev": true }, + "node_modules/@nestjs/axios": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-3.0.3.tgz", + "integrity": "sha512-h6TCn3yJwD6OKqqqfmtRS5Zo4E46Ip2n+gK1sqwzNBC+qxQ9xpCu+ODVRFur6V3alHSCSBxb3nNtt73VEdluyA==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0", + "axios": "^1.3.1", + "rxjs": "^6.0.0 || ^7.0.0" + } + }, "node_modules/@nestjs/cli": { "version": "10.4.5", "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-10.4.5.tgz", @@ -1648,6 +1662,21 @@ } } }, + "node_modules/@nestjs/config": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-3.2.3.tgz", + "integrity": "sha512-p6yv/CvoBewJ72mBq4NXgOAi2rSQNWx3a+IMJLVKS2uiwFCOQQuiIatGwq6MRjXV3Jr+B41iUO8FIf4xBrZ4/w==", + "license": "MIT", + "dependencies": { + "dotenv": "16.4.5", + "dotenv-expand": "10.0.0", + "lodash": "4.17.21" + }, + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", + "rxjs": "^7.1.0" + } + }, "node_modules/@nestjs/core": { "version": "10.4.1", "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.4.1.tgz", @@ -2737,8 +2766,18 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } }, "node_modules/babel-jest": { "version": "29.7.0", @@ -3344,7 +3383,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -3617,7 +3655,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, "engines": { "node": ">=0.4.0" } @@ -3688,6 +3725,27 @@ "node": ">=6.0.0" } }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-10.0.0.tgz", + "integrity": "sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -4414,6 +4472,26 @@ "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", "dev": true }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/foreground-child": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", @@ -4484,7 +4562,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dev": true, "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -6043,8 +6120,7 @@ "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "node_modules/lodash.memoize": { "version": "4.1.2", @@ -6843,6 +6919,12 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", diff --git a/package.json b/package.json index 8630559..a1dac7b 100644 --- a/package.json +++ b/package.json @@ -20,9 +20,12 @@ "test:e2e": "jest --config ./test/jest-e2e.json" }, "dependencies": { + "@nestjs/axios": "^3.0.3", "@nestjs/common": "^10.0.0", + "@nestjs/config": "^3.2.3", "@nestjs/core": "^10.0.0", "@nestjs/platform-express": "^10.0.0", + "axios": "^1.7.7", "express": "^4.19.2", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1" diff --git a/src/app.module.ts b/src/app.module.ts index 71586fa..a940fb6 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,12 +1,12 @@ import { Module } from '@nestjs/common'; -import { RouteController } from './route/route.controller'; -import { RouteModule } from './route/route.module'; import { OtpController } from './otp/otp.controller'; import { OtpModule } from './otp/otp.module'; +import { RoutesController } from './routes/routes.controller'; +import { RoutesModule } from './routes/routes.module'; @Module({ - imports: [RouteModule, OtpModule], - controllers: [RouteController, OtpController], + imports: [OtpModule, RoutesModule], + controllers: [OtpController, RoutesController], providers: [], }) export class AppModule {} diff --git a/src/route/route.controller.ts b/src/route/route.controller.ts deleted file mode 100644 index 83c1abc..0000000 --- a/src/route/route.controller.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Controller, Get, Req } from '@nestjs/common'; -import { ApiTags } from '@nestjs/swagger'; -import { response } from 'express'; - -@Controller('route') -@ApiTags('route') -export class RouteController { - - @Get() - getRoute(@Req() request: Request): object { - return response.status(200).send({ - message: request - }); - } -} diff --git a/src/route/route.module.ts b/src/route/route.module.ts deleted file mode 100644 index 8229fe4..0000000 --- a/src/route/route.module.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Module } from '@nestjs/common'; -import { RouteService } from './route.service'; - -@Module({ - providers: [RouteService] -}) -export class RouteModule {} diff --git a/src/route/route.controller.spec.ts b/src/routes/routes.controller.spec.ts similarity index 52% rename from src/route/route.controller.spec.ts rename to src/routes/routes.controller.spec.ts index 9f5d4ab..48daf16 100644 --- a/src/route/route.controller.spec.ts +++ b/src/routes/routes.controller.spec.ts @@ -1,15 +1,15 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { RouteController } from './route.controller'; +import { RoutesController } from './routes.controller'; -describe('RouteController', () => { - let controller: RouteController; +describe('RoutesController', () => { + let controller: RoutesController; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - controllers: [RouteController], + controllers: [RoutesController], }).compile(); - controller = module.get(RouteController); + controller = module.get(RoutesController); }); it('should be defined', () => { diff --git a/src/routes/routes.controller.ts b/src/routes/routes.controller.ts new file mode 100644 index 0000000..2e33933 --- /dev/null +++ b/src/routes/routes.controller.ts @@ -0,0 +1,4 @@ +import { Controller } from '@nestjs/common'; + +@Controller('routes') +export class RoutesController {} diff --git a/src/routes/routes.module.ts b/src/routes/routes.module.ts new file mode 100644 index 0000000..b5a9db2 --- /dev/null +++ b/src/routes/routes.module.ts @@ -0,0 +1,7 @@ +import { Module } from '@nestjs/common'; +import { RoutesService } from './routes.service'; + +@Module({ + providers: [RoutesService] +}) +export class RoutesModule {} diff --git a/src/route/route.service.spec.ts b/src/routes/routes.service.spec.ts similarity index 55% rename from src/route/route.service.spec.ts rename to src/routes/routes.service.spec.ts index 92cb123..e346fcf 100644 --- a/src/route/route.service.spec.ts +++ b/src/routes/routes.service.spec.ts @@ -1,15 +1,15 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { RouteService } from './route.service'; +import { RoutesService } from './routes.service'; -describe('RouteService', () => { - let service: RouteService; +describe('RoutesService', () => { + let service: RoutesService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [RouteService], + providers: [RoutesService], }).compile(); - service = module.get(RouteService); + service = module.get(RoutesService); }); it('should be defined', () => { diff --git a/src/route/route.service.ts b/src/routes/routes.service.ts similarity index 64% rename from src/route/route.service.ts rename to src/routes/routes.service.ts index 5512d47..bb32e19 100644 --- a/src/route/route.service.ts +++ b/src/routes/routes.service.ts @@ -1,6 +1,4 @@ import { Injectable } from '@nestjs/common'; @Injectable() -export class RouteService { - -} +export class RoutesService {} From 97d2f4495158f189b79d39f6f7ffc383c8f23f4e Mon Sep 17 00:00:00 2001 From: Webster Dave Bontilao Date: Tue, 17 Sep 2024 22:39:33 +0800 Subject: [PATCH 2/2] Added routes endpoint --- src/app.module.ts | 7 +- src/common/utils/request.util.ts | 16 ++++ src/routes/dto/get-routes.dto.ts | 9 +++ src/routes/interfaces/route.interface.ts | 20 +++++ src/routes/routes.controller.ts | 14 +++- src/routes/routes.module.ts | 4 + src/routes/routes.service.ts | 93 +++++++++++++++++++++++- 7 files changed, 158 insertions(+), 5 deletions(-) create mode 100644 src/common/utils/request.util.ts create mode 100644 src/routes/dto/get-routes.dto.ts create mode 100644 src/routes/interfaces/route.interface.ts diff --git a/src/app.module.ts b/src/app.module.ts index a940fb6..c87431e 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -3,10 +3,13 @@ import { OtpController } from './otp/otp.controller'; import { OtpModule } from './otp/otp.module'; import { RoutesController } from './routes/routes.controller'; import { RoutesModule } from './routes/routes.module'; +import { RoutesService } from './routes/routes.service'; +import { HttpModule } from '@nestjs/axios'; +import { ConfigModule } from '@nestjs/config'; @Module({ - imports: [OtpModule, RoutesModule], + imports: [OtpModule, RoutesModule, HttpModule, ConfigModule.forRoot()], controllers: [OtpController, RoutesController], - providers: [], + providers: [RoutesService], }) export class AppModule {} diff --git a/src/common/utils/request.util.ts b/src/common/utils/request.util.ts new file mode 100644 index 0000000..9e3deff --- /dev/null +++ b/src/common/utils/request.util.ts @@ -0,0 +1,16 @@ +import { HttpService } from '@nestjs/axios'; +import { AxiosRequestConfig, AxiosResponse } from 'axios'; +import { firstValueFrom } from 'rxjs'; +import { map } from 'rxjs/operators'; + +export const GET = async ( + httpService: HttpService, + url: string, + config?: AxiosRequestConfig, +): Promise => { + return await firstValueFrom( + httpService + .get(url, config) + .pipe(map((response: AxiosResponse) => response.data)), + ); +}; diff --git a/src/routes/dto/get-routes.dto.ts b/src/routes/dto/get-routes.dto.ts new file mode 100644 index 0000000..c1c0b68 --- /dev/null +++ b/src/routes/dto/get-routes.dto.ts @@ -0,0 +1,9 @@ +import { Route, TripGeometry } from '../interfaces/route.interface'; + +export class GetRoutesDto { + [routeId: Route['id']]: { + Route: Route['longName']; + Outbound: TripGeometry['points']; + Inbound: TripGeometry['points']; + }; +} diff --git a/src/routes/interfaces/route.interface.ts b/src/routes/interfaces/route.interface.ts new file mode 100644 index 0000000..3aab2e0 --- /dev/null +++ b/src/routes/interfaces/route.interface.ts @@ -0,0 +1,20 @@ +export type Route = { + id: string; + shortName: string; + longName: string; + mode: string; + color: string; + agencyName: string; +} + +export type RouteTrip = { + id: string; + serviceId: string; + shapeId: string; + direction: number; +} + +export type TripGeometry = { + points: string; + length: number; +} diff --git a/src/routes/routes.controller.ts b/src/routes/routes.controller.ts index 2e33933..ef049d4 100644 --- a/src/routes/routes.controller.ts +++ b/src/routes/routes.controller.ts @@ -1,4 +1,14 @@ -import { Controller } from '@nestjs/common'; +import { Controller, Get, Param } from '@nestjs/common'; +import { RoutesService } from './routes.service'; +import { ApiTags } from '@nestjs/swagger'; @Controller('routes') -export class RoutesController {} +@ApiTags('routes') +export class RoutesController { + constructor(private readonly routesService: RoutesService) {} + + @Get() + getAllRoutes(): object { + return this.routesService.getRoutes(); + } +} diff --git a/src/routes/routes.module.ts b/src/routes/routes.module.ts index b5a9db2..ae0dba5 100644 --- a/src/routes/routes.module.ts +++ b/src/routes/routes.module.ts @@ -1,7 +1,11 @@ import { Module } from '@nestjs/common'; import { RoutesService } from './routes.service'; +import { RoutesController } from './routes.controller'; +import { HttpModule } from '@nestjs/axios'; @Module({ + imports: [HttpModule], + controllers: [RoutesController], providers: [RoutesService] }) export class RoutesModule {} diff --git a/src/routes/routes.service.ts b/src/routes/routes.service.ts index bb32e19..fd8b6ad 100644 --- a/src/routes/routes.service.ts +++ b/src/routes/routes.service.ts @@ -1,4 +1,95 @@ +import { HttpService } from '@nestjs/axios'; import { Injectable } from '@nestjs/common'; +import { Route, RouteTrip, TripGeometry } from './interfaces/route.interface'; +import { GET } from 'src/common/utils/request.util'; +import { GetRoutesDto } from './dto/get-routes.dto'; @Injectable() -export class RoutesService {} +export class RoutesService { + private OTP_URL: string | undefined; + + constructor(private readonly httpService: HttpService) { + this.OTP_URL = process.env.OTP_API_URL; + } + + async getRoutes(): Promise { + const defaultRoutes = await this.getDefaultRoutes(); + + if (defaultRoutes.length > 0) { + return ( + ( + await Promise.all( + defaultRoutes.map( + async (route) => await this.getRouteDetails(route), + ), + ) + ) + // Let's convert the GetRoutesDto[] to GetRoutesDto using reduce + .reduce((acc, curr) => ({ ...acc, ...curr }), {}) + ); + } else { + throw new Error('Unable to fetch routes'); + } + + return {}; + } + + private async getDefaultRoutes(): Promise { + return GET( + this.httpService, + `${this.OTP_URL}/otp/routers/default/index/routes`, + ); + } + + private async getRouteTrips(route: Route): Promise { + return GET( + this.httpService, + `${this.OTP_URL}/otp/routers/default/index/routes/${route.id}/trips`, + ); + } + + private async getTripGeometry(routeTrip: RouteTrip): Promise { + return GET( + this.httpService, + `${this.OTP_URL}/otp/routers/default/index/trips/${routeTrip.id}/geometry`, + ); + } + + private async getRouteDetails(route: Route): Promise { + const trips = await this.getRouteTrips(route); + const routeDto: GetRoutesDto = {}; + + if (trips.length === 0) { + throw new Error('Unable to fetch trips.'); + } + + const geometry: { + inb: TripGeometry | null; + out: TripGeometry | null; + } = ( + await Promise.all( + trips.map(async (trip) => await this.getTripGeometry(trip)), + ) + ) + // Mapping the geometry result to their corresponding trips + .reduce( + (acc, curr, index) => ({ + ...acc, + [trips[index].id.includes('out') ? 'out' : 'inb']: curr, + }), + { inb: null, out: null }, + ); + + if (Object.keys(geometry).length === 0) { + throw new Error('Unable to fetch geometry.'); + } + + routeDto[route.id] = { + Route: route.longName, + Outbound: geometry.out?.points ?? '', + Inbound: geometry.inb?.points ?? '', + }; + + return routeDto; + } +}