@loopback/rest 5.2.0 → 6.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.
- package/CHANGELOG.md +73 -0
- package/README.md +4 -0
- package/dist/body-parsers/body-parser.js +3 -0
- package/dist/body-parsers/body-parser.js.map +1 -1
- package/dist/coercion/coerce-parameter.js +3 -2
- package/dist/coercion/coerce-parameter.js.map +1 -1
- package/dist/coercion/utils.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/keys.d.ts +18 -3
- package/dist/keys.js +18 -1
- package/dist/keys.js.map +1 -1
- package/dist/parser.d.ts +2 -2
- package/dist/parser.js +3 -2
- package/dist/parser.js.map +1 -1
- package/dist/providers/find-route.provider.d.ts +7 -1
- package/dist/providers/find-route.provider.js +29 -1
- package/dist/providers/find-route.provider.js.map +1 -1
- package/dist/providers/invoke-method.provider.d.ts +7 -1
- package/dist/providers/invoke-method.provider.js +43 -1
- package/dist/providers/invoke-method.provider.js.map +1 -1
- package/dist/providers/parse-params.provider.d.ts +8 -2
- package/dist/providers/parse-params.provider.js +32 -2
- package/dist/providers/parse-params.provider.js.map +1 -1
- package/dist/providers/send.provider.d.ts +10 -2
- package/dist/providers/send.provider.js +46 -1
- package/dist/providers/send.provider.js.map +1 -1
- package/dist/request-context.js.map +1 -1
- package/dist/rest-http-error.d.ts +3 -1
- package/dist/rest-http-error.js +3 -2
- package/dist/rest-http-error.js.map +1 -1
- package/dist/rest.application.js +1 -1
- package/dist/rest.application.js.map +1 -1
- package/dist/rest.component.d.ts +1 -2
- package/dist/rest.component.js +23 -7
- package/dist/rest.component.js.map +1 -1
- package/dist/rest.server.d.ts +8 -4
- package/dist/rest.server.js +25 -12
- package/dist/rest.server.js.map +1 -1
- package/dist/router/base-route.js +3 -3
- package/dist/router/base-route.js.map +1 -1
- package/dist/router/controller-route.js +1 -1
- package/dist/router/controller-route.js.map +1 -1
- package/dist/router/handler-route.js +1 -1
- package/dist/router/handler-route.js.map +1 -1
- package/dist/router/redirect-route.js +1 -1
- package/dist/router/redirect-route.js.map +1 -1
- package/dist/sequence.d.ts +84 -2
- package/dist/sequence.js +135 -2
- package/dist/sequence.js.map +1 -1
- package/dist/types.d.ts +9 -4
- package/dist/validation/ajv-factory.provider.d.ts +3 -2
- package/dist/validation/ajv-factory.provider.js +7 -2
- package/dist/validation/ajv-factory.provider.js.map +1 -1
- package/dist/validation/request-body.validator.d.ts +2 -2
- package/dist/validation/request-body.validator.js +11 -13
- package/dist/validation/request-body.validator.js.map +1 -1
- package/package.json +25 -25
- package/src/body-parsers/body-parser.ts +3 -0
- package/src/coercion/coerce-parameter.ts +4 -3
- package/src/keys.ts +34 -3
- package/src/parser.ts +4 -3
- package/src/providers/find-route.provider.ts +36 -3
- package/src/providers/invoke-method.provider.ts +47 -3
- package/src/providers/parse-params.provider.ts +36 -9
- package/src/providers/send.provider.ts +45 -2
- package/src/request-context.ts +2 -1
- package/src/rest-http-error.ts +6 -2
- package/src/rest.application.ts +1 -1
- package/src/rest.component.ts +40 -10
- package/src/rest.server.ts +50 -16
- package/src/router/base-route.ts +3 -3
- package/src/router/controller-route.ts +1 -1
- package/src/router/handler-route.ts +3 -1
- package/src/router/redirect-route.ts +1 -1
- package/src/sequence.ts +174 -4
- package/src/types.ts +13 -4
- package/src/validation/ajv-factory.provider.ts +10 -8
- package/src/validation/request-body.validator.ts +28 -24
package/src/rest.server.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
ContextObserver,
|
|
13
13
|
CoreBindings,
|
|
14
14
|
createBindingFromClass,
|
|
15
|
+
extensionFor,
|
|
15
16
|
filterByKey,
|
|
16
17
|
filterByTag,
|
|
17
18
|
inject,
|
|
@@ -61,7 +62,13 @@ import {
|
|
|
61
62
|
RoutingTable,
|
|
62
63
|
} from './router';
|
|
63
64
|
import {assignRouterSpec} from './router/router-spec';
|
|
64
|
-
import {
|
|
65
|
+
import {
|
|
66
|
+
DefaultSequence,
|
|
67
|
+
MiddlewareSequence,
|
|
68
|
+
RestMiddlewareGroups,
|
|
69
|
+
SequenceFunction,
|
|
70
|
+
SequenceHandler,
|
|
71
|
+
} from './sequence';
|
|
65
72
|
import {Request, RequestBodyParserOptions, Response} from './types';
|
|
66
73
|
|
|
67
74
|
const debug = debugFactory('loopback:rest:server');
|
|
@@ -104,7 +111,8 @@ const SequenceActions = RestBindings.SequenceActions;
|
|
|
104
111
|
* const server = await app.get('servers.foo');
|
|
105
112
|
* ```
|
|
106
113
|
*/
|
|
107
|
-
export class RestServer
|
|
114
|
+
export class RestServer
|
|
115
|
+
extends BaseMiddlewareRegistry
|
|
108
116
|
implements Server, HttpServerLike {
|
|
109
117
|
/**
|
|
110
118
|
* Handle incoming HTTP(S) request by invoking the corresponding
|
|
@@ -163,6 +171,10 @@ export class RestServer extends BaseMiddlewareRegistry
|
|
|
163
171
|
return this._httpServer ? this._httpServer.listening : false;
|
|
164
172
|
}
|
|
165
173
|
|
|
174
|
+
get httpServer(): HttpServer | undefined {
|
|
175
|
+
return this._httpServer;
|
|
176
|
+
}
|
|
177
|
+
|
|
166
178
|
/**
|
|
167
179
|
* The base url for the server, including the basePath if set. For example,
|
|
168
180
|
* the value will be 'http://localhost:3000/api' if `basePath` is set to
|
|
@@ -216,6 +228,8 @@ export class RestServer extends BaseMiddlewareRegistry
|
|
|
216
228
|
|
|
217
229
|
if (config.sequence) {
|
|
218
230
|
this.sequence(config.sequence);
|
|
231
|
+
} else {
|
|
232
|
+
this.sequence(MiddlewareSequence);
|
|
219
233
|
}
|
|
220
234
|
|
|
221
235
|
if (config.router) {
|
|
@@ -249,8 +263,13 @@ export class RestServer extends BaseMiddlewareRegistry
|
|
|
249
263
|
this.expressMiddleware(cors, this.config.cors, {
|
|
250
264
|
injectConfiguration: false,
|
|
251
265
|
key: 'middleware.cors',
|
|
252
|
-
group:
|
|
253
|
-
})
|
|
266
|
+
group: RestMiddlewareGroups.CORS,
|
|
267
|
+
}).apply(
|
|
268
|
+
extensionFor(
|
|
269
|
+
RestTags.REST_MIDDLEWARE_CHAIN,
|
|
270
|
+
RestTags.ACTION_MIDDLEWARE_CHAIN,
|
|
271
|
+
),
|
|
272
|
+
);
|
|
254
273
|
|
|
255
274
|
// Set up endpoints for OpenAPI spec/ui
|
|
256
275
|
this._setupOpenApiSpecEndpoints();
|
|
@@ -323,8 +342,14 @@ export class RestServer extends BaseMiddlewareRegistry
|
|
|
323
342
|
this._redirectToSwaggerUI(req, res, next),
|
|
324
343
|
);
|
|
325
344
|
this.expressMiddleware('middleware.apiSpec.defaults', router, {
|
|
326
|
-
group:
|
|
327
|
-
|
|
345
|
+
group: RestMiddlewareGroups.API_SPEC,
|
|
346
|
+
upstreamGroups: RestMiddlewareGroups.CORS,
|
|
347
|
+
}).apply(
|
|
348
|
+
extensionFor(
|
|
349
|
+
RestTags.REST_MIDDLEWARE_CHAIN,
|
|
350
|
+
RestTags.ACTION_MIDDLEWARE_CHAIN,
|
|
351
|
+
),
|
|
352
|
+
);
|
|
328
353
|
}
|
|
329
354
|
|
|
330
355
|
/**
|
|
@@ -445,6 +470,9 @@ export class RestServer extends BaseMiddlewareRegistry
|
|
|
445
470
|
|
|
446
471
|
// TODO(bajtos) should we support API spec defined asynchronously?
|
|
447
472
|
const spec: OpenApiSpec = this.getSync(RestBindings.API_SPEC);
|
|
473
|
+
if (spec.components) {
|
|
474
|
+
this._httpHandler.registerApiComponents(spec.components);
|
|
475
|
+
}
|
|
448
476
|
for (const path in spec.paths) {
|
|
449
477
|
for (const verb in spec.paths[path]) {
|
|
450
478
|
const routeSpec: OperationObject = spec.paths[path][verb];
|
|
@@ -797,11 +825,13 @@ export class RestServer extends BaseMiddlewareRegistry
|
|
|
797
825
|
*/
|
|
798
826
|
async getApiSpec(requestContext?: RequestContext): Promise<OpenApiSpec> {
|
|
799
827
|
let spec = await this.get<OpenApiSpec>(RestBindings.API_SPEC);
|
|
828
|
+
spec = cloneDeep(spec);
|
|
800
829
|
const components = this.httpHandler.getApiComponents();
|
|
801
830
|
|
|
802
831
|
// Apply deep clone to prevent getApiSpec() callers from
|
|
803
832
|
// accidentally modifying our internal routing data
|
|
804
|
-
|
|
833
|
+
const paths = cloneDeep(this.httpHandler.describeApiPaths());
|
|
834
|
+
spec.paths = {...paths, ...spec.paths};
|
|
805
835
|
if (components) {
|
|
806
836
|
const defs = cloneDeep(components);
|
|
807
837
|
spec.components = {...spec.components, ...defs};
|
|
@@ -867,10 +897,14 @@ export class RestServer extends BaseMiddlewareRegistry
|
|
|
867
897
|
* }
|
|
868
898
|
* ```
|
|
869
899
|
*
|
|
870
|
-
* @param
|
|
900
|
+
* @param sequenceClass - The sequence class to invoke for each incoming request.
|
|
871
901
|
*/
|
|
872
|
-
public sequence(
|
|
873
|
-
|
|
902
|
+
public sequence(sequenceClass: Constructor<SequenceHandler>) {
|
|
903
|
+
const sequenceBinding = createBindingFromClass(sequenceClass, {
|
|
904
|
+
key: RestBindings.SEQUENCE,
|
|
905
|
+
});
|
|
906
|
+
this.add(sequenceBinding);
|
|
907
|
+
return sequenceBinding;
|
|
874
908
|
}
|
|
875
909
|
|
|
876
910
|
/**
|
|
@@ -949,10 +983,7 @@ export class RestServer extends BaseMiddlewareRegistry
|
|
|
949
983
|
return;
|
|
950
984
|
}
|
|
951
985
|
|
|
952
|
-
const serverOptions = {};
|
|
953
|
-
if (protocol === 'https') Object.assign(serverOptions, httpsOptions);
|
|
954
|
-
Object.assign(serverOptions, {port, host, protocol, path});
|
|
955
|
-
|
|
986
|
+
const serverOptions = {...httpsOptions, port, host, protocol, path};
|
|
956
987
|
this._httpServer = new HttpServer(this.requestHandler, serverOptions);
|
|
957
988
|
|
|
958
989
|
await this._httpServer.start();
|
|
@@ -1067,12 +1098,15 @@ const OPENAPI_SPEC_MAPPING: {[key: string]: OpenApiSpecForm} = {
|
|
|
1067
1098
|
export interface OpenApiSpecOptions {
|
|
1068
1099
|
/**
|
|
1069
1100
|
* Mapping of urls to spec forms, by default:
|
|
1070
|
-
*
|
|
1101
|
+
* <br>
|
|
1071
1102
|
* {
|
|
1103
|
+
* <br>
|
|
1072
1104
|
* '/openapi.json': {version: '3.0.0', format: 'json'},
|
|
1105
|
+
* <br>
|
|
1073
1106
|
* '/openapi.yaml': {version: '3.0.0', format: 'yaml'},
|
|
1107
|
+
* <br>
|
|
1074
1108
|
* }
|
|
1075
|
-
*
|
|
1109
|
+
*
|
|
1076
1110
|
*/
|
|
1077
1111
|
endpointMapping?: {[key: string]: OpenApiSpecForm};
|
|
1078
1112
|
|
package/src/router/base-route.ts
CHANGED
|
@@ -36,11 +36,11 @@ export abstract class BaseRoute implements RouteEntry {
|
|
|
36
36
|
): Promise<OperationRetval>;
|
|
37
37
|
|
|
38
38
|
describe(): string {
|
|
39
|
-
return
|
|
39
|
+
return `${this.verb} ${this.path}`;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
toString() {
|
|
43
|
-
return `${this.constructor.name} - ${this.
|
|
43
|
+
return `${this.constructor.name} - ${this.describe()}`;
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
|
|
@@ -48,6 +48,6 @@ export class RouteSource implements InvocationSource<RouteEntry> {
|
|
|
48
48
|
type = 'route';
|
|
49
49
|
constructor(readonly value: RouteEntry) {}
|
|
50
50
|
toString() {
|
|
51
|
-
return
|
|
51
|
+
return this.value.toString();
|
|
52
52
|
}
|
|
53
53
|
}
|
|
@@ -101,7 +101,7 @@ export class ControllerRoute<T> extends BaseRoute {
|
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
describe(): string {
|
|
104
|
-
return `${this._controllerName}.${this._methodName}`;
|
|
104
|
+
return `${super.describe()} => ${this._controllerName}.${this._methodName}`;
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
updateBindings(requestContext: Context) {
|
|
@@ -20,7 +20,9 @@ export class Route extends BaseRoute {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
describe(): string {
|
|
23
|
-
return
|
|
23
|
+
return `${super.describe()} => ${
|
|
24
|
+
this._handler.name || this._handler.toString()
|
|
25
|
+
}`;
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
updateBindings(requestContext: Context) {
|
|
@@ -42,7 +42,7 @@ export class RedirectRoute implements RouteEntry, ResolvedRoute {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
describe(): string {
|
|
45
|
-
return `
|
|
45
|
+
return `Redirect: "${this.sourcePath}" => "${this.targetLocation}"`;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
/**
|
package/src/sequence.ts
CHANGED
|
@@ -3,12 +3,25 @@
|
|
|
3
3
|
// This file is licensed under the MIT License.
|
|
4
4
|
// License text available at https://opensource.org/licenses/MIT
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
import {
|
|
7
|
+
bind,
|
|
8
|
+
BindingScope,
|
|
9
|
+
config,
|
|
10
|
+
Context,
|
|
11
|
+
inject,
|
|
12
|
+
ValueOrPromise,
|
|
13
|
+
} from '@loopback/core';
|
|
14
|
+
import {
|
|
15
|
+
InvokeMiddleware,
|
|
16
|
+
InvokeMiddlewareOptions,
|
|
17
|
+
MiddlewareGroups,
|
|
18
|
+
MiddlewareView,
|
|
19
|
+
} from '@loopback/express';
|
|
20
|
+
import debugFactory from 'debug';
|
|
21
|
+
import {RestBindings, RestTags} from './keys';
|
|
10
22
|
import {RequestContext} from './request-context';
|
|
11
23
|
import {FindRoute, InvokeMethod, ParseParams, Reject, Send} from './types';
|
|
24
|
+
const debug = debugFactory('loopback:rest:sequence');
|
|
12
25
|
|
|
13
26
|
const SequenceActions = RestBindings.SequenceActions;
|
|
14
27
|
|
|
@@ -121,3 +134,160 @@ export class DefaultSequence implements SequenceHandler {
|
|
|
121
134
|
}
|
|
122
135
|
}
|
|
123
136
|
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Built-in middleware groups for the REST sequence
|
|
140
|
+
*/
|
|
141
|
+
export namespace RestMiddlewareGroups {
|
|
142
|
+
/**
|
|
143
|
+
* Invoke downstream middleware to get the result or catch errors so that it
|
|
144
|
+
* can produce the http response
|
|
145
|
+
*/
|
|
146
|
+
export const SEND_RESPONSE = 'sendResponse';
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Enforce CORS
|
|
150
|
+
*/
|
|
151
|
+
export const CORS = MiddlewareGroups.CORS;
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Server OpenAPI specs
|
|
155
|
+
*/
|
|
156
|
+
export const API_SPEC = MiddlewareGroups.API_SPEC;
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Default middleware group
|
|
160
|
+
*/
|
|
161
|
+
export const MIDDLEWARE = MiddlewareGroups.MIDDLEWARE;
|
|
162
|
+
export const DEFAULT = MIDDLEWARE;
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Find the route that can serve the request
|
|
166
|
+
*/
|
|
167
|
+
export const FIND_ROUTE = 'findRoute';
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Perform authentication
|
|
171
|
+
*/
|
|
172
|
+
export const AUTHENTICATION = 'authentication';
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Parse the http request to extract parameter values for the operation
|
|
176
|
+
*/
|
|
177
|
+
export const PARSE_PARAMS = 'parseParams';
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Invoke the target controller method or handler function
|
|
181
|
+
*/
|
|
182
|
+
export const INVOKE_METHOD = 'invokeMethod';
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* A sequence implementation using middleware chains
|
|
187
|
+
*/
|
|
188
|
+
@bind({scope: BindingScope.SINGLETON})
|
|
189
|
+
export class MiddlewareSequence implements SequenceHandler {
|
|
190
|
+
private middlewareView: MiddlewareView;
|
|
191
|
+
|
|
192
|
+
static defaultOptions: InvokeMiddlewareOptions = {
|
|
193
|
+
chain: RestTags.REST_MIDDLEWARE_CHAIN,
|
|
194
|
+
orderedGroups: [
|
|
195
|
+
// Please note that middleware is cascading. The `sendResponse` is
|
|
196
|
+
// added first to invoke downstream middleware to get the result or
|
|
197
|
+
// catch errors so that it can produce the http response.
|
|
198
|
+
RestMiddlewareGroups.SEND_RESPONSE,
|
|
199
|
+
|
|
200
|
+
RestMiddlewareGroups.CORS,
|
|
201
|
+
RestMiddlewareGroups.API_SPEC,
|
|
202
|
+
RestMiddlewareGroups.MIDDLEWARE,
|
|
203
|
+
|
|
204
|
+
RestMiddlewareGroups.FIND_ROUTE,
|
|
205
|
+
|
|
206
|
+
// authentication depends on the route
|
|
207
|
+
RestMiddlewareGroups.AUTHENTICATION,
|
|
208
|
+
|
|
209
|
+
RestMiddlewareGroups.PARSE_PARAMS,
|
|
210
|
+
|
|
211
|
+
RestMiddlewareGroups.INVOKE_METHOD,
|
|
212
|
+
],
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Reports an error if there are middleware groups are unreachable as they
|
|
216
|
+
* are ordered after the `invokeMethod` group.
|
|
217
|
+
*/
|
|
218
|
+
validate: groups => {
|
|
219
|
+
const index = groups.indexOf(RestMiddlewareGroups.INVOKE_METHOD);
|
|
220
|
+
if (index !== -1) {
|
|
221
|
+
const unreachableGroups = groups.slice(index + 1);
|
|
222
|
+
if (unreachableGroups.length > 0) {
|
|
223
|
+
throw new Error(
|
|
224
|
+
`Middleware groups "${unreachableGroups.join(
|
|
225
|
+
',',
|
|
226
|
+
)}" are not invoked as they are ordered after "${
|
|
227
|
+
RestMiddlewareGroups.INVOKE_METHOD
|
|
228
|
+
}"`,
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Constructor: Injects `InvokeMiddleware` and `InvokeMiddlewareOptions`
|
|
237
|
+
*
|
|
238
|
+
* @param invokeMiddleware - invoker for registered middleware in a chain.
|
|
239
|
+
* To be injected via RestBindings.INVOKE_MIDDLEWARE_SERVICE.
|
|
240
|
+
*/
|
|
241
|
+
constructor(
|
|
242
|
+
@inject.context()
|
|
243
|
+
context: Context,
|
|
244
|
+
|
|
245
|
+
@inject(RestBindings.INVOKE_MIDDLEWARE_SERVICE)
|
|
246
|
+
readonly invokeMiddleware: InvokeMiddleware,
|
|
247
|
+
@config()
|
|
248
|
+
readonly options: InvokeMiddlewareOptions = MiddlewareSequence.defaultOptions,
|
|
249
|
+
) {
|
|
250
|
+
this.middlewareView = new MiddlewareView(context, options);
|
|
251
|
+
debug('Discovered middleware', this.middlewareView.middlewareBindingKeys);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Runs the default sequence. Given a handler context (request and response),
|
|
256
|
+
* running the sequence will produce a response or an error.
|
|
257
|
+
*
|
|
258
|
+
* Default sequence executes these groups of middleware:
|
|
259
|
+
*
|
|
260
|
+
* - `cors`: Enforces `CORS`
|
|
261
|
+
* - `openApiSpec`: Serves OpenAPI specs
|
|
262
|
+
* - `findRoute`: Finds the appropriate controller method, swagger spec and
|
|
263
|
+
* args for invocation
|
|
264
|
+
* - `parseParams`: Parses HTTP request to get API argument list
|
|
265
|
+
* - `invokeMethod`: Invokes the API which is defined in the Application
|
|
266
|
+
* controller method
|
|
267
|
+
*
|
|
268
|
+
* In front of the groups above, we have a special middleware called
|
|
269
|
+
* `sendResponse`, which first invokes downstream middleware to get a result
|
|
270
|
+
* and handles the result or error respectively.
|
|
271
|
+
*
|
|
272
|
+
* - Writes the result from API into the HTTP response (if the HTTP response
|
|
273
|
+
* has not been produced yet by the middleware chain.
|
|
274
|
+
* - Catches error logs it using 'logError' if any of the above steps
|
|
275
|
+
* in the sequence fails with an error.
|
|
276
|
+
*
|
|
277
|
+
* @param context - The request context: HTTP request and response objects,
|
|
278
|
+
* per-request IoC container and more.
|
|
279
|
+
*/
|
|
280
|
+
async handle(context: RequestContext): Promise<void> {
|
|
281
|
+
debug(
|
|
282
|
+
'Invoking middleware chain %s with groups %s',
|
|
283
|
+
this.options.chain,
|
|
284
|
+
this.options.orderedGroups,
|
|
285
|
+
);
|
|
286
|
+
const options: InvokeMiddlewareOptions = {
|
|
287
|
+
middlewareList: this.middlewareView.middlewareBindingKeys,
|
|
288
|
+
validate: MiddlewareSequence.defaultOptions.validate,
|
|
289
|
+
...this.options,
|
|
290
|
+
};
|
|
291
|
+
await this.invokeMiddleware(context, options);
|
|
292
|
+
}
|
|
293
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -26,7 +26,7 @@ export * from '@loopback/express';
|
|
|
26
26
|
export type FindRoute = (request: Request) => ResolvedRoute;
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
|
-
*
|
|
29
|
+
* A function to parse OpenAPI operation parameters for a given route
|
|
30
30
|
*/
|
|
31
31
|
export type ParseParams = (
|
|
32
32
|
request: Request,
|
|
@@ -111,18 +111,23 @@ export type AjvFormat = FormatDefinition & {name: string};
|
|
|
111
111
|
/**
|
|
112
112
|
* Options for any value validation using AJV
|
|
113
113
|
*/
|
|
114
|
-
export interface ValueValidationOptions extends
|
|
114
|
+
export interface ValueValidationOptions extends ValidationOptions {
|
|
115
115
|
/**
|
|
116
116
|
* Where the data comes from. It can be 'body', 'path', 'header',
|
|
117
117
|
* 'query', 'cookie', etc...
|
|
118
118
|
*/
|
|
119
119
|
source?: string;
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Parameter name, as provided in `ParameterObject#name` property.
|
|
123
|
+
*/
|
|
124
|
+
name?: string;
|
|
120
125
|
}
|
|
121
126
|
|
|
122
127
|
/**
|
|
123
128
|
* Options for request body validation using AJV
|
|
124
129
|
*/
|
|
125
|
-
export interface
|
|
130
|
+
export interface ValidationOptions extends ajv.Options {
|
|
126
131
|
/**
|
|
127
132
|
* Custom cache for compiled schemas by AJV. This setting makes it possible
|
|
128
133
|
* to skip the default cache.
|
|
@@ -184,7 +189,7 @@ export interface RequestBodyParserOptions extends Options {
|
|
|
184
189
|
* This setting is global for all request body parsers and it cannot be
|
|
185
190
|
* overridden inside parser specific properties such as `json` or `text`.
|
|
186
191
|
*/
|
|
187
|
-
validation?:
|
|
192
|
+
validation?: ValidationOptions;
|
|
188
193
|
/**
|
|
189
194
|
* Common options for all parsers
|
|
190
195
|
*/
|
|
@@ -225,3 +230,7 @@ export interface Session {
|
|
|
225
230
|
export interface RequestWithSession extends Request {
|
|
226
231
|
session: Session;
|
|
227
232
|
}
|
|
233
|
+
|
|
234
|
+
// For backwards compatibility
|
|
235
|
+
// TODO(SEMVER-MAJOR)
|
|
236
|
+
export type RequestBodyValidationOptions = ValidationOptions;
|
|
@@ -13,17 +13,19 @@ import {
|
|
|
13
13
|
import AjvCtor from 'ajv';
|
|
14
14
|
import debugModule from 'debug';
|
|
15
15
|
import {RestBindings, RestTags} from '../keys';
|
|
16
|
-
import {
|
|
17
|
-
|
|
18
|
-
AjvFormat,
|
|
19
|
-
AjvKeyword,
|
|
20
|
-
RequestBodyValidationOptions,
|
|
21
|
-
} from '../types';
|
|
16
|
+
import {AjvFactory, AjvFormat, AjvKeyword, ValidationOptions} from '../types';
|
|
17
|
+
|
|
22
18
|
const debug = debugModule('loopback:rest:ajv');
|
|
23
19
|
|
|
24
20
|
const ajvKeywords = require('ajv-keywords');
|
|
25
21
|
const ajvErrors = require('ajv-errors');
|
|
26
22
|
|
|
23
|
+
export const DEFAULT_AJV_VALIDATION_OPTIONS: ValidationOptions = {
|
|
24
|
+
$data: true,
|
|
25
|
+
ajvKeywords: true,
|
|
26
|
+
ajvErrors: true,
|
|
27
|
+
};
|
|
28
|
+
|
|
27
29
|
/**
|
|
28
30
|
* A provider class that instantiate an AJV instance
|
|
29
31
|
*/
|
|
@@ -34,7 +36,7 @@ export class AjvFactoryProvider implements Provider<AjvFactory> {
|
|
|
34
36
|
RestBindings.REQUEST_BODY_PARSER_OPTIONS.deepProperty('validation'),
|
|
35
37
|
{optional: true},
|
|
36
38
|
)
|
|
37
|
-
private options:
|
|
39
|
+
private options: ValidationOptions = DEFAULT_AJV_VALIDATION_OPTIONS,
|
|
38
40
|
) {}
|
|
39
41
|
|
|
40
42
|
@inject(filterByTag(RestTags.AJV_KEYWORD))
|
|
@@ -45,7 +47,7 @@ export class AjvFactoryProvider implements Provider<AjvFactory> {
|
|
|
45
47
|
|
|
46
48
|
value(): AjvFactory {
|
|
47
49
|
return options => {
|
|
48
|
-
let validationOptions:
|
|
50
|
+
let validationOptions: ValidationOptions = {
|
|
49
51
|
...this.options,
|
|
50
52
|
...options,
|
|
51
53
|
};
|
|
@@ -12,15 +12,17 @@ import {
|
|
|
12
12
|
} from '@loopback/openapi-v3';
|
|
13
13
|
import ajv, {Ajv} from 'ajv';
|
|
14
14
|
import debugModule from 'debug';
|
|
15
|
-
import _ from 'lodash';
|
|
16
15
|
import util from 'util';
|
|
17
16
|
import {HttpErrors, RequestBody, RestHttpErrors} from '..';
|
|
18
17
|
import {
|
|
19
|
-
RequestBodyValidationOptions,
|
|
20
18
|
SchemaValidatorCache,
|
|
19
|
+
ValidationOptions,
|
|
21
20
|
ValueValidationOptions,
|
|
22
21
|
} from '../types';
|
|
23
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
AjvFactoryProvider,
|
|
24
|
+
DEFAULT_AJV_VALIDATION_OPTIONS,
|
|
25
|
+
} from './ajv-factory.provider';
|
|
24
26
|
|
|
25
27
|
const toJsonSchema = require('@openapi-contrib/openapi-schema-to-json-schema');
|
|
26
28
|
const debug = debugModule('loopback:rest:validation');
|
|
@@ -39,7 +41,7 @@ export async function validateRequestBody(
|
|
|
39
41
|
body: RequestBody,
|
|
40
42
|
requestBodySpec?: RequestBodyObject,
|
|
41
43
|
globalSchemas: SchemasObject = {},
|
|
42
|
-
options:
|
|
44
|
+
options: ValidationOptions = DEFAULT_AJV_VALIDATION_OPTIONS,
|
|
43
45
|
) {
|
|
44
46
|
const required = requestBodySpec?.required;
|
|
45
47
|
|
|
@@ -102,12 +104,12 @@ const DEFAULT_COMPILED_SCHEMA_CACHE: SchemaValidatorCache = new WeakMap();
|
|
|
102
104
|
* Build a cache key for AJV options
|
|
103
105
|
* @param options - Request body validation options
|
|
104
106
|
*/
|
|
105
|
-
function getKeyForOptions(
|
|
107
|
+
function getKeyForOptions(
|
|
108
|
+
options: ValidationOptions = DEFAULT_AJV_VALIDATION_OPTIONS,
|
|
109
|
+
) {
|
|
106
110
|
const ajvOptions: Record<string, unknown> = {};
|
|
107
111
|
// Sort keys for options
|
|
108
|
-
const keys = Object.keys(
|
|
109
|
-
options,
|
|
110
|
-
).sort() as (keyof RequestBodyValidationOptions)[];
|
|
112
|
+
const keys = Object.keys(options).sort() as (keyof ValidationOptions)[];
|
|
111
113
|
for (const k of keys) {
|
|
112
114
|
if (k === 'compiledSchemaCache') continue;
|
|
113
115
|
ajvOptions[k] = options[k];
|
|
@@ -177,30 +179,32 @@ export async function validateValueAgainstSchema(
|
|
|
177
179
|
|
|
178
180
|
// Throw invalid request body error
|
|
179
181
|
if (options.source === 'body') {
|
|
180
|
-
const error = RestHttpErrors.invalidRequestBody(
|
|
181
|
-
|
|
182
|
+
const error = RestHttpErrors.invalidRequestBody(
|
|
183
|
+
buildErrorDetails(validationErrors),
|
|
184
|
+
);
|
|
182
185
|
throw error;
|
|
183
186
|
}
|
|
184
187
|
|
|
185
188
|
// Throw invalid value error
|
|
186
|
-
const error =
|
|
187
|
-
|
|
189
|
+
const error = RestHttpErrors.invalidData(value, options.name ?? '(unknown)', {
|
|
190
|
+
details: buildErrorDetails(validationErrors),
|
|
191
|
+
});
|
|
188
192
|
throw error;
|
|
189
193
|
}
|
|
190
194
|
|
|
191
|
-
function
|
|
192
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
193
|
-
error: any,
|
|
195
|
+
function buildErrorDetails(
|
|
194
196
|
validationErrors: ajv.ErrorObject[],
|
|
195
|
-
) {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
197
|
+
): RestHttpErrors.ValidationErrorDetails[] {
|
|
198
|
+
return validationErrors.map(
|
|
199
|
+
(e: ajv.ErrorObject): RestHttpErrors.ValidationErrorDetails => {
|
|
200
|
+
return {
|
|
201
|
+
path: e.dataPath,
|
|
202
|
+
code: e.keyword,
|
|
203
|
+
message: e.message ?? `must pass validation rule ${e.keyword}`,
|
|
204
|
+
info: e.params,
|
|
205
|
+
};
|
|
206
|
+
},
|
|
207
|
+
);
|
|
204
208
|
}
|
|
205
209
|
|
|
206
210
|
/**
|