feat(core): Add subFolderCount to GET /workflows and /folders (no-changelog) (#13548)

This commit is contained in:
Ricardo Espinoza
2025-03-12 13:33:37 +01:00
committed by GitHub
parent d2df154b49
commit 0066bf890f
6 changed files with 59 additions and 19 deletions

View File

@@ -11,6 +11,7 @@ const VALID_SELECT_FIELDS = [
'tags', 'tags',
'parentFolder', 'parentFolder',
'workflowCount', 'workflowCount',
'subFolderCount',
] as const; ] as const;
const VALID_SORT_OPTIONS = [ const VALID_SORT_OPTIONS = [

View File

@@ -13,8 +13,9 @@ import { Project } from './project';
import { TagEntity } from './tag-entity'; import { TagEntity } from './tag-entity';
import { type WorkflowEntity } from './workflow-entity'; import { type WorkflowEntity } from './workflow-entity';
export type FolderWithWorkflowCount = Folder & { export type FolderWithWorkflowAndSubFolderCount = Folder & {
workflowCount: boolean; workflowCount: boolean;
subFolderCount: number;
}; };
@Entity() @Entity()
@@ -26,6 +27,12 @@ export class Folder extends WithTimestampsAndStringId {
@JoinColumn({ name: 'parentFolderId' }) @JoinColumn({ name: 'parentFolderId' })
parentFolder: Folder | null; parentFolder: Folder | null;
@OneToMany(
() => Folder,
(folder) => folder.parentFolder,
)
subFolders: Folder[];
@ManyToOne(() => Project) @ManyToOne(() => Project)
@JoinColumn({ name: 'projectId' }) @JoinColumn({ name: 'projectId' })
homeProject: Project; homeProject: Project;

View File

@@ -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 () => { it('should return timestamps when specified', async () => {
const [folders] = await folderRepository.getManyAndCount({ const [folders] = await folderRepository.getManyAndCount({
select: { select: {
@@ -400,6 +418,7 @@ describe('FolderRepository', () => {
icon: null, icon: null,
}, },
workflowCount: expect.any(Number), workflowCount: expect.any(Number),
subFolderCount: expect.any(Number),
tags: expect.any(Array), tags: expect.any(Array),
}); });
}); });

View File

@@ -5,30 +5,32 @@ import { PROJECT_ROOT } from 'n8n-workflow';
import type { ListQuery } from '@/requests'; import type { ListQuery } from '@/requests';
import type { FolderWithWorkflowCount } from '../entities/folder'; import type { FolderWithWorkflowAndSubFolderCount } from '../entities/folder';
import { Folder } from '../entities/folder'; import { Folder } from '../entities/folder';
import { FolderTagMapping } from '../entities/folder-tag-mapping'; import { FolderTagMapping } from '../entities/folder-tag-mapping';
import { TagEntity } from '../entities/tag-entity'; import { TagEntity } from '../entities/tag-entity';
@Service() @Service()
export class FolderRepository extends Repository<FolderWithWorkflowCount> { export class FolderRepository extends Repository<FolderWithWorkflowAndSubFolderCount> {
constructor(dataSource: DataSource) { constructor(dataSource: DataSource) {
super(Folder, dataSource.manager); super(Folder, dataSource.manager);
} }
async getManyAndCount( async getManyAndCount(
options: ListQuery.Options = {}, options: ListQuery.Options = {},
): Promise<[FolderWithWorkflowCount[], number]> { ): Promise<[FolderWithWorkflowAndSubFolderCount[], number]> {
const query = this.getManyQuery(options); const query = this.getManyQuery(options);
return await query.getManyAndCount(); return await query.getManyAndCount();
} }
async getMany(options: ListQuery.Options = {}): Promise<FolderWithWorkflowCount[]> { async getMany(options: ListQuery.Options = {}): Promise<FolderWithWorkflowAndSubFolderCount[]> {
const query = this.getManyQuery(options); const query = this.getManyQuery(options);
return await query.getMany(); return await query.getMany();
} }
getManyQuery(options: ListQuery.Options = {}): SelectQueryBuilder<FolderWithWorkflowCount> { getManyQuery(
options: ListQuery.Options = {},
): SelectQueryBuilder<FolderWithWorkflowAndSubFolderCount> {
const query = this.createQueryBuilder('folder'); const query = this.createQueryBuilder('folder');
this.applySelections(query, options.select); this.applySelections(query, options.select);
@@ -40,7 +42,7 @@ export class FolderRepository extends Repository<FolderWithWorkflowCount> {
} }
private applySelections( private applySelections(
query: SelectQueryBuilder<FolderWithWorkflowCount>, query: SelectQueryBuilder<FolderWithWorkflowAndSubFolderCount>,
select?: Record<string, boolean>, select?: Record<string, boolean>,
): void { ): void {
if (select) { if (select) {
@@ -50,12 +52,13 @@ export class FolderRepository extends Repository<FolderWithWorkflowCount> {
} }
} }
private applyDefaultSelect(query: SelectQueryBuilder<FolderWithWorkflowCount>): void { private applyDefaultSelect(query: SelectQueryBuilder<FolderWithWorkflowAndSubFolderCount>): void {
query query
.leftJoinAndSelect('folder.homeProject', 'homeProject') .leftJoinAndSelect('folder.homeProject', 'homeProject')
.leftJoinAndSelect('folder.parentFolder', 'parentFolder') .leftJoinAndSelect('folder.parentFolder', 'parentFolder')
.leftJoinAndSelect('folder.tags', 'tags') .leftJoinAndSelect('folder.tags', 'tags')
.loadRelationCountAndMap('folder.workflowCount', 'folder.workflows') .loadRelationCountAndMap('folder.workflowCount', 'folder.workflows')
.loadRelationCountAndMap('folder.subFolderCount', 'folder.subFolders')
.select([ .select([
'folder', 'folder',
...this.getProjectFields('homeProject'), ...this.getProjectFields('homeProject'),
@@ -65,7 +68,7 @@ export class FolderRepository extends Repository<FolderWithWorkflowCount> {
} }
private applyCustomSelect( private applyCustomSelect(
query: SelectQueryBuilder<FolderWithWorkflowCount>, query: SelectQueryBuilder<FolderWithWorkflowAndSubFolderCount>,
select?: Record<string, boolean>, select?: Record<string, boolean>,
): void { ): void {
const selections = ['folder.id']; const selections = ['folder.id'];
@@ -83,7 +86,7 @@ export class FolderRepository extends Repository<FolderWithWorkflowCount> {
} }
private addRelationFields( private addRelationFields(
query: SelectQueryBuilder<FolderWithWorkflowCount>, query: SelectQueryBuilder<FolderWithWorkflowAndSubFolderCount>,
selections: string[], selections: string[],
select?: Record<string, boolean>, select?: Record<string, boolean>,
): void { ): void {
@@ -105,6 +108,12 @@ export class FolderRepository extends Repository<FolderWithWorkflowCount> {
if (select?.workflowCount) { if (select?.workflowCount) {
query.loadRelationCountAndMap('folder.workflowCount', 'folder.workflows'); 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[] { private getProjectFields(alias: string): string[] {
@@ -120,7 +129,7 @@ export class FolderRepository extends Repository<FolderWithWorkflowCount> {
} }
private applyFilters( private applyFilters(
query: SelectQueryBuilder<FolderWithWorkflowCount>, query: SelectQueryBuilder<FolderWithWorkflowAndSubFolderCount>,
filter?: ListQuery.Options['filter'], filter?: ListQuery.Options['filter'],
): void { ): void {
if (!filter) return; if (!filter) return;
@@ -130,7 +139,7 @@ export class FolderRepository extends Repository<FolderWithWorkflowCount> {
} }
private applyBasicFilters( private applyBasicFilters(
query: SelectQueryBuilder<FolderWithWorkflowCount>, query: SelectQueryBuilder<FolderWithWorkflowAndSubFolderCount>,
filter: ListQuery.Options['filter'], filter: ListQuery.Options['filter'],
): void { ): void {
if (filter?.folderIds && Array.isArray(filter.folderIds)) { if (filter?.folderIds && Array.isArray(filter.folderIds)) {
@@ -163,7 +172,7 @@ export class FolderRepository extends Repository<FolderWithWorkflowCount> {
} }
private applyTagsFilter( private applyTagsFilter(
query: SelectQueryBuilder<FolderWithWorkflowCount>, query: SelectQueryBuilder<FolderWithWorkflowAndSubFolderCount>,
tags?: string[], tags?: string[],
): void { ): void {
if (!Array.isArray(tags) || tags.length === 0) return; if (!Array.isArray(tags) || tags.length === 0) return;
@@ -177,7 +186,7 @@ export class FolderRepository extends Repository<FolderWithWorkflowCount> {
} }
private createTagsSubQuery( private createTagsSubQuery(
query: SelectQueryBuilder<FolderWithWorkflowCount>, query: SelectQueryBuilder<FolderWithWorkflowAndSubFolderCount>,
tags: string[], tags: string[],
): SelectQueryBuilder<FolderTagMapping> { ): SelectQueryBuilder<FolderTagMapping> {
return query return query
@@ -192,7 +201,10 @@ export class FolderRepository extends Repository<FolderWithWorkflowCount> {
}); });
} }
private applySorting(query: SelectQueryBuilder<FolderWithWorkflowCount>, sortBy?: string): void { private applySorting(
query: SelectQueryBuilder<FolderWithWorkflowAndSubFolderCount>,
sortBy?: string,
): void {
if (!sortBy) { if (!sortBy) {
query.orderBy('folder.updatedAt', 'DESC'); query.orderBy('folder.updatedAt', 'DESC');
return; return;
@@ -208,7 +220,7 @@ export class FolderRepository extends Repository<FolderWithWorkflowCount> {
} }
private applySortingByField( private applySortingByField(
query: SelectQueryBuilder<FolderWithWorkflowCount>, query: SelectQueryBuilder<FolderWithWorkflowAndSubFolderCount>,
field: string, field: string,
direction: 'DESC' | 'ASC', direction: 'DESC' | 'ASC',
): void { ): void {
@@ -222,7 +234,7 @@ export class FolderRepository extends Repository<FolderWithWorkflowCount> {
} }
private applyPagination( private applyPagination(
query: SelectQueryBuilder<FolderWithWorkflowCount>, query: SelectQueryBuilder<FolderWithWorkflowAndSubFolderCount>,
options: ListQuery.Options, options: ListQuery.Options,
): void { ): void {
if (options?.take) { if (options?.take) {

View File

@@ -16,7 +16,7 @@ import type { ListQuery } from '@/requests';
import { isStringArray } from '@/utils'; import { isStringArray } from '@/utils';
import { FolderRepository } from './folder.repository'; 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 { TagEntity } from '../entities/tag-entity';
import { WebhookEntity } from '../entities/webhook-entity'; import { WebhookEntity } from '../entities/webhook-entity';
import { WorkflowEntity } from '../entities/workflow-entity'; import { WorkflowEntity } from '../entities/workflow-entity';
@@ -36,7 +36,7 @@ type WorkflowFolderUnionRow = {
export type WorkflowFolderUnionFull = ( export type WorkflowFolderUnionFull = (
| ListQuery.Workflow.Plain | ListQuery.Workflow.Plain
| ListQuery.Workflow.WithSharing | ListQuery.Workflow.WithSharing
| FolderWithWorkflowCount | FolderWithWorkflowAndSubFolderCount
) & { ) & {
resource: ResourceType; resource: ResourceType;
}; };

View File

@@ -1321,6 +1321,7 @@ describe('GET /workflows?includeFolders=true', () => {
}, },
parentFolder: null, parentFolder: null,
workflowCount: 0, workflowCount: 0,
subFolderCount: 0,
}), }),
]), ]),
}); });