From 85f8444c3333e6c2f4ad33cbef9e29d690de6591 Mon Sep 17 00:00:00 2001 From: Xavier Abad <77491413+xabg2@users.noreply.github.com> Date: Fri, 8 May 2026 09:32:30 +0200 Subject: [PATCH 1/5] feat(trash): add parent folder name when returning trashed items --- src/modules/file/file.repository.spec.ts | 83 ++++++++++++- src/modules/file/file.repository.ts | 99 ++++++++++++--- src/modules/folder/folder.repository.spec.ts | 84 ++++++++++++- src/modules/folder/folder.repository.ts | 84 ++++++++++--- src/modules/trash/trash.controller.spec.ts | 113 ++++++++++++++--- src/modules/trash/trash.controller.ts | 36 +++--- .../workspaces/workspaces.usecase.spec.ts | 116 +++++++++++++++++- src/modules/workspaces/workspaces.usecase.ts | 24 +++- 8 files changed, 565 insertions(+), 74 deletions(-) diff --git a/src/modules/file/file.repository.spec.ts b/src/modules/file/file.repository.spec.ts index 42cade753..b84a432e6 100644 --- a/src/modules/file/file.repository.spec.ts +++ b/src/modules/file/file.repository.spec.ts @@ -534,13 +534,67 @@ 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', 'uuid'], + where: { deleted: false, removed: false }, + 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( @@ -558,7 +612,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([]); @@ -579,6 +633,31 @@ 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', 'uuid'], + where: { deleted: false, removed: false }, + required: false, + }), + ]), + }), + ); + }); }); describe('findRecent', () => { diff --git a/src/modules/file/file.repository.ts b/src/modules/file/file.repository.ts index 9d8005b33..b39de0446 100644 --- a/src/modules/file/file.repository.ts +++ b/src/modules/file/file.repository.ts @@ -9,6 +9,7 @@ import { } from './file.domain'; import { type FindOptions, + type Includeable, Op, QueryTypes, Sequelize, @@ -422,7 +423,7 @@ export class SequelizeFileRepository implements FileRepository { 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' }`, ); @@ -455,17 +456,44 @@ export class SequelizeFileRepository implements FileRepository { limit: number, offset: number, order: Array<[keyof FileModel, string]> = [], + include?: Includeable[], ): Promise { - return this.findAllCursorWithThumbnails( - { + const appliedOrder = this.applyCollateToPlainNameSort(order); + + const files = await this.fileModel.findAll({ + limit, + offset, + where: { userId, status: FileStatus.TRASHED, ...(cutoffDate && { updatedAt: { [Op.gte]: cutoffDate } }), }, - limit, - offset, - order, - ); + include: [ + { + model: FolderModel, + as: 'folder', + attributes: ['plainName'], + where: { deleted: false, removed: false }, + required: false, + }, + { + separate: true, + model: this.thumbnailModel, + required: false, + }, + { + separate: true, + model: SharingModel, + attributes: ['type', 'id'], + required: false, + }, + ...include, + ], + subQuery: false, + order: appliedOrder, + }); + + return files.map(this.toDomain.bind(this)); } async findTrashedNotExpiredInWorkspace( @@ -476,17 +504,56 @@ export class SequelizeFileRepository implements FileRepository { offset: number, order: Array<[keyof FileModel, string]> = [], ): Promise { - return this.findAllCursorWithThumbnailsInWorkspace( - createdBy, - workspaceId, - { + const appliedOrder = this.applyCollateToPlainNameSort(order); + + const files = await this.fileModel.findAll({ + limit, + offset, + where: { status: FileStatus.TRASHED, ...(cutoffDate && { updatedAt: { [Op.gte]: cutoffDate } }), }, - limit, - offset, - order, - ); + include: [ + { + model: FolderModel, + as: 'folder', + attributes: ['plainName'], + where: { deleted: false, removed: false }, + required: false, + }, + { + model: this.thumbnailModel, + required: false, + }, + { + separate: true, + model: SharingModel, + attributes: ['type', 'id'], + required: false, + }, + { + model: WorkspaceItemUserModel, + where: { + createdBy, + workspaceId, + itemType: WorkspaceItemType.File, + }, + as: 'workspaceUser', + include: [ + { + model: UserModel, + as: 'creator', + attributes: ['uuid', 'email', 'name', 'lastname', 'userId'], + required: true, + }, + ], + }, + ], + subQuery: false, + order: appliedOrder, + }); + + return files.map(this.toDomain.bind(this)); } async findAllCursorWhereUpdatedAfterInWorkspace( @@ -1087,9 +1154,9 @@ export class SequelizeFileRepository implements FileRepository { status: FileStatus.DELETED, updatedAt: { [Op.lt]: cutoffDate }, }, + order: [['updatedAt', 'ASC']], useMaster: opts?.useMaster, limit, - order: [['updatedAt', 'ASC']], }); return rows.map((r) => r.uuid); diff --git a/src/modules/folder/folder.repository.spec.ts b/src/modules/folder/folder.repository.spec.ts index c51cbf9ac..cac7412f3 100644 --- a/src/modules/folder/folder.repository.spec.ts +++ b/src/modules/folder/folder.repository.spec.ts @@ -352,13 +352,68 @@ 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', 'uuid'], + where: { deleted: false, removed: false }, + 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( @@ -376,7 +431,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([]); @@ -398,6 +453,31 @@ 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', 'uuid'], + where: { deleted: false, removed: false }, + required: false, + }), + ]), + }), + ); + }); }); describe('createWithAttributes', () => { diff --git a/src/modules/folder/folder.repository.ts b/src/modules/folder/folder.repository.ts index 5d4896f0d..396f66296 100644 --- a/src/modules/folder/folder.repository.ts +++ b/src/modules/folder/folder.repository.ts @@ -183,7 +183,7 @@ export class SequelizeFolderRepository implements FolderRepository { structuredClone(order); const [, orderDirection] = order[plainNameIndex]; newOrder[plainNameIndex] = Sequelize.literal( - `plain_name COLLATE "custom_numeric" ${ + `"FolderModel"."plain_name" COLLATE "custom_numeric" ${ orderDirection === 'ASC' ? 'ASC' : 'DESC' }`, ); @@ -250,17 +250,37 @@ export class SequelizeFolderRepository implements FolderRepository { offset: number, order: Array<[keyof FolderModel, 'ASC' | 'DESC']> = [], ): Promise { - return this.findAllCursor( - { + const appliedOrder = this.applyCollateToPlainNameSort(order); + + const folders = await this.folderModel.findAll({ + limit, + offset, + where: { userId, deleted: true, removed: false, ...(cutoffDate && { updatedAt: { [Op.gte]: cutoffDate } }), }, - limit, - offset, - order, - ); + include: [ + { + model: FolderModel, + as: 'parent', + attributes: ['plainName'], + where: { deleted: false, removed: false }, + required: false, + }, + { + separate: true, + model: SharingModel, + attributes: ['type', 'id'], + required: false, + }, + ], + subQuery: false, + order: appliedOrder, + }); + + return folders.map(this.toDomain.bind(this)); } async findTrashedNotExpiredInWorkspace( @@ -271,18 +291,52 @@ export class SequelizeFolderRepository implements FolderRepository { offset: number, order: Array<[keyof FolderModel, 'ASC' | 'DESC']> = [], ): Promise { - return this.findAllCursorInWorkspace( - createdBy, - workspaceId, - { + const appliedOrder = this.applyCollateToPlainNameSort(order); + + const folders = await this.folderModel.findAll({ + limit, + offset, + where: { deleted: true, removed: false, ...(cutoffDate && { updatedAt: { [Op.gte]: cutoffDate } }), }, - limit, - offset, - order, - ); + include: [ + { + model: FolderModel, + as: 'parent', + attributes: ['plainName', 'uuid'], + where: { deleted: false, removed: false }, + required: false, + }, + { + model: WorkspaceItemUserModel, + where: { + createdBy, + workspaceId, + itemType: WorkspaceItemType.Folder, + }, + as: 'workspaceUser', + include: [ + { + model: UserModel, + as: 'creator', + attributes: ['uuid', 'email', 'name', 'lastname', 'userId'], + }, + ], + }, + { + separate: true, + model: SharingModel, + attributes: ['type', 'id'], + required: false, + }, + ], + subQuery: false, + order: appliedOrder, + }); + + return folders.map(this.toDomain.bind(this)); } async findAllCursorWithParent( diff --git a/src/modules/trash/trash.controller.spec.ts b/src/modules/trash/trash.controller.spec.ts index 13e6b944e..a2920fe46 100644 --- a/src/modules/trash/trash.controller.spec.ts +++ b/src/modules/trash/trash.controller.spec.ts @@ -415,9 +415,15 @@ describe('TrashController', () => { ).rejects.toThrow(BadRequestException); }); - it('When type is files, then it should return trashed files', async () => { + it('When trashed files are returned, then each file should include the parent folder name and not expose the full folder object', async () => { + const parentFolder = newFolder({ + attributes: { plainName: 'Parent folder' }, + }); const mockFiles = [ - newFile({ attributes: { status: FileStatus.TRASHED } }), + newFile({ + attributes: { status: FileStatus.TRASHED }, + folder: parentFolder, + }), ]; const expectedExpiresAt = new Date('2026-03-01'); jest.spyOn(fileUseCases, 'getTrashedFiles').mockResolvedValue(mockFiles); @@ -445,17 +451,49 @@ describe('TrashController', () => { }, ); expect(result).toEqual({ - result: mockFiles.map((file) => ({ - ...file.toJSON(), - expiresAt: expectedExpiresAt, - })), + result: mockFiles.map((file) => { + const { folder: _folder, ...fileJson } = file.toJSON(); + return { + ...fileJson, + parentFolderName: parentFolder.plainName, + expiresAt: expectedExpiresAt, + }; + }), }); }); - it('When type is folders, then it should return trashed folders', async () => { - const mockFolders = [ - newFolder({ attributes: { deleted: true, removed: false } }), - ]; + it('When a trashed file has no parent folder, then the parent folder should be null', async () => { + const mockFile = newFile({ + attributes: { status: FileStatus.TRASHED }, + }); + mockFile.folder = null; + jest.spyOn(fileUseCases, 'getTrashedFiles').mockResolvedValue([mockFile]); + jest + .spyOn(trashUseCases, 'getTrashRetentionDays') + .mockResolvedValue(retentionDays); + jest + .spyOn(TrashExpirationUtils, 'calculateTrashExpirationDate') + .mockReturnValue(new Date('2026-03-01')); + + const result = await controller.getTrashedFilesPaginated( + user, + validPagination, + 'files', + ); + + expect((result.result[0] as any).parentFolderName).toBeNull(); + expect((result.result[0] as any).folder).toBeUndefined(); + }); + + it('When trashed folders are returned, then each folder should include the parent folder name and not expose the full parent object', async () => { + const parentFolder = newFolder({ + attributes: { plainName: 'Parent folder' }, + }); + const mockFolder = newFolder({ + attributes: { deleted: true, removed: false }, + }); + mockFolder.parent = parentFolder; + const mockFolders = [mockFolder]; const expectedExpiresAt = new Date('2026-03-01'); jest .spyOn(folderUseCases, 'getTrashedFolders') @@ -484,13 +522,42 @@ describe('TrashController', () => { }, ); expect(result).toEqual({ - result: mockFolders.map((folder) => ({ - ...folder.toJSON(), - expiresAt: expectedExpiresAt, - })), + result: mockFolders.map((folder) => { + const { parent: _parent, ...folderJson } = folder.toJSON(); + return { + ...folderJson, + parentFolderName: parentFolder.plainName, + expiresAt: expectedExpiresAt, + }; + }), }); }); + it('When a trashed folder has no parent folder, then parentFolderName should be null', async () => { + const mockFolder = newFolder({ + attributes: { deleted: true, removed: false }, + }); + mockFolder.parent = null; + jest + .spyOn(folderUseCases, 'getTrashedFolders') + .mockResolvedValue([mockFolder]); + jest + .spyOn(trashUseCases, 'getTrashRetentionDays') + .mockResolvedValue(retentionDays); + jest + .spyOn(TrashExpirationUtils, 'calculateTrashExpirationDate') + .mockReturnValue(new Date('2026-03-01')); + + const result = await controller.getTrashedFilesPaginated( + user, + validPagination, + 'folders', + ); + + expect((result.result[0] as any).parentFolderName).toBeNull(); + expect((result.result[0] as any).parent).toBeUndefined(); + }); + it('When type is files and a file has no updatedAt, then expiresAt should be null', async () => { const mockFile = newFile({ attributes: { status: FileStatus.TRASHED, updatedAt: null }, @@ -510,8 +577,15 @@ describe('TrashController', () => { expect( TrashExpirationUtils.calculateTrashExpirationDate, ).not.toHaveBeenCalled(); + const { folder: _folder, ...fileJson } = mockFile.toJSON(); expect(result).toEqual({ - result: [{ ...mockFile.toJSON(), expiresAt: null }], + result: [ + { + ...fileJson, + parentFolderName: mockFile.folder?.plainName ?? null, + expiresAt: null, + }, + ], }); }); @@ -536,8 +610,15 @@ describe('TrashController', () => { expect( TrashExpirationUtils.calculateTrashExpirationDate, ).not.toHaveBeenCalled(); + const { parent: _parent, ...folderJson } = mockFolder.toJSON(); expect(result).toEqual({ - result: [{ ...mockFolder.toJSON(), expiresAt: null }], + result: [ + { + ...folderJson, + parentFolderName: mockFolder.parent?.plainName ?? null, + expiresAt: null, + }, + ], }); }); diff --git a/src/modules/trash/trash.controller.ts b/src/modules/trash/trash.controller.ts index 09a2f10b7..d67dd3a13 100644 --- a/src/modules/trash/trash.controller.ts +++ b/src/modules/trash/trash.controller.ts @@ -115,13 +115,17 @@ export class TrashController { }, ); - const result = files.map((file) => ({ - ...file.toJSON(), - expiresAt: - retentionDays && file.updatedAt - ? calculateTrashExpirationDate(retentionDays, file.updatedAt) - : null, - })); + const result = files.map((file) => { + const { folder: _folder, ...fileJson } = file.toJSON(); + return { + ...fileJson, + parentFolderName: file.folder?.plainName ?? null, + expiresAt: + retentionDays && file.updatedAt + ? calculateTrashExpirationDate(retentionDays, file.updatedAt) + : null, + }; + }); return { result }; } else { @@ -135,13 +139,17 @@ export class TrashController { }, ); - const result = folders.map((folder) => ({ - ...folder.toJSON(), - expiresAt: - retentionDays && folder.updatedAt - ? calculateTrashExpirationDate(retentionDays, folder.updatedAt) - : null, - })); + const result = folders.map((folder) => { + const { parent: _parent, ...folderJson } = folder.toJSON(); + return { + ...folderJson, + parentFolderName: folder.parent?.plainName ?? null, + expiresAt: + retentionDays && folder.updatedAt + ? calculateTrashExpirationDate(retentionDays, folder.updatedAt) + : null, + }; + }); return { result }; } diff --git a/src/modules/workspaces/workspaces.usecase.spec.ts b/src/modules/workspaces/workspaces.usecase.spec.ts index 394c2a215..248b0fd4f 100644 --- a/src/modules/workspaces/workspaces.usecase.spec.ts +++ b/src/modules/workspaces/workspaces.usecase.spec.ts @@ -3898,8 +3898,15 @@ describe('WorkspacesUsecases', () => { ['plainName', 'ASC'] as any, ); + const { folder: _folder, ...fileJson } = trashedFiles[0].toJSON(); expect(result).toEqual({ - result: [{ ...trashedFiles[0].toJSON(), expiresAt: null }], + result: [ + { + ...fileJson, + parentFolderName: trashedFiles[0].folder?.plainName ?? null, + expiresAt: null, + }, + ], }); expect(fileUseCases.getTrashedFilesInWorkspace).toHaveBeenCalledWith( user.uuid, @@ -3930,8 +3937,15 @@ describe('WorkspacesUsecases', () => { ['plainName', 'ASC'] as any, ); + const { parent: _parent, ...folderJson } = trashedFolders[0].toJSON(); expect(result).toEqual({ - result: [{ ...trashedFolders[0].toJSON(), expiresAt: null }], + result: [ + { + ...folderJson, + parentFolderName: trashedFolders[0].parent?.plainName ?? null, + expiresAt: null, + }, + ], }); expect(folderUseCases.getTrashedFoldersInWorkspace).toHaveBeenCalledWith( user.uuid, @@ -3978,9 +3992,105 @@ describe('WorkspacesUsecases', () => { expectedCutoffDate, { limit, offset, sort: ['plainName', 'ASC'] }, ); + const { folder: _folder, ...fileJson } = trashedFiles[0].toJSON(); expect(result).toEqual({ - result: [{ ...trashedFiles[0].toJSON(), expiresAt: expectedExpiresAt }], + result: [ + { + ...fileJson, + parentFolderName: trashedFiles[0].folder?.plainName ?? null, + expiresAt: expectedExpiresAt, + }, + ], + }); + }); + + it('When a workspace trashed file has a parent folder, then its name should be returned as parentFolderName and the full folder object should not be exposed', async () => { + const parentFolder = newFolder({ + attributes: { plainName: 'My Parent Folder' }, }); + const trashedFile = newFile({ folder: parentFolder }); + jest + .spyOn(fileUseCases, 'getTrashedFilesInWorkspace') + .mockResolvedValue([trashedFile]); + jest + .spyOn(workspaceRepository, 'findWorkspaceResourcesOwner') + .mockResolvedValue(workspaceResourcesOwner); + jest + .spyOn(featureLimitsService, 'getUserLimitByLabel') + .mockResolvedValue(null); + + const result = await service.getWorkspaceUserTrashedItems( + user, + workspaceId, + WorkspaceItemType.File, + limit, + offset, + ); + + expect(result.result[0]).toEqual( + expect.objectContaining({ + parentFolderName: 'My Parent Folder', + }), + ); + expect(result.result[0]).not.toHaveProperty('folder'); + }); + + it('When a workspace trashed folder has a parent folder, then its name should be returned as parentFolderName and the full parent object should not be exposed', async () => { + const parentFolder = newFolder({ + attributes: { plainName: 'Outer Folder' }, + }); + const trashedFolder = newFolder({ attributes: { deleted: true } }); + trashedFolder.parent = parentFolder; + jest + .spyOn(folderUseCases, 'getTrashedFoldersInWorkspace') + .mockResolvedValue([trashedFolder]); + jest + .spyOn(workspaceRepository, 'findWorkspaceResourcesOwner') + .mockResolvedValue(workspaceResourcesOwner); + jest + .spyOn(featureLimitsService, 'getUserLimitByLabel') + .mockResolvedValue(null); + + const result = await service.getWorkspaceUserTrashedItems( + user, + workspaceId, + WorkspaceItemType.Folder, + limit, + offset, + ); + + expect(result.result[0]).toEqual( + expect.objectContaining({ + parentFolderName: 'Outer Folder', + }), + ); + expect(result.result[0]).not.toHaveProperty('parent'); + }); + + it('When a workspace trashed file has no parent folder, then parentFolderName should be null', async () => { + const trashedFile = newFile(); + trashedFile.folder = null; + jest + .spyOn(fileUseCases, 'getTrashedFilesInWorkspace') + .mockResolvedValue([trashedFile]); + jest + .spyOn(workspaceRepository, 'findWorkspaceResourcesOwner') + .mockResolvedValue(workspaceResourcesOwner); + jest + .spyOn(featureLimitsService, 'getUserLimitByLabel') + .mockResolvedValue(null); + + const result = await service.getWorkspaceUserTrashedItems( + user, + workspaceId, + WorkspaceItemType.File, + limit, + offset, + ); + + expect(result.result[0]).toEqual( + expect.objectContaining({ parentFolderName: null }), + ); }); }); diff --git a/src/modules/workspaces/workspaces.usecase.ts b/src/modules/workspaces/workspaces.usecase.ts index 9ea2863bb..9c49b249c 100644 --- a/src/modules/workspaces/workspaces.usecase.ts +++ b/src/modules/workspaces/workspaces.usecase.ts @@ -726,12 +726,24 @@ export class WorkspacesUsecases { ); return { - result: items.map((item) => ({ - ...item.toJSON(), - expiresAt: retentionDays - ? calculateTrashExpirationDate(retentionDays, item.updatedAt) - : null, - })), + result: items.map((item: File | Folder) => { + const parentFolderName = + itemType === WorkspaceItemType.File + ? ((item as File).folder?.plainName ?? null) + : ((item as Folder).parent?.plainName ?? null); + const { + folder: _folder, + parent: _parent, + ...rest + } = item.toJSON() as any; + return { + ...rest, + parentFolderName, + expiresAt: retentionDays + ? calculateTrashExpirationDate(retentionDays, item.updatedAt) + : null, + }; + }), }; } From 5978710260a9ef607775cf2233ae1eee1f304ca8 Mon Sep 17 00:00:00 2001 From: Xavier Abad <77491413+xabg2@users.noreply.github.com> Date: Fri, 8 May 2026 10:01:45 +0200 Subject: [PATCH 2/5] fix: remove useless field uuid from tests --- src/modules/file/file.repository.spec.ts | 4 ++-- src/modules/file/file.repository.ts | 2 +- src/modules/folder/folder.repository.spec.ts | 10 +++++----- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/modules/file/file.repository.spec.ts b/src/modules/file/file.repository.spec.ts index b84a432e6..a5274bc27 100644 --- a/src/modules/file/file.repository.spec.ts +++ b/src/modules/file/file.repository.spec.ts @@ -578,7 +578,7 @@ describe('FileRepository', () => { include: expect.arrayContaining([ expect.objectContaining({ as: 'folder', - attributes: ['plainName', 'uuid'], + attributes: ['plainName'], where: { deleted: false, removed: false }, required: false, }), @@ -650,7 +650,7 @@ describe('FileRepository', () => { include: expect.arrayContaining([ expect.objectContaining({ as: 'folder', - attributes: ['plainName', 'uuid'], + attributes: ['plainName'], where: { deleted: false, removed: false }, required: false, }), diff --git a/src/modules/file/file.repository.ts b/src/modules/file/file.repository.ts index b39de0446..08b07daeb 100644 --- a/src/modules/file/file.repository.ts +++ b/src/modules/file/file.repository.ts @@ -456,7 +456,7 @@ export class SequelizeFileRepository implements FileRepository { limit: number, offset: number, order: Array<[keyof FileModel, string]> = [], - include?: Includeable[], + include: Includeable[] = [], ): Promise { const appliedOrder = this.applyCollateToPlainNameSort(order); diff --git a/src/modules/folder/folder.repository.spec.ts b/src/modules/folder/folder.repository.spec.ts index cac7412f3..c594b856c 100644 --- a/src/modules/folder/folder.repository.spec.ts +++ b/src/modules/folder/folder.repository.spec.ts @@ -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'; @@ -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: { @@ -397,7 +397,7 @@ describe('SequelizeFolderRepository', () => { include: expect.arrayContaining([ expect.objectContaining({ as: 'parent', - attributes: ['plainName', 'uuid'], + attributes: ['plainName'], where: { deleted: false, removed: false }, required: false, }), From 17d7a895e76e4a7fa402cac2e17da9392cba7ee7 Mon Sep 17 00:00:00 2001 From: Xavier Abad <77491413+xabg2@users.noreply.github.com> Date: Fri, 8 May 2026 12:32:50 +0200 Subject: [PATCH 3/5] feat: return parent object with plainName and Status --- src/modules/file/file.repository.spec.ts | 14 ++++---- src/modules/file/file.repository.ts | 21 ++--------- src/modules/folder/folder.repository.spec.ts | 6 ++-- src/modules/folder/folder.repository.ts | 37 ++------------------ src/modules/trash/trash.controller.spec.ts | 25 +++++++++---- src/modules/trash/trash.controller.ts | 12 +++++-- 6 files changed, 41 insertions(+), 74 deletions(-) diff --git a/src/modules/file/file.repository.spec.ts b/src/modules/file/file.repository.spec.ts index a5274bc27..3a289106f 100644 --- a/src/modules/file/file.repository.spec.ts +++ b/src/modules/file/file.repository.spec.ts @@ -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 { @@ -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; @@ -578,8 +578,7 @@ describe('FileRepository', () => { include: expect.arrayContaining([ expect.objectContaining({ as: 'folder', - attributes: ['plainName'], - where: { deleted: false, removed: false }, + attributes: ['plainName', 'removed', 'deleted'], required: false, }), ]), @@ -650,8 +649,7 @@ describe('FileRepository', () => { include: expect.arrayContaining([ expect.objectContaining({ as: 'folder', - attributes: ['plainName'], - where: { deleted: false, removed: false }, + attributes: ['plainName', 'removed', 'deleted'], required: false, }), ]), diff --git a/src/modules/file/file.repository.ts b/src/modules/file/file.repository.ts index 08b07daeb..4e2568a73 100644 --- a/src/modules/file/file.repository.ts +++ b/src/modules/file/file.repository.ts @@ -9,7 +9,6 @@ import { } from './file.domain'; import { type FindOptions, - type Includeable, Op, QueryTypes, Sequelize, @@ -456,7 +455,6 @@ export class SequelizeFileRepository implements FileRepository { limit: number, offset: number, order: Array<[keyof FileModel, string]> = [], - include: Includeable[] = [], ): Promise { const appliedOrder = this.applyCollateToPlainNameSort(order); @@ -472,8 +470,7 @@ export class SequelizeFileRepository implements FileRepository { { model: FolderModel, as: 'folder', - attributes: ['plainName'], - where: { deleted: false, removed: false }, + attributes: ['plainName', 'removed', 'deleted'], required: false, }, { @@ -481,13 +478,6 @@ export class SequelizeFileRepository implements FileRepository { model: this.thumbnailModel, required: false, }, - { - separate: true, - model: SharingModel, - attributes: ['type', 'id'], - required: false, - }, - ...include, ], subQuery: false, order: appliedOrder, @@ -517,20 +507,13 @@ export class SequelizeFileRepository implements FileRepository { { model: FolderModel, as: 'folder', - attributes: ['plainName'], - where: { deleted: false, removed: false }, + attributes: ['plainName', 'removed', 'deleted'], required: false, }, { model: this.thumbnailModel, required: false, }, - { - separate: true, - model: SharingModel, - attributes: ['type', 'id'], - required: false, - }, { model: WorkspaceItemUserModel, where: { diff --git a/src/modules/folder/folder.repository.spec.ts b/src/modules/folder/folder.repository.spec.ts index c594b856c..bf1143fa6 100644 --- a/src/modules/folder/folder.repository.spec.ts +++ b/src/modules/folder/folder.repository.spec.ts @@ -397,8 +397,7 @@ describe('SequelizeFolderRepository', () => { include: expect.arrayContaining([ expect.objectContaining({ as: 'parent', - attributes: ['plainName'], - where: { deleted: false, removed: false }, + attributes: ['plainName', 'removed', 'deleted'], required: false, }), ]), @@ -470,8 +469,7 @@ describe('SequelizeFolderRepository', () => { include: expect.arrayContaining([ expect.objectContaining({ as: 'parent', - attributes: ['plainName', 'uuid'], - where: { deleted: false, removed: false }, + attributes: ['plainName', 'removed', 'deleted'], required: false, }), ]), diff --git a/src/modules/folder/folder.repository.ts b/src/modules/folder/folder.repository.ts index 396f66296..54db4e7e3 100644 --- a/src/modules/folder/folder.repository.ts +++ b/src/modules/folder/folder.repository.ts @@ -4,7 +4,7 @@ import { InjectModel } from '@nestjs/sequelize'; import { Op, QueryTypes, Sequelize, type WhereOptions } from 'sequelize'; import { v4 } from 'uuid'; -import { Folder, FolderStatus } from './folder.domain'; +import { Folder } from './folder.domain'; import { type FolderAttributes } from './folder.attributes'; import { FolderModel } from './folder.model'; import { SharingModel } from '../sharing/models'; @@ -265,14 +265,7 @@ export class SequelizeFolderRepository implements FolderRepository { { model: FolderModel, as: 'parent', - attributes: ['plainName'], - where: { deleted: false, removed: false }, - required: false, - }, - { - separate: true, - model: SharingModel, - attributes: ['type', 'id'], + attributes: ['plainName', 'removed', 'deleted'], required: false, }, ], @@ -305,8 +298,7 @@ export class SequelizeFolderRepository implements FolderRepository { { model: FolderModel, as: 'parent', - attributes: ['plainName', 'uuid'], - where: { deleted: false, removed: false }, + attributes: ['plainName', 'removed', 'deleted'], required: false, }, { @@ -325,12 +317,6 @@ export class SequelizeFolderRepository implements FolderRepository { }, ], }, - { - separate: true, - model: SharingModel, - attributes: ['type', 'id'], - required: false, - }, ], subQuery: false, order: appliedOrder, @@ -1256,19 +1242,6 @@ export class SequelizeFolderRepository implements FolderRepository { ); } - private folderStatusWhereCondition( - status: FolderStatus, - ): Pick { - switch (status) { - case FolderStatus.EXISTS: - return { deleted: false, removed: false }; - case FolderStatus.TRASHED: - return { deleted: true, removed: false }; - case FolderStatus.DELETED: - return { deleted: true, removed: true }; - } - } - private toDomain(model: FolderModel): Folder { const buildUser = (userData: UserModel | null) => userData ? User.build(userData) : null; @@ -1279,8 +1252,4 @@ export class SequelizeFolderRepository implements FolderRepository { user: buildUser(model.user || model.workspaceUser?.creator), }); } - - private toModel(domain: Folder): Partial { - return domain.toJSON(); - } } diff --git a/src/modules/trash/trash.controller.spec.ts b/src/modules/trash/trash.controller.spec.ts index a2920fe46..2d9b05d9f 100644 --- a/src/modules/trash/trash.controller.spec.ts +++ b/src/modules/trash/trash.controller.spec.ts @@ -455,7 +455,10 @@ describe('TrashController', () => { const { folder: _folder, ...fileJson } = file.toJSON(); return { ...fileJson, - parentFolderName: parentFolder.plainName, + parent: { + plainName: parentFolder.plainName, + status: parentFolder.status, + }, expiresAt: expectedExpiresAt, }; }), @@ -481,7 +484,7 @@ describe('TrashController', () => { 'files', ); - expect((result.result[0] as any).parentFolderName).toBeNull(); + expect((result.result[0] as any).parent.plainName).toBeUndefined(); expect((result.result[0] as any).folder).toBeUndefined(); }); @@ -526,14 +529,17 @@ describe('TrashController', () => { const { parent: _parent, ...folderJson } = folder.toJSON(); return { ...folderJson, - parentFolderName: parentFolder.plainName, + parent: { + plainName: parentFolder.plainName, + status: parentFolder.status, + }, expiresAt: expectedExpiresAt, }; }), }); }); - it('When a trashed folder has no parent folder, then parentFolderName should be null', async () => { + it('When a trashed folder has no parent folder, then the parent folder name should be not returned', async () => { const mockFolder = newFolder({ attributes: { deleted: true, removed: false }, }); @@ -554,7 +560,6 @@ describe('TrashController', () => { 'folders', ); - expect((result.result[0] as any).parentFolderName).toBeNull(); expect((result.result[0] as any).parent).toBeUndefined(); }); @@ -582,7 +587,10 @@ describe('TrashController', () => { result: [ { ...fileJson, - parentFolderName: mockFile.folder?.plainName ?? null, + parent: { + plainName: mockFile.folder?.plainName ?? null, + status: mockFile.folder?.status ?? null, + }, expiresAt: null, }, ], @@ -615,7 +623,10 @@ describe('TrashController', () => { result: [ { ...folderJson, - parentFolderName: mockFolder.parent?.plainName ?? null, + parent: { + plainName: mockFolder.parent?.plainName ?? null, + status: mockFolder.parent?.status ?? null, + }, expiresAt: null, }, ], diff --git a/src/modules/trash/trash.controller.ts b/src/modules/trash/trash.controller.ts index d67dd3a13..01eaca5d9 100644 --- a/src/modules/trash/trash.controller.ts +++ b/src/modules/trash/trash.controller.ts @@ -119,7 +119,10 @@ export class TrashController { const { folder: _folder, ...fileJson } = file.toJSON(); return { ...fileJson, - parentFolderName: file.folder?.plainName ?? null, + parent: { + plainName: file.folder?.plainName, + status: file.folder?.status, + }, expiresAt: retentionDays && file.updatedAt ? calculateTrashExpirationDate(retentionDays, file.updatedAt) @@ -143,7 +146,12 @@ export class TrashController { const { parent: _parent, ...folderJson } = folder.toJSON(); return { ...folderJson, - parentFolderName: folder.parent?.plainName ?? null, + ...(folder.parent !== null && { + parent: { + plainName: folder.parent?.plainName ?? null, + status: folder.parent?.status ?? null, + }, + }), expiresAt: retentionDays && folder.updatedAt ? calculateTrashExpirationDate(retentionDays, folder.updatedAt) From c943a0b79854022cc7795650304f70a70759d604 Mon Sep 17 00:00:00 2001 From: Xavier Abad <77491413+xabg2@users.noreply.github.com> Date: Fri, 8 May 2026 12:47:06 +0200 Subject: [PATCH 4/5] refactor(trash): extract duplicated code to a function --- src/modules/file/file.repository.ts | 87 ++++++++++++++----------- src/modules/folder/folder.repository.ts | 84 ++++++++++++++---------- 2 files changed, 99 insertions(+), 72 deletions(-) diff --git a/src/modules/file/file.repository.ts b/src/modules/file/file.repository.ts index 4e2568a73..b3b985329 100644 --- a/src/modules/file/file.repository.ts +++ b/src/modules/file/file.repository.ts @@ -9,6 +9,7 @@ import { } from './file.domain'; import { type FindOptions, + type Includeable, Op, QueryTypes, Sequelize, @@ -456,34 +457,15 @@ export class SequelizeFileRepository implements FileRepository { offset: number, order: Array<[keyof FileModel, string]> = [], ): Promise { - const appliedOrder = this.applyCollateToPlainNameSort(order); - - const files = await this.fileModel.findAll({ + return this.trashedNotExpiredQuery({ + cutoffDate, limit, offset, + order, where: { userId, - status: FileStatus.TRASHED, - ...(cutoffDate && { updatedAt: { [Op.gte]: cutoffDate } }), }, - include: [ - { - model: FolderModel, - as: 'folder', - attributes: ['plainName', 'removed', 'deleted'], - required: false, - }, - { - separate: true, - model: this.thumbnailModel, - required: false, - }, - ], - subQuery: false, - order: appliedOrder, }); - - return files.map(this.toDomain.bind(this)); } async findTrashedNotExpiredInWorkspace( @@ -494,26 +476,12 @@ export class SequelizeFileRepository implements FileRepository { offset: number, order: Array<[keyof FileModel, string]> = [], ): Promise { - const appliedOrder = this.applyCollateToPlainNameSort(order); - - const files = await this.fileModel.findAll({ + return this.trashedNotExpiredQuery({ + cutoffDate, limit, offset, - where: { - status: FileStatus.TRASHED, - ...(cutoffDate && { updatedAt: { [Op.gte]: cutoffDate } }), - }, + order, include: [ - { - model: FolderModel, - as: 'folder', - attributes: ['plainName', 'removed', 'deleted'], - required: false, - }, - { - model: this.thumbnailModel, - required: false, - }, { model: WorkspaceItemUserModel, where: { @@ -532,6 +500,47 @@ export class SequelizeFileRepository implements FileRepository { ], }, ], + }); + } + + async trashedNotExpiredQuery({ + cutoffDate, + limit, + offset, + order = [], + where, + include = [], + }: { + cutoffDate: Date | null; + limit: number; + offset: number; + order: Array<[keyof FileModel, string]>; + where?: WhereOptions; + include?: Includeable[]; + }): Promise { + 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'], + required: false, + }, + { + model: this.thumbnailModel, + required: false, + }, + ...include, + ], subQuery: false, order: appliedOrder, }); diff --git a/src/modules/folder/folder.repository.ts b/src/modules/folder/folder.repository.ts index 54db4e7e3..bf69f276f 100644 --- a/src/modules/folder/folder.repository.ts +++ b/src/modules/folder/folder.repository.ts @@ -1,7 +1,13 @@ import { Injectable, NotFoundException } from '@nestjs/common'; import { withQueryTimeout } from '../../lib/query-timeout'; import { InjectModel } from '@nestjs/sequelize'; -import { Op, QueryTypes, Sequelize, type WhereOptions } from 'sequelize'; +import { + type Includeable, + Op, + QueryTypes, + Sequelize, + type WhereOptions, +} from 'sequelize'; import { v4 } from 'uuid'; import { Folder } from './folder.domain'; @@ -250,30 +256,15 @@ export class SequelizeFolderRepository implements FolderRepository { offset: number, order: Array<[keyof FolderModel, 'ASC' | 'DESC']> = [], ): Promise { - const appliedOrder = this.applyCollateToPlainNameSort(order); - - const folders = await this.folderModel.findAll({ + return this.trashNotExpiredQuery({ + cutoffDate, limit, offset, + order, where: { userId, - deleted: true, - removed: false, - ...(cutoffDate && { updatedAt: { [Op.gte]: cutoffDate } }), }, - include: [ - { - model: FolderModel, - as: 'parent', - attributes: ['plainName', 'removed', 'deleted'], - required: false, - }, - ], - subQuery: false, - order: appliedOrder, }); - - return folders.map(this.toDomain.bind(this)); } async findTrashedNotExpiredInWorkspace( @@ -284,23 +275,12 @@ export class SequelizeFolderRepository implements FolderRepository { offset: number, order: Array<[keyof FolderModel, 'ASC' | 'DESC']> = [], ): Promise { - const appliedOrder = this.applyCollateToPlainNameSort(order); - - const folders = await this.folderModel.findAll({ + return this.trashNotExpiredQuery({ + cutoffDate, limit, offset, - where: { - deleted: true, - removed: false, - ...(cutoffDate && { updatedAt: { [Op.gte]: cutoffDate } }), - }, + order, include: [ - { - model: FolderModel, - as: 'parent', - attributes: ['plainName', 'removed', 'deleted'], - required: false, - }, { model: WorkspaceItemUserModel, where: { @@ -318,6 +298,44 @@ export class SequelizeFolderRepository implements FolderRepository { ], }, ], + }); + } + + async trashNotExpiredQuery({ + cutoffDate, + limit, + offset, + order, + include = [], + where, + }: { + cutoffDate: Date | null; + limit: number; + offset: number; + order: Array<[keyof FolderModel, 'ASC' | 'DESC']>; + include?: Includeable[]; + where?: WhereOptions; + }): Promise { + const appliedOrder = this.applyCollateToPlainNameSort(order); + + const folders = await this.folderModel.findAll({ + limit, + offset, + where: { + deleted: true, + removed: false, + ...(cutoffDate && { updatedAt: { [Op.gte]: cutoffDate } }), + ...where, + }, + include: [ + { + model: FolderModel, + as: 'parent', + attributes: ['plainName', 'removed', 'deleted'], + required: false, + }, + ...include, + ], subQuery: false, order: appliedOrder, }); From 0a47abc5682a16f2b1f5cab6289d52da4cfa385f Mon Sep 17 00:00:00 2001 From: Xavier Abad <77491413+xabg2@users.noreply.github.com> Date: Fri, 8 May 2026 15:07:16 +0200 Subject: [PATCH 5/5] feat: return the parent uuid --- src/modules/file/file.repository.spec.ts | 4 ++-- src/modules/file/file.repository.ts | 2 +- src/modules/folder/folder.repository.spec.ts | 4 ++-- src/modules/folder/folder.repository.ts | 2 +- src/modules/trash/trash.controller.spec.ts | 4 ++++ src/modules/trash/trash.controller.ts | 2 ++ 6 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/modules/file/file.repository.spec.ts b/src/modules/file/file.repository.spec.ts index 3a289106f..61e99af04 100644 --- a/src/modules/file/file.repository.spec.ts +++ b/src/modules/file/file.repository.spec.ts @@ -578,7 +578,7 @@ describe('FileRepository', () => { include: expect.arrayContaining([ expect.objectContaining({ as: 'folder', - attributes: ['plainName', 'removed', 'deleted'], + attributes: ['plainName', 'removed', 'deleted', 'uuid'], required: false, }), ]), @@ -649,7 +649,7 @@ describe('FileRepository', () => { include: expect.arrayContaining([ expect.objectContaining({ as: 'folder', - attributes: ['plainName', 'removed', 'deleted'], + attributes: ['plainName', 'removed', 'deleted', 'uuid'], required: false, }), ]), diff --git a/src/modules/file/file.repository.ts b/src/modules/file/file.repository.ts index b3b985329..4386ce7e9 100644 --- a/src/modules/file/file.repository.ts +++ b/src/modules/file/file.repository.ts @@ -532,7 +532,7 @@ export class SequelizeFileRepository implements FileRepository { { model: FolderModel, as: 'folder', - attributes: ['plainName', 'removed', 'deleted'], + attributes: ['plainName', 'removed', 'deleted', 'uuid'], required: false, }, { diff --git a/src/modules/folder/folder.repository.spec.ts b/src/modules/folder/folder.repository.spec.ts index bf1143fa6..39c8679b7 100644 --- a/src/modules/folder/folder.repository.spec.ts +++ b/src/modules/folder/folder.repository.spec.ts @@ -397,7 +397,7 @@ describe('SequelizeFolderRepository', () => { include: expect.arrayContaining([ expect.objectContaining({ as: 'parent', - attributes: ['plainName', 'removed', 'deleted'], + attributes: ['plainName', 'removed', 'deleted', 'uuid'], required: false, }), ]), @@ -469,7 +469,7 @@ describe('SequelizeFolderRepository', () => { include: expect.arrayContaining([ expect.objectContaining({ as: 'parent', - attributes: ['plainName', 'removed', 'deleted'], + attributes: ['plainName', 'removed', 'deleted', 'uuid'], required: false, }), ]), diff --git a/src/modules/folder/folder.repository.ts b/src/modules/folder/folder.repository.ts index bf69f276f..ac58665aa 100644 --- a/src/modules/folder/folder.repository.ts +++ b/src/modules/folder/folder.repository.ts @@ -331,7 +331,7 @@ export class SequelizeFolderRepository implements FolderRepository { { model: FolderModel, as: 'parent', - attributes: ['plainName', 'removed', 'deleted'], + attributes: ['plainName', 'removed', 'deleted', 'uuid'], required: false, }, ...include, diff --git a/src/modules/trash/trash.controller.spec.ts b/src/modules/trash/trash.controller.spec.ts index 2d9b05d9f..13aad0cc5 100644 --- a/src/modules/trash/trash.controller.spec.ts +++ b/src/modules/trash/trash.controller.spec.ts @@ -458,6 +458,7 @@ describe('TrashController', () => { parent: { plainName: parentFolder.plainName, status: parentFolder.status, + uuid: parentFolder.uuid, }, expiresAt: expectedExpiresAt, }; @@ -532,6 +533,7 @@ describe('TrashController', () => { parent: { plainName: parentFolder.plainName, status: parentFolder.status, + uuid: parentFolder.uuid, }, expiresAt: expectedExpiresAt, }; @@ -590,6 +592,7 @@ describe('TrashController', () => { parent: { plainName: mockFile.folder?.plainName ?? null, status: mockFile.folder?.status ?? null, + uuid: mockFile.folder?.uuid ?? null, }, expiresAt: null, }, @@ -626,6 +629,7 @@ describe('TrashController', () => { parent: { plainName: mockFolder.parent?.plainName ?? null, status: mockFolder.parent?.status ?? null, + uuid: mockFolder.parent?.uuid ?? null, }, expiresAt: null, }, diff --git a/src/modules/trash/trash.controller.ts b/src/modules/trash/trash.controller.ts index 01eaca5d9..f8e3e53a1 100644 --- a/src/modules/trash/trash.controller.ts +++ b/src/modules/trash/trash.controller.ts @@ -122,6 +122,7 @@ export class TrashController { parent: { plainName: file.folder?.plainName, status: file.folder?.status, + uuid: file.folder?.uuid, }, expiresAt: retentionDays && file.updatedAt @@ -150,6 +151,7 @@ export class TrashController { parent: { plainName: folder.parent?.plainName ?? null, status: folder.parent?.status ?? null, + uuid: folder.parent?.uuid ?? null, }, }), expiresAt: