From 415b4613089bb5502259dc6ab581f0d2f49f7d35 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Tue, 30 Sep 2025 15:23:50 -0700 Subject: [PATCH 1/2] 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 --- src/resolvers/ParkingSystemResolvers.ts | 5 +- src/resolvers/QueryResolvers.ts | 15 +++--- src/resolvers/RouteResolvers.ts | 10 +++- src/resolvers/ShuttleResolvers.ts | 10 ++-- src/resolvers/StopResolvers.ts | 6 ++- src/resolvers/SystemResolvers.ts | 9 ++-- .../ParkingSystemResolverTests.test.ts | 43 ++++++++++++++++ .../__tests__/QueryResolverTests.test.ts | 35 +++++++++++++ .../__tests__/RouteResolverTests.test.ts | 38 ++++++++++++++ .../__tests__/ShuttleResolverTests.test.ts | 25 ++++++++++ .../__tests__/StopResolverTests.test.ts | 35 +++++++++++++ .../__tests__/SystemResolverTests.test.ts | 50 ++++++++++++++++++- 12 files changed, 262 insertions(+), 19 deletions(-) diff --git a/src/resolvers/ParkingSystemResolvers.ts b/src/resolvers/ParkingSystemResolvers.ts index 076f18e..c01a813 100644 --- a/src/resolvers/ParkingSystemResolvers.ts +++ b/src/resolvers/ParkingSystemResolvers.ts @@ -12,7 +12,10 @@ export const ParkingSystemResolvers: Resolvers = { if (!parkingRepository) return []; const parkingStructures = await parkingRepository.getParkingStructures(); - return parkingStructures.map((structure) => { + return parkingStructures + .slice() + .sort((a, b) => a.name.localeCompare(b.name)) + .map((structure) => { return { ...structure, systemId: parent.systemId diff --git a/src/resolvers/QueryResolvers.ts b/src/resolvers/QueryResolvers.ts index e1359ce..885ff2c 100644 --- a/src/resolvers/QueryResolvers.ts +++ b/src/resolvers/QueryResolvers.ts @@ -7,12 +7,15 @@ const GRAPHQL_SCHEMA_CURRENT_VERSION = 1; export const QueryResolvers: Resolvers = { Query: { systems: async (_parent, args, contextValue, _info) => { - return contextValue.systems.map((system) => { - return { - name: system.name, - id: system.id, - }; - }) + return contextValue.systems + .slice() + .sort((a, b) => a.name.localeCompare(b.name)) + .map((system) => { + return { + name: system.name, + id: system.id, + }; + }) }, system: async (_parent, args, contextValue, _info) => { if (!args.id) return null; diff --git a/src/resolvers/RouteResolvers.ts b/src/resolvers/RouteResolvers.ts index 0574ed5..cb1bcce 100644 --- a/src/resolvers/RouteResolvers.ts +++ b/src/resolvers/RouteResolvers.ts @@ -9,7 +9,10 @@ export const RouteResolvers: Resolvers = { const shuttles = await system.shuttleRepository.getShuttlesByRouteId(parent.id); - return shuttles.map(({ + return shuttles + .slice() + .sort((a, b) => a.name.localeCompare(b.name)) + .map(({ coordinates, name, id, @@ -51,7 +54,10 @@ export const RouteResolvers: Resolvers = { if (!system) return null; 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, stopId, position, diff --git a/src/resolvers/ShuttleResolvers.ts b/src/resolvers/ShuttleResolvers.ts index 2eb9478..b755e83 100644 --- a/src/resolvers/ShuttleResolvers.ts +++ b/src/resolvers/ShuttleResolvers.ts @@ -3,7 +3,7 @@ import { ServerContext } from "../ServerContext"; export const ShuttleResolvers: Resolvers = { Shuttle: { - eta: async (parent, args, contextValue, info) => { + eta: async (parent, args, contextValue, _) => { if (!args.forStopId) return null; const system = contextValue.findSystemById(parent.systemId); @@ -21,7 +21,7 @@ export const ShuttleResolvers: Resolvers = { updatedTimeMs: etaForStopId.updatedTime, }; }, - etas: async (parent, args, contextValue, info) => { + etas: async (parent, args, contextValue, _) => { const system = contextValue.findSystemById(parent.systemId); if (!system) return null; @@ -45,12 +45,14 @@ export const ShuttleResolvers: Resolvers = { })); if (computedEtas.every((eta) => eta !== null)) { - return computedEtas; + return computedEtas + .slice() + .sort((a, b) => (a!.secondsRemaining - b!.secondsRemaining)); } return []; }, - route: async (parent, args, contextValue, info) => { + route: async (parent, args, contextValue, _) => { const system = contextValue.findSystemById(parent.systemId); if (!system) return null; diff --git a/src/resolvers/StopResolvers.ts b/src/resolvers/StopResolvers.ts index 0c3ea54..05d0cb9 100644 --- a/src/resolvers/StopResolvers.ts +++ b/src/resolvers/StopResolvers.ts @@ -8,14 +8,16 @@ export const StopResolvers: Resolvers = { if (!system) { 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) => { const system = contextValue.findSystemById(parent.systemId); if (!system) { 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); }, }, } diff --git a/src/resolvers/SystemResolvers.ts b/src/resolvers/SystemResolvers.ts index a955fc2..27d46c9 100644 --- a/src/resolvers/SystemResolvers.ts +++ b/src/resolvers/SystemResolvers.ts @@ -9,7 +9,8 @@ export const SystemResolvers: Resolvers = { 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) => { const system = contextValue.findSystemById(parent.id); @@ -17,7 +18,8 @@ export const SystemResolvers: Resolvers = { 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) => { if (!args.id) return null; @@ -78,7 +80,8 @@ export const SystemResolvers: Resolvers = { 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) => { const system = contextValue.findSystemById(parent.id); diff --git a/src/resolvers/__tests__/ParkingSystemResolverTests.test.ts b/src/resolvers/__tests__/ParkingSystemResolverTests.test.ts index 26fb066..015a669 100644 --- a/src/resolvers/__tests__/ParkingSystemResolverTests.test.ts +++ b/src/resolvers/__tests__/ParkingSystemResolverTests.test.ts @@ -81,6 +81,49 @@ describe("ParkingSystemResolver", () => { 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]); + }); }); diff --git a/src/resolvers/__tests__/QueryResolverTests.test.ts b/src/resolvers/__tests__/QueryResolverTests.test.ts index 5d832af..f4b6fdb 100644 --- a/src/resolvers/__tests__/QueryResolverTests.test.ts +++ b/src/resolvers/__tests__/QueryResolverTests.test.ts @@ -40,6 +40,41 @@ describe("QueryResolvers", () => { 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", () => { diff --git a/src/resolvers/__tests__/RouteResolverTests.test.ts b/src/resolvers/__tests__/RouteResolverTests.test.ts index 7193eae..308c485 100644 --- a/src/resolvers/__tests__/RouteResolverTests.test.ts +++ b/src/resolvers/__tests__/RouteResolverTests.test.ts @@ -79,6 +79,27 @@ describe("RouteResolvers", () => { any).system.route.shuttles; 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", () => { @@ -199,5 +220,22 @@ describe("RouteResolvers", () => { const retrievedOrderedStops = (response.body.singleResult.data as any).system.route.orderedStops; 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); + }); }); }); diff --git a/src/resolvers/__tests__/ShuttleResolverTests.test.ts b/src/resolvers/__tests__/ShuttleResolverTests.test.ts index 7f7ec32..0ab62d0 100644 --- a/src/resolvers/__tests__/ShuttleResolverTests.test.ts +++ b/src/resolvers/__tests__/ShuttleResolverTests.test.ts @@ -141,6 +141,31 @@ describe("ShuttleResolvers", () => { 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", () => { const query = ` diff --git a/src/resolvers/__tests__/StopResolverTests.test.ts b/src/resolvers/__tests__/StopResolverTests.test.ts index 6337e1f..4378df5 100644 --- a/src/resolvers/__tests__/StopResolverTests.test.ts +++ b/src/resolvers/__tests__/StopResolverTests.test.ts @@ -67,6 +67,25 @@ describe("StopResolvers", () => { expect(response.body.singleResult.errors).toBeUndefined(); 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", () => { @@ -104,5 +123,21 @@ describe("StopResolvers", () => { expect(response.body.singleResult.errors).toBeUndefined(); 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]); + }); }); }); diff --git a/src/resolvers/__tests__/SystemResolverTests.test.ts b/src/resolvers/__tests__/SystemResolverTests.test.ts index b4e4827..822ae4d 100644 --- a/src/resolvers/__tests__/SystemResolverTests.test.ts +++ b/src/resolvers/__tests__/SystemResolverTests.test.ts @@ -4,7 +4,6 @@ import { generateMockRoutes, generateMockShuttles, generateMockStops, - generateParkingStructures } from "../../../testHelpers/mockDataGenerators"; import { addMockRouteToRepository, @@ -62,6 +61,23 @@ describe("SystemResolvers", () => { const routes = (response.body.singleResult.data as any).system.routes; 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", () => { @@ -90,6 +106,22 @@ describe("SystemResolvers", () => { const stops = (response.body.singleResult.data as any).system.stops; 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", () => { @@ -312,6 +344,22 @@ describe("SystemResolvers", () => { const shuttles = (response.body.singleResult.data as any).system.shuttles; 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)); + }); }); }); From 9040ed7175442dd459793c051b71f7856777379d Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Tue, 30 Sep 2025 15:30:34 -0700 Subject: [PATCH 2/2] Move CLAUDE.md to AGENTS.md --- AGENTS.md | 102 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ CLAUDE.md | 101 ----------------------------------------------------- 2 files changed, 102 insertions(+), 101 deletions(-) create mode 100644 AGENTS.md delete mode 100644 CLAUDE.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..cfeabf8 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,102 @@ +# AGENTS.md + +This file provides guidance to coding agents (e.g., Codex CLI, Claude Code, and other AI coding assistants) when working with code in this repository. + +## Development Commands + +### Core Development +```bash +# Start development server with hot reloading +docker compose run dev + +# Run comprehensive test suite +docker compose run test + +# Generate GraphQL TypeScript types +npm run generate + +# Build for development (install, codegen, tsc) +npm run build:dev +``` + +### Testing +```bash +# Run all tests via npm +npm test + +# Run specific test file +npm test -- --testPathPattern= + +# Run tests with coverage +npm test -- --coverage +``` + +## Architecture Overview + +Project Inter Server is a GraphQL-based backend for college transit tracking with real-time shuttle data, parking availability, and push notifications. + +- InterchangeSystem: Central orchestrator for shuttles, parking, and notifications + - Use `InterchangeSystem.build()` in production + - Use `InterchangeSystem.buildForTesting()` in tests + +- Repository Pattern (data access abstraction) + - Shuttle: `UnoptimizedInMemoryShuttleRepository` + - Parking: `InMemoryParkingRepository` + - Notifications: `RedisNotificationRepository` (prod) / `InMemoryNotificationRepository` (test) + +- Data Loaders (external API integration) + - `ApiBasedShuttleRepositoryLoader` – Passio GO! API + - `ChapmanApiBasedParkingRepositoryLoader` – Parking data + - `TimedApiBasedRepositoryLoader` – Periodic refresh wrapper + +- Notification System + - `ETANotificationScheduler` – Shuttle arrival notifications + - `AppleNotificationSender` – APNS integration + +## GraphQL +- Schema definition: `schema.graphqls` +- Generated types: `src/generated/` +- Resolvers: `src/resolvers/` +- Resolver merge: `src/MergedResolvers.ts` + +## Directory Structure +- `src/entities/` – Core business logic +- `src/repositories/` – Data access layer +- `src/loaders/` – External API integrations +- `src/notifications/` – Push notification system +- `src/resolvers/` – GraphQL resolvers and tests +- `testHelpers/` – Test utilities and mock data + +## Docker Services +- `dev` – Development server with hot reload +- `test` – Unit/integration tests +- `redis` – Persistent Redis +- `redis-no-persistence` – Ephemeral Redis for tests + +## Testing Patterns +- Prefer `buildForTesting()` to construct systems in tests. +- Mock external APIs using JSON snapshots under `testHelpers/jsonSnapshots`. +- Use in-memory repositories for speed where possible. +- When adding features that affect API output, add focused resolver tests in the corresponding `src/resolvers/__tests__` file. + - Separate unit tests from integration tests where practical to keep feedback fast and failures well-scoped. + +## Development Guidelines + +### General Guidelines +- Use test-driven development where possible. Write tests before implementation and run them before and after changes. +- Use Docker Compose for tests. Run `docker compose run test` so you can see full output. + +### Git Workflow +- Name pull requests after their branch name. + +### Code Style +- Prefer arrow functions, especially within classes. +- Keep changes minimal and focused; avoid unrelated refactors. Mention incidental issues separately. +- Respect existing interfaces and types; use non-destructive edits. + +### Agent Tips +- Write or update tests alongside changes; validate with `docker compose run test`. +- When touching GraphQL resolvers, co-locate new tests in `src/resolvers/__tests__`. + +## Multi-tenant Support +Currently supports Chapman University (Passio System ID: `263`). Each university system uses isolated repositories and configuration. New systems should be added via `InterchangeSystem` configuration and appropriate loaders. diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 4b22fa4..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,101 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Development Commands - -### Core Development -```bash -# Start development server with hot reloading -docker compose run dev - -# Run comprehensive test suite -docker compose run test - - -# Generate GraphQL TypeScript types -npm run generate - -# Build for development -npm run build:dev -``` - -### Testing -```bash -# Run all tests -npm test - -# Run specific test file -npm test -- --testPathPattern= - -# Run tests with coverage -npm test -- --coverage -``` - -## Architecture Overview - -**Project Inter Server** is a GraphQL-based backend for college transit tracking with real-time shuttle data, parking availability, and push notifications. - -### Core Components - -1. **InterchangeSystem** - Central orchestrator managing shuttles, parking, and notifications - - Use `InterchangeSystem.build()` for production - - Use `InterchangeSystem.buildForTesting()` for tests - -2. **Repository Pattern** - Data access abstraction - - Shuttle: `UnoptimizedInMemoryShuttleRepository` - - Parking: `InMemoryParkingRepository` - - Notifications: `RedisNotificationRepository` (prod) / `InMemoryNotificationRepository` (test) - -3. **Data Loaders** - External API integration - - `ApiBasedShuttleRepositoryLoader` - Passio GO! API integration - - `ChapmanApiBasedParkingRepositoryLoader` - University parking data - - `TimedApiBasedRepositoryLoader` - Periodic data refresh - -4. **Notification System** - - `ETANotificationScheduler` - Manages shuttle arrival notifications - - `AppleNotificationSender` - APNS integration - - Default threshold: 180 seconds - -### GraphQL Schema -- Schema definition: `schema.graphqls` -- Generated types: `src/generated/` -- Resolvers: `src/resolvers/` -- Combined in: `src/MergedResolvers.ts` - -### Directory Structure -- `src/entities/` - Core business logic -- `src/repositories/` - Data access layer -- `src/loaders/` - External API integrations -- `src/notifications/` - Push notification system -- `test/` - Comprehensive test suite with mock data - -### Multi-tenant Support -Currently supports Chapman University (Passio System ID: "263"). Each university system has: -- System-specific configurations -- Isolated data repositories -- Custom API integrations - -### Docker Services -- `dev` - Development with hot reload -- `test` - Unit/integration testing -- `redis` - Persistent Redis -- `redis-no-persistence` - Ephemeral Redis for tests - -### Testing Patterns -- Use `buildForTesting()` for InterchangeSystem in tests -- Mock external APIs with JSON snapshots in test data -- Separate unit tests from integration tests -- Use in-memory repositories for faster testing - -## Development Guidelines - -### General Guidelines -- Use test-driven development. Always write tests before implementation, and run them before and after implementation. -- Use Docker Compose for tests. Make sure you run it in a way where you can actually see the test result. - -### Git Workflow -- Use the name of the branch for all pull requests - -### Code Style -- Prefer arrow functions, especially in classes \ No newline at end of file