@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.
Files changed (78) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/README.md +4 -0
  3. package/dist/body-parsers/body-parser.js +3 -0
  4. package/dist/body-parsers/body-parser.js.map +1 -1
  5. package/dist/coercion/coerce-parameter.js +3 -2
  6. package/dist/coercion/coerce-parameter.js.map +1 -1
  7. package/dist/coercion/utils.d.ts +1 -1
  8. package/dist/index.js +1 -1
  9. package/dist/keys.d.ts +18 -3
  10. package/dist/keys.js +18 -1
  11. package/dist/keys.js.map +1 -1
  12. package/dist/parser.d.ts +2 -2
  13. package/dist/parser.js +3 -2
  14. package/dist/parser.js.map +1 -1
  15. package/dist/providers/find-route.provider.d.ts +7 -1
  16. package/dist/providers/find-route.provider.js +29 -1
  17. package/dist/providers/find-route.provider.js.map +1 -1
  18. package/dist/providers/invoke-method.provider.d.ts +7 -1
  19. package/dist/providers/invoke-method.provider.js +43 -1
  20. package/dist/providers/invoke-method.provider.js.map +1 -1
  21. package/dist/providers/parse-params.provider.d.ts +8 -2
  22. package/dist/providers/parse-params.provider.js +32 -2
  23. package/dist/providers/parse-params.provider.js.map +1 -1
  24. package/dist/providers/send.provider.d.ts +10 -2
  25. package/dist/providers/send.provider.js +46 -1
  26. package/dist/providers/send.provider.js.map +1 -1
  27. package/dist/request-context.js.map +1 -1
  28. package/dist/rest-http-error.d.ts +3 -1
  29. package/dist/rest-http-error.js +3 -2
  30. package/dist/rest-http-error.js.map +1 -1
  31. package/dist/rest.application.js +1 -1
  32. package/dist/rest.application.js.map +1 -1
  33. package/dist/rest.component.d.ts +1 -2
  34. package/dist/rest.component.js +23 -7
  35. package/dist/rest.component.js.map +1 -1
  36. package/dist/rest.server.d.ts +8 -4
  37. package/dist/rest.server.js +25 -12
  38. package/dist/rest.server.js.map +1 -1
  39. package/dist/router/base-route.js +3 -3
  40. package/dist/router/base-route.js.map +1 -1
  41. package/dist/router/controller-route.js +1 -1
  42. package/dist/router/controller-route.js.map +1 -1
  43. package/dist/router/handler-route.js +1 -1
  44. package/dist/router/handler-route.js.map +1 -1
  45. package/dist/router/redirect-route.js +1 -1
  46. package/dist/router/redirect-route.js.map +1 -1
  47. package/dist/sequence.d.ts +84 -2
  48. package/dist/sequence.js +135 -2
  49. package/dist/sequence.js.map +1 -1
  50. package/dist/types.d.ts +9 -4
  51. package/dist/validation/ajv-factory.provider.d.ts +3 -2
  52. package/dist/validation/ajv-factory.provider.js +7 -2
  53. package/dist/validation/ajv-factory.provider.js.map +1 -1
  54. package/dist/validation/request-body.validator.d.ts +2 -2
  55. package/dist/validation/request-body.validator.js +11 -13
  56. package/dist/validation/request-body.validator.js.map +1 -1
  57. package/package.json +25 -25
  58. package/src/body-parsers/body-parser.ts +3 -0
  59. package/src/coercion/coerce-parameter.ts +4 -3
  60. package/src/keys.ts +34 -3
  61. package/src/parser.ts +4 -3
  62. package/src/providers/find-route.provider.ts +36 -3
  63. package/src/providers/invoke-method.provider.ts +47 -3
  64. package/src/providers/parse-params.provider.ts +36 -9
  65. package/src/providers/send.provider.ts +45 -2
  66. package/src/request-context.ts +2 -1
  67. package/src/rest-http-error.ts +6 -2
  68. package/src/rest.application.ts +1 -1
  69. package/src/rest.component.ts +40 -10
  70. package/src/rest.server.ts +50 -16
  71. package/src/router/base-route.ts +3 -3
  72. package/src/router/controller-route.ts +1 -1
  73. package/src/router/handler-route.ts +3 -1
  74. package/src/router/redirect-route.ts +1 -1
  75. package/src/sequence.ts +174 -4
  76. package/src/types.ts +13 -4
  77. package/src/validation/ajv-factory.provider.ts +10 -8
  78. package/src/validation/request-body.validator.ts +28 -24
@@ -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 {DefaultSequence, SequenceFunction, SequenceHandler} from './sequence';
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 extends BaseMiddlewareRegistry
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: 'cors',
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: 'apiSpec',
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
- spec.paths = cloneDeep(this.httpHandler.describeApiPaths());
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 value - The sequence to invoke for each incoming request.
900
+ * @param sequenceClass - The sequence class to invoke for each incoming request.
871
901
  */
872
- public sequence(value: Constructor<SequenceHandler>) {
873
- this.bind(RestBindings.SEQUENCE).toClass(value);
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
 
@@ -36,11 +36,11 @@ export abstract class BaseRoute implements RouteEntry {
36
36
  ): Promise<OperationRetval>;
37
37
 
38
38
  describe(): string {
39
- return `"${this.verb} ${this.path}"`;
39
+ return `${this.verb} ${this.path}`;
40
40
  }
41
41
 
42
42
  toString() {
43
- return `${this.constructor.name} - ${this.verb} ${this.path}`;
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 `${this.value.verb} ${this.value.path}`;
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 this._handler.name || super.describe();
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 `RedirectRoute from "${this.sourcePath}" to "${this.targetLocation}"`;
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
- const debug = require('debug')('loopback:rest:sequence');
7
- import {inject, ValueOrPromise} from '@loopback/core';
8
- import {InvokeMiddleware} from '@loopback/express';
9
- import {RestBindings} from './keys';
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 RequestBodyValidationOptions {
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 RequestBodyValidationOptions extends ajv.Options {
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?: RequestBodyValidationOptions;
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
- AjvFactory,
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: RequestBodyValidationOptions = {},
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: RequestBodyValidationOptions = {
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 {AjvFactoryProvider} from './ajv-factory.provider';
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: RequestBodyValidationOptions = {},
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(options: RequestBodyValidationOptions) {
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
- addErrorDetails(error, validationErrors);
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 = new HttpErrors.BadRequest('Invalid value.');
187
- addErrorDetails(error, validationErrors);
189
+ const error = RestHttpErrors.invalidData(value, options.name ?? '(unknown)', {
190
+ details: buildErrorDetails(validationErrors),
191
+ });
188
192
  throw error;
189
193
  }
190
194
 
191
- function addErrorDetails(
192
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
193
- error: any,
195
+ function buildErrorDetails(
194
196
  validationErrors: ajv.ErrorObject[],
195
- ) {
196
- error.details = _.map(validationErrors, e => {
197
- return {
198
- path: e.dataPath,
199
- code: e.keyword,
200
- message: e.message,
201
- info: e.params,
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
  /**