Skip to content

Commit

Permalink
Merge pull request #283 from internxt/feat/PB-1768-move-file-folders-…
Browse files Browse the repository at this point in the history
…by-uuid

[PB-1768]: Feat/Move file & folders by UUID
  • Loading branch information
sg-gs authored Mar 13, 2024
2 parents 6ea4614 + cfabaa6 commit 414443a
Show file tree
Hide file tree
Showing 16 changed files with 786 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { SequelizeUserRepository } from '../user/user.repository';
import { SequelizeFeatureLimitsRepository } from './feature-limit.repository';
import { AxiosError } from 'axios';
import { User } from '../user/user.domain';
import { PaymentsService } from 'src/externals/payments/payments.service';
import { PaymentsService } from '../../externals/payments/payments.service';
import { PLAN_FREE_TIER_ID } from './limits.enum';

@Injectable()
Expand Down
4 changes: 2 additions & 2 deletions src/modules/feature-limit/feature-limit.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ import { TierLimitsModel } from './models/tier-limits.model';
import { SharingModule } from '../sharing/sharing.module';
import { FeatureLimitsMigrationService } from './feature-limit-migration.service';
import { UserModule } from '../user/user.module';
import { HttpClientModule } from 'src/externals/http/http.module';
import { HttpClientModule } from '../../externals/http/http.module';
import { ConfigModule } from '@nestjs/config';
import { PaidPlansModel } from './models/paid-plans.model';
import { PaymentsService } from 'src/externals/payments/payments.service';
import { PaymentsService } from '../../externals/payments/payments.service';

@Module({
imports: [
Expand Down
13 changes: 13 additions & 0 deletions src/modules/file/dto/move-file.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { IsNotEmpty, IsUUID } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { File } from '../file.domain';

export class MoveFileDto {
@IsNotEmpty()
@IsUUID()
@ApiProperty({
example: '366be646-6d67-436e-8cb6-4b275dfe1729',
description: 'New Destination Folder UUID',
})
destinationFolder: File['folderUuid'];
}
94 changes: 94 additions & 0 deletions src/modules/file/file.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { createMock } from '@golevelup/ts-jest';
import { Test, TestingModule } from '@nestjs/testing';
import { BadRequestException } from '@nestjs/common';
import { v4 } from 'uuid';
import { newFile, newFolder } from '../../../test/fixtures';
import { FileUseCases } from './file.usecase';
import { User } from '../user/user.domain';
import { File, FileStatus } from './file.domain';
import { FileController } from './file.controller';

describe('FileController', () => {
let fileController: FileController;
let fileUseCases: FileUseCases;
let file: File;

const userMocked = User.build({
id: 1,
userId: 'userId',
name: 'User Owner',
lastname: 'Lastname',
email: '[email protected]',
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: [FileController],
providers: [FileUseCases],
})
.useMocker(() => createMock())
.compile();

fileController = module.get<FileController>(FileController);
fileUseCases = module.get<FileUseCases>(FileUseCases);
file = newFile();
});

describe('move file', () => {
it('When move file is requested with valid params, then the file is returned with its updated properties', async () => {
const destinationFolder = newFolder();
const expectedFile = newFile({
attributes: {
...file,
name: 'newencrypted-' + file.name,
folderId: destinationFolder.id,
folderUuid: destinationFolder.uuid,
status: FileStatus.EXISTS,
},
});

jest.spyOn(fileUseCases, 'moveFile').mockResolvedValue(expectedFile);

const result = await fileController.moveFile(userMocked, file.uuid, {
destinationFolder: destinationFolder.uuid,
});
expect(result).toEqual(expectedFile);
});

it('When move file is requested with invalid params, then it should throw an error', () => {
expect(
fileController.moveFile(userMocked, 'invaliduuid', {
destinationFolder: v4(),
}),
).rejects.toThrow(BadRequestException);

expect(
fileController.moveFile(userMocked, v4(), {
destinationFolder: 'invaliduuid',
}),
).rejects.toThrow(BadRequestException);
});
});
});
19 changes: 19 additions & 0 deletions src/modules/file/file.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
Logger,
NotFoundException,
Param,
Patch,
Put,
Query,
} from '@nestjs/common';
Expand All @@ -19,6 +20,7 @@ import API_LIMITS from '../../lib/http/limits';
import { File } from './file.domain';
import { validate } from 'uuid';
import { ReplaceFileDto } from './dto/replace-file.dto';
import { MoveFileDto } from './dto/move-file.dto';

const filesStatuses = ['ALL', 'EXISTS', 'TRASHED', 'DELETED'] as const;

Expand Down Expand Up @@ -175,4 +177,21 @@ export class FileController {
return f;
});
}

@Patch('/:uuid')
async moveFile(
@UserDecorator() user: User,
@Param('uuid') fileUuid: File['uuid'],
@Body() moveFileData: MoveFileDto,
) {
if (!validate(fileUuid) || !validate(moveFileData.destinationFolder)) {
throw new BadRequestException('Invalid UUID provided');
}
const file = await this.fileUseCases.moveFile(
user,
fileUuid,
moveFileData.destinationFolder,
);
return file;
}
}
30 changes: 29 additions & 1 deletion src/modules/file/file.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,22 @@ export interface FileRepository {
userId: FileAttributes['userId'],
where: FindOptions<FileAttributes>,
): Promise<File | null>;
findByNameAndFolderUuid(
name: FileAttributes['name'],
type: FileAttributes['type'],
folderUuid: FileAttributes['folderUuid'],
status: FileAttributes['status'],
): Promise<File | null>;
updateByFieldIdAndUserId(
fileId: FileAttributes['fileId'],
userId: FileAttributes['userId'],
update: Partial<File>,
): Promise<File>;
updateByUuidAndUserId(
uuid: FileAttributes['uuid'],
userId: FileAttributes['userId'],
update: Partial<File>,
): Promise<void>;
updateManyByFieldIdAndUserId(
fileIds: FileAttributes['fileId'][],
userId: FileAttributes['userId'],
Expand Down Expand Up @@ -99,7 +110,7 @@ export class SequelizeFileRepository implements FileRepository {
fileUuid: string,
userId: number,
where: FindOptions<FileAttributes> = {},
): Promise<File> {
): Promise<File | null> {
const file = await this.fileModel.findOne({
where: {
uuid: fileUuid,
Expand All @@ -111,6 +122,23 @@ export class SequelizeFileRepository implements FileRepository {
return file ? this.toDomain(file) : null;
}

async findByNameAndFolderUuid(
name: FileAttributes['name'],
type: FileAttributes['type'],
folderUuid: FileAttributes['folderUuid'],
status: FileAttributes['status'],
): Promise<File | null> {
const file = await this.fileModel.findOne({
where: {
name: { [Op.eq]: name },
type: { [Op.eq]: type },
folderUuid: { [Op.eq]: folderUuid },
status: { [Op.eq]: status },
},
});
return file ? this.toDomain(file) : null;
}

async findAllCursorWhereUpdatedAfter(
where: Partial<FileAttributes>,
updatedAtAfter: Date,
Expand Down
Loading

0 comments on commit 414443a

Please sign in to comment.