@slates-integrations/zoho 0.2.0-rc.5
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 +11 -0
- package/docs/SPEC.md +155 -0
- package/logo.png +0 -0
- package/package.json +19 -0
- package/slate.json +17 -0
- package/src/auth.ts +339 -0
- package/src/config.ts +11 -0
- package/src/index.ts +44 -0
- package/src/lib/client.ts +733 -0
- package/src/lib/errors.ts +90 -0
- package/src/lib/urls.ts +77 -0
- package/src/spec.ts +12 -0
- package/src/tools/books-get-invoices.ts +114 -0
- package/src/tools/books-manage-contact.ts +168 -0
- package/src/tools/books-manage-expense.ts +142 -0
- package/src/tools/books-manage-invoice.ts +190 -0
- package/src/tools/crm-get-modules.ts +93 -0
- package/src/tools/crm-get-records.ts +137 -0
- package/src/tools/crm-get-related-records.ts +104 -0
- package/src/tools/crm-manage-record.ts +117 -0
- package/src/tools/crm-search-records.ts +109 -0
- package/src/tools/desk-get-tickets.ts +146 -0
- package/src/tools/desk-manage-contact.ts +125 -0
- package/src/tools/desk-manage-ticket.ts +126 -0
- package/src/tools/index.ts +16 -0
- package/src/tools/people-manage-employee.ts +163 -0
- package/src/tools/projects-get-portals.ts +50 -0
- package/src/tools/projects-manage-project.ts +164 -0
- package/src/tools/projects-manage-task.ts +108 -0
- package/src/triggers/crm-record-events.ts +143 -0
- package/src/triggers/desk-events.ts +121 -0
- package/src/triggers/index.ts +2 -0
- package/tsconfig.json +22 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { SlateTool } from 'slates';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { spec } from '../spec';
|
|
4
|
+
import { ZohoProjectsClient } from '../lib/client';
|
|
5
|
+
import type { Datacenter } from '../lib/urls';
|
|
6
|
+
|
|
7
|
+
export let projectsGetPortals = SlateTool.create(spec, {
|
|
8
|
+
name: 'Projects Get Portals',
|
|
9
|
+
key: 'projects_get_portals',
|
|
10
|
+
description:
|
|
11
|
+
'List Zoho Projects portals available to the authenticated user so project and task tools can be called with the correct portalId.',
|
|
12
|
+
tags: {
|
|
13
|
+
readOnly: true
|
|
14
|
+
}
|
|
15
|
+
})
|
|
16
|
+
.input(z.object({}))
|
|
17
|
+
.output(
|
|
18
|
+
z.object({
|
|
19
|
+
portals: z
|
|
20
|
+
.array(
|
|
21
|
+
z.object({
|
|
22
|
+
portalId: z.string(),
|
|
23
|
+
name: z.string().optional(),
|
|
24
|
+
role: z.string().optional()
|
|
25
|
+
})
|
|
26
|
+
)
|
|
27
|
+
.describe('Available Zoho Projects portals')
|
|
28
|
+
})
|
|
29
|
+
)
|
|
30
|
+
.handleInvocation(async ctx => {
|
|
31
|
+
let dc = (ctx.auth.datacenter || ctx.config.datacenter || 'us') as Datacenter;
|
|
32
|
+
let result = await ZohoProjectsClient.listPortals(ctx.auth.token, dc);
|
|
33
|
+
let portals = (result?.portals || [])
|
|
34
|
+
.map((portal: any) => ({
|
|
35
|
+
portalId: portal.id || portal.portal_id || portal.portalId,
|
|
36
|
+
name: portal.name || portal.portal_name,
|
|
37
|
+
role: portal.role
|
|
38
|
+
}))
|
|
39
|
+
.filter((portal: { portalId?: unknown }) => portal.portalId)
|
|
40
|
+
.map((portal: { portalId: unknown; name?: string; role?: string }) => ({
|
|
41
|
+
...portal,
|
|
42
|
+
portalId: String(portal.portalId)
|
|
43
|
+
}));
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
output: { portals },
|
|
47
|
+
message: `Found **${portals.length}** Zoho Projects portals.`
|
|
48
|
+
};
|
|
49
|
+
})
|
|
50
|
+
.build();
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { SlateTool } from 'slates';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { spec } from '../spec';
|
|
4
|
+
import { ZohoProjectsClient } from '../lib/client';
|
|
5
|
+
import type { Datacenter } from '../lib/urls';
|
|
6
|
+
import { zohoServiceError } from '../lib/errors';
|
|
7
|
+
|
|
8
|
+
export let projectsManageProject = SlateTool.create(spec, {
|
|
9
|
+
name: 'Projects Manage Project',
|
|
10
|
+
key: 'projects_manage_project',
|
|
11
|
+
description: `Create, update, delete, or list projects in Zoho Projects. Manage project names, descriptions, status, start/end dates, and owners. Also supports listing tasks and milestones within a project.`,
|
|
12
|
+
instructions: [
|
|
13
|
+
'The portalId is required for all Zoho Projects operations.',
|
|
14
|
+
'Use action "list_tasks" or "list_milestones" with a projectId to view items within a project.'
|
|
15
|
+
],
|
|
16
|
+
tags: {
|
|
17
|
+
destructive: true
|
|
18
|
+
}
|
|
19
|
+
})
|
|
20
|
+
.input(
|
|
21
|
+
z.object({
|
|
22
|
+
portalId: z.string().describe('Zoho Projects portal ID'),
|
|
23
|
+
action: z
|
|
24
|
+
.enum(['list', 'get', 'create', 'update', 'delete', 'list_tasks', 'list_milestones'])
|
|
25
|
+
.describe('Operation to perform'),
|
|
26
|
+
projectId: z
|
|
27
|
+
.string()
|
|
28
|
+
.optional()
|
|
29
|
+
.describe(
|
|
30
|
+
'Project ID (required for get, update, delete, list_tasks, list_milestones)'
|
|
31
|
+
),
|
|
32
|
+
name: z.string().optional().describe('Project name (required for create)'),
|
|
33
|
+
description: z.string().optional().describe('Project description'),
|
|
34
|
+
status: z.string().optional().describe('Project status (e.g., "active", "archived")'),
|
|
35
|
+
startDate: z.string().optional().describe('Start date (MM-dd-yyyy)'),
|
|
36
|
+
endDate: z.string().optional().describe('End date (MM-dd-yyyy)'),
|
|
37
|
+
ownerId: z.string().optional().describe('Project owner user ID'),
|
|
38
|
+
index: z.number().optional().describe('Start index for pagination'),
|
|
39
|
+
range: z.number().optional().describe('Number of records to return')
|
|
40
|
+
})
|
|
41
|
+
)
|
|
42
|
+
.output(
|
|
43
|
+
z.object({
|
|
44
|
+
projects: z.array(z.record(z.string(), z.any())).optional().describe('List of projects'),
|
|
45
|
+
project: z.record(z.string(), z.any()).optional().describe('Single project'),
|
|
46
|
+
tasks: z
|
|
47
|
+
.array(z.record(z.string(), z.any()))
|
|
48
|
+
.optional()
|
|
49
|
+
.describe('Tasks within a project'),
|
|
50
|
+
milestones: z
|
|
51
|
+
.array(z.record(z.string(), z.any()))
|
|
52
|
+
.optional()
|
|
53
|
+
.describe('Milestones within a project'),
|
|
54
|
+
deleted: z.boolean().optional()
|
|
55
|
+
})
|
|
56
|
+
)
|
|
57
|
+
.handleInvocation(async ctx => {
|
|
58
|
+
let dc = (ctx.auth.datacenter || ctx.config.datacenter || 'us') as Datacenter;
|
|
59
|
+
let client = new ZohoProjectsClient({
|
|
60
|
+
token: ctx.auth.token,
|
|
61
|
+
datacenter: dc,
|
|
62
|
+
portalId: ctx.input.portalId
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
if (ctx.input.action === 'list') {
|
|
66
|
+
let result = await client.listProjects({
|
|
67
|
+
index: ctx.input.index,
|
|
68
|
+
range: ctx.input.range,
|
|
69
|
+
status: ctx.input.status
|
|
70
|
+
});
|
|
71
|
+
let projects = result?.projects || [];
|
|
72
|
+
return {
|
|
73
|
+
output: { projects },
|
|
74
|
+
message: `Retrieved **${projects.length}** projects.`
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (ctx.input.action === 'get') {
|
|
79
|
+
if (!ctx.input.projectId) throw zohoServiceError('projectId is required for get');
|
|
80
|
+
let result = await client.getProject(ctx.input.projectId);
|
|
81
|
+
let project = result?.projects?.[0] || result;
|
|
82
|
+
return {
|
|
83
|
+
output: { project },
|
|
84
|
+
message: `Fetched project **${project?.name || ctx.input.projectId}**.`
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (ctx.input.action === 'create') {
|
|
89
|
+
if (!ctx.input.name) throw zohoServiceError('name is required for create');
|
|
90
|
+
let data: Record<string, any> = {};
|
|
91
|
+
if (ctx.input.name) data.name = ctx.input.name;
|
|
92
|
+
if (ctx.input.description) data.description = ctx.input.description;
|
|
93
|
+
if (ctx.input.status) data.status = ctx.input.status;
|
|
94
|
+
if (ctx.input.startDate) data.start_date = ctx.input.startDate;
|
|
95
|
+
if (ctx.input.endDate) data.end_date = ctx.input.endDate;
|
|
96
|
+
if (ctx.input.ownerId) data.owner = ctx.input.ownerId;
|
|
97
|
+
|
|
98
|
+
let result = await client.createProject(data);
|
|
99
|
+
let project = result?.projects?.[0] || result;
|
|
100
|
+
return {
|
|
101
|
+
output: { project },
|
|
102
|
+
message: `Created project **${project?.name}**.`
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (ctx.input.action === 'update') {
|
|
107
|
+
if (!ctx.input.projectId) throw zohoServiceError('projectId is required for update');
|
|
108
|
+
let data: Record<string, any> = {};
|
|
109
|
+
if (ctx.input.name) data.name = ctx.input.name;
|
|
110
|
+
if (ctx.input.description) data.description = ctx.input.description;
|
|
111
|
+
if (ctx.input.status) data.status = ctx.input.status;
|
|
112
|
+
if (ctx.input.startDate) data.start_date = ctx.input.startDate;
|
|
113
|
+
if (ctx.input.endDate) data.end_date = ctx.input.endDate;
|
|
114
|
+
if (ctx.input.ownerId) data.owner = ctx.input.ownerId;
|
|
115
|
+
|
|
116
|
+
let result = await client.updateProject(ctx.input.projectId, data);
|
|
117
|
+
let project = result?.projects?.[0] || result;
|
|
118
|
+
return {
|
|
119
|
+
output: { project },
|
|
120
|
+
message: `Updated project **${ctx.input.projectId}**.`
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (ctx.input.action === 'delete') {
|
|
125
|
+
if (!ctx.input.projectId) throw zohoServiceError('projectId is required for delete');
|
|
126
|
+
await client.deleteProject(ctx.input.projectId);
|
|
127
|
+
return {
|
|
128
|
+
output: { deleted: true },
|
|
129
|
+
message: `Deleted project **${ctx.input.projectId}**.`
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (ctx.input.action === 'list_tasks') {
|
|
134
|
+
if (!ctx.input.projectId) throw zohoServiceError('projectId is required for list_tasks');
|
|
135
|
+
let result = await client.listTasks(ctx.input.projectId, {
|
|
136
|
+
index: ctx.input.index,
|
|
137
|
+
range: ctx.input.range,
|
|
138
|
+
status: ctx.input.status
|
|
139
|
+
});
|
|
140
|
+
let tasks = result?.tasks || [];
|
|
141
|
+
return {
|
|
142
|
+
output: { tasks },
|
|
143
|
+
message: `Retrieved **${tasks.length}** tasks from project **${ctx.input.projectId}**.`
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (ctx.input.action === 'list_milestones') {
|
|
148
|
+
if (!ctx.input.projectId)
|
|
149
|
+
throw zohoServiceError('projectId is required for list_milestones');
|
|
150
|
+
let result = await client.listMilestones(ctx.input.projectId, {
|
|
151
|
+
index: ctx.input.index,
|
|
152
|
+
range: ctx.input.range,
|
|
153
|
+
status: ctx.input.status
|
|
154
|
+
});
|
|
155
|
+
let milestones = result?.milestones || [];
|
|
156
|
+
return {
|
|
157
|
+
output: { milestones },
|
|
158
|
+
message: `Retrieved **${milestones.length}** milestones from project **${ctx.input.projectId}**.`
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
throw zohoServiceError('Invalid Projects project action.');
|
|
163
|
+
})
|
|
164
|
+
.build();
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { SlateTool } from 'slates';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { spec } from '../spec';
|
|
4
|
+
import { ZohoProjectsClient } from '../lib/client';
|
|
5
|
+
import type { Datacenter } from '../lib/urls';
|
|
6
|
+
import { zohoServiceError } from '../lib/errors';
|
|
7
|
+
|
|
8
|
+
export let projectsManageTask = SlateTool.create(spec, {
|
|
9
|
+
name: 'Projects Manage Task',
|
|
10
|
+
key: 'projects_manage_task',
|
|
11
|
+
description: `Create, update, delete, or retrieve tasks within a Zoho Projects project. Set task names, descriptions, owners, priority, start/end dates, and status.`,
|
|
12
|
+
instructions: ['Both portalId and projectId are required.', 'For create, name is required.'],
|
|
13
|
+
tags: {
|
|
14
|
+
destructive: true
|
|
15
|
+
}
|
|
16
|
+
})
|
|
17
|
+
.input(
|
|
18
|
+
z.object({
|
|
19
|
+
portalId: z.string().describe('Zoho Projects portal ID'),
|
|
20
|
+
projectId: z.string().describe('Project ID containing the task'),
|
|
21
|
+
action: z.enum(['get', 'create', 'update', 'delete']).describe('Operation to perform'),
|
|
22
|
+
taskId: z.string().optional().describe('Task ID (required for get, update, delete)'),
|
|
23
|
+
name: z.string().optional().describe('Task name (required for create)'),
|
|
24
|
+
description: z.string().optional().describe('Task description'),
|
|
25
|
+
startDate: z.string().optional().describe('Start date (MM-dd-yyyy)'),
|
|
26
|
+
endDate: z.string().optional().describe('End date (MM-dd-yyyy)'),
|
|
27
|
+
priority: z
|
|
28
|
+
.string()
|
|
29
|
+
.optional()
|
|
30
|
+
.describe('Task priority (e.g., "None", "Low", "Medium", "High")'),
|
|
31
|
+
status: z.string().optional().describe('Task status'),
|
|
32
|
+
owners: z
|
|
33
|
+
.string()
|
|
34
|
+
.optional()
|
|
35
|
+
.describe('Comma-separated user IDs to assign as task owners'),
|
|
36
|
+
percentComplete: z.number().optional().describe('Completion percentage (0-100)')
|
|
37
|
+
})
|
|
38
|
+
)
|
|
39
|
+
.output(
|
|
40
|
+
z.object({
|
|
41
|
+
task: z.record(z.string(), z.any()).optional().describe('Task record'),
|
|
42
|
+
deleted: z.boolean().optional()
|
|
43
|
+
})
|
|
44
|
+
)
|
|
45
|
+
.handleInvocation(async ctx => {
|
|
46
|
+
let dc = (ctx.auth.datacenter || ctx.config.datacenter || 'us') as Datacenter;
|
|
47
|
+
let client = new ZohoProjectsClient({
|
|
48
|
+
token: ctx.auth.token,
|
|
49
|
+
datacenter: dc,
|
|
50
|
+
portalId: ctx.input.portalId
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
if (ctx.input.action === 'get') {
|
|
54
|
+
if (!ctx.input.taskId) throw zohoServiceError('taskId is required for get');
|
|
55
|
+
let result = await client.getTask(ctx.input.projectId, ctx.input.taskId);
|
|
56
|
+
let task = result?.tasks?.[0] || result;
|
|
57
|
+
return {
|
|
58
|
+
output: { task },
|
|
59
|
+
message: `Fetched task **${task?.name || ctx.input.taskId}**.`
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let buildData = () => {
|
|
64
|
+
let data: Record<string, any> = {};
|
|
65
|
+
if (ctx.input.name) data.name = ctx.input.name;
|
|
66
|
+
if (ctx.input.description) data.description = ctx.input.description;
|
|
67
|
+
if (ctx.input.startDate) data.start_date = ctx.input.startDate;
|
|
68
|
+
if (ctx.input.endDate) data.end_date = ctx.input.endDate;
|
|
69
|
+
if (ctx.input.priority) data.priority = ctx.input.priority;
|
|
70
|
+
if (ctx.input.status) data.status_name = ctx.input.status;
|
|
71
|
+
if (ctx.input.owners) data.persons = ctx.input.owners;
|
|
72
|
+
if (ctx.input.percentComplete !== undefined)
|
|
73
|
+
data.percent_complete = ctx.input.percentComplete;
|
|
74
|
+
return data;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
if (ctx.input.action === 'create') {
|
|
78
|
+
if (!ctx.input.name) throw zohoServiceError('name is required for create');
|
|
79
|
+
let result = await client.createTask(ctx.input.projectId, buildData());
|
|
80
|
+
let task = result?.tasks?.[0] || result;
|
|
81
|
+
return {
|
|
82
|
+
output: { task },
|
|
83
|
+
message: `Created task **${task?.name}** in project **${ctx.input.projectId}**.`
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (ctx.input.action === 'update') {
|
|
88
|
+
if (!ctx.input.taskId) throw zohoServiceError('taskId is required for update');
|
|
89
|
+
let result = await client.updateTask(ctx.input.projectId, ctx.input.taskId, buildData());
|
|
90
|
+
let task = result?.tasks?.[0] || result;
|
|
91
|
+
return {
|
|
92
|
+
output: { task },
|
|
93
|
+
message: `Updated task **${ctx.input.taskId}**.`
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (ctx.input.action === 'delete') {
|
|
98
|
+
if (!ctx.input.taskId) throw zohoServiceError('taskId is required for delete');
|
|
99
|
+
await client.deleteTask(ctx.input.projectId, ctx.input.taskId);
|
|
100
|
+
return {
|
|
101
|
+
output: { deleted: true },
|
|
102
|
+
message: `Deleted task **${ctx.input.taskId}**.`
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
throw zohoServiceError('Invalid Projects task action.');
|
|
107
|
+
})
|
|
108
|
+
.build();
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { SlateTrigger } from 'slates';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { spec } from '../spec';
|
|
4
|
+
import { ZohoCrmClient } from '../lib/client';
|
|
5
|
+
import type { Datacenter } from '../lib/urls';
|
|
6
|
+
|
|
7
|
+
export let crmRecordEvents = SlateTrigger.create(spec, {
|
|
8
|
+
name: 'CRM Record Events',
|
|
9
|
+
key: 'crm_record_events',
|
|
10
|
+
description:
|
|
11
|
+
'Triggers when CRM records are created, updated, or deleted in Zoho CRM. Uses the CRM Notification/Watch API to receive real-time push notifications.'
|
|
12
|
+
})
|
|
13
|
+
.input(
|
|
14
|
+
z.object({
|
|
15
|
+
module: z.string().describe('CRM module name (e.g., Leads, Contacts, Deals)'),
|
|
16
|
+
operation: z.string().describe('Operation type (insert, update, delete)'),
|
|
17
|
+
recordIds: z.array(z.string()).describe('Affected record IDs'),
|
|
18
|
+
channelId: z.string().describe('Notification channel ID'),
|
|
19
|
+
token: z.string().optional().describe('Verification token'),
|
|
20
|
+
serverTime: z.string().optional().describe('Server timestamp of the event'),
|
|
21
|
+
affectedFields: z.record(z.string(), z.any()).optional().describe('Changed field values')
|
|
22
|
+
})
|
|
23
|
+
)
|
|
24
|
+
.output(
|
|
25
|
+
z.object({
|
|
26
|
+
module: z.string().describe('CRM module name'),
|
|
27
|
+
recordIds: z.array(z.string()).describe('Affected record IDs'),
|
|
28
|
+
operation: z.string().describe('Operation performed (insert, update, delete)'),
|
|
29
|
+
channelId: z.string().describe('Notification channel ID'),
|
|
30
|
+
serverTime: z.string().optional().describe('Server timestamp'),
|
|
31
|
+
affectedFields: z.record(z.string(), z.any()).optional().describe('Changed field values')
|
|
32
|
+
})
|
|
33
|
+
)
|
|
34
|
+
.webhook({
|
|
35
|
+
autoRegisterWebhook: async ctx => {
|
|
36
|
+
let dc = (ctx.auth.datacenter || ctx.config.datacenter || 'us') as Datacenter;
|
|
37
|
+
let client = new ZohoCrmClient({ token: ctx.auth.token, datacenter: dc });
|
|
38
|
+
|
|
39
|
+
let channelId = `${Date.now()}`;
|
|
40
|
+
|
|
41
|
+
// Subscribe to all events for major modules
|
|
42
|
+
let modules = ['Leads', 'Contacts', 'Accounts', 'Deals', 'Tasks', 'Events', 'Calls'];
|
|
43
|
+
let events = modules.flatMap(m => [`${m}.all`]);
|
|
44
|
+
|
|
45
|
+
// Channel expiry max is 1 week
|
|
46
|
+
let expiry = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString();
|
|
47
|
+
|
|
48
|
+
let result = await client.enableNotifications([
|
|
49
|
+
{
|
|
50
|
+
channelId,
|
|
51
|
+
events,
|
|
52
|
+
notifyUrl: ctx.input.webhookBaseUrl,
|
|
53
|
+
channelExpiry: expiry,
|
|
54
|
+
returnAffectedFieldValues: true
|
|
55
|
+
}
|
|
56
|
+
]);
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
registrationDetails: {
|
|
60
|
+
channelId,
|
|
61
|
+
events,
|
|
62
|
+
result
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
autoUnregisterWebhook: async ctx => {
|
|
68
|
+
let dc = (ctx.auth.datacenter || ctx.config.datacenter || 'us') as Datacenter;
|
|
69
|
+
let client = new ZohoCrmClient({ token: ctx.auth.token, datacenter: dc });
|
|
70
|
+
|
|
71
|
+
let channelId = ctx.input.registrationDetails?.channelId;
|
|
72
|
+
if (channelId) {
|
|
73
|
+
await client.disableNotifications([channelId]);
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
handleRequest: async ctx => {
|
|
78
|
+
let body: any = await ctx.request.json();
|
|
79
|
+
|
|
80
|
+
// Zoho CRM sends notifications as a JSON body with module, operation, ids, etc.
|
|
81
|
+
let inputs: Array<{
|
|
82
|
+
module: string;
|
|
83
|
+
operation: string;
|
|
84
|
+
recordIds: string[];
|
|
85
|
+
channelId: string;
|
|
86
|
+
token?: string;
|
|
87
|
+
serverTime?: string;
|
|
88
|
+
affectedFields?: Record<string, any>;
|
|
89
|
+
}> = [];
|
|
90
|
+
|
|
91
|
+
// The body can be a single notification or contain query_map
|
|
92
|
+
if (body?.module) {
|
|
93
|
+
inputs.push({
|
|
94
|
+
module: body.module,
|
|
95
|
+
operation: body.operation || 'unknown',
|
|
96
|
+
recordIds: body.ids || [],
|
|
97
|
+
channelId: body.channel_id?.toString() || '',
|
|
98
|
+
token: body.token,
|
|
99
|
+
serverTime: body.server_time,
|
|
100
|
+
affectedFields: body.affected_fields
|
|
101
|
+
});
|
|
102
|
+
} else if (body?.query_map) {
|
|
103
|
+
// Some CRM webhooks send via query_map format
|
|
104
|
+
let qm =
|
|
105
|
+
typeof body.query_map === 'string' ? JSON.parse(body.query_map) : body.query_map;
|
|
106
|
+
inputs.push({
|
|
107
|
+
module: qm.module || '',
|
|
108
|
+
operation: qm.operation || 'unknown',
|
|
109
|
+
recordIds: qm.ids || [],
|
|
110
|
+
channelId: qm.channel_id?.toString() || '',
|
|
111
|
+
token: qm.token,
|
|
112
|
+
serverTime: qm.server_time,
|
|
113
|
+
affectedFields: qm.affected_fields
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return { inputs };
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
handleEvent: async ctx => {
|
|
121
|
+
let operationMap: Record<string, string> = {
|
|
122
|
+
insert: 'created',
|
|
123
|
+
update: 'updated',
|
|
124
|
+
delete: 'deleted'
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
let eventType = operationMap[ctx.input.operation] || ctx.input.operation;
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
type: `crm.${ctx.input.module.toLowerCase()}.${eventType}`,
|
|
131
|
+
id: `${ctx.input.channelId}-${ctx.input.module}-${ctx.input.operation}-${ctx.input.recordIds.join(',')}`,
|
|
132
|
+
output: {
|
|
133
|
+
module: ctx.input.module,
|
|
134
|
+
recordIds: ctx.input.recordIds,
|
|
135
|
+
operation: ctx.input.operation,
|
|
136
|
+
channelId: ctx.input.channelId,
|
|
137
|
+
serverTime: ctx.input.serverTime,
|
|
138
|
+
affectedFields: ctx.input.affectedFields
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
})
|
|
143
|
+
.build();
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { SlateTrigger } from 'slates';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { spec } from '../spec';
|
|
4
|
+
import { ZohoDeskClient } from '../lib/client';
|
|
5
|
+
import type { Datacenter } from '../lib/urls';
|
|
6
|
+
import { zohoServiceError } from '../lib/errors';
|
|
7
|
+
|
|
8
|
+
export let deskEvents = SlateTrigger.create(spec, {
|
|
9
|
+
name: 'Desk Events',
|
|
10
|
+
key: 'desk_events',
|
|
11
|
+
description:
|
|
12
|
+
'Triggers when tickets, contacts, accounts, tasks, or other resources are created, updated, or deleted in Zoho Desk. Uses Zoho Desk webhook subscriptions for real-time notifications.'
|
|
13
|
+
})
|
|
14
|
+
.input(
|
|
15
|
+
z.object({
|
|
16
|
+
eventType: z
|
|
17
|
+
.string()
|
|
18
|
+
.describe('Desk event type (e.g., Ticket_Add, Ticket_Update, Contact_Add)'),
|
|
19
|
+
resourceId: z.string().describe('ID of the affected resource'),
|
|
20
|
+
payload: z.record(z.string(), z.any()).describe('Full event payload from Zoho Desk')
|
|
21
|
+
})
|
|
22
|
+
)
|
|
23
|
+
.output(
|
|
24
|
+
z.object({
|
|
25
|
+
eventType: z.string().describe('Event type (e.g., Ticket_Add, Ticket_Update)'),
|
|
26
|
+
resourceType: z.string().describe('Resource type (e.g., ticket, contact, account)'),
|
|
27
|
+
resourceId: z.string().describe('ID of the affected resource'),
|
|
28
|
+
ticketNumber: z.string().optional().describe('Ticket number (for ticket events)'),
|
|
29
|
+
subject: z.string().optional().describe('Ticket subject (for ticket events)'),
|
|
30
|
+
status: z.string().optional().describe('Current status'),
|
|
31
|
+
departmentId: z.string().optional().describe('Department ID'),
|
|
32
|
+
payload: z.record(z.string(), z.any()).describe('Full event payload')
|
|
33
|
+
})
|
|
34
|
+
)
|
|
35
|
+
.webhook({
|
|
36
|
+
autoRegisterWebhook: async ctx => {
|
|
37
|
+
let dc = (ctx.auth.datacenter || ctx.config.datacenter || 'us') as Datacenter;
|
|
38
|
+
// We need an orgId - we'll try to extract from existing config or require it
|
|
39
|
+
// For auto-registration, the orgId must be available; we'll use a default approach
|
|
40
|
+
// The orgId should be provided as part of trigger configuration
|
|
41
|
+
// For now, we register the webhook if we can determine the orgId
|
|
42
|
+
|
|
43
|
+
// Since we don't have orgId from config, we'll try to retrieve from Desk API
|
|
44
|
+
// This is a limitation - the user should configure orgId in trigger setup
|
|
45
|
+
// We'll need to accept that the trigger may need manual configuration
|
|
46
|
+
|
|
47
|
+
// Note: This auto-registration requires orgId. In practice, the user should
|
|
48
|
+
// provide it. We'll throw an informative error if not available.
|
|
49
|
+
throw zohoServiceError(
|
|
50
|
+
'Zoho Desk webhooks require an organization ID (orgId). Please configure the webhook manually in Zoho Desk settings, pointing to the webhook URL.'
|
|
51
|
+
);
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
handleRequest: async ctx => {
|
|
55
|
+
let body: any;
|
|
56
|
+
try {
|
|
57
|
+
body = await ctx.request.json();
|
|
58
|
+
} catch {
|
|
59
|
+
return { inputs: [] };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (!body || typeof body !== 'object') return { inputs: [] };
|
|
63
|
+
|
|
64
|
+
// Zoho Desk webhook payloads include event info and the affected resource
|
|
65
|
+
let eventType = body.eventType || body.event_type || '';
|
|
66
|
+
let payload = body.payload || body;
|
|
67
|
+
|
|
68
|
+
// Extract resource ID based on event type
|
|
69
|
+
let resourceId =
|
|
70
|
+
payload.id ||
|
|
71
|
+
payload.ticketId ||
|
|
72
|
+
payload.contactId ||
|
|
73
|
+
payload.accountId ||
|
|
74
|
+
payload.taskId ||
|
|
75
|
+
'';
|
|
76
|
+
|
|
77
|
+
let inputs = [
|
|
78
|
+
{
|
|
79
|
+
eventType: eventType.toString(),
|
|
80
|
+
resourceId: resourceId.toString(),
|
|
81
|
+
payload
|
|
82
|
+
}
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
return { inputs };
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
handleEvent: async ctx => {
|
|
89
|
+
let payload = ctx.input.payload;
|
|
90
|
+
let eventType = ctx.input.eventType;
|
|
91
|
+
|
|
92
|
+
// Determine resource type from event type
|
|
93
|
+
let resourceType = 'unknown';
|
|
94
|
+
if (eventType.startsWith('Ticket')) resourceType = 'ticket';
|
|
95
|
+
else if (eventType.startsWith('Contact')) resourceType = 'contact';
|
|
96
|
+
else if (eventType.startsWith('Account')) resourceType = 'account';
|
|
97
|
+
else if (eventType.startsWith('Task')) resourceType = 'task';
|
|
98
|
+
else if (eventType.startsWith('Call')) resourceType = 'call';
|
|
99
|
+
else if (eventType.startsWith('Event')) resourceType = 'event';
|
|
100
|
+
else if (eventType.startsWith('IM')) resourceType = 'im';
|
|
101
|
+
else if (eventType.startsWith('Department')) resourceType = 'department';
|
|
102
|
+
|
|
103
|
+
let normalizedType = eventType.toLowerCase().replace(/_/g, '.');
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
type: `desk.${normalizedType}`,
|
|
107
|
+
id: `${eventType}-${ctx.input.resourceId}-${Date.now()}`,
|
|
108
|
+
output: {
|
|
109
|
+
eventType,
|
|
110
|
+
resourceType,
|
|
111
|
+
resourceId: ctx.input.resourceId,
|
|
112
|
+
ticketNumber: payload.ticketNumber,
|
|
113
|
+
subject: payload.subject,
|
|
114
|
+
status: payload.status || payload.statusType,
|
|
115
|
+
departmentId: payload.departmentId,
|
|
116
|
+
payload
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
})
|
|
121
|
+
.build();
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"types": ["node"],
|
|
4
|
+
"lib": ["ESNext"],
|
|
5
|
+
"target": "ESNext",
|
|
6
|
+
"module": "Preserve",
|
|
7
|
+
"moduleDetection": "force",
|
|
8
|
+
"jsx": "react-jsx",
|
|
9
|
+
"allowJs": true,
|
|
10
|
+
"moduleResolution": "bundler",
|
|
11
|
+
"noEmit": true,
|
|
12
|
+
"strict": true,
|
|
13
|
+
"skipLibCheck": true,
|
|
14
|
+
"noFallthroughCasesInSwitch": true,
|
|
15
|
+
"noUncheckedIndexedAccess": true,
|
|
16
|
+
"noImplicitOverride": true,
|
|
17
|
+
"noUnusedLocals": false,
|
|
18
|
+
"noUnusedParameters": false,
|
|
19
|
+
"noPropertyAccessFromIndexSignature": false
|
|
20
|
+
},
|
|
21
|
+
"include": ["src"]
|
|
22
|
+
}
|