filter by credential ids in a better way

This commit is contained in:
Danny Martini 2024-10-30 13:00:08 +01:00
parent ee1b50de3e
commit fd9435c3f1
No known key found for this signature in database
9 changed files with 63 additions and 54 deletions

View file

@ -91,6 +91,9 @@ export class WorkflowEntity extends WithTimestampsAndStringId implements IWorkfl
display() { display() {
return `"${this.name}" (ID: ${this.id})`; return `"${this.name}" (ID: ${this.id})`;
} }
@Column({ type: 'simple-array', default: '' })
credentialIds: string[];
} }
/** /**

View file

@ -0,0 +1,12 @@
import type { MigrationContext, ReversibleMigration } from '@/databases/types';
export class AddWorkflowFilterColumns1730286483664 implements ReversibleMigration {
async up({ schemaBuilder: { addColumns, column } }: MigrationContext) {
console.log('foo');
await addColumns('workflow_entity', [column('credentialIds').text]);
}
async down({ schemaBuilder: { dropColumns } }: MigrationContext) {
await dropColumns('workflow_entity', ['credentialIds']);
}
}

View file

@ -65,6 +65,7 @@ import { CreateAnnotationTables1724753530828 } from '../common/1724753530828-Cre
import { CreateProcessedDataTable1726606152711 } from '../common/1726606152711-CreateProcessedDataTable'; import { CreateProcessedDataTable1726606152711 } from '../common/1726606152711-CreateProcessedDataTable';
import { SeparateExecutionCreationFromStart1727427440136 } from '../common/1727427440136-SeparateExecutionCreationFromStart'; import { SeparateExecutionCreationFromStart1727427440136 } from '../common/1727427440136-SeparateExecutionCreationFromStart';
import { UpdateProcessedDataValueColumnToText1729607673464 } from '../common/1729607673464-UpdateProcessedDataValueColumnToText'; import { UpdateProcessedDataValueColumnToText1729607673464 } from '../common/1729607673464-UpdateProcessedDataValueColumnToText';
import { AddWorkflowFilterColumns1730286483664 } from '../common/1730286483664-AddWorkflowFilterColumns';
const sqliteMigrations: Migration[] = [ const sqliteMigrations: Migration[] = [
InitialMigration1588102412422, InitialMigration1588102412422,
@ -132,6 +133,7 @@ const sqliteMigrations: Migration[] = [
CreateProcessedDataTable1726606152711, CreateProcessedDataTable1726606152711,
AddMissingPrimaryKeyOnAnnotationTagMapping1728659839644, AddMissingPrimaryKeyOnAnnotationTagMapping1728659839644,
UpdateProcessedDataValueColumnToText1729607673464, UpdateProcessedDataValueColumnToText1729607673464,
AddWorkflowFilterColumns1730286483664,
]; ];
export { sqliteMigrations }; export { sqliteMigrations };

View file

@ -13,11 +13,10 @@ 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';
import { 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';
@ -155,11 +154,11 @@ export class WorkflowRepository extends Repository<WorkflowEntity> {
} }
if (credentialIds.length) { if (credentialIds.length) {
where.nodes = Or(...credentialIds.map((id) => Like(`%{"id":"${id}"%`))); where.credentialIds = Or(...credentialIds.map((id) => Like(`%${toBase64(id)}%`)));
} }
const findManyOptions: FindManyOptions<WorkflowEntity> = { const findManyOptions: FindManyOptions<WorkflowEntity> = {
select: { ...select, id: true, nodes: true }, select: { ...select, id: true },
where, where,
}; };
@ -176,43 +175,11 @@ export class WorkflowRepository extends Repository<WorkflowEntity> {
findManyOptions.take = options.take; findManyOptions.take = options.take;
} }
let [workflows, count] = (await this.findAndCount(findManyOptions)) as [ const [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 };
} }

View file

@ -81,14 +81,7 @@ 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 = type OptionalBaseFields = 'name' | 'active' | 'versionId' | 'createdAt' | 'updatedAt' | 'tags';
| '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>>;

View file

@ -0,0 +1,23 @@
import type { WorkflowEntity } from '@/databases/entities/workflow-entity';
export function toBase64(id: string): string {
return Buffer.from(id).toString('base64');
}
export function fromBase64(encoded: string): string {
return Buffer.from(encoded, 'base64').toString();
}
export function getEncodedCredentialIds(workflow: WorkflowEntity): string[] {
const credentialIds: string[] = [];
for (const nodes of workflow.nodes) {
for (const credential of Object.values(nodes.credentials ?? {})) {
if (credential.id) {
credentialIds.push(toBase64(credential.id));
}
}
}
return credentialIds;
}

View file

@ -33,6 +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 { 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';
@ -84,9 +85,10 @@ export class WorkflowService {
} }
workflows.forEach((w) => { workflows.forEach((w) => {
// This is to emulate the old behavior of removing the shared field as // @ts-expect-error: This is to emulate the old behavior of removing the
// part of `addOwnedByAndSharedWith`. We need this field in `addScopes` // shared field as part of `addOwnedByAndSharedWith`. We need this field
// though. So to avoid leaking the information we just delete it. // in `addScopes` though. So to avoid leaking the information we just
// delete it.
delete w.shared; delete w.shared;
}); });
@ -183,9 +185,8 @@ export class WorkflowService {
await validateEntity(workflowUpdateData); await validateEntity(workflowUpdateData);
} }
await this.workflowRepository.update( await this.workflowRepository.update(workflowId, {
workflowId, ...pick(workflowUpdateData, [
pick(workflowUpdateData, [
'name', 'name',
'active', 'active',
'nodes', 'nodes',
@ -196,7 +197,8 @@ export class WorkflowService {
'pinData', 'pinData',
'versionId', 'versionId',
]), ]),
); credentialIds: getEncodedCredentialIds(workflow),
});
if (tagIds && !config.getEnv('workflowTagsDisabled')) { if (tagIds && !config.getEnv('workflowTagsDisabled')) {
await this.workflowTagMappingRepository.overwriteTaggings(workflowId, tagIds); await this.workflowTagMappingRepository.overwriteTaggings(workflowId, tagIds);

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 } from 'n8n-workflow'; import { ApplicationError, createEnvProvider } from 'n8n-workflow';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import { z } from 'zod'; import { z } from 'zod';
@ -44,6 +44,7 @@ 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 {
@ -115,6 +116,8 @@ export class WorkflowsController {
} }
} }
newWorkflow.credentialIds = getEncodedCredentialIds(newWorkflow);
let project: Project | null; let project: Project | null;
const savedWorkflow = await Db.transaction(async (transactionManager) => { const savedWorkflow = await Db.transaction(async (transactionManager) => {
const workflow = await transactionManager.save<WorkflowEntity>(newWorkflow); const workflow = await transactionManager.save<WorkflowEntity>(newWorkflow);

View file

@ -13,6 +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 { EnterpriseWorkflowService } from '@/workflows/workflow.service.ee'; import { EnterpriseWorkflowService } from '@/workflows/workflow.service.ee';
import { mockInstance } from '../../shared/mocking'; import { mockInstance } from '../../shared/mocking';
@ -488,7 +489,10 @@ describe('GET /workflows', () => {
}, },
}, },
}; };
const workflow1 = await createWorkflow({ name: 'First', nodes: [node1] }, owner); const workflow1 = await createWorkflow(
{ name: 'First', nodes: [node1], credentialIds: [toBase64(credential1.id)] },
owner,
);
const credential2 = await saveCredential(randomCredentialPayload(), { const credential2 = await saveCredential(randomCredentialPayload(), {
user: owner, user: owner,