fix: address Bugbot review findings

- Skip module-level integration cache for multi-instance (REDIS_IS_EXTERNAL)
- Add invalidateBoardCache to permission mutations (prevents future leak)
- Remove unused cacheTags.integrationData
- Replace KEYS with SCAN in Redis cache handler refreshTags()
- Document REDIS_CACHE_DB env var
This commit is contained in:
Thomas Camlong committed 2026-08-07 19:04:53 +02:00
1 parent 018f98e318
commit 6b45aca5d5
6 files changed
+25 -16

No files matched your search

-1
View File
@@ -10,7 +10,6 @@ describe("cacheTags", () => {
expect(cacheTags.boardList()).toBe("board-list");
expect(cacheTags.user("u1")).toBe("user:u1");
expect(cacheTags.integration("i1")).toBe("integration:i1");
expect(cacheTags.integrationData("i1", "downloads")).toBe("integration-data:i1:downloads");
expect(cacheTags.app("a1")).toBe("app:a1");
});
-1
View File
@@ -5,6 +5,5 @@ export const cacheTags = {
boardList: () => "board-list" as const,
user: (userId: string) => `user:${userId}` as const,
integration: (integrationId: string) => `integration:${integrationId}` as const,
integrationData: (integrationId: string, query: string) => `integration-data:${integrationId}:${query}` as const,
app: (appId: string) => `app:${appId}` as const,
} as const;
+8 -2
View File
@@ -1,5 +1,7 @@
// ponytail: module-level cache — one snapshot per integration per query key.
// Ceiling: stale data for up to `ttlMs` (60s default). Upgrade: Redis-backed with pub/sub invalidation.
// ponytail: module-level cache is single-instance only.
// Multi-instance deployments (REDIS_IS_EXTERNAL=true) bypass this and rely on
// the Redis-backed Next cache handler for cross-instance coordination.
const isMultiInstance = process.env.REDIS_IS_EXTERNAL === "true";
interface CachedEntry<T> {
data: T;
@@ -19,6 +21,10 @@ export const getCachedIntegrationData = async <T>(
fetcher: () => Promise<T>,
ttlMs = DEFAULT_TTL_MS,
): Promise<T> => {
if (isMultiInstance) {
return fetcher();
}
const key = cacheKey(integrationId, queryKey);
const existing = cache.get(key) as CachedEntry<T> | undefined;
const now = Date.now();
+2
View File
@@ -1426,6 +1426,7 @@ export const boardRouter = createTRPCRouter({
for (const userId of userIds) {
invalidateUserCache(userId);
}
invalidateBoardCache(input.entityId);
}),
saveGroupBoardPermissions: protectedProcedure.input(boardSavePermissionsSchema).mutation(async ({ input, ctx }) => {
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.entityId), "full");
@@ -1488,6 +1489,7 @@ export const boardRouter = createTRPCRouter({
invalidateUserCache(member.userId);
}
}
invalidateBoardCache(input.entityId);
}),
importOldmarrConfig: permissionRequiredProcedure
.requiresPermission("board-create")