Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 83 additions & 6 deletions src/modules/file/file.repository.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
import { Test, type TestingModule } from '@nestjs/testing';

jest.mock('../../lib/query-timeout', () => ({
withQueryTimeout: jest.fn((_sequelize, _timeout, cb) => cb({})),
}));
import { getModelToken } from '@nestjs/sequelize';
import { createMock } from '@golevelup/ts-jest';
import {
Expand All @@ -23,6 +19,10 @@ import { UserModel } from '../user/user.model';
import { WorkspaceItemUserModel } from '../workspaces/models/workspace-items-users.model';
import { Time } from '../../lib/time';

jest.mock('../../lib/query-timeout', () => ({
withQueryTimeout: jest.fn((_sequelize, _timeout, cb) => cb({})),
}));

describe('FileRepository', () => {
let repository: FileRepository;
let fileModel: typeof FileModel;
Expand Down Expand Up @@ -534,13 +534,66 @@ describe('FileRepository', () => {
});
});

describe('findTrashedNotExpired', () => {
const userId = 1;
const limit = 10;
const offset = 0;

it('When no expiration date is set, then it should return all trashed files regardless of when they were trashed', async () => {
jest.spyOn(fileModel, 'findAll').mockResolvedValue([]);

await repository.findTrashedNotExpired(userId, null, limit, offset);

expect(fileModel.findAll).toHaveBeenCalledWith(
expect.objectContaining({
where: { userId, status: FileStatus.TRASHED },
}),
);
});

it('When an expiration date is set, then it should only return trashed files that have not yet expired', async () => {
const cutoffDate = new Date('2026-03-04');
jest.spyOn(fileModel, 'findAll').mockResolvedValue([]);

await repository.findTrashedNotExpired(userId, cutoffDate, limit, offset);

expect(fileModel.findAll).toHaveBeenCalledWith(
expect.objectContaining({
where: {
userId,
status: FileStatus.TRASHED,
updatedAt: { [Op.gte]: cutoffDate },
},
}),
);
});

it('When retrieving trashed files, then it should also load the name of the folder each file belongs to', async () => {
jest.spyOn(fileModel, 'findAll').mockResolvedValue([]);

await repository.findTrashedNotExpired(userId, null, limit, offset);

expect(fileModel.findAll).toHaveBeenCalledWith(
expect.objectContaining({
include: expect.arrayContaining([
expect.objectContaining({
as: 'folder',
attributes: ['plainName', 'removed', 'deleted', 'uuid'],
required: false,
}),
]),
}),
);
});
});

describe('findTrashedNotExpiredInWorkspace', () => {
const createdBy = v4();
const workspaceId = v4();
const limit = 10;
const offset = 0;

it('When cutoffDate is null, then it should query trashed files without a date filter', async () => {
it('When no expiration date is set, then it should return all trashed workspace files regardless of when they were trashed', async () => {
jest.spyOn(fileModel, 'findAll').mockResolvedValue([]);

await repository.findTrashedNotExpiredInWorkspace(
Expand All @@ -558,7 +611,7 @@ describe('FileRepository', () => {
);
});

it('When cutoffDate is provided, then it should add an updatedAt >= cutoffDate filter', async () => {
it('When an expiration date is set, then it should only return workspace trashed files that have not yet expired', async () => {
const cutoffDate = new Date('2026-03-04');
jest.spyOn(fileModel, 'findAll').mockResolvedValue([]);

Expand All @@ -579,6 +632,30 @@ describe('FileRepository', () => {
}),
);
});

it('When retrieving workspace trashed files, then it should also load the name of the folder each file belongs to', async () => {
jest.spyOn(fileModel, 'findAll').mockResolvedValue([]);

await repository.findTrashedNotExpiredInWorkspace(
createdBy,
workspaceId,
null,
limit,
offset,
);

expect(fileModel.findAll).toHaveBeenCalledWith(
expect.objectContaining({
include: expect.arrayContaining([
expect.objectContaining({
as: 'folder',
attributes: ['plainName', 'removed', 'deleted', 'uuid'],
required: false,
}),
]),
}),
);
});
});

describe('findRecent', () => {
Expand Down
93 changes: 76 additions & 17 deletions src/modules/file/file.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
} from './file.domain';
import {
type FindOptions,
type Includeable,
Op,
QueryTypes,
Sequelize,
Expand Down Expand Up @@ -422,7 +423,7 @@
structuredClone(order);
const [, orderDirection] = order[plainNameIndex];
newOrder[plainNameIndex] = Sequelize.literal(
`plain_name COLLATE "custom_numeric" ${
`"FileModel"."plain_name" COLLATE "custom_numeric" ${
orderDirection === 'ASC' ? 'ASC' : 'DESC'
}`,
);
Expand Down Expand Up @@ -456,16 +457,15 @@
offset: number,
order: Array<[keyof FileModel, string]> = [],
): Promise<File[]> {
return this.findAllCursorWithThumbnails(
{
userId,
status: FileStatus.TRASHED,
...(cutoffDate && { updatedAt: { [Op.gte]: cutoffDate } }),
},
return this.trashedNotExpiredQuery({
cutoffDate,
limit,
offset,
order,
);
where: {
userId,
},
});
}

async findTrashedNotExpiredInWorkspace(
Expand All @@ -476,17 +476,76 @@
offset: number,
order: Array<[keyof FileModel, string]> = [],
): Promise<File[]> {
return this.findAllCursorWithThumbnailsInWorkspace(
createdBy,
workspaceId,
{
status: FileStatus.TRASHED,
...(cutoffDate && { updatedAt: { [Op.gte]: cutoffDate } }),
},
return this.trashedNotExpiredQuery({
cutoffDate,
limit,
offset,
order,
);
include: [
{
model: WorkspaceItemUserModel,
where: {
createdBy,
workspaceId,
itemType: WorkspaceItemType.File,
},
as: 'workspaceUser',
include: [
{
model: UserModel,
as: 'creator',
attributes: ['uuid', 'email', 'name', 'lastname', 'userId'],
required: true,
},
],
},
],
});
}

async trashedNotExpiredQuery({
cutoffDate,
limit,
offset,
order = [],
where,
include = [],
}: {
cutoffDate: Date | null;
limit: number;
offset: number;
order: Array<[keyof FileModel, string]>;
where?: WhereOptions<any>;
include?: Includeable[];
}): Promise<File[]> {
const appliedOrder = this.applyCollateToPlainNameSort(order);

const files = await this.fileModel.findAll({
limit,
offset,
where: {
status: FileStatus.TRASHED,
...(cutoffDate && { updatedAt: { [Op.gte]: cutoffDate } }),
...where,
},
include: [
{
model: FolderModel,
as: 'folder',
attributes: ['plainName', 'removed', 'deleted', 'uuid'],
required: false,
},
{
model: this.thumbnailModel,
required: false,
},
...include,
],
subQuery: false,
order: appliedOrder,
});

return files.map(this.toDomain.bind(this));
}

async findAllCursorWhereUpdatedAfterInWorkspace(
Expand Down Expand Up @@ -599,7 +658,7 @@
},
});

return sizes[0]['total'] as unknown as number;

Check warning on line 661 in src/modules/file/file.repository.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

This assertion is unnecessary since the receiver accepts the original type of the expression.

See more on https://sonarcloud.io/project/issues?id=internxt_drive-server-wip&issues=AZ4HKo-NZ30Gkr3r07YX&open=AZ4HKo-NZ30Gkr3r07YX&pullRequest=1056
}

async findAllCursorWithThumbnails(
Expand Down Expand Up @@ -1087,9 +1146,9 @@
status: FileStatus.DELETED,
updatedAt: { [Op.lt]: cutoffDate },
},
order: [['updatedAt', 'ASC']],
useMaster: opts?.useMaster,
limit,
order: [['updatedAt', 'ASC']],
});

return rows.map((r) => r.uuid);
Expand Down
90 changes: 84 additions & 6 deletions src/modules/folder/folder.repository.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
import { createMock } from '@golevelup/ts-jest';

jest.mock('../../lib/query-timeout', () => ({
withQueryTimeout: jest.fn((_sequelize, _timeout, cb) => cb({})),
}));
import { CalculateFolderSizeTimeoutException } from './exception/calculate-folder-size-timeout.exception';
import { SequelizeFolderRepository } from './folder.repository';
import { FolderModel } from './folder.model';
Expand All @@ -19,6 +15,10 @@ import { v4 } from 'uuid';
import { randomInt } from 'crypto';
import { Time } from '../../lib/time';

jest.mock('../../lib/query-timeout', () => ({
withQueryTimeout: jest.fn((_sequelize, _timeout, cb) => cb({})),
}));

jest.mock('./folder.model', () => ({
FolderModel: {
sequelize: {
Expand Down Expand Up @@ -352,13 +352,67 @@ describe('SequelizeFolderRepository', () => {
});
});

describe('findTrashedNotExpired', () => {
const userId = 1;
const limit = 10;
const offset = 0;

it('When no expiration date is set, then it should return all trashed folders regardless of when they were trashed', async () => {
jest.spyOn(folderModel, 'findAll').mockResolvedValueOnce([]);

await repository.findTrashedNotExpired(userId, null, limit, offset);

expect(folderModel.findAll).toHaveBeenCalledWith(
expect.objectContaining({
where: { userId, deleted: true, removed: false },
}),
);
});

it('When an expiration date is set, then it should only return trashed folders that have not yet expired', async () => {
const cutoffDate = new Date('2026-03-04');
jest.spyOn(folderModel, 'findAll').mockResolvedValueOnce([]);

await repository.findTrashedNotExpired(userId, cutoffDate, limit, offset);

expect(folderModel.findAll).toHaveBeenCalledWith(
expect.objectContaining({
where: {
userId,
deleted: true,
removed: false,
updatedAt: { [Op.gte]: cutoffDate },
},
}),
);
});

it('When retrieving trashed folders, then it should also load the name of the parent folder each folder belongs to', async () => {
jest.spyOn(folderModel, 'findAll').mockResolvedValueOnce([]);

await repository.findTrashedNotExpired(userId, null, limit, offset);

expect(folderModel.findAll).toHaveBeenCalledWith(
expect.objectContaining({
include: expect.arrayContaining([
expect.objectContaining({
as: 'parent',
attributes: ['plainName', 'removed', 'deleted', 'uuid'],
required: false,
}),
]),
}),
);
});
});

describe('findTrashedNotExpiredInWorkspace', () => {
const createdBy = v4();
const workspaceId = v4();
const limit = 10;
const offset = 0;

it('When cutoffDate is null, then it should query trashed folders without a date filter', async () => {
it('When no expiration date is set, then it should return all trashed workspace folders regardless of when they were trashed', async () => {
jest.spyOn(folderModel, 'findAll').mockResolvedValueOnce([]);

await repository.findTrashedNotExpiredInWorkspace(
Expand All @@ -376,7 +430,7 @@ describe('SequelizeFolderRepository', () => {
);
});

it('When cutoffDate is provided, then it should add an updatedAt >= cutoffDate filter', async () => {
it('When an expiration date is set, then it should only return workspace trashed folders that have not yet expired', async () => {
const cutoffDate = new Date('2026-03-04');
jest.spyOn(folderModel, 'findAll').mockResolvedValueOnce([]);

Expand All @@ -398,6 +452,30 @@ describe('SequelizeFolderRepository', () => {
}),
);
});

it('When retrieving workspace trashed folders, then it should also load the name of the parent folder each folder belongs to', async () => {
jest.spyOn(folderModel, 'findAll').mockResolvedValueOnce([]);

await repository.findTrashedNotExpiredInWorkspace(
createdBy,
workspaceId,
null,
limit,
offset,
);

expect(folderModel.findAll).toHaveBeenCalledWith(
expect.objectContaining({
include: expect.arrayContaining([
expect.objectContaining({
as: 'parent',
attributes: ['plainName', 'removed', 'deleted', 'uuid'],
required: false,
}),
]),
}),
);
});
});

describe('createWithAttributes', () => {
Expand Down
Loading
Loading