fix(core): Return correct trigger count for nodes with multiple webhooks (#14300)

Co-authored-by: Danny Martini <danny@n8n.io>
Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <aditya@netroy.in>
This commit is contained in:
Marc Littlemore
2025-04-04 14:15:10 +01:00
committed by GitHub
parent 480b44d024
commit 39e2d35a71
4 changed files with 82 additions and 14 deletions

View File

@@ -673,10 +673,23 @@ export class ActiveWorkflowManager {
const triggerFilter = (nodeType: INodeType) =>
!!nodeType.trigger && !nodeType.description.name.includes('manualTrigger');
// Retrieve unique webhooks as some nodes have multiple webhooks
const workflowWebhooks = WebhookHelpers.getWorkflowWebhooks(
workflow,
additionalData,
undefined,
true,
);
const uniqueWebhooks = workflowWebhooks.reduce<Set<string>>((acc, webhook) => {
acc.add(webhook.node);
return acc;
}, new Set());
return (
workflow.queryNodes(triggerFilter).length +
workflow.getPollNodes().length +
WebhookHelpers.getWorkflowWebhooks(workflow, additionalData, undefined, true).length
uniqueWebhooks.size
);
}

View File

@@ -1,12 +1,20 @@
import { Container } from '@n8n/di';
import { mock } from 'jest-mock-extended';
import { Logger } from 'n8n-core';
import { FormTrigger } from 'n8n-nodes-base/nodes/Form/FormTrigger.node';
import { ScheduleTrigger } from 'n8n-nodes-base/nodes/Schedule/ScheduleTrigger.node';
import { NodeApiError, Workflow } from 'n8n-workflow';
import type { IWebhookData, IWorkflowBase, WorkflowActivateMode } from 'n8n-workflow';
import type {
IWebhookData,
IWorkflowBase,
WorkflowActivateMode,
INodeTypeData,
} from 'n8n-workflow';
import { ActiveExecutions } from '@/active-executions';
import { ActiveWorkflowManager } from '@/active-workflow-manager';
import type { WebhookEntity } from '@/databases/entities/webhook-entity';
import { WorkflowRepository } from '@/databases/repositories/workflow.repository';
import { ExecutionService } from '@/executions/execution.service';
import { ExternalHooks } from '@/external-hooks';
import { NodeTypes } from '@/node-types';
@@ -43,7 +51,18 @@ beforeAll(async () => {
activeWorkflowManager = Container.get(ActiveWorkflowManager);
await utils.initNodeTypes();
const nodes: INodeTypeData = {
'n8n-nodes-base.scheduleTrigger': {
type: new ScheduleTrigger(),
sourcePath: '',
},
'n8n-nodes-base.formTrigger': {
type: new FormTrigger(),
sourcePath: '',
},
};
await utils.initNodeTypes(nodes);
const owner = await createOwner();
createActiveWorkflow = async () => await createWorkflow({ active: true }, owner);
@@ -136,6 +155,43 @@ describe('add()', () => {
},
);
});
test('should count workflow triggers correctly when node has multiple webhooks', async () => {
const workflowRepositoryInstance = Container.get(WorkflowRepository);
const updateWorkflowTriggerCountSpy = jest.spyOn(
workflowRepositoryInstance,
'updateWorkflowTriggerCount',
);
await activeWorkflowManager.init();
// Mock all of the webhooks
const mockWebhooks: IWebhookData[] = [
mock<IWebhookData>({ node: 'Form Trigger', httpMethod: 'GET', path: '/webhook-path' }),
mock<IWebhookData>({ node: 'Form Trigger', httpMethod: 'POST', path: '/webhook-path' }),
];
webhookService.getNodeWebhooks.mockReturnValue(mockWebhooks);
webhookService.createWebhook.mockReturnValue(
mock<WebhookEntity>({ webhookPath: '/webhook-path' }),
);
// Create a workflow which has a form trigger
const dbWorkflow = await createWorkflow({
nodes: [
{
id: 'uuid-1',
parameters: { path: 'test-webhook-path', options: {} },
name: 'Form Trigger',
type: 'n8n-nodes-base.formTrigger',
typeVersion: 1,
position: [500, 300],
},
],
});
await activeWorkflowManager.add(dbWorkflow.id, 'activate');
expect(updateWorkflowTriggerCountSpy).toHaveBeenCalledWith(dbWorkflow.id, 1);
});
});
describe('removeAll()', () => {

View File

@@ -9,6 +9,7 @@ import {
import { Ftp } from 'n8n-nodes-base/credentials/Ftp.credentials';
import { GithubApi } from 'n8n-nodes-base/credentials/GithubApi.credentials';
import { Cron } from 'n8n-nodes-base/nodes/Cron/Cron.node';
import { FormTrigger } from 'n8n-nodes-base/nodes/Form/FormTrigger.node';
import { ScheduleTrigger } from 'n8n-nodes-base/nodes/Schedule/ScheduleTrigger.node';
import { Set } from 'n8n-nodes-base/nodes/Set/Set.node';
import { Start } from 'n8n-nodes-base/nodes/Start/Start.node';
@@ -67,9 +68,8 @@ export async function initCredentialsTypes(): Promise<void> {
/**
* Initialize node types.
*/
export async function initNodeTypes() {
ScheduleTrigger.prototype.trigger = async () => ({});
const nodes: INodeTypeData = {
export async function initNodeTypes(customNodes?: INodeTypeData) {
const defaultNodes: INodeTypeData = {
'n8n-nodes-base.start': {
type: new Start(),
sourcePath: '',
@@ -86,7 +86,14 @@ export async function initNodeTypes() {
type: new ScheduleTrigger(),
sourcePath: '',
},
'n8n-nodes-base.formTrigger': {
type: new FormTrigger(),
sourcePath: '',
},
};
ScheduleTrigger.prototype.trigger = async () => ({});
const nodes = customNodes ?? defaultNodes;
const loader = mock<DirectoryLoader>();
loader.getNode.mockImplementation((nodeType) => {
const node = nodes[`n8n-nodes-base.${nodeType}`];

View File

@@ -47,14 +47,6 @@ function getNodeVersion(Trigger: new () => VersionedNodeType, version?: number)
return instance.nodeVersions[version ?? instance.currentVersion];
}
export async function testVersionedTriggerNode(
Trigger: new () => VersionedNodeType,
version?: number,
options: TestTriggerNodeOptions = {},
) {
return await testTriggerNode(getNodeVersion(Trigger, version), options);
}
export async function testTriggerNode(
Trigger: (new () => INodeType) | INodeType,
options: TestTriggerNodeOptions = {},