From 96a61110472897ea8348c71bbc7b777a74bcf03c Mon Sep 17 00:00:00 2001 From: Andres Pinto Date: Thu, 12 Sep 2024 18:46:18 -0400 Subject: [PATCH] feat: look for existenet files with array --- src/modules/file/file.repository.spec.ts | 39 +++++++++-- src/modules/file/file.repository.ts | 58 +++++++++++++-- src/modules/file/file.usecase.ts | 70 ++++++++++++++++--- .../dto/files-existence-in-folder.dto.spec.ts | 43 ++++++------ .../dto/files-existence-in-folder.dto.ts | 38 ++++++---- src/modules/folder/folder.controller.spec.ts | 18 ++--- src/modules/folder/folder.controller.ts | 10 ++- 7 files changed, 205 insertions(+), 71 deletions(-) diff --git a/src/modules/file/file.repository.spec.ts b/src/modules/file/file.repository.spec.ts index c2a7aade3..f1129dfed 100644 --- a/src/modules/file/file.repository.spec.ts +++ b/src/modules/file/file.repository.spec.ts @@ -87,19 +87,46 @@ describe('FileRepository', () => { describe('findFileByFolderUuid', () => { const folderUuid = v4(); - it('When a file is searched, then it should handle the dynamic input', async () => { - const searchCriteria = { plainName: ['Report'], type: 'pdf' }; + it('When multiple files are searched, it should handle an array of search filters', async () => { + const searchCriteria = [ + { plainName: 'Report', type: 'pdf' }, + { plainName: 'Summary', type: 'doc' }, + ]; await repository.findFileByFolderUuid(folderUuid, searchCriteria); expect(fileModel.findAll).toHaveBeenCalledWith({ where: expect.objectContaining({ folderUuid, - plainName: { - [Op.in]: searchCriteria.plainName, - }, - type: searchCriteria.type, status: FileStatus.EXISTS, + [Op.or]: [ + { + plainName: 'Report', + type: 'pdf', + }, + { + plainName: 'Summary', + type: 'doc', + }, + ], + }), + }); + }); + + it('When a file is searched with only plainName, it should handle the missing type', async () => { + const searchCriteria = [{ plainName: 'Report' }]; + + await repository.findFileByFolderUuid(folderUuid, searchCriteria); + + expect(fileModel.findAll).toHaveBeenCalledWith({ + where: expect.objectContaining({ + folderUuid, + status: FileStatus.EXISTS, + [Op.or]: [ + { + plainName: 'Report', + }, + ], }), }); }); diff --git a/src/modules/file/file.repository.ts b/src/modules/file/file.repository.ts index f2697881f..ea1163ac4 100644 --- a/src/modules/file/file.repository.ts +++ b/src/modules/file/file.repository.ts @@ -45,7 +45,7 @@ export interface FileRepository { ): Promise; findFileByFolderUuid( folderUuid: Folder['uuid'], - searchBy: { plainName: File['plainName'][]; type?: File['type'] }, + searchBy: { plainName: File['plainName']; type?: File['type'] }[], ): Promise; findByNameAndFolderUuid( name: FileAttributes['name'], @@ -73,6 +73,16 @@ export interface FileRepository { userId: FileAttributes['userId'], update: Partial, ): Promise; + findFilesWithPagination( + folderUuid: Folder['uuid'], + limit: number, + page: number, + ): Promise<{ + files: File[]; + totalPages: number; + currentPage: number; + nextPage: number | null; + }>; getFilesWhoseFolderIdDoesNotExist(userId: File['userId']): Promise; getFilesCountWhere(where: Partial): Promise; updateFilesStatusToTrashed( @@ -538,16 +548,18 @@ export class SequelizeFileRepository implements FileRepository { async findFileByFolderUuid( folderUuid: Folder['uuid'], - searchBy: { plainName: File['plainName'][]; type?: File['type'] }, + searchFilter: { plainName: File['plainName']; type?: File['type'] }[], ): Promise { const where: WhereOptions = { folderUuid, - ...(searchBy?.type ? { type: searchBy.type } : null), status: FileStatus.EXISTS, }; - if (searchBy?.plainName?.length) { - where.plainName = { [Op.in]: searchBy.plainName }; + if (searchFilter.length) { + where[Op.or] = searchFilter.map((criteria) => ({ + plainName: criteria.plainName, + ...(criteria.type ? { type: criteria.type } : {}), + })); } const files = await this.fileModel.findAll({ @@ -557,6 +569,42 @@ export class SequelizeFileRepository implements FileRepository { return files.map(this.toDomain.bind(this)); } + async findFilesWithPagination( + folderUuid: Folder['uuid'], + limit: number, + page: number, + ): Promise<{ + files: File[]; + totalPages: number; + currentPage: number; + nextPage: number | null; + }> { + const offset = (page - 1) * limit; + + const result = await this.fileModel.findAndCountAll({ + where: { + folderUuid, + status: FileStatus.EXISTS, + }, + limit, + offset, + order: [['id', 'ASC']], + }); + + const { count, rows } = result; + + const totalPages = Math.ceil(count / limit); + + const nextPage = page < totalPages ? page + 1 : null; + + return { + files: rows.map(this.toDomain.bind(this)), + totalPages, + currentPage: page, + nextPage, + }; + } + async updateByFieldIdAndUserId( fileId: FileAttributes['fileId'], userId: FileAttributes['userId'], diff --git a/src/modules/file/file.usecase.ts b/src/modules/file/file.usecase.ts index dee5d0f11..1b880c039 100644 --- a/src/modules/file/file.usecase.ts +++ b/src/modules/file/file.usecase.ts @@ -145,16 +145,70 @@ export class FileUseCases { } async searchFilesInFolder( + folder: Folder, + searchFilter: { plainName: File['plainName']; type?: File['type'] }[], + ): Promise { + return this.fileRepository.findFileByFolderUuid(folder.uuid, searchFilter); + } + + async findFilesWithPagination( + folderUuid: Folder['uuid'], + limit: number, + page: number, + ) { + return this.fileRepository.findFilesWithPagination(folderUuid, limit, page); + } + + async checkMultipleFilesExistence( folderUuid: Folder['uuid'], - { - plainNames, - type, - }: { plainNames: File['plainName'][]; type?: File['type'] }, + dtoFiles: { plainName: string; type?: string }[], ): Promise { - return this.fileRepository.findFileByFolderUuid(folderUuid, { - plainName: plainNames, - type, - }); + const limit = 1000; + let page = 1; + const allExistentFiles: File[] = []; + let nextPage: number | null = null; + + let remainingFiles = [...dtoFiles]; + + do { + const { files, nextPage: newNextPage } = + await this.findFilesWithPagination(folderUuid, limit, page); + + const matchingFiles = files.filter((file) => + remainingFiles.some( + (dtoFile) => + file.plainName === dtoFile.plainName && + (!dtoFile.type || file.type === dtoFile.type), + ), + ); + + allExistentFiles.push(...matchingFiles); + + remainingFiles = remainingFiles.filter( + (dtoFile) => + !matchingFiles.some( + (file) => + file.plainName === dtoFile.plainName && + (!dtoFile.type || file.type === dtoFile.type), + ), + ); + + if (remainingFiles.length === 0) { + break; + } + + nextPage = newNextPage; + page++; + } while (nextPage); + + return allExistentFiles; + } + + async getFilesInFolder( + folder: Folder, + searchFilter: { plainName: File['plainName']; type?: File['type'] }[], + ): Promise { + return this.fileRepository.findFileByFolderUuid(folder.uuid, searchFilter); } async updateFileMetaData( diff --git a/src/modules/folder/dto/files-existence-in-folder.dto.spec.ts b/src/modules/folder/dto/files-existence-in-folder.dto.spec.ts index 38f5b87f2..dc1143bf3 100644 --- a/src/modules/folder/dto/files-existence-in-folder.dto.spec.ts +++ b/src/modules/folder/dto/files-existence-in-folder.dto.spec.ts @@ -5,69 +5,66 @@ import { CheckFileExistenceInFolderDto } from './files-existence-in-folder.dto'; describe('CheckFileExistenceInFolderDto', () => { it('When valid data is passed, then no errors should be returned', async () => { const dto = plainToInstance(CheckFileExistenceInFolderDto, { - plainName: ['file1', 'file2'], - type: 'txt', + files: [ + { plainName: 'file1', type: 'txt' }, + { plainName: 'file2', type: 'pdf' }, + ], }); const errors = await validate(dto); expect(errors.length).toBe(0); }); - it('When a single string is passed for plainName, then it should be transformed into an array and validate successfully', async () => { + it('When a single object is passed in files array, then it should validate successfully', async () => { const dto = plainToInstance(CheckFileExistenceInFolderDto, { - plainName: 'file1', - type: 'txt', + files: [{ plainName: 'file1', type: 'txt' }], }); const errors = await validate(dto); expect(errors.length).toBe(0); - expect(dto.plainName).toEqual(['file1']); + expect(dto.files).toEqual([{ plainName: 'file1', type: 'txt' }]); }); - it('When plainName array exceeds max size, then it should fail', async () => { - const plainName = Array.from({ length: 51 }, (_, i) => `file${i + 1}`); - const dto = plainToInstance(CheckFileExistenceInFolderDto, { - plainName, + it('When files array exceeds max size, then it should fail', async () => { + const files = Array.from({ length: 1991 }, (_, i) => ({ + plainName: `file${i + 1}`, type: 'txt', - }); + })); + const dto = plainToInstance(CheckFileExistenceInFolderDto, { files }); const errors = await validate(dto); expect(errors.length).toBeGreaterThan(0); expect(errors[0].constraints).toBeDefined(); }); - it('When plainName contains non-string values, then it should fail', async () => { + it('When files contain non-string plainName values, then it should fail', async () => { const dto = plainToInstance(CheckFileExistenceInFolderDto, { - plainName: [1, 2, 3], - type: 'txt', + files: [{ plainName: 123, type: 'txt' }], }); const errors = await validate(dto); expect(errors.length).toBeGreaterThan(0); }); - it('When plainName is not provided, then it should fail', async () => { - const dto = plainToInstance(CheckFileExistenceInFolderDto, { - type: 'txt', - }); + it('When files array is not provided, then it should fail', async () => { + const dto = plainToInstance(CheckFileExistenceInFolderDto, {}); const errors = await validate(dto); expect(errors.length).toBeGreaterThan(0); }); - it('When plainName is an empty array, then it should validate successfully', async () => { + it('When files is empty, then it should fail', async () => { const dto = plainToInstance(CheckFileExistenceInFolderDto, { - plainName: [], - type: 'txt', + files: null, }); const errors = await validate(dto); - expect(errors.length).toBe(0); + expect(errors.length).toBeGreaterThan(0); }); it('When type is not provided, then it should validate successfully', async () => { const dto = plainToInstance(CheckFileExistenceInFolderDto, { - plainName: ['file1', 'file2'], + files: [{ plainName: 'file1' }], }); const errors = await validate(dto); diff --git a/src/modules/folder/dto/files-existence-in-folder.dto.ts b/src/modules/folder/dto/files-existence-in-folder.dto.ts index 0bfaccff8..4dff8ebdf 100644 --- a/src/modules/folder/dto/files-existence-in-folder.dto.ts +++ b/src/modules/folder/dto/files-existence-in-folder.dto.ts @@ -1,9 +1,17 @@ -import { Transform } from 'class-transformer'; -import { IsString, ArrayMaxSize, IsArray, IsOptional } from 'class-validator'; +import { Type } from 'class-transformer'; +import { + IsString, + ArrayMaxSize, + IsOptional, + IsNotEmpty, + ValidateNested, + arrayMinSize, + ArrayMinSize, +} from 'class-validator'; import { FileAttributes } from '../../file/file.domain'; import { ApiProperty } from '@nestjs/swagger'; -export class CheckFileExistenceInFolderDto { +export class FilesNameAndType { @ApiProperty({ description: 'Type of file', example: 'pdf', @@ -17,16 +25,18 @@ export class CheckFileExistenceInFolderDto { description: 'Plain name of file', example: 'example', }) - @IsArray() - @ArrayMaxSize(50, { - message: 'Names parameter cannot contain more than 50 names', - }) - @IsString({ each: true }) - @Transform(({ value }) => { - if (typeof value === 'string') { - return [value]; - } - return value; + @IsString() + plainName: FileAttributes['plainName']; +} + +export class CheckFileExistenceInFolderDto { + @IsNotEmpty() + @ApiProperty({ + description: 'Array of files with names and types', }) - plainName: FileAttributes['plainName'][]; + @ArrayMinSize(1) + @ArrayMaxSize(1000) + @ValidateNested() + @Type(() => FilesNameAndType) + files: FilesNameAndType[]; } diff --git a/src/modules/folder/folder.controller.spec.ts b/src/modules/folder/folder.controller.spec.ts index cff1d3983..9c6b463e2 100644 --- a/src/modules/folder/folder.controller.spec.ts +++ b/src/modules/folder/folder.controller.spec.ts @@ -416,8 +416,9 @@ describe('FolderController', () => { describe('checkFilesExistenceInFolder', () => { const user = newUser(); const folderUuid = v4(); - const plainName = ['Report.pdf', 'Image.png']; + const plainName = 'Report.pdf'; const type = 'document'; + const query = { files: [{ plainName, type }] }; it('When files exist matching the criteria, then it should return the files', async () => { const parentFolder = newFolder({ attributes: { uuid: folderUuid } }); @@ -430,13 +431,13 @@ describe('FolderController', () => { .spyOn(folderUseCases, 'getFolderByUuidAndUser') .mockResolvedValue(parentFolder); jest - .spyOn(fileUseCases, 'searchFilesInFolder') + .spyOn(fileUseCases, 'checkMultipleFilesExistence') .mockResolvedValue(mockFiles); const result = await folderController.checkFilesExistenceInFolder( user, folderUuid, - { plainName, type }, + query, ); expect(result).toEqual({ existentFiles: mockFiles }); @@ -448,12 +449,14 @@ describe('FolderController', () => { jest .spyOn(folderUseCases, 'getFolderByUuidAndUser') .mockResolvedValue(parentFolder); - jest.spyOn(fileUseCases, 'searchFilesInFolder').mockResolvedValue([]); + jest + .spyOn(fileUseCases, 'checkMultipleFilesExistence') + .mockResolvedValue([]); const result = await folderController.checkFilesExistenceInFolder( user, folderUuid, - { plainName, type }, + query, ); expect(result).toEqual({ existentFiles: [] }); @@ -465,10 +468,7 @@ describe('FolderController', () => { .mockResolvedValue(null); await expect( - folderController.checkFilesExistenceInFolder(user, folderUuid, { - plainName, - type, - }), + folderController.checkFilesExistenceInFolder(user, folderUuid, query), ).rejects.toThrow(InvalidParentFolderException); }); }); diff --git a/src/modules/folder/folder.controller.ts b/src/modules/folder/folder.controller.ts index 22682157c..169790774 100644 --- a/src/modules/folder/folder.controller.ts +++ b/src/modules/folder/folder.controller.ts @@ -383,7 +383,7 @@ export class FolderController { return { existentFolders: folders }; } - @Get('/content/:uuid/files/existence') + @Post('/content/:uuid/files/existence') @GetDataFromRequest([ { sourceKey: 'params', @@ -399,10 +399,8 @@ export class FolderController { async checkFilesExistenceInFolder( @UserDecorator() user: User, @Param('uuid') folderUuid: string, - @Query() query: CheckFileExistenceInFolderDto, + @Body() query: CheckFileExistenceInFolderDto, ) { - const { plainName, type } = query; - const parentFolder = await this.folderUseCases.getFolderByUuidAndUser( folderUuid, user, @@ -412,9 +410,9 @@ export class FolderController { throw new InvalidParentFolderException('Parent folder not valid!'); } - const files = await this.fileUseCases.searchFilesInFolder( + const files = await this.fileUseCases.checkMultipleFilesExistence( parentFolder.uuid, - { plainNames: plainName, type }, + query.files, ); return { existentFiles: files };