Skip to content

Commit

Permalink
Merge branch 'master' of https://github.com/n8n-io/n8n into node-1863-…
Browse files Browse the repository at this point in the history
…input-fields-dragging
  • Loading branch information
michael-radency committed Nov 27, 2024
2 parents 6cf7309 + 9cc3b21 commit b26ccb0
Show file tree
Hide file tree
Showing 136 changed files with 6,664 additions and 2,505 deletions.
12 changes: 6 additions & 6 deletions cypress/e2e/17-sharing.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ describe('Sharing', { disableAutoLogin: true }, () => {

cy.visit(workflowsPage.url);
workflowsPage.getters.workflowCards().should('have.length', 1);
workflowsPage.getters.workflowCard('Workflow W1').click();
workflowsPage.getters.workflowCardContent('Workflow W1').click();
workflowPage.actions.addNodeToCanvas('Airtable', true, true);
ndv.getters.credentialInput().find('input').should('have.value', 'Credential C2');
ndv.actions.close();
Expand All @@ -104,7 +104,7 @@ describe('Sharing', { disableAutoLogin: true }, () => {

cy.visit(workflowsPage.url);
workflowsPage.getters.workflowCards().should('have.length', 2);
workflowsPage.getters.workflowCard('Workflow W1').click();
workflowsPage.getters.workflowCardContent('Workflow W1').click();
workflowPage.actions.addNodeToCanvas('Airtable', true, true);
ndv.getters.credentialInput().find('input').should('have.value', 'Credential C2');
ndv.actions.close();
Expand Down Expand Up @@ -133,7 +133,7 @@ describe('Sharing', { disableAutoLogin: true }, () => {

cy.visit(workflowsPage.url);
workflowsPage.getters.workflowCards().should('have.length', 2);
workflowsPage.getters.workflowCard('Workflow W1').click();
workflowsPage.getters.workflowCardContent('Workflow W1').click();
workflowPage.actions.openNode('Notion');
ndv.getters
.credentialInput()
Expand All @@ -144,7 +144,7 @@ describe('Sharing', { disableAutoLogin: true }, () => {

cy.waitForLoad();
cy.visit(workflowsPage.url);
workflowsPage.getters.workflowCard('Workflow W2').click('top');
workflowsPage.getters.workflowCardContent('Workflow W2').click('top');
workflowPage.actions.executeWorkflow();
});

Expand Down Expand Up @@ -353,7 +353,7 @@ describe('Credential Usage in Cross Shared Workflows', () => {
credentialsPage.getters.emptyListCreateCredentialButton().click();
credentialsModal.actions.createNewCredential('Notion API');
cy.visit(workflowsPage.url);
workflowsPage.getters.workflowCard(workflowName).click();
workflowsPage.getters.workflowCardContent(workflowName).click();
workflowPage.actions.addNodeToCanvas(NOTION_NODE_NAME, true, true);

// Only the own credential the shared one (+ the 'Create new' option)
Expand Down Expand Up @@ -398,7 +398,7 @@ describe('Credential Usage in Cross Shared Workflows', () => {
credentialsPage.getters.createCredentialButton().click();
credentialsModal.actions.createNewCredential('Notion API');
cy.visit(workflowsPage.url);
workflowsPage.getters.workflowCard(workflowName).click();
workflowsPage.getters.workflowCardContent(workflowName).click();
workflowPage.actions.addNodeToCanvas(NOTION_NODE_NAME, true, true);

// Only the personal credentials of the workflow owner and the global owner
Expand Down
4 changes: 2 additions & 2 deletions cypress/e2e/39-projects.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ describe('Projects', { disableAutoLogin: true }, () => {
menuItems.filter(':contains("Development")[class*=active_]').should('exist');

cy.intercept('GET', '/rest/workflows/*').as('loadWorkflow');
workflowsPage.getters.workflowCards().first().click();
workflowsPage.getters.workflowCards().first().findChildByTestId('card-content').click();

cy.wait('@loadWorkflow');
menuItems = cy.getByTestId('menu-item');
Expand Down Expand Up @@ -747,7 +747,7 @@ describe('Projects', { disableAutoLogin: true }, () => {

// Open the moved workflow
workflowsPage.getters.workflowCards().should('have.length', 1);
workflowsPage.getters.workflowCards().first().click();
workflowsPage.getters.workflowCards().first().findChildByTestId('card-content').click();

// Check if the credential can be changed
workflowPage.getters.canvasNodeByName(NOTION_NODE_NAME).should('be.visible').dblclick();
Expand Down
2 changes: 2 additions & 0 deletions cypress/pages/workflows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ export class WorkflowsPage extends BasePage {
.parents('[data-test-id="resources-list-item"]'),
workflowTags: (workflowName: string) =>
this.getters.workflowCard(workflowName).findChildByTestId('workflow-card-tags'),
workflowCardContent: (workflowName: string) =>
this.getters.workflowCard(workflowName).findChildByTestId('card-content'),
workflowActivator: (workflowName: string) =>
this.getters.workflowCard(workflowName).findChildByTestId('workflow-card-activator'),
workflowActivatorStatus: (workflowName: string) =>
Expand Down
7 changes: 6 additions & 1 deletion cypress/support/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,13 @@ Cypress.Commands.add('signin', ({ email, password }) => {
.then((response) => {
Cypress.env('currentUserId', response.body.data.id);

// @TODO Remove this once the switcher is removed
cy.window().then((win) => {
win.localStorage.setItem('NodeView.switcher.discovered', 'true'); // @TODO Remove this once the switcher is removed
win.localStorage.setItem('NodeView.migrated', 'true');
win.localStorage.setItem('NodeView.switcher.discovered.beta', 'true');

const nodeViewVersion = Cypress.env('NODE_VIEW_VERSION');
win.localStorage.setItem('NodeView.version', nodeViewVersion ?? '1');
});
});
});
Expand Down
5 changes: 0 additions & 5 deletions cypress/support/e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,6 @@ beforeEach(() => {
win.localStorage.setItem('N8N_THEME', 'light');
win.localStorage.setItem('N8N_AUTOCOMPLETE_ONBOARDED', 'true');
win.localStorage.setItem('N8N_MAPPING_ONBOARDED', 'true');

const nodeViewVersion = Cypress.env('NODE_VIEW_VERSION');
if (nodeViewVersion) {
win.localStorage.setItem('NodeView.version', nodeViewVersion);
}
});

cy.intercept('GET', '/rest/settings', (req) => {
Expand Down
2 changes: 1 addition & 1 deletion docker/images/n8n-custom/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ COPY docker/images/n8n/docker-entrypoint.sh /

# Setup the Task Runner Launcher
ARG TARGETPLATFORM
ARG LAUNCHER_VERSION=0.3.0-rc
ARG LAUNCHER_VERSION=0.4.0-rc
COPY docker/images/n8n/n8n-task-runners.json /etc/n8n-task-runners.json
# Download, verify, then extract the launcher binary
RUN \
Expand Down
2 changes: 1 addition & 1 deletion docker/images/n8n/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ RUN set -eux; \

# Setup the Task Runner Launcher
ARG TARGETPLATFORM
ARG LAUNCHER_VERSION=0.3.0-rc
ARG LAUNCHER_VERSION=0.4.0-rc
COPY n8n-task-runners.json /etc/n8n-task-runners.json
# Download, verify, then extract the launcher binary
RUN \
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@
"semver": "^7.5.4",
"tslib": "^2.6.2",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.6.2",
"typescript": "^5.7.2",
"vue-tsc": "^2.1.6",
"ws": ">=8.17.1"
},
Expand All @@ -89,7 +89,8 @@
"[email protected]": "patches/[email protected]",
"@types/[email protected]": "patches/@[email protected]",
"@types/[email protected]": "patches/@[email protected]",
"@types/[email protected]": "patches/@[email protected]"
"@types/[email protected]": "patches/@[email protected]",
"[email protected]": "patches/[email protected]"
}
}
}
1 change: 1 addition & 0 deletions packages/@n8n/api-types/src/push/execution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ type ExecutionStarted = {
workflowId: string;
workflowName?: string;
retryOf?: string;
flattedRunData: string;
};
};

Expand Down
4 changes: 0 additions & 4 deletions packages/@n8n/config/src/configs/runners.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,6 @@ export class TaskRunnersConfig {
@Env('N8N_RUNNERS_MAX_CONCURRENCY')
maxConcurrency: number = 5;

/** Should the output of deduplication be asserted for correctness */
@Env('N8N_RUNNERS_ASSERT_DEDUPLICATION_OUTPUT')
assertDeduplicationOutput: boolean = false;

/** How long (in seconds) a task is allowed to take for completion, else the task will be aborted and the runner restarted. Must be greater than 0. */
@Env('N8N_RUNNERS_TASK_TIMEOUT')
taskTimeout: number = 60;
Expand Down
1 change: 0 additions & 1 deletion packages/@n8n/config/test/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,6 @@ describe('GlobalConfig', () => {
port: 5679,
maxOldSpaceSize: '',
maxConcurrency: 5,
assertDeduplicationOutput: false,
taskTimeout: 60,
heartbeatInterval: 30,
},
Expand Down
2 changes: 1 addition & 1 deletion packages/@n8n/task-runner/src/config/base-runner-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class HealthcheckServerConfig {
host: string = '127.0.0.1';

@Env('N8N_RUNNERS_SERVER_PORT')
port: number = 5680;
port: number = 5681;
}

@Config
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { mock } from 'jest-mock-extended';
import type {
IExecuteData,
INode,
INodeExecutionData,
ITaskDataConnectionsSource,
} from 'n8n-workflow';

import type { DataRequestResponse, InputDataChunkDefinition } from '@/runner-types';

import { DataRequestResponseReconstruct } from '../data-request-response-reconstruct';

describe('DataRequestResponseReconstruct', () => {
const reconstruct = new DataRequestResponseReconstruct();

describe('reconstructConnectionInputItems', () => {
it('should return all input items if no chunk is provided', () => {
const inputData: DataRequestResponse['inputData'] = {
main: [[{ json: { key: 'value' } }]],
};

const result = reconstruct.reconstructConnectionInputItems(inputData);

expect(result).toEqual([{ json: { key: 'value' } }]);
});

it('should reconstruct sparse array when chunk is provided', () => {
const inputData: DataRequestResponse['inputData'] = {
main: [[{ json: { key: 'chunked' } }]],
};
const chunk: InputDataChunkDefinition = { startIndex: 2, count: 1 };

const result = reconstruct.reconstructConnectionInputItems(inputData, chunk);

expect(result).toEqual([undefined, undefined, { json: { key: 'chunked' } }, undefined]);
});

it('should handle empty input data gracefully', () => {
const inputData: DataRequestResponse['inputData'] = { main: [[]] };
const chunk: InputDataChunkDefinition = { startIndex: 1, count: 1 };

const result = reconstruct.reconstructConnectionInputItems(inputData, chunk);

expect(result).toEqual([undefined]);
});
});

describe('reconstructExecuteData', () => {
it('should reconstruct execute data with the provided input items', () => {
const node = mock<INode>();
const connectionInputSource = mock<ITaskDataConnectionsSource>();
const response = mock<DataRequestResponse>({
inputData: { main: [[]] },
node,
connectionInputSource,
});
const inputItems: INodeExecutionData[] = [{ json: { key: 'reconstructed' } }];

const result = reconstruct.reconstructExecuteData(response, inputItems);

expect(result).toEqual<IExecuteData>({
data: {
main: [inputItems],
},
node: response.node,
source: response.connectionInputSource,
});
});

it('should handle empty input items gracefully', () => {
const node = mock<INode>();
const connectionInputSource = mock<ITaskDataConnectionsSource>();
const inputItems: INodeExecutionData[] = [];
const response = mock<DataRequestResponse>({
inputData: { main: [[{ json: { key: 'value' } }]] },
node,
connectionInputSource,
});

const result = reconstruct.reconstructExecuteData(response, inputItems);

expect(result).toEqual<IExecuteData>({
data: {
main: [inputItems],
},
node: response.node,
source: response.connectionInputSource,
});
});
});
});
Original file line number Diff line number Diff line change
@@ -1,27 +1,50 @@
import type { IExecuteData, INodeExecutionData } from 'n8n-workflow';
import type { IExecuteData, INodeExecutionData, ITaskDataConnections } from 'n8n-workflow';

import type { DataRequestResponse } from '@/runner-types';
import type { DataRequestResponse, InputDataChunkDefinition } from '@/runner-types';

/**
* Reconstructs data from a DataRequestResponse to the initial
* data structures.
*/
export class DataRequestResponseReconstruct {
/**
* Reconstructs `connectionInputData` from a DataRequestResponse
* Reconstructs `inputData` from a DataRequestResponse
*/
reconstructConnectionInputData(
reconstructConnectionInputItems(
inputData: DataRequestResponse['inputData'],
): INodeExecutionData[] {
return inputData?.main?.[0] ?? [];
chunk?: InputDataChunkDefinition,
): Array<INodeExecutionData | undefined> {
const inputItems = inputData?.main?.[0] ?? [];
if (!chunk) {
return inputItems;
}

// Only a chunk of the input items was requested. We reconstruct
// the array by filling in the missing items with `undefined`.
let sparseInputItems: Array<INodeExecutionData | undefined> = [];

sparseInputItems = sparseInputItems
.concat(Array.from({ length: chunk.startIndex }))
.concat(inputItems)
.concat(Array.from({ length: inputItems.length - chunk.startIndex - chunk.count }));

return sparseInputItems;
}

/**
* Reconstruct `executeData` from a DataRequestResponse
*/
reconstructExecuteData(response: DataRequestResponse): IExecuteData {
reconstructExecuteData(
response: DataRequestResponse,
inputItems: INodeExecutionData[],
): IExecuteData {
const inputData: ITaskDataConnections = {
...response.inputData,
main: [inputItems],
};

return {
data: response.inputData,
data: inputData,
node: response.node,
source: response.connectionInputSource,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { ExecutionError } from '@/js-task-runner/errors/execution-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 } from '@/runner-types';
import type { DataRequestResponse, InputDataChunkDefinition } from '@/runner-types';
import type { Task } from '@/task-runner';

import {
Expand Down Expand Up @@ -95,17 +95,19 @@ describe('JsTaskRunner', () => {
inputItems,
settings,
runner,
chunk,
}: {
code: string;
inputItems: IDataObject[];
settings?: Partial<JSExecSettings>;

runner?: JsTaskRunner;
chunk?: InputDataChunkDefinition;
}) => {
return await execTaskWithParams({
task: newTaskWithSettings({
code,
nodeMode: 'runOnceForEachItem',
chunk,
...settings,
}),
taskData: newDataRequestResponse(inputItems.map(wrapIntoJson)),
Expand Down Expand Up @@ -509,6 +511,28 @@ describe('JsTaskRunner', () => {
);
});

describe('chunked execution', () => {
it('should use correct index for each item', async () => {
const outcome = await executeForEachItem({
code: 'return { ...$json, idx: $itemIndex }',
inputItems: [{ a: 1 }, { b: 2 }, { c: 3 }],
chunk: {
startIndex: 100,
count: 3,
},
});

expect(outcome).toEqual({
result: [
withPairedItem(100, wrapIntoJson({ a: 1, idx: 100 })),
withPairedItem(101, wrapIntoJson({ b: 2, idx: 101 })),
withPairedItem(102, wrapIntoJson({ c: 3, idx: 102 })),
],
customData: undefined,
});
});
});

it('should return static items', async () => {
const outcome = await executeForEachItem({
code: 'return {json: {b: 1}}',
Expand Down Expand Up @@ -801,7 +825,6 @@ describe('JsTaskRunner', () => {
code: 'unknown; return []',
nodeMode: 'runOnceForAllItems',
continueOnFail: false,
mode: 'manual',
workflowMode: 'manual',
});
runner.runningTasks.set(taskId, task);
Expand Down
Loading

0 comments on commit b26ccb0

Please sign in to comment.