Skip to content

Commit

Permalink
fix(core): fix custom pivot table entities for unidirectional relations
Browse files Browse the repository at this point in the history
  • Loading branch information
B4nan committed Mar 19, 2022
1 parent 1860ff5 commit 01bdbf6
Show file tree
Hide file tree
Showing 6 changed files with 226 additions and 13 deletions.
10 changes: 4 additions & 6 deletions packages/core/src/entity/Collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { AnyEntity, EntityData, EntityDTO, EntityMetadata, FilterQuery, Loa
import { ArrayCollection } from './ArrayCollection';
import { Utils } from '../utils/Utils';
import { ValidationError } from '../errors';
import type { QueryOrderMap , LockMode } from '../enums';
import type { LockMode, QueryOrderMap } from '../enums';
import { QueryOrder, ReferenceType } from '../enums';
import { Reference } from './Reference';
import type { Transaction } from '../connections/Connection';
Expand Down Expand Up @@ -62,12 +62,12 @@ export class Collection<T, O = unknown> extends ArrayCollection<T, O> {
}

const em = this.getEntityManager();
const pivotMeta = em.getMetadata().find(this.property.pivotTable)!;
const pivotMeta = em.getMetadata().find(this.property.pivotEntity)!;

if (!em.getPlatform().usesPivotTable() && this.property.reference === ReferenceType.MANY_TO_MANY) {
this._count = this.length;
} else if (this.property.pivotTable && !(this.property.inversedBy || this.property.mappedBy)) {
this._count = await em.count(this.property.type, this.createLoadCountCondition({} as FilterQuery<T>, pivotMeta), { populate: [{ field: this.property.pivotTable }] });
this._count = await em.count(this.property.type, this.createLoadCountCondition({} as FilterQuery<T>, pivotMeta), { populate: [{ field: this.property.pivotEntity }] });
} else {
this._count = await em.count(this.property.type, this.createLoadCountCondition({} as FilterQuery<T>, pivotMeta));
}
Expand Down Expand Up @@ -317,9 +317,7 @@ export class Collection<T, O = unknown> extends ArrayCollection<T, O> {
if (this.property.reference === ReferenceType.ONE_TO_MANY) {
cond[this.property.mappedBy] = val;
} else if (pivotMeta && this.property.owner && !this.property.inversedBy) {
const pivotProp1 = pivotMeta.properties[this.property.type + '_inverse'];
const inverse = pivotProp1.mappedBy;
const key = `${this.property.pivotTable}.${pivotMeta.properties[inverse].name}`;
const key = `${this.property.pivotEntity}.${pivotMeta.relations[0].name}`;
cond[key] = val;
} else {
const key = this.property.owner ? this.property.inversedBy : this.property.mappedBy;
Expand Down
8 changes: 4 additions & 4 deletions packages/knex/src/query/QueryBuilderHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ export class QueryBuilderHelper {
}

joinManyToManyReference(prop: EntityProperty, ownerAlias: string, alias: string, pivotAlias: string, type: 'leftJoin' | 'innerJoin' | 'pivotJoin', cond: Dictionary, path: string): Dictionary<JoinOptions> {
const meta = this.metadata.find(prop.pivotEntity)!;
const pivotMeta = this.metadata.find(prop.pivotEntity)!;
const ret = {
[`${ownerAlias}.${prop.name}#${pivotAlias}`]: {
prop, type, cond, ownerAlias,
Expand All @@ -175,8 +175,8 @@ export class QueryBuilderHelper {
joinColumns: prop.joinColumns,
inverseJoinColumns: prop.inverseJoinColumns,
primaryKeys: prop.referencedColumnNames,
table: meta.tableName,
schema: this.driver.getSchemaName(meta),
table: pivotMeta.tableName,
schema: this.driver.getSchemaName(pivotMeta),
path: path.endsWith('[pivot]') ? path : `${path}[pivot]`,
} as JoinOptions,
};
Expand All @@ -185,7 +185,7 @@ export class QueryBuilderHelper {
return ret;
}

const prop2 = meta.properties[prop.type + (prop.owner ? '_inverse' : '_owner')];
const prop2 = prop.owner ? pivotMeta.relations[1] : pivotMeta.relations[0];
ret[`${pivotAlias}.${prop2.name}#${alias}`] = this.joinManyToOneReference(prop2, pivotAlias, alias, type);
ret[`${pivotAlias}.${prop2.name}#${alias}`].path = path;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`custom pivot entity for m:n with additional properties (unidirectional) schema 1`] = `
"pragma foreign_keys = off;
create table \`order\` (\`id\` integer not null primary key autoincrement, \`paid\` integer not null, \`shipped\` integer not null, \`created\` datetime not null);
create table \`product\` (\`id\` integer not null primary key autoincrement, \`name\` text not null, \`current_price\` integer not null);
create table \`order_item\` (\`order_id\` integer not null, \`product_id\` integer not null, \`amount\` integer not null default 1, \`offered_price\` integer not null default 0, constraint \`order_item_order_id_foreign\` foreign key(\`order_id\`) references \`order\`(\`id\`) on update cascade, constraint \`order_item_product_id_foreign\` foreign key(\`product_id\`) references \`product\`(\`id\`) on update cascade, primary key (\`order_id\`, \`product_id\`));
create index \`order_item_order_id_index\` on \`order_item\` (\`order_id\`);
create index \`order_item_product_id_index\` on \`order_item\` (\`product_id\`);
pragma foreign_keys = on;
"
`;
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`custom pivot entity for m:n with additional properties schema 1`] = `
exports[`custom pivot entity for m:n with additional properties (bidirectional) schema 1`] = `
"pragma foreign_keys = off;
create table \`order\` (\`id\` integer not null primary key autoincrement, \`paid\` integer not null, \`shipped\` integer not null, \`created\` datetime not null);
Expand Down
184 changes: 184 additions & 0 deletions tests/features/composite-keys/custom-pivot-entity-uni.sqlite.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import { Entity, PrimaryKey, MikroORM, ManyToOne, PrimaryKeyType, Property, wrap, OneToMany, Collection, ManyToMany } from '@mikro-orm/core';

@Entity()
export class Order {

@PrimaryKey()
id!: number;

// eslint-disable-next-line @typescript-eslint/no-use-before-define
@OneToMany(() => OrderItem, item => item.order)
items = new Collection<OrderItem>(this);

// eslint-disable-next-line @typescript-eslint/no-use-before-define
@ManyToMany({ entity: () => Product, pivotEntity: () => OrderItem })
products = new Collection<Product>(this);

@Property()
paid: boolean = false;

@Property()
shipped: boolean = false;

@Property()
created: Date = new Date();

}

@Entity()
export class Product {

@PrimaryKey()
id!: number;

@Property()
name: string;

@Property()
currentPrice: number;

constructor(name: string, currentPrice: number) {
this.name = name;
this.currentPrice = currentPrice;
}

}

@Entity()
export class OrderItem {

@ManyToOne({ primary: true })
order: Order;

@ManyToOne({ primary: true })
product: Product;

@Property({ default: 1 })
amount!: number;

@Property({ default: 0 })
offeredPrice: number;

[PrimaryKeyType]: [number, number];

constructor(order: Order, product: Product) {
this.order = order;
this.product = product;
this.offeredPrice = product.currentPrice;
}

}

describe('custom pivot entity for m:n with additional properties (unidirectional)', () => {

let orm: MikroORM;

beforeAll(async () => {
orm = await MikroORM.init({
entities: [Product, OrderItem, Order],
dbName: ':memory:',
type: 'sqlite',
});
await orm.getSchemaGenerator().createSchema();
});

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

beforeEach(() => orm.getSchemaGenerator().clearDatabase());

test(`schema`, async () => {
const sql = await orm.getSchemaGenerator().getCreateSchemaSQL();
expect(sql).toMatchSnapshot();
});

async function createEntities() {
const order1 = new Order();
const order2 = new Order();
const order3 = new Order();
const product1 = new Product('p1', 111);
const product2 = new Product('p2', 222);
const product3 = new Product('p3', 333);
const product4 = new Product('p4', 444);
const product5 = new Product('p5', 555);
const item11 = new OrderItem(order1, product1);
item11.offeredPrice = 123;
const item12 = new OrderItem(order1, product2);
item12.offeredPrice = 3123;
const item21 = new OrderItem(order2, product1);
item21.offeredPrice = 4123;
const item22 = new OrderItem(order2, product2);
item22.offeredPrice = 1123;
const item23 = new OrderItem(order2, product5);
item23.offeredPrice = 1263;
const item31 = new OrderItem(order3, product3);
item31.offeredPrice = 7123;
const item32 = new OrderItem(order3, product4);
item32.offeredPrice = 9123;
const item33 = new OrderItem(order3, product5);
item33.offeredPrice = 5123;

await orm.em.fork().persistAndFlush([order1, order2, order3]);
return { order1, order2, product1, product2, product3, product4, product5 };
}

test(`should work`, async () => {
const { product1, product2, product3, product4, product5 } = await createEntities();
const productRepository = orm.em.getRepository(Product);

const orders = await orm.em.find(Order, {}, { populate: true });
expect(orders).toHaveLength(3);

// test M:N lazy load
orm.em.clear();
let order = (await orm.em.findOne(Order, { products: product1.id }))!;
expect(order.products.isInitialized()).toBe(false);
await order.products.init();
expect(order.products.isInitialized()).toBe(true);
expect(order.products.count()).toBe(2);
expect(order.products.getItems()[0]).toBeInstanceOf(Product);
expect(order.products.getItems()[0].id).toBeDefined();
expect(wrap(order.products.getItems()[0]).isInitialized()).toBe(true);

// test collection CRUD
// remove
expect(order.products.count()).toBe(2);
order.products.remove(t => t.id === product1.id); // we need to get reference as product1 is detached from current EM
await orm.em.persistAndFlush(order);
orm.em.clear();
order = (await orm.em.findOne(Order, order.id, { populate: ['products'] as const }))!;
expect(order.products.count()).toBe(1);

// add
order.products.add(productRepository.getReference(product1.id)); // we need to get reference as product1 is detached from current EM
const product6 = new Product('fresh', 555);
order.products.add(product6);
await orm.em.persistAndFlush(order);
orm.em.clear();
order = (await orm.em.findOne(Order, order.id, { populate: ['products'] as const }))!;
expect(order.products.count()).toBe(3);

// contains
expect(order.products.contains(productRepository.getReference(product1.id))).toBe(true);
expect(order.products.contains(productRepository.getReference(product2.id))).toBe(true);
expect(order.products.contains(productRepository.getReference(product3.id))).toBe(false);
expect(order.products.contains(productRepository.getReference(product4.id))).toBe(false);
expect(order.products.contains(productRepository.getReference(product5.id))).toBe(false);
expect(order.products.contains(productRepository.getReference(product6.id))).toBe(true);

// removeAll
order.products.removeAll();
await orm.em.persistAndFlush(order);
orm.em.clear();
order = (await orm.em.findOne(Order, order.id, { populate: ['products'] as const }))!;
expect(order.products.count()).toBe(0);
});

test(`search by m:n property and loadCount() works`, async () => {
await createEntities();
const res = await orm.em.find(Order, { products: { name: 'p1' } });
expect(res).toHaveLength(2);
const count = await res[0].products.loadCount();
expect(count).toBe(2);
});

});
19 changes: 17 additions & 2 deletions tests/features/composite-keys/custom-pivot-entity.sqlite.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export class OrderItem {

}

describe('custom pivot entity for m:n with additional properties', () => {
describe('custom pivot entity for m:n with additional properties (bidirectional)', () => {

let orm: MikroORM;

Expand All @@ -87,12 +87,14 @@ describe('custom pivot entity for m:n with additional properties', () => {

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

beforeEach(() => orm.getSchemaGenerator().clearDatabase());

test(`schema`, async () => {
const sql = await orm.getSchemaGenerator().getCreateSchemaSQL();
expect(sql).toMatchSnapshot();
});

test(`should work`, async () => {
async function createEntities() {
const order1 = new Order();
const order2 = new Order();
const order3 = new Order();
Expand All @@ -119,6 +121,11 @@ describe('custom pivot entity for m:n with additional properties', () => {
item33.offeredPrice = 5123;

await orm.em.fork().persistAndFlush([order1, order2, order3]);
return { order1, order2, product1, product2, product3, product4, product5 };
}

test(`should work`, async () => {
const { order1, order2, product1, product2, product3, product4, product5 } = await createEntities();

const orders = await orm.em.find(Order, {}, { populate: true });
expect(orders).toHaveLength(3);
Expand Down Expand Up @@ -207,4 +214,12 @@ describe('custom pivot entity for m:n with additional properties', () => {
expect(order.products.count()).toBe(0);
});

test(`search by m:n property and loadCount() works`, async () => {
await createEntities();
const res = await orm.em.find(Order, { products: { name: 'p1' } });
expect(res).toHaveLength(2);
const count = await res[0].products.loadCount();
expect(count).toBe(2);
});

});

0 comments on commit 01bdbf6

Please sign in to comment.