From 2d4364254aae9eaf519acbbc8627425927e7e252 Mon Sep 17 00:00:00 2001 From: Agnes Lin Date: Thu, 11 Jun 2020 12:46:01 -0400 Subject: [PATCH 1/2] feat(repository): add link and unlink methods --- .../resolve-has-many-through-metadata.unit.ts | 104 +++++++++----- .../has-many-through-repository.factory.ts | 59 ++++---- .../has-many/has-many-through.helpers.ts | 135 +++++++++++++----- .../has-many/has-many-through.repository.ts | 86 ++++++----- 4 files changed, 254 insertions(+), 130 deletions(-) diff --git a/packages/repository/src/__tests__/unit/repositories/relations-helpers/resolve-has-many-through-metadata.unit.ts b/packages/repository/src/__tests__/unit/repositories/relations-helpers/resolve-has-many-through-metadata.unit.ts index f59a38703989..998698157ebf 100644 --- a/packages/repository/src/__tests__/unit/repositories/relations-helpers/resolve-has-many-through-metadata.unit.ts +++ b/packages/repository/src/__tests__/unit/repositories/relations-helpers/resolve-has-many-through-metadata.unit.ts @@ -13,23 +13,53 @@ import { RelationType, } from '../../../..'; import { - createTargetConstraint, - createThroughConstraint, - createThroughFkConstraint, + createTargetConstraintFromThrough, + createThroughConstraintFromSource, + createThroughConstraintFromTarget, + getTargetKeysFromThroughModels, HasManyThroughResolvedDefinition, resolveHasManyThroughMetadata, } from '../../../../relations/has-many/has-many-through.helpers'; describe('HasManyThroughHelpers', () => { - context('createThroughConstraint', () => { - it('can create constraint for searching through models', () => { - const resolved = resolvedMetadata as HasManyThroughResolvedDefinition; - const result = createThroughConstraint(resolved, 1); + context('createThroughConstraintFromSource', () => { + it('creates constraint for searching through models', () => { + const result = createThroughConstraintFromSource(relationMetaData, 1); expect(result).to.containEql({categoryId: 1}); }); }); - context('createTargetConstraint', () => { - it('can create constraint for searching target models', () => { + context('getTargetKeysFromThroughModels', () => { + it('returns the target fk value of a given through instance', () => { + const through1 = createCategoryProductLink({ + id: 1, + categoryId: 2, + productId: 9, + }); + const result = getTargetKeysFromThroughModels(relationMetaData, [ + through1, + ]); + expect(result).to.deepEqual([9]); + }); + it('returns the target fk values of given through instances', () => { + const through1 = createCategoryProductLink({ + id: 1, + categoryId: 2, + productId: 9, + }); + const through2 = createCategoryProductLink({ + id: 2, + categoryId: 2, + productId: 8, + }); + const result = getTargetKeysFromThroughModels(relationMetaData, [ + through1, + through2, + ]); + expect(result).to.containDeep([9, 8]); + }); + }); + context('createTargetConstraintFromThrough', () => { + it('creates constraint for searching target models', () => { const through1 = createCategoryProductLink({ id: 1, categoryId: 2, @@ -40,17 +70,21 @@ describe('HasManyThroughHelpers', () => { categoryId: 2, productId: 8, }); - const resolved = resolvedMetadata as HasManyThroughResolvedDefinition; // single through model - let result = createTargetConstraint(resolved, through1); + let result = createTargetConstraintFromThrough(relationMetaData, [ + through1, + ]); expect(result).to.containEql({id: 9}); // multiple through models - result = createTargetConstraint(resolved, [through1, through2]); + result = createTargetConstraintFromThrough(relationMetaData, [ + through1, + through2, + ]); expect(result).to.containEql({id: {inq: [9, 8]}}); }); - it('can create constraint for searching target models with duplicate keys', () => { + it('creates constraint for searching target models with duplicate keys', () => { const through1 = createCategoryProductLink({ id: 1, categoryId: 2, @@ -61,22 +95,31 @@ describe('HasManyThroughHelpers', () => { categoryId: 3, productId: 9, }); - const resolved = resolvedMetadata as HasManyThroughResolvedDefinition; - const result = createTargetConstraint(resolved, [through1, through2]); + const result = createTargetConstraintFromThrough(relationMetaData, [ + through1, + through2, + ]); expect(result).to.containEql({id: 9}); }); }); - context('createThroughFkConstraint', () => { - it('can create constraint with a given target instance', () => { - const product = createProduct({ - id: 1, - }); - const resolved = resolvedMetadata as HasManyThroughResolvedDefinition; - - const result = createThroughFkConstraint(resolved, product); + context('createThroughConstraintFromTarget', () => { + it('creates constraint with a given fk', () => { + const result = createThroughConstraintFromTarget(relationMetaData, [1]); expect(result).to.containEql({productId: 1}); }); + it('creates constraint with given fks', () => { + const result = createThroughConstraintFromTarget(relationMetaData, [ + 1, + 2, + ]); + expect(result).to.containEql({productId: {inq: [1, 2]}}); + }); + it('throws if fkValue is undefined', () => { + expect(() => + createThroughConstraintFromTarget(relationMetaData, []), + ).to.throw(/"fkValue" must be provided/); + }); }); context('resolveHasManyThroughMetadata', () => { it('throws if the wrong metadata type is used', async () => { @@ -149,7 +192,7 @@ describe('HasManyThroughHelpers', () => { metadata as HasManyDefinition, ); - expect(meta).to.eql(resolvedMetadata); + expect(meta).to.eql(relationMetaData); }); it('infers through.keyFrom if it is not provided', () => { @@ -172,7 +215,7 @@ describe('HasManyThroughHelpers', () => { metadata as HasManyDefinition, ); - expect(meta).to.eql(resolvedMetadata); + expect(meta).to.eql(relationMetaData); }); it('infers through.keyTo if it is not provided', () => { @@ -196,7 +239,7 @@ describe('HasManyThroughHelpers', () => { metadata as HasManyDefinition, ); - expect(meta).to.eql(resolvedMetadata); + expect(meta).to.eql(relationMetaData); }); it('throws if through.keyFrom is not provided in through', async () => { @@ -245,7 +288,7 @@ describe('HasManyThroughHelpers', () => { ); }); - it('throws if the tarhet model does not have the id property', async () => { + it('throws if the target model does not have the id property', async () => { const metadata = { name: 'categories', type: RelationType.hasMany, @@ -315,7 +358,7 @@ describe('HasManyThroughHelpers', () => { } } - const resolvedMetadata = { + const relationMetaData = { name: 'products', type: 'hasMany', targetsMany: true, @@ -328,7 +371,7 @@ describe('HasManyThroughHelpers', () => { keyFrom: 'categoryId', keyTo: 'productId', }, - }; + } as HasManyThroughResolvedDefinition; class InvalidThrough extends Entity {} InvalidThrough.definition = new ModelDefinition('InvalidThrough') @@ -353,7 +396,4 @@ describe('HasManyThroughHelpers', () => { function createCategoryProductLink(properties: Partial) { return new CategoryProductLink(properties); } - function createProduct(properties: Partial) { - return new Product(properties); - } }); diff --git a/packages/repository/src/relations/has-many/has-many-through-repository.factory.ts b/packages/repository/src/relations/has-many/has-many-through-repository.factory.ts index 9234e51d9833..f254cc56d515 100644 --- a/packages/repository/src/relations/has-many/has-many-through-repository.factory.ts +++ b/packages/repository/src/relations/has-many/has-many-through-repository.factory.ts @@ -2,7 +2,6 @@ // Node module: @loopback/repository // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT - import { DataObject, Entity, @@ -11,9 +10,10 @@ import { HasManyDefinition, } from '../..'; import { - createTargetConstraint, - createThroughConstraint, - createThroughFkConstraint, + createTargetConstraintFromThrough, + createThroughConstraintFromSource, + createThroughConstraintFromTarget, + getTargetKeysFromThroughModels, resolveHasManyThroughMetadata, } from './has-many-through.helpers'; import { @@ -33,9 +33,9 @@ export type HasManyThroughRepositoryFactory< TargetEntity extends Entity, TargetID, ThroughEntity extends Entity, - ForeignKeyType + SourceID > = ( - fkValue: ForeignKeyType, + fkValue: SourceID, ) => HasManyThroughRepository; export function createHasManyThroughRepositoryFactory< @@ -43,35 +43,41 @@ export function createHasManyThroughRepositoryFactory< TargetID, Through extends Entity, ThroughID, - ForeignKeyType + SourceID >( relationMetadata: HasManyDefinition, targetRepositoryGetter: Getter>, throughRepositoryGetter: Getter>, -): HasManyThroughRepositoryFactory { +): HasManyThroughRepositoryFactory { const meta = resolveHasManyThroughMetadata(relationMetadata); - const result = function (fkValue: ForeignKeyType) { - function getTargetContraint( - throughInstances: Through | Through[], + const result = function (fkValue: SourceID) { + function getTargetConstraintFromThroughModels( + throughInstances: Through[], ): DataObject { - return createTargetConstraint(meta, throughInstances); + return createTargetConstraintFromThrough( + meta, + throughInstances, + ); + } + function getTargetKeys(throughInstances: Through[]): TargetID[] { + return getTargetKeysFromThroughModels(meta, throughInstances); } - function getThroughConstraint(): DataObject { - const constriant: DataObject = createThroughConstraint< + function getThroughConstraintFromSource(): DataObject { + const constraint: DataObject = createThroughConstraintFromSource< Through, - ForeignKeyType + SourceID >(meta, fkValue); - return constriant; + return constraint; } - function getThroughFkConstraint( - targetInstance: Target, + function getThroughConstraintFromTarget( + fkValues: TargetID[], ): DataObject { - const constriant: DataObject = createThroughFkConstraint< - Target, - Through - >(meta, targetInstance); - return constriant; + const constraint: DataObject = createThroughConstraintFromTarget< + Through, + TargetID + >(meta, fkValues); + return constraint; } return new DefaultHasManyThroughRepository< @@ -84,9 +90,10 @@ export function createHasManyThroughRepositoryFactory< >( targetRepositoryGetter, throughRepositoryGetter, - getTargetContraint, - getThroughConstraint, - getThroughFkConstraint, + getTargetConstraintFromThroughModels, + getTargetKeys, + getThroughConstraintFromSource, + getThroughConstraintFromTarget, ); }; return result; 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 b4a510ec027a..039a8b6b0f53 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 @@ -12,7 +12,6 @@ import { HasManyDefinition, InvalidRelationError, isTypeResolver, - StringKeyOf, } from '../..'; import {resolveHasManyMetaHelper} from './has-many.helpers'; @@ -28,10 +27,9 @@ export type HasManyThroughResolvedDefinition = HasManyDefinition & { }; /** - * Creates constraint used to query target + * Creates target constraint based on through models * @param relationMeta - resolved hasManyThrough metadata - * @param throughInstances - Instances of through entities used to constrain the target - * @internal + * @param throughInstances - an array of through instances * * @example * ```ts @@ -45,8 +43,13 @@ export type HasManyThroughResolvedDefinition = HasManyDefinition & { * keyTo: 'productId', * }, * }; - - * createTargetConstraint(resolvedMetadata, [ + * createTargetConstraintFromThrough(resolvedMetadata,[{ + id: 2, + categoryId: 2, + productId: 8, + }]); + * >>> {id: 8} + * createTargetConstraintFromThrough(resolvedMetadata, [ { id: 2, categoryId: 2, @@ -61,23 +64,19 @@ export type HasManyThroughResolvedDefinition = HasManyDefinition & { >>> {id: {inq: [9, 8]}} * ``` */ -export function createTargetConstraint< +export function createTargetConstraintFromThrough< Target extends Entity, Through extends Entity >( relationMeta: HasManyThroughResolvedDefinition, - throughInstances: Through | Through[], + throughInstances: Through[], ): DataObject { - const targetPrimaryKey = relationMeta.keyTo; - const targetFkName = relationMeta.through.keyTo; - if (!Array.isArray(throughInstances)) { - throughInstances = [throughInstances]; - } - let fkValues = throughInstances.map( - (throughInstance: Through) => - throughInstance[targetFkName as keyof Through], + const fkValues = getTargetKeysFromThroughModels( + relationMeta, + throughInstances, ); - fkValues = deduplicate(fkValues); + const targetPrimaryKey = relationMeta.keyTo; + // eslint-disable-next-line @typescript-eslint/no-explicit-any const constraint: any = { [targetPrimaryKey]: fkValues.length === 1 ? fkValues[0] : {inq: fkValues}, @@ -86,11 +85,64 @@ export function createTargetConstraint< } /** - * Creates constraint used to query through model + * Returns an array of target fks of the given throughInstances. * * @param relationMeta - resolved hasManyThrough metadata - * @param fkValue - Value of the foreign key of the source model used to constrain through - * @param targetInstance - Instance of target entity used to constrain through + * @param throughInstances - an array of through instances + * + * @example + * ```ts + * const resolvedMetadata = { + * // .. other props + * keyFrom: 'id', + * keyTo: 'id', + * through: { + * model: () => CategoryProductLink, + * keyFrom: 'categoryId', + * keyTo: 'productId', + * }, + * }; + * getTargetKeysFromThroughModels(resolvedMetadata,[{ + id: 2, + categoryId: 2, + productId: 8, + }]); + * >>> [8] + * getTargetKeysFromThroughModels(resolvedMetadata, [ + { + id: 2, + categoryId: 2, + productId: 8, + }, { + id: 1, + categoryId: 2, + productId: 9, + } + ]); + >>> [8, 9] + */ +export function getTargetKeysFromThroughModels< + Through extends Entity, + TargetID +>( + relationMeta: HasManyThroughResolvedDefinition, + throughInstances: Through[], +): TargetID[] { + const targetFkName = relationMeta.through.keyTo; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let fkValues: any = throughInstances.map( + (throughInstance: Through) => + throughInstance[targetFkName as keyof Through], + ); + fkValues = deduplicate(fkValues); + return fkValues as TargetID[]; +} + +/** + * Creates through constraint based on the source key + * + * @param relationMeta - resolved hasManyThrough metadata + * @param fkValue - foreign key of the source instance * @internal * * @example @@ -105,14 +157,17 @@ export function createTargetConstraint< * keyTo: 'productId', * }, * }; - * createThroughConstraint(resolvedMetadata, 1); + * createThroughConstraintFromSource(resolvedMetadata, 1); * * >>> {categoryId: 1} * ``` */ -export function createThroughConstraint( +export function createThroughConstraintFromSource< + Through extends Entity, + SourceID +>( relationMeta: HasManyThroughResolvedDefinition, - fkValue: ForeignKeyType, + fkValue: SourceID, ): DataObject { const sourceFkName = relationMeta.through.keyFrom; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -120,10 +175,10 @@ export function createThroughConstraint( return constraint; } /** - * Creates constraint used to create the through model + * Creates through constraint based on the target foreign key * * @param relationMeta - resolved hasManyThrough metadata - * @param targetInstance instance of target entity used to constrain through + * @param fkValue an array of the target instance foreign keys * @internal * * @example @@ -138,25 +193,33 @@ export function createThroughConstraint( * keyTo: 'productId', * }, * }; - * createThroughConstraint(resolvedMetadata, {id: 3, name: 'a product'}); + * createThroughConstraintFromTarget(resolvedMetadata, 3); * - * >>> {productId: 1} + * >>> {productId: 3} * - * createThroughConstraint(resolvedMetadata, {id: {inq:[3,4]}}); + * createThroughConstraintFromTarget(resolvedMetadata, [3,4]); * * >>> {productId: {inq:[3,4]}} */ -export function createThroughFkConstraint( +export function createThroughConstraintFromTarget< + Through extends Entity, + TargetID +>( relationMeta: HasManyThroughResolvedDefinition, - targetInstance: Target, + fkValues: TargetID[], ): DataObject { - const targetKey = relationMeta.keyTo as StringKeyOf; + if (fkValues === undefined || fkValues.length === 0) { + throw new Error('"fkValue" must be provided'); + } const targetFkName = relationMeta.through.keyTo; + // eslint-disable-next-line @typescript-eslint/no-explicit-any - const constraint: any = { - [targetFkName]: targetInstance[targetKey], - }; - return constraint; + const constraint: any = + fkValues.length === 1 + ? {[targetFkName]: fkValues[0]} + : {[targetFkName]: {inq: fkValues}}; + + return constraint as DataObject; } /** @@ -196,7 +259,7 @@ export function resolveHasManyThroughMetadata( relationMeta.keyTo && targetModelProperties[relationMeta.keyTo] ) { - // The explict cast is needed because of a limitation of type inference + // The explicit cast is needed because of a limitation of type inference return relationMeta as HasManyThroughResolvedDefinition; } diff --git a/packages/repository/src/relations/has-many/has-many-through.repository.ts b/packages/repository/src/relations/has-many/has-many-through.repository.ts index a57e4397caa7..17c67f5cdf9b 100644 --- a/packages/repository/src/relations/has-many/has-many-through.repository.ts +++ b/packages/repository/src/relations/has-many/has-many-through.repository.ts @@ -96,7 +96,7 @@ export interface HasManyThroughRepository< throughData?: DataObject; throughOptions?: Options; }, - ): Promise; + ): Promise; /** * Removes an association to an existing target model instance @@ -130,12 +130,13 @@ export class DefaultHasManyThroughRepository< constructor( public getTargetRepository: Getter, public getThroughRepository: Getter, - public getTargetConstraint: ( - throughInstances: ThroughEntity | ThroughEntity[], + public getTargetConstraintFromThroughModels: ( + throughInstances: ThroughEntity[], ) => DataObject, - public getThroughConstraint: () => DataObject, - public getThroughFkConstraint: ( - targetInstance: TargetEntity, + public getTargetKeys: (throughInstances: ThroughEntity[]) => TargetID[], + public getThroughConstraintFromSource: () => DataObject, + public getThroughConstraintFromTarget: ( + targetID: TargetID[], ) => DataObject, ) {} @@ -147,21 +148,11 @@ export class DefaultHasManyThroughRepository< }, ): Promise { const targetRepository = await this.getTargetRepository(); - const throughRepository = await this.getThroughRepository(); const targetInstance = await targetRepository.create( targetModelData, options, ); - const targetConstraint = this.getThroughFkConstraint(targetInstance); - const throughConstraint = this.getThroughConstraint(); - const constraints = {...targetConstraint, ...throughConstraint}; - await throughRepository.create( - constrainDataObject( - options?.throughData ?? {}, - constraints as DataObject, - ), - options?.throughOptions, - ); + await this.link(targetInstance.getId(), options); return targetInstance; } @@ -173,12 +164,14 @@ export class DefaultHasManyThroughRepository< ): Promise { const targetRepository = await this.getTargetRepository(); const throughRepository = await this.getThroughRepository(); - const throughConstraint = this.getThroughConstraint(); + const sourceConstraint = this.getThroughConstraintFromSource(); const throughInstances = await throughRepository.find( - constrainFilter(undefined, throughConstraint), + constrainFilter(undefined, sourceConstraint), options?.throughOptions, ); - const targetConstraint = this.getTargetConstraint(throughInstances); + const targetConstraint = this.getTargetConstraintFromThroughModels( + throughInstances, + ); return targetRepository.find( constrainFilter(filter, targetConstraint), options, @@ -193,22 +186,24 @@ export class DefaultHasManyThroughRepository< ): Promise { const targetRepository = await this.getTargetRepository(); const throughRepository = await this.getThroughRepository(); - const throughConstraint = this.getThroughConstraint(); + const sourceConstraint = this.getThroughConstraintFromSource(); const throughInstances = await throughRepository.find( - constrainFilter(undefined, throughConstraint), + constrainFilter(undefined, sourceConstraint), options?.throughOptions, ); - const targetConstraint = this.getTargetConstraint(throughInstances); - - // delete throughs that have the targets that are going to be deleted - const throughFkConstraint = this.getThroughFkConstraint( - targetConstraint as TargetEntity, + const targetFkValues = this.getTargetKeys(throughInstances); + // delete through instances that have the targets that are going to be deleted + const throughFkConstraint = this.getThroughConstraintFromTarget( + targetFkValues, ); await throughRepository.deleteAll( - constrainWhereOr({}, [throughConstraint, throughFkConstraint]), + constrainWhereOr({}, [sourceConstraint, throughFkConstraint]), ); // delete target(s) + const targetConstraint = this.getTargetConstraintFromThroughModels( + throughInstances, + ); return targetRepository.deleteAll( constrainWhere(where, targetConstraint as Where), options, @@ -224,12 +219,14 @@ export class DefaultHasManyThroughRepository< ): Promise { const targetRepository = await this.getTargetRepository(); const throughRepository = await this.getThroughRepository(); - const throughConstraint = this.getThroughConstraint(); + const sourceConstraint = this.getThroughConstraintFromSource(); const throughInstances = await throughRepository.find( - constrainFilter(undefined, throughConstraint), + constrainFilter(undefined, sourceConstraint), options?.throughOptions, ); - const targetConstraint = this.getTargetConstraint(throughInstances); + const targetConstraint = this.getTargetConstraintFromThroughModels( + throughInstances, + ); return targetRepository.updateAll( constrainDataObject(dataObject, targetConstraint), constrainWhere(where, targetConstraint as Where), @@ -238,21 +235,38 @@ export class DefaultHasManyThroughRepository< } async link( - targetModelId: TargetID, + targetId: TargetID, options?: Options & { throughData?: DataObject; throughOptions?: Options; }, - ): Promise { - throw new Error('Method not implemented.'); + ): Promise { + const throughRepository = await this.getThroughRepository(); + const sourceConstraint = this.getThroughConstraintFromSource(); + const targetConstraint = this.getThroughConstraintFromTarget([targetId]); + const constraints = {...targetConstraint, ...sourceConstraint}; + await throughRepository.create( + constrainDataObject( + options?.throughData ?? {}, + constraints as DataObject, + ), + options?.throughOptions, + ); } async unlink( - targetModelId: TargetID, + targetId: TargetID, options?: Options & { throughOptions?: Options; }, ): Promise { - throw new Error('Method not implemented.'); + const throughRepository = await this.getThroughRepository(); + const sourceConstraint = this.getThroughConstraintFromSource(); + const targetConstraint = this.getThroughConstraintFromTarget([targetId]); + const constraints = {...targetConstraint, ...sourceConstraint}; + await throughRepository.deleteAll( + constrainDataObject({}, constraints as DataObject), + options?.throughOptions, + ); } } From 633f1bdcbe43f96233f385a963d3d9c7be7476f2 Mon Sep 17 00:00:00 2001 From: Agnes Lin Date: Thu, 11 Jun 2020 13:12:53 -0400 Subject: [PATCH 2/2] test(repository): add tests for hasManyThrough link and unlink methods --- .../relation.factory.integration.ts | 189 +++++++++--------- 1 file changed, 90 insertions(+), 99 deletions(-) diff --git a/packages/repository/src/__tests__/integration/repositories/relation.factory.integration.ts b/packages/repository/src/__tests__/integration/repositories/relation.factory.integration.ts index 7b63885ed855..c1b088147e8d 100644 --- a/packages/repository/src/__tests__/integration/repositories/relation.factory.integration.ts +++ b/packages/repository/src/__tests__/integration/repositories/relation.factory.integration.ts @@ -30,7 +30,7 @@ let db: juggler.DataSource; let customerRepo: EntityCrudRepository; let orderRepo: EntityCrudRepository; let cartItemRepo: EntityCrudRepository; -let CustomerCartItemLinkRepo: EntityCrudRepository< +let customerCartItemLinkRepo: EntityCrudRepository< CustomerCartItemLink, typeof CustomerCartItemLink.prototype.id >; @@ -232,40 +232,30 @@ describe('HasManyThrough relation', () => { beforeEach(async function resetDatabase() { await customerRepo.deleteAll(); - await CustomerCartItemLinkRepo.deleteAll(); + await customerCartItemLinkRepo.deleteAll(); await cartItemRepo.deleteAll(); }); - it('creates a target instance alone with the corresponding through model', async () => { - const cartItem = await customerCartItemRepo.create( - { - description: 'an item hasManyThrough', - }, - { - throughData: {id: 99}, - }, - ); + it('creates a target instance along with the corresponding through model', async () => { + const cartItem = await customerCartItemRepo.create({ + description: 'an item hasManyThrough', + }); const persistedItem = await cartItemRepo.findById(cartItem.id); - const persistedLink = await CustomerCartItemLinkRepo.find(); + const persistedLink = await customerCartItemLinkRepo.find(); + expect(cartItem).to.deepEqual(persistedItem); expect(persistedLink).have.length(1); const expected = { - id: 99, customerId: existingCustomerId, itemId: cartItem.id, }; - expect(toJSON(persistedLink[0])).to.deepEqual(toJSON(expected)); + expect(toJSON(persistedLink[0])).to.containEql(toJSON(expected)); }); it('finds an instance via through model', async () => { - const item = await customerCartItemRepo.create( - { - description: 'an item hasManyThrough', - }, - { - throughData: {id: 99}, - }, - ); + const item = await customerCartItemRepo.create({ + description: 'an item hasManyThrough', + }); const notMyItem = await cartItemRepo.create({ description: "someone else's item desc", }); @@ -277,22 +267,10 @@ describe('HasManyThrough relation', () => { }); it('finds instances via through models', async () => { - const item1 = await customerCartItemRepo.create( - { - description: 'group 1', - }, - { - throughData: {id: 99}, - }, - ); - const item2 = await customerCartItemRepo.create( - { - description: 'group 2', - }, - { - throughData: {id: 98}, - }, - ); + const item1 = await customerCartItemRepo.create({description: 'group 1'}); + const item2 = await customerCartItemRepo.create({ + description: 'group 2', + }); const items = await customerCartItemRepo.find(); expect(items).have.length(2); @@ -304,34 +282,24 @@ describe('HasManyThrough relation', () => { }); it('deletes an instance, then deletes the through model', async () => { - await customerCartItemRepo.create( - { - description: 'customer 1', - }, - { - throughData: {id: 98}, - }, - ); + await customerCartItemRepo.create({ + description: 'customer 1', + }); const anotherHasManyThroughRepo = customerCartItemFactory( existingCustomerId + 1, ); - const item2 = await anotherHasManyThroughRepo.create( - { - description: 'customer 2', - }, - { - throughData: {id: 99}, - }, - ); + const item2 = await anotherHasManyThroughRepo.create({ + description: 'customer 2', + }); let items = await cartItemRepo.find(); - let links = await CustomerCartItemLinkRepo.find(); + let links = await customerCartItemLinkRepo.find(); expect(items).have.length(2); expect(links).have.length(2); await customerCartItemRepo.delete(); items = await cartItemRepo.find(); - links = await CustomerCartItemLinkRepo.find(); + links = await customerCartItemLinkRepo.find(); expect(items).have.length(1); expect(links).have.length(1); @@ -341,33 +309,23 @@ describe('HasManyThrough relation', () => { }); it('deletes through model when corresponding target gets deleted', async () => { - const item1 = await customerCartItemRepo.create( - { - description: 'customer 1', - }, - { - throughData: {id: 98}, - }, - ); + const item1 = await customerCartItemRepo.create({ + description: 'customer 1', + }); const anotherHasManyThroughRepo = customerCartItemFactory( existingCustomerId + 1, ); - const item2 = await anotherHasManyThroughRepo.create( - { - description: 'customer 2', - }, - { - throughData: {id: 99}, - }, - ); + const item2 = await anotherHasManyThroughRepo.create({ + description: 'customer 2', + }); // when order1 gets deleted, this through instance should be deleted too. - const through = await CustomerCartItemLinkRepo.create({ + const through = await customerCartItemLinkRepo.create({ id: 1, customerId: existingCustomerId + 1, itemId: item1.id, }); let items = await cartItemRepo.find(); - let links = await CustomerCartItemLinkRepo.find(); + let links = await customerCartItemLinkRepo.find(); expect(items).have.length(2); expect(links).have.length(3); @@ -375,7 +333,7 @@ describe('HasManyThrough relation', () => { await customerCartItemRepo.delete(); items = await cartItemRepo.find(); - links = await CustomerCartItemLinkRepo.find(); + links = await customerCartItemLinkRepo.find(); expect(items).have.length(1); expect(links).have.length(1); @@ -386,22 +344,12 @@ describe('HasManyThrough relation', () => { }); it('patches instances that belong to the same source model (same source fk)', async () => { - const item1 = await customerCartItemRepo.create( - { - description: 'group 1', - }, - { - throughData: {id: 99}, - }, - ); - const item2 = await customerCartItemRepo.create( - { - description: 'group 1', - }, - { - throughData: {id: 98}, - }, - ); + const item1 = await customerCartItemRepo.create({ + description: 'group 1', + }); + const item2 = await customerCartItemRepo.create({ + description: 'group 1', + }); const count = await customerCartItemRepo.patch({description: 'group 2'}); expect(count).to.match({count: 2}); @@ -413,6 +361,51 @@ describe('HasManyThrough relation', () => { ]), ); }); + + it('links a target instance to the source instance', async () => { + const item = await cartItemRepo.create({description: 'an item'}); + let targets = await customerCartItemRepo.find(); + expect(targets).to.deepEqual([]); + + await customerCartItemRepo.link(item.id); + targets = await customerCartItemRepo.find(); + expect(toJSON(targets)).to.containDeep(toJSON([item])); + const link = await customerCartItemLinkRepo.find(); + expect(toJSON(link[0])).to.containEql( + toJSON({customerId: existingCustomerId, itemId: item.id}), + ); + }); + + it('links a target instance to the source instance with specified ThroughData', async () => { + const item = await cartItemRepo.create({description: 'an item'}); + + await customerCartItemRepo.link(item.id, { + throughData: {description: 'a through'}, + }); + const targets = await customerCartItemRepo.find(); + expect(toJSON(targets)).to.containDeep(toJSON([item])); + const link = await customerCartItemLinkRepo.find(); + expect(toJSON(link[0])).to.containEql( + toJSON({ + customerId: existingCustomerId, + itemId: item.id, + description: 'a through', + }), + ); + }); + + it('unlinks a target instance from the source instance', async () => { + const item = await customerCartItemRepo.create({description: 'an item'}); + let targets = await customerCartItemRepo.find(); + expect(toJSON(targets)).to.containDeep(toJSON([item])); + + await customerCartItemRepo.unlink(item.id); + targets = await customerCartItemRepo.find(); + expect(targets).to.deepEqual([]); + // the through model should be deleted + const thoughs = await customerCartItemRepo.find(); + expect(thoughs).to.deepEqual([]); + }); //--- HELPERS ---// async function givenPersistedCustomerInstance() { @@ -443,7 +436,7 @@ describe('HasManyThrough relation', () => { }, } as HasManyDefinition, Getter.fromValue(cartItemRepo), - Getter.fromValue(CustomerCartItemLinkRepo), + Getter.fromValue(customerCartItemLinkRepo), ); customerCartItemRepo = customerCartItemFactory(existingCustomerId); @@ -537,14 +530,12 @@ class CustomerCartItemLink extends Entity { id: number; customerId: number; itemId: number; + description: string; static definition = new ModelDefinition('CustomerCartItemLink') - .addProperty('id', { - type: 'number', - id: true, - required: true, - }) + .addProperty('id', {type: 'number', id: true}) .addProperty('itemId', {type: 'number'}) - .addProperty('customerId', {type: 'number'}); + .addProperty('customerId', {type: 'number'}) + .addProperty('description', {type: 'string'}); } function givenCrudRepositories() { db = new juggler.DataSource({connector: 'memory'}); @@ -552,7 +543,7 @@ function givenCrudRepositories() { customerRepo = new DefaultCrudRepository(Customer, db); orderRepo = new DefaultCrudRepository(Order, db); cartItemRepo = new DefaultCrudRepository(CartItem, db); - CustomerCartItemLinkRepo = new DefaultCrudRepository( + customerCartItemLinkRepo = new DefaultCrudRepository( CustomerCartItemLink, db, );