From 388a83dfbdc6ac301e4df704666df9f09fb7d0b3 Mon Sep 17 00:00:00 2001 From: Jon Date: Wed, 18 Dec 2024 10:55:40 +0000 Subject: [PATCH 01/14] fix(Elasticsearch Node): Fix issue stopping search queries being sent (#11464) --- .../ElasticsearchApi.credentials.ts | 2 +- .../Elasticsearch/Elasticsearch.node.ts | 4 +- .../Elastic/Elasticsearch/GenericFunctions.ts | 4 +- .../tests/GenericFunctions.test.ts | 112 ++++++++++++++++++ 4 files changed, 117 insertions(+), 5 deletions(-) create mode 100644 packages/nodes-base/nodes/Elastic/Elasticsearch/tests/GenericFunctions.test.ts diff --git a/packages/nodes-base/credentials/ElasticsearchApi.credentials.ts b/packages/nodes-base/credentials/ElasticsearchApi.credentials.ts index b487ce894fa22..25811fba80afa 100644 --- a/packages/nodes-base/credentials/ElasticsearchApi.credentials.ts +++ b/packages/nodes-base/credentials/ElasticsearchApi.credentials.ts @@ -56,7 +56,7 @@ export class ElasticsearchApi implements ICredentialType { test: ICredentialTestRequest = { request: { - baseURL: '={{$credentials.baseUrl}}', + baseURL: '={{$credentials.baseUrl}}'.replace(/\/$/, ''), url: '/_xpack?human=false', skipSslCertificateValidation: '={{$credentials.ignoreSSLIssues}}', }, diff --git a/packages/nodes-base/nodes/Elastic/Elasticsearch/Elasticsearch.node.ts b/packages/nodes-base/nodes/Elastic/Elasticsearch/Elasticsearch.node.ts index d13d237514984..ca5906d6ec5c7 100644 --- a/packages/nodes-base/nodes/Elastic/Elasticsearch/Elasticsearch.node.ts +++ b/packages/nodes-base/nodes/Elastic/Elasticsearch/Elasticsearch.node.ts @@ -172,7 +172,7 @@ export class Elasticsearch implements INodeType { } else { responseData = await elasticsearchApiRequest.call( this, - 'GET', + 'POST', `/${indexId}/_search`, body, qs, @@ -184,7 +184,7 @@ export class Elasticsearch implements INodeType { responseData = await elasticsearchApiRequest.call( this, - 'GET', + 'POST', `/${indexId}/_search`, body, qs, diff --git a/packages/nodes-base/nodes/Elastic/Elasticsearch/GenericFunctions.ts b/packages/nodes-base/nodes/Elastic/Elasticsearch/GenericFunctions.ts index 333a412a02ce2..9bfce49d80954 100644 --- a/packages/nodes-base/nodes/Elastic/Elasticsearch/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Elastic/Elasticsearch/GenericFunctions.ts @@ -20,7 +20,7 @@ export async function elasticsearchBulkApiRequest(this: IExecuteFunctions, body: method: 'POST', headers: { 'Content-Type': 'application/x-ndjson' }, body: bulkBody, - url: `${baseUrl}/_bulk`, + url: `${baseUrl.replace(/\/$/, '')}/_bulk`, skipSslCertificateValidation: ignoreSSLIssues, returnFullResponse: true, ignoreHttpStatusErrors: true, @@ -66,7 +66,7 @@ export async function elasticsearchApiRequest( method, body, qs, - url: `${baseUrl}${endpoint}`, + url: `${baseUrl.replace(/\/$/, '')}${endpoint}`, json: true, skipSslCertificateValidation: ignoreSSLIssues, }; diff --git a/packages/nodes-base/nodes/Elastic/Elasticsearch/tests/GenericFunctions.test.ts b/packages/nodes-base/nodes/Elastic/Elasticsearch/tests/GenericFunctions.test.ts new file mode 100644 index 0000000000000..a57834441099e --- /dev/null +++ b/packages/nodes-base/nodes/Elastic/Elasticsearch/tests/GenericFunctions.test.ts @@ -0,0 +1,112 @@ +import { type IExecuteFunctions, NodeApiError } from 'n8n-workflow'; + +import { elasticsearchApiRequest } from '../GenericFunctions'; + +describe('Elasticsearch -> elasticsearchApiRequest', () => { + let mockExecuteFunctions: IExecuteFunctions; + + const mockHttpRequestWithAuthentication = jest.fn(); + + const setupMockFunctions = () => { + mockExecuteFunctions = { + getCredentials: jest.fn().mockResolvedValue({ + baseUrl: 'https://example.com', + ignoreSSLIssues: false, + }), + helpers: { + httpRequestWithAuthentication: mockHttpRequestWithAuthentication, + }, + getNode: jest.fn().mockReturnValue({}), + } as unknown as IExecuteFunctions; + jest.clearAllMocks(); + }; + + beforeEach(() => { + setupMockFunctions(); + mockHttpRequestWithAuthentication.mockClear(); + }); + + const response = { success: true }; + + it('should make a successful GET API request', async () => { + mockHttpRequestWithAuthentication.mockResolvedValue(response); + + const result = await elasticsearchApiRequest.call( + mockExecuteFunctions, + 'GET', + '/test-endpoint', + ); + + expect(result).toEqual(response); + expect(mockExecuteFunctions.helpers.httpRequestWithAuthentication).toHaveBeenCalledWith( + 'elasticsearchApi', + expect.objectContaining({ + method: 'GET', + url: 'https://example.com/test-endpoint', + json: true, + skipSslCertificateValidation: false, + }), + ); + }); + + it('should make a successful POST API request', async () => { + const body = { key: 'value' }; + + mockHttpRequestWithAuthentication.mockResolvedValue(response); + + const result = await elasticsearchApiRequest.call( + mockExecuteFunctions, + 'POST', + '/test-endpoint', + body, + ); + + expect(result).toEqual(response); + expect(mockExecuteFunctions.helpers.httpRequestWithAuthentication).toHaveBeenCalledWith( + 'elasticsearchApi', + expect.objectContaining({ + body, + method: 'POST', + url: 'https://example.com/test-endpoint', + json: true, + skipSslCertificateValidation: false, + }), + ); + }); + + it('should handle API request errors', async () => { + const errorResponse = { message: 'Error occurred' }; + mockHttpRequestWithAuthentication.mockRejectedValue(errorResponse); + + await expect( + elasticsearchApiRequest.call(mockExecuteFunctions, 'GET', '/test-endpoint'), + ).rejects.toThrow(NodeApiError); + + expect(mockExecuteFunctions.helpers.httpRequestWithAuthentication).toHaveBeenCalledWith( + 'elasticsearchApi', + expect.objectContaining({ + method: 'GET', + url: 'https://example.com/test-endpoint', + json: true, + skipSslCertificateValidation: false, + }), + ); + }); + + it('should ignore trailing slashes in the base URL', async () => { + mockHttpRequestWithAuthentication.mockResolvedValue(response); + + mockExecuteFunctions.getCredentials = jest.fn().mockResolvedValue({ + baseUrl: 'https://elastic.domain.com/', + ignoreSSLIssues: false, + }); + await elasticsearchApiRequest.call(mockExecuteFunctions, 'GET', '/test-endpoint'); + + expect(mockExecuteFunctions.helpers.httpRequestWithAuthentication).toHaveBeenCalledWith( + 'elasticsearchApi', + expect.objectContaining({ + url: 'https://elastic.domain.com/test-endpoint', + }), + ); + }); +}); From 6ba91b5e1ed197c67146347a6f6e663ecdf3de48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20G=C3=B3mez=20Morales?= Date: Wed, 18 Dec 2024 12:24:38 +0100 Subject: [PATCH 02/14] fix(editor): Set dangerouslyUseHTMLString in composable (#12280) --- packages/editor-ui/src/composables/useMessage.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/editor-ui/src/composables/useMessage.ts b/packages/editor-ui/src/composables/useMessage.ts index 22bb50fc698c4..9f5e2742ac75a 100644 --- a/packages/editor-ui/src/composables/useMessage.ts +++ b/packages/editor-ui/src/composables/useMessage.ts @@ -26,6 +26,7 @@ export function useMessage() { ...(config ?? (typeof configOrTitle === 'object' ? configOrTitle : {})), cancelButtonClass: 'btn--cancel', confirmButtonClass: 'btn--confirm', + dangerouslyUseHTMLString: true, }; if (typeof configOrTitle === 'string') { @@ -49,6 +50,7 @@ export function useMessage() { distinguishCancelAndClose: true, showClose: config?.showClose ?? false, closeOnClickModal: false, + dangerouslyUseHTMLString: true, ...(config ?? (typeof configOrTitle === 'object' ? configOrTitle : {})), }; @@ -74,6 +76,7 @@ export function useMessage() { ...(config ?? (typeof configOrTitle === 'object' ? configOrTitle : {})), cancelButtonClass: 'btn--cancel', confirmButtonClass: 'btn--confirm', + dangerouslyUseHTMLString: true, }; if (typeof configOrTitle === 'string') { From c3e874c4dd7c019fa4a51ad4a0c07210494c5217 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milorad=20FIlipovi=C4=87?= Date: Wed, 18 Dec 2024 16:08:26 +0100 Subject: [PATCH 03/14] fix(editor): Address workflow inputs product feedback (#12284) --- .../ResourceMapper/ResourceMapper.vue | 4 ++ .../src/composables/usePushConnection.ts | 46 ++----------------- 2 files changed, 7 insertions(+), 43 deletions(-) diff --git a/packages/editor-ui/src/components/ResourceMapper/ResourceMapper.vue b/packages/editor-ui/src/components/ResourceMapper/ResourceMapper.vue index f8103cc624ef2..2bc34a447582d 100644 --- a/packages/editor-ui/src/components/ResourceMapper/ResourceMapper.vue +++ b/packages/editor-ui/src/components/ResourceMapper/ResourceMapper.vue @@ -190,6 +190,10 @@ const matchingColumns = computed(() => { }); const hasAvailableMatchingColumns = computed(() => { + // 'map' mode doesn't require matching columns + if (resourceMapperMode.value === 'map') { + return true; + } if (resourceMapperMode.value !== 'add' && resourceMapperMode.value !== 'upsert') { return ( state.paramValue.schema.filter( diff --git a/packages/editor-ui/src/composables/usePushConnection.ts b/packages/editor-ui/src/composables/usePushConnection.ts index 7a24f95d1b7b3..e3b976a8acbec 100644 --- a/packages/editor-ui/src/composables/usePushConnection.ts +++ b/packages/editor-ui/src/composables/usePushConnection.ts @@ -14,11 +14,11 @@ import type { INodeTypeDescription, NodeError, } from 'n8n-workflow'; -import type { PushMessage, PushPayload } from '@n8n/api-types'; +import type { PushMessage } from '@n8n/api-types'; import { useNodeHelpers } from '@/composables/useNodeHelpers'; import { useToast } from '@/composables/useToast'; -import { EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE, WORKFLOW_SETTINGS_MODAL_KEY } from '@/constants'; +import { WORKFLOW_SETTINGS_MODAL_KEY } from '@/constants'; import { getTriggerNodeServiceName } from '@/utils/nodeTypesUtils'; import { codeNodeEditorEventBus, globalLinkActionsEventBus } from '@/event-bus'; import { useUIStore } from '@/stores/ui.store'; @@ -35,9 +35,8 @@ import { useTelemetry } from '@/composables/useTelemetry'; import type { PushMessageQueueItem } from '@/types'; import { useAssistantStore } from '@/stores/assistant.store'; import NodeExecutionErrorMessage from '@/components/NodeExecutionErrorMessage.vue'; -import type { IExecutionResponse, IWorkflowDb } from '@/Interface'; +import type { IExecutionResponse } from '@/Interface'; import { EASY_AI_WORKFLOW_JSON } from '@/constants.workflows'; -import { isResourceLocatorValue } from '@/utils/typeGuards'; import { clearPopupWindowState } from '../utils/executionUtils'; export function usePushConnection({ router }: { router: ReturnType }) { @@ -489,7 +488,6 @@ export function usePushConnection({ router }: { router: ReturnType) { - const itemCount = - pushData.data?.data?.main.reduce((acc, item) => acc + (item?.length ?? 0), 0) ?? 0; - const node = workflowsStore.getNodeByName(pushData.nodeName); - const status = pushData.data?.executionStatus; - const error = pushData.data?.error; - let subWorkflowId = node?.parameters.workflowId; - let subWorkflowVersion: 'new' | 'legacy' | undefined; - if (subWorkflowId) { - if (isResourceLocatorValue(subWorkflowId)) { - subWorkflowId = subWorkflowId.value; - } - let subWorkflow: IWorkflowDb | undefined; - try { - subWorkflow = subWorkflowId - ? await workflowsStore.getOrFetchWorkflow(String(subWorkflowId)) - : undefined; - } catch { - subWorkflow = undefined; - } - const triggerNode = subWorkflow?.nodes.find( - (n) => n.type === EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE, - ); - subWorkflowVersion = triggerNode?.typeVersion === 1.0 ? 'legacy' : 'new'; - } - telemetry.track('Manual node execution finished', { - items_count: itemCount, - node_type: node?.type, - node_name: pushData.nodeName, - status, - error_message: error?.message, - error_stack: error?.stack, - error_timestamp: error?.timestamp, - error_node: error?.errorResponse?.node, - subworkflow_type: subWorkflowVersion, - }); - } - function getExecutionError(data: IRunExecutionData | IExecuteContextData) { const error = data.resultData.error; From 5c445314f88ee4c9d93c47b9c08dc81a8cc0a650 Mon Sep 17 00:00:00 2001 From: Elias Meire Date: Wed, 18 Dec 2024 16:09:23 +0100 Subject: [PATCH 04/14] test(Gmail Node): Add test coverage (no-changelog) (#12278) --- packages/nodes-base/jest.config.js | 3 + .../Google/Gmail/test/v2/GmailV2.node.test.ts | 396 ++++++ .../Google/Gmail/test/v2/drafts.workflow.json | 386 ++++++ .../Google/Gmail/test/v2/fixtures/labels.json | 14 + .../Gmail/test/v2/fixtures/messages.json | 240 ++++ .../Google/Gmail/test/v2/labels.workflow.json | 163 +++ .../Gmail/test/v2/messages.workflow.json | 1151 ++++++++++++++++ .../Gmail/test/v2/threads.workflow.json | 1174 +++++++++++++++++ .../nodes/Google/Gmail/v2/GmailV2.node.ts | 103 +- .../nodes/Google/Gmail/v2/loadOptions.ts | 75 ++ .../test/nodes/FakeCredentialsMap.ts | 19 + packages/nodes-base/test/nodes/Helpers.ts | 2 +- 12 files changed, 3636 insertions(+), 90 deletions(-) create mode 100644 packages/nodes-base/nodes/Google/Gmail/test/v2/GmailV2.node.test.ts create mode 100644 packages/nodes-base/nodes/Google/Gmail/test/v2/drafts.workflow.json create mode 100644 packages/nodes-base/nodes/Google/Gmail/test/v2/fixtures/labels.json create mode 100644 packages/nodes-base/nodes/Google/Gmail/test/v2/fixtures/messages.json create mode 100644 packages/nodes-base/nodes/Google/Gmail/test/v2/labels.workflow.json create mode 100644 packages/nodes-base/nodes/Google/Gmail/test/v2/messages.workflow.json create mode 100644 packages/nodes-base/nodes/Google/Gmail/test/v2/threads.workflow.json create mode 100644 packages/nodes-base/nodes/Google/Gmail/v2/loadOptions.ts diff --git a/packages/nodes-base/jest.config.js b/packages/nodes-base/jest.config.js index 860b43c6304c4..721030d49c85b 100644 --- a/packages/nodes-base/jest.config.js +++ b/packages/nodes-base/jest.config.js @@ -1,3 +1,6 @@ +// Avoid tests failing because of difference between local and GitHub actions timezone +process.env.TZ = 'UTC'; + /** @type {import('jest').Config} */ module.exports = { ...require('../../jest.config'), diff --git a/packages/nodes-base/nodes/Google/Gmail/test/v2/GmailV2.node.test.ts b/packages/nodes-base/nodes/Google/Gmail/test/v2/GmailV2.node.test.ts new file mode 100644 index 0000000000000..b14fe8132c3b8 --- /dev/null +++ b/packages/nodes-base/nodes/Google/Gmail/test/v2/GmailV2.node.test.ts @@ -0,0 +1,396 @@ +/* eslint-disable n8n-nodes-base/node-param-display-name-miscased */ +import { mock, mockDeep } from 'jest-mock-extended'; +import { jsonParse, type ILoadOptionsFunctions, type INode } from 'n8n-workflow'; +import nock from 'nock'; + +import { testWorkflows } from '@test/nodes/Helpers'; + +import labels from './fixtures/labels.json'; +import messages from './fixtures/messages.json'; +import { getGmailAliases, getLabels, getThreadMessages } from '../../v2/loadOptions'; + +describe('Test Gmail Node v2', () => { + beforeAll(() => { + jest + .useFakeTimers({ doNotFake: ['setImmediate', 'nextTick'] }) + .setSystemTime(new Date('2024-12-16 12:34:56.789Z')); + + nock.disableNetConnect(); + }); + + afterAll(() => { + nock.restore(); + jest.resetAllMocks(); + }); + + describe('Messages', () => { + const gmailNock = nock('https://www.googleapis.com/gmail'); + + beforeAll(() => { + gmailNock.get('/v1/users/me/messages').query({ maxResults: 2 }).reply(200, { + messages, + }); + gmailNock + .get('/v1/users/me/messages') + .query({ + includeSpamTrash: 'true', + labelIds: 'CHAT', + q: 'test from:Test Sender after:1734393600 before:1735171200', + readStatus: 'both', + dataPropertyAttachmentsPrefixName: 'attachment_', + downloadAttachments: 'true', + maxResults: '2', + }) + .reply(200, { messages }); + gmailNock + .get('/v1/users/me/messages/a1b2c3d4e5f6g7h8') + .query({ + maxResults: '2', + format: 'metadata', + metadataHeaders: ['From', 'To', 'Cc', 'Bcc', 'Subject'], + }) + .reply(200, messages[0]); + gmailNock + .get('/v1/users/me/messages/a1b2c3d4e5f6g7h8') + .query({ + includeSpamTrash: 'true', + labelIds: 'CHAT', + q: 'test from:Test Sender after:1734393600 before:1735171200', + readStatus: 'both', + dataPropertyAttachmentsPrefixName: 'attachment_', + downloadAttachments: 'true', + maxResults: '2', + format: 'raw', + }) + .reply(200, { + ...messages[0], + raw: 'TUlNRS1WZXJzaW9uOiAxLjANCkRhdGU6IEZyaSwgMTMgRGVjIDIwMjQgMTE6MTU6MDEgKzAxMDANCk1lc3NhZ2UtSUQ6IDxDQUVHQVByb3d1ZEduS1h4cXJoTWpPdXhhbVRoN3lBcmp3UDdPRDlVQnEtSnBrYjBYOXdAbWFpbC5nbWFpbC5jb20-DQpTdWJqZWN0OiBUZXN0IGRyYWZ0DQpGcm9tOiBub2RlIHFhIDxub2RlOHFhQGdtYWlsLmNvbT4NClRvOiB0ZXN0QGdtYWlsLmNvbQ0KQ29udGVudC1UeXBlOiBtdWx0aXBhcnQvYWx0ZXJuYXRpdmU7IGJvdW5kYXJ5PSIwMDAwMDAwMDAwMDA5ZDU4YjYwNjI5MjQxYTIyIg0KDQotLTAwMDAwMDAwMDAwMDlkNThiNjA2MjkyNDFhMjINCkNvbnRlbnQtVHlwZTogdGV4dC9wbGFpbjsgY2hhcnNldD0iVVRGLTgiDQoNCmRyYWZ0IGJvZHkNCg0KLS0wMDAwMDAwMDAwMDA5ZDU4YjYwNjI5MjQxYTIyDQpDb250ZW50LVR5cGU6IHRleHQvaHRtbDsgY2hhcnNldD0iVVRGLTgiDQoNCjxkaXYgZGlyPSJsdHIiPmRyYWZ0IGJvZHk8YnI-PC9kaXY-DQoNCi0tMDAwMDAwMDAwMDAwOWQ1OGI2MDYyOTI0MWEyMi0t', + }); + gmailNock + .get('/v1/users/me/messages/z9y8x7w6v5u4t3s2') + .query({ + includeSpamTrash: 'true', + labelIds: 'CHAT', + q: 'test from:Test Sender after:1734393600 before:1735171200', + readStatus: 'both', + dataPropertyAttachmentsPrefixName: 'attachment_', + downloadAttachments: 'true', + maxResults: '2', + format: 'raw', + }) + .reply(200, { + ...messages[1], + raw: 'TUlNRS1WZXJzaW9uOiAxLjANCkRhdGU6IEZyaSwgMTMgRGVjIDIwMjQgMTE6MTU6MDEgKzAxMDANCk1lc3NhZ2UtSUQ6IDxDQUVHQVByb3d1ZEduS1h4cXJoTWpPdXhhbVRoN3lBcmp3UDdPRDlVQnEtSnBrYjBYOXdAbWFpbC5nbWFpbC5jb20-DQpTdWJqZWN0OiBUZXN0IGRyYWZ0DQpGcm9tOiBub2RlIHFhIDxub2RlOHFhQGdtYWlsLmNvbT4NClRvOiB0ZXN0QGdtYWlsLmNvbQ0KQ29udGVudC1UeXBlOiBtdWx0aXBhcnQvYWx0ZXJuYXRpdmU7IGJvdW5kYXJ5PSIwMDAwMDAwMDAwMDA5ZDU4YjYwNjI5MjQxYTIyIg0KDQotLTAwMDAwMDAwMDAwMDlkNThiNjA2MjkyNDFhMjINCkNvbnRlbnQtVHlwZTogdGV4dC9wbGFpbjsgY2hhcnNldD0iVVRGLTgiDQoNCmRyYWZ0IGJvZHkNCg0KLS0wMDAwMDAwMDAwMDA5ZDU4YjYwNjI5MjQxYTIyDQpDb250ZW50LVR5cGU6IHRleHQvaHRtbDsgY2hhcnNldD0iVVRGLTgiDQoNCjxkaXYgZGlyPSJsdHIiPmRyYWZ0IGJvZHk8YnI-PC9kaXY-DQoNCi0tMDAwMDAwMDAwMDAwOWQ1OGI2MDYyOTI0MWEyMi0t', + }); + gmailNock + .get('/v1/users/me/messages/z9y8x7w6v5u4t3s2') + .query({ + maxResults: '2', + format: 'metadata', + metadataHeaders: ['From', 'To', 'Cc', 'Bcc', 'Subject'], + }) + .reply(200, messages[0]); + gmailNock.get('/v1/users/me/labels').reply(200, { + labels, + }); + gmailNock.get('/v1/users/me/profile').times(2).reply(200, { emailAddress: 'test@n8n.io' }); + gmailNock + .post('/v1/users/me/messages/send') + .query({ format: 'metadata' }) + .reply(200, messages[0]); + gmailNock.post('/v1/users/me/messages/send').reply(200, messages[0]); + gmailNock + .post('/v1/users/me/messages/send') + .query({ userId: 'me', uploadType: 'media' }) + .reply(200, messages[0]); + gmailNock + .post('/v1/users/me/messages/test/modify', (body) => 'addLabelIds' in body) + .reply(200, messages[0]); + gmailNock + .post('/v1/users/me/messages/test/modify', (body) => 'removeLabelIds' in body) + .reply(200, messages[0]); + gmailNock.delete('/v1/users/me/messages/test').reply(200, messages[0]); + gmailNock + .get('/v1/users/me/messages/test') + .query({ + format: 'metadata', + metadataHeaders: ['From', 'To', 'Cc', 'Bcc', 'Subject'], + }) + .reply(200, messages[0]); + gmailNock.get('/v1/users/me/labels').reply(200, { labels }); + gmailNock + .get('/v1/users/me/messages/test') + .query({ format: 'raw' }) + .reply(200, { raw: 'test email content' }); + gmailNock + .post('/v1/users/me/messages/test/modify', { removeLabelIds: ['UNREAD'] }) + .reply(200, messages[0]); + gmailNock + .post('/v1/users/me/messages/test/modify', { addLabelIds: ['UNREAD'] }) + .reply(200, messages[0]); + gmailNock + .get('/v1/users/me/messages/test') + .query({ + format: 'metadata', + }) + .reply(200, messages[0]); + }); + + testWorkflows(['nodes/Google/Gmail/test/v2/messages.workflow.json']); + + it('should make the correct network calls', () => { + gmailNock.done(); + }); + }); + + describe('Labels', () => { + const gmailNock = nock('https://www.googleapis.com/gmail'); + + beforeAll(() => { + gmailNock + .post('/v1/users/me/labels', { + labelListVisibility: 'labelShow', + messageListVisibility: 'show', + name: 'Test Label Name', + }) + .reply(200, labels[0]); + gmailNock.delete('/v1/users/me/labels/test-label-id').reply(200, labels[0]); + gmailNock.get('/v1/users/me/labels/test-label-id').reply(200, labels[0]); + gmailNock.get('/v1/users/me/labels').reply(200, { + labels, + }); + }); + + testWorkflows(['nodes/Google/Gmail/test/v2/labels.workflow.json']); + + it('should make the correct network calls', () => { + gmailNock.done(); + }); + }); + + describe('Drafts', () => { + const gmailNock = nock('https://www.googleapis.com/gmail'); + + beforeAll(() => { + gmailNock + .filteringRequestBody((body) => { + try { + const parsedBody = jsonParse<{ message: { raw: string; threadId: string } }>(body); + const mail = Buffer.from(parsedBody.message.raw, 'base64').toString('utf-8'); + + // Remove dynamic fields from mail + parsedBody.message.raw = Buffer.from( + mail + .replace(/boundary=".*"/g, 'boundary="--test-boundary"') + .replace(/----.*/g, '----test-boundary') + .replace(/Message-ID:.*/g, 'Message-ID: test-message-id'), + 'utf-8', + ).toString('base64'); + + return JSON.stringify(parsedBody); + } catch (error) { + return body; + } + }) + .post('/v1/users/me/drafts', { + message: { + raw: 'Q29udGVudC1UeXBlOiBtdWx0aXBhcnQvbWl4ZWQ7IGJvdW5kYXJ5PSItLXRlc3QtYm91bmRhcnkiDQpGcm9tOiB0ZXN0LWFsaWFzQG44bi5pbw0KVG86IHRlc3QtdG9AbjhuLmlvDQpDYzogdGVzdC1jY0BuOG4uaW8NCkJjYzogdGVzdC1iY2NAbjhuLmlvDQpSZXBseS1UbzogdGVzdC1yZXBseUBuOG4uaW8NClN1YmplY3Q6IFRlc3QgRHJhZnQgU3ViamVjdA0KTWVzc2FnZS1JRDogdGVzdC1tZXNzYWdlLWlkDQpEYXRlOiBNb24sIDE2IERlYyAyMDI0IDEyOjM0OjU2ICswMDAwDQpNSU1FLVZlcnNpb246IDEuMA0KDQotLS0tdGVzdC1ib3VuZGFyeQ0KQ29udGVudC1UeXBlOiB0ZXh0L3BsYWluOyBjaGFyc2V0PXV0Zi04DQpDb250ZW50LVRyYW5zZmVyLUVuY29kaW5nOiA3Yml0DQoNClRlc3QgRHJhZnQgTWVzc2FnZQ0KLS0tLXRlc3QtYm91bmRhcnkNCkNvbnRlbnQtVHlwZTogYXBwbGljYXRpb24vanNvbjsgbmFtZT1maWxlLmpzb24NCkNvbnRlbnQtVHJhbnNmZXItRW5jb2Rpbmc6IGJhc2U2NA0KQ29udGVudC1EaXNwb3NpdGlvbjogYXR0YWNobWVudDsgZmlsZW5hbWU9ZmlsZS5qc29uDQoNClczc2lZbWx1WVhKNUlqcDBjblZsZlYwPQ0KLS0tLXRlc3QtYm91bmRhcnkNCg==', + threadId: 'test-thread-id', + }, + }) + .query({ userId: 'me', uploadType: 'media' }) + .reply(200, messages[0]); + gmailNock.delete('/v1/users/me/drafts/test-draft-id').reply(200, messages[0]); + gmailNock + .get('/v1/users/me/drafts/test-draft-id') + .query({ format: 'raw' }) + .reply(200, { + message: { + ...messages[0], + raw: 'TUlNRS1WZXJzaW9uOiAxLjANCkRhdGU6IEZyaSwgMTMgRGVjIDIwMjQgMTE6MTU6MDEgKzAxMDANCk1lc3NhZ2UtSUQ6IDxDQUVHQVByb3d1ZEduS1h4cXJoTWpPdXhhbVRoN3lBcmp3UDdPRDlVQnEtSnBrYjBYOXdAbWFpbC5nbWFpbC5jb20-DQpTdWJqZWN0OiBUZXN0IGRyYWZ0DQpGcm9tOiBub2RlIHFhIDxub2RlOHFhQGdtYWlsLmNvbT4NClRvOiB0ZXN0QGdtYWlsLmNvbQ0KQ29udGVudC1UeXBlOiBtdWx0aXBhcnQvYWx0ZXJuYXRpdmU7IGJvdW5kYXJ5PSIwMDAwMDAwMDAwMDA5ZDU4YjYwNjI5MjQxYTIyIg0KDQotLTAwMDAwMDAwMDAwMDlkNThiNjA2MjkyNDFhMjINCkNvbnRlbnQtVHlwZTogdGV4dC9wbGFpbjsgY2hhcnNldD0iVVRGLTgiDQoNCmRyYWZ0IGJvZHkNCg0KLS0wMDAwMDAwMDAwMDA5ZDU4YjYwNjI5MjQxYTIyDQpDb250ZW50LVR5cGU6IHRleHQvaHRtbDsgY2hhcnNldD0iVVRGLTgiDQoNCjxkaXYgZGlyPSJsdHIiPmRyYWZ0IGJvZHk8YnI-PC9kaXY-DQoNCi0tMDAwMDAwMDAwMDAwOWQ1OGI2MDYyOTI0MWEyMi0t', + }, + }); + gmailNock + .get('/v1/users/me/drafts') + .query({ + dataPropertyAttachmentsPrefixName: 'attachment_', + downloadAttachments: true, + includeSpamTrash: true, + maxResults: 100, + }) + .reply(200, { drafts: messages }); + gmailNock + .get('/v1/users/me/drafts/a1b2c3d4e5f6g7h8') + .query({ + dataPropertyAttachmentsPrefixName: 'attachment_', + downloadAttachments: true, + includeSpamTrash: true, + maxResults: 100, + format: 'raw', + }) + .reply(200, { + message: { + ...messages[0], + raw: 'TUlNRS1WZXJzaW9uOiAxLjANCkRhdGU6IEZyaSwgMTMgRGVjIDIwMjQgMTE6MTU6MDEgKzAxMDANCk1lc3NhZ2UtSUQ6IDxDQUVHQVByb3d1ZEduS1h4cXJoTWpPdXhhbVRoN3lBcmp3UDdPRDlVQnEtSnBrYjBYOXdAbWFpbC5nbWFpbC5jb20-DQpTdWJqZWN0OiBUZXN0IGRyYWZ0DQpGcm9tOiBub2RlIHFhIDxub2RlOHFhQGdtYWlsLmNvbT4NClRvOiB0ZXN0QGdtYWlsLmNvbQ0KQ29udGVudC1UeXBlOiBtdWx0aXBhcnQvYWx0ZXJuYXRpdmU7IGJvdW5kYXJ5PSIwMDAwMDAwMDAwMDA5ZDU4YjYwNjI5MjQxYTIyIg0KDQotLTAwMDAwMDAwMDAwMDlkNThiNjA2MjkyNDFhMjINCkNvbnRlbnQtVHlwZTogdGV4dC9wbGFpbjsgY2hhcnNldD0iVVRGLTgiDQoNCmRyYWZ0IGJvZHkNCg0KLS0wMDAwMDAwMDAwMDA5ZDU4YjYwNjI5MjQxYTIyDQpDb250ZW50LVR5cGU6IHRleHQvaHRtbDsgY2hhcnNldD0iVVRGLTgiDQoNCjxkaXYgZGlyPSJsdHIiPmRyYWZ0IGJvZHk8YnI-PC9kaXY-DQoNCi0tMDAwMDAwMDAwMDAwOWQ1OGI2MDYyOTI0MWEyMi0t', + }, + }); + gmailNock + .get('/v1/users/me/drafts/z9y8x7w6v5u4t3s2') + .query({ + dataPropertyAttachmentsPrefixName: 'attachment_', + downloadAttachments: true, + includeSpamTrash: true, + maxResults: 100, + format: 'raw', + }) + .reply(200, { + message: { + ...messages[1], + raw: 'TUlNRS1WZXJzaW9uOiAxLjANCkRhdGU6IEZyaSwgMTMgRGVjIDIwMjQgMTE6MTU6MDEgKzAxMDANCk1lc3NhZ2UtSUQ6IDxDQUVHQVByb3d1ZEduS1h4cXJoTWpPdXhhbVRoN3lBcmp3UDdPRDlVQnEtSnBrYjBYOXdAbWFpbC5nbWFpbC5jb20-DQpTdWJqZWN0OiBUZXN0IGRyYWZ0DQpGcm9tOiBub2RlIHFhIDxub2RlOHFhQGdtYWlsLmNvbT4NClRvOiB0ZXN0QGdtYWlsLmNvbQ0KQ29udGVudC1UeXBlOiBtdWx0aXBhcnQvYWx0ZXJuYXRpdmU7IGJvdW5kYXJ5PSIwMDAwMDAwMDAwMDA5ZDU4YjYwNjI5MjQxYTIyIg0KDQotLTAwMDAwMDAwMDAwMDlkNThiNjA2MjkyNDFhMjINCkNvbnRlbnQtVHlwZTogdGV4dC9wbGFpbjsgY2hhcnNldD0iVVRGLTgiDQoNCmRyYWZ0IGJvZHkNCg0KLS0wMDAwMDAwMDAwMDA5ZDU4YjYwNjI5MjQxYTIyDQpDb250ZW50LVR5cGU6IHRleHQvaHRtbDsgY2hhcnNldD0iVVRGLTgiDQoNCjxkaXYgZGlyPSJsdHIiPmRyYWZ0IGJvZHk8YnI-PC9kaXY-DQoNCi0tMDAwMDAwMDAwMDAwOWQ1OGI2MDYyOTI0MWEyMi0t', + }, + }); + }); + + testWorkflows(['nodes/Google/Gmail/test/v2/drafts.workflow.json']); + + it('should make the correct network calls', () => { + gmailNock.done(); + }); + }); + + describe('Threads', () => { + const gmailNock = nock('https://www.googleapis.com/gmail'); + + beforeAll(() => { + gmailNock.get('/v1/users/me/threads').query({ maxResults: 2 }).reply(200, { + threads: messages, + }); + gmailNock + .get('/v1/users/me/threads') + .query((query) => { + return ( + query.includeSpamTrash === 'true' && + query.labelIds === 'CHAT' && + !!query.q && + query.q.includes('has:attachment') && + query.q.includes('before:') && + query.q.includes('after:') + ); + }) + .reply(200, { threads: messages }); + gmailNock + .post('/v1/users/me/threads/test-thread-id/modify', { addLabelIds: ['CHAT'] }) + .reply(200, messages[0]); + gmailNock + .post('/v1/users/me/threads/test-thread-id/modify', { removeLabelIds: ['CHAT'] }) + .reply(200, messages[0]); + gmailNock.delete('/v1/users/me/threads/test-thread-id').reply(200, messages[0]); + gmailNock + .get('/v1/users/me/threads/test-thread-id') + .query({ + format: 'metadata', + metadataHeaders: ['From', 'To', 'Cc', 'Bcc', 'Subject'], + }) + .reply(200, messages[0]); + gmailNock.get('/v1/users/me/labels').times(2).reply(200, { labels }); + gmailNock + .get('/v1/users/me/threads/test-thread-id') + .query({ format: 'full' }) + .reply(200, messages[0]); + gmailNock.post('/v1/users/me/threads/test-thread-id/trash').reply(200, messages[0]); + gmailNock.post('/v1/users/me/threads/test-thread-id/untrash').reply(200, messages[0]); + gmailNock + .get('/v1/users/me/messages/test%20snippet') + .query({ + userId: 'me', + uploadType: 'media', + format: 'metadata', + }) + .reply(200, messages[0]); + gmailNock.get('/v1/users/me/profile').reply(200, { emailAddress: 'test@n8n.io' }); + gmailNock + .post('/v1/users/me/messages/send') + .query({ userId: 'me', uploadType: 'media', format: 'metadata' }) + .reply(200, messages[0]); + }); + + testWorkflows(['nodes/Google/Gmail/test/v2/threads.workflow.json']); + + it('should make the correct network calls', () => { + gmailNock.done(); + }); + }); + + describe('loadOptions', () => { + describe('getLabels', () => { + it('should return a list of Gmail labels', async () => { + const loadOptionsFunctions = mockDeep({ + getNode: jest.fn(() => mock()), + helpers: mock({ + requestWithAuthentication: jest + .fn() + // 2 pages of labels + .mockImplementationOnce(async () => ({ labels, nextPageToken: 'nextPageToken' })) + .mockImplementationOnce(async () => ({ labels })), + }), + }); + + expect(await getLabels.call(loadOptionsFunctions)).toEqual([ + { name: 'CHAT', value: 'CHAT' }, + { name: 'CHAT', value: 'CHAT' }, + { name: 'SENT', value: 'SENT' }, + { name: 'SENT', value: 'SENT' }, + ]); + }); + }); + + describe('getThreadMessages', () => { + it('should return a list of Gmail thread messages', async () => { + const loadOptionsFunctions = mockDeep({ + getNode: jest.fn(() => mock()), + helpers: mock({ + requestWithAuthentication: jest.fn(async () => ({ messages })), + }), + }); + + expect(await getThreadMessages.call(loadOptionsFunctions)).toEqual([ + { + name: "Don't miss our exclusive holiday discounts on all items! Act now before the sale ends.", + value: 'a1b2c3d4e5f6g7h8', + }, + { + name: 'Your friend John just shared a new photo with you! Check it out now.', + value: 'z9y8x7w6v5u4t3s2', + }, + ]); + }); + }); + + describe('getGmailAliases', () => { + it('should return a list of Gmail aliases', async () => { + const loadOptionsFunctions = mockDeep({ + getNode: jest.fn(() => mock()), + helpers: mock({ + requestWithAuthentication: jest.fn(async () => ({ + sendAs: [ + { isDefault: false, sendAsEmail: 'alias1@n8n.io' }, + { isDefault: true, sendAsEmail: 'alias2@n8n.io' }, + ], + })), + }), + }); + + expect(await getGmailAliases.call(loadOptionsFunctions)).toEqual([ + { + name: 'alias1@n8n.io', + value: 'alias1@n8n.io', + }, + { + name: 'alias2@n8n.io (Default)', + value: 'alias2@n8n.io', + }, + ]); + }); + }); + }); +}); diff --git a/packages/nodes-base/nodes/Google/Gmail/test/v2/drafts.workflow.json b/packages/nodes-base/nodes/Google/Gmail/test/v2/drafts.workflow.json new file mode 100644 index 0000000000000..8000286e88ee7 --- /dev/null +++ b/packages/nodes-base/nodes/Google/Gmail/test/v2/drafts.workflow.json @@ -0,0 +1,386 @@ +{ + "name": "My workflow 130", + "nodes": [ + { + "parameters": {}, + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [-60, 100], + "id": "636b40bc-2c98-4b9a-8ce2-9d1322294518", + "name": "When clicking ‘Test workflow’" + }, + { + "parameters": { + "resource": "draft", + "operation": "get", + "messageId": "test-draft-id", + "options": {} + }, + "type": "n8n-nodes-base.gmail", + "typeVersion": 2.1, + "position": [440, 200], + "id": "8802fdb5-2741-407b-82a4-ccedc4055076", + "name": "Gmail - Drafts - Get", + "webhookId": "3b8b38e0-2f4b-40bc-8b67-37e7ea95cb60", + "credentials": { + "gmailOAuth2": { + "id": "22", + "name": "Gmail 0auth" + } + } + }, + { + "parameters": { + "resource": "draft", + "operation": "delete", + "messageId": "test-draft-id" + }, + "type": "n8n-nodes-base.gmail", + "typeVersion": 2.1, + "position": [440, 20], + "id": "ed979c3a-b2ea-413e-be63-0392cc1714a5", + "name": "Gmail - Drafts - Delete", + "webhookId": "3b8b38e0-2f4b-40bc-8b67-37e7ea95cb60", + "credentials": { + "gmailOAuth2": { + "id": "22", + "name": "Gmail 0auth" + } + } + }, + { + "parameters": { + "resource": "draft", + "subject": "Test Draft Subject", + "message": "Test Draft Message", + "options": { + "attachmentsUi": { + "attachmentsBinary": [ + { + "property": "data" + } + ] + }, + "bccList": "test-bcc@n8n.io", + "ccList": "test-cc@n8n.io", + "fromAlias": "=test-alias@n8n.io", + "replyTo": "test-reply@n8n.io", + "threadId": "test-thread-id", + "sendTo": "test-to@n8n.io" + } + }, + "type": "n8n-nodes-base.gmail", + "typeVersion": 2.1, + "position": [840, -180], + "id": "45758452-3b5b-478d-aece-001e117ce69d", + "name": "Gmail - Drafts - Create", + "webhookId": "3b8b38e0-2f4b-40bc-8b67-37e7ea95cb60", + "credentials": { + "gmailOAuth2": { + "id": "22", + "name": "Gmail 0auth" + } + } + }, + { + "parameters": { + "resource": "draft", + "operation": "getAll", + "returnAll": true, + "options": { + "dataPropertyAttachmentsPrefixName": "attachment_", + "downloadAttachments": true, + "includeSpamTrash": true + } + }, + "type": "n8n-nodes-base.gmail", + "typeVersion": 2.1, + "position": [440, 400], + "id": "bae81586-7641-4fdc-81a4-0006b289bf9d", + "name": "Gmail - Drafts - Get Many", + "webhookId": "3b8b38e0-2f4b-40bc-8b67-37e7ea95cb60", + "credentials": { + "gmailOAuth2": { + "id": "22", + "name": "Gmail 0auth" + } + } + }, + { + "parameters": { + "assignments": { + "assignments": [ + { + "id": "491590a8-27a6-4d14-b342-493947775d16", + "name": "binary", + "value": true, + "type": "boolean" + } + ] + }, + "options": {} + }, + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [420, -180], + "id": "d630c018-1d7b-4779-a280-9f4a21c6a764", + "name": "Edit Fields" + }, + { + "parameters": { + "operation": "toJson", + "options": {} + }, + "type": "n8n-nodes-base.convertToFile", + "typeVersion": 1.1, + "position": [640, -180], + "id": "fc3bdb76-c278-44f6-9aac-153b79c8177b", + "name": "Convert to File" + } + ], + "pinData": { + "Gmail - Drafts - Create": [ + { + "json": { + "id": "a1b2c3d4e5f6g7h8", + "threadId": "a1b2c3d4e5f6g7h8", + "labelIds": ["UNREAD", "CATEGORY_PROMOTIONS", "INBOX"], + "snippet": "Don't miss our exclusive holiday discounts on all items! Act now before the sale ends.", + "payload": { + "partId": "", + "mimeType": "multipart/alternative", + "filename": "", + "headers": [ + { "name": "Delivered-To", "value": "exampleuser@gmail.com" }, + { + "name": "Received", + "value": "by 2001:db8::abcd with SMTP id xyz123abc456; Thu, 5 Dec 2024 08:30:00 -0800 (PST)" + }, + { "name": "X-Google-Smtp-Source", "value": "ABC12345+EXAMPLE123456789" }, + { + "name": "X-Received", + "value": "by 192.0.2.1 with SMTP id 12345abc67890; Thu, 5 Dec 2024 08:30:00 -0800 (PST)" + }, + { + "name": "ARC-Seal", + "value": "i=1; a=rsa-sha256; t=1733405400; cv=none; d=example.com; s=arc-20241205; b=ABCDEFG123456=" + }, + { + "name": "ARC-Message-Signature", + "value": "i=1; a=rsa-sha256; c=relaxed/relaxed; d=example.com; s=arc-20241205; bh=EXAMPLEHASH12345=" + }, + { + "name": "ARC-Authentication-Results", + "value": "i=1; mx.example.com; dkim=pass header.i=@promotion.example.com; spf=pass smtp.mailfrom=promo@promotion.example.com; dmarc=pass header.from=example.com" + }, + { "name": "Return-Path", "value": "" }, + { "name": "Date", "value": "Thu, 5 Dec 2024 08:30:00 -0800" }, + { "name": "From", "value": "Holiday Deals " }, + { "name": "To", "value": "exampleuser@gmail.com" }, + { "name": "Message-ID", "value": "<12345abc67890@promotion.example.com>" }, + { "name": "Subject", "value": "Exclusive Holiday Discounts!" }, + { "name": "MIME-Version", "value": "1.0" }, + { + "name": "Content-Type", + "value": "multipart/alternative; boundary=\"----=_Part_12345_67890.1733405400000\"" + } + ], + "body": { "size": 0 }, + "parts": [ + { + "partId": "0", + "mimeType": "text/plain", + "filename": "", + "headers": [ + { "name": "Content-Type", "value": "text/plain; charset=utf-8" }, + { "name": "Content-Transfer-Encoding", "value": "quoted-printable" } + ], + "body": { + "size": 1234, + "data": "VGhpcyBpcyBhbiBleGFtcGxlIG1lc3NhZ2UuIFRoYW5rIHlvdSBmb3Igc2hvcHBpbmcgd2l0aCB1cy4=" + } + }, + { + "partId": "1", + "mimeType": "text/html", + "filename": "", + "headers": [ + { "name": "Content-Type", "value": "text/html; charset=\"utf-8\"" }, + { "name": "Content-Transfer-Encoding", "value": "quoted-printable" } + ], + "body": { + "size": 5678, + "data": "PGRpdiBzdHlsZT0nZm9udC1mYW1pbHk6IEFyaWFsLCBzYW5zLXNlcmlmOyc+VGhpcyBpcyBhbiBleGFtcGxlIGh0bWwgbWVzc2FnZS4gPGI+VGhhbmsgeW91IGZvciBzaG9wcGluZyB3aXRoIHVzLjwvYj48L2Rpdj4=" + } + } + ] + }, + "sizeEstimate": 67890, + "historyId": "54321", + "internalDate": "1733405400000" + } + } + ], + "Gmail - Drafts - Delete": [{ "json": { "success": true } }], + "Gmail - Drafts - Get": [ + { + "json": { + "threadId": "a1b2c3d4e5f6g7h8", + "labelIds": ["UNREAD", "CATEGORY_PROMOTIONS", "INBOX"], + "sizeEstimate": 67890, + "headers": { + "mime-version": "MIME-Version: 1.0", + "date": "Date: Fri, 13 Dec 2024 11:15:01 +0100", + "message-id": "Message-ID: ", + "subject": "Subject: Test draft", + "from": "From: node qa ", + "to": "To: test@gmail.com", + "content-type": "Content-Type: multipart/alternative; boundary=\"0000000000009d58b60629241a22\"" + }, + "html": "
draft body
\n", + "text": "draft body\n", + "textAsHtml": "

draft body

", + "subject": "Test draft", + "date": "2024-12-13T10:15:01.000Z", + "to": { + "value": [{ "address": "test@gmail.com", "name": "" }], + "html": "test@gmail.com", + "text": "test@gmail.com" + }, + "from": { + "value": [{ "address": "node8qa@gmail.com", "name": "node qa" }], + "html": "node qa <node8qa@gmail.com>", + "text": "\"node qa\" " + }, + "messageId": "a1b2c3d4e5f6g7h8" + } + } + ], + "Gmail - Drafts - Get Many": [ + { + "json": { + "threadId": "a1b2c3d4e5f6g7h8", + "labelIds": ["UNREAD", "CATEGORY_PROMOTIONS", "INBOX"], + "sizeEstimate": 67890, + "headers": { + "mime-version": "MIME-Version: 1.0", + "date": "Date: Fri, 13 Dec 2024 11:15:01 +0100", + "message-id": "Message-ID: ", + "subject": "Subject: Test draft", + "from": "From: node qa ", + "to": "To: test@gmail.com", + "content-type": "Content-Type: multipart/alternative; boundary=\"0000000000009d58b60629241a22\"" + }, + "html": "
draft body
\n", + "text": "draft body\n", + "textAsHtml": "

draft body

", + "subject": "Test draft", + "date": "2024-12-13T10:15:01.000Z", + "to": { + "value": [{ "address": "test@gmail.com", "name": "" }], + "html": "test@gmail.com", + "text": "test@gmail.com" + }, + "from": { + "value": [{ "address": "node8qa@gmail.com", "name": "node qa" }], + "html": "node qa <node8qa@gmail.com>", + "text": "\"node qa\" " + }, + "messageId": "a1b2c3d4e5f6g7h8" + } + }, + { + "json": { + "threadId": "z9y8x7w6v5u4t3s2", + "labelIds": ["UNREAD", "CATEGORY_SOCIAL", "INBOX"], + "sizeEstimate": 54321, + "headers": { + "mime-version": "MIME-Version: 1.0", + "date": "Date: Fri, 13 Dec 2024 11:15:01 +0100", + "message-id": "Message-ID: ", + "subject": "Subject: Test draft", + "from": "From: node qa ", + "to": "To: test@gmail.com", + "content-type": "Content-Type: multipart/alternative; boundary=\"0000000000009d58b60629241a22\"" + }, + "html": "
draft body
\n", + "text": "draft body\n", + "textAsHtml": "

draft body

", + "subject": "Test draft", + "date": "2024-12-13T10:15:01.000Z", + "to": { + "value": [{ "address": "test@gmail.com", "name": "" }], + "html": "test@gmail.com", + "text": "test@gmail.com" + }, + "from": { + "value": [{ "address": "node8qa@gmail.com", "name": "node qa" }], + "html": "node qa <node8qa@gmail.com>", + "text": "\"node qa\" " + }, + "messageId": "z9y8x7w6v5u4t3s2" + } + } + ] + }, + "connections": { + "When clicking ‘Test workflow’": { + "main": [ + [ + { + "node": "Gmail - Drafts - Get Many", + "type": "main", + "index": 0 + }, + { + "node": "Gmail - Drafts - Get", + "type": "main", + "index": 0 + }, + { + "node": "Gmail - Drafts - Delete", + "type": "main", + "index": 0 + }, + { + "node": "Edit Fields", + "type": "main", + "index": 0 + } + ] + ] + }, + "Edit Fields": { + "main": [ + [ + { + "node": "Convert to File", + "type": "main", + "index": 0 + } + ] + ] + }, + "Convert to File": { + "main": [ + [ + { + "node": "Gmail - Drafts - Create", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": { + "executionOrder": "v1" + }, + "versionId": "84fb11a4-4166-45bd-bd9f-60fa378d9e68", + "meta": { + "instanceId": "27cc9b56542ad45b38725555722c50a1c3fee1670bbb67980558314ee08517c4" + }, + "id": "09KDcfGmfDrLInDE", + "tags": [] +} diff --git a/packages/nodes-base/nodes/Google/Gmail/test/v2/fixtures/labels.json b/packages/nodes-base/nodes/Google/Gmail/test/v2/fixtures/labels.json new file mode 100644 index 0000000000000..7d211dc09cced --- /dev/null +++ b/packages/nodes-base/nodes/Google/Gmail/test/v2/fixtures/labels.json @@ -0,0 +1,14 @@ +[ + { + "id": "CHAT", + "name": "CHAT", + "messageListVisibility": "hide", + "labelListVisibility": "labelHide", + "type": "system" + }, + { + "id": "SENT", + "name": "SENT", + "type": "system" + } +] diff --git a/packages/nodes-base/nodes/Google/Gmail/test/v2/fixtures/messages.json b/packages/nodes-base/nodes/Google/Gmail/test/v2/fixtures/messages.json new file mode 100644 index 0000000000000..cb669eb88d6df --- /dev/null +++ b/packages/nodes-base/nodes/Google/Gmail/test/v2/fixtures/messages.json @@ -0,0 +1,240 @@ +[ + { + "id": "a1b2c3d4e5f6g7h8", + "threadId": "a1b2c3d4e5f6g7h8", + "labelIds": ["UNREAD", "CATEGORY_PROMOTIONS", "INBOX"], + "snippet": "Don't miss our exclusive holiday discounts on all items! Act now before the sale ends.", + "payload": { + "partId": "", + "mimeType": "multipart/alternative", + "filename": "", + "headers": [ + { + "name": "Delivered-To", + "value": "exampleuser@gmail.com" + }, + { + "name": "Received", + "value": "by 2001:db8::abcd with SMTP id xyz123abc456; Thu, 5 Dec 2024 08:30:00 -0800 (PST)" + }, + { + "name": "X-Google-Smtp-Source", + "value": "ABC12345+EXAMPLE123456789" + }, + { + "name": "X-Received", + "value": "by 192.0.2.1 with SMTP id 12345abc67890; Thu, 5 Dec 2024 08:30:00 -0800 (PST)" + }, + { + "name": "ARC-Seal", + "value": "i=1; a=rsa-sha256; t=1733405400; cv=none; d=example.com; s=arc-20241205; b=ABCDEFG123456=" + }, + { + "name": "ARC-Message-Signature", + "value": "i=1; a=rsa-sha256; c=relaxed/relaxed; d=example.com; s=arc-20241205; bh=EXAMPLEHASH12345=" + }, + { + "name": "ARC-Authentication-Results", + "value": "i=1; mx.example.com; dkim=pass header.i=@promotion.example.com; spf=pass smtp.mailfrom=promo@promotion.example.com; dmarc=pass header.from=example.com" + }, + { + "name": "Return-Path", + "value": "" + }, + { + "name": "Date", + "value": "Thu, 5 Dec 2024 08:30:00 -0800" + }, + { + "name": "From", + "value": "Holiday Deals " + }, + { + "name": "To", + "value": "exampleuser@gmail.com" + }, + { + "name": "Message-ID", + "value": "<12345abc67890@promotion.example.com>" + }, + { + "name": "Subject", + "value": "Exclusive Holiday Discounts!" + }, + { + "name": "MIME-Version", + "value": "1.0" + }, + { + "name": "Content-Type", + "value": "multipart/alternative; boundary=\"----=_Part_12345_67890.1733405400000\"" + } + ], + "body": { + "size": 0 + }, + "parts": [ + { + "partId": "0", + "mimeType": "text/plain", + "filename": "", + "headers": [ + { + "name": "Content-Type", + "value": "text/plain; charset=utf-8" + }, + { + "name": "Content-Transfer-Encoding", + "value": "quoted-printable" + } + ], + "body": { + "size": 1234, + "data": "VGhpcyBpcyBhbiBleGFtcGxlIG1lc3NhZ2UuIFRoYW5rIHlvdSBmb3Igc2hvcHBpbmcgd2l0aCB1cy4=" + } + }, + { + "partId": "1", + "mimeType": "text/html", + "filename": "", + "headers": [ + { + "name": "Content-Type", + "value": "text/html; charset=\"utf-8\"" + }, + { + "name": "Content-Transfer-Encoding", + "value": "quoted-printable" + } + ], + "body": { + "size": 5678, + "data": "PGRpdiBzdHlsZT0nZm9udC1mYW1pbHk6IEFyaWFsLCBzYW5zLXNlcmlmOyc+VGhpcyBpcyBhbiBleGFtcGxlIGh0bWwgbWVzc2FnZS4gPGI+VGhhbmsgeW91IGZvciBzaG9wcGluZyB3aXRoIHVzLjwvYj48L2Rpdj4=" + } + } + ] + }, + "sizeEstimate": 67890, + "historyId": "54321", + "internalDate": "1733405400000" + }, + { + "id": "z9y8x7w6v5u4t3s2", + "threadId": "z9y8x7w6v5u4t3s2", + "labelIds": ["UNREAD", "CATEGORY_SOCIAL", "INBOX"], + "snippet": "Your friend John just shared a new photo with you! Check it out now.", + "payload": { + "partId": "", + "mimeType": "multipart/alternative", + "filename": "", + "headers": [ + { + "name": "Delivered-To", + "value": "exampleuser2@gmail.com" + }, + { + "name": "Received", + "value": "by 2001:db8::abcd with SMTP id def456ghi789; Fri, 6 Dec 2024 09:45:00 -0800 (PST)" + }, + { + "name": "X-Google-Smtp-Source", + "value": "XYZ67890+EXAMPLE0987654321" + }, + { + "name": "X-Received", + "value": "by 198.51.100.2 with SMTP id 67890def12345; Fri, 6 Dec 2024 09:45:00 -0800 (PST)" + }, + { + "name": "ARC-Seal", + "value": "i=1; a=rsa-sha256; t=1733490900; cv=none; d=example2.com; s=arc-20241206; b=HIJKLMN987654=" + }, + { + "name": "ARC-Message-Signature", + "value": "i=1; a=rsa-sha256; c=relaxed/relaxed; d=example2.com; s=arc-20241206; bh=EXAMPLEHASH67890=" + }, + { + "name": "ARC-Authentication-Results", + "value": "i=1; mx.example2.com; dkim=pass header.i=@social.example2.com; spf=pass smtp.mailfrom=notifications@social.example2.com; dmarc=pass header.from=example2.com" + }, + { + "name": "Return-Path", + "value": "" + }, + { + "name": "Date", + "value": "Fri, 6 Dec 2024 09:45:00 -0800" + }, + { + "name": "From", + "value": "John's Photos " + }, + { + "name": "To", + "value": "exampleuser2@gmail.com" + }, + { + "name": "Message-ID", + "value": "<67890def12345@social.example2.com>" + }, + { + "name": "Subject", + "value": "John shared a new photo with you!" + }, + { + "name": "MIME-Version", + "value": "1.0" + }, + { + "name": "Content-Type", + "value": "multipart/alternative; boundary=\"----=_Part_67890_12345.1733490900000\"" + } + ], + "body": { + "size": 0 + }, + "parts": [ + { + "partId": "0", + "mimeType": "text/plain", + "filename": "", + "headers": [ + { + "name": "Content-Type", + "value": "text/plain; charset=utf-8" + }, + { + "name": "Content-Transfer-Encoding", + "value": "quoted-printable" + } + ], + "body": { + "size": 4321, + "data": "U2VlIHRoZSBhdHRhY2hlZCBwaG90byBhbmQgcmVwbHkgdG8gSm9obi4gV2UgaG9wZSB5b3UgbGlrZSBpdCE=" + } + }, + { + "partId": "1", + "mimeType": "text/html", + "filename": "", + "headers": [ + { + "name": "Content-Type", + "value": "text/html; charset=\"utf-8\"" + }, + { + "name": "Content-Transfer-Encoding", + "value": "quoted-printable" + } + ], + "body": { + "size": 8765, + "data": "PGRpdiBzdHlsZT0nZm9udC1mYW1pbHk6IEFyaWFsLCBzYW5zLXNlcmlmOyc+U2VlIHRoZSBhdHRhY2hlZCBwaG90byBhbmQgcmVwbHkgdG8gPGI+Sm9obi48L2I+IFdlIGhvcGUgeW91IGxpa2UgaXQhPC9kaXY+" + } + } + ] + }, + "sizeEstimate": 54321, + "historyId": "98765", + "internalDate": "1733490900000" + } +] diff --git a/packages/nodes-base/nodes/Google/Gmail/test/v2/labels.workflow.json b/packages/nodes-base/nodes/Google/Gmail/test/v2/labels.workflow.json new file mode 100644 index 0000000000000..fecfce191c4c5 --- /dev/null +++ b/packages/nodes-base/nodes/Google/Gmail/test/v2/labels.workflow.json @@ -0,0 +1,163 @@ +{ + "name": "My workflow 130", + "nodes": [ + { + "parameters": {}, + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [-60, 100], + "id": "636b40bc-2c98-4b9a-8ce2-9d1322294518", + "name": "When clicking ‘Test workflow’" + }, + { + "parameters": { + "resource": "label", + "operation": "create", + "name": "Test Label Name", + "options": {} + }, + "type": "n8n-nodes-base.gmail", + "typeVersion": 2.1, + "position": [220, -180], + "id": "45758452-3b5b-478d-aece-001e117ce69d", + "name": "Gmail - Labels - Create", + "webhookId": "3b8b38e0-2f4b-40bc-8b67-37e7ea95cb60", + "credentials": { + "gmailOAuth2": { + "id": "22", + "name": "Gmail 0auth" + } + } + }, + { + "parameters": { + "resource": "label", + "operation": "delete", + "labelId": "test-label-id" + }, + "type": "n8n-nodes-base.gmail", + "typeVersion": 2.1, + "position": [220, 20], + "id": "ed979c3a-b2ea-413e-be63-0392cc1714a5", + "name": "Gmail - Labels - Delete", + "webhookId": "3b8b38e0-2f4b-40bc-8b67-37e7ea95cb60", + "credentials": { + "gmailOAuth2": { + "id": "22", + "name": "Gmail 0auth" + } + } + }, + { + "parameters": { + "resource": "label", + "operation": "get", + "labelId": "test-label-id" + }, + "type": "n8n-nodes-base.gmail", + "typeVersion": 2.1, + "position": [220, 200], + "id": "8802fdb5-2741-407b-82a4-ccedc4055076", + "name": "Gmail - Labels - Get", + "webhookId": "3b8b38e0-2f4b-40bc-8b67-37e7ea95cb60", + "credentials": { + "gmailOAuth2": { + "id": "22", + "name": "Gmail 0auth" + } + } + }, + { + "parameters": { + "resource": "label", + "limit": 2 + }, + "type": "n8n-nodes-base.gmail", + "typeVersion": 2.1, + "position": [220, 400], + "id": "bae81586-7641-4fdc-81a4-0006b289bf9d", + "name": "Gmail - Labels - Get Many", + "webhookId": "3b8b38e0-2f4b-40bc-8b67-37e7ea95cb60", + "credentials": { + "gmailOAuth2": { + "id": "22", + "name": "Gmail 0auth" + } + } + } + ], + "pinData": { + "Gmail - Labels - Create": [ + { + "json": { + "id": "CHAT", + "name": "CHAT", + "messageListVisibility": "hide", + "labelListVisibility": "labelHide", + "type": "system" + } + } + ], + "Gmail - Labels - Delete": [{ "json": { "success": true } }], + "Gmail - Labels - Get": [ + { + "json": { + "id": "CHAT", + "name": "CHAT", + "messageListVisibility": "hide", + "labelListVisibility": "labelHide", + "type": "system" + } + } + ], + "Gmail - Labels - Get Many": [ + { + "json": { + "id": "CHAT", + "name": "CHAT", + "messageListVisibility": "hide", + "labelListVisibility": "labelHide", + "type": "system" + } + }, + { "json": { "id": "SENT", "name": "SENT", "type": "system" } } + ] + }, + "connections": { + "When clicking ‘Test workflow’": { + "main": [ + [ + { + "node": "Gmail - Labels - Create", + "type": "main", + "index": 0 + }, + { + "node": "Gmail - Labels - Delete", + "type": "main", + "index": 0 + }, + { + "node": "Gmail - Labels - Get", + "type": "main", + "index": 0 + }, + { + "node": "Gmail - Labels - Get Many", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": { + "executionOrder": "v1" + }, + "versionId": "", + "meta": { + "instanceId": "27cc9b56542ad45b38725555722c50a1c3fee1670bbb67980558314ee08517c4" + }, + "tags": [] +} diff --git a/packages/nodes-base/nodes/Google/Gmail/test/v2/messages.workflow.json b/packages/nodes-base/nodes/Google/Gmail/test/v2/messages.workflow.json new file mode 100644 index 0000000000000..ba64983183c2a --- /dev/null +++ b/packages/nodes-base/nodes/Google/Gmail/test/v2/messages.workflow.json @@ -0,0 +1,1151 @@ +{ + "name": "Gmail test - messages", + "nodes": [ + { + "parameters": {}, + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [0, 0], + "id": "765640dd-529f-4319-a435-a1b5411cf761", + "name": "When clicking ‘Test workflow’" + }, + { + "parameters": { + "operation": "getAll", + "limit": 2, + "filters": {} + }, + "type": "n8n-nodes-base.gmail", + "typeVersion": 2.1, + "position": [780, -600], + "id": "51f7694c-0d4c-4f84-9e4f-8a8fe8db18c9", + "name": "Gmail - Messages - All (simplified)", + "webhookId": "f3cbddc1-3cfa-4217-aa73-f0e5a9309661", + "credentials": { + "gmailOAuth2": { + "id": "22", + "name": "Gmail 0auth" + } + } + }, + { + "parameters": { + "operation": "getAll", + "limit": 2, + "simple": false, + "filters": { + "includeSpamTrash": true, + "labelIds": ["CHAT"], + "q": "test", + "readStatus": "both", + "receivedAfter": "2024-12-17T00:00:00", + "receivedBefore": "2024-12-26T00:00:00", + "sender": "Test Sender" + }, + "options": { + "dataPropertyAttachmentsPrefixName": "attachment_", + "downloadAttachments": true + } + }, + "type": "n8n-nodes-base.gmail", + "typeVersion": 2.1, + "position": [780, -400], + "id": "2ea8db65-5e60-47dc-bdca-1981bee1b63e", + "name": "Gmail - Messages - All", + "webhookId": "f3cbddc1-3cfa-4217-aa73-f0e5a9309661", + "credentials": { + "gmailOAuth2": { + "id": "22", + "name": "Gmail 0auth" + } + } + }, + { + "parameters": { + "sendTo": "test@n8n.io", + "subject": "Test Subject", + "message": "Test Message", + "options": { + "appendAttribution": true, + "attachmentsUi": { + "attachmentsBinary": [{}, {}] + }, + "bccList": "test-bcc@n8n.io", + "ccList": "test-cc@n8n.io", + "senderName": "Test Sender", + "replyTo": "test-reply-to@n8n.io", + "replyToSenderOnly": true + } + }, + "type": "n8n-nodes-base.gmail", + "typeVersion": 2.1, + "position": [1060, 1380], + "id": "da96a339-2df5-465f-a253-5302160ef921", + "name": "Gmail - Messages - Send", + "webhookId": "f3cbddc1-3cfa-4217-aa73-f0e5a9309661", + "credentials": { + "gmailOAuth2": { + "id": "22", + "name": "Gmail 0auth" + } + } + }, + { + "parameters": { + "operation": "sendAndWait", + "sendTo": "test@n8n.io", + "subject": "Test Subject", + "message": "Test Message" + }, + "type": "n8n-nodes-base.gmail", + "typeVersion": 2.1, + "position": [780, 1580], + "id": "b12181b3-a98d-44ba-bb71-a22ce4e4c663", + "name": "Gmail - Messages - Send and Wait for Approval", + "webhookId": "f3cbddc1-3cfa-4217-aa73-f0e5a9309661", + "credentials": { + "gmailOAuth2": { + "id": "22", + "name": "Gmail 0auth" + } + } + }, + { + "parameters": { + "operation": "addLabels", + "messageId": "test", + "labelIds": ["CHAT"] + }, + "type": "n8n-nodes-base.gmail", + "typeVersion": 2.1, + "position": [780, -220], + "id": "2f83b7ac-3e7a-466d-b269-88928da60d3a", + "name": "Gmail - Messages - Add Label", + "webhookId": "f3cbddc1-3cfa-4217-aa73-f0e5a9309661", + "credentials": { + "gmailOAuth2": { + "id": "22", + "name": "Gmail 0auth" + } + } + }, + { + "parameters": { + "operation": "delete", + "messageId": "test" + }, + "type": "n8n-nodes-base.gmail", + "typeVersion": 2.1, + "position": [780, 180], + "id": "aba867fa-bf92-4ba9-b96f-c0f74d37c144", + "name": "Gmail - Messages - Delete", + "webhookId": "f3cbddc1-3cfa-4217-aa73-f0e5a9309661", + "credentials": { + "gmailOAuth2": { + "id": "22", + "name": "Gmail 0auth" + } + } + }, + { + "parameters": { + "operation": "get", + "messageId": "test" + }, + "type": "n8n-nodes-base.gmail", + "typeVersion": 2.1, + "position": [780, 380], + "id": "029494b2-caf2-4a1e-b496-1ab3ac96987a", + "name": "Gmail - Messages - Get (simplified)", + "webhookId": "f3cbddc1-3cfa-4217-aa73-f0e5a9309661", + "credentials": { + "gmailOAuth2": { + "id": "22", + "name": "Gmail 0auth" + } + } + }, + { + "parameters": { + "operation": "get", + "messageId": "test", + "simple": false, + "options": {} + }, + "type": "n8n-nodes-base.gmail", + "typeVersion": 2.1, + "position": [780, 580], + "id": "86a77799-6d7b-4dd6-9afa-be3cd64e82df", + "name": "Gmail - Messages - Get", + "webhookId": "f3cbddc1-3cfa-4217-aa73-f0e5a9309661", + "credentials": { + "gmailOAuth2": { + "id": "22", + "name": "Gmail 0auth" + } + } + }, + { + "parameters": { + "operation": "markAsRead", + "messageId": "test" + }, + "type": "n8n-nodes-base.gmail", + "typeVersion": 2.1, + "position": [780, 760], + "id": "aedc33d9-cc57-4bc1-8811-8aa5fe02a363", + "name": "Gmail - Messages - Mark as Read", + "webhookId": "f3cbddc1-3cfa-4217-aa73-f0e5a9309661", + "credentials": { + "gmailOAuth2": { + "id": "22", + "name": "Gmail 0auth" + } + } + }, + { + "parameters": { + "operation": "markAsUnread", + "messageId": "test" + }, + "type": "n8n-nodes-base.gmail", + "typeVersion": 2.1, + "position": [780, 980], + "id": "a463ad5e-3fac-40c9-b9bf-0705d28aa68d", + "name": "Gmail - Messages - Mark as Unread", + "webhookId": "f3cbddc1-3cfa-4217-aa73-f0e5a9309661", + "credentials": { + "gmailOAuth2": { + "id": "22", + "name": "Gmail 0auth" + } + } + }, + { + "parameters": { + "operation": "removeLabels", + "messageId": "test", + "labelIds": ["CHAT"] + }, + "type": "n8n-nodes-base.gmail", + "typeVersion": 2.1, + "position": [780, -20], + "id": "97996089-0116-4c86-b24c-5848661efc9c", + "name": "Gmail - Messages - Remove Label", + "webhookId": "f3cbddc1-3cfa-4217-aa73-f0e5a9309661", + "credentials": { + "gmailOAuth2": { + "id": "22", + "name": "Gmail 0auth" + } + } + }, + { + "parameters": { + "operation": "reply", + "messageId": "test", + "message": "Test reply", + "options": { + "appendAttribution": false, + "bccList": "test-bcc@n8n.io", + "senderName": "Test Sender Name", + "replyToSenderOnly": false + } + }, + "type": "n8n-nodes-base.gmail", + "typeVersion": 2.1, + "position": [780, 1200], + "id": "aa43df3d-5de3-4663-9211-401e3040a4de", + "name": "Gmail - Messages - Reply", + "webhookId": "f3cbddc1-3cfa-4217-aa73-f0e5a9309661", + "credentials": { + "gmailOAuth2": { + "id": "22", + "name": "Gmail 0auth" + } + } + }, + { + "parameters": { + "operation": "toJson", + "options": {} + }, + "type": "n8n-nodes-base.convertToFile", + "typeVersion": 1.1, + "position": [920, 1380], + "id": "6820a547-12dc-4146-8483-a43adf695b40", + "name": "Attachment" + }, + { + "parameters": { + "assignments": { + "assignments": [ + { + "id": "0a4e38fe-ed38-400e-afb1-9d430f167d54", + "name": "attachment", + "value": true, + "type": "boolean" + } + ] + }, + "options": {} + }, + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [780, 1380], + "id": "72bd5e27-dd43-48ad-be97-634caf883dd5", + "name": "Edit Fields" + } + ], + "pinData": { + "Gmail - Messages - All (simplified)": [ + { + "json": { + "id": "a1b2c3d4e5f6g7h8", + "threadId": "a1b2c3d4e5f6g7h8", + "snippet": "Don't miss our exclusive holiday discounts on all items! Act now before the sale ends.", + "payload": { + "partId": "", + "mimeType": "multipart/alternative", + "filename": "", + "body": { "size": 0 }, + "parts": [ + { + "partId": "0", + "mimeType": "text/plain", + "filename": "", + "headers": [ + { "name": "Content-Type", "value": "text/plain; charset=utf-8" }, + { "name": "Content-Transfer-Encoding", "value": "quoted-printable" } + ], + "body": { + "size": 1234, + "data": "VGhpcyBpcyBhbiBleGFtcGxlIG1lc3NhZ2UuIFRoYW5rIHlvdSBmb3Igc2hvcHBpbmcgd2l0aCB1cy4=" + } + }, + { + "partId": "1", + "mimeType": "text/html", + "filename": "", + "headers": [ + { "name": "Content-Type", "value": "text/html; charset=\"utf-8\"" }, + { "name": "Content-Transfer-Encoding", "value": "quoted-printable" } + ], + "body": { + "size": 5678, + "data": "PGRpdiBzdHlsZT0nZm9udC1mYW1pbHk6IEFyaWFsLCBzYW5zLXNlcmlmOyc+VGhpcyBpcyBhbiBleGFtcGxlIGh0bWwgbWVzc2FnZS4gPGI+VGhhbmsgeW91IGZvciBzaG9wcGluZyB3aXRoIHVzLjwvYj48L2Rpdj4=" + } + } + ] + }, + "sizeEstimate": 67890, + "historyId": "54321", + "internalDate": "1733405400000", + "labels": [], + "Delivered-To": "exampleuser@gmail.com", + "Received": "by 2001:db8::abcd with SMTP id xyz123abc456; Thu, 5 Dec 2024 08:30:00 -0800 (PST)", + "X-Google-Smtp-Source": "ABC12345+EXAMPLE123456789", + "X-Received": "by 192.0.2.1 with SMTP id 12345abc67890; Thu, 5 Dec 2024 08:30:00 -0800 (PST)", + "ARC-Seal": "i=1; a=rsa-sha256; t=1733405400; cv=none; d=example.com; s=arc-20241205; b=ABCDEFG123456=", + "ARC-Message-Signature": "i=1; a=rsa-sha256; c=relaxed/relaxed; d=example.com; s=arc-20241205; bh=EXAMPLEHASH12345=", + "ARC-Authentication-Results": "i=1; mx.example.com; dkim=pass header.i=@promotion.example.com; spf=pass smtp.mailfrom=promo@promotion.example.com; dmarc=pass header.from=example.com", + "Return-Path": "", + "Date": "Thu, 5 Dec 2024 08:30:00 -0800", + "From": "Holiday Deals ", + "To": "exampleuser@gmail.com", + "Message-ID": "<12345abc67890@promotion.example.com>", + "Subject": "Exclusive Holiday Discounts!", + "MIME-Version": "1.0", + "Content-Type": "multipart/alternative; boundary=\"----=_Part_12345_67890.1733405400000\"" + } + }, + { + "json": { + "id": "a1b2c3d4e5f6g7h8", + "threadId": "a1b2c3d4e5f6g7h8", + "snippet": "Don't miss our exclusive holiday discounts on all items! Act now before the sale ends.", + "payload": { + "partId": "", + "mimeType": "multipart/alternative", + "filename": "", + "body": { "size": 0 }, + "parts": [ + { + "partId": "0", + "mimeType": "text/plain", + "filename": "", + "headers": [ + { "name": "Content-Type", "value": "text/plain; charset=utf-8" }, + { "name": "Content-Transfer-Encoding", "value": "quoted-printable" } + ], + "body": { + "size": 1234, + "data": "VGhpcyBpcyBhbiBleGFtcGxlIG1lc3NhZ2UuIFRoYW5rIHlvdSBmb3Igc2hvcHBpbmcgd2l0aCB1cy4=" + } + }, + { + "partId": "1", + "mimeType": "text/html", + "filename": "", + "headers": [ + { "name": "Content-Type", "value": "text/html; charset=\"utf-8\"" }, + { "name": "Content-Transfer-Encoding", "value": "quoted-printable" } + ], + "body": { + "size": 5678, + "data": "PGRpdiBzdHlsZT0nZm9udC1mYW1pbHk6IEFyaWFsLCBzYW5zLXNlcmlmOyc+VGhpcyBpcyBhbiBleGFtcGxlIGh0bWwgbWVzc2FnZS4gPGI+VGhhbmsgeW91IGZvciBzaG9wcGluZyB3aXRoIHVzLjwvYj48L2Rpdj4=" + } + } + ] + }, + "sizeEstimate": 67890, + "historyId": "54321", + "internalDate": "1733405400000", + "labels": [], + "Delivered-To": "exampleuser@gmail.com", + "Received": "by 2001:db8::abcd with SMTP id xyz123abc456; Thu, 5 Dec 2024 08:30:00 -0800 (PST)", + "X-Google-Smtp-Source": "ABC12345+EXAMPLE123456789", + "X-Received": "by 192.0.2.1 with SMTP id 12345abc67890; Thu, 5 Dec 2024 08:30:00 -0800 (PST)", + "ARC-Seal": "i=1; a=rsa-sha256; t=1733405400; cv=none; d=example.com; s=arc-20241205; b=ABCDEFG123456=", + "ARC-Message-Signature": "i=1; a=rsa-sha256; c=relaxed/relaxed; d=example.com; s=arc-20241205; bh=EXAMPLEHASH12345=", + "ARC-Authentication-Results": "i=1; mx.example.com; dkim=pass header.i=@promotion.example.com; spf=pass smtp.mailfrom=promo@promotion.example.com; dmarc=pass header.from=example.com", + "Return-Path": "", + "Date": "Thu, 5 Dec 2024 08:30:00 -0800", + "From": "Holiday Deals ", + "To": "exampleuser@gmail.com", + "Message-ID": "<12345abc67890@promotion.example.com>", + "Subject": "Exclusive Holiday Discounts!", + "MIME-Version": "1.0", + "Content-Type": "multipart/alternative; boundary=\"----=_Part_12345_67890.1733405400000\"" + } + } + ], + "Gmail - Messages - All": [ + { + "json": { + "id": "a1b2c3d4e5f6g7h8", + "threadId": "a1b2c3d4e5f6g7h8", + "labelIds": ["UNREAD", "CATEGORY_PROMOTIONS", "INBOX"], + "sizeEstimate": 67890, + "headers": { + "mime-version": "MIME-Version: 1.0", + "date": "Date: Fri, 13 Dec 2024 11:15:01 +0100", + "message-id": "Message-ID: ", + "subject": "Subject: Test draft", + "from": "From: node qa ", + "to": "To: test@gmail.com", + "content-type": "Content-Type: multipart/alternative; boundary=\"0000000000009d58b60629241a22\"" + }, + "html": "
draft body
\n", + "text": "draft body\n", + "textAsHtml": "

draft body

", + "subject": "Test draft", + "date": "2024-12-13T10:15:01.000Z", + "to": { + "value": [{ "address": "test@gmail.com", "name": "" }], + "html": "test@gmail.com", + "text": "test@gmail.com" + }, + "from": { + "value": [{ "address": "node8qa@gmail.com", "name": "node qa" }], + "html": "node qa <node8qa@gmail.com>", + "text": "\"node qa\" " + }, + "messageId": "" + } + }, + { + "json": { + "id": "z9y8x7w6v5u4t3s2", + "threadId": "z9y8x7w6v5u4t3s2", + "labelIds": ["UNREAD", "CATEGORY_SOCIAL", "INBOX"], + "sizeEstimate": 54321, + "headers": { + "mime-version": "MIME-Version: 1.0", + "date": "Date: Fri, 13 Dec 2024 11:15:01 +0100", + "message-id": "Message-ID: ", + "subject": "Subject: Test draft", + "from": "From: node qa ", + "to": "To: test@gmail.com", + "content-type": "Content-Type: multipart/alternative; boundary=\"0000000000009d58b60629241a22\"" + }, + "html": "
draft body
\n", + "text": "draft body\n", + "textAsHtml": "

draft body

", + "subject": "Test draft", + "date": "2024-12-13T10:15:01.000Z", + "to": { + "value": [{ "address": "test@gmail.com", "name": "" }], + "html": "test@gmail.com", + "text": "test@gmail.com" + }, + "from": { + "value": [{ "address": "node8qa@gmail.com", "name": "node qa" }], + "html": "node qa <node8qa@gmail.com>", + "text": "\"node qa\" " + }, + "messageId": "" + } + } + ], + "Gmail - Messages - Send": [ + { + "json": { + "id": "a1b2c3d4e5f6g7h8", + "threadId": "a1b2c3d4e5f6g7h8", + "labelIds": ["UNREAD", "CATEGORY_PROMOTIONS", "INBOX"], + "snippet": "Don't miss our exclusive holiday discounts on all items! Act now before the sale ends.", + "payload": { + "partId": "", + "mimeType": "multipart/alternative", + "filename": "", + "headers": [ + { "name": "Delivered-To", "value": "exampleuser@gmail.com" }, + { + "name": "Received", + "value": "by 2001:db8::abcd with SMTP id xyz123abc456; Thu, 5 Dec 2024 08:30:00 -0800 (PST)" + }, + { "name": "X-Google-Smtp-Source", "value": "ABC12345+EXAMPLE123456789" }, + { + "name": "X-Received", + "value": "by 192.0.2.1 with SMTP id 12345abc67890; Thu, 5 Dec 2024 08:30:00 -0800 (PST)" + }, + { + "name": "ARC-Seal", + "value": "i=1; a=rsa-sha256; t=1733405400; cv=none; d=example.com; s=arc-20241205; b=ABCDEFG123456=" + }, + { + "name": "ARC-Message-Signature", + "value": "i=1; a=rsa-sha256; c=relaxed/relaxed; d=example.com; s=arc-20241205; bh=EXAMPLEHASH12345=" + }, + { + "name": "ARC-Authentication-Results", + "value": "i=1; mx.example.com; dkim=pass header.i=@promotion.example.com; spf=pass smtp.mailfrom=promo@promotion.example.com; dmarc=pass header.from=example.com" + }, + { "name": "Return-Path", "value": "" }, + { "name": "Date", "value": "Thu, 5 Dec 2024 08:30:00 -0800" }, + { "name": "From", "value": "Holiday Deals " }, + { "name": "To", "value": "exampleuser@gmail.com" }, + { "name": "Message-ID", "value": "<12345abc67890@promotion.example.com>" }, + { "name": "Subject", "value": "Exclusive Holiday Discounts!" }, + { "name": "MIME-Version", "value": "1.0" }, + { + "name": "Content-Type", + "value": "multipart/alternative; boundary=\"----=_Part_12345_67890.1733405400000\"" + } + ], + "body": { "size": 0 }, + "parts": [ + { + "partId": "0", + "mimeType": "text/plain", + "filename": "", + "headers": [ + { "name": "Content-Type", "value": "text/plain; charset=utf-8" }, + { "name": "Content-Transfer-Encoding", "value": "quoted-printable" } + ], + "body": { + "size": 1234, + "data": "VGhpcyBpcyBhbiBleGFtcGxlIG1lc3NhZ2UuIFRoYW5rIHlvdSBmb3Igc2hvcHBpbmcgd2l0aCB1cy4=" + } + }, + { + "partId": "1", + "mimeType": "text/html", + "filename": "", + "headers": [ + { "name": "Content-Type", "value": "text/html; charset=\"utf-8\"" }, + { "name": "Content-Transfer-Encoding", "value": "quoted-printable" } + ], + "body": { + "size": 5678, + "data": "PGRpdiBzdHlsZT0nZm9udC1mYW1pbHk6IEFyaWFsLCBzYW5zLXNlcmlmOyc+VGhpcyBpcyBhbiBleGFtcGxlIGh0bWwgbWVzc2FnZS4gPGI+VGhhbmsgeW91IGZvciBzaG9wcGluZyB3aXRoIHVzLjwvYj48L2Rpdj4=" + } + } + ] + }, + "sizeEstimate": 67890, + "historyId": "54321", + "internalDate": "1733405400000" + } + } + ], + "Gmail - Messages - Send and Wait for Approval": [{ "json": {} }], + "Gmail - Messages - Add Label": [ + { + "json": { + "id": "a1b2c3d4e5f6g7h8", + "threadId": "a1b2c3d4e5f6g7h8", + "labelIds": ["UNREAD", "CATEGORY_PROMOTIONS", "INBOX"], + "snippet": "Don't miss our exclusive holiday discounts on all items! Act now before the sale ends.", + "payload": { + "partId": "", + "mimeType": "multipart/alternative", + "filename": "", + "headers": [ + { "name": "Delivered-To", "value": "exampleuser@gmail.com" }, + { + "name": "Received", + "value": "by 2001:db8::abcd with SMTP id xyz123abc456; Thu, 5 Dec 2024 08:30:00 -0800 (PST)" + }, + { "name": "X-Google-Smtp-Source", "value": "ABC12345+EXAMPLE123456789" }, + { + "name": "X-Received", + "value": "by 192.0.2.1 with SMTP id 12345abc67890; Thu, 5 Dec 2024 08:30:00 -0800 (PST)" + }, + { + "name": "ARC-Seal", + "value": "i=1; a=rsa-sha256; t=1733405400; cv=none; d=example.com; s=arc-20241205; b=ABCDEFG123456=" + }, + { + "name": "ARC-Message-Signature", + "value": "i=1; a=rsa-sha256; c=relaxed/relaxed; d=example.com; s=arc-20241205; bh=EXAMPLEHASH12345=" + }, + { + "name": "ARC-Authentication-Results", + "value": "i=1; mx.example.com; dkim=pass header.i=@promotion.example.com; spf=pass smtp.mailfrom=promo@promotion.example.com; dmarc=pass header.from=example.com" + }, + { "name": "Return-Path", "value": "" }, + { "name": "Date", "value": "Thu, 5 Dec 2024 08:30:00 -0800" }, + { "name": "From", "value": "Holiday Deals " }, + { "name": "To", "value": "exampleuser@gmail.com" }, + { "name": "Message-ID", "value": "<12345abc67890@promotion.example.com>" }, + { "name": "Subject", "value": "Exclusive Holiday Discounts!" }, + { "name": "MIME-Version", "value": "1.0" }, + { + "name": "Content-Type", + "value": "multipart/alternative; boundary=\"----=_Part_12345_67890.1733405400000\"" + } + ], + "body": { "size": 0 }, + "parts": [ + { + "partId": "0", + "mimeType": "text/plain", + "filename": "", + "headers": [ + { "name": "Content-Type", "value": "text/plain; charset=utf-8" }, + { "name": "Content-Transfer-Encoding", "value": "quoted-printable" } + ], + "body": { + "size": 1234, + "data": "VGhpcyBpcyBhbiBleGFtcGxlIG1lc3NhZ2UuIFRoYW5rIHlvdSBmb3Igc2hvcHBpbmcgd2l0aCB1cy4=" + } + }, + { + "partId": "1", + "mimeType": "text/html", + "filename": "", + "headers": [ + { "name": "Content-Type", "value": "text/html; charset=\"utf-8\"" }, + { "name": "Content-Transfer-Encoding", "value": "quoted-printable" } + ], + "body": { + "size": 5678, + "data": "PGRpdiBzdHlsZT0nZm9udC1mYW1pbHk6IEFyaWFsLCBzYW5zLXNlcmlmOyc+VGhpcyBpcyBhbiBleGFtcGxlIGh0bWwgbWVzc2FnZS4gPGI+VGhhbmsgeW91IGZvciBzaG9wcGluZyB3aXRoIHVzLjwvYj48L2Rpdj4=" + } + } + ] + }, + "sizeEstimate": 67890, + "historyId": "54321", + "internalDate": "1733405400000" + } + } + ], + "Gmail - Messages - Delete": [{ "json": { "success": true } }], + "Gmail - Messages - Get (simplified)": [ + { + "json": { + "id": "a1b2c3d4e5f6g7h8", + "threadId": "a1b2c3d4e5f6g7h8", + "snippet": "Don't miss our exclusive holiday discounts on all items! Act now before the sale ends.", + "payload": { + "partId": "", + "mimeType": "multipart/alternative", + "filename": "", + "body": { "size": 0 }, + "parts": [ + { + "partId": "0", + "mimeType": "text/plain", + "filename": "", + "headers": [ + { "name": "Content-Type", "value": "text/plain; charset=utf-8" }, + { "name": "Content-Transfer-Encoding", "value": "quoted-printable" } + ], + "body": { + "size": 1234, + "data": "VGhpcyBpcyBhbiBleGFtcGxlIG1lc3NhZ2UuIFRoYW5rIHlvdSBmb3Igc2hvcHBpbmcgd2l0aCB1cy4=" + } + }, + { + "partId": "1", + "mimeType": "text/html", + "filename": "", + "headers": [ + { "name": "Content-Type", "value": "text/html; charset=\"utf-8\"" }, + { "name": "Content-Transfer-Encoding", "value": "quoted-printable" } + ], + "body": { + "size": 5678, + "data": "PGRpdiBzdHlsZT0nZm9udC1mYW1pbHk6IEFyaWFsLCBzYW5zLXNlcmlmOyc+VGhpcyBpcyBhbiBleGFtcGxlIGh0bWwgbWVzc2FnZS4gPGI+VGhhbmsgeW91IGZvciBzaG9wcGluZyB3aXRoIHVzLjwvYj48L2Rpdj4=" + } + } + ] + }, + "sizeEstimate": 67890, + "historyId": "54321", + "internalDate": "1733405400000", + "labels": [], + "Delivered-To": "exampleuser@gmail.com", + "Received": "by 2001:db8::abcd with SMTP id xyz123abc456; Thu, 5 Dec 2024 08:30:00 -0800 (PST)", + "X-Google-Smtp-Source": "ABC12345+EXAMPLE123456789", + "X-Received": "by 192.0.2.1 with SMTP id 12345abc67890; Thu, 5 Dec 2024 08:30:00 -0800 (PST)", + "ARC-Seal": "i=1; a=rsa-sha256; t=1733405400; cv=none; d=example.com; s=arc-20241205; b=ABCDEFG123456=", + "ARC-Message-Signature": "i=1; a=rsa-sha256; c=relaxed/relaxed; d=example.com; s=arc-20241205; bh=EXAMPLEHASH12345=", + "ARC-Authentication-Results": "i=1; mx.example.com; dkim=pass header.i=@promotion.example.com; spf=pass smtp.mailfrom=promo@promotion.example.com; dmarc=pass header.from=example.com", + "Return-Path": "", + "Date": "Thu, 5 Dec 2024 08:30:00 -0800", + "From": "Holiday Deals ", + "To": "exampleuser@gmail.com", + "Message-ID": "<12345abc67890@promotion.example.com>", + "Subject": "Exclusive Holiday Discounts!", + "MIME-Version": "1.0", + "Content-Type": "multipart/alternative; boundary=\"----=_Part_12345_67890.1733405400000\"" + } + } + ], + "Gmail - Messages - Get": [ + { "json": { "headers": { "": "��-zf���'���" }, "html": false } } + ], + "Gmail - Messages - Mark as Read": [ + { + "json": { + "id": "a1b2c3d4e5f6g7h8", + "threadId": "a1b2c3d4e5f6g7h8", + "labelIds": ["UNREAD", "CATEGORY_PROMOTIONS", "INBOX"], + "snippet": "Don't miss our exclusive holiday discounts on all items! Act now before the sale ends.", + "payload": { + "partId": "", + "mimeType": "multipart/alternative", + "filename": "", + "headers": [ + { "name": "Delivered-To", "value": "exampleuser@gmail.com" }, + { + "name": "Received", + "value": "by 2001:db8::abcd with SMTP id xyz123abc456; Thu, 5 Dec 2024 08:30:00 -0800 (PST)" + }, + { "name": "X-Google-Smtp-Source", "value": "ABC12345+EXAMPLE123456789" }, + { + "name": "X-Received", + "value": "by 192.0.2.1 with SMTP id 12345abc67890; Thu, 5 Dec 2024 08:30:00 -0800 (PST)" + }, + { + "name": "ARC-Seal", + "value": "i=1; a=rsa-sha256; t=1733405400; cv=none; d=example.com; s=arc-20241205; b=ABCDEFG123456=" + }, + { + "name": "ARC-Message-Signature", + "value": "i=1; a=rsa-sha256; c=relaxed/relaxed; d=example.com; s=arc-20241205; bh=EXAMPLEHASH12345=" + }, + { + "name": "ARC-Authentication-Results", + "value": "i=1; mx.example.com; dkim=pass header.i=@promotion.example.com; spf=pass smtp.mailfrom=promo@promotion.example.com; dmarc=pass header.from=example.com" + }, + { "name": "Return-Path", "value": "" }, + { "name": "Date", "value": "Thu, 5 Dec 2024 08:30:00 -0800" }, + { "name": "From", "value": "Holiday Deals " }, + { "name": "To", "value": "exampleuser@gmail.com" }, + { "name": "Message-ID", "value": "<12345abc67890@promotion.example.com>" }, + { "name": "Subject", "value": "Exclusive Holiday Discounts!" }, + { "name": "MIME-Version", "value": "1.0" }, + { + "name": "Content-Type", + "value": "multipart/alternative; boundary=\"----=_Part_12345_67890.1733405400000\"" + } + ], + "body": { "size": 0 }, + "parts": [ + { + "partId": "0", + "mimeType": "text/plain", + "filename": "", + "headers": [ + { "name": "Content-Type", "value": "text/plain; charset=utf-8" }, + { "name": "Content-Transfer-Encoding", "value": "quoted-printable" } + ], + "body": { + "size": 1234, + "data": "VGhpcyBpcyBhbiBleGFtcGxlIG1lc3NhZ2UuIFRoYW5rIHlvdSBmb3Igc2hvcHBpbmcgd2l0aCB1cy4=" + } + }, + { + "partId": "1", + "mimeType": "text/html", + "filename": "", + "headers": [ + { "name": "Content-Type", "value": "text/html; charset=\"utf-8\"" }, + { "name": "Content-Transfer-Encoding", "value": "quoted-printable" } + ], + "body": { + "size": 5678, + "data": "PGRpdiBzdHlsZT0nZm9udC1mYW1pbHk6IEFyaWFsLCBzYW5zLXNlcmlmOyc+VGhpcyBpcyBhbiBleGFtcGxlIGh0bWwgbWVzc2FnZS4gPGI+VGhhbmsgeW91IGZvciBzaG9wcGluZyB3aXRoIHVzLjwvYj48L2Rpdj4=" + } + } + ] + }, + "sizeEstimate": 67890, + "historyId": "54321", + "internalDate": "1733405400000" + } + } + ], + "Gmail - Messages - Mark as Unread": [ + { + "json": { + "id": "a1b2c3d4e5f6g7h8", + "threadId": "a1b2c3d4e5f6g7h8", + "labelIds": ["UNREAD", "CATEGORY_PROMOTIONS", "INBOX"], + "snippet": "Don't miss our exclusive holiday discounts on all items! Act now before the sale ends.", + "payload": { + "partId": "", + "mimeType": "multipart/alternative", + "filename": "", + "headers": [ + { "name": "Delivered-To", "value": "exampleuser@gmail.com" }, + { + "name": "Received", + "value": "by 2001:db8::abcd with SMTP id xyz123abc456; Thu, 5 Dec 2024 08:30:00 -0800 (PST)" + }, + { "name": "X-Google-Smtp-Source", "value": "ABC12345+EXAMPLE123456789" }, + { + "name": "X-Received", + "value": "by 192.0.2.1 with SMTP id 12345abc67890; Thu, 5 Dec 2024 08:30:00 -0800 (PST)" + }, + { + "name": "ARC-Seal", + "value": "i=1; a=rsa-sha256; t=1733405400; cv=none; d=example.com; s=arc-20241205; b=ABCDEFG123456=" + }, + { + "name": "ARC-Message-Signature", + "value": "i=1; a=rsa-sha256; c=relaxed/relaxed; d=example.com; s=arc-20241205; bh=EXAMPLEHASH12345=" + }, + { + "name": "ARC-Authentication-Results", + "value": "i=1; mx.example.com; dkim=pass header.i=@promotion.example.com; spf=pass smtp.mailfrom=promo@promotion.example.com; dmarc=pass header.from=example.com" + }, + { "name": "Return-Path", "value": "" }, + { "name": "Date", "value": "Thu, 5 Dec 2024 08:30:00 -0800" }, + { "name": "From", "value": "Holiday Deals " }, + { "name": "To", "value": "exampleuser@gmail.com" }, + { "name": "Message-ID", "value": "<12345abc67890@promotion.example.com>" }, + { "name": "Subject", "value": "Exclusive Holiday Discounts!" }, + { "name": "MIME-Version", "value": "1.0" }, + { + "name": "Content-Type", + "value": "multipart/alternative; boundary=\"----=_Part_12345_67890.1733405400000\"" + } + ], + "body": { "size": 0 }, + "parts": [ + { + "partId": "0", + "mimeType": "text/plain", + "filename": "", + "headers": [ + { "name": "Content-Type", "value": "text/plain; charset=utf-8" }, + { "name": "Content-Transfer-Encoding", "value": "quoted-printable" } + ], + "body": { + "size": 1234, + "data": "VGhpcyBpcyBhbiBleGFtcGxlIG1lc3NhZ2UuIFRoYW5rIHlvdSBmb3Igc2hvcHBpbmcgd2l0aCB1cy4=" + } + }, + { + "partId": "1", + "mimeType": "text/html", + "filename": "", + "headers": [ + { "name": "Content-Type", "value": "text/html; charset=\"utf-8\"" }, + { "name": "Content-Transfer-Encoding", "value": "quoted-printable" } + ], + "body": { + "size": 5678, + "data": "PGRpdiBzdHlsZT0nZm9udC1mYW1pbHk6IEFyaWFsLCBzYW5zLXNlcmlmOyc+VGhpcyBpcyBhbiBleGFtcGxlIGh0bWwgbWVzc2FnZS4gPGI+VGhhbmsgeW91IGZvciBzaG9wcGluZyB3aXRoIHVzLjwvYj48L2Rpdj4=" + } + } + ] + }, + "sizeEstimate": 67890, + "historyId": "54321", + "internalDate": "1733405400000" + } + } + ], + "Gmail - Messages - Remove Label": [ + { + "json": { + "id": "a1b2c3d4e5f6g7h8", + "threadId": "a1b2c3d4e5f6g7h8", + "labelIds": ["UNREAD", "CATEGORY_PROMOTIONS", "INBOX"], + "snippet": "Don't miss our exclusive holiday discounts on all items! Act now before the sale ends.", + "payload": { + "partId": "", + "mimeType": "multipart/alternative", + "filename": "", + "headers": [ + { "name": "Delivered-To", "value": "exampleuser@gmail.com" }, + { + "name": "Received", + "value": "by 2001:db8::abcd with SMTP id xyz123abc456; Thu, 5 Dec 2024 08:30:00 -0800 (PST)" + }, + { "name": "X-Google-Smtp-Source", "value": "ABC12345+EXAMPLE123456789" }, + { + "name": "X-Received", + "value": "by 192.0.2.1 with SMTP id 12345abc67890; Thu, 5 Dec 2024 08:30:00 -0800 (PST)" + }, + { + "name": "ARC-Seal", + "value": "i=1; a=rsa-sha256; t=1733405400; cv=none; d=example.com; s=arc-20241205; b=ABCDEFG123456=" + }, + { + "name": "ARC-Message-Signature", + "value": "i=1; a=rsa-sha256; c=relaxed/relaxed; d=example.com; s=arc-20241205; bh=EXAMPLEHASH12345=" + }, + { + "name": "ARC-Authentication-Results", + "value": "i=1; mx.example.com; dkim=pass header.i=@promotion.example.com; spf=pass smtp.mailfrom=promo@promotion.example.com; dmarc=pass header.from=example.com" + }, + { "name": "Return-Path", "value": "" }, + { "name": "Date", "value": "Thu, 5 Dec 2024 08:30:00 -0800" }, + { "name": "From", "value": "Holiday Deals " }, + { "name": "To", "value": "exampleuser@gmail.com" }, + { "name": "Message-ID", "value": "<12345abc67890@promotion.example.com>" }, + { "name": "Subject", "value": "Exclusive Holiday Discounts!" }, + { "name": "MIME-Version", "value": "1.0" }, + { + "name": "Content-Type", + "value": "multipart/alternative; boundary=\"----=_Part_12345_67890.1733405400000\"" + } + ], + "body": { "size": 0 }, + "parts": [ + { + "partId": "0", + "mimeType": "text/plain", + "filename": "", + "headers": [ + { "name": "Content-Type", "value": "text/plain; charset=utf-8" }, + { "name": "Content-Transfer-Encoding", "value": "quoted-printable" } + ], + "body": { + "size": 1234, + "data": "VGhpcyBpcyBhbiBleGFtcGxlIG1lc3NhZ2UuIFRoYW5rIHlvdSBmb3Igc2hvcHBpbmcgd2l0aCB1cy4=" + } + }, + { + "partId": "1", + "mimeType": "text/html", + "filename": "", + "headers": [ + { "name": "Content-Type", "value": "text/html; charset=\"utf-8\"" }, + { "name": "Content-Transfer-Encoding", "value": "quoted-printable" } + ], + "body": { + "size": 5678, + "data": "PGRpdiBzdHlsZT0nZm9udC1mYW1pbHk6IEFyaWFsLCBzYW5zLXNlcmlmOyc+VGhpcyBpcyBhbiBleGFtcGxlIGh0bWwgbWVzc2FnZS4gPGI+VGhhbmsgeW91IGZvciBzaG9wcGluZyB3aXRoIHVzLjwvYj48L2Rpdj4=" + } + } + ] + }, + "sizeEstimate": 67890, + "historyId": "54321", + "internalDate": "1733405400000" + } + } + ], + "Gmail - Messages - Reply": [ + { + "json": { + "id": "a1b2c3d4e5f6g7h8", + "threadId": "a1b2c3d4e5f6g7h8", + "labelIds": ["UNREAD", "CATEGORY_PROMOTIONS", "INBOX"], + "snippet": "Don't miss our exclusive holiday discounts on all items! Act now before the sale ends.", + "payload": { + "partId": "", + "mimeType": "multipart/alternative", + "filename": "", + "headers": [ + { "name": "Delivered-To", "value": "exampleuser@gmail.com" }, + { + "name": "Received", + "value": "by 2001:db8::abcd with SMTP id xyz123abc456; Thu, 5 Dec 2024 08:30:00 -0800 (PST)" + }, + { "name": "X-Google-Smtp-Source", "value": "ABC12345+EXAMPLE123456789" }, + { + "name": "X-Received", + "value": "by 192.0.2.1 with SMTP id 12345abc67890; Thu, 5 Dec 2024 08:30:00 -0800 (PST)" + }, + { + "name": "ARC-Seal", + "value": "i=1; a=rsa-sha256; t=1733405400; cv=none; d=example.com; s=arc-20241205; b=ABCDEFG123456=" + }, + { + "name": "ARC-Message-Signature", + "value": "i=1; a=rsa-sha256; c=relaxed/relaxed; d=example.com; s=arc-20241205; bh=EXAMPLEHASH12345=" + }, + { + "name": "ARC-Authentication-Results", + "value": "i=1; mx.example.com; dkim=pass header.i=@promotion.example.com; spf=pass smtp.mailfrom=promo@promotion.example.com; dmarc=pass header.from=example.com" + }, + { "name": "Return-Path", "value": "" }, + { "name": "Date", "value": "Thu, 5 Dec 2024 08:30:00 -0800" }, + { "name": "From", "value": "Holiday Deals " }, + { "name": "To", "value": "exampleuser@gmail.com" }, + { "name": "Message-ID", "value": "<12345abc67890@promotion.example.com>" }, + { "name": "Subject", "value": "Exclusive Holiday Discounts!" }, + { "name": "MIME-Version", "value": "1.0" }, + { + "name": "Content-Type", + "value": "multipart/alternative; boundary=\"----=_Part_12345_67890.1733405400000\"" + } + ], + "body": { "size": 0 }, + "parts": [ + { + "partId": "0", + "mimeType": "text/plain", + "filename": "", + "headers": [ + { "name": "Content-Type", "value": "text/plain; charset=utf-8" }, + { "name": "Content-Transfer-Encoding", "value": "quoted-printable" } + ], + "body": { + "size": 1234, + "data": "VGhpcyBpcyBhbiBleGFtcGxlIG1lc3NhZ2UuIFRoYW5rIHlvdSBmb3Igc2hvcHBpbmcgd2l0aCB1cy4=" + } + }, + { + "partId": "1", + "mimeType": "text/html", + "filename": "", + "headers": [ + { "name": "Content-Type", "value": "text/html; charset=\"utf-8\"" }, + { "name": "Content-Transfer-Encoding", "value": "quoted-printable" } + ], + "body": { + "size": 5678, + "data": "PGRpdiBzdHlsZT0nZm9udC1mYW1pbHk6IEFyaWFsLCBzYW5zLXNlcmlmOyc+VGhpcyBpcyBhbiBleGFtcGxlIGh0bWwgbWVzc2FnZS4gPGI+VGhhbmsgeW91IGZvciBzaG9wcGluZyB3aXRoIHVzLjwvYj48L2Rpdj4=" + } + } + ] + }, + "sizeEstimate": 67890, + "historyId": "54321", + "internalDate": "1733405400000" + } + } + ] + }, + "connections": { + "When clicking ‘Test workflow’": { + "main": [ + [ + { + "node": "Gmail - Messages - All (simplified)", + "type": "main", + "index": 0 + }, + { + "node": "Gmail - Messages - All", + "type": "main", + "index": 0 + }, + { + "node": "Gmail - Messages - Send and Wait for Approval", + "type": "main", + "index": 0 + }, + { + "node": "Gmail - Messages - Add Label", + "type": "main", + "index": 0 + }, + { + "node": "Gmail - Messages - Delete", + "type": "main", + "index": 0 + }, + { + "node": "Gmail - Messages - Get (simplified)", + "type": "main", + "index": 0 + }, + { + "node": "Gmail - Messages - Get", + "type": "main", + "index": 0 + }, + { + "node": "Gmail - Messages - Mark as Read", + "type": "main", + "index": 0 + }, + { + "node": "Gmail - Messages - Mark as Unread", + "type": "main", + "index": 0 + }, + { + "node": "Gmail - Messages - Remove Label", + "type": "main", + "index": 0 + }, + { + "node": "Gmail - Messages - Reply", + "type": "main", + "index": 0 + }, + { + "node": "Edit Fields", + "type": "main", + "index": 0 + } + ] + ] + }, + "Attachment": { + "main": [ + [ + { + "node": "Gmail - Messages - Send", + "type": "main", + "index": 0 + } + ] + ] + }, + "Edit Fields": { + "main": [ + [ + { + "node": "Attachment", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": { + "executionOrder": "v1" + }, + "versionId": "f497669f-aced-4f80-8e59-381d1c9422c7", + "meta": { + "instanceId": "27cc9b56542ad45b38725555722c50a1c3fee1670bbb67980558314ee08517c4" + }, + "id": "jOoDfyAYzh3NAli3", + "tags": [] +} diff --git a/packages/nodes-base/nodes/Google/Gmail/test/v2/threads.workflow.json b/packages/nodes-base/nodes/Google/Gmail/test/v2/threads.workflow.json new file mode 100644 index 0000000000000..e92d016877310 --- /dev/null +++ b/packages/nodes-base/nodes/Google/Gmail/test/v2/threads.workflow.json @@ -0,0 +1,1174 @@ +{ + "name": "My workflow 130", + "nodes": [ + { + "parameters": {}, + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [20, 920], + "id": "cfa1d2b9-9706-48a2-9dd4-39e4c3c0ccb4", + "name": "When clicking ‘Test workflow’" + }, + { + "parameters": { + "resource": "thread", + "limit": 2, + "filters": {} + }, + "type": "n8n-nodes-base.gmail", + "typeVersion": 2.1, + "position": [800, 320], + "id": "7b925004-8583-4f43-b1f0-6a37331cabf4", + "name": "Gmail - Threads - All (simplified)", + "webhookId": "f3cbddc1-3cfa-4217-aa73-f0e5a9309661", + "credentials": { + "gmailOAuth2": { + "id": "22", + "name": "Gmail 0auth" + } + } + }, + { + "parameters": { + "resource": "thread", + "limit": 2, + "filters": { + "includeSpamTrash": true, + "labelIds": ["CHAT"], + "q": "has:attachment", + "readStatus": "unread", + "receivedAfter": "2024-12-05T00:00:00", + "receivedBefore": "2024-12-27T00:00:00" + } + }, + "type": "n8n-nodes-base.gmail", + "typeVersion": 2.1, + "position": [800, 520], + "id": "22fa66e3-ac89-4289-82c7-e3be3c271d04", + "name": "Gmail - Threads - All", + "webhookId": "f3cbddc1-3cfa-4217-aa73-f0e5a9309661", + "credentials": { + "gmailOAuth2": { + "id": "22", + "name": "Gmail 0auth" + } + } + }, + { + "parameters": { + "resource": "thread", + "operation": "addLabels", + "threadId": "test-thread-id", + "labelIds": ["CHAT"] + }, + "type": "n8n-nodes-base.gmail", + "typeVersion": 2.1, + "position": [800, 700], + "id": "b94b2e79-a47d-4e05-a74b-957a9efe46a5", + "name": "Gmail - Threads - Add Label", + "webhookId": "f3cbddc1-3cfa-4217-aa73-f0e5a9309661", + "credentials": { + "gmailOAuth2": { + "id": "22", + "name": "Gmail 0auth" + } + } + }, + { + "parameters": { + "resource": "thread", + "operation": "removeLabels", + "threadId": "test-thread-id", + "labelIds": ["CHAT"] + }, + "type": "n8n-nodes-base.gmail", + "typeVersion": 2.1, + "position": [800, 900], + "id": "5322c0f6-e040-4bb4-9014-041662e7a173", + "name": "Gmail - Threads - Remove Label", + "webhookId": "f3cbddc1-3cfa-4217-aa73-f0e5a9309661", + "credentials": { + "gmailOAuth2": { + "id": "22", + "name": "Gmail 0auth" + } + } + }, + { + "parameters": { + "resource": "thread", + "operation": "delete", + "threadId": "test-thread-id" + }, + "type": "n8n-nodes-base.gmail", + "typeVersion": 2.1, + "position": [800, 1100], + "id": "6f7139f1-9236-4d35-b7ba-48f3e00d11ec", + "name": "Gmail - Threads - Delete", + "webhookId": "f3cbddc1-3cfa-4217-aa73-f0e5a9309661", + "credentials": { + "gmailOAuth2": { + "id": "22", + "name": "Gmail 0auth" + } + } + }, + { + "parameters": { + "resource": "thread", + "operation": "get", + "threadId": "test-thread-id", + "options": {} + }, + "type": "n8n-nodes-base.gmail", + "typeVersion": 2.1, + "position": [800, 1300], + "id": "4e7bccd2-e663-4824-b870-bb5fe7ad6c57", + "name": "Gmail - Threads - Get (simplified)", + "webhookId": "f3cbddc1-3cfa-4217-aa73-f0e5a9309661", + "credentials": { + "gmailOAuth2": { + "id": "22", + "name": "Gmail 0auth" + } + } + }, + { + "parameters": { + "resource": "thread", + "operation": "get", + "threadId": "test-thread-id", + "simple": false, + "options": { + "returnOnlyMessages": true + } + }, + "type": "n8n-nodes-base.gmail", + "typeVersion": 2.1, + "position": [800, 1500], + "id": "4084a6fd-6770-4aa2-ac4e-6108f9b159a2", + "name": "Gmail - Threads - Get", + "webhookId": "f3cbddc1-3cfa-4217-aa73-f0e5a9309661", + "credentials": { + "gmailOAuth2": { + "id": "22", + "name": "Gmail 0auth" + } + } + }, + { + "parameters": { + "resource": "thread", + "operation": "reply", + "threadId": "test-thread-id", + "messageId": "=test snippet", + "message": "Test reply", + "options": { + "attachmentsUi": { + "attachmentsBinary": [ + { + "property": "data" + } + ] + }, + "bccList": "test-bcc@n8n.io", + "ccList": "test-cc@n8n.io", + "senderName": "Test Sender Name", + "replyToSenderOnly": true + } + }, + "type": "n8n-nodes-base.gmail", + "typeVersion": 2.1, + "position": [1120, 2080], + "id": "d5b44033-655a-4e18-99c7-6ee19140967b", + "name": "Gmail - Threads - Reply", + "webhookId": "f3cbddc1-3cfa-4217-aa73-f0e5a9309661", + "credentials": { + "gmailOAuth2": { + "id": "22", + "name": "Gmail 0auth" + } + } + }, + { + "parameters": { + "resource": "thread", + "operation": "trash", + "threadId": "test-thread-id" + }, + "type": "n8n-nodes-base.gmail", + "typeVersion": 2.1, + "position": [800, 1680], + "id": "1503c357-043c-45f5-bbe8-1d7141a84546", + "name": "Gmail - Threads - Trash", + "webhookId": "f3cbddc1-3cfa-4217-aa73-f0e5a9309661", + "credentials": { + "gmailOAuth2": { + "id": "22", + "name": "Gmail 0auth" + } + } + }, + { + "parameters": { + "resource": "thread", + "operation": "untrash", + "threadId": "test-thread-id" + }, + "type": "n8n-nodes-base.gmail", + "typeVersion": 2.1, + "position": [800, 1880], + "id": "38c02fc0-2e8e-4a6b-a045-eb45f2811c0a", + "name": "Gmail - Threads - Untrash", + "webhookId": "f3cbddc1-3cfa-4217-aa73-f0e5a9309661", + "credentials": { + "gmailOAuth2": { + "id": "22", + "name": "Gmail 0auth" + } + } + }, + { + "parameters": { + "assignments": { + "assignments": [ + { + "id": "0a4e38fe-ed38-400e-afb1-9d430f167d54", + "name": "attachment", + "value": true, + "type": "boolean" + } + ] + }, + "options": {} + }, + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [800, 2080], + "id": "d6a20bfa-4bee-4aef-b1b0-198ed73d1b3f", + "name": "Edit Fields1" + }, + { + "parameters": { + "operation": "toJson", + "options": {} + }, + "type": "n8n-nodes-base.convertToFile", + "typeVersion": 1.1, + "position": [960, 2080], + "id": "47d4a53b-6019-444b-9746-f957704cd4e0", + "name": "Attachment1" + } + ], + "pinData": { + "Gmail - Threads - All (simplified)": [ + { + "json": { + "id": "a1b2c3d4e5f6g7h8", + "threadId": "a1b2c3d4e5f6g7h8", + "labelIds": ["UNREAD", "CATEGORY_PROMOTIONS", "INBOX"], + "snippet": "Don't miss our exclusive holiday discounts on all items! Act now before the sale ends.", + "payload": { + "partId": "", + "mimeType": "multipart/alternative", + "filename": "", + "headers": [ + { "name": "Delivered-To", "value": "exampleuser@gmail.com" }, + { + "name": "Received", + "value": "by 2001:db8::abcd with SMTP id xyz123abc456; Thu, 5 Dec 2024 08:30:00 -0800 (PST)" + }, + { "name": "X-Google-Smtp-Source", "value": "ABC12345+EXAMPLE123456789" }, + { + "name": "X-Received", + "value": "by 192.0.2.1 with SMTP id 12345abc67890; Thu, 5 Dec 2024 08:30:00 -0800 (PST)" + }, + { + "name": "ARC-Seal", + "value": "i=1; a=rsa-sha256; t=1733405400; cv=none; d=example.com; s=arc-20241205; b=ABCDEFG123456=" + }, + { + "name": "ARC-Message-Signature", + "value": "i=1; a=rsa-sha256; c=relaxed/relaxed; d=example.com; s=arc-20241205; bh=EXAMPLEHASH12345=" + }, + { + "name": "ARC-Authentication-Results", + "value": "i=1; mx.example.com; dkim=pass header.i=@promotion.example.com; spf=pass smtp.mailfrom=promo@promotion.example.com; dmarc=pass header.from=example.com" + }, + { "name": "Return-Path", "value": "" }, + { "name": "Date", "value": "Thu, 5 Dec 2024 08:30:00 -0800" }, + { "name": "From", "value": "Holiday Deals " }, + { "name": "To", "value": "exampleuser@gmail.com" }, + { "name": "Message-ID", "value": "<12345abc67890@promotion.example.com>" }, + { "name": "Subject", "value": "Exclusive Holiday Discounts!" }, + { "name": "MIME-Version", "value": "1.0" }, + { + "name": "Content-Type", + "value": "multipart/alternative; boundary=\"----=_Part_12345_67890.1733405400000\"" + } + ], + "body": { "size": 0 }, + "parts": [ + { + "partId": "0", + "mimeType": "text/plain", + "filename": "", + "headers": [ + { "name": "Content-Type", "value": "text/plain; charset=utf-8" }, + { "name": "Content-Transfer-Encoding", "value": "quoted-printable" } + ], + "body": { + "size": 1234, + "data": "VGhpcyBpcyBhbiBleGFtcGxlIG1lc3NhZ2UuIFRoYW5rIHlvdSBmb3Igc2hvcHBpbmcgd2l0aCB1cy4=" + } + }, + { + "partId": "1", + "mimeType": "text/html", + "filename": "", + "headers": [ + { "name": "Content-Type", "value": "text/html; charset=\"utf-8\"" }, + { "name": "Content-Transfer-Encoding", "value": "quoted-printable" } + ], + "body": { + "size": 5678, + "data": "PGRpdiBzdHlsZT0nZm9udC1mYW1pbHk6IEFyaWFsLCBzYW5zLXNlcmlmOyc+VGhpcyBpcyBhbiBleGFtcGxlIGh0bWwgbWVzc2FnZS4gPGI+VGhhbmsgeW91IGZvciBzaG9wcGluZyB3aXRoIHVzLjwvYj48L2Rpdj4=" + } + } + ] + }, + "sizeEstimate": 67890, + "historyId": "54321", + "internalDate": "1733405400000" + } + }, + { + "json": { + "id": "z9y8x7w6v5u4t3s2", + "threadId": "z9y8x7w6v5u4t3s2", + "labelIds": ["UNREAD", "CATEGORY_SOCIAL", "INBOX"], + "snippet": "Your friend John just shared a new photo with you! Check it out now.", + "payload": { + "partId": "", + "mimeType": "multipart/alternative", + "filename": "", + "headers": [ + { "name": "Delivered-To", "value": "exampleuser2@gmail.com" }, + { + "name": "Received", + "value": "by 2001:db8::abcd with SMTP id def456ghi789; Fri, 6 Dec 2024 09:45:00 -0800 (PST)" + }, + { "name": "X-Google-Smtp-Source", "value": "XYZ67890+EXAMPLE0987654321" }, + { + "name": "X-Received", + "value": "by 198.51.100.2 with SMTP id 67890def12345; Fri, 6 Dec 2024 09:45:00 -0800 (PST)" + }, + { + "name": "ARC-Seal", + "value": "i=1; a=rsa-sha256; t=1733490900; cv=none; d=example2.com; s=arc-20241206; b=HIJKLMN987654=" + }, + { + "name": "ARC-Message-Signature", + "value": "i=1; a=rsa-sha256; c=relaxed/relaxed; d=example2.com; s=arc-20241206; bh=EXAMPLEHASH67890=" + }, + { + "name": "ARC-Authentication-Results", + "value": "i=1; mx.example2.com; dkim=pass header.i=@social.example2.com; spf=pass smtp.mailfrom=notifications@social.example2.com; dmarc=pass header.from=example2.com" + }, + { "name": "Return-Path", "value": "" }, + { "name": "Date", "value": "Fri, 6 Dec 2024 09:45:00 -0800" }, + { "name": "From", "value": "John's Photos " }, + { "name": "To", "value": "exampleuser2@gmail.com" }, + { "name": "Message-ID", "value": "<67890def12345@social.example2.com>" }, + { "name": "Subject", "value": "John shared a new photo with you!" }, + { "name": "MIME-Version", "value": "1.0" }, + { + "name": "Content-Type", + "value": "multipart/alternative; boundary=\"----=_Part_67890_12345.1733490900000\"" + } + ], + "body": { "size": 0 }, + "parts": [ + { + "partId": "0", + "mimeType": "text/plain", + "filename": "", + "headers": [ + { "name": "Content-Type", "value": "text/plain; charset=utf-8" }, + { "name": "Content-Transfer-Encoding", "value": "quoted-printable" } + ], + "body": { + "size": 4321, + "data": "U2VlIHRoZSBhdHRhY2hlZCBwaG90byBhbmQgcmVwbHkgdG8gSm9obi4gV2UgaG9wZSB5b3UgbGlrZSBpdCE=" + } + }, + { + "partId": "1", + "mimeType": "text/html", + "filename": "", + "headers": [ + { "name": "Content-Type", "value": "text/html; charset=\"utf-8\"" }, + { "name": "Content-Transfer-Encoding", "value": "quoted-printable" } + ], + "body": { + "size": 8765, + "data": "PGRpdiBzdHlsZT0nZm9udC1mYW1pbHk6IEFyaWFsLCBzYW5zLXNlcmlmOyc+U2VlIHRoZSBhdHRhY2hlZCBwaG90byBhbmQgcmVwbHkgdG8gPGI+Sm9obi48L2I+IFdlIGhvcGUgeW91IGxpa2UgaXQhPC9kaXY+" + } + } + ] + }, + "sizeEstimate": 54321, + "historyId": "98765", + "internalDate": "1733490900000" + } + } + ], + "Gmail - Threads - All": [ + { + "json": { + "id": "a1b2c3d4e5f6g7h8", + "threadId": "a1b2c3d4e5f6g7h8", + "labelIds": ["UNREAD", "CATEGORY_PROMOTIONS", "INBOX"], + "snippet": "Don't miss our exclusive holiday discounts on all items! Act now before the sale ends.", + "payload": { + "partId": "", + "mimeType": "multipart/alternative", + "filename": "", + "headers": [ + { "name": "Delivered-To", "value": "exampleuser@gmail.com" }, + { + "name": "Received", + "value": "by 2001:db8::abcd with SMTP id xyz123abc456; Thu, 5 Dec 2024 08:30:00 -0800 (PST)" + }, + { "name": "X-Google-Smtp-Source", "value": "ABC12345+EXAMPLE123456789" }, + { + "name": "X-Received", + "value": "by 192.0.2.1 with SMTP id 12345abc67890; Thu, 5 Dec 2024 08:30:00 -0800 (PST)" + }, + { + "name": "ARC-Seal", + "value": "i=1; a=rsa-sha256; t=1733405400; cv=none; d=example.com; s=arc-20241205; b=ABCDEFG123456=" + }, + { + "name": "ARC-Message-Signature", + "value": "i=1; a=rsa-sha256; c=relaxed/relaxed; d=example.com; s=arc-20241205; bh=EXAMPLEHASH12345=" + }, + { + "name": "ARC-Authentication-Results", + "value": "i=1; mx.example.com; dkim=pass header.i=@promotion.example.com; spf=pass smtp.mailfrom=promo@promotion.example.com; dmarc=pass header.from=example.com" + }, + { "name": "Return-Path", "value": "" }, + { "name": "Date", "value": "Thu, 5 Dec 2024 08:30:00 -0800" }, + { "name": "From", "value": "Holiday Deals " }, + { "name": "To", "value": "exampleuser@gmail.com" }, + { "name": "Message-ID", "value": "<12345abc67890@promotion.example.com>" }, + { "name": "Subject", "value": "Exclusive Holiday Discounts!" }, + { "name": "MIME-Version", "value": "1.0" }, + { + "name": "Content-Type", + "value": "multipart/alternative; boundary=\"----=_Part_12345_67890.1733405400000\"" + } + ], + "body": { "size": 0 }, + "parts": [ + { + "partId": "0", + "mimeType": "text/plain", + "filename": "", + "headers": [ + { "name": "Content-Type", "value": "text/plain; charset=utf-8" }, + { "name": "Content-Transfer-Encoding", "value": "quoted-printable" } + ], + "body": { + "size": 1234, + "data": "VGhpcyBpcyBhbiBleGFtcGxlIG1lc3NhZ2UuIFRoYW5rIHlvdSBmb3Igc2hvcHBpbmcgd2l0aCB1cy4=" + } + }, + { + "partId": "1", + "mimeType": "text/html", + "filename": "", + "headers": [ + { "name": "Content-Type", "value": "text/html; charset=\"utf-8\"" }, + { "name": "Content-Transfer-Encoding", "value": "quoted-printable" } + ], + "body": { + "size": 5678, + "data": "PGRpdiBzdHlsZT0nZm9udC1mYW1pbHk6IEFyaWFsLCBzYW5zLXNlcmlmOyc+VGhpcyBpcyBhbiBleGFtcGxlIGh0bWwgbWVzc2FnZS4gPGI+VGhhbmsgeW91IGZvciBzaG9wcGluZyB3aXRoIHVzLjwvYj48L2Rpdj4=" + } + } + ] + }, + "sizeEstimate": 67890, + "historyId": "54321", + "internalDate": "1733405400000" + } + }, + { + "json": { + "id": "z9y8x7w6v5u4t3s2", + "threadId": "z9y8x7w6v5u4t3s2", + "labelIds": ["UNREAD", "CATEGORY_SOCIAL", "INBOX"], + "snippet": "Your friend John just shared a new photo with you! Check it out now.", + "payload": { + "partId": "", + "mimeType": "multipart/alternative", + "filename": "", + "headers": [ + { "name": "Delivered-To", "value": "exampleuser2@gmail.com" }, + { + "name": "Received", + "value": "by 2001:db8::abcd with SMTP id def456ghi789; Fri, 6 Dec 2024 09:45:00 -0800 (PST)" + }, + { "name": "X-Google-Smtp-Source", "value": "XYZ67890+EXAMPLE0987654321" }, + { + "name": "X-Received", + "value": "by 198.51.100.2 with SMTP id 67890def12345; Fri, 6 Dec 2024 09:45:00 -0800 (PST)" + }, + { + "name": "ARC-Seal", + "value": "i=1; a=rsa-sha256; t=1733490900; cv=none; d=example2.com; s=arc-20241206; b=HIJKLMN987654=" + }, + { + "name": "ARC-Message-Signature", + "value": "i=1; a=rsa-sha256; c=relaxed/relaxed; d=example2.com; s=arc-20241206; bh=EXAMPLEHASH67890=" + }, + { + "name": "ARC-Authentication-Results", + "value": "i=1; mx.example2.com; dkim=pass header.i=@social.example2.com; spf=pass smtp.mailfrom=notifications@social.example2.com; dmarc=pass header.from=example2.com" + }, + { "name": "Return-Path", "value": "" }, + { "name": "Date", "value": "Fri, 6 Dec 2024 09:45:00 -0800" }, + { "name": "From", "value": "John's Photos " }, + { "name": "To", "value": "exampleuser2@gmail.com" }, + { "name": "Message-ID", "value": "<67890def12345@social.example2.com>" }, + { "name": "Subject", "value": "John shared a new photo with you!" }, + { "name": "MIME-Version", "value": "1.0" }, + { + "name": "Content-Type", + "value": "multipart/alternative; boundary=\"----=_Part_67890_12345.1733490900000\"" + } + ], + "body": { "size": 0 }, + "parts": [ + { + "partId": "0", + "mimeType": "text/plain", + "filename": "", + "headers": [ + { "name": "Content-Type", "value": "text/plain; charset=utf-8" }, + { "name": "Content-Transfer-Encoding", "value": "quoted-printable" } + ], + "body": { + "size": 4321, + "data": "U2VlIHRoZSBhdHRhY2hlZCBwaG90byBhbmQgcmVwbHkgdG8gSm9obi4gV2UgaG9wZSB5b3UgbGlrZSBpdCE=" + } + }, + { + "partId": "1", + "mimeType": "text/html", + "filename": "", + "headers": [ + { "name": "Content-Type", "value": "text/html; charset=\"utf-8\"" }, + { "name": "Content-Transfer-Encoding", "value": "quoted-printable" } + ], + "body": { + "size": 8765, + "data": "PGRpdiBzdHlsZT0nZm9udC1mYW1pbHk6IEFyaWFsLCBzYW5zLXNlcmlmOyc+U2VlIHRoZSBhdHRhY2hlZCBwaG90byBhbmQgcmVwbHkgdG8gPGI+Sm9obi48L2I+IFdlIGhvcGUgeW91IGxpa2UgaXQhPC9kaXY+" + } + } + ] + }, + "sizeEstimate": 54321, + "historyId": "98765", + "internalDate": "1733490900000" + } + } + ], + "Gmail - Threads - Add Label": [ + { + "json": { + "id": "a1b2c3d4e5f6g7h8", + "threadId": "a1b2c3d4e5f6g7h8", + "labelIds": ["UNREAD", "CATEGORY_PROMOTIONS", "INBOX"], + "snippet": "Don't miss our exclusive holiday discounts on all items! Act now before the sale ends.", + "payload": { + "partId": "", + "mimeType": "multipart/alternative", + "filename": "", + "headers": [ + { "name": "Delivered-To", "value": "exampleuser@gmail.com" }, + { + "name": "Received", + "value": "by 2001:db8::abcd with SMTP id xyz123abc456; Thu, 5 Dec 2024 08:30:00 -0800 (PST)" + }, + { "name": "X-Google-Smtp-Source", "value": "ABC12345+EXAMPLE123456789" }, + { + "name": "X-Received", + "value": "by 192.0.2.1 with SMTP id 12345abc67890; Thu, 5 Dec 2024 08:30:00 -0800 (PST)" + }, + { + "name": "ARC-Seal", + "value": "i=1; a=rsa-sha256; t=1733405400; cv=none; d=example.com; s=arc-20241205; b=ABCDEFG123456=" + }, + { + "name": "ARC-Message-Signature", + "value": "i=1; a=rsa-sha256; c=relaxed/relaxed; d=example.com; s=arc-20241205; bh=EXAMPLEHASH12345=" + }, + { + "name": "ARC-Authentication-Results", + "value": "i=1; mx.example.com; dkim=pass header.i=@promotion.example.com; spf=pass smtp.mailfrom=promo@promotion.example.com; dmarc=pass header.from=example.com" + }, + { "name": "Return-Path", "value": "" }, + { "name": "Date", "value": "Thu, 5 Dec 2024 08:30:00 -0800" }, + { "name": "From", "value": "Holiday Deals " }, + { "name": "To", "value": "exampleuser@gmail.com" }, + { "name": "Message-ID", "value": "<12345abc67890@promotion.example.com>" }, + { "name": "Subject", "value": "Exclusive Holiday Discounts!" }, + { "name": "MIME-Version", "value": "1.0" }, + { + "name": "Content-Type", + "value": "multipart/alternative; boundary=\"----=_Part_12345_67890.1733405400000\"" + } + ], + "body": { "size": 0 }, + "parts": [ + { + "partId": "0", + "mimeType": "text/plain", + "filename": "", + "headers": [ + { "name": "Content-Type", "value": "text/plain; charset=utf-8" }, + { "name": "Content-Transfer-Encoding", "value": "quoted-printable" } + ], + "body": { + "size": 1234, + "data": "VGhpcyBpcyBhbiBleGFtcGxlIG1lc3NhZ2UuIFRoYW5rIHlvdSBmb3Igc2hvcHBpbmcgd2l0aCB1cy4=" + } + }, + { + "partId": "1", + "mimeType": "text/html", + "filename": "", + "headers": [ + { "name": "Content-Type", "value": "text/html; charset=\"utf-8\"" }, + { "name": "Content-Transfer-Encoding", "value": "quoted-printable" } + ], + "body": { + "size": 5678, + "data": "PGRpdiBzdHlsZT0nZm9udC1mYW1pbHk6IEFyaWFsLCBzYW5zLXNlcmlmOyc+VGhpcyBpcyBhbiBleGFtcGxlIGh0bWwgbWVzc2FnZS4gPGI+VGhhbmsgeW91IGZvciBzaG9wcGluZyB3aXRoIHVzLjwvYj48L2Rpdj4=" + } + } + ] + }, + "sizeEstimate": 67890, + "historyId": "54321", + "internalDate": "1733405400000" + } + } + ], + "Gmail - Threads - Remove Label": [ + { + "json": { + "id": "a1b2c3d4e5f6g7h8", + "threadId": "a1b2c3d4e5f6g7h8", + "labelIds": ["UNREAD", "CATEGORY_PROMOTIONS", "INBOX"], + "snippet": "Don't miss our exclusive holiday discounts on all items! Act now before the sale ends.", + "payload": { + "partId": "", + "mimeType": "multipart/alternative", + "filename": "", + "headers": [ + { "name": "Delivered-To", "value": "exampleuser@gmail.com" }, + { + "name": "Received", + "value": "by 2001:db8::abcd with SMTP id xyz123abc456; Thu, 5 Dec 2024 08:30:00 -0800 (PST)" + }, + { "name": "X-Google-Smtp-Source", "value": "ABC12345+EXAMPLE123456789" }, + { + "name": "X-Received", + "value": "by 192.0.2.1 with SMTP id 12345abc67890; Thu, 5 Dec 2024 08:30:00 -0800 (PST)" + }, + { + "name": "ARC-Seal", + "value": "i=1; a=rsa-sha256; t=1733405400; cv=none; d=example.com; s=arc-20241205; b=ABCDEFG123456=" + }, + { + "name": "ARC-Message-Signature", + "value": "i=1; a=rsa-sha256; c=relaxed/relaxed; d=example.com; s=arc-20241205; bh=EXAMPLEHASH12345=" + }, + { + "name": "ARC-Authentication-Results", + "value": "i=1; mx.example.com; dkim=pass header.i=@promotion.example.com; spf=pass smtp.mailfrom=promo@promotion.example.com; dmarc=pass header.from=example.com" + }, + { "name": "Return-Path", "value": "" }, + { "name": "Date", "value": "Thu, 5 Dec 2024 08:30:00 -0800" }, + { "name": "From", "value": "Holiday Deals " }, + { "name": "To", "value": "exampleuser@gmail.com" }, + { "name": "Message-ID", "value": "<12345abc67890@promotion.example.com>" }, + { "name": "Subject", "value": "Exclusive Holiday Discounts!" }, + { "name": "MIME-Version", "value": "1.0" }, + { + "name": "Content-Type", + "value": "multipart/alternative; boundary=\"----=_Part_12345_67890.1733405400000\"" + } + ], + "body": { "size": 0 }, + "parts": [ + { + "partId": "0", + "mimeType": "text/plain", + "filename": "", + "headers": [ + { "name": "Content-Type", "value": "text/plain; charset=utf-8" }, + { "name": "Content-Transfer-Encoding", "value": "quoted-printable" } + ], + "body": { + "size": 1234, + "data": "VGhpcyBpcyBhbiBleGFtcGxlIG1lc3NhZ2UuIFRoYW5rIHlvdSBmb3Igc2hvcHBpbmcgd2l0aCB1cy4=" + } + }, + { + "partId": "1", + "mimeType": "text/html", + "filename": "", + "headers": [ + { "name": "Content-Type", "value": "text/html; charset=\"utf-8\"" }, + { "name": "Content-Transfer-Encoding", "value": "quoted-printable" } + ], + "body": { + "size": 5678, + "data": "PGRpdiBzdHlsZT0nZm9udC1mYW1pbHk6IEFyaWFsLCBzYW5zLXNlcmlmOyc+VGhpcyBpcyBhbiBleGFtcGxlIGh0bWwgbWVzc2FnZS4gPGI+VGhhbmsgeW91IGZvciBzaG9wcGluZyB3aXRoIHVzLjwvYj48L2Rpdj4=" + } + } + ] + }, + "sizeEstimate": 67890, + "historyId": "54321", + "internalDate": "1733405400000" + } + } + ], + "Gmail - Threads - Delete": [{ "json": { "success": true } }], + "Gmail - Threads - Get (simplified)": [ + { + "json": { + "id": "a1b2c3d4e5f6g7h8", + "threadId": "a1b2c3d4e5f6g7h8", + "labelIds": ["UNREAD", "CATEGORY_PROMOTIONS", "INBOX"], + "snippet": "Don't miss our exclusive holiday discounts on all items! Act now before the sale ends.", + "payload": { + "partId": "", + "mimeType": "multipart/alternative", + "filename": "", + "headers": [ + { "name": "Delivered-To", "value": "exampleuser@gmail.com" }, + { + "name": "Received", + "value": "by 2001:db8::abcd with SMTP id xyz123abc456; Thu, 5 Dec 2024 08:30:00 -0800 (PST)" + }, + { "name": "X-Google-Smtp-Source", "value": "ABC12345+EXAMPLE123456789" }, + { + "name": "X-Received", + "value": "by 192.0.2.1 with SMTP id 12345abc67890; Thu, 5 Dec 2024 08:30:00 -0800 (PST)" + }, + { + "name": "ARC-Seal", + "value": "i=1; a=rsa-sha256; t=1733405400; cv=none; d=example.com; s=arc-20241205; b=ABCDEFG123456=" + }, + { + "name": "ARC-Message-Signature", + "value": "i=1; a=rsa-sha256; c=relaxed/relaxed; d=example.com; s=arc-20241205; bh=EXAMPLEHASH12345=" + }, + { + "name": "ARC-Authentication-Results", + "value": "i=1; mx.example.com; dkim=pass header.i=@promotion.example.com; spf=pass smtp.mailfrom=promo@promotion.example.com; dmarc=pass header.from=example.com" + }, + { "name": "Return-Path", "value": "" }, + { "name": "Date", "value": "Thu, 5 Dec 2024 08:30:00 -0800" }, + { "name": "From", "value": "Holiday Deals " }, + { "name": "To", "value": "exampleuser@gmail.com" }, + { "name": "Message-ID", "value": "<12345abc67890@promotion.example.com>" }, + { "name": "Subject", "value": "Exclusive Holiday Discounts!" }, + { "name": "MIME-Version", "value": "1.0" }, + { + "name": "Content-Type", + "value": "multipart/alternative; boundary=\"----=_Part_12345_67890.1733405400000\"" + } + ], + "body": { "size": 0 }, + "parts": [ + { + "partId": "0", + "mimeType": "text/plain", + "filename": "", + "headers": [ + { "name": "Content-Type", "value": "text/plain; charset=utf-8" }, + { "name": "Content-Transfer-Encoding", "value": "quoted-printable" } + ], + "body": { + "size": 1234, + "data": "VGhpcyBpcyBhbiBleGFtcGxlIG1lc3NhZ2UuIFRoYW5rIHlvdSBmb3Igc2hvcHBpbmcgd2l0aCB1cy4=" + } + }, + { + "partId": "1", + "mimeType": "text/html", + "filename": "", + "headers": [ + { "name": "Content-Type", "value": "text/html; charset=\"utf-8\"" }, + { "name": "Content-Transfer-Encoding", "value": "quoted-printable" } + ], + "body": { + "size": 5678, + "data": "PGRpdiBzdHlsZT0nZm9udC1mYW1pbHk6IEFyaWFsLCBzYW5zLXNlcmlmOyc+VGhpcyBpcyBhbiBleGFtcGxlIGh0bWwgbWVzc2FnZS4gPGI+VGhhbmsgeW91IGZvciBzaG9wcGluZyB3aXRoIHVzLjwvYj48L2Rpdj4=" + } + } + ] + }, + "sizeEstimate": 67890, + "historyId": "54321", + "internalDate": "1733405400000", + "messages": [] + } + } + ], + "Gmail - Threads - Get": [], + "Gmail - Threads - Reply": [ + { + "json": { + "id": "a1b2c3d4e5f6g7h8", + "threadId": "a1b2c3d4e5f6g7h8", + "labelIds": ["UNREAD", "CATEGORY_PROMOTIONS", "INBOX"], + "snippet": "Don't miss our exclusive holiday discounts on all items! Act now before the sale ends.", + "payload": { + "partId": "", + "mimeType": "multipart/alternative", + "filename": "", + "headers": [ + { "name": "Delivered-To", "value": "exampleuser@gmail.com" }, + { + "name": "Received", + "value": "by 2001:db8::abcd with SMTP id xyz123abc456; Thu, 5 Dec 2024 08:30:00 -0800 (PST)" + }, + { "name": "X-Google-Smtp-Source", "value": "ABC12345+EXAMPLE123456789" }, + { + "name": "X-Received", + "value": "by 192.0.2.1 with SMTP id 12345abc67890; Thu, 5 Dec 2024 08:30:00 -0800 (PST)" + }, + { + "name": "ARC-Seal", + "value": "i=1; a=rsa-sha256; t=1733405400; cv=none; d=example.com; s=arc-20241205; b=ABCDEFG123456=" + }, + { + "name": "ARC-Message-Signature", + "value": "i=1; a=rsa-sha256; c=relaxed/relaxed; d=example.com; s=arc-20241205; bh=EXAMPLEHASH12345=" + }, + { + "name": "ARC-Authentication-Results", + "value": "i=1; mx.example.com; dkim=pass header.i=@promotion.example.com; spf=pass smtp.mailfrom=promo@promotion.example.com; dmarc=pass header.from=example.com" + }, + { "name": "Return-Path", "value": "" }, + { "name": "Date", "value": "Thu, 5 Dec 2024 08:30:00 -0800" }, + { "name": "From", "value": "Holiday Deals " }, + { "name": "To", "value": "exampleuser@gmail.com" }, + { "name": "Message-ID", "value": "<12345abc67890@promotion.example.com>" }, + { "name": "Subject", "value": "Exclusive Holiday Discounts!" }, + { "name": "MIME-Version", "value": "1.0" }, + { + "name": "Content-Type", + "value": "multipart/alternative; boundary=\"----=_Part_12345_67890.1733405400000\"" + } + ], + "body": { "size": 0 }, + "parts": [ + { + "partId": "0", + "mimeType": "text/plain", + "filename": "", + "headers": [ + { "name": "Content-Type", "value": "text/plain; charset=utf-8" }, + { "name": "Content-Transfer-Encoding", "value": "quoted-printable" } + ], + "body": { + "size": 1234, + "data": "VGhpcyBpcyBhbiBleGFtcGxlIG1lc3NhZ2UuIFRoYW5rIHlvdSBmb3Igc2hvcHBpbmcgd2l0aCB1cy4=" + } + }, + { + "partId": "1", + "mimeType": "text/html", + "filename": "", + "headers": [ + { "name": "Content-Type", "value": "text/html; charset=\"utf-8\"" }, + { "name": "Content-Transfer-Encoding", "value": "quoted-printable" } + ], + "body": { + "size": 5678, + "data": "PGRpdiBzdHlsZT0nZm9udC1mYW1pbHk6IEFyaWFsLCBzYW5zLXNlcmlmOyc+VGhpcyBpcyBhbiBleGFtcGxlIGh0bWwgbWVzc2FnZS4gPGI+VGhhbmsgeW91IGZvciBzaG9wcGluZyB3aXRoIHVzLjwvYj48L2Rpdj4=" + } + } + ] + }, + "sizeEstimate": 67890, + "historyId": "54321", + "internalDate": "1733405400000" + } + } + ], + "Gmail - Threads - Trash": [ + { + "json": { + "id": "a1b2c3d4e5f6g7h8", + "threadId": "a1b2c3d4e5f6g7h8", + "labelIds": ["UNREAD", "CATEGORY_PROMOTIONS", "INBOX"], + "snippet": "Don't miss our exclusive holiday discounts on all items! Act now before the sale ends.", + "payload": { + "partId": "", + "mimeType": "multipart/alternative", + "filename": "", + "headers": [ + { "name": "Delivered-To", "value": "exampleuser@gmail.com" }, + { + "name": "Received", + "value": "by 2001:db8::abcd with SMTP id xyz123abc456; Thu, 5 Dec 2024 08:30:00 -0800 (PST)" + }, + { "name": "X-Google-Smtp-Source", "value": "ABC12345+EXAMPLE123456789" }, + { + "name": "X-Received", + "value": "by 192.0.2.1 with SMTP id 12345abc67890; Thu, 5 Dec 2024 08:30:00 -0800 (PST)" + }, + { + "name": "ARC-Seal", + "value": "i=1; a=rsa-sha256; t=1733405400; cv=none; d=example.com; s=arc-20241205; b=ABCDEFG123456=" + }, + { + "name": "ARC-Message-Signature", + "value": "i=1; a=rsa-sha256; c=relaxed/relaxed; d=example.com; s=arc-20241205; bh=EXAMPLEHASH12345=" + }, + { + "name": "ARC-Authentication-Results", + "value": "i=1; mx.example.com; dkim=pass header.i=@promotion.example.com; spf=pass smtp.mailfrom=promo@promotion.example.com; dmarc=pass header.from=example.com" + }, + { "name": "Return-Path", "value": "" }, + { "name": "Date", "value": "Thu, 5 Dec 2024 08:30:00 -0800" }, + { "name": "From", "value": "Holiday Deals " }, + { "name": "To", "value": "exampleuser@gmail.com" }, + { "name": "Message-ID", "value": "<12345abc67890@promotion.example.com>" }, + { "name": "Subject", "value": "Exclusive Holiday Discounts!" }, + { "name": "MIME-Version", "value": "1.0" }, + { + "name": "Content-Type", + "value": "multipart/alternative; boundary=\"----=_Part_12345_67890.1733405400000\"" + } + ], + "body": { "size": 0 }, + "parts": [ + { + "partId": "0", + "mimeType": "text/plain", + "filename": "", + "headers": [ + { "name": "Content-Type", "value": "text/plain; charset=utf-8" }, + { "name": "Content-Transfer-Encoding", "value": "quoted-printable" } + ], + "body": { + "size": 1234, + "data": "VGhpcyBpcyBhbiBleGFtcGxlIG1lc3NhZ2UuIFRoYW5rIHlvdSBmb3Igc2hvcHBpbmcgd2l0aCB1cy4=" + } + }, + { + "partId": "1", + "mimeType": "text/html", + "filename": "", + "headers": [ + { "name": "Content-Type", "value": "text/html; charset=\"utf-8\"" }, + { "name": "Content-Transfer-Encoding", "value": "quoted-printable" } + ], + "body": { + "size": 5678, + "data": "PGRpdiBzdHlsZT0nZm9udC1mYW1pbHk6IEFyaWFsLCBzYW5zLXNlcmlmOyc+VGhpcyBpcyBhbiBleGFtcGxlIGh0bWwgbWVzc2FnZS4gPGI+VGhhbmsgeW91IGZvciBzaG9wcGluZyB3aXRoIHVzLjwvYj48L2Rpdj4=" + } + } + ] + }, + "sizeEstimate": 67890, + "historyId": "54321", + "internalDate": "1733405400000" + } + } + ], + "Gmail - Threads - Untrash": [ + { + "json": { + "id": "a1b2c3d4e5f6g7h8", + "threadId": "a1b2c3d4e5f6g7h8", + "labelIds": ["UNREAD", "CATEGORY_PROMOTIONS", "INBOX"], + "snippet": "Don't miss our exclusive holiday discounts on all items! Act now before the sale ends.", + "payload": { + "partId": "", + "mimeType": "multipart/alternative", + "filename": "", + "headers": [ + { "name": "Delivered-To", "value": "exampleuser@gmail.com" }, + { + "name": "Received", + "value": "by 2001:db8::abcd with SMTP id xyz123abc456; Thu, 5 Dec 2024 08:30:00 -0800 (PST)" + }, + { "name": "X-Google-Smtp-Source", "value": "ABC12345+EXAMPLE123456789" }, + { + "name": "X-Received", + "value": "by 192.0.2.1 with SMTP id 12345abc67890; Thu, 5 Dec 2024 08:30:00 -0800 (PST)" + }, + { + "name": "ARC-Seal", + "value": "i=1; a=rsa-sha256; t=1733405400; cv=none; d=example.com; s=arc-20241205; b=ABCDEFG123456=" + }, + { + "name": "ARC-Message-Signature", + "value": "i=1; a=rsa-sha256; c=relaxed/relaxed; d=example.com; s=arc-20241205; bh=EXAMPLEHASH12345=" + }, + { + "name": "ARC-Authentication-Results", + "value": "i=1; mx.example.com; dkim=pass header.i=@promotion.example.com; spf=pass smtp.mailfrom=promo@promotion.example.com; dmarc=pass header.from=example.com" + }, + { "name": "Return-Path", "value": "" }, + { "name": "Date", "value": "Thu, 5 Dec 2024 08:30:00 -0800" }, + { "name": "From", "value": "Holiday Deals " }, + { "name": "To", "value": "exampleuser@gmail.com" }, + { "name": "Message-ID", "value": "<12345abc67890@promotion.example.com>" }, + { "name": "Subject", "value": "Exclusive Holiday Discounts!" }, + { "name": "MIME-Version", "value": "1.0" }, + { + "name": "Content-Type", + "value": "multipart/alternative; boundary=\"----=_Part_12345_67890.1733405400000\"" + } + ], + "body": { "size": 0 }, + "parts": [ + { + "partId": "0", + "mimeType": "text/plain", + "filename": "", + "headers": [ + { "name": "Content-Type", "value": "text/plain; charset=utf-8" }, + { "name": "Content-Transfer-Encoding", "value": "quoted-printable" } + ], + "body": { + "size": 1234, + "data": "VGhpcyBpcyBhbiBleGFtcGxlIG1lc3NhZ2UuIFRoYW5rIHlvdSBmb3Igc2hvcHBpbmcgd2l0aCB1cy4=" + } + }, + { + "partId": "1", + "mimeType": "text/html", + "filename": "", + "headers": [ + { "name": "Content-Type", "value": "text/html; charset=\"utf-8\"" }, + { "name": "Content-Transfer-Encoding", "value": "quoted-printable" } + ], + "body": { + "size": 5678, + "data": "PGRpdiBzdHlsZT0nZm9udC1mYW1pbHk6IEFyaWFsLCBzYW5zLXNlcmlmOyc+VGhpcyBpcyBhbiBleGFtcGxlIGh0bWwgbWVzc2FnZS4gPGI+VGhhbmsgeW91IGZvciBzaG9wcGluZyB3aXRoIHVzLjwvYj48L2Rpdj4=" + } + } + ] + }, + "sizeEstimate": 67890, + "historyId": "54321", + "internalDate": "1733405400000" + } + } + ] + }, + "connections": { + "When clicking ‘Test workflow’": { + "main": [ + [ + { + "node": "Gmail - Threads - All (simplified)", + "type": "main", + "index": 0 + }, + { + "node": "Gmail - Threads - All", + "type": "main", + "index": 0 + }, + { + "node": "Gmail - Threads - Add Label", + "type": "main", + "index": 0 + }, + { + "node": "Gmail - Threads - Delete", + "type": "main", + "index": 0 + }, + { + "node": "Gmail - Threads - Get (simplified)", + "type": "main", + "index": 0 + }, + { + "node": "Gmail - Threads - Get", + "type": "main", + "index": 0 + }, + { + "node": "Gmail - Threads - Trash", + "type": "main", + "index": 0 + }, + { + "node": "Gmail - Threads - Untrash", + "type": "main", + "index": 0 + }, + { + "node": "Gmail - Threads - Remove Label", + "type": "main", + "index": 0 + }, + { + "node": "Edit Fields1", + "type": "main", + "index": 0 + } + ] + ] + }, + "Edit Fields1": { + "main": [ + [ + { + "node": "Attachment1", + "type": "main", + "index": 0 + } + ] + ] + }, + "Attachment1": { + "main": [ + [ + { + "node": "Gmail - Threads - Reply", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": { + "executionOrder": "v1" + }, + "versionId": "84fb11a4-4166-45bd-bd9f-60fa378d9e68", + "meta": { + "instanceId": "27cc9b56542ad45b38725555722c50a1c3fee1670bbb67980558314ee08517c4" + }, + "id": "09KDcfGmfDrLInDE", + "tags": [] +} diff --git a/packages/nodes-base/nodes/Google/Gmail/v2/GmailV2.node.ts b/packages/nodes-base/nodes/Google/Gmail/v2/GmailV2.node.ts index 0be7c6507dda6..84cd8013b226c 100644 --- a/packages/nodes-base/nodes/Google/Gmail/v2/GmailV2.node.ts +++ b/packages/nodes-base/nodes/Google/Gmail/v2/GmailV2.node.ts @@ -1,9 +1,7 @@ import type { - IExecuteFunctions, IDataObject, - ILoadOptionsFunctions, + IExecuteFunctions, INodeExecutionData, - INodePropertyOptions, INodeType, INodeTypeBaseDescription, INodeTypeDescription, @@ -15,7 +13,16 @@ import { WAIT_INDEFINITELY, } from 'n8n-workflow'; +import { draftFields, draftOperations } from './DraftDescription'; +import { labelFields, labelOperations } from './LabelDescription'; +import { messageFields, messageOperations } from './MessageDescription'; +import { threadFields, threadOperations } from './ThreadDescription'; import type { IEmail } from '../../../../utils/sendAndWait/interfaces'; +import { + createEmail, + getSendAndWaitProperties, + sendAndWaitWebhook, +} from '../../../../utils/sendAndWait/utils'; import { encodeEmail, googleApiRequest, @@ -29,20 +36,7 @@ import { simplifyOutput, unescapeSnippets, } from '../GenericFunctions'; - -import { messageFields, messageOperations } from './MessageDescription'; - -import { labelFields, labelOperations } from './LabelDescription'; - -import { draftFields, draftOperations } from './DraftDescription'; - -import { threadFields, threadOperations } from './ThreadDescription'; - -import { - getSendAndWaitProperties, - createEmail, - sendAndWaitWebhook, -} from '../../../../utils/sendAndWait/utils'; +import { getGmailAliases, getLabels, getThreadMessages } from './loadOptions'; const versionDescription: INodeTypeDescription = { displayName: 'Gmail', @@ -187,78 +181,9 @@ export class GmailV2 implements INodeType { methods = { loadOptions: { - // Get all the labels to display them to user so that they can - // select them easily - async getLabels(this: ILoadOptionsFunctions): Promise { - const returnData: INodePropertyOptions[] = []; - - const labels = await googleApiRequestAllItems.call( - this, - 'labels', - 'GET', - '/gmail/v1/users/me/labels', - ); - - for (const label of labels) { - returnData.push({ - name: label.name, - value: label.id, - }); - } - - return returnData.sort((a, b) => { - if (a.name < b.name) { - return -1; - } - if (a.name > b.name) { - return 1; - } - return 0; - }); - }, - - async getThreadMessages(this: ILoadOptionsFunctions): Promise { - const returnData: INodePropertyOptions[] = []; - - const id = this.getNodeParameter('threadId', 0) as string; - const { messages } = await googleApiRequest.call( - this, - 'GET', - `/gmail/v1/users/me/threads/${id}`, - {}, - { format: 'minimal' }, - ); - - for (const message of messages || []) { - returnData.push({ - name: message.snippet, - value: message.id, - }); - } - - return returnData; - }, - - async getGmailAliases(this: ILoadOptionsFunctions): Promise { - const returnData: INodePropertyOptions[] = []; - const { sendAs } = await googleApiRequest.call( - this, - 'GET', - '/gmail/v1/users/me/settings/sendAs', - ); - - for (const alias of sendAs || []) { - const displayName = alias.isDefault - ? `${alias.sendAsEmail} (Default)` - : alias.sendAsEmail; - returnData.push({ - name: displayName, - value: alias.sendAsEmail, - }); - } - - return returnData; - }, + getLabels, + getThreadMessages, + getGmailAliases, }, }; diff --git a/packages/nodes-base/nodes/Google/Gmail/v2/loadOptions.ts b/packages/nodes-base/nodes/Google/Gmail/v2/loadOptions.ts new file mode 100644 index 0000000000000..6c5a7df9aa70c --- /dev/null +++ b/packages/nodes-base/nodes/Google/Gmail/v2/loadOptions.ts @@ -0,0 +1,75 @@ +import type { ILoadOptionsFunctions, INodePropertyOptions } from 'n8n-workflow'; + +import { googleApiRequest, googleApiRequestAllItems } from '../GenericFunctions'; + +/** + * Get all the labels to display them to user so that they can select them easily + */ +export async function getLabels(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + + const labels = await googleApiRequestAllItems.call( + this, + 'labels', + 'GET', + '/gmail/v1/users/me/labels', + ); + + for (const label of labels) { + returnData.push({ + name: label.name, + value: label.id, + }); + } + + return returnData.sort((a, b) => { + if (a.name < b.name) { + return -1; + } + if (a.name > b.name) { + return 1; + } + return 0; + }); +} + +export async function getThreadMessages( + this: ILoadOptionsFunctions, +): Promise { + const returnData: INodePropertyOptions[] = []; + + const id = this.getNodeParameter('threadId', 0) as string; + const { messages } = await googleApiRequest.call( + this, + 'GET', + `/gmail/v1/users/me/threads/${id}`, + {}, + { format: 'minimal' }, + ); + + for (const message of messages || []) { + returnData.push({ + name: message.snippet, + value: message.id, + }); + } + + return returnData; +} + +export async function getGmailAliases( + this: ILoadOptionsFunctions, +): Promise { + const returnData: INodePropertyOptions[] = []; + const { sendAs } = await googleApiRequest.call(this, 'GET', '/gmail/v1/users/me/settings/sendAs'); + + for (const alias of sendAs || []) { + const displayName = alias.isDefault ? `${alias.sendAsEmail} (Default)` : alias.sendAsEmail; + returnData.push({ + name: displayName, + value: alias.sendAsEmail, + }); + } + + return returnData; +} diff --git a/packages/nodes-base/test/nodes/FakeCredentialsMap.ts b/packages/nodes-base/test/nodes/FakeCredentialsMap.ts index 788dbcadb6b98..4daacbfd07012 100644 --- a/packages/nodes-base/test/nodes/FakeCredentialsMap.ts +++ b/packages/nodes-base/test/nodes/FakeCredentialsMap.ts @@ -125,6 +125,25 @@ BQIDAQAB accessToken: 'testToken', baseUrl: 'https://api.telegram.org', }, + gmailOAuth2: { + grantType: 'authorizationCode', + authUrl: 'https://accounts.google.com/o/oauth2/v2/auth', + accessTokenUrl: 'https://oauth2.googleapis.com/token', + clientId: 'CLIENTID', + clientSecret: 'CLIENTSECRET', + scope: + 'https://www.googleapis.com/auth/gmail.labels https://www.googleapis.com/auth/gmail.addons.current.action.compose https://www.googleapis.com/auth/gmail.addons.current.message.action https://mail.google.com/ https://www.googleapis.com/auth/gmail.modify https://www.googleapis.com/auth/gmail.compose', + authQueryParameters: 'access_type=offline&prompt=consent', + authentication: 'body', + oauthTokenData: { + access_token: 'ACCESSTOKEN', + refresh_token: 'REFRESHTOKEN', + scope: + 'https://www.googleapis.com/auth/gmail.labels https://www.googleapis.com/auth/gmail.addons.current.action.compose https://www.googleapis.com/auth/gmail.addons.current.message.action https://mail.google.com/ https://www.googleapis.com/auth/gmail.modify https://www.googleapis.com/auth/gmail.compose', + token_type: 'bearer', + expires_in: 86400, + }, + }, notionApi: { apiKey: 'key123', }, diff --git a/packages/nodes-base/test/nodes/Helpers.ts b/packages/nodes-base/test/nodes/Helpers.ts index 829f21e3037e6..a2ebee1370d5b 100644 --- a/packages/nodes-base/test/nodes/Helpers.ts +++ b/packages/nodes-base/test/nodes/Helpers.ts @@ -332,7 +332,7 @@ export const equalityTest = async (testData: WorkflowTestData, types: INodeTypes return expect(resultData, msg).toEqual(testData.output.nodeData[nodeName]); }); - expect(result.finished).toEqual(true); + expect(result.finished || result.status === 'waiting').toEqual(true); }; const preparePinData = (pinData: IDataObject) => { From bf8142b47495ba7e9d10653f51749d0d89924a2b Mon Sep 17 00:00:00 2001 From: Dana <152518854+dana-gill@users.noreply.github.com> Date: Wed, 18 Dec 2024 16:29:13 +0100 Subject: [PATCH 05/14] test(Set Node): Improve tests for Set node in raw mode (#12215) --- .../nodes-base/nodes/Set/test/v2/raw.test.ts | 145 ++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 packages/nodes-base/nodes/Set/test/v2/raw.test.ts diff --git a/packages/nodes-base/nodes/Set/test/v2/raw.test.ts b/packages/nodes-base/nodes/Set/test/v2/raw.test.ts new file mode 100644 index 0000000000000..5cebfbfcab143 --- /dev/null +++ b/packages/nodes-base/nodes/Set/test/v2/raw.test.ts @@ -0,0 +1,145 @@ +import { get } from 'lodash'; +import { constructExecutionMetaData } from 'n8n-core'; +import { + NodeOperationError, + type IDataObject, + type IExecuteFunctions, + type IGetNodeParameterOptions, + type INode, +} from 'n8n-workflow'; + +import { type SetNodeOptions } from '../../v2/helpers/interfaces'; +import * as utils from '../../v2/helpers/utils'; +import { execute } from '../../v2/raw.mode'; + +const node: INode = { + id: '11', + name: 'Set Node', + type: 'n8n-nodes-base.set', + typeVersion: 3, + position: [42, 42], + parameters: { + mode: 'manual', + fields: { + values: [], + }, + include: 'none', + options: {}, + }, +}; + +const createMockExecuteFunction = ( + nodeParameters: IDataObject, + continueOnFail: boolean = false, +) => { + const fakeExecuteFunction = { + getNodeParameter( + parameterName: string, + _itemIndex: number, + fallbackValue?: IDataObject | undefined, + options?: IGetNodeParameterOptions | undefined, + ) { + const parameter = options?.extractValue ? `${parameterName}.value` : parameterName; + return get(nodeParameters, parameter, fallbackValue); + }, + getNode() { + return node; + }, + helpers: { constructExecutionMetaData }, + continueOnFail: () => continueOnFail, + } as unknown as IExecuteFunctions; + return fakeExecuteFunction; +}; + +describe('test Set2, rawMode/json Mode', () => { + const item = { + json: { + input1: 'value1', + input2: 2, + input3: [1, 2, 3], + }, + pairedItem: { + item: 0, + input: undefined, + }, + }; + + const options: SetNodeOptions = { + include: 'none', + }; + + afterEach(() => jest.resetAllMocks()); + + describe('fixed mode', () => { + const jsonData = { jsonData: 1 }; + const fakeExecuteFunction = createMockExecuteFunction({ jsonOutput: jsonData }); + const rawData = { + num1: 55, + str1: '42', + arr1: ['foo', 'bar'], + obj: { + key: 'value', + }, + }; + + it('should parse json with the jsonOutput in node parameter and compose a return item', async () => { + jest.spyOn(utils, 'parseJsonParameter'); + jest.spyOn(utils, 'composeReturnItem'); + + const result = await execute.call(fakeExecuteFunction, item, 0, options, rawData, node); + + expect(result).toEqual({ json: jsonData, pairedItem: { item: 0 } }); + expect(utils.parseJsonParameter).toHaveBeenCalledWith(jsonData, node, 0); + expect(utils.composeReturnItem).toHaveBeenCalledWith(0, item, jsonData, options, 3); + }); + }); + + describe('expression mode', () => { + const jsonData = { my_field_1: 'value' }; + const jsonDataString = '{"my_field_1": "value"}'; + const fakeExecuteFunction = createMockExecuteFunction({ jsonOutput: jsonDataString }); + const rawData = { + num1: 55, + str1: '42', + arr1: ['foo', 'bar'], + obj: { + key: 'value', + }, + jsonOutput: jsonDataString, + }; + + it('should parse json with resolved expression data and compose a return item', async () => { + jest.spyOn(utils, 'parseJsonParameter'); + jest.spyOn(utils, 'composeReturnItem'); + jest.spyOn(utils, 'resolveRawData'); + + const result = await execute.call(fakeExecuteFunction, item, 0, options, rawData, node); + + expect(utils.parseJsonParameter).toHaveBeenCalledWith(jsonDataString, node, 0); + expect(utils.composeReturnItem).toHaveBeenCalledWith(0, item, jsonData, options, 3); + expect(utils.resolveRawData).toHaveBeenCalledWith(jsonDataString, 0); + expect(result).toEqual({ json: jsonData, pairedItem: { item: 0 } }); + }); + }); + + describe('error handling', () => { + it('should return an error object with pairedItem when continueOnFail is true', async () => { + const fakeExecuteFunction = createMockExecuteFunction({ jsonOutput: 'jsonData' }, true); + + const output = await execute.call(fakeExecuteFunction, item, 0, options, {}, node); + + expect(output).toEqual({ + json: { error: "The 'JSON Output' in item 0 contains invalid JSON" }, + pairedItem: { item: 0 }, + }); + }); + + it('should throw an error when continueOnFail is false', async () => { + const fakeExecuteFunction = createMockExecuteFunction({ jsonOutput: 'jsonData' }, false); + + await expect(execute.call(fakeExecuteFunction, item, 0, options, {}, node)).rejects.toThrow( + NodeOperationError, + ); + }); + }); +}); From 92af245d1aab5bfad8618fda69b2405f5206875d Mon Sep 17 00:00:00 2001 From: Dana <152518854+dana-gill@users.noreply.github.com> Date: Wed, 18 Dec 2024 16:59:12 +0100 Subject: [PATCH 06/14] fix(Extract from File Node): Detect file encoding (#12081) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ --- packages/core/package.json | 1 + packages/core/src/NodeExecuteFunctions.ts | 5 + .../node-execution-context/execute-context.ts | 2 + .../execute-single-context.ts | 2 + .../supply-data-context.ts | 2 + .../ExtractFromFile/ExtractFromFile.node.ts | 2 +- .../actions/moveTo.operation.ts | 3 +- .../test/ExtractFromFile.node.test.ts | 6 ++ .../test/workflow.non_utf8_encoding.json | 98 +++++++++++++++++++ packages/workflow/src/Interfaces.ts | 2 + pnpm-lock.yaml | 8 ++ 11 files changed, 129 insertions(+), 2 deletions(-) create mode 100644 packages/nodes-base/nodes/Files/ExtractFromFile/test/ExtractFromFile.node.test.ts create mode 100644 packages/nodes-base/nodes/Files/ExtractFromFile/test/workflow.non_utf8_encoding.json diff --git a/packages/core/package.json b/packages/core/package.json index 7fdd5f99af778..84cd8f7c8a596 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -42,6 +42,7 @@ "@sentry/node": "catalog:", "aws4": "1.11.0", "axios": "catalog:", + "chardet": "2.0.0", "concat-stream": "2.0.0", "cron": "3.1.7", "fast-glob": "catalog:", diff --git a/packages/core/src/NodeExecuteFunctions.ts b/packages/core/src/NodeExecuteFunctions.ts index 578752b3ef983..38679513de201 100644 --- a/packages/core/src/NodeExecuteFunctions.ts +++ b/packages/core/src/NodeExecuteFunctions.ts @@ -15,6 +15,7 @@ import type { import { ClientOAuth2 } from '@n8n/client-oauth2'; import type { AxiosError, AxiosHeaders, AxiosRequestConfig, AxiosResponse } from 'axios'; import axios from 'axios'; +import chardet from 'chardet'; import crypto, { createHmac } from 'crypto'; import FileType from 'file-type'; import FormData from 'form-data'; @@ -1050,6 +1051,10 @@ export async function getBinaryDataBuffer( return await Container.get(BinaryDataService).getAsBuffer(binaryData); } +export function detectBinaryEncoding(buffer: Buffer): string { + return chardet.detect(buffer) as string; +} + /** * Store an incoming IBinaryData & related buffer using the configured binary data manager. * diff --git a/packages/core/src/node-execution-context/execute-context.ts b/packages/core/src/node-execution-context/execute-context.ts index 954059d86d322..d563881bea698 100644 --- a/packages/core/src/node-execution-context/execute-context.ts +++ b/packages/core/src/node-execution-context/execute-context.ts @@ -37,6 +37,7 @@ import { getSSHTunnelFunctions, getFileSystemHelperFunctions, getCheckProcessedHelperFunctions, + detectBinaryEncoding, } from '@/NodeExecuteFunctions'; import { BaseExecuteContext } from './base-execute-context'; @@ -96,6 +97,7 @@ export class ExecuteContext extends BaseExecuteContext implements IExecuteFuncti assertBinaryData(inputData, node, itemIndex, propertyName, 0), getBinaryDataBuffer: async (itemIndex, propertyName) => await getBinaryDataBuffer(inputData, itemIndex, propertyName, 0), + detectBinaryEncoding: (buffer: Buffer) => detectBinaryEncoding(buffer), }; this.nodeHelpers = { diff --git a/packages/core/src/node-execution-context/execute-single-context.ts b/packages/core/src/node-execution-context/execute-single-context.ts index cb46ea9c91e33..af837a12c52ec 100644 --- a/packages/core/src/node-execution-context/execute-single-context.ts +++ b/packages/core/src/node-execution-context/execute-single-context.ts @@ -16,6 +16,7 @@ import { ApplicationError, createDeferredPromise, NodeConnectionType } from 'n8n // eslint-disable-next-line import/no-cycle import { assertBinaryData, + detectBinaryEncoding, getBinaryDataBuffer, getBinaryHelperFunctions, getRequestHelperFunctions, @@ -69,6 +70,7 @@ export class ExecuteSingleContext extends BaseExecuteContext implements IExecute assertBinaryData(inputData, node, itemIndex, propertyName, inputIndex), getBinaryDataBuffer: async (propertyName, inputIndex = 0) => await getBinaryDataBuffer(inputData, itemIndex, propertyName, inputIndex), + detectBinaryEncoding: (buffer) => detectBinaryEncoding(buffer), }; } diff --git a/packages/core/src/node-execution-context/supply-data-context.ts b/packages/core/src/node-execution-context/supply-data-context.ts index c3d7f4546845a..6d8679d75edd7 100644 --- a/packages/core/src/node-execution-context/supply-data-context.ts +++ b/packages/core/src/node-execution-context/supply-data-context.ts @@ -24,6 +24,7 @@ import { assertBinaryData, constructExecutionMetaData, copyInputItems, + detectBinaryEncoding, getBinaryDataBuffer, getBinaryHelperFunctions, getCheckProcessedHelperFunctions, @@ -87,6 +88,7 @@ export class SupplyDataContext extends BaseExecuteContext implements ISupplyData assertBinaryData(inputData, node, itemIndex, propertyName, 0), getBinaryDataBuffer: async (itemIndex, propertyName) => await getBinaryDataBuffer(inputData, itemIndex, propertyName, 0), + detectBinaryEncoding: (buffer: Buffer) => detectBinaryEncoding(buffer), returnJsonArray, normalizeItems, diff --git a/packages/nodes-base/nodes/Files/ExtractFromFile/ExtractFromFile.node.ts b/packages/nodes-base/nodes/Files/ExtractFromFile/ExtractFromFile.node.ts index 66f6ca2c9c632..c863d84818a28 100644 --- a/packages/nodes-base/nodes/Files/ExtractFromFile/ExtractFromFile.node.ts +++ b/packages/nodes-base/nodes/Files/ExtractFromFile/ExtractFromFile.node.ts @@ -6,9 +6,9 @@ import type { } from 'n8n-workflow'; import { NodeConnectionType } from 'n8n-workflow'; -import * as spreadsheet from './actions/spreadsheet.operation'; import * as moveTo from './actions/moveTo.operation'; import * as pdf from './actions/pdf.operation'; +import * as spreadsheet from './actions/spreadsheet.operation'; export class ExtractFromFile implements INodeType { // eslint-disable-next-line n8n-nodes-base/node-class-description-missing-subtitle diff --git a/packages/nodes-base/nodes/Files/ExtractFromFile/actions/moveTo.operation.ts b/packages/nodes-base/nodes/Files/ExtractFromFile/actions/moveTo.operation.ts index e4f36afc06c14..c8b6c115a6b49 100644 --- a/packages/nodes-base/nodes/Files/ExtractFromFile/actions/moveTo.operation.ts +++ b/packages/nodes-base/nodes/Files/ExtractFromFile/actions/moveTo.operation.ts @@ -121,8 +121,9 @@ export async function execute( if (!value) continue; - const encoding = (options.encoding as string) || 'utf8'; const buffer = await this.helpers.getBinaryDataBuffer(itemIndex, binaryPropertyName); + const encoding = + (options.encoding as string) || (this.helpers.detectBinaryEncoding(buffer) as string); if (options.keepSource && options.keepSource !== 'binary') { newItem.json = deepCopy(item.json); diff --git a/packages/nodes-base/nodes/Files/ExtractFromFile/test/ExtractFromFile.node.test.ts b/packages/nodes-base/nodes/Files/ExtractFromFile/test/ExtractFromFile.node.test.ts new file mode 100644 index 0000000000000..aebf28eb73a57 --- /dev/null +++ b/packages/nodes-base/nodes/Files/ExtractFromFile/test/ExtractFromFile.node.test.ts @@ -0,0 +1,6 @@ +import { getWorkflowFilenames, testWorkflows } from '@test/nodes/Helpers'; + +describe('ExtractFromFile', () => { + const workflows = getWorkflowFilenames(__dirname); + testWorkflows(workflows); +}); diff --git a/packages/nodes-base/nodes/Files/ExtractFromFile/test/workflow.non_utf8_encoding.json b/packages/nodes-base/nodes/Files/ExtractFromFile/test/workflow.non_utf8_encoding.json new file mode 100644 index 0000000000000..8462d93b936d2 --- /dev/null +++ b/packages/nodes-base/nodes/Files/ExtractFromFile/test/workflow.non_utf8_encoding.json @@ -0,0 +1,98 @@ +{ + "nodes": [ + { + "parameters": {}, + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [-200, -40], + "id": "64686c0a-64a4-4a33-9e70-038c9d23c25b", + "name": "When clicking ‘Test workflow’" + }, + { + "parameters": { + "operation": "text", + "binaryPropertyName": "myfile", + "options": {} + }, + "type": "n8n-nodes-base.extractFromFile", + "typeVersion": 1, + "position": [420, -40], + "id": "aaac18d3-1e99-4c47-9de8-2dc8bf95abd7", + "name": "Extract from File" + }, + { + "parameters": { + "assignments": { + "assignments": [ + { + "id": "39b5f05f-85c5-499a-86d6-591d6440f147", + "name": "text", + "value": "Karlovy Vary město lázní Příliš žluťoučký kůň úpěl ďábelské ódy.", + "type": "string" + } + ] + }, + "options": {} + }, + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [-20, -40], + "id": "e74c67f1-171a-42a7-be12-b6687935988f", + "name": "Edit Fields" + }, + { + "parameters": { + "operation": "toText", + "sourceProperty": "text", + "binaryPropertyName": "myfile", + "options": { + "encoding": "windows1256" + } + }, + "type": "n8n-nodes-base.convertToFile", + "typeVersion": 1.1, + "position": [180, -40], + "id": "77ddd6d4-1d75-4ad2-8301-e70213d8371e", + "name": "windows1256" + } + ], + "connections": { + "When clicking ‘Test workflow’": { + "main": [ + [ + { + "node": "Edit Fields", + "type": "main", + "index": 0 + } + ] + ] + }, + "Extract from File": { + "main": [[]] + }, + "Edit Fields": { + "main": [ + [ + { + "node": "windows1256", + "type": "main", + "index": 0 + } + ] + ] + }, + "windows1256": { + "main": [ + [ + { + "node": "Extract from File", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "pinData": {} +} diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index 116d392722ef2..c5fa0a938ad37 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -947,6 +947,7 @@ export type IExecuteFunctions = ExecuteFunctions.GetNodeParameterFn & ): NodeExecutionWithMetadata[]; assertBinaryData(itemIndex: number, propertyName: string): IBinaryData; getBinaryDataBuffer(itemIndex: number, propertyName: string): Promise; + detectBinaryEncoding(buffer: Buffer): string; copyInputItems(items: INodeExecutionData[], properties: string[]): IDataObject[]; }; @@ -973,6 +974,7 @@ export interface IExecuteSingleFunctions extends BaseExecutionFunctions { BinaryHelperFunctions & { assertBinaryData(propertyName: string, inputIndex?: number): IBinaryData; getBinaryDataBuffer(propertyName: string, inputIndex?: number): Promise; + detectBinaryEncoding(buffer: Buffer): string; }; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e21c919dee7f9..105345263cd74 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1130,6 +1130,9 @@ importers: axios: specifier: 'catalog:' version: 1.7.4 + chardet: + specifier: 2.0.0 + version: 2.0.0 concat-stream: specifier: 2.0.0 version: 2.0.0 @@ -6833,6 +6836,9 @@ packages: chardet@0.7.0: resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} + chardet@2.0.0: + resolution: {integrity: sha512-xVgPpulCooDjY6zH4m9YW3jbkaBe3FKIAvF5sj5t7aBNsVl2ljIE+xwJ4iNgiDZHFQvNIpjdKdVOQvvk5ZfxbQ==} + charenc@0.0.2: resolution: {integrity: sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==} @@ -19870,6 +19876,8 @@ snapshots: chardet@0.7.0: {} + chardet@2.0.0: {} + charenc@0.0.2: {} chart.js@4.4.0: From 0f1461f2d5d7ec34236ed7fcec3e2f9ee7eb73c4 Mon Sep 17 00:00:00 2001 From: Tomi Turtiainen <10324676+tomi@users.noreply.github.com> Date: Wed, 18 Dec 2024 18:45:05 +0200 Subject: [PATCH 07/14] fix(core): Fix binary data helpers (like `prepareBinaryData`) with task runner (#12259) --- packages/@n8n/task-runner/package.json | 2 + .../__tests__/js-task-runner.test.ts | 122 +++++++++++++++- .../errors/unsupported-function.error.ts | 13 ++ .../src/js-task-runner/js-task-runner.ts | 38 ++++- .../@n8n/task-runner/src/message-types.ts | 6 +- packages/@n8n/task-runner/src/runner-types.ts | 78 +++++++--- packages/@n8n/task-runner/src/task-runner.ts | 29 +--- .../__tests__/task-manager.test.ts | 136 ++++++++++++++++++ .../src/runners/task-managers/task-manager.ts | 14 +- packages/core/src/SerializedBuffer.ts | 24 ++++ packages/core/src/index.ts | 1 + packages/core/test/SerializedBuffer.test.ts | 55 +++++++ pnpm-lock.yaml | 118 ++++----------- 13 files changed, 494 insertions(+), 142 deletions(-) create mode 100644 packages/@n8n/task-runner/src/js-task-runner/errors/unsupported-function.error.ts create mode 100644 packages/cli/src/runners/task-managers/__tests__/task-manager.test.ts create mode 100644 packages/core/src/SerializedBuffer.ts create mode 100644 packages/core/test/SerializedBuffer.test.ts diff --git a/packages/@n8n/task-runner/package.json b/packages/@n8n/task-runner/package.json index 4375aa413bd99..3a5ffc2cf128c 100644 --- a/packages/@n8n/task-runner/package.json +++ b/packages/@n8n/task-runner/package.json @@ -38,6 +38,7 @@ "@sentry/node": "catalog:", "acorn": "8.14.0", "acorn-walk": "8.3.4", + "lodash.set": "4.3.2", "n8n-core": "workspace:*", "n8n-workflow": "workspace:*", "nanoid": "catalog:", @@ -45,6 +46,7 @@ "ws": "^8.18.0" }, "devDependencies": { + "@types/lodash.set": "4.3.9", "luxon": "catalog:" } } diff --git a/packages/@n8n/task-runner/src/js-task-runner/__tests__/js-task-runner.test.ts b/packages/@n8n/task-runner/src/js-task-runner/__tests__/js-task-runner.test.ts index e5df5b64a327b..dbb94038946fd 100644 --- a/packages/@n8n/task-runner/src/js-task-runner/__tests__/js-task-runner.test.ts +++ b/packages/@n8n/task-runner/src/js-task-runner/__tests__/js-task-runner.test.ts @@ -1,5 +1,6 @@ import { mock } from 'jest-mock-extended'; import { DateTime } from 'luxon'; +import type { IBinaryData } from 'n8n-workflow'; import { setGlobalState, type CodeExecutionMode, type IDataObject } from 'n8n-workflow'; import fs from 'node:fs'; import { builtinModules } from 'node:module'; @@ -8,10 +9,15 @@ import type { BaseRunnerConfig } from '@/config/base-runner-config'; import type { JsRunnerConfig } from '@/config/js-runner-config'; import { MainConfig } from '@/config/main-config'; import { ExecutionError } from '@/js-task-runner/errors/execution-error'; +import { UnsupportedFunctionError } from '@/js-task-runner/errors/unsupported-function.error'; import { ValidationError } from '@/js-task-runner/errors/validation-error'; import type { JSExecSettings } from '@/js-task-runner/js-task-runner'; import { JsTaskRunner } from '@/js-task-runner/js-task-runner'; -import type { DataRequestResponse, InputDataChunkDefinition } from '@/runner-types'; +import { + UNSUPPORTED_HELPER_FUNCTIONS, + type DataRequestResponse, + type InputDataChunkDefinition, +} from '@/runner-types'; import type { Task } from '@/task-runner'; import { @@ -567,6 +573,120 @@ describe('JsTaskRunner', () => { ); }); + describe('helpers', () => { + const binaryDataFile: IBinaryData = { + data: 'data', + fileName: 'file.txt', + mimeType: 'text/plain', + }; + + const groups = [ + { + method: 'helpers.assertBinaryData', + invocation: "helpers.assertBinaryData(0, 'binaryFile')", + expectedParams: [0, 'binaryFile'], + }, + { + method: 'helpers.getBinaryDataBuffer', + invocation: "helpers.getBinaryDataBuffer(0, 'binaryFile')", + expectedParams: [0, 'binaryFile'], + }, + { + method: 'helpers.prepareBinaryData', + invocation: "helpers.prepareBinaryData(Buffer.from('123'), 'file.txt', 'text/plain')", + expectedParams: [Buffer.from('123'), 'file.txt', 'text/plain'], + }, + { + method: 'helpers.setBinaryDataBuffer', + invocation: + "helpers.setBinaryDataBuffer({ data: '123', mimeType: 'text/plain' }, Buffer.from('321'))", + expectedParams: [{ data: '123', mimeType: 'text/plain' }, Buffer.from('321')], + }, + { + method: 'helpers.binaryToString', + invocation: "helpers.binaryToString(Buffer.from('123'), 'utf8')", + expectedParams: [Buffer.from('123'), 'utf8'], + }, + { + method: 'helpers.httpRequest', + invocation: "helpers.httpRequest({ method: 'GET', url: 'http://localhost' })", + expectedParams: [{ method: 'GET', url: 'http://localhost' }], + }, + ]; + + for (const group of groups) { + it(`${group.method} for runOnceForAllItems`, async () => { + // Arrange + const rpcCallSpy = jest + .spyOn(defaultTaskRunner, 'makeRpcCall') + .mockResolvedValue(undefined); + + // Act + await execTaskWithParams({ + task: newTaskWithSettings({ + code: `await ${group.invocation}; return []`, + nodeMode: 'runOnceForAllItems', + }), + taskData: newDataRequestResponse( + [{ json: {}, binary: { binaryFile: binaryDataFile } }], + {}, + ), + }); + + expect(rpcCallSpy).toHaveBeenCalledWith('1', group.method, group.expectedParams); + }); + + it(`${group.method} for runOnceForEachItem`, async () => { + // Arrange + const rpcCallSpy = jest + .spyOn(defaultTaskRunner, 'makeRpcCall') + .mockResolvedValue(undefined); + + // Act + await execTaskWithParams({ + task: newTaskWithSettings({ + code: `await ${group.invocation}; return {}`, + nodeMode: 'runOnceForEachItem', + }), + taskData: newDataRequestResponse( + [{ json: {}, binary: { binaryFile: binaryDataFile } }], + {}, + ), + }); + + expect(rpcCallSpy).toHaveBeenCalledWith('1', group.method, group.expectedParams); + }); + } + + describe('unsupported methods', () => { + for (const unsupportedFunction of UNSUPPORTED_HELPER_FUNCTIONS) { + it(`should throw an error if ${unsupportedFunction} is used in runOnceForAllItems`, async () => { + // Act + + await expect( + async () => + await executeForAllItems({ + code: `${unsupportedFunction}()`, + inputItems, + }), + ).rejects.toThrow(UnsupportedFunctionError); + }); + + it(`should throw an error if ${unsupportedFunction} is used in runOnceForEachItem`, async () => { + // Act + + await expect( + async () => + await executeForEachItem({ + code: `${unsupportedFunction}()`, + inputItems, + }), + ).rejects.toThrow(UnsupportedFunctionError); + }); + } + }); + }); + it('should allow access to Node.js Buffers', async () => { const outcomeAll = await execTaskWithParams({ task: newTaskWithSettings({ diff --git a/packages/@n8n/task-runner/src/js-task-runner/errors/unsupported-function.error.ts b/packages/@n8n/task-runner/src/js-task-runner/errors/unsupported-function.error.ts new file mode 100644 index 0000000000000..ad55ee0bbfa26 --- /dev/null +++ b/packages/@n8n/task-runner/src/js-task-runner/errors/unsupported-function.error.ts @@ -0,0 +1,13 @@ +import { ApplicationError } from 'n8n-workflow'; + +/** + * Error that indicates that a specific function is not available in the + * Code Node. + */ +export class UnsupportedFunctionError extends ApplicationError { + constructor(functionName: string) { + super(`The function "${functionName}" is not supported in the Code Node`, { + level: 'info', + }); + } +} diff --git a/packages/@n8n/task-runner/src/js-task-runner/js-task-runner.ts b/packages/@n8n/task-runner/src/js-task-runner/js-task-runner.ts index 89931ce67fd39..0b3c0a647671f 100644 --- a/packages/@n8n/task-runner/src/js-task-runner/js-task-runner.ts +++ b/packages/@n8n/task-runner/src/js-task-runner/js-task-runner.ts @@ -1,3 +1,4 @@ +import set from 'lodash.set'; import { getAdditionalKeys } from 'n8n-core'; import { WorkflowDataProxy, Workflow, ObservableObject } from 'n8n-workflow'; import type { @@ -19,11 +20,14 @@ import * as a from 'node:assert'; import { runInNewContext, type Context } from 'node:vm'; import type { MainConfig } from '@/config/main-config'; -import type { - DataRequestResponse, - InputDataChunkDefinition, - PartialAdditionalData, - TaskResultData, +import { UnsupportedFunctionError } from '@/js-task-runner/errors/unsupported-function.error'; +import { + EXPOSED_RPC_METHODS, + UNSUPPORTED_HELPER_FUNCTIONS, + type DataRequestResponse, + type InputDataChunkDefinition, + type PartialAdditionalData, + type TaskResultData, } from '@/runner-types'; import { type Task, TaskRunner } from '@/task-runner'; @@ -38,6 +42,10 @@ import { createRequireResolver } from './require-resolver'; import { validateRunForAllItemsOutput, validateRunForEachItemOutput } from './result-validation'; import { DataRequestResponseReconstruct } from '../data-request/data-request-response-reconstruct'; +export interface RPCCallObject { + [name: string]: ((...args: unknown[]) => Promise) | RPCCallObject; +} + export interface JSExecSettings { code: string; nodeMode: CodeExecutionMode; @@ -439,4 +447,24 @@ export class JsTaskRunner extends TaskRunner { this.nodeTypes.addNodeTypeDescriptions(nodeTypes); } } + + private buildRpcCallObject(taskId: string) { + const rpcObject: RPCCallObject = {}; + + for (const rpcMethod of EXPOSED_RPC_METHODS) { + set( + rpcObject, + rpcMethod.split('.'), + async (...args: unknown[]) => await this.makeRpcCall(taskId, rpcMethod, args), + ); + } + + for (const rpcMethod of UNSUPPORTED_HELPER_FUNCTIONS) { + set(rpcObject, rpcMethod.split('.'), () => { + throw new UnsupportedFunctionError(rpcMethod); + }); + } + + return rpcObject; + } } diff --git a/packages/@n8n/task-runner/src/message-types.ts b/packages/@n8n/task-runner/src/message-types.ts index 71f236b52a3df..40f7aeca77a33 100644 --- a/packages/@n8n/task-runner/src/message-types.ts +++ b/packages/@n8n/task-runner/src/message-types.ts @@ -2,7 +2,7 @@ import type { INodeTypeBaseDescription } from 'n8n-workflow'; import type { NeededNodeType, - RPC_ALLOW_LIST, + AVAILABLE_RPC_METHODS, TaskDataRequestParams, TaskResultData, } from './runner-types'; @@ -105,7 +105,7 @@ export namespace BrokerMessage { type: 'broker:rpc'; callId: string; taskId: string; - name: (typeof RPC_ALLOW_LIST)[number]; + name: (typeof AVAILABLE_RPC_METHODS)[number]; params: unknown[]; } @@ -239,7 +239,7 @@ export namespace RunnerMessage { type: 'runner:rpc'; callId: string; taskId: string; - name: (typeof RPC_ALLOW_LIST)[number]; + name: (typeof AVAILABLE_RPC_METHODS)[number]; params: unknown[]; } diff --git a/packages/@n8n/task-runner/src/runner-types.ts b/packages/@n8n/task-runner/src/runner-types.ts index 5075b19db2b47..e4e76189e2ae3 100644 --- a/packages/@n8n/task-runner/src/runner-types.ts +++ b/packages/@n8n/task-runner/src/runner-types.ts @@ -100,31 +100,73 @@ export interface PartialAdditionalData { variables: IDataObject; } -export const RPC_ALLOW_LIST = [ - 'helpers.httpRequestWithAuthentication', - 'helpers.requestWithAuthenticationPaginated', - // "helpers.normalizeItems" - // "helpers.constructExecutionMetaData" - // "helpers.assertBinaryData" +/** RPC methods that are exposed directly to the Code Node */ +export const EXPOSED_RPC_METHODS = [ + // assertBinaryData(itemIndex: number, propertyName: string): Promise + 'helpers.assertBinaryData', + + // getBinaryDataBuffer(itemIndex: number, propertyName: string): Promise 'helpers.getBinaryDataBuffer', - // "helpers.copyInputItems" - // "helpers.returnJsonArray" - 'helpers.getSSHClient', - 'helpers.createReadStream', - // "helpers.getStoragePath" - 'helpers.writeContentToFile', + + // prepareBinaryData(binaryData: Buffer, fileName?: string, mimeType?: string): Promise 'helpers.prepareBinaryData', + + // setBinaryDataBuffer(metadata: IBinaryData, buffer: Buffer): Promise 'helpers.setBinaryDataBuffer', + + // binaryToString(body: Buffer, encoding?: string): string + 'helpers.binaryToString', + + // httpRequest(opts: IHttpRequestOptions): Promise + 'helpers.httpRequest', +]; + +/** Helpers that exist but that we are not exposing to the Code Node */ +export const UNSUPPORTED_HELPER_FUNCTIONS = [ + // These rely on checking the credentials from the current node type (Code Node) + // and hence they can't even work (Code Node doesn't have credentials) + 'helpers.httpRequestWithAuthentication', + 'helpers.requestWithAuthenticationPaginated', + + // This has been removed 'helpers.copyBinaryFile', - 'helpers.binaryToBuffer', - // "helpers.binaryToString" - // "helpers.getBinaryPath" + + // We can't support streams over RPC without implementing it ourselves + 'helpers.createReadStream', 'helpers.getBinaryStream', + + // Makes no sense to support this, as it returns either a stream or a buffer + // and we can't support streams over RPC + 'helpers.binaryToBuffer', + + // These are pretty low-level, so we shouldn't expose them + // (require binary data id, which we don't expose) 'helpers.getBinaryMetadata', + 'helpers.getStoragePath', + 'helpers.getBinaryPath', + + // We shouldn't allow arbitrary FS writes + 'helpers.writeContentToFile', + + // Not something we need to expose. Can be done in the node itself + // copyInputItems(items: INodeExecutionData[], properties: string[]): IDataObject[] + 'helpers.copyInputItems', + + // Code Node does these automatically already + 'helpers.returnJsonArray', + 'helpers.normalizeItems', + + // The client is instantiated and lives on the n8n instance, so we can't + // expose it over RPC without implementing object marshalling + 'helpers.getSSHClient', + + // Doesn't make sense to expose 'helpers.createDeferredPromise', - 'helpers.httpRequest', - 'logNodeOutput', -] as const; + 'helpers.constructExecutionMetaData', +]; + +/** List of all RPC methods that task runner supports */ +export const AVAILABLE_RPC_METHODS = [...EXPOSED_RPC_METHODS, 'logNodeOutput'] as const; /** Node types needed for the runner to execute a task. */ export type NeededNodeType = { name: string; version: number }; diff --git a/packages/@n8n/task-runner/src/task-runner.ts b/packages/@n8n/task-runner/src/task-runner.ts index e8ee605ef5881..4254aad99c29f 100644 --- a/packages/@n8n/task-runner/src/task-runner.ts +++ b/packages/@n8n/task-runner/src/task-runner.ts @@ -1,3 +1,4 @@ +import { isSerializedBuffer, toBuffer } from 'n8n-core'; import { ApplicationError, ensureError, randomInt } from 'n8n-workflow'; import { nanoid } from 'nanoid'; import { EventEmitter } from 'node:events'; @@ -6,7 +7,7 @@ import { type MessageEvent, WebSocket } from 'ws'; import type { BaseRunnerConfig } from '@/config/base-runner-config'; import type { BrokerMessage, RunnerMessage } from '@/message-types'; import { TaskRunnerNodeTypes } from '@/node-types'; -import { RPC_ALLOW_LIST, type TaskResultData } from '@/runner-types'; +import type { TaskResultData } from '@/runner-types'; import { TaskCancelledError } from './js-task-runner/errors/task-cancelled-error'; @@ -42,10 +43,6 @@ interface RPCCall { reject: (error: unknown) => void; } -export interface RPCCallObject { - [name: string]: ((...args: unknown[]) => Promise) | RPCCallObject; -} - const OFFER_VALID_TIME_MS = 5000; const OFFER_VALID_EXTRA_MS = 100; @@ -464,7 +461,9 @@ export abstract class TaskRunner extends EventEmitter { }); try { - return await dataPromise; + const returnValue = await dataPromise; + + return isSerializedBuffer(returnValue) ? toBuffer(returnValue) : returnValue; } finally { this.rpcCalls.delete(callId); } @@ -486,24 +485,6 @@ export abstract class TaskRunner extends EventEmitter { } } - buildRpcCallObject(taskId: string) { - const rpcObject: RPCCallObject = {}; - for (const r of RPC_ALLOW_LIST) { - const splitPath = r.split('.'); - let obj = rpcObject; - - splitPath.forEach((s, index) => { - if (index !== splitPath.length - 1) { - obj[s] = {}; - obj = obj[s]; - return; - } - obj[s] = async (...args: unknown[]) => await this.makeRpcCall(taskId, r, args); - }); - } - return rpcObject; - } - /** Close the connection gracefully and wait until has been closed */ async stop() { this.clearIdleTimer(); diff --git a/packages/cli/src/runners/task-managers/__tests__/task-manager.test.ts b/packages/cli/src/runners/task-managers/__tests__/task-manager.test.ts new file mode 100644 index 0000000000000..84584e05df27b --- /dev/null +++ b/packages/cli/src/runners/task-managers/__tests__/task-manager.test.ts @@ -0,0 +1,136 @@ +import { mock } from 'jest-mock-extended'; +import { get, set } from 'lodash'; + +import type { NodeTypes } from '@/node-types'; +import type { Task } from '@/runners/task-managers/task-manager'; +import { TaskManager } from '@/runners/task-managers/task-manager'; + +class TestTaskManager extends TaskManager { + sentMessages: unknown[] = []; + + sendMessage(message: unknown) { + this.sentMessages.push(message); + } +} + +describe('TaskManager', () => { + let instance: TestTaskManager; + const mockNodeTypes = mock(); + + beforeEach(() => { + instance = new TestTaskManager(mockNodeTypes); + }); + + describe('handleRpc', () => { + test.each([ + ['logNodeOutput', ['hello world']], + ['helpers.assertBinaryData', [0, 'propertyName']], + ['helpers.getBinaryDataBuffer', [0, 'propertyName']], + ['helpers.prepareBinaryData', [Buffer.from('data').toJSON(), 'filename', 'mimetype']], + ['helpers.setBinaryDataBuffer', [{ data: '123' }, Buffer.from('data').toJSON()]], + ['helpers.binaryToString', [Buffer.from('data').toJSON(), 'utf8']], + ['helpers.httpRequest', [{ url: 'http://localhost' }]], + ])('should handle %s rpc call', async (methodName, args) => { + const executeFunctions = set({}, methodName.split('.'), jest.fn()); + + const mockTask = mock({ + taskId: 'taskId', + data: { + executeFunctions, + }, + }); + instance.tasks.set('taskId', mockTask); + + await instance.handleRpc('taskId', 'callId', methodName, args); + + expect(instance.sentMessages).toEqual([ + { + callId: 'callId', + data: undefined, + status: 'success', + taskId: 'taskId', + type: 'requester:rpcresponse', + }, + ]); + expect(get(executeFunctions, methodName.split('.'))).toHaveBeenCalledWith(...args); + }); + + it('converts any serialized buffer arguments into buffers', async () => { + const mockPrepareBinaryData = jest.fn().mockResolvedValue(undefined); + const mockTask = mock({ + taskId: 'taskId', + data: { + executeFunctions: { + helpers: { + prepareBinaryData: mockPrepareBinaryData, + }, + }, + }, + }); + instance.tasks.set('taskId', mockTask); + + await instance.handleRpc('taskId', 'callId', 'helpers.prepareBinaryData', [ + Buffer.from('data').toJSON(), + 'filename', + 'mimetype', + ]); + + expect(mockPrepareBinaryData).toHaveBeenCalledWith( + Buffer.from('data'), + 'filename', + 'mimetype', + ); + }); + + describe('errors', () => { + it('sends method not allowed error if method is not in the allow list', async () => { + const mockTask = mock({ + taskId: 'taskId', + data: { + executeFunctions: {}, + }, + }); + instance.tasks.set('taskId', mockTask); + + await instance.handleRpc('taskId', 'callId', 'notAllowedMethod', []); + + expect(instance.sentMessages).toEqual([ + { + callId: 'callId', + data: 'Method not allowed', + status: 'error', + taskId: 'taskId', + type: 'requester:rpcresponse', + }, + ]); + }); + + it('sends error if method throws', async () => { + const error = new Error('Test error'); + const mockTask = mock({ + taskId: 'taskId', + data: { + executeFunctions: { + helpers: { + assertBinaryData: jest.fn().mockRejectedValue(error), + }, + }, + }, + }); + instance.tasks.set('taskId', mockTask); + + await instance.handleRpc('taskId', 'callId', 'helpers.assertBinaryData', []); + + expect(instance.sentMessages).toEqual([ + { + callId: 'callId', + data: error, + status: 'error', + taskId: 'taskId', + type: 'requester:rpcresponse', + }, + ]); + }); + }); + }); +}); diff --git a/packages/cli/src/runners/task-managers/task-manager.ts b/packages/cli/src/runners/task-managers/task-manager.ts index fd62dc2673c4b..44193f9377a17 100644 --- a/packages/cli/src/runners/task-managers/task-manager.ts +++ b/packages/cli/src/runners/task-managers/task-manager.ts @@ -1,5 +1,6 @@ import type { TaskResultData, RequesterMessage, BrokerMessage, TaskData } from '@n8n/task-runner'; -import { RPC_ALLOW_LIST } from '@n8n/task-runner'; +import { AVAILABLE_RPC_METHODS } from '@n8n/task-runner'; +import { isSerializedBuffer, toBuffer } from 'n8n-core'; import { createResultOk, createResultError } from 'n8n-workflow'; import type { EnvProviderState, @@ -288,7 +289,7 @@ export abstract class TaskManager { } try { - if (!RPC_ALLOW_LIST.includes(name)) { + if (!AVAILABLE_RPC_METHODS.includes(name)) { this.sendMessage({ type: 'requester:rpcresponse', taskId, @@ -322,6 +323,15 @@ export abstract class TaskManager { }); return; } + + // Convert any serialized buffers back to buffers + for (let i = 0; i < params.length; i++) { + const paramValue = params[i]; + if (isSerializedBuffer(paramValue)) { + params[i] = toBuffer(paramValue); + } + } + const data = (await func.call(funcs, ...params)) as unknown; this.sendMessage({ diff --git a/packages/core/src/SerializedBuffer.ts b/packages/core/src/SerializedBuffer.ts new file mode 100644 index 0000000000000..48395049b9f02 --- /dev/null +++ b/packages/core/src/SerializedBuffer.ts @@ -0,0 +1,24 @@ +/** A nodejs Buffer gone through JSON.stringify */ +export type SerializedBuffer = { + type: 'Buffer'; + data: number[]; // Array like Uint8Array, each item is uint8 (0-255) +}; + +/** Converts the given SerializedBuffer to nodejs Buffer */ +export function toBuffer(serializedBuffer: SerializedBuffer): Buffer { + return Buffer.from(serializedBuffer.data); +} + +function isObjectLiteral(item: unknown): item is { [key: string]: unknown } { + return typeof item === 'object' && item !== null && !Array.isArray(item); +} + +export function isSerializedBuffer(candidate: unknown): candidate is SerializedBuffer { + return ( + isObjectLiteral(candidate) && + 'type' in candidate && + 'data' in candidate && + candidate.type === 'Buffer' && + Array.isArray(candidate.data) + ); +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index f2f2149b604a5..1fc9d77399159 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -24,3 +24,4 @@ export * from './ExecutionMetadata'; export * from './node-execution-context'; export * from './PartialExecutionUtils'; export { ErrorReporter } from './error-reporter'; +export * from './SerializedBuffer'; diff --git a/packages/core/test/SerializedBuffer.test.ts b/packages/core/test/SerializedBuffer.test.ts new file mode 100644 index 0000000000000..42437296299f0 --- /dev/null +++ b/packages/core/test/SerializedBuffer.test.ts @@ -0,0 +1,55 @@ +import type { SerializedBuffer } from '@/SerializedBuffer'; +import { toBuffer, isSerializedBuffer } from '@/SerializedBuffer'; + +// Mock data for tests +const validSerializedBuffer: SerializedBuffer = { + type: 'Buffer', + data: [65, 66, 67], // Corresponds to 'ABC' in ASCII +}; + +describe('serializedBufferToBuffer', () => { + it('should convert a SerializedBuffer to a Buffer', () => { + const buffer = toBuffer(validSerializedBuffer); + expect(buffer).toBeInstanceOf(Buffer); + expect(buffer.toString()).toBe('ABC'); + }); + + it('should serialize stringified buffer to the same buffer', () => { + const serializedBuffer = JSON.stringify(Buffer.from('n8n on the rocks')); + const buffer = toBuffer(JSON.parse(serializedBuffer)); + expect(buffer).toBeInstanceOf(Buffer); + expect(buffer.toString()).toBe('n8n on the rocks'); + }); +}); + +describe('isSerializedBuffer', () => { + it('should return true for a valid SerializedBuffer', () => { + expect(isSerializedBuffer(validSerializedBuffer)).toBe(true); + }); + + test.each([ + [{ data: [1, 2, 3] }], + [{ data: [1, 2, 256] }], + [{ type: 'Buffer', data: 'notAnArray' }], + [{ data: 42 }], + [{ data: 'test' }], + [{ data: true }], + [null], + [undefined], + [42], + [{}], + ])('should return false for %s', (value) => { + expect(isSerializedBuffer(value)).toBe(false); + }); +}); + +describe('Integration: serializedBufferToBuffer and isSerializedBuffer', () => { + it('should correctly validate and convert a SerializedBuffer', () => { + if (isSerializedBuffer(validSerializedBuffer)) { + const buffer = toBuffer(validSerializedBuffer); + expect(buffer.toString()).toBe('ABC'); + } else { + fail('Expected validSerializedBuffer to be a SerializedBuffer'); + } + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 105345263cd74..164e9af759896 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -672,6 +672,9 @@ importers: acorn-walk: specifier: 8.3.4 version: 8.3.4 + lodash.set: + specifier: 4.3.2 + version: 4.3.2 n8n-core: specifier: workspace:* version: link:../../core @@ -688,6 +691,9 @@ importers: specifier: '>=8.17.1' version: 8.17.1 devDependencies: + '@types/lodash.set': + specifier: 4.3.9 + version: 4.3.9 luxon: specifier: 'catalog:' version: 3.4.4 @@ -1114,7 +1120,7 @@ importers: dependencies: '@langchain/core': specifier: 'catalog:' - version: 0.3.19(openai@4.73.1(zod@3.23.8)) + version: 0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)) '@n8n/client-oauth2': specifier: workspace:* version: link:../@n8n/client-oauth2 @@ -1966,7 +1972,7 @@ importers: devDependencies: '@langchain/core': specifier: 'catalog:' - version: 0.3.19(openai@4.73.1) + version: 0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)) '@types/deep-equal': specifier: ^1.0.1 version: 1.0.1 @@ -5621,6 +5627,9 @@ packages: '@types/lodash-es@4.17.6': resolution: {integrity: sha512-R+zTeVUKDdfoRxpAryaQNRKk3105Rrgx2CFRClIgRGaqDTdjsm8h6IYA8ir584W3ePzkZfst5xIgDwYrlh9HLg==} + '@types/lodash.set@4.3.9': + resolution: {integrity: sha512-KOxyNkZpbaggVmqbpr82N2tDVTx05/3/j0f50Es1prxrWB0XYf9p3QNxqcbWb7P1Q9wlvsUSlCFnwlPCIJ46PQ==} + '@types/lodash@4.14.195': resolution: {integrity: sha512-Hwx9EUgdwf2GLarOjQp5ZH8ZmblzcbTBC2wtQWNKARBSxM9ezRIAUpeDTgoQRAFB0+8CNWXVA9+MaSOzOF3nPg==} @@ -9736,6 +9745,9 @@ packages: lodash.orderby@4.6.0: resolution: {integrity: sha512-T0rZxKmghOOf5YPnn8EY5iLYeWCpZq8G41FfqoVHH5QDTAFaghJRmAdLiadEDq+ztgM2q5PjA+Z1fOwGrLgmtg==} + lodash.set@4.3.2: + resolution: {integrity: sha512-4hNPN5jlm/N/HLMCO43v8BXKq9Z7QdAGc/VGrRD61w8gN9g/6jF9A4L1pbUgBLCffi0w9VsXfTOij5x8iTyFvg==} + lodash.throttle@4.1.1: resolution: {integrity: sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==} @@ -16206,38 +16218,6 @@ snapshots: transitivePeerDependencies: - openai - '@langchain/core@0.3.19(openai@4.73.1(zod@3.23.8))': - dependencies: - ansi-styles: 5.2.0 - camelcase: 6.3.0 - decamelize: 1.2.0 - js-tiktoken: 1.0.12 - langsmith: 0.2.3(openai@4.73.1(zod@3.23.8)) - mustache: 4.2.0 - p-queue: 6.6.2 - p-retry: 4.6.2 - uuid: 10.0.0 - zod: 3.23.8 - zod-to-json-schema: 3.23.3(zod@3.23.8) - transitivePeerDependencies: - - openai - - '@langchain/core@0.3.19(openai@4.73.1)': - dependencies: - ansi-styles: 5.2.0 - camelcase: 6.3.0 - decamelize: 1.2.0 - js-tiktoken: 1.0.12 - langsmith: 0.2.3(openai@4.73.1) - mustache: 4.2.0 - p-queue: 6.6.2 - p-retry: 4.6.2 - uuid: 10.0.0 - zod: 3.23.8 - zod-to-json-schema: 3.23.3(zod@3.23.8) - transitivePeerDependencies: - - openai - '@langchain/google-common@0.1.3(@langchain/core@0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)))(zod@3.23.8)': dependencies: '@langchain/core': 0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)) @@ -18361,6 +18341,10 @@ snapshots: dependencies: '@types/lodash': 4.14.195 + '@types/lodash.set@4.3.9': + dependencies: + '@types/lodash': 4.14.195 + '@types/lodash@4.14.195': {} '@types/long@4.0.2': {} @@ -19466,14 +19450,6 @@ snapshots: transitivePeerDependencies: - debug - axios@1.7.7: - dependencies: - follow-redirects: 1.15.6(debug@4.3.6) - form-data: 4.0.0 - proxy-from-env: 1.1.0 - transitivePeerDependencies: - - debug - axios@1.7.7(debug@4.3.6): dependencies: follow-redirects: 1.15.6(debug@4.3.6) @@ -21187,7 +21163,7 @@ snapshots: eslint-import-resolver-node@0.3.9: dependencies: - debug: 3.2.7(supports-color@5.5.0) + debug: 3.2.7(supports-color@8.1.1) is-core-module: 2.13.1 resolve: 1.22.8 transitivePeerDependencies: @@ -21212,7 +21188,7 @@ snapshots: eslint-module-utils@2.8.0(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.7.2))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0): dependencies: - debug: 3.2.7(supports-color@5.5.0) + debug: 3.2.7(supports-color@8.1.1) optionalDependencies: '@typescript-eslint/parser': 7.2.0(eslint@8.57.0)(typescript@5.7.2) eslint: 8.57.0 @@ -21232,7 +21208,7 @@ snapshots: array.prototype.findlastindex: 1.2.3 array.prototype.flat: 1.3.2 array.prototype.flatmap: 1.3.2 - debug: 3.2.7(supports-color@5.5.0) + debug: 3.2.7(supports-color@8.1.1) doctrine: 2.1.0 eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 @@ -22011,7 +21987,7 @@ snapshots: array-parallel: 0.1.3 array-series: 0.1.5 cross-spawn: 4.0.2 - debug: 3.2.7(supports-color@5.5.0) + debug: 3.2.7(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -22392,7 +22368,7 @@ snapshots: infisical-node@1.3.0: dependencies: - axios: 1.7.7 + axios: 1.7.7(debug@4.3.6) dotenv: 16.3.1 tweetnacl: 1.0.3 tweetnacl-util: 0.15.1 @@ -23369,28 +23345,6 @@ snapshots: optionalDependencies: openai: 4.73.1(encoding@0.1.13)(zod@3.23.8) - langsmith@0.2.3(openai@4.73.1(zod@3.23.8)): - dependencies: - '@types/uuid': 10.0.0 - commander: 10.0.1 - p-queue: 6.6.2 - p-retry: 4.6.2 - semver: 7.6.0 - uuid: 10.0.0 - optionalDependencies: - openai: 4.73.1(zod@3.23.8) - - langsmith@0.2.3(openai@4.73.1): - dependencies: - '@types/uuid': 10.0.0 - commander: 10.0.1 - p-queue: 6.6.2 - p-retry: 4.6.2 - semver: 7.6.0 - uuid: 10.0.0 - optionalDependencies: - openai: 4.73.1(zod@3.23.8) - lazy-ass@1.6.0: {} ldapts@4.2.6: @@ -23572,6 +23526,8 @@ snapshots: lodash.orderby@4.6.0: {} + lodash.set@4.3.2: {} + lodash.throttle@4.1.1: {} lodash@4.17.21: {} @@ -24723,22 +24679,6 @@ snapshots: - encoding - supports-color - openai@4.73.1(zod@3.23.8): - dependencies: - '@types/node': 18.16.16 - '@types/node-fetch': 2.6.4 - abort-controller: 3.0.0 - agentkeepalive: 4.2.1 - form-data-encoder: 1.7.2 - formdata-node: 4.4.1 - node-fetch: 2.7.0(encoding@0.1.13) - optionalDependencies: - zod: 3.23.8 - transitivePeerDependencies: - - encoding - - supports-color - optional: true - openapi-sampler@1.5.1: dependencies: '@types/json-schema': 7.0.15 @@ -24919,7 +24859,7 @@ snapshots: pdf-parse@1.1.1: dependencies: - debug: 3.2.7(supports-color@5.5.0) + debug: 3.2.7(supports-color@8.1.1) node-ensure: 0.0.0 transitivePeerDependencies: - supports-color @@ -25121,7 +25061,7 @@ snapshots: posthog-node@3.2.1: dependencies: - axios: 1.7.7 + axios: 1.7.7(debug@4.3.6) rusha: 0.8.14 transitivePeerDependencies: - debug @@ -25761,7 +25701,7 @@ snapshots: rhea@1.0.24: dependencies: - debug: 3.2.7(supports-color@5.5.0) + debug: 3.2.7(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -26139,7 +26079,7 @@ snapshots: asn1.js: 5.4.1 asn1.js-rfc2560: 5.0.1(asn1.js@5.4.1) asn1.js-rfc5280: 3.0.0 - axios: 1.7.7 + axios: 1.7.7(debug@4.3.6) big-integer: 1.6.51 bignumber.js: 9.1.2 binascii: 0.0.2 From dc7864a86d13a4a9cf0e72839e0e1945edb7c7a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Wed, 18 Dec 2024 20:05:00 +0100 Subject: [PATCH 08/14] docs(core): Update pruning service docline (#12282) --- .../@n8n/config/src/configs/executions.config.ts | 2 +- .../cli/src/services/pruning/pruning.service.ts | 14 +++++++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/@n8n/config/src/configs/executions.config.ts b/packages/@n8n/config/src/configs/executions.config.ts index 8c5d91b3c8f9c..977d53992004d 100644 --- a/packages/@n8n/config/src/configs/executions.config.ts +++ b/packages/@n8n/config/src/configs/executions.config.ts @@ -6,7 +6,7 @@ class PruningIntervalsConfig { @Env('EXECUTIONS_DATA_PRUNE_HARD_DELETE_INTERVAL') hardDelete: number = 15; - /** How often (minutes) execution data should be soft-deleted */ + /** How often (minutes) execution data should be soft-deleted. */ @Env('EXECUTIONS_DATA_PRUNE_SOFT_DELETE_INTERVAL') softDelete: number = 60; } diff --git a/packages/cli/src/services/pruning/pruning.service.ts b/packages/cli/src/services/pruning/pruning.service.ts index a7bc56725d0a2..aad8c5490f340 100644 --- a/packages/cli/src/services/pruning/pruning.service.ts +++ b/packages/cli/src/services/pruning/pruning.service.ts @@ -13,9 +13,17 @@ import { Logger } from '@/logging/logger.service'; import { OrchestrationService } from '../orchestration.service'; /** - * Responsible for pruning executions from the database and their associated binary data - * from the filesystem, on a rolling basis. By default we soft-delete execution rows - * every cycle and hard-delete them and their binary data every 4th cycle. + * Responsible for deleting old executions from the database and deleting their + * associated binary data from the filesystem, on a rolling basis. + * + * By default: + * + * - Soft deletion (every 60m) identifies all prunable executions based on max + * age and/or max count, exempting annotated executions. + * - Hard deletion (every 15m) processes prunable executions in batches of 100, + * switching to 1s intervals until the total to prune is back down low enough, + * or in case the hard deletion fails. + * - Once mostly caught up, hard deletion goes back to the 15m schedule. */ @Service() export class PruningService { From 7ce4e8d169d82b31f8e62af53d386415b5729090 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: Wed, 18 Dec 2024 20:05:41 +0100 Subject: [PATCH 09/14] fix(core): Switch from `lodash.set` to `lodash` to address CVE-2020-8203 (no-changelog) (#12286) --- packages/@n8n/task-runner/package.json | 4 +- .../src/js-task-runner/js-task-runner.ts | 2 +- pnpm-lock.yaml | 124 ++++++++++++++---- 3 files changed, 98 insertions(+), 32 deletions(-) diff --git a/packages/@n8n/task-runner/package.json b/packages/@n8n/task-runner/package.json index 3a5ffc2cf128c..f82692db77c42 100644 --- a/packages/@n8n/task-runner/package.json +++ b/packages/@n8n/task-runner/package.json @@ -38,7 +38,7 @@ "@sentry/node": "catalog:", "acorn": "8.14.0", "acorn-walk": "8.3.4", - "lodash.set": "4.3.2", + "lodash": "catalog:", "n8n-core": "workspace:*", "n8n-workflow": "workspace:*", "nanoid": "catalog:", @@ -46,7 +46,7 @@ "ws": "^8.18.0" }, "devDependencies": { - "@types/lodash.set": "4.3.9", + "@types/lodash": "catalog:", "luxon": "catalog:" } } diff --git a/packages/@n8n/task-runner/src/js-task-runner/js-task-runner.ts b/packages/@n8n/task-runner/src/js-task-runner/js-task-runner.ts index 0b3c0a647671f..04e05fb30aed6 100644 --- a/packages/@n8n/task-runner/src/js-task-runner/js-task-runner.ts +++ b/packages/@n8n/task-runner/src/js-task-runner/js-task-runner.ts @@ -1,4 +1,4 @@ -import set from 'lodash.set'; +import set from 'lodash/set'; import { getAdditionalKeys } from 'n8n-core'; import { WorkflowDataProxy, Workflow, ObservableObject } from 'n8n-workflow'; import type { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 164e9af759896..5744a83fb2a53 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -672,9 +672,9 @@ importers: acorn-walk: specifier: 8.3.4 version: 8.3.4 - lodash.set: - specifier: 4.3.2 - version: 4.3.2 + lodash: + specifier: 'catalog:' + version: 4.17.21 n8n-core: specifier: workspace:* version: link:../../core @@ -691,9 +691,9 @@ importers: specifier: '>=8.17.1' version: 8.17.1 devDependencies: - '@types/lodash.set': - specifier: 4.3.9 - version: 4.3.9 + '@types/lodash': + specifier: 'catalog:' + version: 4.14.195 luxon: specifier: 'catalog:' version: 3.4.4 @@ -1120,7 +1120,7 @@ importers: dependencies: '@langchain/core': specifier: 'catalog:' - version: 0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)) + version: 0.3.19(openai@4.73.1(zod@3.23.8)) '@n8n/client-oauth2': specifier: workspace:* version: link:../@n8n/client-oauth2 @@ -1972,7 +1972,7 @@ importers: devDependencies: '@langchain/core': specifier: 'catalog:' - version: 0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)) + version: 0.3.19(openai@4.73.1) '@types/deep-equal': specifier: ^1.0.1 version: 1.0.1 @@ -5627,9 +5627,6 @@ packages: '@types/lodash-es@4.17.6': resolution: {integrity: sha512-R+zTeVUKDdfoRxpAryaQNRKk3105Rrgx2CFRClIgRGaqDTdjsm8h6IYA8ir584W3ePzkZfst5xIgDwYrlh9HLg==} - '@types/lodash.set@4.3.9': - resolution: {integrity: sha512-KOxyNkZpbaggVmqbpr82N2tDVTx05/3/j0f50Es1prxrWB0XYf9p3QNxqcbWb7P1Q9wlvsUSlCFnwlPCIJ46PQ==} - '@types/lodash@4.14.195': resolution: {integrity: sha512-Hwx9EUgdwf2GLarOjQp5ZH8ZmblzcbTBC2wtQWNKARBSxM9ezRIAUpeDTgoQRAFB0+8CNWXVA9+MaSOzOF3nPg==} @@ -9745,9 +9742,6 @@ packages: lodash.orderby@4.6.0: resolution: {integrity: sha512-T0rZxKmghOOf5YPnn8EY5iLYeWCpZq8G41FfqoVHH5QDTAFaghJRmAdLiadEDq+ztgM2q5PjA+Z1fOwGrLgmtg==} - lodash.set@4.3.2: - resolution: {integrity: sha512-4hNPN5jlm/N/HLMCO43v8BXKq9Z7QdAGc/VGrRD61w8gN9g/6jF9A4L1pbUgBLCffi0w9VsXfTOij5x8iTyFvg==} - lodash.throttle@4.1.1: resolution: {integrity: sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==} @@ -16218,6 +16212,38 @@ snapshots: transitivePeerDependencies: - openai + '@langchain/core@0.3.19(openai@4.73.1(zod@3.23.8))': + dependencies: + ansi-styles: 5.2.0 + camelcase: 6.3.0 + decamelize: 1.2.0 + js-tiktoken: 1.0.12 + langsmith: 0.2.3(openai@4.73.1(zod@3.23.8)) + mustache: 4.2.0 + p-queue: 6.6.2 + p-retry: 4.6.2 + uuid: 10.0.0 + zod: 3.23.8 + zod-to-json-schema: 3.23.3(zod@3.23.8) + transitivePeerDependencies: + - openai + + '@langchain/core@0.3.19(openai@4.73.1)': + dependencies: + ansi-styles: 5.2.0 + camelcase: 6.3.0 + decamelize: 1.2.0 + js-tiktoken: 1.0.12 + langsmith: 0.2.3(openai@4.73.1) + mustache: 4.2.0 + p-queue: 6.6.2 + p-retry: 4.6.2 + uuid: 10.0.0 + zod: 3.23.8 + zod-to-json-schema: 3.23.3(zod@3.23.8) + transitivePeerDependencies: + - openai + '@langchain/google-common@0.1.3(@langchain/core@0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)))(zod@3.23.8)': dependencies: '@langchain/core': 0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)) @@ -18341,10 +18367,6 @@ snapshots: dependencies: '@types/lodash': 4.14.195 - '@types/lodash.set@4.3.9': - dependencies: - '@types/lodash': 4.14.195 - '@types/lodash@4.14.195': {} '@types/long@4.0.2': {} @@ -19450,6 +19472,14 @@ snapshots: transitivePeerDependencies: - debug + axios@1.7.7: + dependencies: + follow-redirects: 1.15.6(debug@4.3.6) + form-data: 4.0.0 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + axios@1.7.7(debug@4.3.6): dependencies: follow-redirects: 1.15.6(debug@4.3.6) @@ -21163,7 +21193,7 @@ snapshots: eslint-import-resolver-node@0.3.9: dependencies: - debug: 3.2.7(supports-color@8.1.1) + debug: 3.2.7(supports-color@5.5.0) is-core-module: 2.13.1 resolve: 1.22.8 transitivePeerDependencies: @@ -21188,7 +21218,7 @@ snapshots: eslint-module-utils@2.8.0(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.7.2))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0): dependencies: - debug: 3.2.7(supports-color@8.1.1) + debug: 3.2.7(supports-color@5.5.0) optionalDependencies: '@typescript-eslint/parser': 7.2.0(eslint@8.57.0)(typescript@5.7.2) eslint: 8.57.0 @@ -21208,7 +21238,7 @@ snapshots: array.prototype.findlastindex: 1.2.3 array.prototype.flat: 1.3.2 array.prototype.flatmap: 1.3.2 - debug: 3.2.7(supports-color@8.1.1) + debug: 3.2.7(supports-color@5.5.0) doctrine: 2.1.0 eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 @@ -21987,7 +22017,7 @@ snapshots: array-parallel: 0.1.3 array-series: 0.1.5 cross-spawn: 4.0.2 - debug: 3.2.7(supports-color@8.1.1) + debug: 3.2.7(supports-color@5.5.0) transitivePeerDependencies: - supports-color @@ -22368,7 +22398,7 @@ snapshots: infisical-node@1.3.0: dependencies: - axios: 1.7.7(debug@4.3.6) + axios: 1.7.7 dotenv: 16.3.1 tweetnacl: 1.0.3 tweetnacl-util: 0.15.1 @@ -23345,6 +23375,28 @@ snapshots: optionalDependencies: openai: 4.73.1(encoding@0.1.13)(zod@3.23.8) + langsmith@0.2.3(openai@4.73.1(zod@3.23.8)): + dependencies: + '@types/uuid': 10.0.0 + commander: 10.0.1 + p-queue: 6.6.2 + p-retry: 4.6.2 + semver: 7.6.0 + uuid: 10.0.0 + optionalDependencies: + openai: 4.73.1(zod@3.23.8) + + langsmith@0.2.3(openai@4.73.1): + dependencies: + '@types/uuid': 10.0.0 + commander: 10.0.1 + p-queue: 6.6.2 + p-retry: 4.6.2 + semver: 7.6.0 + uuid: 10.0.0 + optionalDependencies: + openai: 4.73.1(zod@3.23.8) + lazy-ass@1.6.0: {} ldapts@4.2.6: @@ -23526,8 +23578,6 @@ snapshots: lodash.orderby@4.6.0: {} - lodash.set@4.3.2: {} - lodash.throttle@4.1.1: {} lodash@4.17.21: {} @@ -24679,6 +24729,22 @@ snapshots: - encoding - supports-color + openai@4.73.1(zod@3.23.8): + dependencies: + '@types/node': 18.16.16 + '@types/node-fetch': 2.6.4 + abort-controller: 3.0.0 + agentkeepalive: 4.2.1 + form-data-encoder: 1.7.2 + formdata-node: 4.4.1 + node-fetch: 2.7.0(encoding@0.1.13) + optionalDependencies: + zod: 3.23.8 + transitivePeerDependencies: + - encoding + - supports-color + optional: true + openapi-sampler@1.5.1: dependencies: '@types/json-schema': 7.0.15 @@ -24859,7 +24925,7 @@ snapshots: pdf-parse@1.1.1: dependencies: - debug: 3.2.7(supports-color@8.1.1) + debug: 3.2.7(supports-color@5.5.0) node-ensure: 0.0.0 transitivePeerDependencies: - supports-color @@ -25061,7 +25127,7 @@ snapshots: posthog-node@3.2.1: dependencies: - axios: 1.7.7(debug@4.3.6) + axios: 1.7.7 rusha: 0.8.14 transitivePeerDependencies: - debug @@ -25701,7 +25767,7 @@ snapshots: rhea@1.0.24: dependencies: - debug: 3.2.7(supports-color@8.1.1) + debug: 3.2.7(supports-color@5.5.0) transitivePeerDependencies: - supports-color @@ -26079,7 +26145,7 @@ snapshots: asn1.js: 5.4.1 asn1.js-rfc2560: 5.0.1(asn1.js@5.4.1) asn1.js-rfc5280: 3.0.0 - axios: 1.7.7(debug@4.3.6) + axios: 1.7.7 big-integer: 1.6.51 bignumber.js: 9.1.2 binascii: 0.0.2 From 6e44c71c9ca82cce20eb55bb9003930bbf66a16c Mon Sep 17 00:00:00 2001 From: Shireen Missi <94372015+ShireenMissi@users.noreply.github.com> Date: Wed, 18 Dec 2024 20:39:10 +0000 Subject: [PATCH 10/14] feat(editor): Params pane collection improvements (#11607) Co-authored-by: Elias Meire --- cypress/e2e/16-form-trigger-node.cy.ts | 35 ++-- cypress/e2e/5-ndv.cy.ts | 20 -- .../design-system/src/css/_tokens.dark.scss | 3 + packages/design-system/src/css/_tokens.scss | 4 + packages/editor-ui/package.json | 1 + .../AssignmentCollection/Assignment.vue | 23 ++- .../AssignmentCollection.vue | 49 +++-- .../components/FilterConditions/Condition.vue | 27 ++- .../FilterConditions/FilterConditions.vue | 77 +++++--- .../components/FixedCollectionParameter.vue | 185 +++++++++--------- .../src/components/ParameterInputList.vue | 36 ++-- .../src/plugins/i18n/locales/en.json | 2 + pnpm-lock.yaml | 58 ++++-- 13 files changed, 311 insertions(+), 209 deletions(-) diff --git a/cypress/e2e/16-form-trigger-node.cy.ts b/cypress/e2e/16-form-trigger-node.cy.ts index 60fbd7c419586..ed901107ea60e 100644 --- a/cypress/e2e/16-form-trigger-node.cy.ts +++ b/cypress/e2e/16-form-trigger-node.cy.ts @@ -44,8 +44,7 @@ describe('n8n Form Trigger', () => { ':nth-child(3) > .border-top-dashed > .parameter-input-list-wrapper > :nth-child(1) > .parameter-item', ) .find('input[placeholder*="e.g. What is your name?"]') - .type('Test Field 3') - .blur(); + .type('Test Field 3'); cy.get( ':nth-child(3) > .border-top-dashed > .parameter-input-list-wrapper > :nth-child(2) > .parameter-item', ).click(); @@ -56,27 +55,24 @@ describe('n8n Form Trigger', () => { ':nth-child(4) > .border-top-dashed > .parameter-input-list-wrapper > :nth-child(1) > .parameter-item', ) .find('input[placeholder*="e.g. What is your name?"]') - .type('Test Field 4') - .blur(); + .type('Test Field 4'); cy.get( ':nth-child(4) > .border-top-dashed > .parameter-input-list-wrapper > :nth-child(2) > .parameter-item', ).click(); getVisibleSelect().contains('Dropdown').click(); - cy.get( - '.border-top-dashed > :nth-child(2) > :nth-child(3) > .multi-parameter > .fixed-collection-parameter > :nth-child(2) > .button', - ).click(); - cy.get( - ':nth-child(4) > :nth-child(1) > :nth-child(2) > :nth-child(3) > .multi-parameter > .fixed-collection-parameter > .fixed-collection-parameter-property > :nth-child(1) > :nth-child(1)', - ) - .find('input') - .type('Option 1') - .blur(); - cy.get( - ':nth-child(4) > :nth-child(1) > :nth-child(2) > :nth-child(3) > .multi-parameter > .fixed-collection-parameter > .fixed-collection-parameter-property > :nth-child(1) > :nth-child(2)', - ) - .find('input') - .type('Option 2') - .blur(); + cy.contains('button', 'Add Field Option').click(); + cy.contains('label', 'Field Options') + .parent() + .nextAll() + .find('[data-test-id="parameter-input-field"]') + .eq(0) + .type('Option 1'); + cy.contains('label', 'Field Options') + .parent() + .nextAll() + .find('[data-test-id="parameter-input-field"]') + .eq(1) + .type('Option 2'); //add optional submitted message cy.get('.param-options').click(); @@ -94,7 +90,6 @@ describe('n8n Form Trigger', () => { .children() .children() .first() - .clear() .type('Your test form was successfully submitted'); ndv.getters.backToCanvas().click(); diff --git a/cypress/e2e/5-ndv.cy.ts b/cypress/e2e/5-ndv.cy.ts index e8215db38f345..8bad4245544c0 100644 --- a/cypress/e2e/5-ndv.cy.ts +++ b/cypress/e2e/5-ndv.cy.ts @@ -65,26 +65,6 @@ describe('NDV', () => { cy.shouldNotHaveConsoleErrors(); }); - it('should disconect Switch outputs if rules order was changed', () => { - cy.createFixtureWorkflow('NDV-test-switch_reorder.json', 'NDV test switch reorder'); - workflowPage.actions.zoomToFit(); - - workflowPage.actions.executeWorkflow(); - workflowPage.actions.openNode('Merge'); - ndv.getters.outputPanel().contains('2 items').should('exist'); - cy.contains('span', 'first').should('exist'); - ndv.getters.backToCanvas().click(); - - workflowPage.actions.openNode('Switch'); - cy.get('.cm-line').realMouseMove(100, 100); - cy.get('.fa-angle-down').first().click(); - ndv.getters.backToCanvas().click(); - workflowPage.actions.executeWorkflow(); - workflowPage.actions.openNode('Merge'); - ndv.getters.outputPanel().contains('2 items').should('exist'); - cy.contains('span', 'zero').should('exist'); - }); - it('should show correct validation state for resource locator params', () => { workflowPage.actions.addNodeToCanvas('Typeform', true, true); ndv.getters.container().should('be.visible'); diff --git a/packages/design-system/src/css/_tokens.dark.scss b/packages/design-system/src/css/_tokens.dark.scss index a3fc653550f1a..4390bc1da81cb 100644 --- a/packages/design-system/src/css/_tokens.dark.scss +++ b/packages/design-system/src/css/_tokens.dark.scss @@ -462,6 +462,9 @@ --color-configurable-node-name: var(--color-text-dark); --color-secondary-link: var(--prim-color-secondary-tint-200); --color-secondary-link-hover: var(--prim-color-secondary-tint-100); + //Params + --color-icon-base: var(--color-text-light); + --color-icon-hover: var(--prim-color-primary); --color-menu-background: var(--prim-gray-740); --color-menu-hover-background: var(--prim-gray-670); diff --git a/packages/design-system/src/css/_tokens.scss b/packages/design-system/src/css/_tokens.scss index b8f3049cf2eaf..944652e43e3bc 100644 --- a/packages/design-system/src/css/_tokens.scss +++ b/packages/design-system/src/css/_tokens.scss @@ -621,6 +621,10 @@ --spacing-3xl: 4rem; --spacing-4xl: 8rem; --spacing-5xl: 16rem; + + //Params + --color-icon-base: var(--color-text-light); + --color-icon-hover: var(--prim-color-primary); } :root { diff --git a/packages/editor-ui/package.json b/packages/editor-ui/package.json index 3cf113d98fc0d..68b13e13f492d 100644 --- a/packages/editor-ui/package.json +++ b/packages/editor-ui/package.json @@ -82,6 +82,7 @@ "vue-router": "catalog:frontend", "vue-virtual-scroller": "2.0.0-beta.8", "vue3-touch-events": "^4.1.3", + "vuedraggable": "4.1.0", "xss": "catalog:" }, "devDependencies": { diff --git a/packages/editor-ui/src/components/AssignmentCollection/Assignment.vue b/packages/editor-ui/src/components/AssignmentCollection/Assignment.vue index 7240dac1e2c11..614ae3f590071 100644 --- a/packages/editor-ui/src/components/AssignmentCollection/Assignment.vue +++ b/packages/editor-ui/src/components/AssignmentCollection/Assignment.vue @@ -152,6 +152,14 @@ const onBlur = (): void => { }" data-test-id="assignment" > + { size="mini" icon="trash" data-test-id="assignment-remove" - :class="$style.remove" + :class="[$style.iconButton, $style.extraTopPadding]" @click="onRemove" > @@ -241,7 +249,7 @@ const onBlur = (): void => { } &:hover { - .remove { + .iconButton { opacity: 1; } } @@ -269,12 +277,19 @@ const onBlur = (): void => { } } -.remove { +.iconButton { position: absolute; left: 0; - top: var(--spacing-l); opacity: 0; transition: opacity 100ms ease-in; + color: var(--icon-base-color); +} +.extraTopPadding { + top: calc(20px + var(--spacing-l)); +} + +.defaultTopPadding { + top: var(--spacing-l); } .status { diff --git a/packages/editor-ui/src/components/AssignmentCollection/AssignmentCollection.vue b/packages/editor-ui/src/components/AssignmentCollection/AssignmentCollection.vue index 24545bd7b9085..0aebc16bb64fe 100644 --- a/packages/editor-ui/src/components/AssignmentCollection/AssignmentCollection.vue +++ b/packages/editor-ui/src/components/AssignmentCollection/AssignmentCollection.vue @@ -15,6 +15,7 @@ import ParameterOptions from '../ParameterOptions.vue'; import Assignment from './Assignment.vue'; import { inputDataToAssignments, typeFromExpression } from './utils'; import { propertyNameFromExpression } from '@/utils/mappingUtils'; +import Draggable from 'vuedraggable'; interface Props { parameter: INodeProperties; @@ -133,19 +134,27 @@ function optionSelected(action: string) {
-
- - -
+ + +
diff --git a/packages/editor-ui/src/components/FilterConditions/Condition.vue b/packages/editor-ui/src/components/FilterConditions/Condition.vue index 316b6827fac93..be4d02224c2e7 100644 --- a/packages/editor-ui/src/components/FilterConditions/Condition.vue +++ b/packages/editor-ui/src/components/FilterConditions/Condition.vue @@ -33,6 +33,7 @@ interface Props { canRemove?: boolean; readOnly?: boolean; index?: number; + canDrag?: boolean; } const props = withDefaults(defineProps(), { @@ -41,6 +42,7 @@ const props = withDefaults(defineProps(), { fixedLeftValue: false, readOnly: false, index: 0, + canDrag: true, }); const emit = defineEmits<{ @@ -152,6 +154,15 @@ const onBlur = (): void => { }" data-test-id="filter-condition" > + { icon="trash" data-test-id="filter-remove-condition" :title="i18n.baseText('filter.removeCondition')" - :class="$style.remove" + :class="[$style.iconButton, $style.extraTopPadding]" @click="onRemove" > @@ -248,7 +259,7 @@ const onBlur = (): void => { } &:hover { - .remove { + .iconButton { opacity: 1; } } @@ -261,13 +272,21 @@ const onBlur = (): void => { .statusIcon { padding-left: var(--spacing-4xs); + padding-right: var(--spacing-4xs); } -.remove { +.iconButton { position: absolute; left: 0; - top: var(--spacing-l); opacity: 0; transition: opacity 100ms ease-in; + color: var(--icon-base-color); +} + +.defaultTopPadding { + top: var(--spacing-m); +} +.extraTopPadding { + top: calc(14px + var(--spacing-m)); } diff --git a/packages/editor-ui/src/components/FilterConditions/FilterConditions.vue b/packages/editor-ui/src/components/FilterConditions/FilterConditions.vue index 6b58fb4290be5..41ef0f7430ce8 100644 --- a/packages/editor-ui/src/components/FilterConditions/FilterConditions.vue +++ b/packages/editor-ui/src/components/FilterConditions/FilterConditions.vue @@ -23,6 +23,7 @@ import Condition from './Condition.vue'; import CombinatorSelect from './CombinatorSelect.vue'; import { resolveParameter } from '@/composables/useWorkflowHelpers'; import { v4 as uuid } from 'uuid'; +import Draggable from 'vuedraggable'; interface Props { parameter: INodeProperties; @@ -161,30 +162,41 @@ function getIssues(index: number): string[] {
-
- - - -
+ + +
.combinator { + display: none; +} diff --git a/packages/editor-ui/src/components/FixedCollectionParameter.vue b/packages/editor-ui/src/components/FixedCollectionParameter.vue index de7359dfddb9a..59eacfdb100e6 100644 --- a/packages/editor-ui/src/components/FixedCollectionParameter.vue +++ b/packages/editor-ui/src/components/FixedCollectionParameter.vue @@ -17,6 +17,7 @@ import { N8nButton, } from 'n8n-design-system'; import ParameterInputList from './ParameterInputList.vue'; +import Draggable from 'vuedraggable'; const locale = useI18n(); @@ -126,42 +127,6 @@ const getOptionProperties = (optionName: string) => { return undefined; }; -const moveOptionDown = (optionName: string, index: number) => { - if (Array.isArray(mutableValues.value[optionName])) { - mutableValues.value[optionName].splice( - index + 1, - 0, - mutableValues.value[optionName].splice(index, 1)[0], - ); - } - - const parameterData: ValueChangedEvent = { - name: getPropertyPath(optionName), - value: mutableValues.value[optionName], - type: 'optionsOrderChanged', - }; - - emit('valueChanged', parameterData); -}; - -const moveOptionUp = (optionName: string, index: number) => { - if (Array.isArray(mutableValues.value[optionName])) { - mutableValues.value?.[optionName].splice( - index - 1, - 0, - mutableValues.value[optionName].splice(index, 1)[0], - ); - } - - const parameterData: ValueChangedEvent = { - name: getPropertyPath(optionName), - value: mutableValues.value[optionName], - type: 'optionsOrderChanged', - }; - - emit('valueChanged', parameterData); -}; - const optionSelected = (optionName: string) => { const option = getOptionProperties(optionName); if (option === undefined) { @@ -219,6 +184,15 @@ const optionSelected = (optionName: string) => { const valueChanged = (parameterData: IUpdateInformation) => { emit('valueChanged', parameterData); }; +const onDragChange = (optionName: string) => { + const parameterData: ValueChangedEvent = { + name: getPropertyPath(optionName), + value: mutableValues.value[optionName], + type: 'optionsOrderChanged', + }; + + emit('valueChanged', parameterData); +}; @@ -114,6 +140,11 @@ const displayProjects = computed(() => width: 100%; overflow: hidden; align-items: start; + &:hover { + .plusBtn { + display: block; + } + } } .projectItems { @@ -132,12 +163,40 @@ const displayProjects = computed(() => } .projectsLabel { - margin: 0 var(--spacing-xs) var(--spacing-s); + display: flex; + justify-content: space-between; + margin: 0 0 var(--spacing-s) var(--spacing-xs); padding: 0 var(--spacing-s); text-overflow: ellipsis; overflow: hidden; box-sizing: border-box; color: var(--color-text-base); + + &.collapsed { + padding: 0; + margin-left: 0; + justify-content: center; + } +} + +.plusBtn { + margin: 0; + padding: 0; + color: var(--color-text-lighter); + display: none; +} + +.addFirstProjectBtn { + border: 1px solid var(--color-background-dark); + font-size: var(--font-size-xs); + padding: var(--spacing-3xs); + margin: 0 var(--spacing-m) var(--spacing-m); + + &.collapsed { + > span:last-child { + display: none; + } + } } diff --git a/packages/editor-ui/src/composables/useGlobalEntityCreation.ts b/packages/editor-ui/src/composables/useGlobalEntityCreation.ts index 6427bae1bc6bc..bf1efb66918b6 100644 --- a/packages/editor-ui/src/composables/useGlobalEntityCreation.ts +++ b/packages/editor-ui/src/composables/useGlobalEntityCreation.ts @@ -1,4 +1,4 @@ -import { computed } from 'vue'; +import { computed, ref } from 'vue'; import { VIEWS } from '@/constants'; import { useRouter } from 'vue-router'; import { useI18n } from '@/composables/useI18n'; @@ -35,6 +35,9 @@ export const useGlobalEntityCreation = () => { const router = useRouter(); const i18n = useI18n(); const toast = useToast(); + + const isCreatingProject = ref(false); + const displayProjects = computed(() => sortByProperty( 'name', @@ -156,6 +159,8 @@ export const useGlobalEntityCreation = () => { }); const createProject = async () => { + isCreatingProject.value = true; + try { const newProject = await projectsStore.createProject({ name: i18n.baseText('projects.settings.newProjectName'), @@ -169,6 +174,8 @@ export const useGlobalEntityCreation = () => { }); } catch (error) { toast.showError(error, i18n.baseText('projects.error.title')); + } finally { + isCreatingProject.value = false; } }; @@ -226,5 +233,8 @@ export const useGlobalEntityCreation = () => { createProjectAppendSlotName, projectsLimitReachedMessage, upgradeLabel, + createProject, + isCreatingProject, + displayProjects, }; }; diff --git a/packages/editor-ui/src/plugins/i18n/locales/en.json b/packages/editor-ui/src/plugins/i18n/locales/en.json index d7dd178d93f47..638000b2198ad 100644 --- a/packages/editor-ui/src/plugins/i18n/locales/en.json +++ b/packages/editor-ui/src/plugins/i18n/locales/en.json @@ -2540,7 +2540,7 @@ "projects.menu.overview": "Overview", "projects.menu.title": "Projects", "projects.menu.personal": "Personal", - "projects.menu.addProject": "Add project", + "projects.menu.addFirstProject": "Add first project", "projects.settings": "Project settings", "projects.settings.newProjectName": "My project", "projects.settings.name": "Project name", From e0dc385f8bc8ee13fbc5bbf35e07654e52b193e9 Mon Sep 17 00:00:00 2001 From: Marc Littlemore Date: Thu, 19 Dec 2024 08:13:17 +0000 Subject: [PATCH 13/14] feat(API): Exclude pinned data from workflows (#12261) --- .../src/databases/entities/workflow-entity.ts | 2 +- packages/cli/src/public-api/types.ts | 3 +- .../workflows/spec/paths/workflows.id.yml | 7 +++ .../workflows/spec/paths/workflows.yml | 7 +++ .../handlers/workflows/workflows.handler.ts | 21 ++++++- .../integration/public-api/workflows.test.ts | 61 +++++++++++++++++++ 6 files changed, 98 insertions(+), 3 deletions(-) diff --git a/packages/cli/src/databases/entities/workflow-entity.ts b/packages/cli/src/databases/entities/workflow-entity.ts index b03cf2c28d9cb..67d0f0e345cc3 100644 --- a/packages/cli/src/databases/entities/workflow-entity.ts +++ b/packages/cli/src/databases/entities/workflow-entity.ts @@ -80,7 +80,7 @@ export class WorkflowEntity extends WithTimestampsAndStringId implements IWorkfl nullable: true, transformer: sqlite.jsonColumn, }) - pinData: ISimplifiedPinData; + pinData?: ISimplifiedPinData; @Column({ length: 36 }) versionId: string; diff --git a/packages/cli/src/public-api/types.ts b/packages/cli/src/public-api/types.ts index 327d3630739b1..b10d2f81bda8b 100644 --- a/packages/cli/src/public-api/types.ts +++ b/packages/cli/src/public-api/types.ts @@ -74,11 +74,12 @@ export declare namespace WorkflowRequest { active: boolean; name?: string; projectId?: string; + excludePinnedData?: boolean; } >; type Create = AuthenticatedRequest<{}, {}, WorkflowEntity, {}>; - type Get = AuthenticatedRequest<{ id: string }, {}, {}, {}>; + type Get = AuthenticatedRequest<{ id: string }, {}, {}, { excludePinnedData?: boolean }>; type Delete = Get; type Update = AuthenticatedRequest<{ id: string }, {}, WorkflowEntity, {}>; type Activate = Get; diff --git a/packages/cli/src/public-api/v1/handlers/workflows/spec/paths/workflows.id.yml b/packages/cli/src/public-api/v1/handlers/workflows/spec/paths/workflows.id.yml index 37cad74c86e4d..c8b2bf51cdc29 100644 --- a/packages/cli/src/public-api/v1/handlers/workflows/spec/paths/workflows.id.yml +++ b/packages/cli/src/public-api/v1/handlers/workflows/spec/paths/workflows.id.yml @@ -6,6 +6,13 @@ get: summary: Retrieves a workflow description: Retrieves a workflow. parameters: + - name: excludePinnedData + in: query + required: false + description: Set this to avoid retrieving pinned data + schema: + type: boolean + example: true - $ref: '../schemas/parameters/workflowId.yml' responses: '200': diff --git a/packages/cli/src/public-api/v1/handlers/workflows/spec/paths/workflows.yml b/packages/cli/src/public-api/v1/handlers/workflows/spec/paths/workflows.yml index 1024e36cb5948..4b3bc5e069efd 100644 --- a/packages/cli/src/public-api/v1/handlers/workflows/spec/paths/workflows.yml +++ b/packages/cli/src/public-api/v1/handlers/workflows/spec/paths/workflows.yml @@ -60,6 +60,13 @@ get: schema: type: string example: VmwOO9HeTEj20kxM + - name: excludePinnedData + in: query + required: false + description: Set this to avoid retrieving pinned data + schema: + type: boolean + example: true - $ref: '../../../../shared/spec/parameters/limit.yml' - $ref: '../../../../shared/spec/parameters/cursor.yml' responses: diff --git a/packages/cli/src/public-api/v1/handlers/workflows/workflows.handler.ts b/packages/cli/src/public-api/v1/handlers/workflows/workflows.handler.ts index b0956a15c1bd4..7a9003dc284c6 100644 --- a/packages/cli/src/public-api/v1/handlers/workflows/workflows.handler.ts +++ b/packages/cli/src/public-api/v1/handlers/workflows/workflows.handler.ts @@ -105,6 +105,7 @@ export = { projectScope('workflow:read', 'workflow'), async (req: WorkflowRequest.Get, res: express.Response): Promise => { const { id } = req.params; + const { excludePinnedData = false } = req.query; const workflow = await Container.get(SharedWorkflowRepository).findWorkflowForUser( id, @@ -120,6 +121,10 @@ export = { return res.status(404).json({ message: 'Not Found' }); } + if (excludePinnedData) { + delete workflow.pinData; + } + Container.get(EventService).emit('user-retrieved-workflow', { userId: req.user.id, publicApi: true, @@ -131,7 +136,15 @@ export = { getWorkflows: [ validCursor, async (req: WorkflowRequest.GetAll, res: express.Response): Promise => { - const { offset = 0, limit = 100, active, tags, name, projectId } = req.query; + const { + offset = 0, + limit = 100, + excludePinnedData = false, + active, + tags, + name, + projectId, + } = req.query; const where: FindOptionsWhere = { ...(active !== undefined && { active }), @@ -199,6 +212,12 @@ export = { ...(!config.getEnv('workflowTagsDisabled') && { relations: ['tags'] }), }); + if (excludePinnedData) { + workflows.forEach((workflow) => { + delete workflow.pinData; + }); + } + Container.get(EventService).emit('user-retrieved-all-workflows', { userId: req.user.id, publicApi: true, diff --git a/packages/cli/test/integration/public-api/workflows.test.ts b/packages/cli/test/integration/public-api/workflows.test.ts index 5425455aca790..28f9d444daa27 100644 --- a/packages/cli/test/integration/public-api/workflows.test.ts +++ b/packages/cli/test/integration/public-api/workflows.test.ts @@ -378,6 +378,47 @@ describe('GET /workflows', () => { expect(updatedAt).toBeDefined(); } }); + + test('should return all owned workflows without pinned data', async () => { + await Promise.all([ + createWorkflow( + { + pinData: { + Webhook1: [{ json: { first: 'first' } }], + }, + }, + member, + ), + createWorkflow( + { + pinData: { + Webhook2: [{ json: { second: 'second' } }], + }, + }, + member, + ), + createWorkflow( + { + pinData: { + Webhook3: [{ json: { third: 'third' } }], + }, + }, + member, + ), + ]); + + const response = await authMemberAgent.get('/workflows?excludePinnedData=true'); + + expect(response.statusCode).toBe(200); + expect(response.body.data.length).toBe(3); + expect(response.body.nextCursor).toBeNull(); + + for (const workflow of response.body.data) { + const { pinData } = workflow; + + expect(pinData).not.toBeDefined(); + } + }); }); describe('GET /workflows/:id', () => { @@ -444,6 +485,26 @@ describe('GET /workflows/:id', () => { expect(createdAt).toEqual(workflow.createdAt.toISOString()); expect(updatedAt).toEqual(workflow.updatedAt.toISOString()); }); + + test('should retrieve workflow without pinned data', async () => { + // create and assign workflow to owner + const workflow = await createWorkflow( + { + pinData: { + Webhook1: [{ json: { first: 'first' } }], + }, + }, + member, + ); + + const response = await authMemberAgent.get(`/workflows/${workflow.id}?excludePinnedData=true`); + + expect(response.statusCode).toBe(200); + + const { pinData } = response.body; + + expect(pinData).not.toBeDefined(); + }); }); describe('DELETE /workflows/:id', () => { From 08ae1a428dcb1b3e38c558dc0119cb1ec0b8abcf Mon Sep 17 00:00:00 2001 From: Charlie Kolb Date: Thu, 19 Dec 2024 12:56:08 +0100 Subject: [PATCH 14/14] feat(Execute Workflow Node): Move Type Conversion functionality to ResourceMapper and other Product Feedback (#12268) --- cypress/e2e/48-subworkflow-inputs.cy.ts | 1 + cypress/fixtures/Test_Subworkflow-Inputs.json | 18 +- .../ToolWorkflow/v2/ToolWorkflowV2.node.ts | 2 +- .../v2/methods/resourceMapping.ts | 79 --------- .../v2/utils/WorkflowToolService.ts | 2 +- .../core/src/node-execution-context/utils.ts | 13 +- .../ResourceMapper/ResourceMapper.vue | 62 +++++++ .../src/plugins/i18n/locales/en.json | 4 + .../ExecuteWorkflow/ExecuteWorkflow.node.ts | 57 ++----- .../methods/resourceMapping.ts | 37 ---- .../ExecuteWorkflowTrigger.node.test.ts | 32 ++-- .../ExecuteWorkflowTrigger.node.ts | 82 ++++----- .../workflowInputsResourceMapping/.readme | 2 +- .../GenericFunctions.ts | 160 +++++++++--------- .../constants.ts | 1 - packages/workflow/src/Interfaces.ts | 4 + pnpm-lock.yaml | 120 +++---------- 17 files changed, 270 insertions(+), 406 deletions(-) delete mode 100644 packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/methods/resourceMapping.ts delete mode 100644 packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflow/methods/resourceMapping.ts diff --git a/cypress/e2e/48-subworkflow-inputs.cy.ts b/cypress/e2e/48-subworkflow-inputs.cy.ts index d3315f6cca724..02eff605f8494 100644 --- a/cypress/e2e/48-subworkflow-inputs.cy.ts +++ b/cypress/e2e/48-subworkflow-inputs.cy.ts @@ -107,6 +107,7 @@ function validateAndReturnToParent(targetChild: string, offset: number, fields: // Due to our workaround to remain in the same tab we need to select the correct tab manually navigateWorkflowSelectionDropdown(offset, targetChild); + // This fails, pointing to `usePushConnection` `const triggerNode = subWorkflow?.nodes.find` being `undefined.find()`I ndv.actions.execute(); getOutputTableHeaders().should('have.length', fields.length + 1); diff --git a/cypress/fixtures/Test_Subworkflow-Inputs.json b/cypress/fixtures/Test_Subworkflow-Inputs.json index 1b2320510fdea..5b96c0e3f2c8e 100644 --- a/cypress/fixtures/Test_Subworkflow-Inputs.json +++ b/cypress/fixtures/Test_Subworkflow-Inputs.json @@ -23,13 +23,23 @@ }, { "parameters": { + "workflowId": {}, + "workflowInputs": { + "mappingMode": "defineBelow", + "value": {}, + "matchingColumns": [], + "schema": [], + "ignoreTypeMismatchErrors": false, + "attemptToConvertTypes": false, + "convertFieldsToString": true + }, "options": {} }, - "id": "ddc82976-edd9-4488-a5a5-7f558a7d905b", - "name": "Execute Workflow", "type": "n8n-nodes-base.executeWorkflow", - "typeVersion": 1.1, - "position": [500, 240] + "typeVersion": 1.2, + "position": [500, 240], + "id": "6b6e2e34-c6ab-4083-b8e3-6b0d56be5453", + "name": "Execute Workflow" } ], "connections": { diff --git a/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/ToolWorkflowV2.node.ts b/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/ToolWorkflowV2.node.ts index 5ab1604262bc4..22ca31e4da2b1 100644 --- a/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/ToolWorkflowV2.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/ToolWorkflowV2.node.ts @@ -1,3 +1,4 @@ +import { loadWorkflowInputMappings } from 'n8n-nodes-base/dist/utils/workflowInputsResourceMapping/GenericFunctions'; import type { INodeTypeBaseDescription, ISupplyDataFunctions, @@ -6,7 +7,6 @@ import type { INodeTypeDescription, } from 'n8n-workflow'; -import { loadWorkflowInputMappings } from './methods/resourceMapping'; import { WorkflowToolService } from './utils/WorkflowToolService'; import { versionDescription } from './versionDescription'; diff --git a/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/methods/resourceMapping.ts b/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/methods/resourceMapping.ts deleted file mode 100644 index e0c0b0d6650d2..0000000000000 --- a/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/methods/resourceMapping.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { - getFieldEntries, - getWorkflowInputData, -} from 'n8n-nodes-base/dist/utils/workflowInputsResourceMapping/GenericFunctions'; -import type { - ISupplyDataFunctions, - IDataObject, - FieldValueOption, - ResourceMapperField, - ILocalLoadOptionsFunctions, - ResourceMapperFields, -} from 'n8n-workflow'; -import { EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE } from 'n8n-workflow'; - -export async function loadWorkflowInputMappings( - this: ILocalLoadOptionsFunctions, -): Promise { - const nodeLoadContext = await this.getWorkflowNodeContext(EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE); - let fields: ResourceMapperField[] = []; - if (nodeLoadContext) { - const fieldValues = getFieldEntries(nodeLoadContext); - fields = fieldValues.map((currentWorkflowInput) => { - const field: ResourceMapperField = { - id: currentWorkflowInput.name, - displayName: currentWorkflowInput.name, - required: false, - defaultMatch: false, - display: true, - canBeUsedToMatch: true, - }; - - if (currentWorkflowInput.type !== 'any') { - field.type = currentWorkflowInput.type; - } - - return field; - }); - } - return { fields }; -} - -export function getWorkflowInputValues(this: ISupplyDataFunctions) { - const inputData = this.getInputData(); - - return inputData.map((item, itemIndex) => { - const itemFieldValues = this.getNodeParameter( - 'workflowInputs.value', - itemIndex, - {}, - ) as IDataObject; - - return { - json: { - ...item.json, - ...itemFieldValues, - }, - index: itemIndex, - pairedItem: { - item: itemIndex, - }, - }; - }); -} - -export function getCurrentWorkflowInputData(this: ISupplyDataFunctions) { - const inputData = getWorkflowInputValues.call(this); - - const schema = this.getNodeParameter('workflowInputs.schema', 0, []) as ResourceMapperField[]; - - if (schema.length === 0) { - return inputData; - } else { - const newParams = schema - .filter((x) => !x.removed) - .map((x) => ({ name: x.displayName, type: x.type ?? 'any' })) as FieldValueOption[]; - - return getWorkflowInputData.call(this, inputData, newParams); - } -} diff --git a/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/utils/WorkflowToolService.ts b/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/utils/WorkflowToolService.ts index 7ea2bf0284174..5412f17ccd46c 100644 --- a/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/utils/WorkflowToolService.ts +++ b/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/utils/WorkflowToolService.ts @@ -4,6 +4,7 @@ import get from 'lodash/get'; import isObject from 'lodash/isObject'; import type { SetField, SetNodeOptions } from 'n8n-nodes-base/dist/nodes/Set/v2/helpers/interfaces'; import * as manual from 'n8n-nodes-base/dist/nodes/Set/v2/manual.mode'; +import { getCurrentWorkflowInputData } from 'n8n-nodes-base/dist/utils/workflowInputsResourceMapping/GenericFunctions'; import type { ExecuteWorkflowData, ExecutionError, @@ -22,7 +23,6 @@ import { z } from 'zod'; import type { FromAIArgument } from './FromAIParser'; import { AIParametersParser } from './FromAIParser'; -import { getCurrentWorkflowInputData } from '../methods/resourceMapping'; /** Main class for creating the Workflow tool diff --git a/packages/core/src/node-execution-context/utils.ts b/packages/core/src/node-execution-context/utils.ts index a09147d543b23..bcde2fc6d7365 100644 --- a/packages/core/src/node-execution-context/utils.ts +++ b/packages/core/src/node-execution-context/utils.ts @@ -83,7 +83,6 @@ const validateResourceMapperValue = ( for (let i = 0; i < paramValueNames.length; i++) { const key = paramValueNames[i]; const resolvedValue = paramValues[key]; - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call const schemaEntry = schema.find((s) => s.id === key); if ( @@ -99,15 +98,19 @@ const validateResourceMapperValue = ( }; } - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access if (schemaEntry?.type) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access const validationResult = validateFieldType(key, resolvedValue, schemaEntry.type, { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access valueOptions: schemaEntry.options, + strict: !resourceMapperField.attemptToConvertTypes, + parseStrings: !!resourceMapperField.convertFieldsToString, }); + if (!validationResult.valid) { - return { ...validationResult, fieldName: key }; + if (!resourceMapperField.ignoreTypeMismatchErrors) { + return { ...validationResult, fieldName: key }; + } else { + paramValues[key] = resolvedValue; + } } else { // If it's valid, set the casted value paramValues[key] = validationResult.newValue; diff --git a/packages/editor-ui/src/components/ResourceMapper/ResourceMapper.vue b/packages/editor-ui/src/components/ResourceMapper/ResourceMapper.vue index 2bc34a447582d..8ae72a418df3b 100644 --- a/packages/editor-ui/src/components/ResourceMapper/ResourceMapper.vue +++ b/packages/editor-ui/src/components/ResourceMapper/ResourceMapper.vue @@ -7,6 +7,7 @@ import type { INodeParameters, INodeProperties, INodeTypeDescription, + NodeParameterValueType, ResourceMapperField, ResourceMapperValue, } from 'n8n-workflow'; @@ -52,6 +53,12 @@ const state = reactive({ value: {}, matchingColumns: [] as string[], schema: [] as ResourceMapperField[], + ignoreTypeMismatchErrors: false, + attemptToConvertTypes: false, + // This should always be true if `showTypeConversionOptions` is provided + // It's used to avoid accepting any value as string without casting it + // Which is the legacy behavior without these type options. + convertFieldsToString: false, } as ResourceMapperValue, parameterValues: {} as INodeParameters, loading: false, @@ -97,6 +104,10 @@ onMounted(async () => { ...state.parameterValues, parameters: props.node.parameters, }; + + if (showTypeConversionOptions.value) { + state.paramValue.convertFieldsToString = true; + } } const params = state.parameterValues.parameters as INodeParameters; const parameterName = props.parameter.name; @@ -161,6 +172,10 @@ const showMappingModeSelect = computed(() => { return props.parameter.typeOptions?.resourceMapper?.supportAutoMap !== false; }); +const showTypeConversionOptions = computed(() => { + return props.parameter.typeOptions?.resourceMapper?.showTypeConversionOptions === true; +}); + const showMatchingColumnsSelector = computed(() => { return ( !state.loading && @@ -572,5 +587,52 @@ defineExpose({ }) }} +
+ + +
+ + diff --git a/packages/editor-ui/src/plugins/i18n/locales/en.json b/packages/editor-ui/src/plugins/i18n/locales/en.json index 638000b2198ad..d8084b9eed30c 100644 --- a/packages/editor-ui/src/plugins/i18n/locales/en.json +++ b/packages/editor-ui/src/plugins/i18n/locales/en.json @@ -1588,6 +1588,10 @@ "resourceMapper.addAllFields": "Add All {fieldWord}", "resourceMapper.removeAllFields": "Remove All {fieldWord}", "resourceMapper.refreshFieldList": "Refresh {fieldWord} List", + "resourceMapper.attemptToConvertTypes.displayName": "Attempt to convert types", + "resourceMapper.attemptToConvertTypes.description": "Attempt to convert types when mapping fields", + "resourceMapper.ignoreTypeMismatchErrors.displayName": "Ignore type mismatch errors", + "resourceMapper.ignoreTypeMismatchErrors.description": "Whether type mismatches should be ignored, rather than returning an Error", "runData.openSubExecution": "Inspect Sub-Execution {id}", "runData.openParentExecution": "Inspect Parent Execution {id}", "runData.emptyItemHint": "This is an item, but it's empty.", diff --git a/packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflow/ExecuteWorkflow.node.ts b/packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflow/ExecuteWorkflow.node.ts index 121a5ad38bc26..2f67d7bfe9b6f 100644 --- a/packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflow/ExecuteWorkflow.node.ts +++ b/packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflow/ExecuteWorkflow.node.ts @@ -1,59 +1,18 @@ import { NodeConnectionType, NodeOperationError } from 'n8n-workflow'; import type { ExecuteWorkflowData, - FieldValueOption, - IDataObject, IExecuteFunctions, INodeExecutionData, INodeType, INodeTypeDescription, - ResourceMapperField, } from 'n8n-workflow'; import { getWorkflowInfo } from './GenericFunctions'; -import { loadWorkflowInputMappings } from './methods/resourceMapping'; import { generatePairedItemData } from '../../../utils/utilities'; -import { getWorkflowInputData } from '../../../utils/workflowInputsResourceMapping/GenericFunctions'; - -function getWorkflowInputValues(this: IExecuteFunctions) { - const inputData = this.getInputData(); - - return inputData.map((item, itemIndex) => { - const itemFieldValues = this.getNodeParameter( - 'workflowInputs.value', - itemIndex, - {}, - ) as IDataObject; - - return { - json: { - ...item.json, - ...itemFieldValues, - }, - index: itemIndex, - pairedItem: { - item: itemIndex, - }, - }; - }); -} - -function getCurrentWorkflowInputData(this: IExecuteFunctions) { - const inputData = getWorkflowInputValues.call(this); - - const schema = this.getNodeParameter('workflowInputs.schema', 0, []) as ResourceMapperField[]; - - if (schema.length === 0) { - return inputData; - } else { - const newParams = schema - .filter((x) => !x.removed) - .map((x) => ({ name: x.displayName, type: x.type ?? 'any' })) as FieldValueOption[]; - - return getWorkflowInputData.call(this, inputData, newParams); - } -} - +import { + getCurrentWorkflowInputData, + loadWorkflowInputMappings, +} from '../../../utils/workflowInputsResourceMapping/GenericFunctions'; export class ExecuteWorkflow implements INodeType { description: INodeTypeDescription = { displayName: 'Execute Workflow', @@ -84,6 +43,13 @@ export class ExecuteWorkflow implements INodeType { }, ], }, + { + displayName: 'This node is out of date. Please upgrade by removing it and adding a new one', + name: 'outdatedVersionWarning', + type: 'notice', + displayOptions: { show: { '@version': [{ _cnd: { lte: 1.1 } }] } }, + default: '', + }, { displayName: 'Source', name: 'source', @@ -254,6 +220,7 @@ export class ExecuteWorkflow implements INodeType { addAllFields: true, multiKeyMatch: false, supportAutoMap: false, + showTypeConversionOptions: true, }, }, displayOptions: { diff --git a/packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflow/methods/resourceMapping.ts b/packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflow/methods/resourceMapping.ts deleted file mode 100644 index c747f6f505b9d..0000000000000 --- a/packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflow/methods/resourceMapping.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { - EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE, - type ILocalLoadOptionsFunctions, - type ResourceMapperField, - type ResourceMapperFields, -} from 'n8n-workflow'; - -import { getFieldEntries } from '../../../../utils/workflowInputsResourceMapping/GenericFunctions'; - -export async function loadWorkflowInputMappings( - this: ILocalLoadOptionsFunctions, -): Promise { - const nodeLoadContext = await this.getWorkflowNodeContext(EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE); - let fields: ResourceMapperField[] = []; - - if (nodeLoadContext) { - const fieldValues = getFieldEntries(nodeLoadContext); - - fields = fieldValues.map((currentWorkflowInput) => { - const field: ResourceMapperField = { - id: currentWorkflowInput.name, - displayName: currentWorkflowInput.name, - required: false, - defaultMatch: false, - display: true, - canBeUsedToMatch: true, - }; - - if (currentWorkflowInput.type !== 'any') { - field.type = currentWorkflowInput.type; - } - - return field; - }); - } - return { fields }; -} diff --git a/packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflowTrigger/ExecuteWorkflowTrigger.node.test.ts b/packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflowTrigger/ExecuteWorkflowTrigger.node.test.ts index 953f78fd21d3b..b479538c3a727 100644 --- a/packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflowTrigger/ExecuteWorkflowTrigger.node.test.ts +++ b/packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflowTrigger/ExecuteWorkflowTrigger.node.test.ts @@ -2,10 +2,8 @@ import { mock } from 'jest-mock-extended'; import type { FieldValueOption, IExecuteFunctions, INode, INodeExecutionData } from 'n8n-workflow'; import { ExecuteWorkflowTrigger } from './ExecuteWorkflowTrigger.node'; -import { - getFieldEntries, - getWorkflowInputData, -} from '../../../utils/workflowInputsResourceMapping/GenericFunctions'; +import { WORKFLOW_INPUTS } from '../../../utils/workflowInputsResourceMapping/constants'; +import { getFieldEntries } from '../../../utils/workflowInputsResourceMapping/GenericFunctions'; jest.mock('../../../utils/workflowInputsResourceMapping/GenericFunctions', () => ({ getFieldEntries: jest.fn(), @@ -14,8 +12,8 @@ jest.mock('../../../utils/workflowInputsResourceMapping/GenericFunctions', () => describe('ExecuteWorkflowTrigger', () => { const mockInputData: INodeExecutionData[] = [ - { json: { item: 0, foo: 'bar' } }, - { json: { item: 1, foo: 'quz' } }, + { json: { item: 0, foo: 'bar' }, index: 0 }, + { json: { item: 1, foo: 'quz' }, index: 1 }, ]; const mockNode = mock({ typeVersion: 1 }); const executeFns = mock({ @@ -24,28 +22,32 @@ describe('ExecuteWorkflowTrigger', () => { getNodeParameter: jest.fn(), }); - it('should return its input data on V1', async () => { + it('should return its input data on V1 or V1.1 passthrough', async () => { + // User selection in V1.1, or fallback return value in V1 with dropdown not displayed executeFns.getNodeParameter.mockReturnValueOnce('passthrough'); const result = await new ExecuteWorkflowTrigger().execute.call(executeFns); expect(result).toEqual([mockInputData]); }); - it('should return transformed input data based on newParams when input source is not passthrough', async () => { - executeFns.getNodeParameter.mockReturnValueOnce('usingFieldsBelow'); + it('should filter out parent input in `Using Fields below` mode', async () => { + executeFns.getNodeParameter.mockReturnValueOnce(WORKFLOW_INPUTS); const mockNewParams = [ { name: 'value1', type: 'string' }, { name: 'value2', type: 'number' }, + { name: 'foo', type: 'string' }, ] as FieldValueOption[]; const getFieldEntriesMock = (getFieldEntries as jest.Mock).mockReturnValue(mockNewParams); - const getWorkflowInputDataMock = (getWorkflowInputData as jest.Mock).mockReturnValue( - mockInputData, - ); const result = await new ExecuteWorkflowTrigger().execute.call(executeFns); - - expect(result).toEqual([mockInputData]); + const expected = [ + [ + { index: 0, json: { value1: null, value2: null, foo: mockInputData[0].json.foo } }, + { index: 1, json: { value1: null, value2: null, foo: mockInputData[1].json.foo } }, + ], + ]; + + expect(result).toEqual(expected); expect(getFieldEntriesMock).toHaveBeenCalledWith(executeFns); - expect(getWorkflowInputDataMock).toHaveBeenCalledWith(mockInputData, mockNewParams); }); }); diff --git a/packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflowTrigger/ExecuteWorkflowTrigger.node.ts b/packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflowTrigger/ExecuteWorkflowTrigger.node.ts index 0c8e5f64ce7cf..b2c4321cdb666 100644 --- a/packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflowTrigger/ExecuteWorkflowTrigger.node.ts +++ b/packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflowTrigger/ExecuteWorkflowTrigger.node.ts @@ -1,4 +1,6 @@ +import _ from 'lodash'; import { + type INodeExecutionData, NodeConnectionType, type IExecuteFunctions, type INodeType, @@ -10,14 +12,11 @@ import { WORKFLOW_INPUTS, JSON_EXAMPLE, VALUES, - INPUT_OPTIONS, TYPE_OPTIONS, PASSTHROUGH, + FALLBACK_DEFAULT_VALUE, } from '../../../utils/workflowInputsResourceMapping/constants'; -import { - getFieldEntries, - getWorkflowInputData, -} from '../../../utils/workflowInputsResourceMapping/GenericFunctions'; +import { getFieldEntries } from '../../../utils/workflowInputsResourceMapping/GenericFunctions'; export class ExecuteWorkflowTrigger implements INodeType { description: INodeTypeDescription = { @@ -66,6 +65,23 @@ export class ExecuteWorkflowTrigger implements INodeType { ], default: 'worklfow_call', }, + { + displayName: + "When an ‘execute workflow’ node calls this workflow, the execution starts here. Any data passed into the 'execute workflow' node will be output by this node.", + name: 'notice', + type: 'notice', + default: '', + displayOptions: { + show: { '@version': [{ _cnd: { eq: 1 } }] }, + }, + }, + { + displayName: 'This node is out of date. Please upgrade by removing it and adding a new one', + name: 'outdatedVersionWarning', + type: 'notice', + displayOptions: { show: { '@version': [{ _cnd: { eq: 1 } }] } }, + default: '', + }, { displayName: 'Input Source', name: INPUT_SOURCE, @@ -166,39 +182,6 @@ export class ExecuteWorkflowTrigger implements INodeType { }, ], }, - { - displayName: 'Input Options', - name: INPUT_OPTIONS, - placeholder: 'Options', - type: 'collection', - description: 'Options controlling how input data is handled, converted and rejected', - displayOptions: { - show: { '@version': [{ _cnd: { gte: 1.1 } }] }, - }, - default: {}, - // Note that, while the defaults are true, the user has to add these in the first place - // We default to false if absent in the execute function below - options: [ - { - displayName: 'Attempt to Convert Types', - name: 'attemptToConvertTypes', - type: 'boolean', - default: true, - description: - 'Whether to attempt conversion on type mismatch, rather than directly returning an Error', - noDataExpression: true, - }, - { - displayName: 'Ignore Type Mismatch Errors', - name: 'ignoreTypeErrors', - type: 'boolean', - default: true, - description: - 'Whether type mismatches should be ignored, rather than returning an Error', - noDataExpression: true, - }, - ], - }, ], }; @@ -206,12 +189,33 @@ export class ExecuteWorkflowTrigger implements INodeType { const inputData = this.getInputData(); const inputSource = this.getNodeParameter(INPUT_SOURCE, 0, PASSTHROUGH) as string; + // Note on the data we receive from ExecuteWorkflow caller: + // + // The ExecuteWorkflow node typechecks all fields explicitly provided by the user here via the resourceMapper + // and removes all fields that are in the schema, but `removed` in the resourceMapper. + // + // In passthrough and legacy node versions, inputData will line up since the resourceMapper is empty, + // in which case all input is passed through. + // In other cases we will already have matching types and fields provided by the resource mapper, + // so we just need to be permissive on this end, + // while ensuring we provide default values for fields in our schema, which are removed in the resourceMapper. + if (inputSource === PASSTHROUGH) { return [inputData]; } else { const newParams = getFieldEntries(this); + const newKeys = new Set(newParams.map((x) => x.name)); + const itemsInSchema: INodeExecutionData[] = inputData.map((row, index) => ({ + json: { + ...Object.fromEntries(newParams.map((x) => [x.name, FALLBACK_DEFAULT_VALUE])), + // Need to trim to the expected schema to support legacy Execute Workflow callers passing through all their data + // which we do not want to expose past this node. + ..._.pickBy(row.json, (_value, key) => newKeys.has(key)), + }, + index, + })); - return [getWorkflowInputData.call(this, inputData, newParams)]; + return [itemsInSchema]; } } } diff --git a/packages/nodes-base/utils/workflowInputsResourceMapping/.readme b/packages/nodes-base/utils/workflowInputsResourceMapping/.readme index d92560581d2fe..e5556cc0cccaf 100644 --- a/packages/nodes-base/utils/workflowInputsResourceMapping/.readme +++ b/packages/nodes-base/utils/workflowInputsResourceMapping/.readme @@ -2,4 +2,4 @@ These files contain reusable logic for workflow inputs mapping used in these nod - n8n-nodes-base.executeWorkflow - n8n-nodes-base.executeWorkflowTrigger - - @n8n/n8n-nodes-langchain.toolWorkflow" + - @n8n/n8n-nodes-langchain.toolWorkflow diff --git a/packages/nodes-base/utils/workflowInputsResourceMapping/GenericFunctions.ts b/packages/nodes-base/utils/workflowInputsResourceMapping/GenericFunctions.ts index 88aa5e971cf87..8708b8e18aa8a 100644 --- a/packages/nodes-base/utils/workflowInputsResourceMapping/GenericFunctions.ts +++ b/packages/nodes-base/utils/workflowInputsResourceMapping/GenericFunctions.ts @@ -1,14 +1,20 @@ import { json as generateSchemaFromExample, type SchemaObject } from 'generate-schema'; import type { JSONSchema7 } from 'json-schema'; -import type { - FieldValueOption, - FieldType, - IWorkflowNodeContext, - INodeExecutionData, - IExecuteFunctions, - ISupplyDataFunctions, +import _ from 'lodash'; +import { + type FieldValueOption, + type FieldType, + type IWorkflowNodeContext, + jsonParse, + NodeOperationError, + type INodeExecutionData, + type IDataObject, + type ResourceMapperField, + type ILocalLoadOptionsFunctions, + type ResourceMapperFields, + EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE, + type ISupplyDataFunctions, } from 'n8n-workflow'; -import { jsonParse, NodeOperationError, validateFieldType } from 'n8n-workflow'; import { JSON_EXAMPLE, @@ -16,8 +22,6 @@ import { WORKFLOW_INPUTS, VALUES, TYPE_OPTIONS, - INPUT_OPTIONS, - FALLBACK_DEFAULT_VALUE, PASSTHROUGH, } from './constants'; @@ -92,84 +96,74 @@ export function getFieldEntries(context: IWorkflowNodeContext): FieldValueOption throw new NodeOperationError(context.getNode(), result); } -export function getWorkflowInputData( - this: IExecuteFunctions | ISupplyDataFunctions, - inputData: INodeExecutionData[], - newParams: FieldValueOption[], -): INodeExecutionData[] { - const items: INodeExecutionData[] = []; +export function getWorkflowInputValues(this: ISupplyDataFunctions): INodeExecutionData[] { + const inputData = this.getInputData(); - for (const [itemIndex, item] of inputData.entries()) { - const attemptToConvertTypes = this.getNodeParameter( - `${INPUT_OPTIONS}.attemptToConvertTypes`, - itemIndex, - false, - ); - const ignoreTypeErrors = this.getNodeParameter( - `${INPUT_OPTIONS}.ignoreTypeErrors`, + return inputData.map((item, itemIndex) => { + const itemFieldValues = this.getNodeParameter( + 'workflowInputs.value', itemIndex, - false, - ); - - // Fields listed here will explicitly overwrite original fields - const newItem: INodeExecutionData = { - json: {}, + {}, + ) as IDataObject; + + return { + json: { + ...item.json, + ...itemFieldValues, + }, index: itemIndex, - // TODO: Ensure we handle sub-execution jumps correctly. - // metadata: { - // subExecution: { - // executionId: 'uhh', - // workflowId: 'maybe?', - // }, - // }, - pairedItem: { item: itemIndex }, + pairedItem: { + item: itemIndex, + }, }; - try { - for (const { name, type } of newParams) { - if (!item.json.hasOwnProperty(name)) { - newItem.json[name] = FALLBACK_DEFAULT_VALUE; - continue; - } - - const result = - type === 'any' - ? ({ valid: true, newValue: item.json[name] } as const) - : validateFieldType(name, item.json[name], type, { - strict: !attemptToConvertTypes, - parseStrings: true, // Default behavior is to accept anything as a string, this is a good opportunity for a stricter boundary - }); - - if (!result.valid) { - if (ignoreTypeErrors) { - newItem.json[name] = item.json[name]; - continue; - } - - throw new NodeOperationError(this.getNode(), result.errorMessage, { - itemIndex, - }); - } else { - // If the value is `null` or `undefined`, then `newValue` is not in the returned object - if (result.hasOwnProperty('newValue')) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - newItem.json[name] = result.newValue; - } else { - newItem.json[name] = item.json[name]; - } - } - } + }); +} - items.push(newItem); - } catch (error) { - if (this.continueOnFail()) { - /** todo error case? */ - } else { - throw new NodeOperationError(this.getNode(), error, { - itemIndex, - }); - } - } +export function getCurrentWorkflowInputData(this: ISupplyDataFunctions) { + const inputData: INodeExecutionData[] = getWorkflowInputValues.call(this); + + const schema = this.getNodeParameter('workflowInputs.schema', 0, []) as ResourceMapperField[]; + + if (schema.length === 0) { + return inputData; + } else { + const removedKeys = new Set(schema.filter((x) => x.removed).map((x) => x.displayName)); + + const filteredInputData: INodeExecutionData[] = inputData.map((item, index) => ({ + index, + pairedItem: { item: index }, + json: _.pickBy(item.json, (_v, key) => !removedKeys.has(key)), + })); + + return filteredInputData; } +} - return items; +export async function loadWorkflowInputMappings( + this: ILocalLoadOptionsFunctions, +): Promise { + const nodeLoadContext = await this.getWorkflowNodeContext(EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE); + let fields: ResourceMapperField[] = []; + + if (nodeLoadContext) { + const fieldValues = getFieldEntries(nodeLoadContext); + + fields = fieldValues.map((currentWorkflowInput) => { + const field: ResourceMapperField = { + id: currentWorkflowInput.name, + displayName: currentWorkflowInput.name, + required: false, + defaultMatch: false, + display: true, + canBeUsedToMatch: true, + }; + + if (currentWorkflowInput.type !== 'any') { + field.type = currentWorkflowInput.type; + } + + return field; + }); + } + return { fields }; } diff --git a/packages/nodes-base/utils/workflowInputsResourceMapping/constants.ts b/packages/nodes-base/utils/workflowInputsResourceMapping/constants.ts index 69bb41af7397f..409d8d703ecab 100644 --- a/packages/nodes-base/utils/workflowInputsResourceMapping/constants.ts +++ b/packages/nodes-base/utils/workflowInputsResourceMapping/constants.ts @@ -2,7 +2,6 @@ import type { FieldType } from 'n8n-workflow'; export const INPUT_SOURCE = 'inputSource'; export const WORKFLOW_INPUTS = 'workflowInputs'; -export const INPUT_OPTIONS = 'inputOptions'; export const VALUES = 'values'; export const JSON_EXAMPLE = 'jsonExample'; export const PASSTHROUGH = 'passthrough'; diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index 8689999a6b2a7..4fe84136c303a 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -1328,6 +1328,7 @@ export interface ResourceMapperTypeOptionsBase { description?: string; hint?: string; }; + showTypeConversionOptions?: boolean; } // Enforce at least one of resourceMapperMethod or localResourceMapperMethod @@ -2665,6 +2666,9 @@ export type ResourceMapperValue = { value: { [key: string]: string | number | boolean | null } | null; matchingColumns: string[]; schema: ResourceMapperField[]; + ignoreTypeMismatchErrors: boolean; + attemptToConvertTypes: boolean; + convertFieldsToString: boolean; }; export type FilterOperatorType = diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ba2200020b469..955d19b18e3ae 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -283,7 +283,7 @@ importers: version: 4.0.7 axios: specifier: 'catalog:' - version: 1.7.4(debug@4.3.7) + version: 1.7.4 dotenv: specifier: 8.6.0 version: 8.6.0 @@ -354,7 +354,7 @@ importers: dependencies: axios: specifier: 'catalog:' - version: 1.7.4(debug@4.3.7) + version: 1.7.4 packages/@n8n/codemirror-lang: dependencies: @@ -428,7 +428,7 @@ importers: version: 3.666.0(@aws-sdk/client-sts@3.666.0) '@getzep/zep-cloud': specifier: 1.0.12 - version: 1.0.12(@langchain/core@0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)))(encoding@0.1.13)(langchain@0.3.6(4axcxpjbcq5bce7ff6ajxrpp4i)) + version: 1.0.12(@langchain/core@0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)))(encoding@0.1.13)(langchain@0.3.6(e4rnrwhosnp2xiru36mqgdy2bu)) '@getzep/zep-js': specifier: 0.9.0 version: 0.9.0 @@ -455,7 +455,7 @@ importers: version: 0.3.1(@aws-sdk/client-sso-oidc@3.666.0(@aws-sdk/client-sts@3.666.0))(@langchain/core@0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)))(encoding@0.1.13) '@langchain/community': specifier: 0.3.15 - version: 0.3.15(v4qhcw25bevfr6xzz4fnsvjiqe) + version: 0.3.15(vc5hvyy27o4cmm4jplsptc2fqm) '@langchain/core': specifier: 'catalog:' version: 0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)) @@ -542,7 +542,7 @@ importers: version: 23.0.1 langchain: specifier: 0.3.6 - version: 0.3.6(4axcxpjbcq5bce7ff6ajxrpp4i) + version: 0.3.6(e4rnrwhosnp2xiru36mqgdy2bu) lodash: specifier: 'catalog:' version: 4.17.21 @@ -801,7 +801,7 @@ importers: version: 1.11.0 axios: specifier: 'catalog:' - version: 1.7.4(debug@4.3.7) + version: 1.7.4 bcryptjs: specifier: 2.4.3 version: 2.4.3 @@ -1120,7 +1120,7 @@ importers: dependencies: '@langchain/core': specifier: 'catalog:' - version: 0.3.19(openai@4.73.1(zod@3.23.8)) + version: 0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)) '@n8n/client-oauth2': specifier: workspace:* version: link:../@n8n/client-oauth2 @@ -1135,7 +1135,7 @@ importers: version: 1.11.0 axios: specifier: 'catalog:' - version: 1.7.4(debug@4.3.7) + version: 1.7.4 chardet: specifier: 2.0.0 version: 2.0.0 @@ -1431,7 +1431,7 @@ importers: version: 10.11.0(vue@3.5.13(typescript@5.7.2)) axios: specifier: 'catalog:' - version: 1.7.4(debug@4.3.7) + version: 1.7.4 bowser: specifier: 2.11.0 version: 2.11.0 @@ -1932,7 +1932,7 @@ importers: version: 0.15.2 axios: specifier: 'catalog:' - version: 1.7.4(debug@4.3.7) + version: 1.7.4 callsites: specifier: 3.1.0 version: 3.1.0 @@ -1978,7 +1978,7 @@ importers: devDependencies: '@langchain/core': specifier: 'catalog:' - version: 0.3.19(openai@4.73.1) + version: 0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)) '@types/deep-equal': specifier: ^1.0.1 version: 1.0.1 @@ -15666,7 +15666,7 @@ snapshots: '@gar/promisify@1.1.3': optional: true - '@getzep/zep-cloud@1.0.12(@langchain/core@0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)))(encoding@0.1.13)(langchain@0.3.6(4axcxpjbcq5bce7ff6ajxrpp4i))': + '@getzep/zep-cloud@1.0.12(@langchain/core@0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)))(encoding@0.1.13)(langchain@0.3.6(e4rnrwhosnp2xiru36mqgdy2bu))': dependencies: form-data: 4.0.0 node-fetch: 2.7.0(encoding@0.1.13) @@ -15675,7 +15675,7 @@ snapshots: zod: 3.23.8 optionalDependencies: '@langchain/core': 0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)) - langchain: 0.3.6(4axcxpjbcq5bce7ff6ajxrpp4i) + langchain: 0.3.6(e4rnrwhosnp2xiru36mqgdy2bu) transitivePeerDependencies: - encoding @@ -16139,7 +16139,7 @@ snapshots: - aws-crt - encoding - '@langchain/community@0.3.15(v4qhcw25bevfr6xzz4fnsvjiqe)': + '@langchain/community@0.3.15(vc5hvyy27o4cmm4jplsptc2fqm)': dependencies: '@ibm-cloud/watsonx-ai': 1.1.2 '@langchain/core': 0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)) @@ -16149,7 +16149,7 @@ snapshots: flat: 5.0.2 ibm-cloud-sdk-core: 5.1.0 js-yaml: 4.1.0 - langchain: 0.3.6(4axcxpjbcq5bce7ff6ajxrpp4i) + langchain: 0.3.6(e4rnrwhosnp2xiru36mqgdy2bu) langsmith: 0.2.3(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)) uuid: 10.0.0 zod: 3.23.8 @@ -16162,7 +16162,7 @@ snapshots: '@aws-sdk/client-s3': 3.666.0 '@aws-sdk/credential-provider-node': 3.666.0(@aws-sdk/client-sso-oidc@3.666.0(@aws-sdk/client-sts@3.666.0))(@aws-sdk/client-sts@3.666.0) '@azure/storage-blob': 12.18.0(encoding@0.1.13) - '@getzep/zep-cloud': 1.0.12(@langchain/core@0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)))(encoding@0.1.13)(langchain@0.3.6(4axcxpjbcq5bce7ff6ajxrpp4i)) + '@getzep/zep-cloud': 1.0.12(@langchain/core@0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)))(encoding@0.1.13)(langchain@0.3.6(e4rnrwhosnp2xiru36mqgdy2bu)) '@getzep/zep-js': 0.9.0 '@google-ai/generativelanguage': 2.6.0(encoding@0.1.13) '@google-cloud/storage': 7.12.1(encoding@0.1.13) @@ -16226,38 +16226,6 @@ snapshots: transitivePeerDependencies: - openai - '@langchain/core@0.3.19(openai@4.73.1(zod@3.23.8))': - dependencies: - ansi-styles: 5.2.0 - camelcase: 6.3.0 - decamelize: 1.2.0 - js-tiktoken: 1.0.12 - langsmith: 0.2.3(openai@4.73.1(zod@3.23.8)) - mustache: 4.2.0 - p-queue: 6.6.2 - p-retry: 4.6.2 - uuid: 10.0.0 - zod: 3.23.8 - zod-to-json-schema: 3.23.3(zod@3.23.8) - transitivePeerDependencies: - - openai - - '@langchain/core@0.3.19(openai@4.73.1)': - dependencies: - ansi-styles: 5.2.0 - camelcase: 6.3.0 - decamelize: 1.2.0 - js-tiktoken: 1.0.12 - langsmith: 0.2.3(openai@4.73.1) - mustache: 4.2.0 - p-queue: 6.6.2 - p-retry: 4.6.2 - uuid: 10.0.0 - zod: 3.23.8 - zod-to-json-schema: 3.23.3(zod@3.23.8) - transitivePeerDependencies: - - openai - '@langchain/google-common@0.1.3(@langchain/core@0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)))(zod@3.23.8)': dependencies: '@langchain/core': 0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)) @@ -17173,7 +17141,7 @@ snapshots: '@rudderstack/rudder-sdk-node@2.0.9(tslib@2.6.2)': dependencies: - axios: 1.7.4(debug@4.3.7) + axios: 1.7.4 axios-retry: 3.7.0 component-type: 1.2.1 join-component: 1.1.0 @@ -19470,7 +19438,7 @@ snapshots: '@babel/runtime': 7.24.7 is-retry-allowed: 2.2.0 - axios@1.7.4(debug@4.3.7): + axios@1.7.4: dependencies: follow-redirects: 1.15.6(debug@4.3.6) form-data: 4.0.0 @@ -19478,9 +19446,9 @@ snapshots: transitivePeerDependencies: - debug - axios@1.7.7: + axios@1.7.4(debug@4.3.7): dependencies: - follow-redirects: 1.15.6(debug@4.3.6) + follow-redirects: 1.15.6(debug@4.3.7) form-data: 4.0.0 proxy-from-env: 1.1.0 transitivePeerDependencies: @@ -22347,7 +22315,7 @@ snapshots: isstream: 0.1.2 jsonwebtoken: 9.0.2 mime-types: 2.1.35 - retry-axios: 2.6.0(axios@1.7.4(debug@4.3.7)) + retry-axios: 2.6.0(axios@1.7.4) tough-cookie: 4.1.3 transitivePeerDependencies: - supports-color @@ -23346,7 +23314,7 @@ snapshots: kuler@2.0.0: {} - langchain@0.3.6(4axcxpjbcq5bce7ff6ajxrpp4i): + langchain@0.3.6(e4rnrwhosnp2xiru36mqgdy2bu): dependencies: '@langchain/core': 0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)) '@langchain/openai': 0.3.14(@langchain/core@0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)))(encoding@0.1.13) @@ -23370,7 +23338,7 @@ snapshots: '@langchain/groq': 0.1.2(@langchain/core@0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)))(encoding@0.1.13) '@langchain/mistralai': 0.2.0(@langchain/core@0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8))) '@langchain/ollama': 0.1.2(@langchain/core@0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8))) - axios: 1.7.4(debug@4.3.7) + axios: 1.7.4 cheerio: 1.0.0 handlebars: 4.7.8 transitivePeerDependencies: @@ -23389,28 +23357,6 @@ snapshots: optionalDependencies: openai: 4.73.1(encoding@0.1.13)(zod@3.23.8) - langsmith@0.2.3(openai@4.73.1(zod@3.23.8)): - dependencies: - '@types/uuid': 10.0.0 - commander: 10.0.1 - p-queue: 6.6.2 - p-retry: 4.6.2 - semver: 7.6.0 - uuid: 10.0.0 - optionalDependencies: - openai: 4.73.1(zod@3.23.8) - - langsmith@0.2.3(openai@4.73.1): - dependencies: - '@types/uuid': 10.0.0 - commander: 10.0.1 - p-queue: 6.6.2 - p-retry: 4.6.2 - semver: 7.6.0 - uuid: 10.0.0 - optionalDependencies: - openai: 4.73.1(zod@3.23.8) - lazy-ass@1.6.0: {} ldapts@4.2.6: @@ -24743,22 +24689,6 @@ snapshots: - encoding - supports-color - openai@4.73.1(zod@3.23.8): - dependencies: - '@types/node': 18.16.16 - '@types/node-fetch': 2.6.4 - abort-controller: 3.0.0 - agentkeepalive: 4.2.1 - form-data-encoder: 1.7.2 - formdata-node: 4.4.1 - node-fetch: 2.7.0(encoding@0.1.13) - optionalDependencies: - zod: 3.23.8 - transitivePeerDependencies: - - encoding - - supports-color - optional: true - openapi-sampler@1.5.1: dependencies: '@types/json-schema': 7.0.15 @@ -25754,9 +25684,9 @@ snapshots: ret@0.1.15: {} - retry-axios@2.6.0(axios@1.7.4(debug@4.3.7)): + retry-axios@2.6.0(axios@1.7.4): dependencies: - axios: 1.7.4(debug@4.3.7) + axios: 1.7.4 retry-request@7.0.2(encoding@0.1.13): dependencies: