Skip to content

Commit 53136aa

Browse files
committed
fix(zoho-desk): stop posting null for untouched update_ticket fields
`filterUndefined` strips only `undefined`, but an untouched subBlock never arrives as `undefined`: the workflow serializer initializes every subBlock value to `null` (stores/workflows/utils.ts) and extractBlockParams writes those nulls straight into tool params, with nothing between the serializer and request.body filtering them. Reproduced against the real serializer and block with only `status` set: basic {"subject":null,"status":"Closed"} advanced {"subject":null,"status":"Closed","priority":null,...,"cf":null} `subject` leaks even in basic mode because it declares no `mode`, so shouldSerializeSubBlock never drops it. Zoho documents subject as a writable field, so every status-only edit either failed the PATCH or blanked the ticket's subject; in advanced mode the whole update surface nulled out, including `cf`. Two things hid this. The empty-PATCH guard was unreachable from the block (the body always carried at least `subject`), and the existing test called buildBody with fields *absent* rather than null - the shape the block never produces - so it could not fail on the real path. Replaces filterUndefined with a local omitUnset that drops undefined, null, and '' (a cleared input means "leave unchanged", not "set to empty"). Adds three tests using the real serializer shape, all verified to fail before the fix. Also fixes the same null-blindness in the block's param mapping, where Number(null) === 0 injected from=0 on every operation, and corrects the shared limit placeholder, which claimed max 100 while list_threads allows 200.
1 parent d607d18 commit 53136aa

3 files changed

Lines changed: 62 additions & 5 deletions

File tree

apps/sim/blocks/blocks/zoho-desk.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,7 @@ export const ZohoDeskBlock: BlockConfig<ZohoDeskResponse> = {
322322
id: 'limit',
323323
title: 'Limit',
324324
type: 'short-input',
325-
placeholder: 'Max results (max 100)',
325+
placeholder: 'Max results (tickets/comments 100, threads 200)',
326326
condition: {
327327
field: 'operation',
328328
value: ['list_tickets', 'list_comments', 'list_threads'],
@@ -371,11 +371,14 @@ export const ZohoDeskBlock: BlockConfig<ZohoDeskResponse> = {
371371
// Zoho documents from >= 0 and limit >= 1 as integers; a negative or
372372
// fractional value reaches the API as an opaque provider error, so drop
373373
// anything outside those bounds here rather than round-tripping it.
374-
if (rawFrom !== undefined && rawFrom !== '') {
374+
// `null` is checked explicitly: the serializer initializes untouched
375+
// subBlocks to null, and Number(null) is 0 — which would otherwise inject
376+
// from=0 on every operation instead of leaving the param unset.
377+
if (rawFrom !== undefined && rawFrom !== null && rawFrom !== '') {
375378
const from = Number(rawFrom)
376379
if (Number.isInteger(from) && from >= 0) result.from = from
377380
}
378-
if (rawLimit !== undefined && rawLimit !== '') {
381+
if (rawLimit !== undefined && rawLimit !== null && rawLimit !== '') {
379382
const limit = Number(rawLimit)
380383
if (Number.isInteger(limit) && limit >= 1) result.limit = limit
381384
}

apps/sim/tools/zoho_desk/update_ticket.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,39 @@ describe('zohoDeskUpdateTicketTool request body', () => {
1919
subject: 'Hi',
2020
})
2121
})
22+
23+
// The workflow serializer initializes every untouched subBlock to `null` (not
24+
// `undefined`) and writes those nulls into tool params, so this - not the
25+
// fields-absent case above - is the shape the block actually produces. A
26+
// status-only edit must not post `subject: null` and blank the ticket.
27+
it('drops untouched fields that arrive as null from the serializer', () => {
28+
const serializedParams = {
29+
...base,
30+
subject: null,
31+
status: 'Closed',
32+
priority: null,
33+
assigneeId: null,
34+
departmentId: null,
35+
category: null,
36+
subCategory: null,
37+
dueDate: null,
38+
description: null,
39+
resolution: null,
40+
classification: null,
41+
customFields: null,
42+
}
43+
expect(buildBody(serializedParams)).toEqual({ status: 'Closed' })
44+
})
45+
46+
it('drops fields the user cleared to an empty string', () => {
47+
expect(buildBody({ ...base, status: 'Closed', subject: '' })).toEqual({ status: 'Closed' })
48+
})
49+
50+
// The empty-PATCH guard must be reachable from the real serializer shape, not
51+
// only from the synthetic fields-absent case.
52+
it('throws when every updatable field is null', () => {
53+
expect(() => buildBody({ ...base, subject: null, status: null, priority: null })).toThrow(
54+
/no fields to update/i
55+
)
56+
})
2257
})

apps/sim/tools/zoho_desk/update_ticket.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { filterUndefined } from '@sim/utils/object'
21
import type { ToolConfig } from '@/tools/types'
32
import type { ZohoDeskResponse, ZohoDeskUpdateTicketParams } from '@/tools/zoho_desk/types'
43
import { ZOHO_DESK_TICKET_PROPERTIES } from '@/tools/zoho_desk/types'
@@ -9,6 +8,26 @@ import {
98
requireZohoDeskId,
109
} from '@/tools/zoho_desk/utils'
1110

11+
/**
12+
* Drop keys the caller did not set, so a PATCH only carries real edits.
13+
*
14+
* Deliberately NOT `filterUndefined`: that helper strips `undefined` only, but
15+
* an untouched subBlock does not arrive as `undefined` - the workflow serializer
16+
* initializes every subBlock value to `null` and writes those nulls straight
17+
* into tool params. A status-only update therefore reached Zoho as
18+
* `{"subject": null, "status": "Closed"}`, which either fails the PATCH or
19+
* blanks the ticket's subject. Empty strings are treated the same way: an input
20+
* the user cleared means "leave unchanged", not "set to empty".
21+
*/
22+
function omitUnset(fields: Record<string, unknown>): Record<string, unknown> {
23+
const result: Record<string, unknown> = {}
24+
for (const [key, value] of Object.entries(fields)) {
25+
if (value === undefined || value === null || value === '') continue
26+
result[key] = value
27+
}
28+
return result
29+
}
30+
1231
export const zohoDeskUpdateTicketTool: ToolConfig<ZohoDeskUpdateTicketParams, ZohoDeskResponse> = {
1332
id: 'zoho_desk_update_ticket',
1433
name: 'Zoho Desk Update Ticket',
@@ -122,7 +141,7 @@ export const zohoDeskUpdateTicketTool: ToolConfig<ZohoDeskUpdateTicketParams, Zo
122141
method: 'PATCH',
123142
headers: (params) => buildZohoDeskHeaders(params),
124143
body: (params) => {
125-
const body = filterUndefined({
144+
const body = omitUnset({
126145
subject: params.subject,
127146
status: params.status,
128147
priority: params.priority,

0 commit comments

Comments
 (0)