serverless-openapi-documenter 0.0.53 → 0.0.61

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/README.md CHANGED
@@ -389,6 +389,23 @@ custom:
389
389
 
390
390
  `&ErrorItem` in the above example creates a node anchor (&ErrorItem) to the `ErrorResponse` schema which then can be used in the `PutDocumentResponse` schema via the reference (*ErrorItem). The node anchor needs to be declared first before it can be used elsewhere via the reference, swapping the above example around would result in an error.
391
391
 
392
+ ##### ModelsList - Backwards compatibility
393
+
394
+ It was brought to my attention that an older plugin version allowed the use of `modelsList`. As of 0.0.60, you can continue to use `modelsList` as well as using `models`, however `modelsList` now has to be nested within the `documentation` section. You can write `modelsList` the same way as any of the two styles for [Models](#Models).
395
+
396
+ ```
397
+ custom:
398
+ documentation:
399
+ ...
400
+ modelsList:
401
+ - name: "ErrorResponse"
402
+ description: "This is an error"
403
+ content:
404
+ application/json:
405
+ schema:
406
+ type: string
407
+ ```
408
+
392
409
 
393
410
  #### Functions
394
411
 
@@ -605,6 +622,46 @@ functions:
605
622
  - {}
606
623
  ```
607
624
 
625
+ ##### private
626
+
627
+ If you use the [private](https://www.serverless.com/framework/docs/providers/aws/events/apigateway#setting-api-keys-for-your-rest-api) property on your event:
628
+
629
+ ```yml
630
+ functions:
631
+ getData:
632
+ events:
633
+ - http:
634
+ path: /
635
+ method: get
636
+ private: true
637
+ ```
638
+
639
+ It will automatically setup an apiKey security scheme of `x-api-key` attached to that method. You don't need to add this to the [Security Scheme](#securityschemes) in the main documentation. If you have already added a Security Scheme of an `apiKey` with a name of `x-api-key`, it will associate with that key.
640
+
641
+ ```yml
642
+ custom:
643
+ documentation:
644
+ securitySchemes:
645
+ my_api_key:
646
+ type: apiKey
647
+ name: x-api-key
648
+ in: header
649
+ security:
650
+ - my_api_key: []
651
+
652
+ functions:
653
+ getData:
654
+ events:
655
+ - http:
656
+ path: /
657
+ method: get
658
+ private: true
659
+ documentation:
660
+ ...
661
+ ```
662
+
663
+ Will set the Security Scheme to `my_api_key` for that operation.
664
+
608
665
  #### `requestModels`
609
666
 
610
667
  The `requestModels` property allows you to define models for the HTTP Request of the function event. You can define a different model for each different `Content-Type`. You can define a reference to the relevant request model named in the `models` section of your configuration (see [Defining Models](#models) section).
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.61",
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
 
@@ -303,6 +312,29 @@ class DefinitionGenerator {
303
312
  obj.security = documentation.security
304
313
  }
305
314
 
315
+ if (this.currentEvent?.private && this.currentEvent.private === true) {
316
+ let apiKeyName = 'x-api-key'
317
+ let hasXAPIKey = false
318
+ if (this.openAPI?.components?.[this.componentTypes.securitySchemes]) {
319
+ for (const [schemeName, schemeValue] of Object.entries(this.openAPI.components[this.componentTypes.securitySchemes])) {
320
+ if (schemeValue.type === 'apiKey' && schemeValue.name === 'x-api-key') {
321
+ apiKeyName = schemeName
322
+ hasXAPIKey = true
323
+ }
324
+ }
325
+ }
326
+
327
+ if (hasXAPIKey === false) {
328
+ this.createSecuritySchemes({[apiKeyName]: {type: 'apiKey', name: apiKeyName, in: 'header'}})
329
+ }
330
+
331
+ if (obj.security) {
332
+ obj.security.push({[apiKeyName]: []})
333
+ } else {
334
+ obj.security = [{[apiKeyName]: []}]
335
+ }
336
+ }
337
+
306
338
  if (Object.keys(documentation).includes('deprecated'))
307
339
  obj.deprecated = documentation.deprecated
308
340
 
@@ -360,7 +392,8 @@ class DefinitionGenerator {
360
392
  }
361
393
  }
362
394
  } else {
363
- obj.headers = corsHeaders
395
+ if (Object.keys(corsHeaders).length)
396
+ obj.headers = corsHeaders
364
397
  }
365
398
 
366
399
  Object.assign(responses, { [response.statusCode]: obj })
@@ -414,7 +447,7 @@ class DefinitionGenerator {
414
447
  newHeader.description = headers[header].description || ''
415
448
 
416
449
  if (headers[header].schema) {
417
- const schemaRef = await this.schemaCreator(headers[header].schema, header)
450
+ const schemaRef = await this.schemaHandler.createSchema(header, headers[header].schema)
418
451
  .catch(err => {
419
452
  throw err
420
453
  })
@@ -445,7 +478,7 @@ class DefinitionGenerator {
445
478
 
446
479
  async createMediaTypeObject(models, type) {
447
480
  const mediaTypeObj = {}
448
- for (const mediaTypeDocumentation of this.serverless.service.custom.documentation.models) {
481
+ for (const mediaTypeDocumentation of this.schemaHandler.models) {
449
482
  if (models === undefined || models === null) {
450
483
  throw new Error(`${this.currentFunctionName} is missing a Response Model for statusCode ${this.currentStatusCode}`)
451
484
  }
@@ -477,10 +510,11 @@ class DefinitionGenerator {
477
510
  schema = mediaTypeDocumentation.schema
478
511
  }
479
512
 
480
- const schemaRef = await this.schemaCreator(schema, mediaTypeDocumentation.name)
513
+ const schemaRef = await this.schemaHandler.createSchema(mediaTypeDocumentation.name)
481
514
  .catch(err => {
482
515
  throw err
483
516
  })
517
+
484
518
  obj.schema = {
485
519
  $ref: schemaRef
486
520
  }
@@ -525,7 +559,7 @@ class DefinitionGenerator {
525
559
  obj.examples = this.createExamples(param.examples)
526
560
 
527
561
  if (param.schema) {
528
- const schemaRef = await this.schemaCreator(param.schema, param.name)
562
+ const schemaRef = await this.schemaHandler.createSchema(param.name, param.schema)
529
563
  .catch(err => {
530
564
  throw err
531
565
  })
@@ -539,76 +573,6 @@ class DefinitionGenerator {
539
573
  return params;
540
574
  }
541
575
 
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
576
  addToComponents(type, schema, name) {
613
577
  const schemaObj = {
614
578
  [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
+ }