diff --git a/packages/cli/src/databases/repositories/workflow.repository.ts b/packages/cli/src/databases/repositories/workflow.repository.ts index 0317124472..d7266a5095 100644 --- a/packages/cli/src/databases/repositories/workflow.repository.ts +++ b/packages/cli/src/databases/repositories/workflow.repository.ts @@ -4,6 +4,7 @@ import { Repository, In, Like, + Or, type UpdateResult, type FindOptionsWhere, type FindOptionsSelect, @@ -12,6 +13,8 @@ import { } from '@n8n/typeorm'; import { Service } from 'typedi'; +import * as a from 'assert/strict'; + import config from '@/config'; import type { ListQuery } from '@/requests'; import { isStringArray } from '@/utils'; @@ -95,7 +98,11 @@ export class WorkflowRepository extends Repository { .execute(); } - async getMany(sharedWorkflowIds: string[], options?: ListQuery.Options) { + async getMany( + sharedWorkflowIds: string[], + options: ListQuery.Options = {}, + credentialIds: string[] = [], + ) { if (sharedWorkflowIds.length === 0) return { workflows: [], count: 0 }; if (typeof options?.filter?.projectId === 'string' && options.filter.projectId !== '') { @@ -147,8 +154,12 @@ export class WorkflowRepository extends Repository { where.name = Like(`%${where.name}%`); } + if (credentialIds.length) { + where.nodes = Or(...credentialIds.map((id) => Like(`%{"id":"${id}"%`))); + } + const findManyOptions: FindManyOptions = { - select: { ...select, id: true }, + select: { ...select, id: true, nodes: true }, where, }; @@ -165,11 +176,43 @@ export class WorkflowRepository extends Repository { findManyOptions.take = options.take; } - const [workflows, count] = (await this.findAndCount(findManyOptions)) as [ + let [workflows, count] = (await this.findAndCount(findManyOptions)) as [ ListQuery.Workflow.Plain[] | ListQuery.Workflow.WithSharing[], number, ]; + function workflowUsesCredential( + workflow: ListQuery.Workflow.Plain, + credentialIds: string[], + ): boolean { + a.ok(workflow.nodes); + + return ( + workflow.nodes.findIndex((node) => { + if (node.credentials) { + return ( + Object.values(node.credentials).findIndex((credential) => { + a.ok(credential.id); + return credentialIds.includes(credential.id); + }) !== -1 + ); + } else { + return false; + } + }) !== -1 + ); + } + + if (credentialIds.length) { + workflows = workflows.filter((wf) => workflowUsesCredential(wf, credentialIds)); + + count = workflows.length; + } + + for (const wf of workflows) { + delete wf.nodes; + } + return { workflows, count }; } diff --git a/packages/cli/src/requests.ts b/packages/cli/src/requests.ts index 4765ac1fad..46efbf2d5a 100644 --- a/packages/cli/src/requests.ts +++ b/packages/cli/src/requests.ts @@ -81,7 +81,14 @@ export namespace ListQuery { * Slim workflow returned from a list query operation. */ export namespace Workflow { - type OptionalBaseFields = 'name' | 'active' | 'versionId' | 'createdAt' | 'updatedAt' | 'tags'; + type OptionalBaseFields = + | 'name' + | 'active' + | 'versionId' + | 'createdAt' + | 'updatedAt' + | 'tags' + | 'nodes'; type BaseFields = Pick & Partial>; diff --git a/packages/cli/src/workflows/workflow.request.ts b/packages/cli/src/workflows/workflow.request.ts index d45cfd14d3..c4eb4cb8a4 100644 --- a/packages/cli/src/workflows/workflow.request.ts +++ b/packages/cli/src/workflows/workflow.request.ts @@ -28,7 +28,12 @@ export declare namespace WorkflowRequest { type Get = AuthenticatedRequest<{ workflowId: string }>; - type GetMany = AuthenticatedRequest<{}, {}, {}, ListQuery.Params & { includeScopes?: string }> & { + type GetMany = AuthenticatedRequest< + {}, + {}, + {}, + ListQuery.Params & { includeScopes?: string } & { credentialIds?: string } + > & { listQueryOptions: ListQuery.Options; }; diff --git a/packages/cli/src/workflows/workflow.service.ts b/packages/cli/src/workflows/workflow.service.ts index bce8770303..ae81da84aa 100644 --- a/packages/cli/src/workflows/workflow.service.ts +++ b/packages/cli/src/workflows/workflow.service.ts @@ -57,13 +57,22 @@ export class WorkflowService { private readonly eventService: EventService, ) {} - async getMany(user: User, options?: ListQuery.Options, includeScopes?: boolean) { + async getMany( + user: User, + options?: ListQuery.Options, + includeScopes = false, + credentialIds: string[] = [], + ) { const sharedWorkflowIds = await this.workflowSharingService.getSharedWorkflowIds(user, { scopes: ['workflow:read'], }); // eslint-disable-next-line prefer-const - let { workflows, count } = await this.workflowRepository.getMany(sharedWorkflowIds, options); + let { workflows, count } = await this.workflowRepository.getMany( + sharedWorkflowIds, + options, + credentialIds, + ); if (hasSharing(workflows)) { workflows = workflows.map((w) => this.ownershipService.addOwnedByAndSharedWith(w)); @@ -75,8 +84,8 @@ export class WorkflowService { } workflows.forEach((w) => { - // @ts-expect-error: This is to emulate the old behaviour of removing the shared - // field as part of `addOwnedByAndSharedWith`. We need this field in `addScopes` + // This is to emulate the old behavior of removing the shared field as + // part of `addOwnedByAndSharedWith`. We need this field in `addScopes` // though. So to avoid leaking the information we just delete it. delete w.shared; }); diff --git a/packages/cli/src/workflows/workflows.controller.ts b/packages/cli/src/workflows/workflows.controller.ts index 59f53e0df1..23450c0cba 100644 --- a/packages/cli/src/workflows/workflows.controller.ts +++ b/packages/cli/src/workflows/workflows.controller.ts @@ -198,6 +198,7 @@ export class WorkflowsController { req.user, req.listQueryOptions, !!req.query.includeScopes, + req.query.credentialIds?.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 84c1505887..738cedc575 100644 --- a/packages/cli/test/integration/workflows/workflows.controller.test.ts +++ b/packages/cli/test/integration/workflows/workflows.controller.test.ts @@ -468,6 +468,87 @@ describe('GET /workflows', () => { expect(found.usedCredentials).toBeUndefined(); }); + test('should return workflows filtered by used credentials', async () => { + // ARRANGE + const credential1 = await saveCredential(randomCredentialPayload(), { + user: owner, + role: 'credential:owner', + }); + const node1: INode = { + id: uuid(), + name: 'Action Network', + type: 'n8n-nodes-base.actionNetwork', + parameters: {}, + typeVersion: 1, + position: [0, 0], + credentials: { + actionNetworkApi: { + id: credential1.id, + name: credential1.name, + }, + }, + }; + const workflow1 = await createWorkflow({ name: 'First', nodes: [node1] }, owner); + + const credential2 = await saveCredential(randomCredentialPayload(), { + user: owner, + role: 'credential:owner', + }); + const node2: INode = { + id: uuid(), + name: 'Action Network', + type: 'n8n-nodes-base.actionNetwork', + parameters: {}, + typeVersion: 1, + position: [0, 0], + credentials: { + actionNetworkApi: { + id: credential2.id, + name: credential2.name, + }, + }, + }; + await createWorkflow({ name: 'Second', nodes: [node2] }, owner); + + await createWorkflow({ name: 'Third' }, owner); + + // ACT + const response = await authOwnerAgent + .get('/workflows') + .query(`credentialIds=${credential1.id}`) + .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',