Repository: nestjs/swagger
Pull request #2684 (2023-10-31 00:26:24)

feat(@nestjs/swagger): Support extensions in Document and Schema objects
Closes https://github.com/nestjs/swagger/issues/2683

## PR Checklist
Please check if your PR fulfills the following requirements:

- [x] The commit message follows our guidelines: https://github.com/nestjs/nest/blob/master/CONTRIBUTING.md
- [x] Tests for the changes have been added (for bug fixes / features)
- [ ] Docs have been added / updated (for bug fixes / features)


## PR Type
What kind of change does this PR introduce?

<!-- Please check the one that applies to this PR using "x". -->
- [ ] Bugfix
- [x] Feature
- [ ] Code style update (formatting, local variables)
- [ ] Refactoring (no functional changes, no api changes)
- [ ] Build related changes
- [ ] CI related changes
- [ ] Other... Please describe:

## What is the current behavior?
<!-- Please describe the current behavior that you are modifying, or link to a relevant issue. -->

Issue Number: #2683

## What is the new behavior?
It adds a `DocumentBuilder.addExtension()` method and allows the existing `ApiExtension` annotation to be used on Schema objects.

## Does this PR introduce a breaking change?
- [ ] Yes
- [x] No

<!-- If this PR contains a breaking change, please describe the impact and migration path for existing applications below. -->


## Other information



Patch:
diff --git a/lib/document-builder.ts b/lib/document-builder.ts
index b6fb3cc3..442b1284 100644
--- a/lib/document-builder.ts
+++ b/lib/document-builder.ts
@@ -1,5 +1,5 @@
 import { Logger } from '@nestjs/common';
-import { isString, isUndefined, negate, pickBy } from 'lodash';
+import { clone, isString, isUndefined, negate, pickBy } from 'lodash';
 import { buildDocumentBase } from './fixtures/document.base';
 import { OpenAPIObject } from './interfaces';
 import {
@@ -85,6 +85,18 @@ export class DocumentBuilder {
     return this;
   }
 
+  public addExtension(extensionKey: string, extensionProperties: any): this {
+    if (!extensionKey.startsWith('x-')) {
+      throw new Error(
+        'Extension key is not prefixed. Please ensure you prefix it with `x-`.'
+      );
+    }
+
+    this.document[extensionKey] = clone(extensionProperties);
+
+    return this;
+  }
+
   public addSecurity(name: string, options: SecuritySchemeObject): this {
     this.document.components.securitySchemes = {
       ...(this.document.components.securitySchemes || {}),
diff --git a/lib/services/schema-object-factory.ts b/lib/services/schema-object-factory.ts
index a9ef89cd..bf42e9d7 100644
--- a/lib/services/schema-object-factory.ts
+++ b/lib/services/schema-object-factory.ts
@@ -204,11 +204,13 @@ export class SchemaObjectFactory {
     if (!propertiesWithType) {
       return '';
     }
+    const extensionProperties = Reflect.getMetadata(DECORATORS.API_EXTENSION, type) || {};
     const typeDefinition: SchemaObject = {
       type: 'object',
       properties: mapValues(keyBy(propertiesWithType, 'name'), (property) =>
         omit(property, ['name', 'isArray', 'required', 'enumName'])
-      ) as Record<string, SchemaObject | ReferenceObject>
+      ) as Record<string, SchemaObject | ReferenceObject>,
+      ...extensionProperties
     };
     const typeDefinitionRequiredFields = propertiesWithType
       .filter((property) => property.required != false)


Test patch:
diff --git a/e2e/api-spec.json b/e2e/api-spec.json
index de904cb9..99f19ab4 100644
--- a/e2e/api-spec.json
+++ b/e2e/api-spec.json
@@ -1085,6 +1085,10 @@
             ]
           }
         },
+        "x-tags": [
+          "foo",
+          "bar"
+        ],
         "required": [
           "name",
           "age",
diff --git a/e2e/src/cats/dto/create-cat.dto.ts b/e2e/src/cats/dto/create-cat.dto.ts
index 01ea7f6e..a250cc60 100644
--- a/e2e/src/cats/dto/create-cat.dto.ts
+++ b/e2e/src/cats/dto/create-cat.dto.ts
@@ -1,9 +1,10 @@
-import { ApiExtraModels, ApiProperty } from '../../../../lib';
+import { ApiExtension, ApiExtraModels, ApiProperty } from '../../../../lib';
 import { ExtraModel } from './extra-model.dto';
 import { LettersEnum } from './pagination-query.dto';
 import { TagDto } from './tag.dto';
 
 @ApiExtraModels(ExtraModel)
+@ApiExtension('x-tags', ['foo', 'bar'])
 export class CreateCatDto {
   @ApiProperty()
   readonly name: string;
diff --git a/test/services/schema-object-factory.spec.ts b/test/services/schema-object-factory.spec.ts
index f8126359..2a754673 100644
--- a/test/services/schema-object-factory.spec.ts
+++ b/test/services/schema-object-factory.spec.ts
@@ -1,4 +1,4 @@
-import { ApiProperty } from '../../lib/decorators';
+import { ApiExtension, ApiProperty } from '../../lib/decorators';
 import { SchemasObject } from '../../lib/interfaces/open-api-spec.interface';
 import { ModelPropertiesAccessor } from '../../lib/services/model-properties-accessor';
 import { SchemaObjectFactory } from '../../lib/services/schema-object-factory';
@@ -297,6 +297,20 @@ describe('SchemaObjectFactory', () => {
         properties: { name: { type: 'string', minLength: 1 } }
       });
     });
+
+    it('should include extension properties', () => {
+      @ApiExtension('x-test', 'value')
+      class CreatUserDto {
+        @ApiProperty({ minLength: 0, required: true })
+        name: string;
+      }
+
+      const schemas: Record<string, SchemasObject> = {};
+
+      schemaObjectFactory.exploreModelSchema(CreatUserDto, schemas);
+
+      expect(schemas[CreatUserDto.name]['x-test']).toEqual('value');
+    });
   });
 
   describe('createEnumSchemaType', () => {