UNPKG

4.72 kBJavaScriptView Raw
1'use strict'
2
3const fastClone = require('rfdc')({ circles: false, proto: true })
4const kFluentSchema = Symbol.for('fluent-schema-object')
5
6const {
7 codes: {
8 FST_ERR_SCH_MISSING_ID,
9 FST_ERR_SCH_ALREADY_PRESENT,
10 FST_ERR_SCH_NOT_PRESENT,
11 FST_ERR_SCH_DUPLICATE
12 }
13} = require('./errors')
14
15const URI_NAME_FRAGMENT = /^#[A-Za-z]{1}[\w-:.]{0,}$/
16
17function Schemas () {
18 this.store = {}
19}
20
21Schemas.prototype.add = function (inputSchema) {
22 var schema = fastClone((inputSchema.isFluentSchema || inputSchema[kFluentSchema])
23 ? inputSchema.valueOf()
24 : inputSchema
25 )
26 const id = schema.$id
27 if (id === undefined) {
28 throw new FST_ERR_SCH_MISSING_ID()
29 }
30
31 if (this.store[id] !== undefined) {
32 throw new FST_ERR_SCH_ALREADY_PRESENT(id)
33 }
34
35 this.store[id] = this.resolveRefs(schema, true)
36}
37
38Schemas.prototype.resolve = function (id) {
39 if (this.store[id] === undefined) {
40 throw new FST_ERR_SCH_NOT_PRESENT(id)
41 }
42 return this.store[id]
43}
44
45Schemas.prototype.resolveRefs = function (routeSchemas, dontClearId) {
46 // alias query to querystring schema
47 if (routeSchemas.query) {
48 // check if our schema has both querystring and query
49 if (routeSchemas.querystring) {
50 throw new FST_ERR_SCH_DUPLICATE('querystring')
51 }
52 routeSchemas.querystring = routeSchemas.query
53 }
54
55 // let's check if our schemas have a custom prototype
56 for (const key of ['headers', 'querystring', 'params', 'body']) {
57 if (typeof routeSchemas[key] === 'object' && Object.getPrototypeOf(routeSchemas[key]) !== Object.prototype) {
58 return routeSchemas
59 }
60 }
61
62 // See issue https://github.com/fastify/fastify/issues/1767
63 const cachedSchema = Object.assign({}, routeSchemas)
64
65 try {
66 // this will work only for standard json schemas
67 // other compilers such as Joi will fail
68 this.traverse(routeSchemas)
69
70 // when a plugin uses the 'skip-override' and call addSchema
71 // the same JSON will be pass throug all the avvio tree. In this case
72 // it is not possible clean the id. The id will be cleared
73 // in the startup phase by the call of validation.js. Details PR #1496
74 if (dontClearId !== true) {
75 this.cleanId(routeSchemas)
76 }
77 } catch (err) {
78 // if we have failed because `resolve` has thrown
79 // let's rethrow the error and let avvio handle it
80 if (/FST_ERR_SCH_*/.test(err.code)) throw err
81
82 // otherwise, the schema must not be a JSON schema
83 // so we let the user configured schemaCompiler handle it
84 return cachedSchema
85 }
86
87 if (routeSchemas.headers) {
88 routeSchemas.headers = this.getSchemaAnyway(routeSchemas.headers)
89 }
90
91 if (routeSchemas.querystring) {
92 routeSchemas.querystring = this.getSchemaAnyway(routeSchemas.querystring)
93 }
94
95 if (routeSchemas.params) {
96 routeSchemas.params = this.getSchemaAnyway(routeSchemas.params)
97 }
98
99 return routeSchemas
100}
101
102Schemas.prototype.traverse = function (schema) {
103 for (var key in schema) {
104 // resolve the `sharedSchemaId#' only if is not a standard $ref JSON Pointer
105 if (typeof schema[key] === 'string' && key !== '$schema' && key !== '$ref' && schema[key].slice(-1) === '#') {
106 schema[key] = this.resolve(schema[key].slice(0, -1))
107 }
108
109 if (schema[key] !== null && typeof schema[key] === 'object') {
110 this.traverse(schema[key])
111 }
112 }
113}
114
115Schemas.prototype.cleanId = function (schema) {
116 for (var key in schema) {
117 if (key === '$id' && !URI_NAME_FRAGMENT.test(schema[key])) {
118 delete schema[key]
119 }
120 if (schema[key] !== null && typeof schema[key] === 'object') {
121 this.cleanId(schema[key])
122 }
123 }
124}
125
126Schemas.prototype.getSchemaAnyway = function (schema) {
127 if (schema.oneOf || schema.allOf || schema.anyOf) return schema
128 if (!schema.type || !schema.properties) {
129 return {
130 type: 'object',
131 properties: schema
132 }
133 }
134 return schema
135}
136
137Schemas.prototype.getSchemas = function () {
138 return Object.assign({}, this.store)
139}
140
141Schemas.prototype.getJsonSchemas = function (options) {
142 const store = this.getSchemas()
143 const schemasArray = Object.keys(store).map(schemaKey => {
144 // if the shared-schema "replace-way" has been used, the $id field has been removed
145 if (store[schemaKey].$id === undefined) {
146 store[schemaKey].$id = schemaKey
147 }
148 return store[schemaKey]
149 })
150
151 if (options && options.onlyAbsoluteUri === true) {
152 // the caller wants only the absolute URI (without the shared schema - "replace-way" usage)
153 return schemasArray.filter(_ => !/^\w*$/g.test(_.$id))
154 }
155 return schemasArray
156}
157
158function buildSchemas (s) {
159 const schema = new Schemas()
160 s.getJsonSchemas().forEach(_ => schema.add(_))
161 return schema
162}
163
164module.exports = { Schemas, buildSchemas }