@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 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
+ }