@loopback/openapi-v3 1.10.3 → 2.0.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 (58) hide show
  1. package/CHANGELOG.md +131 -0
  2. package/dist/controller-spec.js +114 -35
  3. package/dist/controller-spec.js.map +1 -1
  4. package/dist/decorators/api.decorator.js +2 -2
  5. package/dist/decorators/api.decorator.js.map +1 -1
  6. package/dist/decorators/deprecated.decorator.d.ts +37 -0
  7. package/dist/decorators/deprecated.decorator.js +71 -0
  8. package/dist/decorators/deprecated.decorator.js.map +1 -0
  9. package/dist/decorators/index.d.ts +20 -0
  10. package/dist/decorators/index.js +24 -0
  11. package/dist/decorators/index.js.map +1 -1
  12. package/dist/decorators/operation.decorator.js +2 -2
  13. package/dist/decorators/operation.decorator.js.map +1 -1
  14. package/dist/decorators/parameter.decorator.js +13 -6
  15. package/dist/decorators/parameter.decorator.js.map +1 -1
  16. package/dist/decorators/request-body.decorator.js +3 -3
  17. package/dist/decorators/request-body.decorator.js.map +1 -1
  18. package/dist/decorators/tags.decorator.d.ts +1 -0
  19. package/dist/decorators/tags.decorator.js +33 -0
  20. package/dist/decorators/tags.decorator.js.map +1 -0
  21. package/dist/enhancers/index.d.ts +3 -0
  22. package/dist/enhancers/index.js +13 -0
  23. package/dist/enhancers/index.js.map +1 -0
  24. package/dist/enhancers/keys.d.ts +6 -0
  25. package/dist/enhancers/keys.js +12 -0
  26. package/dist/enhancers/keys.js.map +1 -0
  27. package/dist/enhancers/spec-enhancer.service.d.ts +73 -0
  28. package/dist/enhancers/spec-enhancer.service.js +135 -0
  29. package/dist/enhancers/spec-enhancer.service.js.map +1 -0
  30. package/dist/enhancers/types.d.ts +18 -0
  31. package/dist/enhancers/types.js +20 -0
  32. package/dist/enhancers/types.js.map +1 -0
  33. package/dist/index.d.ts +1 -0
  34. package/dist/index.js +1 -0
  35. package/dist/index.js.map +1 -1
  36. package/dist/json-to-schema.js +9 -7
  37. package/dist/json-to-schema.js.map +1 -1
  38. package/dist/keys.d.ts +17 -1
  39. package/dist/keys.js +22 -10
  40. package/dist/keys.js.map +1 -1
  41. package/dist/types.d.ts +3 -0
  42. package/package.json +11 -10
  43. package/src/controller-spec.ts +134 -19
  44. package/src/decorators/api.decorator.ts +1 -1
  45. package/src/decorators/deprecated.decorator.ts +87 -0
  46. package/src/decorators/index.ts +30 -0
  47. package/src/decorators/operation.decorator.ts +1 -1
  48. package/src/decorators/parameter.decorator.ts +11 -6
  49. package/src/decorators/request-body.decorator.ts +1 -1
  50. package/src/decorators/tags.decorator.ts +46 -0
  51. package/src/enhancers/index.ts +8 -0
  52. package/src/enhancers/keys.ts +14 -0
  53. package/src/enhancers/spec-enhancer.service.ts +121 -0
  54. package/src/enhancers/types.ts +30 -0
  55. package/src/index.ts +1 -0
  56. package/src/json-to-schema.ts +13 -7
  57. package/src/keys.ts +33 -6
  58. package/src/types.ts +4 -0
package/dist/keys.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"keys.js","sourceRoot":"","sources":["../src/keys.ts"],"names":[],"mappings":";AAAA,iDAAiD;AACjD,oCAAoC;AACpC,+CAA+C;AAC/C,gEAAgE;;AAEhE,+CAAmD;AAInD,iDAAiD;AACjD,oCAAoC;AACpC,+CAA+C;AAC/C,gEAAgE;AAEhE,IAAiB,QAAQ,CAwCxB;AAxCD,WAAiB,QAAQ;IACvB;;OAEG;IACU,oBAAW,GAAG,0BAAgB,CAAC,MAAM,CAGhD,oBAAoB,CAAC,CAAC;IAExB;;OAEG;IACU,uBAAc,GAAG,0BAAgB,CAAC,MAAM,CAGnD,uBAAuB,CAAC,CAAC;IAE3B;;OAEG;IACU,kBAAS,GAAG,0BAAgB,CAAC,MAAM,CAG9C,kBAAkB,CAAC,CAAC;IAEtB;;OAEG;IACU,4BAAmB,GAAG,0BAAgB,CAAC,MAAM,CAGxD,4BAA4B,CAAC,CAAC;IAEhC;;OAEG;IACU,yBAAgB,GAAG,0BAAgB,CAAC,MAAM,CAGrD,yBAAyB,CAAC,CAAC;AAC/B,CAAC,EAxCgB,QAAQ,GAAR,gBAAQ,KAAR,gBAAQ,QAwCxB"}
1
+ {"version":3,"file":"keys.js","sourceRoot":"","sources":["../src/keys.ts"],"names":[],"mappings":";AAAA,iDAAiD;AACjD,oCAAoC;AACpC,+CAA+C;AAC/C,gEAAgE;;AAEhE,yCAAgD;AAIhD,IAAiB,QAAQ,CAwExB;AAxED,WAAiB,QAAQ;IACvB;;OAEG;IACU,oBAAW,GAAG,uBAAgB,CAAC,MAAM,CAGhD,oBAAoB,CAAC,CAAC;IAExB;;OAEG;IACU,8BAAqB,GAAG,uBAAgB,CAAC,MAAM,CAG1D,+BAA+B,CAAC,CAAC;IAEnC;;OAEG;IACU,6BAAoB,GAAG,uBAAgB,CAAC,MAAM,CAGzD,6BAA6B,CAAC,CAAC;IAEjC;;OAEG;IACU,uBAAc,GAAG,uBAAgB,CAAC,MAAM,CAGnD,uBAAuB,CAAC,CAAC;IAE3B;;OAEG;IACU,wBAAe,GAAG,uBAAgB,CAAC,MAAM,CAGpD,yBAAyB,CAAC,CAAC;IAE7B;;OAEG;IACU,uBAAc,GAAG,uBAAgB,CAAC,MAAM,CAGnD,uBAAuB,CAAC,CAAC;IAE3B;;OAEG;IACU,kBAAS,GAAG,uBAAgB,CAAC,MAAM,CAG9C,kBAAkB,CAAC,CAAC;IAEtB;;OAEG;IACU,4BAAmB,GAAG,uBAAgB,CAAC,MAAM,CAGxD,4BAA4B,CAAC,CAAC;IAEhC;;OAEG;IACU,yBAAgB,GAAG,uBAAgB,CAAC,MAAM,CAGrD,yBAAyB,CAAC,CAAC;AAC/B,CAAC,EAxEgB,QAAQ,GAAR,gBAAQ,KAAR,gBAAQ,QAwExB"}
package/dist/types.d.ts CHANGED
@@ -7,3 +7,6 @@ export * from 'openapi3-ts';
7
7
  * @deprecated Use `OpenApiBuilder` from `openapi3-ts` instead.
8
8
  */
9
9
  export declare function createEmptyApiSpec(): OpenApiSpec;
10
+ export interface TagsDecoratorMetadata {
11
+ tags: string[];
12
+ }
package/package.json CHANGED
@@ -1,26 +1,27 @@
1
1
  {
2
2
  "name": "@loopback/openapi-v3",
3
- "version": "1.10.3",
3
+ "version": "2.0.0",
4
4
  "description": "Processes openapi v3 related metadata",
5
5
  "engines": {
6
6
  "node": ">=8.9"
7
7
  },
8
8
  "dependencies": {
9
- "@loopback/context": "^1.25.0",
10
- "@loopback/repository-json-schema": "^1.11.3",
9
+ "@loopback/core": "^1.12.4",
10
+ "@loopback/repository-json-schema": "^1.12.2",
11
11
  "debug": "^4.1.1",
12
+ "json-merge-patch": "^0.2.3",
12
13
  "lodash": "^4.17.15",
13
14
  "openapi3-ts": "^1.3.0"
14
15
  },
15
16
  "devDependencies": {
16
- "@loopback/build": "^3.0.0",
17
- "@loopback/eslint-config": "^5.0.0",
18
- "@loopback/openapi-spec-builder": "^1.2.20",
19
- "@loopback/repository": "^1.16.0",
20
- "@loopback/testlab": "^1.10.0",
17
+ "@loopback/build": "^3.1.1",
18
+ "@loopback/eslint-config": "^5.0.3",
19
+ "@loopback/openapi-spec-builder": "^1.3.1",
20
+ "@loopback/repository": "^1.19.1",
21
+ "@loopback/testlab": "^1.10.3",
21
22
  "@types/debug": "^4.1.5",
22
23
  "@types/lodash": "^4.14.149",
23
- "@types/node": "^10.17.6"
24
+ "@types/node": "^10.17.14"
24
25
  },
25
26
  "scripts": {
26
27
  "build": "lb-tsc",
@@ -55,5 +56,5 @@
55
56
  "url": "https://github.com/strongloop/loopback-next.git",
56
57
  "directory": "packages/openapi-v3"
57
58
  },
58
- "gitHead": "89eb61bacaed75e6eb61ae6840cea266cb888659"
59
+ "gitHead": "6eea5e428b145cafb84a998bd53979da8c8fba07"
59
60
  }
@@ -3,13 +3,13 @@
3
3
  // This file is licensed under the MIT License.
4
4
  // License text available at https://opensource.org/licenses/MIT
5
5
 
6
- import {DecoratorFactory, MetadataInspector} from '@loopback/context';
6
+ import {DecoratorFactory, MetadataInspector} from '@loopback/core';
7
7
  import {
8
8
  getJsonSchema,
9
9
  getJsonSchemaRef,
10
10
  JsonSchemaOptions,
11
11
  } from '@loopback/repository-json-schema';
12
- import _ from 'lodash';
12
+ import {includes} from 'lodash';
13
13
  import {resolveSchema} from './generate-schema';
14
14
  import {jsonToSchemaObject, SchemaRef} from './json-to-schema';
15
15
  import {OAI3Keys} from './keys';
@@ -25,6 +25,7 @@ import {
25
25
  ResponseObject,
26
26
  SchemaObject,
27
27
  SchemasObject,
28
+ TagsDecoratorMetadata,
28
29
  } from './types';
29
30
 
30
31
  const debug = require('debug')('loopback:openapi3:metadata:controller-spec');
@@ -77,6 +78,47 @@ function resolveControllerSpec(constructor: Function): ControllerSpec {
77
78
  spec = {paths: {}};
78
79
  }
79
80
 
81
+ const isClassDeprecated = MetadataInspector.getClassMetadata<boolean>(
82
+ OAI3Keys.DEPRECATED_CLASS_KEY,
83
+ constructor,
84
+ );
85
+
86
+ if (isClassDeprecated) {
87
+ debug(' using class-level @deprecated()');
88
+ }
89
+ const classTags = MetadataInspector.getClassMetadata<TagsDecoratorMetadata>(
90
+ OAI3Keys.TAGS_CLASS_KEY,
91
+ constructor,
92
+ );
93
+
94
+ if (classTags) {
95
+ debug(' using class-level @oas.tags()');
96
+ }
97
+
98
+ if (classTags || isClassDeprecated) {
99
+ for (const path of Object.keys(spec.paths)) {
100
+ for (const method of Object.keys(spec.paths[path])) {
101
+ /* istanbul ignore else */
102
+ if (isClassDeprecated) {
103
+ spec.paths[path][method].deprecated = true;
104
+ }
105
+ /* istanbul ignore else */
106
+ if (classTags) {
107
+ if (
108
+ spec.paths[path][method].tags &&
109
+ spec.paths[path][method].tags.length
110
+ ) {
111
+ spec.paths[path][method].tags = spec.paths[path][
112
+ method
113
+ ].tags.concat(classTags.tags);
114
+ } else {
115
+ spec.paths[path][method].tags = classTags.tags;
116
+ }
117
+ }
118
+ }
119
+ }
120
+ }
121
+
80
122
  let endpoints =
81
123
  MetadataInspector.getAllMethodMetadata<RestEndpoint>(
82
124
  OAI3Keys.METHODS_KEY,
@@ -91,6 +133,23 @@ function resolveControllerSpec(constructor: Function): ControllerSpec {
91
133
  const verb = endpoint.verb!;
92
134
  const path = endpoint.path!;
93
135
 
136
+ const isMethodDeprecated = MetadataInspector.getMethodMetadata<boolean>(
137
+ OAI3Keys.DEPRECATED_METHOD_KEY,
138
+ constructor.prototype,
139
+ op,
140
+ );
141
+ if (isMethodDeprecated) {
142
+ debug(' using method-level deprecation via @deprecated()');
143
+ }
144
+
145
+ const methodTags = MetadataInspector.getMethodMetadata<
146
+ TagsDecoratorMetadata
147
+ >(OAI3Keys.TAGS_METHOD_KEY, constructor.prototype, op);
148
+
149
+ if (methodTags) {
150
+ debug(' using method-level tags via @oas.tags()');
151
+ }
152
+
94
153
  let endpointName = '';
95
154
  /* istanbul ignore if */
96
155
  if (debug.enabled) {
@@ -113,10 +172,34 @@ function resolveControllerSpec(constructor: Function): ControllerSpec {
113
172
  };
114
173
  endpoint.spec = operationSpec;
115
174
  }
175
+
176
+ if (classTags && !operationSpec.tags) {
177
+ operationSpec.tags = classTags.tags;
178
+ }
179
+
180
+ if (methodTags) {
181
+ if (operationSpec.tags && operationSpec.tags.length) {
182
+ operationSpec.tags = operationSpec.tags.concat(methodTags.tags);
183
+ } else {
184
+ operationSpec.tags = methodTags.tags;
185
+ }
186
+ }
187
+
116
188
  debug(' operation for method %s: %j', op, endpoint);
117
189
 
118
190
  debug(' spec responses for method %s: %o', op, operationSpec.responses);
119
191
 
192
+ // Prescedence: method decorator > class decorator > operationSpec > undefined
193
+ const deprecationSpec =
194
+ isMethodDeprecated ??
195
+ isClassDeprecated ??
196
+ operationSpec.deprecated ??
197
+ false;
198
+
199
+ if (deprecationSpec) {
200
+ operationSpec.deprecated = true;
201
+ }
202
+
120
203
  for (const code in operationSpec.responses) {
121
204
  const responseObject: ResponseObject | ReferenceObject =
122
205
  operationSpec.responses[code];
@@ -178,9 +261,11 @@ function resolveControllerSpec(constructor: Function): ControllerSpec {
178
261
 
179
262
  requestBody = requestBodies[0];
180
263
  debug(' requestBody for method %s: %j', op, requestBody);
264
+ /* istanbul ignore else */
181
265
  if (requestBody) {
182
266
  operationSpec.requestBody = requestBody;
183
267
 
268
+ /* istanbul ignore else */
184
269
  const content = requestBody.content || {};
185
270
  for (const mediaType in content) {
186
271
  processSchemaExtensions(spec, content[mediaType].schema);
@@ -222,7 +307,7 @@ function resolveControllerSpec(constructor: Function): ControllerSpec {
222
307
  const paramTypes = opMetadata.parameterTypes;
223
308
 
224
309
  const isComplexType = (ctor: Function) =>
225
- !_.includes([String, Number, Boolean, Array, Object], ctor);
310
+ !includes([String, Number, Boolean, Array, Object], ctor);
226
311
 
227
312
  for (const p of paramTypes) {
228
313
  if (isComplexType(p)) {
@@ -233,6 +318,9 @@ function resolveControllerSpec(constructor: Function): ControllerSpec {
233
318
  return spec;
234
319
  }
235
320
 
321
+ declare type MixKey = 'allOf' | 'anyOf' | 'oneOf';
322
+ const SCHEMA_ARR_KEYS: MixKey[] = ['allOf', 'anyOf', 'oneOf'];
323
+
236
324
  /**
237
325
  * Resolve the x-ts-type in the schema object
238
326
  * @param spec - Controller spec
@@ -248,24 +336,51 @@ function processSchemaExtensions(
248
336
  assignRelatedSchemas(spec, schema.definitions);
249
337
  delete schema.definitions;
250
338
 
251
- if (isReferenceObject(schema)) return;
339
+ /**
340
+ * check if we have been provided a `not`
341
+ * `not` is valid in many cases- here we're checking for
342
+ * `not: { schema: {'x-ts-type': SomeModel }}
343
+ */
344
+ if (schema.not) {
345
+ processSchemaExtensions(spec, schema.not);
346
+ }
252
347
 
253
- const tsType = schema[TS_TYPE_KEY];
254
- debug(' %s => %o', TS_TYPE_KEY, tsType);
255
- if (tsType) {
256
- schema = resolveSchema(tsType, schema);
257
- if (schema.$ref) generateOpenAPISchema(spec, tsType);
348
+ /**
349
+ * check for schema.allOf, schema.oneOf, schema.anyOf arrays first.
350
+ * You cannot provide BOTH a defnintion AND one of these keywords.
351
+ */
352
+ /* istanbul ignore else */
353
+ const hasOwn = (prop: string) => schema?.hasOwnProperty(prop);
354
+
355
+ if (SCHEMA_ARR_KEYS.some(k => hasOwn(k))) {
356
+ SCHEMA_ARR_KEYS.forEach((k: MixKey) => {
357
+ /* istanbul ignore else */
358
+ if (schema?.[k] && Array.isArray(schema[k])) {
359
+ schema[k].forEach((r: (SchemaObject | ReferenceObject)[]) => {
360
+ processSchemaExtensions(spec, r);
361
+ });
362
+ }
363
+ });
364
+ } else {
365
+ if (isReferenceObject(schema)) return;
258
366
 
259
- // We don't want a Function type in the final spec.
260
- delete schema[TS_TYPE_KEY];
261
- return;
262
- }
263
- if (schema.type === 'array') {
264
- processSchemaExtensions(spec, schema.items);
265
- } else if (schema.type === 'object') {
266
- if (schema.properties) {
267
- for (const p in schema.properties) {
268
- processSchemaExtensions(spec, schema.properties[p]);
367
+ const tsType = schema[TS_TYPE_KEY];
368
+ debug(' %s => %o', TS_TYPE_KEY, tsType);
369
+ if (tsType) {
370
+ schema = resolveSchema(tsType, schema);
371
+ if (schema.$ref) generateOpenAPISchema(spec, tsType);
372
+
373
+ // We don't want a Function type in the final spec.
374
+ delete schema[TS_TYPE_KEY];
375
+ return;
376
+ }
377
+ if (schema.type === 'array') {
378
+ processSchemaExtensions(spec, schema.items);
379
+ } else if (schema.type === 'object') {
380
+ if (schema.properties) {
381
+ for (const p in schema.properties) {
382
+ processSchemaExtensions(spec, schema.properties[p]);
383
+ }
269
384
  }
270
385
  }
271
386
  }
@@ -3,7 +3,7 @@
3
3
  // This file is licensed under the MIT License.
4
4
  // License text available at https://opensource.org/licenses/MIT
5
5
 
6
- import {ClassDecoratorFactory} from '@loopback/context';
6
+ import {ClassDecoratorFactory} from '@loopback/core';
7
7
  import {ControllerSpec} from '../controller-spec';
8
8
  import {OAI3Keys} from '../keys';
9
9
 
@@ -0,0 +1,87 @@
1
+ // Copyright IBM Corp. 2018. All Rights Reserved.
2
+ // Node module: @loopback/openapi-v3
3
+ // This file is licensed under the MIT License.
4
+ // License text available at https://opensource.org/licenses/MIT
5
+
6
+ import {
7
+ ClassDecoratorFactory,
8
+ DecoratorFactory,
9
+ MethodDecoratorFactory,
10
+ } from '@loopback/core';
11
+ import {OAI3Keys} from '../keys';
12
+
13
+ const debug = require('debug')(
14
+ 'loopback:openapi3:metadata:controller-spec:deprecated',
15
+ );
16
+
17
+ /**
18
+ * Marks an api path as deprecated. When applied to a class, this decorator
19
+ * marks all paths as deprecated.
20
+ *
21
+ * You can optionally mark all controllers in a class as deprecated, but use
22
+ * `@deprecated(false)` on a specific method to ensure it is not marked
23
+ * as deprecated in the specification.
24
+ *
25
+ * @param isDeprecated - whether or not the path should be marked as deprecated.
26
+ * This is useful for marking a class as deprecated, but a method as
27
+ * not deprecated.
28
+ *
29
+ * @example
30
+ * ```ts
31
+ * @oas.deprecated()
32
+ * class MyController {
33
+ * @get('/greet')
34
+ * public async function greet() {
35
+ * return 'Hello, World!'
36
+ * }
37
+ *
38
+ * @get('/greet-v2')
39
+ * @oas.deprecated(false)
40
+ * public async function greetV2() {
41
+ * return 'Hello, World!'
42
+ * }
43
+ * }
44
+ *
45
+ * class MyOtherController {
46
+ * @get('/echo')
47
+ * public async function echo() {
48
+ * return 'Echo!'
49
+ * }
50
+ * }
51
+ * ```
52
+ */
53
+ export function deprecated(isDeprecated = true) {
54
+ return function deprecatedDecoratorForClassOrMethod(
55
+ // Class or a prototype
56
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
57
+ target: any,
58
+ method?: string,
59
+ // Use `any` to for `TypedPropertyDescriptor`
60
+ // See https://github.com/strongloop/loopback-next/pull/2704
61
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
62
+ methodDescriptor?: TypedPropertyDescriptor<any>,
63
+ ) {
64
+ debug(target, method, methodDescriptor);
65
+
66
+ if (method && methodDescriptor) {
67
+ // Method
68
+ return MethodDecoratorFactory.createDecorator<boolean>(
69
+ OAI3Keys.DEPRECATED_METHOD_KEY,
70
+ isDeprecated,
71
+ {decoratorName: '@oas.deprecated'},
72
+ )(target, method, methodDescriptor);
73
+ } else if (typeof target === 'function' && !method && !methodDescriptor) {
74
+ // Class
75
+ return ClassDecoratorFactory.createDecorator<boolean>(
76
+ OAI3Keys.DEPRECATED_CLASS_KEY,
77
+ isDeprecated,
78
+ {decoratorName: '@oas.deprecated'},
79
+ )(target);
80
+ } else {
81
+ throw new Error(
82
+ '@oas.deprecated cannot be used on a property: ' +
83
+ DecoratorFactory.getTargetName(target, method, methodDescriptor),
84
+ );
85
+ }
86
+ };
87
+ }
@@ -4,6 +4,36 @@
4
4
  // License text available at https://opensource.org/licenses/MIT
5
5
 
6
6
  export * from './api.decorator';
7
+ export * from './deprecated.decorator';
7
8
  export * from './operation.decorator';
8
9
  export * from './parameter.decorator';
9
10
  export * from './request-body.decorator';
11
+
12
+ import {api} from './api.decorator';
13
+ import {deprecated} from './deprecated.decorator';
14
+ import {del, get, operation, patch, post, put} from './operation.decorator';
15
+ import {param} from './parameter.decorator';
16
+ import {requestBody} from './request-body.decorator';
17
+ import {tags} from './tags.decorator';
18
+
19
+ export const oas = {
20
+ api,
21
+ operation,
22
+
23
+ // methods
24
+ get,
25
+ post,
26
+ del,
27
+ patch,
28
+ put,
29
+
30
+ //param
31
+ param,
32
+
33
+ // request body
34
+ requestBody,
35
+
36
+ // oas convenience decorators
37
+ deprecated,
38
+ tags,
39
+ };
@@ -3,7 +3,7 @@
3
3
  // This file is licensed under the MIT License.
4
4
  // License text available at https://opensource.org/licenses/MIT
5
5
 
6
- import {MethodDecoratorFactory} from '@loopback/context';
6
+ import {MethodDecoratorFactory} from '@loopback/core';
7
7
  import {RestEndpoint} from '../controller-spec';
8
8
  import {OAI3Keys} from '../keys';
9
9
  import {OperationObject} from '../types';
@@ -3,7 +3,7 @@
3
3
  // This file is licensed under the MIT License.
4
4
  // License text available at https://opensource.org/licenses/MIT
5
5
 
6
- import {MetadataInspector, ParameterDecoratorFactory} from '@loopback/context';
6
+ import {MetadataInspector, ParameterDecoratorFactory} from '@loopback/core';
7
7
  import {resolveSchema} from '../generate-schema';
8
8
  import {OAI3Keys} from '../keys';
9
9
  import {
@@ -50,8 +50,11 @@ export function param(paramSpec: ParameterObject) {
50
50
  // generate schema if `paramSpec` has `schema` but without `type`
51
51
  (isSchemaObject(paramSpec.schema) && !paramSpec.schema.type)
52
52
  ) {
53
- // please note `resolveSchema` only adds `type` and `format` for `schema`
54
- paramSpec.schema = resolveSchema(paramType, paramSpec.schema);
53
+ // If content explicitly mentioned do not resolve schema
54
+ if (!paramSpec.content) {
55
+ // please note `resolveSchema` only adds `type` and `format` for `schema`
56
+ paramSpec.schema = resolveSchema(paramType, paramSpec.schema);
57
+ }
55
58
  }
56
59
  }
57
60
 
@@ -212,9 +215,11 @@ export namespace param {
212
215
  return param({
213
216
  name,
214
217
  in: 'query',
215
- style: 'deepObject',
216
- explode: true,
217
- schema,
218
+ content: {
219
+ 'application/json': {
220
+ schema,
221
+ },
222
+ },
218
223
  ...spec,
219
224
  });
220
225
  },
@@ -3,7 +3,7 @@
3
3
  // This file is licensed under the MIT License.
4
4
  // License text available at https://opensource.org/licenses/MIT
5
5
 
6
- import {MetadataInspector, ParameterDecoratorFactory} from '@loopback/context';
6
+ import {MetadataInspector, ParameterDecoratorFactory} from '@loopback/core';
7
7
  import _ from 'lodash';
8
8
  import {inspect} from 'util';
9
9
  import {resolveSchema} from '../generate-schema';
@@ -0,0 +1,46 @@
1
+ // Copyright IBM Corp. 2018. All Rights Reserved.
2
+ // Node module: @loopback/openapi-v3
3
+ // This file is licensed under the MIT License.
4
+ // License text available at https://opensource.org/licenses/MIT
5
+
6
+ import {
7
+ ClassDecoratorFactory,
8
+ DecoratorFactory,
9
+ MethodDecoratorFactory,
10
+ } from '@loopback/core';
11
+ import {OAI3Keys} from '../keys';
12
+ import {TagsDecoratorMetadata} from '../types';
13
+
14
+ export function tags(...tagNames: string[]) {
15
+ return function tagsDecoratorForClassOrMethod(
16
+ // Class or a prototype
17
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
18
+ target: any,
19
+ method?: string,
20
+ // Use `any` to for `TypedPropertyDescriptor`
21
+ // See https://github.com/strongloop/loopback-next/pull/2704
22
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
23
+ methodDescriptor?: TypedPropertyDescriptor<any>,
24
+ ) {
25
+ if (method && methodDescriptor) {
26
+ // Method
27
+ return MethodDecoratorFactory.createDecorator<TagsDecoratorMetadata>(
28
+ OAI3Keys.TAGS_METHOD_KEY,
29
+ {tags: tagNames},
30
+ {decoratorName: '@oas.tags'},
31
+ )(target, method, methodDescriptor);
32
+ } else if (typeof target === 'function' && !method && !methodDescriptor) {
33
+ // Class
34
+ return ClassDecoratorFactory.createDecorator<TagsDecoratorMetadata>(
35
+ OAI3Keys.TAGS_CLASS_KEY,
36
+ {tags: tagNames},
37
+ {decoratorName: '@oas.tags'},
38
+ )(target);
39
+ } else {
40
+ throw new Error(
41
+ '@oas.tags cannot be used on a property: ' +
42
+ DecoratorFactory.getTargetName(target, method, methodDescriptor),
43
+ );
44
+ }
45
+ };
46
+ }
@@ -0,0 +1,8 @@
1
+ // Copyright IBM Corp. 2019. All Rights Reserved.
2
+ // Node module: @loopback/openapi-v3
3
+ // This file is licensed under the MIT License.
4
+ // License text available at https://opensource.org/licenses/MIT
5
+
6
+ export * from './keys';
7
+ export * from './spec-enhancer.service';
8
+ export * from './types';
@@ -0,0 +1,14 @@
1
+ // Copyright IBM Corp. 2019. All Rights Reserved.
2
+ // Node module: @loopback/openapi-v3
3
+ // This file is licensed under the MIT License.
4
+ // License text available at https://opensource.org/licenses/MIT
5
+
6
+ import {BindingKey} from '@loopback/core';
7
+ import {OASEnhancerService} from './spec-enhancer.service';
8
+
9
+ /**
10
+ * Strongly-typed binding key for SpecService
11
+ */
12
+ export const OAS_ENHANCER_SERVICE = BindingKey.create<OASEnhancerService>(
13
+ 'services.SpecService',
14
+ );
@@ -0,0 +1,121 @@
1
+ // Copyright IBM Corp. 2019. All Rights Reserved.
2
+ // Node module: @loopback/openapi-v3
3
+ // This file is licensed under the MIT License.
4
+ // License text available at https://opensource.org/licenses/MIT
5
+
6
+ import {config, extensionPoint, extensions, Getter} from '@loopback/core';
7
+ import debugModule from 'debug';
8
+ import * as _ from 'lodash';
9
+ import {inspect} from 'util';
10
+ import {OpenApiSpec} from '../types';
11
+ import {OASEnhancer, OAS_ENHANCER_EXTENSION_POINT_NAME} from './types';
12
+ const jsonmergepatch = require('json-merge-patch');
13
+
14
+ const debug = debugModule('loopback:openapi:spec-enhancer');
15
+
16
+ /**
17
+ * Options for the OpenAPI Spec enhancer extension point
18
+ */
19
+ export interface OASEnhancerServiceOptions {
20
+ // no-op
21
+ }
22
+
23
+ /**
24
+ * An extension point for OpenAPI Spec enhancement
25
+ * This service is used for enhancing an OpenAPI spec by loading and applying one or more
26
+ * registered enhancers.
27
+ *
28
+ * A typical use of it would be generating the OpenAPI spec for the endpoints on a server
29
+ * in the `@loopback/rest` module.
30
+ */
31
+ @extensionPoint(OAS_ENHANCER_EXTENSION_POINT_NAME)
32
+ export class OASEnhancerService {
33
+ constructor(
34
+ /**
35
+ * Inject a getter function to fetch spec enhancers
36
+ */
37
+ @extensions()
38
+ private getEnhancers: Getter<OASEnhancer[]>,
39
+ /**
40
+ * An extension point should be able to receive its options via dependency
41
+ * injection.
42
+ */
43
+ @config()
44
+ public readonly options?: OASEnhancerServiceOptions,
45
+ ) {}
46
+
47
+ private _spec: OpenApiSpec = {
48
+ openapi: '3.0.0',
49
+ info: {
50
+ title: 'LoopBack Application',
51
+ version: '1.0.0',
52
+ },
53
+ paths: {},
54
+ };
55
+
56
+ /**
57
+ * Getter for `_spec`
58
+ */
59
+ get spec(): OpenApiSpec {
60
+ return this._spec;
61
+ }
62
+ /**
63
+ * Setter for `_spec`
64
+ */
65
+ set spec(value: OpenApiSpec) {
66
+ this._spec = value;
67
+ }
68
+
69
+ /**
70
+ * Find an enhancer by its name
71
+ * @param name The name of the enhancer you want to find
72
+ */
73
+ async getEnhancerByName(name: string): Promise<OASEnhancer | undefined> {
74
+ // Get the latest list of enhancers
75
+ const enhancers = await this.getEnhancers();
76
+ return enhancers.find(e => e.name === name);
77
+ }
78
+
79
+ /**
80
+ * Apply a given enhancer's merge function. Return the latest _spec.
81
+ * @param name The name of the enhancer you want to apply
82
+ */
83
+ async applyEnhancerByName(name: string): Promise<OpenApiSpec> {
84
+ const enhancer = await this.getEnhancerByName(name);
85
+ if (enhancer) this._spec = enhancer.modifySpec(this._spec);
86
+ return this._spec;
87
+ }
88
+
89
+ /**
90
+ * Generate OpenAPI spec by applying ALL registered enhancers
91
+ * TBD: load enhancers by group names
92
+ */
93
+ async applyAllEnhancers(options = {}): Promise<OpenApiSpec> {
94
+ const enhancers = await this.getEnhancers();
95
+ if (_.isEmpty(enhancers)) return this._spec;
96
+ for (const e of enhancers) {
97
+ this._spec = e.modifySpec(this._spec);
98
+ }
99
+ debug(`Spec enhancer service, generated spec: ${inspect(this._spec)}`);
100
+ return this._spec;
101
+ }
102
+ }
103
+
104
+ /**
105
+ * The default merge function to patch the current OpenAPI spec.
106
+ * It leverages module `json-merge-patch`'s merge API to merge two json objects.
107
+ * It returns a new merged object without modifying the original one.
108
+ *
109
+ * A list of merging rules can be found in test file:
110
+ * https://github.com/pierreinglebert/json-merge-patch/blob/master/test/lib/merge.js
111
+ *
112
+ * @param currentSpec The original spec
113
+ * @param patchSpec The patch spec to be merged into the original spec
114
+ */
115
+ export function mergeOpenAPISpec(
116
+ currentSpec: Partial<OpenApiSpec>,
117
+ patchSpec: Partial<OpenApiSpec>,
118
+ ) {
119
+ const mergedSpec = jsonmergepatch.merge(currentSpec, patchSpec);
120
+ return mergedSpec;
121
+ }