mirror of
https://github.com/n8n-io/n8n.git
synced 2026-05-29 15:57:00 +02:00
feat(editor): Update workflows list endpoint to support filter by node type (no-changelog) (#20158)
This commit is contained in:
parent
ac0e7e375f
commit
27e0320e41
|
|
@ -435,6 +435,7 @@ export class WorkflowRepository extends Repository<WorkflowEntity> {
|
|||
this.applyTagsFilter(qb, filter);
|
||||
this.applyProjectFilter(qb, filter);
|
||||
this.applyParentFolderFilter(qb, filter);
|
||||
this.applyNodeTypesFilter(qb, filter);
|
||||
this.applyAvailableInMCPFilter(qb, filter);
|
||||
}
|
||||
|
||||
|
|
@ -543,6 +544,22 @@ export class WorkflowRepository extends Repository<WorkflowEntity> {
|
|||
}
|
||||
}
|
||||
|
||||
private applyNodeTypesFilter(
|
||||
qb: SelectQueryBuilder<WorkflowEntity>,
|
||||
filter: ListQuery.Options['filter'],
|
||||
): void {
|
||||
const nodeTypes = isStringArray(filter?.nodeTypes) ? filter.nodeTypes : [];
|
||||
|
||||
if (!nodeTypes.length) return;
|
||||
|
||||
const { whereClause, parameters } = buildWorkflowsByNodesQuery(
|
||||
nodeTypes,
|
||||
this.globalConfig.database.type,
|
||||
);
|
||||
|
||||
qb.andWhere(whereClause, parameters);
|
||||
}
|
||||
|
||||
private applyOwnedByRelation(qb: SelectQueryBuilder<WorkflowEntity>): void {
|
||||
// Check if 'shared' join already exists from project filter
|
||||
if (!qb.expressionMap.aliases.find((alias) => alias.name === 'shared')) {
|
||||
|
|
|
|||
|
|
@ -40,6 +40,12 @@ export class WorkflowFilter extends BaseFilter {
|
|||
@Expose()
|
||||
availableInMCP?: boolean;
|
||||
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
@IsOptional()
|
||||
@Expose()
|
||||
nodeTypes?: string[];
|
||||
|
||||
static async fromString(rawFilter: string) {
|
||||
return await this.toFilter(rawFilter, WorkflowFilter);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ export class WorkflowSelect extends BaseSelect {
|
|||
'versionId',
|
||||
'ownedBy', // non-entity field
|
||||
'parentFolder',
|
||||
'nodes',
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,16 +18,11 @@ import {
|
|||
} from '@n8n/db';
|
||||
import { Container } from '@n8n/di';
|
||||
import type { Scope } from '@n8n/permissions';
|
||||
import { createFolder } from '@test-integration/db/folders';
|
||||
import { DateTime } from 'luxon';
|
||||
import { PROJECT_ROOT, type INode, type IPinData, type IWorkflowBase } from 'n8n-workflow';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { ActiveWorkflowManager } from '@/active-workflow-manager';
|
||||
import { License } from '@/license';
|
||||
import { ProjectService } from '@/services/project.service.ee';
|
||||
import { EnterpriseWorkflowService } from '@/workflows/workflow.service.ee';
|
||||
import { createFolder } from '@test-integration/db/folders';
|
||||
|
||||
import { saveCredential } from '../shared/db/credentials';
|
||||
import { assignTagToWorkflow, createTag } from '../shared/db/tags';
|
||||
import { createManyUsers, createMember, createOwner } from '../shared/db/users';
|
||||
|
|
@ -35,6 +30,11 @@ import type { SuperAgentTest } from '../shared/types';
|
|||
import * as utils from '../shared/utils/';
|
||||
import { makeWorkflow, MOCK_PINDATA } from '../shared/utils/';
|
||||
|
||||
import { ActiveWorkflowManager } from '@/active-workflow-manager';
|
||||
import { License } from '@/license';
|
||||
import { ProjectService } from '@/services/project.service.ee';
|
||||
import { EnterpriseWorkflowService } from '@/workflows/workflow.service.ee';
|
||||
|
||||
let owner: User;
|
||||
let member: User;
|
||||
let anotherMember: User;
|
||||
|
|
@ -1014,6 +1014,126 @@ describe('GET /workflows', () => {
|
|||
expect(response2.body.data).toHaveLength(1);
|
||||
expect(response2.body.data[0].id).toBe(workflow2.id);
|
||||
});
|
||||
|
||||
test('should filter workflows by nodeTypes', async () => {
|
||||
const httpWorkflow = await createWorkflow(
|
||||
{
|
||||
name: 'HTTP Workflow',
|
||||
nodes: [
|
||||
{
|
||||
id: uuid(),
|
||||
name: 'HTTP Request',
|
||||
type: 'n8n-nodes-base.httpRequest',
|
||||
parameters: {},
|
||||
typeVersion: 1,
|
||||
position: [0, 0],
|
||||
},
|
||||
],
|
||||
},
|
||||
owner,
|
||||
);
|
||||
|
||||
const slackWorkflow = await createWorkflow(
|
||||
{
|
||||
name: 'Slack Workflow',
|
||||
nodes: [
|
||||
{
|
||||
id: uuid(),
|
||||
name: 'Slack',
|
||||
type: 'n8n-nodes-base.slack',
|
||||
parameters: {},
|
||||
typeVersion: 1,
|
||||
position: [0, 0],
|
||||
},
|
||||
],
|
||||
},
|
||||
owner,
|
||||
);
|
||||
|
||||
const mixedWorkflow = await createWorkflow(
|
||||
{
|
||||
name: 'Mixed Workflow',
|
||||
nodes: [
|
||||
{
|
||||
id: uuid(),
|
||||
name: 'HTTP Request',
|
||||
type: 'n8n-nodes-base.httpRequest',
|
||||
parameters: {},
|
||||
typeVersion: 1,
|
||||
position: [0, 0],
|
||||
},
|
||||
{
|
||||
id: uuid(),
|
||||
name: 'Slack',
|
||||
type: 'n8n-nodes-base.slack',
|
||||
parameters: {},
|
||||
typeVersion: 1,
|
||||
position: [100, 0],
|
||||
},
|
||||
],
|
||||
},
|
||||
owner,
|
||||
);
|
||||
|
||||
// Filter by single node type
|
||||
const httpResponse = await authOwnerAgent
|
||||
.get('/workflows')
|
||||
.query('filter={ "nodeTypes": ["n8n-nodes-base.httpRequest"] }&select=["nodes"]')
|
||||
.expect(200);
|
||||
|
||||
expect(httpResponse.body.data).toHaveLength(2);
|
||||
const httpWorkflowIds = httpResponse.body.data.map((w: any) => w.id);
|
||||
expect(httpWorkflowIds).toContain(httpWorkflow.id);
|
||||
expect(httpWorkflowIds).toContain(mixedWorkflow.id);
|
||||
expect(httpResponse.body.data[0].nodes).toHaveLength(1);
|
||||
expect(httpResponse.body.data[1].nodes).toHaveLength(2);
|
||||
|
||||
// Filter by multiple node types (OR operation - returns workflows containing ANY of the specified node types)
|
||||
const multipleResponse = await authOwnerAgent
|
||||
.get('/workflows')
|
||||
.query('filter={ "nodeTypes": ["n8n-nodes-base.httpRequest", "n8n-nodes-base.slack"] }')
|
||||
.expect(200);
|
||||
|
||||
expect(multipleResponse.body.data).toHaveLength(3);
|
||||
const multipleWorkflowIds = multipleResponse.body.data.map((w: any) => w.id);
|
||||
expect(multipleWorkflowIds).toContain(httpWorkflow.id);
|
||||
expect(multipleWorkflowIds).toContain(slackWorkflow.id);
|
||||
expect(multipleWorkflowIds).toContain(mixedWorkflow.id);
|
||||
|
||||
// Filter by non-existent node type
|
||||
const emptyResponse = await authOwnerAgent
|
||||
.get('/workflows')
|
||||
.query('filter={ "nodeTypes": ["n8n-nodes-base.nonExistent"] }')
|
||||
.expect(200);
|
||||
|
||||
expect(emptyResponse.body.data).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('should all workflows when filtering by empty nodeTypes array', async () => {
|
||||
await createWorkflow(
|
||||
{
|
||||
name: 'Test Workflow',
|
||||
nodes: [
|
||||
{
|
||||
id: uuid(),
|
||||
name: 'Start',
|
||||
type: 'n8n-nodes-base.start',
|
||||
parameters: {},
|
||||
typeVersion: 1,
|
||||
position: [0, 0],
|
||||
},
|
||||
],
|
||||
},
|
||||
owner,
|
||||
);
|
||||
|
||||
const response = await authOwnerAgent
|
||||
.get('/workflows')
|
||||
.query('filter={ "nodeTypes": [] }')
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.data).toHaveLength(1); // Should return all workflows when nodeTypes is empty
|
||||
});
|
||||
});
|
||||
|
||||
describe('select', () => {
|
||||
|
|
@ -1877,6 +1997,60 @@ describe('GET /workflows?includeFolders=true', () => {
|
|||
expect(response.body.data[1].id).toBe(workflow.id);
|
||||
expect(response.body.data[1].homeProject).not.toBeNull();
|
||||
});
|
||||
|
||||
test('should filter workflows and folders by nodeTypes', async () => {
|
||||
const pp = await getPersonalProject(owner);
|
||||
|
||||
const httpWorkflow = await createWorkflow(
|
||||
{
|
||||
name: 'HTTP Workflow',
|
||||
nodes: [
|
||||
{
|
||||
id: uuid(),
|
||||
name: 'HTTP Request',
|
||||
type: 'n8n-nodes-base.httpRequest',
|
||||
parameters: {},
|
||||
typeVersion: 1,
|
||||
position: [0, 0],
|
||||
},
|
||||
],
|
||||
},
|
||||
owner,
|
||||
);
|
||||
|
||||
await createWorkflow(
|
||||
{
|
||||
name: 'Slack Workflow',
|
||||
nodes: [
|
||||
{
|
||||
id: uuid(),
|
||||
name: 'Slack',
|
||||
type: 'n8n-nodes-base.slack',
|
||||
parameters: {},
|
||||
typeVersion: 1,
|
||||
position: [0, 0],
|
||||
},
|
||||
],
|
||||
},
|
||||
owner,
|
||||
);
|
||||
|
||||
const folder = await createFolder(pp, { name: 'Test Folder' });
|
||||
|
||||
const response = await authOwnerAgent
|
||||
.get('/workflows')
|
||||
.query('filter={ "nodeTypes": ["n8n-nodes-base.httpRequest"] }&includeFolders=true')
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.data).toHaveLength(2); // 1 folder + 1 matching workflow
|
||||
const workflowItems = response.body.data.filter((item: any) => item.resource === 'workflow');
|
||||
const folderItems = response.body.data.filter((item: any) => item.resource === 'folder');
|
||||
|
||||
expect(workflowItems).toHaveLength(1);
|
||||
expect(workflowItems[0].id).toBe(httpWorkflow.id);
|
||||
expect(folderItems).toHaveLength(1);
|
||||
expect(folderItems[0].id).toBe(folder.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sortBy', () => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user