@loopback/rest 5.1.0 → 5.2.1
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/CHANGELOG.md +49 -0
- package/dist/body-parsers/body-parser.d.ts +1 -1
- package/dist/body-parsers/body-parser.js +112 -115
- package/dist/body-parsers/body-parser.js.map +1 -1
- package/dist/body-parsers/body-parser.json.js +24 -27
- package/dist/body-parsers/body-parser.json.js.map +1 -1
- package/dist/body-parsers/body-parser.raw.js +19 -22
- package/dist/body-parsers/body-parser.raw.js.map +1 -1
- package/dist/body-parsers/body-parser.text.js +21 -24
- package/dist/body-parsers/body-parser.text.js.map +1 -1
- package/dist/body-parsers/body-parser.urlencoded.js +19 -22
- package/dist/body-parsers/body-parser.urlencoded.js.map +1 -1
- package/dist/coercion/coerce-parameter.d.ts +3 -1
- package/dist/coercion/coerce-parameter.js +28 -12
- package/dist/coercion/coerce-parameter.js.map +1 -1
- package/dist/http-handler.d.ts +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/keys.d.ts +1 -1
- package/dist/keys.js +33 -34
- package/dist/keys.js.map +1 -1
- package/dist/parser.js +1 -1
- package/dist/parser.js.map +1 -1
- package/dist/providers/find-route.provider.d.ts +1 -1
- package/dist/providers/find-route.provider.js +21 -24
- package/dist/providers/find-route.provider.js.map +1 -1
- package/dist/providers/invoke-method.provider.d.ts +1 -1
- package/dist/providers/invoke-method.provider.js +16 -19
- package/dist/providers/invoke-method.provider.js.map +1 -1
- package/dist/providers/log-error.provider.d.ts +1 -1
- package/dist/providers/parse-params.provider.d.ts +1 -1
- package/dist/providers/parse-params.provider.js +20 -23
- package/dist/providers/parse-params.provider.js.map +1 -1
- package/dist/providers/reject.provider.d.ts +1 -1
- package/dist/providers/reject.provider.js +25 -28
- package/dist/providers/reject.provider.js.map +1 -1
- package/dist/providers/send.provider.d.ts +1 -1
- package/dist/request-context.d.ts +1 -1
- package/dist/rest.application.d.ts +1 -2
- package/dist/rest.application.js.map +1 -1
- package/dist/rest.component.d.ts +1 -1
- package/dist/rest.component.js +46 -49
- package/dist/rest.component.js.map +1 -1
- package/dist/rest.server.d.ts +1 -2
- package/dist/rest.server.js +618 -621
- package/dist/rest.server.js.map +1 -1
- package/dist/router/base-route.d.ts +1 -1
- package/dist/router/controller-route.d.ts +1 -1
- package/dist/router/controller-route.js +3 -4
- package/dist/router/controller-route.js.map +1 -1
- package/dist/router/handler-route.d.ts +1 -1
- package/dist/router/handler-route.js +2 -2
- package/dist/router/handler-route.js.map +1 -1
- package/dist/router/redirect-route.js +2 -2
- package/dist/router/redirect-route.js.map +1 -1
- package/dist/router/regexp-router.js +55 -58
- package/dist/router/regexp-router.js.map +1 -1
- package/dist/router/route-entry.d.ts +1 -1
- package/dist/router/trie-router.js +31 -34
- package/dist/router/trie-router.js.map +1 -1
- package/dist/sequence.d.ts +1 -1
- package/dist/sequence.js +74 -77
- package/dist/sequence.js.map +1 -1
- package/dist/spec-enhancers/consolidate.spec-enhancer.js +89 -92
- package/dist/spec-enhancers/consolidate.spec-enhancer.js.map +1 -1
- package/dist/spec-enhancers/info.spec-enhancer.js +63 -67
- package/dist/spec-enhancers/info.spec-enhancer.js.map +1 -1
- package/dist/types.d.ts +10 -0
- package/dist/validation/ajv-factory.provider.js +63 -66
- package/dist/validation/ajv-factory.provider.js.map +1 -1
- package/dist/validation/request-body.validator.d.ts +10 -2
- package/dist/validation/request-body.validator.js +25 -9
- package/dist/validation/request-body.validator.js.map +1 -1
- package/package.json +29 -27
- package/src/body-parsers/body-parser.json.ts +1 -1
- package/src/body-parsers/body-parser.raw.ts +1 -1
- package/src/body-parsers/body-parser.text.ts +1 -1
- package/src/body-parsers/body-parser.ts +1 -1
- package/src/body-parsers/body-parser.urlencoded.ts +1 -1
- package/src/coercion/coerce-parameter.ts +55 -15
- package/src/http-handler.ts +1 -1
- package/src/index.ts +6 -0
- package/src/keys.ts +1 -2
- package/src/parser.ts +1 -1
- package/src/providers/find-route.provider.ts +1 -1
- package/src/providers/invoke-method.provider.ts +1 -1
- package/src/providers/log-error.provider.ts +1 -1
- package/src/providers/parse-params.provider.ts +1 -1
- package/src/providers/reject.provider.ts +1 -1
- package/src/providers/send.provider.ts +1 -1
- package/src/request-context.ts +1 -1
- package/src/rest.application.ts +4 -2
- package/src/rest.component.ts +1 -1
- package/src/rest.server.ts +5 -3
- package/src/router/base-route.ts +1 -1
- package/src/router/controller-route.ts +2 -2
- package/src/router/external-express-routes.ts +1 -1
- package/src/router/handler-route.ts +1 -1
- package/src/router/redirect-route.ts +1 -2
- package/src/router/regexp-router.ts +1 -1
- package/src/router/route-entry.ts +1 -1
- package/src/router/trie-router.ts +1 -1
- package/src/sequence.ts +1 -1
- package/src/spec-enhancers/info.spec-enhancer.ts +3 -2
- package/src/types.ts +11 -0
- package/src/validation/request-body.validator.ts +35 -12
package/dist/rest.server.js
CHANGED
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
7
|
exports.createBodyParserBinding = exports.RestServer = void 0;
|
|
8
8
|
const tslib_1 = require("tslib");
|
|
9
|
-
const context_1 = require("@loopback/context");
|
|
10
9
|
const core_1 = require("@loopback/core");
|
|
11
10
|
const express_1 = require("@loopback/express");
|
|
12
11
|
const http_server_1 = require("@loopback/http-server");
|
|
@@ -55,653 +54,651 @@ const SequenceActions = keys_1.RestBindings.SequenceActions;
|
|
|
55
54
|
* const server = await app.get('servers.foo');
|
|
56
55
|
* ```
|
|
57
56
|
*/
|
|
58
|
-
let RestServer =
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
57
|
+
let RestServer = class RestServer extends express_1.BaseMiddlewareRegistry {
|
|
58
|
+
/**
|
|
59
|
+
*
|
|
60
|
+
* Creates an instance of RestServer.
|
|
61
|
+
*
|
|
62
|
+
* @param app - The application instance (injected via
|
|
63
|
+
* CoreBindings.APPLICATION_INSTANCE).
|
|
64
|
+
* @param config - The configuration options (injected via
|
|
65
|
+
* RestBindings.CONFIG).
|
|
66
|
+
*
|
|
67
|
+
*/
|
|
68
|
+
constructor(app, config = {}) {
|
|
69
|
+
var _a;
|
|
70
|
+
super(app);
|
|
71
|
+
/*
|
|
72
|
+
* Registry of external routes & static assets
|
|
69
73
|
*/
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
this.bind(keys_1.RestBindings.
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
this.
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
this._setupOASEnhancerIfNeeded();
|
|
99
|
-
return this._OASEnhancer;
|
|
100
|
-
}
|
|
101
|
-
get requestHandler() {
|
|
102
|
-
if (this._requestHandler == null) {
|
|
103
|
-
this._setupRequestHandlerIfNeeded();
|
|
104
|
-
}
|
|
105
|
-
return this._requestHandler;
|
|
106
|
-
}
|
|
107
|
-
get httpHandler() {
|
|
108
|
-
this._setupHandlerIfNeeded();
|
|
109
|
-
return this._httpHandler;
|
|
110
|
-
}
|
|
111
|
-
get listening() {
|
|
112
|
-
return this._httpServer ? this._httpServer.listening : false;
|
|
74
|
+
this._externalRoutes = new router_1.ExternalExpressRoutes();
|
|
75
|
+
this.config = resolveRestServerConfig(config);
|
|
76
|
+
this.bind(keys_1.RestBindings.PORT).to(this.config.port);
|
|
77
|
+
this.bind(keys_1.RestBindings.HOST).to(config.host);
|
|
78
|
+
this.bind(keys_1.RestBindings.PATH).to(config.path);
|
|
79
|
+
this.bind(keys_1.RestBindings.PROTOCOL).to((_a = config.protocol) !== null && _a !== void 0 ? _a : 'http');
|
|
80
|
+
this.bind(keys_1.RestBindings.HTTPS_OPTIONS).to(config);
|
|
81
|
+
if (config.requestBodyParser) {
|
|
82
|
+
this.bind(keys_1.RestBindings.REQUEST_BODY_PARSER_OPTIONS).to(config.requestBodyParser);
|
|
83
|
+
}
|
|
84
|
+
if (config.sequence) {
|
|
85
|
+
this.sequence(config.sequence);
|
|
86
|
+
}
|
|
87
|
+
if (config.router) {
|
|
88
|
+
this.bind(keys_1.RestBindings.ROUTER_OPTIONS).to(config.router);
|
|
89
|
+
}
|
|
90
|
+
this.basePath(config.basePath);
|
|
91
|
+
this.bind(keys_1.RestBindings.BASE_PATH).toDynamicValue(() => this._basePath);
|
|
92
|
+
this.bind(keys_1.RestBindings.HANDLER).toDynamicValue(() => this.httpHandler);
|
|
93
|
+
}
|
|
94
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
95
|
+
get OASEnhancer() {
|
|
96
|
+
this._setupOASEnhancerIfNeeded();
|
|
97
|
+
return this._OASEnhancer;
|
|
98
|
+
}
|
|
99
|
+
get requestHandler() {
|
|
100
|
+
if (this._requestHandler == null) {
|
|
101
|
+
this._setupRequestHandlerIfNeeded();
|
|
113
102
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
103
|
+
return this._requestHandler;
|
|
104
|
+
}
|
|
105
|
+
get httpHandler() {
|
|
106
|
+
this._setupHandlerIfNeeded();
|
|
107
|
+
return this._httpHandler;
|
|
108
|
+
}
|
|
109
|
+
get listening() {
|
|
110
|
+
return this._httpServer ? this._httpServer.listening : false;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* The base url for the server, including the basePath if set. For example,
|
|
114
|
+
* the value will be 'http://localhost:3000/api' if `basePath` is set to
|
|
115
|
+
* '/api'.
|
|
116
|
+
*/
|
|
117
|
+
get url() {
|
|
118
|
+
let serverUrl = this.rootUrl;
|
|
119
|
+
if (!serverUrl)
|
|
124
120
|
return serverUrl;
|
|
121
|
+
serverUrl = serverUrl + (this._basePath || '');
|
|
122
|
+
return serverUrl;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* The root url for the server without the basePath. For example, the value
|
|
126
|
+
* will be 'http://localhost:3000' regardless of the `basePath`.
|
|
127
|
+
*/
|
|
128
|
+
get rootUrl() {
|
|
129
|
+
var _a;
|
|
130
|
+
return (_a = this._httpServer) === null || _a === void 0 ? void 0 : _a.url;
|
|
131
|
+
}
|
|
132
|
+
_setupOASEnhancerIfNeeded() {
|
|
133
|
+
if (this._OASEnhancer != null)
|
|
134
|
+
return;
|
|
135
|
+
this.add(core_1.createBindingFromClass(openapi_v3_1.OASEnhancerService, {
|
|
136
|
+
key: openapi_v3_1.OASEnhancerBindings.OAS_ENHANCER_SERVICE,
|
|
137
|
+
}));
|
|
138
|
+
this._OASEnhancer = this.getSync(openapi_v3_1.OASEnhancerBindings.OAS_ENHANCER_SERVICE);
|
|
139
|
+
}
|
|
140
|
+
_setupRequestHandlerIfNeeded() {
|
|
141
|
+
if (this._expressApp != null)
|
|
142
|
+
return;
|
|
143
|
+
this._expressApp = express_2.default();
|
|
144
|
+
this._applyExpressSettings();
|
|
145
|
+
this._requestHandler = this._expressApp;
|
|
146
|
+
// Allow CORS support for all endpoints so that users
|
|
147
|
+
// can test with online SwaggerUI instance
|
|
148
|
+
this.expressMiddleware(cors_1.default, this.config.cors, {
|
|
149
|
+
injectConfiguration: false,
|
|
150
|
+
key: 'middleware.cors',
|
|
151
|
+
group: 'cors',
|
|
152
|
+
});
|
|
153
|
+
// Set up endpoints for OpenAPI spec/ui
|
|
154
|
+
this._setupOpenApiSpecEndpoints();
|
|
155
|
+
// Mount our router & request handler
|
|
156
|
+
this._expressApp.use(this._basePath, (req, res, next) => {
|
|
157
|
+
this._handleHttpRequest(req, res).catch(next);
|
|
158
|
+
});
|
|
159
|
+
// Mount our error handler
|
|
160
|
+
this._expressApp.use(this._unexpectedErrorHandler());
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Get an Express handler for unexpected errors
|
|
164
|
+
*/
|
|
165
|
+
_unexpectedErrorHandler() {
|
|
166
|
+
const handleUnExpectedError = (err, req, res, next) => {
|
|
167
|
+
// Handle errors reported by Express middleware such as CORS
|
|
168
|
+
// First try to use the `REJECT` action
|
|
169
|
+
this.get(SequenceActions.REJECT, { optional: true })
|
|
170
|
+
.then(reject => {
|
|
171
|
+
if (reject) {
|
|
172
|
+
// TODO(rfeng): There is a possibility that the error is thrown
|
|
173
|
+
// from the `REJECT` action in the sequence
|
|
174
|
+
return reject({ request: req, response: res }, err);
|
|
175
|
+
}
|
|
176
|
+
// Use strong-error handler directly
|
|
177
|
+
strong_error_handler_1.writeErrorToResponse(err, req, res);
|
|
178
|
+
})
|
|
179
|
+
.catch(unexpectedErr => next(unexpectedErr));
|
|
180
|
+
};
|
|
181
|
+
return handleUnExpectedError;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Apply express settings.
|
|
185
|
+
*/
|
|
186
|
+
_applyExpressSettings() {
|
|
187
|
+
assertExists(this._expressApp, 'this._expressApp');
|
|
188
|
+
const settings = this.config.expressSettings;
|
|
189
|
+
for (const key in settings) {
|
|
190
|
+
this._expressApp.set(key, settings[key]);
|
|
191
|
+
}
|
|
192
|
+
if (this.config.router && typeof this.config.router.strict === 'boolean') {
|
|
193
|
+
this._expressApp.set('strict routing', this.config.router.strict);
|
|
125
194
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
this.
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Mount /openapi.json, /openapi.yaml for specs and /swagger-ui, /explorer
|
|
198
|
+
* to redirect to externally hosted API explorer
|
|
199
|
+
*/
|
|
200
|
+
_setupOpenApiSpecEndpoints() {
|
|
201
|
+
assertExists(this._expressApp, 'this._expressApp');
|
|
202
|
+
if (this.config.openApiSpec.disabled)
|
|
203
|
+
return;
|
|
204
|
+
const router = express_2.default.Router();
|
|
205
|
+
const mapping = this.config.openApiSpec.endpointMapping;
|
|
206
|
+
// Serving OpenAPI spec
|
|
207
|
+
for (const p in mapping) {
|
|
208
|
+
this.addOpenApiSpecEndpoint(p, mapping[p], router);
|
|
209
|
+
}
|
|
210
|
+
const explorerPaths = ['/swagger-ui', '/explorer'];
|
|
211
|
+
router.get(explorerPaths, (req, res, next) => this._redirectToSwaggerUI(req, res, next));
|
|
212
|
+
this.expressMiddleware('middleware.apiSpec.defaults', router, {
|
|
213
|
+
group: 'apiSpec',
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Add a new non-controller endpoint hosting a form of the OpenAPI spec.
|
|
218
|
+
*
|
|
219
|
+
* @param path Path at which to host the copy of the OpenAPI
|
|
220
|
+
* @param form Form that should be rendered from that path
|
|
221
|
+
*/
|
|
222
|
+
addOpenApiSpecEndpoint(path, form, router) {
|
|
223
|
+
if (router == null) {
|
|
224
|
+
const key = `middleware.apiSpec.${path}.${form}`;
|
|
225
|
+
if (this.contains(key)) {
|
|
226
|
+
throw new Error(`The path ${path} is already configured for OpenApi hosting`);
|
|
227
|
+
}
|
|
228
|
+
const newRouter = express_2.default.Router();
|
|
229
|
+
newRouter.get(path, (req, res) => this._serveOpenApiSpec(req, res, form));
|
|
230
|
+
this.expressMiddleware(() => newRouter, {}, {
|
|
150
231
|
injectConfiguration: false,
|
|
151
|
-
key:
|
|
152
|
-
group: '
|
|
153
|
-
});
|
|
154
|
-
// Set up endpoints for OpenAPI spec/ui
|
|
155
|
-
this._setupOpenApiSpecEndpoints();
|
|
156
|
-
// Mount our router & request handler
|
|
157
|
-
this._expressApp.use(this._basePath, (req, res, next) => {
|
|
158
|
-
this._handleHttpRequest(req, res).catch(next);
|
|
232
|
+
key: `middleware.apiSpec.${path}.${form}`,
|
|
233
|
+
group: 'apiSpec',
|
|
159
234
|
});
|
|
160
|
-
// Mount our error handler
|
|
161
|
-
this._expressApp.use(this._unexpectedErrorHandler());
|
|
162
235
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
*/
|
|
166
|
-
_unexpectedErrorHandler() {
|
|
167
|
-
const handleUnExpectedError = (err, req, res, next) => {
|
|
168
|
-
// Handle errors reported by Express middleware such as CORS
|
|
169
|
-
// First try to use the `REJECT` action
|
|
170
|
-
this.get(SequenceActions.REJECT, { optional: true })
|
|
171
|
-
.then(reject => {
|
|
172
|
-
if (reject) {
|
|
173
|
-
// TODO(rfeng): There is a possibility that the error is thrown
|
|
174
|
-
// from the `REJECT` action in the sequence
|
|
175
|
-
return reject({ request: req, response: res }, err);
|
|
176
|
-
}
|
|
177
|
-
// Use strong-error handler directly
|
|
178
|
-
strong_error_handler_1.writeErrorToResponse(err, req, res);
|
|
179
|
-
})
|
|
180
|
-
.catch(unexpectedErr => next(unexpectedErr));
|
|
181
|
-
};
|
|
182
|
-
return handleUnExpectedError;
|
|
236
|
+
else {
|
|
237
|
+
router.get(path, (req, res) => this._serveOpenApiSpec(req, res, form));
|
|
183
238
|
}
|
|
239
|
+
}
|
|
240
|
+
_handleHttpRequest(request, response) {
|
|
241
|
+
return this.httpHandler.handleRequest(request, response);
|
|
242
|
+
}
|
|
243
|
+
_setupHandlerIfNeeded() {
|
|
244
|
+
if (this._httpHandler)
|
|
245
|
+
return;
|
|
246
|
+
// Watch for binding events
|
|
247
|
+
// See https://github.com/strongloop/loopback-next/issues/433
|
|
248
|
+
const routesObserver = {
|
|
249
|
+
filter: binding => core_1.filterByKey(keys_1.RestBindings.API_SPEC.key)(binding) ||
|
|
250
|
+
(core_1.filterByKey(/^(controllers|routes)\..+/)(binding) &&
|
|
251
|
+
// Exclude controller routes to avoid circular events
|
|
252
|
+
!core_1.filterByTag(keys_1.RestTags.CONTROLLER_ROUTE)(binding)),
|
|
253
|
+
observe: () => {
|
|
254
|
+
// Rebuild the HttpHandler instance whenever a controller/route was
|
|
255
|
+
// added/deleted.
|
|
256
|
+
this._createHttpHandler();
|
|
257
|
+
},
|
|
258
|
+
};
|
|
259
|
+
this._routesEventSubscription = this.subscribe(routesObserver);
|
|
260
|
+
this._createHttpHandler();
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Create an instance of HttpHandler and populates it with routes
|
|
264
|
+
*/
|
|
265
|
+
_createHttpHandler() {
|
|
184
266
|
/**
|
|
185
|
-
*
|
|
267
|
+
* Check if there is custom router in the context
|
|
186
268
|
*/
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
269
|
+
const router = this.getSync(keys_1.RestBindings.ROUTER, { optional: true });
|
|
270
|
+
const routingTable = new router_1.RoutingTable(router, this._externalRoutes);
|
|
271
|
+
this._httpHandler = new http_handler_1.HttpHandler(this, this.config, routingTable);
|
|
272
|
+
// Remove controller routes
|
|
273
|
+
for (const b of this.findByTag(keys_1.RestTags.CONTROLLER_ROUTE)) {
|
|
274
|
+
this.unbind(b.key);
|
|
275
|
+
}
|
|
276
|
+
for (const b of this.find(`${core_1.CoreBindings.CONTROLLERS}.*`)) {
|
|
277
|
+
const controllerName = b.key.replace(/^controllers\./, '');
|
|
278
|
+
const ctor = b.valueConstructor;
|
|
279
|
+
if (!ctor) {
|
|
280
|
+
throw new Error(`The controller ${controllerName} was not bound via .toClass()`);
|
|
281
|
+
}
|
|
282
|
+
const apiSpec = openapi_v3_1.getControllerSpec(ctor);
|
|
283
|
+
if (!apiSpec) {
|
|
284
|
+
// controller methods are specified through app.api() spec
|
|
285
|
+
debug('Skipping controller %s - no API spec provided', controllerName);
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
debug('Registering controller %s', controllerName);
|
|
289
|
+
if (apiSpec.components) {
|
|
290
|
+
this._httpHandler.registerApiComponents(apiSpec.components);
|
|
291
|
+
}
|
|
292
|
+
const controllerFactory = router_1.createControllerFactoryForBinding(b.key);
|
|
293
|
+
const routes = router_1.createRoutesForController(apiSpec, ctor, controllerFactory);
|
|
294
|
+
for (const route of routes) {
|
|
295
|
+
const binding = this.bindRoute(route);
|
|
296
|
+
binding
|
|
297
|
+
.tag(keys_1.RestTags.CONTROLLER_ROUTE)
|
|
298
|
+
.tag({ [keys_1.RestTags.CONTROLLER_BINDING]: b.key });
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
for (const b of this.findByTag(keys_1.RestTags.REST_ROUTE)) {
|
|
302
|
+
// TODO(bajtos) should we support routes defined asynchronously?
|
|
303
|
+
const route = this.getSync(b.key);
|
|
304
|
+
this._httpHandler.registerRoute(route);
|
|
305
|
+
}
|
|
306
|
+
// TODO(bajtos) should we support API spec defined asynchronously?
|
|
307
|
+
const spec = this.getSync(keys_1.RestBindings.API_SPEC);
|
|
308
|
+
for (const path in spec.paths) {
|
|
309
|
+
for (const verb in spec.paths[path]) {
|
|
310
|
+
const routeSpec = spec.paths[path][verb];
|
|
311
|
+
this._setupOperation(verb, path, routeSpec);
|
|
195
312
|
}
|
|
196
313
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
group: 'apiSpec',
|
|
314
|
+
}
|
|
315
|
+
_setupOperation(verb, path, spec) {
|
|
316
|
+
const handler = spec['x-operation'];
|
|
317
|
+
if (typeof handler === 'function') {
|
|
318
|
+
// Remove a field value that cannot be represented in JSON.
|
|
319
|
+
// Start by creating a shallow-copy of the spec, so that we don't
|
|
320
|
+
// modify the original spec object provided by user.
|
|
321
|
+
spec = Object.assign({}, spec);
|
|
322
|
+
delete spec['x-operation'];
|
|
323
|
+
const route = new router_1.Route(verb, path, spec, handler);
|
|
324
|
+
this._httpHandler.registerRoute(route);
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
const controllerName = spec['x-controller-name'];
|
|
328
|
+
if (typeof controllerName === 'string') {
|
|
329
|
+
const b = this.getBinding(`controllers.${controllerName}`, {
|
|
330
|
+
optional: true,
|
|
215
331
|
});
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
* Add a new non-controller endpoint hosting a form of the OpenAPI spec.
|
|
219
|
-
*
|
|
220
|
-
* @param path Path at which to host the copy of the OpenAPI
|
|
221
|
-
* @param form Form that should be rendered from that path
|
|
222
|
-
*/
|
|
223
|
-
addOpenApiSpecEndpoint(path, form, router) {
|
|
224
|
-
if (router == null) {
|
|
225
|
-
const key = `middleware.apiSpec.${path}.${form}`;
|
|
226
|
-
if (this.contains(key)) {
|
|
227
|
-
throw new Error(`The path ${path} is already configured for OpenApi hosting`);
|
|
228
|
-
}
|
|
229
|
-
const newRouter = express_2.default.Router();
|
|
230
|
-
newRouter.get(path, (req, res) => this._serveOpenApiSpec(req, res, form));
|
|
231
|
-
this.expressMiddleware(() => newRouter, {}, {
|
|
232
|
-
injectConfiguration: false,
|
|
233
|
-
key: `middleware.apiSpec.${path}.${form}`,
|
|
234
|
-
group: 'apiSpec',
|
|
235
|
-
});
|
|
332
|
+
if (!b) {
|
|
333
|
+
throw new Error(`Unknown controller ${controllerName} used by "${verb} ${path}"`);
|
|
236
334
|
}
|
|
237
|
-
|
|
238
|
-
|
|
335
|
+
const ctor = b.valueConstructor;
|
|
336
|
+
if (!ctor) {
|
|
337
|
+
throw new Error(`The controller ${controllerName} was not bound via .toClass()`);
|
|
239
338
|
}
|
|
339
|
+
const controllerFactory = router_1.createControllerFactoryForBinding(b.key);
|
|
340
|
+
const route = new router_1.ControllerRoute(verb, path, spec, ctor, controllerFactory);
|
|
341
|
+
this._httpHandler.registerRoute(route);
|
|
342
|
+
return;
|
|
240
343
|
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
// added/deleted.
|
|
257
|
-
this._createHttpHandler();
|
|
258
|
-
},
|
|
259
|
-
};
|
|
260
|
-
this._routesEventSubscription = this.subscribe(routesObserver);
|
|
261
|
-
this._createHttpHandler();
|
|
262
|
-
}
|
|
263
|
-
/**
|
|
264
|
-
* Create an instance of HttpHandler and populates it with routes
|
|
265
|
-
*/
|
|
266
|
-
_createHttpHandler() {
|
|
267
|
-
/**
|
|
268
|
-
* Check if there is custom router in the context
|
|
269
|
-
*/
|
|
270
|
-
const router = this.getSync(keys_1.RestBindings.ROUTER, { optional: true });
|
|
271
|
-
const routingTable = new router_1.RoutingTable(router, this._externalRoutes);
|
|
272
|
-
this._httpHandler = new http_handler_1.HttpHandler(this, this.config, routingTable);
|
|
273
|
-
// Remove controller routes
|
|
274
|
-
for (const b of this.findByTag(keys_1.RestTags.CONTROLLER_ROUTE)) {
|
|
275
|
-
this.unbind(b.key);
|
|
276
|
-
}
|
|
277
|
-
for (const b of this.find(`${core_1.CoreBindings.CONTROLLERS}.*`)) {
|
|
278
|
-
const controllerName = b.key.replace(/^controllers\./, '');
|
|
279
|
-
const ctor = b.valueConstructor;
|
|
280
|
-
if (!ctor) {
|
|
281
|
-
throw new Error(`The controller ${controllerName} was not bound via .toClass()`);
|
|
282
|
-
}
|
|
283
|
-
const apiSpec = openapi_v3_1.getControllerSpec(ctor);
|
|
284
|
-
if (!apiSpec) {
|
|
285
|
-
// controller methods are specified through app.api() spec
|
|
286
|
-
debug('Skipping controller %s - no API spec provided', controllerName);
|
|
287
|
-
continue;
|
|
288
|
-
}
|
|
289
|
-
debug('Registering controller %s', controllerName);
|
|
290
|
-
if (apiSpec.components) {
|
|
291
|
-
this._httpHandler.registerApiComponents(apiSpec.components);
|
|
292
|
-
}
|
|
293
|
-
const controllerFactory = router_1.createControllerFactoryForBinding(b.key);
|
|
294
|
-
const routes = router_1.createRoutesForController(apiSpec, ctor, controllerFactory);
|
|
295
|
-
for (const route of routes) {
|
|
296
|
-
const binding = this.bindRoute(route);
|
|
297
|
-
binding
|
|
298
|
-
.tag(keys_1.RestTags.CONTROLLER_ROUTE)
|
|
299
|
-
.tag({ [keys_1.RestTags.CONTROLLER_BINDING]: b.key });
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
for (const b of this.findByTag(keys_1.RestTags.REST_ROUTE)) {
|
|
303
|
-
// TODO(bajtos) should we support routes defined asynchronously?
|
|
304
|
-
const route = this.getSync(b.key);
|
|
305
|
-
this._httpHandler.registerRoute(route);
|
|
306
|
-
}
|
|
307
|
-
// TODO(bajtos) should we support API spec defined asynchronously?
|
|
308
|
-
const spec = this.getSync(keys_1.RestBindings.API_SPEC);
|
|
309
|
-
for (const path in spec.paths) {
|
|
310
|
-
for (const verb in spec.paths[path]) {
|
|
311
|
-
const routeSpec = spec.paths[path][verb];
|
|
312
|
-
this._setupOperation(verb, path, routeSpec);
|
|
313
|
-
}
|
|
314
|
-
}
|
|
344
|
+
throw new Error(`There is no handler configured for operation "${verb} ${path}`);
|
|
345
|
+
}
|
|
346
|
+
async _serveOpenApiSpec(request, response, specForm) {
|
|
347
|
+
const requestContext = new request_context_1.RequestContext(request, response, this, this.config);
|
|
348
|
+
specForm = specForm !== null && specForm !== void 0 ? specForm : { version: '3.0.0', format: 'json' };
|
|
349
|
+
const specObj = await this.getApiSpec(requestContext);
|
|
350
|
+
if (specForm.format === 'json') {
|
|
351
|
+
const spec = JSON.stringify(specObj, null, 2);
|
|
352
|
+
response.setHeader('content-type', 'application/json; charset=utf-8');
|
|
353
|
+
response.end(spec, 'utf-8');
|
|
354
|
+
}
|
|
355
|
+
else {
|
|
356
|
+
const yaml = js_yaml_1.safeDump(specObj, {});
|
|
357
|
+
response.setHeader('content-type', 'text/yaml; charset=utf-8');
|
|
358
|
+
response.end(yaml, 'utf-8');
|
|
315
359
|
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
360
|
+
}
|
|
361
|
+
async _redirectToSwaggerUI(request, response, next) {
|
|
362
|
+
const config = this.config.apiExplorer;
|
|
363
|
+
if (config.disabled) {
|
|
364
|
+
debug('Redirect to swagger-ui was disabled by configuration.');
|
|
365
|
+
next();
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
debug('Redirecting to swagger-ui from %j.', request.originalUrl);
|
|
369
|
+
const requestContext = new request_context_1.RequestContext(request, response, this, this.config);
|
|
370
|
+
const protocol = requestContext.requestedProtocol;
|
|
371
|
+
const baseUrl = protocol === 'http' ? config.httpUrl : config.url;
|
|
372
|
+
const openApiUrl = `${requestContext.requestedBaseUrl}/openapi.json`;
|
|
373
|
+
const fullUrl = `${baseUrl}?url=${openApiUrl}`;
|
|
374
|
+
response.redirect(302, fullUrl);
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Register a controller class with this server.
|
|
378
|
+
*
|
|
379
|
+
* @param controllerCtor - The controller class
|
|
380
|
+
* (constructor function).
|
|
381
|
+
* @returns The newly created binding, you can use the reference to
|
|
382
|
+
* further modify the binding, e.g. lock the value to prevent further
|
|
383
|
+
* modifications.
|
|
384
|
+
*
|
|
385
|
+
* @example
|
|
386
|
+
* ```ts
|
|
387
|
+
* class MyController {
|
|
388
|
+
* }
|
|
389
|
+
* app.controller(MyController).lock();
|
|
390
|
+
* ```
|
|
391
|
+
*
|
|
392
|
+
*/
|
|
393
|
+
controller(controllerCtor) {
|
|
394
|
+
return this.bind('controllers.' + controllerCtor.name).toClass(controllerCtor);
|
|
395
|
+
}
|
|
396
|
+
route(routeOrVerb, path, spec, controllerCtorOrHandler, controllerFactory, methodName) {
|
|
397
|
+
if (typeof routeOrVerb === 'object') {
|
|
398
|
+
const r = routeOrVerb;
|
|
399
|
+
// Encode the path to escape special chars
|
|
400
|
+
return this.bindRoute(r);
|
|
401
|
+
}
|
|
402
|
+
if (!path) {
|
|
403
|
+
throw new assert_1.AssertionError({
|
|
404
|
+
message: 'path is required for a controller-based route',
|
|
405
|
+
});
|
|
361
406
|
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
next();
|
|
367
|
-
return;
|
|
368
|
-
}
|
|
369
|
-
debug('Redirecting to swagger-ui from %j.', request.originalUrl);
|
|
370
|
-
const requestContext = new request_context_1.RequestContext(request, response, this, this.config);
|
|
371
|
-
const protocol = requestContext.requestedProtocol;
|
|
372
|
-
const baseUrl = protocol === 'http' ? config.httpUrl : config.url;
|
|
373
|
-
const openApiUrl = `${requestContext.requestedBaseUrl}/openapi.json`;
|
|
374
|
-
const fullUrl = `${baseUrl}?url=${openApiUrl}`;
|
|
375
|
-
response.redirect(302, fullUrl);
|
|
407
|
+
if (!spec) {
|
|
408
|
+
throw new assert_1.AssertionError({
|
|
409
|
+
message: 'spec is required for a controller-based route',
|
|
410
|
+
});
|
|
376
411
|
}
|
|
377
|
-
|
|
378
|
-
* Register a controller class with this server.
|
|
379
|
-
*
|
|
380
|
-
* @param controllerCtor - The controller class
|
|
381
|
-
* (constructor function).
|
|
382
|
-
* @returns The newly created binding, you can use the reference to
|
|
383
|
-
* further modify the binding, e.g. lock the value to prevent further
|
|
384
|
-
* modifications.
|
|
385
|
-
*
|
|
386
|
-
* @example
|
|
387
|
-
* ```ts
|
|
388
|
-
* class MyController {
|
|
389
|
-
* }
|
|
390
|
-
* app.controller(MyController).lock();
|
|
391
|
-
* ```
|
|
392
|
-
*
|
|
393
|
-
*/
|
|
394
|
-
controller(controllerCtor) {
|
|
395
|
-
return this.bind('controllers.' + controllerCtor.name).toClass(controllerCtor);
|
|
396
|
-
}
|
|
397
|
-
route(routeOrVerb, path, spec, controllerCtorOrHandler, controllerFactory, methodName) {
|
|
398
|
-
if (typeof routeOrVerb === 'object') {
|
|
399
|
-
const r = routeOrVerb;
|
|
400
|
-
// Encode the path to escape special chars
|
|
401
|
-
return this.bindRoute(r);
|
|
402
|
-
}
|
|
403
|
-
if (!path) {
|
|
404
|
-
throw new assert_1.AssertionError({
|
|
405
|
-
message: 'path is required for a controller-based route',
|
|
406
|
-
});
|
|
407
|
-
}
|
|
408
|
-
if (!spec) {
|
|
409
|
-
throw new assert_1.AssertionError({
|
|
410
|
-
message: 'spec is required for a controller-based route',
|
|
411
|
-
});
|
|
412
|
-
}
|
|
413
|
-
if (arguments.length === 4) {
|
|
414
|
-
if (!controllerCtorOrHandler) {
|
|
415
|
-
throw new assert_1.AssertionError({
|
|
416
|
-
message: 'handler function is required for a handler-based route',
|
|
417
|
-
});
|
|
418
|
-
}
|
|
419
|
-
return this.route(new router_1.Route(routeOrVerb, path, spec, controllerCtorOrHandler));
|
|
420
|
-
}
|
|
412
|
+
if (arguments.length === 4) {
|
|
421
413
|
if (!controllerCtorOrHandler) {
|
|
422
414
|
throw new assert_1.AssertionError({
|
|
423
|
-
message: '
|
|
415
|
+
message: 'handler function is required for a handler-based route',
|
|
424
416
|
});
|
|
425
417
|
}
|
|
426
|
-
|
|
427
|
-
throw new assert_1.AssertionError({
|
|
428
|
-
message: 'methodName is required for a controller-based route',
|
|
429
|
-
});
|
|
430
|
-
}
|
|
431
|
-
return this.route(new router_1.ControllerRoute(routeOrVerb, path, spec, controllerCtorOrHandler, controllerFactory, methodName));
|
|
432
|
-
}
|
|
433
|
-
bindRoute(r) {
|
|
434
|
-
const namespace = keys_1.RestBindings.ROUTES;
|
|
435
|
-
const encodedPath = encodeURIComponent(r.path).replace(/\./g, '%2E');
|
|
436
|
-
return this.bind(`${namespace}.${r.verb} ${encodedPath}`)
|
|
437
|
-
.to(r)
|
|
438
|
-
.tag(keys_1.RestTags.REST_ROUTE)
|
|
439
|
-
.tag({ [keys_1.RestTags.ROUTE_VERB]: r.verb, [keys_1.RestTags.ROUTE_PATH]: r.path });
|
|
440
|
-
}
|
|
441
|
-
/**
|
|
442
|
-
* Register a route redirecting callers to a different URL.
|
|
443
|
-
*
|
|
444
|
-
* @example
|
|
445
|
-
* ```ts
|
|
446
|
-
* server.redirect('/explorer', '/explorer/');
|
|
447
|
-
* ```
|
|
448
|
-
*
|
|
449
|
-
* @param fromPath - URL path of the redirect endpoint
|
|
450
|
-
* @param toPathOrUrl - Location (URL path or full URL) where to redirect to.
|
|
451
|
-
* If your server is configured with a custom `basePath`, then the base path
|
|
452
|
-
* is prepended to the target location.
|
|
453
|
-
* @param statusCode - HTTP status code to respond with,
|
|
454
|
-
* defaults to 303 (See Other).
|
|
455
|
-
*/
|
|
456
|
-
redirect(fromPath, toPathOrUrl, statusCode) {
|
|
457
|
-
return this.route(new router_1.RedirectRoute(fromPath, this._basePath + toPathOrUrl, statusCode));
|
|
458
|
-
}
|
|
459
|
-
/**
|
|
460
|
-
* Mount static assets to the REST server.
|
|
461
|
-
* See https://expressjs.com/en/4x/api.html#express.static
|
|
462
|
-
* @param path - The path(s) to serve the asset.
|
|
463
|
-
* See examples at https://expressjs.com/en/4x/api.html#path-examples
|
|
464
|
-
* @param rootDir - The root directory from which to serve static assets
|
|
465
|
-
* @param options - Options for serve-static
|
|
466
|
-
*/
|
|
467
|
-
static(path, rootDir, options) {
|
|
468
|
-
this._externalRoutes.registerAssets(path, rootDir, options);
|
|
469
|
-
}
|
|
470
|
-
/**
|
|
471
|
-
* Set the OpenAPI specification that defines the REST API schema for this
|
|
472
|
-
* server. All routes, parameter definitions and return types will be defined
|
|
473
|
-
* in this way.
|
|
474
|
-
*
|
|
475
|
-
* Note that this will override any routes defined via decorators at the
|
|
476
|
-
* controller level (this function takes precedent).
|
|
477
|
-
*
|
|
478
|
-
* @param spec - The OpenAPI specification, as an object.
|
|
479
|
-
* @returns Binding for the spec
|
|
480
|
-
*
|
|
481
|
-
*/
|
|
482
|
-
api(spec) {
|
|
483
|
-
return this.bind(keys_1.RestBindings.API_SPEC).to(spec);
|
|
418
|
+
return this.route(new router_1.Route(routeOrVerb, path, spec, controllerCtorOrHandler));
|
|
484
419
|
}
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
* This method merges operations (HTTP endpoints) from the following sources:
|
|
490
|
-
* - `app.api(spec)`
|
|
491
|
-
* - `app.controller(MyController)`
|
|
492
|
-
* - `app.route(route)`
|
|
493
|
-
* - `app.route('get', '/greet', operationSpec, MyController, 'greet')`
|
|
494
|
-
*
|
|
495
|
-
* If the optional `requestContext` is provided, then the `servers` list
|
|
496
|
-
* in the returned spec will be updated to work in that context.
|
|
497
|
-
* Specifically:
|
|
498
|
-
* 1. if `config.openApi.setServersFromRequest` is enabled, the servers
|
|
499
|
-
* list will be replaced with the context base url
|
|
500
|
-
* 2. Any `servers` entries with a path of `/` will have that path
|
|
501
|
-
* replaced with `requestContext.basePath`
|
|
502
|
-
*
|
|
503
|
-
* @param requestContext - Optional context to update the `servers` list
|
|
504
|
-
* in the returned spec
|
|
505
|
-
*/
|
|
506
|
-
async getApiSpec(requestContext) {
|
|
507
|
-
let spec = await this.get(keys_1.RestBindings.API_SPEC);
|
|
508
|
-
const components = this.httpHandler.getApiComponents();
|
|
509
|
-
// Apply deep clone to prevent getApiSpec() callers from
|
|
510
|
-
// accidentally modifying our internal routing data
|
|
511
|
-
spec.paths = lodash_1.cloneDeep(this.httpHandler.describeApiPaths());
|
|
512
|
-
if (components) {
|
|
513
|
-
const defs = lodash_1.cloneDeep(components);
|
|
514
|
-
spec.components = { ...spec.components, ...defs };
|
|
515
|
-
}
|
|
516
|
-
router_spec_1.assignRouterSpec(spec, this._externalRoutes.routerSpec);
|
|
517
|
-
if (requestContext) {
|
|
518
|
-
spec = this.updateSpecFromRequest(spec, requestContext);
|
|
519
|
-
}
|
|
520
|
-
// Apply OAS enhancers to the OpenAPI specification
|
|
521
|
-
this.OASEnhancer.spec = spec;
|
|
522
|
-
spec = await this.OASEnhancer.applyAllEnhancers();
|
|
523
|
-
return spec;
|
|
524
|
-
}
|
|
525
|
-
/**
|
|
526
|
-
* Update or rebuild OpenAPI Spec object to be appropriate for the context of
|
|
527
|
-
* a specific request for the spec, leveraging both app config and request
|
|
528
|
-
* path information.
|
|
529
|
-
*
|
|
530
|
-
* @param spec base spec object from which to start
|
|
531
|
-
* @param requestContext request to use to infer path information
|
|
532
|
-
* @returns Updated or rebuilt spec object to use in the context of the request
|
|
533
|
-
*/
|
|
534
|
-
updateSpecFromRequest(spec, requestContext) {
|
|
535
|
-
if (this.config.openApiSpec.setServersFromRequest) {
|
|
536
|
-
spec = Object.assign({}, spec);
|
|
537
|
-
spec.servers = [{ url: requestContext.requestedBaseUrl }];
|
|
538
|
-
}
|
|
539
|
-
const basePath = requestContext.basePath;
|
|
540
|
-
if (spec.servers && basePath) {
|
|
541
|
-
for (const s of spec.servers) {
|
|
542
|
-
// Update the default server url to honor `basePath`
|
|
543
|
-
if (s.url === '/') {
|
|
544
|
-
s.url = basePath;
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
return spec;
|
|
420
|
+
if (!controllerCtorOrHandler) {
|
|
421
|
+
throw new assert_1.AssertionError({
|
|
422
|
+
message: 'controller is required for a controller-based route',
|
|
423
|
+
});
|
|
549
424
|
}
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
* ```ts
|
|
555
|
-
* class MySequence implements SequenceHandler {
|
|
556
|
-
* constructor(
|
|
557
|
-
* @inject('send) public send: Send)) {
|
|
558
|
-
* }
|
|
559
|
-
*
|
|
560
|
-
* public async handle({response}: RequestContext) {
|
|
561
|
-
* send(response, 'hello world');
|
|
562
|
-
* }
|
|
563
|
-
* }
|
|
564
|
-
* ```
|
|
565
|
-
*
|
|
566
|
-
* @param value - The sequence to invoke for each incoming request.
|
|
567
|
-
*/
|
|
568
|
-
sequence(value) {
|
|
569
|
-
this.bind(keys_1.RestBindings.SEQUENCE).toClass(value);
|
|
425
|
+
if (!methodName) {
|
|
426
|
+
throw new assert_1.AssertionError({
|
|
427
|
+
message: 'methodName is required for a controller-based route',
|
|
428
|
+
});
|
|
570
429
|
}
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
430
|
+
return this.route(new router_1.ControllerRoute(routeOrVerb, path, spec, controllerCtorOrHandler, controllerFactory, methodName));
|
|
431
|
+
}
|
|
432
|
+
bindRoute(r) {
|
|
433
|
+
const namespace = keys_1.RestBindings.ROUTES;
|
|
434
|
+
const encodedPath = encodeURIComponent(r.path).replace(/\./g, '%2E');
|
|
435
|
+
return this.bind(`${namespace}.${r.verb} ${encodedPath}`)
|
|
436
|
+
.to(r)
|
|
437
|
+
.tag(keys_1.RestTags.REST_ROUTE)
|
|
438
|
+
.tag({ [keys_1.RestTags.ROUTE_VERB]: r.verb, [keys_1.RestTags.ROUTE_PATH]: r.path });
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Register a route redirecting callers to a different URL.
|
|
442
|
+
*
|
|
443
|
+
* @example
|
|
444
|
+
* ```ts
|
|
445
|
+
* server.redirect('/explorer', '/explorer/');
|
|
446
|
+
* ```
|
|
447
|
+
*
|
|
448
|
+
* @param fromPath - URL path of the redirect endpoint
|
|
449
|
+
* @param toPathOrUrl - Location (URL path or full URL) where to redirect to.
|
|
450
|
+
* If your server is configured with a custom `basePath`, then the base path
|
|
451
|
+
* is prepended to the target location.
|
|
452
|
+
* @param statusCode - HTTP status code to respond with,
|
|
453
|
+
* defaults to 303 (See Other).
|
|
454
|
+
*/
|
|
455
|
+
redirect(fromPath, toPathOrUrl, statusCode) {
|
|
456
|
+
return this.route(new router_1.RedirectRoute(fromPath, this._basePath + toPathOrUrl, statusCode));
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* Mount static assets to the REST server.
|
|
460
|
+
* See https://expressjs.com/en/4x/api.html#express.static
|
|
461
|
+
* @param path - The path(s) to serve the asset.
|
|
462
|
+
* See examples at https://expressjs.com/en/4x/api.html#path-examples
|
|
463
|
+
* @param rootDir - The root directory from which to serve static assets
|
|
464
|
+
* @param options - Options for serve-static
|
|
465
|
+
*/
|
|
466
|
+
static(path, rootDir, options) {
|
|
467
|
+
this._externalRoutes.registerAssets(path, rootDir, options);
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Set the OpenAPI specification that defines the REST API schema for this
|
|
471
|
+
* server. All routes, parameter definitions and return types will be defined
|
|
472
|
+
* in this way.
|
|
473
|
+
*
|
|
474
|
+
* Note that this will override any routes defined via decorators at the
|
|
475
|
+
* controller level (this function takes precedent).
|
|
476
|
+
*
|
|
477
|
+
* @param spec - The OpenAPI specification, as an object.
|
|
478
|
+
* @returns Binding for the spec
|
|
479
|
+
*
|
|
480
|
+
*/
|
|
481
|
+
api(spec) {
|
|
482
|
+
return this.bind(keys_1.RestBindings.API_SPEC).to(spec);
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Get the OpenAPI specification describing the REST API provided by
|
|
486
|
+
* this application.
|
|
487
|
+
*
|
|
488
|
+
* This method merges operations (HTTP endpoints) from the following sources:
|
|
489
|
+
* - `app.api(spec)`
|
|
490
|
+
* - `app.controller(MyController)`
|
|
491
|
+
* - `app.route(route)`
|
|
492
|
+
* - `app.route('get', '/greet', operationSpec, MyController, 'greet')`
|
|
493
|
+
*
|
|
494
|
+
* If the optional `requestContext` is provided, then the `servers` list
|
|
495
|
+
* in the returned spec will be updated to work in that context.
|
|
496
|
+
* Specifically:
|
|
497
|
+
* 1. if `config.openApi.setServersFromRequest` is enabled, the servers
|
|
498
|
+
* list will be replaced with the context base url
|
|
499
|
+
* 2. Any `servers` entries with a path of `/` will have that path
|
|
500
|
+
* replaced with `requestContext.basePath`
|
|
501
|
+
*
|
|
502
|
+
* @param requestContext - Optional context to update the `servers` list
|
|
503
|
+
* in the returned spec
|
|
504
|
+
*/
|
|
505
|
+
async getApiSpec(requestContext) {
|
|
506
|
+
let spec = await this.get(keys_1.RestBindings.API_SPEC);
|
|
507
|
+
const components = this.httpHandler.getApiComponents();
|
|
508
|
+
// Apply deep clone to prevent getApiSpec() callers from
|
|
509
|
+
// accidentally modifying our internal routing data
|
|
510
|
+
spec.paths = lodash_1.cloneDeep(this.httpHandler.describeApiPaths());
|
|
511
|
+
if (components) {
|
|
512
|
+
const defs = lodash_1.cloneDeep(components);
|
|
513
|
+
spec.components = { ...spec.components, ...defs };
|
|
514
|
+
}
|
|
515
|
+
router_spec_1.assignRouterSpec(spec, this._externalRoutes.routerSpec);
|
|
516
|
+
if (requestContext) {
|
|
517
|
+
spec = this.updateSpecFromRequest(spec, requestContext);
|
|
518
|
+
}
|
|
519
|
+
// Apply OAS enhancers to the OpenAPI specification
|
|
520
|
+
this.OASEnhancer.spec = spec;
|
|
521
|
+
spec = await this.OASEnhancer.applyAllEnhancers();
|
|
522
|
+
return spec;
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Update or rebuild OpenAPI Spec object to be appropriate for the context of
|
|
526
|
+
* a specific request for the spec, leveraging both app config and request
|
|
527
|
+
* path information.
|
|
528
|
+
*
|
|
529
|
+
* @param spec base spec object from which to start
|
|
530
|
+
* @param requestContext request to use to infer path information
|
|
531
|
+
* @returns Updated or rebuilt spec object to use in the context of the request
|
|
532
|
+
*/
|
|
533
|
+
updateSpecFromRequest(spec, requestContext) {
|
|
534
|
+
if (this.config.openApiSpec.setServersFromRequest) {
|
|
535
|
+
spec = Object.assign({}, spec);
|
|
536
|
+
spec.servers = [{ url: requestContext.requestedBaseUrl }];
|
|
537
|
+
}
|
|
538
|
+
const basePath = requestContext.basePath;
|
|
539
|
+
if (spec.servers && basePath) {
|
|
540
|
+
for (const s of spec.servers) {
|
|
541
|
+
// Update the default server url to honor `basePath`
|
|
542
|
+
if (s.url === '/') {
|
|
543
|
+
s.url = basePath;
|
|
587
544
|
}
|
|
588
545
|
}
|
|
589
|
-
this.sequence(SequenceFromFunction);
|
|
590
|
-
}
|
|
591
|
-
/**
|
|
592
|
-
* Bind a body parser to the server context
|
|
593
|
-
* @param parserClass - Body parser class
|
|
594
|
-
* @param address - Optional binding address
|
|
595
|
-
*/
|
|
596
|
-
bodyParser(bodyParserClass, address) {
|
|
597
|
-
const binding = createBodyParserBinding(bodyParserClass, address);
|
|
598
|
-
this.add(binding);
|
|
599
|
-
return binding;
|
|
600
|
-
}
|
|
601
|
-
/**
|
|
602
|
-
* Configure the `basePath` for the rest server
|
|
603
|
-
* @param path - Base path
|
|
604
|
-
*/
|
|
605
|
-
basePath(path = '') {
|
|
606
|
-
if (this._requestHandler != null) {
|
|
607
|
-
throw new Error('Base path cannot be set as the request handler has been created');
|
|
608
|
-
}
|
|
609
|
-
// Trim leading and trailing `/`
|
|
610
|
-
path = path.replace(/(^\/)|(\/$)/, '');
|
|
611
|
-
if (path)
|
|
612
|
-
path = '/' + path;
|
|
613
|
-
this._basePath = path;
|
|
614
|
-
this.config.basePath = path;
|
|
615
|
-
}
|
|
616
|
-
/**
|
|
617
|
-
* Start this REST API's HTTP/HTTPS server.
|
|
618
|
-
*/
|
|
619
|
-
async start() {
|
|
620
|
-
// Set up the Express app if not done yet
|
|
621
|
-
this._setupRequestHandlerIfNeeded();
|
|
622
|
-
// Setup the HTTP handler so that we can verify the configuration
|
|
623
|
-
// of API spec, controllers and routes at startup time.
|
|
624
|
-
this._setupHandlerIfNeeded();
|
|
625
|
-
const port = await this.get(keys_1.RestBindings.PORT);
|
|
626
|
-
const host = await this.get(keys_1.RestBindings.HOST);
|
|
627
|
-
const path = await this.get(keys_1.RestBindings.PATH);
|
|
628
|
-
const protocol = await this.get(keys_1.RestBindings.PROTOCOL);
|
|
629
|
-
const httpsOptions = await this.get(keys_1.RestBindings.HTTPS_OPTIONS);
|
|
630
|
-
if (this.config.listenOnStart === false) {
|
|
631
|
-
debug('RestServer is not listening as listenOnStart flag is set to false.');
|
|
632
|
-
return;
|
|
633
|
-
}
|
|
634
|
-
const serverOptions = {};
|
|
635
|
-
if (protocol === 'https')
|
|
636
|
-
Object.assign(serverOptions, httpsOptions);
|
|
637
|
-
Object.assign(serverOptions, { port, host, protocol, path });
|
|
638
|
-
this._httpServer = new http_server_1.HttpServer(this.requestHandler, serverOptions);
|
|
639
|
-
await this._httpServer.start();
|
|
640
|
-
this.bind(keys_1.RestBindings.PORT).to(this._httpServer.port);
|
|
641
|
-
this.bind(keys_1.RestBindings.HOST).to(this._httpServer.host);
|
|
642
|
-
this.bind(keys_1.RestBindings.URL).to(this._httpServer.url);
|
|
643
|
-
debug('RestServer listening at %s', this._httpServer.url);
|
|
644
|
-
}
|
|
645
|
-
/**
|
|
646
|
-
* Stop this REST API's HTTP/HTTPS server.
|
|
647
|
-
*/
|
|
648
|
-
async stop() {
|
|
649
|
-
// Kill the server instance.
|
|
650
|
-
if (!this._httpServer)
|
|
651
|
-
return;
|
|
652
|
-
await this._httpServer.stop();
|
|
653
|
-
this._httpServer = undefined;
|
|
654
546
|
}
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
547
|
+
return spec;
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* Configure a custom sequence class for handling incoming requests.
|
|
551
|
+
*
|
|
552
|
+
* @example
|
|
553
|
+
* ```ts
|
|
554
|
+
* class MySequence implements SequenceHandler {
|
|
555
|
+
* constructor(
|
|
556
|
+
* @inject('send) public send: Send)) {
|
|
557
|
+
* }
|
|
558
|
+
*
|
|
559
|
+
* public async handle({response}: RequestContext) {
|
|
560
|
+
* send(response, 'hello world');
|
|
561
|
+
* }
|
|
562
|
+
* }
|
|
563
|
+
* ```
|
|
564
|
+
*
|
|
565
|
+
* @param value - The sequence to invoke for each incoming request.
|
|
566
|
+
*/
|
|
567
|
+
sequence(value) {
|
|
568
|
+
this.bind(keys_1.RestBindings.SEQUENCE).toClass(value);
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Configure a custom sequence function for handling incoming requests.
|
|
572
|
+
*
|
|
573
|
+
* @example
|
|
574
|
+
* ```ts
|
|
575
|
+
* app.handler(({request, response}, sequence) => {
|
|
576
|
+
* sequence.send(response, 'hello world');
|
|
577
|
+
* });
|
|
578
|
+
* ```
|
|
579
|
+
*
|
|
580
|
+
* @param handlerFn - The handler to invoke for each incoming request.
|
|
581
|
+
*/
|
|
582
|
+
handler(handlerFn) {
|
|
583
|
+
class SequenceFromFunction extends sequence_1.DefaultSequence {
|
|
584
|
+
async handle(context) {
|
|
585
|
+
return handlerFn(context, this);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
this.sequence(SequenceFromFunction);
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* Bind a body parser to the server context
|
|
592
|
+
* @param parserClass - Body parser class
|
|
593
|
+
* @param address - Optional binding address
|
|
594
|
+
*/
|
|
595
|
+
bodyParser(bodyParserClass, address) {
|
|
596
|
+
const binding = createBodyParserBinding(bodyParserClass, address);
|
|
597
|
+
this.add(binding);
|
|
598
|
+
return binding;
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* Configure the `basePath` for the rest server
|
|
602
|
+
* @param path - Base path
|
|
603
|
+
*/
|
|
604
|
+
basePath(path = '') {
|
|
605
|
+
if (this._requestHandler != null) {
|
|
606
|
+
throw new Error('Base path cannot be set as the request handler has been created');
|
|
607
|
+
}
|
|
608
|
+
// Trim leading and trailing `/`
|
|
609
|
+
path = path.replace(/(^\/)|(\/$)/, '');
|
|
610
|
+
if (path)
|
|
611
|
+
path = '/' + path;
|
|
612
|
+
this._basePath = path;
|
|
613
|
+
this.config.basePath = path;
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* Start this REST API's HTTP/HTTPS server.
|
|
617
|
+
*/
|
|
618
|
+
async start() {
|
|
619
|
+
// Set up the Express app if not done yet
|
|
620
|
+
this._setupRequestHandlerIfNeeded();
|
|
621
|
+
// Setup the HTTP handler so that we can verify the configuration
|
|
622
|
+
// of API spec, controllers and routes at startup time.
|
|
623
|
+
this._setupHandlerIfNeeded();
|
|
624
|
+
const port = await this.get(keys_1.RestBindings.PORT);
|
|
625
|
+
const host = await this.get(keys_1.RestBindings.HOST);
|
|
626
|
+
const path = await this.get(keys_1.RestBindings.PATH);
|
|
627
|
+
const protocol = await this.get(keys_1.RestBindings.PROTOCOL);
|
|
628
|
+
const httpsOptions = await this.get(keys_1.RestBindings.HTTPS_OPTIONS);
|
|
629
|
+
if (this.config.listenOnStart === false) {
|
|
630
|
+
debug('RestServer is not listening as listenOnStart flag is set to false.');
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
const serverOptions = {};
|
|
634
|
+
if (protocol === 'https')
|
|
635
|
+
Object.assign(serverOptions, httpsOptions);
|
|
636
|
+
Object.assign(serverOptions, { port, host, protocol, path });
|
|
637
|
+
this._httpServer = new http_server_1.HttpServer(this.requestHandler, serverOptions);
|
|
638
|
+
await this._httpServer.start();
|
|
639
|
+
this.bind(keys_1.RestBindings.PORT).to(this._httpServer.port);
|
|
640
|
+
this.bind(keys_1.RestBindings.HOST).to(this._httpServer.host);
|
|
641
|
+
this.bind(keys_1.RestBindings.URL).to(this._httpServer.url);
|
|
642
|
+
debug('RestServer listening at %s', this._httpServer.url);
|
|
643
|
+
}
|
|
644
|
+
/**
|
|
645
|
+
* Stop this REST API's HTTP/HTTPS server.
|
|
646
|
+
*/
|
|
647
|
+
async stop() {
|
|
648
|
+
// Kill the server instance.
|
|
649
|
+
if (!this._httpServer)
|
|
650
|
+
return;
|
|
651
|
+
await this._httpServer.stop();
|
|
652
|
+
this._httpServer = undefined;
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* Mount an Express router to expose additional REST endpoints handled
|
|
656
|
+
* via legacy Express-based stack.
|
|
657
|
+
*
|
|
658
|
+
* @param basePath - Path where to mount the router at, e.g. `/` or `/api`.
|
|
659
|
+
* @param router - The Express router to handle the requests.
|
|
660
|
+
* @param spec - A partial OpenAPI spec describing endpoints provided by the
|
|
661
|
+
* router. LoopBack will prepend `basePath` to all endpoints automatically.
|
|
662
|
+
* This argument is optional. You can leave it out if you don't want to
|
|
663
|
+
* document the routes.
|
|
664
|
+
*/
|
|
665
|
+
mountExpressRouter(basePath, router, spec) {
|
|
666
|
+
this._externalRoutes.mountRouter(basePath, router, spec);
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* Export the OpenAPI spec to the given json or yaml file
|
|
670
|
+
* @param outFile - File name for the spec. The extension of the file
|
|
671
|
+
* determines the format of the file.
|
|
672
|
+
* - `yaml` or `yml`: YAML
|
|
673
|
+
* - `json` or other: JSON
|
|
674
|
+
* If the outFile is not provided or its value is `''` or `'-'`, the spec is
|
|
675
|
+
* written to the console using the `log` function.
|
|
676
|
+
* @param log - Log function, default to `console.log`
|
|
677
|
+
*/
|
|
678
|
+
async exportOpenApiSpec(outFile = '', log = console.log) {
|
|
679
|
+
const spec = await this.getApiSpec();
|
|
680
|
+
if (outFile === '-' || outFile === '') {
|
|
681
|
+
const json = JSON.stringify(spec, null, 2);
|
|
682
|
+
log('%s', json);
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
const fileName = outFile.toLowerCase();
|
|
686
|
+
if (fileName.endsWith('.yaml') || fileName.endsWith('.yml')) {
|
|
687
|
+
const yaml = js_yaml_1.safeDump(spec);
|
|
688
|
+
fs_1.default.writeFileSync(outFile, yaml, 'utf-8');
|
|
689
|
+
}
|
|
690
|
+
else {
|
|
691
|
+
const json = JSON.stringify(spec, null, 2);
|
|
692
|
+
fs_1.default.writeFileSync(outFile, json, 'utf-8');
|
|
693
|
+
}
|
|
694
|
+
log('The OpenAPI spec has been saved to %s.', outFile);
|
|
695
|
+
}
|
|
696
|
+
};
|
|
697
|
+
RestServer = tslib_1.__decorate([
|
|
698
|
+
tslib_1.__param(0, core_1.inject(core_1.CoreBindings.APPLICATION_INSTANCE)),
|
|
699
|
+
tslib_1.__param(1, core_1.inject(keys_1.RestBindings.CONFIG, { optional: true })),
|
|
700
|
+
tslib_1.__metadata("design:paramtypes", [core_1.Application, Object])
|
|
701
|
+
], RestServer);
|
|
705
702
|
exports.RestServer = RestServer;
|
|
706
703
|
/**
|
|
707
704
|
* An assertion type guard for TypeScript to instruct the compiler that the
|
|
@@ -719,9 +716,9 @@ function assertExists(val, name) {
|
|
|
719
716
|
*/
|
|
720
717
|
function createBodyParserBinding(parserClass, key) {
|
|
721
718
|
const address = key !== null && key !== void 0 ? key : `${keys_1.RestBindings.REQUEST_BODY_PARSER}.${parserClass.name}`;
|
|
722
|
-
return
|
|
719
|
+
return core_1.Binding.bind(address)
|
|
723
720
|
.toClass(parserClass)
|
|
724
|
-
.inScope(
|
|
721
|
+
.inScope(core_1.BindingScope.TRANSIENT)
|
|
725
722
|
.tag(body_parsers_1.REQUEST_BODY_PARSER_TAG);
|
|
726
723
|
}
|
|
727
724
|
exports.createBodyParserBinding = createBodyParserBinding;
|