From 6580b927b32756e614b761460c7032ab322d950c Mon Sep 17 00:00:00 2001 From: Jon Date: Thu, 21 Dec 2023 08:40:37 +0000 Subject: [PATCH 01/32] docs: Add notice to TheHive triggers to highlight manual steps (#8051) --- .../nodes-base/nodes/TheHive/TheHiveTrigger.node.ts | 11 ++++++++++- .../TheHiveProject/TheHiveProjectTrigger.node.ts | 7 +++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/nodes-base/nodes/TheHive/TheHiveTrigger.node.ts b/packages/nodes-base/nodes/TheHive/TheHiveTrigger.node.ts index d293d40348355..a3e7b8e5e18f3 100644 --- a/packages/nodes-base/nodes/TheHive/TheHiveTrigger.node.ts +++ b/packages/nodes-base/nodes/TheHive/TheHiveTrigger.node.ts @@ -29,7 +29,16 @@ export class TheHiveTrigger implements INodeType { path: 'webhook', }, ], - properties: [...eventsDescription], + properties: [ + { + displayName: + 'You must set up the webhook in TheHive — instructions here', + name: 'notice', + type: 'notice', + default: '', + }, + ...eventsDescription, + ], }; webhookMethods = { diff --git a/packages/nodes-base/nodes/TheHiveProject/TheHiveProjectTrigger.node.ts b/packages/nodes-base/nodes/TheHiveProject/TheHiveProjectTrigger.node.ts index 93a6c2315dc22..074573e7de56c 100644 --- a/packages/nodes-base/nodes/TheHiveProject/TheHiveProjectTrigger.node.ts +++ b/packages/nodes-base/nodes/TheHiveProject/TheHiveProjectTrigger.node.ts @@ -31,6 +31,13 @@ export class TheHiveProjectTrigger implements INodeType { }, ], properties: [ + { + displayName: + 'You must set up the webhook in TheHive — instructions here', + name: 'notice', + type: 'notice', + default: '', + }, { displayName: 'Events', name: 'events', From 7806a65229878a473f5526bad0b94614e8bfa8aa Mon Sep 17 00:00:00 2001 From: oleg Date: Thu, 21 Dec 2023 10:27:49 +0100 Subject: [PATCH 02/32] feat: Add option to `returnIntermediateSteps` for AI agents (#8113) ## Summary ![CleanShot 2023-12-21 at 08 30 16](https://github.com/n8n-io/n8n/assets/12657221/66b0de47-80cd-41f9-940e-6cacc2c940a9) Signed-off-by: Oleg Ivaniv --- .../agents/Agent/agents/ConversationalAgent/description.ts | 7 +++++++ .../agents/Agent/agents/ConversationalAgent/execute.ts | 2 ++ .../Agent/agents/OpenAiFunctionsAgent/description.ts | 7 +++++++ .../agents/Agent/agents/OpenAiFunctionsAgent/execute.ts | 2 ++ .../nodes/agents/Agent/agents/ReActAgent/description.ts | 7 +++++++ .../nodes/agents/Agent/agents/ReActAgent/execute.ts | 7 ++++++- .../nodes/tools/ToolWikipedia/ToolWikipedia.node.ts | 7 ++++++- 7 files changed, 37 insertions(+), 2 deletions(-) diff --git a/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ConversationalAgent/description.ts b/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ConversationalAgent/description.ts index b65b2fddf2c5c..0bb67fa0a9d46 100644 --- a/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ConversationalAgent/description.ts +++ b/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ConversationalAgent/description.ts @@ -67,6 +67,13 @@ export const conversationalAgentProperties: INodeProperties[] = [ default: 10, description: 'The maximum number of iterations the agent will run before stopping', }, + { + displayName: 'Return Intermediate Steps', + name: 'returnIntermediateSteps', + type: 'boolean', + default: false, + description: 'Whether or not the output should include intermediate steps the agent took', + }, ], }, ]; 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 5d435059e26fe..abc820b325f61 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 @@ -41,6 +41,7 @@ export async function conversationalAgentExecute( systemMessage?: string; humanMessage?: string; maxIterations?: number; + returnIntermediateSteps?: boolean; }; const agentExecutor = await initializeAgentExecutorWithOptions(tools, model, { @@ -50,6 +51,7 @@ export async function conversationalAgentExecute( // memory option, but the memoryKey set on it must be "chat_history". agentType: 'chat-conversational-react-description', memory, + returnIntermediateSteps: options?.returnIntermediateSteps === true, maxIterations: options.maxIterations ?? 10, agentArgs: { systemMessage: options.systemMessage, diff --git a/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/OpenAiFunctionsAgent/description.ts b/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/OpenAiFunctionsAgent/description.ts index 5d7d4cc81bedb..34007d1084744 100644 --- a/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/OpenAiFunctionsAgent/description.ts +++ b/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/OpenAiFunctionsAgent/description.ts @@ -57,6 +57,13 @@ export const openAiFunctionsAgentProperties: INodeProperties[] = [ default: 10, description: 'The maximum number of iterations the agent will run before stopping', }, + { + displayName: 'Return Intermediate Steps', + name: 'returnIntermediateSteps', + type: 'boolean', + default: false, + description: 'Whether or not the output should include intermediate steps the agent took', + }, ], }, ]; 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 ecdd9e7ca2cf3..e21b5bdf420fd 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 @@ -40,6 +40,7 @@ export async function openAiFunctionsAgentExecute( const options = this.getNodeParameter('options', 0, {}) as { systemMessage?: string; maxIterations?: number; + returnIntermediateSteps?: boolean; }; const agentConfig: AgentExecutorInput = { @@ -49,6 +50,7 @@ export async function openAiFunctionsAgentExecute( }), tools, maxIterations: options.maxIterations ?? 10, + returnIntermediateSteps: options?.returnIntermediateSteps === true, memory: memory ?? new BufferMemory({ diff --git a/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ReActAgent/description.ts b/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ReActAgent/description.ts index 8c806b005c092..0feec7bfa6c4b 100644 --- a/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ReActAgent/description.ts +++ b/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ReActAgent/description.ts @@ -82,6 +82,13 @@ export const reActAgentAgentProperties: INodeProperties[] = [ rows: 6, }, }, + { + displayName: 'Return Intermediate Steps', + name: 'returnIntermediateSteps', + type: 'boolean', + default: false, + description: 'Whether or not the output should include intermediate steps the agent took', + }, ], }, ]; 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 13e77ad76c7ce..0366fedf73127 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 @@ -34,6 +34,7 @@ export async function reActAgentAgentExecute( suffix?: string; suffixChat?: string; humanMessageTemplate?: string; + returnIntermediateSteps?: boolean; }; let agent: ChatAgent | ZeroShotAgent; @@ -50,7 +51,11 @@ export async function reActAgentAgentExecute( }); } - const agentExecutor = AgentExecutor.fromAgentAndTools({ agent, tools }); + const agentExecutor = AgentExecutor.fromAgentAndTools({ + agent, + tools, + returnIntermediateSteps: options?.returnIntermediateSteps === true, + }); const returnData: INodeExecutionData[] = []; diff --git a/packages/@n8n/nodes-langchain/nodes/tools/ToolWikipedia/ToolWikipedia.node.ts b/packages/@n8n/nodes-langchain/nodes/tools/ToolWikipedia/ToolWikipedia.node.ts index 7892d8d54b38a..bc8ff97c0a268 100644 --- a/packages/@n8n/nodes-langchain/nodes/tools/ToolWikipedia/ToolWikipedia.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/tools/ToolWikipedia/ToolWikipedia.node.ts @@ -43,8 +43,13 @@ export class ToolWikipedia implements INodeType { }; async supplyData(this: IExecuteFunctions): Promise { + const WikiTool = new WikipediaQueryRun(); + + WikiTool.description = + 'A tool for interacting with and fetching data from the Wikipedia API. The input should always be a string query.'; + return { - response: logWrapper(new WikipediaQueryRun(), this), + response: logWrapper(WikiTool, this), }; } } From 5ffff1bb22691c09c5ca8b3ada2a19d5ce155a0b Mon Sep 17 00:00:00 2001 From: Omar Ajoue Date: Thu, 21 Dec 2023 09:40:39 +0000 Subject: [PATCH 03/32] fix: Stop binary data restoration from preventing execution from finishing (#8082) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In the case of a filesystem failure to rename the binary files as part of the execution's cleanup process, the execution would fail to be saved and would never finish. This catch prevents it. ## Summary Whenever an execution is wrapping u to save the data, if it uses binary data n8n will try to find possibly misallocated files and place them in the right folder. If this process fails, the execution fails to finish. Given the execution has already finished at this point, and we cannot handle the binary data errors more gracefully, all we can do at this point is log the message as it's a filesystem issue. The rest of the execution saving process should remain as normal. ## Related tickets and issues https://linear.app/n8n/issue/HELP-430 ## Review / Merge checklist - [ ] PR title and summary are descriptive. **Remember, the title automatically goes into the changelog. Use `(no-changelog)` otherwise.** ([conventions](https://github.com/n8n-io/n8n/blob/master/.github/pull_request_title_conventions.md)) - [ ] [Docs updated](https://github.com/n8n-io/n8n-docs) or follow-up ticket created. - [ ] Tests included. > A bug is not considered fixed, unless a test is added to prevent it from happening again. > A feature is not complete without tests. --------- Co-authored-by: Iván Ovejero --- .../restoreBinaryDataId.ts | 44 +++++++++++++------ .../restoreBinaryDataId.test.ts | 22 ++++++++++ 2 files changed, 52 insertions(+), 14 deletions(-) diff --git a/packages/cli/src/executionLifecycleHooks/restoreBinaryDataId.ts b/packages/cli/src/executionLifecycleHooks/restoreBinaryDataId.ts index 15c03e1b4df63..84780a785e410 100644 --- a/packages/cli/src/executionLifecycleHooks/restoreBinaryDataId.ts +++ b/packages/cli/src/executionLifecycleHooks/restoreBinaryDataId.ts @@ -3,6 +3,7 @@ import { BinaryDataService } from 'n8n-core'; import type { IRun, WorkflowExecuteMode } from 'n8n-workflow'; import type { BinaryData } from 'n8n-core'; import config from '@/config'; +import { Logger } from '@/Logger'; /** * Whenever the execution ID is not available to the binary data service at the @@ -32,28 +33,43 @@ export async function restoreBinaryDataId( return; } - const { runData } = run.data.resultData; + try { + const { runData } = run.data.resultData; - const promises = Object.keys(runData).map(async (nodeName) => { - const binaryDataId = runData[nodeName]?.[0]?.data?.main?.[0]?.[0]?.binary?.data?.id; + const promises = Object.keys(runData).map(async (nodeName) => { + const binaryDataId = runData[nodeName]?.[0]?.data?.main?.[0]?.[0]?.binary?.data?.id; - if (!binaryDataId) return; + if (!binaryDataId) return; - const [mode, fileId] = binaryDataId.split(':') as [BinaryData.StoredMode, string]; + const [mode, fileId] = binaryDataId.split(':') as [BinaryData.StoredMode, string]; - const isMissingExecutionId = fileId.includes('/temp/'); + const isMissingExecutionId = fileId.includes('/temp/'); - if (!isMissingExecutionId) return; + if (!isMissingExecutionId) return; - const correctFileId = fileId.replace('temp', executionId); + const correctFileId = fileId.replace('temp', executionId); - await Container.get(BinaryDataService).rename(fileId, correctFileId); + await Container.get(BinaryDataService).rename(fileId, correctFileId); - const correctBinaryDataId = `${mode}:${correctFileId}`; + const correctBinaryDataId = `${mode}:${correctFileId}`; - // @ts-expect-error Validated at the top - run.data.resultData.runData[nodeName][0].data.main[0][0].binary.data.id = correctBinaryDataId; - }); + // @ts-expect-error Validated at the top + run.data.resultData.runData[nodeName][0].data.main[0][0].binary.data.id = correctBinaryDataId; + }); - await Promise.all(promises); + await Promise.all(promises); + } catch (e) { + const error = e instanceof Error ? e : new Error(`${e}`); + const logger = Container.get(Logger); + + if (error.message.includes('ENOENT')) { + logger.warn('Failed to restore binary data ID - No such file or dir', { + executionId, + error, + }); + return; + } + + logger.error('Failed to restore binary data ID - Unknown error', { executionId, error }); + } } diff --git a/packages/cli/test/unit/execution-lifecyle/restoreBinaryDataId.test.ts b/packages/cli/test/unit/execution-lifecyle/restoreBinaryDataId.test.ts index 05d828100cbac..3fcdb79c72c7e 100644 --- a/packages/cli/test/unit/execution-lifecyle/restoreBinaryDataId.test.ts +++ b/packages/cli/test/unit/execution-lifecyle/restoreBinaryDataId.test.ts @@ -142,6 +142,28 @@ for (const mode of ['filesystem-v2', 's3'] as const) { expect(binaryDataService.rename).not.toHaveBeenCalled(); }); + + it('should ignore error thrown on renaming', async () => { + const workflowId = '6HYhhKmJch2cYxGj'; + const executionId = 'temp'; + const binaryDataFileUuid = 'a5c3f1ed-9d59-4155-bc68-9a370b3c51f6'; + + const incorrectFileId = `workflows/${workflowId}/executions/temp/binary_data/${binaryDataFileUuid}`; + + const run = toIRun({ + binary: { + data: { id: `s3:${incorrectFileId}` }, + }, + }); + + binaryDataService.rename.mockRejectedValueOnce(new Error('ENOENT')); + + const promise = restoreBinaryDataId(run, executionId, 'webhook'); + + await expect(promise).resolves.not.toThrow(); + + expect(binaryDataService.rename).toHaveBeenCalled(); + }); }); } From 32d397eff315fdc77677c0b134a7a25bcd8ca5d0 Mon Sep 17 00:00:00 2001 From: Marcus <56945030+maspio@users.noreply.github.com> Date: Thu, 21 Dec 2023 13:03:26 +0100 Subject: [PATCH 04/32] feat(Respond to Webhook Node): Overhaul with improvements like returning all items (#8093) --- .../RespondToWebhook/RespondToWebhook.node.ts | 78 +++++++++++++++---- 1 file changed, 65 insertions(+), 13 deletions(-) diff --git a/packages/nodes-base/nodes/RespondToWebhook/RespondToWebhook.node.ts b/packages/nodes-base/nodes/RespondToWebhook/RespondToWebhook.node.ts index 586f9de00587e..d6da0a0c51c34 100644 --- a/packages/nodes-base/nodes/RespondToWebhook/RespondToWebhook.node.ts +++ b/packages/nodes-base/nodes/RespondToWebhook/RespondToWebhook.node.ts @@ -9,6 +9,7 @@ import type { INodeTypeDescription, } from 'n8n-workflow'; import { jsonParse, BINARY_ENCODING, NodeOperationError } from 'n8n-workflow'; +import set from 'lodash/set'; export class RespondToWebhook implements INodeType { description: INodeTypeDescription = { @@ -25,34 +26,52 @@ export class RespondToWebhook implements INodeType { outputs: ['main'], credentials: [], properties: [ + { + displayName: + 'Verify that the "Webhook" node\'s "Respond" parameter is set to "Using Respond to Webhook Node". More details', + name: 'generalNotice', + type: 'notice', + default: '', + }, { displayName: 'Respond With', name: 'respondWith', type: 'options', options: [ { - name: 'Binary', + name: 'All Incoming Items', + value: 'allIncomingItems', + description: 'Respond with all input JSON items', + }, + { + name: 'Binary File', value: 'binary', + description: 'Respond with incoming file binary data', }, { name: 'First Incoming Item', value: 'firstIncomingItem', + description: 'Respond with the first input JSON item', }, { name: 'JSON', value: 'json', + description: 'Respond with a custom JSON body', }, { name: 'No Data', value: 'noData', + description: 'Respond with an empty body', }, { name: 'Redirect', value: 'redirect', + description: 'Respond with a redirect to a given URL', }, { name: 'Text', value: 'text', + description: 'Respond with a simple text message body', }, ], default: 'firstIncomingItem', @@ -60,7 +79,7 @@ export class RespondToWebhook implements INodeType { }, { displayName: - 'When using expressions, note that this node will only run for the first item in the input data.', + 'When using expressions, note that this node will only run for the first item in the input data', name: 'webhookNotice', type: 'notice', displayOptions: { @@ -94,9 +113,13 @@ export class RespondToWebhook implements INodeType { respondWith: ['json'], }, }, - default: '', - placeholder: '{ "key": "value" }', - description: 'The HTTP Response JSON data', + default: '{\n "myField": "value"\n}', + typeOptions: { + editor: 'json', + editorLanguage: 'json', + rows: 4, + }, + description: 'The HTTP response JSON data', }, { displayName: 'Response Body', @@ -107,9 +130,12 @@ export class RespondToWebhook implements INodeType { respondWith: ['text'], }, }, + typeOptions: { + rows: 2, + }, default: '', - placeholder: 'e.g. Workflow started', - description: 'The HTTP Response text data', + placeholder: 'e.g. Workflow completed', + description: 'The HTTP response text data', }, { displayName: 'Response Data Source', @@ -165,7 +191,7 @@ export class RespondToWebhook implements INodeType { maxValue: 599, }, default: 200, - description: 'The HTTP Response code to return. Defaults to 200.', + description: 'The HTTP response code to return. Defaults to 200.', }, { displayName: 'Response Headers', @@ -200,6 +226,19 @@ export class RespondToWebhook implements INodeType { }, ], }, + { + displayName: 'Put Response in Field', + name: 'responseKey', + type: 'string', + displayOptions: { + show: { + ['/respondWith']: ['allIncomingItems', 'firstIncomingItem'], + }, + }, + default: '', + description: 'The name of the response field to put all items in', + placeholder: 'e.g. data', + }, ], }, ], @@ -229,13 +268,26 @@ export class RespondToWebhook implements INodeType { if (typeof responseBodyParameter === 'object') { responseBody = responseBodyParameter; } else { - responseBody = jsonParse(responseBodyParameter, { - errorMessage: "Invalid JSON in 'Response Body' field", - }); + try { + responseBody = jsonParse(responseBodyParameter); + } catch (error) { + throw new NodeOperationError(this.getNode(), error as Error, { + message: "Invalid JSON in 'Response Body' field", + description: + "Check that the syntax of the JSON in the 'Response Body' parameter is valid", + }); + } } } + } else if (respondWith === 'allIncomingItems') { + const respondItems = items.map((item) => item.json); + responseBody = options.responseKey + ? set({}, options.responseKey as string, respondItems) + : respondItems; } else if (respondWith === 'firstIncomingItem') { - responseBody = items[0].json; + responseBody = options.responseKey + ? set({}, options.responseKey as string, items[0].json) + : items[0].json; } else if (respondWith === 'text') { responseBody = this.getNodeParameter('responseBody', 0) as string; } else if (respondWith === 'binary') { @@ -270,7 +322,7 @@ export class RespondToWebhook implements INodeType { if (!headers['content-type']) { headers['content-type'] = binaryData.mimeType; } - } else if (respondWith == 'redirect') { + } else if (respondWith === 'redirect') { headers.location = this.getNodeParameter('redirectURL', 0) as string; statusCode = (options.responseCode as number) ?? 307; } else if (respondWith !== 'noData') { From bba95761e2f2b54af1fcab8a7b1d626ca10d537e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E0=A4=95=E0=A4=BE=E0=A4=B0=E0=A4=A4=E0=A5=8B=E0=A4=AB?= =?UTF-8?q?=E0=A5=8D=E0=A4=AB=E0=A5=87=E0=A4=B2=E0=A4=B8=E0=A5=8D=E0=A4=95?= =?UTF-8?q?=E0=A5=8D=E0=A4=B0=E0=A4=BF=E0=A4=AA=E0=A5=8D=E0=A4=9F=E2=84=A2?= Date: Thu, 21 Dec 2023 13:32:04 +0100 Subject: [PATCH 05/32] fix(core): Prevent axios from force setting a form-urlencoded content-type (#8117) [Since v1 axios is force-setting a content-type of `application/x-www-form-urlencoded` on POST/PUT/PATCH requests, even if they have no payload](https://github.com/axios/axios/blob/v1.x/lib/core/dispatchRequest.js#L45-L47). This is causing nodes that do not support form-urlencoded bodies to fail. By setting the content-type to `false` (if a content-type wasn't already set), we force axios to not overwrite this header. [Workflows tests](https://github.com/n8n-io/n8n/actions/runs/7288103743/job/19860060607) --- packages/core/src/NodeExecuteFunctions.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/core/src/NodeExecuteFunctions.ts b/packages/core/src/NodeExecuteFunctions.ts index 659dbcae8e934..55cfdd2a1a269 100644 --- a/packages/core/src/NodeExecuteFunctions.ts +++ b/packages/core/src/NodeExecuteFunctions.ts @@ -162,6 +162,13 @@ axios.defaults.paramsSerializer = (params) => { } return stringify(params, { arrayFormat: 'indices' }); }; +axios.interceptors.request.use((config) => { + // If no content-type is set by us, prevent axios from force-setting the content-type to `application/x-www-form-urlencoded` + if (config.data === undefined) { + config.headers.setContentType(false, false); + } + return config; +}); const pushFormDataValue = (form: FormData, key: string, value: any) => { if (value?.hasOwnProperty('value') && value.hasOwnProperty('options')) { From b67b5ae6b2fa7abae72b546e28d77ae723b6c240 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 21 Dec 2023 13:51:24 +0100 Subject: [PATCH 06/32] :rocket: Release 1.22.0 (#8115) # [1.22.0](https://github.com/n8n-io/n8n/compare/n8n@1.21.0...n8n@1.22.0) (2023-12-21) ### Bug Fixes * **core:** Close db connection gracefully when exiting ([#8045](https://github.com/n8n-io/n8n/pull/8045)) ([e69707e](https://github.com/n8n-io/n8n/commit/e69707efd4bd947fdf6b9c66f373da63d34f41e9)) * **core:** Consider timeout in shutdown an error ([#8050](https://github.com/n8n-io/n8n/pull/8050)) ([4cae976](https://github.com/n8n-io/n8n/commit/4cae976a3b428bd528fe71ef0b240c0fd6e23bbf)) * **core:** Do not display error when stopping jobless execution in queue mode ([#8007](https://github.com/n8n-io/n8n/pull/8007)) ([8e6b951](https://github.com/n8n-io/n8n/commit/8e6b951a76e08b9ee9740fdd853f77553ad60cd6)) * **core:** Fix shutdown if terminating before hooks are initialized ([#8047](https://github.com/n8n-io/n8n/pull/8047)) ([6ae2f5e](https://github.com/n8n-io/n8n/commit/6ae2f5efea65e23029475ccdc5a65ec7c8152423)) * **core:** Handle multiple termination signals correctly ([#8046](https://github.com/n8n-io/n8n/pull/8046)) ([67bd8ad](https://github.com/n8n-io/n8n/commit/67bd8ad698bd0afe6ff7183d75da8bca4085598e)) * **core:** Initialize queue once in queue mode ([#8025](https://github.com/n8n-io/n8n/pull/8025)) ([53c0b49](https://github.com/n8n-io/n8n/commit/53c0b49d15047461e3b65baed65c9d76dff99539)) * **core:** Prevent axios from force setting a form-urlencoded content-type ([#8117](https://github.com/n8n-io/n8n/pull/8117)) ([bba9576](https://github.com/n8n-io/n8n/commit/bba95761e2f2b54af1fcab8a7b1d626ca10d537e)) * **core:** Remove circular references before serializing executions in public API ([#8043](https://github.com/n8n-io/n8n/pull/8043)) ([989888d](https://github.com/n8n-io/n8n/commit/989888d9bcec6f4eb3c811ce10d480737d96b102)) * **core:** Restore workflow ID during execution creation ([#8031](https://github.com/n8n-io/n8n/pull/8031)) ([c5e6ba8](https://github.com/n8n-io/n8n/commit/c5e6ba8cdd4a8f117ccc2e89e55497117156d8af)) * **core:** Use relative imports for dynamic imports in SecurityAuditService ([#8086](https://github.com/n8n-io/n8n/pull/8086)) ([785bf99](https://github.com/n8n-io/n8n/commit/785bf9974e38ea84c016e210a3108f4af567510d)) * **core:** Stop binary data restoration from preventing execution from finishing ([#8082](https://github.com/n8n-io/n8n/pull/8082)) ([5ffff1b](https://github.com/n8n-io/n8n/commit/5ffff1bb22691c09c5ca8b3ada2a19d5ce155a0b)) * **editor:** Add back credential `use` permission ([#8023](https://github.com/n8n-io/n8n/pull/8023)) ([329e5bf](https://github.com/n8n-io/n8n/commit/329e5bf9eed8556aba2bbd50bad9dbd6d3b373ad)) * **editor:** Cleanup Executions page component ([#8053](https://github.com/n8n-io/n8n/pull/8053)) ([2689c37](https://github.com/n8n-io/n8n/commit/2689c37e87c5b3ae5029121f4d3dc878841e8844)) * **editor:** Disable auto scroll and list size check when clicking on executions ([#7983](https://github.com/n8n-io/n8n/pull/7983)) ([fcb8b91](https://github.com/n8n-io/n8n/commit/fcb8b91f37e1fb0ef42f411c84390180e1ed7bbe)) * **editor:** Ensure execution data overrides pinned data when copying in executions view ([#8009](https://github.com/n8n-io/n8n/pull/8009)) ([1d1cb0d](https://github.com/n8n-io/n8n/commit/1d1cb0d3c530856e0c26d8f146f60b2555625ab6)) * **editor:** Fix copy/paste issue when switch node is in workflow ([#8103](https://github.com/n8n-io/n8n/pull/8103)) ([4b86926](https://github.com/n8n-io/n8n/commit/4b86926752fb1304a46385cb46bdf34fda0d53b6)) * **editor:** Make keyboard shortcuts more strict; don't accept extra Ctrl/Alt/Shift keys ([#8024](https://github.com/n8n-io/n8n/pull/8024)) ([8df49e1](https://github.com/n8n-io/n8n/commit/8df49e134d886267f9f7475573d013371220dcac)) * **editor:** Show credential share info only to appropriate users ([#8020](https://github.com/n8n-io/n8n/pull/8020)) ([b29b4d4](https://github.com/n8n-io/n8n/commit/b29b4d442bb0617aa516748ec48379eae0996cf0)) * **editor:** Turn off executions list auto-refresh after leaving the page ([#8005](https://github.com/n8n-io/n8n/pull/8005)) ([e3c363d](https://github.com/n8n-io/n8n/commit/e3c363d72cf4ee49086d012f92a7b34be958482f)) * **editor:** Update image sizes in template description not to be full width always ([#8037](https://github.com/n8n-io/n8n/pull/8037)) ([63a6e7e](https://github.com/n8n-io/n8n/commit/63a6e7e0340e1b00719f212ac620600a90d70ef1)) * **ActiveCampaign Node:** Fix pagination issue when loading tags ([#8017](https://github.com/n8n-io/n8n/pull/8017)) ([1943857](https://github.com/n8n-io/n8n/commit/19438572312cf9354c333aeb52ccbf1ab81fc51f)) * **HTTP Request Node:** Do not create circular references in HTTP request node output ([#8030](https://github.com/n8n-io/n8n/pull/8030)) ([5b7ea16](https://github.com/n8n-io/n8n/commit/5b7ea16d9a20880c72779b02620e99ebe9f3617a)) * Upgrade axios to address CVE-2023-45857 ([#7713](https://github.com/n8n-io/n8n/pull/7713)) ([64eb9bb](https://github.com/n8n-io/n8n/commit/64eb9bbc3624ee8f2fa90812711ad568926fdca8)) ### Features * Add option to `returnIntermediateSteps` for AI agents ([#8113](https://github.com/n8n-io/n8n/pull/8113)) ([7806a65](https://github.com/n8n-io/n8n/commit/7806a65229878a473f5526bad0b94614e8bfa8aa)) * **core:** Add config option to prefer GET request over LIST when using Hashicorp Vault ([#8049](https://github.com/n8n-io/n8n/pull/8049)) ([439a22d](https://github.com/n8n-io/n8n/commit/439a22d68f7bf32f281b1078b71607307640a09b)) * **core:** Add N8N_GRACEFUL_SHUTDOWN_TIMEOUT env var ([#8068](https://github.com/n8n-io/n8n/pull/8068)) ([614f488](https://github.com/n8n-io/n8n/commit/614f48838626e2af8e3f2e76ee4a144af2d40f72)) * **editor:** Add lead enrichment suggestions to workflow list ([#8042](https://github.com/n8n-io/n8n/pull/8042)) ([36a923c](https://github.com/n8n-io/n8n/commit/36a923cf7bd4d42b8f8decbf01255c41d6dc1671)) * **editor:** Finalize workers view ([#8052](https://github.com/n8n-io/n8n/pull/8052)) ([edfa784](https://github.com/n8n-io/n8n/commit/edfa78414d6bce901becc05e9d860f2521139688)) * **editor:** Gracefully ignore invalid payloads in postMessage handler ([#8096](https://github.com/n8n-io/n8n/pull/8096)) ([9d22c7a](https://github.com/n8n-io/n8n/commit/9d22c7a2782a1908f81bcf80260cd91cb296e239)) * **editor:** Upgrade frontend tooling to address a few vulnerabilities ([#8100](https://github.com/n8n-io/n8n/pull/8100)) ([19b7f1f](https://github.com/n8n-io/n8n/commit/19b7f1ffb17dcd6ac77839f97c2544f60f4ad55e)) * **Filter Node:** Overhaul UI by adding the new filter component ([#8016](https://github.com/n8n-io/n8n/pull/8016)) ([3d53052](https://github.com/n8n-io/n8n/commit/3d530522f828dfc985ae98e4bb551aa3a2bd44c6)) * **Respond to Webhook Node:** Overhaul with improvements like returning all items ([#8093](https://github.com/n8n-io/n8n/pull/8093)) ([32d397e](https://github.com/n8n-io/n8n/commit/32d397eff315fdc77677c0b134a7a25bcd8ca5d0)) ### Performance Improvements * **editor:** Improve canvas rendering performance ([#8022](https://github.com/n8n-io/n8n/pull/8022)) ([b780436](https://github.com/n8n-io/n8n/commit/b780436a6b445dc5951217b5a1f2c61b34961757)) Co-authored-by: ivov --- CHANGELOG.md | 49 ++++++++++++++++++++++ package.json | 2 +- packages/@n8n/chat/package.json | 2 +- packages/@n8n/client-oauth2/package.json | 2 +- packages/@n8n/nodes-langchain/package.json | 2 +- packages/@n8n/permissions/package.json | 2 +- packages/cli/package.json | 2 +- packages/core/package.json | 2 +- packages/design-system/package.json | 2 +- packages/editor-ui/package.json | 2 +- packages/node-dev/package.json | 2 +- packages/nodes-base/package.json | 2 +- packages/workflow/package.json | 2 +- 13 files changed, 61 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eac93ca428d56..2033351b7ab4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,52 @@ +# [1.22.0](https://github.com/n8n-io/n8n/compare/n8n@1.21.0...n8n@1.22.0) (2023-12-21) + + +### Bug Fixes + +* **ActiveCampaign Node:** Fix pagination issue when loading tags ([#8017](https://github.com/n8n-io/n8n/issues/8017)) ([1943857](https://github.com/n8n-io/n8n/commit/19438572312cf9354c333aeb52ccbf1ab81fc51f)) +* **core:** Close db connection gracefully when exiting ([#8045](https://github.com/n8n-io/n8n/issues/8045)) ([e69707e](https://github.com/n8n-io/n8n/commit/e69707efd4bd947fdf6b9c66f373da63d34f41e9)) +* **core:** Consider timeout in shutdown an error ([#8050](https://github.com/n8n-io/n8n/issues/8050)) ([4cae976](https://github.com/n8n-io/n8n/commit/4cae976a3b428bd528fe71ef0b240c0fd6e23bbf)) +* **core:** Do not display error when stopping jobless execution in queue mode ([#8007](https://github.com/n8n-io/n8n/issues/8007)) ([8e6b951](https://github.com/n8n-io/n8n/commit/8e6b951a76e08b9ee9740fdd853f77553ad60cd6)) +* **core:** Fix shutdown if terminating before hooks are initialized ([#8047](https://github.com/n8n-io/n8n/issues/8047)) ([6ae2f5e](https://github.com/n8n-io/n8n/commit/6ae2f5efea65e23029475ccdc5a65ec7c8152423)) +* **core:** Handle multiple termination signals correctly ([#8046](https://github.com/n8n-io/n8n/issues/8046)) ([67bd8ad](https://github.com/n8n-io/n8n/commit/67bd8ad698bd0afe6ff7183d75da8bca4085598e)) +* **core:** Initialize queue once in queue mode ([#8025](https://github.com/n8n-io/n8n/issues/8025)) ([53c0b49](https://github.com/n8n-io/n8n/commit/53c0b49d15047461e3b65baed65c9d76dff99539)) +* **core:** Prevent axios from force setting a form-urlencoded content-type ([#8117](https://github.com/n8n-io/n8n/issues/8117)) ([bba9576](https://github.com/n8n-io/n8n/commit/bba95761e2f2b54af1fcab8a7b1d626ca10d537e)), closes [/github.com/axios/axios/blob/v1.x/lib/core/dispatchRequest.js#L45-L47](https://github.com//github.com/axios/axios/blob/v1.x/lib/core/dispatchRequest.js/issues/L45-L47) +* **core:** Remove circular references before serializing executions in public API ([#8043](https://github.com/n8n-io/n8n/issues/8043)) ([989888d](https://github.com/n8n-io/n8n/commit/989888d9bcec6f4eb3c811ce10d480737d96b102)), closes [#8030](https://github.com/n8n-io/n8n/issues/8030) +* **core:** Restore workflow ID during execution creation ([#8031](https://github.com/n8n-io/n8n/issues/8031)) ([c5e6ba8](https://github.com/n8n-io/n8n/commit/c5e6ba8cdd4a8f117ccc2e89e55497117156d8af)), closes [/github.com/n8n-io/n8n/pull/8002/files#diff-c8cbb62ca9ab2ae45e5f565cd8c63fff6475809a6241ea0b90acc575615224](https://github.com//github.com/n8n-io/n8n/pull/8002/files/issues/diff-c8cbb62ca9ab2ae45e5f565cd8c63fff6475809a6241ea0b90acc575615224) +* **core:** Use relative imports for dynamic imports in SecurityAuditService ([#8086](https://github.com/n8n-io/n8n/issues/8086)) ([785bf99](https://github.com/n8n-io/n8n/commit/785bf9974e38ea84c016e210a3108f4af567510d)), closes [#8085](https://github.com/n8n-io/n8n/issues/8085) +* **editor:** Add back credential `use` permission ([#8023](https://github.com/n8n-io/n8n/issues/8023)) ([329e5bf](https://github.com/n8n-io/n8n/commit/329e5bf9eed8556aba2bbd50bad9dbd6d3b373ad)) +* **editor:** Cleanup Executions page component ([#8053](https://github.com/n8n-io/n8n/issues/8053)) ([2689c37](https://github.com/n8n-io/n8n/commit/2689c37e87c5b3ae5029121f4d3dc878841e8844)) +* **editor:** Disable auto scroll and list size check when clicking on executions ([#7983](https://github.com/n8n-io/n8n/issues/7983)) ([fcb8b91](https://github.com/n8n-io/n8n/commit/fcb8b91f37e1fb0ef42f411c84390180e1ed7bbe)) +* **editor:** Ensure execution data overrides pinned data when copying in executions view ([#8009](https://github.com/n8n-io/n8n/issues/8009)) ([1d1cb0d](https://github.com/n8n-io/n8n/commit/1d1cb0d3c530856e0c26d8f146f60b2555625ab6)) +* **editor:** Fix copy/paste issue when switch node is in workflow ([#8103](https://github.com/n8n-io/n8n/issues/8103)) ([4b86926](https://github.com/n8n-io/n8n/commit/4b86926752fb1304a46385cb46bdf34fda0d53b6)) +* **editor:** Make keyboard shortcuts more strict; don't accept extra Ctrl/Alt/Shift keys ([#8024](https://github.com/n8n-io/n8n/issues/8024)) ([8df49e1](https://github.com/n8n-io/n8n/commit/8df49e134d886267f9f7475573d013371220dcac)) +* **editor:** Show credential share info only to appropriate users ([#8020](https://github.com/n8n-io/n8n/issues/8020)) ([b29b4d4](https://github.com/n8n-io/n8n/commit/b29b4d442bb0617aa516748ec48379eae0996cf0)) +* **editor:** Turn off executions list auto-refresh after leaving the page ([#8005](https://github.com/n8n-io/n8n/issues/8005)) ([e3c363d](https://github.com/n8n-io/n8n/commit/e3c363d72cf4ee49086d012f92a7b34be958482f)) +* **editor:** Update image sizes in template description not to be full width always ([#8037](https://github.com/n8n-io/n8n/issues/8037)) ([63a6e7e](https://github.com/n8n-io/n8n/commit/63a6e7e0340e1b00719f212ac620600a90d70ef1)) +* **HTTP Request Node:** Do not create circular references in HTTP request node output ([#8030](https://github.com/n8n-io/n8n/issues/8030)) ([5b7ea16](https://github.com/n8n-io/n8n/commit/5b7ea16d9a20880c72779b02620e99ebe9f3617a)) +* Stop binary data restoration from preventing execution from finishing ([#8082](https://github.com/n8n-io/n8n/issues/8082)) ([5ffff1b](https://github.com/n8n-io/n8n/commit/5ffff1bb22691c09c5ca8b3ada2a19d5ce155a0b)) +* Upgrade axios to address CVE-2023-45857 ([#7713](https://github.com/n8n-io/n8n/issues/7713)) ([64eb9bb](https://github.com/n8n-io/n8n/commit/64eb9bbc3624ee8f2fa90812711ad568926fdca8)) + + +### Features + +* Add config option to prefer GET request over LIST when using Hashicorp Vault ([#8049](https://github.com/n8n-io/n8n/issues/8049)) ([439a22d](https://github.com/n8n-io/n8n/commit/439a22d68f7bf32f281b1078b71607307640a09b)) +* Add option to `returnIntermediateSteps` for AI agents ([#8113](https://github.com/n8n-io/n8n/issues/8113)) ([7806a65](https://github.com/n8n-io/n8n/commit/7806a65229878a473f5526bad0b94614e8bfa8aa)) +* **core:** Add N8N_GRACEFUL_SHUTDOWN_TIMEOUT env var ([#8068](https://github.com/n8n-io/n8n/issues/8068)) ([614f488](https://github.com/n8n-io/n8n/commit/614f48838626e2af8e3f2e76ee4a144af2d40f72)) +* **editor:** Add lead enrichment suggestions to workflow list ([#8042](https://github.com/n8n-io/n8n/issues/8042)) ([36a923c](https://github.com/n8n-io/n8n/commit/36a923cf7bd4d42b8f8decbf01255c41d6dc1671)), closes [-update-workflows-list-page-to-show-fake-door-templates#comment-b6644c99](https://github.com/-update-workflows-list-page-to-show-fake-door-templates/issues/comment-b6644c99) +* **editor:** Finalize workers view ([#8052](https://github.com/n8n-io/n8n/issues/8052)) ([edfa784](https://github.com/n8n-io/n8n/commit/edfa78414d6bce901becc05e9d860f2521139688)) +* **editor:** Gracefully ignore invalid payloads in postMessage handler ([#8096](https://github.com/n8n-io/n8n/issues/8096)) ([9d22c7a](https://github.com/n8n-io/n8n/commit/9d22c7a2782a1908f81bcf80260cd91cb296e239)) +* **editor:** Upgrade frontend tooling to address a few vulnerabilities ([#8100](https://github.com/n8n-io/n8n/issues/8100)) ([19b7f1f](https://github.com/n8n-io/n8n/commit/19b7f1ffb17dcd6ac77839f97c2544f60f4ad55e)) +* **Filter Node:** Overhaul UI by adding the new filter component ([#8016](https://github.com/n8n-io/n8n/issues/8016)) ([3d53052](https://github.com/n8n-io/n8n/commit/3d530522f828dfc985ae98e4bb551aa3a2bd44c6)) +* **Respond to Webhook Node:** Overhaul with improvements like returning all items ([#8093](https://github.com/n8n-io/n8n/issues/8093)) ([32d397e](https://github.com/n8n-io/n8n/commit/32d397eff315fdc77677c0b134a7a25bcd8ca5d0)) + + +### Performance Improvements + +* **editor:** Improve canvas rendering performance ([#8022](https://github.com/n8n-io/n8n/issues/8022)) ([b780436](https://github.com/n8n-io/n8n/commit/b780436a6b445dc5951217b5a1f2c61b34961757)) + + + # [1.21.0](https://github.com/n8n-io/n8n/compare/n8n@1.20.0...n8n@1.21.0) (2023-12-13) diff --git a/package.json b/package.json index 3bf6fa6fd5cc5..90cee43f90170 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "n8n", - "version": "1.21.0", + "version": "1.22.0", "private": true, "homepage": "https://n8n.io", "engines": { diff --git a/packages/@n8n/chat/package.json b/packages/@n8n/chat/package.json index 932ff3363bba0..63b3a080c721f 100644 --- a/packages/@n8n/chat/package.json +++ b/packages/@n8n/chat/package.json @@ -1,6 +1,6 @@ { "name": "@n8n/chat", - "version": "0.4.0", + "version": "0.5.0", "scripts": { "dev": "pnpm run storybook", "build": "pnpm type-check && pnpm build:vite && pnpm build:prepare", diff --git a/packages/@n8n/client-oauth2/package.json b/packages/@n8n/client-oauth2/package.json index a75c1bafbefdd..a379529a9771e 100644 --- a/packages/@n8n/client-oauth2/package.json +++ b/packages/@n8n/client-oauth2/package.json @@ -1,6 +1,6 @@ { "name": "@n8n/client-oauth2", - "version": "0.10.0", + "version": "0.11.0", "scripts": { "clean": "rimraf dist .turbo", "dev": "pnpm watch", diff --git a/packages/@n8n/nodes-langchain/package.json b/packages/@n8n/nodes-langchain/package.json index 64319d75c2275..12e4dceda96c7 100644 --- a/packages/@n8n/nodes-langchain/package.json +++ b/packages/@n8n/nodes-langchain/package.json @@ -1,6 +1,6 @@ { "name": "@n8n/n8n-nodes-langchain", - "version": "0.6.0", + "version": "0.7.0", "description": "", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", diff --git a/packages/@n8n/permissions/package.json b/packages/@n8n/permissions/package.json index 49911148b8b0d..0423854b9d0e9 100644 --- a/packages/@n8n/permissions/package.json +++ b/packages/@n8n/permissions/package.json @@ -1,6 +1,6 @@ { "name": "@n8n/permissions", - "version": "0.4.0", + "version": "0.5.0", "scripts": { "clean": "rimraf dist .turbo", "dev": "pnpm watch", diff --git a/packages/cli/package.json b/packages/cli/package.json index 880558bbbd979..6e6f8de24a874 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "n8n", - "version": "1.21.0", + "version": "1.22.0", "description": "n8n Workflow Automation Tool", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", diff --git a/packages/core/package.json b/packages/core/package.json index 857bf0d9b5818..b1662464aa41f 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "n8n-core", - "version": "1.21.0", + "version": "1.22.0", "description": "Core functionality of n8n", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", diff --git a/packages/design-system/package.json b/packages/design-system/package.json index 3f3b2097b632b..02492eb5251be 100644 --- a/packages/design-system/package.json +++ b/packages/design-system/package.json @@ -1,6 +1,6 @@ { "name": "n8n-design-system", - "version": "1.15.0", + "version": "1.16.0", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", "author": { diff --git a/packages/editor-ui/package.json b/packages/editor-ui/package.json index 581ccbcd87831..7cd132b5d0a01 100644 --- a/packages/editor-ui/package.json +++ b/packages/editor-ui/package.json @@ -1,6 +1,6 @@ { "name": "n8n-editor-ui", - "version": "1.21.0", + "version": "1.22.0", "description": "Workflow Editor UI for n8n", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", diff --git a/packages/node-dev/package.json b/packages/node-dev/package.json index 77b35b7098e0f..c78da4aa55486 100644 --- a/packages/node-dev/package.json +++ b/packages/node-dev/package.json @@ -1,6 +1,6 @@ { "name": "n8n-node-dev", - "version": "1.21.0", + "version": "1.22.0", "description": "CLI to simplify n8n credentials/node development", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 2b199a5a0d235..b1e06a8ac450d 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -1,6 +1,6 @@ { "name": "n8n-nodes-base", - "version": "1.21.0", + "version": "1.22.0", "description": "Base nodes of n8n", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", diff --git a/packages/workflow/package.json b/packages/workflow/package.json index 5a1c525cf672d..ba58d25bc51de 100644 --- a/packages/workflow/package.json +++ b/packages/workflow/package.json @@ -1,6 +1,6 @@ { "name": "n8n-workflow", - "version": "1.21.0", + "version": "1.22.0", "description": "Workflow base code of n8n", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", From 01e9a79238bbd2c14ae77a12e54fc1c6f41e1246 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E0=A4=95=E0=A4=BE=E0=A4=B0=E0=A4=A4=E0=A5=8B=E0=A4=AB?= =?UTF-8?q?=E0=A5=8D=E0=A4=AB=E0=A5=87=E0=A4=B2=E0=A4=B8=E0=A5=8D=E0=A4=95?= =?UTF-8?q?=E0=A5=8D=E0=A4=B0=E0=A4=BF=E0=A4=AA=E0=A5=8D=E0=A4=9F=E2=84=A2?= Date: Thu, 21 Dec 2023 14:13:02 +0100 Subject: [PATCH 07/32] feat(core): Remove discontinued crypto-js (#8104) Since crypto-js was [discontinued](https://github.com/brix/crypto-js/commit/1da3dabf93f0a0435c47627d6f171ad25f452012), [we migrated all our backend encryption to native crypto](https://github.com/n8n-io/n8n/pull/7556). However I decided back then to not remove crypto-js just yet in expressions, as I wanted to use `SubtleCrypto`. Unfortunately for that to work, we'd need to make expressions async. So, to get rid of `crypto-js`, I propose this interim solution. ## Related tickets and issues N8N-7020 ## Review / Merge checklist - [x] PR title and summary are descriptive - [x] Tests included --- packages/cli/BREAKING-CHANGES.md | 11 +++ packages/workflow/package.json | 5 +- .../src/Extensions/StringExtensions.ts | 69 +++++++++++-------- .../StringExtensions.test.ts | 46 ++++++------- pnpm-lock.yaml | 27 +++++--- 5 files changed, 93 insertions(+), 65 deletions(-) diff --git a/packages/cli/BREAKING-CHANGES.md b/packages/cli/BREAKING-CHANGES.md index 10326e784fa73..1e045ad55359d 100644 --- a/packages/cli/BREAKING-CHANGES.md +++ b/packages/cli/BREAKING-CHANGES.md @@ -2,6 +2,17 @@ This list shows all the versions which include breaking changes and how to upgrade. +## 1.22.0 + +### What changed? + +Hash algorithm `ripemd160` is dropped from `.hash()` expressions. +`sha3` hash algorithm now returns a valid sha3-512 has, unlike the previous implementation that returned a `Keccak` hash instead. + +### When is action necessary? + +If you are using `.hash` helpers in expressions with hash algorithm `ripemd160`, you need to switch to one of the other supported algorithms. + ## 1.15.0 ### What changed? diff --git a/packages/workflow/package.json b/packages/workflow/package.json index ba58d25bc51de..66a1f20f793ae 100644 --- a/packages/workflow/package.json +++ b/packages/workflow/package.json @@ -39,12 +39,12 @@ "dist/**/*" ], "devDependencies": { - "@types/crypto-js": "^4.1.3", "@types/deep-equal": "^1.0.1", "@types/express": "^4.17.6", "@types/jmespath": "^0.15.0", "@types/lodash": "^4.14.195", "@types/luxon": "^3.2.0", + "@types/md5": "^2.3.5", "@types/xml2js": "^0.4.11" }, "dependencies": { @@ -52,14 +52,15 @@ "@n8n_io/riot-tmpl": "4.0.0", "ast-types": "0.15.2", "callsites": "3.1.0", - "crypto-js": "4.2.0", "deep-equal": "2.2.0", "esprima-next": "5.8.4", "form-data": "4.0.0", "jmespath": "0.16.0", "js-base64": "3.7.2", + "jssha": "3.3.1", "lodash": "4.17.21", "luxon": "3.3.0", + "md5": "2.3.0", "recast": "0.21.5", "title-case": "3.0.3", "transliteration": "2.3.5", diff --git a/packages/workflow/src/Extensions/StringExtensions.ts b/packages/workflow/src/Extensions/StringExtensions.ts index da991af75c580..794290d2248bd 100644 --- a/packages/workflow/src/Extensions/StringExtensions.ts +++ b/packages/workflow/src/Extensions/StringExtensions.ts @@ -1,21 +1,20 @@ -// import { createHash } from 'crypto'; +import SHA from 'jssha'; +import MD5 from 'md5'; +import { encode } from 'js-base64'; import { titleCase } from 'title-case'; import type { ExtensionMap } from './Extensions'; -import CryptoJS from 'crypto-js'; -import { encode } from 'js-base64'; import { transliterate } from 'transliteration'; import { ExpressionExtensionError } from '../errors/expression-extension.error'; -const hashFunctions: Record = { - md5: CryptoJS.MD5, - sha1: CryptoJS.SHA1, - sha224: CryptoJS.SHA224, - sha256: CryptoJS.SHA256, - sha384: CryptoJS.SHA384, - sha512: CryptoJS.SHA512, - sha3: CryptoJS.SHA3, - ripemd160: CryptoJS.RIPEMD160, -}; +export const SupportedHashAlgorithms = [ + 'md5', + 'sha1', + 'sha224', + 'sha256', + 'sha384', + 'sha512', + 'sha3', +] as const; // All symbols from https://www.xe.com/symbols/ as for 2022/11/09 const CURRENCY_REGEXP = @@ -113,23 +112,35 @@ const URL_REGEXP = const CHAR_TEST_REGEXP = /\p{L}/u; const PUNC_TEST_REGEXP = /[!?.]/; -function hash(value: string, extraArgs?: unknown): string { - const [algorithm = 'MD5'] = extraArgs as string[]; - if (algorithm.toLowerCase() === 'base64') { - // We're using a library instead of btoa because btoa only - // works on ASCII - return encode(value); - } - const hashFunction = hashFunctions[algorithm.toLowerCase()]; - if (!hashFunction) { - throw new ExpressionExtensionError( - `Unknown algorithm ${algorithm}. Available algorithms are: ${Object.keys(hashFunctions) - .map((s) => s.toUpperCase()) - .join(', ')}, and Base64.`, - ); +function hash(value: string, extraArgs: string[]): string { + const algorithm = extraArgs[0]?.toLowerCase() ?? 'md5'; + switch (algorithm) { + case 'base64': + return encode(value); + case 'md5': + return MD5(value); + case 'sha1': + case 'sha224': + case 'sha256': + case 'sha384': + case 'sha512': + case 'sha3': + const variant = ( + { + sha1: 'SHA-1', + sha224: 'SHA-224', + sha256: 'SHA-256', + sha384: 'SHA-384', + sha512: 'SHA-512', + sha3: 'SHA3-512', + } as const + )[algorithm]; + return new SHA(variant, 'TEXT').update(value).getHash('HEX'); + default: + throw new ExpressionExtensionError( + `Unknown algorithm ${algorithm}. Available algorithms are: ${SupportedHashAlgorithms.join()}, and Base64.`, + ); } - return hashFunction(value.toString()).toString(); - // return createHash(format).update(value.toString()).digest('hex'); } function isEmpty(value: string): boolean { diff --git a/packages/workflow/test/ExpressionExtensions/StringExtensions.test.ts b/packages/workflow/test/ExpressionExtensions/StringExtensions.test.ts index ce67491ab20cd..16f298376fd40 100644 --- a/packages/workflow/test/ExpressionExtensions/StringExtensions.test.ts +++ b/packages/workflow/test/ExpressionExtensions/StringExtensions.test.ts @@ -1,8 +1,6 @@ /** * @jest-environment jsdom */ - -import { stringExtensions } from '@/Extensions/StringExtensions'; import { evaluate } from './Helpers'; describe('Data Transformation Functions', () => { @@ -15,28 +13,28 @@ describe('Data Transformation Functions', () => { expect(evaluate('={{"".isEmpty()}}')).toEqual(true); }); - test('.hash() should work correctly on a string', () => { - expect(evaluate('={{ "12345".hash("sha256") }}')).toEqual( - stringExtensions.functions.hash('12345', ['sha256']), - ); - - expect(evaluate('={{ "12345".hash("sha256") }}')).not.toEqual( - stringExtensions.functions.hash('12345', ['MD5']), - ); - - expect(evaluate('={{ "12345".hash("MD5") }}')).toEqual( - stringExtensions.functions.hash('12345', ['MD5']), - ); - - expect(evaluate('={{ "12345".hash("sha256") }}')).toEqual( - '5994471abb01112afcc18159f6cc74b4f511b99806da59b3caf5a9c173cacfc5', - ); - }); - - test('.hash() alias should work correctly on a string', () => { - expect(evaluate('={{ "12345".hash("sha256") }}')).toEqual( - '5994471abb01112afcc18159f6cc74b4f511b99806da59b3caf5a9c173cacfc5', - ); + describe('.hash()', () => { + test.each([ + ['md5', '827ccb0eea8a706c4c34a16891f84e7b'], + ['sha1', '8cb2237d0679ca88db6464eac60da96345513964'], + ['sha224', 'a7470858e79c282bc2f6adfd831b132672dfd1224c1e78cbf5bcd057'], + ['sha256', '5994471abb01112afcc18159f6cc74b4f511b99806da59b3caf5a9c173cacfc5'], + [ + 'sha384', + '0fa76955abfa9dafd83facca8343a92aa09497f98101086611b0bfa95dbc0dcc661d62e9568a5a032ba81960f3e55d4a', + ], + [ + 'sha512', + '3627909a29c31381a071ec27f7c9ca97726182aed29a7ddd2e54353322cfb30abb9e3a6df2ac2c20fe23436311d678564d0c8d305930575f60e2d3d048184d79', + ], + [ + 'sha3', + '0a2a1719bf3ce682afdbedf3b23857818d526efbe7fcb372b31347c26239a0f916c398b7ad8dd0ee76e8e388604d0b0f925d5e913ad2d3165b9b35b3844cd5e6', + ], + ])('should work for %p', (hashFn, hashValue) => { + expect(evaluate(`={{ "12345".hash("${hashFn}") }}`)).toEqual(hashValue); + expect(evaluate(`={{ "12345".hash("${hashFn.toLowerCase()}") }}`)).toEqual(hashValue); + }); }); test('.urlDecode should work correctly on a string', () => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6c861fb91ef68..c0cfc288076ba 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1494,9 +1494,6 @@ importers: callsites: specifier: 3.1.0 version: 3.1.0 - crypto-js: - specifier: 4.2.0 - version: 4.2.0 deep-equal: specifier: 2.2.0 version: 2.2.0 @@ -1512,12 +1509,18 @@ importers: js-base64: specifier: 3.7.2 version: 3.7.2 + jssha: + specifier: 3.3.1 + version: 3.3.1 lodash: specifier: 4.17.21 version: 4.17.21 luxon: specifier: 3.3.0 version: 3.3.0 + md5: + specifier: 2.3.0 + version: 2.3.0 recast: specifier: 0.21.5 version: 0.21.5 @@ -1531,9 +1534,6 @@ importers: specifier: ^0.5.0 version: 0.5.0 devDependencies: - '@types/crypto-js': - specifier: ^4.1.3 - version: 4.1.3 '@types/deep-equal': specifier: ^1.0.1 version: 1.0.1 @@ -1549,6 +1549,9 @@ importers: '@types/luxon': specifier: ^3.2.0 version: 3.2.0 + '@types/md5': + specifier: ^2.3.5 + version: 2.3.5 '@types/xml2js': specifier: ^0.4.11 version: 0.4.11 @@ -9095,10 +9098,6 @@ packages: '@types/node': 18.16.16 dev: true - /@types/crypto-js@4.1.3: - resolution: {integrity: sha512-YP1sYYayLe7Eg5oXyLLvOLfxBfZ5Fgpz6sVWkpB18wDMywCLPWmqzRz+9gyuOoLF0fzDTTFwlyNbx7koONUwqA==} - dev: true - /@types/dateformat@3.0.1: resolution: {integrity: sha512-KlPPdikagvL6ELjWsljbyDIPzNCeliYkqRpI+zea99vBBbCIA5JNshZAwQKTON139c87y9qvTFVgkFd14rtS4g==} dev: true @@ -9364,6 +9363,10 @@ packages: '@types/linkify-it': 3.0.2 '@types/mdurl': 1.0.2 + /@types/md5@2.3.5: + resolution: {integrity: sha512-/i42wjYNgE6wf0j2bcTX6kuowmdL/6PE4IVitMpm2eYKBUuYCprdcWVK+xEF0gcV6ufMCRhtxmReGfc6hIK7Jw==} + dev: true + /@types/mdurl@1.0.2: resolution: {integrity: sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==} @@ -17661,6 +17664,10 @@ packages: resolution: {integrity: sha512-w9OtT4ALL+fbbwG3gw7erAO0jvS5nfvrukGPMWIAoea359B26ALXGpzy4YJSp9yGnpUvuvOw1nSjSoHDfWSr1w==} dev: false + /jssha@3.3.1: + resolution: {integrity: sha512-VCMZj12FCFMQYcFLPRm/0lOBbLi8uM2BhXPTqw3U4YAfs4AZfiApOoBLoN8cQE60Z50m1MYMTQVCfgF/KaCVhQ==} + dev: false + /jstransformer@1.0.0: resolution: {integrity: sha512-C9YK3Rf8q6VAPDCCU9fnqo3mAfOH6vUGnMcP4AQAYIEpWtfGLpwOTmZ+igtdK5y+VvI2n3CyYSzy4Qh34eq24A==} dependencies: From d1b2affd2c140fefb318af5b411c65c40aae8afe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Thu, 21 Dec 2023 14:15:37 +0100 Subject: [PATCH 08/32] ci: Add lint rule `no-dynamic-import-template` (no-changelog) (#8089) Follow-up to: https://github.com/n8n-io/n8n/pull/8086 `tsc-alias` as of 1.8.7 is unable to resolve template strings in dynamic imports. Since the module name mapper in Jest is able to, this issue is hard to detect, hence the new lint rule `no-dynamic-import-template`. This is for now specific to `@/` in the `cli` package - we can generalize later if needed. Ideally we should contribute a fix upstream when we have more time. Capture 2023-12-19 at 12 39 55@2x --- packages/@n8n_io/eslint-config/local-rules.js | 26 +++++++++++++++++++ packages/cli/.eslintrc.js | 2 ++ 2 files changed, 28 insertions(+) diff --git a/packages/@n8n_io/eslint-config/local-rules.js b/packages/@n8n_io/eslint-config/local-rules.js index 16e405a602d06..0d3ff867b3cff 100644 --- a/packages/@n8n_io/eslint-config/local-rules.js +++ b/packages/@n8n_io/eslint-config/local-rules.js @@ -396,6 +396,32 @@ module.exports = { }; }, }, + + 'no-dynamic-import-template': { + meta: { + type: 'error', + docs: { + description: + 'Disallow non-relative imports in template string argument to `await import()`, because `tsc-alias` as of 1.8.7 is unable to resolve aliased paths in this scenario.', + recommended: true, + }, + }, + create: function (context) { + return { + 'AwaitExpression > ImportExpression TemplateLiteral'(node) { + const templateValue = node.quasis[0].value.cooked; + + if (!templateValue?.startsWith('@/')) return; + + context.report({ + node, + message: + 'Use relative imports in template string argument to `await import()`, because `tsc-alias` as of 1.8.7 is unable to resolve aliased paths in this scenario.', + }); + }, + }; + }, + }, }; const isJsonParseCall = (node) => diff --git a/packages/cli/.eslintrc.js b/packages/cli/.eslintrc.js index 75676e3a4bc95..1840061479716 100644 --- a/packages/cli/.eslintrc.js +++ b/packages/cli/.eslintrc.js @@ -19,6 +19,8 @@ module.exports = { ], rules: { + 'n8n-local-rules/no-dynamic-import-template': 'error', + // TODO: Remove this 'import/no-cycle': 'warn', 'import/order': 'off', From 9ac8825a67754135df916bfc8afd7349c7694957 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Thu, 21 Dec 2023 14:15:46 +0100 Subject: [PATCH 09/32] refactor(core): Move error execution creation to execution service (no-changelog) (#8006) Continue breaking down legacy helpers. Note: `getUserById` is unused. --- packages/cli/src/ActiveWorkflowRunner.ts | 11 ++- packages/cli/src/GenericHelpers.ts | 91 ------------------- .../UserManagement/UserManagementHelper.ts | 9 -- .../src/credentials/credentials.service.ts | 5 +- .../cli/src/executions/executions.service.ts | 86 +++++++++++++++++- .../integration/ActiveWorkflowRunner.test.ts | 2 + packages/cli/test/integration/auth.mw.test.ts | 4 + .../integration/publicApi/workflows.test.ts | 2 + .../test/integration/shared/utils/index.ts | 2 + .../cli/test/integration/users.api.test.ts | 4 + 10 files changed, 106 insertions(+), 110 deletions(-) diff --git a/packages/cli/src/ActiveWorkflowRunner.ts b/packages/cli/src/ActiveWorkflowRunner.ts index 5b4a15f40c39a..07a5258af8af8 100644 --- a/packages/cli/src/ActiveWorkflowRunner.ts +++ b/packages/cli/src/ActiveWorkflowRunner.ts @@ -49,7 +49,7 @@ import * as WorkflowExecuteAdditionalData from '@/WorkflowExecuteAdditionalData' import type { User } from '@db/entities/User'; import type { WorkflowEntity } from '@db/entities/WorkflowEntity'; import { ActiveExecutions } from '@/ActiveExecutions'; -import { createErrorExecution } from '@/GenericHelpers'; +import { ExecutionsService } from './executions/executions.service'; import { STARTING_NODES, WORKFLOW_REACTIVATE_INITIAL_TIMEOUT, @@ -94,6 +94,7 @@ export class ActiveWorkflowRunner implements IWebhookManager { private readonly sharedWorkflowRepository: SharedWorkflowRepository, private readonly multiMainSetup: MultiMainSetup, private readonly activationErrorsService: ActivationErrorsService, + private readonly executionService: ExecutionsService, ) {} async init() { @@ -547,9 +548,11 @@ export class ActiveWorkflowRunner implements IWebhookManager { }; returnFunctions.__emitError = (error: ExecutionError): void => { - void createErrorExecution(error, node, workflowData, workflow, mode).then(() => { - this.executeErrorWorkflow(error, workflowData, mode); - }); + void this.executionService + .createErrorExecution(error, node, workflowData, workflow, mode) + .then(() => { + this.executeErrorWorkflow(error, workflowData, mode); + }); }; return returnFunctions; }; diff --git a/packages/cli/src/GenericHelpers.ts b/packages/cli/src/GenericHelpers.ts index 4d62c05d622f8..4645535332e1e 100644 --- a/packages/cli/src/GenericHelpers.ts +++ b/packages/cli/src/GenericHelpers.ts @@ -1,21 +1,11 @@ import type express from 'express'; -import type { - ExecutionError, - INode, - IRunExecutionData, - Workflow, - WorkflowExecuteMode, -} from 'n8n-workflow'; import { validate } from 'class-validator'; -import { Container } from 'typedi'; import config from '@/config'; -import type { ExecutionPayload, IWorkflowDb } from '@/Interfaces'; import type { WorkflowEntity } from '@db/entities/WorkflowEntity'; import type { CredentialsEntity } from '@db/entities/CredentialsEntity'; import type { TagEntity } from '@db/entities/TagEntity'; import type { User } from '@db/entities/User'; import type { UserUpdatePayload } from '@/requests'; -import { ExecutionRepository } from '@db/repositories/execution.repository'; import { BadRequestError } from './errors/response-errors/bad-request.error'; /** @@ -58,85 +48,4 @@ export async function validateEntity( } } -/** - * Create an error execution - * - * @param {INode} node - * @param {IWorkflowDb} workflowData - * @param {Workflow} workflow - * @param {WorkflowExecuteMode} mode - * @returns - * @memberof ActiveWorkflowRunner - */ - -export async function createErrorExecution( - error: ExecutionError, - node: INode, - workflowData: IWorkflowDb, - workflow: Workflow, - mode: WorkflowExecuteMode, -): Promise { - const saveDataErrorExecutionDisabled = workflowData?.settings?.saveDataErrorExecution === 'none'; - - if (saveDataErrorExecutionDisabled) return; - - const executionData: IRunExecutionData = { - startData: { - destinationNode: node.name, - runNodeFilter: [node.name], - }, - executionData: { - contextData: {}, - metadata: {}, - nodeExecutionStack: [ - { - node, - data: { - main: [ - [ - { - json: {}, - pairedItem: { - item: 0, - }, - }, - ], - ], - }, - source: null, - }, - ], - waitingExecution: {}, - waitingExecutionSource: {}, - }, - resultData: { - runData: { - [node.name]: [ - { - startTime: 0, - executionTime: 0, - error, - source: [], - }, - ], - }, - error, - lastNodeExecuted: node.name, - }, - }; - - const fullExecutionData: ExecutionPayload = { - data: executionData, - mode, - finished: false, - startedAt: new Date(), - workflowData, - workflowId: workflow.id, - stoppedAt: new Date(), - status: 'error', - }; - - await Container.get(ExecutionRepository).createNewExecution(fullExecutionData); -} - export const DEFAULT_EXECUTIONS_GET_ALL_LIMIT = 20; diff --git a/packages/cli/src/UserManagement/UserManagementHelper.ts b/packages/cli/src/UserManagement/UserManagementHelper.ts index 6b9b1a0eafec6..2a1cdd38fdf39 100644 --- a/packages/cli/src/UserManagement/UserManagementHelper.ts +++ b/packages/cli/src/UserManagement/UserManagementHelper.ts @@ -6,7 +6,6 @@ import type { User } from '@db/entities/User'; import config from '@/config'; import { License } from '@/License'; import { getWebhookBaseUrl } from '@/WebhookHelpers'; -import { UserRepository } from '@db/repositories/user.repository'; import type { Scope } from '@n8n/permissions'; export function isSharingEnabled(): boolean { @@ -26,14 +25,6 @@ export function generateUserInviteUrl(inviterId: string, inviteeId: string): str return `${getInstanceBaseUrl()}/signup?inviterId=${inviterId}&inviteeId=${inviteeId}`; } -export async function getUserById(userId: string): Promise { - const user = await Container.get(UserRepository).findOneOrFail({ - where: { id: userId }, - relations: ['globalRole'], - }); - return user; -} - // return the difference between two arrays export function rightDiff( [arr1, keyExtractor1]: [T1[], (item: T1) => string], diff --git a/packages/cli/src/credentials/credentials.service.ts b/packages/cli/src/credentials/credentials.service.ts index ab5e2379770ea..f75f1eccd6982 100644 --- a/packages/cli/src/credentials/credentials.service.ts +++ b/packages/cli/src/credentials/credentials.service.ts @@ -35,10 +35,7 @@ export type CredentialsGetSharedOptions = | { allowGlobalScope: false }; export class CredentialsService { - static async get( - where: FindOptionsWhere, - options?: { relations: string[] }, - ): Promise { + static async get(where: FindOptionsWhere, options?: { relations: string[] }) { return Container.get(CredentialsRepository).findOne({ relations: options?.relations, where, diff --git a/packages/cli/src/executions/executions.service.ts b/packages/cli/src/executions/executions.service.ts index 6790636346ddd..4e2d23ec20723 100644 --- a/packages/cli/src/executions/executions.service.ts +++ b/packages/cli/src/executions/executions.service.ts @@ -1,5 +1,13 @@ import { validate as jsonSchemaValidate } from 'jsonschema'; -import type { IWorkflowBase, JsonObject, ExecutionStatus } from 'n8n-workflow'; +import type { + IWorkflowBase, + JsonObject, + ExecutionStatus, + ExecutionError, + INode, + IRunExecutionData, + WorkflowExecuteMode, +} from 'n8n-workflow'; import { ApplicationError, jsonParse, Workflow, WorkflowOperationError } from 'n8n-workflow'; import type { FindOperator } from 'typeorm'; import { In } from 'typeorm'; @@ -7,9 +15,11 @@ import { ActiveExecutions } from '@/ActiveExecutions'; import config from '@/config'; import type { User } from '@db/entities/User'; import type { + ExecutionPayload, IExecutionFlattedResponse, IExecutionResponse, IExecutionsListResponse, + IWorkflowDb, IWorkflowExecutionDataProcess, } from '@/Interfaces'; import { NodeTypes } from '@/NodeTypes'; @@ -18,7 +28,7 @@ import type { ExecutionRequest } from '@/requests'; import { getSharedWorkflowIds } from '@/WorkflowHelpers'; import { WorkflowRunner } from '@/WorkflowRunner'; import * as GenericHelpers from '@/GenericHelpers'; -import { Container } from 'typedi'; +import { Container, Service } from 'typedi'; import { getStatusUsingPreviousExecutionStatusMethod } from './executionHelpers'; import { ExecutionRepository } from '@db/repositories/execution.repository'; import { WorkflowRepository } from '@db/repositories/workflow.repository'; @@ -75,6 +85,7 @@ const schemaGetExecutionsQueryFilter = { const allowedExecutionsQueryFilterFields = Object.keys(schemaGetExecutionsQueryFilter.properties); +@Service() export class ExecutionsService { /** * Function to get the workflow Ids for a User @@ -362,4 +373,75 @@ export class ExecutionsService { }, ); } + + async createErrorExecution( + error: ExecutionError, + node: INode, + workflowData: IWorkflowDb, + workflow: Workflow, + mode: WorkflowExecuteMode, + ): Promise { + const saveDataErrorExecutionDisabled = + workflowData?.settings?.saveDataErrorExecution === 'none'; + + if (saveDataErrorExecutionDisabled) return; + + const executionData: IRunExecutionData = { + startData: { + destinationNode: node.name, + runNodeFilter: [node.name], + }, + executionData: { + contextData: {}, + metadata: {}, + nodeExecutionStack: [ + { + node, + data: { + main: [ + [ + { + json: {}, + pairedItem: { + item: 0, + }, + }, + ], + ], + }, + source: null, + }, + ], + waitingExecution: {}, + waitingExecutionSource: {}, + }, + resultData: { + runData: { + [node.name]: [ + { + startTime: 0, + executionTime: 0, + error, + source: [], + }, + ], + }, + error, + lastNodeExecuted: node.name, + }, + }; + + const fullExecutionData: ExecutionPayload = { + data: executionData, + mode, + finished: false, + startedAt: new Date(), + workflowData, + workflowId: workflow.id, + stoppedAt: new Date(), + status: 'error', + }; + + await Container.get(ExecutionRepository).createNewExecution(fullExecutionData); + } } diff --git a/packages/cli/test/integration/ActiveWorkflowRunner.test.ts b/packages/cli/test/integration/ActiveWorkflowRunner.test.ts index 07a147a0ebd28..d7adb25edeb57 100644 --- a/packages/cli/test/integration/ActiveWorkflowRunner.test.ts +++ b/packages/cli/test/integration/ActiveWorkflowRunner.test.ts @@ -24,12 +24,14 @@ import { setSchedulerAsLoadedNode } from './shared/utils'; import * as testDb from './shared/testDb'; import { createOwner } from './shared/db/users'; import { createWorkflow } from './shared/db/workflows'; +import { ExecutionsService } from '@/executions/executions.service'; import { WorkflowService } from '@/workflows/workflow.service'; mockInstance(ActiveExecutions); mockInstance(ActiveWorkflows); mockInstance(Push); mockInstance(SecretsHelper); +mockInstance(ExecutionsService); mockInstance(WorkflowService); const webhookService = mockInstance(WebhookService); diff --git a/packages/cli/test/integration/auth.mw.test.ts b/packages/cli/test/integration/auth.mw.test.ts index 8cc77968a123f..f958a630d8fee 100644 --- a/packages/cli/test/integration/auth.mw.test.ts +++ b/packages/cli/test/integration/auth.mw.test.ts @@ -2,8 +2,12 @@ import type { SuperAgentTest } from 'supertest'; import * as utils from './shared/utils/'; import { getGlobalMemberRole } from './shared/db/roles'; import { createUser } from './shared/db/users'; +import { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner'; +import { mockInstance } from '../shared/mocking'; describe('Auth Middleware', () => { + mockInstance(ActiveWorkflowRunner); + const testServer = utils.setupTestServer({ endpointGroups: ['me', 'auth', 'owner', 'users', 'invitations'], }); diff --git a/packages/cli/test/integration/publicApi/workflows.test.ts b/packages/cli/test/integration/publicApi/workflows.test.ts index e4de396a129f1..47b0c2c15fe1b 100644 --- a/packages/cli/test/integration/publicApi/workflows.test.ts +++ b/packages/cli/test/integration/publicApi/workflows.test.ts @@ -18,6 +18,7 @@ import { createWorkflow, createWorkflowWithTrigger } from '../shared/db/workflow import { createTag } from '../shared/db/tags'; import { mockInstance } from '../../shared/mocking'; import { Push } from '@/push'; +import { ExecutionsService } from '@/executions/executions.service'; let workflowOwnerRole: Role; let owner: User; @@ -30,6 +31,7 @@ const testServer = utils.setupTestServer({ endpointGroups: ['publicApi'] }); const license = testServer.license; mockInstance(Push); +mockInstance(ExecutionsService); beforeAll(async () => { const [globalOwnerRole, globalMemberRole, fetchedWorkflowOwnerRole] = await getAllRoles(); diff --git a/packages/cli/test/integration/shared/utils/index.ts b/packages/cli/test/integration/shared/utils/index.ts index f2b9d4e5e33e3..63fcd48621104 100644 --- a/packages/cli/test/integration/shared/utils/index.ts +++ b/packages/cli/test/integration/shared/utils/index.ts @@ -18,6 +18,7 @@ import { SettingsRepository } from '@db/repositories/settings.repository'; import { mockNodeTypesData } from '../../../unit/Helpers'; import { MultiMainSetup } from '@/services/orchestration/main/MultiMainSetup.ee'; import { mockInstance } from '../../../shared/mocking'; +import { ExecutionsService } from '@/executions/executions.service'; export { setupTestServer } from './testServer'; @@ -31,6 +32,7 @@ export { setupTestServer } from './testServer'; export async function initActiveWorkflowRunner() { mockInstance(MultiMainSetup); + mockInstance(ExecutionsService); const { ActiveWorkflowRunner } = await import('@/ActiveWorkflowRunner'); const workflowRunner = Container.get(ActiveWorkflowRunner); await workflowRunner.init(); diff --git a/packages/cli/test/integration/users.api.test.ts b/packages/cli/test/integration/users.api.test.ts index 4e18f13d85834..2ca6bb5350bd7 100644 --- a/packages/cli/test/integration/users.api.test.ts +++ b/packages/cli/test/integration/users.api.test.ts @@ -18,6 +18,10 @@ import * as testDb from './shared/testDb'; import type { SuperAgentTest } from 'supertest'; import type { Role } from '@db/entities/Role'; import type { User } from '@db/entities/User'; +import { ExecutionsService } from '@/executions/executions.service'; +import { mockInstance } from '../shared/mocking'; + +mockInstance(ExecutionsService); const testServer = utils.setupTestServer({ endpointGroups: ['users'], From bec0faed9e51fe6ea20ab3b07b4dfa849b28516b Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Thu, 21 Dec 2023 14:21:09 +0100 Subject: [PATCH 10/32] feat(core): Add closeFunction support to Sub-Nodes (#7708) Github issue / Community forum post (link here to close automatically): --------- Signed-off-by: Oleg Ivaniv Co-authored-by: Oleg Ivaniv --- .../MemoryRedisChat/MemoryRedisChat.node.ts | 5 +++++ packages/core/src/NodeExecuteFunctions.ts | 8 ++++++- packages/core/src/WorkflowExecute.ts | 4 +++- packages/workflow/src/Interfaces.ts | 12 +++++++---- packages/workflow/src/RoutingNode.ts | 3 +++ packages/workflow/src/Workflow.ts | 21 +++++++++++++++++++ 6 files changed, 47 insertions(+), 6 deletions(-) diff --git a/packages/@n8n/nodes-langchain/nodes/memory/MemoryRedisChat/MemoryRedisChat.node.ts b/packages/@n8n/nodes-langchain/nodes/memory/MemoryRedisChat/MemoryRedisChat.node.ts index 10402a2893637..538ce06f38e1d 100644 --- a/packages/@n8n/nodes-langchain/nodes/memory/MemoryRedisChat/MemoryRedisChat.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/memory/MemoryRedisChat/MemoryRedisChat.node.ts @@ -114,7 +114,12 @@ export class MemoryRedisChat implements INodeType { outputKey: 'output', }); + async function closeFunction() { + void client.disconnect(); + } + return { + closeFunction, response: logWrapper(memory, this), }; } diff --git a/packages/core/src/NodeExecuteFunctions.ts b/packages/core/src/NodeExecuteFunctions.ts index 55cfdd2a1a269..df6f7865821d7 100644 --- a/packages/core/src/NodeExecuteFunctions.ts +++ b/packages/core/src/NodeExecuteFunctions.ts @@ -37,6 +37,7 @@ import pick from 'lodash/pick'; import { extension, lookup } from 'mime-types'; import type { BinaryHelperFunctions, + CloseFunction, ConnectionTypes, ContextType, FieldType, @@ -3118,6 +3119,7 @@ export function getExecuteFunctions( additionalData: IWorkflowExecuteAdditionalData, executeData: IExecuteData, mode: WorkflowExecuteMode, + closeFunctions: CloseFunction[], abortSignal?: AbortSignal, ): IExecuteFunctions { return ((workflow, runExecutionData, connectionInputData, inputData, node) => { @@ -3294,7 +3296,11 @@ export function getExecuteFunctions( }; try { - return await nodeType.supplyData.call(context, itemIndex); + const response = await nodeType.supplyData.call(context, itemIndex); + if (response.closeFunction) { + closeFunctions.push(response.closeFunction); + } + return response; } catch (error) { // Propagate errors from sub-nodes if (error.functionality === 'configuration-node') throw error; diff --git a/packages/core/src/WorkflowExecute.ts b/packages/core/src/WorkflowExecute.ts index d4f733cd0d886..781aea9fdbf5d 100644 --- a/packages/core/src/WorkflowExecute.ts +++ b/packages/core/src/WorkflowExecute.ts @@ -32,6 +32,7 @@ import type { IRunExecutionData, IWorkflowExecuteAdditionalData, WorkflowExecuteMode, + CloseFunction, } from 'n8n-workflow'; import { LoggerProxy as Logger, @@ -1074,7 +1075,7 @@ export class WorkflowExecute { const errorItems: INodeExecutionData[] = []; const successItems: INodeExecutionData[] = []; - + const closeFunctions: CloseFunction[] = []; // Create a WorkflowDataProxy instance that we can get the data of the // item which did error const executeFunctions = NodeExecuteFunctions.getExecuteFunctions( @@ -1087,6 +1088,7 @@ export class WorkflowExecute { this.additionalData, executionData, this.mode, + closeFunctions, this.abortController.signal, ); const dataProxy = executeFunctions.getWorkflowDataProxy(0); diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index 4b6fded6598c4..210741c44bdc5 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -377,6 +377,8 @@ export interface IConnections { export type GenericValue = string | object | number | boolean | undefined | null; +export type CloseFunction = () => Promise; + export interface IDataObject { [key: string]: GenericValue | IDataObject | GenericValue[] | IDataObject[]; } @@ -410,7 +412,7 @@ export interface IGetExecuteTriggerFunctions { export interface IRunNodeResponse { data: INodeExecutionData[][] | null | undefined; - closeFunction?: () => Promise; + closeFunction?: CloseFunction; } export interface IGetExecuteFunctions { ( @@ -423,6 +425,7 @@ export interface IGetExecuteFunctions { additionalData: IWorkflowExecuteAdditionalData, executeData: IExecuteData, mode: WorkflowExecuteMode, + closeFunctions: CloseFunction[], abortSignal?: AbortSignal, ): IExecuteFunctions; } @@ -1289,13 +1292,13 @@ export type IParameterLabel = { }; export interface IPollResponse { - closeFunction?: () => Promise; + closeFunction?: CloseFunction; } export interface ITriggerResponse { - closeFunction?: () => Promise; + closeFunction?: CloseFunction; // To manually trigger the run - manualTriggerFunction?: () => Promise; + manualTriggerFunction?: CloseFunction; // Gets added automatically at manual workflow runs resolves with // the first emitted data manualTriggerResponse?: Promise; @@ -1324,6 +1327,7 @@ export namespace MultiPartFormData { export interface SupplyData { metadata?: IDataObject; response: unknown; + closeFunction?: CloseFunction; } export interface INodeType { diff --git a/packages/workflow/src/RoutingNode.ts b/packages/workflow/src/RoutingNode.ts index fcd8fea8c384b..f33382bb39b68 100644 --- a/packages/workflow/src/RoutingNode.ts +++ b/packages/workflow/src/RoutingNode.ts @@ -37,6 +37,7 @@ import type { NodeParameterValueType, PostReceiveAction, JsonObject, + CloseFunction, } from './Interfaces'; import * as NodeHelpers from './NodeHelpers'; @@ -94,6 +95,7 @@ export class RoutingNode { if (nodeType.description.credentials?.length) { credentialType = nodeType.description.credentials[0].name; } + const closeFunctions: CloseFunction[] = []; const executeFunctions = nodeExecuteFunctions.getExecuteFunctions( this.workflow, this.runExecutionData, @@ -104,6 +106,7 @@ export class RoutingNode { this.additionalData, executeData, this.mode, + closeFunctions, abortSignal, ); diff --git a/packages/workflow/src/Workflow.ts b/packages/workflow/src/Workflow.ts index 931dd668b4769..9eda896beaee2 100644 --- a/packages/workflow/src/Workflow.ts +++ b/packages/workflow/src/Workflow.ts @@ -42,6 +42,7 @@ import type { IRunNodeResponse, NodeParameterValueType, ConnectionTypes, + CloseFunction, } from './Interfaces'; import { Node } from './Interfaces'; import type { IDeferredPromise } from './DeferredPromise'; @@ -1298,6 +1299,7 @@ export class Workflow { } if (nodeType.execute) { + const closeFunctions: CloseFunction[] = []; const context = nodeExecuteFunctions.getExecuteFunctions( this, runExecutionData, @@ -1308,12 +1310,31 @@ export class Workflow { additionalData, executionData, mode, + closeFunctions, abortSignal, ); const data = nodeType instanceof Node ? await nodeType.execute(context) : await nodeType.execute.call(context); + + const closeFunctionsResults = await Promise.allSettled( + closeFunctions.map(async (fn) => fn()), + ); + + const closingErrors = closeFunctionsResults + .filter((result): result is PromiseRejectedResult => result.status === 'rejected') + .map((result) => result.reason); + + if (closingErrors.length > 0) { + if (closingErrors[0] instanceof Error) throw closingErrors[0]; + throw new ApplicationError("Error on execution node's close function(s)", { + extra: { nodeName: node.name }, + tags: { nodeType: node.type }, + cause: closingErrors, + }); + } + return { data }; } else if (nodeType.poll) { if (mode === 'manual') { From ffaa30ddc4ee312f44726c17a7ec91b5551092ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E0=A4=95=E0=A4=BE=E0=A4=B0=E0=A4=A4=E0=A5=8B=E0=A4=AB?= =?UTF-8?q?=E0=A5=8D=E0=A4=AB=E0=A5=87=E0=A4=B2=E0=A4=B8=E0=A5=8D=E0=A4=95?= =?UTF-8?q?=E0=A5=8D=E0=A4=B0=E0=A4=BF=E0=A4=AA=E0=A5=8D=E0=A4=9F=E2=84=A2?= Date: Thu, 21 Dec 2023 14:52:42 +0100 Subject: [PATCH 11/32] fix(core): Handle empty executions table in pruning in migrations (#8121) In case someone manually prunes their executions table before upgrading to 1.x, `MigrateIntegerKeysToString` should gracefully handle that, instead of crashing the application. ## Review / Merge checklist - [x] PR title and summary are descriptive --- ...690000000002-MigrateIntegerKeysToString.ts | 32 +++++++++++-------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/packages/cli/src/databases/migrations/sqlite/1690000000002-MigrateIntegerKeysToString.ts b/packages/cli/src/databases/migrations/sqlite/1690000000002-MigrateIntegerKeysToString.ts index 48dfc3ad46f57..83b55787b2f51 100644 --- a/packages/cli/src/databases/migrations/sqlite/1690000000002-MigrateIntegerKeysToString.ts +++ b/packages/cli/src/databases/migrations/sqlite/1690000000002-MigrateIntegerKeysToString.ts @@ -208,20 +208,24 @@ const pruneExecutionsData = async ({ queryRunner, tablePrefix, logger }: Migrati } console.time('pruningData'); - const counting = (await queryRunner.query( - `select count(id) as rows from "${tablePrefix}execution_entity";`, - )) as Array<{ rows: number }>; - - const averageExecutionSize = dbFileSize / counting[0].rows; - const numberOfExecutionsToKeep = Math.floor(DESIRED_DATABASE_FILE_SIZE / averageExecutionSize); - - const query = `SELECT id FROM "${tablePrefix}execution_entity" ORDER BY id DESC limit ${numberOfExecutionsToKeep}, 1`; - const idToKeep = await queryRunner - .query(query) - .then((rows: Array<{ id: number }>) => rows[0].id); - - const removalQuery = `DELETE FROM "${tablePrefix}execution_entity" WHERE id < ${idToKeep} and status IN ('success')`; - await queryRunner.query(removalQuery); + const [{ rowCount }] = (await queryRunner.query( + `select count(id) as rowCount from "${tablePrefix}execution_entity";`, + )) as Array<{ rowCount: number }>; + + if (rowCount > 0) { + const averageExecutionSize = dbFileSize / rowCount; + const numberOfExecutionsToKeep = Math.floor( + DESIRED_DATABASE_FILE_SIZE / averageExecutionSize, + ); + + const query = `SELECT id FROM "${tablePrefix}execution_entity" ORDER BY id DESC limit ${numberOfExecutionsToKeep}, 1`; + const idToKeep = await queryRunner + .query(query) + .then((rows: Array<{ id: number }>) => rows[0].id); + + const removalQuery = `DELETE FROM "${tablePrefix}execution_entity" WHERE id < ${idToKeep} and status IN ('success')`; + await queryRunner.query(removalQuery); + } console.timeEnd('pruningData'); } else { logger.debug('Pruning was requested, but was not enabled'); From 5f27c20a004333d2a7f288fb0867fdd56cae1a0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E0=A4=95=E0=A4=BE=E0=A4=B0=E0=A4=A4=E0=A5=8B=E0=A4=AB?= =?UTF-8?q?=E0=A5=8D=E0=A4=AB=E0=A5=87=E0=A4=B2=E0=A4=B8=E0=A5=8D=E0=A4=95?= =?UTF-8?q?=E0=A5=8D=E0=A4=B0=E0=A4=BF=E0=A4=AA=E0=A5=8D=E0=A4=9F=E2=84=A2?= Date: Thu, 21 Dec 2023 14:52:54 +0100 Subject: [PATCH 12/32] feat(Snowflake Node): Update snowflake-sdk (no-changelog) (#8087) This finally removes `aws-sdk` v2 from n8n's dependencies, which should reduce n8n dependencies and docker image size by about 10%. --- packages/nodes-base/package.json | 4 +- pnpm-lock.yaml | 1150 ++++++++++++++++++++++++------ 2 files changed, 946 insertions(+), 208 deletions(-) diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index b1e06a8ac450d..0781170e50cfa 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -817,7 +817,7 @@ "@types/request-promise-native": "~1.0.15", "@types/rfc2047": "^2.0.1", "@types/showdown": "^1.9.4", - "@types/snowflake-sdk": "^1.6.12", + "@types/snowflake-sdk": "^1.6.20", "@types/ssh2-sftp-client": "^5.1.0", "@types/tmp": "^0.2.0", "@types/uuid": "^8.3.2", @@ -882,7 +882,7 @@ "semver": "7.5.4", "showdown": "2.1.0", "simple-git": "3.17.0", - "snowflake-sdk": "1.8.0", + "snowflake-sdk": "1.9.2", "ssh2-sftp-client": "7.2.3", "tmp-promise": "3.0.3", "typedi": "0.10.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c0cfc288076ba..18180703effa2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1365,8 +1365,8 @@ importers: specifier: 3.17.0 version: 3.17.0 snowflake-sdk: - specifier: 1.8.0 - version: 1.8.0(asn1.js@5.4.1) + specifier: 1.9.2 + version: 1.9.2(asn1.js@5.4.1) ssh2-sftp-client: specifier: 7.2.3 version: 7.2.3 @@ -1456,8 +1456,8 @@ importers: specifier: ^1.9.4 version: 1.9.4 '@types/snowflake-sdk': - specifier: ^1.6.12 - version: 1.6.12 + specifier: ^1.6.20 + version: 1.6.20 '@types/ssh2-sftp-client': specifier: ^5.1.0 version: 5.3.2 @@ -1687,12 +1687,32 @@ packages: tslib: 2.6.1 dev: false + /@aws-crypto/crc32c@3.0.0: + resolution: {integrity: sha512-ENNPPManmnVJ4BTXlOjAgD7URidbAznURqD0KvfREyc4o20DPYdEldU1f5cQ7Jbj0CJJSPaMIk/9ZshdB3210w==} + dependencies: + '@aws-crypto/util': 3.0.0 + '@aws-sdk/types': 3.468.0 + tslib: 2.6.1 + dev: false + /@aws-crypto/ie11-detection@3.0.0: resolution: {integrity: sha512-341lBBkiY1DfDNKai/wXM3aujNBkXR7tq1URPQDL9wi3AUbI80NR74uF1TXHMm7po1AcnFk8iu2S2IeU/+/A+Q==} dependencies: tslib: 2.6.1 dev: false + /@aws-crypto/sha1-browser@3.0.0: + resolution: {integrity: sha512-NJth5c997GLHs6nOYTzFKTbYdMNA6/1XlKVgnZoaZcQ7z7UJlOgj2JdbHE8tiYLS3fzXNCguct77SPGat2raSw==} + dependencies: + '@aws-crypto/ie11-detection': 3.0.0 + '@aws-crypto/supports-web-crypto': 3.0.0 + '@aws-crypto/util': 3.0.0 + '@aws-sdk/types': 3.468.0 + '@aws-sdk/util-locate-window': 3.310.0 + '@aws-sdk/util-utf8-browser': 3.259.0 + tslib: 2.6.1 + dev: false + /@aws-crypto/sha256-browser@3.0.0: resolution: {integrity: sha512-8VLmW2B+gjFbU5uMeqtQM6Nj0/F1bro80xQXCW6CQBWgosFWXTx77aeOF5CAIAmbOK64SdMBJdNr6J41yP5mvQ==} dependencies: @@ -1824,6 +1844,72 @@ packages: dev: false optional: true + /@aws-sdk/client-s3@3.478.0: + resolution: {integrity: sha512-OUpbCCnK71lQQ07BohJOx9ZER0rPqRAGOVIIVhNEkeN0uYFLzB7/o5a7+FEPUQXEd5rZRZgbxN5xEmnNW/0Waw==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-crypto/sha1-browser': 3.0.0 + '@aws-crypto/sha256-browser': 3.0.0 + '@aws-crypto/sha256-js': 3.0.0 + '@aws-sdk/client-sts': 3.478.0 + '@aws-sdk/core': 3.477.0 + '@aws-sdk/credential-provider-node': 3.478.0 + '@aws-sdk/middleware-bucket-endpoint': 3.470.0 + '@aws-sdk/middleware-expect-continue': 3.468.0 + '@aws-sdk/middleware-flexible-checksums': 3.468.0 + '@aws-sdk/middleware-host-header': 3.468.0 + '@aws-sdk/middleware-location-constraint': 3.468.0 + '@aws-sdk/middleware-logger': 3.468.0 + '@aws-sdk/middleware-recursion-detection': 3.468.0 + '@aws-sdk/middleware-sdk-s3': 3.474.0 + '@aws-sdk/middleware-signing': 3.468.0 + '@aws-sdk/middleware-ssec': 3.468.0 + '@aws-sdk/middleware-user-agent': 3.478.0 + '@aws-sdk/region-config-resolver': 3.470.0 + '@aws-sdk/signature-v4-multi-region': 3.474.0 + '@aws-sdk/types': 3.468.0 + '@aws-sdk/util-endpoints': 3.478.0 + '@aws-sdk/util-user-agent-browser': 3.468.0 + '@aws-sdk/util-user-agent-node': 3.470.0 + '@aws-sdk/xml-builder': 3.472.0 + '@smithy/config-resolver': 2.0.21 + '@smithy/core': 1.2.0 + '@smithy/eventstream-serde-browser': 2.0.15 + '@smithy/eventstream-serde-config-resolver': 2.0.15 + '@smithy/eventstream-serde-node': 2.0.15 + '@smithy/fetch-http-handler': 2.3.1 + '@smithy/hash-blob-browser': 2.0.16 + '@smithy/hash-node': 2.0.17 + '@smithy/hash-stream-node': 2.0.17 + '@smithy/invalid-dependency': 2.0.15 + '@smithy/md5-js': 2.0.17 + '@smithy/middleware-content-length': 2.0.17 + '@smithy/middleware-endpoint': 2.2.3 + '@smithy/middleware-retry': 2.0.24 + '@smithy/middleware-serde': 2.0.15 + '@smithy/middleware-stack': 2.0.9 + '@smithy/node-config-provider': 2.1.8 + '@smithy/node-http-handler': 2.2.1 + '@smithy/protocol-http': 3.0.11 + '@smithy/smithy-client': 2.1.18 + '@smithy/types': 2.7.0 + '@smithy/url-parser': 2.0.15 + '@smithy/util-base64': 2.0.1 + '@smithy/util-body-length-browser': 2.0.1 + '@smithy/util-body-length-node': 2.1.0 + '@smithy/util-defaults-mode-browser': 2.0.22 + '@smithy/util-defaults-mode-node': 2.0.29 + '@smithy/util-endpoints': 1.0.7 + '@smithy/util-retry': 2.0.8 + '@smithy/util-stream': 2.0.23 + '@smithy/util-utf8': 2.0.2 + '@smithy/util-waiter': 2.0.15 + fast-xml-parser: 4.2.5 + tslib: 2.6.1 + transitivePeerDependencies: + - aws-crt + dev: false + /@aws-sdk/client-sso@3.398.0: resolution: {integrity: sha512-CygL0jhfibw4kmWXG/3sfZMFNjcXo66XUuPC4BqZBk8Rj5vFoxp1vZeMkDLzTIk97Nvo5J5Bh+QnXKhub6AckQ==} engines: {node: '>=14.0.0'} @@ -1910,6 +1996,51 @@ packages: - aws-crt dev: false + /@aws-sdk/client-sso@3.478.0: + resolution: {integrity: sha512-Jxy9cE1JMkPR0PklCpq3cORHnZq/Z4klhSTNGgZNeBWovMa+plor52kyh8iUNHKl3XEJvTbHM7V+dvrr/x0P1g==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-crypto/sha256-browser': 3.0.0 + '@aws-crypto/sha256-js': 3.0.0 + '@aws-sdk/core': 3.477.0 + '@aws-sdk/middleware-host-header': 3.468.0 + '@aws-sdk/middleware-logger': 3.468.0 + '@aws-sdk/middleware-recursion-detection': 3.468.0 + '@aws-sdk/middleware-user-agent': 3.478.0 + '@aws-sdk/region-config-resolver': 3.470.0 + '@aws-sdk/types': 3.468.0 + '@aws-sdk/util-endpoints': 3.478.0 + '@aws-sdk/util-user-agent-browser': 3.468.0 + '@aws-sdk/util-user-agent-node': 3.470.0 + '@smithy/config-resolver': 2.0.21 + '@smithy/core': 1.2.0 + '@smithy/fetch-http-handler': 2.3.1 + '@smithy/hash-node': 2.0.17 + '@smithy/invalid-dependency': 2.0.15 + '@smithy/middleware-content-length': 2.0.17 + '@smithy/middleware-endpoint': 2.2.3 + '@smithy/middleware-retry': 2.0.24 + '@smithy/middleware-serde': 2.0.15 + '@smithy/middleware-stack': 2.0.9 + '@smithy/node-config-provider': 2.1.8 + '@smithy/node-http-handler': 2.2.1 + '@smithy/protocol-http': 3.0.11 + '@smithy/smithy-client': 2.1.18 + '@smithy/types': 2.7.0 + '@smithy/url-parser': 2.0.15 + '@smithy/util-base64': 2.0.1 + '@smithy/util-body-length-browser': 2.0.1 + '@smithy/util-body-length-node': 2.1.0 + '@smithy/util-defaults-mode-browser': 2.0.22 + '@smithy/util-defaults-mode-node': 2.0.29 + '@smithy/util-endpoints': 1.0.7 + '@smithy/util-retry': 2.0.8 + '@smithy/util-utf8': 2.0.2 + tslib: 2.6.1 + transitivePeerDependencies: + - aws-crt + dev: false + /@aws-sdk/client-sts@3.398.0: resolution: {integrity: sha512-/3Pa9wLMvBZipKraq3AtbmTfXW6q9kyvhwOno64f1Fz7kFb8ijQFMGoATS70B2pGEZTlxkUqJFWDiisT6Q6dFg==} engines: {node: '>=14.0.0'} @@ -2004,6 +2135,54 @@ packages: - aws-crt dev: false + /@aws-sdk/client-sts@3.478.0: + resolution: {integrity: sha512-D+QID0dYzmn9dcxgKP3/nMndUqiQbDLsqI0Zf2pG4MW5gPhVNKlDGIV3Ztz8SkMjzGJExNOLW2L569o8jshJVw==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-crypto/sha256-browser': 3.0.0 + '@aws-crypto/sha256-js': 3.0.0 + '@aws-sdk/core': 3.477.0 + '@aws-sdk/credential-provider-node': 3.478.0 + '@aws-sdk/middleware-host-header': 3.468.0 + '@aws-sdk/middleware-logger': 3.468.0 + '@aws-sdk/middleware-recursion-detection': 3.468.0 + '@aws-sdk/middleware-user-agent': 3.478.0 + '@aws-sdk/region-config-resolver': 3.470.0 + '@aws-sdk/types': 3.468.0 + '@aws-sdk/util-endpoints': 3.478.0 + '@aws-sdk/util-user-agent-browser': 3.468.0 + '@aws-sdk/util-user-agent-node': 3.470.0 + '@smithy/config-resolver': 2.0.21 + '@smithy/core': 1.2.0 + '@smithy/fetch-http-handler': 2.3.1 + '@smithy/hash-node': 2.0.17 + '@smithy/invalid-dependency': 2.0.15 + '@smithy/middleware-content-length': 2.0.17 + '@smithy/middleware-endpoint': 2.2.3 + '@smithy/middleware-retry': 2.0.24 + '@smithy/middleware-serde': 2.0.15 + '@smithy/middleware-stack': 2.0.9 + '@smithy/node-config-provider': 2.1.8 + '@smithy/node-http-handler': 2.2.1 + '@smithy/protocol-http': 3.0.11 + '@smithy/smithy-client': 2.1.18 + '@smithy/types': 2.7.0 + '@smithy/url-parser': 2.0.15 + '@smithy/util-base64': 2.0.1 + '@smithy/util-body-length-browser': 2.0.1 + '@smithy/util-body-length-node': 2.1.0 + '@smithy/util-defaults-mode-browser': 2.0.22 + '@smithy/util-defaults-mode-node': 2.0.29 + '@smithy/util-endpoints': 1.0.7 + '@smithy/util-middleware': 2.0.8 + '@smithy/util-retry': 2.0.8 + '@smithy/util-utf8': 2.0.2 + fast-xml-parser: 4.2.5 + tslib: 2.6.1 + transitivePeerDependencies: + - aws-crt + dev: false + /@aws-sdk/core@3.451.0: resolution: {integrity: sha512-SamWW2zHEf1ZKe3j1w0Piauryl8BQIlej0TBS18A4ACzhjhWXhCs13bO1S88LvPR5mBFXok3XOT6zPOnKDFktw==} engines: {node: '>=14.0.0'} @@ -2012,6 +2191,18 @@ packages: tslib: 2.6.1 dev: false + /@aws-sdk/core@3.477.0: + resolution: {integrity: sha512-o0434EH+d1BxHZvgG7z8vph2SYefciQ5RnJw2MgvETGnthgqsnI4nnNJLSw0FVeqCeS18n6vRtzqlGYR2YPCNg==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/core': 1.2.0 + '@smithy/protocol-http': 3.0.11 + '@smithy/signature-v4': 2.0.5 + '@smithy/smithy-client': 2.1.18 + '@smithy/types': 2.7.0 + tslib: 2.6.1 + dev: false + /@aws-sdk/credential-provider-cognito-identity@3.398.0: resolution: {integrity: sha512-MFUhy1YayHg5ypRTk4OTfDumQRP+OJBagaGv14kA8DzhKH1sNrU4HV7A7y2J4SvkN5hG/KnLJqxpakCtB2/O2g==} engines: {node: '>=14.0.0'} @@ -2047,6 +2238,16 @@ packages: tslib: 2.6.1 dev: false + /@aws-sdk/credential-provider-env@3.468.0: + resolution: {integrity: sha512-k/1WHd3KZn0EQYjadooj53FC0z24/e4dUZhbSKTULgmxyO62pwh9v3Brvw4WRa/8o2wTffU/jo54tf4vGuP/ZA==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.468.0 + '@smithy/property-provider': 2.0.15 + '@smithy/types': 2.7.0 + tslib: 2.6.1 + dev: false + /@aws-sdk/credential-provider-ini@3.398.0: resolution: {integrity: sha512-AsK1lStK3nB9Cn6S6ODb1ktGh7SRejsNVQVKX3t5d3tgOaX+aX1Iwy8FzM/ZEN8uCloeRifUGIY9uQFygg5mSw==} engines: {node: '>=14.0.0'} @@ -2084,6 +2285,24 @@ packages: - aws-crt dev: false + /@aws-sdk/credential-provider-ini@3.478.0: + resolution: {integrity: sha512-SsrYEYUvTG9ZoPC+zB19AnVoOKID+QIEHJDIi1GCZXW5kTVyr1saTVm4orG2TjYvbHQMddsWtHOvGYXZWAYMbw==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/credential-provider-env': 3.468.0 + '@aws-sdk/credential-provider-process': 3.468.0 + '@aws-sdk/credential-provider-sso': 3.478.0 + '@aws-sdk/credential-provider-web-identity': 3.468.0 + '@aws-sdk/types': 3.468.0 + '@smithy/credential-provider-imds': 2.1.2 + '@smithy/property-provider': 2.0.15 + '@smithy/shared-ini-file-loader': 2.2.5 + '@smithy/types': 2.7.0 + tslib: 2.6.1 + transitivePeerDependencies: + - aws-crt + dev: false + /@aws-sdk/credential-provider-node@3.398.0: resolution: {integrity: sha512-odmI/DSKfuWUYeDnGTCEHBbC8/MwnF6yEq874zl6+owoVv0ZsYP8qBHfiJkYqrwg7wQ7Pi40sSAPC1rhesGwzg==} engines: {node: '>=14.0.0'} @@ -2123,6 +2342,25 @@ packages: - aws-crt dev: false + /@aws-sdk/credential-provider-node@3.478.0: + resolution: {integrity: sha512-nwDutJYeHiIZCQDgKIUrsgwAWTil0mNe+cbd+j8fi+wwxkWUzip+F0+z02molJ8WrUUKNRhqB1V5aVx7IranuA==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/credential-provider-env': 3.468.0 + '@aws-sdk/credential-provider-ini': 3.478.0 + '@aws-sdk/credential-provider-process': 3.468.0 + '@aws-sdk/credential-provider-sso': 3.478.0 + '@aws-sdk/credential-provider-web-identity': 3.468.0 + '@aws-sdk/types': 3.468.0 + '@smithy/credential-provider-imds': 2.1.2 + '@smithy/property-provider': 2.0.15 + '@smithy/shared-ini-file-loader': 2.2.5 + '@smithy/types': 2.7.0 + tslib: 2.6.1 + transitivePeerDependencies: + - aws-crt + dev: false + /@aws-sdk/credential-provider-process@3.398.0: resolution: {integrity: sha512-WrkBL1W7TXN508PA9wRXPFtzmGpVSW98gDaHEaa8GolAPHMPa5t2QcC/z/cFpglzrcVv8SA277zu9Z8tELdZhg==} engines: {node: '>=14.0.0'} @@ -2146,6 +2384,17 @@ packages: tslib: 2.6.1 dev: false + /@aws-sdk/credential-provider-process@3.468.0: + resolution: {integrity: sha512-OYSn1A/UsyPJ7Z8Q2cNhTf55O36shPmSsvOfND04nSfu1nPaR+VUvvsP7v+brhGpwC/GAKTIdGAo4blH31BS6A==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.468.0 + '@smithy/property-provider': 2.0.15 + '@smithy/shared-ini-file-loader': 2.2.5 + '@smithy/types': 2.7.0 + tslib: 2.6.1 + dev: false + /@aws-sdk/credential-provider-sso@3.398.0: resolution: {integrity: sha512-2Dl35587xbnzR/GGZqA2MnFs8+kS4wbHQO9BioU0okA+8NRueohNMdrdQmQDdSNK4BfIpFspiZmFkXFNyEAfgw==} engines: {node: '>=14.0.0'} @@ -2177,6 +2426,21 @@ packages: - aws-crt dev: false + /@aws-sdk/credential-provider-sso@3.478.0: + resolution: {integrity: sha512-LsDShG51X/q+s5ZFN7kHVqrd8ZHdyEyHqdhoocmRvvw2Dif50M0AqQfvCrW1ndj5CNzXO4x/eH8EK5ZOVlS6Sg==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/client-sso': 3.478.0 + '@aws-sdk/token-providers': 3.478.0 + '@aws-sdk/types': 3.468.0 + '@smithy/property-provider': 2.0.15 + '@smithy/shared-ini-file-loader': 2.2.5 + '@smithy/types': 2.7.0 + tslib: 2.6.1 + transitivePeerDependencies: + - aws-crt + dev: false + /@aws-sdk/credential-provider-web-identity@3.398.0: resolution: {integrity: sha512-iG3905Alv9pINbQ8/MIsshgqYMbWx+NDQWpxbIW3W0MkSH3iAqdVpSCteYidYX9G/jv2Um1nW3y360ib20bvNg==} engines: {node: '>=14.0.0'} @@ -2198,6 +2462,16 @@ packages: tslib: 2.6.1 dev: false + /@aws-sdk/credential-provider-web-identity@3.468.0: + resolution: {integrity: sha512-rexymPmXjtkwCPfhnUq3EjO1rSkf39R4Jz9CqiM7OsqK2qlT5Y/V3gnMKn0ZMXsYaQOMfM3cT5xly5R+OKDHlw==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.468.0 + '@smithy/property-provider': 2.0.15 + '@smithy/types': 2.7.0 + tslib: 2.6.1 + dev: false + /@aws-sdk/credential-providers@3.398.0: resolution: {integrity: sha512-355vXmImn2e85mIWSYDVb101AF2lIVHKNCaH6sV1U/8i0ZOXh2cJYNdkRYrxNt1ezDB0k97lSKvuDx7RDvJyRg==} engines: {node: '>=14.0.0'} @@ -2222,6 +2496,43 @@ packages: dev: false optional: true + /@aws-sdk/middleware-bucket-endpoint@3.470.0: + resolution: {integrity: sha512-vLXXNWtsRmEIwzJ9HUQfIuTNAsEzvCv0Icsnkvt2BiBZXnmHdp2vIC3e3+kfy1D7dVQloXqMmnfcLu/BUMu2Jw==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.468.0 + '@aws-sdk/util-arn-parser': 3.465.0 + '@smithy/node-config-provider': 2.1.8 + '@smithy/protocol-http': 3.0.11 + '@smithy/types': 2.7.0 + '@smithy/util-config-provider': 2.0.0 + tslib: 2.6.1 + dev: false + + /@aws-sdk/middleware-expect-continue@3.468.0: + resolution: {integrity: sha512-/wmLjmfgeulxhhmnxX3X3N933TvGsYckVIFjAtDSpLjqkbwzEcNiLq7AdmNJ4BfxG0MCMgcht561DCCD19x8Bg==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.468.0 + '@smithy/protocol-http': 3.0.11 + '@smithy/types': 2.7.0 + tslib: 2.6.1 + dev: false + + /@aws-sdk/middleware-flexible-checksums@3.468.0: + resolution: {integrity: sha512-LQwL/N5MCj3Y5keLLewHTqeAXUIMsHFZyxDXRm/uxrOon9ufLKDvGvzAmfwn1/CuSUo66ZfT8VPSA4BsC90RtA==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-crypto/crc32': 3.0.0 + '@aws-crypto/crc32c': 3.0.0 + '@aws-sdk/types': 3.468.0 + '@smithy/is-array-buffer': 2.0.0 + '@smithy/protocol-http': 3.0.11 + '@smithy/types': 2.7.0 + '@smithy/util-utf8': 2.0.2 + tslib: 2.6.1 + dev: false + /@aws-sdk/middleware-host-header@3.398.0: resolution: {integrity: sha512-m+5laWdBaxIZK2ko0OwcCHJZJ5V1MgEIt8QVQ3k4/kOkN9ICjevOYmba751pHoTnbOYB7zQd6D2OT3EYEEsUcA==} engines: {node: '>=14.0.0'} @@ -2243,6 +2554,25 @@ packages: tslib: 2.6.1 dev: false + /@aws-sdk/middleware-host-header@3.468.0: + resolution: {integrity: sha512-gwQ+/QhX+lhof304r6zbZ/V5l5cjhGRxLL3CjH1uJPMcOAbw9wUlMdl+ibr8UwBZ5elfKFGiB1cdW/0uMchw0w==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.468.0 + '@smithy/protocol-http': 3.0.11 + '@smithy/types': 2.7.0 + tslib: 2.6.1 + dev: false + + /@aws-sdk/middleware-location-constraint@3.468.0: + resolution: {integrity: sha512-0gBX/lDynQr4YIhM9h1dVnkVWqrg+34iOCVIUq8jHxzUzgZWglGkG9lHGGg0r1xkLTmegeoo1OKH8wrQ6n33Cg==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.468.0 + '@smithy/types': 2.7.0 + tslib: 2.6.1 + dev: false + /@aws-sdk/middleware-logger@3.398.0: resolution: {integrity: sha512-CiJjW+FL12elS6Pn7/UVjVK8HWHhXMfvHZvOwx/Qkpy340sIhkuzOO6fZEruECDTZhl2Wqn81XdJ1ZQ4pRKpCg==} engines: {node: '>=14.0.0'} @@ -2262,6 +2592,15 @@ packages: tslib: 2.6.1 dev: false + /@aws-sdk/middleware-logger@3.468.0: + resolution: {integrity: sha512-X5XHKV7DHRXI3f29SAhJPe/OxWRFgDWDMMCALfzhmJfCi6Jfh0M14cJKoC+nl+dk9lB+36+jKjhjETZaL2bPlA==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.468.0 + '@smithy/types': 2.7.0 + tslib: 2.6.1 + dev: false + /@aws-sdk/middleware-recursion-detection@3.398.0: resolution: {integrity: sha512-7QpOqPQAZNXDXv6vsRex4R8dLniL0E/80OPK4PPFsrCh9btEyhN9Begh4i1T+5lL28hmYkztLOkTQ2N5J3hgRQ==} engines: {node: '>=14.0.0'} @@ -2283,6 +2622,31 @@ packages: tslib: 2.6.1 dev: false + /@aws-sdk/middleware-recursion-detection@3.468.0: + resolution: {integrity: sha512-vch9IQib2Ng9ucSyRW2eKNQXHUPb5jUPCLA5otTW/8nGjcOU37LxQG4WrxO7uaJ9Oe8hjHO+hViE3P0KISUhtA==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.468.0 + '@smithy/protocol-http': 3.0.11 + '@smithy/types': 2.7.0 + tslib: 2.6.1 + dev: false + + /@aws-sdk/middleware-sdk-s3@3.474.0: + resolution: {integrity: sha512-62aAo/8u5daIabeJ+gseYeHeShe9eYH6mH+kfWmLsHybXCCv1EaD/ZkdXWNhL0HZ3bUI1z1SF1p8jjTAWALnwA==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.468.0 + '@aws-sdk/util-arn-parser': 3.465.0 + '@smithy/node-config-provider': 2.1.8 + '@smithy/protocol-http': 3.0.11 + '@smithy/signature-v4': 2.0.5 + '@smithy/smithy-client': 2.1.18 + '@smithy/types': 2.7.0 + '@smithy/util-config-provider': 2.0.0 + tslib: 2.6.1 + dev: false + /@aws-sdk/middleware-sdk-sts@3.398.0: resolution: {integrity: sha512-+JH76XHEgfVihkY+GurohOQ5Z83zVN1nYcQzwCFnCDTh4dG4KwhnZKG+WPw6XJECocY0R+H0ivofeALHvVWJtQ==} engines: {node: '>=14.0.0'} @@ -2331,6 +2695,28 @@ packages: tslib: 2.6.1 dev: false + /@aws-sdk/middleware-signing@3.468.0: + resolution: {integrity: sha512-s+7fSB1gdnnTj5O0aCCarX3z5Vppop8kazbNSZADdkfHIDWCN80IH4ZNjY3OWqaAz0HmR4LNNrovdR304ojb4Q==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.468.0 + '@smithy/property-provider': 2.0.15 + '@smithy/protocol-http': 3.0.11 + '@smithy/signature-v4': 2.0.5 + '@smithy/types': 2.7.0 + '@smithy/util-middleware': 2.0.8 + tslib: 2.6.1 + dev: false + + /@aws-sdk/middleware-ssec@3.468.0: + resolution: {integrity: sha512-y1qLW24wRkOGBTK5d6eJXf6d8HYo4rzT4a1mNDN1rd18NSffwQ6Yke5qeUiIaxa0y/l+FvvNYErbhYtij2rJoQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.468.0 + '@smithy/types': 2.7.0 + tslib: 2.6.1 + dev: false + /@aws-sdk/middleware-user-agent@3.398.0: resolution: {integrity: sha512-nF1jg0L+18b5HvTcYzwyFgfZQQMELJINFqI0mi4yRKaX7T5a3aGp5RVLGGju/6tAGTuFbfBoEhkhU3kkxexPYQ==} engines: {node: '>=14.0.0'} @@ -2354,6 +2740,17 @@ packages: tslib: 2.6.1 dev: false + /@aws-sdk/middleware-user-agent@3.478.0: + resolution: {integrity: sha512-Rec+nAPIzzwxgHPW+xqY6tooJGFOytpYg/xSRv8/IXl3xKGhmpMGs6gDWzmMBv/qy5nKTvLph/csNWJ98GWXCw==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.468.0 + '@aws-sdk/util-endpoints': 3.478.0 + '@smithy/protocol-http': 3.0.11 + '@smithy/types': 2.7.0 + tslib: 2.6.1 + dev: false + /@aws-sdk/region-config-resolver@3.451.0: resolution: {integrity: sha512-3iMf4OwzrFb4tAAmoROXaiORUk2FvSejnHIw/XHvf/jjR4EqGGF95NZP/n/MeFZMizJWVssrwS412GmoEyoqhg==} engines: {node: '>=14.0.0'} @@ -2365,6 +2762,29 @@ packages: tslib: 2.6.1 dev: false + /@aws-sdk/region-config-resolver@3.470.0: + resolution: {integrity: sha512-C1o1J06iIw8cyAAOvHqT4Bbqf+PgQ/RDlSyjt2gFfP2OovDpc2o2S90dE8f8iZdSGpg70N5MikT1DBhW9NbhtQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/node-config-provider': 2.1.8 + '@smithy/types': 2.7.0 + '@smithy/util-config-provider': 2.0.0 + '@smithy/util-middleware': 2.0.8 + tslib: 2.6.1 + dev: false + + /@aws-sdk/signature-v4-multi-region@3.474.0: + resolution: {integrity: sha512-93OWRQgTJZASXLrlUNX7mmXknNkYxFYldRLARmYQccONmnIqgYQW0lQj8BFwqkHJTzSMik3/UsU0SHKwZ9ynYA==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/middleware-sdk-s3': 3.474.0 + '@aws-sdk/types': 3.468.0 + '@smithy/protocol-http': 3.0.11 + '@smithy/signature-v4': 2.0.5 + '@smithy/types': 2.7.0 + tslib: 2.6.1 + dev: false + /@aws-sdk/token-providers@3.398.0: resolution: {integrity: sha512-nrYgjzavGCKJL/48Vt0EL+OlIc5UZLfNGpgyUW9cv3XZwl+kXV0QB+HH0rHZZLfpbBgZ2RBIJR9uD5ieu/6hpQ==} engines: {node: '>=14.0.0'} @@ -2454,6 +2874,51 @@ packages: - aws-crt dev: false + /@aws-sdk/token-providers@3.478.0: + resolution: {integrity: sha512-7b5tj1y/wGHZIZ+ckjOUKgKrMuCJMF/G1UKZKIqqdekeEsjcThbvoxAMeY0FEowu2ODVk/ggOmpBFxcu0iYd6A==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-crypto/sha256-browser': 3.0.0 + '@aws-crypto/sha256-js': 3.0.0 + '@aws-sdk/middleware-host-header': 3.468.0 + '@aws-sdk/middleware-logger': 3.468.0 + '@aws-sdk/middleware-recursion-detection': 3.468.0 + '@aws-sdk/middleware-user-agent': 3.478.0 + '@aws-sdk/region-config-resolver': 3.470.0 + '@aws-sdk/types': 3.468.0 + '@aws-sdk/util-endpoints': 3.478.0 + '@aws-sdk/util-user-agent-browser': 3.468.0 + '@aws-sdk/util-user-agent-node': 3.470.0 + '@smithy/config-resolver': 2.0.21 + '@smithy/fetch-http-handler': 2.3.1 + '@smithy/hash-node': 2.0.17 + '@smithy/invalid-dependency': 2.0.15 + '@smithy/middleware-content-length': 2.0.17 + '@smithy/middleware-endpoint': 2.2.3 + '@smithy/middleware-retry': 2.0.24 + '@smithy/middleware-serde': 2.0.15 + '@smithy/middleware-stack': 2.0.9 + '@smithy/node-config-provider': 2.1.8 + '@smithy/node-http-handler': 2.2.1 + '@smithy/property-provider': 2.0.15 + '@smithy/protocol-http': 3.0.11 + '@smithy/shared-ini-file-loader': 2.2.5 + '@smithy/smithy-client': 2.1.18 + '@smithy/types': 2.7.0 + '@smithy/url-parser': 2.0.15 + '@smithy/util-base64': 2.0.1 + '@smithy/util-body-length-browser': 2.0.1 + '@smithy/util-body-length-node': 2.1.0 + '@smithy/util-defaults-mode-browser': 2.0.22 + '@smithy/util-defaults-mode-node': 2.0.29 + '@smithy/util-endpoints': 1.0.7 + '@smithy/util-retry': 2.0.8 + '@smithy/util-utf8': 2.0.2 + tslib: 2.6.1 + transitivePeerDependencies: + - aws-crt + dev: false + /@aws-sdk/types@3.398.0: resolution: {integrity: sha512-r44fkS+vsEgKCuEuTV+TIk0t0m5ZlXHNjSDYEUvzLStbbfUFiNus/YG4UCa0wOk9R7VuQI67badsvvPeVPCGDQ==} engines: {node: '>=14.0.0'} @@ -2471,6 +2936,21 @@ packages: tslib: 2.6.1 dev: false + /@aws-sdk/types@3.468.0: + resolution: {integrity: sha512-rx/9uHI4inRbp2tw3Y4Ih4PNZkVj32h7WneSg3MVgVjAoVD5Zti9KhS5hkvsBxfgmQmg0AQbE+b1sy5WGAgntA==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/types': 2.7.0 + tslib: 2.6.1 + dev: false + + /@aws-sdk/util-arn-parser@3.465.0: + resolution: {integrity: sha512-zOJ82vzDJFqBX9yZBlNeHHrul/kpx/DCoxzW5UBbZeb26kfV53QhMSoEmY8/lEbBqlqargJ/sgRC845GFhHNQw==} + engines: {node: '>=14.0.0'} + dependencies: + tslib: 2.6.1 + dev: false + /@aws-sdk/util-endpoints@3.398.0: resolution: {integrity: sha512-Fy0gLYAei/Rd6BrXG4baspCnWTUSd0NdokU1pZh4KlfEAEN1i8SPPgfiO5hLk7+2inqtCmqxVJlfqbMVe9k4bw==} engines: {node: '>=14.0.0'} @@ -2489,6 +2969,15 @@ packages: tslib: 2.6.1 dev: false + /@aws-sdk/util-endpoints@3.478.0: + resolution: {integrity: sha512-u9Mcg3euGJGs5clPt9mBuhBjHiEKiD0PnfvArhfq9i+dcY5mbCq/i1Dezp3iv1fZH9xxQt7hPXDfSpt1yUSM6g==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.468.0 + '@smithy/util-endpoints': 1.0.7 + tslib: 2.6.1 + dev: false + /@aws-sdk/util-locate-window@3.310.0: resolution: {integrity: sha512-qo2t/vBTnoXpjKxlsC2e1gBrRm80M3bId27r0BRB2VniSSe7bL1mmzM+/HFtujm0iAxtPM+aLEflLJlJeDPg0w==} engines: {node: '>=14.0.0'} @@ -2515,6 +3004,15 @@ packages: tslib: 2.6.1 dev: false + /@aws-sdk/util-user-agent-browser@3.468.0: + resolution: {integrity: sha512-OJyhWWsDEizR3L+dCgMXSUmaCywkiZ7HSbnQytbeKGwokIhD69HTiJcibF/sgcM5gk4k3Mq3puUhGnEZ46GIig==} + dependencies: + '@aws-sdk/types': 3.468.0 + '@smithy/types': 2.7.0 + bowser: 2.11.0 + tslib: 2.6.1 + dev: false + /@aws-sdk/util-user-agent-node@3.398.0: resolution: {integrity: sha512-RTVQofdj961ej4//fEkppFf4KXqKGMTCqJYghx3G0C/MYXbg7MGl7LjfNGtJcboRE8pfHHQ/TUWBDA7RIAPPlQ==} engines: {node: '>=14.0.0'} @@ -2546,12 +3044,35 @@ packages: tslib: 2.6.1 dev: false + /@aws-sdk/util-user-agent-node@3.470.0: + resolution: {integrity: sha512-QxsZ9iVHcBB/XRdYvwfM5AMvNp58HfqkIrH88mY0cmxuvtlIGDfWjczdDrZMJk9y0vIq+cuoCHsGXHu7PyiEAQ==} + engines: {node: '>=14.0.0'} + peerDependencies: + aws-crt: '>=1.0.0' + peerDependenciesMeta: + aws-crt: + optional: true + dependencies: + '@aws-sdk/types': 3.468.0 + '@smithy/node-config-provider': 2.1.8 + '@smithy/types': 2.7.0 + tslib: 2.6.1 + dev: false + /@aws-sdk/util-utf8-browser@3.259.0: resolution: {integrity: sha512-UvFa/vR+e19XookZF8RzFZBrw2EUkQWxiBW0yYQAhvk3C+QVGl0H3ouca8LDBlBfQKXwmW3huo/59H8rwb1wJw==} dependencies: tslib: 2.6.1 dev: false + /@aws-sdk/xml-builder@3.472.0: + resolution: {integrity: sha512-PwjVxz1hr9up8QkddabuScPZ/d5aDHgvHYgK4acHYzltXL4wngfvimi5ZqXTzVWF2QANxHmWnHUr45QJX71oJQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/types': 2.7.0 + tslib: 2.6.1 + dev: false + /@azure/abort-controller@1.1.0: resolution: {integrity: sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw==} engines: {node: '>=12.0.0'} @@ -6909,6 +7430,27 @@ packages: tslib: 2.6.1 dev: false + /@smithy/abort-controller@2.0.15: + resolution: {integrity: sha512-JkS36PIS3/UCbq/MaozzV7jECeL+BTt4R75bwY8i+4RASys4xOyUS1HsRyUNSqUXFP4QyCz5aNnh3ltuaxv+pw==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/types': 2.7.0 + tslib: 2.6.1 + dev: false + + /@smithy/chunked-blob-reader-native@2.0.1: + resolution: {integrity: sha512-N2oCZRglhWKm7iMBu7S6wDzXirjAofi7tAd26cxmgibRYOBS4D3hGfmkwCpHdASZzwZDD8rluh0Rcqw1JeZDRw==} + dependencies: + '@smithy/util-base64': 2.0.1 + tslib: 2.6.1 + dev: false + + /@smithy/chunked-blob-reader@2.0.0: + resolution: {integrity: sha512-k+J4GHJsMSAIQPChGBrjEmGS+WbPonCXesoqP9fynIqjn7rdOThdH8FAeCmokP9mxTYKQAKoHCLPzNlm6gh7Wg==} + dependencies: + tslib: 2.6.1 + dev: false + /@smithy/config-resolver@2.0.19: resolution: {integrity: sha512-JsghnQ5zjWmjEVY8TFOulLdEOCj09SjRLugrHlkPZTIBBm7PQitCFVLThbsKPZQOP7N3ME1DU1nKUc1UaVnBog==} engines: {node: '>=14.0.0'} @@ -6920,6 +7462,31 @@ packages: tslib: 2.6.1 dev: false + /@smithy/config-resolver@2.0.21: + resolution: {integrity: sha512-rlLIGT+BeqjnA6C2FWumPRJS1UW07iU5ZxDHtFuyam4W65gIaOFMjkB90ofKCIh+0mLVQrQFrl/VLtQT/6FWTA==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/node-config-provider': 2.1.8 + '@smithy/types': 2.7.0 + '@smithy/util-config-provider': 2.0.0 + '@smithy/util-middleware': 2.0.8 + tslib: 2.6.1 + dev: false + + /@smithy/core@1.2.0: + resolution: {integrity: sha512-l8R89X7+hlt2FEFg+OrNq29LP3h9DfGPmO6ObwT9IXWHD6V7ycpj5u2rVQyIis26ovrgOYakl6nfgmPMm8m1IQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/middleware-endpoint': 2.2.3 + '@smithy/middleware-retry': 2.0.24 + '@smithy/middleware-serde': 2.0.15 + '@smithy/protocol-http': 3.0.11 + '@smithy/smithy-client': 2.1.18 + '@smithy/types': 2.7.0 + '@smithy/util-middleware': 2.0.8 + tslib: 2.6.1 + dev: false + /@smithy/credential-provider-imds@2.1.2: resolution: {integrity: sha512-Y62jBWdoLPSYjr9fFvJf+KwTa1EunjVr6NryTEWCnwIY93OJxwV4t0qxjwdPl/XMsUkq79ppNJSEQN6Ohnhxjw==} engines: {node: '>=14.0.0'} @@ -6931,6 +7498,17 @@ packages: tslib: 2.6.1 dev: false + /@smithy/credential-provider-imds@2.1.4: + resolution: {integrity: sha512-cwPJN1fa1YOQzhBlTXRavABEYRRchci1X79QRwzaNLySnIMJfztyv1Zkst0iZPLMnpn8+CnHu3wOHS11J5Dr3A==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/node-config-provider': 2.1.8 + '@smithy/property-provider': 2.0.16 + '@smithy/types': 2.7.0 + '@smithy/url-parser': 2.0.15 + tslib: 2.6.1 + dev: false + /@smithy/eventstream-codec@2.0.14: resolution: {integrity: sha512-g/OU/MeWGfHDygoXgMWfG/Xb0QqDnAGcM9t2FRrVAhleXYRddGOEnfanR5cmHgB9ue52MJsyorqFjckzXsylaA==} dependencies: @@ -6940,6 +7518,15 @@ packages: tslib: 2.6.1 dev: false + /@smithy/eventstream-codec@2.0.15: + resolution: {integrity: sha512-crjvz3j1gGPwA0us6cwS7+5gAn35CTmqu/oIxVbYJo2Qm/sGAye6zGJnMDk3BKhWZw5kcU1G4MxciTkuBpOZPg==} + dependencies: + '@aws-crypto/crc32': 3.0.0 + '@smithy/types': 2.7.0 + '@smithy/util-hex-encoding': 2.0.0 + tslib: 2.6.1 + dev: false + /@smithy/eventstream-serde-browser@2.0.14: resolution: {integrity: sha512-41wmYE9smDGJi1ZXp+LogH6BR7MkSsQD91wneIFISF/mupKULvoOJUkv/Nf0NMRxWlM3Bf1Vvi9FlR2oV4KU8Q==} engines: {node: '>=14.0.0'} @@ -6949,6 +7536,15 @@ packages: tslib: 2.6.1 dev: false + /@smithy/eventstream-serde-browser@2.0.15: + resolution: {integrity: sha512-WiFG5N9j3jmS5P0z5Xev6dO0c3lf7EJYC2Ncb0xDnWFvShwXNn741AF71ABr5EcZw8F4rQma0362MMjAwJeZog==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/eventstream-serde-universal': 2.0.15 + '@smithy/types': 2.7.0 + tslib: 2.6.1 + dev: false + /@smithy/eventstream-serde-config-resolver@2.0.14: resolution: {integrity: sha512-43IyRIzQ82s+5X+t/3Ood00CcWtAXQdmUIUKMed2Qg9REPk8SVIHhpm3rwewLwg+3G2Nh8NOxXlEQu6DsPUcMw==} engines: {node: '>=14.0.0'} @@ -6957,6 +7553,14 @@ packages: tslib: 2.6.1 dev: false + /@smithy/eventstream-serde-config-resolver@2.0.15: + resolution: {integrity: sha512-o65d2LRjgCbWYH+VVNlWXtmsI231SO99ZTOL4UuIPa6WTjbSHWtlXvUcJG9libhEKWmEV9DIUiH2IqyPWi7ubA==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/types': 2.7.0 + tslib: 2.6.1 + dev: false + /@smithy/eventstream-serde-node@2.0.14: resolution: {integrity: sha512-jVh9E2qAr6DxH5tWfCAl9HV6tI0pEQ3JVmu85JknDvYTC66djcjDdhctPV2EHuKWf2kjRiFJcMIn0eercW4THA==} engines: {node: '>=14.0.0'} @@ -6966,6 +7570,15 @@ packages: tslib: 2.6.1 dev: false + /@smithy/eventstream-serde-node@2.0.15: + resolution: {integrity: sha512-9OOXiIhHq1VeOG6xdHkn2ZayfMYM3vzdUTV3zhcCnt+tMqA3BJK3XXTJFRR2BV28rtRM778DzqbBTf+hqwQPTg==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/eventstream-serde-universal': 2.0.15 + '@smithy/types': 2.7.0 + tslib: 2.6.1 + dev: false + /@smithy/eventstream-serde-universal@2.0.14: resolution: {integrity: sha512-Ie35+AISNn1NmEjn5b2SchIE49pvKp4Q74bE9ME5RULWI1MgXyGkQUajWd5E6OBSr/sqGcs+rD3IjPErXnCm9g==} engines: {node: '>=14.0.0'} @@ -6975,6 +7588,15 @@ packages: tslib: 2.6.1 dev: false + /@smithy/eventstream-serde-universal@2.0.15: + resolution: {integrity: sha512-dP8AQp/pXlWBjvL0TaPBJC3rM0GoYv7O0Uim8d/7UKZ2Wo13bFI3/BhQfY/1DeiP1m23iCHFNFtOQxfQNBB8rQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/eventstream-codec': 2.0.15 + '@smithy/types': 2.7.0 + tslib: 2.6.1 + dev: false + /@smithy/fetch-http-handler@2.2.7: resolution: {integrity: sha512-iSDBjxuH9TgrtMYAr7j5evjvkvgwLY3y+9D547uep+JNkZ1ZT+BaeU20j6I/bO/i26ilCWFImrlXTPsfQtZdIQ==} dependencies: @@ -6985,6 +7607,25 @@ packages: tslib: 2.6.1 dev: false + /@smithy/fetch-http-handler@2.3.1: + resolution: {integrity: sha512-6MNk16fqb8EwcYY8O8WxB3ArFkLZ2XppsSNo1h7SQcFdDDwIumiJeO6wRzm7iB68xvsOQzsdQKbdtTieS3hfSQ==} + dependencies: + '@smithy/protocol-http': 3.0.11 + '@smithy/querystring-builder': 2.0.15 + '@smithy/types': 2.7.0 + '@smithy/util-base64': 2.0.1 + tslib: 2.6.1 + dev: false + + /@smithy/hash-blob-browser@2.0.16: + resolution: {integrity: sha512-cSYRi05LA7DZDwjB1HL0BP8B56eUNNeLglVH147QTXFyuXJq/7erAIiLRfsyXB8+GfFHkSS5BHbc76a7k/AYPA==} + dependencies: + '@smithy/chunked-blob-reader': 2.0.0 + '@smithy/chunked-blob-reader-native': 2.0.1 + '@smithy/types': 2.7.0 + tslib: 2.6.1 + dev: false + /@smithy/hash-node@2.0.16: resolution: {integrity: sha512-Wbi9A0PacMYUOwjAulQP90Wl3mQ6NDwnyrZQzFjDz+UzjXOSyQMgBrTkUBz+pVoYVlX3DUu24gWMZBcit+wOGg==} engines: {node: '>=14.0.0'} @@ -6995,6 +7636,25 @@ packages: tslib: 2.6.1 dev: false + /@smithy/hash-node@2.0.17: + resolution: {integrity: sha512-Il6WuBcI1nD+e2DM7tTADMf01wEPGK8PAhz4D+YmDUVaoBqlA+CaH2uDJhiySifmuKBZj748IfygXty81znKhw==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/types': 2.7.0 + '@smithy/util-buffer-from': 2.0.0 + '@smithy/util-utf8': 2.0.2 + tslib: 2.6.1 + dev: false + + /@smithy/hash-stream-node@2.0.17: + resolution: {integrity: sha512-ey8DtnATzp1mOXgS7rqMwSmAki6iJA+jgNucKcxRkhMB1rrICfHg+rhmIF50iLPDHUhTcS5pBMOrLzzpZftvNQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/types': 2.7.0 + '@smithy/util-utf8': 2.0.2 + tslib: 2.6.1 + dev: false + /@smithy/invalid-dependency@2.0.14: resolution: {integrity: sha512-d8ohpwZo9RzTpGlAfsWtfm1SHBSU7+N4iuZ6MzR10xDTujJJWtmXYHK1uzcr7rggbpUTaWyHpPFgnf91q0EFqQ==} dependencies: @@ -7002,6 +7662,13 @@ packages: tslib: 2.6.1 dev: false + /@smithy/invalid-dependency@2.0.15: + resolution: {integrity: sha512-dlEKBFFwVfzA5QroHlBS94NpgYjXhwN/bFfun+7w3rgxNvVy79SK0w05iGc7UAeC5t+D7gBxrzdnD6hreZnDVQ==} + dependencies: + '@smithy/types': 2.7.0 + tslib: 2.6.1 + dev: false + /@smithy/is-array-buffer@2.0.0: resolution: {integrity: sha512-z3PjFjMyZNI98JFRJi/U0nGoLWMSJlDjAW4QUX2WNZLas5C0CmVV6LJ01JI0k90l7FvpmixjWxPFmENSClQ7ug==} engines: {node: '>=14.0.0'} @@ -7009,6 +7676,14 @@ packages: tslib: 2.6.1 dev: false + /@smithy/md5-js@2.0.17: + resolution: {integrity: sha512-jmISTCnEkOnm2oCNx/rMkvBT/eQh3aA6nktevkzbmn/VYqYEuc5Z2n5sTTqsciMSO01Lvf56wG1A4twDqovYeQ==} + dependencies: + '@smithy/types': 2.7.0 + '@smithy/util-utf8': 2.0.2 + tslib: 2.6.1 + dev: false + /@smithy/middleware-content-length@2.0.16: resolution: {integrity: sha512-9ddDia3pp1d3XzLXKcm7QebGxLq9iwKf+J1LapvlSOhpF8EM9SjMeSrMOOFgG+2TfW5K3+qz4IAJYYm7INYCng==} engines: {node: '>=14.0.0'} @@ -7018,6 +7693,15 @@ packages: tslib: 2.6.1 dev: false + /@smithy/middleware-content-length@2.0.17: + resolution: {integrity: sha512-OyadvMcKC7lFXTNBa8/foEv7jOaqshQZkjWS9coEXPRZnNnihU/Ls+8ZuJwGNCOrN2WxXZFmDWhegbnM4vak8w==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/protocol-http': 3.0.11 + '@smithy/types': 2.7.0 + tslib: 2.6.1 + dev: false + /@smithy/middleware-endpoint@2.2.1: resolution: {integrity: sha512-dVDS7HNJl/wb0lpByXor6whqDbb1YlLoaoWYoelyYzLHioXOE7y/0iDwJWtDcN36/tVCw9EPBFZ3aans84jLpg==} engines: {node: '>=14.0.0'} @@ -7031,6 +7715,19 @@ packages: tslib: 2.6.1 dev: false + /@smithy/middleware-endpoint@2.2.3: + resolution: {integrity: sha512-nYfxuq0S/xoAjdLbyn1ixeVB6cyH9wYCMtbbOCpcCRYR5u2mMtqUtVjjPAZ/DIdlK3qe0tpB0Q76szFGNuz+kQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/middleware-serde': 2.0.15 + '@smithy/node-config-provider': 2.1.8 + '@smithy/shared-ini-file-loader': 2.2.7 + '@smithy/types': 2.7.0 + '@smithy/url-parser': 2.0.15 + '@smithy/util-middleware': 2.0.8 + tslib: 2.6.1 + dev: false + /@smithy/middleware-retry@2.0.21: resolution: {integrity: sha512-EZS1EXv1k6IJX6hyu/0yNQuPcPaXwG8SWljQHYueyRbOxmqYgoWMWPtfZj0xRRQ4YtLawQSpBgAeiJltq8/MPw==} engines: {node: '>=14.0.0'} @@ -7045,6 +7742,21 @@ packages: uuid: 8.3.2 dev: false + /@smithy/middleware-retry@2.0.24: + resolution: {integrity: sha512-q2SvHTYu96N7lYrn3VSuX3vRpxXHR/Cig6MJpGWxd0BWodUQUWlKvXpWQZA+lTaFJU7tUvpKhRd4p4MU3PbeJg==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/node-config-provider': 2.1.8 + '@smithy/protocol-http': 3.0.11 + '@smithy/service-error-classification': 2.0.8 + '@smithy/smithy-client': 2.1.18 + '@smithy/types': 2.7.0 + '@smithy/util-middleware': 2.0.8 + '@smithy/util-retry': 2.0.8 + tslib: 2.6.1 + uuid: 8.3.2 + dev: false + /@smithy/middleware-serde@2.0.14: resolution: {integrity: sha512-hFi3FqoYWDntCYA2IGY6gJ6FKjq2gye+1tfxF2HnIJB5uW8y2DhpRNBSUMoqP+qvYzRqZ6ntv4kgbG+o3pX57g==} engines: {node: '>=14.0.0'} @@ -7053,6 +7765,14 @@ packages: tslib: 2.6.1 dev: false + /@smithy/middleware-serde@2.0.15: + resolution: {integrity: sha512-FOZRFk/zN4AT4wzGuBY+39XWe+ZnCFd0gZtyw3f9Okn2CJPixl9GyWe98TIaljeZdqWkgrzGyPre20AcW2UMHQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/types': 2.7.0 + tslib: 2.6.1 + dev: false + /@smithy/middleware-stack@2.0.8: resolution: {integrity: sha512-7/N59j0zWqVEKExJcA14MrLDZ/IeN+d6nbkN8ucs+eURyaDUXWYlZrQmMOd/TyptcQv0+RDlgag/zSTTV62y/Q==} engines: {node: '>=14.0.0'} @@ -7061,6 +7781,14 @@ packages: tslib: 2.6.1 dev: false + /@smithy/middleware-stack@2.0.9: + resolution: {integrity: sha512-bCB5dUtGQ5wh7QNL2ELxmDc6g7ih7jWU3Kx6MYH1h4mZbv9xL3WyhKHojRltThCB1arLPyTUFDi+x6fB/oabtA==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/types': 2.7.0 + tslib: 2.6.1 + dev: false + /@smithy/node-config-provider@2.1.6: resolution: {integrity: sha512-HLqTs6O78m3M3z1cPLFxddxhEPv5MkVatfPuxoVO3A+cHZanNd/H5I6btcdHy6N2CB1MJ/lihJC92h30SESsBA==} engines: {node: '>=14.0.0'} @@ -7071,6 +7799,16 @@ packages: tslib: 2.6.1 dev: false + /@smithy/node-config-provider@2.1.8: + resolution: {integrity: sha512-+w26OKakaBUGp+UG+dxYZtFb5fs3tgHg3/QrRrmUZj+rl3cIuw840vFUXX35cVPTUCQIiTqmz7CpVF7+hdINdQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/property-provider': 2.0.16 + '@smithy/shared-ini-file-loader': 2.2.7 + '@smithy/types': 2.7.0 + tslib: 2.6.1 + dev: false + /@smithy/node-http-handler@2.1.10: resolution: {integrity: sha512-lkALAwtN6odygIM4nB8aHDahINM6WXXjNrZmWQAh0RSossySRT2qa31cFv0ZBuAYVWeprskRk13AFvvLmf1WLw==} engines: {node: '>=14.0.0'} @@ -7082,6 +7820,17 @@ packages: tslib: 2.6.1 dev: false + /@smithy/node-http-handler@2.2.1: + resolution: {integrity: sha512-8iAKQrC8+VFHPAT8pg4/j6hlsTQh+NKOWlctJBrYtQa4ExcxX7aSg3vdQ2XLoYwJotFUurg/NLqFCmZaPRrogw==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/abort-controller': 2.0.15 + '@smithy/protocol-http': 3.0.11 + '@smithy/querystring-builder': 2.0.15 + '@smithy/types': 2.7.0 + tslib: 2.6.1 + dev: false + /@smithy/property-provider@2.0.15: resolution: {integrity: sha512-YbRFBn8oiiC3o1Kn3a4KjGa6k47rCM9++5W9cWqYn9WnkyH+hBWgfJAckuxpyA2Hq6Ys4eFrWzXq6fqHEw7iew==} engines: {node: '>=14.0.0'} @@ -7090,6 +7839,14 @@ packages: tslib: 2.6.1 dev: false + /@smithy/property-provider@2.0.16: + resolution: {integrity: sha512-28Ky0LlOqtEjwg5CdHmwwaDRHcTWfPRzkT6HrhwOSRS2RryAvuDfJrZpM+BMcrdeCyEg1mbcgIMoqTla+rdL8Q==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/types': 2.7.0 + tslib: 2.6.1 + dev: false + /@smithy/protocol-http@2.0.5: resolution: {integrity: sha512-d2hhHj34mA2V86doiDfrsy2fNTnUOowGaf9hKb0hIPHqvcnShU4/OSc4Uf1FwHkAdYF3cFXTrj5VGUYbEuvMdw==} engines: {node: '>=14.0.0'} @@ -7107,6 +7864,14 @@ packages: tslib: 2.6.1 dev: false + /@smithy/protocol-http@3.0.11: + resolution: {integrity: sha512-3ziB8fHuXIRamV/akp/sqiWmNPR6X+9SB8Xxnozzj+Nq7hSpyKdFHd1FLpBkgfGFUTzzcBJQlDZPSyxzmdcx5A==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/types': 2.7.0 + tslib: 2.6.1 + dev: false + /@smithy/querystring-builder@2.0.14: resolution: {integrity: sha512-lQ4pm9vTv9nIhl5jt6uVMPludr6syE2FyJmHsIJJuOD7QPIJnrf9HhUGf1iHh9KJ4CUv21tpOU3X6s0rB6uJ0g==} engines: {node: '>=14.0.0'} @@ -7116,6 +7881,15 @@ packages: tslib: 2.6.1 dev: false + /@smithy/querystring-builder@2.0.15: + resolution: {integrity: sha512-e1q85aT6HutvouOdN+dMsN0jcdshp50PSCvxDvo6aIM57LqeXimjfONUEgfqQ4IFpYWAtVixptyIRE5frMp/2A==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/types': 2.7.0 + '@smithy/util-uri-escape': 2.0.0 + tslib: 2.6.1 + dev: false + /@smithy/querystring-parser@2.0.14: resolution: {integrity: sha512-+cbtXWI9tNtQjlgQg3CA+pvL3zKTAxPnG3Pj6MP89CR3vi3QMmD0SOWoq84tqZDnJCxlsusbgIXk1ngMReXo+A==} engines: {node: '>=14.0.0'} @@ -7124,6 +7898,14 @@ packages: tslib: 2.6.1 dev: false + /@smithy/querystring-parser@2.0.15: + resolution: {integrity: sha512-jbBvoK3cc81Cj1c1TH1qMYxNQKHrYQ2DoTntN9FBbtUWcGhc+T4FP6kCKYwRLXyU4AajwGIZstvNAmIEgUUNTQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/types': 2.7.0 + tslib: 2.6.1 + dev: false + /@smithy/service-error-classification@2.0.7: resolution: {integrity: sha512-LLxgW12qGz8doYto15kZ4x1rHjtXl0BnCG6T6Wb8z2DI4PT9cJfOSvzbuLzy7+5I24PAepKgFeWHRd9GYy3Z9w==} engines: {node: '>=14.0.0'} @@ -7131,6 +7913,13 @@ packages: '@smithy/types': 2.6.0 dev: false + /@smithy/service-error-classification@2.0.8: + resolution: {integrity: sha512-jCw9+005im8tsfYvwwSc4TTvd29kXRFkH9peQBg5R/4DD03ieGm6v6Hpv9nIAh98GwgYg1KrztcINC1s4o7/hg==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/types': 2.7.0 + dev: false + /@smithy/shared-ini-file-loader@2.2.5: resolution: {integrity: sha512-LHA68Iu7SmNwfAVe8egmjDCy648/7iJR/fK1UnVw+iAOUJoEYhX2DLgVd5pWllqdDiRbQQzgaHLcRokM+UFR1w==} engines: {node: '>=14.0.0'} @@ -7139,6 +7928,14 @@ packages: tslib: 2.6.1 dev: false + /@smithy/shared-ini-file-loader@2.2.7: + resolution: {integrity: sha512-0Qt5CuiogIuvQIfK+be7oVHcPsayLgfLJGkPlbgdbl0lD28nUKu4p11L+UG3SAEsqc9UsazO+nErPXw7+IgDpQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/types': 2.7.0 + tslib: 2.6.1 + dev: false + /@smithy/signature-v4@2.0.5: resolution: {integrity: sha512-ABIzXmUDXK4n2c9cXjQLELgH2RdtABpYKT+U131e2I6RbCypFZmxIHmIBufJzU2kdMCQ3+thBGDWorAITFW04A==} engines: {node: '>=14.0.0'} @@ -7163,6 +7960,16 @@ packages: tslib: 2.6.1 dev: false + /@smithy/smithy-client@2.1.18: + resolution: {integrity: sha512-7FqdbaJiVaHJDD9IfDhmzhSDbpjyx+ZsfdYuOpDJF09rl8qlIAIlZNoSaflKrQ3cEXZN2YxGPaNWGhbYimyIRQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/middleware-stack': 2.0.9 + '@smithy/types': 2.7.0 + '@smithy/util-stream': 2.0.23 + tslib: 2.6.1 + dev: false + /@smithy/types@2.6.0: resolution: {integrity: sha512-PgqxJq2IcdMF9iAasxcqZqqoOXBHufEfmbEUdN1pmJrJltT42b0Sc8UiYSWWzKkciIp9/mZDpzYi4qYG1qqg6g==} engines: {node: '>=14.0.0'} @@ -7170,6 +7977,13 @@ packages: tslib: 2.6.1 dev: false + /@smithy/types@2.7.0: + resolution: {integrity: sha512-1OIFyhK+vOkMbu4aN2HZz/MomREkrAC/HqY5mlJMUJfGrPRwijJDTeiN8Rnj9zUaB8ogXAfIOtZrrgqZ4w7Wnw==} + engines: {node: '>=14.0.0'} + dependencies: + tslib: 2.6.1 + dev: false + /@smithy/url-parser@2.0.14: resolution: {integrity: sha512-kbu17Y1AFXi5lNlySdDj7ZzmvupyWKCX/0jNZ8ffquRyGdbDZb+eBh0QnWqsSmnZa/ctyWaTf7n4l/pXLExrnw==} dependencies: @@ -7178,6 +7992,14 @@ packages: tslib: 2.6.1 dev: false + /@smithy/url-parser@2.0.15: + resolution: {integrity: sha512-sADUncUj9rNbOTrdDGm4EXlUs0eQ9dyEo+V74PJoULY4jSQxS+9gwEgsPYyiu8PUOv16JC/MpHonOgqP/IEDZA==} + dependencies: + '@smithy/querystring-parser': 2.0.15 + '@smithy/types': 2.7.0 + tslib: 2.6.1 + dev: false + /@smithy/util-base64@2.0.1: resolution: {integrity: sha512-DlI6XFYDMsIVN+GH9JtcRp3j02JEVuWIn/QOZisVzpIAprdsxGveFed0bjbMRCqmIFe8uetn5rxzNrBtIGrPIQ==} engines: {node: '>=14.0.0'} @@ -7192,6 +8014,12 @@ packages: tslib: 2.6.1 dev: false + /@smithy/util-body-length-browser@2.0.1: + resolution: {integrity: sha512-NXYp3ttgUlwkaug4bjBzJ5+yIbUbUx8VsSLuHZROQpoik+gRkIBeEG9MPVYfvPNpuXb/puqodeeUXcKFe7BLOQ==} + dependencies: + tslib: 2.6.1 + dev: false + /@smithy/util-body-length-node@2.1.0: resolution: {integrity: sha512-/li0/kj/y3fQ3vyzn36NTLGmUwAICb7Jbe/CsWCktW363gh1MOcpEcSO3mJ344Gv2dqz8YJCLQpb6hju/0qOWw==} engines: {node: '>=14.0.0'} @@ -7225,6 +8053,17 @@ packages: tslib: 2.6.1 dev: false + /@smithy/util-defaults-mode-browser@2.0.22: + resolution: {integrity: sha512-qcF20IHHH96FlktvBRICDXDhLPtpVmtksHmqNGtotb9B0DYWXsC6jWXrkhrrwF7tH26nj+npVTqh9isiFV1gdA==} + engines: {node: '>= 10.0.0'} + dependencies: + '@smithy/property-provider': 2.0.16 + '@smithy/smithy-client': 2.1.18 + '@smithy/types': 2.7.0 + bowser: 2.11.0 + tslib: 2.6.1 + dev: false + /@smithy/util-defaults-mode-node@2.0.26: resolution: {integrity: sha512-lGFPOFCHv1ql019oegYqa54BZH7HREw6EBqjDLbAr0wquMX0BDi2sg8TJ6Eq+JGLijkZbJB73m4+aK8OFAapMg==} engines: {node: '>= 10.0.0'} @@ -7238,6 +8077,19 @@ packages: tslib: 2.6.1 dev: false + /@smithy/util-defaults-mode-node@2.0.29: + resolution: {integrity: sha512-+uG/15VoUh6JV2fdY9CM++vnSuMQ1VKZ6BdnkUM7R++C/vLjnlg+ToiSR1FqKZbMmKBXmsr8c/TsDWMAYvxbxQ==} + engines: {node: '>= 10.0.0'} + dependencies: + '@smithy/config-resolver': 2.0.21 + '@smithy/credential-provider-imds': 2.1.4 + '@smithy/node-config-provider': 2.1.8 + '@smithy/property-provider': 2.0.16 + '@smithy/smithy-client': 2.1.18 + '@smithy/types': 2.7.0 + tslib: 2.6.1 + dev: false + /@smithy/util-endpoints@1.0.5: resolution: {integrity: sha512-K7qNuCOD5K/90MjHvHm9kJldrfm40UxWYQxNEShMFxV/lCCCRIg8R4uu1PFAxRvPxNpIdcrh1uK6I1ISjDXZJw==} engines: {node: '>= 14.0.0'} @@ -7247,6 +8099,15 @@ packages: tslib: 2.6.1 dev: false + /@smithy/util-endpoints@1.0.7: + resolution: {integrity: sha512-Q2gEind3jxoLk6hdKWyESMU7LnXz8aamVwM+VeVjOYzYT1PalGlY/ETa48hv2YpV4+YV604y93YngyzzzQ4IIA==} + engines: {node: '>= 14.0.0'} + dependencies: + '@smithy/node-config-provider': 2.1.8 + '@smithy/types': 2.7.0 + tslib: 2.6.1 + dev: false + /@smithy/util-hex-encoding@2.0.0: resolution: {integrity: sha512-c5xY+NUnFqG6d7HFh1IFfrm3mGl29lC+vF+geHv4ToiuJCBmIfzx6IeHLg+OgRdPFKDXIw6pvi+p3CsscaMcMA==} engines: {node: '>=14.0.0'} @@ -7262,6 +8123,14 @@ packages: tslib: 2.6.1 dev: false + /@smithy/util-middleware@2.0.8: + resolution: {integrity: sha512-qkvqQjM8fRGGA8P2ydWylMhenCDP8VlkPn8kiNuFEaFz9xnUKC2irfqsBSJrfrOB9Qt6pQsI58r3zvvumhFMkw==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/types': 2.7.0 + tslib: 2.6.1 + dev: false + /@smithy/util-retry@2.0.7: resolution: {integrity: sha512-fIe5yARaF0+xVT1XKcrdnHKTJ1Vc4+3e3tLDjCuIcE9b6fkBzzGFY7AFiX4M+vj6yM98DrwkuZeHf7/hmtVp0Q==} engines: {node: '>= 14.0.0'} @@ -7271,6 +8140,15 @@ packages: tslib: 2.6.1 dev: false + /@smithy/util-retry@2.0.8: + resolution: {integrity: sha512-cQTPnVaVFMjjS6cb44WV2yXtHVyXDC5icKyIbejMarJEApYeJWpBU3LINTxHqp/tyLI+MZOUdosr2mZ3sdziNg==} + engines: {node: '>= 14.0.0'} + dependencies: + '@smithy/service-error-classification': 2.0.8 + '@smithy/types': 2.7.0 + tslib: 2.6.1 + dev: false + /@smithy/util-stream@2.0.21: resolution: {integrity: sha512-0BUE16d7n1x7pi1YluXJdB33jOTyBChT0j/BlOkFa9uxfg6YqXieHxjHNuCdJRARa7AZEj32LLLEPJ1fSa4inA==} engines: {node: '>=14.0.0'} @@ -7285,6 +8163,20 @@ packages: tslib: 2.6.1 dev: false + /@smithy/util-stream@2.0.23: + resolution: {integrity: sha512-OJMWq99LAZJUzUwTk+00plyxX3ESktBaGPhqNIEVab+53gLULiWN9B/8bRABLg0K6R6Xg4t80uRdhk3B/LZqMQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/fetch-http-handler': 2.3.1 + '@smithy/node-http-handler': 2.2.1 + '@smithy/types': 2.7.0 + '@smithy/util-base64': 2.0.1 + '@smithy/util-buffer-from': 2.0.0 + '@smithy/util-hex-encoding': 2.0.0 + '@smithy/util-utf8': 2.0.2 + tslib: 2.6.1 + dev: false + /@smithy/util-uri-escape@2.0.0: resolution: {integrity: sha512-ebkxsqinSdEooQduuk9CbKcI+wheijxEb3utGXkCoYQkJnwTnLbH1JXGimJtUkQwNQbsbuYwG2+aFVyZf5TLaw==} engines: {node: '>=14.0.0'} @@ -7300,6 +8192,15 @@ packages: tslib: 2.6.1 dev: false + /@smithy/util-waiter@2.0.15: + resolution: {integrity: sha512-9Y+btzzB7MhLADW7xgD6SjvmoYaRkrb/9SCbNGmNdfO47v38rxb90IGXyDtAK0Shl9bMthTmLgjlfYc+vtz2Qw==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/abort-controller': 2.0.15 + '@smithy/types': 2.7.0 + tslib: 2.6.1 + dev: false + /@sqltools/formatter@1.2.5: resolution: {integrity: sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==} dev: false @@ -8832,8 +9733,8 @@ packages: resolution: {integrity: sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==} dev: false - /@techteamer/ocsp@1.0.0: - resolution: {integrity: sha512-lNAOoFHaZN+4huo30ukeqVrUmfC+avoEBYQ11QAnAw1PFhnI5oBCg8O/TNiCoEWix7gNGBIEjrQwtPREqKMPog==} + /@techteamer/ocsp@1.0.1: + resolution: {integrity: sha512-q4pW5wAC6Pc3JI8UePwE37CkLQ5gDGZMgjSX4MEEm4D4Di59auDQ8UNIDzC4gRnPNmmcwjpPxozq8p5pjiOmOw==} dependencies: asn1.js: 5.4.1 asn1.js-rfc2560: 5.0.1(asn1.js@5.4.1) @@ -9586,8 +10487,8 @@ packages: resolution: {integrity: sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==} dev: true - /@types/snowflake-sdk@1.6.12: - resolution: {integrity: sha512-Ndz6x750TkuvHvdRBH8Cb7T8et2anyyY1j8kFChJ+s8FtURNRKut7DWaq9YdseKXZvqwA3ZYZxKaQRwtSq67eg==} + /@types/snowflake-sdk@1.6.20: + resolution: {integrity: sha512-Dr7oIXrWthlk9wVWpZgpm49BT8cFFXz43u7SkJKyiZK3WHiHQo4b+m2/p3WIpkYzZCcOZZ/t1B09XMd7+u1Wjw==} dependencies: '@types/node': 18.16.16 generic-pool: 3.9.0 @@ -10554,7 +11455,6 @@ packages: debug: 4.3.4(supports-color@8.1.1) transitivePeerDependencies: - supports-color - dev: true /agentkeepalive@4.2.1: resolution: {integrity: sha512-Zn4cw2NEqd+9fiSVWMscnjyQ1a8Yfoc5oBajLeo5w+YBHgDUcEBY2hS4YpTz6iN5f/2zQiktcuM6tS8x1p9dpA==} @@ -11115,22 +12015,6 @@ packages: engines: {node: '>=0.11'} dev: false - /aws-sdk@2.1231.0: - resolution: {integrity: sha512-ONBuRsOxsu0zL8u/Vmz49tPWi9D4ls2pjb6szdfSx9VQef7bOnWe9gJpWoA94OTzcjOWsvjsG7UgjvQJkIuPBg==} - engines: {node: '>= 10.0.0'} - dependencies: - buffer: 4.9.2 - events: 1.1.1 - ieee754: 1.1.13 - jmespath: 0.16.0 - querystring: 0.2.0 - sax: 1.2.1 - url: 0.10.3 - util: 0.12.5 - uuid: 8.0.0 - xml2js: 0.5.0 - dev: false - /aws-sign2@0.7.0: resolution: {integrity: sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==} dev: true @@ -11166,27 +12050,28 @@ packages: - debug dev: false - /axios@0.27.2(debug@3.2.7): + /axios@0.27.2(debug@4.3.4): resolution: {integrity: sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==} dependencies: - follow-redirects: 1.15.2(debug@3.2.7) + follow-redirects: 1.15.2(debug@4.3.4) form-data: 4.0.0 transitivePeerDependencies: - debug - dev: false - /axios@0.27.2(debug@4.3.4): - resolution: {integrity: sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==} + /axios@1.6.2: + resolution: {integrity: sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==} dependencies: follow-redirects: 1.15.2(debug@4.3.4) form-data: 4.0.0 + proxy-from-env: 1.1.0 transitivePeerDependencies: - debug + dev: false - /axios@1.6.2: + /axios@1.6.2(debug@3.2.7): resolution: {integrity: sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==} dependencies: - follow-redirects: 1.15.2(debug@4.3.4) + follow-redirects: 1.15.2(debug@3.2.7) form-data: 4.0.0 proxy-from-env: 1.1.0 transitivePeerDependencies: @@ -11389,14 +12274,14 @@ packages: resolution: {integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==} dev: true - /bignumber.js@2.4.0: - resolution: {integrity: sha512-uw4ra6Cv483Op/ebM0GBKKfxZlSmn6NgFRby5L3yGTlunLj53KQgndDlqy2WVFOwgvurocApYkSud0aO+mvrpQ==} - dev: false - /bignumber.js@9.1.1: resolution: {integrity: sha512-pHm4LsMJ6lzgNGVfZHjMoO8sdoRhOzOH4MLmY65Jg70bpxCKu5iOHNJyfF6OyvYw7t8Fpf35RuzUyqnQsj8Vig==} dev: false + /bignumber.js@9.1.2: + resolution: {integrity: sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==} + dev: false + /binary-extensions@2.2.0: resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} engines: {node: '>=8'} @@ -11591,14 +12476,6 @@ packages: engines: {node: '>=4'} dev: false - /buffer@4.9.2: - resolution: {integrity: sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==} - dependencies: - base64-js: 1.5.1 - ieee754: 1.2.1 - isarray: 1.0.0 - dev: false - /buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} dependencies: @@ -12504,10 +13381,6 @@ packages: toggle-selection: 1.0.6 dev: false - /copy-to@2.0.1: - resolution: {integrity: sha512-3DdaFaU/Zf1AnpLiFDeNCD4TOWe3Zl2RZaTzUvWiIk5ERzcCodOE20Vqq4fzCbNoHURFHT4/us/Lfq+S2zyY4w==} - dev: false - /core-js-compat@3.32.0: resolution: {integrity: sha512-7a9a3D1k4UCVKnLhrgALyFcP7YCsLOQIxPd0dKjf/6GuPcgyiGP70ewWdCGrSK7evyhymi0qO4EqCmSJofDeYw==} dependencies: @@ -13012,13 +13885,6 @@ packages: engines: {node: '>= 0.10'} dev: true - /default-user-agent@1.0.0: - resolution: {integrity: sha512-bDF7bg6OSNcSwFWPu4zYKpVkJZQYVrAANMYB8bc9Szem1D0yKdm4sa/rOCs2aC9+2GMqQ7KnwtZRvDhmLF0dXw==} - engines: {node: '>= 0.10.0'} - dependencies: - os-name: 1.0.3 - dev: false - /defaults@1.0.4: resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} dependencies: @@ -13192,11 +14058,6 @@ packages: md5: 2.3.0 dev: false - /digest-header@1.1.0: - resolution: {integrity: sha512-glXVh42vz40yZb9Cq2oMOt70FIoWiv+vxNvdKdU8CwjLad25qHM3trLxhl9bVjdr6WaslIXhWpn0NO8T/67Qjg==} - engines: {node: '>= 8.0.0'} - dev: false - /dingbat-to-unicode@1.0.1: resolution: {integrity: sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w==} dev: false @@ -14212,11 +15073,6 @@ packages: resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} dev: false - /events@1.1.1: - resolution: {integrity: sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw==} - engines: {node: '>=0.4.x'} - dev: false - /events@3.3.0: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} @@ -14446,6 +15302,7 @@ packages: engines: {node: '>=0.10.0'} dependencies: is-extendable: 0.1.1 + dev: true /extend-shallow@3.0.2: resolution: {integrity: sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==} @@ -14592,6 +15449,11 @@ packages: strnum: 1.0.5 dev: false + /fastest-levenshtein@1.0.16: + resolution: {integrity: sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==} + engines: {node: '>= 4.9.1'} + dev: false + /fastq@1.15.0: resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==} dependencies: @@ -14967,14 +15829,6 @@ packages: once: 1.4.0 dev: false - /formstream@1.1.1: - resolution: {integrity: sha512-yHRxt3qLFnhsKAfhReM4w17jP+U1OlhUjnKPPtonwKbIJO7oBP0MvoxkRUwb8AU9n0MIkYy5X5dK6pQnbj+R2Q==} - dependencies: - destroy: 1.2.0 - mime: 2.6.0 - pause-stream: 0.0.11 - dev: false - /forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} @@ -15905,7 +16759,6 @@ packages: debug: 4.3.4(supports-color@8.1.1) transitivePeerDependencies: - supports-color - dev: true /human-signals@1.1.1: resolution: {integrity: sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==} @@ -15967,10 +16820,6 @@ packages: postcss: 7.0.39 dev: true - /ieee754@1.1.13: - resolution: {integrity: sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==} - dev: false - /ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} @@ -16192,10 +17041,6 @@ packages: - supports-color dev: false - /ip@1.1.8: - resolution: {integrity: sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg==} - dev: false - /ip@2.0.0: resolution: {integrity: sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==} @@ -16357,6 +17202,7 @@ packages: /is-extendable@0.1.1: resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} engines: {node: '>=0.10.0'} + dev: true /is-extendable@1.0.1: resolution: {integrity: sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==} @@ -20165,15 +21011,6 @@ packages: lcid: 1.0.0 dev: true - /os-name@1.0.3: - resolution: {integrity: sha512-f5estLO2KN8vgtTRaILIgEGBoBrMnZ3JQ7W9TMZCnOIGwHe8TRGSpcagnWDo+Dfhd/z08k9Xe75hvciJJ8Qaew==} - engines: {node: '>=0.10.0'} - hasBin: true - dependencies: - osx-release: 1.1.0 - win-release: 1.1.1 - dev: false - /os-tmpdir@1.0.2: resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} engines: {node: '>=0.10.0'} @@ -20182,14 +21019,6 @@ packages: resolution: {integrity: sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA==} dev: true - /osx-release@1.1.0: - resolution: {integrity: sha512-ixCMMwnVxyHFQLQnINhmIpWqXIfS2YOXchwQrk+OFzmo6nDjQ0E4KXAyyUh0T0MZgV4bUhkRrAbVqlE4yLVq4A==} - engines: {node: '>=0.10.0'} - hasBin: true - dependencies: - minimist: 1.2.8 - dev: false - /otpauth@9.1.1: resolution: {integrity: sha512-XhimxmkREwf6GJvV4svS9OVMFJ/qRGz+QBEGwtW5OMf9jZlx9yw25RZMXdrO6r7DHgfIaETJb1lucZXZtn3jgw==} dependencies: @@ -20573,6 +21402,7 @@ packages: resolution: {integrity: sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==} dependencies: through: 2.3.8 + dev: true /pause@0.0.1: resolution: {integrity: sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==} @@ -21326,6 +22156,7 @@ packages: dependencies: end-of-stream: 1.4.4 once: 1.4.0 + dev: true /pumpify@1.5.1: resolution: {integrity: sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==} @@ -21335,10 +22166,6 @@ packages: pump: 2.0.1 dev: true - /punycode@1.3.2: - resolution: {integrity: sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==} - dev: false - /punycode@2.2.0: resolution: {integrity: sha512-LN6QV1IJ9ZhxWTNdktaPClrNfp8xdSAYS0Zk2ddX7XsXZAxckMHPCBcHRo0cTcEIgYPRiGEkmji3Idkh2yFtYw==} engines: {node: '>=6'} @@ -21444,12 +22271,6 @@ packages: strict-uri-encode: 2.0.0 dev: false - /querystring@0.2.0: - resolution: {integrity: sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==} - engines: {node: '>=0.4.x'} - deprecated: The querystring API is considered Legacy. new code should use the URLSearchParams API instead. - dev: false - /querystringify@2.2.0: resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} @@ -22304,10 +23125,6 @@ packages: source-map-js: 1.0.2 dev: true - /sax@1.2.1: - resolution: {integrity: sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==} - dev: false - /sax@1.2.4: resolution: {integrity: sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==} dev: false @@ -22650,19 +23467,22 @@ packages: - supports-color dev: true - /snowflake-sdk@1.8.0(asn1.js@5.4.1): - resolution: {integrity: sha512-zdU1c+ytIZclF4K6D4XPPHa5II6l6cOQdsLdvKP95IwSdTYJz324ESA7fPcg/rwYV7vUKnIZJ9OCjB1mE7D2IQ==} + /snowflake-sdk@1.9.2(asn1.js@5.4.1): + resolution: {integrity: sha512-pPTE8V6RHw78KBAn56rGVri3LRciuJDz/oDUmkJej3fUnOHP28YBLl3kjiW0J5bbPzDPHhSAkeeCsjrbGyAB/w==} + peerDependencies: + asn1.js: ^5.4.1 dependencies: + '@aws-sdk/client-s3': 3.478.0 '@azure/storage-blob': 12.11.0 '@google-cloud/storage': 6.11.0 - '@techteamer/ocsp': 1.0.0 + '@techteamer/ocsp': 1.0.1 agent-base: 6.0.2 + asn1.js: 5.4.1 asn1.js-rfc2560: 5.0.1(asn1.js@5.4.1) asn1.js-rfc5280: 3.0.0 - aws-sdk: 2.1231.0 - axios: 0.27.2(debug@3.2.7) + axios: 1.6.2(debug@3.2.7) big-integer: 1.6.51 - bignumber.js: 2.4.0 + bignumber.js: 9.1.2 binascii: 0.0.2 bn.js: 5.2.1 browser-request: 0.3.3 @@ -22670,9 +23490,10 @@ packages: expand-tilde: 2.0.2 extend: 3.0.2 fast-xml-parser: 4.2.7 + fastest-levenshtein: 1.0.16 generic-pool: 3.9.0 - glob: 7.2.3 - https-proxy-agent: 5.0.1 + glob: 9.3.2 + https-proxy-agent: 7.0.2 jsonwebtoken: 9.0.0 mime-types: 2.1.35 mkdirp: 1.0.4 @@ -22681,15 +23502,12 @@ packages: open: 7.4.2 python-struct: 1.1.3 simple-lru-cache: 0.0.2 - string-similarity: 4.0.4 tmp: 0.2.1 - urllib: 2.41.0 - uuid: 3.4.0 + uuid: 8.3.2 winston: 3.8.2 transitivePeerDependencies: - - asn1.js + - aws-crt - encoding - - proxy-agent - supports-color dev: false @@ -22956,11 +23774,6 @@ packages: object-copy: 0.1.0 dev: true - /statuses@1.5.0: - resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==} - engines: {node: '>= 0.6'} - dev: false - /statuses@2.0.1: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} @@ -23100,11 +23913,6 @@ packages: strip-ansi: 6.0.1 dev: true - /string-similarity@4.0.4: - resolution: {integrity: sha512-/q/8Q4Bl4ZKAPjj8WerIBJWALKkaPRfrvhfF8k/B23i4nzrlRj2/go1m90In7nG/3XDSbOo0+pu6RvCTM9RGMQ==} - deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. - dev: false - /string-width@1.0.2: resolution: {integrity: sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==} engines: {node: '>=0.10.0'} @@ -24380,13 +25188,6 @@ packages: '@fastify/busboy': 2.0.0 dev: false - /unescape@1.0.1: - resolution: {integrity: sha512-O0+af1Gs50lyH1nUu3ZyYS1cRh01Q/kUKatTOkSs7jukXE6/NebucDVxyiDsA9AQ4JC1V1jUH9EO8JX2nMDgGQ==} - engines: {node: '>=0.10.0'} - dependencies: - extend-shallow: 2.0.1 - dev: false - /unicode-canonical-property-names-ecmascript@2.0.0: resolution: {integrity: sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==} engines: {node: '>=4'} @@ -24597,40 +25398,6 @@ packages: engines: {node: '>=6.0.0'} dev: false - /url@0.10.3: - resolution: {integrity: sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ==} - dependencies: - punycode: 1.3.2 - querystring: 0.2.0 - dev: false - - /urllib@2.41.0: - resolution: {integrity: sha512-pNXdxEv52L67jahLT+/7QE+Fup1y2Gc6EdmrAhQ6OpQIC2rl14oWwv9hvk1GXOZqEnJNwRXHABuwgPOs1CtL7g==} - engines: {node: '>= 0.10.0'} - peerDependencies: - proxy-agent: ^5.0.0 - peerDependenciesMeta: - proxy-agent: - optional: true - dependencies: - any-promise: 1.3.0 - content-type: 1.0.4 - debug: 2.6.9 - default-user-agent: 1.0.0 - digest-header: 1.1.0 - ee-first: 1.1.1 - formstream: 1.1.1 - humanize-ms: 1.2.1 - iconv-lite: 0.4.24 - ip: 1.1.8 - pump: 3.0.0 - qs: 6.11.0 - statuses: 1.5.0 - utility: 1.17.0 - transitivePeerDependencies: - - supports-color - dev: false - /use-callback-ref@1.3.0(react@18.2.0): resolution: {integrity: sha512-3FT9PRuRdbB9HfXhEq35u4oZkvpJ5kuYbpqhCfmiZyReuRgpnhDlbr2ZEnnuS0RrJAPn6l23xjFg9kpDM+Ms7w==} engines: {node: '>=10'} @@ -24717,17 +25484,6 @@ packages: is-typed-array: 1.1.10 which-typed-array: 1.1.11 - /utility@1.17.0: - resolution: {integrity: sha512-KdVkF9An/0239BJ4+dqOa7NPrPIOeQE9AGfx0XS16O9DBiHNHRJMoeU5nL6pRGAkgJOqdOu8R4gBRcXnAocJKw==} - engines: {node: '>= 0.12.0'} - dependencies: - copy-to: 2.0.1 - escape-html: 1.0.3 - mkdirp: 0.5.6 - mz: 2.7.0 - unescape: 1.0.1 - dev: false - /utils-merge@1.0.1: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} @@ -24736,17 +25492,6 @@ packages: resolution: {integrity: sha512-yEEhCuCi5wRV7Z5ZVf9iV2gWMvUZqKJhAs1ecFdKJ0qzbyaVelmsE3QjYAamehfp9FKLiZbKldd+jklG3O0LfA==} dev: false - /uuid@3.4.0: - resolution: {integrity: sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==} - deprecated: Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details. - hasBin: true - dev: false - - /uuid@8.0.0: - resolution: {integrity: sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==} - hasBin: true - dev: false - /uuid@8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} hasBin: true @@ -25422,13 +26167,6 @@ packages: dependencies: string-width: 4.2.3 - /win-release@1.1.1: - resolution: {integrity: sha512-iCRnKVvGxOQdsKhcQId2PXV1vV3J/sDPXKA4Oe9+Eti2nb2ESEsYHRYls/UjoUW3bIc5ZDO8dTH50A/5iVN+bw==} - engines: {node: '>=0.10.0'} - dependencies: - semver: 7.5.4 - dev: false - /winston-transport@4.5.0: resolution: {integrity: sha512-YpZzcUzBedhlTAfJg6vJDlyEai/IFMIVcaEZZyl3UXIl4gmqRpU7AE89AHLkbzLUsv0NVmw7ts+iztqKxxPW1Q==} engines: {node: '>= 6.4.0'} From e9c7fd73975ced504d5a16a0dbbc313f15ccd8ab Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Thu, 21 Dec 2023 09:06:54 -0500 Subject: [PATCH 13/32] fix: Show public API upgrade CTA when feature is not enabled (#8109) ## Summary > Describe what the PR does and how to test. Photos and videos are recommended. Shows the public API upgrade CTA when the feature is not enabled. Now trialing users in cloud would see the API on the settings menu and can upgrade from there. When public API feature disabled: image When public API feature enabled with no API key: image When public API feature enabled with API key: image ## Related tickets and issues [> Include links to **Linear ticket** or Github issue or Community forum post. Important in order to close *automatically* and provide context to r](https://linear.app/n8n/issue/ADO-1282/feature-api-page-missing-for-trial-users)eviewers. ## Review / Merge checklist - [x] PR title and summary are descriptive. **Remember, the title automatically goes into the changelog. Use `(no-changelog)` otherwise.** ([conventions](https://github.com/n8n-io/n8n/blob/master/.github/pull_request_title_conventions.md)) - [x] Tests included. > A bug is not considered fixed, unless a test is added to prevent it from happening again. > A feature is not complete without tests. --- cypress/e2e/27-cloud.cy.ts | 119 ++++++++++++++++++ cypress/e2e/27-opt-in-trial-banner.cy.ts | 83 ------------ cypress/fixtures/Plan_data_opt_in_trial.json | 3 +- cypress/pages/index.ts | 1 + cypress/pages/settings-public-api.ts | 5 + packages/editor-ui/src/router.ts | 8 +- .../editor-ui/src/views/SettingsApiView.vue | 8 +- 7 files changed, 134 insertions(+), 93 deletions(-) create mode 100644 cypress/e2e/27-cloud.cy.ts delete mode 100644 cypress/e2e/27-opt-in-trial-banner.cy.ts create mode 100644 cypress/pages/settings-public-api.ts diff --git a/cypress/e2e/27-cloud.cy.ts b/cypress/e2e/27-cloud.cy.ts new file mode 100644 index 0000000000000..965bc5bccfedf --- /dev/null +++ b/cypress/e2e/27-cloud.cy.ts @@ -0,0 +1,119 @@ +import { + BannerStack, + MainSidebar, + WorkflowPage, + visitPublicApiPage, + getPublicApiUpgradeCTA, +} from '../pages'; +import planData from '../fixtures/Plan_data_opt_in_trial.json'; +import { INSTANCE_OWNER } from '../constants'; + +const mainSidebar = new MainSidebar(); +const bannerStack = new BannerStack(); +const workflowPage = new WorkflowPage(); + +describe('Cloud', { disableAutoLogin: true }, () => { + before(() => { + const now = new Date(); + const fiveDaysFromNow = new Date(now.getTime() + 5 * 24 * 60 * 60 * 1000); + planData.expirationDate = fiveDaysFromNow.toJSON(); + }); + + describe('BannerStack', () => { + it('should render trial banner for opt-in cloud user', () => { + cy.intercept('GET', '/rest/admin/cloud-plan', { + body: planData, + }).as('getPlanData'); + + cy.intercept('GET', '/rest/settings', (req) => { + req.on('response', (res) => { + res.send({ + data: { ...res.body.data, deployment: { type: 'cloud' }, n8nMetadata: { userId: 1 } }, + }); + }); + }).as('loadSettings'); + + cy.signin({ email: INSTANCE_OWNER.email, password: INSTANCE_OWNER.password }); + + cy.visit(workflowPage.url); + + cy.wait('@getPlanData'); + + bannerStack.getters.banner().should('be.visible'); + + mainSidebar.actions.signout(); + + bannerStack.getters.banner().should('not.be.visible'); + + cy.signin({ email: INSTANCE_OWNER.email, password: INSTANCE_OWNER.password }); + + cy.visit(workflowPage.url); + + bannerStack.getters.banner().should('be.visible'); + + mainSidebar.actions.signout(); + }); + + it('should not render opt-in-trial banner for non cloud deployment', () => { + cy.intercept('GET', '/rest/settings', (req) => { + req.on('response', (res) => { + res.send({ + data: { ...res.body.data, deployment: { type: 'default' } }, + }); + }); + }).as('loadSettings'); + + cy.signin({ email: INSTANCE_OWNER.email, password: INSTANCE_OWNER.password }); + + cy.visit(workflowPage.url); + + bannerStack.getters.banner().should('not.be.visible'); + + mainSidebar.actions.signout(); + }); + }); + + describe('Admin Home', () => { + it('Should show admin button', () => { + cy.intercept('GET', '/rest/settings', (req) => { + req.on('response', (res) => { + res.send({ + data: { ...res.body.data, deployment: { type: 'cloud' }, n8nMetadata: { userId: 1 } }, + }); + }); + }).as('loadSettings'); + + cy.signin({ email: INSTANCE_OWNER.email, password: INSTANCE_OWNER.password }); + + cy.visit(workflowPage.url); + + mainSidebar.getters.adminPanel().should('be.visible'); + }); + }); + + describe('Public API', () => { + it('Should show upgrade CTA for Public API if user is trialing', () => { + cy.intercept('GET', '/rest/admin/cloud-plan', { + body: planData, + }).as('getPlanData'); + + cy.intercept('GET', '/rest/settings', (req) => { + req.on('response', (res) => { + res.send({ + data: { + ...res.body.data, + deployment: { type: 'cloud' }, + n8nMetadata: { userId: 1 }, + }, + }); + }); + }).as('loadSettings'); + + cy.signin({ email: INSTANCE_OWNER.email, password: INSTANCE_OWNER.password }); + + visitPublicApiPage(); + + getPublicApiUpgradeCTA().should('be.visible'); + }); + }); +}); diff --git a/cypress/e2e/27-opt-in-trial-banner.cy.ts b/cypress/e2e/27-opt-in-trial-banner.cy.ts deleted file mode 100644 index 6e24343bc770a..0000000000000 --- a/cypress/e2e/27-opt-in-trial-banner.cy.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { BannerStack, MainSidebar, WorkflowPage } from '../pages'; -import planData from '../fixtures/Plan_data_opt_in_trial.json'; -import { INSTANCE_OWNER } from '../constants'; - -const mainSidebar = new MainSidebar(); -const bannerStack = new BannerStack(); -const workflowPage = new WorkflowPage(); - -describe('BannerStack', { disableAutoLogin: true }, () => { - before(() => { - const now = new Date(); - const fiveDaysFromNow = new Date(now.getTime() + 5 * 24 * 60 * 60 * 1000); - planData.expirationDate = fiveDaysFromNow.toJSON(); - }); - - it('should render trial banner for opt-in cloud user', () => { - cy.intercept('GET', '/rest/admin/cloud-plan', { - body: planData, - }).as('getPlanData'); - - cy.intercept('GET', '/rest/settings', (req) => { - req.on('response', (res) => { - res.send({ - data: { ...res.body.data, deployment: { type: 'cloud' }, n8nMetadata: { userId: 1 } }, - }); - }); - }).as('loadSettings'); - - cy.signin({ email: INSTANCE_OWNER.email, password: INSTANCE_OWNER.password }); - - cy.visit(workflowPage.url); - - cy.wait('@getPlanData'); - - bannerStack.getters.banner().should('be.visible'); - - mainSidebar.actions.signout(); - - bannerStack.getters.banner().should('not.be.visible'); - - cy.signin({ email: INSTANCE_OWNER.email, password: INSTANCE_OWNER.password }); - - cy.visit(workflowPage.url); - - bannerStack.getters.banner().should('be.visible'); - - mainSidebar.actions.signout(); - }); - - it('should not render opt-in-trial banner for non cloud deployment', () => { - cy.intercept('GET', '/rest/settings', (req) => { - req.on('response', (res) => { - res.send({ - data: { ...res.body.data, deployment: { type: 'default' } }, - }); - }); - }).as('loadSettings'); - - cy.signin({ email: INSTANCE_OWNER.email, password: INSTANCE_OWNER.password }); - - cy.visit(workflowPage.url); - - bannerStack.getters.banner().should('not.be.visible'); - - mainSidebar.actions.signout(); - }); - - it('Should show admin button', () => { - cy.intercept('GET', '/rest/settings', (req) => { - req.on('response', (res) => { - res.send({ - data: { ...res.body.data, deployment: { type: 'cloud' }, n8nMetadata: { userId: 1 } }, - }); - }); - }).as('loadSettings'); - - cy.signin({ email: INSTANCE_OWNER.email, password: INSTANCE_OWNER.password }); - - cy.visit(workflowPage.url); - - mainSidebar.getters.adminPanel().should('be.visible'); - }); -}); diff --git a/cypress/fixtures/Plan_data_opt_in_trial.json b/cypress/fixtures/Plan_data_opt_in_trial.json index 504805de320e1..7a805708c651d 100644 --- a/cypress/fixtures/Plan_data_opt_in_trial.json +++ b/cypress/fixtures/Plan_data_opt_in_trial.json @@ -13,8 +13,7 @@ "feat:advancedExecutionFilters": true, "quota:users": -1, "quota:maxVariables": -1, - "feat:variables": true, - "feat:apiDisabled": true + "feat:variables": true }, "metadata": { "version": "v1", diff --git a/cypress/pages/index.ts b/cypress/pages/index.ts index 6f03962c2ac13..39c9be3b5648b 100644 --- a/cypress/pages/index.ts +++ b/cypress/pages/index.ts @@ -12,3 +12,4 @@ export * from './workflow-executions-tab'; export * from './signin'; export * from './workflow-history'; export * from './workerView'; +export * from './settings-public-api'; diff --git a/cypress/pages/settings-public-api.ts b/cypress/pages/settings-public-api.ts new file mode 100644 index 0000000000000..1a7d668136775 --- /dev/null +++ b/cypress/pages/settings-public-api.ts @@ -0,0 +1,5 @@ +export const getPublicApiUpgradeCTA = () => cy.getByTestId('public-api-upgrade-cta'); + +export const visitPublicApiPage = () => { + cy.visit('/settings/api'); +}; diff --git a/packages/editor-ui/src/router.ts b/packages/editor-ui/src/router.ts index eac3900d0d1e3..b9f9d1c180507 100644 --- a/packages/editor-ui/src/router.ts +++ b/packages/editor-ui/src/router.ts @@ -517,13 +517,7 @@ export const routes = [ settingsView: SettingsApiView, }, meta: { - middleware: ['authenticated', 'custom'], - middlewareOptions: { - custom: () => { - const settingsStore = useSettingsStore(); - return settingsStore.isPublicApiEnabled; - }, - }, + middleware: ['authenticated'], telemetry: { pageCategory: 'settings', getProperties(route: RouteLocation) { diff --git a/packages/editor-ui/src/views/SettingsApiView.vue b/packages/editor-ui/src/views/SettingsApiView.vue index d09b1d4c4c046..2f6c33f6b1408 100644 --- a/packages/editor-ui/src/views/SettingsApiView.vue +++ b/packages/editor-ui/src/views/SettingsApiView.vue @@ -64,7 +64,8 @@ Date: Thu, 21 Dec 2023 17:37:08 +0100 Subject: [PATCH 14/32] fix(core): Remove circular dependency in WorkflowService and ActiveWorkflowRunner (#8128) ## Summary A circular dependency between `WorkflowService` and `ActiveWorkflowRunner` is sometimes causing `this.activeWorkflowRunner` to be `undefined` in `WorkflowService`. Breaking this circular dependency should hopefully fix this issue. ## Related tickets and issues #8122 ## Review / Merge checklist - [x] PR title and summary are descriptive - [ ] Tests included --- packages/cli/src/ActiveWorkflowRunner.ts | 22 +++--- packages/cli/src/WebhookHelpers.ts | 4 -- .../cli/src/WorkflowExecuteAdditionalData.ts | 12 ++-- .../repositories/workflow.repository.ts | 27 +++++++- .../src/executions/executions.service.ee.ts | 4 +- .../cli/src/workflows/workflow.service.ee.ts | 4 +- .../cli/src/workflows/workflow.service.ts | 67 ++----------------- .../workflows/workflowStaticData.service.ts | 41 ++++++++++++ .../src/workflows/workflows.controller.ee.ts | 3 +- 9 files changed, 98 insertions(+), 86 deletions(-) create mode 100644 packages/cli/src/workflows/workflowStaticData.service.ts diff --git a/packages/cli/src/ActiveWorkflowRunner.ts b/packages/cli/src/ActiveWorkflowRunner.ts index 07a5258af8af8..bb037590c674e 100644 --- a/packages/cli/src/ActiveWorkflowRunner.ts +++ b/packages/cli/src/ActiveWorkflowRunner.ts @@ -1,8 +1,7 @@ /* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ - -import { Container, Service } from 'typedi'; +import { Service } from 'typedi'; import { ActiveWorkflows, NodeExecuteFunctions } from 'n8n-core'; import type { @@ -59,7 +58,6 @@ import { NodeTypes } from '@/NodeTypes'; import { WorkflowRunner } from '@/WorkflowRunner'; import { ExternalHooks } from '@/ExternalHooks'; import { whereClause } from './UserManagement/UserManagementHelper'; -import { WorkflowService } from './workflows/workflow.service'; import { WebhookNotFoundError } from './errors/response-errors/webhook-not-found.error'; import { In } from 'typeorm'; import { WebhookService } from './services/webhook.service'; @@ -70,6 +68,7 @@ import { MultiMainSetup } from '@/services/orchestration/main/MultiMainSetup.ee' import { ActivationErrorsService } from '@/ActivationErrors.service'; import type { Scope } from '@n8n/permissions'; import { NotFoundError } from './errors/response-errors/not-found.error'; +import { WorkflowStaticDataService } from '@/workflows/workflowStaticData.service'; @Service() export class ActiveWorkflowRunner implements IWebhookManager { @@ -95,6 +94,7 @@ export class ActiveWorkflowRunner implements IWebhookManager { private readonly multiMainSetup: MultiMainSetup, private readonly activationErrorsService: ActivationErrorsService, private readonly executionService: ExecutionsService, + private readonly workflowStaticDataService: WorkflowStaticDataService, ) {} async init() { @@ -214,10 +214,12 @@ export class ActiveWorkflowRunner implements IWebhookManager { undefined, request, response, - (error: Error | null, data: object) => { + async (error: Error | null, data: object) => { if (error !== null) { return reject(error); } + // Save static data if it changed + await this.workflowStaticDataService.saveStaticData(workflow); resolve(data); }, ); @@ -413,7 +415,7 @@ export class ActiveWorkflowRunner implements IWebhookManager { } await this.webhookService.populateCache(); - await Container.get(WorkflowService).saveStaticData(workflow); + await this.workflowStaticDataService.saveStaticData(workflow); } /** @@ -452,7 +454,7 @@ export class ActiveWorkflowRunner implements IWebhookManager { await workflow.deleteWebhook(webhookData, NodeExecuteFunctions, mode, 'update'); } - await Container.get(WorkflowService).saveStaticData(workflow); + await this.workflowStaticDataService.saveStaticData(workflow); await this.webhookService.deleteWorkflowWebhooks(workflowId); } @@ -525,7 +527,7 @@ export class ActiveWorkflowRunner implements IWebhookManager { donePromise?: IDeferredPromise, ): void => { this.logger.debug(`Received event to trigger execution for workflow "${workflow.name}"`); - void Container.get(WorkflowService).saveStaticData(workflow); + void this.workflowStaticDataService.saveStaticData(workflow); const executePromise = this.runWorkflow( workflowData, node, @@ -582,7 +584,7 @@ export class ActiveWorkflowRunner implements IWebhookManager { donePromise?: IDeferredPromise, ): void => { this.logger.debug(`Received trigger for workflow "${workflow.name}"`); - void Container.get(WorkflowService).saveStaticData(workflow); + void this.workflowStaticDataService.saveStaticData(workflow); const executePromise = this.runWorkflow( workflowData, @@ -817,7 +819,7 @@ export class ActiveWorkflowRunner implements IWebhookManager { await this.activationErrorsService.unset(workflowId); const triggerCount = this.countTriggers(workflow, additionalData); - await Container.get(WorkflowService).updateWorkflowTriggerCount(workflow.id, triggerCount); + await this.workflowRepository.updateWorkflowTriggerCount(workflow.id, triggerCount); } catch (e) { const error = e instanceof Error ? e : new Error(`${e}`); await this.activationErrorsService.set(workflowId, error.message); @@ -827,7 +829,7 @@ export class ActiveWorkflowRunner implements IWebhookManager { // If for example webhooks get created it sometimes has to save the // id of them in the static data. So make sure that data gets persisted. - await Container.get(WorkflowService).saveStaticData(workflow); + await this.workflowStaticDataService.saveStaticData(workflow); } /** diff --git a/packages/cli/src/WebhookHelpers.ts b/packages/cli/src/WebhookHelpers.ts index 0446d5a92b6fa..f218ec11c1b7a 100644 --- a/packages/cli/src/WebhookHelpers.ts +++ b/packages/cli/src/WebhookHelpers.ts @@ -60,7 +60,6 @@ import type { WorkflowEntity } from '@db/entities/WorkflowEntity'; import { EventsService } from '@/services/events.service'; import { OwnershipService } from './services/ownership.service'; import { parseBody } from './middlewares'; -import { WorkflowService } from './workflows/workflow.service'; import { Logger } from './Logger'; import { NotFoundError } from './errors/response-errors/not-found.error'; import { InternalServerError } from './errors/response-errors/internal-server.error'; @@ -386,9 +385,6 @@ export async function executeWebhook( }; } - // Save static data if it changed - await Container.get(WorkflowService).saveStaticData(workflow); - const additionalKeys: IWorkflowDataProxyAdditionalKeys = { $executionId: executionId, }; diff --git a/packages/cli/src/WorkflowExecuteAdditionalData.ts b/packages/cli/src/WorkflowExecuteAdditionalData.ts index 1f6bcc627b4e9..e81d97098273f 100644 --- a/packages/cli/src/WorkflowExecuteAdditionalData.ts +++ b/packages/cli/src/WorkflowExecuteAdditionalData.ts @@ -52,7 +52,6 @@ import * as WebhookHelpers from '@/WebhookHelpers'; import * as WorkflowHelpers from '@/WorkflowHelpers'; import { findSubworkflowStart, isWorkflowIdValid } from '@/utils'; import { PermissionChecker } from './UserManagement/PermissionChecker'; -import { WorkflowService } from './workflows/workflow.service'; import { InternalHooks } from '@/InternalHooks'; import { ExecutionRepository } from '@db/repositories/execution.repository'; import { EventsService } from '@/services/events.service'; @@ -67,6 +66,8 @@ import { restoreBinaryDataId } from './executionLifecycleHooks/restoreBinaryData import { toSaveSettings } from './executionLifecycleHooks/toSaveSettings'; import { Logger } from './Logger'; import { saveExecutionProgress } from './executionLifecycleHooks/saveExecutionProgress'; +import { WorkflowStaticDataService } from './workflows/workflowStaticData.service'; +import { WorkflowRepository } from './databases/repositories/workflow.repository'; const ERROR_TRIGGER_TYPE = config.getEnv('nodes.errorTriggerType'); @@ -418,7 +419,7 @@ function hookFunctionsSave(parentProcessMode?: string): IWorkflowExecuteHooks { if (!isManualMode && isWorkflowIdValid(this.workflowData.id) && newStaticData) { // Workflow is saved so update in database try { - await Container.get(WorkflowService).saveStaticDataById( + await Container.get(WorkflowStaticDataService).saveStaticDataById( this.workflowData.id as string, newStaticData, ); @@ -564,7 +565,7 @@ function hookFunctionsSaveWorker(): IWorkflowExecuteHooks { if (isWorkflowIdValid(this.workflowData.id) && newStaticData) { // Workflow is saved so update in database try { - await Container.get(WorkflowService).saveStaticDataById( + await Container.get(WorkflowStaticDataService).saveStaticDataById( this.workflowData.id as string, newStaticData, ); @@ -714,7 +715,10 @@ export async function getWorkflowData( if (workflowInfo.id !== undefined) { const relations = config.getEnv('workflowTagsDisabled') ? [] : ['tags']; - workflowData = await Container.get(WorkflowService).get({ id: workflowInfo.id }, { relations }); + workflowData = await Container.get(WorkflowRepository).get( + { id: workflowInfo.id }, + { relations }, + ); if (workflowData === undefined || workflowData === null) { throw new ApplicationError('Workflow does not exist.', { diff --git a/packages/cli/src/databases/repositories/workflow.repository.ts b/packages/cli/src/databases/repositories/workflow.repository.ts index d5b193ff2678c..990b2b829cb45 100644 --- a/packages/cli/src/databases/repositories/workflow.repository.ts +++ b/packages/cli/src/databases/repositories/workflow.repository.ts @@ -1,5 +1,6 @@ import { Service } from 'typedi'; -import { DataSource, Repository } from 'typeorm'; +import { DataSource, Repository, type UpdateResult, type FindOptionsWhere } from 'typeorm'; +import config from '@/config'; import { WorkflowEntity } from '../entities/WorkflowEntity'; @Service() @@ -8,6 +9,13 @@ export class WorkflowRepository extends Repository { super(WorkflowEntity, dataSource.manager); } + async get(where: FindOptionsWhere, options?: { relations: string[] }) { + return this.findOne({ + where, + relations: options?.relations, + }); + } + async getAllActive() { return this.find({ where: { active: true }, @@ -28,4 +36,21 @@ export class WorkflowRepository extends Repository { }); return totalTriggerCount ?? 0; } + + async updateWorkflowTriggerCount(id: string, triggerCount: number): Promise { + const qb = this.createQueryBuilder('workflow'); + return qb + .update() + .set({ + triggerCount, + updatedAt: () => { + if (['mysqldb', 'mariadb'].includes(config.getEnv('database.type'))) { + return 'updatedAt'; + } + return '"updatedAt"'; + }, + }) + .where('id = :id', { id }) + .execute(); + } } diff --git a/packages/cli/src/executions/executions.service.ee.ts b/packages/cli/src/executions/executions.service.ee.ts index 9fbfa8ca71919..662ce3875948f 100644 --- a/packages/cli/src/executions/executions.service.ee.ts +++ b/packages/cli/src/executions/executions.service.ee.ts @@ -6,7 +6,7 @@ import type { IExecutionResponse, IExecutionFlattedResponse } from '@/Interfaces import { EnterpriseWorkflowService } from '../workflows/workflow.service.ee'; import type { WorkflowWithSharingsAndCredentials } from '@/workflows/workflows.types'; import Container from 'typedi'; -import { WorkflowService } from '@/workflows/workflow.service'; +import { WorkflowRepository } from '@/databases/repositories/workflow.repository'; export class EEExecutionsService extends ExecutionsService { /** @@ -26,7 +26,7 @@ export class EEExecutionsService extends ExecutionsService { const relations = ['shared', 'shared.user', 'shared.role']; - const workflow = (await Container.get(WorkflowService).get( + const workflow = (await Container.get(WorkflowRepository).get( { id: execution.workflowId }, { relations }, )) as WorkflowWithSharingsAndCredentials; diff --git a/packages/cli/src/workflows/workflow.service.ee.ts b/packages/cli/src/workflows/workflow.service.ee.ts index 57c2e88ea8c23..6431a3654c326 100644 --- a/packages/cli/src/workflows/workflow.service.ee.ts +++ b/packages/cli/src/workflows/workflow.service.ee.ts @@ -18,6 +18,7 @@ import type { CredentialsEntity } from '@db/entities/CredentialsEntity'; import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository'; import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { NotFoundError } from '@/errors/response-errors/not-found.error'; +import { WorkflowRepository } from '@/databases/repositories/workflow.repository'; @Service() export class EnterpriseWorkflowService { @@ -26,6 +27,7 @@ export class EnterpriseWorkflowService { private readonly userService: UserService, private readonly roleService: RoleService, private readonly sharedWorkflowRepository: SharedWorkflowRepository, + private readonly workflowRepository: WorkflowRepository, ) {} async isOwned( @@ -182,7 +184,7 @@ export class EnterpriseWorkflowService { } async preventTampering(workflow: WorkflowEntity, workflowId: string, user: User) { - const previousVersion = await this.workflowService.get({ id: workflowId }); + const previousVersion = await this.workflowRepository.get({ id: workflowId }); if (!previousVersion) { throw new NotFoundError('Workflow not found'); diff --git a/packages/cli/src/workflows/workflow.service.ts b/packages/cli/src/workflows/workflow.service.ts index d4854d123dff0..fb2380ccdbf7a 100644 --- a/packages/cli/src/workflows/workflow.service.ts +++ b/packages/cli/src/workflows/workflow.service.ts @@ -1,7 +1,7 @@ import Container, { Service } from 'typedi'; -import type { IDataObject, INode, IPinData } from 'n8n-workflow'; -import { NodeApiError, ErrorReporterProxy as ErrorReporter, Workflow } from 'n8n-workflow'; -import type { FindManyOptions, FindOptionsSelect, FindOptionsWhere, UpdateResult } from 'typeorm'; +import type { INode, IPinData } from 'n8n-workflow'; +import { NodeApiError, Workflow } from 'n8n-workflow'; +import type { FindManyOptions, FindOptionsSelect, FindOptionsWhere } from 'typeorm'; import { In, Like } from 'typeorm'; import pick from 'lodash/pick'; import omit from 'lodash/omit'; @@ -25,7 +25,7 @@ import { whereClause } from '@/UserManagement/UserManagementHelper'; import { InternalHooks } from '@/InternalHooks'; import { WorkflowRepository } from '@db/repositories/workflow.repository'; import { OwnershipService } from '@/services/ownership.service'; -import { isStringArray, isWorkflowIdValid } from '@/utils'; +import { isStringArray } from '@/utils'; import { WorkflowHistoryService } from './workflowHistory/workflowHistory.service.ee'; import { BinaryDataService } from 'n8n-core'; import type { Scope } from '@n8n/permissions'; @@ -120,13 +120,6 @@ export class WorkflowService { return pinnedTriggers.find((pt) => pt.name === checkNodeName) ?? null; // partial execution } - async get(workflow: FindOptionsWhere, options?: { relations: string[] }) { - return this.workflowRepository.findOne({ - where: workflow, - relations: options?.relations, - }); - } - async getMany(sharedWorkflowIds: string[], options?: ListQuery.Options) { if (sharedWorkflowIds.length === 0) return { workflows: [], count: 0 }; @@ -512,56 +505,4 @@ export class WorkflowService { return sharedWorkflow.workflow; } - - async updateWorkflowTriggerCount(id: string, triggerCount: number): Promise { - const qb = this.workflowRepository.createQueryBuilder('workflow'); - return qb - .update() - .set({ - triggerCount, - updatedAt: () => { - if (['mysqldb', 'mariadb'].includes(config.getEnv('database.type'))) { - return 'updatedAt'; - } - return '"updatedAt"'; - }, - }) - .where('id = :id', { id }) - .execute(); - } - - /** - * Saves the static data if it changed - */ - async saveStaticData(workflow: Workflow): Promise { - if (workflow.staticData.__dataChanged === true) { - // Static data of workflow changed and so has to be saved - if (isWorkflowIdValid(workflow.id)) { - // Workflow is saved so update in database - try { - await this.saveStaticDataById(workflow.id, workflow.staticData); - workflow.staticData.__dataChanged = false; - } catch (error) { - ErrorReporter.error(error); - this.logger.error( - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - `There was a problem saving the workflow with id "${workflow.id}" to save changed Data: "${error.message}"`, - { workflowId: workflow.id }, - ); - } - } - } - } - - /** - * Saves the given static data on workflow - * - * @param {(string)} workflowId The id of the workflow to save data on - * @param {IDataObject} newStaticData The static data to save - */ - async saveStaticDataById(workflowId: string, newStaticData: IDataObject): Promise { - await this.workflowRepository.update(workflowId, { - staticData: newStaticData, - }); - } } diff --git a/packages/cli/src/workflows/workflowStaticData.service.ts b/packages/cli/src/workflows/workflowStaticData.service.ts new file mode 100644 index 0000000000000..b569c69a3002d --- /dev/null +++ b/packages/cli/src/workflows/workflowStaticData.service.ts @@ -0,0 +1,41 @@ +import { Service } from 'typedi'; +import { type IDataObject, type Workflow, ErrorReporterProxy as ErrorReporter } from 'n8n-workflow'; +import { Logger } from '@/Logger'; +import { WorkflowRepository } from '@db/repositories/workflow.repository'; +import { isWorkflowIdValid } from '@/utils'; + +@Service() +export class WorkflowStaticDataService { + constructor( + private readonly logger: Logger, + private readonly workflowRepository: WorkflowRepository, + ) {} + + /** Saves the static data if it changed */ + async saveStaticData(workflow: Workflow): Promise { + if (workflow.staticData.__dataChanged === true) { + // Static data of workflow changed and so has to be saved + if (isWorkflowIdValid(workflow.id)) { + // Workflow is saved so update in database + try { + await this.saveStaticDataById(workflow.id, workflow.staticData); + workflow.staticData.__dataChanged = false; + } catch (error) { + ErrorReporter.error(error); + this.logger.error( + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + `There was a problem saving the workflow with id "${workflow.id}" to save changed Data: "${error.message}"`, + { workflowId: workflow.id }, + ); + } + } + } + } + + /** Saves the given static data on workflow */ + async saveStaticDataById(workflowId: string, newStaticData: IDataObject): Promise { + await this.workflowRepository.update(workflowId, { + staticData: newStaticData, + }); + } +} diff --git a/packages/cli/src/workflows/workflows.controller.ee.ts b/packages/cli/src/workflows/workflows.controller.ee.ts index 900c8d3660249..27a3abc0cfff2 100644 --- a/packages/cli/src/workflows/workflows.controller.ee.ts +++ b/packages/cli/src/workflows/workflows.controller.ee.ts @@ -28,6 +28,7 @@ import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { NotFoundError } from '@/errors/response-errors/not-found.error'; import { InternalServerError } from '@/errors/response-errors/internal-server.error'; import { WorkflowService } from './workflow.service'; +import { WorkflowRepository } from '@/databases/repositories/workflow.repository'; export const EEWorkflowController = express.Router(); @@ -129,7 +130,7 @@ EEWorkflowController.get( relations.push('tags'); } - const workflow = await Container.get(WorkflowService).get({ id: workflowId }, { relations }); + const workflow = await Container.get(WorkflowRepository).get({ id: workflowId }, { relations }); if (!workflow) { throw new NotFoundError(`Workflow with ID "${workflowId}" does not exist`); From 1d2666b37cfdb42c7ae7ab18235e071fdd25d2c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E0=A4=95=E0=A4=BE=E0=A4=B0=E0=A4=A4=E0=A5=8B=E0=A4=AB?= =?UTF-8?q?=E0=A5=8D=E0=A4=AB=E0=A5=87=E0=A4=B2=E0=A4=B8=E0=A5=8D=E0=A4=95?= =?UTF-8?q?=E0=A5=8D=E0=A4=B0=E0=A4=BF=E0=A4=AA=E0=A5=8D=E0=A4=9F=E2=84=A2?= Date: Thu, 21 Dec 2023 18:22:32 +0100 Subject: [PATCH 15/32] refactor(Peekalink Node): Stricter typing for Peekalink api call + Tests (no-changelog) (#8125) This PR is an example for how we can 1. improve typing and remove boilerplate code in may of our nodes 2. use nock to write effective unit tests for nodes that make external calls ## Review / Merge checklist - [x] PR title and summary are descriptive - [x] Add tests --- packages/nodes-base/.eslintrc.js | 6 + .../nodes/Peekalink/GenericFunctions.ts | 41 ----- .../nodes/Peekalink/Peekalink.node.ts | 80 ++++---- .../Peekalink/test/Peekalink.node.test.ts | 171 ++++++++++++++++++ .../nodes-base/test/nodes/ExecuteWorkflow.ts | 8 + packages/nodes-base/test/nodes/Helpers.ts | 11 ++ 6 files changed, 231 insertions(+), 86 deletions(-) delete mode 100644 packages/nodes-base/nodes/Peekalink/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/Peekalink/test/Peekalink.node.test.ts diff --git a/packages/nodes-base/.eslintrc.js b/packages/nodes-base/.eslintrc.js index 5229bbe9644d1..5193ea70b715d 100644 --- a/packages/nodes-base/.eslintrc.js +++ b/packages/nodes-base/.eslintrc.js @@ -151,5 +151,11 @@ module.exports = { 'n8n-nodes-base/node-param-type-options-password-missing': 'error', }, }, + { + files: ['**/*.test.ts', '**/test/**/*.ts'], + rules: { + 'import/no-extraneous-dependencies': 'off', + }, + }, ], }; diff --git a/packages/nodes-base/nodes/Peekalink/GenericFunctions.ts b/packages/nodes-base/nodes/Peekalink/GenericFunctions.ts deleted file mode 100644 index 222b4bac00c5f..0000000000000 --- a/packages/nodes-base/nodes/Peekalink/GenericFunctions.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { OptionsWithUri } from 'request'; - -import type { - JsonObject, - IExecuteFunctions, - IHookFunctions, - ILoadOptionsFunctions, - IDataObject, -} from 'n8n-workflow'; -import { NodeApiError } from 'n8n-workflow'; - -export async function peekalinkApiRequest( - this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, - method: string, - resource: string, - - body: any = {}, - qs: IDataObject = {}, - uri?: string, - option: IDataObject = {}, -): Promise { - try { - const credentials = await this.getCredentials('peekalinkApi'); - let options: OptionsWithUri = { - headers: { - 'X-API-Key': credentials.apiKey, - }, - method, - qs, - body, - uri: uri || `https://api.peekalink.io${resource}`, - json: true, - }; - - options = Object.assign({}, options, option); - - return await this.helpers.request(options); - } catch (error) { - throw new NodeApiError(this.getNode(), error as JsonObject); - } -} diff --git a/packages/nodes-base/nodes/Peekalink/Peekalink.node.ts b/packages/nodes-base/nodes/Peekalink/Peekalink.node.ts index 920564fb5a404..638ca9f4093ab 100644 --- a/packages/nodes-base/nodes/Peekalink/Peekalink.node.ts +++ b/packages/nodes-base/nodes/Peekalink/Peekalink.node.ts @@ -1,14 +1,17 @@ -import type { - IExecuteFunctions, - IDataObject, - INodeExecutionData, - INodeType, - INodeTypeDescription, +import { + Node, + NodeApiError, + type IExecuteFunctions, + type INodeExecutionData, + type INodeTypeDescription, + type JsonObject, } from 'n8n-workflow'; -import { peekalinkApiRequest } from './GenericFunctions'; +export const apiUrl = 'https://api.peekalink.io'; -export class Peekalink implements INodeType { +type Operation = 'preview' | 'isAvailable'; + +export class Peekalink extends Node { description: INodeTypeDescription = { displayName: 'Peekalink', name: 'peekalink', @@ -61,44 +64,31 @@ export class Peekalink implements INodeType { ], }; - async execute(this: IExecuteFunctions): Promise { - const items = this.getInputData(); - const returnData: IDataObject[] = []; - const length = items.length; - let responseData; - const operation = this.getNodeParameter('operation', 0); - - for (let i = 0; i < length; i++) { - try { - if (operation === 'isAvailable') { - const url = this.getNodeParameter('url', i) as string; - const body: IDataObject = { - link: url, - }; - - responseData = await peekalinkApiRequest.call(this, 'POST', '/is-available/', body); - } - if (operation === 'preview') { - const url = this.getNodeParameter('url', i) as string; - const body: IDataObject = { - link: url, - }; + async execute(context: IExecuteFunctions): Promise { + const items = context.getInputData(); + const operation = context.getNodeParameter('operation', 0) as Operation; + const credentials = await context.getCredentials('peekalinkApi'); - responseData = await peekalinkApiRequest.call(this, 'POST', '/', body); - } - if (Array.isArray(responseData)) { - returnData.push.apply(returnData, responseData as IDataObject[]); - } else { - returnData.push(responseData as IDataObject); - } - } catch (error) { - if (this.continueOnFail()) { - returnData.push({ error: error.message }); - continue; + const returnData = await Promise.all( + items.map(async (_, i) => { + try { + const link = context.getNodeParameter('url', i) as string; + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return await context.helpers.request({ + method: 'POST', + uri: operation === 'preview' ? apiUrl : `${apiUrl}/is-available/`, + body: { link }, + headers: { 'X-API-Key': credentials.apiKey }, + json: true, + }); + } catch (error) { + if (context.continueOnFail()) { + return { error: error.message }; + } + throw new NodeApiError(context.getNode(), error as JsonObject); } - throw error; - } - } - return [this.helpers.returnJsonArray(returnData)]; + }), + ); + return [context.helpers.returnJsonArray(returnData)]; } } diff --git a/packages/nodes-base/nodes/Peekalink/test/Peekalink.node.test.ts b/packages/nodes-base/nodes/Peekalink/test/Peekalink.node.test.ts new file mode 100644 index 0000000000000..f1262863ce643 --- /dev/null +++ b/packages/nodes-base/nodes/Peekalink/test/Peekalink.node.test.ts @@ -0,0 +1,171 @@ +import { apiUrl } from '../Peekalink.node'; +import type { WorkflowTestData } from '@test/nodes/types'; +import { executeWorkflow } from '@test/nodes/ExecuteWorkflow'; +import * as Helpers from '@test/nodes/Helpers'; + +describe('Peekalink Node', () => { + const exampleComPreview = { + url: 'https://example.com/', + domain: 'example.com', + lastUpdated: '2022-11-13T22:43:20.986744Z', + nextUpdate: '2022-11-20T22:43:20.982384Z', + contentType: 'html', + mimeType: 'text/html', + size: 648, + redirected: false, + title: 'Example Domain', + description: 'This domain is for use in illustrative examples in documents', + name: 'EXAMPLE.COM', + trackersDetected: false, + }; + + const tests: WorkflowTestData[] = [ + { + description: 'should run isAvailable operation', + input: { + workflowData: { + nodes: [ + { + parameters: {}, + id: '8b7bb389-e4ef-424a-bca1-e7ead60e43eb', + name: 'When clicking "Execute Workflow"', + type: 'n8n-nodes-base.manualTrigger', + typeVersion: 1, + position: [740, 380], + }, + { + parameters: { + operation: 'isAvailable', + url: 'https://example.com/', + }, + id: '7354367e-39a7-4fc1-8cdd-442f0b0c7b62', + name: 'Peekalink', + type: 'n8n-nodes-base.peekalink', + typeVersion: 1, + position: [960, 380], + credentials: { + peekalinkApi: 'token', + }, + }, + ], + connections: { + 'When clicking "Execute Workflow"': { + main: [ + [ + { + node: 'Peekalink', + type: 'main', + index: 0, + }, + ], + ], + }, + }, + }, + }, + output: { + nodeExecutionOrder: ['Start'], + nodeData: { + Peekalink: [ + [ + { + json: { + isAvailable: true, + }, + }, + ], + ], + }, + }, + nock: { + baseUrl: apiUrl, + mocks: [ + { + method: 'post', + path: '/is-available/', + statusCode: 200, + responseBody: { isAvailable: true }, + }, + ], + }, + }, + { + description: 'should run preview operation', + input: { + workflowData: { + nodes: [ + { + parameters: {}, + id: '8b7bb389-e4ef-424a-bca1-e7ead60e43eb', + name: 'When clicking "Execute Workflow"', + type: 'n8n-nodes-base.manualTrigger', + typeVersion: 1, + position: [740, 380], + }, + { + parameters: { + operation: 'preview', + url: 'https://example.com/', + }, + id: '7354367e-39a7-4fc1-8cdd-442f0b0c7b62', + name: 'Peekalink', + type: 'n8n-nodes-base.peekalink', + typeVersion: 1, + position: [960, 380], + credentials: { + peekalinkApi: 'token', + }, + }, + ], + connections: { + 'When clicking "Execute Workflow"': { + main: [ + [ + { + node: 'Peekalink', + type: 'main', + index: 0, + }, + ], + ], + }, + }, + }, + }, + output: { + nodeExecutionOrder: ['Start'], + nodeData: { + Peekalink: [ + [ + { + json: exampleComPreview, + }, + ], + ], + }, + }, + nock: { + baseUrl: apiUrl, + mocks: [ + { + method: 'post', + path: '/', + statusCode: 200, + responseBody: exampleComPreview, + }, + ], + }, + }, + ]; + + const nodeTypes = Helpers.setup(tests); + + test.each(tests)('$description', async (testData) => { + const { result } = await executeWorkflow(testData, nodeTypes); + const resultNodeData = Helpers.getResultNodeData(result, testData); + resultNodeData.forEach(({ nodeName, resultData }) => + expect(resultData).toEqual(testData.output.nodeData[nodeName]), + ); + expect(result.finished).toEqual(true); + }); +}); diff --git a/packages/nodes-base/test/nodes/ExecuteWorkflow.ts b/packages/nodes-base/test/nodes/ExecuteWorkflow.ts index a7814f04cd832..528eca40a901b 100644 --- a/packages/nodes-base/test/nodes/ExecuteWorkflow.ts +++ b/packages/nodes-base/test/nodes/ExecuteWorkflow.ts @@ -1,3 +1,4 @@ +import nock from 'nock'; import { WorkflowExecute } from 'n8n-core'; import type { INodeTypes, IRun, IRunExecutionData } from 'n8n-workflow'; import { createDeferredPromise, Workflow } from 'n8n-workflow'; @@ -5,6 +6,13 @@ import * as Helpers from './Helpers'; import type { WorkflowTestData } from './types'; export async function executeWorkflow(testData: WorkflowTestData, nodeTypes: INodeTypes) { + if (testData.nock) { + const { baseUrl, mocks } = testData.nock; + const agent = nock(baseUrl); + mocks.forEach(({ method, path, statusCode, responseBody }) => + agent[method](path).reply(statusCode, responseBody), + ); + } const executionMode = testData.trigger?.mode ?? 'manual'; const workflowInstance = new Workflow({ id: 'test', diff --git a/packages/nodes-base/test/nodes/Helpers.ts b/packages/nodes-base/test/nodes/Helpers.ts index be4e7559ce026..9cf4dbe8e67a6 100644 --- a/packages/nodes-base/test/nodes/Helpers.ts +++ b/packages/nodes-base/test/nodes/Helpers.ts @@ -1,6 +1,7 @@ import { readFileSync, readdirSync, mkdtempSync } from 'fs'; import path from 'path'; import { tmpdir } from 'os'; +import nock from 'nock'; import { isEmpty } from 'lodash'; import { get } from 'lodash'; import { BinaryDataService, Credentials, constructExecutionMetaData } from 'n8n-core'; @@ -231,6 +232,16 @@ export function setup(testData: WorkflowTestData[] | WorkflowTestData) { testData = [testData]; } + if (testData.some((t) => !!t.nock)) { + beforeAll(() => { + nock.disableNetConnect(); + }); + + afterAll(() => { + nock.restore(); + }); + } + const nodeTypes = new NodeTypes(); const nodes = [...new Set(testData.flatMap((data) => data.input.workflowData.nodes))]; From a169b7406279de43dbd3fd7d13166d987c60d01a Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Thu, 21 Dec 2023 12:29:26 -0500 Subject: [PATCH 16/32] fix(Redis Trigger Node): Activating a workflow with a Redis trigger fails (#8129) ## Summary > Describe what the PR does and how to test. Photos and videos are recommended. We were awaiting for the promise to resolve before returning. Because the trigger method does not return until the first message is received or the connection errors, the requests that actives the workflows did not respond making the activation button irresponsive. Without the change: https://www.loom.com/share/769b26d5d4ee407e999344fab3905eae With the change: https://www.loom.com/share/d1691ee1941248bc97f2ed97b0c129a3 ## Related tickets and issues https://linear.app/n8n/issue/ADO-895/activating-a-workflow-with-a-redis-trigger-fails ## Review / Merge checklist - [x] PR title and summary are descriptive. **Remember, the title automatically goes into the changelog. Use `(no-changelog)` otherwise.** ([conventions](https://github.com/n8n-io/n8n/blob/master/.github/pull_request_title_conventions.md)) - [x] [Docs updated](https://github.com/n8n-io/n8n-docs) or follow-up ticket created. --- packages/nodes-base/nodes/Redis/RedisTrigger.node.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nodes-base/nodes/Redis/RedisTrigger.node.ts b/packages/nodes-base/nodes/Redis/RedisTrigger.node.ts index 2224ee652d291..39622f0c0c08e 100644 --- a/packages/nodes-base/nodes/Redis/RedisTrigger.node.ts +++ b/packages/nodes-base/nodes/Redis/RedisTrigger.node.ts @@ -118,7 +118,7 @@ export class RedisTrigger implements INodeType { }; if (this.getMode() === 'trigger') { - await manualTriggerFunction(); + void manualTriggerFunction(); } async function closeFunction() { From 15ffd4fb9f967302e2444a873a804d2ccb64e748 Mon Sep 17 00:00:00 2001 From: Aaron Gutierrez Date: Thu, 21 Dec 2023 10:21:08 -0800 Subject: [PATCH 17/32] fix(Asana Node): Omit body from GET, HEAD, and DELETE requests (#8057) Avoid unnecessarily including a request body with GET and HEAD requests. Per RFC 7230 clients should not include a body for these requests, and we (Asana) are rolling out an infrastructure change that will cause these requests to fail. --- packages/nodes-base/nodes/Asana/GenericFunctions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nodes-base/nodes/Asana/GenericFunctions.ts b/packages/nodes-base/nodes/Asana/GenericFunctions.ts index 05f1150a5e33d..106ede80c434a 100644 --- a/packages/nodes-base/nodes/Asana/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Asana/GenericFunctions.ts @@ -27,7 +27,7 @@ export async function asanaApiRequest( const options: IHttpRequestOptions = { headers: {}, method, - body: { data: body }, + body: method === 'GET' || method === 'HEAD' || method === 'DELETE' ? null : { data: body }, qs: query, url: uri || `https://app.asana.com/api/1.0${endpoint}`, json: true, From 711fa2b9251154b50d8e5e7cd9a857ccb2c0bec6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milorad=20FIlipovi=C4=87?= Date: Fri, 22 Dec 2023 08:41:20 +0100 Subject: [PATCH 18/32] fix(editor): Fix operation change failing in certain conditions (#8114) ## Summary This PR handles the case when there are multiple parameters with the same name but different `options` and `displayOptions`. In this case, if one of such fields is set, changing the dependent parameter value so the other should be shown causes an error in case their options are not compatible (this [check](https://github.com/n8n-io/n8n/blob/7806a65229878a473f5526bad0b94614e8bfa8aa/packages/workflow/src/NodeHelpers.ts#L786)). #### Example: LDAP node has two `options` properties with the same name: 1. `attributes` with predefined options (`add`, `replace`, `delete`). Shown when **Update** operation is selected 2. `attributes` with a collection of `attribute` objects. Shows for the **Create** operation Setting one of these parameter values and switching operation so the other is shown breaks the app. This PR checks if there is a value saved for such parameter and removes it before calling `getNodeParameters` in `valueChanged` handler. ## Related tickets and issues Fixes ADO-1589 ## Review / Merge checklist - [x] PR title and summary are descriptive. **Remember, the title automatically goes into the changelog. Use `(no-changelog)` otherwise.** ([conventions](https://github.com/n8n-io/n8n/blob/master/.github/pull_request_title_conventions.md)) - [ ] [Docs updated](https://github.com/n8n-io/n8n-docs) or follow-up ticket created. - [x] Tests included. > A bug is not considered fixed, unless a test is added to prevent it from happening again. > A feature is not complete without tests. --- cypress/e2e/5-ndv.cy.ts | 15 +++++++ cypress/pages/ndv.ts | 6 ++- .../editor-ui/src/components/NodeSettings.vue | 44 +++++++++++++++++++ 3 files changed, 64 insertions(+), 1 deletion(-) diff --git a/cypress/e2e/5-ndv.cy.ts b/cypress/e2e/5-ndv.cy.ts index b7711b36e81d5..8089a1240733a 100644 --- a/cypress/e2e/5-ndv.cy.ts +++ b/cypress/e2e/5-ndv.cy.ts @@ -490,4 +490,19 @@ describe('NDV', () => { ndv.getters.nodeVersion().should('have.text', 'Function node version 1 (Deprecated)'); ndv.actions.close(); }); + it('Should handle mismatched option attributes', () => { + workflowPage.actions.addInitialNodeToCanvas('LDAP', { keepNdvOpen: true, action: 'Create a new entry' }); + // Add some attributes in Create operation + cy.getByTestId('parameter-item').contains('Add Attributes').click(); + ndv.actions.changeNodeOperation('Update'); + // Attributes should be empty after operation change + cy.getByTestId('parameter-item').contains('Currently no items exist').should('exist'); + }); + it('Should keep RLC values after operation change', () => { + const TEST_DOC_ID = '1111'; + workflowPage.actions.addInitialNodeToCanvas('Google Sheets', { keepNdvOpen: true, action: 'Append row in sheet' }); + ndv.actions.setRLCValue('documentId', TEST_DOC_ID); + ndv.actions.changeNodeOperation('Update Row'); + ndv.getters.resourceLocatorInput('documentId').find('input').should('have.value', TEST_DOC_ID); + }); }); diff --git a/cypress/pages/ndv.ts b/cypress/pages/ndv.ts index 0eaa1361cfa82..8d8f5297e9e81 100644 --- a/cypress/pages/ndv.ts +++ b/cypress/pages/ndv.ts @@ -246,10 +246,14 @@ export class NDV extends BasePage { }); this.actions.validateExpressionPreview(fieldName, `node doesn't exist`); }, - openSettings: () => { this.getters.nodeSettingsTab().click(); }, + changeNodeOperation: (operation: string) => { + this.getters.parameterInput('operation').click(); + cy.get('.el-select-dropdown__item').contains(new RegExp(`^${operation}$`)).click({ force: true }); + this.getters.parameterInput('operation').find('input').should('have.value', operation); + }, }; } diff --git a/packages/editor-ui/src/components/NodeSettings.vue b/packages/editor-ui/src/components/NodeSettings.vue index b1d70fb972ec2..7c95ba1805d66 100644 --- a/packages/editor-ui/src/components/NodeSettings.vue +++ b/packages/editor-ui/src/components/NodeSettings.vue @@ -864,6 +864,12 @@ export default defineComponent({ } else { set(nodeParameters as object, parameterPath, newValue); } + // If value is updated, remove parameter values that have invalid options + // so getNodeParameters checks don't fail + this.removeMismatchedOptionValues(nodeType, nodeParameters, { + name: parameterPath, + value: newValue, + }); } // Get the parameters with the now new defaults according to the @@ -919,6 +925,44 @@ export default defineComponent({ this.workflowsStore.setNodeValue(updateInformation); } }, + /** + * Removes node values that are not valid options for the given parameter. + * This can happen when there are multiple node parameters with the same name + * but different options and display conditions + * @param nodeType The node type description + * @param nodeParameterValues Current node parameter values + * @param updatedParameter The parameter that was updated. Will be used to determine which parameters to remove based on their display conditions and option values + */ + removeMismatchedOptionValues( + nodeType: INodeTypeDescription, + nodeParameterValues: INodeParameters | null, + updatedParameter: { name: string; value: NodeParameterValue }, + ) { + nodeType.properties.forEach((prop) => { + const displayOptions = prop.displayOptions; + // Not processing parameters that are not set or don't have options + if (!nodeParameterValues?.hasOwnProperty(prop.name) || !displayOptions || !prop.options) { + return; + } + // Only process the parameters that should be hidden + const showCondition = displayOptions.show?.[updatedParameter.name]; + const hideCondition = displayOptions.hide?.[updatedParameter.name]; + if (showCondition === undefined && hideCondition === undefined) { + return; + } + // Every value should be a possible option + const hasValidOptions = Object.keys(nodeParameterValues).every( + (key) => (prop.options ?? []).find((option) => option.name === key) !== undefined, + ); + if ( + !hasValidOptions || + showCondition !== updatedParameter.value || + hideCondition === updatedParameter.value + ) { + unset(nodeParameterValues as object, prop.name); + } + }); + }, /** * Sets the values of the active node in the internal settings variables */ From 39e45d8b929d474f1e7587329b003fe15b61636d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milorad=20FIlipovi=C4=87?= Date: Fri, 22 Dec 2023 08:42:53 +0100 Subject: [PATCH 19/32] fix(editor): Prevent canvas undo/redo when NDV is open (#8118) ## Summary Preventing canvas undo/redo while NDV or any modal is open. We already had a NDV open check in place but looks like it was broken by unreactive ref inside `useHistoryHelper` composable. This PR fixes this by using store getter directly inside the helper method and adds modal open check. ## Related tickets and issues Fixes ADO-657 ## Review / Merge checklist - [ ] PR title and summary are descriptive. **Remember, the title automatically goes into the changelog. Use `(no-changelog)` otherwise.** ([conventions](https://github.com/n8n-io/n8n/blob/master/.github/pull_request_title_conventions.md)) - [ ] [Docs updated](https://github.com/n8n-io/n8n-docs) or follow-up ticket created. - [ ] Tests included. > A bug is not considered fixed, unless a test is added to prevent it from happening again. > A feature is not complete without tests. --- cypress/e2e/10-undo-redo.cy.ts | 34 +++++++++++++ .../editor-ui/src/components/AboutModal.vue | 7 ++- .../__tests__/useHistoryHelper.test.ts | 8 ++- .../src/composables/useHistoryHelper.ts | 49 ++++++++++++------- packages/editor-ui/src/stores/ndv.store.ts | 3 ++ packages/editor-ui/src/stores/ui.store.ts | 3 ++ 6 files changed, 84 insertions(+), 20 deletions(-) diff --git a/cypress/e2e/10-undo-redo.cy.ts b/cypress/e2e/10-undo-redo.cy.ts index e904d891b1a53..4182c75507b21 100644 --- a/cypress/e2e/10-undo-redo.cy.ts +++ b/cypress/e2e/10-undo-redo.cy.ts @@ -1,12 +1,14 @@ import { CODE_NODE_NAME, SET_NODE_NAME, EDIT_FIELDS_SET_NODE_NAME } from './../constants'; import { SCHEDULE_TRIGGER_NODE_NAME } from '../constants'; import { WorkflowPage as WorkflowPageClass } from '../pages/workflow'; +import { MessageBox as MessageBoxClass } from '../pages/modals/message-box'; import { NDV } from '../pages/ndv'; // Suite-specific constants const CODE_NODE_NEW_NAME = 'Something else'; const WorkflowPage = new WorkflowPageClass(); +const messageBox = new MessageBoxClass(); const ndv = new NDV(); describe('Undo/Redo', () => { @@ -354,4 +356,36 @@ describe('Undo/Redo', () => { .should('have.css', 'left', `637px`) .should('have.css', 'top', `501px`); }); + + it('should not undo/redo when NDV or a modal is open', () => { + WorkflowPage.actions.addInitialNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME, { keepNdvOpen: true }); + // Try while NDV is open + WorkflowPage.actions.hitUndo(); + WorkflowPage.getters.canvasNodes().should('have.have.length', 1); + ndv.getters.backToCanvas().click(); + // Try while modal is open + cy.getByTestId('menu-item').contains('About n8n').click({ force: true }); + cy.getByTestId('about-modal').should('be.visible'); + WorkflowPage.actions.hitUndo(); + WorkflowPage.getters.canvasNodes().should('have.have.length', 1); + cy.getByTestId('close-about-modal-button').click(); + // Should work now + WorkflowPage.actions.hitUndo(); + WorkflowPage.getters.canvasNodes().should('have.have.length', 0); + }); + + it('should not undo/redo when NDV or a prompt is open', () => { + WorkflowPage.actions.addInitialNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME, { keepNdvOpen: false }); + WorkflowPage.getters.workflowMenu().click(); + WorkflowPage.getters.workflowMenuItemImportFromURLItem().should('be.visible'); + WorkflowPage.getters.workflowMenuItemImportFromURLItem().click(); + // Try while prompt is open + messageBox.getters.header().click(); + WorkflowPage.actions.hitUndo(); + WorkflowPage.getters.canvasNodes().should('have.have.length', 1); + // Close prompt and try again + messageBox.actions.cancel(); + WorkflowPage.actions.hitUndo(); + WorkflowPage.getters.canvasNodes().should('have.have.length', 0); + }); }); diff --git a/packages/editor-ui/src/components/AboutModal.vue b/packages/editor-ui/src/components/AboutModal.vue index 2be4c4faa69dc..4893e4e62ee3b 100644 --- a/packages/editor-ui/src/components/AboutModal.vue +++ b/packages/editor-ui/src/components/AboutModal.vue @@ -47,7 +47,12 @@ diff --git a/packages/editor-ui/src/composables/__tests__/useHistoryHelper.test.ts b/packages/editor-ui/src/composables/__tests__/useHistoryHelper.test.ts index 9176fd9d2e5d8..a035654b1f20c 100644 --- a/packages/editor-ui/src/composables/__tests__/useHistoryHelper.test.ts +++ b/packages/editor-ui/src/composables/__tests__/useHistoryHelper.test.ts @@ -22,7 +22,13 @@ vi.mock('@/stores/history.store', () => { }), }; }); -vi.mock('@/stores/ui.store'); +vi.mock('@/stores/ui.store', () => { + return { + useUIStore: () => ({ + isAnyModalOpen: false, + }), + }; +}); vi.mock('vue-router', () => ({ useRoute: () => ({}), })); diff --git a/packages/editor-ui/src/composables/useHistoryHelper.ts b/packages/editor-ui/src/composables/useHistoryHelper.ts index a221586654339..599154f3f96f5 100644 --- a/packages/editor-ui/src/composables/useHistoryHelper.ts +++ b/packages/editor-ui/src/composables/useHistoryHelper.ts @@ -5,13 +5,14 @@ import { BulkCommand, Command } from '@/models/history'; import { useHistoryStore } from '@/stores/history.store'; import { useUIStore } from '@/stores/ui.store'; -import { ref, onMounted, onUnmounted, nextTick, getCurrentInstance } from 'vue'; +import { onMounted, onUnmounted, nextTick, getCurrentInstance } from 'vue'; import { useDebounceHelper } from './useDebounce'; import { useDeviceSupport } from 'n8n-design-system/composables/useDeviceSupport'; import { getNodeViewTab } from '@/utils/canvasUtils'; import type { Route } from 'vue-router'; const UNDO_REDO_DEBOUNCE_INTERVAL = 100; +const ELEMENT_UI_OVERLAY_SELECTOR = '.el-overlay'; export function useHistoryHelper(activeRoute: Route) { const instance = getCurrentInstance(); @@ -24,8 +25,6 @@ export function useHistoryHelper(activeRoute: Route) { const { callDebounced } = useDebounceHelper(); const { isCtrlKeyPressed } = useDeviceSupport(); - const isNDVOpen = ref(ndvStore.activeNodeName !== null); - const undo = async () => callDebounced( async () => { @@ -95,29 +94,43 @@ export function useHistoryHelper(activeRoute: Route) { } } - function trackUndoAttempt(event: KeyboardEvent) { - if (isNDVOpen.value && !event.shiftKey) { - const activeNode = ndvStore.activeNode; - if (activeNode) { - telemetry?.track('User hit undo in NDV', { node_type: activeNode.type }); - } + function trackUndoAttempt() { + const activeNode = ndvStore.activeNode; + if (activeNode) { + telemetry?.track('User hit undo in NDV', { node_type: activeNode.type }); } } + /** + * Checks if there is a Element UI dialog open by querying + * for the visible overlay element. + */ + function isMessageDialogOpen(): boolean { + return ( + document.querySelector(`${ELEMENT_UI_OVERLAY_SELECTOR}:not([style*="display: none"])`) !== + null + ); + } + function handleKeyDown(event: KeyboardEvent) { const currentNodeViewTab = getNodeViewTab(activeRoute); + const isNDVOpen = ndvStore.isNDVOpen; + const isAnyModalOpen = uiStore.isAnyModalOpen || isMessageDialogOpen(); + const undoKeysPressed = isCtrlKeyPressed(event) && event.key.toLowerCase() === 'z'; if (event.repeat || currentNodeViewTab !== MAIN_HEADER_TABS.WORKFLOW) return; - if (isCtrlKeyPressed(event) && event.key.toLowerCase() === 'z') { + if (isNDVOpen || isAnyModalOpen) { + if (isNDVOpen && undoKeysPressed && !event.shiftKey) { + trackUndoAttempt(); + } + return; + } + if (undoKeysPressed) { event.preventDefault(); - if (!isNDVOpen.value) { - if (event.shiftKey) { - void redo(); - } else { - void undo(); - } - } else if (!event.shiftKey) { - trackUndoAttempt(event); + if (event.shiftKey) { + void redo(); + } else { + void undo(); } } } diff --git a/packages/editor-ui/src/stores/ndv.store.ts b/packages/editor-ui/src/stores/ndv.store.ts index 5ca6dca6d8b6b..062cf05ac6da7 100644 --- a/packages/editor-ui/src/stores/ndv.store.ts +++ b/packages/editor-ui/src/stores/ndv.store.ts @@ -139,6 +139,9 @@ export const useNDVStore = defineStore(STORES.NDV, { return null; }, + isNDVOpen(): boolean { + return this.activeNodeName !== null; + }, }, actions: { setActiveNodeName(nodeName: string | null): void { diff --git a/packages/editor-ui/src/stores/ui.store.ts b/packages/editor-ui/src/stores/ui.store.ts index 6eeb2f1a5b2e2..9667f80e5c690 100644 --- a/packages/editor-ui/src/stores/ui.store.ts +++ b/packages/editor-ui/src/stores/ui.store.ts @@ -410,6 +410,9 @@ export const useUIStore = defineStore(STORES.UI, { const style = getComputedStyle(document.body); return Number(style.getPropertyValue('--header-height')); }, + isAnyModalOpen(): boolean { + return this.modalStack.length > 0; + }, }, actions: { setTheme(theme: ThemeOption): void { From 021add0f3957db1df8ddea3650299953347034da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E0=A4=95=E0=A4=BE=E0=A4=B0=E0=A4=A4=E0=A5=8B=E0=A4=AB?= =?UTF-8?q?=E0=A5=8D=E0=A4=AB=E0=A5=87=E0=A4=B2=E0=A4=B8=E0=A5=8D=E0=A4=95?= =?UTF-8?q?=E0=A5=8D=E0=A4=B0=E0=A4=BF=E0=A4=AA=E0=A5=8D=E0=A4=9F=E2=84=A2?= Date: Fri, 22 Dec 2023 11:28:42 +0100 Subject: [PATCH 20/32] refactor(core): Move active workflows endpoints to a decorated controller class (no-changelog) (#8101) This is a continuation of migrating all rest endpoints to decorated controller classes --- .../e2e/30-editor-after-route-changes.cy.ts | 10 +-- packages/cli/src/ActiveWorkflowRunner.ts | 80 +++--------------- packages/cli/src/Server.ts | 49 +---------- .../controllers/activeWorkflows.controller.ts | 25 ++++++ .../repositories/sharedWorkflow.repository.ts | 23 +++++- .../repositories/workflow.repository.ts | 8 ++ .../src/services/activeWorkflows.service.ts | 52 ++++++++++++ .../integration/ActiveWorkflowRunner.test.ts | 17 ++-- .../services/activeWorkflows.service.test.ts | 81 +++++++++++++++++++ packages/core/src/ActiveWorkflows.ts | 6 +- packages/editor-ui/src/api/workflows.ts | 2 +- .../editor-ui/src/stores/workflows.store.ts | 6 +- 12 files changed, 226 insertions(+), 133 deletions(-) create mode 100644 packages/cli/src/controllers/activeWorkflows.controller.ts create mode 100644 packages/cli/src/services/activeWorkflows.service.ts create mode 100644 packages/cli/test/unit/services/activeWorkflows.service.test.ts diff --git a/cypress/e2e/30-editor-after-route-changes.cy.ts b/cypress/e2e/30-editor-after-route-changes.cy.ts index 8ce31cbbdc636..656d7e9b781c3 100644 --- a/cypress/e2e/30-editor-after-route-changes.cy.ts +++ b/cypress/e2e/30-editor-after-route-changes.cy.ts @@ -99,17 +99,17 @@ const switchBetweenEditorAndHistory = () => { workflowPage.getters.canvasNodes().first().should('be.visible'); workflowPage.getters.canvasNodes().last().should('be.visible'); -} +}; const switchBetweenEditorAndWorkflowlist = () => { cy.getByTestId('menu-item').first().click(); - cy.wait(['@getUsers', '@getWorkflows', '@getActive', '@getCredentials']); + cy.wait(['@getUsers', '@getWorkflows', '@getActiveWorkflows', '@getCredentials']); cy.getByTestId('resources-list-item').first().click(); workflowPage.getters.canvasNodes().first().should('be.visible'); workflowPage.getters.canvasNodes().last().should('be.visible'); -} +}; const zoomInAndCheckNodes = () => { cy.getByTestId('zoom-in-button').click(); @@ -119,7 +119,7 @@ const zoomInAndCheckNodes = () => { workflowPage.getters.canvasNodes().first().should('not.be.visible'); workflowPage.getters.canvasNodes().last().should('not.be.visible'); -} +}; describe('Editor actions should work', () => { beforeEach(() => { @@ -199,7 +199,7 @@ describe('Editor zoom should work after route changes', () => { cy.intercept('GET', '/rest/workflow-history/workflow/*').as('getHistory'); cy.intercept('GET', '/rest/users').as('getUsers'); cy.intercept('GET', '/rest/workflows').as('getWorkflows'); - cy.intercept('GET', '/rest/active').as('getActive'); + cy.intercept('GET', '/rest/active-workflows').as('getActiveWorkflows'); cy.intercept('GET', '/rest/credentials').as('getCredentials'); switchBetweenEditorAndHistory(); diff --git a/packages/cli/src/ActiveWorkflowRunner.ts b/packages/cli/src/ActiveWorkflowRunner.ts index bb037590c674e..4542aa1603343 100644 --- a/packages/cli/src/ActiveWorkflowRunner.ts +++ b/packages/cli/src/ActiveWorkflowRunner.ts @@ -45,7 +45,6 @@ import type { import * as WebhookHelpers from '@/WebhookHelpers'; import * as WorkflowExecuteAdditionalData from '@/WorkflowExecuteAdditionalData'; -import type { User } from '@db/entities/User'; import type { WorkflowEntity } from '@db/entities/WorkflowEntity'; import { ActiveExecutions } from '@/ActiveExecutions'; import { ExecutionsService } from './executions/executions.service'; @@ -57,44 +56,40 @@ import { import { NodeTypes } from '@/NodeTypes'; import { WorkflowRunner } from '@/WorkflowRunner'; import { ExternalHooks } from '@/ExternalHooks'; -import { whereClause } from './UserManagement/UserManagementHelper'; import { WebhookNotFoundError } from './errors/response-errors/webhook-not-found.error'; -import { In } from 'typeorm'; import { WebhookService } from './services/webhook.service'; import { Logger } from './Logger'; -import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository'; import { WorkflowRepository } from '@db/repositories/workflow.repository'; import { MultiMainSetup } from '@/services/orchestration/main/MultiMainSetup.ee'; import { ActivationErrorsService } from '@/ActivationErrors.service'; -import type { Scope } from '@n8n/permissions'; import { NotFoundError } from './errors/response-errors/not-found.error'; +import { ActiveWorkflowsService } from '@/services/activeWorkflows.service'; import { WorkflowStaticDataService } from '@/workflows/workflowStaticData.service'; +interface QueuedActivation { + activationMode: WorkflowActivateMode; + lastTimeout: number; + timeout: NodeJS.Timeout; + workflowData: IWorkflowDb; +} + @Service() export class ActiveWorkflowRunner implements IWebhookManager { - activeWorkflows = new ActiveWorkflows(); - - private queuedActivations: { - [workflowId: string]: { - activationMode: WorkflowActivateMode; - lastTimeout: number; - timeout: NodeJS.Timeout; - workflowData: IWorkflowDb; - }; - } = {}; + private queuedActivations: { [workflowId: string]: QueuedActivation } = {}; constructor( private readonly logger: Logger, + private readonly activeWorkflows: ActiveWorkflows, private readonly activeExecutions: ActiveExecutions, private readonly externalHooks: ExternalHooks, private readonly nodeTypes: NodeTypes, private readonly webhookService: WebhookService, private readonly workflowRepository: WorkflowRepository, - private readonly sharedWorkflowRepository: SharedWorkflowRepository, private readonly multiMainSetup: MultiMainSetup, private readonly activationErrorsService: ActivationErrorsService, private readonly executionService: ExecutionsService, private readonly workflowStaticDataService: WorkflowStaticDataService, + private readonly activeWorkflowsService: ActiveWorkflowsService, ) {} async init() { @@ -119,7 +114,7 @@ export class ActiveWorkflowRunner implements IWebhookManager { activeWorkflowIds.push(...this.activeWorkflows.allActiveWorkflows()); - const activeWorkflows = await this.allActiveInStorage(); + const activeWorkflows = await this.activeWorkflowsService.getAllActiveIdsInStorage(); activeWorkflowIds = [...activeWorkflowIds, ...activeWorkflows]; // Make sure IDs are unique activeWorkflowIds = Array.from(new Set(activeWorkflowIds)); @@ -269,50 +264,6 @@ export class ActiveWorkflowRunner implements IWebhookManager { return this.activeWorkflows.allActiveWorkflows(); } - /** - * Get the IDs of active workflows from storage. - */ - async allActiveInStorage(options?: { user: User; scope: Scope | Scope[] }) { - const isFullAccess = !options?.user || options.user.hasGlobalScope(options.scope); - - const activationErrors = await this.activationErrorsService.getAll(); - - if (isFullAccess) { - const activeWorkflows = await this.workflowRepository.find({ - select: ['id'], - where: { active: true }, - }); - - return activeWorkflows - .map((workflow) => workflow.id) - .filter((workflowId) => !activationErrors[workflowId]); - } - - const where = whereClause({ - user: options.user, - globalScope: 'workflow:list', - entityType: 'workflow', - }); - - const activeWorkflows = await this.workflowRepository.find({ - select: ['id'], - where: { active: true }, - }); - - const activeIds = activeWorkflows.map((workflow) => workflow.id); - - Object.assign(where, { workflowId: In(activeIds) }); - - const sharings = await this.sharedWorkflowRepository.find({ - select: ['workflowId'], - where, - }); - - return sharings - .map((sharing) => sharing.workflowId) - .filter((workflowId) => !activationErrors[workflowId]); - } - /** * Returns if the workflow is stored as `active`. * @@ -328,13 +279,6 @@ export class ActiveWorkflowRunner implements IWebhookManager { return !!workflow?.active; } - /** - * Return error if there was a problem activating the workflow - */ - async getActivationError(workflowId: string) { - return this.activationErrorsService.get(workflowId); - } - /** * Register workflow-defined webhooks in the `workflow_entity` table. */ diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index e672ec9a49c94..5a7afc2a5d8f6 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -46,7 +46,7 @@ import { TEMPLATES_DIR, } from '@/constants'; import { credentialsController } from '@/credentials/credentials.controller'; -import type { CurlHelper, ExecutionRequest, WorkflowRequest } from '@/requests'; +import type { CurlHelper, ExecutionRequest } from '@/requests'; import { registerController } from '@/decorators'; import { AuthController } from '@/controllers/auth.controller'; import { BinaryDataController } from '@/controllers/binaryData.controller'; @@ -66,7 +66,6 @@ import { WorkflowStatisticsController } from '@/controllers/workflowStatistics.c import { ExternalSecretsController } from '@/ExternalSecrets/ExternalSecrets.controller.ee'; import { executionsController } from '@/executions/executions.controller'; import { isApiEnabled, loadPublicApiVersions } from '@/PublicApi'; -import { whereClause } from '@/UserManagement/UserManagementHelper'; import type { ICredentialsOverwrite, IDiagnosticInfo, IExecutionsStopData } from '@/Interfaces'; import { ActiveExecutions } from '@/ActiveExecutions'; import { CredentialsOverwrites } from '@/CredentialsOverwrites'; @@ -112,6 +111,7 @@ import { handleMfaDisable, isMfaFeatureEnabled } from './Mfa/helpers'; import type { FrontendService } from './services/frontend.service'; import { RoleService } from './services/role.service'; import { UserService } from './services/user.service'; +import { ActiveWorkflowsController } from './controllers/activeWorkflows.controller'; import { OrchestrationController } from './controllers/orchestration.controller'; import { WorkflowHistoryController } from './workflows/workflowHistory/workflowHistory.controller.ee'; import { InvitationController } from './controllers/invitation.controller'; @@ -305,6 +305,7 @@ export class Server extends AbstractServer { ), Container.get(VariablesController), Container.get(RoleController), + Container.get(ActiveWorkflowsController), ]; if (Container.get(MultiMainSetup).isEnabled) { @@ -443,50 +444,6 @@ export class Server extends AbstractServer { this.logger.warn(`Source Control initialization failed: ${error.message}`); } - // ---------------------------------------- - // Active Workflows - // ---------------------------------------- - - // Returns the active workflow ids - this.app.get( - `/${this.restEndpoint}/active`, - ResponseHelper.send(async (req: WorkflowRequest.GetAllActive) => { - return this.activeWorkflowRunner.allActiveInStorage({ - user: req.user, - scope: 'workflow:list', - }); - }), - ); - - // Returns if the workflow with the given id had any activation errors - this.app.get( - `/${this.restEndpoint}/active/error/:id`, - ResponseHelper.send(async (req: WorkflowRequest.GetActivationError) => { - const { id: workflowId } = req.params; - - const shared = await Container.get(SharedWorkflowRepository).findOne({ - relations: ['workflow'], - where: whereClause({ - user: req.user, - globalScope: 'workflow:read', - entityType: 'workflow', - entityId: workflowId, - }), - }); - - if (!shared) { - this.logger.verbose('User attempted to access workflow errors without permissions', { - workflowId, - userId: req.user.id, - }); - - throw new BadRequestError(`Workflow with ID "${workflowId}" could not be found.`); - } - - return this.activeWorkflowRunner.getActivationError(workflowId); - }), - ); - // ---------------------------------------- // curl-converter // ---------------------------------------- diff --git a/packages/cli/src/controllers/activeWorkflows.controller.ts b/packages/cli/src/controllers/activeWorkflows.controller.ts new file mode 100644 index 0000000000000..e1a427b3ec148 --- /dev/null +++ b/packages/cli/src/controllers/activeWorkflows.controller.ts @@ -0,0 +1,25 @@ +import { Service } from 'typedi'; +import { Authorized, Get, RestController } from '@/decorators'; +import { WorkflowRequest } from '@/requests'; +import { ActiveWorkflowsService } from '@/services/activeWorkflows.service'; + +@Service() +@Authorized() +@RestController('/active-workflows') +export class ActiveWorkflowsController { + constructor(private readonly activeWorkflowsService: ActiveWorkflowsService) {} + + @Get('/') + async getActiveWorkflows(req: WorkflowRequest.GetAllActive) { + return this.activeWorkflowsService.getAllActiveIdsFor(req.user); + } + + @Get('/error/:id') + async getActivationError(req: WorkflowRequest.GetActivationError) { + const { + user, + params: { id: workflowId }, + } = req; + return this.activeWorkflowsService.getActivationError(workflowId, user); + } +} diff --git a/packages/cli/src/databases/repositories/sharedWorkflow.repository.ts b/packages/cli/src/databases/repositories/sharedWorkflow.repository.ts index e8c21df37985c..eaaf1d6e0db6b 100644 --- a/packages/cli/src/databases/repositories/sharedWorkflow.repository.ts +++ b/packages/cli/src/databases/repositories/sharedWorkflow.repository.ts @@ -1,10 +1,31 @@ import { Service } from 'typedi'; -import { DataSource, Repository } from 'typeorm'; +import { DataSource, type FindOptionsWhere, Repository, In } from 'typeorm'; import { SharedWorkflow } from '../entities/SharedWorkflow'; +import { type User } from '../entities/User'; @Service() export class SharedWorkflowRepository extends Repository { constructor(dataSource: DataSource) { super(SharedWorkflow, dataSource.manager); } + + async hasAccess(workflowId: string, user: User) { + const where: FindOptionsWhere = { + workflowId, + }; + if (!user.hasGlobalScope('workflow:read')) { + where.userId = user.id; + } + return this.exist({ where }); + } + + async getSharedWorkflowIds(workflowIds: string[]) { + const sharedWorkflows = await this.find({ + select: ['workflowId'], + where: { + workflowId: In(workflowIds), + }, + }); + return sharedWorkflows.map((sharing) => sharing.workflowId); + } } diff --git a/packages/cli/src/databases/repositories/workflow.repository.ts b/packages/cli/src/databases/repositories/workflow.repository.ts index 990b2b829cb45..01415abc5c1e6 100644 --- a/packages/cli/src/databases/repositories/workflow.repository.ts +++ b/packages/cli/src/databases/repositories/workflow.repository.ts @@ -23,6 +23,14 @@ export class WorkflowRepository extends Repository { }); } + async getActiveIds() { + const activeWorkflows = await this.find({ + select: ['id'], + where: { active: true }, + }); + return activeWorkflows.map((workflow) => workflow.id); + } + async findById(workflowId: string) { return this.findOne({ where: { id: workflowId }, diff --git a/packages/cli/src/services/activeWorkflows.service.ts b/packages/cli/src/services/activeWorkflows.service.ts new file mode 100644 index 0000000000000..7684cb32756ff --- /dev/null +++ b/packages/cli/src/services/activeWorkflows.service.ts @@ -0,0 +1,52 @@ +import { Service } from 'typedi'; + +import type { User } from '@db/entities/User'; +import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository'; +import { WorkflowRepository } from '@db/repositories/workflow.repository'; +import { ActivationErrorsService } from '@/ActivationErrors.service'; +import { BadRequestError } from '@/errors/response-errors/bad-request.error'; +import { Logger } from '@/Logger'; + +@Service() +export class ActiveWorkflowsService { + constructor( + private readonly logger: Logger, + private readonly workflowRepository: WorkflowRepository, + private readonly sharedWorkflowRepository: SharedWorkflowRepository, + private readonly activationErrorsService: ActivationErrorsService, + ) {} + + async getAllActiveIdsInStorage() { + const activationErrors = await this.activationErrorsService.getAll(); + const activeWorkflowIds = await this.workflowRepository.getActiveIds(); + return activeWorkflowIds.filter((workflowId) => !activationErrors[workflowId]); + } + + async getAllActiveIdsFor(user: User) { + const activationErrors = await this.activationErrorsService.getAll(); + const activeWorkflowIds = await this.workflowRepository.getActiveIds(); + + const hasFullAccess = user.hasGlobalScope('workflow:list'); + if (hasFullAccess) { + return activeWorkflowIds.filter((workflowId) => !activationErrors[workflowId]); + } + + const sharedWorkflowIds = + await this.sharedWorkflowRepository.getSharedWorkflowIds(activeWorkflowIds); + return sharedWorkflowIds.filter((workflowId) => !activationErrors[workflowId]); + } + + async getActivationError(workflowId: string, user: User) { + const hasAccess = await this.sharedWorkflowRepository.hasAccess(workflowId, user); + if (!hasAccess) { + this.logger.verbose('User attempted to access workflow errors without permissions', { + workflowId, + userId: user.id, + }); + + throw new BadRequestError(`Workflow with ID "${workflowId}" could not be found.`); + } + + return this.activationErrorsService.get(workflowId); + } +} diff --git a/packages/cli/test/integration/ActiveWorkflowRunner.test.ts b/packages/cli/test/integration/ActiveWorkflowRunner.test.ts index d7adb25edeb57..d4f2baa5e2d9f 100644 --- a/packages/cli/test/integration/ActiveWorkflowRunner.test.ts +++ b/packages/cli/test/integration/ActiveWorkflowRunner.test.ts @@ -2,7 +2,6 @@ import { Container } from 'typedi'; import { NodeApiError, NodeOperationError, Workflow } from 'n8n-workflow'; import type { IWebhookData, WorkflowActivateMode } from 'n8n-workflow'; -import { ActiveWorkflows } from 'n8n-core'; import { ActiveExecutions } from '@/ActiveExecutions'; import { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner'; @@ -26,9 +25,9 @@ import { createOwner } from './shared/db/users'; import { createWorkflow } from './shared/db/workflows'; import { ExecutionsService } from '@/executions/executions.service'; import { WorkflowService } from '@/workflows/workflow.service'; +import { ActiveWorkflowsService } from '@/services/activeWorkflows.service'; mockInstance(ActiveExecutions); -mockInstance(ActiveWorkflows); mockInstance(Push); mockInstance(SecretsHelper); mockInstance(ExecutionsService); @@ -45,6 +44,7 @@ setSchedulerAsLoadedNode(); const externalHooks = mockInstance(ExternalHooks); +let activeWorkflowsService: ActiveWorkflowsService; let activeWorkflowRunner: ActiveWorkflowRunner; let owner: User; @@ -59,6 +59,7 @@ const NON_LEADERSHIP_CHANGE_MODES: WorkflowActivateMode[] = [ beforeAll(async () => { await testDb.init(); + activeWorkflowsService = Container.get(ActiveWorkflowsService); activeWorkflowRunner = Container.get(ActiveWorkflowRunner); owner = await createOwner(); }); @@ -90,8 +91,8 @@ describe('init()', () => { test('should start with no active workflows', async () => { await activeWorkflowRunner.init(); - const inStorage = activeWorkflowRunner.allActiveInStorage(); - await expect(inStorage).resolves.toHaveLength(0); + const inStorage = await activeWorkflowsService.getAllActiveIdsInStorage(); + expect(inStorage).toHaveLength(0); const inMemory = activeWorkflowRunner.allActiveInMemory(); expect(inMemory).toHaveLength(0); @@ -102,8 +103,8 @@ describe('init()', () => { await activeWorkflowRunner.init(); - const inStorage = activeWorkflowRunner.allActiveInStorage(); - await expect(inStorage).resolves.toHaveLength(1); + const inStorage = await activeWorkflowsService.getAllActiveIdsInStorage(); + expect(inStorage).toHaveLength(1); const inMemory = activeWorkflowRunner.allActiveInMemory(); expect(inMemory).toHaveLength(1); @@ -115,8 +116,8 @@ describe('init()', () => { await activeWorkflowRunner.init(); - const inStorage = activeWorkflowRunner.allActiveInStorage(); - await expect(inStorage).resolves.toHaveLength(2); + const inStorage = await activeWorkflowsService.getAllActiveIdsInStorage(); + expect(inStorage).toHaveLength(2); const inMemory = activeWorkflowRunner.allActiveInMemory(); expect(inMemory).toHaveLength(2); diff --git a/packages/cli/test/unit/services/activeWorkflows.service.test.ts b/packages/cli/test/unit/services/activeWorkflows.service.test.ts new file mode 100644 index 0000000000000..7432d22491a34 --- /dev/null +++ b/packages/cli/test/unit/services/activeWorkflows.service.test.ts @@ -0,0 +1,81 @@ +import type { ActivationErrorsService } from '@/ActivationErrors.service'; +import type { User } from '@db/entities/User'; +import type { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository'; +import type { WorkflowRepository } from '@db/repositories/workflow.repository'; +import { ActiveWorkflowsService } from '@/services/activeWorkflows.service'; +import { mock } from 'jest-mock-extended'; +import { BadRequestError } from '@/errors/response-errors/bad-request.error'; + +describe('ActiveWorkflowsService', () => { + const user = mock(); + const workflowRepository = mock(); + const sharedWorkflowRepository = mock(); + const activationErrorsService = mock(); + const service = new ActiveWorkflowsService( + mock(), + workflowRepository, + sharedWorkflowRepository, + activationErrorsService, + ); + const activeIds = ['1', '2', '3', '4']; + + beforeEach(() => jest.clearAllMocks()); + + describe('getAllActiveIdsInStorage', () => { + it('should filter out any workflow ids that have activation errors', async () => { + activationErrorsService.getAll.mockResolvedValue({ 1: 'some error' }); + workflowRepository.getActiveIds.mockResolvedValue(activeIds); + + const ids = await service.getAllActiveIdsInStorage(); + expect(ids).toEqual(['2', '3', '4']); + }); + }); + + describe('getAllActiveIdsFor', () => { + beforeEach(() => { + activationErrorsService.getAll.mockResolvedValue({ 1: 'some error' }); + workflowRepository.getActiveIds.mockResolvedValue(activeIds); + }); + + it('should return all workflow ids when user has full access', async () => { + user.hasGlobalScope.mockReturnValue(true); + const ids = await service.getAllActiveIdsFor(user); + + expect(ids).toEqual(['2', '3', '4']); + expect(user.hasGlobalScope).toHaveBeenCalledWith('workflow:list'); + expect(sharedWorkflowRepository.getSharedWorkflowIds).not.toHaveBeenCalled(); + }); + + it('should filter out workflow ids that the user does not have access to', async () => { + user.hasGlobalScope.mockReturnValue(false); + sharedWorkflowRepository.getSharedWorkflowIds.mockResolvedValue(['3']); + const ids = await service.getAllActiveIdsFor(user); + + expect(ids).toEqual(['3']); + expect(user.hasGlobalScope).toHaveBeenCalledWith('workflow:list'); + expect(sharedWorkflowRepository.getSharedWorkflowIds).toHaveBeenCalledWith(activeIds); + }); + }); + + describe('getActivationError', () => { + const workflowId = 'workflowId'; + + it('should throw a BadRequestError a user does not have access to the workflow id', async () => { + sharedWorkflowRepository.hasAccess.mockResolvedValue(false); + await expect(service.getActivationError(workflowId, user)).rejects.toThrow(BadRequestError); + + expect(sharedWorkflowRepository.hasAccess).toHaveBeenCalledWith(workflowId, user); + expect(activationErrorsService.get).not.toHaveBeenCalled(); + }); + + it('should return the error when the user has access', async () => { + sharedWorkflowRepository.hasAccess.mockResolvedValue(true); + activationErrorsService.get.mockResolvedValue('some-error'); + const error = await service.getActivationError(workflowId, user); + + expect(error).toEqual('some-error'); + expect(sharedWorkflowRepository.hasAccess).toHaveBeenCalledWith(workflowId, user); + expect(activationErrorsService.get).toHaveBeenCalledWith(workflowId); + }); + }); +}); diff --git a/packages/core/src/ActiveWorkflows.ts b/packages/core/src/ActiveWorkflows.ts index 9b6c4c60783c1..d42729b1f2693 100644 --- a/packages/core/src/ActiveWorkflows.ts +++ b/packages/core/src/ActiveWorkflows.ts @@ -1,3 +1,4 @@ +import { Service } from 'typedi'; import { CronJob } from 'cron'; import type { @@ -22,10 +23,9 @@ import { import type { IWorkflowData } from './Interfaces'; +@Service() export class ActiveWorkflows { - private activeWorkflows: { - [workflowId: string]: IWorkflowData; - } = {}; + private activeWorkflows: { [workflowId: string]: IWorkflowData } = {}; /** * Returns if the workflow is active in memory. diff --git a/packages/editor-ui/src/api/workflows.ts b/packages/editor-ui/src/api/workflows.ts index 2c2cbc6b54053..4f36bedac4653 100644 --- a/packages/editor-ui/src/api/workflows.ts +++ b/packages/editor-ui/src/api/workflows.ts @@ -24,7 +24,7 @@ export async function getWorkflows(context: IRestApiContext, filter?: object) { } export async function getActiveWorkflows(context: IRestApiContext) { - return makeRestApiRequest(context, 'GET', '/active'); + return makeRestApiRequest(context, 'GET', '/active-workflows'); } export async function getCurrentExecutions(context: IRestApiContext, filter: IDataObject) { diff --git a/packages/editor-ui/src/stores/workflows.store.ts b/packages/editor-ui/src/stores/workflows.store.ts index a217641863410..e89c341e9bc10 100644 --- a/packages/editor-ui/src/stores/workflows.store.ts +++ b/packages/editor-ui/src/stores/workflows.store.ts @@ -373,7 +373,11 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, { async getActivationError(id: string): Promise { const rootStore = useRootStore(); - return makeRestApiRequest(rootStore.getRestApiContext, 'GET', `/active/error/${id}`); + return makeRestApiRequest( + rootStore.getRestApiContext, + 'GET', + `/active-workflows/error/${id}`, + ); }, async fetchAllWorkflows(): Promise { From c158ca24717eb9c15ced96257f7f68a005d07259 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E0=A4=95=E0=A4=BE=E0=A4=B0=E0=A4=A4=E0=A5=8B=E0=A4=AB?= =?UTF-8?q?=E0=A5=8D=E0=A4=AB=E0=A5=87=E0=A4=B2=E0=A4=B8=E0=A5=8D=E0=A4=95?= =?UTF-8?q?=E0=A5=8D=E0=A4=B0=E0=A4=BF=E0=A4=AA=E0=A5=8D=E0=A4=9F=E2=84=A2?= Date: Fri, 22 Dec 2023 11:39:20 +0100 Subject: [PATCH 21/32] refactor(core): Upgrade more dependencies to remove axios 0.x (no-changelog) (#8105) Had to [fork localtunnel](https://github.com/n8n-io/localtunnel) to get the axios upgrade, since localtunnel doesn't seem to be actively maintained. --- docker/images/n8n/README.md | 2 +- package.json | 2 +- packages/cli/package.json | 5 +- packages/cli/src/commands/start.ts | 12 +- pnpm-lock.yaml | 169 +++++++++-------------------- 5 files changed, 62 insertions(+), 128 deletions(-) diff --git a/docker/images/n8n/README.md b/docker/images/n8n/README.md index eb8549bcbca0e..848c21c7cdf9d 100644 --- a/docker/images/n8n/README.md +++ b/docker/images/n8n/README.md @@ -63,7 +63,7 @@ You can then access n8n by opening: To be able to use webhooks which all triggers of external services like Github rely on n8n has to be reachable from the web. To make that easy n8n has a -special tunnel service (uses this code: [https://github.com/localtunnel/localtunnel](https://github.com/localtunnel/localtunnel)) which redirects requests from our servers to your local +special tunnel service (uses this code: [https://github.com/n8n-io/localtunnel](https://github.com/n8n-io/localtunnel)) which redirects requests from our servers to your local n8n instance. To use it simply start n8n with `--tunnel` diff --git a/package.json b/package.json index 90cee43f90170..4e65f45069d90 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "p-limit": "^3.1.0", "rimraf": "^5.0.1", "run-script-os": "^1.0.7", - "start-server-and-test": "^2.0.0", + "start-server-and-test": "^2.0.3", "supertest": "^6.3.3", "ts-jest": "^29.1.1", "tsc-alias": "^1.8.7", diff --git a/packages/cli/package.json b/packages/cli/package.json index 6e6f8de24a874..ceaaf4628018a 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -76,7 +76,6 @@ "@types/formidable": "^3.4.0", "@types/json-diff": "^1.0.0", "@types/jsonwebtoken": "^9.0.1", - "@types/localtunnel": "^2.0.4", "@types/lodash": "^4.14.195", "@types/passport-jwt": "^3.0.6", "@types/psl": "^1.1.0", @@ -99,6 +98,7 @@ }, "dependencies": { "@n8n/client-oauth2": "workspace:*", + "@n8n/localtunnel": "2.1.0", "@n8n/permissions": "workspace:*", "@n8n_io/license-sdk": "2.7.2", "@oclif/command": "1.8.18", @@ -145,7 +145,6 @@ "jsonwebtoken": "9.0.0", "jwks-rsa": "3.0.1", "ldapts": "4.2.6", - "localtunnel": "2.0.2", "lodash": "4.17.21", "luxon": "3.3.0", "mysql2": "2.3.3", @@ -168,7 +167,7 @@ "pg": "8.8.0", "picocolors": "1.0.0", "pkce-challenge": "3.0.0", - "posthog-node": "2.2.2", + "posthog-node": "3.2.1", "prom-client": "13.2.0", "psl": "1.9.0", "raw-body": "2.5.1", diff --git a/packages/cli/src/commands/start.ts b/packages/cli/src/commands/start.ts index e2eeecc981e17..d48ea8567f1a8 100644 --- a/packages/cli/src/commands/start.ts +++ b/packages/cli/src/commands/start.ts @@ -4,7 +4,6 @@ import { Container } from 'typedi'; import path from 'path'; import { mkdir } from 'fs/promises'; import { createReadStream, createWriteStream, existsSync } from 'fs'; -import localtunnel from 'localtunnel'; import { flags } from '@oclif/command'; import stream from 'stream'; import replaceStream from 'replacestream'; @@ -308,14 +307,13 @@ export class Start extends BaseCommand { this.instanceSettings.update({ tunnelSubdomain }); } - const tunnelSettings: localtunnel.TunnelConfig = { - host: 'https://hooks.n8n.cloud', - subdomain: tunnelSubdomain, - }; - + const { default: localtunnel } = await import('@n8n/localtunnel'); const port = config.getEnv('port'); - const webhookTunnel = await localtunnel(port, tunnelSettings); + const webhookTunnel = await localtunnel(port, { + host: 'https://hooks.n8n.cloud', + subdomain: tunnelSubdomain, + }); process.env.WEBHOOK_URL = `${webhookTunnel.url}/`; this.log(`Tunnel URL: ${process.env.WEBHOOK_URL}\n`); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 18180703effa2..11044d2760a35 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -105,8 +105,8 @@ importers: specifier: ^1.0.7 version: 1.1.6 start-server-and-test: - specifier: ^2.0.0 - version: 2.0.0 + specifier: ^2.0.3 + version: 2.0.3 supertest: specifier: ^6.3.3 version: 6.3.3 @@ -170,7 +170,7 @@ importers: dependencies: axios: specifier: 1.6.2 - version: 1.6.2 + version: 1.6.2(debug@3.2.7) packages/@n8n/nodes-langchain: dependencies: @@ -342,6 +342,9 @@ importers: '@n8n/client-oauth2': specifier: workspace:* version: link:../@n8n/client-oauth2 + '@n8n/localtunnel': + specifier: 2.1.0 + version: 2.1.0 '@n8n/n8n-nodes-langchain': specifier: workspace:* version: link:../@n8n/nodes-langchain @@ -374,7 +377,7 @@ importers: version: 7.87.0 axios: specifier: 1.6.2 - version: 1.6.2 + version: 1.6.2(debug@3.2.7) basic-auth: specifier: 2.0.1 version: 2.0.1 @@ -483,9 +486,6 @@ importers: ldapts: specifier: 4.2.6 version: 4.2.6 - localtunnel: - specifier: 2.0.2 - version: 2.0.2 lodash: specifier: 4.17.21 version: 4.17.21 @@ -550,8 +550,8 @@ importers: specifier: 3.0.0 version: 3.0.0(patch_hash=dypouzb3lve7vncq25i5fuanki) posthog-node: - specifier: 2.2.2 - version: 2.2.2 + specifier: 3.2.1 + version: 3.2.1 prom-client: specifier: 13.2.0 version: 13.2.0 @@ -661,9 +661,6 @@ importers: '@types/jsonwebtoken': specifier: ^9.0.1 version: 9.0.1 - '@types/localtunnel': - specifier: ^2.0.4 - version: 2.0.4 '@types/lodash': specifier: ^4.14.195 version: 4.14.195 @@ -732,7 +729,7 @@ importers: version: 1.11.0 axios: specifier: 1.6.2 - version: 1.6.2 + version: 1.6.2(debug@3.2.7) concat-stream: specifier: 2.0.0 version: 2.0.0 @@ -1028,7 +1025,7 @@ importers: version: 10.5.0(vue@3.3.4) axios: specifier: ^1.6.2 - version: 1.6.2 + version: 1.6.2(debug@3.2.7) chart.js: specifier: ^4.4.0 version: 4.4.0 @@ -6085,6 +6082,19 @@ packages: - '@lezer/common' dev: false + /@n8n/localtunnel@2.1.0: + resolution: {integrity: sha512-k5mb+Aeb3+4cRfWaEsMSiTsF1oARFvAuI3EOvxitrgVmiWk592h4nCI7vtjq1nsVT+PWKmB6dBoTQ0+6HHIpug==} + engines: {node: '>=18.0.0'} + hasBin: true + dependencies: + axios: 1.6.2(debug@4.3.4) + debug: 4.3.4(supports-color@8.1.1) + openurl: 1.1.1 + yargs: 17.7.2 + transitivePeerDependencies: + - supports-color + dev: false + /@n8n/tournament@1.0.2: resolution: {integrity: sha512-fTpi7F8ra5flGSVfRzohPyG7czAAKCZPlLjdKdwbLJivLoI/Ekhgodov1jfVSCVFVbwQ06gRQRxLEDzl2jl8ig==} engines: {node: '>=18.10', pnpm: '>=8.6'} @@ -7390,8 +7400,8 @@ packages: '@hapi/hoek': 9.3.0 dev: true - /@sideway/formula@3.0.0: - resolution: {integrity: sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg==} + /@sideway/formula@3.0.1: + resolution: {integrity: sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==} dev: true /@sideway/pinpoint@2.0.0: @@ -10214,12 +10224,6 @@ packages: /@types/linkify-it@3.0.2: resolution: {integrity: sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA==} - /@types/localtunnel@2.0.4: - resolution: {integrity: sha512-7WM5nlEfEKp8MpwthPa2utdy+f/7ZBxMPzu8qw6EijFFTcpzh5CXgt2YoncxWAZNOPNieMofXCKFudtDEY4bag==} - dependencies: - '@types/node': 18.16.16 - dev: true - /@types/lodash-es@4.17.6: resolution: {integrity: sha512-R+zTeVUKDdfoRxpAryaQNRKk3105Rrgx2CFRClIgRGaqDTdjsm8h6IYA8ir584W3ePzkZfst5xIgDwYrlh9HLg==} dependencies: @@ -12037,46 +12041,29 @@ packages: /axios@0.21.4: resolution: {integrity: sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==} dependencies: - follow-redirects: 1.15.2(debug@4.3.4) - transitivePeerDependencies: - - debug - dev: false - - /axios@0.21.4(debug@4.3.2): - resolution: {integrity: sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==} - dependencies: - follow-redirects: 1.15.2(debug@4.3.2) + follow-redirects: 1.15.2(debug@3.2.7) transitivePeerDependencies: - debug dev: false - /axios@0.27.2(debug@4.3.4): - resolution: {integrity: sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==} - dependencies: - follow-redirects: 1.15.2(debug@4.3.4) - form-data: 4.0.0 - transitivePeerDependencies: - - debug - - /axios@1.6.2: + /axios@1.6.2(debug@3.2.7): resolution: {integrity: sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==} dependencies: - follow-redirects: 1.15.2(debug@4.3.4) + follow-redirects: 1.15.2(debug@3.2.7) form-data: 4.0.0 proxy-from-env: 1.1.0 transitivePeerDependencies: - debug dev: false - /axios@1.6.2(debug@3.2.7): + /axios@1.6.2(debug@4.3.4): resolution: {integrity: sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==} dependencies: - follow-redirects: 1.15.2(debug@3.2.7) + follow-redirects: 1.15.2(debug@4.3.4) form-data: 4.0.0 proxy-from-env: 1.1.0 transitivePeerDependencies: - debug - dev: false /babel-core@7.0.0-bridge.0(@babel/core@7.22.9): resolution: {integrity: sha512-poPX9mZH/5CSanm50Q+1toVci6pv5KSRv/5TWCwtzQS5XEwn40BcCrgIeMFWP9CKKIniKXNxoIOnOq4VVlGXhg==} @@ -13768,18 +13755,6 @@ packages: supports-color: 8.1.1 dev: true - /debug@4.3.2: - resolution: {integrity: sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - dependencies: - ms: 2.1.2 - dev: false - /debug@4.3.4(supports-color@8.1.1): resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} engines: {node: '>=6.0'} @@ -15715,18 +15690,6 @@ packages: debug: 3.2.7(supports-color@5.5.0) dev: false - /follow-redirects@1.15.2(debug@4.3.2): - resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==} - engines: {node: '>=4.0'} - peerDependencies: - debug: '*' - peerDependenciesMeta: - debug: - optional: true - dependencies: - debug: 4.3.2 - dev: false - /follow-redirects@1.15.2(debug@4.3.4): resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==} engines: {node: '>=4.0'} @@ -15760,7 +15723,7 @@ packages: engines: {node: '>=14'} dependencies: cross-spawn: 7.0.3 - signal-exit: 4.0.2 + signal-exit: 4.1.0 dev: true /forever-agent@0.6.1: @@ -16901,7 +16864,7 @@ packages: /infisical-node@1.3.0: resolution: {integrity: sha512-tTnnExRAO/ZyqiRdnSlBisErNToYWgtunMWh+8opClEt5qjX7l6HC/b4oGo2AuR2Pf41IR+oqo+dzkM1TCvlUA==} dependencies: - axios: 1.6.2 + axios: 1.6.2(debug@3.2.7) dotenv: 16.3.1 tweetnacl: 1.0.3 tweetnacl-util: 0.15.1 @@ -18133,13 +18096,13 @@ packages: engines: {node: '>= 0.6.0'} dev: false - /joi@17.7.0: - resolution: {integrity: sha512-1/ugc8djfn93rTE3WRKdCzGGt/EtiYKxITMO4Wiv6q5JL1gl9ePt4kBsl1S499nbosspfctIQTpYIhSmHA3WAg==} + /joi@17.11.0: + resolution: {integrity: sha512-NgB+lZLNoqISVy1rZocE9PZI36bL/77ie924Ri43yEvi9GUUMPeyVIr8KdFTMUlby1p0PBYMk9spIxEUQYqrJQ==} dependencies: '@hapi/hoek': 9.3.0 '@hapi/topo': 5.1.0 '@sideway/address': 4.1.4 - '@sideway/formula': 3.0.0 + '@sideway/formula': 3.0.1 '@sideway/pinpoint': 2.0.0 dev: true @@ -19232,19 +19195,6 @@ packages: lie: 3.1.1 dev: false - /localtunnel@2.0.2: - resolution: {integrity: sha512-n418Cn5ynvJd7m/N1d9WVJISLJF/ellZnfsLnx8WBWGzxv/ntNcFkJ1o6se5quUhCplfLGBNL5tYHiq5WF3Nug==} - engines: {node: '>=8.3.0'} - hasBin: true - dependencies: - axios: 0.21.4(debug@4.3.2) - debug: 4.3.2 - openurl: 1.1.1 - yargs: 17.1.1 - transitivePeerDependencies: - - supports-color - dev: false - /locate-path@3.0.0: resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==} engines: {node: '>=6'} @@ -21790,11 +21740,12 @@ packages: xtend: 4.0.2 dev: false - /posthog-node@2.2.2: - resolution: {integrity: sha512-aXYe/D+28kF63W8Cz53t09ypEORz+ULeDCahdAqhVrRm2scbOXFbtnn0GGhvMpYe45grepLKuwui9KxrZ2ZuMw==} - engines: {node: '>=14.17.0'} + /posthog-node@3.2.1: + resolution: {integrity: sha512-ofNX3TPfZPlWErVc2EDk66cIrfp9EXeKBsXFxf8ISXK57b10ANwRnKAlf5rQjxjRKqcUWmV0d3ZfOeVeYracMw==} + engines: {node: '>=15.0.0'} dependencies: - axios: 0.27.2(debug@4.3.4) + axios: 1.6.2(debug@3.2.7) + rusha: 0.8.14 transitivePeerDependencies: - debug dev: false @@ -23013,6 +22964,10 @@ packages: hasBin: true dev: true + /rusha@0.8.14: + resolution: {integrity: sha512-cLgakCUf6PedEu15t8kbsjnwIFFR2D4RfL+W3iWFJ4iac7z4B0ZI8fxy4R3J956kAI68HclCFGL8MPoUVC3qVA==} + dev: false + /rw@1.3.3: resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==} dev: false @@ -23360,11 +23315,6 @@ packages: /signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} - /signal-exit@4.0.2: - resolution: {integrity: sha512-MY2/qGx4enyjprQnFaZsHib3Yadh3IXyV2C321GY0pjGfVBu4un0uDJkwgdxqO+Rdx8JMT8IfJIRwbYVz3Ob3Q==} - engines: {node: '>=14'} - dev: true - /signal-exit@4.1.0: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} @@ -23743,9 +23693,9 @@ packages: /standard-as-callback@2.1.0: resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} - /start-server-and-test@2.0.0: - resolution: {integrity: sha512-UqKLw0mJbfrsG1jcRLTUlvuRi9sjNuUiDOLI42r7R5fA9dsFoywAy9DoLXNYys9B886E4RCKb+qM1Gzu96h7DQ==} - engines: {node: '>=6'} + /start-server-and-test@2.0.3: + resolution: {integrity: sha512-QsVObjfjFZKJE6CS6bSKNwWZCKBG6975/jKRPPGFfFh+yOQglSeGXiNWjzgQNXdphcBI9nXbyso9tPfX4YAUhg==} + engines: {node: '>=16'} hasBin: true dependencies: arg: 5.0.2 @@ -23755,7 +23705,7 @@ packages: execa: 5.1.1 lazy-ass: 1.6.0 ps-tree: 1.2.0 - wait-on: 7.0.1(debug@4.3.4) + wait-on: 7.2.0(debug@4.3.4) transitivePeerDependencies: - supports-color dev: true @@ -25936,13 +25886,13 @@ packages: xml-name-validator: 5.0.0 dev: true - /wait-on@7.0.1(debug@4.3.4): - resolution: {integrity: sha512-9AnJE9qTjRQOlTZIldAaf/da2eW0eSRSgcqq85mXQja/DW3MriHxkpODDSUEg+Gri/rKEcXUZHe+cevvYItaog==} + /wait-on@7.2.0(debug@4.3.4): + resolution: {integrity: sha512-wCQcHkRazgjG5XoAq9jbTMLpNIjoSlZslrJ2+N9MxDsGEv1HnFoVjOCexL0ESva7Y9cu350j+DWADdk54s4AFQ==} engines: {node: '>=12.0.0'} hasBin: true dependencies: - axios: 0.27.2(debug@4.3.4) - joi: 17.7.0 + axios: 1.6.2(debug@4.3.4) + joi: 17.11.0 lodash: 4.17.21 minimist: 1.2.8 rxjs: 7.8.1 @@ -26509,19 +26459,6 @@ packages: yargs-parser: 20.2.9 dev: false - /yargs@17.1.1: - resolution: {integrity: sha512-c2k48R0PwKIqKhPMWjeiF6y2xY/gPMUlro0sgxqXpbOIohWiLNXWslsootttv7E1e73QPAMQSg5FeySbVcpsPQ==} - engines: {node: '>=12'} - dependencies: - cliui: 7.0.4 - escalade: 3.1.1 - get-caller-file: 2.0.5 - require-directory: 2.1.1 - string-width: 4.2.3 - y18n: 5.0.8 - yargs-parser: 20.2.9 - dev: false - /yargs@17.6.0: resolution: {integrity: sha512-8H/wTDqlSwoSnScvV2N/JHfLWOKuh5MVla9hqLjK3nsfyy6Y4kDSYSvkU5YCUEPOSnRXfIyx3Sq+B/IWudTo4g==} engines: {node: '>=12'} From 3a881be6c25b3e16d8c53227dc851cb420f5f1bf Mon Sep 17 00:00:00 2001 From: Tomi Turtiainen <10324676+tomi@users.noreply.github.com> Date: Fri, 22 Dec 2023 12:39:58 +0200 Subject: [PATCH 22/32] feat(core): Unify application components shutdown (#8097) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Add `ShutdownService` and `OnShutdown` decorator for more unified way to shutdown different components. Use this new way in the following components: - HTTP(S) server - Pruning service - Push connection - License --------- Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ --- packages/cli/src/AbstractServer.ts | 29 +++- packages/cli/src/ActiveWorkflowRunner.ts | 2 + packages/cli/src/License.ts | 13 ++ packages/cli/src/Server.ts | 3 +- packages/cli/src/WebhookServer.ts | 2 + packages/cli/src/commands/BaseCommand.ts | 9 +- packages/cli/src/commands/start.ts | 10 +- packages/cli/src/commands/webhook.ts | 2 +- packages/cli/src/decorators/OnShutdown.ts | 38 ++++++ packages/cli/src/push/abstract.push.ts | 13 ++ packages/cli/src/push/index.ts | 6 + packages/cli/src/services/pruning.service.ts | 14 ++ packages/cli/src/shutdown/Shutdown.service.ts | 85 ++++++++++++ .../test/unit/decorators/OnShutdown.test.ts | 76 +++++++++++ .../unit/shutdown/Shutdown.service.test.ts | 127 ++++++++++++++++++ 15 files changed, 412 insertions(+), 17 deletions(-) create mode 100644 packages/cli/src/decorators/OnShutdown.ts create mode 100644 packages/cli/src/shutdown/Shutdown.service.ts create mode 100644 packages/cli/test/unit/decorators/OnShutdown.test.ts create mode 100644 packages/cli/test/unit/shutdown/Shutdown.service.test.ts diff --git a/packages/cli/src/AbstractServer.ts b/packages/cli/src/AbstractServer.ts index e97e4ce0a6a83..679e22142a147 100644 --- a/packages/cli/src/AbstractServer.ts +++ b/packages/cli/src/AbstractServer.ts @@ -1,4 +1,4 @@ -import { Container } from 'typedi'; +import { Container, Service } from 'typedi'; import { readFile } from 'fs/promises'; import type { Server } from 'http'; import express from 'express'; @@ -9,7 +9,8 @@ import config from '@/config'; import { N8N_VERSION, inDevelopment, inTest } from '@/constants'; import { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner'; import * as Db from '@/Db'; -import type { N8nInstanceType, IExternalHooksClass } from '@/Interfaces'; +import { N8nInstanceType } from '@/Interfaces'; +import type { IExternalHooksClass } from '@/Interfaces'; import { ExternalHooks } from '@/ExternalHooks'; import { send, sendErrorResponse } from '@/ResponseHelper'; import { rawBodyReader, bodyParser, corsMiddleware } from '@/middlewares'; @@ -20,7 +21,9 @@ import { webhookRequestHandler } from '@/WebhookHelpers'; import { generateHostInstanceId } from './databases/utils/generators'; import { Logger } from '@/Logger'; import { ServiceUnavailableError } from './errors/response-errors/service-unavailable.error'; +import { OnShutdown } from '@/decorators/OnShutdown'; +@Service() export abstract class AbstractServer { protected logger: Logger; @@ -246,4 +249,26 @@ export abstract class AbstractServer { await this.externalHooks.run('n8n.ready', [this, config]); } } + + /** + * Stops the HTTP(S) server from accepting new connections. Gives all + * connections configured amount of time to finish their work and + * then closes them forcefully. + */ + @OnShutdown() + async onShutdown(): Promise { + if (!this.server) { + return; + } + + this.logger.debug(`Shutting down ${this.protocol} server`); + + this.server.close((error) => { + if (error) { + this.logger.error(`Error while shutting down ${this.protocol} server`, { error }); + } + + this.logger.debug(`${this.protocol} server shut down`); + }); + } } diff --git a/packages/cli/src/ActiveWorkflowRunner.ts b/packages/cli/src/ActiveWorkflowRunner.ts index 4542aa1603343..b45d0602d453d 100644 --- a/packages/cli/src/ActiveWorkflowRunner.ts +++ b/packages/cli/src/ActiveWorkflowRunner.ts @@ -65,6 +65,7 @@ import { ActivationErrorsService } from '@/ActivationErrors.service'; import { NotFoundError } from './errors/response-errors/not-found.error'; import { ActiveWorkflowsService } from '@/services/activeWorkflows.service'; import { WorkflowStaticDataService } from '@/workflows/workflowStaticData.service'; +import { OnShutdown } from '@/decorators/OnShutdown'; interface QueuedActivation { activationMode: WorkflowActivateMode; @@ -664,6 +665,7 @@ export class ActiveWorkflowRunner implements IWebhookManager { await this.addActiveWorkflows('leadershipChange'); } + @OnShutdown() async removeAllTriggerAndPollerBasedWorkflows() { await this.activeWorkflows.removeAllTriggerAndPollerBasedWorkflows(); } diff --git a/packages/cli/src/License.ts b/packages/cli/src/License.ts index fec5f6cd8cacb..e10bacd77e2ac 100644 --- a/packages/cli/src/License.ts +++ b/packages/cli/src/License.ts @@ -17,6 +17,7 @@ import type { BooleanLicenseFeature, N8nInstanceType, NumericLicenseFeature } fr import type { RedisServicePubSubPublisher } from './services/redis/RedisServicePubSubPublisher'; import { RedisService } from './services/redis.service'; import { MultiMainSetup } from '@/services/orchestration/main/MultiMainSetup.ee'; +import { OnShutdown } from '@/decorators/OnShutdown'; type FeatureReturnType = Partial< { @@ -30,6 +31,8 @@ export class License { private redisPublisher: RedisServicePubSubPublisher; + private isShuttingDown = false; + constructor( private readonly logger: Logger, private readonly instanceSettings: InstanceSettings, @@ -40,6 +43,11 @@ export class License { async init(instanceType: N8nInstanceType = 'main') { if (this.manager) { + this.logger.warn('License manager already initialized or shutting down'); + return; + } + if (this.isShuttingDown) { + this.logger.warn('License manager already shutting down'); return; } @@ -191,7 +199,12 @@ export class License { await this.manager.renew(); } + @OnShutdown() async shutdown() { + // Shut down License manager to unclaim any floating entitlements + // Note: While this saves a new license cert to DB, the previous entitlements are still kept in memory so that the shutdown process can complete + this.isShuttingDown = true; + if (!this.manager) { return; } diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index 5a7afc2a5d8f6..8d1dc08149351 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -6,6 +6,7 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ +import { Container, Service } from 'typedi'; import assert from 'assert'; import { exec as callbackExec } from 'child_process'; import { access as fsAccess } from 'fs/promises'; @@ -84,7 +85,6 @@ import { handleLdapInit, isLdapEnabled } from './Ldap/helpers'; import { AbstractServer } from './AbstractServer'; import { PostHogClient } from './posthog'; import { eventBus } from './eventbus'; -import { Container } from 'typedi'; import { InternalHooks } from './InternalHooks'; import { License } from './License'; import { getStatusUsingPreviousExecutionStatusMethod } from './executions/executionHelpers'; @@ -124,6 +124,7 @@ import { PasswordUtility } from './services/password.utility'; const exec = promisify(callbackExec); +@Service() export class Server extends AbstractServer { private endpointPresetCredentials: string; diff --git a/packages/cli/src/WebhookServer.ts b/packages/cli/src/WebhookServer.ts index a8daadc51a7f9..60f59f606d95e 100644 --- a/packages/cli/src/WebhookServer.ts +++ b/packages/cli/src/WebhookServer.ts @@ -1,5 +1,7 @@ +import { Service } from 'typedi'; import { AbstractServer } from '@/AbstractServer'; +@Service() export class WebhookServer extends AbstractServer { constructor() { super('webhook'); diff --git a/packages/cli/src/commands/BaseCommand.ts b/packages/cli/src/commands/BaseCommand.ts index 1949bc6f69cfc..91a356b4e1617 100644 --- a/packages/cli/src/commands/BaseCommand.ts +++ b/packages/cli/src/commands/BaseCommand.ts @@ -22,6 +22,7 @@ import { ExternalSecretsManager } from '@/ExternalSecrets/ExternalSecretsManager import { initExpressionEvaluator } from '@/ExpressionEvaluator'; import { generateHostInstanceId } from '@db/utils/generators'; import { WorkflowHistoryManager } from '@/workflows/workflowHistory/workflowHistoryManager.ee'; +import { ShutdownService } from '@/shutdown/Shutdown.service'; export abstract class BaseCommand extends Command { protected logger = Container.get(Logger); @@ -38,7 +39,7 @@ export abstract class BaseCommand extends Command { protected server?: AbstractServer; - protected isShuttingDown = false; + protected shutdownService: ShutdownService = Container.get(ShutdownService); /** * How long to wait for graceful shutdown before force killing the process. @@ -309,7 +310,7 @@ export abstract class BaseCommand extends Command { private onTerminationSignal(signal: string) { return async () => { - if (this.isShuttingDown) { + if (this.shutdownService.isShuttingDown()) { this.logger.info(`Received ${signal}. Already shutting down...`); return; } @@ -323,9 +324,9 @@ export abstract class BaseCommand extends Command { }, this.gracefulShutdownTimeoutInS * 1000); this.logger.info(`Received ${signal}. Shutting down...`); - this.isShuttingDown = true; + this.shutdownService.shutdown(); - await this.stopProcess(); + await Promise.all([this.stopProcess(), this.shutdownService.waitForShutdown()]); clearTimeout(forceShutdownTimer); }; diff --git a/packages/cli/src/commands/start.ts b/packages/cli/src/commands/start.ts index d48ea8567f1a8..be9039fd6b4ad 100644 --- a/packages/cli/src/commands/start.ts +++ b/packages/cli/src/commands/start.ts @@ -63,7 +63,7 @@ export class Start extends BaseCommand { protected activeWorkflowRunner: ActiveWorkflowRunner; - protected server = new Server(); + protected server = Container.get(Server); private pruningService: PruningService; @@ -101,14 +101,6 @@ export class Start extends BaseCommand { await this.externalHooks?.run('n8n.stop', []); - // Shut down License manager to unclaim any floating entitlements - // Note: While this saves a new license cert to DB, the previous entitlements are still kept in memory so that the shutdown process can complete - await Container.get(License).shutdown(); - - if (this.pruningService.isPruningEnabled()) { - this.pruningService.stopPruning(); - } - if (Container.get(MultiMainSetup).isEnabled) { await this.activeWorkflowRunner.removeAllTriggerAndPollerBasedWorkflows(); diff --git a/packages/cli/src/commands/webhook.ts b/packages/cli/src/commands/webhook.ts index 07374ecf03a88..1ff1dd8bb8224 100644 --- a/packages/cli/src/commands/webhook.ts +++ b/packages/cli/src/commands/webhook.ts @@ -19,7 +19,7 @@ export class Webhook extends BaseCommand { help: flags.help({ char: 'h' }), }; - protected server = new WebhookServer(); + protected server = Container.get(WebhookServer); constructor(argv: string[], cmdConfig: IConfig) { super(argv, cmdConfig); diff --git a/packages/cli/src/decorators/OnShutdown.ts b/packages/cli/src/decorators/OnShutdown.ts new file mode 100644 index 0000000000000..87e8a6a45773d --- /dev/null +++ b/packages/cli/src/decorators/OnShutdown.ts @@ -0,0 +1,38 @@ +import { Container } from 'typedi'; +import { ApplicationError } from 'n8n-workflow'; +import { type ServiceClass, ShutdownService } from '@/shutdown/Shutdown.service'; + +/** + * Decorator that registers a method as a shutdown hook. The method will + * be called when the application is shutting down. + * + * Priority is used to determine the order in which the hooks are called. + * + * NOTE: Requires also @Service() decorator to be used on the class. + * + * @example + * ```ts + * @Service() + * class MyClass { + * @OnShutdown() + * async shutdown() { + * // Will be called when the app is shutting down + * } + * } + * ``` + */ +export const OnShutdown = + (priority = 100): MethodDecorator => + (prototype, propertyKey, descriptor) => { + const serviceClass = prototype.constructor as ServiceClass; + const methodName = String(propertyKey); + // TODO: assert that serviceClass is decorated with @Service + if (typeof descriptor?.value === 'function') { + Container.get(ShutdownService).register(priority, { serviceClass, methodName }); + } else { + const name = `${serviceClass.name}.${methodName}()`; + throw new ApplicationError( + `${name} must be a method on ${serviceClass.name} to use "OnShutdown"`, + ); + } + }; diff --git a/packages/cli/src/push/abstract.push.ts b/packages/cli/src/push/abstract.push.ts index f990e212f695f..42adadaa7beae 100644 --- a/packages/cli/src/push/abstract.push.ts +++ b/packages/cli/src/push/abstract.push.ts @@ -94,4 +94,17 @@ export abstract class AbstractPush extends EventEmitter { this.sendToSessions(type, data, userSessionIds); } + + /** + * Closes all push existing connections + */ + closeAllConnections() { + for (const sessionId in this.connections) { + // Signal the connection that we want to close it. + // We are not removing the sessions here because it should be + // the implementation's responsibility to do so once the connection + // has actually closed. + this.close(this.connections[sessionId]); + } + } } diff --git a/packages/cli/src/push/index.ts b/packages/cli/src/push/index.ts index e89c5a7a5b551..d8705a475c5fe 100644 --- a/packages/cli/src/push/index.ts +++ b/packages/cli/src/push/index.ts @@ -14,6 +14,7 @@ import { WebSocketPush } from './websocket.push'; import type { PushResponse, SSEPushRequest, WebSocketPushRequest } from './types'; import type { IPushDataType } from '@/Interfaces'; import type { User } from '@db/entities/User'; +import { OnShutdown } from '@/decorators/OnShutdown'; const useWebSockets = config.getEnv('push.backend') === 'websocket'; @@ -70,6 +71,11 @@ export class Push extends EventEmitter { sendToUsers(type: IPushDataType, data: D, userIds: Array) { this.backend.sendToUsers(type, data, userIds); } + + @OnShutdown() + onShutdown(): void { + this.backend.closeAllConnections(); + } } export const setupPushServer = (restEndpoint: string, server: Server, app: Application) => { diff --git a/packages/cli/src/services/pruning.service.ts b/packages/cli/src/services/pruning.service.ts index 0727881a0857b..10b7d589c5605 100644 --- a/packages/cli/src/services/pruning.service.ts +++ b/packages/cli/src/services/pruning.service.ts @@ -10,6 +10,7 @@ import { ExecutionRepository } from '@db/repositories/execution.repository'; import { Logger } from '@/Logger'; import { ExecutionEntity } from '@db/entities/ExecutionEntity'; import { jsonStringify } from 'n8n-workflow'; +import { OnShutdown } from '@/decorators/OnShutdown'; @Service() export class PruningService { @@ -24,6 +25,8 @@ export class PruningService { public hardDeletionTimeout: NodeJS.Timeout | undefined; + private isShuttingDown = false; + constructor( private readonly logger: Logger, private readonly executionRepository: ExecutionRepository, @@ -54,6 +57,11 @@ export class PruningService { * @important Call this method only after DB migrations have completed. */ startPruning() { + if (this.isShuttingDown) { + this.logger.warn('[Pruning] Cannot start pruning while shutting down'); + return; + } + this.logger.debug('[Pruning] Starting soft-deletion and hard-deletion timers'); this.setSoftDeletionInterval(); @@ -158,6 +166,12 @@ export class PruningService { this.logger.debug('[Pruning] Soft-deleted executions', { count: result.affected }); } + @OnShutdown() + shutdown(): void { + this.isShuttingDown = true; + this.stopPruning(); + } + /** * Permanently remove all soft-deleted executions and their binary data, in a pruning cycle. * @return Delay in ms after which the next cycle should be started diff --git a/packages/cli/src/shutdown/Shutdown.service.ts b/packages/cli/src/shutdown/Shutdown.service.ts new file mode 100644 index 0000000000000..b52d8ab11a0e3 --- /dev/null +++ b/packages/cli/src/shutdown/Shutdown.service.ts @@ -0,0 +1,85 @@ +import { Container, Service } from 'typedi'; +import { ApplicationError, ErrorReporterProxy, assert } from 'n8n-workflow'; +import { Logger } from '@/Logger'; + +export interface ServiceClass { + new (): Record Promise | void>; +} + +export interface ShutdownHandler { + serviceClass: ServiceClass; + methodName: string; +} + +/** Error reported when a listener fails to shutdown gracefully */ +export class ComponentShutdownError extends ApplicationError { + constructor(componentName: string, cause: Error) { + super('Failed to shutdown gracefully', { + level: 'error', + cause, + extra: { component: componentName }, + }); + } +} + +/** Service responsible for orchestrating a graceful shutdown of the application */ +@Service() +export class ShutdownService { + private readonly handlersByPriority: ShutdownHandler[][] = []; + + private shutdownPromise: Promise | undefined; + + constructor(private readonly logger: Logger) {} + + /** Registers given listener to be notified when the application is shutting down */ + register(priority: number, handler: ShutdownHandler) { + if (!this.handlersByPriority[priority]) { + this.handlersByPriority[priority] = []; + } + this.handlersByPriority[priority].push(handler); + } + + /** Signals all registered listeners that the application is shutting down */ + shutdown() { + if (this.shutdownPromise) { + throw new ApplicationError('App is already shutting down'); + } + + this.shutdownPromise = this.startShutdown(); + } + + /** Returns a promise that resolves when all the registered listeners have shut down */ + async waitForShutdown(): Promise { + if (!this.shutdownPromise) { + throw new ApplicationError('App is not shutting down'); + } + + await this.shutdownPromise; + } + + isShuttingDown() { + return !!this.shutdownPromise; + } + + private async startShutdown() { + const handlers = Object.values(this.handlersByPriority).reverse(); + for (const handlerGroup of handlers) { + await Promise.allSettled( + handlerGroup.map(async (handler) => this.shutdownComponent(handler)), + ); + } + } + + private async shutdownComponent({ serviceClass, methodName }: ShutdownHandler) { + const name = `${serviceClass.name}.${methodName}()`; + try { + this.logger.debug(`Shutting down component "${name}"`); + const service = Container.get(serviceClass); + const method = service[methodName]; + await method.call(service); + } catch (error) { + assert(error instanceof Error); + ErrorReporterProxy.error(new ComponentShutdownError(name, error)); + } + } +} diff --git a/packages/cli/test/unit/decorators/OnShutdown.test.ts b/packages/cli/test/unit/decorators/OnShutdown.test.ts new file mode 100644 index 0000000000000..1870d95122fd6 --- /dev/null +++ b/packages/cli/test/unit/decorators/OnShutdown.test.ts @@ -0,0 +1,76 @@ +import Container, { Service } from 'typedi'; +import { OnShutdown } from '@/decorators/OnShutdown'; +import { ShutdownService } from '@/shutdown/Shutdown.service'; +import { mock } from 'jest-mock-extended'; + +describe('OnShutdown', () => { + let shutdownService: ShutdownService; + let registerSpy: jest.SpyInstance; + + beforeEach(() => { + shutdownService = new ShutdownService(mock()); + Container.set(ShutdownService, shutdownService); + registerSpy = jest.spyOn(shutdownService, 'register'); + }); + + it('should register a methods that is decorated with OnShutdown', () => { + @Service() + class TestClass { + @OnShutdown() + async onShutdown() {} + } + + expect(shutdownService.register).toHaveBeenCalledTimes(1); + expect(shutdownService.register).toHaveBeenCalledWith(100, { + methodName: 'onShutdown', + serviceClass: TestClass, + }); + }); + + it('should register multiple methods in the same class', () => { + @Service() + class TestClass { + @OnShutdown() + async one() {} + + @OnShutdown() + async two() {} + } + + expect(shutdownService.register).toHaveBeenCalledTimes(2); + expect(shutdownService.register).toHaveBeenCalledWith(100, { + methodName: 'one', + serviceClass: TestClass, + }); + expect(shutdownService.register).toHaveBeenCalledWith(100, { + methodName: 'two', + serviceClass: TestClass, + }); + }); + + it('should use the given priority', () => { + class TestClass { + @OnShutdown(10) + async onShutdown() { + // Will be called when the app is shutting down + } + } + + expect(shutdownService.register).toHaveBeenCalledTimes(1); + // @ts-expect-error We are checking internal parts of the shutdown service + expect(shutdownService.handlersByPriority[10].length).toEqual(1); + }); + + it('should throw an error if the decorated member is not a function', () => { + expect(() => { + @Service() + class TestClass { + // @ts-expect-error Testing invalid code + @OnShutdown() + onShutdown = 'not a function'; + } + + new TestClass(); + }).toThrow('TestClass.onShutdown() must be a method on TestClass to use "OnShutdown"'); + }); +}); diff --git a/packages/cli/test/unit/shutdown/Shutdown.service.test.ts b/packages/cli/test/unit/shutdown/Shutdown.service.test.ts new file mode 100644 index 0000000000000..d0f761524ee92 --- /dev/null +++ b/packages/cli/test/unit/shutdown/Shutdown.service.test.ts @@ -0,0 +1,127 @@ +import { ApplicationError, ErrorReporterProxy } from 'n8n-workflow'; +import { mock } from 'jest-mock-extended'; +import type { ServiceClass } from '@/shutdown/Shutdown.service'; +import { ShutdownService } from '@/shutdown/Shutdown.service'; +import Container from 'typedi'; + +class MockComponent { + onShutdown() {} +} + +describe('ShutdownService', () => { + let shutdownService: ShutdownService; + let mockComponent: MockComponent; + let onShutdownSpy: jest.SpyInstance; + let mockErrorReporterProxy: jest.SpyInstance; + + beforeEach(() => { + shutdownService = new ShutdownService(mock()); + mockComponent = new MockComponent(); + Container.set(MockComponent, mockComponent); + onShutdownSpy = jest.spyOn(mockComponent, 'onShutdown'); + mockErrorReporterProxy = jest.spyOn(ErrorReporterProxy, 'error').mockImplementation(() => {}); + }); + + describe('shutdown', () => { + it('should signal shutdown', () => { + shutdownService.register(10, { + serviceClass: MockComponent as unknown as ServiceClass, + methodName: 'onShutdown', + }); + shutdownService.shutdown(); + expect(onShutdownSpy).toBeCalledTimes(1); + }); + + it('should signal shutdown in the priority order', async () => { + class MockService { + onShutdownHighPrio() {} + + onShutdownLowPrio() {} + } + + const order: string[] = []; + const mockService = new MockService(); + Container.set(MockService, mockService); + + jest.spyOn(mockService, 'onShutdownHighPrio').mockImplementation(() => order.push('high')); + jest.spyOn(mockService, 'onShutdownLowPrio').mockImplementation(() => order.push('low')); + + shutdownService.register(100, { + serviceClass: MockService as unknown as ServiceClass, + methodName: 'onShutdownHighPrio', + }); + + shutdownService.register(10, { + serviceClass: MockService as unknown as ServiceClass, + methodName: 'onShutdownLowPrio', + }); + + shutdownService.shutdown(); + await shutdownService.waitForShutdown(); + expect(order).toEqual(['high', 'low']); + }); + + it('should throw error if shutdown is already in progress', () => { + shutdownService.register(10, { + methodName: 'onShutdown', + serviceClass: MockComponent as unknown as ServiceClass, + }); + shutdownService.shutdown(); + expect(() => shutdownService.shutdown()).toThrow('App is already shutting down'); + }); + + it('should report error if component shutdown fails', async () => { + const componentError = new Error('Something went wrong'); + onShutdownSpy.mockImplementation(() => { + throw componentError; + }); + shutdownService.register(10, { + serviceClass: MockComponent as unknown as ServiceClass, + methodName: 'onShutdown', + }); + shutdownService.shutdown(); + await shutdownService.waitForShutdown(); + + expect(mockErrorReporterProxy).toHaveBeenCalledTimes(1); + const error = mockErrorReporterProxy.mock.calls[0][0]; + expect(error).toBeInstanceOf(ApplicationError); + expect(error.message).toBe('Failed to shutdown gracefully'); + expect(error.extra).toEqual({ + component: 'MockComponent.onShutdown()', + }); + expect(error.cause).toBe(componentError); + }); + }); + + describe('waitForShutdown', () => { + it('should wait for shutdown', async () => { + shutdownService.register(10, { + serviceClass: MockComponent as unknown as ServiceClass, + methodName: 'onShutdown', + }); + shutdownService.shutdown(); + await expect(shutdownService.waitForShutdown()).resolves.toBeUndefined(); + }); + + it('should throw error if app is not shutting down', async () => { + await expect(async () => shutdownService.waitForShutdown()).rejects.toThrow( + 'App is not shutting down', + ); + }); + }); + + describe('isShuttingDown', () => { + it('should return true if app is shutting down', () => { + shutdownService.register(10, { + serviceClass: MockComponent as unknown as ServiceClass, + methodName: 'onShutdown', + }); + shutdownService.shutdown(); + expect(shutdownService.isShuttingDown()).toBe(true); + }); + + it('should return false if app is not shutting down', () => { + expect(shutdownService.isShuttingDown()).toBe(false); + }); + }); +}); From ab74bade05cb30e7fa65a491789a3df3ab7bf8b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milorad=20FIlipovi=C4=87?= Date: Fri, 22 Dec 2023 12:50:36 +0100 Subject: [PATCH 23/32] feat(editor): Add node execution status indicator to output panel (#8124) ## Summary Adding node execution status indicator to the output panel ([Figma HiFi](https://www.figma.com/file/iUduV3M4W5wZT7Gw5vgDn1/NDV-output-pane-success-state)). ## Related tickets and issues Fixes ADO-480 ## Review / Merge checklist - [x] PR title and summary are descriptive. **Remember, the title automatically goes into the changelog. Use `(no-changelog)` otherwise.** ([conventions](https://github.com/n8n-io/n8n/blob/master/.github/pull_request_title_conventions.md)) - [ ] [Docs updated](https://github.com/n8n-io/n8n-docs) or follow-up ticket created. - [x] Tests included. > A bug is not considered fixed, unless a test is added to prevent it from happening again. > A feature is not complete without tests. --- cypress/e2e/5-ndv.cy.ts | 23 +++++++ cypress/pages/ndv.ts | 2 + .../src/components/N8nInfoTip/InfoTip.vue | 69 +++++++++++++------ .../__snapshots__/InfoTip.spec.ts.snap | 4 +- .../src/components/N8nTooltip/Tooltip.vue | 2 +- .../editor-ui/src/components/OutputPanel.vue | 22 ++---- packages/editor-ui/src/components/RunInfo.vue | 41 ++++++++++- .../__snapshots__/RunDataSchema.test.ts.snap | 2 +- .../editor-ui/src/mixins/executionsHelpers.ts | 3 +- .../editor-ui/src/mixins/genericHelpers.ts | 9 --- .../src/plugins/i18n/locales/en.json | 2 + .../src/utils/formatters/dateFormatter.ts | 12 ++++ 12 files changed, 135 insertions(+), 56 deletions(-) create mode 100644 packages/editor-ui/src/utils/formatters/dateFormatter.ts diff --git a/cypress/e2e/5-ndv.cy.ts b/cypress/e2e/5-ndv.cy.ts index 8089a1240733a..4249018b89bbc 100644 --- a/cypress/e2e/5-ndv.cy.ts +++ b/cypress/e2e/5-ndv.cy.ts @@ -490,6 +490,29 @@ describe('NDV', () => { ndv.getters.nodeVersion().should('have.text', 'Function node version 1 (Deprecated)'); ndv.actions.close(); }); + it('should properly show node execution indicator', () => { + workflowPage.actions.addInitialNodeToCanvas('Code'); + workflowPage.actions.openNode('Code'); + // Should not show run info before execution + ndv.getters.nodeRunSuccessIndicator().should('not.exist'); + ndv.getters.nodeRunErrorIndicator().should('not.exist'); + ndv.getters.nodeExecuteButton().click(); + ndv.getters.nodeRunSuccessIndicator().should('exist'); + }); + it('should properly show node execution indicator for multiple nodes', () => { + workflowPage.actions.addInitialNodeToCanvas('Code'); + workflowPage.actions.openNode('Code'); + ndv.actions.typeIntoParameterInput('jsCode', 'testets'); + ndv.getters.backToCanvas().click(); + workflowPage.actions.executeWorkflow(); + // Manual tigger node should show success indicator + workflowPage.actions.openNode('When clicking "Execute Workflow"'); + ndv.getters.nodeRunSuccessIndicator().should('exist'); + // Code node should show error + ndv.getters.backToCanvas().click(); + workflowPage.actions.openNode('Code'); + ndv.getters.nodeRunErrorIndicator().should('exist'); + }); it('Should handle mismatched option attributes', () => { workflowPage.actions.addInitialNodeToCanvas('LDAP', { keepNdvOpen: true, action: 'Create a new entry' }); // Add some attributes in Create operation diff --git a/cypress/pages/ndv.ts b/cypress/pages/ndv.ts index 8d8f5297e9e81..66116b0fc4e09 100644 --- a/cypress/pages/ndv.ts +++ b/cypress/pages/ndv.ts @@ -98,6 +98,8 @@ export class NDV extends BasePage { pagination: () => cy.getByTestId('ndv-data-pagination'), nodeVersion: () => cy.getByTestId('node-version'), nodeSettingsTab: () => cy.getByTestId('tab-settings'), + nodeRunSuccessIndicator: () => cy.getByTestId('node-run-info-success'), + nodeRunErrorIndicator: () => cy.getByTestId('node-run-info-danger'), }; actions = { diff --git a/packages/design-system/src/components/N8nInfoTip/InfoTip.vue b/packages/design-system/src/components/N8nInfoTip/InfoTip.vue index 7a81cbb0fd003..394b994c8e7c8 100644 --- a/packages/design-system/src/components/N8nInfoTip/InfoTip.vue +++ b/packages/design-system/src/components/N8nInfoTip/InfoTip.vue @@ -2,6 +2,7 @@
- - + + - + @@ -48,7 +49,7 @@ export default defineComponent({ type: String, default: 'info', validator: (value: string): boolean => - ['info', 'info-light', 'warning', 'danger'].includes(value), + ['info', 'info-light', 'warning', 'danger', 'success'].includes(value), }, type: { type: String, @@ -64,10 +65,50 @@ export default defineComponent({ default: 'top', }, }, + computed: { + iconData(): { icon: string; color: string } { + switch (this.theme) { + case 'info': + return { + icon: 'info-circle', + color: '--color-text-light)', + }; + case 'info-light': + return { + icon: 'info-circle', + color: 'var(--color-foreground-dark)', + }; + case 'warning': + return { + icon: 'exclamation-triangle', + color: 'var(--color-warning)', + }; + case 'danger': + return { + icon: 'exclamation-triangle', + color: 'var(--color-danger)', + }; + case 'success': + return { + icon: 'check-circle', + color: 'var(--color-success)', + }; + default: + return { + icon: 'info-circle', + color: '--color-text-light)', + }; + } + }, + }, }); diff --git a/packages/design-system/src/components/N8nInfoTip/__tests__/__snapshots__/InfoTip.spec.ts.snap b/packages/design-system/src/components/N8nInfoTip/__tests__/__snapshots__/InfoTip.spec.ts.snap index dc755e961d5ee..3a49e215d5349 100644 --- a/packages/design-system/src/components/N8nInfoTip/__tests__/__snapshots__/InfoTip.spec.ts.snap +++ b/packages/design-system/src/components/N8nInfoTip/__tests__/__snapshots__/InfoTip.spec.ts.snap @@ -1,9 +1,9 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`N8nInfoTip > should render correctly as note 1`] = `""`; +exports[`N8nInfoTip > should render correctly as note 1`] = `"
Need help doing something?Open docs
"`; exports[`N8nInfoTip > should render correctly as tooltip 1`] = ` -"
+"
" `; diff --git a/packages/design-system/src/components/N8nTooltip/Tooltip.vue b/packages/design-system/src/components/N8nTooltip/Tooltip.vue index ee8c53683a4c6..c49df4118457b 100644 --- a/packages/design-system/src/components/N8nTooltip/Tooltip.vue +++ b/packages/design-system/src/components/N8nTooltip/Tooltip.vue @@ -1,5 +1,5 @@