n8n/packages/editor-ui/src/composables/useNodeHelpers.ts
oleg 184ed8e17d
refactor: Migrate genericHelpers mixin to composable (#8220)
## Summary
- Moved out canvas loading handling to canvas store
- Tag editable routes via meta to remove router dependency from generic
helpers
- Replace all occurrences of `genericHelpers` mixin with composable and
audit usage
- Moved out `isRedirectSafe` and `getRedirectQueryParameter` out of
genericHelpers to remove dependency on router

Removing the router dependency is important, because `useRouter` and
`useRoute` compostables are only available if called from component
instance. So if composable is nested within another composable, we
wouldn't be able to use these. In this case we'd always need to inject
the router and pass it through several composables. That's why I moved
the `readonly` logic to router meta and `isRedirectSafe` and
`getRedirectQueryParameter` out as they were only used in a single
component.

---------

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>
2024-01-05 12:23:28 +01:00

723 lines
19 KiB
TypeScript

import { useHistoryStore } from '@/stores/history.store';
import { CUSTOM_API_CALL_KEY, PLACEHOLDER_FILLED_AT_EXECUTION_TIME } from '@/constants';
import { NodeHelpers, NodeConnectionType, ExpressionEvaluatorProxy } from 'n8n-workflow';
import type {
INodeProperties,
INodeCredentialDescription,
INodeTypeDescription,
INodeIssues,
ICredentialType,
INodeIssueObjectProperty,
ConnectionTypes,
INodeInputConfiguration,
Workflow,
INodeExecutionData,
ITaskDataConnections,
IRunData,
IBinaryKeyData,
IDataObject,
INode,
INodePropertyOptions,
INodeCredentialsDetails,
INodeParameters,
} from 'n8n-workflow';
import type {
ICredentialsResponse,
INodeUi,
INodeUpdatePropertiesInformation,
NodePanelType,
} from '@/Interface';
import { isString } from '@/utils/typeGuards';
import { isObject } from '@/utils/objectUtils';
import { useSettingsStore } from '@/stores/settings.store';
import { useUsersStore } from '@/stores/users.store';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { useCredentialsStore } from '@/stores/credentials.store';
import { get } from 'lodash-es';
import { useI18n } from './useI18n';
import { EnableNodeToggleCommand } from '@/models/history';
import { useTelemetry } from './useTelemetry';
import { getCredentialPermissions } from '@/permissions';
import { hasPermission } from '@/rbac/permissions';
declare namespace HttpRequestNode {
namespace V2 {
type AuthParams = {
authentication: 'none' | 'genericCredentialType' | 'predefinedCredentialType';
genericAuthType: string;
nodeCredentialType: string;
};
}
}
export function useNodeHelpers() {
const credentialsStore = useCredentialsStore();
const historyStore = useHistoryStore();
const nodeTypesStore = useNodeTypesStore();
const workflowsStore = useWorkflowsStore();
const i18n = useI18n();
function hasProxyAuth(node: INodeUi): boolean {
return Object.keys(node.parameters).includes('nodeCredentialType');
}
function isCustomApiCallSelected(nodeValues: INodeParameters): boolean {
const { parameters } = nodeValues;
if (!isObject(parameters)) return false;
const { resource, operation } = parameters;
if (!isString(resource) || !isString(operation)) return false;
return resource.includes(CUSTOM_API_CALL_KEY) || operation.includes(CUSTOM_API_CALL_KEY);
}
function getParameterValue(nodeValues: INodeParameters, parameterName: string, path: string) {
return get(nodeValues, path ? path + '.' + parameterName : parameterName);
}
// Returns if the given parameter should be displayed or not
function displayParameter(
nodeValues: INodeParameters,
parameter: INodeProperties | INodeCredentialDescription,
path: string,
node: INodeUi | null,
) {
return NodeHelpers.displayParameterPath(nodeValues, parameter, path, node);
}
function refreshNodeIssues(): void {
const nodes = workflowsStore.allNodes;
const workflow = workflowsStore.getCurrentWorkflow();
let nodeType: INodeTypeDescription | null;
let foundNodeIssues: INodeIssues | null;
nodes.forEach((node) => {
if (node.disabled === true) return;
nodeType = nodeTypesStore.getNodeType(node.type, node.typeVersion);
foundNodeIssues = getNodeIssues(nodeType, node, workflow);
if (foundNodeIssues !== null) {
node.issues = foundNodeIssues;
}
});
}
function getNodeIssues(
nodeType: INodeTypeDescription | null,
node: INodeUi,
workflow: Workflow,
ignoreIssues?: string[],
): INodeIssues | null {
const pinDataNodeNames = Object.keys(workflowsStore.pinnedWorkflowData ?? {});
let nodeIssues: INodeIssues | null = null;
ignoreIssues = ignoreIssues ?? [];
if (node.disabled === true || pinDataNodeNames.includes(node.name)) {
// Ignore issues on disabled and pindata nodes
return null;
}
if (nodeType === null) {
// Node type is not known
if (!ignoreIssues.includes('typeUnknown')) {
nodeIssues = {
typeUnknown: true,
};
}
} else {
// Node type is known
// Add potential parameter issues
if (!ignoreIssues.includes('parameters')) {
nodeIssues = NodeHelpers.getNodeParametersIssues(nodeType.properties, node);
}
if (!ignoreIssues.includes('credentials')) {
// Add potential credential issues
const nodeCredentialIssues = getNodeCredentialIssues(node, nodeType);
if (nodeIssues === null) {
nodeIssues = nodeCredentialIssues;
} else {
NodeHelpers.mergeIssues(nodeIssues, nodeCredentialIssues);
}
}
const nodeInputIssues = getNodeInputIssues(workflow, node, nodeType);
if (nodeIssues === null) {
nodeIssues = nodeInputIssues;
} else {
NodeHelpers.mergeIssues(nodeIssues, nodeInputIssues);
}
}
if (hasNodeExecutionIssues(node) && !ignoreIssues.includes('execution')) {
if (nodeIssues === null) {
nodeIssues = {};
}
nodeIssues.execution = true;
}
return nodeIssues;
}
// Set the status on all the nodes which produced an error so that it can be
// displayed in the node-view
function hasNodeExecutionIssues(node: INodeUi): boolean {
const workflowResultData = workflowsStore.getWorkflowRunData;
if (workflowResultData === null || !workflowResultData.hasOwnProperty(node.name)) {
return false;
}
for (const taskData of workflowResultData[node.name]) {
if (taskData.error !== undefined) {
return true;
}
}
return false;
}
function reportUnsetCredential(credentialType: ICredentialType) {
return {
credentials: {
[credentialType.name]: [
i18n.baseText('nodeHelpers.credentialsUnset', {
interpolate: {
credentialType: credentialType.displayName,
},
}),
],
},
};
}
function updateNodesInputIssues() {
const nodes = workflowsStore.allNodes;
const workflow = workflowsStore.getCurrentWorkflow();
for (const node of nodes) {
const nodeType = nodeTypesStore.getNodeType(node.type, node.typeVersion);
if (!nodeType) {
return;
}
const nodeInputIssues = getNodeInputIssues(workflow, node, nodeType);
workflowsStore.setNodeIssue({
node: node.name,
type: 'input',
value: nodeInputIssues?.input ? nodeInputIssues.input : null,
});
}
}
function updateNodesExecutionIssues() {
const nodes = workflowsStore.allNodes;
for (const node of nodes) {
workflowsStore.setNodeIssue({
node: node.name,
type: 'execution',
value: hasNodeExecutionIssues(node) ? true : null,
});
}
}
function updateNodeCredentialIssuesByName(name: string): void {
const node = workflowsStore.getNodeByName(name);
if (node) {
updateNodeCredentialIssues(node);
}
}
function updateNodeCredentialIssues(node: INodeUi): void {
const fullNodeIssues: INodeIssues | null = getNodeCredentialIssues(node);
let newIssues: INodeIssueObjectProperty | null = null;
if (fullNodeIssues !== null) {
newIssues = fullNodeIssues.credentials!;
}
workflowsStore.setNodeIssue({
node: node.name,
type: 'credentials',
value: newIssues,
});
}
function updateNodeParameterIssuesByName(name: string): void {
const node = workflowsStore.getNodeByName(name);
if (node) {
updateNodeParameterIssues(node);
}
}
function updateNodeParameterIssues(node: INodeUi, nodeType?: INodeTypeDescription): void {
const localNodeType = nodeType ?? nodeTypesStore.getNodeType(node.type, node.typeVersion);
if (localNodeType === null) {
// Could not find localNodeType so can not update issues
return;
}
// All data got updated everywhere so update now the issues
const fullNodeIssues: INodeIssues | null = NodeHelpers.getNodeParametersIssues(
localNodeType.properties,
node,
);
let newIssues: INodeIssueObjectProperty | null = null;
if (fullNodeIssues !== null) {
newIssues = fullNodeIssues.parameters!;
}
workflowsStore.setNodeIssue({
node: node.name,
type: 'parameters',
value: newIssues,
});
}
function getNodeInputIssues(
workflow: Workflow,
node: INodeUi,
nodeType?: INodeTypeDescription,
): INodeIssues | null {
const foundIssues: INodeIssueObjectProperty = {};
const workflowNode = workflow.getNode(node.name);
let inputs: Array<ConnectionTypes | INodeInputConfiguration> = [];
if (nodeType && workflowNode) {
inputs = NodeHelpers.getNodeInputs(workflow, workflowNode, nodeType);
}
inputs.forEach((input) => {
if (typeof input === 'string' || input.required !== true) {
return;
}
const parentNodes = workflow.getParentNodes(node.name, input.type, 1);
if (parentNodes.length === 0) {
foundIssues[input.type] = [
i18n.baseText('nodeIssues.input.missing', {
interpolate: { inputName: input.displayName || input.type },
}),
];
}
});
if (Object.keys(foundIssues).length) {
return {
input: foundIssues,
};
}
return null;
}
function getNodeCredentialIssues(
node: INodeUi,
nodeType?: INodeTypeDescription,
): INodeIssues | null {
const localNodeType = nodeType ?? nodeTypesStore.getNodeType(node.type, node.typeVersion);
if (node.disabled) {
// Node is disabled
return null;
}
if (!localNodeType?.credentials) {
// Node does not need any credentials or nodeType could not be found
return null;
}
const foundIssues: INodeIssueObjectProperty = {};
let userCredentials: ICredentialsResponse[] | null;
let credentialType: ICredentialType | undefined;
let credentialDisplayName: string;
let selectedCredentials: INodeCredentialsDetails;
const { authentication, genericAuthType, nodeCredentialType } =
node.parameters as HttpRequestNode.V2.AuthParams;
if (
authentication === 'genericCredentialType' &&
genericAuthType !== '' &&
selectedCredsAreUnusable(node, genericAuthType)
) {
const credential = credentialsStore.getCredentialTypeByName(genericAuthType);
return credential ? reportUnsetCredential(credential) : null;
}
if (
hasProxyAuth(node) &&
authentication === 'predefinedCredentialType' &&
nodeCredentialType !== '' &&
node.credentials !== undefined
) {
const stored = credentialsStore.getCredentialsByType(nodeCredentialType);
if (selectedCredsDoNotExist(node, nodeCredentialType, stored)) {
const credential = credentialsStore.getCredentialTypeByName(nodeCredentialType);
return credential ? reportUnsetCredential(credential) : null;
}
}
if (
hasProxyAuth(node) &&
authentication === 'predefinedCredentialType' &&
nodeCredentialType !== '' &&
selectedCredsAreUnusable(node, nodeCredentialType)
) {
const credential = credentialsStore.getCredentialTypeByName(nodeCredentialType);
return credential ? reportUnsetCredential(credential) : null;
}
for (const credentialTypeDescription of localNodeType.credentials) {
// Check if credentials should be displayed else ignore
if (!displayParameter(node.parameters, credentialTypeDescription, '', node)) {
continue;
}
// Get the display name of the credential type
credentialType = credentialsStore.getCredentialTypeByName(credentialTypeDescription.name);
if (!credentialType) {
credentialDisplayName = credentialTypeDescription.name;
} else {
credentialDisplayName = credentialType.displayName;
}
if (!node.credentials?.[credentialTypeDescription.name]) {
// Credentials are not set
if (credentialTypeDescription.required) {
foundIssues[credentialTypeDescription.name] = [
i18n.baseText('nodeIssues.credentials.notSet', {
interpolate: { type: localNodeType.displayName },
}),
];
}
} else {
// If they are set check if the value is valid
selectedCredentials = node.credentials[credentialTypeDescription.name];
if (typeof selectedCredentials === 'string') {
selectedCredentials = {
id: null,
name: selectedCredentials,
};
}
const usersStore = useUsersStore();
const currentUser = usersStore.currentUser;
userCredentials = credentialsStore
.getCredentialsByType(credentialTypeDescription.name)
.filter((credential: ICredentialsResponse) => {
const permissions = getCredentialPermissions(currentUser, credential);
return permissions.use;
});
if (userCredentials === null) {
userCredentials = [];
}
if (selectedCredentials.id) {
const idMatch = userCredentials.find(
(credentialData) => credentialData.id === selectedCredentials.id,
);
if (idMatch) {
continue;
}
}
const nameMatches = userCredentials.filter(
(credentialData) => credentialData.name === selectedCredentials.name,
);
if (nameMatches.length > 1) {
foundIssues[credentialTypeDescription.name] = [
i18n.baseText('nodeIssues.credentials.notIdentified', {
interpolate: { name: selectedCredentials.name, type: credentialDisplayName },
}),
i18n.baseText('nodeIssues.credentials.notIdentified.hint'),
];
continue;
}
if (nameMatches.length === 0) {
const isCredentialUsedInWorkflow =
workflowsStore.usedCredentials?.[selectedCredentials.id as string];
if (
!isCredentialUsedInWorkflow &&
!hasPermission(['rbac'], { rbac: { scope: 'credential:read' } })
) {
foundIssues[credentialTypeDescription.name] = [
i18n.baseText('nodeIssues.credentials.doNotExist', {
interpolate: { name: selectedCredentials.name, type: credentialDisplayName },
}),
i18n.baseText('nodeIssues.credentials.doNotExist.hint'),
];
}
}
}
}
// TODO: Could later check also if the node has access to the credentials
if (Object.keys(foundIssues).length === 0) {
return null;
}
return {
credentials: foundIssues,
};
}
/**
* Whether the node has no selected credentials, or none of the node's
* selected credentials are of the specified type.
*/
function selectedCredsAreUnusable(node: INodeUi, credentialType: string) {
return !node.credentials || !Object.keys(node.credentials).includes(credentialType);
}
/**
* Whether the node's selected credentials of the specified type
* can no longer be found in the database.
*/
function selectedCredsDoNotExist(
node: INodeUi,
nodeCredentialType: string,
storedCredsByType: ICredentialsResponse[] | null,
) {
if (!node.credentials || !storedCredsByType) return false;
const selectedCredsByType = node.credentials[nodeCredentialType];
if (!selectedCredsByType) return false;
return !storedCredsByType.find((c) => c.id === selectedCredsByType.id);
}
function updateNodesCredentialsIssues() {
const nodes = workflowsStore.allNodes;
let issues: INodeIssues | null;
for (const node of nodes) {
issues = getNodeCredentialIssues(node);
workflowsStore.setNodeIssue({
node: node.name,
type: 'credentials',
value: issues === null ? null : issues.credentials,
});
}
}
function getNodeInputData(
node: INodeUi | null,
runIndex = 0,
outputIndex = 0,
paneType: NodePanelType = 'output',
connectionType: ConnectionTypes = NodeConnectionType.Main,
): INodeExecutionData[] {
if (node === null) {
return [];
}
if (workflowsStore.getWorkflowExecution === null) {
return [];
}
const executionData = workflowsStore.getWorkflowExecution.data;
if (!executionData?.resultData) {
// unknown status
return [];
}
const runData = executionData.resultData.runData;
const taskData = get(runData, [node.name, runIndex]);
if (!taskData) {
return [];
}
let data: ITaskDataConnections | undefined = taskData.data;
if (paneType === 'input' && taskData.inputOverride) {
data = taskData.inputOverride!;
}
if (!data) {
return [];
}
return getInputData(data, outputIndex, connectionType);
}
function getInputData(
connectionsData: ITaskDataConnections,
outputIndex: number,
connectionType: ConnectionTypes = NodeConnectionType.Main,
): INodeExecutionData[] {
if (
!connectionsData ||
!connectionsData.hasOwnProperty(connectionType) ||
connectionsData[connectionType] === undefined ||
connectionsData[connectionType].length < outputIndex ||
connectionsData[connectionType][outputIndex] === null
) {
return [];
}
return connectionsData[connectionType][outputIndex] as INodeExecutionData[];
}
function getBinaryData(
workflowRunData: IRunData | null,
node: string | null,
runIndex: number,
outputIndex: number,
connectionType: ConnectionTypes = NodeConnectionType.Main,
): IBinaryKeyData[] {
if (node === null) {
return [];
}
const runData: IRunData | null = workflowRunData;
if (!runData?.[node]?.[runIndex]?.data) {
return [];
}
const inputData = getInputData(runData[node][runIndex].data!, outputIndex, connectionType);
const returnData: IBinaryKeyData[] = [];
for (let i = 0; i < inputData.length; i++) {
if (inputData[i].hasOwnProperty('binary') && inputData[i].binary !== undefined) {
returnData.push(inputData[i].binary!);
}
}
return returnData;
}
function disableNodes(nodes: INodeUi[], trackHistory = false) {
const telemetry = useTelemetry();
if (trackHistory) {
historyStore.startRecordingUndo();
}
for (const node of nodes) {
const oldState = node.disabled;
// Toggle disabled flag
const updateInformation = {
name: node.name,
properties: {
disabled: !oldState,
} as IDataObject,
} as INodeUpdatePropertiesInformation;
telemetry.track('User set node enabled status', {
node_type: node.type,
is_enabled: node.disabled,
workflow_id: workflowsStore.workflowId,
});
workflowsStore.updateNodeProperties(updateInformation);
workflowsStore.clearNodeExecutionData(node.name);
updateNodeParameterIssues(node);
updateNodeCredentialIssues(node);
updateNodesInputIssues();
if (trackHistory) {
historyStore.pushCommandToUndo(
new EnableNodeToggleCommand(node.name, oldState === true, node.disabled === true),
);
}
}
if (trackHistory) {
historyStore.stopRecordingUndo();
}
}
function getNodeSubtitle(
data: INode,
nodeType: INodeTypeDescription,
workflow: Workflow,
): string | undefined {
if (!data) {
return undefined;
}
if (data.notesInFlow) {
return data.notes;
}
if (nodeType?.subtitle !== undefined) {
try {
ExpressionEvaluatorProxy.setEvaluator(
useSettingsStore().settings.expressions?.evaluator ?? 'tmpl',
);
return workflow.expression.getSimpleParameterValue(
data,
nodeType.subtitle,
'internal',
{},
undefined,
PLACEHOLDER_FILLED_AT_EXECUTION_TIME,
) as string | undefined;
} catch (e) {
return undefined;
}
}
if (data.parameters.operation !== undefined) {
const operation = data.parameters.operation as string;
if (nodeType === null) {
return operation;
}
const operationData = nodeType.properties.find((property: INodeProperties) => {
return property.name === 'operation';
});
if (operationData === undefined) {
return operation;
}
if (operationData.options === undefined) {
return operation;
}
const optionData = operationData.options.find((option) => {
return (option as INodePropertyOptions).value === data.parameters.operation;
});
if (optionData === undefined) {
return operation;
}
return optionData.name;
}
return undefined;
}
return {
hasProxyAuth,
isCustomApiCallSelected,
getParameterValue,
displayParameter,
getNodeIssues,
refreshNodeIssues,
updateNodesInputIssues,
updateNodesExecutionIssues,
updateNodeCredentialIssuesByName,
updateNodeCredentialIssues,
updateNodeParameterIssuesByName,
updateNodeParameterIssues,
getBinaryData,
disableNodes,
getNodeSubtitle,
updateNodesCredentialsIssues,
getNodeInputData,
};
}