Skip to content

Commit

Permalink
feat: look for existenet files with array
Browse files Browse the repository at this point in the history
  • Loading branch information
apsantiso committed Sep 12, 2024
1 parent 5e08c93 commit 96a6111
Show file tree
Hide file tree
Showing 7 changed files with 205 additions and 71 deletions.
39 changes: 33 additions & 6 deletions src/modules/file/file.repository.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
},
],
}),
});
});
Expand Down
58 changes: 53 additions & 5 deletions src/modules/file/file.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export interface FileRepository {
): Promise<File | null>;
findFileByFolderUuid(
folderUuid: Folder['uuid'],
searchBy: { plainName: File['plainName'][]; type?: File['type'] },
searchBy: { plainName: File['plainName']; type?: File['type'] }[],
): Promise<File[]>;
findByNameAndFolderUuid(
name: FileAttributes['name'],
Expand Down Expand Up @@ -73,6 +73,16 @@ export interface FileRepository {
userId: FileAttributes['userId'],
update: Partial<File>,
): Promise<void>;
findFilesWithPagination(
folderUuid: Folder['uuid'],
limit: number,
page: number,
): Promise<{
files: File[];
totalPages: number;
currentPage: number;
nextPage: number | null;
}>;
getFilesWhoseFolderIdDoesNotExist(userId: File['userId']): Promise<number>;
getFilesCountWhere(where: Partial<File>): Promise<number>;
updateFilesStatusToTrashed(
Expand Down Expand Up @@ -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<File[]> {
const where: WhereOptions<File> = {
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({
Expand All @@ -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'],
Expand Down
70 changes: 62 additions & 8 deletions src/modules/file/file.usecase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,16 +145,70 @@ export class FileUseCases {
}

async searchFilesInFolder(
folder: Folder,
searchFilter: { plainName: File['plainName']; type?: File['type'] }[],
): Promise<File[]> {
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<File[]> {
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<File[]> {
return this.fileRepository.findFileByFolderUuid(folder.uuid, searchFilter);
}

async updateFileMetaData(
Expand Down
43 changes: 20 additions & 23 deletions src/modules/folder/dto/files-existence-in-folder.dto.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
38 changes: 24 additions & 14 deletions src/modules/folder/dto/files-existence-in-folder.dto.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -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[];
}
Loading

0 comments on commit 96a6111

Please sign in to comment.