├── index.ts ├── plugin.ts ├── lib ├── swagger-ui │ ├── index.ts │ ├── helpers.ts │ └── swagger-ui.ts ├── utils │ ├── index.ts │ ├── validate-path.util.ts │ ├── validate-global-prefix.util.ts │ ├── resolve-path.util.ts │ ├── is-date-ctor.util.ts │ ├── strip-last-slash.util.ts │ ├── merge-and-uniq.util.ts │ ├── is-body-parameter.util.ts │ ├── normalize-rel-path.ts │ ├── remove-undefined-keys.ts │ ├── get-global-prefix.ts │ ├── is-built-in-type.util.ts │ ├── reverse-object-keys.util.ts │ ├── sort-object-lexicographically.ts │ ├── extend-metadata.util.ts │ ├── assign-two-levels-deep.ts │ ├── get-schema-path.util.ts │ └── enum.utils.ts ├── services │ ├── constants.ts │ ├── mimetype-content-wrapper.ts │ ├── model-properties-accessor.ts │ ├── response-object-mapper.ts │ ├── parameters-metadata-mapper.ts │ ├── parameter-metadata-accessor.ts │ └── decorators-properties.ts ├── plugin │ ├── index.ts │ ├── utils │ │ ├── is-filename-matched.util.ts │ │ └── type-reference-to-identifier.util.ts │ ├── plugin-constants.ts │ ├── plugin-debug-logger.ts │ ├── metadata-loader.ts │ ├── compiler-plugin.ts │ ├── visitors │ │ ├── abstract.visitor.ts │ │ └── readonly.visitor.ts │ └── merge-options.ts ├── types │ └── swagger-enum.type.ts ├── interfaces │ ├── module-route.interface.ts │ ├── index.ts │ ├── denormalized-doc-resolvers.interface.ts │ ├── callback-object.interface.ts │ ├── enum-schema-attributes.interface.ts │ ├── denormalized-doc.interface.ts │ ├── swagger-ui-init-options.interface.ts │ ├── swagger-ui-options.interface.ts │ ├── schema-object-metadata.interface.ts │ ├── swagger-document-options.interface.ts │ └── swagger-custom-options.interface.ts ├── type-helpers │ ├── index.ts │ ├── mapped-types.utils.ts │ ├── omit-type.helper.ts │ ├── pick-type.helper.ts │ ├── intersection-type.helper.ts │ └── partial-type.helper.ts ├── decorators │ ├── api-basic.decorator.ts │ ├── api-bearer.decorator.ts │ ├── api-cookie.decorator.ts │ ├── api-hide-property.decorator.ts │ ├── api-oauth2.decorator.ts │ ├── api-use-tags.decorator.ts │ ├── api-consumes.decorator.ts │ ├── api-produces.decorator.ts │ ├── api-exclude-controller.decorator.ts │ ├── api-exclude-endpoint.decorator.ts │ ├── api-callbacks.decorator.ts │ ├── api-schema.decorator.ts │ ├── api-operation.decorator.ts │ ├── api-extra-models.decorator.ts │ ├── index.ts │ ├── api-security.decorator.ts │ ├── api-default-getter.decorator.ts │ ├── api-body.decorator.ts │ ├── api-param.decorator.ts │ ├── api-link.decorator.ts │ ├── api-extension.decorator.ts │ ├── api-header.decorator.ts │ ├── api-query.decorator.ts │ └── api-property.decorator.ts ├── index.ts ├── explorers │ ├── api-exclude-controller.explorer.ts │ ├── api-exclude-endpoint.explorer.ts │ ├── api-headers.explorer.ts │ ├── api-produces.explorer.ts │ ├── api-consumes.explorer.ts │ ├── api-security.explorer.ts │ ├── api-extra-models.explorer.ts │ ├── api-use-tags.explorer.ts │ ├── api-callbacks.explorer.ts │ ├── api-operation.explorer.ts │ └── api-parameters.explorer.ts ├── fixtures │ └── document.base.ts ├── storages │ ├── global-parameters.storage.ts │ └── global-responses.storage.ts ├── swagger-transformer.ts └── constants.ts ├── .prettierrc ├── e2e ├── public │ ├── logo.png │ ├── favicon.ico │ └── theme.css ├── src │ ├── cats │ │ ├── enums │ │ │ ├── x-enum-test.enum.ts │ │ │ └── cat-breed.enum.ts │ │ ├── dto │ │ │ ├── tag.dto.ts │ │ │ ├── extra-model.dto.ts │ │ │ ├── pagination-query.dto.ts │ │ │ └── create-cat.dto.ts │ │ ├── cats.module.ts │ │ ├── cats.service.ts │ │ └── classes │ │ │ └── cat.class.ts │ ├── express.controller.ts │ ├── app.module.ts │ ├── app.controller.ts │ ├── common │ │ └── dto │ │ │ └── validation-error.dto.ts │ └── fastify.controller.ts └── jest-e2e.json ├── test ├── plugin │ ├── fixtures │ │ ├── project │ │ │ ├── cats │ │ │ │ ├── dto │ │ │ │ │ ├── non-exported.dto.ts │ │ │ │ │ ├── tag.dto.ts │ │ │ │ │ ├── extra-model.dto.ts │ │ │ │ │ ├── pagination-query.dto.ts │ │ │ │ │ └── create-cat.dto.ts │ │ │ │ ├── cats.module.ts │ │ │ │ ├── cats.service.ts │ │ │ │ ├── classes │ │ │ │ │ └── cat.class.ts │ │ │ │ └── cats.controller.ts │ │ │ ├── app.module.ts │ │ │ ├── tsconfig.json │ │ │ └── app.controller.ts │ │ ├── string-literal.dto.ts │ │ ├── changed-class.dto.ts │ │ ├── create-option.ts │ │ ├── parameter-property.dto.ts │ │ ├── app.controller-tabs.ts │ │ ├── nullable.dto.ts │ │ ├── create-cat-alt.dto.ts │ │ ├── es5-class.dto.ts │ │ ├── create-cat-alt2.dto.ts │ │ └── create-cat-exclusive.dto.ts │ ├── merge-option.spec.ts │ ├── controller-class-visitor.spec.ts │ ├── readonly-visitor.spec.ts │ └── helpers │ │ └── metadata-printer.ts ├── type-helpers │ ├── type-helpers.test-utils.ts │ ├── fixtures │ │ ├── serialized-metadata.fixture.ts │ │ └── create-user-dto.fixture.ts │ ├── omit-type.helper.spec.ts │ ├── pick-type.helper.spec.ts │ ├── intersection-type.helper.spec.ts │ └── partial-type.helper.spec.ts ├── services │ ├── fixtures │ │ ├── create-profile.dto.ts │ │ └── create-user.dto.ts │ └── model-properties-accessor.spec.ts ├── extra │ └── shim.spec.ts └── decorators │ ├── api-query.decorator.spec.ts │ └── api-param.decorator.spec.ts ├── nest-cli.json ├── .release-it.json ├── renovate.json ├── jest.config.json ├── .gitignore ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── Feature_request.yml │ ├── Regression.yml │ └── Bug_report.yml ├── lock.yml └── PULL_REQUEST_TEMPLATE.md ├── plugin.js ├── .npmignore ├── tsconfig.json ├── tsconfig.build.json ├── .commitlintrc.json ├── LICENSE ├── eslint.config.mjs ├── .circleci └── config.yml ├── README.md └── package.json /index.ts: -------------------------------------------------------------------------------- 1 | export * from './dist'; 2 | -------------------------------------------------------------------------------- /plugin.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/plugin'; 2 | -------------------------------------------------------------------------------- /lib/swagger-ui/index.ts: -------------------------------------------------------------------------------- 1 | export * from './swagger-ui'; 2 | -------------------------------------------------------------------------------- /lib/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './get-schema-path.util'; 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "none" 4 | } -------------------------------------------------------------------------------- /e2e/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nestjs/swagger/HEAD/e2e/public/logo.png -------------------------------------------------------------------------------- /e2e/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nestjs/swagger/HEAD/e2e/public/favicon.ico -------------------------------------------------------------------------------- /lib/services/constants.ts: -------------------------------------------------------------------------------- 1 | export const BUILT_IN_TYPES = [String, Boolean, Number, Object, Array]; 2 | -------------------------------------------------------------------------------- /lib/plugin/index.ts: -------------------------------------------------------------------------------- 1 | export * from './compiler-plugin'; 2 | export * from './visitors/readonly.visitor'; 3 | -------------------------------------------------------------------------------- /test/plugin/fixtures/project/cats/dto/non-exported.dto.ts: -------------------------------------------------------------------------------- 1 | class NonExportedDto { 2 | name: string; 3 | } 4 | -------------------------------------------------------------------------------- /e2e/src/cats/enums/x-enum-test.enum.ts: -------------------------------------------------------------------------------- 1 | export enum XEnumTest { 2 | APPROVED = 1, 3 | PENDING, 4 | REJECTED 5 | } 6 | -------------------------------------------------------------------------------- /e2e/src/cats/enums/cat-breed.enum.ts: -------------------------------------------------------------------------------- 1 | export enum CatBreed { 2 | Persian = 'persian', 3 | Siamese = 'siamese', 4 | MaineCoon = 'maine-coon' 5 | } 6 | -------------------------------------------------------------------------------- /lib/types/swagger-enum.type.ts: -------------------------------------------------------------------------------- 1 | export type SwaggerEnumType = 2 | | string[] 3 | | number[] 4 | | (string | number)[] 5 | | Record; 6 | -------------------------------------------------------------------------------- /lib/utils/validate-path.util.ts: -------------------------------------------------------------------------------- 1 | export const validatePath = (inputPath: string): string => 2 | inputPath.charAt(0) !== '/' ? '/' + inputPath : inputPath; 3 | -------------------------------------------------------------------------------- /lib/utils/validate-global-prefix.util.ts: -------------------------------------------------------------------------------- 1 | export const validateGlobalPrefix = (globalPrefix: string): boolean => 2 | globalPrefix && !globalPrefix.match(/^(\/?)$/); 3 | -------------------------------------------------------------------------------- /e2e/src/cats/dto/tag.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '../../../../lib'; 2 | 3 | export class TagDto { 4 | @ApiProperty({ description: 'name' }) 5 | name: string; 6 | } 7 | -------------------------------------------------------------------------------- /lib/interfaces/module-route.interface.ts: -------------------------------------------------------------------------------- 1 | import { OpenAPIObject } from '.'; 2 | 3 | export type ModuleRoute = Omit & 4 | Record<'root', any>; 5 | -------------------------------------------------------------------------------- /lib/plugin/utils/is-filename-matched.util.ts: -------------------------------------------------------------------------------- 1 | export const isFilenameMatched = (patterns: string[], filename: string) => 2 | patterns.some((path) => filename.includes(path)); 3 | -------------------------------------------------------------------------------- /lib/utils/resolve-path.util.ts: -------------------------------------------------------------------------------- 1 | import * as pathLib from 'path'; 2 | 3 | export function resolvePath(path: string): string { 4 | return path ? pathLib.resolve(path) : path; 5 | } 6 | -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nest-cli", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "e2e", 5 | "entryFile": "e2e/manual-e2e" 6 | } 7 | -------------------------------------------------------------------------------- /lib/type-helpers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './intersection-type.helper'; 2 | export * from './omit-type.helper'; 3 | export * from './partial-type.helper'; 4 | export * from './pick-type.helper'; 5 | -------------------------------------------------------------------------------- /lib/utils/is-date-ctor.util.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@nestjs/common'; 2 | 3 | export function isDateCtor(type: Type | Function | string): boolean { 4 | return type === Date; 5 | } 6 | -------------------------------------------------------------------------------- /lib/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export { OpenAPIObject } from './open-api-spec.interface'; 2 | export * from './swagger-custom-options.interface'; 3 | export * from './swagger-document-options.interface'; 4 | -------------------------------------------------------------------------------- /test/plugin/fixtures/project/cats/dto/tag.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '../../../../lib'; 2 | 3 | export class TagDto { 4 | @ApiProperty({ description: 'name' }) 5 | name: string; 6 | } 7 | -------------------------------------------------------------------------------- /lib/plugin/plugin-constants.ts: -------------------------------------------------------------------------------- 1 | export const OPENAPI_NAMESPACE = 'openapi'; 2 | export const OPENAPI_PACKAGE_NAME = '@nestjs/swagger'; 3 | export const METADATA_FACTORY_NAME = '_OPENAPI_METADATA_FACTORY'; 4 | -------------------------------------------------------------------------------- /lib/utils/strip-last-slash.util.ts: -------------------------------------------------------------------------------- 1 | export function stripLastSlash(path: string): string { 2 | return path && path[path.length - 1] === '/' 3 | ? path.slice(0, path.length - 1) 4 | : path; 5 | } 6 | -------------------------------------------------------------------------------- /lib/plugin/plugin-debug-logger.ts: -------------------------------------------------------------------------------- 1 | import { ConsoleLogger } from '@nestjs/common'; 2 | 3 | class PluginDebugLogger extends ConsoleLogger {} 4 | 5 | export const pluginDebugLogger = new PluginDebugLogger('CLI Plugin'); 6 | -------------------------------------------------------------------------------- /lib/utils/merge-and-uniq.util.ts: -------------------------------------------------------------------------------- 1 | import { merge, uniq } from 'lodash'; 2 | 3 | export function mergeAndUniq(a: unknown = [], b: unknown = []): T { 4 | return uniq(merge(a, b) as any) as unknown as T; 5 | } 6 | -------------------------------------------------------------------------------- /lib/decorators/api-basic.decorator.ts: -------------------------------------------------------------------------------- 1 | import { ApiSecurity } from './api-security.decorator'; 2 | 3 | /** 4 | * @publicApi 5 | */ 6 | export function ApiBasicAuth(name = 'basic') { 7 | return ApiSecurity(name); 8 | } 9 | -------------------------------------------------------------------------------- /lib/decorators/api-bearer.decorator.ts: -------------------------------------------------------------------------------- 1 | import { ApiSecurity } from './api-security.decorator'; 2 | 3 | /** 4 | * @publicApi 5 | */ 6 | export function ApiBearerAuth(name = 'bearer') { 7 | return ApiSecurity(name); 8 | } 9 | -------------------------------------------------------------------------------- /lib/decorators/api-cookie.decorator.ts: -------------------------------------------------------------------------------- 1 | import { ApiSecurity } from './api-security.decorator'; 2 | 3 | /** 4 | * @publicApi 5 | */ 6 | export function ApiCookieAuth(name = 'cookie') { 7 | return ApiSecurity(name); 8 | } 9 | -------------------------------------------------------------------------------- /lib/decorators/api-hide-property.decorator.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @publicApi 3 | */ 4 | export function ApiHideProperty(): PropertyDecorator { 5 | return (target: Record, propertyKey: string | symbol) => {}; 6 | } 7 | -------------------------------------------------------------------------------- /lib/interfaces/denormalized-doc-resolvers.interface.ts: -------------------------------------------------------------------------------- 1 | export interface DenormalizedDocResolvers { 2 | root: Function[]; 3 | security: Function[]; 4 | tags: Function[]; 5 | callbacks: Function[]; 6 | responses: Function[]; 7 | } 8 | -------------------------------------------------------------------------------- /lib/utils/is-body-parameter.util.ts: -------------------------------------------------------------------------------- 1 | import { ParamWithTypeMetadata } from '../services/parameter-metadata-accessor'; 2 | 3 | export function isBodyParameter(param: ParamWithTypeMetadata): boolean { 4 | return param.in === 'body'; 5 | } 6 | -------------------------------------------------------------------------------- /lib/utils/normalize-rel-path.ts: -------------------------------------------------------------------------------- 1 | export function normalizeRelPath(input: string) { 2 | // replaces duplicate slashes with single slash: ////test///1 -> /test/1 3 | const output = input.replace(/\/\/+/g, '/'); 4 | return output; 5 | } 6 | -------------------------------------------------------------------------------- /lib/decorators/api-oauth2.decorator.ts: -------------------------------------------------------------------------------- 1 | import { ApiSecurity } from './api-security.decorator'; 2 | 3 | /** 4 | * @publicApi 5 | */ 6 | export function ApiOAuth2(scopes: string[], name = 'oauth2') { 7 | return ApiSecurity(name, scopes); 8 | } 9 | -------------------------------------------------------------------------------- /.release-it.json: -------------------------------------------------------------------------------- 1 | { 2 | "git": { 3 | "commitMessage": "chore(): release v${version}" 4 | }, 5 | "github": { 6 | "release": true, 7 | "releaseNotes": "npx lerna-changelog --from ${latestTag}", 8 | "web": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /lib/index.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | export * from './decorators'; 3 | export * from './document-builder'; 4 | export * from './interfaces'; 5 | export * from './swagger-module'; 6 | export * from './type-helpers'; 7 | export * from './utils'; 8 | -------------------------------------------------------------------------------- /lib/utils/remove-undefined-keys.ts: -------------------------------------------------------------------------------- 1 | export function removeUndefinedKeys(obj: { [x: string]: any }) { 2 | Object.entries(obj).forEach(([key, value]) => { 3 | if (value === undefined) { 4 | delete obj[key]; 5 | } 6 | }); 7 | return obj; 8 | } 9 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "semanticCommits": true, 3 | "packageRules": [ 4 | { 5 | "depTypeList": ["devDependencies"], 6 | "automerge": true 7 | } 8 | ], 9 | "labels": ["dependencies"], 10 | "extends": ["config:base"] 11 | } 12 | -------------------------------------------------------------------------------- /test/plugin/fixtures/project/cats/dto/extra-model.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '../../../../lib'; 2 | 3 | export class ExtraModel { 4 | @ApiProperty() 5 | readonly one: string; 6 | 7 | @ApiProperty() 8 | readonly two: number; 9 | } 10 | -------------------------------------------------------------------------------- /jest.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": "test", 4 | "testEnvironment": "node", 5 | "testRegex": ".spec.ts$", 6 | "verbose": true, 7 | "transform": { 8 | "^.+\\.(t|j)s$": "ts-jest" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /e2e/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "verbose": true, 7 | "transform": { 8 | "^.+\\.(t|j)s$": "ts-jest" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | 4 | # IDE 5 | /.idea 6 | /.awcache 7 | /.vscode 8 | .history 9 | 10 | # misc 11 | npm-debug.log 12 | .DS_Store 13 | 14 | # tests 15 | /coverage 16 | /.nyc_output 17 | 18 | # source 19 | dist 20 | index.js 21 | index.d.ts 22 | /sample -------------------------------------------------------------------------------- /lib/utils/get-global-prefix.ts: -------------------------------------------------------------------------------- 1 | import { INestApplication } from '@nestjs/common'; 2 | 3 | export function getGlobalPrefix(app: INestApplication): string { 4 | const internalConfigRef = (app as any).config; 5 | return (internalConfigRef && internalConfigRef.getGlobalPrefix()) || ''; 6 | } 7 | -------------------------------------------------------------------------------- /e2e/src/express.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | 3 | @Controller({ 4 | version: ['1', '2'] 5 | }) 6 | export class ExpressController { 7 | @Get('express\\:colon\\:another/:prop') 8 | withColons(): string { 9 | return 'Hello world!'; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /lib/decorators/api-use-tags.decorator.ts: -------------------------------------------------------------------------------- 1 | import { DECORATORS } from '../constants'; 2 | import { createMixedDecorator } from './helpers'; 3 | 4 | /** 5 | * @publicApi 6 | */ 7 | export function ApiTags(...tags: string[]) { 8 | return createMixedDecorator(DECORATORS.API_TAGS, tags); 9 | } 10 | -------------------------------------------------------------------------------- /lib/interfaces/callback-object.interface.ts: -------------------------------------------------------------------------------- 1 | export interface CallBackObject { 2 | name: string; 3 | callbackUrl: string; 4 | method: string; 5 | requestBody: { 6 | type: T; 7 | }; 8 | expectedResponse: { 9 | status: number; 10 | description?: string; 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /e2e/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AppController } from './app.controller'; 3 | import { CatsModule } from './cats/cats.module'; 4 | 5 | @Module({ 6 | imports: [CatsModule], 7 | controllers: [AppController] 8 | }) 9 | export class ApplicationModule {} 10 | -------------------------------------------------------------------------------- /e2e/src/cats/cats.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { CatsController } from './cats.controller'; 3 | import { CatsService } from './cats.service'; 4 | 5 | @Module({ 6 | controllers: [CatsController], 7 | providers: [CatsService] 8 | }) 9 | export class CatsModule {} 10 | -------------------------------------------------------------------------------- /lib/explorers/api-exclude-controller.explorer.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@nestjs/common'; 2 | import { DECORATORS } from '../constants'; 3 | 4 | export const exploreApiExcludeControllerMetadata = (metatype: Type) => 5 | Reflect.getMetadata(DECORATORS.API_EXCLUDE_CONTROLLER, metatype)?.[0] === 6 | true; 7 | -------------------------------------------------------------------------------- /lib/interfaces/enum-schema-attributes.interface.ts: -------------------------------------------------------------------------------- 1 | import { SchemaObject } from './open-api-spec.interface'; 2 | 3 | export type EnumSchemaAttributes = Pick< 4 | SchemaObject, 5 | | 'default' 6 | | 'description' 7 | | 'deprecated' 8 | | 'readOnly' 9 | | 'writeOnly' 10 | | 'nullable' 11 | >; 12 | -------------------------------------------------------------------------------- /lib/decorators/api-consumes.decorator.ts: -------------------------------------------------------------------------------- 1 | import { DECORATORS } from '../constants'; 2 | import { createMixedDecorator } from './helpers'; 3 | 4 | /** 5 | * @publicApi 6 | */ 7 | export function ApiConsumes(...mimeTypes: string[]) { 8 | return createMixedDecorator(DECORATORS.API_CONSUMES, mimeTypes); 9 | } 10 | -------------------------------------------------------------------------------- /lib/decorators/api-produces.decorator.ts: -------------------------------------------------------------------------------- 1 | import { DECORATORS } from '../constants'; 2 | import { createMixedDecorator } from './helpers'; 3 | 4 | /** 5 | * @publicApi 6 | */ 7 | export function ApiProduces(...mimeTypes: string[]) { 8 | return createMixedDecorator(DECORATORS.API_PRODUCES, mimeTypes); 9 | } 10 | -------------------------------------------------------------------------------- /test/plugin/fixtures/project/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AppController } from './app.controller'; 3 | import { CatsModule } from './cats/cats.module'; 4 | 5 | @Module({ 6 | imports: [CatsModule], 7 | controllers: [AppController] 8 | }) 9 | export class ApplicationModule {} 10 | -------------------------------------------------------------------------------- /test/plugin/fixtures/project/cats/cats.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { CatsController } from './cats.controller'; 3 | import { CatsService } from './cats.service'; 4 | 5 | @Module({ 6 | controllers: [CatsController], 7 | providers: [CatsService] 8 | }) 9 | export class CatsModule {} 10 | -------------------------------------------------------------------------------- /lib/explorers/api-exclude-endpoint.explorer.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@nestjs/common'; 2 | import { DECORATORS } from '../constants'; 3 | 4 | export const exploreApiExcludeEndpointMetadata = ( 5 | instance: object, 6 | prototype: Type, 7 | method: object 8 | ) => Reflect.getMetadata(DECORATORS.API_EXCLUDE_ENDPOINT, method); 9 | -------------------------------------------------------------------------------- /e2e/src/cats/dto/extra-model.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty, ApiSchema } from '../../../../lib'; 2 | 3 | @ApiSchema({ 4 | name: 'ExtraModel', 5 | description: 'ExtraModel description' 6 | }) 7 | export class ExtraModelDto { 8 | @ApiProperty() 9 | readonly one: string; 10 | 11 | @ApiProperty() 12 | readonly two: number; 13 | } 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | ## To encourage contributors to use issue templates, we don't allow blank issues 2 | blank_issues_enabled: false 3 | 4 | contact_links: 5 | - name: "\u2753 Discord Community of NestJS" 6 | url: "https://discord.gg/NestJS" 7 | about: "Please ask support questions or discuss suggestions/enhancements here." 8 | -------------------------------------------------------------------------------- /lib/decorators/api-exclude-controller.decorator.ts: -------------------------------------------------------------------------------- 1 | import { DECORATORS } from '../constants'; 2 | import { createClassDecorator } from './helpers'; 3 | 4 | /** 5 | * @publicApi 6 | */ 7 | export function ApiExcludeController(disable = true): ClassDecorator { 8 | return createClassDecorator(DECORATORS.API_EXCLUDE_CONTROLLER, [disable]); 9 | } 10 | -------------------------------------------------------------------------------- /e2e/src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | 3 | @Controller({ 4 | version: ['1', '2'] 5 | }) 6 | export class AppController { 7 | @Get() 8 | getHello(): string { 9 | return 'Hello world!'; 10 | } 11 | 12 | @Get(['alias1', 'alias2']) 13 | withAliases(): string { 14 | return 'Hello world!'; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/decorators/api-exclude-endpoint.decorator.ts: -------------------------------------------------------------------------------- 1 | import { DECORATORS } from '../constants'; 2 | import { createMethodDecorator } from './helpers'; 3 | 4 | /** 5 | * @publicApi 6 | */ 7 | export function ApiExcludeEndpoint(disable = true): MethodDecorator { 8 | return createMethodDecorator(DECORATORS.API_EXCLUDE_ENDPOINT, { 9 | disable 10 | }); 11 | } 12 | -------------------------------------------------------------------------------- /lib/explorers/api-headers.explorer.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@nestjs/common'; 2 | import { DECORATORS } from '../constants'; 3 | 4 | export const exploreGlobalApiHeaderMetadata = (metatype: Type) => { 5 | const headers = Reflect.getMetadata(DECORATORS.API_HEADERS, metatype); 6 | return headers ? { root: { parameters: headers }, depth: 1 } : undefined; 7 | }; 8 | -------------------------------------------------------------------------------- /lib/fixtures/document.base.ts: -------------------------------------------------------------------------------- 1 | import { OpenAPIObject } from '../interfaces'; 2 | 3 | export const buildDocumentBase = (): Omit => ({ 4 | openapi: '3.0.0', 5 | info: { 6 | title: '', 7 | description: '', 8 | version: '1.0.0', 9 | contact: {} 10 | }, 11 | tags: [], 12 | servers: [], 13 | components: {} 14 | }); 15 | -------------------------------------------------------------------------------- /lib/utils/is-built-in-type.util.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@nestjs/common'; 2 | import { isFunction } from 'lodash'; 3 | import { BUILT_IN_TYPES } from '../services/constants'; 4 | 5 | export function isBuiltInType( 6 | type: Type | Function | string 7 | ): boolean { 8 | return isFunction(type) && BUILT_IN_TYPES.some((item) => item === type); 9 | } 10 | -------------------------------------------------------------------------------- /lib/utils/reverse-object-keys.util.ts: -------------------------------------------------------------------------------- 1 | export function reverseObjectKeys( 2 | originalObject: Record 3 | ): Record { 4 | const reversedObject = {}; 5 | const keys = Object.keys(originalObject).reverse(); 6 | for (const key of keys) { 7 | reversedObject[key] = originalObject[key]; 8 | } 9 | return reversedObject; 10 | } 11 | -------------------------------------------------------------------------------- /plugin.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | function __export(m) { 3 | for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; 4 | } 5 | exports.__esModule = true; 6 | const plugin = require('./dist/plugin'); 7 | __export(plugin); 8 | 9 | /** Compatibility with ts-patch/ttypescript */ 10 | exports.default = (program, options) => plugin.before(options, program); 11 | -------------------------------------------------------------------------------- /lib/interfaces/denormalized-doc.interface.ts: -------------------------------------------------------------------------------- 1 | import { 2 | OpenAPIObject, 3 | OperationObject, 4 | ResponsesObject 5 | } from './open-api-spec.interface'; 6 | 7 | export interface DenormalizedDoc extends Partial { 8 | root?: { 9 | method: string; 10 | path: string; 11 | } & OperationObject; 12 | responses?: ResponsesObject; 13 | } 14 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # source 2 | lib 3 | sample 4 | test 5 | e2e 6 | 7 | jest.config.json 8 | index.ts 9 | plugin.ts 10 | package-lock.json 11 | .eslintignore 12 | .eslintrc.js 13 | tsconfig.json 14 | .prettierrc 15 | .circleci 16 | .github 17 | 18 | # misc 19 | renovate.json 20 | npm-debug.log 21 | .DS_Store 22 | .commitlintrc.json 23 | .release-it.json 24 | CONTRIBUTING.md 25 | -------------------------------------------------------------------------------- /lib/interfaces/swagger-ui-init-options.interface.ts: -------------------------------------------------------------------------------- 1 | import { OpenAPIObject } from './open-api-spec.interface'; 2 | import { SwaggerUiOptions } from './swagger-ui-options.interface'; 3 | 4 | /** 5 | * @publicApi 6 | */ 7 | export interface SwaggerUIInitOptions { 8 | swaggerDoc: OpenAPIObject; 9 | customOptions: SwaggerUiOptions; 10 | swaggerUrl: string; 11 | } 12 | -------------------------------------------------------------------------------- /lib/utils/sort-object-lexicographically.ts: -------------------------------------------------------------------------------- 1 | export function sortObjectLexicographically(obj: { [key: string]: any }): { 2 | [key: string]: any; 3 | } { 4 | const sortedKeys = Object.keys(obj).sort(); 5 | 6 | const sortedObj: { [key: string]: any } = {}; 7 | for (const key of sortedKeys) { 8 | sortedObj[key] = obj[key]; 9 | } 10 | 11 | return sortedObj; 12 | } 13 | -------------------------------------------------------------------------------- /e2e/src/common/dto/validation-error.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '../../../../lib'; 2 | 3 | export class ValidationErrorDto { 4 | @ApiProperty({ 5 | type: 'number', 6 | description: 'HTTP status code' 7 | }) 8 | status: number; 9 | 10 | @ApiProperty({ 11 | type: 'string', 12 | description: 'Error description' 13 | }) 14 | description: string; 15 | } 16 | -------------------------------------------------------------------------------- /lib/utils/extend-metadata.util.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | 3 | export function extendMetadata[] = any[]>( 4 | metadata: T, 5 | metakey: string, 6 | target: object 7 | ) { 8 | const existingMetadata = Reflect.getMetadata(metakey, target); 9 | if (!existingMetadata) { 10 | return metadata; 11 | } 12 | return existingMetadata.concat(metadata); 13 | } 14 | -------------------------------------------------------------------------------- /test/type-helpers/type-helpers.test-utils.ts: -------------------------------------------------------------------------------- 1 | import { getFromContainer, MetadataStorage } from 'class-validator'; 2 | 3 | export function getValidationMetadataByTarget(target: Function) { 4 | const metadataStorage = getFromContainer(MetadataStorage); 5 | const targetMetadata = metadataStorage.getTargetValidationMetadatas( 6 | target, 7 | null 8 | ); 9 | return targetMetadata; 10 | } 11 | -------------------------------------------------------------------------------- /e2e/src/fastify.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | 3 | @Controller({ 4 | version: ['1', '2'] 5 | }) 6 | export class FastifyController { 7 | @Get('fastify::colon::another/:prop') 8 | withColons(): string { 9 | return 'Hello world!'; 10 | } 11 | 12 | @Get('/example/:file(^\\d+).png') 13 | withRegexp(): string { 14 | return 'Hello world!'; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/decorators/api-callbacks.decorator.ts: -------------------------------------------------------------------------------- 1 | import { DECORATORS } from '../constants'; 2 | import { createMixedDecorator } from './helpers'; 3 | import { CallBackObject } from '../interfaces/callback-object.interface'; 4 | 5 | /** 6 | * @publicApi 7 | */ 8 | export function ApiCallbacks(...callbackObject: Array>) { 9 | return createMixedDecorator(DECORATORS.API_CALLBACKS, callbackObject); 10 | } 11 | -------------------------------------------------------------------------------- /test/services/fixtures/create-profile.dto.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import { ApiProperty } from '../../../lib/decorators'; 3 | import { CreateUserDto } from './create-user.dto'; 4 | 5 | export class CreateProfileDto { 6 | @ApiProperty() 7 | firstname: string; 8 | 9 | @ApiProperty() 10 | lastname: string; 11 | 12 | @ApiProperty({ 13 | type: () => CreateUserDto, 14 | name: 'parent' 15 | }) 16 | parent: CreateUserDto; 17 | } 18 | -------------------------------------------------------------------------------- /e2e/src/cats/cats.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { Cat } from './classes/cat.class'; 3 | import { CreateCatDto } from './dto/create-cat.dto'; 4 | 5 | @Injectable() 6 | export class CatsService { 7 | private readonly cats: Cat[] = []; 8 | 9 | create(cat: CreateCatDto): Cat { 10 | this.cats.push(cat); 11 | return cat; 12 | } 13 | 14 | findOne(id: number): Cat { 15 | return this.cats[id]; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/plugin/fixtures/project/cats/cats.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { Cat } from './classes/cat.class'; 3 | import { CreateCatDto } from './dto/create-cat.dto'; 4 | 5 | @Injectable() 6 | export class CatsService { 7 | private readonly cats: Cat[] = []; 8 | 9 | create(cat: CreateCatDto): Cat { 10 | this.cats.push(cat); 11 | return cat; 12 | } 13 | 14 | findOne(id: number): Cat { 15 | return this.cats[id]; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/type-helpers/fixtures/serialized-metadata.fixture.ts: -------------------------------------------------------------------------------- 1 | export const SERIALIZED_METADATA = { 2 | '@nestjs/swagger': { 3 | models: [ 4 | [ 5 | import('./create-user-dto.fixture'), 6 | { 7 | CreateUserDto: { 8 | active: { 9 | type: () => Boolean 10 | }, 11 | role: { 12 | type: () => String 13 | } 14 | } 15 | } 16 | ] 17 | ] 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /test/plugin/fixtures/project/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "ES2021", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "skipLibCheck": true 15 | }, 16 | "include": ["./**/*"] 17 | } 18 | -------------------------------------------------------------------------------- /e2e/public/theme.css: -------------------------------------------------------------------------------- 1 | .swagger-ui .btn.authorize { 2 | line-height: 1; 3 | display: inline; 4 | color: #336E7B; 5 | border-color: #336E7B; 6 | } 7 | .swagger-ui .btn.authorize svg { 8 | fill: #ef0505; 9 | } 10 | 11 | 12 | .swagger-ui body { 13 | margin: 0; 14 | background: #fafafa 15 | } 16 | 17 | img[alt="Swagger UI"] { 18 | display: block; 19 | -moz-box-sizing: border-box; 20 | box-sizing: border-box; 21 | content: url('/public/logo.png'); 22 | max-width: 100%; 23 | max-height: 100%; 24 | } 25 | -------------------------------------------------------------------------------- /lib/services/mimetype-content-wrapper.ts: -------------------------------------------------------------------------------- 1 | import { ContentObject } from '../interfaces/open-api-spec.interface'; 2 | import { removeUndefinedKeys } from '../utils/remove-undefined-keys'; 3 | 4 | export class MimetypeContentWrapper { 5 | wrap( 6 | mimetype: string[], 7 | obj: Record 8 | ): Record<'content', ContentObject> { 9 | const content = mimetype.reduce( 10 | (acc, item) => ({ ...acc, [item]: removeUndefinedKeys(obj) }), 11 | {} 12 | ); 13 | return { content }; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/explorers/api-produces.explorer.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@nestjs/common'; 2 | import { DECORATORS } from '../constants'; 3 | 4 | export const exploreGlobalApiProducesMetadata = (metatype: Type) => { 5 | const produces = Reflect.getMetadata(DECORATORS.API_PRODUCES, metatype); 6 | return produces ? { produces } : undefined; 7 | }; 8 | 9 | export const exploreApiProducesMetadata = ( 10 | instance: object, 11 | prototype: Type, 12 | method: object 13 | ) => Reflect.getMetadata(DECORATORS.API_PRODUCES, method); 14 | -------------------------------------------------------------------------------- /lib/explorers/api-consumes.explorer.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@nestjs/common'; 2 | import { DECORATORS } from '../constants'; 3 | 4 | export const exploreGlobalApiConsumesMetadata = (metatype: Type) => { 5 | const consumes = Reflect.getMetadata(DECORATORS.API_CONSUMES, metatype); 6 | return consumes ? { consumes } : undefined; 7 | }; 8 | 9 | export const exploreApiConsumesMetadata = ( 10 | instance: object, 11 | prototype: Type, 12 | method: object 13 | ): string[] | undefined => Reflect.getMetadata(DECORATORS.API_CONSUMES, method); 14 | -------------------------------------------------------------------------------- /lib/explorers/api-security.explorer.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@nestjs/common'; 2 | import { DECORATORS } from '../constants'; 3 | 4 | export const exploreGlobalApiSecurityMetadata = (metatype: Type) => { 5 | const security = Reflect.getMetadata(DECORATORS.API_SECURITY, metatype); 6 | return security ? { security } : undefined; 7 | }; 8 | 9 | export const exploreApiSecurityMetadata = ( 10 | instance: object, 11 | prototype: Type, 12 | method: object 13 | ) => { 14 | return Reflect.getMetadata(DECORATORS.API_SECURITY, method); 15 | }; 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "noImplicitAny": false, 6 | "removeComments": true, 7 | "noLib": false, 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "target": "ES2021", 11 | "sourceMap": false, 12 | "outDir": "./dist", 13 | "rootDir": "./lib", 14 | "skipLibCheck": true 15 | }, 16 | "include": [ 17 | "lib/**/*", 18 | "test/**/*", 19 | "e2e/**/*" 20 | ], 21 | "exclude": [ 22 | "node_modules", 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "declaration": true, 6 | "noImplicitAny": false, 7 | "removeComments": true, 8 | "noLib": false, 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "target": "es6", 12 | "sourceMap": false, 13 | "outDir": "./dist", 14 | "rootDir": "./lib", 15 | "skipLibCheck": true 16 | }, 17 | "include": [ 18 | "lib/**/*" 19 | ], 20 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts", "e2e"] 21 | } 22 | -------------------------------------------------------------------------------- /.commitlintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@commitlint/config-angular"], 3 | "rules": { 4 | "subject-case": [ 5 | 2, 6 | "always", 7 | ["sentence-case", "start-case", "pascal-case", "upper-case", "lower-case"] 8 | ], 9 | "type-enum": [ 10 | 2, 11 | "always", 12 | [ 13 | "build", 14 | "chore", 15 | "ci", 16 | "docs", 17 | "feat", 18 | "fix", 19 | "perf", 20 | "refactor", 21 | "revert", 22 | "style", 23 | "test", 24 | "sample" 25 | ] 26 | ] 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/explorers/api-extra-models.explorer.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@nestjs/common'; 2 | import { DECORATORS } from '../constants'; 3 | 4 | export const exploreGlobalApiExtraModelsMetadata = ( 5 | metatype: Type 6 | ): Function[] => { 7 | const extraModels = Reflect.getMetadata( 8 | DECORATORS.API_EXTRA_MODELS, 9 | metatype 10 | ); 11 | return extraModels || []; 12 | }; 13 | 14 | export const exploreApiExtraModelsMetadata = ( 15 | instance: object, 16 | prototype: Type, 17 | method: object 18 | ): Function[] => Reflect.getMetadata(DECORATORS.API_EXTRA_MODELS, method) || []; 19 | -------------------------------------------------------------------------------- /test/extra/shim.spec.ts: -------------------------------------------------------------------------------- 1 | import * as Shim from '../../lib/extra/swagger-shim'; 2 | import * as Actual from '../../lib'; 3 | 4 | describe('Shim file', () => { 5 | it('contains all types export by package', () => { 6 | const exceptions = ['getSchemaPath', 'refs']; 7 | 8 | const shimExportNames = Object.keys(Shim); 9 | const packageExportNames = Object.keys(Actual); 10 | 11 | const exportsMissingInShim = packageExportNames.filter( 12 | (exportName) => 13 | shimExportNames.indexOf(exportName) === -1 && 14 | exceptions.indexOf(exportName) === -1 15 | ); 16 | 17 | expect(exportsMissingInShim).toEqual([]); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /lib/decorators/api-schema.decorator.ts: -------------------------------------------------------------------------------- 1 | import { DECORATORS } from '../constants'; 2 | import { SchemaObjectMetadata } from '../interfaces/schema-object-metadata.interface'; 3 | import { createClassDecorator } from './helpers'; 4 | 5 | export interface ApiSchemaOptions extends Pick { 6 | /** 7 | * Name of the schema. 8 | */ 9 | name?: string; 10 | 11 | /** 12 | * Description of the schema. 13 | */ 14 | description?: string; 15 | } 16 | 17 | 18 | /** 19 | * @publicApi 20 | */ 21 | export function ApiSchema(options?: ApiSchemaOptions): ClassDecorator { 22 | return createClassDecorator(DECORATORS.API_SCHEMA, [options]); 23 | } 24 | -------------------------------------------------------------------------------- /lib/storages/global-parameters.storage.ts: -------------------------------------------------------------------------------- 1 | import { ParameterObject } from '../interfaces/open-api-spec.interface'; 2 | 3 | export class GlobalParametersStorageHost { 4 | private parameters = new Array(); 5 | 6 | add(...parameters: ParameterObject[]) { 7 | this.parameters.push(...parameters); 8 | } 9 | 10 | getAll() { 11 | return this.parameters; 12 | } 13 | 14 | clear() { 15 | this.parameters = []; 16 | } 17 | } 18 | 19 | const globalRef = global as any; 20 | export const GlobalParametersStorage: GlobalParametersStorageHost = 21 | globalRef.SwaggerGlobalParametersStorage || 22 | (globalRef.SwaggerGlobalParametersStorage = 23 | new GlobalParametersStorageHost()); 24 | -------------------------------------------------------------------------------- /test/plugin/fixtures/project/cats/classes/cat.class.ts: -------------------------------------------------------------------------------- 1 | import { LettersEnum } from '../dto/pagination-query.dto'; 2 | 3 | export class Cat { 4 | name: string; 5 | 6 | /** 7 | * The age of the Cat 8 | * @example 4 9 | */ 10 | age: number; 11 | 12 | /** 13 | * The breed of the Cat 14 | */ 15 | breed: string; 16 | 17 | tags?: string[]; 18 | 19 | createdAt: Date; 20 | 21 | urls?: string[]; 22 | 23 | options?: Record[]; 24 | 25 | enum: LettersEnum; 26 | 27 | enumArr: LettersEnum; 28 | 29 | uppercaseString: Uppercase; 30 | 31 | lowercaseString: Lowercase; 32 | 33 | capitalizeString: Capitalize; 34 | 35 | uncapitalizeString: Uncapitalize; 36 | } 37 | -------------------------------------------------------------------------------- /lib/utils/assign-two-levels-deep.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Merge one level deeper than a regular Object.assign(). 3 | * 4 | * @example 5 | * 6 | * ``` 7 | * const a = {foo: {bar: 1, baz: 2, bag: {x : 1}}}; 8 | * const b = {foo: {baz: 3, bag: {y: 2}}}; 9 | * 10 | * assignTwoLevelsDeep(a, b); 11 | * 12 | * // a is {foo: {bar: 1, baz: 3, bag: {y: 2}}} 13 | * ``` 14 | */ 15 | export function assignTwoLevelsDeep(_dest: TObject, ...args: T[]) { 16 | const dest = _dest as TObject & T; 17 | 18 | for (const arg of args) { 19 | for (const [key, value] of Object.entries(arg ?? {}) as Array< 20 | [keyof T, T[keyof T]] 21 | >) { 22 | dest[key] = { ...dest[key], ...value }; 23 | } 24 | } 25 | 26 | return dest; 27 | } 28 | -------------------------------------------------------------------------------- /test/plugin/fixtures/string-literal.dto.ts: -------------------------------------------------------------------------------- 1 | export const stringLiteralDtoText = ` 2 | export class StringLiteralDto { 3 | @ApiProperty() 4 | valueOne: "one"; 5 | @ApiProperty() 6 | valueTwo: "one" | "two"; 7 | } 8 | `; 9 | 10 | export const stringLiteralDtoTextTranspiled = `import * as openapi from "@nestjs/swagger"; 11 | export class StringLiteralDto { 12 | static _OPENAPI_METADATA_FACTORY() { 13 | return { valueOne: { required: true, type: () => String }, valueTwo: { required: true, type: () => Object } }; 14 | } 15 | } 16 | __decorate([ 17 | ApiProperty() 18 | ], StringLiteralDto.prototype, "valueOne", void 0); 19 | __decorate([ 20 | ApiProperty() 21 | ], StringLiteralDto.prototype, "valueTwo", void 0); 22 | `; 23 | -------------------------------------------------------------------------------- /lib/interfaces/swagger-ui-options.interface.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @see https://swagger.io/docs/open-source-tools/swagger-ui/usage/configuration/ 3 | * 4 | * @publicApi 5 | */ 6 | export interface SwaggerUiOptions { 7 | /** 8 | * @see https://swagger.io/docs/open-source-tools/swagger-ui/usage/oauth2/ 9 | **/ 10 | initOAuth?: { 11 | clientId?: string; 12 | clientSecret?: string; 13 | realm?: string; 14 | appName?: string; 15 | scopeSeparator?: string; 16 | scopes?: string[]; 17 | additionalQueryStringParams?: Record; 18 | useBasicAuthenticationWithAccessCodeGrant?: boolean; 19 | usePkceWithAuthorizationCodeGrant?: boolean; 20 | }; 21 | persistAuthorization?: boolean; 22 | [key: string]: any; 23 | } 24 | -------------------------------------------------------------------------------- /lib/storages/global-responses.storage.ts: -------------------------------------------------------------------------------- 1 | import { ApiResponseOptions } from '../decorators'; 2 | 3 | type GlobalResponesMap = Record>; 4 | 5 | export class GlobalResponsesStorageHost { 6 | private responses: GlobalResponesMap = {}; 7 | 8 | add(responses: GlobalResponesMap) { 9 | this.responses = { 10 | ...this.responses, 11 | ...responses 12 | }; 13 | } 14 | 15 | getAll() { 16 | return this.responses; 17 | } 18 | 19 | clear() { 20 | this.responses = {}; 21 | } 22 | } 23 | 24 | const globalRef = global as any; 25 | export const GlobalResponsesStorage: GlobalResponsesStorageHost = 26 | globalRef.SwaggerGlobalResponsesStorage || 27 | (globalRef.SwaggerGlobalResponsesStorage = new GlobalResponsesStorageHost()); 28 | -------------------------------------------------------------------------------- /lib/swagger-ui/helpers.ts: -------------------------------------------------------------------------------- 1 | import { SwaggerUIInitOptions } from '../interfaces/swagger-ui-init-options.interface'; 2 | 3 | /** 4 | * Transforms options JS object into a string that can be inserted as 'variable' into JS file 5 | */ 6 | export function buildJSInitOptions(initOptions: SwaggerUIInitOptions) { 7 | const functionPlaceholder = '____FUNCTION_PLACEHOLDER____'; 8 | const fns = []; 9 | let json = JSON.stringify( 10 | initOptions, 11 | (key, value) => { 12 | if (typeof value === 'function') { 13 | fns.push(value); 14 | return functionPlaceholder; 15 | } 16 | return value; 17 | }, 18 | 2 19 | ); 20 | 21 | json = json.replace(new RegExp('"' + functionPlaceholder + '"', 'g'), () => 22 | fns.shift() 23 | ); 24 | 25 | return `let options = ${json};`; 26 | } 27 | -------------------------------------------------------------------------------- /test/plugin/fixtures/changed-class.dto.ts: -------------------------------------------------------------------------------- 1 | export const originalCatDtoText = ` 2 | export class ChangedCatDto { 3 | name: string; 4 | status: string; 5 | } 6 | `; 7 | 8 | export const changedCatDtoText = ` 9 | export class ChangedCatDto { 10 | name: string; 11 | } 12 | `; 13 | 14 | export const changedCatDtoTextTranspiled = `\"use strict\"; 15 | Object.defineProperty(exports, \"__esModule\", { value: true }); 16 | exports.ChangedCatDto = void 0; 17 | var openapi = require(\"@nestjs/swagger\"); 18 | var ChangedCatDto = /** @class */ (function () { 19 | function ChangedCatDto() { 20 | } 21 | ChangedCatDto._OPENAPI_METADATA_FACTORY = function () { 22 | return { name: { required: true, type: function () { return String; } } }; 23 | }; 24 | return ChangedCatDto; 25 | }()); 26 | exports.ChangedCatDto = ChangedCatDto; 27 | `; 28 | -------------------------------------------------------------------------------- /test/plugin/fixtures/project/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | import { ApiOperation } from '../../../../lib/decorators'; 3 | 4 | @Controller() 5 | export class AppController { 6 | /** 7 | * Says hello 8 | * @deprecated 9 | */ 10 | @Get() 11 | getHello(): string { 12 | return 'Hello world!'; 13 | } 14 | 15 | @Get(['alias1', 'alias2']) 16 | withAliases(): string { 17 | return 'Hello world!'; 18 | } 19 | 20 | @Get('express[:]colon[:]another/:prop') 21 | withColonExpress(): string { 22 | return 'Hello world!'; 23 | } 24 | 25 | /** 26 | * Returns information about the application 27 | */ 28 | @ApiOperation({ summary: 'Returns Hello World' }) 29 | @Get('fastify::colon::another/:prop') 30 | withColonFastify(): string { 31 | return 'Hello world!'; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /test/type-helpers/fixtures/create-user-dto.fixture.ts: -------------------------------------------------------------------------------- 1 | import { Expose, Transform } from 'class-transformer'; 2 | import { IsEnum, IsString } from 'class-validator'; 3 | import { ApiProperty } from '../../../lib/decorators'; 4 | import { METADATA_FACTORY_NAME } from '../../../lib/plugin/plugin-constants'; 5 | 6 | export class CreateUserDto { 7 | @IsString() 8 | firstName: string; 9 | 10 | @IsString() 11 | lastName: string; 12 | 13 | @IsEnum(['admin', 'user']) 14 | role: string; 15 | 16 | @ApiProperty({ required: true }) 17 | login: string; 18 | 19 | @Expose() 20 | @Transform((str) => str + '_transformed') 21 | @IsString() 22 | @ApiProperty({ minLength: 10 }) 23 | password: string; 24 | 25 | static [METADATA_FACTORY_NAME]() { 26 | return { 27 | firstName: { required: true, type: String }, 28 | lastName: { required: true, type: String } 29 | }; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lib/decorators/api-operation.decorator.ts: -------------------------------------------------------------------------------- 1 | import { isUndefined, negate, pickBy } from 'lodash'; 2 | import { DECORATORS } from '../constants'; 3 | import { OperationObject } from '../interfaces/open-api-spec.interface'; 4 | import { createMethodDecorator } from './helpers'; 5 | 6 | export type ApiOperationOptions = Partial; 7 | 8 | const defaultOperationOptions: ApiOperationOptions = { 9 | summary: '' 10 | }; 11 | 12 | /** 13 | * @publicApi 14 | */ 15 | export function ApiOperation( 16 | options: ApiOperationOptions, 17 | { overrideExisting } = { overrideExisting: true } 18 | ): MethodDecorator { 19 | return createMethodDecorator( 20 | DECORATORS.API_OPERATION, 21 | pickBy( 22 | { 23 | ...defaultOperationOptions, 24 | ...options 25 | } as ApiOperationOptions, 26 | negate(isUndefined) 27 | ), 28 | { overrideExisting } 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /test/services/model-properties-accessor.spec.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@nestjs/common'; 2 | import { ApiProperty } from '../../lib/decorators'; 3 | import { ModelPropertiesAccessor } from '../../lib/services/model-properties-accessor'; 4 | 5 | describe('ModelPropertiesAccessor', () => { 6 | class CreateUserDto { 7 | @ApiProperty() 8 | login: string; 9 | 10 | @ApiProperty() 11 | password: string; 12 | } 13 | 14 | let modelPropertiesAccessor: ModelPropertiesAccessor; 15 | 16 | beforeEach(() => { 17 | modelPropertiesAccessor = new ModelPropertiesAccessor(); 18 | }); 19 | 20 | describe('getModelProperties', () => { 21 | it('should return all decorated properties', () => { 22 | expect( 23 | modelPropertiesAccessor.getModelProperties( 24 | (CreateUserDto.prototype as any) as Type 25 | ) 26 | ).toEqual(['login', 'password']); 27 | }); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /test/plugin/merge-option.spec.ts: -------------------------------------------------------------------------------- 1 | import { mergePluginOptions } from '../../lib/plugin/merge-options'; 2 | import { 3 | createCliPluginMultiOption, 4 | createCliPluginSingleOption, 5 | mergedCliPluginMultiOption, 6 | mergedCliPluginSingleOption 7 | } from './fixtures/create-option'; 8 | 9 | describe('CLI Plugin options', () => { 10 | it('should skip element when dtoFileNameSuffix key has more than one element and include ".ts"', () => { 11 | const merged = mergePluginOptions(createCliPluginMultiOption); 12 | expect(JSON.stringify(merged)).toEqual( 13 | JSON.stringify(mergedCliPluginMultiOption) 14 | ); 15 | }); 16 | 17 | it('should delete key when dtoFileNameSuffix key has 1 element and element is “.ts”', () => { 18 | const merged = mergePluginOptions(createCliPluginSingleOption); 19 | expect(JSON.stringify(merged)).toEqual( 20 | JSON.stringify(mergedCliPluginSingleOption) 21 | ); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /lib/decorators/api-extra-models.decorator.ts: -------------------------------------------------------------------------------- 1 | import { DECORATORS } from '../constants'; 2 | 3 | /** 4 | * @publicApi 5 | */ 6 | export function ApiExtraModels(...models: Function[]) { 7 | return ( 8 | target: object, 9 | key?: string | symbol, 10 | descriptor?: TypedPropertyDescriptor 11 | ): any => { 12 | if (descriptor) { 13 | const extraModels = 14 | Reflect.getMetadata(DECORATORS.API_EXTRA_MODELS, descriptor.value) || 15 | []; 16 | Reflect.defineMetadata( 17 | DECORATORS.API_EXTRA_MODELS, 18 | [...extraModels, ...models], 19 | descriptor.value 20 | ); 21 | return descriptor; 22 | } 23 | 24 | const extraModels = 25 | Reflect.getMetadata(DECORATORS.API_EXTRA_MODELS, target) || []; 26 | Reflect.defineMetadata( 27 | DECORATORS.API_EXTRA_MODELS, 28 | [...extraModels, ...models], 29 | target 30 | ); 31 | return target; 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /lib/explorers/api-use-tags.explorer.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@nestjs/common'; 2 | import { DECORATORS } from '../constants'; 3 | 4 | export const exploreGlobalApiTagsMetadata = 5 | (autoTagControllers?: boolean) => (metatype: Type) => { 6 | const decoratorTags = Reflect.getMetadata(DECORATORS.API_TAGS, metatype); 7 | const isEmpty = !decoratorTags || decoratorTags.length === 0; 8 | if (isEmpty && autoTagControllers) { 9 | // When there are no tags defined in the controller 10 | // use the controller name without the suffix `Controller` 11 | // as the default tag 12 | 13 | const defaultTag = metatype.name.replace(/Controller$/, ''); 14 | return { 15 | tags: [defaultTag] 16 | }; 17 | } 18 | return isEmpty ? undefined : { tags: decoratorTags }; 19 | }; 20 | 21 | export const exploreApiTagsMetadata = ( 22 | instance: object, 23 | prototype: Type, 24 | method: object 25 | ) => Reflect.getMetadata(DECORATORS.API_TAGS, method); 26 | -------------------------------------------------------------------------------- /lib/utils/get-schema-path.util.ts: -------------------------------------------------------------------------------- 1 | import { isString } from '@nestjs/common/utils/shared.utils'; 2 | import { DECORATORS } from '../constants'; 3 | import { ApiSchemaOptions } from '../decorators/api-schema.decorator'; 4 | 5 | export function getSchemaPath(model: string | Function): string { 6 | const modelName = isString(model) ? model : getSchemaNameByClass(model); 7 | return `#/components/schemas/${modelName}`; 8 | } 9 | 10 | function getSchemaNameByClass(target: Function): string { 11 | if (!target || typeof target !== 'function') { 12 | return ''; 13 | } 14 | 15 | const customSchema: ApiSchemaOptions[] = Reflect.getOwnMetadata( 16 | DECORATORS.API_SCHEMA, 17 | target 18 | ); 19 | 20 | if (!customSchema || customSchema.length === 0) { 21 | return target.name; 22 | } 23 | 24 | return customSchema[customSchema.length - 1].name ?? target.name; 25 | } 26 | 27 | export function refs(...models: Function[]) { 28 | return models.map((item) => ({ 29 | $ref: getSchemaPath(item.name) 30 | })); 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-2022 Kamil Mysliwiec 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/swagger-transformer.ts: -------------------------------------------------------------------------------- 1 | import { filter, groupBy, keyBy, mapValues, omit } from 'lodash'; 2 | import { OpenAPIObject } from './interfaces'; 3 | import { sortObjectLexicographically } from './utils/sort-object-lexicographically'; 4 | 5 | export class SwaggerTransformer { 6 | public normalizePaths( 7 | denormalizedDoc: (Partial & Record<'root', any>)[] 8 | ): Record<'paths', OpenAPIObject['paths']> { 9 | const roots = filter(denormalizedDoc, (r) => r.root); 10 | const groupedByPath = groupBy( 11 | roots, 12 | ({ root }: Record<'root', any>) => root.path 13 | ); 14 | const paths = mapValues(groupedByPath, (routes) => { 15 | const keyByMethod = keyBy( 16 | routes, 17 | ({ root }: Record<'root', any>) => root.method 18 | ); 19 | return mapValues(keyByMethod, (route: any) => { 20 | const mergedDefinition = { 21 | ...omit(route, 'root'), 22 | ...omit(route.root, ['method', 'path']) 23 | }; 24 | return sortObjectLexicographically(mergedDefinition); 25 | }); 26 | }); 27 | return { 28 | paths 29 | }; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test/plugin/fixtures/create-option.ts: -------------------------------------------------------------------------------- 1 | export const createCliPluginMultiOption = { 2 | dtoFileNameSuffix: ['.ts', '.dto.ts'], 3 | introspectComments: true 4 | }; 5 | 6 | export const createCliPluginSingleOption = { 7 | dtoFileNameSuffix: ['.ts'], 8 | introspectComments: true 9 | }; 10 | 11 | export const mergedCliPluginMultiOption = { 12 | dtoFileNameSuffix: ['.dto.ts'], 13 | controllerFileNameSuffix: ['.controller.ts'], 14 | classValidatorShim: true, 15 | classTransformerShim: false, 16 | dtoKeyOfComment: 'description', 17 | controllerKeyOfComment: 'summary', 18 | introspectComments: true, 19 | esmCompatible: false, 20 | readonly: false, 21 | debug: false, 22 | skipDefaultValues: false 23 | }; 24 | 25 | export const mergedCliPluginSingleOption = { 26 | dtoFileNameSuffix: ['.dto.ts', '.entity.ts'], 27 | controllerFileNameSuffix: ['.controller.ts'], 28 | classValidatorShim: true, 29 | classTransformerShim: false, 30 | dtoKeyOfComment: 'description', 31 | controllerKeyOfComment: 'summary', 32 | introspectComments: true, 33 | esmCompatible: false, 34 | readonly: false, 35 | debug: false, 36 | skipDefaultValues: false 37 | }; 38 | -------------------------------------------------------------------------------- /lib/constants.ts: -------------------------------------------------------------------------------- 1 | export const DECORATORS_PREFIX = 'swagger'; 2 | export const DECORATORS = { 3 | API_OPERATION: `${DECORATORS_PREFIX}/apiOperation`, 4 | API_RESPONSE: `${DECORATORS_PREFIX}/apiResponse`, 5 | API_PRODUCES: `${DECORATORS_PREFIX}/apiProduces`, 6 | API_CONSUMES: `${DECORATORS_PREFIX}/apiConsumes`, 7 | API_TAGS: `${DECORATORS_PREFIX}/apiUseTags`, 8 | API_CALLBACKS: `${DECORATORS_PREFIX}/apiCallbacks`, 9 | API_PARAMETERS: `${DECORATORS_PREFIX}/apiParameters`, 10 | API_HEADERS: `${DECORATORS_PREFIX}/apiHeaders`, 11 | API_MODEL_PROPERTIES: `${DECORATORS_PREFIX}/apiModelProperties`, 12 | API_MODEL_PROPERTIES_ARRAY: `${DECORATORS_PREFIX}/apiModelPropertiesArray`, 13 | API_SECURITY: `${DECORATORS_PREFIX}/apiSecurity`, 14 | API_EXCLUDE_ENDPOINT: `${DECORATORS_PREFIX}/apiExcludeEndpoint`, 15 | API_EXCLUDE_CONTROLLER: `${DECORATORS_PREFIX}/apiExcludeController`, 16 | API_EXTRA_MODELS: `${DECORATORS_PREFIX}/apiExtraModels`, 17 | API_EXTENSION: `${DECORATORS_PREFIX}/apiExtension`, 18 | API_SCHEMA: `${DECORATORS_PREFIX}/apiSchema`, 19 | API_DEFAULT_GETTER: `${DECORATORS_PREFIX}/apiDefaultGetter`, 20 | API_LINK: `${DECORATORS_PREFIX}/apiLink` 21 | }; 22 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /lib/decorators/index.ts: -------------------------------------------------------------------------------- 1 | export * from './api-basic.decorator'; 2 | export * from './api-bearer.decorator'; 3 | export * from './api-body.decorator'; 4 | export * from './api-consumes.decorator'; 5 | export * from './api-cookie.decorator'; 6 | export * from './api-default-getter.decorator'; 7 | export * from './api-exclude-endpoint.decorator'; 8 | export * from './api-exclude-controller.decorator'; 9 | export * from './api-extra-models.decorator'; 10 | export * from './api-header.decorator'; 11 | export * from './api-hide-property.decorator'; 12 | export * from './api-link.decorator'; 13 | export * from './api-oauth2.decorator'; 14 | export * from './api-operation.decorator'; 15 | export * from './api-param.decorator'; 16 | export * from './api-produces.decorator'; 17 | export { 18 | ApiProperty, 19 | ApiPropertyOptional, 20 | ApiPropertyOptions, 21 | ApiResponseProperty 22 | } from './api-property.decorator'; 23 | export * from './api-query.decorator'; 24 | export * from './api-response.decorator'; 25 | export * from './api-security.decorator'; 26 | export * from './api-use-tags.decorator'; 27 | export * from './api-callbacks.decorator'; 28 | export * from './api-extension.decorator'; 29 | export * from './api-schema.decorator'; 30 | -------------------------------------------------------------------------------- /.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 | - [ ] Bugfix 14 | - [ ] Feature 15 | - [ ] Code style update (formatting, local variables) 16 | - [ ] Refactoring (no functional changes, no api changes) 17 | - [ ] Build related changes 18 | - [ ] CI related changes 19 | - [ ] Other... Please describe: 20 | 21 | ## What is the current behavior? 22 | 23 | 24 | Issue Number: N/A 25 | 26 | 27 | ## What is the new behavior? 28 | 29 | 30 | ## Does this PR introduce a breaking change? 31 | - [ ] Yes 32 | - [ ] No 33 | 34 | 35 | 36 | 37 | ## Other information 38 | -------------------------------------------------------------------------------- /lib/plugin/metadata-loader.ts: -------------------------------------------------------------------------------- 1 | import { METADATA_FACTORY_NAME } from './plugin-constants'; 2 | 3 | export class MetadataLoader { 4 | private static readonly refreshHooks = new Array<() => void>(); 5 | 6 | static addRefreshHook(hook: () => void) { 7 | return MetadataLoader.refreshHooks.push(hook); 8 | } 9 | 10 | async load(metadata: Record) { 11 | const pkgMetadata = metadata['@nestjs/swagger']; 12 | if (!pkgMetadata) { 13 | return; 14 | } 15 | const { models, controllers } = pkgMetadata; 16 | if (models) { 17 | await this.applyMetadata(models); 18 | } 19 | if (controllers) { 20 | await this.applyMetadata(controllers); 21 | } 22 | this.runHooks(); 23 | } 24 | 25 | private async applyMetadata( 26 | meta: Array<[Promise, Record]> 27 | ) { 28 | const loadPromises = meta.map(async ([fileImport, fileMeta]) => { 29 | const fileRef = await fileImport; 30 | Object.keys(fileMeta).map((key) => { 31 | const clsRef = fileRef[key]; 32 | clsRef[METADATA_FACTORY_NAME] = () => fileMeta[key]; 33 | }); 34 | }); 35 | await Promise.all(loadPromises); 36 | } 37 | 38 | private runHooks() { 39 | MetadataLoader.refreshHooks.forEach((hook) => hook()); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/decorators/api-security.decorator.ts: -------------------------------------------------------------------------------- 1 | import { isString } from 'lodash'; 2 | import { DECORATORS } from '../constants'; 3 | import { SecurityRequirementObject } from '../interfaces/open-api-spec.interface'; 4 | import { extendMetadata } from '../utils/extend-metadata.util'; 5 | 6 | /** 7 | * @publicApi 8 | */ 9 | export function ApiSecurity( 10 | name: string | SecurityRequirementObject, 11 | requirements: string[] = [] 12 | ): ClassDecorator & MethodDecorator { 13 | let metadata: SecurityRequirementObject[]; 14 | 15 | if (isString(name)) { 16 | metadata = [{ [name]: requirements }]; 17 | } else { 18 | metadata = [name]; 19 | } 20 | 21 | return ( 22 | target: object, 23 | key?: string | symbol, 24 | descriptor?: TypedPropertyDescriptor 25 | ): any => { 26 | if (descriptor) { 27 | metadata = extendMetadata( 28 | metadata, 29 | DECORATORS.API_SECURITY, 30 | descriptor.value 31 | ); 32 | Reflect.defineMetadata( 33 | DECORATORS.API_SECURITY, 34 | metadata, 35 | descriptor.value 36 | ); 37 | return descriptor; 38 | } 39 | metadata = extendMetadata(metadata, DECORATORS.API_SECURITY, target); 40 | Reflect.defineMetadata(DECORATORS.API_SECURITY, metadata, target); 41 | return target; 42 | }; 43 | } 44 | -------------------------------------------------------------------------------- /test/type-helpers/omit-type.helper.spec.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@nestjs/common'; 2 | import { MetadataLoader } from '../../lib/plugin/metadata-loader'; 3 | import { ModelPropertiesAccessor } from '../../lib/services/model-properties-accessor'; 4 | import { OmitType } from '../../lib/type-helpers'; 5 | import { CreateUserDto } from './fixtures/create-user-dto.fixture'; 6 | import { SERIALIZED_METADATA } from './fixtures/serialized-metadata.fixture'; 7 | 8 | class UpdateUserDto extends OmitType(CreateUserDto, ['login', 'lastName']) {} 9 | 10 | describe('OmitType', () => { 11 | const metadataLoader = new MetadataLoader(); 12 | 13 | let modelPropertiesAccessor: ModelPropertiesAccessor; 14 | 15 | beforeEach(() => { 16 | modelPropertiesAccessor = new ModelPropertiesAccessor(); 17 | }); 18 | 19 | describe('OpenAPI metadata', () => { 20 | it('should omit "login" property', async () => { 21 | await metadataLoader.load(SERIALIZED_METADATA); 22 | 23 | const prototype = UpdateUserDto.prototype as any as Type; 24 | 25 | modelPropertiesAccessor.applyMetadataFactory(prototype); 26 | expect(modelPropertiesAccessor.getModelProperties(prototype)).toEqual([ 27 | 'password', 28 | 'firstName', 29 | 'active', 30 | 'role' 31 | ]); 32 | }); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /lib/type-helpers/mapped-types.utils.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@nestjs/common'; 2 | import { identity } from 'lodash'; 3 | import { METADATA_FACTORY_NAME } from '../plugin/plugin-constants'; 4 | 5 | export function clonePluginMetadataFactory( 6 | target: Type, 7 | parent: Type, 8 | transformFn: (metadata: Record) => Record = identity 9 | ) { 10 | let targetMetadata = {}; 11 | 12 | do { 13 | if (!parent.constructor) { 14 | return; 15 | } 16 | if (!parent.constructor[METADATA_FACTORY_NAME]) { 17 | continue; 18 | } 19 | const parentMetadata = parent.constructor[METADATA_FACTORY_NAME](); 20 | targetMetadata = { 21 | ...parentMetadata, 22 | ...targetMetadata 23 | }; 24 | } while ( 25 | (parent = Reflect.getPrototypeOf(parent) as Type) && 26 | parent !== Object.prototype && 27 | parent 28 | ); 29 | targetMetadata = transformFn(targetMetadata); 30 | 31 | if (target[METADATA_FACTORY_NAME]) { 32 | const originalFactory = target[METADATA_FACTORY_NAME]; 33 | target[METADATA_FACTORY_NAME] = () => { 34 | const originalMetadata = originalFactory(); 35 | return { 36 | ...originalMetadata, 37 | ...targetMetadata 38 | }; 39 | }; 40 | } else { 41 | target[METADATA_FACTORY_NAME] = () => targetMetadata; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/plugin/compiler-plugin.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | import { mergePluginOptions } from './merge-options'; 3 | import { pluginDebugLogger } from './plugin-debug-logger'; 4 | import { isFilenameMatched } from './utils/is-filename-matched.util'; 5 | import { ControllerClassVisitor } from './visitors/controller-class.visitor'; 6 | import { ModelClassVisitor } from './visitors/model-class.visitor'; 7 | 8 | const modelClassVisitor = new ModelClassVisitor(); 9 | const controllerClassVisitor = new ControllerClassVisitor(); 10 | 11 | export const before = (options?: Record, program?: ts.Program) => { 12 | options = mergePluginOptions(options); 13 | 14 | if (!program) { 15 | const error = `The "program" reference must be provided when using the CLI Plugin. This error is likely caused by the "isolatedModules" compiler option being set to "true".`; 16 | pluginDebugLogger.debug(error); 17 | throw new Error(error); 18 | } 19 | 20 | return (ctx: ts.TransformationContext): ts.Transformer => { 21 | return (sf: ts.SourceFile) => { 22 | if (isFilenameMatched(options.dtoFileNameSuffix, sf.fileName)) { 23 | return modelClassVisitor.visit(sf, ctx, program, options); 24 | } 25 | if (isFilenameMatched(options.controllerFileNameSuffix, sf.fileName)) { 26 | return controllerClassVisitor.visit(sf, ctx, program, options); 27 | } 28 | return sf; 29 | }; 30 | }; 31 | }; 32 | -------------------------------------------------------------------------------- /lib/decorators/api-default-getter.decorator.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@nestjs/common'; 2 | import { DECORATORS } from '../constants'; 3 | 4 | /** 5 | * Set the default getter for the given type to the decorated method 6 | * 7 | * This is to be used in conjunction with `ApiProperty({link: () => Type})` to generate link objects 8 | * in the swagger schema 9 | * 10 | * ```typescript 11 | * @Controller('users') 12 | * class UserController { 13 | * @Get(':userId') 14 | * @ApiDefaultGetter(UserGet, 'userId') 15 | * getUser(@Param('userId') userId: string) { 16 | * // ... 17 | * } 18 | * } 19 | * ``` 20 | * 21 | * @param type The type for which the decorated function is the default getter 22 | * @param parameter Name of the parameter in the route of the getter which corresponds to the id of the type 23 | * 24 | * @see [Swagger link objects](https://swagger.io/docs/specification/links/) 25 | * 26 | * @publicApi 27 | */ 28 | export function ApiDefaultGetter( 29 | type: Type | Function, 30 | parameter: string 31 | ): MethodDecorator { 32 | return ( 33 | prototype: object, 34 | key: string | symbol, 35 | descriptor: PropertyDescriptor 36 | ) => { 37 | if (type.prototype) { 38 | Reflect.defineMetadata( 39 | DECORATORS.API_DEFAULT_GETTER, 40 | { getter: descriptor.value, parameter, prototype }, 41 | type.prototype 42 | ); 43 | } 44 | 45 | return descriptor; 46 | }; 47 | } 48 | -------------------------------------------------------------------------------- /test/plugin/fixtures/project/cats/cats.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Get, Param, Post, Query } from '@nestjs/common'; 2 | import { ApiOperation } from '../../../../lib'; 3 | import { CatsService } from './cats.service'; 4 | import { Cat } from './classes/cat.class'; 5 | import { CreateCatDto } from './dto/create-cat.dto'; 6 | import { LettersEnum, PaginationQuery } from './dto/pagination-query.dto'; 7 | 8 | @Controller('cats') 9 | export class CatsController { 10 | constructor(private readonly catsService: CatsService) {} 11 | 12 | @Post() 13 | @ApiOperation({ summary: 'Create cat' }) 14 | async create(@Body() createCatDto: CreateCatDto): Promise { 15 | return this.catsService.create(createCatDto); 16 | } 17 | 18 | @Get(':id') 19 | findOne(@Param('id') id: string): Cat { 20 | return this.catsService.findOne(+id); 21 | } 22 | 23 | @Get() 24 | findAll(@Query() paginationQuery: PaginationQuery) {} 25 | 26 | @Post('bulk') 27 | async createBulk(@Body() createCatDto: CreateCatDto[]): Promise { 28 | return null; 29 | } 30 | 31 | @Post('as-form-data') 32 | @ApiOperation({ summary: 'Create cat' }) 33 | async createAsFormData(@Body() createCatDto: CreateCatDto): Promise { 34 | return this.catsService.create(createCatDto); 35 | } 36 | 37 | @Get('with-enum/:type') 38 | getWithEnumParam(@Param('type') type: LettersEnum) {} 39 | 40 | @Get('with-random-query') 41 | getWithRandomQuery(@Query('type') type: string) {} 42 | } 43 | -------------------------------------------------------------------------------- /lib/services/model-properties-accessor.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@nestjs/common'; 2 | import { isFunction, isString } from '@nestjs/common/utils/shared.utils'; 3 | import 'reflect-metadata'; 4 | import { DECORATORS } from '../constants'; 5 | import { createApiPropertyDecorator } from '../decorators/api-property.decorator'; 6 | import { METADATA_FACTORY_NAME } from '../plugin/plugin-constants'; 7 | 8 | export class ModelPropertiesAccessor { 9 | getModelProperties(prototype: Type): string[] { 10 | const properties = 11 | Reflect.getMetadata(DECORATORS.API_MODEL_PROPERTIES_ARRAY, prototype) || 12 | []; 13 | 14 | return properties 15 | .filter(isString) 16 | .filter( 17 | (key: string) => key.charAt(0) === ':' && !isFunction(prototype[key]) 18 | ) 19 | .map((key: string) => key.slice(1)); 20 | } 21 | 22 | applyMetadataFactory(prototype: Type) { 23 | const classPrototype = prototype; 24 | do { 25 | if (!prototype.constructor) { 26 | return; 27 | } 28 | if (!prototype.constructor[METADATA_FACTORY_NAME]) { 29 | continue; 30 | } 31 | const metadata = prototype.constructor[METADATA_FACTORY_NAME](); 32 | const properties = Object.keys(metadata); 33 | properties.forEach((key) => { 34 | createApiPropertyDecorator(metadata[key], false)(classPrototype, key); 35 | }); 36 | } while ( 37 | (prototype = Reflect.getPrototypeOf(prototype) as Type) && 38 | prototype !== Object.prototype && 39 | prototype 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/interfaces/schema-object-metadata.interface.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@nestjs/common'; 2 | import { EnumSchemaAttributes } from './enum-schema-attributes.interface'; 3 | import { ReferenceObject, SchemaObject } from './open-api-spec.interface'; 4 | 5 | export type EnumAllowedTypes = 6 | | any[] 7 | | Record 8 | | (() => any[] | Record); 9 | 10 | interface SchemaObjectCommonMetadata 11 | extends Omit { 12 | isArray?: boolean; 13 | name?: string; 14 | enum?: EnumAllowedTypes; 15 | } 16 | 17 | export type SchemaObjectMetadata = 18 | | (SchemaObjectCommonMetadata & { 19 | type?: 20 | | Type 21 | | Function 22 | | [Function] 23 | | 'array' 24 | | 'string' 25 | | 'number' 26 | | 'boolean' 27 | | 'integer' 28 | | 'null'; 29 | required?: boolean; 30 | }) 31 | | ({ 32 | type?: Type | Function | [Function] | Record; 33 | required?: boolean; 34 | enumName: string; 35 | enumSchema?: EnumSchemaAttributes; 36 | } & SchemaObjectCommonMetadata) 37 | | ({ 38 | type: 'object'; 39 | properties: Record; 40 | required?: string[]; 41 | selfRequired?: boolean; 42 | } & SchemaObjectCommonMetadata) 43 | | ({ 44 | type: 'object'; 45 | properties?: Record; 46 | additionalProperties: SchemaObject | ReferenceObject | boolean; 47 | required?: string[]; 48 | selfRequired?: boolean; 49 | } & SchemaObjectCommonMetadata); 50 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import eslint from '@eslint/js'; 3 | import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'; 4 | import globals from 'globals'; 5 | import tseslint from 'typescript-eslint'; 6 | 7 | export default tseslint.config( 8 | { 9 | ignores: ['tests/**'], 10 | }, 11 | eslint.configs.recommended, 12 | ...tseslint.configs.recommendedTypeChecked, 13 | eslintPluginPrettierRecommended, 14 | { 15 | languageOptions: { 16 | globals: { 17 | ...globals.node, 18 | ...globals.jest, 19 | }, 20 | ecmaVersion: 5, 21 | sourceType: 'module', 22 | parserOptions: { 23 | projectService: true, 24 | tsconfigRootDir: import.meta.dirname, 25 | }, 26 | }, 27 | }, 28 | { 29 | rules: { 30 | '@typescript-eslint/no-explicit-any': 'off', 31 | '@typescript-eslint/no-unsafe-assignment': 'off', 32 | '@typescript-eslint/no-unsafe-call': 'off', 33 | '@typescript-eslint/no-unsafe-member-access': 'off', 34 | '@typescript-eslint/no-unsafe-function-type': 'off', 35 | '@typescript-eslint/no-unsafe-argument': 'off', 36 | '@typescript-eslint/no-unsafe-return': 'off', 37 | '@typescript-eslint/require-await': 'warn', 38 | '@typescript-eslint/no-unused-vars': 'off', 39 | '@typescript-eslint/unbound-method': 'off', 40 | '@typescript-eslint/no-unsafe-enum-comparison': 'off', 41 | '@typescript-eslint/no-redundant-type-constituents': 'warn', 42 | '@typescript-eslint/only-throw-error': 'warn', 43 | 'no-useless-escape': 'off', 44 | '@typescript-eslint/no-require-imports': 'off' 45 | }, 46 | }, 47 | ); -------------------------------------------------------------------------------- /lib/explorers/api-callbacks.explorer.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@nestjs/common'; 2 | import { DECORATORS } from '../constants'; 3 | import { getSchemaPath } from '../utils'; 4 | import { CallBackObject } from '../interfaces/callback-object.interface'; 5 | 6 | export const exploreApiCallbacksMetadata = ( 7 | instance: object, 8 | prototype: Type, 9 | method: object 10 | ) => { 11 | const callbacksData = Reflect.getMetadata(DECORATORS.API_CALLBACKS, method); 12 | if (!callbacksData) return callbacksData; 13 | 14 | return callbacksData.reduce( 15 | (acc, callbackData: CallBackObject) => { 16 | const { 17 | name: eventName, 18 | callbackUrl, 19 | method: callbackMethod, 20 | requestBody, 21 | expectedResponse 22 | } = callbackData; 23 | return { 24 | ...acc, 25 | [eventName]: { 26 | [callbackUrl]: { 27 | [callbackMethod]: { 28 | requestBody: { 29 | required: true, 30 | content: { 31 | 'application/json': { 32 | schema: { 33 | $ref: getSchemaPath(requestBody.type) 34 | } 35 | } 36 | } 37 | }, 38 | responses: { 39 | [expectedResponse.status]: { 40 | description: 41 | expectedResponse.description || 42 | 'Your server returns this code if it accepts the callback' 43 | } 44 | } 45 | } 46 | } 47 | } 48 | }; 49 | }, 50 | {} 51 | ); 52 | }; 53 | -------------------------------------------------------------------------------- /test/type-helpers/pick-type.helper.spec.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@nestjs/common'; 2 | import { Transform } from 'class-transformer'; 3 | import { MinLength } from 'class-validator'; 4 | import { ApiProperty } from '../../lib/decorators'; 5 | import { METADATA_FACTORY_NAME } from '../../lib/plugin/plugin-constants'; 6 | import { ModelPropertiesAccessor } from '../../lib/services/model-properties-accessor'; 7 | import { PickType } from '../../lib/type-helpers'; 8 | 9 | describe('PickType', () => { 10 | class CreateUserDto { 11 | @Transform((str) => str + '_transformed') 12 | @MinLength(10) 13 | @ApiProperty({ required: true }) 14 | login: string; 15 | 16 | @MinLength(10) 17 | @ApiProperty({ minLength: 10 }) 18 | password: string; 19 | 20 | firstName: string; 21 | 22 | static [METADATA_FACTORY_NAME]() { 23 | return { 24 | firstName: { required: true, type: () => String }, 25 | lastName: { required: true, type: () => String } 26 | }; 27 | } 28 | } 29 | 30 | class UpdateUserDto extends PickType(CreateUserDto, ['login', 'firstName']) {} 31 | 32 | let modelPropertiesAccessor: ModelPropertiesAccessor; 33 | 34 | beforeEach(() => { 35 | modelPropertiesAccessor = new ModelPropertiesAccessor(); 36 | }); 37 | 38 | describe('OpenAPI metadata', () => { 39 | it('should pick "login" property', () => { 40 | const prototype = (UpdateUserDto.prototype as any) as Type; 41 | 42 | modelPropertiesAccessor.applyMetadataFactory(prototype); 43 | expect(modelPropertiesAccessor.getModelProperties(prototype)).toEqual([ 44 | 'login', 45 | 'firstName' 46 | ]); 47 | }); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /test/plugin/fixtures/project/cats/dto/pagination-query.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '../../../../lib'; 2 | 3 | export enum LettersEnum { 4 | A = 'A', 5 | B = 'B', 6 | C = 'C' 7 | } 8 | 9 | export class PaginationQuery { 10 | @ApiProperty({ 11 | minimum: 0, 12 | maximum: 10000, 13 | title: 'Page', 14 | exclusiveMaximum: true, 15 | exclusiveMinimum: true, 16 | format: 'int32', 17 | default: 0, 18 | example: 123 19 | }) 20 | page: number; 21 | 22 | @ApiProperty({ 23 | name: '_sortBy', 24 | nullable: true, 25 | example: ['sort1', 'sort2'] 26 | }) 27 | sortBy: string[]; 28 | 29 | @ApiProperty() 30 | limit: number; 31 | 32 | @ApiProperty({ 33 | oneOf: [ 34 | { 35 | minimum: 0, 36 | maximum: 10, 37 | format: 'int32' 38 | }, 39 | { 40 | minimum: 100, 41 | maximum: 100, 42 | format: 'int32' 43 | } 44 | ], 45 | }) 46 | constrainedLimit?: number; 47 | 48 | @ApiProperty({ 49 | enum: LettersEnum, 50 | enumName: 'LettersEnum' 51 | }) 52 | enum: LettersEnum; 53 | 54 | @ApiProperty({ 55 | enum: LettersEnum, 56 | enumName: 'LettersEnum', 57 | isArray: true 58 | }) 59 | enumArr: LettersEnum[]; 60 | 61 | @ApiProperty({ 62 | enum: LettersEnum, 63 | enumName: 'Letter', 64 | isArray: true, 65 | }) 66 | letters: LettersEnum[]; 67 | 68 | @ApiProperty() 69 | beforeDate: Date; 70 | 71 | @ApiProperty({ 72 | type: 'object', 73 | additionalProperties: true 74 | }) 75 | filter: Record; 76 | 77 | static _OPENAPI_METADATA_FACTORY() { 78 | return { 79 | sortBy: { type: () => [String] } 80 | }; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /lib/decorators/api-body.decorator.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@nestjs/common'; 2 | import { omit } from 'lodash'; 3 | import { 4 | ExamplesObject, 5 | ReferenceObject, 6 | RequestBodyObject, 7 | SchemaObject 8 | } from '../interfaces/open-api-spec.interface'; 9 | import { SwaggerEnumType } from '../types/swagger-enum.type'; 10 | import { 11 | addEnumArraySchema, 12 | addEnumSchema, 13 | isEnumArray, 14 | isEnumDefined 15 | } from '../utils/enum.utils'; 16 | import { createParamDecorator, getTypeIsArrayTuple } from './helpers'; 17 | 18 | type RequestBodyOptions = Omit; 19 | 20 | interface ApiBodyMetadata extends RequestBodyOptions { 21 | type?: Type | Function | [Function] | string; 22 | isArray?: boolean; 23 | enum?: SwaggerEnumType; 24 | } 25 | 26 | interface ApiBodySchemaHost extends RequestBodyOptions { 27 | schema: SchemaObject | ReferenceObject; 28 | examples?: ExamplesObject; 29 | } 30 | 31 | export type ApiBodyOptions = ApiBodyMetadata | ApiBodySchemaHost; 32 | 33 | const defaultBodyMetadata: ApiBodyMetadata = { 34 | type: String, 35 | required: true 36 | }; 37 | 38 | /** 39 | * @publicApi 40 | */ 41 | export function ApiBody(options: ApiBodyOptions): MethodDecorator { 42 | const [type, isArray] = getTypeIsArrayTuple( 43 | (options as ApiBodyMetadata).type, 44 | (options as ApiBodyMetadata).isArray 45 | ); 46 | const param: ApiBodyMetadata & Record = { 47 | in: 'body', 48 | ...omit(options, 'enum'), 49 | type, 50 | isArray 51 | }; 52 | 53 | if (isEnumArray(options)) { 54 | addEnumArraySchema(param, options); 55 | } else if (isEnumDefined(options)) { 56 | addEnumSchema(param, options); 57 | } 58 | return createParamDecorator(param, defaultBodyMetadata); 59 | } 60 | -------------------------------------------------------------------------------- /lib/decorators/api-param.decorator.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@nestjs/common'; 2 | import { isNil, omit } from 'lodash'; 3 | import { EnumSchemaAttributes } from '../interfaces/enum-schema-attributes.interface'; 4 | import { 5 | ParameterObject, 6 | SchemaObject 7 | } from '../interfaces/open-api-spec.interface'; 8 | import { SwaggerEnumType } from '../types/swagger-enum.type'; 9 | import { addEnumSchema, isEnumDefined } from '../utils/enum.utils'; 10 | import { createParamDecorator } from './helpers'; 11 | 12 | type ParameterOptions = Omit; 13 | 14 | interface ApiParamCommonMetadata extends ParameterOptions { 15 | type?: Type | Function | [Function] | string; 16 | format?: string; 17 | enum?: SwaggerEnumType; 18 | enumName?: string; 19 | enumSchema?: EnumSchemaAttributes; 20 | } 21 | 22 | type ApiParamMetadata = 23 | | ApiParamCommonMetadata 24 | | (ApiParamCommonMetadata & { 25 | enumName: string; 26 | enumSchema?: EnumSchemaAttributes; 27 | }); 28 | 29 | interface ApiParamSchemaHost extends ParameterOptions { 30 | schema: SchemaObject; 31 | } 32 | 33 | export type ApiParamOptions = ApiParamMetadata | ApiParamSchemaHost; 34 | 35 | const defaultParamOptions: ApiParamOptions = { 36 | name: '', 37 | required: true 38 | }; 39 | 40 | /** 41 | * @publicApi 42 | */ 43 | export function ApiParam( 44 | options: ApiParamOptions 45 | ): MethodDecorator & ClassDecorator { 46 | const param: ApiParamMetadata & Record = { 47 | name: isNil(options.name) ? defaultParamOptions.name : options.name, 48 | in: 'path', 49 | ...omit(options, 'enum') 50 | }; 51 | 52 | if (isEnumDefined(options)) { 53 | addEnumSchema(param, options); 54 | } 55 | 56 | return createParamDecorator(param, defaultParamOptions); 57 | } 58 | -------------------------------------------------------------------------------- /lib/interfaces/swagger-document-options.interface.ts: -------------------------------------------------------------------------------- 1 | export type OperationIdFactory = ( 2 | controllerKey: string, 3 | methodKey: string, 4 | version?: string 5 | ) => string; 6 | 7 | /** 8 | * @publicApi 9 | */ 10 | export interface SwaggerDocumentOptions { 11 | /** 12 | * List of modules to include in the specification 13 | */ 14 | include?: Function[]; 15 | 16 | /** 17 | * Additional, extra models that should be inspected and included in the specification 18 | */ 19 | extraModels?: Function[]; 20 | 21 | /** 22 | * If `true`, swagger will ignore the global prefix set through `setGlobalPrefix()` method 23 | */ 24 | ignoreGlobalPrefix?: boolean; 25 | 26 | /** 27 | * If `true`, swagger will also load routes from the modules imported by `include` modules 28 | */ 29 | deepScanRoutes?: boolean; 30 | 31 | /** 32 | * Custom operationIdFactory that will be used to generate the `operationId` 33 | * based on the `controllerKey`, `methodKey`, and version. 34 | * @default () => controllerKey_methodKey_version 35 | */ 36 | operationIdFactory?: OperationIdFactory; 37 | 38 | /** 39 | * Custom linkNameFactory that will be used to generate the name of links 40 | * in the `links` field of responses 41 | * 42 | * @see [Link objects](https://swagger.io/docs/specification/links/) 43 | * 44 | * @default () => `${controllerKey}_${methodKey}_from_${fieldKey}` 45 | */ 46 | linkNameFactory?: ( 47 | controllerKey: string, 48 | methodKey: string, 49 | fieldKey: string 50 | ) => string; 51 | 52 | /* 53 | * Generate tags automatically based on the controller name. 54 | * If `false`, you must use the `@ApiTags()` decorator to define tags. 55 | * Otherwise, the controller name without the suffix `Controller` will be used. 56 | * @default true 57 | */ 58 | autoTagControllers?: boolean; 59 | } 60 | -------------------------------------------------------------------------------- /lib/plugin/visitors/abstract.visitor.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | import { OPENAPI_NAMESPACE, OPENAPI_PACKAGE_NAME } from '../plugin-constants'; 3 | 4 | const [major, minor] = ts.versionMajorMinor.split('.').map((x) => +x); 5 | 6 | export class AbstractFileVisitor { 7 | updateImports( 8 | sourceFile: ts.SourceFile, 9 | factory: ts.NodeFactory | undefined, 10 | program: ts.Program 11 | ): ts.SourceFile { 12 | if (major <= 4 && minor < 8) { 13 | throw new Error('Nest CLI plugin does not support TypeScript < v4.8'); 14 | } 15 | const importEqualsDeclaration: ts.ImportEqualsDeclaration = 16 | factory.createImportEqualsDeclaration( 17 | undefined, 18 | false, 19 | factory.createIdentifier(OPENAPI_NAMESPACE), 20 | factory.createExternalModuleReference( 21 | factory.createStringLiteral(OPENAPI_PACKAGE_NAME) 22 | ) 23 | ); 24 | 25 | const compilerOptions = program.getCompilerOptions(); 26 | if ( 27 | compilerOptions.module >= ts.ModuleKind.ES2015 && 28 | compilerOptions.module <= ts.ModuleKind.ESNext 29 | ) { 30 | const importAsDeclaration = (factory.createImportDeclaration as any)( 31 | undefined, 32 | factory.createImportClause( 33 | false, 34 | undefined, 35 | factory.createNamespaceImport( 36 | factory.createIdentifier(OPENAPI_NAMESPACE) 37 | ) 38 | ), 39 | factory.createStringLiteral(OPENAPI_PACKAGE_NAME), 40 | undefined 41 | ); 42 | return factory.updateSourceFile(sourceFile, [ 43 | importAsDeclaration, 44 | ...sourceFile.statements 45 | ]); 46 | } else { 47 | return factory.updateSourceFile(sourceFile, [ 48 | importEqualsDeclaration, 49 | ...sourceFile.statements 50 | ]); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /lib/plugin/visitors/readonly.visitor.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | import { PluginOptions, mergePluginOptions } from '../merge-options'; 3 | import { isFilenameMatched } from '../utils/is-filename-matched.util'; 4 | import { ControllerClassVisitor } from './controller-class.visitor'; 5 | import { ModelClassVisitor } from './model-class.visitor'; 6 | 7 | export class ReadonlyVisitor { 8 | public readonly key = '@nestjs/swagger'; 9 | private readonly modelClassVisitor = new ModelClassVisitor(); 10 | private readonly controllerClassVisitor = new ControllerClassVisitor(); 11 | 12 | get typeImports() { 13 | return { 14 | ...this.modelClassVisitor.typeImports, 15 | ...this.controllerClassVisitor.typeImports 16 | }; 17 | } 18 | 19 | constructor(private readonly options: PluginOptions) { 20 | options.readonly = true; 21 | 22 | if (!options.pathToSource) { 23 | throw new Error(`"pathToSource" must be defined in plugin options`); 24 | } 25 | } 26 | 27 | visit(program: ts.Program, sf: ts.SourceFile) { 28 | const factoryHost = { factory: ts.factory } as any; 29 | const parsedOptions: Record = mergePluginOptions(this.options); 30 | 31 | if (isFilenameMatched(parsedOptions.dtoFileNameSuffix, sf.fileName)) { 32 | return this.modelClassVisitor.visit( 33 | sf, 34 | factoryHost, 35 | program, 36 | parsedOptions 37 | ); 38 | } 39 | if ( 40 | isFilenameMatched(parsedOptions.controllerFileNameSuffix, sf.fileName) 41 | ) { 42 | return this.controllerClassVisitor.visit( 43 | sf, 44 | factoryHost, 45 | program, 46 | parsedOptions 47 | ); 48 | } 49 | } 50 | 51 | collect() { 52 | return { 53 | models: this.modelClassVisitor.collectedMetadata(this.options), 54 | controllers: this.controllerClassVisitor.collectedMetadata(this.options) 55 | }; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | aliases: 4 | - &restore-cache 5 | restore_cache: 6 | key: dependency-cache-{{ checksum "package.json" }} 7 | - &install-deps 8 | run: 9 | name: Install dependencies 10 | command: npm install --ignore-scripts --legacy-peer-deps 11 | - &build-packages 12 | run: 13 | name: Build 14 | command: npm run build 15 | - &run-unit-tests 16 | run: 17 | name: Test 18 | command: npm run test -- --runInBand 19 | - &run-e2e-tests 20 | run: 21 | name: E2E test 22 | command: npm run test:e2e 23 | 24 | jobs: 25 | build: 26 | working_directory: ~/nest 27 | docker: 28 | - image: cimg/node:24.11.1 29 | steps: 30 | - checkout 31 | - restore_cache: 32 | key: dependency-cache-{{ checksum "package.json" }} 33 | - run: 34 | name: Install dependencies 35 | command: npm install --ignore-scripts --legacy-peer-deps 36 | - save_cache: 37 | key: dependency-cache-{{ checksum "package.json" }} 38 | paths: 39 | - ./node_modules 40 | - run: 41 | name: Build 42 | command: npm run build 43 | 44 | unit_tests: 45 | working_directory: ~/nest 46 | docker: 47 | - image: cimg/node:24.11.1 48 | steps: 49 | - checkout 50 | - *restore-cache 51 | - *install-deps 52 | - *build-packages 53 | - *run-unit-tests 54 | 55 | e2e_tests: 56 | working_directory: ~/nest 57 | docker: 58 | - image: cimg/node:24.11.1 59 | steps: 60 | - checkout 61 | - *restore-cache 62 | - *install-deps 63 | - *build-packages 64 | - *run-e2e-tests 65 | 66 | workflows: 67 | version: 2 68 | build-and-test: 69 | jobs: 70 | - build 71 | - unit_tests: 72 | requires: 73 | - build 74 | - e2e_tests: 75 | requires: 76 | - build 77 | -------------------------------------------------------------------------------- /lib/services/response-object-mapper.ts: -------------------------------------------------------------------------------- 1 | import { omit, pick } from 'lodash'; 2 | import { ApiResponseMetadata, ApiResponseSchemaHost } from '../decorators'; 3 | import { getSchemaPath } from '../utils'; 4 | import { MimetypeContentWrapper } from './mimetype-content-wrapper'; 5 | 6 | export class ResponseObjectMapper { 7 | private readonly mimetypeContentWrapper = new MimetypeContentWrapper(); 8 | 9 | toArrayRefObject( 10 | response: Record, 11 | name: string, 12 | produces: string[] 13 | ) { 14 | const exampleKeys = ['example', 'examples']; 15 | return { 16 | ...omit(response, exampleKeys), 17 | ...this.mimetypeContentWrapper.wrap(produces, { 18 | schema: { 19 | type: 'array', 20 | items: { 21 | $ref: getSchemaPath(name) 22 | } 23 | }, 24 | ...pick(response, exampleKeys) 25 | }) 26 | }; 27 | } 28 | 29 | toRefObject(response: Record, name: string, produces: string[]) { 30 | const exampleKeys = ['example', 'examples']; 31 | return { 32 | ...omit(response, exampleKeys), 33 | ...this.mimetypeContentWrapper.wrap(produces, { 34 | schema: { 35 | $ref: getSchemaPath(name) 36 | }, 37 | ...pick(response, exampleKeys) 38 | }) 39 | }; 40 | } 41 | 42 | wrapSchemaWithContent( 43 | response: ApiResponseSchemaHost & ApiResponseMetadata, 44 | produces: string[] 45 | ) { 46 | if ( 47 | !response.schema && 48 | !('example' in response) && 49 | !('examples' in response) 50 | ) { 51 | return response; 52 | } 53 | const exampleKeys = ['example', 'examples']; 54 | const content = this.mimetypeContentWrapper.wrap(produces, { 55 | schema: response.schema, 56 | ...pick(response, exampleKeys) 57 | }); 58 | 59 | const keysToOmit = [...exampleKeys, 'schema']; 60 | return { 61 | ...omit(response, keysToOmit), 62 | ...content 63 | }; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /test/decorators/api-query.decorator.spec.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Query } from '@nestjs/common'; 2 | import { DECORATORS } from '../../lib/constants'; 3 | import { ApiQuery } from '../../lib/decorators'; 4 | 5 | describe('ApiQuery', () => { 6 | describe('when applied on the class level', () => { 7 | @ApiQuery({ name: 'testId' }) 8 | @Controller('test') 9 | class TestAppController { 10 | @Get() 11 | public get(@Query('testId') testId: string): string { 12 | return testId; 13 | } 14 | 15 | public noAPiMethod(): void {} 16 | } 17 | 18 | it('should attach metadata to all API methods', () => { 19 | const controller = new TestAppController(); 20 | expect( 21 | Reflect.hasMetadata(DECORATORS.API_PARAMETERS, controller.get) 22 | ).toBeTruthy(); 23 | expect( 24 | Reflect.getMetadata(DECORATORS.API_PARAMETERS, controller.get) 25 | ).toEqual([{ in: 'query', name: 'testId', required: true }]); 26 | }); 27 | 28 | it('should not attach metadata to non-API method (not a route)', () => { 29 | const controller = new TestAppController(); 30 | expect( 31 | Reflect.hasMetadata(DECORATORS.API_PARAMETERS, controller.noAPiMethod) 32 | ).toBeFalsy(); 33 | }); 34 | }); 35 | 36 | describe('when applied on the method level', () => { 37 | @Controller('tests/:testId') 38 | class TestAppController { 39 | @Get() 40 | @ApiQuery({ name: 'testId' }) 41 | public get(@Query('testId') testId: string): string { 42 | return testId; 43 | } 44 | } 45 | 46 | it('should attach metadata to a given method', () => { 47 | const controller = new TestAppController(); 48 | expect( 49 | Reflect.hasMetadata(DECORATORS.API_PARAMETERS, controller.get) 50 | ).toBeTruthy(); 51 | expect( 52 | Reflect.getMetadata(DECORATORS.API_PARAMETERS, controller.get) 53 | ).toEqual([{ in: 'query', name: 'testId', required: true }]); 54 | }); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /test/decorators/api-param.decorator.spec.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Param } from '@nestjs/common'; 2 | import { DECORATORS } from '../../lib/constants'; 3 | import { ApiParam } from '../../lib/decorators'; 4 | 5 | describe('ApiParam', () => { 6 | describe('when applied on the class level', () => { 7 | @ApiParam({ name: 'testId' }) 8 | @Controller('tests/:testId') 9 | class TestAppController { 10 | @Get() 11 | public get(@Param('testId') testId: string): string { 12 | return testId; 13 | } 14 | 15 | public noAPiMethod(): void {} 16 | } 17 | 18 | it('should attach metadata to all API methods', () => { 19 | const controller = new TestAppController(); 20 | expect( 21 | Reflect.hasMetadata(DECORATORS.API_PARAMETERS, controller.get) 22 | ).toBeTruthy(); 23 | expect( 24 | Reflect.getMetadata(DECORATORS.API_PARAMETERS, controller.get) 25 | ).toEqual([{ in: 'path', name: 'testId', required: true }]); 26 | }); 27 | 28 | it('should not attach metadata to non-API method (not a route)', () => { 29 | const controller = new TestAppController(); 30 | expect( 31 | Reflect.hasMetadata(DECORATORS.API_PARAMETERS, controller.noAPiMethod) 32 | ).toBeFalsy(); 33 | }); 34 | }); 35 | 36 | describe('when applied on the method level', () => { 37 | @Controller('tests/:testId') 38 | class TestAppController { 39 | @Get() 40 | @ApiParam({ name: 'testId' }) 41 | public get(@Param('testId') testId: string): string { 42 | return testId; 43 | } 44 | } 45 | 46 | it('should attach metadata to a given method', () => { 47 | const controller = new TestAppController(); 48 | expect( 49 | Reflect.hasMetadata(DECORATORS.API_PARAMETERS, controller.get) 50 | ).toBeTruthy(); 51 | expect( 52 | Reflect.getMetadata(DECORATORS.API_PARAMETERS, controller.get) 53 | ).toEqual([{ in: 'path', name: 'testId', required: true }]); 54 | }); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /lib/explorers/api-operation.explorer.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@nestjs/common'; 2 | import { DECORATORS } from '../constants'; 3 | import { ApiOperation } from '../decorators/api-operation.decorator'; 4 | import { METADATA_FACTORY_NAME } from '../plugin/plugin-constants'; 5 | 6 | export const exploreApiOperationMetadata = ( 7 | instance: object, 8 | prototype: Type, 9 | method: object 10 | ) => { 11 | applyMetadataFactory(prototype, instance); 12 | return Reflect.getMetadata(DECORATORS.API_OPERATION, method); 13 | }; 14 | 15 | function applyMetadataFactory(prototype: Type, instance: object) { 16 | const classPrototype = prototype; 17 | do { 18 | if (!prototype.constructor) { 19 | return; 20 | } 21 | if (!prototype.constructor[METADATA_FACTORY_NAME]) { 22 | continue; 23 | } 24 | const metadata = prototype.constructor[METADATA_FACTORY_NAME](); 25 | const methodKeys = Object.keys(metadata).filter( 26 | (key) => typeof instance[key] === 'function' 27 | ); 28 | 29 | methodKeys.forEach((key) => { 30 | const operationMeta = {}; 31 | const { summary, deprecated, tags, description } = metadata[key]; 32 | 33 | applyIfNotNil(operationMeta, 'summary', summary); 34 | applyIfNotNil(operationMeta, 'deprecated', deprecated); 35 | applyIfNotNil(operationMeta, 'tags', tags); 36 | applyIfNotNil(operationMeta, 'description', description); 37 | 38 | if (Object.keys(operationMeta).length === 0) { 39 | return; 40 | } 41 | ApiOperation(operationMeta, { overrideExisting: false })( 42 | classPrototype, 43 | key, 44 | Object.getOwnPropertyDescriptor(classPrototype, key) 45 | ); 46 | }); 47 | } while ( 48 | (prototype = Reflect.getPrototypeOf(prototype) as Type) && 49 | prototype !== Object.prototype && 50 | prototype 51 | ); 52 | } 53 | 54 | function applyIfNotNil(target: Record, key: string, value: any) { 55 | if (value !== undefined && value !== null) { 56 | target[key] = value; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /test/plugin/fixtures/parameter-property.dto.ts: -------------------------------------------------------------------------------- 1 | import { getOutputExtension } from '../../../lib/plugin/utils/plugin-utils'; 2 | 3 | export const parameterPropertyDtoText = ` 4 | export class ParameterPropertyDto { 5 | constructor( 6 | readonly readonlyValue?: string, 7 | private privateValue: string | null, 8 | public publicValue: ItemDto[], 9 | regularParameter: string 10 | protected protectedValue: string = '1234', 11 | ) {} 12 | } 13 | 14 | export enum LettersEnum { 15 | A = 'A', 16 | B = 'B', 17 | C = 'C' 18 | } 19 | 20 | export class ItemDto { 21 | constructor(readonly enumValue: LettersEnum) {} 22 | } 23 | `; 24 | 25 | export const parameterPropertyDtoTextTranspiled = (esmCompatible?: boolean) => { 26 | const fileName = 'parameter-property.dto'; 27 | const fileImport = esmCompatible 28 | ? `(await import("./${fileName}${getOutputExtension(fileName)}"))` 29 | : `require("./${fileName}")`; 30 | 31 | return `import * as openapi from "@nestjs/swagger"; 32 | export class ParameterPropertyDto { 33 | constructor(readonlyValue, privateValue, publicValue, regularParameter, protectedValue = '1234') { 34 | this.readonlyValue = readonlyValue; 35 | this.privateValue = privateValue; 36 | this.publicValue = publicValue; 37 | this.protectedValue = protectedValue; 38 | } 39 | static _OPENAPI_METADATA_FACTORY() { 40 | return { readonlyValue: { required: false, type: () => String }, privateValue: { required: true, type: () => String, nullable: true }, publicValue: { required: true, type: () => [${fileImport}.ItemDto] }, protectedValue: { required: true, type: () => String, default: "1234" } }; 41 | } 42 | } 43 | export var LettersEnum; 44 | (function (LettersEnum) { 45 | LettersEnum["A"] = "A"; 46 | LettersEnum["B"] = "B"; 47 | LettersEnum["C"] = "C"; 48 | })(LettersEnum || (LettersEnum = {})); 49 | export class ItemDto { 50 | constructor(enumValue) { 51 | this.enumValue = enumValue; 52 | } 53 | static _OPENAPI_METADATA_FACTORY() { 54 | return { enumValue: { required: true, enum: ${fileImport}.LettersEnum } }; 55 | } 56 | } 57 | `; 58 | }; 59 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Feature_request.yml: -------------------------------------------------------------------------------- 1 | name: "\U0001F680 Feature Request" 2 | description: "I have a suggestion \U0001F63B!" 3 | labels: ["feature"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | ## :warning: We use GitHub Issues to track bug reports, feature requests and regressions 9 | 10 | If you are not sure that your issue is a bug, you could: 11 | 12 | - use our [Discord community](https://discord.gg/NestJS) 13 | - use [StackOverflow using the tag `nestjs`](https://stackoverflow.com/questions/tagged/nestjs) 14 | - If it's just a quick question you can ping [our Twitter](https://twitter.com/nestframework) 15 | 16 | --- 17 | 18 | - type: checkboxes 19 | attributes: 20 | label: "Is there an existing issue that is already proposing this?" 21 | description: "Please search [here](./?q=is%3Aissue) to see if an issue already exists for the feature you are requesting" 22 | options: 23 | - label: "I have searched the existing issues" 24 | required: true 25 | 26 | - type: textarea 27 | validations: 28 | required: true 29 | attributes: 30 | label: "Is your feature request related to a problem? Please describe it" 31 | description: "A clear and concise description of what the problem is" 32 | placeholder: | 33 | I have an issue when ... 34 | 35 | - type: textarea 36 | validations: 37 | required: true 38 | attributes: 39 | label: "Describe the solution you'd like" 40 | description: "A clear and concise description of what you want to happen. Add any considered drawbacks" 41 | 42 | - type: textarea 43 | attributes: 44 | label: "Teachability, documentation, adoption, migration strategy" 45 | description: "If you can, explain how users will be able to use this and possibly write out a version the docs. Maybe a screenshot or design?" 46 | 47 | - type: textarea 48 | validations: 49 | required: true 50 | attributes: 51 | label: "What is the motivation / use case for changing the behavior?" 52 | description: "Describe the motivation or the concrete use case" 53 | -------------------------------------------------------------------------------- /lib/services/parameters-metadata-mapper.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@nestjs/common'; 2 | import { isFunction } from '@nestjs/common/utils/shared.utils'; 3 | import { flatMap, identity } from 'lodash'; 4 | import { DECORATORS } from '../constants'; 5 | import { isBodyParameter } from '../utils/is-body-parameter.util'; 6 | import { ModelPropertiesAccessor } from './model-properties-accessor'; 7 | import { 8 | ParamWithTypeMetadata, 9 | ParamsWithType 10 | } from './parameter-metadata-accessor'; 11 | 12 | export class ParametersMetadataMapper { 13 | constructor( 14 | private readonly modelPropertiesAccessor: ModelPropertiesAccessor 15 | ) {} 16 | 17 | transformModelToProperties( 18 | parameters: ParamsWithType 19 | ): ParamWithTypeMetadata[] { 20 | const properties = flatMap(parameters, (param: ParamWithTypeMetadata) => { 21 | if (!param || param.type === Object || !param.type) { 22 | return undefined; 23 | } 24 | if (param.name) { 25 | // when "name" is present, the "data" argument was passed to the decorator 26 | // e.g. `@Query('param') 27 | return param; 28 | } 29 | if (isBodyParameter(param)) { 30 | const isCtor = param.type && isFunction(param.type); 31 | const name = isCtor ? param.type.name : param.type; 32 | return { ...param, name }; 33 | } 34 | const { prototype } = param.type; 35 | 36 | this.modelPropertiesAccessor.applyMetadataFactory(prototype); 37 | const modelProperties = 38 | this.modelPropertiesAccessor.getModelProperties(prototype); 39 | 40 | return modelProperties.map((key) => 41 | this.mergeImplicitWithExplicit(key, prototype, param) 42 | ); 43 | }); 44 | return properties.filter(identity); 45 | } 46 | 47 | mergeImplicitWithExplicit( 48 | key: string, 49 | prototype: Type, 50 | param: ParamWithTypeMetadata 51 | ): ParamWithTypeMetadata { 52 | const reflectedParam = 53 | Reflect.getMetadata(DECORATORS.API_MODEL_PROPERTIES, prototype, key) || 54 | {}; 55 | 56 | return { 57 | ...param, 58 | ...reflectedParam, 59 | name: reflectedParam.name || key 60 | }; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /lib/decorators/api-link.decorator.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@nestjs/common'; 2 | import { DECORATORS } from '../constants'; 3 | 4 | export interface ApiLinkOptions { 5 | from: Type | Function; 6 | /** 7 | * Field in the type `from` which is used as a parameter in the decorated route 8 | * 9 | * @default 'id' 10 | */ 11 | fromField?: string; 12 | /** 13 | * Name of the parameter in the decorated route 14 | */ 15 | routeParam: string; 16 | } 17 | 18 | /** 19 | * Defines this route as a link between two types 20 | * 21 | * Typically used when the link between the types is not present in the `from` type, 22 | * eg with the following 23 | * 24 | * ```typescript 25 | * class User { 26 | * @ApiProperty() 27 | * id: string 28 | * 29 | * // no field documentIds: string[] 30 | * } 31 | * 32 | * class Document { 33 | * @ApiProperty() 34 | * id: string 35 | * } 36 | * 37 | * @Controller() 38 | * class UserController { 39 | * @Get(':userId/documents') 40 | * @ApiLink({from: User, fromField: 'id', routeParam: 'userId'}) 41 | * getDocuments(@Param('userId') userId: string)): Promise 42 | * } 43 | * ``` 44 | * 45 | * @param type The type for which the decorated function is the default getter 46 | * @param parameter Name of the parameter in the route of the getter which corresponds to the id of the type 47 | * 48 | * @see [Swagger link objects](https://swagger.io/docs/specification/links/) 49 | * 50 | * @publicApi 51 | */ 52 | export function ApiLink({ 53 | from, 54 | fromField = 'id', 55 | routeParam 56 | }: ApiLinkOptions): MethodDecorator { 57 | return ( 58 | controllerPrototype: object, 59 | key: string | symbol, 60 | descriptor: PropertyDescriptor 61 | ) => { 62 | const { prototype } = from; 63 | if (prototype) { 64 | const links = Reflect.getMetadata(DECORATORS.API_LINK, prototype) ?? []; 65 | 66 | links.push({ 67 | method: descriptor.value, 68 | prototype: controllerPrototype, 69 | field: fromField, 70 | parameter: routeParam 71 | }); 72 | 73 | Reflect.defineMetadata(DECORATORS.API_LINK, links, prototype); 74 | } 75 | 76 | return descriptor; 77 | }; 78 | } 79 | -------------------------------------------------------------------------------- /lib/decorators/api-extension.decorator.ts: -------------------------------------------------------------------------------- 1 | import { METHOD_METADATA } from '@nestjs/common/constants'; 2 | import { DECORATORS } from '../constants'; 3 | import { clone, merge } from 'lodash'; 4 | import { isConstructor } from '@nestjs/common/utils/shared.utils'; 5 | 6 | function applyExtension(target: any, key: string, value: any): void { 7 | const extensions = 8 | Reflect.getMetadata(DECORATORS.API_EXTENSION, target) || {}; 9 | Reflect.defineMetadata( 10 | DECORATORS.API_EXTENSION, 11 | { [key]: value, ...extensions }, 12 | target 13 | ); 14 | } 15 | 16 | /** 17 | * @publicApi 18 | */ 19 | export function ApiExtension(extensionKey: string, extensionProperties: any) { 20 | if (!extensionKey.startsWith('x-')) { 21 | throw new Error( 22 | 'Extension key is not prefixed. Please ensure you prefix it with `x-`.' 23 | ); 24 | } 25 | 26 | return ( 27 | target: object | Function, 28 | key?: string | symbol, 29 | descriptor?: TypedPropertyDescriptor 30 | ): any => { 31 | const extensionValue = clone(extensionProperties); 32 | 33 | // Method-level decorator 34 | if (descriptor) { 35 | applyExtension(descriptor.value, extensionKey, extensionValue); 36 | return descriptor; 37 | } 38 | 39 | // Ensure decorator is used on a class 40 | if (typeof target === 'object') { 41 | return target; 42 | } 43 | 44 | // Look for API methods 45 | const apiMethods = Object.getOwnPropertyNames(target.prototype) 46 | .filter((propertyKey) => !isConstructor(propertyKey)) 47 | .map((propertyKey) => 48 | Object.getOwnPropertyDescriptor(target.prototype, propertyKey)?.value 49 | ) 50 | .filter((methodDescriptor) => 51 | methodDescriptor !== undefined && Reflect.hasMetadata(METHOD_METADATA, methodDescriptor) 52 | ); 53 | 54 | // If we found API methods, apply the extension, otherwise assume it's a DTO and apply to the class itself. 55 | if (apiMethods.length > 0) { 56 | apiMethods.forEach((method) => 57 | applyExtension(method, extensionKey, extensionValue) 58 | ); 59 | } else { 60 | applyExtension(target, extensionKey, extensionValue); 61 | } 62 | 63 | return target; 64 | }; 65 | } 66 | -------------------------------------------------------------------------------- /lib/plugin/merge-options.ts: -------------------------------------------------------------------------------- 1 | import { isString } from '@nestjs/common/utils/shared.utils'; 2 | import { pluginDebugLogger } from './plugin-debug-logger'; 3 | 4 | export interface PluginOptions { 5 | dtoFileNameSuffix?: string | string[]; 6 | controllerFileNameSuffix?: string | string[]; 7 | classValidatorShim?: boolean; 8 | classTransformerShim?: boolean | 'exclusive'; 9 | dtoKeyOfComment?: string; 10 | controllerKeyOfComment?: string; 11 | introspectComments?: boolean; 12 | esmCompatible?: boolean; 13 | readonly?: boolean; 14 | pathToSource?: string; 15 | debug?: boolean; 16 | parameterProperties?: boolean; 17 | /** 18 | * Skip auto-annotating controller methods with HTTP status codes (e.g., @HttpCode(201)) 19 | */ 20 | skipAutoHttpCode?: boolean; 21 | /** 22 | * Skip add default for properties that do not specify default values. 23 | */ 24 | skipDefaultValues?: boolean; 25 | } 26 | 27 | const defaultOptions: PluginOptions = { 28 | dtoFileNameSuffix: ['.dto.ts', '.entity.ts'], 29 | controllerFileNameSuffix: ['.controller.ts'], 30 | classValidatorShim: true, 31 | classTransformerShim: false, 32 | dtoKeyOfComment: 'description', 33 | controllerKeyOfComment: 'summary', 34 | introspectComments: false, 35 | esmCompatible: false, 36 | readonly: false, 37 | debug: false, 38 | skipDefaultValues: false 39 | }; 40 | 41 | export const mergePluginOptions = ( 42 | options: Record = {} 43 | ): PluginOptions => { 44 | if (isString(options.dtoFileNameSuffix)) { 45 | options.dtoFileNameSuffix = [options.dtoFileNameSuffix]; 46 | } 47 | if (isString(options.controllerFileNameSuffix)) { 48 | options.controllerFileNameSuffix = [options.controllerFileNameSuffix]; 49 | } 50 | for (const key of ['dtoFileNameSuffix', 'controllerFileNameSuffix']) { 51 | if (options[key] && options[key].includes('.ts')) { 52 | pluginDebugLogger.warn( 53 | `Skipping ${key} option ".ts" because it can cause unwanted behaviour.` 54 | ); 55 | options[key] = options[key].filter((pattern) => pattern !== '.ts'); 56 | if (options[key].length == 0) { 57 | delete options[key]; 58 | } 59 | } 60 | } 61 | return { 62 | ...defaultOptions, 63 | ...options 64 | }; 65 | }; 66 | -------------------------------------------------------------------------------- /e2e/src/cats/dto/pagination-query.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '../../../../lib'; 2 | 3 | export enum LettersEnum { 4 | A = 'A', 5 | B = 'B', 6 | C = 'C' 7 | } 8 | 9 | export class PaginationQuery { 10 | @ApiProperty({ 11 | minimum: 0, 12 | maximum: 10000, 13 | title: 'Page', 14 | exclusiveMaximum: true, 15 | exclusiveMinimum: true, 16 | format: 'int32', 17 | default: 0, 18 | example: 123 19 | }) 20 | page: number; 21 | 22 | @ApiProperty({ 23 | name: '_sortBy', 24 | nullable: true, 25 | example: ['sort1', 'sort2'] 26 | }) 27 | sortBy: string[]; 28 | 29 | @ApiProperty() 30 | limit: number; 31 | 32 | @ApiProperty({ 33 | oneOf: [ 34 | { 35 | minimum: 0, 36 | maximum: 10, 37 | format: 'int32' 38 | }, 39 | { 40 | minimum: 100, 41 | maximum: 100, 42 | format: 'int32' 43 | } 44 | ] 45 | }) 46 | constrainedLimit?: number; 47 | 48 | @ApiProperty({ 49 | enum: LettersEnum, 50 | enumName: 'LettersEnum' 51 | }) 52 | enum: LettersEnum; 53 | 54 | @ApiProperty({ 55 | enum: LettersEnum, 56 | enumName: 'LettersEnum', 57 | isArray: true 58 | }) 59 | enumArr: LettersEnum[]; 60 | 61 | @ApiProperty({ 62 | enum: LettersEnum, 63 | enumName: 'Letter', 64 | isArray: true 65 | }) 66 | letters: LettersEnum[]; 67 | 68 | @ApiProperty() 69 | beforeDate: Date; 70 | 71 | @ApiProperty({ 72 | type: 'object', 73 | properties: { 74 | name: { 75 | type: 'string' 76 | }, 77 | age: { 78 | type: 'number' 79 | } 80 | }, 81 | additionalProperties: true 82 | }) 83 | filter: Record; 84 | 85 | static _OPENAPI_METADATA_FACTORY() { 86 | return { 87 | sortBy: { type: () => [String] }, 88 | strArray: { required: true, type: () => [String] }, 89 | raw: { 90 | required: true, 91 | type: () => ({ foo: { required: true, type: () => String } }) 92 | }, 93 | rawArray: { 94 | required: false, 95 | type: () => [{ foo: { required: true, type: () => String } }] 96 | } 97 | }; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /test/type-helpers/intersection-type.helper.spec.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@nestjs/common'; 2 | import { Expose, Transform } from 'class-transformer'; 3 | import { IsBoolean, IsString } from 'class-validator'; 4 | import { ApiProperty } from '../../lib/decorators'; 5 | import { METADATA_FACTORY_NAME } from '../../lib/plugin/plugin-constants'; 6 | import { ModelPropertiesAccessor } from '../../lib/services/model-properties-accessor'; 7 | import { IntersectionType } from '../../lib/type-helpers'; 8 | 9 | describe('IntersectionType', () => { 10 | class CreateUserDto { 11 | @ApiProperty({ required: true }) 12 | login: string; 13 | 14 | @Expose() 15 | @Transform((str) => str + '_transformed') 16 | @IsString() 17 | @ApiProperty({ minLength: 10 }) 18 | password: string; 19 | 20 | static [METADATA_FACTORY_NAME]() { 21 | return { dateOfBirth: { required: true, type: () => String } }; 22 | } 23 | } 24 | 25 | class UserDto { 26 | @IsString() 27 | @ApiProperty({ required: false }) 28 | firstName: string; 29 | 30 | static [METADATA_FACTORY_NAME]() { 31 | return { dateOfBirth2: { required: true, type: () => String } }; 32 | } 33 | } 34 | 35 | class AuthorityDto { 36 | @IsBoolean() 37 | @ApiProperty({ required: true }) 38 | isAdmin: boolean; 39 | 40 | static [METADATA_FACTORY_NAME]() { 41 | return { dateOfBirth3: { required: true, type: () => String } }; 42 | } 43 | } 44 | 45 | class UpdateUserDto extends IntersectionType( 46 | UserDto, 47 | CreateUserDto, 48 | AuthorityDto 49 | ) {} 50 | 51 | let modelPropertiesAccessor: ModelPropertiesAccessor; 52 | 53 | beforeEach(() => { 54 | modelPropertiesAccessor = new ModelPropertiesAccessor(); 55 | }); 56 | 57 | describe('OpenAPI metadata', () => { 58 | it('should return combined class', () => { 59 | const prototype = UpdateUserDto.prototype as any as Type; 60 | 61 | modelPropertiesAccessor.applyMetadataFactory(prototype); 62 | expect(modelPropertiesAccessor.getModelProperties(prototype)).toEqual([ 63 | 'firstName', 64 | 'login', 65 | 'password', 66 | 'isAdmin', 67 | 'dateOfBirth2', 68 | 'dateOfBirth', 69 | 'dateOfBirth3' 70 | ]); 71 | }); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /lib/decorators/api-header.decorator.ts: -------------------------------------------------------------------------------- 1 | import { isNil, isUndefined, negate, pickBy } from 'lodash'; 2 | import { DECORATORS } from '../constants'; 3 | import { 4 | ParameterLocation, 5 | ParameterObject 6 | } from '../interfaces/open-api-spec.interface'; 7 | import { SwaggerEnumType } from '../types/swagger-enum.type'; 8 | import { getEnumType, getEnumValues } from '../utils/enum.utils'; 9 | import { createClassDecorator, createParamDecorator } from './helpers'; 10 | 11 | export interface ApiHeaderOptions extends Omit { 12 | enum?: SwaggerEnumType; 13 | } 14 | 15 | const defaultHeaderOptions: Partial = { 16 | name: '' 17 | }; 18 | 19 | /** 20 | * @publicApi 21 | */ 22 | export function ApiHeader( 23 | options: ApiHeaderOptions 24 | ): MethodDecorator & ClassDecorator { 25 | const param = pickBy( 26 | { 27 | name: isNil(options.name) ? defaultHeaderOptions.name : options.name, 28 | in: 'header', 29 | description: options.description, 30 | required: options.required, 31 | examples: options.examples, 32 | schema: { 33 | type: 'string', 34 | ...(options.schema || {}) 35 | } 36 | }, 37 | negate(isUndefined) 38 | ); 39 | 40 | if (options.enum) { 41 | const enumValues = getEnumValues(options.enum); 42 | param.schema = { 43 | ...param.schema, 44 | enum: enumValues, 45 | type: getEnumType(enumValues) 46 | }; 47 | } 48 | 49 | return ( 50 | target: object | Function, 51 | key?: string | symbol, 52 | descriptor?: TypedPropertyDescriptor 53 | ): any => { 54 | if (descriptor) { 55 | return createParamDecorator(param, defaultHeaderOptions)( 56 | target, 57 | key, 58 | descriptor 59 | ); 60 | } 61 | return createClassDecorator(DECORATORS.API_HEADERS, [param])( 62 | target as Function 63 | ); 64 | }; 65 | } 66 | 67 | export const ApiHeaders = ( 68 | headers: ApiHeaderOptions[] 69 | ): MethodDecorator & ClassDecorator => { 70 | return ( 71 | target: object | Function, 72 | key?: string | symbol, 73 | descriptor?: TypedPropertyDescriptor 74 | ): any => { 75 | headers.forEach((options) => ApiHeader(options)(target, key, descriptor)); 76 | }; 77 | }; 78 | -------------------------------------------------------------------------------- /lib/decorators/api-query.decorator.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@nestjs/common'; 2 | import { omit } from 'lodash'; 3 | import { EnumSchemaAttributes } from '../interfaces/enum-schema-attributes.interface'; 4 | import { 5 | ParameterObject, 6 | ReferenceObject, 7 | SchemaObject 8 | } from '../interfaces/open-api-spec.interface'; 9 | import { SwaggerEnumType } from '../types/swagger-enum.type'; 10 | import { 11 | addEnumArraySchema, 12 | addEnumSchema, 13 | isEnumArray, 14 | isEnumDefined 15 | } from '../utils/enum.utils'; 16 | import { createParamDecorator, getTypeIsArrayTuple } from './helpers'; 17 | 18 | type ParameterOptions = Omit; 19 | 20 | interface ApiQueryCommonMetadata extends ParameterOptions { 21 | type?: Type | Function | [Function] | string; 22 | isArray?: boolean; 23 | enum?: SwaggerEnumType; 24 | } 25 | 26 | export type ApiQueryMetadata = 27 | | ApiQueryCommonMetadata 28 | | ({ name: string } & ApiQueryCommonMetadata & Omit) 29 | | ({ 30 | name?: string; 31 | enumName: string; 32 | enumSchema?: EnumSchemaAttributes; 33 | } & ApiQueryCommonMetadata); 34 | 35 | interface ApiQuerySchemaHost extends ParameterOptions { 36 | name?: string; 37 | schema: SchemaObject | ReferenceObject; 38 | } 39 | 40 | export type ApiQueryOptions = ApiQueryMetadata | ApiQuerySchemaHost; 41 | 42 | const defaultQueryOptions = { 43 | name: '', 44 | required: true 45 | }; 46 | 47 | /** 48 | * @publicApi 49 | */ 50 | export function ApiQuery( 51 | options: ApiQueryOptions 52 | ): MethodDecorator & ClassDecorator { 53 | const apiQueryMetadata = options as ApiQueryMetadata; 54 | const [type, isArray] = getTypeIsArrayTuple( 55 | apiQueryMetadata.type, 56 | apiQueryMetadata.isArray 57 | ); 58 | 59 | const param: ApiQueryMetadata & Record = { 60 | name: 'name' in options ? options.name : defaultQueryOptions.name, 61 | in: 'query', 62 | ...omit(options, 'enum'), 63 | type 64 | }; 65 | 66 | if (isEnumArray(options)) { 67 | addEnumArraySchema(param, options); 68 | } else if (isEnumDefined(options)) { 69 | addEnumSchema(param, options); 70 | } 71 | 72 | if (isArray) { 73 | param.isArray = isArray; 74 | } 75 | 76 | return createParamDecorator(param, defaultQueryOptions); 77 | } 78 | -------------------------------------------------------------------------------- /lib/type-helpers/omit-type.helper.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@nestjs/common'; 2 | import { 3 | inheritPropertyInitializers, 4 | inheritTransformationMetadata, 5 | inheritValidationMetadata 6 | } from '@nestjs/mapped-types'; 7 | import { omit } from 'lodash'; 8 | import { DECORATORS } from '../constants'; 9 | import { ApiProperty } from '../decorators'; 10 | import { MetadataLoader } from '../plugin/metadata-loader'; 11 | import { ModelPropertiesAccessor } from '../services/model-properties-accessor'; 12 | import { clonePluginMetadataFactory } from './mapped-types.utils'; 13 | 14 | const modelPropertiesAccessor = new ModelPropertiesAccessor(); 15 | 16 | /** 17 | * @publicApi 18 | */ 19 | export function OmitType( 20 | classRef: Type, 21 | keys: readonly K[] 22 | ): Type> { 23 | const fields = modelPropertiesAccessor 24 | .getModelProperties(classRef.prototype) 25 | .filter((item) => !keys.includes(item as K)); 26 | 27 | const isInheritedPredicate = (propertyKey: string) => 28 | !keys.includes(propertyKey as K); 29 | abstract class OmitTypeClass { 30 | constructor() { 31 | inheritPropertyInitializers(this, classRef, isInheritedPredicate); 32 | } 33 | } 34 | 35 | inheritValidationMetadata(classRef, OmitTypeClass, isInheritedPredicate); 36 | inheritTransformationMetadata(classRef, OmitTypeClass, isInheritedPredicate); 37 | 38 | function applyFields(fields: string[]) { 39 | clonePluginMetadataFactory( 40 | OmitTypeClass as Type, 41 | classRef.prototype, 42 | (metadata: Record) => omit(metadata, keys) 43 | ); 44 | 45 | fields.forEach((propertyKey) => { 46 | const metadata = Reflect.getMetadata( 47 | DECORATORS.API_MODEL_PROPERTIES, 48 | classRef.prototype, 49 | propertyKey 50 | ); 51 | const decoratorFactory = ApiProperty(metadata); 52 | decoratorFactory(OmitTypeClass.prototype, propertyKey); 53 | }); 54 | } 55 | applyFields(fields); 56 | 57 | MetadataLoader.addRefreshHook(() => { 58 | const fields = modelPropertiesAccessor 59 | .getModelProperties(classRef.prototype) 60 | .filter((item) => !keys.includes(item as K)); 61 | 62 | applyFields(fields); 63 | }); 64 | 65 | return OmitTypeClass as Type>; 66 | } 67 | -------------------------------------------------------------------------------- /lib/type-helpers/pick-type.helper.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@nestjs/common'; 2 | import { 3 | inheritPropertyInitializers, 4 | inheritTransformationMetadata, 5 | inheritValidationMetadata 6 | } from '@nestjs/mapped-types'; 7 | import { pick } from 'lodash'; 8 | import { DECORATORS } from '../constants'; 9 | import { ApiProperty } from '../decorators'; 10 | import { MetadataLoader } from '../plugin/metadata-loader'; 11 | import { ModelPropertiesAccessor } from '../services/model-properties-accessor'; 12 | import { clonePluginMetadataFactory } from './mapped-types.utils'; 13 | 14 | const modelPropertiesAccessor = new ModelPropertiesAccessor(); 15 | 16 | /** 17 | * @publicApi 18 | */ 19 | export function PickType( 20 | classRef: Type, 21 | keys: readonly K[] 22 | ): Type> { 23 | const fields = modelPropertiesAccessor 24 | .getModelProperties(classRef.prototype) 25 | .filter((item) => keys.includes(item as K)); 26 | 27 | const isInheritedPredicate = (propertyKey: string) => 28 | keys.includes(propertyKey as K); 29 | 30 | abstract class PickTypeClass { 31 | constructor() { 32 | inheritPropertyInitializers(this, classRef, isInheritedPredicate); 33 | } 34 | } 35 | 36 | inheritValidationMetadata(classRef, PickTypeClass, isInheritedPredicate); 37 | inheritTransformationMetadata(classRef, PickTypeClass, isInheritedPredicate); 38 | 39 | function applyFields(fields: string[]) { 40 | clonePluginMetadataFactory( 41 | PickTypeClass as Type, 42 | classRef.prototype, 43 | (metadata: Record) => pick(metadata, keys) 44 | ); 45 | 46 | fields.forEach((propertyKey) => { 47 | const metadata = Reflect.getMetadata( 48 | DECORATORS.API_MODEL_PROPERTIES, 49 | classRef.prototype, 50 | propertyKey 51 | ); 52 | const decoratorFactory = ApiProperty(metadata); 53 | decoratorFactory(PickTypeClass.prototype, propertyKey); 54 | }); 55 | } 56 | applyFields(fields); 57 | 58 | MetadataLoader.addRefreshHook(() => { 59 | const fields = modelPropertiesAccessor 60 | .getModelProperties(classRef.prototype) 61 | .filter((item) => keys.includes(item as K)); 62 | 63 | applyFields(fields); 64 | }); 65 | 66 | return PickTypeClass as Type>; 67 | } 68 | -------------------------------------------------------------------------------- /lib/plugin/utils/type-reference-to-identifier.util.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | import { PluginOptions } from '../merge-options'; 3 | import { pluginDebugLogger } from '../plugin-debug-logger'; 4 | import { replaceImportPath } from './plugin-utils'; 5 | 6 | export function typeReferenceToIdentifier( 7 | typeReferenceDescriptor: { 8 | typeName: string; 9 | isArray?: boolean; 10 | arrayDepth?: number; 11 | }, 12 | hostFilename: string, 13 | options: PluginOptions, 14 | factory: ts.NodeFactory, 15 | type: ts.Type, 16 | typeImports: Record 17 | ) { 18 | if (options.readonly) { 19 | assertReferenceableType( 20 | type, 21 | typeReferenceDescriptor.typeName, 22 | hostFilename, 23 | options 24 | ); 25 | } 26 | 27 | const { typeReference, importPath, typeName } = replaceImportPath( 28 | typeReferenceDescriptor.typeName, 29 | hostFilename, 30 | options 31 | ); 32 | 33 | let identifier: ts.Identifier; 34 | if (options.readonly && typeReference?.includes('import')) { 35 | if (!typeImports[importPath]) { 36 | typeImports[importPath] = typeReference; 37 | } 38 | 39 | let ref = `t["${importPath}"].${typeName}`; 40 | if (typeReferenceDescriptor.isArray) { 41 | ref = wrapTypeInArray(ref, typeReferenceDescriptor.arrayDepth); 42 | } 43 | identifier = factory.createIdentifier(ref); 44 | } else { 45 | let ref = typeReference; 46 | if (typeReferenceDescriptor.isArray) { 47 | ref = wrapTypeInArray(ref, typeReferenceDescriptor.arrayDepth); 48 | } 49 | identifier = factory.createIdentifier(ref); 50 | } 51 | return identifier; 52 | } 53 | 54 | function wrapTypeInArray(typeRef: string, arrayDepth: number) { 55 | for (let i = 0; i < arrayDepth; i++) { 56 | typeRef = `[${typeRef}]`; 57 | } 58 | return typeRef; 59 | } 60 | 61 | function assertReferenceableType( 62 | type: ts.Type, 63 | parsedTypeName: string, 64 | hostFilename: string, 65 | options: PluginOptions 66 | ) { 67 | if (!type.symbol) { 68 | return true; 69 | } 70 | if (!(type.symbol as any).isReferenced) { 71 | return true; 72 | } 73 | if (parsedTypeName.includes('import')) { 74 | return true; 75 | } 76 | const errorMessage = `Type "${parsedTypeName}" is not referenceable ("${hostFilename}"). To fix this, make sure to export this type.`; 77 | if (options.debug) { 78 | pluginDebugLogger.debug(errorMessage); 79 | } 80 | throw new Error(errorMessage); 81 | } 82 | -------------------------------------------------------------------------------- /test/services/fixtures/create-user.dto.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import { ApiProperty, ApiPropertyOptional } from '../../../lib/decorators'; 3 | import { CreateProfileDto } from './create-profile.dto'; 4 | 5 | class House {} 6 | 7 | export class CreateUserDto { 8 | @ApiProperty() 9 | login: string; 10 | 11 | @ApiProperty({ 12 | example: 'password123' 13 | }) 14 | password: string; 15 | 16 | @ApiPropertyOptional({ 17 | format: 'int64', 18 | example: 10 19 | }) 20 | age?: number; 21 | 22 | @ApiProperty({ 23 | required: false, 24 | readOnly: true, 25 | type: 'array', 26 | maxItems: 10, 27 | minItems: 1, 28 | items: { 29 | type: 'array', 30 | items: { 31 | type: 'number' 32 | } 33 | } 34 | }) 35 | custom: any; 36 | 37 | @ApiProperty({ 38 | description: 'Profile', 39 | nullable: true, 40 | type: () => CreateProfileDto 41 | }) 42 | profile: CreateProfileDto; 43 | 44 | @ApiProperty() 45 | tags: string[]; 46 | 47 | @ApiProperty() 48 | twoDimensionPrimitives: string[][]; 49 | 50 | @ApiProperty({ 51 | type: () => [[CreateProfileDto]] 52 | }) 53 | twoDimensionModels: CreateProfileDto[][]; 54 | 55 | @ApiProperty({ 56 | type: String, 57 | isArray: true, 58 | format: 'uri' 59 | }) 60 | urls: string[]; 61 | 62 | @ApiProperty({ 63 | type: 'integer', 64 | isArray: true 65 | }) 66 | luckyNumbers: number[]; 67 | 68 | @ApiProperty({ 69 | type: 'array', 70 | items: { 71 | type: 'object', 72 | properties: { 73 | isReadonly: { 74 | type: 'string' 75 | } 76 | } 77 | } 78 | }) 79 | options?: Record[]; 80 | 81 | @ApiProperty({ 82 | oneOf: [ 83 | { $ref: '#/components/schemas/Cat' }, 84 | { $ref: '#/components/schemas/Dog' } 85 | ], 86 | discriminator: { propertyName: 'pet_type' } 87 | }) 88 | allOf?: Record; 89 | 90 | @ApiProperty({ type: [House] }) 91 | houses: House[]; 92 | 93 | @ApiProperty() 94 | createdAt: Date; 95 | 96 | @ApiProperty() 97 | amount: bigint; 98 | 99 | @ApiProperty({ type: [String], format: 'uuid' }) 100 | formatArray: string[]; 101 | 102 | static _OPENAPI_METADATA_FACTORY() { 103 | return { 104 | tags: { type: () => [String] }, 105 | twoDimensionPrimitives: { type: () => [[String]] } 106 | }; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /test/plugin/fixtures/app.controller-tabs.ts: -------------------------------------------------------------------------------- 1 | // prettier-ignore 2 | export const appControllerWithTabsText = `import { Controller, Post, HttpStatus } from '@nestjs/common'; 3 | import { ApiOperation } from '@nestjs/swagger'; 4 | 5 | class Cat {} 6 | 7 | @Controller('cats') 8 | export class AppController { 9 | onApplicationBootstrap() {} 10 | 11 | /** 12 | * create a Cat 13 | * 14 | * @returns {Promise} 15 | * @memberof AppController 16 | */ 17 | @Post() 18 | async create(): Promise {} 19 | 20 | /** 21 | * find a Cat 22 | */ 23 | @ApiOperation({}) 24 | @Get() 25 | async findOne(): Promise {} 26 | 27 | /** 28 | * find all Cats im comment 29 | * 30 | * @returns {Promise} 31 | * @memberof AppController 32 | */ 33 | @ApiOperation({ 34 | description: 'find all Cats', 35 | }) 36 | @Get() 37 | @HttpCode(HttpStatus.NO_CONTENT) 38 | async findAll(): Promise {} 39 | }`; 40 | 41 | // prettier-ignore 42 | export const appControllerWithTabsTextTranspiled = `\"use strict\"; 43 | Object.defineProperty(exports, \"__esModule\", { value: true }); 44 | exports.AppController = void 0; 45 | const openapi = require(\"@nestjs/swagger\"); 46 | const common_1 = require(\"@nestjs/common\"); 47 | const swagger_1 = require(\"@nestjs/swagger\"); 48 | class Cat { 49 | } 50 | let AppController = class AppController { 51 | onApplicationBootstrap() { } 52 | /** 53 | * create a Cat 54 | * 55 | * @returns {Promise} 56 | * @memberof AppController 57 | */ 58 | async create() { } 59 | /** 60 | * find a Cat 61 | */ 62 | async findOne() { } 63 | /** 64 | * find all Cats im comment 65 | * 66 | * @returns {Promise} 67 | * @memberof AppController 68 | */ 69 | async findAll() { } 70 | }; 71 | exports.AppController = AppController; 72 | __decorate([ 73 | openapi.ApiOperation({ summary: \"create a Cat\" }), 74 | (0, common_1.Post)(), 75 | openapi.ApiResponse({ status: 201, type: Cat }) 76 | ], AppController.prototype, \"create\", null); 77 | __decorate([ 78 | (0, swagger_1.ApiOperation)({ summary: \"find a Cat\" }), 79 | Get(), 80 | openapi.ApiResponse({ status: 200, type: Cat }) 81 | ], AppController.prototype, \"findOne\", null); 82 | __decorate([ 83 | (0, swagger_1.ApiOperation)({ summary: \"find all Cats im comment\", description: 'find all Cats' }), 84 | Get(), 85 | HttpCode(common_1.HttpStatus.NO_CONTENT), 86 | openapi.ApiResponse({ status: common_1.HttpStatus.NO_CONTENT, type: [Cat] }) 87 | ], AppController.prototype, \"findAll\", null); 88 | exports.AppController = AppController = __decorate([ 89 | (0, common_1.Controller)('cats') 90 | ], AppController); 91 | `; 92 | -------------------------------------------------------------------------------- /lib/type-helpers/intersection-type.helper.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@nestjs/common'; 2 | import { 3 | inheritPropertyInitializers, 4 | inheritTransformationMetadata, 5 | inheritValidationMetadata 6 | } from '@nestjs/mapped-types'; 7 | import { DECORATORS } from '../constants'; 8 | import { ApiProperty } from '../decorators'; 9 | import { MetadataLoader } from '../plugin/metadata-loader'; 10 | import { ModelPropertiesAccessor } from '../services/model-properties-accessor'; 11 | import { clonePluginMetadataFactory } from './mapped-types.utils'; 12 | 13 | const modelPropertiesAccessor = new ModelPropertiesAccessor(); 14 | 15 | type UnionToIntersection = (U extends any ? (k: U) => void : never) extends ( 16 | k: infer I 17 | ) => void 18 | ? I 19 | : never; 20 | 21 | type ClassRefsToConstructors = { 22 | [U in keyof T]: T[U] extends Type ? V : never; 23 | }; 24 | 25 | type Intersection = Type< 26 | UnionToIntersection[number]> 27 | >; 28 | 29 | /** 30 | * @publicApi 31 | */ 32 | export function IntersectionType(...classRefs: T) { 33 | abstract class IntersectionClassType { 34 | constructor() { 35 | classRefs.forEach((classRef) => { 36 | inheritPropertyInitializers(this, classRef); 37 | }); 38 | } 39 | } 40 | 41 | classRefs.forEach((classRef) => { 42 | const fields = modelPropertiesAccessor.getModelProperties( 43 | classRef.prototype 44 | ); 45 | 46 | inheritValidationMetadata(classRef, IntersectionClassType); 47 | inheritTransformationMetadata(classRef, IntersectionClassType); 48 | 49 | function applyFields(fields: string[]) { 50 | clonePluginMetadataFactory( 51 | IntersectionClassType as Type, 52 | classRef.prototype 53 | ); 54 | 55 | fields.forEach((propertyKey) => { 56 | const metadata = Reflect.getMetadata( 57 | DECORATORS.API_MODEL_PROPERTIES, 58 | classRef.prototype, 59 | propertyKey 60 | ); 61 | const decoratorFactory = ApiProperty(metadata); 62 | decoratorFactory(IntersectionClassType.prototype, propertyKey); 63 | }); 64 | } 65 | applyFields(fields); 66 | 67 | MetadataLoader.addRefreshHook(() => { 68 | const fields = modelPropertiesAccessor.getModelProperties( 69 | classRef.prototype 70 | ); 71 | applyFields(fields); 72 | }); 73 | }); 74 | 75 | const intersectedNames = classRefs.reduce((prev, ref) => prev + ref.name, ''); 76 | Object.defineProperty(IntersectionClassType, 'name', { 77 | value: `Intersection${intersectedNames}` 78 | }); 79 | return IntersectionClassType as Intersection; 80 | } 81 | -------------------------------------------------------------------------------- /e2e/src/cats/classes/cat.class.ts: -------------------------------------------------------------------------------- 1 | import { ApiExtension, ApiProperty } from '../../../../lib'; 2 | import { LettersEnum } from '../dto/pagination-query.dto'; 3 | 4 | @ApiExtension('x-schema-extension', { test: 'test' }) 5 | @ApiExtension('x-schema-extension-multiple', { test: 'test*2' }) 6 | export class Cat { 7 | @ApiProperty({ example: 'Kitty', description: 'The name of the Cat' }) 8 | name: string; 9 | 10 | @ApiProperty({ example: 1, minimum: 0, description: 'The age of the Cat' }) 11 | age: number; 12 | 13 | @ApiProperty({ 14 | example: 'Maine Coon', 15 | description: 'The breed of the Cat' 16 | }) 17 | breed: string; 18 | 19 | @ApiProperty({ 20 | name: '_tags', 21 | type: [String] 22 | }) 23 | tags?: string[]; 24 | 25 | @ApiProperty() 26 | createdAt: Date; 27 | 28 | @ApiProperty({ 29 | type: String, 30 | isArray: true 31 | }) 32 | urls?: string[]; 33 | 34 | @ApiProperty({ 35 | name: '_options', 36 | type: 'array', 37 | items: { 38 | type: 'object', 39 | properties: { 40 | isReadonly: { 41 | type: 'string' 42 | } 43 | } 44 | } 45 | }) 46 | options?: Record[]; 47 | 48 | @ApiProperty({ 49 | type: 'object', 50 | properties: { 51 | name: { 52 | type: 'string', 53 | example: 'ErrorName' 54 | }, 55 | status: { 56 | type: 'number', 57 | example: 400 58 | } 59 | }, 60 | required: ['name', 'status'], 61 | selfRequired: true 62 | }) 63 | rawDefinition: Record; 64 | 65 | @ApiProperty({ 66 | type: 'object', 67 | additionalProperties: { type: 'boolean' }, 68 | selfRequired: false 69 | }) 70 | optionalRawDefinition?: Record; 71 | 72 | @ApiProperty({ 73 | enum: LettersEnum 74 | }) 75 | enum: LettersEnum; 76 | 77 | @ApiProperty({ 78 | enum: LettersEnum, 79 | isArray: true 80 | }) 81 | enumArr: LettersEnum[]; 82 | 83 | @ApiProperty({ 84 | enum: LettersEnum, 85 | enumName: 'LettersEnum', 86 | description: 'A small assortment of letters?', 87 | default: 'A', 88 | deprecated: true 89 | }) 90 | enumWithRef: LettersEnum; 91 | 92 | @ApiProperty({ 93 | oneOf: [ 94 | { type: 'array', items: { type: 'string' } }, 95 | { type: 'array', items: { type: 'number' } }, 96 | { type: 'array', items: { type: 'boolean' } } 97 | ], 98 | description: 'Array of values that uses "oneOf"' 99 | }) 100 | oneOfExample?: string[] | number[] | boolean[]; 101 | 102 | @ApiProperty({ type: [String], link: () => Cat }) 103 | kittenIds?: string[]; 104 | } 105 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Nest Logo 3 |

4 | 5 |

A progressive Node.js framework for building efficient and scalable server-side applications.

6 |

7 | NPM Version 8 | Package License 9 | NPM Downloads 10 | CircleCI 11 | Discord 12 | Backers on Open Collective 13 | Sponsors on Open Collective 14 | 15 | 16 |

17 | 19 | 20 | ## Description 21 | 22 | [OpenAPI (Swagger)](https://www.openapis.org/) module for [Nest](https://github.com/nestjs/nest). 23 | 24 | ## Installation 25 | 26 | ```bash 27 | $ npm i --save @nestjs/swagger 28 | ``` 29 | 30 | ## Quick Start 31 | 32 | [Overview & Tutorial](https://docs.nestjs.com/openapi/introduction) 33 | 34 | ## Support 35 | 36 | Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support). 37 | 38 | ## Stay in touch 39 | 40 | - Author - [Kamil Myśliwiec](https://twitter.com/kammysliwiec) 41 | - Website - [https://nestjs.com](https://nestjs.com/) 42 | - Twitter - [@nestframework](https://twitter.com/nestframework) 43 | 44 | ## License 45 | 46 | Nest is [MIT licensed](LICENSE). 47 | -------------------------------------------------------------------------------- /test/plugin/fixtures/nullable.dto.ts: -------------------------------------------------------------------------------- 1 | export const nullableDtoText = ` 2 | enum OneValueEnum { 3 | ONE 4 | } 5 | 6 | enum Status { 7 | ENABLED, 8 | DISABLED 9 | } 10 | 11 | export class NullableDto { 12 | @ApiProperty() 13 | stringValue: string | null; 14 | @ApiProperty() 15 | stringArr: string[] | null; 16 | @ApiProperty() 17 | optionalString?: string; 18 | @ApiProperty() 19 | undefinedString: string | undefined; 20 | @ApiProperty() 21 | nullableEnumValue: OneValueEnum | null; 22 | @ApiProperty() 23 | optionalEnumValue?: OneValueEnum; 24 | @ApiProperty() 25 | undefinedEnumValue: OneValueEnum | undefined; 26 | @ApiProperty() 27 | enumValue: Status | null; 28 | @ApiProperty() 29 | optionalNullableEnumValue?: Status | null; 30 | } 31 | `; 32 | 33 | export const nullableDtoTextTranspiled = `import * as openapi from "@nestjs/swagger"; 34 | var OneValueEnum; 35 | (function (OneValueEnum) { 36 | OneValueEnum[OneValueEnum["ONE"] = 0] = "ONE"; 37 | })(OneValueEnum || (OneValueEnum = {})); 38 | var Status; 39 | (function (Status) { 40 | Status[Status["ENABLED"] = 0] = "ENABLED"; 41 | Status[Status["DISABLED"] = 1] = "DISABLED"; 42 | })(Status || (Status = {})); 43 | export class NullableDto { 44 | static _OPENAPI_METADATA_FACTORY() { 45 | return { stringValue: { required: true, type: () => String, nullable: true }, stringArr: { required: true, type: () => [String], nullable: true }, optionalString: { required: false, type: () => String }, undefinedString: { required: true, type: () => String }, nullableEnumValue: { required: true, nullable: true, enum: OneValueEnum }, optionalEnumValue: { required: false, enum: OneValueEnum }, undefinedEnumValue: { required: true, enum: OneValueEnum }, enumValue: { required: true, nullable: true, enum: Status }, optionalNullableEnumValue: { required: false, nullable: true, enum: Status } }; 46 | } 47 | } 48 | __decorate([ 49 | ApiProperty() 50 | ], NullableDto.prototype, "stringValue", void 0); 51 | __decorate([ 52 | ApiProperty() 53 | ], NullableDto.prototype, "stringArr", void 0); 54 | __decorate([ 55 | ApiProperty() 56 | ], NullableDto.prototype, "optionalString", void 0); 57 | __decorate([ 58 | ApiProperty() 59 | ], NullableDto.prototype, "undefinedString", void 0); 60 | __decorate([ 61 | ApiProperty() 62 | ], NullableDto.prototype, "nullableEnumValue", void 0); 63 | __decorate([ 64 | ApiProperty() 65 | ], NullableDto.prototype, "optionalEnumValue", void 0); 66 | __decorate([ 67 | ApiProperty() 68 | ], NullableDto.prototype, "undefinedEnumValue", void 0); 69 | __decorate([ 70 | ApiProperty() 71 | ], NullableDto.prototype, "enumValue", void 0); 72 | __decorate([ 73 | ApiProperty() 74 | ], NullableDto.prototype, "optionalNullableEnumValue", void 0); 75 | `; 76 | -------------------------------------------------------------------------------- /e2e/src/cats/dto/create-cat.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiExtension, ApiExtraModels, ApiProperty } from '../../../../lib'; 2 | import { XEnumTest } from '../enums/x-enum-test.enum'; 3 | import { ExtraModelDto } from './extra-model.dto'; 4 | import { LettersEnum } from './pagination-query.dto'; 5 | import { TagDto } from './tag.dto'; 6 | 7 | @ApiExtraModels(ExtraModelDto) 8 | @ApiExtension('x-tags', ['foo', 'bar']) 9 | export class CreateCatDto { 10 | @ApiProperty() 11 | readonly name: string; 12 | 13 | @ApiProperty({ minimum: 1, maximum: 200 }) 14 | readonly age: number; 15 | 16 | @ApiProperty({ name: '_breed', type: String }) 17 | readonly breed: string; 18 | 19 | @ApiProperty({ 20 | format: 'uri', 21 | type: [String] 22 | }) 23 | readonly tags?: string[]; 24 | 25 | @ApiProperty() 26 | createdAt: Date; 27 | 28 | @ApiProperty({ 29 | type: 'string', 30 | isArray: true 31 | }) 32 | readonly urls?: string[]; 33 | 34 | @ApiProperty({ 35 | type: 'array', 36 | items: { 37 | type: 'object', 38 | properties: { 39 | isReadonly: { 40 | type: 'string' 41 | } 42 | } 43 | } 44 | }) 45 | readonly options?: Record[]; 46 | 47 | @ApiProperty({ 48 | type: 'object', 49 | properties: { 50 | name: { 51 | type: 'string', 52 | example: 'ErrorName' 53 | }, 54 | status: { 55 | type: 'number', 56 | example: 400 57 | } 58 | }, 59 | required: ['name', 'status'], 60 | selfRequired: true 61 | }) 62 | rawDefinition: Record; 63 | 64 | @ApiProperty({ 65 | description: 'Enum with description' 66 | }) 67 | readonly enumWithDescription: LettersEnum; 68 | 69 | @ApiProperty({ 70 | enum: LettersEnum, 71 | enumName: 'LettersEnum' 72 | }) 73 | readonly enum: LettersEnum; 74 | 75 | @ApiProperty({ 76 | enum: LettersEnum, 77 | enumName: 'LettersEnum', 78 | isArray: true, 79 | description: 'This is a description for the enumArr attribute' 80 | }) 81 | readonly enumArr: LettersEnum[]; 82 | 83 | @ApiProperty({ 84 | enum: LettersEnum, 85 | enumName: 'LettersEnum', 86 | description: 'A small assortment of letters (in DTO)?', 87 | default: 'A', 88 | deprecated: true, 89 | enumSchema: { 90 | description: 'This is a description for the LettersEnum schema', 91 | deprecated: true 92 | } 93 | }) 94 | readonly enumWithRef: LettersEnum; 95 | 96 | @ApiProperty({ description: 'tag', required: false }) 97 | readonly tag: TagDto; 98 | 99 | nested: { 100 | first: string; 101 | second: number; 102 | }; 103 | 104 | @ApiProperty({ 105 | description: 'The x-enumNames test', 106 | enum: XEnumTest, 107 | enumName: 'XEnumTest', 108 | 'x-enumNames': ['APPROVED', 'PENDING', 'REJECTED'] 109 | }) 110 | xEnumTest: XEnumTest; 111 | } 112 | -------------------------------------------------------------------------------- /test/plugin/fixtures/project/cats/dto/create-cat.dto.ts: -------------------------------------------------------------------------------- 1 | import { ConsoleLogger } from '@nestjs/common'; 2 | import { 3 | IsIn, 4 | IsNegative, 5 | IsPositive, 6 | Length, 7 | Matches, 8 | Max, 9 | Min 10 | } from 'class-validator'; 11 | import { randomUUID } from 'node:crypto'; 12 | import { ApiExtraModels, ApiProperty } from '../../../../lib'; 13 | import { ExtraModel } from './extra-model.dto'; 14 | import { LettersEnum } from './pagination-query.dto'; 15 | import { TagDto } from './tag.dto'; 16 | 17 | enum NonExportedEnum { 18 | YES = 'YES', 19 | NO = 'NO' 20 | } 21 | 22 | class NonExportedClass { 23 | prop: string; 24 | } 25 | 26 | export enum CategoryState { 27 | OK = 'OK', 28 | DEPRECATED = 'DEPRECATED' 29 | } 30 | 31 | const MAX_AGE = 200; 32 | 33 | @ApiExtraModels(ExtraModel) 34 | export class CreateCatDto { 35 | @IsIn(['a', 'b']) 36 | isIn: string; 37 | 38 | @Matches(/^[+]?abc$/) 39 | pattern: string; 40 | 41 | @IsPositive() 42 | positive: number = 5; 43 | 44 | @IsNegative() 45 | negative: number = -1; 46 | 47 | @Length(2) 48 | lengthMin: string | null = null; 49 | 50 | @Length(3, 5) 51 | lengthMinMax: string; 52 | 53 | date = new Date(); 54 | 55 | active: boolean = false; 56 | 57 | @ApiProperty() 58 | name: string = randomUUID(); 59 | 60 | @Min(1) 61 | @Max(MAX_AGE) 62 | @ApiProperty({ minimum: 1, maximum: 200 }) 63 | age: number = 14; 64 | 65 | @ApiProperty({ name: '_breed', type: String }) 66 | breed: string = 'Persian'; 67 | 68 | @ApiProperty({ 69 | format: 'uri', 70 | type: [String] 71 | }) 72 | tags?: string[]; 73 | 74 | @ApiProperty() 75 | createdAt: Date; 76 | 77 | @ApiProperty({ 78 | type: 'string', 79 | isArray: true 80 | }) 81 | urls?: string[]; 82 | 83 | @ApiProperty({ 84 | type: 'array', 85 | items: { 86 | type: 'object', 87 | properties: { 88 | isReadonly: { 89 | type: 'string' 90 | } 91 | } 92 | } 93 | }) 94 | options?: Record[]; 95 | 96 | @ApiProperty({ 97 | enum: LettersEnum, 98 | enumName: 'LettersEnum' 99 | }) 100 | enum: LettersEnum; 101 | 102 | /** 103 | * Available language in the application 104 | * @example FR 105 | */ 106 | state?: CategoryState; 107 | 108 | @ApiProperty({ 109 | enum: LettersEnum, 110 | enumName: 'LettersEnum', 111 | isArray: true 112 | }) 113 | enumArr: LettersEnum; 114 | 115 | enumArr2: LettersEnum[]; 116 | 117 | @ApiProperty({ description: 'tag', required: false }) 118 | tag: TagDto; 119 | 120 | multipleTags: TagDto[]; 121 | 122 | nested: { 123 | first: string; 124 | second: number; 125 | }; 126 | 127 | // Both props should be ignored 128 | nonExportedEnum: NonExportedEnum; 129 | nonExportedClass: NonExportedClass; 130 | 131 | // Default value should be ignored 132 | logger = new ConsoleLogger(); 133 | } 134 | -------------------------------------------------------------------------------- /test/plugin/controller-class-visitor.spec.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | import { before } from '../../lib/plugin/compiler-plugin'; 3 | import { 4 | appControllerText, 5 | appControllerTextTranspiled 6 | } from './fixtures/app.controller'; 7 | import { 8 | appControllerWithTabsText, 9 | appControllerWithTabsTextTranspiled 10 | } from './fixtures/app.controller-tabs'; 11 | import { 12 | appControllerWithoutModifiersText, 13 | appControllerWithoutModifiersTextTranspiled 14 | } from './fixtures/app.controller-without-modifiers'; 15 | 16 | describe('Controller methods', () => { 17 | it('should add response based on the return value (spaces)', () => { 18 | const options: ts.CompilerOptions = { 19 | module: ts.ModuleKind.CommonJS, 20 | target: ts.ScriptTarget.ES2021, 21 | newLine: ts.NewLineKind.LineFeed, 22 | noEmitHelpers: true, 23 | experimentalDecorators: true 24 | }; 25 | const filename = 'app.controller.ts'; 26 | const fakeProgram = ts.createProgram([filename], options); 27 | 28 | const result = ts.transpileModule(appControllerText, { 29 | compilerOptions: options, 30 | fileName: filename, 31 | transformers: { 32 | before: [before({ introspectComments: true }, fakeProgram)] 33 | } 34 | }); 35 | expect(result.outputText).toEqual(appControllerTextTranspiled); 36 | }); 37 | 38 | it('should add response based on the return value (tabs)', () => { 39 | const options: ts.CompilerOptions = { 40 | module: ts.ModuleKind.CommonJS, 41 | target: ts.ScriptTarget.ES2021, 42 | newLine: ts.NewLineKind.LineFeed, 43 | noEmitHelpers: true, 44 | experimentalDecorators: true 45 | }; 46 | const filename = 'app.controller.ts'; 47 | const fakeProgram = ts.createProgram([filename], options); 48 | 49 | const result = ts.transpileModule(appControllerWithTabsText, { 50 | compilerOptions: options, 51 | fileName: filename, 52 | transformers: { 53 | before: [before({ introspectComments: true }, fakeProgram)] 54 | } 55 | }); 56 | expect(result.outputText).toEqual(appControllerWithTabsTextTranspiled); 57 | }); 58 | 59 | it('should add response based on the return value (without modifiers)', () => { 60 | const options: ts.CompilerOptions = { 61 | module: ts.ModuleKind.CommonJS, 62 | target: ts.ScriptTarget.ES2021, 63 | newLine: ts.NewLineKind.LineFeed, 64 | noEmitHelpers: true, 65 | experimentalDecorators: true 66 | }; 67 | const filename = 'app.controller.ts'; 68 | const fakeProgram = ts.createProgram([filename], options); 69 | 70 | const result = ts.transpileModule(appControllerWithoutModifiersText, { 71 | compilerOptions: options, 72 | fileName: filename, 73 | transformers: { 74 | before: [before({ introspectComments: true }, fakeProgram)] 75 | } 76 | }); 77 | expect(result.outputText).toEqual( 78 | appControllerWithoutModifiersTextTranspiled 79 | ); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /test/plugin/fixtures/create-cat-alt.dto.ts: -------------------------------------------------------------------------------- 1 | export const createCatDtoAltText = ` 2 | import { IsInt, IsString } from 'class-validator'; 3 | import * as package from 'class-validator'; 4 | 5 | enum Status { 6 | ENABLED, 7 | DISABLED 8 | } 9 | 10 | interface Node { 11 | id: number; 12 | } 13 | 14 | type AliasedType = { 15 | type: string; 16 | }; 17 | type NumberAlias = number; 18 | 19 | export class CreateCatDto2 { 20 | @package.IsString() 21 | name: string; 22 | age: number = 3; 23 | tags: string[]; 24 | status: Status = Status.ENABLED; 25 | readonly breed?: string | undefined; 26 | nodes: Node[]; 27 | alias: AliasedType; 28 | /** NumberAlias */ 29 | numberAlias: NumberAlias; 30 | union: 1 | 2; 31 | intersection: Function & string; 32 | str: string[]; 33 | rawArray: { foo: string }[]; 34 | nested: { 35 | first: string, 36 | second: number, 37 | status: Status, 38 | tags: string[], 39 | nodes: Node[] 40 | alias: AliasedType, 41 | numberAlias: NumberAlias, 42 | }; 43 | prop:{ 44 | [x: string]: string; 45 | } 46 | amount: bigint; 47 | #privateProperty: string; 48 | } 49 | `; 50 | 51 | export const createCatDtoTextAltTranspiled = `var _CreateCatDto2_privateProperty; 52 | import * as openapi from "@nestjs/swagger"; 53 | import * as package from 'class-validator'; 54 | var Status; 55 | (function (Status) { 56 | Status[Status["ENABLED"] = 0] = "ENABLED"; 57 | Status[Status["DISABLED"] = 1] = "DISABLED"; 58 | })(Status || (Status = {})); 59 | export class CreateCatDto2 { 60 | constructor() { 61 | this.age = 3; 62 | this.status = Status.ENABLED; 63 | _CreateCatDto2_privateProperty.set(this, void 0); 64 | } 65 | static _OPENAPI_METADATA_FACTORY() { 66 | return { name: { required: true, type: () => String }, age: { required: true, type: () => Number, default: 3 }, tags: { required: true, type: () => [String] }, status: { required: true, default: Status.ENABLED, enum: Status }, breed: { required: false, type: () => String }, nodes: { required: true, type: () => [Object] }, alias: { required: true, type: () => Object }, numberAlias: { required: true, type: () => Number, description: "NumberAlias" }, union: { required: true, type: () => Object }, intersection: { required: true, type: () => Object }, str: { required: true, type: () => [String] }, rawArray: { required: true, type: () => [({ foo: { required: true, type: () => String } })] }, nested: { required: true, type: () => ({ first: { required: true, type: () => String }, second: { required: true, type: () => Number }, status: { required: true, enum: Status }, tags: { required: true, type: () => [String] }, nodes: { required: true, type: () => [Object] }, alias: { required: true, type: () => Object }, numberAlias: { required: true, type: () => Number } }) }, amount: { required: true, type: () => BigInt } }; 67 | } 68 | } 69 | _CreateCatDto2_privateProperty = new WeakMap(); 70 | __decorate([ 71 | package.IsString() 72 | ], CreateCatDto2.prototype, "name", void 0); 73 | `; 74 | -------------------------------------------------------------------------------- /lib/services/parameter-metadata-accessor.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@nestjs/common'; 2 | import { 3 | PARAMTYPES_METADATA, 4 | ROUTE_ARGS_METADATA 5 | } from '@nestjs/common/constants'; 6 | import { RouteParamtypes } from '@nestjs/common/enums/route-paramtypes.enum'; 7 | import { isEmpty, mapValues, omitBy } from 'lodash'; 8 | import { EnumSchemaAttributes } from '../interfaces/enum-schema-attributes.interface'; 9 | import { 10 | ParameterLocation, 11 | SchemaObject 12 | } from '../interfaces/open-api-spec.interface'; 13 | import { reverseObjectKeys } from '../utils/reverse-object-keys.util'; 14 | 15 | interface ParamMetadata { 16 | index: number; 17 | data?: string | number | object; 18 | } 19 | type ParamsMetadata = Record; 20 | 21 | export interface ParamWithTypeMetadata { 22 | name?: string | number | object; 23 | type?: Type; 24 | in?: ParameterLocation | 'body' | typeof PARAM_TOKEN_PLACEHOLDER; 25 | isArray?: boolean; 26 | items?: SchemaObject; 27 | required?: boolean; 28 | enum?: unknown[]; 29 | enumName?: string; 30 | enumSchema?: EnumSchemaAttributes; 31 | selfRequired?: boolean; 32 | } 33 | export type ParamsWithType = Record; 34 | 35 | const PARAM_TOKEN_PLACEHOLDER = 'placeholder'; 36 | 37 | export class ParameterMetadataAccessor { 38 | explore( 39 | instance: object, 40 | prototype: Type, 41 | method: Function 42 | ): ParamsWithType { 43 | const types: Type[] = Reflect.getMetadata( 44 | PARAMTYPES_METADATA, 45 | instance, 46 | method.name 47 | ); 48 | if (!types?.length) { 49 | return undefined; 50 | } 51 | const routeArgsMetadata: ParamsMetadata = 52 | Reflect.getMetadata( 53 | ROUTE_ARGS_METADATA, 54 | instance.constructor, 55 | method.name 56 | ) || {}; 57 | 58 | const parametersWithType: ParamsWithType = mapValues( 59 | reverseObjectKeys(routeArgsMetadata), 60 | (param: ParamMetadata) => 61 | ({ 62 | type: types[param.index], 63 | name: param.data, 64 | required: true 65 | }) as unknown as ParamsWithType 66 | ); 67 | const excludePredicate = (val: ParamWithTypeMetadata) => 68 | val.in === PARAM_TOKEN_PLACEHOLDER || (val.name && val.in === 'body'); 69 | 70 | const parameters = omitBy( 71 | mapValues(parametersWithType, (val, key) => ({ 72 | ...val, 73 | in: this.mapParamType(key) 74 | })), 75 | excludePredicate as Function 76 | ); 77 | return !isEmpty(parameters) ? (parameters as ParamsWithType) : undefined; 78 | } 79 | 80 | private mapParamType(key: string): string { 81 | const keyPair = key.split(':'); 82 | switch (Number(keyPair[0])) { 83 | case RouteParamtypes.BODY: 84 | return 'body'; 85 | case RouteParamtypes.PARAM: 86 | return 'path'; 87 | case RouteParamtypes.QUERY: 88 | return 'query'; 89 | case RouteParamtypes.HEADERS: 90 | return 'header'; 91 | default: 92 | return PARAM_TOKEN_PLACEHOLDER; 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /lib/swagger-ui/swagger-ui.ts: -------------------------------------------------------------------------------- 1 | import { OpenAPIObject, SwaggerCustomOptions } from '../interfaces'; 2 | import { favIconHtml, htmlTemplateString, jsTemplateString } from './constants'; 3 | import { buildJSInitOptions } from './helpers'; 4 | 5 | /** 6 | * Used to create swagger ui initialization js file ( 7 | */ 8 | export function buildSwaggerInitJS( 9 | swaggerDoc: OpenAPIObject, 10 | customOptions: SwaggerCustomOptions = {} 11 | ) { 12 | const { swaggerOptions = {}, swaggerUrl } = customOptions; 13 | const swaggerInitOptions = { 14 | swaggerDoc, 15 | swaggerUrl, 16 | customOptions: swaggerOptions 17 | }; 18 | 19 | const jsInitOptions = buildJSInitOptions(swaggerInitOptions); 20 | return jsTemplateString.replace('<% swaggerOptions %>', jsInitOptions); 21 | } 22 | 23 | let swaggerAssetsAbsoluteFSPath: string | undefined; 24 | 25 | /** 26 | * Returns the absolute path to swagger-ui assets. 27 | */ 28 | export function getSwaggerAssetsAbsoluteFSPath() { 29 | if (!swaggerAssetsAbsoluteFSPath) { 30 | swaggerAssetsAbsoluteFSPath = require('swagger-ui-dist/absolute-path.js')(); 31 | } 32 | 33 | return swaggerAssetsAbsoluteFSPath; 34 | } 35 | 36 | function toExternalScriptTag(url: string) { 37 | return ``; 38 | } 39 | 40 | function toInlineScriptTag(jsCode: string) { 41 | return ``; 42 | } 43 | 44 | function toExternalStylesheetTag(url: string) { 45 | return ``; 46 | } 47 | 48 | function toTags( 49 | customCode: string | string[] | undefined, 50 | toScript: (url: string) => string 51 | ) { 52 | if (!customCode) { 53 | return ''; 54 | } 55 | 56 | if (typeof customCode === 'string') { 57 | return toScript(customCode); 58 | } else { 59 | return customCode.map(toScript).join('\n'); 60 | } 61 | } 62 | 63 | /** 64 | * Used to build swagger-ui custom html 65 | */ 66 | export function buildSwaggerHTML( 67 | baseUrl: string, 68 | customOptions: SwaggerCustomOptions = {} 69 | ) { 70 | const { 71 | customCss = '', 72 | customJs = '', 73 | customJsStr = '', 74 | customfavIcon = false, 75 | customSiteTitle = 'Swagger UI', 76 | customCssUrl = '', 77 | explorer = false 78 | } = customOptions; 79 | 80 | const favIconString = customfavIcon 81 | ? `` 82 | : favIconHtml; 83 | 84 | const explorerCss = explorer 85 | ? '' 86 | : '.swagger-ui .topbar .download-url-wrapper { display: none }'; 87 | return htmlTemplateString 88 | .replace('<% customCss %>', customCss) 89 | .replace('<% explorerCss %>', explorerCss) 90 | .replace('<% favIconString %>', favIconString) 91 | .replace(/<% baseUrl %>/g, baseUrl) 92 | .replace('<% customJs %>', toTags(customJs, toExternalScriptTag)) 93 | .replace('<% customJsStr %>', toTags(customJsStr, toInlineScriptTag)) 94 | .replace( 95 | '<% customCssUrl %>', 96 | toTags(customCssUrl, toExternalStylesheetTag) 97 | ) 98 | .replace('<% title %>', customSiteTitle); 99 | } 100 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Regression.yml: -------------------------------------------------------------------------------- 1 | name: "\U0001F4A5 Regression" 2 | description: "Report an unexpected behavior while upgrading your Nest application!" 3 | labels: ["needs triage"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | ## :warning: We use GitHub Issues to track bug reports, feature requests and regressions 9 | 10 | If you are not sure that your issue is a bug, you could: 11 | 12 | - use our [Discord community](https://discord.gg/NestJS) 13 | - use [StackOverflow using the tag `nestjs`](https://stackoverflow.com/questions/tagged/nestjs) 14 | - If it's just a quick question you can ping [our Twitter](https://twitter.com/nestframework) 15 | 16 | **NOTE:** You don't need to answer questions that you know that aren't relevant. 17 | 18 | --- 19 | 20 | - type: checkboxes 21 | attributes: 22 | label: "Did you read the migration guide?" 23 | description: "Check out the [migration guide here](https://docs.nestjs.com/migration-guide)!" 24 | options: 25 | - label: "I have read the whole migration guide" 26 | required: false 27 | 28 | - type: checkboxes 29 | attributes: 30 | label: "Is there an existing issue that is already proposing this?" 31 | description: "Please search [here](./?q=is%3Aissue) to see if an issue already exists for the feature you are requesting" 32 | options: 33 | - label: "I have searched the existing issues" 34 | required: true 35 | 36 | - type: input 37 | attributes: 38 | label: "Potential Commit/PR that introduced the regression" 39 | description: "If you have time to investigate, what PR/date/version introduced this issue" 40 | placeholder: "PR #123 or commit 5b3c4a4" 41 | 42 | - type: input 43 | attributes: 44 | label: "Versions" 45 | description: "From which version of `@nestjs/swagger` to which version you are upgrading" 46 | placeholder: "8.1.0 -> 8.1.3" 47 | 48 | - type: textarea 49 | validations: 50 | required: true 51 | attributes: 52 | label: "Describe the regression" 53 | description: "A clear and concise description of what the regression is" 54 | 55 | - type: textarea 56 | attributes: 57 | label: "Minimum reproduction code" 58 | description: | 59 | Please share a git repo, a gist, or step-by-step instructions. [Wtf is a minimum reproduction?](https://jmcdo29.github.io/wtf-is-a-minimum-reproduction) 60 | **Tip:** If you leave a minimum repository, we will understand your issue faster! 61 | value: | 62 | ```ts 63 | 64 | ``` 65 | 66 | - type: textarea 67 | validations: 68 | required: true 69 | attributes: 70 | label: "Expected behavior" 71 | description: "A clear and concise description of what you expected to happend (or code)" 72 | 73 | - type: textarea 74 | attributes: 75 | label: "Other" 76 | description: | 77 | Anything else relevant? eg: Logs, OS version, IDE, package manager, etc. 78 | **Tip:** You can attach images, recordings or log files by clicking this area to highlight it and then dragging files in 79 | -------------------------------------------------------------------------------- /test/plugin/fixtures/es5-class.dto.ts: -------------------------------------------------------------------------------- 1 | export const es5CreateCatDtoText = ` 2 | import { IsInt, IsString } from 'class-validator'; 3 | import { Status } from './status'; 4 | import { CONSTANT_STRING, CONSTANT_OBJECT, MIN_VAL } from './constants'; 5 | 6 | export class CreateCatDtoEs5 { 7 | // field name 8 | name: string = CONSTANT_STRING; 9 | /** status */ 10 | status: Status = Status.ENABLED; 11 | obj = CONSTANT_OBJECT; 12 | 13 | @Min(MIN_VAL) 14 | @Max(10) 15 | age: number = 3; 16 | } 17 | `; 18 | 19 | export const es5CreateCatDtoTextTranspiled = `\"use strict\"; 20 | Object.defineProperty(exports, \"__esModule\", { value: true }); 21 | exports.CreateCatDtoEs5 = void 0; 22 | var openapi = require(\"@nestjs/swagger\"); 23 | var status_1 = require(\"./status\"); 24 | var constants_1 = require(\"./constants\"); 25 | var CreateCatDtoEs5 = /** @class */ (function () { 26 | function CreateCatDtoEs5() { 27 | // field name 28 | this.name = constants_1.CONSTANT_STRING; 29 | /** status */ 30 | this.status = status_1.Status.ENABLED; 31 | this.obj = constants_1.CONSTANT_OBJECT; 32 | this.age = 3; 33 | } 34 | CreateCatDtoEs5._OPENAPI_METADATA_FACTORY = function () { 35 | return { name: { required: true, type: function () { return String; }, default: constants_1.CONSTANT_STRING }, status: { required: true, type: function () { return Object; }, description: "status", default: status_1.Status.ENABLED }, obj: { required: true, type: function () { return Object; }, default: constants_1.CONSTANT_OBJECT }, age: { required: true, type: function () { return Number; }, default: 3, minimum: constants_1.MIN_VAL, maximum: 10 } }; 36 | }; 37 | __decorate([ 38 | Min(constants_1.MIN_VAL), 39 | Max(10) 40 | ], CreateCatDtoEs5.prototype, \"age\", void 0); 41 | return CreateCatDtoEs5; 42 | }()); 43 | exports.CreateCatDtoEs5 = CreateCatDtoEs5; 44 | `; 45 | 46 | export const es5CreateCatDtoTextTranspiledV5 = `\"use strict\"; 47 | Object.defineProperty(exports, \"__esModule\", { value: true }); 48 | exports.CreateCatDtoEs5 = void 0; 49 | var openapi = require(\"@nestjs/swagger\"); 50 | var status_1 = require(\"./status\"); 51 | var constants_1 = require(\"./constants\"); 52 | var CreateCatDtoEs5 = /** @class */ (function () { 53 | function CreateCatDtoEs5() { 54 | // field name 55 | this.name = constants_1.CONSTANT_STRING; 56 | /** status */ 57 | this.status = status_1.Status.ENABLED; 58 | this.obj = constants_1.CONSTANT_OBJECT; 59 | this.age = 3; 60 | } 61 | CreateCatDtoEs5._OPENAPI_METADATA_FACTORY = function () { 62 | return { name: { required: true, type: function () { return String; }, default: constants_1.CONSTANT_STRING }, status: { required: true, type: function () { return Object; }, description: "status", default: status_1.Status.ENABLED }, obj: { required: true, type: function () { return Object; }, default: constants_1.CONSTANT_OBJECT }, age: { required: true, type: function () { return Number; }, default: 3, minimum: constants_1.MIN_VAL, maximum: 10 } }; 63 | }; 64 | __decorate([ 65 | Min(constants_1.MIN_VAL), 66 | Max(10) 67 | ], CreateCatDtoEs5.prototype, \"age\", void 0); 68 | return CreateCatDtoEs5; 69 | }()); 70 | exports.CreateCatDtoEs5 = CreateCatDtoEs5; 71 | `; 72 | -------------------------------------------------------------------------------- /lib/decorators/api-property.decorator.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@nestjs/common'; 2 | import { DECORATORS } from '../constants'; 3 | import { EnumSchemaAttributes } from '../interfaces/enum-schema-attributes.interface'; 4 | import { 5 | EnumAllowedTypes, 6 | SchemaObjectMetadata 7 | } from '../interfaces/schema-object-metadata.interface'; 8 | import { getEnumType, getEnumValues } from '../utils/enum.utils'; 9 | import { createPropertyDecorator, getTypeIsArrayTuple } from './helpers'; 10 | 11 | export type ApiPropertyCommonOptions = SchemaObjectMetadata & { 12 | 'x-enumNames'?: string[]; 13 | /** 14 | * Lazy function returning the type for which the decorated property 15 | * can be used as an id 16 | * 17 | * Use together with @ApiDefaultGetter on the getter route of the type 18 | * to generate OpenAPI link objects 19 | * 20 | * @see [Swagger link objects](https://swagger.io/docs/specification/links/) 21 | */ 22 | link?: () => Type | Function; 23 | }; 24 | 25 | export type ApiPropertyOptions = 26 | | ApiPropertyCommonOptions 27 | | (ApiPropertyCommonOptions & { 28 | enumName: string; 29 | enumSchema?: EnumSchemaAttributes; 30 | }); 31 | 32 | const isEnumArray = ( 33 | opts: ApiPropertyOptions 34 | ): opts is { 35 | isArray: true; 36 | enum: EnumAllowedTypes; 37 | type: any; 38 | items: any; 39 | } => opts.isArray && 'enum' in opts && opts.enum !== undefined; 40 | 41 | /** 42 | * @publicApi 43 | */ 44 | export function ApiProperty( 45 | options: ApiPropertyOptions = {} 46 | ): PropertyDecorator { 47 | return createApiPropertyDecorator(options); 48 | } 49 | 50 | export function createApiPropertyDecorator( 51 | options: ApiPropertyOptions = {}, 52 | overrideExisting = true 53 | ): PropertyDecorator { 54 | const [type, isArray] = getTypeIsArrayTuple(options.type, options.isArray); 55 | options = { 56 | ...options, 57 | type, 58 | isArray 59 | } as ApiPropertyOptions; 60 | 61 | if (isEnumArray(options)) { 62 | options.type = 'array'; 63 | 64 | const enumValues = getEnumValues(options.enum); 65 | options.items = { 66 | type: getEnumType(enumValues), 67 | enum: enumValues 68 | }; 69 | delete options.enum; 70 | } else if ('enum' in options && options.enum !== undefined) { 71 | const enumValues = getEnumValues(options.enum); 72 | 73 | options.enum = enumValues; 74 | options.type = getEnumType(enumValues); 75 | } 76 | 77 | if (Array.isArray(options.type)) { 78 | options.type = 'array'; 79 | options.items = { 80 | type: 'array', 81 | items: { 82 | type: options.type[0] 83 | } 84 | }; 85 | } 86 | 87 | return createPropertyDecorator( 88 | DECORATORS.API_MODEL_PROPERTIES, 89 | options, 90 | overrideExisting 91 | ); 92 | } 93 | 94 | export function ApiPropertyOptional( 95 | options: ApiPropertyOptions = {} 96 | ): PropertyDecorator { 97 | return ApiProperty({ 98 | ...options, 99 | required: false 100 | } as ApiPropertyOptions); 101 | } 102 | 103 | export function ApiResponseProperty( 104 | options: Pick< 105 | ApiPropertyOptions, 106 | 'type' | 'example' | 'format' | 'deprecated' | 'enum' 107 | > = {} 108 | ): PropertyDecorator { 109 | return ApiProperty({ 110 | readOnly: true, 111 | ...options 112 | } as ApiPropertyOptions); 113 | } 114 | -------------------------------------------------------------------------------- /test/plugin/readonly-visitor.spec.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'fs'; 2 | import { join } from 'path'; 3 | import * as ts from 'typescript'; 4 | import { ReadonlyVisitor } from '../../lib/plugin/visitors/readonly.visitor'; 5 | import { PluginMetadataPrinter } from './helpers/metadata-printer'; 6 | 7 | function createTsProgram(tsconfigPath: string) { 8 | const parsedCmd = ts.getParsedCommandLineOfConfigFile( 9 | tsconfigPath, 10 | undefined, 11 | ts.sys as unknown as ts.ParseConfigFileHost 12 | ); 13 | const { options, fileNames: rootNames, projectReferences } = parsedCmd!; 14 | const program = ts.createProgram({ options, rootNames, projectReferences }); 15 | return program; 16 | } 17 | 18 | describe('Readonly visitor', () => { 19 | const visitor = new ReadonlyVisitor({ 20 | pathToSource: join(__dirname, 'fixtures', 'project'), 21 | introspectComments: true, 22 | dtoFileNameSuffix: ['.dto.ts', '.model.ts', '.class.ts'], 23 | classValidatorShim: true, 24 | debug: true 25 | }); 26 | const esmVisitor = new ReadonlyVisitor({ 27 | pathToSource: join(__dirname, 'fixtures', 'project'), 28 | introspectComments: true, 29 | esmCompatible: true, 30 | dtoFileNameSuffix: ['.dto.ts', '.model.ts', '.class.ts'], 31 | classValidatorShim: true, 32 | debug: true 33 | }); 34 | const metadataPrinter = new PluginMetadataPrinter(); 35 | 36 | it('should generate a serialized metadata', () => { 37 | const tsconfigPath = join( 38 | __dirname, 39 | 'fixtures', 40 | 'project', 41 | 'tsconfig.json' 42 | ); 43 | const program = createTsProgram(tsconfigPath); 44 | 45 | for (const sourceFile of program.getSourceFiles()) { 46 | if (!sourceFile.isDeclarationFile) { 47 | visitor.visit(program, sourceFile); 48 | } 49 | } 50 | 51 | const result = metadataPrinter.print( 52 | { 53 | [visitor.key]: visitor.collect() 54 | }, 55 | visitor.typeImports 56 | ); 57 | 58 | const expectedOutput = readFileSync( 59 | join(__dirname, 'fixtures', 'serialized-meta.fixture.ts'), 60 | 'utf-8' 61 | ) 62 | .replace(/\r\n/g, '\n') 63 | .replace(/\r/g, '\n'); 64 | /** Normalize the file line endings to LF */ 65 | 66 | // writeFileSync( 67 | // join(__dirname, 'fixtures', 'serialized-meta.fixture.ts'), 68 | // result, 69 | // 'utf-8' 70 | // ); 71 | 72 | expect(result).toEqual(expectedOutput); 73 | }); 74 | 75 | it('should generate a serialized metadata esm', () => { 76 | const tsconfigPath = join( 77 | __dirname, 78 | 'fixtures', 79 | 'project', 80 | 'tsconfig.json' 81 | ); 82 | const program = createTsProgram(tsconfigPath); 83 | 84 | for (const sourceFile of program.getSourceFiles()) { 85 | if (!sourceFile.isDeclarationFile) { 86 | esmVisitor.visit(program, sourceFile); 87 | } 88 | } 89 | 90 | const result = metadataPrinter.print( 91 | { 92 | [esmVisitor.key]: esmVisitor.collect() 93 | }, 94 | esmVisitor.typeImports 95 | ); 96 | 97 | const expectedOutput = readFileSync( 98 | join(__dirname, 'fixtures', 'serialized-meta-esm.fixture.ts'), 99 | 'utf-8' 100 | ) 101 | .replace(/\r\n/g, '\n') 102 | .replace(/\r/g, '\n'); 103 | 104 | expect(result).toEqual(expectedOutput); 105 | }); 106 | }); 107 | -------------------------------------------------------------------------------- /lib/utils/enum.utils.ts: -------------------------------------------------------------------------------- 1 | import { isString } from 'lodash'; 2 | import { SchemaObject } from '../interfaces/open-api-spec.interface'; 3 | import { SchemaObjectMetadata } from '../interfaces/schema-object-metadata.interface'; 4 | import { SwaggerEnumType } from '../types/swagger-enum.type'; 5 | 6 | export function getEnumValues( 7 | enumType: SwaggerEnumType | (() => SwaggerEnumType) 8 | ): string[] | number[] { 9 | if (typeof enumType === 'function') { 10 | return getEnumValues(enumType()); 11 | } 12 | 13 | if (Array.isArray(enumType)) { 14 | return enumType as string[]; 15 | } 16 | if (typeof enumType !== 'object') { 17 | return []; 18 | } 19 | // Enums with numeric values 20 | // enum Size { 21 | // SMALL = 1, 22 | // BIG = 2 23 | // } 24 | // are transpiled to include a reverse mapping 25 | // const Size = { 26 | // "1": "SMALL", 27 | // "2": "BIG", 28 | // "SMALL": 1, 29 | // "BIG": 2, 30 | // } 31 | const numericValues = Object.values(enumType) 32 | .filter((value) => typeof value === 'number') 33 | .map((value: any) => value.toString()); 34 | 35 | return Object.keys(enumType) 36 | .filter((key) => !numericValues.includes(key)) 37 | .map((key) => enumType[key]); 38 | } 39 | 40 | export function getEnumType(values: (string | number)[]): 'string' | 'number' { 41 | const hasString = values.filter(isString).length > 0; 42 | return hasString ? 'string' : 'number'; 43 | } 44 | 45 | export function addEnumArraySchema( 46 | paramDefinition: Partial< 47 | Record<'schema' | 'isArray' | 'enumName' | 'enumSchema', any> 48 | >, 49 | decoratorOptions: Partial> 50 | ) { 51 | const paramSchema: SchemaObject = paramDefinition.schema || {}; 52 | paramDefinition.schema = paramSchema; 53 | paramSchema.type = 'array'; 54 | delete paramDefinition.isArray; 55 | 56 | const enumValues = getEnumValues(decoratorOptions.enum); 57 | paramSchema.items = { 58 | type: getEnumType(enumValues), 59 | enum: enumValues 60 | }; 61 | 62 | if (decoratorOptions.enumName) { 63 | paramDefinition.enumName = decoratorOptions.enumName; 64 | } 65 | 66 | if (decoratorOptions.enumSchema) { 67 | paramDefinition.enumSchema = decoratorOptions.enumSchema; 68 | } 69 | } 70 | 71 | export function addEnumSchema( 72 | paramDefinition: Partial>, 73 | decoratorOptions: Partial> 74 | ) { 75 | const paramSchema: SchemaObject = paramDefinition.schema || {}; 76 | const enumValues = getEnumValues(decoratorOptions.enum); 77 | 78 | paramDefinition.schema = paramSchema; 79 | paramSchema.enum = enumValues; 80 | paramSchema.type = getEnumType(enumValues); 81 | 82 | if (decoratorOptions.enumName) { 83 | paramDefinition.enumName = decoratorOptions.enumName; 84 | } 85 | 86 | if (decoratorOptions.enumSchema) { 87 | paramDefinition.enumSchema = decoratorOptions.enumSchema; 88 | } 89 | } 90 | 91 | export const isEnumArray = >>( 92 | obj: Record 93 | ): obj is T => obj.isArray && obj.enum; 94 | 95 | export const isEnumDefined = >>( 96 | obj: Record 97 | ): obj is T => obj.enum; 98 | 99 | export const isEnumMetadata = (metadata: SchemaObjectMetadata) => 100 | metadata.enum || (metadata.isArray && metadata.items?.['enum']); 101 | -------------------------------------------------------------------------------- /lib/type-helpers/partial-type.helper.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@nestjs/common'; 2 | import { 3 | applyIsOptionalDecorator, 4 | applyValidateIfDefinedDecorator, 5 | inheritPropertyInitializers, 6 | inheritTransformationMetadata, 7 | inheritValidationMetadata 8 | } from '@nestjs/mapped-types'; 9 | import { mapValues } from 'lodash'; 10 | import { DECORATORS } from '../constants'; 11 | import { ApiProperty } from '../decorators'; 12 | import { MetadataLoader } from '../plugin/metadata-loader'; 13 | import { METADATA_FACTORY_NAME } from '../plugin/plugin-constants'; 14 | import { ModelPropertiesAccessor } from '../services/model-properties-accessor'; 15 | import { clonePluginMetadataFactory } from './mapped-types.utils'; 16 | 17 | const modelPropertiesAccessor = new ModelPropertiesAccessor(); 18 | 19 | /** 20 | * @publicApi 21 | */ 22 | export function PartialType( 23 | classRef: Type, 24 | /** 25 | * Configuration options. 26 | */ 27 | options: { 28 | /** 29 | * If true, validations will be ignored on a property if it is either null or undefined. If 30 | * false, validations will be ignored only if the property is undefined. 31 | * @default true 32 | */ 33 | skipNullProperties?: boolean; 34 | } = {} 35 | ): Type> { 36 | const applyPartialDecoratorFn = 37 | options.skipNullProperties === false 38 | ? applyValidateIfDefinedDecorator 39 | : applyIsOptionalDecorator; 40 | 41 | const fields = modelPropertiesAccessor.getModelProperties(classRef.prototype); 42 | 43 | abstract class PartialTypeClass { 44 | constructor() { 45 | inheritPropertyInitializers(this, classRef); 46 | } 47 | } 48 | const keysWithValidationConstraints = inheritValidationMetadata( 49 | classRef, 50 | PartialTypeClass 51 | ); 52 | if (keysWithValidationConstraints) { 53 | keysWithValidationConstraints 54 | .filter((key) => !fields.includes(key)) 55 | .forEach((key) => applyPartialDecoratorFn(PartialTypeClass, key)); 56 | } 57 | 58 | inheritTransformationMetadata(classRef, PartialTypeClass); 59 | 60 | function applyFields(fields: string[]) { 61 | clonePluginMetadataFactory( 62 | PartialTypeClass as Type, 63 | classRef.prototype, 64 | (metadata: Record) => 65 | mapValues(metadata, (item) => ({ ...item, required: false })) 66 | ); 67 | 68 | if (PartialTypeClass[METADATA_FACTORY_NAME]) { 69 | const pluginFields = Object.keys( 70 | PartialTypeClass[METADATA_FACTORY_NAME]() 71 | ); 72 | pluginFields.forEach((key) => 73 | applyPartialDecoratorFn(PartialTypeClass, key) 74 | ); 75 | } 76 | 77 | fields.forEach((key) => { 78 | const metadata = 79 | Reflect.getMetadata( 80 | DECORATORS.API_MODEL_PROPERTIES, 81 | classRef.prototype, 82 | key 83 | ) || {}; 84 | 85 | const decoratorFactory = ApiProperty({ 86 | ...metadata, 87 | required: false 88 | }); 89 | decoratorFactory(PartialTypeClass.prototype, key); 90 | applyPartialDecoratorFn(PartialTypeClass, key); 91 | }); 92 | } 93 | applyFields(fields); 94 | 95 | MetadataLoader.addRefreshHook(() => { 96 | const fields = modelPropertiesAccessor.getModelProperties( 97 | classRef.prototype 98 | ); 99 | applyFields(fields); 100 | }); 101 | 102 | return PartialTypeClass as Type>; 103 | } 104 | -------------------------------------------------------------------------------- /lib/explorers/api-parameters.explorer.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@nestjs/common'; 2 | import { assign, find, isNil, map, omitBy, some, unionWith } from 'lodash'; 3 | import { DECORATORS } from '../constants'; 4 | import { SchemaObject } from '../interfaces/open-api-spec.interface'; 5 | import { ModelPropertiesAccessor } from '../services/model-properties-accessor'; 6 | import { 7 | ParameterMetadataAccessor, 8 | ParamWithTypeMetadata 9 | } from '../services/parameter-metadata-accessor'; 10 | import { ParametersMetadataMapper } from '../services/parameters-metadata-mapper'; 11 | import { SchemaObjectFactory } from '../services/schema-object-factory'; 12 | import { SwaggerTypesMapper } from '../services/swagger-types-mapper'; 13 | import { GlobalParametersStorage } from '../storages/global-parameters.storage'; 14 | 15 | const parameterMetadataAccessor = new ParameterMetadataAccessor(); 16 | const modelPropertiesAccessor = new ModelPropertiesAccessor(); 17 | const parametersMetadataMapper = new ParametersMetadataMapper( 18 | modelPropertiesAccessor 19 | ); 20 | const swaggerTypesMapper = new SwaggerTypesMapper(); 21 | const schemaObjectFactory = new SchemaObjectFactory( 22 | modelPropertiesAccessor, 23 | swaggerTypesMapper 24 | ); 25 | 26 | export const exploreApiParametersMetadata = ( 27 | schemas: Record, 28 | instance: object, 29 | prototype: Type, 30 | method: Function 31 | ) => { 32 | const explicitParameters: any[] = Reflect.getMetadata( 33 | DECORATORS.API_PARAMETERS, 34 | method 35 | ); 36 | const globalParameters = GlobalParametersStorage.getAll(); 37 | const parametersMetadata = parameterMetadataAccessor.explore( 38 | instance, 39 | prototype, 40 | method 41 | ); 42 | const noExplicitAndGlobalMetadata = 43 | isNil(explicitParameters) && isNil(globalParameters); 44 | if (noExplicitAndGlobalMetadata && isNil(parametersMetadata)) { 45 | return undefined; 46 | } 47 | const reflectedParametersAsProperties = 48 | parametersMetadataMapper.transformModelToProperties( 49 | parametersMetadata || {} 50 | ); 51 | 52 | let properties = reflectedParametersAsProperties; 53 | if (!noExplicitAndGlobalMetadata) { 54 | const mergeImplicitAndExplicit = (item: ParamWithTypeMetadata) => 55 | assign(item, find(explicitParameters, ['name', item.name])); 56 | 57 | properties = removeBodyMetadataIfExplicitExists( 58 | properties, 59 | explicitParameters 60 | ); 61 | properties = map(properties, mergeImplicitAndExplicit); 62 | properties = unionWith( 63 | properties, 64 | explicitParameters, 65 | globalParameters, 66 | (arrVal, othVal) => { 67 | return arrVal.name === othVal.name && arrVal.in === othVal.in; 68 | } 69 | ); 70 | } 71 | 72 | const paramsWithDefinitions = schemaObjectFactory.createFromModel( 73 | properties, 74 | schemas 75 | ); 76 | const parameters = swaggerTypesMapper.mapParamTypes(paramsWithDefinitions); 77 | return parameters ? { parameters } : undefined; 78 | }; 79 | 80 | function removeBodyMetadataIfExplicitExists( 81 | properties: ParamWithTypeMetadata[], 82 | explicitParams: any[] 83 | ) { 84 | const isBodyReflected = some(properties, (p) => p.in === 'body'); 85 | const isBodyDefinedExplicitly = some(explicitParams, (p) => p.in === 'body'); 86 | if (isBodyReflected && isBodyDefinedExplicitly) { 87 | return omitBy( 88 | properties, 89 | (p) => p.in === 'body' 90 | ) as ParamWithTypeMetadata[]; 91 | } 92 | return properties; 93 | } 94 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Bug_report.yml: -------------------------------------------------------------------------------- 1 | name: "\U0001F41B Bug Report" 2 | description: "If something isn't working as expected \U0001F914" 3 | labels: ["needs triage", "bug"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | ## :warning: We use GitHub Issues to track bug reports, feature requests and regressions 9 | 10 | If you are not sure that your issue is a bug, you could: 11 | 12 | - use our [Discord community](https://discord.gg/NestJS) 13 | - use [StackOverflow using the tag `nestjs`](https://stackoverflow.com/questions/tagged/nestjs) 14 | - If it's just a quick question you can ping [our Twitter](https://twitter.com/nestframework) 15 | 16 | **NOTE:** You don't need to answer questions that you know that aren't relevant. 17 | 18 | --- 19 | 20 | - type: checkboxes 21 | attributes: 22 | label: "Is there an existing issue for this?" 23 | description: "Please search [here](./?q=is%3Aissue) to see if an issue already exists for the bug you encountered" 24 | options: 25 | - label: "I have searched the existing issues" 26 | required: true 27 | 28 | - type: textarea 29 | validations: 30 | required: true 31 | attributes: 32 | label: "Current behavior" 33 | description: "How the issue manifests?" 34 | 35 | - type: input 36 | validations: 37 | required: true 38 | attributes: 39 | label: "Minimum reproduction code" 40 | description: "An URL to some git repository or gist that reproduces this issue. [Wtf is a minimum reproduction?](https://jmcdo29.github.io/wtf-is-a-minimum-reproduction)" 41 | placeholder: "https://github.com/..." 42 | 43 | - type: textarea 44 | attributes: 45 | label: "Steps to reproduce" 46 | description: | 47 | How the issue manifests? 48 | You could leave this blank if you alread write this in your reproduction code/repo 49 | placeholder: | 50 | 1. `npm i` 51 | 2. `npm start:dev` 52 | 3. See error... 53 | 54 | - type: textarea 55 | validations: 56 | required: true 57 | attributes: 58 | label: "Expected behavior" 59 | description: "A clear and concise description of what you expected to happend (or code)" 60 | 61 | - type: markdown 62 | attributes: 63 | value: | 64 | --- 65 | 66 | - type: input 67 | validations: 68 | required: true 69 | attributes: 70 | label: "Package version" 71 | description: | 72 | Which version of `@nestjs/swagger` are you using? 73 | **Tip**: Make sure that all of yours `@nestjs/*` dependencies are in sync! 74 | placeholder: "8.1.3" 75 | 76 | - type: input 77 | attributes: 78 | label: "NestJS version" 79 | description: "Which version of `@nestjs/core` are you using?" 80 | placeholder: "8.1.3" 81 | 82 | - type: input 83 | attributes: 84 | label: "Node.js version" 85 | description: "Which version of Node.js are you using?" 86 | placeholder: "14.17.6" 87 | 88 | - type: checkboxes 89 | attributes: 90 | label: "In which operating systems have you tested?" 91 | options: 92 | - label: macOS 93 | - label: Windows 94 | - label: Linux 95 | 96 | - type: markdown 97 | attributes: 98 | value: | 99 | --- 100 | 101 | - type: textarea 102 | attributes: 103 | label: "Other" 104 | description: | 105 | Anything else relevant? eg: Logs, OS version, IDE, package manager, etc. 106 | **Tip:** You can attach images, recordings or log files by clicking this area to highlight it and then dragging files in 107 | -------------------------------------------------------------------------------- /test/plugin/fixtures/create-cat-alt2.dto.ts: -------------------------------------------------------------------------------- 1 | export const createCatDtoAlt2Text = ` 2 | import { CreateDateColumn, UpdateDateColumn, VersionColumn } from 'typeorm'; 3 | 4 | export abstract class Audit { 5 | /** test on createdAt */ 6 | @CreateDateColumn() 7 | createdAt; 8 | 9 | // commentedOutProperty: string; 10 | 11 | // test on updatedAt1 12 | // test on updatedAt2 13 | @UpdateDateColumn() 14 | updatedAt; 15 | 16 | /** 17 | * test 18 | * version 19 | * @example 'version 123' 20 | * @example ignore this 21 | * @memberof Audit 22 | */ 23 | @VersionColumn() 24 | version: string 25 | 26 | /** 27 | * testVersion 28 | * 29 | * @example '0.0.1' 30 | * @example '0.0.2' 31 | * @deprecated 32 | * @memberof Audit 33 | */ 34 | testVersion; 35 | 36 | /** 37 | * testVersion2 38 | * 39 | * @example '0.0.1' 40 | * @example '0.0.2' 41 | * @deprecated Use version instead 42 | * @memberof Audit 43 | */ 44 | testVersion2; 45 | 46 | /** 47 | * testVersionArray 48 | * 49 | * @example ['0.0.1', '0.0.2'] 50 | * @memberof Audit 51 | */ 52 | testVersionArray: string[]; 53 | 54 | /** 55 | * testVersionArray 56 | * 57 | * @example ['version 123', 'version 321'] 58 | * @memberof Audit 59 | */ 60 | testVersionArray2: string[]; 61 | 62 | /** 63 | * testVersionArray 64 | * 65 | * @example [123, 321] 66 | * @memberof Audit 67 | */ 68 | testVersionArray3: number[]; 69 | 70 | /** 71 | * testBoolean 72 | * 73 | * @example true 74 | */ 75 | testBoolean: boolean; 76 | 77 | /** 78 | * testNumber 79 | * 80 | * @example 1.0 81 | * @example 5 82 | */ 83 | testNumber: number; 84 | 85 | /** 86 | * privateProperty 87 | * @example 'secret' 88 | */ 89 | #privateProperty: string; 90 | } 91 | `; 92 | 93 | export const createCatDtoTextAlt2Transpiled = `var _Audit_privateProperty; 94 | import * as openapi from "@nestjs/swagger"; 95 | import { CreateDateColumn, UpdateDateColumn, VersionColumn } from 'typeorm'; 96 | export class Audit { 97 | constructor() { 98 | /** 99 | * privateProperty 100 | * @example 'secret' 101 | */ 102 | _Audit_privateProperty.set(this, void 0); 103 | } 104 | static _OPENAPI_METADATA_FACTORY() { 105 | return { createdAt: { required: true, type: () => Object, description: "test on createdAt" }, updatedAt: { required: true, type: () => Object }, version: { required: true, type: () => String, description: "test\\nversion", example: "version 123" }, testVersion: { required: true, type: () => Object, description: "testVersion", examples: ["0.0.1", "0.0.2"], deprecated: true }, testVersion2: { required: true, type: () => Object, description: "testVersion2", examples: ["0.0.1", "0.0.2"], deprecated: true }, testVersionArray: { required: true, type: () => [String], description: "testVersionArray", example: ["0.0.1", "0.0.2"] }, testVersionArray2: { required: true, type: () => [String], description: "testVersionArray", example: ["version 123", "version 321"] }, testVersionArray3: { required: true, type: () => [Number], description: "testVersionArray", example: [123, 321] }, testBoolean: { required: true, type: () => Boolean, description: "testBoolean", example: true }, testNumber: { required: true, type: () => Number, description: "testNumber", examples: [1, 5] } }; 106 | } 107 | } 108 | _Audit_privateProperty = new WeakMap(); 109 | __decorate([ 110 | CreateDateColumn() 111 | ], Audit.prototype, "createdAt", void 0); 112 | __decorate([ 113 | UpdateDateColumn() 114 | ], Audit.prototype, "updatedAt", void 0); 115 | __decorate([ 116 | VersionColumn() 117 | ], Audit.prototype, "version", void 0); 118 | `; 119 | -------------------------------------------------------------------------------- /test/plugin/fixtures/create-cat-exclusive.dto.ts: -------------------------------------------------------------------------------- 1 | export const createCatExclusiveDtoText = ` 2 | import { IsInt, IsString, IsPositive, IsNegative, Length, Matches, IsIn } from 'class-validator'; 3 | 4 | enum Status { 5 | ENABLED, 6 | DISABLED 7 | } 8 | 9 | enum OneValueEnum { 10 | ONE 11 | } 12 | 13 | interface Node { 14 | id: number; 15 | } 16 | 17 | class OtherNode { 18 | id: number; 19 | } 20 | 21 | export class CreateCatDto { 22 | @IsIn(['a', 'b']) 23 | isIn: string; 24 | @Matches(/^[+]?abc$/) 25 | pattern: string; 26 | name: string; 27 | @Min(0) 28 | @Max(10) 29 | age: number = 3; 30 | @IsPositive() 31 | positive: number = 5; 32 | @IsNegative() 33 | negative: number = -1; 34 | @Length(2) 35 | lengthMin: string; 36 | @Length(3, 5) 37 | lengthMinMax: string; 38 | tags: string[]; 39 | status: Status = Status.ENABLED; 40 | status2?: Status; 41 | statusArr?: Status[]; 42 | oneValueEnum?: OneValueEnum; 43 | oneValueEnumArr?: OneValueEnum[]; 44 | 45 | /** this is breed im comment */ 46 | @ApiProperty({ description: "this is breed", type: String }) 47 | @IsString() 48 | readonly breed?: string; 49 | 50 | nodes: Node[]; 51 | optionalBoolean?: boolean; 52 | date: Date; 53 | 54 | twoDimensionPrimitives: string[][]; 55 | twoDimensionNodes: OtherNode[][]; 56 | 57 | @ApiHideProperty() 58 | hidden: number; 59 | 60 | @Exclude() 61 | excluded: number; 62 | 63 | @Expose() 64 | exposed: number; 65 | 66 | static staticProperty: string; 67 | } 68 | `; 69 | 70 | export const createCatExclusiveDtoTextTranspiled = `import * as openapi from "@nestjs/swagger"; 71 | import { IsString, IsPositive, IsNegative, Length, Matches, IsIn } from 'class-validator'; 72 | var Status; 73 | (function (Status) { 74 | Status[Status[\"ENABLED\"] = 0] = \"ENABLED\"; 75 | Status[Status[\"DISABLED\"] = 1] = \"DISABLED\"; 76 | })(Status || (Status = {})); 77 | var OneValueEnum; 78 | (function (OneValueEnum) { 79 | OneValueEnum[OneValueEnum[\"ONE\"] = 0] = \"ONE\"; 80 | })(OneValueEnum || (OneValueEnum = {})); 81 | class OtherNode { 82 | static _OPENAPI_METADATA_FACTORY() { 83 | return {}; 84 | } 85 | } 86 | export class CreateCatDto { 87 | constructor() { 88 | this.age = 3; 89 | this.positive = 5; 90 | this.negative = -1; 91 | this.status = Status.ENABLED; 92 | } 93 | static _OPENAPI_METADATA_FACTORY() { 94 | return { breed: { required: false, type: () => String, title: "this is breed im comment" }, exposed: { required: true, type: () => Number } }; 95 | } 96 | } 97 | __decorate([ 98 | IsIn(['a', 'b']) 99 | ], CreateCatDto.prototype, \"isIn\", void 0); 100 | __decorate([ 101 | Matches(/^[+]?abc$/) 102 | ], CreateCatDto.prototype, \"pattern\", void 0); 103 | __decorate([ 104 | Min(0), 105 | Max(10) 106 | ], CreateCatDto.prototype, \"age\", void 0); 107 | __decorate([ 108 | IsPositive() 109 | ], CreateCatDto.prototype, \"positive\", void 0); 110 | __decorate([ 111 | IsNegative() 112 | ], CreateCatDto.prototype, \"negative\", void 0); 113 | __decorate([ 114 | Length(2) 115 | ], CreateCatDto.prototype, \"lengthMin\", void 0); 116 | __decorate([ 117 | Length(3, 5) 118 | ], CreateCatDto.prototype, \"lengthMinMax\", void 0); 119 | __decorate([ 120 | ApiProperty({ description: "this is breed", type: String }), 121 | IsString() 122 | ], CreateCatDto.prototype, \"breed\", void 0); 123 | __decorate([ 124 | ApiHideProperty() 125 | ], CreateCatDto.prototype, \"hidden\", void 0); 126 | __decorate([ 127 | Exclude() 128 | ], CreateCatDto.prototype, "excluded", void 0); 129 | __decorate([ 130 | Expose() 131 | ], CreateCatDto.prototype, "exposed", void 0); 132 | `; 133 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nestjs/swagger", 3 | "version": "11.2.3", 4 | "description": "Nest - modern, fast, powerful node.js web framework (@swagger)", 5 | "author": "Kamil Mysliwiec", 6 | "license": "MIT", 7 | "repository": "https://github.com/nestjs/swagger", 8 | "main": "dist/index.js", 9 | "types": "dist/index.d.ts", 10 | "scripts": { 11 | "build": "tsc -p tsconfig.build.json", 12 | "format": "prettier \"lib/**/*.ts\" --write", 13 | "lint": "eslint 'lib/**/*.ts' --fix", 14 | "prepublish:next": "npm run build", 15 | "publish:next": "npm publish --access public --tag next", 16 | "prepublish:npm": "npm run build", 17 | "publish:npm": "npm publish --access public", 18 | "prepare": "npm run build", 19 | "test": "jest", 20 | "test:dev": "jest --watch", 21 | "test:e2e": "jest --config e2e/jest-e2e.json", 22 | "prerelease": "npm run build", 23 | "release": "release-it", 24 | "---manual-tests---": "", 25 | "start": "nest start", 26 | "start:dev": "nest start --watch", 27 | "start:debug": "nest start --watch --debug" 28 | }, 29 | "dependencies": { 30 | "@microsoft/tsdoc": "0.16.0", 31 | "@nestjs/mapped-types": "2.1.0", 32 | "js-yaml": "4.1.1", 33 | "lodash": "4.17.21", 34 | "path-to-regexp": "8.3.0", 35 | "swagger-ui-dist": "5.30.2" 36 | }, 37 | "devDependencies": { 38 | "@commitlint/cli": "20.2.0", 39 | "@commitlint/config-angular": "20.2.0", 40 | "@eslint/eslintrc": "3.3.3", 41 | "@eslint/js": "9.39.2", 42 | "@fastify/static": "8.3.0", 43 | "@nestjs/common": "11.1.9", 44 | "@nestjs/core": "11.1.9", 45 | "@nestjs/platform-express": "11.1.9", 46 | "@nestjs/platform-fastify": "11.1.9", 47 | "@types/jest": "30.0.0", 48 | "@types/js-yaml": "4.0.9", 49 | "@types/lodash": "4.17.21", 50 | "@types/node": "24.10.4", 51 | "class-transformer": "0.5.1", 52 | "class-validator": "0.14.3", 53 | "eslint": "9.39.2", 54 | "eslint-config-prettier": "10.1.8", 55 | "eslint-plugin-prettier": "5.5.4", 56 | "express": "5.2.1", 57 | "fastify": "5.6.2", 58 | "globals": "16.5.0", 59 | "husky": "9.1.7", 60 | "jest": "30.2.0", 61 | "lerna-changelog": "2.2.0", 62 | "lint-staged": "16.2.7", 63 | "openapi-types": "12.1.3", 64 | "prettier": "3.7.4", 65 | "prettier-v2": "npm:prettier@2.8.8", 66 | "reflect-metadata": "0.2.2", 67 | "release-it": "19.1.0", 68 | "supertest": "7.1.4", 69 | "swagger-parser": "10.0.3", 70 | "ts-jest": "29.4.6", 71 | "typescript": "5.9.3", 72 | "typescript-eslint": "8.50.0" 73 | }, 74 | "peerDependencies": { 75 | "@fastify/static": "^8.0.0", 76 | "@nestjs/common": "^11.0.1", 77 | "@nestjs/core": "^11.0.1", 78 | "class-transformer": "*", 79 | "class-validator": "*", 80 | "reflect-metadata": "^0.1.12 || ^0.2.0" 81 | }, 82 | "peerDependenciesMeta": { 83 | "@fastify/static": { 84 | "optional": true 85 | }, 86 | "class-transformer": { 87 | "optional": true 88 | }, 89 | "class-validator": { 90 | "optional": true 91 | } 92 | }, 93 | "lint-staged": { 94 | "*.ts": [ 95 | "prettier --write", 96 | "git add -f" 97 | ] 98 | }, 99 | "husky": { 100 | "hooks": { 101 | "pre-commit": "lint-staged", 102 | "commit-msg": "commitlint -c .commitlintrc.json -E HUSKY_GIT_PARAMS" 103 | } 104 | }, 105 | "changelog": { 106 | "labels": { 107 | "feature": "Features", 108 | "bug": "Bug fixes", 109 | "enhancement": "Enhancements", 110 | "docs": "Docs", 111 | "dependencies": "Dependencies", 112 | "type: code style": "Code style tweaks", 113 | "breaking change": "Breaking changes" 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /lib/interfaces/swagger-custom-options.interface.ts: -------------------------------------------------------------------------------- 1 | import { OpenAPIObject } from './open-api-spec.interface'; 2 | import { SwaggerUiOptions } from './swagger-ui-options.interface'; 3 | 4 | /** 5 | * @publicApi 6 | */ 7 | export interface SwaggerCustomOptions { 8 | /** 9 | * If `true`, Swagger resources paths will be prefixed by the global prefix set through `setGlobalPrefix()`. 10 | * Default: `false`. 11 | * @see https://docs.nestjs.com/faq/global-prefix 12 | */ 13 | useGlobalPrefix?: boolean; 14 | 15 | /** 16 | * If `false`, the Swagger UI will not be served. Only API definitions (JSON and YAML) 17 | * will be accessible (on `/{path}-json` and `/{path}-yaml`). To fully disable both the Swagger UI and API definitions, use `raw: false`. 18 | * Default: `true`. 19 | * @deprecated Use `ui` instead. 20 | */ 21 | swaggerUiEnabled?: boolean; 22 | 23 | /** 24 | * If `false`, the Swagger UI will not be served. Only API definitions (JSON and YAML) 25 | * will be accessible (on `/{path}-json` and `/{path}-yaml`). To fully disable both the Swagger UI and API definitions, use `raw: false`. 26 | * Default: `true`. 27 | */ 28 | ui?: boolean; 29 | 30 | /** 31 | * If `true`, raw definitions for all formats will be served. 32 | * Alternatively, you can pass an array to specify the formats to be served, e.g., `raw: ['json']` to serve only JSON definitions. 33 | * If omitted or set to an empty array, no definitions (JSON or YAML) will be served. 34 | * Use this option to control the availability of Swagger-related endpoints. 35 | * Default: `true`. 36 | */ 37 | raw?: boolean | Array<'json' | 'yaml'>; 38 | 39 | /** 40 | * Url point the API definition to load in Swagger UI. 41 | */ 42 | swaggerUrl?: string; 43 | 44 | /** 45 | * Path of the JSON API definition to serve. 46 | * Default: `{{path}}-json`. 47 | */ 48 | jsonDocumentUrl?: string; 49 | 50 | /** 51 | * Path of the YAML API definition to serve. 52 | * Default: `{{path}}-json`. 53 | */ 54 | yamlDocumentUrl?: string; 55 | 56 | /** 57 | * Hook allowing to alter the OpenAPI document before being served. 58 | * It's called after the document is generated and before it is served as JSON & YAML. 59 | */ 60 | patchDocumentOnRequest?: ( 61 | req: TRequest, 62 | res: TResponse, 63 | document: OpenAPIObject 64 | ) => OpenAPIObject; 65 | 66 | /** 67 | * If `true`, the selector of OpenAPI definitions is displayed in the Swagger UI interface. 68 | * Default: `false`. 69 | */ 70 | explorer?: boolean; 71 | 72 | /** 73 | * Additional Swagger UI options 74 | */ 75 | swaggerOptions?: SwaggerUiOptions; 76 | 77 | /** 78 | * Custom CSS styles to inject in Swagger UI page. 79 | */ 80 | customCss?: string; 81 | 82 | /** 83 | * URL(s) of a custom CSS stylesheet to load in Swagger UI page. 84 | */ 85 | customCssUrl?: string | string[]; 86 | 87 | /** 88 | * URL(s) of custom JavaScript files to load in Swagger UI page. 89 | */ 90 | customJs?: string | string[]; 91 | 92 | /** 93 | * Custom JavaScript scripts to load in Swagger UI page. 94 | */ 95 | customJsStr?: string | string[]; 96 | 97 | /** 98 | * Custom favicon for Swagger UI page. 99 | */ 100 | customfavIcon?: string; 101 | 102 | /** 103 | * Custom title for Swagger UI page. 104 | */ 105 | customSiteTitle?: string; 106 | 107 | /** 108 | * File system path (ex: ./node_modules/swagger-ui-dist) containing static Swagger UI assets. 109 | */ 110 | customSwaggerUiPath?: string; 111 | 112 | /** 113 | * @deprecated This property has no effect. 114 | */ 115 | validatorUrl?: string; 116 | 117 | /** 118 | * @deprecated This property has no effect. 119 | */ 120 | url?: string; 121 | 122 | /** 123 | * @deprecated This property has no effect. 124 | */ 125 | urls?: Record<'url' | 'name', string>[]; 126 | } 127 | -------------------------------------------------------------------------------- /lib/services/decorators-properties.ts: -------------------------------------------------------------------------------- 1 | export enum decoratorsPropertiesMappingType { 2 | DIRECT, 3 | INDIRECT_VALUE, 4 | INDIRECT_ARGUMENT 5 | } 6 | export const decoratorsProperties = [ 7 | { 8 | mappingType: decoratorsPropertiesMappingType.DIRECT, 9 | decorator: 'Min', 10 | property: 'minimum', 11 | value: undefined 12 | }, 13 | { 14 | mappingType: decoratorsPropertiesMappingType.DIRECT, 15 | decorator: 'Max', 16 | property: 'maximum', 17 | value: undefined 18 | }, 19 | { 20 | mappingType: decoratorsPropertiesMappingType.DIRECT, 21 | decorator: 'MinLength', 22 | property: 'minLength', 23 | value: undefined 24 | }, 25 | { 26 | mappingType: decoratorsPropertiesMappingType.DIRECT, 27 | decorator: 'MaxLength', 28 | property: 'maxLength', 29 | value: undefined 30 | }, 31 | { 32 | mappingType: decoratorsPropertiesMappingType.INDIRECT_VALUE, 33 | decorator: 'ArrayNotEmpty', 34 | property: 'minItems', 35 | value: 1 36 | }, 37 | { 38 | mappingType: decoratorsPropertiesMappingType.INDIRECT_VALUE, 39 | decorator: 'IsPositive', 40 | property: 'minimum', 41 | value: 1 42 | }, 43 | { 44 | mappingType: decoratorsPropertiesMappingType.INDIRECT_VALUE, 45 | decorator: 'IsNegative', 46 | property: 'maximum', 47 | value: -1 48 | }, 49 | { 50 | mappingType: decoratorsPropertiesMappingType.INDIRECT_VALUE, 51 | decorator: 'ArrayUnique', 52 | property: 'uniqueItems', 53 | value: true 54 | }, 55 | { 56 | mappingType: decoratorsPropertiesMappingType.INDIRECT_VALUE, 57 | decorator: 'IsBase64', 58 | property: 'format', 59 | value: 'base64' 60 | }, 61 | { 62 | mappingType: decoratorsPropertiesMappingType.INDIRECT_VALUE, 63 | decorator: 'IsCreditCard', 64 | property: 'format', 65 | value: 'credit-card' 66 | }, 67 | { 68 | mappingType: decoratorsPropertiesMappingType.INDIRECT_VALUE, 69 | decorator: 'IsCurrency', 70 | property: 'format', 71 | value: 'currency' 72 | }, 73 | { 74 | mappingType: decoratorsPropertiesMappingType.INDIRECT_VALUE, 75 | decorator: 'IsEmail', 76 | property: 'format', 77 | value: 'email' 78 | }, 79 | { 80 | mappingType: decoratorsPropertiesMappingType.INDIRECT_VALUE, 81 | decorator: 'IsJSON', 82 | property: 'format', 83 | value: 'json' 84 | }, 85 | { 86 | mappingType: decoratorsPropertiesMappingType.INDIRECT_VALUE, 87 | decorator: 'IsUrl', 88 | property: 'format', 89 | value: 'uri' 90 | }, 91 | { 92 | mappingType: decoratorsPropertiesMappingType.INDIRECT_VALUE, 93 | decorator: 'IsUUID', 94 | property: 'format', 95 | value: 'uuid' 96 | }, 97 | { 98 | mappingType: decoratorsPropertiesMappingType.INDIRECT_VALUE, 99 | decorator: 'IsMobilePhone', 100 | property: 'format', 101 | value: 'mobile-phone' 102 | }, 103 | { 104 | mappingType: decoratorsPropertiesMappingType.INDIRECT_VALUE, 105 | decorator: 'IsAscii', 106 | property: 'pattern', 107 | value: '^[\\x00-\\x7F]+$' 108 | }, 109 | { 110 | mappingType: decoratorsPropertiesMappingType.INDIRECT_VALUE, 111 | decorator: 'IsHexColor', 112 | property: 'pattern', 113 | value: '^#?([0-9A-F]{3}|[0-9A-F]{4}|[0-9A-F]{6}|[0-9A-F]{8})$' 114 | }, 115 | { 116 | mappingType: decoratorsPropertiesMappingType.INDIRECT_VALUE, 117 | decorator: 'IsHexadecimal', 118 | property: 'pattern', 119 | value: '^(0x|0h)?[0-9A-F]+$' 120 | }, 121 | { 122 | mappingType: decoratorsPropertiesMappingType.INDIRECT_ARGUMENT, 123 | decorator: 'ArrayMinSize', 124 | property: 'minItems', 125 | value: undefined 126 | }, 127 | { 128 | mappingType: decoratorsPropertiesMappingType.INDIRECT_ARGUMENT, 129 | decorator: 'ArrayMaxSize', 130 | property: 'maxItems', 131 | value: undefined 132 | }, 133 | { 134 | mappingType: decoratorsPropertiesMappingType.INDIRECT_ARGUMENT, 135 | decorator: 'IsDivisibleBy', 136 | property: 'multipleOf', 137 | value: undefined 138 | }, 139 | { 140 | mappingType: decoratorsPropertiesMappingType.INDIRECT_ARGUMENT, 141 | decorator: 'Contains', 142 | property: 'pattern', 143 | value: undefined 144 | } 145 | ]; 146 | -------------------------------------------------------------------------------- /test/type-helpers/partial-type.helper.spec.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@nestjs/common'; 2 | import { validate } from 'class-validator'; 3 | import { DECORATORS } from '../../lib/constants'; 4 | import { MetadataLoader } from '../../lib/plugin/metadata-loader'; 5 | import { ModelPropertiesAccessor } from '../../lib/services/model-properties-accessor'; 6 | import { PartialType } from '../../lib/type-helpers'; 7 | import { CreateUserDto } from './fixtures/create-user-dto.fixture'; 8 | import { SERIALIZED_METADATA } from './fixtures/serialized-metadata.fixture'; 9 | 10 | class UpdateUserDto extends PartialType(CreateUserDto) {} 11 | 12 | describe('PartialType', () => { 13 | const metadataLoader = new MetadataLoader(); 14 | 15 | let modelPropertiesAccessor: ModelPropertiesAccessor; 16 | 17 | beforeEach(() => { 18 | modelPropertiesAccessor = new ModelPropertiesAccessor(); 19 | }); 20 | 21 | describe('Validation metadata', () => { 22 | it('should apply @IsOptional to properties reflected by the plugin', async () => { 23 | const updateDto = new UpdateUserDto(); 24 | updateDto.firstName = null; 25 | const validationErrors = await validate(updateDto); 26 | expect(validationErrors).toHaveLength(0); 27 | }); 28 | 29 | it('should apply @IsOptional to properties reflected by the plugin if option `skipNullProperties` is true', async () => { 30 | class UpdateUserWithNullableDto extends PartialType(CreateUserDto, { 31 | skipNullProperties: true 32 | }) {} 33 | const updateDto = new UpdateUserWithNullableDto(); 34 | updateDto.firstName = null; 35 | const validationErrors = await validate(updateDto); 36 | expect(validationErrors).toHaveLength(0); 37 | }); 38 | 39 | it('should apply @IsOptional to properties reflected by the plugin if option `skipNullProperties` is undefined', async () => { 40 | class UpdateUserWithoutNullableDto extends PartialType( 41 | CreateUserDto, 42 | {} 43 | ) {} 44 | const updateDto = new UpdateUserWithoutNullableDto(); 45 | updateDto.firstName = null; 46 | const validationErrors = await validate(updateDto); 47 | expect(validationErrors).toHaveLength(0); 48 | }); 49 | 50 | it('should apply @ValidateIf to properties reflected by the plugin if option `skipNullProperties` is false', async () => { 51 | class UpdateUserWithoutNullableDto extends PartialType(CreateUserDto, { 52 | skipNullProperties: false 53 | }) {} 54 | const updateDto = new UpdateUserWithoutNullableDto(); 55 | updateDto.firstName = null; 56 | const validationErrors = await validate(updateDto); 57 | expect(validationErrors).toHaveLength(1); 58 | expect(validationErrors[0].constraints).toEqual({ 59 | isString: 'firstName must be a string' 60 | }); 61 | }); 62 | }); 63 | describe('OpenAPI metadata', () => { 64 | it('should return partial class', async () => { 65 | await metadataLoader.load(SERIALIZED_METADATA); 66 | 67 | const prototype = UpdateUserDto.prototype as any as Type; 68 | 69 | modelPropertiesAccessor.applyMetadataFactory(prototype); 70 | expect(modelPropertiesAccessor.getModelProperties(prototype)).toEqual([ 71 | 'login', 72 | 'password', 73 | 'firstName', 74 | 'lastName', 75 | 'active', 76 | 'role' 77 | ]); 78 | }); 79 | 80 | it('should set "required" option to "false" for each property', () => { 81 | const classRef = UpdateUserDto.prototype as any as Type; 82 | const keys = modelPropertiesAccessor.getModelProperties(classRef); 83 | const metadata = keys.map((key) => { 84 | return Reflect.getMetadata( 85 | DECORATORS.API_MODEL_PROPERTIES, 86 | classRef, 87 | key 88 | ); 89 | }); 90 | 91 | expect(metadata[0]).toEqual({ 92 | isArray: false, 93 | required: false, 94 | type: String 95 | }); 96 | expect(metadata[1]).toEqual({ 97 | isArray: false, 98 | required: false, 99 | minLength: 10, 100 | type: String 101 | }); 102 | expect(metadata[2]).toEqual({ 103 | isArray: false, 104 | required: false, 105 | type: String 106 | }); 107 | }); 108 | }); 109 | }); 110 | -------------------------------------------------------------------------------- /test/plugin/helpers/metadata-printer.ts: -------------------------------------------------------------------------------- 1 | import * as prettier from 'prettier-v2'; 2 | import * as ts from 'typescript'; 3 | 4 | export class PluginMetadataPrinter { 5 | print( 6 | metadata: Record>>, 7 | typeImports: Record 8 | ) { 9 | const objectLiteralExpr = ts.factory.createObjectLiteralExpression( 10 | Object.keys(metadata).map((key) => 11 | this.recursivelyCreatePropertyAssignment( 12 | key, 13 | metadata[key] as unknown as Array<[ts.CallExpression, any]> 14 | ) 15 | ) 16 | ); 17 | const exportAssignment = ts.factory.createExportAssignment( 18 | undefined, 19 | undefined, 20 | ts.factory.createArrowFunction( 21 | [ts.factory.createToken(ts.SyntaxKind.AsyncKeyword)], 22 | undefined, 23 | [], 24 | undefined, 25 | ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), 26 | ts.factory.createBlock( 27 | [ 28 | ts.factory.createVariableStatement( 29 | undefined, 30 | ts.factory.createVariableDeclarationList( 31 | [ 32 | ts.factory.createVariableDeclaration( 33 | ts.factory.createIdentifier('t'), 34 | undefined, 35 | undefined, 36 | ts.factory.createObjectLiteralExpression( 37 | Object.keys(typeImports).map((ti) => 38 | this.createPropertyAssignment(ti, typeImports[ti]) 39 | ), 40 | true 41 | ) 42 | ) 43 | ], 44 | ts.NodeFlags.Const | 45 | ts.NodeFlags.AwaitContext | 46 | ts.NodeFlags.ContextFlags | 47 | ts.NodeFlags.TypeExcludesFlags 48 | ) 49 | ), 50 | ts.factory.createReturnStatement(objectLiteralExpr) 51 | ], 52 | true 53 | ) 54 | ) 55 | ); 56 | 57 | const printer = ts.createPrinter({ 58 | newLine: ts.NewLineKind.LineFeed 59 | }); 60 | const resultFile = ts.createSourceFile( 61 | 'file.ts', 62 | '', 63 | ts.ScriptTarget.Latest, 64 | /*setParentNodes*/ false, 65 | ts.ScriptKind.TS 66 | ); 67 | const output = printer.printNode( 68 | ts.EmitHint.Unspecified, 69 | exportAssignment, 70 | resultFile 71 | ); 72 | return ( 73 | `// @ts-nocheck\n` + 74 | prettier.format(output, { 75 | parser: 'typescript', 76 | singleQuote: true, 77 | trailingComma: 'none' 78 | }) 79 | ); 80 | } 81 | 82 | private createPropertyAssignment(identifier: string, target: string) { 83 | return ts.factory.createPropertyAssignment( 84 | ts.factory.createComputedPropertyName( 85 | ts.factory.createStringLiteral(identifier) 86 | ), 87 | ts.factory.createIdentifier(target) 88 | ); 89 | } 90 | 91 | private recursivelyCreatePropertyAssignment( 92 | identifier: string, 93 | meta: any | Array<[ts.CallExpression, any]> 94 | ): ts.PropertyAssignment { 95 | if (Array.isArray(meta)) { 96 | return ts.factory.createPropertyAssignment( 97 | ts.factory.createStringLiteral(identifier), 98 | ts.factory.createArrayLiteralExpression( 99 | meta.map(([importExpr, meta]) => 100 | ts.factory.createArrayLiteralExpression([ 101 | importExpr, 102 | ts.factory.createObjectLiteralExpression( 103 | Object.keys(meta).map((key) => 104 | this.recursivelyCreatePropertyAssignment(key, meta[key]) 105 | ) 106 | ) 107 | ]) 108 | ) 109 | ) 110 | ); 111 | } 112 | return ts.factory.createPropertyAssignment( 113 | ts.factory.createStringLiteral(identifier), 114 | ts.isObjectLiteralExpression(meta as unknown as ts.Node) 115 | ? (meta as ts.ObjectLiteralExpression) 116 | : ts.factory.createObjectLiteralExpression( 117 | Object.keys(meta).map((key) => 118 | this.recursivelyCreatePropertyAssignment(key, meta[key]) 119 | ) 120 | ) 121 | ); 122 | } 123 | } 124 | --------------------------------------------------------------------------------