1 | 'use strict'
|
2 |
|
3 | const fastJsonStringify = require('fast-json-stringify')
|
4 | const Ajv = require('ajv')
|
5 |
|
6 | const bodySchema = Symbol('body-schema')
|
7 | const querystringSchema = Symbol('querystring-schema')
|
8 | const paramsSchema = Symbol('params-schema')
|
9 | const responseSchema = Symbol('response-schema')
|
10 | const headersSchema = Symbol('headers-schema')
|
11 | const kFluentSchema = Symbol.for('fluent-schema-object')
|
12 |
|
13 | function getValidatorForStatusCodeSchema (statusCodeDefinition, externalSchema) {
|
14 | return fastJsonStringify(statusCodeDefinition, { schema: externalSchema })
|
15 | }
|
16 |
|
17 | function getResponseSchema (responseSchemaDefinition, sharedSchemas) {
|
18 | var statusCodes = Object.keys(responseSchemaDefinition)
|
19 | return statusCodes.reduce(function (r, statusCode) {
|
20 | r[statusCode] = getValidatorForStatusCodeSchema(responseSchemaDefinition[statusCode], sharedSchemas)
|
21 | return r
|
22 | }, {})
|
23 | }
|
24 |
|
25 | function build (context, compile, schemas, schemaResolver) {
|
26 | if (!context.schema) {
|
27 | return
|
28 | }
|
29 |
|
30 | generateFluentSchema(context.schema)
|
31 | context.schema = schemas.resolveRefs(context.schema, false, schemaResolver)
|
32 |
|
33 | const headers = context.schema.headers
|
34 |
|
35 | if (headers && Object.getPrototypeOf(headers) !== Object.prototype) {
|
36 |
|
37 | context[headersSchema] = compile(headers)
|
38 | } else if (headers) {
|
39 |
|
40 |
|
41 | const headersSchemaLowerCase = {}
|
42 | Object.keys(headers).forEach(k => { headersSchemaLowerCase[k] = headers[k] })
|
43 | if (headersSchemaLowerCase.required instanceof Array) {
|
44 | headersSchemaLowerCase.required = headersSchemaLowerCase.required.map(h => h.toLowerCase())
|
45 | }
|
46 | if (headers.properties) {
|
47 | headersSchemaLowerCase.properties = {}
|
48 | Object.keys(headers.properties).forEach(k => {
|
49 | headersSchemaLowerCase.properties[k.toLowerCase()] = headers.properties[k]
|
50 | })
|
51 | }
|
52 | context[headersSchema] = compile(headersSchemaLowerCase)
|
53 | }
|
54 |
|
55 | if (context.schema.response) {
|
56 | context[responseSchema] = getResponseSchema(context.schema.response, schemas.getSchemas())
|
57 | }
|
58 |
|
59 | if (context.schema.body) {
|
60 | context[bodySchema] = compile(context.schema.body)
|
61 | }
|
62 |
|
63 | if (context.schema.querystring) {
|
64 | context[querystringSchema] = compile(context.schema.querystring)
|
65 | }
|
66 |
|
67 | if (context.schema.params) {
|
68 | context[paramsSchema] = compile(context.schema.params)
|
69 | }
|
70 | }
|
71 |
|
72 | function generateFluentSchema (schema) {
|
73 | ;['params', 'body', 'querystring', 'query', 'headers'].forEach(key => {
|
74 | if (schema[key] && (schema[key].isFluentSchema || schema[key][kFluentSchema])) {
|
75 | schema[key] = schema[key].valueOf()
|
76 | }
|
77 | })
|
78 |
|
79 | if (schema.response) {
|
80 | Object.keys(schema.response).forEach(code => {
|
81 | if (schema.response[code].isFluentSchema || schema.response[code][kFluentSchema]) {
|
82 | schema.response[code] = schema.response[code].valueOf()
|
83 | }
|
84 | })
|
85 | }
|
86 | }
|
87 |
|
88 | function validateParam (validatorFunction, request, paramName) {
|
89 | var ret = validatorFunction && validatorFunction(request[paramName])
|
90 | if (ret === false) return validatorFunction.errors
|
91 | if (ret && ret.error) return ret.error
|
92 | if (ret && ret.value) request[paramName] = ret.value
|
93 | return false
|
94 | }
|
95 |
|
96 | function validate (context, request) {
|
97 | var params = validateParam(context[paramsSchema], request, 'params')
|
98 | if (params) {
|
99 | return wrapValidationError(params, 'params')
|
100 | }
|
101 | var body = validateParam(context[bodySchema], request, 'body')
|
102 | if (body) {
|
103 | return wrapValidationError(body, 'body')
|
104 | }
|
105 | var query = validateParam(context[querystringSchema], request, 'query')
|
106 | if (query) {
|
107 | return wrapValidationError(query, 'querystring')
|
108 | }
|
109 | var headers = validateParam(context[headersSchema], request, 'headers')
|
110 | if (headers) {
|
111 | return wrapValidationError(headers, 'headers')
|
112 | }
|
113 | return null
|
114 | }
|
115 |
|
116 | function wrapValidationError (result, dataVar) {
|
117 | if (result instanceof Error) {
|
118 | return result
|
119 | }
|
120 | var error = new Error(schemaErrorsText(result, dataVar))
|
121 | error.validation = result
|
122 | error.validationContext = dataVar
|
123 | return error
|
124 | }
|
125 |
|
126 | function serialize (context, data, statusCode) {
|
127 | var responseSchemaDef = context[responseSchema]
|
128 | if (!responseSchemaDef) {
|
129 | return JSON.stringify(data)
|
130 | }
|
131 | if (responseSchemaDef[statusCode]) {
|
132 | return responseSchemaDef[statusCode](data)
|
133 | }
|
134 | var fallbackStatusCode = (statusCode + '')[0] + 'xx'
|
135 | if (responseSchemaDef[fallbackStatusCode]) {
|
136 | return responseSchemaDef[fallbackStatusCode](data)
|
137 | }
|
138 | return JSON.stringify(data)
|
139 | }
|
140 |
|
141 | function isValidLogger (logger) {
|
142 | if (!logger) {
|
143 | return false
|
144 | }
|
145 |
|
146 | var result = true
|
147 | const methods = ['info', 'error', 'debug', 'fatal', 'warn', 'trace', 'child']
|
148 | for (var i = 0; i < methods.length; i += 1) {
|
149 | if (!logger[methods[i]] || typeof logger[methods[i]] !== 'function') {
|
150 | result = false
|
151 | break
|
152 | }
|
153 | }
|
154 | return result
|
155 | }
|
156 |
|
157 | function schemaErrorsText (errors, dataVar) {
|
158 | var text = ''
|
159 | var separator = ', '
|
160 | for (var i = 0; i < errors.length; i++) {
|
161 | var e = errors[i]
|
162 | text += dataVar + (e.dataPath || '') + ' ' + e.message + separator
|
163 | }
|
164 | return text.slice(0, -separator.length)
|
165 | }
|
166 |
|
167 | function buildSchemaCompiler (externalSchemas, options, cache) {
|
168 |
|
169 |
|
170 | const ajv = new Ajv(Object.assign({
|
171 | coerceTypes: true,
|
172 | useDefaults: true,
|
173 | removeAdditional: true,
|
174 |
|
175 |
|
176 | allErrors: false,
|
177 | nullable: true
|
178 | }, options.customOptions, { cache }))
|
179 |
|
180 | if (options.plugins && options.plugins.length > 0) {
|
181 | for (const plugin of options.plugins) {
|
182 | plugin[0](ajv, plugin[1])
|
183 | }
|
184 | }
|
185 |
|
186 | if (Array.isArray(externalSchemas)) {
|
187 | externalSchemas.forEach(s => ajv.addSchema(s))
|
188 | }
|
189 |
|
190 | return ajv.compile.bind(ajv)
|
191 | }
|
192 |
|
193 | module.exports = { build, validate, serialize, isValidLogger, buildSchemaCompiler }
|
194 | module.exports.symbols = { bodySchema, querystringSchema, responseSchema, paramsSchema, headersSchema }
|