diff --git a/packages/@n8n/api-types/src/dto/folders/list-folder-query.dto.ts b/packages/@n8n/api-types/src/dto/folders/list-folder-query.dto.ts index f513272de1..7da236099d 100644 --- a/packages/@n8n/api-types/src/dto/folders/list-folder-query.dto.ts +++ b/packages/@n8n/api-types/src/dto/folders/list-folder-query.dto.ts @@ -11,6 +11,7 @@ const VALID_SELECT_FIELDS = [ 'tags', 'parentFolder', 'workflowCount', + 'subFolderCount', ] as const; const VALID_SORT_OPTIONS = [ diff --git a/packages/cli/src/databases/entities/folder.ts b/packages/cli/src/databases/entities/folder.ts index 78e07a237e..7a599b7048 100644 --- a/packages/cli/src/databases/entities/folder.ts +++ b/packages/cli/src/databases/entities/folder.ts @@ -13,8 +13,9 @@ import { Project } from './project'; import { TagEntity } from './tag-entity'; import { type WorkflowEntity } from './workflow-entity'; -export type FolderWithWorkflowCount = Folder & { +export type FolderWithWorkflowAndSubFolderCount = Folder & { workflowCount: boolean; + subFolderCount: number; }; @Entity() @@ -26,6 +27,12 @@ export class Folder extends WithTimestampsAndStringId { @JoinColumn({ name: 'parentFolderId' }) parentFolder: Folder | null; + @OneToMany( + () => Folder, + (folder) => folder.parentFolder, + ) + subFolders: Folder[]; + @ManyToOne(() => Project) @JoinColumn({ name: 'projectId' }) homeProject: Project; diff --git a/packages/cli/src/databases/repositories/__tests__/folder.repository.test.ts b/packages/cli/src/databases/repositories/__tests__/folder.repository.test.ts index 0b200d4e39..beafdf8a02 100644 --- a/packages/cli/src/databases/repositories/__tests__/folder.repository.test.ts +++ b/packages/cli/src/databases/repositories/__tests__/folder.repository.test.ts @@ -366,6 +366,24 @@ describe('FolderRepository', () => { }); }); + it('should return id, name and subFolderCount when specified', async () => { + const [folders] = await folderRepository.getManyAndCount({ + select: { + id: true, + name: true, + subFolderCount: true, + }, + }); + + expect(folders).toHaveLength(2); + folders.forEach((folder) => { + expect(Object.keys(folder).sort()).toEqual(['id', 'name', 'subFolderCount']); + expect(folder.id).toBeDefined(); + expect(folder.name).toBeDefined(); + expect(folder.subFolderCount).toBeDefined(); + }); + }); + it('should return timestamps when specified', async () => { const [folders] = await folderRepository.getManyAndCount({ select: { @@ -400,6 +418,7 @@ describe('FolderRepository', () => { icon: null, }, workflowCount: expect.any(Number), + subFolderCount: expect.any(Number), tags: expect.any(Array), }); }); diff --git a/packages/cli/src/databases/repositories/folder.repository.ts b/packages/cli/src/databases/repositories/folder.repository.ts index 6eb5d6e208..16e410c51c 100644 --- a/packages/cli/src/databases/repositories/folder.repository.ts +++ b/packages/cli/src/databases/repositories/folder.repository.ts @@ -5,30 +5,32 @@ import { PROJECT_ROOT } from 'n8n-workflow'; import type { ListQuery } from '@/requests'; -import type { FolderWithWorkflowCount } from '../entities/folder'; +import type { FolderWithWorkflowAndSubFolderCount } from '../entities/folder'; import { Folder } from '../entities/folder'; import { FolderTagMapping } from '../entities/folder-tag-mapping'; import { TagEntity } from '../entities/tag-entity'; @Service() -export class FolderRepository extends Repository { +export class FolderRepository extends Repository { constructor(dataSource: DataSource) { super(Folder, dataSource.manager); } async getManyAndCount( options: ListQuery.Options = {}, - ): Promise<[FolderWithWorkflowCount[], number]> { + ): Promise<[FolderWithWorkflowAndSubFolderCount[], number]> { const query = this.getManyQuery(options); return await query.getManyAndCount(); } - async getMany(options: ListQuery.Options = {}): Promise { + async getMany(options: ListQuery.Options = {}): Promise { const query = this.getManyQuery(options); return await query.getMany(); } - getManyQuery(options: ListQuery.Options = {}): SelectQueryBuilder { + getManyQuery( + options: ListQuery.Options = {}, + ): SelectQueryBuilder { const query = this.createQueryBuilder('folder'); this.applySelections(query, options.select); @@ -40,7 +42,7 @@ export class FolderRepository extends Repository { } private applySelections( - query: SelectQueryBuilder, + query: SelectQueryBuilder, select?: Record, ): void { if (select) { @@ -50,12 +52,13 @@ export class FolderRepository extends Repository { } } - private applyDefaultSelect(query: SelectQueryBuilder): void { + private applyDefaultSelect(query: SelectQueryBuilder): void { 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', ...this.getProjectFields('homeProject'), @@ -65,7 +68,7 @@ export class FolderRepository extends Repository { } private applyCustomSelect( - query: SelectQueryBuilder, + query: SelectQueryBuilder, select?: Record, ): void { const selections = ['folder.id']; @@ -83,7 +86,7 @@ export class FolderRepository extends Repository { } private addRelationFields( - query: SelectQueryBuilder, + query: SelectQueryBuilder, selections: string[], select?: Record, ): void { @@ -105,6 +108,12 @@ export class FolderRepository extends Repository { if (select?.workflowCount) { query.loadRelationCountAndMap('folder.workflowCount', 'folder.workflows'); } + + if (select?.subFolderCount) { + if (!query.hasRelation(Folder, 'folder.parentFolder')) { + query.loadRelationCountAndMap('folder.subFolderCount', 'folder.subFolders'); + } + } } private getProjectFields(alias: string): string[] { @@ -120,7 +129,7 @@ export class FolderRepository extends Repository { } private applyFilters( - query: SelectQueryBuilder, + query: SelectQueryBuilder, filter?: ListQuery.Options['filter'], ): void { if (!filter) return; @@ -130,7 +139,7 @@ export class FolderRepository extends Repository { } private applyBasicFilters( - query: SelectQueryBuilder, + query: SelectQueryBuilder, filter: ListQuery.Options['filter'], ): void { if (filter?.folderIds && Array.isArray(filter.folderIds)) { @@ -163,7 +172,7 @@ export class FolderRepository extends Repository { } private applyTagsFilter( - query: SelectQueryBuilder, + query: SelectQueryBuilder, tags?: string[], ): void { if (!Array.isArray(tags) || tags.length === 0) return; @@ -177,7 +186,7 @@ export class FolderRepository extends Repository { } private createTagsSubQuery( - query: SelectQueryBuilder, + query: SelectQueryBuilder, tags: string[], ): SelectQueryBuilder { return query @@ -192,7 +201,10 @@ export class FolderRepository extends Repository { }); } - private applySorting(query: SelectQueryBuilder, sortBy?: string): void { + private applySorting( + query: SelectQueryBuilder, + sortBy?: string, + ): void { if (!sortBy) { query.orderBy('folder.updatedAt', 'DESC'); return; @@ -208,7 +220,7 @@ export class FolderRepository extends Repository { } private applySortingByField( - query: SelectQueryBuilder, + query: SelectQueryBuilder, field: string, direction: 'DESC' | 'ASC', ): void { @@ -222,7 +234,7 @@ export class FolderRepository extends Repository { } private applyPagination( - query: SelectQueryBuilder, + query: SelectQueryBuilder, options: ListQuery.Options, ): void { if (options?.take) { diff --git a/packages/cli/src/databases/repositories/workflow.repository.ts b/packages/cli/src/databases/repositories/workflow.repository.ts index b865aab28b..99cc8b592a 100644 --- a/packages/cli/src/databases/repositories/workflow.repository.ts +++ b/packages/cli/src/databases/repositories/workflow.repository.ts @@ -16,7 +16,7 @@ import type { ListQuery } from '@/requests'; import { isStringArray } from '@/utils'; import { FolderRepository } from './folder.repository'; -import type { Folder, FolderWithWorkflowCount } from '../entities/folder'; +import type { Folder, FolderWithWorkflowAndSubFolderCount } from '../entities/folder'; import { TagEntity } from '../entities/tag-entity'; import { WebhookEntity } from '../entities/webhook-entity'; import { WorkflowEntity } from '../entities/workflow-entity'; @@ -36,7 +36,7 @@ type WorkflowFolderUnionRow = { export type WorkflowFolderUnionFull = ( | ListQuery.Workflow.Plain | ListQuery.Workflow.WithSharing - | FolderWithWorkflowCount + | FolderWithWorkflowAndSubFolderCount ) & { resource: ResourceType; }; diff --git a/packages/cli/test/integration/workflows/workflows.controller.test.ts b/packages/cli/test/integration/workflows/workflows.controller.test.ts index 51fa617e6f..84c33415e0 100644 --- a/packages/cli/test/integration/workflows/workflows.controller.test.ts +++ b/packages/cli/test/integration/workflows/workflows.controller.test.ts @@ -1321,6 +1321,7 @@ describe('GET /workflows?includeFolders=true', () => { }, parentFolder: null, workflowCount: 0, + subFolderCount: 0, }), ]), });