-
-
Notifications
You must be signed in to change notification settings - Fork 17
/
index.js
134 lines (116 loc) · 3.84 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
'use strict'
const fp = require('fastify-plugin')
const Ajv = require('ajv')
const AjvCore = require('ajv/dist/core')
function fastifyResponseValidation (fastify, opts, next) {
let ajv
if (opts.ajv && opts.ajv instanceof AjvCore.default) {
ajv = opts.ajv
} else {
const { plugins: ajvPlugins, ...ajvOptions } = Object.assign({
coerceTypes: false,
useDefaults: true,
removeAdditional: true,
allErrors: true,
plugins: []
}, opts.ajv)
if (!Array.isArray(ajvPlugins)) {
next(new Error(`ajv.plugins option should be an array, instead got '${typeof ajvPlugins}'`))
return
}
ajv = new Ajv(ajvOptions)
for (const plugin of ajvPlugins) {
if (Array.isArray(plugin)) {
plugin[0](ajv, plugin[1])
} else {
plugin(ajv)
}
}
}
if (opts.responseValidation !== false) {
fastify.addHook('onRoute', onRoute)
}
function onRoute (routeOpts) {
if (routeOpts.responseValidation === false) return
if (routeOpts.schema && routeOpts.schema.response) {
const responseStatusCodeValidation = routeOpts.responseStatusCodeValidation || opts.responseStatusCodeValidation || false
routeOpts.preSerialization = routeOpts.preSerialization || []
routeOpts.preSerialization.push(buildHook(routeOpts.schema.response, responseStatusCodeValidation))
}
}
function buildHook (schema, responseStatusCodeValidation) {
const statusCodes = {}
for (const statusCode in schema) {
const responseSchema = schema[statusCode]
if (responseSchema.content !== undefined) {
statusCodes[statusCode] = {}
for (const mediaName in responseSchema.content) {
statusCodes[statusCode][mediaName] = ajv.compile(
getSchemaAnyway(responseSchema.content[mediaName].schema)
)
}
} else {
statusCodes[statusCode] = ajv.compile(
getSchemaAnyway(responseSchema)
)
}
}
return preSerialization
function preSerialization (req, reply, payload, next) {
let validate = statusCodes[reply.statusCode] || statusCodes[(reply.statusCode + '')[0] + 'xx']
if (responseStatusCodeValidation && validate === undefined) {
next(new Error(`No schema defined for status code ${reply.statusCode}`))
return
}
if (validate !== undefined) {
// Per media type validation
if (validate.constructor === Object) {
const mediaName = reply.getHeader('content-type').split(';', 1)[0]
if (validate[mediaName] == null) {
next(new Error(`No schema defined for media type ${mediaName}`))
return
}
validate = validate[mediaName]
}
const valid = validate(payload)
if (!valid) {
const err = new Error(schemaErrorsText(validate.errors))
err.validation = validate.errors
reply.code(500)
return next(err)
}
}
next()
}
}
next()
}
/**
* Copy-paste of getSchemaAnyway from fastify
*
* https://github.com/fastify/fastify/blob/23371945d01c270af24f4a5b7e2e31c4e806e6b3/lib/schemas.js#L113
*/
function getSchemaAnyway (schema) {
if (schema.$ref || schema.oneOf || schema.allOf || schema.anyOf || schema.$merge || schema.$patch) return schema
if (!schema.type && !schema.properties) {
return {
type: 'object',
properties: schema
}
}
return schema
}
function schemaErrorsText (errors) {
let text = ''
const separator = ', '
for (const e of errors) {
text += 'response' + (e.instancePath || '') + ' ' + e.message + separator
}
return text.slice(0, -separator.length)
}
module.exports = fp(fastifyResponseValidation, {
fastify: '5.x',
name: '@fastify/response-validation'
})
module.exports.default = fastifyResponseValidation
module.exports.fastifyResponseValidation = fastifyResponseValidation