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() {
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 { SeparateExecutionCreationFromStart1727427440136 } from '../common/1727427440136-SeparateExecutionCreationFromStart';
import { UpdateProcessedDataValueColumnToText1729607673464 } from '../common/1729607673464-UpdateProcessedDataValueColumnToText';
import { AddWorkflowFilterColumns1730286483664 } from '../common/1730286483664-AddWorkflowFilterColumns';
const sqliteMigrations: Migration[] = [
InitialMigration1588102412422,
@ -132,6 +133,7 @@ const sqliteMigrations: Migration[] = [
CreateProcessedDataTable1726606152711,
AddMissingPrimaryKeyOnAnnotationTagMapping1728659839644,
UpdateProcessedDataValueColumnToText1729607673464,
AddWorkflowFilterColumns1730286483664,
];
export { sqliteMigrations };

View file

@ -13,11 +13,10 @@ 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';
import { toBase64 } from '@/workflows/utils';
import { WebhookEntity } from '../entities/webhook-entity';
import { WorkflowEntity } from '../entities/workflow-entity';
@ -155,11 +154,11 @@ export class WorkflowRepository extends Repository<WorkflowEntity> {
}
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> = {
select: { ...select, id: true, nodes: true },
select: { ...select, id: true },
where,
};
@ -176,43 +175,11 @@ export class WorkflowRepository extends Repository<WorkflowEntity> {
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[],
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 };
}

View file

@ -81,14 +81,7 @@ export namespace ListQuery {
* Slim workflow returned from a list query operation.
*/
export namespace Workflow {
type OptionalBaseFields =
| 'name'
| 'active'
| 'versionId'
| 'createdAt'
| 'updatedAt'
| 'tags'
| 'nodes';
type OptionalBaseFields = 'name' | 'active' | 'versionId' | 'createdAt' | 'updatedAt' | 'tags';
type BaseFields = Pick<WorkflowEntity, 'id'> &
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 * as WorkflowHelpers from '@/workflow-helpers';
import { getEncodedCredentialIds } from './utils';
import { WorkflowHistoryService } from './workflow-history/workflow-history.service.ee';
import { WorkflowSharingService } from './workflow-sharing.service';
@ -84,9 +85,10 @@ export class WorkflowService {
}
workflows.forEach((w) => {
// 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.
// @ts-expect-error: 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;
});
@ -183,9 +185,8 @@ export class WorkflowService {
await validateEntity(workflowUpdateData);
}
await this.workflowRepository.update(
workflowId,
pick(workflowUpdateData, [
await this.workflowRepository.update(workflowId, {
...pick(workflowUpdateData, [
'name',
'active',
'nodes',
@ -196,7 +197,8 @@ export class WorkflowService {
'pinData',
'versionId',
]),
);
credentialIds: getEncodedCredentialIds(workflow),
});
if (tagIds && !config.getEnv('workflowTagsDisabled')) {
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 axios from 'axios';
import express from 'express';
import { ApplicationError } from 'n8n-workflow';
import { ApplicationError, createEnvProvider } from 'n8n-workflow';
import { v4 as uuid } from 'uuid';
import { z } from 'zod';
@ -44,6 +44,7 @@ import { WorkflowRequest } from './workflow.request';
import { WorkflowService } from './workflow.service';
import { EnterpriseWorkflowService } from './workflow.service.ee';
import { CredentialsService } from '../credentials/credentials.service';
import { getEncodedCredentialIds } from './utils';
@RestController('/workflows')
export class WorkflowsController {
@ -115,6 +116,8 @@ export class WorkflowsController {
}
}
newWorkflow.credentialIds = getEncodedCredentialIds(newWorkflow);
let project: Project | null;
const savedWorkflow = await Db.transaction(async (transactionManager) => {
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 type { ListQuery } from '@/requests';
import { ProjectService } from '@/services/project.service';
import { toBase64 } from '@/workflows/utils';
import { EnterpriseWorkflowService } from '@/workflows/workflow.service.ee';
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(), {
user: owner,