serverless-openapi-documenter 0.0.52 → 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.52",
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
 
@@ -79,7 +88,7 @@ class DefinitionGenerator {
79
88
 
80
89
  if (this.serverless.service.custom.documentation.servers) {
81
90
  const servers = this.createServers(this.serverless.service.custom.documentation.servers)
82
- Object.assign(this.openAPI, {servers: servers})
91
+ Object.assign(this.openAPI, { servers: servers })
83
92
  }
84
93
 
85
94
  if (this.serverless.service.custom.documentation.tags) {
@@ -88,7 +97,7 @@ class DefinitionGenerator {
88
97
 
89
98
  if (this.serverless.service.custom.documentation.externalDocumentation) {
90
99
  const extDoc = this.createExternalDocumentation(this.serverless.service.custom.documentation.externalDocumentation)
91
- Object.assign(this.openAPI, {externalDocs: extDoc})
100
+ Object.assign(this.openAPI, { externalDocs: extDoc })
92
101
  }
93
102
  }
94
103
 
@@ -113,7 +122,7 @@ class DefinitionGenerator {
113
122
  contactObj.url = documentation.contact.url
114
123
 
115
124
  contactObj.email = documentation.contact.email || ''
116
- Object.assign(info, {contact: contactObj})
125
+ Object.assign(info, { contact: contactObj })
117
126
  }
118
127
 
119
128
  if (documentation.license && documentation.license.name) {
@@ -123,16 +132,16 @@ class DefinitionGenerator {
123
132
  if (documentation.license.url)
124
133
  licenseObj.url = documentation.license.url || ''
125
134
 
126
- Object.assign(info, {license: licenseObj})
135
+ Object.assign(info, { license: licenseObj })
127
136
  }
128
137
 
129
138
  for (const key of Object.keys(documentation)) {
130
139
  if (/^[x\-]/i.test(key)) {
131
- Object.assign(info, {[key]: documentation[key]})
140
+ Object.assign(info, { [key]: documentation[key] })
132
141
  }
133
142
  }
134
143
 
135
- Object.assign(this.openAPI, {info})
144
+ Object.assign(this.openAPI, { info })
136
145
  }
137
146
 
138
147
  async createPaths() {
@@ -173,18 +182,18 @@ class DefinitionGenerator {
173
182
  let slashPath = (event?.http?.path || event.httpApi?.path) ?? '/'
174
183
  const pathStart = new RegExp(/^\//, 'g')
175
184
  if (pathStart.test(slashPath) === false) {
176
- slashPath = `/${(event?.http?.path||event.httpApi?.path)?? ''}`
185
+ slashPath = `/${(event?.http?.path || event.httpApi?.path) ?? ''}`
177
186
  }
178
187
 
179
188
  if (paths[slashPath]) {
180
189
  Object.assign(paths[slashPath], path);
181
190
  } else {
182
- Object.assign(paths, {[slashPath]: path});
191
+ Object.assign(paths, { [slashPath]: path });
183
192
  }
184
193
  }
185
194
  }
186
195
  }
187
- Object.assign(this.openAPI, {paths})
196
+ Object.assign(this.openAPI, { paths })
188
197
  }
189
198
 
190
199
  createServers(servers) {
@@ -227,7 +236,7 @@ class DefinitionGenerator {
227
236
  }
228
237
 
229
238
  createExternalDocumentation(docs) {
230
- return {...docs}
239
+ return { ...docs }
231
240
  // const documentation = this.serverless.service.custom.documentation
232
241
  // if (documentation.externalDocumentation) {
233
242
  // // Object.assign(this.openAPI, {externalDocs: {...documentation.externalDocumentation}})
@@ -251,7 +260,7 @@ class DefinitionGenerator {
251
260
  }
252
261
  tags.push(obj)
253
262
  }
254
- Object.assign(this.openAPI, {tags: tags})
263
+ Object.assign(this.openAPI, { tags: tags })
255
264
  }
256
265
 
257
266
  async createOperationObject(method, documentation, name = uuid()) {
@@ -323,7 +332,7 @@ class DefinitionGenerator {
323
332
  obj.servers = servers
324
333
  }
325
334
 
326
- return {[method.toLowerCase()]: obj}
335
+ return { [method.toLowerCase()]: obj }
327
336
  }
328
337
 
329
338
  async createResponses(documentation) {
@@ -360,10 +369,11 @@ 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
- Object.assign(responses,{[response.statusCode]: obj})
376
+ Object.assign(responses, { [response.statusCode]: obj })
367
377
  }
368
378
 
369
379
  return responses
@@ -380,7 +390,7 @@ class DefinitionGenerator {
380
390
  const newHeaders = {}
381
391
  for (const key of Object.keys(this.DEFAULT_CORS_HEADERS)) {
382
392
  if (key === 'Access-Control-Allow-Credentials' &&
383
- this.currentEvent.cors.allowCredentials === undefined || this.currentEvent.cors?.allowCredentials === false) {
393
+ (this.currentEvent.cors.allowCredentials === undefined || this.currentEvent.cors?.allowCredentials === false)) {
384
394
  continue
385
395
  }
386
396
 
@@ -394,7 +404,7 @@ class DefinitionGenerator {
394
404
  }
395
405
  }
396
406
 
397
- Object.assign(newHeaders, {[key]: obj})
407
+ Object.assign(newHeaders, { [key]: obj })
398
408
  }
399
409
 
400
410
  headers = await this.createResponseHeaders(newHeaders)
@@ -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
  })
@@ -423,7 +433,7 @@ class DefinitionGenerator {
423
433
  }
424
434
  }
425
435
 
426
- Object.assign(obj, {[header]: newHeader})
436
+ Object.assign(obj, { [header]: newHeader })
427
437
  }
428
438
 
429
439
  return obj
@@ -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,15 +487,16 @@ 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
  }
487
498
 
488
- Object.assign(mediaTypeObj, {[contentKey]: obj})
499
+ Object.assign(mediaTypeObj, { [contentKey]: obj })
489
500
  }
490
501
  }
491
502
  return mediaTypeObj
@@ -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
@@ -618,7 +559,7 @@ class DefinitionGenerator {
618
559
  if (this.openAPI.components[type]) {
619
560
  Object.assign(this.openAPI.components[type], schemaObj)
620
561
  } else {
621
- Object.assign(this.openAPI.components, {[type]: schemaObj})
562
+ Object.assign(this.openAPI.components, { [type]: schemaObj })
622
563
  }
623
564
  } else {
624
565
  const components = {
@@ -639,30 +580,30 @@ class DefinitionGenerator {
639
580
  if (securityScheme.description)
640
581
  schema.description = securityScheme.description
641
582
 
642
- switch(securityScheme.type.toLowerCase()) {
583
+ switch (securityScheme.type.toLowerCase()) {
643
584
  case 'apikey':
644
585
  const apiKeyScheme = this.createAPIKeyScheme(securityScheme)
645
586
  schema.type = 'apiKey'
646
587
  Object.assign(schema, apiKeyScheme)
647
- break;
588
+ break;
648
589
 
649
590
  case 'http':
650
591
  const HTTPScheme = this.createHTTPScheme(securityScheme)
651
592
  schema.type = 'http'
652
593
  Object.assign(schema, HTTPScheme)
653
- break;
594
+ break;
654
595
 
655
596
  case 'openidconnect':
656
597
  const openIdConnectScheme = this.createOpenIDConnectScheme(securityScheme)
657
598
  schema.type = 'openIdConnect'
658
599
  Object.assign(schema, openIdConnectScheme)
659
- break;
600
+ break;
660
601
 
661
602
  case 'oauth2':
662
603
  const oAuth2Scheme = this.createOAuth2Scheme(securityScheme)
663
604
  schema.type = 'oauth2'
664
605
  Object.assign(schema, oAuth2Scheme)
665
- break;
606
+ break;
666
607
  }
667
608
 
668
609
  this.addToComponents(this.componentTypes.securitySchemes, schema, scheme)
@@ -712,7 +653,7 @@ class DefinitionGenerator {
712
653
  const schema = {}
713
654
  if (securitySchema.flows) {
714
655
  const flows = this.createOAuthFlows(securitySchema.flows)
715
- Object.assign(schema, {flows: flows})
656
+ Object.assign(schema, { flows: flows })
716
657
  } else
717
658
  throw new Error('Security Scheme for "oauth2" requires flows')
718
659
 
@@ -744,7 +685,7 @@ class DefinitionGenerator {
744
685
  else
745
686
  throw new Error(`oAuth2 ${flow} flow requires scopes`)
746
687
 
747
- Object.assign(obj, {[flow]: schema})
688
+ Object.assign(obj, { [flow]: schema })
748
689
  }
749
690
  return obj
750
691
  }
@@ -752,8 +693,8 @@ class DefinitionGenerator {
752
693
  createExamples(examples) {
753
694
  const examplesObj = {}
754
695
 
755
- for(const example of examples) {
756
- Object.assign(examplesObj, {[example.name]: example})
696
+ for (const example of examples) {
697
+ Object.assign(examplesObj, { [example.name]: example })
757
698
  delete examplesObj[example.name].name
758
699
  }
759
700
 
@@ -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
+ }