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',
'parentFolder',
'workflowCount',
'subFolderCount',
] as const;
const VALID_SORT_OPTIONS = [

View File

@@ -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;

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 () => {
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),
});
});

View File

@@ -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<FolderWithWorkflowCount> {
export class FolderRepository extends Repository<FolderWithWorkflowAndSubFolderCount> {
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<FolderWithWorkflowCount[]> {
async getMany(options: ListQuery.Options = {}): Promise<FolderWithWorkflowAndSubFolderCount[]> {
const query = this.getManyQuery(options);
return await query.getMany();
}
getManyQuery(options: ListQuery.Options = {}): SelectQueryBuilder<FolderWithWorkflowCount> {
getManyQuery(
options: ListQuery.Options = {},
): SelectQueryBuilder<FolderWithWorkflowAndSubFolderCount> {
const query = this.createQueryBuilder('folder');
this.applySelections(query, options.select);
@@ -40,7 +42,7 @@ export class FolderRepository extends Repository<FolderWithWorkflowCount> {
}
private applySelections(
query: SelectQueryBuilder<FolderWithWorkflowCount>,
query: SelectQueryBuilder<FolderWithWorkflowAndSubFolderCount>,
select?: Record<string, boolean>,
): void {
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
.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<FolderWithWorkflowCount> {
}
private applyCustomSelect(
query: SelectQueryBuilder<FolderWithWorkflowCount>,
query: SelectQueryBuilder<FolderWithWorkflowAndSubFolderCount>,
select?: Record<string, boolean>,
): void {
const selections = ['folder.id'];
@@ -83,7 +86,7 @@ export class FolderRepository extends Repository<FolderWithWorkflowCount> {
}
private addRelationFields(
query: SelectQueryBuilder<FolderWithWorkflowCount>,
query: SelectQueryBuilder<FolderWithWorkflowAndSubFolderCount>,
selections: string[],
select?: Record<string, boolean>,
): void {
@@ -105,6 +108,12 @@ export class FolderRepository extends Repository<FolderWithWorkflowCount> {
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<FolderWithWorkflowCount> {
}
private applyFilters(
query: SelectQueryBuilder<FolderWithWorkflowCount>,
query: SelectQueryBuilder<FolderWithWorkflowAndSubFolderCount>,
filter?: ListQuery.Options['filter'],
): void {
if (!filter) return;
@@ -130,7 +139,7 @@ export class FolderRepository extends Repository<FolderWithWorkflowCount> {
}
private applyBasicFilters(
query: SelectQueryBuilder<FolderWithWorkflowCount>,
query: SelectQueryBuilder<FolderWithWorkflowAndSubFolderCount>,
filter: ListQuery.Options['filter'],
): void {
if (filter?.folderIds && Array.isArray(filter.folderIds)) {
@@ -163,7 +172,7 @@ export class FolderRepository extends Repository<FolderWithWorkflowCount> {
}
private applyTagsFilter(
query: SelectQueryBuilder<FolderWithWorkflowCount>,
query: SelectQueryBuilder<FolderWithWorkflowAndSubFolderCount>,
tags?: string[],
): void {
if (!Array.isArray(tags) || tags.length === 0) return;
@@ -177,7 +186,7 @@ export class FolderRepository extends Repository<FolderWithWorkflowCount> {
}
private createTagsSubQuery(
query: SelectQueryBuilder<FolderWithWorkflowCount>,
query: SelectQueryBuilder<FolderWithWorkflowAndSubFolderCount>,
tags: string[],
): SelectQueryBuilder<FolderTagMapping> {
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) {
query.orderBy('folder.updatedAt', 'DESC');
return;
@@ -208,7 +220,7 @@ export class FolderRepository extends Repository<FolderWithWorkflowCount> {
}
private applySortingByField(
query: SelectQueryBuilder<FolderWithWorkflowCount>,
query: SelectQueryBuilder<FolderWithWorkflowAndSubFolderCount>,
field: string,
direction: 'DESC' | 'ASC',
): void {
@@ -222,7 +234,7 @@ export class FolderRepository extends Repository<FolderWithWorkflowCount> {
}
private applyPagination(
query: SelectQueryBuilder<FolderWithWorkflowCount>,
query: SelectQueryBuilder<FolderWithWorkflowAndSubFolderCount>,
options: ListQuery.Options,
): void {
if (options?.take) {

View File

@@ -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;
};

View File

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