diff --git a/packages/editor-ui/src/utils/templates/__tests__/templateTransforms.test.ts b/packages/editor-ui/src/utils/templates/__tests__/templateTransforms.test.ts new file mode 100644 index 0000000000..e19a4081a5 --- /dev/null +++ b/packages/editor-ui/src/utils/templates/__tests__/templateTransforms.test.ts @@ -0,0 +1,47 @@ +import { + keyFromCredentialTypeAndName, + replaceAllTemplateNodeCredentials, +} from '@/utils/templates/templateTransforms'; +import { newWorkflowTemplateNode } from '@/utils/testData/templateTestData'; + +describe('templateTransforms', () => { + describe('replaceAllTemplateNodeCredentials', () => { + it('should replace credentials of nodes that have credentials', () => { + const node = newWorkflowTemplateNode({ + type: 'n8n-nodes-base.twitter', + credentials: { + twitterOAuth1Api: 'old1', + }, + }); + + const toReplaceWith = { + [keyFromCredentialTypeAndName('twitterOAuth1Api', 'old1')]: { + id: 'new1', + name: 'Twitter creds', + }, + }; + + const [replacedNode] = replaceAllTemplateNodeCredentials([node], toReplaceWith); + + expect(replacedNode.credentials).toEqual({ + twitterOAuth1Api: { id: 'new1', name: 'Twitter creds' }, + }); + }); + + it('should not replace credentials of nodes that do not have credentials', () => { + const node = newWorkflowTemplateNode({ + type: 'n8n-nodes-base.twitter', + }); + const toReplaceWith = { + [keyFromCredentialTypeAndName('twitterOAuth1Api', 'old1')]: { + id: 'new1', + name: 'Twitter creds', + }, + }; + + const [replacedNode] = replaceAllTemplateNodeCredentials([node], toReplaceWith); + + expect(replacedNode.credentials).toBeUndefined(); + }); + }); +}); diff --git a/packages/editor-ui/src/utils/templates/templateActions.ts b/packages/editor-ui/src/utils/templates/templateActions.ts index f353b072f3..220d037666 100644 --- a/packages/editor-ui/src/utils/templates/templateActions.ts +++ b/packages/editor-ui/src/utils/templates/templateActions.ts @@ -5,6 +5,7 @@ import type { useRootStore } from '@/stores/n8nRoot.store'; import type { useWorkflowsStore } from '@/stores/workflows.store'; import { FeatureFlag, isFeatureFlagEnabled } from '@/utils/featureFlag'; import { getFixedNodesList } from '@/utils/nodeViewUtils'; +import type { TemplateCredentialKey } from '@/utils/templates/templateTransforms'; import { replaceAllTemplateNodeCredentials } from '@/utils/templates/templateTransforms'; import type { INodeCredentialsDetails } from 'n8n-workflow'; import type { RouteLocationRaw, Router } from 'vue-router'; @@ -14,7 +15,7 @@ import type { RouteLocationRaw, Router } from 'vue-router'; */ export async function createWorkflowFromTemplate( template: IWorkflowTemplate, - credentialOverrides: Record, + credentialOverrides: Record, rootStore: ReturnType, workflowsStore: ReturnType, ) { diff --git a/packages/editor-ui/src/utils/templates/templateTransforms.ts b/packages/editor-ui/src/utils/templates/templateTransforms.ts index 41266aaacd..22ec022a80 100644 --- a/packages/editor-ui/src/utils/templates/templateTransforms.ts +++ b/packages/editor-ui/src/utils/templates/templateTransforms.ts @@ -5,6 +5,24 @@ import type { INodeCredentials, INodeCredentialsDetails } from 'n8n-workflow'; export type IWorkflowTemplateNodeWithCredentials = IWorkflowTemplateNode & Required>; +const credentialKeySymbol = Symbol('credentialKey'); + +/** + * A key that uniquely identifies a credential in a template node. + * It encodes the credential type name and the credential name. + * Uses a symbol typing trick to get nominal typing. + * Use `keyFromCredentialTypeAndName` to create a key. + */ +export type TemplateCredentialKey = string & { [credentialKeySymbol]: never }; + +/** + * Forms a key from credential type name and credential name + */ +export const keyFromCredentialTypeAndName = ( + credentialTypeName: string, + credentialName: string, +): TemplateCredentialKey => `${credentialTypeName}-${credentialName}` as TemplateCredentialKey; + /** * Checks if a template workflow node has credentials defined */ @@ -32,16 +50,16 @@ export const normalizeTemplateNodeCredentials = ( * * @example * const nodeCredentials = { twitterOAuth1Api: "twitter" }; - * const toReplaceByType = { twitter: { + * const toReplaceByKey = { 'twitterOAuth1Api-twitter': { * id: "BrEOZ5Cje6VYh9Pc", * name: "X OAuth account" * }}; - * replaceTemplateNodeCredentials(nodeCredentials, toReplaceByType); + * replaceTemplateNodeCredentials(nodeCredentials, toReplaceByKey); * // => { twitterOAuth1Api: { id: "BrEOZ5Cje6VYh9Pc", name: "X OAuth account" } } */ export const replaceTemplateNodeCredentials = ( nodeCredentials: IWorkflowTemplateNodeCredentials, - toReplaceByName: Record, + toReplaceByKey: Record, ) => { if (!nodeCredentials) { return undefined; @@ -51,7 +69,8 @@ export const replaceTemplateNodeCredentials = ( const normalizedCredentials = normalizeTemplateNodeCredentials(nodeCredentials); for (const credentialType in normalizedCredentials) { const credentialNameInTemplate = normalizedCredentials[credentialType]; - const toReplaceWith = toReplaceByName[credentialNameInTemplate]; + const key = keyFromCredentialTypeAndName(credentialType, credentialNameInTemplate); + const toReplaceWith = toReplaceByKey[key]; if (toReplaceWith) { newNodeCredentials[credentialType] = toReplaceWith; } @@ -66,7 +85,7 @@ export const replaceTemplateNodeCredentials = ( */ export const replaceAllTemplateNodeCredentials = ( nodes: IWorkflowTemplateNode[], - toReplaceWith: Record, + toReplaceWith: Record, ) => { return nodes.map((node) => { if (hasNodeCredentials(node)) { diff --git a/packages/editor-ui/src/utils/testData/templateTestData.ts b/packages/editor-ui/src/utils/testData/templateTestData.ts new file mode 100644 index 0000000000..92325e4602 --- /dev/null +++ b/packages/editor-ui/src/utils/testData/templateTestData.ts @@ -0,0 +1,16 @@ +// eslint-disable-next-line import/no-extraneous-dependencies +import { faker } from '@faker-js/faker/locale/en'; +import type { IWorkflowTemplateNode } from '@/Interface'; + +export const newWorkflowTemplateNode = ({ + type, + ...optionalOpts +}: Pick & + Partial): IWorkflowTemplateNode => ({ + type, + name: faker.commerce.productName(), + position: [0, 0], + parameters: {}, + typeVersion: 1, + ...optionalOpts, +}); diff --git a/packages/editor-ui/src/views/SetupWorkflowFromTemplateView/SetupTemplateFormStep.vue b/packages/editor-ui/src/views/SetupWorkflowFromTemplateView/SetupTemplateFormStep.vue index 5db317c1f9..161d501d00 100644 --- a/packages/editor-ui/src/views/SetupWorkflowFromTemplateView/SetupTemplateFormStep.vue +++ b/packages/editor-ui/src/views/SetupWorkflowFromTemplateView/SetupTemplateFormStep.vue @@ -1,13 +1,14 @@