diff --git a/packages/editor-ui/src/stores/nodeTypes.store.ts b/packages/editor-ui/src/stores/nodeTypes.store.ts index 924e225b43..4efaca5bab 100644 --- a/packages/editor-ui/src/stores/nodeTypes.store.ts +++ b/packages/editor-ui/src/stores/nodeTypes.store.ts @@ -1,297 +1,331 @@ -import { - getNodeParameterOptions, - getNodesInformation, - getNodeTranslationHeaders, - getNodeTypes, - getResourceLocatorResults, - getResourceMapperFields, -} from '@/api/nodeTypes'; +import * as nodeTypesApi from '@/api/nodeTypes'; import { HTTP_REQUEST_NODE_TYPE, STORES, CREDENTIAL_ONLY_HTTP_NODE_VERSION } from '@/constants'; -import type { INodeTypesState, DynamicNodeParameters } from '@/Interface'; +import type { DynamicNodeParameters, NodeTypesByTypeNameAndVersion } from '@/Interface'; import { addHeaders, addNodeTranslation } from '@/plugins/i18n'; import { omit } from '@/utils/typesUtils'; import type { ConnectionTypes, INode, - INodeListSearchResult, INodeOutputConfiguration, - INodePropertyOptions, INodeTypeDescription, INodeTypeNameVersion, - ResourceMapperFields, Workflow, } from 'n8n-workflow'; import { NodeConnectionType, NodeHelpers } from 'n8n-workflow'; import { defineStore } from 'pinia'; import { useCredentialsStore } from './credentials.store'; import { useRootStore } from './root.store'; -import { - getCredentialOnlyNodeType, - getCredentialTypeName, - isCredentialOnlyNodeType, -} from '@/utils/credentialOnlyNodes'; +import * as utils from '@/utils/credentialOnlyNodes'; import { groupNodeTypesByNameAndType } from '@/utils/nodeTypes/nodeTypeTransforms'; +import { computed, ref } from 'vue'; export type NodeTypesStore = ReturnType; -export const useNodeTypesStore = defineStore(STORES.NODE_TYPES, { - state: (): INodeTypesState => ({ - nodeTypes: {}, - }), - getters: { - allNodeTypes(): INodeTypeDescription[] { - return Object.values(this.nodeTypes).reduce( - (allNodeTypes, nodeType) => { - const versionNumbers = Object.keys(nodeType).map(Number); - const allNodeVersions = versionNumbers.map((version) => nodeType[version]); +export const useNodeTypesStore = defineStore(STORES.NODE_TYPES, () => { + const nodeTypes = ref({}); - return [...allNodeTypes, ...allNodeVersions]; - }, - [], - ); - }, - allLatestNodeTypes(): INodeTypeDescription[] { - return Object.values(this.nodeTypes).reduce( - (allLatestNodeTypes, nodeVersions) => { - const versionNumbers = Object.keys(nodeVersions).map(Number); - const latestNodeVersion = nodeVersions[Math.max(...versionNumbers)]; + const rootStore = useRootStore(); - if (!latestNodeVersion) return allLatestNodeTypes; + // --------------------------------------------------------------------------- + // #region Computed + // --------------------------------------------------------------------------- - return [...allLatestNodeTypes, latestNodeVersion]; - }, - [], - ); - }, - getNodeType() { - return (nodeTypeName: string, version?: number): INodeTypeDescription | null => { - if (isCredentialOnlyNodeType(nodeTypeName)) { - return this.getCredentialOnlyNodeType(nodeTypeName, version); - } + const allNodeTypes = computed(() => { + return Object.values(nodeTypes.value).reduce( + (allNodeTypes, nodeType) => { + const versionNumbers = Object.keys(nodeType).map(Number); + const allNodeVersions = versionNumbers.map((version) => nodeType[version]); - const nodeVersions = this.nodeTypes[nodeTypeName]; - - if (!nodeVersions) return null; + return [...allNodeTypes, ...allNodeVersions]; + }, + [], + ); + }); + const allLatestNodeTypes = computed(() => { + return Object.values(nodeTypes.value).reduce( + (allLatestNodeTypes, nodeVersions) => { const versionNumbers = Object.keys(nodeVersions).map(Number); - const nodeType = nodeVersions[version ?? Math.max(...versionNumbers)]; - return nodeType ?? null; - }; - }, - getNodeVersions() { - return (nodeTypeName: string): number[] => { - return Object.keys(this.nodeTypes[nodeTypeName] ?? {}).map(Number); - }; - }, - 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() { - return (workflow: Workflow, node: INode, nodeTypeName: string): boolean => { - if (!workflow.nodes[node.name]) { - return false; - } - const nodeType = this.getNodeType(nodeTypeName); - if (!nodeType) { - return false; - } - const outputs = NodeHelpers.getNodeOutputs(workflow, node, nodeType); - const outputTypes = NodeHelpers.getConnectionTypes(outputs); + const latestNodeVersion = nodeVersions[Math.max(...versionNumbers)]; - return outputTypes - ? outputTypes.filter((output) => output !== NodeConnectionType.Main).length > 0 - : false; - }; - }, - isConfigurableNode() { - return (workflow: Workflow, node: INode, nodeTypeName: string): boolean => { - const nodeType = this.getNodeType(nodeTypeName); - if (nodeType === null) { - return false; - } - const inputs = NodeHelpers.getNodeInputs(workflow, node, nodeType); - const inputTypes = NodeHelpers.getConnectionTypes(inputs); + if (!latestNodeVersion) return allLatestNodeTypes; + + return [...allLatestNodeTypes, latestNodeVersion]; + }, + [], + ); + }); + + const getNodeType = computed(() => { + return (nodeTypeName: string, version?: number): INodeTypeDescription | null => { + if (utils.isCredentialOnlyNodeType(nodeTypeName)) { + return getCredentialOnlyNodeType.value(nodeTypeName, version); + } + + const nodeVersions = nodeTypes.value[nodeTypeName]; + + if (!nodeVersions) return null; + + const versionNumbers = Object.keys(nodeVersions).map(Number); + const nodeType = nodeVersions[version ?? Math.max(...versionNumbers)]; + return nodeType ?? null; + }; + }); + + const getNodeVersions = computed(() => { + return (nodeTypeName: string): number[] => { + return Object.keys(nodeTypes.value[nodeTypeName] ?? {}).map(Number); + }; + }); + + const getCredentialOnlyNodeType = computed(() => { + return (nodeTypeName: string, version?: number): INodeTypeDescription | null => { + const credentialName = utils.getCredentialTypeName(nodeTypeName); + const httpNode = getNodeType.value( + HTTP_REQUEST_NODE_TYPE, + version ?? CREDENTIAL_ONLY_HTTP_NODE_VERSION, + ); + const credential = useCredentialsStore().getCredentialTypeByName(credentialName); + return utils.getCredentialOnlyNodeType(httpNode, credential) ?? null; + }; + }); + + const isConfigNode = computed(() => { + return (workflow: Workflow, node: INode, nodeTypeName: string): boolean => { + if (!workflow.nodes[node.name]) { + return false; + } + const nodeType = getNodeType.value(nodeTypeName); + if (!nodeType) { + return false; + } + const outputs = NodeHelpers.getNodeOutputs(workflow, node, nodeType); + const outputTypes = NodeHelpers.getConnectionTypes(outputs); + + return outputTypes + ? outputTypes.filter((output) => output !== NodeConnectionType.Main).length > 0 + : false; + }; + }); + + const isTriggerNode = computed(() => { + return (nodeTypeName: string) => { + const nodeType = getNodeType.value(nodeTypeName); + return !!(nodeType && nodeType.group.includes('trigger')); + }; + }); + + const isCoreNodeType = computed(() => { + return (nodeType: INodeTypeDescription) => { + return nodeType.codex?.categories?.includes('Core Nodes'); + }; + }); + + const visibleNodeTypes = computed(() => { + return allLatestNodeTypes.value.filter((nodeType: INodeTypeDescription) => !nodeType.hidden); + }); + + const nativelyNumberSuffixedDefaults = computed(() => { + return allNodeTypes.value.reduce((acc, cur) => { + if (/\d$/.test(cur.defaults.name as string)) acc.push(cur.defaults.name as string); + return acc; + }, []); + }); + + const visibleNodeTypesByOutputConnectionTypeNames = computed(() => { + const nodesByOutputType = visibleNodeTypes.value.reduce( + (acc, node) => { + const outputTypes = node.outputs; + if (Array.isArray(outputTypes)) { + outputTypes.forEach((value: ConnectionTypes | INodeOutputConfiguration) => { + const outputType = typeof value === 'string' ? value : value.type; + if (!acc[outputType]) { + acc[outputType] = []; + } + acc[outputType].push(node.name); + }); + } else { + // If outputs is not an array, it must be a string expression + // in which case we'll try to match all possible non-main output types that are supported + const connectorTypes: ConnectionTypes[] = [ + NodeConnectionType.AiVectorStore, + NodeConnectionType.AiChain, + NodeConnectionType.AiDocument, + NodeConnectionType.AiEmbedding, + NodeConnectionType.AiLanguageModel, + NodeConnectionType.AiMemory, + NodeConnectionType.AiOutputParser, + NodeConnectionType.AiTextSplitter, + NodeConnectionType.AiTool, + ]; + connectorTypes.forEach((outputType: ConnectionTypes) => { + if (outputTypes.includes(outputType)) { + acc[outputType] = acc[outputType] || []; + acc[outputType].push(node.name); + } + }); + } - return inputTypes - ? inputTypes.filter((input) => input !== NodeConnectionType.Main).length > 0 - : false; - }; - }, - isTriggerNode() { - return (nodeTypeName: string) => { - const nodeType = this.getNodeType(nodeTypeName); - return !!(nodeType && nodeType.group.includes('trigger')); - }; - }, - isCoreNodeType() { - return (nodeType: INodeTypeDescription) => { - return nodeType.codex?.categories?.includes('Core Nodes'); - }; - }, - visibleNodeTypes(): INodeTypeDescription[] { - return this.allLatestNodeTypes.filter((nodeType: INodeTypeDescription) => !nodeType.hidden); - }, - /** - * Getter for node default names ending with a number: `'S3'`, `'Magento 2'`, etc. - */ - nativelyNumberSuffixedDefaults(): string[] { - return this.allNodeTypes.reduce((acc, cur) => { - if (/\d$/.test(cur.defaults.name as string)) acc.push(cur.defaults.name as string); return acc; - }, []); - }, - visibleNodeTypesByOutputConnectionTypeNames(): { [key: string]: string[] } { - const nodesByOutputType = this.visibleNodeTypes.reduce( - (acc, node) => { - const outputTypes = node.outputs; - if (Array.isArray(outputTypes)) { - outputTypes.forEach((value: ConnectionTypes | INodeOutputConfiguration) => { - const outputType = typeof value === 'string' ? value : value.type; - if (!acc[outputType]) { - acc[outputType] = []; - } - acc[outputType].push(node.name); - }); - } else { - // If outputs is not an array, it must be a string expression - // in which case we'll try to match all possible non-main output types that are supported - const connectorTypes: ConnectionTypes[] = [ - NodeConnectionType.AiVectorStore, - NodeConnectionType.AiChain, - NodeConnectionType.AiDocument, - NodeConnectionType.AiEmbedding, - NodeConnectionType.AiLanguageModel, - NodeConnectionType.AiMemory, - NodeConnectionType.AiOutputParser, - NodeConnectionType.AiTextSplitter, - NodeConnectionType.AiTool, - ]; - connectorTypes.forEach((outputType: ConnectionTypes) => { - if (outputTypes.includes(outputType)) { - acc[outputType] = acc[outputType] || []; - acc[outputType].push(node.name); - } - }); - } + }, + {} as { [key: string]: string[] }, + ); - return acc; - }, - {} as { [key: string]: string[] }, - ); + return nodesByOutputType; + }); - return nodesByOutputType; - }, - visibleNodeTypesByInputConnectionTypeNames(): { [key: string]: string[] } { - const nodesByOutputType = this.visibleNodeTypes.reduce( - (acc, node) => { - const inputTypes = node.inputs; - if (Array.isArray(inputTypes)) { - inputTypes.forEach((value: ConnectionTypes | INodeOutputConfiguration) => { - const outputType = typeof value === 'string' ? value : value.type; - if (!acc[outputType]) { - acc[outputType] = []; - } - acc[outputType].push(node.name); - }); - } - - return acc; - }, - {} as { [key: string]: string[] }, - ); - - return nodesByOutputType; - }, - }, - actions: { - setNodeTypes(newNodeTypes: INodeTypeDescription[] = []): void { - const nodeTypes = groupNodeTypesByNameAndType(newNodeTypes); - this.nodeTypes = { - ...this.nodeTypes, - ...nodeTypes, - }; - }, - removeNodeTypes(nodeTypesToRemove: INodeTypeDescription[]): void { - this.nodeTypes = nodeTypesToRemove.reduce( - (oldNodes, newNodeType) => omit(newNodeType.name, oldNodes), - this.nodeTypes, - ); - }, - async getNodesInformation( - nodeInfos: INodeTypeNameVersion[], - replace = true, - ): Promise { - const rootStore = useRootStore(); - const nodesInformation = await getNodesInformation(rootStore.restApiContext, nodeInfos); - - nodesInformation.forEach((nodeInformation) => { - if (nodeInformation.translation) { - const nodeType = nodeInformation.name.replace('n8n-nodes-base.', ''); - - addNodeTranslation({ [nodeType]: nodeInformation.translation }, rootStore.defaultLocale); + const visibleNodeTypesByInputConnectionTypeNames = computed(() => { + const nodesByOutputType = visibleNodeTypes.value.reduce( + (acc, node) => { + const inputTypes = node.inputs; + if (Array.isArray(inputTypes)) { + inputTypes.forEach((value: ConnectionTypes | INodeOutputConfiguration) => { + const outputType = typeof value === 'string' ? value : value.type; + if (!acc[outputType]) { + acc[outputType] = []; + } + acc[outputType].push(node.name); + }); } - }); - if (replace) this.setNodeTypes(nodesInformation); - return nodesInformation; - }, - async getFullNodesProperties(nodesToBeFetched: INodeTypeNameVersion[]): Promise { - const credentialsStore = useCredentialsStore(); - await credentialsStore.fetchCredentialTypes(true); - await this.getNodesInformation(nodesToBeFetched); - }, - async getNodeTypes(): Promise { - const rootStore = useRootStore(); - const nodeTypes = await getNodeTypes(rootStore.baseUrl); - if (nodeTypes.length) { - this.setNodeTypes(nodeTypes); - } - }, - /** - * Loads node types if they haven't been loaded yet - */ - async loadNodeTypesIfNotLoaded(): Promise { - if (Object.keys(this.nodeTypes).length === 0) { - await this.getNodeTypes(); - } - }, - async getNodeTranslationHeaders(): Promise { - const rootStore = useRootStore(); - const headers = await getNodeTranslationHeaders(rootStore.restApiContext); + return acc; + }, + {} as { [key: string]: string[] }, + ); - if (headers) { - addHeaders(headers, rootStore.defaultLocale); + return nodesByOutputType; + }); + + const isConfigurableNode = computed(() => { + return (workflow: Workflow, node: INode, nodeTypeName: string): boolean => { + const nodeType = getNodeType.value(nodeTypeName); + if (nodeType === null) { + return false; } - }, - async getNodeParameterOptions( - sendData: DynamicNodeParameters.OptionsRequest, - ): Promise { - const rootStore = useRootStore(); - return await getNodeParameterOptions(rootStore.restApiContext, sendData); - }, - async getResourceLocatorResults( - sendData: DynamicNodeParameters.ResourceLocatorResultsRequest, - ): Promise { - const rootStore = useRootStore(); - return await getResourceLocatorResults(rootStore.restApiContext, sendData); - }, - async getResourceMapperFields( - sendData: DynamicNodeParameters.ResourceMapperFieldsRequest, - ): Promise { - const rootStore = useRootStore(); - try { - return await getResourceMapperFields(rootStore.restApiContext, sendData); - } catch (error) { - return null; + const inputs = NodeHelpers.getNodeInputs(workflow, node, nodeType); + const inputTypes = NodeHelpers.getConnectionTypes(inputs); + + return inputTypes + ? inputTypes.filter((input) => input !== NodeConnectionType.Main).length > 0 + : false; + }; + }); + + // #endregion + + // --------------------------------------------------------------------------- + // #region Methods + // --------------------------------------------------------------------------- + + const setNodeTypes = (newNodeTypes: INodeTypeDescription[] = []) => { + const groupedNodeTypes = groupNodeTypesByNameAndType(newNodeTypes); + nodeTypes.value = { + ...nodeTypes.value, + ...groupedNodeTypes, + }; + }; + + const removeNodeTypes = (nodeTypesToRemove: INodeTypeDescription[]) => { + nodeTypes.value = nodeTypesToRemove.reduce( + (oldNodes, newNodeType) => omit(newNodeType.name, oldNodes), + nodeTypes.value, + ); + }; + + const getNodesInformation = async ( + nodeInfos: INodeTypeNameVersion[], + replace = true, + ): Promise => { + const nodesInformation = await nodeTypesApi.getNodesInformation( + rootStore.restApiContext, + nodeInfos, + ); + + nodesInformation.forEach((nodeInformation) => { + if (nodeInformation.translation) { + const nodeType = nodeInformation.name.replace('n8n-nodes-base.', ''); + + addNodeTranslation({ [nodeType]: nodeInformation.translation }, rootStore.defaultLocale); } - }, - }, + }); + if (replace) setNodeTypes(nodesInformation); + + return nodesInformation; + }; + + const getFullNodesProperties = async (nodesToBeFetched: INodeTypeNameVersion[]) => { + const credentialsStore = useCredentialsStore(); + await credentialsStore.fetchCredentialTypes(true); + await getNodesInformation(nodesToBeFetched); + }; + + const getNodeTypes = async () => { + const nodeTypes = await nodeTypesApi.getNodeTypes(rootStore.baseUrl); + if (nodeTypes.length) { + setNodeTypes(nodeTypes); + } + }; + + const loadNodeTypesIfNotLoaded = async () => { + if (Object.keys(nodeTypes.value).length === 0) { + await getNodeTypes(); + } + }; + + const getNodeTranslationHeaders = async () => { + const headers = await nodeTypesApi.getNodeTranslationHeaders(rootStore.restApiContext); + + if (headers) { + addHeaders(headers, rootStore.defaultLocale); + } + }; + + const getNodeParameterOptions = async (sendData: DynamicNodeParameters.OptionsRequest) => { + return await nodeTypesApi.getNodeParameterOptions(rootStore.restApiContext, sendData); + }; + + const getResourceLocatorResults = async ( + sendData: DynamicNodeParameters.ResourceLocatorResultsRequest, + ) => { + return await nodeTypesApi.getResourceLocatorResults(rootStore.restApiContext, sendData); + }; + + const getResourceMapperFields = async ( + sendData: DynamicNodeParameters.ResourceMapperFieldsRequest, + ) => { + try { + return await nodeTypesApi.getResourceMapperFields(rootStore.restApiContext, sendData); + } catch (error) { + return null; + } + }; + + // #endregion + + return { + nodeTypes, + allNodeTypes, + allLatestNodeTypes, + getNodeType, + getNodeVersions, + getCredentialOnlyNodeType, + isConfigNode, + isTriggerNode, + isCoreNodeType, + visibleNodeTypes, + nativelyNumberSuffixedDefaults, + visibleNodeTypesByOutputConnectionTypeNames, + visibleNodeTypesByInputConnectionTypeNames, + isConfigurableNode, + getResourceMapperFields, + getResourceLocatorResults, + getNodeParameterOptions, + getNodesInformation, + getFullNodesProperties, + getNodeTypes, + loadNodeTypesIfNotLoaded, + getNodeTranslationHeaders, + setNodeTypes, + removeNodeTypes, + }; });