feat(editor): Add HTTP request nodes for credentials without a node (#7157)

Github issue / Community forum post (link here to close automatically):

---------

Co-authored-by: Giulio Andreini <g.andreini@gmail.com>
Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <aditya@netroy.in>
This commit is contained in:
Elias Meire 2023-11-13 12:11:16 +01:00 committed by GitHub
parent 460ac85fda
commit 14035e1244
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
62 changed files with 665 additions and 146 deletions

View file

@ -478,11 +478,9 @@ describe('Node Creator', () => {
nodeCreatorFeature.getters.searchBar().find('input').clear().type('wa'); nodeCreatorFeature.getters.searchBar().find('input').clear().type('wa');
nodeCreatorFeature.getters.nodeItemName().first().should('have.text', 'Wait'); nodeCreatorFeature.getters.nodeItemName().first().should('have.text', 'Wait');
nodeCreatorFeature.getters.nodeItemName().eq(1).should('have.text', 'Merge');
nodeCreatorFeature.getters.searchBar().find('input').clear().type('wait'); nodeCreatorFeature.getters.searchBar().find('input').clear().type('wait');
nodeCreatorFeature.getters.nodeItemName().first().should('have.text', 'Wait'); nodeCreatorFeature.getters.nodeItemName().first().should('have.text', 'Wait');
nodeCreatorFeature.getters.nodeItemName().eq(1).should('have.text', 'Merge');
nodeCreatorFeature.getters.searchBar().find('input').clear().type('spreadsheet'); nodeCreatorFeature.getters.searchBar().find('input').clear().type('spreadsheet');
nodeCreatorFeature.getters.nodeItemName().first().should('have.text', 'Spreadsheet File'); nodeCreatorFeature.getters.nodeItemName().first().should('have.text', 'Spreadsheet File');

View file

@ -1,6 +1,8 @@
import { WorkflowPage, NDV } from '../pages'; import { WorkflowPage, NDV } from '../pages';
import { NodeCreator } from '../pages/features/node-creator';
const workflowPage = new WorkflowPage(); const workflowPage = new WorkflowPage();
const nodeCreatorFeature = new NodeCreator();
const ndv = new NDV(); const ndv = new NDV();
describe('HTTP Request node', () => { describe('HTTP Request node', () => {
@ -18,4 +20,40 @@ describe('HTTP Request node', () => {
ndv.getters.outputPanel().contains('fact'); ndv.getters.outputPanel().contains('fact');
}); });
describe('Credential-only HTTP Request Node variants', () => {
it('should render a modified HTTP Request Node', () => {
workflowPage.actions.addInitialNodeToCanvas('Manual');
workflowPage.getters.nodeCreatorPlusButton().click();
workflowPage.getters.nodeCreatorSearchBar().type('VirusTotal');
expect(nodeCreatorFeature.getters.nodeItemName().first().should('have.text', 'VirusTotal'));
expect(
nodeCreatorFeature.getters
.nodeItemDescription()
.first()
.should('have.text', 'HTTP request'),
);
nodeCreatorFeature.actions.selectNode('VirusTotal');
expect(ndv.getters.nodeNameContainer().should('contain.text', 'VirusTotal HTTP Request'));
expect(
ndv.getters
.parameterInput('url')
.find('input')
.should('contain.value', 'https://www.virustotal.com/api/v3/'),
);
// These parameters exist for normal HTTP Request Node, but are hidden for credential-only variants
expect(ndv.getters.parameterInput('authentication').should('not.exist'));
expect(ndv.getters.parameterInput('nodeCredentialType').should('not.exist'));
expect(
workflowPage.getters
.nodeCredentialsLabel()
.should('contain.text', 'Credential for VirusTotal'),
);
});
});
}); });

View file

@ -20,6 +20,7 @@ export class NodeCreator extends BasePage {
communityNodeTooltip: () => cy.getByTestId('node-item-community-tooltip'), communityNodeTooltip: () => cy.getByTestId('node-item-community-tooltip'),
noResults: () => cy.getByTestId('node-creator-no-results'), noResults: () => cy.getByTestId('node-creator-no-results'),
nodeItemName: () => cy.getByTestId('node-creator-item-name'), nodeItemName: () => cy.getByTestId('node-creator-item-name'),
nodeItemDescription: () => cy.getByTestId('node-creator-item-description'),
activeSubcategory: () => cy.getByTestId('nodes-list-header'), activeSubcategory: () => cy.getByTestId('nodes-list-header'),
expandedCategories: () => expandedCategories: () =>
this.getters.creatorItem().find('>div').filter('.active').invoke('text'), this.getters.creatorItem().find('>div').filter('.active').invoke('text'),

View file

@ -17,8 +17,8 @@ export class CredentialTypes implements ICredentialTypes {
return this.getCredential(credentialType).type; return this.getCredential(credentialType).type;
} }
getNodeTypesToTestWith(type: string): string[] { getSupportedNodes(type: string): string[] {
return this.loadNodesAndCredentials.knownCredentials[type]?.nodesToTestWith ?? []; return this.loadNodesAndCredentials.knownCredentials[type]?.supportedNodes ?? [];
} }
/** /**

View file

@ -490,8 +490,8 @@ export class CredentialsHelper extends ICredentialsHelper {
}; };
} }
const nodeTypesToTestWith = this.credentialTypes.getNodeTypesToTestWith(credentialType); const supportedNodes = this.credentialTypes.getSupportedNodes(credentialType);
for (const nodeName of nodeTypesToTestWith) { for (const nodeName of supportedNodes) {
const node = this.nodeTypes.getByName(nodeName); const node = this.nodeTypes.getByName(nodeName);
// Always set to an array even if node is not versioned to not having // Always set to an array even if node is not versioned to not having

View file

@ -290,15 +290,15 @@ export class LoadNodesAndCredentials {
const { const {
className, className,
sourcePath, sourcePath,
nodesToTestWith, supportedNodes,
extends: extendsArr, extends: extendsArr,
} = known.credentials[type]; } = known.credentials[type];
this.known.credentials[type] = { this.known.credentials[type] = {
className, className,
sourcePath: path.join(directory, sourcePath), sourcePath: path.join(directory, sourcePath),
nodesToTestWith: supportedNodes:
loader instanceof PackageDirectoryLoader loader instanceof PackageDirectoryLoader
? nodesToTestWith?.map((nodeName) => `${loader.packageName}.${nodeName}`) ? supportedNodes?.map((nodeName) => `${loader.packageName}.${nodeName}`)
: undefined, : undefined,
extends: extendsArr, extends: extendsArr,
}; };

View file

@ -2,7 +2,8 @@
const path = require('path'); const path = require('path');
const glob = require('fast-glob'); const glob = require('fast-glob');
const { LoggerProxy } = require('n8n-workflow'); const uniq = require('lodash/uniq');
const { LoggerProxy, getCredentialsForNode } = require('n8n-workflow');
const { packageDir, writeJSON } = require('./common'); const { packageDir, writeJSON } = require('./common');
const { loadClassInIsolation } = require('../dist/ClassLoader'); const { loadClassInIsolation } = require('../dist/ClassLoader');
@ -19,48 +20,80 @@ const loadClass = (sourcePath) => {
} }
}; };
const nodesToTestWith = {}; const generateKnownNodes = async () => {
const nodeClasses = glob
const generate = async (kind) => { .sync('dist/nodes/**/*.node.js', { cwd: packageDir })
const data = glob
.sync(`dist/${kind}/**/*.${kind === 'nodes' ? 'node' : kind}.js`, {
cwd: packageDir,
})
.map(loadClass) .map(loadClass)
.filter((data) => !!data) // Ignore node versions
.reduce((obj, { className, sourcePath, instance }) => { .filter((nodeClass) => nodeClass && !/[vV]\d.node\.js$/.test(nodeClass.sourcePath));
const name = kind === 'nodes' ? instance.description.name : instance.name;
if (!/[vV]\d.node\.js$/.test(sourcePath)) { const nodes = {};
if (name in obj) console.error('already loaded', kind, name, sourcePath); const nodesByCredential = {};
else obj[name] = { className, sourcePath };
for (const { className, sourcePath, instance } of nodeClasses) {
const nodeName = instance.description.name;
nodes[nodeName] = { className, sourcePath };
for (const credential of getCredentialsForNode(instance)) {
if (!nodesByCredential[credential.name]) {
nodesByCredential[credential.name] = [];
} }
if (kind === 'credentials' && Array.isArray(instance.extends)) { nodesByCredential[credential.name].push(nodeName);
obj[name].extends = instance.extends; }
}
LoggerProxy.info(`Detected ${Object.keys(nodes).length} nodes`);
await writeJSON('known/nodes.json', nodes);
return { nodes, nodesByCredential };
};
const generateKnownCredentials = async (nodesByCredential) => {
const credentialClasses = glob
.sync(`dist/credentials/**/*.credentials.js`, { cwd: packageDir })
.map(loadClass)
.filter((data) => !!data);
for (const { instance } of credentialClasses) {
if (Array.isArray(instance.extends)) {
for (const extendedCredential of instance.extends) {
nodesByCredential[extendedCredential] = [
...(nodesByCredential[extendedCredential] ?? []),
...(nodesByCredential[instance.name] ?? []),
];
}
}
}
const credentials = credentialClasses.reduce(
(credentials, { className, sourcePath, instance }) => {
const credentialName = instance.name;
const credential = {
className,
sourcePath,
};
if (Array.isArray(instance.extends)) {
credential.extends = instance.extends;
} }
if (kind === 'nodes') { if (nodesByCredential[credentialName]) {
const { credentials } = instance.description; credential.supportedNodes = Array.from(new Set(nodesByCredential[credentialName]));
if (credentials && credentials.length) {
for (const credential of credentials) {
nodesToTestWith[credential.name] = nodesToTestWith[credential.name] || [];
nodesToTestWith[credential.name].push(name);
}
}
} else {
if (name in nodesToTestWith) {
obj[name].nodesToTestWith = nodesToTestWith[name];
}
} }
return obj;
}, {});
LoggerProxy.info(`Detected ${Object.keys(data).length} ${kind}`); credentials[credentialName] = credential;
await writeJSON(`known/${kind}.json`, data);
return data; return credentials;
},
{},
);
LoggerProxy.info(`Detected ${Object.keys(credentials).length} credentials`);
await writeJSON('known/credentials.json', credentials);
return credentials;
}; };
(async () => { (async () => {
await generate('nodes'); const { nodesByCredential } = await generateKnownNodes();
await generate('credentials'); await generateKnownCredentials(nodesByCredential);
})(); })();

View file

@ -45,7 +45,14 @@ function addWebhookLifecycle(nodeType) {
const loader = new PackageDirectoryLoader(packageDir); const loader = new PackageDirectoryLoader(packageDir);
await loader.loadAll(); await loader.loadAll();
const credentialTypes = Object.values(loader.credentialTypes).map((data) => data.type); const knownCredentials = loader.known.credentials;
const credentialTypes = Object.values(loader.credentialTypes).map((data) => {
const credentialType = data.type;
if (knownCredentials[credentialType.name].supportedNodes?.length > 0) {
delete credentialType.httpRequestNode;
}
return credentialType;
});
const loaderNodeTypes = Object.values(loader.nodeTypes); const loaderNodeTypes = Object.values(loader.nodeTypes);
@ -75,13 +82,12 @@ function addWebhookLifecycle(nodeType) {
addWebhookLifecycle(nodeType); addWebhookLifecycle(nodeType);
return data.type; return data.type;
}) })
.flatMap((nodeData) => { .flatMap((nodeType) =>
return NodeHelpers.getVersionedNodeTypeAll(nodeData).map((item) => { NodeHelpers.getVersionedNodeTypeAll(nodeType).map((item) => {
const { __loadOptionsMethods, ...rest } = item.description; const { __loadOptionsMethods, ...rest } = item.description;
return rest; return rest;
}); }),
}); );
const referencedMethods = findReferencedMethods(nodeTypes); const referencedMethods = findReferencedMethods(nodeTypes);

View file

@ -1,7 +1,5 @@
import * as path from 'path';
import { readFile } from 'fs/promises';
import glob from 'fast-glob'; import glob from 'fast-glob';
import { jsonParse, getVersionedNodeTypeAll, LoggerProxy as Logger } from 'n8n-workflow'; import { readFile } from 'fs/promises';
import type { import type {
CodexData, CodexData,
DocumentationLink, DocumentationLink,
@ -9,15 +7,22 @@ import type {
ICredentialTypeData, ICredentialTypeData,
INodeType, INodeType,
INodeTypeBaseDescription, INodeTypeBaseDescription,
INodeTypeDescription,
INodeTypeData, INodeTypeData,
INodeTypeDescription,
INodeTypeNameVersion, INodeTypeNameVersion,
IVersionedNodeType, IVersionedNodeType,
KnownNodesAndCredentials, KnownNodesAndCredentials,
} from 'n8n-workflow'; } from 'n8n-workflow';
import {
LoggerProxy as Logger,
getCredentialsForNode,
getVersionedNodeTypeAll,
jsonParse,
} from 'n8n-workflow';
import * as path from 'path';
import { loadClassInIsolation } from './ClassLoader';
import { CUSTOM_NODES_CATEGORY } from './Constants'; import { CUSTOM_NODES_CATEGORY } from './Constants';
import type { n8n } from './Interfaces'; import type { n8n } from './Interfaces';
import { loadClassInIsolation } from './ClassLoader';
function toJSON(this: ICredentialType) { function toJSON(this: ICredentialType) {
return { return {
@ -44,6 +49,8 @@ export abstract class DirectoryLoader {
types: Types = { nodes: [], credentials: [] }; types: Types = { nodes: [], credentials: [] };
protected nodesByCredential: Record<string, string[]> = {};
constructor( constructor(
readonly directory: string, readonly directory: string,
protected readonly excludeNodes: string[] = [], protected readonly excludeNodes: string[] = [],
@ -140,6 +147,13 @@ export abstract class DirectoryLoader {
getVersionedNodeTypeAll(tempNode).forEach(({ description }) => { getVersionedNodeTypeAll(tempNode).forEach(({ description }) => {
this.types.nodes.push(description); this.types.nodes.push(description);
}); });
for (const credential of getCredentialsForNode(tempNode)) {
if (!this.nodesByCredential[credential.name]) {
this.nodesByCredential[credential.name] = [];
}
this.nodesByCredential[credential.name].push(fullNodeName);
}
} }
protected loadCredentialFromFile(credentialName: string, filePath: string): void { protected loadCredentialFromFile(credentialName: string, filePath: string): void {
@ -168,6 +182,7 @@ export abstract class DirectoryLoader {
className: credentialName, className: credentialName,
sourcePath: filePath, sourcePath: filePath,
extends: tempCredential.extends, extends: tempCredential.extends,
supportedNodes: this.nodesByCredential[tempCredential.name],
}; };
this.credentialTypes[tempCredential.name] = { this.credentialTypes[tempCredential.name] = {
@ -276,19 +291,24 @@ export class CustomDirectoryLoader extends DirectoryLoader {
packageName = 'CUSTOM'; packageName = 'CUSTOM';
override async loadAll() { override async loadAll() {
const filePaths = await glob('**/*.@(node|credentials).js', { const nodes = await glob('**/*.node.js', {
cwd: this.directory, cwd: this.directory,
absolute: true, absolute: true,
}); });
for (const filePath of filePaths) { for (const nodePath of nodes) {
const [fileName, type] = path.parse(filePath).name.split('.'); const [fileName] = path.parse(nodePath).name.split('.');
this.loadNodeFromFile(fileName, nodePath);
}
if (type === 'node') { const credentials = await glob('**/*.credentials.js', {
this.loadNodeFromFile(fileName, filePath); cwd: this.directory,
} else if (type === 'credentials') { absolute: true,
this.loadCredentialFromFile(fileName, filePath); });
}
for (const credentialPath of credentials) {
const [fileName] = path.parse(credentialPath).name.split('.');
this.loadCredentialFromFile(fileName, credentialPath);
} }
} }
} }
@ -315,15 +335,6 @@ export class PackageDirectoryLoader extends DirectoryLoader {
const { nodes, credentials } = n8n; const { nodes, credentials } = n8n;
if (Array.isArray(credentials)) {
for (const credential of credentials) {
const filePath = this.resolvePath(credential);
const [credentialName] = path.parse(credential).name.split('.');
this.loadCredentialFromFile(credentialName, filePath);
}
}
if (Array.isArray(nodes)) { if (Array.isArray(nodes)) {
for (const node of nodes) { for (const node of nodes) {
const filePath = this.resolvePath(node); const filePath = this.resolvePath(node);
@ -333,6 +344,15 @@ export class PackageDirectoryLoader extends DirectoryLoader {
} }
} }
if (Array.isArray(credentials)) {
for (const credential of credentials) {
const filePath = this.resolvePath(credential);
const [credentialName] = path.parse(credential).name.split('.');
this.loadCredentialFromFile(credentialName, filePath);
}
}
Logger.debug(`Loaded all credentials and nodes from ${this.packageName}`, { Logger.debug(`Loaded all credentials and nodes from ${this.packageName}`, {
credentials: credentials?.length ?? 0, credentials: credentials?.length ?? 0,
nodes: nodes?.length ?? 0, nodes: nodes?.length ?? 0,

View file

@ -53,7 +53,12 @@ const i18n = useI18n();
<n8n-icon :class="$style.tooltipIcon" icon="cube" /> <n8n-icon :class="$style.tooltipIcon" icon="cube" />
</n8n-tooltip> </n8n-tooltip>
</div> </div>
<p :class="$style.description" v-if="description" v-text="description" /> <p
v-if="description"
data-test-id="node-creator-item-description"
:class="$style.description"
v-text="description"
/>
</div> </div>
<slot name="dragContent" /> <slot name="dragContent" />
<button :class="$style.panelIcon" v-if="showActionArrow"> <button :class="$style.panelIcon" v-if="showActionArrow">

View file

@ -24,6 +24,9 @@
<div v-if="type !== 'unknown'" :class="$style.icon"> <div v-if="type !== 'unknown'" :class="$style.icon">
<img v-if="type === 'file'" :src="src" :class="$style.nodeIconImage" /> <img v-if="type === 'file'" :src="src" :class="$style.nodeIconImage" />
<font-awesome-icon v-else :icon="name" :style="fontStyleData" /> <font-awesome-icon v-else :icon="name" :style="fontStyleData" />
<div v-if="badge" :class="$style.badge" :style="badgeStyleData">
<n8n-node-icon :type="badge.type" :src="badge.src" :size="badgeSize"></n8n-node-icon>
</div>
</div> </div>
<div v-else :class="$style.nodeIconPlaceholder"> <div v-else :class="$style.nodeIconPlaceholder">
{{ nodeTypeName ? nodeTypeName.charAt(0) : '?' }} {{ nodeTypeName ? nodeTypeName.charAt(0) : '?' }}
@ -35,9 +38,9 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue';
import N8nTooltip from '../N8nTooltip';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
import { defineComponent, type PropType } from 'vue';
import N8nTooltip from '../N8nTooltip';
export default defineComponent({ export default defineComponent({
name: 'n8n-node-icon', name: 'n8n-node-icon',
@ -75,6 +78,7 @@ export default defineComponent({
showTooltip: { showTooltip: {
type: Boolean, type: Boolean,
}, },
badge: { type: Object as PropType<{ src: string; type: string }> },
}, },
computed: { computed: {
iconStyleData(): Record<string, string> { iconStyleData(): Record<string, string> {
@ -92,6 +96,25 @@ export default defineComponent({
'line-height': `${this.size}px`, 'line-height': `${this.size}px`,
}; };
}, },
badgeSize(): number {
switch (this.size) {
case 40:
return 18;
case 24:
return 10;
case 18:
default:
return 8;
}
},
badgeStyleData(): Record<string, string> {
const size = this.badgeSize;
return {
padding: `${Math.floor(size / 4)}px`,
right: `-${Math.floor(size / 2)}px`,
bottom: `-${Math.floor(size / 2)}px`,
};
},
fontStyleData(): Record<string, string> { fontStyleData(): Record<string, string> {
if (!this.size) { if (!this.size) {
return {}; return {};
@ -113,7 +136,6 @@ export default defineComponent({
color: var(--node-icon-color, #444); color: var(--node-icon-color, #444);
line-height: var(--node-icon-size, 26px); line-height: var(--node-icon-size, 26px);
font-size: 1.1em; font-size: 1.1em;
overflow: hidden;
text-align: center; text-align: center;
font-weight: bold; font-weight: bold;
font-size: 20px; font-size: 20px;
@ -125,6 +147,7 @@ export default defineComponent({
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
position: relative;
svg { svg {
max-width: 100%; max-width: 100%;
@ -145,6 +168,12 @@ export default defineComponent({
max-height: 100%; max-height: 100%;
} }
.badge {
position: absolute;
background: var(--color-background-node-icon-badge, var(--color-background-base));
border-radius: 50%;
}
.circle { .circle {
border-radius: 50%; border-radius: 50%;
} }

View file

@ -809,6 +809,7 @@ export type SimplifiedNodeType = Pick<
| 'group' | 'group'
| 'icon' | 'icon'
| 'iconUrl' | 'iconUrl'
| 'badgeIconUrl'
| 'codex' | 'codex'
| 'defaults' | 'defaults'
| 'outputs' | 'outputs'

View file

@ -1,6 +1,6 @@
<template> <template>
<div :class="$style.container"> <div :class="$style.container">
<el-row> <el-row v-if="nodesWithAccess.length > 0">
<el-col :span="8" :class="$style.accessLabel"> <el-col :span="8" :class="$style.accessLabel">
<n8n-text :compact="true" :bold="true"> <n8n-text :compact="true" :bold="true">
{{ $locale.baseText('credentialEdit.credentialInfo.allowUseBy') }} {{ $locale.baseText('credentialEdit.credentialInfo.allowUseBy') }}

View file

@ -781,6 +781,7 @@ export default defineComponent({
border: 2px solid var(--color-foreground-xdark); border: 2px solid var(--color-foreground-xdark);
border-radius: var(--border-radius-large); border-radius: var(--border-radius-large);
background-color: var(--color-canvas-node-background); background-color: var(--color-canvas-node-background);
--color-background-node-icon-badge: var(--color-canvas-node-background);
&.executing { &.executing {
background-color: $node-background-executing !important; background-color: $node-background-executing !important;

View file

@ -5,7 +5,7 @@
@dragstart="onDragStart" @dragstart="onDragStart"
@dragend="onDragEnd" @dragend="onDragEnd"
:class="$style.nodeItem" :class="$style.nodeItem"
:description="subcategory !== DEFAULT_SUBCATEGORY ? description : ''" :description="description"
:title="displayName" :title="displayName"
:show-action-arrow="showActionArrow" :show-action-arrow="showActionArrow"
:is-trigger="isTrigger" :is-trigger="isTrigger"
@ -44,6 +44,7 @@ import { computed, ref } from 'vue';
import type { SimplifiedNodeType } from '@/Interface'; import type { SimplifiedNodeType } from '@/Interface';
import { import {
COMMUNITY_NODES_INSTALLATION_DOCS_URL, COMMUNITY_NODES_INSTALLATION_DOCS_URL,
CREDENTIAL_ONLY_NODE_PREFIX,
DEFAULT_SUBCATEGORY, DEFAULT_SUBCATEGORY,
DRAG_EVENT_DATA_KEY, DRAG_EVENT_DATA_KEY,
} from '@/constants'; } from '@/constants';
@ -78,6 +79,13 @@ const draggablePosition = ref({ x: -100, y: -100 });
const draggableDataTransfer = ref(null as Element | null); const draggableDataTransfer = ref(null as Element | null);
const description = computed<string>(() => { const description = computed<string>(() => {
if (
props.subcategory === DEFAULT_SUBCATEGORY &&
!props.nodeType.name.startsWith(CREDENTIAL_ONLY_NODE_PREFIX)
) {
return '';
}
return i18n.headerText({ return i18n.headerText({
key: `headers.${shortNodeType.value}.description`, key: `headers.${shortNodeType.value}.description`,
fallback: props.nodeType.description, fallback: props.nodeType.description,

View file

@ -30,7 +30,7 @@ import { useViewStacks } from './composables/useViewStacks';
import { useKeyboardNavigation } from './composables/useKeyboardNavigation'; import { useKeyboardNavigation } from './composables/useKeyboardNavigation';
import { useActionsGenerator } from './composables/useActionsGeneration'; import { useActionsGenerator } from './composables/useActionsGeneration';
import NodesListPanel from './Panel/NodesListPanel.vue'; import NodesListPanel from './Panel/NodesListPanel.vue';
import { useUIStore } from '@/stores'; import { useCredentialsStore, useUIStore } from '@/stores';
import { DRAG_EVENT_DATA_KEY } from '@/constants'; import { DRAG_EVENT_DATA_KEY } from '@/constants';
export interface Props { export interface Props {
@ -135,9 +135,12 @@ registerKeyHook('NodeCreatorCloseTab', {
}); });
watch( watch(
() => useNodeTypesStore().visibleNodeTypes, () => ({
(nodeTypes) => { httpOnlyCredentials: useCredentialsStore().httpOnlyCredentialTypes,
const { actions, mergedNodes } = generateMergedNodesAndActions(nodeTypes); nodeTypes: useNodeTypesStore().visibleNodeTypes,
}),
({ nodeTypes, httpOnlyCredentials }) => {
const { actions, mergedNodes } = generateMergedNodesAndActions(nodeTypes, httpOnlyCredentials);
setActions(actions); setActions(actions);
setMergeNodes(mergedNodes); setMergeNodes(mergedNodes);

View file

@ -235,6 +235,7 @@ function onBackButton() {
background: var(--color-background-xlight); background: var(--color-background-xlight);
height: 100%; height: 100%;
background-color: $node-creator-background-color; background-color: $node-creator-background-color;
--color-background-node-icon-badge: var(--color-background-xlight);
width: 385px; width: 385px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View file

@ -1,15 +1,18 @@
import type { ActionTypeDescription, ActionsRecord, SimplifiedNodeType } from '@/Interface';
import { CUSTOM_API_CALL_KEY, HTTP_REQUEST_NODE_TYPE } from '@/constants';
import { memoize, startCase } from 'lodash-es'; import { memoize, startCase } from 'lodash-es';
import type { import type {
ICredentialType,
INodeProperties,
INodePropertyCollection, INodePropertyCollection,
INodePropertyOptions, INodePropertyOptions,
INodeProperties,
INodeTypeDescription, INodeTypeDescription,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { CUSTOM_API_CALL_KEY } from '@/constants';
import type { ActionTypeDescription, SimplifiedNodeType, ActionsRecord } from '@/Interface';
import { i18n } from '@/plugins/i18n'; import { i18n } from '@/plugins/i18n';
import { getCredentialOnlyNodeType } from '@/utils/credentialOnlyNodes';
const PLACEHOLDER_RECOMMENDED_ACTION_KEY = 'placeholder_recommended'; const PLACEHOLDER_RECOMMENDED_ACTION_KEY = 'placeholder_recommended';
function translate(...args: Parameters<typeof i18n.baseText>) { function translate(...args: Parameters<typeof i18n.baseText>) {
@ -241,7 +244,18 @@ export function useActionsGenerator() {
} }
function getSimplifiedNodeType(node: INodeTypeDescription): SimplifiedNodeType { function getSimplifiedNodeType(node: INodeTypeDescription): SimplifiedNodeType {
const { displayName, defaults, description, name, group, icon, iconUrl, outputs, codex } = node; const {
displayName,
defaults,
description,
name,
group,
icon,
iconUrl,
badgeIconUrl,
outputs,
codex,
} = node;
return { return {
displayName, displayName,
@ -251,12 +265,16 @@ export function useActionsGenerator() {
group, group,
icon, icon,
iconUrl, iconUrl,
badgeIconUrl,
outputs, outputs,
codex, codex,
}; };
} }
function generateMergedNodesAndActions(nodeTypes: INodeTypeDescription[]) { function generateMergedNodesAndActions(
nodeTypes: INodeTypeDescription[],
httpOnlyCredentials: ICredentialType[],
) {
const visibleNodeTypes = [...nodeTypes]; const visibleNodeTypes = [...nodeTypes];
const actions: ActionsRecord<typeof mergedNodes> = {}; const actions: ActionsRecord<typeof mergedNodes> = {};
const mergedNodes: SimplifiedNodeType[] = []; const mergedNodes: SimplifiedNodeType[] = [];
@ -267,6 +285,13 @@ export function useActionsGenerator() {
const appActions = generateNodeActions(app); const appActions = generateNodeActions(app);
actions[app.name] = appActions; actions[app.name] = appActions;
if (app.name === HTTP_REQUEST_NODE_TYPE) {
const credentialOnlyNodes = httpOnlyCredentials.map((credentialType) =>
getSimplifiedNodeType(getCredentialOnlyNodeType(app, credentialType)),
);
mergedNodes.push(...credentialOnlyNodes);
}
mergedNodes.push(getSimplifiedNodeType(app)); mergedNodes.push(getSimplifiedNodeType(app));
}); });

View file

@ -128,7 +128,7 @@ import { useWorkflowsStore } from '@/stores/workflows.store';
import { useNodeTypesStore } from '@/stores/nodeTypes.store'; import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { useCredentialsStore } from '@/stores/credentials.store'; import { useCredentialsStore } from '@/stores/credentials.store';
import { useNDVStore } from '@/stores/ndv.store'; import { useNDVStore } from '@/stores/ndv.store';
import { KEEP_AUTH_IN_NDV_FOR_NODES } from '@/constants'; import { CREDENTIAL_ONLY_NODE_PREFIX, KEEP_AUTH_IN_NDV_FOR_NODES } from '@/constants';
import { import {
getAuthTypeForNodeCredential, getAuthTypeForNodeCredential,
getMainAuthField, getMainAuthField,
@ -248,7 +248,7 @@ export default defineComponent({
// When active node parameters change, check if authentication type has been changed // When active node parameters change, check if authentication type has been changed
// and set `subscribedToCredentialType` to corresponding credential type // and set `subscribedToCredentialType` to corresponding credential type
const isActive = this.node.name === this.ndvStore.activeNode?.name; const isActive = this.node.name === this.ndvStore.activeNode?.name;
const nodeType = this.nodeTypesStore.getNodeType(this.node.type, this.node.typeVersion); const nodeType = this.nodeType;
// Only do this for active node and if it's listening for auth change // Only do this for active node and if it's listening for auth change
if (isActive && nodeType && this.listeningForAuthChange) { if (isActive && nodeType && this.listeningForAuthChange) {
if (this.mainNodeAuthField && oldValue && newValue) { if (this.mainNodeAuthField && oldValue && newValue) {
@ -297,7 +297,7 @@ export default defineComponent({
if (credType) return [credType]; if (credType) return [credType];
const activeNodeType = this.nodeTypesStore.getNodeType(node.type, node.typeVersion); const activeNodeType = this.nodeType;
if (activeNodeType?.credentials) { if (activeNodeType?.credentials) {
return activeNodeType.credentials; return activeNodeType.credentials;
} }
@ -548,19 +548,24 @@ export default defineComponent({
this.subscribedToCredentialType = credentialType; this.subscribedToCredentialType = credentialType;
}, },
showMixedCredentials(credentialType: INodeCredentialDescription): boolean { showMixedCredentials(credentialType: INodeCredentialDescription): boolean {
const nodeType = this.nodeTypesStore.getNodeType(this.node.type, this.node.typeVersion); const nodeType = this.nodeType;
const isRequired = isRequiredCredential(nodeType, credentialType); const isRequired = isRequiredCredential(nodeType, credentialType);
return !KEEP_AUTH_IN_NDV_FOR_NODES.includes(this.node.type || '') && isRequired; return !KEEP_AUTH_IN_NDV_FOR_NODES.includes(this.node.type || '') && isRequired;
}, },
getCredentialsFieldLabel(credentialType: INodeCredentialDescription): string { getCredentialsFieldLabel(credentialType: INodeCredentialDescription): string {
const credentialTypeName = this.credentialTypeNames[credentialType.name]; const credentialTypeName = this.credentialTypeNames[credentialType.name];
const isCredentialOnlyNode = this.node.type.startsWith(CREDENTIAL_ONLY_NODE_PREFIX);
if (isCredentialOnlyNode) {
return this.$locale.baseText('nodeCredentials.credentialFor', {
interpolate: { credentialType: this.nodeType?.displayName ?? credentialTypeName },
});
}
if (!this.showMixedCredentials(credentialType)) { if (!this.showMixedCredentials(credentialType)) {
return this.$locale.baseText('nodeCredentials.credentialFor', { return this.$locale.baseText('nodeCredentials.credentialFor', {
interpolate: { interpolate: { credentialType: credentialTypeName },
credentialType: credentialTypeName,
},
}); });
} }
return this.$locale.baseText('nodeCredentials.credentialsLabel'); return this.$locale.baseText('nodeCredentials.credentialsLabel');

View file

@ -9,6 +9,7 @@
:circle="circle" :circle="circle"
:nodeTypeName="nodeType ? nodeType.displayName : ''" :nodeTypeName="nodeType ? nodeType.displayName : ''"
:showTooltip="showTooltip" :showTooltip="showTooltip"
:badge="badge"
@click="(e) => $emit('click')" @click="(e) => $emit('click')"
></n8n-node-icon> ></n8n-node-icon>
</template> </template>
@ -18,7 +19,7 @@ import type { IVersionNode } from '@/Interface';
import { useRootStore } from '@/stores/n8nRoot.store'; import { useRootStore } from '@/stores/n8nRoot.store';
import type { INodeTypeDescription } from 'n8n-workflow'; import type { INodeTypeDescription } from 'n8n-workflow';
import { mapStores } from 'pinia'; import { mapStores } from 'pinia';
import { defineComponent } from 'vue'; import { defineComponent, type PropType } from 'vue';
interface NodeIconSource { interface NodeIconSource {
path?: string; path?: string;
@ -29,7 +30,9 @@ interface NodeIconSource {
export default defineComponent({ export default defineComponent({
name: 'NodeIcon', name: 'NodeIcon',
props: { props: {
nodeType: {}, nodeType: {
type: Object as PropType<INodeTypeDescription | IVersionNode | null>,
},
size: { size: {
type: Number, type: Number,
required: false, required: false,
@ -54,7 +57,7 @@ export default defineComponent({
computed: { computed: {
...mapStores(useRootStore), ...mapStores(useRootStore),
type(): string { type(): string {
const nodeType = this.nodeType as INodeTypeDescription | IVersionNode | null; const nodeType = this.nodeType;
let iconType = 'unknown'; let iconType = 'unknown';
if (nodeType) { if (nodeType) {
if (nodeType.iconUrl) return 'file'; if (nodeType.iconUrl) return 'file';
@ -67,8 +70,8 @@ export default defineComponent({
return iconType; return iconType;
}, },
color(): string { color(): string {
const nodeType = this.nodeType as INodeTypeDescription | IVersionNode | null; const nodeType = this.nodeType;
if (nodeType && nodeType.defaults && nodeType.defaults.color) { if (nodeType?.defaults?.color) {
return nodeType.defaults.color.toString(); return nodeType.defaults.color.toString();
} }
if (this.colorDefault) { if (this.colorDefault) {
@ -77,7 +80,7 @@ export default defineComponent({
return ''; return '';
}, },
iconSource(): NodeIconSource { iconSource(): NodeIconSource {
const nodeType = this.nodeType as INodeTypeDescription | IVersionNode | null; const nodeType = this.nodeType;
const baseUrl = this.rootStore.getBaseUrl; const baseUrl = this.rootStore.getBaseUrl;
const iconSource = {} as NodeIconSource; const iconSource = {} as NodeIconSource;
@ -104,6 +107,14 @@ export default defineComponent({
} }
return iconSource; return iconSource;
}, },
badge(): { src: string; type: string } | undefined {
const nodeType = this.nodeType as INodeTypeDescription;
if (nodeType && 'badgeIconUrl' in nodeType && nodeType.badgeIconUrl) {
return { type: 'file', src: this.rootStore.getBaseUrl + nodeType.badgeIconUrl };
}
return undefined;
},
}, },
}); });
</script> </script>

View file

@ -40,8 +40,8 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue';
import NodeIcon from '@/components/NodeIcon.vue'; import NodeIcon from '@/components/NodeIcon.vue';
import { defineComponent } from 'vue';
export default defineComponent({ export default defineComponent({
name: 'NodeTitle', name: 'NodeTitle',

View file

@ -419,6 +419,7 @@ import type { EventBus } from 'n8n-design-system/utils';
import { createEventBus } from 'n8n-design-system/utils'; import { createEventBus } from 'n8n-design-system/utils';
import { useI18n } from '@/composables'; import { useI18n } from '@/composables';
import type { N8nInput } from 'n8n-design-system'; import type { N8nInput } from 'n8n-design-system';
import { isCredentialOnlyNodeType } from '@/utils/credentialOnlyNodes';
export default defineComponent({ export default defineComponent({
name: 'parameter-input', name: 'parameter-input',
@ -961,7 +962,7 @@ export default defineComponent({
return; return;
} }
if (this.node.type.startsWith('n8n-nodes-base')) { if (this.node.type.startsWith('n8n-nodes-base') || isCredentialOnlyNodeType(this.node.type)) {
this.$telemetry.track('User opened Expression Editor', { this.$telemetry.track('User opened Expression Editor', {
node_type: this.node.type, node_type: this.node.type,
parameter_name: this.parameter.displayName, parameter_name: this.parameter.displayName,

View file

@ -23,11 +23,7 @@
</div> </div>
<import-parameter <import-parameter
v-else-if=" v-else-if="parameter.type === 'curlImport'"
parameter.type === 'curlImport' &&
nodeTypeName === 'n8n-nodes-base.httpRequest' &&
nodeTypeVersion >= 3
"
:isReadOnly="isReadOnly" :isReadOnly="isReadOnly"
@valueChanged="valueChanged" @valueChanged="valueChanged"
/> />
@ -102,7 +98,10 @@
labelSize="small" labelSize="small"
@valueChanged="valueChanged" @valueChanged="valueChanged"
/> />
<div v-else-if="displayNodeParameter(parameter)" class="parameter-item"> <div
v-else-if="displayNodeParameter(parameter) && credentialsParameterIndex !== index"
class="parameter-item"
>
<div <div
class="delete-option clickable" class="delete-option clickable"
:title="$locale.baseText('parameterInputList.delete')" :title="$locale.baseText('parameterInputList.delete')"
@ -137,9 +136,6 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineAsyncComponent, defineComponent } from 'vue';
import type { PropType } from 'vue';
import { mapStores } from 'pinia';
import type { import type {
INodeParameters, INodeParameters,
INodeProperties, INodeProperties,
@ -147,19 +143,22 @@ import type {
NodeParameterValue, NodeParameterValue,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { deepCopy } from 'n8n-workflow'; import { deepCopy } from 'n8n-workflow';
import { mapStores } from 'pinia';
import type { PropType } from 'vue';
import { defineAsyncComponent, defineComponent } from 'vue';
import type { INodeUi, IUpdateInformation } from '@/Interface'; import type { INodeUi, IUpdateInformation } from '@/Interface';
import MultipleParameter from '@/components/MultipleParameter.vue';
import { workflowHelpers } from '@/mixins/workflowHelpers';
import ParameterInputFull from '@/components/ParameterInputFull.vue';
import ImportParameter from '@/components/ImportParameter.vue'; import ImportParameter from '@/components/ImportParameter.vue';
import MultipleParameter from '@/components/MultipleParameter.vue';
import ParameterInputFull from '@/components/ParameterInputFull.vue';
import ResourceMapper from '@/components/ResourceMapper/ResourceMapper.vue'; import ResourceMapper from '@/components/ResourceMapper/ResourceMapper.vue';
import { get, set } from 'lodash-es'; import { KEEP_AUTH_IN_NDV_FOR_NODES } from '@/constants';
import { workflowHelpers } from '@/mixins/workflowHelpers';
import { useNDVStore } from '@/stores/ndv.store'; import { useNDVStore } from '@/stores/ndv.store';
import { useNodeTypesStore } from '@/stores/nodeTypes.store'; import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { isAuthRelatedParameter, getNodeAuthFields, getMainAuthField } from '@/utils'; import { getMainAuthField, getNodeAuthFields, isAuthRelatedParameter } from '@/utils';
import { KEEP_AUTH_IN_NDV_FOR_NODES } from '@/constants'; import { get, set } from 'lodash-es';
import { nodeViewEventBus } from '@/event-bus'; import { nodeViewEventBus } from '@/event-bus';
const FixedCollectionParameter = defineAsyncComponent( const FixedCollectionParameter = defineAsyncComponent(
@ -242,7 +241,16 @@ export default defineComponent({
nodeAuthFields(): INodeProperties[] { nodeAuthFields(): INodeProperties[] {
return getNodeAuthFields(this.nodeType); return getNodeAuthFields(this.nodeType);
}, },
credentialsParameterIndex(): number {
return this.filteredParameters.findIndex((parameter) => parameter.type === 'credentials');
},
indexToShowSlotAt(): number { indexToShowSlotAt(): number {
const credentialsParameterIndex = this.credentialsParameterIndex;
if (credentialsParameterIndex !== -1) {
return credentialsParameterIndex;
}
let index = 0; let index = 0;
// For nodes that use old credentials UI, keep credentials below authentication field in NDV // For nodes that use old credentials UI, keep credentials below authentication field in NDV
// otherwise credentials will use auth filed position since the auth field is moved to credentials modal // otherwise credentials will use auth filed position since the auth field is moved to credentials modal
@ -255,7 +263,7 @@ export default defineComponent({
} }
}); });
return index < this.filteredParameters.length ? index : this.filteredParameters.length - 1; return Math.min(index, this.filteredParameters.length - 1);
}, },
mainNodeAuthField(): INodeProperties | null { mainNodeAuthField(): INodeProperties | null {
return getMainAuthField(this.nodeType || null); return getMainAuthField(this.nodeType || null);

View file

@ -157,6 +157,9 @@ export const ZENDESK_NODE_TYPE = 'n8n-nodes-base.zendesk';
export const ZENDESK_TRIGGER_NODE_TYPE = 'n8n-nodes-base.zendeskTrigger'; export const ZENDESK_TRIGGER_NODE_TYPE = 'n8n-nodes-base.zendeskTrigger';
export const DISCORD_NODE_TYPE = 'n8n-nodes-base.discord'; export const DISCORD_NODE_TYPE = 'n8n-nodes-base.discord';
export const CREDENTIAL_ONLY_NODE_PREFIX = 'n8n-creds-base';
export const CREDENTIAL_ONLY_HTTP_NODE_VERSION = 4.1;
export const EXECUTABLE_TRIGGER_NODE_TYPES = [ export const EXECUTABLE_TRIGGER_NODE_TYPES = [
START_NODE_TYPE, START_NODE_TYPE,
MANUAL_TRIGGER_NODE_TYPE, MANUAL_TRIGGER_NODE_TYPE,

View file

@ -66,6 +66,7 @@ import { useWorkflowsStore } from '@/stores/workflows.store';
import { getSourceItems } from '@/utils'; import { getSourceItems } from '@/utils';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import { useSettingsStore } from '@/stores/settings.store'; import { useSettingsStore } from '@/stores/settings.store';
import { getCredentialTypeName, isCredentialOnlyNodeType } from '@/utils/credentialOnlyNodes';
export function getParentMainInputNode(workflow: Workflow, node: INode): INode { export function getParentMainInputNode(workflow: Workflow, node: INode): INode {
const nodeType = useNodeTypesStore().getNodeType(node.type); const nodeType = useNodeTypesStore().getNodeType(node.type);
@ -683,11 +684,18 @@ export const workflowHelpers = defineComponent({
const nodeType = this.nodeTypesStore.getNodeType(node.type, node.typeVersion); const nodeType = this.nodeTypesStore.getNodeType(node.type, node.typeVersion);
if (nodeType !== null) { if (nodeType !== null) {
const isCredentialOnly = isCredentialOnlyNodeType(nodeType.name);
if (isCredentialOnly) {
nodeData.type = HTTP_REQUEST_NODE_TYPE;
nodeData.extendsCredential = getCredentialTypeName(nodeType.name);
}
// Node-Type is known so we can save the parameters correctly // Node-Type is known so we can save the parameters correctly
const nodeParameters = NodeHelpers.getNodeParameters( const nodeParameters = NodeHelpers.getNodeParameters(
nodeType.properties, nodeType.properties,
node.parameters, node.parameters,
false, isCredentialOnly,
false, false,
node, node,
); );

View file

@ -1,6 +1,6 @@
import { defineComponent } from 'vue';
import { mapStores } from 'pinia';
import type { IExecutionPushResponse, IExecutionResponse, IStartRunData } from '@/Interface'; import type { IExecutionPushResponse, IExecutionResponse, IStartRunData } from '@/Interface';
import { mapStores } from 'pinia';
import { defineComponent } from 'vue';
import type { IRunData, IRunExecutionData, ITaskData, IWorkflowBase } from 'n8n-workflow'; import type { IRunData, IRunExecutionData, ITaskData, IWorkflowBase } from 'n8n-workflow';
import { import {
@ -10,15 +10,15 @@ import {
FORM_TRIGGER_PATH_IDENTIFIER, FORM_TRIGGER_PATH_IDENTIFIER,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { useToast } from '@/composables';
import { externalHooks } from '@/mixins/externalHooks'; import { externalHooks } from '@/mixins/externalHooks';
import { workflowHelpers } from '@/mixins/workflowHelpers'; import { workflowHelpers } from '@/mixins/workflowHelpers';
import { useToast } from '@/composables';
import { useTitleChange } from '@/composables/useTitleChange'; import { useTitleChange } from '@/composables/useTitleChange';
import { useRootStore } from '@/stores/n8nRoot.store';
import { useUIStore } from '@/stores/ui.store'; import { useUIStore } from '@/stores/ui.store';
import { useWorkflowsStore } from '@/stores/workflows.store'; import { useWorkflowsStore } from '@/stores/workflows.store';
import { useRootStore } from '@/stores/n8nRoot.store'; import { FORM_TRIGGER_NODE_TYPE } from '@/constants';
import { FORM_TRIGGER_NODE_TYPE } from '../constants';
import { openPopUpWindow } from '@/utils/executionUtils'; import { openPopUpWindow } from '@/utils/executionUtils';
export const workflowRun = defineComponent({ export const workflowRun = defineComponent({

View file

@ -814,6 +814,7 @@
"ndv.pinData.error.tooLarge.description": "Workflow has reached the maximum allowed pinned data size", "ndv.pinData.error.tooLarge.description": "Workflow has reached the maximum allowed pinned data size",
"ndv.pinData.error.tooLargeWorkflow.title": "Pinned data too big", "ndv.pinData.error.tooLargeWorkflow.title": "Pinned data too big",
"ndv.pinData.error.tooLargeWorkflow.description": "Workflow has reached the maximum allowed size", "ndv.pinData.error.tooLargeWorkflow.description": "Workflow has reached the maximum allowed size",
"ndv.httpRequest.credentialOnly.docsNotice": "Use the <a target=\"_blank\" href=\"{docsUrl}\">{nodeName} docs</a> to construct your request. We'll take care of the authentication part if you add a {nodeName} credential below.",
"noTagsView.readyToOrganizeYourWorkflows": "Ready to organize your workflows?", "noTagsView.readyToOrganizeYourWorkflows": "Ready to organize your workflows?",
"noTagsView.withWorkflowTagsYouReFree": "With workflow tags, you're free to create the perfect tagging system for your flows", "noTagsView.withWorkflowTagsYouReFree": "With workflow tags, you're free to create the perfect tagging system for your flows",
"node.thisIsATriggerNode": "This is a Trigger node. <a target=\"_blank\" href=\"https://docs.n8n.io/workflows/components/nodes/\">Learn more</a>", "node.thisIsATriggerNode": "This is a Trigger node. <a target=\"_blank\" href=\"https://docs.n8n.io/workflows/components/nodes/\">Learn more</a>",

View file

@ -191,6 +191,9 @@ export const useCredentialsStore = defineStore(STORES.CREDENTIALS, {
return this.getCredentialOwnerName(credential); return this.getCredentialOwnerName(credential);
}; };
}, },
httpOnlyCredentialTypes(): ICredentialType[] {
return this.allCredentialTypes.filter((credentialType) => credentialType.httpRequestNode);
},
}, },
actions: { actions: {
setCredentialTypes(credentialTypes: ICredentialType[]): void { setCredentialTypes(credentialTypes: ICredentialType[]): void {

View file

@ -6,7 +6,12 @@ import {
getResourceLocatorResults, getResourceLocatorResults,
getResourceMapperFields, getResourceMapperFields,
} from '@/api/nodeTypes'; } from '@/api/nodeTypes';
import { DEFAULT_NODETYPE_VERSION, STORES } from '@/constants'; import {
DEFAULT_NODETYPE_VERSION,
HTTP_REQUEST_NODE_TYPE,
STORES,
CREDENTIAL_ONLY_HTTP_NODE_VERSION,
} from '@/constants';
import type { import type {
INodeTypesState, INodeTypesState,
IResourceLocatorReqParams, IResourceLocatorReqParams,
@ -15,6 +20,7 @@ import type {
import { addHeaders, addNodeTranslation } from '@/plugins/i18n'; import { addHeaders, addNodeTranslation } from '@/plugins/i18n';
import { omit } from '@/utils'; import { omit } from '@/utils';
import type { import type {
ConnectionTypes,
ILoadOptions, ILoadOptions,
INode, INode,
INodeCredentials, INodeCredentials,
@ -26,12 +32,16 @@ import type {
INodeTypeNameVersion, INodeTypeNameVersion,
ResourceMapperFields, ResourceMapperFields,
Workflow, Workflow,
ConnectionTypes,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { NodeConnectionType, NodeHelpers } from 'n8n-workflow';
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { useCredentialsStore } from './credentials.store'; import { useCredentialsStore } from './credentials.store';
import { useRootStore } from './n8nRoot.store'; import { useRootStore } from './n8nRoot.store';
import { NodeHelpers, NodeConnectionType } from 'n8n-workflow'; import {
getCredentialOnlyNodeType,
getCredentialTypeName,
isCredentialOnlyNodeType,
} from '@/utils/credentialOnlyNodes';
function getNodeVersions(nodeType: INodeTypeDescription) { function getNodeVersions(nodeType: INodeTypeDescription) {
return Array.isArray(nodeType.version) ? nodeType.version : [nodeType.version]; return Array.isArray(nodeType.version) ? nodeType.version : [nodeType.version];
@ -68,14 +78,28 @@ export const useNodeTypesStore = defineStore(STORES.NODE_TYPES, {
}, },
getNodeType() { getNodeType() {
return (nodeTypeName: string, version?: number): INodeTypeDescription | null => { return (nodeTypeName: string, version?: number): INodeTypeDescription | null => {
if (isCredentialOnlyNodeType(nodeTypeName)) {
return this.getCredentialOnlyNodeType(nodeTypeName, version);
}
const nodeVersions = this.nodeTypes[nodeTypeName]; const nodeVersions = this.nodeTypes[nodeTypeName];
if (!nodeVersions) return null; if (!nodeVersions) return null;
const versionNumbers = Object.keys(nodeVersions).map(Number); const versionNumbers = Object.keys(nodeVersions).map(Number);
const nodeType = nodeVersions[version || Math.max(...versionNumbers)]; const nodeType = nodeVersions[version ?? Math.max(...versionNumbers)];
return nodeType ?? null;
return nodeType || null; };
},
getCredentialOnlyNodeType() {
return (nodeTypeName: string, version?: number): INodeTypeDescription | null => {
const credentialName = getCredentialTypeName(nodeTypeName);
const httpNode = this.getNodeType(
HTTP_REQUEST_NODE_TYPE,
version ?? CREDENTIAL_ONLY_HTTP_NODE_VERSION,
);
const credential = useCredentialsStore().getCredentialTypeByName(credentialName);
return getCredentialOnlyNodeType(httpNode, credential) ?? null;
}; };
}, },
isConfigNode() { isConfigNode() {

View file

@ -86,6 +86,7 @@ import { useNDVStore } from '@/stores/ndv.store';
import { useNodeTypesStore } from '@/stores/nodeTypes.store'; import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { useUsersStore } from '@/stores/users.store'; import { useUsersStore } from '@/stores/users.store';
import { useSettingsStore } from '@/stores/settings.store'; import { useSettingsStore } from '@/stores/settings.store';
import { getCredentialOnlyNodeTypeName } from '@/utils/credentialOnlyNodes';
const defaults: Omit<IWorkflowDb, 'id'> & { settings: NonNullable<IWorkflowDb['settings']> } = { const defaults: Omit<IWorkflowDb, 'id'> & { settings: NonNullable<IWorkflowDb['settings']> } = {
name: '', name: '',
@ -954,6 +955,10 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, {
return; return;
} }
if (nodeData.extendsCredential) {
nodeData.type = getCredentialOnlyNodeTypeName(nodeData.extendsCredential);
}
this.workflow.nodes.push(nodeData); this.workflow.nodes.push(nodeData);
// Init node metadata // Init node metadata
if (!this.nodeMetadata[nodeData.name]) { if (!this.nodeMetadata[nodeData.name]) {

View file

@ -0,0 +1,90 @@
import { deepCopy, type ICredentialType, type INodeTypeDescription } from 'n8n-workflow';
import { CREDENTIAL_ONLY_NODE_PREFIX } from '../constants';
import { i18n } from '@/plugins/i18n';
export function isCredentialOnlyNodeType(nodeTypeName: string): boolean {
return nodeTypeName?.startsWith(CREDENTIAL_ONLY_NODE_PREFIX) ?? false;
}
export function getCredentialTypeName(nodeTypeName: string): string {
return nodeTypeName.split('.')[1];
}
export function getCredentialOnlyNodeTypeName(credentialTypeName: string): string {
return `${CREDENTIAL_ONLY_NODE_PREFIX}.${credentialTypeName}`;
}
export function getCredentialOnlyNodeType(
httpNode?: INodeTypeDescription | null,
credentialType?: ICredentialType,
): INodeTypeDescription | undefined {
const { httpRequestNode } = credentialType ?? {};
if (!httpNode || !credentialType || !httpRequestNode) return undefined;
const { docsUrl, name: displayName } = httpRequestNode;
const credentialOnlyNode = deepCopy(httpNode);
const httpIcon = httpNode.iconUrl;
credentialOnlyNode.name = getCredentialOnlyNodeTypeName(credentialType.name);
credentialOnlyNode.extendsCredential = credentialType.name;
credentialOnlyNode.displayName = displayName ?? credentialType.displayName;
credentialOnlyNode.description = 'HTTP request';
credentialOnlyNode.defaults.name = `${displayName} HTTP Request`;
credentialOnlyNode.codex = {
...credentialOnlyNode.codex,
alias: [],
categories: [],
subcategories: {},
};
credentialOnlyNode.credentials = [{ name: credentialType.name, required: true }];
if (credentialType.icon ?? credentialType.iconUrl) {
credentialOnlyNode.icon = credentialType.icon;
credentialOnlyNode.iconUrl = credentialType.iconUrl;
credentialOnlyNode.badgeIconUrl = httpIcon;
} else {
credentialOnlyNode.iconUrl = httpIcon;
}
credentialOnlyNode.properties = httpNode.properties.map((prop) => {
switch (prop.name) {
case 'authentication':
return { ...prop, type: 'hidden', default: 'predefinedCredentialType' };
case 'nodeCredentialType':
return { ...prop, type: 'hidden', default: credentialType.name };
case 'url':
const properties = { ...prop };
if ('apiBaseUrl' in httpRequestNode) {
const { apiBaseUrl } = httpRequestNode;
properties.default = apiBaseUrl;
properties.placeholder = apiBaseUrl ? `e.g. ${apiBaseUrl}` : prop.placeholder;
} else {
properties.placeholder = httpRequestNode.apiBaseUrlPlaceholder;
}
return properties;
default:
return prop;
}
});
credentialOnlyNode.properties.splice(1, 0, {
type: 'notice',
displayName: i18n.baseText('ndv.httpRequest.credentialOnly.docsNotice', {
interpolate: { nodeName: displayName, docsUrl },
}),
name: 'httpVariantWarning',
default: '',
});
credentialOnlyNode.properties.splice(4, 0, {
type: 'credentials',
displayName: '',
name: '',
default: '',
});
return credentialOnlyNode;
}

View file

@ -14,6 +14,12 @@ export class AlienVaultApi implements ICredentialType {
icon = 'file:icons/AlienVault.png'; icon = 'file:icons/AlienVault.png';
httpRequestNode = {
name: 'AlienVault',
docsUrl: 'https://otx.alienvault.com/api',
apiBaseUrl: 'https://otx.alienvault.com/api/v1/',
};
properties: INodeProperties[] = [ properties: INodeProperties[] = [
{ {
displayName: 'OTX Key', displayName: 'OTX Key',

View file

@ -16,6 +16,12 @@ export class Auth0ManagementApi implements ICredentialType {
icon = 'file:icons/Auth0.svg'; icon = 'file:icons/Auth0.svg';
httpRequestNode = {
name: 'Auth0',
docsUrl: 'https://auth0.com/docs/api/management/v2',
apiBaseUrlPlaceholder: 'https://your-tenant.auth0.com/api/v2/users/',
};
properties: INodeProperties[] = [ properties: INodeProperties[] = [
{ {
displayName: 'Session Token', displayName: 'Session Token',

View file

@ -9,6 +9,12 @@ export class CarbonBlackApi implements ICredentialType {
documentationUrl = 'carbonblack'; documentationUrl = 'carbonblack';
httpRequestNode = {
name: 'Carbon Black',
docsUrl: 'https://developer.carbonblack.com/reference',
apiBaseUrl: '',
};
properties: INodeProperties[] = [ properties: INodeProperties[] = [
{ {
displayName: 'URL', displayName: 'URL',

View file

@ -9,6 +9,12 @@ export class CiscoMerakiApi implements ICredentialType {
icon = 'file:icons/Cisco.svg'; icon = 'file:icons/Cisco.svg';
httpRequestNode = {
name: 'Cisco Meraki',
docsUrl: 'https://developer.cisco.com/meraki/api/',
apiBaseUrl: 'https://api.meraki.com/api/v1/',
};
properties: INodeProperties[] = [ properties: INodeProperties[] = [
{ {
displayName: 'API Key', displayName: 'API Key',

View file

@ -17,6 +17,12 @@ export class CiscoSecureEndpointApi implements ICredentialType {
icon = 'file:icons/Cisco.svg'; icon = 'file:icons/Cisco.svg';
httpRequestNode = {
name: 'Cisco Secure Endpoint',
docsUrl: 'https://developer.cisco.com/docs/secure-endpoint/',
apiBaseUrl: '',
};
properties: INodeProperties[] = [ properties: INodeProperties[] = [
{ {
displayName: 'Region', displayName: 'Region',

View file

@ -16,6 +16,12 @@ export class CiscoUmbrellaApi implements ICredentialType {
icon = 'file:icons/Cisco.svg'; icon = 'file:icons/Cisco.svg';
httpRequestNode = {
name: 'Cisco Umbrella',
docsUrl: 'https://developer.cisco.com/docs/cloud-security/',
apiBaseUrl: 'https://api.umbrella.com/',
};
properties: INodeProperties[] = [ properties: INodeProperties[] = [
{ {
displayName: 'Session Token', displayName: 'Session Token',

View file

@ -16,6 +16,12 @@ export class CrowdStrikeOAuth2Api implements ICredentialType {
icon = 'file:icons/CrowdStrike.svg'; icon = 'file:icons/CrowdStrike.svg';
httpRequestNode = {
name: 'CrowdStrike',
docsUrl: 'https://developer.crowdstrike.com/',
apiBaseUrl: '',
};
properties: INodeProperties[] = [ properties: INodeProperties[] = [
{ {
displayName: 'Session Token', displayName: 'Session Token',

View file

@ -9,6 +9,12 @@ export class F5BigIpApi implements ICredentialType {
icon = 'file:icons/F5.svg'; icon = 'file:icons/F5.svg';
httpRequestNode = {
name: 'F5 Big-IP',
docsUrl: 'https://clouddocs.f5.com/api/',
apiBaseUrl: '',
};
properties: INodeProperties[] = [ properties: INodeProperties[] = [
{ {
displayName: 'Username', displayName: 'Username',

View file

@ -9,6 +9,13 @@ export class FortiGateApi implements ICredentialType {
icon = 'file:icons/Fortinet.svg'; icon = 'file:icons/Fortinet.svg';
httpRequestNode = {
name: 'Fortinet FortiGate',
docsUrl:
'https://docs.fortinet.com/document/fortigate/7.4.1/administration-guide/940602/using-apis',
apiBaseUrl: '',
};
properties: INodeProperties[] = [ properties: INodeProperties[] = [
{ {
displayName: 'Access Token', displayName: 'Access Token',

View file

@ -9,6 +9,12 @@ export class HybridAnalysisApi implements ICredentialType {
icon = 'file:icons/Hybrid.png'; icon = 'file:icons/Hybrid.png';
httpRequestNode = {
name: 'Hybrid Analysis',
docsUrl: 'https://www.hybrid-analysis.com/docs/api/v2',
apiBaseUrl: 'https://www.hybrid-analysis.com/api/',
};
properties: INodeProperties[] = [ properties: INodeProperties[] = [
{ {
displayName: 'API Key', displayName: 'API Key',

View file

@ -9,6 +9,12 @@ export class ImpervaWafApi implements ICredentialType {
icon = 'file:icons/Imperva.svg'; icon = 'file:icons/Imperva.svg';
httpRequestNode = {
name: 'Imperva WAF',
docsUrl: 'https://docs.imperva.com/bundle/api-docs',
apiBaseUrl: 'https://api.imperva.com/',
};
properties: INodeProperties[] = [ properties: INodeProperties[] = [
{ {
displayName: 'API ID', displayName: 'API ID',

View file

@ -14,6 +14,12 @@ export class KibanaApi implements ICredentialType {
icon = 'file:icons/Kibana.svg'; icon = 'file:icons/Kibana.svg';
httpRequestNode = {
name: 'Kibana',
docsUrl: 'https://www.elastic.co/guide/en/kibana/current/api.html',
apiBaseUrl: '',
};
properties: INodeProperties[] = [ properties: INodeProperties[] = [
{ {
displayName: 'URL', displayName: 'URL',

View file

@ -14,6 +14,12 @@ export class MistApi implements ICredentialType {
documentationUrl = 'mist'; documentationUrl = 'mist';
httpRequestNode = {
name: 'Mist',
docsUrl: 'https://www.mist.com/documentation/mist-api-introduction/',
apiBaseUrl: '',
};
properties: INodeProperties[] = [ properties: INodeProperties[] = [
{ {
displayName: 'API Token', displayName: 'API Token',

View file

@ -14,6 +14,12 @@ export class OktaApi implements ICredentialType {
icon = 'file:icons/Okta.svg'; icon = 'file:icons/Okta.svg';
httpRequestNode = {
name: 'Okta',
docsUrl: 'https://developer.okta.com/docs/reference/',
apiBaseUrl: '',
};
properties: INodeProperties[] = [ properties: INodeProperties[] = [
{ {
displayName: 'URL', displayName: 'URL',

View file

@ -9,6 +9,12 @@ export class OpenCTIApi implements ICredentialType {
icon = 'file:icons/OpenCTI.png'; icon = 'file:icons/OpenCTI.png';
httpRequestNode = {
name: 'OpenCTI',
docsUrl: 'https://docs.opencti.io/latest/deployment/integrations/?h=api#graphql-api',
apiBaseUrl: '',
};
properties: INodeProperties[] = [ properties: INodeProperties[] = [
{ {
displayName: 'API Key', displayName: 'API Key',

View file

@ -9,6 +9,12 @@ export class QRadarApi implements ICredentialType {
documentationUrl = 'qradar'; documentationUrl = 'qradar';
httpRequestNode = {
name: 'QRadar',
docsUrl: 'https://www.ibm.com/docs/en/qradar-common',
apiBaseUrl: '',
};
properties: INodeProperties[] = [ properties: INodeProperties[] = [
{ {
displayName: 'API Key', displayName: 'API Key',

View file

@ -9,6 +9,12 @@ export class QualysApi implements ICredentialType {
documentationUrl = 'qualys'; documentationUrl = 'qualys';
httpRequestNode = {
name: 'Qualys',
docsUrl: 'https://qualysguard.qg2.apps.qualys.com/qwebhelp/fo_portal/api_doc/index.htm',
apiBaseUrl: 'https://qualysapi.qualys.com/api/',
};
properties: INodeProperties[] = [ properties: INodeProperties[] = [
{ {
displayName: 'Username', displayName: 'Username',

View file

@ -9,6 +9,12 @@ export class RecordedFutureApi implements ICredentialType {
icon = 'file:icons/RecordedFuture.svg'; icon = 'file:icons/RecordedFuture.svg';
httpRequestNode = {
name: 'Recorded Future',
docsUrl: 'https://api.recordedfuture.com',
apiBaseUrl: 'https://api.recordedfuture.com/v2/',
};
properties: INodeProperties[] = [ properties: INodeProperties[] = [
{ {
displayName: 'Access Token', displayName: 'Access Token',

View file

@ -9,6 +9,12 @@ export class SekoiaApi implements ICredentialType {
documentationUrl = 'sekoia'; documentationUrl = 'sekoia';
httpRequestNode = {
name: 'Sekoia',
docsUrl: 'https://docs.sekoia.io/cti/features/integrations/api/',
apiBaseUrl: 'https://api.sekoia.io/',
};
properties: INodeProperties[] = [ properties: INodeProperties[] = [
{ {
displayName: 'API Key', displayName: 'API Key',

View file

@ -14,6 +14,12 @@ export class ShufflerApi implements ICredentialType {
documentationUrl = 'shuffler'; documentationUrl = 'shuffler';
httpRequestNode = {
name: 'Shuffler',
docsUrl: 'https://shuffler.io/docs/API',
apiBaseUrl: 'https://shuffler.io/api/v1/',
};
properties: INodeProperties[] = [ properties: INodeProperties[] = [
{ {
displayName: 'API Key', displayName: 'API Key',

View file

@ -9,6 +9,12 @@ export class TrellixEpoApi implements ICredentialType {
icon = 'file:icons/Trellix.svg'; icon = 'file:icons/Trellix.svg';
httpRequestNode = {
name: 'Trellix (McAfee) ePolicy Orchestrator',
docsUrl: 'https://docs.trellix.com/en/bundle/epolicy-orchestrator-web-api-reference-guide',
apiBaseUrl: '',
};
properties: INodeProperties[] = [ properties: INodeProperties[] = [
{ {
displayName: 'Username', displayName: 'Username',

View file

@ -5,8 +5,16 @@ export class TwakeServerApi implements ICredentialType {
displayName = 'Twake Server API'; displayName = 'Twake Server API';
icon = 'file:icons/Twake.png';
documentationUrl = 'twake'; documentationUrl = 'twake';
httpRequestNode = {
name: 'Twake Server',
docsUrl: 'https://doc.twake.app/developers-api/home',
apiBaseUrl: '',
};
properties: INodeProperties[] = [ properties: INodeProperties[] = [
{ {
displayName: 'Host URL', displayName: 'Host URL',

View file

@ -8,12 +8,18 @@ import type {
export class VirusTotalApi implements ICredentialType { export class VirusTotalApi implements ICredentialType {
name = 'virusTotalApi'; name = 'virusTotalApi';
displayName = 'Virus Total API'; displayName = 'VirusTotal API';
documentationUrl = 'virustotal'; documentationUrl = 'virustotal';
icon = 'file:icons/VirusTotal.svg'; icon = 'file:icons/VirusTotal.svg';
httpRequestNode = {
name: 'VirusTotal',
docsUrl: 'https://developers.virustotal.com/reference/overview',
apiBaseUrl: 'https://www.virustotal.com/api/v3/',
};
properties: INodeProperties[] = [ properties: INodeProperties[] = [
{ {
displayName: 'API Token', displayName: 'API Token',

View file

@ -16,6 +16,12 @@ export class ZscalerZiaApi implements ICredentialType {
icon = 'file:icons/Zscaler.svg'; icon = 'file:icons/Zscaler.svg';
httpRequestNode = {
name: 'Zscaler ZIA',
docsUrl: 'https://help.zscaler.com/zia/getting-started-zia-api',
apiBaseUrl: '',
};
properties: INodeProperties[] = [ properties: INodeProperties[] = [
{ {
displayName: 'Cookie', displayName: 'Cookie',

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

View file

@ -14,6 +14,9 @@ export class BrevoTrigger implements INodeType {
{ {
name: 'sendInBlueApi', name: 'sendInBlueApi',
required: true, required: true,
displayOptions: {
show: {},
},
}, },
], ],
displayName: 'Brevo Trigger', displayName: 'Brevo Trigger',

View file

@ -14,5 +14,6 @@
"url": "https://docs.n8n.io/integrations/builtin/trigger-nodes/n8n-nodes-base.facebooktrigger/" "url": "https://docs.n8n.io/integrations/builtin/trigger-nodes/n8n-nodes-base.facebooktrigger/"
} }
] ]
} },
"alias": ["FB"]
} }

View file

@ -72,6 +72,12 @@ export const versionDescription: INodeTypeDescription = {
type: 'notice', type: 'notice',
default: '', default: '',
}, },
{
displayName: '',
name: 'Credentials',
type: 'credentials',
default: '',
},
{ {
displayName: 'Resource', displayName: 'Resource',
name: 'resource', name: 'resource',

View file

@ -82,8 +82,8 @@ class CredentialType implements ICredentialTypes {
return this.credentialTypes[credentialType].type; return this.credentialTypes[credentialType].type;
} }
getNodeTypesToTestWith(type: string): string[] { getSupportedNodes(type: string): string[] {
return knownCredentials[type]?.nodesToTestWith ?? []; return knownCredentials[type]?.supportedNodes ?? [];
} }
getParentTypes(typeName: string): string[] { getParentTypes(typeName: string): string[] {

View file

@ -308,6 +308,11 @@ export interface ICredentialTestRequestData {
testRequest: ICredentialTestRequest; testRequest: ICredentialTestRequest;
} }
type ICredentialHttpRequestNode = {
name: string;
docsUrl: string;
} & ({ apiBaseUrl: string } | { apiBaseUrlPlaceholder: string });
export interface ICredentialType { export interface ICredentialType {
name: string; name: string;
displayName: string; displayName: string;
@ -324,12 +329,13 @@ export interface ICredentialType {
) => Promise<IDataObject>; ) => Promise<IDataObject>;
test?: ICredentialTestRequest; test?: ICredentialTestRequest;
genericAuth?: boolean; genericAuth?: boolean;
httpRequestNode?: ICredentialHttpRequestNode;
} }
export interface ICredentialTypes { export interface ICredentialTypes {
recognizes(credentialType: string): boolean; recognizes(credentialType: string): boolean;
getByName(credentialType: string): ICredentialType; getByName(credentialType: string): ICredentialType;
getNodeTypesToTestWith(type: string): string[]; getSupportedNodes(type: string): string[];
getParentTypes(typeName: string): string[]; getParentTypes(typeName: string): string[];
} }
@ -958,6 +964,7 @@ export interface INode {
parameters: INodeParameters; parameters: INodeParameters;
credentials?: INodeCredentials; credentials?: INodeCredentials;
webhookId?: string; webhookId?: string;
extendsCredential?: string;
} }
export interface IPinData { export interface IPinData {
@ -1058,7 +1065,8 @@ export type NodePropertyTypes =
| 'credentialsSelect' | 'credentialsSelect'
| 'resourceLocator' | 'resourceLocator'
| 'curlImport' | 'curlImport'
| 'resourceMapper'; | 'resourceMapper'
| 'credentials';
export type CodeAutocompleteTypes = 'function' | 'functionItem'; export type CodeAutocompleteTypes = 'function' | 'functionItem';
@ -1398,6 +1406,7 @@ export interface INodeTypeBaseDescription {
name: string; name: string;
icon?: string; icon?: string;
iconUrl?: string; iconUrl?: string;
badgeIconUrl?: string;
group: string[]; group: string[];
description: string; description: string;
documentationUrl?: string; documentationUrl?: string;
@ -1611,6 +1620,7 @@ export interface INodeTypeDescription extends INodeTypeBaseDescription {
inactive: string; inactive: string;
}; };
}; };
extendsCredential?: string;
__loadOptionsMethods?: string[]; // only for validation during build __loadOptionsMethods?: string[]; // only for validation during build
} }
@ -1710,7 +1720,7 @@ export type LoadingDetails = {
}; };
export type CredentialLoadingDetails = LoadingDetails & { export type CredentialLoadingDetails = LoadingDetails & {
nodesToTestWith?: string[]; supportedNodes?: string[];
extends?: string[]; extends?: string[];
}; };

View file

@ -10,9 +10,12 @@
import get from 'lodash/get'; import get from 'lodash/get';
import isEqual from 'lodash/isEqual'; import isEqual from 'lodash/isEqual';
import uniqBy from 'lodash/uniqBy';
import type { import type {
FieldType,
IContextObject, IContextObject,
IHttpRequestMethods,
INode, INode,
INodeCredentialDescription, INodeCredentialDescription,
INodeIssueObjectProperty, INodeIssueObjectProperty,
@ -23,17 +26,15 @@ import type {
INodePropertyCollection, INodePropertyCollection,
INodePropertyMode, INodePropertyMode,
INodePropertyModeValidation, INodePropertyModeValidation,
INodePropertyOptions,
INodePropertyRegexValidation, INodePropertyRegexValidation,
INodeType, INodeType,
IVersionedNodeType,
IParameterDependencies, IParameterDependencies,
IRunExecutionData, IRunExecutionData,
IVersionedNodeType,
IWebhookData, IWebhookData,
IWorkflowExecuteAdditionalData, IWorkflowExecuteAdditionalData,
NodeParameterValue, NodeParameterValue,
IHttpRequestMethods,
FieldType,
INodePropertyOptions,
ResourceMapperValue, ResourceMapperValue,
ValidationResult, ValidationResult,
ConnectionTypes, ConnectionTypes,
@ -45,8 +46,8 @@ import type {
import { isResourceMapperValue, isValidResourceLocatorParameterValue } from './type-guards'; import { isResourceMapperValue, isValidResourceLocatorParameterValue } from './type-guards';
import { deepCopy } from './utils'; import { deepCopy } from './utils';
import type { Workflow } from './Workflow';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import type { Workflow } from './Workflow';
export const cronNodeOptions: INodePropertyCollection[] = [ export const cronNodeOptions: INodePropertyCollection[] = [
{ {
@ -1733,10 +1734,33 @@ export function getVersionedNodeType(
export function getVersionedNodeTypeAll(object: IVersionedNodeType | INodeType): INodeType[] { export function getVersionedNodeTypeAll(object: IVersionedNodeType | INodeType): INodeType[] {
if ('nodeVersions' in object) { if ('nodeVersions' in object) {
return Object.values(object.nodeVersions).map((element) => { return uniqBy(
element.description.name = object.description.name; Object.values(object.nodeVersions)
return element; .map((element) => {
}); element.description.name = object.description.name;
return element;
})
.reverse(),
(node) => {
const { version } = node.description;
return Array.isArray(version) ? version.join(',') : version.toString();
},
);
} }
return [object]; return [object];
} }
export function getCredentialsForNode(
object: IVersionedNodeType | INodeType,
): INodeCredentialDescription[] {
if ('nodeVersions' in object) {
return uniqBy(
Object.values(object.nodeVersions).flatMap(
(version) => version.description.credentials ?? [],
),
'name',
);
}
return object.description.credentials ?? [];
}