From 2bbcb477890fc87cb2a349922d83bfdb54f68b59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Ad=C3=A1mek?= Date: Fri, 17 Mar 2023 14:53:40 +0100 Subject: [PATCH] fix(core): fix nested inlined embedded property hydration Closes #4145 --- packages/core/src/hydration/ObjectHydrator.ts | 28 ++++-- tests/features/embeddables/GH4145.test.ts | 91 +++++++++++++++++++ ...entities-in-embeddables.mongo.test.ts.snap | 6 +- ...ities-in-embeddables.postgres.test.ts.snap | 6 +- 4 files changed, 117 insertions(+), 14 deletions(-) create mode 100644 tests/features/embeddables/GH4145.test.ts diff --git a/packages/core/src/hydration/ObjectHydrator.ts b/packages/core/src/hydration/ObjectHydrator.ts index f31e124dc085..9de8feab433f 100644 --- a/packages/core/src/hydration/ObjectHydrator.ts +++ b/packages/core/src/hydration/ObjectHydrator.ts @@ -5,7 +5,6 @@ import { Reference } from '../entity/Reference'; import { Utils } from '../utils/Utils'; import { ReferenceType } from '../enums'; import type { EntityFactory } from '../entity/EntityFactory'; -import { JsonType } from '../types/JsonType'; type EntityHydrator = (entity: T, data: EntityData, factory: EntityFactory, newEntity: boolean, convertCustomTypes: boolean, schema?: string) => void; @@ -219,12 +218,8 @@ export class ObjectHydrator extends Hydrator { } }; - const hydrateEmbedded = (prop: EntityProperty, path: string[], dataKey: string): string[] => { - const entityKey = path.map(k => this.wrap(k)).join(''); - const ret: string[] = []; + const createCond = (prop: EntityProperty, dataKey: string) => { const conds: string[] = []; - registerEmbeddedPrototype(prop, path); - parseObjectEmbeddable(prop, dataKey, ret); if (prop.object) { conds.push(`data${dataKey} != null`); @@ -232,10 +227,27 @@ export class ObjectHydrator extends Hydrator { const notNull = prop.nullable ? '!= null' : '!== undefined'; meta.props .filter(p => p.embedded?.[0] === prop.name) - .forEach(p => conds.push(`data${this.wrap(p.name)} ${notNull}`)); + .forEach(p => { + if (p.reference === ReferenceType.EMBEDDED && !p.object && !p.array) { + conds.push(...createCond(p, dataKey + this.wrap(p.embedded![1]))); + return; + } + + conds.push(`data${this.wrap(p.name)} ${notNull}`); + }); } - ret.push(` if (${conds.join(' || ')}) {`); + return conds; + }; + + const hydrateEmbedded = (prop: EntityProperty, path: string[], dataKey: string): string[] => { + const entityKey = path.map(k => this.wrap(k)).join(''); + const ret: string[] = []; + + registerEmbeddedPrototype(prop, path); + parseObjectEmbeddable(prop, dataKey, ret); + + ret.push(` if (${createCond(prop, dataKey).join(' || ')}) {`); if (prop.targetMeta?.polymorphs) { prop.targetMeta.polymorphs!.forEach(meta => { diff --git a/tests/features/embeddables/GH4145.test.ts b/tests/features/embeddables/GH4145.test.ts new file mode 100644 index 000000000000..3e4d429da673 --- /dev/null +++ b/tests/features/embeddables/GH4145.test.ts @@ -0,0 +1,91 @@ +import { Embeddable, Embedded, Entity, PrimaryKey, Property, wrap } from '@mikro-orm/core'; +import { MikroORM } from '@mikro-orm/better-sqlite'; + +@Embeddable() +class Time { + + @Property() + hour: number; + + @Property() + minute: number; + + constructor(hour: number, minute: number) { + this.hour = hour; + this.minute = minute; + } + +} + +@Embeddable() +class TimeInterval { + + @Embedded(() => Time) + start!: Time; + + @Embedded(() => Time) + end!: Time; + +} + +@Entity() +class Example { + + @PrimaryKey() + name!: string; + + @Embedded(() => TimeInterval) + timeInterval!: TimeInterval; + +} + +describe('embedded entities without other properties', () => { + let orm: MikroORM; + + beforeAll(async () => { + orm = await MikroORM.init({ + entities: [Example], + dbName: ':memory:', + }); + await orm.schema.refreshDatabase(); + }); + + afterAll(async () => { + await orm.close(true); + }); + + test('fetching entity should populate embedded property', async () => { + const example = new Example(); + example.name = 'Test'; + example.timeInterval = new TimeInterval(); + example.timeInterval.start = new Time(9, 0); + example.timeInterval.end = new Time(17, 59); + + expect(wrap(example.timeInterval).toJSON()).toEqual({ + start: { + hour: 9, + minute: 0, + }, + end: { + hour: 17, + minute: 59, + }, + }); + + await orm.em.persistAndFlush(example); + orm.em.clear(); + + const fetched = await orm.em.findOneOrFail(Example, { name: example.name }); + expect(fetched.timeInterval).not.toBeUndefined(); + expect(wrap(fetched.timeInterval).toJSON()).toEqual({ + start: { + hour: 9, + minute: 0, + }, + end: { + hour: 17, + minute: 59, + }, + }); + }); +}); diff --git a/tests/features/embeddables/__snapshots__/entities-in-embeddables.mongo.test.ts.snap b/tests/features/embeddables/__snapshots__/entities-in-embeddables.mongo.test.ts.snap index f4beb4797e11..cfaf9a02a043 100644 --- a/tests/features/embeddables/__snapshots__/entities-in-embeddables.mongo.test.ts.snap +++ b/tests/features/embeddables/__snapshots__/entities-in-embeddables.mongo.test.ts.snap @@ -221,12 +221,12 @@ exports[`embedded entities in mongo diffing 2`] = ` "function(entity, data, factory, newEntity, convertCustomTypes, schema) { if (typeof data._id !== 'undefined') entity._id = data._id; if (typeof data.name !== 'undefined') entity.name = data.name; - if (data.profile1_username !== undefined || data.profile1_identity !== undefined || data.profile1_source !== undefined) { + if (data.profile1_username !== undefined || data.profile1_identity_email !== undefined || data.profile1_identity_meta_foo != null || data.profile1_identity_meta_bar != null || data.profile1_identity_meta_source != null || data.profile1_identity_links !== undefined || data.profile1_identity_source !== undefined || data.profile1_source !== undefined) { if (entity.profile1 == null) { entity.profile1 = factory.createEmbeddable('Profile', data, { newEntity, convertCustomTypes }); } if (typeof data.profile1_username !== 'undefined') entity.profile1.username = data.profile1_username; - if (data.profile1_identity_email !== undefined || data.profile1_identity_meta !== undefined || data.profile1_identity_links !== undefined || data.profile1_identity_source !== undefined) { + if (data.profile1_identity_email !== undefined || data.profile1_identity_meta_foo != null || data.profile1_identity_meta_bar != null || data.profile1_identity_meta_source != null || data.profile1_identity_links !== undefined || data.profile1_identity_source !== undefined) { if (entity.profile1.identity == null) { entity.profile1.identity = factory.createEmbeddable('Identity', data, { newEntity, convertCustomTypes }); } @@ -479,7 +479,7 @@ exports[`embedded entities in mongo diffing 2`] = ` entity.profile1 = factory.createEmbeddable('Profile', data.profile1, { newEntity, convertCustomTypes }); } if (data.profile1 && typeof data.profile1.username !== 'undefined') entity.profile1.username = data.profile1.username; - if (data.profile1_identity_email !== undefined || data.profile1_identity_meta !== undefined || data.profile1_identity_links !== undefined || data.profile1_identity_source !== undefined) { + if (data.profile1_identity_email !== undefined || data.profile1_identity_meta_foo != null || data.profile1_identity_meta_bar != null || data.profile1_identity_meta_source != null || data.profile1_identity_links !== undefined || data.profile1_identity_source !== undefined) { if (entity.profile1.identity == null) { entity.profile1.identity = factory.createEmbeddable('Identity', data, { newEntity, convertCustomTypes }); } diff --git a/tests/features/embeddables/__snapshots__/entities-in-embeddables.postgres.test.ts.snap b/tests/features/embeddables/__snapshots__/entities-in-embeddables.postgres.test.ts.snap index 22d0281a80b8..1609c3988b54 100644 --- a/tests/features/embeddables/__snapshots__/entities-in-embeddables.postgres.test.ts.snap +++ b/tests/features/embeddables/__snapshots__/entities-in-embeddables.postgres.test.ts.snap @@ -221,12 +221,12 @@ exports[`embedded entities in postgres diffing 2`] = ` "function(entity, data, factory, newEntity, convertCustomTypes, schema) { if (typeof data.id !== 'undefined') entity.id = data.id; if (typeof data.name !== 'undefined') entity.name = data.name; - if (data.profile1_username !== undefined || data.profile1_identity !== undefined || data.profile1_source !== undefined) { + if (data.profile1_username !== undefined || data.profile1_identity_email !== undefined || data.profile1_identity_meta_foo != null || data.profile1_identity_meta_bar != null || data.profile1_identity_meta_source != null || data.profile1_identity_links !== undefined || data.profile1_identity_source !== undefined || data.profile1_source !== undefined) { if (entity.profile1 == null) { entity.profile1 = factory.createEmbeddable('Profile', data, { newEntity, convertCustomTypes }); } if (typeof data.profile1_username !== 'undefined') entity.profile1.username = data.profile1_username; - if (data.profile1_identity_email !== undefined || data.profile1_identity_meta !== undefined || data.profile1_identity_links !== undefined || data.profile1_identity_source !== undefined) { + if (data.profile1_identity_email !== undefined || data.profile1_identity_meta_foo != null || data.profile1_identity_meta_bar != null || data.profile1_identity_meta_source != null || data.profile1_identity_links !== undefined || data.profile1_identity_source !== undefined) { if (entity.profile1.identity == null) { entity.profile1.identity = factory.createEmbeddable('Identity', data, { newEntity, convertCustomTypes }); } @@ -479,7 +479,7 @@ exports[`embedded entities in postgres diffing 2`] = ` entity.profile1 = factory.createEmbeddable('Profile', data.profile1, { newEntity, convertCustomTypes }); } if (data.profile1 && typeof data.profile1.username !== 'undefined') entity.profile1.username = data.profile1.username; - if (data.profile1_identity_email !== undefined || data.profile1_identity_meta !== undefined || data.profile1_identity_links !== undefined || data.profile1_identity_source !== undefined) { + if (data.profile1_identity_email !== undefined || data.profile1_identity_meta_foo != null || data.profile1_identity_meta_bar != null || data.profile1_identity_meta_source != null || data.profile1_identity_links !== undefined || data.profile1_identity_source !== undefined) { if (entity.profile1.identity == null) { entity.profile1.identity = factory.createEmbeddable('Identity', data, { newEntity, convertCustomTypes }); }