Skip to content

Commit 5907da0

Browse files
committed
fix(core): ensure correct state of custom typed data after merging to existing results
Closes #6926
1 parent a109a32 commit 5907da0

File tree

7 files changed

+61
-24
lines changed

7 files changed

+61
-24
lines changed

packages/core/src/entity/EntityFactory.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,11 @@ export class EntityFactory {
205205
diff2[key] = entity[prop.name] ? helper(entity[prop.name]!).getPrimaryKey(options.convertCustomTypes) as EntityDataValue<T> : null;
206206
}
207207

208+
if ([ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE, ReferenceKind.SCALAR].includes(prop.kind) && prop.customType?.ensureComparable(meta, prop) && diff2[key] != null) {
209+
const converted = prop.customType.convertToJSValue(diff2[key], this.platform, { force: true });
210+
diff2[key] = prop.customType.convertToDatabaseValue(converted, this.platform, { fromQuery: true });
211+
}
212+
208213
originalEntityData[key] = diff2[key] === null ? nullVal : diff2[key];
209214
helper(entity).__loadedProperties.add(key as string);
210215
});

packages/core/src/platforms/Platform.ts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -393,13 +393,7 @@ export abstract class Platform {
393393
return JSON.stringify(value);
394394
}
395395

396-
convertJsonToJSValue(value: unknown, prop: EntityProperty): unknown {
397-
const isObjectEmbedded = prop.embedded && prop.object;
398-
399-
if ((this.convertsJsonAutomatically() || isObjectEmbedded) && ['json', 'jsonb', this.getJsonDeclarationSQL()].includes(prop.columnTypes[0])) {
400-
return value;
401-
}
402-
396+
convertJsonToJSValue(value: unknown, context?: TransformContext): unknown {
403397
return parseJsonSafe(value);
404398
}
405399

packages/core/src/types/JsonType.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,15 @@ export class JsonType extends Type<unknown, string | null> {
2020
return key + platform.castColumn(this.prop);
2121
}
2222

23-
override convertToJSValue(value: string | unknown, platform: Platform): unknown {
24-
return platform.convertJsonToJSValue(value, this.prop!);
23+
override convertToJSValue(value: string | unknown, platform: Platform, context?: TransformContext): unknown {
24+
const isJsonColumn = ['json', 'jsonb', platform.getJsonDeclarationSQL()].includes(this.prop!.columnTypes[0]);
25+
const isObjectEmbedded = this.prop!.embedded && this.prop!.object;
26+
27+
if ((platform.convertsJsonAutomatically() || isObjectEmbedded) && isJsonColumn && !context?.force) {
28+
return value;
29+
}
30+
31+
return platform.convertJsonToJSValue(value, context);
2532
}
2633

2734
override getColumnType(prop: EntityProperty, platform: Platform): string {

packages/core/src/types/Type.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { Constructor, EntityMetadata, EntityProperty } from '../typings';
44

55
export interface TransformContext {
66
fromQuery?: boolean;
7+
force?: boolean;
78
key?: string;
89
mode?: 'hydration' | 'query' | 'query-data' | 'discovery' | 'serialization';
910
}
@@ -32,7 +33,7 @@ export abstract class Type<JSType = string, DBType = JSType> {
3233
/**
3334
* Converts a value from its database representation to its JS representation of this type.
3435
*/
35-
convertToJSValue(value: DBType, platform: Platform): JSType {
36+
convertToJSValue(value: DBType, platform: Platform, context?: TransformContext): JSType {
3637
return value as unknown as JSType;
3738
}
3839

packages/core/src/unit-of-work/UnitOfWork.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,8 +130,8 @@ export class UnitOfWork {
130130
}
131131

132132
if (prop.hydrate === false && prop.customType?.ensureComparable(wrapped.__meta, prop)) {
133-
const converted = prop.customType.convertToJSValue(data[key], this.platform);
134-
data[key] = prop.customType.convertToDatabaseValue(converted, this.platform);
133+
const converted = prop.customType.convertToJSValue(data[key], this.platform, { key, mode: 'hydration', force: true });
134+
data[key] = prop.customType.convertToDatabaseValue(converted, this.platform, { key, mode: 'hydration' });
135135
}
136136

137137
if (forceUndefined) {

packages/mongodb/src/MongoPlatform.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,23 @@
11
import { ObjectId } from 'bson';
22
import {
3-
Platform, MongoNamingStrategy, Utils, ReferenceKind, MetadataError, type
4-
IPrimaryKey, type Primary, type NamingStrategy, type Constructor, type EntityRepository, type EntityProperty, type
5-
PopulateOptions, type EntityMetadata, type IDatabaseDriver, type EntityManager, type Configuration, type MikroORM,
3+
Platform,
4+
MongoNamingStrategy,
5+
Utils,
6+
ReferenceKind,
7+
MetadataError,
8+
type IPrimaryKey,
9+
type Primary,
10+
type NamingStrategy,
11+
type Constructor,
12+
type EntityRepository,
13+
type EntityProperty,
14+
type PopulateOptions,
15+
type EntityMetadata,
16+
type IDatabaseDriver,
17+
type EntityManager,
18+
type Configuration,
19+
type MikroORM,
20+
type TransformContext,
621
} from '@mikro-orm/core';
722
import { MongoExceptionConverter } from './MongoExceptionConverter';
823
import { MongoEntityRepository } from './MongoEntityRepository';
@@ -83,7 +98,7 @@ export class MongoPlatform extends Platform {
8398
return Utils.copy(value);
8499
}
85100

86-
override convertJsonToJSValue(value: unknown, prop: EntityProperty): unknown {
101+
override convertJsonToJSValue(value: unknown, context?: TransformContext): unknown {
87102
return value;
88103
}
89104

tests/issues/GH6723.test.ts

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,24 @@
1-
import { MikroORM, ArrayType, Entity, JsonType, PrimaryKey, Property } from '@mikro-orm/postgresql';
1+
import {
2+
ArrayType,
3+
Entity,
4+
JsonType,
5+
MikroORM,
6+
PrimaryKey,
7+
Property,
8+
} from '@mikro-orm/postgresql';
29
import { mockLogger } from '../helpers';
310

4-
type Child = {
5-
email: string;
6-
};
7-
811
@Entity()
9-
export class User {
12+
class User {
1013

1114
@PrimaryKey()
1215
id!: number;
1316

1417
@Property({ nullable: true, type: JsonType })
15-
children?: Child[];
18+
children?: any[];
19+
20+
@Property()
21+
email!: string;
1622

1723
@Property({ type: ArrayType, nullable: true, persist: true, hydrate: true })
1824
get childEmails(): string[] | undefined {
@@ -36,6 +42,7 @@ beforeAll(async () => {
3642
await orm.schema.refreshDatabase();
3743

3844
orm.em.create(User, {
45+
email: 'test@example.com',
3946
children: [{
4047
email: 'test@example.com',
4148
}],
@@ -48,7 +55,7 @@ afterAll(async () => {
4855
await orm.close(true);
4956
});
5057

51-
test('should not try to persist persisted getter if its value has not changed', async () => {
58+
test('should not try to persist persisted getter if its value has not changed 1', async () => {
5259
const r = await orm.em.findAll(User);
5360

5461
const mock = mockLogger(orm);
@@ -60,3 +67,11 @@ test('should not try to persist persisted getter if its value has not changed',
6067
// child_emails is hydrated, and its value didn't change
6168
expect(mock.mock.calls[1][0]).toMatch('update "user" set "children" = \'[{"email":"test@example.com"},{"email":"test2"}]\', "child_emails2" = \'{test@example.com,test2}\' where "id" = 1');
6269
});
70+
71+
test('should not try to persist persisted getter if its value has not changed 2', async () => {
72+
await orm.em.findOneOrFail(User, 1);
73+
await orm.em.findOneOrFail(User, { email: 'test@example.com' });
74+
const mock = mockLogger(orm);
75+
await orm.em.flush();
76+
expect(mock).not.toHaveBeenCalled();
77+
});

0 commit comments

Comments
 (0)