-
Notifications
You must be signed in to change notification settings - Fork 1.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix(rest): Improve rest metadata inheritance #746
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,6 +4,8 @@ | |
// License text available at https://opensource.org/licenses/MIT | ||
|
||
import * as assert from 'assert'; | ||
import * as _ from 'lodash'; | ||
|
||
import {Reflector} from '@loopback/context'; | ||
import { | ||
OperationObject, | ||
|
@@ -21,6 +23,17 @@ const API_SPEC_KEY = 'rest:api-spec'; | |
|
||
// tslint:disable:no-any | ||
|
||
function cloneDeep<T>(val: T): T { | ||
if (val === undefined) { | ||
return {} as T; | ||
} | ||
return _.cloneDeepWith(val, v => { | ||
// Do not clone functions | ||
if (typeof v === 'function') return v; | ||
return undefined; | ||
}); | ||
} | ||
|
||
export interface ControllerSpec { | ||
/** | ||
* The base path on which the Controller API is served. | ||
|
@@ -73,6 +86,20 @@ interface RestEndpoint { | |
target: any; | ||
} | ||
|
||
function getEndpoints( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we please have ts docs for this function? (or maybe we don't need it because it's internal? |
||
target: any, | ||
): {[property: string]: Partial<RestEndpoint>} { | ||
let endpoints = Reflector.getOwnMetadata(ENDPOINTS_KEY, target); | ||
if (!endpoints) { | ||
// Clone the endpoints so that subclasses won't mutate the metadata | ||
// in the base class | ||
const baseEndpoints = Reflector.getMetadata(ENDPOINTS_KEY, target); | ||
endpoints = cloneDeep(baseEndpoints); | ||
Reflector.defineMetadata(ENDPOINTS_KEY, endpoints, target); | ||
} | ||
return endpoints; | ||
} | ||
|
||
/** | ||
* Build the api spec from class and method level decorations | ||
* @param constructor Controller class | ||
|
@@ -86,33 +113,39 @@ function resolveControllerSpec( | |
|
||
if (spec) { | ||
debug(' using class-level spec defined via @api()', spec); | ||
spec = Object.assign({}, spec); | ||
spec = cloneDeep(spec); | ||
} else { | ||
spec = {paths: {}}; | ||
} | ||
|
||
const endpoints = | ||
Reflector.getMetadata(ENDPOINTS_KEY, constructor.prototype) || {}; | ||
const endpoints = getEndpoints(constructor.prototype); | ||
|
||
for (const op in endpoints) { | ||
const endpoint = endpoints[op]; | ||
const className = | ||
endpoint.target.constructor.name || | ||
constructor.name || | ||
'<AnonymousClass>'; | ||
const fullMethodName = `${className}.${op}`; | ||
|
||
const {verb, path} = endpoint; | ||
const endpointName = `${fullMethodName} (${verb} ${path})`; | ||
const verb = endpoint.verb!; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For my understanding, what does having a |
||
const path = endpoint.path!; | ||
|
||
let endpointName = ''; | ||
if (debug.enabled) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. +1 |
||
const className = | ||
endpoint.target.constructor.name || | ||
constructor.name || | ||
'<AnonymousClass>'; | ||
const fullMethodName = `${className}.${op}`; | ||
endpointName = `${fullMethodName} (${verb} ${path})`; | ||
} | ||
|
||
let operationSpec = endpoint.spec; | ||
if (!operationSpec) { | ||
// The operation was defined via @operation(verb, path) with no spec | ||
operationSpec = { | ||
responses: {}, | ||
}; | ||
endpoint.spec = operationSpec; | ||
} | ||
|
||
operationSpec['x-operation-name'] = op; | ||
|
||
if (!spec.paths[path]) { | ||
spec.paths[path] = {}; | ||
} | ||
|
@@ -123,9 +156,7 @@ function resolveControllerSpec( | |
} | ||
|
||
debug(` adding ${endpointName}`, operationSpec); | ||
spec.paths[path][verb] = Object.assign({}, operationSpec, { | ||
'x-operation-name': op, | ||
}); | ||
spec.paths[path][verb] = operationSpec; | ||
} | ||
return spec; | ||
} | ||
|
@@ -135,7 +166,7 @@ function resolveControllerSpec( | |
* @param constructor Controller class | ||
*/ | ||
export function getControllerSpec(constructor: Function): ControllerSpec { | ||
let spec = Reflector.getMetadata(API_SPEC_KEY, constructor); | ||
let spec = Reflector.getOwnMetadata(API_SPEC_KEY, constructor); | ||
if (!spec) { | ||
spec = resolveControllerSpec(constructor, spec); | ||
Reflector.defineMetadata(API_SPEC_KEY, spec, constructor); | ||
|
@@ -222,14 +253,9 @@ export function operation(verb: string, path: string, spec?: OperationObject) { | |
'@operation decorator can be applied to methods only', | ||
); | ||
|
||
let endpoints = Object.assign( | ||
{}, | ||
Reflector.getMetadata(ENDPOINTS_KEY, target), | ||
); | ||
Reflector.defineMetadata(ENDPOINTS_KEY, endpoints, target); | ||
|
||
let endpoint: Partial<RestEndpoint> = endpoints[propertyKey]; | ||
if (!endpoint) { | ||
const endpoints = getEndpoints(target); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we change the tsdocs for this function to say that child classes can override REST operations? |
||
let endpoint = endpoints[propertyKey]; | ||
if (!endpoint || endpoint.target !== target) { | ||
// Add the new endpoint metadata for the method | ||
endpoint = {verb, path, spec, target}; | ||
endpoints[propertyKey] = endpoint; | ||
|
@@ -305,16 +331,13 @@ export function param(paramSpec: ParameterObject) { | |
'@param decorator can be applied to methods only', | ||
); | ||
|
||
let endpoints = Object.assign( | ||
{}, | ||
Reflector.getMetadata(ENDPOINTS_KEY, target), | ||
); | ||
Reflector.defineMetadata(ENDPOINTS_KEY, endpoints, target); | ||
|
||
let endpoint: Partial<RestEndpoint> = endpoints[propertyKey]; | ||
if (!endpoint) { | ||
const endpoints = getEndpoints(target); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ditto: should we change ts-docs for this function to say that child classes can override REST parameters? |
||
let endpoint = endpoints[propertyKey]; | ||
if (!endpoint || endpoint.target !== target) { | ||
const baseEndpoint = endpoint; | ||
// Add the new endpoint metadata for the method | ||
endpoint = {target}; | ||
endpoint = cloneDeep(baseEndpoint); | ||
endpoint.target = target; | ||
endpoints[propertyKey] = endpoint; | ||
} | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This needs to be in dependencies because we re-export the http-errors objects for use in @loopback/rest.
Removing this will cause compilation failures downstream!