Add system ID prefix to all Redis keys to prevent cross-system ID clashes
When multiple university systems share the same Redis instance, entity IDs (shuttles, stops, routes, etc.) could collide. This namespaces all Redis keys with the system ID (e.g., `1:shuttle:stop:123` instead of `shuttle:stop:123`). - Add systemId field and prefixKey() helper to BaseRedisRepository - Update all Redis repository subclasses to use prefixed keys - Wire system ID from InterchangeSystem.build() into Redis repositories - Add migration utility (migrateRedisKeysToSystemPrefix) with tests - Update all test holders to pass a test system ID https://claude.ai/code/session_012Vfz1NHWJbVtoDEWcE5tq6
This commit is contained in:
@@ -3,11 +3,11 @@ import { BaseRedisRepository } from "../../BaseRedisRepository";
|
||||
import { ETAGetterRepository, ETARepositoryEvent, ETARepositoryEventListener, ETARepositoryEventName } from "./ETAGetterRepository";
|
||||
|
||||
export abstract class BaseRedisETARepository extends BaseRedisRepository implements ETAGetterRepository {
|
||||
private static readonly ETA_KEY_PREFIX = 'shuttle:eta:';
|
||||
private get etaKeyPrefix() { return this.prefixKey('shuttle:eta:'); }
|
||||
|
||||
// Helper methods
|
||||
protected createEtaKey = (shuttleId: string, stopId: string) =>
|
||||
`${BaseRedisETARepository.ETA_KEY_PREFIX}${shuttleId}:${stopId}`;
|
||||
`${this.etaKeyPrefix}${shuttleId}:${stopId}`;
|
||||
|
||||
createRedisHashFromEta = (eta: IEta): Record<string, string> => ({
|
||||
secondsRemaining: eta.secondsRemaining.toString(),
|
||||
@@ -27,7 +27,7 @@ export abstract class BaseRedisETARepository extends BaseRedisRepository impleme
|
||||
|
||||
// Getter implementations
|
||||
async getEtasForShuttleId(shuttleId: string): Promise<IEta[]> {
|
||||
const keys = await this.redisClient.keys(`${BaseRedisETARepository.ETA_KEY_PREFIX}${shuttleId}:*`);
|
||||
const keys = await this.redisClient.keys(`${this.etaKeyPrefix}${shuttleId}:*`);
|
||||
const etas: IEta[] = [];
|
||||
|
||||
for (const key of keys) {
|
||||
@@ -41,7 +41,7 @@ export abstract class BaseRedisETARepository extends BaseRedisRepository impleme
|
||||
}
|
||||
|
||||
async getEtasForStopId(stopId: string): Promise<IEta[]> {
|
||||
const keys = await this.redisClient.keys(`${BaseRedisETARepository.ETA_KEY_PREFIX}*`);
|
||||
const keys = await this.redisClient.keys(`${this.etaKeyPrefix}*`);
|
||||
const etas: IEta[] = [];
|
||||
|
||||
for (const key of keys) {
|
||||
|
||||
@@ -2,8 +2,17 @@ import { IEta } from "../../../entities/ShuttleRepositoryEntities";
|
||||
import { BaseRedisETARepository } from "./BaseRedisETARepository";
|
||||
import { ExternalSourceETARepository } from "./ExternalSourceETARepository";
|
||||
import { ETARepositoryEvent } from "./ETAGetterRepository";
|
||||
import { RedisClientType } from "redis";
|
||||
import createRedisClientForRepository from "../../../helpers/createRedisClientForRepository";
|
||||
|
||||
export class RedisExternalSourceETARepository extends BaseRedisETARepository implements ExternalSourceETARepository {
|
||||
constructor(
|
||||
redisClient: RedisClientType = createRedisClientForRepository(),
|
||||
systemId: string = '',
|
||||
) {
|
||||
super(redisClient, systemId);
|
||||
}
|
||||
|
||||
async addOrUpdateEtaFromExternalSource(eta: IEta): Promise<void> {
|
||||
await this.addOrUpdateEta(eta);
|
||||
}
|
||||
|
||||
@@ -13,8 +13,9 @@ export class RedisSelfUpdatingETARepository extends BaseRedisETARepository imple
|
||||
readonly shuttleRepository: ShuttleGetterRepository,
|
||||
redisClient: RedisClientType = createRedisClientForRepository(),
|
||||
private referenceTime: Date | null = null,
|
||||
systemId: string = '',
|
||||
) {
|
||||
super(redisClient);
|
||||
super(redisClient, systemId);
|
||||
|
||||
this.setReferenceTime = this.setReferenceTime.bind(this);
|
||||
this.getAverageTravelTimeSeconds = this.getAverageTravelTimeSeconds.bind(this);
|
||||
@@ -28,7 +29,7 @@ export class RedisSelfUpdatingETARepository extends BaseRedisETARepository imple
|
||||
}
|
||||
|
||||
private createHistoricalEtaTimeSeriesKey = (routeId: string, fromStopId: string, toStopId: string) => {
|
||||
return `shuttle:eta:historical:${routeId}:${fromStopId}:${toStopId}`;
|
||||
return this.prefixKey(`shuttle:eta:historical:${routeId}:${fromStopId}:${toStopId}`);
|
||||
}
|
||||
|
||||
setReferenceTime(referenceTime: Date) {
|
||||
|
||||
@@ -16,7 +16,7 @@ class RedisExternalSourceETARepositoryHolder implements RepositoryHolder<Externa
|
||||
url: process.env.REDIS_URL,
|
||||
});
|
||||
await this.redisClient.connect();
|
||||
this.repo = new RedisExternalSourceETARepository(this.redisClient);
|
||||
this.repo = new RedisExternalSourceETARepository(this.redisClient, 'test-system');
|
||||
return this.repo;
|
||||
}
|
||||
teardown = async () => {
|
||||
|
||||
@@ -22,10 +22,12 @@ class RedisSelfUpdatingETARepositoryHolder implements RepositoryHolder<SelfUpdat
|
||||
});
|
||||
await this.redisClient.connect();
|
||||
await this.redisClient.flushAll();
|
||||
this.shuttleRepo = new RedisShuttleRepository(this.redisClient);
|
||||
this.shuttleRepo = new RedisShuttleRepository(this.redisClient, 0.001, 'test-system');
|
||||
this.repo = new RedisSelfUpdatingETARepository(
|
||||
this.shuttleRepo,
|
||||
this.redisClient,
|
||||
null,
|
||||
'test-system',
|
||||
);
|
||||
return this.repo;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user