@superhero/http-server-using-oas 4.7.2
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/LICENCE +21 -0
- package/config.json +49 -0
- package/dispatcher/downstream/responses.js +46 -0
- package/dispatcher/options.js +201 -0
- package/dispatcher/upstream/parameters.js +35 -0
- package/dispatcher/upstream/request-bodies.js +28 -0
- package/index.js +135 -0
- package/index.test.js +279 -0
- package/package.json +43 -0
package/LICENCE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Erik Landvall
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/config.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"dependency":
|
|
3
|
+
{
|
|
4
|
+
"@superhero/http-server": true
|
|
5
|
+
},
|
|
6
|
+
"locator":
|
|
7
|
+
{
|
|
8
|
+
"@superhero/http-server-using-oas":
|
|
9
|
+
{
|
|
10
|
+
"uses":
|
|
11
|
+
[
|
|
12
|
+
"@superhero/oas",
|
|
13
|
+
"@superhero/http-server"
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
"@superhero/http-server-using-oas/dispatcher/options":
|
|
17
|
+
{
|
|
18
|
+
"path": "./dispatcher/options.js",
|
|
19
|
+
"uses": [ "@superhero/http-server-using-oas" ]
|
|
20
|
+
},
|
|
21
|
+
"@superhero/http-server-using-oas/dispatcher/upstream/parameters":
|
|
22
|
+
{
|
|
23
|
+
"path": "./dispatcher/upstream/parameters.js",
|
|
24
|
+
"uses": [ "@superhero/http-server-using-oas" ]
|
|
25
|
+
},
|
|
26
|
+
"@superhero/http-server-using-oas/dispatcher/upstream/request-bodies":
|
|
27
|
+
{
|
|
28
|
+
"path": "./dispatcher/upstream/request-bodies.js",
|
|
29
|
+
"uses": [ "@superhero/http-server-using-oas" ]
|
|
30
|
+
},
|
|
31
|
+
"@superhero/http-server-using-oas/dispatcher/downstream/responses":
|
|
32
|
+
{
|
|
33
|
+
"path": "./dispatcher/downstream/responses.js",
|
|
34
|
+
"uses": [ "@superhero/http-server-using-oas" ]
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"oas":
|
|
38
|
+
{
|
|
39
|
+
"components":
|
|
40
|
+
{
|
|
41
|
+
"headers" : {},
|
|
42
|
+
"parameters" : {},
|
|
43
|
+
"requestBodies" : {},
|
|
44
|
+
"responses" : {},
|
|
45
|
+
"schemas" : {}
|
|
46
|
+
},
|
|
47
|
+
"paths": {}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export function locate(locator)
|
|
2
|
+
{
|
|
3
|
+
const oas = locator('@superhero/oas')
|
|
4
|
+
return new ResponsesMiddleware(oas)
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export default class ResponsesMiddleware
|
|
8
|
+
{
|
|
9
|
+
constructor(oas)
|
|
10
|
+
{
|
|
11
|
+
this.oas = oas
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async dispatch(request, session)
|
|
15
|
+
{
|
|
16
|
+
await session.chain.next()
|
|
17
|
+
|
|
18
|
+
if(false === !!session.abortion.signal.aborted)
|
|
19
|
+
{
|
|
20
|
+
const
|
|
21
|
+
status = session.view.status,
|
|
22
|
+
responses = session.route.operation.responses
|
|
23
|
+
|
|
24
|
+
if(status in responses)
|
|
25
|
+
{
|
|
26
|
+
this.oas.responses.conform(responses[status], session.view)
|
|
27
|
+
}
|
|
28
|
+
else
|
|
29
|
+
{
|
|
30
|
+
const error = new Error(`Invalid status code: ${status}`)
|
|
31
|
+
error.code = 'E_OAS_INVALID_RESPONSE_STATUS'
|
|
32
|
+
error.cause = `The operation supports status codes: ${Object.keys(responses).join(', ')}`
|
|
33
|
+
throw error
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
onError(reason, request, session)
|
|
39
|
+
{
|
|
40
|
+
const error = new Error(`Invalid response for operation ${request.method} ${request.url}`)
|
|
41
|
+
error.code = 'E_OAS_INVALID_RESPONSE'
|
|
42
|
+
error.status = 400
|
|
43
|
+
error.cause = reason
|
|
44
|
+
session.abortion.abort(error)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
export function locate(locator)
|
|
2
|
+
{
|
|
3
|
+
const specification = locator.config.find('oas')
|
|
4
|
+
return new OptionsDispatcher(specification)
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export default class OptionsDispatcher
|
|
8
|
+
{
|
|
9
|
+
constructor(specification)
|
|
10
|
+
{
|
|
11
|
+
this.specification = specification
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
dispatch(request, session)
|
|
15
|
+
{
|
|
16
|
+
const [ validResource, ...validOperations ] = session.route.operation.operationId.split('#')
|
|
17
|
+
const
|
|
18
|
+
output = {},
|
|
19
|
+
components = {},
|
|
20
|
+
paths = {},
|
|
21
|
+
parameters = {},
|
|
22
|
+
requestBodies = {},
|
|
23
|
+
responses = {},
|
|
24
|
+
schemas = {},
|
|
25
|
+
depth = request.url.pathname.split('/').length
|
|
26
|
+
|
|
27
|
+
// Loop through all paths in the OpenAPI Specification and find the ones that
|
|
28
|
+
// match the defined operation, or the beginning of the request path if no operation is defined...
|
|
29
|
+
for(const path in this.specification.paths || {})
|
|
30
|
+
{
|
|
31
|
+
let valid = false
|
|
32
|
+
|
|
33
|
+
// validate the path depending on if the operation-id specify a specific operation, or not...
|
|
34
|
+
if(validResource)
|
|
35
|
+
{
|
|
36
|
+
valid = validResource === path
|
|
37
|
+
}
|
|
38
|
+
// validate the beginning of the request path if no specific operation is defined...
|
|
39
|
+
else
|
|
40
|
+
{
|
|
41
|
+
const
|
|
42
|
+
partial = path.split('/').slice(0, depth).join('/'),
|
|
43
|
+
regexp = partial.replace(/{[^}]+}/g, '([^/]*)')
|
|
44
|
+
|
|
45
|
+
valid = new RegExp(`^${regexp}$`).test(request.url.pathname)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Only include a scoped version of the specification
|
|
49
|
+
if(valid)
|
|
50
|
+
{
|
|
51
|
+
paths[path] = { ...this.specification.paths[path] }
|
|
52
|
+
|
|
53
|
+
// Operations
|
|
54
|
+
for(let method in paths[path])
|
|
55
|
+
{
|
|
56
|
+
const lowerCasedMethod = method.toLowerCase()
|
|
57
|
+
|
|
58
|
+
switch(lowerCasedMethod)
|
|
59
|
+
{
|
|
60
|
+
case 'get':
|
|
61
|
+
case 'put':
|
|
62
|
+
case 'post':
|
|
63
|
+
case 'delete':
|
|
64
|
+
case 'options':
|
|
65
|
+
case 'head':
|
|
66
|
+
case 'patch':
|
|
67
|
+
case 'trace':
|
|
68
|
+
{
|
|
69
|
+
if(validOperations.langth === 0 // if not specified, then not concidered restrictive...
|
|
70
|
+
|| validOperations.map(validMethod => validMethod.toLowerCase()).includes(lowerCasedMethod))
|
|
71
|
+
{
|
|
72
|
+
// break to proceed to process
|
|
73
|
+
break
|
|
74
|
+
}
|
|
75
|
+
else
|
|
76
|
+
{
|
|
77
|
+
// hide restricted methods
|
|
78
|
+
delete paths[path][method]
|
|
79
|
+
continue
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
default:
|
|
83
|
+
{
|
|
84
|
+
// ignore any non HTTP method
|
|
85
|
+
continue
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const operation = paths[path][method]
|
|
90
|
+
|
|
91
|
+
// Parameters
|
|
92
|
+
for(let parameter of operation.parameters || [])
|
|
93
|
+
{
|
|
94
|
+
parameter = this.#augment(parameters, parameter.$ref) || parameter
|
|
95
|
+
// Parameters - schemas
|
|
96
|
+
this.#augmentSchemasRecursively(schemas, parameter.schema)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Request Bodies
|
|
100
|
+
let
|
|
101
|
+
requestBody = operation.requestBody || {}
|
|
102
|
+
requestBody = this.#augment(requestBodies, requestBody.$ref) || requestBody
|
|
103
|
+
this.#augment(requestBodies, requestBody.$ref)
|
|
104
|
+
// Request Bodies - schemas
|
|
105
|
+
for(const contentType in requestBody.content || {})
|
|
106
|
+
{
|
|
107
|
+
this.#augmentSchemasRecursively(schemas, operation.requestBody.content[contentType].schema)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Responses
|
|
111
|
+
for(const status in operation.responses || {})
|
|
112
|
+
{
|
|
113
|
+
let
|
|
114
|
+
response = operation.responses[status] || {}
|
|
115
|
+
response = this.#augment(responses, response.$ref) || response
|
|
116
|
+
// Responses - schemas
|
|
117
|
+
for(const contentType in response.content || {})
|
|
118
|
+
{
|
|
119
|
+
this.#augmentSchemasRecursively(schemas, response.content[contentType].schema)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Aport with a 404 error if no paths were found in the specification
|
|
127
|
+
if(false === Object.keys(paths).length)
|
|
128
|
+
{
|
|
129
|
+
const error = new Error(`No endpoints found matching the requested path "${request.url.pathname}"`)
|
|
130
|
+
error.code = 'E_OAS_NO_ENDPOINTS_FOUND'
|
|
131
|
+
error.status = 404
|
|
132
|
+
return session.abortion.abort(error)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
output.openapi = this.specification.openapi
|
|
136
|
+
output.info = this.specification.info
|
|
137
|
+
output.paths = paths
|
|
138
|
+
|
|
139
|
+
if(Object.keys(parameters) .length) components.parameters = parameters
|
|
140
|
+
if(Object.keys(requestBodies) .length) components.requestBodies = requestBodies
|
|
141
|
+
if(Object.keys(responses) .length) components.responses = responses
|
|
142
|
+
if(Object.keys(schemas) .length) components.schemas = schemas
|
|
143
|
+
if(Object.keys(components) .length) output.components = components
|
|
144
|
+
|
|
145
|
+
this.view.body = output
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
#augment(component, ref)
|
|
149
|
+
{
|
|
150
|
+
// only augment if the reference is defined
|
|
151
|
+
if('string' !== typeof ref) return
|
|
152
|
+
|
|
153
|
+
const [ uri, pointer ] = ref.split('#')
|
|
154
|
+
|
|
155
|
+
// only augment local references
|
|
156
|
+
if(true === !!uri) return
|
|
157
|
+
|
|
158
|
+
// component name
|
|
159
|
+
const name = pointer.split('/').pop()
|
|
160
|
+
|
|
161
|
+
// avoid redundant traversals and augmentations
|
|
162
|
+
if(name in component) return
|
|
163
|
+
|
|
164
|
+
// augment the component with the branch at the traversed pointer in the specification
|
|
165
|
+
const traversePath = pointer.split('/').filter(Boolean)
|
|
166
|
+
return component[name] = traversePath.reduce((obj, key) => obj && obj[key], this.specification)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
#augmentSchemasRecursively(component, schema)
|
|
170
|
+
{
|
|
171
|
+
// avoid undefined schemas
|
|
172
|
+
if(false === !!schema) return
|
|
173
|
+
// loop through the schemas if it is an array
|
|
174
|
+
if(Array.isArray(schema)) return schema.forEach(schema => this.#augmentSchemasRecursively(component, schema))
|
|
175
|
+
// replace the schema with the augmented one, if the schema defined a reference
|
|
176
|
+
if(schema.$ref) schema = this.#augment(component, schema.$ref)
|
|
177
|
+
// avoid redundant processing
|
|
178
|
+
if(false === !!schema) return
|
|
179
|
+
|
|
180
|
+
this.#augmentSchemasRecursively(component, Object.values(schema.properties || {}))
|
|
181
|
+
this.#augmentSchemasRecursively(component, schema.additionalProperties)
|
|
182
|
+
this.#augmentSchemasRecursively(component, schema.propertyNames)
|
|
183
|
+
this.#augmentSchemasRecursively(component, schema.items)
|
|
184
|
+
this.#augmentSchemasRecursively(component, schema.allOf)
|
|
185
|
+
this.#augmentSchemasRecursively(component, schema.anyOf)
|
|
186
|
+
this.#augmentSchemasRecursively(component, schema.oneOf)
|
|
187
|
+
this.#augmentSchemasRecursively(component, schema.if)
|
|
188
|
+
this.#augmentSchemasRecursively(component, schema.not)
|
|
189
|
+
this.#augmentSchemasRecursively(component, schema.then)
|
|
190
|
+
this.#augmentSchemasRecursively(component, schema.else)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
onError(reason, _, session)
|
|
194
|
+
{
|
|
195
|
+
const error = new Error(`Server error occured while attempting to declare API options`)
|
|
196
|
+
error.code = 'E_OAS_INVALID_REQUEST_PARAMETERS'
|
|
197
|
+
error.cause = reason
|
|
198
|
+
error.status = 500
|
|
199
|
+
session.abortion.abort(error)
|
|
200
|
+
}
|
|
201
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export function locate(locator)
|
|
2
|
+
{
|
|
3
|
+
const oas = locator('@superhero/oas')
|
|
4
|
+
return new ParametersMiddleware(oas)
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export default class ParametersMiddleware
|
|
8
|
+
{
|
|
9
|
+
constructor(oas)
|
|
10
|
+
{
|
|
11
|
+
this.oas = oas
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
dispatch(request, session)
|
|
15
|
+
{
|
|
16
|
+
const parameters = session.route.operation.parameters
|
|
17
|
+
|
|
18
|
+
if(parameters)
|
|
19
|
+
{
|
|
20
|
+
for(const parameter of parameters)
|
|
21
|
+
{
|
|
22
|
+
this.oas.parameters.conform(parameter, request)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
onError(reason, request, session)
|
|
28
|
+
{
|
|
29
|
+
const error = new Error(`Invalid request-parameters for operation ${request.method} ${request.url}`)
|
|
30
|
+
error.code = 'E_OAS_INVALID_REQUEST_PARAMETERS'
|
|
31
|
+
error.cause = reason
|
|
32
|
+
error.status = 400
|
|
33
|
+
session.abortion.abort(error)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export function locate(locator)
|
|
2
|
+
{
|
|
3
|
+
const oas = locator('@superhero/oas')
|
|
4
|
+
return new RequestBodiesMiddleware(oas)
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export default class RequestBodiesMiddleware
|
|
8
|
+
{
|
|
9
|
+
constructor(oas)
|
|
10
|
+
{
|
|
11
|
+
this.oas = oas
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
dispatch(request, session)
|
|
15
|
+
{
|
|
16
|
+
const requestBody = session.route.operation.requestBody
|
|
17
|
+
requestBody && this.oas.requestBodies.conform(requestBody, request)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
onError(reason, request, session)
|
|
21
|
+
{
|
|
22
|
+
const error = new Error(`Invalid request-body for operation ${request.method} ${request.url}`)
|
|
23
|
+
error.code = 'E_OAS_INVALID_REQUEST_BODY'
|
|
24
|
+
error.cause = reason
|
|
25
|
+
error.status = 400
|
|
26
|
+
session.abortion.abort(error)
|
|
27
|
+
}
|
|
28
|
+
}
|
package/index.js
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
export async function locate(locator)
|
|
2
|
+
{
|
|
3
|
+
const
|
|
4
|
+
server = locator.locate('@superhero/http-server'),
|
|
5
|
+
oas = locator.locate('@superhero/oas')
|
|
6
|
+
|
|
7
|
+
return new HttpServerUsingOas(server, oas)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A class that integrates the HTTP server and OAS (OpenAPI Specification) components.
|
|
12
|
+
* Use the service to set routes based off the configiured OAS specifications.
|
|
13
|
+
*/
|
|
14
|
+
export default class HttpServerUsingOas
|
|
15
|
+
{
|
|
16
|
+
/**
|
|
17
|
+
* @param {@superhero/http-server} server - The HTTP server instance.
|
|
18
|
+
* @param {@superhero/oas} oas - The OpenAPI Specification instance.
|
|
19
|
+
*/
|
|
20
|
+
constructor(server, oas)
|
|
21
|
+
{
|
|
22
|
+
this.server = server
|
|
23
|
+
this.oas = oas
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Set a route by mapping an OpenAPI operation to a dispatcher.
|
|
28
|
+
*
|
|
29
|
+
* @param {string} path - The operation to validate.
|
|
30
|
+
* @param {string} method - The HTTP method of the operation.
|
|
31
|
+
* @param {string} dispatcher - The dispatcher to handle the operation.
|
|
32
|
+
* @param {Array<string>} [middlewares=[]] - Optional middlewares to apply to the route.
|
|
33
|
+
*
|
|
34
|
+
* @throws {TypeError} - E_HTTP_SERVER_USING_OAS_SET_ROUTE_INVALID_PATH
|
|
35
|
+
* @throws {TypeError} - E_HTTP_SERVER_USING_OAS_SET_ROUTE_INVALID_METHOD
|
|
36
|
+
* @throws {TypeError} - E_HTTP_SERVER_USING_OAS_SET_ROUTE_INVALID_DISPATCHER
|
|
37
|
+
* @throws {Error} - E_HTTP_SERVER_USING_OAS_SET_ROUTE_INVALID_OPERATION
|
|
38
|
+
* @throws {Error} - E_HTTP_SERVER_USING_OAS_SET_ROUTE_INVALID_CONTENT_TYPE
|
|
39
|
+
*/
|
|
40
|
+
setOasRoute(path, method, dispatcher, middlewares = [])
|
|
41
|
+
{
|
|
42
|
+
if('string' !== typeof path)
|
|
43
|
+
{
|
|
44
|
+
const error = new TypeError(`The path must be a string, received ${typeof path}`)
|
|
45
|
+
error.code = 'E_HTTP_SERVER_USING_OAS_SET_ROUTE_INVALID_PATH'
|
|
46
|
+
throw error
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if('string' !== typeof method)
|
|
50
|
+
{
|
|
51
|
+
const error = new TypeError(`The method must be a string, received ${typeof method}`)
|
|
52
|
+
error.code = 'E_HTTP_SERVER_USING_OAS_SET_ROUTE_INVALID_METHOD'
|
|
53
|
+
throw error
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if(false === !!dispatcher)
|
|
57
|
+
{
|
|
58
|
+
const error = new TypeError(`The dispatcher must be set when setting an OAS route`)
|
|
59
|
+
error.code = 'E_HTTP_SERVER_USING_OAS_SET_ROUTE_INVALID_DISPATCHER'
|
|
60
|
+
throw error
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
method = method.toLowerCase()
|
|
64
|
+
|
|
65
|
+
const route = { dispatcher, conditions:[], middlewares:[] }
|
|
66
|
+
let operation
|
|
67
|
+
|
|
68
|
+
try
|
|
69
|
+
{
|
|
70
|
+
operation = this.oas.specification.paths[path][method]
|
|
71
|
+
this.oas.validateOperation(operation)
|
|
72
|
+
operation = this.oas.denormalizeOperation(operation)
|
|
73
|
+
}
|
|
74
|
+
catch(reason)
|
|
75
|
+
{
|
|
76
|
+
const error = new Error(`Invalid method "${method}" in path "${path}"`)
|
|
77
|
+
error.code = 'E_HTTP_SERVER_USING_OAS_SET_ROUTE_INVALID_OPERATION'
|
|
78
|
+
error.cause = reason
|
|
79
|
+
throw error
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
route['condition.method'] = method
|
|
83
|
+
route.conditions.push('@superhero/http-server/condition/upstream/method')
|
|
84
|
+
|
|
85
|
+
if(operation.requestBody?.content)
|
|
86
|
+
{
|
|
87
|
+
const supportedContentTypes = Object.keys(operation.requestBody.content)
|
|
88
|
+
route['condition.content-type'] = supportedContentTypes
|
|
89
|
+
|
|
90
|
+
for(const supportedContentType of supportedContentTypes)
|
|
91
|
+
{
|
|
92
|
+
route['content-type.' + supportedContentType] = this.requestBodyContentTypeRouteMap(supportedContentType)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
route.conditions.push('@superhero/http-server/condition/upstream/header/content-type')
|
|
96
|
+
route.middlewares.push('@superhero/http-server/dispatcher/upstream/header/content-type')
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
route.condition = path.replace(/\{([^}]+)\}/g, ':$1')
|
|
100
|
+
route.middlewares.push('@superhero/http-server-using-oas/dispatcher/upstream/parameters')
|
|
101
|
+
route.middlewares.push('@superhero/http-server-using-oas/dispatcher/upstream/request-bodies')
|
|
102
|
+
route.middlewares.push('@superhero/http-server-using-oas/dispatcher/downstream/responses')
|
|
103
|
+
route.middlewares.push(...[middlewares].flat())
|
|
104
|
+
|
|
105
|
+
Object.defineProperty(route, 'operation', { value: operation })
|
|
106
|
+
|
|
107
|
+
this.server.router.set(`${method} ${path}`, route)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Returns a middleware that can be used to resolve the content-type of the request-body.
|
|
112
|
+
*
|
|
113
|
+
* @param {string} supportedContentType - The content type to map.
|
|
114
|
+
* @returns {string} - The middleware path to handle the request body content-type.
|
|
115
|
+
*
|
|
116
|
+
* @throws {Error} - E_OAS_UNSUPPORTED_CONTENT_TYPE
|
|
117
|
+
*/
|
|
118
|
+
requestBodyContentTypeRouteMap(supportedContentType)
|
|
119
|
+
{
|
|
120
|
+
switch(supportedContentType)
|
|
121
|
+
{
|
|
122
|
+
case 'application/json':
|
|
123
|
+
{
|
|
124
|
+
return '@superhero/http-server/dispatcher/upstream/header/content-type/' + supportedContentType
|
|
125
|
+
}
|
|
126
|
+
default:
|
|
127
|
+
{
|
|
128
|
+
const error = new Error(`Unsupported content type "${supportedContentType}" in request body`)
|
|
129
|
+
error.code = 'E_HTTP_SERVER_USING_OAS_SET_ROUTE_INVALID_CONTENT_TYPE'
|
|
130
|
+
error.cause = `For the moment, the only request body supported has content-type: "application/json"`
|
|
131
|
+
throw error
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
package/index.test.js
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import Core from '@superhero/core'
|
|
2
|
+
import Request from '@superhero/http-request'
|
|
3
|
+
import assert from 'node:assert'
|
|
4
|
+
import util from 'node:util'
|
|
5
|
+
import { suite, test, beforeEach } from 'node:test'
|
|
6
|
+
|
|
7
|
+
util.inspect.defaultOptions.depth = 3
|
|
8
|
+
|
|
9
|
+
suite('@superhero/http-server-using-oas', () =>
|
|
10
|
+
{
|
|
11
|
+
let core
|
|
12
|
+
|
|
13
|
+
beforeEach(async () =>
|
|
14
|
+
{
|
|
15
|
+
if(beforeEach.skip)
|
|
16
|
+
{
|
|
17
|
+
return
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if(core)
|
|
21
|
+
{
|
|
22
|
+
await core.destroy()
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
core = new Core()
|
|
26
|
+
await core.add('@superhero/oas')
|
|
27
|
+
await core.add('@superhero/http-server')
|
|
28
|
+
await core.add('@superhero/http-server-using-oas')
|
|
29
|
+
|
|
30
|
+
core.locate.set('placeholder', { dispatch: () => 'placeholder' })
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
test('Can set a simple specification', async () =>
|
|
34
|
+
{
|
|
35
|
+
const specification =
|
|
36
|
+
{ paths:
|
|
37
|
+
{ '/foo':
|
|
38
|
+
{ get: { responses: { 200: {} }},
|
|
39
|
+
post: { responses: { 200: {} }}
|
|
40
|
+
},
|
|
41
|
+
'/bar':
|
|
42
|
+
{ put: { responses: { 200: {} }} }}}
|
|
43
|
+
|
|
44
|
+
core.locate.config.assign({ oas:specification })
|
|
45
|
+
|
|
46
|
+
await core.bootstrap()
|
|
47
|
+
|
|
48
|
+
const oas = core.locate('@superhero/http-server-using-oas')
|
|
49
|
+
oas.setOasRoute('/foo', 'get', 'placeholder')
|
|
50
|
+
oas.setOasRoute('/foo', 'post', 'placeholder')
|
|
51
|
+
oas.setOasRoute('/bar', 'put', 'placeholder')
|
|
52
|
+
|
|
53
|
+
assert.ok(oas.server.router.has('get /foo'), 'Route for "get /foo" should be added')
|
|
54
|
+
assert.ok(oas.server.router.has('post /foo'), 'Route for "post /foo" should be added')
|
|
55
|
+
assert.ok(oas.server.router.has('put /bar'), 'Route for "put /bar" should be added')
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('Can add middleware for requestBody content', async () =>
|
|
59
|
+
{
|
|
60
|
+
const specification =
|
|
61
|
+
{ paths:
|
|
62
|
+
{ '/foo':
|
|
63
|
+
{ post:
|
|
64
|
+
{ requestBody : { content: { 'application/json': {} } },
|
|
65
|
+
responses : { 200: {} } }}}}
|
|
66
|
+
|
|
67
|
+
core.locate.config.assign({ oas:specification })
|
|
68
|
+
await core.bootstrap()
|
|
69
|
+
const oas = core.locate('@superhero/http-server-using-oas')
|
|
70
|
+
|
|
71
|
+
oas.setOasRoute('/foo', 'post', 'placeholder')
|
|
72
|
+
|
|
73
|
+
const
|
|
74
|
+
route = oas.server.router.get('post /foo'),
|
|
75
|
+
requestBodyMiddleware = core.locate('@superhero/http-server-using-oas/dispatcher/upstream/request-bodies'),
|
|
76
|
+
hasRequestBodies = route.route.middlewares.includes(requestBodyMiddleware)
|
|
77
|
+
|
|
78
|
+
assert.ok(hasRequestBodies, 'Middleware for requestBody should be added')
|
|
79
|
+
assert.equal(
|
|
80
|
+
route.route['content-type.application/json'],
|
|
81
|
+
'@superhero/http-server/dispatcher/upstream/header/content-type/application/json')
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
test('Can add middleware for parameters', async () =>
|
|
85
|
+
{
|
|
86
|
+
const specification =
|
|
87
|
+
{ paths:
|
|
88
|
+
{ '/foo':
|
|
89
|
+
{ get:
|
|
90
|
+
{ parameters : [],
|
|
91
|
+
responses : { 200: {} } }}}}
|
|
92
|
+
|
|
93
|
+
core.locate.config.assign({ oas:specification })
|
|
94
|
+
await core.bootstrap()
|
|
95
|
+
const oas = core.locate('@superhero/http-server-using-oas')
|
|
96
|
+
|
|
97
|
+
oas.setOasRoute('/foo', 'get', 'placeholder')
|
|
98
|
+
|
|
99
|
+
const
|
|
100
|
+
route = oas.server.router.get('get /foo'),
|
|
101
|
+
parametersMiddleware = core.locate('@superhero/http-server-using-oas/dispatcher/upstream/parameters'),
|
|
102
|
+
hasParameters = route.route.middlewares.includes(parametersMiddleware)
|
|
103
|
+
|
|
104
|
+
assert.ok(hasParameters, 'Middleware for parameters should be added')
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
test('Specification with reference to components', async sub =>
|
|
108
|
+
{
|
|
109
|
+
const specification =
|
|
110
|
+
{ components:
|
|
111
|
+
{ headers:
|
|
112
|
+
{ ContentType:
|
|
113
|
+
{ required: true,
|
|
114
|
+
schema: { type: 'string' }}
|
|
115
|
+
},
|
|
116
|
+
parameters:
|
|
117
|
+
{ DefaultFoo: { name: 'foo', in: 'query', required: true, schema: { '$ref': '#/components/schemas/String' }, nullable: true, default: null },
|
|
118
|
+
RequiredFoo: { name: 'foo', in: 'query', required: true, schema: { '$ref': '#/components/schemas/String' }},
|
|
119
|
+
PathFoo: { name: 'foo', in: 'path', required: true, schema: { '$ref': '#/components/schemas/String' }},
|
|
120
|
+
QueryFoo: { name: 'foo', in: 'query', required: false, schema: { '$ref': '#/components/schemas/String' }},
|
|
121
|
+
HeaderFoo: { name: 'foo', in: 'header', required: false, schema: { '$ref': '#/components/schemas/String' }}
|
|
122
|
+
},
|
|
123
|
+
requestBodies:
|
|
124
|
+
{ ExampleRequestBody: { '$ref': '#/components/requestBodies/GenericRequestBody' },
|
|
125
|
+
GenericRequestBody:
|
|
126
|
+
{ required: true,
|
|
127
|
+
content: { 'application/json': { schema: { '$ref': '#/components/schemas/Foo' }}}}
|
|
128
|
+
},
|
|
129
|
+
responses:
|
|
130
|
+
{ SuccessResult:
|
|
131
|
+
{ description: 'Successful result',
|
|
132
|
+
headers: { 'Content-Type': { '$ref': '#/components/headers/ContentType' }},
|
|
133
|
+
content: { 'application/json': { schema: { '$ref': '#/components/schemas/Result' }}}
|
|
134
|
+
},
|
|
135
|
+
BadRequest:
|
|
136
|
+
{ description: 'Bad Request',
|
|
137
|
+
schema: { '$ref': '#/components/schemas/Result' }}
|
|
138
|
+
},
|
|
139
|
+
schemas:
|
|
140
|
+
{ String: { type: 'string' },
|
|
141
|
+
Foo:
|
|
142
|
+
{ type: 'object',
|
|
143
|
+
properties: { foo: { '$ref': '#/components/schemas/String' }}},
|
|
144
|
+
Result:
|
|
145
|
+
{ type: 'object',
|
|
146
|
+
properties: { result: { '$ref': '#/components/schemas/String' } }}
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
paths:
|
|
150
|
+
{ '/example/default':
|
|
151
|
+
{ get:
|
|
152
|
+
{ parameters: [{ '$ref': '#/components/parameters/DefaultFoo' }],
|
|
153
|
+
responses:
|
|
154
|
+
{ 200: { '$ref': '#/components/responses/SuccessResult' },
|
|
155
|
+
400: { '$ref': '#/components/responses/BadRequest' }}}
|
|
156
|
+
},
|
|
157
|
+
'/example/required':
|
|
158
|
+
{ get:
|
|
159
|
+
{ parameters: [{ '$ref': '#/components/parameters/RequiredFoo' }],
|
|
160
|
+
responses:
|
|
161
|
+
{ 200: { '$ref': '#/components/responses/SuccessResult' },
|
|
162
|
+
400: { '$ref': '#/components/responses/BadRequest' }}}
|
|
163
|
+
},
|
|
164
|
+
'/example/foo/{foo}':
|
|
165
|
+
{ get:
|
|
166
|
+
{ parameters: [{ '$ref': '#/components/parameters/PathFoo' }],
|
|
167
|
+
responses:
|
|
168
|
+
{ 200: { '$ref': '#/components/responses/SuccessResult' },
|
|
169
|
+
400: { '$ref': '#/components/responses/BadRequest' }}}
|
|
170
|
+
},
|
|
171
|
+
'/example':
|
|
172
|
+
{ get:
|
|
173
|
+
{ parameters:
|
|
174
|
+
[ { '$ref': '#/components/parameters/QueryFoo' },
|
|
175
|
+
{ '$ref': '#/components/parameters/HeaderFoo' }
|
|
176
|
+
],
|
|
177
|
+
responses:
|
|
178
|
+
{ 200: { '$ref': '#/components/responses/SuccessResult' },
|
|
179
|
+
400: { '$ref': '#/components/responses/BadRequest' }}
|
|
180
|
+
},
|
|
181
|
+
post:
|
|
182
|
+
{ requestBody: { '$ref': '#/components/requestBodies/ExampleRequestBody' },
|
|
183
|
+
responses:
|
|
184
|
+
{ 200: { '$ref': '#/components/responses/SuccessResult' },
|
|
185
|
+
400: { '$ref': '#/components/responses/BadRequest' }}}}}}
|
|
186
|
+
|
|
187
|
+
core.locate.config.assign({ oas:specification })
|
|
188
|
+
await core.bootstrap()
|
|
189
|
+
const oas = core.locate('@superhero/http-server-using-oas')
|
|
190
|
+
const
|
|
191
|
+
dispatcher1 = { dispatch: (request, session) => session.view.body.result = request.param.foo },
|
|
192
|
+
dispatcher2 = { dispatch: (request, session) => session.view.body.result = request.body.foo }
|
|
193
|
+
|
|
194
|
+
core.locate.set('test/dispatcher/1', dispatcher1)
|
|
195
|
+
core.locate.set('test/dispatcher/2', dispatcher2)
|
|
196
|
+
|
|
197
|
+
oas.setOasRoute('/example', 'get', 'test/dispatcher/1')
|
|
198
|
+
oas.setOasRoute('/example', 'post', 'test/dispatcher/2')
|
|
199
|
+
oas.setOasRoute('/example/foo/{foo}', 'get', 'test/dispatcher/1')
|
|
200
|
+
oas.setOasRoute('/example/required', 'get', 'placeholder')
|
|
201
|
+
oas.setOasRoute('/example/default', 'get', 'placeholder')
|
|
202
|
+
|
|
203
|
+
const
|
|
204
|
+
route = oas.server.router.get('get /example'),
|
|
205
|
+
parametersMiddleware = core.locate('@superhero/http-server-using-oas/dispatcher/upstream/parameters'),
|
|
206
|
+
requestBodiesMiddleware = core.locate('@superhero/http-server-using-oas/dispatcher/upstream/request-bodies'),
|
|
207
|
+
responsesMiddleware = core.locate('@superhero/http-server-using-oas/dispatcher/downstream/responses')
|
|
208
|
+
|
|
209
|
+
assert.ok(route, 'route for "get /example" should exist')
|
|
210
|
+
assert.ok(route.route.middlewares.includes(parametersMiddleware))
|
|
211
|
+
assert.ok(route.route.middlewares.includes(requestBodiesMiddleware))
|
|
212
|
+
assert.ok(route.route.middlewares.includes(responsesMiddleware))
|
|
213
|
+
|
|
214
|
+
assert.equal(route.route['condition.method'], 'get', 'Correct GET method condition')
|
|
215
|
+
|
|
216
|
+
const httpServer = core.locate('@superhero/http-server')
|
|
217
|
+
await httpServer.listen()
|
|
218
|
+
|
|
219
|
+
beforeEach.skip = true
|
|
220
|
+
|
|
221
|
+
await sub.test('GET method using default parameter query parameter', async () =>
|
|
222
|
+
{
|
|
223
|
+
const
|
|
224
|
+
baseUrl = `http://localhost:${httpServer.gateway.address().port}`,
|
|
225
|
+
request = new Request({ url: baseUrl, doNotThrowOnErrorStatus: true }),
|
|
226
|
+
response = await request.get({ url: '/example/default?foo=query', headers: { 'connection': 'close', 'content-type': 'application/json' }})
|
|
227
|
+
|
|
228
|
+
assert.equal(response.status, 200, '200 status code for GET method')
|
|
229
|
+
assert.equal(response.body.result, null, 'Correct response body for GET method')
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
await sub.test('GET method not using required parameter', async () =>
|
|
233
|
+
{
|
|
234
|
+
const
|
|
235
|
+
baseUrl = `http://localhost:${httpServer.gateway.address().port}`,
|
|
236
|
+
request = new Request({ url: baseUrl, doNotThrowOnErrorStatus: true }),
|
|
237
|
+
response = await request.get({ url: '/example/required', headers: { 'connection': 'close', 'content-type': 'application/json' }})
|
|
238
|
+
|
|
239
|
+
assert.equal(response.status, 400, '400 status code for GET method')
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
await sub.test('GET method using bar parameter', async () =>
|
|
243
|
+
{
|
|
244
|
+
const
|
|
245
|
+
baseUrl = `http://localhost:${httpServer.gateway.address().port}`,
|
|
246
|
+
request = new Request({ url: baseUrl, doNotThrowOnErrorStatus: true }),
|
|
247
|
+
response = await request.get({ url: '/example/foo/bar', headers: { 'connection': 'close', 'content-type': 'application/json' }})
|
|
248
|
+
|
|
249
|
+
assert.equal(response.status, 200, '200 status code for GET method')
|
|
250
|
+
assert.equal(response.body.result, 'bar', 'Correct response body for GET method')
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
await sub.test('GET method using header parameter', async () =>
|
|
254
|
+
{
|
|
255
|
+
const
|
|
256
|
+
baseUrl = `http://localhost:${httpServer.gateway.address().port}`,
|
|
257
|
+
request = new Request({ url: baseUrl, doNotThrowOnErrorStatus: true }),
|
|
258
|
+
response = await request.get({ url: '/example', headers: { 'foo':'header', 'connection': 'close', 'content-type': 'application/json' }})
|
|
259
|
+
|
|
260
|
+
assert.equal(response.status, 200, '200 status code for GET method')
|
|
261
|
+
assert.equal(response.body.result, 'header', 'Correct response body for GET method')
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
await sub.test('POST method using request body', async () =>
|
|
265
|
+
{
|
|
266
|
+
const
|
|
267
|
+
baseUrl = `http://localhost:${httpServer.gateway.address().port}`,
|
|
268
|
+
request = new Request({ url: baseUrl, doNotThrowOnErrorStatus: true }),
|
|
269
|
+
response = await request.post({ url: '/example', body: { foo: 'body' }, headers: { 'connection': 'close', 'content-type': 'application/json' }})
|
|
270
|
+
|
|
271
|
+
assert.equal(response.status, 200, '200 status code for POST method')
|
|
272
|
+
assert.equal(response.body.result, 'body', 'Correct response body for POST method')
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
beforeEach.skip = false
|
|
276
|
+
|
|
277
|
+
await httpServer.close()
|
|
278
|
+
})
|
|
279
|
+
})
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@superhero/http-server-using-oas",
|
|
3
|
+
"version": "4.7.2",
|
|
4
|
+
"description": "Integrates the HTTP server and OAS (OpenAPI Specification) @superhero components",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"HTTP 2.0",
|
|
7
|
+
"HTTP 1.1",
|
|
8
|
+
"HTTPS",
|
|
9
|
+
"OAS",
|
|
10
|
+
"OpenAPI",
|
|
11
|
+
"Router"
|
|
12
|
+
],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"type": "module",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": "./index.js",
|
|
17
|
+
"./dispatcher/*" : "./dispatcher/*.js",
|
|
18
|
+
"./dispatcher/downstream/*" : "./dispatcher/downstream/*.js",
|
|
19
|
+
"./dispatcher/upstream/*" : "./dispatcher/upstream/*.js"
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"syntax-check": "syntax-check",
|
|
23
|
+
"test": "syntax-check; node --test --test-reporter=@superhero/audit/reporter --experimental-test-coverage"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@superhero/oas": "4.7.2",
|
|
27
|
+
"@superhero/http-server": "4.7.2"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@superhero/audit": "4.7.2",
|
|
31
|
+
"@superhero/syntax-check": "0.0.2",
|
|
32
|
+
"@superhero/http-request": "4.7.2",
|
|
33
|
+
"@superhero/core": "4.7.2"
|
|
34
|
+
},
|
|
35
|
+
"author": {
|
|
36
|
+
"name": "Erik Landvall",
|
|
37
|
+
"email": "erik@landvall.se"
|
|
38
|
+
},
|
|
39
|
+
"repository": {
|
|
40
|
+
"type": "git",
|
|
41
|
+
"url": "https://github.com/superhero/http-server-using-oas"
|
|
42
|
+
}
|
|
43
|
+
}
|