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