diff --git a/cypress/e2e/13-pinning.cy.ts b/cypress/e2e/13-pinning.cy.ts index 709000b5e62b7..a9ccc7881857e 100644 --- a/cypress/e2e/13-pinning.cy.ts +++ b/cypress/e2e/13-pinning.cy.ts @@ -134,7 +134,7 @@ describe('Data pinning', () => { ndv.getters.pinDataButton().should('not.exist'); ndv.getters.editPinnedDataButton().should('be.visible'); - ndv.actions.setPinnedData([ + ndv.actions.pastePinnedData([ { test: '1'.repeat(Cypress.env('MAX_PINNED_DATA_SIZE')), }, diff --git a/cypress/e2e/14-mapping.cy.ts b/cypress/e2e/14-mapping.cy.ts index b797a10acae01..4029336c11f39 100644 --- a/cypress/e2e/14-mapping.cy.ts +++ b/cypress/e2e/14-mapping.cy.ts @@ -206,7 +206,7 @@ describe('Data mapping', () => { workflowPage.actions.addInitialNodeToCanvas(MANUAL_TRIGGER_NODE_NAME); workflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click(); workflowPage.actions.openNode(MANUAL_TRIGGER_NODE_DISPLAY_NAME); - ndv.actions.setPinnedData([ + ndv.actions.pastePinnedData([ { input: [ { diff --git a/cypress/e2e/17-workflow-tags.cy.ts b/cypress/e2e/17-workflow-tags.cy.ts index 299c96b41d94d..dea45d1a9afc8 100644 --- a/cypress/e2e/17-workflow-tags.cy.ts +++ b/cypress/e2e/17-workflow-tags.cy.ts @@ -14,7 +14,7 @@ describe('Workflow tags', () => { wf.actions.addTags(TEST_TAGS.slice(0, 2)); wf.getters.tagPills().should('have.length', 2); wf.getters.nthTagPill(1).click(); - wf.actions.addTags(TEST_TAGS[2]); + wf.actions.addTags(TEST_TAGS[1].toUpperCase()); wf.getters.tagPills().should('have.length', 3); wf.getters.isWorkflowSaved(); }); diff --git a/cypress/e2e/24-ndv-paired-item.cy.ts b/cypress/e2e/24-ndv-paired-item.cy.ts index 382be75bf3d59..1b2b4f1efeaad 100644 --- a/cypress/e2e/24-ndv-paired-item.cy.ts +++ b/cypress/e2e/24-ndv-paired-item.cy.ts @@ -324,7 +324,7 @@ describe('NDV', () => { ]; /* prettier-ignore */ workflowPage.actions.openNode('Get thread details1'); - ndv.actions.setPinnedData(PINNED_DATA); + ndv.actions.pastePinnedData(PINNED_DATA); ndv.actions.close(); workflowPage.actions.executeWorkflow(); diff --git a/cypress/e2e/5-ndv.cy.ts b/cypress/e2e/5-ndv.cy.ts index 89647eee6c900..7ce95aba055f1 100644 --- a/cypress/e2e/5-ndv.cy.ts +++ b/cypress/e2e/5-ndv.cy.ts @@ -56,6 +56,26 @@ describe('NDV', () => { cy.shouldNotHaveConsoleErrors(); }); + it('should disconect Switch outputs if rules order was changed', () => { + cy.createFixtureWorkflow('NDV-test-switch_reorder.json', `NDV test switch reorder`); + workflowPage.actions.zoomToFit(); + + workflowPage.actions.executeWorkflow(); + workflowPage.actions.openNode('Merge'); + ndv.getters.outputPanel().contains('2 items').should('exist'); + cy.contains('span', 'first').should('exist'); + ndv.getters.backToCanvas().click(); + + workflowPage.actions.openNode('Switch'); + cy.get('.cm-line').realMouseMove(100, 100); + cy.get('.fa-angle-down').click(); + ndv.getters.backToCanvas().click(); + workflowPage.actions.executeWorkflow(); + workflowPage.actions.openNode('Merge'); + ndv.getters.outputPanel().contains('1 item').should('exist'); + cy.contains('span', 'zero').should('exist'); + }); + it('should show correct validation state for resource locator params', () => { workflowPage.actions.addNodeToCanvas('Typeform', true, true); ndv.getters.container().should('be.visible'); @@ -660,16 +680,19 @@ describe('NDV', () => { }); it('Stop listening for trigger event from NDV', () => { + cy.intercept('POST', '/rest/workflows/run').as('workflowRun'); workflowPage.actions.addInitialNodeToCanvas('Local File Trigger', { keepNdvOpen: true, action: 'On Changes To A Specific File', isTrigger: true, }); ndv.getters.triggerPanelExecuteButton().should('exist'); - ndv.getters.triggerPanelExecuteButton().click(); + ndv.getters.triggerPanelExecuteButton().realClick(); ndv.getters.triggerPanelExecuteButton().should('contain', 'Stop Listening'); - ndv.getters.triggerPanelExecuteButton().click(); - ndv.getters.triggerPanelExecuteButton().should('contain', 'Test step'); - workflowPage.getters.successToast().should('exist'); + ndv.getters.triggerPanelExecuteButton().realClick(); + cy.wait('@workflowRun').then(() => { + ndv.getters.triggerPanelExecuteButton().should('contain', 'Test step'); + workflowPage.getters.successToast().should('exist'); + }); }); }); diff --git a/cypress/fixtures/NDV-test-switch_reorder.json b/cypress/fixtures/NDV-test-switch_reorder.json new file mode 100644 index 0000000000000..cf970434f3efb --- /dev/null +++ b/cypress/fixtures/NDV-test-switch_reorder.json @@ -0,0 +1,235 @@ +{ + "name": "switch reorder", + "nodes": [ + { + "parameters": {}, + "id": "b3f0815d-b733-413f-ab3f-74e48277bd3a", + "name": "When clicking \"Test workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [ + -20, + 620 + ] + }, + { + "parameters": {}, + "id": "fbc5b12a-6165-4cab-80a1-9fd6e4fbe39f", + "name": "One", + "type": "n8n-nodes-base.noOp", + "typeVersion": 1, + "position": [ + 620, + 720 + ] + }, + { + "parameters": { + "duplicateItem": true, + "duplicateCount": 1, + "assignments": { + "assignments": [ + { + "id": "ec6c1d1d-a17a-4537-8135-d474df7fded1", + "name": "entry", + "value": "first", + "type": "string" + } + ] + }, + "options": {} + }, + "id": "8c5a72a5-17ef-40e0-8477-764f24770174", + "name": "Edit Fields", + "type": "n8n-nodes-base.set", + "typeVersion": 3.3, + "position": [ + 160, + 740 + ] + }, + { + "parameters": { + "assignments": { + "assignments": [ + { + "id": "d8ec7c46-d02f-4bf5-931e-5ec2fb8bea22", + "name": "entry", + "value": "zero", + "type": "string" + } + ] + }, + "options": {} + }, + "id": "bc3fb81d-2ddf-4b28-a93d-762a48e8fd6b", + "name": "Edit Fields1", + "type": "n8n-nodes-base.set", + "typeVersion": 3.3, + "position": [ + 160, + 500 + ] + }, + { + "parameters": { + "rules": { + "values": [ + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict" + }, + "conditions": [ + { + "leftValue": "={{ $json.entry }}", + "rightValue": "first", + "operator": { + "type": "string", + "operation": "equals" + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "1" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict" + }, + "conditions": [ + { + "id": "ffa570ef-fc16-49ec-87be-56159f14a44b", + "leftValue": "={{ $json.entry }}", + "rightValue": "=second", + "operator": { + "type": "string", + "operation": "equals" + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "2" + } + ] + }, + "options": {} + }, + "id": "296ba553-c6c5-4c84-89fb-9056b24bab30", + "name": "Switch", + "type": "n8n-nodes-base.switch", + "typeVersion": 3, + "position": [ + 360, + 740 + ] + }, + { + "parameters": {}, + "id": "da787dd6-8e85-4dd5-8326-198705b4ae4b", + "name": "Merge", + "type": "n8n-nodes-base.merge", + "typeVersion": 2.1, + "position": [ + 880, + 520 + ] + } + ], + "pinData": { + "Edit Fields": [ + { + "json": { + "entry": "first" + } + }, + { + "json": { + "entry": "second" + } + } + ] + }, + "connections": { + "When clicking \"Test workflow\"": { + "main": [ + [ + { + "node": "Edit Fields", + "type": "main", + "index": 0 + }, + { + "node": "Edit Fields1", + "type": "main", + "index": 0 + } + ] + ] + }, + "Edit Fields": { + "main": [ + [ + { + "node": "Switch", + "type": "main", + "index": 0 + } + ] + ] + }, + "One": { + "main": [ + [ + { + "node": "Merge", + "type": "main", + "index": 1 + } + ] + ] + }, + "Edit Fields1": { + "main": [ + [ + { + "node": "Merge", + "type": "main", + "index": 0 + } + ] + ] + }, + "Switch": { + "main": [ + [ + { + "node": "One", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": { + "executionOrder": "v1" + }, + "versionId": "ce5db792-5e38-4d54-895b-88d85f2545d0", + "meta": { + "templateCredsSetupCompleted": true, + "instanceId": "be251a83c052a9862eeac953816fbb1464f89dfbf79d7ac490a8e336a8cc8bfd" + }, + "id": "uMpL0bN7t1NYZDJS", + "tags": [] +} diff --git a/cypress/pages/ndv.ts b/cypress/pages/ndv.ts index 8460282ade229..f9cb5b130445b 100644 --- a/cypress/pages/ndv.ts +++ b/cypress/pages/ndv.ts @@ -155,6 +155,17 @@ export class NDV extends BasePage { this.actions.savePinnedData(); }, + pastePinnedData: (data: object) => { + this.getters.editPinnedDataButton().click(); + + this.getters.pinnedDataEditor().click(); + this.getters + .pinnedDataEditor() + .type('{selectall}{backspace}', { delay: 0 }) + .paste(JSON.stringify(data)); + + this.actions.savePinnedData(); + }, clearParameterInput: (parameterName: string) => { this.getters.parameterInput(parameterName).type(`{selectall}{backspace}`); }, diff --git a/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ConversationalAgent/execute.ts b/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ConversationalAgent/execute.ts index ea148c493cbb7..67ebec8469bb6 100644 --- a/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ConversationalAgent/execute.ts +++ b/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ConversationalAgent/execute.ts @@ -1,9 +1,5 @@ -import { - type IExecuteFunctions, - type INodeExecutionData, - NodeConnectionType, - NodeOperationError, -} from 'n8n-workflow'; +import { NodeConnectionType, NodeOperationError } from 'n8n-workflow'; +import type { IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; import { initializeAgentExecutorWithOptions } from 'langchain/agents'; import type { BaseChatMemory } from '@langchain/community/memory/chat_memory'; @@ -16,13 +12,13 @@ import { getOptionalOutputParsers, getConnectedTools, } from '../../../../../utils/helpers'; +import { getTracingConfig } from '../../../../../utils/tracing'; export async function conversationalAgentExecute( this: IExecuteFunctions, nodeVersion: number, ): Promise { this.logger.verbose('Executing Conversational Agent'); - const model = await this.getInputConnectionData(NodeConnectionType.AiLanguageModel, 0); if (!isChatInstance(model)) { @@ -104,7 +100,9 @@ export async function conversationalAgentExecute( input = (await prompt.invoke({ input })).value; } - let response = await agentExecutor.call({ input, outputParsers }); + let response = await agentExecutor + .withConfig(getTracingConfig(this)) + .invoke({ input, outputParsers }); if (outputParser) { response = { output: await outputParser.parse(response.output as string) }; diff --git a/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/OpenAiFunctionsAgent/execute.ts b/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/OpenAiFunctionsAgent/execute.ts index b2cc7b68a0b4a..7f9ea2040a782 100644 --- a/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/OpenAiFunctionsAgent/execute.ts +++ b/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/OpenAiFunctionsAgent/execute.ts @@ -17,6 +17,7 @@ import { getOptionalOutputParsers, getPromptInputByType, } from '../../../../../utils/helpers'; +import { getTracingConfig } from '../../../../../utils/tracing'; export async function openAiFunctionsAgentExecute( this: IExecuteFunctions, @@ -104,7 +105,9 @@ export async function openAiFunctionsAgentExecute( input = (await prompt.invoke({ input })).value; } - let response = await agentExecutor.call({ input, outputParsers }); + let response = await agentExecutor + .withConfig(getTracingConfig(this)) + .invoke({ input, outputParsers }); if (outputParser) { response = { output: await outputParser.parse(response.output as string) }; diff --git a/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/PlanAndExecuteAgent/execute.ts b/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/PlanAndExecuteAgent/execute.ts index 3912dffadf01b..8c4a9667e077f 100644 --- a/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/PlanAndExecuteAgent/execute.ts +++ b/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/PlanAndExecuteAgent/execute.ts @@ -15,6 +15,7 @@ import { getOptionalOutputParsers, getPromptInputByType, } from '../../../../../utils/helpers'; +import { getTracingConfig } from '../../../../../utils/tracing'; export async function planAndExecuteAgentExecute( this: IExecuteFunctions, @@ -79,7 +80,9 @@ export async function planAndExecuteAgentExecute( input = (await prompt.invoke({ input })).value; } - let response = await agentExecutor.call({ input, outputParsers }); + let response = await agentExecutor + .withConfig(getTracingConfig(this)) + .invoke({ input, outputParsers }); if (outputParser) { response = { output: await outputParser.parse(response.output as string) }; diff --git a/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ReActAgent/execute.ts b/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ReActAgent/execute.ts index e8f5ea0b5d910..94359aa47f0b2 100644 --- a/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ReActAgent/execute.ts +++ b/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ReActAgent/execute.ts @@ -17,6 +17,7 @@ import { getPromptInputByType, isChatInstance, } from '../../../../../utils/helpers'; +import { getTracingConfig } from '../../../../../utils/tracing'; export async function reActAgentAgentExecute( this: IExecuteFunctions, @@ -100,7 +101,10 @@ export async function reActAgentAgentExecute( input = (await prompt.invoke({ input })).value; } - let response = await agentExecutor.call({ input, outputParsers }); + let response = await agentExecutor + .withConfig(getTracingConfig(this)) + .invoke({ input, outputParsers }); + if (outputParser) { response = { output: await outputParser.parse(response.output as string) }; } diff --git a/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/SqlAgent/execute.ts b/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/SqlAgent/execute.ts index 783a0a86a4bce..e8b989d865bed 100644 --- a/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/SqlAgent/execute.ts +++ b/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/SqlAgent/execute.ts @@ -14,6 +14,7 @@ import type { BaseChatMemory } from '@langchain/community/memory/chat_memory'; import type { DataSource } from '@n8n/typeorm'; import { getPromptInputByType, serializeChatHistory } from '../../../../../utils/helpers'; +import { getTracingConfig } from '../../../../../utils/tracing'; import { getSqliteDataSource } from './other/handlers/sqlite'; import { getPostgresDataSource } from './other/handlers/postgres'; import { SQL_PREFIX, SQL_SUFFIX } from './other/prompts'; @@ -126,7 +127,7 @@ export async function sqlAgentAgentExecute( let response: IDataObject; try { - response = await agentExecutor.call({ + response = await agentExecutor.withConfig(getTracingConfig(this)).invoke({ input, signal: this.getExecutionCancelSignal(), chatHistory, diff --git a/packages/@n8n/nodes-langchain/nodes/agents/OpenAiAssistant/OpenAiAssistant.node.ts b/packages/@n8n/nodes-langchain/nodes/agents/OpenAiAssistant/OpenAiAssistant.node.ts index 4c7243c7d5957..5dafa6d187296 100644 --- a/packages/@n8n/nodes-langchain/nodes/agents/OpenAiAssistant/OpenAiAssistant.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/agents/OpenAiAssistant/OpenAiAssistant.node.ts @@ -10,6 +10,7 @@ import type { } from 'n8n-workflow'; import type { OpenAIToolType } from 'langchain/dist/experimental/openai_assistant/schema'; import { getConnectedTools } from '../../../utils/helpers'; +import { getTracingConfig } from '../../../utils/tracing'; import { formatToOpenAIAssistantTool } from './utils'; export class OpenAiAssistant implements INodeType { @@ -373,7 +374,7 @@ export class OpenAiAssistant implements INodeType { tools, }); - const response = await agentExecutor.call({ + const response = await agentExecutor.withConfig(getTracingConfig(this)).invoke({ content: input, signal: this.getExecutionCancelSignal(), timeout: options.timeout ?? 10000, diff --git a/packages/@n8n/nodes-langchain/nodes/chains/ChainLLM/ChainLlm.node.ts b/packages/@n8n/nodes-langchain/nodes/chains/ChainLLM/ChainLlm.node.ts index 3aba05397e92a..030eef34a26bc 100644 --- a/packages/@n8n/nodes-langchain/nodes/chains/ChainLLM/ChainLlm.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/chains/ChainLLM/ChainLlm.node.ts @@ -27,6 +27,7 @@ import { getPromptInputByType, isChatInstance, } from '../../../utils/helpers'; +import { getTracingConfig } from '../../../utils/tracing'; interface MessagesTemplate { type: string; @@ -154,9 +155,9 @@ async function createSimpleLLMChain( const chain = new LLMChain({ llm, prompt, - }); + }).withConfig(getTracingConfig(context)); - const response = (await chain.call({ + const response = (await chain.invoke({ query, signal: context.getExecutionCancelSignal(), })) as string[]; @@ -203,8 +204,9 @@ async function getChain( ); const chain = prompt.pipe(llm).pipe(combinedOutputParser); - - const response = (await chain.invoke({ query })) as string | string[]; + const response = (await chain.withConfig(getTracingConfig(context)).invoke({ query })) as + | string + | string[]; return Array.isArray(response) ? response : [response]; } diff --git a/packages/@n8n/nodes-langchain/nodes/chains/ChainRetrievalQA/ChainRetrievalQa.node.ts b/packages/@n8n/nodes-langchain/nodes/chains/ChainRetrievalQA/ChainRetrievalQa.node.ts index 07017dcf37471..0652f7cf47174 100644 --- a/packages/@n8n/nodes-langchain/nodes/chains/ChainRetrievalQA/ChainRetrievalQa.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/chains/ChainRetrievalQA/ChainRetrievalQa.node.ts @@ -12,6 +12,7 @@ import type { BaseLanguageModel } from '@langchain/core/language_models/base'; import type { BaseRetriever } from '@langchain/core/retrievers'; import { getTemplateNoticeField } from '../../../utils/sharedFields'; import { getPromptInputByType } from '../../../utils/helpers'; +import { getTracingConfig } from '../../../utils/tracing'; export class ChainRetrievalQa implements INodeType { description: INodeTypeDescription = { @@ -176,7 +177,7 @@ export class ChainRetrievalQa implements INodeType { throw new NodeOperationError(this.getNode(), 'The ‘query‘ parameter is empty.'); } - const response = await chain.call({ query }); + const response = await chain.withConfig(getTracingConfig(this)).invoke({ query }); returnData.push({ json: { response } }); } return await this.prepareOutputData(returnData); diff --git a/packages/@n8n/nodes-langchain/nodes/chains/ChainSummarization/V2/ChainSummarizationV2.node.ts b/packages/@n8n/nodes-langchain/nodes/chains/ChainSummarization/V2/ChainSummarizationV2.node.ts index 6c9aa29bb4847..30cab761c1638 100644 --- a/packages/@n8n/nodes-langchain/nodes/chains/ChainSummarization/V2/ChainSummarizationV2.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/chains/ChainSummarization/V2/ChainSummarizationV2.node.ts @@ -18,6 +18,7 @@ import { N8nBinaryLoader } from '../../../../utils/N8nBinaryLoader'; import { getTemplateNoticeField } from '../../../../utils/sharedFields'; import { REFINE_PROMPT_TEMPLATE, DEFAULT_PROMPT_TEMPLATE } from '../prompt'; import { getChainPromptsArgs } from '../helpers'; +import { getTracingConfig } from '../../../../utils/tracing'; function getInputs(parameters: IDataObject) { const chunkingMode = parameters?.chunkingMode; @@ -211,10 +212,10 @@ export class ChainSummarizationV2 implements INodeType { ], }, { - displayName: 'Final Prompt to Combine', + displayName: 'Individual Summary Prompt', name: 'combineMapPrompt', type: 'string', - hint: 'The prompt to combine individual summaries', + hint: 'The prompt to summarize an individual document (or chunk)', displayOptions: { hide: { '/options.summarizationMethodAndPrompts.values.summarizationMethod': [ @@ -229,11 +230,11 @@ export class ChainSummarizationV2 implements INodeType { }, }, { - displayName: 'Individual Summary Prompt', + displayName: 'Final Prompt to Combine', name: 'prompt', type: 'string', default: DEFAULT_PROMPT_TEMPLATE, - hint: 'The prompt to summarize an individual document (or chunk)', + hint: 'The prompt to combine individual summaries', displayOptions: { hide: { '/options.summarizationMethodAndPrompts.values.summarizationMethod': [ @@ -364,7 +365,7 @@ export class ChainSummarizationV2 implements INodeType { ? await documentInput.processItem(item, itemIndex) : documentInput; - const response = await chain.call({ + const response = await chain.withConfig(getTracingConfig(this)).invoke({ input_documents: processedDocuments, }); diff --git a/packages/@n8n/nodes-langchain/nodes/retrievers/RetrieverWorkflow/RetrieverWorkflow.node.ts b/packages/@n8n/nodes-langchain/nodes/retrievers/RetrieverWorkflow/RetrieverWorkflow.node.ts index 4f1d288e8308e..5b2d852178932 100644 --- a/packages/@n8n/nodes-langchain/nodes/retrievers/RetrieverWorkflow/RetrieverWorkflow.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/retrievers/RetrieverWorkflow/RetrieverWorkflow.node.ts @@ -16,6 +16,7 @@ import { Document } from '@langchain/core/documents'; import type { SetField, SetNodeOptions } from 'n8n-nodes-base/dist/nodes/Set/v2/helpers/interfaces'; import * as manual from 'n8n-nodes-base/dist/nodes/Set/v2/manual.mode'; +import type { CallbackManagerForRetrieverRun } from '@langchain/core/callbacks/manager'; import { logWrapper } from '../../../utils/logWrapper'; function objectToString(obj: Record | IDataObject, level = 0) { @@ -287,7 +288,10 @@ export class RetrieverWorkflow implements INodeType { this.executeFunctions = executeFunctions; } - async getRelevantDocuments(query: string): Promise { + async _getRelevantDocuments( + query: string, + config?: CallbackManagerForRetrieverRun, + ): Promise { const source = this.executeFunctions.getNodeParameter('source', itemIndex) as string; const baseMetadata: IDataObject = { @@ -360,6 +364,7 @@ export class RetrieverWorkflow implements INodeType { receivedItems = (await this.executeFunctions.executeWorkflow( workflowInfo, items, + config?.getChild(), )) as INodeExecutionData[][]; } catch (error) { // Make sure a valid error gets returned that can by json-serialized else it will diff --git a/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/ToolWorkflow.node.ts b/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/ToolWorkflow.node.ts index 373fd82077aea..5130cf7d2924d 100644 --- a/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/ToolWorkflow.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/ToolWorkflow.node.ts @@ -16,6 +16,7 @@ import * as manual from 'n8n-nodes-base/dist/nodes/Set/v2/manual.mode'; import { DynamicTool } from '@langchain/core/tools'; import get from 'lodash/get'; import isObject from 'lodash/isObject'; +import type { CallbackManagerForToolRun } from '@langchain/core/callbacks/manager'; import { getConnectionHintNoticeField } from '../../../utils/sharedFields'; export class ToolWorkflow implements INodeType { @@ -320,7 +321,10 @@ export class ToolWorkflow implements INodeType { const name = this.getNodeParameter('name', itemIndex) as string; const description = this.getNodeParameter('description', itemIndex) as string; - const runFunction = async (query: string): Promise => { + const runFunction = async ( + query: string, + runManager?: CallbackManagerForToolRun, + ): Promise => { const source = this.getNodeParameter('source', itemIndex) as string; const responsePropertyName = this.getNodeParameter( 'responsePropertyName', @@ -385,7 +389,11 @@ export class ToolWorkflow implements INodeType { let receivedData: INodeExecutionData; try { - receivedData = (await this.executeWorkflow(workflowInfo, items)) as INodeExecutionData; + receivedData = (await this.executeWorkflow( + workflowInfo, + items, + runManager?.getChild(), + )) as INodeExecutionData; } catch (error) { // Make sure a valid error gets returned that can by json-serialized else it will // not show up in the frontend @@ -413,13 +421,13 @@ export class ToolWorkflow implements INodeType { name, description, - func: async (query: string): Promise => { + func: async (query: string, runManager?: CallbackManagerForToolRun): Promise => { const { index } = this.addInputData(NodeConnectionType.AiTool, [[{ json: { query } }]]); let response: string = ''; let executionError: ExecutionError | undefined; try { - response = await runFunction(query); + response = await runFunction(query, runManager); } catch (error) { // TODO: Do some more testing. Issues here should actually fail the workflow // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment diff --git a/packages/@n8n/nodes-langchain/nodes/vendors/OpenAi/actions/assistant/message.operation.ts b/packages/@n8n/nodes-langchain/nodes/vendors/OpenAi/actions/assistant/message.operation.ts index 6d7e669e3923b..f2e994d7eaf31 100644 --- a/packages/@n8n/nodes-langchain/nodes/vendors/OpenAi/actions/assistant/message.operation.ts +++ b/packages/@n8n/nodes-langchain/nodes/vendors/OpenAi/actions/assistant/message.operation.ts @@ -11,6 +11,7 @@ import { formatToOpenAIAssistantTool } from '../../helpers/utils'; import { assistantRLC } from '../descriptions'; import { getConnectedTools } from '../../../../../utils/helpers'; +import { getTracingConfig } from '../../../../../utils/tracing'; const properties: INodeProperties[] = [ assistantRLC, @@ -181,7 +182,7 @@ export async function execute(this: IExecuteFunctions, i: number): Promise; +} + +export function getTracingConfig( + context: IExecuteFunctions, + config: TracingConfig = {}, +): BaseCallbackConfig { + const parentRunManager = context.getParentCallbackManager + ? context.getParentCallbackManager() + : undefined; + + return { + runName: `[${context.getWorkflow().name}] ${context.getNode().name}`, + metadata: { + execution_id: context.getExecutionId(), + workflow: context.getWorkflow(), + node: context.getNode().name, + ...(config.additionalMetadata ?? {}), + }, + callbacks: parentRunManager, + }; +} diff --git a/packages/cli/BREAKING-CHANGES.md b/packages/cli/BREAKING-CHANGES.md index afa1ed17e144d..a00c800de3259 100644 --- a/packages/cli/BREAKING-CHANGES.md +++ b/packages/cli/BREAKING-CHANGES.md @@ -2,11 +2,21 @@ This list shows all the versions which include breaking changes and how to upgrade. +## 1.37.0 + +### What changed? + +The `--file` flag for the `execute` CLI command has been removed. + +### When is action necessary? + +If you have scripts relying on the `--file` flag for the `execute` CLI command, update them to first import the workflow and then execute it using the `--id` flag. + ## 1.32.0 ### What changed? -N8n auth cookie has `Secure` flag set by default now. +n8n auth cookie has `Secure` flag set by default now. ### When is action necessary? diff --git a/packages/cli/src/CredentialsHelper.ts b/packages/cli/src/CredentialsHelper.ts index ee8ffea484d60..323fdc760fdf3 100644 --- a/packages/cli/src/CredentialsHelper.ts +++ b/packages/cli/src/CredentialsHelper.ts @@ -268,7 +268,6 @@ export class CredentialsHelper extends ICredentialsHelper { return new Credentials( { id: credential.id, name: credential.name }, credential.type, - credential.nodesAccess, credential.data, ); } @@ -483,9 +482,9 @@ export function createCredentialsFromCredentialsEntity( credential: CredentialsEntity, encrypt = false, ): Credentials { - const { id, name, type, nodesAccess, data } = credential; + const { id, name, type, data } = credential; if (encrypt) { - return new Credentials({ id: null, name }, type, nodesAccess); + return new Credentials({ id: null, name }, type); } - return new Credentials({ id, name }, type, nodesAccess, data); + return new Credentials({ id, name }, type, data); } diff --git a/packages/cli/src/InternalHooks.ts b/packages/cli/src/InternalHooks.ts index a7baaf24b4903..6d01bd65d4a97 100644 --- a/packages/cli/src/InternalHooks.ts +++ b/packages/cli/src/InternalHooks.ts @@ -917,6 +917,55 @@ export class InternalHooks { ]); } + async onUserUpdatedCredentials(userUpdatedCredentialsData: { + user: User; + credential_name: string; + credential_type: string; + credential_id: string; + }): Promise { + void Promise.all([ + this.eventBus.sendAuditEvent({ + eventName: 'n8n.audit.user.credentials.updated', + payload: { + ...userToPayload(userUpdatedCredentialsData.user), + credentialName: userUpdatedCredentialsData.credential_name, + credentialType: userUpdatedCredentialsData.credential_type, + credentialId: userUpdatedCredentialsData.credential_id, + }, + }), + this.telemetry.track('User updated credentials', { + user_id: userUpdatedCredentialsData.user.id, + credential_type: userUpdatedCredentialsData.credential_type, + credential_id: userUpdatedCredentialsData.credential_id, + }), + ]); + } + + async onUserDeletedCredentials(userUpdatedCredentialsData: { + user: User; + credential_name: string; + credential_type: string; + credential_id: string; + }): Promise { + void Promise.all([ + this.eventBus.sendAuditEvent({ + eventName: 'n8n.audit.user.credentials.deleted', + payload: { + ...userToPayload(userUpdatedCredentialsData.user), + credentialName: userUpdatedCredentialsData.credential_name, + credentialType: userUpdatedCredentialsData.credential_type, + credentialId: userUpdatedCredentialsData.credential_id, + }, + }), + this.telemetry.track('User deleted credentials', { + user_id: userUpdatedCredentialsData.user.id, + credential_type: userUpdatedCredentialsData.credential_type, + credential_id: userUpdatedCredentialsData.credential_id, + instance_id: this.instanceSettings.instanceId, + }), + ]); + } + /** * Community nodes backend telemetry events */ diff --git a/packages/cli/src/PublicApi/v1/handlers/credentials/credentials.handler.ts b/packages/cli/src/PublicApi/v1/handlers/credentials/credentials.handler.ts index 1a4275f949dc2..165c7f9116b49 100644 --- a/packages/cli/src/PublicApi/v1/handlers/credentials/credentials.handler.ts +++ b/packages/cli/src/PublicApi/v1/handlers/credentials/credentials.handler.ts @@ -69,7 +69,7 @@ export = { return res.status(404).json({ message: 'Not Found' }); } - await removeCredential(credential); + await removeCredential(req.user, credential); return res.json(sanitizeCredentials(credential)); }, ], diff --git a/packages/cli/src/PublicApi/v1/handlers/credentials/credentials.service.ts b/packages/cli/src/PublicApi/v1/handlers/credentials/credentials.service.ts index 81a82616a93ba..38a2413716e57 100644 --- a/packages/cli/src/PublicApi/v1/handlers/credentials/credentials.service.ts +++ b/packages/cli/src/PublicApi/v1/handlers/credentials/credentials.service.ts @@ -16,6 +16,7 @@ import type { CredentialRequest } from '@/requests'; import { Container } from 'typedi'; import { CredentialsRepository } from '@db/repositories/credentials.repository'; import { SharedCredentialsRepository } from '@db/repositories/sharedCredentials.repository'; +import { InternalHooks } from '@/InternalHooks'; export async function getCredentials(credentialId: string): Promise { return await Container.get(CredentialsRepository).findOneBy({ id: credentialId }); @@ -41,20 +42,6 @@ export async function createCredential( Object.assign(newCredential, properties); - if (!newCredential.nodesAccess || newCredential.nodesAccess.length === 0) { - newCredential.nodesAccess = [ - { - nodeType: `n8n-nodes-base.${properties.type?.toLowerCase() ?? 'unknown'}`, - date: new Date(), - }, - ]; - } else { - // Add the added date for node access permissions - newCredential.nodesAccess.forEach((nodeAccess) => { - nodeAccess.date = new Date(); - }); - } - return newCredential; } @@ -64,6 +51,13 @@ export async function saveCredential( encryptedData: ICredentialsDb, ): Promise { await Container.get(ExternalHooks).run('credentials.create', [encryptedData]); + void Container.get(InternalHooks).onUserCreatedCredentials({ + user, + credential_name: credential.name, + credential_type: credential.type, + credential_id: credential.id, + public_api: true, + }); return await Db.transaction(async (transactionManager) => { const savedCredential = await transactionManager.save(credential); @@ -84,18 +78,23 @@ export async function saveCredential( }); } -export async function removeCredential(credentials: CredentialsEntity): Promise { +export async function removeCredential( + user: User, + credentials: CredentialsEntity, +): Promise { await Container.get(ExternalHooks).run('credentials.delete', [credentials.id]); + void Container.get(InternalHooks).onUserDeletedCredentials({ + user, + credential_name: credentials.name, + credential_type: credentials.type, + credential_id: credentials.id, + }); return await Container.get(CredentialsRepository).remove(credentials); } export async function encryptCredential(credential: CredentialsEntity): Promise { // Encrypt the data - const coreCredential = new Credentials( - { id: null, name: credential.name }, - credential.type, - credential.nodesAccess, - ); + const coreCredential = new Credentials({ id: null, name: credential.name }, credential.type); // @ts-ignore coreCredential.setData(credential.data); @@ -115,7 +114,7 @@ export function sanitizeCredentials( const credentialsList = argIsArray ? credentials : [credentials]; const sanitizedCredentials = credentialsList.map((credential) => { - const { data, nodesAccess, shared, ...rest } = credential; + const { data, shared, ...rest } = credential; return rest; }); diff --git a/packages/cli/src/PublicApi/v1/handlers/workflows/spec/schemas/workflowSettings.yml b/packages/cli/src/PublicApi/v1/handlers/workflows/spec/schemas/workflowSettings.yml index 49f8e72066ff4..c969b2eec8204 100644 --- a/packages/cli/src/PublicApi/v1/handlers/workflows/spec/schemas/workflowSettings.yml +++ b/packages/cli/src/PublicApi/v1/handlers/workflows/spec/schemas/workflowSettings.yml @@ -22,3 +22,6 @@ properties: timezone: type: string example: America/New_York + executionOrder: + type: string + example: v1 diff --git a/packages/cli/src/WorkflowExecuteAdditionalData.ts b/packages/cli/src/WorkflowExecuteAdditionalData.ts index 4ec7d31e3172e..17799baedf480 100644 --- a/packages/cli/src/WorkflowExecuteAdditionalData.ts +++ b/packages/cli/src/WorkflowExecuteAdditionalData.ts @@ -24,6 +24,7 @@ import type { ExecutionStatus, ExecutionError, EventNamesAiNodesType, + CallbackManager, } from 'n8n-workflow'; import { ApplicationError, @@ -754,6 +755,7 @@ async function executeWorkflow( loadedWorkflowData?: IWorkflowBase; loadedRunData?: IWorkflowExecutionDataProcess; parentWorkflowSettings?: IWorkflowSettings; + parentCallbackManager?: CallbackManager; }, ): Promise | IWorkflowExecuteProcess> { const internalHooks = Container.get(InternalHooks); @@ -815,6 +817,7 @@ async function executeWorkflow( workflowData, ); additionalDataIntegrated.executionId = executionId; + additionalDataIntegrated.parentCallbackManager = options.parentCallbackManager; // Make sure we pass on the original executeWorkflow function we received // This one already contains changes to talk to parent process diff --git a/packages/cli/src/commands/execute.ts b/packages/cli/src/commands/execute.ts index d3f0eb469d869..a375d19c31034 100644 --- a/packages/cli/src/commands/execute.ts +++ b/packages/cli/src/commands/execute.ts @@ -1,7 +1,5 @@ import { Container } from 'typedi'; import { Flags } from '@oclif/core'; -import { promises as fs } from 'fs'; -import { PLACEHOLDER_EMPTY_WORKFLOW_ID } from 'n8n-core'; import type { IWorkflowBase } from 'n8n-workflow'; import { ApplicationError, ExecutionBaseError } from 'n8n-workflow'; @@ -17,13 +15,10 @@ import { OwnershipService } from '@/services/ownership.service'; export class Execute extends BaseCommand { static description = '\nExecutes a given workflow'; - static examples = ['$ n8n execute --id=5', '$ n8n execute --file=workflow.json']; + static examples = ['$ n8n execute --id=5']; static flags = { help: Flags.help({ char: 'h' }), - file: Flags.string({ - description: 'path to a workflow file to execute', - }), id: Flags.string({ description: 'id of the workflow to execute', }), @@ -41,42 +36,20 @@ export class Execute extends BaseCommand { async run() { const { flags } = await this.parse(Execute); - if (!flags.id && !flags.file) { - this.logger.info('Either option "--id" or "--file" have to be set!'); + if (!flags.id) { + this.logger.info('"--id" has to be set!'); return; } - if (flags.id && flags.file) { - this.logger.info('Either "id" or "file" can be set never both!'); - return; + if (flags.file) { + throw new ApplicationError( + 'The --file flag is no longer supported. Please first import the workflow and then execute it using the --id flag.', + { level: 'warning' }, + ); } let workflowId: string | undefined; let workflowData: IWorkflowBase | null = null; - if (flags.file) { - // Path to workflow is given - try { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - workflowData = JSON.parse(await fs.readFile(flags.file, 'utf8')); - } catch (error) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - if (error.code === 'ENOENT') { - this.logger.info(`The file "${flags.file}" could not be found.`); - return; - } - - throw error; - } - - // Do a basic check if the data in the file looks right - // TODO: Later check with the help of TypeScript data if it is valid or not - if (workflowData?.nodes === undefined || workflowData.connections === undefined) { - this.logger.info(`The file "${flags.file}" does not contain valid workflow data.`); - return; - } - - workflowId = workflowData.id ?? PLACEHOLDER_EMPTY_WORKFLOW_ID; - } if (flags.id) { // Id of workflow is given diff --git a/packages/cli/src/commands/export/credentials.ts b/packages/cli/src/commands/export/credentials.ts index b0914cc4f561e..e02c5e3a44d99 100644 --- a/packages/cli/src/commands/export/credentials.ts +++ b/packages/cli/src/commands/export/credentials.ts @@ -111,9 +111,9 @@ export class ExportCredentialsCommand extends BaseCommand { if (flags.decrypted) { for (let i = 0; i < credentials.length; i++) { - const { name, type, nodesAccess, data } = credentials[i]; + const { name, type, data } = credentials[i]; const id = credentials[i].id; - const credential = new Credentials({ id, name }, type, nodesAccess, data); + const credential = new Credentials({ id, name }, type, data); const plainData = credential.getData(); (credentials[i] as ICredentialsDecryptedDb).data = plainData; } diff --git a/packages/cli/src/commands/import/credentials.ts b/packages/cli/src/commands/import/credentials.ts index 287452d7b6119..2ff971b2d2826 100644 --- a/packages/cli/src/commands/import/credentials.ts +++ b/packages/cli/src/commands/import/credentials.ts @@ -141,9 +141,6 @@ export class ImportCredentialsCommand extends BaseCommand { } private async storeCredential(credential: Partial, user: User) { - if (!credential.nodesAccess) { - credential.nodesAccess = []; - } const result = await this.transactionManager.upsert(CredentialsEntity, credential, ['id']); await this.transactionManager.upsert( SharedCredentials, diff --git a/packages/cli/src/commands/worker.ts b/packages/cli/src/commands/worker.ts index faee68cc58993..965a6ac2895c5 100644 --- a/packages/cli/src/commands/worker.ts +++ b/packages/cli/src/commands/worker.ts @@ -29,6 +29,7 @@ import { OrchestrationWorkerService } from '@/services/orchestration/worker/orch import type { WorkerJobStatusSummary } from '@/services/orchestration/worker/types'; import { ServiceUnavailableError } from '@/errors/response-errors/service-unavailable.error'; import { BaseCommand } from './BaseCommand'; +import { MaxStalledCountError } from '@/errors/max-stalled-count.error'; export class Worker extends BaseCommand { static description = '\nStarts a n8n worker'; @@ -366,6 +367,11 @@ export class Worker extends BaseCommand { process.exit(2); } else { this.logger.error('Error from queue: ', error); + + if (error.message.includes('job stalled more than maxStalledCount')) { + throw new MaxStalledCountError(error); + } + throw error; } }); diff --git a/packages/cli/src/controllers/oauth/abstractOAuth.controller.ts b/packages/cli/src/controllers/oauth/abstractOAuth.controller.ts index 518238db716e3..6f21069150ee5 100644 --- a/packages/cli/src/controllers/oauth/abstractOAuth.controller.ts +++ b/packages/cli/src/controllers/oauth/abstractOAuth.controller.ts @@ -95,7 +95,7 @@ export abstract class AbstractOAuthController { credential: ICredentialsDb, decryptedData: ICredentialDataDecryptedObject, ) { - const credentials = new Credentials(credential, credential.type, credential.nodesAccess); + const credentials = new Credentials(credential, credential.type); credentials.setData(decryptedData); await this.credentialsRepository.update(credential.id, { ...credentials.getDataToSave(), diff --git a/packages/cli/src/credentials/credentials.controller.ts b/packages/cli/src/credentials/credentials.controller.ts index f7fae043c86d8..aba4e15437409 100644 --- a/packages/cli/src/credentials/credentials.controller.ts +++ b/packages/cli/src/credentials/credentials.controller.ts @@ -252,6 +252,13 @@ export class CredentialsController { this.logger.verbose('Credential updated', { credentialId }); + void this.internalHooks.onUserUpdatedCredentials({ + user: req.user, + credential_name: credential.name, + credential_type: credential.type, + credential_id: credential.id, + }); + return { ...rest }; } @@ -291,6 +298,13 @@ export class CredentialsController { await this.credentialsService.delete(credential); + void this.internalHooks.onUserDeletedCredentials({ + user: req.user, + credential_name: credential.name, + credential_type: credential.type, + credential_id: credential.id, + }); + return true; } diff --git a/packages/cli/src/credentials/credentials.service.ts b/packages/cli/src/credentials/credentials.service.ts index 9bfb505ea3ab8..ac0598aa3f91e 100644 --- a/packages/cli/src/credentials/credentials.service.ts +++ b/packages/cli/src/credentials/credentials.service.ts @@ -109,11 +109,6 @@ export class CredentialsService { await validateEntity(newCredentials); - // Add the date for newly added node access permissions - for (const nodeAccess of newCredentials.nodesAccess) { - nodeAccess.date = new Date(); - } - return newCredentials; } @@ -132,13 +127,6 @@ export class CredentialsService { await validateEntity(updateData); - // Add the date for newly added node access permissions - for (const nodeAccess of updateData.nodesAccess) { - if (!nodeAccess.date) { - nodeAccess.date = new Date(); - } - } - // Do not overwrite the oauth data else data like the access or refresh token would get lost // everytime anybody changes anything on the credentials even if it is just the name. if (decryptedData.oauthTokenData) { @@ -149,11 +137,7 @@ export class CredentialsService { } createEncryptedData(credentialId: string | null, data: CredentialsEntity): ICredentialsDb { - const credentials = new Credentials( - { id: credentialId, name: data.name }, - data.type, - data.nodesAccess, - ); + const credentials = new Credentials({ id: credentialId, name: data.name }, data.type); credentials.setData(data.data as unknown as ICredentialDataDecryptedObject); diff --git a/packages/cli/src/databases/entities/CredentialsEntity.ts b/packages/cli/src/databases/entities/CredentialsEntity.ts index cc365c2e7009e..2c6206590e17f 100644 --- a/packages/cli/src/databases/entities/CredentialsEntity.ts +++ b/packages/cli/src/databases/entities/CredentialsEntity.ts @@ -1,8 +1,7 @@ -import type { ICredentialNodeAccess } from 'n8n-workflow'; import { Column, Entity, Index, OneToMany } from '@n8n/typeorm'; -import { IsArray, IsObject, IsString, Length } from 'class-validator'; +import { IsObject, IsString, Length } from 'class-validator'; import type { SharedCredentials } from './SharedCredentials'; -import { WithTimestampsAndStringId, jsonColumnType } from './AbstractEntity'; +import { WithTimestampsAndStringId } from './AbstractEntity'; import type { ICredentialsDb } from '@/Interfaces'; @Entity() @@ -27,8 +26,4 @@ export class CredentialsEntity extends WithTimestampsAndStringId implements ICre @OneToMany('SharedCredentials', 'credentials') shared: SharedCredentials[]; - - @Column(jsonColumnType) - @IsArray() - nodesAccess: ICredentialNodeAccess[]; } diff --git a/packages/cli/src/databases/migrations/common/1711390882123-MoveSshKeysToDatabase.ts b/packages/cli/src/databases/migrations/common/1711390882123-MoveSshKeysToDatabase.ts index b02450b3cc4db..d875f4c65a403 100644 --- a/packages/cli/src/databases/migrations/common/1711390882123-MoveSshKeysToDatabase.ts +++ b/packages/cli/src/databases/migrations/common/1711390882123-MoveSshKeysToDatabase.ts @@ -46,6 +46,11 @@ export class MoveSshKeysToDatabase1711390882123 implements ReversibleMigration { return; } + if (!privateKey) { + logger.error(`[${migrationName}] No private key found, skipping`); + return; + } + const value = JSON.stringify({ encryptedPrivateKey: this.cipher.encrypt(privateKey), publicKey, diff --git a/packages/cli/src/databases/migrations/common/1712044305787-RemoveNodesAccess.ts b/packages/cli/src/databases/migrations/common/1712044305787-RemoveNodesAccess.ts new file mode 100644 index 0000000000000..8460af61c4c97 --- /dev/null +++ b/packages/cli/src/databases/migrations/common/1712044305787-RemoveNodesAccess.ts @@ -0,0 +1,7 @@ +import type { IrreversibleMigration, MigrationContext } from '@db/types'; + +export class RemoveNodesAccess1712044305787 implements IrreversibleMigration { + async up({ schemaBuilder: { dropColumns } }: MigrationContext) { + await dropColumns('credentials_entity', ['nodesAccess']); + } +} diff --git a/packages/cli/src/databases/migrations/mysqldb/index.ts b/packages/cli/src/databases/migrations/mysqldb/index.ts index 89a6a2b0ee348..7952a923ab0a9 100644 --- a/packages/cli/src/databases/migrations/mysqldb/index.ts +++ b/packages/cli/src/databases/migrations/mysqldb/index.ts @@ -54,6 +54,7 @@ import { AddGlobalAdminRole1700571993961 } from '../common/1700571993961-AddGlob import { DropRoleMapping1705429061930 } from '../common/1705429061930-DropRoleMapping'; import { RemoveFailedExecutionStatus1711018413374 } from '../common/1711018413374-RemoveFailedExecutionStatus'; import { MoveSshKeysToDatabase1711390882123 } from '../common/1711390882123-MoveSshKeysToDatabase'; +import { RemoveNodesAccess1712044305787 } from '../common/1712044305787-RemoveNodesAccess'; export const mysqlMigrations: Migration[] = [ InitialMigration1588157391238, @@ -111,4 +112,5 @@ export const mysqlMigrations: Migration[] = [ DropRoleMapping1705429061930, RemoveFailedExecutionStatus1711018413374, MoveSshKeysToDatabase1711390882123, + RemoveNodesAccess1712044305787, ]; diff --git a/packages/cli/src/databases/migrations/postgresdb/index.ts b/packages/cli/src/databases/migrations/postgresdb/index.ts index 3b572c2e5784a..cbf63389a7684 100644 --- a/packages/cli/src/databases/migrations/postgresdb/index.ts +++ b/packages/cli/src/databases/migrations/postgresdb/index.ts @@ -53,6 +53,7 @@ import { AddGlobalAdminRole1700571993961 } from '../common/1700571993961-AddGlob import { DropRoleMapping1705429061930 } from '../common/1705429061930-DropRoleMapping'; import { RemoveFailedExecutionStatus1711018413374 } from '../common/1711018413374-RemoveFailedExecutionStatus'; import { MoveSshKeysToDatabase1711390882123 } from '../common/1711390882123-MoveSshKeysToDatabase'; +import { RemoveNodesAccess1712044305787 } from '../common/1712044305787-RemoveNodesAccess'; export const postgresMigrations: Migration[] = [ InitialMigration1587669153312, @@ -109,4 +110,5 @@ export const postgresMigrations: Migration[] = [ DropRoleMapping1705429061930, RemoveFailedExecutionStatus1711018413374, MoveSshKeysToDatabase1711390882123, + RemoveNodesAccess1712044305787, ]; diff --git a/packages/cli/src/databases/migrations/sqlite/index.ts b/packages/cli/src/databases/migrations/sqlite/index.ts index bc25048b53028..f26f070087ed7 100644 --- a/packages/cli/src/databases/migrations/sqlite/index.ts +++ b/packages/cli/src/databases/migrations/sqlite/index.ts @@ -51,6 +51,7 @@ import { AddGlobalAdminRole1700571993961 } from '../common/1700571993961-AddGlob import { DropRoleMapping1705429061930 } from './1705429061930-DropRoleMapping'; import { RemoveFailedExecutionStatus1711018413374 } from '../common/1711018413374-RemoveFailedExecutionStatus'; import { MoveSshKeysToDatabase1711390882123 } from '../common/1711390882123-MoveSshKeysToDatabase'; +import { RemoveNodesAccess1712044305787 } from '../common/1712044305787-RemoveNodesAccess'; const sqliteMigrations: Migration[] = [ InitialMigration1588102412422, @@ -105,6 +106,7 @@ const sqliteMigrations: Migration[] = [ DropRoleMapping1705429061930, RemoveFailedExecutionStatus1711018413374, MoveSshKeysToDatabase1711390882123, + RemoveNodesAccess1712044305787, ]; export { sqliteMigrations }; diff --git a/packages/cli/src/databases/repositories/credentials.repository.ts b/packages/cli/src/databases/repositories/credentials.repository.ts index 0b11b4015122f..02ed714cad395 100644 --- a/packages/cli/src/databases/repositories/credentials.repository.ts +++ b/packages/cli/src/databases/repositories/credentials.repository.ts @@ -46,7 +46,7 @@ export class CredentialsRepository extends Repository { type Select = Array; const defaultRelations = ['shared', 'shared.user']; - const defaultSelect: Select = ['id', 'name', 'type', 'nodesAccess', 'createdAt', 'updatedAt']; + const defaultSelect: Select = ['id', 'name', 'type', 'createdAt', 'updatedAt']; if (!listQueryOptions) return { select: defaultSelect, relations: defaultRelations }; diff --git a/packages/cli/src/databases/repositories/execution.repository.ts b/packages/cli/src/databases/repositories/execution.repository.ts index 44b607fb9dab5..2c962f28bb8a1 100644 --- a/packages/cli/src/databases/repositories/execution.repository.ts +++ b/packages/cli/src/databases/repositories/execution.repository.ts @@ -407,7 +407,7 @@ export class ExecutionRepository extends Repository { } const query = this.createQueryBuilder('execution') - .select(['execution.id']) + .select(['execution.id', 'execution.workflowId']) .andWhere('execution.workflowId IN (:...accessibleWorkflowIds)', { accessibleWorkflowIds }); if (deleteConditions.deleteBefore) { @@ -433,12 +433,19 @@ export class ExecutionRepository extends Repository { return; } - const executionIds = executions.map(({ id }) => id); + const ids = executions.map(({ id, workflowId }) => ({ + executionId: id, + workflowId, + })); + do { // Delete in batches to avoid "SQLITE_ERROR: Expression tree is too large (maximum depth 1000)" error - const batch = executionIds.splice(0, this.hardDeletionBatchSize); - await this.delete(batch); - } while (executionIds.length > 0); + const batch = ids.splice(0, this.hardDeletionBatchSize); + await Promise.all([ + this.delete(batch.map(({ executionId }) => executionId)), + this.binaryDataService.deleteMany(batch), + ]); + } while (ids.length > 0); } async getIdsSince(date: Date) { diff --git a/packages/cli/src/environments/sourceControl/sourceControl.service.ee.ts b/packages/cli/src/environments/sourceControl/sourceControl.service.ee.ts index 759c69f74b7e7..c52c0300d5c17 100644 --- a/packages/cli/src/environments/sourceControl/sourceControl.service.ee.ts +++ b/packages/cli/src/environments/sourceControl/sourceControl.service.ee.ts @@ -25,7 +25,6 @@ import { SourceControlPreferencesService } from './sourceControlPreferences.serv import { writeFileSync } from 'fs'; import { SourceControlImportService } from './sourceControlImport.service.ee'; import type { User } from '@db/entities/User'; -import isEqual from 'lodash/isEqual'; import type { SourceControlGetStatus } from './types/sourceControlGetStatus'; import type { TagEntity } from '@db/entities/TagEntity'; import type { Variables } from '@db/entities/Variables'; @@ -384,7 +383,7 @@ export class SourceControlService { * Does a comparison between the local and remote workfolder based on NOT the git status, * but certain parameters within the items being synced. * For workflows, it compares the versionIds - * For credentials, it compares the name, type and nodeAccess + * For credentials, it compares the name and type * For variables, it compares the name * For tags, it compares the name and mapping * @returns either SourceControlledFile[] if verbose is false, @@ -565,12 +564,7 @@ export class SourceControlService { > = []; credLocalIds.forEach((local) => { const mismatchingCreds = credRemoteIds.find((remote) => { - return ( - remote.id === local.id && - (remote.name !== local.name || - remote.type !== local.type || - !isEqual(remote.nodesAccess, local.nodesAccess)) - ); + return remote.id === local.id && (remote.name !== local.name || remote.type !== local.type); }); if (mismatchingCreds) { credModifiedInEither.push({ diff --git a/packages/cli/src/environments/sourceControl/sourceControlExport.service.ee.ts b/packages/cli/src/environments/sourceControl/sourceControlExport.service.ee.ts index 30c0174fa1408..eb8269493033d 100644 --- a/packages/cli/src/environments/sourceControl/sourceControlExport.service.ee.ts +++ b/packages/cli/src/environments/sourceControl/sourceControlExport.service.ee.ts @@ -240,15 +240,14 @@ export class SourceControlExportService { } await Promise.all( credentialsToBeExported.map(async (sharing) => { - const { name, type, nodesAccess, data, id } = sharing.credentials; - const credentials = new Credentials({ id, name }, type, nodesAccess, data); + const { name, type, data, id } = sharing.credentials; + const credentials = new Credentials({ id, name }, type, data); const stub: ExportableCredential = { id, name, type, data: this.replaceCredentialData(credentials.getData()), - nodesAccess, ownedBy: sharing.user.email, }; diff --git a/packages/cli/src/environments/sourceControl/sourceControlImport.service.ee.ts b/packages/cli/src/environments/sourceControl/sourceControlImport.service.ee.ts index bf0adc6bb0dce..a81aaae17e607 100644 --- a/packages/cli/src/environments/sourceControl/sourceControlImport.service.ee.ts +++ b/packages/cli/src/environments/sourceControl/sourceControlImport.service.ee.ts @@ -142,13 +142,12 @@ export class SourceControlImportService { Array > { const localCredentials = await Container.get(CredentialsRepository).find({ - select: ['id', 'name', 'type', 'nodesAccess'], + select: ['id', 'name', 'type'], }); return localCredentials.map((local) => ({ id: local.id, name: local.name, type: local.type, - nodesAccess: local.nodesAccess, filename: getCredentialExportPath(local.id, this.credentialExportFolder), })) as Array; } @@ -339,14 +338,13 @@ export class SourceControlImportService { (e) => e.id === credential.id && e.type === credential.type, ); - const { name, type, data, id, nodesAccess } = credential; - const newCredentialObject = new Credentials({ id, name }, type, []); + const { name, type, data, id } = credential; + const newCredentialObject = new Credentials({ id, name }, type); if (existingCredential?.data) { newCredentialObject.data = existingCredential.data; } else { newCredentialObject.setData(data); } - newCredentialObject.nodesAccess = nodesAccess || existingCredential?.nodesAccess || []; this.logger.debug(`Updating credential id ${newCredentialObject.id as string}`); await Container.get(CredentialsRepository).upsert(newCredentialObject, ['id']); diff --git a/packages/cli/src/environments/sourceControl/types/exportableCredential.ts b/packages/cli/src/environments/sourceControl/types/exportableCredential.ts index 2bf8903cc9f34..36197da6caa80 100644 --- a/packages/cli/src/environments/sourceControl/types/exportableCredential.ts +++ b/packages/cli/src/environments/sourceControl/types/exportableCredential.ts @@ -1,11 +1,10 @@ -import type { ICredentialDataDecryptedObject, ICredentialNodeAccess } from 'n8n-workflow'; +import type { ICredentialDataDecryptedObject } from 'n8n-workflow'; export interface ExportableCredential { id: string; name: string; type: string; data: ICredentialDataDecryptedObject; - nodesAccess: ICredentialNodeAccess[]; /** * Email of the user who owns this credential at the source instance. diff --git a/packages/cli/src/errors/max-stalled-count.error.ts b/packages/cli/src/errors/max-stalled-count.error.ts new file mode 100644 index 0000000000000..6715de0ade837 --- /dev/null +++ b/packages/cli/src/errors/max-stalled-count.error.ts @@ -0,0 +1,13 @@ +import { ApplicationError } from 'n8n-workflow'; + +/** + * See https://github.com/OptimalBits/bull/blob/60fa88f08637f0325639988a3f054880a04ce402/docs/README.md?plain=1#L133 + */ +export class MaxStalledCountError extends ApplicationError { + constructor(cause: Error) { + super('The execution has reached the maximum number of attempts and will no longer retry.', { + level: 'warning', + cause, + }); + } +} diff --git a/packages/cli/src/eventbus/EventMessageClasses/index.ts b/packages/cli/src/eventbus/EventMessageClasses/index.ts index 51ab91fb01a10..cecfcdbaf5188 100644 --- a/packages/cli/src/eventbus/EventMessageClasses/index.ts +++ b/packages/cli/src/eventbus/EventMessageClasses/index.ts @@ -27,6 +27,8 @@ export const eventNamesAudit = [ 'n8n.audit.user.reset', 'n8n.audit.user.credentials.created', 'n8n.audit.user.credentials.shared', + 'n8n.audit.user.credentials.updated', + 'n8n.audit.user.credentials.deleted', 'n8n.audit.user.api.created', 'n8n.audit.user.api.deleted', 'n8n.audit.package.installed', diff --git a/packages/cli/src/requests.ts b/packages/cli/src/requests.ts index e3f004805836a..5946b07117659 100644 --- a/packages/cli/src/requests.ts +++ b/packages/cli/src/requests.ts @@ -2,7 +2,6 @@ import type express from 'express'; import type { BannerName, ICredentialDataDecryptedObject, - ICredentialNodeAccess, IDataObject, INodeCredentialTestRequest, INodeCredentials, @@ -158,7 +157,6 @@ export declare namespace CredentialRequest { id: string; // delete if sent name: string; type: string; - nodesAccess: ICredentialNodeAccess[]; data: ICredentialDataDecryptedObject; }>; diff --git a/packages/cli/src/services/cache/cache.service.ts b/packages/cli/src/services/cache/cache.service.ts index 09499a4da4df1..226c68438fba8 100644 --- a/packages/cli/src/services/cache/cache.service.ts +++ b/packages/cli/src/services/cache/cache.service.ts @@ -2,7 +2,7 @@ import EventEmitter from 'node:events'; import { Service } from 'typedi'; import { caching } from 'cache-manager'; -import { jsonStringify } from 'n8n-workflow'; +import { ApplicationError, jsonStringify } from 'n8n-workflow'; import config from '@/config'; import { getDefaultRedisClient, getRedisPrefix } from '@/services/redis/RedisServiceHelper'; @@ -137,10 +137,9 @@ export class CacheService extends EventEmitter { if (!key?.length) return; if (this.cache.kind === 'memory') { - setTimeout(async () => { - await this.cache.store.del(key); - }, ttlMs); - return; + throw new ApplicationError('Method `expire` not yet implemented for in-memory cache', { + level: 'warning', + }); } await this.cache.store.expire(key, ttlMs / TIME.SECOND); diff --git a/packages/cli/src/services/test-webhook-registrations.service.ts b/packages/cli/src/services/test-webhook-registrations.service.ts index b1cc8920a1078..e2abae5605f98 100644 --- a/packages/cli/src/services/test-webhook-registrations.service.ts +++ b/packages/cli/src/services/test-webhook-registrations.service.ts @@ -3,6 +3,7 @@ import { CacheService } from '@/services/cache/cache.service'; import type { IWebhookData } from 'n8n-workflow'; import type { IWorkflowDb } from '@/Interfaces'; import { TEST_WEBHOOK_TIMEOUT, TEST_WEBHOOK_TIMEOUT_BUFFER } from '@/constants'; +import { OrchestrationService } from './orchestration.service'; export type TestWebhookRegistration = { pushRef?: string; @@ -13,7 +14,10 @@ export type TestWebhookRegistration = { @Service() export class TestWebhookRegistrationsService { - constructor(private readonly cacheService: CacheService) {} + constructor( + private readonly cacheService: CacheService, + private readonly orchestrationService: OrchestrationService, + ) {} private readonly cacheKey = 'test-webhooks'; @@ -22,6 +26,8 @@ export class TestWebhookRegistrationsService { await this.cacheService.setHash(this.cacheKey, { [hashKey]: registration }); + if (!this.orchestrationService.isMultiMainSetupEnabled) return; + /** * Multi-main setup: In a manual webhook execution, the main process that * handles a webhook might not be the same as the main process that created diff --git a/packages/cli/test/integration/commands/credentials.cmd.test.ts b/packages/cli/test/integration/commands/credentials.cmd.test.ts index d5beb03552e17..1ec68f72633cd 100644 --- a/packages/cli/test/integration/commands/credentials.cmd.test.ts +++ b/packages/cli/test/integration/commands/credentials.cmd.test.ts @@ -45,6 +45,5 @@ test('import:credentials should import a credential', async () => { expect(after.length).toBe(1); expect(after[0].name).toBe('cred-aws-test'); expect(after[0].id).toBe('123'); - expect(after[0].nodesAccess).toStrictEqual([]); mockExit.mockRestore(); }); diff --git a/packages/cli/test/integration/commands/importCredentials/credentials.json b/packages/cli/test/integration/commands/importCredentials/credentials.json index 136a2205b6e4b..0e6269d2670f1 100644 --- a/packages/cli/test/integration/commands/importCredentials/credentials.json +++ b/packages/cli/test/integration/commands/importCredentials/credentials.json @@ -9,7 +9,6 @@ "accessKeyId": "999999999999", "secretAccessKey": "aaaaaaaaaaaaa" }, - "type": "aws", - "nodesAccess": "" + "type": "aws" } ] diff --git a/packages/cli/test/integration/credentials.controller.test.ts b/packages/cli/test/integration/credentials.controller.test.ts index 806d30eb95894..df88c2b12c1c8 100644 --- a/packages/cli/test/integration/credentials.controller.test.ts +++ b/packages/cli/test/integration/credentials.controller.test.ts @@ -264,11 +264,10 @@ describe('GET /credentials', () => { }); function validateCredential(credential: ListQuery.Credentials.WithOwnedByAndSharedWith) { - const { name, type, nodesAccess, sharedWith, ownedBy } = credential; + const { name, type, sharedWith, ownedBy } = credential; expect(typeof name).toBe('string'); expect(typeof type).toBe('string'); - expect(typeof nodesAccess[0].nodeType).toBe('string'); expect('data' in credential).toBe(false); if (sharedWith) expect(Array.isArray(sharedWith)).toBe(true); diff --git a/packages/cli/test/integration/credentials.ee.test.ts b/packages/cli/test/integration/credentials.ee.test.ts index e62c19338c9fd..279f33c019dde 100644 --- a/packages/cli/test/integration/credentials.ee.test.ts +++ b/packages/cli/test/integration/credentials.ee.test.ts @@ -539,7 +539,6 @@ describe('PUT /credentials/:id/share', () => { function validateMainCredentialData(credential: ListQuery.Credentials.WithOwnedByAndSharedWith) { expect(typeof credential.name).toBe('string'); expect(typeof credential.type).toBe('string'); - expect(typeof credential.nodesAccess[0].nodeType).toBe('string'); expect(credential.ownedBy).toBeDefined(); expect(Array.isArray(credential.sharedWith)).toBe(true); } diff --git a/packages/cli/test/integration/credentials.test.ts b/packages/cli/test/integration/credentials.test.ts index 122a46f0b3c13..aa40616ad8681 100644 --- a/packages/cli/test/integration/credentials.test.ts +++ b/packages/cli/test/integration/credentials.test.ts @@ -96,21 +96,16 @@ describe('POST /credentials', () => { expect(response.statusCode).toBe(200); - const { id, name, type, nodesAccess, data: encryptedData } = response.body.data; + const { id, name, type, data: encryptedData } = response.body.data; expect(name).toBe(payload.name); expect(type).toBe(payload.type); - if (!payload.nodesAccess) { - fail('Payload did not contain a nodesAccess array'); - } - expect(nodesAccess[0].nodeType).toBe(payload.nodesAccess[0].nodeType); expect(encryptedData).not.toBe(payload.data); const credential = await Container.get(CredentialsRepository).findOneByOrFail({ id }); expect(credential.name).toBe(payload.name); expect(credential.type).toBe(payload.type); - expect(credential.nodesAccess[0].nodeType).toBe(payload.nodesAccess[0].nodeType); expect(credential.data).not.toBe(payload.data); const sharedCredential = await Container.get(SharedCredentialsRepository).findOneOrFail({ @@ -258,14 +253,10 @@ describe('PATCH /credentials/:id', () => { expect(response.statusCode).toBe(200); - const { id, name, type, nodesAccess, data: encryptedData } = response.body.data; + const { id, name, type, data: encryptedData } = response.body.data; expect(name).toBe(patchPayload.name); expect(type).toBe(patchPayload.type); - if (!patchPayload.nodesAccess) { - fail('Payload did not contain a nodesAccess array'); - } - expect(nodesAccess[0].nodeType).toBe(patchPayload.nodesAccess[0].nodeType); expect(encryptedData).not.toBe(patchPayload.data); @@ -273,7 +264,6 @@ describe('PATCH /credentials/:id', () => { expect(credential.name).toBe(patchPayload.name); expect(credential.type).toBe(patchPayload.type); - expect(credential.nodesAccess[0].nodeType).toBe(patchPayload.nodesAccess[0].nodeType); expect(credential.data).not.toBe(patchPayload.data); const sharedCredential = await Container.get(SharedCredentialsRepository).findOneOrFail({ @@ -304,7 +294,6 @@ describe('PATCH /credentials/:id', () => { const credentialObject = new Credentials( { id: credential.id, name: credential.name }, credential.type, - credential.nodesAccess, credential.data, ); expect(credentialObject.getData()).toStrictEqual(patchPayload.data); @@ -327,23 +316,17 @@ describe('PATCH /credentials/:id', () => { expect(response.statusCode).toBe(200); - const { id, name, type, nodesAccess, data: encryptedData } = response.body.data; + const { id, name, type, data: encryptedData } = response.body.data; expect(name).toBe(patchPayload.name); expect(type).toBe(patchPayload.type); - if (!patchPayload.nodesAccess) { - fail('Payload did not contain a nodesAccess array'); - } - expect(nodesAccess[0].nodeType).toBe(patchPayload.nodesAccess[0].nodeType); - expect(encryptedData).not.toBe(patchPayload.data); const credential = await Container.get(CredentialsRepository).findOneByOrFail({ id }); expect(credential.name).toBe(patchPayload.name); expect(credential.type).toBe(patchPayload.type); - expect(credential.nodesAccess[0].nodeType).toBe(patchPayload.nodesAccess[0].nodeType); expect(credential.data).not.toBe(patchPayload.data); const sharedCredential = await Container.get(SharedCredentialsRepository).findOneOrFail({ @@ -545,11 +528,10 @@ describe('GET /credentials/:id', () => { }); function validateMainCredentialData(credential: ListQuery.Credentials.WithOwnedByAndSharedWith) { - const { name, type, nodesAccess, sharedWith, ownedBy } = credential; + const { name, type, sharedWith, ownedBy } = credential; expect(typeof name).toBe('string'); expect(typeof type).toBe('string'); - expect(typeof nodesAccess?.[0].nodeType).toBe('string'); if (sharedWith) { expect(Array.isArray(sharedWith)).toBe(true); @@ -568,23 +550,15 @@ function validateMainCredentialData(credential: ListQuery.Credentials.WithOwnedB const INVALID_PAYLOADS = [ { type: randomName(), - nodesAccess: [{ nodeType: randomName() }], data: { accessToken: randomString(6, 16) }, }, { name: randomName(), - nodesAccess: [{ nodeType: randomName() }], - data: { accessToken: randomString(6, 16) }, - }, - { - name: randomName(), - type: randomName(), data: { accessToken: randomString(6, 16) }, }, { name: randomName(), type: randomName(), - nodesAccess: [{ nodeType: randomName() }], }, {}, undefined, diff --git a/packages/cli/test/integration/environments/source-control-import.service.test.ts b/packages/cli/test/integration/environments/source-control-import.service.test.ts index d0615f1dd44c8..fd3327cedd5a9 100644 --- a/packages/cli/test/integration/environments/source-control-import.service.test.ts +++ b/packages/cli/test/integration/environments/source-control-import.service.test.ts @@ -54,7 +54,6 @@ describe('SourceControlImportService', () => { name: 'My Credential', type: 'someCredentialType', data: {}, - nodesAccess: [], ownedBy: member.email, // user at source instance owns credential }; @@ -90,7 +89,6 @@ describe('SourceControlImportService', () => { name: 'My Credential', type: 'someCredentialType', data: {}, - nodesAccess: [], ownedBy: null, }; @@ -126,7 +124,6 @@ describe('SourceControlImportService', () => { name: 'My Credential', type: 'someCredentialType', data: {}, - nodesAccess: [], ownedBy: 'user@test.com', // user at source instance owns credential }; diff --git a/packages/cli/test/integration/publicApi/credentials.test.ts b/packages/cli/test/integration/publicApi/credentials.test.ts index 6ce0723874a4d..d028834638ca0 100644 --- a/packages/cli/test/integration/publicApi/credentials.test.ts +++ b/packages/cli/test/integration/publicApi/credentials.test.ts @@ -258,7 +258,6 @@ const credentialPayload = (): CredentialPayload => ({ const dbCredential = () => { const credential = credentialPayload(); - credential.nodesAccess = [{ nodeType: credential.type }]; return credential; }; @@ -276,13 +275,6 @@ const INVALID_PAYLOADS = [ name: randomName(), type: randomName(), }, - { - name: randomName(), - type: 'ftp', - data: { - username: randomName(), - }, - }, {}, [], undefined, diff --git a/packages/cli/test/integration/publicApi/workflows.test.ts b/packages/cli/test/integration/publicApi/workflows.test.ts index 80a88f134ceaf..c91178750cc18 100644 --- a/packages/cli/test/integration/publicApi/workflows.test.ts +++ b/packages/cli/test/integration/publicApi/workflows.test.ts @@ -696,6 +696,7 @@ describe('POST /workflows', () => { saveDataSuccessExecution: 'all', executionTimeout: 3600, timezone: 'America/New_York', + executionOrder: 'v1', }, }; diff --git a/packages/cli/test/integration/security-audit/CredentialsRiskReporter.test.ts b/packages/cli/test/integration/security-audit/CredentialsRiskReporter.test.ts index 9a10e6e70c8a0..c395147e1d5a6 100644 --- a/packages/cli/test/integration/security-audit/CredentialsRiskReporter.test.ts +++ b/packages/cli/test/integration/security-audit/CredentialsRiskReporter.test.ts @@ -33,7 +33,6 @@ test('should report credentials not in any use', async () => { name: 'My Slack Credential', data: 'U2FsdGVkX18WjITBG4IDqrGB1xE/uzVNjtwDAG3lP7E=', type: 'slackApi', - nodesAccess: [{ nodeType: 'n8n-nodes-base.slack', date: '2022-12-21T11:23:00.561Z' }], }; const workflowDetails = { @@ -79,7 +78,6 @@ test('should report credentials not in active use', async () => { name: 'My Slack Credential', data: 'U2FsdGVkX18WjITBG4IDqrGB1xE/uzVNjtwDAG3lP7E=', type: 'slackApi', - nodesAccess: [{ nodeType: 'n8n-nodes-base.slack', date: '2022-12-21T11:23:00.561Z' }], }; const credential = await Container.get(CredentialsRepository).save(credentialDetails); @@ -124,7 +122,6 @@ test('should report credential in not recently executed workflow', async () => { name: 'My Slack Credential', data: 'U2FsdGVkX18WjITBG4IDqrGB1xE/uzVNjtwDAG3lP7E=', type: 'slackApi', - nodesAccess: [{ nodeType: 'n8n-nodes-base.slack', date: '2022-12-21T11:23:00.561Z' }], }; const credential = await Container.get(CredentialsRepository).save(credentialDetails); @@ -192,7 +189,6 @@ test('should not report credentials in recently executed workflow', async () => name: 'My Slack Credential', data: 'U2FsdGVkX18WjITBG4IDqrGB1xE/uzVNjtwDAG3lP7E=', type: 'slackApi', - nodesAccess: [{ nodeType: 'n8n-nodes-base.slack', date: '2022-12-21T11:23:00.561Z' }], }; const credential = await Container.get(CredentialsRepository).save(credentialDetails); diff --git a/packages/cli/test/integration/shared/db/credentials.ts b/packages/cli/test/integration/shared/db/credentials.ts index 2208e089362c1..4e77b2fcc2322 100644 --- a/packages/cli/test/integration/shared/db/credentials.ts +++ b/packages/cli/test/integration/shared/db/credentials.ts @@ -21,7 +21,6 @@ const emptyAttributes = { name: 'test', type: 'test', data: '', - nodesAccess: [], }; export async function createManyCredentials( diff --git a/packages/cli/test/integration/shared/db/users.ts b/packages/cli/test/integration/shared/db/users.ts index 80508191adb56..4f4ed8af184b9 100644 --- a/packages/cli/test/integration/shared/db/users.ts +++ b/packages/cli/test/integration/shared/db/users.ts @@ -12,12 +12,10 @@ import { randomApiKey, randomEmail, randomName, randomValidPassword } from '../r // pre-computed bcrypt hash for the string 'password', using `await hash('password', 10)` const passwordHash = '$2a$10$njedH7S6V5898mj6p0Jr..IGY9Ms.qNwR7RbSzzX9yubJocKfvGGK'; -/** - * Store a user in the DB, defaulting to a `member`. - */ -export async function createUser(attributes: Partial = {}): Promise { +/** Store a new user object, defaulting to a `member` */ +export async function newUser(attributes: Partial = {}): Promise { const { email, password, firstName, lastName, role, ...rest } = attributes; - const user = Container.get(UserRepository).create({ + return Container.get(UserRepository).create({ email: email ?? randomEmail(), password: password ? await hash(password, 1) : passwordHash, firstName: firstName ?? randomName(), @@ -25,8 +23,12 @@ export async function createUser(attributes: Partial = {}): Promise role: role ?? 'global:member', ...rest, }); - user.computeIsOwner(); +} +/** Store a user object in the DB */ +export async function createUser(attributes: Partial = {}): Promise { + const user = await newUser(attributes); + user.computeIsOwner(); return await Container.get(UserRepository).save(user); } @@ -98,21 +100,11 @@ export async function createManyUsers( amount: number, attributes: Partial = {}, ): Promise { - let { email, password, firstName, lastName, role, ...rest } = attributes; - const users = await Promise.all( - [...Array(amount)].map(async () => - Container.get(UserRepository).create({ - email: email ?? randomEmail(), - password: password ? await hash(password, 1) : passwordHash, - firstName: firstName ?? randomName(), - lastName: lastName ?? randomName(), - role: role ?? 'global:member', - ...rest, - }), - ), + Array(amount) + .fill(0) + .map(async () => await newUser(attributes)), ); - return await Container.get(UserRepository).save(users); } diff --git a/packages/cli/test/integration/shared/random.ts b/packages/cli/test/integration/shared/random.ts index 98befbaa75471..62b5c73ee0a9c 100644 --- a/packages/cli/test/integration/shared/random.ts +++ b/packages/cli/test/integration/shared/random.ts @@ -59,7 +59,6 @@ export const randomName = () => randomString(4, 8); export const randomCredentialPayload = (): CredentialPayload => ({ name: randomName(), type: randomName(), - nodesAccess: [{ nodeType: randomName() }], data: { accessToken: randomString(6, 16) }, }); diff --git a/packages/cli/test/integration/shared/types.ts b/packages/cli/test/integration/shared/types.ts index 2482beb95c3b4..8355d6f39c3f0 100644 --- a/packages/cli/test/integration/shared/types.ts +++ b/packages/cli/test/integration/shared/types.ts @@ -1,5 +1,5 @@ import type { Application } from 'express'; -import type { ICredentialDataDecryptedObject, ICredentialNodeAccess } from 'n8n-workflow'; +import type { ICredentialDataDecryptedObject } from 'n8n-workflow'; import type { SuperAgentTest } from 'supertest'; import type { Server } from 'http'; @@ -52,7 +52,6 @@ export interface TestServer { export type CredentialPayload = { name: string; type: string; - nodesAccess?: ICredentialNodeAccess[]; data: ICredentialDataDecryptedObject; }; diff --git a/packages/cli/test/integration/users.api.test.ts b/packages/cli/test/integration/users.api.test.ts index fefb0161b12f7..5c9bdc2600a8e 100644 --- a/packages/cli/test/integration/users.api.test.ts +++ b/packages/cli/test/integration/users.api.test.ts @@ -244,7 +244,7 @@ describe('DELETE /users/:id', () => { const savedWorkflow = await createWorkflow({ name: randomName() }, member); const savedCredential = await saveCredential( - { name: randomName(), type: '', data: {}, nodesAccess: [] }, + { name: randomName(), type: '', data: {} }, { user: member, role: 'credential:owner' }, ); @@ -286,7 +286,7 @@ describe('DELETE /users/:id', () => { const [savedWorkflow, savedCredential] = await Promise.all([ await createWorkflow({ name: randomName() }, member), await saveCredential( - { name: randomName(), type: '', data: {}, nodesAccess: [] }, + { name: randomName(), type: '', data: {} }, { user: member, role: 'credential:owner', diff --git a/packages/cli/test/shared/mocking.ts b/packages/cli/test/shared/mocking.ts index 7f941378b1e73..5a6183c0e8cbc 100644 --- a/packages/cli/test/shared/mocking.ts +++ b/packages/cli/test/shared/mocking.ts @@ -1,6 +1,7 @@ import { Container } from 'typedi'; import { mock } from 'jest-mock-extended'; import type { DeepPartial } from 'ts-essentials'; +import { DataSource, EntityManager, type EntityMetadata } from '@n8n/typeorm'; import type { Class } from 'n8n-core'; export const mockInstance = ( @@ -11,3 +12,13 @@ export const mockInstance = ( Container.set(serviceClass, instance); return instance; }; + +export const mockEntityManager = (entityClass: Class) => { + const entityManager = mockInstance(EntityManager); + const dataSource = mockInstance(DataSource, { + manager: entityManager, + getMetadata: () => mock({ target: entityClass }), + }); + Object.assign(entityManager, { connection: dataSource }); + return entityManager; +}; diff --git a/packages/cli/test/unit/controllers/oAuth1Credential.controller.test.ts b/packages/cli/test/unit/controllers/oAuth1Credential.controller.test.ts index 59d9b8f6360a4..6b4b55788a259 100644 --- a/packages/cli/test/unit/controllers/oAuth1Credential.controller.test.ts +++ b/packages/cli/test/unit/controllers/oAuth1Credential.controller.test.ts @@ -39,7 +39,6 @@ describe('OAuth1CredentialController', () => { const credential = mock({ id: '1', name: 'Test Credential', - nodesAccess: [], type: 'oAuth1Api', }); diff --git a/packages/cli/test/unit/controllers/oAuth2Credential.controller.test.ts b/packages/cli/test/unit/controllers/oAuth2Credential.controller.test.ts index 9acbe305be809..16e3e93345796 100644 --- a/packages/cli/test/unit/controllers/oAuth2Credential.controller.test.ts +++ b/packages/cli/test/unit/controllers/oAuth2Credential.controller.test.ts @@ -43,7 +43,6 @@ describe('OAuth2CredentialController', () => { const credential = mock({ id: '1', name: 'Test Credential', - nodesAccess: [], type: 'oAuth2Api', }); diff --git a/packages/cli/test/unit/repositories/execution.repository.test.ts b/packages/cli/test/unit/repositories/execution.repository.test.ts index 57a223df25e2d..f09d22fe72a1b 100644 --- a/packages/cli/test/unit/repositories/execution.repository.test.ts +++ b/packages/cli/test/unit/repositories/execution.repository.test.ts @@ -1,20 +1,20 @@ -import { mock } from 'jest-mock-extended'; import Container from 'typedi'; -import type { EntityMetadata } from '@n8n/typeorm'; -import { EntityManager, DataSource, Not, LessThanOrEqual } from '@n8n/typeorm'; + +import type { SelectQueryBuilder } from '@n8n/typeorm'; +import { Not, LessThanOrEqual } from '@n8n/typeorm'; import config from '@/config'; import { ExecutionEntity } from '@db/entities/ExecutionEntity'; import { ExecutionRepository } from '@db/repositories/execution.repository'; - +import { mockEntityManager } from '../../shared/mocking'; import { mockInstance } from '../../shared/mocking'; +import { BinaryDataService } from 'n8n-core'; +import { nanoid } from 'nanoid'; +import { mock } from 'jest-mock-extended'; describe('ExecutionRepository', () => { - const entityManager = mockInstance(EntityManager); - const dataSource = mockInstance(DataSource, { manager: entityManager }); - dataSource.getMetadata.mockReturnValue(mock({ target: ExecutionEntity })); - Object.assign(entityManager, { connection: dataSource }); - + const entityManager = mockEntityManager(ExecutionEntity); + const binaryDataService = mockInstance(BinaryDataService); const executionRepository = Container.get(ExecutionRepository); const mockDate = new Date('2023-12-28 12:34:56.789Z'); @@ -49,4 +49,22 @@ describe('ExecutionRepository', () => { }, ); }); + + describe('deleteExecutionsByFilter', () => { + test('should delete binary data', async () => { + const workflowId = nanoid(); + + jest.spyOn(executionRepository, 'createQueryBuilder').mockReturnValue( + mock>({ + select: jest.fn().mockReturnThis(), + andWhere: jest.fn().mockReturnThis(), + getMany: jest.fn().mockResolvedValue([{ id: '1', workflowId }]), + }), + ); + + await executionRepository.deleteExecutionsByFilter({ id: '1' }, ['1'], { ids: ['1'] }); + + expect(binaryDataService.deleteMany).toHaveBeenCalledWith([{ executionId: '1', workflowId }]); + }); + }); }); diff --git a/packages/cli/test/unit/repositories/sharedCredentials.repository.test.ts b/packages/cli/test/unit/repositories/sharedCredentials.repository.test.ts index 4a808bfb35da6..1624cf5e655b9 100644 --- a/packages/cli/test/unit/repositories/sharedCredentials.repository.test.ts +++ b/packages/cli/test/unit/repositories/sharedCredentials.repository.test.ts @@ -1,21 +1,16 @@ import { Container } from 'typedi'; -import { DataSource, EntityManager, type EntityMetadata } from '@n8n/typeorm'; import { mock } from 'jest-mock-extended'; +import { hasScope } from '@n8n/permissions'; + import type { User } from '@db/entities/User'; import type { CredentialsEntity } from '@db/entities/CredentialsEntity'; import { SharedCredentials } from '@db/entities/SharedCredentials'; import { SharedCredentialsRepository } from '@db/repositories/sharedCredentials.repository'; -import { mockInstance } from '../../shared/mocking'; import { memberPermissions, ownerPermissions } from '@/permissions/roles'; -import { hasScope } from '@n8n/permissions'; +import { mockEntityManager } from '../../shared/mocking'; describe('SharedCredentialsRepository', () => { - const entityManager = mockInstance(EntityManager); - const dataSource = mockInstance(DataSource, { - manager: entityManager, - getMetadata: () => mock({ target: SharedCredentials }), - }); - Object.assign(entityManager, { connection: dataSource }); + const entityManager = mockEntityManager(SharedCredentials); const repository = Container.get(SharedCredentialsRepository); describe('findCredentialForUser', () => { diff --git a/packages/cli/test/unit/repositories/workflowStatistics.test.ts b/packages/cli/test/unit/repositories/workflowStatistics.test.ts index ea56b2d84c8ed..86e0ee1c92bd1 100644 --- a/packages/cli/test/unit/repositories/workflowStatistics.test.ts +++ b/packages/cli/test/unit/repositories/workflowStatistics.test.ts @@ -1,22 +1,23 @@ -import { WorkflowStatisticsRepository } from '@db/repositories/workflowStatistics.repository'; -import { DataSource, EntityManager, InsertResult, QueryFailedError } from '@n8n/typeorm'; -import { mockInstance } from '../../shared/mocking'; +import { Container } from 'typedi'; +import { type InsertResult, QueryFailedError } from '@n8n/typeorm'; import { mock, mockClear } from 'jest-mock-extended'; -import { StatisticsNames, WorkflowStatistics } from '@/databases/entities/WorkflowStatistics'; -const entityManager = mockInstance(EntityManager); -const dataSource = mockInstance(DataSource, { manager: entityManager }); -dataSource.getMetadata.mockReturnValue(mock()); -Object.assign(entityManager, { connection: dataSource }); -const workflowStatisticsRepository = new WorkflowStatisticsRepository(dataSource); +import { StatisticsNames, WorkflowStatistics } from '@db/entities/WorkflowStatistics'; +import { WorkflowStatisticsRepository } from '@db/repositories/workflowStatistics.repository'; + +import { mockEntityManager } from '../../shared/mocking'; describe('insertWorkflowStatistics', () => { + const entityManager = mockEntityManager(WorkflowStatistics); + const workflowStatisticsRepository = Container.get(WorkflowStatisticsRepository); + beforeEach(() => { mockClear(entityManager.insert); }); + it('Successfully inserts data when it is not yet present', async () => { entityManager.findOne.mockResolvedValueOnce(null); - entityManager.insert.mockResolvedValueOnce(mockInstance(InsertResult)); + entityManager.insert.mockResolvedValueOnce(mock()); const insertionResult = await workflowStatisticsRepository.insertWorkflowStatistics( StatisticsNames.dataLoaded, @@ -27,7 +28,7 @@ describe('insertWorkflowStatistics', () => { }); it('Does not insert when data is present', async () => { - entityManager.findOne.mockResolvedValueOnce(mockInstance(WorkflowStatistics)); + entityManager.findOne.mockResolvedValueOnce(mock()); const insertionResult = await workflowStatisticsRepository.insertWorkflowStatistics( StatisticsNames.dataLoaded, 'workflowId', @@ -40,7 +41,7 @@ describe('insertWorkflowStatistics', () => { it('throws an error when insertion fails', async () => { entityManager.findOne.mockResolvedValueOnce(null); entityManager.insert.mockImplementation(async () => { - throw new QueryFailedError('Query', [], 'driver error'); + throw new QueryFailedError('Query', [], new Error('driver error')); }); const insertionResult = await workflowStatisticsRepository.insertWorkflowStatistics( diff --git a/packages/cli/test/unit/services/test-webhook-registrations.service.test.ts b/packages/cli/test/unit/services/test-webhook-registrations.service.test.ts index 4bd9efac47a31..c93540938c1bd 100644 --- a/packages/cli/test/unit/services/test-webhook-registrations.service.test.ts +++ b/packages/cli/test/unit/services/test-webhook-registrations.service.test.ts @@ -1,11 +1,15 @@ import type { CacheService } from '@/services/cache/cache.service'; +import type { OrchestrationService } from '@/services/orchestration.service'; import type { TestWebhookRegistration } from '@/services/test-webhook-registrations.service'; import { TestWebhookRegistrationsService } from '@/services/test-webhook-registrations.service'; import { mock } from 'jest-mock-extended'; describe('TestWebhookRegistrationsService', () => { const cacheService = mock(); - const registrations = new TestWebhookRegistrationsService(cacheService); + const registrations = new TestWebhookRegistrationsService( + cacheService, + mock({ isMultiMainSetupEnabled: false }), + ); const registration = mock({ webhook: { httpMethod: 'GET', path: 'hello', webhookId: undefined }, @@ -20,6 +24,12 @@ describe('TestWebhookRegistrationsService', () => { expect(cacheService.setHash).toHaveBeenCalledWith(cacheKey, { [webhookKey]: registration }); }); + + test('should skip setting TTL in single-main setup', async () => { + await registrations.register(registration); + + expect(cacheService.expire).not.toHaveBeenCalled(); + }); }); describe('deregister()', () => { diff --git a/packages/core/src/Credentials.ts b/packages/core/src/Credentials.ts index 7714df6898326..1a54d96cd2760 100644 --- a/packages/core/src/Credentials.ts +++ b/packages/core/src/Credentials.ts @@ -6,19 +6,6 @@ import { Cipher } from './Cipher'; export class Credentials extends ICredentials { private readonly cipher = Container.get(Cipher); - /** - * Returns if the given nodeType has access to data - */ - hasNodeAccess(nodeType: string): boolean { - for (const accessData of this.nodesAccess) { - if (accessData.nodeType === nodeType) { - return true; - } - } - - return false; - } - /** * Sets new credential object */ @@ -29,14 +16,7 @@ export class Credentials extends ICredentials { /** * Returns the decrypted credential object */ - getData(nodeType?: string): ICredentialDataDecryptedObject { - if (nodeType && !this.hasNodeAccess(nodeType)) { - throw new ApplicationError('Node does not have access to credential', { - tags: { nodeType, credentialType: this.type }, - extra: { credentialName: this.name }, - }); - } - + getData(): ICredentialDataDecryptedObject { if (this.data === undefined) { throw new ApplicationError('No data is set so nothing can be returned.'); } @@ -65,7 +45,6 @@ export class Credentials extends ICredentials { name: this.name, type: this.type, data: this.data, - nodesAccess: this.nodesAccess, }; } } diff --git a/packages/core/src/NodeExecuteFunctions.ts b/packages/core/src/NodeExecuteFunctions.ts index 1f61e51b23e97..23b54432c84c1 100644 --- a/packages/core/src/NodeExecuteFunctions.ts +++ b/packages/core/src/NodeExecuteFunctions.ts @@ -98,6 +98,7 @@ import type { Workflow, WorkflowActivateMode, WorkflowExecuteMode, + CallbackManager, } from 'n8n-workflow'; import { ExpressionError, @@ -3487,6 +3488,7 @@ export function getExecuteFunctions( async executeWorkflow( workflowInfo: IExecuteWorkflowInfo, inputData?: INodeExecutionData[], + parentCallbackManager?: CallbackManager, ): Promise { return await additionalData .executeWorkflow(workflowInfo, additionalData, { @@ -3494,6 +3496,7 @@ export function getExecuteFunctions( inputData, parentWorkflowSettings: workflow.settings, node, + parentCallbackManager, }) .then( async (result) => @@ -3719,6 +3722,7 @@ export function getExecuteFunctions( msg, }); }, + getParentCallbackManager: () => additionalData.parentCallbackManager, }; })(workflow, runExecutionData, connectionInputData, inputData, node) as IExecuteFunctions; } diff --git a/packages/core/test/Credentials.test.ts b/packages/core/test/Credentials.test.ts index aca77870309d7..ada86a07b0a89 100644 --- a/packages/core/test/Credentials.test.ts +++ b/packages/core/test/Credentials.test.ts @@ -22,7 +22,7 @@ describe('Credentials', () => { describe('without nodeType set', () => { test('should be able to set and read key data without initial data set', () => { - const credentials = new Credentials({ id: null, name: 'testName' }, 'testType', []); + const credentials = new Credentials({ id: null, name: 'testName' }, 'testType'); const key = 'key1'; const newData = 1234; @@ -42,7 +42,6 @@ describe('Credentials', () => { const credentials = new Credentials( { id: null, name: 'testName' }, 'testType', - [], initialDataEncoded, ); @@ -56,46 +55,4 @@ describe('Credentials', () => { expect(credentials.getData().key1).toEqual(initialData); }); }); - - describe('with nodeType set', () => { - test('should be able to set and read key data without initial data set', () => { - const nodeAccess = [ - { - nodeType: 'base.noOp', - user: 'userName', - date: new Date(), - }, - ]; - - const credentials = new Credentials({ id: null, name: 'testName' }, 'testType', nodeAccess); - - const key = 'key1'; - const nodeType = 'base.noOp'; - const newData = 1234; - - setDataKey(credentials, key, newData); - - // Should be able to read with nodeType which has access - expect(credentials.getData(nodeType)[key]).toEqual(newData); - - // Should not be able to read with nodeType which does NOT have access - // expect(credentials.getData('base.otherNode')[key]).toThrowError(Error); - try { - credentials.getData('base.otherNode'); - expect(true).toBe(false); - } catch (e) { - expect(e.message).toBe('Node does not have access to credential'); - } - - // Get the data which will be saved in database - const dbData = credentials.getDataToSave(); - expect(dbData.name).toEqual('testName'); - expect(dbData.type).toEqual('testType'); - expect(dbData.nodesAccess).toEqual(nodeAccess); - // Compare only the first 6 characters as the rest seems to change with each execution - expect(dbData.data!.slice(0, 6)).toEqual( - 'U2FsdGVkX1+wpQWkj+YTzaPSNTFATjnlmFKIsUTZdhk='.slice(0, 6), - ); - }); - }); }); diff --git a/packages/design-system/src/css/_tokens.dark.scss b/packages/design-system/src/css/_tokens.dark.scss index eb306801668f3..04cfe54ec5db7 100644 --- a/packages/design-system/src/css/_tokens.dark.scss +++ b/packages/design-system/src/css/_tokens.dark.scss @@ -96,7 +96,7 @@ --color-json-boolean: var(--prim-color-alt-a); --color-json-number: var(--prim-color-alt-a); --color-json-string: var(--prim-color-secondary-tint-200); - --color-json-key: var(--prim-gray-670); + --color-json-key: var(--color-text-dark); --color-json-brackets: var(--prim-gray-670); --color-json-brackets-hover: var(--prim-color-alt-e); --color-json-line: var(--prim-gray-200); @@ -191,6 +191,9 @@ // Input Triple --color-background-input-triple: var(--prim-gray-800); + // Node error + --color-node-error-output-text-color: var(--prim-color-alt-c-tint-150); + // Various --color-info-tint-1: var(--prim-gray-420); --color-info-tint-2: var(--prim-gray-740); diff --git a/packages/design-system/src/css/_tokens.scss b/packages/design-system/src/css/_tokens.scss index a02179d303b3b..0ac9d97618ffa 100644 --- a/packages/design-system/src/css/_tokens.scss +++ b/packages/design-system/src/css/_tokens.scss @@ -267,6 +267,9 @@ // Input Triple --color-background-input-triple: var(--color-background-light); + // Node error + --color-node-error-output-text-color: var(--color-danger); + // Various --color-avatar-accent-1: var(--prim-gray-120); --color-avatar-accent-2: var(--prim-color-alt-e-shade-100); diff --git a/packages/editor-ui/package.json b/packages/editor-ui/package.json index 6aaf9ef007471..b7fd022f3b8d0 100644 --- a/packages/editor-ui/package.json +++ b/packages/editor-ui/package.json @@ -50,7 +50,7 @@ "axios": "1.6.7", "chart.js": "^4.4.0", "codemirror-lang-html-n8n": "^1.0.0", - "codemirror-lang-n8n-expression": "^0.2.0", + "codemirror-lang-n8n-expression": "^0.3.0", "dateformat": "^3.0.3", "email-providers": "^2.0.1", "esprima-next": "5.8.4", diff --git a/packages/editor-ui/src/Interface.ts b/packages/editor-ui/src/Interface.ts index a5c0202c4be2f..7fa606bca4871 100644 --- a/packages/editor-ui/src/Interface.ts +++ b/packages/editor-ui/src/Interface.ts @@ -139,6 +139,7 @@ export interface IUpdateInformation { | INodeParameters; // with null makes problems in NodeSettings.vue node?: string; oldValue?: string | number; + type?: 'optionsOrderChanged'; } export interface INodeUpdatePropertiesInformation { diff --git a/packages/editor-ui/src/__tests__/server/factories/credential.ts b/packages/editor-ui/src/__tests__/server/factories/credential.ts index b97bbbaea5d43..1cd28a06e6fac 100644 --- a/packages/editor-ui/src/__tests__/server/factories/credential.ts +++ b/packages/editor-ui/src/__tests__/server/factories/credential.ts @@ -12,9 +12,6 @@ export const credentialFactory = Factory.extend({ name() { return faker.company.name(); }, - nodesAccess() { - return []; - }, type() { return 'notionApi'; }, diff --git a/packages/editor-ui/src/components/BinaryDataDisplay.vue b/packages/editor-ui/src/components/BinaryDataDisplay.vue index 711923397592e..b84af41bb4feb 100644 --- a/packages/editor-ui/src/components/BinaryDataDisplay.vue +++ b/packages/editor-ui/src/components/BinaryDataDisplay.vue @@ -99,7 +99,7 @@ export default defineComponent({ z-index: 10; width: 100%; height: calc(100% - 50px); - background-color: var(--color-background-base); + background-color: var(--color-run-data-background); overflow: hidden; text-align: center; diff --git a/packages/editor-ui/src/components/CredentialCard.vue b/packages/editor-ui/src/components/CredentialCard.vue index ddce55096fbd8..f9eb6cbe1341a 100644 --- a/packages/editor-ui/src/components/CredentialCard.vue +++ b/packages/editor-ui/src/components/CredentialCard.vue @@ -68,7 +68,6 @@ export default defineComponent({ updatedAt: '', type: '', name: '', - nodesAccess: [], sharedWith: [], ownedBy: {} as IUser, }), diff --git a/packages/editor-ui/src/components/CredentialEdit/CredentialEdit.vue b/packages/editor-ui/src/components/CredentialEdit/CredentialEdit.vue index 86a97e52367c9..491a03e6b68c9 100644 --- a/packages/editor-ui/src/components/CredentialEdit/CredentialEdit.vue +++ b/packages/editor-ui/src/components/CredentialEdit/CredentialEdit.vue @@ -119,7 +119,6 @@ import type { ICredentialsResponse, IUser } from '@/Interface'; import type { CredentialInformation, ICredentialDataDecryptedObject, - ICredentialNodeAccess, ICredentialsDecrypted, ICredentialType, INode, @@ -165,10 +164,6 @@ import { isValidCredentialResponse, isCredentialModalState } from '@/utils/typeG import { isExpression, isTestableExpression } from '@/utils/expressions'; import { useExternalHooks } from '@/composables/useExternalHooks'; -interface NodeAccessMap { - [nodeType: string]: ICredentialNodeAccess | null; -} - export default defineComponent({ name: 'CredentialEdit', components: { @@ -212,7 +207,6 @@ export default defineComponent({ credentialName: '', credentialData: {} as ICredentialDataDecryptedObject, modalBus: createEventBus(), - nodeAccess: {} as NodeAccessMap, isDeleting: false, isSaving: false, isTesting: false, @@ -233,8 +227,6 @@ export default defineComponent({ isCredentialModalState(this.uiStore.modals[CREDENTIAL_EDIT_MODAL_KEY]) && this.uiStore.modals[CREDENTIAL_EDIT_MODAL_KEY].showAuthSelector === true; - this.setupNodeAccess(); - if (this.mode === 'new' && this.credentialTypeName) { this.credentialName = await this.credentialsStore.getNewCredentialName({ credentialTypeName: this.defaultCredentialTypeName, @@ -765,7 +757,6 @@ export default defineComponent({ name: this.credentialName, type: this.credentialTypeName!, data: credentialData, - nodesAccess: [], }; this.isRetesting = true; @@ -817,7 +808,6 @@ export default defineComponent({ name: this.credentialName, type: this.credentialTypeName!, data: data as unknown as ICredentialDataDecryptedObject, - nodesAccess: [], sharedWith, ownedBy, }; @@ -1091,7 +1081,6 @@ export default defineComponent({ if (credentialsForType) { this.selectedCredential = credentialsForType.name; this.resetCredentialData(); - this.setupNodeAccess(); // Update current node auth type so credentials dropdown can be displayed properly updateNodeAuthType(this.ndvStore.activeNode, type); // Also update credential name but only if the default name is still used @@ -1103,9 +1092,6 @@ export default defineComponent({ } } }, - setupNodeAccess(): void { - this.nodeAccess = {}; - }, resetCredentialData(): void { if (!this.credentialType) { return; diff --git a/packages/editor-ui/src/components/FixedCollectionParameter.vue b/packages/editor-ui/src/components/FixedCollectionParameter.vue index e1545f9e9efa2..6fabba8532868 100644 --- a/packages/editor-ui/src/components/FixedCollectionParameter.vue +++ b/packages/editor-ui/src/components/FixedCollectionParameter.vue @@ -254,6 +254,7 @@ export default defineComponent({ const parameterData = { name: this.getPropertyPath(optionName), value: this.mutableValues[optionName], + type: 'optionsOrderChanged', }; this.$emit('valueChanged', parameterData); @@ -270,6 +271,7 @@ export default defineComponent({ const parameterData = { name: this.getPropertyPath(optionName), value: this.mutableValues[optionName], + type: 'optionsOrderChanged', }; this.$emit('valueChanged', parameterData); diff --git a/packages/editor-ui/src/components/InlineExpressionEditor/InlineExpressionEditorOutput.vue b/packages/editor-ui/src/components/InlineExpressionEditor/InlineExpressionEditorOutput.vue index 4c2bda319505a..ba45d6d1bfaa4 100644 --- a/packages/editor-ui/src/components/InlineExpressionEditor/InlineExpressionEditorOutput.vue +++ b/packages/editor-ui/src/components/InlineExpressionEditor/InlineExpressionEditorOutput.vue @@ -11,8 +11,8 @@ import InlineExpressionTip from './InlineExpressionTip.vue'; interface InlineExpressionEditorOutputProps { segments: Segment[]; - unresolvedExpression: string; hoveringItemNumber: number; + unresolvedExpression?: string; editorState?: EditorState; selection?: SelectionRange; isReadOnly?: boolean; @@ -26,6 +26,7 @@ const props = withDefaults(defineProps(), { noInputData: false, editorState: undefined, selection: undefined, + unresolvedExpression: undefined, }); const i18n = useI18n(); diff --git a/packages/editor-ui/src/components/Node.vue b/packages/editor-ui/src/components/Node.vue index c3db780cb43f6..246373600002e 100644 --- a/packages/editor-ui/src/components/Node.vue +++ b/packages/editor-ui/src/components/Node.vue @@ -1294,10 +1294,10 @@ export default defineComponent({ &.error { path { - fill: var(--node-error-output-color); + fill: var(--color-node-error-output-text-color); } rect { - stroke: var(--node-error-output-color); + stroke: var(--color-node-error-output-text-color); } } @@ -1436,7 +1436,7 @@ export default defineComponent({ } .node-output-endpoint-label.node-connection-category-error { - color: var(--node-error-output-color); + color: var(--color-node-error-output-text-color); } .node-output-endpoint-label { diff --git a/packages/editor-ui/src/components/Node/NodeCreator/Panel/SearchBar.vue b/packages/editor-ui/src/components/Node/NodeCreator/Panel/SearchBar.vue index 6f2bc8637ea14..eb43450826716 100644 --- a/packages/editor-ui/src/components/Node/NodeCreator/Panel/SearchBar.vue +++ b/packages/editor-ui/src/components/Node/NodeCreator/Panel/SearchBar.vue @@ -130,7 +130,7 @@ defineExpose({ } .clear { - background-color: $node-creator-search-clear-color; + background-color: transparent; padding: 0; border: none; cursor: pointer; diff --git a/packages/editor-ui/src/components/Node/NodeCreator/__tests__/useActions.test.ts b/packages/editor-ui/src/components/Node/NodeCreator/__tests__/useActions.test.ts index d041911d6743b..14a03254e164d 100644 --- a/packages/editor-ui/src/components/Node/NodeCreator/__tests__/useActions.test.ts +++ b/packages/editor-ui/src/components/Node/NodeCreator/__tests__/useActions.test.ts @@ -1,6 +1,7 @@ import { setActivePinia } from 'pinia'; import { createTestingPinia } from '@pinia/testing'; import { useNodeCreatorStore } from '@/stores/nodeCreator.store'; +import { useNodeTypesStore } from '@/stores/nodeTypes.store'; import { useWorkflowsStore } from '@/stores/workflows.store'; import { useActions } from '../composables/useActions'; import { @@ -9,8 +10,10 @@ import { NODE_CREATOR_OPEN_SOURCES, NO_OP_NODE_TYPE, SCHEDULE_TRIGGER_NODE_TYPE, + SLACK_NODE_TYPE, SPLIT_IN_BATCHES_NODE_TYPE, TRIGGER_NODE_CREATOR_VIEW, + WEBHOOK_NODE_TYPE, } from '@/constants'; describe('useActions', () => { @@ -89,5 +92,85 @@ describe('useActions', () => { ], }); }); + + test('should connect node to schedule trigger when adding them together', () => { + const workflowsStore = useWorkflowsStore(); + const nodeCreatorStore = useNodeCreatorStore(); + const nodeTypesStore = useNodeTypesStore(); + + vi.spyOn(workflowsStore, 'workflowTriggerNodes', 'get').mockReturnValue([ + { type: SCHEDULE_TRIGGER_NODE_TYPE } as never, + ]); + vi.spyOn(nodeCreatorStore, 'openSource', 'get').mockReturnValue( + NODE_CREATOR_OPEN_SOURCES.ADD_NODE_BUTTON, + ); + vi.spyOn(nodeCreatorStore, 'selectedView', 'get').mockReturnValue(TRIGGER_NODE_CREATOR_VIEW); + nodeTypesStore.nodeTypes = { + [SCHEDULE_TRIGGER_NODE_TYPE]: { + 1: { + name: SCHEDULE_TRIGGER_NODE_TYPE, + displayName: 'Schedule Trigger', + group: ['trigger'], + version: 1, + defaults: {}, + inputs: [], + outputs: [], + properties: [], + description: '', + }, + }, + }; + const { getAddedNodesAndConnections } = useActions(); + + expect( + getAddedNodesAndConnections([ + { type: SCHEDULE_TRIGGER_NODE_TYPE, openDetail: true }, + { type: SLACK_NODE_TYPE }, + ]), + ).toEqual({ + connections: [{ from: { nodeIndex: 0 }, to: { nodeIndex: 1 } }], + nodes: [{ type: SCHEDULE_TRIGGER_NODE_TYPE, openDetail: true }, { type: SLACK_NODE_TYPE }], + }); + }); + + test('should connect node to webhook trigger when adding them together', () => { + const workflowsStore = useWorkflowsStore(); + const nodeCreatorStore = useNodeCreatorStore(); + const nodeTypesStore = useNodeTypesStore(); + + vi.spyOn(workflowsStore, 'workflowTriggerNodes', 'get').mockReturnValue([ + { type: SCHEDULE_TRIGGER_NODE_TYPE } as never, + ]); + vi.spyOn(nodeCreatorStore, 'openSource', 'get').mockReturnValue( + NODE_CREATOR_OPEN_SOURCES.ADD_NODE_BUTTON, + ); + vi.spyOn(nodeCreatorStore, 'selectedView', 'get').mockReturnValue(TRIGGER_NODE_CREATOR_VIEW); + nodeTypesStore.nodeTypes = { + [WEBHOOK_NODE_TYPE]: { + 1: { + name: WEBHOOK_NODE_TYPE, + displayName: 'Webhook', + group: ['trigger'], + version: 1, + defaults: {}, + inputs: [], + outputs: [], + properties: [], + description: '', + }, + }, + }; + const { getAddedNodesAndConnections } = useActions(); + + expect( + getAddedNodesAndConnections([ + { type: WEBHOOK_NODE_TYPE, openDetail: true }, + { type: SLACK_NODE_TYPE }, + ]), + ).toEqual({ + connections: [{ from: { nodeIndex: 0 }, to: { nodeIndex: 1 } }], + nodes: [{ type: WEBHOOK_NODE_TYPE, openDetail: true }, { type: SLACK_NODE_TYPE }], + }); + }); }); }); diff --git a/packages/editor-ui/src/components/Node/NodeCreator/composables/useActions.ts b/packages/editor-ui/src/components/Node/NodeCreator/composables/useActions.ts index 0e1b15462795b..3a49ff03e149c 100644 --- a/packages/editor-ui/src/components/Node/NodeCreator/composables/useActions.ts +++ b/packages/editor-ui/src/components/Node/NodeCreator/composables/useActions.ts @@ -163,6 +163,18 @@ export const useActions = () => { }; } + /** + * Checks if added nodes contain trigger followed by another node + * In this case, we should connect the trigger with the following node + */ + function shouldConnectWithExistingTrigger(addedNodes: AddedNode[]): boolean { + if (addedNodes.length === 2) { + const isTriggerNode = useNodeTypesStore().isTriggerNode(addedNodes[0].type); + return isTriggerNode; + } + return false; + } + function shouldPrependManualTrigger(addedNodes: AddedNode[]): boolean { const { selectedView, openSource } = useNodeCreatorStore(); const { workflowTriggerNodes } = useWorkflowsStore(); @@ -228,6 +240,11 @@ export const useActions = () => { from: { nodeIndex: 0 }, to: { nodeIndex: 1 }, }); + } else if (shouldConnectWithExistingTrigger(addedNodes)) { + connections.push({ + from: { nodeIndex: 0 }, + to: { nodeIndex: 1 }, + }); } addedNodes.forEach((node, index) => { diff --git a/packages/editor-ui/src/components/NodeExecuteButton.vue b/packages/editor-ui/src/components/NodeExecuteButton.vue index 1fcd5cf6efe76..af90ba2e0521a 100644 --- a/packages/editor-ui/src/components/NodeExecuteButton.vue +++ b/packages/editor-ui/src/components/NodeExecuteButton.vue @@ -12,7 +12,7 @@ :label="buttonLabel" :type="type" :size="size" - :icon="!isListeningForEvents && !hideIcon && 'flask'" + :icon="!isListeningForEvents && !hideIcon ? 'flask' : undefined" :transparent-background="transparent" :title="!isTriggerNode ? $locale.baseText('ndv.execute.testNode.description') : ''" @click="onClick" diff --git a/packages/editor-ui/src/components/NodeSettings.vue b/packages/editor-ui/src/components/NodeSettings.vue index bd225c178545a..3b464f9dc9bb3 100644 --- a/packages/editor-ui/src/components/NodeSettings.vue +++ b/packages/editor-ui/src/components/NodeSettings.vue @@ -200,6 +200,7 @@ import { CUSTOM_NODES_DOCS_URL, MAIN_NODE_PANEL_WIDTH, IMPORT_CURL_MODAL_KEY, + SHOULD_CLEAR_NODE_OUTPUTS, } from '@/constants'; import NodeTitle from '@/components/NodeTitle.vue'; @@ -223,6 +224,7 @@ import { useCredentialsStore } from '@/stores/credentials.store'; import type { EventBus } from 'n8n-design-system'; import { useExternalHooks } from '@/composables/useExternalHooks'; import { useNodeHelpers } from '@/composables/useNodeHelpers'; +import { useToast } from '@/composables/useToast'; export default defineComponent({ name: 'NodeSettings', @@ -238,10 +240,12 @@ export default defineComponent({ setup() { const nodeHelpers = useNodeHelpers(); const externalHooks = useExternalHooks(); + const { showMessage } = useToast(); return { externalHooks, nodeHelpers, + showMessage, }; }, computed: { @@ -853,6 +857,20 @@ export default defineComponent({ return; } + if ( + parameterData.type && + this.workflowsStore.nodeHasOutputConnection(node.name) && + SHOULD_CLEAR_NODE_OUTPUTS[nodeType.name]?.eventTypes.includes(parameterData.type) && + SHOULD_CLEAR_NODE_OUTPUTS[nodeType.name]?.parameterPaths.includes(parameterData.name) + ) { + this.workflowsStore.removeAllNodeConnection(node, { preserveInputConnections: true }); + this.showMessage({ + type: 'warning', + title: this.$locale.baseText('nodeSettings.outputCleared.title'), + message: this.$locale.baseText('nodeSettings.outputCleared.message'), + }); + } + // Get only the parameters which are different to the defaults let nodeParameters = NodeHelpers.getNodeParameters( nodeType.properties, diff --git a/packages/editor-ui/src/components/RunData.vue b/packages/editor-ui/src/components/RunData.vue index db685a2e21022..5adafcd71e944 100644 --- a/packages/editor-ui/src/components/RunData.vue +++ b/packages/editor-ui/src/components/RunData.vue @@ -217,8 +217,8 @@
@@ -725,45 +725,6 @@ export default defineComponent({ search: '', }; }, - mounted() { - this.init(); - - if (!this.isPaneTypeInput) { - this.showPinDataDiscoveryTooltip(this.jsonData); - } - this.ndvStore.setNDVBranchIndex({ - pane: this.paneType as 'input' | 'output', - branchIndex: this.currentOutputIndex, - }); - - if (this.paneType === 'output') { - this.setDisplayMode(); - this.activatePane(); - } - - if (this.hasRunError) { - const error = this.workflowRunData?.[this.node.name]?.[this.runIndex]?.error; - const errorsToTrack = ['unknown error']; - - if (error && errorsToTrack.some((e) => error.message.toLowerCase().includes(e))) { - this.$telemetry.track( - `User encountered an error: "${error.message}"`, - { - node: this.node.type, - errorMessage: error.message, - nodeVersion: this.node.typeVersion, - n8nVersion: this.rootStore.versionCli, - }, - { - withPostHog: true, - }, - ); - } - } - }, - beforeUnmount() { - this.hidePinDataDiscoveryTooltip(); - }, computed: { ...mapStores( useNodeTypesStore, @@ -803,21 +764,14 @@ export default defineComponent({ return this.nodeTypesStore.isTriggerNode(this.node.type); }, canPinData(): boolean { - // Only "main" inputs can pin data - if (this.node === null) { return false; } - const workflow = this.workflowsStore.getCurrentWorkflow(); - const workflowNode = workflow.getNode(this.node.name); - const inputs = NodeHelpers.getNodeInputs(workflow, workflowNode!, this.nodeType!); - const inputNames = NodeHelpers.getConnectionTypes(inputs); - - const nonMainInputs = !!inputNames.find((inputName) => inputName !== NodeConnectionType.Main); + const canPinNode = usePinnedData(this.node).canPinNode(false); return ( - !nonMainInputs && + canPinNode && !this.isPaneTypeInput && this.pinnedData.isValidNodeType.value && !(this.binaryData && this.binaryData.length > 0) @@ -1035,6 +989,87 @@ export default defineComponent({ return this.hasNodeRun && !this.inputData.length && this.search; }, }, + watch: { + node(newNode: INodeUi, prevNode: INodeUi) { + if (newNode.id === prevNode.id) return; + this.init(); + }, + hasNodeRun() { + if (this.paneType === 'output') this.setDisplayMode(); + }, + inputDataPage: { + handler(data: INodeExecutionData[]) { + if (this.paneType && data) { + this.ndvStore.setNDVPanelDataIsEmpty({ + panel: this.paneType as 'input' | 'output', + isEmpty: data.every((item) => isEmpty(item.json)), + }); + } + }, + immediate: true, + deep: true, + }, + jsonData(data: IDataObject[], prevData: IDataObject[]) { + if (isEqual(data, prevData)) return; + this.refreshDataSize(); + this.showPinDataDiscoveryTooltip(data); + }, + binaryData(newData: IBinaryKeyData[], prevData: IBinaryKeyData[]) { + if (newData.length && !prevData.length && this.displayMode !== 'binary') { + this.switchToBinary(); + } else if (!newData.length && this.displayMode === 'binary') { + this.onDisplayModeChange('table'); + } + }, + currentOutputIndex(branchIndex: number) { + this.ndvStore.setNDVBranchIndex({ + pane: this.paneType as 'input' | 'output', + branchIndex, + }); + }, + search(newSearch: string) { + this.$emit('search', newSearch); + }, + }, + mounted() { + this.init(); + + if (!this.isPaneTypeInput) { + this.showPinDataDiscoveryTooltip(this.jsonData); + } + this.ndvStore.setNDVBranchIndex({ + pane: this.paneType as 'input' | 'output', + branchIndex: this.currentOutputIndex, + }); + + if (this.paneType === 'output') { + this.setDisplayMode(); + this.activatePane(); + } + + if (this.hasRunError) { + const error = this.workflowRunData?.[this.node.name]?.[this.runIndex]?.error; + const errorsToTrack = ['unknown error']; + + if (error && errorsToTrack.some((e) => error.message.toLowerCase().includes(e))) { + this.$telemetry.track( + `User encountered an error: "${error.message}"`, + { + node: this.node.type, + errorMessage: error.message, + nodeVersion: this.node.typeVersion, + n8nVersion: this.rootStore.versionCli, + }, + { + withPostHog: true, + }, + ); + } + } + }, + beforeUnmount() { + this.hidePinDataDiscoveryTooltip(); + }, methods: { getResolvedNodeOutputs() { if (this.node && this.nodeType) { @@ -1500,48 +1535,6 @@ export default defineComponent({ document.dispatchEvent(new KeyboardEvent('keyup', { key: '/' })); }, }, - watch: { - node(newNode: INodeUi, prevNode: INodeUi) { - if (newNode.id === prevNode.id) return; - this.init(); - }, - hasNodeRun() { - if (this.paneType === 'output') this.setDisplayMode(); - }, - inputDataPage: { - handler(data: INodeExecutionData[]) { - if (this.paneType && data) { - this.ndvStore.setNDVPanelDataIsEmpty({ - panel: this.paneType as 'input' | 'output', - isEmpty: data.every((item) => isEmpty(item.json)), - }); - } - }, - immediate: true, - deep: true, - }, - jsonData(data: IDataObject[], prevData: IDataObject[]) { - if (isEqual(data, prevData)) return; - this.refreshDataSize(); - this.showPinDataDiscoveryTooltip(data); - }, - binaryData(newData: IBinaryKeyData[], prevData: IBinaryKeyData[]) { - if (newData.length && !prevData.length && this.displayMode !== 'binary') { - this.switchToBinary(); - } else if (!newData.length && this.displayMode === 'binary') { - this.onDisplayModeChange('table'); - } - }, - currentOutputIndex(branchIndex: number) { - this.ndvStore.setNDVBranchIndex({ - pane: this.paneType as 'input' | 'output', - branchIndex, - }); - }, - search(newSearch: string) { - this.$emit('search', newSearch); - }, - }, }); diff --git a/packages/editor-ui/src/components/TagsDropdown.vue b/packages/editor-ui/src/components/TagsDropdown.vue index 242709e0a72e1..f0fdf49e4e743 100644 --- a/packages/editor-ui/src/components/TagsDropdown.vue +++ b/packages/editor-ui/src/components/TagsDropdown.vue @@ -124,9 +124,7 @@ export default defineComponent({ }); const options = computed(() => { - return allTags.value.filter( - (tag: ITag) => tag && tag.name.toLowerCase().includes(filter.value.toLowerCase()), - ); + return allTags.value.filter((tag: ITag) => tag && tag.name.includes(filter.value)); }); const appliedTags = computed(() => { @@ -182,7 +180,7 @@ export default defineComponent({ } function filterOptions(value = '') { - filter.value = value.trim(); + filter.value = value; void nextTick(() => focusFirstOption()); } diff --git a/packages/editor-ui/src/composables/useContextMenu.ts b/packages/editor-ui/src/composables/useContextMenu.ts index ebf78bdb67d24..b4aa25b9e3ef6 100644 --- a/packages/editor-ui/src/composables/useContextMenu.ts +++ b/packages/editor-ui/src/composables/useContextMenu.ts @@ -1,20 +1,15 @@ import type { XYPosition } from '@/Interface'; -import { - NOT_DUPLICATABE_NODE_TYPES, - PIN_DATA_NODE_TYPES_DENYLIST, - STICKY_NODE_TYPE, -} from '@/constants'; +import { NOT_DUPLICATABE_NODE_TYPES, STICKY_NODE_TYPE } from '@/constants'; import { useNodeTypesStore } from '@/stores/nodeTypes.store'; import { useSourceControlStore } from '@/stores/sourceControl.store'; import { useUIStore } from '@/stores/ui.store'; import { useWorkflowsStore } from '@/stores/workflows.store'; import type { IActionDropdownItem } from 'n8n-design-system/src/components/N8nActionDropdown/ActionDropdown.vue'; -import { NodeHelpers, NodeConnectionType } from 'n8n-workflow'; import type { INode, INodeTypeDescription } from 'n8n-workflow'; import { computed, ref, watch } from 'vue'; import { getMousePosition } from '../utils/nodeViewUtils'; import { useI18n } from './useI18n'; -import { useDataSchema } from './useDataSchema'; +import { usePinnedData } from './usePinnedData'; export type ContextMenuTarget = | { source: 'canvas' } @@ -47,7 +42,7 @@ export const useContextMenu = (onAction: ContextMenuActionCallback = () => {}) = const nodeTypesStore = useNodeTypesStore(); const workflowsStore = useWorkflowsStore(); const sourceControlStore = useSourceControlStore(); - const { getInputDataWithPinned } = useDataSchema(); + const i18n = useI18n(); const isReadOnly = computed( @@ -83,13 +78,6 @@ export const useContextMenu = (onAction: ContextMenuActionCallback = () => {}) = return canAddNodeOfType(nodeType); }; - const canPinNode = (node: INode): boolean => { - const nodeType = nodeTypesStore.getNodeType(node.type, node.typeVersion); - const dataToPin = getInputDataWithPinned(node); - if (!nodeType || dataToPin.length === 0) return false; - return nodeType.outputs.length === 1 && !PIN_DATA_NODE_TYPES_DENYLIST.includes(node.type); - }; - const hasPinData = (node: INode): boolean => { return !!workflowsStore.pinDataByNodeName(node.name); }; @@ -159,16 +147,6 @@ export const useContextMenu = (onAction: ContextMenuActionCallback = () => {}) = ...selectionActions, ]; } else { - const nonMainInputs = (node: INode) => { - const workflow = workflowsStore.getCurrentWorkflow(); - const workflowNode = workflow.getNode(node.name); - const nodeType = nodeTypesStore.getNodeType(node.type, node.typeVersion); - const inputs = NodeHelpers.getNodeInputs(workflow, workflowNode!, nodeType!); - const inputNames = NodeHelpers.getConnectionTypes(inputs); - - return !!inputNames.find((inputName) => inputName !== NodeConnectionType.Main); - }; - const menuActions: IActionDropdownItem[] = [ !onlyStickies && { id: 'toggle_activation', @@ -184,7 +162,7 @@ export const useContextMenu = (onAction: ContextMenuActionCallback = () => {}) = ? i18n.baseText('contextMenu.unpin', i18nOptions) : i18n.baseText('contextMenu.pin', i18nOptions), shortcut: { keys: ['p'] }, - disabled: nodes.some(nonMainInputs) || isReadOnly.value || !nodes.every(canPinNode), + disabled: isReadOnly.value || !nodes.every((n) => usePinnedData(n).canPinNode(true)), }, { id: 'copy', diff --git a/packages/editor-ui/src/composables/usePinnedData.ts b/packages/editor-ui/src/composables/usePinnedData.ts index a35fdaa9ba89f..1dce344656123 100644 --- a/packages/editor-ui/src/composables/usePinnedData.ts +++ b/packages/editor-ui/src/composables/usePinnedData.ts @@ -1,7 +1,7 @@ import { useToast } from '@/composables/useToast'; import { useI18n } from '@/composables/useI18n'; import type { INodeExecutionData, IPinData } from 'n8n-workflow'; -import { jsonParse, jsonStringify } from 'n8n-workflow'; +import { jsonParse, jsonStringify, NodeConnectionType, NodeHelpers } from 'n8n-workflow'; import { MAX_EXPECTED_REQUEST_SIZE, MAX_PINNED_DATA_SIZE, @@ -18,6 +18,8 @@ import { computed, unref } from 'vue'; import { useRootStore } from '@/stores/n8nRoot.store'; import { storeToRefs } from 'pinia'; import { useNodeType } from '@/composables/useNodeType'; +import { useDataSchema } from './useDataSchema'; +import { useNodeTypesStore } from '@/stores/nodeTypes.store'; export type PinDataSource = | 'pin-icon-click' @@ -47,6 +49,7 @@ export function usePinnedData( const i18n = useI18n(); const telemetry = useTelemetry(); const externalHooks = useExternalHooks(); + const { getInputDataWithPinned } = useDataSchema(); const { pushRef } = storeToRefs(rootStore); const { isSubNodeType, isMultipleOutputsNodeType } = useNodeType({ @@ -73,6 +76,26 @@ export function usePinnedData( ); }); + function canPinNode(checkDataEmpty = false) { + const targetNode = unref(node); + if (targetNode === null) return false; + + const nodeType = useNodeTypesStore().getNodeType(targetNode.type, targetNode.typeVersion); + const dataToPin = getInputDataWithPinned(targetNode); + + if (!nodeType || (checkDataEmpty && dataToPin.length === 0)) return false; + + const workflow = workflowsStore.getCurrentWorkflow(); + const outputs = NodeHelpers.getNodeOutputs(workflow, targetNode, nodeType); + const mainOutputs = outputs.filter((output) => + typeof output === 'string' + ? output === NodeConnectionType.Main + : output.type === NodeConnectionType.Main, + ); + + return mainOutputs.length === 1 && !PIN_DATA_NODE_TYPES_DENYLIST.includes(targetNode.type); + } + function isValidJSON(data: string): boolean { try { JSON.parse(data); @@ -246,6 +269,7 @@ export function usePinnedData( data, hasData, isValidNodeType, + canPinNode, setData, onSetDataSuccess, onSetDataError, diff --git a/packages/editor-ui/src/constants.ts b/packages/editor-ui/src/constants.ts index 44dbab412bf82..728ee7588c41e 100644 --- a/packages/editor-ui/src/constants.ts +++ b/packages/editor-ui/src/constants.ts @@ -653,6 +653,20 @@ export const MFA_AUTHENTICATION_RECOVERY_CODE_INPUT_MAX_LENGTH = 36; export const NODE_TYPES_EXCLUDED_FROM_OUTPUT_NAME_APPEND = [FILTER_NODE_TYPE, SWITCH_NODE_TYPE]; +type ClearOutgoingConnectonsEvents = { + [nodeName: string]: { + parameterPaths: string[]; + eventTypes: string[]; + }; +}; + +export const SHOULD_CLEAR_NODE_OUTPUTS: ClearOutgoingConnectonsEvents = { + [SWITCH_NODE_TYPE]: { + parameterPaths: ['parameters.rules.values'], + eventTypes: ['optionsOrderChanged'], + }, +}; + export const ALLOWED_HTML_ATTRIBUTES = ['href', 'name', 'target', 'title', 'class', 'id', 'style']; export const ALLOWED_HTML_TAGS = [ diff --git a/packages/editor-ui/src/mixins/nodeBase.ts b/packages/editor-ui/src/mixins/nodeBase.ts index d80d813519af0..2852021bb0c31 100644 --- a/packages/editor-ui/src/mixins/nodeBase.ts +++ b/packages/editor-ui/src/mixins/nodeBase.ts @@ -600,7 +600,7 @@ export const nodeBase = defineComponent({ nodeTypeData, this.__getEndpointColor(NodeConnectionType.Main), ), - fill: 'var(--node-error-output-color)', + fill: 'var(--color-danger)', }, cssClass: `dot-${type}-endpoint`, }; diff --git a/packages/editor-ui/src/n8n-theme.scss b/packages/editor-ui/src/n8n-theme.scss index 93061ca1296d8..d3eed63484613 100644 --- a/packages/editor-ui/src/n8n-theme.scss +++ b/packages/editor-ui/src/n8n-theme.scss @@ -170,7 +170,6 @@ var(--node-type-ai_vectorStore-color-s), var(--node-type-background-l) ); - --node-error-output-color: #991818; --chat--spacing: var(--spacing-s); diff --git a/packages/editor-ui/src/plugins/codemirror/resolvableHighlighter.ts b/packages/editor-ui/src/plugins/codemirror/resolvableHighlighter.ts index 4ad38305bcb00..6bbe3dcf4233a 100644 --- a/packages/editor-ui/src/plugins/codemirror/resolvableHighlighter.ts +++ b/packages/editor-ui/src/plugins/codemirror/resolvableHighlighter.ts @@ -15,7 +15,6 @@ const cssClasses = { validResolvable: 'cm-valid-resolvable', invalidResolvable: 'cm-invalid-resolvable', pendingResolvable: 'cm-pending-resolvable', - brokenResolvable: 'cm-broken-resolvable', plaintext: 'cm-plaintext', }; @@ -129,10 +128,6 @@ const resolvableStyle = syntaxHighlighting( tag: tags.content, class: cssClasses.plaintext, }, - { - tag: tags.className, - class: cssClasses.brokenResolvable, - }, /** * CSS classes for valid and invalid resolvables * dynamically applied based on state fields diff --git a/packages/editor-ui/src/plugins/i18n/locales/en.json b/packages/editor-ui/src/plugins/i18n/locales/en.json index ad5d7e0ade023..1c07021e8f9db 100644 --- a/packages/editor-ui/src/plugins/i18n/locales/en.json +++ b/packages/editor-ui/src/plugins/i18n/locales/en.json @@ -1093,6 +1093,8 @@ "nodeSettings.latest": "Latest", "nodeSettings.deprecated": "Deprecated", "nodeSettings.latestVersion": "Latest version: {version}", + "nodeSettings.outputCleared.title": "Parameters changed", + "nodeSettings.outputCleared.message": "Order of parameters changed, outgoing connections were cleared", "nodeSettings.nodeVersion": "{node} node version {version}", "nodeView.addNode": "Add node", "nodeView.openNodesPanel": "Open nodes panel", diff --git a/packages/editor-ui/src/stores/workflows.store.ts b/packages/editor-ui/src/stores/workflows.store.ts index 6e19b8d8f331a..8eed4d08fc3ff 100644 --- a/packages/editor-ui/src/stores/workflows.store.ts +++ b/packages/editor-ui/src/stores/workflows.store.ts @@ -200,6 +200,12 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, { return {}; }; }, + nodeHasOutputConnection() { + return (nodeName: string): boolean => { + if (this.workflow.connections.hasOwnProperty(nodeName)) return true; + return false; + }; + }, isNodeInOutgoingNodeConnections() { return (firstNode: string, secondNode: string): boolean => { const firstNodeConnections = this.outgoingConnectionsByNodeName(firstNode); @@ -841,15 +847,19 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, { this.workflow.connections = {}; }, - removeAllNodeConnection(node: INodeUi): void { + removeAllNodeConnection( + node: INodeUi, + { preserveInputConnections = false, preserveOutputConnections = false } = {}, + ): void { const uiStore = useUIStore(); uiStore.stateIsDirty = true; // Remove all source connections - if (this.workflow.connections.hasOwnProperty(node.name)) { + if (!preserveOutputConnections && this.workflow.connections.hasOwnProperty(node.name)) { delete this.workflow.connections[node.name]; } // Remove all destination connections + if (preserveInputConnections) return; const indexesToRemove = []; let sourceNode: string, type: string, diff --git a/packages/editor-ui/src/utils/nodeViewUtils.ts b/packages/editor-ui/src/utils/nodeViewUtils.ts index 15fc1e7384654..bafa67891200b 100644 --- a/packages/editor-ui/src/utils/nodeViewUtils.ts +++ b/packages/editor-ui/src/utils/nodeViewUtils.ts @@ -113,7 +113,7 @@ export const CONNECTOR_PAINT_STYLE_DATA: PaintStyle = { export const getConnectorColor = (type: ConnectionTypes, category?: string): string => { if (category === 'error') { - return '--node-error-output-color'; + return '--color-node-error-output-text-color'; } if (type === NodeConnectionType.Main) { diff --git a/packages/editor-ui/src/views/SetupWorkflowFromTemplateView/__tests__/setupTemplate.store.testData.ts b/packages/editor-ui/src/views/SetupWorkflowFromTemplateView/__tests__/setupTemplate.store.testData.ts index aafff75971204..0696ed2b7c1fb 100644 --- a/packages/editor-ui/src/views/SetupWorkflowFromTemplateView/__tests__/setupTemplate.store.testData.ts +++ b/packages/editor-ui/src/views/SetupWorkflowFromTemplateView/__tests__/setupTemplate.store.testData.ts @@ -51,7 +51,6 @@ export const newCredential = ( updatedAt: faker.date.past().toISOString(), id: faker.string.alphanumeric({ length: 16 }), name: faker.commerce.productName(), - nodesAccess: [], ...opts, }); @@ -61,16 +60,6 @@ export const credentialsTelegram1: ICredentialsResponse = { id: 'YaSKdvEcT1TSFrrr1', name: 'Telegram account', type: 'telegramApi', - nodesAccess: [ - { - nodeType: 'n8n-nodes-base.telegram', - date: new Date('2023-11-23T14:26:07.962Z'), - }, - { - nodeType: 'n8n-nodes-base.telegramTrigger', - date: new Date('2023-11-23T14:26:07.962Z'), - }, - ], ownedBy: { id: '713ef3e7-9e65-4b0a-893c-8a653cbb2c4f', email: 'user@n8n.io', @@ -86,16 +75,6 @@ export const credentialsTelegram2: ICredentialsResponse = { id: 'YaSKdvEcT1TSFrrr2', name: 'Telegram account', type: 'telegramApi', - nodesAccess: [ - { - nodeType: 'n8n-nodes-base.telegram', - date: new Date('2023-11-23T14:26:07.962Z'), - }, - { - nodeType: 'n8n-nodes-base.telegramTrigger', - date: new Date('2023-11-23T14:26:07.962Z'), - }, - ], ownedBy: { id: '713ef3e7-9e65-4b0a-893c-8a653cbb2c4f', email: 'user@n8n.io', diff --git a/packages/nodes-base/nodes/MySql/MySql.node.ts b/packages/nodes-base/nodes/MySql/MySql.node.ts index bb82d3e4caf04..aad3df9bdce8a 100644 --- a/packages/nodes-base/nodes/MySql/MySql.node.ts +++ b/packages/nodes-base/nodes/MySql/MySql.node.ts @@ -11,7 +11,7 @@ export class MySql extends VersionedNodeType { name: 'mySql', icon: 'file:mysql.svg', group: ['input'], - defaultVersion: 2.2, + defaultVersion: 2.3, description: 'Get, add and update data in MySQL', parameterPane: 'wide', }; @@ -21,6 +21,7 @@ export class MySql extends VersionedNodeType { 2: new MySqlV2(baseDescription), 2.1: new MySqlV2(baseDescription), 2.2: new MySqlV2(baseDescription), + 2.3: new MySqlV2(baseDescription), }; super(nodeVersions, baseDescription); diff --git a/packages/nodes-base/nodes/MySql/test/v2/operations.test.ts b/packages/nodes-base/nodes/MySql/test/v2/operations.test.ts index 599ff8f9ef4b7..359c21c465c2f 100644 --- a/packages/nodes-base/nodes/MySql/test/v2/operations.test.ts +++ b/packages/nodes-base/nodes/MySql/test/v2/operations.test.ts @@ -254,6 +254,47 @@ describe('Test MySql V2, operations', () => { ); expect(connectionQuerySpy).toBeCalledWith('select * from `test_table`'); }); + it('executeQuery, should parse numbers', async () => { + const nodeParameters: IDataObject = { + operation: 'executeQuery', + query: 'SELECT * FROM users LIMIT $1, $2', + options: { + queryBatching: 'independently', + queryReplacement: '2, 5', + nodeVersion: 2.3, + }, + }; + + const nodeOptions = nodeParameters.options as IDataObject; + + const fakeConnectionCopy = { ...fakeConnection }; + + fakeConnectionCopy.query = jest.fn(async (query?: string) => { + return [{ query }]; + }); + const pool = createFakePool(fakeConnectionCopy); + + const connectionQuerySpy = jest.spyOn(fakeConnectionCopy, 'query'); + + const fakeExecuteFunction = createMockExecuteFunction(nodeParameters, mySqlMockNode); + + const runQueries: QueryRunner = configureQueryRunner.call( + fakeExecuteFunction, + nodeOptions, + pool, + ); + + const result = await executeQuery.execute.call( + fakeExecuteFunction, + emptyInputItems, + runQueries, + nodeOptions, + ); + + expect(result).toBeDefined(); + + expect(connectionQuerySpy).toBeCalledWith('SELECT * FROM users LIMIT 2, 5'); + }); it('select, should call runQueries with', async () => { const nodeParameters: IDataObject = { diff --git a/packages/nodes-base/nodes/MySql/v2/actions/database/executeQuery.operation.ts b/packages/nodes-base/nodes/MySql/v2/actions/database/executeQuery.operation.ts index ee173f45aa7ff..9af1b9eaa3872 100644 --- a/packages/nodes-base/nodes/MySql/v2/actions/database/executeQuery.operation.ts +++ b/packages/nodes-base/nodes/MySql/v2/actions/database/executeQuery.operation.ts @@ -81,6 +81,13 @@ export async function execute( const preparedQuery = prepareQueryAndReplacements(rawQuery, values); + if ((nodeOptions.nodeVersion as number) >= 2.3) { + const parsedNumbers = preparedQuery.values.map((value) => { + return Number(value) ? Number(value) : value; + }); + preparedQuery.values = parsedNumbers; + } + queries.push(preparedQuery); } diff --git a/packages/nodes-base/nodes/MySql/v2/actions/versionDescription.ts b/packages/nodes-base/nodes/MySql/v2/actions/versionDescription.ts index 366e8fcaa8c8f..c1802bd063663 100644 --- a/packages/nodes-base/nodes/MySql/v2/actions/versionDescription.ts +++ b/packages/nodes-base/nodes/MySql/v2/actions/versionDescription.ts @@ -8,7 +8,7 @@ export const versionDescription: INodeTypeDescription = { name: 'mySql', icon: 'file:mysql.svg', group: ['input'], - version: [2, 2.1, 2.2], + version: [2, 2.1, 2.2, 2.3], subtitle: '={{ $parameter["operation"] }}', description: 'Get, add and update data in MySQL', defaults: { diff --git a/packages/workflow/package.json b/packages/workflow/package.json index 448a8adbe95b5..3ea3049aeb5f1 100644 --- a/packages/workflow/package.json +++ b/packages/workflow/package.json @@ -65,6 +65,7 @@ "recast": "0.21.5", "title-case": "3.0.3", "transliteration": "2.3.5", - "xml2js": "0.6.2" + "xml2js": "0.6.2", + "@langchain/core": "0.1.41" } } diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index 7a90966aca855..2c6f3741f5de4 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -19,6 +19,7 @@ import type { WorkflowHooks } from './WorkflowHooks'; import type { NodeOperationError } from './errors/node-operation.error'; import type { NodeApiError } from './errors/node-api.error'; import type { AxiosProxyConfig } from 'axios'; +import type { CallbackManager as CallbackManagerLC } from '@langchain/core/callbacks/manager'; export interface IAdditionalCredentialOptions { oauth2?: IOAuth2Options; @@ -93,18 +94,10 @@ export abstract class ICredentials { data: string | undefined; - nodesAccess: ICredentialNodeAccess[]; - - constructor( - nodeCredentials: INodeCredentialsDetails, - type: string, - nodesAccess: ICredentialNodeAccess[], - data?: string, - ) { + constructor(nodeCredentials: INodeCredentialsDetails, type: string, data?: string) { this.id = nodeCredentials.id ?? undefined; this.name = nodeCredentials.name; this.type = type; - this.nodesAccess = nodesAccess; this.data = data; } @@ -112,8 +105,6 @@ export abstract class ICredentials { abstract getDataToSave(): ICredentialsEncrypted; - abstract hasNodeAccess(nodeType: string): boolean; - abstract setData(data: ICredentialDataDecryptedObject): void; } @@ -124,19 +115,10 @@ export interface IUser { lastName: string; } -// Defines which nodes are allowed to access the credentials and -// when that access got granted from which user -export interface ICredentialNodeAccess { - nodeType: string; - user?: string; - date?: Date; -} - export interface ICredentialsDecrypted { id: string; name: string; type: string; - nodesAccess: ICredentialNodeAccess[]; data?: ICredentialDataDecryptedObject; ownedBy?: IUser; sharedWith?: IUser[]; @@ -146,7 +128,6 @@ export interface ICredentialsEncrypted { id?: string; name: string; type: string; - nodesAccess: ICredentialNodeAccess[]; data?: string; } @@ -345,7 +326,6 @@ export interface ICredentialData { id?: string; name: string; data: string; // Contains the access data as encrypted JSON string - nodesAccess: ICredentialNodeAccess[]; } // The encrypted credentials which the nodes can access @@ -863,6 +843,7 @@ export type IExecuteFunctions = ExecuteFunctions.GetNodeParameterFn & executeWorkflow( workflowInfo: IExecuteWorkflowInfo, inputData?: INodeExecutionData[], + parentCallbackManager?: CallbackManager, ): Promise; getInputConnectionData( inputName: ConnectionTypes, @@ -902,6 +883,8 @@ export type IExecuteFunctions = ExecuteFunctions.GetNodeParameterFn & getBinaryDataBuffer(itemIndex: number, propertyName: string): Promise; copyInputItems(items: INodeExecutionData[], properties: string[]): IDataObject[]; }; + + getParentCallbackManager(): CallbackManager | undefined; }; export interface IExecuteSingleFunctions extends BaseExecutionFunctions { @@ -2049,6 +2032,7 @@ export interface IWorkflowExecuteAdditionalData { loadedWorkflowData?: IWorkflowBase; loadedRunData?: any; parentWorkflowSettings?: IWorkflowSettings; + parentCallbackManager?: CallbackManager; }, ) => Promise; executionId?: string; @@ -2080,6 +2064,7 @@ export interface IWorkflowExecuteAdditionalData { nodeType?: string; }, ) => Promise; + parentCallbackManager?: CallbackManager; } export type WorkflowExecuteMode = @@ -2604,3 +2589,5 @@ export type BannerName = export type Functionality = 'regular' | 'configuration-node' | 'pairedItem'; export type Result = { ok: true; result: T } | { ok: false; error: E }; + +export type CallbackManager = CallbackManagerLC; diff --git a/packages/workflow/test/Helpers.ts b/packages/workflow/test/Helpers.ts index db017a4b55c88..8b07f3d6aa366 100644 --- a/packages/workflow/test/Helpers.ts +++ b/packages/workflow/test/Helpers.ts @@ -46,10 +46,6 @@ export interface INodeTypesObject { } export class Credentials extends ICredentials { - hasNodeAccess() { - return true; - } - setData(data: ICredentialDataDecryptedObject) { this.data = JSON.stringify(data); } @@ -71,7 +67,6 @@ export class Credentials extends ICredentials { name: this.name, type: this.type, data: this.data, - nodesAccess: this.nodesAccess, }; } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a7abbcb9eee3d..901856b834ce6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1069,8 +1069,8 @@ importers: specifier: ^1.0.0 version: 1.0.0 codemirror-lang-n8n-expression: - specifier: ^0.2.0 - version: 0.2.0(@codemirror/state@6.3.3)(@codemirror/view@6.22.3)(@lezer/common@1.1.0) + specifier: ^0.3.0 + version: 0.3.0(@codemirror/state@6.3.3)(@codemirror/view@6.22.3)(@lezer/common@1.1.0) dateformat: specifier: ^3.0.3 version: 3.0.3 @@ -1515,6 +1515,9 @@ importers: packages/workflow: dependencies: + '@langchain/core': + specifier: 0.1.41 + version: 0.1.41 '@n8n/tournament': specifier: 1.0.2 version: 1.0.2 @@ -8985,7 +8988,7 @@ packages: ts-dedent: 2.2.0 type-fest: 2.19.0 vue: 3.4.21(typescript@5.4.2) - vue-component-type-helpers: 2.0.7 + vue-component-type-helpers: 2.0.11 transitivePeerDependencies: - encoding - supports-color @@ -12510,8 +12513,8 @@ packages: '@lezer/lr': 1.2.3 dev: false - /codemirror-lang-n8n-expression@0.2.0(@codemirror/state@6.3.3)(@codemirror/view@6.22.3)(@lezer/common@1.1.0): - resolution: {integrity: sha512-kdlpzevdCpWcpbNcwES9YZy+rDFwWOdO6Z78SWxT6jMhCPmdHQmO+gJ39aXAXlUI7OGLfOBtg1/ONxPjRpEIYQ==} + /codemirror-lang-n8n-expression@0.3.0(@codemirror/state@6.3.3)(@codemirror/view@6.22.3)(@lezer/common@1.1.0): + resolution: {integrity: sha512-lY3qD+FB0/JCotK9hV40YhE+jC98iyC8L667pnguq7gxgIU3Kgy2RHy+PAxz/GpLR7BYtUMyVumZU7dJnjiXbw==} dependencies: '@codemirror/autocomplete': 6.11.1(@codemirror/language@6.9.3)(@codemirror/state@6.3.3)(@codemirror/view@6.22.3)(@lezer/common@1.1.0) '@codemirror/language': 6.9.3 @@ -12804,11 +12807,6 @@ packages: dependencies: safe-buffer: 5.2.1 - /content-type@1.0.4: - resolution: {integrity: sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==} - engines: {node: '>= 0.6'} - dev: false - /content-type@1.0.5: resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} engines: {node: '>= 0.6'} @@ -25663,8 +25661,8 @@ packages: resolution: {integrity: sha512-0vOfAtI67UjeO1G6UiX5Kd76CqaQ67wrRZiOe7UAb9Jm6GzlUr/fC7CV90XfwapJRjpCMaZFhv1V0ajWRmE9Dg==} dev: true - /vue-component-type-helpers@2.0.7: - resolution: {integrity: sha512-7e12Evdll7JcTIocojgnCgwocX4WzIYStGClBQ+QuWPinZo/vQolv2EMq4a3lg16TKfwWafLimG77bxb56UauA==} + /vue-component-type-helpers@2.0.11: + resolution: {integrity: sha512-8aluKz5oVC8PvVQAYgyIefOlqzKVmAOTCx2imbrFBVLbF7mnJvyMsE2A7rqX/4f4uT6ee9o8u3GcoRpUWc0xsw==} dev: true /vue-demi@0.14.5(vue@3.4.21):