fastify 4.2.0 → 4.4.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.
Files changed (63) hide show
  1. package/README.md +13 -14
  2. package/docs/Guides/Database.md +2 -2
  3. package/docs/Guides/Ecosystem.md +42 -18
  4. package/docs/Guides/Migration-Guide-V4.md +29 -0
  5. package/docs/Guides/Plugins-Guide.md +5 -0
  6. package/docs/Reference/HTTP2.md +1 -3
  7. package/docs/Reference/Hooks.md +4 -1
  8. package/docs/Reference/Logging.md +1 -1
  9. package/docs/Reference/Reply.md +177 -0
  10. package/docs/Reference/Request.md +171 -0
  11. package/docs/Reference/Routes.md +4 -2
  12. package/docs/Reference/Server.md +4 -1
  13. package/docs/Reference/Type-Providers.md +1 -15
  14. package/docs/Reference/TypeScript.md +27 -3
  15. package/fastify.d.ts +1 -1
  16. package/fastify.js +3 -3
  17. package/lib/contentTypeParser.js +10 -2
  18. package/lib/context.js +10 -1
  19. package/lib/error-serializer.js +12 -12
  20. package/lib/errors.js +8 -0
  21. package/lib/handleRequest.js +2 -2
  22. package/lib/httpMethods.js +22 -0
  23. package/lib/reply.js +101 -24
  24. package/lib/request.js +97 -1
  25. package/lib/route.js +3 -1
  26. package/lib/symbols.js +15 -9
  27. package/package.json +7 -7
  28. package/test/build/error-serializer.test.js +3 -1
  29. package/test/content-parser.test.js +15 -0
  30. package/test/copy.test.js +41 -0
  31. package/test/internals/all.test.js +8 -2
  32. package/test/internals/reply-serialize.test.js +583 -0
  33. package/test/internals/reply.test.js +4 -1
  34. package/test/internals/request-validate.test.js +1269 -0
  35. package/test/internals/request.test.js +11 -2
  36. package/test/lock.test.js +73 -0
  37. package/test/mkcol.test.js +38 -0
  38. package/test/move.test.js +45 -0
  39. package/test/propfind.test.js +108 -0
  40. package/test/proppatch.test.js +78 -0
  41. package/test/request-error.test.js +44 -1
  42. package/test/schema-validation.test.js +71 -0
  43. package/test/search.test.js +100 -0
  44. package/test/trace.test.js +21 -0
  45. package/test/types/fastify.test-d.ts +12 -1
  46. package/test/types/hooks.test-d.ts +1 -2
  47. package/test/types/import.ts +1 -1
  48. package/test/types/instance.test-d.ts +4 -1
  49. package/test/types/logger.test-d.ts +4 -5
  50. package/test/types/reply.test-d.ts +44 -3
  51. package/test/types/request.test-d.ts +10 -29
  52. package/test/types/type-provider.test-d.ts +79 -7
  53. package/test/unlock.test.js +41 -0
  54. package/test/validation-error-handling.test.js +6 -1
  55. package/types/hooks.d.ts +19 -39
  56. package/types/instance.d.ts +78 -130
  57. package/types/logger.d.ts +7 -4
  58. package/types/reply.d.ts +7 -1
  59. package/types/request.d.ts +16 -3
  60. package/types/route.d.ts +23 -29
  61. package/types/schema.d.ts +4 -1
  62. package/types/type-provider.d.ts +8 -20
  63. package/types/utils.d.ts +2 -1
@@ -0,0 +1,583 @@
1
+ 'use strict'
2
+
3
+ const { test } = require('tap')
4
+ const { kReplySerializeWeakMap } = require('../../lib/symbols')
5
+ const Fastify = require('../../fastify')
6
+
7
+ function getDefaultSchema () {
8
+ return {
9
+ type: 'object',
10
+ required: ['hello'],
11
+ properties: {
12
+ hello: { type: 'string' },
13
+ world: { type: 'string' }
14
+ }
15
+ }
16
+ }
17
+
18
+ function getResponseSchema () {
19
+ return {
20
+ 201: {
21
+ type: 'object',
22
+ required: ['status'],
23
+ properties: {
24
+ status: {
25
+ type: 'string',
26
+ enum: ['ok']
27
+ },
28
+ message: {
29
+ type: 'string'
30
+ }
31
+ }
32
+ },
33
+ '4xx': {
34
+ type: 'object',
35
+ properties: {
36
+ status: {
37
+ type: 'string',
38
+ enum: ['error']
39
+ },
40
+ code: {
41
+ type: 'integer',
42
+ minimum: 1
43
+ },
44
+ message: {
45
+ type: 'string'
46
+ }
47
+ }
48
+ }
49
+ }
50
+ }
51
+
52
+ test('Reply#compileSerializationSchema', t => {
53
+ t.plan(4)
54
+
55
+ t.test('Should return a serialization function', async t => {
56
+ const fastify = Fastify()
57
+
58
+ t.plan(4)
59
+
60
+ fastify.get('/', (req, reply) => {
61
+ const serialize = reply.compileSerializationSchema(getDefaultSchema())
62
+ const input = { hello: 'world' }
63
+ t.type(serialize, Function)
64
+ t.type(serialize(input), 'string')
65
+ t.equal(serialize(input), JSON.stringify(input))
66
+
67
+ try {
68
+ serialize({ world: 'foo' })
69
+ } catch (err) {
70
+ t.equal(err.message, '"hello" is required!')
71
+ }
72
+
73
+ reply.send({ hello: 'world' })
74
+ })
75
+
76
+ await fastify.inject({
77
+ path: '/',
78
+ method: 'GET'
79
+ })
80
+ })
81
+
82
+ t.test('Should reuse the serialize fn across multiple invocations - Route without schema',
83
+ async t => {
84
+ const fastify = Fastify()
85
+ let serialize = null
86
+ let counter = 0
87
+
88
+ t.plan(17)
89
+
90
+ const schemaObj = getDefaultSchema()
91
+
92
+ fastify.get('/', (req, reply) => {
93
+ const input = { hello: 'world' }
94
+ counter++
95
+ if (counter > 1) {
96
+ const newSerialize = reply.compileSerializationSchema(schemaObj)
97
+ t.equal(serialize, newSerialize, 'Are the same validate function')
98
+ serialize = newSerialize
99
+ } else {
100
+ t.pass('build the schema compilation function')
101
+ serialize = reply.compileSerializationSchema(schemaObj)
102
+ }
103
+
104
+ t.type(serialize, Function)
105
+ t.equal(serialize(input), JSON.stringify(input))
106
+
107
+ try {
108
+ serialize({ world: 'foo' })
109
+ } catch (err) {
110
+ t.equal(err.message, '"hello" is required!')
111
+ }
112
+
113
+ reply.send({ hello: 'world' })
114
+ })
115
+
116
+ await Promise.all([
117
+ fastify.inject('/'),
118
+ fastify.inject('/'),
119
+ fastify.inject('/'),
120
+ fastify.inject('/')
121
+ ])
122
+
123
+ t.equal(counter, 4)
124
+ }
125
+ )
126
+
127
+ t.test('Should use the custom serializer compiler for the route',
128
+ async t => {
129
+ const fastify = Fastify()
130
+ let called = 0
131
+ const custom = ({ schema, httpStatus, url, method }) => {
132
+ t.equal(schema, schemaObj)
133
+ t.equal(url, '/')
134
+ t.equal(method, 'GET')
135
+ t.equal(httpStatus, '201')
136
+
137
+ return input => {
138
+ called++
139
+ t.same(input, { hello: 'world' })
140
+ return JSON.stringify(input)
141
+ }
142
+ }
143
+
144
+ t.plan(10)
145
+ const schemaObj = getDefaultSchema()
146
+
147
+ fastify.get('/', { serializerCompiler: custom }, (req, reply) => {
148
+ const input = { hello: 'world' }
149
+ const first = reply.compileSerializationSchema(schemaObj, '201')
150
+ const second = reply.compileSerializationSchema(schemaObj, '201')
151
+
152
+ t.equal(first, second)
153
+ t.ok(first(input), JSON.stringify(input))
154
+ t.ok(second(input), JSON.stringify(input))
155
+ t.equal(called, 2)
156
+
157
+ reply.send({ hello: 'world' })
158
+ })
159
+
160
+ await fastify.inject({
161
+ path: '/',
162
+ method: 'GET'
163
+ })
164
+ }
165
+ )
166
+
167
+ t.test('Should build a WeakMap for cache when called', async t => {
168
+ const fastify = Fastify()
169
+
170
+ t.plan(4)
171
+
172
+ fastify.get('/', (req, reply) => {
173
+ const input = { hello: 'world' }
174
+
175
+ t.equal(reply.context[kReplySerializeWeakMap], null)
176
+ t.equal(reply.compileSerializationSchema(getDefaultSchema())(input), JSON.stringify(input))
177
+ t.type(reply.context[kReplySerializeWeakMap], WeakMap)
178
+ t.equal(reply.compileSerializationSchema(getDefaultSchema())(input), JSON.stringify(input))
179
+
180
+ reply.send({ hello: 'world' })
181
+ })
182
+
183
+ await fastify.inject({
184
+ path: '/',
185
+ method: 'GET'
186
+ })
187
+ })
188
+ })
189
+
190
+ test('Reply#getSerializationFunction', t => {
191
+ t.plan(3)
192
+
193
+ t.test('Should retrieve the serialization function from the Schema definition',
194
+ async t => {
195
+ const fastify = Fastify()
196
+ const okInput201 = {
197
+ status: 'ok',
198
+ message: 'done!'
199
+ }
200
+ const notOkInput201 = {
201
+ message: 'created'
202
+ }
203
+ const okInput4xx = {
204
+ status: 'error',
205
+ code: 2,
206
+ message: 'oops!'
207
+ }
208
+ const notOkInput4xx = {
209
+ status: 'error',
210
+ code: 'something'
211
+ }
212
+ let cached4xx
213
+ let cached201
214
+
215
+ t.plan(9)
216
+
217
+ const responseSchema = getResponseSchema()
218
+
219
+ fastify.get(
220
+ '/:id',
221
+ {
222
+ params: {
223
+ id: {
224
+ type: 'integer'
225
+ }
226
+ },
227
+ schema: {
228
+ response: responseSchema
229
+ }
230
+ },
231
+ (req, reply) => {
232
+ const { id } = req.params
233
+
234
+ if (parseInt(id) === 1) {
235
+ const serialize4xx = reply.getSerializationFunction('4xx')
236
+ const serialize201 = reply.getSerializationFunction(201)
237
+ const serializeUndefined = reply.getSerializationFunction(undefined)
238
+
239
+ cached4xx = serialize4xx
240
+ cached201 = serialize201
241
+
242
+ t.type(serialize4xx, Function)
243
+ t.type(serialize201, Function)
244
+ t.equal(serialize4xx(okInput4xx), JSON.stringify(okInput4xx))
245
+ t.equal(serialize201(okInput201), JSON.stringify(okInput201))
246
+ t.notOk(serializeUndefined)
247
+
248
+ try {
249
+ serialize4xx(notOkInput4xx)
250
+ } catch (err) {
251
+ t.equal(
252
+ err.message,
253
+ 'The value "something" cannot be converted to an integer.'
254
+ )
255
+ }
256
+
257
+ try {
258
+ serialize201(notOkInput201)
259
+ } catch (err) {
260
+ t.equal(err.message, '"status" is required!')
261
+ }
262
+
263
+ reply.status(201).send(okInput201)
264
+ } else {
265
+ const serialize201 = reply.getSerializationFunction(201)
266
+ const serialize4xx = reply.getSerializationFunction('4xx')
267
+
268
+ t.equal(serialize4xx, cached4xx)
269
+ t.equal(serialize201, cached201)
270
+ reply.status(401).send(okInput4xx)
271
+ }
272
+ }
273
+ )
274
+
275
+ await Promise.all([
276
+ fastify.inject('/1'),
277
+ fastify.inject('/2')
278
+ ])
279
+ }
280
+ )
281
+
282
+ t.test('Should retrieve the serialization function from the cached one',
283
+ async t => {
284
+ const fastify = Fastify()
285
+
286
+ const schemaObj = getDefaultSchema()
287
+
288
+ const okInput = {
289
+ hello: 'world',
290
+ world: 'done!'
291
+ }
292
+ const notOkInput = {
293
+ world: 'done!'
294
+ }
295
+ let cached
296
+
297
+ t.plan(6)
298
+
299
+ fastify.get(
300
+ '/:id',
301
+ {
302
+ params: {
303
+ id: {
304
+ type: 'integer'
305
+ }
306
+ }
307
+ },
308
+ (req, reply) => {
309
+ const { id } = req.params
310
+
311
+ if (parseInt(id) === 1) {
312
+ const serialize = reply.compileSerializationSchema(schemaObj)
313
+
314
+ t.type(serialize, Function)
315
+ t.equal(serialize(okInput), JSON.stringify(okInput))
316
+
317
+ try {
318
+ serialize(notOkInput)
319
+ } catch (err) {
320
+ t.equal(err.message, '"hello" is required!')
321
+ }
322
+
323
+ cached = serialize
324
+ } else {
325
+ const serialize = reply.getSerializationFunction(schemaObj)
326
+
327
+ t.equal(serialize, cached)
328
+ t.equal(serialize(okInput), JSON.stringify(okInput))
329
+
330
+ try {
331
+ serialize(notOkInput)
332
+ } catch (err) {
333
+ t.equal(err.message, '"hello" is required!')
334
+ }
335
+ }
336
+
337
+ reply.status(201).send(okInput)
338
+ }
339
+ )
340
+
341
+ await Promise.all([
342
+ fastify.inject('/1'),
343
+ fastify.inject('/2')
344
+ ])
345
+ }
346
+ )
347
+
348
+ t.test('Should not instantiate a WeakMap if it is not needed', async t => {
349
+ const fastify = Fastify()
350
+
351
+ t.plan(4)
352
+
353
+ fastify.get('/', (req, reply) => {
354
+ t.notOk(reply.getSerializationFunction(getDefaultSchema()))
355
+ t.equal(reply.context[kReplySerializeWeakMap], null)
356
+ t.notOk(reply.getSerializationFunction('200'))
357
+ t.equal(reply.context[kReplySerializeWeakMap], null)
358
+
359
+ reply.send({ hello: 'world' })
360
+ })
361
+
362
+ await fastify.inject({
363
+ path: '/',
364
+ method: 'GET'
365
+ })
366
+ })
367
+ })
368
+
369
+ test('Reply#serializeInput', t => {
370
+ t.plan(5)
371
+
372
+ t.test(
373
+ 'Should throw if missed serialization function from HTTP status',
374
+ async t => {
375
+ const fastify = Fastify()
376
+
377
+ t.plan(2)
378
+
379
+ fastify.get('/', (req, reply) => {
380
+ reply.serializeInput({}, 201)
381
+ })
382
+
383
+ const result = await fastify.inject({
384
+ path: '/',
385
+ method: 'GET'
386
+ })
387
+
388
+ t.equal(result.statusCode, 500)
389
+ t.same(result.json(), {
390
+ statusCode: 500,
391
+ code: 'FST_ERR_MISSING_SERIALIZATION_FN',
392
+ error: 'Internal Server Error',
393
+ message: 'Missing serialization function. Key "201"'
394
+ })
395
+ }
396
+ )
397
+
398
+ t.test('Should use a serializer fn from HTTP status', async t => {
399
+ const fastify = Fastify()
400
+ const okInput201 = {
401
+ status: 'ok',
402
+ message: 'done!'
403
+ }
404
+ const notOkInput201 = {
405
+ message: 'created'
406
+ }
407
+ const okInput4xx = {
408
+ status: 'error',
409
+ code: 2,
410
+ message: 'oops!'
411
+ }
412
+ const notOkInput4xx = {
413
+ status: 'error',
414
+ code: 'something'
415
+ }
416
+
417
+ t.plan(4)
418
+
419
+ fastify.get(
420
+ '/',
421
+ {
422
+ params: {
423
+ id: {
424
+ type: 'integer'
425
+ }
426
+ },
427
+ schema: {
428
+ response: getResponseSchema()
429
+ }
430
+ },
431
+ (req, reply) => {
432
+ t.equal(
433
+ reply.serializeInput(okInput4xx, '4xx'),
434
+ JSON.stringify(okInput4xx)
435
+ )
436
+ t.equal(
437
+ reply.serializeInput(okInput201, 201),
438
+ JSON.stringify(okInput201)
439
+ )
440
+
441
+ try {
442
+ reply.serializeInput(notOkInput4xx, '4xx')
443
+ } catch (err) {
444
+ t.equal(
445
+ err.message,
446
+ 'The value "something" cannot be converted to an integer.'
447
+ )
448
+ }
449
+
450
+ try {
451
+ reply.serializeInput(notOkInput201, 201)
452
+ } catch (err) {
453
+ t.equal(err.message, '"status" is required!')
454
+ }
455
+
456
+ reply.status(204).send('')
457
+ }
458
+ )
459
+
460
+ await fastify.inject({
461
+ path: '/',
462
+ method: 'GET'
463
+ })
464
+ })
465
+
466
+ t.test(
467
+ 'Should compile a serializer out of a schema if serializer fn missed',
468
+ async t => {
469
+ let compilerCalled = 0
470
+ let serializerCalled = 0
471
+ const testInput = { hello: 'world' }
472
+ const schemaObj = getDefaultSchema()
473
+ const fastify = Fastify()
474
+ const serializerCompiler = ({ schema, httpStatus, method, url }) => {
475
+ t.equal(schema, schemaObj)
476
+ t.notOk(httpStatus)
477
+ t.equal(method, 'GET')
478
+ t.equal(url, '/')
479
+
480
+ compilerCalled++
481
+ return input => {
482
+ t.equal(input, testInput)
483
+ serializerCalled++
484
+ return JSON.stringify(input)
485
+ }
486
+ }
487
+
488
+ t.plan(10)
489
+
490
+ fastify.get('/', { serializerCompiler }, (req, reply) => {
491
+ t.equal(
492
+ reply.serializeInput(testInput, schemaObj),
493
+ JSON.stringify(testInput)
494
+ )
495
+
496
+ t.equal(
497
+ reply.serializeInput(testInput, schemaObj),
498
+ JSON.stringify(testInput)
499
+ )
500
+
501
+ reply.status(201).send(testInput)
502
+ })
503
+
504
+ await fastify.inject({
505
+ path: '/',
506
+ method: 'GET'
507
+ })
508
+
509
+ t.equal(compilerCalled, 1)
510
+ t.equal(serializerCalled, 2)
511
+ }
512
+ )
513
+
514
+ t.test('Should use a cached serializer fn', async t => {
515
+ let compilerCalled = 0
516
+ let serializerCalled = 0
517
+ let cached
518
+ const testInput = { hello: 'world' }
519
+ const schemaObj = getDefaultSchema()
520
+ const fastify = Fastify()
521
+ const serializer = input => {
522
+ t.equal(input, testInput)
523
+ serializerCalled++
524
+ return JSON.stringify(input)
525
+ }
526
+ const serializerCompiler = ({ schema, httpStatus, method, url }) => {
527
+ t.equal(schema, schemaObj)
528
+ t.notOk(httpStatus)
529
+ t.equal(method, 'GET')
530
+ t.equal(url, '/')
531
+
532
+ compilerCalled++
533
+ return serializer
534
+ }
535
+
536
+ t.plan(12)
537
+
538
+ fastify.get('/', { serializerCompiler }, (req, reply) => {
539
+ t.equal(
540
+ reply.serializeInput(testInput, schemaObj),
541
+ JSON.stringify(testInput)
542
+ )
543
+
544
+ cached = reply.getSerializationFunction(schemaObj)
545
+
546
+ t.equal(
547
+ reply.serializeInput(testInput, schemaObj),
548
+ cached(testInput)
549
+ )
550
+
551
+ reply.status(201).send(testInput)
552
+ })
553
+
554
+ await fastify.inject({
555
+ path: '/',
556
+ method: 'GET'
557
+ })
558
+
559
+ t.equal(cached, serializer)
560
+ t.equal(compilerCalled, 1)
561
+ t.equal(serializerCalled, 3)
562
+ })
563
+
564
+ t.test('Should instantiate a WeakMap after first call', async t => {
565
+ const fastify = Fastify()
566
+
567
+ t.plan(3)
568
+
569
+ fastify.get('/', (req, reply) => {
570
+ const input = { hello: 'world' }
571
+ t.equal(reply.context[kReplySerializeWeakMap], null)
572
+ t.equal(reply.serializeInput(input, getDefaultSchema()), JSON.stringify(input))
573
+ t.type(reply.context[kReplySerializeWeakMap], WeakMap)
574
+
575
+ reply.send({ hello: 'world' })
576
+ })
577
+
578
+ await fastify.inject({
579
+ path: '/',
580
+ method: 'GET'
581
+ })
582
+ })
583
+ })
@@ -1044,7 +1044,7 @@ test('reply.hasHeader returns correct values', t => {
1044
1044
  })
1045
1045
 
1046
1046
  test('reply.getHeader returns correct values', t => {
1047
- t.plan(4)
1047
+ t.plan(5)
1048
1048
 
1049
1049
  const fastify = require('../../')()
1050
1050
 
@@ -1055,6 +1055,9 @@ test('reply.getHeader returns correct values', t => {
1055
1055
  reply.header('x-foo', 'bar')
1056
1056
  t.strictSame(reply.getHeader('x-foo'), 'bar')
1057
1057
 
1058
+ reply.header('x-foo', 42)
1059
+ t.strictSame(reply.getHeader('x-foo'), 42)
1060
+
1058
1061
  reply.header('set-cookie', 'one')
1059
1062
  reply.header('set-cookie', 'two')
1060
1063
  t.strictSame(reply.getHeader('set-cookie'), ['one', 'two'])