diff --git a/schema.graphqls b/schema.graphqls index 4e3c22d..60a3d63 100644 --- a/schema.graphqls +++ b/schema.graphqls @@ -1,3 +1,6 @@ +# The Interchange system schema. +# Note how Passio ID and parking ID are abstracted away +# from the endpoints. type System { id: ID! name: String! @@ -8,7 +11,11 @@ type System { shuttles: [Shuttle!] shuttle(id: ID): Shuttle - # TODO: Implement these in system resolvers + parkingSystem: ParkingSystem +} + +type ParkingSystem { + systemId: ID! parkingStructures: [ParkingStructure!] parkingStructure(id: ID): ParkingStructure } @@ -84,6 +91,8 @@ type Query { isNotificationScheduled(input: NotificationInput!): Boolean secondsThresholdForNotification(input: NotificationInput!): Int + + schemaVersion: ID! } # Mutations diff --git a/src/MergedResolvers.ts b/src/MergedResolvers.ts index 3c0caad..795ec38 100644 --- a/src/MergedResolvers.ts +++ b/src/MergedResolvers.ts @@ -8,9 +8,11 @@ import { StopResolvers } from "./resolvers/StopResolvers"; import { ShuttleResolvers } from "./resolvers/ShuttleResolvers"; import { RouteResolvers } from "./resolvers/RouteResolvers"; import { MutationResolvers } from "./resolvers/MutationResolvers"; +import { ParkingSystemResolvers } from "./resolvers/ParkingSystemResolvers"; export const MergedResolvers: Resolvers = { ...QueryResolvers, + ...ParkingSystemResolvers, ...SystemResolvers, ...RouteResolvers, ...ShuttleResolvers, diff --git a/src/entities/InterchangeSystem.ts b/src/entities/InterchangeSystem.ts index d1a33c4..2cb9072 100644 --- a/src/entities/InterchangeSystem.ts +++ b/src/entities/InterchangeSystem.ts @@ -7,6 +7,12 @@ import { ShuttleGetterSetterRepository } from "../repositories/ShuttleGetterSett import { InMemoryNotificationRepository } from "../repositories/InMemoryNotificationRepository"; import { AppleNotificationSender } from "../notifications/senders/AppleNotificationSender"; import { ApiBasedShuttleRepositoryLoader } from "../loaders/shuttle/ApiBasedShuttleRepositoryLoader"; +import { ParkingGetterSetterRepository } from "../repositories/ParkingGetterSetterRepository"; +import { InMemoryParkingRepository } from "../repositories/InMemoryParkingRepository"; +import { + buildParkingRepositoryLoaderIfExists, + ParkingRepositoryLoaderBuilderArguments +} from "../loaders/parking/buildParkingRepositoryLoaderIfExists"; export interface InterchangeSystemBuilderArguments { name: string; @@ -20,16 +26,23 @@ export interface InterchangeSystemBuilderArguments { * ID for fetching shuttle data from the Passio GO! system. */ passioSystemId: string; + + /** + * ID for the parking repository ID in the codebase. + */ + parkingSystemId?: string; } export class InterchangeSystem { - constructor( + private constructor( public name: string, public id: string, public shuttleTimedDataLoader: TimedApiBasedRepositoryLoader, public shuttleRepository: ShuttleGetterSetterRepository, public notificationScheduler: ETANotificationScheduler, public notificationRepository: NotificationRepository, + public parkingTimedDataLoader: TimedApiBasedRepositoryLoader | null, + public parkingRepository: ParkingGetterSetterRepository | null, ) { } @@ -61,6 +74,9 @@ export class InterchangeSystem { ); notificationScheduler.startListeningForUpdates(); + let { parkingRepository, timedParkingLoader } = this.buildParkingLoaderAndRepository(args.parkingSystemId); + timedParkingLoader?.start(); + return new InterchangeSystem( args.name, args.id, @@ -68,6 +84,8 @@ export class InterchangeSystem { shuttleRepository, notificationScheduler, notificationRepository, + timedParkingLoader, + parkingRepository, ); } @@ -100,6 +118,9 @@ export class InterchangeSystem { ); notificationScheduler.startListeningForUpdates(); + let { parkingRepository, timedParkingLoader } = this.buildParkingLoaderAndRepository(args.parkingSystemId); + // Timed parking loader is not started + return new InterchangeSystem( args.name, args.id, @@ -107,6 +128,32 @@ export class InterchangeSystem { shuttleRepository, notificationScheduler, notificationRepository, + timedParkingLoader, + parkingRepository, ); } + + private static buildParkingLoaderAndRepository(id?: string) { + if (id === undefined) { + return { parkingRepository: null, timedParkingLoader: null }; + } + + let parkingRepository: ParkingGetterSetterRepository | null = new InMemoryParkingRepository(); + const loaderBuilderArguments: ParkingRepositoryLoaderBuilderArguments = { + id, + repository: parkingRepository, + }; + let parkingLoader = buildParkingRepositoryLoaderIfExists( + loaderBuilderArguments, + ); + + let timedParkingLoader = null; + if (parkingLoader == null) { + parkingRepository = null; + } else { + timedParkingLoader = new TimedApiBasedRepositoryLoader(parkingLoader); + } + return { parkingRepository, timedParkingLoader }; + } + } diff --git a/src/index.ts b/src/index.ts index 00cd04b..76a0839 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,8 +3,11 @@ import { ApolloServer } from "@apollo/server"; import { startStandaloneServer } from "@apollo/server/standalone"; import { MergedResolvers } from "./MergedResolvers"; import { ServerContext } from "./ServerContext"; -import { loadShuttleTestData, supportedIntegrationTestSystems } from "./loaders/shuttle/loadShuttleTestData"; +import { loadShuttleTestData } from "./loaders/shuttle/loadShuttleTestData"; import { InterchangeSystem, InterchangeSystemBuilderArguments } from "./entities/InterchangeSystem"; +import { ChapmanApiBasedParkingRepositoryLoader } from "./loaders/parking/ChapmanApiBasedParkingRepositoryLoader"; +import { supportedIntegrationTestSystems } from "./loaders/supportedIntegrationTestSystems"; +import { loadParkingTestData } from "./loaders/parking/loadParkingTestData"; const typeDefs = readFileSync("./schema.graphqls", "utf8"); @@ -13,6 +16,7 @@ const supportedSystems: InterchangeSystemBuilderArguments[] = [ { id: "1", passioSystemId: "263", + parkingSystemId: ChapmanApiBasedParkingRepositoryLoader.id, name: "Chapman University", } ] @@ -35,6 +39,9 @@ async function main() { // TODO: Have loading of different data for different systems in the future await loadShuttleTestData(system.shuttleRepository); + if (system.parkingRepository) { + await loadParkingTestData(system.parkingRepository); + } return system; } diff --git a/src/loaders/parking/ChapmanApiBasedParkingRepositoryLoader.ts b/src/loaders/parking/ChapmanApiBasedParkingRepositoryLoader.ts index f922c5b..9730df8 100644 --- a/src/loaders/parking/ChapmanApiBasedParkingRepositoryLoader.ts +++ b/src/loaders/parking/ChapmanApiBasedParkingRepositoryLoader.ts @@ -48,7 +48,7 @@ export class ChapmanApiBasedParkingRepositoryLoader implements ParkingRepository } } - private constructIParkingStructureFromJson(jsonStructure: any) { + public constructIParkingStructureFromJson(jsonStructure: any) { const structureToReturn: IParkingStructure = { capacity: jsonStructure.Capacity, coordinates: { @@ -57,7 +57,7 @@ export class ChapmanApiBasedParkingRepositoryLoader implements ParkingRepository }, id: ChapmanApiBasedParkingRepositoryLoader.generateId(jsonStructure.Address), name: jsonStructure.Name, - spotsAvailable: jsonStructure.CurrentCount, + spotsAvailable: jsonStructure.CurrentCount > jsonStructure.Capacity ? jsonStructure.Capacity : jsonStructure.CurrentCount, address: jsonStructure.Address } diff --git a/src/loaders/parking/buildParkingRepositoryLoaderIfExists.ts b/src/loaders/parking/buildParkingRepositoryLoaderIfExists.ts index cea713b..ae0383e 100644 --- a/src/loaders/parking/buildParkingRepositoryLoaderIfExists.ts +++ b/src/loaders/parking/buildParkingRepositoryLoaderIfExists.ts @@ -1,12 +1,12 @@ import { ParkingGetterSetterRepository } from "../../repositories/ParkingGetterSetterRepository"; import { ChapmanApiBasedParkingRepositoryLoader } from "./ChapmanApiBasedParkingRepositoryLoader"; -interface ParkingRepositoryBuilderArguments { +export interface ParkingRepositoryLoaderBuilderArguments { id: string; repository: ParkingGetterSetterRepository; } -export function buildParkingRepositoryLoaderIfExists(args: ParkingRepositoryBuilderArguments) { +export function buildParkingRepositoryLoaderIfExists(args: ParkingRepositoryLoaderBuilderArguments) { if (args.id === ChapmanApiBasedParkingRepositoryLoader.id) { return new ChapmanApiBasedParkingRepositoryLoader(args.repository); } diff --git a/src/loaders/parking/loadParkingTestData.ts b/src/loaders/parking/loadParkingTestData.ts new file mode 100644 index 0000000..f73d15a --- /dev/null +++ b/src/loaders/parking/loadParkingTestData.ts @@ -0,0 +1,33 @@ +import { ParkingGetterSetterRepository } from "../../repositories/ParkingGetterSetterRepository"; +import { IParkingStructure } from "../../entities/ParkingRepositoryEntities"; + +const parkingStructures: IParkingStructure[] = [ + { + address: "300 E Walnut, Orange, CA 92867", + capacity: 871, + spotsAvailable: 211, + coordinates: { + latitude: 33.7945513, + longitude: -117.8518707, + }, + name: "Anderson Structure", + id: "1", + }, + { + address: "200 W Sycamore Ave, Orange, CA 92866-1053", + capacity: 692, + spotsAvailable: 282, + coordinates: { + latitude: 33.792937, + longitude: -117.854782 + }, + name: "Barrera", + id: "2", + } +]; + +export async function loadParkingTestData(repository: ParkingGetterSetterRepository) { + await Promise.all(parkingStructures.map(async structure => { + await repository.addOrUpdateParkingStructure(structure); + })) +} diff --git a/src/loaders/shuttle/loadShuttleTestData.ts b/src/loaders/shuttle/loadShuttleTestData.ts index c7603bf..c0c4e63 100644 --- a/src/loaders/shuttle/loadShuttleTestData.ts +++ b/src/loaders/shuttle/loadShuttleTestData.ts @@ -1,15 +1,7 @@ // Mock data import { IEta, IOrderedStop, IRoute, IShuttle, IStop } from "../../entities/ShuttleRepositoryEntities"; import { ShuttleGetterSetterRepository } from "../../repositories/ShuttleGetterSetterRepository"; -import { InterchangeSystemBuilderArguments } from "../../entities/InterchangeSystem"; - -export const supportedIntegrationTestSystems: InterchangeSystemBuilderArguments[] = [ - { - id: "1", - name: "Chapman University", - passioSystemId: "263", - }, -]; +import { supportedIntegrationTestSystems } from "../supportedIntegrationTestSystems"; const redRoutePolylineCoordinates = [ { diff --git a/src/loaders/supportedIntegrationTestSystems.ts b/src/loaders/supportedIntegrationTestSystems.ts new file mode 100644 index 0000000..8fd7d6d --- /dev/null +++ b/src/loaders/supportedIntegrationTestSystems.ts @@ -0,0 +1,11 @@ +import { InterchangeSystemBuilderArguments } from "../entities/InterchangeSystem"; +import { ChapmanApiBasedParkingRepositoryLoader } from "./parking/ChapmanApiBasedParkingRepositoryLoader"; + +export const supportedIntegrationTestSystems: InterchangeSystemBuilderArguments[] = [ + { + id: "1", + name: "Chapman University", + passioSystemId: "263", + parkingSystemId: ChapmanApiBasedParkingRepositoryLoader.id, + }, +]; diff --git a/src/resolvers/ParkingSystemResolvers.ts b/src/resolvers/ParkingSystemResolvers.ts new file mode 100644 index 0000000..73a5303 --- /dev/null +++ b/src/resolvers/ParkingSystemResolvers.ts @@ -0,0 +1,29 @@ +import { Resolvers } from "../generated/graphql"; +import { ServerContext } from "../ServerContext"; + +export const ParkingSystemResolvers: Resolvers = { + ParkingSystem: { + parkingStructures: async (parent, _args, contextValue, _info) => { + const system = contextValue.findSystemById(parent.systemId); + if (!system) { + return []; + } + const parkingRepository = system.parkingRepository; + if (!parkingRepository) return []; + + return await parkingRepository.getParkingStructures(); + }, + parkingStructure: async (parent, args, contextValue, info) => { + if (!args.id) return null; + + const system = contextValue.findSystemById(parent.systemId); + if (!system) { + return null; + } + const parkingRepository = system.parkingRepository; + if (!parkingRepository) return null; + + return await parkingRepository.getParkingStructureById(args.id); + }, + } +} diff --git a/src/resolvers/QueryResolvers.ts b/src/resolvers/QueryResolvers.ts index 08a415f..5bbaed1 100644 --- a/src/resolvers/QueryResolvers.ts +++ b/src/resolvers/QueryResolvers.ts @@ -1,6 +1,8 @@ import { ServerContext } from "../ServerContext"; import { Resolvers } from "../generated/graphql"; +const GRAPHQL_SCHEMA_VERSION = "1"; + export const QueryResolvers: Resolvers = { Query: { systems: async (_parent, args, contextValue, _info) => { @@ -46,5 +48,8 @@ export const QueryResolvers: Resolvers = { return null; }, + schemaVersion: async (_parent, _args, _contextValue, _info) => { + return GRAPHQL_SCHEMA_VERSION; + }, }, } diff --git a/src/resolvers/SystemResolvers.ts b/src/resolvers/SystemResolvers.ts index 4f5205f..dc10fe9 100644 --- a/src/resolvers/SystemResolvers.ts +++ b/src/resolvers/SystemResolvers.ts @@ -78,11 +78,17 @@ export const SystemResolvers: Resolvers = { return await system.shuttleRepository.getShuttles(); }, - parkingStructures: async (_parent, _args, _contextValue, _info) => { - return []; - }, - parkingStructure: async (_parent, _args, _contextValue, _info) => { - return null; + parkingSystem: async (parent, _args, contextValue, _info) => { + const system = contextValue.findSystemById(parent.id); + if (!system) { + return null; + } + + if (!system.parkingRepository) return null; + + return { + systemId: parent.id, + }; }, }, } diff --git a/test/loaders/parking/ChapmanApiBasedParkingRepositoryLoaderTests.test.ts b/test/loaders/parking/ChapmanApiBasedParkingRepositoryLoaderTests.test.ts index 55d82c5..9d9ebfc 100644 --- a/test/loaders/parking/ChapmanApiBasedParkingRepositoryLoaderTests.test.ts +++ b/test/loaders/parking/ChapmanApiBasedParkingRepositoryLoaderTests.test.ts @@ -42,8 +42,6 @@ describe("ChapmanApiBasedParkingRepositoryLoader", () => { }); }); - - describe("fetchAndUpdateParkingStructures", () => { it("fetches and update parking structures with unique IDs", async () => { updateGlobalFetchMockJson(chapmanParkingStructureData); @@ -89,4 +87,20 @@ describe("ChapmanApiBasedParkingRepositoryLoader", () => { }); }) }); + + describe("constructIParkingStructureFromJson", () => { + it("normalizes the spots available if it's over the capacity", async () => { + const sampleJsonStructure: any = { + Capacity: 10, + Latitude: 1, + Longitude: 1, + Address: "300 E Walnut, Orange, CA 92867", + Name: "Anderson Structure", + CurrentCount: 11, + }; + + const returnedStructure = loader.constructIParkingStructureFromJson(sampleJsonStructure); + expect(returnedStructure.spotsAvailable).toEqual(returnedStructure.capacity); + }); + }); }); diff --git a/test/resolvers/ParkingSystemResolverTests.test.ts b/test/resolvers/ParkingSystemResolverTests.test.ts new file mode 100644 index 0000000..9515c18 --- /dev/null +++ b/test/resolvers/ParkingSystemResolverTests.test.ts @@ -0,0 +1,139 @@ +import { beforeEach, describe, expect, it } from "@jest/globals"; +import { generateParkingStructures } from "../testHelpers/mockDataGenerators"; +import { setupTestServerContext, setupTestServerHolder } from "../testHelpers/apolloTestServerHelpers"; +import { InterchangeSystem } from "../../src/entities/InterchangeSystem"; +import assert = require("node:assert"); + +describe("ParkingSystemResolver", () => { + const holder = setupTestServerHolder(); + const context = setupTestServerContext(); + + let mockSystem: InterchangeSystem; + + beforeEach(async () => { + mockSystem = context.systems[0]; + }); + + async function getResponseFromQueryNeedingSystemId(query: string) { + return await holder.testServer.executeOperation({ + query, + variables: { + systemId: mockSystem.id, + }, + }, { + contextValue: context, + }); + } + + describe("parkingStructures", () => { + const query = ` + query GetParkingStructuresBySystem($systemId: ID!) { + system(id: $systemId) { + parkingSystem { + parkingStructures { + name + id + capacity + spotsAvailable + coordinates { + latitude + longitude + } + address + } + } + } + } + ` + + it("gets parking structures associated with the system id", async () => { + const expectedParkingStructures = generateParkingStructures(); + await Promise.all(expectedParkingStructures.map(async (structure) => { + await context.systems[0].parkingRepository?.addOrUpdateParkingStructure(structure); + })); + + const response = await getResponseFromQueryNeedingSystemId(query); + + assert(response.body.kind === "single"); + expect(response.body.singleResult.errors).toBeUndefined(); + const parkingStructures = (response.body.singleResult.data as any).system.parkingSystem.parkingStructures; + expect(parkingStructures).toEqual(expectedParkingStructures); + }); + + it("returns a blank array if there are no parking structures", async () => { + const response = await getResponseFromQueryNeedingSystemId(query); + + assert(response.body.kind === "single"); + expect(response.body.singleResult.errors).toBeUndefined(); + const parkingStructures = (response.body.singleResult.data as any).system.parkingSystem.parkingStructures; + expect(parkingStructures).toHaveLength(0); + }); + }); + + + describe("parkingStructure", () => { + async function getResponseForParkingStructureQuery(parkingStructureId: string) { + const query = ` + query GetParkingStructureBySystem($systemId: ID!, $parkingStructureId: ID!) { + system(id: $systemId) { + parkingSystem { + parkingStructure(id: $parkingStructureId) { + name + id + capacity + spotsAvailable + coordinates { + latitude + longitude + } + address + } + } + } + } + `; + + return await holder.testServer.executeOperation({ + query, + variables: { + systemId: mockSystem.id, + parkingStructureId, + } + }, { + contextValue: context + + }); + } + + it("returns the correct parking structure given the id", async () => { + const generatedParkingStructures = generateParkingStructures(); + await Promise.all(generatedParkingStructures.map(async (structure) => { + await context.systems[0].parkingRepository?.addOrUpdateParkingStructure(structure); + })); + const expectedParkingStructure = generatedParkingStructures[1]; + + const response = await getResponseForParkingStructureQuery(expectedParkingStructure.id); + + assert(response.body.kind === "single"); + expect(response.body.singleResult.errors).toBeUndefined(); + const parkingStructure = (response.body.singleResult.data as any).system.parkingSystem.parkingStructure; + expect(parkingStructure).toEqual(expectedParkingStructure); + }); + + it("returns null if there is no matching parking structure", async () => { + const generatedParkingStructures = generateParkingStructures(); + await Promise.all(generatedParkingStructures.map(async (structure) => { + await context.systems[0].parkingRepository?.addOrUpdateParkingStructure(structure); + })); + + const nonexistentId = generatedParkingStructures[0].id + "12345"; + + const response = await getResponseForParkingStructureQuery(nonexistentId); + + assert(response.body.kind === "single"); + expect(response.body.singleResult.errors).toBeUndefined(); + const parkingStructure = (response.body.singleResult.data as any).system.parkingSystem.parkingStructure; + expect(parkingStructure).toBeNull(); + }); + }); +}); diff --git a/test/resolvers/SystemResolverTests.test.ts b/test/resolvers/SystemResolverTests.test.ts index 4333fef..6c0e7aa 100644 --- a/test/resolvers/SystemResolverTests.test.ts +++ b/test/resolvers/SystemResolverTests.test.ts @@ -1,6 +1,11 @@ import { beforeEach, describe, expect, it } from "@jest/globals"; import { setupTestServerContext, setupTestServerHolder } from "../testHelpers/apolloTestServerHelpers"; -import { generateMockRoutes, generateMockShuttles, generateMockStops } from "../testHelpers/mockDataGenerators"; +import { + generateMockRoutes, + generateMockShuttles, + generateMockStops, + generateParkingStructures +} from "../testHelpers/mockDataGenerators"; import { addMockRouteToRepository, addMockShuttleToRepository, @@ -308,4 +313,5 @@ describe("SystemResolvers", () => { expect(shuttles.length === expectedShuttles.length); }); }); + }); diff --git a/test/testHelpers/apolloTestServerHelpers.ts b/test/testHelpers/apolloTestServerHelpers.ts index 731ae98..c367e3b 100644 --- a/test/testHelpers/apolloTestServerHelpers.ts +++ b/test/testHelpers/apolloTestServerHelpers.ts @@ -1,12 +1,12 @@ import { readFileSync } from "fs"; import { ApolloServer } from "@apollo/server"; import { MergedResolvers } from "../../src/MergedResolvers"; -import { UnoptimizedInMemoryShuttleRepository } from "../../src/repositories/UnoptimizedInMemoryShuttleRepository"; import { beforeEach } from "@jest/globals"; import { ServerContext } from "../../src/ServerContext"; -import { ETANotificationScheduler } from "../../src/notifications/schedulers/ETANotificationScheduler"; -import { InMemoryNotificationRepository } from "../../src/repositories/InMemoryNotificationRepository"; import { InterchangeSystem } from "../../src/entities/InterchangeSystem"; +import { + ChapmanApiBasedParkingRepositoryLoader +} from "../../src/loaders/parking/ChapmanApiBasedParkingRepositoryLoader"; function setUpTestServer() { @@ -20,7 +20,10 @@ function setUpTestServer() { } const systemInfoForTesting = { - id: "1", name: "Chapman University", passioSystemId: "263" + id: "1", + name: "Chapman University", + passioSystemId: "263", + parkingSystemId: ChapmanApiBasedParkingRepositoryLoader.id, }; export function buildSystemForTesting() { diff --git a/test/testHelpers/mockDataGenerators.ts b/test/testHelpers/mockDataGenerators.ts index 36d422f..b28cff7 100644 --- a/test/testHelpers/mockDataGenerators.ts +++ b/test/testHelpers/mockDataGenerators.ts @@ -1,8 +1,37 @@ import { IEta, IOrderedStop, IRoute, IShuttle, IStop } from "../../src/entities/ShuttleRepositoryEntities"; +import { IParkingStructure } from "../../src/entities/ParkingRepositoryEntities"; // Use a single set of generators in case any of the // interfaces change in the future +export function generateParkingStructures(): IParkingStructure[] { + // Copied from debugger + return [ + { + "capacity": 871, + "coordinates": { + "latitude": 33.7945513, + "longitude": -117.8518707 + }, + "id": "b0723baf8a6b8bcc37c821473373049e", + "name": "Anderson Structure", + "spotsAvailable": 163, + "address": "300 E Walnut, Orange, CA 92867" + }, + { + "capacity": 692, + "coordinates": { + "latitude": 33.792937, + "longitude": -117.854782 + }, + "id": "81b9e1ed004cf6def2e6c568aaf79ece", + "name": "Barrera", + "spotsAvailable": 179, + "address": "200 W Sycamore Ave, Orange, CA 92866-1053" + } + ]; +} + export function generateMockShuttles(): IShuttle[] { return [ {