This repository has been archived by the owner on Dec 9, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #10 from zarathustra323/graphql-redis-cache
Cache site context and GraphQL responses via Redis
- Loading branch information
Showing
20 changed files
with
340 additions
and
30 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,20 @@ | ||
const { apolloClient } = require('@parameter1/base-cms-express-apollo'); | ||
const { parseBooleanHeader } = require('@parameter1/base-cms-utils'); | ||
|
||
module.exports = (app, uri, config = {}) => { | ||
app.use(apolloClient(uri, config, config.link)); | ||
/** | ||
* Force sets GraphQL cache headers when cookies are present on the site. | ||
* This can either force enable cache when globally disabled, or force disable cache | ||
* when globally enabled. | ||
*/ | ||
const contextFn = ({ req }) => { | ||
const headers = ['x-cache-site-context', 'x-cache-responses'].reduce((o, key) => { | ||
const cookie = req.cookies[key]; | ||
if (!cookie) return o; // do nothing if no cookie is set. | ||
// otherwise parse the value and set the GraphQL header to match. | ||
return { ...o, [key]: parseBooleanHeader(cookie) }; | ||
}, {}); | ||
return { headers }; | ||
}; | ||
app.use(apolloClient(uri, config, config.link, contextFn)); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
module.exports = (value) => { | ||
const falsey = ['false', '0']; | ||
if (falsey.includes(value)) return false; | ||
return Boolean(value); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
144 changes: 144 additions & 0 deletions
144
services/graphql-server/src/graphql/plugins/redis-cache.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,144 @@ | ||
const { createHash } = require('crypto'); | ||
const { parseBooleanHeader } = require('@parameter1/base-cms-utils'); | ||
const redis = require('../../redis'); | ||
|
||
/** | ||
* | ||
* @param {GraphQLRequestContext} requestContext | ||
*/ | ||
const isGraphQLQuery = (requestContext) => { | ||
const { operation } = requestContext; | ||
return operation && operation.operation === 'query'; | ||
}; | ||
|
||
/** | ||
* | ||
* @param {GraphQLRequestContext} requestContext | ||
*/ | ||
const isIntrospectionQuery = (requestContext) => { | ||
const { operation } = requestContext; | ||
return operation.selectionSet.selections.every((selection) => { | ||
const fieldName = selection.name.value; | ||
return fieldName.startsWith('__'); | ||
}); | ||
}; | ||
|
||
/** | ||
* | ||
* @param {object} obj The object to use to generate the key. | ||
*/ | ||
const stringifyCacheKey = obj => createHash('sha256').update(JSON.stringify(obj)).digest('hex'); | ||
|
||
/** | ||
* | ||
* @param {string} tenant The tenant key. Is used in the key prefix. | ||
* @param {object} obj The object to use to generate the key. | ||
*/ | ||
const createKey = (tenant, obj) => `base_gql:${tenant}:${stringifyCacheKey(obj)}`; | ||
|
||
const setHeader = (http, key, value) => { | ||
if (http) http.headers.set(key, `${value}`); | ||
}; | ||
|
||
/** | ||
* @todo Eventually this should adhere to `@cacheControl` directives. | ||
* @todo Adjust header-based cache enabling to hook function | ||
*/ | ||
class RedisCacheGraphQLPlugin { | ||
/** | ||
* | ||
* @param {object} params | ||
* @param {boolean} [params.enabled=true] Whether cache is globally enabled | ||
* @param {function} [params.onCacheError] A function to invoke when a cache-set error is thrown. | ||
*/ | ||
constructor({ enabled = true, onCacheError } = {}) { | ||
this.enabled = enabled; | ||
this.redis = redis; | ||
this.onCacheError = onCacheError; | ||
} | ||
|
||
/** | ||
* | ||
*/ | ||
requestDidStart() { | ||
let cacheKeyObj; | ||
let cacheKey; | ||
let age; | ||
|
||
return { | ||
/** | ||
* | ||
* @param {GraphQLRequestContext} requestContext | ||
*/ | ||
responseForOperation: async (requestContext) => { | ||
if (!this.canCache(requestContext)) return null; | ||
const { context } = requestContext; | ||
|
||
cacheKeyObj = { | ||
source: requestContext.source, // the raw query source string | ||
operationName: requestContext.operationName, // the op name (if set) | ||
variables: { ...(requestContext.request.variables || {}) }, // all query vars | ||
context: { siteId: context.site.id() }, // specific context variables | ||
}; | ||
|
||
cacheKey = createKey(context.tenant, cacheKeyObj); | ||
|
||
const serialized = await this.redis.get(cacheKey); | ||
if (!serialized) return null; | ||
|
||
const cacheValue = JSON.parse(serialized); | ||
// eslint-disable-next-line no-param-reassign | ||
requestContext.metrics.responseCacheHit = true; | ||
age = Math.round((Date.now() - cacheValue.cacheTime) / 1000); | ||
return { data: cacheValue.data }; | ||
}, | ||
|
||
/** | ||
* | ||
* @param {GraphQLRequestContext} requestContext | ||
*/ | ||
willSendResponse: (requestContext) => { | ||
if (!this.canCache(requestContext)) return; | ||
const { response } = requestContext; | ||
const { http } = response; | ||
const { responseCacheHit: hit } = requestContext.metrics; | ||
setHeader(http, 'x-cache', hit ? 'hit' : 'miss'); | ||
|
||
if (hit) { | ||
// do not write cache for cached responses. but set the age header. | ||
if (age != null) setHeader(http, 'age', age); | ||
return; | ||
} | ||
|
||
const cacheValue = { | ||
data: response.data, | ||
cacheTime: Date.now(), | ||
}; | ||
if (!cacheKey) throw new Error('Unable to get cache key from previous hook.'); | ||
|
||
// set cache but do not await | ||
const { onCacheError } = this; | ||
redis.set(cacheKey, JSON.stringify(cacheValue), 'EX', 30).catch((e) => { | ||
if (typeof onCacheError === 'function') onCacheError(e); | ||
}); | ||
}, | ||
}; | ||
} | ||
|
||
/** | ||
* | ||
* @param {GraphQLRequestContext} requestContext | ||
*/ | ||
canCache(requestContext) { | ||
if (!this.enabled) return false; | ||
const { request, response } = requestContext; | ||
const { http } = request; | ||
if (!http) return false; | ||
const cacheEnabled = parseBooleanHeader(http.headers.get('x-cache-responses')); | ||
if (!cacheEnabled) return false; | ||
const { errors } = response || {}; | ||
return isGraphQLQuery(requestContext) && !isIntrospectionQuery(requestContext) && !errors; | ||
} | ||
} | ||
|
||
module.exports = RedisCacheGraphQLPlugin; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.