Skip to content

Commit

Permalink
feat(core): allow updating nested 1:1 and m:1 references with EntityA…
Browse files Browse the repository at this point in the history
…ssigner (#1535)

Adds `updateNestedEntities` flag that mimics the `mergeObjects` flag for M:1 and 1:1 relations.

```ts
const authorEntity = new Author('Jon2 Snow', 'snow3@wall.st');
book.author = authorEntity;
wrap(book).assign({ author: { name: 'Jon Snow2' } }, { updateNestedEntities: true });
console.log(book.author); // { ... name: 'Jon Snow2' ... }
console.log(book.author === authorEntity) // true

// this will have no effect as author is an entity
wrap(book).assign({ author: { name: 'Jon Snow2' } }, { mergeObjects: true });
console.log(book.author); // { ... name: 'Jon Snow2' ... }
console.log(book.author === authorEntity) // false
```
  • Loading branch information
jsprw committed Mar 16, 2021
1 parent 0fde021 commit c1dd048
Show file tree
Hide file tree
Showing 3 changed files with 112 additions and 3 deletions.
20 changes: 19 additions & 1 deletion docs/docs/entity-helper.md
Expand Up @@ -44,7 +44,7 @@ wrap(book).assign({
```

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,
e.g. it does not merge things recursively. To enable deep merging of object properties (not referenced entities),
use second parameter to enable `mergeObjects` flag:

```typescript
Expand All @@ -59,6 +59,24 @@ wrap(book).assign({ meta: { foo: 4 } });
console.log(book.meta); // { foo: 4 }
```

To get the same behavior as `mergeObjects` flag for m:1 and 1:1 references, enable the `updateNestedEntities` flag.

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

const authorEntity = new Author('Jon2 Snow', 'snow3@wall.st');
book.author = authorEntity;

wrap(book).assign({ author: { name: 'Jon Snow2' } }, { updateNestedEntities: true });
console.log(book.author); // { ... name: 'Jon Snow2' ... }
console.log(book.author === authorEntity) // true

//this will have no influence as author is an entity
wrap(book).assign({ author: { name: 'Jon Snow2' } }, { mergeObjects: true });
console.log(book.author); // { ... name: 'Jon Snow2' ... }
console.log(book.author === authorEntity) // false
```

## `WrappedEntity` and `wrap()` helper

`IWrappedEntity` is an interface that defines public helper methods provided
Expand Down
12 changes: 12 additions & 0 deletions packages/core/src/entity/EntityAssigner.ts
Expand Up @@ -6,6 +6,7 @@ import { Utils } from '../utils/Utils';
import { Reference } from './Reference';
import { ReferenceType, SCALAR_TYPES } from '../enums';
import { EntityValidator } from './EntityValidator';
import { wrap } from './wrap';

const validator = new EntityValidator(false);

Expand Down Expand Up @@ -43,6 +44,16 @@ export class EntityAssigner {
}

if ([ReferenceType.MANY_TO_ONE, ReferenceType.ONE_TO_ONE].includes(props[prop]?.reference) && Utils.isDefined(value, true) && EntityAssigner.validateEM(em)) {

// eslint-disable-next-line no-prototype-builtins
if (options.updateNestedEntities && entity.hasOwnProperty(prop) && (Utils.isEntity(entity[prop]) || Reference.isReference(entity[prop])) && Utils.isPlainObject(value)) {
const unwrappedEntity = Reference.unwrapReference(entity[prop]);

if (wrap(unwrappedEntity).isInitialized()) {
return EntityAssigner.assign(unwrappedEntity, value, options);
}
}

return EntityAssigner.assignReference<T>(entity, value, props[prop], em!, options);
}

Expand Down Expand Up @@ -172,6 +183,7 @@ export class EntityAssigner {
export const assign = EntityAssigner.assign;

export interface AssignOptions {
updateNestedEntities?: boolean;
onlyProperties?: boolean;
convertCustomTypes?: boolean;
mergeObjects?: boolean;
Expand Down
83 changes: 81 additions & 2 deletions tests/EntityAssigner.mysql.test.ts
@@ -1,7 +1,7 @@
import { MikroORM, wrap } from '@mikro-orm/core';
import { MikroORM, Reference, wrap } from '@mikro-orm/core';
import { MySqlDriver } from '@mikro-orm/mysql';
import { initORMMySql, wipeDatabaseMySql } from './bootstrap';
import { Author2, Book2, BookTag2, FooBar2 } from './entities-sql';
import { Author2, Book2, BookTag2, FooBar2, Publisher2, PublisherType } from './entities-sql';

describe('EntityAssignerMySql', () => {

Expand Down Expand Up @@ -76,6 +76,85 @@ describe('EntityAssignerMySql', () => {
expect(book2.tags.getIdentifiers()).toMatchObject([tag2.id]);
});

test('assign() should update m:1 or 1:1 nested entities [mysql]', async () => {
const jon = new Author2('Jon Snow', 'snow@wall.st');
const book1 = new Book2('Book2', jon);
const jon2 = new Author2('Jon2 Snow', 'snow3@wall.st');
const book2 = new Book2('Book2', jon2);
await orm.em.persistAndFlush(book1);
await orm.em.persistAndFlush(book2);
wrap(book1).assign({ author: { name: 'Jon Snow2' } });
expect(book1.author.name).toEqual('Jon Snow2');
expect(book1.author.email).toBeUndefined();
expect(book1.author).not.toEqual(jon);

wrap(book2).assign({ author: { name: 'Jon Snow2' } }, { updateNestedEntities: true });
expect(book2.author.name).toEqual('Jon Snow2');
expect(book2.author.email).toEqual('snow3@wall.st');
expect(book2.author).toEqual(jon2);
});

test('assign() with updateNestedEntities flag should ignore not initialized entities [mysql]', async () => {
const jon = new Author2('Jon2 Snow', 'snow3@wall.st');
const book = new Book2('Book2', jon);
const publisher = new Publisher2('Good Books LLC', PublisherType.LOCAL);
book.publisher = Reference.create(publisher);
await orm.em.persistAndFlush(book);

const id = book.uuid;

orm.em.clear();

const book2 = (await orm.em.getRepository(Book2).findOne(id))!;
const originalAuthorRef = book2.author;
const originalPublisherWrappedRef = book2.publisher;

expect(Reference.isReference(book2.author)).toEqual(false);
expect(wrap(book2.author).isInitialized()).toEqual(false);

expect(wrap(book2.publisher).isInitialized()).toEqual(false);
expect(Reference.isReference(book2.publisher)).toEqual(true);

const value = Reference.unwrapReference(book2.publisher!);

wrap(book2).assign({ author: { name: 'Jon Snow2' }, publisher: { name: 'Better Books LLC' } }, { updateNestedEntities: true });

expect(book2.author).not.toEqual(originalAuthorRef);
expect(book2.publisher).not.toEqual(originalPublisherWrappedRef);
});

test('assign() with updateNestedEntities flag should update wrapped initialized entities [mysql]', async () => {
const jon = new Author2('Jon2 Snow', 'snow3@wall.st');
const book = new Book2('Book2', jon);
const publisher = new Publisher2('Good Books LLC', PublisherType.LOCAL);
book.publisher = Reference.create(publisher);
await orm.em.persistAndFlush(book);

const id = book.uuid;

orm.em.clear();

const book2 = (await orm.em.getRepository(Book2).findOne(id))!;

expect(wrap(book2.publisher).isInitialized()).toEqual(false);
expect(Reference.isReference(book2.publisher)).toEqual(true);

await book2.publisher?.load();

expect(wrap(book2.publisher).isInitialized()).toEqual(true);

const originalValue = Reference.unwrapReference(book2.publisher!);
const originalRef = book2.publisher!;
expect(originalValue.name).toEqual('Good Books LLC');

wrap(book2).assign({ author: { name: 'Jon Snow2' }, publisher: { name: 'Better Books LLC' } }, { updateNestedEntities: true });

// this means that the original object has been replaced, something updateNestedEntities does not do
expect(book2.publisher).toEqual(originalRef);
expect(book2.publisher!.unwrap()).toEqual(originalValue);
expect(book2.publisher!.unwrap().name).toEqual('Better Books LLC');
});

test('assign() should update not initialized collection [mysql]', async () => {
const other = new BookTag2('other');
await orm.em.persistAndFlush(other);
Expand Down

0 comments on commit c1dd048

Please sign in to comment.