From 27c0cfbb8b2a84b22aa69769606d2e4cffaf0ee2 Mon Sep 17 00:00:00 2001 From: Danny Martini Date: Wed, 30 Oct 2024 15:30:30 +0100 Subject: [PATCH] filter by webhook urls --- .../src/databases/entities/workflow-entity.ts | 3 + .../1730286483664-AddWorkflowFilterColumns.ts | 8 +- .../repositories/workflow.repository.ts | 6 ++ packages/cli/src/workflows/utils.ts | 16 ++++ .../cli/src/workflows/workflow.request.ts | 1 + .../cli/src/workflows/workflow.service.ts | 10 ++- .../cli/src/workflows/workflows.controller.ts | 9 +- .../workflows/workflows.controller.test.ts | 83 ++++++++++++++++++- 8 files changed, 132 insertions(+), 4 deletions(-) diff --git a/packages/cli/src/databases/entities/workflow-entity.ts b/packages/cli/src/databases/entities/workflow-entity.ts index 1b4f066b8c..12dbf5e3fc 100644 --- a/packages/cli/src/databases/entities/workflow-entity.ts +++ b/packages/cli/src/databases/entities/workflow-entity.ts @@ -100,6 +100,9 @@ export class WorkflowEntity extends WithTimestampsAndStringId implements IWorkfl @Column({ type: 'simple-array', default: '' }) nodeNames: string[]; + + @Column({ type: 'simple-array', default: '' }) + webhookURLs: string[]; } /** diff --git a/packages/cli/src/databases/migrations/common/1730286483664-AddWorkflowFilterColumns.ts b/packages/cli/src/databases/migrations/common/1730286483664-AddWorkflowFilterColumns.ts index 031eda65ab..92b5e61941 100644 --- a/packages/cli/src/databases/migrations/common/1730286483664-AddWorkflowFilterColumns.ts +++ b/packages/cli/src/databases/migrations/common/1730286483664-AddWorkflowFilterColumns.ts @@ -6,10 +6,16 @@ export class AddWorkflowFilterColumns1730286483664 implements ReversibleMigratio column('credentialIds').text, column('nodeTypes').text, column('nodeNames').text, + column('webhookURLs').text, ]); } async down({ schemaBuilder: { dropColumns } }: MigrationContext) { - await dropColumns('workflow_entity', ['credentialIds', 'nodeTypes', 'nodeNames']); + await dropColumns('workflow_entity', [ + 'credentialIds', + 'nodeTypes', + 'nodeNames', + 'webhookURLs', + ]); } } diff --git a/packages/cli/src/databases/repositories/workflow.repository.ts b/packages/cli/src/databases/repositories/workflow.repository.ts index 821252b131..588627294b 100644 --- a/packages/cli/src/databases/repositories/workflow.repository.ts +++ b/packages/cli/src/databases/repositories/workflow.repository.ts @@ -97,12 +97,14 @@ export class WorkflowRepository extends Repository { .execute(); } + // eslint-disable-next-line complexity async getMany( sharedWorkflowIds: string[], options: ListQuery.Options = {}, credentialIds: string[] = [], nodeTypes: string[] = [], nodeName = '', + webhookURL = '', ) { if (sharedWorkflowIds.length === 0) return { workflows: [], count: 0 }; @@ -167,6 +169,10 @@ export class WorkflowRepository extends Repository { where.nodeNames = Like(`%${escapeCommas(nodeName)}%`); } + if (webhookURL) { + where.webhookURLs = Like(`%${escapeCommas(webhookURL)}%`); + } + const findManyOptions: FindManyOptions = { select: { ...select, id: true }, where, diff --git a/packages/cli/src/workflows/utils.ts b/packages/cli/src/workflows/utils.ts index e8e19bfbd5..a6f1a34d22 100644 --- a/packages/cli/src/workflows/utils.ts +++ b/packages/cli/src/workflows/utils.ts @@ -45,3 +45,19 @@ export function getEscapedNodeNames(workflow: WorkflowEntity): string[] { return nodeNames; } + +export function getEscapedWebhookURLs(workflow: WorkflowEntity): string[] { + const webhookURLs: string[] = []; + + for (const node of workflow.nodes) { + if ( + node.type === 'n8n-nodes-base.webhook' && + typeof node.parameters.path === 'string' && + node.parameters.path.length > 0 + ) { + webhookURLs.push(escapeCommas(node.parameters.path)); + } + } + + return webhookURLs; +} diff --git a/packages/cli/src/workflows/workflow.request.ts b/packages/cli/src/workflows/workflow.request.ts index d04dea9e4c..7c9a9b2af9 100644 --- a/packages/cli/src/workflows/workflow.request.ts +++ b/packages/cli/src/workflows/workflow.request.ts @@ -36,6 +36,7 @@ export declare namespace WorkflowRequest { credentialIds?: string; nodeTypes?: string; nodeName?: string; + webhookURL?: string; } > & { listQueryOptions: ListQuery.Options; diff --git a/packages/cli/src/workflows/workflow.service.ts b/packages/cli/src/workflows/workflow.service.ts index eba61abc4a..6af2ee095c 100644 --- a/packages/cli/src/workflows/workflow.service.ts +++ b/packages/cli/src/workflows/workflow.service.ts @@ -33,7 +33,12 @@ import { RoleService } from '@/services/role.service'; import { TagService } from '@/services/tag.service'; import * as WorkflowHelpers from '@/workflow-helpers'; -import { getEncodedCredentialIds, getEncodedNodeTypes, getEscapedNodeNames } from './utils'; +import { + getEncodedCredentialIds, + getEncodedNodeTypes, + getEscapedNodeNames, + getEscapedWebhookURLs, +} from './utils'; import { WorkflowHistoryService } from './workflow-history/workflow-history.service.ee'; import { WorkflowSharingService } from './workflow-sharing.service'; @@ -65,6 +70,7 @@ export class WorkflowService { credentialIds: string[] = [], nodeTypes: string[] = [], nodeName = '', + webhookURL = '', ) { const sharedWorkflowIds = await this.workflowSharingService.getSharedWorkflowIds(user, { scopes: ['workflow:read'], @@ -77,6 +83,7 @@ export class WorkflowService { credentialIds, nodeTypes, nodeName, + webhookURL, ); if (hasSharing(workflows)) { @@ -204,6 +211,7 @@ export class WorkflowService { credentialIds: getEncodedCredentialIds(workflow), nodeTypes: getEncodedNodeTypes(workflow), nodeNames: getEscapedNodeNames(workflow), + webhookURLs: getEscapedWebhookURLs(workflow), }); if (tagIds && !config.getEnv('workflowTagsDisabled')) { diff --git a/packages/cli/src/workflows/workflows.controller.ts b/packages/cli/src/workflows/workflows.controller.ts index 15097882bf..687357a23e 100644 --- a/packages/cli/src/workflows/workflows.controller.ts +++ b/packages/cli/src/workflows/workflows.controller.ts @@ -38,7 +38,12 @@ import { UserManagementMailer } from '@/user-management/email'; import * as utils from '@/utils'; import * as WorkflowHelpers from '@/workflow-helpers'; -import { getEncodedCredentialIds, getEncodedNodeTypes, getEscapedNodeNames } from './utils'; +import { + getEncodedCredentialIds, + getEncodedNodeTypes, + getEscapedNodeNames, + getEscapedWebhookURLs, +} from './utils'; import { WorkflowExecutionService } from './workflow-execution.service'; import { WorkflowHistoryService } from './workflow-history/workflow-history.service.ee'; import { WorkflowRequest } from './workflow.request'; @@ -119,6 +124,7 @@ export class WorkflowsController { newWorkflow.credentialIds = getEncodedCredentialIds(newWorkflow); newWorkflow.nodeTypes = getEncodedNodeTypes(newWorkflow); newWorkflow.nodeNames = getEscapedNodeNames(newWorkflow); + newWorkflow.webhookURLs = getEscapedWebhookURLs(newWorkflow); let project: Project | null; const savedWorkflow = await Db.transaction(async (transactionManager) => { @@ -206,6 +212,7 @@ export class WorkflowsController { req.query.credentialIds?.split(','), req.query.nodeTypes?.split(','), req.query.nodeName, + req.query.webhookURL, ); res.json({ count, data }); diff --git a/packages/cli/test/integration/workflows/workflows.controller.test.ts b/packages/cli/test/integration/workflows/workflows.controller.test.ts index 21bb2dd49f..5df1fe73bd 100644 --- a/packages/cli/test/integration/workflows/workflows.controller.test.ts +++ b/packages/cli/test/integration/workflows/workflows.controller.test.ts @@ -127,6 +127,23 @@ describe('POST /workflows', () => { expect(savedWorkflow.nodeNames[0]).toEqual(escapeCommas(workflow.nodes[0].name)); }); + test('should populate `nodeTypes`', async () => { + // ARRANGE + const workflow = makeWorkflow(); + workflow.nodes[0].type = 'n8n-nodes-base.webhook'; + workflow.nodes[0].parameters.path = 'foobar'; + + // ACT + await authOwnerAgent.post('/workflows').send(workflow).expect(200); + + // ASSERT + const savedWorkflow = await getWorkflowById(workflow.id); + a.ok(savedWorkflow); + + expect(savedWorkflow.webhookURLs).toHaveLength(1); + expect(savedWorkflow.webhookURLs[0]).toEqual(escapeCommas(workflow.nodes[0].parameters.path)); + }); + test('should return scopes on created workflow', async () => { const payload = { name: 'testing', @@ -666,7 +683,7 @@ describe('GET /workflows', () => { expect(found.usedCredentials).toBeUndefined(); }); - test('should return workflows filtered by used node types', async () => { + test('should return workflows filtered by node name', async () => { // ARRANGE const node1: INode = { id: uuid(), @@ -730,6 +747,70 @@ describe('GET /workflows', () => { expect(found.usedCredentials).toBeUndefined(); }); + test('should return workflows filtered by node name', async () => { + // ARRANGE + const node1 = { + id: uuid(), + name: 'Action Network', + type: 'n8n-nodes-base.webhook', + parameters: { path: 'foobar' }, + typeVersion: 1, + position: [0, 0], + } satisfies INode; + const workflow1 = await createWorkflow( + { name: 'First', nodes: [node1], webhookURLs: [escapeCommas(node1.parameters.path)] }, + owner, + ); + + const node2: INode = { + id: uuid(), + name: 'Action Network', + type: 'n8n-nodes-base.webhook', + parameters: { path: 'bar foo' }, + typeVersion: 1, + position: [0, 0], + }; + await createWorkflow({ name: 'Second', nodes: [node2] }, owner); + + await createWorkflow({ name: 'Third' }, owner); + + // ACT + const response = await authOwnerAgent + .get('/workflows') + .query(`webhookURL=${node1.parameters.path}`) + .expect(200); + + // ASSERT + const ownerPersonalProject = await projectRepository.getPersonalProjectForUserOrFail(owner.id); + expect(response.body.count).toBe(1); + expect(response.body.data).toHaveLength(1); + expect(response.body.data[0]).toEqual( + objectContaining({ + id: workflow1.id, + name: 'First', + active: workflow1.active, + tags: [], + createdAt: workflow1.createdAt.toISOString(), + updatedAt: workflow1.updatedAt.toISOString(), + versionId: workflow1.versionId, + homeProject: { + id: ownerPersonalProject.id, + name: owner.createPersonalProjectName(), + type: ownerPersonalProject.type, + }, + sharedWithProjects: [], + }), + ); + + const found = response.body.data.find( + (w: ListQuery.Workflow.WithOwnership) => w.name === 'First', + ); + + expect(found.nodes).toBeUndefined(); + expect(found.sharedWithProjects).toHaveLength(0); + expect(found.usedCredentials).toBeUndefined(); + }); + test('should return workflows with scopes when ?includeScopes=true', async () => { const [member1, member2] = await createManyUsers(2, { role: 'global:member',