@friggframework/api-module-zoho-crm 2.0.0-next.2 → 2.0.0-next.4

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 CHANGED
@@ -52,7 +52,7 @@ If you've already done this, skip to the next section.
52
52
  ```shell
53
53
  ZOHO_CRM_CLIENT_ID=your_client_id
54
54
  ZOHO_CRM_CLIENT_SECRET=your_client_secret
55
- ZOHO_CRM_SCOPE=ZohoCRM.users.ALL,ZohoCRM.org.ALL,ZohoCRM.settings.roles.ALL,ZohoCRM.settings.profiles.ALL,ZohoCRM.modules.contacts.ALL,ZohoCRM.modules.leads.ALL,ZohoCRM.modules.accounts.ALL
55
+ ZOHO_CRM_SCOPE=ZohoCRM.users.ALL ZohoCRM.org.ALL ZohoCRM.settings.roles.ALL ZohoCRM.settings.profiles.ALL ZohoCRM.modules.contacts.ALL ZohoCRM.modules.leads.ALL ZohoCRM.modules.accounts.ALL ZohoCRM.modules.calls.ALL
56
56
  REDIRECT_URI=http://localhost:3000/redirect
57
57
  ```
58
58
 
@@ -90,6 +90,21 @@ If you've already done this, skip to the next section.
90
90
  - `getAccount(accountId)` - Get a specific account by ID
91
91
  - `searchAccounts(searchParams)` - Search accounts by phone, criteria, or word
92
92
 
93
+ ### Calls
94
+ - `logCall(callData)` - Log a call in the Zoho CRM Calls module
95
+ - `updateCall(callId, callData)` - Update an existing call record
96
+
97
+ **Call Data Fields:**
98
+ | Field | Type | Required | Description |
99
+ |-------|------|----------|-------------|
100
+ | `Subject` | string | Yes | Call subject/title |
101
+ | `Call_Type` | string | Yes | `Inbound`, `Outbound`, or `Missed` |
102
+ | `Call_Start_Time` | string | Yes | ISO 8601 datetime |
103
+ | `Call_Duration` | string | Yes* | `mm:ss` format (*required for Inbound/Outbound, cannot be zero) |
104
+ | `Description` | string | No | Call notes |
105
+ | `Who_Id` | string | No | Contact/Lead ID to associate |
106
+ | `$se_module` | string | No | Module for Who_Id: `Contacts` or `Leads` |
107
+
93
108
  ## Using the API Module from the Terminal
94
109
 
95
110
  With your `.env` in place, you can test the API from a Node terminal.
package/dist/api.d.ts CHANGED
@@ -1,12 +1,24 @@
1
1
  import { OAuth2Requester } from '@friggframework/core';
2
- import { ZohoConfig, QueryParams, SearchParams, UsersResponse, RolesResponse, ProfilesResponse, ContactsResponse, ContactResponse, LeadsResponse, LeadResponse, AccountsResponse, AccountResponse, TokenResponse, CreateNoteData, NotesResponse, NoteListResponse, NotificationWatchConfig, NotificationResponse, NotificationDetailsResponse } from './types';
2
+ import { ZohoConfig, ZohoLocation, QueryParams, SearchParams, UsersResponse, RolesResponse, ProfilesResponse, ContactsResponse, ContactResponse, LeadsResponse, LeadResponse, AccountsResponse, AccountResponse, TokenResponse, CreateNoteData, NotesResponse, NoteListResponse, NotificationWatchConfig, NotificationResponse, NotificationDetailsResponse, ZohoCallData, CallsResponse } from './types';
3
3
  export declare class Api extends OAuth2Requester {
4
4
  URLs: Record<string, string | ((id: string) => string)>;
5
+ location: ZohoLocation;
6
+ accountsServer: string | null;
5
7
  private static readonly CONTACTS_DEFAULT_FIELDS;
6
8
  private static readonly LEADS_DEFAULT_FIELDS;
7
9
  private static readonly ACCOUNTS_DEFAULT_FIELDS;
8
10
  constructor(params: ZohoConfig);
9
11
  getAuthUri(): string;
12
+ /**
13
+ * Sets the datacenter location and updates URLs accordingly.
14
+ * Note: tokenUri is only updated if accountsServer is not set.
15
+ */
16
+ setLocation(location: ZohoLocation): void;
17
+ /**
18
+ * Sets the accounts server URL for token operations.
19
+ * Call this when accounts-server is provided in OAuth callback.
20
+ */
21
+ setAccountsServer(accountsServer: string): void;
10
22
  getTokenFromCode(code: string): Promise<TokenResponse>;
11
23
  _delete(options: any): Promise<any>;
12
24
  /**
@@ -98,4 +110,19 @@ export declare class Api extends OAuth2Requester {
98
110
  * @see https://www.zoho.com/crm/developer/docs/api/v8/notifications/get-details.html
99
111
  */
100
112
  getNotificationDetails(): Promise<NotificationDetailsResponse>;
113
+ /**
114
+ * Log a call in Zoho CRM Calls module
115
+ * @param callData - Call record data with Subject, Call_Type, Call_Start_Time, Call_Duration
116
+ * @returns Promise<CallsResponse> Created call response
117
+ * @see https://www.zoho.com/crm/developer/docs/api/v8/insert-records.html
118
+ */
119
+ logCall(callData: ZohoCallData): Promise<CallsResponse>;
120
+ /**
121
+ * Update an existing call record in Zoho CRM Calls module
122
+ * @param callId - Zoho Call ID to update
123
+ * @param callData - Partial call data to update (e.g., Description, Subject)
124
+ * @returns Promise<CallsResponse> Updated call response
125
+ * @see https://www.zoho.com/crm/developer/docs/api/v8/update-records.html
126
+ */
127
+ updateCall(callId: string, callData: Partial<ZohoCallData>): Promise<CallsResponse>;
101
128
  }
package/dist/api.js CHANGED
@@ -3,12 +3,35 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Api = void 0;
4
4
  const FormData = require("form-data");
5
5
  const core_1 = require("@friggframework/core");
6
+ /**
7
+ * Zoho datacenter URL configuration
8
+ * @see https://www.zoho.com/crm/developer/docs/api/v8/multi-dc.html
9
+ */
10
+ const LOCATION_CONFIG = {
11
+ us: { accounts: 'https://accounts.zoho.com', api: 'https://www.zohoapis.com' },
12
+ eu: { accounts: 'https://accounts.zoho.eu', api: 'https://www.zohoapis.eu' },
13
+ in: { accounts: 'https://accounts.zoho.in', api: 'https://www.zohoapis.in' },
14
+ au: { accounts: 'https://accounts.zoho.com.au', api: 'https://www.zohoapis.com.au' },
15
+ cn: { accounts: 'https://accounts.zoho.com.cn', api: 'https://www.zohoapis.com.cn' },
16
+ ca: { accounts: 'https://accounts.zoho.ca', api: 'https://www.zohoapis.ca' },
17
+ jp: { accounts: 'https://accounts.zoho.jp', api: 'https://www.zohoapis.jp' },
18
+ sa: { accounts: 'https://accounts.zoho.sa', api: 'https://www.zohoapis.sa' },
19
+ };
20
+ const DEFAULT_LOCATION = 'us';
6
21
  class Api extends core_1.OAuth2Requester {
7
22
  constructor(params) {
8
23
  super(params);
9
- this.baseUrl = 'https://www.zohoapis.com/crm/v8';
10
- this.authorizationUri = encodeURI(`https://accounts.zoho.com/oauth/v2/auth?scope=${this.scope}&client_id=${this.client_id}&redirect_uri=${this.redirect_uri}&response_type=code&access_type=offline`);
11
- this.tokenUri = 'https://accounts.zoho.com/oauth/v2/token';
24
+ this.accountsServer = (0, core_1.get)(params, 'accountsServer', null);
25
+ this.location = (0, core_1.get)(params, 'location', DEFAULT_LOCATION);
26
+ if (!LOCATION_CONFIG[this.location]) {
27
+ this.location = DEFAULT_LOCATION;
28
+ }
29
+ const locationConfig = LOCATION_CONFIG[this.location];
30
+ this.baseUrl = `${locationConfig.api}/crm/v8`;
31
+ this.tokenUri = this.accountsServer
32
+ ? `${this.accountsServer}/oauth/v2/token`
33
+ : `${locationConfig.accounts}/oauth/v2/token`;
34
+ this.authorizationUri = encodeURI(`${locationConfig.accounts}/oauth/v2/auth?scope=${this.scope}&client_id=${this.client_id}&redirect_uri=${this.redirect_uri}&response_type=code&access_type=offline`);
12
35
  this.access_token = (0, core_1.get)(params, 'access_token', null);
13
36
  this.refresh_token = (0, core_1.get)(params, 'refresh_token', null);
14
37
  this.URLs = {
@@ -26,12 +49,38 @@ class Api extends core_1.OAuth2Requester {
26
49
  accounts: '/Accounts',
27
50
  account: (accountId) => `/Accounts/${accountId}`,
28
51
  accountSearch: '/Accounts/search',
52
+ calls: '/Calls',
53
+ call: (callId) => `/Calls/${callId}`,
29
54
  notificationsWatch: '/actions/watch',
30
55
  };
31
56
  }
32
57
  getAuthUri() {
33
58
  return this.authorizationUri;
34
59
  }
60
+ /**
61
+ * Sets the datacenter location and updates URLs accordingly.
62
+ * Note: tokenUri is only updated if accountsServer is not set.
63
+ */
64
+ setLocation(location) {
65
+ if (!LOCATION_CONFIG[location]) {
66
+ throw new Error(`Invalid Zoho location: ${location}. Must be one of: ${Object.keys(LOCATION_CONFIG).join(', ')}`);
67
+ }
68
+ this.location = location;
69
+ const locationConfig = LOCATION_CONFIG[location];
70
+ this.baseUrl = `${locationConfig.api}/crm/v8`;
71
+ if (!this.accountsServer) {
72
+ this.tokenUri = `${locationConfig.accounts}/oauth/v2/token`;
73
+ }
74
+ this.authorizationUri = encodeURI(`${locationConfig.accounts}/oauth/v2/auth?scope=${this.scope}&client_id=${this.client_id}&redirect_uri=${this.redirect_uri}&response_type=code&access_type=offline`);
75
+ }
76
+ /**
77
+ * Sets the accounts server URL for token operations.
78
+ * Call this when accounts-server is provided in OAuth callback.
79
+ */
80
+ setAccountsServer(accountsServer) {
81
+ this.accountsServer = accountsServer;
82
+ this.tokenUri = `${accountsServer}/oauth/v2/token`;
83
+ }
35
84
  async getTokenFromCode(code) {
36
85
  const formData = new FormData();
37
86
  formData.append('grant_type', 'authorization_code');
@@ -201,6 +250,12 @@ class Api extends core_1.OAuth2Requester {
201
250
  });
202
251
  }
203
252
  catch (error) {
253
+ // Zoho returns 204 No Content with empty body when no results found
254
+ // This causes JSON parsing to fail with "Unexpected end of JSON input"
255
+ if (error?.message?.includes('Unexpected end of JSON input') ||
256
+ error?.message?.includes('invalid json response body')) {
257
+ return { data: [], info: undefined };
258
+ }
204
259
  throw error;
205
260
  }
206
261
  }
@@ -247,6 +302,11 @@ class Api extends core_1.OAuth2Requester {
247
302
  });
248
303
  }
249
304
  catch (error) {
305
+ // Zoho returns 204 No Content with empty body when no results found
306
+ if (error?.message?.includes('Unexpected end of JSON input') ||
307
+ error?.message?.includes('invalid json response body')) {
308
+ return { data: [], info: undefined };
309
+ }
250
310
  throw error;
251
311
  }
252
312
  }
@@ -293,6 +353,11 @@ class Api extends core_1.OAuth2Requester {
293
353
  });
294
354
  }
295
355
  catch (error) {
356
+ // Zoho returns 204 No Content with empty body when no results found
357
+ if (error?.message?.includes('Unexpected end of JSON input') ||
358
+ error?.message?.includes('invalid json response body')) {
359
+ return { data: [], info: undefined };
360
+ }
296
361
  throw error;
297
362
  }
298
363
  }
@@ -514,6 +579,72 @@ class Api extends core_1.OAuth2Requester {
514
579
  throw error;
515
580
  }
516
581
  }
582
+ /**
583
+ * Log a call in Zoho CRM Calls module
584
+ * @param callData - Call record data with Subject, Call_Type, Call_Start_Time, Call_Duration
585
+ * @returns Promise<CallsResponse> Created call response
586
+ * @see https://www.zoho.com/crm/developer/docs/api/v8/insert-records.html
587
+ */
588
+ async logCall(callData) {
589
+ if (!callData.Subject) {
590
+ throw new Error('callData.Subject is required');
591
+ }
592
+ if (!callData.Call_Type) {
593
+ throw new Error('callData.Call_Type is required');
594
+ }
595
+ if (!callData.Call_Start_Time) {
596
+ throw new Error('callData.Call_Start_Time is required');
597
+ }
598
+ // Duration is mandatory for Inbound/Outbound calls and cannot be zero
599
+ if ((callData.Call_Type === 'Inbound' || callData.Call_Type === 'Outbound') && !callData.Call_Duration) {
600
+ throw new Error('callData.Call_Duration is required for Inbound/Outbound calls');
601
+ }
602
+ const body = {
603
+ data: [callData]
604
+ };
605
+ try {
606
+ return await this._post({
607
+ url: this.baseUrl + this.URLs.calls,
608
+ body: body,
609
+ headers: {
610
+ 'Content-Type': 'application/json',
611
+ },
612
+ });
613
+ }
614
+ catch (error) {
615
+ throw error;
616
+ }
617
+ }
618
+ /**
619
+ * Update an existing call record in Zoho CRM Calls module
620
+ * @param callId - Zoho Call ID to update
621
+ * @param callData - Partial call data to update (e.g., Description, Subject)
622
+ * @returns Promise<CallsResponse> Updated call response
623
+ * @see https://www.zoho.com/crm/developer/docs/api/v8/update-records.html
624
+ */
625
+ async updateCall(callId, callData) {
626
+ if (!callId) {
627
+ throw new Error('callId is required');
628
+ }
629
+ if (!callData || Object.keys(callData).length === 0) {
630
+ throw new Error('callData must contain at least one field to update');
631
+ }
632
+ const body = {
633
+ data: [callData]
634
+ };
635
+ try {
636
+ return await this._put({
637
+ url: this.baseUrl + this.URLs.call(callId),
638
+ body: body,
639
+ headers: {
640
+ 'Content-Type': 'application/json',
641
+ },
642
+ });
643
+ }
644
+ catch (error) {
645
+ throw error;
646
+ }
647
+ }
517
648
  }
518
649
  exports.Api = Api;
519
650
  Api.CONTACTS_DEFAULT_FIELDS = 'id,First_Name,Last_Name,Email,Phone,Mobile,Account_Name,Company,Owner,Lead_Source,Created_Time,Modified_Time';
@@ -7,7 +7,7 @@ export declare const Definition: {
7
7
  getToken: (api: Api, params: any) => Promise<void>;
8
8
  apiPropertiesToPersist: {
9
9
  credential: string[];
10
- entity: never[];
10
+ entity: string[];
11
11
  };
12
12
  getCredentialDetails: (api: Api, userId: string) => Promise<any>;
13
13
  getEntityDetails: (api: Api, callbackParams: any, tokenResponse: any, userId: string) => Promise<any>;
@@ -38,11 +38,19 @@ exports.Definition = {
38
38
  requiredAuthMethods: {
39
39
  getToken: async function (api, params) {
40
40
  const code = (0, core_1.get)(params, 'code');
41
+ const location = (0, core_1.get)(params, 'location', null);
42
+ const accountsServer = (0, core_1.get)(params, 'accounts-server', null);
43
+ if (location) {
44
+ api.setLocation(location);
45
+ }
46
+ if (accountsServer) {
47
+ api.setAccountsServer(accountsServer);
48
+ }
41
49
  await api.getTokenFromCode(code);
42
50
  },
43
51
  apiPropertiesToPersist: {
44
52
  credential: ['access_token', 'refresh_token'],
45
- entity: [],
53
+ entity: ['location', 'accountsServer'],
46
54
  },
47
55
  getCredentialDetails: async function (api, userId) {
48
56
  const response = await api.listUsers({ type: 'CurrentUser' });
@@ -58,7 +66,9 @@ exports.Definition = {
58
66
  return {
59
67
  identifiers: { externalId: currentUser.id, user: userId },
60
68
  details: {
61
- name: currentUser.email
69
+ name: currentUser.email,
70
+ location: api.location,
71
+ accountsServer: api.accountsServer,
62
72
  },
63
73
  };
64
74
  },
package/dist/types.d.ts CHANGED
@@ -1,3 +1,8 @@
1
+ /**
2
+ * Zoho datacenter locations
3
+ * @see https://www.zoho.com/crm/developer/docs/api/v8/multi-dc.html
4
+ */
5
+ export type ZohoLocation = 'us' | 'eu' | 'in' | 'au' | 'cn' | 'ca' | 'jp' | 'sa';
1
6
  export interface ZohoConfig {
2
7
  client_id: string;
3
8
  client_secret: string;
@@ -5,6 +10,8 @@ export interface ZohoConfig {
5
10
  redirect_uri: string;
6
11
  access_token?: string | null;
7
12
  refresh_token?: string | null;
13
+ location?: ZohoLocation;
14
+ accountsServer?: string | null;
8
15
  }
9
16
  export interface PaginationInfo {
10
17
  per_page: number;
@@ -354,3 +361,47 @@ export interface NotificationCallbackPayload {
354
361
  /** Verification token (if provided during setup) */
355
362
  token?: string;
356
363
  }
364
+ /**
365
+ * Data for creating/updating a call record in Zoho CRM Calls module
366
+ */
367
+ export interface ZohoCallData {
368
+ /** Call subject/title (required) */
369
+ Subject: string;
370
+ /** Call type: Inbound, Outbound, or Missed (required) */
371
+ Call_Type: 'Inbound' | 'Outbound' | 'Missed';
372
+ /** Call start time in ISO 8601 format (required) */
373
+ Call_Start_Time: string;
374
+ /** Call duration in "HH:mm" or "mm:ss" format (required for Inbound/Outbound, cannot be zero) */
375
+ Call_Duration: string;
376
+ /** Call notes/description */
377
+ Description?: string;
378
+ /** Contact or Lead ID to associate the call with */
379
+ Who_Id?: string;
380
+ /** Module name for the Who_Id association: "Contacts" or "Leads" */
381
+ $se_module?: string;
382
+ /** Additional custom fields */
383
+ [key: string]: any;
384
+ }
385
+ /**
386
+ * Response from Calls module operations
387
+ */
388
+ export interface CallsResponse {
389
+ data: Array<{
390
+ code: string;
391
+ details: {
392
+ id: string;
393
+ Created_Time: string;
394
+ Modified_Time: string;
395
+ Created_By?: {
396
+ name: string;
397
+ id: string;
398
+ };
399
+ Modified_By?: {
400
+ name: string;
401
+ id: string;
402
+ };
403
+ };
404
+ message: string;
405
+ status: string;
406
+ }>;
407
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@friggframework/api-module-zoho-crm",
3
- "version": "2.0.0-next.2",
3
+ "version": "2.0.0-next.4",
4
4
  "prettier": "@friggframework/prettier-config",
5
5
  "description": "Zoho CRM API module that lets the Frigg Framework interact with Zoho CRM",
6
6
  "main": "dist/index.js",
@@ -21,8 +21,8 @@
21
21
  "author": "",
22
22
  "license": "MIT",
23
23
  "devDependencies": {
24
- "@friggframework/devtools": "^2.0.0-next.16",
25
- "@friggframework/test": "^2.0.0-next.16",
24
+ "@friggframework/devtools": "^2.0.0-next.68",
25
+ "@friggframework/test": "^2.0.0-next.68",
26
26
  "@types/node": "^20.8.0",
27
27
  "dotenv": "^16.0.3",
28
28
  "eslint": "^8.22.0",
@@ -31,10 +31,10 @@
31
31
  "vitest": "^1.0.0"
32
32
  },
33
33
  "dependencies": {
34
- "@friggframework/core": "^2.0.0-next.16"
34
+ "@friggframework/core": "^2.0.0-next.68"
35
35
  },
36
36
  "publishConfig": {
37
37
  "access": "public"
38
38
  },
39
- "gitHead": "fa3f5ec40cbb9300511849233bddb0c463bfab2a"
39
+ "gitHead": "e067a887c7a7674018dc7e47bcf02a684400de70"
40
40
  }