@@ -16,6 +16,9 @@ import {
16
16
17
17
const debug = require ( 'debug' ) ( 'loopback:core:router:metadata' ) ;
18
18
19
+ const ENDPOINTS_KEY = 'rest:endpoints' ;
20
+ const API_SPEC_KEY = 'rest:api-spec' ;
21
+
19
22
// tslint:disable:no-any
20
23
21
24
export interface ControllerSpec {
@@ -36,6 +39,14 @@ export interface ControllerSpec {
36
39
* Decorate the given Controller constructor with metadata describing
37
40
* the HTTP/REST API the Controller implements/provides.
38
41
*
42
+ * `@api` can be applied to controller classes. For example,
43
+ * ```
44
+ * @api ({basePath: '/my'})
45
+ * class MyController {
46
+ * // ...
47
+ * }
48
+ * ```
49
+ *
39
50
* @param spec OpenAPI specification describing the endpoints
40
51
* handled by this controller
41
52
*
@@ -47,95 +58,89 @@ export function api(spec: ControllerSpec) {
47
58
typeof constructor === 'function' ,
48
59
'The @api decorator can be applied to constructors only.' ,
49
60
) ;
50
- Reflector . defineMetadata ( 'loopback:api-spec' , spec , constructor ) ;
61
+ const apiSpec = resolveControllerSpec ( constructor , spec ) ;
62
+ Reflector . defineMetadata ( API_SPEC_KEY , apiSpec , constructor ) ;
51
63
} ;
52
64
}
53
65
66
+ /**
67
+ * Data structure for REST related metadata
68
+ */
54
69
interface RestEndpoint {
55
70
verb : string ;
56
71
path : string ;
72
+ spec ?: OperationObject ;
73
+ target : any ;
57
74
}
58
75
59
- export function getControllerSpec ( constructor : Function ) : ControllerSpec {
76
+ /**
77
+ * Build the api spec from class and method level decorations
78
+ * @param constructor Controller class
79
+ * @param spec API spec
80
+ */
81
+ function resolveControllerSpec (
82
+ constructor : Function ,
83
+ spec ?: ControllerSpec ,
84
+ ) : ControllerSpec {
60
85
debug ( `Retrieving OpenAPI specification for controller ${ constructor . name } ` ) ;
61
86
62
- let spec : ControllerSpec = Reflector . getMetadata (
63
- 'loopback:api-spec' ,
64
- constructor ,
65
- ) ;
66
-
67
87
if ( spec ) {
68
88
debug ( ' using class-level spec defined via @api()' , spec ) ;
69
- return spec ;
89
+ spec = Object . assign ( { } , spec ) ;
90
+ } else {
91
+ spec = { paths : { } } ;
70
92
}
71
93
72
- spec = { paths : { } } ;
73
- for (
74
- let proto = constructor . prototype ;
75
- proto && proto !== Object . prototype ;
76
- proto = Object . getPrototypeOf ( proto )
77
- ) {
78
- addPrototypeMethodsToSpec ( spec , proto ) ;
79
- }
80
- return spec ;
81
- }
82
-
83
- function addPrototypeMethodsToSpec ( spec : ControllerSpec , proto : any ) {
84
- const controllerMethods = Object . getOwnPropertyNames ( proto ) . filter (
85
- key => key !== 'constructor' && typeof proto [ key ] === 'function' ,
86
- ) ;
87
- for ( const methodName of controllerMethods ) {
88
- addControllerMethodToSpec ( spec , proto , methodName ) ;
89
- }
90
- }
94
+ const endpoints =
95
+ Reflector . getMetadata ( ENDPOINTS_KEY , constructor . prototype ) || { } ;
96
+
97
+ for ( const op in endpoints ) {
98
+ const endpoint = endpoints [ op ] ;
99
+ const className =
100
+ endpoint . target . constructor . name ||
101
+ constructor . name ||
102
+ '<AnonymousClass>' ;
103
+ const fullMethodName = `${ className } .${ op } ` ;
104
+
105
+ const { verb, path} = endpoint ;
106
+ const endpointName = `${ fullMethodName } (${ verb } ${ path } )` ;
107
+
108
+ let operationSpec = endpoint . spec ;
109
+ if ( ! operationSpec ) {
110
+ // The operation was defined via @operation (verb, path) with no spec
111
+ operationSpec = {
112
+ responses : { } ,
113
+ } ;
114
+ }
91
115
92
- function addControllerMethodToSpec (
93
- spec : ControllerSpec ,
94
- proto : any ,
95
- methodName : string ,
96
- ) {
97
- const className = proto . constructor . name || '<UnknownClass>' ;
98
- const fullMethodName = `${ className } .${ methodName } ` ;
99
-
100
- const endpoint : RestEndpoint = Reflector . getMetadata (
101
- 'loopback:operation-endpoint' ,
102
- proto ,
103
- methodName ,
104
- ) ;
105
-
106
- if ( ! endpoint ) {
107
- debug ( ` skipping ${ fullMethodName } - no endpoint is defined` ) ;
108
- return ;
109
- }
116
+ if ( ! spec . paths [ path ] ) {
117
+ spec . paths [ path ] = { } ;
118
+ }
110
119
111
- const { verb, path} = endpoint ;
112
- const endpointName = `${ fullMethodName } (${ verb } ${ path } )` ;
113
-
114
- let operationSpec = Reflector . getMetadata (
115
- 'loopback:operation-spec' ,
116
- proto ,
117
- methodName ,
118
- ) ;
119
- if ( ! operationSpec ) {
120
- // The operation was defined via @operation (verb, path) with no spec
121
- operationSpec = {
122
- responses : { } ,
123
- } ;
124
- }
120
+ if ( spec . paths [ path ] [ verb ] ) {
121
+ // Operations from subclasses override those from the base
122
+ debug ( ` Overriding ${ endpointName } - endpoint was already defined` ) ;
123
+ }
125
124
126
- if ( ! spec . paths [ path ] ) {
127
- spec . paths [ path ] = { } ;
125
+ debug ( ` adding ${ endpointName } ` , operationSpec ) ;
126
+ spec . paths [ path ] [ verb ] = Object . assign ( { } , operationSpec , {
127
+ 'x-operation-name' : op ,
128
+ } ) ;
128
129
}
130
+ return spec ;
131
+ }
129
132
130
- if ( spec . paths [ path ] [ verb ] ) {
131
- debug ( ` skipping ${ endpointName } - endpoint was already defined` ) ;
132
- return ;
133
+ /**
134
+ * Get the controller spec for the given class
135
+ * @param constructor Controller class
136
+ */
137
+ export function getControllerSpec ( constructor : Function ) : ControllerSpec {
138
+ let spec = Reflector . getMetadata ( API_SPEC_KEY , constructor ) ;
139
+ if ( ! spec ) {
140
+ spec = resolveControllerSpec ( constructor , spec ) ;
141
+ Reflector . defineMetadata ( API_SPEC_KEY , spec , constructor ) ;
133
142
}
134
-
135
- debug ( ` adding ${ endpointName } ` , operationSpec ) ;
136
- spec . paths [ path ] [ verb ] = Object . assign ( { } , operationSpec , {
137
- 'x-operation-name' : methodName ,
138
- } ) ;
143
+ return spec ;
139
144
}
140
145
141
146
/**
@@ -207,20 +212,35 @@ export function del(path: string, spec?: OperationObject) {
207
212
* of this operation.
208
213
*/
209
214
export function operation ( verb : string , path : string , spec ?: OperationObject ) {
210
- // tslint:disable-next-line:no-any
211
215
return function (
212
- target : object ,
216
+ target : any ,
213
217
propertyKey : string ,
214
218
descriptor : PropertyDescriptor ,
215
219
) {
216
- const endpoint : RestEndpoint = { verb, path} ;
217
- Reflector . defineMetadata (
218
- 'loopback:operation-endpoint' ,
219
- endpoint ,
220
- target ,
221
- propertyKey ,
220
+ assert (
221
+ typeof target [ propertyKey ] === 'function' ,
222
+ '@operation decorator can be applied to methods only' ,
222
223
) ;
223
224
225
+ let endpoints = Object . assign (
226
+ { } ,
227
+ Reflector . getMetadata ( ENDPOINTS_KEY , target ) ,
228
+ ) ;
229
+ Reflector . defineMetadata ( ENDPOINTS_KEY , endpoints , target ) ;
230
+
231
+ let endpoint : Partial < RestEndpoint > = endpoints [ propertyKey ] ;
232
+ if ( ! endpoint ) {
233
+ // Add the new endpoint metadata for the method
234
+ endpoint = { verb, path, spec, target} ;
235
+ endpoints [ propertyKey ] = endpoint ;
236
+ } else {
237
+ // Update the endpoint metadata
238
+ // It can be created by @param
239
+ endpoint . verb = verb ;
240
+ endpoint . path = path ;
241
+ endpoint . target = target ;
242
+ }
243
+
224
244
if ( ! spec ) {
225
245
// Users can define parameters and responses using decorators
226
246
return ;
@@ -231,7 +251,7 @@ export function operation(verb: string, path: string, spec?: OperationObject) {
231
251
// will invoke param() decorator first and operation() second.
232
252
// As a result, we need to preserve any partial definitions
233
253
// already provided by other decorators.
234
- editOperationSpec ( target , propertyKey , overrides => {
254
+ editOperationSpec ( endpoint , overrides => {
235
255
const mergedSpec = Object . assign ( { } , spec , overrides ) ;
236
256
237
257
// Merge "responses" definitions
@@ -285,7 +305,20 @@ export function param(paramSpec: ParameterObject) {
285
305
'@param decorator can be applied to methods only' ,
286
306
) ;
287
307
288
- editOperationSpec ( target , propertyKey , operationSpec => {
308
+ let endpoints = Object . assign (
309
+ { } ,
310
+ Reflector . getMetadata ( ENDPOINTS_KEY , target ) ,
311
+ ) ;
312
+ Reflector . defineMetadata ( ENDPOINTS_KEY , endpoints , target ) ;
313
+
314
+ let endpoint : Partial < RestEndpoint > = endpoints [ propertyKey ] ;
315
+ if ( ! endpoint ) {
316
+ // Add the new endpoint metadata for the method
317
+ endpoint = { target} ;
318
+ endpoints [ propertyKey ] = endpoint ;
319
+ }
320
+
321
+ editOperationSpec ( endpoint , operationSpec => {
289
322
let decoratorStyle ;
290
323
if ( typeof descriptorOrParameterIndex === 'number' ) {
291
324
decoratorStyle = 'parameter' ;
@@ -318,30 +351,18 @@ export function param(paramSpec: ParameterObject) {
318
351
}
319
352
320
353
function editOperationSpec (
321
- target : any ,
322
- propertyKey : string ,
354
+ endpoint : Partial < RestEndpoint > ,
323
355
updateFn : ( spec : OperationObject ) => OperationObject ,
324
356
) {
325
- let spec : OperationObject = Reflector . getMetadata (
326
- 'loopback:operation-spec' ,
327
- target ,
328
- propertyKey ,
329
- ) ;
330
-
357
+ let spec = endpoint . spec ;
331
358
if ( ! spec ) {
332
359
spec = {
333
360
responses : { } ,
334
361
} ;
335
362
}
336
363
337
364
spec = updateFn ( spec ) ;
338
-
339
- Reflector . defineMetadata (
340
- 'loopback:operation-spec' ,
341
- spec ,
342
- target ,
343
- propertyKey ,
344
- ) ;
365
+ endpoint . spec = spec ;
345
366
}
346
367
347
368
export namespace param {
0 commit comments