From 3cfe6d2cbb4256bb667cbf4315e1df7fce6e6e5c Mon Sep 17 00:00:00 2001 From: Naz Date: Wed, 22 Feb 2023 15:17:39 +0800 Subject: [PATCH] Added cache support to api-framework refs https://github.com/TryGhost/Toolbox/issues/522 - API-level response caching allows to cache responses bypassing the "pipeline" processing - The main usecase for these caches is caching GET requests for expensive Content API requests - To enable response caching add a "cache" key with a cache instance as a value, for example for posts public cache configuration can look like: ``` module.exports = { docName: 'posts', browse: { cache: postsPublicService.api.cache, options: [ ... ``` --- ghost/api-framework/lib/pipeline.js | 16 ++++- ghost/api-framework/test/pipeline.test.js | 83 +++++++++++++++++++++++ 2 files changed, 97 insertions(+), 2 deletions(-) diff --git a/ghost/api-framework/lib/pipeline.js b/ghost/api-framework/lib/pipeline.js index 45856281f5..5d6e1bbb0a 100644 --- a/ghost/api-framework/lib/pipeline.js +++ b/ghost/api-framework/lib/pipeline.js @@ -187,7 +187,7 @@ const pipeline = (apiController, apiUtils, apiType) => { return keys.reduce((obj, method) => { const apiImpl = _.cloneDeep(apiController)[method]; - obj[method] = function wrapper() { + obj[method] = async function wrapper() { const apiConfig = {docName, method}; let options; let data; @@ -229,6 +229,15 @@ const pipeline = (apiController, apiUtils, apiType) => { frame.docName = docName; frame.method = method; + let cacheKey = JSON.stringify(frame.options); + if (apiImpl.cache) { + const response = await apiImpl.cache.get(cacheKey); + + if (response) { + return Promise.resolve(response); + } + } + return Promise.resolve() .then(() => { return STAGES.validation.input(apiUtils, apiConfig, apiImpl, frame); @@ -245,7 +254,10 @@ const pipeline = (apiController, apiUtils, apiType) => { .then((response) => { return STAGES.serialisation.output(response, apiUtils, apiConfig, apiImpl, frame); }) - .then(() => { + .then(async () => { + if (apiImpl.cache) { + await apiImpl.cache.set(cacheKey, frame.response); + } return frame.response; }); }; diff --git a/ghost/api-framework/test/pipeline.test.js b/ghost/api-framework/test/pipeline.test.js index 0f6786cb7d..3e57081eaa 100644 --- a/ghost/api-framework/test/pipeline.test.js +++ b/ghost/api-framework/test/pipeline.test.js @@ -250,4 +250,87 @@ describe('Pipeline', function () { }); }); }); + + describe('caching', function () { + beforeEach(function () { + sinon.stub(shared.pipeline.STAGES.validation, 'input'); + sinon.stub(shared.pipeline.STAGES.serialisation, 'input'); + sinon.stub(shared.pipeline.STAGES.serialisation, 'output'); + sinon.stub(shared.pipeline.STAGES, 'permissions'); + sinon.stub(shared.pipeline.STAGES, 'query'); + }); + + it('should set a cache if configured on endpoint level', async function () { + const apiController = { + browse: { + cache: { + get: sinon.stub().resolves(null), + set: sinon.stub().resolves(true) + } + } + }; + + const apiUtils = {}; + const result = shared.pipeline(apiController, apiUtils); + + shared.pipeline.STAGES.validation.input.resolves(); + shared.pipeline.STAGES.serialisation.input.resolves(); + shared.pipeline.STAGES.permissions.resolves(); + shared.pipeline.STAGES.query.resolves('response'); + shared.pipeline.STAGES.serialisation.output.callsFake(function (response, _apiUtils, apiConfig, apiImpl, frame) { + frame.response = response; + }); + + const response = await result.browse(); + + response.should.eql('response'); + + // request went through all stages + shared.pipeline.STAGES.validation.input.calledOnce.should.be.true(); + shared.pipeline.STAGES.serialisation.input.calledOnce.should.be.true(); + shared.pipeline.STAGES.permissions.calledOnce.should.be.true(); + shared.pipeline.STAGES.query.calledOnce.should.be.true(); + shared.pipeline.STAGES.serialisation.output.calledOnce.should.be.true(); + + // cache was set + apiController.browse.cache.set.calledOnce.should.be.true(); + apiController.browse.cache.set.args[0][1].should.equal('response'); + }); + + it('should use cache if configured on endpoint level', async function () { + const apiController = { + browse: { + cache: { + get: sinon.stub().resolves('CACHED RESPONSE'), + set: sinon.stub().resolves(true) + } + } + }; + + const apiUtils = {}; + const result = shared.pipeline(apiController, apiUtils); + + shared.pipeline.STAGES.validation.input.resolves(); + shared.pipeline.STAGES.serialisation.input.resolves(); + shared.pipeline.STAGES.permissions.resolves(); + shared.pipeline.STAGES.query.resolves('response'); + shared.pipeline.STAGES.serialisation.output.callsFake(function (response, _apiUtils, apiConfig, apiImpl, frame) { + frame.response = response; + }); + + const response = await result.browse(); + + response.should.eql('CACHED RESPONSE'); + + // request went through all stages + shared.pipeline.STAGES.validation.input.calledOnce.should.be.false(); + shared.pipeline.STAGES.serialisation.input.calledOnce.should.be.false(); + shared.pipeline.STAGES.permissions.calledOnce.should.be.false(); + shared.pipeline.STAGES.query.calledOnce.should.be.false(); + shared.pipeline.STAGES.serialisation.output.calledOnce.should.be.false(); + + // cache not set + apiController.browse.cache.set.calledOnce.should.be.false(); + }); + }); });