Add sorting to all data types

- Add name-based sorting for entities with names
- Add order-based sorting for ordered stops
- Add "seconds remaining" based sorting for ETAs
- Add tests to check sorting
This commit is contained in:
2025-09-30 15:23:50 -07:00
parent c244a4b037
commit 415b461308
12 changed files with 262 additions and 19 deletions

View File

@@ -12,7 +12,10 @@ export const ParkingSystemResolvers: Resolvers<ServerContext> = {
if (!parkingRepository) return []; if (!parkingRepository) return [];
const parkingStructures = await parkingRepository.getParkingStructures(); const parkingStructures = await parkingRepository.getParkingStructures();
return parkingStructures.map((structure) => { return parkingStructures
.slice()
.sort((a, b) => a.name.localeCompare(b.name))
.map((structure) => {
return { return {
...structure, ...structure,
systemId: parent.systemId systemId: parent.systemId

View File

@@ -7,7 +7,10 @@ const GRAPHQL_SCHEMA_CURRENT_VERSION = 1;
export const QueryResolvers: Resolvers<ServerContext> = { export const QueryResolvers: Resolvers<ServerContext> = {
Query: { Query: {
systems: async (_parent, args, contextValue, _info) => { systems: async (_parent, args, contextValue, _info) => {
return contextValue.systems.map((system) => { return contextValue.systems
.slice()
.sort((a, b) => a.name.localeCompare(b.name))
.map((system) => {
return { return {
name: system.name, name: system.name,
id: system.id, id: system.id,

View File

@@ -9,7 +9,10 @@ export const RouteResolvers: Resolvers<ServerContext> = {
const shuttles = await system.shuttleRepository.getShuttlesByRouteId(parent.id); const shuttles = await system.shuttleRepository.getShuttlesByRouteId(parent.id);
return shuttles.map(({ return shuttles
.slice()
.sort((a, b) => a.name.localeCompare(b.name))
.map(({
coordinates, coordinates,
name, name,
id, id,
@@ -51,7 +54,10 @@ export const RouteResolvers: Resolvers<ServerContext> = {
if (!system) return null; if (!system) return null;
const orderedStops = await system.shuttleRepository.getOrderedStopsByRouteId(parent.id); const orderedStops = await system.shuttleRepository.getOrderedStopsByRouteId(parent.id);
return orderedStops.map(({ routeId, stopId, position, systemId, updatedTime }) => ({ return orderedStops
.slice()
.sort((a, b) => a.position - b.position)
.map(({ routeId, stopId, position, systemId, updatedTime }) => ({
routeId, routeId,
stopId, stopId,
position, position,

View File

@@ -3,7 +3,7 @@ import { ServerContext } from "../ServerContext";
export const ShuttleResolvers: Resolvers<ServerContext> = { export const ShuttleResolvers: Resolvers<ServerContext> = {
Shuttle: { Shuttle: {
eta: async (parent, args, contextValue, info) => { eta: async (parent, args, contextValue, _) => {
if (!args.forStopId) return null; if (!args.forStopId) return null;
const system = contextValue.findSystemById(parent.systemId); const system = contextValue.findSystemById(parent.systemId);
@@ -21,7 +21,7 @@ export const ShuttleResolvers: Resolvers<ServerContext> = {
updatedTimeMs: etaForStopId.updatedTime, updatedTimeMs: etaForStopId.updatedTime,
}; };
}, },
etas: async (parent, args, contextValue, info) => { etas: async (parent, args, contextValue, _) => {
const system = contextValue.findSystemById(parent.systemId); const system = contextValue.findSystemById(parent.systemId);
if (!system) return null; if (!system) return null;
@@ -45,12 +45,14 @@ export const ShuttleResolvers: Resolvers<ServerContext> = {
})); }));
if (computedEtas.every((eta) => eta !== null)) { if (computedEtas.every((eta) => eta !== null)) {
return computedEtas; return computedEtas
.slice()
.sort((a, b) => (a!.secondsRemaining - b!.secondsRemaining));
} }
return []; return [];
}, },
route: async (parent, args, contextValue, info) => { route: async (parent, args, contextValue, _) => {
const system = contextValue.findSystemById(parent.systemId); const system = contextValue.findSystemById(parent.systemId);
if (!system) return null; if (!system) return null;

View File

@@ -8,14 +8,16 @@ export const StopResolvers: Resolvers<ServerContext> = {
if (!system) { if (!system) {
return []; return [];
} }
return await system.shuttleRepository.getOrderedStopsByStopId(parent.id); const orderedStops = await system.shuttleRepository.getOrderedStopsByStopId(parent.id);
return orderedStops.slice().sort((a, b) => a.position - b.position);
}, },
etas: async (parent, args, contextValue, _info) => { etas: async (parent, args, contextValue, _info) => {
const system = contextValue.findSystemById(parent.systemId); const system = contextValue.findSystemById(parent.systemId);
if (!system) { if (!system) {
return []; return [];
} }
return await system.shuttleRepository.getEtasForStopId(parent.id); const etas = await system.shuttleRepository.getEtasForStopId(parent.id);
return etas.slice().sort((a, b) => a.secondsRemaining - b.secondsRemaining);
}, },
}, },
} }

View File

@@ -9,7 +9,8 @@ export const SystemResolvers: Resolvers<ServerContext> = {
return []; return [];
} }
return await system.shuttleRepository.getRoutes(); const routes = await system.shuttleRepository.getRoutes();
return routes.slice().sort((a, b) => a.name.localeCompare(b.name));
}, },
stops: async (parent, _args, contextValue, _info) => { stops: async (parent, _args, contextValue, _info) => {
const system = contextValue.findSystemById(parent.id); const system = contextValue.findSystemById(parent.id);
@@ -17,7 +18,8 @@ export const SystemResolvers: Resolvers<ServerContext> = {
return []; return [];
} }
return await system.shuttleRepository.getStops(); const stops = await system.shuttleRepository.getStops();
return stops.slice().sort((a, b) => a.name.localeCompare(b.name));
}, },
stop: async (parent, args, contextValue, _info) => { stop: async (parent, args, contextValue, _info) => {
if (!args.id) return null; if (!args.id) return null;
@@ -78,7 +80,8 @@ export const SystemResolvers: Resolvers<ServerContext> = {
return []; return [];
} }
return await system.shuttleRepository.getShuttles(); const shuttles = await system.shuttleRepository.getShuttles();
return shuttles.slice().sort((a, b) => a.name.localeCompare(b.name));
}, },
parkingSystem: async (parent, _args, contextValue, _info) => { parkingSystem: async (parent, _args, contextValue, _info) => {
const system = contextValue.findSystemById(parent.id); const system = contextValue.findSystemById(parent.id);

View File

@@ -81,6 +81,49 @@ describe("ParkingSystemResolver", () => {
expect(parkingStructures).toHaveLength(0); expect(parkingStructures).toHaveLength(0);
}); });
it("returns parking structures sorted by name", async () => {
// Create two out-of-order structures by name
const s1 = {
id: "ps1",
name: "Barrera",
capacity: 100,
spotsAvailable: 50,
coordinates: { latitude: 0, longitude: 0 },
address: "1 Anywhere",
updatedTime: new Date(),
};
const s2 = {
id: "ps2",
name: "Anderson",
capacity: 100,
spotsAvailable: 50,
coordinates: { latitude: 0, longitude: 0 },
address: "2 Anywhere",
updatedTime: new Date(),
};
await context.systems[0].parkingRepository?.addOrUpdateParkingStructure(s1);
await context.systems[0].parkingRepository?.addOrUpdateParkingStructure(s2);
const minimalQuery = `
query GetParkingStructuresBySystem($systemId: ID!) {
system(id: $systemId) {
parkingSystem {
parkingStructures {
name
}
}
}
}
`;
const response = await getResponseFromQueryNeedingSystemId(minimalQuery);
assert(response.body.kind === "single");
expect(response.body.singleResult.errors).toBeUndefined();
const names = (response.body.singleResult.data as any).system.parkingSystem.parkingStructures.map((p: any) => p.name);
expect(names).toEqual([s2.name, s1.name]);
});
}); });

View File

@@ -40,6 +40,41 @@ describe("QueryResolvers", () => {
expect(response.body.singleResult.data?.systems).toHaveLength(systems.length); expect(response.body.singleResult.data?.systems).toHaveLength(systems.length);
}); });
it("returns systems sorted by name", async () => {
const s1 = buildSystemForTesting();
const s2 = buildSystemForTesting();
const s3 = buildSystemForTesting();
s1.name = "Chapman University";
s1.id = "a";
s2.name = "City of Monterey";
s2.id = "b";
s3.name = "Cal State Long Beach";
s3.id = "c";
context.systems = [s1, s2, s3];
const query = `
query GetSystems
{
systems {
name
}
}
`;
const response = await holder.testServer.executeOperation({
query,
}, {
contextValue: context
});
assert(response.body.kind === "single");
expect(response.body.singleResult.errors).toBeUndefined();
const names = (response.body.singleResult.data as any).systems.map((s: any) => s.name);
expect(names).toEqual(["Cal State Long Beach", "Chapman University", "City of Monterey"]);
});
}); });
describe("system", () => { describe("system", () => {

View File

@@ -79,6 +79,27 @@ describe("RouteResolvers", () => {
any).system.route.shuttles; any).system.route.shuttles;
expect(shuttles.length).toEqual(0); expect(shuttles.length).toEqual(0);
}); });
it("returns shuttles sorted by name", async () => {
const shuttles = generateMockShuttles();
// use two shuttles for determinism
const s1 = { ...shuttles[0], name: "Zed" };
const s2 = { ...shuttles[1], name: "Alpha" };
s1.systemId = mockSystem.id;
s1.routeId = mockRoute.id;
s2.systemId = mockSystem.id;
s2.routeId = mockRoute.id;
await context.systems[0].shuttleRepository.addOrUpdateShuttle(s1);
await context.systems[0].shuttleRepository.addOrUpdateShuttle(s2);
const response = await getResponseForShuttlesQuery();
assert(response.body.kind === "single");
expect(response.body.singleResult.errors).toBeUndefined()
const names = (response.body.singleResult.data as any).system.route.shuttles.map((s: any) => s.name);
expect(names).toEqual(["Alpha", "Zed"]);
});
}); });
describe("orderedStop", () => { describe("orderedStop", () => {
@@ -199,5 +220,22 @@ describe("RouteResolvers", () => {
const retrievedOrderedStops = (response.body.singleResult.data as any).system.route.orderedStops; const retrievedOrderedStops = (response.body.singleResult.data as any).system.route.orderedStops;
expect(retrievedOrderedStops).toHaveLength(0); expect(retrievedOrderedStops).toHaveLength(0);
}); });
it("returns ordered stops sorted by position", async () => {
const stops = generateMockOrderedStops().slice(0, 3).map((s) => ({ ...s }));
// Force same routeId and distinct positions out of order
stops[0].routeId = mockRoute.id; stops[0].position = 3; stops[0].stopId = "stA";
stops[1].routeId = mockRoute.id; stops[1].position = 1; stops[1].stopId = "stB";
stops[2].routeId = mockRoute.id; stops[2].position = 2; stops[2].stopId = "stC";
await Promise.all(stops.map(s => context.systems[0].shuttleRepository.addOrUpdateOrderedStop(s)));
const response = await getResponseForOrderedStopsQuery();
assert(response.body.kind === "single");
expect(response.body.singleResult.errors).toBeUndefined();
const stopIds = (response.body.singleResult.data as any).system.route.orderedStops.map((s: any) => s.stopId);
const expectedOrder = [...stops].sort((a, b) => a.position - b.position).map(s => s.stopId);
expect(stopIds).toEqual(expectedOrder);
});
}); });
}); });

View File

@@ -141,6 +141,31 @@ describe("ShuttleResolvers", () => {
any).system.shuttle.etas).toHaveLength(0); any).system.shuttle.etas).toHaveLength(0);
}); });
it("returns ETAs sorted by secondsRemaining", async () => {
const e1 = { ...generateMockEtas()[0], shuttleId: mockShuttle.id, stopId: "stA", secondsRemaining: 300 };
const e2 = { ...generateMockEtas()[0], shuttleId: mockShuttle.id, stopId: "stB", secondsRemaining: 30 };
const e3 = { ...generateMockEtas()[0], shuttleId: mockShuttle.id, stopId: "stC", secondsRemaining: 120 };
await context.systems[0].shuttleRepository.addOrUpdateEta(e1);
await context.systems[0].shuttleRepository.addOrUpdateEta(e2);
await context.systems[0].shuttleRepository.addOrUpdateEta(e3);
const response = await holder.testServer.executeOperation({
query,
variables: {
systemId: mockSystem.id,
shuttleId: mockShuttle.id,
},
}, {
contextValue: context
});
assert(response.body.kind === "single");
expect(response.body.singleResult.errors).toBeUndefined();
const seconds = (response.body.singleResult.data as any).system.shuttle.etas.map((e: any) => e.secondsRemaining);
expect(seconds).toEqual([30, 120, 300]);
});
}); });
describe("route", () => { describe("route", () => {
const query = ` const query = `

View File

@@ -67,6 +67,25 @@ describe("StopResolvers", () => {
expect(response.body.singleResult.errors).toBeUndefined(); expect(response.body.singleResult.errors).toBeUndefined();
expect((response.body.singleResult.data as any).system.stop.orderedStops).toHaveLength(0); expect((response.body.singleResult.data as any).system.stop.orderedStops).toHaveLength(0);
}); });
it("returns ordered stops sorted by position", async () => {
// Create three ordered stops with out-of-order positions and distinct routeIds
const base = generateMockOrderedStops()[0];
const o1 = { ...base, stopId: mockStop.id, routeId: "rA", position: 3 };
const o2 = { ...base, stopId: mockStop.id, routeId: "rB", position: 1 };
const o3 = { ...base, stopId: mockStop.id, routeId: "rC", position: 2 };
await context.systems[0].shuttleRepository.addOrUpdateOrderedStop(o1);
await context.systems[0].shuttleRepository.addOrUpdateOrderedStop(o2);
await context.systems[0].shuttleRepository.addOrUpdateOrderedStop(o3);
const response = await getResponseForQuery(query);
assert(response.body.kind === "single");
expect(response.body.singleResult.errors).toBeUndefined();
const routeIds = (response.body.singleResult.data as any).system.stop.orderedStops.map((s: any) => s.routeId);
const expectedOrder = [o2, o3, o1].map(s => s.routeId);
expect(routeIds).toEqual(expectedOrder);
});
}); });
describe("etas", () => { describe("etas", () => {
@@ -104,5 +123,21 @@ describe("StopResolvers", () => {
expect(response.body.singleResult.errors).toBeUndefined(); expect(response.body.singleResult.errors).toBeUndefined();
expect((response.body.singleResult.data as any).system.stop.etas).toHaveLength(0); expect((response.body.singleResult.data as any).system.stop.etas).toHaveLength(0);
}); });
it("returns ETAs sorted by secondsRemaining", async () => {
const e1 = { ...generateMockEtas()[0], stopId: mockStop.id, shuttleId: "shA", secondsRemaining: 240 };
const e2 = { ...generateMockEtas()[0], stopId: mockStop.id, shuttleId: "shB", secondsRemaining: 60 };
const e3 = { ...generateMockEtas()[0], stopId: mockStop.id, shuttleId: "shC", secondsRemaining: 120 };
await context.systems[0].shuttleRepository.addOrUpdateEta(e1);
await context.systems[0].shuttleRepository.addOrUpdateEta(e2);
await context.systems[0].shuttleRepository.addOrUpdateEta(e3);
const response = await getResponseForQuery(query);
assert(response.body.kind === "single");
expect(response.body.singleResult.errors).toBeUndefined();
const seconds = (response.body.singleResult.data as any).system.stop.etas.map((e: any) => e.secondsRemaining);
expect(seconds).toEqual([60, 120, 240]);
});
}); });
}); });

View File

@@ -4,7 +4,6 @@ import {
generateMockRoutes, generateMockRoutes,
generateMockShuttles, generateMockShuttles,
generateMockStops, generateMockStops,
generateParkingStructures
} from "../../../testHelpers/mockDataGenerators"; } from "../../../testHelpers/mockDataGenerators";
import { import {
addMockRouteToRepository, addMockRouteToRepository,
@@ -62,6 +61,23 @@ describe("SystemResolvers", () => {
const routes = (response.body.singleResult.data as any).system.routes; const routes = (response.body.singleResult.data as any).system.routes;
expect(routes.length === expectedRoutes.length); expect(routes.length === expectedRoutes.length);
}); });
it("returns routes sorted by name", async () => {
const routes = generateMockRoutes();
// Insert in reverse name order to verify sorting
const reversed = [...routes].sort((a, b) => b.name.localeCompare(a.name));
await Promise.all(reversed.map(async (route) => {
route.systemId = mockSystem.id;
await context.systems[0].shuttleRepository.addOrUpdateRoute(route);
}));
const response = await getResponseFromQueryNeedingSystemId(query);
assert(response.body.kind === "single");
expect(response.body.singleResult.errors).toBeUndefined();
const names = (response.body.singleResult.data as any).system.routes.map((r: any) => r.name);
expect(names).toEqual([...routes].sort((a, b) => a.name.localeCompare(b.name)).map(r => r.name));
});
}); });
describe("stops", () => { describe("stops", () => {
@@ -90,6 +106,22 @@ describe("SystemResolvers", () => {
const stops = (response.body.singleResult.data as any).system.stops; const stops = (response.body.singleResult.data as any).system.stops;
expect(stops.length === expectedStops.length); expect(stops.length === expectedStops.length);
}); });
it("returns stops sorted by name", async () => {
const stops = generateMockStops();
const reversed = [...stops].sort((a, b) => b.name.localeCompare(a.name));
await Promise.all(reversed.map(async (stop) => {
stop.systemId = mockSystem.id;
await context.systems[0].shuttleRepository.addOrUpdateStop(stop);
}));
const response = await getResponseFromQueryNeedingSystemId(query);
assert(response.body.kind === "single");
expect(response.body.singleResult.errors).toBeUndefined();
const names = (response.body.singleResult.data as any).system.stops.map((s: any) => s.name);
expect(names).toEqual([...stops].sort((a, b) => a.name.localeCompare(b.name)).map(s => s.name));
});
}); });
describe("stop", () => { describe("stop", () => {
@@ -312,6 +344,22 @@ describe("SystemResolvers", () => {
const shuttles = (response.body.singleResult.data as any).system.shuttles; const shuttles = (response.body.singleResult.data as any).system.shuttles;
expect(shuttles.length === expectedShuttles.length); expect(shuttles.length === expectedShuttles.length);
}); });
it("returns shuttles sorted by name", async () => {
const shuttles = generateMockShuttles();
const reversed = [...shuttles].sort((a, b) => b.name.localeCompare(a.name));
await Promise.all(reversed.map(async (shuttle) => {
shuttle.systemId = mockSystem.id;
await context.systems[0].shuttleRepository.addOrUpdateShuttle(shuttle);
}));
const response = await getResponseFromQueryNeedingSystemId(query);
assert(response.body.kind === "single");
expect(response.body.singleResult.errors).toBeUndefined();
const names = (response.body.singleResult.data as any).system.shuttles.map((s: any) => s.name);
expect(names).toEqual([...shuttles].sort((a, b) => a.name.localeCompare(b.name)).map(s => s.name));
});
}); });
}); });