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,
|
Repository,
|
||||||
In,
|
In,
|
||||||
Like,
|
Like,
|
||||||
|
Or,
|
||||||
type UpdateResult,
|
type UpdateResult,
|
||||||
type FindOptionsWhere,
|
type FindOptionsWhere,
|
||||||
type FindOptionsSelect,
|
type FindOptionsSelect,
|
||||||
|
@ -12,6 +13,8 @@ import {
|
||||||
} from '@n8n/typeorm';
|
} from '@n8n/typeorm';
|
||||||
import { Service } from 'typedi';
|
import { Service } from 'typedi';
|
||||||
|
|
||||||
|
import * as a from 'assert/strict';
|
||||||
|
|
||||||
import config from '@/config';
|
import config from '@/config';
|
||||||
import type { ListQuery } from '@/requests';
|
import type { ListQuery } from '@/requests';
|
||||||
import { isStringArray } from '@/utils';
|
import { isStringArray } from '@/utils';
|
||||||
|
@ -95,7 +98,11 @@ export class WorkflowRepository extends Repository<WorkflowEntity> {
|
||||||
.execute();
|
.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 (sharedWorkflowIds.length === 0) return { workflows: [], count: 0 };
|
||||||
|
|
||||||
if (typeof options?.filter?.projectId === 'string' && options.filter.projectId !== '') {
|
if (typeof options?.filter?.projectId === 'string' && options.filter.projectId !== '') {
|
||||||
|
@ -147,8 +154,12 @@ export class WorkflowRepository extends Repository<WorkflowEntity> {
|
||||||
where.name = Like(`%${where.name}%`);
|
where.name = Like(`%${where.name}%`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (credentialIds.length) {
|
||||||
|
where.nodes = Or(...credentialIds.map((id) => Like(`%{"id":"${id}"%`)));
|
||||||
|
}
|
||||||
|
|
||||||
const findManyOptions: FindManyOptions<WorkflowEntity> = {
|
const findManyOptions: FindManyOptions<WorkflowEntity> = {
|
||||||
select: { ...select, id: true },
|
select: { ...select, id: true, nodes: true },
|
||||||
where,
|
where,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -165,11 +176,43 @@ export class WorkflowRepository extends Repository<WorkflowEntity> {
|
||||||
findManyOptions.take = options.take;
|
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[],
|
ListQuery.Workflow.Plain[] | ListQuery.Workflow.WithSharing[],
|
||||||
number,
|
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 };
|
return { workflows, count };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -81,7 +81,14 @@ export namespace ListQuery {
|
||||||
* Slim workflow returned from a list query operation.
|
* Slim workflow returned from a list query operation.
|
||||||
*/
|
*/
|
||||||
export namespace Workflow {
|
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'> &
|
type BaseFields = Pick<WorkflowEntity, 'id'> &
|
||||||
Partial<Pick<WorkflowEntity, OptionalBaseFields>>;
|
Partial<Pick<WorkflowEntity, OptionalBaseFields>>;
|
||||||
|
|
|
@ -28,7 +28,12 @@ export declare namespace WorkflowRequest {
|
||||||
|
|
||||||
type Get = AuthenticatedRequest<{ workflowId: string }>;
|
type Get = AuthenticatedRequest<{ workflowId: string }>;
|
||||||
|
|
||||||
type GetMany = AuthenticatedRequest<{}, {}, {}, ListQuery.Params & { includeScopes?: string }> & {
|
type GetMany = AuthenticatedRequest<
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
ListQuery.Params & { includeScopes?: string } & { credentialIds?: string }
|
||||||
|
> & {
|
||||||
listQueryOptions: ListQuery.Options;
|
listQueryOptions: ListQuery.Options;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -57,13 +57,22 @@ export class WorkflowService {
|
||||||
private readonly eventService: EventService,
|
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, {
|
const sharedWorkflowIds = await this.workflowSharingService.getSharedWorkflowIds(user, {
|
||||||
scopes: ['workflow:read'],
|
scopes: ['workflow:read'],
|
||||||
});
|
});
|
||||||
|
|
||||||
// eslint-disable-next-line prefer-const
|
// 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)) {
|
if (hasSharing(workflows)) {
|
||||||
workflows = workflows.map((w) => this.ownershipService.addOwnedByAndSharedWith(w));
|
workflows = workflows.map((w) => this.ownershipService.addOwnedByAndSharedWith(w));
|
||||||
|
@ -75,8 +84,8 @@ export class WorkflowService {
|
||||||
}
|
}
|
||||||
|
|
||||||
workflows.forEach((w) => {
|
workflows.forEach((w) => {
|
||||||
// @ts-expect-error: This is to emulate the old behaviour of removing the shared
|
// This is to emulate the old behavior of removing the shared field as
|
||||||
// field as part of `addOwnedByAndSharedWith`. We need this field in `addScopes`
|
// part of `addOwnedByAndSharedWith`. We need this field in `addScopes`
|
||||||
// though. So to avoid leaking the information we just delete it.
|
// though. So to avoid leaking the information we just delete it.
|
||||||
delete w.shared;
|
delete w.shared;
|
||||||
});
|
});
|
||||||
|
|
|
@ -198,6 +198,7 @@ export class WorkflowsController {
|
||||||
req.user,
|
req.user,
|
||||||
req.listQueryOptions,
|
req.listQueryOptions,
|
||||||
!!req.query.includeScopes,
|
!!req.query.includeScopes,
|
||||||
|
req.query.credentialIds?.split(','),
|
||||||
);
|
);
|
||||||
|
|
||||||
res.json({ count, data });
|
res.json({ count, data });
|
||||||
|
|
|
@ -468,6 +468,87 @@ describe('GET /workflows', () => {
|
||||||
expect(found.usedCredentials).toBeUndefined();
|
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 () => {
|
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',
|
||||||
|
|
Loading…
Reference in a new issue