├── .nvmrc ├── .husky ├── .gitignore └── pre-commit ├── .prettierignore ├── .npmrc ├── .gitattributes ├── sample ├── felines │ ├── rto │ │ ├── index.ts │ │ └── feline.rto.ts │ ├── felines.constants.ts │ ├── dto │ │ ├── index.ts │ │ ├── journaling-data.dto.ts │ │ └── create-feline.dto.ts │ ├── class │ │ ├── lion.class.ts │ │ ├── tiger.class.ts │ │ ├── index.ts │ │ ├── cat.class.ts │ │ ├── message.class.ts │ │ └── feline.class.ts │ ├── felines.module.ts │ ├── felines.service.ts │ ├── felines.gateway.ts │ └── felines.controller.ts ├── index.ts ├── app.module.ts ├── constants.ts ├── main.ts └── common.ts ├── .prettierrc ├── renovate.json ├── lib ├── binding │ ├── index.ts │ ├── asyncapi.kafka.interfaces.ts │ └── asyncapi.amqp.interfaces.ts ├── explorers │ ├── index.ts │ ├── asyncapi-class.explorer.ts │ └── asyncapi-operation.explorer.ts ├── index.ts ├── interface │ ├── asyncapi-document-options.interface.ts │ ├── asyncapi-operation-headers.interface.ts │ ├── denormalized-doc-resolvers.interface.ts │ ├── asyncapi-operation-payload.interface.ts │ ├── denormalized-doc.interface.ts │ ├── asyncapi-message.interface.ts │ ├── asyncapi-operation-options.interface.ts │ ├── asyncapi-template-options.interface.ts │ ├── asyncapi-server.interface.ts │ ├── index.ts │ ├── asyncapi-operation-options-raw.interface.ts │ ├── generator-options.interface.ts │ └── asyncapi-common.interfaces.ts ├── decorators │ ├── index.ts │ ├── asyncapi-service.decorator.ts │ ├── asyncapi-operation.decorator.ts │ ├── asyncapi-pub.decorator.ts │ ├── asyncapi-sub.decorator.ts │ └── asyncapi-operation-for-meta-key.decorator.ts ├── services │ ├── index.ts │ ├── asyncapi.generator.ts │ ├── asyncapi.transformer.ts │ ├── operation-object.factory.ts │ ├── asyncapi.explorer.ts │ └── asyncapi.scanner.ts ├── asyncapi.constants.ts ├── asyncapi.module.ts └── asyncapi-document.builder.ts ├── tsconfig.build.json ├── .release-it.json ├── misc ├── take-snaphots.sh └── references │ ├── ref.json │ └── ref.yaml ├── nest-cli.json ├── .asyncapi-tool ├── .npmignore ├── test ├── configs │ ├── jest-e2e.config.ts │ ├── jest-base.config.ts │ └── jest-swagger-plugin.js ├── express.e2e-spec.ts └── fastify.e2e-spec.ts ├── .github ├── workflows │ ├── code-quality.yml │ ├── first-interaction.yaml │ └── base.yml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── lock.yml ├── PULL_REQUEST_TEMPLATE.md └── ISSUE_TEMPLATE.md ├── .gitignore ├── tsconfig.json ├── LICENSE ├── .eslintrc.json ├── README.md ├── package.json └── CONTRIBUTING.md /.nvmrc: -------------------------------------------------------------------------------- 1 | 20.11.0 -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | misc/samples -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | misc/** linguist-generated 2 | -------------------------------------------------------------------------------- /sample/felines/rto/index.ts: -------------------------------------------------------------------------------- 1 | export * from './feline.rto'; 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /sample/felines/felines.constants.ts: -------------------------------------------------------------------------------- 1 | export const FELINES_MS = 'FELINES_MS'; 2 | -------------------------------------------------------------------------------- /sample/felines/dto/index.ts: -------------------------------------------------------------------------------- 1 | export * from '../class/message.class'; 2 | export * from './create-feline.dto'; 3 | -------------------------------------------------------------------------------- /sample/index.ts: -------------------------------------------------------------------------------- 1 | export * from './app.module'; 2 | export * from './common'; 3 | export * from './constants'; 4 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx validate-branch-name 5 | npx lint-staged -------------------------------------------------------------------------------- /lib/binding/index.ts: -------------------------------------------------------------------------------- 1 | export * from './asyncapi.amqp.interfaces'; 2 | export * from './asyncapi.kafka.interfaces'; 3 | -------------------------------------------------------------------------------- /lib/explorers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './asyncapi-operation.explorer'; 2 | export * from './asyncapi-class.explorer'; 3 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /.release-it.json: -------------------------------------------------------------------------------- 1 | { 2 | "git": { 3 | "commitMessage": "chore(): release v${version}" 4 | }, 5 | "github": { 6 | "release": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /sample/felines/class/lion.class.ts: -------------------------------------------------------------------------------- 1 | import { Feline } from './feline.class'; 2 | 3 | export class Lion extends Feline { 4 | roarVolume: number; 5 | } 6 | -------------------------------------------------------------------------------- /sample/felines/class/tiger.class.ts: -------------------------------------------------------------------------------- 1 | import { Feline } from './feline.class'; 2 | 3 | export class Tiger extends Feline { 4 | numberOfStripes: number; 5 | } 6 | -------------------------------------------------------------------------------- /lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from './asyncapi-document.builder'; 2 | export * from './interface'; 3 | export * from './asyncapi.module'; 4 | export * from './decorators'; 5 | -------------------------------------------------------------------------------- /lib/interface/asyncapi-document-options.interface.ts: -------------------------------------------------------------------------------- 1 | import { SwaggerDocumentOptions } from '@nestjs/swagger'; 2 | 3 | export interface AsyncApiDocumentOptions extends SwaggerDocumentOptions {} 4 | -------------------------------------------------------------------------------- /lib/interface/asyncapi-operation-headers.interface.ts: -------------------------------------------------------------------------------- 1 | export interface AsyncApiOperationHeaders { 2 | [key: string]: { 3 | description: string; 4 | [key: string]: unknown; 5 | }; 6 | } 7 | -------------------------------------------------------------------------------- /sample/felines/class/index.ts: -------------------------------------------------------------------------------- 1 | export * from './feline.class'; 2 | export * from './cat.class'; 3 | export * from './lion.class'; 4 | export * from './tiger.class'; 5 | export * from './message.class'; 6 | -------------------------------------------------------------------------------- /lib/interface/denormalized-doc-resolvers.interface.ts: -------------------------------------------------------------------------------- 1 | export interface DenormalizedDocResolvers { 2 | root: Function[]; 3 | security: Function[]; 4 | tags: Function[]; 5 | operations: Function[]; 6 | } 7 | -------------------------------------------------------------------------------- /sample/felines/dto/journaling-data.dto.ts: -------------------------------------------------------------------------------- 1 | import { Message } from '../class'; 2 | 3 | export class JournalingDataDto extends Message> { 4 | payload: Record; 5 | } 6 | -------------------------------------------------------------------------------- /lib/decorators/index.ts: -------------------------------------------------------------------------------- 1 | export * from './asyncapi-service.decorator'; 2 | export * from './asyncapi-operation.decorator'; 3 | export * from './asyncapi-sub.decorator'; 4 | export * from './asyncapi-pub.decorator'; 5 | -------------------------------------------------------------------------------- /lib/interface/asyncapi-operation-payload.interface.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@nestjs/common'; 2 | 3 | export type AsyncOperationPayload = 4 | | Type 5 | | Function 6 | | [Function] 7 | | string; 8 | -------------------------------------------------------------------------------- /sample/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { FelinesModule } from './felines/felines.module'; 3 | 4 | @Module({ 5 | imports: [FelinesModule], 6 | }) 7 | export class AppModule {} 8 | -------------------------------------------------------------------------------- /lib/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './asyncapi.generator'; 2 | export * from './operation-object.factory'; 3 | export * from './asyncapi.explorer'; 4 | export * from './asyncapi.scanner'; 5 | export * from './asyncapi.scanner'; 6 | -------------------------------------------------------------------------------- /sample/felines/class/cat.class.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { Feline } from './feline.class'; 3 | 4 | export class Cat extends Feline { 5 | @ApiProperty({ 6 | example: 'Maine Coon', 7 | }) 8 | breed: string; 9 | } 10 | -------------------------------------------------------------------------------- /lib/asyncapi.constants.ts: -------------------------------------------------------------------------------- 1 | const DECORATORS_PREFIX = 'asyncapi'; 2 | 3 | export const DECORATORS = { 4 | AsyncApiClass: `${DECORATORS_PREFIX}/class`, 5 | AsyncApiOperation: `${DECORATORS_PREFIX}/operation`, 6 | AsyncApiPub: `${DECORATORS_PREFIX}/pub`, 7 | AsyncApiSub: `${DECORATORS_PREFIX}/sub`, 8 | }; 9 | -------------------------------------------------------------------------------- /misc/take-snaphots.sh: -------------------------------------------------------------------------------- 1 | curl http://localhost:4001/async-api > misc/references/ref.html 2 | curl http://localhost:4001/async-api > docs/live-demo/index.html 3 | curl http://localhost:4001/async-api-json > misc/references/ref.json 4 | curl http://localhost:4001/async-api-yaml > misc/references/ref.yaml 5 | echo 'test snapshots were taken' 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /lib/decorators/asyncapi-service.decorator.ts: -------------------------------------------------------------------------------- 1 | import { createMixedDecorator } from '@nestjs/swagger/dist/decorators/helpers'; 2 | import { DECORATORS } from '../asyncapi.constants'; 3 | 4 | /** 5 | * Mark class that has to be scanned for AsyncApi operations 6 | */ 7 | export function AsyncApi() { 8 | return createMixedDecorator(DECORATORS.AsyncApiClass, true); 9 | } 10 | -------------------------------------------------------------------------------- /sample/constants.ts: -------------------------------------------------------------------------------- 1 | export const DOC_RELATIVE_PATH = '/async-api'; 2 | export const BOOTSTRAP = 'Bootstrap'; 3 | export const PORT = 4001; 4 | export const HOST = '0.0.0.0'; 5 | export const SERVER = { 6 | europe: 'europe', 7 | asia: 'asia', 8 | northAmerica: 'north-america', 9 | southAmerica: 'south-america', 10 | africa: 'africa', 11 | australia: 'australia', 12 | }; 13 | -------------------------------------------------------------------------------- /lib/interface/denormalized-doc.interface.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AsyncApiDocument, 3 | AsyncChannelObject, 4 | AsyncOperationObject, 5 | } from './asyncapi-common.interfaces'; 6 | 7 | export interface DenormalizedDoc extends Partial { 8 | root?: { name: string } & AsyncChannelObject; 9 | operations?: { pub: AsyncOperationObject; sub: AsyncOperationObject }; 10 | } 11 | -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "sample", 4 | "entryFile": "sample/main", 5 | "compilerOptions": { 6 | "plugins": [ 7 | { 8 | "name": "@nestjs/swagger", 9 | "options": { 10 | "introspectComments": true, 11 | "dtoFileNameSuffix": [".dto.ts", ".class.ts"] 12 | } 13 | } 14 | ] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/interface/asyncapi-message.interface.ts: -------------------------------------------------------------------------------- 1 | import { AsyncApiOperationHeaders } from './asyncapi-operation-headers.interface'; 2 | import { AsyncOperationPayload } from './asyncapi-operation-payload.interface'; 3 | 4 | export interface OneAsyncApiMessage { 5 | name?: string; 6 | payload: AsyncOperationPayload; 7 | headers?: AsyncApiOperationHeaders; 8 | } 9 | 10 | export type AsyncApiMessage = OneAsyncApiMessage | OneAsyncApiMessage[]; 11 | -------------------------------------------------------------------------------- /sample/felines/dto/create-feline.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty, getSchemaPath } from '@nestjs/swagger'; 2 | import { Cat, Feline, Lion, Message, Tiger } from '../class'; 3 | 4 | export class CreateFelineDto extends Message { 5 | @ApiProperty({ 6 | oneOf: [ 7 | { $ref: getSchemaPath(Cat) }, 8 | { $ref: getSchemaPath(Lion) }, 9 | { $ref: getSchemaPath(Tiger) }, 10 | ], 11 | }) 12 | payload: Feline; 13 | } 14 | -------------------------------------------------------------------------------- /.asyncapi-tool: -------------------------------------------------------------------------------- 1 | { 2 | "title": "nestjs-asyncapi", 3 | "description": "Utilize decorators to generate AsyncAPI document utilizing DTOs (similar to @nestjs/swagger) and a web UI.", 4 | "links": { 5 | "repoUrl": "https://github.com/flamewow/nestjs-asyncapi" 6 | }, 7 | "filters": { 8 | "language": "Typescript", 9 | "technology": ["Node.js", "NestJS"], 10 | "categories": ["code-first"], 11 | "hasCommercial": false 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | 3 | logs 4 | *.log 5 | npm-debug.log* 6 | yarn-debug.log* 7 | yarn-error.log* 8 | lerna-debug.log* 9 | 10 | 11 | # IDEs and editors 12 | /.idea 13 | .project 14 | .classpath 15 | .c9/ 16 | *.launch 17 | .settings/ 18 | *.sublime-workspace 19 | 20 | # IDE - VSCode 21 | .vscode/* 22 | !.vscode/settings.json 23 | !.vscode/tasks.json 24 | !.vscode/launch.json 25 | !.vscode/extensions.json 26 | 27 | .env 28 | .local_dev_data 29 | -------------------------------------------------------------------------------- /lib/decorators/asyncapi-operation.decorator.ts: -------------------------------------------------------------------------------- 1 | import { DECORATORS } from '../asyncapi.constants'; 2 | import { AsyncApiOperationOptions } from '../interface'; 3 | import { AsyncApiOperationForMetaKey } from './asyncapi-operation-for-meta-key.decorator'; 4 | 5 | export function AsyncApiOperation( 6 | ...options: AsyncApiOperationOptions[] 7 | ): MethodDecorator { 8 | return AsyncApiOperationForMetaKey(DECORATORS.AsyncApiOperation, options); 9 | } 10 | -------------------------------------------------------------------------------- /lib/interface/asyncapi-operation-options.interface.ts: -------------------------------------------------------------------------------- 1 | import { AsyncOperationObject } from './asyncapi-common.interfaces'; 2 | import { AsyncApiMessage } from './asyncapi-message.interface'; 3 | 4 | export interface AsyncApiSpecificOperationOptions 5 | extends Omit { 6 | message: AsyncApiMessage; 7 | } 8 | 9 | export interface AsyncApiOperationOptions 10 | extends AsyncApiSpecificOperationOptions { 11 | type: 'pub' | 'sub'; 12 | } 13 | -------------------------------------------------------------------------------- /sample/felines/class/message.class.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | 3 | export abstract class Message> { 4 | @ApiProperty({ format: 'uuid' }) 5 | correlationId: string; 6 | 7 | @ApiProperty({ format: 'x.y.z', example: '1.0.1' }) 8 | version: string; 9 | 10 | timestamp: Date; 11 | 12 | abstract payload: T; 13 | 14 | constructor(partialData: Partial>) { 15 | Object.assign(this, partialData); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /sample/felines/rto/feline.rto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty, getSchemaPath } from '@nestjs/swagger'; 2 | import { Cat, Feline, Lion, Message, Tiger } from '../class'; 3 | 4 | export class FelineRto extends Message { 5 | @ApiProperty({ 6 | oneOf: [ 7 | { $ref: getSchemaPath(Cat) }, 8 | { $ref: getSchemaPath(Lion) }, 9 | { $ref: getSchemaPath(Tiger) }, 10 | ], 11 | }) 12 | payload: Feline; 13 | } 14 | 15 | export class FelineExtendedRto extends FelineRto { 16 | extra: string; 17 | } 18 | -------------------------------------------------------------------------------- /test/configs/jest-e2e.config.ts: -------------------------------------------------------------------------------- 1 | import { JestConfigWithTsJest } from 'ts-jest'; 2 | import { baseConfig } from './jest-base.config'; 3 | 4 | export const e2eConfig: JestConfigWithTsJest = { 5 | ...baseConfig, 6 | testRegex: '.\\.e2e-spec\\.ts$', 7 | transform: { 8 | '^.+\\.(t|j)s$': [ 9 | 'ts-jest', 10 | { 11 | astTransformers: { 12 | before: ['test/configs/jest-swagger-plugin.js'], 13 | }, 14 | }, 15 | ], 16 | }, 17 | }; 18 | 19 | export default e2eConfig; 20 | -------------------------------------------------------------------------------- /.github/workflows/code-quality.yml: -------------------------------------------------------------------------------- 1 | name: code-quality 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | lint: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | - name: use node@20 12 | uses: actions/setup-node@v4 13 | with: 14 | node-version: '20' 15 | cache: 'npm' 16 | - name: 'install dependencies' 17 | run: npm ci 18 | env: 19 | PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: true 20 | - run: npm run lint 21 | -------------------------------------------------------------------------------- /sample/felines/class/feline.class.ts: -------------------------------------------------------------------------------- 1 | enum PawsEnum { 2 | left = 'left', 3 | right = 'right', 4 | } 5 | 6 | enum GendersEnum { 7 | male = 'male', 8 | female = 'female', 9 | } 10 | 11 | export abstract class Feline { 12 | id: number; 13 | 14 | name: string; 15 | 16 | age: number; 17 | 18 | gender: GendersEnum; 19 | 20 | dominantPaw: PawsEnum; 21 | 22 | tags: string[]; 23 | 24 | birthDatetime: Date; 25 | 26 | constructor(initializer: Record) { 27 | Object.assign(this, initializer); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # OS 14 | .DS_Store 15 | 16 | # Tests 17 | /coverage 18 | /.nyc_output 19 | 20 | # IDEs and editors 21 | /.idea 22 | .project 23 | .classpath 24 | .c9/ 25 | *.launch 26 | .settings/ 27 | *.sublime-workspace 28 | 29 | # IDE - VSCode 30 | .vscode/* 31 | !.vscode/settings.json 32 | !.vscode/tasks.json 33 | !.vscode/launch.json 34 | !.vscode/extensions.json 35 | 36 | .env 37 | .local_dev_data 38 | .eslintcache -------------------------------------------------------------------------------- /lib/interface/asyncapi-template-options.interface.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @see https://github.com/asyncapi/html-template#supported-parameters 3 | **/ 4 | export interface AsyncApiTemplateOptions { 5 | /** @default byTagsNoRoot **/ 6 | sidebarOrganization?: 'byTags' | 'byTagsNoRoot'; 7 | /** 8 | * @example /docs 9 | **/ 10 | baseHref?: string; 11 | /** @default true **/ 12 | singleFile?: boolean; 13 | /** @example asyncapi.html **/ 14 | outFilename?: string; 15 | /** 16 | * @description Generates output HTML as PDF 17 | * @default false 18 | */ 19 | pdf?: boolean; 20 | } 21 | -------------------------------------------------------------------------------- /test/configs/jest-base.config.ts: -------------------------------------------------------------------------------- 1 | import { JestConfigWithTsJest } from 'ts-jest'; 2 | 3 | export const baseConfig: JestConfigWithTsJest = { 4 | moduleFileExtensions: ['js', 'json', 'ts'], 5 | rootDir: '../../', 6 | transform: { 7 | '^.+\\.(t|j)s$': 'ts-jest', 8 | }, 9 | collectCoverageFrom: ['**/*.(t|j)s'], 10 | coverageDirectory: './coverage', 11 | testEnvironment: 'node', 12 | moduleNameMapper: { 13 | '#lib(|/.*)$': '/lib/$1', 14 | '#sample(|/.*)$': '/sample/$1', 15 | '#test(|/.*)$': '/test/$1', 16 | }, 17 | }; 18 | 19 | export default baseConfig; 20 | -------------------------------------------------------------------------------- /lib/interface/asyncapi-server.interface.ts: -------------------------------------------------------------------------------- 1 | import { ServerObject } from '@nestjs/swagger/dist/interfaces/open-api-spec.interface'; 2 | import { AmqpServerBinding, KafkaServerBinding } from '../binding'; 3 | import { 4 | AsyncServerVariableObject, 5 | SecurityObject, 6 | } from './asyncapi-common.interfaces'; 7 | 8 | export interface AsyncServerObject extends Omit { 9 | variables?: Record; 10 | protocol: string; 11 | protocolVersion?: string; 12 | security?: SecurityObject[]; 13 | bindings?: Record; 14 | } 15 | -------------------------------------------------------------------------------- /sample/felines/felines.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ClientsModule, Transport } from '@nestjs/microservices'; 3 | import { FELINES_MS } from './felines.constants'; 4 | import { FelinesController } from './felines.controller'; 5 | import { FelinesGateway } from './felines.gateway'; 6 | import { FelinesService } from './felines.service'; 7 | 8 | @Module({ 9 | imports: [ 10 | ClientsModule.register([{ name: FELINES_MS, transport: Transport.TCP }]), 11 | ], 12 | providers: [FelinesService, FelinesGateway], 13 | controllers: [FelinesController], 14 | }) 15 | export class FelinesModule {} 16 | -------------------------------------------------------------------------------- /lib/interface/index.ts: -------------------------------------------------------------------------------- 1 | export * from './asyncapi-common.interfaces'; 2 | export * from './asyncapi-template-options.interface'; 3 | export * from './asyncapi-operation-headers.interface'; 4 | export * from './asyncapi-operation-payload.interface'; 5 | export * from './asyncapi-operation-options.interface'; 6 | export * from './asyncapi-document-options.interface'; 7 | export * from './asyncapi-server.interface'; 8 | export * from './denormalized-doc.interface'; 9 | export * from './denormalized-doc-resolvers.interface'; 10 | export * from './generator-options.interface'; 11 | 12 | export * from './asyncapi-operation-options-raw.interface'; 13 | -------------------------------------------------------------------------------- /lib/decorators/asyncapi-pub.decorator.ts: -------------------------------------------------------------------------------- 1 | import { DECORATORS } from '../asyncapi.constants'; 2 | import { 3 | AsyncApiOperationOptions, 4 | AsyncApiSpecificOperationOptions, 5 | } from '../interface'; 6 | import { AsyncApiOperationForMetaKey } from './asyncapi-operation-for-meta-key.decorator'; 7 | 8 | export function AsyncApiPub( 9 | ...specificOperationOptions: AsyncApiSpecificOperationOptions[] 10 | ) { 11 | const options: AsyncApiOperationOptions[] = specificOperationOptions.map( 12 | (i) => ({ 13 | ...i, 14 | type: 'pub', 15 | }), 16 | ); 17 | return AsyncApiOperationForMetaKey(DECORATORS.AsyncApiPub, options); 18 | } 19 | -------------------------------------------------------------------------------- /lib/decorators/asyncapi-sub.decorator.ts: -------------------------------------------------------------------------------- 1 | import { DECORATORS } from '../asyncapi.constants'; 2 | import { 3 | AsyncApiOperationOptions, 4 | AsyncApiSpecificOperationOptions, 5 | } from '../interface'; 6 | import { AsyncApiOperationForMetaKey } from './asyncapi-operation-for-meta-key.decorator'; 7 | 8 | export function AsyncApiSub( 9 | ...specificOperationOptions: AsyncApiSpecificOperationOptions[] 10 | ) { 11 | const options: AsyncApiOperationOptions[] = specificOperationOptions.map( 12 | (i) => ({ 13 | ...i, 14 | type: 'sub', 15 | }), 16 | ); 17 | return AsyncApiOperationForMetaKey(DECORATORS.AsyncApiSub, options); 18 | } 19 | -------------------------------------------------------------------------------- /test/configs/jest-swagger-plugin.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-var-requires 2 | const transformer = require('@nestjs/swagger/plugin'); 3 | 4 | module.exports.name = 'nestjs-swagger-transformer'; 5 | // you should change the version number anytime you change the configuration below - otherwise, jest will not detect changes 6 | module.exports.version = 1; 7 | 8 | module.exports.factory = (cs) => { 9 | return transformer.before( 10 | { 11 | introspectComments: true, 12 | dtoFileNameSuffix: ['.dto.ts', '.class.ts'], 13 | }, 14 | cs.program, // "cs.tsCompiler.program" for older versions of Jest (<= v27) 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /lib/interface/asyncapi-operation-options-raw.interface.ts: -------------------------------------------------------------------------------- 1 | import { AsyncOperationObject } from './asyncapi-common.interfaces'; 2 | import { AsyncOperationPayload } from './asyncapi-operation-payload.interface'; 3 | 4 | export interface RawAsyncApiMessage { 5 | name?: string; 6 | payload: { 7 | type: AsyncOperationPayload; 8 | }; 9 | headers?: { 10 | type: 'object'; 11 | properties: { 12 | [key: string]: { 13 | description: string; 14 | type: 'string'; 15 | [key: string]: unknown; 16 | }; 17 | }; 18 | }; 19 | } 20 | 21 | export interface AsyncApiOperationOptionsRaw extends AsyncOperationObject { 22 | type: 'sub' | 'pub'; 23 | } 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /sample/felines/felines.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { Feline } from './class'; 3 | import { CreateFelineDto } from './dto'; 4 | 5 | @Injectable() 6 | export class FelinesService { 7 | private readonly felines: Feline[] = []; 8 | 9 | async get(id: number): Promise { 10 | return this.felines[id]; 11 | } 12 | 13 | async delete(id: number): Promise { 14 | const felineToDelete = this.get(id); 15 | delete this.felines[id]; 16 | return !!felineToDelete; 17 | } 18 | 19 | async create(createFelineDto: CreateFelineDto): Promise { 20 | const feline = createFelineDto.payload; 21 | this.felines.push(feline); 22 | return feline; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "esModuleInterop": true, 10 | "sourceMap": true, 11 | "allowJs": true, 12 | "resolveJsonModule": true, 13 | "incremental": true, 14 | "noImplicitAny": false, 15 | "noLib": false, 16 | "target": "es6", 17 | "outDir": "./dist", 18 | "skipLibCheck": true, 19 | "baseUrl": ".", 20 | "paths": { 21 | "#lib": [ 22 | "lib" 23 | ], 24 | "#sample/*": [ 25 | "sample/*" 26 | ], 27 | "#test/*": [ 28 | "test/*" 29 | ] 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/binding/asyncapi.kafka.interfaces.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Kafka binding 3 | * @see https://github.com/asyncapi/bindings/tree/master/kafka 4 | */ 5 | import { SchemaObject } from '@nestjs/swagger/dist/interfaces/open-api-spec.interface'; 6 | 7 | export interface KafkaServerBinding { 8 | schemaRegistryUrl: string; 9 | schemaRegistryVendor: string; 10 | /** 11 | * x.y.z 12 | */ 13 | bindingVersion: string; 14 | } 15 | 16 | export interface KafkaChannelBinding { 17 | topic: string; 18 | partitions: number; 19 | replicas: number; 20 | /** 21 | * x.y.z 22 | */ 23 | bindingVersion: string; 24 | } 25 | 26 | export interface KafkaOperationBinding { 27 | groupId?: SchemaObject; 28 | clientId?: SchemaObject; 29 | bindingVersion?: string; 30 | } 31 | 32 | export interface KafkaMessageBinding { 33 | key?: SchemaObject; 34 | bindingVersion?: string; 35 | } 36 | -------------------------------------------------------------------------------- /lib/interface/generator-options.interface.ts: -------------------------------------------------------------------------------- 1 | import { AsyncApiDocument } from './asyncapi-common.interfaces'; 2 | import { AsyncApiTemplateOptions } from './asyncapi-template-options.interface'; 3 | 4 | export interface GeneratorOptions { 5 | templateName: string; 6 | targetDir: string; 7 | entrypoint?: string; 8 | noOverwriteGlobs: string[]; 9 | disabledHooks: { [key: string]: string | boolean | string[] }; 10 | output: 'string' | 'fs'; 11 | forceWrite: boolean; 12 | debug: boolean; 13 | install: boolean; 14 | templateConfig: Record; 15 | hooks: Record; 16 | templateParams: AsyncApiTemplateOptions; 17 | generate: (document: AsyncApiDocument) => Promise; 18 | generateFromURL: (url: string) => Promise; 19 | generateFromFile: (path: string) => Promise; 20 | generateFromString: (yaml: string, args?: unknown) => Promise; 21 | } 22 | -------------------------------------------------------------------------------- /.github/workflows/first-interaction.yaml: -------------------------------------------------------------------------------- 1 | name: first-interaction 2 | 3 | on: 4 | issues: 5 | types: [opened] 6 | pull_request: 7 | branches: [main] 8 | types: [opened] 9 | 10 | jobs: 11 | check_for_first_interaction: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: actions/first-interaction@main 16 | with: 17 | repo-token: ${{ secrets.GITHUB_TOKEN }} 18 | issue-message: | 19 | Hello! Thank you for filing an issue. 20 | 21 | If this is a bug report, please include relevant logs to help us debug the problem. 22 | pr-message: | 23 | Hello! Thank you for your contribution. 24 | 25 | If you are fixing a bug, please reference the issue number in the description. 26 | 27 | If you are implementing a feature request, please check with the maintainers that the feature will be accepted first. -------------------------------------------------------------------------------- /lib/services/asyncapi.generator.ts: -------------------------------------------------------------------------------- 1 | import Generator from '@asyncapi/generator'; 2 | import jsyaml from 'js-yaml'; 3 | import os from 'os'; 4 | import { 5 | AsyncApiDocument, 6 | AsyncApiTemplateOptions, 7 | GeneratorOptions, 8 | } from '../interface'; 9 | 10 | export class AsyncapiGenerator { 11 | private readonly generator: GeneratorOptions; 12 | 13 | constructor(readonly templateOptions?: AsyncApiTemplateOptions) { 14 | this.generator = new Generator('@asyncapi/html-template', os.tmpdir(), { 15 | forceWrite: true, 16 | entrypoint: 'index.html', 17 | output: 'string', 18 | templateParams: { 19 | singleFile: true, 20 | ...templateOptions, 21 | }, 22 | }); 23 | } 24 | 25 | public async generate(contract: AsyncApiDocument): Promise { 26 | const yaml = jsyaml.dump(contract); 27 | return this.generator.generateFromString(yaml, { 28 | resolve: { 29 | file: false, 30 | }, 31 | }); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /.github/workflows/base.yml: -------------------------------------------------------------------------------- 1 | name: base 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | workflow_dispatch: 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: use node@20 16 | uses: actions/setup-node@v4 17 | with: 18 | node-version: '20' 19 | cache: 'npm' 20 | - name: 'install dependencies' 21 | run: npm ci 22 | env: 23 | PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: true 24 | - run: npm run build 25 | 26 | test: 27 | runs-on: ubuntu-latest 28 | steps: 29 | - uses: actions/checkout@v4 30 | - name: use node@20 31 | uses: actions/setup-node@v4 32 | with: 33 | node-version: '20' 34 | cache: 'npm' 35 | - name: 'install dependencies' 36 | run: npm ci 37 | env: 38 | PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: true 39 | - run: npm run test:e2e 40 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | 27 | **Environment** 28 | 29 | Nest version: X.Y.Z 30 | 31 | Check whether this is still an issue in the most recent Nest version 32 | 33 | For Tooling issues: 34 | - Node version: XX 35 | - Platform: 36 | 37 | Others: 38 | - Anything else relevant? Operating system version, IDE, package manager, ... 39 | 40 | **Additional context** 41 | Add any other context about the problem here. 42 | -------------------------------------------------------------------------------- /lib/services/asyncapi.transformer.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AsyncChannelObject, 3 | AsyncChannelsObject, 4 | DenormalizedDoc, 5 | } from '../interface'; 6 | 7 | export class AsyncapiTransformer { 8 | public normalizeChannels( 9 | denormalizedDocs: DenormalizedDoc[], 10 | ): Record<'channels', AsyncChannelsObject> { 11 | const flatChannels = denormalizedDocs.map((doc: DenormalizedDoc) => { 12 | const key = doc.root.name; 13 | const value: AsyncChannelObject = { 14 | description: doc.root.description, 15 | bindings: doc.root.bindings, 16 | parameters: doc.root.parameters, 17 | subscribe: doc.operations.sub, 18 | publish: doc.operations.pub, 19 | }; 20 | return { key, value }; 21 | }); 22 | 23 | const channels = flatChannels.reduce((acc, { key, value }) => { 24 | if (!acc[key]) { 25 | acc[key] = value; 26 | } 27 | 28 | acc[key].publish = acc[key].publish ?? value.publish; 29 | acc[key].subscribe = acc[key].subscribe ?? value.subscribe; 30 | 31 | return acc; 32 | }, {}); 33 | 34 | return { channels }; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Ilya Moroz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/explorers/asyncapi-class.explorer.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@nestjs/common'; 2 | import { CONTROLLER_WATERMARK } from '@nestjs/common/constants'; 3 | import { DECORATORS } from '../asyncapi.constants'; 4 | 5 | let GATEWAY_METADATA; 6 | try { 7 | // eslint-disable-next-line @typescript-eslint/no-var-requires 8 | const wsConstants = require('@nestjs/websockets/constants'); 9 | GATEWAY_METADATA = wsConstants.GATEWAY_METADATA; 10 | } catch { 11 | GATEWAY_METADATA = '__gateway__'; // in case @nestjs/websockets is not installed GATEWAY_METADATA value is irrelevant 12 | } 13 | 14 | export const asyncApiClassAnnotationLabels = [ 15 | DECORATORS.AsyncApiClass, 16 | CONTROLLER_WATERMARK, 17 | GATEWAY_METADATA, 18 | ]; 19 | 20 | export const exploreAsyncapiClassMetadata = (metatype: Type) => { 21 | return Reflect.getMetadata(DECORATORS.AsyncApiClass, metatype); 22 | }; 23 | 24 | export const exploreControllerMetadata = (metatype: Type) => { 25 | return Reflect.getMetadata(CONTROLLER_WATERMARK, metatype); 26 | }; 27 | 28 | export const exploreGatewayMetadata = (metatype: Type) => { 29 | return Reflect.getMetadata(GATEWAY_METADATA, metatype); 30 | }; 31 | -------------------------------------------------------------------------------- /lib/binding/asyncapi.amqp.interfaces.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * AMPQ binding 3 | * @see https://github.com/asyncapi/bindings/tree/master/amqp 4 | */ 5 | 6 | /** 7 | * This object MUST NOT contain any properties. Its name is reserved for future use. 8 | */ 9 | export interface AmqpServerBinding {} 10 | 11 | export interface AmqpChannelBinding { 12 | is: string; 13 | exchange?: { 14 | name: string; 15 | type: string; 16 | durable?: boolean; 17 | autoDelete?: boolean; 18 | vhost?: string; 19 | }; 20 | queue?: { 21 | name: string; 22 | durable?: boolean; 23 | exclusive?: boolean; 24 | autoDelete?: boolean; 25 | vhost?: string; 26 | }; 27 | bindingVersion?: string; 28 | } 29 | 30 | export interface AmqpOperationBinding { 31 | expiration?: number; 32 | userId?: string; 33 | cc?: string[]; 34 | priority?: number; 35 | deliveryMode?: number; 36 | mandatory?: boolean; 37 | bcc?: string[]; 38 | replyTo?: string; 39 | timestamp?: boolean; 40 | ack?: boolean; 41 | bindingVersion?: string; 42 | } 43 | 44 | export interface AmqpMessageBinding { 45 | contentEncoding?: string; 46 | messageType?: string; 47 | bindingVersion?: string; 48 | } 49 | -------------------------------------------------------------------------------- /.github/lock.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before a closed issue or pull request is locked 2 | daysUntilLock: 90 3 | 4 | # Skip issues and pull requests created before a given timestamp. Timestamp must 5 | # follow ISO 8601 (`YYYY-MM-DD`). Set to `false` to disable 6 | skipCreatedBefore: false 7 | 8 | # Issues and pull requests with these labels will be ignored. Set to `[]` to disable 9 | exemptLabels: [] 10 | 11 | # Label to add before locking, such as `outdated`. Set to `false` to disable 12 | lockLabel: false 13 | 14 | # Comment to post before locking. Set to `false` to disable 15 | lockComment: > 16 | This thread has been automatically locked since there has not been 17 | any recent activity after it was closed. Please open a new issue for 18 | related bugs. 19 | 20 | # Assign `resolved` as the reason for locking. Set to `false` to disable 21 | setLockReason: true 22 | 23 | # Limit to only `issues` or `pulls` 24 | # only: issues 25 | 26 | # Optionally, specify configuration settings just for `issues` or `pulls` 27 | # issues: 28 | # exemptLabels: 29 | # - help-wanted 30 | # lockLabel: outdated 31 | 32 | # pulls: 33 | # daysUntilLock: 30 34 | 35 | # Repository to extend settings from 36 | # _extends: repo 37 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## PR Checklist 2 | Please check if your PR fulfills the following requirements: 3 | 4 | - [ ] The commit message follows our guidelines: https://github.com/nestjs/nest/blob/master/CONTRIBUTING.md 5 | - [ ] Tests for the changes have been added (for bug fixes / features) 6 | - [ ] Docs have been added / updated (for bug fixes / features) 7 | 8 | 9 | ## PR Type 10 | What kind of change does this PR introduce? 11 | 12 | 13 | ``` 14 | [ ] Bugfix 15 | [ ] Feature 16 | [ ] Code style update (formatting, local variables) 17 | [ ] Refactoring (no functional changes, no api changes) 18 | [ ] Build related changes 19 | [ ] CI related changes 20 | [ ] Other... Please describe: 21 | ``` 22 | 23 | ## What is the current behavior? 24 | 25 | 26 | Issue Number: N/A 27 | 28 | 29 | ## What is the new behavior? 30 | 31 | 32 | ## Does this PR introduce a breaking change? 33 | ``` 34 | [ ] Yes 35 | [ ] No 36 | ``` 37 | 38 | 39 | 40 | 41 | ## Other information -------------------------------------------------------------------------------- /sample/main.ts: -------------------------------------------------------------------------------- 1 | import { INestApplication, Logger } from '@nestjs/common'; 2 | import { NestFactory } from '@nestjs/core'; 3 | import { MicroserviceOptions, Transport } from '@nestjs/microservices'; 4 | import { ExpressAdapter } from '@nestjs/platform-express'; 5 | import { FastifyAdapter } from '@nestjs/platform-fastify'; 6 | import { AppModule } from './app.module'; 7 | import { makeAsyncapiDocument } from './common'; 8 | import { BOOTSTRAP, DOC_RELATIVE_PATH, HOST, PORT } from './constants'; 9 | import { AsyncApiModule } from '#lib'; 10 | 11 | const USE_FASTIFY = false; 12 | 13 | const adapter = USE_FASTIFY 14 | ? new FastifyAdapter({ 15 | ignoreTrailingSlash: false, 16 | }) 17 | : new ExpressAdapter(); 18 | 19 | async function bootstrap(): Promise { 20 | const app = await NestFactory.create( 21 | AppModule, 22 | adapter, 23 | {}, 24 | ); 25 | const asyncapiDocument = await makeAsyncapiDocument(app); 26 | await AsyncApiModule.setup(DOC_RELATIVE_PATH, app, asyncapiDocument); 27 | 28 | app.connectMicroservice({ transport: Transport.TCP }); 29 | 30 | await app.startAllMicroservices(); 31 | await app.listen(PORT, HOST); 32 | 33 | const baseUrl = `http://${HOST}:${PORT}`; 34 | const docUrl = baseUrl + DOC_RELATIVE_PATH; 35 | Logger.log(`Server started at ${baseUrl}; AsyncApi at ${docUrl};`, BOOTSTRAP); 36 | } 37 | 38 | bootstrap(); 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | ## I'm submitting a... 8 | 11 |

12 | [ ] Regression 
13 | [ ] Bug report
14 | [ ] Feature request
15 | [ ] Documentation issue or request
16 | [ ] Support request => Please do not submit support request here, instead post your question on Stack Overflow.
17 | 
18 | 19 | ## Current behavior 20 | 21 | 22 | 23 | ## Expected behavior 24 | 25 | 26 | 27 | ## Minimal reproduction of the problem with instructions 28 | 29 | 30 | ## What is the motivation / use case for changing the behavior? 31 | 32 | 33 | 34 | ## Environment 35 | 36 |

37 | Nest version: X.Y.Z
38 | 
39 |  
40 | For Tooling issues:
41 | - Node version: XX  
42 | - Platform:  
43 | 
44 | Others:
45 | 
46 | 
47 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "parserOptions": { 4 | "project": "tsconfig.json", 5 | "sourceType": "module" 6 | }, 7 | "plugins": [ 8 | "@typescript-eslint/eslint-plugin", 9 | "import" 10 | ], 11 | "extends": [ 12 | "plugin:@typescript-eslint/eslint-recommended", 13 | "plugin:@typescript-eslint/recommended", 14 | "prettier" 15 | ], 16 | "root": true, 17 | "env": { 18 | "node": true, 19 | "jest": true 20 | }, 21 | "ignorePatterns": [ 22 | ".eslintrc.js" 23 | ], 24 | "rules": { 25 | "@typescript-eslint/interface-name-prefix": "off", 26 | "@typescript-eslint/explicit-function-return-type": "off", 27 | "@typescript-eslint/explicit-module-boundary-types": "off", 28 | "@typescript-eslint/no-explicit-any": "warn", 29 | "@typescript-eslint/no-use-before-define": "warn", 30 | "@typescript-eslint/no-unused-vars": [ 31 | "error", { 32 | "args": "none" 33 | }], 34 | "@typescript-eslint/ban-types": "off", 35 | "@typescript-eslint/no-empty-interface": "off", 36 | "max-len": [ 37 | 1, 38 | { 39 | "code": 200, 40 | "tabWidth": 2, 41 | "ignoreUrls": true 42 | } 43 | ], 44 | "sort-imports": ["warn", { "ignoreDeclarationSort": true, "ignoreCase": true }], 45 | "import/order": [ 46 | "warn", 47 | { 48 | "alphabetize": { "order": "asc", "caseInsensitive": true }, 49 | "groups": [["builtin", "external"], "parent", "sibling", "index"], 50 | "newlines-between": "never" 51 | } 52 | ], 53 | "no-return-await": "error" 54 | } 55 | } -------------------------------------------------------------------------------- /sample/common.ts: -------------------------------------------------------------------------------- 1 | import { INestApplicationContext } from '@nestjs/common'; 2 | import { HOST, PORT, SERVER } from './constants'; 3 | import { Cat, Feline, Lion, Tiger } from './felines/class'; 4 | import { FelinesModule } from './felines/felines.module'; 5 | import { 6 | AsyncApiDocument, 7 | AsyncApiDocumentBuilder, 8 | AsyncApiModule, 9 | AsyncServerObject, 10 | } from '#lib'; 11 | 12 | export async function makeAsyncapiDocument( 13 | app: INestApplicationContext, 14 | ): Promise { 15 | const asyncApiServer: AsyncServerObject = { 16 | url: `ws://${HOST}:${PORT}`, 17 | protocol: 'socket.io', 18 | protocolVersion: '4', 19 | description: 20 | 'Allows you to connect using the websocket protocol to our Socket.io server.', 21 | security: [{ 'user-password': [] }], 22 | variables: { 23 | port: { 24 | description: 'Secure connection (TLS) is available through port 443.', 25 | default: '443', 26 | }, 27 | }, 28 | bindings: {}, 29 | }; 30 | 31 | const servers = Object.keys(SERVER).map((i) => ({ 32 | name: SERVER[i], 33 | server: asyncApiServer, 34 | })); 35 | 36 | const asyncApiOptions = new AsyncApiDocumentBuilder() 37 | .setTitle('Feline') 38 | .setDescription('Feline server description here') 39 | .setVersion('1.0') 40 | .setDefaultContentType('application/json') 41 | .addSecurity('user-password', { type: 'userPassword' }) 42 | .addServers(servers) 43 | .build(); 44 | 45 | return AsyncApiModule.createDocument(app, asyncApiOptions, { 46 | include: [FelinesModule], 47 | extraModels: [Cat, Lion, Tiger, Feline], 48 | }); 49 | } 50 | -------------------------------------------------------------------------------- /lib/explorers/asyncapi-operation.explorer.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@nestjs/common'; 2 | import { SchemaObject } from '@nestjs/swagger/dist/interfaces/open-api-spec.interface'; 3 | import { DECORATORS } from '../asyncapi.constants'; 4 | import { AsyncApiOperationOptionsRaw } from '../interface'; 5 | import { OperationObjectFactory } from '../services'; 6 | 7 | const operationObjectFactory = new OperationObjectFactory(); 8 | 9 | export const exploreAsyncApiOperationMetadata = ( 10 | schemas: Record, 11 | _instance: object, 12 | _prototype: Type, 13 | method: object, 14 | ) => { 15 | const metadataOperations: AsyncApiOperationOptionsRaw[] = Reflect.getMetadata( 16 | DECORATORS.AsyncApiOperation, 17 | method, 18 | ); 19 | const metadataSubs: AsyncApiOperationOptionsRaw[] = Reflect.getMetadata( 20 | DECORATORS.AsyncApiSub, 21 | method, 22 | ); 23 | const metadataPubs: AsyncApiOperationOptionsRaw[] = Reflect.getMetadata( 24 | DECORATORS.AsyncApiPub, 25 | method, 26 | ); 27 | 28 | const metadataCombined = [ 29 | ...(metadataOperations ? Object.values(metadataOperations) : []), 30 | ...(metadataSubs ? Object.values(metadataSubs) : []), 31 | ...(metadataPubs ? Object.values(metadataPubs) : []), 32 | ]; 33 | 34 | return metadataCombined.map((option: AsyncApiOperationOptionsRaw) => { 35 | const { channel, type } = option; 36 | 37 | const methodTypeData = { 38 | ...option, 39 | ...operationObjectFactory.create(option, ['application/json'], schemas), 40 | channel: undefined, 41 | type: undefined, 42 | }; 43 | 44 | return { 45 | channel, 46 | [type]: methodTypeData, 47 | }; 48 | }); 49 | }; 50 | -------------------------------------------------------------------------------- /lib/decorators/asyncapi-operation-for-meta-key.decorator.ts: -------------------------------------------------------------------------------- 1 | import { createMethodDecorator } from '@nestjs/swagger/dist/decorators/helpers'; 2 | import { 3 | AsyncApiOperationHeaders, 4 | AsyncApiOperationOptions, 5 | AsyncMessageObject, 6 | AsyncOperationObject, 7 | } from '../interface'; 8 | import { OneAsyncApiMessage } from '../interface/asyncapi-message.interface'; 9 | 10 | function makeHeaders(headers?: AsyncApiOperationHeaders) { 11 | return headers 12 | ? { 13 | type: 'object', 14 | properties: Object.entries(headers) 15 | .map(([key, value]) => ({ 16 | [key]: { 17 | type: 'string', 18 | ...value, 19 | }, 20 | })) 21 | .reduce((acc, j) => ({ ...acc, ...j }), {}), 22 | } 23 | : undefined; 24 | } 25 | 26 | function makeMessage( 27 | message: OneAsyncApiMessage, 28 | defaultName: string, 29 | ): AsyncMessageObject { 30 | return { 31 | ...message, 32 | name: message.name || defaultName, 33 | payload: { 34 | type: message.payload, 35 | }, 36 | headers: makeHeaders(message.headers), 37 | }; 38 | } 39 | 40 | export function AsyncApiOperationForMetaKey( 41 | metaKey: string, 42 | options: AsyncApiOperationOptions[], 43 | ): MethodDecorator { 44 | return (target, propertyKey: string | symbol, descriptor) => { 45 | const methodName = `${target.constructor.name}#${String(propertyKey)}`; 46 | 47 | const transformedOptions: AsyncOperationObject[] = options.map((i) => { 48 | const message = Array.isArray(i.message) 49 | ? { 50 | oneOf: i.message.map((i, index) => 51 | makeMessage(i, `${methodName}#${index}`), 52 | ), 53 | } 54 | : makeMessage(i.message, methodName); 55 | 56 | const transformedOptionInstance = { 57 | ...i, 58 | message, 59 | }; 60 | 61 | return transformedOptionInstance; 62 | }); 63 | 64 | return createMethodDecorator(metaKey, transformedOptions)( 65 | target, 66 | propertyKey, 67 | descriptor, 68 | ); 69 | }; 70 | } 71 | -------------------------------------------------------------------------------- /lib/services/operation-object.factory.ts: -------------------------------------------------------------------------------- 1 | import { SchemaObject } from '@nestjs/swagger/dist/interfaces/open-api-spec.interface'; 2 | import { ModelPropertiesAccessor } from '@nestjs/swagger/dist/services/model-properties-accessor'; 3 | import { SchemaObjectFactory } from '@nestjs/swagger/dist/services/schema-object-factory'; 4 | import { SwaggerTypesMapper } from '@nestjs/swagger/dist/services/swagger-types-mapper'; 5 | import { getSchemaPath } from '@nestjs/swagger/dist/utils'; 6 | import { 7 | AsyncApiOperationOptionsRaw, 8 | AsyncMessageObject, 9 | AsyncOperationObject, 10 | OneOfMessageType, 11 | } from '../interface'; 12 | 13 | export class OperationObjectFactory { 14 | private readonly modelPropertiesAccessor = new ModelPropertiesAccessor(); 15 | private readonly swaggerTypesMapper = new SwaggerTypesMapper(); 16 | private readonly schemaObjectFactory = new SchemaObjectFactory( 17 | this.modelPropertiesAccessor, 18 | this.swaggerTypesMapper, 19 | ); 20 | 21 | create( 22 | operation: AsyncApiOperationOptionsRaw, 23 | produces: string[], 24 | schemas: Record, 25 | ): AsyncOperationObject { 26 | const { message } = operation; 27 | const { oneOf } = message as OneOfMessageType; 28 | 29 | if (oneOf) { 30 | return { 31 | ...operation, 32 | message: { 33 | oneOf: oneOf.map((i) => ({ 34 | ...i, 35 | payload: { 36 | $ref: getSchemaPath( 37 | this.getDtoName(i as AsyncMessageObject, schemas), 38 | ), 39 | }, 40 | })), 41 | }, 42 | }; 43 | } 44 | 45 | return { 46 | ...operation, 47 | message: { 48 | ...operation.message, 49 | payload: { 50 | $ref: getSchemaPath( 51 | this.getDtoName(message as AsyncMessageObject, schemas), 52 | ), 53 | }, 54 | }, 55 | }; 56 | } 57 | 58 | private getDtoName( 59 | message: AsyncMessageObject, 60 | schemas: Record, 61 | ): string { 62 | const messagePayloadType = message.payload.type as Function; 63 | return this.schemaObjectFactory.exploreModelSchema( 64 | messagePayloadType, 65 | schemas, 66 | ); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /lib/asyncapi.module.ts: -------------------------------------------------------------------------------- 1 | import { 2 | INestApplication, 3 | INestApplicationContext, 4 | Logger, 5 | } from '@nestjs/common'; 6 | import { validatePath } from '@nestjs/swagger/dist/utils/validate-path.util'; 7 | import jsyaml from 'js-yaml'; 8 | import { 9 | AsyncApiDocument, 10 | AsyncApiDocumentOptions, 11 | AsyncApiTemplateOptions, 12 | } from './interface'; 13 | import { AsyncapiGenerator, AsyncapiScanner } from './services'; 14 | 15 | export class AsyncApiModule { 16 | private static readonly logger = new Logger(AsyncApiModule.name); 17 | 18 | public static createDocument( 19 | app: INestApplicationContext, 20 | config: Omit, 21 | options: AsyncApiDocumentOptions = {}, 22 | ): AsyncApiDocument { 23 | const asyncapiScanner = new AsyncapiScanner(); 24 | const document = asyncapiScanner.scanApplication(app, options); 25 | 26 | document.components = { 27 | ...(config.components || {}), 28 | ...document.components, 29 | }; 30 | 31 | return { 32 | asyncapi: '2.5.0', 33 | ...config, 34 | ...document, 35 | }; 36 | } 37 | 38 | static async composeHtml( 39 | contract: AsyncApiDocument, 40 | templateOptions?: AsyncApiTemplateOptions, 41 | ) { 42 | const generator = new AsyncapiGenerator(templateOptions); 43 | return generator.generate(contract).catch((err) => { 44 | this.logger.error(err); 45 | throw err; 46 | }); 47 | } 48 | 49 | public static async setup( 50 | path: string, 51 | app: INestApplication, 52 | document: AsyncApiDocument, 53 | templateOptions?: AsyncApiTemplateOptions, 54 | ) { 55 | const httpAdapter = app.getHttpAdapter(); 56 | const finalPath = validatePath(path); 57 | 58 | const html = await this.composeHtml(document, templateOptions); 59 | const yamlDocument = jsyaml.dump(document); 60 | const jsonDocument = JSON.stringify(document); 61 | 62 | httpAdapter.get(finalPath, (req, res) => { 63 | res.type('text/html'); 64 | res.send(html); 65 | }); 66 | 67 | httpAdapter.get(finalPath + '-json', (req, res) => { 68 | res.type('application/json'); 69 | res.send(jsonDocument); 70 | }); 71 | 72 | httpAdapter.get(finalPath + '-yaml', (req, res) => { 73 | res.type('text/yaml'); 74 | res.send(yamlDocument); 75 | }); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /sample/felines/felines.gateway.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from '@nestjs/common'; 2 | import { 3 | ConnectedSocket, 4 | MessageBody, 5 | OnGatewayDisconnect, 6 | OnGatewayInit, 7 | SubscribeMessage, 8 | WebSocketGateway, 9 | WebSocketServer, 10 | } from '@nestjs/websockets'; 11 | import { Namespace, Server } from 'socket.io'; 12 | import { Socket } from 'socket.io-client'; 13 | import { FelinesService } from './/felines.service'; 14 | import { CreateFelineDto } from './dto'; 15 | import { FelineExtendedRto, FelineRto } from './rto'; 16 | import { AsyncApiPub, AsyncApiSub } from '#lib'; 17 | 18 | const EventPatternsWS = { 19 | createFeline: 'ws/create/feline', 20 | }; 21 | 22 | /** 23 | * How to use AsyncApi in a websockets 24 | */ 25 | @WebSocketGateway({ transports: ['websocket'], namespace: 'ws' }) 26 | export class FelinesGateway implements OnGatewayInit, OnGatewayDisconnect { 27 | @WebSocketServer() 28 | private readonly server: Server; 29 | private readonly logger: Logger = new Logger(FelinesGateway.name); 30 | 31 | constructor(private readonly felinesService: FelinesService) {} 32 | 33 | afterInit(nsp: Namespace) { 34 | this.logger.log(`Gateway server init: ${nsp?.name}`); 35 | } 36 | 37 | handleDisconnect(client: Socket) { 38 | this.logger.log(`Client disconnected: ${client.id}`); 39 | } 40 | 41 | @SubscribeMessage(EventPatternsWS.createFeline) 42 | @AsyncApiPub({ 43 | channel: EventPatternsWS.createFeline, 44 | message: { 45 | payload: CreateFelineDto, 46 | }, 47 | }) 48 | async createFeline( 49 | @ConnectedSocket() client: Socket, 50 | @MessageBody() createFelineDto: CreateFelineDto, 51 | ) { 52 | this.logger.log( 53 | `data from client ${client.id} : ${JSON.stringify(createFelineDto)}`, 54 | ); 55 | 56 | const feline = await this.felinesService.create(createFelineDto); 57 | await this.emitCreatedFeline(new FelineRto({ payload: feline })); 58 | } 59 | 60 | @AsyncApiSub({ 61 | channel: EventPatternsWS.createFeline, 62 | message: [ 63 | { 64 | name: 'oneOf demo #1', 65 | payload: FelineRto, 66 | }, 67 | { 68 | name: 'oneOf demo #2', 69 | payload: FelineExtendedRto, 70 | }, 71 | ], 72 | }) 73 | async emitCreatedFeline(felineRto: FelineRto) { 74 | this.server.emit(EventPatternsWS.createFeline, felineRto); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /test/express.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { 3 | ExpressAdapter, 4 | NestExpressApplication, 5 | } from '@nestjs/platform-express'; 6 | import * as fs from 'fs/promises'; 7 | import jsyaml from 'js-yaml'; 8 | import request from 'supertest'; 9 | import { AsyncApiModule } from '#lib'; 10 | import { AppModule } from '#sample/app.module'; 11 | import { makeAsyncapiDocument } from '#sample/common'; 12 | import { DOC_RELATIVE_PATH } from '#sample/constants'; 13 | 14 | describe('Express AsyncAPI', () => { 15 | let app: NestExpressApplication; 16 | 17 | beforeAll(async () => { 18 | app = await NestFactory.create( 19 | AppModule, 20 | new ExpressAdapter(), 21 | { logger: false }, 22 | ); 23 | const asyncapiDocument = await makeAsyncapiDocument(app); 24 | await AsyncApiModule.setup(DOC_RELATIVE_PATH, app, asyncapiDocument); 25 | 26 | await app.init(); 27 | }); 28 | 29 | it('should serve doc html', async () => { 30 | const { text } = await request(app.getHttpServer()) 31 | .get(DOC_RELATIVE_PATH) 32 | .expect(200) 33 | .expect('Content-Type', /text\/html/); 34 | const htmlSample = await fs.readFile('./misc/references/ref.html', { 35 | encoding: 'utf8', 36 | }); 37 | expect(text).toEqual(htmlSample); 38 | }); 39 | 40 | it('should serve doc json', async () => { 41 | const { text } = await request(app.getHttpServer()) 42 | .get(`${DOC_RELATIVE_PATH}-json`) 43 | .expect(200) 44 | .expect('Content-Type', /application\/json/); 45 | 46 | const jsonFetched = JSON.parse(text); 47 | const jsonReferenceSample = JSON.parse( 48 | await fs.readFile('./misc/references/ref.json', { 49 | encoding: 'utf8', 50 | }), 51 | ); 52 | 53 | expect(jsonFetched).toEqual(jsonReferenceSample); 54 | }); 55 | 56 | it('should serve doc yaml', async () => { 57 | const { text } = await request(app.getHttpServer()) 58 | .get(`${DOC_RELATIVE_PATH}-yaml`) 59 | .expect(200) 60 | .expect('Content-Type', /text\/yaml/); 61 | 62 | const yamlFetched = jsyaml.load(text); 63 | const yamlReferenceSample = jsyaml.load( 64 | await fs.readFile('./misc/references/ref.yaml', { 65 | encoding: 'utf8', 66 | }), 67 | ); 68 | 69 | expect(yamlFetched).toEqual(yamlReferenceSample); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /test/fastify.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { 3 | FastifyAdapter, 4 | NestFastifyApplication, 5 | } from '@nestjs/platform-fastify'; 6 | import fs from 'fs/promises'; 7 | import jsyaml from 'js-yaml'; 8 | import request from 'supertest'; 9 | import { AsyncApiModule } from '#lib'; 10 | import { AppModule } from '#sample/app.module'; 11 | import { makeAsyncapiDocument } from '#sample/common'; 12 | import { DOC_RELATIVE_PATH } from '#sample/constants'; 13 | 14 | describe('Fastify AsyncAPI', () => { 15 | let app: NestFastifyApplication; 16 | 17 | beforeAll(async () => { 18 | app = await NestFactory.create( 19 | AppModule, 20 | new FastifyAdapter(), 21 | { logger: false }, 22 | ); 23 | const asyncapiDocument = await makeAsyncapiDocument(app); 24 | await AsyncApiModule.setup(DOC_RELATIVE_PATH, app, asyncapiDocument); 25 | 26 | await app.init(); 27 | await app.getHttpAdapter().getInstance().ready(); 28 | }); 29 | 30 | it('should serve doc html', async () => { 31 | const { text } = await request(app.getHttpServer()) 32 | .get(DOC_RELATIVE_PATH) 33 | .expect(200) 34 | .expect('Content-Type', /text\/html/); 35 | const htmlSample = await fs.readFile('./misc/references/ref.html', { 36 | encoding: 'utf8', 37 | }); 38 | expect(text).toEqual(htmlSample); 39 | }); 40 | 41 | it('should serve doc json', async () => { 42 | const { text } = await request(app.getHttpServer()) 43 | .get(`${DOC_RELATIVE_PATH}-json`) 44 | .expect(200) 45 | .expect('Content-Type', /application\/json/); 46 | 47 | const jsonFetched = JSON.parse(text); 48 | const jsonReferenceSample = JSON.parse( 49 | await fs.readFile('./misc/references/ref.json', { 50 | encoding: 'utf8', 51 | }), 52 | ); 53 | 54 | expect(jsonFetched).toEqual(jsonReferenceSample); 55 | }); 56 | 57 | it('should serve doc yaml', async () => { 58 | const { text } = await request(app.getHttpServer()) 59 | .get(`${DOC_RELATIVE_PATH}-yaml`) 60 | .expect(200) 61 | .expect('Content-Type', /text\/yaml/); 62 | 63 | const yamlFetched = jsyaml.load(text); 64 | const yamlReferenceSample = jsyaml.load( 65 | await fs.readFile('./misc/references/ref.yaml', { 66 | encoding: 'utf8', 67 | }), 68 | ); 69 | 70 | expect(yamlFetched).toEqual(yamlReferenceSample); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /sample/felines/felines.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Inject, Logger } from '@nestjs/common'; 2 | import { ClientProxy, EventPattern } from '@nestjs/microservices'; 3 | import { CreateFelineDto } from './dto'; 4 | import { AsyncApiPub, AsyncApiSub } from '#lib'; 5 | import { Cat, Feline } from '#sample/felines/class'; 6 | import { JournalingDataDto } from '#sample/felines/dto/journaling-data.dto'; 7 | import { FELINES_MS } from '#sample/felines/felines.constants'; 8 | import { FelinesService } from '#sample/felines/felines.service'; 9 | import { FelineRto } from '#sample/felines/rto'; 10 | 11 | const EventPatternsMS = { 12 | createFeline: 'ms/create/feline', 13 | journal: 'ms/journal', 14 | }; 15 | 16 | /** 17 | * How to use AsyncApi in a microservices 18 | */ 19 | @Controller() 20 | export class FelinesController { 21 | private logger: Logger = new Logger(FelinesController.name); 22 | 23 | constructor( 24 | @Inject(FELINES_MS) 25 | private readonly client: ClientProxy, 26 | private readonly felinesService: FelinesService, 27 | ) {} 28 | 29 | @AsyncApiSub({ 30 | channel: EventPatternsMS.journal, 31 | message: { 32 | payload: JournalingDataDto, 33 | }, 34 | }) 35 | @EventPattern(EventPatternsMS.journal) 36 | async journal(dataForJournaling: JournalingDataDto) { 37 | const dataForJournalingString = JSON.stringify(dataForJournaling, null, 4); 38 | this.logger.log(`journaling:\n${dataForJournalingString}`); 39 | } 40 | 41 | @AsyncApiPub({ 42 | channel: EventPatternsMS.createFeline, 43 | message: { 44 | payload: CreateFelineDto, 45 | }, 46 | }) 47 | @AsyncApiSub({ 48 | channel: EventPatternsMS.createFeline, 49 | message: { 50 | payload: FelineRto, 51 | }, 52 | }) 53 | @EventPattern(EventPatternsMS.createFeline) 54 | async createFeline(createFelineDto: CreateFelineDto) { 55 | const feline = await this.felinesService.create(createFelineDto); 56 | this.logger.debug(`feline created:\n${JSON.stringify(feline)}`); 57 | this.publishCreatedFeline(feline); 58 | } 59 | 60 | publishCreatedFeline(feline: Feline) { 61 | const felineRto = new FelineRto({ payload: feline }); 62 | return this.client.emit(EventPatternsMS.journal, felineRto); 63 | } 64 | 65 | @Get() 66 | async do() { 67 | const cat = new Cat({ 68 | id: 123, 69 | name: 'demo', 70 | }); 71 | 72 | const felineRto = new CreateFelineDto({ payload: cat }); 73 | 74 | await this.createFeline(felineRto); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | [AsyncApi](https://www.asyncapi.com/) module for [Nest](https://github.com/nestjs/nest). 4 | 5 | Generate [AsyncApi](https://www.asyncapi.com/) documentation (for event-based services, like websockets) in a similar 6 | to [nestjs/swagger](https://github.com/nestjs/swagger) fashion. 7 | 8 | ### [Live Preview](https://flamewow.github.io/nestjs-asyncapi/live-preview) 9 | 10 | [AsyncApi playground](https://playground.asyncapi.io/?load=https://raw.githubusercontent.com/asyncapi/asyncapi/v2.1.0/examples/simple.yml) 11 | 12 | ## Installation 13 | 14 | full installation (with chromium) 15 | 16 | ```bash 17 | $ npm i --save nestjs-asyncapi 18 | ``` 19 | 20 | nestjs-async api package doesn't require chromium (which is required by asyncapi lib), so u can skip chromium 21 | installation by setting PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true environment variable. 22 | 23 | ```bash 24 | $ PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true npm i --save nestjs-asyncapi 25 | ``` 26 | 27 | ## Quick Start 28 | 29 | Include AsyncApi initialization into your bootstrap function. 30 | 31 | ```typescript 32 | async function bootstrap() { 33 | const app = await NestFactory.create(AppModule); 34 | 35 | const asyncApiOptions = new AsyncApiDocumentBuilder() 36 | .setTitle('Feline') 37 | .setDescription('Feline server description here') 38 | .setVersion('1.0') 39 | .setDefaultContentType('application/json') 40 | .addSecurity('user-password', {type: 'userPassword'}) 41 | .addServer('feline-ws', { 42 | url: 'ws://localhost:3000', 43 | protocol: 'socket.io', 44 | }) 45 | .build(); 46 | 47 | const asyncapiDocument = await AsyncApiModule.createDocument(app, asyncApiOptions); 48 | await AsyncApiModule.setup(docRelPath, app, asyncapiDocument); 49 | 50 | // other bootstrap procedures here 51 | 52 | return app.listen(3000); 53 | } 54 | ``` 55 | 56 | AsyncApi module explores `Controllers` & `WebSocketGateway` by default. 57 | In most cases you won't need to add extra annotation, 58 | but if you need to define asyncApi operations in a class that's not a controller or gateway use the `AsyncApi` class 59 | decorator. 60 | 61 | Mark pub/sub methods via `AsyncApiPub` or `AsyncApiSub` decorators
62 | 63 | ```typescript 64 | class CreateFelineDto { 65 | @ApiProperty() 66 | demo: string; 67 | } 68 | 69 | @Controller() 70 | class DemoController { 71 | @AsyncApiPub({ 72 | channel: 'create/feline', 73 | message: { 74 | payload: CreateFelineDto 75 | }, 76 | }) 77 | async createFeline() { 78 | // logic here 79 | } 80 | 81 | @AsyncApiSub({ 82 | channel: 'create/feline', 83 | message: { 84 | payload: CreateFelineDto 85 | }, 86 | }) 87 | async createFeline() { 88 | // logic here 89 | } 90 | } 91 | 92 | ``` 93 | 94 | For more detailed examples please check out https://github.com/flamewow/nestjs-asyncapi/tree/main/sample sample app. 95 | 96 |
Do you use this library and like it? Don't be shy to give it a star 97 | on github
98 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nestjs-asyncapi", 3 | "version": "1.4.0", 4 | "description": "NestJS AsyncAPI module - generate documentation of your event-based services using decorators", 5 | "author": "Ilya Moroz", 6 | "license": "MIT", 7 | "main": "./dist/lib/index.js", 8 | "types": "./dist/lib/index.d.ts", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/flamewow/nestjs-asyncapi" 12 | }, 13 | "validate-branch-name": { 14 | "pattern": "^(main|dev){1}$|^(feat|fix|release|test|refactor|docs|perf|style|chore)/.+$", 15 | "errorMsg": "The branch name isn't valid." 16 | }, 17 | "keywords": [ 18 | "asyncapi", 19 | "nest", 20 | "nestjs", 21 | "swagger", 22 | "openapi", 23 | "documentation", 24 | "socket.io", 25 | "websockets", 26 | "pubsub", 27 | "microservices" 28 | ], 29 | "peerDependencies": { 30 | "@nestjs/common": "^11.0.0 || ^10.0.0 || ^9.0.0", 31 | "@nestjs/core": "^11.0.0 || ^10.0.0 || ^9.0.0", 32 | "@nestjs/swagger": "^11.0.0 || ^8.0.0 || ^7.0.0 || ^6.0.0", 33 | "@nestjs/websockets": "^11.0.0 || ^10.0.0 || ^9.0.0" 34 | }, 35 | "peerDependenciesMeta": { 36 | "@nestjs/websockets": { 37 | "optional": true 38 | } 39 | }, 40 | "dependencies": { 41 | "@asyncapi/generator": "1.13.1", 42 | "@asyncapi/html-template": "0.28.4", 43 | "js-yaml": "4.1.0", 44 | "reflect-metadata": "0.2.1" 45 | }, 46 | "devDependencies": { 47 | "@nestjs/cli": "11.0.5", 48 | "@nestjs/microservices": "11.0.11", 49 | "@nestjs/platform-express": "11.0.11", 50 | "@nestjs/platform-fastify": "11.0.11", 51 | "@nestjs/platform-socket.io": "11.0.11", 52 | "@nestjs/schematics": "11.0.2", 53 | "@nestjs/testing": "11.0.11", 54 | "@nestjs/websockets": "11.0.11", 55 | "@types/express": "4.17.21", 56 | "@types/jest": "29.5.11", 57 | "@types/js-yaml": "4.0.9", 58 | "@types/node": "20.11.14", 59 | "@types/supertest": "2.0.12", 60 | "@types/url-join": "4.0.3", 61 | "@typescript-eslint/eslint-plugin": "6.21.0", 62 | "@typescript-eslint/parser": "6.21.0", 63 | "eslint": "8.57.0", 64 | "eslint-config-prettier": "9.1.0", 65 | "eslint-plugin-import": "2.29.1", 66 | "eslint-plugin-prettier": "5.1.3", 67 | "husky": "9.0.10", 68 | "jest": "29.7.0", 69 | "lint-staged": "15.2.0", 70 | "prettier": "3.2.4", 71 | "release-it": "17.0.3", 72 | "socket.io": "4.7.4", 73 | "socket.io-client": "4.7.4", 74 | "supertest": "6.3.3", 75 | "ts-jest": "29.1.2", 76 | "ts-loader": "9.5.1", 77 | "ts-node": "10.9.2", 78 | "tsconfig-paths": "4.2.0", 79 | "typescript": "5.3.3" 80 | }, 81 | "config": { 82 | "puppeteer_skip_chromium_download": true 83 | }, 84 | "lint-staged": { 85 | "*.ts": [ 86 | "prettier --write", 87 | "eslint --cache --fix" 88 | ] 89 | }, 90 | "scripts": { 91 | "build": "rm -rf dist && tsc -p tsconfig.build.json", 92 | "prepare": "husky install", 93 | "format": "prettier --write \"lib/**/*.{js,ts,json}\" \"e2e/**/*.{js,ts,json}\"", 94 | "lint": "eslint --fix \"lib/**/*.ts\" \"sample/**/*.ts\" \"test/**/*.ts\"", 95 | "pre-commit": "lint-staged", 96 | "prestart": "npm install && npm run build", 97 | "make:snapshots": "bash misc/take-snaphots.sh", 98 | "---execution---": "", 99 | "start": "nest start", 100 | "start:dev": "nest start --watch", 101 | "start:debug": "nest start --watch --debug", 102 | "---tests---": "", 103 | "test:e2e": "jest --runInBand --config test/configs/jest-e2e.config.ts", 104 | "---npm---": "", 105 | "publish:next": "npm publish --access public --tag next", 106 | "publish:beta": "npm publish --access public --tag beta", 107 | "prepublish:npm": "npm run build", 108 | "publish:npm": "npm publish --access public", 109 | "prerelease": "npm run build", 110 | "release": "release-it" 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /lib/services/asyncapi.explorer.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@nestjs/common'; 2 | import { MetadataScanner } from '@nestjs/core'; 3 | import { InstanceWrapper } from '@nestjs/core/injector/instance-wrapper'; 4 | import { SchemaObject } from '@nestjs/swagger/dist/interfaces/open-api-spec.interface'; 5 | import { flatten } from 'lodash'; 6 | import { 7 | asyncApiClassAnnotationLabels, 8 | exploreAsyncapiClassMetadata, 9 | exploreAsyncApiOperationMetadata, 10 | exploreControllerMetadata, 11 | exploreGatewayMetadata, 12 | } from '../explorers'; 13 | import { DenormalizedDoc, DenormalizedDocResolvers } from '../interface'; 14 | 15 | export class AsyncApiExplorer { 16 | private readonly metadataScanner = new MetadataScanner(); 17 | private readonly schemas: SchemaObject[] = []; 18 | private readonly schemaRefsStack: string[] = []; 19 | 20 | private operationIdFactory = (controllerKey: string, methodKey: string) => 21 | controllerKey ? `${controllerKey}_${methodKey}` : methodKey; 22 | 23 | public explorerAsyncapiServices( 24 | wrapper: InstanceWrapper, 25 | modulePath?: string, 26 | globalPrefix?: string, 27 | operationIdFactory?: (controllerKey: string, methodKey: string) => string, 28 | ) { 29 | if (operationIdFactory) { 30 | this.operationIdFactory = operationIdFactory; 31 | } 32 | 33 | const { instance, metatype } = wrapper; 34 | if ( 35 | !instance || 36 | !metatype || 37 | !Reflect.getMetadataKeys(metatype).find((label) => 38 | asyncApiClassAnnotationLabels.includes(label), 39 | ) 40 | ) { 41 | return []; 42 | } 43 | 44 | const prototype = Object.getPrototypeOf(instance); 45 | const documentResolvers: DenormalizedDocResolvers = { 46 | root: [ 47 | exploreAsyncapiClassMetadata, 48 | exploreControllerMetadata, 49 | exploreGatewayMetadata, 50 | ], 51 | security: [], 52 | tags: [], 53 | operations: [exploreAsyncApiOperationMetadata], 54 | }; 55 | 56 | return this.generateDenormalizedDocument( 57 | metatype as Type, 58 | prototype, 59 | instance, 60 | documentResolvers, 61 | modulePath, 62 | globalPrefix, 63 | ); 64 | } 65 | 66 | public getSchemas(): Record { 67 | const ret = { ...this.schemas } as unknown as Record; 68 | return ret; 69 | } 70 | 71 | private generateDenormalizedDocument( 72 | metatype: Type, 73 | prototype: Type, 74 | instance: object, 75 | documentResolvers: DenormalizedDocResolvers, 76 | _modulePath?: string, 77 | _globalPrefix?: string, 78 | ): DenormalizedDoc[] { 79 | const denormalizedAsyncapiServices = this.metadataScanner.scanFromPrototype< 80 | unknown, 81 | DenormalizedDoc[] 82 | >(instance, prototype, (name) => { 83 | const targetCallback = prototype[name]; 84 | const methodMetadata = documentResolvers.root.reduce((_metadata, fn) => { 85 | const serviceMetadata = fn(metatype); 86 | 87 | const channels = documentResolvers.operations.reduce( 88 | (operations, exploreOperationsMeta) => { 89 | const meta = exploreOperationsMeta( 90 | this.schemas, 91 | instance, 92 | prototype, 93 | targetCallback, 94 | ); 95 | if (!meta) { 96 | return operations; 97 | } 98 | 99 | meta.forEach((op) => { 100 | if (operations.hasOwnProperty(op.channel)) { 101 | operations[op.channel] = { ...operations[op.channel], ...op }; 102 | } else { 103 | operations[op.channel] = op; 104 | } 105 | }); 106 | return operations; 107 | }, 108 | {}, 109 | ); 110 | 111 | return Object.keys(channels).map((channel) => ({ 112 | root: { ...serviceMetadata, name: channel }, 113 | operations: channels[channel], 114 | })); 115 | }, []); 116 | return methodMetadata; 117 | }); 118 | 119 | return flatten(denormalizedAsyncapiServices); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /lib/asyncapi-document.builder.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ExternalDocumentationObject, 3 | SecuritySchemeObject, 4 | TagObject, 5 | } from '@nestjs/swagger/dist/interfaces/open-api-spec.interface'; 6 | import { isUndefined, negate, pickBy } from 'lodash'; 7 | import { 8 | AsyncApiDocument, 9 | AsyncSecuritySchemeObject, 10 | AsyncServerObject, 11 | } from './interface'; 12 | 13 | export class AsyncApiDocumentBuilder { 14 | private readonly buildDocumentBase = (): Omit< 15 | AsyncApiDocument, 16 | 'channels' 17 | > => ({ 18 | asyncapi: '2.5.0', 19 | info: { 20 | title: '', 21 | description: '', 22 | version: '1.0.0', 23 | contact: {}, 24 | }, 25 | tags: [], 26 | servers: {}, 27 | components: {}, 28 | }); 29 | 30 | private readonly document: Omit = 31 | this.buildDocumentBase(); 32 | 33 | public setTitle(title: string): this { 34 | this.document.info.title = title; 35 | return this; 36 | } 37 | 38 | public setDescription(description: string): this { 39 | this.document.info.description = description; 40 | return this; 41 | } 42 | 43 | public setVersion(version: string): this { 44 | this.document.info.version = version; 45 | return this; 46 | } 47 | 48 | public setTermsOfService(termsOfService: string): this { 49 | this.document.info.termsOfService = termsOfService; 50 | return this; 51 | } 52 | 53 | public setContact(name: string, url: string, email: string): this { 54 | this.document.info.contact = { name, url, email }; 55 | return this; 56 | } 57 | 58 | public setLicense(name: string, url: string): this { 59 | this.document.info.license = { name, url }; 60 | return this; 61 | } 62 | 63 | public addServer(name: string, server: AsyncServerObject): this { 64 | this.document.servers[name] = server; 65 | return this; 66 | } 67 | 68 | public addServers( 69 | servers: { name: string; server: AsyncServerObject }[], 70 | ): this { 71 | for (const { name, server } of servers) { 72 | this.addServer(name, server); 73 | } 74 | 75 | return this; 76 | } 77 | 78 | public setExternalDoc(description: string, url: string): this { 79 | this.document.externalDocs = { description, url }; 80 | return this; 81 | } 82 | 83 | public setDefaultContentType(contentType: string) { 84 | this.document.defaultContentType = contentType; 85 | return this; 86 | } 87 | 88 | public addTag( 89 | name: string, 90 | description = '', 91 | externalDocs?: ExternalDocumentationObject, 92 | ): this { 93 | this.document.tags = this.document.tags.concat( 94 | pickBy( 95 | { 96 | name, 97 | description, 98 | externalDocs, 99 | }, 100 | negate(isUndefined), 101 | ) as TagObject, 102 | ); 103 | return this; 104 | } 105 | 106 | public addSecurity(name: string, options: AsyncSecuritySchemeObject): this { 107 | this.document.components.securitySchemes = { 108 | ...(this.document.components.securitySchemes || {}), 109 | [name]: options, 110 | }; 111 | return this; 112 | } 113 | 114 | public addBearerAuth( 115 | options: SecuritySchemeObject = { 116 | type: 'http', 117 | }, 118 | name = 'bearer', 119 | ): this { 120 | this.addSecurity(name, { 121 | scheme: 'bearer', 122 | bearerFormat: 'JWT', 123 | ...options, 124 | }); 125 | return this; 126 | } 127 | 128 | public addOAuth2( 129 | options: SecuritySchemeObject = { 130 | type: 'oauth2', 131 | }, 132 | name = 'oauth2', 133 | ): this { 134 | this.addSecurity(name, { 135 | type: 'oauth2', 136 | flows: {}, 137 | ...options, 138 | }); 139 | return this; 140 | } 141 | 142 | public addApiKey( 143 | options: SecuritySchemeObject = { 144 | type: 'apiKey', 145 | }, 146 | name = 'api_key', 147 | ): this { 148 | this.addSecurity(name, { 149 | type: 'apiKey', 150 | in: 'header', 151 | name, 152 | ...options, 153 | }); 154 | return this; 155 | } 156 | 157 | public addBasicAuth( 158 | options: SecuritySchemeObject = { 159 | type: 'http', 160 | }, 161 | name = 'basic', 162 | ): this { 163 | this.addSecurity(name, { 164 | type: 'http', 165 | scheme: 'basic', 166 | ...options, 167 | }); 168 | return this; 169 | } 170 | 171 | public addCookieAuth( 172 | cookieName = 'connect.sid', 173 | options: SecuritySchemeObject = { 174 | type: 'apiKey', 175 | }, 176 | securityName = 'cookie', 177 | ): this { 178 | this.addSecurity(securityName, { 179 | type: 'apiKey', 180 | in: 'cookie', 181 | name: cookieName, 182 | ...options, 183 | }); 184 | return this; 185 | } 186 | 187 | public build(): Omit { 188 | return this.document; 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /lib/interface/asyncapi-common.interfaces.ts: -------------------------------------------------------------------------------- 1 | import { 2 | InfoObject, 3 | ReferenceObject, 4 | SchemaObject, 5 | ServerVariableObject, 6 | } from '@nestjs/swagger/dist/interfaces/open-api-spec.interface'; 7 | import { 8 | AmqpChannelBinding, 9 | AmqpMessageBinding, 10 | AmqpOperationBinding, 11 | AmqpServerBinding, 12 | KafkaChannelBinding, 13 | KafkaMessageBinding, 14 | KafkaOperationBinding, 15 | KafkaServerBinding, 16 | } from '../binding'; 17 | import { AsyncOperationPayload } from './asyncapi-operation-payload.interface'; 18 | import { AsyncServerObject } from './asyncapi-server.interface'; 19 | 20 | export interface AsyncApiDocument { 21 | asyncapi: string; 22 | id?: string; 23 | info: InfoObject; 24 | servers?: Record; 25 | channels: AsyncChannelsObject; 26 | components?: AsyncComponentsObject; 27 | tags?: AsyncTagObject[]; 28 | externalDocs?: ExternalDocumentationObject; 29 | defaultContentType?: string; 30 | } 31 | 32 | export type AsyncChannelsObject = Record; 33 | export interface AsyncChannelObject { 34 | description?: string; 35 | subscribe?: AsyncOperationObject; 36 | publish?: AsyncOperationObject; 37 | parameters?: Record; 38 | bindings?: Record; 39 | } 40 | 41 | export interface AsyncServerVariableObject extends ServerVariableObject { 42 | examples?: string[]; 43 | } 44 | 45 | export type SecurityObject = Record; 46 | 47 | export interface AsyncComponentsObject { 48 | schemas?: Record; 49 | messages?: Record; 50 | securitySchemes?: Record; 51 | parameters?: Record; 52 | correlationIds?: Record; 53 | operationTraits?: Record; 54 | messageTraits?: Record; 55 | serverBindings?: Record; 56 | channelBindings?: Record; 57 | operationBindings?: Record< 58 | string, 59 | KafkaOperationBinding | AmqpOperationBinding 60 | >; 61 | messageBindings?: Record; 62 | } 63 | 64 | export interface AsyncMessageObject extends AsyncMessageTraitObject { 65 | payload?: { 66 | type?: AsyncOperationPayload; 67 | $ref?: AsyncOperationPayload; 68 | }; 69 | } 70 | 71 | export type MessageType = AsyncMessageObject | ReferenceObject; 72 | export interface OneOfMessageType { 73 | oneOf: MessageType[]; 74 | } 75 | 76 | export type AsyncOperationMessage = OneOfMessageType | MessageType; 77 | 78 | export interface AsyncOperationObject { 79 | channel: string; 80 | operationId?: string; 81 | summary?: string; 82 | description?: string; 83 | tags?: AsyncTagObject[]; 84 | externalDocs?: ExternalDocumentationObject; 85 | bindings?: Record; 86 | traits?: Record; 87 | message?: AsyncOperationMessage; 88 | } 89 | 90 | export interface AsyncOperationTraitObject { 91 | operationId?: string; 92 | summary?: string; 93 | description?: string; 94 | tags?: AsyncTagObject[]; 95 | externalDocs?: ExternalDocumentationObject; 96 | bindings?: Record; 97 | } 98 | 99 | export interface AsyncMessageTraitObject { 100 | headers?: SchemaObject; 101 | correlationId?: AsyncCorrelationObject; 102 | schemaFormat?: string; 103 | contentType?: string; 104 | name?: string; 105 | title?: string; 106 | summary?: string; 107 | description?: string; 108 | tags?: AsyncTagObject[]; 109 | externalDocs?: ExternalDocumentationObject; 110 | bindings?: Record; 111 | } 112 | 113 | export interface AsyncCorrelationObject { 114 | description?: string; 115 | location: string; 116 | } 117 | 118 | export interface AsyncTagObject { 119 | name: string; 120 | description?: string; 121 | externalDocs?: ExternalDocumentationObject; 122 | } 123 | 124 | export interface AsyncSecuritySchemeObject { 125 | type: SecuritySchemeType; 126 | description?: string; 127 | name?: string; 128 | in?: string; 129 | scheme?: string; 130 | bearerFormat?: string; 131 | flows?: OAuthFlowsObject; 132 | openIdConnectUrl?: string; 133 | } 134 | 135 | export declare type SecuritySchemeType = 136 | | 'userPassword' 137 | | 'apiKey' 138 | | 'X509' 139 | | 'symmetricEncryption' 140 | | 'asymmetricEncryption' 141 | | 'http' 142 | | 'oauth2' 143 | | 'openIdConnect'; 144 | 145 | export interface OAuthFlowsObject { 146 | implicit?: OAuthFlowObject; 147 | password?: OAuthFlowObject; 148 | clientCredentials?: OAuthFlowObject; 149 | authorizationCode?: OAuthFlowObject; 150 | } 151 | 152 | export interface OAuthFlowObject { 153 | authorizationUrl?: string; 154 | tokenUrl?: string; 155 | refreshUrl?: string; 156 | scopes: ScopesObject; 157 | } 158 | 159 | export type ScopesObject = Record; 160 | 161 | export type ParameterObject = BaseParameterObject; 162 | 163 | export interface BaseParameterObject { 164 | description?: string; 165 | schema?: SchemaObject | ReferenceObject; 166 | location?: string; 167 | } 168 | 169 | export interface ExternalDocumentationObject { 170 | description?: string; 171 | url: string; 172 | } 173 | -------------------------------------------------------------------------------- /lib/services/asyncapi.scanner.ts: -------------------------------------------------------------------------------- 1 | import { INestApplicationContext, Type } from '@nestjs/common'; 2 | import { MODULE_PATH } from '@nestjs/common/constants'; 3 | import { Injectable, InjectionToken } from '@nestjs/common/interfaces'; 4 | import { NestContainer } from '@nestjs/core/injector/container'; 5 | import { InstanceWrapper } from '@nestjs/core/injector/instance-wrapper'; 6 | import { Module } from '@nestjs/core/injector/module'; 7 | import { SchemaObject } from '@nestjs/swagger/dist/interfaces/open-api-spec.interface'; 8 | import { ModelPropertiesAccessor } from '@nestjs/swagger/dist/services/model-properties-accessor'; 9 | import { SchemaObjectFactory } from '@nestjs/swagger/dist/services/schema-object-factory'; 10 | import { SwaggerTypesMapper } from '@nestjs/swagger/dist/services/swagger-types-mapper'; 11 | import { stripLastSlash } from '@nestjs/swagger/dist/utils/strip-last-slash.util'; 12 | import { flatten, isEmpty } from 'lodash'; 13 | import { 14 | AsyncApiDocument, 15 | AsyncApiDocumentOptions, 16 | DenormalizedDoc, 17 | } from '../interface'; 18 | import { AsyncApiExplorer } from './asyncapi.explorer'; 19 | import { AsyncapiTransformer } from './asyncapi.transformer'; 20 | 21 | export class AsyncapiScanner { 22 | private readonly transformer = new AsyncapiTransformer(); 23 | private readonly explorer = new AsyncApiExplorer(); 24 | private readonly modelPropertiesAccessor = new ModelPropertiesAccessor(); 25 | private readonly swaggerTypesMapper = new SwaggerTypesMapper(); 26 | private readonly schemaObjectFactory = new SchemaObjectFactory( 27 | this.modelPropertiesAccessor, 28 | this.swaggerTypesMapper, 29 | ); 30 | 31 | public scanApplication( 32 | app: INestApplicationContext, 33 | options: AsyncApiDocumentOptions, 34 | ): Omit { 35 | const { 36 | deepScanRoutes, 37 | include: includedModules = [], 38 | extraModels = [], 39 | ignoreGlobalPrefix = false, 40 | operationIdFactory, 41 | } = options; 42 | 43 | const container: NestContainer = (app as any).container; 44 | const modules: Module[] = this.getModules( 45 | container.getModules(), 46 | includedModules, 47 | ); 48 | const globalPrefix = !ignoreGlobalPrefix 49 | ? stripLastSlash(this.getGlobalPrefix(app)) 50 | : ''; 51 | 52 | const denormalizedChannels = modules.reduce( 53 | (channels, { providers, metatype, imports, controllers }) => { 54 | let allProviders = new Map([...providers, ...controllers]); 55 | 56 | if (deepScanRoutes) { 57 | // only load submodules routes if asked 58 | const isGlobal = (module: Type) => 59 | !container.isGlobalModule(module); 60 | 61 | Array.from(imports.values()) 62 | .filter(isGlobal as any) 63 | .map( 64 | ({ 65 | providers: relatedProviders, 66 | controllers: relatedControllers, 67 | }) => ({ 68 | relatedProviders, 69 | relatedControllers, 70 | }), 71 | ) 72 | .forEach(({ relatedProviders, relatedControllers }) => { 73 | allProviders = new Map([ 74 | ...allProviders, 75 | ...relatedProviders, 76 | ...relatedControllers, 77 | ]); 78 | }); 79 | } 80 | const path = metatype 81 | ? Reflect.getMetadata(MODULE_PATH, metatype) 82 | : undefined; 83 | 84 | return [ 85 | ...channels, 86 | ...this.scanModuleProviders( 87 | allProviders, 88 | path, 89 | globalPrefix, 90 | operationIdFactory, 91 | ), 92 | ]; 93 | }, 94 | [], 95 | ); 96 | 97 | const schemas = this.explorer.getSchemas(); 98 | this.addExtraModels(schemas, extraModels); 99 | const normalizedChannels = this.transformer.normalizeChannels( 100 | flatten(denormalizedChannels), 101 | ); 102 | return { 103 | ...normalizedChannels, 104 | components: { schemas }, 105 | }; 106 | } 107 | 108 | private scanModuleProviders( 109 | providers: Map>, 110 | modulePath?: string, 111 | globalPrefix?: string, 112 | operationIdFactory?: (controllerKey: string, methodKey: string) => string, 113 | ): DenormalizedDoc[] { 114 | const denormalizedArray = [...providers.values()].reduce( 115 | (denormalized, prov) => { 116 | const object = this.explorer.explorerAsyncapiServices( 117 | prov, 118 | modulePath, 119 | globalPrefix, 120 | operationIdFactory, 121 | ); 122 | return [...denormalized, ...object]; 123 | }, 124 | [], 125 | ); 126 | 127 | return flatten(denormalizedArray) as any; 128 | } 129 | 130 | private getModules( 131 | modulesContainer: Map, 132 | include: Function[], 133 | ): Module[] { 134 | if (!include || isEmpty(include)) { 135 | return [...modulesContainer.values()]; 136 | } 137 | return [...modulesContainer.values()].filter(({ metatype }) => 138 | include.some((item) => item === metatype), 139 | ); 140 | } 141 | 142 | private getGlobalPrefix(app: INestApplicationContext): string { 143 | const internalConfigRef = (app as any).config; 144 | return internalConfigRef?.getGlobalPrefix() || ''; 145 | } 146 | 147 | private addExtraModels( 148 | schemas: Record, 149 | extraModels: Function[], 150 | ) { 151 | extraModels.forEach((item) => { 152 | this.schemaObjectFactory.exploreModelSchema(item, schemas); 153 | }); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /misc/references/ref.json: -------------------------------------------------------------------------------- 1 | {"asyncapi":"2.5.0","info":{"title":"Feline","description":"Feline server description here","version":"1.0","contact":{}},"tags":[],"servers":{"europe":{"url":"ws://0.0.0.0:4001","protocol":"socket.io","protocolVersion":"4","description":"Allows you to connect using the websocket protocol to our Socket.io server.","security":[{"user-password":[]}],"variables":{"port":{"description":"Secure connection (TLS) is available through port 443.","default":"443"}},"bindings":{}},"asia":{"url":"ws://0.0.0.0:4001","protocol":"socket.io","protocolVersion":"4","description":"Allows you to connect using the websocket protocol to our Socket.io server.","security":[{"user-password":[]}],"variables":{"port":{"description":"Secure connection (TLS) is available through port 443.","default":"443"}},"bindings":{}},"north-america":{"url":"ws://0.0.0.0:4001","protocol":"socket.io","protocolVersion":"4","description":"Allows you to connect using the websocket protocol to our Socket.io server.","security":[{"user-password":[]}],"variables":{"port":{"description":"Secure connection (TLS) is available through port 443.","default":"443"}},"bindings":{}},"south-america":{"url":"ws://0.0.0.0:4001","protocol":"socket.io","protocolVersion":"4","description":"Allows you to connect using the websocket protocol to our Socket.io server.","security":[{"user-password":[]}],"variables":{"port":{"description":"Secure connection (TLS) is available through port 443.","default":"443"}},"bindings":{}},"africa":{"url":"ws://0.0.0.0:4001","protocol":"socket.io","protocolVersion":"4","description":"Allows you to connect using the websocket protocol to our Socket.io server.","security":[{"user-password":[]}],"variables":{"port":{"description":"Secure connection (TLS) is available through port 443.","default":"443"}},"bindings":{}},"australia":{"url":"ws://0.0.0.0:4001","protocol":"socket.io","protocolVersion":"4","description":"Allows you to connect using the websocket protocol to our Socket.io server.","security":[{"user-password":[]}],"variables":{"port":{"description":"Secure connection (TLS) is available through port 443.","default":"443"}},"bindings":{}}},"components":{"securitySchemes":{"user-password":{"type":"userPassword"}},"schemas":{"Feline":{"type":"object","properties":{"id":{"type":"number"},"name":{"type":"string"},"age":{"type":"number"},"gender":{"enum":["male","female"],"type":"string"},"dominantPaw":{"enum":["left","right"],"type":"string"},"tags":{"type":"array","items":{"type":"string"}},"birthDatetime":{"format":"date-time","type":"string"}},"required":["id","name","age","gender","dominantPaw","tags","birthDatetime"]},"CreateFelineDto":{"type":"object","properties":{"correlationId":{"type":"string","format":"uuid"},"version":{"type":"string","format":"x.y.z","example":"1.0.1"},"payload":{"oneOf":[{"$ref":"#/components/schemas/Cat"},{"$ref":"#/components/schemas/Lion"},{"$ref":"#/components/schemas/Tiger"}],"allOf":[{"$ref":"#/components/schemas/Feline"}]},"timestamp":{"format":"date-time","type":"string"}},"required":["correlationId","version","payload","timestamp"]},"FelineRto":{"type":"object","properties":{"correlationId":{"type":"string","format":"uuid"},"version":{"type":"string","format":"x.y.z","example":"1.0.1"},"payload":{"oneOf":[{"$ref":"#/components/schemas/Cat"},{"$ref":"#/components/schemas/Lion"},{"$ref":"#/components/schemas/Tiger"}],"allOf":[{"$ref":"#/components/schemas/Feline"}]},"timestamp":{"format":"date-time","type":"string"}},"required":["correlationId","version","payload","timestamp"]},"FelineExtendedRto":{"type":"object","properties":{"correlationId":{"type":"string","format":"uuid"},"version":{"type":"string","format":"x.y.z","example":"1.0.1"},"payload":{"oneOf":[{"$ref":"#/components/schemas/Cat"},{"$ref":"#/components/schemas/Lion"},{"$ref":"#/components/schemas/Tiger"}],"allOf":[{"$ref":"#/components/schemas/Feline"}]},"timestamp":{"format":"date-time","type":"string"}},"required":["correlationId","version","payload","timestamp"]},"JournalingDataDto":{"type":"object","properties":{"correlationId":{"type":"string","format":"uuid"},"version":{"type":"string","format":"x.y.z","example":"1.0.1"},"payload":{"type":"object"},"timestamp":{"format":"date-time","type":"string"}},"required":["correlationId","version","payload","timestamp"]},"Cat":{"type":"object","properties":{"breed":{"type":"string","example":"Maine Coon"},"id":{"type":"number"},"name":{"type":"string"},"age":{"type":"number"},"gender":{"enum":["male","female"],"type":"string"},"dominantPaw":{"enum":["left","right"],"type":"string"},"tags":{"type":"array","items":{"type":"string"}},"birthDatetime":{"format":"date-time","type":"string"}},"required":["breed","id","name","age","gender","dominantPaw","tags","birthDatetime"]},"Lion":{"type":"object","properties":{"id":{"type":"number"},"name":{"type":"string"},"age":{"type":"number"},"gender":{"enum":["male","female"],"type":"string"},"dominantPaw":{"enum":["left","right"],"type":"string"},"tags":{"type":"array","items":{"type":"string"}},"birthDatetime":{"format":"date-time","type":"string"},"roarVolume":{"type":"number"}},"required":["id","name","age","gender","dominantPaw","tags","birthDatetime","roarVolume"]},"Tiger":{"type":"object","properties":{"id":{"type":"number"},"name":{"type":"string"},"age":{"type":"number"},"gender":{"enum":["male","female"],"type":"string"},"dominantPaw":{"enum":["left","right"],"type":"string"},"tags":{"type":"array","items":{"type":"string"}},"birthDatetime":{"format":"date-time","type":"string"},"numberOfStripes":{"type":"number"}},"required":["id","name","age","gender","dominantPaw","tags","birthDatetime","numberOfStripes"]}}},"defaultContentType":"application/json","channels":{"ws/create/feline":{"subscribe":{"message":{"oneOf":[{"name":"oneOf demo #1","payload":{"$ref":"#/components/schemas/FelineRto"}},{"name":"oneOf demo #2","payload":{"$ref":"#/components/schemas/FelineExtendedRto"}}]}},"publish":{"message":{"payload":{"$ref":"#/components/schemas/CreateFelineDto"},"name":"FelinesGateway#createFeline"}}},"ms/journal":{"subscribe":{"message":{"payload":{"$ref":"#/components/schemas/JournalingDataDto"},"name":"FelinesController#journal"}}},"ms/create/feline":{"subscribe":{"message":{"payload":{"$ref":"#/components/schemas/FelineRto"},"name":"FelinesController#createFeline"}},"publish":{"message":{"payload":{"$ref":"#/components/schemas/CreateFelineDto"},"name":"FelinesController#createFeline"}}}}} -------------------------------------------------------------------------------- /misc/references/ref.yaml: -------------------------------------------------------------------------------- 1 | asyncapi: 2.5.0 2 | info: 3 | title: Feline 4 | description: Feline server description here 5 | version: '1.0' 6 | contact: {} 7 | tags: [] 8 | servers: 9 | europe: &ref_0 10 | url: ws://0.0.0.0:4001 11 | protocol: socket.io 12 | protocolVersion: '4' 13 | description: >- 14 | Allows you to connect using the websocket protocol to our Socket.io 15 | server. 16 | security: 17 | - user-password: [] 18 | variables: 19 | port: 20 | description: Secure connection (TLS) is available through port 443. 21 | default: '443' 22 | bindings: {} 23 | asia: *ref_0 24 | north-america: *ref_0 25 | south-america: *ref_0 26 | africa: *ref_0 27 | australia: *ref_0 28 | components: 29 | securitySchemes: 30 | user-password: 31 | type: userPassword 32 | schemas: 33 | Feline: 34 | type: object 35 | properties: 36 | id: 37 | type: number 38 | name: 39 | type: string 40 | age: 41 | type: number 42 | gender: 43 | enum: &ref_2 44 | - male 45 | - female 46 | type: string 47 | dominantPaw: 48 | enum: &ref_3 49 | - left 50 | - right 51 | type: string 52 | tags: 53 | type: array 54 | items: 55 | type: string 56 | birthDatetime: 57 | format: date-time 58 | type: string 59 | required: 60 | - id 61 | - name 62 | - age 63 | - gender 64 | - dominantPaw 65 | - tags 66 | - birthDatetime 67 | CreateFelineDto: 68 | type: object 69 | properties: 70 | correlationId: 71 | type: string 72 | format: uuid 73 | version: 74 | type: string 75 | format: x.y.z 76 | example: 1.0.1 77 | payload: 78 | oneOf: 79 | - $ref: '#/components/schemas/Cat' 80 | - $ref: '#/components/schemas/Lion' 81 | - $ref: '#/components/schemas/Tiger' 82 | allOf: 83 | - $ref: '#/components/schemas/Feline' 84 | timestamp: 85 | format: date-time 86 | type: string 87 | required: 88 | - correlationId 89 | - version 90 | - payload 91 | - timestamp 92 | FelineRto: 93 | type: object 94 | properties: 95 | correlationId: 96 | type: string 97 | format: uuid 98 | version: 99 | type: string 100 | format: x.y.z 101 | example: 1.0.1 102 | payload: 103 | oneOf: &ref_1 104 | - $ref: '#/components/schemas/Cat' 105 | - $ref: '#/components/schemas/Lion' 106 | - $ref: '#/components/schemas/Tiger' 107 | allOf: 108 | - $ref: '#/components/schemas/Feline' 109 | timestamp: 110 | format: date-time 111 | type: string 112 | required: 113 | - correlationId 114 | - version 115 | - payload 116 | - timestamp 117 | FelineExtendedRto: 118 | type: object 119 | properties: 120 | correlationId: 121 | type: string 122 | format: uuid 123 | version: 124 | type: string 125 | format: x.y.z 126 | example: 1.0.1 127 | payload: 128 | oneOf: *ref_1 129 | allOf: 130 | - $ref: '#/components/schemas/Feline' 131 | timestamp: 132 | format: date-time 133 | type: string 134 | required: 135 | - correlationId 136 | - version 137 | - payload 138 | - timestamp 139 | JournalingDataDto: 140 | type: object 141 | properties: 142 | correlationId: 143 | type: string 144 | format: uuid 145 | version: 146 | type: string 147 | format: x.y.z 148 | example: 1.0.1 149 | payload: 150 | type: object 151 | timestamp: 152 | format: date-time 153 | type: string 154 | required: 155 | - correlationId 156 | - version 157 | - payload 158 | - timestamp 159 | Cat: 160 | type: object 161 | properties: 162 | breed: 163 | type: string 164 | example: Maine Coon 165 | id: 166 | type: number 167 | name: 168 | type: string 169 | age: 170 | type: number 171 | gender: 172 | enum: *ref_2 173 | type: string 174 | dominantPaw: 175 | enum: *ref_3 176 | type: string 177 | tags: 178 | type: array 179 | items: 180 | type: string 181 | birthDatetime: 182 | format: date-time 183 | type: string 184 | required: 185 | - breed 186 | - id 187 | - name 188 | - age 189 | - gender 190 | - dominantPaw 191 | - tags 192 | - birthDatetime 193 | Lion: 194 | type: object 195 | properties: 196 | id: 197 | type: number 198 | name: 199 | type: string 200 | age: 201 | type: number 202 | gender: 203 | enum: *ref_2 204 | type: string 205 | dominantPaw: 206 | enum: *ref_3 207 | type: string 208 | tags: 209 | type: array 210 | items: 211 | type: string 212 | birthDatetime: 213 | format: date-time 214 | type: string 215 | roarVolume: 216 | type: number 217 | required: 218 | - id 219 | - name 220 | - age 221 | - gender 222 | - dominantPaw 223 | - tags 224 | - birthDatetime 225 | - roarVolume 226 | Tiger: 227 | type: object 228 | properties: 229 | id: 230 | type: number 231 | name: 232 | type: string 233 | age: 234 | type: number 235 | gender: 236 | enum: *ref_2 237 | type: string 238 | dominantPaw: 239 | enum: *ref_3 240 | type: string 241 | tags: 242 | type: array 243 | items: 244 | type: string 245 | birthDatetime: 246 | format: date-time 247 | type: string 248 | numberOfStripes: 249 | type: number 250 | required: 251 | - id 252 | - name 253 | - age 254 | - gender 255 | - dominantPaw 256 | - tags 257 | - birthDatetime 258 | - numberOfStripes 259 | defaultContentType: application/json 260 | channels: 261 | ws/create/feline: 262 | subscribe: 263 | message: 264 | oneOf: 265 | - name: 'oneOf demo #1' 266 | payload: 267 | $ref: '#/components/schemas/FelineRto' 268 | - name: 'oneOf demo #2' 269 | payload: 270 | $ref: '#/components/schemas/FelineExtendedRto' 271 | publish: 272 | message: 273 | payload: 274 | $ref: '#/components/schemas/CreateFelineDto' 275 | name: FelinesGateway#createFeline 276 | ms/journal: 277 | subscribe: 278 | message: 279 | payload: 280 | $ref: '#/components/schemas/JournalingDataDto' 281 | name: FelinesController#journal 282 | ms/create/feline: 283 | subscribe: 284 | message: 285 | payload: 286 | $ref: '#/components/schemas/FelineRto' 287 | name: FelinesController#createFeline 288 | publish: 289 | message: 290 | payload: 291 | $ref: '#/components/schemas/CreateFelineDto' 292 | name: FelinesController#createFeline 293 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Nest 2 | 3 | We would love for you to contribute to Nest and help make it even better than it is 4 | today! As a contributor, here are the guidelines we would like you to follow: 5 | 6 | - [Code of Conduct](#coc) 7 | - [Question or Problem?](#question) 8 | - [Issues and Bugs](#issue) 9 | - [Feature Requests](#feature) 10 | - [Submission Guidelines](#submit) 11 | - [Coding Rules](#rules) 12 | - [Commit Message Guidelines](#commit) 13 | 14 | 15 | 17 | 18 | ## Got a Question or Problem? 19 | 20 | **Do not open issues for general support questions as we want to keep GitHub issues for bug reports and feature requests.** You've got much better chances of getting your question answered on [Stack Overflow](https://stackoverflow.com/questions/tagged/nestjs) where the questions should be tagged with tag `nestjs`. 21 | 22 | Stack Overflow is a much better place to ask questions since: 23 | 24 | 25 | - questions and answers stay available for public viewing so your question / answer might help someone else 26 | - Stack Overflow's voting system assures that the best answers are prominently visible. 27 | 28 | To save your and our time, we will systematically close all issues that are requests for general support and redirect people to Stack Overflow. 29 | 30 | If you would like to chat about the question in real-time, you can reach out via [our gitter channel][gitter]. 31 | 32 | ## Found a Bug? 33 | If you find a bug in the source code, you can help us by 34 | [submitting an issue](#submit-issue) to our [GitHub Repository][github]. Even better, you can 35 | [submit a Pull Request](#submit-pr) with a fix. 36 | 37 | ## Missing a Feature? 38 | You can *request* a new feature by [submitting an issue](#submit-issue) to our GitHub 39 | Repository. If you would like to *implement* a new feature, please submit an issue with 40 | a proposal for your work first, to be sure that we can use it. 41 | Please consider what kind of change it is: 42 | 43 | * For a **Major Feature**, first open an issue and outline your proposal so that it can be 44 | discussed. This will also allow us to better coordinate our efforts, prevent duplication of work, 45 | and help you to craft the change so that it is successfully accepted into the project. For your issue name, please prefix your proposal with `[discussion]`, for example "[discussion]: your feature idea". 46 | * **Small Features** can be crafted and directly [submitted as a Pull Request](#submit-pr). 47 | 48 | ## Submission Guidelines 49 | 50 | ### Submitting an Issue 51 | 52 | Before you submit an issue, please search the issue tracker, maybe an issue for your problem already exists and the discussion might inform you of workarounds readily available. 53 | 54 | We want to fix all the issues as soon as possible, but before fixing a bug we need to reproduce and confirm it. In order to reproduce bugs we will systematically ask you to provide a minimal reproduction scenario using a repository or [Gist](https://gist.github.com/). Having a live, reproducible scenario gives us wealth of important information without going back & forth to you with additional questions like: 55 | 56 | - version of NestJS used 57 | - 3rd-party libraries and their versions 58 | - and most importantly - a use-case that fails 59 | 60 | 64 | 65 | 66 | 67 | Unfortunately, we are not able to investigate / fix bugs without a minimal reproduction, so if we don't hear back from you we are going to close an issue that don't have enough info to be reproduced. 68 | 69 | You can file new issues by filling out our [new issue form](https://github.com/flamewow/nestjs-asyncapi/issues/new). 70 | 71 | 72 | ### Submitting a Pull Request (PR) 73 | Before you submit your Pull Request (PR) consider the following guidelines: 74 | 75 | 1. Search [GitHub](https://github.com/flamewow/nestjs-asyncapi/pulls) for an open or closed PR 76 | that relates to your submission. You don't want to duplicate effort. 77 | 79 | 1. Fork the nestjs/nest repo. 80 | 1. Make your changes in a new git branch: 81 | 82 | ```shell 83 | git checkout -b my-fix-branch main 84 | ``` 85 | 86 | 1. Create your patch, **including appropriate test cases**. 87 | 1. Follow our [Coding Rules](#rules). 88 | 1. Run the full Nest test suite, as described in the [developer documentation][dev-doc], 89 | and ensure that all tests pass. 90 | 1. Commit your changes using a descriptive commit message that follows our 91 | [commit message conventions](#commit). Adherence to these conventions 92 | is necessary because release notes are automatically generated from these messages. 93 | 94 | ```shell 95 | git commit -a 96 | ``` 97 | Note: the optional commit `-a` command line option will automatically "add" and "rm" edited files. 98 | 99 | 1. Push your branch to GitHub: 100 | 101 | ```shell 102 | git push origin my-fix-branch 103 | ``` 104 | 105 | 1. In GitHub, send a pull request to `nestjs:main`. 106 | * If we suggest changes then: 107 | * Make the required updates. 108 | * Re-run the Nest test suites to ensure tests are still passing. 109 | * Rebase your branch and force push to your GitHub repository (this will update your Pull Request): 110 | 111 | ```shell 112 | git rebase main -i 113 | git push -f 114 | ``` 115 | 116 | That's it! Thank you for your contribution! 117 | 118 | #### After your pull request is merged 119 | 120 | After your pull request is merged, you can safely delete your branch and pull the changes 121 | from the main (upstream) repository: 122 | 123 | * Delete the remote branch on GitHub either through the GitHub web UI or your local shell as follows: 124 | 125 | ```shell 126 | git push origin --delete my-fix-branch 127 | ``` 128 | 129 | * Check out the main branch: 130 | 131 | ```shell 132 | git checkout main -f 133 | ``` 134 | 135 | * Delete the local branch: 136 | 137 | ```shell 138 | git branch -D my-fix-branch 139 | ``` 140 | 141 | * Update your main with the latest upstream version: 142 | 143 | ```shell 144 | git pull --ff upstream main 145 | ``` 146 | 147 | ## Coding Rules 148 | To ensure consistency throughout the source code, keep these rules in mind as you are working: 149 | 150 | * All features or bug fixes **must be tested** by one or more specs (unit-tests). 151 | 154 | * We follow [Google's JavaScript Style Guide][js-style-guide], but wrap all code at 155 | **100 characters**. An automated formatter is available (`npm run format`). 156 | 157 | ## Commit Message Guidelines 158 | 159 | We have very precise rules over how our git commit messages can be formatted. This leads to **more 160 | readable messages** that are easy to follow when looking through the **project history**. But also, 161 | we use the git commit messages to **generate the Nest change log**. 162 | 163 | ### Commit Message Format 164 | Each commit message consists of a **header**, a **body** and a **footer**. The header has a special 165 | format that includes a **type**, a **scope** and a **subject**: 166 | 167 | ``` 168 | (): 169 | 170 | 171 | 172 |