mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-18 02:21:13 +00:00
feat(core): Add subFolderCount to GET /workflows and /folders (no-changelog) (#13548)
This commit is contained in:
@@ -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 = [
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1321,6 +1321,7 @@ describe('GET /workflows?includeFolders=true', () => {
|
|||||||
},
|
},
|
||||||
parentFolder: null,
|
parentFolder: null,
|
||||||
workflowCount: 0,
|
workflowCount: 0,
|
||||||
|
subFolderCount: 0,
|
||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user