mirror of
https://github.com/brendan-ch/project-inter-server.git
synced 2026-04-17 07:50:31 +00:00
Merge pull request #2 from brendan-ch/feat/api-based-repository
feat/api-based-repository
This commit is contained in:
5
src/ServerContext.ts
Normal file
5
src/ServerContext.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { GetterRepository } from "./repositories/GetterRepository";
|
||||||
|
|
||||||
|
export interface ServerContext {
|
||||||
|
repository: GetterRepository
|
||||||
|
}
|
||||||
51
src/entities/entities.ts
Normal file
51
src/entities/entities.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
export interface IEntityWithOptionalTimestamp {
|
||||||
|
millisecondsSinceEpoch?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IEntityWithId {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISystem extends IEntityWithId, IEntityWithOptionalTimestamp {
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ICoordinates {
|
||||||
|
latitude: number;
|
||||||
|
longitude: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IRoute extends IEntityWithId, IEntityWithOptionalTimestamp {
|
||||||
|
name: string;
|
||||||
|
color: string;
|
||||||
|
polylineCoordinates: ICoordinates[];
|
||||||
|
systemId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IStop extends IEntityWithId, IEntityWithOptionalTimestamp {
|
||||||
|
name: string;
|
||||||
|
systemId: string;
|
||||||
|
coordinates: ICoordinates;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IShuttle extends IEntityWithId, IEntityWithOptionalTimestamp {
|
||||||
|
coordinates: ICoordinates;
|
||||||
|
name: string;
|
||||||
|
routeId: string;
|
||||||
|
systemId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IEta extends IEntityWithOptionalTimestamp {
|
||||||
|
secondsRemaining: number;
|
||||||
|
shuttleId: string;
|
||||||
|
stopId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IOrderedStop extends IEntityWithOptionalTimestamp {
|
||||||
|
nextStop?: IOrderedStop;
|
||||||
|
previousStop?: IOrderedStop;
|
||||||
|
routeId: string;
|
||||||
|
stopId: string;
|
||||||
|
position: number;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -2,10 +2,10 @@ import { readFileSync } from "fs";
|
|||||||
import { ApolloServer } from "@apollo/server";
|
import { ApolloServer } from "@apollo/server";
|
||||||
import { startStandaloneServer } from "@apollo/server/standalone";
|
import { startStandaloneServer } from "@apollo/server/standalone";
|
||||||
import { resolvers } from "./resolvers";
|
import { resolvers } from "./resolvers";
|
||||||
import { loadTestData } from "./testData";
|
import { loadTestData } from "./loaders/loadTestData";
|
||||||
import { ServerContext } from "./serverContext";
|
import { ServerContext } from "./ServerContext";
|
||||||
import { UnoptimizedInMemoryRepository } from "./unoptimizedInMemoryRepository";
|
import { UnoptimizedInMemoryRepository } from "./repositories/UnoptimizedInMemoryRepository";
|
||||||
import { RepositoryDataLoader } from "./repositoryDataLoader";
|
import { RepositoryDataLoader } from "./loaders/RepositoryDataLoader";
|
||||||
|
|
||||||
const typeDefs = readFileSync("./schema.graphqls", "utf8");
|
const typeDefs = readFileSync("./schema.graphqls", "utf8");
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { IOrderedStop, IRoute, IShuttle, IStop, ISystem, Repository } from "./repository";
|
import { GetterSetterRepository } from "../repositories/GetterSetterRepository";
|
||||||
|
import { IRoute, IShuttle, IStop, ISystem } from "../entities/entities";
|
||||||
|
|
||||||
const timeout = 10000;
|
const timeout = 10000;
|
||||||
const systemIdsToSupport = ["263"];
|
const systemIdsToSupport = ["263"];
|
||||||
@@ -23,7 +24,7 @@ export class RepositoryDataLoader {
|
|||||||
private shouldBeRunning: boolean = false;
|
private shouldBeRunning: boolean = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private repository: Repository,
|
private repository: GetterSetterRepository,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public async start() {
|
public async start() {
|
||||||
@@ -107,7 +108,6 @@ export class RepositoryDataLoader {
|
|||||||
name: jsonRoute.name,
|
name: jsonRoute.name,
|
||||||
color: jsonRoute.color,
|
color: jsonRoute.color,
|
||||||
id: jsonRoute.myid,
|
id: jsonRoute.myid,
|
||||||
// TODO associate polyline coordinates with routes
|
|
||||||
polylineCoordinates: [],
|
polylineCoordinates: [],
|
||||||
systemId: system.id,
|
systemId: system.id,
|
||||||
};
|
};
|
||||||
@@ -122,13 +122,13 @@ export class RepositoryDataLoader {
|
|||||||
// Fetch from the API
|
// Fetch from the API
|
||||||
// Pass JSON output into two different methods to update repository
|
// Pass JSON output into two different methods to update repository
|
||||||
const systems = await this.repository.getSystems();
|
const systems = await this.repository.getSystems();
|
||||||
await Promise.all(systems.map(async (system: any) => {
|
await Promise.all(systems.map(async (system: ISystem) => {
|
||||||
const params = {
|
const params = {
|
||||||
getStops: "2",
|
getStops: "2",
|
||||||
};
|
};
|
||||||
|
|
||||||
const formDataJsonObject = {
|
const formDataJsonObject = {
|
||||||
"s0": "263",
|
"s0": system.id,
|
||||||
"sA": 1
|
"sA": 1
|
||||||
};
|
};
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
// Mock data
|
// Mock data
|
||||||
import { IEta, IOrderedStop, IRoute, IShuttle, IStop, ISystem, Repository } from "./repository";
|
import { IEta, IOrderedStop, IRoute, IShuttle, IStop, ISystem } from "../entities/entities";
|
||||||
|
import { GetterSetterRepository } from "../repositories/GetterSetterRepository";
|
||||||
|
|
||||||
const systems: ISystem[] = [
|
const systems: ISystem[] = [
|
||||||
{
|
{
|
||||||
@@ -103,7 +104,7 @@ const etas: IEta[] = [
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
export async function loadTestData(repository: Repository) {
|
export async function loadTestData(repository: GetterSetterRepository) {
|
||||||
await Promise.all(systems.map(async (system) => {
|
await Promise.all(systems.map(async (system) => {
|
||||||
await repository.addOrUpdateSystem(system);
|
await repository.addOrUpdateSystem(system);
|
||||||
}));
|
}));
|
||||||
376
src/repositories/ApiBasedRepository.ts
Normal file
376
src/repositories/ApiBasedRepository.ts
Normal file
@@ -0,0 +1,376 @@
|
|||||||
|
import { GetterRepository } from "./GetterRepository";
|
||||||
|
import { IEta, IOrderedStop, IRoute, IShuttle, IStop, ISystem } from "../entities/entities";
|
||||||
|
|
||||||
|
const baseUrl = "https://passiogo.com/mapGetData.php"
|
||||||
|
|
||||||
|
// TODO: implement TTL functionality
|
||||||
|
// TODO: add TTL values to everything
|
||||||
|
// TODO: remove RepositoryDataLoader and UnoptimizedInMemoryRepository
|
||||||
|
// TODO: make milliseconds (TTL) required on everything
|
||||||
|
// TODO: extract cache into its own class
|
||||||
|
|
||||||
|
export interface ApiBasedRepositoryCache {
|
||||||
|
etasForShuttleId?: {
|
||||||
|
[shuttleId: string]: IEta[],
|
||||||
|
},
|
||||||
|
etasForStopId?: {
|
||||||
|
[stopId: string]: IEta[],
|
||||||
|
},
|
||||||
|
stopsBySystemId?: {
|
||||||
|
[systemId: string]: IStop[],
|
||||||
|
},
|
||||||
|
stopByStopId?: {
|
||||||
|
[stopId: string]: IStop,
|
||||||
|
},
|
||||||
|
shuttleByShuttleId?: {
|
||||||
|
[shuttleId: string]: IShuttle,
|
||||||
|
},
|
||||||
|
shuttlesBySystemId?: {
|
||||||
|
[systemId: string]: IShuttle[],
|
||||||
|
},
|
||||||
|
// To speed things up, implement caches for other data later
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiBasedRepositoryMillisecondTTLs {
|
||||||
|
etasForShuttleId?: number,
|
||||||
|
etasForStopId?: number,
|
||||||
|
shuttleByShuttleId?: number,
|
||||||
|
shuttlesBySystemId?: number,
|
||||||
|
}
|
||||||
|
|
||||||
|
const emptyCache: ApiBasedRepositoryCache = {
|
||||||
|
etasForShuttleId: {},
|
||||||
|
etasForStopId: {},
|
||||||
|
shuttleByShuttleId: {},
|
||||||
|
shuttlesBySystemId: {},
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultTtls: ApiBasedRepositoryMillisecondTTLs = {
|
||||||
|
etasForShuttleId: 10000,
|
||||||
|
etasForStopId: 10000,
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ApiBasedRepository implements GetterRepository {
|
||||||
|
private cache: ApiBasedRepositoryCache;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
initialCache: ApiBasedRepositoryCache | undefined = emptyCache,
|
||||||
|
private ttls: ApiBasedRepositoryMillisecondTTLs = defaultTtls,
|
||||||
|
) {
|
||||||
|
this.cache = initialCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seed the initial data in the cache, so data can be updated
|
||||||
|
* given the correct ID.
|
||||||
|
* Alternatively, pass in an `initialCache` with existing data
|
||||||
|
* (useful for testing).
|
||||||
|
* @param systemId
|
||||||
|
*/
|
||||||
|
public async seedCacheForSystemId(systemId: string): Promise<void> {
|
||||||
|
await this.getShuttlesBySystemId(systemId);
|
||||||
|
await this.getShuttlesByRouteId(systemId);
|
||||||
|
await this.getStopsBySystemId(systemId);
|
||||||
|
await this.getSystemById(systemId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getEtaForShuttleAndStopId(shuttleId: string, stopId: string): Promise<IEta | null> {
|
||||||
|
const shuttle = await this.getShuttleById(shuttleId);
|
||||||
|
const systemId = shuttle?.systemId;
|
||||||
|
if (!systemId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
await this.updateEtasForSystemIfTTL(systemId);
|
||||||
|
|
||||||
|
if (this.cache?.etasForStopId && this.cache.etasForStopId[stopId]) {
|
||||||
|
const etas = this.cache.etasForStopId[stopId];
|
||||||
|
const foundEta = etas.find((eta) => eta.stopId === stopId);
|
||||||
|
if (foundEta) {
|
||||||
|
return foundEta;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getEtasForShuttleId(shuttleId: string): Promise<IEta[]> {
|
||||||
|
const shuttle = await this.getShuttleById(shuttleId);
|
||||||
|
const systemId = shuttle?.systemId;
|
||||||
|
if (!systemId) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
await this.updateEtasForSystemIfTTL(systemId);
|
||||||
|
|
||||||
|
if (!this.cache?.etasForShuttleId || !this.cache.etasForShuttleId[shuttleId]) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.cache.etasForShuttleId[shuttleId];
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getEtasForStopId(stopId: string): Promise<IEta[]> {
|
||||||
|
const stop = await this.getStopById(stopId);
|
||||||
|
const systemId = stop?.systemId;
|
||||||
|
if (!systemId) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
await this.updateEtasForSystemIfTTL(systemId);
|
||||||
|
|
||||||
|
if (!this.cache?.etasForStopId || !this.cache.etasForStopId[stopId]) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.cache.etasForStopId[stopId];
|
||||||
|
}
|
||||||
|
|
||||||
|
public async updateEtasForSystemIfTTL(systemId: string) {
|
||||||
|
// TODO: check if TTL
|
||||||
|
try {
|
||||||
|
const stops = await this.getStopsBySystemId(systemId);
|
||||||
|
await Promise.all(stops.map(async (stop) => {
|
||||||
|
const params = {
|
||||||
|
eta: "3",
|
||||||
|
stopIds: stop.id,
|
||||||
|
};
|
||||||
|
|
||||||
|
const query = new URLSearchParams(params).toString();
|
||||||
|
const response = await fetch(`${baseUrl}?${query}`, {
|
||||||
|
method: "GET",
|
||||||
|
});
|
||||||
|
const json = await response.json();
|
||||||
|
|
||||||
|
if (json.ETAs && json.ETAs[stop.id]) {
|
||||||
|
if (!this.cache.etasForStopId) {
|
||||||
|
this.cache.etasForStopId = {};
|
||||||
|
}
|
||||||
|
this.cache.etasForStopId[stop.id] = [];
|
||||||
|
|
||||||
|
// This is technically incorrect, the entire shuttle cache
|
||||||
|
// should not be reset like this
|
||||||
|
// TODO: restore normal cache behavior
|
||||||
|
this.cache.etasForShuttleId = {};
|
||||||
|
|
||||||
|
// Continue with the parsing
|
||||||
|
json.ETAs[stop.id].forEach((jsonEta: any) => {
|
||||||
|
// Update cache
|
||||||
|
if (!this.cache.etasForStopId) {
|
||||||
|
this.cache.etasForStopId = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.cache.etasForShuttleId) {
|
||||||
|
this.cache.etasForShuttleId = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: create cache abstraction to deal with possibly undefined properties
|
||||||
|
|
||||||
|
const shuttleId: string = jsonEta.busId;
|
||||||
|
if (!this.cache.etasForShuttleId[shuttleId]) {
|
||||||
|
this.cache.etasForShuttleId[shuttleId] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const eta: IEta = {
|
||||||
|
secondsRemaining: jsonEta.secondsSpent,
|
||||||
|
shuttleId: `${shuttleId}`,
|
||||||
|
stopId: stop.id,
|
||||||
|
millisecondsSinceEpoch: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
this.cache.etasForStopId[stop.id].push(eta);
|
||||||
|
this.cache.etasForShuttleId[shuttleId].push(eta);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: migrate rest of logic over to this class
|
||||||
|
public async getOrderedStopByRouteAndStopId(routeId: string, stopId: string): Promise<| null> {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getOrderedStopsByRouteId(routeId: string): Promise<[]> {
|
||||||
|
return Promise.resolve([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getOrderedStopsByStopId(stopId: string): Promise<[]> {
|
||||||
|
return Promise.resolve([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getRouteById(routeId: string): Promise<| null> {
|
||||||
|
return Promise.resolve(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getRoutesBySystemId(systemId: string): Promise<[]> {
|
||||||
|
return Promise.resolve([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getShuttleById(shuttleId: string): Promise<IShuttle | null> {
|
||||||
|
if (!this.cache.shuttleByShuttleId) return null;
|
||||||
|
let shuttle = this.cache.shuttleByShuttleId[shuttleId];
|
||||||
|
if (!shuttle) return null;
|
||||||
|
|
||||||
|
// Call getShuttlesBySystemId to update the data if not TTL
|
||||||
|
await this.updateShuttlesForSystemIfTTL(shuttle.systemId);
|
||||||
|
|
||||||
|
shuttle = this.cache.shuttleByShuttleId[shuttleId];
|
||||||
|
if (!shuttle) return null;
|
||||||
|
return shuttle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getShuttlesByRouteId(routeId: string): Promise<[]> {
|
||||||
|
return Promise.resolve([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getShuttlesBySystemId(systemId: string): Promise<IShuttle[]> {
|
||||||
|
return Promise.resolve([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async updateShuttlesForSystemIfTTL(systemId: string) {
|
||||||
|
// TODO: check if TTL
|
||||||
|
try {
|
||||||
|
// TODO: update shuttlesByRouteId
|
||||||
|
// Update shuttleByShuttleId, shuttlesBySystemId
|
||||||
|
const params = {
|
||||||
|
getBuses: "2"
|
||||||
|
};
|
||||||
|
|
||||||
|
const formDataJsonObject = {
|
||||||
|
"s0": systemId,
|
||||||
|
"sA": "1"
|
||||||
|
};
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.set("json", JSON.stringify(formDataJsonObject));
|
||||||
|
|
||||||
|
const query = new URLSearchParams(params).toString();
|
||||||
|
const response = await fetch(`${baseUrl}?${query}`, {
|
||||||
|
method: "POST",
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
const json = await response.json();
|
||||||
|
|
||||||
|
if (json.buses && json.buses["-1"] === undefined) {
|
||||||
|
const jsonBuses = Object.values(json.buses).map((busesArr: any) => {
|
||||||
|
return busesArr[0];
|
||||||
|
});
|
||||||
|
|
||||||
|
// Store shuttles by system, with the additional side effect that
|
||||||
|
// shuttleByShuttleId is updated
|
||||||
|
const shuttles = await Promise.all(jsonBuses.map(async (jsonBus: any) => {
|
||||||
|
const constructedShuttle: IShuttle = {
|
||||||
|
name: jsonBus.bus,
|
||||||
|
coordinates: {
|
||||||
|
latitude: parseFloat(jsonBus.latitude),
|
||||||
|
longitude: parseFloat(jsonBus.longitude),
|
||||||
|
},
|
||||||
|
routeId: jsonBus.routeId,
|
||||||
|
systemId: systemId,
|
||||||
|
id: `${jsonBus.busId}`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.cache.shuttleByShuttleId) {
|
||||||
|
this.cache.shuttleByShuttleId[jsonBus.busId] = constructedShuttle;
|
||||||
|
}
|
||||||
|
|
||||||
|
return constructedShuttle;
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (this.cache.shuttlesBySystemId) {
|
||||||
|
this.cache.shuttlesBySystemId[systemId] = shuttles;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn(`No shuttle data available for system ID ${systemId} and JSON output
|
||||||
|
${json}`);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getStopById(stopId: string): Promise<IStop | null> {
|
||||||
|
if (!this.cache.stopByStopId) return null;
|
||||||
|
const oldStop = this.cache.stopByStopId[stopId];
|
||||||
|
if (!oldStop) return null;
|
||||||
|
|
||||||
|
await this.updateStopsForSystemIdIfTTL(oldStop.systemId);
|
||||||
|
|
||||||
|
const newStop = this.cache.stopByStopId[stopId];
|
||||||
|
if (!newStop) return null;
|
||||||
|
|
||||||
|
return newStop;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getStopsBySystemId(systemId: string): Promise<IStop[]> {
|
||||||
|
await this.updateStopsForSystemIdIfTTL(systemId);
|
||||||
|
|
||||||
|
if (!this.cache.stopsBySystemId || this.cache.stopsBySystemId[systemId]) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return this.cache.stopsBySystemId[systemId];
|
||||||
|
}
|
||||||
|
|
||||||
|
public async updateStopsForSystemIdIfTTL(systemId: string) {
|
||||||
|
// TODO: check if TTL
|
||||||
|
|
||||||
|
try {
|
||||||
|
const params = {
|
||||||
|
getStops: "2",
|
||||||
|
};
|
||||||
|
|
||||||
|
const formDataJsonObject = {
|
||||||
|
"s0": systemId,
|
||||||
|
"sA": 1
|
||||||
|
};
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.set("json", JSON.stringify(formDataJsonObject));
|
||||||
|
|
||||||
|
const query = new URLSearchParams(params).toString();
|
||||||
|
const response = await fetch(`${baseUrl}?${query}`, {
|
||||||
|
method: "POST",
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
const json = await response.json();
|
||||||
|
|
||||||
|
// TODO: update polyline data
|
||||||
|
// TODO: update ordered stop data
|
||||||
|
|
||||||
|
if (json.stops) {
|
||||||
|
const jsonStops = Object.values(json.stops);
|
||||||
|
|
||||||
|
await Promise.all(jsonStops.map(async (stop: any) => {
|
||||||
|
const constructedStop: IStop = {
|
||||||
|
name: stop.name,
|
||||||
|
id: stop.id,
|
||||||
|
systemId: systemId,
|
||||||
|
coordinates: {
|
||||||
|
latitude: parseFloat(stop.latitude),
|
||||||
|
longitude: parseFloat(stop.longitude),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.cache.stopsBySystemId) {
|
||||||
|
this.cache.stopsBySystemId[systemId]?.push(constructedStop);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.cache.stopByStopId) {
|
||||||
|
this.cache.stopByStopId[constructedStop.id] = constructedStop;
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getSystemById(systemId: string): Promise<ISystem| null> {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getSystems(): Promise<[]> {
|
||||||
|
return Promise.resolve([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
37
src/repositories/GetterRepository.ts
Normal file
37
src/repositories/GetterRepository.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { IEta, IOrderedStop, IRoute, IShuttle, IStop, ISystem } from "../entities/entities";
|
||||||
|
|
||||||
|
export interface GetterRepository {
|
||||||
|
getSystems(): Promise<ISystem[]>;
|
||||||
|
getSystemById(systemId: string): Promise<ISystem | null>;
|
||||||
|
|
||||||
|
getStopsBySystemId(systemId: string): Promise<IStop[]>;
|
||||||
|
getStopById(stopId: string): Promise<IStop | null>;
|
||||||
|
|
||||||
|
getRoutesBySystemId(systemId: string): Promise<IRoute[]>;
|
||||||
|
getRouteById(routeId: string): Promise<IRoute | null>;
|
||||||
|
|
||||||
|
getShuttlesBySystemId(systemId: string): Promise<IShuttle[]>;
|
||||||
|
getShuttleById(shuttleId: string): Promise<IShuttle | null>;
|
||||||
|
getShuttlesByRouteId(routeId: string): Promise<IShuttle[]>;
|
||||||
|
|
||||||
|
getEtasForShuttleId(shuttleId: string): Promise<IEta[]>;
|
||||||
|
getEtasForStopId(stopId: string): Promise<IEta[]>;
|
||||||
|
|
||||||
|
getEtaForShuttleAndStopId(shuttleId: string, stopId: string): Promise<IEta | null>;
|
||||||
|
|
||||||
|
getOrderedStopByRouteAndStopId(routeId: string, stopId: string): Promise<IOrderedStop | null>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get ordered stops with the given stop ID.
|
||||||
|
* Returns an empty array if no ordered stops found.
|
||||||
|
* @param stopId
|
||||||
|
*/
|
||||||
|
getOrderedStopsByStopId(stopId: string): Promise<IOrderedStop[]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get ordered stops with the given route ID.
|
||||||
|
* Returns an empty array if no ordered stops found.
|
||||||
|
* @param routeId
|
||||||
|
*/
|
||||||
|
getOrderedStopsByRouteId(routeId: string): Promise<IOrderedStop[]>;
|
||||||
|
}
|
||||||
21
src/repositories/GetterSetterRepository.ts
Normal file
21
src/repositories/GetterSetterRepository.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
// If types match closely, we can use TypeScript "casting"
|
||||||
|
// to convert from data repo to GraphQL schema
|
||||||
|
|
||||||
|
import { GetterRepository } from "./GetterRepository";
|
||||||
|
import { IEta, IOrderedStop, IRoute, IShuttle, IStop, ISystem } from "../entities/entities";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GetterRepository interface for data derived from Passio API.
|
||||||
|
* The repository is not designed to have write locks in place.
|
||||||
|
* Objects passed from/to the repository should be treated
|
||||||
|
* as disposable.
|
||||||
|
*/
|
||||||
|
export interface GetterSetterRepository extends GetterRepository {
|
||||||
|
// Setter methods
|
||||||
|
addOrUpdateSystem(system: ISystem): Promise<void>;
|
||||||
|
addOrUpdateRoute(route: IRoute): Promise<void>;
|
||||||
|
addOrUpdateShuttle(shuttle: IShuttle): Promise<void>;
|
||||||
|
addOrUpdateStop(stop: IStop): Promise<void>;
|
||||||
|
addOrUpdateOrderedStop(orderedStop: IOrderedStop): Promise<void>;
|
||||||
|
addOrUpdateEta(eta: IEta): Promise<void>;
|
||||||
|
}
|
||||||
@@ -1,11 +1,20 @@
|
|||||||
import { IEntityWithId, IEta, IOrderedStop, IRoute, IShuttle, IStop, ISystem, Repository } from "./repository";
|
import { GetterSetterRepository } from "./GetterSetterRepository";
|
||||||
|
import {
|
||||||
|
IEntityWithId,
|
||||||
|
IEta,
|
||||||
|
IOrderedStop,
|
||||||
|
IRoute,
|
||||||
|
IShuttle,
|
||||||
|
IStop,
|
||||||
|
ISystem
|
||||||
|
} from "../entities/entities";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An unoptimized in memory repository.
|
* An unoptimized in memory repository.
|
||||||
* (I would optimize it with actual data structures, but I'm
|
* (I would optimize it with actual data structures, but I'm
|
||||||
* switching to another data store later anyways)
|
* switching to another data store later anyways)
|
||||||
*/
|
*/
|
||||||
export class UnoptimizedInMemoryRepository implements Repository {
|
export class UnoptimizedInMemoryRepository implements GetterSetterRepository {
|
||||||
private systems: ISystem[] = [];
|
private systems: ISystem[] = [];
|
||||||
private stops: IStop[] = [];
|
private stops: IStop[] = [];
|
||||||
private routes: IRoute[] = [];
|
private routes: IRoute[] = [];
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
// If types match closely, we can use TypeScript "casting"
|
|
||||||
// to convert from data repo to GraphQL schema
|
|
||||||
|
|
||||||
export interface IEntityWithId {
|
|
||||||
id: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ISystem extends IEntityWithId {
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ICoordinates {
|
|
||||||
latitude: number;
|
|
||||||
longitude: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IRoute extends IEntityWithId {
|
|
||||||
name: string;
|
|
||||||
color: string;
|
|
||||||
polylineCoordinates: ICoordinates[];
|
|
||||||
systemId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IStop extends IEntityWithId {
|
|
||||||
name: string;
|
|
||||||
systemId: string;
|
|
||||||
coordinates: ICoordinates;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IShuttle extends IEntityWithId {
|
|
||||||
coordinates: ICoordinates;
|
|
||||||
name: string;
|
|
||||||
routeId: string;
|
|
||||||
systemId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IEta {
|
|
||||||
secondsRemaining: number;
|
|
||||||
shuttleId: string;
|
|
||||||
stopId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IOrderedStop {
|
|
||||||
nextStop?: IOrderedStop;
|
|
||||||
previousStop?: IOrderedStop;
|
|
||||||
routeId: string;
|
|
||||||
stopId: string;
|
|
||||||
position: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Repository interface for data derived from Passio API.
|
|
||||||
* The repository is not designed to have write locks in place.
|
|
||||||
* Objects passed from/to the repository should be treated
|
|
||||||
* as disposable.
|
|
||||||
*/
|
|
||||||
export interface Repository {
|
|
||||||
// Getter methods
|
|
||||||
|
|
||||||
getSystems(): Promise<ISystem[]>;
|
|
||||||
getSystemById(systemId: string): Promise<ISystem | null>;
|
|
||||||
|
|
||||||
getStopsBySystemId(systemId: string): Promise<IStop[]>;
|
|
||||||
getStopById(stopId: string): Promise<IStop | null>;
|
|
||||||
|
|
||||||
getRoutesBySystemId(systemId: string): Promise<IRoute[]>;
|
|
||||||
getRouteById(routeId: string): Promise<IRoute | null>;
|
|
||||||
|
|
||||||
getShuttlesBySystemId(systemId: string): Promise<IShuttle[]>;
|
|
||||||
getShuttleById(shuttleId: string): Promise<IShuttle | null>;
|
|
||||||
getShuttlesByRouteId(routeId: string): Promise<IShuttle[]>;
|
|
||||||
|
|
||||||
getEtasForShuttleId(shuttleId: string): Promise<IEta[]>;
|
|
||||||
getEtasForStopId(stopId: string): Promise<IEta[]>;
|
|
||||||
|
|
||||||
getEtaForShuttleAndStopId(shuttleId: string, stopId: string): Promise<IEta | null>;
|
|
||||||
|
|
||||||
getOrderedStopByRouteAndStopId(routeId: string, stopId: string): Promise<IOrderedStop | null>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get ordered stops with the given stop ID.
|
|
||||||
* Returns an empty array if no ordered stops found.
|
|
||||||
* @param stopId
|
|
||||||
*/
|
|
||||||
getOrderedStopsByStopId(stopId: string): Promise<IOrderedStop[]>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get ordered stops with the given route ID.
|
|
||||||
* Returns an empty array if no ordered stops found.
|
|
||||||
* @param routeId
|
|
||||||
*/
|
|
||||||
getOrderedStopsByRouteId(routeId: string): Promise<IOrderedStop[]>;
|
|
||||||
|
|
||||||
// Setter methods
|
|
||||||
addOrUpdateSystem(system: ISystem): Promise<void>;
|
|
||||||
addOrUpdateRoute(route: IRoute): Promise<void>;
|
|
||||||
addOrUpdateShuttle(shuttle: IShuttle): Promise<void>;
|
|
||||||
addOrUpdateStop(stop: IStop): Promise<void>;
|
|
||||||
addOrUpdateOrderedStop(orderedStop: IOrderedStop): Promise<void>;
|
|
||||||
addOrUpdateEta(eta: IEta): Promise<void>;
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Coordinates, Eta, OrderedStop, Resolvers, Route, Shuttle, Stop, System } from "./generated/graphql";
|
import { Coordinates, Eta, OrderedStop, Resolvers, Route, Shuttle, Stop, System } from "./generated/graphql";
|
||||||
import { ServerContext } from "./serverContext";
|
import { ServerContext } from "./ServerContext";
|
||||||
|
|
||||||
export const resolvers: Resolvers<ServerContext> = {
|
export const resolvers: Resolvers<ServerContext> = {
|
||||||
Query: {
|
Query: {
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
import { Repository } from "./repository";
|
|
||||||
|
|
||||||
export interface ServerContext {
|
|
||||||
repository: Repository
|
|
||||||
}
|
|
||||||
632
test/repositories/ApiBasedRepositoryTests.test.ts
Normal file
632
test/repositories/ApiBasedRepositoryTests.test.ts
Normal file
@@ -0,0 +1,632 @@
|
|||||||
|
import { beforeEach, describe, expect, jest, test } from "@jest/globals";
|
||||||
|
import {
|
||||||
|
ApiBasedRepository,
|
||||||
|
ApiBasedRepositoryCache,
|
||||||
|
ApiBasedRepositoryMillisecondTTLs
|
||||||
|
} from "../../src/repositories/ApiBasedRepository";
|
||||||
|
import { IEta, IShuttle, IStop } from "../../src/entities/entities";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the global fetch function to return a specific object.
|
||||||
|
* @param obj
|
||||||
|
*/
|
||||||
|
function updateGlobalFetchMockJson(obj: any) {
|
||||||
|
// @ts-ignore
|
||||||
|
global.fetch = jest.fn(() => {
|
||||||
|
return Promise.resolve({
|
||||||
|
json: () => Promise.resolve(obj)
|
||||||
|
})
|
||||||
|
}) as jest.Mock;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset the global fetch function mock's JSON to return an empty object.
|
||||||
|
* @param obj
|
||||||
|
*/
|
||||||
|
function resetGlobalFetchMockJson() {
|
||||||
|
updateGlobalFetchMockJson({})
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
resetGlobalFetchMockJson();
|
||||||
|
})
|
||||||
|
|
||||||
|
// Snapshots taken from the Passio GO! API
|
||||||
|
const genericEtaDataByStopId = {
|
||||||
|
"ETAs": {
|
||||||
|
"177666": [
|
||||||
|
{
|
||||||
|
"OOS": 0,
|
||||||
|
"busName": "08",
|
||||||
|
"distance": 1,
|
||||||
|
"speed": 10.028535400123669,
|
||||||
|
"routeBlockId": "142270",
|
||||||
|
"actualRouteBlockId": "142270",
|
||||||
|
"arrived": null,
|
||||||
|
"eta": "10 min ",
|
||||||
|
"color": "#000000",
|
||||||
|
"bg": "#ffea3f",
|
||||||
|
"order": 0,
|
||||||
|
"dwell": null,
|
||||||
|
"stopsAmount": 2,
|
||||||
|
"secondsSpent": 587,
|
||||||
|
"etaR": "10",
|
||||||
|
"error": null,
|
||||||
|
"outdated": 0,
|
||||||
|
"routeId": "53966",
|
||||||
|
"serviceTime": "",
|
||||||
|
"scheduleTimes": [],
|
||||||
|
"goShowSchedule": 0,
|
||||||
|
"looping": "1",
|
||||||
|
"routeGroupId": "6703",
|
||||||
|
"busId": 5577,
|
||||||
|
"tripId": 751430,
|
||||||
|
"deviceId": 402840,
|
||||||
|
"created": "2025-01-07 15:00:09",
|
||||||
|
"routePointPosition": 6,
|
||||||
|
"routeStopPosition": 1,
|
||||||
|
"stopRoutePointPosition": 217,
|
||||||
|
"timezoneOffset": -10800,
|
||||||
|
"busLatLng": [
|
||||||
|
33.7933406,
|
||||||
|
-117.8539321
|
||||||
|
],
|
||||||
|
"busProjectionLatlng": {
|
||||||
|
"lat": 33.79331052666975,
|
||||||
|
"lng": -117.85392945849208
|
||||||
|
},
|
||||||
|
"busProjectionError": 3,
|
||||||
|
"stopId": "177666",
|
||||||
|
"theStop": {
|
||||||
|
"name": "Chapman Court",
|
||||||
|
"position": 3,
|
||||||
|
"userId": "263",
|
||||||
|
"routeStopId": "1348785",
|
||||||
|
"busId": 5577,
|
||||||
|
"routeName": "Red Route",
|
||||||
|
"shortName": null,
|
||||||
|
"routeId": "53966",
|
||||||
|
"stopId": "177666"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"OOS": 0,
|
||||||
|
"busName": "07",
|
||||||
|
"distance": 1,
|
||||||
|
"speed": 12.160256921380398,
|
||||||
|
"routeBlockId": "142270",
|
||||||
|
"actualRouteBlockId": "142270",
|
||||||
|
"arrived": null,
|
||||||
|
"eta": "11 min ",
|
||||||
|
"color": "#000000",
|
||||||
|
"bg": "#ffea3f",
|
||||||
|
"order": 0,
|
||||||
|
"dwell": null,
|
||||||
|
"stopsAmount": 2,
|
||||||
|
"secondsSpent": 635,
|
||||||
|
"etaR": "11",
|
||||||
|
"error": null,
|
||||||
|
"outdated": 0,
|
||||||
|
"routeId": "53966",
|
||||||
|
"serviceTime": "",
|
||||||
|
"scheduleTimes": [],
|
||||||
|
"goShowSchedule": 0,
|
||||||
|
"looping": "1",
|
||||||
|
"routeGroupId": "6703",
|
||||||
|
"busId": 5576,
|
||||||
|
"tripId": 751430,
|
||||||
|
"deviceId": 441065,
|
||||||
|
"created": "2025-01-07 15:00:10",
|
||||||
|
"routePointPosition": 448,
|
||||||
|
"routeStopPosition": 4,
|
||||||
|
"stopRoutePointPosition": 217,
|
||||||
|
"timezoneOffset": -10800,
|
||||||
|
"busLatLng": [
|
||||||
|
33.7933284,
|
||||||
|
-117.855132
|
||||||
|
],
|
||||||
|
"busProjectionLatlng": {
|
||||||
|
"lat": 33.79332033922653,
|
||||||
|
"lng": -117.85513217762522
|
||||||
|
},
|
||||||
|
"busProjectionError": 1,
|
||||||
|
"stopId": "177666",
|
||||||
|
"theStop": {
|
||||||
|
"name": "Chapman Court",
|
||||||
|
"position": 3,
|
||||||
|
"userId": "263",
|
||||||
|
"routeStopId": "1348785",
|
||||||
|
"busId": 5576,
|
||||||
|
"routeName": "Red Route",
|
||||||
|
"shortName": null,
|
||||||
|
"routeId": "53966",
|
||||||
|
"stopId": "177666"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const genericShuttleDataBySystemId = {
|
||||||
|
"alertCRC": "23c1b91c",
|
||||||
|
"buses": {
|
||||||
|
"402840": [
|
||||||
|
{
|
||||||
|
"deviceId": 402840,
|
||||||
|
"created": "08:24 PM",
|
||||||
|
"createdTime": "08:24 PM",
|
||||||
|
"paxLoad": 0,
|
||||||
|
"bus": "08",
|
||||||
|
"busId": 5577,
|
||||||
|
"userId": "263",
|
||||||
|
"routeBlockId": "142270",
|
||||||
|
"latitude": "33.791781800",
|
||||||
|
"longitude": "-117.858964600",
|
||||||
|
"calculatedCourse": "351.796001302109",
|
||||||
|
"outOfService": 0,
|
||||||
|
"more": "102",
|
||||||
|
"totalCap": 20,
|
||||||
|
"color": "#d62728",
|
||||||
|
"busName": "08",
|
||||||
|
"busType": "",
|
||||||
|
"routeId": "53966",
|
||||||
|
"route": "Red Route",
|
||||||
|
"outdated": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"404873": [
|
||||||
|
{
|
||||||
|
"deviceId": 404873,
|
||||||
|
"created": "08:24 PM",
|
||||||
|
"createdTime": "08:24 PM",
|
||||||
|
"paxLoad": 0,
|
||||||
|
"bus": "10",
|
||||||
|
"busId": 7105,
|
||||||
|
"userId": "263",
|
||||||
|
"routeBlockId": "142270",
|
||||||
|
"latitude": "33.789331300",
|
||||||
|
"longitude": "-117.888790600",
|
||||||
|
"calculatedCourse": "76.005762226701",
|
||||||
|
"outOfService": 0,
|
||||||
|
"more": "101",
|
||||||
|
"totalCap": 20,
|
||||||
|
"color": "#d62728",
|
||||||
|
"busName": "10",
|
||||||
|
"busType": "",
|
||||||
|
"routeId": "53966",
|
||||||
|
"route": "Red Route",
|
||||||
|
"outdated": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"421421": [
|
||||||
|
{
|
||||||
|
"deviceId": 421421,
|
||||||
|
"created": "08:24 PM",
|
||||||
|
"createdTime": "08:24 PM",
|
||||||
|
"paxLoad": 0,
|
||||||
|
"bus": "17",
|
||||||
|
"busId": 12502,
|
||||||
|
"userId": "263",
|
||||||
|
"routeBlockId": "142660",
|
||||||
|
"latitude": "33.790699500",
|
||||||
|
"longitude": "-117.890385500",
|
||||||
|
"calculatedCourse": "10.65684824528148",
|
||||||
|
"outOfService": 0,
|
||||||
|
"more": "101",
|
||||||
|
"totalCap": 32,
|
||||||
|
"color": "#bd9e39",
|
||||||
|
"busName": "17",
|
||||||
|
"busType": "Shuttle Bus",
|
||||||
|
"routeId": "54256",
|
||||||
|
"route": "Gold Route",
|
||||||
|
"outdated": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"441065": [
|
||||||
|
{
|
||||||
|
"deviceId": 441065,
|
||||||
|
"created": "08:19 PM",
|
||||||
|
"createdTime": "08:19 PM",
|
||||||
|
"paxLoad": 0,
|
||||||
|
"bus": "07",
|
||||||
|
"busId": 5576,
|
||||||
|
"userId": "263",
|
||||||
|
"routeBlockId": "142270",
|
||||||
|
"latitude": "33.793278900",
|
||||||
|
"longitude": "-117.852629400",
|
||||||
|
"calculatedCourse": "299.74488110904485",
|
||||||
|
"outOfService": 0,
|
||||||
|
"more": "22",
|
||||||
|
"totalCap": 20,
|
||||||
|
"color": "#d62728",
|
||||||
|
"busName": "07",
|
||||||
|
"busType": "",
|
||||||
|
"routeId": "53966",
|
||||||
|
"route": "Red Route",
|
||||||
|
"outdated": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"microtime": 0.023222923278808594,
|
||||||
|
"time": {
|
||||||
|
"263": "08:24 PM"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("getEtaForShuttleAndStopId", () => {
|
||||||
|
test("getEtaForShuttleAndStopId returns correct ETA data", async () => {
|
||||||
|
updateGlobalFetchMockJson(genericEtaDataByStopId);
|
||||||
|
|
||||||
|
const initialCache: ApiBasedRepositoryCache = {
|
||||||
|
etasForStopId: {
|
||||||
|
"177666": [
|
||||||
|
{
|
||||||
|
secondsRemaining: 587,
|
||||||
|
shuttleId: "5577",
|
||||||
|
stopId: "177666",
|
||||||
|
millisecondsSinceEpoch: Date.now(),
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const repository = new ApiBasedRepository(initialCache);
|
||||||
|
|
||||||
|
repository.getShuttleById = jest.fn(async () => {
|
||||||
|
const shuttle: IShuttle = {
|
||||||
|
id: "5577",
|
||||||
|
name: "08",
|
||||||
|
coordinates: {
|
||||||
|
latitude: 33.7933406,
|
||||||
|
longitude: -117.8539321,
|
||||||
|
},
|
||||||
|
routeId: "53966",
|
||||||
|
systemId: "1",
|
||||||
|
};
|
||||||
|
return shuttle;
|
||||||
|
});
|
||||||
|
|
||||||
|
repository.updateEtasForSystemIfTTL = jest.fn(async () => {
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await repository.getEtaForShuttleAndStopId("5577", "177666");
|
||||||
|
|
||||||
|
expect(result?.secondsRemaining).toEqual(587);
|
||||||
|
expect(result?.millisecondsSinceEpoch).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("getEtaForShuttleAndStopId returns null if API call is invalid and cache is empty", async () => {
|
||||||
|
const repository = new ApiBasedRepository();
|
||||||
|
const result = await repository.getEtaForShuttleAndStopId("5577", "177666");
|
||||||
|
|
||||||
|
expect(result).toEqual(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getEtasForShuttleId", () => {
|
||||||
|
test("getEtasForShuttleId returns correct ETA data", async () => {
|
||||||
|
updateGlobalFetchMockJson(genericEtaDataByStopId);
|
||||||
|
|
||||||
|
const initialCache: ApiBasedRepositoryCache = {
|
||||||
|
etasForStopId: {},
|
||||||
|
etasForShuttleId: {
|
||||||
|
"5577": [
|
||||||
|
{
|
||||||
|
secondsRemaining: 587,
|
||||||
|
shuttleId: "5577",
|
||||||
|
stopId: "177666",
|
||||||
|
millisecondsSinceEpoch: Date.now(),
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const ttls: ApiBasedRepositoryMillisecondTTLs = {
|
||||||
|
etasForShuttleId: 100000,
|
||||||
|
etasForStopId: 100000,
|
||||||
|
};
|
||||||
|
|
||||||
|
const repository = new ApiBasedRepository(initialCache, ttls);
|
||||||
|
repository.updateEtasForSystemIfTTL = jest.fn(async () => {
|
||||||
|
});
|
||||||
|
repository.getShuttleById = jest.fn(async () => {
|
||||||
|
const shuttle: IShuttle = {
|
||||||
|
id: "5577",
|
||||||
|
name: "08",
|
||||||
|
coordinates: {
|
||||||
|
latitude: 33.7933406,
|
||||||
|
longitude: -117.8539321,
|
||||||
|
},
|
||||||
|
routeId: "53966",
|
||||||
|
systemId: "1",
|
||||||
|
};
|
||||||
|
return shuttle;
|
||||||
|
});
|
||||||
|
const result = await repository.getEtasForShuttleId("5577");
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
expect(result).toEqual(initialCache.etasForShuttleId["5577"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("getEtasForShuttleId returns empty array if no data available", async () => {
|
||||||
|
const repository = new ApiBasedRepository();
|
||||||
|
repository.updateEtasForSystemIfTTL = jest.fn(async () => {
|
||||||
|
});
|
||||||
|
const result = await repository.getEtasForShuttleId("5577");
|
||||||
|
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getEtasForStopId", () => {
|
||||||
|
test("getEtasForStopId returns correct ETA data", async () => {
|
||||||
|
// Because I'm testing updateEtasForSystemIfTTL separately,
|
||||||
|
// stub it out here
|
||||||
|
|
||||||
|
updateGlobalFetchMockJson(genericEtaDataByStopId);
|
||||||
|
|
||||||
|
const initialCache: ApiBasedRepositoryCache = {
|
||||||
|
etasForStopId: {
|
||||||
|
"177666": [
|
||||||
|
{
|
||||||
|
secondsRemaining: 587,
|
||||||
|
shuttleId: "5577",
|
||||||
|
stopId: "177666",
|
||||||
|
millisecondsSinceEpoch: Date.now(),
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
etasForShuttleId: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
const ttls: ApiBasedRepositoryMillisecondTTLs = {
|
||||||
|
etasForShuttleId: 100000,
|
||||||
|
etasForStopId: 100000,
|
||||||
|
};
|
||||||
|
|
||||||
|
const repository = new ApiBasedRepository(initialCache, ttls);
|
||||||
|
repository.getStopById = jest.fn(async () => {
|
||||||
|
const stop: IStop = {
|
||||||
|
name: "Chapman Court",
|
||||||
|
systemId: "1",
|
||||||
|
id: "177666",
|
||||||
|
coordinates: {
|
||||||
|
latitude: 33.796796,
|
||||||
|
longitude: -117.889293
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return stop;
|
||||||
|
});
|
||||||
|
repository.updateEtasForSystemIfTTL = jest.fn(async () => {
|
||||||
|
});
|
||||||
|
const result = await repository.getEtasForStopId("177666");
|
||||||
|
|
||||||
|
expect(result).toEqual(initialCache.etasForStopId!["177666"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("getEtasForStopId returns empty array if no data available", async () => {
|
||||||
|
const repository = new ApiBasedRepository();
|
||||||
|
repository.updateEtasForSystemIfTTL = jest.fn(async () => {
|
||||||
|
});
|
||||||
|
const result = await repository.getEtasForShuttleId("5577");
|
||||||
|
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("updateEtasForSystemIfTTL", () => {
|
||||||
|
// test("updateEtasForSystemIfTTL does nothing if data is not TTL", async () => {
|
||||||
|
// updateGlobalFetchMockJson(genericEtaDataByStopId);
|
||||||
|
//
|
||||||
|
// // If ETA data is not TTL, then don't do anything
|
||||||
|
// const expectedEta: IEta = {
|
||||||
|
// secondsRemaining: 587,
|
||||||
|
// shuttleId: "5577",
|
||||||
|
// stopId: "177666",
|
||||||
|
// millisecondsSinceEpoch: Date.now() - 1000,
|
||||||
|
// };
|
||||||
|
//
|
||||||
|
// const initialCache: ApiBasedRepositoryCache = {
|
||||||
|
// etasForShuttleId: {
|
||||||
|
// "5577": [
|
||||||
|
// expectedEta,
|
||||||
|
// ],
|
||||||
|
// },
|
||||||
|
// etasForStopId: {
|
||||||
|
// "177666": [
|
||||||
|
// expectedEta,
|
||||||
|
// ],
|
||||||
|
// },
|
||||||
|
// stopsBySystemId: {
|
||||||
|
// "1": [
|
||||||
|
// {
|
||||||
|
// systemId: "1",
|
||||||
|
// millisecondsSinceEpoch: Date.now() - 1000,
|
||||||
|
// name: "Chapman Court",
|
||||||
|
// id: "177666",
|
||||||
|
// coordinates: {
|
||||||
|
// latitude: 33.796796,
|
||||||
|
// longitude: -117.889293
|
||||||
|
// },
|
||||||
|
// }
|
||||||
|
// ],
|
||||||
|
// },
|
||||||
|
// };
|
||||||
|
//
|
||||||
|
// const ttls: ApiBasedRepositoryMillisecondTTLs = {
|
||||||
|
// etasForShuttleId: 100000,
|
||||||
|
// etasForStopId: 100000,
|
||||||
|
// };
|
||||||
|
//
|
||||||
|
// const repository = new ApiBasedRepository(initialCache, ttls);
|
||||||
|
// await repository.updateEtasForSystemIfTTL("1");
|
||||||
|
//
|
||||||
|
// const updatedResult = await repository.getEtaForShuttleAndStopId(
|
||||||
|
// "5577",
|
||||||
|
// "177666",
|
||||||
|
// );
|
||||||
|
// expect(updatedResult?.millisecondsSinceEpoch).toEqual(expectedEta.millisecondsSinceEpoch);
|
||||||
|
// });
|
||||||
|
|
||||||
|
test("updateEtasForSystemIfTTL updates all ETA data if data is TTL", async () => {
|
||||||
|
updateGlobalFetchMockJson(genericEtaDataByStopId);
|
||||||
|
|
||||||
|
const sampleStop: IStop = {
|
||||||
|
name: "Chapman Court",
|
||||||
|
systemId: "1",
|
||||||
|
id: "177666",
|
||||||
|
coordinates: {
|
||||||
|
latitude: 33.796796,
|
||||||
|
longitude: -117.889293
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const repository = new ApiBasedRepository();
|
||||||
|
repository.getStopsBySystemId = jest.fn(async () => {
|
||||||
|
return [
|
||||||
|
sampleStop
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
repository.getStopById = jest.fn(async () => {
|
||||||
|
return sampleStop;
|
||||||
|
});
|
||||||
|
|
||||||
|
await repository.updateEtasForSystemIfTTL("1");
|
||||||
|
|
||||||
|
const updatedResult = await repository.getEtasForStopId("177666");
|
||||||
|
expect(updatedResult.length).toEqual(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getShuttleById", () => {
|
||||||
|
test("getShuttleById returns null if unseeded cache", async () => {
|
||||||
|
updateGlobalFetchMockJson(genericShuttleDataBySystemId);
|
||||||
|
|
||||||
|
const initialCache: ApiBasedRepositoryCache = {};
|
||||||
|
const repository = new ApiBasedRepository(initialCache);
|
||||||
|
|
||||||
|
const shuttle = await repository.getShuttleById("5577");
|
||||||
|
expect(shuttle).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("getShuttleById returns data if present", async () => {
|
||||||
|
updateGlobalFetchMockJson(genericShuttleDataBySystemId);
|
||||||
|
|
||||||
|
const initialCacheShuttle = {
|
||||||
|
coordinates: {
|
||||||
|
latitude: 33.7917818,
|
||||||
|
longitude: -117.8589646,
|
||||||
|
},
|
||||||
|
name: "08",
|
||||||
|
routeId: "53966",
|
||||||
|
systemId: "1",
|
||||||
|
id: "5577",
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialCache: ApiBasedRepositoryCache = {
|
||||||
|
shuttleByShuttleId: {
|
||||||
|
"5577": initialCacheShuttle
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const ttls: ApiBasedRepositoryMillisecondTTLs = {
|
||||||
|
shuttleByShuttleId: 1000,
|
||||||
|
};
|
||||||
|
|
||||||
|
const repository = new ApiBasedRepository(initialCache, ttls);
|
||||||
|
repository.updateStopsForSystemIdIfTTL = jest.fn(async () => {
|
||||||
|
})
|
||||||
|
|
||||||
|
const shuttle = await repository.getShuttleById("5577");
|
||||||
|
expect(shuttle).toEqual(initialCacheShuttle);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: enable when implemented
|
||||||
|
// describe("getShuttlesBySystemId", () => {
|
||||||
|
// test("getShuttlesBySystemId returns old data if not expired", async () => {
|
||||||
|
// updateGlobalFetchMockJson(genericShuttleDataBySystemId);
|
||||||
|
//
|
||||||
|
// const initialCacheShuttle = {
|
||||||
|
// coordinates: {
|
||||||
|
// latitude: 33.791781,
|
||||||
|
// longitude: -117.8589646,
|
||||||
|
// },
|
||||||
|
// name: "08",
|
||||||
|
// routeId: "53966",
|
||||||
|
// systemId: "1",
|
||||||
|
// id: "5577",
|
||||||
|
// millisecondsSinceEpoch: Date.now() - 1000,
|
||||||
|
// };
|
||||||
|
//
|
||||||
|
// const initialCache: ApiBasedRepositoryCache = {
|
||||||
|
// shuttlesBySystemId: {
|
||||||
|
// "1": [
|
||||||
|
// initialCacheShuttle,
|
||||||
|
// ]
|
||||||
|
// },
|
||||||
|
// shuttleByShuttleId: {
|
||||||
|
// "5577": initialCacheShuttle,
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
//
|
||||||
|
// const ttls: ApiBasedRepositoryMillisecondTTLs = {
|
||||||
|
// shuttleByShuttleId: 100000,
|
||||||
|
// shuttlesBySystemId: 100000,
|
||||||
|
// };
|
||||||
|
//
|
||||||
|
// const repository = new ApiBasedRepository(initialCache, ttls);
|
||||||
|
// const shuttles = await repository.getShuttlesBySystemId("1");
|
||||||
|
// expect(shuttles.length).toEqual(1);
|
||||||
|
// expect(shuttles[0].id).toEqual(initialCacheShuttle.id);
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// test("getShuttlesBySystemId returns fresh data if expired", async () => {
|
||||||
|
// updateGlobalFetchMockJson(genericShuttleDataBySystemId);
|
||||||
|
//
|
||||||
|
// // TODO: move construction of shuttle into method
|
||||||
|
// const initialCacheShuttle = {
|
||||||
|
// coordinates: {
|
||||||
|
// latitude: 33.791781,
|
||||||
|
// longitude: -117.8589646,
|
||||||
|
// },
|
||||||
|
// name: "08",
|
||||||
|
// routeId: "53966",
|
||||||
|
// systemId: "1",
|
||||||
|
// id: "5577",
|
||||||
|
// millisecondsSinceEpoch: Date.now() - 100000,
|
||||||
|
// };
|
||||||
|
//
|
||||||
|
// const initialCache: ApiBasedRepositoryCache = {
|
||||||
|
// shuttlesBySystemId: {
|
||||||
|
// "1": [
|
||||||
|
// initialCacheShuttle,
|
||||||
|
// ]
|
||||||
|
// },
|
||||||
|
// shuttleByShuttleId: {
|
||||||
|
// "5577": initialCacheShuttle,
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
//
|
||||||
|
// const ttls: ApiBasedRepositoryMillisecondTTLs = {
|
||||||
|
// shuttleByShuttleId: 1000,
|
||||||
|
// shuttlesBySystemId: 1000,
|
||||||
|
// };
|
||||||
|
//
|
||||||
|
// const repository = new ApiBasedRepository(initialCache, ttls);
|
||||||
|
// const shuttles = await repository.getShuttlesBySystemId("1");
|
||||||
|
//
|
||||||
|
// expect(shuttles.length).toEqual(1);
|
||||||
|
// expect(shuttles[0].id).toEqual("5577");
|
||||||
|
// expect(shuttles[0].millisecondsSinceEpoch).not.toEqual(initialCacheShuttle.millisecondsSinceEpoch);
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// test("getShuttlesBySystemId returns fresh data if no seeded data", async () => {
|
||||||
|
// updateGlobalFetchMockJson(genericShuttleDataBySystemId);
|
||||||
|
//
|
||||||
|
// const repository = new ApiBasedRepository();
|
||||||
|
// const shuttles = await repository.getShuttlesBySystemId("1");
|
||||||
|
//
|
||||||
|
// expect(shuttles.length).toEqual(1);
|
||||||
|
// });
|
||||||
|
// });
|
||||||
Reference in New Issue
Block a user