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.
@@ -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/heply/fastify-bcrypt) A Bcrypt hash
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/heply/fastify-crud-generator) A
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/heply/fastify-polyglot) A plugin to
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/heply/fastify-totp) A plugin to handle
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
@@ -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 the specified
69
- schema and returns a serialization function using the default (or customized)
70
- `SerializerCompiler`. The optional `httpStatus` is forwarded to the
71
- `SerializerCompiler` if provided, default to `undefined`.
72
- - `.serializeInput(data, schema, [,httpStatus])` - Serializes the specified data
73
- using the specified schema and returns the serialized payload.
74
- If the optional `httpStatus` is provided, the function will use the serializer
75
- function given for that HTTP Status Code. Default to `undefined`.
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 that can be used to
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 paramater `httpStatus`, if provided, is forwarded directly
385
- the `SerializerCompiler`, so it can be used to compile the serialization
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)
@@ -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 } })`: function that
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
@@ -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
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const VERSION = '4.8.1'
3
+ const VERSION = '4.9.0'
4
4
 
5
5
  const Avvio = require('avvio')
6
6
  const http = require('http')
@@ -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
@@ -217,7 +217,7 @@ class Serializer {
217
217
  }
218
218
 
219
219
 
220
-
220
+
221
221
 
222
222
  module.exports = main
223
223
 
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
- 'The hook callback must be a function',
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
- serialize = this[kRouteContext][kSchemaResponse]?.[schemaOrStatus]
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
- serialize = this[kRouteContext][kSchemaResponse]?.[httpStatus]
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) throw new FST_ERR_MISSING_SERIALIZATION_FN(httpStatus)
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,
@@ -15,12 +15,16 @@ function buildSchemaController (parentSchemaCtrl, opts) {
15
15
  return new SchemaController(parentSchemaCtrl, opts)
16
16
  }
17
17
 
18
- let compilersFactory = {
19
- buildValidator: ValidatorSelector(),
20
- buildSerializer: SerializerSelector()
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 (opts && opts.compilersFactory) {
23
- compilersFactory = Object.assign(compilersFactory, opts.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
- routeSchemas.response[code] = getSchemaAnyway(routeSchemas.response[code], serverOptions.jsonShorthand)
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
- acc[statusCode] = compile({
26
- schema,
27
- url,
28
- method,
29
- httpStatus: statusCode
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.8.1",
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": "^3.1.0",
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
+ })
@@ -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, noop)
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, 'The hook callback must be a function')
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
- t.plan(10)
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(9)
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(5)
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(4)
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
  }
package/types/schema.d.ts CHANGED
@@ -20,6 +20,7 @@ export interface FastifyRouteSchemaDef<T> {
20
20
  url: string;
21
21
  httpPart?: string;
22
22
  httpStatus?: string;
23
+ contentType?: string;
23
24
  }
24
25
 
25
26
  export interface FastifySchemaValidationError {