@rawdash/connector-zendesk 0.0.1

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/dist/index.js ADDED
@@ -0,0 +1,696 @@
1
+ // ../../connector-shared/dist/index.js
2
+ var HTTP_CLIENT_VERSION = "0.0.0";
3
+ var DEFAULT_USER_AGENT = `rawdash-connector/${HTTP_CLIENT_VERSION} (+https://rawdash.dev)`;
4
+ function connectorUserAgent(connectorId) {
5
+ return `rawdash-connector-${connectorId}/${HTTP_CLIENT_VERSION} (+https://rawdash.dev)`;
6
+ }
7
+ function parseEpoch(value, unit) {
8
+ if (value === null || value === void 0) {
9
+ return null;
10
+ }
11
+ if (unit === "iso") {
12
+ if (typeof value !== "string") {
13
+ return null;
14
+ }
15
+ const ms = new Date(value).getTime();
16
+ return Number.isFinite(ms) ? ms : null;
17
+ }
18
+ if (typeof value === "string" && value.trim() === "") {
19
+ return null;
20
+ }
21
+ const n = typeof value === "number" ? value : Number(value);
22
+ if (!Number.isFinite(n)) {
23
+ return null;
24
+ }
25
+ const result = unit === "s" ? n * 1e3 : n;
26
+ return Number.isFinite(result) ? result : null;
27
+ }
28
+
29
+ // src/zendesk.ts
30
+ import {
31
+ BaseConnector,
32
+ defineConfigFields,
33
+ defineConnectorDoc,
34
+ defineResources,
35
+ makeChunkedCursorGuard,
36
+ paginateChunked,
37
+ schemasFromResources,
38
+ selectActivePhases
39
+ } from "@rawdash/core";
40
+ import { z } from "zod";
41
+ var configFields = defineConfigFields(
42
+ z.object({
43
+ subdomain: z.string().trim().min(1).regex(
44
+ /^[a-z0-9][a-z0-9-]*$/i,
45
+ 'Use the subdomain only (e.g. "acme" for acme.zendesk.com), without the protocol or path.'
46
+ ).meta({
47
+ label: "Account subdomain",
48
+ description: 'Your Zendesk account subdomain, the "acme" in acme.zendesk.com.',
49
+ placeholder: "acme"
50
+ }),
51
+ email: z.string().trim().email().meta({
52
+ label: "Agent email",
53
+ description: "Email address of an agent (or admin) on the Zendesk account; paired with the API token for Basic auth.",
54
+ placeholder: "agent@acme.com"
55
+ }),
56
+ apiToken: z.object({ $secret: z.string() }).meta({
57
+ label: "API token",
58
+ description: "Zendesk API token. Create one in Admin Center -> Apps and integrations -> Zendesk API -> Settings -> Add API token.",
59
+ placeholder: "aB1c2D3...",
60
+ secret: true
61
+ }),
62
+ resources: z.array(
63
+ z.enum([
64
+ "users",
65
+ "groups",
66
+ "tickets",
67
+ "ticket_events",
68
+ "satisfaction_ratings"
69
+ ])
70
+ ).nonempty().optional().meta({
71
+ label: "Resources",
72
+ description: "Which Zendesk resources to sync. Omit to sync all of them. The API token only needs read scopes for the resources listed here."
73
+ })
74
+ })
75
+ );
76
+ var doc = defineConnectorDoc({
77
+ displayName: "Zendesk",
78
+ category: "support",
79
+ brandColor: "#03363D",
80
+ tagline: "Sync tickets, ticket state-change events, satisfaction ratings, users, and groups from Zendesk Support for queue depth, response time, and CSAT analytics.",
81
+ vendor: {
82
+ name: "Zendesk",
83
+ apiDocs: "https://developer.zendesk.com/api-reference/ticketing/introduction/",
84
+ website: "https://www.zendesk.com"
85
+ },
86
+ auth: {
87
+ summary: "HTTP Basic auth using an agent (or admin) email address paired with a Zendesk API token. The token must belong to an account with read access to tickets, users, and groups.",
88
+ setup: [
89
+ "Open Admin Center -> Apps and integrations -> Zendesk API.",
90
+ "On the Settings tab, enable Token access if it is not already on.",
91
+ "Click Add API token, give it a label, and copy the generated token value (you cannot view it again).",
92
+ 'Store the token as a secret and reference it from config as `apiToken: secret("ZENDESK_API_TOKEN")`, alongside the agent email and your account subdomain (the "acme" in acme.zendesk.com).'
93
+ ]
94
+ },
95
+ rateLimit: "Zendesk Support API enforces per-account quotas (default ~700 requests/minute on Professional plans, higher on Enterprise) and signals throttling via 429 with a Retry-After header; the shared HTTP client honors Retry-After on backoff.",
96
+ limitations: [
97
+ "Ticket comment bodies and per-event audit transcripts are not synced.",
98
+ "Zendesk Chat, Talk (voice), and Sell are separate product lines and are out of scope.",
99
+ "Ticket state-change events are derived from each ticket\u2019s timestamps (created, updated, solved); full audit-event history is not synced."
100
+ ]
101
+ });
102
+ var zendeskCredentials = {
103
+ email: {
104
+ description: "Zendesk agent email",
105
+ auth: "required"
106
+ },
107
+ apiToken: {
108
+ description: "Zendesk API token",
109
+ auth: "required"
110
+ }
111
+ };
112
+ var PHASE_ORDER = [
113
+ "users",
114
+ "groups",
115
+ "tickets",
116
+ "ticket_events",
117
+ "satisfaction_ratings"
118
+ ];
119
+ var isZendeskSyncCursor = makeChunkedCursorGuard(PHASE_ORDER);
120
+ var PAGE_SIZE = 100;
121
+ var INCREMENTAL_PAGE_SIZE = 1e3;
122
+ var USER_ENTITY = "zendesk_user";
123
+ var GROUP_ENTITY = "zendesk_group";
124
+ var TICKET_ENTITY = "zendesk_ticket";
125
+ var TICKET_STATE_EVENT = "zendesk_ticket_state_change";
126
+ var SATISFACTION_RATING_ENTITY = "zendesk_satisfaction_rating";
127
+ var idNumber = z.number();
128
+ var isoString = z.string();
129
+ var userSchema = z.object({
130
+ id: idNumber,
131
+ name: z.string().nullish(),
132
+ email: z.string().nullish(),
133
+ role: z.string().nullish(),
134
+ active: z.boolean().nullish(),
135
+ suspended: z.boolean().nullish(),
136
+ default_group_id: z.number().nullish(),
137
+ created_at: isoString.nullish(),
138
+ updated_at: isoString.nullish()
139
+ });
140
+ var groupSchema = z.object({
141
+ id: idNumber,
142
+ name: z.string().nullish(),
143
+ default: z.boolean().nullish(),
144
+ deleted: z.boolean().nullish(),
145
+ created_at: isoString.nullish(),
146
+ updated_at: isoString.nullish()
147
+ });
148
+ var ticketSchema = z.object({
149
+ id: idNumber,
150
+ subject: z.string().nullish(),
151
+ status: z.string().nullish(),
152
+ priority: z.string().nullish(),
153
+ type: z.string().nullish(),
154
+ channel: z.string().nullish(),
155
+ assignee_id: z.number().nullish(),
156
+ requester_id: z.number().nullish(),
157
+ submitter_id: z.number().nullish(),
158
+ group_id: z.number().nullish(),
159
+ organization_id: z.number().nullish(),
160
+ tags: z.array(z.string()).nullish(),
161
+ via: z.object({ channel: z.string().nullish() }).nullish(),
162
+ satisfaction_rating: z.object({ score: z.string().nullish() }).nullish(),
163
+ created_at: isoString.nullish(),
164
+ updated_at: isoString.nullish()
165
+ });
166
+ var satisfactionRatingSchema = z.object({
167
+ id: idNumber,
168
+ score: z.string().nullish(),
169
+ ticket_id: z.number().nullish(),
170
+ assignee_id: z.number().nullish(),
171
+ requester_id: z.number().nullish(),
172
+ group_id: z.number().nullish(),
173
+ comment: z.string().nullish(),
174
+ created_at: isoString.nullish(),
175
+ updated_at: isoString.nullish()
176
+ });
177
+ function numericIdOrNull(value) {
178
+ if (value === null || value === void 0) {
179
+ return null;
180
+ }
181
+ return String(value);
182
+ }
183
+ function isoToMs(value) {
184
+ return parseEpoch(value ?? null, "iso");
185
+ }
186
+ function isoToMsOrZero(value) {
187
+ return isoToMs(value) ?? 0;
188
+ }
189
+ function viaChannel(ticket) {
190
+ return ticket.via?.channel ?? ticket.channel ?? null;
191
+ }
192
+ function csatScore(ticket) {
193
+ return ticket.satisfaction_rating?.score ?? null;
194
+ }
195
+ var zendeskResources = defineResources({
196
+ [USER_ENTITY]: {
197
+ shape: "entity",
198
+ description: "Zendesk users (agents, admins, and end-users) with role and activity flags.",
199
+ endpoint: "GET /api/v2/users.json",
200
+ fields: [
201
+ { name: "name", description: "User display name." },
202
+ { name: "email", description: "User email address." },
203
+ {
204
+ name: "role",
205
+ description: "User role (end-user, agent, or admin)."
206
+ },
207
+ { name: "active", description: "Whether the user is active." },
208
+ { name: "suspended", description: "Whether the user is suspended." },
209
+ {
210
+ name: "defaultGroupId",
211
+ description: "Default group the user belongs to (agents only)."
212
+ },
213
+ {
214
+ name: "createdAt",
215
+ description: "When the user was created (Unix ms)."
216
+ }
217
+ ],
218
+ responses: { users: z.array(userSchema) }
219
+ },
220
+ [GROUP_ENTITY]: {
221
+ shape: "entity",
222
+ description: "Agent groups used to route tickets.",
223
+ endpoint: "GET /api/v2/groups.json",
224
+ fields: [
225
+ { name: "name", description: "Group name." },
226
+ {
227
+ name: "isDefault",
228
+ description: "Whether this is the account default group."
229
+ },
230
+ { name: "deleted", description: "Whether the group is soft-deleted." },
231
+ {
232
+ name: "createdAt",
233
+ description: "When the group was created (Unix ms)."
234
+ }
235
+ ],
236
+ responses: { groups: z.array(groupSchema) }
237
+ },
238
+ [TICKET_ENTITY]: {
239
+ shape: "entity",
240
+ description: "Tickets with status, priority, assignment, channel, and tags.",
241
+ endpoint: "GET /api/v2/incremental/tickets/cursor.json",
242
+ fields: [
243
+ { name: "subject", description: "Ticket subject line." },
244
+ {
245
+ name: "status",
246
+ description: "Ticket status (new, open, pending, hold, solved, closed)."
247
+ },
248
+ {
249
+ name: "priority",
250
+ description: "Ticket priority (low, normal, high, urgent)."
251
+ },
252
+ { name: "type", description: "Ticket type (question, incident, etc.)." },
253
+ {
254
+ name: "channel",
255
+ description: "Channel the ticket was created from (email, web, etc.)."
256
+ },
257
+ {
258
+ name: "assigneeId",
259
+ description: "Assigned agent id (null if unassigned)."
260
+ },
261
+ { name: "requesterId", description: "Requester (end-user) id." },
262
+ {
263
+ name: "groupId",
264
+ description: "Group the ticket is routed to (null if unrouted)."
265
+ },
266
+ {
267
+ name: "organizationId",
268
+ description: "Organization id (null if none)."
269
+ },
270
+ {
271
+ name: "tags",
272
+ description: "Flat list of tags applied to the ticket."
273
+ },
274
+ {
275
+ name: "satisfactionScore",
276
+ description: "Per-ticket CSAT score from the satisfaction_rating block (offered, good, bad, unoffered)."
277
+ },
278
+ {
279
+ name: "createdAt",
280
+ description: "When the ticket was created (Unix ms)."
281
+ }
282
+ ],
283
+ responses: { tickets: z.array(ticketSchema) }
284
+ },
285
+ [TICKET_STATE_EVENT]: {
286
+ shape: "event",
287
+ description: "Ticket state-change events (created / solved) derived from each ticket.",
288
+ endpoint: "GET /api/v2/incremental/tickets/cursor.json",
289
+ notes: "Derived from each ticket\u2019s timestamps; the scope is cleared and rewritten on every sync.",
290
+ fields: [
291
+ {
292
+ name: "ticketId",
293
+ description: "The ticket the event belongs to."
294
+ },
295
+ {
296
+ name: "transition",
297
+ description: "created or solved."
298
+ },
299
+ {
300
+ name: "status",
301
+ description: "Ticket status at sync time."
302
+ },
303
+ {
304
+ name: "priority",
305
+ description: "Ticket priority at sync time."
306
+ },
307
+ {
308
+ name: "assigneeId",
309
+ description: "Assigned agent id at sync time (null if unassigned)."
310
+ },
311
+ {
312
+ name: "groupId",
313
+ description: "Group id at sync time (null if unrouted)."
314
+ },
315
+ { name: "channel", description: "Channel the ticket was created from." }
316
+ ],
317
+ responses: { ticket_events: z.array(ticketSchema) }
318
+ },
319
+ [SATISFACTION_RATING_ENTITY]: {
320
+ shape: "entity",
321
+ description: "Per-ticket customer satisfaction (CSAT) ratings with score and free-text comment.",
322
+ endpoint: "GET /api/v2/satisfaction_ratings.json",
323
+ fields: [
324
+ { name: "score", description: "Rating score (good, bad, offered)." },
325
+ { name: "ticketId", description: "The ticket the rating is for." },
326
+ {
327
+ name: "assigneeId",
328
+ description: "Agent assigned at the time of rating."
329
+ },
330
+ { name: "requesterId", description: "Requester (end-user) id." },
331
+ { name: "groupId", description: "Group id at the time of rating." },
332
+ {
333
+ name: "hasComment",
334
+ description: "Whether a free-text comment is set."
335
+ },
336
+ {
337
+ name: "createdAt",
338
+ description: "When the rating was submitted (Unix ms)."
339
+ }
340
+ ],
341
+ responses: { satisfaction_ratings: z.array(satisfactionRatingSchema) }
342
+ }
343
+ });
344
+ var id = "zendesk";
345
+ var ZendeskConnector = class _ZendeskConnector extends BaseConnector {
346
+ static id = id;
347
+ static resources = zendeskResources;
348
+ static schemas = schemasFromResources(zendeskResources);
349
+ static create(input, ctx) {
350
+ const parsed = configFields.parse(input);
351
+ return new _ZendeskConnector(
352
+ {
353
+ subdomain: parsed.subdomain,
354
+ resources: parsed.resources
355
+ },
356
+ { email: parsed.email, apiToken: parsed.apiToken },
357
+ ctx
358
+ );
359
+ }
360
+ id = id;
361
+ credentials = zendeskCredentials;
362
+ get baseUrl() {
363
+ return `https://${this.settings.subdomain}.zendesk.com`;
364
+ }
365
+ buildHeaders() {
366
+ return {
367
+ Authorization: encodeBasicAuth(
368
+ `${this.creds.email}/token`,
369
+ this.creds.apiToken
370
+ ),
371
+ Accept: "application/json",
372
+ "User-Agent": connectorUserAgent("zendesk")
373
+ };
374
+ }
375
+ apiGet(url, resource, signal) {
376
+ return this.get(url, {
377
+ resource,
378
+ headers: this.buildHeaders(),
379
+ signal
380
+ });
381
+ }
382
+ // -------------------------------------------------------------------------
383
+ // users — GET /api/v2/users.json (cursor pagination)
384
+ // -------------------------------------------------------------------------
385
+ buildUserListUrl(cursor) {
386
+ const params = new URLSearchParams({ "page[size]": String(PAGE_SIZE) });
387
+ if (cursor) {
388
+ params.set("page[after]", cursor);
389
+ }
390
+ return `${this.baseUrl}/api/v2/users.json?${params.toString()}`;
391
+ }
392
+ async fetchUsers(page, signal) {
393
+ const res = await this.apiGet(
394
+ this.buildUserListUrl(page),
395
+ "users",
396
+ signal
397
+ );
398
+ return {
399
+ items: res.body.users ?? [],
400
+ next: res.body.meta?.has_more ? res.body.meta?.after_cursor ?? null : null
401
+ };
402
+ }
403
+ async writeUsers(storage, items) {
404
+ for (const user of items) {
405
+ await storage.entity({
406
+ type: USER_ENTITY,
407
+ id: String(user.id),
408
+ attributes: {
409
+ name: user.name ?? null,
410
+ email: user.email ?? null,
411
+ role: user.role ?? null,
412
+ active: user.active ?? null,
413
+ suspended: user.suspended ?? null,
414
+ defaultGroupId: numericIdOrNull(user.default_group_id),
415
+ createdAt: isoToMs(user.created_at)
416
+ },
417
+ updated_at: isoToMsOrZero(user.updated_at ?? user.created_at)
418
+ });
419
+ }
420
+ }
421
+ // -------------------------------------------------------------------------
422
+ // groups — GET /api/v2/groups.json (cursor pagination)
423
+ // -------------------------------------------------------------------------
424
+ buildGroupListUrl(cursor) {
425
+ const params = new URLSearchParams({ "page[size]": String(PAGE_SIZE) });
426
+ if (cursor) {
427
+ params.set("page[after]", cursor);
428
+ }
429
+ return `${this.baseUrl}/api/v2/groups.json?${params.toString()}`;
430
+ }
431
+ async fetchGroups(page, signal) {
432
+ const res = await this.apiGet(
433
+ this.buildGroupListUrl(page),
434
+ "groups",
435
+ signal
436
+ );
437
+ return {
438
+ items: res.body.groups ?? [],
439
+ next: res.body.meta?.has_more ? res.body.meta?.after_cursor ?? null : null
440
+ };
441
+ }
442
+ async writeGroups(storage, items) {
443
+ for (const group of items) {
444
+ await storage.entity({
445
+ type: GROUP_ENTITY,
446
+ id: String(group.id),
447
+ attributes: {
448
+ name: group.name ?? null,
449
+ isDefault: group.default ?? null,
450
+ deleted: group.deleted ?? null,
451
+ createdAt: isoToMs(group.created_at)
452
+ },
453
+ updated_at: isoToMsOrZero(group.updated_at ?? group.created_at)
454
+ });
455
+ }
456
+ }
457
+ // -------------------------------------------------------------------------
458
+ // tickets — GET /api/v2/incremental/tickets/cursor.json (cursor pagination)
459
+ // -------------------------------------------------------------------------
460
+ buildIncrementalTicketsUrl(cursor, options) {
461
+ const params = new URLSearchParams({
462
+ per_page: String(INCREMENTAL_PAGE_SIZE)
463
+ });
464
+ if (cursor) {
465
+ params.set("cursor", cursor);
466
+ } else {
467
+ params.set("start_time", String(sinceUnixSec(options) ?? 0));
468
+ }
469
+ return `${this.baseUrl}/api/v2/incremental/tickets/cursor.json?${params.toString()}`;
470
+ }
471
+ async fetchTickets(page, resource, options, signal) {
472
+ const fetchOptions = resource === "ticket_events" ? { ...options, since: void 0 } : options;
473
+ const res = await this.apiGet(
474
+ this.buildIncrementalTicketsUrl(page, fetchOptions),
475
+ resource,
476
+ signal
477
+ );
478
+ const next = res.body.end_of_stream ? null : res.body.after_cursor ?? null;
479
+ return { items: res.body.tickets ?? [], next };
480
+ }
481
+ async writeTickets(storage, items) {
482
+ for (const ticket of items) {
483
+ const attributes = {
484
+ subject: ticket.subject ?? null,
485
+ status: ticket.status ?? null,
486
+ priority: ticket.priority ?? null,
487
+ type: ticket.type ?? null,
488
+ channel: viaChannel(ticket),
489
+ assigneeId: numericIdOrNull(ticket.assignee_id),
490
+ requesterId: numericIdOrNull(ticket.requester_id),
491
+ submitterId: numericIdOrNull(ticket.submitter_id),
492
+ groupId: numericIdOrNull(ticket.group_id),
493
+ organizationId: numericIdOrNull(ticket.organization_id),
494
+ tags: ticket.tags ?? [],
495
+ satisfactionScore: csatScore(ticket),
496
+ createdAt: isoToMs(ticket.created_at)
497
+ };
498
+ await storage.entity({
499
+ type: TICKET_ENTITY,
500
+ id: String(ticket.id),
501
+ attributes,
502
+ updated_at: isoToMsOrZero(ticket.updated_at ?? ticket.created_at)
503
+ });
504
+ }
505
+ }
506
+ // -------------------------------------------------------------------------
507
+ // ticket_events — derived from the same /incremental/tickets payload
508
+ // -------------------------------------------------------------------------
509
+ async writeTicketEvents(storage, items) {
510
+ for (const ticket of items) {
511
+ const baseAttrs = {
512
+ ticketId: String(ticket.id),
513
+ status: ticket.status ?? null,
514
+ priority: ticket.priority ?? null,
515
+ assigneeId: numericIdOrNull(ticket.assignee_id),
516
+ groupId: numericIdOrNull(ticket.group_id),
517
+ channel: viaChannel(ticket)
518
+ };
519
+ const createdMs = isoToMs(ticket.created_at);
520
+ if (createdMs !== null) {
521
+ await storage.event({
522
+ name: TICKET_STATE_EVENT,
523
+ start_ts: createdMs,
524
+ end_ts: null,
525
+ attributes: { ...baseAttrs, transition: "created" }
526
+ });
527
+ }
528
+ if ((ticket.status === "solved" || ticket.status === "closed") && ticket.updated_at) {
529
+ const solvedMs = isoToMs(ticket.updated_at);
530
+ if (solvedMs !== null) {
531
+ await storage.event({
532
+ name: TICKET_STATE_EVENT,
533
+ start_ts: solvedMs,
534
+ end_ts: null,
535
+ attributes: { ...baseAttrs, transition: "solved" }
536
+ });
537
+ }
538
+ }
539
+ }
540
+ }
541
+ // -------------------------------------------------------------------------
542
+ // satisfaction_ratings — GET /api/v2/satisfaction_ratings.json
543
+ // -------------------------------------------------------------------------
544
+ buildSatisfactionRatingsUrl(cursor, options) {
545
+ const params = new URLSearchParams({ "page[size]": String(PAGE_SIZE) });
546
+ if (cursor) {
547
+ params.set("page[after]", cursor);
548
+ } else {
549
+ const startTime = sinceUnixSec(options);
550
+ if (startTime !== null) {
551
+ params.set("start_time", String(startTime));
552
+ }
553
+ }
554
+ return `${this.baseUrl}/api/v2/satisfaction_ratings.json?${params.toString()}`;
555
+ }
556
+ async fetchSatisfactionRatings(page, options, signal) {
557
+ const res = await this.apiGet(
558
+ this.buildSatisfactionRatingsUrl(page, options),
559
+ "satisfaction_ratings",
560
+ signal
561
+ );
562
+ return {
563
+ items: res.body.satisfaction_ratings ?? [],
564
+ next: res.body.meta?.has_more ? res.body.meta?.after_cursor ?? null : null
565
+ };
566
+ }
567
+ async writeSatisfactionRatings(storage, items) {
568
+ for (const rating of items) {
569
+ await storage.entity({
570
+ type: SATISFACTION_RATING_ENTITY,
571
+ id: String(rating.id),
572
+ attributes: {
573
+ score: rating.score ?? null,
574
+ ticketId: numericIdOrNull(rating.ticket_id),
575
+ assigneeId: numericIdOrNull(rating.assignee_id),
576
+ requesterId: numericIdOrNull(rating.requester_id),
577
+ groupId: numericIdOrNull(rating.group_id),
578
+ hasComment: typeof rating.comment === "string" && rating.comment !== "",
579
+ createdAt: isoToMs(rating.created_at)
580
+ },
581
+ updated_at: isoToMsOrZero(rating.updated_at ?? rating.created_at)
582
+ });
583
+ }
584
+ }
585
+ // -------------------------------------------------------------------------
586
+ // Scope clearing (idempotency)
587
+ // -------------------------------------------------------------------------
588
+ async clearScopeOnFirstPage(storage, phase, isFull) {
589
+ if (phase === "ticket_events") {
590
+ await storage.events([], { names: [TICKET_STATE_EVENT] });
591
+ return;
592
+ }
593
+ if (!isFull) {
594
+ return;
595
+ }
596
+ const entityType = ENTITY_TYPE_BY_PHASE[phase];
597
+ if (entityType) {
598
+ await storage.entities([], { types: [entityType] });
599
+ }
600
+ }
601
+ async writePhase(storage, phase, items) {
602
+ switch (phase) {
603
+ case "users":
604
+ await this.writeUsers(storage, items);
605
+ return;
606
+ case "groups":
607
+ await this.writeGroups(storage, items);
608
+ return;
609
+ case "tickets":
610
+ await this.writeTickets(storage, items);
611
+ return;
612
+ case "ticket_events":
613
+ await this.writeTicketEvents(storage, items);
614
+ return;
615
+ case "satisfaction_ratings":
616
+ await this.writeSatisfactionRatings(
617
+ storage,
618
+ items
619
+ );
620
+ return;
621
+ }
622
+ }
623
+ async sync(options, storage, signal) {
624
+ const cursor = isZendeskSyncCursor(options.cursor) ? options.cursor : void 0;
625
+ const isFull = options.mode === "full";
626
+ const phases = selectActivePhases(
627
+ (r) => r,
628
+ PHASE_ORDER,
629
+ this.settings.resources
630
+ );
631
+ return paginateChunked({
632
+ phases,
633
+ cursor,
634
+ signal,
635
+ logger: this.logger,
636
+ fetchPage: async (phase, page, sig) => {
637
+ switch (phase) {
638
+ case "users":
639
+ return this.fetchUsers(page, sig);
640
+ case "groups":
641
+ return this.fetchGroups(page, sig);
642
+ case "tickets":
643
+ case "ticket_events":
644
+ return this.fetchTickets(page, phase, options, sig);
645
+ case "satisfaction_ratings":
646
+ return this.fetchSatisfactionRatings(page, options, sig);
647
+ }
648
+ },
649
+ writeBatch: async (phase, items, page) => {
650
+ if (page === null) {
651
+ await this.clearScopeOnFirstPage(storage, phase, isFull);
652
+ }
653
+ await this.writePhase(storage, phase, items);
654
+ }
655
+ });
656
+ }
657
+ };
658
+ var ENTITY_TYPE_BY_PHASE = {
659
+ users: USER_ENTITY,
660
+ groups: GROUP_ENTITY,
661
+ tickets: TICKET_ENTITY,
662
+ satisfaction_ratings: SATISFACTION_RATING_ENTITY
663
+ };
664
+ function sinceUnixSec(options) {
665
+ if (!options.since) {
666
+ return null;
667
+ }
668
+ const ms = new Date(options.since).getTime();
669
+ if (!Number.isFinite(ms)) {
670
+ return null;
671
+ }
672
+ return Math.floor(ms / 1e3);
673
+ }
674
+ function encodeBasicAuth(username, secret) {
675
+ const raw = `${username}:${secret}`;
676
+ if (typeof btoa === "function") {
677
+ return `Basic ${btoa(raw)}`;
678
+ }
679
+ const bufferCtor = globalThis.Buffer;
680
+ if (bufferCtor) {
681
+ return `Basic ${bufferCtor.from(raw).toString("base64")}`;
682
+ }
683
+ throw new Error("No base64 encoder available in this runtime");
684
+ }
685
+
686
+ // src/index.ts
687
+ var index_default = ZendeskConnector;
688
+ export {
689
+ ZendeskConnector,
690
+ configFields,
691
+ index_default as default,
692
+ doc,
693
+ id,
694
+ zendeskResources as resources
695
+ };
696
+ //# sourceMappingURL=index.js.map