11import NodeCache from 'node-cache' ;
2+ import { z } from 'zod' ;
23
34import type { Eatery , Event } from '@prisma/client' ;
45
@@ -9,55 +10,64 @@ import {
910 Router ,
1011} from 'express' ;
1112
12- import { EaterySchema } from '../eateries/eateries.schema .js' ;
13+ import { validateRequest } from '../middleware/validateRequest .js' ;
1314import { prisma } from '../prisma.js' ;
1415import { UnauthorizedError } from './AppError.js' ;
1516
1617export const cacheRouter = Router ( ) ;
17- export const appCache = new NodeCache ( { stdTTL : 0 } ) ; // never expire
18+ const appCache = new NodeCache ( { stdTTL : 0 } ) ; // never expire
1819
1920export type EateryWithEvents = Eatery & { events : Event [ ] } ;
2021
21- function requireSecret ( req : Request , _res : Response , next : NextFunction ) : void {
22+ /**
23+ * Cache keys used throughout the application
24+ */
25+ const CACHE_KEYS = {
26+ ALL_EATERIES_DATA : 'allEateriesData' ,
27+ ALL_EATERIES_ETAG : 'allEateriesEtag' ,
28+ } as const ;
29+
30+ /**
31+ * Schema for validating cached eatery data
32+ * This will be validated by the scraper that sends the data
33+ * Max 100 eateries for safety to prevent memory issues
34+ */
35+ const allEateriesSchema = z . object ( {
36+ body : z . object ( {
37+ eateries : z . array ( z . any ( ) ) . max ( 100 ) ,
38+ } ) ,
39+ } ) ;
40+
41+ function requireCacheRefreshSecret (
42+ req : Request ,
43+ _res : Response ,
44+ next : NextFunction ,
45+ ) : void {
2246 const provided = req . header ( process . env . CACHE_REFRESH_HEADER ! ) ;
2347 if ( ! provided || provided !== process . env . CACHE_REFRESH_SECRET ) {
24- throw new UnauthorizedError ( ) ;
48+ throw new UnauthorizedError ( 'Invalid cache refresh secret provided' ) ;
2549 }
2650 next ( ) ;
2751}
2852
2953export function getAllEateriesData ( ) : EateryWithEvents [ ] {
30- const data = appCache . get < EateryWithEvents [ ] > ( 'allEateriesData' ) ;
54+ const data = appCache . get < EateryWithEvents [ ] > ( CACHE_KEYS . ALL_EATERIES_DATA ) ;
3155 if ( ! data ) {
32- const err = new Error (
33- 'Cache is cold. allEateriesData is not set.' ,
34- ) as Error & { status ?: number } ;
35- err . status = 503 ;
36- throw err ;
56+ throw new Error ( 'Cache miss: eateries data not found in cache' ) ;
3757 }
3858 return data ;
3959}
4060
41- // Private cache refresh route for scraper
42- cacheRouter . post ( '/internal/refresh-cache' , requireSecret , ( req , res ) => {
43- const parse = EaterySchema . safeParse ( req . body ) ;
44- if ( ! parse . success ) {
45- res
46- . status ( 400 )
47- . json ( { error : 'Invalid payload' , issues : parse . error . issues } ) ;
48- return ;
61+ export function getEateriesEtag ( ) : string {
62+ const etag = appCache . get < string > ( CACHE_KEYS . ALL_EATERIES_ETAG ) ;
63+ if ( ! etag ) {
64+ throw new Error ( 'Cache miss: eateries ETag not found in cache' ) ;
4965 }
50-
51- appCache . set ( 'allEateriesData' , parse . data ) ;
52- const etag = `"eateries-${ Date . now ( ) } "` ;
53- appCache . set ( 'allEateriesEtag' , etag ) ;
54-
55- console . log ( 'Cache updated, allEateriesData refreshed.' ) ;
56- res . status ( 200 ) . json ( { ok : true } ) ;
57- } ) ;
66+ return etag ;
67+ }
5868
5969export async function refreshCacheFromDB ( ) {
60- appCache . del ( 'allEateriesData' ) ;
70+ clearAppCache ( ) ;
6171 const eateries = await prisma . eatery . findMany ( {
6272 include : {
6373 events : {
@@ -80,12 +90,36 @@ export async function refreshCacheFromDB() {
8090 } ,
8191 } ,
8292 } ) ;
83- appCache . set ( 'allEateriesData' , eateries ) ;
84- const etag = `"eateries-${ Date . now ( ) } "` ;
85- appCache . set ( 'allEateriesEtag' , etag ) ;
86- console . log ( 'Cache updated from DB, allEateriesData refreshed.' ) ;
93+ populateCache ( eateries ) ;
8794}
8895
8996export function clearAppCache ( ) : void {
9097 appCache . flushAll ( ) ;
9198}
99+
100+ function populateCache ( eateries : EateryWithEvents [ ] ) : void {
101+ appCache . set ( CACHE_KEYS . ALL_EATERIES_DATA , eateries ) ;
102+ const etag = `"eateries-${ Date . now ( ) } "` ;
103+ appCache . set ( CACHE_KEYS . ALL_EATERIES_ETAG , etag ) ;
104+ console . log (
105+ `Cache updated at ${ new Date ( ) . toISOString ( ) } , ${ eateries . length } eateries cached.` ,
106+ ) ;
107+ }
108+
109+ /**
110+ * Private cache refresh route for scraper
111+ * Allows the scraper to update the cache with new eatery data
112+ */
113+ cacheRouter . post (
114+ '/' ,
115+ requireCacheRefreshSecret ,
116+ validateRequest ( allEateriesSchema ) ,
117+ ( req , res ) => {
118+ const { eateries } = req . body ;
119+ populateCache ( eateries ) ;
120+ return res . status ( 200 ) . json ( {
121+ message : 'Cache refreshed successfully' ,
122+ count : eateries . length ,
123+ } ) ;
124+ } ,
125+ ) ;
0 commit comments