mirror of
https://github.com/brendan-ch/project-inter-server.git
synced 2026-04-17 07:50:31 +00:00
Merge pull request #41 from brendan-ch/feat/parking-repository-loader
[INT-61] feat/parking-repository-loader
This commit is contained in:
@@ -1,13 +1,12 @@
|
|||||||
import { ShuttleRepositoryLoader } from "../loaders/ShuttleRepositoryLoader";
|
|
||||||
import { ETANotificationScheduler } from "../notifications/schedulers/ETANotificationScheduler";
|
import { ETANotificationScheduler } from "../notifications/schedulers/ETANotificationScheduler";
|
||||||
import { TimedApiBasedShuttleRepositoryLoader } from "../loaders/TimedApiBasedShuttleRepositoryLoader";
|
import { TimedApiBasedRepositoryLoader } from "../loaders/TimedApiBasedRepositoryLoader";
|
||||||
import { UnoptimizedInMemoryShuttleRepository } from "../repositories/UnoptimizedInMemoryShuttleRepository";
|
import { UnoptimizedInMemoryShuttleRepository } from "../repositories/UnoptimizedInMemoryShuttleRepository";
|
||||||
import { RedisNotificationRepository } from "../repositories/RedisNotificationRepository";
|
import { RedisNotificationRepository } from "../repositories/RedisNotificationRepository";
|
||||||
import { NotificationRepository } from "../repositories/NotificationRepository";
|
import { NotificationRepository } from "../repositories/NotificationRepository";
|
||||||
import { ShuttleGetterSetterRepository } from "../repositories/ShuttleGetterSetterRepository";
|
import { ShuttleGetterSetterRepository } from "../repositories/ShuttleGetterSetterRepository";
|
||||||
import { InMemoryNotificationRepository } from "../repositories/InMemoryNotificationRepository";
|
import { InMemoryNotificationRepository } from "../repositories/InMemoryNotificationRepository";
|
||||||
import { AppleNotificationSender } from "../notifications/senders/AppleNotificationSender";
|
import { AppleNotificationSender } from "../notifications/senders/AppleNotificationSender";
|
||||||
import { ApiBasedShuttleRepositoryLoader } from "../loaders/ApiBasedShuttleRepositoryLoader";
|
import { ApiBasedShuttleRepositoryLoader } from "../loaders/shuttle/ApiBasedShuttleRepositoryLoader";
|
||||||
|
|
||||||
export interface InterchangeSystemBuilderArguments {
|
export interface InterchangeSystemBuilderArguments {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -27,7 +26,7 @@ export class InterchangeSystem {
|
|||||||
constructor(
|
constructor(
|
||||||
public name: string,
|
public name: string,
|
||||||
public id: string,
|
public id: string,
|
||||||
public shuttleDataLoader: ShuttleRepositoryLoader,
|
public shuttleTimedDataLoader: TimedApiBasedRepositoryLoader,
|
||||||
public shuttleRepository: ShuttleGetterSetterRepository,
|
public shuttleRepository: ShuttleGetterSetterRepository,
|
||||||
public notificationScheduler: ETANotificationScheduler,
|
public notificationScheduler: ETANotificationScheduler,
|
||||||
public notificationRepository: NotificationRepository,
|
public notificationRepository: NotificationRepository,
|
||||||
@@ -43,12 +42,15 @@ export class InterchangeSystem {
|
|||||||
args: InterchangeSystemBuilderArguments,
|
args: InterchangeSystemBuilderArguments,
|
||||||
) {
|
) {
|
||||||
const shuttleRepository = new UnoptimizedInMemoryShuttleRepository();
|
const shuttleRepository = new UnoptimizedInMemoryShuttleRepository();
|
||||||
const shuttleDataLoader = new TimedApiBasedShuttleRepositoryLoader(
|
const shuttleDataLoader = new ApiBasedShuttleRepositoryLoader(
|
||||||
args.passioSystemId,
|
args.passioSystemId,
|
||||||
args.id,
|
args.id,
|
||||||
shuttleRepository
|
shuttleRepository
|
||||||
);
|
);
|
||||||
await shuttleDataLoader.start();
|
const timedShuttleDataLoader = new TimedApiBasedRepositoryLoader(
|
||||||
|
shuttleDataLoader,
|
||||||
|
);
|
||||||
|
await timedShuttleDataLoader.start();
|
||||||
|
|
||||||
const notificationRepository = new RedisNotificationRepository();
|
const notificationRepository = new RedisNotificationRepository();
|
||||||
await notificationRepository.connect();
|
await notificationRepository.connect();
|
||||||
@@ -62,7 +64,7 @@ export class InterchangeSystem {
|
|||||||
return new InterchangeSystem(
|
return new InterchangeSystem(
|
||||||
args.name,
|
args.name,
|
||||||
args.id,
|
args.id,
|
||||||
shuttleDataLoader,
|
timedShuttleDataLoader,
|
||||||
shuttleRepository,
|
shuttleRepository,
|
||||||
notificationScheduler,
|
notificationScheduler,
|
||||||
notificationRepository,
|
notificationRepository,
|
||||||
@@ -84,6 +86,11 @@ export class InterchangeSystem {
|
|||||||
args.id,
|
args.id,
|
||||||
shuttleRepository
|
shuttleRepository
|
||||||
);
|
);
|
||||||
|
// Note that this loader should not be started,
|
||||||
|
// so the test data doesn't get overwritten
|
||||||
|
const timedShuttleLoader = new TimedApiBasedRepositoryLoader(
|
||||||
|
shuttleDataLoader,
|
||||||
|
);
|
||||||
|
|
||||||
const notificationRepository = new InMemoryNotificationRepository();
|
const notificationRepository = new InMemoryNotificationRepository();
|
||||||
const notificationScheduler = new ETANotificationScheduler(
|
const notificationScheduler = new ETANotificationScheduler(
|
||||||
@@ -96,7 +103,7 @@ export class InterchangeSystem {
|
|||||||
return new InterchangeSystem(
|
return new InterchangeSystem(
|
||||||
args.name,
|
args.name,
|
||||||
args.id,
|
args.id,
|
||||||
shuttleDataLoader,
|
timedShuttleLoader,
|
||||||
shuttleRepository,
|
shuttleRepository,
|
||||||
notificationScheduler,
|
notificationScheduler,
|
||||||
notificationRepository,
|
notificationRepository,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { ApolloServer } from "@apollo/server";
|
|||||||
import { startStandaloneServer } from "@apollo/server/standalone";
|
import { startStandaloneServer } from "@apollo/server/standalone";
|
||||||
import { MergedResolvers } from "./MergedResolvers";
|
import { MergedResolvers } from "./MergedResolvers";
|
||||||
import { ServerContext } from "./ServerContext";
|
import { ServerContext } from "./ServerContext";
|
||||||
import { loadShuttleTestData, supportedIntegrationTestSystems } from "./loaders/loadShuttleTestData";
|
import { loadShuttleTestData, supportedIntegrationTestSystems } from "./loaders/shuttle/loadShuttleTestData";
|
||||||
import { InterchangeSystem, InterchangeSystemBuilderArguments } from "./entities/InterchangeSystem";
|
import { InterchangeSystem, InterchangeSystemBuilderArguments } from "./entities/InterchangeSystem";
|
||||||
|
|
||||||
const typeDefs = readFileSync("./schema.graphqls", "utf8");
|
const typeDefs = readFileSync("./schema.graphqls", "utf8");
|
||||||
|
|||||||
6
src/loaders/ApiResponseError.ts
Normal file
6
src/loaders/ApiResponseError.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export class ApiResponseError extends Error {
|
||||||
|
constructor(message: string) {
|
||||||
|
super(message);
|
||||||
|
this.name = "ApiResponseError";
|
||||||
|
}
|
||||||
|
}
|
||||||
3
src/loaders/RepositoryLoader.ts
Normal file
3
src/loaders/RepositoryLoader.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export interface RepositoryLoader {
|
||||||
|
fetchAndUpdateAll(): Promise<void>;
|
||||||
|
}
|
||||||
43
src/loaders/TimedApiBasedRepositoryLoader.ts
Normal file
43
src/loaders/TimedApiBasedRepositoryLoader.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { RepositoryLoader } from "./RepositoryLoader";
|
||||||
|
|
||||||
|
// To break down timed loading in the future:
|
||||||
|
// Add flags to the repository indicating which data users are subscribed to
|
||||||
|
// In the loader's `fetchAll` method, check flags and update only needed data
|
||||||
|
|
||||||
|
export class TimedApiBasedRepositoryLoader {
|
||||||
|
private shouldBeRunning: boolean = false;
|
||||||
|
private timer: any;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public loader: RepositoryLoader,
|
||||||
|
public readonly timeoutMs: number = 10000,
|
||||||
|
) {
|
||||||
|
this.startFetchDataAndUpdate = this.startFetchDataAndUpdate.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async start() {
|
||||||
|
if (this.shouldBeRunning) {
|
||||||
|
console.warn("DataLoader timer is already running");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.shouldBeRunning = true;
|
||||||
|
await this.startFetchDataAndUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
public stop() {
|
||||||
|
this.shouldBeRunning = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async startFetchDataAndUpdate() {
|
||||||
|
if (!this.shouldBeRunning) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.loader.fetchAndUpdateAll();
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.timer = setTimeout(this.startFetchDataAndUpdate, this.timeoutMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
import { ShuttleGetterSetterRepository } from "../repositories/ShuttleGetterSetterRepository";
|
|
||||||
import { ApiBasedShuttleRepositoryLoader } from "./ApiBasedShuttleRepositoryLoader";
|
|
||||||
|
|
||||||
// Ideas to break this into smaller pieces in the future:
|
|
||||||
// Have one repository data loader running for each supported system
|
|
||||||
// Each data loader independently updates data based on frequency of usage
|
|
||||||
|
|
||||||
// Notes on this: we only need to reload ETA data frequently
|
|
||||||
// Other data can be reloaded periodically
|
|
||||||
// Detailed list:
|
|
||||||
// - ETA: reload frequently or switch to write-through approach
|
|
||||||
// - Shuttles: reload every minute
|
|
||||||
// - Routes: reload every few minutes
|
|
||||||
// - Stops: reload every few minutes
|
|
||||||
// - OrderedStops: reload every few minutes
|
|
||||||
// - Systems: reload once a day
|
|
||||||
|
|
||||||
export class TimedApiBasedShuttleRepositoryLoader extends ApiBasedShuttleRepositoryLoader {
|
|
||||||
private shouldBeRunning: boolean = false;
|
|
||||||
private timer: any;
|
|
||||||
|
|
||||||
readonly timeout = 10000;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
public passioSystemId: string,
|
|
||||||
public systemIdForConstructedData: string,
|
|
||||||
repository: ShuttleGetterSetterRepository,
|
|
||||||
) {
|
|
||||||
super(passioSystemId, systemIdForConstructedData, repository);
|
|
||||||
this.startFetchDataAndUpdate = this.startFetchDataAndUpdate.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async start() {
|
|
||||||
if (this.shouldBeRunning) {
|
|
||||||
console.warn("DataLoader timer is already running");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.shouldBeRunning = true;
|
|
||||||
await this.startFetchDataAndUpdate();
|
|
||||||
}
|
|
||||||
|
|
||||||
public stop() {
|
|
||||||
this.shouldBeRunning = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async startFetchDataAndUpdate() {
|
|
||||||
if (!this.shouldBeRunning) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.fetchAndUpdateRouteDataForSystem();
|
|
||||||
await this.fetchAndUpdateStopAndPolylineDataForRoutesInSystem();
|
|
||||||
await this.fetchAndUpdateShuttleDataForSystem();
|
|
||||||
|
|
||||||
// Because ETA method doesn't support pruning yet,
|
|
||||||
// add a call to the clear method here
|
|
||||||
await this.repository.clearEtaData();
|
|
||||||
await this.fetchAndUpdateEtaDataForExistingStopsForSystem();
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.timer = setTimeout(this.startFetchDataAndUpdate, this.timeout);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import { ParkingRepositoryLoader } from "./ParkingRepositoryLoader";
|
||||||
|
import { ParkingGetterSetterRepository } from "../../repositories/ParkingGetterSetterRepository";
|
||||||
|
import { createHash } from "node:crypto";
|
||||||
|
import { ApiResponseError } from "../ApiResponseError";
|
||||||
|
import { IParkingStructure } from "../../entities/ParkingRepositoryEntities";
|
||||||
|
|
||||||
|
class ApiParseError extends Error {
|
||||||
|
constructor(message: string) {
|
||||||
|
super(message);
|
||||||
|
this.name = "ApiParseError";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ChapmanApiBasedParkingRepositoryLoader implements ParkingRepositoryLoader {
|
||||||
|
public static readonly id = "chapman-parking-loader";
|
||||||
|
private readonly fetchUrl = "https://webfarm.chapman.edu/ParkingService/ParkingService/counts";
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public repository: ParkingGetterSetterRepository
|
||||||
|
) {
|
||||||
|
this.fetchAndUpdateParkingStructures = this.fetchAndUpdateParkingStructures.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchAndUpdateAll() {
|
||||||
|
await this.fetchAndUpdateParkingStructures();
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchAndUpdateParkingStructures(): Promise<void> {
|
||||||
|
let json: any;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(this.fetchUrl);
|
||||||
|
json = await response.json();
|
||||||
|
} catch(e: any) {
|
||||||
|
throw new ApiResponseError(e.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (typeof json.Structures === "object") {
|
||||||
|
const parkingStructures: IParkingStructure[] = json.Structures.map(this.constructIParkingStructureFromJson);
|
||||||
|
|
||||||
|
await Promise.all(parkingStructures.map(async (structure: IParkingStructure) => {
|
||||||
|
await this.repository.addOrUpdateParkingStructure(structure);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
} catch(e: any) {
|
||||||
|
throw new ApiParseError(e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private constructIParkingStructureFromJson(jsonStructure: any) {
|
||||||
|
const structureToReturn: IParkingStructure = {
|
||||||
|
capacity: jsonStructure.Capacity,
|
||||||
|
coordinates: {
|
||||||
|
latitude: jsonStructure.Latitude,
|
||||||
|
longitude: jsonStructure.Longitude,
|
||||||
|
},
|
||||||
|
id: ChapmanApiBasedParkingRepositoryLoader.generateId(jsonStructure.Address),
|
||||||
|
name: jsonStructure.Name,
|
||||||
|
spotsAvailable: jsonStructure.CurrentCount,
|
||||||
|
address: jsonStructure.Address
|
||||||
|
}
|
||||||
|
|
||||||
|
return structureToReturn;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static normalizeAddress(address: string): string {
|
||||||
|
return address
|
||||||
|
.toLowerCase()
|
||||||
|
.split(/\s+/)
|
||||||
|
.filter(part => part.length > 0)
|
||||||
|
.join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static generateId(address: string): string {
|
||||||
|
const normalized = this.normalizeAddress(address);
|
||||||
|
const hash = createHash('sha256')
|
||||||
|
.update(normalized)
|
||||||
|
.digest('hex');
|
||||||
|
|
||||||
|
return hash.substring(0, 32);
|
||||||
|
}
|
||||||
|
}
|
||||||
5
src/loaders/parking/ParkingRepositoryLoader.ts
Normal file
5
src/loaders/parking/ParkingRepositoryLoader.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { RepositoryLoader } from "../RepositoryLoader";
|
||||||
|
|
||||||
|
export interface ParkingRepositoryLoader extends RepositoryLoader {
|
||||||
|
fetchAndUpdateParkingStructures(): Promise<void>;
|
||||||
|
}
|
||||||
15
src/loaders/parking/buildParkingRepositoryLoaderIfExists.ts
Normal file
15
src/loaders/parking/buildParkingRepositoryLoaderIfExists.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { ParkingGetterSetterRepository } from "../../repositories/ParkingGetterSetterRepository";
|
||||||
|
import { ChapmanApiBasedParkingRepositoryLoader } from "./ChapmanApiBasedParkingRepositoryLoader";
|
||||||
|
|
||||||
|
interface ParkingRepositoryBuilderArguments {
|
||||||
|
id: string;
|
||||||
|
repository: ParkingGetterSetterRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildParkingRepositoryLoaderIfExists(args: ParkingRepositoryBuilderArguments) {
|
||||||
|
if (args.id === ChapmanApiBasedParkingRepositoryLoader.id) {
|
||||||
|
return new ChapmanApiBasedParkingRepositoryLoader(args.repository);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@@ -1,14 +1,8 @@
|
|||||||
import { ShuttleGetterSetterRepository } from "../repositories/ShuttleGetterSetterRepository";
|
import { ShuttleGetterSetterRepository } from "../../repositories/ShuttleGetterSetterRepository";
|
||||||
import { IEta, IRoute, IShuttle, IStop } from "../entities/ShuttleRepositoryEntities";
|
import { IEta, IRoute, IShuttle, IStop } from "../../entities/ShuttleRepositoryEntities";
|
||||||
import { ShuttleRepositoryLoader } from "./ShuttleRepositoryLoader";
|
import { ShuttleRepositoryLoader } from "./ShuttleRepositoryLoader";
|
||||||
import { IEntityWithId } from "../entities/SharedEntities";
|
import { IEntityWithId } from "../../entities/SharedEntities";
|
||||||
|
import { ApiResponseError } from "../ApiResponseError";
|
||||||
export class ApiResponseError extends Error {
|
|
||||||
constructor(message: string) {
|
|
||||||
super(message);
|
|
||||||
this.name = "ApiResponseError";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class which can load data into a repository from the
|
* Class which can load data into a repository from the
|
||||||
@@ -34,6 +28,17 @@ export class ApiBasedShuttleRepositoryLoader implements ShuttleRepositoryLoader
|
|||||||
return ids;
|
return ids;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async fetchAndUpdateAll() {
|
||||||
|
await this.fetchAndUpdateRouteDataForSystem();
|
||||||
|
await this.fetchAndUpdateStopAndPolylineDataForRoutesInSystem();
|
||||||
|
await this.fetchAndUpdateShuttleDataForSystem();
|
||||||
|
|
||||||
|
// Because ETA method doesn't support pruning yet,
|
||||||
|
// add a call to the clear method here
|
||||||
|
await this.repository.clearEtaData();
|
||||||
|
await this.fetchAndUpdateEtaDataForExistingStopsForSystem();
|
||||||
|
}
|
||||||
|
|
||||||
public async fetchAndUpdateRouteDataForSystem() {
|
public async fetchAndUpdateRouteDataForSystem() {
|
||||||
const systemId = this.passioSystemId;
|
const systemId = this.passioSystemId;
|
||||||
const routeIdsToPrune = await this.constructExistingEntityIdSet(async () => {
|
const routeIdsToPrune = await this.constructExistingEntityIdSet(async () => {
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
export interface ShuttleRepositoryLoader {
|
import { RepositoryLoader } from "../RepositoryLoader";
|
||||||
|
|
||||||
|
export interface ShuttleRepositoryLoader extends RepositoryLoader {
|
||||||
fetchAndUpdateRouteDataForSystem(): Promise<void>;
|
fetchAndUpdateRouteDataForSystem(): Promise<void>;
|
||||||
fetchAndUpdateStopAndPolylineDataForRoutesInSystem(): Promise<void>;
|
fetchAndUpdateStopAndPolylineDataForRoutesInSystem(): Promise<void>;
|
||||||
fetchAndUpdateShuttleDataForSystem(): Promise<void>;
|
fetchAndUpdateShuttleDataForSystem(): Promise<void>;
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
// Mock data
|
// Mock data
|
||||||
import { IEta, IOrderedStop, IRoute, IShuttle, IStop } from "../entities/ShuttleRepositoryEntities";
|
import { IEta, IOrderedStop, IRoute, IShuttle, IStop } from "../../entities/ShuttleRepositoryEntities";
|
||||||
import { ShuttleGetterSetterRepository } from "../repositories/ShuttleGetterSetterRepository";
|
import { ShuttleGetterSetterRepository } from "../../repositories/ShuttleGetterSetterRepository";
|
||||||
import { InterchangeSystemBuilderArguments } from "../entities/InterchangeSystem";
|
import { InterchangeSystemBuilderArguments } from "../../entities/InterchangeSystem";
|
||||||
|
|
||||||
export const supportedIntegrationTestSystems: InterchangeSystemBuilderArguments[] = [
|
export const supportedIntegrationTestSystems: InterchangeSystemBuilderArguments[] = [
|
||||||
{
|
{
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
export const chapmanParkingStructureData = {
|
||||||
|
"CenterOnLatitude": 33.793379,
|
||||||
|
"CenterOnLongitude": -117.853099,
|
||||||
|
"LatitudeZoom": 0.0045,
|
||||||
|
"LongitudeZoom": 0.0045,
|
||||||
|
"Structures": [
|
||||||
|
{
|
||||||
|
"Address": "300 E Walnut, Orange, CA 92867",
|
||||||
|
"Capacity": 871,
|
||||||
|
"CurrentCount": 211,
|
||||||
|
"HdpiDetailImage": "https://webfarm.chapman.edu/ParkingService/lastinger-hdpi.png",
|
||||||
|
"Latitude": 33.7945513,
|
||||||
|
"LdpiDetailImage": "https://webfarm.chapman.edu/ParkingService/lastinger-ldpi.png",
|
||||||
|
"Levels": [
|
||||||
|
{
|
||||||
|
"Capacity": 871,
|
||||||
|
"CurrentCount": 211,
|
||||||
|
"FriendlyName": "All Levels",
|
||||||
|
"SystemName": "All"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Capacity": 401,
|
||||||
|
"CurrentCount": 26,
|
||||||
|
"FriendlyName": "B1 Level",
|
||||||
|
"SystemName": "L1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Capacity": 470,
|
||||||
|
"CurrentCount": 185,
|
||||||
|
"FriendlyName": "B2 Level",
|
||||||
|
"SystemName": "L2"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Longitude": -117.8518707,
|
||||||
|
"MdpiDetailImage": "https://webfarm.chapman.edu/ParkingService/lastinger-mdpi.png",
|
||||||
|
"Name": "Anderson Structure",
|
||||||
|
"Timestamp": "/Date(1744327687147-0700)/",
|
||||||
|
"XhdpiDetailImage": "https://webfarm.chapman.edu/ParkingService/lastinger-xhdpi.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Address": "200 W Sycamore Ave, Orange, CA 92866-1053",
|
||||||
|
"Capacity": 692,
|
||||||
|
"CurrentCount": 282,
|
||||||
|
"HdpiDetailImage": "https://webfarm.chapman.edu/ParkingService/barrera-hdpi.png",
|
||||||
|
"Latitude": 33.792937,
|
||||||
|
"LdpiDetailImage": "https://webfarm.chapman.edu/ParkingService/barrera-ldpi.png",
|
||||||
|
"Levels": [
|
||||||
|
{
|
||||||
|
"Capacity": 692,
|
||||||
|
"CurrentCount": 282,
|
||||||
|
"FriendlyName": "All Levels",
|
||||||
|
"SystemName": "All"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Capacity": 85,
|
||||||
|
"CurrentCount": 0,
|
||||||
|
"FriendlyName": "Level 1",
|
||||||
|
"SystemName": "L1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Capacity": 145,
|
||||||
|
"CurrentCount": 21,
|
||||||
|
"FriendlyName": "Level 2",
|
||||||
|
"SystemName": "L2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Capacity": 150,
|
||||||
|
"CurrentCount": 50,
|
||||||
|
"FriendlyName": "Level 3",
|
||||||
|
"SystemName": "L3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Capacity": 150,
|
||||||
|
"CurrentCount": 79,
|
||||||
|
"FriendlyName": "Level 4",
|
||||||
|
"SystemName": "L4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Capacity": 162,
|
||||||
|
"CurrentCount": 132,
|
||||||
|
"FriendlyName": "Level 5",
|
||||||
|
"SystemName": "L5"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Longitude": -117.854782,
|
||||||
|
"MdpiDetailImage": "https://webfarm.chapman.edu/ParkingService/barrera-mdpi.png",
|
||||||
|
"Name": "Barrera",
|
||||||
|
"Timestamp": "/Date(1744327647113-0700)/",
|
||||||
|
"XhdpiDetailImage": "https://webfarm.chapman.edu/ParkingService/barrera-xhdpi.png"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, jest } from "@jest/globals";
|
import { afterEach, beforeAll, beforeEach, describe, expect, it, jest } from "@jest/globals";
|
||||||
import { TimedApiBasedShuttleRepositoryLoader } from "../../src/loaders/TimedApiBasedShuttleRepositoryLoader";
|
import { TimedApiBasedRepositoryLoader } from "../../src/loaders/TimedApiBasedRepositoryLoader";
|
||||||
import { resetGlobalFetchMockJson } from "../testHelpers/fetchMockHelpers";
|
import { resetGlobalFetchMockJson } from "../testHelpers/fetchMockHelpers";
|
||||||
import { UnoptimizedInMemoryShuttleRepository } from "../../src/repositories/UnoptimizedInMemoryShuttleRepository";
|
import { UnoptimizedInMemoryShuttleRepository } from "../../src/repositories/UnoptimizedInMemoryShuttleRepository";
|
||||||
|
import { ApiBasedShuttleRepositoryLoader } from "../../src/loaders/shuttle/ApiBasedShuttleRepositoryLoader";
|
||||||
|
|
||||||
describe("TimedApiBasedRepositoryLoader", () => {
|
describe("TimedApiBasedRepositoryLoader", () => {
|
||||||
let loader: TimedApiBasedShuttleRepositoryLoader;
|
let timedLoader: TimedApiBasedRepositoryLoader;
|
||||||
let spies: any;
|
let spies: any;
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
@@ -15,17 +16,17 @@ describe("TimedApiBasedRepositoryLoader", () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
resetGlobalFetchMockJson();
|
resetGlobalFetchMockJson();
|
||||||
|
|
||||||
loader = new TimedApiBasedShuttleRepositoryLoader(
|
const mockLoader = new ApiBasedShuttleRepositoryLoader(
|
||||||
"1",
|
"1",
|
||||||
"1",
|
"1",
|
||||||
new UnoptimizedInMemoryShuttleRepository()
|
new UnoptimizedInMemoryShuttleRepository(),
|
||||||
|
);
|
||||||
|
timedLoader = new TimedApiBasedRepositoryLoader(
|
||||||
|
mockLoader,
|
||||||
);
|
);
|
||||||
|
|
||||||
spies = {
|
spies = {
|
||||||
fetchAndUpdateRouteDataForSystem: jest.spyOn(loader, 'fetchAndUpdateRouteDataForSystem'),
|
fetchAndUpdateAll: jest.spyOn(mockLoader, 'fetchAndUpdateAll'),
|
||||||
fetchAndUpdateStopAndPolylineDataForRoutesInSystem: jest.spyOn(loader, 'fetchAndUpdateStopAndPolylineDataForRoutesInSystem'),
|
|
||||||
fetchAndUpdateShuttleDataForSystem: jest.spyOn(loader, 'fetchAndUpdateShuttleDataForSystem'),
|
|
||||||
fetchAndUpdateEtaDataForExistingStopsForSystem: jest.spyOn(loader, 'fetchAndUpdateEtaDataForExistingStopsForSystem')
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Object.values(spies).forEach((spy: any) => {
|
Object.values(spies).forEach((spy: any) => {
|
||||||
@@ -40,20 +41,20 @@ describe("TimedApiBasedRepositoryLoader", () => {
|
|||||||
|
|
||||||
describe("start", () => {
|
describe("start", () => {
|
||||||
it("should update internal state, call data fetching methods, and start a timer", async () => {
|
it("should update internal state, call data fetching methods, and start a timer", async () => {
|
||||||
await loader.start();
|
await timedLoader.start();
|
||||||
expect(loader["shouldBeRunning"]).toBe(true);
|
expect(timedLoader["shouldBeRunning"]).toBe(true);
|
||||||
|
|
||||||
Object.values(spies).forEach((spy: any) => {
|
Object.values(spies).forEach((spy: any) => {
|
||||||
expect(spy).toHaveBeenCalled();
|
expect(spy).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(setTimeout).toHaveBeenCalledWith(expect.any(Function), loader.timeout);
|
expect(setTimeout).toHaveBeenCalledWith(expect.any(Function), timedLoader.timeoutMs);
|
||||||
expect(loader.timeout).not.toBeUndefined();
|
expect(timedLoader.timeoutMs).not.toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does nothing if timer is already running", async () => {
|
it("does nothing if timer is already running", async () => {
|
||||||
await loader.start();
|
await timedLoader.start();
|
||||||
await loader.start();
|
await timedLoader.start();
|
||||||
|
|
||||||
Object.values(spies).forEach((spy: any) => {
|
Object.values(spies).forEach((spy: any) => {
|
||||||
expect(spy).toHaveBeenCalledTimes(1);
|
expect(spy).toHaveBeenCalledTimes(1);
|
||||||
@@ -63,8 +64,8 @@ describe("TimedApiBasedRepositoryLoader", () => {
|
|||||||
|
|
||||||
describe("stop", () => {
|
describe("stop", () => {
|
||||||
it("should update internal state", async () => {
|
it("should update internal state", async () => {
|
||||||
loader.stop();
|
timedLoader.stop();
|
||||||
expect(loader['shouldBeRunning']).toBe(false);
|
expect(timedLoader['shouldBeRunning']).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import { beforeEach, describe, expect, it, jest } from "@jest/globals";
|
||||||
|
import {
|
||||||
|
ChapmanApiBasedParkingRepositoryLoader
|
||||||
|
} from "../../../src/loaders/parking/ChapmanApiBasedParkingRepositoryLoader";
|
||||||
|
import { InMemoryParkingRepository } from "../../../src/repositories/InMemoryParkingRepository";
|
||||||
|
import {
|
||||||
|
resetGlobalFetchMockJson,
|
||||||
|
updateGlobalFetchMockJson,
|
||||||
|
updateGlobalFetchMockJsonToThrowSyntaxError
|
||||||
|
} from "../../testHelpers/fetchMockHelpers";
|
||||||
|
import {
|
||||||
|
chapmanParkingStructureData
|
||||||
|
} from "../../jsonSnapshots/chapmanParkingStructureData/chapmanParkingStructureData";
|
||||||
|
import { IParkingStructure } from "../../../src/entities/ParkingRepositoryEntities";
|
||||||
|
import { assertAsyncCallbackThrowsApiResponseError } from "../../testHelpers/assertAsyncCallbackThrowsApiResponseError";
|
||||||
|
|
||||||
|
describe("ChapmanApiBasedParkingRepositoryLoader", () => {
|
||||||
|
let loader: ChapmanApiBasedParkingRepositoryLoader;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
loader = new ChapmanApiBasedParkingRepositoryLoader(
|
||||||
|
new InMemoryParkingRepository(),
|
||||||
|
);
|
||||||
|
resetGlobalFetchMockJson();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("fetchAndUpdateAll", () => {
|
||||||
|
it("calls all the correct methods", async () => {
|
||||||
|
const spies = {
|
||||||
|
fetchAndUpdateParkingStructures: jest.spyOn(loader, "fetchAndUpdateParkingStructures"),
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.values(spies).forEach((spy: any) => {
|
||||||
|
spy.mockResolvedValue(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
await loader.fetchAndUpdateAll();
|
||||||
|
|
||||||
|
Object.values(spies).forEach((spy: any) => {
|
||||||
|
expect(spy).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
describe("fetchAndUpdateParkingStructures", () => {
|
||||||
|
it("fetches and update parking structures with unique IDs", async () => {
|
||||||
|
updateGlobalFetchMockJson(chapmanParkingStructureData);
|
||||||
|
|
||||||
|
await loader.fetchAndUpdateParkingStructures();
|
||||||
|
|
||||||
|
let expectedStructures: IParkingStructure[] = [
|
||||||
|
{
|
||||||
|
address: "300 E Walnut, Orange, CA 92867",
|
||||||
|
capacity: 871,
|
||||||
|
spotsAvailable: 211,
|
||||||
|
coordinates: {
|
||||||
|
latitude: 33.7945513,
|
||||||
|
longitude: -117.8518707,
|
||||||
|
},
|
||||||
|
name: "Anderson Structure",
|
||||||
|
id: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
address: "200 W Sycamore Ave, Orange, CA 92866-1053",
|
||||||
|
capacity: 692,
|
||||||
|
spotsAvailable: 282,
|
||||||
|
coordinates: {
|
||||||
|
latitude: 33.792937,
|
||||||
|
longitude: -117.854782
|
||||||
|
},
|
||||||
|
name: "Barrera",
|
||||||
|
id: "",
|
||||||
|
}
|
||||||
|
];
|
||||||
|
expectedStructures[0].id = ChapmanApiBasedParkingRepositoryLoader.generateId(expectedStructures[0].address);
|
||||||
|
expectedStructures[1].id = ChapmanApiBasedParkingRepositoryLoader.generateId(expectedStructures[1].address);
|
||||||
|
|
||||||
|
const structuresFromLoader = await loader.repository.getParkingStructures();
|
||||||
|
expect(structuresFromLoader).toEqual(expectedStructures);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws ApiResponseError if data is incorrect", async () => {
|
||||||
|
updateGlobalFetchMockJsonToThrowSyntaxError();
|
||||||
|
|
||||||
|
await assertAsyncCallbackThrowsApiResponseError(async () => {
|
||||||
|
await loader.fetchAndUpdateParkingStructures();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,26 +1,23 @@
|
|||||||
import { beforeEach, describe, expect, it, jest } from "@jest/globals";
|
import { afterEach, beforeEach, describe, expect, it, jest } from "@jest/globals";
|
||||||
import { ApiBasedShuttleRepositoryLoader, ApiResponseError } from "../../src/loaders/ApiBasedShuttleRepositoryLoader";
|
import { ApiBasedShuttleRepositoryLoader } from "../../../src/loaders/shuttle/ApiBasedShuttleRepositoryLoader";
|
||||||
import { UnoptimizedInMemoryShuttleRepository } from "../../src/repositories/UnoptimizedInMemoryShuttleRepository";
|
import { UnoptimizedInMemoryShuttleRepository } from "../../../src/repositories/UnoptimizedInMemoryShuttleRepository";
|
||||||
import { fetchRouteDataSuccessfulResponse } from "../jsonSnapshots/fetchRouteData/fetchRouteDataSuccessfulResponse";
|
import { fetchRouteDataSuccessfulResponse } from "../../jsonSnapshots/fetchRouteData/fetchRouteDataSuccessfulResponse";
|
||||||
import {
|
import {
|
||||||
fetchStopAndPolylineDataSuccessfulResponse
|
fetchStopAndPolylineDataSuccessfulResponse
|
||||||
} from "../jsonSnapshots/fetchStopAndPolylineData/fetchStopAndPolylineDataSuccessfulResponse";
|
} from "../../jsonSnapshots/fetchStopAndPolylineData/fetchStopAndPolylineDataSuccessfulResponse";
|
||||||
import { generateMockRoutes, generateMockShuttles, generateMockStops } from "../testHelpers/mockDataGenerators";
|
import { generateMockRoutes, generateMockShuttles, generateMockStops } from "../../testHelpers/mockDataGenerators";
|
||||||
import {
|
import {
|
||||||
fetchShuttleDataSuccessfulResponse
|
fetchShuttleDataSuccessfulResponse
|
||||||
} from "../jsonSnapshots/fetchShuttleData/fetchShuttleDataSuccessfulResponse";
|
} from "../../jsonSnapshots/fetchShuttleData/fetchShuttleDataSuccessfulResponse";
|
||||||
import { fetchEtaDataSuccessfulResponse } from "../jsonSnapshots/fetchEtaData/fetchEtaDataSuccessfulResponse";
|
import { fetchEtaDataSuccessfulResponse } from "../../jsonSnapshots/fetchEtaData/fetchEtaDataSuccessfulResponse";
|
||||||
import {
|
import {
|
||||||
resetGlobalFetchMockJson,
|
resetGlobalFetchMockJson,
|
||||||
updateGlobalFetchMockJson,
|
updateGlobalFetchMockJson,
|
||||||
updateGlobalFetchMockJsonToThrowSyntaxError
|
updateGlobalFetchMockJsonToThrowSyntaxError
|
||||||
} from "../testHelpers/fetchMockHelpers";
|
} from "../../testHelpers/fetchMockHelpers";
|
||||||
|
import { assertAsyncCallbackThrowsApiResponseError } from "../../testHelpers/assertAsyncCallbackThrowsApiResponseError";
|
||||||
|
|
||||||
async function assertAsyncCallbackThrowsApiResponseError(callback: () => Promise<any>) {
|
describe("ApiBasedShuttleRepositoryLoader", () => {
|
||||||
await expect(callback).rejects.toThrow(ApiResponseError);
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("ApiBasedRepositoryLoader", () => {
|
|
||||||
let loader: ApiBasedShuttleRepositoryLoader;
|
let loader: ApiBasedShuttleRepositoryLoader;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -28,7 +25,33 @@ describe("ApiBasedRepositoryLoader", () => {
|
|||||||
resetGlobalFetchMockJson();
|
resetGlobalFetchMockJson();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
const systemId = "1";
|
const systemId = "1";
|
||||||
|
|
||||||
|
describe("fetchAndUpdateAll", () => {
|
||||||
|
it("calls all the correct methods", async () => {
|
||||||
|
const spies = {
|
||||||
|
fetchAndUpdateRouteDataForSystem: jest.spyOn(loader, "fetchAndUpdateRouteDataForSystem"),
|
||||||
|
fetchAndUpdateStopAndPolylineDataForRoutesInSystem: jest.spyOn(loader, "fetchAndUpdateStopAndPolylineDataForRoutesInSystem"),
|
||||||
|
fetchAndUpdateShuttleDataForSystem: jest.spyOn(loader, "fetchAndUpdateShuttleDataForSystem"),
|
||||||
|
fetchAndUpdateEtaDataForExistingStopsForSystem: jest.spyOn(loader, "fetchAndUpdateEtaDataForExistingStopsForSystem"),
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.values(spies).forEach((spy: any) => {
|
||||||
|
spy.mockResolvedValue(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
await loader.fetchAndUpdateAll();
|
||||||
|
|
||||||
|
Object.values(spies).forEach((spy: any) => {
|
||||||
|
expect(spy).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("fetchAndUpdateRouteDataForSystem", () => {
|
describe("fetchAndUpdateRouteDataForSystem", () => {
|
||||||
it("updates route data in repository if response received", async () => {
|
it("updates route data in repository if response received", async () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { expect } from "@jest/globals";
|
||||||
|
import { ApiResponseError } from "../../src/loaders/ApiResponseError";
|
||||||
|
|
||||||
|
export async function assertAsyncCallbackThrowsApiResponseError(callback: () => Promise<any>) {
|
||||||
|
await expect(callback).rejects.toThrow(ApiResponseError);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user