Skip to content

Commit

Permalink
fix(core): convert custom types when snapshotting scalar composite keys
Browse files Browse the repository at this point in the history
Closes #3988
  • Loading branch information
B4nan committed Feb 12, 2023
1 parent b32df88 commit 391732e
Show file tree
Hide file tree
Showing 3 changed files with 151 additions and 29 deletions.
28 changes: 15 additions & 13 deletions packages/core/src/utils/EntityComparator.ts
Expand Up @@ -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(` };`);
Expand Down Expand Up @@ -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';
}

Expand Down
38 changes: 22 additions & 16 deletions packages/core/src/utils/Utils.ts
Expand Up @@ -498,29 +498,35 @@ export class Utils {
}

static getPrimaryKeyValues<T>(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<T>[]);
} 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<T>[]);
return pk;
}

return [pk];
}

static getPrimaryKeyCond<T>(entity: T, primaryKeys: string[]): Record<string, Primary<T>> | null {
Expand Down
114 changes: 114 additions & 0 deletions 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<Id, string> {

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<ChildEntity>(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'],
]);
});

0 comments on commit 391732e

Please sign in to comment.