Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions docs/site/decorators/Decorators_openapi.md
Original file line number Diff line number Diff line change
Expand Up @@ -626,6 +626,47 @@ class MyOtherController {
}
```

### @oas.visibility

[API document](https://loopback.io/doc/en/lb4/apidocs.openapi-v3.visibility.html)

This decorator can be applied to class and/or a class method. It will set the
`x-visibility` property, which dictates if a class method appears in the OAS3
spec. When applied to a class, it will mark all operation methods of that class,
unless a method overloads with `@oas.visibility(<different visibility type>)`.

When not explicitly defined, `x-visibility` is `undefined`, which is identical
to `x-visibility: 'documented`.

Currently, the supported values are `documented` or `undocumented`.
Comment thread
achrinza marked this conversation as resolved.

This decorator does not currently support marking
(parameters)[https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#parameter-object].

```ts
@oas.visibility('undocumented')
class MyController {
@oas.get('/greet')
async function greet() {
return 'Hello, World!'
}

@oas.get('/greet-v2')
@oas.visibility('documented')
async function greetV2() {
return 'Hello, World!'
}
}

class MyOtherController {
@oas.get('/echo')
@oas.visibility('undocumented')
async function echo() {
return 'Echo!'
}
}
```

### @oas.response

[API document](https://loopback.io/doc/en/lb4/apidocs.openapi-v3.oas.html#oas-variable),
Expand Down
5 changes: 3 additions & 2 deletions docs/site/extending/rest-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,15 +123,16 @@ export class MyComponent implements Component {

Sometimes it's desirable to treat the new endpoints as internal (undocumented)
and leave them out from the OpenAPI document describing application's REST API.
This can be achieved using custom LoopBack extension `x-visibility`.
This can be achieved using
[`@oas.visibility(OperationVisibility.UNDOCUMENTED)`](../Decorators/Decorators_openapi.md#oasvisibility).

```ts
class MyController {
// constructor

@oas.visibility(OperationVisibility.UNDOCUMENTED)
@get('/health', {
responses: {},
'x-visibility': 'undocumented',
})
health() {
// ...
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// Copyright IBM Corp. 2020. All Rights Reserved.
// Node module: @loopback/openapi-v3
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {anOpenApiSpec} from '@loopback/openapi-spec-builder';
import {expect} from '@loopback/testlab';
import {api, get, getControllerSpec, oas, OperationVisibility} from '../../..';

/**
* Verification of the Controller Spec uses string literals (e.g.
* 'documented', 'undocumented') instead of the `OperationVisibility` enum so
* as to verify that the enum itself did not change.
*/
describe('visibility decorator', () => {
it('Returns a spec with all the items decorated from the class level', () => {
const expectedSpec = anOpenApiSpec()
.withOperationReturningString('get', '/greet', 'greet')
.withOperationReturningString('get', '/echo', 'echo')
.build();

@api(expectedSpec)
@oas.visibility(OperationVisibility.UNDOCUMENTED)
class MyController {
greet() {
return 'Hello world!';
}
echo() {
return 'Hello world!';
}
}

const actualSpec = getControllerSpec(MyController);
expect(actualSpec.paths['/greet'].get['x-visibility']).to.eql(
'undocumented',
);
expect(actualSpec.paths['/echo'].get['x-visibility']).to.eql(
'undocumented',
);
});

it('Returns a spec where only one method is undocumented', () => {
class MyController {
@get('/greet')
greet() {
return 'Hello world!';
}

@get('/echo')
@oas.visibility(OperationVisibility.UNDOCUMENTED)
echo() {
return 'Hello world!';
}
}

const actualSpec = getControllerSpec(MyController);
expect(actualSpec.paths['/greet'].get['x-visibility']).to.be.undefined();
expect(actualSpec.paths['/echo'].get['x-visibility']).to.eql(
'undocumented',
);
});

it('Allows a method to override the visibility of a class', () => {
@oas.visibility(OperationVisibility.UNDOCUMENTED)
class MyController {
@get('/greet')
greet() {
return 'Hello world!';
}

@get('/echo')
echo() {
return 'Hello world!';
}

@get('/yell')
@oas.visibility(OperationVisibility.DOCUMENTED)
yell() {
return 'HELLO WORLD!';
}
}
const actualSpec = getControllerSpec(MyController);
expect(actualSpec.paths['/greet'].get['x-visibility']).to.eql(
'undocumented',
);
expect(actualSpec.paths['/echo'].get['x-visibility']).to.eql(
'undocumented',
);
expect(actualSpec.paths['/yell'].get['x-visibility']).to.be.eql(
'documented',
);
});

it('Allows a class to not be decorated with @oas.visibility at all', () => {
class MyController {
@get('/greet')
greet() {
return 'Hello world!';
}

@get('/echo')
echo() {
return 'Hello world!';
}
}

const actualSpec = getControllerSpec(MyController);
expect(actualSpec.paths['/greet'].get['x-visibility']).to.be.undefined();
expect(actualSpec.paths['/echo'].get['x-visibility']).to.be.undefined();
});

it('Does not allow a member variable to be decorated', () => {
const shouldThrow = () => {
class MyController {
@oas.visibility(OperationVisibility.UNDOCUMENTED)
public foo: string;

@get('/greet')
greet() {}
}

return getControllerSpec(MyController);
};

expect(shouldThrow).to.throw(
/^\@oas.visibility cannot be used on a property:/,
);
});
});
37 changes: 35 additions & 2 deletions packages/openapi-v3/src/controller-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
ISpecificationExtension,
isReferenceObject,
OperationObject,
OperationVisibility,
ParameterObject,
PathObject,
ReferenceObject,
Expand Down Expand Up @@ -93,17 +94,31 @@ function resolveControllerSpec(constructor: Function): ControllerSpec {
constructor,
);

const classVisibility = MetadataInspector.getClassMetadata<
OperationVisibility
>(OAI3Keys.VISIBILITY_CLASS_KEY, constructor);

if (classVisibility) {
debug(` using class-level @oas.visibility(): '${classVisibility}'`);
}

if (classTags) {
debug(' using class-level @oas.tags()');
}

if (classTags || isClassDeprecated) {
if (classTags || isClassDeprecated || classVisibility) {
for (const path of Object.keys(spec.paths)) {
for (const method of Object.keys(spec.paths[path])) {
/* istanbul ignore else */
if (isClassDeprecated) {
spec.paths[path][method].deprecated = true;
}

/* istanbul ignore else */
if (classVisibility) {
spec.paths[path][method]['x-visibility'] = classVisibility;
}

/* istanbul ignore else */
if (classTags) {
if (spec.paths[path][method].tags?.length) {
Expand Down Expand Up @@ -141,6 +156,16 @@ function resolveControllerSpec(constructor: Function): ControllerSpec {
debug(' using method-level deprecation via @deprecated()');
}

const methodVisibility = MetadataInspector.getMethodMetadata<
OperationVisibility
>(OAI3Keys.VISIBILITY_METHOD_KEY, constructor.prototype, op);

if (methodVisibility) {
debug(
` using method-level visibility via @visibility(): '${methodVisibility}'`,
);
}

const methodTags = MetadataInspector.getMethodMetadata<
TagsDecoratorMetadata
>(OAI3Keys.TAGS_METHOD_KEY, constructor.prototype, op);
Expand Down Expand Up @@ -202,7 +227,7 @@ function resolveControllerSpec(constructor: Function): ControllerSpec {

debug(' spec responses for method %s: %o', op, operationSpec.responses);

// Prescedence: method decorator > class decorator > operationSpec > undefined
// Precedence: method decorator > class decorator > operationSpec > undefined
const deprecationSpec =
isMethodDeprecated ??
isClassDeprecated ??
Expand All @@ -213,6 +238,14 @@ function resolveControllerSpec(constructor: Function): ControllerSpec {
operationSpec.deprecated = true;
}

// Precedence: method decorator > class decorator > operationSpec > 'documented'
const visibilitySpec: OperationVisibility =
methodVisibility ?? classVisibility ?? operationSpec['x-visibility'];

if (visibilitySpec) {
operationSpec['x-visibility'] = visibilitySpec;
}

for (const code in operationSpec.responses) {
const responseObject: ResponseObject | ReferenceObject =
operationSpec.responses[code];
Expand Down
3 changes: 3 additions & 0 deletions packages/openapi-v3/src/decorators/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export * from './deprecated.decorator';
export * from './operation.decorator';
export * from './parameter.decorator';
export * from './request-body.decorator';
export * from './visibility.decorator';

import {api} from './api.decorator';
import {deprecated} from './deprecated.decorator';
Expand All @@ -16,6 +17,7 @@ import {param} from './parameter.decorator';
import {requestBody} from './request-body.decorator';
import {response} from './response.decorator';
import {tags} from './tags.decorator';
import {visibility} from './visibility.decorator';

export const oas = {
api,
Expand All @@ -38,4 +40,5 @@ export const oas = {
deprecated,
response,
tags,
visibility,
};
88 changes: 88 additions & 0 deletions packages/openapi-v3/src/decorators/visibility.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// Copyright IBM Corp. 2020. All Rights Reserved.
// Node module: @loopback/openapi-v3
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {
ClassDecoratorFactory,
DecoratorFactory,
MethodDecoratorFactory,
} from '@loopback/core';
import {OAI3Keys} from '../keys';
import {OperationVisibility} from '../types';

const debug = require('debug')(
'loopback:openapi3:metadata:controller-spec:visibility',
);

/**
* Marks an api path with the specfied visibility. When applied to a class,
* this decorator marks all paths with the specified visibility.
*
* You can optionally mark all controllers in a class with
* `@visibility('undocumented')`, but use `@visibility('documented')`
* on a specific method to ensure it is not marked as `undocumented`.
*
* @param visibilityTyoe - The visbility of the api path on the OAS3 spec.
*
* @example
* ```ts
* @oas.visibility('undocumented')
* class MyController {
* @get('/greet')
* async function greet() {
* return 'Hello, World!'
* }
*
* @get('/greet-v2')
* @oas.deprecated('documented')
* async function greetV2() {
* return 'Hello, World!'
* }
* }
*
* class MyOtherController {
* @get('/echo')
* async function echo() {
* return 'Echo!'
* }
* }
* ```
*/
export function visibility(visibilityType: OperationVisibility) {
return function visibilityDecoratorForClassOrMethod(
// Class or a prototype
// eslint-disable-next-line @typescript-eslint/no-explicit-any
target: any,
method?: string,
// Use `any` to for `TypedPropertyDescriptor`
// See https://github.com/strongloop/loopback-next/pull/2704
// eslint-disable-next-line @typescript-eslint/no-explicit-any
methodDescriptor?: TypedPropertyDescriptor<any>,
) {
debug(target, method, methodDescriptor);

if (method && methodDescriptor) {
// Method
return MethodDecoratorFactory.createDecorator<OperationVisibility>(
OAI3Keys.VISIBILITY_METHOD_KEY,
visibilityType,
{
decoratorName: '@oas.visibility',
},
)(target, method, methodDescriptor);
} else if (typeof target === 'function' && !method && !methodDescriptor) {
// Class
return ClassDecoratorFactory.createDecorator<OperationVisibility>(
OAI3Keys.VISIBILITY_CLASS_KEY,
visibilityType,
{decoratorName: '@oas.visibility'},
)(target);
} else {
throw new Error(
'@oas.visibility cannot be used on a property: ' +
DecoratorFactory.getTargetName(target, method, methodDescriptor),
);
}
};
}
Loading