diff --git a/server/routers/external.ts b/server/routers/external.ts index 2b95c2bfc..33fcb3ab3 100644 --- a/server/routers/external.ts +++ b/server/routers/external.ts @@ -500,6 +500,12 @@ authenticated.get( launcher.listLauncherViews ); +authenticated.post( + "/org/:orgId/launcher/invalidate-cache", + verifyOrgAccess, + launcher.invalidateLauncherCache +); + authenticated.post( "/org/:orgId/launcher/views", verifyOrgAccess, diff --git a/server/routers/launcher/index.ts b/server/routers/launcher/index.ts index b9edafc22..1c3fed44c 100644 --- a/server/routers/launcher/index.ts +++ b/server/routers/launcher/index.ts @@ -10,3 +10,4 @@ export { updateLauncherView } from "./updateLauncherView"; export { deleteLauncherView } from "./deleteLauncherView"; export { upsertLauncherDefaultView } from "./upsertLauncherDefaultView"; export { deleteLauncherDefaultView } from "./deleteLauncherDefaultView"; +export { invalidateLauncherCache } from "./invalidateLauncherCache"; diff --git a/server/routers/launcher/invalidateLauncherCache.ts b/server/routers/launcher/invalidateLauncherCache.ts new file mode 100644 index 000000000..5e4ff1618 --- /dev/null +++ b/server/routers/launcher/invalidateLauncherCache.ts @@ -0,0 +1,65 @@ +import { regionalCache as cache } from "#dynamic/lib/cache"; +import { response } from "@server/lib/response"; +import HttpCode from "@server/types/HttpCode"; +import { NextFunction, Request, Response } from "express"; +import createHttpError from "http-errors"; + +async function invalidateLauncherCacheForUser( + orgId: string, + userId: string +): Promise { + const prefixes = [ + `launcherAccessibleIds:${orgId}:${userId}:`, + `launcher:groups:${orgId}:${userId}:`, + `launcher:results:${orgId}:${userId}:`, + `launcher:scale:counts:${orgId}:${userId}:` + ]; + + const keys = ( + await Promise.all( + prefixes.map((prefix) => cache.keysWithPrefix(prefix)) + ) + ).flat(); + + if (keys.length > 0) { + await cache.del(keys); + } +} + +export async function invalidateLauncherCache( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const orgId = req.userOrgId; + const userId = req.user!.userId; + + if (!orgId) { + return next( + createHttpError(HttpCode.BAD_REQUEST, "Invalid organization ID") + ); + } + + await invalidateLauncherCacheForUser(orgId, userId); + + return response(res, { + data: null, + success: true, + error: false, + message: "Launcher cache invalidated successfully", + status: HttpCode.OK + }); + } catch (error) { + if (createHttpError.isHttpError(error)) { + return next(error); + } + console.error("Error invalidating launcher cache:", error); + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "Internal server error" + ) + ); + } +} diff --git a/src/components/resource-launcher/ResourceLauncher.tsx b/src/components/resource-launcher/ResourceLauncher.tsx index 9f9d44d11..3f1249efc 100644 --- a/src/components/resource-launcher/ResourceLauncher.tsx +++ b/src/components/resource-launcher/ResourceLauncher.tsx @@ -44,7 +44,7 @@ import { type LauncherViewConfig, type LauncherViewRecord } from "@server/routers/launcher/types"; -import { useMutation, useQuery } from "@tanstack/react-query"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Search } from "lucide-react"; import { useTranslations } from "next-intl"; import { useRouter } from "next/navigation"; @@ -105,6 +105,7 @@ export default function ResourceLauncher({ const { toast } = useToast(); const { env } = useEnvContext(); const api = createApiClient({ env }); + const queryClient = useQueryClient(); const router = useRouter(); const { navigate, isNavigating, searchParams } = useNavigationContext(); const [isRefreshing, startRefreshTransition] = useTransition(); @@ -489,6 +490,10 @@ export default function ResourceLauncher({ const refreshData = () => { startRefreshTransition(async () => { try { + await api.post(`/org/${orgId}/launcher/invalidate-cache`); + await queryClient.invalidateQueries({ + queryKey: ["ORG", orgId, "LAUNCHER"] + }); router.refresh(); } catch { toast({