Skip to content

Commit

Permalink
feat(cacheModifier): Added cache modifier function, bumped dependencies
Browse files Browse the repository at this point in the history
  • Loading branch information
BowlingX committed Mar 4, 2019
1 parent a8ab447 commit 4e24acc
Show file tree
Hide file tree
Showing 6 changed files with 91 additions and 80 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,10 @@ interface Cache<K, V> {

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
Expand Down
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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"
}
Expand Down
6 changes: 3 additions & 3 deletions src/proxyCacheLink.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, Object>) => {
export const proxyCacheLink = (queryCache: Cache<string, Object>, cacheKeyModifier: CacheKeyModifier) => {
return new class NodeCacheLink extends ApolloLink {
request(operation: Object, forward): Observable<any> {
const directives = 'directive @cache on QUERY'
Expand All @@ -23,7 +23,7 @@ export const proxyCacheLink = (queryCache: Cache<String, Object>) => {
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)

Expand Down
127 changes: 64 additions & 63 deletions src/proxyCacheMiddleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, Object>) => (app: Object, endpoint: string, proxyConfig: Object) => {
app.use(endpoint, (req, response, next) => {
if (!req.body) {
(queryCache: Cache<string, Object>, 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)
}
}
}
}))
}
}))
}
10 changes: 8 additions & 2 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,15 +50,21 @@ export interface Cache<K, V> {
set(key: K, value: V): Cache<K, V>;
}

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.`)
}
Expand Down
16 changes: 8 additions & 8 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"

Expand Down

0 comments on commit 4e24acc

Please sign in to comment.