mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-25 12:44:07 -08:00
fix: Fix credential setup for templates with unnamed credentials (no-changelog) (#7891)
The template credentials were matched before solely based on the credential name on the template. This fixes the matching to use both credential type name and credential name.
This commit is contained in:
parent
bcd04981ef
commit
f8e93b3852
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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<string, INodeCredentialsDetails>,
|
||||
credentialOverrides: Record<TemplateCredentialKey, INodeCredentialsDetails>,
|
||||
rootStore: ReturnType<typeof useRootStore>,
|
||||
workflowsStore: ReturnType<typeof useWorkflowsStore>,
|
||||
) {
|
||||
|
|
|
@ -5,6 +5,24 @@ import type { INodeCredentials, INodeCredentialsDetails } from 'n8n-workflow';
|
|||
export type IWorkflowTemplateNodeWithCredentials = IWorkflowTemplateNode &
|
||||
Required<Pick<IWorkflowTemplateNode, 'credentials'>>;
|
||||
|
||||
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<string, INodeCredentialsDetails>,
|
||||
toReplaceByKey: Record<TemplateCredentialKey, INodeCredentialsDetails>,
|
||||
) => {
|
||||
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<string, INodeCredentialsDetails>,
|
||||
toReplaceWith: Record<TemplateCredentialKey, INodeCredentialsDetails>,
|
||||
) => {
|
||||
return nodes.map((node) => {
|
||||
if (hasNodeCredentials(node)) {
|
||||
|
|
16
packages/editor-ui/src/utils/testData/templateTestData.ts
Normal file
16
packages/editor-ui/src/utils/testData/templateTestData.ts
Normal file
|
@ -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<IWorkflowTemplateNode, 'type'> &
|
||||
Partial<IWorkflowTemplateNode>): IWorkflowTemplateNode => ({
|
||||
type,
|
||||
name: faker.commerce.productName(),
|
||||
position: [0, 0],
|
||||
parameters: {},
|
||||
typeVersion: 1,
|
||||
...optionalOpts,
|
||||
});
|
|
@ -1,13 +1,14 @@
|
|||
<script setup lang="ts">
|
||||
import type { PropType } from 'vue';
|
||||
import { computed } from 'vue';
|
||||
import N8nHeading from 'n8n-design-system/components/N8nHeading';
|
||||
import NodeIcon from '@/components/NodeIcon.vue';
|
||||
import CredentialPicker from '@/components/CredentialPicker/CredentialPicker.vue';
|
||||
import IconSuccess from './IconSuccess.vue';
|
||||
import { assert } from '@/utils/assert';
|
||||
import { getAppNameFromNodeName } from '@/utils/nodeTypesUtils';
|
||||
import { formatList } from '@/utils/formatters/listFormatter';
|
||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||
import type { CredentialUsages } from '@/views/SetupWorkflowFromTemplateView/setupTemplate.store';
|
||||
import { useSetupTemplateStore } from '@/views/SetupWorkflowFromTemplateView/setupTemplate.store';
|
||||
import type { IWorkflowTemplateNode } from '@/Interface';
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
|
@ -18,8 +19,8 @@ const props = defineProps({
|
|||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
credentialName: {
|
||||
type: String,
|
||||
credentials: {
|
||||
type: Object as PropType<CredentialUsages>,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
@ -31,34 +32,26 @@ const i18n = useI18n();
|
|||
|
||||
//#region Computed
|
||||
|
||||
const credentials = computed(() => {
|
||||
const credential = setupTemplateStore.credentialsByName.get(props.credentialName);
|
||||
assert(credential);
|
||||
return credential;
|
||||
});
|
||||
|
||||
const node = computed(() => credentials.value.usedBy[0]);
|
||||
const node = computed(() => props.credentials.usedBy[0]);
|
||||
|
||||
const nodeType = computed(() =>
|
||||
nodeTypesStore.getNodeType(node.value.type, node.value.typeVersion),
|
||||
);
|
||||
|
||||
const credentialType = computed(() => credentials.value.credentialType);
|
||||
|
||||
const appName = computed(() =>
|
||||
nodeType.value ? getAppNameFromNodeName(nodeType.value.displayName) : node.value.type,
|
||||
);
|
||||
|
||||
const nodeNames = computed(() => {
|
||||
const formatNodeName = (nodeToFormat: IWorkflowTemplateNode) => `<b>${nodeToFormat.name}</b>`;
|
||||
return formatList(credentials.value.usedBy, {
|
||||
return formatList(props.credentials.usedBy, {
|
||||
formatFn: formatNodeName,
|
||||
i18n,
|
||||
});
|
||||
});
|
||||
|
||||
const selectedCredentialId = computed(
|
||||
() => setupTemplateStore.selectedCredentialIdByName[props.credentialName],
|
||||
() => setupTemplateStore.selectedCredentialIdByKey[props.credentials.key],
|
||||
);
|
||||
|
||||
//#endregion Computed
|
||||
|
@ -66,11 +59,11 @@ const selectedCredentialId = computed(
|
|||
//#region Methods
|
||||
|
||||
const onCredentialSelected = (credentialId: string) => {
|
||||
setupTemplateStore.setSelectedCredentialId(props.credentialName, credentialId);
|
||||
setupTemplateStore.setSelectedCredentialId(props.credentials.key, credentialId);
|
||||
};
|
||||
|
||||
const onCredentialDeselected = () => {
|
||||
setupTemplateStore.unsetSelectedCredential(props.credentialName);
|
||||
setupTemplateStore.unsetSelectedCredential(props.credentials.key);
|
||||
};
|
||||
|
||||
//#endregion Methods
|
||||
|
@ -101,7 +94,7 @@ const onCredentialDeselected = () => {
|
|||
<CredentialPicker
|
||||
:class="$style.credentialPicker"
|
||||
:app-name="appName"
|
||||
:credentialType="credentialType"
|
||||
:credentialType="props.credentials.credentialType"
|
||||
:selectedCredentialId="selectedCredentialId"
|
||||
@credential-selected="onCredentialSelected"
|
||||
@credential-deselected="onCredentialDeselected"
|
||||
|
|
|
@ -120,11 +120,10 @@ onMounted(async () => {
|
|||
<ol v-if="isReady" :class="$style.appCredentialsContainer">
|
||||
<SetupTemplateFormStep
|
||||
:class="$style.appCredential"
|
||||
v-bind:key="credentials.credentialName"
|
||||
:key="credentials.key"
|
||||
v-for="(credentials, index) in setupTemplateStore.credentialUsages"
|
||||
:order="index + 1"
|
||||
:credentials="credentials"
|
||||
:credentialName="credentials.credentialName"
|
||||
/>
|
||||
</ol>
|
||||
<div v-else :class="$style.appCredentialsContainer">
|
||||
|
|
|
@ -1,19 +1,24 @@
|
|||
import { useTemplatesStore } from '@/stores/templates.store';
|
||||
import type { IWorkflowTemplateNodeWithCredentials } from '@/utils/templates/templateTransforms';
|
||||
import { keyFromCredentialTypeAndName } from '@/utils/templates/templateTransforms';
|
||||
import type {
|
||||
TemplateCredentialKey,
|
||||
IWorkflowTemplateNodeWithCredentials,
|
||||
} from '@/utils/templates/templateTransforms';
|
||||
import type { CredentialUsages } from '@/views/SetupWorkflowFromTemplateView/setupTemplate.store';
|
||||
import {
|
||||
getAppCredentials,
|
||||
getAppsRequiringCredentials,
|
||||
groupNodeCredentialsByName,
|
||||
useSetupTemplateStore,
|
||||
groupNodeCredentialsByKey,
|
||||
} from '@/views/SetupWorkflowFromTemplateView/setupTemplate.store';
|
||||
import { setActivePinia } from 'pinia';
|
||||
import * as testData from './setupTemplate.store.testData';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { useCredentialsStore } from '@/stores/credentials.store';
|
||||
import { newWorkflowTemplateNode } from '@/utils/testData/templateTestData';
|
||||
|
||||
const objToMap = <T>(obj: Record<string, T>) => {
|
||||
return new Map<string, T>(Object.entries(obj));
|
||||
const objToMap = <TKey extends string, T>(obj: Record<TKey, T>) => {
|
||||
return new Map(Object.entries(obj)) as Map<TKey, T>;
|
||||
};
|
||||
|
||||
describe('SetupWorkflowFromTemplateView store', () => {
|
||||
|
@ -60,27 +65,30 @@ describe('SetupWorkflowFromTemplateView store', () => {
|
|||
},
|
||||
} satisfies Record<string, IWorkflowTemplateNodeWithCredentials>;
|
||||
|
||||
describe('groupNodeCredentialsByName', () => {
|
||||
describe('groupNodeCredentialsByTypeAndName', () => {
|
||||
it('returns an empty array if there are no nodes', () => {
|
||||
expect(groupNodeCredentialsByName([])).toEqual(new Map());
|
||||
expect(groupNodeCredentialsByKey([])).toEqual(new Map());
|
||||
});
|
||||
|
||||
it('returns credentials grouped by name', () => {
|
||||
expect(groupNodeCredentialsByName(Object.values(nodesByName))).toEqual(
|
||||
it('returns credentials grouped by type and name', () => {
|
||||
expect(groupNodeCredentialsByKey(Object.values(nodesByName))).toEqual(
|
||||
objToMap({
|
||||
twitter: {
|
||||
'twitterOAuth1Api-twitter': {
|
||||
key: 'twitterOAuth1Api-twitter',
|
||||
credentialName: 'twitter',
|
||||
credentialType: 'twitterOAuth1Api',
|
||||
nodeTypeName: 'n8n-nodes-base.twitter',
|
||||
usedBy: [nodesByName.Twitter],
|
||||
},
|
||||
telegram: {
|
||||
'telegramApi-telegram': {
|
||||
key: 'telegramApi-telegram',
|
||||
credentialName: 'telegram',
|
||||
credentialType: 'telegramApi',
|
||||
nodeTypeName: 'n8n-nodes-base.telegram',
|
||||
usedBy: [nodesByName.Telegram],
|
||||
},
|
||||
shopify: {
|
||||
'shopifyApi-shopify': {
|
||||
key: 'shopifyApi-shopify',
|
||||
credentialName: 'shopify',
|
||||
credentialType: 'shopifyApi',
|
||||
nodeTypeName: 'n8n-nodes-base.shopifyTrigger',
|
||||
|
@ -89,6 +97,42 @@ describe('SetupWorkflowFromTemplateView store', () => {
|
|||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('returns credentials grouped when the credential names are the same', () => {
|
||||
const [node1, node2] = [
|
||||
newWorkflowTemplateNode({
|
||||
type: 'n8n-nodes-base.twitter',
|
||||
credentials: {
|
||||
twitterOAuth1Api: 'credential',
|
||||
},
|
||||
}) as IWorkflowTemplateNodeWithCredentials,
|
||||
newWorkflowTemplateNode({
|
||||
type: 'n8n-nodes-base.telegram',
|
||||
credentials: {
|
||||
telegramApi: 'credential',
|
||||
},
|
||||
}) as IWorkflowTemplateNodeWithCredentials,
|
||||
];
|
||||
|
||||
expect(groupNodeCredentialsByKey([node1, node2])).toEqual(
|
||||
objToMap({
|
||||
'twitterOAuth1Api-credential': {
|
||||
key: 'twitterOAuth1Api-credential',
|
||||
credentialName: 'credential',
|
||||
credentialType: 'twitterOAuth1Api',
|
||||
nodeTypeName: 'n8n-nodes-base.twitter',
|
||||
usedBy: [node1],
|
||||
},
|
||||
'telegramApi-credential': {
|
||||
key: 'telegramApi-credential',
|
||||
credentialName: 'credential',
|
||||
credentialType: 'telegramApi',
|
||||
nodeTypeName: 'n8n-nodes-base.telegram',
|
||||
usedBy: [node2],
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAppsRequiringCredentials', () => {
|
||||
|
@ -98,8 +142,9 @@ describe('SetupWorkflowFromTemplateView store', () => {
|
|||
});
|
||||
|
||||
it('returns an array of apps requiring credentials', () => {
|
||||
const credentialUsages: Map<string, CredentialUsages> = objToMap({
|
||||
twitter: {
|
||||
const credentialUsages = objToMap<TemplateCredentialKey, CredentialUsages>({
|
||||
[keyFromCredentialTypeAndName('twitterOAuth1Api', 'twitter')]: {
|
||||
key: keyFromCredentialTypeAndName('twitterOAuth1Api', 'twitter'),
|
||||
credentialName: 'twitter',
|
||||
credentialType: 'twitterOAuth1Api',
|
||||
nodeTypeName: 'n8n-nodes-base.twitter',
|
||||
|
@ -127,6 +172,7 @@ describe('SetupWorkflowFromTemplateView store', () => {
|
|||
it('returns an array of apps requiring credentials', () => {
|
||||
const credentialUsages: CredentialUsages[] = [
|
||||
{
|
||||
key: keyFromCredentialTypeAndName('twitterOAuth1Api', 'twitter'),
|
||||
credentialName: 'twitter',
|
||||
credentialType: 'twitterOAuth1Api',
|
||||
nodeTypeName: 'n8n-nodes-base.twitter',
|
||||
|
@ -141,6 +187,7 @@ describe('SetupWorkflowFromTemplateView store', () => {
|
|||
appName: 'Twitter',
|
||||
credentials: [
|
||||
{
|
||||
key: 'twitterOAuth1Api-twitter',
|
||||
credentialName: 'twitter',
|
||||
credentialType: 'twitterOAuth1Api',
|
||||
nodeTypeName: 'n8n-nodes-base.twitter',
|
||||
|
@ -152,6 +199,54 @@ describe('SetupWorkflowFromTemplateView store', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('credentialOverrides', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(
|
||||
createTestingPinia({
|
||||
stubActions: false,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('returns an empty object if there are no credential overrides', () => {
|
||||
// Setup
|
||||
const credentialsStore = useCredentialsStore();
|
||||
credentialsStore.setCredentialTypes([testData.credentialTypeTelegram]);
|
||||
const templatesStore = useTemplatesStore();
|
||||
templatesStore.addWorkflows([testData.fullShopifyTelegramTwitterTemplate]);
|
||||
|
||||
const setupTemplateStore = useSetupTemplateStore();
|
||||
setupTemplateStore.setTemplateId(testData.fullShopifyTelegramTwitterTemplate.id.toString());
|
||||
|
||||
expect(setupTemplateStore.credentialUsages.length).toBe(3);
|
||||
expect(setupTemplateStore.credentialOverrides).toEqual({});
|
||||
});
|
||||
|
||||
it('returns overrides for one node', () => {
|
||||
// Setup
|
||||
const credentialsStore = useCredentialsStore();
|
||||
credentialsStore.setCredentialTypes([testData.credentialTypeTelegram]);
|
||||
credentialsStore.setCredentials([testData.credentialsTelegram1]);
|
||||
const templatesStore = useTemplatesStore();
|
||||
templatesStore.addWorkflows([testData.fullShopifyTelegramTwitterTemplate]);
|
||||
|
||||
const setupTemplateStore = useSetupTemplateStore();
|
||||
setupTemplateStore.setTemplateId(testData.fullShopifyTelegramTwitterTemplate.id.toString());
|
||||
setupTemplateStore.setSelectedCredentialId(
|
||||
keyFromCredentialTypeAndName('twitterOAuth1Api', 'twitter'),
|
||||
testData.credentialsTelegram1.id,
|
||||
);
|
||||
|
||||
expect(setupTemplateStore.credentialUsages.length).toBe(3);
|
||||
expect(setupTemplateStore.credentialOverrides).toEqual({
|
||||
'twitterOAuth1Api-twitter': {
|
||||
id: testData.credentialsTelegram1.id,
|
||||
name: testData.credentialsTelegram1.name,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('setInitialCredentialsSelection', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(
|
||||
|
@ -174,7 +269,7 @@ describe('SetupWorkflowFromTemplateView store', () => {
|
|||
// Execute
|
||||
setupTemplateStore.setInitialCredentialSelection();
|
||||
|
||||
expect(setupTemplateStore.selectedCredentialIdByName).toEqual({});
|
||||
expect(setupTemplateStore.selectedCredentialIdByKey).toEqual({});
|
||||
});
|
||||
|
||||
it("selects credential when there's only one", () => {
|
||||
|
@ -191,8 +286,8 @@ describe('SetupWorkflowFromTemplateView store', () => {
|
|||
// Execute
|
||||
setupTemplateStore.setInitialCredentialSelection();
|
||||
|
||||
expect(setupTemplateStore.selectedCredentialIdByName).toEqual({
|
||||
telegram_habot: 'YaSKdvEcT1TSFrrr1',
|
||||
expect(setupTemplateStore.selectedCredentialIdByKey).toEqual({
|
||||
[keyFromCredentialTypeAndName('telegramApi', 'telegram_habot')]: 'YaSKdvEcT1TSFrrr1',
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -213,7 +308,7 @@ describe('SetupWorkflowFromTemplateView store', () => {
|
|||
// Execute
|
||||
setupTemplateStore.setInitialCredentialSelection();
|
||||
|
||||
expect(setupTemplateStore.selectedCredentialIdByName).toEqual({});
|
||||
expect(setupTemplateStore.selectedCredentialIdByKey).toEqual({});
|
||||
});
|
||||
|
||||
test.each([
|
||||
|
@ -255,7 +350,7 @@ describe('SetupWorkflowFromTemplateView store', () => {
|
|||
setupTemplateStore.setInitialCredentialSelection();
|
||||
|
||||
expect(setupTemplateStore.credentialUsages.length).toBe(1);
|
||||
expect(setupTemplateStore.selectedCredentialIdByName).toEqual({});
|
||||
expect(setupTemplateStore.selectedCredentialIdByKey).toEqual({});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -19,9 +19,13 @@ import type {
|
|||
import type { Telemetry } from '@/plugins/telemetry';
|
||||
import { VIEWS } from '@/constants';
|
||||
import { createWorkflowFromTemplate } from '@/utils/templates/templateActions';
|
||||
import type { IWorkflowTemplateNodeWithCredentials } from '@/utils/templates/templateTransforms';
|
||||
import type {
|
||||
TemplateCredentialKey,
|
||||
IWorkflowTemplateNodeWithCredentials,
|
||||
} from '@/utils/templates/templateTransforms';
|
||||
import {
|
||||
hasNodeCredentials,
|
||||
keyFromCredentialTypeAndName,
|
||||
normalizeTemplateNodeCredentials,
|
||||
} from '@/utils/templates/templateTransforms';
|
||||
|
||||
|
@ -37,6 +41,11 @@ export type RequiredCredentials = {
|
|||
};
|
||||
|
||||
export type CredentialUsages = {
|
||||
/**
|
||||
* Key is a combination of the credential name and the credential type name,
|
||||
* e.g. "twitter-twitterOAuth1Api"
|
||||
*/
|
||||
key: TemplateCredentialKey;
|
||||
credentialName: string;
|
||||
credentialType: string;
|
||||
nodeTypeName: string;
|
||||
|
@ -65,30 +74,32 @@ export const getNodesRequiringCredentials = (
|
|||
return template.workflow.nodes.filter(hasNodeCredentials);
|
||||
};
|
||||
|
||||
export const groupNodeCredentialsByName = (nodes: IWorkflowTemplateNodeWithCredentials[]) => {
|
||||
const credentialsByName = new Map<string, CredentialUsages>();
|
||||
export const groupNodeCredentialsByKey = (nodes: IWorkflowTemplateNodeWithCredentials[]) => {
|
||||
const credentialsByTypeName = new Map<TemplateCredentialKey, CredentialUsages>();
|
||||
|
||||
for (const node of nodes) {
|
||||
const normalizedCreds = normalizeTemplateNodeCredentials(node.credentials);
|
||||
for (const credentialType in normalizedCreds) {
|
||||
const credentialName = normalizedCreds[credentialType];
|
||||
const key = keyFromCredentialTypeAndName(credentialType, credentialName);
|
||||
|
||||
let credentialUsages = credentialsByName.get(credentialName);
|
||||
let credentialUsages = credentialsByTypeName.get(key);
|
||||
if (!credentialUsages) {
|
||||
credentialUsages = {
|
||||
key,
|
||||
nodeTypeName: node.type,
|
||||
credentialName,
|
||||
credentialType,
|
||||
usedBy: [],
|
||||
};
|
||||
credentialsByName.set(credentialName, credentialUsages);
|
||||
credentialsByTypeName.set(key, credentialUsages);
|
||||
}
|
||||
|
||||
credentialUsages.usedBy.push(node);
|
||||
}
|
||||
}
|
||||
|
||||
return credentialsByName;
|
||||
return credentialsByTypeName;
|
||||
};
|
||||
|
||||
export const getAppCredentials = (
|
||||
|
@ -154,8 +165,8 @@ export const useSetupTemplateStore = defineStore('setupTemplate', () => {
|
|||
* Credentials user has selected from the UI. Map from credential
|
||||
* name in the template to the credential ID.
|
||||
*/
|
||||
const selectedCredentialIdByName = ref<
|
||||
Record<CredentialUsages['credentialName'], ICredentialsResponse['id']>
|
||||
const selectedCredentialIdByKey = ref<
|
||||
Record<CredentialUsages['key'], ICredentialsResponse['id']>
|
||||
>({});
|
||||
|
||||
//#endregion State
|
||||
|
@ -185,12 +196,12 @@ export const useSetupTemplateStore = defineStore('setupTemplate', () => {
|
|||
return nodeType ? getAppNameFromNodeName(nodeType.displayName) : nodeTypeName;
|
||||
};
|
||||
|
||||
const credentialsByName = computed(() => {
|
||||
return groupNodeCredentialsByName(nodesRequiringCredentialsSorted.value);
|
||||
const credentialsByKey = computed(() => {
|
||||
return groupNodeCredentialsByKey(nodesRequiringCredentialsSorted.value);
|
||||
});
|
||||
|
||||
const credentialUsages = computed(() => {
|
||||
return Array.from(credentialsByName.value.values());
|
||||
return Array.from(credentialsByKey.value.values());
|
||||
});
|
||||
|
||||
const appCredentials = computed(() => {
|
||||
|
@ -198,20 +209,16 @@ export const useSetupTemplateStore = defineStore('setupTemplate', () => {
|
|||
});
|
||||
|
||||
const credentialOverrides = computed(() => {
|
||||
const overrides: Record<string, INodeCredentialsDetails> = {};
|
||||
|
||||
for (const credentialNameInTemplate of Object.keys(selectedCredentialIdByName.value)) {
|
||||
const credentialId = selectedCredentialIdByName.value[credentialNameInTemplate];
|
||||
if (!credentialId) {
|
||||
continue;
|
||||
}
|
||||
const overrides: Record<TemplateCredentialKey, INodeCredentialsDetails> = {};
|
||||
|
||||
for (const [key, credentialId] of Object.entries(selectedCredentialIdByKey.value)) {
|
||||
const credential = credentialsStore.getCredentialById(credentialId);
|
||||
if (!credential) {
|
||||
continue;
|
||||
}
|
||||
|
||||
overrides[credentialNameInTemplate] = {
|
||||
// Object.entries fails to give the more accurate key type
|
||||
overrides[key as TemplateCredentialKey] = {
|
||||
id: credentialId,
|
||||
name: credential.name,
|
||||
};
|
||||
|
@ -221,7 +228,7 @@ export const useSetupTemplateStore = defineStore('setupTemplate', () => {
|
|||
});
|
||||
|
||||
const numCredentialsLeft = computed(() => {
|
||||
return credentialUsages.value.length - Object.keys(selectedCredentialIdByName.value).length;
|
||||
return credentialUsages.value.length - Object.keys(selectedCredentialIdByKey.value).length;
|
||||
});
|
||||
|
||||
//#endregion Getters
|
||||
|
@ -255,7 +262,7 @@ export const useSetupTemplateStore = defineStore('setupTemplate', () => {
|
|||
const availableCreds = credentialsStore.getCredentialsByType(credUsage.credentialType);
|
||||
|
||||
if (availableCreds.length === 1) {
|
||||
selectedCredentialIdByName.value[credUsage.credentialName] = availableCreds[0].id;
|
||||
selectedCredentialIdByKey.value[credUsage.key] = availableCreds[0].id;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -279,7 +286,7 @@ export const useSetupTemplateStore = defineStore('setupTemplate', () => {
|
|||
const init = async () => {
|
||||
isLoading.value = true;
|
||||
try {
|
||||
selectedCredentialIdByName.value = {};
|
||||
selectedCredentialIdByKey.value = {};
|
||||
|
||||
await Promise.all([
|
||||
credentialsStore.fetchAllCredentials(),
|
||||
|
@ -349,26 +356,27 @@ export const useSetupTemplateStore = defineStore('setupTemplate', () => {
|
|||
}
|
||||
};
|
||||
|
||||
const setSelectedCredentialId = (credentialName: string, credentialId: string) => {
|
||||
selectedCredentialIdByName.value[credentialName] = credentialId;
|
||||
const setSelectedCredentialId = (credentialKey: TemplateCredentialKey, credentialId: string) => {
|
||||
selectedCredentialIdByKey.value[credentialKey] = credentialId;
|
||||
};
|
||||
|
||||
const unsetSelectedCredential = (credentialName: string) => {
|
||||
delete selectedCredentialIdByName.value[credentialName];
|
||||
const unsetSelectedCredential = (credentialKey: TemplateCredentialKey) => {
|
||||
delete selectedCredentialIdByKey.value[credentialKey];
|
||||
};
|
||||
|
||||
//#endregion Actions
|
||||
|
||||
return {
|
||||
credentialsByName,
|
||||
credentialsByKey,
|
||||
isLoading,
|
||||
isSaving,
|
||||
appCredentials,
|
||||
nodesRequiringCredentialsSorted,
|
||||
template,
|
||||
credentialUsages,
|
||||
selectedCredentialIdByName,
|
||||
selectedCredentialIdByKey,
|
||||
numCredentialsLeft,
|
||||
credentialOverrides,
|
||||
createWorkflow,
|
||||
skipSetup,
|
||||
init,
|
||||
|
|
Loading…
Reference in a new issue