diff --git a/packages/@n8n/db/src/repositories/folder.repository.ts b/packages/@n8n/db/src/repositories/folder.repository.ts index a34960182d..1b75244056 100644 --- a/packages/@n8n/db/src/repositories/folder.repository.ts +++ b/packages/@n8n/db/src/repositories/folder.repository.ts @@ -29,7 +29,7 @@ export class FolderRepository extends Repository { const query = this.createQueryBuilder('folder'); - this.applySelections(query, options.select); + this.applySelections(query, options.select, options.filter); this.applyFilters(query, options.filter); this.applySorting(query, options.sortBy); this.applyPagination(query, options); @@ -39,21 +39,41 @@ export class FolderRepository extends Repository, - select?: Record, + select?: ListQuery.Options['select'], + filter?: ListQuery.Options['filter'], ): void { if (select) { - this.applyCustomSelect(query, select); + this.applyCustomSelect(query, select, filter); } else { - this.applyDefaultSelect(query); + this.applyDefaultSelect(query, filter); } } - private applyDefaultSelect(query: SelectQueryBuilder): void { + private applyWorkflowCountSelect( + query: SelectQueryBuilder, + filter?: ListQuery.Options['filter'], + ): void { + if (typeof filter?.isArchived === 'boolean') { + query.loadRelationCountAndMap('folder.workflowCount', 'folder.workflows', 'workflow', (qb) => + qb.andWhere('workflow.isArchived = :isArchived', { + isArchived: filter.isArchived, + }), + ); + } else { + query.loadRelationCountAndMap('folder.workflowCount', 'folder.workflows'); + } + } + + private applyDefaultSelect( + query: SelectQueryBuilder, + filter?: ListQuery.Options['filter'], + ): void { + this.applyWorkflowCountSelect(query, filter); + query .leftJoinAndSelect('folder.homeProject', 'homeProject') .leftJoinAndSelect('folder.parentFolder', 'parentFolder') .leftJoinAndSelect('folder.tags', 'tags') - .loadRelationCountAndMap('folder.workflowCount', 'folder.workflows') .loadRelationCountAndMap('folder.subFolderCount', 'folder.subFolders') .select([ 'folder', @@ -65,17 +85,18 @@ export class FolderRepository extends Repository, - select?: Record, + select?: ListQuery.Options['select'], + filter?: ListQuery.Options['filter'], ): void { const selections = ['folder.id']; this.addBasicFields(selections, select); - this.addRelationFields(query, selections, select); + this.addRelationFields(query, selections, select, filter); query.select(selections); } - private addBasicFields(selections: string[], select?: Record): void { + private addBasicFields(selections: string[], select?: ListQuery.Options['select']): void { if (select?.name) selections.push('folder.name'); if (select?.createdAt) selections.push('folder.createdAt'); if (select?.updatedAt) selections.push('folder.updatedAt'); @@ -84,7 +105,8 @@ export class FolderRepository extends Repository, selections: string[], - select?: Record, + select?: ListQuery.Options['select'], + filter?: ListQuery.Options['filter'], ): void { if (select?.project) { query.leftJoin('folder.homeProject', 'homeProject'); @@ -102,7 +124,7 @@ export class FolderRepository extends Repository { expect(folders[0].parentFolder?.id).toBe(parentFolder.id); expect(folders[0].tags[0].name).toBe('important'); }); + + describe('workflowCount', () => { + let testFolder: Folder; + + beforeEach(async () => { + const parentFolder = await createFolder(project, { name: 'Parent' }); + testFolder = await createFolder(project, { name: 'Test Folder', parentFolder }); + + await createWorkflow({ parentFolder: testFolder, isArchived: false }); + await createWorkflow({ parentFolder: testFolder, isArchived: false }); + await createWorkflow({ parentFolder: testFolder, isArchived: true }); + await createWorkflow({ parentFolder: testFolder, isArchived: true }); + await createWorkflow({ parentFolder: testFolder, isArchived: true }); + }); + + it('should include archived workflows in the workflow count by default', async () => { + const [folders] = await folderRepository.getManyAndCount({ + select: { workflowCount: true }, + }); + + expect(folders).toHaveLength(2); + expect(folders).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: testFolder.id, + workflowCount: 5, + }), + expect.objectContaining({ + id: testFolder.parentFolderId, + workflowCount: 0, + }), + ]), + ); + }); + + it('should include only archived workflows in the workflow count if filtered', async () => { + const [folders] = await folderRepository.getManyAndCount({ + select: { workflowCount: true }, + filter: { isArchived: true }, + }); + + expect(folders).toHaveLength(2); + expect(folders).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: testFolder.id, + workflowCount: 3, + }), + expect.objectContaining({ + id: testFolder.parentFolderId, + workflowCount: 0, + }), + ]), + ); + }); + + it('should return only unarchived workflows in the workflow count if filtered', async () => { + const [folders] = await folderRepository.getManyAndCount({ + select: { workflowCount: true }, + filter: { isArchived: false }, + }); + + expect(folders).toHaveLength(2); + expect(folders).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: testFolder.id, + workflowCount: 2, + }), + expect.objectContaining({ + id: testFolder.parentFolderId, + workflowCount: 0, + }), + ]), + ); + }); + }); }); describe('select', () => { diff --git a/packages/cli/src/databases/repositories/workflow.repository.ts b/packages/cli/src/databases/repositories/workflow.repository.ts index e3a880f2c1..9a1e490519 100644 --- a/packages/cli/src/databases/repositories/workflow.repository.ts +++ b/packages/cli/src/databases/repositories/workflow.repository.ts @@ -287,7 +287,10 @@ export class WorkflowRepository extends Repository { this.getWorkflowsAndFoldersCount(workflowIds, options), ]); - const { workflows, folders } = await this.fetchExtraData(workflowsAndFolders); + const isArchived = + typeof options.filter?.isArchived === 'boolean' ? options.filter.isArchived : undefined; + + const { workflows, folders } = await this.fetchExtraData(workflowsAndFolders, isArchived); const enrichedWorkflowsAndFolders = this.enrichDataWithExtras(workflowsAndFolders, { workflows, @@ -323,13 +326,16 @@ export class WorkflowRepository extends Repository { .map((item) => item.id); } - private async fetchExtraData(workflowsAndFolders: WorkflowFolderUnionRow[]) { + private async fetchExtraData( + workflowsAndFolders: WorkflowFolderUnionRow[], + isArchived?: boolean, + ) { const workflowIds = this.getWorkflowsIds(workflowsAndFolders); const folderIds = this.getFolderIds(workflowsAndFolders); const [workflows, folders] = await Promise.all([ this.getMany(workflowIds), - this.folderRepository.getMany({ filter: { folderIds } }), + this.folderRepository.getMany({ filter: { folderIds, isArchived } }), ]); return { workflows, folders }; diff --git a/packages/cli/src/services/folder.service.ts b/packages/cli/src/services/folder.service.ts index 17b6257c3b..f38b0aa363 100644 --- a/packages/cli/src/services/folder.service.ts +++ b/packages/cli/src/services/folder.service.ts @@ -253,7 +253,8 @@ export class FolderService { const workflowCountQuery = this.workflowRepository .createQueryBuilder('workflow') .select('COUNT(workflow.id)', 'count') - .where((qb) => { + .where('workflow.isArchived = :isArchived', { isArchived: false }) + .andWhere((qb) => { const folderQuery = qb.subQuery().from('folder_path', 'fp').select('fp.id').getQuery(); return `workflow.parentFolderId IN ${folderQuery}`; }) @@ -279,7 +280,7 @@ export class FolderService { } async getManyAndCount(projectId: string, options: ListQuery.Options) { - options.filter = { ...options.filter, projectId }; + options.filter = { ...options.filter, projectId, isArchived: false }; return await this.folderRepository.getManyAndCount(options); } } diff --git a/packages/cli/test/integration/folder/folder.controller.test.ts b/packages/cli/test/integration/folder/folder.controller.test.ts index e0bcba6a57..a842e11b1e 100644 --- a/packages/cli/test/integration/folder/folder.controller.test.ts +++ b/packages/cli/test/integration/folder/folder.controller.test.ts @@ -1320,6 +1320,23 @@ describe('GET /projects/:projectId/folders', () => { ['Owner Folder 1', 'Owner Folder 2'].sort(), ); }); + + test('should include workflow count', async () => { + const folder = await createFolder(ownerProject, { name: 'Test Folder' }); + await createWorkflow({ parentFolder: folder, isArchived: false }, ownerProject); + await createWorkflow({ parentFolder: folder, isArchived: false }, ownerProject); + // Not included in the count + await createWorkflow({ parentFolder: folder, isArchived: true }, ownerProject); + + const response = await authOwnerAgent + .get(`/projects/${ownerProject.id}/folders`) + .query({ filter: '{ "name": "test" }' }) + .expect(200); + + expect(response.body.count).toBe(1); + expect(response.body.data).toHaveLength(1); + expect(response.body.data[0].workflowCount).toEqual(2); + }); }); describe('GET /projects/:projectId/folders/content', () => { @@ -1371,6 +1388,11 @@ describe('GET /projects/:projectId/folders/content', () => { await createWorkflow({ parentFolder: personalFolder1 }, ownerProject); await createWorkflow({ parentFolder: personalProjectSubfolder1 }, ownerProject); await createWorkflow({ parentFolder: personalProjectSubfolder2 }, ownerProject); + // Not included in the count + await createWorkflow( + { parentFolder: personalProjectSubfolder2, isArchived: true }, + ownerProject, + ); const response = await authOwnerAgent .get(`/projects/${ownerProject.id}/folders/${personalFolder1.id}/content`)