Merge pull request #63 from brendan-ch/feat/parking-statistics-data-in-graphql

feat/parking-statistics-data-in-graphql
This commit is contained in:
2025-07-21 19:49:24 -04:00
committed by GitHub
39 changed files with 283 additions and 102 deletions

View File

@@ -9,3 +9,5 @@ APNS_BUNDLE_ID=
APNS_PRIVATE_KEY=
PARKING_LOGGING_INTERVAL_MS=
PARKING_HISTORICAL_AVERAGE_MINIMUM_INTERVAL=
PARKING_HISTORICAL_AVERAGE_MAXIMUM_TIMESPAN=

View File

@@ -110,7 +110,7 @@ const config = {
// reporters: undefined,
// Automatically reset mock state before every test
// resetMocks: false,
resetMocks: true,
// Reset the module registry before running each individual test
// resetModules: false,
@@ -119,7 +119,7 @@ const config = {
// resolver: undefined,
// Automatically restore mock state and implementation before every test
// restoreMocks: false,
restoreMocks: true,
// The root directory that Jest should scan for tests and modules within
// rootDir: undefined,

View File

@@ -30,6 +30,21 @@ type ParkingStructure {
coordinates: Coordinates!
address: String!
updatedTime: DateTime
systemId: ID!
historicalAverages(input: HistoricalParkingAverageQueryInput): [HistoricalParkingAverageQueryResult!]
}
type HistoricalParkingAverageQueryResult {
from: DateTime!
to: DateTime!
averageSpotsAvailable: Float!
}
input HistoricalParkingAverageQueryInput {
from: DateTime!
to: DateTime!
intervalMs: Int!
}
type Route {

View File

@@ -10,6 +10,7 @@ import { RouteResolvers } from "./resolvers/RouteResolvers";
import { MutationResolvers } from "./resolvers/MutationResolvers";
import { ParkingSystemResolvers } from "./resolvers/ParkingSystemResolvers";
import { DateTime } from "./scalars/DateTime";
import { ParkingStructureResolvers } from "./resolvers/ParkingStructureResolvers";
export const MergedResolvers: Resolvers<ServerContext> = {
...QueryResolvers,
@@ -21,5 +22,6 @@ export const MergedResolvers: Resolvers<ServerContext> = {
...OrderedStopResolvers,
...EtaResolvers,
...MutationResolvers,
...ParkingStructureResolvers,
DateTime: DateTime,
};

View File

@@ -0,0 +1,5 @@
export const PARKING_LOGGING_INTERVAL_MS = 10000;
export const PARKING_HISTORICAL_AVERAGE_MINIMUM_INTERVAL = 1000;
export const PARKING_HISTORICAL_AVERAGE_MAXIMUM_TIMESPAN = 60000 * 60 * 24;

View File

@@ -1,19 +1,19 @@
import { ETANotificationScheduler } from "../notifications/schedulers/ETANotificationScheduler";
import { TimedApiBasedRepositoryLoader } from "../loaders/TimedApiBasedRepositoryLoader";
import { UnoptimizedInMemoryShuttleRepository } from "../repositories/UnoptimizedInMemoryShuttleRepository";
import { RedisNotificationRepository } from "../repositories/RedisNotificationRepository";
import { NotificationRepository } from "../repositories/NotificationRepository";
import { ShuttleGetterSetterRepository } from "../repositories/ShuttleGetterSetterRepository";
import { InMemoryNotificationRepository } from "../repositories/InMemoryNotificationRepository";
import { UnoptimizedInMemoryShuttleRepository } from "../repositories/shuttle/UnoptimizedInMemoryShuttleRepository";
import { RedisNotificationRepository } from "../repositories/notifications/RedisNotificationRepository";
import { NotificationRepository } from "../repositories/notifications/NotificationRepository";
import { ShuttleGetterSetterRepository } from "../repositories/shuttle/ShuttleGetterSetterRepository";
import { InMemoryNotificationRepository } from "../repositories/notifications/InMemoryNotificationRepository";
import { AppleNotificationSender } from "../notifications/senders/AppleNotificationSender";
import { ApiBasedShuttleRepositoryLoader } from "../loaders/shuttle/ApiBasedShuttleRepositoryLoader";
import { ParkingGetterSetterRepository } from "../repositories/ParkingGetterSetterRepository";
import { InMemoryParkingRepository } from "../repositories/InMemoryParkingRepository";
import { ParkingGetterSetterRepository } from "../repositories/parking/ParkingGetterSetterRepository";
import { InMemoryParkingRepository } from "../repositories/parking/InMemoryParkingRepository";
import {
buildParkingRepositoryLoaderIfExists,
ParkingRepositoryLoaderBuilderArguments
} from "../loaders/parking/buildParkingRepositoryLoaderIfExists";
import { RedisParkingRepository } from "../repositories/RedisParkingRepository";
import { RedisParkingRepository } from "../repositories/parking/RedisParkingRepository";
export interface InterchangeSystemBuilderArguments {
name: string;

View File

@@ -9,7 +9,7 @@ export interface IParkingStructure extends IEntityWithTimestamp, IEntityWithId {
}
export interface IParkingStructureTimestampRecord {
timestampMs: number;
timestampMs: Date;
id: string;
spotsAvailable: number;
}

11
src/environment.ts Normal file
View File

@@ -0,0 +1,11 @@
export const PARKING_LOGGING_INTERVAL_MS = process.env.PARKING_LOGGING_INTERVAL_MS
? parseInt(process.env.PARKING_LOGGING_INTERVAL_MS)
: 600000; // Every 10 minutes
export const PARKING_HISTORICAL_AVERAGE_MINIMUM_INTERVAL = process.env.PARKING_HISTORICAL_AVERAGE_MINIMUM_INTERVAL
? parseInt(process.env.PARKING_HISTORICAL_AVERAGE_MINIMUM_INTERVAL)
: 60000 * 60 * 2;
export const PARKING_HISTORICAL_AVERAGE_MAXIMUM_TIMESPAN = process.env.PARKING_HISTORICAL_AVERAGE_MAXIMUM_TIMESPAN
? parseInt(process.env.PARKING_HISTORICAL_AVERAGE_MAXIMUM_TIMESPAN)
: 60000 * 60 * 24;

View File

@@ -1,5 +1,5 @@
import { ParkingRepositoryLoader } from "./ParkingRepositoryLoader";
import { ParkingGetterSetterRepository } from "../../repositories/ParkingGetterSetterRepository";
import { ParkingGetterSetterRepository } from "../../repositories/parking/ParkingGetterSetterRepository";
import { createHash } from "node:crypto";
import { ApiResponseError } from "../ApiResponseError";
import { IParkingStructure } from "../../entities/ParkingRepositoryEntities";

View File

@@ -1,4 +1,4 @@
import { ParkingGetterSetterRepository } from "../../repositories/ParkingGetterSetterRepository";
import { ParkingGetterSetterRepository } from "../../repositories/parking/ParkingGetterSetterRepository";
import { ChapmanApiBasedParkingRepositoryLoader } from "./ChapmanApiBasedParkingRepositoryLoader";
export interface ParkingRepositoryLoaderBuilderArguments {

View File

@@ -1,4 +1,4 @@
import { ParkingGetterSetterRepository } from "../../repositories/ParkingGetterSetterRepository";
import { ParkingGetterSetterRepository } from "../../repositories/parking/ParkingGetterSetterRepository";
import { IParkingStructure } from "../../entities/ParkingRepositoryEntities";
const parkingStructures: IParkingStructure[] = [

View File

@@ -1,4 +1,4 @@
import { ShuttleGetterSetterRepository } from "../../repositories/ShuttleGetterSetterRepository";
import { ShuttleGetterSetterRepository } from "../../repositories/shuttle/ShuttleGetterSetterRepository";
import { IEta, IRoute, IShuttle, IStop } from "../../entities/ShuttleRepositoryEntities";
import { ShuttleRepositoryLoader } from "./ShuttleRepositoryLoader";
import { IEntityWithId } from "../../entities/SharedEntities";

View File

@@ -1,6 +1,6 @@
// Mock data
import { IEta, IOrderedStop, IRoute, IShuttle, IStop } from "../../entities/ShuttleRepositoryEntities";
import { ShuttleGetterSetterRepository } from "../../repositories/ShuttleGetterSetterRepository";
import { ShuttleGetterSetterRepository } from "../../repositories/shuttle/ShuttleGetterSetterRepository";
import { supportedIntegrationTestSystems } from "../supportedIntegrationTestSystems";
const redRoutePolylineCoordinates = [

View File

@@ -1,11 +1,11 @@
import { ShuttleGetterRepository } from "../../repositories/ShuttleGetterRepository";
import { ShuttleGetterRepository } from "../../repositories/shuttle/ShuttleGetterRepository";
import { IEta } from "../../entities/ShuttleRepositoryEntities";
import { AppleNotificationSender, NotificationAlertArguments } from "../senders/AppleNotificationSender";
import {
NotificationRepository,
ScheduledNotification
} from "../../repositories/NotificationRepository";
import { InMemoryNotificationRepository } from "../../repositories/InMemoryNotificationRepository";
} from "../../repositories/notifications/NotificationRepository";
import { InMemoryNotificationRepository } from "../../repositories/notifications/InMemoryNotificationRepository";
export class ETANotificationScheduler {
public static readonly defaultSecondsThresholdForNotificationToFire = 180;

View File

@@ -16,7 +16,7 @@ export interface NotificationAlertArguments {
export class AppleNotificationSender {
private apnsToken: string | undefined = undefined;
private _lastRefreshedTimeMs: number | undefined = undefined;
private _lastRefreshedTimeMs: Date | undefined = undefined;
constructor(
private shouldActuallySendNotifications = true,
@@ -34,13 +34,13 @@ export class AppleNotificationSender {
}
}
get lastRefreshedTimeMs(): number | undefined {
get lastRefreshedTimeMs(): Date | undefined {
return this._lastRefreshedTimeMs;
}
private lastReloadedTimeForAPNsIsTooRecent() {
const thirtyMinutesMs = 1800000;
return this._lastRefreshedTimeMs && Date.now() - this._lastRefreshedTimeMs < thirtyMinutesMs;
return this._lastRefreshedTimeMs && Date.now() - this._lastRefreshedTimeMs.getTime() < thirtyMinutesMs;
}
public reloadAPNsTokenIfTimePassed() {
@@ -70,7 +70,7 @@ export class AppleNotificationSender {
algorithm: "ES256",
header: tokenHeader
});
this._lastRefreshedTimeMs = nowMs;
this._lastRefreshedTimeMs = new Date(nowMs);
}
/**

View File

@@ -1,3 +0,0 @@
export const PARKING_LOGGING_INTERVAL_MS = process.env.PARKING_LOGGING_INTERVAL_MS
? parseInt(process.env.PARKING_LOGGING_INTERVAL_MS)
: 600000; // Every 10 minutes

View File

@@ -5,7 +5,7 @@ import {
NotificationRepository,
ScheduledNotification
} from "./NotificationRepository";
import { TupleKey } from "../types/TupleKey";
import { TupleKey } from "../../types/TupleKey";
type DeviceIdSecondsThresholdAssociation = { [key: string]: number };

View File

@@ -1,4 +1,4 @@
import { TupleKey } from '../types/TupleKey';
import { TupleKey } from '../../types/TupleKey';
import {
Listener,
NotificationEvent,
@@ -6,7 +6,7 @@ import {
NotificationRepository,
ScheduledNotification
} from "./NotificationRepository";
import { BaseRedisRepository } from "./BaseRedisRepository";
import { BaseRedisRepository } from "../BaseRedisRepository";
export class RedisNotificationRepository extends BaseRedisRepository implements NotificationRepository {
private listeners: Listener[] = [];

View File

@@ -2,10 +2,10 @@ import { ParkingGetterSetterRepository } from "./ParkingGetterSetterRepository";
import {
IParkingStructure,
IParkingStructureTimestampRecord
} from "../entities/ParkingRepositoryEntities";
import { HistoricalParkingAverageQueryResult, ParkingStructureCountOptions } from "./ParkingGetterRepository";
import { CircularQueue } from "../types/CircularQueue";
import { PARKING_LOGGING_INTERVAL_MS } from "./ParkingRepositoryConstants";
} from "../../entities/ParkingRepositoryEntities";
import { HistoricalParkingAverageQueryResult, HistoricalParkingAverageQueryArguments } from "./ParkingGetterRepository";
import { CircularQueue } from "../../types/CircularQueue";
import { PARKING_LOGGING_INTERVAL_MS } from "../../environment";
// If every 10 minutes, two weeks of data (6x per hour * 24x per day * 7x per week * 2)
export const MAX_NUM_ENTRIES = 2016;
@@ -63,7 +63,7 @@ export class InMemoryParkingRepository implements ParkingGetterSetterRepository
return null;
};
getHistoricalAveragesOfParkingStructureCounts = async (id: string, options: ParkingStructureCountOptions): Promise<HistoricalParkingAverageQueryResult[]> => {
getHistoricalAveragesOfParkingStructureCounts = async (id: string, options: HistoricalParkingAverageQueryArguments): Promise<HistoricalParkingAverageQueryResult[]> => {
const queue = this.historicalData.get(id);
if (!queue || queue.size() === 0) {
return [];
@@ -80,7 +80,7 @@ export class InMemoryParkingRepository implements ParkingGetterSetterRepository
private createTimestampRecord = (structure: IParkingStructure, timestampMs: number): IParkingStructureTimestampRecord => ({
id: structure.id,
spotsAvailable: structure.spotsAvailable,
timestampMs,
timestampMs: new Date(timestampMs),
});
private ensureHistoricalDataExists = (structureId: string): void => {
@@ -90,7 +90,7 @@ export class InMemoryParkingRepository implements ParkingGetterSetterRepository
};
private addRecordToHistoricalData = (structureId: string, record: IParkingStructureTimestampRecord): void => {
const sortingCallback = (a: IParkingStructureTimestampRecord, b: IParkingStructureTimestampRecord) => a.timestampMs - b.timestampMs;
const sortingCallback = (a: IParkingStructureTimestampRecord, b: IParkingStructureTimestampRecord) => a.timestampMs.getTime() - b.timestampMs.getTime();
this.historicalData.get(structureId)?.appendWithSorting(record, sortingCallback);
};
@@ -107,15 +107,16 @@ export class InMemoryParkingRepository implements ParkingGetterSetterRepository
private calculateAveragesFromRecords = (
records: IParkingStructureTimestampRecord[],
options: ParkingStructureCountOptions
options: HistoricalParkingAverageQueryArguments
): HistoricalParkingAverageQueryResult[] => {
const results: HistoricalParkingAverageQueryResult[] = [];
const { startUnixEpochMs, endUnixEpochMs, intervalMs } = options;
const { from, to, intervalMs } = options;
let currentIntervalStart = startUnixEpochMs;
let currentIntervalStart = from.getTime();
const endTime = to.getTime();
while (currentIntervalStart < endUnixEpochMs) {
const currentIntervalEnd = Math.min(currentIntervalStart + intervalMs, endUnixEpochMs);
while (currentIntervalStart < endTime) {
const currentIntervalEnd = Math.min(currentIntervalStart + intervalMs, endTime);
const recordsInInterval = this.getRecordsInTimeRange(records, currentIntervalStart, currentIntervalEnd);
if (recordsInInterval.length > 0) {
@@ -135,7 +136,7 @@ export class InMemoryParkingRepository implements ParkingGetterSetterRepository
endMs: number
): IParkingStructureTimestampRecord[] => {
return records.filter(record =>
record.timestampMs >= startMs && record.timestampMs < endMs
record.timestampMs.getTime() >= startMs && record.timestampMs.getTime() < endMs
);
};
@@ -148,8 +149,8 @@ export class InMemoryParkingRepository implements ParkingGetterSetterRepository
const averageSpotsAvailable = totalSpotsAvailable / records.length;
return {
fromUnixEpochMs: fromMs,
toUnixEpochMs: toMs,
from: new Date(fromMs),
to: new Date(toMs),
averageSpotsAvailable
};
};

View File

@@ -1,14 +1,14 @@
import { IParkingStructure } from "../entities/ParkingRepositoryEntities";
import { IParkingStructure } from "../../entities/ParkingRepositoryEntities";
export interface ParkingStructureCountOptions {
startUnixEpochMs: number;
endUnixEpochMs: number;
export interface HistoricalParkingAverageQueryArguments {
from: Date;
to: Date;
intervalMs: number;
}
export interface HistoricalParkingAverageQueryResult {
fromUnixEpochMs: number;
toUnixEpochMs: number;
from: Date;
to: Date;
averageSpotsAvailable: number;
}
@@ -22,5 +22,5 @@ export interface ParkingGetterRepository {
* @param id
* @param options
*/
getHistoricalAveragesOfParkingStructureCounts(id: string, options: ParkingStructureCountOptions): Promise<HistoricalParkingAverageQueryResult[]>;
getHistoricalAveragesOfParkingStructureCounts(id: string, options: HistoricalParkingAverageQueryArguments): Promise<HistoricalParkingAverageQueryResult[]>;
}

View File

@@ -1,4 +1,4 @@
import { IParkingStructure } from "../entities/ParkingRepositoryEntities";
import { IParkingStructure } from "../../entities/ParkingRepositoryEntities";
import { ParkingGetterRepository } from "./ParkingGetterRepository";
export interface ParkingGetterSetterRepository extends ParkingGetterRepository {

View File

@@ -1,8 +1,8 @@
import { ParkingGetterSetterRepository } from "./ParkingGetterSetterRepository";
import { IParkingStructure } from "../entities/ParkingRepositoryEntities";
import { HistoricalParkingAverageQueryResult, ParkingStructureCountOptions } from "./ParkingGetterRepository";
import { BaseRedisRepository } from "./BaseRedisRepository";
import { PARKING_LOGGING_INTERVAL_MS } from "./ParkingRepositoryConstants";
import { IParkingStructure } from "../../entities/ParkingRepositoryEntities";
import { HistoricalParkingAverageQueryResult, HistoricalParkingAverageQueryArguments } from "./ParkingGetterRepository";
import { BaseRedisRepository } from "../BaseRedisRepository";
import { PARKING_LOGGING_INTERVAL_MS } from "../../environment";
export type ParkingStructureID = string;
@@ -75,7 +75,7 @@ export class RedisParkingRepository extends BaseRedisRepository implements Parki
return null;
};
getHistoricalAveragesOfParkingStructureCounts = async (id: string, options: ParkingStructureCountOptions): Promise<HistoricalParkingAverageQueryResult[]> => {
getHistoricalAveragesOfParkingStructureCounts = async (id: string, options: HistoricalParkingAverageQueryArguments): Promise<HistoricalParkingAverageQueryResult[]> => {
return this.calculateAveragesFromRecords(id, options);
};
@@ -157,17 +157,18 @@ export class RedisParkingRepository extends BaseRedisRepository implements Parki
private calculateAveragesFromRecords = async (
id: string,
options: ParkingStructureCountOptions
options: HistoricalParkingAverageQueryArguments
): Promise<HistoricalParkingAverageQueryResult[]> => {
const keys = this.createRedisKeys(id);
const { startUnixEpochMs, endUnixEpochMs, intervalMs } = options;
const { from, to, intervalMs } = options;
const results: HistoricalParkingAverageQueryResult[] = [];
let currentIntervalStart = startUnixEpochMs;
let currentIntervalStart = from.getTime();
const endTime = to.getTime();
while (currentIntervalStart < endTime) {
const currentIntervalEnd = Math.min(currentIntervalStart + intervalMs, endTime);
while (currentIntervalStart < endUnixEpochMs) {
const currentIntervalEnd = Math.min(currentIntervalStart + intervalMs, endUnixEpochMs);
try {
const aggregationResult = await this.redisClient.sendCommand([
'TS.RANGE',
@@ -182,8 +183,8 @@ export class RedisParkingRepository extends BaseRedisRepository implements Parki
if (aggregationResult && aggregationResult.length > 0) {
const [, averageValue] = aggregationResult[0];
results.push({
fromUnixEpochMs: currentIntervalStart,
toUnixEpochMs: currentIntervalEnd,
from: new Date(currentIntervalStart),
to: new Date(currentIntervalEnd),
averageSpotsAvailable: parseFloat(averageValue)
});
}

View File

@@ -1,4 +1,4 @@
import { IEta, IOrderedStop, IRoute, IShuttle, IStop } from "../entities/ShuttleRepositoryEntities";
import { IEta, IOrderedStop, IRoute, IShuttle, IStop } from "../../entities/ShuttleRepositoryEntities";
/**
* Shuttle getter repository to be linked to a system.

View File

@@ -2,7 +2,7 @@
// to convert from data repo to GraphQL schema
import { ShuttleGetterRepository } from "./ShuttleGetterRepository";
import { IEta, IOrderedStop, IRoute, IShuttle, IStop } from "../entities/ShuttleRepositoryEntities";
import { IEta, IOrderedStop, IRoute, IShuttle, IStop } from "../../entities/ShuttleRepositoryEntities";
/**
* ShuttleGetterRepository interface for data derived from Passio API.

View File

@@ -1,6 +1,6 @@
import { ShuttleGetterSetterRepository } from "./ShuttleGetterSetterRepository";
import { IEta, IOrderedStop, IRoute, IShuttle, IStop } from "../entities/ShuttleRepositoryEntities";
import { IEntityWithId } from "../entities/SharedEntities";
import { IEta, IOrderedStop, IRoute, IShuttle, IStop } from "../../entities/ShuttleRepositoryEntities";
import { IEntityWithId } from "../../entities/SharedEntities";
/**
* An unoptimized in memory repository.

View File

@@ -3,7 +3,7 @@ import { ServerContext } from "../ServerContext";
import {
ETANotificationScheduler,
} from "../notifications/schedulers/ETANotificationScheduler";
import { ScheduledNotification } from "../repositories/NotificationRepository";
import { ScheduledNotification } from "../repositories/notifications/NotificationRepository";
import { InterchangeSystem } from "../entities/InterchangeSystem";
async function temp_findMatchingSystemBasedOnShuttleId(context: ServerContext, args: Omit<MutationScheduleNotificationArgs, "input"> & {

View File

@@ -0,0 +1,59 @@
import { Resolvers } from "../generated/graphql";
import { ServerContext } from "../ServerContext";
import { HistoricalParkingAverageQueryArguments } from "../repositories/parking/ParkingGetterRepository";
import { GraphQLError } from "graphql/error";
import {
PARKING_HISTORICAL_AVERAGE_MAXIMUM_TIMESPAN,
PARKING_HISTORICAL_AVERAGE_MINIMUM_INTERVAL
} from "../environment";
export const ParkingStructureResolvers: Resolvers<ServerContext> = {
ParkingStructure: {
historicalAverages: async (parent, args, contextValue, _info) => {
/**
* @param errorMessage
*/
function throwBadUserInputError(errorMessage: string) {
throw new GraphQLError(errorMessage, {
extensions: {
code: 'BAD_USER_INPUT',
}
});
}
const system = contextValue.findSystemById(parent.systemId);
if (!args.input?.intervalMs) {
throwBadUserInputError('No interval provided');
return null;
}
const queryArguments: HistoricalParkingAverageQueryArguments = {
from: new Date(args.input.from),
intervalMs: args.input.intervalMs,
to: new Date(args.input.to),
}
if (Number.isNaN(queryArguments.from.getTime()
|| Number.isNaN(queryArguments.to.getTime()))) {
throwBadUserInputError('One or more incorrect dates provided');
}
if (queryArguments.from.getTime() > queryArguments.to.getTime()) {
throwBadUserInputError("`from` date can't be greater than the `to` date");
}
// Limit queries for improved performance
if (queryArguments.to.getTime() - queryArguments.from.getTime() > PARKING_HISTORICAL_AVERAGE_MAXIMUM_TIMESPAN) {
throwBadUserInputError('Maximum timespan exceeded');
}
if (queryArguments.intervalMs < PARKING_HISTORICAL_AVERAGE_MINIMUM_INTERVAL) {
throwBadUserInputError('Provided interval is less than minimum interval');
}
const parkingAverages = await system?.parkingRepository?.getHistoricalAveragesOfParkingStructureCounts(parent.id, queryArguments);
if (!parkingAverages) {
return null;
}
return parkingAverages;
}
}
}

View File

@@ -11,9 +11,15 @@ export const ParkingSystemResolvers: Resolvers<ServerContext> = {
const parkingRepository = system.parkingRepository;
if (!parkingRepository) return [];
return await parkingRepository.getParkingStructures();
const parkingStructures = await parkingRepository.getParkingStructures();
return parkingStructures.map((structure) => {
return {
...structure,
systemId: parent.systemId
};
});
},
parkingStructure: async (parent, args, contextValue, info) => {
parkingStructure: async (parent, args, contextValue, _info) => {
if (!args.id) return null;
const system = contextValue.findSystemById(parent.systemId);
@@ -23,7 +29,11 @@ export const ParkingSystemResolvers: Resolvers<ServerContext> = {
const parkingRepository = system.parkingRepository;
if (!parkingRepository) return null;
return await parkingRepository.getParkingStructureById(args.id);
const parkingStructure = await parkingRepository.getParkingStructureById(args.id);
return parkingStructure ? {
...parkingStructure,
systemId: parent.systemId,
} : null;
},
}
}

View File

@@ -1,19 +1,16 @@
import { afterEach, beforeAll, beforeEach, describe, expect, it, jest } from "@jest/globals";
import { TimedApiBasedRepositoryLoader } from "../../src/loaders/TimedApiBasedRepositoryLoader";
import { resetGlobalFetchMockJson } from "../testHelpers/fetchMockHelpers";
import { UnoptimizedInMemoryShuttleRepository } from "../../src/repositories/UnoptimizedInMemoryShuttleRepository";
import { UnoptimizedInMemoryShuttleRepository } from "../../src/repositories/shuttle/UnoptimizedInMemoryShuttleRepository";
import { ApiBasedShuttleRepositoryLoader } from "../../src/loaders/shuttle/ApiBasedShuttleRepositoryLoader";
describe("TimedApiBasedRepositoryLoader", () => {
let timedLoader: TimedApiBasedRepositoryLoader;
let spies: any;
beforeAll(() => {
beforeEach(() => {
jest.useFakeTimers();
jest.spyOn(global, "setTimeout");
});
beforeEach(() => {
resetGlobalFetchMockJson();
const mockLoader = new ApiBasedShuttleRepositoryLoader(

View File

@@ -2,7 +2,7 @@ import { beforeEach, describe, expect, it, jest } from "@jest/globals";
import {
ChapmanApiBasedParkingRepositoryLoader
} from "../../../src/loaders/parking/ChapmanApiBasedParkingRepositoryLoader";
import { InMemoryParkingRepository } from "../../../src/repositories/InMemoryParkingRepository";
import { InMemoryParkingRepository } from "../../../src/repositories/parking/InMemoryParkingRepository";
import {
resetGlobalFetchMockJson,
updateGlobalFetchMockJson,

View File

@@ -1,6 +1,6 @@
import { afterEach, beforeEach, describe, expect, it, jest } from "@jest/globals";
import { ApiBasedShuttleRepositoryLoader } from "../../../src/loaders/shuttle/ApiBasedShuttleRepositoryLoader";
import { UnoptimizedInMemoryShuttleRepository } from "../../../src/repositories/UnoptimizedInMemoryShuttleRepository";
import { UnoptimizedInMemoryShuttleRepository } from "../../../src/repositories/shuttle/UnoptimizedInMemoryShuttleRepository";
import { fetchRouteDataSuccessfulResponse } from "../../jsonSnapshots/fetchRouteData/fetchRouteDataSuccessfulResponse";
import {
fetchStopAndPolylineDataSuccessfulResponse

View File

@@ -1,11 +1,11 @@
import { beforeEach, describe, expect, it, jest } from "@jest/globals";
import { ETANotificationScheduler } from "../../../src/notifications/schedulers/ETANotificationScheduler";
import { UnoptimizedInMemoryShuttleRepository } from "../../../src/repositories/UnoptimizedInMemoryShuttleRepository";
import { UnoptimizedInMemoryShuttleRepository } from "../../../src/repositories/shuttle/UnoptimizedInMemoryShuttleRepository";
import { IEta, IShuttle, IStop } from "../../../src/entities/ShuttleRepositoryEntities";
import { addMockShuttleToRepository, addMockStopToRepository } from "../../testHelpers/repositorySetupHelpers";
import { AppleNotificationSender } from "../../../src/notifications/senders/AppleNotificationSender";
import { InMemoryNotificationRepository } from "../../../src/repositories/InMemoryNotificationRepository";
import { NotificationRepository } from "../../../src/repositories/NotificationRepository";
import { InMemoryNotificationRepository } from "../../../src/repositories/notifications/InMemoryNotificationRepository";
import { NotificationRepository } from "../../../src/repositories/notifications/NotificationRepository";
jest.mock("http2");
jest.mock("../../../src/notifications/senders/AppleNotificationSender");

View File

@@ -1,7 +1,7 @@
import { afterEach, beforeEach, describe, expect, it, jest } from "@jest/globals";
import { InMemoryNotificationRepository } from "../../src/repositories/InMemoryNotificationRepository";
import { NotificationEvent, NotificationRepository } from "../../src/repositories/NotificationRepository";
import { RedisNotificationRepository } from "../../src/repositories/RedisNotificationRepository";
import { InMemoryNotificationRepository } from "../../src/repositories/notifications/InMemoryNotificationRepository";
import { NotificationEvent, NotificationRepository } from "../../src/repositories/notifications/NotificationRepository";
import { RedisNotificationRepository } from "../../src/repositories/notifications/RedisNotificationRepository";
interface RepositoryHolder {
name: string;

View File

@@ -1,9 +1,9 @@
import { afterEach, beforeEach, describe, expect, it, jest } from "@jest/globals";
import { InMemoryParkingRepository, } from "../../src/repositories/InMemoryParkingRepository";
import { InMemoryParkingRepository, } from "../../src/repositories/parking/InMemoryParkingRepository";
import { IParkingStructure } from "../../src/entities/ParkingRepositoryEntities";
import { ParkingStructureCountOptions } from "../../src/repositories/ParkingGetterRepository";
import { ParkingGetterSetterRepository } from "../../src/repositories/ParkingGetterSetterRepository";
import { RedisParkingRepository } from "../../src/repositories/RedisParkingRepository";
import { HistoricalParkingAverageQueryArguments } from "../../src/repositories/parking/ParkingGetterRepository";
import { ParkingGetterSetterRepository } from "../../src/repositories/parking/ParkingGetterSetterRepository";
import { RedisParkingRepository } from "../../src/repositories/parking/RedisParkingRepository";
interface RepositoryHolder {
name: string;
@@ -151,9 +151,9 @@ describe.each(repositoryImplementations)('$name', (holder) => {
describe("getHistoricalAveragesOfParkingStructureCounts", () => {
it("should return empty array for non-existent structure or no data", async () => {
const options: ParkingStructureCountOptions = {
startUnixEpochMs: 1000,
endUnixEpochMs: 2000,
const options: HistoricalParkingAverageQueryArguments = {
from: new Date(1000),
to: new Date(2000),
intervalMs: 500
};
@@ -163,7 +163,7 @@ describe.each(repositoryImplementations)('$name', (holder) => {
expect(await repository.getHistoricalAveragesOfParkingStructureCounts(testStructure.id, options)).toEqual([]);
});
it("should calculate averages for intervals with manual historical data", async () => {
it("should calculate average for one single large interval", async () => {
// Set logging interval to 0 so every update creates historical data
repository.setLoggingInterval(0);
@@ -182,9 +182,9 @@ describe.each(repositoryImplementations)('$name', (holder) => {
}
const now = Date.now();
const options: ParkingStructureCountOptions = {
startUnixEpochMs: now - 10000, // Look back 10 seconds
endUnixEpochMs: now + 10000, // Look forward 10 seconds
const options: HistoricalParkingAverageQueryArguments = {
from: new Date(now - 10000), // Look back 10 seconds
to: new Date(now + 10000), // Look forward 10 seconds
intervalMs: 20000 // Single large interval
};
@@ -193,8 +193,10 @@ describe.each(repositoryImplementations)('$name', (holder) => {
// Should have at least some historical data
expect(result.length).toEqual(1);
if (result.length > 0) {
expect(result[0]).toHaveProperty('fromUnixEpochMs');
expect(result[0]).toHaveProperty('toUnixEpochMs');
expect(result[0]).toHaveProperty('from');
expect(result[0]).toHaveProperty('to');
expect(result[0].from).toBeInstanceOf(Date);
expect(result[0].to).toBeInstanceOf(Date);
expect(result[0]).toHaveProperty('averageSpotsAvailable');
expect(result[0].averageSpotsAvailable).toBeCloseTo(52.5);
}

View File

@@ -1,5 +1,5 @@
import { beforeEach, describe, expect, jest, test } from "@jest/globals";
import { UnoptimizedInMemoryShuttleRepository } from "../../src/repositories/UnoptimizedInMemoryShuttleRepository";
import { UnoptimizedInMemoryShuttleRepository } from "../../src/repositories/shuttle/UnoptimizedInMemoryShuttleRepository";
import {
generateMockEtas,
generateMockOrderedStops,

View File

@@ -0,0 +1,79 @@
import { beforeEach, describe, expect, it, jest } from "@jest/globals";
import { setupTestServerContext, setupTestServerHolder } from "../testHelpers/apolloTestServerHelpers";
import { InterchangeSystem } from "../../src/entities/InterchangeSystem";
import { generateParkingStructures } from "../testHelpers/mockDataGenerators";
import { HistoricalParkingAverageQueryInput } from "../../src/generated/graphql";
import assert = require("node:assert");
jest.mock("../../src/environment");
describe("ParkingStructureResolver", () => {
const holder = setupTestServerHolder();
const context = setupTestServerContext();
let mockSystem: InterchangeSystem;
beforeEach(async () => {
mockSystem = context.systems[0];
jest.useRealTimers();
});
describe("historicalAverages", () => {
const query = `
query GetParkingStructureHistoricalAverages(
$systemId: ID!,
$parkingStructureId: ID!,
$historicalAverageInput: HistoricalParkingAverageQueryInput!
) {
system(id: $systemId) {
parkingSystem {
parkingStructure(id: $parkingStructureId) {
historicalAverages(input: $historicalAverageInput) {
from
to
averageSpotsAvailable
}
}
}
}
}
`;
it("gets data for historical averages", async () => {
jest.useFakeTimers();
jest.setSystemTime(new Date());
const parkingStructure = generateParkingStructures()[0];
parkingStructure.spotsAvailable = parkingStructure.capacity;
mockSystem.parkingRepository?.setLoggingInterval(100);
// Simulate repeated updates
for (let i = 0; i < 6; i += 1) {
jest.setSystemTime(new Date(Date.now() + 1000));
parkingStructure.spotsAvailable = parkingStructure.spotsAvailable - 100;
await mockSystem.parkingRepository?.addOrUpdateParkingStructure(parkingStructure);
}
const historicalAverageInput: HistoricalParkingAverageQueryInput = {
from: new Date(Date.now() - 5000).getTime(),
intervalMs: 2000,
to: new Date().getTime(),
};
const response = await holder.testServer.executeOperation({
query,
variables: {
systemId: mockSystem.id,
parkingStructureId: parkingStructure.id,
historicalAverageInput,
},
}, {
contextValue: context,
});
assert(response.body.kind === 'single');
expect(response.body.singleResult.errors).toBeUndefined();
const historicalAverages = (response.body.singleResult.data as any).system.parkingSystem.parkingStructure.historicalAverages;
expect(historicalAverages).toHaveLength(3);
});
});
});

View File

@@ -6,7 +6,7 @@ import {
} from "../testHelpers/apolloTestServerHelpers";
import assert = require("node:assert");
import { addMockShuttleToRepository, addMockStopToRepository } from "../testHelpers/repositorySetupHelpers";
import { ScheduledNotification } from "../../src/repositories/NotificationRepository";
import { ScheduledNotification } from "../../src/repositories/notifications/NotificationRepository";
// See Apollo documentation for integration test guide
// https://www.apollographql.com/docs/apollo-server/testing/testing

View File

@@ -4,7 +4,7 @@ import {
generateMockShuttles,
generateMockStops,
} from "./mockDataGenerators";
import { ShuttleGetterSetterRepository } from "../../src/repositories/ShuttleGetterSetterRepository";
import { ShuttleGetterSetterRepository } from "../../src/repositories/shuttle/ShuttleGetterSetterRepository";
export async function addMockRouteToRepository(repository: ShuttleGetterSetterRepository, systemId: string) {
const mockRoutes = generateMockRoutes();