From 800f750aee5bd954b5f57647876ab0dc3719919b Mon Sep 17 00:00:00 2001 From: Danny Martini Date: Wed, 30 Oct 2024 13:28:01 +0100 Subject: [PATCH] filter by node types --- .../src/databases/entities/workflow-entity.ts | 3 + .../1730286483664-AddWorkflowFilterColumns.ts | 5 +- .../repositories/workflow.repository.ts | 5 + packages/cli/src/workflows/utils.ts | 10 ++ .../cli/src/workflows/workflow.request.ts | 2 +- .../cli/src/workflows/workflow.service.ts | 5 +- .../cli/src/workflows/workflows.controller.ts | 6 +- .../workflows/workflows.controller.test.ts | 103 +++++++++++++++++- 8 files changed, 130 insertions(+), 9 deletions(-) diff --git a/packages/cli/src/databases/entities/workflow-entity.ts b/packages/cli/src/databases/entities/workflow-entity.ts index 37173e54e5..a6aa4cc277 100644 --- a/packages/cli/src/databases/entities/workflow-entity.ts +++ b/packages/cli/src/databases/entities/workflow-entity.ts @@ -94,6 +94,9 @@ export class WorkflowEntity extends WithTimestampsAndStringId implements IWorkfl @Column({ type: 'simple-array', default: '' }) credentialIds: string[]; + + @Column({ type: 'simple-array', default: '' }) + nodeTypes: string[]; } /** diff --git a/packages/cli/src/databases/migrations/common/1730286483664-AddWorkflowFilterColumns.ts b/packages/cli/src/databases/migrations/common/1730286483664-AddWorkflowFilterColumns.ts index 3f19b07b97..0a9f4f8d84 100644 --- a/packages/cli/src/databases/migrations/common/1730286483664-AddWorkflowFilterColumns.ts +++ b/packages/cli/src/databases/migrations/common/1730286483664-AddWorkflowFilterColumns.ts @@ -2,11 +2,10 @@ import type { MigrationContext, ReversibleMigration } from '@/databases/types'; export class AddWorkflowFilterColumns1730286483664 implements ReversibleMigration { async up({ schemaBuilder: { addColumns, column } }: MigrationContext) { - console.log('foo'); - await addColumns('workflow_entity', [column('credentialIds').text]); + await addColumns('workflow_entity', [column('credentialIds').text, column('nodeTypes').text]); } async down({ schemaBuilder: { dropColumns } }: MigrationContext) { - await dropColumns('workflow_entity', ['credentialIds']); + await dropColumns('workflow_entity', ['credentialIds', 'nodeTypes']); } } diff --git a/packages/cli/src/databases/repositories/workflow.repository.ts b/packages/cli/src/databases/repositories/workflow.repository.ts index 2c90d44f4d..f26e4e63fc 100644 --- a/packages/cli/src/databases/repositories/workflow.repository.ts +++ b/packages/cli/src/databases/repositories/workflow.repository.ts @@ -101,6 +101,7 @@ export class WorkflowRepository extends Repository { sharedWorkflowIds: string[], options: ListQuery.Options = {}, credentialIds: string[] = [], + nodeTypes: string[] = [], ) { if (sharedWorkflowIds.length === 0) return { workflows: [], count: 0 }; @@ -157,6 +158,10 @@ export class WorkflowRepository extends Repository { where.credentialIds = Or(...credentialIds.map((id) => Like(`%${toBase64(id)}%`))); } + if (nodeTypes.length) { + where.nodeTypes = Or(...nodeTypes.map((id) => Like(`%${toBase64(id)}%`))); + } + 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 cce24c02ad..2dd1ba34c8 100644 --- a/packages/cli/src/workflows/utils.ts +++ b/packages/cli/src/workflows/utils.ts @@ -21,3 +21,13 @@ export function getEncodedCredentialIds(workflow: WorkflowEntity): string[] { return credentialIds; } + +export function getEncodedNodeTypes(workflow: WorkflowEntity): string[] { + const nodeTypes: string[] = []; + + for (const node of workflow.nodes) { + nodeTypes.push(toBase64(node.type)); + } + + return nodeTypes; +} diff --git a/packages/cli/src/workflows/workflow.request.ts b/packages/cli/src/workflows/workflow.request.ts index c4eb4cb8a4..2ced512200 100644 --- a/packages/cli/src/workflows/workflow.request.ts +++ b/packages/cli/src/workflows/workflow.request.ts @@ -32,7 +32,7 @@ export declare namespace WorkflowRequest { {}, {}, {}, - ListQuery.Params & { includeScopes?: string } & { credentialIds?: string } + ListQuery.Params & { includeScopes?: string } & { credentialIds?: string; nodeTypes?: string } > & { listQueryOptions: ListQuery.Options; }; diff --git a/packages/cli/src/workflows/workflow.service.ts b/packages/cli/src/workflows/workflow.service.ts index 17bdad9d9b..4f79c9c106 100644 --- a/packages/cli/src/workflows/workflow.service.ts +++ b/packages/cli/src/workflows/workflow.service.ts @@ -33,7 +33,7 @@ import { RoleService } from '@/services/role.service'; import { TagService } from '@/services/tag.service'; import * as WorkflowHelpers from '@/workflow-helpers'; -import { getEncodedCredentialIds } from './utils'; +import { getEncodedCredentialIds, getEncodedNodeTypes } from './utils'; import { WorkflowHistoryService } from './workflow-history/workflow-history.service.ee'; import { WorkflowSharingService } from './workflow-sharing.service'; @@ -63,6 +63,7 @@ export class WorkflowService { options?: ListQuery.Options, includeScopes = false, credentialIds: string[] = [], + nodeTypes: string[] = [], ) { const sharedWorkflowIds = await this.workflowSharingService.getSharedWorkflowIds(user, { scopes: ['workflow:read'], @@ -73,6 +74,7 @@ export class WorkflowService { sharedWorkflowIds, options, credentialIds, + nodeTypes, ); if (hasSharing(workflows)) { @@ -198,6 +200,7 @@ export class WorkflowService { 'versionId', ]), credentialIds: getEncodedCredentialIds(workflow), + nodeTypes: getEncodedNodeTypes(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 c7ab6eaeff..8158ca382e 100644 --- a/packages/cli/src/workflows/workflows.controller.ts +++ b/packages/cli/src/workflows/workflows.controller.ts @@ -3,7 +3,7 @@ import { GlobalConfig } from '@n8n/config'; import { In, type FindOptionsRelations } from '@n8n/typeorm'; import axios from 'axios'; import express from 'express'; -import { ApplicationError, createEnvProvider } from 'n8n-workflow'; +import { ApplicationError } from 'n8n-workflow'; import { v4 as uuid } from 'uuid'; import { z } from 'zod'; @@ -38,13 +38,13 @@ import { UserManagementMailer } from '@/user-management/email'; import * as utils from '@/utils'; import * as WorkflowHelpers from '@/workflow-helpers'; +import { getEncodedCredentialIds, getEncodedNodeTypes } from './utils'; import { WorkflowExecutionService } from './workflow-execution.service'; import { WorkflowHistoryService } from './workflow-history/workflow-history.service.ee'; import { WorkflowRequest } from './workflow.request'; import { WorkflowService } from './workflow.service'; import { EnterpriseWorkflowService } from './workflow.service.ee'; import { CredentialsService } from '../credentials/credentials.service'; -import { getEncodedCredentialIds } from './utils'; @RestController('/workflows') export class WorkflowsController { @@ -117,6 +117,7 @@ export class WorkflowsController { } newWorkflow.credentialIds = getEncodedCredentialIds(newWorkflow); + newWorkflow.nodeTypes = getEncodedNodeTypes(newWorkflow); let project: Project | null; const savedWorkflow = await Db.transaction(async (transactionManager) => { @@ -202,6 +203,7 @@ export class WorkflowsController { req.listQueryOptions, !!req.query.includeScopes, req.query.credentialIds?.split(','), + req.query.nodeTypes?.split(','), ); 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 c928e69c54..a1f1ebe83c 100644 --- a/packages/cli/test/integration/workflows/workflows.controller.test.ts +++ b/packages/cli/test/integration/workflows/workflows.controller.test.ts @@ -13,7 +13,7 @@ import { WorkflowRepository } from '@/databases/repositories/workflow.repository import { License } from '@/license'; import type { ListQuery } from '@/requests'; import { ProjectService } from '@/services/project.service'; -import { toBase64 } from '@/workflows/utils'; +import { fromBase64, toBase64 } from '@/workflows/utils'; import { EnterpriseWorkflowService } from '@/workflows/workflow.service.ee'; import { mockInstance } from '../../shared/mocking'; @@ -21,12 +21,16 @@ import { saveCredential } from '../shared/db/credentials'; import { createTeamProject, linkUserToProject } from '../shared/db/projects'; import { createTag } from '../shared/db/tags'; import { createManyUsers, createMember, createOwner } from '../shared/db/users'; -import { createWorkflow, shareWorkflowWithProjects } from '../shared/db/workflows'; +import { createWorkflow, getAllWorkflows, shareWorkflowWithProjects } from '../shared/db/workflows'; import { randomCredentialPayload } from '../shared/random'; import * as testDb from '../shared/test-db'; import type { SuperAgentTest } from '../shared/types'; import * as utils from '../shared/utils/'; import { makeWorkflow, MOCK_PINDATA } from '../shared/utils/'; +import { getWorkflow } from '@test-integration/workflow'; +import { getWorkflowById } from '@/public-api/v1/handlers/workflows/workflows.service'; + +import * as a from 'assert/strict'; let owner: User; let member: User; @@ -78,6 +82,37 @@ describe('POST /workflows', () => { expect(pinData).toBeNull(); }); + test('should populate `nodeTypes`', async () => { + // ARRANGE + const workflow = makeWorkflow(); + + // ACT + await authOwnerAgent.post('/workflows').send(workflow).expect(200); + + // ASSERT + const savedWorkflow = await getWorkflowById(workflow.id); + a.ok(savedWorkflow); + + expect(savedWorkflow.nodeTypes).toHaveLength(1); + expect(fromBase64(savedWorkflow?.nodeTypes[0])).toEqual(workflow.nodes[0].type); + }); + + test('should populate `credentialIds`', async () => { + // ARRANGE + const credential = { id: '1', name: 'cred1' }; + const workflow = makeWorkflow({ withPinData: false, withCredential: credential }); + + // ACT + await authOwnerAgent.post('/workflows').send(workflow).expect(200); + + // ASSERT + const savedWorkflow = await getWorkflowById(workflow.id); + a.ok(savedWorkflow); + + expect(savedWorkflow.credentialIds).toHaveLength(1); + expect(fromBase64(savedWorkflow?.credentialIds[0])).toEqual(credential.id); + }); + test('should return scopes on created workflow', async () => { const payload = { name: 'testing', @@ -553,6 +588,70 @@ describe('GET /workflows', () => { expect(found.usedCredentials).toBeUndefined(); }); + test('should return workflows filtered by used node types', async () => { + // ARRANGE + const node1: INode = { + id: uuid(), + name: 'Action Network', + type: 'n8n-nodes-base.actionNetwork', + parameters: {}, + typeVersion: 1, + position: [0, 0], + }; + const workflow1 = await createWorkflow( + { name: 'First', nodes: [node1], nodeTypes: [toBase64(node1.type)] }, + owner, + ); + + const node2: INode = { + id: uuid(), + name: 'Action Network', + type: 'n8n-nodes-base.airTable', + parameters: {}, + 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(`nodeTypes=${node1.type}`) + .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',