@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 ADDED
@@ -0,0 +1,13 @@
1
+
2
+ root = true
3
+
4
+ [*]
5
+ charset = utf-8
6
+ end_of_line = lf
7
+ insert_final_newline = true
8
+ indent_style = space
9
+ indent_size = 2
10
+ trim_trailing_whitespace = true
11
+
12
+ # [*.md]
13
+ # trim_trailing_whitespace = false
@@ -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
+ ![CI](https://github.com/fastify/fastify-caching/workflows/CI/badge.svg)
4
+ [![NPM version](https://img.shields.io/npm/v/fastify-caching.svg?style=flat)](https://www.npmjs.com/package/fastify-caching)
5
+ [![Known Vulnerabilities](https://snyk.io/test/github/fastify/fastify-caching/badge.svg)](https://snyk.io/test/github/fastify/fastify-caching)
6
+ [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](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));