From c04ea948f83124e6fa35935f6be91dbd6285ce99 Mon Sep 17 00:00:00 2001 From: Agnes Lin Date: Wed, 17 Jun 2020 17:35:16 -0400 Subject: [PATCH] test(repository-tests): adding acceptance tests for hasManyThrough relation --- .../has-many-through.relation.acceptance.ts | 248 ++++++++++++++++++ .../fixtures/models/cart-item.model.ts | 34 +++ .../models/customer-cart-item-link.model.ts | 44 ++++ .../fixtures/models/customer.model.ts | 13 + .../crud/relations/fixtures/models/index.ts | 2 + .../repositories/cart-item.repository.ts | 21 ++ .../customer-cart-item-link.repository.ts | 20 ++ .../repositories/customer.repository.ts | 37 ++- .../relations/fixtures/repositories/index.ts | 2 + .../fixtures/repositories/order.repository.ts | 8 +- .../src/crud/relations/helpers.ts | 30 +++ .../has-many/has-many-through.helpers.ts | 2 +- 12 files changed, 449 insertions(+), 12 deletions(-) create mode 100644 packages/repository-tests/src/crud/relations/acceptance/has-many-through.relation.acceptance.ts create mode 100644 packages/repository-tests/src/crud/relations/fixtures/models/cart-item.model.ts create mode 100644 packages/repository-tests/src/crud/relations/fixtures/models/customer-cart-item-link.model.ts create mode 100644 packages/repository-tests/src/crud/relations/fixtures/repositories/cart-item.repository.ts create mode 100644 packages/repository-tests/src/crud/relations/fixtures/repositories/customer-cart-item-link.repository.ts diff --git a/packages/repository-tests/src/crud/relations/acceptance/has-many-through.relation.acceptance.ts b/packages/repository-tests/src/crud/relations/acceptance/has-many-through.relation.acceptance.ts new file mode 100644 index 000000000000..a29425278187 --- /dev/null +++ b/packages/repository-tests/src/crud/relations/acceptance/has-many-through.relation.acceptance.ts @@ -0,0 +1,248 @@ +// 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 { + CartItem, + CartItemRepository, + Customer, + CustomerCartItemLink, + CustomerCartItemLinkRepository, + CustomerRepository, +} 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 cartItemRepo: CartItemRepository; + let customerCartItemLinkRepo: CustomerCartItemLinkRepository; + let existingCustomerId: MixedIdType; + + before( + withCrudCtx(async function setupRepository(ctx: CrudTestContext) { + ({ + customerRepo, + cartItemRepo, + customerCartItemLinkRepo, + } = givenBoundCrudRepositories( + ctx.dataSource, + repositoryClass, + features, + )); + await ctx.dataSource.automigrate([ + Customer.name, + CartItem.name, + CustomerCartItemLink.name, + ]); + }), + ); + + beforeEach(async () => { + await customerRepo.deleteAll(); + await cartItemRepo.deleteAll(); + await customerCartItemLinkRepo.deleteAll(); + }); + + beforeEach(async () => { + existingCustomerId = (await givenPersistedCustomerInstance()).id; + }); + + it('can create an instance of the related model alone with a through model', async () => { + const item = await customerRepo + .cartItems(existingCustomerId) + .create( + {description: 'an item'}, + {throughData: {description: 'a through model'}}, + ); + + expect(toJSON(item)).containDeep( + toJSON({ + id: item.id, + description: 'an item', + }), + ); + + const persistedItem = await cartItemRepo.findById(item.id); + expect(toJSON(persistedItem)).to.deepEqual(toJSON(item)); + const persistedLink = await customerCartItemLinkRepo.find(); + expect(toJSON(persistedLink[0])).to.containDeep( + toJSON({ + customerId: existingCustomerId, + cartItemId: item.id, + description: 'a through model', + }), + ); + }); + + it('can find instances of the related model', async () => { + const item = await customerRepo + .cartItems(existingCustomerId) + .create( + {description: 'an item'}, + {throughData: {description: 'a through model'}}, + ); + const notMyItem = await cartItemRepo.create({ + description: "someone else's item", + }); + const result = await customerRepo.cartItems(existingCustomerId).find(); + expect(toJSON(result)).to.containDeep(toJSON([item])); + expect(toJSON(result[0])).to.not.containEql(toJSON(notMyItem)); + }); + + it('can patch instances', async () => { + const item1 = await customerRepo + .cartItems(existingCustomerId) + .create({description: 'group 1'}); + const item2 = await customerRepo + .cartItems(existingCustomerId) + .create({description: 'group 1'}); + const count = await customerRepo + .cartItems(existingCustomerId) + .patch({description: 'updated'}); + + expect(count.count).to.equal(2); + const result = await customerRepo.cartItems(existingCustomerId).find(); + expect(toJSON(result)).to.containDeep( + toJSON([ + {id: item1.id, description: 'updated'}, + {id: item2.id, description: 'updated'}, + ]), + ); + }); + + it('can patch an instance based on the filter', async () => { + const item1 = await customerRepo + .cartItems(existingCustomerId) + .create({description: 'group 1'}); + const item2 = await customerRepo + .cartItems(existingCustomerId) + .create({description: 'group 1'}); + const count = await customerRepo + .cartItems(existingCustomerId) + .patch({description: 'group 2'}, {id: item2.id}); + + expect(count.count).to.equal(1); + const result = await customerRepo.cartItems(existingCustomerId).find(); + expect(toJSON(result)).to.containDeep( + toJSON([ + {id: item1.id, description: 'group 1'}, + {id: item2.id, description: 'group 2'}, + ]), + ); + }); + + it('throws error when query tries to change the target id', async () => { + // a diff id for CartItem instance + const anotherId = (await givenPersistedCustomerInstance()).id; + await customerRepo + .cartItems(existingCustomerId) + .create({description: 'group 1'}); + + await expect( + customerRepo.cartItems(existingCustomerId).patch({id: anotherId}), + ).to.be.rejectedWith(/Property "id" cannot be changed!/); + }); + + it('can delete many instances and their through models', async () => { + await customerRepo + .cartItems(existingCustomerId) + .create({description: 'group 1'}); + await customerRepo + .cartItems(existingCustomerId) + .create({description: 'group 1'}); + + let links = await customerCartItemLinkRepo.find(); + let cartItems = await cartItemRepo.find(); + expect(links).have.length(2); + expect(cartItems).have.length(2); + + await customerRepo.cartItems(existingCustomerId).delete(); + links = await customerCartItemLinkRepo.find(); + cartItems = await cartItemRepo.find(); + expect(links).have.length(0); + expect(cartItems).have.length(0); + }); + + it('can delete corresponding through models when the target gets deleted', async () => { + const item = await customerRepo + .cartItems(existingCustomerId) + .create({description: 'group 1'}); + const anotherId = (await givenPersistedCustomerInstance()).id; + // another through model that links to the same item + await customerCartItemLinkRepo.create({ + customerId: anotherId, + cartItemId: item.id, + }); + + let links = await customerCartItemLinkRepo.find(); + let cartItems = await cartItemRepo.find(); + expect(links).have.length(2); + expect(cartItems).have.length(1); + + await customerRepo.cartItems(existingCustomerId).delete(); + links = await customerCartItemLinkRepo.find(); + cartItems = await cartItemRepo.find(); + expect(links).have.length(0); + expect(cartItems).have.length(0); + }); + + //FIXME(Agnes): should be able to do deletion based on filters + + it('can link a target model to a source model', async () => { + const item = await cartItemRepo.create({description: 'an item'}); + + let targets = await customerRepo.cartItems(existingCustomerId).find(); + expect(targets).to.be.empty(); + + await customerRepo.cartItems(existingCustomerId).link(item.id); + + targets = await customerRepo.cartItems(existingCustomerId).find(); + expect(targets).to.deepEqual([item]); + + const link = await customerCartItemLinkRepo.find(); + expect(toJSON(link[0])).to.containEql( + toJSON({customerId: existingCustomerId, cartItemId: item.id}), + ); + }); + + it('can unlink a target model from a source model', async () => { + const item = await customerRepo + .cartItems(existingCustomerId) + .create({description: 'an item'}); + + let targets = await customerRepo.cartItems(existingCustomerId).find(); + expect(targets).to.deepEqual([item]); + + await customerRepo.cartItems(existingCustomerId).unlink(item.id); + + targets = await customerRepo.cartItems(existingCustomerId).find(); + expect(targets).to.be.empty(); + + const link = await customerCartItemLinkRepo.find(); + expect(link).to.be.empty(); + }); + + async function givenPersistedCustomerInstance() { + return customerRepo.create({name: 'a customer'}); + } + }); +} diff --git a/packages/repository-tests/src/crud/relations/fixtures/models/cart-item.model.ts b/packages/repository-tests/src/crud/relations/fixtures/models/cart-item.model.ts new file mode 100644 index 000000000000..4f0dfe069a64 --- /dev/null +++ b/packages/repository-tests/src/crud/relations/fixtures/models/cart-item.model.ts @@ -0,0 +1,34 @@ +// 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, + model, + property, +} from '@loopback/repository'; +import {MixedIdType} from '../../../../helpers.repository-tests'; + +@model() +export class CartItem extends Entity { + @property({ + id: true, + generated: true, + useDefaultIdType: true, + }) + id: MixedIdType; + + @property({ + type: 'string', + }) + description: string; +} + +export interface CartItemRelations {} + +export type CartItemWithRelations = CartItem & CartItemRelations; + +export interface CartItemRepository + extends EntityCrudRepository {} diff --git a/packages/repository-tests/src/crud/relations/fixtures/models/customer-cart-item-link.model.ts b/packages/repository-tests/src/crud/relations/fixtures/models/customer-cart-item-link.model.ts new file mode 100644 index 000000000000..cd5b8fc68417 --- /dev/null +++ b/packages/repository-tests/src/crud/relations/fixtures/models/customer-cart-item-link.model.ts @@ -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 { + Entity, + EntityCrudRepository, + model, + property, +} from '@loopback/repository'; +import {MixedIdType} from '../../../../helpers.repository-tests'; + +@model() +export class CustomerCartItemLink extends Entity { + @property({ + id: true, + generated: true, + useDefaultIdType: true, + }) + id: MixedIdType; + + @property() + customerId: MixedIdType; + + @property() + cartItemId: MixedIdType; + + @property({ + type: 'string', + }) + description?: string; +} + +export interface CustomerCartItemLinkRelations {} + +export type CustomerCartItemLinkWithRelations = CustomerCartItemLink & + CustomerCartItemLinkRelations; + +export interface CustomerCartItemLinkRepository + extends EntityCrudRepository< + CustomerCartItemLink, + typeof CustomerCartItemLink.prototype.id + > {} diff --git a/packages/repository-tests/src/crud/relations/fixtures/models/customer.model.ts b/packages/repository-tests/src/crud/relations/fixtures/models/customer.model.ts index cac6702bc682..78dcbfecc812 100644 --- a/packages/repository-tests/src/crud/relations/fixtures/models/customer.model.ts +++ b/packages/repository-tests/src/crud/relations/fixtures/models/customer.model.ts @@ -9,6 +9,7 @@ import { EntityCrudRepository, hasMany, HasManyRepositoryFactory, + HasManyThroughRepositoryFactory, hasOne, HasOneRepositoryFactory, model, @@ -17,6 +18,8 @@ import { import {BelongsToAccessor} from '@loopback/repository/src'; import {MixedIdType} from '../../../../helpers.repository-tests'; import {Address, AddressWithRelations} from './address.model'; +import {CartItem, CartItemWithRelations} from './cart-item.model'; +import {CustomerCartItemLink} from './customer-cart-item-link.model'; import {Order, OrderWithRelations} from './order.model'; @model() @@ -44,6 +47,9 @@ export class Customer extends Entity { @belongsTo(() => Customer) parentId?: MixedIdType; + + @hasMany(() => CartItem, {through: {model: () => CustomerCartItemLink}}) + cartItems: CartItem[]; } export interface CustomerRelations { @@ -51,6 +57,7 @@ export interface CustomerRelations { orders?: OrderWithRelations[]; customers?: CustomerWithRelations[]; parentCustomer?: CustomerWithRelations; + cartItems?: CartItemWithRelations[]; } export type CustomerWithRelations = Customer & CustomerRelations; @@ -62,4 +69,10 @@ export interface CustomerRepository orders: HasManyRepositoryFactory; customers: HasManyRepositoryFactory; parent: BelongsToAccessor; + cartItems: HasManyThroughRepositoryFactory< + CartItem, + MixedIdType, + CustomerCartItemLink, + MixedIdType + >; } diff --git a/packages/repository-tests/src/crud/relations/fixtures/models/index.ts b/packages/repository-tests/src/crud/relations/fixtures/models/index.ts index 4fafc42e4f5c..60d0704c96e7 100644 --- a/packages/repository-tests/src/crud/relations/fixtures/models/index.ts +++ b/packages/repository-tests/src/crud/relations/fixtures/models/index.ts @@ -4,6 +4,8 @@ // License text available at https://opensource.org/licenses/MIT export * from './address.model'; +export * from './cart-item.model'; +export * from './customer-cart-item-link.model'; export * from './customer.model'; export * from './order.model'; export * from './shipment.model'; diff --git a/packages/repository-tests/src/crud/relations/fixtures/repositories/cart-item.repository.ts b/packages/repository-tests/src/crud/relations/fixtures/repositories/cart-item.repository.ts new file mode 100644 index 000000000000..2aa7bbe7ac8c --- /dev/null +++ b/packages/repository-tests/src/crud/relations/fixtures/repositories/cart-item.repository.ts @@ -0,0 +1,21 @@ +// 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 {juggler} from '@loopback/repository'; +import {CrudRepositoryCtor} from '../../../..'; +import {CartItem, CartItemRelations} from '../models'; + +// create the CartItemRepo by calling this func so that it can be extended from CrudRepositoryCtor +export function createCartItemRepo(repoClass: CrudRepositoryCtor) { + return class CartItemRepository extends repoClass< + CartItem, + typeof CartItem.prototype.id, + CartItemRelations + > { + constructor(db: juggler.DataSource) { + super(CartItem, db); + } + }; +} diff --git a/packages/repository-tests/src/crud/relations/fixtures/repositories/customer-cart-item-link.repository.ts b/packages/repository-tests/src/crud/relations/fixtures/repositories/customer-cart-item-link.repository.ts new file mode 100644 index 000000000000..7ecf103b30db --- /dev/null +++ b/packages/repository-tests/src/crud/relations/fixtures/repositories/customer-cart-item-link.repository.ts @@ -0,0 +1,20 @@ +// 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 {juggler} from '@loopback/repository'; +import {CrudRepositoryCtor} from '../../../..'; +import {CustomerCartItemLink, CustomerCartItemLinkRelations} from '../models'; +// create the CustomerCartItemLinkRepo by calling this func so that it can be extended from CrudRepositoryCtor +export function createCustomerCartItemLinkRepo(repoClass: CrudRepositoryCtor) { + return class CustomerCartItemLinkRepository extends repoClass< + CustomerCartItemLink, + typeof CustomerCartItemLink.prototype.id, + CustomerCartItemLinkRelations + > { + constructor(db: juggler.DataSource) { + super(CustomerCartItemLink, db); + } + }; +} diff --git a/packages/repository-tests/src/crud/relations/fixtures/repositories/customer.repository.ts b/packages/repository-tests/src/crud/relations/fixtures/repositories/customer.repository.ts index c647d52dfefc..2d0447e4da27 100644 --- a/packages/repository-tests/src/crud/relations/fixtures/repositories/customer.repository.ts +++ b/packages/repository-tests/src/crud/relations/fixtures/repositories/customer.repository.ts @@ -6,18 +6,27 @@ import {Getter} from '@loopback/core'; import { BelongsToAccessor, - HasManyRepositoryFactory, - HasOneRepositoryFactory, - juggler, + BelongsToDefinition, + createBelongsToAccessor, createHasManyRepositoryFactory, + createHasManyThroughRepositoryFactory, + createHasOneRepositoryFactory, HasManyDefinition, + HasManyRepositoryFactory, + 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, + CartItem, + Customer, + CustomerCartItemLink, + CustomerRelations, + Order, +} from '../models'; // create the CustomerRepo by calling this func so that it can be extended from CrudRepositoryCtor export function createCustomerRepo(repoClass: CrudRepositoryCtor) { @@ -42,11 +51,19 @@ export function createCustomerRepo(repoClass: CrudRepositoryCtor) { Customer, typeof Customer.prototype.id >; + public readonly cartItems: HasManyThroughRepositoryFactory< + CartItem, + typeof CartItem.prototype.id, + CustomerCartItemLink, + typeof CustomerCartItemLink.prototype.id + >; constructor( db: juggler.DataSource, orderRepositoryGetter: Getter, addressRepositoryGetter: Getter, + cartItemRepositoryGetter: Getter, + customerCartItemLinkRepositoryGetter: Getter, ) { super(Customer, db); const ordersMeta = this.entityClass.definition.relations['orders']; @@ -72,6 +89,12 @@ export function createCustomerRepo(repoClass: CrudRepositoryCtor) { Getter.fromValue(this), this, ); + const cartItemsMeta = this.entityClass.definition.relations['cartItems']; + this.cartItems = createHasManyThroughRepositoryFactory( + cartItemsMeta as HasManyDefinition, + cartItemRepositoryGetter, + customerCartItemLinkRepositoryGetter, + ); } }; } diff --git a/packages/repository-tests/src/crud/relations/fixtures/repositories/index.ts b/packages/repository-tests/src/crud/relations/fixtures/repositories/index.ts index e908c0cfdad5..7d785ece88b7 100644 --- a/packages/repository-tests/src/crud/relations/fixtures/repositories/index.ts +++ b/packages/repository-tests/src/crud/relations/fixtures/repositories/index.ts @@ -4,6 +4,8 @@ // License text available at https://opensource.org/licenses/MIT export * from './address.repository'; +export * from './cart-item.repository'; +export * from './customer-cart-item-link.repository'; export * from './customer.repository'; export * from './order.repository'; export * from './shipment.repository'; diff --git a/packages/repository-tests/src/crud/relations/fixtures/repositories/order.repository.ts b/packages/repository-tests/src/crud/relations/fixtures/repositories/order.repository.ts index e93c6f756d32..8af671c1a2dd 100644 --- a/packages/repository-tests/src/crud/relations/fixtures/repositories/order.repository.ts +++ b/packages/repository-tests/src/crud/relations/fixtures/repositories/order.repository.ts @@ -1,4 +1,4 @@ -// Copyright IBM Corp. 2019. All Rights Reserved. +// Copyright IBM Corp. 2019,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 @@ -6,12 +6,12 @@ import {Getter} from '@loopback/core'; import { BelongsToAccessor, - juggler, - createBelongsToAccessor, BelongsToDefinition, + createBelongsToAccessor, + juggler, } from '@loopback/repository'; -import {Customer, Order, OrderRelations, Shipment} from '../models'; import {CrudRepositoryCtor} from '../../../..'; +import {Customer, Order, OrderRelations, Shipment} from '../models'; // create the OrderRepo by calling this func so that it can be extended from CrudRepositoryCtor export function createOrderRepo(repoClass: CrudRepositoryCtor) { diff --git a/packages/repository-tests/src/crud/relations/helpers.ts b/packages/repository-tests/src/crud/relations/helpers.ts index 1bfa269876da..0d57158981c4 100644 --- a/packages/repository-tests/src/crud/relations/helpers.ts +++ b/packages/repository-tests/src/crud/relations/helpers.ts @@ -8,7 +8,11 @@ import {CrudFeatures, CrudRepositoryCtor} from '../..'; import { Address, AddressRepository, + CartItem, + CartItemRepository, Customer, + CustomerCartItemLink, + CustomerCartItemLinkRepository, CustomerRepository, Order, OrderRepository, @@ -17,6 +21,8 @@ import { } from './fixtures/models'; import { createAddressRepo, + createCartItemRepo, + createCustomerCartItemLinkRepo, createCustomerRepo, createOrderRepo, createShipmentRepo, @@ -30,6 +36,8 @@ export function givenBoundCrudRepositories( Order.definition.properties.id.type = features.idType; Address.definition.properties.id.type = features.idType; Customer.definition.properties.id.type = features.idType; + CartItem.definition.properties.id.type = features.idType; + CustomerCartItemLink.definition.properties.id.type = features.idType; Shipment.definition.properties.id.type = features.idType; // when running the test suite on MongoDB, we don't really need to setup // this config for mongo connector to pass the test. @@ -43,12 +51,22 @@ export function givenBoundCrudRepositories( Address.definition.properties.customerId.mongodb = { dataType: 'ObjectID', }; + CustomerCartItemLink.definition.properties.customerId.type = features.idType; + CustomerCartItemLink.definition.properties.customerId.mongodb = { + dataType: 'ObjectID', + }; + CustomerCartItemLink.definition.properties.cartItemId.type = features.idType; + CustomerCartItemLink.definition.properties.cartItemId.mongodb = { + dataType: 'ObjectID', + }; // get the repository class and create a new instance of it const customerRepoClass = createCustomerRepo(repositoryClass); const customerRepo: CustomerRepository = new customerRepoClass( db, async () => orderRepo, async () => addressRepo, + async () => cartItemRepo, + async () => customerCartItemLinkRepo, ); // register the inclusionResolvers here for customerRepo @@ -94,10 +112,22 @@ export function givenBoundCrudRepositories( async () => customerRepo, ); + const cartItemRepoClass = createCartItemRepo(repositoryClass); + const cartItemRepo: CartItemRepository = new cartItemRepoClass(db); + + const customerCartItemLinkRepoClass = createCustomerCartItemLinkRepo( + repositoryClass, + ); + const customerCartItemLinkRepo: CustomerCartItemLinkRepository = new customerCartItemLinkRepoClass( + db, + ); + return { customerRepo, orderRepo, shipmentRepo, addressRepo, + cartItemRepo, + customerCartItemLinkRepo, }; } diff --git a/packages/repository/src/relations/has-many/has-many-through.helpers.ts b/packages/repository/src/relations/has-many/has-many-through.helpers.ts index 039a8b6b0f53..0d8e8a5c7552 100644 --- a/packages/repository/src/relations/has-many/has-many-through.helpers.ts +++ b/packages/repository/src/relations/has-many/has-many-through.helpers.ts @@ -193,7 +193,7 @@ export function createThroughConstraintFromSource< * keyTo: 'productId', * }, * }; - * createThroughConstraintFromTarget(resolvedMetadata, 3); + * createThroughConstraintFromTarget(resolvedMetadata, [3]); * * >>> {productId: 3} *