@trg-admin/n8n-nodes-zoho-desk 0.1.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 ADDED
@@ -0,0 +1,312 @@
1
+ # n8n Zoho Desk Node
2
+
3
+ A comprehensive n8n community node for **Zoho Desk** with automatic OAuth2 token refresh handling, supporting Tickets, Contacts, and Comments.
4
+
5
+ ## ✨ Features
6
+
7
+ - **Automatic Token Refresh**: Uses OAuth2 with refresh tokens - no manual token management needed
8
+ - **Multi-Region Support**: Automatically detects and uses the correct Zoho data center (US, EU, IN, AU, CN)
9
+ - **Combined CRM & Desk Access**: Single credential works for both Zoho CRM and Zoho Desk nodes
10
+ - **Comprehensive Operations**:
11
+ - **Tickets**: Create, Get, Get Many, Update
12
+ - **Contacts**: Create, Get, Get Many, Search, Update
13
+ - **Comments**: Add, Get Many
14
+
15
+ ## 🔐 Authentication Setup
16
+
17
+ This node uses a **custom OAuth2 credential** (`zohoCRMDeskOAuth2Api`) that properly handles refresh tokens and multi-region support.
18
+
19
+ ### Step 1: Create OAuth2 App in Zoho API Console
20
+
21
+ 1. Go to [Zoho API Console](https://api-console.zoho.com/)
22
+ 2. Click **ADD CLIENT** → **Server-based Applications**
23
+ 3. Fill in:
24
+ - **Client Name**: `n8n Integration` (or your preferred name)
25
+ - **Homepage URL**: `https://your-n8n-instance.com`
26
+ - **Authorized Redirect URI**: `https://your-n8n-instance.com/rest/oauth2-credential/callback`
27
+ 4. Click **CREATE**
28
+ 5. Copy the **Client ID** and **Client Secret** (you'll need these in n8n)
29
+
30
+ ### Step 2: Configure Scopes
31
+
32
+ When you create the credential in n8n, the default scope is:
33
+
34
+ ```
35
+ ZohoCRM.modules.ALL Desk.tickets.ALL Desk.contacts.ALL Desk.search.READ
36
+ ```
37
+
38
+ **Available Zoho Desk Scopes:**
39
+ - `Desk.tickets.ALL` - Full access to tickets
40
+ - `Desk.contacts.ALL` - Full access to contacts
41
+ - `Desk.search.READ` - Search across modules
42
+ - `Desk.settings.ALL` - Access to settings and configurations
43
+ - `Desk.tasks.ALL` - Task management
44
+ - `Desk.basic.READ` - Read-only access to basic information
45
+
46
+ You can customize the scope field in the credential to add or remove permissions as needed.
47
+
48
+ ### Step 3: Get Your Organization ID
49
+
50
+ 1. Log in to [Zoho Desk](https://desk.zoho.com/)
51
+ 2. Go to **Setup** (gear icon) → **Developer Space** → **API**
52
+ 3. Copy your **Organization ID** (looks like: `12345678`)
53
+ 4. You'll enter this as a parameter in each Zoho Desk node
54
+
55
+ ### Step 4: Configure Credential in n8n
56
+
57
+ 1. In n8n, go to **Credentials** → **New**
58
+ 2. Search for **Zoho CRM & Desk OAuth2 API**
59
+ 3. Fill in:
60
+ - **Authorization URL**: Select your region (e.g., `https://accounts.zoho.com/oauth/v2/auth` for US)
61
+ - **Access Token URL**: Select matching region (e.g., `https://accounts.zoho.com/oauth/v2/token` for US)
62
+ - **Client ID**: From Step 1
63
+ - **Client Secret**: From Step 1
64
+ - **Scope**: Default works for most cases, customize if needed
65
+ 4. Click **Connect my account**
66
+ 5. Authorize access in Zoho
67
+ 6. ✅ Credential is ready to use!
68
+
69
+ ### Regional Mapping
70
+
71
+ Select the **same region** for both Authorization URL and Access Token URL:
72
+
73
+ | Region | Authorization URL | Access Token URL |
74
+ |--------|-------------------|------------------|
75
+ | **US** | `https://accounts.zoho.com/oauth/v2/auth` | `https://accounts.zoho.com/oauth/v2/token` |
76
+ | **EU** | `https://accounts.zoho.com/oauth/v2/auth` | `https://accounts.zoho.eu/oauth/v2/token` |
77
+ | **India** | `https://accounts.zoho.com/oauth/v2/auth` | `https://accounts.zoho.in/oauth/v2/token` |
78
+ | **Australia** | `https://accounts.zoho.com/oauth/v2/auth` | `https://accounts.zoho.com.au/oauth/v2/token` |
79
+ | **China** | `https://accounts.zoho.com.cn/oauth/v2/auth` | `https://accounts.zoho.com.cn/oauth/v2/token` |
80
+
81
+ **Note**: The API domain (e.g., `https://desk.zoho.com`) is automatically detected from the OAuth response - you don't need to configure it manually.
82
+
83
+ ## 📦 Installation
84
+
85
+ ### Option 1: Install from npm (when published)
86
+
87
+ ```bash
88
+ npm install n8n-nodes-zoho -g
89
+ ```
90
+
91
+ ### Option 2: Local Development
92
+
93
+ 1. Clone this repository:
94
+ ```bash
95
+ git clone <your-repo-url>
96
+ cd n8n-nodes-zoho
97
+ ```
98
+
99
+ 2. Install dependencies and build:
100
+ ```bash
101
+ npm install
102
+ npm run build
103
+ ```
104
+
105
+ 3. Link to your n8n instance:
106
+ ```bash
107
+ npm link
108
+ ```
109
+
110
+ 4. In your n8n installation directory:
111
+ ```bash
112
+ npm link n8n-nodes-zoho
113
+ ```
114
+
115
+ 5. Restart n8n
116
+
117
+ ## 🚀 Usage Examples
118
+
119
+ ### Create a Ticket
120
+
121
+ **Resource**: Ticket
122
+ **Operation**: Create
123
+ **Parameters**:
124
+ - **Organization ID**: `12345678`
125
+ - **Subject**: `Unable to log in`
126
+ - **Contact Email**: `customer@example.com`
127
+ - **Department ID**: `987654321`
128
+ - **Description**: `Customer reports they cannot access their account`
129
+ - **Additional Fields**:
130
+ - Priority: `High`
131
+ - Status: `Open`
132
+
133
+ ### Search for Contacts
134
+
135
+ **Resource**: Contact
136
+ **Operation**: Search
137
+ **Parameters**:
138
+ - **Organization ID**: `12345678`
139
+ - **Search Query**: `john@example.com`
140
+ - **Limit**: `10`
141
+
142
+ ### Add Comment to Ticket
143
+
144
+ **Resource**: Comment
145
+ **Operation**: Add
146
+ **Parameters**:
147
+ - **Organization ID**: `12345678`
148
+ - **Ticket ID**: `123456`
149
+ - **Content**: `Thank you for contacting support. We're looking into this issue.`
150
+ - **Is Public**: `✓` (visible to customer)
151
+ - **Content Type**: `Plain Text`
152
+
153
+ ## 🔧 How Token Refresh Works
154
+
155
+ This node uses n8n's built-in OAuth2 framework with `requestOAuth2`, which provides automatic token refresh:
156
+
157
+ 1. **Initial Authorization**: When you connect the credential, Zoho returns:
158
+ - Access token (valid for ~1 hour)
159
+ - Refresh token (long-lived)
160
+ - Regional API domain
161
+
162
+ 2. **Automatic Refresh**: When the access token expires:
163
+ - n8n detects the 401 error
164
+ - Automatically calls Zoho's token endpoint with the refresh token
165
+ - Updates the stored access token
166
+ - Retries the original request
167
+ - **You never see the error or interruption**
168
+
169
+ 3. **No Manual Intervention**: Unlike custom API key implementations, you never need to manually refresh tokens or update credentials.
170
+
171
+ ## 🤔 Why Not Use the Built-in `zohoOAuth2Api`?
172
+
173
+ The built-in Zoho CRM credential (`zohoOAuth2Api`) has **hardcoded scopes** for CRM only:
174
+
175
+ ```typescript
176
+ scope: 'ZohoCRM.modules.ALL,ZohoCRM.settings.all,ZohoCRM.users.all'
177
+ ```
178
+
179
+ The scope property is `type: 'hidden'`, so users cannot modify it to add Desk permissions. This is why we created a custom credential that:
180
+ - Extends the same OAuth2 framework
181
+ - Allows editable scopes
182
+ - Defaults to both CRM and Desk permissions
183
+ - Uses the exact same token refresh mechanism
184
+
185
+ ## 🐛 Troubleshooting
186
+
187
+ ### "INVALID_OAUTH" or "Authentication failed" errors
188
+
189
+ **Cause**: Credentials may be using the wrong region
190
+ **Fix**: Ensure Authorization URL and Access Token URL match your Zoho account region
191
+
192
+ ### "Invalid OrgId" error
193
+
194
+ **Cause**: Organization ID is incorrect or missing
195
+ **Fix**: Double-check your Organization ID in Zoho Desk Setup → Developer Space → API
196
+
197
+ ### "Insufficient scope" or 401 Unauthorized
198
+
199
+ **Cause**: OAuth app doesn't have required scopes
200
+ **Fix**: Update the **Scope** field in your credential to include the necessary permissions (e.g., add `Desk.tasks.ALL` if you need task access)
201
+
202
+ ### Token refresh not happening automatically
203
+
204
+ **Cause**: Using `httpRequest` instead of `requestOAuth2`
205
+ **Fix**: This should not happen with this node - it's built to use `requestOAuth2`. If you see this, please file an issue.
206
+
207
+ ### "Department ID not found" when creating tickets
208
+
209
+ **Cause**: Invalid department ID
210
+ **Fix**: Get valid department IDs from Zoho Desk Setup → Channels & Departments, or use the Zoho Desk API to list departments
211
+
212
+ ## 📚 API Documentation
213
+
214
+ - [Zoho Desk API Reference](https://desk.zoho.com/DeskAPIDocument)
215
+ - [Zoho OAuth 2.0 Guide](https://www.zoho.com/accounts/protocol/oauth.html)
216
+ - [n8n Community Nodes Documentation](https://docs.n8n.io/integrations/creating-nodes/)
217
+
218
+ ## 🛠️ Development
219
+
220
+ ### Build
221
+
222
+ ```bash
223
+ npm run build
224
+ ```
225
+
226
+ ### Type Check
227
+
228
+ ```bash
229
+ npm run typecheck
230
+ ```
231
+
232
+ ### Validate
233
+
234
+ ```bash
235
+ npm test
236
+ ```
237
+
238
+ ## 📝 Publishing to npm
239
+
240
+ 1. Update `package.json`:
241
+ - Bump `version`
242
+ - Update `name` if needed (must be unique on npm)
243
+ - Verify `description`, `author`, `keywords`
244
+
245
+ 2. Build and test:
246
+ ```bash
247
+ npm run typecheck
248
+ npm run build
249
+ npm test
250
+ ```
251
+
252
+ 3. Publish:
253
+ ```bash
254
+ npm login
255
+ npm publish
256
+ ```
257
+
258
+ ## 📄 License
259
+
260
+ MIT
261
+
262
+ ## 🤝 Contributing
263
+
264
+ Contributions are welcome! Please feel free to submit issues or pull requests.
265
+
266
+ ## 🙏 Credits
267
+
268
+ Built following n8n's community node patterns and inspired by the official Zoho CRM node implementation.
269
+
270
+ ```bash
271
+ npm login
272
+ npm whoami
273
+ ```
274
+
275
+ If your org uses 2FA, npm will ask for OTP during publish.
276
+
277
+ ### 4) Publish
278
+
279
+ ```bash
280
+ npm publish
281
+ ```
282
+
283
+ For updates, bump version first:
284
+
285
+ ```bash
286
+ npm version patch
287
+ npm publish
288
+ ```
289
+
290
+ ### 5) Install in n8n
291
+
292
+ On your n8n host:
293
+
294
+ ```bash
295
+ npm install n8n-nodes-zoho-desk
296
+ ```
297
+
298
+ Then restart n8n. The new node should appear in the editor.
299
+
300
+ ### 6) Configure credentials in n8n
301
+
302
+ In n8n UI:
303
+ 1. Create credentials using **Zoho OAuth2 API**.
304
+ 2. Add `Zoho Desk` node to a workflow.
305
+ 3. Set `Data Center` and `Organization ID`.
306
+ 4. Run ticket operations.
307
+
308
+ ## Troubleshooting npm publish
309
+
310
+ - **`403 Forbidden` when publishing**: package name already exists or account lacks rights; change name or publish under the correct npm org/scope.
311
+ - **`402 Payment Required`**: scoped package defaults to private; use `--access public` or keep `publishConfig.access = public`.
312
+ - **Missing files after install**: confirm `files` in `package.json` includes your built `dist` output and rerun `npm pack`.
@@ -0,0 +1,8 @@
1
+ import type { ICredentialType, INodeProperties } from 'n8n-workflow';
2
+ export declare class ZohoCRMDeskOAuth2Api implements ICredentialType {
3
+ name: string;
4
+ extends: string[];
5
+ displayName: string;
6
+ documentationUrl: string;
7
+ properties: INodeProperties[];
8
+ }
@@ -0,0 +1,90 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ZohoCRMDeskOAuth2Api = void 0;
4
+ class ZohoCRMDeskOAuth2Api {
5
+ constructor() {
6
+ this.name = 'zohoCRMDeskOAuth2Api';
7
+ this.extends = ['oAuth2Api'];
8
+ this.displayName = 'Zoho CRM & Desk OAuth2 API';
9
+ this.documentationUrl = 'https://www.zoho.com/desk/developer-guide/';
10
+ this.properties = [
11
+ {
12
+ displayName: 'Grant Type',
13
+ name: 'grantType',
14
+ type: 'hidden',
15
+ default: 'authorizationCode',
16
+ },
17
+ {
18
+ displayName: 'Authorization URL',
19
+ name: 'authUrl',
20
+ type: 'options',
21
+ options: [
22
+ {
23
+ name: 'https://accounts.zoho.com/oauth/v2/auth',
24
+ value: 'https://accounts.zoho.com/oauth/v2/auth',
25
+ description: 'For US, EU, AU, and IN domains',
26
+ },
27
+ {
28
+ name: 'https://accounts.zoho.com.cn/oauth/v2/auth',
29
+ value: 'https://accounts.zoho.com.cn/oauth/v2/auth',
30
+ description: 'For China domain',
31
+ },
32
+ ],
33
+ default: 'https://accounts.zoho.com/oauth/v2/auth',
34
+ required: true,
35
+ },
36
+ {
37
+ displayName: 'Access Token URL',
38
+ name: 'accessTokenUrl',
39
+ type: 'options',
40
+ options: [
41
+ {
42
+ name: 'US - https://accounts.zoho.com/oauth/v2/token',
43
+ value: 'https://accounts.zoho.com/oauth/v2/token',
44
+ },
45
+ {
46
+ name: 'EU - https://accounts.zoho.eu/oauth/v2/token',
47
+ value: 'https://accounts.zoho.eu/oauth/v2/token',
48
+ },
49
+ {
50
+ name: 'IN - https://accounts.zoho.in/oauth/v2/token',
51
+ value: 'https://accounts.zoho.in/oauth/v2/token',
52
+ },
53
+ {
54
+ name: 'AU - https://accounts.zoho.com.au/oauth/v2/token',
55
+ value: 'https://accounts.zoho.com.au/oauth/v2/token',
56
+ },
57
+ {
58
+ name: 'CN - https://accounts.zoho.com.cn/oauth/v2/token',
59
+ value: 'https://accounts.zoho.com.cn/oauth/v2/token',
60
+ },
61
+ ],
62
+ default: 'https://accounts.zoho.com/oauth/v2/token',
63
+ required: true,
64
+ },
65
+ {
66
+ displayName: 'Scope',
67
+ name: 'scope',
68
+ type: 'string',
69
+ default: 'ZohoCRM.modules.ALL Desk.tickets.ALL Desk.contacts.ALL Desk.search.READ',
70
+ required: true,
71
+ description: 'Space-separated OAuth scopes. Default includes both CRM and Desk permissions. Add or remove scopes as needed (e.g., Desk.settings.ALL, Desk.tasks.ALL).',
72
+ },
73
+ {
74
+ displayName: 'Auth URI Query Parameters',
75
+ name: 'authQueryParameters',
76
+ type: 'hidden',
77
+ default: 'access_type=offline',
78
+ description: 'Requests offline access with refresh token for automatic token renewal',
79
+ },
80
+ {
81
+ displayName: 'Authentication',
82
+ name: 'authentication',
83
+ type: 'hidden',
84
+ default: 'body',
85
+ description: 'Zoho requires client credentials in request body',
86
+ },
87
+ ];
88
+ }
89
+ }
90
+ exports.ZohoCRMDeskOAuth2Api = ZohoCRMDeskOAuth2Api;
@@ -0,0 +1,2 @@
1
+ export * from './nodes/ZohoDesk/ZohoDesk.node';
2
+ export * from './credentials/ZohoCRMDeskOAuth2Api.credentials';
package/dist/index.js ADDED
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./nodes/ZohoDesk/ZohoDesk.node"), exports);
18
+ __exportStar(require("./credentials/ZohoCRMDeskOAuth2Api.credentials"), exports);
@@ -0,0 +1,6 @@
1
+ import type { IExecuteFunctions, ILoadOptionsFunctions, IHttpRequestMethods, IDataObject } from 'n8n-workflow';
2
+ export declare function zohoDeskApiRequest(this: IExecuteFunctions | ILoadOptionsFunctions, method: IHttpRequestMethods, endpoint: string, body?: IDataObject, qs?: IDataObject, itemIndex?: number): Promise<any>;
3
+ /**
4
+ * Make an API request to paginate through all items
5
+ */
6
+ export declare function zohoDeskApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions, method: IHttpRequestMethods, endpoint: string, body?: IDataObject, qs?: IDataObject, itemIndex?: number): Promise<any>;
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.zohoDeskApiRequest = zohoDeskApiRequest;
4
+ exports.zohoDeskApiRequestAllItems = zohoDeskApiRequestAllItems;
5
+ const n8n_workflow_1 = require("n8n-workflow");
6
+ async function zohoDeskApiRequest(method, endpoint, body = {}, qs = {}, itemIndex = 0) {
7
+ // Get credentials with OAuth token data containing regional API domain
8
+ const credentials = (await this.getCredentials('zohoCRMDeskOAuth2Api'));
9
+ const { oauthTokenData } = credentials;
10
+ // Get organization ID from node parameters
11
+ const organizationId = this.getNodeParameter('organizationId', itemIndex);
12
+ // Build request options
13
+ const options = {
14
+ method,
15
+ qs,
16
+ body,
17
+ uri: `${oauthTokenData.api_domain}/api/v1${endpoint}`,
18
+ headers: {
19
+ orgId: organizationId,
20
+ 'Content-Type': 'application/json',
21
+ },
22
+ json: true,
23
+ };
24
+ // Clean up empty parameters
25
+ if (!Object.keys(body).length) {
26
+ delete options.body;
27
+ }
28
+ if (!Object.keys(qs).length) {
29
+ delete options.qs;
30
+ }
31
+ try {
32
+ // Use requestOAuth2 for automatic token refresh handling
33
+ const responseData = await this.helpers.requestOAuth2?.call(this, 'zohoCRMDeskOAuth2Api', options);
34
+ if (responseData === undefined) {
35
+ return [];
36
+ }
37
+ return responseData;
38
+ }
39
+ catch (error) {
40
+ throw new n8n_workflow_1.NodeApiError(this.getNode(), error);
41
+ }
42
+ }
43
+ /**
44
+ * Make an API request to paginate through all items
45
+ */
46
+ async function zohoDeskApiRequestAllItems(method, endpoint, body = {}, qs = {}, itemIndex = 0) {
47
+ const returnData = [];
48
+ let responseData;
49
+ qs.limit = 100; // Zoho Desk max limit per page
50
+ qs.from = 0;
51
+ do {
52
+ responseData = await zohoDeskApiRequest.call(this, method, endpoint, body, qs, itemIndex);
53
+ if (responseData.data && Array.isArray(responseData.data)) {
54
+ returnData.push(...responseData.data);
55
+ if (responseData.data.length < 100) {
56
+ // Last page
57
+ break;
58
+ }
59
+ qs.from = qs.from + 100;
60
+ }
61
+ else {
62
+ break;
63
+ }
64
+ } while (true);
65
+ return returnData;
66
+ }
@@ -0,0 +1,5 @@
1
+ import type { IExecuteFunctions, INodeExecutionData, INodeType, INodeTypeDescription } from 'n8n-workflow';
2
+ export declare class ZohoDesk implements INodeType {
3
+ description: INodeTypeDescription;
4
+ execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]>;
5
+ }
@@ -0,0 +1,516 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ZohoDesk = void 0;
4
+ const n8n_workflow_1 = require("n8n-workflow");
5
+ const GenericFunctions_1 = require("./GenericFunctions");
6
+ class ZohoDesk {
7
+ constructor() {
8
+ this.description = {
9
+ displayName: 'Zoho Desk',
10
+ name: 'zohoDesk',
11
+ icon: 'file:zohodesk.svg',
12
+ group: ['transform'],
13
+ version: 1,
14
+ subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
15
+ description: 'Zoho Desk starter node using n8n built-in zohoOAuth2Api credential for automatic refresh-token handling',
16
+ defaults: {
17
+ name: 'Zoho Desk',
18
+ },
19
+ inputs: ['main'],
20
+ outputs: ['main'],
21
+ credentials: [
22
+ {
23
+ name: 'zohoCRMDeskOAuth2Api',
24
+ required: true,
25
+ },
26
+ ],
27
+ properties: [
28
+ {
29
+ displayName: 'Organization ID',
30
+ name: 'organizationId',
31
+ type: 'string',
32
+ default: '',
33
+ required: true,
34
+ description: 'Your Zoho Desk Organization ID. Find this in Setup > Developer Space > API.',
35
+ },
36
+ {
37
+ displayName: 'Resource',
38
+ name: 'resource',
39
+ type: 'options',
40
+ noDataExpression: true,
41
+ default: 'ticket',
42
+ options: [
43
+ { name: 'Ticket', value: 'ticket' },
44
+ { name: 'Contact', value: 'contact' },
45
+ { name: 'Comment', value: 'comment' },
46
+ ],
47
+ },
48
+ // Ticket Operations
49
+ {
50
+ displayName: 'Operation',
51
+ name: 'operation',
52
+ type: 'options',
53
+ noDataExpression: true,
54
+ displayOptions: { show: { resource: ['ticket'] } },
55
+ default: 'get',
56
+ options: [
57
+ { name: 'Create', value: 'create', action: 'Create a ticket' },
58
+ { name: 'Get', value: 'get', action: 'Get a ticket' },
59
+ { name: 'Get Many', value: 'getAll', action: 'Get many tickets' },
60
+ { name: 'Update', value: 'update', action: 'Update a ticket' },
61
+ ],
62
+ },
63
+ // Contact Operations
64
+ {
65
+ displayName: 'Operation',
66
+ name: 'operation',
67
+ type: 'options',
68
+ noDataExpression: true,
69
+ displayOptions: { show: { resource: ['contact'] } },
70
+ default: 'get',
71
+ options: [
72
+ { name: 'Create', value: 'create', action: 'Create a contact' },
73
+ { name: 'Get', value: 'get', action: 'Get a contact' },
74
+ { name: 'Get Many', value: 'getAll', action: 'Get many contacts' },
75
+ { name: 'Search', value: 'search', action: 'Search contacts' },
76
+ { name: 'Update', value: 'update', action: 'Update a contact' },
77
+ ],
78
+ },
79
+ // Comment Operations
80
+ {
81
+ displayName: 'Operation',
82
+ name: 'operation',
83
+ type: 'options',
84
+ noDataExpression: true,
85
+ displayOptions: { show: { resource: ['comment'] } },
86
+ default: 'add',
87
+ options: [
88
+ { name: 'Add', value: 'add', action: 'Add a comment to ticket' },
89
+ { name: 'Get Many', value: 'getAll', action: 'Get all comments from ticket' },
90
+ ],
91
+ },
92
+ // ================== Ticket Resource Parameters ==================
93
+ {
94
+ displayName: 'Ticket ID',
95
+ name: 'ticketId',
96
+ type: 'string',
97
+ default: '',
98
+ required: true,
99
+ displayOptions: { show: { resource: ['ticket'], operation: ['get', 'update'] } },
100
+ description: 'The unique ID of the ticket',
101
+ },
102
+ {
103
+ displayName: 'Subject',
104
+ name: 'subject',
105
+ type: 'string',
106
+ default: '',
107
+ required: true,
108
+ displayOptions: { show: { resource: ['ticket'], operation: ['create'] } },
109
+ description: 'Subject/title of the ticket',
110
+ },
111
+ {
112
+ displayName: 'Contact Email',
113
+ name: 'email',
114
+ type: 'string',
115
+ default: '',
116
+ required: true,
117
+ displayOptions: { show: { resource: ['ticket'], operation: ['create'] } },
118
+ description: 'Email address of the contact creating this ticket',
119
+ },
120
+ {
121
+ displayName: 'Department ID',
122
+ name: 'departmentId',
123
+ type: 'string',
124
+ default: '',
125
+ required: true,
126
+ displayOptions: { show: { resource: ['ticket'], operation: ['create'] } },
127
+ description: 'ID of the department to assign the ticket',
128
+ },
129
+ {
130
+ displayName: 'Description',
131
+ name: 'description',
132
+ type: 'string',
133
+ typeOptions: { alwaysOpenEditWindow: true },
134
+ default: '',
135
+ displayOptions: { show: { resource: ['ticket'], operation: ['create', 'update'] } },
136
+ description: 'Description/content of the ticket',
137
+ },
138
+ {
139
+ displayName: 'Additional Fields',
140
+ name: 'additionalFields',
141
+ type: 'collection',
142
+ placeholder: 'Add Field',
143
+ default: {},
144
+ displayOptions: { show: { resource: ['ticket'], operation: ['create', 'update'] } },
145
+ options: [
146
+ {
147
+ displayName: 'Assignee ID',
148
+ name: 'assigneeId',
149
+ type: 'string',
150
+ default: '',
151
+ description: 'Agent ID to assign the ticket to',
152
+ },
153
+ {
154
+ displayName: 'Contact ID',
155
+ name: 'contactId',
156
+ type: 'string',
157
+ default: '',
158
+ description: 'ID of existing contact (alternative to email)',
159
+ },
160
+ {
161
+ displayName: 'Due Date',
162
+ name: 'dueDate',
163
+ type: 'dateTime',
164
+ default: '',
165
+ description: 'Due date for the ticket (ISO 8601 format)',
166
+ },
167
+ {
168
+ displayName: 'Priority',
169
+ name: 'priority',
170
+ type: 'options',
171
+ options: [
172
+ { name: 'High', value: 'High' },
173
+ { name: 'Medium', value: 'Medium' },
174
+ { name: 'Low', value: 'Low' },
175
+ ],
176
+ default: 'Medium',
177
+ description: 'Priority level of the ticket',
178
+ },
179
+ {
180
+ displayName: 'Status',
181
+ name: 'status',
182
+ type: 'string',
183
+ default: '',
184
+ description: 'Status of the ticket (e.g., Open, In Progress, Closed)',
185
+ },
186
+ ],
187
+ },
188
+ {
189
+ displayName: 'Limit',
190
+ name: 'limit',
191
+ type: 'number',
192
+ typeOptions: { minValue: 1, maxValue: 100 },
193
+ default: 20,
194
+ displayOptions: { show: { resource: ['ticket'], operation: ['getAll'] } },
195
+ description: 'Max number of results to return',
196
+ },
197
+ {
198
+ displayName: 'Return All',
199
+ name: 'returnAll',
200
+ type: 'boolean',
201
+ default: false,
202
+ displayOptions: { show: { resource: ['ticket'], operation: ['getAll'] } },
203
+ description: 'Whether to return all results or limit to specified number',
204
+ },
205
+ // ================== Contact Resource Parameters ==================
206
+ {
207
+ displayName: 'Contact ID',
208
+ name: 'contactId',
209
+ type: 'string',
210
+ default: '',
211
+ required: true,
212
+ displayOptions: { show: { resource: ['contact'], operation: ['get', 'update'] } },
213
+ description: 'The unique ID of the contact',
214
+ },
215
+ {
216
+ displayName: 'Last Name',
217
+ name: 'lastName',
218
+ type: 'string',
219
+ default: '',
220
+ required: true,
221
+ displayOptions: { show: { resource: ['contact'], operation: ['create'] } },
222
+ description: 'Last name of the contact',
223
+ },
224
+ {
225
+ displayName: 'Email',
226
+ name: 'email',
227
+ type: 'string',
228
+ default: '',
229
+ required: true,
230
+ displayOptions: { show: { resource: ['contact'], operation: ['create'] } },
231
+ description: 'Email address of the contact',
232
+ },
233
+ {
234
+ displayName: 'Additional Fields',
235
+ name: 'additionalFields',
236
+ type: 'collection',
237
+ placeholder: 'Add Field',
238
+ default: {},
239
+ displayOptions: { show: { resource: ['contact'], operation: ['create', 'update'] } },
240
+ options: [
241
+ {
242
+ displayName: 'First Name',
243
+ name: 'firstName',
244
+ type: 'string',
245
+ default: '',
246
+ description: 'First name of the contact',
247
+ },
248
+ {
249
+ displayName: 'Phone',
250
+ name: 'phone',
251
+ type: 'string',
252
+ default: '',
253
+ description: 'Phone number of the contact',
254
+ },
255
+ {
256
+ displayName: 'Mobile',
257
+ name: 'mobile',
258
+ type: 'string',
259
+ default: '',
260
+ description: 'Mobile number of the contact',
261
+ },
262
+ {
263
+ displayName: 'Account ID',
264
+ name: 'accountId',
265
+ type: 'string',
266
+ default: '',
267
+ description: 'ID of the account to associate with',
268
+ },
269
+ {
270
+ displayName: 'Description',
271
+ name: 'description',
272
+ type: 'string',
273
+ default: '',
274
+ description: 'Additional description about the contact',
275
+ },
276
+ ],
277
+ },
278
+ {
279
+ displayName: 'Search Query',
280
+ name: 'searchQuery',
281
+ type: 'string',
282
+ default: '',
283
+ required: true,
284
+ displayOptions: { show: { resource: ['contact'], operation: ['search'] } },
285
+ description: 'Search query string (e.g., email, name, phone)',
286
+ },
287
+ {
288
+ displayName: 'Limit',
289
+ name: 'limit',
290
+ type: 'number',
291
+ typeOptions: { minValue: 1, maxValue: 100 },
292
+ default: 20,
293
+ displayOptions: { show: { resource: ['contact'], operation: ['getAll', 'search'] } },
294
+ description: 'Max number of results to return',
295
+ },
296
+ {
297
+ displayName: 'Return All',
298
+ name: 'returnAll',
299
+ type: 'boolean',
300
+ default: false,
301
+ displayOptions: { show: { resource: ['contact'], operation: ['getAll'] } },
302
+ description: 'Whether to return all results or limit to specified number',
303
+ },
304
+ // ================== Comment Resource Parameters ==================
305
+ {
306
+ displayName: 'Ticket ID',
307
+ name: 'ticketId',
308
+ type: 'string',
309
+ default: '',
310
+ required: true,
311
+ displayOptions: { show: { resource: ['comment'], operation: ['add', 'getAll'] } },
312
+ description: 'ID of the ticket to add comment to or get comments from',
313
+ },
314
+ {
315
+ displayName: 'Content',
316
+ name: 'content',
317
+ type: 'string',
318
+ typeOptions: { alwaysOpenEditWindow: true },
319
+ default: '',
320
+ required: true,
321
+ displayOptions: { show: { resource: ['comment'], operation: ['add'] } },
322
+ description: 'Content of the comment',
323
+ },
324
+ {
325
+ displayName: 'Is Public',
326
+ name: 'isPublic',
327
+ type: 'boolean',
328
+ default: true,
329
+ displayOptions: { show: { resource: ['comment'], operation: ['add'] } },
330
+ description: 'Whether the comment is public (visible to customer) or private (internal note)',
331
+ },
332
+ {
333
+ displayName: 'Content Type',
334
+ name: 'contentType',
335
+ type: 'options',
336
+ options: [
337
+ { name: 'Plain Text', value: 'plainText' },
338
+ { name: 'HTML', value: 'html' },
339
+ ],
340
+ default: 'plainText',
341
+ displayOptions: { show: { resource: ['comment'], operation: ['add'] } },
342
+ description: 'Format of the comment content',
343
+ },
344
+ {
345
+ displayName: 'Limit',
346
+ name: 'limit',
347
+ type: 'number',
348
+ typeOptions: { minValue: 1, maxValue: 100 },
349
+ default: 20,
350
+ displayOptions: { show: { resource: ['comment'], operation: ['getAll'] } },
351
+ description: 'Max number of results to return',
352
+ },
353
+ ],
354
+ };
355
+ }
356
+ async execute() {
357
+ const items = this.getInputData();
358
+ const returnData = [];
359
+ for (let i = 0; i < items.length; i++) {
360
+ const resource = this.getNodeParameter('resource', i);
361
+ const operation = this.getNodeParameter('operation', i);
362
+ try {
363
+ // ==================== TICKET OPERATIONS ====================
364
+ if (resource === 'ticket') {
365
+ if (operation === 'get') {
366
+ const ticketId = this.getNodeParameter('ticketId', i);
367
+ const responseData = await GenericFunctions_1.zohoDeskApiRequest.call(this, 'GET', `/tickets/${ticketId}`, {}, {}, i);
368
+ returnData.push({ json: responseData });
369
+ }
370
+ if (operation === 'getAll') {
371
+ const returnAll = this.getNodeParameter('returnAll', i, false);
372
+ if (returnAll) {
373
+ const responseData = await GenericFunctions_1.zohoDeskApiRequestAllItems.call(this, 'GET', '/tickets', {}, {}, i);
374
+ for (const row of responseData) {
375
+ returnData.push({ json: row });
376
+ }
377
+ }
378
+ else {
379
+ const limit = this.getNodeParameter('limit', i);
380
+ const responseData = await GenericFunctions_1.zohoDeskApiRequest.call(this, 'GET', '/tickets', {}, { limit }, i);
381
+ const data = Array.isArray(responseData.data)
382
+ ? responseData.data
383
+ : [responseData];
384
+ for (const row of data) {
385
+ returnData.push({ json: row });
386
+ }
387
+ }
388
+ }
389
+ if (operation === 'create') {
390
+ const subject = this.getNodeParameter('subject', i);
391
+ const email = this.getNodeParameter('email', i);
392
+ const departmentId = this.getNodeParameter('departmentId', i);
393
+ const description = this.getNodeParameter('description', i, '');
394
+ const additionalFields = this.getNodeParameter('additionalFields', i, {});
395
+ const body = {
396
+ subject,
397
+ email,
398
+ departmentId,
399
+ ...additionalFields,
400
+ };
401
+ if (description) {
402
+ body.description = description;
403
+ }
404
+ const responseData = await GenericFunctions_1.zohoDeskApiRequest.call(this, 'POST', '/tickets', body, {}, i);
405
+ returnData.push({ json: responseData });
406
+ }
407
+ if (operation === 'update') {
408
+ const ticketId = this.getNodeParameter('ticketId', i);
409
+ const description = this.getNodeParameter('description', i, '');
410
+ const additionalFields = this.getNodeParameter('additionalFields', i, {});
411
+ const body = {
412
+ ...additionalFields,
413
+ };
414
+ if (description) {
415
+ body.description = description;
416
+ }
417
+ const responseData = await GenericFunctions_1.zohoDeskApiRequest.call(this, 'PATCH', `/tickets/${ticketId}`, body, {}, i);
418
+ returnData.push({ json: responseData });
419
+ }
420
+ }
421
+ // ==================== CONTACT OPERATIONS ====================
422
+ if (resource === 'contact') {
423
+ if (operation === 'get') {
424
+ const contactId = this.getNodeParameter('contactId', i);
425
+ const responseData = await GenericFunctions_1.zohoDeskApiRequest.call(this, 'GET', `/contacts/${contactId}`, {}, {}, i);
426
+ returnData.push({ json: responseData });
427
+ }
428
+ if (operation === 'getAll') {
429
+ const returnAll = this.getNodeParameter('returnAll', i, false);
430
+ if (returnAll) {
431
+ const responseData = await GenericFunctions_1.zohoDeskApiRequestAllItems.call(this, 'GET', '/contacts', {}, {}, i);
432
+ for (const row of responseData) {
433
+ returnData.push({ json: row });
434
+ }
435
+ }
436
+ else {
437
+ const limit = this.getNodeParameter('limit', i);
438
+ const responseData = await GenericFunctions_1.zohoDeskApiRequest.call(this, 'GET', '/contacts', {}, { limit }, i);
439
+ const data = Array.isArray(responseData.data)
440
+ ? responseData.data
441
+ : [responseData];
442
+ for (const row of data) {
443
+ returnData.push({ json: row });
444
+ }
445
+ }
446
+ }
447
+ if (operation === 'create') {
448
+ const lastName = this.getNodeParameter('lastName', i);
449
+ const email = this.getNodeParameter('email', i);
450
+ const additionalFields = this.getNodeParameter('additionalFields', i, {});
451
+ const body = {
452
+ lastName,
453
+ email,
454
+ ...additionalFields,
455
+ };
456
+ const responseData = await GenericFunctions_1.zohoDeskApiRequest.call(this, 'POST', '/contacts', body, {}, i);
457
+ returnData.push({ json: responseData });
458
+ }
459
+ if (operation === 'update') {
460
+ const contactId = this.getNodeParameter('contactId', i);
461
+ const additionalFields = this.getNodeParameter('additionalFields', i, {});
462
+ const responseData = await GenericFunctions_1.zohoDeskApiRequest.call(this, 'PATCH', `/contacts/${contactId}`, additionalFields, {}, i);
463
+ returnData.push({ json: responseData });
464
+ }
465
+ if (operation === 'search') {
466
+ const searchQuery = this.getNodeParameter('searchQuery', i);
467
+ const limit = this.getNodeParameter('limit', i);
468
+ const responseData = await GenericFunctions_1.zohoDeskApiRequest.call(this, 'GET', '/contacts/search', {}, { limit, searchStr: searchQuery }, i);
469
+ const data = Array.isArray(responseData.data)
470
+ ? responseData.data
471
+ : [responseData];
472
+ for (const row of data) {
473
+ returnData.push({ json: row });
474
+ }
475
+ }
476
+ }
477
+ // ==================== COMMENT OPERATIONS ====================
478
+ if (resource === 'comment') {
479
+ if (operation === 'add') {
480
+ const ticketId = this.getNodeParameter('ticketId', i);
481
+ const content = this.getNodeParameter('content', i);
482
+ const isPublic = this.getNodeParameter('isPublic', i);
483
+ const contentType = this.getNodeParameter('contentType', i);
484
+ const body = {
485
+ content,
486
+ isPublic,
487
+ contentType,
488
+ };
489
+ const responseData = await GenericFunctions_1.zohoDeskApiRequest.call(this, 'POST', `/tickets/${ticketId}/comments`, body, {}, i);
490
+ returnData.push({ json: responseData });
491
+ }
492
+ if (operation === 'getAll') {
493
+ const ticketId = this.getNodeParameter('ticketId', i);
494
+ const limit = this.getNodeParameter('limit', i);
495
+ const responseData = await GenericFunctions_1.zohoDeskApiRequest.call(this, 'GET', `/tickets/${ticketId}/comments`, {}, { limit }, i);
496
+ const data = Array.isArray(responseData.data)
497
+ ? responseData.data
498
+ : [responseData];
499
+ for (const row of data) {
500
+ returnData.push({ json: row });
501
+ }
502
+ }
503
+ }
504
+ }
505
+ catch (error) {
506
+ if (this.continueOnFail()) {
507
+ returnData.push({ json: { error: error.message }, pairedItem: i });
508
+ continue;
509
+ }
510
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), error, { itemIndex: i });
511
+ }
512
+ }
513
+ return [returnData];
514
+ }
515
+ }
516
+ exports.ZohoDesk = ZohoDesk;
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@trg-admin/n8n-nodes-zoho-desk",
3
+ "version": "0.1.0",
4
+ "description": "Community n8n node starter for Zoho Desk using built-in Zoho OAuth2 auth behavior",
5
+ "license": "MIT",
6
+ "type": "commonjs",
7
+ "main": "dist/index.js",
8
+ "scripts": {
9
+ "build": "tsc",
10
+ "lint": "echo 'No linter configured'",
11
+ "typecheck": "tsc --noEmit",
12
+ "test": "node scripts.validate.js",
13
+ "prepublishOnly": "npm run typecheck && npm run build"
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "devDependencies": {
19
+ "typescript": "^5.9.3"
20
+ },
21
+ "n8n": {
22
+ "n8nNodesApiVersion": 1,
23
+ "nodes": [
24
+ "dist/nodes/ZohoDesk/ZohoDesk.node.js"
25
+ ],
26
+ "credentials": [
27
+ "dist/credentials/ZohoCRMDeskOAuth2Api.credentials.js"
28
+ ]
29
+ },
30
+ "peerDependencies": {
31
+ "n8n-workflow": "*"
32
+ },
33
+ "publishConfig": {
34
+ "access": "public"
35
+ }
36
+ }