diff --git a/api/package.json b/api/package.json index 7c88597be..fe84e164a 100644 --- a/api/package.json +++ b/api/package.json @@ -36,6 +36,7 @@ "@sentry/tracing": "^7.55.2", "got": "^13.0.0", "ioredis": "^5.3.2", + "ioredis-mock": "^8.7.0", "koa": "^2.14.2", "koa-body": "^6.0.1", "koa-static": "^5.0.0", @@ -46,6 +47,7 @@ "swagger-ui-dist": "^4.15.5" }, "devDependencies": { + "@types/ioredis-mock": "^8.2.2", "@types/koa": "^2.13.6", "@types/koa-static": "^4.0.2", "@types/koa__cors": "^4.0.0", diff --git a/api/source/index.ts b/api/source/index.ts index 05cd9a96d..50a9bc390 100644 --- a/api/source/index.ts +++ b/api/source/index.ts @@ -9,6 +9,7 @@ import { catchErrors } from './errors.js' import openapi from './openapi.json' assert { type: 'json' } import { plausibleMiddleware } from './plausible.js' import { rateLimiterMiddleware } from './rate-limiter.js' +import { redisCacheMiddleware } from './redis-cache.js' import { docRoutes } from './route/doc.js' import { openapiRoutes } from './route/openapi.js' import Sentry, { requestHandler, tracingMiddleWare } from './sentry.js' @@ -43,7 +44,13 @@ router.use('/api/v1', docRoutes(), openapiRoutes(openapi)) const apiRoutes = publicodesAPI(new Engine(rules)) -router.use('/api/v1', plausibleMiddleware, rateLimiterMiddleware, apiRoutes) +router.use( + '/api/v1', + rateLimiterMiddleware, + redisCacheMiddleware(), + plausibleMiddleware, + apiRoutes +) app.use(router.routes()) app.use(router.allowedMethods()) diff --git a/api/source/redis-cache.ts b/api/source/redis-cache.ts new file mode 100644 index 000000000..474e2906b --- /dev/null +++ b/api/source/redis-cache.ts @@ -0,0 +1,50 @@ +import Router from '@koa/router' +import IORedis from 'ioredis' +import IORedisMock from 'ioredis-mock' +import { koaBody } from 'koa-body' + +const Redis = IORedis.default +const RedisMock = IORedisMock.default + +// cache expires in 2 hours +const CACHE_EXPIRE = 2 * 60 * 60 + +const redis = + process.env.NODE_ENV === 'production' && process.env.SCALINGO_REDIS_URL + ? new Redis(process.env.SCALINGO_REDIS_URL, { + enableOfflineQueue: false, + }) + : new RedisMock() + +export const redisCacheMiddleware = () => { + const router = new Router() + + router.post('/evaluate', koaBody(), async (ctx, next) => { + if (!redis || !ctx.request.body) { + await next() + + return + } + + const cacheKey = JSON.stringify(ctx.request.body) + const cachedResponse = await redis.get(cacheKey) + if (cachedResponse) { + ctx.body = JSON.parse(cachedResponse) as unknown + + return + } + + await next() + + if (ctx.status === 200) { + await redis.set( + cacheKey, + JSON.stringify({ responseCachedAt: Date.now(), ...ctx.body }), + 'EX', + CACHE_EXPIRE + ) + } + }) + + return router.routes() +} diff --git a/yarn.lock b/yarn.lock index 9cbc4dcde..6213e1ad9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3579,7 +3579,14 @@ __metadata: languageName: node linkType: hard -"@ioredis/commands@npm:^1.1.1": +"@ioredis/as-callback@npm:^3.0.0": + version: 3.0.0 + resolution: "@ioredis/as-callback@npm:3.0.0" + checksum: 2835e39631497fe4f8b07d95576abea165c9f7efef81e9e55c733588051ff4edcb31eeb59f36127923dae0cb1a8e21b4e27ee3ab79a065a0baeecf861c3bc0b1 + languageName: node + linkType: hard + +"@ioredis/commands@npm:^1.1.1, @ioredis/commands@npm:^1.2.0": version: 1.2.0 resolution: "@ioredis/commands@npm:1.2.0" checksum: 9b20225ba36ef3e5caf69b3c0720597c3016cc9b1e157f519ea388f621dd9037177f84cfe7e25c4c32dad7dd90c70ff9123cd411f747e053cf292193c9c461e2 @@ -8863,7 +8870,16 @@ __metadata: languageName: node linkType: hard -"@types/formidable@npm:^2.0.4, @types/formidable@npm:^2.0.5": +"@types/formidable@npm:^2.0.4": + version: 2.0.6 + resolution: "@types/formidable@npm:2.0.6" + dependencies: + "@types/node": "*" + checksum: d6be0ac12bf8dd2e4f8a022271ee6e501c7f6d7dd58d71c68497ca7da84bee1538d1a2a64a90b56dad557ddb291d48c5731206269e9ab53ed91264e68a4d1476 + languageName: node + linkType: hard + +"@types/formidable@npm:^2.0.5": version: 2.0.5 resolution: "@types/formidable@npm:2.0.5" dependencies: @@ -8970,6 +8986,15 @@ __metadata: languageName: node linkType: hard +"@types/ioredis-mock@npm:^8.2.2": + version: 8.2.2 + resolution: "@types/ioredis-mock@npm:8.2.2" + dependencies: + ioredis: ">=5" + checksum: caf0fd904d84ec6da31cde540cd04a0619e20ea42f88f2b11758c7afd24a9b4213661589c74efb03e109ba24cbd90794e0ef34294c7e5f9fe0c033536509a83e + languageName: node + linkType: hard + "@types/istanbul-lib-coverage@npm:*, @types/istanbul-lib-coverage@npm:^2.0.0, @types/istanbul-lib-coverage@npm:^2.0.1": version: 2.0.4 resolution: "@types/istanbul-lib-coverage@npm:2.0.4" @@ -10277,6 +10302,7 @@ __metadata: "@publicodes/api": ^1.0.0-beta.70 "@sentry/node": ^7.55.2 "@sentry/tracing": ^7.55.2 + "@types/ioredis-mock": ^8.2.2 "@types/koa": ^2.13.6 "@types/koa-static": ^4.0.2 "@types/koa__cors": ^4.0.0 @@ -10286,6 +10312,7 @@ __metadata: chai-http: ^4.4.0 got: ^13.0.0 ioredis: ^5.3.2 + ioredis-mock: ^8.7.0 koa: ^2.14.2 koa-body: ^6.0.1 koa-static: ^5.0.0 @@ -15278,6 +15305,26 @@ __metadata: languageName: node linkType: hard +"fengari-interop@npm:^0.1.3": + version: 0.1.3 + resolution: "fengari-interop@npm:0.1.3" + peerDependencies: + fengari: ^0.1.0 + checksum: f483e0aedec3a0b49911ffd3207a55f73c861f95ef84fb7582deb45bc65afa2e7bf8f9fc3734563f6c106a438909f94059b5d08f5a7872ffcc3d45d260a3ee15 + languageName: node + linkType: hard + +"fengari@npm:^0.1.4": + version: 0.1.4 + resolution: "fengari@npm:0.1.4" + dependencies: + readline-sync: ^1.4.9 + sprintf-js: ^1.1.1 + tmp: ^0.0.33 + checksum: bd6b04f9738f9cbb58e3c80684a72bf6f7e74c902d26e4e812b1cfbd69c06da5ffb2a8a67a62ecd75603fc565b6b183c1b72be60f2e60c18aa487aad65b62196 + languageName: node + linkType: hard + "fetch-blob@npm:^3.1.2, fetch-blob@npm:^3.1.4": version: 3.2.0 resolution: "fetch-blob@npm:3.2.0" @@ -17446,7 +17493,23 @@ __metadata: languageName: node linkType: hard -"ioredis@npm:^5.3.2": +"ioredis-mock@npm:^8.7.0": + version: 8.7.0 + resolution: "ioredis-mock@npm:8.7.0" + dependencies: + "@ioredis/as-callback": ^3.0.0 + "@ioredis/commands": ^1.2.0 + fengari: ^0.1.4 + fengari-interop: ^0.1.3 + semver: ^7.3.8 + peerDependencies: + "@types/ioredis-mock": ^8 + ioredis: ^5 + checksum: 46a4ee40b899e950569cdfa3f009b797fc5c5acbdc6b1784f8cdeb54ba781ed288b61c4c86c88760a02ea6a481e655660df0507096f93c66c2e8285f029ff8fd + languageName: node + linkType: hard + +"ioredis@npm:>=5, ioredis@npm:^5.3.2": version: 5.3.2 resolution: "ioredis@npm:5.3.2" dependencies: @@ -22576,7 +22639,7 @@ __metadata: languageName: node linkType: hard -"qs@npm:^6.11.2": +"qs@npm:^6.11.2, qs@npm:^6.4.0": version: 6.11.2 resolution: "qs@npm:6.11.2" dependencies: @@ -22585,15 +22648,6 @@ __metadata: languageName: node linkType: hard -"qs@npm:^6.4.0": - version: 6.11.1 - resolution: "qs@npm:6.11.1" - dependencies: - side-channel: ^1.0.4 - checksum: 82ee78ef12a16f3372fae5b64f76f8aedecb000feea882bbff1af146c147f6eb66b08f9c3f34d7e076f28563586956318b9b2ca41141846cdd6d5ad6f241d52f - languageName: node - linkType: hard - "qs@npm:~6.5.2": version: 6.5.3 resolution: "qs@npm:6.5.3" @@ -23360,6 +23414,13 @@ __metadata: languageName: node linkType: hard +"readline-sync@npm:^1.4.9": + version: 1.4.10 + resolution: "readline-sync@npm:1.4.10" + checksum: 4dbd8925af028dc4cb1bb813f51ca3479035199aa5224886b560eec8e768ab27d7ebf11d69a67ed93d5a130b7c994f0bdb77796326e563cf928bbfd560e3747e + languageName: node + linkType: hard + "real-require@npm:^0.2.0": version: 0.2.0 resolution: "real-require@npm:0.2.0"