Skip to content

Commit

Permalink
feat(discovery): use both entity name and path as key in Metadat… (#488)
Browse files Browse the repository at this point in the history
Decorators are using static MetadataStorage, now the keys includes also the path to entity, so technically it is possible to have multiple entities with the same name (although not in the same ORM context, but with multiple ORM instances).

This should also fix issues with HMR and "multiple property decorators used" validation error.
  • Loading branch information
B4nan committed Aug 9, 2020
1 parent 636e861 commit 72f0aca
Show file tree
Hide file tree
Showing 15 changed files with 66 additions and 59 deletions.
4 changes: 2 additions & 2 deletions packages/core/src/decorators/Entity.ts
Expand Up @@ -5,10 +5,10 @@ import { AnyEntity, Constructor } from '../typings';

export function Entity(options: EntityOptions<any> = {}): Function {
return function <T extends { new(...args: any[]): AnyEntity<T> }>(target: T) {
const meta = MetadataStorage.getMetadata(target.name);
const meta = MetadataStorage.getMetadataFromDecorator(target);
Utils.merge(meta, options);
meta.class = target;
Utils.lookupPathFromDecorator(meta);
meta.name = target.name;

return target;
};
Expand Down
4 changes: 1 addition & 3 deletions packages/core/src/decorators/Enum.ts
Expand Up @@ -2,14 +2,12 @@ import { MetadataStorage } from '../metadata';
import { ReferenceType } from '../entity';
import { PropertyOptions } from '.';
import { EntityProperty, AnyEntity, Dictionary } from '../typings';
import { Utils } from '../utils';

export function Enum(options: EnumOptions | (() => Dictionary) = {}): Function {
return function (target: AnyEntity, propertyName: string) {
const meta = MetadataStorage.getMetadata(target.constructor.name);
const meta = MetadataStorage.getMetadataFromDecorator(target.constructor);
options = options instanceof Function ? { items: options } : options;
meta.properties[propertyName] = Object.assign({ name: propertyName, reference: ReferenceType.SCALAR, enum: true }, options) as EntityProperty;
Utils.lookupPathFromDecorator(meta);
};
}

Expand Down
5 changes: 1 addition & 4 deletions packages/core/src/decorators/Indexed.ts
@@ -1,12 +1,9 @@
import { MetadataStorage } from '../metadata';
import { AnyEntity, Dictionary } from '../typings';
import { Utils } from '../utils';

function createDecorator(options: IndexOptions | UniqueOptions, unique: boolean): Function {
return function (target: AnyEntity, propertyName?: string) {
const entityName = propertyName ? target.constructor.name : target.name;
const meta = MetadataStorage.getMetadata(entityName);
Utils.lookupPathFromDecorator(meta);
const meta = MetadataStorage.getMetadataFromDecorator(propertyName ? target.constructor : target as Function);
options.properties = options.properties || propertyName;
const key = unique ? 'uniques' : 'indexes';
meta[key].push(options as Required<IndexOptions | UniqueOptions>);
Expand Down
3 changes: 1 addition & 2 deletions packages/core/src/decorators/ManyToMany.ts
Expand Up @@ -12,9 +12,8 @@ export function ManyToMany<T extends AnyEntity<T>>(
) {
return function (target: AnyEntity, propertyName: string) {
options = Utils.isObject<ManyToManyOptions<T>>(entity) ? entity : { ...options, entity, mappedBy };
const meta = MetadataStorage.getMetadata(target.constructor.name);
const meta = MetadataStorage.getMetadataFromDecorator(target.constructor);
EntityValidator.validateSingleDecorator(meta, propertyName);
Utils.lookupPathFromDecorator(meta);
const property = { name: propertyName, reference: ReferenceType.MANY_TO_MANY } as EntityProperty<T>;
meta.properties[propertyName] = Object.assign(property, options);
};
Expand Down
8 changes: 1 addition & 7 deletions packages/core/src/decorators/ManyToOne.ts
Expand Up @@ -10,14 +10,8 @@ export function ManyToOne<T extends AnyEntity<T>>(
) {
return function (target: AnyEntity, propertyName: string) {
options = Utils.isObject<ManyToOneOptions<T>>(entity) ? entity : { ...options, entity };

if ((options as any).fk) {
throw new Error(`@ManyToOne({ fk })' is deprecated, use 'inversedBy' instead in '${target.constructor.name}.${propertyName}'`);
}

const meta = MetadataStorage.getMetadata(target.constructor.name);
const meta = MetadataStorage.getMetadataFromDecorator(target.constructor);
EntityValidator.validateSingleDecorator(meta, propertyName);
Utils.lookupPathFromDecorator(meta);
const property = { name: propertyName, reference: ReferenceType.MANY_TO_ONE } as EntityProperty;
meta.properties[propertyName] = Object.assign(property, options);
};
Expand Down
3 changes: 1 addition & 2 deletions packages/core/src/decorators/OneToMany.ts
Expand Up @@ -13,9 +13,8 @@ export function createOneToDecorator<T extends AnyEntity<T>>(
) {
return function (target: AnyEntity, propertyName: string) {
options = Utils.isObject<OneToManyOptions<T>>(entity) ? entity : { ...options, entity, mappedBy };
const meta = MetadataStorage.getMetadata(target.constructor.name);
const meta = MetadataStorage.getMetadataFromDecorator(target.constructor);
EntityValidator.validateSingleDecorator(meta, propertyName);
Utils.lookupPathFromDecorator(meta);

const prop = { name: propertyName, reference } as EntityProperty<T>;
Object.assign(prop, options);
Expand Down
4 changes: 1 addition & 3 deletions packages/core/src/decorators/PrimaryKey.ts
Expand Up @@ -2,12 +2,10 @@ import { MetadataStorage } from '../metadata';
import { ReferenceType } from '../entity';
import { PropertyOptions } from '.';
import { AnyEntity, EntityProperty } from '../typings';
import { Utils } from '../utils';

function createDecorator(options: PrimaryKeyOptions | SerializedPrimaryKeyOptions, serialized: boolean): Function {
return function (target: AnyEntity, propertyName: string) {
const meta = MetadataStorage.getMetadata(target.constructor.name);
Utils.lookupPathFromDecorator(meta);
const meta = MetadataStorage.getMetadataFromDecorator(target.constructor);
const k = serialized ? 'serializedPrimaryKey' as const : 'primary' as const;
options[k] = true;
meta.properties[propertyName] = Object.assign({ name: propertyName, reference: ReferenceType.SCALAR }, options) as EntityProperty;
Expand Down
3 changes: 1 addition & 2 deletions packages/core/src/decorators/Property.ts
Expand Up @@ -6,10 +6,9 @@ import { Type } from '../types';

export function Property(options: PropertyOptions = {}): Function {
return function (target: AnyEntity, propertyName: string) {
const meta = MetadataStorage.getMetadata(target.constructor.name);
const meta = MetadataStorage.getMetadataFromDecorator(target.constructor);
const desc = Object.getOwnPropertyDescriptor(target, propertyName) || {};
EntityValidator.validateSingleDecorator(meta, propertyName);
Utils.lookupPathFromDecorator(meta);
const name = options.name || propertyName;

if (propertyName !== name && !(desc.value instanceof Function)) {
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/decorators/Repository.ts
@@ -1,10 +1,10 @@
import { AnyEntity, Constructor, EntityClass } from '../typings';
import { AnyEntity, Constructor, Dictionary, EntityClass } from '../typings';
import { EntityRepository } from '../entity';
import { MetadataStorage } from '../metadata';

export function Repository<T extends AnyEntity>(entity: EntityClass<T>) {
return function (target: Constructor<EntityRepository<T>>) {
const meta = MetadataStorage.getMetadata(entity.name);
const meta = MetadataStorage.getMetadata(entity.name, (entity as Dictionary).__path);
meta.customRepository = () => target;
};
}
2 changes: 1 addition & 1 deletion packages/core/src/decorators/hooks.ts
Expand Up @@ -3,7 +3,7 @@ import { HookType } from '../typings';

function hook(type: HookType) {
return function (target: any, method: string) {
const meta = MetadataStorage.getMetadata(target.constructor.name);
const meta = MetadataStorage.getMetadataFromDecorator(target.constructor);

if (!meta.hooks[type]) {
meta.hooks[type] = [];
Expand Down
12 changes: 10 additions & 2 deletions packages/core/src/metadata/MetadataDiscovery.ts
Expand Up @@ -2,7 +2,7 @@ import { basename, extname } from 'path';
import globby from 'globby';
import chalk from 'chalk';

import { AnyEntity, Constructor, EntityClass, EntityClassGroup, EntityMetadata, EntityProperty } from '../typings';
import { AnyEntity, Constructor, Dictionary, EntityClass, EntityClassGroup, EntityMetadata, EntityProperty } from '../typings';
import { Configuration, Utils, ValidationError } from '../utils';
import { MetadataValidator } from './MetadataValidator';
import { MetadataStorage } from './MetadataStorage';
Expand Down Expand Up @@ -104,7 +104,7 @@ export class MetadataDiscovery {
continue;
}

this.metadata.set(name, MetadataStorage.getMetadata(name));
this.metadata.set(name, Utils.copy(MetadataStorage.getMetadata(name, path)));
await this.discoverEntity(target, path);
}
}
Expand All @@ -130,6 +130,14 @@ export class MetadataDiscovery {
return entity;
}

const path = (entity as Dictionary).__path;

if (path) {
const meta = Utils.copy(MetadataStorage.getMetadata(entity.name, path));
meta.path = Utils.relativePath(path, this.config.get('baseDir'));
this.metadata.set(entity.name, meta);
}

const schema = new EntitySchema<T>(this.metadata.get<T>(entity.name, true), true);
schema.setClass(entity);
schema.meta.useCache = true;
Expand Down
22 changes: 16 additions & 6 deletions packages/core/src/metadata/MetadataStorage.ts
Expand Up @@ -13,19 +13,29 @@ export class MetadataStorage {
}

static getMetadata(): Dictionary<EntityMetadata>;
static getMetadata<T extends AnyEntity<T> = any>(entity: string): EntityMetadata<T>;
static getMetadata<T extends AnyEntity<T> = any>(entity?: string): Dictionary<EntityMetadata> | EntityMetadata<T> {
if (entity && !MetadataStorage.metadata[entity]) {
MetadataStorage.metadata[entity] = { className: entity, properties: {}, hooks: {}, indexes: [] as any[], uniques: [] as any[] } as EntityMetadata;
static getMetadata<T extends AnyEntity<T> = any>(entity: string, path: string): EntityMetadata<T>;
static getMetadata<T extends AnyEntity<T> = any>(entity?: string, path?: string): Dictionary<EntityMetadata> | EntityMetadata<T> {
const key = entity && path ? entity + '-' + Utils.hash(path) : null;

if (key && !MetadataStorage.metadata[key]) {
MetadataStorage.metadata[key] = { className: entity, path, properties: {}, hooks: {}, indexes: [] as any[], uniques: [] as any[] } as EntityMetadata;
}

if (entity) {
return MetadataStorage.metadata[entity];
if (key) {
return MetadataStorage.metadata[key];
}

return MetadataStorage.metadata;
}

static getMetadataFromDecorator<T = any>(target: Function): EntityMetadata<T> {
const path = Utils.lookupPathFromDecorator();
const meta = MetadataStorage.getMetadata(target.name, path);
Object.defineProperty(target, '__path', { value: path, writable: true });

return meta;
}

static init(): MetadataStorage {
return new MetadataStorage(MetadataStorage.metadata);
}
Expand Down
11 changes: 3 additions & 8 deletions packages/core/src/utils/Utils.ts
Expand Up @@ -410,27 +410,22 @@ export class Utils {
* Uses some dark magic to get source path to caller where decorator is used.
* Analyses stack trace of error created inside the function call.
*/
static lookupPathFromDecorator(meta: EntityMetadata, stack?: string[]): string {
if (meta.path) {
return meta.path;
}

static lookupPathFromDecorator(stack?: string[]): string {
// use some dark magic to get source path to caller
stack = stack || new Error().stack!.split('\n');
let line = stack.findIndex(line => line.includes('__decorate'))!;

if (line === -1) {
return meta.path;
throw new Error('Cannot find path to entity');
}

if (Utils.normalizePath(stack[line]).includes('node_modules/tslib/tslib')) {
line++;
}

const re = stack[line].match(/\(.+\)/i) ? /\((.*):\d+:\d+\)/ : /at\s*(.*):\d+:\d+$/;
meta.path = Utils.normalizePath(stack[line].match(re)![1]);

return meta.path;
return Utils.normalizePath(stack[line].match(re)![1]);
}

/**
Expand Down
11 changes: 7 additions & 4 deletions tests/Utils.test.ts
Expand Up @@ -278,7 +278,7 @@ describe('Utils', () => {
' at Module.load (internal/modules/cjs/loader.js:643:32)',
' at Function.Module._load (internal/modules/cjs/loader.js:556:12)',
];
expect(Utils.lookupPathFromDecorator({} as any, stack1)).toBe('/usr/local/var/www/my-project/dist/entities/Customer.js');
expect(Utils.lookupPathFromDecorator(stack1)).toBe('/usr/local/var/www/my-project/dist/entities/Customer.js');

// no tslib, via ts-node
const stack2 = [
Expand All @@ -293,7 +293,7 @@ describe('Utils', () => {
' at Module._extensions.js (internal/modules/cjs/loader.js:787:10)',
' at Object.require.extensions.<computed> [as .ts] (/usr/local/var/www/my-project/node_modules/ts-node/src/index.ts:476:12)',
];
expect(Utils.lookupPathFromDecorator({} as any, stack2)).toBe('/usr/local/var/www/my-project/src/entities/Customer.ts');
expect(Utils.lookupPathFromDecorator(stack2)).toBe('/usr/local/var/www/my-project/src/entities/Customer.ts');

// no parens
const stack3 = [
Expand All @@ -308,7 +308,10 @@ describe('Utils', () => {
' at Module.load (internal/modules/cjs/loader.js:643:32)',
' at Function.Module._load (internal/modules/cjs/loader.js:556:12)',
];
expect(Utils.lookupPathFromDecorator({} as any, stack3)).toBe('/usr/local/var/www/my-project/dist/entities/Customer.js');
expect(Utils.lookupPathFromDecorator(stack3)).toBe('/usr/local/var/www/my-project/dist/entities/Customer.js');

// no decorated line found
expect(() => Utils.lookupPathFromDecorator()).toThrowError('Cannot find path to entity');
});

test('lookup path from decorator on windows', () => {
Expand All @@ -325,7 +328,7 @@ describe('Utils', () => {
' at Module.load (internal/modules/cjs/loader.js:790:32)',
' at Function.Module._load (internal/modules/cjs/loader.js:703:12)',
];
expect(Utils.lookupPathFromDecorator({} as any, stack1)).toBe('C:/www/my-project/src/entities/Customer.ts');
expect(Utils.lookupPathFromDecorator(stack1)).toBe('C:/www/my-project/src/entities/Customer.ts');
});

afterAll(async () => orm.close(true));
Expand Down
29 changes: 18 additions & 11 deletions tests/decorators.test.ts
@@ -1,4 +1,4 @@
import { ManyToMany, ManyToOne, OneToMany, OneToOne, Property, MetadataStorage, ReferenceType } from '@mikro-orm/core';
import { ManyToMany, ManyToOne, OneToMany, OneToOne, Property, MetadataStorage, ReferenceType, Utils } from '@mikro-orm/core';
import { Test } from './entities';

class Test2 {}
Expand All @@ -9,39 +9,46 @@ class Test6 {}

describe('decorators', () => {

const lookupPathFromDecorator = jest.spyOn(Utils, 'lookupPathFromDecorator');
lookupPathFromDecorator.mockReturnValue('/path/to/entity');

test('ManyToMany', () => {
const storage = MetadataStorage.getMetadata();
const key = 'Test2-' + Utils.hash('/path/to/entity');
ManyToMany({ entity: () => Test })(new Test2(), 'test0');
expect(storage.Test2.properties.test0).toMatchObject({ reference: ReferenceType.MANY_TO_MANY, name: 'test0' });
expect(storage.Test2.properties.test0.entity()).toBe(Test);
expect(storage[key].properties.test0).toMatchObject({ reference: ReferenceType.MANY_TO_MANY, name: 'test0' });
expect(storage[key].properties.test0.entity()).toBe(Test);
});

test('ManyToOne', () => {
expect(() => ManyToOne(() => Test, { fk: 'test' } as any)(new Test3(), 'test1')).toThrowError(`@ManyToOne({ fk })' is deprecated, use 'inversedBy' instead in 'Test3.test1`);
const storage = MetadataStorage.getMetadata();
const key = 'Test3-' + Utils.hash('/path/to/entity');
ManyToOne({ entity: () => Test })(new Test3(), 'test1');
expect(storage.Test3.properties.test1).toMatchObject({ reference: ReferenceType.MANY_TO_ONE, name: 'test1' });
expect(storage.Test3.properties.test1.entity()).toBe(Test);
expect(storage[key].properties.test1).toMatchObject({ reference: ReferenceType.MANY_TO_ONE, name: 'test1' });
expect(storage[key].properties.test1.entity()).toBe(Test);
});

test('OneToOne', () => {
const storage = MetadataStorage.getMetadata();
const key = 'Test6-' + Utils.hash('/path/to/entity');
OneToOne({ entity: () => Test, inversedBy: 'test5' } as any)(new Test6(), 'test1');
expect(storage.Test6.properties.test1).toMatchObject({ reference: ReferenceType.ONE_TO_ONE, name: 'test1', inversedBy: 'test5' });
expect(storage.Test6.properties.test1.entity()).toBe(Test);
expect(storage[key].properties.test1).toMatchObject({ reference: ReferenceType.ONE_TO_ONE, name: 'test1', inversedBy: 'test5' });
expect(storage[key].properties.test1.entity()).toBe(Test);
});

test('OneToMany', () => {
const storage = MetadataStorage.getMetadata();
const key = 'Test4-' + Utils.hash('/path/to/entity');
OneToMany({ entity: () => Test, mappedBy: 'test' } as any)(new Test4(), 'test2');
expect(storage.Test4.properties.test2).toMatchObject({ reference: ReferenceType.ONE_TO_MANY, name: 'test2', mappedBy: 'test' });
expect(storage.Test4.properties.test2.entity()).toBe(Test);
expect(storage[key].properties.test2).toMatchObject({ reference: ReferenceType.ONE_TO_MANY, name: 'test2', mappedBy: 'test' });
expect(storage[key].properties.test2.entity()).toBe(Test);
});

test('Property', () => {
const storage = MetadataStorage.getMetadata();
const key = 'Test5-' + Utils.hash('/path/to/entity');
Property()(new Test5(), 'test3');
expect(storage.Test5.properties.test3).toMatchObject({ reference: ReferenceType.SCALAR, name: 'test3' });
expect(storage[key].properties.test3).toMatchObject({ reference: ReferenceType.SCALAR, name: 'test3' });
});

});

0 comments on commit 72f0aca

Please sign in to comment.