Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Types transformation utils #1295

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
186 changes: 186 additions & 0 deletions src/helpers/inherit-metedata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
// Inspired by @nestjs/mapped-types
import { ClassType } from "../interfaces";

export function applyIsOptionalDecorator(targetClass: Function, propertyKey: string) {
if (!isClassValidatorAvailable()) {
return;
}
const classValidator: typeof import("class-validator") = require("class-validator");
const decoratorFactory = classValidator.IsOptional();
decoratorFactory(targetClass.prototype, propertyKey);
}

export function inheritValidationMetadata(
parentClass: ClassType<any>,
targetClass: Function,
isPropertyInherited?: (key: string) => boolean,
) {
if (!isClassValidatorAvailable()) {
return;
}
try {
const classValidator: typeof import("class-validator") = require("class-validator");
const metadataStorage: import("class-validator").MetadataStorage = (classValidator as any)
.getMetadataStorage
? (classValidator as any).getMetadataStorage()
: classValidator.getFromContainer(classValidator.MetadataStorage);

const getTargetValidationMetadatasArgs = [parentClass, null!, false, false];
const targetMetadata: ReturnType<
typeof metadataStorage.getTargetValidationMetadatas
> = (metadataStorage.getTargetValidationMetadatas as Function)(
...getTargetValidationMetadatasArgs,
);
targetMetadata
.filter(({ propertyName }) => !isPropertyInherited || isPropertyInherited(propertyName))
.map(value => {
const originalType = Reflect.getMetadata(
"design:type",
parentClass.prototype,
value.propertyName,
);
if (originalType) {
// @ts-ignore
Reflect.defineMetadata(
"design:type",
originalType,
targetClass.prototype,
value.propertyName,
);
}

metadataStorage.addValidationMetadata({
...value,
target: targetClass,
});
return value.propertyName;
});
} catch (err) {
if (err.code !== "EEXIST") {
throw err;
}
}
}

type TransformMetadataKey =
| "_excludeMetadatas"
| "_exposeMetadatas"
| "_typeMetadatas"
| "_transformMetadatas";

export function inheritTransformationMetadata(
parentClass: ClassType<any>,
targetClass: Function,
isPropertyInherited?: (key: string) => boolean,
) {
if (!isClassTransformerAvailable()) {
return;
}
try {
const transformMetadataKeys: TransformMetadataKey[] = [
"_excludeMetadatas",
"_exposeMetadatas",
"_transformMetadatas",
"_typeMetadatas",
];
transformMetadataKeys.forEach(key =>
inheritTransformerMetadata(key, parentClass, targetClass, isPropertyInherited),
);
} catch (err) {
if (err.code !== "EEXIST") {
throw err;
}
}
}

function inheritTransformerMetadata(
key: TransformMetadataKey,
parentClass: ClassType<any>,
targetClass: Function,
isPropertyInherited?: (key: string) => boolean,
) {
let classTransformer: any;
try {
/** "class-transformer" >= v0.3.x */
classTransformer = require("class-transformer/cjs/storage");
} catch {
/** "class-transformer" <= v0.3.x */
classTransformer = require("class-transformer/storage");
}
const metadataStorage /*: typeof import('class-transformer/types/storage').defaultMetadataStorage */ =
classTransformer.defaultMetadataStorage;

while (parentClass && parentClass !== Object) {
if (metadataStorage[key].has(parentClass)) {
const metadataMap = metadataStorage[key] as Map<Function, Map<string, any>>;
const parentMetadata = metadataMap.get(parentClass);

const targetMetadataEntries: Iterable<[string, any]> = Array.from(parentMetadata!.entries())
.filter(([keyInEntries]) => !isPropertyInherited || isPropertyInherited(keyInEntries))
.map(([keyInEntries, metadata]) => {
if (Array.isArray(metadata)) {
// "_transformMetadatas" is an array of elements
const targetMetadata = metadata.map(item => ({
...item,
target: targetClass,
}));
return [keyInEntries, targetMetadata];
}
return [keyInEntries, { ...metadata, target: targetClass }];
});

if (metadataMap.has(targetClass)) {
const existingRules = metadataMap.get(targetClass)!.entries();
metadataMap.set(targetClass, new Map([...existingRules, ...targetMetadataEntries]));
} else {
metadataMap.set(targetClass, new Map(targetMetadataEntries));
}
}
parentClass = Object.getPrototypeOf(parentClass);
}
}

function isClassValidatorAvailable() {
try {
require("class-validator");
return true;
} catch {
return false;
}
}

function isClassTransformerAvailable() {
try {
require("class-transformer");
return true;
} catch {
return false;
}
}

export function inheritPropertyInitializers(
target: Record<string, any>,
sourceClass: ClassType<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 (err) {
if (err.code !== "EEXIST") {
throw err;
}
}
}
7 changes: 7 additions & 0 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,10 @@ export {
defaultPrintSchemaOptions,
} from "./emitSchemaDefinitionFile";
export { ContainerType, ContainerGetter } from "./container";
export {
PartialType,
PickType,
RequiredType,
OmitType,
IntersectionType,
} from "./types-transformation";
136 changes: 136 additions & 0 deletions src/utils/types-transformation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { ObjectType, InputType, InterfaceType } from "../decorators";
import {
inheritValidationMetadata,
inheritTransformationMetadata,
applyIsOptionalDecorator,
} from "../helpers/inherit-metedata";
import { ClassType } from "../interfaces";
import { getMetadataStorage } from "../metadata";

export function PartialType<T>(BaseClass: ClassType<T>): ClassType<Partial<T>> {
const PartialClass = abstractClass();
inheritValidationMetadata(BaseClass, PartialClass);
inheritTransformationMetadata(BaseClass, PartialClass);

const fields = getMetadataStorage().fields.filter(
f => f.target === BaseClass || BaseClass.prototype instanceof f.target,
);

fields.forEach(field => {
getMetadataStorage().collectClassFieldMetadata({
...field,
typeOptions: { ...field.typeOptions, nullable: true },
target: PartialClass,
});
applyIsOptionalDecorator(PartialClass, field.name);
});

return PartialClass as ClassType<Partial<T>>;
}

export function RequiredType<T>(BaseClass: ClassType<T>): ClassType<Required<T>> {
const RequiredClass = abstractClass();
inheritValidationMetadata(BaseClass, RequiredClass);
inheritTransformationMetadata(BaseClass, RequiredClass);

const fields = getMetadataStorage().fields.filter(
f => f.target === BaseClass || BaseClass.prototype instanceof f.target,
);

fields.forEach(field => {
getMetadataStorage().collectClassFieldMetadata({
...field,
typeOptions: { ...field.typeOptions, nullable: false },
target: RequiredClass,
});
});
return RequiredClass as ClassType<Required<T>>;
}

export function PickType<T, K extends keyof T>(
BaseClass: ClassType<T>,
...pickFields: K[]
): ClassType<Pick<T, K>> {
const PickClass = abstractClass();

const isInheritedPredicate = (propertyKey: string) => pickFields.includes(propertyKey as K);
inheritValidationMetadata(BaseClass, PickClass, isInheritedPredicate);
inheritTransformationMetadata(BaseClass, PickClass, isInheritedPredicate);

const fields = getMetadataStorage().fields.filter(
f =>
(f.target === BaseClass || BaseClass.prototype instanceof f.target) &&
pickFields.includes(f.name as K),
);

fields.forEach(field => {
getMetadataStorage().collectClassFieldMetadata({
...field,
target: PickClass,
});
});
return PickClass as ClassType<Pick<T, K>>;
}

export function OmitType<T, K extends keyof T>(
BaseClass: ClassType<T>,
...omitFields: K[]
): ClassType<Omit<T, K>> {
const OmitClass = abstractClass();

const isInheritedPredicate = (propertyKey: string) => !omitFields.includes(propertyKey as K);
inheritValidationMetadata(BaseClass, OmitClass, isInheritedPredicate);
inheritTransformationMetadata(BaseClass, OmitClass, isInheritedPredicate);

const fields = getMetadataStorage().fields.filter(
f =>
(f.target === BaseClass || BaseClass.prototype instanceof f.target) &&
!omitFields.includes(f.name as K),
);

fields.forEach(field => {
getMetadataStorage().collectClassFieldMetadata({
...field,
target: OmitClass,
});
});
return OmitClass as ClassType<Omit<T, K>>;
}

export function IntersectionType<A, B>(BaseClassA: ClassType<A>, BaseClassB: ClassType<B>) {
const IntersectionClass = abstractClass();
inheritValidationMetadata(BaseClassA, IntersectionClass);
inheritTransformationMetadata(BaseClassA, IntersectionClass);
inheritValidationMetadata(BaseClassB, IntersectionClass);
inheritTransformationMetadata(BaseClassB, IntersectionClass);

const fields = getMetadataStorage().fields.filter(
f =>
f.target === BaseClassB ||
BaseClassB.prototype instanceof f.target ||
f.target === BaseClassA ||
BaseClassA.prototype instanceof f.target,
);

fields.forEach(field => {
getMetadataStorage().collectClassFieldMetadata({
...field,
target: IntersectionClass,
});
});

return IntersectionClass as ClassType<A & B>;
}

function abstractClass() {
class AbstractClass {}
InputType({ isAbstract: true })(AbstractClass);
ObjectType({ isAbstract: true })(AbstractClass);
InterfaceType({ isAbstract: true })(AbstractClass);
getMetadataStorage().collectArgsMetadata({
name: AbstractClass.name,
isAbstract: true,
target: AbstractClass,
});
return AbstractClass;
}
Loading