From eccbc4748a29a66b941ed678c96af74edfd4bd2f Mon Sep 17 00:00:00 2001 From: "tembo-io[bot]" <208362400+tembo-io[bot]@users.noreply.github.com> Date: Tue, 2 Sep 2025 11:10:02 +0000 Subject: [PATCH 1/2] fix(google-calendar): prevent timeRangeEmpty errors with validation - Add time range validation in events() and freeBusy() methods - Ensure timeMax is always after timeMin before API calls - Enhanced error logging for time range validation failures - Add context logging with calendar/schedule details for debugging --- apps/web/next-env.d.ts | 1 + bun.lock | 2 +- .../providers/calendars/google-calendar.ts | 52 +++++++++++++++++-- 3 files changed, 49 insertions(+), 6 deletions(-) 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..432829bf 100644 --- a/packages/api/src/providers/calendars/google-calendar.ts +++ b/packages/api/src/providers/calendars/google-calendar.ts @@ -100,9 +100,17 @@ export class GoogleCalendarProvider implements CalendarProvider { recurringMasterEvents: CalendarEvent[]; }> { 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 { items } = await this.client.calendars.events.list(calendar.id, { - timeMin: timeMin.withTimeZone("UTC").toInstant().toString(), - timeMax: timeMax.withTimeZone("UTC").toInstant().toString(), + timeMin: timeMinInstant.toString(), + timeMax: timeMaxInstant.toString(), singleEvents: CALENDAR_DEFAULTS.SINGLE_EVENTS, orderBy: CALENDAR_DEFAULTS.ORDER_BY, maxResults: CALENDAR_DEFAULTS.MAX_EVENTS_PER_CALENDAR, @@ -134,6 +142,11 @@ export class GoogleCalendarProvider implements CalendarProvider { ); return { events, recurringMasterEvents }; + }, { + calendarId: calendar.id, + timeMin: timeMin.toString(), + timeMax: timeMax.toString(), + timeZone }); } @@ -350,14 +363,26 @@ export class GoogleCalendarProvider implements CalendarProvider { timeMax: Temporal.ZonedDateTime, ): Promise { 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})`); + } + const response = await this.client.checkFreeBusy.checkFreeBusy({ - timeMin: timeMin.withTimeZone("UTC").toInstant().toString(), - timeMax: timeMax.withTimeZone("UTC").toInstant().toString(), + timeMin: timeMinInstant.toString(), + timeMax: timeMaxInstant.toString(), timeZone: "UTC", items: schedules.map((id) => ({ id })), }); return parseGoogleCalendarFreeBusy(response); + }, { + schedules, + timeMin: timeMin.toString(), + timeMax: timeMax.toString(), }); } @@ -369,7 +394,24 @@ 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); } From 8816806331d597a5b21088f378b3db77c298abde Mon Sep 17 00:00:00 2001 From: "tembo-io[bot]" <208362400+tembo-io[bot]@users.noreply.github.com> Date: Tue, 2 Sep 2025 11:13:24 +0000 Subject: [PATCH 2/2] style: format google-calendar.ts with prettier --- .../providers/calendars/google-calendar.ts | 150 ++++++++++-------- 1 file changed, 83 insertions(+), 67 deletions(-) diff --git a/packages/api/src/providers/calendars/google-calendar.ts b/packages/api/src/providers/calendars/google-calendar.ts index 432829bf..a938eec4 100644 --- a/packages/api/src/providers/calendars/google-calendar.ts +++ b/packages/api/src/providers/calendars/google-calendar.ts @@ -99,55 +99,61 @@ export class GoogleCalendarProvider implements CalendarProvider { events: CalendarEvent[]; recurringMasterEvents: CalendarEvent[]; }> { - 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})`); - } + 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 { 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, - }); + 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, + }); - 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 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!); + } - if (masters.size === 0) { - return { events, recurringMasterEvents: [] }; - } + if (masters.size === 0) { + return { events, recurringMasterEvents: [] }; + } - const recurringMasterEvents = await Promise.all( - Array.from(masters).map((id) => this.event(calendar, id, timeZone)), - ); + 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 - }); + return { events, recurringMasterEvents }; + }, + { + calendarId: calendar.id, + timeMin: timeMin.toString(), + timeMax: timeMax.toString(), + timeZone, + }, + ); } async event( @@ -362,28 +368,34 @@ export class GoogleCalendarProvider implements CalendarProvider { timeMin: Temporal.ZonedDateTime, timeMax: Temporal.ZonedDateTime, ): Promise { - 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 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})`, + ); + } - const response = await this.client.checkFreeBusy.checkFreeBusy({ - timeMin: timeMinInstant.toString(), - timeMax: timeMaxInstant.toString(), - timeZone: "UTC", - items: schedules.map((id) => ({ id })), - }); + 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(), - }); + return parseGoogleCalendarFreeBusy(response); + }, + { + schedules, + timeMin: timeMin.toString(), + timeMax: timeMax.toString(), + }, + ); } private async withErrorHandler( @@ -394,10 +406,14 @@ export class GoogleCalendarProvider implements CalendarProvider { try { return await Promise.resolve(fn()); } catch (error: unknown) { - const errorMessage = error instanceof Error ? error.message : String(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')) { + if ( + errorMessage.includes("timeRangeEmpty") || + errorMessage.includes("Invalid time range") + ) { console.error(`Time range validation failed in ${operation}:`, { error: errorMessage, context,