recal-sdk 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,927 @@
1
+ # Recal SDK for JavaScript/TypeScript
2
+
3
+ A powerful, type-safe SDK for interacting with the Recal calendar API. Build sophisticated calendar integrations with support for Google and Microsoft calendar providers.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/recal-sdk.svg)](https://www.npmjs.com/package/recal-sdk)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+ [![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue.svg)](https://www.typescriptlang.org/)
8
+
9
+ ## Features
10
+
11
+ - **Multi-Provider Support**: Seamlessly work with Google Calendar and Microsoft Outlook
12
+ - **Type Safety**: Full TypeScript support with runtime validation
13
+ - **Rich Calendar Operations**: Events, busy queries, scheduling, and more
14
+ - **Organization Management**: Handle organizations and users calendars
15
+ - **OAuth Integration**: Built-in OAuth flow support for calendar connections
16
+ - **Error Handling**: Comprehensive error types for robust applications
17
+ - **Modern Architecture**: Clean, testable service-based design
18
+
19
+ ## Installation
20
+
21
+ ```bash
22
+ # Using npm
23
+ npm install recal-sdk
24
+
25
+ # Using yarn
26
+ yarn add recal-sdk
27
+
28
+ # Using bun
29
+ bun add recal-sdk
30
+ ```
31
+
32
+ ## Quick Start
33
+
34
+ ### Basic Setup
35
+
36
+ ```typescript
37
+ import { RecalClient } from 'recal-sdk'
38
+
39
+ // Initialize the client with token from .env file (RECAL_TOKEN)
40
+ const recal = new RecalClient()
41
+
42
+ // Or manually provide the token
43
+ const recal = new RecalClient({
44
+ token: "recal_xyz"
45
+ })
46
+ ```
47
+
48
+ > **Security Note**: This SDK is designed for server-side use. Never expose your API token in client-side code.
49
+
50
+ ## Core Concepts
51
+
52
+ ### Services
53
+
54
+ The SDK is organized into logical service modules:
55
+
56
+ - **`calendar`** - Event management and busy queries
57
+ - **`scheduling`** - Availability and booking management
58
+ - **`users`** - User profile and settings
59
+ - **`organizations`** - Team and organization management
60
+ - **`oauth`** - Calendar provider authentication
61
+
62
+ ### Time Zones
63
+
64
+ All date/time operations support timezone specification via the `timeZone` parameter.
65
+
66
+ ## API Reference
67
+
68
+ > TypeScript note
69
+ >
70
+ > - Use the exported `Provider` enum for provider arguments.
71
+ > - Date/time fields in responses are parsed into `Date` objects at runtime.
72
+ >
73
+ > ```typescript
74
+ > import { Provider } from 'recal-sdk'
75
+ > ```
76
+
77
+ ### Calendar Service
78
+
79
+ #### Get Busy Information
80
+
81
+ ```typescript
82
+ // Get user's availability (simplest form)
83
+ const busy = await recal.calendar.getBusy(
84
+ 'user_id',
85
+ new Date('2024-01-01'),
86
+ new Date('2024-01-07')
87
+ )
88
+
89
+ // Or with optional filters
90
+ const busyFiltered = await recal.calendar.getBusy(
91
+ 'user_id',
92
+ new Date('2024-01-01'),
93
+ new Date('2024-01-07'),
94
+ {
95
+ provider: Provider.GOOGLE, // optional: filter by provider
96
+ timeZone: 'America/New_York', // optional: timezone
97
+ }
98
+ )
99
+ ```
100
+
101
+ #### List Events
102
+
103
+ ```typescript
104
+ // Get all events in a date range (simplest form)
105
+ const events = await recal.calendar.getEvents(
106
+ 'user_id',
107
+ new Date('2024-01-01'),
108
+ new Date('2024-01-31')
109
+ )
110
+
111
+ // Or with optional filters
112
+ const eventsFiltered = await recal.calendar.getEvents(
113
+ 'user_id',
114
+ new Date('2024-01-01'),
115
+ new Date('2024-01-31'),
116
+ {
117
+ provider: Provider.GOOGLE, // optional: filter by provider
118
+ timeZone: 'Europe/London' // optional: timezone
119
+ }
120
+ )
121
+ ```
122
+
123
+ #### Create Event
124
+
125
+ ```typescript
126
+ // Create a new event (without optional timezone)
127
+ const event = await recal.calendar.createEvent({
128
+ userId: 'user_id',
129
+ provider: Provider.GOOGLE,
130
+ calendarId: 'calendar_id',
131
+ event: {
132
+ subject: 'Team Meeting',
133
+ description: 'Weekly sync',
134
+ start: new Date('2024-01-15T10:00:00Z'),
135
+ end: new Date('2024-01-15T11:00:00Z'),
136
+ attendees: [
137
+ { email: 'colleague@company.com' }
138
+ ]
139
+ }
140
+ })
141
+
142
+ // Or with timezone option
143
+ const eventWithTZ = await recal.calendar.createEvent({
144
+ userId: 'user_id',
145
+ provider: Provider.GOOGLE,
146
+ calendarId: 'calendar_id',
147
+ event: {
148
+ subject: 'Team Meeting',
149
+ description: 'Weekly sync',
150
+ start: new Date('2024-01-15T10:00:00Z'),
151
+ end: new Date('2024-01-15T11:00:00Z'),
152
+ attendees: [
153
+ { email: 'colleague@company.com' }
154
+ ]
155
+ },
156
+ options: { timeZone: 'Europe/Berlin' } // optional
157
+ })
158
+ ```
159
+ #### Get Event
160
+
161
+ ```typescript
162
+ // Get an existing event
163
+ const event = await recal.calendar.getEvent({
164
+ userId: 'user_id',
165
+ provider: Provider.GOOGLE,
166
+ calendarId: 'calendar_id',
167
+ eventId: 'event_id',
168
+ options: { timeZone: 'Europe/Berlin' } // optional
169
+ })
170
+ ```
171
+
172
+ #### Update Event
173
+
174
+ ```typescript
175
+ // Update an existing event (simplest form)
176
+ const updated = await recal.calendar.updateEvent({
177
+ userId: 'user_id',
178
+ provider: Provider.GOOGLE,
179
+ calendarId: 'calendar_id',
180
+ eventId: 'event_id',
181
+ event: {
182
+ subject: 'Updated Meeting Title',
183
+ start: new Date('2024-01-15T14:00:00Z'),
184
+ end: new Date('2024-01-15T15:00:00Z')
185
+ }
186
+ })
187
+ ```
188
+
189
+ ```typescript
190
+ // or with more options
191
+ const updated = await recal.calendar.updateEvent({
192
+ userId: 'user_id',
193
+ provider: Provider.GOOGLE,
194
+ calendarId: 'calendar_id',
195
+ eventId: 'event_id',
196
+ event: {
197
+ subject: 'Updated Meeting title',
198
+ description: 'Updated description',
199
+ start: new Date('2024-01-15T11:00:00Z'),
200
+ end: new Date('2024-01-15T12:00:00Z'),
201
+ attendees: [
202
+ { email: 'colleague@company.com' }
203
+ ]
204
+ },
205
+ options: { timeZone: 'Europe/Berlin' } // optional
206
+ })
207
+ ```
208
+
209
+ #### Delete Event
210
+
211
+ ```typescript
212
+ // Delete an event
213
+ await recal.calendar.deleteEvent({
214
+ userId: 'user_id',
215
+ provider: Provider.GOOGLE,
216
+ calendarId: 'calendar_id',
217
+ eventId: 'event_id'
218
+ })
219
+ ```
220
+
221
+ #### Cross-Calendar Operations (Meta Events)
222
+
223
+ Meta events allow you to work with events across multiple calendar providers:
224
+
225
+ ```typescript
226
+ // Create event across all connected calendars (default behavior)
227
+ const metaEvent = await recal.calendar.createEventByMetaId(
228
+ 'user_id',
229
+ {
230
+ subject: 'Cross-platform meeting',
231
+ start: new Date('2024-01-20T15:00:00Z'),
232
+ end: new Date('2024-01-20T16:00:00Z')
233
+ }
234
+ )
235
+
236
+ // Or specify which providers and timezone to use
237
+ const metaEventSpecific = await recal.calendar.createEventByMetaId(
238
+ 'user_id',
239
+ {
240
+ subject: 'Cross-platform meeting',
241
+ start: new Date('2024-01-20T15:00:00Z'),
242
+ end: new Date('2024-01-20T16:00:00Z')
243
+ },
244
+ {
245
+ provider: [Provider.GOOGLE, Provider.MICROSOFT], // Create on specific providers
246
+ timeZone: 'Europe/Berlin' // optional
247
+ }
248
+ )
249
+
250
+ // Get event across all connected calendars (default behavior)
251
+ const metaEventGet = await recal.calendar.getEventByMetaId(
252
+ 'user_id',
253
+ metaEvent.metaId
254
+ )
255
+
256
+ // Update across all calendars using meta ID
257
+ await recal.calendar.updateEventByMetaId(
258
+ 'user_id',
259
+ metaEvent.metaId,
260
+ { subject: 'Updated title' }
261
+ )
262
+
263
+ // Delete from all calendars
264
+ await recal.calendar.deleteEventByMetaId(
265
+ 'user_id',
266
+ metaEvent.metaId
267
+ )
268
+ ```
269
+
270
+ ### Scheduling Service
271
+
272
+ #### Get User Availability (Basic)
273
+
274
+ ```typescript
275
+ // Find available time slots (minimal config)
276
+ const availability = await recal.scheduling.userSchedulingBasic(
277
+ 'user_id',
278
+ new Date('2024-01-15'),
279
+ new Date('2024-01-20'),
280
+ {
281
+ slotDuration: 30 // Only required: slot duration in minutes
282
+ }
283
+ )
284
+
285
+ // Or with more options
286
+ const availabilityDetailed = await recal.scheduling.userSchedulingBasic(
287
+ 'user_id',
288
+ new Date('2024-01-15'),
289
+ new Date('2024-01-20'),
290
+ {
291
+ slotDuration: 30, // Duration of each slot in minutes
292
+ padding: 0, // Padding between slots
293
+ earliestTimeEachDay: '09:00', // Format: HH:mm
294
+ latestTimeEachDay: '17:00', // Format: HH:mm
295
+ provider: Provider.GOOGLE, // optional: filter by provider
296
+ timeZone: 'America/New_York' // optional
297
+ }
298
+ )
299
+ ```
300
+
301
+ #### Get User Availability (Advanced)
302
+
303
+ ```typescript
304
+ // Find available time slots with custom schedules
305
+ const schedules = [
306
+ {
307
+ days: ['monday'], // Monday
308
+ start: '09:00',
309
+ end: '17:00'
310
+ },
311
+ // ... more schedule rules
312
+ ]
313
+
314
+ // Minimal config
315
+ const availability = await recal.scheduling.userSchedulingAdvanced(
316
+ 'user_id',
317
+ schedules,
318
+ new Date('2024-01-15'),
319
+ new Date('2024-01-20'),
320
+ { slotDuration: 30 } // Only required option
321
+ )
322
+
323
+ // Or with more options
324
+ const availabilityDetailed = await recal.scheduling.userSchedulingAdvanced(
325
+ 'user_id',
326
+ schedules,
327
+ new Date('2024-01-15'),
328
+ new Date('2024-01-20'),
329
+ {
330
+ slotDuration: 30,
331
+ padding: 15,
332
+ provider: Provider.GOOGLE, // optional
333
+ timeZone: 'America/New_York' // optional
334
+ }
335
+ )
336
+ ```
337
+
338
+ #### Get Organization-Wide Availability
339
+
340
+ ```typescript
341
+ // Find organization-wide available time slots (minimal)
342
+ const orgAvailability = await recal.scheduling.getOrgWideAvailability(
343
+ 'org-slug',
344
+ new Date('2024-01-15'),
345
+ new Date('2024-01-20'),
346
+ { slotDuration: 60 } // Only required option
347
+ )
348
+
349
+ // Or with constraints
350
+ const orgAvailabilityConstrained = await recal.scheduling.getOrgWideAvailability(
351
+ 'org-slug',
352
+ new Date('2024-01-15'),
353
+ new Date('2024-01-20'),
354
+ {
355
+ slotDuration: 60,
356
+ padding: 0,
357
+ earliestTimeEachDay: '09:00',
358
+ latestTimeEachDay: '17:00',
359
+ provider: [Provider.GOOGLE, Provider.MICROSOFT], // optional
360
+ timeZone: 'America/New_York' // optional
361
+ }
362
+ )
363
+ ```
364
+
365
+ ### Users Service
366
+
367
+ #### Get User
368
+
369
+ ```typescript
370
+ // Get user information (basic)
371
+ const user = await recal.users.get('user_id', {})
372
+
373
+ // Or with additional data
374
+ const userWithDetails = await recal.users.get('user_id', {
375
+ includeOrgs: true, // Include organizations
376
+ includeOAuth: true // Include OAuth connections
377
+ })
378
+ console.log(user.id)
379
+ ```
380
+
381
+ #### List All Users
382
+
383
+ ```typescript
384
+ // Get all users
385
+ const users = await recal.users.listAll()
386
+ ```
387
+
388
+ #### Create User
389
+
390
+ ```typescript
391
+ // Create a new user (without organizations)
392
+ const user = await recal.users.create('user_id')
393
+
394
+ // Or with organization memberships
395
+ const userWithOrgs = await recal.users.create(
396
+ 'user_id',
397
+ ['org-slug-1', 'org-slug-2'] // optional: organization slugs
398
+ )
399
+ ```
400
+
401
+ #### Update User
402
+
403
+ ```typescript
404
+ // Update user ID
405
+ const updatedUser = await recal.users.update('old_user_id', {
406
+ id: 'new_user_id'
407
+ })
408
+ ```
409
+
410
+ #### Delete User
411
+
412
+ ```typescript
413
+ // Delete a user
414
+ const deletedUser = await recal.users.delete('user_id')
415
+ ```
416
+
417
+ ### Organizations Service
418
+
419
+ #### Get Organization
420
+
421
+ ```typescript
422
+ // Get organization by slug
423
+ const org = await recal.organizations.get('acme-corp')
424
+ ```
425
+
426
+ #### List All Organizations
427
+
428
+ ```typescript
429
+ // Get all organizations
430
+ const orgs = await recal.organizations.listAll()
431
+
432
+ // Get organizations for a specific user
433
+ const userOrgs = await recal.organizations.listAllFromUser('user_id')
434
+ ```
435
+
436
+ #### Create Organization
437
+
438
+ ```typescript
439
+ // Create a new organization
440
+ const org = await recal.organizations.create(
441
+ 'acme-corp', // slug
442
+ 'Acme Corporation' // name
443
+ )
444
+ ```
445
+
446
+ #### Update Organization
447
+
448
+ ```typescript
449
+ // Update organization
450
+ const updated = await recal.organizations.update('acme-corp', {
451
+ slug: 'new-slug',
452
+ name: 'New Name'
453
+ })
454
+ ```
455
+
456
+ #### Manage Members
457
+
458
+ ```typescript
459
+ // Get all members
460
+ const members = await recal.organizations.getMembers('acme-corp')
461
+
462
+ // Add members
463
+ await recal.organizations.addMembers(
464
+ 'acme-corp',
465
+ ['user_id_1', 'user_id_2']
466
+ )
467
+
468
+ // Remove members
469
+ await recal.organizations.removeMembers(
470
+ 'acme-corp',
471
+ ['user_id_1', 'user_id_2']
472
+ )
473
+ ```
474
+
475
+ #### Organization-Wide Busy
476
+
477
+ ```typescript
478
+ // Get team availability (simplest form)
479
+ const teamBusy = await recal.calendar.getOrgWideBusy(
480
+ 'acme-corp',
481
+ new Date('2024-01-15'),
482
+ new Date('2024-01-20'),
483
+ true // primaryOnly: only check primary calendars
484
+ )
485
+
486
+ // Or with optional filters
487
+ const teamBusyFiltered = await recal.calendar.getOrgWideBusy(
488
+ 'acme-corp',
489
+ new Date('2024-01-15'),
490
+ new Date('2024-01-20'),
491
+ true, // primaryOnly: only check primary calendars
492
+ {
493
+ provider: Provider.GOOGLE, // optional: filter by provider
494
+ timeZone: 'America/New_York' // optional
495
+ }
496
+ )
497
+ ```
498
+
499
+ ### OAuth Service
500
+
501
+ #### Get OAuth Link
502
+
503
+ ```typescript
504
+ // Get OAuth authorization URL (with defaults)
505
+ const link = await recal.oauth.getLink(
506
+ 'user_id',
507
+ Provider.GOOGLE
508
+ )
509
+
510
+ // Or with custom options
511
+ const linkWithOptions = await recal.oauth.getLink(
512
+ 'user_id',
513
+ Provider.GOOGLE,
514
+ {
515
+ scope: 'edit', // 'edit' or 'free-busy' (for OAuth scopes)
516
+ accessType: 'offline', // 'offline' or 'online'
517
+ redirectUrl: 'https://app.example.com/callback' // optional
518
+ }
519
+ )
520
+ console.log(link.url) // Use this URL to redirect user
521
+ ```
522
+
523
+ #### Get Multiple OAuth Links
524
+
525
+ ```typescript
526
+ // Get OAuth URLs for all providers (simplest)
527
+ const links = await recal.oauth.getBulkLinks('user_id')
528
+
529
+ // Or with specific providers and options
530
+ const linksFiltered = await recal.oauth.getBulkLinks(
531
+ 'user_id',
532
+ {
533
+ provider: [Provider.GOOGLE, Provider.MICROSOFT],
534
+ scope: 'edit',
535
+ accessType: 'offline'
536
+ }
537
+ )
538
+ ```
539
+
540
+ #### Manage OAuth Connections
541
+
542
+ ```typescript
543
+ // Get all OAuth connections for a user
544
+ const connections = await recal.oauth.getAllConnections(
545
+ 'user_id',
546
+ true // redacted (default: true)
547
+ )
548
+
549
+ // Get specific provider connection
550
+ const googleConnection = await recal.oauth.getConnection(
551
+ 'user_id',
552
+ Provider.GOOGLE,
553
+ false // redacted
554
+ )
555
+
556
+ // Set OAuth tokens manually
557
+ const connection = await recal.oauth.setConnection(
558
+ 'user_id',
559
+ Provider.GOOGLE,
560
+ {
561
+ accessToken: 'access_token',
562
+ refreshToken: 'refresh_token', // optional
563
+ scope: ['calendar.events', 'calendar.readonly'],
564
+ expiresAt: new Date('2024-12-31'), // optional
565
+ email: 'user@example.com' // optional
566
+ }
567
+ )
568
+
569
+ // Disconnect a provider
570
+ await recal.oauth.disconnect('user_id', Provider.GOOGLE)
571
+ ```
572
+
573
+ #### Verify OAuth Callback
574
+
575
+ ```typescript
576
+ // Verify OAuth code from callback
577
+ const result = await recal.oauth.verify(
578
+ Provider.GOOGLE,
579
+ 'auth_code_from_callback',
580
+ 'edit', // 'edit' or 'free-busy' - single scope, not array
581
+ 'state_parameter',
582
+ 'https://app.example.com/callback' // optional
583
+ )
584
+ ```
585
+
586
+ ## Advanced Usage
587
+
588
+ ### Error Handling
589
+
590
+ The SDK provides specific error types for different scenarios:
591
+
592
+ ```typescript
593
+ import {
594
+ UserNotFoundError,
595
+ EventNotFoundError,
596
+ OAuthConnectionNotFoundError,
597
+ OrganizationNotFoundError
598
+ } from 'recal-sdk'
599
+
600
+ try {
601
+ const event = await recal.calendar.getEvent({
602
+ userId: 'user_id',
603
+ provider: Provider.GOOGLE,
604
+ calendarId: 'calendar_id',
605
+ eventId: 'event_id'
606
+ })
607
+ } catch (error) {
608
+ if (error instanceof UserNotFoundError) {
609
+ console.log('User does not exist:', error.userId)
610
+ } else if (error instanceof EventNotFoundError) {
611
+ console.log('Event not found:', error.eventId)
612
+ } else if (error instanceof OAuthConnectionNotFoundError) {
613
+ console.log('Calendar not connected:', error.provider)
614
+ }
615
+ }
616
+ ```
617
+
618
+ ### Batch Operations
619
+
620
+ ```typescript
621
+ // Process multiple users' calendars (simplest form)
622
+ const userIds = ['user1', 'user2', 'user3']
623
+ const allEvents = await Promise.all(
624
+ userIds.map(userId =>
625
+ recal.calendar.getEvents(
626
+ userId,
627
+ new Date('2024-01-01'),
628
+ new Date('2024-01-31')
629
+ )
630
+ )
631
+ )
632
+ ```
633
+
634
+ ### Working with Multiple Providers
635
+
636
+ ```typescript
637
+ // Get all busy data (without filtering)
638
+ const startDate = new Date('2024-01-01')
639
+ const endDate = new Date('2024-01-31')
640
+
641
+ const allBusy = await recal.calendar.getBusy(
642
+ 'user_id',
643
+ startDate,
644
+ endDate
645
+ )
646
+
647
+ // Or aggregate by specific providers
648
+ const providers: Provider[] = [Provider.GOOGLE, Provider.MICROSOFT]
649
+ const busyTimes = await Promise.all(
650
+ providers.map(provider =>
651
+ recal.calendar.getBusy(
652
+ 'user_id',
653
+ startDate,
654
+ endDate,
655
+ { provider } // filter by specific provider
656
+ )
657
+ )
658
+ )
659
+
660
+ // Process the busy times as needed for your application
661
+ // Each element is Busy = TimeRange[]; flatten into a single array of TimeRange
662
+ const allBusyPeriods = busyTimes.flat()
663
+ ```
664
+
665
+ ### Custom Request Configuration
666
+
667
+ ```typescript
668
+ // Use custom base URL
669
+ const recal = new RecalClient({
670
+ token: 'recal_token',
671
+ url: 'https://api.recal.dev' // optional, this is the default
672
+ })
673
+ ```
674
+
675
+ ## Examples
676
+
677
+ ### Building a Booking System
678
+
679
+ ```typescript
680
+ // 1. Check availability
681
+ const availability = await recal.scheduling.userSchedulingBasic(
682
+ 'consultant_id',
683
+ new Date(),
684
+ new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // Next 7 days
685
+ {
686
+ slotDuration: 60, // 60-minute slots
687
+ padding: 15, // 15-minute padding between slots
688
+ earliestTimeEachDay: '09:00',
689
+ latestTimeEachDay: '17:00',
690
+ timeZone: 'America/New_York'
691
+ }
692
+ )
693
+
694
+ // 2. Display available slots to user
695
+ const availableSlots = availability.availableSlots // Already filtered for availability
696
+
697
+ // 3. User selects a slot and provides their information
698
+ const selectedSlot = availableSlots[0] // Example: first available slot
699
+ const clientName = 'John Doe'
700
+ const clientEmail = 'john@example.com'
701
+
702
+ // 4. Create an event for the selected slot (using calendar service)
703
+ const booking = await recal.calendar.createEvent({
704
+ userId: 'consultant_id',
705
+ provider: Provider.GOOGLE,
706
+ calendarId: 'primary',
707
+ event: {
708
+ subject: 'Consultation with ' + clientName,
709
+ description: 'Initial consultation',
710
+ start: selectedSlot.start,
711
+ end: selectedSlot.end,
712
+ attendees: [{ email: clientEmail }]
713
+ }
714
+ })
715
+
716
+ // 5. Send confirmation
717
+ console.log('Booking confirmed:', booking.id)
718
+ ```
719
+
720
+ ### Syncing Calendars
721
+
722
+ ```typescript
723
+ // Sync events between providers
724
+ async function syncCalendars(userId: string) {
725
+ // Get all events from all providers
726
+ const allEvents = await recal.calendar.getEvents(
727
+ userId,
728
+ new Date(),
729
+ new Date(Date.now() + 30 * 24 * 60 * 60 * 1000)
730
+ )
731
+
732
+ // Or get events from Google only
733
+ const googleEvents = await recal.calendar.getEvents(
734
+ userId,
735
+ new Date(),
736
+ new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
737
+ { provider: Provider.GOOGLE }
738
+ )
739
+
740
+ // Copy to Microsoft calendar
741
+ for (const event of googleEvents) {
742
+ if (!event.metaId) { // Not already synced
743
+ await recal.calendar.createEvent({
744
+ userId,
745
+ provider: Provider.MICROSOFT,
746
+ calendarId: 'primary',
747
+ event: {
748
+ subject: event.subject,
749
+ description: event.description,
750
+ start: event.start,
751
+ end: event.end,
752
+ attendees: event.attendees
753
+ }
754
+ })
755
+ }
756
+ }
757
+ }
758
+ ```
759
+
760
+ ### Team Scheduling
761
+
762
+ ```typescript
763
+ // Find time when entire team is available
764
+ async function findTeamSlot(
765
+ orgSlug: string,
766
+ duration: number,
767
+ startDate: Date,
768
+ endDate: Date
769
+ ) {
770
+ // Option 1: Get raw busy times for manual processing
771
+ const busyTimes = await recal.calendar.getOrgWideBusy(
772
+ orgSlug,
773
+ startDate,
774
+ endDate,
775
+ true // Only check primary calendars
776
+ )
777
+ // Process busyTimes array to find gaps for your needs
778
+
779
+ // Option 2: Use the scheduling service (recommended)
780
+ const availability = await recal.scheduling.getOrgWideAvailability(
781
+ orgSlug,
782
+ startDate,
783
+ endDate,
784
+ {
785
+ slotDuration: duration,
786
+ padding: 0,
787
+ earliestTimeEachDay: '09:00',
788
+ latestTimeEachDay: '17:00'
789
+ }
790
+ )
791
+
792
+ // Returns ready-to-use available time slots
793
+ return availability.availableSlots
794
+ }
795
+ ```
796
+
797
+ ## SDK Development
798
+
799
+ ### Prerequisites
800
+
801
+ - Node.js 18+ or Bun 1.0+
802
+ - TypeScript 5.0+
803
+ - Biome 2.1.2 (optional)
804
+
805
+ ### Setup
806
+
807
+ ```bash
808
+ # Clone the repository
809
+ git clone https://github.com/recal-dev/recal-sdk-js.git
810
+ cd recal-sdk-js
811
+
812
+ # Install dependencies
813
+ bun install
814
+
815
+ # Run tests
816
+ bun test
817
+
818
+ # Build the SDK
819
+ bun run build
820
+ ```
821
+
822
+ ### Project Structure
823
+
824
+ ```
825
+ src/
826
+ ├── index.ts # Main client and exports
827
+ ├── services/ # Service implementations
828
+ │ ├── calendar.service.ts
829
+ │ ├── scheduling.service.ts
830
+ │ ├── users.service.ts
831
+ │ ├── organizations.service.ts
832
+ │ └── oauth.service.ts
833
+ ├── entities/ # Domain models
834
+ │ ├── user.ts
835
+ │ └── organization.ts
836
+ ├── types/ # TypeScript type definitions
837
+ │ ├── calendar.types.ts
838
+ │ ├── scheduling.types.ts
839
+ │ ├── internal.types.ts
840
+ │ └── oauth.types.ts
841
+ ├── typebox/ # Runtime validation schemas (auto-generated)
842
+ │ ├── calendar.tb.ts
843
+ │ ├── scheduling.tb.ts
844
+ │ ├── oauth.tb.ts
845
+ │ ├── organization.tb.ts
846
+ │ ├── user.tb.ts
847
+ │ ├── timeString.tb.ts
848
+ │ ├── organization.stripped.tb.ts
849
+ │ └── user.stripped.tb.ts
850
+ ├── utils/ # Helper utilities
851
+ │ ├── fetch.helper.ts
852
+ │ ├── fetchErrorHandler.ts
853
+ │ ├── includes.helper.ts
854
+ │ ├── functionize.ts
855
+ │ └── omit.ts
856
+ └── errors.ts # Custom error classes
857
+ ```
858
+
859
+ ### Code Style
860
+
861
+ This project uses Biome for formatting and linting:
862
+
863
+ ```bash
864
+ # Format code
865
+ bun run format:fix
866
+
867
+ # Lint code
868
+ bun run lint:fix
869
+
870
+ # Run all checks
871
+ bun run check:fix
872
+ ```
873
+
874
+ ### Testing
875
+
876
+ ```bash
877
+ # Run all tests
878
+ bun test
879
+
880
+ # Run specific test file
881
+ bun test tests/integrations/users.test.ts
882
+
883
+ # Run with coverage
884
+ bun test --coverage
885
+ ```
886
+
887
+ ## Contributing
888
+
889
+ We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details.
890
+
891
+ ### Development Workflow
892
+
893
+ 1. Fork the repository
894
+ 2. Create a feature branch (`git checkout -b feature/amazing-feature`)
895
+ 3. Make your changes
896
+ 4. Run tests and linting (`bun test && bun run check:fix`)
897
+ 5. Commit your changes (`git commit -m 'Add amazing feature'`)
898
+ 6. Push to your branch (`git push origin feature/amazing-feature`)
899
+ 7. Open a Pull Request
900
+
901
+ ### Reporting Issues
902
+
903
+ Found a bug or have a feature request? Please [open an issue](https://github.com/recal-dev/recal-sdk-js/issues) with:
904
+
905
+ - Clear description
906
+ - Steps to reproduce (for bugs)
907
+ - Expected vs actual behavior
908
+ - SDK version and environment details
909
+
910
+ ## Support
911
+
912
+ - **Documentation**: [https://docs.recal.dev](https://docs.recal.dev)
913
+ - **API Reference**: [https://api.recal.dev/docs](https://api.recal.dev/docs)
914
+ - **Email**: team@recal.dev
915
+ - **Discord**: [Join our community](https://discord.gg/recal)
916
+
917
+ ## License
918
+
919
+ This SDK is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
920
+
921
+ ## Changelog
922
+
923
+ See [CHANGELOG.md](CHANGELOG.md) for a list of changes in each version.
924
+
925
+ ---
926
+
927
+ Built with ❤️ by the [Recal](https://recal.dev) team