Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
3a1b89c
switch to structured destination node
mfsiega Nov 21, 2025
3cbf6b5
test fixes
mfsiega Nov 21, 2025
a08dcf4
another test update
mfsiega Nov 21, 2025
0475b55
more test fixes
mfsiega Nov 21, 2025
d4db7c5
migrate v0 to v1 RunExecutionData at the repository layer
mfsiega Nov 21, 2025
813513f
remove console log
mfsiega Nov 21, 2025
6954d2f
better test name
mfsiega Nov 21, 2025
db58367
small cleanup
mfsiega Nov 21, 2025
7299fe5
test fixes
mfsiega Nov 21, 2025
a066e56
undo debug loggin
mfsiega Nov 21, 2025
b23178f
remove console logs
mfsiega Nov 21, 2025
c97b305
remove test.only
mfsiega Nov 21, 2025
4bee97a
test fix
mfsiega Nov 21, 2025
0c7222d
more fixes
mfsiega Nov 21, 2025
5bd6cc6
remove extra console log
mfsiega Nov 23, 2025
3f1a3e5
fix test data
mfsiega Nov 23, 2025
261048d
Merge branch 'master' into cat-1265-dont-run-destination-node5
mfsiega Nov 23, 2025
c031684
test fix (#22192)
mfsiega Nov 23, 2025
acd3d2a
handle serialized undefined properly
mfsiega Nov 24, 2025
6c2adaf
clean up handling of execution run data
mfsiega Nov 24, 2025
e2492cb
Merge branch 'master' into cat-1265-dont-run-destination-node5
mfsiega Nov 24, 2025
9fa728f
revert stringified representation of undefined
mfsiega Nov 24, 2025
22ce9ce
Merge branch 'master' into cat-1265-dont-run-destination-node5
mfsiega Nov 24, 2025
d679d03
docs: add docstring to IDestinationNode.mode
despairblue Nov 24, 2025
a3eb84f
refactor: simplify migrateRunExecutionData
despairblue Nov 24, 2025
fda9225
refactor: clean up handleExecutionRunData
despairblue Nov 24, 2025
1af3876
refactor: remove unnecessary type annotation in workflow-execution.se…
despairblue Nov 24, 2025
2be7f86
chore: add TODO for destinationNode type migration
despairblue Nov 24, 2025
c3dbad5
test: use createErrorExecutionData factory in telemetry tests
despairblue Nov 24, 2025
6c96dd2
test: add coverage for destination node exclusive/inclusive modes
despairblue Nov 24, 2025
26a992d
fixup! refactor: remove unnecessary type annotation in workflow-execu…
despairblue Nov 24, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 55 additions & 38 deletions packages/@n8n/db/src/repositories/execution.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,12 @@ import type {
ExecutionStatus,
ExecutionSummary,
IRunExecutionData,
IRunExecutionDataAll,
} from 'n8n-workflow';
import {
createEmptyRunExecutionData,
ManualExecutionCancelledError,
migrateRunExecutionData,
UnexpectedError,
} from 'n8n-workflow';

Expand Down Expand Up @@ -196,9 +198,7 @@ export class ExecutionRepository extends Repository<ExecutionEntity> {
},
): Promise<IExecutionFlattedDb[] | IExecutionResponse[] | IExecutionBase[]> {
if (options?.includeData) {
if (!queryParams.relations) {
queryParams.relations = [];
}
queryParams.relations ??= [];

if (Array.isArray(queryParams.relations)) {
queryParams.relations.push('executionData', 'metadata');
Expand All @@ -210,35 +210,26 @@ export class ExecutionRepository extends Repository<ExecutionEntity> {

const executions = await this.find(queryParams);

if (options?.includeData && options?.unflattenData) {
const [valid, invalid] = separate(executions, (e) => e.executionData !== null);
this.reportInvalidExecutions(invalid);
return valid.map((execution) => {
const { executionData, metadata, ...rest } = execution;
return {
...rest,
data: parse(executionData.data) as IRunExecutionData,
workflowData: executionData.workflowData,
customData: Object.fromEntries(metadata.map((m) => [m.key, m.value])),
} as IExecutionResponse;
});
} else if (options?.includeData) {
const [valid, invalid] = separate(executions, (e) => e.executionData !== null);
this.reportInvalidExecutions(invalid);
return valid.map((execution) => {
const { executionData, metadata, ...rest } = execution;
return {
...rest,
data: execution.executionData.data,
workflowData: execution.executionData.workflowData,
customData: Object.fromEntries(metadata.map((m) => [m.key, m.value])),
} as IExecutionFlattedDb;
});
const [valid, invalid] = separate(executions, (e) => e.executionData !== null);
this.reportInvalidExecutions(invalid);

if (!options?.includeData) {
// No data to include, so we exclude it and return early.
return executions.map((execution) => {
const { executionData, ...rest } = execution;
return rest;
}) as IExecutionFlattedDb[] | IExecutionResponse[] | IExecutionBase[];
}

return executions.map((execution) => {
const { executionData, ...rest } = execution;
return rest;
return valid.map((execution) => {
const { executionData, metadata, ...rest } = execution;
const data = this.handleExecutionRunData(executionData.data, options);
return {
...rest,
data,
workflowData: executionData.workflowData,
customData: Object.fromEntries(metadata.map((m) => [m.key, m.value])),
};
}) as IExecutionFlattedDb[] | IExecutionResponse[] | IExecutionBase[];
}

Expand Down Expand Up @@ -339,15 +330,23 @@ export class ExecutionRepository extends Repository<ExecutionEntity> {
});
}

if (!options?.includeData) {
// Not including run data, so return early.
const { executionData, ...rest } = execution;
return {
...rest,
...(options?.includeAnnotation &&
serializedAnnotation && { annotation: serializedAnnotation }),
} as IExecutionFlattedDb | IExecutionResponse | IExecutionBase;
}

// Include the run data.
const data = this.handleExecutionRunData(executionData.data, options);
return {
...rest,
...(options?.includeData && {
data: options?.unflattenData
? (parse(executionData.data) as IRunExecutionData)
: executionData.data,
workflowData: executionData?.workflowData,
customData: Object.fromEntries(metadata.map((m) => [m.key, m.value])),
}),
data,
workflowData: executionData.workflowData,
customData: Object.fromEntries(metadata.map((m) => [m.key, m.value])),
...(options?.includeAnnotation &&
serializedAnnotation && { annotation: serializedAnnotation }),
} as IExecutionFlattedDb | IExecutionResponse | IExecutionBase;
Expand Down Expand Up @@ -804,7 +803,8 @@ export class ExecutionRepository extends Repository<ExecutionEntity> {
async stopDuringRun(execution: IExecutionResponse) {
const error = new ManualExecutionCancelledError(execution.id);

execution.data ??= createEmptyRunExecutionData();
execution.data = execution.data || createEmptyRunExecutionData();

execution.data.resultData.error = {
...error,
message: error.message,
Expand Down Expand Up @@ -1164,4 +1164,21 @@ export class ExecutionRepository extends Repository<ExecutionEntity> {

return concurrentExecutionsCount;
}

private handleExecutionRunData(
data: string,
options: { unflattenData?: boolean } = {},
): IRunExecutionData | string | undefined {
if (options.unflattenData) {
// Parse the serialized data.
const deserializedData: unknown = parse(data);
// If it parses to an object, migrate and return it.
if (deserializedData) {
return migrateRunExecutionData(deserializedData as IRunExecutionDataAll);
}
return undefined;
}
// Just return the string data as-is.
return data;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -477,7 +477,7 @@ describe('JsTaskRunner', () => {
});

const helsinkiTimeNow = DateTime.now().setZone('Europe/Helsinki').toSeconds();
expect(outcome.result).toEqual({ val: expect.closeTo(helsinkiTimeNow, 1) });
expect(outcome.result).toEqual({ val: expect.closeTo(helsinkiTimeNow, 0) });
});
});

Expand Down
27 changes: 19 additions & 8 deletions packages/cli/src/__tests__/manual-execution.service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type {
IWaitingForExecution,
IWaitingForExecutionSource,
INodeExecutionData,
IDestinationNode,
} from 'n8n-workflow';
import type PCancelable from 'p-cancelable';

Expand Down Expand Up @@ -190,6 +191,10 @@ describe('ManualExecutionService', () => {
const startNodeName = 'startNode';
const triggerNodeName = 'triggerNode';
const destinationNodeName = 'destinationNode';
const destinationNode: IDestinationNode = {
nodeName: destinationNodeName,
mode: 'inclusive',
};

const data = mock<IWorkflowExecutionDataProcess>({
triggerToStartFrom: {
Expand All @@ -198,7 +203,7 @@ describe('ManualExecutionService', () => {
},
startNodes: [{ name: startNodeName }],
executionMode: 'manual',
destinationNode: destinationNodeName,
destinationNode,
});

const startNode = mock<INode>({ name: startNodeName });
Expand All @@ -225,9 +230,7 @@ describe('ManualExecutionService', () => {
additionalData,
data.executionMode,
expect.objectContaining({
startData: {
destinationNode: destinationNodeName,
},
startData: { destinationNode },
resultData: expect.any(Object),
executionData: expect.any(Object),
}),
Expand Down Expand Up @@ -379,12 +382,16 @@ describe('ManualExecutionService', () => {
const mockRunData = { node1: [{ data: { main: [[{ json: {} }]] } }] };
const dirtyNodeNames = ['node2', 'node3'];
const destinationNodeName = 'destinationNode';
const destinationNode: IDestinationNode = {
nodeName: destinationNodeName,
mode: 'inclusive',
};
const data = mock<IWorkflowExecutionDataProcess>({
executionMode: 'manual',
runData: mockRunData,
startNodes: [{ name: 'node1' }],
dirtyNodeNames,
destinationNode: destinationNodeName,
destinationNode,
});

const workflow = mock<Workflow>({
Expand All @@ -411,7 +418,7 @@ describe('ManualExecutionService', () => {

expect(mockRunPartialWorkflow2).toHaveBeenCalled();
expect(mockRunPartialWorkflow2.mock.calls[0][0]).toBe(workflow);
expect(mockRunPartialWorkflow2.mock.calls[0][4]).toBe(destinationNodeName);
expect(mockRunPartialWorkflow2.mock.calls[0][4]).toEqual(destinationNode);
});

it('should validate nodes exist before execution', async () => {
Expand Down Expand Up @@ -489,11 +496,15 @@ describe('ManualExecutionService', () => {
it('should call runPartialWorkflow2 with runData and empty startNodes', async () => {
const mockRunData = { nodeA: [{ data: { main: [[{ json: { value: 'test' } }]] } }] };
const destinationNodeName = 'nodeB';
const destinationNode: IDestinationNode = {
nodeName: destinationNodeName,
mode: 'inclusive',
};
const data = mock<IWorkflowExecutionDataProcess>({
executionMode: 'manual',
runData: mockRunData,
startNodes: [],
destinationNode: destinationNodeName,
destinationNode,
pinData: {},
dirtyNodeNames: [],
agentRequest: undefined,
Expand Down Expand Up @@ -526,7 +537,7 @@ describe('ManualExecutionService', () => {
mockRunData,
data.pinData,
data.dirtyNodeNames,
destinationNodeName,
destinationNode,
data.agentRequest,
);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -458,7 +458,10 @@ describe('TestRunnerService', () => {
const runCallArg = workflowRunner.run.mock.calls[0][0];

// Verify it has the correct structure
expect(runCallArg).toHaveProperty('destinationNode', triggerNodeName);
expect(runCallArg).toHaveProperty('destinationNode', {
nodeName: triggerNodeName,
mode: 'inclusive',
});
expect(runCallArg).toHaveProperty('executionMode', 'manual');
expect(runCallArg).toHaveProperty('workflowData.settings.saveManualExecutions', false);
expect(runCallArg).toHaveProperty('workflowData.settings.saveDataErrorExecution', 'none');
Expand Down Expand Up @@ -531,7 +534,10 @@ describe('TestRunnerService', () => {
const runCallArg = workflowRunner.run.mock.calls[0][0];

// Verify it has the correct structure
expect(runCallArg).toHaveProperty('destinationNode', triggerNodeName);
expect(runCallArg).toHaveProperty('destinationNode', {
nodeName: triggerNodeName,
mode: 'inclusive',
});
expect(runCallArg).toHaveProperty('executionMode', 'manual');
expect(runCallArg).toHaveProperty('workflowData.settings.saveManualExecutions', false);
expect(runCallArg).toHaveProperty('workflowData.settings.saveDataErrorExecution', 'none');
Expand All @@ -546,7 +552,10 @@ describe('TestRunnerService', () => {
// But executionData itself should still exist with startData and manualData
expect(runCallArg).toHaveProperty('executionData');
expect(runCallArg.executionData).toBeDefined();
expect(runCallArg).toHaveProperty('executionData.startData.destinationNode', triggerNodeName);
expect(runCallArg).toHaveProperty('executionData.startData.destinationNode', {
nodeName: triggerNodeName,
mode: 'inclusive',
});
expect(runCallArg).toHaveProperty('executionData.manualData.userId', metadata.userId);
expect(runCallArg).toHaveProperty(
'executionData.manualData.triggerToStartFrom.name',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,7 @@ export class TestRunnerService {
};

const data: IWorkflowExecutionDataProcess = {
destinationNode: triggerNode.name,
destinationNode: { nodeName: triggerNode.name, mode: 'inclusive' },
executionMode: 'manual',
runData: {},
workflowData: {
Expand All @@ -334,7 +334,7 @@ export class TestRunnerService {
userId: metadata.userId,
executionData: createRunExecutionData({
startData: {
destinationNode: triggerNode.name,
destinationNode: { nodeName: triggerNode.name, mode: 'inclusive' },
},
manualData: {
userId: metadata.userId,
Expand Down
Loading
Loading