Merge branch 'add-workflow-filters' of github.com:n8n-io/n8n into add-workflow-filters

This commit is contained in:
Mutasem Aldmour 2024-10-30 13:34:26 +01:00
commit e449ed01d2
No known key found for this signature in database
GPG key ID: 3DFA8122BB7FD6B8
8 changed files with 130 additions and 9 deletions

View file

@ -94,6 +94,9 @@ export class WorkflowEntity extends WithTimestampsAndStringId implements IWorkfl
@Column({ type: 'simple-array', default: '' }) @Column({ type: 'simple-array', default: '' })
credentialIds: string[]; credentialIds: string[];
@Column({ type: 'simple-array', default: '' })
nodeTypes: string[];
} }
/** /**

View file

@ -2,11 +2,10 @@ 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) {
console.log('foo'); await addColumns('workflow_entity', [column('credentialIds').text, column('nodeTypes').text]);
await addColumns('workflow_entity', [column('credentialIds').text]);
} }
async down({ schemaBuilder: { dropColumns } }: MigrationContext) { async down({ schemaBuilder: { dropColumns } }: MigrationContext) {
await dropColumns('workflow_entity', ['credentialIds']); await dropColumns('workflow_entity', ['credentialIds', 'nodeTypes']);
} }
} }

View file

@ -101,6 +101,7 @@ export class WorkflowRepository extends Repository<WorkflowEntity> {
sharedWorkflowIds: string[], sharedWorkflowIds: string[],
options: ListQuery.Options = {}, options: ListQuery.Options = {},
credentialIds: string[] = [], credentialIds: string[] = [],
nodeTypes: string[] = [],
) { ) {
if (sharedWorkflowIds.length === 0) return { workflows: [], count: 0 }; if (sharedWorkflowIds.length === 0) return { workflows: [], count: 0 };
@ -157,6 +158,10 @@ export class WorkflowRepository extends Repository<WorkflowEntity> {
where.credentialIds = Or(...credentialIds.map((id) => Like(`%${toBase64(id)}%`))); where.credentialIds = Or(...credentialIds.map((id) => Like(`%${toBase64(id)}%`)));
} }
if (nodeTypes.length) {
where.nodeTypes = Or(...nodeTypes.map((id) => Like(`%${toBase64(id)}%`)));
}
const findManyOptions: FindManyOptions<WorkflowEntity> = { const findManyOptions: FindManyOptions<WorkflowEntity> = {
select: { ...select, id: true }, select: { ...select, id: true },
where, where,

View file

@ -21,3 +21,13 @@ export function getEncodedCredentialIds(workflow: WorkflowEntity): string[] {
return credentialIds; return credentialIds;
} }
export function getEncodedNodeTypes(workflow: WorkflowEntity): string[] {
const nodeTypes: string[] = [];
for (const node of workflow.nodes) {
nodeTypes.push(toBase64(node.type));
}
return nodeTypes;
}

View file

@ -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; listQueryOptions: ListQuery.Options;
}; };

View file

@ -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 } from './utils'; import { getEncodedCredentialIds, getEncodedNodeTypes } 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';
@ -63,6 +63,7 @@ export class WorkflowService {
options?: ListQuery.Options, options?: ListQuery.Options,
includeScopes = false, includeScopes = false,
credentialIds: string[] = [], credentialIds: string[] = [],
nodeTypes: string[] = [],
) { ) {
const sharedWorkflowIds = await this.workflowSharingService.getSharedWorkflowIds(user, { const sharedWorkflowIds = await this.workflowSharingService.getSharedWorkflowIds(user, {
scopes: ['workflow:read'], scopes: ['workflow:read'],
@ -73,6 +74,7 @@ export class WorkflowService {
sharedWorkflowIds, sharedWorkflowIds,
options, options,
credentialIds, credentialIds,
nodeTypes,
); );
if (hasSharing(workflows)) { if (hasSharing(workflows)) {
@ -198,6 +200,7 @@ export class WorkflowService {
'versionId', 'versionId',
]), ]),
credentialIds: getEncodedCredentialIds(workflow), credentialIds: getEncodedCredentialIds(workflow),
nodeTypes: getEncodedNodeTypes(workflow),
}); });
if (tagIds && !config.getEnv('workflowTagsDisabled')) { if (tagIds && !config.getEnv('workflowTagsDisabled')) {

View file

@ -3,7 +3,7 @@ import { GlobalConfig } from '@n8n/config';
import { In, type FindOptionsRelations } from '@n8n/typeorm'; import { In, type FindOptionsRelations } from '@n8n/typeorm';
import axios from 'axios'; import axios from 'axios';
import express from 'express'; import express from 'express';
import { ApplicationError, createEnvProvider } from 'n8n-workflow'; import { ApplicationError } from 'n8n-workflow';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import { z } from 'zod'; import { z } from 'zod';
@ -38,13 +38,13 @@ 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 { 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';
import { WorkflowService } from './workflow.service'; import { WorkflowService } from './workflow.service';
import { EnterpriseWorkflowService } from './workflow.service.ee'; import { EnterpriseWorkflowService } from './workflow.service.ee';
import { CredentialsService } from '../credentials/credentials.service'; import { CredentialsService } from '../credentials/credentials.service';
import { getEncodedCredentialIds } from './utils';
@RestController('/workflows') @RestController('/workflows')
export class WorkflowsController { export class WorkflowsController {
@ -117,6 +117,7 @@ export class WorkflowsController {
} }
newWorkflow.credentialIds = getEncodedCredentialIds(newWorkflow); newWorkflow.credentialIds = getEncodedCredentialIds(newWorkflow);
newWorkflow.nodeTypes = getEncodedNodeTypes(newWorkflow);
let project: Project | null; let project: Project | null;
const savedWorkflow = await Db.transaction(async (transactionManager) => { const savedWorkflow = await Db.transaction(async (transactionManager) => {
@ -202,6 +203,7 @@ export class WorkflowsController {
req.listQueryOptions, req.listQueryOptions,
!!req.query.includeScopes, !!req.query.includeScopes,
req.query.credentialIds?.split(','), req.query.credentialIds?.split(','),
req.query.nodeTypes?.split(','),
); );
res.json({ count, data }); res.json({ count, data });

View file

@ -13,7 +13,7 @@ import { WorkflowRepository } from '@/databases/repositories/workflow.repository
import { License } from '@/license'; import { License } from '@/license';
import type { ListQuery } from '@/requests'; import type { ListQuery } from '@/requests';
import { ProjectService } from '@/services/project.service'; 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 { EnterpriseWorkflowService } from '@/workflows/workflow.service.ee';
import { mockInstance } from '../../shared/mocking'; import { mockInstance } from '../../shared/mocking';
@ -21,12 +21,16 @@ 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, shareWorkflowWithProjects } from '../shared/db/workflows'; import { createWorkflow, getAllWorkflows, 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;
@ -78,6 +82,37 @@ describe('POST /workflows', () => {
expect(pinData).toBeNull(); 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 () => { test('should return scopes on created workflow', async () => {
const payload = { const payload = {
name: 'testing', name: 'testing',
@ -553,6 +588,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], 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 () => { 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',