Skip to content

Commit

Permalink
Merge 3539341 into d95753a
Browse files Browse the repository at this point in the history
  • Loading branch information
B4nan committed Dec 13, 2019
2 parents d95753a + 3539341 commit 30e731d
Show file tree
Hide file tree
Showing 25 changed files with 240 additions and 146 deletions.
2 changes: 1 addition & 1 deletion .codeclimate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ checks:
threshold: 500
method-lines:
config:
threshold: 30
threshold: 50
method-count:
config:
threshold: 40
Expand Down
13 changes: 13 additions & 0 deletions docs/docs/entity-helper.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,19 @@ console.log(book.author); // instance of Author with id: '...id...'
console.log(book.author.id); // '...id...'
```

To use `entity.assign()` on not managed entities, you need to provide `EntityManager`
instance explicitly:

```typescript
import { wrap } from 'mikro-orm';

const book = new Book();
wrap(book).assign({
title: 'Better Book 1',
author: '...id...',
}, { em: orm.em });
```

By default, `entity.assign(data)` behaves same way as `Object.assign(entity, data)`,
e.g. it does not merge things recursively. To enable deep merging of object properties,
use second parameter to enable `mergeObjects` flag:
Expand Down
14 changes: 14 additions & 0 deletions docs/docs/upgrading-v2-to-v3.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,20 @@ override the order column name via `fixedOrderColumn: 'order'`.

You can also specify default ordering via `orderBy: { ... }` attribute.

## EntityAssigner.assign() requires EM for new entities

Previously all entities had internal reference to the root EM - the one created when
initializing the ORM. Now only managed entities (those merged to the EM, e.g. loaded
from the database) have this internal reference.

To use `assign()` method on new (not managed) entities, you need to provide the `em`
parameter:

```typescript
const book = new Book();
wrap(book).assign(data, { em: orm.em });
```

## Strict FilterQuery and smart query conditions

`FilterQuery` now does not allow using smart query operators. You can either cast your condition
Expand Down
7 changes: 4 additions & 3 deletions lib/EntityManager.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { v4 as uuid } from 'uuid';

import { Configuration, RequestContext, Utils, ValidationError } from './utils';
import { EntityAssigner, EntityFactory, EntityLoader, EntityRepository, EntityValidator, IdentifiedReference, Reference, ReferenceType, wrap } from './entity';
import { LockMode, UnitOfWork } from './unit-of-work';
Expand All @@ -9,11 +11,12 @@ import { Transaction } from './connections';

export class EntityManager<D extends IDatabaseDriver = IDatabaseDriver> {

readonly id = uuid();
private readonly validator = new EntityValidator(this.config.get('strict'));
private readonly repositoryMap: Record<string, EntityRepository<AnyEntity>> = {};
private readonly entityLoader: EntityLoader = new EntityLoader(this);
private readonly unitOfWork = new UnitOfWork(this);
private readonly entityFactory = new EntityFactory(this.unitOfWork, this.driver, this.config, this.metadata);
private readonly entityFactory = new EntityFactory(this.unitOfWork, this);
private transactionContext?: Transaction;

constructor(readonly config: Configuration,
Expand Down Expand Up @@ -208,12 +211,10 @@ export class EntityManager<D extends IDatabaseDriver = IDatabaseDriver> {
let entity = this.getUnitOfWork().tryGetById<T>(entityName, data as FilterQuery<T>);

if (entity && wrap(entity).isInitialized() && !refresh) {
Object.defineProperty(entity, '__em', { value: this, writable: true });
return entity;
}

entity = Utils.isEntity<T>(data) ? data : this.getEntityFactory().create<T>(entityName, data as EntityData<T>);
Object.defineProperty(entity, '__em', { value: this, writable: true });

// add to IM immediately - needed for self-references that can be part of `data` (and do not trigger cascade merge)
this.getUnitOfWork().merge(entity, [entity]);
Expand Down
4 changes: 2 additions & 2 deletions lib/entity/ArrayCollection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export class ArrayCollection<T extends AnyEntity<T>> {

toArray(): Dictionary[] {
return this.getItems().map(item => {
const meta = wrap(this.owner).__em.getMetadata().get(item.constructor.name);
const meta = wrap(this.owner).__internal.metadata.get(item.constructor.name);
const args = [...meta.toJsonParams.map(() => undefined), [this.property.name]];

return wrap(item).toJSON(...args);
Expand Down Expand Up @@ -141,7 +141,7 @@ export class ArrayCollection<T extends AnyEntity<T>> {

protected get property() {
if (!this._property) {
const meta = wrap(this.owner).__em.getMetadata().get(this.owner.constructor.name);
const meta = wrap(this.owner).__meta;
const field = Object.keys(meta.properties).find(k => this.owner[k] === this);
this._property = meta.properties[field!];
}
Expand Down
51 changes: 24 additions & 27 deletions lib/entity/Collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Dictionary, FilterQuery, Primary, QueryOrder, QueryOrderMap, Utils, wra
import { EntityData, AnyEntity } from '../types';
import { ArrayCollection } from './ArrayCollection';
import { ReferenceType } from './enums';
import { ValidationError } from '../utils';

export class Collection<T extends AnyEntity<T>> extends ArrayCollection<T> {

Expand All @@ -20,34 +21,29 @@ export class Collection<T extends AnyEntity<T>> extends ArrayCollection<T> {
}

add(...items: T[]): void {
items = items.map(item => this.mapToEntity(item));
items.map(item => this.validateItemType(item));
this.modify('add', items);

for (const item of items) {
wrap(this.owner).__em.getUnitOfWork().cancelOrphanRemoval(item);
}
this.cancelOrphanRemoval(items);
}

set(items: T[], initialize = false): void {
if (initialize) {
this.initialized = true;
}

items = items.map(item => this.mapToEntity(item));
items.map(item => this.validateItemType(item));
super.set(items);
this.dirty = !initialize;

for (const item of items) {
wrap(this.owner).__em.getUnitOfWork().cancelOrphanRemoval(item);
}
this.setDirty(!initialize);
this.cancelOrphanRemoval(items);
}

remove(...items: T[]): void {
this.modify('remove', items);
const em = wrap(this.owner).__em;

if (this.property.orphanRemoval) {
if (this.property.orphanRemoval && em) {
for (const item of items) {
wrap(this.owner).__em.getUnitOfWork().scheduleOrphanRemoval(item);
em.getUnitOfWork().scheduleOrphanRemoval(item);
}
}
}
Expand Down Expand Up @@ -92,6 +88,10 @@ export class Collection<T extends AnyEntity<T>> extends ArrayCollection<T> {
const options = Utils.isObject<InitOptions<T>>(populate) ? populate : { populate, where, orderBy };
const em = wrap(this.owner).__em;

if (!em) {
throw ValidationError.entityNotManaged(this.owner);
}

if (!this.initialized && this.property.reference === ReferenceType.MANY_TO_MANY && em.getDriver().getPlatform().usesPivotTable()) {
const map = await em.getDriver().loadFromPivotTable<T>(this.property, [wrap(this.owner).__primaryKey], options.where, options.orderBy);
this.set(map[wrap(this.owner).__primaryKey].map(item => em.merge<T>(this.property.type, item)), true);
Expand Down Expand Up @@ -148,7 +148,7 @@ export class Collection<T extends AnyEntity<T>> extends ArrayCollection<T> {
}

private createManyToManyCondition(cond: Dictionary) {
if (this.property.owner || wrap(this.owner).__em.getDriver().getPlatform().usesPivotTable()) {
if (this.property.owner || wrap(this.owner).__internal.platform.usesPivotTable()) {
const pk = wrap(this.items[0]).__meta.primaryKey; // we know there is at least one item as it was checked in load method
cond[pk] = { $in: this.items.map(item => wrap(item).__primaryKey) };
} else {
Expand Down Expand Up @@ -177,25 +177,22 @@ export class Collection<T extends AnyEntity<T>> extends ArrayCollection<T> {
}
}

private mapToEntity(item: T | Primary<T> | EntityData<T>): T {
if (Utils.isEntity(item)) {
return item as T;
}

private cancelOrphanRemoval(items: T[]): void {
const em = wrap(this.owner).__em;
let entity: T;

if (Utils.isPrimaryKey<T>(item)) {
entity = em.getReference(this.property.type, item);
} else {
entity = em.create(this.property.type, item);
if (!em) {
return;
}

if (wrap(entity).__primaryKey) {
return em.merge<T>(this.property.type, entity);
for (const item of items) {
em!.getUnitOfWork().cancelOrphanRemoval(item);
}
}

return entity;
private validateItemType(item: T | Primary<T> | EntityData<T>): void {
if (!Utils.isEntity(item)) {
throw ValidationError.notEntity(this.owner, this.property, item);
}
}

}
Expand Down
26 changes: 18 additions & 8 deletions lib/entity/EntityAssigner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,11 @@ export class EntityAssigner {
static assign<T extends AnyEntity<T>>(entity: T, data: EntityData<T>, options?: AssignOptions): T;
static assign<T extends AnyEntity<T>>(entity: T, data: EntityData<T>, onlyProperties?: boolean): T;
static assign<T extends AnyEntity<T>>(entity: T, data: EntityData<T>, onlyProperties: AssignOptions | boolean = false): T {
const em = wrap(entity).__em;
const meta = em.getMetadata().get(entity.constructor.name);
const props = meta.properties;
const options = (typeof onlyProperties === 'boolean' ? { onlyProperties } : onlyProperties);
const em = options.em || wrap(entity).__em;
const meta = wrap(entity).__internal.metadata.get(entity.constructor.name);
const validator = wrap(entity).__internal.validator;
const props = meta.properties;

Object.keys(data).forEach(prop => {
if (options.onlyProperties && !(prop in props)) {
Expand All @@ -24,16 +25,16 @@ export class EntityAssigner {

const value = data[prop as keyof EntityData<T>];

if (props[prop] && [ReferenceType.MANY_TO_ONE, ReferenceType.ONE_TO_ONE].includes(props[prop].reference) && value) {
return EntityAssigner.assignReference<T>(entity, value, props[prop], em);
if (props[prop] && [ReferenceType.MANY_TO_ONE, ReferenceType.ONE_TO_ONE].includes(props[prop].reference) && value && EntityAssigner.validateEM(em)) {
return EntityAssigner.assignReference<T>(entity, value, props[prop], em!);
}

if (props[prop] && Utils.isCollection(entity[prop as keyof T], props[prop]) && Array.isArray(value)) {
return EntityAssigner.assignCollection<T>(entity, entity[prop as keyof T] as unknown as Collection<AnyEntity>, value, props[prop], em);
if (props[prop] && Utils.isCollection(entity[prop as keyof T], props[prop]) && Array.isArray(value) && EntityAssigner.validateEM(em)) {
return EntityAssigner.assignCollection<T>(entity, entity[prop as keyof T] as unknown as Collection<AnyEntity>, value, props[prop], em!);
}

if (props[prop] && props[prop].reference === ReferenceType.SCALAR && SCALAR_TYPES.includes(props[prop].type) && (!props[prop].getter || props[prop].setter)) {
return entity[prop as keyof T] = em.getValidator().validateProperty(props[prop], value, entity);
return entity[prop as keyof T] = validator.validateProperty(props[prop], value, entity);
}

if (options.mergeObjects && Utils.isObject(value)) {
Expand Down Expand Up @@ -64,6 +65,14 @@ export class EntityAssigner {
}
}

private static validateEM(em?: EntityManager): boolean {
if (!em) {
throw new Error(`To use assign() on not managed entities, explicitly provide EM instance: wrap(entity).assign(data, { em: orm.em })`);
}

return true;
}

private static assignReference<T extends AnyEntity<T>>(entity: T, value: any, prop: EntityProperty, em: EntityManager): void {
let valid = false;

Expand Down Expand Up @@ -122,4 +131,5 @@ export class EntityAssigner {
export interface AssignOptions {
onlyProperties?: boolean;
mergeObjects?: boolean;
em?: EntityManager;
}
14 changes: 7 additions & 7 deletions lib/entity/EntityFactory.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import { Configuration, Utils } from '../utils';
import { Utils } from '../utils';
import { AnyEntity, Constructor, EntityData, EntityMetadata, EntityName, Primary } from '../types';
import { MetadataStorage } from '../metadata';
import { UnitOfWork } from '../unit-of-work';
import { ReferenceType } from './enums';
import { IDatabaseDriver } from '..';
import { EntityManager } from '..';

export const SCALAR_TYPES = ['string', 'number', 'boolean', 'Date'];

export class EntityFactory {

private readonly hydrator = this.config.getHydrator(this);
private readonly driver = this.em.getDriver();
private readonly config = this.em.config;
private readonly metadata = this.em.getMetadata();
private readonly hydrator = this.config.getHydrator(this, this.em);

constructor(private readonly unitOfWork: UnitOfWork,
private readonly driver: IDatabaseDriver,
private readonly config: Configuration,
private readonly metadata: MetadataStorage) { }
private readonly em: EntityManager) { }

create<T extends AnyEntity<T>>(entityName: EntityName<T>, data: EntityData<T>, initialized = true, newEntity = false): T {
entityName = Utils.className(entityName);
Expand Down
27 changes: 21 additions & 6 deletions lib/entity/EntityHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,19 @@ import { EntityTransformer } from './EntityTransformer';
import { AssignOptions, EntityAssigner } from './EntityAssigner';
import { LockMode } from '../unit-of-work';
import { Reference } from './Reference';
import { Platform } from '../platforms';
import { ValidationError } from '../utils';

export class EntityHelper {

static async init<T extends AnyEntity<T>>(entity: T, populated = true, lockMode?: LockMode): Promise<T> {
await wrap(entity).__em.findOne(entity.constructor.name, entity, { refresh: true, lockMode });
const em = wrap(entity).__em;

if (!em) {
throw ValidationError.entityNotManaged(entity);
}

await em.findOne(entity.constructor.name, entity, { refresh: true, lockMode });
wrap(entity).populated(populated);
Object.defineProperty(entity, '__lazyInitialized', { value: true, writable: true });

Expand All @@ -20,7 +28,7 @@ export class EntityHelper {
const pk = meta.properties[meta.primaryKey];

if (pk.name === '_id') {
EntityHelper.defineIdProperty(meta, em);
EntityHelper.defineIdProperty(meta, em.getDriver().getPlatform());
}

EntityHelper.defineBaseProperties(meta, em);
Expand All @@ -44,24 +52,31 @@ export class EntityHelper {
/**
* defines magic id property getter/setter if PK property is `_id` and there is no `id` property defined
*/
private static defineIdProperty<T extends AnyEntity<T>>(meta: EntityMetadata<T>, em: EntityManager): void {
private static defineIdProperty<T extends AnyEntity<T>>(meta: EntityMetadata<T>, platform: Platform): void {
Object.defineProperty(meta.prototype, 'id', {
get(): string | null {
return this._id ? em.getDriver().getPlatform().normalizePrimaryKey<string>(this._id) : null;
return this._id ? platform.normalizePrimaryKey<string>(this._id) : null;
},
set(id: string): void {
this._id = id ? em.getDriver().getPlatform().denormalizePrimaryKey(id) : null;
this._id = id ? platform.denormalizePrimaryKey(id) : null;
},
});
}

private static defineBaseProperties<T extends AnyEntity<T>>(meta: EntityMetadata<T>, em: EntityManager) {
const internal = {
platform: em.getDriver().getPlatform(),
metadata: em.getMetadata(),
validator: em.getValidator(),
};

Object.defineProperties(meta.prototype, {
__populated: { value: false, writable: true },
__lazyInitialized: { value: false, writable: true },
__entity: { value: true },
__em: { value: em, writable: true },
__em: { value: undefined, writable: true },
__meta: { value: meta },
__internal: { value: internal },
__uuid: {
get(): string {
if (!this.___uuid) {
Expand Down
7 changes: 3 additions & 4 deletions lib/entity/EntityTransformer.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { Utils } from '../utils';
import { ArrayCollection } from './ArrayCollection';
import { Collection } from './Collection';
import { EntityData, EntityMetadata, EntityProperty, AnyEntity, IPrimaryKey } from '../types';
import { AnyEntity, EntityData, EntityMetadata, EntityProperty, IPrimaryKey } from '../types';
import { Reference } from './Reference';
import { wrap } from './EntityHelper';

export class EntityTransformer {

static toObject<T extends AnyEntity<T>>(entity: T, ignoreFields: string[] = [], visited: string[] = []): EntityData<T> {
const wrapped = wrap(entity);
const platform = wrapped.__em.getDriver().getPlatform();
const platform = wrapped.__internal.platform;
const pk = platform.getSerializedPrimaryKeyField(wrapped.__meta.primaryKey);
const meta = wrapped.__meta;
const pkProp = meta.properties[meta.primaryKey];
Expand Down Expand Up @@ -60,14 +60,13 @@ export class EntityTransformer {

private static processEntity<T extends AnyEntity<T>>(prop: keyof T, entity: T, ignoreFields: string[], visited: string[]): T[keyof T] | undefined {
const child = wrap(entity[prop] as unknown as T | Reference<T>);
const platform = child.__em.getDriver().getPlatform();

if (child.isInitialized() && child.__populated && child !== entity && !child.__lazyInitialized) {
const args = [...child.__meta.toJsonParams.map(() => undefined), ignoreFields, visited];
return child.toJSON(...args) as T[keyof T];
}

return platform.normalizePrimaryKey(child.__primaryKey as IPrimaryKey) as unknown as T[keyof T];
return child.__internal.platform.normalizePrimaryKey(child.__primaryKey as IPrimaryKey) as unknown as T[keyof T];
}

private static processCollection<T extends AnyEntity<T>>(prop: keyof T, entity: T): T[keyof T] | undefined {
Expand Down
Loading

0 comments on commit 30e731d

Please sign in to comment.