@automate.ax/integration-contracts 0.55.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/dist/airtable/api.d.ts +19 -0
- package/dist/airtable/api.js +44 -0
- package/dist/airtable/index.d.ts +353 -0
- package/dist/airtable/index.js +32 -0
- package/dist/airtable/schemas.d.ts +716 -0
- package/dist/airtable/schemas.js +184 -0
- package/dist/asana/api.d.ts +70 -0
- package/dist/asana/api.js +119 -0
- package/dist/asana/index.d.ts +384 -0
- package/dist/asana/index.js +38 -0
- package/dist/asana/schemas.d.ts +1060 -0
- package/dist/asana/schemas.js +512 -0
- package/dist/brevo/api.d.ts +31 -0
- package/dist/brevo/api.js +64 -0
- package/dist/brevo/index.d.ts +822 -0
- package/dist/brevo/index.js +51 -0
- package/dist/brevo/schemas.d.ts +1217 -0
- package/dist/brevo/schemas.js +332 -0
- package/dist/github/index.d.ts +37 -0
- package/dist/github/index.js +106 -0
- package/dist/github/schemas.d.ts +8014 -0
- package/dist/github/schemas.js +113 -0
- package/dist/gmail/gmail.d.ts +172 -0
- package/dist/gmail/gmail.js +405 -0
- package/dist/gmail/index.d.ts +262 -0
- package/dist/gmail/index.js +23 -0
- package/dist/gmail/schemas.d.ts +444 -0
- package/dist/gmail/schemas.js +242 -0
- package/dist/google-calendar/event-schemas.d.ts +117 -0
- package/dist/google-calendar/event-schemas.js +13 -0
- package/dist/google-calendar/google-calendar.d.ts +147 -0
- package/dist/google-calendar/google-calendar.js +583 -0
- package/dist/google-calendar/index.d.ts +365 -0
- package/dist/google-calendar/index.js +24 -0
- package/dist/google-calendar/schemas.d.ts +592 -0
- package/dist/google-calendar/schemas.js +379 -0
- package/dist/google-forms/event-schemas.d.ts +105 -0
- package/dist/google-forms/event-schemas.js +20 -0
- package/dist/google-forms/google-forms.d.ts +243 -0
- package/dist/google-forms/google-forms.js +581 -0
- package/dist/google-forms/index.d.ts +141 -0
- package/dist/google-forms/index.js +20 -0
- package/dist/google-forms/schemas.d.ts +618 -0
- package/dist/google-forms/schemas.js +383 -0
- package/dist/linear/api.d.ts +63 -0
- package/dist/linear/api.js +88 -0
- package/dist/linear/index.d.ts +1338 -0
- package/dist/linear/index.js +55 -0
- package/dist/linear/schemas.d.ts +1795 -0
- package/dist/linear/schemas.js +760 -0
- package/dist/outlook/graph.d.ts +34 -0
- package/dist/outlook/graph.js +98 -0
- package/dist/outlook/index.d.ts +115 -0
- package/dist/outlook/index.js +11 -0
- package/dist/outlook/schemas.d.ts +529 -0
- package/dist/outlook/schemas.js +312 -0
- package/dist/resend/index.d.ts +707 -0
- package/dist/resend/index.js +54 -0
- package/dist/resend/schemas.d.ts +1033 -0
- package/dist/resend/schemas.js +377 -0
- package/dist/slack/index.d.ts +151 -0
- package/dist/slack/index.js +22 -0
- package/dist/slack/schemas.d.ts +641 -0
- package/dist/slack/schemas.js +581 -0
- package/dist/teams/graph.d.ts +33 -0
- package/dist/teams/graph.js +95 -0
- package/dist/teams/index.d.ts +164 -0
- package/dist/teams/index.js +21 -0
- package/dist/teams/schemas.d.ts +675 -0
- package/dist/teams/schemas.js +365 -0
- package/dist/trello/api.d.ts +69 -0
- package/dist/trello/api.js +140 -0
- package/dist/trello/event-schemas.d.ts +1101 -0
- package/dist/trello/event-schemas.js +233 -0
- package/dist/trello/index.d.ts +721 -0
- package/dist/trello/index.js +35 -0
- package/dist/trello/schemas.d.ts +283 -0
- package/dist/trello/schemas.js +401 -0
- package/dist/triggers.d.ts +26 -0
- package/dist/triggers.js +33 -0
- package/dist/vercel/index.d.ts +1065 -0
- package/dist/vercel/index.js +114 -0
- package/dist/vercel/schemas.d.ts +1518 -0
- package/dist/vercel/schemas.js +335 -0
- package/dist/whatsapp/api.d.ts +62 -0
- package/dist/whatsapp/api.js +121 -0
- package/dist/whatsapp/index.d.ts +614 -0
- package/dist/whatsapp/index.js +35 -0
- package/dist/whatsapp/schemas.d.ts +926 -0
- package/dist/whatsapp/schemas.js +326 -0
- package/package.json +169 -0
- package/src/airtable/api.ts +66 -0
- package/src/airtable/index.ts +41 -0
- package/src/airtable/schemas.ts +231 -0
- package/src/asana/api.ts +171 -0
- package/src/asana/index.ts +49 -0
- package/src/asana/schemas.ts +655 -0
- package/src/brevo/api.ts +89 -0
- package/src/brevo/index.ts +66 -0
- package/src/brevo/schemas.ts +449 -0
- package/src/github/index.ts +217 -0
- package/src/github/schemas.ts +435 -0
- package/src/gmail/gmail.ts +577 -0
- package/src/gmail/index.ts +26 -0
- package/src/gmail/schemas.ts +327 -0
- package/src/google-calendar/event-schemas.ts +16 -0
- package/src/google-calendar/google-calendar.ts +727 -0
- package/src/google-calendar/index.ts +27 -0
- package/src/google-calendar/schemas.ts +529 -0
- package/src/google-forms/event-schemas.ts +26 -0
- package/src/google-forms/google-forms.ts +683 -0
- package/src/google-forms/index.ts +23 -0
- package/src/google-forms/schemas.ts +499 -0
- package/src/linear/api.ts +141 -0
- package/src/linear/index.ts +71 -0
- package/src/linear/schemas.ts +1060 -0
- package/src/outlook/graph.ts +132 -0
- package/src/outlook/index.ts +14 -0
- package/src/outlook/schemas.ts +388 -0
- package/src/resend/index.ts +70 -0
- package/src/resend/schemas.ts +462 -0
- package/src/slack/index.ts +29 -0
- package/src/slack/schemas.ts +737 -0
- package/src/teams/graph.ts +130 -0
- package/src/teams/index.ts +27 -0
- package/src/teams/schemas.ts +463 -0
- package/src/trello/api.ts +190 -0
- package/src/trello/event-schemas.ts +290 -0
- package/src/trello/index.ts +45 -0
- package/src/trello/schemas.ts +507 -0
- package/src/triggers.ts +68 -0
- package/src/vercel/index.ts +147 -0
- package/src/vercel/schemas.ts +594 -0
- package/src/whatsapp/api.ts +171 -0
- package/src/whatsapp/index.ts +46 -0
- package/src/whatsapp/schemas.ts +341 -0
|
@@ -0,0 +1,727 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer"
|
|
2
|
+
import { auth, calendar, type calendar_v3 } from "@googleapis/calendar"
|
|
3
|
+
import { z } from "zod"
|
|
4
|
+
import {
|
|
5
|
+
ACL_PAGE_SCHEMA,
|
|
6
|
+
ACL_RULE_SCHEMA,
|
|
7
|
+
CALENDAR_EVENT_SCHEMA,
|
|
8
|
+
CALENDAR_PAGE_SCHEMA,
|
|
9
|
+
CALENDAR_SCHEMA,
|
|
10
|
+
COLOR_PALETTE_SCHEMA,
|
|
11
|
+
EVENT_PAGE_SCHEMA,
|
|
12
|
+
EVENT_DETAILS_INPUT_SCHEMA,
|
|
13
|
+
EVENT_SCHEDULE_INPUT_SCHEMA,
|
|
14
|
+
FREE_BUSY_SCHEMA,
|
|
15
|
+
} from "./schemas"
|
|
16
|
+
|
|
17
|
+
const GOOGLE_CALENDAR_HOSTS = new Set(["calendar.google.com", "www.google.com"])
|
|
18
|
+
const GOOGLE_CALENDAR_SECRET_SCHEMA = z.object({
|
|
19
|
+
accessToken: z.string().min(1),
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Creates the official Calendar client from a resolved Google integration
|
|
24
|
+
* secret.
|
|
25
|
+
*
|
|
26
|
+
* @param secret - Refreshed Google integration secret.
|
|
27
|
+
*/
|
|
28
|
+
export function getGoogleCalendarApi(secret: Record<string, unknown>) {
|
|
29
|
+
const { accessToken } = GOOGLE_CALENDAR_SECRET_SCHEMA.parse(secret)
|
|
30
|
+
return calendar({
|
|
31
|
+
auth: new auth.OAuth2({
|
|
32
|
+
credentials: { access_token: accessToken },
|
|
33
|
+
}),
|
|
34
|
+
version: "v3",
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Resolves an exact calendar title or ID to the provider's canonical ID.
|
|
40
|
+
*
|
|
41
|
+
* Unknown references are returned unchanged so email-address and otherwise
|
|
42
|
+
* unlisted calendar IDs remain usable.
|
|
43
|
+
*
|
|
44
|
+
* @param calendarApi - Authenticated Google Calendar client.
|
|
45
|
+
* @param reference - Calendar title or ID, defaulting to `primary`.
|
|
46
|
+
*/
|
|
47
|
+
export async function resolveCalendarId(
|
|
48
|
+
calendarApi: calendar_v3.Calendar,
|
|
49
|
+
reference?: string,
|
|
50
|
+
) {
|
|
51
|
+
return (await resolveCalendarIds(calendarApi, [reference]))[0]!
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Resolves multiple exact calendar titles or IDs with one paginated listing.
|
|
56
|
+
*
|
|
57
|
+
* Unknown references are returned unchanged so email-address and otherwise
|
|
58
|
+
* unlisted calendar IDs remain usable.
|
|
59
|
+
*
|
|
60
|
+
* @param calendarApi - Authenticated Google Calendar client.
|
|
61
|
+
* @param references - Calendar titles or IDs, with absent values as primary.
|
|
62
|
+
*/
|
|
63
|
+
export async function resolveCalendarIds(
|
|
64
|
+
calendarApi: calendar_v3.Calendar,
|
|
65
|
+
references: (string | undefined)[],
|
|
66
|
+
) {
|
|
67
|
+
if (references.every((value) => value === undefined || value === "primary")) {
|
|
68
|
+
return references.map(() => "primary")
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const calendars: calendar_v3.Schema$CalendarListEntry[] = []
|
|
72
|
+
let pageToken: string | undefined
|
|
73
|
+
do {
|
|
74
|
+
const { data } = await calendarApi.calendarList.list({
|
|
75
|
+
maxResults: 250,
|
|
76
|
+
pageToken,
|
|
77
|
+
showDeleted: false,
|
|
78
|
+
showHidden: true,
|
|
79
|
+
})
|
|
80
|
+
calendars.push(...(data.items ?? []))
|
|
81
|
+
pageToken = data.nextPageToken ?? undefined
|
|
82
|
+
} while (pageToken)
|
|
83
|
+
|
|
84
|
+
return references.map((reference) =>
|
|
85
|
+
resolveCalendarReference(calendars, reference),
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Resolves one calendar reference against an already fetched calendar list.
|
|
91
|
+
*
|
|
92
|
+
* @param calendars - Visible Google Calendar list entries.
|
|
93
|
+
* @param reference - Calendar title or ID, defaulting to primary.
|
|
94
|
+
* @throws When the calendar title matches more than one visible calendar.
|
|
95
|
+
*/
|
|
96
|
+
export function resolveCalendarReference(
|
|
97
|
+
calendars: calendar_v3.Schema$CalendarListEntry[],
|
|
98
|
+
reference?: string,
|
|
99
|
+
) {
|
|
100
|
+
if (reference === undefined || reference === "primary") return "primary"
|
|
101
|
+
|
|
102
|
+
const idMatch = calendars.find(({ id }) => id === reference)
|
|
103
|
+
if (idMatch?.id) return idMatch.id
|
|
104
|
+
|
|
105
|
+
const normalizedReference = reference.trim().toLowerCase()
|
|
106
|
+
const nameMatches = calendars.filter(
|
|
107
|
+
({ summary, summaryOverride }) =>
|
|
108
|
+
summary?.trim().toLowerCase() === normalizedReference ||
|
|
109
|
+
summaryOverride?.trim().toLowerCase() === normalizedReference,
|
|
110
|
+
)
|
|
111
|
+
if (nameMatches.length === 1 && nameMatches[0]!.id) {
|
|
112
|
+
return nameMatches[0]!.id
|
|
113
|
+
}
|
|
114
|
+
if (nameMatches.length > 1) {
|
|
115
|
+
throw new Error(
|
|
116
|
+
`Google Calendar "${reference}" is ambiguous; matching IDs: ${nameMatches.map(({ id }) => id).join(", ")}.`,
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
return reference
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Accepts a raw event ID or a standard Google Calendar event URL.
|
|
124
|
+
*
|
|
125
|
+
* @param event - Event ID or URL.
|
|
126
|
+
* @throws When a URL is not a recognizable Google Calendar event URL.
|
|
127
|
+
*/
|
|
128
|
+
export function normalizeEventId(event: string) {
|
|
129
|
+
const value = event.trim()
|
|
130
|
+
if (!URL.canParse(value)) return value
|
|
131
|
+
|
|
132
|
+
const url = new URL(value)
|
|
133
|
+
if (!GOOGLE_CALENDAR_HOSTS.has(url.hostname)) {
|
|
134
|
+
throw new Error(`Expected a Google Calendar URL, received "${value}".`)
|
|
135
|
+
}
|
|
136
|
+
const encodedEvent = url.searchParams.get("eid")
|
|
137
|
+
if (!encodedEvent) {
|
|
138
|
+
throw new Error(`Could not find an event ID in "${value}".`)
|
|
139
|
+
}
|
|
140
|
+
const [eventId] = Buffer.from(encodedEvent, "base64url")
|
|
141
|
+
.toString("utf8")
|
|
142
|
+
.split(" ")
|
|
143
|
+
if (!eventId) {
|
|
144
|
+
throw new Error(`Could not decode the event ID in "${value}".`)
|
|
145
|
+
}
|
|
146
|
+
return eventId
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Converts an ergonomic timed or all-day schedule to Calendar API date-times.
|
|
151
|
+
*
|
|
152
|
+
* @param schedule - Timed or all-day event schedule.
|
|
153
|
+
*/
|
|
154
|
+
export function toEventDateTimes(
|
|
155
|
+
schedule: z.infer<typeof EVENT_SCHEDULE_INPUT_SCHEMA>,
|
|
156
|
+
) {
|
|
157
|
+
if ("startDate" in schedule) {
|
|
158
|
+
return {
|
|
159
|
+
end: {
|
|
160
|
+
date: schedule.endDate ?? addDays(schedule.startDate, 1),
|
|
161
|
+
},
|
|
162
|
+
start: { date: schedule.startDate },
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
end: { dateTime: schedule.end, timeZone: schedule.timeZone },
|
|
167
|
+
start: { dateTime: schedule.start, timeZone: schedule.timeZone },
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Normalizes a provider event into the stable public Calendar event shape.
|
|
173
|
+
*
|
|
174
|
+
* @param event - Provider event.
|
|
175
|
+
* @param calendarId - Calendar from which the event was read.
|
|
176
|
+
* @throws When the provider omits the event ID.
|
|
177
|
+
*/
|
|
178
|
+
export function normalizeCalendarEvent(
|
|
179
|
+
event: calendar_v3.Schema$Event,
|
|
180
|
+
calendarId: string,
|
|
181
|
+
): z.infer<typeof CALENDAR_EVENT_SCHEMA> {
|
|
182
|
+
if (!event.id) {
|
|
183
|
+
throw new Error("Google Calendar returned an event without an ID.")
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
attachments: (event.attachments ?? []).flatMap((attachment) =>
|
|
187
|
+
attachment.fileUrl
|
|
188
|
+
? [
|
|
189
|
+
{
|
|
190
|
+
fileId: attachment.fileId ?? undefined,
|
|
191
|
+
fileUrl: attachment.fileUrl,
|
|
192
|
+
iconLink: attachment.iconLink ?? undefined,
|
|
193
|
+
mimeType: attachment.mimeType ?? undefined,
|
|
194
|
+
title: attachment.title ?? undefined,
|
|
195
|
+
},
|
|
196
|
+
]
|
|
197
|
+
: [],
|
|
198
|
+
),
|
|
199
|
+
attendees: (event.attendees ?? []).flatMap((attendee) =>
|
|
200
|
+
attendee.email
|
|
201
|
+
? [
|
|
202
|
+
{
|
|
203
|
+
additionalGuests: attendee.additionalGuests ?? undefined,
|
|
204
|
+
comment: attendee.comment ?? undefined,
|
|
205
|
+
displayName: attendee.displayName ?? undefined,
|
|
206
|
+
email: attendee.email,
|
|
207
|
+
optional: attendee.optional ?? undefined,
|
|
208
|
+
organizer: attendee.organizer ?? false,
|
|
209
|
+
resource: attendee.resource ?? undefined,
|
|
210
|
+
responseStatus: normalizeResponseStatus(attendee.responseStatus),
|
|
211
|
+
self: attendee.self ?? false,
|
|
212
|
+
},
|
|
213
|
+
]
|
|
214
|
+
: [],
|
|
215
|
+
),
|
|
216
|
+
calendarId,
|
|
217
|
+
colorId: event.colorId ?? undefined,
|
|
218
|
+
conference: normalizeConference(event.conferenceData),
|
|
219
|
+
createdAt: event.created ?? undefined,
|
|
220
|
+
creator: normalizePrincipal(event.creator),
|
|
221
|
+
description: event.description ?? undefined,
|
|
222
|
+
end: event.end ? normalizeEventTime(event.end) : undefined,
|
|
223
|
+
extendedProperties: {
|
|
224
|
+
private: withoutNullValues(event.extendedProperties?.private),
|
|
225
|
+
shared: withoutNullValues(event.extendedProperties?.shared),
|
|
226
|
+
},
|
|
227
|
+
guestsCanInviteOthers: event.guestsCanInviteOthers ?? true,
|
|
228
|
+
guestsCanModify: event.guestsCanModify ?? false,
|
|
229
|
+
guestsCanSeeOtherGuests: event.guestsCanSeeOtherGuests ?? true,
|
|
230
|
+
hangoutLink: event.hangoutLink ?? undefined,
|
|
231
|
+
htmlLink: event.htmlLink ?? undefined,
|
|
232
|
+
id: event.id,
|
|
233
|
+
iCalUID: event.iCalUID ?? undefined,
|
|
234
|
+
location: event.location ?? undefined,
|
|
235
|
+
originalStart: event.originalStartTime
|
|
236
|
+
? normalizeEventTime(event.originalStartTime)
|
|
237
|
+
: undefined,
|
|
238
|
+
organizer: normalizePrincipal(event.organizer),
|
|
239
|
+
recurrence: event.recurrence ?? [],
|
|
240
|
+
recurringEventId: event.recurringEventId ?? undefined,
|
|
241
|
+
reminders: {
|
|
242
|
+
overrides: (event.reminders?.overrides ?? []).flatMap((reminder) =>
|
|
243
|
+
reminder.method && reminder.minutes != null
|
|
244
|
+
? [
|
|
245
|
+
{
|
|
246
|
+
method: normalizeReminderMethod(reminder.method),
|
|
247
|
+
minutes: reminder.minutes,
|
|
248
|
+
},
|
|
249
|
+
]
|
|
250
|
+
: [],
|
|
251
|
+
),
|
|
252
|
+
useDefault: event.reminders?.useDefault ?? true,
|
|
253
|
+
},
|
|
254
|
+
sequence: event.sequence ?? 0,
|
|
255
|
+
start: event.start ? normalizeEventTime(event.start) : undefined,
|
|
256
|
+
status: normalizeEventStatus(event.status),
|
|
257
|
+
summary: event.summary ?? "",
|
|
258
|
+
transparency:
|
|
259
|
+
event.transparency === "transparent" ? "transparent" : "opaque",
|
|
260
|
+
type: event.eventType ?? "default",
|
|
261
|
+
updatedAt: event.updated ?? undefined,
|
|
262
|
+
visibility: normalizeVisibility(event.visibility),
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Normalizes a provider event page while retaining pagination and sync tokens.
|
|
268
|
+
*
|
|
269
|
+
* @param events - Provider event collection.
|
|
270
|
+
* @param calendarId - Calendar from which events were read.
|
|
271
|
+
*/
|
|
272
|
+
export function normalizeEventPage(
|
|
273
|
+
events: calendar_v3.Schema$Events,
|
|
274
|
+
calendarId: string,
|
|
275
|
+
): z.infer<typeof EVENT_PAGE_SCHEMA> {
|
|
276
|
+
return {
|
|
277
|
+
events: (events.items ?? []).map((event) =>
|
|
278
|
+
normalizeCalendarEvent(event, calendarId),
|
|
279
|
+
),
|
|
280
|
+
nextPageToken: events.nextPageToken ?? undefined,
|
|
281
|
+
nextSyncToken: events.nextSyncToken ?? undefined,
|
|
282
|
+
timeZone: events.timeZone ?? undefined,
|
|
283
|
+
updatedAt: events.updated ?? undefined,
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Normalizes a calendar or calendar-list entry.
|
|
289
|
+
*
|
|
290
|
+
* @param value - Provider calendar metadata.
|
|
291
|
+
* @param id - Calendar ID when not included in the provider resource.
|
|
292
|
+
* @throws When neither the resource nor caller supplies a calendar ID.
|
|
293
|
+
*/
|
|
294
|
+
export function normalizeCalendar(
|
|
295
|
+
value: calendar_v3.Schema$Calendar | calendar_v3.Schema$CalendarListEntry,
|
|
296
|
+
id?: string,
|
|
297
|
+
): z.infer<typeof CALENDAR_SCHEMA> {
|
|
298
|
+
const calendarId = value.id ?? id
|
|
299
|
+
if (!calendarId) {
|
|
300
|
+
throw new Error("Google Calendar returned calendar metadata without an ID.")
|
|
301
|
+
}
|
|
302
|
+
const listEntry = "accessRole" in value ? value : undefined
|
|
303
|
+
return {
|
|
304
|
+
accessRole: listEntry?.accessRole ?? undefined,
|
|
305
|
+
autoAcceptInvitations: value.autoAcceptInvitations ?? false,
|
|
306
|
+
backgroundColor: listEntry?.backgroundColor ?? undefined,
|
|
307
|
+
colorId: listEntry?.colorId ?? undefined,
|
|
308
|
+
conferenceSolutionTypes:
|
|
309
|
+
value.conferenceProperties?.allowedConferenceSolutionTypes ?? [],
|
|
310
|
+
defaultReminders:
|
|
311
|
+
listEntry?.defaultReminders?.flatMap((reminder) =>
|
|
312
|
+
reminder.method && reminder.minutes != null
|
|
313
|
+
? [{ method: reminder.method, minutes: reminder.minutes }]
|
|
314
|
+
: [],
|
|
315
|
+
) ?? [],
|
|
316
|
+
dataOwner: value.dataOwner ?? undefined,
|
|
317
|
+
deleted: listEntry?.deleted ?? false,
|
|
318
|
+
description: value.description ?? undefined,
|
|
319
|
+
foregroundColor: listEntry?.foregroundColor ?? undefined,
|
|
320
|
+
hidden: listEntry?.hidden ?? false,
|
|
321
|
+
id: calendarId,
|
|
322
|
+
location: value.location ?? undefined,
|
|
323
|
+
notifications:
|
|
324
|
+
listEntry?.notificationSettings?.notifications?.flatMap((notification) =>
|
|
325
|
+
notification.type &&
|
|
326
|
+
(notification.type === "eventCreation" ||
|
|
327
|
+
notification.type === "eventChange" ||
|
|
328
|
+
notification.type === "eventCancellation" ||
|
|
329
|
+
notification.type === "eventResponse" ||
|
|
330
|
+
notification.type === "agenda")
|
|
331
|
+
? [{ type: notification.type }]
|
|
332
|
+
: [],
|
|
333
|
+
) ?? [],
|
|
334
|
+
primary: listEntry?.primary ?? id === "primary",
|
|
335
|
+
selected: listEntry?.selected ?? false,
|
|
336
|
+
summary: value.summary ?? "",
|
|
337
|
+
summaryOverride: listEntry?.summaryOverride ?? undefined,
|
|
338
|
+
timeZone: value.timeZone ?? undefined,
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Normalizes a provider calendar-list page.
|
|
344
|
+
*
|
|
345
|
+
* @param value - Provider calendar-list collection.
|
|
346
|
+
*/
|
|
347
|
+
export function normalizeCalendarPage(
|
|
348
|
+
value: calendar_v3.Schema$CalendarList,
|
|
349
|
+
): z.infer<typeof CALENDAR_PAGE_SCHEMA> {
|
|
350
|
+
return {
|
|
351
|
+
calendars: (value.items ?? []).map((entry) => normalizeCalendar(entry)),
|
|
352
|
+
nextPageToken: value.nextPageToken ?? undefined,
|
|
353
|
+
nextSyncToken: value.nextSyncToken ?? undefined,
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Normalizes a provider free/busy response.
|
|
359
|
+
*
|
|
360
|
+
* @param value - Provider free/busy response.
|
|
361
|
+
* @throws When the provider omits the requested time range.
|
|
362
|
+
*/
|
|
363
|
+
export function normalizeFreeBusy(
|
|
364
|
+
value: calendar_v3.Schema$FreeBusyResponse,
|
|
365
|
+
): z.infer<typeof FREE_BUSY_SCHEMA> {
|
|
366
|
+
if (!value.timeMin || !value.timeMax) {
|
|
367
|
+
throw new Error("Google Calendar returned an incomplete free/busy range.")
|
|
368
|
+
}
|
|
369
|
+
return {
|
|
370
|
+
calendars: Object.entries(value.calendars ?? {}).map(
|
|
371
|
+
([calendarId, result]) => ({
|
|
372
|
+
busy: (result.busy ?? []).flatMap((period) =>
|
|
373
|
+
period.start && period.end
|
|
374
|
+
? [{ end: period.end, start: period.start }]
|
|
375
|
+
: [],
|
|
376
|
+
),
|
|
377
|
+
calendarId,
|
|
378
|
+
errors: (result.errors ?? []).map((error) => ({
|
|
379
|
+
domain: error.domain ?? undefined,
|
|
380
|
+
reason: error.reason ?? undefined,
|
|
381
|
+
})),
|
|
382
|
+
}),
|
|
383
|
+
),
|
|
384
|
+
timeMax: value.timeMax,
|
|
385
|
+
timeMin: value.timeMin,
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Normalizes Google Calendar color definitions.
|
|
391
|
+
*
|
|
392
|
+
* @param value - Provider color palette.
|
|
393
|
+
*/
|
|
394
|
+
export function normalizeColors(
|
|
395
|
+
value: calendar_v3.Schema$Colors,
|
|
396
|
+
): z.infer<typeof COLOR_PALETTE_SCHEMA> {
|
|
397
|
+
return {
|
|
398
|
+
calendars: normalizeColorDefinitions(value.calendar),
|
|
399
|
+
events: normalizeColorDefinitions(value.event),
|
|
400
|
+
updatedAt: value.updated ?? undefined,
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Normalizes one calendar ACL rule.
|
|
406
|
+
*
|
|
407
|
+
* @param value - Provider ACL rule.
|
|
408
|
+
* @throws When the provider omits the rule ID or scope.
|
|
409
|
+
*/
|
|
410
|
+
export function normalizeAclRule(
|
|
411
|
+
value: calendar_v3.Schema$AclRule,
|
|
412
|
+
): z.infer<typeof ACL_RULE_SCHEMA> {
|
|
413
|
+
if (!value.id || !value.scope?.type) {
|
|
414
|
+
throw new Error("Google Calendar returned an incomplete ACL rule.")
|
|
415
|
+
}
|
|
416
|
+
return {
|
|
417
|
+
id: value.id,
|
|
418
|
+
role: normalizeAclRole(value.role),
|
|
419
|
+
scope: {
|
|
420
|
+
type: normalizeAclScope(value.scope.type),
|
|
421
|
+
value: value.scope.value ?? undefined,
|
|
422
|
+
},
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Normalizes a page of calendar ACL rules.
|
|
428
|
+
*
|
|
429
|
+
* @param value - Provider ACL collection.
|
|
430
|
+
*/
|
|
431
|
+
export function normalizeAclPage(
|
|
432
|
+
value: calendar_v3.Schema$Acl,
|
|
433
|
+
): z.infer<typeof ACL_PAGE_SCHEMA> {
|
|
434
|
+
return {
|
|
435
|
+
nextPageToken: value.nextPageToken ?? undefined,
|
|
436
|
+
nextSyncToken: value.nextSyncToken ?? undefined,
|
|
437
|
+
rules: (value.items ?? []).map(normalizeAclRule),
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Finds common free windows of a requested duration.
|
|
443
|
+
*
|
|
444
|
+
* @param freeBusy - Normalized free/busy response.
|
|
445
|
+
* @param durationMinutes - Required duration.
|
|
446
|
+
* @param stepMinutes - Candidate-start increment.
|
|
447
|
+
* @param limit - Maximum returned slots.
|
|
448
|
+
*/
|
|
449
|
+
export function findAvailableSlots(
|
|
450
|
+
freeBusy: z.infer<typeof FREE_BUSY_SCHEMA>,
|
|
451
|
+
durationMinutes: number,
|
|
452
|
+
stepMinutes: number,
|
|
453
|
+
limit: number,
|
|
454
|
+
) {
|
|
455
|
+
const queryEnd = new Date(freeBusy.timeMax).getTime()
|
|
456
|
+
const duration = durationMinutes * 60_000
|
|
457
|
+
const step = stepMinutes * 60_000
|
|
458
|
+
const busy = mergePeriods(
|
|
459
|
+
freeBusy.calendars.flatMap(({ busy: periods }) =>
|
|
460
|
+
periods.map(({ end, start }) => ({
|
|
461
|
+
end: new Date(end).getTime(),
|
|
462
|
+
start: new Date(start).getTime(),
|
|
463
|
+
})),
|
|
464
|
+
),
|
|
465
|
+
)
|
|
466
|
+
const slots: { durationMinutes: number; end: string; start: string }[] = []
|
|
467
|
+
|
|
468
|
+
for (
|
|
469
|
+
let candidate = new Date(freeBusy.timeMin).getTime();
|
|
470
|
+
candidate + duration <= queryEnd && slots.length < limit;
|
|
471
|
+
candidate += step
|
|
472
|
+
) {
|
|
473
|
+
const candidateEnd = candidate + duration
|
|
474
|
+
if (
|
|
475
|
+
!busy.some(
|
|
476
|
+
(period) => candidate < period.end && candidateEnd > period.start,
|
|
477
|
+
)
|
|
478
|
+
) {
|
|
479
|
+
slots.push({
|
|
480
|
+
durationMinutes,
|
|
481
|
+
end: new Date(candidateEnd).toISOString(),
|
|
482
|
+
start: new Date(candidate).toISOString(),
|
|
483
|
+
})
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
return slots
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Builds the provider event body shared by create, update, and import actions.
|
|
491
|
+
*
|
|
492
|
+
* @param input - Public event fields.
|
|
493
|
+
*/
|
|
494
|
+
export function toEventResource(
|
|
495
|
+
input: z.infer<typeof EVENT_DETAILS_INPUT_SCHEMA>,
|
|
496
|
+
): calendar_v3.Schema$Event {
|
|
497
|
+
return {
|
|
498
|
+
attachments: input.attachments,
|
|
499
|
+
attendees: input.attendees,
|
|
500
|
+
colorId: input.colorId,
|
|
501
|
+
description: input.description,
|
|
502
|
+
...(input.schedule ? toEventDateTimes(input.schedule) : {}),
|
|
503
|
+
extendedProperties: input.extendedProperties,
|
|
504
|
+
guestsCanInviteOthers: input.guestsCanInviteOthers,
|
|
505
|
+
guestsCanModify: input.guestsCanModify,
|
|
506
|
+
guestsCanSeeOtherGuests: input.guestsCanSeeOtherGuests,
|
|
507
|
+
location: input.location,
|
|
508
|
+
recurrence: input.recurrence,
|
|
509
|
+
reminders: input.reminders,
|
|
510
|
+
summary: input.summary,
|
|
511
|
+
transparency: input.transparency,
|
|
512
|
+
visibility: input.visibility,
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Adds whole UTC days to an ISO calendar date.
|
|
518
|
+
*
|
|
519
|
+
* @param date - ISO calendar date.
|
|
520
|
+
* @param days - Number of days to add.
|
|
521
|
+
*/
|
|
522
|
+
function addDays(date: string, days: number) {
|
|
523
|
+
const value = new Date(`${date}T00:00:00.000Z`)
|
|
524
|
+
value.setUTCDate(value.getUTCDate() + days)
|
|
525
|
+
return value.toISOString().slice(0, 10)
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Normalizes an all-day or timed provider event boundary.
|
|
530
|
+
*
|
|
531
|
+
* @param value - Provider event date-time.
|
|
532
|
+
*/
|
|
533
|
+
function normalizeEventTime(value: calendar_v3.Schema$EventDateTime) {
|
|
534
|
+
return {
|
|
535
|
+
date: value.date ?? undefined,
|
|
536
|
+
dateTime: value.dateTime ?? undefined,
|
|
537
|
+
timeZone: value.timeZone ?? undefined,
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Normalizes an event creator or organizer.
|
|
543
|
+
*
|
|
544
|
+
* @param value - Provider event principal.
|
|
545
|
+
*/
|
|
546
|
+
function normalizePrincipal(value: calendar_v3.Schema$Event["creator"]) {
|
|
547
|
+
return value
|
|
548
|
+
? {
|
|
549
|
+
displayName: value.displayName ?? undefined,
|
|
550
|
+
email: value.email ?? undefined,
|
|
551
|
+
self: value.self ?? false,
|
|
552
|
+
}
|
|
553
|
+
: undefined
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Normalizes event conference entry points and creation state.
|
|
558
|
+
*
|
|
559
|
+
* @param value - Provider conference data.
|
|
560
|
+
*/
|
|
561
|
+
function normalizeConference(
|
|
562
|
+
value: calendar_v3.Schema$ConferenceData | null | undefined,
|
|
563
|
+
) {
|
|
564
|
+
if (!value) return undefined
|
|
565
|
+
return {
|
|
566
|
+
conferenceId: value.conferenceId ?? undefined,
|
|
567
|
+
creationStatus: normalizeConferenceStatus(
|
|
568
|
+
value.createRequest?.status?.statusCode,
|
|
569
|
+
),
|
|
570
|
+
entryPoints: (value.entryPoints ?? []).flatMap((entryPoint) =>
|
|
571
|
+
entryPoint.entryPointType && entryPoint.uri
|
|
572
|
+
? [
|
|
573
|
+
{
|
|
574
|
+
label: entryPoint.label ?? undefined,
|
|
575
|
+
passcode:
|
|
576
|
+
entryPoint.passcode ??
|
|
577
|
+
entryPoint.password ??
|
|
578
|
+
entryPoint.pin ??
|
|
579
|
+
undefined,
|
|
580
|
+
type: entryPoint.entryPointType,
|
|
581
|
+
uri: entryPoint.uri,
|
|
582
|
+
},
|
|
583
|
+
]
|
|
584
|
+
: [],
|
|
585
|
+
),
|
|
586
|
+
name: value.conferenceSolution?.name ?? undefined,
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* Narrows a provider conference creation status.
|
|
592
|
+
*
|
|
593
|
+
* @param value - Provider status.
|
|
594
|
+
*/
|
|
595
|
+
function normalizeConferenceStatus(
|
|
596
|
+
value: string | null | undefined,
|
|
597
|
+
): "pending" | "success" | "failure" | undefined {
|
|
598
|
+
return value === "pending" || value === "success" || value === "failure"
|
|
599
|
+
? value
|
|
600
|
+
: undefined
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* Narrows an attendee response status.
|
|
605
|
+
*
|
|
606
|
+
* @param value - Provider response status.
|
|
607
|
+
*/
|
|
608
|
+
function normalizeResponseStatus(value: string | null | undefined) {
|
|
609
|
+
return value === "declined" || value === "tentative" || value === "accepted"
|
|
610
|
+
? value
|
|
611
|
+
: "needsAction"
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* Narrows a reminder delivery method.
|
|
616
|
+
*
|
|
617
|
+
* @param value - Provider reminder method.
|
|
618
|
+
*/
|
|
619
|
+
function normalizeReminderMethod(value: string) {
|
|
620
|
+
return value === "email" ? "email" : "popup"
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* Narrows an event lifecycle status.
|
|
625
|
+
*
|
|
626
|
+
* @param value - Provider event status.
|
|
627
|
+
*/
|
|
628
|
+
function normalizeEventStatus(value: string | null | undefined) {
|
|
629
|
+
return value === "tentative" || value === "cancelled" ? value : "confirmed"
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* Narrows an event visibility.
|
|
634
|
+
*
|
|
635
|
+
* @param value - Provider visibility.
|
|
636
|
+
*/
|
|
637
|
+
function normalizeVisibility(value: string | null | undefined) {
|
|
638
|
+
return value === "public" || value === "private" || value === "confidential"
|
|
639
|
+
? value
|
|
640
|
+
: "default"
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* Narrows an ACL permission role.
|
|
645
|
+
*
|
|
646
|
+
* @param value - Provider ACL role.
|
|
647
|
+
*/
|
|
648
|
+
function normalizeAclRole(value: string | null | undefined) {
|
|
649
|
+
return value === "none" ||
|
|
650
|
+
value === "freeBusyReader" ||
|
|
651
|
+
value === "reader" ||
|
|
652
|
+
value === "writer"
|
|
653
|
+
? value
|
|
654
|
+
: "owner"
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Narrows an ACL scope type.
|
|
659
|
+
*
|
|
660
|
+
* @param value - Provider ACL scope type.
|
|
661
|
+
*/
|
|
662
|
+
function normalizeAclScope(value: string) {
|
|
663
|
+
return value === "default" || value === "group" || value === "domain"
|
|
664
|
+
? value
|
|
665
|
+
: "user"
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* Removes null values from a provider string record.
|
|
670
|
+
*
|
|
671
|
+
* @param value - Nullable provider record.
|
|
672
|
+
*/
|
|
673
|
+
function withoutNullValues(
|
|
674
|
+
value: Record<string, string | null> | null | undefined,
|
|
675
|
+
) {
|
|
676
|
+
return Object.fromEntries(
|
|
677
|
+
Object.entries(value ?? {}).flatMap(([key, item]) =>
|
|
678
|
+
item === null ? [] : [[key, item]],
|
|
679
|
+
),
|
|
680
|
+
)
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* Normalizes complete color definitions keyed by provider ID.
|
|
685
|
+
*
|
|
686
|
+
* @param definitions - Provider color definitions.
|
|
687
|
+
*/
|
|
688
|
+
function normalizeColorDefinitions(
|
|
689
|
+
definitions:
|
|
690
|
+
| Record<string, calendar_v3.Schema$ColorDefinition>
|
|
691
|
+
| null
|
|
692
|
+
| undefined,
|
|
693
|
+
) {
|
|
694
|
+
return Object.fromEntries(
|
|
695
|
+
Object.entries(definitions ?? {}).flatMap(([id, definition]) =>
|
|
696
|
+
definition.background && definition.foreground
|
|
697
|
+
? [
|
|
698
|
+
[
|
|
699
|
+
id,
|
|
700
|
+
{
|
|
701
|
+
background: definition.background,
|
|
702
|
+
foreground: definition.foreground,
|
|
703
|
+
},
|
|
704
|
+
],
|
|
705
|
+
]
|
|
706
|
+
: [],
|
|
707
|
+
),
|
|
708
|
+
)
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* Merges overlapping numeric time periods.
|
|
713
|
+
*
|
|
714
|
+
* @param periods - Busy periods expressed as epoch milliseconds.
|
|
715
|
+
*/
|
|
716
|
+
function mergePeriods(periods: { end: number; start: number }[]) {
|
|
717
|
+
return periods
|
|
718
|
+
.toSorted((left, right) => left.start - right.start)
|
|
719
|
+
.reduce<{ end: number; start: number }[]>((merged, period) => {
|
|
720
|
+
const previous = merged.at(-1)
|
|
721
|
+
if (!previous || period.start > previous.end) return [...merged, period]
|
|
722
|
+
return [
|
|
723
|
+
...merged.slice(0, -1),
|
|
724
|
+
{ end: Math.max(previous.end, period.end), start: previous.start },
|
|
725
|
+
]
|
|
726
|
+
}, [])
|
|
727
|
+
}
|