diff --git a/apps/web/next-env.d.ts b/apps/web/next-env.d.ts index 1b3be084..830fb594 100644 --- a/apps/web/next-env.d.ts +++ b/apps/web/next-env.d.ts @@ -1,5 +1,6 @@ /// /// +/// // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/bun.lock b/bun.lock index 39dfe2ec..8d028fc3 100644 --- a/bun.lock +++ b/bun.lock @@ -225,7 +225,7 @@ "devDependencies": { "@repo/typescript-config": "workspace:*", "@types/node": "^22.9.0", - "typescript": "^5.8.3", + "typescript": "^5.9.2", }, }, "packages/google-tasks": { diff --git a/packages/api/src/providers/calendars/google-calendar.ts b/packages/api/src/providers/calendars/google-calendar.ts index 38ca56c0..a938eec4 100644 --- a/packages/api/src/providers/calendars/google-calendar.ts +++ b/packages/api/src/providers/calendars/google-calendar.ts @@ -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([]); - - 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([]); + + 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( @@ -349,16 +368,34 @@ export class GoogleCalendarProvider implements CalendarProvider { timeMin: Temporal.ZonedDateTime, timeMax: Temporal.ZonedDateTime, ): Promise { - 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( @@ -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); }