mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
filter by node name
This commit is contained in:
parent
63357aad82
commit
73745cd562
|
@ -97,6 +97,9 @@ export class WorkflowEntity extends WithTimestampsAndStringId implements IWorkfl
|
||||||
|
|
||||||
@Column({ type: 'simple-array', default: '' })
|
@Column({ type: 'simple-array', default: '' })
|
||||||
nodeTypes: string[];
|
nodeTypes: string[];
|
||||||
|
|
||||||
|
@Column({ type: 'simple-array', default: '' })
|
||||||
|
nodeNames: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -2,10 +2,14 @@ import type { MigrationContext, ReversibleMigration } from '@/databases/types';
|
||||||
|
|
||||||
export class AddWorkflowFilterColumns1730286483664 implements ReversibleMigration {
|
export class AddWorkflowFilterColumns1730286483664 implements ReversibleMigration {
|
||||||
async up({ schemaBuilder: { addColumns, column } }: MigrationContext) {
|
async up({ schemaBuilder: { addColumns, column } }: MigrationContext) {
|
||||||
await addColumns('workflow_entity', [column('credentialIds').text, column('nodeTypes').text]);
|
await addColumns('workflow_entity', [
|
||||||
|
column('credentialIds').text,
|
||||||
|
column('nodeTypes').text,
|
||||||
|
column('nodeNames').text,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async down({ schemaBuilder: { dropColumns } }: MigrationContext) {
|
async down({ schemaBuilder: { dropColumns } }: MigrationContext) {
|
||||||
await dropColumns('workflow_entity', ['credentialIds', 'nodeTypes']);
|
await dropColumns('workflow_entity', ['credentialIds', 'nodeTypes', 'nodeNames']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@ import { Service } from 'typedi';
|
||||||
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';
|
||||||
import { toBase64 } from '@/workflows/utils';
|
import { escapeCommas, toBase64 } from '@/workflows/utils';
|
||||||
|
|
||||||
import { WebhookEntity } from '../entities/webhook-entity';
|
import { WebhookEntity } from '../entities/webhook-entity';
|
||||||
import { WorkflowEntity } from '../entities/workflow-entity';
|
import { WorkflowEntity } from '../entities/workflow-entity';
|
||||||
|
@ -102,6 +102,7 @@ export class WorkflowRepository extends Repository<WorkflowEntity> {
|
||||||
options: ListQuery.Options = {},
|
options: ListQuery.Options = {},
|
||||||
credentialIds: string[] = [],
|
credentialIds: string[] = [],
|
||||||
nodeTypes: string[] = [],
|
nodeTypes: string[] = [],
|
||||||
|
nodeName = '',
|
||||||
) {
|
) {
|
||||||
if (sharedWorkflowIds.length === 0) return { workflows: [], count: 0 };
|
if (sharedWorkflowIds.length === 0) return { workflows: [], count: 0 };
|
||||||
|
|
||||||
|
@ -162,6 +163,10 @@ export class WorkflowRepository extends Repository<WorkflowEntity> {
|
||||||
where.nodeTypes = Or(...nodeTypes.map((id) => Like(`%${toBase64(id)}%`)));
|
where.nodeTypes = Or(...nodeTypes.map((id) => Like(`%${toBase64(id)}%`)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (nodeName) {
|
||||||
|
where.nodeNames = Like(`%${escapeCommas(nodeName)}%`);
|
||||||
|
}
|
||||||
|
|
||||||
const findManyOptions: FindManyOptions<WorkflowEntity> = {
|
const findManyOptions: FindManyOptions<WorkflowEntity> = {
|
||||||
select: { ...select, id: true },
|
select: { ...select, id: true },
|
||||||
where,
|
where,
|
||||||
|
|
|
@ -31,3 +31,17 @@ export function getEncodedNodeTypes(workflow: WorkflowEntity): string[] {
|
||||||
|
|
||||||
return nodeTypes;
|
return nodeTypes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function escapeCommas(value: string): string {
|
||||||
|
return value.replaceAll(',', '_');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getEscapedNodeNames(workflow: WorkflowEntity): string[] {
|
||||||
|
const nodeNames: string[] = [];
|
||||||
|
|
||||||
|
for (const node of workflow.nodes) {
|
||||||
|
nodeNames.push(escapeCommas(node.name));
|
||||||
|
}
|
||||||
|
|
||||||
|
return nodeNames;
|
||||||
|
}
|
||||||
|
|
|
@ -32,7 +32,11 @@ export declare namespace WorkflowRequest {
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
ListQuery.Params & { includeScopes?: string } & { credentialIds?: string; nodeTypes?: string }
|
ListQuery.Params & { includeScopes?: string } & {
|
||||||
|
credentialIds?: string;
|
||||||
|
nodeTypes?: string;
|
||||||
|
nodeName?: string;
|
||||||
|
}
|
||||||
> & {
|
> & {
|
||||||
listQueryOptions: ListQuery.Options;
|
listQueryOptions: ListQuery.Options;
|
||||||
};
|
};
|
||||||
|
|
|
@ -33,7 +33,7 @@ import { RoleService } from '@/services/role.service';
|
||||||
import { TagService } from '@/services/tag.service';
|
import { TagService } from '@/services/tag.service';
|
||||||
import * as WorkflowHelpers from '@/workflow-helpers';
|
import * as WorkflowHelpers from '@/workflow-helpers';
|
||||||
|
|
||||||
import { getEncodedCredentialIds, getEncodedNodeTypes } from './utils';
|
import { getEncodedCredentialIds, getEncodedNodeTypes, getEscapedNodeNames } from './utils';
|
||||||
import { WorkflowHistoryService } from './workflow-history/workflow-history.service.ee';
|
import { WorkflowHistoryService } from './workflow-history/workflow-history.service.ee';
|
||||||
import { WorkflowSharingService } from './workflow-sharing.service';
|
import { WorkflowSharingService } from './workflow-sharing.service';
|
||||||
|
|
||||||
|
@ -64,6 +64,7 @@ export class WorkflowService {
|
||||||
includeScopes = false,
|
includeScopes = false,
|
||||||
credentialIds: string[] = [],
|
credentialIds: string[] = [],
|
||||||
nodeTypes: string[] = [],
|
nodeTypes: string[] = [],
|
||||||
|
nodeName = '',
|
||||||
) {
|
) {
|
||||||
const sharedWorkflowIds = await this.workflowSharingService.getSharedWorkflowIds(user, {
|
const sharedWorkflowIds = await this.workflowSharingService.getSharedWorkflowIds(user, {
|
||||||
scopes: ['workflow:read'],
|
scopes: ['workflow:read'],
|
||||||
|
@ -75,6 +76,7 @@ export class WorkflowService {
|
||||||
options,
|
options,
|
||||||
credentialIds,
|
credentialIds,
|
||||||
nodeTypes,
|
nodeTypes,
|
||||||
|
nodeName,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (hasSharing(workflows)) {
|
if (hasSharing(workflows)) {
|
||||||
|
@ -201,6 +203,7 @@ export class WorkflowService {
|
||||||
]),
|
]),
|
||||||
credentialIds: getEncodedCredentialIds(workflow),
|
credentialIds: getEncodedCredentialIds(workflow),
|
||||||
nodeTypes: getEncodedNodeTypes(workflow),
|
nodeTypes: getEncodedNodeTypes(workflow),
|
||||||
|
nodeNames: getEscapedNodeNames(workflow),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (tagIds && !config.getEnv('workflowTagsDisabled')) {
|
if (tagIds && !config.getEnv('workflowTagsDisabled')) {
|
||||||
|
|
|
@ -38,7 +38,7 @@ import { UserManagementMailer } from '@/user-management/email';
|
||||||
import * as utils from '@/utils';
|
import * as utils from '@/utils';
|
||||||
import * as WorkflowHelpers from '@/workflow-helpers';
|
import * as WorkflowHelpers from '@/workflow-helpers';
|
||||||
|
|
||||||
import { getEncodedCredentialIds, getEncodedNodeTypes } from './utils';
|
import { getEncodedCredentialIds, getEncodedNodeTypes, getEscapedNodeNames } from './utils';
|
||||||
import { WorkflowExecutionService } from './workflow-execution.service';
|
import { WorkflowExecutionService } from './workflow-execution.service';
|
||||||
import { WorkflowHistoryService } from './workflow-history/workflow-history.service.ee';
|
import { WorkflowHistoryService } from './workflow-history/workflow-history.service.ee';
|
||||||
import { WorkflowRequest } from './workflow.request';
|
import { WorkflowRequest } from './workflow.request';
|
||||||
|
@ -118,6 +118,7 @@ export class WorkflowsController {
|
||||||
|
|
||||||
newWorkflow.credentialIds = getEncodedCredentialIds(newWorkflow);
|
newWorkflow.credentialIds = getEncodedCredentialIds(newWorkflow);
|
||||||
newWorkflow.nodeTypes = getEncodedNodeTypes(newWorkflow);
|
newWorkflow.nodeTypes = getEncodedNodeTypes(newWorkflow);
|
||||||
|
newWorkflow.nodeNames = getEscapedNodeNames(newWorkflow);
|
||||||
|
|
||||||
let project: Project | null;
|
let project: Project | null;
|
||||||
const savedWorkflow = await Db.transaction(async (transactionManager) => {
|
const savedWorkflow = await Db.transaction(async (transactionManager) => {
|
||||||
|
@ -204,6 +205,7 @@ export class WorkflowsController {
|
||||||
!!req.query.includeScopes,
|
!!req.query.includeScopes,
|
||||||
req.query.credentialIds?.split(','),
|
req.query.credentialIds?.split(','),
|
||||||
req.query.nodeTypes?.split(','),
|
req.query.nodeTypes?.split(','),
|
||||||
|
req.query.nodeName,
|
||||||
);
|
);
|
||||||
|
|
||||||
res.json({ count, data });
|
res.json({ count, data });
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import type { Scope } from '@n8n/permissions';
|
import type { Scope } from '@n8n/permissions';
|
||||||
|
import * as a from 'assert/strict';
|
||||||
import type { INode, IPinData } from 'n8n-workflow';
|
import type { INode, IPinData } from 'n8n-workflow';
|
||||||
import Container from 'typedi';
|
import Container from 'typedi';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
|
@ -11,9 +12,10 @@ import { SharedWorkflowRepository } from '@/databases/repositories/shared-workfl
|
||||||
import { WorkflowHistoryRepository } from '@/databases/repositories/workflow-history.repository';
|
import { WorkflowHistoryRepository } from '@/databases/repositories/workflow-history.repository';
|
||||||
import { WorkflowRepository } from '@/databases/repositories/workflow.repository';
|
import { WorkflowRepository } from '@/databases/repositories/workflow.repository';
|
||||||
import { License } from '@/license';
|
import { License } from '@/license';
|
||||||
|
import { getWorkflowById } from '@/public-api/v1/handlers/workflows/workflows.service';
|
||||||
import type { ListQuery } from '@/requests';
|
import type { ListQuery } from '@/requests';
|
||||||
import { ProjectService } from '@/services/project.service';
|
import { ProjectService } from '@/services/project.service';
|
||||||
import { fromBase64, toBase64 } from '@/workflows/utils';
|
import { escapeCommas, fromBase64, toBase64 } from '@/workflows/utils';
|
||||||
import { EnterpriseWorkflowService } from '@/workflows/workflow.service.ee';
|
import { EnterpriseWorkflowService } from '@/workflows/workflow.service.ee';
|
||||||
|
|
||||||
import { mockInstance } from '../../shared/mocking';
|
import { mockInstance } from '../../shared/mocking';
|
||||||
|
@ -21,16 +23,12 @@ import { saveCredential } from '../shared/db/credentials';
|
||||||
import { createTeamProject, linkUserToProject } from '../shared/db/projects';
|
import { createTeamProject, linkUserToProject } from '../shared/db/projects';
|
||||||
import { createTag } from '../shared/db/tags';
|
import { createTag } from '../shared/db/tags';
|
||||||
import { createManyUsers, createMember, createOwner } from '../shared/db/users';
|
import { createManyUsers, createMember, createOwner } from '../shared/db/users';
|
||||||
import { createWorkflow, getAllWorkflows, shareWorkflowWithProjects } from '../shared/db/workflows';
|
import { createWorkflow, shareWorkflowWithProjects } from '../shared/db/workflows';
|
||||||
import { randomCredentialPayload } from '../shared/random';
|
import { randomCredentialPayload } from '../shared/random';
|
||||||
import * as testDb from '../shared/test-db';
|
import * as testDb from '../shared/test-db';
|
||||||
import type { SuperAgentTest } from '../shared/types';
|
import type { SuperAgentTest } from '../shared/types';
|
||||||
import * as utils from '../shared/utils/';
|
import * as utils from '../shared/utils/';
|
||||||
import { makeWorkflow, MOCK_PINDATA } 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 owner: User;
|
||||||
let member: User;
|
let member: User;
|
||||||
|
@ -94,7 +92,7 @@ describe('POST /workflows', () => {
|
||||||
a.ok(savedWorkflow);
|
a.ok(savedWorkflow);
|
||||||
|
|
||||||
expect(savedWorkflow.nodeTypes).toHaveLength(1);
|
expect(savedWorkflow.nodeTypes).toHaveLength(1);
|
||||||
expect(fromBase64(savedWorkflow?.nodeTypes[0])).toEqual(workflow.nodes[0].type);
|
expect(fromBase64(savedWorkflow.nodeTypes[0])).toEqual(workflow.nodes[0].type);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should populate `credentialIds`', async () => {
|
test('should populate `credentialIds`', async () => {
|
||||||
|
@ -110,7 +108,23 @@ describe('POST /workflows', () => {
|
||||||
a.ok(savedWorkflow);
|
a.ok(savedWorkflow);
|
||||||
|
|
||||||
expect(savedWorkflow.credentialIds).toHaveLength(1);
|
expect(savedWorkflow.credentialIds).toHaveLength(1);
|
||||||
expect(fromBase64(savedWorkflow?.credentialIds[0])).toEqual(credential.id);
|
expect(fromBase64(savedWorkflow.credentialIds[0])).toEqual(credential.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should populate `nodeTypes`', async () => {
|
||||||
|
// ARRANGE
|
||||||
|
const workflow = makeWorkflow();
|
||||||
|
workflow.nodes[0].name = 'Cron,with,commas';
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
await authOwnerAgent.post('/workflows').send(workflow).expect(200);
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
const savedWorkflow = await getWorkflowById(workflow.id);
|
||||||
|
a.ok(savedWorkflow);
|
||||||
|
|
||||||
|
expect(savedWorkflow.nodeNames).toHaveLength(1);
|
||||||
|
expect(savedWorkflow.nodeNames[0]).toEqual(escapeCommas(workflow.nodes[0].name));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should return scopes on created workflow', async () => {
|
test('should return scopes on created workflow', async () => {
|
||||||
|
@ -652,6 +666,70 @@ describe('GET /workflows', () => {
|
||||||
expect(found.usedCredentials).toBeUndefined();
|
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], nodeNames: [escapeCommas(node1.name)] },
|
||||||
|
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(`nodeName=${node1.name}`)
|
||||||
|
.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