n8n/packages/core/src/execution-engine/execution-context.service.ts
2025-12-02 09:05:50 +01:00

149 lines
4.6 KiB
TypeScript

import { Logger } from '@n8n/backend-common';
import { Service } from '@n8n/di';
import {
IExecuteData,
IExecutionContext,
INodeExecutionData,
PlaintextExecutionContext,
toCredentialContext,
toExecutionContextEstablishmentHookParameter,
Workflow,
} from 'n8n-workflow';
import { Cipher } from '@/encryption';
import { deepMerge } from '@/utils/deep-merge';
import { ExecutionContextHookRegistry } from './execution-context-hook-registry.service';
@Service()
export class ExecutionContextService {
constructor(
private readonly logger: Logger,
private readonly executionContextHookRegistry: ExecutionContextHookRegistry,
private readonly cipher: Cipher,
) {}
decryptExecutionContext(context: IExecutionContext): PlaintextExecutionContext {
let credentials = undefined;
if (context.credentials) {
const decrypted = this.cipher.decrypt(context.credentials);
credentials = toCredentialContext(decrypted);
}
return {
...context,
credentials,
};
}
encryptExecutionContext(context: PlaintextExecutionContext): IExecutionContext {
let credentials = undefined;
if (context.credentials) {
credentials = this.cipher.encrypt(context.credentials);
}
return {
...context,
credentials,
};
}
mergeExecutionContexts(
baseContext: PlaintextExecutionContext,
contextToMerge: Partial<PlaintextExecutionContext>,
): PlaintextExecutionContext {
return deepMerge(baseContext, contextToMerge);
}
// startItem is mutated to reflect any changes to trigger items made by the hooks
async augmentExecutionContextWithHooks(
workflow: Workflow,
startItem: IExecuteData,
contextToAugment: IExecutionContext,
): Promise<{
context: IExecutionContext;
triggerItems: INodeExecutionData[] | null;
}> {
// Main input data is an array of items, each item represents an event that triggers the workflow execution
// The 'main' selector selects the input name of the nodes, and the 0 index represents the runIndex,
// 0 being the first run of this node in the workflow.
let currentTriggerItems = startItem.data['main'][0];
const contextEstablishmentHookParameters = {
...(workflow.getNode(startItem.node.name)?.parameters ?? {}),
...startItem.node.parameters,
};
const startNodeParametersResult = toExecutionContextEstablishmentHookParameter(
contextEstablishmentHookParameters,
);
if (!startNodeParametersResult || startNodeParametersResult.error) {
if (startNodeParametersResult?.error) {
this.logger.warn(
`Failed to parse execution context establishment hook parameters for node ${startItem.node.name}: ${startNodeParametersResult.error.message}`,
);
}
// no execution establishment hooks found, we just return the original context
return {
context: contextToAugment,
triggerItems: currentTriggerItems,
};
}
// startNodeParameters will hold the parameters of the start node
// this can be the settings for the different hooks to be executed
// for example to extract the bearer token from the start node data.
const startNodeParameters = startNodeParametersResult.data;
// decrypt the context to work with plaintext data
let context = this.decryptExecutionContext(contextToAugment);
// based on startNodeParameters, startNodeType and currentTriggerItems we can now
// iterate over the different hooks to extract specific data for the runtime context
for (const hookParameters of startNodeParameters.contextEstablishmentHooks.hooks) {
const hook = this.executionContextHookRegistry.getHookByName(hookParameters.hookName);
if (!hook) {
this.logger.warn(
`Execution context establishment hook ${hookParameters.hookName} not found, skipping this hook`,
);
continue;
}
try {
// call the hook to let it modify the context and/or the main input data
const result = await hook.execute({
triggerNode: startItem.node,
workflow,
triggerItems: currentTriggerItems,
context,
options: hookParameters,
});
if (result.triggerItems !== undefined) {
// Update trigger items in case they were modified by the hook
currentTriggerItems = result.triggerItems;
}
if (result.contextUpdate) {
// Merge any returned context fields into the execution context
context = this.mergeExecutionContexts(context, result.contextUpdate);
}
} catch (error) {
this.logger.warn(
`Failed to execute context establishment hook ${hookParameters.hookName}`,
{ error },
);
if (!hookParameters.isAllowedToFail) {
// If the hook is not allowed to fail, rethrow the error
throw error;
}
}
}
return {
context: this.encryptExecutionContext(context),
triggerItems: currentTriggerItems,
};
}
}