@loopback/openapi-v3 1.10.2 → 1.13.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 +44 -0
- package/dist/controller-spec.js +114 -31
- package/dist/controller-spec.js.map +1 -1
- package/dist/decorators/api.decorator.js +2 -2
- package/dist/decorators/api.decorator.js.map +1 -1
- package/dist/decorators/deprecated.decorator.d.ts +37 -0
- package/dist/decorators/deprecated.decorator.js +71 -0
- package/dist/decorators/deprecated.decorator.js.map +1 -0
- package/dist/decorators/index.d.ts +20 -0
- package/dist/decorators/index.js +24 -0
- package/dist/decorators/index.js.map +1 -1
- package/dist/decorators/operation.decorator.js +2 -2
- package/dist/decorators/operation.decorator.js.map +1 -1
- package/dist/decorators/parameter.decorator.js +5 -4
- package/dist/decorators/parameter.decorator.js.map +1 -1
- package/dist/decorators/request-body.decorator.js +12 -8
- package/dist/decorators/request-body.decorator.js.map +1 -1
- package/dist/decorators/tags.decorator.d.ts +1 -0
- package/dist/decorators/tags.decorator.js +33 -0
- package/dist/decorators/tags.decorator.js.map +1 -0
- package/dist/enhancers/index.d.ts +3 -0
- package/dist/enhancers/index.js +13 -0
- package/dist/enhancers/index.js.map +1 -0
- package/dist/enhancers/keys.d.ts +6 -0
- package/dist/enhancers/keys.js +12 -0
- package/dist/enhancers/keys.js.map +1 -0
- package/dist/enhancers/spec-enhancer.service.d.ts +73 -0
- package/dist/enhancers/spec-enhancer.service.js +135 -0
- package/dist/enhancers/spec-enhancer.service.js.map +1 -0
- package/dist/enhancers/types.d.ts +18 -0
- package/dist/enhancers/types.js +20 -0
- package/dist/enhancers/types.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/json-to-schema.js +16 -11
- package/dist/json-to-schema.js.map +1 -1
- package/dist/keys.d.ts +17 -1
- package/dist/keys.js +22 -10
- package/dist/keys.js.map +1 -1
- package/dist/types.d.ts +3 -0
- package/package.json +11 -10
- package/src/controller-spec.ts +136 -21
- package/src/decorators/api.decorator.ts +1 -1
- package/src/decorators/deprecated.decorator.ts +87 -0
- package/src/decorators/index.ts +30 -0
- package/src/decorators/operation.decorator.ts +1 -1
- package/src/decorators/parameter.decorator.ts +2 -2
- package/src/decorators/request-body.decorator.ts +4 -4
- package/src/decorators/tags.decorator.ts +46 -0
- package/src/enhancers/index.ts +8 -0
- package/src/enhancers/keys.ts +14 -0
- package/src/enhancers/spec-enhancer.service.ts +121 -0
- package/src/enhancers/types.ts +30 -0
- package/src/index.ts +1 -0
- package/src/json-to-schema.ts +14 -8
- package/src/keys.ts +33 -6
- package/src/types.ts +4 -0
package/package.json
CHANGED
|
@@ -1,26 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@loopback/openapi-v3",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.13.0",
|
|
4
4
|
"description": "Processes openapi v3 related metadata",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=8.9"
|
|
7
7
|
},
|
|
8
8
|
"dependencies": {
|
|
9
|
-
"@loopback/
|
|
10
|
-
"@loopback/repository-json-schema": "^1.
|
|
9
|
+
"@loopback/core": "^1.12.3",
|
|
10
|
+
"@loopback/repository-json-schema": "^1.12.1",
|
|
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": "^
|
|
17
|
-
"@loopback/eslint-config": "^
|
|
18
|
-
"@loopback/openapi-spec-builder": "^1.
|
|
19
|
-
"@loopback/repository": "^1.
|
|
20
|
-
"@loopback/testlab": "^1.
|
|
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.0",
|
|
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.
|
|
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": "
|
|
59
|
+
"gitHead": "b5e53892d327c6cf4c6023055d2b38f13fe4729e"
|
|
59
60
|
}
|
package/src/controller-spec.ts
CHANGED
|
@@ -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/
|
|
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
|
|
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,11 +78,52 @@ 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,
|
|
83
125
|
constructor.prototype,
|
|
84
|
-
)
|
|
126
|
+
) ?? {};
|
|
85
127
|
|
|
86
128
|
endpoints = DecoratorFactory.cloneDeep(endpoints);
|
|
87
129
|
for (const op in endpoints) {
|
|
@@ -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,15 +172,39 @@ 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];
|
|
123
206
|
if (isReferenceObject(responseObject)) continue;
|
|
124
|
-
const content = responseObject.content
|
|
207
|
+
const content = responseObject.content ?? {};
|
|
125
208
|
for (const c in content) {
|
|
126
209
|
debug(' processing response code %s with content-type %', code, c);
|
|
127
210
|
processSchemaExtensions(spec, content[c].schema);
|
|
@@ -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
|
-
!
|
|
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
|
-
|
|
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
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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/
|
|
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
|
+
}
|
package/src/decorators/index.ts
CHANGED
|
@@ -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/
|
|
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/
|
|
6
|
+
import {MetadataInspector, ParameterDecoratorFactory} from '@loopback/core';
|
|
7
7
|
import {resolveSchema} from '../generate-schema';
|
|
8
8
|
import {OAI3Keys} from '../keys';
|
|
9
9
|
import {
|
|
@@ -37,7 +37,7 @@ export function param(paramSpec: ParameterObject) {
|
|
|
37
37
|
paramSpec = paramSpec || {};
|
|
38
38
|
// Get the design time method parameter metadata
|
|
39
39
|
const methodSig = MetadataInspector.getDesignTypeForMethod(target, member);
|
|
40
|
-
const paramTypes =
|
|
40
|
+
const paramTypes = methodSig?.parameterTypes || [];
|
|
41
41
|
|
|
42
42
|
// Map design-time parameter type to the OpenAPI param type
|
|
43
43
|
|
|
@@ -3,8 +3,8 @@
|
|
|
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/
|
|
7
|
-
import
|
|
6
|
+
import {MetadataInspector, ParameterDecoratorFactory} from '@loopback/core';
|
|
7
|
+
import _ from 'lodash';
|
|
8
8
|
import {inspect} from 'util';
|
|
9
9
|
import {resolveSchema} from '../generate-schema';
|
|
10
10
|
import {OAI3Keys} from '../keys';
|
|
@@ -86,14 +86,14 @@ export function requestBody(requestBodySpec?: Partial<RequestBodyObject>) {
|
|
|
86
86
|
debug(' options: %s', inspect(requestBodySpec, {depth: null}));
|
|
87
87
|
|
|
88
88
|
// Use 'application/json' as default content if `requestBody` is undefined
|
|
89
|
-
requestBodySpec = requestBodySpec
|
|
89
|
+
requestBodySpec = requestBodySpec ?? {content: {}};
|
|
90
90
|
|
|
91
91
|
if (_.isEmpty(requestBodySpec.content))
|
|
92
92
|
requestBodySpec.content = {'application/json': {}};
|
|
93
93
|
|
|
94
94
|
// Get the design time method parameter metadata
|
|
95
95
|
const methodSig = MetadataInspector.getDesignTypeForMethod(target, member);
|
|
96
|
-
const paramTypes =
|
|
96
|
+
const paramTypes = methodSig?.parameterTypes || [];
|
|
97
97
|
|
|
98
98
|
const paramType = paramTypes[index];
|
|
99
99
|
const schema = resolveSchema(paramType);
|
|
@@ -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
|
+
}
|