Skip to content

Commit 9319139

Browse files
mfsiegadespairblueclaude
authored
feat(core): Switch to structured destination node (no-changelog) (#22143)
Co-authored-by: Danny Martini <[email protected]> Co-authored-by: Claude <[email protected]>
1 parent 36b4eda commit 9319139

File tree

30 files changed

+614
-298
lines changed

30 files changed

+614
-298
lines changed

packages/@n8n/db/src/repositories/execution.repository.ts

Lines changed: 55 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,12 @@ import type {
2929
ExecutionStatus,
3030
ExecutionSummary,
3131
IRunExecutionData,
32+
IRunExecutionDataAll,
3233
} from 'n8n-workflow';
3334
import {
3435
createEmptyRunExecutionData,
3536
ManualExecutionCancelledError,
37+
migrateRunExecutionData,
3638
UnexpectedError,
3739
} from 'n8n-workflow';
3840

@@ -196,9 +198,7 @@ export class ExecutionRepository extends Repository<ExecutionEntity> {
196198
},
197199
): Promise<IExecutionFlattedDb[] | IExecutionResponse[] | IExecutionBase[]> {
198200
if (options?.includeData) {
199-
if (!queryParams.relations) {
200-
queryParams.relations = [];
201-
}
201+
queryParams.relations ??= [];
202202

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

211211
const executions = await this.find(queryParams);
212212

213-
if (options?.includeData && options?.unflattenData) {
214-
const [valid, invalid] = separate(executions, (e) => e.executionData !== null);
215-
this.reportInvalidExecutions(invalid);
216-
return valid.map((execution) => {
217-
const { executionData, metadata, ...rest } = execution;
218-
return {
219-
...rest,
220-
data: parse(executionData.data) as IRunExecutionData,
221-
workflowData: executionData.workflowData,
222-
customData: Object.fromEntries(metadata.map((m) => [m.key, m.value])),
223-
} as IExecutionResponse;
224-
});
225-
} else if (options?.includeData) {
226-
const [valid, invalid] = separate(executions, (e) => e.executionData !== null);
227-
this.reportInvalidExecutions(invalid);
228-
return valid.map((execution) => {
229-
const { executionData, metadata, ...rest } = execution;
230-
return {
231-
...rest,
232-
data: execution.executionData.data,
233-
workflowData: execution.executionData.workflowData,
234-
customData: Object.fromEntries(metadata.map((m) => [m.key, m.value])),
235-
} as IExecutionFlattedDb;
236-
});
213+
const [valid, invalid] = separate(executions, (e) => e.executionData !== null);
214+
this.reportInvalidExecutions(invalid);
215+
216+
if (!options?.includeData) {
217+
// No data to include, so we exclude it and return early.
218+
return executions.map((execution) => {
219+
const { executionData, ...rest } = execution;
220+
return rest;
221+
}) as IExecutionFlattedDb[] | IExecutionResponse[] | IExecutionBase[];
237222
}
238223

239-
return executions.map((execution) => {
240-
const { executionData, ...rest } = execution;
241-
return rest;
224+
return valid.map((execution) => {
225+
const { executionData, metadata, ...rest } = execution;
226+
const data = this.handleExecutionRunData(executionData.data, options);
227+
return {
228+
...rest,
229+
data,
230+
workflowData: executionData.workflowData,
231+
customData: Object.fromEntries(metadata.map((m) => [m.key, m.value])),
232+
};
242233
}) as IExecutionFlattedDb[] | IExecutionResponse[] | IExecutionBase[];
243234
}
244235

@@ -339,15 +330,23 @@ export class ExecutionRepository extends Repository<ExecutionEntity> {
339330
});
340331
}
341332

333+
if (!options?.includeData) {
334+
// Not including run data, so return early.
335+
const { executionData, ...rest } = execution;
336+
return {
337+
...rest,
338+
...(options?.includeAnnotation &&
339+
serializedAnnotation && { annotation: serializedAnnotation }),
340+
} as IExecutionFlattedDb | IExecutionResponse | IExecutionBase;
341+
}
342+
343+
// Include the run data.
344+
const data = this.handleExecutionRunData(executionData.data, options);
342345
return {
343346
...rest,
344-
...(options?.includeData && {
345-
data: options?.unflattenData
346-
? (parse(executionData.data) as IRunExecutionData)
347-
: executionData.data,
348-
workflowData: executionData?.workflowData,
349-
customData: Object.fromEntries(metadata.map((m) => [m.key, m.value])),
350-
}),
347+
data,
348+
workflowData: executionData.workflowData,
349+
customData: Object.fromEntries(metadata.map((m) => [m.key, m.value])),
351350
...(options?.includeAnnotation &&
352351
serializedAnnotation && { annotation: serializedAnnotation }),
353352
} as IExecutionFlattedDb | IExecutionResponse | IExecutionBase;
@@ -804,7 +803,8 @@ export class ExecutionRepository extends Repository<ExecutionEntity> {
804803
async stopDuringRun(execution: IExecutionResponse) {
805804
const error = new ManualExecutionCancelledError(execution.id);
806805

807-
execution.data ??= createEmptyRunExecutionData();
806+
execution.data = execution.data || createEmptyRunExecutionData();
807+
808808
execution.data.resultData.error = {
809809
...error,
810810
message: error.message,
@@ -1164,4 +1164,21 @@ export class ExecutionRepository extends Repository<ExecutionEntity> {
11641164

11651165
return concurrentExecutionsCount;
11661166
}
1167+
1168+
private handleExecutionRunData(
1169+
data: string,
1170+
options: { unflattenData?: boolean } = {},
1171+
): IRunExecutionData | string | undefined {
1172+
if (options.unflattenData) {
1173+
// Parse the serialized data.
1174+
const deserializedData: unknown = parse(data);
1175+
// If it parses to an object, migrate and return it.
1176+
if (deserializedData) {
1177+
return migrateRunExecutionData(deserializedData as IRunExecutionDataAll);
1178+
}
1179+
return undefined;
1180+
}
1181+
// Just return the string data as-is.
1182+
return data;
1183+
}
11671184
}

packages/@n8n/task-runner/src/js-task-runner/__tests__/js-task-runner.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -477,7 +477,7 @@ describe('JsTaskRunner', () => {
477477
});
478478

479479
const helsinkiTimeNow = DateTime.now().setZone('Europe/Helsinki').toSeconds();
480-
expect(outcome.result).toEqual({ val: expect.closeTo(helsinkiTimeNow, 1) });
480+
expect(outcome.result).toEqual({ val: expect.closeTo(helsinkiTimeNow, 0) });
481481
});
482482
});
483483

packages/cli/src/__tests__/manual-execution.service.test.ts

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import type {
1313
IWaitingForExecution,
1414
IWaitingForExecutionSource,
1515
INodeExecutionData,
16+
IDestinationNode,
1617
} from 'n8n-workflow';
1718
import type PCancelable from 'p-cancelable';
1819

@@ -190,6 +191,10 @@ describe('ManualExecutionService', () => {
190191
const startNodeName = 'startNode';
191192
const triggerNodeName = 'triggerNode';
192193
const destinationNodeName = 'destinationNode';
194+
const destinationNode: IDestinationNode = {
195+
nodeName: destinationNodeName,
196+
mode: 'inclusive',
197+
};
193198

194199
const data = mock<IWorkflowExecutionDataProcess>({
195200
triggerToStartFrom: {
@@ -198,7 +203,7 @@ describe('ManualExecutionService', () => {
198203
},
199204
startNodes: [{ name: startNodeName }],
200205
executionMode: 'manual',
201-
destinationNode: destinationNodeName,
206+
destinationNode,
202207
});
203208

204209
const startNode = mock<INode>({ name: startNodeName });
@@ -225,9 +230,7 @@ describe('ManualExecutionService', () => {
225230
additionalData,
226231
data.executionMode,
227232
expect.objectContaining({
228-
startData: {
229-
destinationNode: destinationNodeName,
230-
},
233+
startData: { destinationNode },
231234
resultData: expect.any(Object),
232235
executionData: expect.any(Object),
233236
}),
@@ -379,12 +382,16 @@ describe('ManualExecutionService', () => {
379382
const mockRunData = { node1: [{ data: { main: [[{ json: {} }]] } }] };
380383
const dirtyNodeNames = ['node2', 'node3'];
381384
const destinationNodeName = 'destinationNode';
385+
const destinationNode: IDestinationNode = {
386+
nodeName: destinationNodeName,
387+
mode: 'inclusive',
388+
};
382389
const data = mock<IWorkflowExecutionDataProcess>({
383390
executionMode: 'manual',
384391
runData: mockRunData,
385392
startNodes: [{ name: 'node1' }],
386393
dirtyNodeNames,
387-
destinationNode: destinationNodeName,
394+
destinationNode,
388395
});
389396

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

412419
expect(mockRunPartialWorkflow2).toHaveBeenCalled();
413420
expect(mockRunPartialWorkflow2.mock.calls[0][0]).toBe(workflow);
414-
expect(mockRunPartialWorkflow2.mock.calls[0][4]).toBe(destinationNodeName);
421+
expect(mockRunPartialWorkflow2.mock.calls[0][4]).toEqual(destinationNode);
415422
});
416423

417424
it('should validate nodes exist before execution', async () => {
@@ -489,11 +496,15 @@ describe('ManualExecutionService', () => {
489496
it('should call runPartialWorkflow2 with runData and empty startNodes', async () => {
490497
const mockRunData = { nodeA: [{ data: { main: [[{ json: { value: 'test' } }]] } }] };
491498
const destinationNodeName = 'nodeB';
499+
const destinationNode: IDestinationNode = {
500+
nodeName: destinationNodeName,
501+
mode: 'inclusive',
502+
};
492503
const data = mock<IWorkflowExecutionDataProcess>({
493504
executionMode: 'manual',
494505
runData: mockRunData,
495506
startNodes: [],
496-
destinationNode: destinationNodeName,
507+
destinationNode,
497508
pinData: {},
498509
dirtyNodeNames: [],
499510
agentRequest: undefined,
@@ -526,7 +537,7 @@ describe('ManualExecutionService', () => {
526537
mockRunData,
527538
data.pinData,
528539
data.dirtyNodeNames,
529-
destinationNodeName,
540+
destinationNode,
530541
data.agentRequest,
531542
);
532543
});

packages/cli/src/evaluation.ee/test-runner/__tests__/test-runner.service.ee.test.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -458,7 +458,10 @@ describe('TestRunnerService', () => {
458458
const runCallArg = workflowRunner.run.mock.calls[0][0];
459459

460460
// Verify it has the correct structure
461-
expect(runCallArg).toHaveProperty('destinationNode', triggerNodeName);
461+
expect(runCallArg).toHaveProperty('destinationNode', {
462+
nodeName: triggerNodeName,
463+
mode: 'inclusive',
464+
});
462465
expect(runCallArg).toHaveProperty('executionMode', 'manual');
463466
expect(runCallArg).toHaveProperty('workflowData.settings.saveManualExecutions', false);
464467
expect(runCallArg).toHaveProperty('workflowData.settings.saveDataErrorExecution', 'none');
@@ -531,7 +534,10 @@ describe('TestRunnerService', () => {
531534
const runCallArg = workflowRunner.run.mock.calls[0][0];
532535

533536
// Verify it has the correct structure
534-
expect(runCallArg).toHaveProperty('destinationNode', triggerNodeName);
537+
expect(runCallArg).toHaveProperty('destinationNode', {
538+
nodeName: triggerNodeName,
539+
mode: 'inclusive',
540+
});
535541
expect(runCallArg).toHaveProperty('executionMode', 'manual');
536542
expect(runCallArg).toHaveProperty('workflowData.settings.saveManualExecutions', false);
537543
expect(runCallArg).toHaveProperty('workflowData.settings.saveDataErrorExecution', 'none');
@@ -546,7 +552,10 @@ describe('TestRunnerService', () => {
546552
// But executionData itself should still exist with startData and manualData
547553
expect(runCallArg).toHaveProperty('executionData');
548554
expect(runCallArg.executionData).toBeDefined();
549-
expect(runCallArg).toHaveProperty('executionData.startData.destinationNode', triggerNodeName);
555+
expect(runCallArg).toHaveProperty('executionData.startData.destinationNode', {
556+
nodeName: triggerNodeName,
557+
mode: 'inclusive',
558+
});
550559
expect(runCallArg).toHaveProperty('executionData.manualData.userId', metadata.userId);
551560
expect(runCallArg).toHaveProperty(
552561
'executionData.manualData.triggerToStartFrom.name',

packages/cli/src/evaluation.ee/test-runner/test-runner.service.ee.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -318,7 +318,7 @@ export class TestRunnerService {
318318
};
319319

320320
const data: IWorkflowExecutionDataProcess = {
321-
destinationNode: triggerNode.name,
321+
destinationNode: { nodeName: triggerNode.name, mode: 'inclusive' },
322322
executionMode: 'manual',
323323
runData: {},
324324
workflowData: {
@@ -334,7 +334,7 @@ export class TestRunnerService {
334334
userId: metadata.userId,
335335
executionData: createRunExecutionData({
336336
startData: {
337-
destinationNode: triggerNode.name,
337+
destinationNode: { nodeName: triggerNode.name, mode: 'inclusive' },
338338
},
339339
manualData: {
340340
userId: metadata.userId,

0 commit comments

Comments
 (0)