UNPKG

6.04 kBJavaScriptView Raw
1'use strict'
2
3const fastClone = require('rfdc')({ circles: false, proto: true })
4const { kSchemaVisited, kSchemaResponse } = require('./symbols')
5const kFluentSchema = Symbol.for('fluent-schema-object')
6
7const {
8 FST_ERR_SCH_MISSING_ID,
9 FST_ERR_SCH_ALREADY_PRESENT,
10 FST_ERR_SCH_DUPLICATE,
11 FST_ERR_SCH_CONTENT_MISSING_SCHEMA
12} = require('./errors')
13
14const SCHEMAS_SOURCE = ['params', 'body', 'querystring', 'query', 'headers']
15
16function Schemas (initStore) {
17 this.store = initStore || {}
18}
19
20Schemas.prototype.add = function (inputSchema) {
21 const schema = fastClone((inputSchema.isFluentSchema || inputSchema.isFluentJSONSchema || inputSchema[kFluentSchema])
22 ? inputSchema.valueOf()
23 : inputSchema
24 )
25
26 // developers can add schemas without $id, but with $def instead
27 const id = schema.$id
28 if (!id) {
29 throw new FST_ERR_SCH_MISSING_ID()
30 }
31
32 if (this.store[id]) {
33 throw new FST_ERR_SCH_ALREADY_PRESENT(id)
34 }
35
36 this.store[id] = schema
37}
38
39Schemas.prototype.getSchemas = function () {
40 return Object.assign({}, this.store)
41}
42
43Schemas.prototype.getSchema = function (schemaId) {
44 return this.store[schemaId]
45}
46
47/**
48 * Checks whether a schema is a non-plain object.
49 *
50 * @param {*} schema the schema to check
51 * @returns {boolean} true if schema has a custom prototype
52 */
53function isCustomSchemaPrototype (schema) {
54 return typeof schema === 'object' && Object.getPrototypeOf(schema) !== Object.prototype
55}
56
57function normalizeSchema (routeSchemas, serverOptions) {
58 if (routeSchemas[kSchemaVisited]) {
59 return routeSchemas
60 }
61
62 // alias query to querystring schema
63 if (routeSchemas.query) {
64 // check if our schema has both querystring and query
65 if (routeSchemas.querystring) {
66 throw new FST_ERR_SCH_DUPLICATE('querystring')
67 }
68 routeSchemas.querystring = routeSchemas.query
69 }
70
71 generateFluentSchema(routeSchemas)
72
73 for (const key of SCHEMAS_SOURCE) {
74 const schema = routeSchemas[key]
75 if (schema && !isCustomSchemaPrototype(schema)) {
76 routeSchemas[key] = getSchemaAnyway(schema, serverOptions.jsonShorthand)
77 }
78 }
79
80 if (routeSchemas.response) {
81 const httpCodes = Object.keys(routeSchemas.response)
82 for (const code of httpCodes) {
83 if (isCustomSchemaPrototype(routeSchemas.response[code])) {
84 continue
85 }
86
87 const contentProperty = routeSchemas.response[code].content
88
89 let hasContentMultipleContentTypes = false
90 if (contentProperty) {
91 const keys = Object.keys(contentProperty)
92 for (let i = 0; i < keys.length; i++) {
93 const mediaName = keys[i]
94 if (!contentProperty[mediaName].schema) {
95 if (keys.length === 1) { break }
96 throw new FST_ERR_SCH_CONTENT_MISSING_SCHEMA(mediaName)
97 }
98 routeSchemas.response[code].content[mediaName].schema = getSchemaAnyway(contentProperty[mediaName].schema, serverOptions.jsonShorthand)
99 if (i === keys.length - 1) {
100 hasContentMultipleContentTypes = true
101 }
102 }
103 }
104
105 if (!hasContentMultipleContentTypes) {
106 routeSchemas.response[code] = getSchemaAnyway(routeSchemas.response[code], serverOptions.jsonShorthand)
107 }
108 }
109 }
110
111 routeSchemas[kSchemaVisited] = true
112 return routeSchemas
113}
114
115function generateFluentSchema (schema) {
116 for (const key of SCHEMAS_SOURCE) {
117 if (schema[key] && (schema[key].isFluentSchema || schema[key][kFluentSchema])) {
118 schema[key] = schema[key].valueOf()
119 }
120 }
121
122 if (schema.response) {
123 const httpCodes = Object.keys(schema.response)
124 for (const code of httpCodes) {
125 if (schema.response[code].isFluentSchema || schema.response[code][kFluentSchema]) {
126 schema.response[code] = schema.response[code].valueOf()
127 }
128 }
129 }
130}
131
132function getSchemaAnyway (schema, jsonShorthand) {
133 if (!jsonShorthand || schema.$ref || schema.oneOf || schema.allOf || schema.anyOf || schema.$merge || schema.$patch) return schema
134 if (!schema.type && !schema.properties) {
135 return {
136 type: 'object',
137 properties: schema
138 }
139 }
140 return schema
141}
142
143/**
144 * Search for the right JSON schema compiled function in the request context
145 * setup by the route configuration `schema.response`.
146 * It will look for the exact match (eg 200) or generic (eg 2xx)
147 *
148 * @param {object} context the request context
149 * @param {number} statusCode the http status code
150 * @param {string} [contentType] the reply content type
151 * @returns {function|false} the right JSON Schema function to serialize
152 * the reply or false if it is not set
153 */
154function getSchemaSerializer (context, statusCode, contentType) {
155 const responseSchemaDef = context[kSchemaResponse]
156 if (!responseSchemaDef) {
157 return false
158 }
159 if (responseSchemaDef[statusCode]) {
160 if (responseSchemaDef[statusCode].constructor === Object && contentType) {
161 const mediaName = contentType.split(';', 1)[0]
162 if (responseSchemaDef[statusCode][mediaName]) {
163 return responseSchemaDef[statusCode][mediaName]
164 }
165
166 return false
167 }
168 return responseSchemaDef[statusCode]
169 }
170 const fallbackStatusCode = (statusCode + '')[0] + 'xx'
171 if (responseSchemaDef[fallbackStatusCode]) {
172 if (responseSchemaDef[fallbackStatusCode].constructor === Object && contentType) {
173 const mediaName = contentType.split(';', 1)[0]
174 if (responseSchemaDef[fallbackStatusCode][mediaName]) {
175 return responseSchemaDef[fallbackStatusCode][mediaName]
176 }
177
178 return false
179 }
180
181 return responseSchemaDef[fallbackStatusCode]
182 }
183 if (responseSchemaDef.default) {
184 if (responseSchemaDef.default.constructor === Object && contentType) {
185 const mediaName = contentType.split(';', 1)[0]
186 if (responseSchemaDef.default[mediaName]) {
187 return responseSchemaDef.default[mediaName]
188 }
189
190 return false
191 }
192
193 return responseSchemaDef.default
194 }
195 return false
196}
197
198module.exports = {
199 buildSchemas (initStore) { return new Schemas(initStore) },
200 getSchemaSerializer,
201 normalizeSchema
202}