diff --git a/api/src/chat/repositories/block.repository.spec.ts b/api/src/chat/repositories/block.repository.spec.ts index 2f7cf226..534c6c73 100644 --- a/api/src/chat/repositories/block.repository.spec.ts +++ b/api/src/chat/repositories/block.repository.spec.ts @@ -9,7 +9,7 @@ import { EventEmitter2 } from '@nestjs/event-emitter'; import { MongooseModule, getModelToken } from '@nestjs/mongoose'; import { Test } from '@nestjs/testing'; -import { Model } from 'mongoose'; +import { Model, Types } from 'mongoose'; import { blockFixtures, @@ -153,7 +153,7 @@ describe('BlockRepository', () => { }); }); - describe('updateBlocksInScope', () => { + describe('prepareBlocksInCategoryUpdateScope', () => { it('should update blocks within the scope based on category and ids', async () => { jest.spyOn(blockRepository, 'findOne').mockResolvedValue({ id: validIds[0], @@ -164,7 +164,10 @@ describe('BlockRepository', () => { const mockUpdateOne = jest.spyOn(blockRepository, 'updateOne'); - await blockRepository.updateBlocksInScope(validCategory, validIds); + await blockRepository.prepareBlocksInCategoryUpdateScope( + validCategory, + validIds, + ); expect(mockUpdateOne).toHaveBeenCalledWith(validIds[0], { nextBlocks: [validIds[1]], @@ -182,13 +185,16 @@ describe('BlockRepository', () => { const mockUpdateOne = jest.spyOn(blockRepository, 'updateOne'); - await blockRepository.updateBlocksInScope(validCategory, validIds); + await blockRepository.prepareBlocksInCategoryUpdateScope( + validCategory, + validIds, + ); expect(mockUpdateOne).not.toHaveBeenCalled(); }); }); - describe('updateExternalBlocks', () => { + describe('prepareBlocksOutOfCategoryUpdateScope', () => { it('should update blocks outside the scope by removing references from attachedBlock', async () => { const otherBlocks = [ { @@ -200,7 +206,10 @@ describe('BlockRepository', () => { const mockUpdateOne = jest.spyOn(blockRepository, 'updateOne'); - await blockRepository.updateExternalBlocks(otherBlocks, validIds); + await blockRepository.prepareBlocksOutOfCategoryUpdateScope( + otherBlocks, + validIds, + ); expect(mockUpdateOne).toHaveBeenCalledWith('64abc1234def567890fedcab', { attachedBlock: null, @@ -218,10 +227,12 @@ describe('BlockRepository', () => { const mockUpdateOne = jest.spyOn(blockRepository, 'updateOne'); - await blockRepository.updateExternalBlocks(otherBlocks, [validIds[0]]); + await blockRepository.prepareBlocksOutOfCategoryUpdateScope(otherBlocks, [ + validIds[0], + ]); expect(mockUpdateOne).toHaveBeenCalledWith('64abc1234def567890fedcab', { - $pull: { nextBlocks: [validIds[1]] }, + nextBlocks: [new Types.ObjectId(validIds[1])], }); }); }); @@ -236,13 +247,13 @@ describe('BlockRepository', () => { }, ] as Block[]); - const mockUpdateBlocksInScope = jest.spyOn( + const prepareBlocksInCategoryUpdateScope = jest.spyOn( blockRepository, - 'updateBlocksInScope', + 'prepareBlocksInCategoryUpdateScope', ); - const mockUpdateExternalBlocks = jest.spyOn( + const prepareBlocksOutOfCategoryUpdateScope = jest.spyOn( blockRepository, - 'updateExternalBlocks', + 'prepareBlocksOutOfCategoryUpdateScope', ); await blockRepository.preUpdateMany( @@ -252,11 +263,11 @@ describe('BlockRepository', () => { ); expect(mockFind).toHaveBeenCalled(); - expect(mockUpdateBlocksInScope).toHaveBeenCalledWith( + expect(prepareBlocksInCategoryUpdateScope).toHaveBeenCalledWith( validCategory, - validIds, + ['64abc1234def567890fedcab'], ); - expect(mockUpdateExternalBlocks).toHaveBeenCalledWith( + expect(prepareBlocksOutOfCategoryUpdateScope).toHaveBeenCalledWith( [ { id: '64abc1234def567890fedcab', @@ -264,26 +275,26 @@ describe('BlockRepository', () => { nextBlocks: [validIds[0]], }, ], - validIds, + ['64abc1234def567890fedcab'], ); }); it('should not perform updates if no category is provided', async () => { const mockFind = jest.spyOn(blockRepository, 'find'); - const mockUpdateBlocksInScope = jest.spyOn( + const prepareBlocksInCategoryUpdateScope = jest.spyOn( blockRepository, - 'updateBlocksInScope', + 'prepareBlocksInCategoryUpdateScope', ); - const mockUpdateExternalBlocks = jest.spyOn( + const prepareBlocksOutOfCategoryUpdateScope = jest.spyOn( blockRepository, - 'updateExternalBlocks', + 'prepareBlocksOutOfCategoryUpdateScope', ); - await blockRepository.preUpdateMany({} as any, {}, {}); + await blockRepository.preUpdateMany({} as any, {}, { $set: {} }); expect(mockFind).not.toHaveBeenCalled(); - expect(mockUpdateBlocksInScope).not.toHaveBeenCalled(); - expect(mockUpdateExternalBlocks).not.toHaveBeenCalled(); + expect(prepareBlocksInCategoryUpdateScope).not.toHaveBeenCalled(); + expect(prepareBlocksOutOfCategoryUpdateScope).not.toHaveBeenCalled(); }); }); }); diff --git a/api/src/chat/repositories/block.repository.ts b/api/src/chat/repositories/block.repository.ts index 8b610bed..f3efda6c 100644 --- a/api/src/chat/repositories/block.repository.ts +++ b/api/src/chat/repositories/block.repository.ts @@ -9,7 +9,7 @@ import { Injectable, Optional } from '@nestjs/common'; import { EventEmitter2 } from '@nestjs/event-emitter'; import { InjectModel } from '@nestjs/mongoose'; -import mongoose, { +import { Document, Model, Query, @@ -94,27 +94,27 @@ export class BlockRepository extends BaseRepository< | UpdateWithAggregationPipeline | UpdateQuery>, ): Promise { - const movedBlock = await this.findOne(criteria); - if (!movedBlock) { - return; - } const update: BlockUpdateDto = updates?.['$set']; + if (update?.category) { - const movedBlockId = criteria._id; + const movedBlock: Block = await this.findOne(criteria); + + if (!movedBlock) { + return; + } // Find and update blocks that reference the moved block await this.updateMany( - { nextBlocks: movedBlockId }, - { $pull: { nextBlocks: movedBlockId } }, + { nextBlocks: movedBlock.id }, + { $pull: { nextBlocks: movedBlock.id } }, ); await this.updateMany( - { attachedBlock: movedBlockId }, + { attachedBlock: movedBlock.id }, { $set: { attachedBlock: null } }, ); + this.checkDeprecatedAttachmentUrl(update); } - - this.checkDeprecatedAttachmentUrl(update); } /** @@ -136,29 +136,32 @@ export class BlockRepository extends BaseRepository< criteria: TFilterQuery, updates: UpdateQuery>, ): Promise { - if (criteria._id?.$in && updates?.$set?.category) { - const ids: string[] = criteria._id?.$in || []; - const category: string = updates.$set.category; + const categoryId: string = updates.$set.category; + if (categoryId) { + const movedBlocks = await this.find(criteria); - // Step 1: Map IDs and Category - const objIds = ids.map((id) => new mongoose.Types.ObjectId(id)); - const objCategory = new mongoose.Types.ObjectId(category); + if (movedBlocks.length) { + const ids: string[] = movedBlocks.map(({ id }) => id); - // Step 2: Find other blocks - const otherBlocks = await this.find({ - _id: { $nin: objIds }, - category: { $ne: objCategory }, - $or: [ - { attachedBlock: { $in: objIds } }, - { nextBlocks: { $in: objIds } }, - ], - }); + // Step 1: Map IDs and Category + const objIds = ids.map((id) => new Types.ObjectId(id)); + const objCategoryId = new Types.ObjectId(categoryId); - // Step 3: Update blocks in the provided scope - await this.updateBlocksInScope(category, ids); + // Step 2: Find other blocks + const otherBlocks = await this.find({ + _id: { $nin: objIds }, + category: { $ne: objCategoryId }, + $or: [ + { attachedBlock: { $in: objIds } }, + { nextBlocks: { $in: objIds } }, + ], + }); + // Step 3: Update blocks in the provided scope + await this.prepareBlocksInCategoryUpdateScope(categoryId, ids); - // Step 4: Update external blocks - await this.updateExternalBlocks(otherBlocks, ids); + // Step 4: Update external blocks + await this.prepareBlocksOutOfCategoryUpdateScope(otherBlocks, ids); + } } } @@ -170,9 +173,12 @@ export class BlockRepository extends BaseRepository< * @param ids - IDs representing the blocks to update. * @returns A promise that resolves once all updates within the scope are complete. */ - async updateBlocksInScope(category: string, ids: string[]): Promise { + async prepareBlocksInCategoryUpdateScope( + category: string, + ids: string[], + ): Promise { for (const id of ids) { - const oldState = await this.findOne(id); + const oldState: Block = await this.findOne(id); if (oldState.category !== category) { const updatedNextBlocks = oldState.nextBlocks.filter((nextBlock) => ids.includes(nextBlock), @@ -198,7 +204,7 @@ export class BlockRepository extends BaseRepository< * @param ids - An array of the Ids to disassociate. * @returns A promise that resolves once all external block updates are complete. */ - async updateExternalBlocks( + async prepareBlocksOutOfCategoryUpdateScope( otherBlocks: Block[], ids: string[], ): Promise { @@ -207,14 +213,12 @@ export class BlockRepository extends BaseRepository< await this.updateOne(block.id, { attachedBlock: null }); } - const updatedNextBlocks = block.nextBlocks.filter( - (nextBlock) => !ids.includes(nextBlock), - ); + const nextBlocks = block.nextBlocks + .filter((nextBlock) => !ids.includes(nextBlock)) + .map((id) => new Types.ObjectId(id)); - if (updatedNextBlocks.length > 0) { - await this.updateOne(block.id, { - $pull: { nextBlocks: updatedNextBlocks }, - }); + if (nextBlocks.length > 0) { + await this.updateOne(block.id, { nextBlocks }); } } } diff --git a/api/src/utils/generics/base-repository.spec.ts b/api/src/utils/generics/base-repository.spec.ts index 3dd28ad6..3f729bd4 100644 --- a/api/src/utils/generics/base-repository.spec.ts +++ b/api/src/utils/generics/base-repository.spec.ts @@ -8,7 +8,7 @@ import { getModelToken } from '@nestjs/mongoose'; import { Test, TestingModule } from '@nestjs/testing'; -import mongoose, { Model } from 'mongoose'; +import { Model, Types } from 'mongoose'; import { DummyRepository } from '@/utils/test/dummy/repositories/dummy.repository'; import { closeInMongodConnection } from '@/utils/test/test'; @@ -150,7 +150,7 @@ describe('BaseRepository', () => { expect(spyBeforeUpdate).toHaveBeenCalledWith( expect.objectContaining({ $useProjection: true }), { - _id: new mongoose.Types.ObjectId(created.id), + _id: new Types.ObjectId(created.id), }, expect.objectContaining({ $set: expect.objectContaining(mockUpdate) }), ); @@ -202,7 +202,7 @@ describe('BaseRepository', () => { expect(spyBeforeDelete).toHaveBeenCalledWith( expect.objectContaining({ $useProjection: true }), { - _id: new mongoose.Types.ObjectId(createdId), + _id: new Types.ObjectId(createdId), }, ); expect(spyAfterDelete).toHaveBeenCalledWith(