fix(Zulip Node): Normalize multiOptions recipients when expression returns a string (#31492)

Co-authored-by: Dawid Myslak <dawid.myslak@gmail.com>
This commit is contained in:
Hammad Khan 2026-06-02 14:03:28 +05:00 committed by GitHub
parent 60c8517004
commit 01cc906ebd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 70 additions and 2 deletions

View File

@ -56,6 +56,28 @@ export async function zulipApiRequest(
}
}
// 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 function validateJSON(json: string | undefined): any {
let result;
try {

View File

@ -10,7 +10,7 @@ import type {
} from 'n8n-workflow';
import { NodeConnectionTypes, NodeOperationError } from 'n8n-workflow';
import { validateJSON, zulipApiRequest } from './GenericFunctions';
import { toMultiOptionsCsv, validateJSON, zulipApiRequest } from './GenericFunctions';
import { messageFields, messageOperations } from './MessageDescription';
import type { IMessage } from './MessageInterface';
import { streamFields, streamOperations } from './StreamDescription';
@ -138,7 +138,7 @@ export class Zulip implements INodeType {
if (resource === 'message') {
//https://zulipchat.com/api/send-message
if (operation === 'sendPrivate') {
const to = (this.getNodeParameter('to', i) as string[]).join(',');
const to = toMultiOptionsCsv(this.getNodeParameter('to', i));
const content = this.getNodeParameter('content', i) as string;
const body: IMessage = {
type: 'private',

View File

@ -0,0 +1,46 @@
import { toMultiOptionsCsv } from '../GenericFunctions';
describe('Zulip > GenericFunctions', () => {
describe('toMultiOptionsCsv', () => {
it('joins array values', () => {
expect(toMultiOptionsCsv(['user1@example.com', 'user2@example.com'])).toBe(
'user1@example.com,user2@example.com',
);
});
it('trims entries inside an array (interpolated array elements)', () => {
expect(toMultiOptionsCsv(['user1@example.com ', ' user2@example.com'])).toBe(
'user1@example.com,user2@example.com',
);
});
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('user1@example.com,user2@example.com ')).toBe(
'user1@example.com,user2@example.com',
);
});
it('trims whitespace around each entry in a comma-string', () => {
expect(toMultiOptionsCsv(' user1@example.com , user2@example.com ')).toBe(
'user1@example.com,user2@example.com',
);
});
it('drops empty entries', () => {
expect(toMultiOptionsCsv(['user1@example.com', '', ' ', 'user2@example.com'])).toBe(
'user1@example.com,user2@example.com',
);
});
it('returns an empty string for undefined/null/empty values', () => {
expect(toMultiOptionsCsv(undefined)).toBe('');
expect(toMultiOptionsCsv(null)).toBe('');
expect(toMultiOptionsCsv('')).toBe('');
expect(toMultiOptionsCsv([])).toBe('');
});
});
});