serverless-openapi-documenter 0.0.53 → 0.0.60
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/definitionGenerator.js +19 -78
- package/src/schemaHandler.js +189 -0
- package/test/models/models/models-alt.json +17 -0
- package/test/models/models/models.json +20 -0
- package/test/models/models/modelsList-alt.json +17 -0
- package/test/models/models/modelsList.json +20 -0
- package/test/unit/definitionGenerator.spec.js +25 -502
- package/test/unit/schemaHandler.spec.js +700 -0
package/package.json
CHANGED
|
@@ -4,9 +4,8 @@ const path = require('path')
|
|
|
4
4
|
|
|
5
5
|
const { v4: uuid } = require('uuid')
|
|
6
6
|
const validator = require('oas-validator');
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
const isEqual = require('lodash.isequal')
|
|
7
|
+
|
|
8
|
+
const SchemaHandler = require('./schemaHandler')
|
|
10
9
|
|
|
11
10
|
class DefinitionGenerator {
|
|
12
11
|
constructor(serverless, options = {}) {
|
|
@@ -25,8 +24,13 @@ class DefinitionGenerator {
|
|
|
25
24
|
|
|
26
25
|
this.openAPI = {
|
|
27
26
|
openapi: this.version,
|
|
27
|
+
components: {
|
|
28
|
+
schemas: {}
|
|
29
|
+
}
|
|
28
30
|
}
|
|
29
31
|
|
|
32
|
+
this.schemaHandler = new SchemaHandler(serverless, this.openAPI)
|
|
33
|
+
|
|
30
34
|
this.operationIds = []
|
|
31
35
|
this.schemaIDs = []
|
|
32
36
|
|
|
@@ -64,6 +68,11 @@ class DefinitionGenerator {
|
|
|
64
68
|
async parse() {
|
|
65
69
|
this.createInfo()
|
|
66
70
|
|
|
71
|
+
await this.schemaHandler.addModelsToOpenAPI()
|
|
72
|
+
.catch(err => {
|
|
73
|
+
throw err
|
|
74
|
+
})
|
|
75
|
+
|
|
67
76
|
if (this.serverless.service.custom.documentation.securitySchemes) {
|
|
68
77
|
this.createSecuritySchemes(this.serverless.service.custom.documentation.securitySchemes)
|
|
69
78
|
|
|
@@ -360,7 +369,8 @@ class DefinitionGenerator {
|
|
|
360
369
|
}
|
|
361
370
|
}
|
|
362
371
|
} else {
|
|
363
|
-
|
|
372
|
+
if (Object.keys(corsHeaders).length)
|
|
373
|
+
obj.headers = corsHeaders
|
|
364
374
|
}
|
|
365
375
|
|
|
366
376
|
Object.assign(responses, { [response.statusCode]: obj })
|
|
@@ -414,7 +424,7 @@ class DefinitionGenerator {
|
|
|
414
424
|
newHeader.description = headers[header].description || ''
|
|
415
425
|
|
|
416
426
|
if (headers[header].schema) {
|
|
417
|
-
const schemaRef = await this.
|
|
427
|
+
const schemaRef = await this.schemaHandler.createSchema(header, headers[header].schema)
|
|
418
428
|
.catch(err => {
|
|
419
429
|
throw err
|
|
420
430
|
})
|
|
@@ -445,7 +455,7 @@ class DefinitionGenerator {
|
|
|
445
455
|
|
|
446
456
|
async createMediaTypeObject(models, type) {
|
|
447
457
|
const mediaTypeObj = {}
|
|
448
|
-
for (const mediaTypeDocumentation of this.
|
|
458
|
+
for (const mediaTypeDocumentation of this.schemaHandler.models) {
|
|
449
459
|
if (models === undefined || models === null) {
|
|
450
460
|
throw new Error(`${this.currentFunctionName} is missing a Response Model for statusCode ${this.currentStatusCode}`)
|
|
451
461
|
}
|
|
@@ -477,10 +487,11 @@ class DefinitionGenerator {
|
|
|
477
487
|
schema = mediaTypeDocumentation.schema
|
|
478
488
|
}
|
|
479
489
|
|
|
480
|
-
const schemaRef = await this.
|
|
490
|
+
const schemaRef = await this.schemaHandler.createSchema(mediaTypeDocumentation.name)
|
|
481
491
|
.catch(err => {
|
|
482
492
|
throw err
|
|
483
493
|
})
|
|
494
|
+
|
|
484
495
|
obj.schema = {
|
|
485
496
|
$ref: schemaRef
|
|
486
497
|
}
|
|
@@ -525,7 +536,7 @@ class DefinitionGenerator {
|
|
|
525
536
|
obj.examples = this.createExamples(param.examples)
|
|
526
537
|
|
|
527
538
|
if (param.schema) {
|
|
528
|
-
const schemaRef = await this.
|
|
539
|
+
const schemaRef = await this.schemaHandler.createSchema(param.name, param.schema)
|
|
529
540
|
.catch(err => {
|
|
530
541
|
throw err
|
|
531
542
|
})
|
|
@@ -539,76 +550,6 @@ class DefinitionGenerator {
|
|
|
539
550
|
return params;
|
|
540
551
|
}
|
|
541
552
|
|
|
542
|
-
async dereferenceSchema(schema) {
|
|
543
|
-
let originalSchema = await $RefParser.bundle(schema, this.refParserOptions)
|
|
544
|
-
.catch(err => {
|
|
545
|
-
console.error(err)
|
|
546
|
-
throw err
|
|
547
|
-
})
|
|
548
|
-
|
|
549
|
-
let deReferencedSchema = await $RefParser.dereference(originalSchema, this.refParserOptions)
|
|
550
|
-
.catch(err => {
|
|
551
|
-
console.error(err)
|
|
552
|
-
throw err
|
|
553
|
-
})
|
|
554
|
-
|
|
555
|
-
// deal with schemas that have been de-referenced poorly: naive
|
|
556
|
-
if (deReferencedSchema?.$ref === '#') {
|
|
557
|
-
const oldRef = originalSchema.$ref
|
|
558
|
-
const path = oldRef.split('/')
|
|
559
|
-
|
|
560
|
-
const pathTitle = path[path.length - 1]
|
|
561
|
-
const referencedProperties = deReferencedSchema.definitions[pathTitle]
|
|
562
|
-
|
|
563
|
-
Object.assign(deReferencedSchema, { ...referencedProperties })
|
|
564
|
-
|
|
565
|
-
delete deReferencedSchema.$ref
|
|
566
|
-
deReferencedSchema = await this.dereferenceSchema(deReferencedSchema)
|
|
567
|
-
.catch((err) => {
|
|
568
|
-
throw err
|
|
569
|
-
})
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
return deReferencedSchema
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
async schemaCreator(schema, name) {
|
|
576
|
-
let schemaName = name
|
|
577
|
-
let finalName = schemaName
|
|
578
|
-
const dereferencedSchema = await this.dereferenceSchema(schema)
|
|
579
|
-
.catch(err => {
|
|
580
|
-
console.error(err)
|
|
581
|
-
throw err
|
|
582
|
-
})
|
|
583
|
-
|
|
584
|
-
const convertedSchemas = SchemaConvertor.convert(dereferencedSchema, schemaName)
|
|
585
|
-
|
|
586
|
-
for (const convertedSchemaName of Object.keys(convertedSchemas.schemas)) {
|
|
587
|
-
const convertedSchema = convertedSchemas.schemas[convertedSchemaName]
|
|
588
|
-
if (this.existsInComponents(convertedSchemaName)) {
|
|
589
|
-
if (this.isTheSameSchema(convertedSchema, convertedSchemaName) === false) {
|
|
590
|
-
if (convertedSchemaName === schemaName) {
|
|
591
|
-
finalName = `${schemaName}-${uuid()}`
|
|
592
|
-
this.addToComponents(this.componentTypes.schemas, convertedSchema, finalName)
|
|
593
|
-
} else
|
|
594
|
-
this.addToComponents(this.componentTypes.schemas, convertedSchema, convertedSchemaName)
|
|
595
|
-
}
|
|
596
|
-
} else {
|
|
597
|
-
this.addToComponents(this.componentTypes.schemas, convertedSchema, convertedSchemaName)
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
return `#/components/schemas/${finalName}`
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
existsInComponents(name) {
|
|
605
|
-
return Boolean(this.openAPI?.components?.schemas?.[name])
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
isTheSameSchema(schema, otherSchemaName) {
|
|
609
|
-
return isEqual(schema, this.openAPI.components.schemas[otherSchemaName])
|
|
610
|
-
}
|
|
611
|
-
|
|
612
553
|
addToComponents(type, schema, name) {
|
|
613
554
|
const schemaObj = {
|
|
614
555
|
[name]: schema
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const path = require('path')
|
|
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')
|
|
9
|
+
|
|
10
|
+
class SchemaHandler {
|
|
11
|
+
constructor(serverless, openAPI) {
|
|
12
|
+
this.documentation = serverless.service.custom.documentation
|
|
13
|
+
this.openAPI = openAPI
|
|
14
|
+
|
|
15
|
+
this.modelReferences = {}
|
|
16
|
+
|
|
17
|
+
this.__standardiseModels()
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
this.refParserOptions = require(path.resolve('options', 'ref-parser.js'))
|
|
21
|
+
} catch (err) {
|
|
22
|
+
this.refParserOptions = {}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Standardises the models to a specific format
|
|
28
|
+
*/
|
|
29
|
+
__standardiseModels() {
|
|
30
|
+
const standardModel = (model) => {
|
|
31
|
+
if (model.schema) {
|
|
32
|
+
return model
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const contentType = Object.keys(model.content)[0]
|
|
36
|
+
model.contentType = contentType
|
|
37
|
+
model.schema = model.content[contentType].schema
|
|
38
|
+
|
|
39
|
+
return model
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const standardisedModels = this.documentation?.models?.map(standardModel) || []
|
|
43
|
+
const standardisedModelsList = this.documentation?.modelsList?.map(standardModel) || []
|
|
44
|
+
|
|
45
|
+
this.models = standardisedModels.length ? standardisedModels.concat(standardisedModelsList) : standardisedModelsList
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async addModelsToOpenAPI() {
|
|
49
|
+
for (const model of this.models) {
|
|
50
|
+
const modelName = model.name
|
|
51
|
+
const modelSchema = model.schema
|
|
52
|
+
|
|
53
|
+
const dereferencedSchema = await this.__dereferenceSchema(modelSchema)
|
|
54
|
+
.catch(err => {
|
|
55
|
+
if(err.errors) {
|
|
56
|
+
for (const error of err?.errors) {
|
|
57
|
+
if (error.message.includes('HTTP ERROR')) {
|
|
58
|
+
throw err
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return modelSchema
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
const convertedSchemas = SchemaConvertor.convert(dereferencedSchema, modelName)
|
|
66
|
+
|
|
67
|
+
for (const [schemaName, schemaValue] of Object.entries(convertedSchemas.schemas)) {
|
|
68
|
+
if (schemaName === modelName) {
|
|
69
|
+
this.modelReferences[schemaName] = `#/components/schemas/${modelName}`
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
this.__addToComponents('schemas', schemaValue, schemaName)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async createSchema(name, schema) {
|
|
78
|
+
let originalName = name;
|
|
79
|
+
let finalName = name;
|
|
80
|
+
|
|
81
|
+
if (this.modelReferences[name] && schema === undefined) {
|
|
82
|
+
return this.modelReferences[name]
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const dereferencedSchema = await this.__dereferenceSchema(schema)
|
|
86
|
+
.catch(err => {
|
|
87
|
+
throw err
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
const convertedSchemas = SchemaConvertor.convert(dereferencedSchema, name)
|
|
91
|
+
|
|
92
|
+
for (const [schemaName, schemaValue] of Object.entries(convertedSchemas.schemas)) {
|
|
93
|
+
if (this.__existsInComponents(schemaName)) {
|
|
94
|
+
if (this.__isTheSameSchema(schemaValue, schemaName) === false) {
|
|
95
|
+
if (schemaName === originalName) {
|
|
96
|
+
finalName = `${schemaName}-${uuid()}`
|
|
97
|
+
this.__addToComponents('schemas', schemaValue, finalName)
|
|
98
|
+
} else {
|
|
99
|
+
this.__addToComponents('schemas', schemaValue, schemaName)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
} else {
|
|
103
|
+
this.__addToComponents('schemas', schemaValue, schemaName)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return `#/components/schemas/${finalName}`
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async __dereferenceSchema(schema) {
|
|
111
|
+
const bundledSchema = await $RefParser.bundle(schema, this.refParserOptions)
|
|
112
|
+
.catch(err => {
|
|
113
|
+
throw err
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
let deReferencedSchema = await $RefParser.dereference(bundledSchema, this.refParserOptions)
|
|
117
|
+
.catch(err => {
|
|
118
|
+
throw err
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
// deal with schemas that have been de-referenced poorly: naive
|
|
122
|
+
if (deReferencedSchema?.$ref === '#') {
|
|
123
|
+
const oldRef = bundledSchema.$ref
|
|
124
|
+
const path = oldRef.split('/')
|
|
125
|
+
|
|
126
|
+
const pathTitle = path[path.length - 1]
|
|
127
|
+
const referencedProperties = deReferencedSchema.definitions[pathTitle]
|
|
128
|
+
|
|
129
|
+
Object.assign(deReferencedSchema, { ...referencedProperties })
|
|
130
|
+
|
|
131
|
+
delete deReferencedSchema.$ref
|
|
132
|
+
deReferencedSchema = await this.__dereferenceSchema(deReferencedSchema)
|
|
133
|
+
.catch((err) => {
|
|
134
|
+
throw err
|
|
135
|
+
})
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return deReferencedSchema
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* @function existsInComponents
|
|
143
|
+
* @param {string} name - The name of the Schema
|
|
144
|
+
* @returns {boolean} Whether it exists in components already
|
|
145
|
+
*/
|
|
146
|
+
__existsInComponents(name) {
|
|
147
|
+
return Boolean(this.openAPI?.components?.schemas?.[name])
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* @function isTheSameSchema
|
|
152
|
+
* @param {object} schema - The schema value
|
|
153
|
+
* @param {string} otherSchemaName - The name of the schema
|
|
154
|
+
* @returns {boolean} Whether the schema provided is the same one as in components already
|
|
155
|
+
*/
|
|
156
|
+
__isTheSameSchema(schema, otherSchemaName) {
|
|
157
|
+
return isEqual(schema, this.openAPI.components.schemas[otherSchemaName])
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* @function addToComponents
|
|
162
|
+
* @param {string} type - The component type
|
|
163
|
+
* @param {object} schema - The schema
|
|
164
|
+
* @param {string} name - The name of the schema
|
|
165
|
+
*/
|
|
166
|
+
__addToComponents(type, schema, name) {
|
|
167
|
+
const schemaObj = {
|
|
168
|
+
[name]: schema
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (this.openAPI?.components) {
|
|
172
|
+
if (this.openAPI.components[type]) {
|
|
173
|
+
Object.assign(this.openAPI.components[type], schemaObj)
|
|
174
|
+
} else {
|
|
175
|
+
Object.assign(this.openAPI.components, { [type]: schemaObj })
|
|
176
|
+
}
|
|
177
|
+
} else {
|
|
178
|
+
const components = {
|
|
179
|
+
components: {
|
|
180
|
+
[type]: schemaObj
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
Object.assign(this.openAPI, components)
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
module.exports = SchemaHandler;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"models": [
|
|
3
|
+
{
|
|
4
|
+
"name": "ErrorResponse",
|
|
5
|
+
"description": "The Error Response",
|
|
6
|
+
"content": {
|
|
7
|
+
"application/json": {
|
|
8
|
+
"schema": {
|
|
9
|
+
"type": "object",
|
|
10
|
+
"properties": {
|
|
11
|
+
"error": {
|
|
12
|
+
"type": "string"
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"modelsList": [
|
|
3
|
+
{
|
|
4
|
+
"name": "ErrorResponse",
|
|
5
|
+
"description": "The Error Response",
|
|
6
|
+
"content": {
|
|
7
|
+
"application/json": {
|
|
8
|
+
"schema": {
|
|
9
|
+
"type": "object",
|
|
10
|
+
"properties": {
|
|
11
|
+
"error": {
|
|
12
|
+
"type": "string"
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
}
|