feat(core): Update list folders endpoint to support filter excludeFolderIdAndDescendants (no-changelog) (#13880)

This commit is contained in:
Ricardo Espinoza
2025-03-13 09:56:12 -04:00
committed by GitHub
parent c646346c54
commit 03f70b5079
3 changed files with 75 additions and 0 deletions

View File

@@ -29,6 +29,7 @@ export const filterSchema = z
parentFolderId: z.string().optional(),
name: z.string().optional(),
tags: z.array(z.string()).optional(),
excludeFolderIdAndDescendants: z.string().optional(),
})
.strict();

View File

@@ -136,6 +136,13 @@ export class FolderRepository extends Repository<FolderWithWorkflowAndSubFolderC
this.applyBasicFilters(query, filter);
this.applyTagsFilter(query, Array.isArray(filter?.tags) ? filter.tags : undefined);
if (
filter?.excludeFolderIdAndDescendants &&
typeof filter.excludeFolderIdAndDescendants === 'string'
) {
this.applyExcludeFolderFilter(query, filter.excludeFolderIdAndDescendants);
}
}
private applyBasicFilters(
@@ -293,4 +300,38 @@ export class FolderRepository extends Repository<FolderWithWorkflowAndSubFolderC
},
);
}
private applyExcludeFolderFilter(
query: SelectQueryBuilder<FolderWithWorkflowAndSubFolderCount>,
excludeFolderIdAndDescendants: string,
): void {
// Exclude the specific folder by ID
query.andWhere('folder.id != :excludeFolderIdAndDescendants', {
excludeFolderIdAndDescendants,
});
// Use a WITH RECURSIVE CTE to find all child folders of the excluded folder
const baseQuery = this.createQueryBuilder('f')
.select('f.id', 'id')
.addSelect('f.parentFolderId', 'parentFolderId')
.where('f.id = :excludeFolderIdAndDescendants', { excludeFolderIdAndDescendants });
const recursiveQuery = this.createQueryBuilder('child')
.select('child.id', 'id')
.addSelect('child.parentFolderId', 'parentFolderId')
.innerJoin('folder_tree', 'parent', 'child.parentFolderId = parent.id');
const subQuery = this.createQueryBuilder()
.select('tree.id')
.addCommonTableExpression(
`${baseQuery.getQuery()} UNION ALL ${recursiveQuery.getQuery()}`,
'folder_tree',
{ recursive: true },
)
.from('folder_tree', 'tree')
.setParameters({ excludeFolderIdAndDescendants });
// Exclude all children of the specified folder
query.andWhere(`folder.id NOT IN (${subQuery.getQuery()})`);
}
}

View File

@@ -1014,6 +1014,39 @@ describe('GET /projects/:projectId/folders', () => {
expect(response.body.data[0].name).toBe('Folder 3');
});
test('should filter folders by excludeFolderIdAndDescendants', async () => {
const folder1 = await createFolder(ownerProject, { name: 'folder level 1' });
await createFolder(ownerProject, {
name: 'folder level 1.1',
parentFolder: folder1,
});
const folder12 = await createFolder(ownerProject, {
name: 'folder level 1.2',
parentFolder: folder1,
});
await createFolder(ownerProject, {
name: 'folder level 1.2.1',
parentFolder: folder12,
});
const folder122 = await createFolder(ownerProject, {
name: 'folder level 1.2.2',
parentFolder: folder12,
});
await createFolder(ownerProject, {
name: 'folder level 1.2.2.1',
parentFolder: folder122,
});
const response = await authOwnerAgent
.get(`/projects/${ownerProject.id}/folders`)
.query({ filter: `{ "excludeFolderIdAndDescendants": "${folder122.id}" }` });
expect(response.body.data.length).toBe(4);
expect(response.body.data.map((f: any) => f.name).sort()).toEqual(
['folder level 1', 'folder level 1.1', 'folder level 1.2.1', 'folder level 1.2'].sort(),
);
});
test('should apply pagination with take parameter', async () => {
// Create folders with consistent timestamps
for (let i = 1; i <= 5; i++) {