Skip to content

Commit

Permalink
chore: resolve conflicts, tiny updates
Browse files Browse the repository at this point in the history
  • Loading branch information
kamilmysliwiec committed Oct 24, 2024
2 parents 787e9f4 + a995c97 commit a8d64ed
Show file tree
Hide file tree
Showing 18 changed files with 523 additions and 22 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ The following decorators have been changed/renamed:
- `@ApiOperation({ title: 'test' })` is now `@ApiOperation({ summary: 'test' })`
- `@ApiUseTags` is now `@ApiTags`

The following decorators have been added:

- `@ApiDefaultGetter` to generate [link objects](https://swagger.io/docs/specification/links/) together with `@ApiProperty({link: () => Type})`
- `@ApiLink` to directly generate link objects

`DocumentBuilder` breaking changes (updated method signatures):

- `addTag`
Expand Down
33 changes: 32 additions & 1 deletion e2e/api-spec.json
Original file line number Diff line number Diff line change
Expand Up @@ -551,6 +551,14 @@
"responses": {
"200": {
"description": "The found record",
"links": {
"CatsController_findOne_from_kittenIds": {
"operationId": "CatsController_findOne",
"parameters": {
"id": "$response.body#/kittenIds"
}
}
},
"content": {
"application/json": {
"schema": {
Expand Down Expand Up @@ -655,6 +663,14 @@
"responses": {
"200": {
"description": "",
"links": {
"CatsController_findOne_from_kittenIds": {
"operationId": "CatsController_findOne",
"parameters": {
"id": "$response.body#/kittenIds"
}
}
},
"content": {
"application/json": {
"schema": {
Expand Down Expand Up @@ -775,6 +791,14 @@
"responses": {
"201": {
"description": "The record has been successfully created.",
"links": {
"CatsController_findOne_from_kittenIds": {
"operationId": "CatsController_findOne",
"parameters": {
"id": "$response.body#/kittenIds"
}
}
},
"content": {
"application/json": {
"schema": {
Expand Down Expand Up @@ -1286,6 +1310,12 @@
}
],
"description": "Array of values that uses \"oneOf\""
},
"kittenIds": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
Expand All @@ -1299,7 +1329,8 @@
"enum",
"enumArr",
"enumWithRef",
"oneOfExample"
"oneOfExample",
"kittenIds"
]
},
"Letter": {
Expand Down
2 changes: 2 additions & 0 deletions e2e/src/cats/cats.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
ApiBearerAuth,
ApiBody,
ApiConsumes,
ApiDefaultGetter,
ApiExtension,
ApiHeader,
ApiOperation,
Expand Down Expand Up @@ -94,6 +95,7 @@ export class CatsController {
type: Cat
})
@ApiExtension('x-auth-type', 'NONE')
@ApiDefaultGetter(Cat, 'id')
findOne(@Param('id') id: string): Cat {
return this.catsService.findOne(+id);
}
Expand Down
3 changes: 3 additions & 0 deletions e2e/src/cats/classes/cat.class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,7 @@ export class Cat {
description: 'Array of values that uses "oneOf"'
})
oneOfExample?: string[] | number[] | boolean[];

@ApiProperty({ type: [String], link: () => Cat })
kittenIds?: string[];
}
4 changes: 3 additions & 1 deletion lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,7 @@ export const DECORATORS = {
API_EXCLUDE_CONTROLLER: `${DECORATORS_PREFIX}/apiExcludeController`,
API_EXTRA_MODELS: `${DECORATORS_PREFIX}/apiExtraModels`,
API_EXTENSION: `${DECORATORS_PREFIX}/apiExtension`,
API_SCHEMA: `${DECORATORS_PREFIX}/apiSchema`
API_SCHEMA: `${DECORATORS_PREFIX}/apiSchema`,
API_DEFAULT_GETTER: `${DECORATORS_PREFIX}/apiDefaultGetter`,
API_LINK: `${DECORATORS_PREFIX}/apiLink`
};
45 changes: 45 additions & 0 deletions lib/decorators/api-default-getter.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { Type } from '@nestjs/common';
import { DECORATORS } from '../constants';

/**
* Set the default getter for the given type to the decorated method
*
* This is to be used in conjunction with `ApiProperty({link: () => Type})` to generate link objects
* in the swagger schema
*
* ```typescript
* @Controller('users')
* class UserController {
* @Get(':userId')
* @ApiDefaultGetter(UserGet, 'userId')
* getUser(@Param('userId') userId: string) {
* // ...
* }
* }
* ```
*
* @param type The type for which the decorated function is the default getter
* @param parameter Name of the parameter in the route of the getter which corresponds to the id of the type
*
* @see [Swagger link objects](https://swagger.io/docs/specification/links/)
*/
export function ApiDefaultGetter(
type: Type<unknown> | Function,
parameter: string
): MethodDecorator {
return (
prototype: object,
key: string | symbol,
descriptor: PropertyDescriptor
) => {
if (type.prototype) {
Reflect.defineMetadata(
DECORATORS.API_DEFAULT_GETTER,
{ getter: descriptor.value, parameter, prototype },
type.prototype
);
}

return descriptor;
};
}
76 changes: 76 additions & 0 deletions lib/decorators/api-link.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { Type } from '@nestjs/common';
import { DECORATORS } from '../constants';

export interface ApiLinkOptions {
from: Type<unknown> | Function;
/**
* Field in the type `from` which is used as a parameter in the decorated route
*
* @default 'id'
*/
fromField?: string;
/**
* Name of the parameter in the decorated route
*/
routeParam: string;
}

/**
* Defines this route as a link between two types
*
* Typically used when the link between the types is not present in the `from` type,
* eg with the following
*
* ```typescript
* class User {
* @ApiProperty()
* id: string
*
* // no field documentIds: string[]
* }
*
* class Document {
* @ApiProperty()
* id: string
* }
*
* @Controller()
* class UserController {
* @Get(':userId/documents')
* @ApiLink({from: User, fromField: 'id', routeParam: 'userId'})
* getDocuments(@Param('userId') userId: string)): Promise<Documents[]>
* }
* ```
*
* @param type The type for which the decorated function is the default getter
* @param parameter Name of the parameter in the route of the getter which corresponds to the id of the type
*
* @see [Swagger link objects](https://swagger.io/docs/specification/links/)
*/
export function ApiLink({
from,
fromField = 'id',
routeParam
}: ApiLinkOptions): MethodDecorator {
return (
controllerPrototype: object,
key: string | symbol,
descriptor: PropertyDescriptor
) => {
const { prototype } = from;
if (prototype) {
const links = Reflect.getMetadata(DECORATORS.API_LINK, prototype) ?? [];

links.push({
method: descriptor.value,
prototype: controllerPrototype,
field: fromField,
parameter: routeParam
});

Reflect.defineMetadata(DECORATORS.API_LINK, links, prototype);
}

return descriptor;
};
}
11 changes: 11 additions & 0 deletions lib/decorators/api-property.decorator.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Type } from '@nestjs/common';
import { DECORATORS } from '../constants';
import { SchemaObjectMetadata } from '../interfaces/schema-object-metadata.interface';
import { getEnumType, getEnumValues } from '../utils/enum.utils';
Expand All @@ -9,6 +10,16 @@ export interface ApiPropertyOptions
enum?: any[] | Record<string, any> | (() => any[] | Record<string, any>);
enumName?: string;
'x-enumNames'?: string[];
/**
* Lazy function returning the type for which the decorated property
* can be used as an id
*
* Use together with @ApiDefaultGetter on the getter route of the type
* to generate OpenAPI link objects
*
* @see [Swagger link objects](https://swagger.io/docs/specification/links/)
*/
link?: () => Type<unknown> | Function;
}

const isEnumArray = (obj: ApiPropertyOptions): boolean =>
Expand Down
2 changes: 2 additions & 0 deletions lib/decorators/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ export * from './api-bearer.decorator';
export * from './api-body.decorator';
export * from './api-consumes.decorator';
export * from './api-cookie.decorator';
export * from './api-default-getter.decorator';
export * from './api-exclude-endpoint.decorator';
export * from './api-exclude-controller.decorator';
export * from './api-extra-models.decorator';
export * from './api-header.decorator';
export * from './api-hide-property.decorator';
export * from './api-link.decorator';
export * from './api-oauth2.decorator';
export * from './api-operation.decorator';
export * from './api-param.decorator';
Expand Down
28 changes: 22 additions & 6 deletions lib/explorers/api-response.explorer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,18 @@ import { DECORATORS } from '../constants';
import { ApiResponse, ApiResponseMetadata } from '../decorators';
import { SchemaObject } from '../interfaces/open-api-spec.interface';
import { METADATA_FACTORY_NAME } from '../plugin/plugin-constants';
import { ResponseObjectFactory } from '../services/response-object-factory';
import {
FactoriesNeededByResponseFactory,
ResponseObjectFactory
} from '../services/response-object-factory';
import { mergeAndUniq } from '../utils/merge-and-uniq.util';

const responseObjectFactory = new ResponseObjectFactory();

export const exploreGlobalApiResponseMetadata = (
schemas: Record<string, SchemaObject>,
metatype: Type<unknown>
metatype: Type<unknown>,
factories: FactoriesNeededByResponseFactory
) => {
const responses: ApiResponseMetadata[] = Reflect.getMetadata(
DECORATORS.API_RESPONSE,
Expand All @@ -22,13 +26,19 @@ export const exploreGlobalApiResponseMetadata = (
const produces = Reflect.getMetadata(DECORATORS.API_PRODUCES, metatype);
return responses
? {
responses: mapResponsesToSwaggerResponses(responses, schemas, produces)
responses: mapResponsesToSwaggerResponses(
responses,
schemas,
produces,
factories
)
}
: undefined;
};

export const exploreApiResponseMetadata = (
schemas: Record<string, SchemaObject>,
factories: FactoriesNeededByResponseFactory,
instance: object,
prototype: Type<unknown>,
method: Function
Expand All @@ -46,7 +56,12 @@ export const exploreApiResponseMetadata = (
get(classProduces, 'produces'),
methodProduces
);
return mapResponsesToSwaggerResponses(responses, schemas, produces);
return mapResponsesToSwaggerResponses(
responses,
schemas,
produces,
factories
);
}
const status = getStatusCode(method);
if (status) {
Expand Down Expand Up @@ -76,14 +91,15 @@ const omitParamType = (param: Record<string, any>) => omit(param, 'type');
const mapResponsesToSwaggerResponses = (
responses: ApiResponseMetadata[],
schemas: Record<string, SchemaObject>,
produces: string[] = ['application/json']
produces: string[] = ['application/json'],
factories: FactoriesNeededByResponseFactory
) => {
produces = isEmpty(produces) ? ['application/json'] : produces;

const openApiResponses = mapValues(
responses,
(response: ApiResponseMetadata) =>
responseObjectFactory.create(response, produces, schemas)
responseObjectFactory.create(response, produces, schemas, factories)
);
return mapValues(openApiResponses, omitParamType);
};
Expand Down
6 changes: 6 additions & 0 deletions lib/extra/swagger-shim.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,12 @@ export function ApiTags() {
export function ApiCallbacks() {
return () => {};
}
export function ApiLink() {
return () => {};
}
export function ApiDefaultGetter() {
return () => {};
}
export function ApiExtension() {
return () => {};
}
Expand Down
18 changes: 16 additions & 2 deletions lib/interfaces/swagger-document-options.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,22 @@ export interface SwaggerDocumentOptions {

/**
* Custom operationIdFactory that will be used to generate the `operationId`
* based on the `controllerKey` and `methodKey`
* @default () => controllerKey_methodKey
* based on the `controllerKey`, `methodKey`, and version.
* @default () => controllerKey_methodKey_version
*/
operationIdFactory?: OperationIdFactory;

/**
* Custom linkNameFactory that will be used to generate the name of links
* in the `links` field of responses
*
* @see [Link objects](https://swagger.io/docs/specification/links/)
*
* @default () => `${controllerKey}_${methodKey}_from_${fieldKey}`
*/
linkNameFactory?: (
controllerKey: string,
methodKey: string,
fieldKey: string
) => string;
}
Loading

0 comments on commit a8d64ed

Please sign in to comment.