Skip to content

Commit

Permalink
feat(): inherit property initializers
Browse files Browse the repository at this point in the history
  • Loading branch information
kamilmysliwiec committed Aug 20, 2020
1 parent 8828473 commit 565e256
Show file tree
Hide file tree
Showing 9 changed files with 95 additions and 25 deletions.
8 changes: 7 additions & 1 deletion lib/intersection-type.helper.ts
@@ -1,5 +1,6 @@
import { Type } from '@nestjs/common';
import {
inheritPropertyInitializers,
inheritTransformationMetadata,
inheritValidationMetadata,
} from './type-helpers.utils';
Expand All @@ -8,7 +9,12 @@ export function IntersectionType<A, B>(
classARef: Type<A>,
classBRef: Type<B>,
): Type<A & B> {
abstract class IntersectionClassType {}
abstract class IntersectionClassType {
constructor() {
inheritPropertyInitializers(this, classARef);
inheritPropertyInitializers(this, classBRef);
}
}

inheritValidationMetadata(classARef, IntersectionClassType);
inheritValidationMetadata(classBRef, IntersectionClassType);
Expand Down
10 changes: 8 additions & 2 deletions lib/omit-type.helper.ts
@@ -1,5 +1,6 @@
import { Type } from '@nestjs/common';
import {
inheritPropertyInitializers,
inheritTransformationMetadata,
inheritValidationMetadata,
} from './type-helpers.utils';
Expand All @@ -8,10 +9,15 @@ export function OmitType<T, K extends keyof T>(
classRef: Type<T>,
keys: readonly K[],
): Type<Omit<T, typeof keys[number]>> {
abstract class OmitClassType {}

const isInheritedPredicate = (propertyKey: string) =>
!keys.includes(propertyKey as K);

abstract class OmitClassType {
constructor() {
inheritPropertyInitializers(this, classRef, isInheritedPredicate);
}
}

inheritValidationMetadata(classRef, OmitClassType, isInheritedPredicate);
inheritTransformationMetadata(classRef, OmitClassType, isInheritedPredicate);

Expand Down
9 changes: 7 additions & 2 deletions lib/partial-type.helper.ts
@@ -1,18 +1,23 @@
import { Type } from '@nestjs/common';
import {
applyIsOptionalDecorator,
inheritPropertyInitializers,
inheritTransformationMetadata,
inheritValidationMetadata,
} from './type-helpers.utils';

export function PartialType<T>(classRef: Type<T>): Type<Partial<T>> {
abstract class PartialClassType {}
abstract class PartialClassType {
constructor() {
inheritPropertyInitializers(this, classRef);
}
}

const propertyKeys = inheritValidationMetadata(classRef, PartialClassType);
inheritTransformationMetadata(classRef, PartialClassType);

if (propertyKeys) {
propertyKeys.forEach(key => {
propertyKeys.forEach((key) => {
applyIsOptionalDecorator(PartialClassType, key);
});
}
Expand Down
9 changes: 7 additions & 2 deletions lib/pick-type.helper.ts
@@ -1,5 +1,6 @@
import { Type } from '@nestjs/common';
import {
inheritPropertyInitializers,
inheritTransformationMetadata,
inheritValidationMetadata,
} from './type-helpers.utils';
Expand All @@ -8,10 +9,14 @@ export function PickType<T, K extends keyof T>(
classRef: Type<T>,
keys: readonly K[],
): Type<Pick<T, typeof keys[number]>> {
abstract class PickClassType {}

const isInheritedPredicate = (propertyKey: string) =>
keys.includes(propertyKey as K);

abstract class PickClassType {
constructor() {
inheritPropertyInitializers(this, classRef, isInheritedPredicate);
}
}
inheritValidationMetadata(classRef, PickClassType, isInheritedPredicate);
inheritTransformationMetadata(classRef, PickClassType, isInheritedPredicate);

Expand Down
23 changes: 23 additions & 0 deletions lib/type-helpers.utils.ts
Expand Up @@ -155,3 +155,26 @@ function isClassTransformerAvailable() {
return false;
}
}

export function inheritPropertyInitializers(
target: Record<string, any>,
sourceClass: Type<any>,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
isPropertyInherited = (key: string) => true,
) {
try {
const tempInstance = new sourceClass();
const propertyNames = Object.getOwnPropertyNames(tempInstance);

propertyNames
.filter(
(propertyName) =>
typeof tempInstance[propertyName] !== 'undefined' &&
typeof target[propertyName] === 'undefined',
)
.filter((propertyName) => isPropertyInherited(propertyName))
.forEach((propertyName) => {
target[propertyName] = tempInstance[propertyName];
});
} catch {}
}
28 changes: 15 additions & 13 deletions tests/intersection-type.helper.spec.ts
Expand Up @@ -6,18 +6,18 @@ import { getValidationMetadataByTarget } from './type-helpers.test-utils';
describe('IntersectionType', () => {
class ClassA {
@MinLength(10)
login!: string;
login = 'defaultLoginWithMin10Chars';

@Transform(str => str + '_transformed')
@Transform((str) => str + '_transformed')
@MinLength(10)
password!: string;
}

class ClassB {
@IsString()
firstName!: string;
firstName = 'defaultFirst';

@Transform(str => str + '_transformed')
@Transform((str) => str + '_transformed')
@MinLength(5)
lastName!: string;
}
Expand All @@ -27,7 +27,7 @@ describe('IntersectionType', () => {
describe('Validation metadata', () => {
it('should inherit metadata for all properties from class A and class B', () => {
const validationKeys = getValidationMetadataByTarget(UpdateUserDto).map(
item => item.propertyName,
(item) => item.propertyName,
);
expect(validationKeys).toEqual([
'login',
Expand All @@ -43,17 +43,11 @@ describe('IntersectionType', () => {

const validationErrors = await validate(updateDto);

expect(validationErrors.length).toEqual(4);
expect(validationErrors.length).toEqual(2);
expect(validationErrors[0].constraints).toEqual({
minLength: 'login must be longer than or equal to 10 characters',
});
expect(validationErrors[1].constraints).toEqual({
minLength: 'password must be longer than or equal to 10 characters',
});
expect(validationErrors[2].constraints).toEqual({
isString: 'firstName must be a string',
});
expect(validationErrors[3].constraints).toEqual({
expect(validationErrors[1].constraints).toEqual({
minLength: 'lastName must be longer than or equal to 5 characters',
});
});
Expand Down Expand Up @@ -86,4 +80,12 @@ describe('IntersectionType', () => {
expect(transformedDto.password).toEqual(password + '_transformed');
});
});

describe('Property initializers', () => {
it('should inherit property initializers', () => {
const updateUserDto = new UpdateUserDto();
expect(updateUserDto.login).toEqual('defaultLoginWithMin10Chars');
expect(updateUserDto.firstName).toEqual('defaultFirst');
});
});
});
10 changes: 9 additions & 1 deletion tests/omit-type.helper.spec.ts
Expand Up @@ -10,7 +10,7 @@ describe('OmitType', () => {

@Transform((str) => str + '_transformed')
@MinLength(10)
password!: string;
password = 'defaultPassword';
}

class UpdateUserDto extends OmitType(CreateUserDto, ['login']) {}
Expand Down Expand Up @@ -56,4 +56,12 @@ describe('OmitType', () => {
expect(transformedDto.password).toEqual(password + '_transformed');
});
});

describe('Property initializers', () => {
it('should inherit property initializers', () => {
const updateUserDto = new UpdateUserDto();
expect((updateUserDto as any)['login']).toBeUndefined();
expect(updateUserDto.password).toEqual('defaultPassword');
});
});
});
9 changes: 8 additions & 1 deletion tests/partial-type.helper.spec.ts
Expand Up @@ -12,7 +12,7 @@ describe('PartialType', () => {
}

class CreateUserDto extends BaseUserDto {
login!: string;
login: string = 'defaultLogin';

@Expose()
@Transform((str) => str + '_transformed')
Expand Down Expand Up @@ -74,4 +74,11 @@ describe('PartialType', () => {
);
});
});

describe('Property initializers', () => {
it('should inherit property initializers', () => {
const updateUserDto = new UpdateUserDto();
expect(updateUserDto.login).toEqual('defaultLogin');
});
});
});
14 changes: 11 additions & 3 deletions tests/pick-type.helper.spec.ts
Expand Up @@ -5,9 +5,9 @@ import { getValidationMetadataByTarget } from './type-helpers.test-utils';

describe('PickType', () => {
class CreateUserDto {
@Transform(str => str + '_transformed')
@Transform((str) => str + '_transformed')
@MinLength(10)
login!: string;
login = 'defaultLogin';

@MinLength(10)
password!: string;
Expand All @@ -18,7 +18,7 @@ describe('PickType', () => {
describe('Validation metadata', () => {
it('should inherit metadata with "password" property excluded', () => {
const validationKeys = getValidationMetadataByTarget(UpdateUserDto).map(
item => item.propertyName,
(item) => item.propertyName,
);
expect(validationKeys).toEqual(['login']);
});
Expand Down Expand Up @@ -56,4 +56,12 @@ describe('PickType', () => {
expect(transformedDto.login).toEqual(login + '_transformed');
});
});

describe('Property initializers', () => {
it('should inherit property initializers', () => {
const updateUserDto = new UpdateUserDto();
expect((updateUserDto as any)['password']).toBeUndefined();
expect(updateUserDto.login).toEqual('defaultLogin');
});
});
});

0 comments on commit 565e256

Please sign in to comment.