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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "serverless-openapi-documenter",
3
- "version": "0.0.53",
3
+ "version": "0.0.60",
4
4
  "description": "Generate OpenAPI v3 documentation and Postman Collections from your Serverless Config",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -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
- const SchemaConvertor = require('json-schema-for-openapi')
8
- const $RefParser = require("@apidevtools/json-schema-ref-parser")
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
- obj.headers = corsHeaders
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.schemaCreator(headers[header].schema, header)
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.serverless.service.custom.documentation.models) {
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.schemaCreator(schema, mediaTypeDocumentation.name)
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.schemaCreator(param.schema, param.name)
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,17 @@
1
+ {
2
+ "models": [
3
+ {
4
+ "name": "ErrorResponse",
5
+ "description": "The Error Response",
6
+ "contentType": "application/json",
7
+ "schema": {
8
+ "type": "object",
9
+ "properties": {
10
+ "error": {
11
+ "type": "string"
12
+ }
13
+ }
14
+ }
15
+ }
16
+ ]
17
+ }
@@ -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,17 @@
1
+ {
2
+ "modelsList": [
3
+ {
4
+ "name": "ErrorResponse",
5
+ "description": "The Error Response",
6
+ "contentType": "application/json",
7
+ "schema": {
8
+ "type": "object",
9
+ "properties": {
10
+ "error": {
11
+ "type": "string"
12
+ }
13
+ }
14
+ }
15
+ }
16
+ ]
17
+ }
@@ -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
+ }