diff --git a/packages/@n8n/task-runner/src/js-task-runner/__tests__/js-task-runner.test.ts b/packages/@n8n/task-runner/src/js-task-runner/__tests__/js-task-runner.test.ts index 61c19c5cc2..d2dac70778 100644 --- a/packages/@n8n/task-runner/src/js-task-runner/__tests__/js-task-runner.test.ts +++ b/packages/@n8n/task-runner/src/js-task-runner/__tests__/js-task-runner.test.ts @@ -1,6 +1,10 @@ import { DateTime, Duration, Interval } from 'luxon'; -import type { IBinaryData } from 'n8n-workflow'; -import { setGlobalState, type CodeExecutionMode, type IDataObject } from 'n8n-workflow'; +import { + type IBinaryData, + setGlobalState, + type CodeExecutionMode, + type IDataObject, +} from 'n8n-workflow'; import fs from 'node:fs'; import { builtinModules } from 'node:module'; @@ -26,6 +30,7 @@ import { withPairedItem, wrapIntoJson, } from './test-data'; +import { ReservedKeyFoundError } from '../errors/reserved-key-not-found.error'; jest.mock('ws'); @@ -805,6 +810,15 @@ describe('JsTaskRunner', () => { }), ).rejects.toThrow(ValidationError); }); + + it('should throw a ReservedKeyFoundError if there are unknown keys alongside reserved keys', async () => { + await expect( + executeForAllItems({ + code: 'return [{json: {b: 1}, objectId: "123"}]', + inputItems: [{ a: 1 }], + }), + ).rejects.toThrow(ReservedKeyFoundError); + }); }); it('should return static items', async () => { diff --git a/packages/@n8n/task-runner/src/js-task-runner/errors/reserved-key-not-found.error.ts b/packages/@n8n/task-runner/src/js-task-runner/errors/reserved-key-not-found.error.ts new file mode 100644 index 0000000000..2e32466b85 --- /dev/null +++ b/packages/@n8n/task-runner/src/js-task-runner/errors/reserved-key-not-found.error.ts @@ -0,0 +1,11 @@ +import { ValidationError } from './validation-error'; + +export class ReservedKeyFoundError extends ValidationError { + constructor(reservedKey: string, itemIndex: number) { + super({ + message: 'Invalid output format', + description: `An output item contains the reserved key ${reservedKey}. To get around this, please wrap each item in an object, under a key called json. Example`, + itemIndex, + }); + } +} diff --git a/packages/@n8n/task-runner/src/js-task-runner/result-validation.ts b/packages/@n8n/task-runner/src/js-task-runner/result-validation.ts index d5999d1ce4..dd5378a916 100644 --- a/packages/@n8n/task-runner/src/js-task-runner/result-validation.ts +++ b/packages/@n8n/task-runner/src/js-task-runner/result-validation.ts @@ -1,6 +1,7 @@ import { normalizeItems } from 'n8n-core'; import type { INodeExecutionData } from 'n8n-workflow'; +import { ReservedKeyFoundError } from './errors/reserved-key-not-found.error'; import { ValidationError } from './errors/validation-error'; import { isObject } from './obj-utils'; @@ -19,17 +20,28 @@ export const REQUIRED_N8N_ITEM_KEYS = new Set([ ]); function validateTopLevelKeys(item: INodeExecutionData, itemIndex: number) { - for (const key in item) { - if (Object.prototype.hasOwnProperty.call(item, key)) { - if (REQUIRED_N8N_ITEM_KEYS.has(key)) continue; + let foundReservedKey: string | null = null; + const unknownKeys: string[] = []; - throw new ValidationError({ - message: `Unknown top-level item key: ${key}`, - description: 'Access the properties of an item under `.json`, e.g. `item.json`', - itemIndex, - }); + for (const key in item) { + if (!Object.prototype.hasOwnProperty.call(item, key)) continue; + + if (REQUIRED_N8N_ITEM_KEYS.has(key)) { + foundReservedKey ??= key; + } else { + unknownKeys.push(key); } } + + if (unknownKeys.length > 0) { + if (foundReservedKey) throw new ReservedKeyFoundError(foundReservedKey, itemIndex); + + throw new ValidationError({ + message: `Unknown top-level item key: ${unknownKeys[0]}`, + description: 'Access the properties of an item under `.json`, e.g. `item.json`', + itemIndex, + }); + } } function validateItem({ json, binary }: INodeExecutionData, itemIndex: number) { diff --git a/packages/nodes-base/nodes/Code/Sandbox.ts b/packages/nodes-base/nodes/Code/Sandbox.ts index b3e3057d23..0cfa4fb534 100644 --- a/packages/nodes-base/nodes/Code/Sandbox.ts +++ b/packages/nodes-base/nodes/Code/Sandbox.ts @@ -185,13 +185,37 @@ export abstract class Sandbox extends EventEmitter { } private validateTopLevelKeys(item: INodeExecutionData, itemIndex: number) { - Object.keys(item).forEach((key) => { - if (REQUIRED_N8N_ITEM_KEYS.has(key)) return; + let foundReservedKey: string | null = null; + const unknownKeys: string[] = []; + + for (const key in item) { + if (!Object.prototype.hasOwnProperty.call(item, key)) continue; + + if (REQUIRED_N8N_ITEM_KEYS.has(key)) { + foundReservedKey ??= key; + } else { + unknownKeys.push(key); + } + } + + if (unknownKeys.length > 0) { + if (foundReservedKey) throw new ReservedKeyFoundError(foundReservedKey, itemIndex); + throw new ValidationError({ - message: `Unknown top-level item key: ${key}`, + message: `Unknown top-level item key: ${unknownKeys[0]}`, description: 'Access the properties of an item under `.json`, e.g. `item.json`', itemIndex, }); + } + } +} + +class ReservedKeyFoundError extends ValidationError { + constructor(reservedKey: string, itemIndex: number) { + super({ + message: 'Invalid output format', + description: `An output item contains the reserved key ${reservedKey}. To get around this, please wrap each item in an object, under a key called json. Example`, + itemIndex, }); } }