diff --git a/packages/core/src/decorators/Embedded.ts b/packages/core/src/decorators/Embedded.ts index a8d0ff9e2768..ad2fe46ef778 100644 --- a/packages/core/src/decorators/Embedded.ts +++ b/packages/core/src/decorators/Embedded.ts @@ -30,4 +30,5 @@ export type EmbeddedOptions = { serializer?: (value: any) => any; serializedName?: string; groups?: string[]; + persist?: boolean; }; diff --git a/packages/core/src/hydration/ObjectHydrator.ts b/packages/core/src/hydration/ObjectHydrator.ts index 3cb45dbff0ce..51cb706c6df1 100644 --- a/packages/core/src/hydration/ObjectHydrator.ts +++ b/packages/core/src/hydration/ObjectHydrator.ts @@ -66,6 +66,10 @@ export class ObjectHydrator extends Hydrator { const idx = this.tmpIndex++; const nullVal = this.config.get('forceUndefined') ? 'undefined' : 'null'; + if (prop.getter && !prop.setter) { + return []; + } + if (prop.ref) { context.set('ScalarReference', ScalarReference); ret.push(` const oldValue_${idx} = entity${entityKey};`); diff --git a/packages/core/src/metadata/MetadataDiscovery.ts b/packages/core/src/metadata/MetadataDiscovery.ts index 21a2913a382f..236e98f0a63e 100644 --- a/packages/core/src/metadata/MetadataDiscovery.ts +++ b/packages/core/src/metadata/MetadataDiscovery.ts @@ -962,7 +962,7 @@ export class MetadataDiscovery { this.initFieldName(embeddedProp, rootProperty !== embeddedProp && object); const prefix = embeddedProp.prefix === false ? '' : embeddedProp.prefix === true ? embeddedProp.embeddedPath?.join('_') ?? embeddedProp.fieldNames[0] + '_' : embeddedProp.prefix; - for (const prop of Object.values(embeddable.properties).filter(p => p.persist !== false)) { + for (const prop of Object.values(embeddable.properties)) { const name = (embeddedProp.embeddedPath?.join('_') ?? embeddedProp.fieldNames[0] + '_') + prop.name; meta.properties[name] = Utils.copy(prop, false); @@ -970,6 +970,7 @@ export class MetadataDiscovery { meta.properties[name].embedded = [embeddedProp.name, prop.name]; meta.propertyOrder.set(name, (order += 0.01)); embeddedProp.embeddedProps[prop.name] = meta.properties[name]; + meta.properties[name].persist ??= embeddedProp.persist; if (embeddedProp.nullable) { meta.properties[name].nullable = true; diff --git a/tests/features/embeddables/GH5578.postgres.test.ts b/tests/features/embeddables/GH5578.postgres.test.ts new file mode 100644 index 000000000000..67a6307542af --- /dev/null +++ b/tests/features/embeddables/GH5578.postgres.test.ts @@ -0,0 +1,79 @@ +import { Collection, Embeddable, Embedded, Entity, ManyToOne, MikroORM, OneToMany, PrimaryKey, Property, raw } from '@mikro-orm/postgresql'; + + +@Embeddable() +class Statistic { + + @Property() + revenue!: number; + +} + +@Entity() +class Event { + + @PrimaryKey() + id!: number; + + @Property() + name!: string; + + @OneToMany({ entity: () => Order, mappedBy: order => order.event }) + orders = new Collection(this); + + @Embedded({ entity: () => Statistic, nullable: true, prefix: false, persist: false }) + statistic?: Statistic; + +} + +@Entity() +class Order { + + @PrimaryKey() + id!: number; + + @Property() + total!: number; + + @ManyToOne(() => Event, { nullable: true }) + event!: Event; + +} + +let orm: MikroORM; + +beforeAll(async () => { + orm = await MikroORM.init({ + entities: [Order, Event], + dbName: '5578', + }); + await orm.schema.refreshDatabase(); +}); + +beforeEach(() => orm.schema.clearDatabase()); +afterAll(() => orm.close(true)); + +test('Hydrate non persistent properties on embeddable', async () => { + const eventFoo = orm.em.create(Event, { id: 1, name: 'Foo' }); + const eventBar = orm.em.create(Event, { id: 2, name: 'Bar' }); + orm.em.create(Order, { id: 1, total: 100, event: eventFoo }); + orm.em.create(Order, { id: 2, total: 150, event: eventFoo }); + orm.em.create(Order, { id: 3, total: 40, event: eventBar }); + orm.em.create(Order, { id: 4, total: 20, event: eventBar }); + await orm.em.flush(); + orm.em.clear(); + + const qb = orm.em.createQueryBuilder(Event, 'e'); + + const results = await qb + .select([ + 'e.*', + raw('sum(o.total) as revenue'), + ]) + .leftJoin('e.orders', 'o') + .groupBy('e.id') + .getResult(); + + expect(results.find(e => e.name === 'Foo')?.statistic?.revenue).toBe(250); + expect(results.find(e => e.name === 'Bar')?.statistic?.revenue).toBe(60); +}); diff --git a/tests/features/embeddables/__snapshots__/embeddable-custom-types.postgres.test.ts.snap b/tests/features/embeddables/__snapshots__/embeddable-custom-types.postgres.test.ts.snap index 51b9fa069e82..8ee8122b29b5 100644 --- a/tests/features/embeddables/__snapshots__/embeddable-custom-types.postgres.test.ts.snap +++ b/tests/features/embeddables/__snapshots__/embeddable-custom-types.postgres.test.ts.snap @@ -3,7 +3,7 @@ exports[`embedded entities with custom types schema: embeddables custom types 1 1`] = ` "create table "parent" ("id" serial primary key, "nested_some_value" varchar not null, "nested_deep_some_value" varchar not null, "nested2" jsonb not null, "after" int null, "some_value" varchar null); -create table "user" ("id" serial primary key, "savings_amount" numeric(14,2) not null, "after" int null); +create table "user" ("id" serial primary key, "savings_amount" numeric(14,2) not null, "views" double precision null, "after" int null); " `; diff --git a/tests/features/embeddables/embeddable-custom-types.postgres.test.ts b/tests/features/embeddables/embeddable-custom-types.postgres.test.ts index e3e04ffc0b15..0a6c0148afed 100644 --- a/tests/features/embeddables/embeddable-custom-types.postgres.test.ts +++ b/tests/features/embeddables/embeddable-custom-types.postgres.test.ts @@ -1,5 +1,5 @@ import type { EntityProperty, Platform } from '@mikro-orm/core'; -import { Embeddable, Embedded, Entity, MikroORM, PrimaryKey, Property, Type } from '@mikro-orm/core'; +import { DoubleType, Embeddable, Embedded, Entity, MikroORM, PrimaryKey, Property, Type } from '@mikro-orm/core'; import { PostgreSqlDriver } from '@mikro-orm/postgresql'; import { mockLogger } from '../../helpers'; @@ -95,6 +95,21 @@ class Savings { } +@Embeddable() +class Statistic { + + @Property({ type: DoubleType }) + total: number; + + @Property({ type: DoubleType, persist: true }) + views!: number; + + constructor(total: number) { + this.total = total; + } + +} + @Entity() class User { @@ -104,6 +119,9 @@ class User { @Embedded(() => Savings) savings!: Savings; + @Embedded(() => Statistic, { prefix: false, nullable: true, persist: false }) + statistic?: Statistic; + @Property({ nullable: true }) after?: number; // property after embeddables to verify order props in resulting schema