mirror of
https://github.com/n8n-io/n8n.git
synced 2026-05-29 15:57:00 +02:00
feat(core): Inject hooks into applicable trigger node properties for the node UI (#22290)
This commit is contained in:
parent
80ee4490fa
commit
92dca5f739
|
|
@ -4,6 +4,7 @@ import type {
|
|||
INodeExecutionData,
|
||||
PlaintextExecutionContext,
|
||||
IWorkflowBase,
|
||||
INodeProperties,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
/**
|
||||
|
|
@ -137,7 +138,17 @@ export type ContextEstablishmentResult = {
|
|||
* @ContextEstablishmentHook()
|
||||
* export class BearerTokenHook implements IContextEstablishmentHook {
|
||||
* hookDescription = {
|
||||
* name: 'credentials.bearerToken'
|
||||
* name: 'credentials.bearerToken',
|
||||
* displayName: 'Bearer Token',
|
||||
* options: [
|
||||
* {
|
||||
* displayName: 'Remove from Item',
|
||||
* name: 'removeFromItem',
|
||||
* type: 'boolean',
|
||||
* default: true,
|
||||
* description: 'Whether to remove the Authorization header from the trigger item'
|
||||
* }
|
||||
* ]
|
||||
* };
|
||||
*
|
||||
* // ... hook implementation
|
||||
|
|
@ -171,6 +182,45 @@ export type HookDescription = {
|
|||
* @example 'audit.requestMetadata'
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* Human-readable display name for the hook.
|
||||
* Used in the UI when presenting the hook selection to users.
|
||||
* If not provided, the name will be used as the display name.
|
||||
*
|
||||
* @example 'Bearer Token Authentication'
|
||||
* @example 'API Key from Header'
|
||||
*/
|
||||
displayName?: string;
|
||||
|
||||
/**
|
||||
* Hook-specific configuration options that will be exposed in the trigger node UI.
|
||||
* These options are passed to the hook's execute() method via the options parameter.
|
||||
*
|
||||
* Each option should be a valid node property object with at minimum:
|
||||
* displayName, name, type, and default fields.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* options: [
|
||||
* {
|
||||
* displayName: 'Remove from Item',
|
||||
* name: 'removeFromItem',
|
||||
* type: 'boolean',
|
||||
* default: true,
|
||||
* description: 'Whether to remove the Authorization header from trigger items'
|
||||
* },
|
||||
* {
|
||||
* displayName: 'Header Name',
|
||||
* name: 'headerName',
|
||||
* type: 'string',
|
||||
* default: 'Authorization',
|
||||
* description: 'The name of the header containing the bearer token'
|
||||
* }
|
||||
* ]
|
||||
* ```
|
||||
*/
|
||||
options?: INodeProperties[];
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import { Service } from '@n8n/di';
|
||||
import watcher from '@parcel/watcher';
|
||||
import fs from 'fs/promises';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import type { DirectoryLoader } from 'n8n-core';
|
||||
import type { INodeProperties, INodeTypeDescription } from 'n8n-workflow';
|
||||
import { NodeConnectionTypes } from 'n8n-workflow';
|
||||
import watcher from '@parcel/watcher';
|
||||
|
||||
import { LoadNodesAndCredentials } from '../load-nodes-and-credentials';
|
||||
import { Service } from '@n8n/di';
|
||||
|
||||
jest.mock('lodash/debounce', () => (fn: () => void) => fn);
|
||||
|
||||
|
|
@ -30,7 +30,7 @@ describe('LoadNodesAndCredentials', () => {
|
|||
let instance: LoadNodesAndCredentials;
|
||||
|
||||
beforeEach(() => {
|
||||
instance = new LoadNodesAndCredentials(mock(), mock(), mock(), mock(), mock());
|
||||
instance = new LoadNodesAndCredentials(mock(), mock(), mock(), mock(), mock(), mock());
|
||||
instance.loaders.package1 = mock<DirectoryLoader>({
|
||||
directory: '/icons/package1',
|
||||
});
|
||||
|
|
@ -58,7 +58,7 @@ describe('LoadNodesAndCredentials', () => {
|
|||
});
|
||||
|
||||
describe('convertNodeToAiTool', () => {
|
||||
const instance = new LoadNodesAndCredentials(mock(), mock(), mock(), mock(), mock());
|
||||
const instance = new LoadNodesAndCredentials(mock(), mock(), mock(), mock(), mock(), mock());
|
||||
|
||||
let fullNodeWrapper: { description: INodeTypeDescription };
|
||||
|
||||
|
|
@ -290,7 +290,7 @@ describe('LoadNodesAndCredentials', () => {
|
|||
let instance: LoadNodesAndCredentials;
|
||||
|
||||
beforeEach(() => {
|
||||
instance = new LoadNodesAndCredentials(mock(), mock(), mock(), mock(), mock());
|
||||
instance = new LoadNodesAndCredentials(mock(), mock(), mock(), mock(), mock(), mock());
|
||||
instance.knownNodes['n8n-nodes-base.test'] = {
|
||||
className: 'Test',
|
||||
sourcePath: '/nodes-base/dist/nodes/Test/Test.node.js',
|
||||
|
|
@ -330,7 +330,7 @@ describe('LoadNodesAndCredentials', () => {
|
|||
let instance: LoadNodesAndCredentials;
|
||||
|
||||
beforeEach(() => {
|
||||
instance = new LoadNodesAndCredentials(mock(), mock(), mock(), mock(), mock());
|
||||
instance = new LoadNodesAndCredentials(mock(), mock(), mock(), mock(), mock(), mock());
|
||||
instance.types.nodes = [
|
||||
{
|
||||
name: 'testNode',
|
||||
|
|
@ -447,7 +447,7 @@ describe('LoadNodesAndCredentials', () => {
|
|||
let instance: LoadNodesAndCredentials;
|
||||
|
||||
beforeEach(() => {
|
||||
instance = new LoadNodesAndCredentials(mock(), mock(), mock(), mock(), mock());
|
||||
instance = new LoadNodesAndCredentials(mock(), mock(), mock(), mock(), mock(), mock());
|
||||
});
|
||||
it('should return true for credentials with authenticate property', () => {
|
||||
const credential = {
|
||||
|
|
@ -572,6 +572,257 @@ describe('LoadNodesAndCredentials', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('injectContextEstablishmentHooks', () => {
|
||||
let instance: LoadNodesAndCredentials;
|
||||
let mockLogger: { debug: jest.Mock };
|
||||
let mockExecutionContextHookRegistry: { getHookForTriggerType: jest.Mock };
|
||||
|
||||
beforeEach(() => {
|
||||
// Enable the feature flag for tests
|
||||
process.env.N8N_ENV_FEAT_CONTEXT_ESTABLISHMENT_HOOKS = 'true';
|
||||
|
||||
mockLogger = {
|
||||
debug: jest.fn(),
|
||||
};
|
||||
mockExecutionContextHookRegistry = {
|
||||
getHookForTriggerType: jest.fn(),
|
||||
};
|
||||
instance = new LoadNodesAndCredentials(
|
||||
mockLogger as never,
|
||||
mock(),
|
||||
mock(),
|
||||
mock(),
|
||||
mock(),
|
||||
mockExecutionContextHookRegistry as never,
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Clean up the environment variable after each test
|
||||
delete process.env.N8N_ENV_FEAT_CONTEXT_ESTABLISHMENT_HOOKS;
|
||||
});
|
||||
|
||||
it('should not inject hooks when feature flag is disabled', () => {
|
||||
// Disable the feature flag
|
||||
delete process.env.N8N_ENV_FEAT_CONTEXT_ESTABLISHMENT_HOOKS;
|
||||
|
||||
const triggerNode: INodeTypeDescription = {
|
||||
name: 'webhookTrigger',
|
||||
displayName: 'Webhook Trigger',
|
||||
group: ['trigger'],
|
||||
description: 'A webhook trigger',
|
||||
version: 1,
|
||||
defaults: {},
|
||||
inputs: [],
|
||||
outputs: ['main'],
|
||||
properties: [],
|
||||
};
|
||||
|
||||
instance.types.nodes = [triggerNode];
|
||||
|
||||
const mockHook = {
|
||||
hookDescription: {
|
||||
name: 'credentials.bearerToken',
|
||||
displayName: 'Bearer Token',
|
||||
options: [],
|
||||
},
|
||||
};
|
||||
|
||||
mockExecutionContextHookRegistry.getHookForTriggerType.mockReturnValue([mockHook]);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(instance as any).injectContextEstablishmentHooks();
|
||||
|
||||
expect(triggerNode.properties).toHaveLength(0);
|
||||
expect(mockLogger.debug).toHaveBeenCalledWith(
|
||||
'Context establishment hooks feature is disabled',
|
||||
);
|
||||
|
||||
// Re-enable for other tests
|
||||
process.env.N8N_ENV_FEAT_CONTEXT_ESTABLISHMENT_HOOKS = 'true';
|
||||
});
|
||||
|
||||
it('should not inject hooks if no hooks exist', () => {
|
||||
const triggerNode: INodeTypeDescription = {
|
||||
name: 'manualTrigger',
|
||||
displayName: 'Manual Trigger',
|
||||
group: ['trigger'],
|
||||
description: 'A manual trigger',
|
||||
version: 1,
|
||||
defaults: {},
|
||||
inputs: [],
|
||||
outputs: ['main'],
|
||||
properties: [],
|
||||
};
|
||||
|
||||
instance.types.nodes = [triggerNode];
|
||||
|
||||
mockExecutionContextHookRegistry.getHookForTriggerType.mockReturnValue([]);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(instance as any).injectContextEstablishmentHooks();
|
||||
|
||||
expect(triggerNode.properties).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should not inject hooks when isApplicableToTriggerNode returns false', () => {
|
||||
const manualTriggerNode: INodeTypeDescription = {
|
||||
name: 'manualTrigger',
|
||||
displayName: 'Manual Trigger',
|
||||
group: ['trigger'],
|
||||
description: 'A manual trigger',
|
||||
version: 1,
|
||||
defaults: {},
|
||||
inputs: [],
|
||||
outputs: ['main'],
|
||||
properties: [],
|
||||
};
|
||||
|
||||
instance.types.nodes = [manualTriggerNode];
|
||||
|
||||
const mockNonApplicableHook = {
|
||||
hookDescription: {
|
||||
name: 'credentials.bearerToken',
|
||||
displayName: 'Bearer Token',
|
||||
options: [
|
||||
{
|
||||
displayName: 'Remove from Item',
|
||||
name: 'removeFromItem',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
isApplicableToTriggerNode: jest.fn((nodeType: string) => {
|
||||
// Only applicable to webhook trigger
|
||||
return nodeType === 'webhookTrigger';
|
||||
}),
|
||||
};
|
||||
|
||||
// Mock getHookForTriggerType to simulate real filtering behavior
|
||||
mockExecutionContextHookRegistry.getHookForTriggerType.mockImplementation((nodeType) => {
|
||||
const allHooks = [mockNonApplicableHook];
|
||||
// Filter hooks based on isApplicableToTriggerNode
|
||||
return allHooks.filter((hook) => hook.isApplicableToTriggerNode(nodeType));
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(instance as any).injectContextEstablishmentHooks();
|
||||
|
||||
// Verify isApplicableToTriggerNode was called, but no properties added
|
||||
expect(mockNonApplicableHook.isApplicableToTriggerNode).toHaveBeenCalledWith('manualTrigger');
|
||||
expect(manualTriggerNode.properties).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should inject hooks with multiple hook types and options', () => {
|
||||
const triggerNode: INodeTypeDescription = {
|
||||
name: 'webhookTrigger',
|
||||
displayName: 'Webhook Trigger',
|
||||
group: ['trigger'],
|
||||
description: 'A webhook trigger',
|
||||
version: 1,
|
||||
defaults: {},
|
||||
inputs: [],
|
||||
outputs: ['main'],
|
||||
properties: [],
|
||||
};
|
||||
|
||||
instance.types.nodes = [triggerNode];
|
||||
|
||||
// Hook 1: With 2 options (one with existing displayOptions)
|
||||
const mockHookWithOptions = {
|
||||
hookDescription: {
|
||||
name: 'credentials.bearerToken',
|
||||
displayName: 'Bearer Token',
|
||||
options: [
|
||||
{
|
||||
displayName: 'Remove from Item',
|
||||
name: 'removeFromItem',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
displayName: 'Advanced Option',
|
||||
name: 'advancedOption',
|
||||
type: 'string',
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
someOtherField: ['value'],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
// Hook 2: Without options
|
||||
const mockHookWithoutOptions = {
|
||||
hookDescription: {
|
||||
name: 'credentials.apiKey',
|
||||
displayName: 'API Key',
|
||||
options: [],
|
||||
},
|
||||
};
|
||||
|
||||
mockExecutionContextHookRegistry.getHookForTriggerType.mockReturnValue([
|
||||
mockHookWithOptions,
|
||||
mockHookWithoutOptions,
|
||||
]);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(instance as any).injectContextEstablishmentHooks();
|
||||
|
||||
// Verify three properties are injected
|
||||
expect(triggerNode.properties).toHaveLength(3);
|
||||
expect(triggerNode.properties[0].name).toBe('executionsHooksVersion');
|
||||
expect(triggerNode.properties[0].type).toBe('hidden');
|
||||
expect(triggerNode.properties[1].name).toBe('contextEstablishmentHooks');
|
||||
expect(triggerNode.properties[1].type).toBe('fixedCollection');
|
||||
expect(triggerNode.properties[2].name).toBe('contextHooksNotice');
|
||||
expect(triggerNode.properties[2].type).toBe('notice');
|
||||
|
||||
// Verify the hook collection structure
|
||||
const hookProperty = triggerNode.properties[1];
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const hookValues = (hookProperty as any).options?.[0]?.values as INodeProperties[];
|
||||
|
||||
// Should have: hookName selector + isAllowedToFail + 2 options from first hook = 4 values
|
||||
expect(hookValues).toHaveLength(4);
|
||||
|
||||
// Verify hookName selector with both hook options
|
||||
expect(hookValues[0].name).toBe('hookName');
|
||||
expect(hookValues[0].type).toBe('options');
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const hookNameOptions = (hookValues[0] as any).options;
|
||||
expect(hookNameOptions).toHaveLength(2);
|
||||
expect(hookNameOptions[0].value).toBe('credentials.bearerToken');
|
||||
expect(hookNameOptions[1].value).toBe('credentials.apiKey');
|
||||
|
||||
// Verify isAllowedToFail field
|
||||
expect(hookValues[1].name).toBe('isAllowedToFail');
|
||||
expect(hookValues[1].type).toBe('boolean');
|
||||
expect(hookValues[1].default).toBe(false);
|
||||
|
||||
// Verify first option has display condition
|
||||
expect(hookValues[2].name).toBe('removeFromItem');
|
||||
expect(hookValues[2].displayOptions).toEqual({
|
||||
show: {
|
||||
hookName: ['credentials.bearerToken'],
|
||||
},
|
||||
});
|
||||
|
||||
// Verify second option merges existing displayOptions with hookName
|
||||
expect(hookValues[3].name).toBe('advancedOption');
|
||||
expect(hookValues[3].displayOptions).toEqual({
|
||||
show: {
|
||||
someOtherField: ['value'],
|
||||
hookName: ['credentials.bearerToken'],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('setupHotReload', () => {
|
||||
let instance: LoadNodesAndCredentials;
|
||||
|
||||
|
|
@ -584,7 +835,7 @@ describe('LoadNodesAndCredentials', () => {
|
|||
});
|
||||
|
||||
beforeEach(() => {
|
||||
instance = new LoadNodesAndCredentials(mock(), mock(), mock(), mock(), mock());
|
||||
instance = new LoadNodesAndCredentials(mock(), mock(), mock(), mock(), mock(), mock());
|
||||
instance.loaders = { CUSTOM: mockLoader };
|
||||
|
||||
// Allow access to directory
|
||||
|
|
|
|||
|
|
@ -98,6 +98,8 @@ export abstract class BaseCommand<F = never> {
|
|||
process.once('SIGINT', this.onTerminationSignal('SIGINT'));
|
||||
|
||||
this.nodeTypes = Container.get(NodeTypes);
|
||||
|
||||
await this.executionContextHookRegistry.init();
|
||||
await Container.get(LoadNodesAndCredentials).init();
|
||||
|
||||
await this.dbConnection
|
||||
|
|
|
|||
|
|
@ -262,8 +262,6 @@ export class Start extends BaseCommand<z.infer<typeof flagsSchema>> {
|
|||
|
||||
Container.get(MultiMainSetup).registerEventHandlers();
|
||||
}
|
||||
|
||||
await this.executionContextHookRegistry.init();
|
||||
}
|
||||
|
||||
async initOrchestration() {
|
||||
|
|
|
|||
|
|
@ -115,8 +115,6 @@ export class Worker extends BaseCommand<z.infer<typeof flagsSchema>> {
|
|||
);
|
||||
|
||||
await this.moduleRegistry.initModules(this.instanceSettings.instanceType);
|
||||
|
||||
await this.executionContextHookRegistry.init();
|
||||
}
|
||||
|
||||
async initEventBus() {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import {
|
|||
LazyPackageDirectoryLoader,
|
||||
UnrecognizedCredentialTypeError,
|
||||
UnrecognizedNodeTypeError,
|
||||
ExecutionContextHookRegistry,
|
||||
} from 'n8n-core';
|
||||
import type {
|
||||
KnownNodesAndCredentials,
|
||||
|
|
@ -57,6 +58,7 @@ export class LoadNodesAndCredentials {
|
|||
private readonly instanceSettings: InstanceSettings,
|
||||
private readonly globalConfig: GlobalConfig,
|
||||
private readonly moduleRegistry: ModuleRegistry,
|
||||
private readonly executionContextHookRegistry: ExecutionContextHookRegistry,
|
||||
) {}
|
||||
|
||||
async init() {
|
||||
|
|
@ -279,6 +281,128 @@ export class LoadNodesAndCredentials {
|
|||
});
|
||||
}
|
||||
|
||||
private injectContextEstablishmentHooks() {
|
||||
// Check if the feature is enabled via environment variable
|
||||
const isEnabled = process.env.N8N_ENV_FEAT_CONTEXT_ESTABLISHMENT_HOOKS === 'true';
|
||||
|
||||
if (!isEnabled) {
|
||||
this.logger.debug('Context establishment hooks feature is disabled');
|
||||
return;
|
||||
}
|
||||
|
||||
const triggerNodes = this.types.nodes.filter((node: INodeTypeDescription) =>
|
||||
node.group.includes('trigger'),
|
||||
);
|
||||
|
||||
this.logger.debug(
|
||||
`Injecting context establishment hooks for ${triggerNodes.length} trigger nodes`,
|
||||
);
|
||||
|
||||
triggerNodes.forEach((node: INodeTypeDescription) => {
|
||||
const hooks = this.executionContextHookRegistry.getHookForTriggerType(node.name);
|
||||
|
||||
if (hooks.length > 0) {
|
||||
this.logger.debug(`Found ${hooks.length} hooks for trigger node: ${node.name}`);
|
||||
}
|
||||
|
||||
// Only inject hook properties if there are applicable hooks
|
||||
if (hooks.length === 0) return;
|
||||
|
||||
// Create a fixedCollection with multipleValues for multiple hook selection
|
||||
// Each hook becomes a separate item that can be added multiple times
|
||||
const allHookValues: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Hook',
|
||||
name: 'hookName',
|
||||
type: 'options',
|
||||
options: hooks.map((hook) => {
|
||||
const displayName = hook.hookDescription.displayName ?? hook.hookDescription.name;
|
||||
return {
|
||||
name: displayName,
|
||||
value: hook.hookDescription.name,
|
||||
description: `Use ${displayName} hook`,
|
||||
};
|
||||
}),
|
||||
// No default - force user to explicitly select a hook
|
||||
// This ensures hookName is always serialized in the workflow JSON
|
||||
default: '',
|
||||
description: 'Select which context establishment hook to use',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
displayName: 'Allow Failure',
|
||||
name: 'isAllowedToFail',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Whether to continue workflow execution if this hook fails',
|
||||
},
|
||||
];
|
||||
|
||||
// Add all hook-specific options with display conditions
|
||||
for (const hook of hooks) {
|
||||
const hookOptions = hook.hookDescription.options ?? [];
|
||||
if (hookOptions.length > 0) {
|
||||
for (const hookOption of hookOptions) {
|
||||
// Add display condition to show only when this specific hook is selected
|
||||
const enhancedOption: INodeProperties = {
|
||||
...hookOption,
|
||||
displayOptions: {
|
||||
...hookOption.displayOptions,
|
||||
show: {
|
||||
...hookOption.displayOptions?.show,
|
||||
hookName: [hook.hookDescription.name],
|
||||
},
|
||||
},
|
||||
};
|
||||
allHookValues.push(enhancedOption);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create a hidden version property to track the hooks format version
|
||||
const executionsHooksVersion: INodeProperties = {
|
||||
displayName: 'Executions Hooks Version',
|
||||
name: 'executionsHooksVersion',
|
||||
type: 'hidden',
|
||||
default: 1,
|
||||
};
|
||||
|
||||
// Create the main context establishment hooks property as a fixedCollection
|
||||
const contextHooksProperty: INodeProperties = {
|
||||
displayName: 'Context Establishment Hooks',
|
||||
name: 'contextEstablishmentHooks',
|
||||
type: 'fixedCollection',
|
||||
placeholder: 'Add Hook',
|
||||
default: {},
|
||||
typeOptions: {
|
||||
multipleValues: true,
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'hooks',
|
||||
displayName: 'Hooks',
|
||||
values: allHookValues,
|
||||
},
|
||||
],
|
||||
description:
|
||||
'Add and configure context establishment hooks to extract data from trigger items. <a href="https://docs.n8n.io/integrations/builtin/core-nodes/hooks/" target="_blank">Learn more</a>',
|
||||
};
|
||||
|
||||
// Create a notice that always appears after the hooks collection
|
||||
const contextHooksNotice: INodeProperties = {
|
||||
displayName:
|
||||
'Context establishment hooks allow you to extract data from trigger items to use in subsequent nodes. <a href="https://docs.n8n.io/integrations/builtin/core-nodes/hooks/" target="_blank">Learn more</a>',
|
||||
name: 'contextHooksNotice',
|
||||
type: 'notice',
|
||||
default: '',
|
||||
};
|
||||
|
||||
node.properties.push(executionsHooksVersion);
|
||||
node.properties.push(contextHooksProperty);
|
||||
node.properties.push(contextHooksNotice);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a loader of source files of nodes and credentials in a directory.
|
||||
*/
|
||||
|
|
@ -415,6 +539,8 @@ export class LoadNodesAndCredentials {
|
|||
|
||||
this.injectCustomApiCallOptions();
|
||||
|
||||
this.injectContextEstablishmentHooks();
|
||||
|
||||
for (const postProcessor of this.postProcessors) {
|
||||
await postProcessor();
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user