diff --git a/packages/core/src/unit-of-work/ChangeSetPersister.ts b/packages/core/src/unit-of-work/ChangeSetPersister.ts index a7d45e296285..2fb940099a05 100644 --- a/packages/core/src/unit-of-work/ChangeSetPersister.ts +++ b/packages/core/src/unit-of-work/ChangeSetPersister.ts @@ -1,16 +1,18 @@ import { MetadataStorage } from '../metadata'; -import { AnyEntity, Dictionary, EntityMetadata, EntityProperty, FilterQuery, IPrimaryKey } from '../typings'; +import { AnyEntity, Dictionary, EntityData, EntityMetadata, EntityProperty, FilterQuery, IPrimaryKey } from '../typings'; import { EntityIdentifier, wrap } from '../entity'; import { ChangeSet, ChangeSetType } from './ChangeSet'; import { QueryResult, Transaction } from '../connections'; import { Utils, ValidationError } from '../utils'; import { IDatabaseDriver } from '../drivers'; +import { Hydrator } from '../hydration'; export class ChangeSetPersister { constructor(private readonly driver: IDatabaseDriver, private readonly identifierMap: Dictionary, - private readonly metadata: MetadataStorage) { } + private readonly metadata: MetadataStorage, + private readonly hydrator: Hydrator) { } async persistToDatabase>(changeSet: ChangeSet, ctx?: Transaction): Promise { const meta = this.metadata.get(changeSet.name); @@ -110,11 +112,14 @@ export class ChangeSetPersister { */ private mapReturnedValues>(entity: T, res: QueryResult, meta: EntityMetadata): void { if (res.row && Object.keys(res.row).length > 0) { - Object.values(meta.properties).forEach(prop => { + const data = Object.values(meta.properties).reduce((data, prop) => { if (prop.fieldNames && res.row![prop.fieldNames[0]] && !Utils.isDefined(entity[prop.name], true)) { - entity[prop.name] = res.row![prop.fieldNames[0]]; + data[prop.name] = res.row![prop.fieldNames[0]]; } - }); + + return data; + }, {} as Dictionary); + this.hydrator.hydrate(entity, meta, data as EntityData, false); } } diff --git a/packages/core/src/unit-of-work/UnitOfWork.ts b/packages/core/src/unit-of-work/UnitOfWork.ts index dd6d464c5ebb..a2c157b2943c 100644 --- a/packages/core/src/unit-of-work/UnitOfWork.ts +++ b/packages/core/src/unit-of-work/UnitOfWork.ts @@ -27,7 +27,7 @@ export class UnitOfWork { private readonly metadata = this.em.getMetadata(); private readonly platform = this.em.getDriver().getPlatform(); private readonly changeSetComputer = new ChangeSetComputer(this.em.getValidator(), this.originalEntityData, this.identifierMap, this.collectionUpdates, this.removeStack, this.metadata, this.platform); - private readonly changeSetPersister = new ChangeSetPersister(this.em.getDriver(), this.identifierMap, this.metadata); + private readonly changeSetPersister = new ChangeSetPersister(this.em.getDriver(), this.identifierMap, this.metadata, this.em.config.getHydrator(this.em.getEntityFactory(), this.em)); private working = false; constructor(private readonly em: EntityManager) { } diff --git a/tests/issues/GH725.test.ts b/tests/issues/GH725.test.ts new file mode 100644 index 000000000000..43948c293302 --- /dev/null +++ b/tests/issues/GH725.test.ts @@ -0,0 +1,117 @@ +import { EntitySchema, MikroORM, Type, ValidationError } from '@mikro-orm/core'; +import { AbstractSqlDriver, SchemaGenerator } from '@mikro-orm/knex'; + +export class DateTime { + + constructor(private readonly date: Date) { } + + toDate() { + return this.date; + } + + static fromString(d: string) { + return new DateTime(new Date(d)); + } + +} + +type Maybe = T | null | undefined; + +export type TimestampTypeOptions = { + hasTimeZone: boolean; +}; + +export class DateTimeType extends Type, Maybe> { + + convertToDatabaseValue(value: unknown): Maybe { + if (value === undefined || value === null || value instanceof Date) { + return value; + } + + if (value instanceof DateTime) { + return value.toDate(); + } + + throw ValidationError.invalidType(DateTimeType, value, 'JS'); + } + + convertToJSValue(value: unknown): Maybe { + if (value === undefined || value === null) { + return value; + } + + if (value instanceof Date) { + return new DateTime(value); + } + + throw ValidationError.invalidType(DateTimeType, value, 'database'); + } + + getColumnType(): string { + return 'timestamptz'; + } + +} + +export class Test { + + id!: string; + createdAt!: DateTime; + +} + +export const TestSchema = new EntitySchema({ + class: Test, + properties: { + id: { + primary: true, + type: String, + columnType: 'uuid', + defaultRaw: 'uuid_generate_v4()', + }, + createdAt: { + defaultRaw: 'now()', + type: DateTimeType, + }, + }, +}); + +describe('GH issue 725', () => { + + let orm: MikroORM; + + beforeAll(async () => { + orm = await MikroORM.init({ + entities: [TestSchema], + dbName: `mikro_orm_test_gh_725`, + type: 'postgresql', + }); + await orm.getSchemaGenerator().ensureDatabase(); + await orm.getSchemaGenerator().execute('CREATE EXTENSION IF NOT EXISTS "uuid-ossp"'); + await new SchemaGenerator(orm.em).dropSchema(); + await new SchemaGenerator(orm.em).createSchema(); + }); + + afterAll(() => orm.close(true)); + + test('mapping values from returning statement to custom types', async () => { + const test = new Test(); + orm.em.persist(test); + expect(test.id).toBeUndefined(); + expect(test.createdAt).toBeUndefined(); + + await orm.em.flush(); + expect(typeof test.id).toBe('string'); + expect(test.id).toHaveLength(36); + expect(test.createdAt).toBeInstanceOf(DateTime); + + test.createdAt = DateTime.fromString('2020-01-01T00:00:00Z'); + await orm.em.flush(); + orm.em.clear(); + + const t1 = await orm.em.findOneOrFail(Test, test); + expect(t1.createdAt).toBeInstanceOf(DateTime); + expect(t1.createdAt.toDate().toISOString()).toBe('2020-01-01T00:00:00.000Z'); + }); + +});