|
1 | | -'use strict' |
| 1 | +"use strict"; |
2 | 2 |
|
3 | | -const path = require('path') |
| 3 | +const path = require("path"); |
4 | 4 |
|
5 | | -const $RefParser = require("@apidevtools/json-schema-ref-parser") |
6 | | -const SchemaConvertor = require('json-schema-for-openapi') |
7 | | -const isEqual = require('lodash.isequal') |
8 | | -const { v4: uuid } = require('uuid') |
| 5 | +const $RefParser = require("@apidevtools/json-schema-ref-parser"); |
| 6 | +const SchemaConvertor = require("json-schema-for-openapi"); |
| 7 | +const isEqual = require("lodash.isequal"); |
| 8 | +const { v4: uuid } = require("uuid"); |
9 | 9 |
|
10 | 10 | class SchemaHandler { |
11 | | - constructor(serverless, openAPI) { |
12 | | - this.apiGatewayModels = serverless.service?.provider?.apiGateway?.request?.schemas || {} |
13 | | - this.documentation = serverless.service.custom.documentation |
14 | | - this.openAPI = openAPI |
| 11 | + constructor(serverless, openAPI) { |
| 12 | + this.apiGatewayModels = |
| 13 | + serverless.service?.provider?.apiGateway?.request?.schemas || {}; |
| 14 | + this.documentation = serverless.service.custom.documentation; |
| 15 | + this.openAPI = openAPI; |
15 | 16 |
|
16 | | - this.modelReferences = {} |
| 17 | + this.modelReferences = {}; |
17 | 18 |
|
18 | | - this.__standardiseModels() |
| 19 | + this.__standardiseModels(); |
19 | 20 |
|
20 | | - try { |
21 | | - this.refParserOptions = require(path.resolve('options', 'ref-parser.js')) |
22 | | - } catch (err) { |
23 | | - this.refParserOptions = {} |
24 | | - } |
| 21 | + try { |
| 22 | + this.refParserOptions = require(path.resolve("options", "ref-parser.js")); |
| 23 | + } catch (err) { |
| 24 | + this.refParserOptions = {}; |
25 | 25 | } |
26 | | - |
27 | | - /** |
28 | | - * Standardises the models to a specific format |
29 | | - */ |
30 | | - __standardiseModels() { |
31 | | - const standardModel = (model) => { |
32 | | - if (model.schema) { |
33 | | - return model |
| 26 | + } |
| 27 | + |
| 28 | + /** |
| 29 | + * Standardises the models to a specific format |
| 30 | + */ |
| 31 | + __standardiseModels() { |
| 32 | + const standardModel = (model) => { |
| 33 | + if (model.schema) { |
| 34 | + return model; |
| 35 | + } |
| 36 | + |
| 37 | + const contentType = Object.keys(model.content)[0]; |
| 38 | + model.contentType = contentType; |
| 39 | + model.schema = model.content[contentType].schema; |
| 40 | + |
| 41 | + return model; |
| 42 | + }; |
| 43 | + |
| 44 | + const standardisedModels = |
| 45 | + this.documentation?.models?.map(standardModel) || []; |
| 46 | + const standardisedModelsList = |
| 47 | + this.documentation?.modelsList?.map(standardModel) || []; |
| 48 | + |
| 49 | + const standardisedGatewayModels = |
| 50 | + Object.keys(this.apiGatewayModels).flatMap((key) => { |
| 51 | + const gatewayModel = this.apiGatewayModels[key]; |
| 52 | + return standardModel(gatewayModel); |
| 53 | + }) || []; |
| 54 | + |
| 55 | + this.models = standardisedModels.concat( |
| 56 | + standardisedModelsList, |
| 57 | + standardisedGatewayModels |
| 58 | + ); |
| 59 | + } |
| 60 | + |
| 61 | + async addModelsToOpenAPI() { |
| 62 | + for (const model of this.models) { |
| 63 | + const modelName = model.name; |
| 64 | + const modelSchema = model.schema; |
| 65 | + |
| 66 | + const dereferencedSchema = await this.__dereferenceSchema( |
| 67 | + modelSchema |
| 68 | + ).catch((err) => { |
| 69 | + if (err.errors) { |
| 70 | + for (const error of err?.errors) { |
| 71 | + if (error.message.includes("HTTP ERROR")) { |
| 72 | + throw err; |
34 | 73 | } |
35 | | - |
36 | | - const contentType = Object.keys(model.content)[0] |
37 | | - model.contentType = contentType |
38 | | - model.schema = model.content[contentType].schema |
39 | | - |
40 | | - return model |
| 74 | + } |
41 | 75 | } |
42 | | - |
43 | | - const standardisedModels = this.documentation?.models?.map(standardModel) || [] |
44 | | - const standardisedModelsList = this.documentation?.modelsList?.map(standardModel) || [] |
45 | | - |
46 | | - const standardisedGatewayModels = Object.keys(this.apiGatewayModels).flatMap(key => { |
47 | | - const gatewayModel = this.apiGatewayModels[key] |
48 | | - return standardModel(gatewayModel) |
49 | | - }) || [] |
50 | | - |
51 | | - this.models = standardisedModels.concat(standardisedModelsList, standardisedGatewayModels) |
52 | | - } |
53 | | - |
54 | | - async addModelsToOpenAPI() { |
55 | | - for (const model of this.models) { |
56 | | - const modelName = model.name |
57 | | - const modelSchema = model.schema |
58 | | - |
59 | | - const dereferencedSchema = await this.__dereferenceSchema(modelSchema) |
60 | | - .catch(err => { |
61 | | - if(err.errors) { |
62 | | - for (const error of err?.errors) { |
63 | | - if (error.message.includes('HTTP ERROR')) { |
64 | | - throw err |
65 | | - } |
66 | | - } |
67 | | - } |
68 | | - return modelSchema |
69 | | - }) |
70 | | - |
71 | | - const convertedSchemas = SchemaConvertor.convert(dereferencedSchema, modelName) |
72 | | - |
73 | | - for (const [schemaName, schemaValue] of Object.entries(convertedSchemas.schemas)) { |
74 | | - if (schemaName === modelName) { |
75 | | - this.modelReferences[schemaName] = `#/components/schemas/${modelName}` |
76 | | - } |
77 | | - |
78 | | - this.__addToComponents('schemas', schemaValue, schemaName) |
79 | | - } |
| 76 | + return modelSchema; |
| 77 | + }); |
| 78 | + |
| 79 | + const convertedSchemas = SchemaConvertor.convert( |
| 80 | + dereferencedSchema, |
| 81 | + modelName |
| 82 | + ); |
| 83 | + |
| 84 | + if ( |
| 85 | + typeof convertedSchemas.schemas === "object" && |
| 86 | + !Array.isArray(convertedSchemas.schemas) && |
| 87 | + convertedSchemas.schemas !== null |
| 88 | + ) { |
| 89 | + for (const [schemaName, schemaValue] of Object.entries( |
| 90 | + convertedSchemas.schemas |
| 91 | + )) { |
| 92 | + if (schemaName === modelName) { |
| 93 | + this.modelReferences[ |
| 94 | + schemaName |
| 95 | + ] = `#/components/schemas/${modelName}`; |
| 96 | + } |
| 97 | + |
| 98 | + this.__addToComponents("schemas", schemaValue, schemaName); |
80 | 99 | } |
| 100 | + } else { |
| 101 | + throw new Error( |
| 102 | + `There was an error converting the ${ |
| 103 | + model.name |
| 104 | + } schema. Model received looks like: \n\n${JSON.stringify(model)}` |
| 105 | + ); |
| 106 | + } |
81 | 107 | } |
| 108 | + } |
82 | 109 |
|
83 | | - async createSchema(name, schema) { |
84 | | - let originalName = name; |
85 | | - let finalName = name; |
86 | | - |
87 | | - if (this.modelReferences[name] && schema === undefined) { |
88 | | - return this.modelReferences[name] |
89 | | - } |
90 | | - |
91 | | - const dereferencedSchema = await this.__dereferenceSchema(schema) |
92 | | - .catch(err => { |
93 | | - throw err |
94 | | - }) |
95 | | - |
96 | | - const convertedSchemas = SchemaConvertor.convert(dereferencedSchema, name) |
97 | | - |
98 | | - for (const [schemaName, schemaValue] of Object.entries(convertedSchemas.schemas)) { |
99 | | - if (this.__existsInComponents(schemaName)) { |
100 | | - if (this.__isTheSameSchema(schemaValue, schemaName) === false) { |
101 | | - if (schemaName === originalName) { |
102 | | - finalName = `${schemaName}-${uuid()}` |
103 | | - this.__addToComponents('schemas', schemaValue, finalName) |
104 | | - } else { |
105 | | - this.__addToComponents('schemas', schemaValue, schemaName) |
106 | | - } |
107 | | - } |
108 | | - } else { |
109 | | - this.__addToComponents('schemas', schemaValue, schemaName) |
110 | | - } |
111 | | - } |
| 110 | + async createSchema(name, schema) { |
| 111 | + let originalName = name; |
| 112 | + let finalName = name; |
112 | 113 |
|
113 | | - return `#/components/schemas/${finalName}` |
| 114 | + if (this.modelReferences[name] && schema === undefined) { |
| 115 | + return this.modelReferences[name]; |
114 | 116 | } |
115 | 117 |
|
116 | | - async __dereferenceSchema(schema) { |
117 | | - const bundledSchema = await $RefParser.bundle(schema, this.refParserOptions) |
118 | | - .catch(err => { |
119 | | - throw err |
120 | | - }) |
121 | | - |
122 | | - let deReferencedSchema = await $RefParser.dereference(bundledSchema, this.refParserOptions) |
123 | | - .catch(err => { |
124 | | - throw err |
125 | | - }) |
126 | | - |
127 | | - // deal with schemas that have been de-referenced poorly: naive |
128 | | - if (deReferencedSchema?.$ref === '#') { |
129 | | - const oldRef = bundledSchema.$ref |
130 | | - const path = oldRef.split('/') |
131 | | - |
132 | | - const pathTitle = path[path.length - 1] |
133 | | - const referencedProperties = deReferencedSchema.definitions[pathTitle] |
134 | | - |
135 | | - Object.assign(deReferencedSchema, { ...referencedProperties }) |
136 | | - |
137 | | - delete deReferencedSchema.$ref |
138 | | - deReferencedSchema = await this.__dereferenceSchema(deReferencedSchema) |
139 | | - .catch((err) => { |
140 | | - throw err |
141 | | - }) |
| 118 | + const dereferencedSchema = await this.__dereferenceSchema(schema).catch( |
| 119 | + (err) => { |
| 120 | + throw err; |
| 121 | + } |
| 122 | + ); |
| 123 | + |
| 124 | + const convertedSchemas = SchemaConvertor.convert(dereferencedSchema, name); |
| 125 | + |
| 126 | + for (const [schemaName, schemaValue] of Object.entries( |
| 127 | + convertedSchemas.schemas |
| 128 | + )) { |
| 129 | + if (this.__existsInComponents(schemaName)) { |
| 130 | + if (this.__isTheSameSchema(schemaValue, schemaName) === false) { |
| 131 | + if (schemaName === originalName) { |
| 132 | + finalName = `${schemaName}-${uuid()}`; |
| 133 | + this.__addToComponents("schemas", schemaValue, finalName); |
| 134 | + } else { |
| 135 | + this.__addToComponents("schemas", schemaValue, schemaName); |
| 136 | + } |
142 | 137 | } |
143 | | - |
144 | | - return deReferencedSchema |
145 | | - } |
146 | | - |
147 | | - /** |
148 | | - * @function existsInComponents |
149 | | - * @param {string} name - The name of the Schema |
150 | | - * @returns {boolean} Whether it exists in components already |
151 | | - */ |
152 | | - __existsInComponents(name) { |
153 | | - return Boolean(this.openAPI?.components?.schemas?.[name]) |
| 138 | + } else { |
| 139 | + this.__addToComponents("schemas", schemaValue, schemaName); |
| 140 | + } |
154 | 141 | } |
155 | 142 |
|
156 | | - /** |
157 | | - * @function isTheSameSchema |
158 | | - * @param {object} schema - The schema value |
159 | | - * @param {string} otherSchemaName - The name of the schema |
160 | | - * @returns {boolean} Whether the schema provided is the same one as in components already |
161 | | - */ |
162 | | - __isTheSameSchema(schema, otherSchemaName) { |
163 | | - return isEqual(schema, this.openAPI.components.schemas[otherSchemaName]) |
| 143 | + return `#/components/schemas/${finalName}`; |
| 144 | + } |
| 145 | + |
| 146 | + async __dereferenceSchema(schema) { |
| 147 | + const bundledSchema = await $RefParser |
| 148 | + .bundle(schema, this.refParserOptions) |
| 149 | + .catch((err) => { |
| 150 | + throw err; |
| 151 | + }); |
| 152 | + |
| 153 | + let deReferencedSchema = await $RefParser |
| 154 | + .dereference(bundledSchema, this.refParserOptions) |
| 155 | + .catch((err) => { |
| 156 | + throw err; |
| 157 | + }); |
| 158 | + |
| 159 | + // deal with schemas that have been de-referenced poorly: naive |
| 160 | + if (deReferencedSchema?.$ref === "#") { |
| 161 | + const oldRef = bundledSchema.$ref; |
| 162 | + const path = oldRef.split("/"); |
| 163 | + |
| 164 | + const pathTitle = path[path.length - 1]; |
| 165 | + const referencedProperties = deReferencedSchema.definitions[pathTitle]; |
| 166 | + |
| 167 | + Object.assign(deReferencedSchema, { ...referencedProperties }); |
| 168 | + |
| 169 | + delete deReferencedSchema.$ref; |
| 170 | + deReferencedSchema = await this.__dereferenceSchema( |
| 171 | + deReferencedSchema |
| 172 | + ).catch((err) => { |
| 173 | + throw err; |
| 174 | + }); |
164 | 175 | } |
165 | 176 |
|
166 | | - /** |
167 | | - * @function addToComponents |
168 | | - * @param {string} type - The component type |
169 | | - * @param {object} schema - The schema |
170 | | - * @param {string} name - The name of the schema |
171 | | - */ |
172 | | - __addToComponents(type, schema, name) { |
173 | | - const schemaObj = { |
174 | | - [name]: schema |
175 | | - } |
176 | | - |
177 | | - if (this.openAPI?.components) { |
178 | | - if (this.openAPI.components[type]) { |
179 | | - Object.assign(this.openAPI.components[type], schemaObj) |
180 | | - } else { |
181 | | - Object.assign(this.openAPI.components, { [type]: schemaObj }) |
182 | | - } |
183 | | - } else { |
184 | | - const components = { |
185 | | - components: { |
186 | | - [type]: schemaObj |
187 | | - } |
188 | | - } |
189 | | - |
190 | | - Object.assign(this.openAPI, components) |
191 | | - } |
| 177 | + return deReferencedSchema; |
| 178 | + } |
| 179 | + |
| 180 | + /** |
| 181 | + * @function existsInComponents |
| 182 | + * @param {string} name - The name of the Schema |
| 183 | + * @returns {boolean} Whether it exists in components already |
| 184 | + */ |
| 185 | + __existsInComponents(name) { |
| 186 | + return Boolean(this.openAPI?.components?.schemas?.[name]); |
| 187 | + } |
| 188 | + |
| 189 | + /** |
| 190 | + * @function isTheSameSchema |
| 191 | + * @param {object} schema - The schema value |
| 192 | + * @param {string} otherSchemaName - The name of the schema |
| 193 | + * @returns {boolean} Whether the schema provided is the same one as in components already |
| 194 | + */ |
| 195 | + __isTheSameSchema(schema, otherSchemaName) { |
| 196 | + return isEqual(schema, this.openAPI.components.schemas[otherSchemaName]); |
| 197 | + } |
| 198 | + |
| 199 | + /** |
| 200 | + * @function addToComponents |
| 201 | + * @param {string} type - The component type |
| 202 | + * @param {object} schema - The schema |
| 203 | + * @param {string} name - The name of the schema |
| 204 | + */ |
| 205 | + __addToComponents(type, schema, name) { |
| 206 | + const schemaObj = { |
| 207 | + [name]: schema, |
| 208 | + }; |
| 209 | + |
| 210 | + if (this.openAPI?.components) { |
| 211 | + if (this.openAPI.components[type]) { |
| 212 | + Object.assign(this.openAPI.components[type], schemaObj); |
| 213 | + } else { |
| 214 | + Object.assign(this.openAPI.components, { [type]: schemaObj }); |
| 215 | + } |
| 216 | + } else { |
| 217 | + const components = { |
| 218 | + components: { |
| 219 | + [type]: schemaObj, |
| 220 | + }, |
| 221 | + }; |
| 222 | + |
| 223 | + Object.assign(this.openAPI, components); |
192 | 224 | } |
| 225 | + } |
193 | 226 | } |
194 | 227 |
|
195 | 228 | module.exports = SchemaHandler; |
0 commit comments