Repository: nestjs/swagger
Pull request #677 (2020-04-16 13:27:23)

feat(@nestjs/swagger): add `IntersectionType` helper
see nestjs/graphql#776

## 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)
- [x] 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: N/A


## What is the new behavior?


## 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/type-helpers/index.ts b/lib/type-helpers/index.ts
index fba5b5df..dbd86ed6 100644
--- a/lib/type-helpers/index.ts
+++ b/lib/type-helpers/index.ts
@@ -1,3 +1,4 @@
+export * from './intersection-type.helper';
 export * from './omit-type.helper';
 export * from './partial-type.helper';
 export * from './pick-type.helper';
diff --git a/lib/type-helpers/intersection-type.helper.ts b/lib/type-helpers/intersection-type.helper.ts
new file mode 100644
index 00000000..df61b0e0
--- /dev/null
+++ b/lib/type-helpers/intersection-type.helper.ts
@@ -0,0 +1,53 @@
+import { Type } from '@nestjs/common';
+import { DECORATORS } from '../constants';
+import { ApiProperty } from '../decorators';
+import { ModelPropertiesAccessor } from '../services/model-properties-accessor';
+import {
+  inheritTransformationMetadata,
+  inheritValidationMetadata
+} from './type-helpers.utils';
+
+const modelPropertiesAccessor = new ModelPropertiesAccessor();
+
+export function IntersectionType<A, B>(
+  classARef: Type<A>,
+  classBRef: Type<B>
+): Type<A & B> {
+  const fieldsOfA = modelPropertiesAccessor.getModelProperties(
+    classARef.prototype
+  );
+  const fieldsOfB = modelPropertiesAccessor.getModelProperties(
+    classBRef.prototype
+  );
+
+  abstract class IntersectionTypeClass {}
+  inheritValidationMetadata(classARef, IntersectionTypeClass);
+  inheritTransformationMetadata(classARef, IntersectionTypeClass);
+  inheritValidationMetadata(classBRef, IntersectionTypeClass);
+  inheritTransformationMetadata(classBRef, IntersectionTypeClass);
+
+  fieldsOfA.forEach((propertyKey) => {
+    const metadata = Reflect.getMetadata(
+      DECORATORS.API_MODEL_PROPERTIES,
+      classARef.prototype,
+      propertyKey
+    );
+    const decoratorFactory = ApiProperty(metadata);
+    decoratorFactory(IntersectionTypeClass.prototype, propertyKey);
+  });
+
+  fieldsOfB.forEach((propertyKey) => {
+    const metadata = Reflect.getMetadata(
+      DECORATORS.API_MODEL_PROPERTIES,
+      classBRef.prototype,
+      propertyKey
+    );
+    const decoratorFactory = ApiProperty(metadata);
+    decoratorFactory(IntersectionTypeClass.prototype, propertyKey);
+  });
+
+  Object.defineProperty(IntersectionTypeClass, 'name', {
+    value: `Intersection${classARef.name}${classBRef.name}`
+  });
+  return IntersectionTypeClass as Type<A & B>;
+}


Test patch:
diff --git a/test/type-helpers/intersection-type.helper.spec.ts b/test/type-helpers/intersection-type.helper.spec.ts
new file mode 100644
index 00000000..a5c3646d
--- /dev/null
+++ b/test/type-helpers/intersection-type.helper.spec.ts
@@ -0,0 +1,89 @@
+import { Type } from '@nestjs/common';
+import { classToClass, Expose, Transform } from 'class-transformer';
+import { IsString, validate } from 'class-validator';
+import { ApiProperty } from '../../lib/decorators';
+import { ModelPropertiesAccessor } from '../../lib/services/model-properties-accessor';
+import { IntersectionType } from '../../lib/type-helpers';
+import { getValidationMetadataByTarget } from './type-helpers.test-utils';
+
+describe('IntersectionType', () => {
+  class CreateUserDto {
+    @ApiProperty({ required: true })
+    login: string;
+
+    @Expose()
+    @Transform((str) => str + '_transformed')
+    @IsString()
+    @ApiProperty({ minLength: 10 })
+    password: string;
+  }
+
+  class UserDto {
+    @IsString()
+    @ApiProperty({ required: false })
+    firstName: string;
+  }
+
+  class UpdateUserDto extends IntersectionType(UserDto, CreateUserDto) {}
+
+  let modelPropertiesAccessor: ModelPropertiesAccessor;
+
+  beforeEach(() => {
+    modelPropertiesAccessor = new ModelPropertiesAccessor();
+  });
+
+  describe('OpenAPI metadata', () => {
+    it('should return combined class', () => {
+      expect(
+        modelPropertiesAccessor.getModelProperties(
+          (UpdateUserDto.prototype as any) as Type<any>
+        )
+      ).toEqual(['firstName', 'login', 'password']);
+    });
+  });
+  describe('Validation metadata', () => {
+    it('should inherit metadata with all properties', () => {
+      const validationKeys = getValidationMetadataByTarget(UpdateUserDto).map(
+        (item) => item.propertyName
+      );
+      expect(validationKeys).toEqual(['firstName', 'password']);
+    });
+    describe('when object does not fulfil validation rules', () => {
+      it('"validate" should return validation errors', async () => {
+        const updateDto = new UpdateUserDto();
+        updateDto.password = 1234567 as any;
+        updateDto.firstName = 'test';
+
+        const validationErrors = await validate(updateDto);
+
+        expect(validationErrors.length).toEqual(1);
+        expect(validationErrors[0].constraints).toEqual({
+          isString: 'password must be a string'
+        });
+      });
+    });
+    describe('otherwise', () => {
+      it('"validate" should return an empty array', async () => {
+        const updateDto = new UpdateUserDto();
+        updateDto.login = '1234567891011';
+        updateDto.password = '1234567891011';
+        updateDto.firstName = '1234567891011';
+
+        const validationErrors = await validate(updateDto);
+        expect(validationErrors.length).toEqual(0);
+      });
+    });
+  });
+
+  describe('Transformer metadata', () => {
+    it('should inherit transformer metadata', () => {
+      const password = '1234567891011';
+      const updateDto = new UpdateUserDto();
+      updateDto.password = password;
+      updateDto.firstName = 'test';
+
+      const transformedDto = classToClass(updateDto);
+      expect(transformedDto.password).toEqual(password + '_transformed');
+    });
+  });
+});