From 4e24accd24313d3f828003782ca8e6bd1d078087 Mon Sep 17 00:00:00 2001 From: David Heidrich Date: Mon, 4 Mar 2019 15:30:01 +0100 Subject: [PATCH] feat(cacheModifier): Added cache modifier function, bumped dependencies --- README.md | 4 ++ package.json | 8 +-- src/proxyCacheLink.js | 6 +- src/proxyCacheMiddleware.js | 127 ++++++++++++++++++------------------ src/utils.js | 10 ++- yarn.lock | 16 ++--- 6 files changed, 91 insertions(+), 80 deletions(-) diff --git a/README.md b/README.md index ac5561f..21c8fef 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,10 @@ interface Cache { Javascript's `Map` implements this interface, so you can use that as default. +### Customize the cache key + +You can pass a function as second argument (`type CacheKeyModifier = (?string, ?Object) => ?string`) on `proxyCacheLink` and `proxyCacheMiddleware` that allows you to modify the key before saving. This is useful if your queries depend on a global context. e.g. a http header that modifies the result independend of the query parameters (e.g. `Accept-Language`) + ### TODO - Add support for batch queries diff --git a/package.json b/package.json index 8a38a20..dd06b88 100644 --- a/package.json +++ b/package.json @@ -26,10 +26,11 @@ "eslint-config-google": "^0.11.0", "eslint-plugin-flowtype": "^3.2.1", "eslint-plugin-import": "^2.14.0", - "flow-bin": "^0.90.0", + "flow-bin": "^0.94.0", "flow-copy-source": "^2.0.2", "rimraf": "^2.6.3", - "semantic-release": "^15.13.3" + "semantic-release": "^15.13.3", + "babel-eslint": "^10.0.1" }, "scripts": { "prepare": "yarn run build:clean && yarn run build:lib && yarn run build:flow", @@ -52,8 +53,7 @@ "@babel/polyfill": "^7.2.5", "apollo-link": "^1.2.6", "apollo-utilities": "^1.0.27", - "babel-eslint": "^10.0.1", - "graphql": "^14.0.2", + "graphql": "^14.1.1", "http-proxy-middleware": "^0.19.1", "lodash": "^4.17.11" } diff --git a/src/proxyCacheLink.js b/src/proxyCacheLink.js index 316544f..232a696 100644 --- a/src/proxyCacheLink.js +++ b/src/proxyCacheLink.js @@ -6,9 +6,9 @@ import { } from 'apollo-link' import { hasDirectives } from 'apollo-utilities' import { calculateArguments, didTimeout, DIRECTIVE, removeCacheDirective } from './utils' -import type { Cache } from './utils' +import type { Cache, CacheKeyModifier } from './utils' -export const proxyCacheLink = (queryCache: Cache) => { +export const proxyCacheLink = (queryCache: Cache, cacheKeyModifier: CacheKeyModifier) => { return new class NodeCacheLink extends ApolloLink { request(operation: Object, forward): Observable { const directives = 'directive @cache on QUERY' @@ -23,7 +23,7 @@ export const proxyCacheLink = (queryCache: Cache) => { const server = removeCacheDirective(operation.query) const { query } = operation if (server) operation.query = server - const { id, timeout } = calculateArguments(query, operation.variables) + const { id, timeout } = calculateArguments(query, operation.variables, cacheKeyModifier) const possibleData = queryCache.get(id) diff --git a/src/proxyCacheMiddleware.js b/src/proxyCacheMiddleware.js index 1e07ecd..bcd21d5 100644 --- a/src/proxyCacheMiddleware.js +++ b/src/proxyCacheMiddleware.js @@ -5,78 +5,79 @@ import { parse } from 'graphql' import { print } from 'graphql/language/printer' import { hasDirectives } from 'apollo-utilities' import { calculateArguments, didTimeout, DIRECTIVE, removeCacheDirective } from './utils' -import type { Cache } from './utils' +import type { Cache, CacheKeyModifier } from './utils' const CACHE_HEADER = 'X-Proxy-Cached' export const proxyCacheMiddleware = - (queryCache: Cache) => (app: Object, endpoint: string, proxyConfig: Object) => { - app.use(endpoint, (req, response, next) => { - if (!req.body) { + (queryCache: Cache, cacheKeyModifier: CacheKeyModifier) => + (app: Object, endpoint: string, proxyConfig: Object) => { + app.use(endpoint, (req, response, next) => { + if (!req.body) { console.warn('[skip] proxy-cache-middleware, request.body is not populated. Please add "body-parser" middleware (or similar).') // eslint-disable-line - return next() - } - if (!req.body.query) { - return next() - } - const doc = parse(req.body.query) - const isCache = hasDirectives([ DIRECTIVE ], doc) + return next() + } + if (!req.body.query) { + return next() + } + const doc = parse(req.body.query) + const isCache = hasDirectives([ DIRECTIVE ], doc) - // we remove the @cache directive if it exists - if (isCache) { - const nextQuery = removeCacheDirective(doc) - const { id, timeout } = calculateArguments(doc, req.body.variables) - const possibleData = queryCache.get(id) - if (possibleData) { - const { data, time } = possibleData - if (didTimeout(timeout, time)) { - queryCache.delete(id) - } else { - response.set(CACHE_HEADER, 'true') - return response.json({ data }) + // we remove the @cache directive if it exists + if (isCache) { + const nextQuery = removeCacheDirective(doc) + const { id, timeout } = calculateArguments(doc, req.body.variables, cacheKeyModifier) + const possibleData = queryCache.get(id) + if (possibleData) { + const { data, time } = possibleData + if (didTimeout(timeout, time)) { + queryCache.delete(id) + } else { + response.set(CACHE_HEADER, 'true') + return response.json({ data }) + } } + req._hasCache = { id, timeout } + // could this be piped here (with req.pipe) + req.body = { ...req.body, query: print(nextQuery) } } - req._hasCache = { id, timeout } - // could this be piped here (with req.pipe) - req.body = { ...req.body, query: print(nextQuery) } - } - next() - }) + next() + }) - app.use(endpoint, proxy({ - ...proxyConfig, - onProxyReq: (proxyReq, req, res) => { + app.use(endpoint, proxy({ + ...proxyConfig, + onProxyReq: (proxyReq, req, res) => { // We have to rewrite the request body due to body-parser's removal of the content. - const data = JSON.stringify(req.body) - proxyReq.setHeader('Content-Length', Buffer.byteLength(data)) - proxyReq.write(data) - if (proxyConfig.onProxyReq) { - proxyConfig.onProxyReq(proxyReq, req, res) - } - }, - onProxyRes: (proxyRes, req, res) => { - if (req._hasCache) { - const { id } = req._hasCache - // Save data into cache - let body = new Buffer('') - proxyRes.on('data', function(data) { - body = Buffer.concat([ body, data ]) - }) - proxyRes.on('end', function() { - try { - const response = JSON.parse(body.toString()) - // We don't cache when there are any errors in the response - if (!response.errors && response.data) { - queryCache.set(id, { data: response.data, time: Number(new Date()) }) - } - } catch (e) { + const data = JSON.stringify(req.body) + proxyReq.setHeader('Content-Length', Buffer.byteLength(data)) + proxyReq.write(data) + if (proxyConfig.onProxyReq) { + proxyConfig.onProxyReq(proxyReq, req, res) + } + }, + onProxyRes: (proxyRes, req, res) => { + if (req._hasCache) { + const { id } = req._hasCache + // Save data into cache + let body = new Buffer('') + proxyRes.on('data', function(data) { + body = Buffer.concat([ body, data ]) + }) + proxyRes.on('end', function() { + try { + const response = JSON.parse(body.toString()) + // We don't cache when there are any errors in the response + if (!response.errors && response.data) { + queryCache.set(id, { data: response.data, time: Number(new Date()) }) + } + } catch (e) { console.error(`Exception during cache processing with id ${id}`, e) // eslint-disable-line - } - }) - } - if (proxyConfig.onProxyRes) { - proxyConfig.onProxyRes(proxyRes, req, res) + } + }) + } + if (proxyConfig.onProxyRes) { + proxyConfig.onProxyRes(proxyRes, req, res) + } } - } - })) - } + })) + } diff --git a/src/utils.js b/src/utils.js index 9807a56..ed620de 100644 --- a/src/utils.js +++ b/src/utils.js @@ -50,15 +50,21 @@ export interface Cache { set(key: K, value: V): Cache; } +export type CacheKeyModifier = (?string, ?Object) => ?string + export const didTimeout = (timeout: number, time: number) => timeout > 0 && ((time + (Number(timeout) * 1000)) < Number(new Date())) -export const calculateArguments = (query: DocumentNode, variables: ?Object) => { +export const calculateArguments = (query: DocumentNode, variables: ?Object, cacheKeyModifier: ?CacheKeyModifier) => { const { id, timeout, modifier } = getDirectiveArgumentsAsObject(query, DIRECTIVE) - const thisId = modifier ? modifier.reduce((next, path) => { + let thisId = modifier ? modifier.reduce((next, path) => { return `${next}.${_get(variables, path, '')}` }, id) : id + if (cacheKeyModifier) { + thisId = cacheKeyModifier(thisId, variables) + } + if (!thisId) { throw new Error(`@${DIRECTIVE} directive requires a unique id.`) } diff --git a/yarn.lock b/yarn.lock index 18a14a8..3cbf5c5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2390,10 +2390,10 @@ flat-cache@^1.2.1: rimraf "~2.6.2" write "^0.2.1" -flow-bin@^0.90.0: - version "0.90.0" - resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.90.0.tgz#733b6d29a8c8a22b9a5d273611d9a8402faf3445" - integrity sha512-/syDchjhLLL7nELK1ggyWJifGXuMCTz74kvkjR1t9DcmasMrilLl9qAAotsACcNb98etEEJpsCrvP7WL64kadw== +flow-bin@^0.94.0: + version "0.94.0" + resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.94.0.tgz#b5d58fe7559705b73a18229f97edfc3ab6ffffcb" + integrity sha512-DYF7r9CJ/AksfmmB4+q+TyLMoeQPRnqtF1Pk7KY3zgfkB/nVuA3nXyzqgsIPIvnMSiFEXQcFK4z+iPxSLckZhQ== flow-copy-source@^2.0.2: version "2.0.2" @@ -2682,10 +2682,10 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6 resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00" integrity sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA== -graphql@^14.0.2: - version "14.0.2" - resolved "https://registry.yarnpkg.com/graphql/-/graphql-14.0.2.tgz#7dded337a4c3fd2d075692323384034b357f5650" - integrity sha512-gUC4YYsaiSJT1h40krG3J+USGlwhzNTXSb4IOZljn9ag5Tj+RkoXrWp+Kh7WyE3t1NCfab5kzCuxBIvOMERMXw== +graphql@^14.1.1: + version "14.1.1" + resolved "https://registry.yarnpkg.com/graphql/-/graphql-14.1.1.tgz#d5d77df4b19ef41538d7215d1e7a28834619fac0" + integrity sha512-C5zDzLqvfPAgTtP8AUPIt9keDabrdRAqSWjj2OPRKrKxI9Fb65I36s1uCs1UUBFnSWTdO7hyHi7z1ZbwKMKF6Q== dependencies: iterall "^1.2.2"