From f94c8dd629d8fcb002cd3b030ce7986dd0a0477a Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Wed, 22 Jan 2025 19:29:11 -0800 Subject: [PATCH 01/18] add method stubs to remove data by id --- src/repositories/GetterSetterRepository.ts | 7 +++++++ .../UnoptimizedInMemoryRepository.ts | 18 ++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/src/repositories/GetterSetterRepository.ts b/src/repositories/GetterSetterRepository.ts index ddcf68b..6327a20 100644 --- a/src/repositories/GetterSetterRepository.ts +++ b/src/repositories/GetterSetterRepository.ts @@ -19,6 +19,13 @@ export interface GetterSetterRepository extends GetterRepository { addOrUpdateOrderedStop(orderedStop: IOrderedStop): Promise; addOrUpdateEta(eta: IEta): Promise; + removeSystem(systemId: string): Promise; + removeRoute(routeId: string): Promise; + removeShuttle(shuttleId: string): Promise; + removeStop(stopId: string): Promise; + removeOrderedStop(stopId: string, routeId: string): Promise; + removeEta(shuttleId: string, stopId: string): Promise; + clearSystemData(): Promise; clearRouteData(): Promise; clearShuttleData(): Promise; diff --git a/src/repositories/UnoptimizedInMemoryRepository.ts b/src/repositories/UnoptimizedInMemoryRepository.ts index c24a3c5..61191e8 100644 --- a/src/repositories/UnoptimizedInMemoryRepository.ts +++ b/src/repositories/UnoptimizedInMemoryRepository.ts @@ -148,6 +148,24 @@ export class UnoptimizedInMemoryRepository implements GetterSetterRepository { } } + + public async removeEta(shuttleId: string, stopId: string): Promise { + } + + public async removeOrderedStop(stopId: string, routeId: string): Promise { + } + + public async removeRoute(routeId: string): Promise { + } + + public async removeShuttle(shuttleId: string): Promise { + } + + public async removeStop(stopId: string): Promise { + } + + public async removeSystem(systemId: string): Promise { + } public async clearSystemData() { this.systems = []; } From f080c1ab5ea84d446d4c32fbaa6807478623a852 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Wed, 22 Jan 2025 19:41:31 -0800 Subject: [PATCH 02/18] update stub methods to clarify behavior --- src/repositories/GetterSetterRepository.ts | 12 +++++------ .../UnoptimizedInMemoryRepository.ts | 21 ++++++++++++------- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/src/repositories/GetterSetterRepository.ts b/src/repositories/GetterSetterRepository.ts index 6327a20..1c914c0 100644 --- a/src/repositories/GetterSetterRepository.ts +++ b/src/repositories/GetterSetterRepository.ts @@ -19,12 +19,12 @@ export interface GetterSetterRepository extends GetterRepository { addOrUpdateOrderedStop(orderedStop: IOrderedStop): Promise; addOrUpdateEta(eta: IEta): Promise; - removeSystem(systemId: string): Promise; - removeRoute(routeId: string): Promise; - removeShuttle(shuttleId: string): Promise; - removeStop(stopId: string): Promise; - removeOrderedStop(stopId: string, routeId: string): Promise; - removeEta(shuttleId: string, stopId: string): Promise; + removeSystemIfExists(systemId: string): Promise; + removeRouteIfExists(routeId: string): Promise; + removeShuttleIfExists(shuttleId: string): Promise; + removeStopIfExists(stopId: string): Promise; + removeOrderedStopIfExists(stopId: string, routeId: string): Promise; + removeEtaIfExists(shuttleId: string, stopId: string): Promise; clearSystemData(): Promise; clearRouteData(): Promise; diff --git a/src/repositories/UnoptimizedInMemoryRepository.ts b/src/repositories/UnoptimizedInMemoryRepository.ts index 61191e8..18845ea 100644 --- a/src/repositories/UnoptimizedInMemoryRepository.ts +++ b/src/repositories/UnoptimizedInMemoryRepository.ts @@ -148,24 +148,30 @@ export class UnoptimizedInMemoryRepository implements GetterSetterRepository { } } - - public async removeEta(shuttleId: string, stopId: string): Promise { + removeSystemIfExists(systemId: string): Promise { + return Promise.resolve(null); } - public async removeOrderedStop(stopId: string, routeId: string): Promise { + removeRouteIfExists(routeId: string): Promise { + return Promise.resolve(null); } - public async removeRoute(routeId: string): Promise { + removeShuttleIfExists(shuttleId: string): Promise { + return Promise.resolve(null); } - public async removeShuttle(shuttleId: string): Promise { + removeStopIfExists(stopId: string): Promise { + return Promise.resolve(null); } - public async removeStop(stopId: string): Promise { + removeOrderedStopIfExists(stopId: string, routeId: string): Promise { + return Promise.resolve(null); } - public async removeSystem(systemId: string): Promise { + removeEtaIfExists(shuttleId: string, stopId: string): Promise { + return Promise.resolve(null); } + public async clearSystemData() { this.systems = []; } @@ -189,4 +195,5 @@ export class UnoptimizedInMemoryRepository implements GetterSetterRepository { public async clearStopData(): Promise { this.stops = []; } + } \ No newline at end of file From dd5e95f7d13c3f58baa21f6b740331a7e2f51aea Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Wed, 22 Jan 2025 19:43:52 -0800 Subject: [PATCH 03/18] add test cases --- ...UnoptimizedInMemoryRepositoryTests.test.ts | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/test/repositories/UnoptimizedInMemoryRepositoryTests.test.ts b/test/repositories/UnoptimizedInMemoryRepositoryTests.test.ts index e9e8166..8d5dcb7 100644 --- a/test/repositories/UnoptimizedInMemoryRepositoryTests.test.ts +++ b/test/repositories/UnoptimizedInMemoryRepositoryTests.test.ts @@ -429,6 +429,66 @@ describe("UnoptimizedInMemoryRepository", () => { }); }); + describe("removeSystem", () => { + test("removes system given ID", async () => { + + }); + + test("does nothing if system doesn't exist", async () => { + + }); + }); + + describe("removeRoute", () => { + test("removes route given ID", async () => { + + }); + + test("does nothing if route doesn't exist", async () => { + + }); + }); + + describe("removeShuttle", () => { + test("removes shuttle given ID", async () => { + + }); + + test("does nothing if shuttle doesn't exist", async () => { + + }); + }); + + describe("removeStop", () => { + test("removes stop given ID", async () => { + + }); + + test("does nothing if stop doesn't exist", async () => { + + }); + }); + + describe("removeOrderedStop", () => { + test("removes ordered stop given stop ID and route ID", async () => { + + }); + + test("does nothing if ordered stop doesn't exist", async () => { + + }); + }); + + describe("removeEta", () => { + test("removes eta given shuttle ID and stop ID", async () => { + + }); + + test("does nothing if eta doesn't exist", async () => { + + }); + }); + describe("clearSystemData", () => { test("clears all systems from the repository", async () => { const mockSystems = generateMockSystems(); From 8d81ab1449c35ef31e8aefdf2bddc2381e287fd9 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Wed, 22 Jan 2025 19:50:04 -0800 Subject: [PATCH 04/18] add tests for systems and routes --- ...UnoptimizedInMemoryRepositoryTests.test.ts | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/test/repositories/UnoptimizedInMemoryRepositoryTests.test.ts b/test/repositories/UnoptimizedInMemoryRepositoryTests.test.ts index 8d5dcb7..531cd00 100644 --- a/test/repositories/UnoptimizedInMemoryRepositoryTests.test.ts +++ b/test/repositories/UnoptimizedInMemoryRepositoryTests.test.ts @@ -431,21 +431,59 @@ describe("UnoptimizedInMemoryRepository", () => { describe("removeSystem", () => { test("removes system given ID", async () => { + const mockSystems = generateMockSystems(); + await Promise.all(mockSystems.map(async (system) => { + await repository.addOrUpdateSystem(system); + })); + const systemToRemove = mockSystems[0]; + await repository.removeSystemIfExists(systemToRemove.id); + + const remainingSystems = await repository.getSystems(); + expect(remainingSystems).toHaveLength(mockSystems.length - 1); }); test("does nothing if system doesn't exist", async () => { + const mockSystems = generateMockSystems(); + await Promise.all(mockSystems.map(async (system) => { + await repository.addOrUpdateSystem(system); + })); + await repository.removeSystemIfExists("nonexistent-id"); + + const remainingSystems = await repository.getSystems(); + expect(remainingSystems).toHaveLength(mockSystems.length); }); }); describe("removeRoute", () => { test("removes route given ID", async () => { + const systemId = "1"; + const mockRoutes = generateMockRoutes(); + await Promise.all(mockRoutes.map(async (route) => { + route.systemId = systemId; + await repository.addOrUpdateRoute(route); + })); + const routeToRemove = mockRoutes[0]; + await repository.removeRouteIfExists(routeToRemove.id); + + const remainingRoutes = await repository.getRoutesBySystemId(systemId); + expect(remainingRoutes).toHaveLength(mockRoutes.length - 1); }); test("does nothing if route doesn't exist", async () => { + const systemId = "1"; + const mockRoutes = generateMockRoutes(); + await Promise.all(mockRoutes.map(async (route) => { + route.systemId = systemId; + await repository.addOrUpdateRoute(route); + })); + await repository.removeRouteIfExists("nonexistent-id"); + + const remainingRoutes = await repository.getRoutesBySystemId(systemId); + expect(remainingRoutes).toHaveLength(mockRoutes.length); }); }); From 3a5116ae0c6b2a7a1dc1456af27a00f990d9af25 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Wed, 22 Jan 2025 19:55:09 -0800 Subject: [PATCH 05/18] add tests for shuttles and stops --- ...UnoptimizedInMemoryRepositoryTests.test.ts | 54 ++++++++++++++++--- 1 file changed, 48 insertions(+), 6 deletions(-) diff --git a/test/repositories/UnoptimizedInMemoryRepositoryTests.test.ts b/test/repositories/UnoptimizedInMemoryRepositoryTests.test.ts index 531cd00..a3f2f7e 100644 --- a/test/repositories/UnoptimizedInMemoryRepositoryTests.test.ts +++ b/test/repositories/UnoptimizedInMemoryRepositoryTests.test.ts @@ -429,7 +429,7 @@ describe("UnoptimizedInMemoryRepository", () => { }); }); - describe("removeSystem", () => { + describe("removeSystemIfExists", () => { test("removes system given ID", async () => { const mockSystems = generateMockSystems(); await Promise.all(mockSystems.map(async (system) => { @@ -456,7 +456,7 @@ describe("UnoptimizedInMemoryRepository", () => { }); }); - describe("removeRoute", () => { + describe("removeRouteIfExists", () => { test("removes route given ID", async () => { const systemId = "1"; const mockRoutes = generateMockRoutes(); @@ -487,27 +487,69 @@ describe("UnoptimizedInMemoryRepository", () => { }); }); - describe("removeShuttle", () => { + describe("removeShuttleIfExists", () => { test("removes shuttle given ID", async () => { + const systemId = "1"; + const mockShuttles = generateMockShuttles(); + await Promise.all(mockShuttles.map(async (shuttle) => { + shuttle.systemId = systemId; + await repository.addOrUpdateShuttle(shuttle); + })); + const shuttleToRemove = mockShuttles[0]; + await repository.removeShuttleIfExists(shuttleToRemove.id); + + const remainingShuttles = await repository.getShuttlesBySystemId(systemId); + expect(remainingShuttles).toHaveLength(mockShuttles.length - 1); }); test("does nothing if shuttle doesn't exist", async () => { + const systemId = "1"; + const mockShuttles = generateMockShuttles(); + await Promise.all(mockShuttles.map(async (shuttle) => { + shuttle.systemId = systemId; + await repository.addOrUpdateShuttle(shuttle); + })); + await repository.removeShuttleIfExists("nonexistent-id"); + + const remainingShuttles = await repository.getShuttlesBySystemId(systemId); + expect(remainingShuttles).toHaveLength(mockShuttles.length); }); }); - describe("removeStop", () => { + describe("removeStopIfExists", () => { test("removes stop given ID", async () => { + const systemId = "1"; + const mockStops = generateMockStops(); + await Promise.all(mockStops.map(async (stop) => { + stop.systemId = systemId; + await repository.addOrUpdateStop(stop); + })); + const stopToRemove = mockStops[0]; + await repository.removeStopIfExists(stopToRemove.id); + + const remainingStops = await repository.getStopsBySystemId(systemId); + expect(remainingStops).toHaveLength(mockStops.length - 1); }); test("does nothing if stop doesn't exist", async () => { + const systemId = "1"; + const mockStops = generateMockStops(); + await Promise.all(mockStops.map(async (stop) => { + stop.systemId = systemId; + await repository.addOrUpdateStop(stop); + })); + await repository.removeStopIfExists("nonexistent-id"); + + const remainingStops = await repository.getStopsBySystemId(systemId); + expect(remainingStops).toHaveLength(mockStops.length); }); }); - describe("removeOrderedStop", () => { + describe("removeOrderedStopIfExists", () => { test("removes ordered stop given stop ID and route ID", async () => { }); @@ -517,7 +559,7 @@ describe("UnoptimizedInMemoryRepository", () => { }); }); - describe("removeEta", () => { + describe("removeEtaIfExists", () => { test("removes eta given shuttle ID and stop ID", async () => { }); From fbb57cbf8ec53f94fe1a018182fcd6df1a43f885 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Wed, 22 Jan 2025 20:02:53 -0800 Subject: [PATCH 06/18] add remaining test cases --- ...UnoptimizedInMemoryRepositoryTests.test.ts | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/test/repositories/UnoptimizedInMemoryRepositoryTests.test.ts b/test/repositories/UnoptimizedInMemoryRepositoryTests.test.ts index a3f2f7e..8c04994 100644 --- a/test/repositories/UnoptimizedInMemoryRepositoryTests.test.ts +++ b/test/repositories/UnoptimizedInMemoryRepositoryTests.test.ts @@ -551,21 +551,63 @@ describe("UnoptimizedInMemoryRepository", () => { describe("removeOrderedStopIfExists", () => { test("removes ordered stop given stop ID and route ID", async () => { + const routeId = "1"; + const mockOrderedStops = generateMockOrderedStops(); + await Promise.all(mockOrderedStops.map(async (stop) => { + stop.routeId = routeId; + await repository.addOrUpdateOrderedStop(stop); + })); + const orderedStopToRemove = mockOrderedStops[0]; + await repository.removeOrderedStopIfExists(orderedStopToRemove.stopId, orderedStopToRemove.routeId); + + const remainingOrderedStops = await repository.getOrderedStopsByRouteId(routeId); + expect(remainingOrderedStops).toHaveLength(mockOrderedStops.length - 1); }); test("does nothing if ordered stop doesn't exist", async () => { + const routeId = "1"; + const mockOrderedStops = generateMockOrderedStops(); + await Promise.all(mockOrderedStops.map(async (stop) => { + stop.routeId = routeId; + await repository.addOrUpdateOrderedStop(stop); + })); + await repository.removeOrderedStopIfExists("nonexistent-stop-id", "nonexistent-route-id"); + + const remainingOrderedStops = await repository.getOrderedStopsByRouteId(routeId); + expect(remainingOrderedStops).toHaveLength(mockOrderedStops.length); }); }); describe("removeEtaIfExists", () => { test("removes eta given shuttle ID and stop ID", async () => { + const stopId = "1"; + const mockEtas = generateMockEtas(); + await Promise.all(mockEtas.map(async (eta) => { + eta.stopId = stopId; + await repository.addOrUpdateEta(eta); + })); + const etaToRemove = mockEtas[0]; + await repository.removeEtaIfExists(etaToRemove.shuttleId, etaToRemove.stopId); + + const remainingEtas = await repository.getEtasForStopId(stopId); + expect(remainingEtas).toHaveLength(mockEtas.length - 1); }); test("does nothing if eta doesn't exist", async () => { + const stopId = "1"; + const mockEtas = generateMockEtas(); + await Promise.all(mockEtas.map(async (eta) => { + eta.stopId = stopId; + await repository.addOrUpdateEta(eta); + })); + await repository.removeEtaIfExists("nonexistent-shuttle-id", "nonexistent-stop-id"); + + const remainingEtas = await repository.getEtasForStopId(stopId); + expect(remainingEtas).toHaveLength(mockEtas.length); }); }); From bc07fe5622999a66c5bde97fd35938eae42b904f Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Wed, 22 Jan 2025 20:05:47 -0800 Subject: [PATCH 07/18] add implementation for removeSystemIfExists --- .../UnoptimizedInMemoryRepository.ts | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/src/repositories/UnoptimizedInMemoryRepository.ts b/src/repositories/UnoptimizedInMemoryRepository.ts index 18845ea..6aba616 100644 --- a/src/repositories/UnoptimizedInMemoryRepository.ts +++ b/src/repositories/UnoptimizedInMemoryRepository.ts @@ -148,28 +148,35 @@ export class UnoptimizedInMemoryRepository implements GetterSetterRepository { } } - removeSystemIfExists(systemId: string): Promise { - return Promise.resolve(null); + public async removeSystemIfExists(systemId: string): Promise { + const index = this.systems.findIndex((s) => s.id === systemId); + if (index > -1) { + const systemToReturn = this.systems[index]; + this.systems.splice(index, 1); + return systemToReturn; + } + + return null; } - removeRouteIfExists(routeId: string): Promise { - return Promise.resolve(null); + public async removeRouteIfExists(routeId: string): Promise { + return null; } - removeShuttleIfExists(shuttleId: string): Promise { - return Promise.resolve(null); + public async removeShuttleIfExists(shuttleId: string): Promise { + return null; } - removeStopIfExists(stopId: string): Promise { - return Promise.resolve(null); + public async removeStopIfExists(stopId: string): Promise { + return null; } - removeOrderedStopIfExists(stopId: string, routeId: string): Promise { - return Promise.resolve(null); + public async removeOrderedStopIfExists(stopId: string, routeId: string): Promise { + return null; } - removeEtaIfExists(shuttleId: string, stopId: string): Promise { - return Promise.resolve(null); + public async removeEtaIfExists(shuttleId: string, stopId: string): Promise { + return null; } public async clearSystemData() { From 144fa7c8526b2e074f4b9a6bc228e29493bbd86a Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Wed, 22 Jan 2025 20:09:49 -0800 Subject: [PATCH 08/18] move index finding to separate method --- .../UnoptimizedInMemoryRepository.ts | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/repositories/UnoptimizedInMemoryRepository.ts b/src/repositories/UnoptimizedInMemoryRepository.ts index 6aba616..08d5aeb 100644 --- a/src/repositories/UnoptimizedInMemoryRepository.ts +++ b/src/repositories/UnoptimizedInMemoryRepository.ts @@ -1,13 +1,5 @@ import { GetterSetterRepository } from "./GetterSetterRepository"; -import { - IEntityWithId, - IEta, - IOrderedStop, - IRoute, - IShuttle, - IStop, - ISystem -} from "../entities/entities"; +import { IEntityWithId, IEta, IOrderedStop, IRoute, IShuttle, IStop, ISystem } from "../entities/entities"; /** * An unoptimized in memory repository. @@ -94,6 +86,10 @@ export class UnoptimizedInMemoryRepository implements GetterSetterRepository { return entity; } + private findEntityIndexById(entityId: string, arrayToSearchIn: T[]) { + return arrayToSearchIn.findIndex((value) => value.id === entityId); + } + public async addOrUpdateSystem(system: ISystem): Promise { const index = this.systems.findIndex((s) => s.id === system.id); if (index !== -1) { @@ -149,7 +145,7 @@ export class UnoptimizedInMemoryRepository implements GetterSetterRepository { } public async removeSystemIfExists(systemId: string): Promise { - const index = this.systems.findIndex((s) => s.id === systemId); + const index = this.findEntityIndexById(systemId, this.systems); if (index > -1) { const systemToReturn = this.systems[index]; this.systems.splice(index, 1); From e9703e53ae81e0c184a8a315981f2e22f61f2696 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Wed, 22 Jan 2025 20:10:50 -0800 Subject: [PATCH 09/18] implement route removal --- src/repositories/UnoptimizedInMemoryRepository.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/repositories/UnoptimizedInMemoryRepository.ts b/src/repositories/UnoptimizedInMemoryRepository.ts index 08d5aeb..eb0385b 100644 --- a/src/repositories/UnoptimizedInMemoryRepository.ts +++ b/src/repositories/UnoptimizedInMemoryRepository.ts @@ -156,6 +156,13 @@ export class UnoptimizedInMemoryRepository implements GetterSetterRepository { } public async removeRouteIfExists(routeId: string): Promise { + const index = this.findEntityIndexById(routeId, this.routes); + if (index > -1) { + const routeToReturn = this.routes[index]; + this.routes.splice(index, 1); + return routeToReturn; + } + return null; } From be2efe23536a57369bf349ff031c6cfc506c99d6 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Wed, 22 Jan 2025 20:41:24 -0800 Subject: [PATCH 10/18] implement rest of methods --- .../UnoptimizedInMemoryRepository.ts | 45 ++++++++++--------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/src/repositories/UnoptimizedInMemoryRepository.ts b/src/repositories/UnoptimizedInMemoryRepository.ts index eb0385b..938f4ea 100644 --- a/src/repositories/UnoptimizedInMemoryRepository.ts +++ b/src/repositories/UnoptimizedInMemoryRepository.ts @@ -86,10 +86,6 @@ export class UnoptimizedInMemoryRepository implements GetterSetterRepository { return entity; } - private findEntityIndexById(entityId: string, arrayToSearchIn: T[]) { - return arrayToSearchIn.findIndex((value) => value.id === entityId); - } - public async addOrUpdateSystem(system: ISystem): Promise { const index = this.systems.findIndex((s) => s.id === system.id); if (index !== -1) { @@ -144,42 +140,49 @@ export class UnoptimizedInMemoryRepository implements GetterSetterRepository { } } - public async removeSystemIfExists(systemId: string): Promise { - const index = this.findEntityIndexById(systemId, this.systems); + private async removeEntityByMatcherIfExists(callback: (value: T) => boolean, arrayToSearchIn: T[]) { + const index = arrayToSearchIn.findIndex(callback); if (index > -1) { - const systemToReturn = this.systems[index]; - this.systems.splice(index, 1); - return systemToReturn; + const entityToReturn = arrayToSearchIn[index]; + arrayToSearchIn.splice(index, 1); + return entityToReturn; } return null; } + private async removeEntityByIdIfExists(entityId: string, arrayToSearchIn: T[]) { + return await this.removeEntityByMatcherIfExists((value) => value.id === entityId, arrayToSearchIn); + } + + public async removeSystemIfExists(systemId: string): Promise { + return await this.removeEntityByIdIfExists(systemId, this.systems); + } + public async removeRouteIfExists(routeId: string): Promise { - const index = this.findEntityIndexById(routeId, this.routes); - if (index > -1) { - const routeToReturn = this.routes[index]; - this.routes.splice(index, 1); - return routeToReturn; - } - - return null; + return await this.removeEntityByIdIfExists(routeId, this.routes); } public async removeShuttleIfExists(shuttleId: string): Promise { - return null; + return await this.removeEntityByIdIfExists(shuttleId, this.shuttles); } public async removeStopIfExists(stopId: string): Promise { - return null; + return await this.removeEntityByIdIfExists(stopId, this.stops); } public async removeOrderedStopIfExists(stopId: string, routeId: string): Promise { - return null; + return await this.removeEntityByMatcherIfExists((orderedStop) => { + return orderedStop.stopId === stopId + && orderedStop.routeId === routeId + }, this.orderedStops); } public async removeEtaIfExists(shuttleId: string, stopId: string): Promise { - return null; + return await this.removeEntityByMatcherIfExists((eta) => { + return eta.stopId === stopId + && eta.shuttleId === shuttleId + }, this.etas); } public async clearSystemData() { From d58495d8b43901bcf17d7683702d64446164d579 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Wed, 22 Jan 2025 20:42:07 -0800 Subject: [PATCH 11/18] fix behavior of tests with duplicate mock data --- ...UnoptimizedInMemoryRepositoryTests.test.ts | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/test/repositories/UnoptimizedInMemoryRepositoryTests.test.ts b/test/repositories/UnoptimizedInMemoryRepositoryTests.test.ts index 8c04994..906894b 100644 --- a/test/repositories/UnoptimizedInMemoryRepositoryTests.test.ts +++ b/test/repositories/UnoptimizedInMemoryRepositoryTests.test.ts @@ -551,8 +551,9 @@ describe("UnoptimizedInMemoryRepository", () => { describe("removeOrderedStopIfExists", () => { test("removes ordered stop given stop ID and route ID", async () => { - const routeId = "1"; - const mockOrderedStops = generateMockOrderedStops(); + let mockOrderedStops = generateMockOrderedStops(); + const routeId = mockOrderedStops[0].routeId; + mockOrderedStops = mockOrderedStops.filter((orderedStop) => orderedStop.routeId === routeId); await Promise.all(mockOrderedStops.map(async (stop) => { stop.routeId = routeId; await repository.addOrUpdateOrderedStop(stop); @@ -566,8 +567,9 @@ describe("UnoptimizedInMemoryRepository", () => { }); test("does nothing if ordered stop doesn't exist", async () => { - const routeId = "1"; - const mockOrderedStops = generateMockOrderedStops(); + let mockOrderedStops = generateMockOrderedStops(); + const routeId = mockOrderedStops[0].routeId; + mockOrderedStops = mockOrderedStops.filter((orderedStop) => orderedStop.routeId === routeId); await Promise.all(mockOrderedStops.map(async (stop) => { stop.routeId = routeId; await repository.addOrUpdateOrderedStop(stop); @@ -582,8 +584,10 @@ describe("UnoptimizedInMemoryRepository", () => { describe("removeEtaIfExists", () => { test("removes eta given shuttle ID and stop ID", async () => { - const stopId = "1"; - const mockEtas = generateMockEtas(); + let mockEtas = generateMockEtas(); + const stopId = mockEtas[0].stopId; + mockEtas = mockEtas.filter((eta) => eta.stopId === stopId); + await Promise.all(mockEtas.map(async (eta) => { eta.stopId = stopId; await repository.addOrUpdateEta(eta); @@ -597,8 +601,10 @@ describe("UnoptimizedInMemoryRepository", () => { }); test("does nothing if eta doesn't exist", async () => { - const stopId = "1"; - const mockEtas = generateMockEtas(); + let mockEtas = generateMockEtas(); + const stopId = mockEtas[0].stopId; + mockEtas = mockEtas.filter((eta) => eta.stopId === stopId); + await Promise.all(mockEtas.map(async (eta) => { eta.stopId = stopId; await repository.addOrUpdateEta(eta); From a8594032bcd99119922097dc42e07ebc495a159a Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Wed, 22 Jan 2025 21:10:50 -0800 Subject: [PATCH 12/18] update system test to add some systems to prune --- test/loaders/ApiBasedRepositoryLoaderTests.test.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/loaders/ApiBasedRepositoryLoaderTests.test.ts b/test/loaders/ApiBasedRepositoryLoaderTests.test.ts index 7bb4e7f..b75e817 100644 --- a/test/loaders/ApiBasedRepositoryLoaderTests.test.ts +++ b/test/loaders/ApiBasedRepositoryLoaderTests.test.ts @@ -33,11 +33,19 @@ describe("ApiBasedRepositoryLoader", () => { describe("fetchAndUpdateSystemData", () => { it("updates system data in repository if response received", async () => { + // Arrange + const systemsToPrune = generateMockSystems(); + await Promise.all(systemsToPrune.map(async (system) => { + await loader.repository.addOrUpdateSystem(system); + })); + const numberOfSystemsInResponse = fetchSystemDataSuccessfulResponse.all.length; updateGlobalFetchMockJson(fetchSystemDataSuccessfulResponse); + // Act await loader.fetchAndUpdateSystemData(); + // Assert const systems = await loader.repository.getSystems(); if (loader.supportedSystemIds.length < numberOfSystemsInResponse) { expect(systems).toHaveLength(loader.supportedSystemIds.length); From e7e513b0b1ed8c7cda5dea24269d6b778e123941 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Wed, 22 Jan 2025 21:10:59 -0800 Subject: [PATCH 13/18] implement pruning code --- src/loaders/ApiBasedRepositoryLoader.ts | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/loaders/ApiBasedRepositoryLoader.ts b/src/loaders/ApiBasedRepositoryLoader.ts index de48179..f60bd0a 100644 --- a/src/loaders/ApiBasedRepositoryLoader.ts +++ b/src/loaders/ApiBasedRepositoryLoader.ts @@ -1,5 +1,5 @@ import { GetterSetterRepository } from "../repositories/GetterSetterRepository"; -import { IEta, IRoute, IShuttle, IStop, ISystem } from "../entities/entities"; +import { IEntityWithId, IEta, IRoute, IShuttle, IStop, ISystem } from "../entities/entities"; export class ApiResponseError extends Error { constructor(message: string) { @@ -17,12 +17,25 @@ export class ApiBasedRepositoryLoader { ) { } + private async constructExistingEntityIdSet(entitySearchCallback: () => Promise) { + const existingEntities = await entitySearchCallback(); + const ids = new Set(); + existingEntities.forEach((entity) => { + ids.add(entity.id); + }); + return ids; + } + public async fetchAndUpdateSystemData() { const params = { getSystems: "2", }; const query = new URLSearchParams(params).toString(); + const systemIds = await this.constructExistingEntityIdSet(async () => { + return await this.repository.getSystems(); + }) + try { const response = await fetch(`${this.baseUrl}?${query}`); const json = await response.json(); @@ -41,10 +54,18 @@ export class ApiBasedRepositoryLoader { }; await this.repository.addOrUpdateSystem(constructedSystem); + if (systemIds.has(constructedSystem.id)) { + systemIds.delete(constructedSystem.id); + } })); } else { throw new Error("Received JSON object does not contain `all` field") } + + // Prune systems + await Promise.all(Array.from(systemIds).map(async (systemId) => { + await this.repository.removeSystemIfExists(systemId); + })); } catch(e: any) { throw new ApiResponseError(e.message); } From eb0882b2e57d5575b616d5140f83e0c8dd5a8840 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Wed, 22 Jan 2025 21:21:44 -0800 Subject: [PATCH 14/18] add pruning checks for other data models --- .../ApiBasedRepositoryLoaderTests.test.ts | 64 +++++++++++++++---- 1 file changed, 52 insertions(+), 12 deletions(-) diff --git a/test/loaders/ApiBasedRepositoryLoaderTests.test.ts b/test/loaders/ApiBasedRepositoryLoaderTests.test.ts index b75e817..c050046 100644 --- a/test/loaders/ApiBasedRepositoryLoaderTests.test.ts +++ b/test/loaders/ApiBasedRepositoryLoaderTests.test.ts @@ -7,7 +7,14 @@ import { fetchRouteDataSuccessfulResponse } from "../jsonSnapshots/fetchRouteDat import { fetchStopAndPolylineDataSuccessfulResponse } from "../jsonSnapshots/fetchStopAndPolylineData/fetchStopAndPolylineDataSuccessfulResponse"; -import { generateMockStops, generateMockSystems } from "../generators"; +import { + generateMockEtas, + generateMockOrderedStops, + generateMockRoutes, + generateMockShuttles, + generateMockStops, + generateMockSystems +} from "../generators"; import { IStop } from "../../src/entities/entities"; import { fetchShuttleDataSuccessfulResponse @@ -89,12 +96,23 @@ describe("ApiBasedRepositoryLoader", () => { }); describe("fetchAndUpdateRouteDataForSystemId", () => { + const systemId = "263"; it("updates route data in repository if response received", async () => { + // Arrange + // Test pruning + const routesToPrune = generateMockRoutes(); + await Promise.all(routesToPrune.map(async (route) => { + route.systemId = systemId; + await loader.repository.addOrUpdateRoute(route); + })); + updateGlobalFetchMockJson(fetchRouteDataSuccessfulResponse); - await loader.fetchAndUpdateRouteDataForSystemId("263"); + // Act + await loader.fetchAndUpdateRouteDataForSystemId(systemId); - const routes = await loader.repository.getRoutesBySystemId("263"); + // Assert + const routes = await loader.repository.getRoutesBySystemId(systemId); expect(routes.length).toEqual(fetchRouteDataSuccessfulResponse.all.length) }); @@ -106,7 +124,7 @@ describe("ApiBasedRepositoryLoader", () => { updateGlobalFetchMockJsonToThrowSyntaxError(); await assertAsyncCallbackThrowsApiResponseError(async () => { - await loader.fetchAndUpdateRouteDataForSystemId("263"); + await loader.fetchAndUpdateRouteDataForSystemId(systemId); }); }); }); @@ -128,14 +146,23 @@ describe("ApiBasedRepositoryLoader", () => { }) describe("fetchAndUpdateStopAndPolylineDataForRoutesWithSystemId", () => { + const systemId = "263"; it("updates stop and polyline data if response received", async () => { + // Arrange + // Test pruning of stops only + const stopsToPrune = generateMockStops(); + await Promise.all(stopsToPrune.map(async (stop) => { + stop.systemId = systemId; + await loader.repository.addOrUpdateStop(stop); + })); + updateGlobalFetchMockJson(fetchStopAndPolylineDataSuccessfulResponse); const stopsArray = Object.values(fetchStopAndPolylineDataSuccessfulResponse.stops); - await loader.fetchAndUpdateStopAndPolylineDataForRoutesWithSystemId("263"); + await loader.fetchAndUpdateStopAndPolylineDataForRoutesWithSystemId(systemId); - const stops = await loader.repository.getStopsBySystemId("263"); + const stops = await loader.repository.getStopsBySystemId(systemId); expect(stops.length).toEqual(stopsArray.length); await Promise.all(stops.map(async (stop) => { @@ -143,7 +170,7 @@ describe("ApiBasedRepositoryLoader", () => { expect(orderedStops.length).toBeGreaterThan(0); })); - const routes = await loader.repository.getRoutesBySystemId("263"); + const routes = await loader.repository.getRoutesBySystemId(systemId); routes.forEach((route) => { expect(route.polylineCoordinates.length).toBeGreaterThan(0); }); @@ -153,7 +180,7 @@ describe("ApiBasedRepositoryLoader", () => { updateGlobalFetchMockJsonToThrowSyntaxError(); await assertAsyncCallbackThrowsApiResponseError(async () => { - await loader.fetchAndUpdateStopAndPolylineDataForRoutesWithSystemId("263"); + await loader.fetchAndUpdateStopAndPolylineDataForRoutesWithSystemId(systemId); }); }) }); @@ -174,13 +201,20 @@ describe("ApiBasedRepositoryLoader", () => { }); describe("fetchAndUpdateShuttleDataForSystemId", () => { + const systemId = "263"; it("updates shuttle data in repository if response received", async () => { + const shuttlesToPrune = generateMockShuttles(); + await Promise.all(shuttlesToPrune.map(async (shuttle) => { + shuttle.systemId = systemId; + await loader.repository.addOrUpdateShuttle(shuttle); + })) + updateGlobalFetchMockJson(fetchShuttleDataSuccessfulResponse); const busesInResponse = Object.values(fetchShuttleDataSuccessfulResponse.buses); - await loader.fetchAndUpdateShuttleDataForSystemId("263"); + await loader.fetchAndUpdateShuttleDataForSystemId(systemId); - const shuttles = await loader.repository.getShuttlesBySystemId("263"); + const shuttles = await loader.repository.getShuttlesBySystemId(systemId); expect(shuttles.length).toEqual(busesInResponse.length); }); @@ -189,7 +223,7 @@ describe("ApiBasedRepositoryLoader", () => { updateGlobalFetchMockJsonToThrowSyntaxError(); await assertAsyncCallbackThrowsApiResponseError(async () => { - await loader.fetchAndUpdateShuttleDataForSystemId("263"); + await loader.fetchAndUpdateShuttleDataForSystemId(systemId); }); }); }); @@ -229,9 +263,15 @@ describe("ApiBasedRepositoryLoader", () => { }); describe("fetchAndUpdateEtaDataForStopId", () => { + const stopId = "177666"; it("updates ETA data for stop id if response received", async () => { + const etasToPrune = generateMockEtas(); + await Promise.all(etasToPrune.map(async (eta) => { + eta.stopId = stopId; + await loader.repository.addOrUpdateEta(eta); + })) + updateGlobalFetchMockJson(fetchEtaDataSuccessfulResponse); - const stopId = "177666"; // @ts-ignore const etasFromResponse = fetchEtaDataSuccessfulResponse.ETAs[stopId] From d8119b750640ba8ed571a0a7890fd2c37a511e09 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Wed, 22 Jan 2025 21:36:21 -0800 Subject: [PATCH 15/18] add implementations for data types with id --- src/loaders/ApiBasedRepositoryLoader.ts | 42 ++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 5 deletions(-) diff --git a/src/loaders/ApiBasedRepositoryLoader.ts b/src/loaders/ApiBasedRepositoryLoader.ts index f60bd0a..f863cf6 100644 --- a/src/loaders/ApiBasedRepositoryLoader.ts +++ b/src/loaders/ApiBasedRepositoryLoader.ts @@ -54,9 +54,7 @@ export class ApiBasedRepositoryLoader { }; await this.repository.addOrUpdateSystem(constructedSystem); - if (systemIds.has(constructedSystem.id)) { - systemIds.delete(constructedSystem.id); - } + systemIds.delete(constructedSystem.id); })); } else { throw new Error("Received JSON object does not contain `all` field") @@ -79,6 +77,10 @@ export class ApiBasedRepositoryLoader { } public async fetchAndUpdateRouteDataForSystemId(systemId: string) { + const routeIdsToPrune = await this.constructExistingEntityIdSet(async () => { + return await this.repository.getRoutesBySystemId(systemId); + }); + const params = { getRoutes: "2", }; @@ -110,8 +112,14 @@ export class ApiBasedRepositoryLoader { }; await this.repository.addOrUpdateRoute(constructedRoute); + + routeIdsToPrune.delete(constructedRoute.id); })) } + + await Promise.all(Array.from(routeIdsToPrune).map(async (routeId) => { + await this.repository.removeRouteIfExists(routeId); + })); } catch(e: any) { throw new ApiResponseError(e.message); } @@ -127,6 +135,10 @@ export class ApiBasedRepositoryLoader { public async fetchAndUpdateStopAndPolylineDataForRoutesWithSystemId(systemId: string) { // Fetch from the API // Pass JSON output into two different methods to update repository + const stopIdsToPrune = await this.constructExistingEntityIdSet(async () => { + return await this.repository.getStopsBySystemId(systemId); + }); + const params = { getStops: "2", }; @@ -147,9 +159,13 @@ export class ApiBasedRepositoryLoader { }); const json = await response.json(); - await this.updateStopDataForSystemAndApiResponse(systemId, json); + await this.updateStopDataForSystemAndApiResponse(systemId, json, stopIdsToPrune); await this.updateOrderedStopDataForExistingStops(json); await this.updatePolylineDataForExistingRoutesAndApiResponse(json); + + await Promise.all(Array.from(stopIdsToPrune).map(async (stopId) => { + await this.repository.removeStopIfExists(stopId); + })); } catch(e: any) { throw new ApiResponseError(e.message); } @@ -164,6 +180,10 @@ export class ApiBasedRepositoryLoader { } public async fetchAndUpdateShuttleDataForSystemId(systemId: string) { + const shuttleIdsToPrune = await this.constructExistingEntityIdSet(async () => { + return await this.repository.getShuttlesBySystemId(systemId); + }); + const params = { getBuses: "2" }; @@ -203,8 +223,14 @@ export class ApiBasedRepositoryLoader { } await this.repository.addOrUpdateShuttle(constructedShuttle); + + shuttleIdsToPrune.delete(constructedShuttle.id); })); } + + await Promise.all(Array.from(shuttleIdsToPrune).map(async (shuttleId) => { + await this.repository.removeShuttleIfExists(shuttleId); + })); } catch(e: any) { throw new ApiResponseError(e.message); } @@ -261,7 +287,11 @@ export class ApiBasedRepositoryLoader { } } - protected async updateStopDataForSystemAndApiResponse(systemId: string, json: any) { + protected async updateStopDataForSystemAndApiResponse( + systemId: string, + json: any, + setOfIdsToPrune: Set = new Set(), + ) { if (json.stops) { const jsonStops = Object.values(json.stops); @@ -277,6 +307,8 @@ export class ApiBasedRepositoryLoader { }; await this.repository.addOrUpdateStop(constructedStop); + + setOfIdsToPrune.delete(constructedStop.id); })); } } From 15a93307229121e6a8075162b74aa4348cbdb5c7 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Wed, 22 Jan 2025 22:29:01 -0800 Subject: [PATCH 16/18] add note about pruning behavior for ApiBasedRepositoryLoader --- src/loaders/ApiBasedRepositoryLoader.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/loaders/ApiBasedRepositoryLoader.ts b/src/loaders/ApiBasedRepositoryLoader.ts index f863cf6..af8fd66 100644 --- a/src/loaders/ApiBasedRepositoryLoader.ts +++ b/src/loaders/ApiBasedRepositoryLoader.ts @@ -8,6 +8,11 @@ export class ApiResponseError extends Error { } } +/** + * Class which can load data into a repository from the + * Passio Go API. Supports automatic pruning of all data types + * which inherit from `IEntityWithId`. + */ export class ApiBasedRepositoryLoader { supportedSystemIds = ["263"]; baseUrl = "https://passiogo.com/mapGetData.php"; From d7528757d4423631ed28feb823843b8f74e498d4 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Wed, 22 Jan 2025 22:31:39 -0800 Subject: [PATCH 17/18] remove clearing of data in timed repository and update tests --- src/loaders/TimedApiBasedRepositoryLoader.ts | 5 ----- ...TimedApiBasedRepositoryLoaderTests.test.ts | 19 ++----------------- 2 files changed, 2 insertions(+), 22 deletions(-) diff --git a/src/loaders/TimedApiBasedRepositoryLoader.ts b/src/loaders/TimedApiBasedRepositoryLoader.ts index 89fe34b..863ccc9 100644 --- a/src/loaders/TimedApiBasedRepositoryLoader.ts +++ b/src/loaders/TimedApiBasedRepositoryLoader.ts @@ -47,15 +47,10 @@ export class TimedApiBasedRepositoryLoader extends ApiBasedRepositoryLoader { if (!this.shouldBeRunning) return; try { - await this.repository.clearSystemData(); await this.fetchAndUpdateSystemData(); - await this.repository.clearRouteData(); await this.fetchAndUpdateRouteDataForExistingSystemsInRepository(); - await this.repository.clearStopData(); await this.fetchAndUpdateStopAndPolylineDataForRoutesInExistingSystemsInRepository(); - await this.repository.clearShuttleData(); await this.fetchAndUpdateShuttleDataForExistingSystemsInRepository(); - await this.repository.clearEtaData(); await this.fetchAndUpdateEtaDataForExistingStopsForSystemsInRepository(); } catch (e) { console.error(e); diff --git a/test/loaders/TimedApiBasedRepositoryLoaderTests.test.ts b/test/loaders/TimedApiBasedRepositoryLoaderTests.test.ts index ec37052..18f4616 100644 --- a/test/loaders/TimedApiBasedRepositoryLoaderTests.test.ts +++ b/test/loaders/TimedApiBasedRepositoryLoaderTests.test.ts @@ -1,10 +1,9 @@ import { afterEach, beforeAll, beforeEach, describe, expect, it, jest } from "@jest/globals"; import { TimedApiBasedRepositoryLoader } from "../../src/loaders/TimedApiBasedRepositoryLoader"; import { resetGlobalFetchMockJson } from "../mockHelpers/fetchMockHelpers"; -import { GetterSetterRepository } from "../../src/repositories/GetterSetterRepository"; +import { UnoptimizedInMemoryRepository } from "../../src/repositories/UnoptimizedInMemoryRepository"; describe("TimedApiBasedRepositoryLoader", () => { - let repositoryMock: GetterSetterRepository; let loader: TimedApiBasedRepositoryLoader; let spies: any; @@ -16,15 +15,7 @@ describe("TimedApiBasedRepositoryLoader", () => { beforeEach(() => { resetGlobalFetchMockJson(); - repositoryMock = { - clearSystemData: jest.fn(), - clearRouteData: jest.fn(), - clearStopData: jest.fn(), - clearShuttleData: jest.fn(), - clearEtaData: jest.fn(), - } as unknown as GetterSetterRepository; - - loader = new TimedApiBasedRepositoryLoader(repositoryMock); + loader = new TimedApiBasedRepositoryLoader(new UnoptimizedInMemoryRepository()); spies = { fetchAndUpdateSystemData: jest.spyOn(loader, 'fetchAndUpdateSystemData'), @@ -49,9 +40,6 @@ describe("TimedApiBasedRepositoryLoader", () => { await loader.start(); expect(loader["shouldBeRunning"]).toBe(true); - Object.values(repositoryMock).forEach((mockFn) => { - expect(mockFn).toHaveBeenCalled(); - }); Object.values(spies).forEach((spy: any) => { expect(spy).toHaveBeenCalled(); }); @@ -64,9 +52,6 @@ describe("TimedApiBasedRepositoryLoader", () => { await loader.start(); await loader.start(); - Object.values(repositoryMock).forEach((mockFn) => { - expect(mockFn).toHaveBeenCalledTimes(1); - }); Object.values(spies).forEach((spy: any) => { expect(spy).toHaveBeenCalledTimes(1); }); From b7d8727b25a46f11da76266eedc8628776a6f2f6 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Wed, 22 Jan 2025 22:32:10 -0800 Subject: [PATCH 18/18] remove check of eta pruning --- test/loaders/ApiBasedRepositoryLoaderTests.test.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/test/loaders/ApiBasedRepositoryLoaderTests.test.ts b/test/loaders/ApiBasedRepositoryLoaderTests.test.ts index c050046..173f696 100644 --- a/test/loaders/ApiBasedRepositoryLoaderTests.test.ts +++ b/test/loaders/ApiBasedRepositoryLoaderTests.test.ts @@ -265,12 +265,6 @@ describe("ApiBasedRepositoryLoader", () => { describe("fetchAndUpdateEtaDataForStopId", () => { const stopId = "177666"; it("updates ETA data for stop id if response received", async () => { - const etasToPrune = generateMockEtas(); - await Promise.all(etasToPrune.map(async (eta) => { - eta.stopId = stopId; - await loader.repository.addOrUpdateEta(eta); - })) - updateGlobalFetchMockJson(fetchEtaDataSuccessfulResponse); // @ts-ignore const etasFromResponse = fetchEtaDataSuccessfulResponse.ETAs[stopId]