-
-
Notifications
You must be signed in to change notification settings - Fork 2.2k
/
schemas.js
183 lines (154 loc) · 5.44 KB
/
schemas.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
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
'use strict'
const fastClone = require('rfdc')({ circles: false, proto: true })
const { kSchemaVisited } = require('./symbols')
const kFluentSchema = Symbol.for('fluent-schema-object')
const {
codes: {
FST_ERR_SCH_MISSING_ID,
FST_ERR_SCH_ALREADY_PRESENT,
FST_ERR_SCH_NOT_PRESENT,
FST_ERR_SCH_DUPLICATE
}
} = require('./errors')
const URI_NAME_FRAGMENT = /^#[A-Za-z]{1}[\w-:.]{0,}$/
function Schemas () {
this.store = {}
}
Schemas.prototype.add = function (inputSchema, refResolver) {
var schema = fastClone((inputSchema.isFluentSchema || inputSchema[kFluentSchema])
? inputSchema.valueOf()
: inputSchema
)
const id = schema.$id
if (id === undefined) {
throw new FST_ERR_SCH_MISSING_ID()
}
if (this.store[id] !== undefined) {
throw new FST_ERR_SCH_ALREADY_PRESENT(id)
}
this.store[id] = this.resolveRefs(schema, true, refResolver)
}
Schemas.prototype.resolve = function (id) {
if (this.store[id] === undefined) {
throw new FST_ERR_SCH_NOT_PRESENT(id)
}
return Object.assign({}, this.store[id])
}
Schemas.prototype.resolveRefs = function (routeSchemas, dontClearId, refResolver) {
if (routeSchemas[kSchemaVisited]) {
return routeSchemas
}
// alias query to querystring schema
if (routeSchemas.query) {
// check if our schema has both querystring and query
if (routeSchemas.querystring) {
throw new FST_ERR_SCH_DUPLICATE('querystring')
}
routeSchemas.querystring = routeSchemas.query
}
// let's check if our schemas have a custom prototype
for (const key of ['headers', 'querystring', 'params', 'body']) {
if (typeof routeSchemas[key] === 'object' && Object.getPrototypeOf(routeSchemas[key]) !== Object.prototype) {
return routeSchemas
}
}
// See issue https://github.com/fastify/fastify/issues/1767
const cachedSchema = Object.assign({}, routeSchemas)
try {
// this will work only for standard json schemas
// other compilers such as Joi will fail
this.traverse(routeSchemas, refResolver)
// when a plugin uses the 'skip-override' and call addSchema
// the same JSON will be pass throug all the avvio tree. In this case
// it is not possible clean the id. The id will be cleared
// in the startup phase by the call of validation.js. Details PR #1496
if (dontClearId !== true) {
this.cleanId(routeSchemas)
}
} catch (err) {
// if we have failed because `resolve` has thrown
// let's rethrow the error and let avvio handle it
if (/FST_ERR_SCH_*/.test(err.code)) throw err
// otherwise, the schema must not be a JSON schema
// so we let the user configured schemaCompiler handle it
return cachedSchema
}
if (routeSchemas.headers) {
routeSchemas.headers = this.getSchemaAnyway(routeSchemas.headers)
}
if (routeSchemas.querystring) {
routeSchemas.querystring = this.getSchemaAnyway(routeSchemas.querystring)
}
if (routeSchemas.params) {
routeSchemas.params = this.getSchemaAnyway(routeSchemas.params)
}
routeSchemas[kSchemaVisited] = true
return routeSchemas
}
Schemas.prototype.traverse = function (schema, refResolver) {
for (var key in schema) {
// resolve the `sharedSchemaId#' only if is not a standard $ref JSON Pointer
if (typeof schema[key] === 'string' && key !== '$schema' && key !== '$ref' && schema[key].slice(-1) === '#') {
schema[key] = this.resolve(schema[key].slice(0, -1))
} else if (key === '$ref' && refResolver) {
const refValue = schema[key]
const framePos = refValue.indexOf('#')
const refId = framePos >= 0 ? refValue.slice(0, framePos) : refValue
if (refId.length > 0 && !this.store[refId]) {
const resolvedSchema = refResolver(refId)
if (resolvedSchema) {
this.add(resolvedSchema, refResolver)
}
}
}
if (schema[key] !== null && typeof schema[key] === 'object' &&
(key !== 'enum' || (key === 'enum' && schema.type !== 'string'))) {
// don't traverse non-object values and the `enum` keyword when used for string type
this.traverse(schema[key], refResolver)
}
}
}
Schemas.prototype.cleanId = function (schema) {
for (var key in schema) {
if (key === '$id' && !URI_NAME_FRAGMENT.test(schema[key])) {
delete schema[key]
}
if (schema[key] !== null && typeof schema[key] === 'object') {
this.cleanId(schema[key])
}
}
}
Schemas.prototype.getSchemaAnyway = function (schema) {
if (schema.oneOf || schema.allOf || schema.anyOf || schema.$merge || schema.$patch) return schema
if (!schema.type || !schema.properties) {
return {
type: 'object',
properties: schema
}
}
return schema
}
Schemas.prototype.getSchemas = function () {
return Object.assign({}, this.store)
}
Schemas.prototype.getJsonSchemas = function (options) {
const store = this.getSchemas()
const schemasArray = Object.keys(store).map(schemaKey => {
// if the shared-schema "replace-way" has been used, the $id field has been removed
if (store[schemaKey].$id === undefined) {
store[schemaKey].$id = schemaKey
}
return store[schemaKey]
})
if (options && options.onlyAbsoluteUri === true) {
// the caller wants only the absolute URI (without the shared schema - "replace-way" usage)
return schemasArray.filter(_ => !/^\w*$/g.test(_.$id))
}
return schemasArray
}
function buildSchemas (s) {
const schema = new Schemas()
s.getJsonSchemas().forEach(_ => schema.add(_))
return schema
}
module.exports = { Schemas, buildSchemas }