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.
@@ -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