diff --git a/README.md b/README.md
index 0e95616..c75ea17 100644
--- a/README.md
+++ b/README.md
@@ -107,15 +107,24 @@ In case you need you can force invalidate a cache response passing `force=true`
curl https://myserver.dev/user # MISS (first access)
curl https://myserver.dev/user # HIT (served from cache)
curl https://myserver.dev/user # HIT (served from cache)
-curl https://myserver.dev/user?force=true # MISS (forcing invalidation)
+curl https://myserver.dev/user?force=true # BYPASS (skip cache copy)
```
+In that case, the `x-cache-status` will reflect a `'BYPASS'` value.
+
## API
### cacheableResponse([options])
#### options
+##### bypassQueryParameter
+
+Type: `boolean`
+Default: `'force'`
+
+The name of the query parameter to be used for skipping the cache copy in an intentional way.
+
##### cache
Type: `boolean`
diff --git a/index.js b/index.js
index 0080480..db7b44f 100644
--- a/index.js
+++ b/index.js
@@ -16,11 +16,14 @@ const isEmpty = value =>
(typeof value === 'object' && Object.keys(value).length === 0) ||
(typeof value === 'string' && value.trim().length === 0)
-const getKeyDefault = ({ req }) => {
+const hasQueryParameter = (req, key) =>
+ Boolean(req.query ? req.query[key] : parse(req.url.split('?')[1])[key])
+
+const getKeyDefault = ({ req }, bypassQueryParameter) => {
const url = new URL(req.url, 'http://localhost').toString()
const { origin } = new URL(url)
const baseKey = normalizeUrl(url, {
- removeQueryParameters: ['force', /^utm_\w+/i]
+ removeQueryParameters: [bypassQueryParameter, /^utm_\w+/i]
})
return baseKey.replace(origin, '').replace('/?', '')
}
@@ -52,12 +55,13 @@ const createSetHeaders = ({ revalidate }) => {
}
module.exports = ({
+ bypassQueryParameter = 'force',
cache = new Keyv({ namespace: 'ssr' }),
compress: enableCompression = false,
- getKey = getKeyDefault,
get,
- send,
+ getKey = getKeyDefault,
revalidate = ttl => Math.round(ttl * 0.2),
+ send,
ttl: defaultTtl = 7200000,
...compressOpts
} = {}) => {
@@ -75,10 +79,8 @@ module.exports = ({
return async opts => {
const { req, res } = opts
- const hasForce = Boolean(
- req.query ? req.query.force : parse(req.url.split('?')[1]).force
- )
- const key = getKey(opts)
+ const hasForce = hasQueryParameter(req, bypassQueryParameter)
+ const key = getKey(opts, bypassQueryParameter)
const cachedResult = await decompress(await cache.get(key))
const isHit = !hasForce && cachedResult !== undefined
const result = isHit ? cachedResult : await get(opts)
diff --git a/package.json b/package.json
index c7bf5a7..af3c5b8 100644
--- a/package.json
+++ b/package.json
@@ -105,7 +105,8 @@
"license": "MIT",
"ava": {
"files": [
- "test/**/*.js"
+ "test/**/*.js",
+ "!test/util.js"
]
},
"commitlint": {
@@ -115,7 +116,7 @@
},
"lint-staged": {
"package.json": [
- "finepack"
+ "finepack --sort-ignore-object-at ava"
],
"*.js,!*.min.js,": [
"prettier-standard"
diff --git a/test/custom.js b/test/custom.js
new file mode 100644
index 0000000..873412b
--- /dev/null
+++ b/test/custom.js
@@ -0,0 +1,95 @@
+'use strict'
+
+const test = require('ava')
+const got = require('got')
+
+const { parseCacheControl, createServer } = require('./util')
+
+test('ttl', async t => {
+ const url = await createServer({
+ get: ({ req, res }) => ({ data: { foo: 'bar' }, ttl: 86400000 }),
+ send: ({ data, headers, res, req, ...props }) => {
+ res.end('Welcome to Micro')
+ }
+ })
+
+ const { headers } = await got(`${url}/kikobeats`)
+ const cacheControl = parseCacheControl(headers)
+
+ t.true(cacheControl.public)
+ t.true(cacheControl['must-revalidate'])
+ t.true([86399, 86400].includes(cacheControl['max-age']))
+ t.true([17279, 17280].includes(cacheControl['stale-while-revalidate']))
+ t.true([17279, 17280].includes(cacheControl['stale-if-error']))
+})
+
+test('revalidate', async t => {
+ const url = await createServer({
+ revalidate: ttl => ttl * 0.1,
+ get: ({ req, res }) => ({ data: { foo: 'bar' }, ttl: 86400000 }),
+ send: ({ data, headers, res, req, ...props }) => {
+ res.end('Welcome to Micro')
+ }
+ })
+
+ const { headers } = await got(`${url}/kikobeats`)
+ const cacheControl = parseCacheControl(headers)
+
+ t.true(cacheControl.public)
+ t.true(cacheControl['must-revalidate'])
+ t.true([86399, 86400].includes(cacheControl['max-age']))
+ t.true([8639, 8640].includes(cacheControl['stale-while-revalidate']))
+ t.true([8639, 8640].includes(cacheControl['stale-if-error']))
+})
+
+test('fixed revalidate', async t => {
+ const url = await createServer({
+ revalidate: 300000,
+ get: ({ req, res }) => ({ data: { foo: 'bar' }, ttl: 86400000 }),
+ send: ({ data, headers, res, req, ...props }) => {
+ res.end('Welcome to Micro')
+ }
+ })
+
+ const { headers } = await got(`${url}/kikobeats`)
+ const cacheControl = parseCacheControl(headers)
+
+ t.true(cacheControl.public)
+ t.true(cacheControl['must-revalidate'])
+ t.true([86399, 86400].includes(cacheControl['max-age']))
+ t.true([299, 300].includes(cacheControl['stale-while-revalidate']))
+ t.true([299, 300].includes(cacheControl['stale-if-error']))
+})
+
+test('bypass query parameter', async t => {
+ const url = await createServer({
+ bypassQueryParameter: 'bypass',
+ get: ({ req, res }) => {
+ return {
+ data: { foo: 'bar' },
+ ttl: 86400000,
+ createdAt: Date.now(),
+ foo: { bar: true }
+ }
+ },
+ send: ({ data, headers, res, req, ...props }) => {
+ res.end('Welcome to Micro')
+ }
+ })
+
+ const { headers: headersOne } = await got(`${url}/kikobeats`)
+ t.is(headersOne['x-cache-status'], 'MISS')
+
+ const { headers: headersTwo } = await got(`${url}/kikobeats`)
+ t.is(headersTwo['x-cache-status'], 'HIT')
+
+ const { headers: headersThree } = await got(`${url}/kikobeats?bypass=true`)
+ t.is(headersThree['x-cache-status'], 'BYPASS')
+ t.is(headersThree['x-cache-expired-at'], '0ms')
+
+ const { headers: headersFour } = await got(`${url}/kikobeats`)
+ t.is(headersFour['x-cache-status'], 'HIT')
+
+ const { headers: headersFive } = await got(`${url}/kikobeats?force=true`)
+ t.is(headersFive['x-cache-status'], 'MISS')
+})
diff --git a/test/index.js b/test/index.js
index 9a14d91..4197bfb 100644
--- a/test/index.js
+++ b/test/index.js
@@ -1,44 +1,10 @@
-const { AssertionError } = require('assert')
+'use strict'
-const listen = require('test-listen')
const Keyv = require('@keyvhq/core')
-const micro = require('micro')
const test = require('ava')
const got = require('got')
-const cacheableResponse = require('..')
-
-const createServer = props => {
- const server = cacheableResponse(props)
- const api = micro((req, res) => server({ req, res }))
- return listen(api)
-}
-
-const parseCacheControl = headers => {
- const header = headers['cache-control']
- return header.split(', ').reduce((acc, rawKey) => {
- let value = true
- let key = rawKey
- if (rawKey.includes('=')) {
- const [parsedKey, parsedValue] = rawKey.split('=')
- key = parsedKey
- value = Number(parsedValue)
- }
- return { ...acc, [key]: value }
- }, {})
-}
-
-test('.get is required', t => {
- const error = t.throws(() => cacheableResponse({}))
- t.true(error instanceof AssertionError)
- t.is(error.message, '.get required')
-})
-
-test('.send is required', t => {
- const error = t.throws(() => cacheableResponse({ get: true }))
- t.true(error instanceof AssertionError)
- t.is(error.message, '.send required')
-})
+const { parseCacheControl, createServer } = require('./util')
test('default ttl and revalidate', async t => {
const url = await createServer({
@@ -58,62 +24,6 @@ test('default ttl and revalidate', async t => {
t.true([1439, 1440].includes(cacheControl['stale-if-error']))
})
-test('custom ttl', async t => {
- const url = await createServer({
- get: ({ req, res }) => ({ data: { foo: 'bar' }, ttl: 86400000 }),
- send: ({ data, headers, res, req, ...props }) => {
- res.end('Welcome to Micro')
- }
- })
-
- const { headers } = await got(`${url}/kikobeats`)
- const cacheControl = parseCacheControl(headers)
-
- t.true(cacheControl.public)
- t.true(cacheControl['must-revalidate'])
- t.true([86399, 86400].includes(cacheControl['max-age']))
- t.true([17279, 17280].includes(cacheControl['stale-while-revalidate']))
- t.true([17279, 17280].includes(cacheControl['stale-if-error']))
-})
-
-test('custom revalidate', async t => {
- const url = await createServer({
- revalidate: ttl => ttl * 0.1,
- get: ({ req, res }) => ({ data: { foo: 'bar' }, ttl: 86400000 }),
- send: ({ data, headers, res, req, ...props }) => {
- res.end('Welcome to Micro')
- }
- })
-
- const { headers } = await got(`${url}/kikobeats`)
- const cacheControl = parseCacheControl(headers)
-
- t.true(cacheControl.public)
- t.true(cacheControl['must-revalidate'])
- t.true([86399, 86400].includes(cacheControl['max-age']))
- t.true([8639, 8640].includes(cacheControl['stale-while-revalidate']))
- t.true([8639, 8640].includes(cacheControl['stale-if-error']))
-})
-
-test('custom fixed revalidate', async t => {
- const url = await createServer({
- revalidate: 300000,
- get: ({ req, res }) => ({ data: { foo: 'bar' }, ttl: 86400000 }),
- send: ({ data, headers, res, req, ...props }) => {
- res.end('Welcome to Micro')
- }
- })
-
- const { headers } = await got(`${url}/kikobeats`)
- const cacheControl = parseCacheControl(headers)
-
- t.true(cacheControl.public)
- t.true(cacheControl['must-revalidate'])
- t.true([86399, 86400].includes(cacheControl['max-age']))
- t.true([299, 300].includes(cacheControl['stale-while-revalidate']))
- t.true([299, 300].includes(cacheControl['stale-if-error']))
-})
-
test('disable revalidation', async t => {
const url = await createServer({
revalidate: false,
@@ -187,7 +97,6 @@ test('force query params to invalidate', async t => {
const { headers: headersOne } = await got(`${url}/kikobeats`)
t.is(headersOne['x-cache-status'], 'MISS')
- // t.snapshot(parseCacheControl(headersOne))
const { headers: headersTwo } = await got(`${url}/kikobeats`)
t.is(headersTwo['x-cache-status'], 'HIT')
@@ -195,7 +104,6 @@ test('force query params to invalidate', async t => {
const { headers: headersThree } = await got(`${url}/kikobeats?force=true`)
t.is(headersThree['x-cache-status'], 'BYPASS')
t.is(headersThree['x-cache-expired-at'], '0ms')
- // t.snapshot(parseCacheControl(headersThree))
const { headers: headersFour } = await got(`${url}/kikobeats`)
t.is(headersFour['x-cache-status'], 'HIT')
diff --git a/test/required.js b/test/required.js
new file mode 100644
index 0000000..5f57f1d
--- /dev/null
+++ b/test/required.js
@@ -0,0 +1,19 @@
+'use strict'
+
+const test = require('ava')
+
+const cacheableResponse = require('..')
+
+const { AssertionError } = require('assert')
+
+test('.get', t => {
+ const error = t.throws(() => cacheableResponse({}))
+ t.true(error instanceof AssertionError)
+ t.is(error.message, '.get required')
+})
+
+test('.send', t => {
+ const error = t.throws(() => cacheableResponse({ get: true }))
+ t.true(error instanceof AssertionError)
+ t.is(error.message, '.send required')
+})
diff --git a/test/util.js b/test/util.js
new file mode 100644
index 0000000..f32e97f
--- /dev/null
+++ b/test/util.js
@@ -0,0 +1,30 @@
+'use strict'
+
+const cacheableResponse = require('..')
+const listen = require('test-listen')
+const micro = require('micro')
+
+const createServer = props => {
+ const server = cacheableResponse(props)
+ const api = micro((req, res) => server({ req, res }))
+ return listen(api)
+}
+
+const parseCacheControl = headers => {
+ const header = headers['cache-control']
+ return header.split(', ').reduce((acc, rawKey) => {
+ let value = true
+ let key = rawKey
+ if (rawKey.includes('=')) {
+ const [parsedKey, parsedValue] = rawKey.split('=')
+ key = parsedKey
+ value = Number(parsedValue)
+ }
+ return { ...acc, [key]: value }
+ }, {})
+}
+
+module.exports = {
+ parseCacheControl,
+ createServer
+}