@rawdash/connector-calendly 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,567 @@
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/calendly.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
+ apiToken: z.object({ $secret: z.string() }).meta({
44
+ label: "Personal access token",
45
+ description: "Calendly personal access token with read access. Create one under Integrations -> API & Webhooks -> Personal Access Tokens.",
46
+ placeholder: "eyJraWQiOi...",
47
+ secret: true
48
+ }),
49
+ organizationUri: z.string().url().regex(
50
+ /\/organizations\/[^/]+$/,
51
+ "Organization URI looks like https://api.calendly.com/organizations/AAAAAAAAAAAAAAAA."
52
+ ).meta({
53
+ label: "Organization URI",
54
+ description: "The full Calendly organization URI to sync. Fetch it from GET https://api.calendly.com/users/me (current_organization).",
55
+ placeholder: "https://api.calendly.com/organizations/AAAAAAAAAAAAAAAA"
56
+ }),
57
+ lookbackDays: z.number().int().positive().optional().meta({
58
+ label: "Lookback days",
59
+ description: "How many days of past scheduled events to sync. Defaults to 90. Events are synced over a rolling window and rewritten on every sync.",
60
+ placeholder: "90"
61
+ }),
62
+ resources: z.array(z.enum(["event_types", "scheduled_events", "cancellations"])).nonempty().optional().meta({
63
+ label: "Resources",
64
+ description: "Which Calendly resources to sync. Omit to sync all of them. 'cancellations' is derived from the scheduled-events scan, so enabling it without 'scheduled_events' still walks scheduled events but only writes cancellation events."
65
+ })
66
+ })
67
+ );
68
+ var doc = defineConnectorDoc({
69
+ displayName: "Calendly",
70
+ category: "sales",
71
+ brandColor: "#006BFF",
72
+ tagline: "Sync Calendly event types, scheduled events, and cancellations for booking, no-show, and meeting-mix dashboards.",
73
+ vendor: {
74
+ name: "Calendly",
75
+ domain: "calendly.com",
76
+ apiDocs: "https://developer.calendly.com/api-docs",
77
+ website: "https://calendly.com"
78
+ },
79
+ auth: {
80
+ summary: "Authenticates with a personal access token sent as a Bearer credential. The token inherits the permissions of the account that created it, so use an organization admin to sync organization-wide scheduled events.",
81
+ setup: [
82
+ "Sign in to Calendly and open Integrations -> API & Webhooks -> Personal Access Tokens.",
83
+ "Create a token, give it a name, and copy the value (it is shown only once).",
84
+ 'Store the token as a secret and reference it from the connector config as `apiToken: secret("CALENDLY_API_TOKEN")`.',
85
+ "Fetch your organization URI from GET https://api.calendly.com/users/me (the `current_organization` field) and set it as `organizationUri`."
86
+ ]
87
+ },
88
+ rateLimit: "Calendly enforces per-token rate limits and returns HTTP 429 with a Retry-After header when exceeded; the shared HTTP client honors it with backoff. Enabling scheduled events fetches invitees per event, which increases request volume on large windows.",
89
+ limitations: [
90
+ "Scheduled events and cancellations are synced over a rolling window (lookbackDays back through 60 days ahead) and rewritten on every sync, so bookings outside the window age out.",
91
+ "No-show and invitee email are read from the per-event invitees endpoint, so they are only populated when the scheduled_events resource is enabled.",
92
+ "Cancellation events are derived from canceled scheduled events within the window; a cancellation of an event whose start time has aged out of the window is not retained."
93
+ ]
94
+ });
95
+ var calendlyCredentials = {
96
+ apiToken: {
97
+ description: "Calendly personal access token",
98
+ auth: "required"
99
+ }
100
+ };
101
+ var PHASE_ORDER = ["event_types", "scheduled_events"];
102
+ var isCalendlySyncCursor = makeChunkedCursorGuard(PHASE_ORDER);
103
+ var iso = z.string();
104
+ var uri = z.string().min(1);
105
+ var paginationSchema = z.object({
106
+ next_page_token: z.string().nullable().optional()
107
+ });
108
+ var eventTypesResponseSchema = z.object({
109
+ collection: z.array(
110
+ z.object({
111
+ uri,
112
+ name: z.string().nullable().optional(),
113
+ active: z.boolean().nullable().optional(),
114
+ slug: z.string().nullable().optional(),
115
+ kind: z.string().nullable().optional(),
116
+ pooling_type: z.string().nullable().optional(),
117
+ duration: z.number().nullable().optional(),
118
+ color: z.string().nullable().optional(),
119
+ scheduling_url: z.string().nullable().optional(),
120
+ created_at: iso.nullable().optional(),
121
+ updated_at: iso,
122
+ profile: z.object({
123
+ type: z.string().nullable().optional(),
124
+ name: z.string().nullable().optional(),
125
+ owner: z.string().nullable().optional()
126
+ }).nullable().optional()
127
+ })
128
+ ),
129
+ pagination: paginationSchema
130
+ });
131
+ var cancellationSchema = z.object({
132
+ canceled_by: z.string().nullable().optional(),
133
+ canceler_type: z.string().nullable().optional(),
134
+ reason: z.string().nullable().optional(),
135
+ created_at: iso.nullable().optional()
136
+ }).nullable().optional();
137
+ var scheduledEventsResponseSchema = z.object({
138
+ collection: z.array(
139
+ z.object({
140
+ uri,
141
+ name: z.string().nullable().optional(),
142
+ status: z.string().nullable().optional(),
143
+ start_time: iso,
144
+ end_time: iso.nullable().optional(),
145
+ created_at: iso.nullable().optional(),
146
+ updated_at: iso.nullable().optional(),
147
+ event_type: z.string().nullable().optional(),
148
+ location: z.object({ type: z.string().nullable().optional() }).nullable().optional(),
149
+ invitees_counter: z.object({
150
+ total: z.number().nullable().optional(),
151
+ active: z.number().nullable().optional(),
152
+ limit: z.number().nullable().optional()
153
+ }).nullable().optional(),
154
+ event_memberships: z.array(
155
+ z.object({
156
+ user: z.string().nullable().optional(),
157
+ user_email: z.string().nullable().optional(),
158
+ user_name: z.string().nullable().optional()
159
+ })
160
+ ).nullable().optional(),
161
+ cancellation: cancellationSchema
162
+ })
163
+ ),
164
+ pagination: paginationSchema
165
+ });
166
+ var inviteesResponseSchema = z.object({
167
+ collection: z.array(
168
+ z.object({
169
+ uri,
170
+ email: z.string().nullable().optional(),
171
+ name: z.string().nullable().optional(),
172
+ status: z.string().nullable().optional(),
173
+ created_at: iso.nullable().optional(),
174
+ no_show: z.object({
175
+ uri: z.string().nullable().optional(),
176
+ created_at: iso.nullable().optional()
177
+ }).nullable().optional()
178
+ })
179
+ ),
180
+ pagination: paginationSchema
181
+ });
182
+ var calendlyResources = defineResources({
183
+ calendly_event_type: {
184
+ shape: "entity",
185
+ filterable: [
186
+ { field: "active", ops: ["eq"] },
187
+ { field: "kind", ops: ["eq"] }
188
+ ],
189
+ description: "Event types (meeting templates) in the organization with name, active state, duration, kind, and owner.",
190
+ endpoint: "GET /event_types?organization={organizationUri}",
191
+ fields: [
192
+ { name: "name", description: "Event type name." },
193
+ { name: "active", description: "Whether the event type is active." },
194
+ { name: "slug", description: "URL slug of the event type." },
195
+ { name: "kind", description: "Event type kind (e.g. solo, group)." },
196
+ {
197
+ name: "poolingType",
198
+ description: "Pooling type for round-robin/collective types."
199
+ },
200
+ { name: "durationMinutes", description: "Default duration in minutes." },
201
+ { name: "color", description: "Display color." },
202
+ { name: "schedulingUrl", description: "Public scheduling URL." },
203
+ { name: "ownerUri", description: "URI of the owning user or team." },
204
+ { name: "ownerType", description: "Owner type (user or team)." },
205
+ { name: "createdAt", description: "Creation time (epoch ms)." }
206
+ ],
207
+ responses: { event_types: eventTypesResponseSchema }
208
+ },
209
+ calendly_scheduled_event: {
210
+ shape: "event",
211
+ filterable: [],
212
+ description: "Scheduled events (bookings) timestamped at their start time, carrying status, event type, host, invitee, and no-show info.",
213
+ endpoint: "GET /scheduled_events?organization={organizationUri}",
214
+ notes: "start_ts is the meeting start time, end_ts the meeting end time. Invitee email and no-show counts come from the per-event invitees endpoint. Timestamps are Unix epoch milliseconds.",
215
+ fields: [
216
+ { name: "uri", description: "Calendly scheduled event URI." },
217
+ { name: "name", description: "Event name." },
218
+ { name: "eventTypeUri", description: "URI of the booked event type." },
219
+ {
220
+ name: "status",
221
+ description: "Event status (active or canceled)."
222
+ },
223
+ { name: "canceled", description: "Whether the event was canceled." },
224
+ {
225
+ name: "inviteeEmail",
226
+ description: "Email of the primary invitee, if available."
227
+ },
228
+ { name: "inviteeName", description: "Name of the primary invitee." },
229
+ { name: "inviteeCount", description: "Total invitees on the event." },
230
+ {
231
+ name: "activeInviteeCount",
232
+ description: "Invitees that have not canceled."
233
+ },
234
+ {
235
+ name: "noShowCount",
236
+ description: "Number of invitees marked as no-show."
237
+ },
238
+ { name: "hostEmail", description: "Email of the first host." },
239
+ { name: "hostName", description: "Name of the first host." },
240
+ { name: "locationType", description: "Meeting location type." },
241
+ { name: "createdAt", description: "Booking creation time (epoch ms)." },
242
+ { name: "updatedAt", description: "Last update time (epoch ms)." }
243
+ ],
244
+ responses: {
245
+ scheduled_events: scheduledEventsResponseSchema,
246
+ event_invitees: inviteesResponseSchema
247
+ }
248
+ },
249
+ calendly_cancellation: {
250
+ shape: "event",
251
+ filterable: [],
252
+ description: "Cancellation events derived from canceled scheduled events, timestamped at cancellation time with reason and canceler.",
253
+ endpoint: "GET /scheduled_events?organization={organizationUri}",
254
+ notes: "Derived from the scheduled-events scan; one event per canceled scheduled event. start_ts is the cancellation time. Timestamps are Unix epoch milliseconds.",
255
+ fields: [
256
+ { name: "eventUri", description: "URI of the canceled scheduled event." },
257
+ { name: "eventName", description: "Name of the canceled event." },
258
+ { name: "eventTypeUri", description: "URI of the booked event type." },
259
+ { name: "reason", description: "Cancellation reason, if provided." },
260
+ { name: "canceledBy", description: "Name of who canceled." },
261
+ {
262
+ name: "cancelerType",
263
+ description: "Whether the host or invitee canceled."
264
+ },
265
+ {
266
+ name: "eventStartTime",
267
+ description: "Start time of the canceled event (epoch ms)."
268
+ }
269
+ ],
270
+ responses: { scheduled_event_cancellations: scheduledEventsResponseSchema }
271
+ }
272
+ });
273
+ var API_BASE = "https://api.calendly.com";
274
+ var MS_PER_DAY = 24 * 60 * 60 * 1e3;
275
+ var DEFAULT_LOOKBACK_DAYS = 90;
276
+ var FORWARD_HORIZON_DAYS = 60;
277
+ var EVENT_TYPES_PAGE_SIZE = 100;
278
+ var SCHEDULED_EVENTS_PAGE_SIZE = 100;
279
+ var INVITEES_PAGE_SIZE = 100;
280
+ function getStartWindow(lookbackDays, now = Date.now()) {
281
+ return {
282
+ minStartTime: new Date(now - lookbackDays * MS_PER_DAY).toISOString(),
283
+ maxStartTime: new Date(
284
+ now + FORWARD_HORIZON_DAYS * MS_PER_DAY
285
+ ).toISOString()
286
+ };
287
+ }
288
+ function eventUuid(uri2) {
289
+ const trimmed = uri2.replace(/\/+$/, "");
290
+ const idx = trimmed.lastIndexOf("/");
291
+ return idx === -1 ? trimmed : trimmed.slice(idx + 1);
292
+ }
293
+ function eventTypeToEntity(record, now) {
294
+ return {
295
+ type: "calendly_event_type",
296
+ id: record.uri,
297
+ attributes: {
298
+ name: record.name ?? null,
299
+ active: record.active ?? null,
300
+ slug: record.slug ?? null,
301
+ kind: record.kind ?? null,
302
+ poolingType: record.pooling_type ?? null,
303
+ durationMinutes: record.duration ?? null,
304
+ color: record.color ?? null,
305
+ schedulingUrl: record.scheduling_url ?? null,
306
+ ownerUri: record.profile?.owner ?? null,
307
+ ownerType: record.profile?.type ?? null,
308
+ createdAt: parseEpoch(record.created_at ?? null, "iso")
309
+ },
310
+ updated_at: parseEpoch(record.updated_at, "iso") ?? now
311
+ };
312
+ }
313
+ function scheduledEventToEvent(ctx) {
314
+ const { event, invitees } = ctx;
315
+ const startMs = parseEpoch(event.start_time, "iso") ?? 0;
316
+ const primary = invitees[0];
317
+ const noShowCount = invitees.filter((i) => i.no_show != null).length;
318
+ const host = event.event_memberships?.[0];
319
+ const attributes = {
320
+ uri: event.uri,
321
+ name: event.name ?? null,
322
+ eventTypeUri: event.event_type ?? null,
323
+ status: event.status ?? null,
324
+ canceled: event.status === "canceled",
325
+ inviteeEmail: primary?.email ?? null,
326
+ inviteeName: primary?.name ?? null,
327
+ inviteeCount: event.invitees_counter?.total ?? invitees.length,
328
+ activeInviteeCount: event.invitees_counter?.active ?? null,
329
+ noShowCount,
330
+ hostEmail: host?.user_email ?? null,
331
+ hostName: host?.user_name ?? null,
332
+ locationType: event.location?.type ?? null,
333
+ createdAt: parseEpoch(event.created_at ?? null, "iso"),
334
+ updatedAt: parseEpoch(event.updated_at ?? null, "iso")
335
+ };
336
+ return {
337
+ name: "calendly_scheduled_event",
338
+ start_ts: startMs,
339
+ end_ts: parseEpoch(event.end_time ?? null, "iso"),
340
+ attributes
341
+ };
342
+ }
343
+ function cancellationToEvent(event) {
344
+ if (event.status !== "canceled" || !event.cancellation) {
345
+ return null;
346
+ }
347
+ const startMs = parseEpoch(event.start_time, "iso");
348
+ const canceledMs = parseEpoch(event.cancellation.created_at ?? null, "iso") ?? startMs ?? 0;
349
+ const attributes = {
350
+ eventUri: event.uri,
351
+ eventName: event.name ?? null,
352
+ eventTypeUri: event.event_type ?? null,
353
+ reason: event.cancellation.reason ?? null,
354
+ canceledBy: event.cancellation.canceled_by ?? null,
355
+ cancelerType: event.cancellation.canceler_type ?? null,
356
+ eventStartTime: startMs
357
+ };
358
+ return {
359
+ name: "calendly_cancellation",
360
+ start_ts: canceledMs,
361
+ end_ts: null,
362
+ attributes
363
+ };
364
+ }
365
+ var id = "calendly";
366
+ var CalendlyConnector = class _CalendlyConnector extends BaseConnector {
367
+ static id = id;
368
+ static resources = calendlyResources;
369
+ static schemas = schemasFromResources(calendlyResources);
370
+ static create(input, ctx) {
371
+ const parsed = configFields.parse(input);
372
+ return new _CalendlyConnector(
373
+ {
374
+ organizationUri: parsed.organizationUri,
375
+ lookbackDays: parsed.lookbackDays,
376
+ resources: parsed.resources
377
+ },
378
+ { apiToken: parsed.apiToken },
379
+ ctx
380
+ );
381
+ }
382
+ id = id;
383
+ credentials = calendlyCredentials;
384
+ buildHeaders() {
385
+ return {
386
+ Authorization: `Bearer ${this.creds.apiToken}`,
387
+ Accept: "application/json",
388
+ "User-Agent": connectorUserAgent("calendly")
389
+ };
390
+ }
391
+ fetch(url, resource, signal) {
392
+ return this.get(url, {
393
+ resource,
394
+ headers: this.buildHeaders(),
395
+ signal
396
+ });
397
+ }
398
+ activePhases() {
399
+ return selectActivePhases(
400
+ (r) => {
401
+ switch (r) {
402
+ case "event_types":
403
+ return "event_types";
404
+ case "scheduled_events":
405
+ case "cancellations":
406
+ return "scheduled_events";
407
+ }
408
+ },
409
+ PHASE_ORDER,
410
+ this.settings.resources
411
+ );
412
+ }
413
+ async fetchEventTypesPage(page, signal) {
414
+ const u = new URL(`${API_BASE}/event_types`);
415
+ u.searchParams.set("organization", this.settings.organizationUri);
416
+ u.searchParams.set("sort", "updated_at:desc");
417
+ u.searchParams.set("count", String(EVENT_TYPES_PAGE_SIZE));
418
+ if (page !== null) {
419
+ u.searchParams.set("page_token", page);
420
+ }
421
+ const res = await this.fetch(
422
+ u.toString(),
423
+ "event_types",
424
+ signal
425
+ );
426
+ return {
427
+ items: res.body.collection,
428
+ next: res.body.pagination.next_page_token ?? null
429
+ };
430
+ }
431
+ async fetchInvitees(eventUri, signal) {
432
+ const out = [];
433
+ let pageToken = null;
434
+ do {
435
+ signal?.throwIfAborted();
436
+ const u = new URL(
437
+ `${API_BASE}/scheduled_events/${eventUuid(eventUri)}/invitees`
438
+ );
439
+ u.searchParams.set("count", String(INVITEES_PAGE_SIZE));
440
+ if (pageToken !== null) {
441
+ u.searchParams.set("page_token", pageToken);
442
+ }
443
+ const res = await this.fetch(
444
+ u.toString(),
445
+ "event_invitees",
446
+ signal
447
+ );
448
+ out.push(...res.body.collection);
449
+ pageToken = res.body.pagination.next_page_token ?? null;
450
+ } while (pageToken !== null);
451
+ return out;
452
+ }
453
+ async fetchScheduledEventsPage(page, window, signal) {
454
+ const wantInvitees = this.isResourceEnabled("scheduled_events");
455
+ const u = new URL(`${API_BASE}/scheduled_events`);
456
+ u.searchParams.set("organization", this.settings.organizationUri);
457
+ u.searchParams.set("sort", "start_time:asc");
458
+ u.searchParams.set("min_start_time", window.minStartTime);
459
+ u.searchParams.set("max_start_time", window.maxStartTime);
460
+ u.searchParams.set("count", String(SCHEDULED_EVENTS_PAGE_SIZE));
461
+ if (page !== null) {
462
+ u.searchParams.set("page_token", page);
463
+ }
464
+ const res = await this.fetch(
465
+ u.toString(),
466
+ "scheduled_events",
467
+ signal
468
+ );
469
+ const items = [];
470
+ for (const event of res.body.collection) {
471
+ const invitees = wantInvitees ? await this.fetchInvitees(event.uri, signal) : [];
472
+ items.push({ event, invitees });
473
+ }
474
+ return {
475
+ items,
476
+ next: res.body.pagination.next_page_token ?? null
477
+ };
478
+ }
479
+ async writeEventTypes(storage, items) {
480
+ const now = Date.now();
481
+ for (const record of items) {
482
+ await storage.entity(eventTypeToEntity(record, now));
483
+ }
484
+ }
485
+ async writeScheduledEvents(storage, items) {
486
+ const writeEvents = this.isResourceEnabled("scheduled_events");
487
+ const writeCancellations = this.isResourceEnabled("cancellations");
488
+ for (const ctx of items) {
489
+ if (writeEvents) {
490
+ await storage.event(scheduledEventToEvent(ctx));
491
+ }
492
+ if (writeCancellations) {
493
+ const cancellation = cancellationToEvent(ctx.event);
494
+ if (cancellation) {
495
+ await storage.event(cancellation);
496
+ }
497
+ }
498
+ }
499
+ }
500
+ async sync(options, storage, signal) {
501
+ const cursor = isCalendlySyncCursor(options.cursor) ? options.cursor : void 0;
502
+ const isFull = options.mode === "full";
503
+ const lookbackDays = this.settings.lookbackDays ?? DEFAULT_LOOKBACK_DAYS;
504
+ const window = getStartWindow(lookbackDays);
505
+ const phases = this.activePhases();
506
+ return paginateChunked({
507
+ phases,
508
+ cursor,
509
+ signal,
510
+ logger: this.logger,
511
+ fetchPage: async (phase, page, sig) => {
512
+ switch (phase) {
513
+ case "event_types":
514
+ return this.fetchEventTypesPage(page, sig);
515
+ case "scheduled_events":
516
+ return this.fetchScheduledEventsPage(page, window, sig);
517
+ }
518
+ },
519
+ writeBatch: async (phase, items, page) => {
520
+ if (page === null) {
521
+ switch (phase) {
522
+ case "event_types":
523
+ if (isFull) {
524
+ await storage.entities([], {
525
+ types: ["calendly_event_type"]
526
+ });
527
+ }
528
+ break;
529
+ case "scheduled_events":
530
+ if (this.isResourceEnabled("scheduled_events")) {
531
+ await storage.events([], {
532
+ names: ["calendly_scheduled_event"]
533
+ });
534
+ }
535
+ if (this.isResourceEnabled("cancellations")) {
536
+ await storage.events([], {
537
+ names: ["calendly_cancellation"]
538
+ });
539
+ }
540
+ break;
541
+ }
542
+ }
543
+ switch (phase) {
544
+ case "event_types":
545
+ return this.writeEventTypes(storage, items);
546
+ case "scheduled_events":
547
+ return this.writeScheduledEvents(
548
+ storage,
549
+ items
550
+ );
551
+ }
552
+ }
553
+ });
554
+ }
555
+ };
556
+
557
+ // src/index.ts
558
+ var index_default = CalendlyConnector;
559
+ export {
560
+ CalendlyConnector,
561
+ configFields,
562
+ index_default as default,
563
+ doc,
564
+ id,
565
+ calendlyResources as resources
566
+ };
567
+ //# sourceMappingURL=index.js.map