Skip to content

Commit 929b8cc

Browse files
authored
feat: customize bypass query parameter (#79)
* chore: use @keyvhq/core instead of keyv * chore: add BYPASS cache state * ci: use github actions * chore: remove keyv dependency * feat: customize bypass query parameter closes #72 * docs: add bypassQueryParameter
1 parent 98fa8df commit 929b8cc

7 files changed

Lines changed: 169 additions & 105 deletions

File tree

README.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,15 +107,24 @@ In case you need you can force invalidate a cache response passing `force=true`
107107
curl https://myserver.dev/user # MISS (first access)
108108
curl https://myserver.dev/user # HIT (served from cache)
109109
curl https://myserver.dev/user # HIT (served from cache)
110-
curl https://myserver.dev/user?force=true # MISS (forcing invalidation)
110+
curl https://myserver.dev/user?force=true # BYPASS (skip cache copy)
111111
```
112112

113+
In that case, the `x-cache-status` will reflect a `'BYPASS'` value.
114+
113115
## API
114116

115117
### cacheableResponse([options])
116118

117119
#### options
118120

121+
##### bypassQueryParameter
122+
123+
Type: `boolean`<br/>
124+
Default: `'force'`
125+
126+
The name of the query parameter to be used for skipping the cache copy in an intentional way.
127+
119128
##### cache
120129

121130
Type: `boolean`<br/>

index.js

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,14 @@ const isEmpty = value =>
1616
(typeof value === 'object' && Object.keys(value).length === 0) ||
1717
(typeof value === 'string' && value.trim().length === 0)
1818

19-
const getKeyDefault = ({ req }) => {
19+
const hasQueryParameter = (req, key) =>
20+
Boolean(req.query ? req.query[key] : parse(req.url.split('?')[1])[key])
21+
22+
const getKeyDefault = ({ req }, bypassQueryParameter) => {
2023
const url = new URL(req.url, 'http://localhost').toString()
2124
const { origin } = new URL(url)
2225
const baseKey = normalizeUrl(url, {
23-
removeQueryParameters: ['force', /^utm_\w+/i]
26+
removeQueryParameters: [bypassQueryParameter, /^utm_\w+/i]
2427
})
2528
return baseKey.replace(origin, '').replace('/?', '')
2629
}
@@ -52,12 +55,13 @@ const createSetHeaders = ({ revalidate }) => {
5255
}
5356

5457
module.exports = ({
58+
bypassQueryParameter = 'force',
5559
cache = new Keyv({ namespace: 'ssr' }),
5660
compress: enableCompression = false,
57-
getKey = getKeyDefault,
5861
get,
59-
send,
62+
getKey = getKeyDefault,
6063
revalidate = ttl => Math.round(ttl * 0.2),
64+
send,
6165
ttl: defaultTtl = 7200000,
6266
...compressOpts
6367
} = {}) => {
@@ -75,10 +79,8 @@ module.exports = ({
7579

7680
return async opts => {
7781
const { req, res } = opts
78-
const hasForce = Boolean(
79-
req.query ? req.query.force : parse(req.url.split('?')[1]).force
80-
)
81-
const key = getKey(opts)
82+
const hasForce = hasQueryParameter(req, bypassQueryParameter)
83+
const key = getKey(opts, bypassQueryParameter)
8284
const cachedResult = await decompress(await cache.get(key))
8385
const isHit = !hasForce && cachedResult !== undefined
8486
const result = isHit ? cachedResult : await get(opts)

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,8 @@
105105
"license": "MIT",
106106
"ava": {
107107
"files": [
108-
"test/**/*.js"
108+
"test/**/*.js",
109+
"!test/util.js"
109110
]
110111
},
111112
"commitlint": {
@@ -115,7 +116,7 @@
115116
},
116117
"lint-staged": {
117118
"package.json": [
118-
"finepack"
119+
"finepack --sort-ignore-object-at ava"
119120
],
120121
"*.js,!*.min.js,": [
121122
"prettier-standard"

test/custom.js

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
'use strict'
2+
3+
const test = require('ava')
4+
const got = require('got')
5+
6+
const { parseCacheControl, createServer } = require('./util')
7+
8+
test('ttl', async t => {
9+
const url = await createServer({
10+
get: ({ req, res }) => ({ data: { foo: 'bar' }, ttl: 86400000 }),
11+
send: ({ data, headers, res, req, ...props }) => {
12+
res.end('Welcome to Micro')
13+
}
14+
})
15+
16+
const { headers } = await got(`${url}/kikobeats`)
17+
const cacheControl = parseCacheControl(headers)
18+
19+
t.true(cacheControl.public)
20+
t.true(cacheControl['must-revalidate'])
21+
t.true([86399, 86400].includes(cacheControl['max-age']))
22+
t.true([17279, 17280].includes(cacheControl['stale-while-revalidate']))
23+
t.true([17279, 17280].includes(cacheControl['stale-if-error']))
24+
})
25+
26+
test('revalidate', async t => {
27+
const url = await createServer({
28+
revalidate: ttl => ttl * 0.1,
29+
get: ({ req, res }) => ({ data: { foo: 'bar' }, ttl: 86400000 }),
30+
send: ({ data, headers, res, req, ...props }) => {
31+
res.end('Welcome to Micro')
32+
}
33+
})
34+
35+
const { headers } = await got(`${url}/kikobeats`)
36+
const cacheControl = parseCacheControl(headers)
37+
38+
t.true(cacheControl.public)
39+
t.true(cacheControl['must-revalidate'])
40+
t.true([86399, 86400].includes(cacheControl['max-age']))
41+
t.true([8639, 8640].includes(cacheControl['stale-while-revalidate']))
42+
t.true([8639, 8640].includes(cacheControl['stale-if-error']))
43+
})
44+
45+
test('fixed revalidate', async t => {
46+
const url = await createServer({
47+
revalidate: 300000,
48+
get: ({ req, res }) => ({ data: { foo: 'bar' }, ttl: 86400000 }),
49+
send: ({ data, headers, res, req, ...props }) => {
50+
res.end('Welcome to Micro')
51+
}
52+
})
53+
54+
const { headers } = await got(`${url}/kikobeats`)
55+
const cacheControl = parseCacheControl(headers)
56+
57+
t.true(cacheControl.public)
58+
t.true(cacheControl['must-revalidate'])
59+
t.true([86399, 86400].includes(cacheControl['max-age']))
60+
t.true([299, 300].includes(cacheControl['stale-while-revalidate']))
61+
t.true([299, 300].includes(cacheControl['stale-if-error']))
62+
})
63+
64+
test('bypass query parameter', async t => {
65+
const url = await createServer({
66+
bypassQueryParameter: 'bypass',
67+
get: ({ req, res }) => {
68+
return {
69+
data: { foo: 'bar' },
70+
ttl: 86400000,
71+
createdAt: Date.now(),
72+
foo: { bar: true }
73+
}
74+
},
75+
send: ({ data, headers, res, req, ...props }) => {
76+
res.end('Welcome to Micro')
77+
}
78+
})
79+
80+
const { headers: headersOne } = await got(`${url}/kikobeats`)
81+
t.is(headersOne['x-cache-status'], 'MISS')
82+
83+
const { headers: headersTwo } = await got(`${url}/kikobeats`)
84+
t.is(headersTwo['x-cache-status'], 'HIT')
85+
86+
const { headers: headersThree } = await got(`${url}/kikobeats?bypass=true`)
87+
t.is(headersThree['x-cache-status'], 'BYPASS')
88+
t.is(headersThree['x-cache-expired-at'], '0ms')
89+
90+
const { headers: headersFour } = await got(`${url}/kikobeats`)
91+
t.is(headersFour['x-cache-status'], 'HIT')
92+
93+
const { headers: headersFive } = await got(`${url}/kikobeats?force=true`)
94+
t.is(headersFive['x-cache-status'], 'MISS')
95+
})

test/index.js

Lines changed: 2 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,10 @@
1-
const { AssertionError } = require('assert')
1+
'use strict'
22

3-
const listen = require('test-listen')
43
const Keyv = require('@keyvhq/core')
5-
const micro = require('micro')
64
const test = require('ava')
75
const got = require('got')
86

9-
const cacheableResponse = require('..')
10-
11-
const createServer = props => {
12-
const server = cacheableResponse(props)
13-
const api = micro((req, res) => server({ req, res }))
14-
return listen(api)
15-
}
16-
17-
const parseCacheControl = headers => {
18-
const header = headers['cache-control']
19-
return header.split(', ').reduce((acc, rawKey) => {
20-
let value = true
21-
let key = rawKey
22-
if (rawKey.includes('=')) {
23-
const [parsedKey, parsedValue] = rawKey.split('=')
24-
key = parsedKey
25-
value = Number(parsedValue)
26-
}
27-
return { ...acc, [key]: value }
28-
}, {})
29-
}
30-
31-
test('.get is required', t => {
32-
const error = t.throws(() => cacheableResponse({}))
33-
t.true(error instanceof AssertionError)
34-
t.is(error.message, '.get required')
35-
})
36-
37-
test('.send is required', t => {
38-
const error = t.throws(() => cacheableResponse({ get: true }))
39-
t.true(error instanceof AssertionError)
40-
t.is(error.message, '.send required')
41-
})
7+
const { parseCacheControl, createServer } = require('./util')
428

439
test('default ttl and revalidate', async t => {
4410
const url = await createServer({
@@ -58,62 +24,6 @@ test('default ttl and revalidate', async t => {
5824
t.true([1439, 1440].includes(cacheControl['stale-if-error']))
5925
})
6026

61-
test('custom ttl', async t => {
62-
const url = await createServer({
63-
get: ({ req, res }) => ({ data: { foo: 'bar' }, ttl: 86400000 }),
64-
send: ({ data, headers, res, req, ...props }) => {
65-
res.end('Welcome to Micro')
66-
}
67-
})
68-
69-
const { headers } = await got(`${url}/kikobeats`)
70-
const cacheControl = parseCacheControl(headers)
71-
72-
t.true(cacheControl.public)
73-
t.true(cacheControl['must-revalidate'])
74-
t.true([86399, 86400].includes(cacheControl['max-age']))
75-
t.true([17279, 17280].includes(cacheControl['stale-while-revalidate']))
76-
t.true([17279, 17280].includes(cacheControl['stale-if-error']))
77-
})
78-
79-
test('custom revalidate', async t => {
80-
const url = await createServer({
81-
revalidate: ttl => ttl * 0.1,
82-
get: ({ req, res }) => ({ data: { foo: 'bar' }, ttl: 86400000 }),
83-
send: ({ data, headers, res, req, ...props }) => {
84-
res.end('Welcome to Micro')
85-
}
86-
})
87-
88-
const { headers } = await got(`${url}/kikobeats`)
89-
const cacheControl = parseCacheControl(headers)
90-
91-
t.true(cacheControl.public)
92-
t.true(cacheControl['must-revalidate'])
93-
t.true([86399, 86400].includes(cacheControl['max-age']))
94-
t.true([8639, 8640].includes(cacheControl['stale-while-revalidate']))
95-
t.true([8639, 8640].includes(cacheControl['stale-if-error']))
96-
})
97-
98-
test('custom fixed revalidate', async t => {
99-
const url = await createServer({
100-
revalidate: 300000,
101-
get: ({ req, res }) => ({ data: { foo: 'bar' }, ttl: 86400000 }),
102-
send: ({ data, headers, res, req, ...props }) => {
103-
res.end('Welcome to Micro')
104-
}
105-
})
106-
107-
const { headers } = await got(`${url}/kikobeats`)
108-
const cacheControl = parseCacheControl(headers)
109-
110-
t.true(cacheControl.public)
111-
t.true(cacheControl['must-revalidate'])
112-
t.true([86399, 86400].includes(cacheControl['max-age']))
113-
t.true([299, 300].includes(cacheControl['stale-while-revalidate']))
114-
t.true([299, 300].includes(cacheControl['stale-if-error']))
115-
})
116-
11727
test('disable revalidation', async t => {
11828
const url = await createServer({
11929
revalidate: false,
@@ -187,15 +97,13 @@ test('force query params to invalidate', async t => {
18797

18898
const { headers: headersOne } = await got(`${url}/kikobeats`)
18999
t.is(headersOne['x-cache-status'], 'MISS')
190-
// t.snapshot(parseCacheControl(headersOne))
191100

192101
const { headers: headersTwo } = await got(`${url}/kikobeats`)
193102
t.is(headersTwo['x-cache-status'], 'HIT')
194103

195104
const { headers: headersThree } = await got(`${url}/kikobeats?force=true`)
196105
t.is(headersThree['x-cache-status'], 'BYPASS')
197106
t.is(headersThree['x-cache-expired-at'], '0ms')
198-
// t.snapshot(parseCacheControl(headersThree))
199107

200108
const { headers: headersFour } = await got(`${url}/kikobeats`)
201109
t.is(headersFour['x-cache-status'], 'HIT')

test/required.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
'use strict'
2+
3+
const test = require('ava')
4+
5+
const cacheableResponse = require('..')
6+
7+
const { AssertionError } = require('assert')
8+
9+
test('.get', t => {
10+
const error = t.throws(() => cacheableResponse({}))
11+
t.true(error instanceof AssertionError)
12+
t.is(error.message, '.get required')
13+
})
14+
15+
test('.send', t => {
16+
const error = t.throws(() => cacheableResponse({ get: true }))
17+
t.true(error instanceof AssertionError)
18+
t.is(error.message, '.send required')
19+
})

test/util.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
'use strict'
2+
3+
const cacheableResponse = require('..')
4+
const listen = require('test-listen')
5+
const micro = require('micro')
6+
7+
const createServer = props => {
8+
const server = cacheableResponse(props)
9+
const api = micro((req, res) => server({ req, res }))
10+
return listen(api)
11+
}
12+
13+
const parseCacheControl = headers => {
14+
const header = headers['cache-control']
15+
return header.split(', ').reduce((acc, rawKey) => {
16+
let value = true
17+
let key = rawKey
18+
if (rawKey.includes('=')) {
19+
const [parsedKey, parsedValue] = rawKey.split('=')
20+
key = parsedKey
21+
value = Number(parsedValue)
22+
}
23+
return { ...acc, [key]: value }
24+
}, {})
25+
}
26+
27+
module.exports = {
28+
parseCacheControl,
29+
createServer
30+
}

0 commit comments

Comments
 (0)