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(discovery): use both entity name and path as key in MetadataStorage #488

Merged
merged 1 commit into from
Apr 17, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
"lint": "eslint packages/**/*.ts"
},
"jest": {
"testTimeout": 10000,
"testTimeout": 15000,
"preset": "ts-jest",
"collectCoverage": false,
"collectCoverageFrom": [
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/decorators/Entity.ts
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
Expand Up @@ -404,27 +404,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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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' });
});

});