From 26d82390c68e365b552483b0ee1bb04188e54853 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Tue, 21 Jan 2025 13:26:52 -0800 Subject: [PATCH 1/4] remove ApiBasedRepository.ts and tests --- src/ServerContext.ts | 1 - src/index.ts | 11 - src/repositories/ApiBasedRepository.ts | 387 ---------------- .../ApiBasedRepositoryTests.test.ts | 422 ------------------ 4 files changed, 821 deletions(-) delete mode 100644 src/repositories/ApiBasedRepository.ts delete mode 100644 test/repositories/ApiBasedRepositoryTests.test.ts diff --git a/src/ServerContext.ts b/src/ServerContext.ts index 8034915..c380583 100644 --- a/src/ServerContext.ts +++ b/src/ServerContext.ts @@ -2,5 +2,4 @@ import { GetterRepository } from "./repositories/GetterRepository"; export interface ServerContext { repository: GetterRepository; - apiBasedRepository: GetterRepository; } \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index a931d05..7080959 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,11 +2,9 @@ import { readFileSync } from "fs"; import { ApolloServer } from "@apollo/server"; import { startStandaloneServer } from "@apollo/server/standalone"; import { resolvers } from "./resolvers"; -import { loadTestData } from "./loaders/loadTestData"; import { ServerContext } from "./ServerContext"; import { UnoptimizedInMemoryRepository } from "./repositories/UnoptimizedInMemoryRepository"; import { RepositoryDataLoader } from "./loaders/RepositoryDataLoader"; -import { ApiBasedRepository } from "./repositories/ApiBasedRepository"; const typeDefs = readFileSync("./schema.graphqls", "utf8"); @@ -25,14 +23,6 @@ async function main() { ); await repositoryDataUpdater.start(); - // TODO: Migrate all logic over to this repository - const apiBasedRepository = new ApiBasedRepository(); - - const systemIds = ["263"]; - await Promise.all(systemIds.map(async (systemId) => { - await apiBasedRepository.seedCacheForSystemId(systemId); - })); - const { url } = await startStandaloneServer(server, { listen: { port: process.env.PORT ? parseInt(process.env.PORT) : 4000, @@ -40,7 +30,6 @@ async function main() { context: async ({ req, res }) => { return { repository, - apiBasedRepository, } }, }); diff --git a/src/repositories/ApiBasedRepository.ts b/src/repositories/ApiBasedRepository.ts deleted file mode 100644 index 8ad821f..0000000 --- a/src/repositories/ApiBasedRepository.ts +++ /dev/null @@ -1,387 +0,0 @@ -import { GetterRepository } from "./GetterRepository"; -import { IEta, IOrderedStop, IRoute, IShuttle, IStop, ISystem } from "../entities/entities"; - -const baseUrl = "https://passiogo.com/mapGetData.php" - -// TODO: implement TTL functionality -// TODO: add TTL values to everything -// TODO: remove RepositoryDataLoader and UnoptimizedInMemoryRepository -// TODO: make milliseconds (TTL) required on everything -// TODO: extract cache into its own class - -export interface ApiBasedRepositoryCache { - etasForShuttleId?: { - [shuttleId: string]: IEta[], - }, - etasForStopId?: { - [stopId: string]: IEta[], - }, - stopsBySystemId?: { - [systemId: string]: IStop[], - }, - stopByStopId?: { - [stopId: string]: IStop, - }, - shuttleByShuttleId?: { - [shuttleId: string]: IShuttle, - }, - shuttlesBySystemId?: { - [systemId: string]: IShuttle[], - }, - // To speed things up, implement caches for other data later -} - -export interface ApiBasedRepositoryMillisecondTTLs { - etasForShuttleId?: number, - etasForStopId?: number, - shuttleByShuttleId?: number, - shuttlesBySystemId?: number, -} - -const emptyCache: ApiBasedRepositoryCache = { - etasForShuttleId: {}, - etasForStopId: {}, - shuttleByShuttleId: {}, - shuttlesBySystemId: {}, -} - -const defaultTtls: ApiBasedRepositoryMillisecondTTLs = { - etasForShuttleId: 10000, - etasForStopId: 10000, -} - -export class ApiBasedRepository implements GetterRepository { - private cache: ApiBasedRepositoryCache; - - constructor( - initialCache: ApiBasedRepositoryCache | undefined = emptyCache, - private ttls: ApiBasedRepositoryMillisecondTTLs = defaultTtls, - ) { - this.cache = initialCache; - } - - /** - * Seed the initial data in the cache, so data can be updated - * given the correct ID. - * Alternatively, pass in an `initialCache` with existing data - * (useful for testing). - * @param systemId - */ - public async seedCacheForSystemId(systemId: string): Promise { - await this.updateShuttlesForSystemIfTTL(systemId); - await this.updateEtasForSystemIfTTL(systemId); - await this.updateStopsForSystemIdIfTTL(systemId); - } - - public async getEtaForShuttleAndStopId(shuttleId: string, stopId: string): Promise { - const shuttle = await this.getShuttleById(shuttleId); - const systemId = shuttle?.systemId; - if (!systemId) { - return null; - } - await this.updateEtasForSystemIfTTL(systemId); - - if (this.cache?.etasForStopId && this.cache.etasForStopId[stopId]) { - const etas = this.cache.etasForStopId[stopId]; - const foundEta = etas.find((eta) => eta.shuttleId === shuttleId); - if (foundEta) { - return foundEta; - } - } - - return null; - } - - public async getEtasForShuttleId(shuttleId: string): Promise { - const shuttle = await this.getShuttleById(shuttleId); - const systemId = shuttle?.systemId; - if (!systemId) { - return []; - } - await this.updateEtasForSystemIfTTL(systemId); - - if (!this.cache?.etasForShuttleId || !this.cache.etasForShuttleId[shuttleId]) { - return []; - } - - return this.cache.etasForShuttleId[shuttleId]; - } - - public async getEtasForStopId(stopId: string): Promise { - const stop = await this.getStopById(stopId); - const systemId = stop?.systemId; - if (!systemId) { - return []; - } - await this.updateEtasForSystemIfTTL(systemId); - - if (!this.cache?.etasForStopId || !this.cache.etasForStopId[stopId]) { - return []; - } - - return this.cache.etasForStopId[stopId]; - } - - public async updateEtasForSystemIfTTL(systemId: string) { - // TODO: check if TTL - try { - const stops = await this.getStopsBySystemId(systemId); - await Promise.all(stops.map(async (stop) => { - const params = { - eta: "3", - stopIds: stop.id, - }; - - const query = new URLSearchParams(params).toString(); - const response = await fetch(`${baseUrl}?${query}`, { - method: "GET", - }); - const json = await response.json(); - - if (json.ETAs && json.ETAs[stop.id]) { - if (!this.cache.etasForStopId) { - this.cache.etasForStopId = {}; - } - this.cache.etasForStopId[stop.id] = []; - - // This is technically incorrect, the entire shuttle cache - // should not be reset like this - // TODO: restore normal cache behavior - this.cache.etasForShuttleId = {}; - - // Continue with the parsing - json.ETAs[stop.id].forEach((jsonEta: any) => { - // Update cache - if (!this.cache.etasForStopId) { - this.cache.etasForStopId = {}; - } - - if (!this.cache.etasForShuttleId) { - this.cache.etasForShuttleId = {}; - } - - // TODO: create cache abstraction to deal with possibly undefined properties - - const shuttleId: string = jsonEta.busId; - if (!this.cache.etasForShuttleId[shuttleId]) { - this.cache.etasForShuttleId[shuttleId] = []; - } - - const eta: IEta = { - secondsRemaining: jsonEta.secondsSpent, - shuttleId: `${shuttleId}`, - stopId: stop.id, - millisecondsSinceEpoch: Date.now(), - }; - - this.cache.etasForStopId[stop.id].push(eta); - this.cache.etasForShuttleId[shuttleId].push(eta); - }); - } - })); - - } catch (e) { - console.error(e); - } - } - - // TODO: migrate rest of logic over to this class - public async getOrderedStopByRouteAndStopId(routeId: string, stopId: string): Promise<| null> { - return null; - } - - public async getOrderedStopsByRouteId(routeId: string): Promise<[]> { - return Promise.resolve([]); - } - - public async getOrderedStopsByStopId(stopId: string): Promise<[]> { - return Promise.resolve([]); - } - - public async getRouteById(routeId: string): Promise<| null> { - return Promise.resolve(null); - } - - public async getRoutesBySystemId(systemId: string): Promise<[]> { - return Promise.resolve([]); - } - - public async getShuttleById(shuttleId: string): Promise { - if (!this.cache.shuttleByShuttleId) return null; - let shuttle = this.cache.shuttleByShuttleId[shuttleId]; - if (!shuttle) return null; - - // Call getShuttlesBySystemId to update the data if not TTL - await this.updateShuttlesForSystemIfTTL(shuttle.systemId); - - shuttle = this.cache.shuttleByShuttleId[shuttleId]; - if (!shuttle) return null; - return shuttle; - } - - public async getShuttlesByRouteId(routeId: string): Promise<[]> { - return Promise.resolve([]); - } - - public async getShuttlesBySystemId(systemId: string): Promise { - return Promise.resolve([]); - } - - public async updateShuttlesForSystemIfTTL(systemId: string) { - // TODO: check if TTL - try { - // TODO: update shuttlesByRouteId - // Update shuttleByShuttleId, shuttlesBySystemId - const params = { - getBuses: "2" - }; - - const formDataJsonObject = { - "s0": systemId, - "sA": "1" - }; - - const formData = new FormData(); - formData.set("json", JSON.stringify(formDataJsonObject)); - - const query = new URLSearchParams(params).toString(); - const response = await fetch(`${baseUrl}?${query}`, { - method: "POST", - body: formData, - }); - const json = await response.json(); - - if (json.buses && json.buses["-1"] === undefined) { - const jsonBuses = Object.values(json.buses).map((busesArr: any) => { - return busesArr[0]; - }); - - // Store shuttles by system, with the additional side effect that - // shuttleByShuttleId is updated - const shuttles = await Promise.all(jsonBuses.map(async (jsonBus: any) => { - const constructedShuttle: IShuttle = { - name: jsonBus.bus, - coordinates: { - latitude: parseFloat(jsonBus.latitude), - longitude: parseFloat(jsonBus.longitude), - }, - routeId: jsonBus.routeId, - systemId: systemId, - id: `${jsonBus.busId}` - } - - if (this.cache.shuttleByShuttleId) { - this.cache.shuttleByShuttleId[jsonBus.busId] = constructedShuttle; - } - - return constructedShuttle; - })); - - if (this.cache.shuttlesBySystemId) { - this.cache.shuttlesBySystemId[systemId] = shuttles; - } - } else { - console.warn(`No shuttle data available for system ID ${systemId} and JSON output -${json}`); - } - } catch (e) { - console.error(e); - } - } - - public async getStopById(stopId: string): Promise { - if (!this.cache.stopByStopId) return null; - const oldStop = this.cache.stopByStopId[stopId]; - if (!oldStop) return null; - - await this.updateStopsForSystemIdIfTTL(oldStop.systemId); - - const newStop = this.cache.stopByStopId[stopId]; - if (!newStop) return null; - - return newStop; - } - - public async getStopsBySystemId(systemId: string): Promise { - await this.updateStopsForSystemIdIfTTL(systemId); - - if (!this.cache.stopsBySystemId || !this.cache.stopsBySystemId[systemId]) { - return []; - } - return this.cache.stopsBySystemId[systemId]; - } - - public async updateStopsForSystemIdIfTTL(systemId: string) { - // TODO: check if TTL - - try { - const params = { - getStops: "2", - }; - - const formDataJsonObject = { - "s0": systemId, - "sA": 1 - }; - const formData = new FormData(); - formData.set("json", JSON.stringify(formDataJsonObject)); - - const query = new URLSearchParams(params).toString(); - const response = await fetch(`${baseUrl}?${query}`, { - method: "POST", - body: formData, - }); - const json = await response.json(); - - // TODO: update polyline data - // TODO: update ordered stop data - - if (json.stops) { - const jsonStops = Object.values(json.stops); - - // TODO: restore normal cache behavior - this.cache.stopsBySystemId = {}; - this.cache.stopByStopId = {}; - - await Promise.all(jsonStops.map(async (stop: any) => { - const constructedStop: IStop = { - name: stop.name, - id: stop.id, - systemId: systemId, - coordinates: { - latitude: parseFloat(stop.latitude), - longitude: parseFloat(stop.longitude), - }, - }; - - if (!this.cache.stopsBySystemId) { - this.cache.stopsBySystemId = {}; - } - - if (!this.cache.stopsBySystemId[systemId]) { - this.cache.stopsBySystemId[systemId] = []; - } - - this.cache.stopsBySystemId[systemId].push(constructedStop); - - if (!this.cache.stopByStopId) { - this.cache.stopByStopId = {}; - } - - this.cache.stopByStopId[constructedStop.id] = constructedStop; - })); - } - } catch (e) { - console.error(e); - } - } - - public async getSystemById(systemId: string): Promise { - return null; - } - - public async getSystems(): Promise<[]> { - return Promise.resolve([]); - } - -} \ No newline at end of file diff --git a/test/repositories/ApiBasedRepositoryTests.test.ts b/test/repositories/ApiBasedRepositoryTests.test.ts deleted file mode 100644 index 3248494..0000000 --- a/test/repositories/ApiBasedRepositoryTests.test.ts +++ /dev/null @@ -1,422 +0,0 @@ -import { beforeEach, describe, expect, jest, test } from "@jest/globals"; -import { - ApiBasedRepository, - ApiBasedRepositoryCache, - ApiBasedRepositoryMillisecondTTLs -} from "../../src/repositories/ApiBasedRepository"; -import { IEta, IShuttle, IStop } from "../../src/entities/entities"; -import { genericEtaDataByStopId } from "../jsonSnapshots/genericEtaDataBySystemId"; -import { genericShuttleDataBySystemId } from "../jsonSnapshots/genericShuttleDataBySystemId"; - -/** - * Update the global fetch function to return a specific object. - * @param obj - */ -function updateGlobalFetchMockJson(obj: any) { - // @ts-ignore - global.fetch = jest.fn(() => { - return Promise.resolve({ - json: () => Promise.resolve(obj) - }) - }) as jest.Mock; -} - -/** - * Reset the global fetch function mock's JSON to return an empty object. - * @param obj - */ -function resetGlobalFetchMockJson() { - updateGlobalFetchMockJson({}) -} - -beforeEach(() => { - resetGlobalFetchMockJson(); -}) - - -describe("getEtaForShuttleAndStopId", () => { - test("getEtaForShuttleAndStopId returns correct ETA data", async () => { - updateGlobalFetchMockJson(genericEtaDataByStopId); - - const initialCache: ApiBasedRepositoryCache = { - etasForStopId: { - "177666": [ - { - secondsRemaining: 587, - shuttleId: "5577", - stopId: "177666", - millisecondsSinceEpoch: Date.now(), - }, - { - secondsRemaining: 226, - shuttleId: "9909", - stopId: "177666", - millisecondsSinceEpoch: Date.now(), - } - ], - }, - } - - const repository = new ApiBasedRepository(initialCache); - - repository.getShuttleById = jest.fn(async () => { - const shuttle: IShuttle = { - id: "5577", - name: "08", - coordinates: { - latitude: 33.7933406, - longitude: -117.8539321, - }, - routeId: "53966", - systemId: "1", - }; - return shuttle; - }); - - repository.updateEtasForSystemIfTTL = jest.fn(async () => { - }); - - const result = await repository.getEtaForShuttleAndStopId("5577", "177666"); - - expect(result?.secondsRemaining).toEqual(587); - expect(result?.millisecondsSinceEpoch).toBeDefined(); - expect(result?.shuttleId).toEqual("5577"); - expect(result?.stopId).toEqual("177666"); - }); - - test("getEtaForShuttleAndStopId returns null if API call is invalid and cache is empty", async () => { - const repository = new ApiBasedRepository(); - const result = await repository.getEtaForShuttleAndStopId("5577", "177666"); - - expect(result).toEqual(null); - }); -}); - -describe("getEtasForShuttleId", () => { - test("getEtasForShuttleId returns correct ETA data", async () => { - updateGlobalFetchMockJson(genericEtaDataByStopId); - - const initialCache: ApiBasedRepositoryCache = { - etasForStopId: {}, - etasForShuttleId: { - "5577": [ - { - secondsRemaining: 587, - shuttleId: "5577", - stopId: "177666", - millisecondsSinceEpoch: Date.now(), - } - ] - }, - }; - - const ttls: ApiBasedRepositoryMillisecondTTLs = { - etasForShuttleId: 100000, - etasForStopId: 100000, - }; - - const repository = new ApiBasedRepository(initialCache, ttls); - repository.updateEtasForSystemIfTTL = jest.fn(async () => { - }); - repository.getShuttleById = jest.fn(async () => { - const shuttle: IShuttle = { - id: "5577", - name: "08", - coordinates: { - latitude: 33.7933406, - longitude: -117.8539321, - }, - routeId: "53966", - systemId: "1", - }; - return shuttle; - }); - const result = await repository.getEtasForShuttleId("5577"); - - // @ts-ignore - expect(result).toEqual(initialCache.etasForShuttleId["5577"]); - }); - - test("getEtasForShuttleId returns empty array if no data available", async () => { - const repository = new ApiBasedRepository(); - repository.updateEtasForSystemIfTTL = jest.fn(async () => { - }); - const result = await repository.getEtasForShuttleId("5577"); - - expect(result).toEqual([]); - }); -}); - -describe("getEtasForStopId", () => { - test("getEtasForStopId returns correct ETA data", async () => { - // Because I'm testing updateEtasForSystemIfTTL separately, - // stub it out here - - updateGlobalFetchMockJson(genericEtaDataByStopId); - - const initialCache: ApiBasedRepositoryCache = { - etasForStopId: { - "177666": [ - { - secondsRemaining: 587, - shuttleId: "5577", - stopId: "177666", - millisecondsSinceEpoch: Date.now(), - } - ] - }, - etasForShuttleId: {} - }; - - const ttls: ApiBasedRepositoryMillisecondTTLs = { - etasForShuttleId: 100000, - etasForStopId: 100000, - }; - - const repository = new ApiBasedRepository(initialCache, ttls); - repository.getStopById = jest.fn(async () => { - const stop: IStop = { - name: "Chapman Court", - systemId: "1", - id: "177666", - coordinates: { - latitude: 33.796796, - longitude: -117.889293 - }, - }; - return stop; - }); - repository.updateEtasForSystemIfTTL = jest.fn(async () => { - }); - const result = await repository.getEtasForStopId("177666"); - - expect(result).toEqual(initialCache.etasForStopId!["177666"]); - }); - - test("getEtasForStopId returns empty array if no data available", async () => { - const repository = new ApiBasedRepository(); - repository.updateEtasForSystemIfTTL = jest.fn(async () => { - }); - const result = await repository.getEtasForShuttleId("5577"); - - expect(result).toEqual([]); - }); -}); - -describe("updateEtasForSystemIfTTL", () => { - // test("updateEtasForSystemIfTTL does nothing if data is not TTL", async () => { - // updateGlobalFetchMockJson(genericEtaDataByStopId); - // - // // If ETA data is not TTL, then don't do anything - // const expectedEta: IEta = { - // secondsRemaining: 587, - // shuttleId: "5577", - // stopId: "177666", - // millisecondsSinceEpoch: Date.now() - 1000, - // }; - // - // const initialCache: ApiBasedRepositoryCache = { - // etasForShuttleId: { - // "5577": [ - // expectedEta, - // ], - // }, - // etasForStopId: { - // "177666": [ - // expectedEta, - // ], - // }, - // stopsBySystemId: { - // "1": [ - // { - // systemId: "1", - // millisecondsSinceEpoch: Date.now() - 1000, - // name: "Chapman Court", - // id: "177666", - // coordinates: { - // latitude: 33.796796, - // longitude: -117.889293 - // }, - // } - // ], - // }, - // }; - // - // const ttls: ApiBasedRepositoryMillisecondTTLs = { - // etasForShuttleId: 100000, - // etasForStopId: 100000, - // }; - // - // const repository = new ApiBasedRepository(initialCache, ttls); - // await repository.updateEtasForSystemIfTTL("1"); - // - // const updatedResult = await repository.getEtaForShuttleAndStopId( - // "5577", - // "177666", - // ); - // expect(updatedResult?.millisecondsSinceEpoch).toEqual(expectedEta.millisecondsSinceEpoch); - // }); - - test("updateEtasForSystemIfTTL updates all ETA data if data is TTL", async () => { - updateGlobalFetchMockJson(genericEtaDataByStopId); - - const sampleStop: IStop = { - name: "Chapman Court", - systemId: "1", - id: "177666", - coordinates: { - latitude: 33.796796, - longitude: -117.889293 - }, - } - - const repository = new ApiBasedRepository(); - repository.getStopsBySystemId = jest.fn(async () => { - return [ - sampleStop - ]; - }); - - repository.getStopById = jest.fn(async () => { - return sampleStop; - }); - - await repository.updateEtasForSystemIfTTL("1"); - - const updatedResult = await repository.getEtasForStopId("177666"); - expect(updatedResult.length).toEqual(2); - }); -}); - -describe("getShuttleById", () => { - test("getShuttleById returns null if unseeded cache", async () => { - updateGlobalFetchMockJson(genericShuttleDataBySystemId); - - const initialCache: ApiBasedRepositoryCache = {}; - const repository = new ApiBasedRepository(initialCache); - - const shuttle = await repository.getShuttleById("5577"); - expect(shuttle).toBeNull(); - }); - - test("getShuttleById returns data if present", async () => { - updateGlobalFetchMockJson(genericShuttleDataBySystemId); - - const initialCacheShuttle = { - coordinates: { - latitude: 33.7917818, - longitude: -117.8589646, - }, - name: "08", - routeId: "53966", - systemId: "1", - id: "5577", - } - - const initialCache: ApiBasedRepositoryCache = { - shuttleByShuttleId: { - "5577": initialCacheShuttle - } - }; - - const ttls: ApiBasedRepositoryMillisecondTTLs = { - shuttleByShuttleId: 1000, - }; - - const repository = new ApiBasedRepository(initialCache, ttls); - repository.updateStopsForSystemIdIfTTL = jest.fn(async () => { - }) - - const shuttle = await repository.getShuttleById("5577"); - expect(shuttle).toEqual(initialCacheShuttle); - }); -}); - -// TODO: enable when implemented -// describe("getShuttlesBySystemId", () => { -// test("getShuttlesBySystemId returns old data if not expired", async () => { -// updateGlobalFetchMockJson(genericShuttleDataBySystemId); -// -// const initialCacheShuttle = { -// coordinates: { -// latitude: 33.791781, -// longitude: -117.8589646, -// }, -// name: "08", -// routeId: "53966", -// systemId: "1", -// id: "5577", -// millisecondsSinceEpoch: Date.now() - 1000, -// }; -// -// const initialCache: ApiBasedRepositoryCache = { -// shuttlesBySystemId: { -// "1": [ -// initialCacheShuttle, -// ] -// }, -// shuttleByShuttleId: { -// "5577": initialCacheShuttle, -// } -// }; -// -// const ttls: ApiBasedRepositoryMillisecondTTLs = { -// shuttleByShuttleId: 100000, -// shuttlesBySystemId: 100000, -// }; -// -// const repository = new ApiBasedRepository(initialCache, ttls); -// const shuttles = await repository.getShuttlesBySystemId("1"); -// expect(shuttles.length).toEqual(1); -// expect(shuttles[0].id).toEqual(initialCacheShuttle.id); -// }); -// -// test("getShuttlesBySystemId returns fresh data if expired", async () => { -// updateGlobalFetchMockJson(genericShuttleDataBySystemId); -// -// // TODO: move construction of shuttle into method -// const initialCacheShuttle = { -// coordinates: { -// latitude: 33.791781, -// longitude: -117.8589646, -// }, -// name: "08", -// routeId: "53966", -// systemId: "1", -// id: "5577", -// millisecondsSinceEpoch: Date.now() - 100000, -// }; -// -// const initialCache: ApiBasedRepositoryCache = { -// shuttlesBySystemId: { -// "1": [ -// initialCacheShuttle, -// ] -// }, -// shuttleByShuttleId: { -// "5577": initialCacheShuttle, -// } -// }; -// -// const ttls: ApiBasedRepositoryMillisecondTTLs = { -// shuttleByShuttleId: 1000, -// shuttlesBySystemId: 1000, -// }; -// -// const repository = new ApiBasedRepository(initialCache, ttls); -// const shuttles = await repository.getShuttlesBySystemId("1"); -// -// expect(shuttles.length).toEqual(1); -// expect(shuttles[0].id).toEqual("5577"); -// expect(shuttles[0].millisecondsSinceEpoch).not.toEqual(initialCacheShuttle.millisecondsSinceEpoch); -// }); -// -// test("getShuttlesBySystemId returns fresh data if no seeded data", async () => { -// updateGlobalFetchMockJson(genericShuttleDataBySystemId); -// -// const repository = new ApiBasedRepository(); -// const shuttles = await repository.getShuttlesBySystemId("1"); -// -// expect(shuttles.length).toEqual(1); -// }); -// }); From fc23021c70c0247341dcfd2728fb26043af03c1a Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Tue, 21 Jan 2025 13:27:42 -0800 Subject: [PATCH 2/4] rename repository loader to TimedApiBasedRepositoryLoader --- src/index.ts | 4 ++-- ...positoryDataLoader.ts => TimedApiBasedRepositoryLoader.ts} | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename src/loaders/{RepositoryDataLoader.ts => TimedApiBasedRepositoryLoader.ts} (99%) diff --git a/src/index.ts b/src/index.ts index 7080959..925c9bc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,7 +4,7 @@ import { startStandaloneServer } from "@apollo/server/standalone"; import { resolvers } from "./resolvers"; import { ServerContext } from "./ServerContext"; import { UnoptimizedInMemoryRepository } from "./repositories/UnoptimizedInMemoryRepository"; -import { RepositoryDataLoader } from "./loaders/RepositoryDataLoader"; +import { TimedApiBasedRepositoryLoader } from "./loaders/TimedApiBasedRepositoryLoader"; const typeDefs = readFileSync("./schema.graphqls", "utf8"); @@ -18,7 +18,7 @@ async function main() { const repository = new UnoptimizedInMemoryRepository(); // await loadTestData(repository); - const repositoryDataUpdater = new RepositoryDataLoader( + const repositoryDataUpdater = new TimedApiBasedRepositoryLoader( repository ); await repositoryDataUpdater.start(); diff --git a/src/loaders/RepositoryDataLoader.ts b/src/loaders/TimedApiBasedRepositoryLoader.ts similarity index 99% rename from src/loaders/RepositoryDataLoader.ts rename to src/loaders/TimedApiBasedRepositoryLoader.ts index edcc831..4b92ce3 100644 --- a/src/loaders/RepositoryDataLoader.ts +++ b/src/loaders/TimedApiBasedRepositoryLoader.ts @@ -19,7 +19,7 @@ const baseUrl = "https://passiogo.com/mapGetData.php"; // - OrderedStops: reload every few minutes // - Systems: reload once a day -export class RepositoryDataLoader { +export class TimedApiBasedRepositoryLoader { private shouldBeRunning: boolean = false; private timer: any; From 900d6494091fe63b73b66817dcf6934efc9e56b9 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Tue, 21 Jan 2025 13:28:45 -0800 Subject: [PATCH 3/4] add note to turn loadTestData into class --- src/loaders/loadTestData.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/loaders/loadTestData.ts b/src/loaders/loadTestData.ts index 9e83e8a..f9c4bf1 100644 --- a/src/loaders/loadTestData.ts +++ b/src/loaders/loadTestData.ts @@ -2,6 +2,8 @@ import { IEta, IOrderedStop, IRoute, IShuttle, IStop, ISystem } from "../entities/entities"; import { GetterSetterRepository } from "../repositories/GetterSetterRepository"; +// TODO: turn this into a class for testing reusability + const systems: ISystem[] = [ { id: "1", From 59af309bec3bd365ab84ba9ee0d8dcc639cc676b Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Tue, 21 Jan 2025 13:33:28 -0800 Subject: [PATCH 4/4] move api logic to parent class ApiBasedRepositoryLoader --- src/loaders/ApiBasedRepositoryLoader.ts | 268 +++++++++++++++++++ src/loaders/TimedApiBasedRepositoryLoader.ts | 263 +----------------- 2 files changed, 272 insertions(+), 259 deletions(-) create mode 100644 src/loaders/ApiBasedRepositoryLoader.ts diff --git a/src/loaders/ApiBasedRepositoryLoader.ts b/src/loaders/ApiBasedRepositoryLoader.ts new file mode 100644 index 0000000..964dac0 --- /dev/null +++ b/src/loaders/ApiBasedRepositoryLoader.ts @@ -0,0 +1,268 @@ +import { GetterSetterRepository } from "../repositories/GetterSetterRepository"; +import { IEta, IRoute, IShuttle, IStop, ISystem } from "../entities/entities"; + +const systemIdsToSupport = ["263"]; +const baseUrl = "https://passiogo.com/mapGetData.php"; + +export class ApiBasedRepositoryLoader { + constructor( + protected repository: GetterSetterRepository, + ) { + } + + protected async fetchAndUpdateSystemData() { + const params = { + getSystems: "2", + }; + const query = new URLSearchParams(params).toString(); + const response = await fetch(`${baseUrl}?${query}`); + const json = await response.json() + + if (typeof json.all === "object") { + // filter down to supported systems + const filteredSystems = json.all.filter((jsonSystem: any) => systemIdsToSupport.includes(jsonSystem.id)); + await Promise.all(filteredSystems.map(async (system: any) => { + const constructedSystem: ISystem = { + id: system.id, + name: system.fullname, + }; + + await this.repository.addOrUpdateSystem(constructedSystem); + })); + } + } + + protected async fetchAndUpdateRouteDataForExistingSystems() { + const systems = await this.repository.getSystems(); + await Promise.all(systems.map(async (system) => { + const params = { + getRoutes: "2", + }; + + const formDataJsonObject = { + "systemSelected0": system.id, + "amount": "1", + } + const formData = new FormData(); + formData.set("json", JSON.stringify(formDataJsonObject)); + + const query = new URLSearchParams(params).toString(); + const response = await fetch(`${baseUrl}?${query}`, { + method: "POST", + body: formData, + }); + const json = await response.json(); + + if (typeof json.all === "object") { + await Promise.all(json.all.map(async (jsonRoute: any) => { + const constructedRoute: IRoute = { + name: jsonRoute.name, + color: jsonRoute.color, + id: jsonRoute.myid, + polylineCoordinates: [], + systemId: system.id, + }; + + await this.repository.addOrUpdateRoute(constructedRoute); + })) + } + })); + } + + protected async fetchAndUpdateStopAndPolylineDataForRoutesInExistingSystems() { + // Fetch from the API + // Pass JSON output into two different methods to update repository + const systems = await this.repository.getSystems(); + await Promise.all(systems.map(async (system: ISystem) => { + const params = { + getStops: "2", + }; + + const formDataJsonObject = { + "s0": system.id, + "sA": 1 + }; + const formData = new FormData(); + formData.set("json", JSON.stringify(formDataJsonObject)); + + const query = new URLSearchParams(params).toString(); + const response = await fetch(`${baseUrl}?${query}`, { + method: "POST", + body: formData, + }); + const json = await response.json(); + + await this.updateStopDataForSystemAndApiResponse(system, json); + await this.updateOrderedStopDataForExistingStops(json); + await this.updatePolylineDataForExistingRoutesAndApiResponse(json); + })); + } + + protected async fetchAndUpdateShuttleDataForExistingSystems() { + const systems = await this.repository.getSystems(); + await Promise.all(systems.map(async (system: ISystem) => { + const params = { + getBuses: "2" + }; + + const formDataJsonObject = { + "s0": system.id, + "sA": "1" + }; + + const formData = new FormData(); + formData.set("json", JSON.stringify(formDataJsonObject)); + + const query = new URLSearchParams(params).toString(); + const response = await fetch(`${baseUrl}?${query}`, { + method: "POST", + body: formData, + }); + const json = await response.json(); + + if (json.buses && json.buses["-1"] === undefined) { + const jsonBuses = Object.values(json.buses).map((busesArr: any) => { + return busesArr[0]; + }); + + await Promise.all(jsonBuses.map(async (jsonBus: any) => { + const constructedShuttle: IShuttle = { + name: jsonBus.bus, + coordinates: { + latitude: parseFloat(jsonBus.latitude), + longitude: parseFloat(jsonBus.longitude), + }, + routeId: jsonBus.routeId, + systemId: system.id, + id: `${jsonBus.busId}` + } + + await this.repository.addOrUpdateShuttle(constructedShuttle); + })) + } + })); + } + + protected async fetchAndUpdateEtaDataForExistingOrderedStops() { + // TODO implement once I figure out how to associate ETA data with shuttles + + const systems = await this.repository.getSystems() + await Promise.all(systems.map(async (system: ISystem) => { + const stops = await this.repository.getStopsBySystemId(system.id); + await Promise.all(stops.map(async (stop) => { + const params = { + eta: "3", + stopIds: stop.id, + }; + + const query = new URLSearchParams(params).toString(); + const response = await fetch(`${baseUrl}?${query}`, { + method: "GET", + }); + const json = await response.json(); + + if (json.ETAs && json.ETAs[stop.id]) { + // Continue with the parsing + json.ETAs[stop.id].forEach((jsonEta: any) => { + // Update cache + const shuttleId: string = jsonEta.busId; + + const eta: IEta = { + secondsRemaining: jsonEta.secondsSpent, + shuttleId: `${shuttleId}`, + stopId: stop.id, + millisecondsSinceEpoch: Date.now(), + }; + + this.repository.addOrUpdateEta(eta); + }); + } + })); + })) + } + + protected async updateStopDataForSystemAndApiResponse(system: ISystem, json: any) { + if (json.stops) { + const jsonStops = Object.values(json.stops); + + await Promise.all(jsonStops.map(async (stop: any) => { + const constructedStop: IStop = { + name: stop.name, + id: stop.id, + systemId: system.id, + coordinates: { + latitude: parseFloat(stop.latitude), + longitude: parseFloat(stop.longitude), + }, + }; + + await this.repository.addOrUpdateStop(constructedStop); + })); + } + } + + protected async updateOrderedStopDataForExistingStops(json: any) { + if (json.routes) { + await Promise.all(Object.keys(json.routes).map(async (routeId) => { + const jsonOrderedStopData: any[][] = json.routes[routeId].slice(2); + + // The API may return the same stop twice to indicate that + // the route runs in a loop + // If the API does not do this, assume the route is one-way + // To account for this, check if ordered stop already exists in repo + // If it does, write to that stop instead + + for (let index = 0; index < jsonOrderedStopData.length; index++) { + const orderedStopDataArray = jsonOrderedStopData[index]; + + const stopId = orderedStopDataArray[1]; + let constructedOrderedStop = await this.repository.getOrderedStopByRouteAndStopId(routeId, stopId) + if (constructedOrderedStop === null) { + constructedOrderedStop = { + routeId, + stopId, + position: index + 1, + }; + } + + if (index >= 1) { + constructedOrderedStop.previousStop = { + routeId, + stopId: jsonOrderedStopData[index - 1][1], + position: index, + }; + } + if (index < jsonOrderedStopData.length - 1) { + constructedOrderedStop.nextStop = { + routeId, + stopId: jsonOrderedStopData[index + 1][1], + position: index + 2, + }; + } + + await this.repository.addOrUpdateOrderedStop(constructedOrderedStop); + } + })); + } + } + + protected async updatePolylineDataForExistingRoutesAndApiResponse(json: any) { + if (json.routePoints) { + await Promise.all(Object.keys(json.routePoints).map(async (routeId) => { + const routePoints = json.routePoints[routeId][0]; + + const existingRoute = await this.repository.getRouteById(routeId); + if (!existingRoute) return; + + existingRoute.polylineCoordinates = routePoints.map((point: any) => { + return { + latitude: parseFloat(point.lat), + longitude: parseFloat(point.lng), + }; + }); + + await this.repository.addOrUpdateRoute(existingRoute); + })) + } + } +} \ No newline at end of file diff --git a/src/loaders/TimedApiBasedRepositoryLoader.ts b/src/loaders/TimedApiBasedRepositoryLoader.ts index 4b92ce3..6fd40f4 100644 --- a/src/loaders/TimedApiBasedRepositoryLoader.ts +++ b/src/loaders/TimedApiBasedRepositoryLoader.ts @@ -1,9 +1,8 @@ import { GetterSetterRepository } from "../repositories/GetterSetterRepository"; import { IEta, IRoute, IShuttle, IStop, ISystem } from "../entities/entities"; +import { ApiBasedRepositoryLoader } from "./ApiBasedRepositoryLoader"; const timeout = 10000; -const systemIdsToSupport = ["263"]; -const baseUrl = "https://passiogo.com/mapGetData.php"; // Ideas to break this into smaller pieces in the future: // Have one repository data loader running for each supported system @@ -19,13 +18,14 @@ const baseUrl = "https://passiogo.com/mapGetData.php"; // - OrderedStops: reload every few minutes // - Systems: reload once a day -export class TimedApiBasedRepositoryLoader { +export class TimedApiBasedRepositoryLoader extends ApiBasedRepositoryLoader { private shouldBeRunning: boolean = false; private timer: any; constructor( - private repository: GetterSetterRepository, + repository: GetterSetterRepository, ) { + super(repository); this.startFetchDataAndUpdate = this.startFetchDataAndUpdate.bind(this); } @@ -64,259 +64,4 @@ export class TimedApiBasedRepositoryLoader { this.timer = setTimeout(this.startFetchDataAndUpdate, timeout); } - private async fetchAndUpdateSystemData() { - const params = { - getSystems: "2", - }; - const query = new URLSearchParams(params).toString(); - const response = await fetch(`${baseUrl}?${query}`); - const json = await response.json() - - if (typeof json.all === "object") { - // filter down to supported systems - const filteredSystems = json.all.filter((jsonSystem: any) => systemIdsToSupport.includes(jsonSystem.id)); - await Promise.all(filteredSystems.map(async (system: any) => { - const constructedSystem: ISystem = { - id: system.id, - name: system.fullname, - }; - - await this.repository.addOrUpdateSystem(constructedSystem); - })); - } - } - - private async fetchAndUpdateRouteDataForExistingSystems() { - const systems = await this.repository.getSystems(); - await Promise.all(systems.map(async (system) => { - const params = { - getRoutes: "2", - }; - - const formDataJsonObject = { - "systemSelected0": system.id, - "amount": "1", - } - const formData = new FormData(); - formData.set("json", JSON.stringify(formDataJsonObject)); - - const query = new URLSearchParams(params).toString(); - const response = await fetch(`${baseUrl}?${query}`, { - method: "POST", - body: formData, - }); - const json = await response.json(); - - if (typeof json.all === "object") { - await Promise.all(json.all.map(async (jsonRoute: any) => { - const constructedRoute: IRoute = { - name: jsonRoute.name, - color: jsonRoute.color, - id: jsonRoute.myid, - polylineCoordinates: [], - systemId: system.id, - }; - - await this.repository.addOrUpdateRoute(constructedRoute); - })) - } - })); - } - - private async fetchAndUpdateStopAndPolylineDataForRoutesInExistingSystems() { - // Fetch from the API - // Pass JSON output into two different methods to update repository - const systems = await this.repository.getSystems(); - await Promise.all(systems.map(async (system: ISystem) => { - const params = { - getStops: "2", - }; - - const formDataJsonObject = { - "s0": system.id, - "sA": 1 - }; - const formData = new FormData(); - formData.set("json", JSON.stringify(formDataJsonObject)); - - const query = new URLSearchParams(params).toString(); - const response = await fetch(`${baseUrl}?${query}`, { - method: "POST", - body: formData, - }); - const json = await response.json(); - - await this.updateStopDataForSystemAndApiResponse(system, json); - await this.updateOrderedStopDataForExistingStops(json); - await this.updatePolylineDataForExistingRoutesAndApiResponse(json); - })); - } - - private async fetchAndUpdateShuttleDataForExistingSystems() { - const systems = await this.repository.getSystems(); - await Promise.all(systems.map(async (system: ISystem) => { - const params = { - getBuses: "2" - }; - - const formDataJsonObject = { - "s0": system.id, - "sA": "1" - }; - - const formData = new FormData(); - formData.set("json", JSON.stringify(formDataJsonObject)); - - const query = new URLSearchParams(params).toString(); - const response = await fetch(`${baseUrl}?${query}`, { - method: "POST", - body: formData, - }); - const json = await response.json(); - - if (json.buses && json.buses["-1"] === undefined) { - const jsonBuses = Object.values(json.buses).map((busesArr: any) => { - return busesArr[0]; - }); - - await Promise.all(jsonBuses.map(async (jsonBus: any) => { - const constructedShuttle: IShuttle = { - name: jsonBus.bus, - coordinates: { - latitude: parseFloat(jsonBus.latitude), - longitude: parseFloat(jsonBus.longitude), - }, - routeId: jsonBus.routeId, - systemId: system.id, - id: `${jsonBus.busId}` - } - - await this.repository.addOrUpdateShuttle(constructedShuttle); - })) - } - })); - } - - private async fetchAndUpdateEtaDataForExistingOrderedStops() { - // TODO implement once I figure out how to associate ETA data with shuttles - - const systems = await this.repository.getSystems() - await Promise.all(systems.map(async (system: ISystem) => { - const stops = await this.repository.getStopsBySystemId(system.id); - await Promise.all(stops.map(async (stop) => { - const params = { - eta: "3", - stopIds: stop.id, - }; - - const query = new URLSearchParams(params).toString(); - const response = await fetch(`${baseUrl}?${query}`, { - method: "GET", - }); - const json = await response.json(); - - if (json.ETAs && json.ETAs[stop.id]) { - // Continue with the parsing - json.ETAs[stop.id].forEach((jsonEta: any) => { - // Update cache - const shuttleId: string = jsonEta.busId; - - const eta: IEta = { - secondsRemaining: jsonEta.secondsSpent, - shuttleId: `${shuttleId}`, - stopId: stop.id, - millisecondsSinceEpoch: Date.now(), - }; - - this.repository.addOrUpdateEta(eta); - }); - } - })); - })) - } - - private async updateStopDataForSystemAndApiResponse(system: ISystem, json: any) { - if (json.stops) { - const jsonStops = Object.values(json.stops); - - await Promise.all(jsonStops.map(async (stop: any) => { - const constructedStop: IStop = { - name: stop.name, - id: stop.id, - systemId: system.id, - coordinates: { - latitude: parseFloat(stop.latitude), - longitude: parseFloat(stop.longitude), - }, - }; - - await this.repository.addOrUpdateStop(constructedStop); - })); - } - } - - private async updateOrderedStopDataForExistingStops(json: any) { - if (json.routes) { - await Promise.all(Object.keys(json.routes).map(async (routeId) => { - const jsonOrderedStopData: any[][] = json.routes[routeId].slice(2); - - // The API may return the same stop twice to indicate that - // the route runs in a loop - // If the API does not do this, assume the route is one-way - // To account for this, check if ordered stop already exists in repo - // If it does, write to that stop instead - - for (let index = 0; index < jsonOrderedStopData.length; index++) { - const orderedStopDataArray = jsonOrderedStopData[index]; - - const stopId = orderedStopDataArray[1]; - let constructedOrderedStop = await this.repository.getOrderedStopByRouteAndStopId(routeId, stopId) - if (constructedOrderedStop === null) { - constructedOrderedStop = { - routeId, - stopId, - position: index + 1, - }; - } - - if (index >= 1) { - constructedOrderedStop.previousStop = { - routeId, - stopId: jsonOrderedStopData[index - 1][1], - position: index, - }; - } - if (index < jsonOrderedStopData.length - 1) { - constructedOrderedStop.nextStop = { - routeId, - stopId: jsonOrderedStopData[index + 1][1], - position: index + 2, - }; - } - - await this.repository.addOrUpdateOrderedStop(constructedOrderedStop); - } - })); - } - } - - private async updatePolylineDataForExistingRoutesAndApiResponse(json: any) { - if (json.routePoints) { - await Promise.all(Object.keys(json.routePoints).map(async (routeId) => { - const routePoints = json.routePoints[routeId][0]; - - const existingRoute = await this.repository.getRouteById(routeId); - if (!existingRoute) return; - - existingRoute.polylineCoordinates = routePoints.map((point: any) => { - return { - latitude: parseFloat(point.lat), - longitude: parseFloat(point.lng), - }; - }); - - await this.repository.addOrUpdateRoute(existingRoute); - })) - } - } } \ No newline at end of file