mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
feat: Add support for $env in the js task runner (no-changelog) (#11177)
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { DateTime } from 'luxon';
|
||||
import type { CodeExecutionMode, IDataObject } from 'n8n-workflow';
|
||||
|
||||
import { ValidationError } from '@/js-task-runner/errors/validation-error';
|
||||
@@ -30,6 +31,36 @@ describe('JsTaskRunner', () => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
const executeForAllItems = async ({
|
||||
code,
|
||||
inputItems,
|
||||
settings,
|
||||
}: { code: string; inputItems: IDataObject[]; settings?: Partial<JSExecSettings> }) => {
|
||||
return await execTaskWithParams({
|
||||
task: newTaskWithSettings({
|
||||
code,
|
||||
nodeMode: 'runOnceForAllItems',
|
||||
...settings,
|
||||
}),
|
||||
taskData: newAllCodeTaskData(inputItems.map(wrapIntoJson)),
|
||||
});
|
||||
};
|
||||
|
||||
const executeForEachItem = async ({
|
||||
code,
|
||||
inputItems,
|
||||
settings,
|
||||
}: { code: string; inputItems: IDataObject[]; settings?: Partial<JSExecSettings> }) => {
|
||||
return await execTaskWithParams({
|
||||
task: newTaskWithSettings({
|
||||
code,
|
||||
nodeMode: 'runOnceForEachItem',
|
||||
...settings,
|
||||
}),
|
||||
taskData: newAllCodeTaskData(inputItems.map(wrapIntoJson)),
|
||||
});
|
||||
};
|
||||
|
||||
describe('console', () => {
|
||||
test.each<[CodeExecutionMode]>([['runOnceForAllItems'], ['runOnceForEachItem']])(
|
||||
'should make an rpc call for console log in %s mode',
|
||||
@@ -52,22 +83,178 @@ describe('JsTaskRunner', () => {
|
||||
);
|
||||
});
|
||||
|
||||
describe('runOnceForAllItems', () => {
|
||||
const executeForAllItems = async ({
|
||||
code,
|
||||
inputItems,
|
||||
settings,
|
||||
}: { code: string; inputItems: IDataObject[]; settings?: Partial<JSExecSettings> }) => {
|
||||
return await execTaskWithParams({
|
||||
task: newTaskWithSettings({
|
||||
code,
|
||||
nodeMode: 'runOnceForAllItems',
|
||||
...settings,
|
||||
}),
|
||||
taskData: newAllCodeTaskData(inputItems.map(wrapIntoJson)),
|
||||
describe('built-in methods and variables available in the context', () => {
|
||||
const inputItems = [{ a: 1 }];
|
||||
|
||||
const testExpressionForAllItems = async (
|
||||
expression: string,
|
||||
expected: IDataObject | string | number | boolean,
|
||||
) => {
|
||||
const needsWrapping = typeof expected !== 'object';
|
||||
const outcome = await executeForAllItems({
|
||||
code: needsWrapping ? `return { val: ${expression} }` : `return ${expression}`,
|
||||
inputItems,
|
||||
});
|
||||
|
||||
expect(outcome.result).toEqual([wrapIntoJson(needsWrapping ? { val: expected } : expected)]);
|
||||
};
|
||||
|
||||
const testExpressionForEachItem = async (
|
||||
expression: string,
|
||||
expected: IDataObject | string | number | boolean,
|
||||
) => {
|
||||
const needsWrapping = typeof expected !== 'object';
|
||||
const outcome = await executeForEachItem({
|
||||
code: needsWrapping ? `return { val: ${expression} }` : `return ${expression}`,
|
||||
inputItems,
|
||||
});
|
||||
|
||||
expect(outcome.result).toEqual([
|
||||
withPairedItem(0, wrapIntoJson(needsWrapping ? { val: expected } : expected)),
|
||||
]);
|
||||
};
|
||||
|
||||
const testGroups = {
|
||||
// https://docs.n8n.io/code/builtin/current-node-input/
|
||||
'current node input': [
|
||||
['$input.first()', inputItems[0]],
|
||||
['$input.last()', inputItems[inputItems.length - 1]],
|
||||
['$input.params', { manualTriggerParam: 'empty' }],
|
||||
],
|
||||
// https://docs.n8n.io/code/builtin/output-other-nodes/
|
||||
'output of other nodes': [
|
||||
['$("Trigger").first()', inputItems[0]],
|
||||
['$("Trigger").last()', inputItems[inputItems.length - 1]],
|
||||
['$("Trigger").params', { manualTriggerParam: 'empty' }],
|
||||
],
|
||||
// https://docs.n8n.io/code/builtin/date-time/
|
||||
'date and time': [
|
||||
['$now', expect.any(DateTime)],
|
||||
['$today', expect.any(DateTime)],
|
||||
['{dt: DateTime}', { dt: expect.any(Function) }],
|
||||
],
|
||||
// https://docs.n8n.io/code/builtin/jmespath/
|
||||
JMESPath: [['{ val: $jmespath([{ f: 1 },{ f: 2 }], "[*].f") }', { val: [1, 2] }]],
|
||||
// https://docs.n8n.io/code/builtin/n8n-metadata/
|
||||
'n8n metadata': [
|
||||
[
|
||||
'$execution',
|
||||
{
|
||||
id: 'exec-id',
|
||||
mode: 'test',
|
||||
resumeFormUrl: 'http://formWaitingBaseUrl/exec-id',
|
||||
resumeUrl: 'http://webhookWaitingBaseUrl/exec-id',
|
||||
customData: {
|
||||
get: expect.any(Function),
|
||||
getAll: expect.any(Function),
|
||||
set: expect.any(Function),
|
||||
setAll: expect.any(Function),
|
||||
},
|
||||
},
|
||||
],
|
||||
['$("Trigger").isExecuted', true],
|
||||
['$nodeVersion', 2],
|
||||
['$prevNode.name', 'Trigger'],
|
||||
['$prevNode.outputIndex', 0],
|
||||
['$runIndex', 0],
|
||||
['{ wf: $workflow }', { wf: { active: true, id: '1', name: 'Test Workflow' } }],
|
||||
['$vars', { var: 'value' }],
|
||||
],
|
||||
};
|
||||
|
||||
for (const [groupName, tests] of Object.entries(testGroups)) {
|
||||
describe(`${groupName} runOnceForAllItems`, () => {
|
||||
test.each(tests)(
|
||||
'should have the %s available in the context',
|
||||
async (expression, expected) => {
|
||||
await testExpressionForAllItems(expression, expected);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe(`${groupName} runOnceForEachItem`, () => {
|
||||
test.each(tests)(
|
||||
'should have the %s available in the context',
|
||||
async (expression, expected) => {
|
||||
await testExpressionForEachItem(expression, expected);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
describe('$env', () => {
|
||||
it('should have the env available in context when access has not been blocked', async () => {
|
||||
const outcome = await execTaskWithParams({
|
||||
task: newTaskWithSettings({
|
||||
code: 'return { val: $env.VAR1 }',
|
||||
nodeMode: 'runOnceForAllItems',
|
||||
}),
|
||||
taskData: newAllCodeTaskData(inputItems.map(wrapIntoJson), {
|
||||
envProviderState: {
|
||||
isEnvAccessBlocked: false,
|
||||
isProcessAvailable: true,
|
||||
env: { VAR1: 'value' },
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
expect(outcome.result).toEqual([wrapIntoJson({ val: 'value' })]);
|
||||
});
|
||||
|
||||
it('should be possible to access env if it has been blocked', async () => {
|
||||
await expect(
|
||||
execTaskWithParams({
|
||||
task: newTaskWithSettings({
|
||||
code: 'return { val: $env.VAR1 }',
|
||||
nodeMode: 'runOnceForAllItems',
|
||||
}),
|
||||
taskData: newAllCodeTaskData(inputItems.map(wrapIntoJson), {
|
||||
envProviderState: {
|
||||
isEnvAccessBlocked: true,
|
||||
isProcessAvailable: true,
|
||||
env: { VAR1: 'value' },
|
||||
},
|
||||
}),
|
||||
}),
|
||||
).rejects.toThrow('access to env vars denied');
|
||||
});
|
||||
|
||||
it('should not be possible to iterate $env', async () => {
|
||||
const outcome = await execTaskWithParams({
|
||||
task: newTaskWithSettings({
|
||||
code: 'return Object.values($env).concat(Object.keys($env))',
|
||||
nodeMode: 'runOnceForAllItems',
|
||||
}),
|
||||
taskData: newAllCodeTaskData(inputItems.map(wrapIntoJson), {
|
||||
envProviderState: {
|
||||
isEnvAccessBlocked: false,
|
||||
isProcessAvailable: true,
|
||||
env: { VAR1: '1', VAR2: '2', VAR3: '3' },
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
expect(outcome.result).toEqual([]);
|
||||
});
|
||||
|
||||
it("should not expose task runner's env variables even if no env state is received", async () => {
|
||||
process.env.N8N_RUNNERS_N8N_URI = 'http://127.0.0.1:5679';
|
||||
const outcome = await execTaskWithParams({
|
||||
task: newTaskWithSettings({
|
||||
code: 'return { val: $env.N8N_RUNNERS_N8N_URI }',
|
||||
nodeMode: 'runOnceForAllItems',
|
||||
}),
|
||||
taskData: newAllCodeTaskData(inputItems.map(wrapIntoJson), {
|
||||
envProviderState: undefined,
|
||||
}),
|
||||
});
|
||||
|
||||
expect(outcome.result).toEqual([wrapIntoJson({ val: undefined })]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('runOnceForAllItems', () => {
|
||||
describe('continue on fail', () => {
|
||||
it('should return an item with the error if continueOnFail is true', async () => {
|
||||
const outcome = await executeForAllItems({
|
||||
@@ -181,21 +368,6 @@ describe('JsTaskRunner', () => {
|
||||
});
|
||||
|
||||
describe('runForEachItem', () => {
|
||||
const executeForEachItem = async ({
|
||||
code,
|
||||
inputItems,
|
||||
settings,
|
||||
}: { code: string; inputItems: IDataObject[]; settings?: Partial<JSExecSettings> }) => {
|
||||
return await execTaskWithParams({
|
||||
task: newTaskWithSettings({
|
||||
code,
|
||||
nodeMode: 'runOnceForEachItem',
|
||||
...settings,
|
||||
}),
|
||||
taskData: newAllCodeTaskData(inputItems.map(wrapIntoJson)),
|
||||
});
|
||||
};
|
||||
|
||||
describe('continue on fail', () => {
|
||||
it('should return an item with the error if continueOnFail is true', async () => {
|
||||
const outcome = await executeForEachItem({
|
||||
|
||||
Reference in New Issue
Block a user