fastify 4.8.1 → 4.9.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/docs/Guides/Ecosystem.md +11 -5
- package/docs/Reference/Reply.md +52 -23
- package/docs/Reference/Routes.md +2 -2
- package/docs/Reference/Server.md +1 -1
- package/docs/Reference/Validation-and-Serialization.md +39 -1
- package/fastify.js +1 -1
- package/lib/error-handler.js +2 -0
- package/lib/error-serializer.js +1 -1
- package/lib/errors.js +9 -1
- package/lib/hooks.js +1 -1
- package/lib/reply.js +31 -12
- package/lib/route.js +16 -1
- package/lib/schema-controller.js +9 -5
- package/lib/schemas.js +51 -3
- package/lib/validation.js +22 -6
- package/package.json +2 -2
- package/test/content-type.test.js +43 -0
- package/test/hooks.test.js +38 -0
- package/test/internals/hooks.test.js +3 -3
- package/test/internals/reply-serialize.test.js +120 -4
- package/test/schema-serialization.test.js +230 -0
- package/test/schema-special-usage.test.js +42 -0
- package/types/reply.d.ts +4 -4
- package/types/schema.d.ts +1 -0
package/docs/Guides/Ecosystem.md
CHANGED
|
@@ -155,6 +155,10 @@ section.
|
|
|
155
155
|
- [`@ethicdevs/fastify-git-server`](https://github.com/EthicDevs/fastify-git-server)
|
|
156
156
|
A plugin to easily create git server and make one/many Git repositories available
|
|
157
157
|
for clone/fetch/push through the standard `git` (over http) commands.
|
|
158
|
+
- [`@fastify-userland/request-id`](https://github.com/fastify-userland/request-id)
|
|
159
|
+
Fastify Request ID Plugin
|
|
160
|
+
- [`@fastify-userland/typeorm-query-runner`](https://github.com/fastify-userland/typeorm-query-runner)
|
|
161
|
+
Fastify typeorm QueryRunner plugin
|
|
158
162
|
- [`@gquittet/graceful-server`](https://github.com/gquittet/graceful-server)
|
|
159
163
|
Tiny (~5k), Fast, KISS, and dependency-free Node.JS library to make your
|
|
160
164
|
Fastify API graceful.
|
|
@@ -235,7 +239,7 @@ section.
|
|
|
235
239
|
send HTTP requests via [axios](https://github.com/axios/axios).
|
|
236
240
|
- [`fastify-babel`](https://github.com/cfware/fastify-babel) Fastify plugin for
|
|
237
241
|
development servers that require Babel transformations of JavaScript sources.
|
|
238
|
-
- [`fastify-bcrypt`](https://github.com/
|
|
242
|
+
- [`fastify-bcrypt`](https://github.com/beliven-it/fastify-bcrypt) A Bcrypt hash
|
|
239
243
|
generator & checker.
|
|
240
244
|
- [`fastify-blipp`](https://github.com/PavelPolyakov/fastify-blipp) Prints your
|
|
241
245
|
routes to the console, so you definitely know which endpoints are available.
|
|
@@ -263,8 +267,8 @@ section.
|
|
|
263
267
|
Sequelize ORM.
|
|
264
268
|
- [`fastify-couchdb`](https://github.com/nigelhanlon/fastify-couchdb) Fastify
|
|
265
269
|
plugin to add CouchDB support via [nano](https://github.com/apache/nano).
|
|
266
|
-
- [`fastify-crud-generator`](https://github.com/
|
|
267
|
-
plugin to rapidly generate CRUD routes for any entity.
|
|
270
|
+
- [`fastify-crud-generator`](https://github.com/beliven-it/fastify-crud-generator)
|
|
271
|
+
A plugin to rapidly generate CRUD routes for any entity.
|
|
268
272
|
- [`fastify-custom-healthcheck`](https://github.com/gkampitakis/fastify-custom-healthcheck)
|
|
269
273
|
Fastify plugin to add health route in your server that asserts custom
|
|
270
274
|
functions.
|
|
@@ -453,7 +457,7 @@ section.
|
|
|
453
457
|
Fastify plugin for memoize responses by expressive settings.
|
|
454
458
|
- [`fastify-piscina`](https://github.com/piscinajs/fastify-piscina) A worker
|
|
455
459
|
thread pool plugin using [Piscina](https://github.com/piscinajs/piscina).
|
|
456
|
-
- [`fastify-polyglot`](https://github.com/
|
|
460
|
+
- [`fastify-polyglot`](https://github.com/beliven-it/fastify-polyglot) A plugin to
|
|
457
461
|
handle i18n using
|
|
458
462
|
[node-polyglot](https://www.npmjs.com/package/node-polyglot).
|
|
459
463
|
- [`fastify-postgraphile`](https://github.com/alemagio/fastify-postgraphile)
|
|
@@ -547,7 +551,7 @@ section.
|
|
|
547
551
|
[Tokenize](https://github.com/Bowser65/Tokenize) plugin for Fastify that
|
|
548
552
|
removes the pain of managing authentication tokens, with built-in integration
|
|
549
553
|
for `fastify-auth`.
|
|
550
|
-
- [`fastify-totp`](https://github.com/
|
|
554
|
+
- [`fastify-totp`](https://github.com/beliven-it/fastify-totp) A plugin to handle
|
|
551
555
|
TOTP (e.g. for 2FA).
|
|
552
556
|
- [`fastify-twitch-ebs-tools`](https://github.com/lukemnet/fastify-twitch-ebs-tools)
|
|
553
557
|
Useful functions for Twitch Extension Backend Services (EBS).
|
|
@@ -606,6 +610,8 @@ section.
|
|
|
606
610
|
and updated Typeorm plugin for use with Fastify.
|
|
607
611
|
|
|
608
612
|
#### [Community Tools](#community-tools)
|
|
613
|
+
- [`@fastify-userland/workflows`](https://github.com/fastify-userland/workflows)
|
|
614
|
+
Reusable workflows for use in the Fastify plugin
|
|
609
615
|
- [`fast-maker`](https://github.com/imjuni/fast-maker) route configuration
|
|
610
616
|
generator by directory structure.
|
|
611
617
|
- [`simple-tjscli`](https://github.com/imjuni/simple-tjscli) CLI tool to
|
package/docs/Reference/Reply.md
CHANGED
|
@@ -20,9 +20,9 @@
|
|
|
20
20
|
- [.callNotFound()](#callnotfound)
|
|
21
21
|
- [.getResponseTime()](#getresponsetime)
|
|
22
22
|
- [.type(contentType)](#typecontenttype)
|
|
23
|
-
- [.getSerializationFunction(schema | httpStatus)](#getserializationfunctionschema--httpstatus)
|
|
24
|
-
- [.compileSerializationSchema(schema, httpStatus)](#compileserializationschemaschema-httpstatus)
|
|
25
|
-
- [.serializeInput(data, [schema | httpStatus], [httpStatus])](#serializeinputdata-schema--httpstatus-httpstatus)
|
|
23
|
+
- [.getSerializationFunction(schema | httpStatus, [contentType])](#getserializationfunctionschema--httpstatus)
|
|
24
|
+
- [.compileSerializationSchema(schema, [httpStatus], [contentType])](#compileserializationschemaschema-httpstatus)
|
|
25
|
+
- [.serializeInput(data, [schema | httpStatus], [httpStatus], [contentType])](#serializeinputdata-schema--httpstatus-httpstatus)
|
|
26
26
|
- [.serializer(func)](#serializerfunc)
|
|
27
27
|
- [.raw](#raw)
|
|
28
28
|
- [.sent](#sent)
|
|
@@ -63,16 +63,17 @@ object that exposes the following functions and properties:
|
|
|
63
63
|
- `.serialize(payload)` - Serializes the specified payload using the default
|
|
64
64
|
JSON serializer or using the custom serializer (if one is set) and returns the
|
|
65
65
|
serialized payload.
|
|
66
|
-
- `.getSerializationFunction(schema | httpStatus)` - Returns the serialization
|
|
66
|
+
- `.getSerializationFunction(schema | httpStatus, [contentType])` - Returns the serialization
|
|
67
67
|
function for the specified schema or http status, if any of either are set.
|
|
68
|
-
- `.compileSerializationSchema(schema, httpStatus)` - Compiles
|
|
69
|
-
schema and returns a serialization function using the default
|
|
70
|
-
`SerializerCompiler`. The optional `httpStatus` is forwarded
|
|
71
|
-
`SerializerCompiler` if provided, default to `undefined`.
|
|
72
|
-
- `.serializeInput(data, schema, [,httpStatus])` - Serializes
|
|
73
|
-
using the specified schema and returns the serialized payload.
|
|
74
|
-
If the optional `httpStatus`
|
|
75
|
-
function given for that
|
|
68
|
+
- `.compileSerializationSchema(schema, [httpStatus], [contentType])` - Compiles
|
|
69
|
+
the specified schema and returns a serialization function using the default
|
|
70
|
+
(or customized) `SerializerCompiler`. The optional `httpStatus` is forwarded
|
|
71
|
+
to the `SerializerCompiler` if provided, default to `undefined`.
|
|
72
|
+
- `.serializeInput(data, schema, [,httpStatus], [contentType])` - Serializes
|
|
73
|
+
the specified data using the specified schema and returns the serialized payload.
|
|
74
|
+
If the optional `httpStatus`, and `contentType` are provided, the function
|
|
75
|
+
will use the serializer function given for that specific content type and
|
|
76
|
+
HTTP Status Code. Default to `undefined`.
|
|
76
77
|
- `.serializer(function)` - Sets a custom serializer for the payload.
|
|
77
78
|
- `.send(payload)` - Sends the payload to the user, could be a plain text, a
|
|
78
79
|
buffer, JSON, stream, or an Error object.
|
|
@@ -339,12 +340,12 @@ reply.type('text/html')
|
|
|
339
340
|
If the `Content-Type` has a JSON subtype, and the charset parameter is not set,
|
|
340
341
|
`utf-8` will be used as the charset by default.
|
|
341
342
|
|
|
342
|
-
### .getSerializationFunction(schema | httpStatus)
|
|
343
|
+
### .getSerializationFunction(schema | httpStatus, [contentType])
|
|
343
344
|
<a id="getserializationfunction"></a>
|
|
344
345
|
|
|
345
346
|
By calling this function using a provided `schema` or `httpStatus`,
|
|
346
|
-
it will return a `serialzation` function
|
|
347
|
-
serialize diverse inputs. It returns `undefined` if no
|
|
347
|
+
and the optional `contentType`, it will return a `serialzation` function
|
|
348
|
+
that can be used to serialize diverse inputs. It returns `undefined` if no
|
|
348
349
|
serialization function was found using either of the provided inputs.
|
|
349
350
|
|
|
350
351
|
This heavily depends of the `schema#responses` attached to the route, or
|
|
@@ -367,12 +368,18 @@ serialize({ foo: 'bar' }) // '{"foo":"bar"}'
|
|
|
367
368
|
const serialize = reply
|
|
368
369
|
.getSerializationFunction(200)
|
|
369
370
|
serialize({ foo: 'bar' }) // '{"foo":"bar"}'
|
|
371
|
+
|
|
372
|
+
// or
|
|
373
|
+
|
|
374
|
+
const serialize = reply
|
|
375
|
+
.getSerializationFunction(200, 'application/json')
|
|
376
|
+
serialize({ foo: 'bar' }) // '{"foo":"bar"}'
|
|
370
377
|
```
|
|
371
378
|
|
|
372
|
-
See [.compileSerializationSchema(schema, [httpStatus])](#compileserializationschema)
|
|
379
|
+
See [.compileSerializationSchema(schema, [httpStatus], [contentType])](#compileserializationschema)
|
|
373
380
|
for more information on how to compile serialization schemas.
|
|
374
381
|
|
|
375
|
-
### .compileSerializationSchema(schema, httpStatus)
|
|
382
|
+
### .compileSerializationSchema(schema, [httpStatus], [contentType])
|
|
376
383
|
<a id="compileserializationschema"></a>
|
|
377
384
|
|
|
378
385
|
This function will compile a serialization schema and
|
|
@@ -381,9 +388,9 @@ The function returned (a.k.a. _serialization function_) returned is compiled
|
|
|
381
388
|
by using the provided `SerializerCompiler`. Also this is cached by using
|
|
382
389
|
a `WeakMap` for reducing compilation calls.
|
|
383
390
|
|
|
384
|
-
The optional
|
|
385
|
-
the `SerializerCompiler`, so it can be used
|
|
386
|
-
function if a custom `SerializerCompiler` is used.
|
|
391
|
+
The optional paramaters `httpStatus` and `contentType`, if provided,
|
|
392
|
+
are forwarded directly to the `SerializerCompiler`, so it can be used
|
|
393
|
+
to compile the serialization function if a custom `SerializerCompiler` is used.
|
|
387
394
|
|
|
388
395
|
This heavily depends of the `schema#responses` attached to the route, or
|
|
389
396
|
the serialization functions compiled by using `compileSerializationSchema`.
|
|
@@ -412,6 +419,23 @@ const serialize = reply
|
|
|
412
419
|
}
|
|
413
420
|
}, 200)
|
|
414
421
|
serialize({ foo: 'bar' }) // '{"foo":"bar"}'
|
|
422
|
+
|
|
423
|
+
// or
|
|
424
|
+
|
|
425
|
+
const serialize = reply
|
|
426
|
+
.compileSerializationSchema({
|
|
427
|
+
'3xx': {
|
|
428
|
+
content: {
|
|
429
|
+
'application/json': {
|
|
430
|
+
schema: {
|
|
431
|
+
name: { type: 'string' },
|
|
432
|
+
phone: { type: 'number' }
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}, '3xx', 'application/json')
|
|
438
|
+
serialize({ name: 'Jone', phone: 201090909090 }) // '{"name":"Jone", "phone":201090909090}'
|
|
415
439
|
```
|
|
416
440
|
|
|
417
441
|
Note that you should be careful when using this function, as it will cache
|
|
@@ -461,14 +485,14 @@ const newSerialize = reply.compileSerializationSchema(newSchema)
|
|
|
461
485
|
console.log(newSerialize === serialize) // false
|
|
462
486
|
```
|
|
463
487
|
|
|
464
|
-
### .serializeInput(data, [schema | httpStatus], [httpStatus])
|
|
488
|
+
### .serializeInput(data, [schema | httpStatus], [httpStatus], [contentType])
|
|
465
489
|
<a id="serializeinput"></a>
|
|
466
490
|
|
|
467
491
|
This function will serialize the input data based on the provided schema,
|
|
468
492
|
or http status code. If both provided, the `httpStatus` will take presedence.
|
|
469
493
|
|
|
470
494
|
If there is not a serialization function for a given `schema`, a new serialization
|
|
471
|
-
function will be compiled forwarding the `httpStatus` if provided.
|
|
495
|
+
function will be compiled forwarding the `httpStatus`, and the `contentType` if provided.
|
|
472
496
|
|
|
473
497
|
```js
|
|
474
498
|
reply
|
|
@@ -497,9 +521,14 @@ reply
|
|
|
497
521
|
|
|
498
522
|
reply
|
|
499
523
|
.serializeInput({ foo: 'bar'}, 200) // '{"foo":"bar"}'
|
|
524
|
+
|
|
525
|
+
// or
|
|
526
|
+
|
|
527
|
+
reply
|
|
528
|
+
.serializeInput({ name: 'Jone', age: 18 }, '200', 'application/vnd.v1+json') // '{"name": "Jone", "age": 18}'
|
|
500
529
|
```
|
|
501
530
|
|
|
502
|
-
See [.compileSerializationSchema(schema, [httpStatus])](#compileserializationschema)
|
|
531
|
+
See [.compileSerializationSchema(schema, [httpStatus], [contentType])](#compileserializationschema)
|
|
503
532
|
for more information on how to compile serialization schemas.
|
|
504
533
|
|
|
505
534
|
### .serializer(func)
|
package/docs/Reference/Routes.md
CHANGED
|
@@ -94,8 +94,8 @@ fastify.route(options)
|
|
|
94
94
|
schemas for request validations. See the [Validation and
|
|
95
95
|
Serialization](./Validation-and-Serialization.md#schema-validator)
|
|
96
96
|
documentation.
|
|
97
|
-
* `serializerCompiler({ { schema, method, url, httpStatus } })`:
|
|
98
|
-
builds schemas for response serialization. See the [Validation and
|
|
97
|
+
* `serializerCompiler({ { schema, method, url, httpStatus, contentType } })`:
|
|
98
|
+
function that builds schemas for response serialization. See the [Validation and
|
|
99
99
|
Serialization](./Validation-and-Serialization.md#schema-serializer)
|
|
100
100
|
documentation.
|
|
101
101
|
* `schemaErrorFormatter(errors, dataVar)`: function that formats the errors from
|
package/docs/Reference/Server.md
CHANGED
|
@@ -1370,7 +1370,7 @@ const fastify = Fastify({
|
|
|
1370
1370
|
buildSerializer: function factory (externalSchemas, serializerOptsServerOption) {
|
|
1371
1371
|
// This factory function must return a schema serializer compiler.
|
|
1372
1372
|
// See [#schema-serializer](./Validation-and-Serialization.md#schema-serializer) for details.
|
|
1373
|
-
return function serializerCompiler ({ schema, method, url, httpStatus }) {
|
|
1373
|
+
return function serializerCompiler ({ schema, method, url, httpStatus, contentType }) {
|
|
1374
1374
|
return data => JSON.stringify(data)
|
|
1375
1375
|
}
|
|
1376
1376
|
}
|
|
@@ -611,6 +611,44 @@ const schema = {
|
|
|
611
611
|
|
|
612
612
|
fastify.post('/the/url', { schema }, handler)
|
|
613
613
|
```
|
|
614
|
+
You can even have a specific response schema for different content types.
|
|
615
|
+
For example:
|
|
616
|
+
```js
|
|
617
|
+
const schema = {
|
|
618
|
+
response: {
|
|
619
|
+
200: {
|
|
620
|
+
description: 'Response schema that support different content types'
|
|
621
|
+
content: {
|
|
622
|
+
'application/json': {
|
|
623
|
+
schema: {
|
|
624
|
+
name: { type: 'string' },
|
|
625
|
+
image: { type: 'string' },
|
|
626
|
+
address: { type: 'string' }
|
|
627
|
+
}
|
|
628
|
+
},
|
|
629
|
+
'application/vnd.v1+json': {
|
|
630
|
+
schema: {
|
|
631
|
+
type: 'array',
|
|
632
|
+
items: { $ref: 'test' }
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
},
|
|
637
|
+
'3xx': {
|
|
638
|
+
content: {
|
|
639
|
+
'application/vnd.v2+json': {
|
|
640
|
+
schema: {
|
|
641
|
+
fullName: { type: 'string' },
|
|
642
|
+
phone: { type: 'string' }
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
fastify.post('/url', { schema }, handler)
|
|
651
|
+
```
|
|
614
652
|
|
|
615
653
|
#### Serializer Compiler
|
|
616
654
|
<a id="schema-serializer"></a>
|
|
@@ -621,7 +659,7 @@ change the default serialization method by providing a function to serialize
|
|
|
621
659
|
every route where you do.
|
|
622
660
|
|
|
623
661
|
```js
|
|
624
|
-
fastify.setSerializerCompiler(({ schema, method, url, httpStatus }) => {
|
|
662
|
+
fastify.setSerializerCompiler(({ schema, method, url, httpStatus, contentType }) => {
|
|
625
663
|
return data => JSON.stringify(data)
|
|
626
664
|
})
|
|
627
665
|
|
package/fastify.js
CHANGED
package/lib/error-handler.js
CHANGED
|
@@ -49,6 +49,8 @@ function handleError (reply, error, cb) {
|
|
|
49
49
|
// In case the error handler throws, we set the next errorHandler so we can error again
|
|
50
50
|
reply[kReplyNextErrorHandler] = Object.getPrototypeOf(errorHandler)
|
|
51
51
|
|
|
52
|
+
// we need to remove content-type to allow content-type guessing for serialization
|
|
53
|
+
delete reply[kReplyHeaders]['content-type']
|
|
52
54
|
delete reply[kReplyHeaders]['content-length']
|
|
53
55
|
|
|
54
56
|
const func = errorHandler.func
|
package/lib/error-serializer.js
CHANGED
package/lib/errors.js
CHANGED
|
@@ -97,7 +97,7 @@ const codes = {
|
|
|
97
97
|
),
|
|
98
98
|
FST_ERR_HOOK_INVALID_HANDLER: createError(
|
|
99
99
|
'FST_ERR_HOOK_INVALID_HANDLER',
|
|
100
|
-
'
|
|
100
|
+
'%s hook should be a function, instead got %s',
|
|
101
101
|
500,
|
|
102
102
|
TypeError
|
|
103
103
|
),
|
|
@@ -165,6 +165,10 @@ const codes = {
|
|
|
165
165
|
'FST_ERR_MISSING_SERIALIZATION_FN',
|
|
166
166
|
'Missing serialization function. Key "%s"'
|
|
167
167
|
),
|
|
168
|
+
FST_ERR_MISSING_CONTENTTYPE_SERIALIZATION_FN: createError(
|
|
169
|
+
'FST_ERR_MISSING_CONTENTTYPE_SERIALIZATION_FN',
|
|
170
|
+
'Missing serialization function. Key "%s:%s"'
|
|
171
|
+
),
|
|
168
172
|
FST_ERR_REQ_INVALID_VALIDATION_INVOCATION: createError(
|
|
169
173
|
'FST_ERR_REQ_INVALID_VALIDATION_INVOCATION',
|
|
170
174
|
'Invalid validation invocation. Missing validation function for HTTP part "%s" nor schema provided.'
|
|
@@ -181,6 +185,10 @@ const codes = {
|
|
|
181
185
|
'FST_ERR_SCH_ALREADY_PRESENT',
|
|
182
186
|
"Schema with id '%s' already declared!"
|
|
183
187
|
),
|
|
188
|
+
FST_ERR_SCH_CONTENT_MISSING_SCHEMA: createError(
|
|
189
|
+
'FST_ERR_SCH_CONTENT_MISSING_SCHEMA',
|
|
190
|
+
"Schema is missing for the content type '%s'"
|
|
191
|
+
),
|
|
184
192
|
FST_ERR_SCH_DUPLICATE: createError(
|
|
185
193
|
'FST_ERR_SCH_DUPLICATE',
|
|
186
194
|
"Schema with '%s' already present!"
|
package/lib/hooks.js
CHANGED
|
@@ -49,10 +49,10 @@ function Hooks () {
|
|
|
49
49
|
|
|
50
50
|
Hooks.prototype.validate = function (hook, fn) {
|
|
51
51
|
if (typeof hook !== 'string') throw new FST_ERR_HOOK_INVALID_TYPE()
|
|
52
|
-
if (typeof fn !== 'function') throw new FST_ERR_HOOK_INVALID_HANDLER()
|
|
53
52
|
if (supportedHooks.indexOf(hook) === -1) {
|
|
54
53
|
throw new Error(`${hook} hook not supported!`)
|
|
55
54
|
}
|
|
55
|
+
if (typeof fn !== 'function') throw new FST_ERR_HOOK_INVALID_HANDLER(hook, typeof fn)
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
Hooks.prototype.add = function (hook, fn) {
|
package/lib/reply.js
CHANGED
|
@@ -44,7 +44,8 @@ const {
|
|
|
44
44
|
FST_ERR_BAD_STATUS_CODE,
|
|
45
45
|
FST_ERR_BAD_TRAILER_NAME,
|
|
46
46
|
FST_ERR_BAD_TRAILER_VALUE,
|
|
47
|
-
FST_ERR_MISSING_SERIALIZATION_FN
|
|
47
|
+
FST_ERR_MISSING_SERIALIZATION_FN,
|
|
48
|
+
FST_ERR_MISSING_CONTENTTYPE_SERIALIZATION_FN
|
|
48
49
|
} = require('./errors')
|
|
49
50
|
const warning = require('./warnings')
|
|
50
51
|
|
|
@@ -312,11 +313,15 @@ Reply.prototype.code = function (code) {
|
|
|
312
313
|
|
|
313
314
|
Reply.prototype.status = Reply.prototype.code
|
|
314
315
|
|
|
315
|
-
Reply.prototype.getSerializationFunction = function (schemaOrStatus) {
|
|
316
|
+
Reply.prototype.getSerializationFunction = function (schemaOrStatus, contentType) {
|
|
316
317
|
let serialize
|
|
317
318
|
|
|
318
319
|
if (typeof schemaOrStatus === 'string' || typeof schemaOrStatus === 'number') {
|
|
319
|
-
|
|
320
|
+
if (typeof contentType === 'string') {
|
|
321
|
+
serialize = this[kRouteContext][kSchemaResponse]?.[schemaOrStatus]?.[contentType]
|
|
322
|
+
} else {
|
|
323
|
+
serialize = this[kRouteContext][kSchemaResponse]?.[schemaOrStatus]
|
|
324
|
+
}
|
|
320
325
|
} else if (typeof schemaOrStatus === 'object') {
|
|
321
326
|
serialize = this[kRouteContext][kReplySerializeWeakMap]?.get(schemaOrStatus)
|
|
322
327
|
}
|
|
@@ -324,7 +329,7 @@ Reply.prototype.getSerializationFunction = function (schemaOrStatus) {
|
|
|
324
329
|
return serialize
|
|
325
330
|
}
|
|
326
331
|
|
|
327
|
-
Reply.prototype.compileSerializationSchema = function (schema, httpStatus = null) {
|
|
332
|
+
Reply.prototype.compileSerializationSchema = function (schema, httpStatus = null, contentType = null) {
|
|
328
333
|
const { request } = this
|
|
329
334
|
const { method, url } = request
|
|
330
335
|
|
|
@@ -346,7 +351,8 @@ Reply.prototype.compileSerializationSchema = function (schema, httpStatus = null
|
|
|
346
351
|
schema,
|
|
347
352
|
method,
|
|
348
353
|
url,
|
|
349
|
-
httpStatus
|
|
354
|
+
httpStatus,
|
|
355
|
+
contentType
|
|
350
356
|
})
|
|
351
357
|
|
|
352
358
|
// We create a WeakMap to compile the schema only once
|
|
@@ -363,22 +369,34 @@ Reply.prototype.compileSerializationSchema = function (schema, httpStatus = null
|
|
|
363
369
|
return serializeFn
|
|
364
370
|
}
|
|
365
371
|
|
|
366
|
-
Reply.prototype.serializeInput = function (input, schema, httpStatus) {
|
|
372
|
+
Reply.prototype.serializeInput = function (input, schema, httpStatus, contentType) {
|
|
373
|
+
const possibleContentType = httpStatus
|
|
367
374
|
let serialize
|
|
368
375
|
httpStatus = typeof schema === 'string' || typeof schema === 'number'
|
|
369
376
|
? schema
|
|
370
377
|
: httpStatus
|
|
371
378
|
|
|
379
|
+
contentType = httpStatus && possibleContentType !== httpStatus
|
|
380
|
+
? possibleContentType
|
|
381
|
+
: contentType
|
|
382
|
+
|
|
372
383
|
if (httpStatus != null) {
|
|
373
|
-
|
|
384
|
+
if (contentType != null) {
|
|
385
|
+
serialize = this[kRouteContext][kSchemaResponse]?.[httpStatus]?.[contentType]
|
|
386
|
+
} else {
|
|
387
|
+
serialize = this[kRouteContext][kSchemaResponse]?.[httpStatus]
|
|
388
|
+
}
|
|
374
389
|
|
|
375
|
-
if (serialize == null)
|
|
390
|
+
if (serialize == null) {
|
|
391
|
+
if (contentType) throw new FST_ERR_MISSING_CONTENTTYPE_SERIALIZATION_FN(httpStatus, contentType)
|
|
392
|
+
throw new FST_ERR_MISSING_SERIALIZATION_FN(httpStatus)
|
|
393
|
+
}
|
|
376
394
|
} else {
|
|
377
395
|
// Check if serialize function already compiled
|
|
378
396
|
if (this[kRouteContext][kReplySerializeWeakMap]?.has(schema)) {
|
|
379
397
|
serialize = this[kRouteContext][kReplySerializeWeakMap].get(schema)
|
|
380
398
|
} else {
|
|
381
|
-
serialize = this.compileSerializationSchema(schema, httpStatus)
|
|
399
|
+
serialize = this.compileSerializationSchema(schema, httpStatus, contentType)
|
|
382
400
|
}
|
|
383
401
|
}
|
|
384
402
|
|
|
@@ -483,7 +501,7 @@ function preserializeHookEnd (err, request, reply, payload) {
|
|
|
483
501
|
} else if (reply[kRouteContext] && reply[kRouteContext][kReplySerializerDefault]) {
|
|
484
502
|
payload = reply[kRouteContext][kReplySerializerDefault](payload, reply.raw.statusCode)
|
|
485
503
|
} else {
|
|
486
|
-
payload = serialize(reply[kRouteContext], payload, reply.raw.statusCode)
|
|
504
|
+
payload = serialize(reply[kRouteContext], payload, reply.raw.statusCode, reply[kReplyHeaders]['content-type'])
|
|
487
505
|
}
|
|
488
506
|
} catch (e) {
|
|
489
507
|
wrapSeralizationError(e, reply)
|
|
@@ -797,10 +815,11 @@ function notFound (reply) {
|
|
|
797
815
|
* @param {object} context the request context
|
|
798
816
|
* @param {object} data the JSON payload to serialize
|
|
799
817
|
* @param {number} statusCode the http status code
|
|
818
|
+
* @param {string} contentType the reply content type
|
|
800
819
|
* @returns {string} the serialized payload
|
|
801
820
|
*/
|
|
802
|
-
function serialize (context, data, statusCode) {
|
|
803
|
-
const fnSerialize = getSchemaSerializer(context, statusCode)
|
|
821
|
+
function serialize (context, data, statusCode, contentType) {
|
|
822
|
+
const fnSerialize = getSchemaSerializer(context, statusCode, contentType)
|
|
804
823
|
if (fnSerialize) {
|
|
805
824
|
return fnSerialize(data)
|
|
806
825
|
}
|
package/lib/route.js
CHANGED
|
@@ -20,7 +20,8 @@ const {
|
|
|
20
20
|
FST_ERR_DEFAULT_ROUTE_INVALID_TYPE,
|
|
21
21
|
FST_ERR_DUPLICATED_ROUTE,
|
|
22
22
|
FST_ERR_INVALID_URL,
|
|
23
|
-
FST_ERR_SEND_UNDEFINED_ERR
|
|
23
|
+
FST_ERR_SEND_UNDEFINED_ERR,
|
|
24
|
+
FST_ERR_HOOK_INVALID_HANDLER
|
|
24
25
|
} = require('./errors')
|
|
25
26
|
|
|
26
27
|
const {
|
|
@@ -243,6 +244,20 @@ function buildRouting (options) {
|
|
|
243
244
|
}
|
|
244
245
|
}
|
|
245
246
|
|
|
247
|
+
for (const hook of lifecycleHooks) {
|
|
248
|
+
if (opts && hook in opts) {
|
|
249
|
+
if (Array.isArray(opts[hook])) {
|
|
250
|
+
for (const func of opts[hook]) {
|
|
251
|
+
if (typeof func !== 'function') {
|
|
252
|
+
throw new FST_ERR_HOOK_INVALID_HANDLER(hook, typeof func)
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
} else if (typeof opts[hook] !== 'function') {
|
|
256
|
+
throw new FST_ERR_HOOK_INVALID_HANDLER(hook, typeof opts[hook])
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
246
261
|
const constraints = opts.constraints || {}
|
|
247
262
|
const config = {
|
|
248
263
|
...opts.config,
|
package/lib/schema-controller.js
CHANGED
|
@@ -15,12 +15,16 @@ function buildSchemaController (parentSchemaCtrl, opts) {
|
|
|
15
15
|
return new SchemaController(parentSchemaCtrl, opts)
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
buildValidator:
|
|
20
|
-
buildSerializer:
|
|
18
|
+
const compilersFactory = Object.assign({
|
|
19
|
+
buildValidator: null,
|
|
20
|
+
buildSerializer: null
|
|
21
|
+
}, opts?.compilersFactory)
|
|
22
|
+
|
|
23
|
+
if (!compilersFactory.buildValidator) {
|
|
24
|
+
compilersFactory.buildValidator = ValidatorSelector()
|
|
21
25
|
}
|
|
22
|
-
if (
|
|
23
|
-
compilersFactory =
|
|
26
|
+
if (!compilersFactory.buildSerializer) {
|
|
27
|
+
compilersFactory.buildSerializer = SerializerSelector()
|
|
24
28
|
}
|
|
25
29
|
|
|
26
30
|
const option = {
|
package/lib/schemas.js
CHANGED
|
@@ -7,7 +7,8 @@ const kFluentSchema = Symbol.for('fluent-schema-object')
|
|
|
7
7
|
const {
|
|
8
8
|
FST_ERR_SCH_MISSING_ID,
|
|
9
9
|
FST_ERR_SCH_ALREADY_PRESENT,
|
|
10
|
-
FST_ERR_SCH_DUPLICATE
|
|
10
|
+
FST_ERR_SCH_DUPLICATE,
|
|
11
|
+
FST_ERR_SCH_CONTENT_MISSING_SCHEMA
|
|
11
12
|
} = require('./errors')
|
|
12
13
|
|
|
13
14
|
const SCHEMAS_SOURCE = ['params', 'body', 'querystring', 'query', 'headers']
|
|
@@ -86,7 +87,27 @@ function normalizeSchema (routeSchemas, serverOptions) {
|
|
|
86
87
|
if (routeSchemas.response) {
|
|
87
88
|
const httpCodes = Object.keys(routeSchemas.response)
|
|
88
89
|
for (const code of httpCodes) {
|
|
89
|
-
|
|
90
|
+
const contentProperty = routeSchemas.response[code].content
|
|
91
|
+
|
|
92
|
+
let hasContentMultipleContentTypes = false
|
|
93
|
+
if (contentProperty) {
|
|
94
|
+
const keys = Object.keys(contentProperty)
|
|
95
|
+
for (let i = 0; i < keys.length; i++) {
|
|
96
|
+
const mediaName = keys[i]
|
|
97
|
+
if (!contentProperty[mediaName].schema) {
|
|
98
|
+
if (keys.length === 1) { break }
|
|
99
|
+
throw new FST_ERR_SCH_CONTENT_MISSING_SCHEMA(mediaName)
|
|
100
|
+
}
|
|
101
|
+
routeSchemas.response[code].content[mediaName].schema = getSchemaAnyway(contentProperty[mediaName].schema, serverOptions.jsonShorthand)
|
|
102
|
+
if (i === keys.length - 1) {
|
|
103
|
+
hasContentMultipleContentTypes = true
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (!hasContentMultipleContentTypes) {
|
|
109
|
+
routeSchemas.response[code] = getSchemaAnyway(routeSchemas.response[code], serverOptions.jsonShorthand)
|
|
110
|
+
}
|
|
90
111
|
}
|
|
91
112
|
}
|
|
92
113
|
|
|
@@ -129,22 +150,49 @@ function getSchemaAnyway (schema, jsonShorthand) {
|
|
|
129
150
|
*
|
|
130
151
|
* @param {object} context the request context
|
|
131
152
|
* @param {number} statusCode the http status code
|
|
153
|
+
* @param {string} contentType the reply content type
|
|
132
154
|
* @returns {function|boolean} the right JSON Schema function to serialize
|
|
133
155
|
* the reply or false if it is not set
|
|
134
156
|
*/
|
|
135
|
-
function getSchemaSerializer (context, statusCode) {
|
|
157
|
+
function getSchemaSerializer (context, statusCode, contentType) {
|
|
136
158
|
const responseSchemaDef = context[kSchemaResponse]
|
|
137
159
|
if (!responseSchemaDef) {
|
|
138
160
|
return false
|
|
139
161
|
}
|
|
140
162
|
if (responseSchemaDef[statusCode]) {
|
|
163
|
+
if (responseSchemaDef[statusCode].constructor === Object) {
|
|
164
|
+
const mediaName = contentType.split(';')[0]
|
|
165
|
+
if (responseSchemaDef[statusCode][mediaName]) {
|
|
166
|
+
return responseSchemaDef[statusCode][mediaName]
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return false
|
|
170
|
+
}
|
|
141
171
|
return responseSchemaDef[statusCode]
|
|
142
172
|
}
|
|
143
173
|
const fallbackStatusCode = (statusCode + '')[0] + 'xx'
|
|
144
174
|
if (responseSchemaDef[fallbackStatusCode]) {
|
|
175
|
+
if (responseSchemaDef[fallbackStatusCode].constructor === Object) {
|
|
176
|
+
const mediaName = contentType.split(';')[0]
|
|
177
|
+
if (responseSchemaDef[fallbackStatusCode][mediaName]) {
|
|
178
|
+
return responseSchemaDef[fallbackStatusCode][mediaName]
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return false
|
|
182
|
+
}
|
|
183
|
+
|
|
145
184
|
return responseSchemaDef[fallbackStatusCode]
|
|
146
185
|
}
|
|
147
186
|
if (responseSchemaDef.default) {
|
|
187
|
+
if (responseSchemaDef.default.constructor === Object) {
|
|
188
|
+
const mediaName = contentType.split(';')[0]
|
|
189
|
+
if (responseSchemaDef.default[mediaName]) {
|
|
190
|
+
return responseSchemaDef.default[mediaName]
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return false
|
|
194
|
+
}
|
|
195
|
+
|
|
148
196
|
return responseSchemaDef.default
|
|
149
197
|
}
|
|
150
198
|
return false
|
package/lib/validation.js
CHANGED
|
@@ -22,12 +22,28 @@ function compileSchemasForSerialization (context, compile) {
|
|
|
22
22
|
throw new Error('response schemas should be nested under a valid status code, e.g { 2xx: { type: "object" } }')
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
25
|
+
if (schema.content) {
|
|
26
|
+
const contentTypesSchemas = {}
|
|
27
|
+
for (const mediaName of Object.keys(schema.content)) {
|
|
28
|
+
const contentSchema = schema.content[mediaName].schema
|
|
29
|
+
contentTypesSchemas[mediaName] = compile({
|
|
30
|
+
schema: contentSchema,
|
|
31
|
+
url,
|
|
32
|
+
method,
|
|
33
|
+
httpStatus: statusCode,
|
|
34
|
+
contentType: mediaName
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
acc[statusCode] = contentTypesSchemas
|
|
38
|
+
} else {
|
|
39
|
+
acc[statusCode] = compile({
|
|
40
|
+
schema,
|
|
41
|
+
url,
|
|
42
|
+
method,
|
|
43
|
+
httpStatus: statusCode
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
|
|
31
47
|
return acc
|
|
32
48
|
}, {})
|
|
33
49
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fastify",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.9.0",
|
|
4
4
|
"description": "Fast and low overhead web framework, for Node.js",
|
|
5
5
|
"main": "fastify.js",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -146,7 +146,7 @@
|
|
|
146
146
|
"fast-json-body": "^1.1.0",
|
|
147
147
|
"fast-json-stringify": "^5.3.0",
|
|
148
148
|
"fastify-plugin": "^4.2.1",
|
|
149
|
-
"fluent-json-schema": "^
|
|
149
|
+
"fluent-json-schema": "^4.0.0",
|
|
150
150
|
"form-data": "^4.0.0",
|
|
151
151
|
"h2url": "^0.2.0",
|
|
152
152
|
"http-errors": "^2.0.0",
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const t = require('tap')
|
|
4
|
+
const test = t.test
|
|
5
|
+
const Fastify = require('..')
|
|
6
|
+
|
|
7
|
+
test('should remove content-type for setErrorHandler', async t => {
|
|
8
|
+
t.plan(8)
|
|
9
|
+
let count = 0
|
|
10
|
+
|
|
11
|
+
const fastify = Fastify()
|
|
12
|
+
fastify.setErrorHandler(function (error, request, reply) {
|
|
13
|
+
t.same(error.message, 'kaboom')
|
|
14
|
+
t.same(reply.hasHeader('content-type'), false)
|
|
15
|
+
reply.code(400).send({ foo: 'bar' })
|
|
16
|
+
})
|
|
17
|
+
fastify.addHook('onSend', async function (request, reply, payload) {
|
|
18
|
+
count++
|
|
19
|
+
t.same(typeof payload, 'string')
|
|
20
|
+
switch (count) {
|
|
21
|
+
case 1: {
|
|
22
|
+
// should guess the correct content-type based on payload
|
|
23
|
+
t.same(reply.getHeader('content-type'), 'text/plain; charset=utf-8')
|
|
24
|
+
throw Error('kaboom')
|
|
25
|
+
}
|
|
26
|
+
case 2: {
|
|
27
|
+
// should guess the correct content-type based on payload
|
|
28
|
+
t.same(reply.getHeader('content-type'), 'application/json; charset=utf-8')
|
|
29
|
+
return payload
|
|
30
|
+
}
|
|
31
|
+
default: {
|
|
32
|
+
t.fail('should not reach')
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
fastify.get('/', function (request, reply) {
|
|
37
|
+
reply.send('plain-text')
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
const { statusCode, body } = await fastify.inject({ method: 'GET', path: '/' })
|
|
41
|
+
t.same(statusCode, 400)
|
|
42
|
+
t.same(body, JSON.stringify({ foo: 'bar' }))
|
|
43
|
+
})
|
package/test/hooks.test.js
CHANGED
|
@@ -3301,3 +3301,41 @@ test('onTimeout should be triggered and socket _meta is set', t => {
|
|
|
3301
3301
|
})
|
|
3302
3302
|
})
|
|
3303
3303
|
})
|
|
3304
|
+
|
|
3305
|
+
test('registering invalid hooks should throw an error', async t => {
|
|
3306
|
+
t.plan(3)
|
|
3307
|
+
|
|
3308
|
+
const fastify = Fastify()
|
|
3309
|
+
|
|
3310
|
+
t.throws(() => {
|
|
3311
|
+
fastify.route({
|
|
3312
|
+
method: 'GET',
|
|
3313
|
+
path: '/invalidHook',
|
|
3314
|
+
onRequest: [undefined],
|
|
3315
|
+
async handler () {
|
|
3316
|
+
return 'hello world'
|
|
3317
|
+
}
|
|
3318
|
+
})
|
|
3319
|
+
}, new Error('onRequest hook should be a function, instead got undefined'))
|
|
3320
|
+
|
|
3321
|
+
t.throws(() => {
|
|
3322
|
+
fastify.route({
|
|
3323
|
+
method: 'GET',
|
|
3324
|
+
path: '/invalidHook',
|
|
3325
|
+
onRequest: undefined,
|
|
3326
|
+
async handler () {
|
|
3327
|
+
return 'hello world'
|
|
3328
|
+
}
|
|
3329
|
+
})
|
|
3330
|
+
}, new Error('onRequest hook should be a function, instead got undefined'))
|
|
3331
|
+
|
|
3332
|
+
t.throws(() => {
|
|
3333
|
+
fastify.addHook('onRoute', (routeOptions) => {
|
|
3334
|
+
routeOptions.onSend = [undefined]
|
|
3335
|
+
})
|
|
3336
|
+
|
|
3337
|
+
fastify.get('/', function (request, reply) {
|
|
3338
|
+
reply.send('hello world')
|
|
3339
|
+
})
|
|
3340
|
+
}, new Error('onSend hook should be a function, instead got undefined'))
|
|
3341
|
+
})
|
|
@@ -66,7 +66,7 @@ test('should throw on wrong parameters', t => {
|
|
|
66
66
|
const hooks = new Hooks()
|
|
67
67
|
t.plan(4)
|
|
68
68
|
try {
|
|
69
|
-
hooks.add(null,
|
|
69
|
+
hooks.add(null, () => {})
|
|
70
70
|
t.fail()
|
|
71
71
|
} catch (e) {
|
|
72
72
|
t.equal(e.code, 'FST_ERR_HOOK_INVALID_TYPE')
|
|
@@ -74,10 +74,10 @@ test('should throw on wrong parameters', t => {
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
try {
|
|
77
|
-
hooks.add('', null)
|
|
77
|
+
hooks.add('onSend', null)
|
|
78
78
|
t.fail()
|
|
79
79
|
} catch (e) {
|
|
80
80
|
t.equal(e.code, 'FST_ERR_HOOK_INVALID_HANDLER')
|
|
81
|
-
t.equal(e.message, '
|
|
81
|
+
t.equal(e.message, 'onSend hook should be a function, instead got object')
|
|
82
82
|
}
|
|
83
83
|
})
|
|
@@ -45,6 +45,16 @@ function getResponseSchema () {
|
|
|
45
45
|
type: 'string'
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
|
+
},
|
|
49
|
+
'3xx': {
|
|
50
|
+
content: {
|
|
51
|
+
'application/json': {
|
|
52
|
+
schema: {
|
|
53
|
+
fullName: { type: 'string' },
|
|
54
|
+
phone: { type: 'number' }
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
48
58
|
}
|
|
49
59
|
}
|
|
50
60
|
}
|
|
@@ -141,7 +151,20 @@ test('Reply#compileSerializationSchema', t => {
|
|
|
141
151
|
}
|
|
142
152
|
}
|
|
143
153
|
|
|
144
|
-
|
|
154
|
+
const custom2 = ({ schema, httpStatus, url, method, contentType }) => {
|
|
155
|
+
t.equal(schema, schemaObj)
|
|
156
|
+
t.equal(url, '/user')
|
|
157
|
+
t.equal(method, 'GET')
|
|
158
|
+
t.equal(httpStatus, '3xx')
|
|
159
|
+
t.equal(contentType, 'application/json')
|
|
160
|
+
|
|
161
|
+
return input => {
|
|
162
|
+
t.same(input, { fullName: 'Jone', phone: 1090243795 })
|
|
163
|
+
return JSON.stringify(input)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
t.plan(17)
|
|
145
168
|
const schemaObj = getDefaultSchema()
|
|
146
169
|
|
|
147
170
|
fastify.get('/', { serializerCompiler: custom }, (req, reply) => {
|
|
@@ -157,10 +180,22 @@ test('Reply#compileSerializationSchema', t => {
|
|
|
157
180
|
reply.send({ hello: 'world' })
|
|
158
181
|
})
|
|
159
182
|
|
|
183
|
+
fastify.get('/user', { serializerCompiler: custom2 }, (req, reply) => {
|
|
184
|
+
const input = { fullName: 'Jone', phone: 1090243795 }
|
|
185
|
+
const first = reply.compileSerializationSchema(schemaObj, '3xx', 'application/json')
|
|
186
|
+
t.ok(first(input), JSON.stringify(input))
|
|
187
|
+
reply.send(input)
|
|
188
|
+
})
|
|
189
|
+
|
|
160
190
|
await fastify.inject({
|
|
161
191
|
path: '/',
|
|
162
192
|
method: 'GET'
|
|
163
193
|
})
|
|
194
|
+
|
|
195
|
+
await fastify.inject({
|
|
196
|
+
path: '/user',
|
|
197
|
+
method: 'GET'
|
|
198
|
+
})
|
|
164
199
|
}
|
|
165
200
|
)
|
|
166
201
|
|
|
@@ -209,10 +244,19 @@ test('Reply#getSerializationFunction', t => {
|
|
|
209
244
|
status: 'error',
|
|
210
245
|
code: 'something'
|
|
211
246
|
}
|
|
247
|
+
const okInput3xx = {
|
|
248
|
+
fullName: 'Jone',
|
|
249
|
+
phone: 0
|
|
250
|
+
}
|
|
251
|
+
const noOkInput3xx = {
|
|
252
|
+
fullName: 'Jone',
|
|
253
|
+
phone: 'phone'
|
|
254
|
+
}
|
|
212
255
|
let cached4xx
|
|
213
256
|
let cached201
|
|
257
|
+
let cachedJson3xx
|
|
214
258
|
|
|
215
|
-
t.plan(
|
|
259
|
+
t.plan(13)
|
|
216
260
|
|
|
217
261
|
const responseSchema = getResponseSchema()
|
|
218
262
|
|
|
@@ -234,15 +278,19 @@ test('Reply#getSerializationFunction', t => {
|
|
|
234
278
|
if (Number(id) === 1) {
|
|
235
279
|
const serialize4xx = reply.getSerializationFunction('4xx')
|
|
236
280
|
const serialize201 = reply.getSerializationFunction(201)
|
|
281
|
+
const serializeJson3xx = reply.getSerializationFunction('3xx', 'application/json')
|
|
237
282
|
const serializeUndefined = reply.getSerializationFunction(undefined)
|
|
238
283
|
|
|
239
284
|
cached4xx = serialize4xx
|
|
240
285
|
cached201 = serialize201
|
|
286
|
+
cachedJson3xx = serializeJson3xx
|
|
241
287
|
|
|
242
288
|
t.type(serialize4xx, Function)
|
|
243
289
|
t.type(serialize201, Function)
|
|
290
|
+
t.type(serializeJson3xx, Function)
|
|
244
291
|
t.equal(serialize4xx(okInput4xx), JSON.stringify(okInput4xx))
|
|
245
292
|
t.equal(serialize201(okInput201), JSON.stringify(okInput201))
|
|
293
|
+
t.equal(serializeJson3xx(okInput3xx), JSON.stringify(okInput3xx))
|
|
246
294
|
t.notOk(serializeUndefined)
|
|
247
295
|
|
|
248
296
|
try {
|
|
@@ -260,13 +308,21 @@ test('Reply#getSerializationFunction', t => {
|
|
|
260
308
|
t.equal(err.message, '"status" is required!')
|
|
261
309
|
}
|
|
262
310
|
|
|
311
|
+
try {
|
|
312
|
+
serializeJson3xx(noOkInput3xx)
|
|
313
|
+
} catch (err) {
|
|
314
|
+
t.equal(err.message, 'The value "phone" cannot be converted to a number.')
|
|
315
|
+
}
|
|
316
|
+
|
|
263
317
|
reply.status(201).send(okInput201)
|
|
264
318
|
} else {
|
|
265
319
|
const serialize201 = reply.getSerializationFunction(201)
|
|
266
320
|
const serialize4xx = reply.getSerializationFunction('4xx')
|
|
321
|
+
const serializeJson3xx = reply.getSerializationFunction('3xx', 'application/json')
|
|
267
322
|
|
|
268
323
|
t.equal(serialize4xx, cached4xx)
|
|
269
324
|
t.equal(serialize201, cached201)
|
|
325
|
+
t.equal(serializeJson3xx, cachedJson3xx)
|
|
270
326
|
reply.status(401).send(okInput4xx)
|
|
271
327
|
}
|
|
272
328
|
}
|
|
@@ -367,7 +423,7 @@ test('Reply#getSerializationFunction', t => {
|
|
|
367
423
|
})
|
|
368
424
|
|
|
369
425
|
test('Reply#serializeInput', t => {
|
|
370
|
-
t.plan(
|
|
426
|
+
t.plan(6)
|
|
371
427
|
|
|
372
428
|
t.test(
|
|
373
429
|
'Should throw if missed serialization function from HTTP status',
|
|
@@ -395,6 +451,47 @@ test('Reply#serializeInput', t => {
|
|
|
395
451
|
}
|
|
396
452
|
)
|
|
397
453
|
|
|
454
|
+
t.test(
|
|
455
|
+
'Should throw if missed serialization function from HTTP status with specific content type',
|
|
456
|
+
async t => {
|
|
457
|
+
const fastify = Fastify()
|
|
458
|
+
|
|
459
|
+
t.plan(2)
|
|
460
|
+
|
|
461
|
+
fastify.get('/', {
|
|
462
|
+
schema: {
|
|
463
|
+
response: {
|
|
464
|
+
'3xx': {
|
|
465
|
+
content: {
|
|
466
|
+
'application/json': {
|
|
467
|
+
schema: {
|
|
468
|
+
fullName: { type: 'string' },
|
|
469
|
+
phone: { type: 'number' }
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}, (req, reply) => {
|
|
477
|
+
reply.serializeInput({}, '3xx', 'application/vnd.v1+json')
|
|
478
|
+
})
|
|
479
|
+
|
|
480
|
+
const result = await fastify.inject({
|
|
481
|
+
path: '/',
|
|
482
|
+
method: 'GET'
|
|
483
|
+
})
|
|
484
|
+
|
|
485
|
+
t.equal(result.statusCode, 500)
|
|
486
|
+
t.same(result.json(), {
|
|
487
|
+
statusCode: 500,
|
|
488
|
+
code: 'FST_ERR_MISSING_CONTENTTYPE_SERIALIZATION_FN',
|
|
489
|
+
error: 'Internal Server Error',
|
|
490
|
+
message: 'Missing serialization function. Key "3xx:application/vnd.v1+json"'
|
|
491
|
+
})
|
|
492
|
+
}
|
|
493
|
+
)
|
|
494
|
+
|
|
398
495
|
t.test('Should use a serializer fn from HTTP status', async t => {
|
|
399
496
|
const fastify = Fastify()
|
|
400
497
|
const okInput201 = {
|
|
@@ -413,8 +510,16 @@ test('Reply#serializeInput', t => {
|
|
|
413
510
|
status: 'error',
|
|
414
511
|
code: 'something'
|
|
415
512
|
}
|
|
513
|
+
const okInput3xx = {
|
|
514
|
+
fullName: 'Jone',
|
|
515
|
+
phone: 0
|
|
516
|
+
}
|
|
517
|
+
const noOkInput3xx = {
|
|
518
|
+
fullName: 'Jone',
|
|
519
|
+
phone: 'phone'
|
|
520
|
+
}
|
|
416
521
|
|
|
417
|
-
t.plan(
|
|
522
|
+
t.plan(6)
|
|
418
523
|
|
|
419
524
|
fastify.get(
|
|
420
525
|
'/',
|
|
@@ -438,6 +543,17 @@ test('Reply#serializeInput', t => {
|
|
|
438
543
|
JSON.stringify(okInput201)
|
|
439
544
|
)
|
|
440
545
|
|
|
546
|
+
t.equal(
|
|
547
|
+
reply.serializeInput(okInput3xx, {}, '3xx', 'application/json'),
|
|
548
|
+
JSON.stringify(okInput3xx)
|
|
549
|
+
)
|
|
550
|
+
|
|
551
|
+
try {
|
|
552
|
+
reply.serializeInput(noOkInput3xx, '3xx', 'application/json')
|
|
553
|
+
} catch (err) {
|
|
554
|
+
t.equal(err.message, 'The value "phone" cannot be converted to a number.')
|
|
555
|
+
}
|
|
556
|
+
|
|
441
557
|
try {
|
|
442
558
|
reply.serializeInput(notOkInput4xx, '4xx')
|
|
443
559
|
} catch (err) {
|
|
@@ -60,6 +60,236 @@ test('custom serializer options', t => {
|
|
|
60
60
|
})
|
|
61
61
|
})
|
|
62
62
|
|
|
63
|
+
test('Different content types', t => {
|
|
64
|
+
t.plan(32)
|
|
65
|
+
|
|
66
|
+
const fastify = Fastify()
|
|
67
|
+
fastify.addSchema({
|
|
68
|
+
$id: 'test',
|
|
69
|
+
type: 'object',
|
|
70
|
+
properties: {
|
|
71
|
+
name: { type: 'string' },
|
|
72
|
+
age: { type: 'number' },
|
|
73
|
+
verified: { type: 'boolean' }
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
fastify.get('/', {
|
|
78
|
+
schema: {
|
|
79
|
+
response: {
|
|
80
|
+
200: {
|
|
81
|
+
content: {
|
|
82
|
+
'application/json': {
|
|
83
|
+
schema: {
|
|
84
|
+
name: { type: 'string' },
|
|
85
|
+
image: { type: 'string' },
|
|
86
|
+
address: { type: 'string' }
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
'application/vnd.v1+json': {
|
|
90
|
+
schema: {
|
|
91
|
+
type: 'array',
|
|
92
|
+
items: { $ref: 'test' }
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
201: {
|
|
98
|
+
content: { type: 'string' }
|
|
99
|
+
},
|
|
100
|
+
202: {
|
|
101
|
+
content: { const: 'Processing exclusive content' }
|
|
102
|
+
},
|
|
103
|
+
'3xx': {
|
|
104
|
+
content: {
|
|
105
|
+
'application/vnd.v2+json': {
|
|
106
|
+
schema: {
|
|
107
|
+
fullName: { type: 'string' },
|
|
108
|
+
phone: { type: 'string' }
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
default: {
|
|
114
|
+
content: {
|
|
115
|
+
'application/json': {
|
|
116
|
+
schema: {
|
|
117
|
+
details: { type: 'string' }
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}, function (req, reply) {
|
|
125
|
+
switch (req.headers.accept) {
|
|
126
|
+
case 'application/json':
|
|
127
|
+
reply.header('Content-Type', 'application/json')
|
|
128
|
+
reply.send({ id: 1, name: 'Foo', image: 'profile picture', address: 'New Node' })
|
|
129
|
+
break
|
|
130
|
+
case 'application/vnd.v1+json':
|
|
131
|
+
reply.header('Content-Type', 'application/vnd.v1+json')
|
|
132
|
+
reply.send([{ id: 2, name: 'Boo', age: 18, verified: false }, { id: 3, name: 'Woo', age: 30, verified: true }])
|
|
133
|
+
break
|
|
134
|
+
case 'application/vnd.v2+json':
|
|
135
|
+
reply.header('Content-Type', 'application/vnd.v2+json')
|
|
136
|
+
reply.code(300)
|
|
137
|
+
reply.send({ fullName: 'Jhon Smith', phone: '01090000000', authMethod: 'google' })
|
|
138
|
+
break
|
|
139
|
+
case 'application/vnd.v3+json':
|
|
140
|
+
reply.header('Content-Type', 'application/vnd.v3+json')
|
|
141
|
+
reply.code(300)
|
|
142
|
+
reply.send({ firstName: 'New', lastName: 'Hoo', country: 'eg', city: 'node' })
|
|
143
|
+
break
|
|
144
|
+
case 'application/vnd.v4+json':
|
|
145
|
+
reply.header('Content-Type', 'application/vnd.v4+json')
|
|
146
|
+
reply.code(201)
|
|
147
|
+
reply.send({ boxId: 1, content: 'Games' })
|
|
148
|
+
break
|
|
149
|
+
case 'application/vnd.v5+json':
|
|
150
|
+
reply.header('Content-Type', 'application/vnd.v5+json')
|
|
151
|
+
reply.code(202)
|
|
152
|
+
reply.send({ content: 'interesting content' })
|
|
153
|
+
break
|
|
154
|
+
case 'application/vnd.v6+json':
|
|
155
|
+
reply.header('Content-Type', 'application/vnd.v6+json')
|
|
156
|
+
reply.code(400)
|
|
157
|
+
reply.send({ desc: 'age is missing', details: 'validation error' })
|
|
158
|
+
break
|
|
159
|
+
case 'application/vnd.v7+json':
|
|
160
|
+
reply.code(400)
|
|
161
|
+
reply.send({ details: 'validation error' })
|
|
162
|
+
break
|
|
163
|
+
default:
|
|
164
|
+
// to test if schema not found
|
|
165
|
+
reply.header('Content-Type', 'application/vnd.v3+json')
|
|
166
|
+
reply.code(200)
|
|
167
|
+
reply.send([{ type: 'student', grade: 6 }, { type: 'student', grade: 9 }])
|
|
168
|
+
}
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
fastify.get('/test', {
|
|
172
|
+
serializerCompiler: ({ contentType }) => {
|
|
173
|
+
t.equal(contentType, 'application/json')
|
|
174
|
+
return data => JSON.stringify(data)
|
|
175
|
+
},
|
|
176
|
+
schema: {
|
|
177
|
+
response: {
|
|
178
|
+
200: {
|
|
179
|
+
content: {
|
|
180
|
+
'application/json': {
|
|
181
|
+
schema: {
|
|
182
|
+
name: { type: 'string' },
|
|
183
|
+
image: { type: 'string' },
|
|
184
|
+
address: { type: 'string' }
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}, function (req, reply) {
|
|
192
|
+
reply.header('Content-Type', 'application/json')
|
|
193
|
+
reply.send({ age: 18, city: 'AU' })
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
fastify.inject({ method: 'GET', url: '/', headers: { Accept: 'application/json' } }, (err, res) => {
|
|
197
|
+
t.error(err)
|
|
198
|
+
t.equal(res.payload, JSON.stringify({ name: 'Foo', image: 'profile picture', address: 'New Node' }))
|
|
199
|
+
t.equal(res.statusCode, 200)
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
fastify.inject({ method: 'GET', url: '/', headers: { Accept: 'application/vnd.v1+json' } }, (err, res) => {
|
|
203
|
+
t.error(err)
|
|
204
|
+
t.equal(res.payload, JSON.stringify([{ name: 'Boo', age: 18, verified: false }, { name: 'Woo', age: 30, verified: true }]))
|
|
205
|
+
t.equal(res.statusCode, 200)
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
fastify.inject({ method: 'GET', url: '/' }, (err, res) => {
|
|
209
|
+
t.error(err)
|
|
210
|
+
t.equal(res.payload, JSON.stringify([{ type: 'student', grade: 6 }, { type: 'student', grade: 9 }]))
|
|
211
|
+
t.equal(res.statusCode, 200)
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
fastify.inject({ method: 'GET', url: '/', headers: { Accept: 'application/vnd.v2+json' } }, (err, res) => {
|
|
215
|
+
t.error(err)
|
|
216
|
+
t.equal(res.payload, JSON.stringify({ fullName: 'Jhon Smith', phone: '01090000000' }))
|
|
217
|
+
t.equal(res.statusCode, 300)
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
fastify.inject({ method: 'GET', url: '/', headers: { Accept: 'application/vnd.v3+json' } }, (err, res) => {
|
|
221
|
+
t.error(err)
|
|
222
|
+
t.equal(res.payload, JSON.stringify({ firstName: 'New', lastName: 'Hoo', country: 'eg', city: 'node' }))
|
|
223
|
+
t.equal(res.statusCode, 300)
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
fastify.inject({ method: 'GET', url: '/', headers: { Accept: 'application/vnd.v4+json' } }, (err, res) => {
|
|
227
|
+
t.error(err)
|
|
228
|
+
t.equal(res.payload, JSON.stringify({ content: 'Games' }))
|
|
229
|
+
t.equal(res.statusCode, 201)
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
fastify.inject({ method: 'GET', url: '/', headers: { Accept: 'application/vnd.v5+json' } }, (err, res) => {
|
|
233
|
+
t.error(err)
|
|
234
|
+
t.equal(res.payload, JSON.stringify({ content: 'Processing exclusive content' }))
|
|
235
|
+
t.equal(res.statusCode, 202)
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
fastify.inject({ method: 'GET', url: '/', headers: { Accept: 'application/vnd.v6+json' } }, (err, res) => {
|
|
239
|
+
t.error(err)
|
|
240
|
+
t.equal(res.payload, JSON.stringify({ desc: 'age is missing', details: 'validation error' }))
|
|
241
|
+
t.equal(res.statusCode, 400)
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
fastify.inject({ method: 'GET', url: '/', headers: { Accept: 'application/vnd.v7+json' } }, (err, res) => {
|
|
245
|
+
t.error(err)
|
|
246
|
+
t.equal(res.payload, JSON.stringify({ details: 'validation error' }))
|
|
247
|
+
t.equal(res.statusCode, 400)
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
fastify.inject({ method: 'GET', url: '/test' }, (err, res) => {
|
|
251
|
+
t.error(err)
|
|
252
|
+
t.equal(res.payload, JSON.stringify({ age: 18, city: 'AU' }))
|
|
253
|
+
t.equal(res.statusCode, 200)
|
|
254
|
+
})
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
test('Invalid multiple content schema, throw FST_ERR_SCH_CONTENT_MISSING_SCHEMA error', t => {
|
|
258
|
+
t.plan(3)
|
|
259
|
+
const fastify = Fastify()
|
|
260
|
+
|
|
261
|
+
fastify.get('/testInvalid', {
|
|
262
|
+
schema: {
|
|
263
|
+
response: {
|
|
264
|
+
200: {
|
|
265
|
+
content: {
|
|
266
|
+
'application/json': {
|
|
267
|
+
schema: {
|
|
268
|
+
fullName: { type: 'string' },
|
|
269
|
+
phone: { type: 'string' }
|
|
270
|
+
},
|
|
271
|
+
example: {
|
|
272
|
+
fullName: 'John Doe',
|
|
273
|
+
phone: '201090243795'
|
|
274
|
+
}
|
|
275
|
+
},
|
|
276
|
+
type: 'string'
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}, function (req, reply) {
|
|
282
|
+
reply.header('Content-Type', 'application/json')
|
|
283
|
+
reply.send({ fullName: 'Any name', phone: '0109001010' })
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
fastify.ready((err) => {
|
|
287
|
+
t.equal(err.message, "Schema is missing for the content type 'type'")
|
|
288
|
+
t.equal(err.statusCode, 500)
|
|
289
|
+
t.equal(err.code, 'FST_ERR_SCH_CONTENT_MISSING_SCHEMA')
|
|
290
|
+
})
|
|
291
|
+
})
|
|
292
|
+
|
|
63
293
|
test('Use the same schema id in different places', t => {
|
|
64
294
|
t.plan(2)
|
|
65
295
|
const fastify = Fastify()
|
|
@@ -702,3 +702,45 @@ test('Custom schema object should not trigger FST_ERR_SCH_DUPLICATE', async t =>
|
|
|
702
702
|
await fastify.ready()
|
|
703
703
|
t.pass('fastify is ready')
|
|
704
704
|
})
|
|
705
|
+
|
|
706
|
+
test('The default schema compilers should not be called when overwritte by the user', async t => {
|
|
707
|
+
const Fastify = t.mock('../', {
|
|
708
|
+
'@fastify/ajv-compiler': () => {
|
|
709
|
+
t.fail('The default validator compiler should not be called')
|
|
710
|
+
},
|
|
711
|
+
'@fastify/fast-json-stringify-compiler': () => {
|
|
712
|
+
t.fail('The default serializer compiler should not be called')
|
|
713
|
+
}
|
|
714
|
+
})
|
|
715
|
+
|
|
716
|
+
const fastify = Fastify({
|
|
717
|
+
schemaController: {
|
|
718
|
+
compilersFactory: {
|
|
719
|
+
buildValidator: function factory () {
|
|
720
|
+
t.pass('The custom validator compiler should be called')
|
|
721
|
+
return function validatorCompiler () {
|
|
722
|
+
return () => { return true }
|
|
723
|
+
}
|
|
724
|
+
},
|
|
725
|
+
buildSerializer: function factory () {
|
|
726
|
+
t.pass('The custom serializer compiler should be called')
|
|
727
|
+
return function serializerCompiler () {
|
|
728
|
+
return () => { return true }
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
})
|
|
734
|
+
|
|
735
|
+
fastify.get('/',
|
|
736
|
+
{
|
|
737
|
+
schema: {
|
|
738
|
+
query: { foo: { type: 'string' } },
|
|
739
|
+
response: {
|
|
740
|
+
200: { type: 'object' }
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
}, () => {})
|
|
744
|
+
|
|
745
|
+
await fastify.ready()
|
|
746
|
+
})
|
package/types/reply.d.ts
CHANGED
|
@@ -55,10 +55,10 @@ export interface FastifyReply<
|
|
|
55
55
|
serializer(fn: (payload: any) => string): FastifyReply<RawServer, RawRequest, RawReply, RouteGeneric, ContextConfig, SchemaCompiler, TypeProvider>;
|
|
56
56
|
serialize(payload: any): string | ArrayBuffer | Buffer;
|
|
57
57
|
// Serialization Methods
|
|
58
|
-
getSerializationFunction(httpStatus: string): (payload: {[key: string]: unknown}) => string;
|
|
58
|
+
getSerializationFunction(httpStatus: string, contentType?: string): (payload: {[key: string]: unknown}) => string;
|
|
59
59
|
getSerializationFunction(schema: {[key: string]: unknown}): (payload: {[key: string]: unknown}) => string;
|
|
60
|
-
compileSerializationSchema(schema: {[key: string]: unknown}, httpStatus?: string): (payload: {[key: string]: unknown}) => string;
|
|
61
|
-
serializeInput(input: {[key: string]: unknown}, schema: {[key: string]: unknown}, httpStatus?: string): string;
|
|
62
|
-
serializeInput(input: {[key: string]: unknown}, httpStatus: string): unknown;
|
|
60
|
+
compileSerializationSchema(schema: {[key: string]: unknown}, httpStatus?: string, contentType?: string): (payload: {[key: string]: unknown}) => string;
|
|
61
|
+
serializeInput(input: {[key: string]: unknown}, schema: {[key: string]: unknown}, httpStatus?: string, contentType?: string): string;
|
|
62
|
+
serializeInput(input: {[key: string]: unknown}, httpStatus: string, contentType?: string): unknown;
|
|
63
63
|
then(fulfilled: () => void, rejected: (err: Error) => void): void;
|
|
64
64
|
}
|