Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/web/next-env.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference path="./.next/types/routes.d.ts" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
2 changes: 1 addition & 1 deletion bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

142 changes: 100 additions & 42 deletions packages/api/src/providers/calendars/google-calendar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,42 +99,61 @@ export class GoogleCalendarProvider implements CalendarProvider {
events: CalendarEvent[];
recurringMasterEvents: CalendarEvent[];
}> {
return this.withErrorHandler("events", async () => {
const { items } = await this.client.calendars.events.list(calendar.id, {
timeMin: timeMin.withTimeZone("UTC").toInstant().toString(),
timeMax: timeMax.withTimeZone("UTC").toInstant().toString(),
singleEvents: CALENDAR_DEFAULTS.SINGLE_EVENTS,
orderBy: CALENDAR_DEFAULTS.ORDER_BY,
maxResults: CALENDAR_DEFAULTS.MAX_EVENTS_PER_CALENDAR,
});
return this.withErrorHandler(
"events",
async () => {
// Validate time range to prevent empty time range errors
const timeMinInstant = timeMin.withTimeZone("UTC").toInstant();
const timeMaxInstant = timeMax.withTimeZone("UTC").toInstant();

if (Temporal.Instant.compare(timeMinInstant, timeMaxInstant) >= 0) {
throw new Error(
`Invalid time range: timeMax (${timeMaxInstant}) must be after timeMin (${timeMinInstant})`,
);
}

const events: CalendarEvent[] =
items?.map((event) =>
parseGoogleCalendarEvent({
calendar,
accountId: this.accountId,
event,
defaultTimeZone: timeZone ?? "UTC",
}),
) ?? [];

const instances = events.filter((e) => e.recurringEventId);
const masters = new Set<string>([]);

for (const instance of instances) {
masters.add(instance.recurringEventId!);
}
const { items } = await this.client.calendars.events.list(calendar.id, {
timeMin: timeMinInstant.toString(),
timeMax: timeMaxInstant.toString(),
singleEvents: CALENDAR_DEFAULTS.SINGLE_EVENTS,
orderBy: CALENDAR_DEFAULTS.ORDER_BY,
maxResults: CALENDAR_DEFAULTS.MAX_EVENTS_PER_CALENDAR,
});

if (masters.size === 0) {
return { events, recurringMasterEvents: [] };
}
const events: CalendarEvent[] =
items?.map((event) =>
parseGoogleCalendarEvent({
calendar,
accountId: this.accountId,
event,
defaultTimeZone: timeZone ?? "UTC",
}),
) ?? [];

const instances = events.filter((e) => e.recurringEventId);
const masters = new Set<string>([]);

for (const instance of instances) {
masters.add(instance.recurringEventId!);
}

const recurringMasterEvents = await Promise.all(
Array.from(masters).map((id) => this.event(calendar, id, timeZone)),
);
if (masters.size === 0) {
return { events, recurringMasterEvents: [] };
}

return { events, recurringMasterEvents };
});
const recurringMasterEvents = await Promise.all(
Array.from(masters).map((id) => this.event(calendar, id, timeZone)),
);

return { events, recurringMasterEvents };
},
{
calendarId: calendar.id,
timeMin: timeMin.toString(),
timeMax: timeMax.toString(),
timeZone,
},
);
}

async event(
Expand Down Expand Up @@ -349,16 +368,34 @@ export class GoogleCalendarProvider implements CalendarProvider {
timeMin: Temporal.ZonedDateTime,
timeMax: Temporal.ZonedDateTime,
): Promise<CalendarFreeBusy[]> {
return this.withErrorHandler("freeBusy", async () => {
const response = await this.client.checkFreeBusy.checkFreeBusy({
timeMin: timeMin.withTimeZone("UTC").toInstant().toString(),
timeMax: timeMax.withTimeZone("UTC").toInstant().toString(),
timeZone: "UTC",
items: schedules.map((id) => ({ id })),
});
return this.withErrorHandler(
"freeBusy",
async () => {
// Validate time range to prevent empty time range errors
const timeMinInstant = timeMin.withTimeZone("UTC").toInstant();
const timeMaxInstant = timeMax.withTimeZone("UTC").toInstant();

if (Temporal.Instant.compare(timeMinInstant, timeMaxInstant) >= 0) {
throw new Error(
`Invalid time range: timeMax (${timeMaxInstant}) must be after timeMin (${timeMinInstant})`,
);
}

return parseGoogleCalendarFreeBusy(response);
});
const response = await this.client.checkFreeBusy.checkFreeBusy({
timeMin: timeMinInstant.toString(),
timeMax: timeMaxInstant.toString(),
timeZone: "UTC",
items: schedules.map((id) => ({ id })),
});

return parseGoogleCalendarFreeBusy(response);
},
{
schedules,
timeMin: timeMin.toString(),
timeMax: timeMax.toString(),
},
);
}

private async withErrorHandler<T>(
Expand All @@ -369,7 +406,28 @@ export class GoogleCalendarProvider implements CalendarProvider {
try {
return await Promise.resolve(fn());
} catch (error: unknown) {
console.error(`Failed to ${operation}:`, error);
const errorMessage =
error instanceof Error ? error.message : String(error);

// Enhanced logging for time range errors
if (
errorMessage.includes("timeRangeEmpty") ||
errorMessage.includes("Invalid time range")
) {
console.error(`Time range validation failed in ${operation}:`, {
error: errorMessage,
context,
operation,
accountId: this.accountId,
});
} else {
console.error(`Failed to ${operation}:`, {
error: errorMessage,
context,
operation,
accountId: this.accountId,
});
}

throw new ProviderError(error as Error, operation, context);
}
Expand Down