filter by http node url

This commit is contained in:
Danny Martini 2024-10-30 16:00:37 +01:00
parent 97109b0069
commit 7a072ac729
No known key found for this signature in database
8 changed files with 127 additions and 6 deletions

View file

@ -103,6 +103,9 @@ export class WorkflowEntity extends WithTimestampsAndStringId implements IWorkfl
@Column({ type: 'simple-array', default: '' }) @Column({ type: 'simple-array', default: '' })
webhookURLs: string[]; webhookURLs: string[];
@Column({ type: 'simple-array', default: '' })
httpNodeURLs: string[];
} }
/** /**

View file

@ -7,6 +7,7 @@ export class AddWorkflowFilterColumns1730286483664 implements ReversibleMigratio
column('nodeTypes').text, column('nodeTypes').text,
column('nodeNames').text, column('nodeNames').text,
column('webhookURLs').text, column('webhookURLs').text,
column('httpNodeURLs').text,
]); ]);
} }
@ -16,6 +17,7 @@ export class AddWorkflowFilterColumns1730286483664 implements ReversibleMigratio
'nodeTypes', 'nodeTypes',
'nodeNames', 'nodeNames',
'webhookURLs', 'webhookURLs',
'httpNodeURLs',
]); ]);
} }
} }

View file

@ -105,6 +105,7 @@ export class WorkflowRepository extends Repository<WorkflowEntity> {
nodeTypes: string[] = [], nodeTypes: string[] = [],
nodeName = '', nodeName = '',
webhookURL = '', webhookURL = '',
httpNodeURL = '',
) { ) {
if (sharedWorkflowIds.length === 0) return { workflows: [], count: 0 }; if (sharedWorkflowIds.length === 0) return { workflows: [], count: 0 };
@ -173,6 +174,10 @@ export class WorkflowRepository extends Repository<WorkflowEntity> {
where.webhookURLs = Like(`%${escapeCommas(webhookURL)}%`); where.webhookURLs = Like(`%${escapeCommas(webhookURL)}%`);
} }
if (httpNodeURL) {
where.httpNodeURLs = Like(`%${escapeCommas(httpNodeURL)}%`);
}
const findManyOptions: FindManyOptions<WorkflowEntity> = { const findManyOptions: FindManyOptions<WorkflowEntity> = {
select: { ...select, id: true }, select: { ...select, id: true },
where, where,

View file

@ -61,3 +61,19 @@ export function getEscapedWebhookURLs(workflow: WorkflowEntity): string[] {
return webhookURLs; 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;
}

View file

@ -37,6 +37,7 @@ export declare namespace WorkflowRequest {
nodeTypes?: string; nodeTypes?: string;
nodeName?: string; nodeName?: string;
webhookURL?: string; webhookURL?: string;
httpNodeURL?: string;
} }
> & { > & {
listQueryOptions: ListQuery.Options; listQueryOptions: ListQuery.Options;

View file

@ -36,6 +36,7 @@ import * as WorkflowHelpers from '@/workflow-helpers';
import { import {
getEncodedCredentialIds, getEncodedCredentialIds,
getEncodedNodeTypes, getEncodedNodeTypes,
getEscapedHttpNodeURLs,
getEscapedNodeNames, getEscapedNodeNames,
getEscapedWebhookURLs, getEscapedWebhookURLs,
} from './utils'; } from './utils';
@ -71,6 +72,7 @@ export class WorkflowService {
nodeTypes: string[] = [], nodeTypes: string[] = [],
nodeName = '', nodeName = '',
webhookURL = '', webhookURL = '',
httpNodeURL = '',
) { ) {
const sharedWorkflowIds = await this.workflowSharingService.getSharedWorkflowIds(user, { const sharedWorkflowIds = await this.workflowSharingService.getSharedWorkflowIds(user, {
scopes: ['workflow:read'], scopes: ['workflow:read'],
@ -84,6 +86,7 @@ export class WorkflowService {
nodeTypes, nodeTypes,
nodeName, nodeName,
webhookURL, webhookURL,
httpNodeURL,
); );
if (hasSharing(workflows)) { if (hasSharing(workflows)) {
@ -212,6 +215,7 @@ export class WorkflowService {
nodeTypes: getEncodedNodeTypes(workflow), nodeTypes: getEncodedNodeTypes(workflow),
nodeNames: getEscapedNodeNames(workflow), nodeNames: getEscapedNodeNames(workflow),
webhookURLs: getEscapedWebhookURLs(workflow), webhookURLs: getEscapedWebhookURLs(workflow),
httpNodeURLs: getEscapedHttpNodeURLs(workflow),
}); });
if (tagIds && !config.getEnv('workflowTagsDisabled')) { if (tagIds && !config.getEnv('workflowTagsDisabled')) {

View file

@ -41,6 +41,7 @@ import * as WorkflowHelpers from '@/workflow-helpers';
import { import {
getEncodedCredentialIds, getEncodedCredentialIds,
getEncodedNodeTypes, getEncodedNodeTypes,
getEscapedHttpNodeURLs,
getEscapedNodeNames, getEscapedNodeNames,
getEscapedWebhookURLs, getEscapedWebhookURLs,
} from './utils'; } from './utils';
@ -125,6 +126,7 @@ export class WorkflowsController {
newWorkflow.nodeTypes = getEncodedNodeTypes(newWorkflow); newWorkflow.nodeTypes = getEncodedNodeTypes(newWorkflow);
newWorkflow.nodeNames = getEscapedNodeNames(newWorkflow); newWorkflow.nodeNames = getEscapedNodeNames(newWorkflow);
newWorkflow.webhookURLs = getEscapedWebhookURLs(newWorkflow); newWorkflow.webhookURLs = getEscapedWebhookURLs(newWorkflow);
newWorkflow.httpNodeURLs = getEscapedHttpNodeURLs(newWorkflow);
let project: Project | null; let project: Project | null;
const savedWorkflow = await Db.transaction(async (transactionManager) => { const savedWorkflow = await Db.transaction(async (transactionManager) => {
@ -213,6 +215,7 @@ export class WorkflowsController {
req.query.nodeTypes?.split(','), req.query.nodeTypes?.split(','),
req.query.nodeName, req.query.nodeName,
req.query.webhookURL, req.query.webhookURL,
req.query.httpNodeURL,
); );
res.json({ count, data }); res.json({ count, data });

View file

@ -111,7 +111,7 @@ describe('POST /workflows', () => {
expect(fromBase64(savedWorkflow.credentialIds[0])).toEqual(credential.id); expect(fromBase64(savedWorkflow.credentialIds[0])).toEqual(credential.id);
}); });
test('should populate `nodeTypes`', async () => { test('should populate `nodeNames`', async () => {
// ARRANGE // ARRANGE
const workflow = makeWorkflow(); const workflow = makeWorkflow();
workflow.nodes[0].name = 'Cron,with,commas'; workflow.nodes[0].name = 'Cron,with,commas';
@ -127,7 +127,7 @@ describe('POST /workflows', () => {
expect(savedWorkflow.nodeNames[0]).toEqual(escapeCommas(workflow.nodes[0].name)); expect(savedWorkflow.nodeNames[0]).toEqual(escapeCommas(workflow.nodes[0].name));
}); });
test('should populate `nodeTypes`', async () => { test('should populate `webhookURLs`', async () => {
// ARRANGE // ARRANGE
const workflow = makeWorkflow(); const workflow = makeWorkflow();
workflow.nodes[0].type = 'n8n-nodes-base.webhook'; 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)); 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 () => { test('should return scopes on created workflow', async () => {
const payload = { const payload = {
name: 'testing', name: 'testing',
@ -747,7 +764,7 @@ describe('GET /workflows', () => {
expect(found.usedCredentials).toBeUndefined(); expect(found.usedCredentials).toBeUndefined();
}); });
test('should return workflows filtered by node name', async () => { test('should return workflows filtered by webhook url', async () => {
// ARRANGE // ARRANGE
const node1 = { const node1 = {
id: uuid(), id: uuid(),
@ -762,15 +779,18 @@ describe('GET /workflows', () => {
owner, owner,
); );
const node2: INode = { const node2 = {
id: uuid(), id: uuid(),
name: 'Action Network', name: 'Action Network',
type: 'n8n-nodes-base.webhook', type: 'n8n-nodes-base.webhook',
parameters: { path: 'bar foo' }, parameters: { path: 'bar foo' },
typeVersion: 1, typeVersion: 1,
position: [0, 0], position: [0, 0],
}; } satisfies INode;
await createWorkflow({ name: 'Second', nodes: [node2] }, owner); await createWorkflow(
{ name: 'Second', nodes: [node2], webhookURLs: [escapeCommas(node2.parameters.path)] },
owner,
);
await createWorkflow({ name: 'Third' }, owner); await createWorkflow({ name: 'Third' }, owner);
@ -811,6 +831,73 @@ describe('GET /workflows', () => {
expect(found.usedCredentials).toBeUndefined(); 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 () => { test('should return workflows with scopes when ?includeScopes=true', async () => {
const [member1, member2] = await createManyUsers(2, { const [member1, member2] = await createManyUsers(2, {
role: 'global:member', role: 'global:member',