Skip to content

Commit

Permalink
fix(core): fix conditions in em.upsertMany with composite keys
Browse files Browse the repository at this point in the history
Closes #4786
  • Loading branch information
B4nan committed Oct 5, 2023
1 parent cbc0c50 commit 2f58556
Show file tree
Hide file tree
Showing 2 changed files with 145 additions and 3 deletions.
16 changes: 13 additions & 3 deletions packages/core/src/EntityManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -840,6 +840,12 @@ export class EntityManager<D extends IDatabaseDriver = IDatabaseDriver> {
}

row = QueryHelper.processObjectParams(row) as EntityData<Entity>;
where = QueryHelper.processWhere({
where,
entityName,
metadata: this.metadata,
platform: this.getPlatform(),
});
em.validator.validateParams(row, 'insert data');
allData.push(row);
allWhere.push(where);
Expand Down Expand Up @@ -900,20 +906,24 @@ export class EntityManager<D extends IDatabaseDriver = IDatabaseDriver> {

const where = { $or: [] as Dictionary[] };
data.forEach((item, idx) => {
uniqueFields.forEach(prop => where.$or[idx] = { [prop]: item[prop as string] });
where.$or[idx] = {};
uniqueFields.forEach(prop => {
where.$or[idx][prop as string] = item[prop as string];
});
});

const data2 = await this.driver.find(meta.className, where, {
fields: returning.concat(...add).concat(...uniqueFields as string[]),
ctx: em.transactionContext,
convertCustomTypes: true,
});

for (const [entity, cond] of loadPK.entries()) {
const row = data2.find(pk => {
const row = data2.find(row => {
const tmp = {};
add.forEach(k => {
if (!meta.properties[k]?.primary) {
tmp[k] = pk[k];
tmp[k] = row[k];
}
});
return this.comparator.matching(entityName, cond, tmp);
Expand Down
132 changes: 132 additions & 0 deletions tests/features/upsert/GH4786.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import {
Collection,
Entity,
ManyToOne,
OneToMany,
OptionalProps,
PrimaryKey,
Property,
SimpleLogger,
Unique,
} from '@mikro-orm/core';
import { MikroORM } from '@mikro-orm/sqlite';
import { mockLogger } from '../../helpers';

@Entity({ abstract: true })
abstract class ApplicationEntity<Optionals = never> {

[OptionalProps]?: Optionals | 'createdAt' | 'updatedAt';

@PrimaryKey()
id!: number;

@Property({ name: 'created_at', defaultRaw: 'current_timestamp' })
createdAt!: Date;

@Property({ name: 'updated_at', defaultRaw: 'current_timestamp', onUpdate: () => new Date() })
updatedAt!: Date;

}

@Entity()
class InternalRole extends ApplicationEntity {

@Property()
name!: string;

@OneToMany(() => InternalRolePermission, permission => permission.internalRole, { orphanRemoval: true })
permissions = new Collection<InternalRolePermission>(this);

}

@Entity()
@Unique({ properties: ['subject', 'action', 'internalRole'] })
class InternalRolePermission extends ApplicationEntity {

@Property()
subject!: string;

@Property()
action!: string;

@ManyToOne(() => InternalRole, { onDelete: 'cascade' })
internalRole!: InternalRole;

}

let orm: MikroORM;

beforeAll(async () => {
orm = await MikroORM.init({
dbName: ':memory:',
entities: [InternalRole],
loggerFactory: options => new SimpleLogger(options),
});
await orm.schema.createSchema();
});

beforeAll(async () => {
await orm.schema.clearDatabase();
});

afterAll(async () => {
await orm.close(true);
});

test('4786 (em.upsert)', async () => {
orm.em.create(InternalRole, { id: 1, name: 'role' });
await orm.em.flush();
orm.em.clear();

const mock = mockLogger(orm);
let role = await orm.em.findOneOrFail(InternalRole, 1);
await orm.em.upsert(InternalRolePermission, { subject: 'User', action: 'read', internalRole: role });

expect(mock.mock.calls).toEqual([
['[query] select `i0`.* from `internal_role` as `i0` where `i0`.`id` = 1 limit 1'],
['[query] insert into `internal_role_permission` (`action`, `internal_role_id`, `subject`) values (\'read\', 1, \'User\') on conflict (`subject`, `action`, `internal_role_id`) do nothing returning `id`, `created_at`, `updated_at`'],
]);

mock.mockReset();
orm.em.clear();
role = await orm.em.findOneOrFail(InternalRole, 1);
await orm.em.upsert(InternalRolePermission, { subject: 'User', action: 'read', internalRole: role });

expect(mock.mock.calls).toEqual([
['[query] select `i0`.* from `internal_role` as `i0` where `i0`.`id` = 1 limit 1'],
['[query] insert into `internal_role_permission` (`action`, `internal_role_id`, `subject`) values (\'read\', 1, \'User\') on conflict (`subject`, `action`, `internal_role_id`) do nothing returning `id`, `created_at`, `updated_at`'],
['[query] select `i0`.`id`, `i0`.`created_at`, `i0`.`updated_at` from `internal_role_permission` as `i0` where `i0`.`subject` = \'User\' and `i0`.`action` = \'read\' and `i0`.`internal_role_id` = 1 limit 1'],
]);
});

test('4786 (em.upsertMany)', async () => {
orm.em.create(InternalRole, { id: 1, name: 'role' });
await orm.em.flush();
orm.em.clear();

const mock = mockLogger(orm);
let role = await orm.em.findOneOrFail(InternalRole, 1);
await orm.em.upsertMany(InternalRolePermission, [
{ subject: 'User', action: 'read', internalRole: role },
{ subject: 'User', action: 'update', internalRole: role },
]);

expect(mock.mock.calls).toEqual([
['[query] select `i0`.* from `internal_role` as `i0` where `i0`.`id` = 1 limit 1'],
['[query] insert into `internal_role_permission` (`action`, `internal_role_id`, `subject`) select \'read\' as `action`, 1 as `internal_role_id`, \'User\' as `subject` union all select \'update\' as `action`, 1 as `internal_role_id`, \'User\' as `subject` where true on conflict (`subject`, `action`, `internal_role_id`) do nothing returning `id`, `created_at`, `updated_at`'],
]);

mock.mockReset();
orm.em.clear();
role = await orm.em.findOneOrFail(InternalRole, 1);
await orm.em.upsertMany(InternalRolePermission, [
{ subject: 'User', action: 'read', internalRole: role },
{ subject: 'User', action: 'update', internalRole: role },
]);

expect(mock.mock.calls).toEqual([
['[query] select `i0`.* from `internal_role` as `i0` where `i0`.`id` = 1 limit 1'],
['[query] insert into `internal_role_permission` (`action`, `internal_role_id`, `subject`) select \'read\' as `action`, 1 as `internal_role_id`, \'User\' as `subject` union all select \'update\' as `action`, 1 as `internal_role_id`, \'User\' as `subject` where true on conflict (`subject`, `action`, `internal_role_id`) do nothing returning `id`, `created_at`, `updated_at`'],
['[query] select `i0`.`id`, `i0`.`created_at`, `i0`.`updated_at`, `i0`.`subject`, `i0`.`action`, `i0`.`internal_role_id` from `internal_role_permission` as `i0` where ((`i0`.`subject` = \'User\' and `i0`.`action` = \'read\' and `i0`.`internal_role_id` = 1) or (`i0`.`subject` = \'User\' and `i0`.`action` = \'update\' and `i0`.`internal_role_id` = 1))'],
]);
});

0 comments on commit 2f58556

Please sign in to comment.