Skip to content

Commit

Permalink
fix(core): map values from returning clause via hydrator
Browse files Browse the repository at this point in the history
Closes #725
  • Loading branch information
B4nan committed Aug 10, 2020
1 parent 4e0d8bd commit c5384b4
Show file tree
Hide file tree
Showing 3 changed files with 128 additions and 6 deletions.
15 changes: 10 additions & 5 deletions 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<EntityIdentifier>,
private readonly metadata: MetadataStorage) { }
private readonly metadata: MetadataStorage,
private readonly hydrator: Hydrator) { }

async persistToDatabase<T extends AnyEntity<T>>(changeSet: ChangeSet<T>, ctx?: Transaction): Promise<void> {
const meta = this.metadata.get(changeSet.name);
Expand Down Expand Up @@ -110,11 +112,14 @@ export class ChangeSetPersister {
*/
private mapReturnedValues<T extends AnyEntity<T>>(entity: T, res: QueryResult, meta: EntityMetadata<T>): void {
if (res.row && Object.keys(res.row).length > 0) {
Object.values<EntityProperty>(meta.properties).forEach(prop => {
const data = Object.values<EntityProperty>(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<T>(entity, meta, data as EntityData<T>, false);
}
}

Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/unit-of-work/UnitOfWork.ts
Expand Up @@ -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) { }
Expand Down
117 changes: 117 additions & 0 deletions 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> = T | null | undefined;

export type TimestampTypeOptions = {
hasTimeZone: boolean;
};

export class DateTimeType extends Type<Maybe<DateTime>, Maybe<Date>> {

convertToDatabaseValue(value: unknown): Maybe<Date> {
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<DateTime> {
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<Test>({
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<AbstractSqlDriver>;

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');
});

});

0 comments on commit c5384b4

Please sign in to comment.