mirror of
https://github.com/brendan-ch/project-inter-server.git
synced 2026-04-16 23:40:32 +00:00
Merge pull request #63 from brendan-ch/feat/parking-statistics-data-in-graphql
feat/parking-statistics-data-in-graphql
This commit is contained in:
@@ -9,3 +9,5 @@ APNS_BUNDLE_ID=
|
||||
APNS_PRIVATE_KEY=
|
||||
|
||||
PARKING_LOGGING_INTERVAL_MS=
|
||||
PARKING_HISTORICAL_AVERAGE_MINIMUM_INTERVAL=
|
||||
PARKING_HISTORICAL_AVERAGE_MAXIMUM_TIMESPAN=
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
5
src/__mocks__/environment.ts
Normal file
5
src/__mocks__/environment.ts
Normal 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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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
11
src/environment.ts
Normal 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;
|
||||
@@ -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";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ParkingGetterSetterRepository } from "../../repositories/ParkingGetterSetterRepository";
|
||||
import { ParkingGetterSetterRepository } from "../../repositories/parking/ParkingGetterSetterRepository";
|
||||
import { ChapmanApiBasedParkingRepositoryLoader } from "./ChapmanApiBasedParkingRepositoryLoader";
|
||||
|
||||
export interface ParkingRepositoryLoaderBuilderArguments {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ParkingGetterSetterRepository } from "../../repositories/ParkingGetterSetterRepository";
|
||||
import { ParkingGetterSetterRepository } from "../../repositories/parking/ParkingGetterSetterRepository";
|
||||
import { IParkingStructure } from "../../entities/ParkingRepositoryEntities";
|
||||
|
||||
const parkingStructures: IParkingStructure[] = [
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
NotificationRepository,
|
||||
ScheduledNotification
|
||||
} from "./NotificationRepository";
|
||||
import { TupleKey } from "../types/TupleKey";
|
||||
import { TupleKey } from "../../types/TupleKey";
|
||||
|
||||
type DeviceIdSecondsThresholdAssociation = { [key: string]: number };
|
||||
|
||||
@@ -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[] = [];
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
@@ -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[]>;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { IParkingStructure } from "../entities/ParkingRepositoryEntities";
|
||||
import { IParkingStructure } from "../../entities/ParkingRepositoryEntities";
|
||||
import { ParkingGetterRepository } from "./ParkingGetterRepository";
|
||||
|
||||
export interface ParkingGetterSetterRepository extends ParkingGetterRepository {
|
||||
@@ -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)
|
||||
});
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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"> & {
|
||||
|
||||
59
src/resolvers/ParkingStructureResolvers.ts
Normal file
59
src/resolvers/ParkingStructureResolvers.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
79
test/resolvers/ParkingStructureResolverTests.test.ts
Normal file
79
test/resolvers/ParkingStructureResolverTests.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user