Compare commits
15 Commits
claude/add
...
506ac612bc
| Author | SHA1 | Date | |
|---|---|---|---|
| 506ac612bc | |||
| 30edc307af | |||
| 20adae3675 | |||
| cd7d529b86 | |||
| 15504ecba3 | |||
| e98ef96b2e | |||
| 98eb1083eb | |||
| 3aa2a2583d | |||
| 5b441b3698 | |||
| e45a94f260 | |||
| c088f1003b | |||
| e0e4092218 | |||
| 893274ca6a | |||
| 0601d9e6a0 | |||
| 91f5f4ba7c |
47
.github/workflows/deploy-coolify.yml
vendored
Normal file
47
.github/workflows/deploy-coolify.yml
vendored
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
name: "Deploy to Coolify"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Check out repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
|
- name: Login to Gitea Container Registry
|
||||||
|
uses: docker/login-action@v4
|
||||||
|
with:
|
||||||
|
registry: gitea.bchen.dev
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.PAT }}
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v4
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
uses: docker/build-push-action@v7
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
tags: gitea.bchen.dev/${{ github.actor }}/project-inter-server:latest
|
||||||
|
|
||||||
|
- name: Set up Tailscale
|
||||||
|
uses: tailscale/github-action@v4
|
||||||
|
with:
|
||||||
|
oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }}
|
||||||
|
oauth-secret: ${{ secrets.TS_OAUTH_SECRET }}
|
||||||
|
tags: tag:ci
|
||||||
|
use-cache: false
|
||||||
|
tailscaled-args: --tun=userspace-networking --socks5-server=localhost:1055 --outbound-http-proxy-listen=localhost:1055
|
||||||
|
|
||||||
|
- name: Deploy to Coolify
|
||||||
|
run: |
|
||||||
|
curl --request POST '${{ secrets.COOLIFY_WEBHOOK }}' --header 'Authorization: Bearer ${{ secrets.COOLIFY_TOKEN }}'
|
||||||
60
.github/workflows/deploy.yml
vendored
60
.github/workflows/deploy.yml
vendored
@@ -1,60 +0,0 @@
|
|||||||
name: "Deploy to birb co."
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
deploy:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Check out repository
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
# See documentation: https://github.com/tailscale/github-action?tab=readme-ov-file
|
|
||||||
- name: Connect to Tailscale
|
|
||||||
uses: tailscale/github-action@v4
|
|
||||||
with:
|
|
||||||
oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }}
|
|
||||||
oauth-secret: ${{ secrets.TS_OAUTH_CLIENT_SECRET }}
|
|
||||||
tags: tag:ci
|
|
||||||
|
|
||||||
- name: Set up SSH key
|
|
||||||
run: |
|
|
||||||
mkdir -p ~/.ssh
|
|
||||||
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519
|
|
||||||
chmod 600 ~/.ssh/id_ed25519
|
|
||||||
ssh-keyscan ${{ vars.HOST }} >> ~/.ssh/known_hosts
|
|
||||||
|
|
||||||
- name: Remove directory from server
|
|
||||||
run: |
|
|
||||||
ssh -i ~/.ssh/id_ed25519 ${{ vars.USERNAME }}@${{ vars.HOST }} << 'EOF'
|
|
||||||
rm -rf ~/${{ vars.DIRECTORY_NAME }}
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# Avoid needing to set up SSH access to GitHub for this user
|
|
||||||
- name: Transfer repository files to server
|
|
||||||
run: |
|
|
||||||
scp -i ~/.ssh/id_ed25519 -r ./* ${{ vars.USERNAME }}@${{ vars.HOST }}:~/${{ vars.DIRECTORY_NAME }}
|
|
||||||
|
|
||||||
- name: Deploy on server with Docker
|
|
||||||
run: |
|
|
||||||
ssh -i ~/.ssh/id_ed25519 ${{ vars.USERNAME }}@${{ vars.HOST }} << 'EOF'
|
|
||||||
cd ~/${{ vars.DIRECTORY_NAME }}
|
|
||||||
export APNS_IS_PRODUCTION=${{ secrets.APNS_IS_PRODUCTION }}
|
|
||||||
export APNS_BUNDLE_ID=${{ secrets.APNS_BUNDLE_ID }}
|
|
||||||
export APNS_TEAM_ID=${{ secrets.APNS_TEAM_ID }}
|
|
||||||
export APNS_KEY_ID=${{ secrets.APNS_KEY_ID }}
|
|
||||||
export APNS_PRIVATE_KEY=${{ secrets.APNS_PRIVATE_KEY }}
|
|
||||||
export PARKING_LOGGING_INTERVAL_MS=${{ secrets.PARKING_LOGGING_INTERVAL_MS }}
|
|
||||||
export RATE_LIMITS_DISABLED=${{ secrets.RATE_LIMITS_DISABLED }}
|
|
||||||
export RATE_LIMIT_WINDOW_MS=${{ secrets.RATE_LIMIT_WINDOW_MS }}
|
|
||||||
export RATE_LIMIT_DELAY_AFTER_REQUESTS=${{ secrets.RATE_LIMIT_DELAY_AFTER_REQUESTS }}
|
|
||||||
export RATE_LIMIT_DELAY_MULTIPLIER_MS=${{ secrets.RATE_LIMIT_DELAY_MULTIPLIER_MS }}
|
|
||||||
export DOCKER_HOST=unix://$XDG_RUNTIME_DIR/docker.sock
|
|
||||||
docker compose -f docker-compose.prod.yml down --remove-orphans
|
|
||||||
docker compose -f docker-compose.prod.yml up -d --build
|
|
||||||
EOF
|
|
||||||
103
AGENTS.md
Normal file
103
AGENTS.md
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to all agents 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
|
||||||
|
```
|
||||||
|
|
||||||
|
Only use Docker Compose for running tests, and only use `docker compose run test`
|
||||||
|
to run tests; don't try to run tests for individual files.
|
||||||
|
|
||||||
|
## 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.
|
||||||
94
CLAUDE.md
94
CLAUDE.md
@@ -1,93 +1 @@
|
|||||||
# CLAUDE.md
|
See AGENTS.md
|
||||||
|
|
||||||
This file provides guidance to Claude 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 (install, codegen, tsc)
|
|
||||||
npm run build:dev
|
|
||||||
```
|
|
||||||
|
|
||||||
Only use Docker Compose for running tests, and only use `docker compose run test`
|
|
||||||
to run tests; don't try to run tests for individual files.
|
|
||||||
|
|
||||||
## 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.
|
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
FROM node:22-alpine
|
FROM node:22-alpine
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
COPY . .
|
COPY . .
|
||||||
|
RUN npm run build
|
||||||
EXPOSE 4000
|
EXPOSE 4000
|
||||||
|
|
||||||
|
CMD ["npm", "run", "start"]
|
||||||
|
|||||||
1121
package-lock.json
generated
1121
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@
|
|||||||
"build:dev": "npm install --include=dev && npm run generate && tsc --project tsconfig.build.json",
|
"build:dev": "npm install --include=dev && npm run generate && tsc --project tsconfig.build.json",
|
||||||
"build": "npm install --include=dev && npm run generate && tsc --project tsconfig.build.json && npm prune --omit=dev",
|
"build": "npm install --include=dev && npm run generate && tsc --project tsconfig.build.json && npm prune --omit=dev",
|
||||||
"start:dev": "npm run build:dev && node ./dist/index.js",
|
"start:dev": "npm run build:dev && node ./dist/index.js",
|
||||||
"start": "npm run build && node ./dist/index.js",
|
"start": "node ./dist/index.js",
|
||||||
"generate": "graphql-codegen --config codegen.ts",
|
"generate": "graphql-codegen --config codegen.ts",
|
||||||
"test": "npm run build:dev && jest --runInBand"
|
"test": "npm run build:dev && jest --runInBand"
|
||||||
},
|
},
|
||||||
@@ -26,12 +26,12 @@
|
|||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apollo/server": "^4.11.2",
|
"@apollo/server": "^5.0.0",
|
||||||
"@as-integrations/express5": "^1.1.2",
|
"@as-integrations/express5": "^1.1.2",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
"express-rate-limit": "^8.0.1",
|
"express-rate-limit": "^8.0.1",
|
||||||
"express-slow-down": "^3.0.0",
|
"express-slow-down": "^3.0.0",
|
||||||
"graphql": "^16.10.0",
|
"graphql": "^16.11.0",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"redis": "^4.7.0"
|
"redis": "^4.7.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ export class InterchangeSystem {
|
|||||||
);
|
);
|
||||||
notificationScheduler.startListeningForUpdates();
|
notificationScheduler.startListeningForUpdates();
|
||||||
|
|
||||||
let { parkingRepository, timedParkingLoader } = await InterchangeSystem.buildRedisParkingLoaderAndRepository(args.parkingSystemId, args.id);
|
let { parkingRepository, timedParkingLoader } = await InterchangeSystem.buildRedisParkingLoaderAndRepository(args.parkingSystemId);
|
||||||
timedParkingLoader?.start();
|
timedParkingLoader?.start();
|
||||||
|
|
||||||
return new InterchangeSystem(
|
return new InterchangeSystem(
|
||||||
@@ -145,7 +145,7 @@ export class InterchangeSystem {
|
|||||||
shuttleRepository: ShuttleGetterRepository,
|
shuttleRepository: ShuttleGetterRepository,
|
||||||
args: InterchangeSystemBuilderArguments
|
args: InterchangeSystemBuilderArguments
|
||||||
) {
|
) {
|
||||||
const notificationRepository = new RedisNotificationRepository(undefined, args.id);
|
const notificationRepository = new RedisNotificationRepository();
|
||||||
await notificationRepository.connect();
|
await notificationRepository.connect();
|
||||||
const notificationScheduler = new ETANotificationScheduler(
|
const notificationScheduler = new ETANotificationScheduler(
|
||||||
etaRepository,
|
etaRepository,
|
||||||
@@ -157,12 +157,12 @@ export class InterchangeSystem {
|
|||||||
return { notificationScheduler, notificationRepository };
|
return { notificationScheduler, notificationRepository };
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async buildRedisParkingLoaderAndRepository(id?: string, systemId: string = '') {
|
private static async buildRedisParkingLoaderAndRepository(id?: string) {
|
||||||
if (id === undefined) {
|
if (id === undefined) {
|
||||||
return { parkingRepository: null, timedParkingLoader: null };
|
return { parkingRepository: null, timedParkingLoader: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
let parkingRepository: RedisParkingRepository | null = new RedisParkingRepository(undefined, systemId);
|
let parkingRepository: RedisParkingRepository | null = new RedisParkingRepository();
|
||||||
await parkingRepository.connect();
|
await parkingRepository.connect();
|
||||||
|
|
||||||
const loaderBuilderArguments: ParkingRepositoryLoaderBuilderArguments = {
|
const loaderBuilderArguments: ParkingRepositoryLoaderBuilderArguments = {
|
||||||
|
|||||||
@@ -4,23 +4,17 @@ import createRedisClientForRepository from '../helpers/createRedisClientForRepos
|
|||||||
|
|
||||||
export abstract class BaseRedisRepository extends EventEmitter {
|
export abstract class BaseRedisRepository extends EventEmitter {
|
||||||
protected redisClient;
|
protected redisClient;
|
||||||
protected readonly systemId: string;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
redisClient: RedisClientType = createRedisClientForRepository(),
|
redisClient: RedisClientType = createRedisClientForRepository(),
|
||||||
systemId: string = '',
|
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
this.redisClient = redisClient;
|
this.redisClient = redisClient;
|
||||||
this.systemId = systemId;
|
|
||||||
this.redisClient.on('error', (err) => {
|
this.redisClient.on('error', (err) => {
|
||||||
console.error(err.stack);
|
console.error(err.stack);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected prefixKey = (key: string): string =>
|
|
||||||
this.systemId ? `${this.systemId}:${key}` : key;
|
|
||||||
|
|
||||||
get isReady() {
|
get isReady() {
|
||||||
return this.redisClient.isReady;
|
return this.redisClient.isReady;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { RedisClientType } from 'redis';
|
|
||||||
import { TupleKey } from '../../types/TupleKey';
|
import { TupleKey } from '../../types/TupleKey';
|
||||||
import {
|
import {
|
||||||
Listener,
|
Listener,
|
||||||
@@ -8,22 +7,14 @@ import {
|
|||||||
ScheduledNotification
|
ScheduledNotification
|
||||||
} from "./NotificationRepository";
|
} from "./NotificationRepository";
|
||||||
import { BaseRedisRepository } from "../BaseRedisRepository";
|
import { BaseRedisRepository } from "../BaseRedisRepository";
|
||||||
import createRedisClientForRepository from '../../helpers/createRedisClientForRepository';
|
|
||||||
|
|
||||||
export class RedisNotificationRepository extends BaseRedisRepository implements NotificationRepository {
|
export class RedisNotificationRepository extends BaseRedisRepository implements NotificationRepository {
|
||||||
private notificationListeners: Listener[] = [];
|
private notificationListeners: Listener[] = [];
|
||||||
private readonly NOTIFICATION_KEY_PREFIX = 'notification:';
|
private readonly NOTIFICATION_KEY_PREFIX = 'notification:';
|
||||||
|
|
||||||
constructor(
|
|
||||||
redisClient: RedisClientType = createRedisClientForRepository(),
|
|
||||||
systemId: string = '',
|
|
||||||
) {
|
|
||||||
super(redisClient, systemId);
|
|
||||||
}
|
|
||||||
|
|
||||||
private getNotificationKey = (shuttleId: string, stopId: string): string => {
|
private getNotificationKey = (shuttleId: string, stopId: string): string => {
|
||||||
const tuple = new TupleKey(shuttleId, stopId);
|
const tuple = new TupleKey(shuttleId, stopId);
|
||||||
return this.prefixKey(`${this.NOTIFICATION_KEY_PREFIX}${tuple.toString()}`);
|
return `${this.NOTIFICATION_KEY_PREFIX}${tuple.toString()}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
public addOrUpdateNotification = async (notification: ScheduledNotification): Promise<void> => {
|
public addOrUpdateNotification = async (notification: ScheduledNotification): Promise<void> => {
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ class RedisNotificationRepositoryHolder implements RepositoryHolder {
|
|||||||
url: process.env.REDIS_URL,
|
url: process.env.REDIS_URL,
|
||||||
});
|
});
|
||||||
await this.redisClient.connect();
|
await this.redisClient.connect();
|
||||||
this.repo = new RedisNotificationRepository(this.redisClient, 'test-system');
|
this.repo = new RedisNotificationRepository(this.redisClient);
|
||||||
return this.repo;
|
return this.repo;
|
||||||
}
|
}
|
||||||
teardown = async () => {
|
teardown = async () => {
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ import { IParkingStructure } from "../../entities/ParkingRepositoryEntities";
|
|||||||
import { HistoricalParkingAverageQueryResult, HistoricalParkingAverageFilterArguments } from "./ParkingGetterRepository";
|
import { HistoricalParkingAverageQueryResult, HistoricalParkingAverageFilterArguments } from "./ParkingGetterRepository";
|
||||||
import { BaseRedisRepository } from "../BaseRedisRepository";
|
import { BaseRedisRepository } from "../BaseRedisRepository";
|
||||||
import { PARKING_LOGGING_INTERVAL_MS } from "../../environment";
|
import { PARKING_LOGGING_INTERVAL_MS } from "../../environment";
|
||||||
import { RedisClientType } from "redis";
|
|
||||||
import createRedisClientForRepository from "../../helpers/createRedisClientForRepository";
|
|
||||||
|
|
||||||
export type ParkingStructureID = string;
|
export type ParkingStructureID = string;
|
||||||
|
|
||||||
@@ -12,13 +10,6 @@ export class RedisParkingRepository extends BaseRedisRepository implements Parki
|
|||||||
private dataLastAdded: Map<ParkingStructureID, Date> = new Map();
|
private dataLastAdded: Map<ParkingStructureID, Date> = new Map();
|
||||||
private loggingIntervalMs = PARKING_LOGGING_INTERVAL_MS;
|
private loggingIntervalMs = PARKING_LOGGING_INTERVAL_MS;
|
||||||
|
|
||||||
constructor(
|
|
||||||
redisClient: RedisClientType = createRedisClientForRepository(),
|
|
||||||
systemId: string = '',
|
|
||||||
) {
|
|
||||||
super(redisClient, systemId);
|
|
||||||
}
|
|
||||||
|
|
||||||
addOrUpdateParkingStructure = async (structure: IParkingStructure): Promise<void> => {
|
addOrUpdateParkingStructure = async (structure: IParkingStructure): Promise<void> => {
|
||||||
const keys = this.createRedisKeys(structure.id);
|
const keys = this.createRedisKeys(structure.id);
|
||||||
await this.redisClient.hSet(keys.structure, this.createRedisHashFromStructure(structure));
|
await this.redisClient.hSet(keys.structure, this.createRedisHashFromStructure(structure));
|
||||||
@@ -37,8 +28,8 @@ export class RedisParkingRepository extends BaseRedisRepository implements Parki
|
|||||||
};
|
};
|
||||||
|
|
||||||
clearParkingStructureData = async (): Promise<void> => {
|
clearParkingStructureData = async (): Promise<void> => {
|
||||||
const structureKeys = await this.redisClient.keys(this.prefixKey('parking:structure:*'));
|
const structureKeys = await this.redisClient.keys('parking:structure:*');
|
||||||
const timeSeriesKeys = await this.redisClient.keys(this.prefixKey('parking:timeseries:*'));
|
const timeSeriesKeys = await this.redisClient.keys('parking:timeseries:*');
|
||||||
|
|
||||||
const allKeys = [...structureKeys, ...timeSeriesKeys];
|
const allKeys = [...structureKeys, ...timeSeriesKeys];
|
||||||
if (allKeys.length > 0) {
|
if (allKeys.length > 0) {
|
||||||
@@ -60,7 +51,7 @@ export class RedisParkingRepository extends BaseRedisRepository implements Parki
|
|||||||
};
|
};
|
||||||
|
|
||||||
getParkingStructures = async (): Promise<IParkingStructure[]> => {
|
getParkingStructures = async (): Promise<IParkingStructure[]> => {
|
||||||
const keys = await this.redisClient.keys(this.prefixKey('parking:structure:*'));
|
const keys = await this.redisClient.keys('parking:structure:*');
|
||||||
const structures: IParkingStructure[] = [];
|
const structures: IParkingStructure[] = [];
|
||||||
|
|
||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
@@ -89,8 +80,8 @@ export class RedisParkingRepository extends BaseRedisRepository implements Parki
|
|||||||
};
|
};
|
||||||
|
|
||||||
private createRedisKeys = (structureId: string) => ({
|
private createRedisKeys = (structureId: string) => ({
|
||||||
structure: this.prefixKey(`parking:structure:${structureId}`),
|
structure: `parking:structure:${structureId}`,
|
||||||
timeSeries: this.prefixKey(`parking:timeseries:${structureId}`),
|
timeSeries: `parking:timeseries:${structureId}`
|
||||||
});
|
});
|
||||||
|
|
||||||
private createRedisHashFromStructure = (structure: IParkingStructure): Record<string, string> => ({
|
private createRedisHashFromStructure = (structure: IParkingStructure): Record<string, string> => ({
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ class RedisParkingRepositoryHolder implements RepositoryHolder<ParkingGetterSett
|
|||||||
url: process.env.REDIS_URL,
|
url: process.env.REDIS_URL,
|
||||||
});
|
});
|
||||||
await this.redisClient.connect();
|
await this.redisClient.connect();
|
||||||
this.repo = new RedisParkingRepository(this.redisClient, 'test-system');
|
this.repo = new RedisParkingRepository(this.redisClient);
|
||||||
return this.repo;
|
return this.repo;
|
||||||
};
|
};
|
||||||
teardown = async () => {
|
teardown = async () => {
|
||||||
|
|||||||
@@ -17,9 +17,8 @@ export class RedisShuttleRepository extends BaseRedisRepository implements Shutt
|
|||||||
constructor(
|
constructor(
|
||||||
redisClient: RedisClientType = createRedisClientForRepository(),
|
redisClient: RedisClientType = createRedisClientForRepository(),
|
||||||
readonly shuttleStopArrivalDegreeDelta: number = 0.001,
|
readonly shuttleStopArrivalDegreeDelta: number = 0.001,
|
||||||
systemId: string = '',
|
|
||||||
) {
|
) {
|
||||||
super(redisClient, systemId);
|
super(redisClient);
|
||||||
}
|
}
|
||||||
|
|
||||||
get isReady() {
|
get isReady() {
|
||||||
@@ -84,24 +83,24 @@ export class RedisShuttleRepository extends BaseRedisRepository implements Shutt
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Key prefixes for individual entity keys
|
// Key prefixes for individual entity keys
|
||||||
private get stopKeyPrefix() { return this.prefixKey('shuttle:stop:'); }
|
private readonly stopKeyPrefix = 'shuttle:stop:';
|
||||||
private get routeKeyPrefix() { return this.prefixKey('shuttle:route:'); }
|
private readonly routeKeyPrefix = 'shuttle:route:';
|
||||||
private get shuttleKeyPrefix() { return this.prefixKey('shuttle:shuttle:'); }
|
private readonly shuttleKeyPrefix = 'shuttle:shuttle:';
|
||||||
private get orderedStopKeyPrefix() { return this.prefixKey('shuttle:orderedstop:'); }
|
private readonly orderedStopKeyPrefix = 'shuttle:orderedstop:';
|
||||||
private get lastStopKeyPrefix() { return this.prefixKey('shuttle:laststop:'); }
|
private readonly lastStopKeyPrefix = 'shuttle:laststop:';
|
||||||
private get historicalEtaKeyPrefix() { return this.prefixKey('shuttle:eta:historical:'); }
|
private readonly historicalEtaKeyPrefix = 'shuttle:eta:historical:';
|
||||||
|
|
||||||
// Key patterns for bulk operations (e.g., getting all keys, clearing data)
|
// Key patterns for bulk operations (e.g., getting all keys, clearing data)
|
||||||
private get stopKeyPattern() { return this.prefixKey('shuttle:stop:*'); }
|
private readonly stopKeyPattern = 'shuttle:stop:*';
|
||||||
private get routeKeyPattern() { return this.prefixKey('shuttle:route:*'); }
|
private readonly routeKeyPattern = 'shuttle:route:*';
|
||||||
private get shuttleKeyPattern() { return this.prefixKey('shuttle:shuttle:*'); }
|
private readonly shuttleKeyPattern = 'shuttle:shuttle:*';
|
||||||
private get orderedStopKeyPattern() { return this.prefixKey('shuttle:orderedstop:*'); }
|
private readonly orderedStopKeyPattern = 'shuttle:orderedstop:*';
|
||||||
private get lastStopKeyPattern() { return this.prefixKey('shuttle:laststop:*'); }
|
private readonly lastStopKeyPattern = 'shuttle:laststop:*';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a set storing the shuttles that are currently at a stop.
|
* Represents a set storing the shuttles that are currently at a stop.
|
||||||
*/
|
*/
|
||||||
private get shuttleIsAtStopKey() { return this.prefixKey('shuttle:atstop'); }
|
private readonly shuttleIsAtStopKey = 'shuttle:atstop';
|
||||||
|
|
||||||
// Helper methods for Redis key generation
|
// Helper methods for Redis key generation
|
||||||
private readonly createStopKey = (stopId: string) => `${this.stopKeyPrefix}${stopId}`;
|
private readonly createStopKey = (stopId: string) => `${this.stopKeyPrefix}${stopId}`;
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ class RedisShuttleRepositoryHolder implements RepositoryHolder<ShuttleGetterSett
|
|||||||
url: process.env.REDIS_URL,
|
url: process.env.REDIS_URL,
|
||||||
});
|
});
|
||||||
await this.redisClient.connect();
|
await this.redisClient.connect();
|
||||||
this.repo = new RedisShuttleRepository(this.redisClient, 0.001, 'test-system');
|
this.repo = new RedisShuttleRepository(this.redisClient);
|
||||||
return this.repo;
|
return this.repo;
|
||||||
};
|
};
|
||||||
teardown = async () => {
|
teardown = async () => {
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ import { BaseRedisRepository } from "../../BaseRedisRepository";
|
|||||||
import { ETAGetterRepository, ETARepositoryEvent, ETARepositoryEventListener, ETARepositoryEventName } from "./ETAGetterRepository";
|
import { ETAGetterRepository, ETARepositoryEvent, ETARepositoryEventListener, ETARepositoryEventName } from "./ETAGetterRepository";
|
||||||
|
|
||||||
export abstract class BaseRedisETARepository extends BaseRedisRepository implements ETAGetterRepository {
|
export abstract class BaseRedisETARepository extends BaseRedisRepository implements ETAGetterRepository {
|
||||||
private get etaKeyPrefix() { return this.prefixKey('shuttle:eta:'); }
|
private static readonly ETA_KEY_PREFIX = 'shuttle:eta:';
|
||||||
|
|
||||||
// Helper methods
|
// Helper methods
|
||||||
protected createEtaKey = (shuttleId: string, stopId: string) =>
|
protected createEtaKey = (shuttleId: string, stopId: string) =>
|
||||||
`${this.etaKeyPrefix}${shuttleId}:${stopId}`;
|
`${BaseRedisETARepository.ETA_KEY_PREFIX}${shuttleId}:${stopId}`;
|
||||||
|
|
||||||
createRedisHashFromEta = (eta: IEta): Record<string, string> => ({
|
createRedisHashFromEta = (eta: IEta): Record<string, string> => ({
|
||||||
secondsRemaining: eta.secondsRemaining.toString(),
|
secondsRemaining: eta.secondsRemaining.toString(),
|
||||||
@@ -27,7 +27,7 @@ export abstract class BaseRedisETARepository extends BaseRedisRepository impleme
|
|||||||
|
|
||||||
// Getter implementations
|
// Getter implementations
|
||||||
async getEtasForShuttleId(shuttleId: string): Promise<IEta[]> {
|
async getEtasForShuttleId(shuttleId: string): Promise<IEta[]> {
|
||||||
const keys = await this.redisClient.keys(`${this.etaKeyPrefix}${shuttleId}:*`);
|
const keys = await this.redisClient.keys(`${BaseRedisETARepository.ETA_KEY_PREFIX}${shuttleId}:*`);
|
||||||
const etas: IEta[] = [];
|
const etas: IEta[] = [];
|
||||||
|
|
||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
@@ -41,7 +41,7 @@ export abstract class BaseRedisETARepository extends BaseRedisRepository impleme
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getEtasForStopId(stopId: string): Promise<IEta[]> {
|
async getEtasForStopId(stopId: string): Promise<IEta[]> {
|
||||||
const keys = await this.redisClient.keys(`${this.etaKeyPrefix}*`);
|
const keys = await this.redisClient.keys(`${BaseRedisETARepository.ETA_KEY_PREFIX}*`);
|
||||||
const etas: IEta[] = [];
|
const etas: IEta[] = [];
|
||||||
|
|
||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
|
|||||||
@@ -2,17 +2,8 @@ import { IEta } from "../../../entities/ShuttleRepositoryEntities";
|
|||||||
import { BaseRedisETARepository } from "./BaseRedisETARepository";
|
import { BaseRedisETARepository } from "./BaseRedisETARepository";
|
||||||
import { ExternalSourceETARepository } from "./ExternalSourceETARepository";
|
import { ExternalSourceETARepository } from "./ExternalSourceETARepository";
|
||||||
import { ETARepositoryEvent } from "./ETAGetterRepository";
|
import { ETARepositoryEvent } from "./ETAGetterRepository";
|
||||||
import { RedisClientType } from "redis";
|
|
||||||
import createRedisClientForRepository from "../../../helpers/createRedisClientForRepository";
|
|
||||||
|
|
||||||
export class RedisExternalSourceETARepository extends BaseRedisETARepository implements ExternalSourceETARepository {
|
export class RedisExternalSourceETARepository extends BaseRedisETARepository implements ExternalSourceETARepository {
|
||||||
constructor(
|
|
||||||
redisClient: RedisClientType = createRedisClientForRepository(),
|
|
||||||
systemId: string = '',
|
|
||||||
) {
|
|
||||||
super(redisClient, systemId);
|
|
||||||
}
|
|
||||||
|
|
||||||
async addOrUpdateEtaFromExternalSource(eta: IEta): Promise<void> {
|
async addOrUpdateEtaFromExternalSource(eta: IEta): Promise<void> {
|
||||||
await this.addOrUpdateEta(eta);
|
await this.addOrUpdateEta(eta);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,9 +13,8 @@ export class RedisSelfUpdatingETARepository extends BaseRedisETARepository imple
|
|||||||
readonly shuttleRepository: ShuttleGetterRepository,
|
readonly shuttleRepository: ShuttleGetterRepository,
|
||||||
redisClient: RedisClientType = createRedisClientForRepository(),
|
redisClient: RedisClientType = createRedisClientForRepository(),
|
||||||
private referenceTime: Date | null = null,
|
private referenceTime: Date | null = null,
|
||||||
systemId: string = '',
|
|
||||||
) {
|
) {
|
||||||
super(redisClient, systemId);
|
super(redisClient);
|
||||||
|
|
||||||
this.setReferenceTime = this.setReferenceTime.bind(this);
|
this.setReferenceTime = this.setReferenceTime.bind(this);
|
||||||
this.getAverageTravelTimeSeconds = this.getAverageTravelTimeSeconds.bind(this);
|
this.getAverageTravelTimeSeconds = this.getAverageTravelTimeSeconds.bind(this);
|
||||||
@@ -29,7 +28,7 @@ export class RedisSelfUpdatingETARepository extends BaseRedisETARepository imple
|
|||||||
}
|
}
|
||||||
|
|
||||||
private createHistoricalEtaTimeSeriesKey = (routeId: string, fromStopId: string, toStopId: string) => {
|
private createHistoricalEtaTimeSeriesKey = (routeId: string, fromStopId: string, toStopId: string) => {
|
||||||
return this.prefixKey(`shuttle:eta:historical:${routeId}:${fromStopId}:${toStopId}`);
|
return `shuttle:eta:historical:${routeId}:${fromStopId}:${toStopId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
setReferenceTime(referenceTime: Date) {
|
setReferenceTime(referenceTime: Date) {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ class RedisExternalSourceETARepositoryHolder implements RepositoryHolder<Externa
|
|||||||
url: process.env.REDIS_URL,
|
url: process.env.REDIS_URL,
|
||||||
});
|
});
|
||||||
await this.redisClient.connect();
|
await this.redisClient.connect();
|
||||||
this.repo = new RedisExternalSourceETARepository(this.redisClient, 'test-system');
|
this.repo = new RedisExternalSourceETARepository(this.redisClient);
|
||||||
return this.repo;
|
return this.repo;
|
||||||
}
|
}
|
||||||
teardown = async () => {
|
teardown = async () => {
|
||||||
|
|||||||
@@ -22,12 +22,10 @@ class RedisSelfUpdatingETARepositoryHolder implements RepositoryHolder<SelfUpdat
|
|||||||
});
|
});
|
||||||
await this.redisClient.connect();
|
await this.redisClient.connect();
|
||||||
await this.redisClient.flushAll();
|
await this.redisClient.flushAll();
|
||||||
this.shuttleRepo = new RedisShuttleRepository(this.redisClient, 0.001, 'test-system');
|
this.shuttleRepo = new RedisShuttleRepository(this.redisClient);
|
||||||
this.repo = new RedisSelfUpdatingETARepository(
|
this.repo = new RedisSelfUpdatingETARepository(
|
||||||
this.shuttleRepo,
|
this.shuttleRepo,
|
||||||
this.redisClient,
|
this.redisClient,
|
||||||
null,
|
|
||||||
'test-system',
|
|
||||||
);
|
);
|
||||||
return this.repo;
|
return this.repo;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user