Skip to content

Commit

Permalink
fix(serialization): run custom serializer on getters (#4860)
Browse files Browse the repository at this point in the history
Closes #4859
  • Loading branch information
ivanpondal committed Oct 23, 2023
1 parent 6b444ed commit e76836e
Show file tree
Hide file tree
Showing 3 changed files with 130 additions and 6 deletions.
15 changes: 12 additions & 3 deletions packages/core/src/serialization/EntitySerializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,13 +96,13 @@ export class EntitySerializer {

// decorated getters
meta.props
.filter(prop => prop.getter && typeof entity[prop.name] !== 'undefined' && isVisible(meta, prop.name, options))
.forEach(prop => ret[this.propertyName(meta, prop.name, wrapped.__platform)] = entity[prop.name]);
.filter(prop => prop.getter && prop.getterName === undefined && typeof entity[prop.name] !== 'undefined' && isVisible(meta, prop.name, options))
.forEach(prop => ret[this.propertyName(meta, prop.name, wrapped.__platform)] = this.processProperty(prop.name, entity, options));

// decorated get methods
meta.props
.filter(prop => prop.getterName && entity[prop.getterName] as unknown instanceof Function && isVisible(meta, prop.name, options))
.forEach(prop => ret[this.propertyName(meta, prop.name, wrapped.__platform)] = (entity[prop.getterName!] as unknown as () => T[keyof T & string])());
.forEach(prop => ret[this.propertyName(meta, prop.name, wrapped.__platform)] = this.processProperty(prop.getterName as keyof T & string, entity, options));

return ret;
}
Expand All @@ -127,6 +127,15 @@ export class EntitySerializer {
const property = wrapped.__meta.properties[prop];
const serializer = property?.serializer;

// getter method
if (entity[prop] as unknown instanceof Function) {
const returnValue = (entity[prop] as unknown as () => T[keyof T & string])();
if (!options.ignoreSerializers && serializer) {
return serializer(returnValue);
}
return returnValue;
}

/* istanbul ignore next */
if (!options.ignoreSerializers && serializer) {
return serializer(entity[prop]);
Expand Down
15 changes: 12 additions & 3 deletions packages/core/src/serialization/EntityTransformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,13 +82,13 @@ export class EntityTransformer {

// decorated getters
meta.props
.filter(prop => prop.getter && !prop.hidden && typeof entity[prop.name] !== 'undefined')
.forEach(prop => ret[this.propertyName(meta, prop.name, wrapped.__platform)] = entity[prop.name]);
.filter(prop => prop.getter && prop.getterName === undefined && !prop.hidden && typeof entity[prop.name] !== 'undefined')
.forEach(prop => ret[this.propertyName(meta, prop.name, wrapped.__platform)] = this.processProperty(prop.name, entity, raw));

// decorated get methods
meta.props
.filter(prop => prop.getterName && !prop.hidden && entity[prop.getterName] as unknown instanceof Function)
.forEach(prop => ret[this.propertyName(meta, prop.name, wrapped.__platform)] = (entity[prop.getterName!] as unknown as () => T[keyof T & string])());
.forEach(prop => ret[this.propertyName(meta, prop.name, wrapped.__platform)] = this.processProperty(prop.getterName as keyof T & string, entity, raw));

if (contextCreated) {
root.close();
Expand All @@ -114,6 +114,15 @@ export class EntityTransformer {
const property = wrapped.__meta.properties[prop];
const serializer = property?.serializer;

// getter method
if (entity[prop] as unknown instanceof Function) {
const returnValue = (entity[prop] as unknown as () => T[keyof T & string])();
if (serializer) {
return serializer(returnValue);
}
return returnValue;
}

if (serializer) {
return serializer(entity[prop]);
}
Expand Down
106 changes: 106 additions & 0 deletions tests/features/serialization/GH4859.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { Entity, EntityDTO, PrimaryKey, Property, serialize, wrap } from '@mikro-orm/core';
import { MikroORM } from '@mikro-orm/sqlite';

@Entity()
class TimeSeriesAccessorGetterEntityTest {

@PrimaryKey()
id!: string;

@Property()
data!: number[];

@Property({
serializer: value => ({ AVG: value.average, MAX: value.max, TOTAL: value.totalItems }),
persist: false,
})
get stats() {
return {
average: this.data.reduce((sum, curr) => sum + curr, 0) / this.data.length,
max: this.data.reduce((max, curr) => max > curr ? max : curr, 0),
totalItems: this.data.length,
};
}

}

@Entity()
class TimeSeriesMethodGetterEntityTest {

@PrimaryKey()
id!: string;

@Property()
data!: number[];

@Property({
serializer: value => ({ AVG: value.average, MAX: value.max, TOTAL: value.totalItems }),
persist: false,
})
stats() {
return {
average: this.data.reduce((sum, curr) => sum + curr, 0) / this.data.length,
max: this.data.reduce((max, curr) => max > curr ? max : curr, 0),
totalItems: this.data.length,
};
}

}

let orm: MikroORM;

beforeAll(async () => {
orm = await MikroORM.init({
entities: [TimeSeriesAccessorGetterEntityTest, TimeSeriesMethodGetterEntityTest],
dbName: ':memory:',
});
await orm.schema.createSchema();
});

afterAll(() => orm.close(true));

const testScenarios: [string, (entity: any) => EntityDTO<any>][] = [
['serialize', (entity: any) => serialize(entity)],
['toObject', (entity: any) => wrap(entity).toObject()],
];

testScenarios.forEach(([testName, testFn]) => {
describe(testName, () => {

test('custom serializer should be called in accessor getter', async () => {
const timeSeries = new TimeSeriesAccessorGetterEntityTest();
timeSeries.id = 'weather';
timeSeries.data = [45, 56, 75, 34];

const dto = testFn(timeSeries);

expect(dto).toStrictEqual({
id: 'weather',
data: [45, 56, 75, 34],
stats: {
AVG: 52.5,
MAX: 75,
TOTAL: 4,
},
});
});

test('custom serializer should be called in method getter', async () => {
const timeSeries = new TimeSeriesMethodGetterEntityTest();
timeSeries.id = 'weather';
timeSeries.data = [45, 56, 75, 34];

const dto = testFn(timeSeries);

expect(dto).toStrictEqual({
id: 'weather',
data: [45, 56, 75, 34],
stats: {
AVG: 52.5,
MAX: 75,
TOTAL: 4,
},
});
});
});
});

0 comments on commit e76836e

Please sign in to comment.