diff --git a/.gitignore b/.gitignore index cf61e9f1d..89cd5e63c 100644 --- a/.gitignore +++ b/.gitignore @@ -36,5 +36,6 @@ lerna-debug.log* .npmrc +.env .env.development .env.production \ No newline at end of file diff --git a/src/externals/notifications/events/event.ts b/src/externals/notifications/events/event.ts index 314ef2295..fba46e63e 100644 --- a/src/externals/notifications/events/event.ts +++ b/src/externals/notifications/events/event.ts @@ -1,6 +1,9 @@ export class Event { private createdAt: Date; - constructor(public name: string, public payload: Record) { + constructor( + public name: string, + public payload: Record, + ) { this.createdAt = new Date(); } } diff --git a/src/modules/file/file.controller.ts b/src/modules/file/file.controller.ts index 94ff7e76d..36af5170b 100644 --- a/src/modules/file/file.controller.ts +++ b/src/modules/file/file.controller.ts @@ -112,7 +112,7 @@ export class FileController { @UserDecorator() user: User, @Query('limit') limit: number, @Query('offset') offset: number, - @Query('status') status: typeof filesStatuses[number], + @Query('status') status: (typeof filesStatuses)[number], @Query('sort') sort?: string, @Query('order') order?: 'ASC' | 'DESC', @Query('updatedAt') updatedAt?: string, diff --git a/src/modules/folder/folder.attributes.ts b/src/modules/folder/folder.attributes.ts index fac658679..55cdd2cbd 100644 --- a/src/modules/folder/folder.attributes.ts +++ b/src/modules/folder/folder.attributes.ts @@ -3,6 +3,7 @@ import { Sharing } from '../sharing/sharing.domain'; export interface FolderAttributes { id: number; parentId: number; + parentUuid?: string; parent?: any; name: string; bucket: string; diff --git a/src/modules/folder/folder.controller.spec.ts b/src/modules/folder/folder.controller.spec.ts index 7239801f1..9ad8c70ef 100644 --- a/src/modules/folder/folder.controller.spec.ts +++ b/src/modules/folder/folder.controller.spec.ts @@ -1,17 +1,55 @@ import { createMock } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; -import { newFolder } from '../../../test/fixtures'; +import { BadRequestException } from '@nestjs/common'; +import { newFile, newFolder } from '../../../test/fixtures'; import { FileUseCases } from '../file/file.usecase'; -import { FolderController } from './folder.controller'; +import { + BadRequestInvalidOffsetException, + BadRequestOutOfRangeLimitException, + FolderController, +} from './folder.controller'; import { Folder } from './folder.domain'; import { FolderUseCases } from './folder.usecase'; import { CalculateFolderSizeTimeoutException } from './exception/calculate-folder-size-timeout.exception'; +import { User } from '../user/user.domain'; +import { FileStatus } from '../file/file.domain'; describe('FolderController', () => { let folderController: FolderController; let folderUseCases: FolderUseCases; + let fileUseCases: FileUseCases; let folder: Folder; + const userMocked = User.build({ + id: 1, + userId: 'userId', + name: 'User Owner', + lastname: 'Lastname', + email: 'fake@internxt.com', + username: 'fake', + bridgeUser: null, + rootFolderId: 1, + errorLoginCount: 0, + isEmailActivitySended: 1, + referralCode: null, + referrer: null, + syncDate: new Date(), + uuid: 'uuid', + lastResend: new Date(), + credit: null, + welcomePack: true, + registerCompleted: true, + backupsBucket: 'bucket', + sharedWorkspace: true, + avatar: 'avatar', + password: '', + mnemonic: '', + hKey: undefined, + secret_2FA: '', + tempKey: '', + lastPasswordChangedAt: new Date(), + }); + beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ controllers: [FolderController], @@ -23,6 +61,7 @@ describe('FolderController', () => { folderController = module.get(FolderController); folderUseCases = module.get(FolderUseCases); + fileUseCases = module.get(FileUseCases); folder = newFolder(); }); @@ -47,4 +86,151 @@ describe('FolderController', () => { ); }); }); + + describe('get folder content', () => { + it('When get folder subfiles are requested by folder uuid, then the child files are returned', async () => { + const expectedSubfiles = [ + newFile({ attributes: { id: 1, folderUuid: folder.uuid } }), + newFile({ attributes: { id: 2, folderUuid: folder.uuid } }), + newFile({ attributes: { id: 3, folderUuid: folder.uuid } }), + ]; + jest.spyOn(fileUseCases, 'getFiles').mockResolvedValue(expectedSubfiles); + + const result = await folderController.getFolderContentFiles( + userMocked, + folder.uuid, + 50, + 0, + 'id', + 'ASC', + ); + expect(result).toEqual({ files: expectedSubfiles }); + }); + + it('When get folder subfolders are requested by folder uuid, then the child folders are returned', async () => { + const expectedSubfolders = [ + newFolder({ attributes: { id: 1, parentUuid: folder.uuid } }), + newFolder({ attributes: { id: 2, parentUuid: folder.uuid } }), + newFolder({ attributes: { id: 3, parentUuid: folder.uuid } }), + ]; + const mappedSubfolders = expectedSubfolders.map((f) => { + let folderStatus: FileStatus; + if (f.removed) { + folderStatus = FileStatus.DELETED; + } else if (f.deleted) { + folderStatus = FileStatus.TRASHED; + } else { + folderStatus = FileStatus.EXISTS; + } + return { ...f, status: folderStatus }; + }); + + jest + .spyOn(folderUseCases, 'getFolders') + .mockResolvedValue(expectedSubfolders); + + const result = await folderController.getFolderContentFolders( + userMocked, + folder.uuid, + 50, + 0, + 'id', + 'ASC', + ); + + expect(result).toEqual({ folders: mappedSubfolders }); + }); + + it('When get folder subfiles are requested by invalid params, then it should throw an error', () => { + expect( + folderController.getFolderContentFiles( + userMocked, + 'invalidUUID', + 50, + 0, + 'id', + 'ASC', + ), + ).rejects.toThrow(BadRequestException); + + expect( + folderController.getFolderContentFiles( + userMocked, + folder.uuid, + 0, + 0, + 'id', + 'ASC', + ), + ).rejects.toThrow(BadRequestOutOfRangeLimitException); + + expect( + folderController.getFolderContentFiles( + userMocked, + folder.uuid, + 51, + 0, + 'id', + 'ASC', + ), + ).rejects.toThrow(BadRequestOutOfRangeLimitException); + + expect( + folderController.getFolderContentFiles( + userMocked, + folder.uuid, + 50, + -1, + 'id', + 'ASC', + ), + ).rejects.toThrow(BadRequestInvalidOffsetException); + }); + + it('When get folder subfolders are requested by invalid folder uuid, then it should throw an error', async () => { + expect( + folderController.getFolderContentFolders( + userMocked, + 'invalidUUID', + 50, + 0, + 'id', + 'ASC', + ), + ).rejects.toThrow(BadRequestException); + + expect( + folderController.getFolderContentFolders( + userMocked, + folder.uuid, + 0, + 0, + 'id', + 'ASC', + ), + ).rejects.toThrow(BadRequestOutOfRangeLimitException); + + expect( + folderController.getFolderContentFolders( + userMocked, + folder.uuid, + 51, + 0, + 'id', + 'ASC', + ), + ).rejects.toThrow(BadRequestOutOfRangeLimitException); + + expect( + folderController.getFolderContentFolders( + userMocked, + folder.uuid, + 50, + -1, + 'id', + 'ASC', + ), + ).rejects.toThrow(BadRequestInvalidOffsetException); + }); + }); }); diff --git a/src/modules/folder/folder.controller.ts b/src/modules/folder/folder.controller.ts index 6505c2a44..23619bdac 100644 --- a/src/modules/folder/folder.controller.ts +++ b/src/modules/folder/folder.controller.ts @@ -16,11 +16,20 @@ import { FolderUseCases } from './folder.usecase'; import { User as UserDecorator } from '../auth/decorators/user.decorator'; import { User } from '../user/user.domain'; import { FileUseCases } from '../file/file.usecase'; -import { Folder, SortableFolderAttributes } from './folder.domain'; -import { FileStatus, SortableFileAttributes } from '../file/file.domain'; +import { + Folder, + FolderAttributes, + SortableFolderAttributes, +} from './folder.domain'; +import { + FileAttributes, + FileStatus, + SortableFileAttributes, +} from '../file/file.domain'; import logger from '../../externals/logger'; import { validate } from 'uuid'; import { HttpExceptionFilter } from '../../lib/http/http-exception.filter'; +import { isNumber } from '../../lib/validators'; const foldersStatuses = ['ALL', 'EXISTS', 'TRASHED', 'DELETED'] as const; @@ -112,6 +121,47 @@ export class FolderController { } } + @Get('/content/:uuid/files') + async getFolderContentFiles( + @UserDecorator() user: User, + @Param('uuid') folderUuid: string, + @Query('limit') limit: number, + @Query('offset') offset: number, + @Query('sort') sort?: SortableFileAttributes, + @Query('order') order?: 'ASC' | 'DESC', + ): Promise<{ files: FileAttributes[] }> { + if (!validate(folderUuid)) { + throw new BadRequestException('Invalid UUID provided'); + } + + if (!isNumber(limit) || !isNumber(offset)) { + throw new BadRequestWrongOffsetOrLimitException(); + } + + if (limit < 1 || limit > 50) { + throw new BadRequestOutOfRangeLimitException(); + } + + if (offset < 0) { + throw new BadRequestInvalidOffsetException(); + } + + const files = await this.fileUseCases.getFiles( + user.id, + { + folderUuid, + status: FileStatus.EXISTS, + }, + { + limit, + offset, + sort: sort && order && [[sort, order]], + }, + ); + + return { files }; + } + @Get(':id/files') async getFolderFiles( @UserDecorator() user: User, @@ -121,8 +171,6 @@ export class FolderController { @Query('sort') sort?: SortableFileAttributes, @Query('order') order?: 'ASC' | 'DESC', ) { - const isNumber = (n) => !Number.isNaN(parseInt(n.toString())); - if (folderId < 1 || !isNumber(folderId)) { throw new BadRequestWrongFolderIdException(); } @@ -162,8 +210,6 @@ export class FolderController { @Query('name') name: string, @Query('type') type: string, ) { - const isNumber = (n) => !Number.isNaN(parseInt(n.toString())); - if (folderId < 1 || !isNumber(folderId)) { throw new BadRequestWrongFolderIdException(); } @@ -195,6 +241,61 @@ export class FolderController { return singleFile; } + @Get('/content/:uuid/folders') + async getFolderContentFolders( + @UserDecorator() user: User, + @Param('uuid') folderUuid: string, + @Query('limit') limit: number, + @Query('offset') offset: number, + @Query('sort') sort?: SortableFolderAttributes, + @Query('order') order?: 'ASC' | 'DESC', + ): Promise<{ folders: (FolderAttributes & { status: FileStatus })[] }> { + if (!validate(folderUuid)) { + throw new BadRequestException('Invalid UUID provided'); + } + + if (!isNumber(limit) || !isNumber(offset)) { + throw new BadRequestWrongOffsetOrLimitException(); + } + + if (limit < 1 || limit > 50) { + throw new BadRequestOutOfRangeLimitException(); + } + + if (offset < 0) { + throw new BadRequestInvalidOffsetException(); + } + + const folders = await this.folderUseCases.getFolders( + user.id, + { + parentUuid: folderUuid, + deleted: false, + }, + { + limit, + offset, + sort: sort && order && [[sort, order]], + }, + ); + + return { + folders: folders.map((f) => { + let folderStatus: FileStatus; + + if (f.removed) { + folderStatus = FileStatus.DELETED; + } else if (f.deleted) { + folderStatus = FileStatus.TRASHED; + } else { + folderStatus = FileStatus.EXISTS; + } + + return { ...f, status: folderStatus }; + }), + }; + } + @Get(':id/folders') async getFolderFolders( @UserDecorator() user: User, @@ -204,8 +305,6 @@ export class FolderController { @Query('sort') sort?: SortableFolderAttributes, @Query('order') order?: 'ASC' | 'DESC', ) { - const isNumber = (n) => !Number.isNaN(parseInt(n.toString())); - if (folderId < 1 || !isNumber(folderId)) { throw new BadRequestWrongFolderIdException(); } @@ -273,8 +372,6 @@ export class FolderController { throw new BadRequestException(`Unknown status "${status.toString()}"`); } - const isNumber = (n) => !Number.isNaN(parseInt(n.toString())); - if (!isNumber(limit) || !isNumber(offset)) { throw new BadRequestWrongOffsetOrLimitException(); } diff --git a/src/modules/folder/folder.domain.ts b/src/modules/folder/folder.domain.ts index 76f4684fc..6ab8114b0 100644 --- a/src/modules/folder/folder.domain.ts +++ b/src/modules/folder/folder.domain.ts @@ -16,6 +16,7 @@ export interface FolderOptions { export class Folder implements FolderAttributes { id: number; parentId: number | null; + parentUuid: string | null; parent: Folder; type: string; name: string; @@ -38,6 +39,7 @@ export class Folder implements FolderAttributes { id, uuid, parentId, + parentUuid, parent, name, bucket, @@ -56,6 +58,7 @@ export class Folder implements FolderAttributes { this.type = 'folder'; this.id = id; this.parentId = parentId; + this.parentUuid = parentUuid; this.name = name; this.setParent(parent); this.bucket = bucket; diff --git a/src/modules/folder/folder.usecase.spec.ts b/src/modules/folder/folder.usecase.spec.ts index 81f7f73ba..7018344a8 100644 --- a/src/modules/folder/folder.usecase.spec.ts +++ b/src/modules/folder/folder.usecase.spec.ts @@ -9,6 +9,7 @@ import { NotFoundException, UnprocessableEntityException, } from '@nestjs/common'; +import { v4 } from 'uuid'; import { Folder, FolderAttributes, FolderOptions } from './folder.domain'; import { BridgeModule } from '../../externals/bridge/bridge.module'; import { CryptoModule } from '../../externals/crypto/crypto.module'; @@ -46,6 +47,7 @@ describe('FolderUseCases', () => { const mockFolder = Folder.build({ id: 1, parentId: null, + parentUuid: null, name: 'name', bucket: 'bucket', userId: 1, @@ -81,6 +83,7 @@ describe('FolderUseCases', () => { const mockFolder = Folder.build({ id: 1, parentId: null, + parentUuid: null, name: 'name', bucket: 'bucket', userId: 1, @@ -143,6 +146,7 @@ describe('FolderUseCases', () => { Folder.build({ id: 4, parentId: 1, + parentUuid: v4(), name: nameEncrypted, bucket: 'bucket', userId: 1, @@ -213,6 +217,7 @@ describe('FolderUseCases', () => { const folder = Folder.build({ id: folderId, parentId: 3388762609, + parentUuid: v4(), name: 'name', bucket: 'bucket', userId: 1, @@ -272,6 +277,7 @@ describe('FolderUseCases', () => { const folder = Folder.build({ id: folderId, parentId: 3388762609, + parentUuid: v4(), name: 'name', bucket: 'bucket', userId: 1, @@ -336,6 +342,7 @@ describe('FolderUseCases', () => { const folder = Folder.build({ id: folderId, parentId: null, + parentUuid: null, name: 'name', bucket: 'bucket', userId: 1, @@ -401,6 +408,7 @@ describe('FolderUseCases', () => { const folderAtributes: FolderAttributes = { id: 1, parentId: null, + parentUuid: null, parent: null, name: 'name', bucket: 'bucket', @@ -437,6 +445,7 @@ describe('FolderUseCases', () => { size: 0, }; delete expectedResult.parentId; + delete expectedResult.parentUuid; expect(result).toStrictEqual({ ...expectedResult, sharings: undefined }); }); diff --git a/src/modules/fuzzy-search/look-up.domain.ts b/src/modules/fuzzy-search/look-up.domain.ts index be87ecc25..d35857c33 100644 --- a/src/modules/fuzzy-search/look-up.domain.ts +++ b/src/modules/fuzzy-search/look-up.domain.ts @@ -4,7 +4,7 @@ import { UserModel } from '../user/user.model'; export const itemTypes = ['file', 'folder'] as const; -export type ItemType = typeof itemTypes[number]; +export type ItemType = (typeof itemTypes)[number]; export interface LookUpAttributes { id: string; diff --git a/src/modules/sharing/dto/create-sharing-role.dto.ts b/src/modules/sharing/dto/create-sharing-role.dto.ts index 005e559a5..4bd1c46b1 100644 --- a/src/modules/sharing/dto/create-sharing-role.dto.ts +++ b/src/modules/sharing/dto/create-sharing-role.dto.ts @@ -1,7 +1,6 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsNotEmpty } from 'class-validator'; -import { SharingInvite, SharingRole } from '../sharing.domain'; -import { User } from '../../user/user.domain'; +import { SharingRole } from '../sharing.domain'; export class CreateSharingRoleDto { @ApiProperty({ diff --git a/src/modules/sharing/sharing.controller.spec.ts b/src/modules/sharing/sharing.controller.spec.ts index f82423336..2d5f1434c 100644 --- a/src/modules/sharing/sharing.controller.spec.ts +++ b/src/modules/sharing/sharing.controller.spec.ts @@ -2,7 +2,7 @@ import { SharingController } from './sharing.controller'; import { SharingService } from './sharing.service'; import { UuidDto } from '../../common/uuid.dto'; import { createMock } from '@golevelup/ts-jest'; -import { Sharing } from './sharing.domain' +import { Sharing } from './sharing.domain'; import { newSharing } from '../../../test/fixtures'; describe('SharingController', () => { @@ -13,7 +13,7 @@ describe('SharingController', () => { beforeEach(async () => { sharingService = createMock(); controller = new SharingController(sharingService); - sharing = newSharing({}) + sharing = newSharing({}); }); describe('get public sharing folder size', () => { diff --git a/src/modules/sharing/sharing.controller.ts b/src/modules/sharing/sharing.controller.ts index 6a42f0cfa..dda83021d 100644 --- a/src/modules/sharing/sharing.controller.ts +++ b/src/modules/sharing/sharing.controller.ts @@ -58,9 +58,6 @@ import { ThrottlerGuard } from '../../guards/throttler.guard'; import { SetSharingPasswordDto } from './dto/set-sharing-password.dto'; import { UuidDto } from '../../common/uuid.dto'; import { HttpExceptionFilter } from '../../lib/http/http-exception.filter'; -import { ApplyLimit } from '../feature-limit/decorators/apply-limit.decorator'; -import { LimitLabels } from '../feature-limit/limits.enum'; -import { FeatureLimit } from '../feature-limit/feature-limits.guard'; @ApiTags('Sharing') @Controller('sharings') diff --git a/test/fixtures.ts b/test/fixtures.ts index 7e1b2d454..3046e7db7 100644 --- a/test/fixtures.ts +++ b/test/fixtures.ts @@ -34,12 +34,12 @@ export type FilesSettableAttributes = Pick< >; type NewFolderParams = { - attributes?: Partial; + attributes?: Partial; owner?: User; }; type NewFilesParams = { - attributes?: Partial; + attributes?: Partial; owner?: User; folder?: Folder; }; @@ -54,6 +54,7 @@ export const newFolder = (params?: NewFolderParams): Folder => { length: 20, }), parentId: randomDataGenerator.natural({ min: 1 }), + parentUuid: v4(), userId: randomDataGenerator.natural({ min: 1 }), createdAt: randomCreatedAt, updatedAt: new Date(