From 7a072ac7294aeca8a58727abcb1c71a00bc8eeb1 Mon Sep 17 00:00:00 2001 From: Danny Martini Date: Wed, 30 Oct 2024 16:00:37 +0100 Subject: [PATCH] filter by http node url --- .../src/databases/entities/workflow-entity.ts | 3 + .../1730286483664-AddWorkflowFilterColumns.ts | 2 + .../repositories/workflow.repository.ts | 5 + packages/cli/src/workflows/utils.ts | 16 +++ .../cli/src/workflows/workflow.request.ts | 1 + .../cli/src/workflows/workflow.service.ts | 4 + .../cli/src/workflows/workflows.controller.ts | 3 + .../workflows/workflows.controller.test.ts | 99 +++++++++++++++++-- 8 files changed, 127 insertions(+), 6 deletions(-) diff --git a/packages/cli/src/databases/entities/workflow-entity.ts b/packages/cli/src/databases/entities/workflow-entity.ts index 12dbf5e3fc..53a8f7660a 100644 --- a/packages/cli/src/databases/entities/workflow-entity.ts +++ b/packages/cli/src/databases/entities/workflow-entity.ts @@ -103,6 +103,9 @@ export class WorkflowEntity extends WithTimestampsAndStringId implements IWorkfl @Column({ type: 'simple-array', default: '' }) webhookURLs: string[]; + + @Column({ type: 'simple-array', default: '' }) + httpNodeURLs: string[]; } /** diff --git a/packages/cli/src/databases/migrations/common/1730286483664-AddWorkflowFilterColumns.ts b/packages/cli/src/databases/migrations/common/1730286483664-AddWorkflowFilterColumns.ts index 92b5e61941..84ca31f2bd 100644 --- a/packages/cli/src/databases/migrations/common/1730286483664-AddWorkflowFilterColumns.ts +++ b/packages/cli/src/databases/migrations/common/1730286483664-AddWorkflowFilterColumns.ts @@ -7,6 +7,7 @@ export class AddWorkflowFilterColumns1730286483664 implements ReversibleMigratio column('nodeTypes').text, column('nodeNames').text, column('webhookURLs').text, + column('httpNodeURLs').text, ]); } @@ -16,6 +17,7 @@ export class AddWorkflowFilterColumns1730286483664 implements ReversibleMigratio 'nodeTypes', 'nodeNames', 'webhookURLs', + 'httpNodeURLs', ]); } } diff --git a/packages/cli/src/databases/repositories/workflow.repository.ts b/packages/cli/src/databases/repositories/workflow.repository.ts index 588627294b..b9bedd088d 100644 --- a/packages/cli/src/databases/repositories/workflow.repository.ts +++ b/packages/cli/src/databases/repositories/workflow.repository.ts @@ -105,6 +105,7 @@ export class WorkflowRepository extends Repository { nodeTypes: string[] = [], nodeName = '', webhookURL = '', + httpNodeURL = '', ) { if (sharedWorkflowIds.length === 0) return { workflows: [], count: 0 }; @@ -173,6 +174,10 @@ export class WorkflowRepository extends Repository { where.webhookURLs = Like(`%${escapeCommas(webhookURL)}%`); } + if (httpNodeURL) { + where.httpNodeURLs = Like(`%${escapeCommas(httpNodeURL)}%`); + } + 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 a6f1a34d22..315ab2ff66 100644 --- a/packages/cli/src/workflows/utils.ts +++ b/packages/cli/src/workflows/utils.ts @@ -61,3 +61,19 @@ export function getEscapedWebhookURLs(workflow: WorkflowEntity): string[] { return webhookURLs; } + +export function getEscapedHttpNodeURLs(workflow: WorkflowEntity): string[] { + const webhookURLs: string[] = []; + + for (const node of workflow.nodes) { + if ( + node.type === 'n8n-nodes-base.httpRequest' && + typeof node.parameters.url === 'string' && + node.parameters.url.length > 0 + ) { + webhookURLs.push(escapeCommas(node.parameters.url)); + } + } + + return webhookURLs; +} diff --git a/packages/cli/src/workflows/workflow.request.ts b/packages/cli/src/workflows/workflow.request.ts index 7c9a9b2af9..1b6c2dcda2 100644 --- a/packages/cli/src/workflows/workflow.request.ts +++ b/packages/cli/src/workflows/workflow.request.ts @@ -37,6 +37,7 @@ export declare namespace WorkflowRequest { nodeTypes?: string; nodeName?: string; webhookURL?: string; + httpNodeURL?: string; } > & { listQueryOptions: ListQuery.Options; diff --git a/packages/cli/src/workflows/workflow.service.ts b/packages/cli/src/workflows/workflow.service.ts index 6af2ee095c..3fc68ecf12 100644 --- a/packages/cli/src/workflows/workflow.service.ts +++ b/packages/cli/src/workflows/workflow.service.ts @@ -36,6 +36,7 @@ import * as WorkflowHelpers from '@/workflow-helpers'; import { getEncodedCredentialIds, getEncodedNodeTypes, + getEscapedHttpNodeURLs, getEscapedNodeNames, getEscapedWebhookURLs, } from './utils'; @@ -71,6 +72,7 @@ export class WorkflowService { nodeTypes: string[] = [], nodeName = '', webhookURL = '', + httpNodeURL = '', ) { const sharedWorkflowIds = await this.workflowSharingService.getSharedWorkflowIds(user, { scopes: ['workflow:read'], @@ -84,6 +86,7 @@ export class WorkflowService { nodeTypes, nodeName, webhookURL, + httpNodeURL, ); if (hasSharing(workflows)) { @@ -212,6 +215,7 @@ export class WorkflowService { nodeTypes: getEncodedNodeTypes(workflow), nodeNames: getEscapedNodeNames(workflow), webhookURLs: getEscapedWebhookURLs(workflow), + httpNodeURLs: getEscapedHttpNodeURLs(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 687357a23e..1a500ed60c 100644 --- a/packages/cli/src/workflows/workflows.controller.ts +++ b/packages/cli/src/workflows/workflows.controller.ts @@ -41,6 +41,7 @@ import * as WorkflowHelpers from '@/workflow-helpers'; import { getEncodedCredentialIds, getEncodedNodeTypes, + getEscapedHttpNodeURLs, getEscapedNodeNames, getEscapedWebhookURLs, } from './utils'; @@ -125,6 +126,7 @@ export class WorkflowsController { newWorkflow.nodeTypes = getEncodedNodeTypes(newWorkflow); newWorkflow.nodeNames = getEscapedNodeNames(newWorkflow); newWorkflow.webhookURLs = getEscapedWebhookURLs(newWorkflow); + newWorkflow.httpNodeURLs = getEscapedHttpNodeURLs(newWorkflow); let project: Project | null; const savedWorkflow = await Db.transaction(async (transactionManager) => { @@ -213,6 +215,7 @@ export class WorkflowsController { req.query.nodeTypes?.split(','), req.query.nodeName, req.query.webhookURL, + req.query.httpNodeURL, ); 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 5df1fe73bd..8642f3c7c3 100644 --- a/packages/cli/test/integration/workflows/workflows.controller.test.ts +++ b/packages/cli/test/integration/workflows/workflows.controller.test.ts @@ -111,7 +111,7 @@ describe('POST /workflows', () => { expect(fromBase64(savedWorkflow.credentialIds[0])).toEqual(credential.id); }); - test('should populate `nodeTypes`', async () => { + test('should populate `nodeNames`', async () => { // ARRANGE const workflow = makeWorkflow(); workflow.nodes[0].name = 'Cron,with,commas'; @@ -127,7 +127,7 @@ describe('POST /workflows', () => { expect(savedWorkflow.nodeNames[0]).toEqual(escapeCommas(workflow.nodes[0].name)); }); - test('should populate `nodeTypes`', async () => { + test('should populate `webhookURLs`', async () => { // ARRANGE const workflow = makeWorkflow(); workflow.nodes[0].type = 'n8n-nodes-base.webhook'; @@ -144,6 +144,23 @@ describe('POST /workflows', () => { expect(savedWorkflow.webhookURLs[0]).toEqual(escapeCommas(workflow.nodes[0].parameters.path)); }); + test('should populate `httpNodeURLs`', async () => { + // ARRANGE + const workflow = makeWorkflow(); + workflow.nodes[0].type = 'n8n-nodes-base.httpRequest'; + workflow.nodes[0].parameters.url = 'foobar'; + + // ACT + await authOwnerAgent.post('/workflows').send(workflow).expect(200); + + // ASSERT + const savedWorkflow = await getWorkflowById(workflow.id); + a.ok(savedWorkflow); + + expect(savedWorkflow.httpNodeURLs).toHaveLength(1); + expect(savedWorkflow.httpNodeURLs[0]).toEqual(escapeCommas(workflow.nodes[0].parameters.url)); + }); + test('should return scopes on created workflow', async () => { const payload = { name: 'testing', @@ -747,7 +764,7 @@ describe('GET /workflows', () => { expect(found.usedCredentials).toBeUndefined(); }); - test('should return workflows filtered by node name', async () => { + test('should return workflows filtered by webhook url', async () => { // ARRANGE const node1 = { id: uuid(), @@ -762,15 +779,18 @@ describe('GET /workflows', () => { owner, ); - const node2: INode = { + const node2 = { 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); + } satisfies INode; + await createWorkflow( + { name: 'Second', nodes: [node2], webhookURLs: [escapeCommas(node2.parameters.path)] }, + owner, + ); await createWorkflow({ name: 'Third' }, owner); @@ -811,6 +831,73 @@ describe('GET /workflows', () => { expect(found.usedCredentials).toBeUndefined(); }); + test('should return workflows filtered by http node url', async () => { + // ARRANGE + const node1 = { + id: uuid(), + name: 'Action Network', + type: 'n8n-nodes-base.httpRequest', + parameters: { url: 'foobar' }, + typeVersion: 1, + position: [0, 0], + } satisfies INode; + const workflow1 = await createWorkflow( + { name: 'First', nodes: [node1], httpNodeURLs: [escapeCommas(node1.parameters.url)] }, + owner, + ); + + const node2 = { + id: uuid(), + name: 'Action Network', + type: 'n8n-nodes-base.httpRequest', + parameters: { url: 'bar foo' }, + typeVersion: 1, + position: [0, 0], + } satisfies INode; + await createWorkflow( + { name: 'Second', nodes: [node2], httpNodeURLs: [escapeCommas(node2.parameters.url)] }, + owner, + ); + + await createWorkflow({ name: 'Third' }, owner); + + // ACT + const response = await authOwnerAgent + .get('/workflows') + .query(`httpNodeURL=${node1.parameters.url}`) + .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',