Skip to content

Commit

Permalink
feat(core): make assign options configurable globally
Browse files Browse the repository at this point in the history
Also changes how assigning to object properties work once again. This was changed in v5 with a refactoring related to embedded entities, but mistakenly propagated to the JSON properties too.

Now we do not merge data when assigning to JSON properties by default, and require explicit `mergeObjectProperties: true` in the options. For assigning to embedded properties, this does not apply, and a new option called `mergeEmbeddedProperties` is introduced to allow disabling the default behavior.

Closes #5410
  • Loading branch information
B4nan committed Apr 4, 2024
1 parent bb1a3f9 commit bc9f6f5
Show file tree
Hide file tree
Showing 9 changed files with 43 additions and 16 deletions.
16 changes: 16 additions & 0 deletions docs/docs/entity-helper.md
Expand Up @@ -53,6 +53,8 @@ wrap(book).assign({ meta: { foo: 4 } });
console.log(book.meta); // { foo: 4 }
```

One exception to this rule is assigning to embedded properties. Those are by default merged with the data recursively. You can opt out of that via `mergeEmbeddedProperties` flag (which defaults to `true`).

### Updating deep entity graph

Since v5, `assign` allows updating deep entity graph by default. To update existing entity, we need to provide its PK in the `data`, as well as to **load that entity first into current context**.
Expand Down Expand Up @@ -144,6 +146,20 @@ class UpdateAuthorDTO extends PlainObject {
em.assign(user, dto);
```

### Global configuration

Since v6.2, you can also configure how the `assign` helper works globally:

```ts
await MikroORM.init({
// default values:
updateNestedEntities: true,
updateByPrimaryKey: true,
mergeObjectProperties: false,
mergeEmbeddedProperties: true,
});
```

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

`IWrappedEntity` is an interface that defines public helper methods provided by the ORM:
Expand Down
15 changes: 7 additions & 8 deletions packages/core/src/entity/EntityAssigner.ts
Expand Up @@ -10,11 +10,11 @@ import type {
EntityKey,
EntityProperty,
EntityValue,
Primary,
RequiredEntityData,
IsSubset,
FromEntityType,
IsSubset,
MergeSelected,
Primary,
RequiredEntityData,
} from '../typings';
import { Utils } from '../utils/Utils';
import { Reference } from './Reference';
Expand Down Expand Up @@ -44,9 +44,7 @@ export class EntityAssigner {
opts.visited.add(entity);
const wrapped = helper(entity);
opts = {
updateNestedEntities: true,
updateByPrimaryKey: true,
mergeObjectProperties: true,
...wrapped.__config.get('assign'),
schema: wrapped.__schema,
...opts, // allow overriding the defaults
};
Expand Down Expand Up @@ -266,9 +264,9 @@ export class EntityAssigner {

const create = () => EntityAssigner.validateEM(em) && em!.getEntityFactory().createEmbeddable<T>(prop.type, value, {
convertCustomTypes: options.convertCustomTypes,
newEntity: options.mergeObjectProperties ? !('propName' in entity) : true,
newEntity: options.mergeEmbeddedProperties ? !('propName' in entity) : true,
});
entity[propName] = (options.mergeObjectProperties ? (entity[propName] || create()) : create()) as EntityValue<T>;
entity[propName] = (options.mergeEmbeddedProperties ? (entity[propName] || create()) : create()) as EntityValue<T>;

Object.keys(value).forEach(key => {
EntityAssigner.assignProperty(entity[propName], key, prop.embeddedProps, value, options);
Expand Down Expand Up @@ -308,6 +306,7 @@ export interface AssignOptions<Convert extends boolean> {
onlyOwnProperties?: boolean;
convertCustomTypes?: Convert;
mergeObjectProperties?: boolean;
mergeEmbeddedProperties?: boolean;
merge?: boolean;
schema?: string;
em?: EntityManager;
Expand Down
8 changes: 8 additions & 0 deletions packages/core/src/utils/Configuration.ts
Expand Up @@ -31,6 +31,7 @@ import type { MetadataProvider } from '../metadata/MetadataProvider';
import type { MetadataStorage } from '../metadata/MetadataStorage';
import { ReflectMetadataProvider } from '../metadata/ReflectMetadataProvider';
import type { EventSubscriber } from '../events';
import type { AssignOptions } from '../entity/EntityAssigner';
import type { EntityManagerType, IDatabaseDriver } from '../drivers/IDatabaseDriver';
import { NotFoundError } from '../errors';
import { RequestContext } from './RequestContext';
Expand Down Expand Up @@ -85,6 +86,12 @@ export class Configuration<D extends IDatabaseDriver = IDatabaseDriver, EM exten
serialization: {
includePrimaryKeys: true,
},
assign: {
updateNestedEntities: true,
updateByPrimaryKey: true,
mergeObjectProperties: false,
mergeEmbeddedProperties: true,
},
persistOnCreate: true,
forceEntityConstructor: false,
forceUndefined: false,
Expand Down Expand Up @@ -549,6 +556,7 @@ export interface MikroORMOptions<D extends IDatabaseDriver = IDatabaseDriver, EM
/** Enforce unpopulated references to be returned as objects, e.g. `{ author: { id: 1 } }` instead of `{ author: 1 }`. */
forceObject?: boolean;
};
assign: AssignOptions<boolean>;
persistOnCreate: boolean;
forceEntityConstructor: boolean | (Constructor<AnyEntity> | string)[];
forceUndefined: boolean;
Expand Down
Expand Up @@ -461,7 +461,7 @@ describe('embedded entities in mongo', () => {
expect(jon.profile1.identity.meta!.foo).toBe('f');
expect(jon.profile1.identity.meta).toBeInstanceOf(IdentityMeta);

orm.em.assign(jon, { profile1: { identity: { email: 'e4' } } }, { mergeObjectProperties: false });
orm.em.assign(jon, { profile1: { identity: { email: 'e4' } } }, { mergeEmbeddedProperties: false });
expect(jon.profile1.username).toBeUndefined();
expect(jon.profile1.identity.email).toBe('e4');
expect(jon.profile1.identity.meta).toBeUndefined();
Expand Down
Expand Up @@ -449,7 +449,7 @@ describe('embedded entities in postgres', () => {
expect(jon.profile1.identity.meta!.foo).toBe('f');
expect(jon.profile1.identity.meta).toBeInstanceOf(IdentityMeta);

orm.em.assign(jon, { profile1: { identity: { email: 'e4' } } }, { mergeObjectProperties: false });
orm.em.assign(jon, { profile1: { identity: { email: 'e4' } } }, { mergeEmbeddedProperties: false });
expect(jon.profile1.username).toBeUndefined();
expect(jon.profile1.identity.email).toBe('e4');
expect(jon.profile1.identity.meta).toBeUndefined();
Expand Down
Expand Up @@ -279,7 +279,7 @@ describe('embedded entities in mongo', () => {
expect(jon.profile1.identity.meta!.foo).toBe('f');
expect(jon.profile1.identity.meta).toBeInstanceOf(IdentityMeta);

orm.em.assign(jon, { profile1: { identity: { email: 'e4' } } }, { mergeObjectProperties: false });
orm.em.assign(jon, { profile1: { identity: { email: 'e4' } } }, { mergeEmbeddedProperties: false });
expect(jon.profile1.username).toBeUndefined();
expect(jon.profile1.identity.email).toBe('e4');
expect(jon.profile1.identity.meta).toBeUndefined();
Expand Down
Expand Up @@ -353,10 +353,11 @@ describe('embedded entities in postgres', () => {
delete jon.profile1.identity.meta;

orm.em.assign(jon, { profile1: { identity: { meta: { foo: 'f' } } } });
expect(jon.profile1.identity.email).toBe('e3');
expect(jon.profile1.identity.meta!.foo).toBe('f');
expect(jon.profile1.identity.meta).toBeInstanceOf(IdentityMeta);

orm.em.assign(jon, { profile1: { identity: { email: 'e4' } } }, { mergeObjectProperties: false });
orm.em.assign(jon, { profile1: { identity: { email: 'e4' } } }, { mergeEmbeddedProperties: false });
expect(jon.profile1.username).toBeUndefined();
expect(jon.profile1.identity.email).toBe('e4');
expect(jon.profile1.identity.meta).toBeUndefined();
Expand Down
8 changes: 4 additions & 4 deletions tests/features/entity-assigner/EntityAssigner.mysql.test.ts
Expand Up @@ -192,14 +192,14 @@ describe('EntityAssignerMySql', () => {
book.meta = { items: 5, category: 'test' };
wrap(book).assign({ meta: { items: 3, category: 'foo' } });
expect(book.meta).toEqual({ items: 3, category: 'foo' });
wrap(book).assign({ meta: { category: 'bar' } });
wrap(book).assign({ meta: { category: 'bar' } }, { mergeObjectProperties: true });
expect(book.meta).toEqual({ items: 3, category: 'bar' });
wrap(book).assign({ meta: { category: 'bar 1' } });
expect(book.meta).toEqual({ items: 3, category: 'bar 1' });
wrap(book).assign({ meta: { category: 'bar 2' } }, { mergeObjectProperties: false });
expect(book.meta).toEqual({ category: 'bar 1' });
wrap(book).assign({ meta: { category: 'bar 2' } });
expect(book.meta).toEqual({ category: 'bar 2' });
jon.identities = ['1', '2'];
wrap(jon).assign({ identities: ['3', '4'] });
wrap(jon).assign({ identities: ['3', '4'] }, { mergeObjectProperties: true });
expect(jon.identities).toEqual(['3', '4']);
});

Expand Down
3 changes: 3 additions & 0 deletions tests/features/entity-assigner/GH5158.test.ts
Expand Up @@ -24,6 +24,9 @@ beforeAll(async () => {
orm = await MikroORM.init({
dbName: '5158',
entities: [User],
assign: {
mergeObjectProperties: true,
},
});
});

Expand Down

0 comments on commit bc9f6f5

Please sign in to comment.