diff --git a/packages/cli/src/databases/entities/workflow-entity.ts b/packages/cli/src/databases/entities/workflow-entity.ts index b03cf2c28d..37173e54e5 100644 --- a/packages/cli/src/databases/entities/workflow-entity.ts +++ b/packages/cli/src/databases/entities/workflow-entity.ts @@ -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[]; } /** diff --git a/packages/cli/src/databases/migrations/common/1730286483664-AddWorkflowFilterColumns.ts b/packages/cli/src/databases/migrations/common/1730286483664-AddWorkflowFilterColumns.ts new file mode 100644 index 0000000000..3f19b07b97 --- /dev/null +++ b/packages/cli/src/databases/migrations/common/1730286483664-AddWorkflowFilterColumns.ts @@ -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']); + } +} diff --git a/packages/cli/src/databases/migrations/sqlite/index.ts b/packages/cli/src/databases/migrations/sqlite/index.ts index e53b5f43bd..668def3ae5 100644 --- a/packages/cli/src/databases/migrations/sqlite/index.ts +++ b/packages/cli/src/databases/migrations/sqlite/index.ts @@ -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 }; diff --git a/packages/cli/src/databases/repositories/workflow.repository.ts b/packages/cli/src/databases/repositories/workflow.repository.ts index d7266a5095..2c90d44f4d 100644 --- a/packages/cli/src/databases/repositories/workflow.repository.ts +++ b/packages/cli/src/databases/repositories/workflow.repository.ts @@ -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 { } if (credentialIds.length) { - where.nodes = Or(...credentialIds.map((id) => Like(`%{"id":"${id}"%`))); + where.credentialIds = Or(...credentialIds.map((id) => Like(`%${toBase64(id)}%`))); } const findManyOptions: FindManyOptions = { - select: { ...select, id: true, nodes: true }, + select: { ...select, id: true }, where, }; @@ -176,43 +175,11 @@ export class WorkflowRepository extends Repository { 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 }; } diff --git a/packages/cli/src/requests.ts b/packages/cli/src/requests.ts index 46efbf2d5a..4765ac1fad 100644 --- a/packages/cli/src/requests.ts +++ b/packages/cli/src/requests.ts @@ -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 & Partial>; diff --git a/packages/cli/src/workflows/utils.ts b/packages/cli/src/workflows/utils.ts new file mode 100644 index 0000000000..cce24c02ad --- /dev/null +++ b/packages/cli/src/workflows/utils.ts @@ -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; +} diff --git a/packages/cli/src/workflows/workflow.service.ts b/packages/cli/src/workflows/workflow.service.ts index ae81da84aa..17bdad9d9b 100644 --- a/packages/cli/src/workflows/workflow.service.ts +++ b/packages/cli/src/workflows/workflow.service.ts @@ -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); diff --git a/packages/cli/src/workflows/workflows.controller.ts b/packages/cli/src/workflows/workflows.controller.ts index 23450c0cba..c7ab6eaeff 100644 --- a/packages/cli/src/workflows/workflows.controller.ts +++ b/packages/cli/src/workflows/workflows.controller.ts @@ -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(newWorkflow); diff --git a/packages/cli/test/integration/workflows/workflows.controller.test.ts b/packages/cli/test/integration/workflows/workflows.controller.test.ts index 738cedc575..c928e69c54 100644 --- a/packages/cli/test/integration/workflows/workflows.controller.test.ts +++ b/packages/cli/test/integration/workflows/workflows.controller.test.ts @@ -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,