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 +927 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +66 -20
- package/dist/services/calendar.service.d.ts +81 -34
- package/dist/services/calendar.service.d.ts.map +1 -1
- package/dist/services/oauth.service.d.ts +2 -1
- package/dist/services/oauth.service.d.ts.map +1 -1
- package/dist/services/scheduling.service.d.ts +30 -27
- package/dist/services/scheduling.service.d.ts.map +1 -1
- package/dist/typebox/calendar.tb.d.ts +2 -2
- package/dist/typebox/calendar.tb.d.ts.map +1 -1
- package/dist/typebox/oauth.tb.d.ts +2 -2
- package/dist/typebox/user.tb.d.ts +1 -1
- package/dist/types/calendar.types.d.ts +3 -3
- package/dist/types/calendar.types.d.ts.map +1 -1
- package/package.json +2 -1
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
|
+
[](https://www.npmjs.com/package/recal-sdk)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
[](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
|