Skip to content

Commit

Permalink
Merge pull request #277 from internxt/feat/add-items-to-trash-by-uuid
Browse files Browse the repository at this point in the history
[PB-1740]: feat/add items to trash by uuid
  • Loading branch information
sg-gs authored Mar 22, 2024
2 parents 0436912 + e291d45 commit 223cc83
Show file tree
Hide file tree
Showing 10 changed files with 421 additions and 71 deletions.
35 changes: 34 additions & 1 deletion src/modules/file/file.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,14 @@ export interface FileRepository {
): Promise<void>;
getFilesWhoseFolderIdDoesNotExist(userId: File['userId']): Promise<number>;
getFilesCountWhere(where: Partial<File>): Promise<number>;
updateFilesStatusToTrashed(
user: User,
fileIds: File['fileId'][],
): Promise<void>;
updateFilesStatusToTrashedByUuid(
user: User,
fileUuids: File['uuid'][],
): Promise<void>;
}

@Injectable()
Expand Down Expand Up @@ -409,7 +417,32 @@ export class SequelizeFileRepository implements FileRepository {
[Op.in]: fileIds,
},
status: {
[Op.not]: FileStatus.DELETED,
[Op.eq]: FileStatus.EXISTS,
},
},
},
);
}

async updateFilesStatusToTrashedByUuid(
user: User,
fileUuids: File['uuid'][],
): Promise<void> {
await this.fileModel.update(
{
deleted: true,
deletedAt: new Date(),
status: FileStatus.TRASHED,
updatedAt: new Date(),
},
{
where: {
userId: user.id,
uuid: {
[Op.in]: fileUuids,
},
status: {
[Op.eq]: FileStatus.EXISTS,
},
},
},
Expand Down
85 changes: 34 additions & 51 deletions src/modules/file/file.usecase.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { FileUseCases } from './file.usecase';
import { SequelizeFileRepository, FileRepository } from './file.repository';
import {
ForbiddenException,
NotFoundException,
UnprocessableEntityException,
} from '@nestjs/common';
import { File, FileAttributes, FileStatus } from './file.domain';
Expand Down Expand Up @@ -89,61 +88,45 @@ describe('FileUseCases', () => {
});

describe('move file to trash', () => {
it.skip('calls moveFilesToTrash and return file', async () => {
const mockFile = File.build({
id: 1,
fileId: '',
name: '',
type: '',
size: null,
bucket: '',
folderId: 4,
encryptVersion: '',
deleted: false,
deletedAt: new Date(),
userId: 1,
modificationTime: new Date(),
createdAt: new Date(),
updatedAt: new Date(),
uuid: '',
folderUuid: '',
removed: false,
removedAt: undefined,
plainName: '',
status: FileStatus.EXISTS,
});
jest
.spyOn(fileRepository, 'updateByFieldIdAndUserId')
.mockResolvedValue(mockFile);
const result = await service.moveFilesToTrash(userMocked, [fileId]);
expect(result).toEqual(mockFile);
});

it.skip('throws an error if the file is not found', async () => {
jest
.spyOn(fileRepository, 'updateByFieldIdAndUserId')
.mockRejectedValue(new NotFoundException());
expect(service.moveFilesToTrash(userMocked, [fileId])).rejects.toThrow(
NotFoundException,
);
const mockFile = File.build({
id: 1,
fileId: '',
name: '',
type: '',
size: null,
bucket: '',
folderId: 4,
encryptVersion: '',
deleted: false,
deletedAt: new Date(),
userId: 1,
modificationTime: new Date(),
createdAt: new Date(),
updatedAt: new Date(),
uuid: '723274e5-ca2a-4e61-bf17-d9fba3b8d430',
folderUuid: '',
removed: false,
removedAt: undefined,
plainName: '',
status: FileStatus.EXISTS,
});
});

describe('move multiple files to trash', () => {
it.skip('calls moveFilesToTrash', async () => {
it('When you try to trash files with id and uuid, then functions are called with respective values', async () => {
const fileIds = [fileId];
jest
.spyOn(fileRepository, 'updateManyByFieldIdAndUserId')
.mockImplementation(() => {
return new Promise((resolve) => {
resolve();
});
});
const result = await service.moveFilesToTrash(userMocked, fileIds);
expect(result).toEqual(undefined);
expect(fileRepository.updateManyByFieldIdAndUserId).toHaveBeenCalledTimes(
const fileUuids = [mockFile.uuid];
jest.spyOn(fileRepository, 'updateFilesStatusToTrashed');
jest.spyOn(fileRepository, 'updateFilesStatusToTrashedByUuid');
await service.moveFilesToTrash(userMocked, fileIds, fileUuids);
expect(fileRepository.updateFilesStatusToTrashed).toHaveBeenCalledTimes(
1,
);
expect(fileRepository.updateFilesStatusToTrashed).toHaveBeenCalledWith(
userMocked,
fileIds,
);
expect(
fileRepository.updateFilesStatusToTrashedByUuid,
).toHaveBeenCalledWith(userMocked, fileUuids);
});
});

Expand Down
8 changes: 6 additions & 2 deletions src/modules/file/file.usecase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,8 +248,12 @@ export class FileUseCases {
moveFilesToTrash(
user: User,
fileIds: FileAttributes['fileId'][],
): Promise<void> {
return this.fileRepository.updateFilesStatusToTrashed(user, fileIds);
fileUuids: FileAttributes['uuid'][] = [],
): Promise<[void, void]> {
return Promise.all([
this.fileRepository.updateFilesStatusToTrashed(user, fileIds),
this.fileRepository.updateFilesStatusToTrashedByUuid(user, fileUuids),
]);
}

async getEncryptionKeyFileFromShare(
Expand Down
16 changes: 16 additions & 0 deletions src/modules/folder/folder.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ export interface FolderRepository {
deleteById(folderId: FolderAttributes['id']): Promise<void>;
clearOrphansFolders(userId: FolderAttributes['userId']): Promise<number>;
calculateFolderSize(folderUuid: string): Promise<number>;
findUserFoldersByUuid(
user: User,
uuids: FolderAttributes['uuid'][],
): Promise<Folder[]>;
}

@Injectable()
Expand Down Expand Up @@ -143,6 +147,18 @@ export class SequelizeFolderRepository implements FolderRepository {
return folders.map((folder) => this.toDomain(folder));
}

async findUserFoldersByUuid(user: User, uuids: FolderAttributes['uuid'][]) {
const folders = await this.folderModel.findAll({
where: {
uuid: { [Op.in]: uuids },
userId: user.id,
deleted: false,
removed: false,
},
});
return folders.map((folder) => this.toDomain(folder));
}

async findAllByParentIdCursor(
where: Partial<FolderAttributes>,
limit: number,
Expand Down
95 changes: 94 additions & 1 deletion src/modules/folder/folder.usecase.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@ import { BridgeModule } from '../../externals/bridge/bridge.module';
import { CryptoModule } from '../../externals/crypto/crypto.module';
import { CryptoService } from '../../externals/crypto/crypto.service';
import { User } from '../user/user.domain';
import { newFolder } from '../../../test/fixtures';
import { newFolder, newUser } from '../../../test/fixtures';
import { CalculateFolderSizeTimeoutException } from './exception/calculate-folder-size-timeout.exception';

const folderId = 4;
const user = newUser();

describe('FolderUseCases', () => {
let service: FolderUseCases;
let folderRepository: FolderRepository;
Expand Down Expand Up @@ -107,6 +109,97 @@ describe('FolderUseCases', () => {
});
});

describe('move multiple folders to trash', () => {
const rootFolderBucket = 'bucketRoot';
const mockFolder = Folder.build({
id: 1,
parentId: null,
parentUuid: null,
name: 'name',
bucket: rootFolderBucket,
userId: 1,
encryptVersion: '03-aes',
deleted: true,
deletedAt: new Date(),
createdAt: new Date(),
updatedAt: new Date(),
uuid: '2545feaf-4d6b-40d8-9bf8-550285268bd3',
plainName: '',
removed: false,
removedAt: null,
});

it('When uuid and id are passed and there is a backup and drive folder, then backups and drive folders should be updated', async () => {
const mockBackupFolder = Folder.build({
id: 1,
parentId: null,
parentUuid: null,
name: 'name',
bucket: 'bucketIdforBackup',
userId: 1,
encryptVersion: '03-aes',
deleted: true,
deletedAt: new Date(),
createdAt: new Date(),
updatedAt: new Date(),
uuid: '',
plainName: '',
removed: false,
removedAt: null,
});

jest
.spyOn(service, 'getFoldersByIds')
.mockResolvedValue([mockBackupFolder]);
jest
.spyOn(folderRepository, 'findUserFoldersByUuid')
.mockResolvedValue([mockFolder]);
jest.spyOn(service, 'getFolder').mockResolvedValue({
bucket: rootFolderBucket,
} as Folder);
jest.spyOn(folderRepository, 'updateManyByFolderId');

await service.moveFoldersToTrash(
user,
[mockBackupFolder.id],
[mockFolder.uuid],
);

expect(folderRepository.updateManyByFolderId).toHaveBeenCalledTimes(2);
expect(folderRepository.updateManyByFolderId).toHaveBeenCalledWith(
[mockFolder.id],
{
deleted: true,
deletedAt: expect.any(Date),
},
);
expect(folderRepository.updateManyByFolderId).toHaveBeenCalledWith(
[mockBackupFolder.id],
{
deleted: true,
deletedAt: expect.any(Date),
removed: true,
removedAt: expect.any(Date),
},
);
});

it('When only ids are passed, then only folders by id should be searched', async () => {
jest.spyOn(service, 'getFoldersByIds').mockResolvedValue([mockFolder]);
jest.spyOn(service, 'getFolder').mockResolvedValue({
bucket: rootFolderBucket,
} as Folder);
jest.spyOn(folderRepository, 'findUserFoldersByUuid');
jest.spyOn(service, 'getFoldersByIds');

await service.moveFoldersToTrash(user, [mockFolder.id]);
expect(folderRepository.findUserFoldersByUuid).not.toHaveBeenCalled();
expect(service.getFoldersByIds).toHaveBeenCalledWith(user, [
mockFolder.id,
]);
});
});

describe('get folder use case', () => {
it('calls getFolder and return folder', async () => {
const mockFolder = Folder.build({
Expand Down
8 changes: 7 additions & 1 deletion src/modules/folder/folder.usecase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,12 +293,18 @@ export class FolderUseCases {
async moveFoldersToTrash(
user: User,
folderIds: FolderAttributes['id'][],
folderUuids: FolderAttributes['uuid'][] = [],
): Promise<void> {
const [folders, driveRootFolder] = await Promise.all([
const [foldersById, driveRootFolder, foldersByUuid] = await Promise.all([
this.getFoldersByIds(user, folderIds),
this.getFolder(user.rootFolderId),
folderUuids.length > 0
? this.folderRepository.findUserFoldersByUuid(user, folderUuids)
: Promise.resolve<Folder[]>([]),
]);

const folders = foldersById.concat(foldersByUuid);

const backups = folders.filter((f) => f.isBackup(driveRootFolder));
const driveFolders = folders.filter((f) => !f.isBackup(driveRootFolder));

Expand Down
65 changes: 65 additions & 0 deletions src/modules/trash/dto/controllers/move-items-to-trash.dto.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { validate } from 'class-validator';
import { plainToInstance } from 'class-transformer';
import {
ItemToTrash,
ItemType,
MoveItemsToTrashDto,
} from './move-items-to-trash.dto';

describe('MoveItemsToTrashDto', () => {
it('When valid data is passed, then no errors should be returned', async () => {
const dto = plainToInstance(MoveItemsToTrashDto, {
items: [
{ id: '1', type: ItemType.FILE },
{ uuid: '5bf9dca1-fd68-4864-9a16-ef36b77d063b', type: ItemType.FOLDER },
],
});

const errors = await validate(dto);
expect(errors.length).toBe(0);
});

it('When items array exceeds max size, then should fail', async () => {
const items = Array.from({ length: 51 }, (_, i) => ({
id: `${i + 1}`,
type: ItemType.FILE,
}));
const dto = plainToInstance(MoveItemsToTrashDto, { items });

const errors = await validate(dto);
expect(errors.length).toBeGreaterThan(0);
expect(errors[0].constraints).toBeDefined();
});

describe('ItemToTrash', () => {
it('When both id and uuid are provided in one item, then should fail', async () => {
const item = plainToInstance(ItemToTrash, {
id: '1',
uuid: '5bf9dca1-fd68-4864-9a16-ef36b77d063b',
type: ItemType.FILE,
});
const errors = await validate(item);
expect(errors.length).toBeGreaterThan(0);
});

it('When neither id nor uuid are provided in one item, then should fail', async () => {
const item = plainToInstance(ItemToTrash, { type: ItemType.FILE });
const errors = await validate(item);
expect(errors.length).toBeGreaterThan(0);
});

it('when either id or uuid are provided, then should validate successfuly ', async () => {
const onlyIdErrors = await validate(
plainToInstance(ItemToTrash, { id: '1', type: ItemType.FILE }),
);
const onlyUuidErrors = await validate(
plainToInstance(ItemToTrash, {
uuid: '5bf9dca1-fd68-4864-9a16-ef36b77d063b',
type: ItemType.FILE,
}),
);
expect(onlyIdErrors.length).toBe(0);
expect(onlyUuidErrors.length).toBe(0);
});
});
});
Loading

0 comments on commit 223cc83

Please sign in to comment.