Skip to content

Commit

Permalink
allow to use all decorators in constructor as well
Browse files Browse the repository at this point in the history
  • Loading branch information
marcj committed Mar 18, 2019
1 parent 2158cf4 commit 7fd6e23
Show file tree
Hide file tree
Showing 4 changed files with 110 additions and 70 deletions.
133 changes: 80 additions & 53 deletions packages/core/src/decorators.ts
Expand Up @@ -81,7 +81,7 @@ export class PropertySchema {
}

if (!this.classType) {
throw new Error(`No classType given for ${this.name}.`);
throw new Error(`No classType given for ${this.name}. Use @Field(forwardRef(() => MyClass)) for circular dependencies.`);
}

return this.classType;
Expand Down Expand Up @@ -233,6 +233,60 @@ export function DatabaseName<T>(name: string) {
};
}

/**
* Helper for decorators that are allowed to be placed in property declaration and constructor property declaration.
* We detect the name by reading the constructor' signature, which would be otherwise lost.
*/
function FieldDecoratorWrapper(
cb: (target: Object, property: string, returnType?: any) => void
): (target: Object, property?: string, parameterIndexOrDescriptor?: any) => void {
return (target: Object, property?: string, parameterIndexOrDescriptor?: any) => {
let returnType;

if (property) {
returnType = Reflect.getMetadata('design:type', target, property);
}

if (isNumber(parameterIndexOrDescriptor)) {
const constructorParamNames = getCachedParameterNames(target as ClassType<any>);
property = constructorParamNames[parameterIndexOrDescriptor];

const returnTypes = Reflect.getMetadata('design:paramtypes', target);
returnType = returnTypes[parameterIndexOrDescriptor];
target = target['prototype'];
}

if (!property) {
throw new Error(`Could not detect property in ${getClassName(target)}`);
}

cb(target, property, returnType)
};
}

function FieldAndClassDecoratorWrapper(
cb: (target: Object, property?: string, returnType?: any) => void
): (target: Object, property?: string, parameterIndexOrDescriptor?: any) => void {
return (target: Object, property?: string, parameterIndexOrDescriptor?: any) => {
let returnType;

if (property) {
returnType = Reflect.getMetadata('design:type', target, property);
}

if (isNumber(parameterIndexOrDescriptor)) {
const constructorParamNames = getCachedParameterNames(target as ClassType<any>);
property = constructorParamNames[parameterIndexOrDescriptor];

const returnTypes = Reflect.getMetadata('design:paramtypes', target);
returnType = returnTypes[parameterIndexOrDescriptor];
target = target['prototype'];
}

cb(target, property, returnType)
};
}

/**
* Used to define a field as decorated.
* This is necessary if you want to wrap a field value in the class instance using
Expand Down Expand Up @@ -282,11 +336,11 @@ export function DatabaseName<T>(name: string) {
* If you use classToPlain(PageClass, ...) or classToMongo(PageClass, ...) the field value of `children` will be the type of
* `PageCollection.pages` (always the field where @Decorator() is applied to), here a array of PagesClass `PageClass[]`.
*/
export function Decorated<T>() {
return (target: T, property: string) => {
export function Decorated() {
return FieldDecoratorWrapper((target: Object, property: string) => {
getOrCreateEntitySchema(target).decorator = property;
getOrCreateEntitySchema(target).getOrCreateProperty(property).isDecorated = true;
};
});
}

/**
Expand All @@ -298,11 +352,11 @@ export function Decorated<T>() {
*
* @category Decorator
*/
export function IDField<T>() {
return (target: T, property: string) => {
export function IDField() {
return FieldDecoratorWrapper((target: Object, property: string) => {
getOrCreateEntitySchema(target).idField = property;
getOrCreateEntitySchema(target).getOrCreateProperty(property).isId = true;
};
});
}

/**
Expand Down Expand Up @@ -348,10 +402,10 @@ export function IDField<T>() {
* }
* ```
*/
export function ParentReference<T>() {
return (target: T, property: string) => {
export function ParentReference() {
return FieldDecoratorWrapper((target: Object, property: string) => {
getOrCreateEntitySchema(target).getOrCreateProperty(property).isParentReference = true;
};
});
}

/**
Expand Down Expand Up @@ -394,10 +448,10 @@ export function OnLoad<T>(options: { fullLoad?: boolean } = {}) {
*
* @category Decorator
*/
export function Exclude<T>() {
return (target: T, property: string) => {
export function Exclude() {
return FieldDecoratorWrapper((target: Object, property: string) => {
getOrCreateEntitySchema(target).getOrCreateProperty(property).exclude = 'all';
};
});
}

/**
Expand All @@ -406,10 +460,10 @@ export function Exclude<T>() {
*
* @category Decorator
*/
export function ExcludeToMongo<T>() {
return (target: T, property: string) => {
export function ExcludeToMongo() {
return FieldDecoratorWrapper((target: Object, property: string) => {
getOrCreateEntitySchema(target).getOrCreateProperty(property).exclude = 'mongo';
};
});
}

/**
Expand All @@ -418,10 +472,10 @@ export function ExcludeToMongo<T>() {
*
* @category Decorator
*/
export function ExcludeToPlain<T>() {
return (target: T, property: string) => {
export function ExcludeToPlain() {
return FieldDecoratorWrapper((target: Object, property: string) => {
getOrCreateEntitySchema(target).getOrCreateProperty(property).exclude = 'plain';
};
});
}

type FieldTypes = String | Number | Date | ClassType<any> | ForwardedRef<any>;
Expand Down Expand Up @@ -467,33 +521,6 @@ export function forwardRef<T>(forward: ForwardRefFn<T>): ForwardedRef<T> {
return new ForwardedRef(forward);
}

/**
* Helper for decorators that are allowed to be placed in property declaration and constructor property declaration.
* We detect the name by reading the constructor' signature, which would be otherwise lost.
*/
function FieldDecoratorWrapper(
cb: (target: Object, property?: string, returnType?: any) => void
): (target: Object, property?: string, parameterIndexOrDescriptor?: any) => void {
return (target: Object, property?: string, parameterIndexOrDescriptor?: any) => {
let returnType;

if (property) {
returnType = Reflect.getMetadata('design:type', target, property);
}

if (isNumber(parameterIndexOrDescriptor)) {
const constructorParamNames = getCachedParameterNames(target as ClassType<any>);
property = constructorParamNames[parameterIndexOrDescriptor];

const returnTypes = Reflect.getMetadata('design:paramtypes', target);
returnType = returnTypes[parameterIndexOrDescriptor];
target = target['prototype'];
}

cb(target, property, returnType)
};
}

interface FieldOptions {
/**
* Whether the type is a map. You should prefer the short {} annotation
Expand Down Expand Up @@ -573,9 +600,7 @@ interface FieldOptions {
* @category Decorator
*/
export function Field(type?: FieldTypes | FieldTypes[] | { [n: string]: FieldTypes }, options?: FieldOptions) {
return FieldDecoratorWrapper((target: Object, property?: string, returnType?: any) => {
if (!property) return;

return FieldDecoratorWrapper((target: Object, property: string, returnType?: any) => {
options = options || {};

const id = getClassName(target) + '::' + property;
Expand Down Expand Up @@ -792,8 +817,8 @@ export function FieldArray(type: FieldTypes) {
* @hidden
*/
function Type<T>(type: Types) {
return FieldDecoratorWrapper((target: Object, property?: string, returnType?: any) => {
getOrCreateEntitySchema(target).getOrCreateProperty(property!).type = type;
return FieldDecoratorWrapper((target: Object, property: string, returnType?: any) => {
getOrCreateEntitySchema(target).getOrCreateProperty(property).type = type;
});
}

Expand Down Expand Up @@ -838,7 +863,7 @@ export function UUIDField() {
* @category Decorator
*/
export function Index(options?: IndexOptions, fields?: string | string[], name?: string) {
return FieldDecoratorWrapper((target: Object, property?: string, returnType?: any) => {
return FieldAndClassDecoratorWrapper((target: Object, property?: string, returnType?: any) => {
const schema = getOrCreateEntitySchema(target);

if (isArray(fields)) {
Expand All @@ -854,10 +879,12 @@ export function Index(options?: IndexOptions, fields?: string | string[], name?:
/**
* Used to define a field as Enum.
*
* If allowLabelsAsValue is set, you can use the enum labels as well for setting the property value using plainToClass().
*
* @category Decorator
*/
export function EnumType<T>(type: any, allowLabelsAsValue = false) {
return FieldDecoratorWrapper((target: Object, property?: string, returnType?: any) => {
return FieldDecoratorWrapper((target: Object, property: string, returnType?: any) => {
if (property) {
Type('enum')(target, property);
getOrCreateEntitySchema(target).getOrCreateProperty(property).classType = type;
Expand Down
22 changes: 13 additions & 9 deletions packages/core/src/mapper.ts
Expand Up @@ -194,16 +194,20 @@ export function getReflectionType<T>(classType: ClassType<T>, propertyName: stri
if (undefined === value) {
const schema = getEntitySchema(classType).getPropertyOrUndefined(propertyName);

if (schema) {
value = {
type: schema.type,
typeValue: schema.getResolvedClassTypeForValidType()
};
} else {
value = {
type: undefined,
typeValue: undefined
try {
if (schema) {
value = {
type: schema.type,
typeValue: schema.getResolvedClassTypeForValidType()
};
} else {
value = {
type: undefined,
typeValue: undefined
}
}
} catch (error) {
throw new Error(`${getClassPropertyName(classType, propertyName)}: ${error}`);
}

valueMap.set('getReflectionType::' + propertyName, value);
Expand Down
9 changes: 9 additions & 0 deletions packages/core/tests/decorator.spec.ts
Expand Up @@ -20,6 +20,9 @@ import {
} from "../";
import {Buffer} from "buffer";
import {SimpleModel} from "./entities";
import {PageClass} from "./document-scenario/PageClass";
import {DocumentClass} from "./document-scenario/DocumentClass";
import {PageCollection} from "./document-scenario/PageCollection";

test('test invalid usage', async () => {
class Config {}
Expand Down Expand Up @@ -52,6 +55,12 @@ test('test invalid usage', async () => {
}).toThrowError('Property bla not found');
});

test('test circular', async () => {
expect(getEntitySchema(PageClass).getProperty('children').getResolvedClassType()).toBe(PageCollection);
expect(getEntitySchema(PageClass).getProperty('parent').getResolvedClassType()).toBe(PageClass);
expect(getEntitySchema(PageClass).getProperty('document').getResolvedClassType()).toBe(DocumentClass);
});

test('test inheritance', async () => {
class Base {
constructor(
Expand Down
16 changes: 8 additions & 8 deletions packages/core/tests/document-scenario/PageClass.ts
Expand Up @@ -10,25 +10,25 @@ export class PageClass {
@UUIDField()
id: string = uuid();

@Field()
name?: string;

@Field(forwardRef(() => PageCollection))
children: PageCollection = new PageCollection;

@Field(Buffer)
picture?: Buffer;

@Field(PageClass)
@Field()
@ParentReference()
@Optional()
parent?: PageClass;

@Field(forwardRef(() => DocumentClass))
@ParentReference()
document: DocumentClass;
constructor(
@Field(forwardRef(() => DocumentClass))
@ParentReference()
public readonly document: DocumentClass,

constructor(document: DocumentClass, name: string) {
@Field()
public readonly name: string
) {
this.document = document;
this.name = name;
}
Expand Down

0 comments on commit 7fd6e23

Please sign in to comment.