From ff107a2564ded70e4c916dfdcf4612a23398a88d Mon Sep 17 00:00:00 2001 From: enlorik Date: Thu, 11 Jun 2026 20:42:47 +0200 Subject: [PATCH] fix: normalize multiOptions fields in Hunter node to handle string coercion --- .../nodes/Hunter/GenericFunctions.ts | 22 +++++++++ .../nodes-base/nodes/Hunter/Hunter.node.ts | 6 +-- .../Hunter/test/GenericFunctions.test.ts | 46 +++++++++++++++++++ 3 files changed, 71 insertions(+), 3 deletions(-) create mode 100644 packages/nodes-base/nodes/Hunter/test/GenericFunctions.test.ts diff --git a/packages/nodes-base/nodes/Hunter/GenericFunctions.ts b/packages/nodes-base/nodes/Hunter/GenericFunctions.ts index d78a2d204a3..cbcf026ea12 100644 --- a/packages/nodes-base/nodes/Hunter/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Hunter/GenericFunctions.ts @@ -9,6 +9,28 @@ import type { } from 'n8n-workflow'; import { NodeApiError } from 'n8n-workflow'; +// A multiOptions parameter normally resolves to an array. When its value comes +// from an expression that is wrapped in surrounding text/whitespace, n8n +// switches to string interpolation and the array is coerced to a comma-joined +// string. Accept both shapes so the node degrades gracefully instead of +// throwing a low-level "join is not a function" TypeError. +export function toMultiOptionsCsv(value: unknown): string { + if (Array.isArray(value)) { + return value + .map((entry) => String(entry).trim()) + .filter((entry) => entry.length > 0) + .join(','); + } + if (typeof value === 'string') { + return value + .split(',') + .map((entry) => entry.trim()) + .filter((entry) => entry.length > 0) + .join(','); + } + return ''; +} + export async function hunterApiRequest( this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, method: IHttpRequestMethods, diff --git a/packages/nodes-base/nodes/Hunter/Hunter.node.ts b/packages/nodes-base/nodes/Hunter/Hunter.node.ts index 5566f238e28..0dcd7fef1e2 100644 --- a/packages/nodes-base/nodes/Hunter/Hunter.node.ts +++ b/packages/nodes-base/nodes/Hunter/Hunter.node.ts @@ -7,7 +7,7 @@ import type { } from 'n8n-workflow'; import { NodeConnectionTypes } from 'n8n-workflow'; -import { hunterApiRequest, hunterApiRequestAllItems } from './GenericFunctions'; +import { hunterApiRequest, hunterApiRequestAllItems, toMultiOptionsCsv } from './GenericFunctions'; export class Hunter implements INodeType { description: INodeTypeDescription = { @@ -295,10 +295,10 @@ export class Hunter implements INodeType { qs.type = filters.type; } if (filters.seniority) { - qs.seniority = (filters.seniority as string[]).join(','); + qs.seniority = toMultiOptionsCsv(filters.seniority); } if (filters.department) { - qs.department = (filters.department as string[]).join(','); + qs.department = toMultiOptionsCsv(filters.department); } if (returnAll) { responseData = await hunterApiRequestAllItems.call( diff --git a/packages/nodes-base/nodes/Hunter/test/GenericFunctions.test.ts b/packages/nodes-base/nodes/Hunter/test/GenericFunctions.test.ts new file mode 100644 index 00000000000..6993c3f5c3f --- /dev/null +++ b/packages/nodes-base/nodes/Hunter/test/GenericFunctions.test.ts @@ -0,0 +1,46 @@ +import { toMultiOptionsCsv } from '../GenericFunctions'; + +describe('Hunter > GenericFunctions', () => { + describe('toMultiOptionsCsv', () => { + it('joins array values', () => { + expect(toMultiOptionsCsv(['junior', 'senior'])).toBe('junior,senior'); + }); + + it('trims entries inside an array (interpolated array elements)', () => { + expect(toMultiOptionsCsv(['junior ', ' senior'])).toBe('junior,senior'); + }); + + it('coerces non-string array entries via String()', () => { + expect(toMultiOptionsCsv([1, 2, 3])).toBe('1,2,3'); + }); + + it('accepts a comma-joined string (the whitespace-expression coercion case)', () => { + expect(toMultiOptionsCsv('junior,senior')).toBe('junior,senior'); + }); + + it('trims whitespace around each entry in a comma-string', () => { + expect(toMultiOptionsCsv(' junior , senior ')).toBe('junior,senior'); + }); + + it('drops empty entries from an array', () => { + expect(toMultiOptionsCsv(['junior', '', ' ', 'senior'])).toBe('junior,senior'); + }); + + it('drops empty entries from a string', () => { + expect(toMultiOptionsCsv('junior,,senior')).toBe('junior,senior'); + }); + + it('returns an empty string for an empty array', () => { + expect(toMultiOptionsCsv([])).toBe(''); + }); + + it('returns an empty string for an empty string', () => { + expect(toMultiOptionsCsv('')).toBe(''); + }); + + it('returns an empty string for non-string non-array values', () => { + expect(toMultiOptionsCsv(undefined)).toBe(''); + expect(toMultiOptionsCsv(null)).toBe(''); + }); + }); +});