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 +}