Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] feat(repository): hasManyThrough #4438

Closed
wants to merge 10 commits into from
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// Copyright IBM Corp. 2020. All Rights Reserved.
// Node module: @loopback/repository-tests
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {expect, toJSON} from '@loopback/testlab';
import {
CrudFeatures,
CrudRepositoryCtor,
CrudTestContext,
DataSourceOptions,
} from '../../..';
import {
deleteAllModelsInDefaultDataSource,
MixedIdType,
withCrudCtx,
} from '../../../helpers.repository-tests';
import {
Customer,
CustomerRepository,
Order,
OrderRepository,
Seller,
SellerRepository,
} from '../fixtures/models';
import {givenBoundCrudRepositories} from '../helpers';

export function hasManyThroughRelationAcceptance(
dataSourceOptions: DataSourceOptions,
repositoryClass: CrudRepositoryCtor,
features: CrudFeatures,
) {
describe('HasManyThrough relation (acceptance)', () => {
before(deleteAllModelsInDefaultDataSource);
let customerRepo: CustomerRepository;
let orderRepo: OrderRepository;
let sellerRepo: SellerRepository;
let existingCustomerId: MixedIdType;

before(
withCrudCtx(async function setupRepository(ctx: CrudTestContext) {
({customerRepo, orderRepo, sellerRepo} = givenBoundCrudRepositories(
agnes512 marked this conversation as resolved.
Show resolved Hide resolved
ctx.dataSource,
repositoryClass,
features,
));
await ctx.dataSource.automigrate([
Customer.name,
Order.name,
Seller.name,
]);
}),
);

beforeEach(async () => {
await customerRepo.deleteAll();
await orderRepo.deleteAll();
await sellerRepo.deleteAll();
});

beforeEach(async () => {
existingCustomerId = (await givenPersistedCustomerInstance()).id;
});

it('can create related models', async () => {
// TODO(derdeka): creating seller though order is not the best example usecase - find alternative
const sellerData: Partial<Seller> = {
name: 'Domino’s Pizza',
};
const orderData: Partial<Order> = {
description: 'pizza',
};
const seller = await customerRepo
.sellers(existingCustomerId)
.create(sellerData, {
throughData: orderData,
});
expect(toJSON(seller)).containDeep(toJSON(sellerData));
Comment on lines +73 to +78
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Creating a seller though order is not the best example / test usecase for hasManyThrough.
Can anyone give a hint, which models / usecase we could use?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about Category-Product domain, where each category can have multiple products and each product can belong to multiple categories? The "through" model can be CategoryProduct (or CategoryProductLink?) and the operation to test is "create a new product in the given category".

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively, we can reverse the relation to "Seller has many Customer through Order". I think in such case it's more reasonable to create a new customer via the seller?


// check target object
const persistedSeller = await sellerRepo.findById(seller.id);
expect(toJSON(persistedSeller)).containDeep(toJSON(sellerData));

// check through object
const persistedOrders: Order[] = await orderRepo.find({
where: {
sellerId: seller.id,
customerId: existingCustomerId,
},
});
expect(persistedOrders.length).to.eql(1);
expect(persistedOrders[0]).containDeep(toJSON(orderData));
});

async function givenPersistedCustomerInstance() {
return customerRepo.create({name: 'a customer'});
}
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,21 @@

import {
belongsTo,
BelongsToAccessor,
Entity,
EntityCrudRepository,
hasMany,
HasManyRepositoryFactory,
HasManyThroughRepositoryFactory,
hasOne,
HasOneRepositoryFactory,
model,
property,
} from '@loopback/repository';
import {BelongsToAccessor} from '@loopback/repository/src';
import {MixedIdType} from '../../../../helpers.repository-tests';
import {Address, AddressWithRelations} from './address.model';
import {Order, OrderWithRelations} from './order.model';
import {Seller, SellerWithRelations} from './seller.model';

@model()
export class Customer extends Entity {
Expand All @@ -36,6 +38,13 @@ export class Customer extends Entity {
@hasMany(() => Order)
orders: Order[];

@hasMany(() => Seller, {
through: {
model: () => Order,
},
})
sellers: Seller[];

@hasOne(() => Address)
address: Address;

Expand All @@ -49,6 +58,7 @@ export class Customer extends Entity {
export interface CustomerRelations {
address?: AddressWithRelations;
orders?: OrderWithRelations[];
sellers?: SellerWithRelations[];
customers?: CustomerWithRelations[];
parentCustomer?: CustomerWithRelations;
}
Expand All @@ -60,6 +70,12 @@ export interface CustomerRepository
// define additional members like relation methods here
address: HasOneRepositoryFactory<Address, MixedIdType>;
orders: HasManyRepositoryFactory<Order, MixedIdType>;
sellers: HasManyThroughRepositoryFactory<
Seller,
typeof Seller.prototype.id,
Order,
typeof Customer.prototype.id
>;
customers: HasManyRepositoryFactory<Customer, MixedIdType>;
parent: BelongsToAccessor<Customer, MixedIdType>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@
export * from './address.model';
export * from './customer.model';
export * from './order.model';
export * from './seller.model';
export * from './shipment.model';
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
} from '@loopback/repository';
import {MixedIdType} from '../../../../helpers.repository-tests';
import {Customer, CustomerWithRelations} from './customer.model';
import {Seller} from './seller.model';
import {Shipment, ShipmentWithRelations} from './shipment.model';

@model()
Expand All @@ -39,6 +40,9 @@ export class Order extends Entity {
@belongsTo(() => Customer)
customerId: MixedIdType;

@belongsTo(() => Seller)
sellerId: MixedIdType;

@belongsTo(() => Shipment, {keyTo: 'shipment_id', name: 'shipment'})
shipmentInfo: number;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Copyright IBM Corp. 2020. All Rights Reserved.
// Node module: @loopback/repository-tests
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {
Entity,
EntityCrudRepository,
hasMany,
HasManyThroughRepositoryFactory,
model,
property,
} from '@loopback/repository';
import {Customer, CustomerWithRelations} from './customer.model';
import {Order} from './order.model';

@model()
export class Seller extends Entity {
@property({
type: 'number',
id: true,
})
id: number;

@property({
type: 'string',
required: true,
})
name: string;

@hasMany(() => Customer, {
through: {
model: () => Order,
},
})
customers?: Customer[];
}

export interface SellerRelations {
customers?: CustomerWithRelations;
}

export type SellerWithRelations = Seller & SellerRelations;

export interface SellerRepository
extends EntityCrudRepository<Seller, typeof Seller.prototype.id> {
// define additional members like relation methods here
customers: HasManyThroughRepositoryFactory<
Customer,
typeof Customer.prototype.id,
Order,
typeof Seller.prototype.id
>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,21 @@
import {Getter} from '@loopback/context';
import {
BelongsToAccessor,
HasManyRepositoryFactory,
HasOneRepositoryFactory,
juggler,
BelongsToDefinition,
createBelongsToAccessor,
createHasManyRepositoryFactory,
createHasManyThroughRepositoryFactory,
createHasOneRepositoryFactory,
HasManyDefinition,
HasManyRepositoryFactory,
HasManyThroughDefinition,
HasManyThroughRepositoryFactory,
HasOneDefinition,
createBelongsToAccessor,
BelongsToDefinition,
createHasOneRepositoryFactory,
HasOneRepositoryFactory,
juggler,
} from '@loopback/repository';
import {Address, Customer, CustomerRelations, Order} from '../models';
import {CrudRepositoryCtor} from '../../../../types.repository-tests';
import {Address, Customer, CustomerRelations, Order, Seller} from '../models';

// create the CustomerRepo by calling this func so that it can be extended from CrudRepositoryCtor
export function createCustomerRepo(repoClass: CrudRepositoryCtor) {
Expand All @@ -30,6 +33,12 @@ export function createCustomerRepo(repoClass: CrudRepositoryCtor) {
Order,
typeof Customer.prototype.id
>;
public readonly sellers: HasManyThroughRepositoryFactory<
Seller,
typeof Seller.prototype.id,
Order,
typeof Customer.prototype.id
>;
public readonly address: HasOneRepositoryFactory<
Address,
typeof Customer.prototype.id
Expand All @@ -47,6 +56,7 @@ export function createCustomerRepo(repoClass: CrudRepositoryCtor) {
db: juggler.DataSource,
orderRepositoryGetter: Getter<typeof repoClass.prototype>,
addressRepositoryGetter: Getter<typeof repoClass.prototype>,
sellersRepositoryGetter: Getter<typeof repoClass.prototype>,
) {
super(Customer, db);
const ordersMeta = this.entityClass.definition.relations['orders'];
Expand All @@ -56,6 +66,13 @@ export function createCustomerRepo(repoClass: CrudRepositoryCtor) {
orderRepositoryGetter,
);

const sellersMeta = this.entityClass.definition.relations['sellers'];
this.sellers = createHasManyThroughRepositoryFactory(
sellersMeta as HasManyThroughDefinition,
sellersRepositoryGetter,
orderRepositoryGetter,
);

const addressMeta = this.entityClass.definition.relations['address'];
this.address = createHasOneRepositoryFactory(
addressMeta as HasOneDefinition,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@
export * from './address.repository';
export * from './customer.repository';
export * from './order.repository';
export * from './seller.repository';
export * from './shipment.repository';
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright IBM Corp. 2020. All Rights Reserved.
// Node module: @loopback/repository-tests
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {Getter} from '@loopback/context';
import {
createHasManyThroughRepositoryFactory,
HasManyThroughDefinition,
HasManyThroughRepositoryFactory,
juggler,
} from '@loopback/repository';
import {CrudRepositoryCtor} from '../../../../types.repository-tests';
import {Customer, Order, Seller, SellerRelations} from '../models';

// create the SellerRepo by calling this func so that it can be extended from CrudRepositoryCtor
export function createSellerRepo(repoClass: CrudRepositoryCtor) {
return class SellerRepository extends repoClass<
Seller,
typeof Seller.prototype.id,
SellerRelations
> {
public readonly customers: HasManyThroughRepositoryFactory<
Customer,
typeof Customer.prototype.id,
Order,
typeof Seller.prototype.id
>;

constructor(
db: juggler.DataSource,
customerRepositoryGetter: Getter<typeof repoClass.prototype>,
orderRepositoryGetter: Getter<typeof repoClass.prototype>,
) {
super(Seller, db);
const customersMeta = this.entityClass.definition.relations['customers'];
this.customers = createHasManyThroughRepositoryFactory(
customersMeta as HasManyThroughDefinition,
customerRepositoryGetter,
orderRepositoryGetter,
);
}
};
}
Loading