n8n-nodes-zoho-desk 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +339 -0
- package/dist/credentials/ZohoDeskOAuth2Api.credentials.d.ts +11 -0
- package/dist/credentials/ZohoDeskOAuth2Api.credentials.d.ts.map +1 -0
- package/dist/credentials/ZohoDeskOAuth2Api.credentials.js +118 -0
- package/dist/credentials/ZohoDeskOAuth2Api.credentials.js.map +1 -0
- package/dist/nodes/ZohoDesk/ZohoDesk.node.d.ts +20 -0
- package/dist/nodes/ZohoDesk/ZohoDesk.node.d.ts.map +1 -0
- package/dist/nodes/ZohoDesk/ZohoDesk.node.js +2534 -0
- package/dist/nodes/ZohoDesk/ZohoDesk.node.js.map +1 -0
- package/dist/nodes/ZohoDesk/ZohoDeskTrigger.node.d.ts +18 -0
- package/dist/nodes/ZohoDesk/ZohoDeskTrigger.node.d.ts.map +1 -0
- package/dist/nodes/ZohoDesk/ZohoDeskTrigger.node.js +322 -0
- package/dist/nodes/ZohoDesk/ZohoDeskTrigger.node.js.map +1 -0
- package/dist/nodes/ZohoDesk/zohoDesk.svg +1 -0
- package/index.js +3 -0
- package/package.json +84 -0
|
@@ -0,0 +1,2534 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ZohoDesk = void 0;
|
|
4
|
+
const n8n_workflow_1 = require("n8n-workflow");
|
|
5
|
+
/**
|
|
6
|
+
* Optional fields for ticket create operation
|
|
7
|
+
* Note: Some fields like 'secondaryContacts' and 'cf'
|
|
8
|
+
* are handled separately with custom parsing logic (see addCommonTicketFields function)
|
|
9
|
+
* Note: 'priority', 'classification', 'dueDate', 'description', and 'teamId' are primary fields for create operation
|
|
10
|
+
*/
|
|
11
|
+
const TICKET_CREATE_OPTIONAL_FIELDS = [
|
|
12
|
+
'accountId',
|
|
13
|
+
'assigneeId',
|
|
14
|
+
'category',
|
|
15
|
+
'channel',
|
|
16
|
+
'email',
|
|
17
|
+
'language',
|
|
18
|
+
'phone',
|
|
19
|
+
'productId',
|
|
20
|
+
'resolution',
|
|
21
|
+
'status',
|
|
22
|
+
'subCategory',
|
|
23
|
+
];
|
|
24
|
+
/**
|
|
25
|
+
* Optional fields for ticket update operation
|
|
26
|
+
* Note: Some fields like 'dueDate', 'priority', 'secondaryContacts', and 'cf'
|
|
27
|
+
* are handled separately with custom parsing logic (see addCommonTicketFields function)
|
|
28
|
+
* Note: 'description' is now a primary field for update operation
|
|
29
|
+
* Note: 'channel' is not updatable - it represents how the ticket was originally created
|
|
30
|
+
* Note: 'classification' is now handled as an options field with "No Change" option
|
|
31
|
+
*/
|
|
32
|
+
const TICKET_UPDATE_OPTIONAL_FIELDS = [
|
|
33
|
+
'accountId',
|
|
34
|
+
'assigneeId',
|
|
35
|
+
'category',
|
|
36
|
+
'contactId',
|
|
37
|
+
'departmentId',
|
|
38
|
+
'email',
|
|
39
|
+
'language',
|
|
40
|
+
'phone',
|
|
41
|
+
'productId',
|
|
42
|
+
'resolution',
|
|
43
|
+
'status',
|
|
44
|
+
'subCategory',
|
|
45
|
+
'subject',
|
|
46
|
+
'teamId',
|
|
47
|
+
];
|
|
48
|
+
/**
|
|
49
|
+
* Minimum number of digits for a valid Zoho Desk ticket ID
|
|
50
|
+
* Zoho Desk ticket IDs are typically 16-19 digits, but we allow 10+ for flexibility
|
|
51
|
+
* across different Zoho configurations and data centers
|
|
52
|
+
*/
|
|
53
|
+
const MIN_TICKET_ID_LENGTH = 10;
|
|
54
|
+
/**
|
|
55
|
+
* Default status for new tickets
|
|
56
|
+
*/
|
|
57
|
+
const DEFAULT_TICKET_STATUS = 'Open';
|
|
58
|
+
/**
|
|
59
|
+
* Zoho Desk API version
|
|
60
|
+
*/
|
|
61
|
+
const ZOHO_DESK_API_VERSION = 'v1';
|
|
62
|
+
/**
|
|
63
|
+
* Default base URL for Zoho Desk API
|
|
64
|
+
*/
|
|
65
|
+
const DEFAULT_BASE_URL = `https://desk.zoho.com/api/${ZOHO_DESK_API_VERSION}`;
|
|
66
|
+
/**
|
|
67
|
+
* Field length limits with documented standards.
|
|
68
|
+
*
|
|
69
|
+
* IMPORTANT: Only includes limits that are based on official standards or would cause
|
|
70
|
+
* system issues if exceeded. For fields without documented limits, we rely on the
|
|
71
|
+
* Zoho Desk API to return validation errors.
|
|
72
|
+
*
|
|
73
|
+
* This approach prevents false rejections of valid user input while still providing
|
|
74
|
+
* protection for fields with known constraints.
|
|
75
|
+
*/
|
|
76
|
+
const FIELD_LENGTH_LIMITS = {
|
|
77
|
+
email: 254, // RFC 5321 maximum email length - official standard
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* Pre-compiled regex patterns for performance optimization
|
|
81
|
+
*/
|
|
82
|
+
const N8N_EXPRESSION_PATTERN = /\{\{.+?\}\}/;
|
|
83
|
+
const RFC5322_EMAIL_PATTERN = /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
|
|
84
|
+
const ZOHO_DESK_ID_PATTERN = /^\d{10,}$/;
|
|
85
|
+
const TICKET_ID_PATTERN = new RegExp(`^\\d{${MIN_TICKET_ID_LENGTH},}$`);
|
|
86
|
+
/**
|
|
87
|
+
* Zoho Desk API documentation URLs
|
|
88
|
+
*/
|
|
89
|
+
const ZOHO_DESK_CREATE_TICKET_DOCS = 'https://desk.zoho.com/support/APIDocument#Tickets#Tickets_CreateTicket';
|
|
90
|
+
const ZOHO_DESK_UPDATE_TICKET_DOCS = 'https://desk.zoho.com/support/APIDocument#Tickets#Tickets_UpdateTicket';
|
|
91
|
+
const ZOHO_DESK_GET_TICKET_DOCS = 'https://desk.zoho.com/support/APIDocument#Tickets#Tickets_GetTicket';
|
|
92
|
+
const ZOHO_DESK_LIST_TICKETS_DOCS = 'https://desk.zoho.com/support/APIDocument#Tickets#Tickets_ListAllTickets';
|
|
93
|
+
const ZOHO_DESK_COMMENTS_DOCS = 'https://desk.zoho.com/support/APIDocument#TicketComments';
|
|
94
|
+
const ZOHO_DESK_THREADS_DOCS = 'https://desk.zoho.com/support/APIDocument#TicketThreads';
|
|
95
|
+
const ZOHO_DESK_CONTACTS_DOCS = 'https://desk.zoho.com/support/APIDocument#Contacts';
|
|
96
|
+
const ZOHO_DESK_ACCOUNTS_DOCS = 'https://desk.zoho.com/support/APIDocument#Accounts';
|
|
97
|
+
/**
|
|
98
|
+
* Optional fields for contact create operation
|
|
99
|
+
*/
|
|
100
|
+
const CONTACT_CREATE_OPTIONAL_FIELDS = [
|
|
101
|
+
'firstName',
|
|
102
|
+
'phone',
|
|
103
|
+
'mobile',
|
|
104
|
+
'accountId',
|
|
105
|
+
'twitter',
|
|
106
|
+
'facebook',
|
|
107
|
+
'type',
|
|
108
|
+
'description',
|
|
109
|
+
];
|
|
110
|
+
/**
|
|
111
|
+
* Optional fields for contact update operation
|
|
112
|
+
*/
|
|
113
|
+
const CONTACT_UPDATE_OPTIONAL_FIELDS = [
|
|
114
|
+
'firstName',
|
|
115
|
+
'lastName',
|
|
116
|
+
'email',
|
|
117
|
+
'phone',
|
|
118
|
+
'mobile',
|
|
119
|
+
'accountId',
|
|
120
|
+
'twitter',
|
|
121
|
+
'facebook',
|
|
122
|
+
'type',
|
|
123
|
+
'description',
|
|
124
|
+
];
|
|
125
|
+
/**
|
|
126
|
+
* Optional fields for account create operation
|
|
127
|
+
*/
|
|
128
|
+
const ACCOUNT_CREATE_OPTIONAL_FIELDS = [
|
|
129
|
+
'website',
|
|
130
|
+
'phone',
|
|
131
|
+
'fax',
|
|
132
|
+
'industry',
|
|
133
|
+
'description',
|
|
134
|
+
'code',
|
|
135
|
+
'city',
|
|
136
|
+
'country',
|
|
137
|
+
'state',
|
|
138
|
+
'street',
|
|
139
|
+
'zip',
|
|
140
|
+
];
|
|
141
|
+
/**
|
|
142
|
+
* Optional fields for account update operation
|
|
143
|
+
*/
|
|
144
|
+
const ACCOUNT_UPDATE_OPTIONAL_FIELDS = [
|
|
145
|
+
'accountName',
|
|
146
|
+
'website',
|
|
147
|
+
'phone',
|
|
148
|
+
'fax',
|
|
149
|
+
'industry',
|
|
150
|
+
'description',
|
|
151
|
+
'code',
|
|
152
|
+
'city',
|
|
153
|
+
'country',
|
|
154
|
+
'state',
|
|
155
|
+
'street',
|
|
156
|
+
'zip',
|
|
157
|
+
];
|
|
158
|
+
/**
|
|
159
|
+
* Type guard to check if value is a plain object (not array or null)
|
|
160
|
+
* @param value - Value to check
|
|
161
|
+
* @returns True if value is a plain object
|
|
162
|
+
*/
|
|
163
|
+
function isPlainObject(value) {
|
|
164
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Parse comma-separated list and filter out empty values
|
|
168
|
+
* @param value - Comma-separated string (can be undefined)
|
|
169
|
+
* @returns Array of trimmed non-empty values
|
|
170
|
+
*/
|
|
171
|
+
function parseCommaSeparatedList(value) {
|
|
172
|
+
if (!value) {
|
|
173
|
+
return [];
|
|
174
|
+
}
|
|
175
|
+
return value
|
|
176
|
+
.split(',')
|
|
177
|
+
.map((item) => item.trim())
|
|
178
|
+
.filter((item) => item.length > 0);
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Parse custom fields JSON with enhanced error handling, type validation, and CRLF removal
|
|
182
|
+
* @param cf - Custom fields as JSON string or object
|
|
183
|
+
* @returns Parsed and sanitized custom fields object
|
|
184
|
+
* @throws Error with detailed message if JSON parsing fails or result is not a plain object
|
|
185
|
+
*/
|
|
186
|
+
function parseCustomFields(cf) {
|
|
187
|
+
try {
|
|
188
|
+
let parsed;
|
|
189
|
+
if (typeof cf === 'string') {
|
|
190
|
+
parsed = JSON.parse(cf);
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
parsed = cf;
|
|
194
|
+
}
|
|
195
|
+
// Validate that parsed result is a plain object (not array or primitive)
|
|
196
|
+
if (!isPlainObject(parsed)) {
|
|
197
|
+
throw new n8n_workflow_1.ApplicationError('Custom fields must be a JSON object, not an array or primitive value. ' +
|
|
198
|
+
'See: ' +
|
|
199
|
+
ZOHO_DESK_CREATE_TICKET_DOCS);
|
|
200
|
+
}
|
|
201
|
+
// Sanitize all string values in custom fields to remove CRLF characters
|
|
202
|
+
const sanitized = {};
|
|
203
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
204
|
+
if (typeof value === 'string') {
|
|
205
|
+
// Validate length for custom field values (no length limit specified)
|
|
206
|
+
sanitized[key] = validateFieldLength(value, undefined, `Custom field "${key}"`);
|
|
207
|
+
}
|
|
208
|
+
else if (value !== null && value !== undefined) {
|
|
209
|
+
// Keep non-string, non-null values as-is
|
|
210
|
+
sanitized[key] = value;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return sanitized;
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
// Check if error is already a validation error from isPlainObject check or validateFieldLength
|
|
217
|
+
if (error instanceof Error &&
|
|
218
|
+
(error.message.includes('Custom fields must be a JSON object') ||
|
|
219
|
+
error.message.includes('exceeds maximum length'))) {
|
|
220
|
+
// Re-throw validation errors without wrapping to preserve specific details
|
|
221
|
+
throw error;
|
|
222
|
+
}
|
|
223
|
+
// Wrap JSON parsing errors with helpful context
|
|
224
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
225
|
+
throw new n8n_workflow_1.ApplicationError(`Custom fields must be valid JSON. Parse error: ${errorMessage}. ` +
|
|
226
|
+
`Please ensure your JSON is properly formatted, e.g., {"cf_field": "value"}. ` +
|
|
227
|
+
'See: ' +
|
|
228
|
+
ZOHO_DESK_CREATE_TICKET_DOCS);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Add optional fields to request body if they exist in source object
|
|
233
|
+
* Removes CRLF characters from string fields to prevent header injection
|
|
234
|
+
* @param body - Target object to add fields to
|
|
235
|
+
* @param source - Source object containing field values
|
|
236
|
+
* @param fields - Array of field names to copy
|
|
237
|
+
*/
|
|
238
|
+
function addOptionalFields(body, source, fields) {
|
|
239
|
+
for (const field of fields) {
|
|
240
|
+
if (source[field] !== undefined) {
|
|
241
|
+
// Validate length for string fields (XSS protection handled by Zoho Desk API)
|
|
242
|
+
if (typeof source[field] === 'string') {
|
|
243
|
+
const stringValue = source[field];
|
|
244
|
+
// Capitalize field name once for all uses (avoid variable shadowing)
|
|
245
|
+
const fieldDisplayName = field.charAt(0).toUpperCase() + field.slice(1);
|
|
246
|
+
// Validate ID fields - empty strings are allowed (Zoho Desk API will ignore them)
|
|
247
|
+
if (field.endsWith('Id') && stringValue.trim() !== '') {
|
|
248
|
+
isValidZohoDeskId(stringValue, fieldDisplayName);
|
|
249
|
+
}
|
|
250
|
+
// Apply length limits from FIELD_LENGTH_LIMITS or use undefined for no limit
|
|
251
|
+
const maxLength = field in FIELD_LENGTH_LIMITS
|
|
252
|
+
? FIELD_LENGTH_LIMITS[field]
|
|
253
|
+
: undefined;
|
|
254
|
+
body[field] = validateFieldLength(stringValue, maxLength, fieldDisplayName);
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
body[field] = source[field];
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Validate ticket ID format
|
|
264
|
+
* @param ticketId - Ticket ID to validate
|
|
265
|
+
* @returns True if ticket ID is valid (numeric with proper length or n8n expression), false otherwise
|
|
266
|
+
*/
|
|
267
|
+
function isValidTicketId(ticketId) {
|
|
268
|
+
const trimmed = ticketId.trim();
|
|
269
|
+
// Allow n8n expressions (e.g., {{$json.ticketId}}) - validation happens at runtime
|
|
270
|
+
// Use pre-compiled pattern for performance
|
|
271
|
+
if (N8N_EXPRESSION_PATTERN.test(trimmed)) {
|
|
272
|
+
return true;
|
|
273
|
+
}
|
|
274
|
+
// Zoho Desk ticket IDs are typically 16-19 digit numeric strings
|
|
275
|
+
// Use pre-compiled pattern for performance
|
|
276
|
+
return TICKET_ID_PATTERN.test(trimmed);
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Validate generic Zoho Desk ID format (for accounts, departments, teams, etc.)
|
|
280
|
+
* @param id - ID to validate
|
|
281
|
+
* @param fieldName - Field name for error messages
|
|
282
|
+
* @returns True if ID is valid (numeric or n8n expression), false otherwise
|
|
283
|
+
*/
|
|
284
|
+
function isValidZohoDeskId(id, fieldName) {
|
|
285
|
+
const trimmed = id.trim();
|
|
286
|
+
// Allow n8n expressions - validation happens at runtime
|
|
287
|
+
// Use pre-compiled pattern for performance
|
|
288
|
+
if (N8N_EXPRESSION_PATTERN.test(trimmed)) {
|
|
289
|
+
return true;
|
|
290
|
+
}
|
|
291
|
+
// Zoho Desk IDs are numeric strings (minimum 10 digits for safety)
|
|
292
|
+
// Less strict than ticket IDs as different resources may have different lengths
|
|
293
|
+
// Use pre-compiled pattern for performance
|
|
294
|
+
if (!ZOHO_DESK_ID_PATTERN.test(trimmed)) {
|
|
295
|
+
throw new n8n_workflow_1.ApplicationError(`Invalid ${fieldName} format: "${trimmed}". ` +
|
|
296
|
+
`${fieldName} must be a numeric value with at least 10 digits.`);
|
|
297
|
+
}
|
|
298
|
+
return true;
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Validate field length without modifying content
|
|
302
|
+
*
|
|
303
|
+
* IMPORTANT: Does NOT sanitize CRLF characters because:
|
|
304
|
+
* - This node sends JSON request bodies, not HTTP headers
|
|
305
|
+
* - CRLF injection only affects HTTP headers (e.g., "Header: value\r\nInjected-Header: malicious")
|
|
306
|
+
* - In JSON bodies, newlines are safe and expected (e.g., multi-line descriptions)
|
|
307
|
+
* - JSON serialization automatically escapes newlines as \n in the wire format
|
|
308
|
+
* - Removing newlines breaks user-expected behavior for description and custom fields
|
|
309
|
+
*
|
|
310
|
+
* Example: "Line 1\nLine 2\nLine 3" → sent as-is → JSON serializes to "Line 1\\nLine 2\\nLine 3"
|
|
311
|
+
*
|
|
312
|
+
* SECURITY NOTE: Does NOT protect against XSS - handled by Zoho Desk API server-side.
|
|
313
|
+
* Input like '<script>alert(XSS)</script>' passes through unchanged (as expected).
|
|
314
|
+
*
|
|
315
|
+
* @param value - String value to validate
|
|
316
|
+
* @param maxLength - Maximum allowed length (optional). Throws error if exceeded.
|
|
317
|
+
* @param fieldName - Field name for error messages (optional, defaults to 'Field')
|
|
318
|
+
* @returns Original string value unchanged (preserves newlines and formatting)
|
|
319
|
+
* @throws Error if value exceeds maxLength
|
|
320
|
+
*/
|
|
321
|
+
function validateFieldLength(value, maxLength, fieldName) {
|
|
322
|
+
// Enforce length limit if specified - THROW ERROR instead of silent truncation
|
|
323
|
+
if (maxLength && value.length > maxLength) {
|
|
324
|
+
throw new n8n_workflow_1.ApplicationError(`${fieldName || 'Field'} exceeds maximum length of ${maxLength} characters (${value.length} provided). ` +
|
|
325
|
+
'Please shorten your input and try again.');
|
|
326
|
+
}
|
|
327
|
+
return value;
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Validate and normalize ISO 8601 date string for Zoho Desk API
|
|
331
|
+
*
|
|
332
|
+
* @param dateString - ISO 8601 formatted date string (e.g., "2025-11-20T10:30:00" or "2025-11-20T10:30:00.000Z")
|
|
333
|
+
* @param fieldName - Field name for error messages (e.g., "Due Date")
|
|
334
|
+
* @param docsUrl - URL to Zoho Desk API documentation for error messages
|
|
335
|
+
* @returns ISO 8601 string with milliseconds and timezone (e.g., "2025-11-20T10:30:00.000Z")
|
|
336
|
+
* @throws Error if dateString is invalid or cannot be parsed
|
|
337
|
+
*/
|
|
338
|
+
function convertDateToTimestamp(dateString, fieldName, docsUrl) {
|
|
339
|
+
// Validate that date string is not empty or whitespace-only
|
|
340
|
+
if (!dateString || dateString.trim() === '') {
|
|
341
|
+
throw new n8n_workflow_1.ApplicationError(`${fieldName} cannot be empty. Expected ISO 8601 format (e.g., "2025-11-20T10:30:00.000Z"). ` +
|
|
342
|
+
'See: ' +
|
|
343
|
+
docsUrl);
|
|
344
|
+
}
|
|
345
|
+
// Validate date string format
|
|
346
|
+
const date = new Date(dateString);
|
|
347
|
+
if (isNaN(date.getTime())) {
|
|
348
|
+
throw new n8n_workflow_1.ApplicationError(`Invalid ${fieldName} format: "${dateString}". Expected ISO 8601 format (e.g., "2025-11-20T10:30:00.000Z"). ` +
|
|
349
|
+
'See: ' +
|
|
350
|
+
docsUrl);
|
|
351
|
+
}
|
|
352
|
+
// Return ISO 8601 string format (Zoho Desk API expects ISO 8601 string, not milliseconds timestamp)
|
|
353
|
+
// If the input doesn't have milliseconds or timezone, add them
|
|
354
|
+
return date.toISOString();
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Validate email format using RFC 5322 compliant regex (simplified version)
|
|
358
|
+
* @param email - Email address to validate
|
|
359
|
+
* @returns True if email format is valid
|
|
360
|
+
*/
|
|
361
|
+
function isValidEmail(email) {
|
|
362
|
+
// Use pre-compiled RFC 5322 compliant regex for performance
|
|
363
|
+
// RFC 5321 maximum email length is 254 characters
|
|
364
|
+
return RFC5322_EMAIL_PATTERN.test(email) && email.length <= FIELD_LENGTH_LIMITS.email;
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Add a contact field with validation and type checking
|
|
368
|
+
* Reduces code duplication in contact validation
|
|
369
|
+
* @param contact - Target contact object
|
|
370
|
+
* @param contactValues - Source contact values
|
|
371
|
+
* @param fieldName - Name of the field to add
|
|
372
|
+
* @param fieldLabel - Display label for error messages
|
|
373
|
+
* @param maxLength - Maximum allowed length for the field (optional)
|
|
374
|
+
*/
|
|
375
|
+
function addContactField(contact, contactValues, fieldName, fieldLabel, maxLength) {
|
|
376
|
+
// Explicit null/undefined check - don't skip falsy values like 0 or empty string
|
|
377
|
+
// (empty strings are handled below after trimming)
|
|
378
|
+
if (contactValues[fieldName] === undefined || contactValues[fieldName] === null)
|
|
379
|
+
return;
|
|
380
|
+
const fieldType = typeof contactValues[fieldName];
|
|
381
|
+
if (fieldType !== 'string' && fieldType !== 'number') {
|
|
382
|
+
throw new n8n_workflow_1.ApplicationError(`Contact validation failed: ${fieldLabel} must be a string or number, not a complex object. ` +
|
|
383
|
+
'See: ' +
|
|
384
|
+
ZOHO_DESK_CREATE_TICKET_DOCS);
|
|
385
|
+
}
|
|
386
|
+
// Type check above guarantees only string/number reach this point (no objects/arrays)
|
|
387
|
+
// Safe to coerce to string since numbers will become their string representation
|
|
388
|
+
const fieldStr = String(contactValues[fieldName]).trim();
|
|
389
|
+
if (fieldStr !== '') {
|
|
390
|
+
// Sanitize CRLF characters (does NOT protect against XSS - handled by Zoho Desk API)
|
|
391
|
+
const cleaned = validateFieldLength(fieldStr, maxLength, `Contact ${fieldLabel}`);
|
|
392
|
+
// Additional validation for email field
|
|
393
|
+
if (fieldName === 'email' && !isValidEmail(cleaned)) {
|
|
394
|
+
throw new n8n_workflow_1.ApplicationError(`Contact validation failed: Invalid email format "${cleaned}". ` +
|
|
395
|
+
'See: ' +
|
|
396
|
+
ZOHO_DESK_CREATE_TICKET_DOCS);
|
|
397
|
+
}
|
|
398
|
+
contact[fieldName] = cleaned;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Fetch all pages from a paginated Zoho Desk API endpoint
|
|
403
|
+
* @param context - The IExecuteFunctions context
|
|
404
|
+
* @param baseUrl - API base URL
|
|
405
|
+
* @param endpoint - API endpoint (e.g., '/tickets')
|
|
406
|
+
* @param orgId - Organization ID
|
|
407
|
+
* @param limit - Items per page (max varies by endpoint, typically 50-200)
|
|
408
|
+
* @param dataKey - Response key containing data array (default: 'data')
|
|
409
|
+
* @param queryParams - Additional query parameters to include
|
|
410
|
+
* @returns Array of all items from all pages
|
|
411
|
+
*/
|
|
412
|
+
async function getAllPaginatedItems(context, baseUrl, endpoint, orgId, limit, dataKey = 'data', queryParams) {
|
|
413
|
+
const allItems = [];
|
|
414
|
+
let from = 1;
|
|
415
|
+
let hasMore = true;
|
|
416
|
+
while (hasMore) {
|
|
417
|
+
const options = {
|
|
418
|
+
method: 'GET',
|
|
419
|
+
headers: { orgId },
|
|
420
|
+
uri: `${baseUrl}${endpoint}`,
|
|
421
|
+
qs: { from, limit, ...queryParams },
|
|
422
|
+
json: true,
|
|
423
|
+
};
|
|
424
|
+
const response = await context.helpers.requestOAuth2.call(context, 'zohoDeskOAuth2Api', options);
|
|
425
|
+
const items = response[dataKey] || [];
|
|
426
|
+
allItems.push(...items);
|
|
427
|
+
if (items.length < limit) {
|
|
428
|
+
hasMore = false;
|
|
429
|
+
}
|
|
430
|
+
else {
|
|
431
|
+
from += limit;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
return allItems;
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Add common ticket fields (description, secondaryContacts, custom fields)
|
|
438
|
+
* This eliminates code duplication between create and update operations
|
|
439
|
+
* @param body - Target object to add fields to
|
|
440
|
+
* @param fields - Source object containing field values
|
|
441
|
+
* @param includePriorityAndDueDate - Whether to include priority and dueDate fields (true for update, false for create)
|
|
442
|
+
* For create operation: priority and dueDate are primary fields, not in additionalFields
|
|
443
|
+
* For update operation: priority and dueDate are optional fields in updateFields
|
|
444
|
+
*/
|
|
445
|
+
function addCommonTicketFields(body, fields, includePriorityAndDueDate = true) {
|
|
446
|
+
// Description is now a primary field, so it's not handled here anymore
|
|
447
|
+
// Only include dueDate and priority for update operation
|
|
448
|
+
// For create operation, these are primary fields set separately
|
|
449
|
+
if (includePriorityAndDueDate) {
|
|
450
|
+
if (fields.dueDate !== undefined) {
|
|
451
|
+
const dueDateValue = fields.dueDate;
|
|
452
|
+
if (dueDateValue && dueDateValue.trim() !== '') {
|
|
453
|
+
// Validate and normalize ISO 8601 string for Zoho Desk API
|
|
454
|
+
body.dueDate = convertDateToTimestamp(dueDateValue, 'Due Date', ZOHO_DESK_UPDATE_TICKET_DOCS);
|
|
455
|
+
}
|
|
456
|
+
else {
|
|
457
|
+
// Empty string means "no change" for update operation
|
|
458
|
+
body.dueDate = '';
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
if (fields.priority !== undefined) {
|
|
462
|
+
body.priority = fields.priority;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
if (fields.secondaryContacts !== undefined && typeof fields.secondaryContacts === 'string') {
|
|
466
|
+
const contacts = parseCommaSeparatedList(fields.secondaryContacts);
|
|
467
|
+
if (contacts.length > 0) {
|
|
468
|
+
body.secondaryContacts = contacts;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
if (fields.cf !== undefined) {
|
|
472
|
+
body.cf = parseCustomFields(fields.cf);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
class ZohoDesk {
|
|
476
|
+
constructor() {
|
|
477
|
+
this.description = {
|
|
478
|
+
displayName: 'Zoho Desk',
|
|
479
|
+
name: 'zohoDesk',
|
|
480
|
+
icon: 'file:zohoDesk.svg',
|
|
481
|
+
group: ['transform'],
|
|
482
|
+
version: 1,
|
|
483
|
+
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
|
|
484
|
+
description: 'Manage tickets, contacts, and accounts in Zoho Desk',
|
|
485
|
+
defaults: {
|
|
486
|
+
name: 'Zoho Desk',
|
|
487
|
+
},
|
|
488
|
+
inputs: ['main'],
|
|
489
|
+
outputs: ['main'],
|
|
490
|
+
credentials: [
|
|
491
|
+
{
|
|
492
|
+
name: 'zohoDeskOAuth2Api',
|
|
493
|
+
required: true,
|
|
494
|
+
},
|
|
495
|
+
],
|
|
496
|
+
properties: [
|
|
497
|
+
{
|
|
498
|
+
displayName: 'Resource',
|
|
499
|
+
name: 'resource',
|
|
500
|
+
type: 'options',
|
|
501
|
+
noDataExpression: true,
|
|
502
|
+
options: [
|
|
503
|
+
{
|
|
504
|
+
name: 'Account',
|
|
505
|
+
value: 'account',
|
|
506
|
+
},
|
|
507
|
+
{
|
|
508
|
+
name: 'Contact',
|
|
509
|
+
value: 'contact',
|
|
510
|
+
},
|
|
511
|
+
{
|
|
512
|
+
name: 'Ticket',
|
|
513
|
+
value: 'ticket',
|
|
514
|
+
},
|
|
515
|
+
],
|
|
516
|
+
default: 'ticket',
|
|
517
|
+
},
|
|
518
|
+
// ==================== TICKET OPERATIONS ====================
|
|
519
|
+
{
|
|
520
|
+
displayName: 'Operation',
|
|
521
|
+
name: 'operation',
|
|
522
|
+
type: 'options',
|
|
523
|
+
noDataExpression: true,
|
|
524
|
+
displayOptions: {
|
|
525
|
+
show: {
|
|
526
|
+
resource: ['ticket'],
|
|
527
|
+
},
|
|
528
|
+
},
|
|
529
|
+
options: [
|
|
530
|
+
{
|
|
531
|
+
name: 'Add Comment',
|
|
532
|
+
value: 'addComment',
|
|
533
|
+
description: 'Add a comment to a ticket',
|
|
534
|
+
action: 'Add comment to ticket',
|
|
535
|
+
},
|
|
536
|
+
{
|
|
537
|
+
name: 'Create',
|
|
538
|
+
value: 'create',
|
|
539
|
+
description: 'Create a new ticket',
|
|
540
|
+
action: 'Create a ticket',
|
|
541
|
+
},
|
|
542
|
+
{
|
|
543
|
+
name: 'Delete',
|
|
544
|
+
value: 'delete',
|
|
545
|
+
description: 'Delete a ticket',
|
|
546
|
+
action: 'Delete a ticket',
|
|
547
|
+
},
|
|
548
|
+
{
|
|
549
|
+
name: 'Get',
|
|
550
|
+
value: 'get',
|
|
551
|
+
description: 'Get a ticket by ID',
|
|
552
|
+
action: 'Get a ticket',
|
|
553
|
+
},
|
|
554
|
+
{
|
|
555
|
+
name: 'List',
|
|
556
|
+
value: 'list',
|
|
557
|
+
description: 'List all tickets',
|
|
558
|
+
action: 'List tickets',
|
|
559
|
+
},
|
|
560
|
+
{
|
|
561
|
+
name: 'List Threads',
|
|
562
|
+
value: 'listThreads',
|
|
563
|
+
description: 'List ticket conversations/threads',
|
|
564
|
+
action: 'List ticket threads',
|
|
565
|
+
},
|
|
566
|
+
{
|
|
567
|
+
name: 'Update',
|
|
568
|
+
value: 'update',
|
|
569
|
+
description: 'Update an existing ticket',
|
|
570
|
+
action: 'Update a ticket',
|
|
571
|
+
},
|
|
572
|
+
],
|
|
573
|
+
default: 'create',
|
|
574
|
+
},
|
|
575
|
+
// ==================== CONTACT OPERATIONS ====================
|
|
576
|
+
{
|
|
577
|
+
displayName: 'Operation',
|
|
578
|
+
name: 'operation',
|
|
579
|
+
type: 'options',
|
|
580
|
+
noDataExpression: true,
|
|
581
|
+
displayOptions: {
|
|
582
|
+
show: {
|
|
583
|
+
resource: ['contact'],
|
|
584
|
+
},
|
|
585
|
+
},
|
|
586
|
+
options: [
|
|
587
|
+
{
|
|
588
|
+
name: 'Create',
|
|
589
|
+
value: 'create',
|
|
590
|
+
description: 'Create a new contact',
|
|
591
|
+
action: 'Create a contact',
|
|
592
|
+
},
|
|
593
|
+
{
|
|
594
|
+
name: 'Delete',
|
|
595
|
+
value: 'delete',
|
|
596
|
+
description: 'Delete a contact',
|
|
597
|
+
action: 'Delete a contact',
|
|
598
|
+
},
|
|
599
|
+
{
|
|
600
|
+
name: 'Get',
|
|
601
|
+
value: 'get',
|
|
602
|
+
description: 'Get a contact by ID',
|
|
603
|
+
action: 'Get a contact',
|
|
604
|
+
},
|
|
605
|
+
{
|
|
606
|
+
name: 'List',
|
|
607
|
+
value: 'list',
|
|
608
|
+
description: 'List all contacts',
|
|
609
|
+
action: 'List contacts',
|
|
610
|
+
},
|
|
611
|
+
{
|
|
612
|
+
name: 'Update',
|
|
613
|
+
value: 'update',
|
|
614
|
+
description: 'Update a contact',
|
|
615
|
+
action: 'Update a contact',
|
|
616
|
+
},
|
|
617
|
+
],
|
|
618
|
+
default: 'create',
|
|
619
|
+
},
|
|
620
|
+
// ==================== ACCOUNT OPERATIONS ====================
|
|
621
|
+
{
|
|
622
|
+
displayName: 'Operation',
|
|
623
|
+
name: 'operation',
|
|
624
|
+
type: 'options',
|
|
625
|
+
noDataExpression: true,
|
|
626
|
+
displayOptions: {
|
|
627
|
+
show: {
|
|
628
|
+
resource: ['account'],
|
|
629
|
+
},
|
|
630
|
+
},
|
|
631
|
+
options: [
|
|
632
|
+
{
|
|
633
|
+
name: 'Create',
|
|
634
|
+
value: 'create',
|
|
635
|
+
description: 'Create a new account',
|
|
636
|
+
action: 'Create an account',
|
|
637
|
+
},
|
|
638
|
+
{
|
|
639
|
+
name: 'Delete',
|
|
640
|
+
value: 'delete',
|
|
641
|
+
description: 'Delete an account',
|
|
642
|
+
action: 'Delete an account',
|
|
643
|
+
},
|
|
644
|
+
{
|
|
645
|
+
name: 'Get',
|
|
646
|
+
value: 'get',
|
|
647
|
+
description: 'Get an account by ID',
|
|
648
|
+
action: 'Get an account',
|
|
649
|
+
},
|
|
650
|
+
{
|
|
651
|
+
name: 'List',
|
|
652
|
+
value: 'list',
|
|
653
|
+
description: 'List all accounts',
|
|
654
|
+
action: 'List accounts',
|
|
655
|
+
},
|
|
656
|
+
{
|
|
657
|
+
name: 'Update',
|
|
658
|
+
value: 'update',
|
|
659
|
+
description: 'Update an account',
|
|
660
|
+
action: 'Update an account',
|
|
661
|
+
},
|
|
662
|
+
],
|
|
663
|
+
default: 'create',
|
|
664
|
+
},
|
|
665
|
+
// Create Operation Fields
|
|
666
|
+
{
|
|
667
|
+
displayName: 'Department Name or ID',
|
|
668
|
+
name: 'departmentId',
|
|
669
|
+
type: 'options',
|
|
670
|
+
typeOptions: {
|
|
671
|
+
loadOptionsMethod: 'getDepartments',
|
|
672
|
+
},
|
|
673
|
+
required: true,
|
|
674
|
+
displayOptions: {
|
|
675
|
+
show: {
|
|
676
|
+
resource: ['ticket'],
|
|
677
|
+
operation: ['create'],
|
|
678
|
+
},
|
|
679
|
+
},
|
|
680
|
+
default: '',
|
|
681
|
+
description: 'The department to which the ticket belongs. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
|
|
682
|
+
},
|
|
683
|
+
{
|
|
684
|
+
displayName: 'Team Name or ID',
|
|
685
|
+
name: 'teamId',
|
|
686
|
+
type: 'options',
|
|
687
|
+
typeOptions: {
|
|
688
|
+
loadOptionsMethod: 'getTeams',
|
|
689
|
+
loadOptionsDependsOn: ['departmentId'],
|
|
690
|
+
},
|
|
691
|
+
displayOptions: {
|
|
692
|
+
show: {
|
|
693
|
+
resource: ['ticket'],
|
|
694
|
+
operation: ['create'],
|
|
695
|
+
},
|
|
696
|
+
},
|
|
697
|
+
default: '',
|
|
698
|
+
description: 'The team assigned to resolve the ticket. Teams will only load if Department is selected first. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
|
|
699
|
+
},
|
|
700
|
+
{
|
|
701
|
+
displayName: 'Subject',
|
|
702
|
+
name: 'subject',
|
|
703
|
+
type: 'string',
|
|
704
|
+
required: true,
|
|
705
|
+
displayOptions: {
|
|
706
|
+
show: {
|
|
707
|
+
resource: ['ticket'],
|
|
708
|
+
operation: ['create'],
|
|
709
|
+
},
|
|
710
|
+
},
|
|
711
|
+
default: '',
|
|
712
|
+
description: 'Subject of the ticket',
|
|
713
|
+
},
|
|
714
|
+
{
|
|
715
|
+
displayName: 'Contact',
|
|
716
|
+
name: 'contact',
|
|
717
|
+
type: 'fixedCollection',
|
|
718
|
+
typeOptions: {
|
|
719
|
+
multipleValues: false,
|
|
720
|
+
},
|
|
721
|
+
displayOptions: {
|
|
722
|
+
show: {
|
|
723
|
+
resource: ['ticket'],
|
|
724
|
+
operation: ['create'],
|
|
725
|
+
},
|
|
726
|
+
},
|
|
727
|
+
default: {},
|
|
728
|
+
description: 'Details of the contact who raised the ticket. If email exists, contactId is used; otherwise, a new contact is created. Either lastName or email must be present.',
|
|
729
|
+
options: [
|
|
730
|
+
{
|
|
731
|
+
name: 'contactValues',
|
|
732
|
+
displayName: 'Contact Details',
|
|
733
|
+
values: [
|
|
734
|
+
{
|
|
735
|
+
displayName: 'Email',
|
|
736
|
+
name: 'email',
|
|
737
|
+
type: 'string',
|
|
738
|
+
placeholder: 'name@email.com',
|
|
739
|
+
default: '',
|
|
740
|
+
description: 'Email address of the contact (required if lastName is not provided)',
|
|
741
|
+
},
|
|
742
|
+
{
|
|
743
|
+
displayName: 'First Name',
|
|
744
|
+
name: 'firstName',
|
|
745
|
+
type: 'string',
|
|
746
|
+
default: '',
|
|
747
|
+
description: 'First name of the contact',
|
|
748
|
+
},
|
|
749
|
+
{
|
|
750
|
+
displayName: 'Last Name',
|
|
751
|
+
name: 'lastName',
|
|
752
|
+
type: 'string',
|
|
753
|
+
default: '',
|
|
754
|
+
description: 'Last name of the contact (required if email is not provided)',
|
|
755
|
+
},
|
|
756
|
+
{
|
|
757
|
+
displayName: 'Phone',
|
|
758
|
+
name: 'phone',
|
|
759
|
+
type: 'string',
|
|
760
|
+
default: '',
|
|
761
|
+
description: 'Phone number of the contact',
|
|
762
|
+
},
|
|
763
|
+
],
|
|
764
|
+
},
|
|
765
|
+
],
|
|
766
|
+
},
|
|
767
|
+
{
|
|
768
|
+
displayName: 'Priority',
|
|
769
|
+
name: 'priority',
|
|
770
|
+
type: 'options',
|
|
771
|
+
displayOptions: {
|
|
772
|
+
show: {
|
|
773
|
+
resource: ['ticket'],
|
|
774
|
+
operation: ['create'],
|
|
775
|
+
},
|
|
776
|
+
},
|
|
777
|
+
options: [
|
|
778
|
+
{
|
|
779
|
+
name: 'Low',
|
|
780
|
+
value: 'Low',
|
|
781
|
+
},
|
|
782
|
+
{
|
|
783
|
+
name: 'Medium',
|
|
784
|
+
value: 'Medium',
|
|
785
|
+
},
|
|
786
|
+
{
|
|
787
|
+
name: 'High',
|
|
788
|
+
value: 'High',
|
|
789
|
+
},
|
|
790
|
+
],
|
|
791
|
+
default: 'Medium',
|
|
792
|
+
description: 'Priority of the ticket',
|
|
793
|
+
},
|
|
794
|
+
{
|
|
795
|
+
displayName: 'Classification',
|
|
796
|
+
name: 'classification',
|
|
797
|
+
type: 'options',
|
|
798
|
+
displayOptions: {
|
|
799
|
+
show: {
|
|
800
|
+
resource: ['ticket'],
|
|
801
|
+
operation: ['create'],
|
|
802
|
+
},
|
|
803
|
+
},
|
|
804
|
+
options: [
|
|
805
|
+
{
|
|
806
|
+
name: 'Question',
|
|
807
|
+
value: 'Question',
|
|
808
|
+
},
|
|
809
|
+
{
|
|
810
|
+
name: 'Problem',
|
|
811
|
+
value: 'Problem',
|
|
812
|
+
},
|
|
813
|
+
{
|
|
814
|
+
name: 'Request',
|
|
815
|
+
value: 'Request',
|
|
816
|
+
},
|
|
817
|
+
{
|
|
818
|
+
name: 'Others',
|
|
819
|
+
value: 'Others',
|
|
820
|
+
},
|
|
821
|
+
],
|
|
822
|
+
default: 'Question',
|
|
823
|
+
description: 'Classification of the ticket',
|
|
824
|
+
},
|
|
825
|
+
{
|
|
826
|
+
displayName: 'Due Date',
|
|
827
|
+
name: 'dueDate',
|
|
828
|
+
type: 'dateTime',
|
|
829
|
+
displayOptions: {
|
|
830
|
+
show: {
|
|
831
|
+
resource: ['ticket'],
|
|
832
|
+
operation: ['create'],
|
|
833
|
+
},
|
|
834
|
+
},
|
|
835
|
+
default: '',
|
|
836
|
+
description: 'The due date for resolving the ticket',
|
|
837
|
+
},
|
|
838
|
+
{
|
|
839
|
+
displayName: 'Description',
|
|
840
|
+
name: 'description',
|
|
841
|
+
type: 'string',
|
|
842
|
+
typeOptions: {
|
|
843
|
+
rows: 5,
|
|
844
|
+
},
|
|
845
|
+
displayOptions: {
|
|
846
|
+
show: {
|
|
847
|
+
resource: ['ticket'],
|
|
848
|
+
operation: ['create'],
|
|
849
|
+
},
|
|
850
|
+
},
|
|
851
|
+
default: '',
|
|
852
|
+
description: 'Description of the ticket',
|
|
853
|
+
},
|
|
854
|
+
{
|
|
855
|
+
displayName: 'Additional Fields',
|
|
856
|
+
name: 'additionalFields',
|
|
857
|
+
type: 'collection',
|
|
858
|
+
placeholder: 'Add Field',
|
|
859
|
+
default: {},
|
|
860
|
+
displayOptions: {
|
|
861
|
+
show: {
|
|
862
|
+
resource: ['ticket'],
|
|
863
|
+
operation: ['create'],
|
|
864
|
+
},
|
|
865
|
+
},
|
|
866
|
+
options: [
|
|
867
|
+
{
|
|
868
|
+
displayName: 'Account ID',
|
|
869
|
+
name: 'accountId',
|
|
870
|
+
type: 'string',
|
|
871
|
+
default: '',
|
|
872
|
+
description: 'The ID of the account associated with the ticket',
|
|
873
|
+
},
|
|
874
|
+
{
|
|
875
|
+
displayName: 'Assignee ID',
|
|
876
|
+
name: 'assigneeId',
|
|
877
|
+
type: 'string',
|
|
878
|
+
default: '',
|
|
879
|
+
description: 'The ID of the agent to whom the ticket is assigned',
|
|
880
|
+
},
|
|
881
|
+
{
|
|
882
|
+
displayName: 'Category',
|
|
883
|
+
name: 'category',
|
|
884
|
+
type: 'string',
|
|
885
|
+
default: '',
|
|
886
|
+
description: 'Category to which the ticket belongs',
|
|
887
|
+
},
|
|
888
|
+
{
|
|
889
|
+
displayName: 'Channel',
|
|
890
|
+
name: 'channel',
|
|
891
|
+
type: 'options',
|
|
892
|
+
options: [
|
|
893
|
+
{
|
|
894
|
+
name: 'Chat',
|
|
895
|
+
value: 'CHAT',
|
|
896
|
+
},
|
|
897
|
+
{
|
|
898
|
+
name: 'Email',
|
|
899
|
+
value: 'EMAIL',
|
|
900
|
+
},
|
|
901
|
+
{
|
|
902
|
+
name: 'Facebook',
|
|
903
|
+
value: 'FACEBOOK',
|
|
904
|
+
},
|
|
905
|
+
{
|
|
906
|
+
name: 'Feedback Widget',
|
|
907
|
+
value: 'FEEDBACK_WIDGET',
|
|
908
|
+
},
|
|
909
|
+
{
|
|
910
|
+
name: 'Forums',
|
|
911
|
+
value: 'FORUMS',
|
|
912
|
+
},
|
|
913
|
+
{
|
|
914
|
+
name: 'Phone',
|
|
915
|
+
value: 'PHONE',
|
|
916
|
+
},
|
|
917
|
+
{
|
|
918
|
+
name: 'Twitter',
|
|
919
|
+
value: 'TWITTER',
|
|
920
|
+
},
|
|
921
|
+
{
|
|
922
|
+
name: 'Web',
|
|
923
|
+
value: 'WEB',
|
|
924
|
+
},
|
|
925
|
+
],
|
|
926
|
+
default: 'EMAIL',
|
|
927
|
+
description: 'The channel through which the ticket was created',
|
|
928
|
+
},
|
|
929
|
+
{
|
|
930
|
+
displayName: 'Custom Fields',
|
|
931
|
+
name: 'cf',
|
|
932
|
+
type: 'json',
|
|
933
|
+
default: '',
|
|
934
|
+
description: 'Custom fields as JSON object',
|
|
935
|
+
placeholder: '{"cf_modelname": "F3 2017", "cf_phone": "123456"}',
|
|
936
|
+
},
|
|
937
|
+
{
|
|
938
|
+
displayName: 'Email',
|
|
939
|
+
name: 'email',
|
|
940
|
+
type: 'string',
|
|
941
|
+
placeholder: 'name@email.com',
|
|
942
|
+
default: '',
|
|
943
|
+
description: 'Email address of the contact',
|
|
944
|
+
},
|
|
945
|
+
{
|
|
946
|
+
displayName: 'Language',
|
|
947
|
+
name: 'language',
|
|
948
|
+
type: 'string',
|
|
949
|
+
default: '',
|
|
950
|
+
description: 'Language in which the ticket was created',
|
|
951
|
+
},
|
|
952
|
+
{
|
|
953
|
+
displayName: 'Phone',
|
|
954
|
+
name: 'phone',
|
|
955
|
+
type: 'string',
|
|
956
|
+
default: '',
|
|
957
|
+
description: 'Phone number of the contact',
|
|
958
|
+
},
|
|
959
|
+
{
|
|
960
|
+
displayName: 'Product ID',
|
|
961
|
+
name: 'productId',
|
|
962
|
+
type: 'string',
|
|
963
|
+
default: '',
|
|
964
|
+
description: 'The ID of the product associated with the ticket',
|
|
965
|
+
},
|
|
966
|
+
{
|
|
967
|
+
displayName: 'Resolution',
|
|
968
|
+
name: 'resolution',
|
|
969
|
+
type: 'string',
|
|
970
|
+
typeOptions: {
|
|
971
|
+
rows: 5,
|
|
972
|
+
},
|
|
973
|
+
default: '',
|
|
974
|
+
description: 'Resolution content of the ticket',
|
|
975
|
+
},
|
|
976
|
+
{
|
|
977
|
+
displayName: 'Secondary Contacts',
|
|
978
|
+
name: 'secondaryContacts',
|
|
979
|
+
type: 'string',
|
|
980
|
+
default: '',
|
|
981
|
+
description: 'Comma-separated list of contact IDs for secondary contacts',
|
|
982
|
+
placeholder: '1892000000042038, 1892000000042042',
|
|
983
|
+
},
|
|
984
|
+
{
|
|
985
|
+
displayName: 'Status',
|
|
986
|
+
name: 'status',
|
|
987
|
+
type: 'string',
|
|
988
|
+
default: DEFAULT_TICKET_STATUS,
|
|
989
|
+
description: `Status of the ticket (default: ${DEFAULT_TICKET_STATUS} for new tickets)`,
|
|
990
|
+
},
|
|
991
|
+
{
|
|
992
|
+
displayName: 'Sub Category',
|
|
993
|
+
name: 'subCategory',
|
|
994
|
+
type: 'string',
|
|
995
|
+
default: '',
|
|
996
|
+
description: 'Sub-category to which the ticket belongs',
|
|
997
|
+
},
|
|
998
|
+
],
|
|
999
|
+
},
|
|
1000
|
+
// ==================== TICKET: GET/DELETE/UPDATE/ADDCOMMENT/LISTTHREADS ====================
|
|
1001
|
+
{
|
|
1002
|
+
displayName: 'Ticket ID',
|
|
1003
|
+
name: 'ticketId',
|
|
1004
|
+
type: 'string',
|
|
1005
|
+
required: true,
|
|
1006
|
+
displayOptions: {
|
|
1007
|
+
show: {
|
|
1008
|
+
resource: ['ticket'],
|
|
1009
|
+
operation: ['get', 'delete', 'update', 'addComment', 'listThreads'],
|
|
1010
|
+
},
|
|
1011
|
+
},
|
|
1012
|
+
default: '',
|
|
1013
|
+
description: 'The ID of the ticket',
|
|
1014
|
+
},
|
|
1015
|
+
// ==================== TICKET: LIST ====================
|
|
1016
|
+
{
|
|
1017
|
+
displayName: 'Return All',
|
|
1018
|
+
name: 'returnAll',
|
|
1019
|
+
type: 'boolean',
|
|
1020
|
+
displayOptions: {
|
|
1021
|
+
show: {
|
|
1022
|
+
resource: ['ticket'],
|
|
1023
|
+
operation: ['list'],
|
|
1024
|
+
},
|
|
1025
|
+
},
|
|
1026
|
+
default: false,
|
|
1027
|
+
description: 'Whether to return all results or only up to a given limit',
|
|
1028
|
+
},
|
|
1029
|
+
{
|
|
1030
|
+
displayName: 'Limit',
|
|
1031
|
+
name: 'limit',
|
|
1032
|
+
type: 'number',
|
|
1033
|
+
displayOptions: {
|
|
1034
|
+
show: {
|
|
1035
|
+
resource: ['ticket'],
|
|
1036
|
+
operation: ['list'],
|
|
1037
|
+
returnAll: [false],
|
|
1038
|
+
},
|
|
1039
|
+
},
|
|
1040
|
+
typeOptions: {
|
|
1041
|
+
minValue: 1,
|
|
1042
|
+
},
|
|
1043
|
+
default: 50,
|
|
1044
|
+
description: 'Max number of results to return',
|
|
1045
|
+
},
|
|
1046
|
+
{
|
|
1047
|
+
displayName: 'Filters',
|
|
1048
|
+
name: 'filters',
|
|
1049
|
+
type: 'collection',
|
|
1050
|
+
placeholder: 'Add Filter',
|
|
1051
|
+
default: {},
|
|
1052
|
+
displayOptions: {
|
|
1053
|
+
show: {
|
|
1054
|
+
resource: ['ticket'],
|
|
1055
|
+
operation: ['list'],
|
|
1056
|
+
},
|
|
1057
|
+
},
|
|
1058
|
+
options: [
|
|
1059
|
+
{
|
|
1060
|
+
displayName: 'Assignee ID',
|
|
1061
|
+
name: 'assigneeId',
|
|
1062
|
+
type: 'string',
|
|
1063
|
+
default: '',
|
|
1064
|
+
description: 'Filter by assignee ID',
|
|
1065
|
+
},
|
|
1066
|
+
{
|
|
1067
|
+
displayName: 'Department ID',
|
|
1068
|
+
name: 'departmentId',
|
|
1069
|
+
type: 'string',
|
|
1070
|
+
default: '',
|
|
1071
|
+
description: 'Filter by department ID',
|
|
1072
|
+
},
|
|
1073
|
+
{
|
|
1074
|
+
displayName: 'Status',
|
|
1075
|
+
name: 'status',
|
|
1076
|
+
type: 'string',
|
|
1077
|
+
default: '',
|
|
1078
|
+
description: 'Filter by status (e.g., Open, On Hold, Closed)',
|
|
1079
|
+
},
|
|
1080
|
+
],
|
|
1081
|
+
},
|
|
1082
|
+
// ==================== TICKET: ADD COMMENT ====================
|
|
1083
|
+
{
|
|
1084
|
+
displayName: 'Comment Content',
|
|
1085
|
+
name: 'content',
|
|
1086
|
+
type: 'string',
|
|
1087
|
+
typeOptions: {
|
|
1088
|
+
rows: 4,
|
|
1089
|
+
},
|
|
1090
|
+
required: true,
|
|
1091
|
+
displayOptions: {
|
|
1092
|
+
show: {
|
|
1093
|
+
resource: ['ticket'],
|
|
1094
|
+
operation: ['addComment'],
|
|
1095
|
+
},
|
|
1096
|
+
},
|
|
1097
|
+
default: '',
|
|
1098
|
+
description: 'The content of the comment',
|
|
1099
|
+
},
|
|
1100
|
+
{
|
|
1101
|
+
displayName: 'Is Public',
|
|
1102
|
+
name: 'isPublic',
|
|
1103
|
+
type: 'boolean',
|
|
1104
|
+
displayOptions: {
|
|
1105
|
+
show: {
|
|
1106
|
+
resource: ['ticket'],
|
|
1107
|
+
operation: ['addComment'],
|
|
1108
|
+
},
|
|
1109
|
+
},
|
|
1110
|
+
default: true,
|
|
1111
|
+
description: 'Whether the comment is visible to customers (public) or internal only (private)',
|
|
1112
|
+
},
|
|
1113
|
+
// ==================== TICKET: LIST THREADS ====================
|
|
1114
|
+
{
|
|
1115
|
+
displayName: 'Return All',
|
|
1116
|
+
name: 'returnAll',
|
|
1117
|
+
type: 'boolean',
|
|
1118
|
+
displayOptions: {
|
|
1119
|
+
show: {
|
|
1120
|
+
resource: ['ticket'],
|
|
1121
|
+
operation: ['listThreads'],
|
|
1122
|
+
},
|
|
1123
|
+
},
|
|
1124
|
+
default: false,
|
|
1125
|
+
description: 'Whether to return all threads or only up to a given limit',
|
|
1126
|
+
},
|
|
1127
|
+
{
|
|
1128
|
+
displayName: 'Limit',
|
|
1129
|
+
name: 'limit',
|
|
1130
|
+
type: 'number',
|
|
1131
|
+
displayOptions: {
|
|
1132
|
+
show: {
|
|
1133
|
+
resource: ['ticket'],
|
|
1134
|
+
operation: ['listThreads'],
|
|
1135
|
+
returnAll: [false],
|
|
1136
|
+
},
|
|
1137
|
+
},
|
|
1138
|
+
typeOptions: {
|
|
1139
|
+
minValue: 1,
|
|
1140
|
+
},
|
|
1141
|
+
default: 50,
|
|
1142
|
+
description: 'Max number of results to return',
|
|
1143
|
+
},
|
|
1144
|
+
// ==================== TICKET: UPDATE ====================
|
|
1145
|
+
{
|
|
1146
|
+
displayName: 'Description',
|
|
1147
|
+
name: 'description',
|
|
1148
|
+
type: 'string',
|
|
1149
|
+
typeOptions: {
|
|
1150
|
+
rows: 5,
|
|
1151
|
+
},
|
|
1152
|
+
displayOptions: {
|
|
1153
|
+
show: {
|
|
1154
|
+
resource: ['ticket'],
|
|
1155
|
+
operation: ['update'],
|
|
1156
|
+
},
|
|
1157
|
+
},
|
|
1158
|
+
default: '',
|
|
1159
|
+
description: 'Description of the ticket',
|
|
1160
|
+
},
|
|
1161
|
+
{
|
|
1162
|
+
displayName: 'Update Fields',
|
|
1163
|
+
name: 'updateFields',
|
|
1164
|
+
type: 'collection',
|
|
1165
|
+
placeholder: 'Add Field',
|
|
1166
|
+
default: {},
|
|
1167
|
+
displayOptions: {
|
|
1168
|
+
show: {
|
|
1169
|
+
resource: ['ticket'],
|
|
1170
|
+
operation: ['update'],
|
|
1171
|
+
},
|
|
1172
|
+
},
|
|
1173
|
+
options: [
|
|
1174
|
+
{
|
|
1175
|
+
displayName: 'Account ID',
|
|
1176
|
+
name: 'accountId',
|
|
1177
|
+
type: 'string',
|
|
1178
|
+
default: '',
|
|
1179
|
+
description: 'The ID of the account associated with the ticket',
|
|
1180
|
+
},
|
|
1181
|
+
{
|
|
1182
|
+
displayName: 'Assignee ID',
|
|
1183
|
+
name: 'assigneeId',
|
|
1184
|
+
type: 'string',
|
|
1185
|
+
default: '',
|
|
1186
|
+
description: 'The ID of the agent to whom the ticket is assigned',
|
|
1187
|
+
},
|
|
1188
|
+
{
|
|
1189
|
+
displayName: 'Category',
|
|
1190
|
+
name: 'category',
|
|
1191
|
+
type: 'string',
|
|
1192
|
+
default: '',
|
|
1193
|
+
description: 'Category to which the ticket belongs',
|
|
1194
|
+
},
|
|
1195
|
+
{
|
|
1196
|
+
displayName: 'Classification',
|
|
1197
|
+
name: 'classification',
|
|
1198
|
+
type: 'options',
|
|
1199
|
+
options: [
|
|
1200
|
+
{
|
|
1201
|
+
name: 'No Change',
|
|
1202
|
+
value: '',
|
|
1203
|
+
},
|
|
1204
|
+
{
|
|
1205
|
+
name: 'Others',
|
|
1206
|
+
value: 'Others',
|
|
1207
|
+
},
|
|
1208
|
+
{
|
|
1209
|
+
name: 'Problem',
|
|
1210
|
+
value: 'Problem',
|
|
1211
|
+
},
|
|
1212
|
+
{
|
|
1213
|
+
name: 'Question',
|
|
1214
|
+
value: 'Question',
|
|
1215
|
+
},
|
|
1216
|
+
{
|
|
1217
|
+
name: 'Request',
|
|
1218
|
+
value: 'Request',
|
|
1219
|
+
},
|
|
1220
|
+
],
|
|
1221
|
+
default: '',
|
|
1222
|
+
description: 'Classification of the ticket',
|
|
1223
|
+
},
|
|
1224
|
+
{
|
|
1225
|
+
displayName: 'Contact ID',
|
|
1226
|
+
name: 'contactId',
|
|
1227
|
+
type: 'string',
|
|
1228
|
+
default: '',
|
|
1229
|
+
description: 'The ID of the contact who raised the ticket',
|
|
1230
|
+
},
|
|
1231
|
+
{
|
|
1232
|
+
displayName: 'Custom Fields',
|
|
1233
|
+
name: 'cf',
|
|
1234
|
+
type: 'json',
|
|
1235
|
+
default: '',
|
|
1236
|
+
description: 'Custom fields as JSON object',
|
|
1237
|
+
placeholder: '{"cf_modelname": "F3 2017", "cf_phone": "123456"}',
|
|
1238
|
+
},
|
|
1239
|
+
{
|
|
1240
|
+
displayName: 'Department Name or ID',
|
|
1241
|
+
name: 'departmentId',
|
|
1242
|
+
type: 'options',
|
|
1243
|
+
typeOptions: {
|
|
1244
|
+
loadOptionsMethod: 'getDepartments',
|
|
1245
|
+
},
|
|
1246
|
+
default: '',
|
|
1247
|
+
description: 'The department to which the ticket belongs. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
|
|
1248
|
+
},
|
|
1249
|
+
{
|
|
1250
|
+
displayName: 'Due Date',
|
|
1251
|
+
name: 'dueDate',
|
|
1252
|
+
type: 'dateTime',
|
|
1253
|
+
default: '',
|
|
1254
|
+
description: 'The due date for resolving the ticket (leave empty to keep current due date)',
|
|
1255
|
+
},
|
|
1256
|
+
{
|
|
1257
|
+
displayName: 'Email',
|
|
1258
|
+
name: 'email',
|
|
1259
|
+
type: 'string',
|
|
1260
|
+
placeholder: 'name@email.com',
|
|
1261
|
+
default: '',
|
|
1262
|
+
description: 'Email address of the contact',
|
|
1263
|
+
},
|
|
1264
|
+
{
|
|
1265
|
+
displayName: 'Language',
|
|
1266
|
+
name: 'language',
|
|
1267
|
+
type: 'string',
|
|
1268
|
+
default: '',
|
|
1269
|
+
description: 'Language in which the ticket was created',
|
|
1270
|
+
},
|
|
1271
|
+
{
|
|
1272
|
+
displayName: 'Phone',
|
|
1273
|
+
name: 'phone',
|
|
1274
|
+
type: 'string',
|
|
1275
|
+
default: '',
|
|
1276
|
+
description: 'Phone number of the contact',
|
|
1277
|
+
},
|
|
1278
|
+
{
|
|
1279
|
+
displayName: 'Priority',
|
|
1280
|
+
name: 'priority',
|
|
1281
|
+
type: 'options',
|
|
1282
|
+
options: [
|
|
1283
|
+
{
|
|
1284
|
+
name: 'No Change',
|
|
1285
|
+
value: '',
|
|
1286
|
+
},
|
|
1287
|
+
{
|
|
1288
|
+
name: 'Low',
|
|
1289
|
+
value: 'Low',
|
|
1290
|
+
},
|
|
1291
|
+
{
|
|
1292
|
+
name: 'Medium',
|
|
1293
|
+
value: 'Medium',
|
|
1294
|
+
},
|
|
1295
|
+
{
|
|
1296
|
+
name: 'High',
|
|
1297
|
+
value: 'High',
|
|
1298
|
+
},
|
|
1299
|
+
],
|
|
1300
|
+
default: '',
|
|
1301
|
+
description: 'Priority of the ticket',
|
|
1302
|
+
},
|
|
1303
|
+
{
|
|
1304
|
+
displayName: 'Product ID',
|
|
1305
|
+
name: 'productId',
|
|
1306
|
+
type: 'string',
|
|
1307
|
+
default: '',
|
|
1308
|
+
description: 'The ID of the product associated with the ticket',
|
|
1309
|
+
},
|
|
1310
|
+
{
|
|
1311
|
+
displayName: 'Resolution',
|
|
1312
|
+
name: 'resolution',
|
|
1313
|
+
type: 'string',
|
|
1314
|
+
typeOptions: {
|
|
1315
|
+
rows: 5,
|
|
1316
|
+
},
|
|
1317
|
+
default: '',
|
|
1318
|
+
description: 'Resolution content of the ticket',
|
|
1319
|
+
},
|
|
1320
|
+
{
|
|
1321
|
+
displayName: 'Secondary Contacts',
|
|
1322
|
+
name: 'secondaryContacts',
|
|
1323
|
+
type: 'string',
|
|
1324
|
+
default: '',
|
|
1325
|
+
description: 'Comma-separated list of contact IDs for secondary contacts',
|
|
1326
|
+
placeholder: '1892000000042038, 1892000000042042',
|
|
1327
|
+
},
|
|
1328
|
+
{
|
|
1329
|
+
displayName: 'Status',
|
|
1330
|
+
name: 'status',
|
|
1331
|
+
type: 'string',
|
|
1332
|
+
default: '',
|
|
1333
|
+
description: 'Status of the ticket (leave empty to keep current status)',
|
|
1334
|
+
},
|
|
1335
|
+
{
|
|
1336
|
+
displayName: 'Sub Category',
|
|
1337
|
+
name: 'subCategory',
|
|
1338
|
+
type: 'string',
|
|
1339
|
+
default: '',
|
|
1340
|
+
description: 'Sub-category to which the ticket belongs',
|
|
1341
|
+
},
|
|
1342
|
+
{
|
|
1343
|
+
displayName: 'Subject',
|
|
1344
|
+
name: 'subject',
|
|
1345
|
+
type: 'string',
|
|
1346
|
+
default: '',
|
|
1347
|
+
description: 'Subject of the ticket',
|
|
1348
|
+
},
|
|
1349
|
+
{
|
|
1350
|
+
displayName: 'Team Name or ID',
|
|
1351
|
+
name: 'teamId',
|
|
1352
|
+
type: 'options',
|
|
1353
|
+
typeOptions: {
|
|
1354
|
+
loadOptionsMethod: 'getTeams',
|
|
1355
|
+
loadOptionsDependsOn: ['departmentId'],
|
|
1356
|
+
},
|
|
1357
|
+
default: '',
|
|
1358
|
+
description: 'The team assigned to the ticket. Note: Teams will only load if Department is selected first. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
|
|
1359
|
+
},
|
|
1360
|
+
],
|
|
1361
|
+
},
|
|
1362
|
+
// ==================== CONTACT PARAMETERS ====================
|
|
1363
|
+
// Contact: Create
|
|
1364
|
+
{
|
|
1365
|
+
displayName: 'Last Name',
|
|
1366
|
+
name: 'lastName',
|
|
1367
|
+
type: 'string',
|
|
1368
|
+
required: true,
|
|
1369
|
+
displayOptions: {
|
|
1370
|
+
show: {
|
|
1371
|
+
resource: ['contact'],
|
|
1372
|
+
operation: ['create'],
|
|
1373
|
+
},
|
|
1374
|
+
},
|
|
1375
|
+
default: '',
|
|
1376
|
+
description: 'Last name of the contact',
|
|
1377
|
+
},
|
|
1378
|
+
{
|
|
1379
|
+
displayName: 'Email',
|
|
1380
|
+
name: 'email',
|
|
1381
|
+
type: 'string',
|
|
1382
|
+
placeholder: 'name@email.com',
|
|
1383
|
+
required: true,
|
|
1384
|
+
displayOptions: {
|
|
1385
|
+
show: {
|
|
1386
|
+
resource: ['contact'],
|
|
1387
|
+
operation: ['create'],
|
|
1388
|
+
},
|
|
1389
|
+
},
|
|
1390
|
+
default: '',
|
|
1391
|
+
description: 'Email address of the contact',
|
|
1392
|
+
},
|
|
1393
|
+
{
|
|
1394
|
+
displayName: 'Additional Fields',
|
|
1395
|
+
name: 'additionalFields',
|
|
1396
|
+
type: 'collection',
|
|
1397
|
+
placeholder: 'Add Field',
|
|
1398
|
+
default: {},
|
|
1399
|
+
displayOptions: {
|
|
1400
|
+
show: {
|
|
1401
|
+
resource: ['contact'],
|
|
1402
|
+
operation: ['create'],
|
|
1403
|
+
},
|
|
1404
|
+
},
|
|
1405
|
+
options: [
|
|
1406
|
+
{
|
|
1407
|
+
displayName: 'Account ID',
|
|
1408
|
+
name: 'accountId',
|
|
1409
|
+
type: 'string',
|
|
1410
|
+
default: '',
|
|
1411
|
+
description: 'The ID of the account associated with this contact',
|
|
1412
|
+
},
|
|
1413
|
+
{
|
|
1414
|
+
displayName: 'Custom Fields',
|
|
1415
|
+
name: 'cf',
|
|
1416
|
+
type: 'json',
|
|
1417
|
+
default: '',
|
|
1418
|
+
description: 'Custom fields as JSON object',
|
|
1419
|
+
placeholder: '{"cf_field": "value"}',
|
|
1420
|
+
},
|
|
1421
|
+
{
|
|
1422
|
+
displayName: 'Description',
|
|
1423
|
+
name: 'description',
|
|
1424
|
+
type: 'string',
|
|
1425
|
+
typeOptions: {
|
|
1426
|
+
rows: 4,
|
|
1427
|
+
},
|
|
1428
|
+
default: '',
|
|
1429
|
+
description: 'Description of the contact',
|
|
1430
|
+
},
|
|
1431
|
+
{
|
|
1432
|
+
displayName: 'Facebook',
|
|
1433
|
+
name: 'facebook',
|
|
1434
|
+
type: 'string',
|
|
1435
|
+
default: '',
|
|
1436
|
+
description: 'Facebook handle of the contact',
|
|
1437
|
+
},
|
|
1438
|
+
{
|
|
1439
|
+
displayName: 'First Name',
|
|
1440
|
+
name: 'firstName',
|
|
1441
|
+
type: 'string',
|
|
1442
|
+
default: '',
|
|
1443
|
+
description: 'First name of the contact',
|
|
1444
|
+
},
|
|
1445
|
+
{
|
|
1446
|
+
displayName: 'Mobile',
|
|
1447
|
+
name: 'mobile',
|
|
1448
|
+
type: 'string',
|
|
1449
|
+
default: '',
|
|
1450
|
+
description: 'Mobile number of the contact',
|
|
1451
|
+
},
|
|
1452
|
+
{
|
|
1453
|
+
displayName: 'Phone',
|
|
1454
|
+
name: 'phone',
|
|
1455
|
+
type: 'string',
|
|
1456
|
+
default: '',
|
|
1457
|
+
description: 'Phone number of the contact',
|
|
1458
|
+
},
|
|
1459
|
+
{
|
|
1460
|
+
displayName: 'Twitter',
|
|
1461
|
+
name: 'twitter',
|
|
1462
|
+
type: 'string',
|
|
1463
|
+
default: '',
|
|
1464
|
+
description: 'Twitter handle of the contact',
|
|
1465
|
+
},
|
|
1466
|
+
{
|
|
1467
|
+
displayName: 'Type',
|
|
1468
|
+
name: 'type',
|
|
1469
|
+
type: 'string',
|
|
1470
|
+
default: '',
|
|
1471
|
+
description: 'Type of the contact',
|
|
1472
|
+
},
|
|
1473
|
+
],
|
|
1474
|
+
},
|
|
1475
|
+
// Contact: Get/Delete/Update
|
|
1476
|
+
{
|
|
1477
|
+
displayName: 'Contact ID',
|
|
1478
|
+
name: 'contactId',
|
|
1479
|
+
type: 'string',
|
|
1480
|
+
required: true,
|
|
1481
|
+
displayOptions: {
|
|
1482
|
+
show: {
|
|
1483
|
+
resource: ['contact'],
|
|
1484
|
+
operation: ['get', 'delete', 'update'],
|
|
1485
|
+
},
|
|
1486
|
+
},
|
|
1487
|
+
default: '',
|
|
1488
|
+
description: 'The ID of the contact',
|
|
1489
|
+
},
|
|
1490
|
+
// Contact: List
|
|
1491
|
+
{
|
|
1492
|
+
displayName: 'Return All',
|
|
1493
|
+
name: 'returnAll',
|
|
1494
|
+
type: 'boolean',
|
|
1495
|
+
displayOptions: {
|
|
1496
|
+
show: {
|
|
1497
|
+
resource: ['contact'],
|
|
1498
|
+
operation: ['list'],
|
|
1499
|
+
},
|
|
1500
|
+
},
|
|
1501
|
+
default: false,
|
|
1502
|
+
description: 'Whether to return all results or only up to a given limit',
|
|
1503
|
+
},
|
|
1504
|
+
{
|
|
1505
|
+
displayName: 'Limit',
|
|
1506
|
+
name: 'limit',
|
|
1507
|
+
type: 'number',
|
|
1508
|
+
displayOptions: {
|
|
1509
|
+
show: {
|
|
1510
|
+
resource: ['contact'],
|
|
1511
|
+
operation: ['list'],
|
|
1512
|
+
returnAll: [false],
|
|
1513
|
+
},
|
|
1514
|
+
},
|
|
1515
|
+
typeOptions: {
|
|
1516
|
+
minValue: 1,
|
|
1517
|
+
},
|
|
1518
|
+
default: 50,
|
|
1519
|
+
description: 'Max number of results to return',
|
|
1520
|
+
},
|
|
1521
|
+
// Contact: Update
|
|
1522
|
+
{
|
|
1523
|
+
displayName: 'Update Fields',
|
|
1524
|
+
name: 'updateFields',
|
|
1525
|
+
type: 'collection',
|
|
1526
|
+
placeholder: 'Add Field',
|
|
1527
|
+
default: {},
|
|
1528
|
+
displayOptions: {
|
|
1529
|
+
show: {
|
|
1530
|
+
resource: ['contact'],
|
|
1531
|
+
operation: ['update'],
|
|
1532
|
+
},
|
|
1533
|
+
},
|
|
1534
|
+
options: [
|
|
1535
|
+
{
|
|
1536
|
+
displayName: 'Account ID',
|
|
1537
|
+
name: 'accountId',
|
|
1538
|
+
type: 'string',
|
|
1539
|
+
default: '',
|
|
1540
|
+
description: 'The ID of the account associated with this contact',
|
|
1541
|
+
},
|
|
1542
|
+
{
|
|
1543
|
+
displayName: 'Custom Fields',
|
|
1544
|
+
name: 'cf',
|
|
1545
|
+
type: 'json',
|
|
1546
|
+
default: '',
|
|
1547
|
+
description: 'Custom fields as JSON object',
|
|
1548
|
+
placeholder: '{"cf_field": "value"}',
|
|
1549
|
+
},
|
|
1550
|
+
{
|
|
1551
|
+
displayName: 'Description',
|
|
1552
|
+
name: 'description',
|
|
1553
|
+
type: 'string',
|
|
1554
|
+
typeOptions: {
|
|
1555
|
+
rows: 4,
|
|
1556
|
+
},
|
|
1557
|
+
default: '',
|
|
1558
|
+
description: 'Description of the contact',
|
|
1559
|
+
},
|
|
1560
|
+
{
|
|
1561
|
+
displayName: 'Email',
|
|
1562
|
+
name: 'email',
|
|
1563
|
+
type: 'string',
|
|
1564
|
+
placeholder: 'name@email.com',
|
|
1565
|
+
default: '',
|
|
1566
|
+
description: 'Email address of the contact',
|
|
1567
|
+
},
|
|
1568
|
+
{
|
|
1569
|
+
displayName: 'Facebook',
|
|
1570
|
+
name: 'facebook',
|
|
1571
|
+
type: 'string',
|
|
1572
|
+
default: '',
|
|
1573
|
+
description: 'Facebook handle of the contact',
|
|
1574
|
+
},
|
|
1575
|
+
{
|
|
1576
|
+
displayName: 'First Name',
|
|
1577
|
+
name: 'firstName',
|
|
1578
|
+
type: 'string',
|
|
1579
|
+
default: '',
|
|
1580
|
+
description: 'First name of the contact',
|
|
1581
|
+
},
|
|
1582
|
+
{
|
|
1583
|
+
displayName: 'Last Name',
|
|
1584
|
+
name: 'lastName',
|
|
1585
|
+
type: 'string',
|
|
1586
|
+
default: '',
|
|
1587
|
+
description: 'Last name of the contact',
|
|
1588
|
+
},
|
|
1589
|
+
{
|
|
1590
|
+
displayName: 'Mobile',
|
|
1591
|
+
name: 'mobile',
|
|
1592
|
+
type: 'string',
|
|
1593
|
+
default: '',
|
|
1594
|
+
description: 'Mobile number of the contact',
|
|
1595
|
+
},
|
|
1596
|
+
{
|
|
1597
|
+
displayName: 'Phone',
|
|
1598
|
+
name: 'phone',
|
|
1599
|
+
type: 'string',
|
|
1600
|
+
default: '',
|
|
1601
|
+
description: 'Phone number of the contact',
|
|
1602
|
+
},
|
|
1603
|
+
{
|
|
1604
|
+
displayName: 'Twitter',
|
|
1605
|
+
name: 'twitter',
|
|
1606
|
+
type: 'string',
|
|
1607
|
+
default: '',
|
|
1608
|
+
description: 'Twitter handle of the contact',
|
|
1609
|
+
},
|
|
1610
|
+
{
|
|
1611
|
+
displayName: 'Type',
|
|
1612
|
+
name: 'type',
|
|
1613
|
+
type: 'string',
|
|
1614
|
+
default: '',
|
|
1615
|
+
description: 'Type of the contact',
|
|
1616
|
+
},
|
|
1617
|
+
],
|
|
1618
|
+
},
|
|
1619
|
+
// ==================== ACCOUNT PARAMETERS ====================
|
|
1620
|
+
// Account: Create
|
|
1621
|
+
{
|
|
1622
|
+
displayName: 'Account Name',
|
|
1623
|
+
name: 'accountName',
|
|
1624
|
+
type: 'string',
|
|
1625
|
+
required: true,
|
|
1626
|
+
displayOptions: {
|
|
1627
|
+
show: {
|
|
1628
|
+
resource: ['account'],
|
|
1629
|
+
operation: ['create'],
|
|
1630
|
+
},
|
|
1631
|
+
},
|
|
1632
|
+
default: '',
|
|
1633
|
+
description: 'Name of the account',
|
|
1634
|
+
},
|
|
1635
|
+
{
|
|
1636
|
+
displayName: 'Additional Fields',
|
|
1637
|
+
name: 'additionalFields',
|
|
1638
|
+
type: 'collection',
|
|
1639
|
+
placeholder: 'Add Field',
|
|
1640
|
+
default: {},
|
|
1641
|
+
displayOptions: {
|
|
1642
|
+
show: {
|
|
1643
|
+
resource: ['account'],
|
|
1644
|
+
operation: ['create'],
|
|
1645
|
+
},
|
|
1646
|
+
},
|
|
1647
|
+
options: [
|
|
1648
|
+
{
|
|
1649
|
+
displayName: 'City',
|
|
1650
|
+
name: 'city',
|
|
1651
|
+
type: 'string',
|
|
1652
|
+
default: '',
|
|
1653
|
+
description: 'City of the account',
|
|
1654
|
+
},
|
|
1655
|
+
{
|
|
1656
|
+
displayName: 'Code',
|
|
1657
|
+
name: 'code',
|
|
1658
|
+
type: 'string',
|
|
1659
|
+
default: '',
|
|
1660
|
+
description: 'Account code',
|
|
1661
|
+
},
|
|
1662
|
+
{
|
|
1663
|
+
displayName: 'Country',
|
|
1664
|
+
name: 'country',
|
|
1665
|
+
type: 'string',
|
|
1666
|
+
default: '',
|
|
1667
|
+
description: 'Country of the account',
|
|
1668
|
+
},
|
|
1669
|
+
{
|
|
1670
|
+
displayName: 'Custom Fields',
|
|
1671
|
+
name: 'cf',
|
|
1672
|
+
type: 'json',
|
|
1673
|
+
default: '',
|
|
1674
|
+
description: 'Custom fields as JSON object',
|
|
1675
|
+
placeholder: '{"cf_field": "value"}',
|
|
1676
|
+
},
|
|
1677
|
+
{
|
|
1678
|
+
displayName: 'Description',
|
|
1679
|
+
name: 'description',
|
|
1680
|
+
type: 'string',
|
|
1681
|
+
typeOptions: {
|
|
1682
|
+
rows: 4,
|
|
1683
|
+
},
|
|
1684
|
+
default: '',
|
|
1685
|
+
description: 'Description of the account',
|
|
1686
|
+
},
|
|
1687
|
+
{
|
|
1688
|
+
displayName: 'Fax',
|
|
1689
|
+
name: 'fax',
|
|
1690
|
+
type: 'string',
|
|
1691
|
+
default: '',
|
|
1692
|
+
description: 'Fax number of the account',
|
|
1693
|
+
},
|
|
1694
|
+
{
|
|
1695
|
+
displayName: 'Industry',
|
|
1696
|
+
name: 'industry',
|
|
1697
|
+
type: 'string',
|
|
1698
|
+
default: '',
|
|
1699
|
+
description: 'Industry of the account',
|
|
1700
|
+
},
|
|
1701
|
+
{
|
|
1702
|
+
displayName: 'Phone',
|
|
1703
|
+
name: 'phone',
|
|
1704
|
+
type: 'string',
|
|
1705
|
+
default: '',
|
|
1706
|
+
description: 'Phone number of the account',
|
|
1707
|
+
},
|
|
1708
|
+
{
|
|
1709
|
+
displayName: 'State',
|
|
1710
|
+
name: 'state',
|
|
1711
|
+
type: 'string',
|
|
1712
|
+
default: '',
|
|
1713
|
+
description: 'State of the account',
|
|
1714
|
+
},
|
|
1715
|
+
{
|
|
1716
|
+
displayName: 'Street',
|
|
1717
|
+
name: 'street',
|
|
1718
|
+
type: 'string',
|
|
1719
|
+
default: '',
|
|
1720
|
+
description: 'Street address of the account',
|
|
1721
|
+
},
|
|
1722
|
+
{
|
|
1723
|
+
displayName: 'Website',
|
|
1724
|
+
name: 'website',
|
|
1725
|
+
type: 'string',
|
|
1726
|
+
default: '',
|
|
1727
|
+
description: 'Website URL of the account',
|
|
1728
|
+
},
|
|
1729
|
+
{
|
|
1730
|
+
displayName: 'Zip',
|
|
1731
|
+
name: 'zip',
|
|
1732
|
+
type: 'string',
|
|
1733
|
+
default: '',
|
|
1734
|
+
description: 'ZIP/Postal code of the account',
|
|
1735
|
+
},
|
|
1736
|
+
],
|
|
1737
|
+
},
|
|
1738
|
+
// Account: Get/Delete/Update
|
|
1739
|
+
{
|
|
1740
|
+
displayName: 'Account ID',
|
|
1741
|
+
name: 'accountId',
|
|
1742
|
+
type: 'string',
|
|
1743
|
+
required: true,
|
|
1744
|
+
displayOptions: {
|
|
1745
|
+
show: {
|
|
1746
|
+
resource: ['account'],
|
|
1747
|
+
operation: ['get', 'delete', 'update'],
|
|
1748
|
+
},
|
|
1749
|
+
},
|
|
1750
|
+
default: '',
|
|
1751
|
+
description: 'The ID of the account',
|
|
1752
|
+
},
|
|
1753
|
+
// Account: List
|
|
1754
|
+
{
|
|
1755
|
+
displayName: 'Return All',
|
|
1756
|
+
name: 'returnAll',
|
|
1757
|
+
type: 'boolean',
|
|
1758
|
+
displayOptions: {
|
|
1759
|
+
show: {
|
|
1760
|
+
resource: ['account'],
|
|
1761
|
+
operation: ['list'],
|
|
1762
|
+
},
|
|
1763
|
+
},
|
|
1764
|
+
default: false,
|
|
1765
|
+
description: 'Whether to return all results or only up to a given limit',
|
|
1766
|
+
},
|
|
1767
|
+
{
|
|
1768
|
+
displayName: 'Limit',
|
|
1769
|
+
name: 'limit',
|
|
1770
|
+
type: 'number',
|
|
1771
|
+
displayOptions: {
|
|
1772
|
+
show: {
|
|
1773
|
+
resource: ['account'],
|
|
1774
|
+
operation: ['list'],
|
|
1775
|
+
returnAll: [false],
|
|
1776
|
+
},
|
|
1777
|
+
},
|
|
1778
|
+
typeOptions: {
|
|
1779
|
+
minValue: 1,
|
|
1780
|
+
},
|
|
1781
|
+
default: 50,
|
|
1782
|
+
description: 'Max number of results to return',
|
|
1783
|
+
},
|
|
1784
|
+
// Account: Update
|
|
1785
|
+
{
|
|
1786
|
+
displayName: 'Update Fields',
|
|
1787
|
+
name: 'updateFields',
|
|
1788
|
+
type: 'collection',
|
|
1789
|
+
placeholder: 'Add Field',
|
|
1790
|
+
default: {},
|
|
1791
|
+
displayOptions: {
|
|
1792
|
+
show: {
|
|
1793
|
+
resource: ['account'],
|
|
1794
|
+
operation: ['update'],
|
|
1795
|
+
},
|
|
1796
|
+
},
|
|
1797
|
+
options: [
|
|
1798
|
+
{
|
|
1799
|
+
displayName: 'Account Name',
|
|
1800
|
+
name: 'accountName',
|
|
1801
|
+
type: 'string',
|
|
1802
|
+
default: '',
|
|
1803
|
+
description: 'Name of the account',
|
|
1804
|
+
},
|
|
1805
|
+
{
|
|
1806
|
+
displayName: 'City',
|
|
1807
|
+
name: 'city',
|
|
1808
|
+
type: 'string',
|
|
1809
|
+
default: '',
|
|
1810
|
+
description: 'City of the account',
|
|
1811
|
+
},
|
|
1812
|
+
{
|
|
1813
|
+
displayName: 'Code',
|
|
1814
|
+
name: 'code',
|
|
1815
|
+
type: 'string',
|
|
1816
|
+
default: '',
|
|
1817
|
+
description: 'Account code',
|
|
1818
|
+
},
|
|
1819
|
+
{
|
|
1820
|
+
displayName: 'Country',
|
|
1821
|
+
name: 'country',
|
|
1822
|
+
type: 'string',
|
|
1823
|
+
default: '',
|
|
1824
|
+
description: 'Country of the account',
|
|
1825
|
+
},
|
|
1826
|
+
{
|
|
1827
|
+
displayName: 'Custom Fields',
|
|
1828
|
+
name: 'cf',
|
|
1829
|
+
type: 'json',
|
|
1830
|
+
default: '',
|
|
1831
|
+
description: 'Custom fields as JSON object',
|
|
1832
|
+
placeholder: '{"cf_field": "value"}',
|
|
1833
|
+
},
|
|
1834
|
+
{
|
|
1835
|
+
displayName: 'Description',
|
|
1836
|
+
name: 'description',
|
|
1837
|
+
type: 'string',
|
|
1838
|
+
typeOptions: {
|
|
1839
|
+
rows: 4,
|
|
1840
|
+
},
|
|
1841
|
+
default: '',
|
|
1842
|
+
description: 'Description of the account',
|
|
1843
|
+
},
|
|
1844
|
+
{
|
|
1845
|
+
displayName: 'Fax',
|
|
1846
|
+
name: 'fax',
|
|
1847
|
+
type: 'string',
|
|
1848
|
+
default: '',
|
|
1849
|
+
description: 'Fax number of the account',
|
|
1850
|
+
},
|
|
1851
|
+
{
|
|
1852
|
+
displayName: 'Industry',
|
|
1853
|
+
name: 'industry',
|
|
1854
|
+
type: 'string',
|
|
1855
|
+
default: '',
|
|
1856
|
+
description: 'Industry of the account',
|
|
1857
|
+
},
|
|
1858
|
+
{
|
|
1859
|
+
displayName: 'Phone',
|
|
1860
|
+
name: 'phone',
|
|
1861
|
+
type: 'string',
|
|
1862
|
+
default: '',
|
|
1863
|
+
description: 'Phone number of the account',
|
|
1864
|
+
},
|
|
1865
|
+
{
|
|
1866
|
+
displayName: 'State',
|
|
1867
|
+
name: 'state',
|
|
1868
|
+
type: 'string',
|
|
1869
|
+
default: '',
|
|
1870
|
+
description: 'State of the account',
|
|
1871
|
+
},
|
|
1872
|
+
{
|
|
1873
|
+
displayName: 'Street',
|
|
1874
|
+
name: 'street',
|
|
1875
|
+
type: 'string',
|
|
1876
|
+
default: '',
|
|
1877
|
+
description: 'Street address of the account',
|
|
1878
|
+
},
|
|
1879
|
+
{
|
|
1880
|
+
displayName: 'Website',
|
|
1881
|
+
name: 'website',
|
|
1882
|
+
type: 'string',
|
|
1883
|
+
default: '',
|
|
1884
|
+
description: 'Website URL of the account',
|
|
1885
|
+
},
|
|
1886
|
+
{
|
|
1887
|
+
displayName: 'Zip',
|
|
1888
|
+
name: 'zip',
|
|
1889
|
+
type: 'string',
|
|
1890
|
+
default: '',
|
|
1891
|
+
description: 'ZIP/Postal code of the account',
|
|
1892
|
+
},
|
|
1893
|
+
],
|
|
1894
|
+
},
|
|
1895
|
+
],
|
|
1896
|
+
};
|
|
1897
|
+
this.methods = {
|
|
1898
|
+
loadOptions: {
|
|
1899
|
+
/**
|
|
1900
|
+
* Load all departments from Zoho Desk
|
|
1901
|
+
* @returns Array of department options for dropdown
|
|
1902
|
+
*/
|
|
1903
|
+
async getDepartments() {
|
|
1904
|
+
try {
|
|
1905
|
+
const credentials = await this.getCredentials('zohoDeskOAuth2Api');
|
|
1906
|
+
const orgId = credentials.orgId;
|
|
1907
|
+
const baseUrl = credentials.baseUrl || DEFAULT_BASE_URL;
|
|
1908
|
+
const options = {
|
|
1909
|
+
method: 'GET',
|
|
1910
|
+
headers: {
|
|
1911
|
+
orgId: orgId,
|
|
1912
|
+
},
|
|
1913
|
+
uri: `${baseUrl}/departments`,
|
|
1914
|
+
json: true,
|
|
1915
|
+
};
|
|
1916
|
+
const response = await this.helpers.requestOAuth2.call(this, 'zohoDeskOAuth2Api', options);
|
|
1917
|
+
// Runtime validation of API response structure
|
|
1918
|
+
if (!response ||
|
|
1919
|
+
typeof response !== 'object' ||
|
|
1920
|
+
!('data' in response) ||
|
|
1921
|
+
!Array.isArray(response.data)) {
|
|
1922
|
+
throw new n8n_workflow_1.ApplicationError('Invalid API response structure from Zoho Desk');
|
|
1923
|
+
}
|
|
1924
|
+
const typedResponse = response;
|
|
1925
|
+
return typedResponse.data.map((department) => ({
|
|
1926
|
+
name: department.name,
|
|
1927
|
+
value: department.id,
|
|
1928
|
+
}));
|
|
1929
|
+
}
|
|
1930
|
+
catch (error) {
|
|
1931
|
+
// Return error option in dropdown so users can see what went wrong
|
|
1932
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1933
|
+
return [
|
|
1934
|
+
{
|
|
1935
|
+
name: `⚠️ Error loading departments: ${errorMessage}`,
|
|
1936
|
+
value: '',
|
|
1937
|
+
},
|
|
1938
|
+
];
|
|
1939
|
+
}
|
|
1940
|
+
},
|
|
1941
|
+
/**
|
|
1942
|
+
* Load teams for a specific department from Zoho Desk
|
|
1943
|
+
* @returns Array of team options for dropdown, or empty array if department not selected
|
|
1944
|
+
*/
|
|
1945
|
+
async getTeams() {
|
|
1946
|
+
try {
|
|
1947
|
+
const credentials = await this.getCredentials('zohoDeskOAuth2Api');
|
|
1948
|
+
const orgId = credentials.orgId;
|
|
1949
|
+
const baseUrl = credentials.baseUrl || DEFAULT_BASE_URL;
|
|
1950
|
+
const departmentId = this.getCurrentNodeParameter('departmentId');
|
|
1951
|
+
// Type guard: departmentId is optional in update operation
|
|
1952
|
+
if (!departmentId || typeof departmentId !== 'string') {
|
|
1953
|
+
return [];
|
|
1954
|
+
}
|
|
1955
|
+
const options = {
|
|
1956
|
+
method: 'GET',
|
|
1957
|
+
headers: {
|
|
1958
|
+
orgId: orgId,
|
|
1959
|
+
},
|
|
1960
|
+
uri: `${baseUrl}/departments/${encodeURIComponent(departmentId)}/teams`,
|
|
1961
|
+
json: true,
|
|
1962
|
+
};
|
|
1963
|
+
const response = await this.helpers.requestOAuth2.call(this, 'zohoDeskOAuth2Api', options);
|
|
1964
|
+
// Runtime validation of API response structure with detailed error reporting
|
|
1965
|
+
// Note: Teams endpoint uses 'teams' property instead of 'data'
|
|
1966
|
+
if (!response || typeof response !== 'object') {
|
|
1967
|
+
throw new n8n_workflow_1.ApplicationError(`Invalid API response structure from Zoho Desk. Expected an object, received: ${typeof response}. ` +
|
|
1968
|
+
`Response: ${JSON.stringify(response)}`);
|
|
1969
|
+
}
|
|
1970
|
+
if (!('teams' in response)) {
|
|
1971
|
+
throw new n8n_workflow_1.ApplicationError(`Invalid API response structure from Zoho Desk. Missing 'teams' property. ` +
|
|
1972
|
+
`Response keys: ${Object.keys(response).join(', ')}. ` +
|
|
1973
|
+
`Response: ${JSON.stringify(response)}`);
|
|
1974
|
+
}
|
|
1975
|
+
if (!Array.isArray(response.teams)) {
|
|
1976
|
+
throw new n8n_workflow_1.ApplicationError(`Invalid API response structure from Zoho Desk. Expected 'teams' to be an array, received: ${typeof response.teams}. ` +
|
|
1977
|
+
`Response: ${JSON.stringify(response)}`);
|
|
1978
|
+
}
|
|
1979
|
+
const typedResponse = response;
|
|
1980
|
+
return typedResponse.teams.map((team) => ({
|
|
1981
|
+
name: team.name,
|
|
1982
|
+
value: team.id,
|
|
1983
|
+
}));
|
|
1984
|
+
}
|
|
1985
|
+
catch (error) {
|
|
1986
|
+
// Return error option in dropdown so users can see what went wrong
|
|
1987
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1988
|
+
return [
|
|
1989
|
+
{
|
|
1990
|
+
name: `⚠️ Error loading teams: ${errorMessage}`,
|
|
1991
|
+
value: '',
|
|
1992
|
+
},
|
|
1993
|
+
];
|
|
1994
|
+
}
|
|
1995
|
+
},
|
|
1996
|
+
},
|
|
1997
|
+
};
|
|
1998
|
+
}
|
|
1999
|
+
async execute() {
|
|
2000
|
+
const items = this.getInputData();
|
|
2001
|
+
const returnData = [];
|
|
2002
|
+
const resource = this.getNodeParameter('resource', 0);
|
|
2003
|
+
const operation = this.getNodeParameter('operation', 0);
|
|
2004
|
+
// Fetch credentials once for all items (optimization)
|
|
2005
|
+
const credentials = await this.getCredentials('zohoDeskOAuth2Api');
|
|
2006
|
+
const orgId = credentials.orgId;
|
|
2007
|
+
const baseUrl = credentials.baseUrl || DEFAULT_BASE_URL;
|
|
2008
|
+
for (let i = 0; i < items.length; i++) {
|
|
2009
|
+
try {
|
|
2010
|
+
if (resource === 'ticket') {
|
|
2011
|
+
if (operation === 'create') {
|
|
2012
|
+
// Create ticket
|
|
2013
|
+
const departmentId = this.getNodeParameter('departmentId', i);
|
|
2014
|
+
const teamId = this.getNodeParameter('teamId', i);
|
|
2015
|
+
const rawSubject = this.getNodeParameter('subject', i);
|
|
2016
|
+
const contactData = this.getNodeParameter('contact', i);
|
|
2017
|
+
const priority = this.getNodeParameter('priority', i);
|
|
2018
|
+
const classification = this.getNodeParameter('classification', i);
|
|
2019
|
+
const dueDate = this.getNodeParameter('dueDate', i);
|
|
2020
|
+
const description = this.getNodeParameter('description', i);
|
|
2021
|
+
const additionalFields = this.getNodeParameter('additionalFields', i);
|
|
2022
|
+
// Validate length for subject (XSS protection handled by Zoho Desk API)
|
|
2023
|
+
// No length limit - let Zoho Desk API validate
|
|
2024
|
+
const subject = validateFieldLength(rawSubject, undefined, 'Subject');
|
|
2025
|
+
const body = {
|
|
2026
|
+
departmentId,
|
|
2027
|
+
subject,
|
|
2028
|
+
priority,
|
|
2029
|
+
classification,
|
|
2030
|
+
};
|
|
2031
|
+
// Add teamId if provided (optional field, same level as departmentId)
|
|
2032
|
+
if (teamId && teamId.trim() !== '') {
|
|
2033
|
+
body.teamId = teamId;
|
|
2034
|
+
}
|
|
2035
|
+
// Add dueDate if provided - validate and normalize ISO 8601 string
|
|
2036
|
+
if (dueDate && dueDate.trim() !== '') {
|
|
2037
|
+
// n8n dateTime field returns ISO 8601 string, Zoho Desk API expects ISO 8601 with milliseconds and timezone
|
|
2038
|
+
body.dueDate = convertDateToTimestamp(dueDate, 'Due Date', ZOHO_DESK_CREATE_TICKET_DOCS);
|
|
2039
|
+
}
|
|
2040
|
+
// Add description if provided
|
|
2041
|
+
if (description) {
|
|
2042
|
+
body.description = validateFieldLength(description, undefined, 'Description');
|
|
2043
|
+
}
|
|
2044
|
+
// Handle and validate contact object (OPTIONAL)
|
|
2045
|
+
// Contact field is optional in UI (required: false) but if provided, must be valid
|
|
2046
|
+
// Contact auto-creation flow (when provided):
|
|
2047
|
+
// 1. If email exists in Zoho Desk → Existing contact is used
|
|
2048
|
+
// 2. If email doesn't exist → New contact is created with provided details
|
|
2049
|
+
// 3. Either email OR lastName must be provided (Zoho Desk requirement)
|
|
2050
|
+
if (contactData && contactData.contactValues) {
|
|
2051
|
+
const contactValues = contactData.contactValues;
|
|
2052
|
+
// Type guard for contactValues using isPlainObject helper
|
|
2053
|
+
if (!isPlainObject(contactValues)) {
|
|
2054
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Contact validation failed: Invalid contact data format. ' +
|
|
2055
|
+
'See: ' +
|
|
2056
|
+
ZOHO_DESK_CREATE_TICKET_DOCS);
|
|
2057
|
+
}
|
|
2058
|
+
// Check if any non-empty values exist before processing
|
|
2059
|
+
// This prevents unnecessary validation errors when user provides empty contact fields
|
|
2060
|
+
const hasNonEmptyValue = Object.values(contactValues).some((value) => value && String(value).trim() !== '');
|
|
2061
|
+
if (hasNonEmptyValue) {
|
|
2062
|
+
// Build contact object with available non-empty fields
|
|
2063
|
+
// Zoho Desk will automatically match by email or create new contact
|
|
2064
|
+
// Type validation ensures we only coerce strings/numbers, not complex objects
|
|
2065
|
+
const contact = {};
|
|
2066
|
+
// Use helper function to add contact fields with validation and CRLF sanitization
|
|
2067
|
+
// Only email has a documented length limit (RFC 5321), others validated by API
|
|
2068
|
+
addContactField(contact, contactValues, 'email', 'email', FIELD_LENGTH_LIMITS.email);
|
|
2069
|
+
addContactField(contact, contactValues, 'lastName', 'lastName', undefined);
|
|
2070
|
+
addContactField(contact, contactValues, 'firstName', 'firstName', undefined);
|
|
2071
|
+
addContactField(contact, contactValues, 'phone', 'phone', undefined);
|
|
2072
|
+
// Validation: if contact has values, ensure at least email or lastName is present
|
|
2073
|
+
// This catches edge cases where only firstName/phone are provided
|
|
2074
|
+
if (!contact.email && !contact.lastName) {
|
|
2075
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Contact validation failed: Either email or lastName must be provided. ' +
|
|
2076
|
+
'See: ' +
|
|
2077
|
+
ZOHO_DESK_CREATE_TICKET_DOCS);
|
|
2078
|
+
}
|
|
2079
|
+
body.contact = contact;
|
|
2080
|
+
}
|
|
2081
|
+
// If all values are empty, skip contact entirely (no error thrown)
|
|
2082
|
+
}
|
|
2083
|
+
// Add common fields (description, secondaryContacts, custom fields)
|
|
2084
|
+
// Note: priority and dueDate are already set as primary fields above
|
|
2085
|
+
addCommonTicketFields(body, additionalFields, false);
|
|
2086
|
+
// Add other additional fields
|
|
2087
|
+
addOptionalFields(body, additionalFields, TICKET_CREATE_OPTIONAL_FIELDS);
|
|
2088
|
+
const options = {
|
|
2089
|
+
method: 'POST',
|
|
2090
|
+
headers: {
|
|
2091
|
+
orgId: orgId,
|
|
2092
|
+
},
|
|
2093
|
+
body,
|
|
2094
|
+
uri: `${baseUrl}/tickets`,
|
|
2095
|
+
json: true,
|
|
2096
|
+
};
|
|
2097
|
+
const response = await this.helpers.requestOAuth2.call(this, 'zohoDeskOAuth2Api', options);
|
|
2098
|
+
returnData.push({
|
|
2099
|
+
json: response,
|
|
2100
|
+
pairedItem: { item: i },
|
|
2101
|
+
});
|
|
2102
|
+
}
|
|
2103
|
+
if (operation === 'update') {
|
|
2104
|
+
// Update ticket
|
|
2105
|
+
const ticketId = this.getNodeParameter('ticketId', i);
|
|
2106
|
+
const description = this.getNodeParameter('description', i);
|
|
2107
|
+
const updateFields = this.getNodeParameter('updateFields', i);
|
|
2108
|
+
// Validate ticket ID format (should be numeric)
|
|
2109
|
+
if (!isValidTicketId(ticketId)) {
|
|
2110
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Invalid ticket ID format: "${ticketId}". Ticket ID must be a numeric value. ` +
|
|
2111
|
+
'See: ' +
|
|
2112
|
+
ZOHO_DESK_UPDATE_TICKET_DOCS);
|
|
2113
|
+
}
|
|
2114
|
+
const body = {};
|
|
2115
|
+
// Add common fields (description, dueDate, priority, secondaryContacts, custom fields)
|
|
2116
|
+
addCommonTicketFields(body, updateFields);
|
|
2117
|
+
// Add other update fields
|
|
2118
|
+
// Add description if provided
|
|
2119
|
+
if (description) {
|
|
2120
|
+
body.description = validateFieldLength(description, undefined, 'Description');
|
|
2121
|
+
}
|
|
2122
|
+
addOptionalFields(body, updateFields, TICKET_UPDATE_OPTIONAL_FIELDS);
|
|
2123
|
+
const options = {
|
|
2124
|
+
method: 'PATCH',
|
|
2125
|
+
headers: {
|
|
2126
|
+
orgId: orgId,
|
|
2127
|
+
},
|
|
2128
|
+
body,
|
|
2129
|
+
uri: `${baseUrl}/tickets/${encodeURIComponent(ticketId)}`,
|
|
2130
|
+
json: true,
|
|
2131
|
+
};
|
|
2132
|
+
const response = await this.helpers.requestOAuth2.call(this, 'zohoDeskOAuth2Api', options);
|
|
2133
|
+
returnData.push({
|
|
2134
|
+
json: response,
|
|
2135
|
+
pairedItem: { item: i },
|
|
2136
|
+
});
|
|
2137
|
+
}
|
|
2138
|
+
if (operation === 'get') {
|
|
2139
|
+
const ticketId = this.getNodeParameter('ticketId', i);
|
|
2140
|
+
if (!isValidTicketId(ticketId)) {
|
|
2141
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Invalid ticket ID format: "${ticketId}". Ticket ID must be a numeric value. ` +
|
|
2142
|
+
'See: ' +
|
|
2143
|
+
ZOHO_DESK_GET_TICKET_DOCS);
|
|
2144
|
+
}
|
|
2145
|
+
const options = {
|
|
2146
|
+
method: 'GET',
|
|
2147
|
+
headers: { orgId },
|
|
2148
|
+
uri: `${baseUrl}/tickets/${encodeURIComponent(ticketId)}`,
|
|
2149
|
+
json: true,
|
|
2150
|
+
};
|
|
2151
|
+
const response = await this.helpers.requestOAuth2.call(this, 'zohoDeskOAuth2Api', options);
|
|
2152
|
+
returnData.push({
|
|
2153
|
+
json: response,
|
|
2154
|
+
pairedItem: { item: i },
|
|
2155
|
+
});
|
|
2156
|
+
}
|
|
2157
|
+
if (operation === 'list') {
|
|
2158
|
+
const returnAll = this.getNodeParameter('returnAll', i);
|
|
2159
|
+
const filters = this.getNodeParameter('filters', i);
|
|
2160
|
+
// Build query params from filters
|
|
2161
|
+
const queryParams = {};
|
|
2162
|
+
if (filters.departmentId)
|
|
2163
|
+
queryParams.departmentId = filters.departmentId;
|
|
2164
|
+
if (filters.assigneeId)
|
|
2165
|
+
queryParams.assigneeId = filters.assigneeId;
|
|
2166
|
+
if (filters.status)
|
|
2167
|
+
queryParams.status = filters.status;
|
|
2168
|
+
if (returnAll) {
|
|
2169
|
+
const items = await getAllPaginatedItems(this, baseUrl, '/tickets', orgId, 100, 'data', queryParams);
|
|
2170
|
+
for (const item of items) {
|
|
2171
|
+
returnData.push({
|
|
2172
|
+
json: item,
|
|
2173
|
+
pairedItem: { item: i },
|
|
2174
|
+
});
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
else {
|
|
2178
|
+
const limit = this.getNodeParameter('limit', i);
|
|
2179
|
+
const options = {
|
|
2180
|
+
method: 'GET',
|
|
2181
|
+
headers: { orgId },
|
|
2182
|
+
uri: `${baseUrl}/tickets`,
|
|
2183
|
+
qs: { from: 1, limit, ...queryParams },
|
|
2184
|
+
json: true,
|
|
2185
|
+
};
|
|
2186
|
+
const response = await this.helpers.requestOAuth2.call(this, 'zohoDeskOAuth2Api', options);
|
|
2187
|
+
for (const ticket of response.data || []) {
|
|
2188
|
+
returnData.push({
|
|
2189
|
+
json: ticket,
|
|
2190
|
+
pairedItem: { item: i },
|
|
2191
|
+
});
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2195
|
+
if (operation === 'delete') {
|
|
2196
|
+
const ticketId = this.getNodeParameter('ticketId', i);
|
|
2197
|
+
if (!isValidTicketId(ticketId)) {
|
|
2198
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Invalid ticket ID format: "${ticketId}". Ticket ID must be a numeric value. ` +
|
|
2199
|
+
'See: ' +
|
|
2200
|
+
ZOHO_DESK_UPDATE_TICKET_DOCS);
|
|
2201
|
+
}
|
|
2202
|
+
const options = {
|
|
2203
|
+
method: 'POST',
|
|
2204
|
+
headers: { orgId },
|
|
2205
|
+
uri: `${baseUrl}/tickets/moveToTrash`,
|
|
2206
|
+
body: { ticketIds: [ticketId] },
|
|
2207
|
+
json: true,
|
|
2208
|
+
};
|
|
2209
|
+
await this.helpers.requestOAuth2.call(this, 'zohoDeskOAuth2Api', options);
|
|
2210
|
+
returnData.push({
|
|
2211
|
+
json: { success: true, ticketId },
|
|
2212
|
+
pairedItem: { item: i },
|
|
2213
|
+
});
|
|
2214
|
+
}
|
|
2215
|
+
if (operation === 'addComment') {
|
|
2216
|
+
const ticketId = this.getNodeParameter('ticketId', i);
|
|
2217
|
+
const content = this.getNodeParameter('content', i);
|
|
2218
|
+
const isPublic = this.getNodeParameter('isPublic', i);
|
|
2219
|
+
if (!isValidTicketId(ticketId)) {
|
|
2220
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Invalid ticket ID format: "${ticketId}". Ticket ID must be a numeric value. ` +
|
|
2221
|
+
'See: ' +
|
|
2222
|
+
ZOHO_DESK_COMMENTS_DOCS);
|
|
2223
|
+
}
|
|
2224
|
+
const body = {
|
|
2225
|
+
content: validateFieldLength(content, undefined, 'Comment Content'),
|
|
2226
|
+
isPublic,
|
|
2227
|
+
};
|
|
2228
|
+
const options = {
|
|
2229
|
+
method: 'POST',
|
|
2230
|
+
headers: { orgId },
|
|
2231
|
+
uri: `${baseUrl}/tickets/${encodeURIComponent(ticketId)}/comments`,
|
|
2232
|
+
body,
|
|
2233
|
+
json: true,
|
|
2234
|
+
};
|
|
2235
|
+
const response = await this.helpers.requestOAuth2.call(this, 'zohoDeskOAuth2Api', options);
|
|
2236
|
+
returnData.push({
|
|
2237
|
+
json: response,
|
|
2238
|
+
pairedItem: { item: i },
|
|
2239
|
+
});
|
|
2240
|
+
}
|
|
2241
|
+
if (operation === 'listThreads') {
|
|
2242
|
+
const ticketId = this.getNodeParameter('ticketId', i);
|
|
2243
|
+
const returnAll = this.getNodeParameter('returnAll', i);
|
|
2244
|
+
if (!isValidTicketId(ticketId)) {
|
|
2245
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Invalid ticket ID format: "${ticketId}". Ticket ID must be a numeric value. ` +
|
|
2246
|
+
'See: ' +
|
|
2247
|
+
ZOHO_DESK_THREADS_DOCS);
|
|
2248
|
+
}
|
|
2249
|
+
if (returnAll) {
|
|
2250
|
+
const items = await getAllPaginatedItems(this, baseUrl, `/tickets/${encodeURIComponent(ticketId)}/conversations`, orgId, 100, 'data');
|
|
2251
|
+
for (const item of items) {
|
|
2252
|
+
returnData.push({
|
|
2253
|
+
json: item,
|
|
2254
|
+
pairedItem: { item: i },
|
|
2255
|
+
});
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
else {
|
|
2259
|
+
const limit = this.getNodeParameter('limit', i);
|
|
2260
|
+
const options = {
|
|
2261
|
+
method: 'GET',
|
|
2262
|
+
headers: { orgId },
|
|
2263
|
+
uri: `${baseUrl}/tickets/${encodeURIComponent(ticketId)}/conversations`,
|
|
2264
|
+
qs: { from: 1, limit },
|
|
2265
|
+
json: true,
|
|
2266
|
+
};
|
|
2267
|
+
const response = await this.helpers.requestOAuth2.call(this, 'zohoDeskOAuth2Api', options);
|
|
2268
|
+
for (const thread of response.data || []) {
|
|
2269
|
+
returnData.push({
|
|
2270
|
+
json: thread,
|
|
2271
|
+
pairedItem: { item: i },
|
|
2272
|
+
});
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2277
|
+
// ==================== CONTACT RESOURCE ====================
|
|
2278
|
+
if (resource === 'contact') {
|
|
2279
|
+
if (operation === 'create') {
|
|
2280
|
+
const lastName = this.getNodeParameter('lastName', i);
|
|
2281
|
+
const email = this.getNodeParameter('email', i);
|
|
2282
|
+
const additionalFields = this.getNodeParameter('additionalFields', i);
|
|
2283
|
+
if (!isValidEmail(email)) {
|
|
2284
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Invalid email format: "${email}". See: ${ZOHO_DESK_CONTACTS_DOCS}`);
|
|
2285
|
+
}
|
|
2286
|
+
const body = {
|
|
2287
|
+
lastName: validateFieldLength(lastName, undefined, 'Last Name'),
|
|
2288
|
+
email: validateFieldLength(email, FIELD_LENGTH_LIMITS.email, 'Email'),
|
|
2289
|
+
};
|
|
2290
|
+
addOptionalFields(body, additionalFields, CONTACT_CREATE_OPTIONAL_FIELDS);
|
|
2291
|
+
if (additionalFields.cf !== undefined) {
|
|
2292
|
+
body.cf = parseCustomFields(additionalFields.cf);
|
|
2293
|
+
}
|
|
2294
|
+
const options = {
|
|
2295
|
+
method: 'POST',
|
|
2296
|
+
headers: { orgId },
|
|
2297
|
+
uri: `${baseUrl}/contacts`,
|
|
2298
|
+
body,
|
|
2299
|
+
json: true,
|
|
2300
|
+
};
|
|
2301
|
+
const response = await this.helpers.requestOAuth2.call(this, 'zohoDeskOAuth2Api', options);
|
|
2302
|
+
returnData.push({
|
|
2303
|
+
json: response,
|
|
2304
|
+
pairedItem: { item: i },
|
|
2305
|
+
});
|
|
2306
|
+
}
|
|
2307
|
+
if (operation === 'get') {
|
|
2308
|
+
const contactId = this.getNodeParameter('contactId', i);
|
|
2309
|
+
isValidZohoDeskId(contactId, 'Contact ID');
|
|
2310
|
+
const options = {
|
|
2311
|
+
method: 'GET',
|
|
2312
|
+
headers: { orgId },
|
|
2313
|
+
uri: `${baseUrl}/contacts/${encodeURIComponent(contactId)}`,
|
|
2314
|
+
json: true,
|
|
2315
|
+
};
|
|
2316
|
+
const response = await this.helpers.requestOAuth2.call(this, 'zohoDeskOAuth2Api', options);
|
|
2317
|
+
returnData.push({
|
|
2318
|
+
json: response,
|
|
2319
|
+
pairedItem: { item: i },
|
|
2320
|
+
});
|
|
2321
|
+
}
|
|
2322
|
+
if (operation === 'list') {
|
|
2323
|
+
const returnAll = this.getNodeParameter('returnAll', i);
|
|
2324
|
+
if (returnAll) {
|
|
2325
|
+
const items = await getAllPaginatedItems(this, baseUrl, '/contacts', orgId, 100, 'data');
|
|
2326
|
+
for (const item of items) {
|
|
2327
|
+
returnData.push({
|
|
2328
|
+
json: item,
|
|
2329
|
+
pairedItem: { item: i },
|
|
2330
|
+
});
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2333
|
+
else {
|
|
2334
|
+
const limit = this.getNodeParameter('limit', i);
|
|
2335
|
+
const options = {
|
|
2336
|
+
method: 'GET',
|
|
2337
|
+
headers: { orgId },
|
|
2338
|
+
uri: `${baseUrl}/contacts`,
|
|
2339
|
+
qs: { from: 1, limit },
|
|
2340
|
+
json: true,
|
|
2341
|
+
};
|
|
2342
|
+
const response = await this.helpers.requestOAuth2.call(this, 'zohoDeskOAuth2Api', options);
|
|
2343
|
+
for (const contact of response.data || []) {
|
|
2344
|
+
returnData.push({
|
|
2345
|
+
json: contact,
|
|
2346
|
+
pairedItem: { item: i },
|
|
2347
|
+
});
|
|
2348
|
+
}
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
if (operation === 'update') {
|
|
2352
|
+
const contactId = this.getNodeParameter('contactId', i);
|
|
2353
|
+
const updateFields = this.getNodeParameter('updateFields', i);
|
|
2354
|
+
isValidZohoDeskId(contactId, 'Contact ID');
|
|
2355
|
+
const body = {};
|
|
2356
|
+
addOptionalFields(body, updateFields, CONTACT_UPDATE_OPTIONAL_FIELDS);
|
|
2357
|
+
if (updateFields.cf !== undefined) {
|
|
2358
|
+
body.cf = parseCustomFields(updateFields.cf);
|
|
2359
|
+
}
|
|
2360
|
+
const options = {
|
|
2361
|
+
method: 'PATCH',
|
|
2362
|
+
headers: { orgId },
|
|
2363
|
+
uri: `${baseUrl}/contacts/${encodeURIComponent(contactId)}`,
|
|
2364
|
+
body,
|
|
2365
|
+
json: true,
|
|
2366
|
+
};
|
|
2367
|
+
const response = await this.helpers.requestOAuth2.call(this, 'zohoDeskOAuth2Api', options);
|
|
2368
|
+
returnData.push({
|
|
2369
|
+
json: response,
|
|
2370
|
+
pairedItem: { item: i },
|
|
2371
|
+
});
|
|
2372
|
+
}
|
|
2373
|
+
if (operation === 'delete') {
|
|
2374
|
+
const contactId = this.getNodeParameter('contactId', i);
|
|
2375
|
+
isValidZohoDeskId(contactId, 'Contact ID');
|
|
2376
|
+
const options = {
|
|
2377
|
+
method: 'POST',
|
|
2378
|
+
headers: { orgId },
|
|
2379
|
+
uri: `${baseUrl}/contacts/moveToTrash`,
|
|
2380
|
+
body: { contactIds: [contactId] },
|
|
2381
|
+
json: true,
|
|
2382
|
+
};
|
|
2383
|
+
await this.helpers.requestOAuth2.call(this, 'zohoDeskOAuth2Api', options);
|
|
2384
|
+
returnData.push({
|
|
2385
|
+
json: { success: true, contactId },
|
|
2386
|
+
pairedItem: { item: i },
|
|
2387
|
+
});
|
|
2388
|
+
}
|
|
2389
|
+
}
|
|
2390
|
+
// ==================== ACCOUNT RESOURCE ====================
|
|
2391
|
+
if (resource === 'account') {
|
|
2392
|
+
if (operation === 'create') {
|
|
2393
|
+
const accountName = this.getNodeParameter('accountName', i);
|
|
2394
|
+
const additionalFields = this.getNodeParameter('additionalFields', i);
|
|
2395
|
+
const body = {
|
|
2396
|
+
accountName: validateFieldLength(accountName, undefined, 'Account Name'),
|
|
2397
|
+
};
|
|
2398
|
+
addOptionalFields(body, additionalFields, ACCOUNT_CREATE_OPTIONAL_FIELDS);
|
|
2399
|
+
if (additionalFields.cf !== undefined) {
|
|
2400
|
+
body.cf = parseCustomFields(additionalFields.cf);
|
|
2401
|
+
}
|
|
2402
|
+
const options = {
|
|
2403
|
+
method: 'POST',
|
|
2404
|
+
headers: { orgId },
|
|
2405
|
+
uri: `${baseUrl}/accounts`,
|
|
2406
|
+
body,
|
|
2407
|
+
json: true,
|
|
2408
|
+
};
|
|
2409
|
+
const response = await this.helpers.requestOAuth2.call(this, 'zohoDeskOAuth2Api', options);
|
|
2410
|
+
returnData.push({
|
|
2411
|
+
json: response,
|
|
2412
|
+
pairedItem: { item: i },
|
|
2413
|
+
});
|
|
2414
|
+
}
|
|
2415
|
+
if (operation === 'get') {
|
|
2416
|
+
const accountId = this.getNodeParameter('accountId', i);
|
|
2417
|
+
isValidZohoDeskId(accountId, 'Account ID');
|
|
2418
|
+
const options = {
|
|
2419
|
+
method: 'GET',
|
|
2420
|
+
headers: { orgId },
|
|
2421
|
+
uri: `${baseUrl}/accounts/${encodeURIComponent(accountId)}`,
|
|
2422
|
+
json: true,
|
|
2423
|
+
};
|
|
2424
|
+
const response = await this.helpers.requestOAuth2.call(this, 'zohoDeskOAuth2Api', options);
|
|
2425
|
+
returnData.push({
|
|
2426
|
+
json: response,
|
|
2427
|
+
pairedItem: { item: i },
|
|
2428
|
+
});
|
|
2429
|
+
}
|
|
2430
|
+
if (operation === 'list') {
|
|
2431
|
+
const returnAll = this.getNodeParameter('returnAll', i);
|
|
2432
|
+
if (returnAll) {
|
|
2433
|
+
const items = await getAllPaginatedItems(this, baseUrl, '/accounts', orgId, 100, 'data');
|
|
2434
|
+
for (const item of items) {
|
|
2435
|
+
returnData.push({
|
|
2436
|
+
json: item,
|
|
2437
|
+
pairedItem: { item: i },
|
|
2438
|
+
});
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
else {
|
|
2442
|
+
const limit = this.getNodeParameter('limit', i);
|
|
2443
|
+
const options = {
|
|
2444
|
+
method: 'GET',
|
|
2445
|
+
headers: { orgId },
|
|
2446
|
+
uri: `${baseUrl}/accounts`,
|
|
2447
|
+
qs: { from: 1, limit },
|
|
2448
|
+
json: true,
|
|
2449
|
+
};
|
|
2450
|
+
const response = await this.helpers.requestOAuth2.call(this, 'zohoDeskOAuth2Api', options);
|
|
2451
|
+
for (const account of response.data || []) {
|
|
2452
|
+
returnData.push({
|
|
2453
|
+
json: account,
|
|
2454
|
+
pairedItem: { item: i },
|
|
2455
|
+
});
|
|
2456
|
+
}
|
|
2457
|
+
}
|
|
2458
|
+
}
|
|
2459
|
+
if (operation === 'update') {
|
|
2460
|
+
const accountId = this.getNodeParameter('accountId', i);
|
|
2461
|
+
const updateFields = this.getNodeParameter('updateFields', i);
|
|
2462
|
+
isValidZohoDeskId(accountId, 'Account ID');
|
|
2463
|
+
const body = {};
|
|
2464
|
+
addOptionalFields(body, updateFields, ACCOUNT_UPDATE_OPTIONAL_FIELDS);
|
|
2465
|
+
if (updateFields.cf !== undefined) {
|
|
2466
|
+
body.cf = parseCustomFields(updateFields.cf);
|
|
2467
|
+
}
|
|
2468
|
+
const options = {
|
|
2469
|
+
method: 'PATCH',
|
|
2470
|
+
headers: { orgId },
|
|
2471
|
+
uri: `${baseUrl}/accounts/${encodeURIComponent(accountId)}`,
|
|
2472
|
+
body,
|
|
2473
|
+
json: true,
|
|
2474
|
+
};
|
|
2475
|
+
const response = await this.helpers.requestOAuth2.call(this, 'zohoDeskOAuth2Api', options);
|
|
2476
|
+
returnData.push({
|
|
2477
|
+
json: response,
|
|
2478
|
+
pairedItem: { item: i },
|
|
2479
|
+
});
|
|
2480
|
+
}
|
|
2481
|
+
if (operation === 'delete') {
|
|
2482
|
+
const accountId = this.getNodeParameter('accountId', i);
|
|
2483
|
+
isValidZohoDeskId(accountId, 'Account ID');
|
|
2484
|
+
const options = {
|
|
2485
|
+
method: 'POST',
|
|
2486
|
+
headers: { orgId },
|
|
2487
|
+
uri: `${baseUrl}/accounts/moveToTrash`,
|
|
2488
|
+
body: { accountIds: [accountId] },
|
|
2489
|
+
json: true,
|
|
2490
|
+
};
|
|
2491
|
+
await this.helpers.requestOAuth2.call(this, 'zohoDeskOAuth2Api', options);
|
|
2492
|
+
returnData.push({
|
|
2493
|
+
json: { success: true, accountId },
|
|
2494
|
+
pairedItem: { item: i },
|
|
2495
|
+
});
|
|
2496
|
+
}
|
|
2497
|
+
}
|
|
2498
|
+
}
|
|
2499
|
+
catch (error) {
|
|
2500
|
+
// Check for rate limiting (HTTP 429)
|
|
2501
|
+
const errorObj = error;
|
|
2502
|
+
if (errorObj.statusCode === 429 || errorObj.code === 429) {
|
|
2503
|
+
const rateLimitError = new Error('Zoho Desk API rate limit exceeded (10 requests/second per organization). ' +
|
|
2504
|
+
'Please wait a moment and try again, or reduce the number of items being processed.');
|
|
2505
|
+
if (this.continueOnFail()) {
|
|
2506
|
+
returnData.push({
|
|
2507
|
+
json: {
|
|
2508
|
+
error: rateLimitError.message,
|
|
2509
|
+
},
|
|
2510
|
+
pairedItem: { item: i },
|
|
2511
|
+
});
|
|
2512
|
+
continue;
|
|
2513
|
+
}
|
|
2514
|
+
throw rateLimitError;
|
|
2515
|
+
}
|
|
2516
|
+
// Handle other errors
|
|
2517
|
+
if (this.continueOnFail()) {
|
|
2518
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2519
|
+
returnData.push({
|
|
2520
|
+
json: {
|
|
2521
|
+
error: errorMessage,
|
|
2522
|
+
},
|
|
2523
|
+
pairedItem: { item: i },
|
|
2524
|
+
});
|
|
2525
|
+
continue;
|
|
2526
|
+
}
|
|
2527
|
+
throw error;
|
|
2528
|
+
}
|
|
2529
|
+
}
|
|
2530
|
+
return [returnData];
|
|
2531
|
+
}
|
|
2532
|
+
}
|
|
2533
|
+
exports.ZohoDesk = ZohoDesk;
|
|
2534
|
+
//# sourceMappingURL=ZohoDesk.node.js.map
|