chore: Pull the identical run-less getCredentials into the node execution context base (#38048)

Co-authored-by: n8n-cat-bot[bot] <n8n-cat-bot[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
n8n-cat-bot[bot] 2026-09-09 15:47:23 +00:00 committed by GitHub
parent e88301b65d
commit b93c6bc96e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 49 additions and 31 deletions

View File

@ -364,6 +364,38 @@ describe('NodeExecutionContext', () => {
});
});
describe('_getRunlessCredentials', () => {
it('should pass placeholder execute data that only carries the node', async () => {
const credentialDetails = { id: 'cred-runless', name: 'Runless Cred' };
const runlessNode = mock<INode>({ type: 'n8n-nodes-base.httpRequest' });
runlessNode.credentials = { someCred: credentialDetails };
const mockCredentialsHelper = {
getDecrypted: vi.fn().mockResolvedValue({ token: 'ok' }),
getCredentialsProperties: vi.fn(),
isCredentialUsableByNode: vi.fn().mockReturnValue(true),
};
const ctx = new TestContext(
workflow,
runlessNode,
mock<IWorkflowExecuteAdditionalData>({ credentialsHelper: mockCredentialsHelper }),
mode,
);
await expect(ctx['_getRunlessCredentials']('someCred')).resolves.toEqual({ token: 'ok' });
expect(mockCredentialsHelper.getDecrypted).toHaveBeenCalledWith(
expect.anything(),
credentialDetails,
'someCred',
mode,
{ data: {}, node: runlessNode, source: null },
false,
undefined,
);
});
});
describe('prepareOutputData', () => {
it('should return the input array wrapped in another array', async () => {
const outputData = [mock<INodeExecutionData>(), mock<INodeExecutionData>()];

View File

@ -1,7 +1,6 @@
import { UnexpectedError } from 'n8n-workflow';
import type {
ICredentialDataDecryptedObject,
IExecuteData,
INode,
IHookFunctions,
IWorkflowExecuteAdditionalData,
@ -37,12 +36,7 @@ export class HookContext extends NodeExecutionContext implements IHookFunctions
}
async getCredentials<T extends object = ICredentialDataDecryptedObject>(type: string) {
// No real task run backs a webhook-registration hook, so this only exists to
// surface `node` to the credentials helper (e.g. for policy checks) — `data`/
// `source` are unused.
const executeData: IExecuteData = { data: {}, node: this.node, source: null };
return await this._getCredentials<T>(type, executeData);
return await this._getRunlessCredentials<T>(type);
}
getNodeWebhookUrl(name: WebhookType): string | undefined {

View File

@ -1,7 +1,6 @@
import get from 'lodash/get';
import type {
ICredentialDataDecryptedObject,
IExecuteData,
IGetNodeParameterOptions,
INode,
ILoadOptionsFunctions,
@ -45,12 +44,7 @@ export class LoadOptionsContext extends NodeExecutionContext implements ILoadOpt
}
async getCredentials<T extends object = ICredentialDataDecryptedObject>(type: string) {
// No real task run backs design-time parameter loading, so this only exists to
// surface `node` to the credentials helper (e.g. for policy checks) — `data`/`source`
// are unused.
const executeData: IExecuteData = { data: {}, node: this.node, source: null };
return await this._getCredentials<T>(type, executeData);
return await this._getRunlessCredentials<T>(type);
}
getCurrentNodeParameter(

View File

@ -470,6 +470,18 @@ export abstract class NodeExecutionContext implements Omit<FunctionsBase, 'getCr
return decryptedDataObject as T;
}
/**
* Returns the requested decrypted credentials for a context that no real task run backs
* (a trigger, a poll, a webhook, and so on). The placeholder execute data only exists to
* surface `node` to the credentials helper (e.g. for policy checks) — `data`/`source` are
* unused.
*/
protected async _getRunlessCredentials<T extends object = ICredentialDataDecryptedObject>(
type: string,
) {
return await this._getCredentials<T>(type, { data: {}, node: this.node, source: null });
}
@Memoized
protected get additionalKeys() {
return getAdditionalKeys(this.additionalData, this.mode, this.runExecutionData);

View File

@ -3,7 +3,6 @@ import { createDeferredPromise } from '@n8n/utils/promise/deferred-promise';
import type {
ICredentialDataDecryptedObject,
IDataObject,
IExecuteData,
INode,
IPollFunctions,
IWorkflowExecuteAdditionalData,
@ -73,10 +72,6 @@ export class PollContext extends NodeExecutionContext implements IPollFunctions
}
async getCredentials<T extends object = ICredentialDataDecryptedObject>(type: string) {
// No real task run backs a poll, so this only exists to surface `node` to
// the credentials helper (e.g. for policy checks) — `data`/`source` are unused.
const executeData: IExecuteData = { data: {}, node: this.node, source: null };
return await this._getCredentials<T>(type, executeData);
return await this._getRunlessCredentials<T>(type);
}
}

View File

@ -1,7 +1,6 @@
import { createDeferredPromise } from '@n8n/utils/promise/deferred-promise';
import type {
ICredentialDataDecryptedObject,
IExecuteData,
INode,
ITriggerFunctions,
IWorkflowExecuteAdditionalData,
@ -66,10 +65,6 @@ export class TriggerContext extends NodeExecutionContext implements ITriggerFunc
}
async getCredentials<T extends object = ICredentialDataDecryptedObject>(type: string) {
// No real task run backs a trigger, so this only exists to surface `node` to
// the credentials helper (e.g. for policy checks) — `data`/`source` are unused.
const executeData: IExecuteData = { data: {}, node: this.node, source: null };
return await this._getCredentials<T>(type, executeData);
return await this._getRunlessCredentials<T>(type);
}
}

View File

@ -100,11 +100,7 @@ export class WebhookContext extends NodeExecutionContext implements IWebhookFunc
}
async getCredentials<T extends object = ICredentialDataDecryptedObject>(type: string) {
// No real task run backs a webhook call, so this only exists to surface `node`
// to the credentials helper (e.g. for policy checks) — `data`/`source` are unused.
const executeData: IExecuteData = { data: {}, node: this.node, source: null };
return await this._getCredentials<T>(type, executeData);
return await this._getRunlessCredentials<T>(type);
}
getBodyData() {