@loopback/rest 5.0.1 → 5.2.0

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