Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
B4nan committed Mar 13, 2022
1 parent f359929 commit 1d9fb5e
Show file tree
Hide file tree
Showing 7 changed files with 239 additions and 10 deletions.
1 change: 1 addition & 0 deletions packages/core/src/decorators/ManyToMany.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export interface ManyToManyOptions<T, O> extends ReferenceOptions<T, O> {
fixedOrder?: boolean;
fixedOrderColumn?: string;
pivotTable?: string;
pivotEntity?: string | (() => EntityName<any>);
joinColumn?: string;
joinColumns?: string[];
inverseJoinColumn?: string;
Expand Down
8 changes: 6 additions & 2 deletions packages/core/src/drivers/DatabaseDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ export abstract class DatabaseDriver<C extends Connection> implements IDatabaseD
}

if (prop.fixedOrder) {
return [{ [`${prop.pivotTable}.${prop.fixedOrderColumn}`]: QueryOrder.ASC } as QueryOrderMap<T>];
return [{ [`${prop.pivotEntity}.${prop.fixedOrderColumn}`]: QueryOrder.ASC } as QueryOrderMap<T>];
}

return [];
Expand All @@ -203,10 +203,14 @@ export abstract class DatabaseDriver<C extends Connection> implements IDatabaseD
}

protected getPivotInverseProperty(prop: EntityProperty): EntityProperty {
const pivotMeta = this.metadata.find(prop.pivotTable)!;
console.log(prop);
const pivotMeta = this.metadata.find(prop.pivotEntity)!;
const targetType = prop.targetMeta?.root.className;
let inverse: string;

console.log(targetType);
console.log(pivotMeta);

if (prop.owner) {
const pivotProp1 = pivotMeta.properties[targetType + '_inverse'];
inverse = pivotProp1.mappedBy;
Expand Down
17 changes: 15 additions & 2 deletions packages/core/src/metadata/MetadataDiscovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,12 @@ export class MetadataDiscovery {
const meta2 = this.metadata.get(prop.type);
Utils.defaultValue(prop, 'fixedOrder', !!prop.fixedOrderColumn);

if (prop.pivotEntity) {
const pivotMeta = this.metadata.get(prop.pivotEntity);
pivotMeta.tableName ??= this.namingStrategy.classToTableName(pivotMeta.className);
prop.pivotTable = pivotMeta.tableName;
}

if (!prop.pivotTable && prop.owner && this.platform.usesPivotTable()) {
prop.pivotTable = this.namingStrategy.joinTableName(meta.collection, meta2.collection, prop.name);
}
Expand Down Expand Up @@ -430,7 +436,7 @@ export class MetadataDiscovery {
}

private initFactoryField<T>(meta: EntityMetadata<T>, prop: EntityProperty<T>): void {
['mappedBy', 'inversedBy'].forEach(type => {
['mappedBy', 'inversedBy', 'pivotEntity'].forEach(type => {
const value = prop[type];

if (value instanceof Function) {
Expand All @@ -445,6 +451,10 @@ export class MetadataDiscovery {
}

private async definePivotTableEntity(meta: EntityMetadata, prop: EntityProperty): Promise<EntityMetadata> {
if (prop.pivotEntity) {
return this.metadata.get(prop.pivotEntity);
}

let tableName = prop.pivotTable;
let schemaName: string | undefined;

Expand All @@ -461,6 +471,9 @@ export class MetadataDiscovery {
schema: schemaName,
pivotTable: true,
});
prop.pivotEntity = data.className;
console.log(prop);
// TODO ensure pivotEntity is always set

if (prop.fixedOrder) {
const primaryProp = await this.defineFixedOrderProperty(prop, targetType);
Expand All @@ -486,7 +499,7 @@ export class MetadataDiscovery {
data.properties[meta.root.name + '_owner'] = await this.definePivotProperty(prop, meta.root.name + '_owner', meta.root.name!, targetType + '_inverse', true);
data.properties[targetType + '_inverse'] = await this.definePivotProperty(prop, targetType + '_inverse', targetType, meta.root.name + '_owner', false);

return this.metadata.set(prop.pivotTable, data);
return this.metadata.set(data.className, data);
}

private async defineFixedOrderProperty(prop: EntityProperty, targetType: string): Promise<EntityProperty> {
Expand Down
5 changes: 5 additions & 0 deletions packages/core/src/typings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,7 @@ export interface EntityProperty<T extends AnyEntity<T> = any> {
fixedOrder?: boolean;
fixedOrderColumn?: string;
pivotTable: string;
pivotEntity: string;
joinColumns: string[];
inverseJoinColumns: string[];
referencedColumnNames: string[];
Expand Down Expand Up @@ -302,6 +303,10 @@ export class EntityMetadata<T extends AnyEntity<T> = any> {
}

addProperty(prop: EntityProperty<T>, sync = true) {
if (prop.pivotTable && !prop.pivotEntity) {
prop.pivotEntity = prop.pivotTable;
}

this.properties[prop.name] = prop;
this.propertyOrder.set(prop.name, this.props.length);

Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/unit-of-work/CommitOrderCalculator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export class CommitOrderCalculator {

discoverProperty(prop: EntityProperty, entityName: string): void {
const toOneOwner = (prop.reference === ReferenceType.ONE_TO_ONE && prop.owner) || prop.reference === ReferenceType.MANY_TO_ONE;
const toManyOwner = prop.reference === ReferenceType.MANY_TO_MANY && prop.owner && !prop.pivotTable;
const toManyOwner = prop.reference === ReferenceType.MANY_TO_MANY && prop.owner && !prop.pivotEntity;

if (!toOneOwner && !toManyOwner) {
return;
Expand Down
10 changes: 5 additions & 5 deletions packages/knex/src/AbstractSqlDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -452,7 +452,7 @@ export abstract class AbstractSqlDriver<C extends AbstractSqlConnection = Abstra
async loadFromPivotTable<T, O>(prop: EntityProperty, owners: Primary<O>[][], where: FilterQuery<T> = {} as FilterQuery<T>, orderBy?: QueryOrderMap<T>[], ctx?: Transaction, options?: FindOptions<T>): Promise<Dictionary<T[]>> {
const pivotProp2 = this.getPivotInverseProperty(prop);
const ownerMeta = this.metadata.find(pivotProp2.type)!;
const cond = { [`${prop.pivotTable}.${pivotProp2.name}`]: { $in: ownerMeta.compositePK ? owners : owners.map(o => o[0]) } };
const cond = { [`${prop.pivotEntity}.${pivotProp2.name}`]: { $in: ownerMeta.compositePK ? owners : owners.map(o => o[0]) } };

/* istanbul ignore if */
if (!Utils.isEmpty(where) && Object.keys(where as Dictionary).every(k => Utils.isOperator(k, false))) {
Expand All @@ -465,7 +465,7 @@ export abstract class AbstractSqlDriver<C extends AbstractSqlConnection = Abstra
const qb = this.createQueryBuilder<T>(prop.type, ctx, options?.connectionType)
.unsetFlag(QueryFlag.CONVERT_CUSTOM_TYPES)
.withSchema(this.getSchemaName(prop.targetMeta, options));
const populate = this.autoJoinOneToOneOwner(prop.targetMeta!, [{ field: prop.pivotTable }]);
const populate = this.autoJoinOneToOneOwner(prop.targetMeta!, [{ field: prop.pivotEntity }]);
const fields = this.buildFields(prop.targetMeta!, (options?.populate ?? []) as unknown as PopulateOptions<T>[], [], qb, options?.fields as Field<T>[]);
qb.select(fields).populate(populate).where(where).orderBy(orderBy!).setLockMode(options?.lockMode, options?.lockTableAliases);

Expand Down Expand Up @@ -677,7 +677,7 @@ export abstract class AbstractSqlDriver<C extends AbstractSqlConnection = Abstra
}

if (deleteDiff === true || deleteDiff.length > 0) {
const qb1 = this.createQueryBuilder(prop.pivotTable, options?.ctx, 'write')
const qb1 = this.createQueryBuilder(prop.pivotEntity, options?.ctx, 'write')
.withSchema(this.getSchemaName(meta, options))
.unsetFlag(QueryFlag.CONVERT_CUSTOM_TYPES);
const knex = qb1.getKnex();
Expand All @@ -704,10 +704,10 @@ export abstract class AbstractSqlDriver<C extends AbstractSqlConnection = Abstra

/* istanbul ignore else */
if (this.platform.allowsMultiInsert()) {
await this.nativeInsertMany<T>(prop.pivotTable, items as EntityData<T>[], { ...options, convertCustomTypes: false, processCollections: false });
await this.nativeInsertMany<T>(prop.pivotEntity, items as EntityData<T>[], { ...options, convertCustomTypes: false, processCollections: false });
} else {
await Utils.runSerial(items, item => {
return this.createQueryBuilder(prop.pivotTable, options?.ctx, 'write')
return this.createQueryBuilder(prop.pivotEntity, options?.ctx, 'write')
.unsetFlag(QueryFlag.CONVERT_CUSTOM_TYPES)
.withSchema(this.getSchemaName(meta, options))
.insert(item)
Expand Down
206 changes: 206 additions & 0 deletions tests/features/composite-keys/custom-pivot-entity.sqlite.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
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 = false;

@Property()
shipped = false;

@Property()
created = 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()
amount = 1;

@Property()
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', () => {

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));

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

test(`should work`, async () => {
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;
console.log(order1);

await orm.em.fork().persistAndFlush([order1, order2, order3]);

const orders = await orm.em.find(Order, {}, { populate: true });
console.log(orders);

// // test inverse side
// const productRepository = orm.em.getRepository(Product);
// let products = await productRepository.findAll();
// expect(products).toBeInstanceOf(Array);
// expect(products.length).toBe(5);
// expect(products[0]).toBeInstanceOf(Product);
// expect(products[0].name).toBe('silly');
// expect(products[0].orders).toBeInstanceOf(Collection);
// expect(products[0].orders.isInitialized()).toBe(true);
// expect(products[0].orders.isDirty()).toBe(false);
// expect(products[0].orders.count()).toBe(2);
// expect(products[0].orders.length).toBe(2);
//
// orm.em.clear();
// products = await orm.em.find(Product, {});
// expect(products[0].orders.isInitialized()).toBe(false);
// expect(products[0].orders.isDirty()).toBe(false);
// expect(() => products[0].orders.getItems()).toThrowError(/Collection<Order> of entity Product\[\d+] not initialized/);
// expect(() => products[0].orders.remove(order1, order2)).toThrowError(/Collection<Order> of entity Product\[\d+] not initialized/);
// expect(() => products[0].orders.removeAll()).toThrowError(/Collection<Order> of entity Product\[\d+] not initialized/);
// expect(() => products[0].orders.contains(order1)).toThrowError(/Collection<Order> of entity Product\[\d+] not initialized/);
//
// // test M:N lazy load
// orm.em.clear();
// products = await productRepository.findAll();
// await products[0].orders.init();
// expect(products[0].orders.count()).toBe(2);
// expect(products[0].orders.getItems()[0]).toBeInstanceOf(Order);
// expect(products[0].orders.getItems()[0].uuid).toBeDefined();
// expect(wrap(products[0].orders.getItems()[0]).isInitialized()).toBe(true);
// expect(products[0].orders.isInitialized()).toBe(true);
// const old = products[0];
// expect(products[1].orders.isInitialized()).toBe(false);
// products = await productRepository.findAll({ populate: ['orders'] as const });
// expect(products[1].orders.isInitialized()).toBe(true);
// expect(products[0].id).toBe(old.id);
// expect(products[0]).toBe(old);
// expect(products[0].orders).toBe(old.orders);
//
// // 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.uuid, { 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
// order.products.add(new Product('fresh'));
// await orm.em.persistAndFlush(order);
// orm.em.clear();
// order = (await orm.em.findOne(Order, order.uuid, { 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(false);
// expect(order.products.contains(productRepository.getReference(product3.id))).toBe(true);
// expect(order.products.contains(productRepository.getReference(product4.id))).toBe(false);
// expect(order.products.contains(productRepository.getReference(product5.id))).toBe(false);
//
// // removeAll
// order.products.removeAll();
// await orm.em.persistAndFlush(order);
// orm.em.clear();
// order = (await orm.em.findOne(Order, order.uuid, { populate: ['products'] as const }))!;
// expect(order.products.count()).toBe(0);
});

});

0 comments on commit 1d9fb5e

Please sign in to comment.