@fastify/caching 7.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.editorconfig +13 -0
- package/.github/dependabot.yml +13 -0
- package/.github/stale.yml +21 -0
- package/.github/workflows/ci.yml +50 -0
- package/LICENSE +21 -0
- package/Readme.md +153 -0
- package/package.json +55 -0
- package/plugin.d.ts +124 -0
- package/plugin.js +104 -0
- package/test/cache.test.js +256 -0
- package/test/headers.test.js +278 -0
- package/test/types/plugin.test-d.ts +52 -0
package/.editorconfig
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
version: 2
|
|
2
|
+
updates:
|
|
3
|
+
- package-ecosystem: "github-actions"
|
|
4
|
+
directory: "/"
|
|
5
|
+
schedule:
|
|
6
|
+
interval: "monthly"
|
|
7
|
+
open-pull-requests-limit: 10
|
|
8
|
+
|
|
9
|
+
- package-ecosystem: "npm"
|
|
10
|
+
directory: "/"
|
|
11
|
+
schedule:
|
|
12
|
+
interval: "weekly"
|
|
13
|
+
open-pull-requests-limit: 10
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Number of days of inactivity before an issue becomes stale
|
|
2
|
+
daysUntilStale: 15
|
|
3
|
+
# Number of days of inactivity before a stale issue is closed
|
|
4
|
+
daysUntilClose: 7
|
|
5
|
+
# Issues with these labels will never be considered stale
|
|
6
|
+
exemptLabels:
|
|
7
|
+
- "discussion"
|
|
8
|
+
- "feature request"
|
|
9
|
+
- "bug"
|
|
10
|
+
- "help wanted"
|
|
11
|
+
- "plugin suggestion"
|
|
12
|
+
- "good first issue"
|
|
13
|
+
# Label to use when marking an issue as stale
|
|
14
|
+
staleLabel: stale
|
|
15
|
+
# Comment to post when marking an issue as stale. Set to `false` to disable
|
|
16
|
+
markComment: >
|
|
17
|
+
This issue has been automatically marked as stale because it has not had
|
|
18
|
+
recent activity. It will be closed if no further activity occurs. Thank you
|
|
19
|
+
for your contributions.
|
|
20
|
+
# Comment to post when closing a stale issue. Set to `false` to disable
|
|
21
|
+
closeComment: false
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
'on':
|
|
3
|
+
push:
|
|
4
|
+
paths-ignore:
|
|
5
|
+
- docs/**
|
|
6
|
+
- '*.md'
|
|
7
|
+
pull_request:
|
|
8
|
+
paths-ignore:
|
|
9
|
+
- docs/**
|
|
10
|
+
- '*.md'
|
|
11
|
+
jobs:
|
|
12
|
+
test:
|
|
13
|
+
runs-on: ${{ matrix.os }}
|
|
14
|
+
strategy:
|
|
15
|
+
matrix:
|
|
16
|
+
node-version:
|
|
17
|
+
- 10
|
|
18
|
+
- 12
|
|
19
|
+
- 14
|
|
20
|
+
- 16
|
|
21
|
+
- 17
|
|
22
|
+
os:
|
|
23
|
+
- macos-latest
|
|
24
|
+
- ubuntu-latest
|
|
25
|
+
- windows-latest
|
|
26
|
+
steps:
|
|
27
|
+
- uses: actions/checkout@v3
|
|
28
|
+
- name: Use Node.js
|
|
29
|
+
uses: actions/setup-node@v3
|
|
30
|
+
with:
|
|
31
|
+
node-version: ${{ matrix.node-version }}
|
|
32
|
+
- name: Install Dependencies
|
|
33
|
+
run: |
|
|
34
|
+
npm install --ignore-scripts
|
|
35
|
+
- name: Lint
|
|
36
|
+
run: |
|
|
37
|
+
npm run lint:ci
|
|
38
|
+
- name: Run Tests
|
|
39
|
+
run: |
|
|
40
|
+
npm test
|
|
41
|
+
automerge:
|
|
42
|
+
needs: test
|
|
43
|
+
runs-on: ubuntu-latest
|
|
44
|
+
permissions:
|
|
45
|
+
pull-requests: write
|
|
46
|
+
contents: write
|
|
47
|
+
steps:
|
|
48
|
+
- uses: fastify/github-action-merge-dependabot@v3
|
|
49
|
+
with:
|
|
50
|
+
github-token: ${{ secrets.GITHUB_TOKEN }}
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2018 James Sumners <james.sumners@gmail.com>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/Readme.md
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# fastify-caching
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
[](https://www.npmjs.com/package/fastify-caching)
|
|
5
|
+
[](https://snyk.io/test/github/fastify/fastify-caching)
|
|
6
|
+
[](https://standardjs.com/)
|
|
7
|
+
|
|
8
|
+
*fastify-caching* is a plugin for the [Fastify](http://fastify.io/) framework
|
|
9
|
+
that provides mechanisms for manipulating HTTP cache headers according to
|
|
10
|
+
[RFC 2616 §14.9](https://tools.ietf.org/html/rfc2616#section-14.9).
|
|
11
|
+
|
|
12
|
+
Supports Fastify versions ^3.0.0. Version v5.x supports Fastify ^3.0.0 in
|
|
13
|
+
the [`v5.x` branch](https://github.com/fastify/fastify-caching/tree/v5.x).
|
|
14
|
+
|
|
15
|
+
This plugin fully supports Fastify's encapsulation. Therefore, routes that
|
|
16
|
+
should have differing cache settings should be registered within different
|
|
17
|
+
contexts.
|
|
18
|
+
|
|
19
|
+
In addition to providing header manipulation, the plugin also decorates the
|
|
20
|
+
server instance with an object that can be used for caching items. **Note:**
|
|
21
|
+
the default cache should not be used in a "production" environment. It is
|
|
22
|
+
an LRU, in-memory cache that is capped at 100,000 items. It is *highly*
|
|
23
|
+
recommended that a full featured cache object be supplied, e.g.
|
|
24
|
+
[abstract-cache-redis][acache-redis].
|
|
25
|
+
|
|
26
|
+
[acache-redis]: https://www.npmjs.com/package/abstract-cache-redis
|
|
27
|
+
|
|
28
|
+
## Example
|
|
29
|
+
|
|
30
|
+
This example shows using the plugin to disable client side caching of all
|
|
31
|
+
routes.
|
|
32
|
+
|
|
33
|
+
```js
|
|
34
|
+
const http = require('http')
|
|
35
|
+
const fastify = require('fastify')()
|
|
36
|
+
const fastifyCaching = require('fastify-caching')
|
|
37
|
+
|
|
38
|
+
fastify.register(
|
|
39
|
+
fastifyCaching,
|
|
40
|
+
{privacy: fastifyCaching.privacy.NOCACHE},
|
|
41
|
+
(err) => { if (err) throw err }
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
fastify.get('/', (req, reply) => {
|
|
45
|
+
reply.send({hello: 'world'})
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
fastify.listen(3000, (err) => {
|
|
49
|
+
if (err) throw err
|
|
50
|
+
|
|
51
|
+
http.get('http://127.0.0.1:3000/', (res) => {
|
|
52
|
+
console.log(res.headers['cache-control'])
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
This example shows how to register the plugin such that it only provides
|
|
58
|
+
a server-local cache. It will not set any cache control headers on responses.
|
|
59
|
+
It also shows how to retain a reference to the cache object so that it can
|
|
60
|
+
be re-used.
|
|
61
|
+
|
|
62
|
+
```js
|
|
63
|
+
const IORedis = require('ioredis')
|
|
64
|
+
const redis = new IORedis({host: '127.0.0.1'})
|
|
65
|
+
const abcache = require('abstract-cache')({
|
|
66
|
+
useAwait: false,
|
|
67
|
+
driver: {
|
|
68
|
+
name: 'abstract-cache-redis', // must be installed via `npm install`
|
|
69
|
+
options: {client: redis}
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
const fastify = require('fastify')()
|
|
74
|
+
fastify
|
|
75
|
+
.register(require('fastify-redis'), {client: redis})
|
|
76
|
+
.register(require('fastify-caching'), {cache: abcache})
|
|
77
|
+
|
|
78
|
+
fastify.get('/', (req, reply) => {
|
|
79
|
+
fastify.cache.set('hello', {hello: 'world'}, 10000, (err) => {
|
|
80
|
+
if (err) return reply.send(err)
|
|
81
|
+
reply.send({hello: 'world'})
|
|
82
|
+
})
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
fastify.listen(3000, (err) => {
|
|
86
|
+
if (err) throw err
|
|
87
|
+
})
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## API
|
|
91
|
+
|
|
92
|
+
### Options
|
|
93
|
+
|
|
94
|
+
*fastify-caching* accepts the options object:
|
|
95
|
+
|
|
96
|
+
```js
|
|
97
|
+
{
|
|
98
|
+
privacy: 'value',
|
|
99
|
+
expiresIn: 300,
|
|
100
|
+
cache: {get, set},
|
|
101
|
+
cacheSegment: 'segment-name'
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
+ `privacy` (Default: `undefined`): can be set to any string that is valid
|
|
106
|
+
for a *cache-response-directive* as defined by RFC 2616.
|
|
107
|
+
+ `expiresIn` (Default: `undefined`): a value, in seconds, for the *max-age* the
|
|
108
|
+
resource may be cached. When this is set, and `privacy` is not set to `no-cache`,
|
|
109
|
+
then `', max-age=<value>'` will be appended to the `cache-control` header.
|
|
110
|
+
+ `cache` (Default: `abstract-cache.memclient`): an [abstract-cache][acache]
|
|
111
|
+
protocol compliant cache object. Note: the plugin requires a cache instance to
|
|
112
|
+
properly support the ETag mechanism. Therefore, if a falsy value is supplied
|
|
113
|
+
the default will be used.
|
|
114
|
+
+ `cacheSegment` (Default: `'fastify-caching'`): segment identifier to use when
|
|
115
|
+
communicating with the cache.
|
|
116
|
+
+ `serverExpiresIn` (Default: `undefined`): a value, in seconds, for the length of time the resource is fresh and may be held in a shared cache (e.g. a CDN). Shared caches will ignore max-age when this is specified, though browsers will continue to use max-age. Should be used with expiresIn, not in place of it. When this is set, and `privacy` is set to `public`, then `', s-maxage=<value>'` will be appended to the `cache-control` header.
|
|
117
|
+
|
|
118
|
+
[acache]: https://www.npmjs.com/package/abstract-cache
|
|
119
|
+
|
|
120
|
+
### `reply.etag(string, number)`
|
|
121
|
+
|
|
122
|
+
This method allows setting of the `etag` header. It accepts any arbitrary
|
|
123
|
+
string. Be sure to supply a string that is valid for HTTP headers.
|
|
124
|
+
|
|
125
|
+
If a tag string is not supplied then [uid-safe][uid-safe] will be used to
|
|
126
|
+
generate a tag. This operation will be performed ***synchronously***. It is
|
|
127
|
+
recommended to always supply a value to this method to avoid this operation.
|
|
128
|
+
|
|
129
|
+
All incoming requests to paths affected by this plugin will be inspected for
|
|
130
|
+
the `if-none-match` header. If the header is present, the value will be used
|
|
131
|
+
to lookup the tag within the cache associated with this plugin. If the tag is
|
|
132
|
+
found, then the response will be ended with a 304 status code sent to
|
|
133
|
+
the client.
|
|
134
|
+
|
|
135
|
+
Tags will be cached according to the second parameter. If the second parameter
|
|
136
|
+
is supplied, and it is an integer, then it will be used as the time to cache
|
|
137
|
+
the etag for generating 304 responses. The time must be specified in
|
|
138
|
+
milliseconds. The default lifetime, when the parameter is not specified, is
|
|
139
|
+
`3600000`.
|
|
140
|
+
|
|
141
|
+
[uid-safe]: https://www.npmjs.com/package/uid-safe
|
|
142
|
+
|
|
143
|
+
### `reply.expires(date)`
|
|
144
|
+
|
|
145
|
+
This method allows setting of the `expires` header. It accepts a regular `Date`
|
|
146
|
+
object, or a string that is a valid date string according to
|
|
147
|
+
[RFC 2616 §14.21][sec14.21].
|
|
148
|
+
|
|
149
|
+
[sec14.21]: https://tools.ietf.org/html/rfc2616#section-14.21
|
|
150
|
+
|
|
151
|
+
## License
|
|
152
|
+
|
|
153
|
+
[MIT License](https://jsumners.mit-license.org/)
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fastify/caching",
|
|
3
|
+
"version": "7.0.0",
|
|
4
|
+
"description": "A plugin for Fastify to enable management of cache control headers",
|
|
5
|
+
"main": "plugin.js",
|
|
6
|
+
"types": "plugin.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"lint": "standard --verbose | snazzy",
|
|
9
|
+
"lint:ci": "standard",
|
|
10
|
+
"test": "npm run unit && npm run test:types",
|
|
11
|
+
"test:types": "tsd",
|
|
12
|
+
"unit": "tap -J \"test/*.test.js\"",
|
|
13
|
+
"unit:report": "npm run unit -- --coverage-report=html",
|
|
14
|
+
"unit:verbose": "npm run unit -- -Rspec"
|
|
15
|
+
},
|
|
16
|
+
"precommit": [
|
|
17
|
+
"lint",
|
|
18
|
+
"test"
|
|
19
|
+
],
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+ssh://git@github.com/fastify/fastify-caching.git"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"fastify",
|
|
26
|
+
"cache",
|
|
27
|
+
"caching"
|
|
28
|
+
],
|
|
29
|
+
"author": "James Sumners <james.sumners@gmail.com>",
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"bugs": {
|
|
32
|
+
"url": "https://github.com/fastify/fastify-caching/issues"
|
|
33
|
+
},
|
|
34
|
+
"homepage": "https://github.com/fastify/fastify-caching#readme",
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/node": "^17.0.21",
|
|
37
|
+
"fastify": "^3.27.2",
|
|
38
|
+
"pre-commit": "^1.2.2",
|
|
39
|
+
"snazzy": "^9.0.0",
|
|
40
|
+
"standard": "^17.0.0",
|
|
41
|
+
"tap": "^16.0.0",
|
|
42
|
+
"tsd": "^0.20.0"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"abstract-cache": "^1.0.1",
|
|
46
|
+
"fastify-plugin": "^3.0.1",
|
|
47
|
+
"uid-safe": "^2.1.5"
|
|
48
|
+
},
|
|
49
|
+
"tsd": {
|
|
50
|
+
"directory": "test/types"
|
|
51
|
+
},
|
|
52
|
+
"publishConfig": {
|
|
53
|
+
"access": "public"
|
|
54
|
+
}
|
|
55
|
+
}
|
package/plugin.d.ts
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/// <reference types='node' />
|
|
2
|
+
|
|
3
|
+
import { FastifyPluginCallback } from 'fastify';
|
|
4
|
+
|
|
5
|
+
declare module 'fastify' {
|
|
6
|
+
interface FastifyInstance {
|
|
7
|
+
cache: AbstractCacheCompliantObject;
|
|
8
|
+
cacheSegment: string;
|
|
9
|
+
etagMaxLife: number | undefined;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface FastifyReply {
|
|
13
|
+
/**
|
|
14
|
+
* This method allows setting of the `expires` header.
|
|
15
|
+
*
|
|
16
|
+
* @link [reply.expires() documentation](https://github.com/fastify/fastify-caching#replyexpiresdate)
|
|
17
|
+
*
|
|
18
|
+
* @param date A regular `Date` object, or a valid date string according to [RFC 2616 section 14.21](https://datatracker.ietf.org/doc/html/rfc2616#section-14.21).
|
|
19
|
+
*/
|
|
20
|
+
expires(date?: Date): this;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* This method allows setting of the `etag` header.
|
|
24
|
+
*
|
|
25
|
+
* @link [reply.etag() documentation](https://github.com/fastify/fastify-caching#replyetagstring-number)
|
|
26
|
+
*
|
|
27
|
+
* @param tag Any arbitrary string that is valid for HTTP headers.
|
|
28
|
+
* @param timeToLive The time must be specified in milliseconds. The default lifetime, when the parameter is not specified, is `3600000`.
|
|
29
|
+
*/
|
|
30
|
+
etag(tag?: string, timeToLive?: number): this;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @link [`abstract-cache` protocol documentation](https://github.com/jsumners/abstract-cache#protocol)
|
|
36
|
+
*/
|
|
37
|
+
export interface AbstractCacheCompliantObject {
|
|
38
|
+
get(
|
|
39
|
+
key: string | { id: string; segment: string },
|
|
40
|
+
callback?: (error: unknown, result: unknown) => void
|
|
41
|
+
): void;
|
|
42
|
+
set(
|
|
43
|
+
key: string | { id: string; segment: string },
|
|
44
|
+
value: unknown,
|
|
45
|
+
timeToLive: number,
|
|
46
|
+
callback?: (error: unknown, result: unknown) => void
|
|
47
|
+
): void;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface Privacy {
|
|
51
|
+
NOCACHE: 'no-cache';
|
|
52
|
+
PRIVATE: 'private';
|
|
53
|
+
PUBLIC: 'public';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @link [`fastify-caching` options documentation](https://github.com/fastify/fastify-caching#options)
|
|
58
|
+
*/
|
|
59
|
+
export interface FastifyCachingPluginOptions {
|
|
60
|
+
/**
|
|
61
|
+
* An [abstract-cache](https://www.npmjs.com/package/abstract-cache) protocol compliant cache object.
|
|
62
|
+
* Note: the plugin requires a cache instance to properly support the ETag mechanism.
|
|
63
|
+
* Therefore, if a falsy value is supplied the default will be used.
|
|
64
|
+
*
|
|
65
|
+
* - Default value: `abstract-cache.memclient`
|
|
66
|
+
*/
|
|
67
|
+
cache?: AbstractCacheCompliantObject;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The segment identifier to use when communicating with the cache.
|
|
71
|
+
*
|
|
72
|
+
* - Default value: `fastify-caching`
|
|
73
|
+
*/
|
|
74
|
+
cacheSegment?: string;
|
|
75
|
+
|
|
76
|
+
etagMaxLife?: number;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* A value, in seconds, for the max-age the resource may be cached.
|
|
80
|
+
* When this is set, and privacy is not set to no-cache, then ', max-age=<value>'
|
|
81
|
+
* will be appended to the cache-control header.
|
|
82
|
+
*
|
|
83
|
+
* - Default value: `undefined`
|
|
84
|
+
*/
|
|
85
|
+
expiresIn?: number;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* It can be set to any string that is valid for a cache-response-directive as
|
|
89
|
+
* defined by [RFC 2616](https://datatracker.ietf.org/doc/html/rfc2616#section-14.9).
|
|
90
|
+
*
|
|
91
|
+
* - Default value: `undefined`
|
|
92
|
+
*
|
|
93
|
+
* @link [MDN Cache-Control - Response Directives](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#response_directives)
|
|
94
|
+
*
|
|
95
|
+
* @example
|
|
96
|
+
* const fastifyCaching = require('fastify-caching');
|
|
97
|
+
*
|
|
98
|
+
* // Disabling client side caching of all routes.
|
|
99
|
+
* fastify.register(fastifyCaching, { privacy: fastifyCaching.privacy.NOCACHE });
|
|
100
|
+
*/
|
|
101
|
+
privacy?: string;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* A value, in seconds, for the length of time the resource is fresh and may be
|
|
105
|
+
* held in a shared cache (e.g. a CDN). Shared caches will ignore max-age when
|
|
106
|
+
* this is specified, though browsers will continue to use max-age. Should be
|
|
107
|
+
* used with expiresIn, not in place of it. When this is set, and privacy is set
|
|
108
|
+
* to public, then ', s-maxage=<value>' will be appended to the cache-control header.
|
|
109
|
+
*
|
|
110
|
+
* - Default value: `undefined`
|
|
111
|
+
*/
|
|
112
|
+
serverExpiresIn?: number;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
declare const fastifyCaching: FastifyPluginCallback<FastifyCachingPluginOptions> & {
|
|
116
|
+
privacy: Privacy;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
declare const privacy: {
|
|
120
|
+
privacy: Privacy;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export default fastifyCaching;
|
|
124
|
+
export { fastifyCaching, privacy };
|
package/plugin.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/* eslint no-prototype-builtins: 0 */
|
|
4
|
+
|
|
5
|
+
const fp = require('fastify-plugin')
|
|
6
|
+
const uidSafe = require('uid-safe')
|
|
7
|
+
const abstractCache = require('abstract-cache')
|
|
8
|
+
|
|
9
|
+
const defaultOptions = {
|
|
10
|
+
expiresIn: undefined,
|
|
11
|
+
serverExpiresIn: undefined,
|
|
12
|
+
privacy: undefined,
|
|
13
|
+
cache: undefined,
|
|
14
|
+
cacheSegment: 'fastify-caching'
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function cachingExpires (date) {
|
|
18
|
+
if (!date) return this
|
|
19
|
+
this.header('Expires', (Date.prototype.isPrototypeOf(date)) ? date.toUTCString() : date)
|
|
20
|
+
return this
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function etag (value, lifetime) {
|
|
24
|
+
if (value) {
|
|
25
|
+
this.header('ETag', value)
|
|
26
|
+
} else {
|
|
27
|
+
this.header('ETag', uidSafe.sync(18))
|
|
28
|
+
}
|
|
29
|
+
this._etagLife = Number.isInteger(lifetime) ? lifetime : 3600000
|
|
30
|
+
return this
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function etagHandleRequest (req, res, next) {
|
|
34
|
+
if (!req.headers['if-none-match']) return next()
|
|
35
|
+
const etag = req.headers['if-none-match']
|
|
36
|
+
this.cache.get({ id: etag, segment: this.cacheSegment }, (err, cached) => {
|
|
37
|
+
if (err) return next(err)
|
|
38
|
+
if (cached && cached.item) {
|
|
39
|
+
return res.status(304).send()
|
|
40
|
+
}
|
|
41
|
+
next()
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function etagOnSend (req, res, payload, next) {
|
|
46
|
+
const etag = res.getHeader('etag')
|
|
47
|
+
if (!etag || !res._etagLife) return next()
|
|
48
|
+
this.cache.set(
|
|
49
|
+
{ id: etag, segment: this.cacheSegment },
|
|
50
|
+
true,
|
|
51
|
+
res._etagLife,
|
|
52
|
+
(err) => next(err, payload)
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function fastifyCachingPlugin (instance, options, next) {
|
|
57
|
+
let _options
|
|
58
|
+
if (Function.prototype.isPrototypeOf(options)) {
|
|
59
|
+
_options = Object.assign({}, defaultOptions)
|
|
60
|
+
} else {
|
|
61
|
+
_options = Object.assign({}, defaultOptions, options)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (!_options.cache) _options.cache = abstractCache()
|
|
65
|
+
|
|
66
|
+
if (_options.privacy) {
|
|
67
|
+
// https://tools.ietf.org/html/rfc2616#section-14.9.4
|
|
68
|
+
let value = _options.privacy
|
|
69
|
+
if (_options.privacy.toLowerCase() !== 'no-cache' && _options.expiresIn) {
|
|
70
|
+
value = `${_options.privacy}, max-age=${_options.expiresIn}`
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (_options.privacy !== undefined && _options.privacy.toLowerCase() === 'public' && _options.serverExpiresIn) {
|
|
74
|
+
value += `, s-maxage=${_options.serverExpiresIn}`
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
instance.addHook('onRequest', (req, res, next) => {
|
|
78
|
+
res.header('Cache-control', value)
|
|
79
|
+
next()
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
instance.decorate('cache', _options.cache)
|
|
84
|
+
instance.decorate('cacheSegment', _options.cacheSegment)
|
|
85
|
+
instance.decorate('etagMaxLife', _options.etagMaxLife)
|
|
86
|
+
instance.decorateReply('etag', etag)
|
|
87
|
+
instance.decorateReply('expires', cachingExpires)
|
|
88
|
+
instance.addHook('onRequest', etagHandleRequest)
|
|
89
|
+
instance.addHook('onSend', etagOnSend)
|
|
90
|
+
|
|
91
|
+
instance[Symbol.for('fastify-caching.registered')] = true
|
|
92
|
+
next()
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
module.exports = fp(fastifyCachingPlugin, {
|
|
96
|
+
fastify: '^3.0.0',
|
|
97
|
+
name: 'fastify-caching'
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
module.exports.privacy = {
|
|
101
|
+
NOCACHE: 'no-cache',
|
|
102
|
+
PUBLIC: 'public',
|
|
103
|
+
PRIVATE: 'private'
|
|
104
|
+
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { test } = require('tap')
|
|
4
|
+
const plugin = require('../plugin')
|
|
5
|
+
|
|
6
|
+
const Fastify = require('fastify')
|
|
7
|
+
|
|
8
|
+
test('cache property gets added to instance', async (t) => {
|
|
9
|
+
t.plan(2)
|
|
10
|
+
|
|
11
|
+
const fastify = Fastify()
|
|
12
|
+
await fastify.register(plugin)
|
|
13
|
+
await fastify.ready()
|
|
14
|
+
|
|
15
|
+
t.ok(fastify.cache)
|
|
16
|
+
t.ok(fastify.cache.set)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
test('cache is usable', async (t) => {
|
|
20
|
+
t.plan(4)
|
|
21
|
+
|
|
22
|
+
const fastify = Fastify()
|
|
23
|
+
await fastify.register(async (instance, options) => {
|
|
24
|
+
instance.addHook('onRequest', async function (req, reply) {
|
|
25
|
+
t.notOk(instance[Symbol.for('fastify-caching.registered')])
|
|
26
|
+
})
|
|
27
|
+
})
|
|
28
|
+
await fastify.register(plugin)
|
|
29
|
+
|
|
30
|
+
fastify.addHook('onRequest', async function (req, reply) {
|
|
31
|
+
t.equal(this[Symbol.for('fastify-caching.registered')], true)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
fastify.get('/one', (req, reply) => {
|
|
35
|
+
fastify.cache.set('one', { one: true }, 1000, (err) => {
|
|
36
|
+
if (err) return reply.send(err)
|
|
37
|
+
reply.redirect('/two')
|
|
38
|
+
})
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
fastify.get('/two', (req, reply) => {
|
|
42
|
+
fastify.cache.get('one', (err, obj) => {
|
|
43
|
+
t.error(err)
|
|
44
|
+
t.same(obj.item, { one: true })
|
|
45
|
+
|
|
46
|
+
reply.send()
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
await fastify.ready()
|
|
51
|
+
|
|
52
|
+
const response = await fastify.inject({
|
|
53
|
+
method: 'GET',
|
|
54
|
+
path: '/one'
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
if (
|
|
58
|
+
response.statusCode > 300 &&
|
|
59
|
+
response.statusCode < 400 &&
|
|
60
|
+
response.headers.location
|
|
61
|
+
) {
|
|
62
|
+
await fastify.inject({
|
|
63
|
+
method: 'GET',
|
|
64
|
+
path: response.headers.location
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
test('cache is usable with function as plugin default options input', async (t) => {
|
|
70
|
+
t.plan(4)
|
|
71
|
+
|
|
72
|
+
const fastify = Fastify()
|
|
73
|
+
await fastify.register(async (instance, options) => {
|
|
74
|
+
instance.addHook('onRequest', async function (req, reply) {
|
|
75
|
+
t.notOk(instance[Symbol.for('fastify-caching.registered')])
|
|
76
|
+
})
|
|
77
|
+
})
|
|
78
|
+
await fastify.register(plugin, () => () => { })
|
|
79
|
+
|
|
80
|
+
fastify.addHook('onRequest', async function (req, reply) {
|
|
81
|
+
t.equal(this[Symbol.for('fastify-caching.registered')], true)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
fastify.get('/one', (req, reply) => {
|
|
85
|
+
fastify.cache.set('one', { one: true }, 1000, (err) => {
|
|
86
|
+
if (err) return reply.send(err)
|
|
87
|
+
reply.redirect('/two')
|
|
88
|
+
})
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
fastify.get('/two', (req, reply) => {
|
|
92
|
+
fastify.cache.get('one', (err, obj) => {
|
|
93
|
+
t.error(err)
|
|
94
|
+
t.same(obj.item, { one: true })
|
|
95
|
+
|
|
96
|
+
reply.send()
|
|
97
|
+
})
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
await fastify.ready()
|
|
101
|
+
|
|
102
|
+
const response = await fastify.inject({
|
|
103
|
+
method: 'GET',
|
|
104
|
+
path: '/one'
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
if (
|
|
108
|
+
response.statusCode > 300 &&
|
|
109
|
+
response.statusCode < 400 &&
|
|
110
|
+
response.headers.location
|
|
111
|
+
) {
|
|
112
|
+
await fastify.inject({
|
|
113
|
+
method: 'GET',
|
|
114
|
+
path: response.headers.location
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
test('getting cache item with error returns error', async (t) => {
|
|
120
|
+
t.plan(1)
|
|
121
|
+
|
|
122
|
+
const mockCache = {
|
|
123
|
+
get: (info, callback) => callback(new Error('cache.get always errors')),
|
|
124
|
+
set: (key, value, ttl, callback) => callback()
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const fastify = Fastify()
|
|
128
|
+
await fastify.register(plugin, { cache: mockCache })
|
|
129
|
+
|
|
130
|
+
fastify.get('/one', (req, reply) => {
|
|
131
|
+
fastify.cache.set('one', { one: true }, 1000, (err) => {
|
|
132
|
+
if (err) return reply.send(err)
|
|
133
|
+
return reply
|
|
134
|
+
.etag('123456')
|
|
135
|
+
.send({ hello: 'world' })
|
|
136
|
+
})
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
fastify.get('/two', (req, reply) => {
|
|
140
|
+
fastify.cache.get('one', (err, obj) => {
|
|
141
|
+
t.notOk(err)
|
|
142
|
+
t.notOk(obj)
|
|
143
|
+
})
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
await fastify.ready()
|
|
147
|
+
|
|
148
|
+
await fastify.inject({
|
|
149
|
+
method: 'GET',
|
|
150
|
+
path: '/one'
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
const response = await fastify.inject({
|
|
154
|
+
method: 'GET',
|
|
155
|
+
path: '/two',
|
|
156
|
+
headers: {
|
|
157
|
+
'if-none-match': '123456'
|
|
158
|
+
}
|
|
159
|
+
})
|
|
160
|
+
t.equal(response.statusCode, 500)
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
test('etags get stored in cache', async (t) => {
|
|
164
|
+
t.plan(1)
|
|
165
|
+
|
|
166
|
+
const fastify = Fastify()
|
|
167
|
+
await fastify.register(plugin)
|
|
168
|
+
|
|
169
|
+
fastify.get('/one', (req, reply) => {
|
|
170
|
+
reply
|
|
171
|
+
.etag('123456')
|
|
172
|
+
.send({ hello: 'world' })
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
await fastify.ready()
|
|
176
|
+
|
|
177
|
+
await fastify.inject({
|
|
178
|
+
method: 'GET',
|
|
179
|
+
path: '/one'
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
const response = await fastify.inject({
|
|
183
|
+
method: 'GET',
|
|
184
|
+
path: '/one',
|
|
185
|
+
headers: {
|
|
186
|
+
'if-none-match': '123456'
|
|
187
|
+
}
|
|
188
|
+
})
|
|
189
|
+
t.equal(response.statusCode, 304)
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
test('etag cache life is customizable', (t) => {
|
|
193
|
+
t.plan(4)
|
|
194
|
+
|
|
195
|
+
const fastify = Fastify()
|
|
196
|
+
fastify.register(plugin)
|
|
197
|
+
|
|
198
|
+
fastify.get('/one', function (req, reply) {
|
|
199
|
+
reply
|
|
200
|
+
// We set a cache lifetime of 50 milliseconds
|
|
201
|
+
.etag('123456', 50)
|
|
202
|
+
.send({ hello: 'world' })
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
fastify.ready((err) => {
|
|
206
|
+
t.error(err)
|
|
207
|
+
|
|
208
|
+
fastify.inject({
|
|
209
|
+
method: 'GET',
|
|
210
|
+
path: '/one'
|
|
211
|
+
}, (err, _response) => {
|
|
212
|
+
t.error(err)
|
|
213
|
+
|
|
214
|
+
// We wait 70 milliseconds that the cache expires
|
|
215
|
+
setTimeout(() => {
|
|
216
|
+
fastify.inject({
|
|
217
|
+
method: 'GET',
|
|
218
|
+
path: '/one',
|
|
219
|
+
headers: {
|
|
220
|
+
'if-none-match': '123456'
|
|
221
|
+
}
|
|
222
|
+
}, (err, response) => {
|
|
223
|
+
t.error(err)
|
|
224
|
+
t.equal(response.statusCode, 200)
|
|
225
|
+
})
|
|
226
|
+
}, 70)
|
|
227
|
+
})
|
|
228
|
+
})
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
test('returns response payload', async (t) => {
|
|
232
|
+
t.plan(1)
|
|
233
|
+
|
|
234
|
+
const fastify = Fastify()
|
|
235
|
+
await fastify.register(plugin)
|
|
236
|
+
|
|
237
|
+
fastify.get('/one', (req, reply) => {
|
|
238
|
+
reply
|
|
239
|
+
.etag('123456', 300)
|
|
240
|
+
.send({ hello: 'world' })
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
await fastify.ready()
|
|
244
|
+
|
|
245
|
+
await fastify.inject({
|
|
246
|
+
method: 'GET',
|
|
247
|
+
path: '/one'
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
const response = await fastify.inject({
|
|
251
|
+
method: 'GET',
|
|
252
|
+
path: '/one'
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
t.same(JSON.parse(response.payload), { hello: 'world' })
|
|
256
|
+
})
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { test } = require('tap')
|
|
4
|
+
const Fastify = require('fastify')
|
|
5
|
+
const plugin = require('../plugin')
|
|
6
|
+
|
|
7
|
+
test('decorators get added', async (t) => {
|
|
8
|
+
t.plan(1)
|
|
9
|
+
|
|
10
|
+
const fastify = Fastify()
|
|
11
|
+
await fastify.register(plugin)
|
|
12
|
+
|
|
13
|
+
fastify.get('/', (req, reply) => {
|
|
14
|
+
t.ok(reply.etag)
|
|
15
|
+
reply.send()
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
await fastify.ready()
|
|
19
|
+
|
|
20
|
+
await fastify.inject({
|
|
21
|
+
method: 'GET',
|
|
22
|
+
path: '/'
|
|
23
|
+
})
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test('decorators add headers', async (t) => {
|
|
27
|
+
t.plan(2)
|
|
28
|
+
|
|
29
|
+
const tag = '123456'
|
|
30
|
+
|
|
31
|
+
const fastify = Fastify()
|
|
32
|
+
await fastify.register(plugin)
|
|
33
|
+
|
|
34
|
+
fastify.get('/', (req, reply) => {
|
|
35
|
+
reply
|
|
36
|
+
.etag(tag)
|
|
37
|
+
.send()
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
await fastify.ready()
|
|
41
|
+
|
|
42
|
+
const response = await fastify.inject({
|
|
43
|
+
method: 'GET',
|
|
44
|
+
path: '/'
|
|
45
|
+
})
|
|
46
|
+
t.ok(response.headers.etag)
|
|
47
|
+
t.equal(response.headers.etag, tag)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
test('sets etag header for falsy argument', async (t) => {
|
|
51
|
+
t.plan(1)
|
|
52
|
+
|
|
53
|
+
const fastify = Fastify()
|
|
54
|
+
await fastify.register(plugin)
|
|
55
|
+
|
|
56
|
+
fastify.get('/', (req, reply) => {
|
|
57
|
+
reply
|
|
58
|
+
.etag()
|
|
59
|
+
.send()
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
await fastify.ready()
|
|
63
|
+
|
|
64
|
+
const response = await fastify.inject({
|
|
65
|
+
method: 'GET',
|
|
66
|
+
path: '/'
|
|
67
|
+
})
|
|
68
|
+
t.ok(response.headers.etag)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
test('sets no-cache header', async (t) => {
|
|
72
|
+
t.plan(2)
|
|
73
|
+
|
|
74
|
+
const fastify = Fastify()
|
|
75
|
+
await fastify.register(plugin, { privacy: plugin.privacy.NOCACHE })
|
|
76
|
+
|
|
77
|
+
fastify.get('/', (req, reply) => {
|
|
78
|
+
reply.send({ hello: 'world' })
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
await fastify.ready()
|
|
82
|
+
|
|
83
|
+
const response = await fastify.inject({
|
|
84
|
+
method: 'GET',
|
|
85
|
+
path: '/'
|
|
86
|
+
})
|
|
87
|
+
t.ok(response.headers['cache-control'])
|
|
88
|
+
t.equal(response.headers['cache-control'], 'no-cache')
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
test('sets private with max-age header', async (t) => {
|
|
92
|
+
t.plan(2)
|
|
93
|
+
|
|
94
|
+
const opts = {
|
|
95
|
+
privacy: plugin.privacy.PRIVATE,
|
|
96
|
+
expiresIn: 300
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const fastify = Fastify()
|
|
100
|
+
await fastify.register(plugin, opts)
|
|
101
|
+
|
|
102
|
+
fastify.get('/', (req, reply) => {
|
|
103
|
+
reply.send({ hello: 'world' })
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
await fastify.ready()
|
|
107
|
+
|
|
108
|
+
const response = await fastify.inject({
|
|
109
|
+
method: 'GET',
|
|
110
|
+
path: '/'
|
|
111
|
+
})
|
|
112
|
+
t.ok(response.headers['cache-control'])
|
|
113
|
+
t.equal(response.headers['cache-control'], 'private, max-age=300')
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
test('sets public with max-age and s-maxage header', async (t) => {
|
|
117
|
+
t.plan(2)
|
|
118
|
+
|
|
119
|
+
const opts = {
|
|
120
|
+
privacy: plugin.privacy.PUBLIC,
|
|
121
|
+
expiresIn: 300,
|
|
122
|
+
serverExpiresIn: 12345
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const fastify = Fastify()
|
|
126
|
+
await fastify.register(plugin, opts)
|
|
127
|
+
|
|
128
|
+
fastify.get('/', (req, reply) => {
|
|
129
|
+
reply.send({ hello: 'world' })
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
await fastify.ready()
|
|
133
|
+
|
|
134
|
+
const response = await fastify.inject({
|
|
135
|
+
method: 'GET',
|
|
136
|
+
path: '/'
|
|
137
|
+
})
|
|
138
|
+
t.ok(response.headers['cache-control'])
|
|
139
|
+
t.equal(response.headers['cache-control'], 'public, max-age=300, s-maxage=12345')
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
test('only sets max-age and ignores s-maxage with private header', async (t) => {
|
|
143
|
+
t.plan(2)
|
|
144
|
+
|
|
145
|
+
const opts = {
|
|
146
|
+
privacy: plugin.privacy.PRIVATE,
|
|
147
|
+
expiresIn: 300,
|
|
148
|
+
serverExpiresIn: 12345
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const fastify = Fastify()
|
|
152
|
+
await fastify.register(plugin, opts)
|
|
153
|
+
|
|
154
|
+
fastify.get('/', (req, reply) => {
|
|
155
|
+
reply.send({ hello: 'world' })
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
await fastify.ready()
|
|
159
|
+
|
|
160
|
+
const response = await fastify.inject({
|
|
161
|
+
method: 'GET',
|
|
162
|
+
path: '/'
|
|
163
|
+
})
|
|
164
|
+
t.ok(response.headers['cache-control'])
|
|
165
|
+
t.equal(response.headers['cache-control'], 'private, max-age=300')
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
test('s-maxage is optional with public header', async (t) => {
|
|
169
|
+
t.plan(2)
|
|
170
|
+
|
|
171
|
+
const opts = {
|
|
172
|
+
privacy: plugin.privacy.PUBLIC,
|
|
173
|
+
expiresIn: 300
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const fastify = Fastify()
|
|
177
|
+
await fastify.register(plugin, opts)
|
|
178
|
+
|
|
179
|
+
fastify.get('/', (req, reply) => {
|
|
180
|
+
reply.send({ hello: 'world' })
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
await fastify.ready()
|
|
184
|
+
|
|
185
|
+
const response = await fastify.inject({
|
|
186
|
+
method: 'GET',
|
|
187
|
+
path: '/'
|
|
188
|
+
})
|
|
189
|
+
t.ok(response.headers['cache-control'])
|
|
190
|
+
t.equal(response.headers['cache-control'], 'public, max-age=300')
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
test('sets no-store with max-age header', async (t) => {
|
|
194
|
+
t.plan(2)
|
|
195
|
+
|
|
196
|
+
const fastify = Fastify()
|
|
197
|
+
await fastify.register(plugin, { privacy: 'no-store', expiresIn: 300 })
|
|
198
|
+
|
|
199
|
+
fastify.get('/', (req, reply) => {
|
|
200
|
+
reply.send({ hello: 'world' })
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
await fastify.ready()
|
|
204
|
+
|
|
205
|
+
const response = await fastify.inject({
|
|
206
|
+
method: 'GET',
|
|
207
|
+
path: '/'
|
|
208
|
+
})
|
|
209
|
+
t.ok(response.headers['cache-control'])
|
|
210
|
+
t.equal(response.headers['cache-control'], 'no-store, max-age=300')
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
test('sets the expires header', async (t) => {
|
|
214
|
+
t.plan(2)
|
|
215
|
+
|
|
216
|
+
const now = new Date()
|
|
217
|
+
|
|
218
|
+
const fastify = Fastify()
|
|
219
|
+
await fastify.register(plugin, { privacy: plugin.privacy.NOCACHE })
|
|
220
|
+
|
|
221
|
+
fastify.get('/', (req, reply) => {
|
|
222
|
+
reply
|
|
223
|
+
.expires(now)
|
|
224
|
+
.send({ hello: 'world' })
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
await fastify.ready()
|
|
228
|
+
|
|
229
|
+
const response = await fastify.inject({
|
|
230
|
+
method: 'GET',
|
|
231
|
+
path: '/'
|
|
232
|
+
})
|
|
233
|
+
t.ok(response.headers.expires)
|
|
234
|
+
t.equal(response.headers.expires, now.toUTCString())
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
test('sets the expires header to a falsy value', async (t) => {
|
|
238
|
+
t.plan(1)
|
|
239
|
+
|
|
240
|
+
const fastify = Fastify()
|
|
241
|
+
await fastify.register(plugin, { privacy: plugin.privacy.NOCACHE })
|
|
242
|
+
|
|
243
|
+
fastify.get('/', (req, reply) => {
|
|
244
|
+
reply
|
|
245
|
+
.expires()
|
|
246
|
+
.send({ hello: 'world' })
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
await fastify.ready()
|
|
250
|
+
|
|
251
|
+
const response = await fastify.inject({
|
|
252
|
+
method: 'GET',
|
|
253
|
+
path: '/'
|
|
254
|
+
})
|
|
255
|
+
t.notOk(response.headers.expires)
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
test('sets the expires header to a custom value', async (t) => {
|
|
259
|
+
t.plan(2)
|
|
260
|
+
|
|
261
|
+
const fastify = Fastify()
|
|
262
|
+
await fastify.register(plugin, { privacy: plugin.privacy.NOCACHE })
|
|
263
|
+
|
|
264
|
+
fastify.get('/', (req, reply) => {
|
|
265
|
+
reply
|
|
266
|
+
.expires('foobar')
|
|
267
|
+
.send({ hello: 'world' })
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
await fastify.ready()
|
|
271
|
+
|
|
272
|
+
const response = await fastify.inject({
|
|
273
|
+
method: 'GET',
|
|
274
|
+
path: '/'
|
|
275
|
+
})
|
|
276
|
+
t.ok(response.headers.expires)
|
|
277
|
+
t.equal(response.headers.expires, 'foobar')
|
|
278
|
+
})
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import Fastify, { FastifyReply } from 'fastify';
|
|
2
|
+
import { expectAssignable, expectError, expectType } from 'tsd';
|
|
3
|
+
import fastifyCaching, {
|
|
4
|
+
AbstractCacheCompliantObject,
|
|
5
|
+
FastifyCachingPluginOptions,
|
|
6
|
+
} from '../..';
|
|
7
|
+
|
|
8
|
+
const fastify = Fastify({ logger: true });
|
|
9
|
+
|
|
10
|
+
const cachingOptions: FastifyCachingPluginOptions = {
|
|
11
|
+
privacy: fastifyCaching.privacy.PUBLIC,
|
|
12
|
+
expiresIn: 300,
|
|
13
|
+
cacheSegment: 'fastify-caching',
|
|
14
|
+
};
|
|
15
|
+
expectAssignable<FastifyCachingPluginOptions>(cachingOptions);
|
|
16
|
+
|
|
17
|
+
fastify.register(fastifyCaching, cachingOptions);
|
|
18
|
+
|
|
19
|
+
expectType<AbstractCacheCompliantObject>(fastify.cache);
|
|
20
|
+
expectType<AbstractCacheCompliantObject['get']>(fastify.cache.get);
|
|
21
|
+
expectType<AbstractCacheCompliantObject['set']>(fastify.cache.set);
|
|
22
|
+
expectType<string>(fastify.cacheSegment);
|
|
23
|
+
// expectType<number>(fastify.etagMaxLife);
|
|
24
|
+
|
|
25
|
+
fastify.get('/one', async (request, reply) => {
|
|
26
|
+
expectType<(tag?: string, timeToLive?: number) => FastifyReply>(reply.etag);
|
|
27
|
+
expectType<(date?: Date) => FastifyReply>(reply.expires);
|
|
28
|
+
|
|
29
|
+
expectType<FastifyReply>(reply.etag('hello', 6000));
|
|
30
|
+
expectType<FastifyReply>(reply.expires(new Date(Date.now() + 6000)));
|
|
31
|
+
|
|
32
|
+
return { message: 'one' };
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
fastify.get('/two', async (request, reply) => {
|
|
36
|
+
expectType<FastifyReply>(
|
|
37
|
+
reply.etag('hello', 6000).expires(new Date(Date.now() + 6000))
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
return { message: 'two' };
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// We register a new instance that should trigger a typescript error.
|
|
44
|
+
const shouldErrorApp = Fastify({ logger: true });
|
|
45
|
+
|
|
46
|
+
const badCachingOptions = {
|
|
47
|
+
privacy: fastifyCaching.privacy.PRIVATE,
|
|
48
|
+
expiresIn: 'a string instead of a number of second',
|
|
49
|
+
cacheSegment: 'fastify-caching',
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
expectError(shouldErrorApp.register(fastifyCaching, badCachingOptions));
|