From 74f77f22212f67ce0849a0bc88f9623d304af92f Mon Sep 17 00:00:00 2001 From: Miguel Savignano Date: Wed, 11 Jan 2023 01:32:23 +0100 Subject: [PATCH] feat(core): Add new decorator ApiCallbacks --- e2e/api-spec.json | 46 ++++++++++++++++- e2e/src/cats/cats.controller.ts | 26 +++++++++- e2e/validate-schema.e2e-spec.ts | 14 ++++++ lib/constants.ts | 1 + lib/decorators/api-callbacks.decorator.ts | 7 +++ lib/decorators/index.ts | 1 + lib/explorers/api-callbacks.explorer.ts | 49 +++++++++++++++++++ lib/extra/swagger-shim.ts | 3 ++ lib/interfaces/callback-object.interface.ts | 12 +++++ .../denormalized-doc-resolvers.interface.ts | 1 + lib/swagger-explorer.ts | 2 + 11 files changed, 160 insertions(+), 2 deletions(-) create mode 100644 lib/decorators/api-callbacks.decorator.ts create mode 100644 lib/explorers/api-callbacks.explorer.ts create mode 100644 lib/interfaces/callback-object.interface.ts diff --git a/e2e/api-spec.json b/e2e/api-spec.json index ea765f814..c17f8ed12 100644 --- a/e2e/api-spec.json +++ b/e2e/api-spec.json @@ -170,7 +170,51 @@ { "basic": [] } - ] + ], + "callbacks": { + "myEvent": { + "{$request.body#/callbackUrl}": { + "post": { + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Cat" + } + } + } + }, + "responses": { + "200": { + "description": "Your server returns this code if it accepts the callback" + } + } + } + } + }, + "mySecondEvent": { + "{$request.body#/callbackUrl}": { + "post": { + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Cat" + } + } + } + }, + "responses": { + "200": { + "description": "Your server returns this code if it accepts the callback" + } + } + } + } + } + } }, "get": { "operationId": "CatsController_findAll", diff --git a/e2e/src/cats/cats.controller.ts b/e2e/src/cats/cats.controller.ts index 89300f136..0888abb06 100644 --- a/e2e/src/cats/cats.controller.ts +++ b/e2e/src/cats/cats.controller.ts @@ -9,7 +9,9 @@ import { ApiQuery, ApiResponse, ApiSecurity, - ApiTags + ApiTags, + ApiCallbacks, + getSchemaPath } from '../../../lib'; import { CatsService } from './cats.service'; import { Cat } from './classes/cat.class'; @@ -30,6 +32,28 @@ import { LettersEnum, PaginationQuery } from './dto/pagination-query.dto'; export class CatsController { constructor(private readonly catsService: CatsService) {} + @ApiCallbacks({ + name: 'mySecondEvent', + callbackUrl: '{$request.body#/callbackUrl}', + method: 'post', + requestBody: { + type: Cat + }, + expectedResponse: { + status: 200 + } + }) + @ApiCallbacks({ + name: 'myEvent', + callbackUrl: '{$request.body#/callbackUrl}', + method: 'post', + requestBody: { + type: Cat + }, + expectedResponse: { + status: 200 + } + }) @ApiTags('create cats') @Post() @ApiOperation({ summary: 'Create cat' }) diff --git a/e2e/validate-schema.e2e-spec.ts b/e2e/validate-schema.e2e-spec.ts index 06e6dd307..e3409811e 100644 --- a/e2e/validate-schema.e2e-spec.ts +++ b/e2e/validate-schema.e2e-spec.ts @@ -57,6 +57,20 @@ describe('Validate OpenAPI schema', () => { api.info.version ); expect(api.info.title).toEqual('Cats example'); + expect( + api.paths['/api/cats']['post']['callbacks']['myEvent'][ + '{$request.body#/callbackUrl}' + ]['post']['requestBody']['content']['application/json']['schema'][ + 'properties' + ]['breed']['type'] + ).toEqual('string'); + expect( + api.paths['/api/cats']['post']['callbacks']['mySecondEvent'][ + '{$request.body#/callbackUrl}' + ]['post']['requestBody']['content']['application/json']['schema'][ + 'properties' + ]['breed']['type'] + ).toEqual('string'); expect(api.paths['/api/cats']['get']['x-codeSamples'][0]['lang']).toEqual( 'JavaScript' ); diff --git a/lib/constants.ts b/lib/constants.ts index 158de3624..775c0fdd7 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -5,6 +5,7 @@ export const DECORATORS = { API_PRODUCES: `${DECORATORS_PREFIX}/apiProduces`, API_CONSUMES: `${DECORATORS_PREFIX}/apiConsumes`, API_TAGS: `${DECORATORS_PREFIX}/apiUseTags`, + API_CALLBACKS: `${DECORATORS_PREFIX}/apiCallbacks`, API_PARAMETERS: `${DECORATORS_PREFIX}/apiParameters`, API_HEADERS: `${DECORATORS_PREFIX}/apiHeaders`, API_MODEL_PROPERTIES: `${DECORATORS_PREFIX}/apiModelProperties`, diff --git a/lib/decorators/api-callbacks.decorator.ts b/lib/decorators/api-callbacks.decorator.ts new file mode 100644 index 000000000..a3c4bd9ff --- /dev/null +++ b/lib/decorators/api-callbacks.decorator.ts @@ -0,0 +1,7 @@ +import { DECORATORS } from '../constants'; +import { createMixedDecorator } from './helpers'; +import { CallBackObject } from '../interfaces/callback-object.interface' + +export function ApiCallbacks(...callbackObject: Array>) { + return createMixedDecorator(DECORATORS.API_CALLBACKS, callbackObject); +} diff --git a/lib/decorators/index.ts b/lib/decorators/index.ts index f507d0720..99033a4b8 100644 --- a/lib/decorators/index.ts +++ b/lib/decorators/index.ts @@ -22,4 +22,5 @@ export * from './api-query.decorator'; export * from './api-response.decorator'; export * from './api-security.decorator'; export * from './api-use-tags.decorator'; +export * from './api-callbacks.decorator'; export * from './api-extension.decorator'; diff --git a/lib/explorers/api-callbacks.explorer.ts b/lib/explorers/api-callbacks.explorer.ts new file mode 100644 index 000000000..4c89b4edf --- /dev/null +++ b/lib/explorers/api-callbacks.explorer.ts @@ -0,0 +1,49 @@ +import { Type } from '@nestjs/common'; +import { DECORATORS } from '../constants'; +import { getSchemaPath } from '../utils'; +import { CallBackObject } from '../interfaces/callback-object.interface' + +export const exploreApiCallbacksMetadata = ( + instance: object, + prototype: Type, + method: object +) => { + const callbacksData = Reflect.getMetadata(DECORATORS.API_CALLBACKS, method); + if (!callbacksData) return callbacksData; + + return callbacksData.reduce((acc, callbackData: CallBackObject) => { + const { + name: eventName, + callbackUrl, + method: callbackMethod, + requestBody, + expectedResponse + } = callbackData; + return { + ...acc, + [eventName]: { + [callbackUrl]: { + [callbackMethod]: { + requestBody: { + required: true, + content: { + 'application/json': { + schema: { + $ref: getSchemaPath(requestBody.type) + } + } + } + }, + responses: { + [expectedResponse.status]: { + description: + expectedResponse.description || + 'Your server returns this code if it accepts the callback' + } + } + } + } + } + }; + }, {}); +}; diff --git a/lib/extra/swagger-shim.ts b/lib/extra/swagger-shim.ts index 9e9d3ca3a..3afd0ec1a 100644 --- a/lib/extra/swagger-shim.ts +++ b/lib/extra/swagger-shim.ts @@ -143,6 +143,9 @@ export function ApiSecurity() { export function ApiTags() { return () => {}; } +export function ApiCallbacks() { + return () => {}; +} export function ApiExtension() { return () => {}; } diff --git a/lib/interfaces/callback-object.interface.ts b/lib/interfaces/callback-object.interface.ts new file mode 100644 index 000000000..3c6ca2b87 --- /dev/null +++ b/lib/interfaces/callback-object.interface.ts @@ -0,0 +1,12 @@ +export interface CallBackObject { + name: string, + callbackUrl: string, + method: string, + requestBody: { + type: T + }, + expectedResponse: { + status: number + description?: string + }, +} diff --git a/lib/interfaces/denormalized-doc-resolvers.interface.ts b/lib/interfaces/denormalized-doc-resolvers.interface.ts index 0f3feadc1..edb765e3a 100644 --- a/lib/interfaces/denormalized-doc-resolvers.interface.ts +++ b/lib/interfaces/denormalized-doc-resolvers.interface.ts @@ -2,5 +2,6 @@ export interface DenormalizedDocResolvers { root: Function[]; security: Function[]; tags: Function[]; + callbacks: Function[]; responses: Function[]; } diff --git a/lib/swagger-explorer.ts b/lib/swagger-explorer.ts index 1ee4a74b5..8b219a3df 100644 --- a/lib/swagger-explorer.ts +++ b/lib/swagger-explorer.ts @@ -32,6 +32,7 @@ import { } from 'lodash'; import * as pathToRegexp from 'path-to-regexp'; import { DECORATORS } from './constants'; +import { exploreApiCallbacksMetadata } from './explorers/api-callbacks.explorer'; import { exploreApiExcludeControllerMetadata } from './explorers/api-exclude-controller.explorer'; import { exploreApiExcludeEndpointMetadata } from './explorers/api-exclude-endpoint.explorer'; import { @@ -96,6 +97,7 @@ export class SwaggerExplorer { ], security: [exploreApiSecurityMetadata], tags: [exploreApiTagsMetadata], + callbacks: [exploreApiCallbacksMetadata], responses: [exploreApiResponseMetadata.bind(null, this.schemas)] }; return this.generateDenormalizedDocument(