From 391732ee9fd35d1f245f2420f99d58bc986a6375 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Ad=C3=A1mek?= Date: Sun, 12 Feb 2023 14:11:55 +0100 Subject: [PATCH] fix(core): convert custom types when snapshotting scalar composite keys Closes #3988 --- packages/core/src/utils/EntityComparator.ts | 28 ++--- packages/core/src/utils/Utils.ts | 38 ++++--- tests/issues/GH3988.test.ts | 114 ++++++++++++++++++++ 3 files changed, 151 insertions(+), 29 deletions(-) create mode 100644 tests/issues/GH3988.test.ts diff --git a/packages/core/src/utils/EntityComparator.ts b/packages/core/src/utils/EntityComparator.ts index e843b506c386..e2d7b61f0304 100644 --- a/packages/core/src/utils/EntityComparator.ts +++ b/packages/core/src/utils/EntityComparator.ts @@ -121,7 +121,13 @@ export class EntityComparator { if (meta.properties[pk].reference !== ReferenceType.SCALAR) { lines.push(` ${pk}: (entity${this.wrap(pk)} != null && (entity${this.wrap(pk)}.__entity || entity${this.wrap(pk)}.__reference)) ? entity${this.wrap(pk)}.__helper.getPrimaryKey(true) : entity${this.wrap(pk)},`); } else { - lines.push(` ${pk}: entity${this.wrap(pk)},`); + if (meta.properties[pk].customType) { + const convertorKey = this.safeKey(pk); + context.set(`convertToDatabaseValue_${convertorKey}`, (val: any) => meta.properties[pk].customType.convertToDatabaseValue(val, this.platform, { mode: 'serialization' })); + lines.push(` ${pk}: convertToDatabaseValue_${convertorKey}(entity${this.wrap(pk)}),`); + } else { + lines.push(` ${pk}: entity${this.wrap(pk)},`); + } } }); lines.push(` };`); @@ -451,22 +457,18 @@ export class EntityComparator { if (prop.reference === ReferenceType.ONE_TO_ONE || prop.reference === ReferenceType.MANY_TO_ONE) { if (prop.mapToPk) { - ret += ` ret${dataKey} = entity${entityKey};\n`; + if (prop.customType) { + context.set(`convertToDatabaseValue_${convertorKey}`, (val: any) => prop.customType.convertToDatabaseValue(val, this.platform, { mode: 'serialization' })); + ret += ` ret${dataKey} = convertToDatabaseValue_${convertorKey}(entity${entityKey});\n`; + } else { + ret += ` ret${dataKey} = entity${entityKey};\n`; + } } else { - context.set(`getPrimaryKeyValues_${convertorKey}`, (val: any) => val && Utils.getPrimaryKeyValues(val, this.metadata.find(prop.type)!.primaryKeys, true)); + const meta2 = this.metadata.find(prop.type); + context.set(`getPrimaryKeyValues_${convertorKey}`, (val: any) => val && Utils.getPrimaryKeyValues(val, meta2!.primaryKeys, true, true)); ret += ` ret${dataKey} = getPrimaryKeyValues_${convertorKey}(entity${entityKey});\n`; } - if (prop.customType) { - context.set(`convertToDatabaseValue_${convertorKey}`, (val: any) => prop.customType.convertToDatabaseValue(val, this.platform, { mode: 'serialization' })); - - if (['number', 'string', 'boolean'].includes(prop.customType.compareAsType().toLowerCase())) { - return ret + ` ret${dataKey} = convertToDatabaseValue_${convertorKey}(ret${dataKey});\n }\n`; - } - - return ret + ` ret${dataKey} = clone(convertToDatabaseValue_${convertorKey}(ret${dataKey}));\n }\n`; - } - return ret + ' }\n'; } diff --git a/packages/core/src/utils/Utils.ts b/packages/core/src/utils/Utils.ts index ae947e88f262..a2a4044b56d4 100644 --- a/packages/core/src/utils/Utils.ts +++ b/packages/core/src/utils/Utils.ts @@ -498,29 +498,35 @@ export class Utils { } static getPrimaryKeyValues(entity: T, primaryKeys: string[], allowScalar = false, convertCustomTypes = false) { - if (allowScalar && primaryKeys.length === 1) { - if (Utils.isEntity(entity[primaryKeys[0]], true)) { - return entity[primaryKeys[0]].__helper!.getPrimaryKey(convertCustomTypes); + if (entity == null) { + return entity; + } + + function toArray(val: unknown): unknown { + if (Utils.isPlainObject(val)) { + return Object.values(val).flatMap(v => toArray(v)); } - return entity[primaryKeys[0]]; + return val; } - return primaryKeys.reduce((ret, pk) => { - if (Utils.isEntity(entity[pk], true)) { - const childPk = entity[pk].__helper!.getPrimaryKey(convertCustomTypes); + const pk = Utils.isEntity(entity, true) + ? helper(entity).getPrimaryKey(convertCustomTypes) + : primaryKeys.reduce((o, pk) => { o[pk] = entity[pk]; return o; }, {} as Dictionary); - if (entity[pk].__meta.compositePK) { - ret.push(...Object.values(childPk) as Primary[]); - } else { - ret.push(childPk); - } - } else { - ret.push(entity[pk]); + if (primaryKeys.length > 1) { + return toArray(pk!); + } + + if (allowScalar) { + if (Utils.isPlainObject(pk)) { + return pk[primaryKeys[0]]; } - return ret; - }, [] as Primary[]); + return pk; + } + + return [pk]; } static getPrimaryKeyCond(entity: T, primaryKeys: string[]): Record> | null { diff --git a/tests/issues/GH3988.test.ts b/tests/issues/GH3988.test.ts new file mode 100644 index 000000000000..34a108ab274c --- /dev/null +++ b/tests/issues/GH3988.test.ts @@ -0,0 +1,114 @@ +import { Collection, Entity, ManyToOne, OneToMany, PrimaryKey, SimpleLogger, Type } from '@mikro-orm/core'; +import { MikroORM } from '@mikro-orm/sqlite'; +import { mockLogger } from '../helpers'; + +class Id { + + readonly value: number; + + constructor(value: number) { + this.value = value; + } + +} + +export class IdType extends Type { + + override convertToDatabaseValue(value: any) { + if (value instanceof Id) { + return value.value; + } + + return value; + } + + override convertToJSValue(value: any) { + if (typeof value === 'string') { + const id = Object.create(Id.prototype); + + return Object.assign(id, { + value, + }); + } + + return value; + } + + override compareAsType() { + return 'number'; + } + + override getColumnType() { + return 'integer'; + } + +} + +@Entity() +class ParentEntity { + + @PrimaryKey({ type: IdType, autoincrement: false }) + id!: Id; + + @PrimaryKey({ type: IdType, autoincrement: false }) + id2!: Id; + + @OneToMany({ + entity: () => ChildEntity, + mappedBy: 'parent', + }) + children = new Collection(this); + +} + +@Entity() +class ChildEntity { + + @PrimaryKey({ type: IdType, autoincrement: false }) + id!: Id; + + @ManyToOne(() => ParentEntity) + parent!: ParentEntity; + +} + +let orm: MikroORM; + +beforeAll(async () => { + orm = await MikroORM.init({ + entities: [ParentEntity, ChildEntity], + dbName: ':memory:', + loggerFactory: options => new SimpleLogger(options), + }); + + await orm.schema.createSchema(); +}); + +afterAll(async () => { + await orm.close(); +}); + +it('should create and persist entity along with child entity', async () => { + const parentRepository = orm.em.fork().getRepository(ParentEntity); + + // Create parent + const parent = new ParentEntity(); + parent.id = new Id(1); + parent.id2 = new Id(2); + + // Create child + const child = new ChildEntity(); + child.id = new Id(1); + + // Add child to parent + parent.children.add(child); + + const mock = mockLogger(orm); + await parentRepository.persistAndFlush(parent); + expect(mock.mock.calls).toEqual([ + ['[query] begin'], + ['[query] insert into `parent_entity` (`id`, `id2`) values (1, 2) returning `id`, `id2`'], + ['[query] insert into `child_entity` (`id`, `parent_id`, `parent_id2`) values (1, 1, 2) returning `id`'], + ['[query] commit'], + ]); +});