mirror of
https://github.com/n8n-io/n8n.git
synced 2024-11-14 16:44:07 -08:00
allow filtering workflows by credential ids
This commit is contained in:
parent
33a4873bc7
commit
ee1b50de3e
|
@ -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<WorkflowEntity> {
|
|||
.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<WorkflowEntity> {
|
|||
where.name = Like(`%${where.name}%`);
|
||||
}
|
||||
|
||||
if (credentialIds.length) {
|
||||
where.nodes = Or(...credentialIds.map((id) => Like(`%{"id":"${id}"%`)));
|
||||
}
|
||||
|
||||
const findManyOptions: FindManyOptions<WorkflowEntity> = {
|
||||
select: { ...select, id: true },
|
||||
select: { ...select, id: true, nodes: true },
|
||||
where,
|
||||
};
|
||||
|
||||
|
@ -165,11 +176,43 @@ export class WorkflowRepository extends Repository<WorkflowEntity> {
|
|||
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 };
|
||||
}
|
||||
|
||||
|
|
|
@ -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<WorkflowEntity, 'id'> &
|
||||
Partial<Pick<WorkflowEntity, OptionalBaseFields>>;
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
|
|
|
@ -198,6 +198,7 @@ export class WorkflowsController {
|
|||
req.user,
|
||||
req.listQueryOptions,
|
||||
!!req.query.includeScopes,
|
||||
req.query.credentialIds?.split(','),
|
||||
);
|
||||
|
||||
res.json({ count, data });
|
||||
|
|
|
@ -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',
|
||||
|
|
Loading…
Reference in a new issue