From fabc51468ef7cdb8c684c82b16449fd73f74655a Mon Sep 17 00:00:00 2001 From: "Jean P.D. Meijer" Date: Tue, 2 Dec 2025 00:23:58 +0100 Subject: [PATCH 1/3] wip --- apps/web/src/components/calendar-view.tsx | 4 +- .../context/calendar-colors-provider.tsx | 4 +- .../calendar/event/event-context-menu.tsx | 11 +- .../components/calendar/event/event-item.tsx | 2 +- .../delete-event/delete-queue-provider.tsx | 3 +- .../calendar/flows/update-event/utils.ts | 30 +--- .../calendar-delete-dialog.tsx | 2 +- .../calendar-picker-item-toggle.tsx | 2 +- .../calendar-picker/calendar-picker-item.tsx | 10 +- .../calendar-picker/calendar-picker.tsx | 8 +- .../calendar-rename-dialog.tsx | 2 +- .../calendar/hooks/use-default-calendar.ts | 2 +- .../calendar/hooks/use-event-mutations.ts | 3 +- .../hooks/use-optimistic-mutations.ts | 7 +- .../web/src/components/calendar/utils/move.ts | 4 +- .../components/command-bar/context-view.tsx | 2 +- .../command-menu/event-search-commands.tsx | 2 +- .../event-form/fields/calendar-field.tsx | 24 +-- .../components/event-form/utils/defaults.ts | 13 +- .../src/components/event-form/utils/schema.ts | 8 +- .../event-form/utils/transform/input.ts | 13 +- .../event-form/utils/transform/output.ts | 4 +- .../event-form/utils/use-event-form.ts | 2 +- .../tabs/accounts/connected-accounts-list.tsx | 6 +- apps/web/src/lib/db.ts | 82 +++++++-- packages/api/src/routers/accounts.ts | 14 +- packages/api/src/routers/calendars.ts | 33 ++-- packages/api/src/routers/conferencing.ts | 7 +- packages/api/src/routers/events.ts | 167 +++++++++--------- packages/api/src/routers/tasks.ts | 13 +- packages/api/src/utils/index.ts | 9 +- .../src/calendars/google-calendar.ts | 30 ++-- .../calendars/google-calendar/calendars.ts | 11 +- .../src/calendars/google-calendar/events.ts | 11 +- .../src/calendars/microsoft-calendar.ts | 44 ++--- .../calendars/microsoft-calendar/calendars.ts | 11 +- .../calendars/microsoft-calendar/events.ts | 9 +- .../providers/src/conferencing/google-meet.ts | 8 +- packages/providers/src/conferencing/zoom.ts | 2 +- packages/providers/src/index.ts | 16 +- .../providers/src/interfaces/calendars.ts | 11 +- packages/providers/src/interfaces/events.ts | 17 +- .../interfaces/providers/provider-config.ts | 2 +- packages/providers/src/interfaces/tasks.ts | 4 +- packages/providers/src/tasks/google-tasks.ts | 20 +-- .../providers/src/tasks/google-tasks/utils.ts | 6 +- packages/schemas/src/calendars.ts | 8 +- packages/schemas/src/events.ts | 18 +- packages/schemas/src/tasks.ts | 4 +- 49 files changed, 382 insertions(+), 343 deletions(-) diff --git a/apps/web/src/components/calendar-view.tsx b/apps/web/src/components/calendar-view.tsx index b31a3527..73336bbd 100644 --- a/apps/web/src/components/calendar-view.tsx +++ b/apps/web/src/components/calendar-view.tsx @@ -70,8 +70,8 @@ function CalendarContent({ scrollContainerRef }: CalendarContentProps) { return pastFiltered.filter((eventItem) => { const preference = getCalendarPreference( calendarPreferences, - eventItem.event.accountId, - eventItem.event.calendarId, + eventItem.event.calendar.provider.accountId, + eventItem.event.calendar.id, ); return !(preference?.hidden === true); diff --git a/apps/web/src/components/calendar/context/calendar-colors-provider.tsx b/apps/web/src/components/calendar/context/calendar-colors-provider.tsx index d672f772..173b40fb 100644 --- a/apps/web/src/components/calendar/context/calendar-colors-provider.tsx +++ b/apps/web/src/components/calendar/context/calendar-colors-provider.tsx @@ -26,12 +26,12 @@ export function CalendarColorsProvider({ for (const calendar of calendars) { const preference = getCalendarPreference( calendarPreferences, - calendar.accountId, + calendar.provider.accountId, calendar.id, ); document.documentElement.style.setProperty( - calendarColorVariable(calendar.accountId, calendar.id), + calendarColorVariable(calendar.provider.accountId, calendar.id), preference?.color ?? calendar.color ?? "var(--color-muted-foreground)", ); } diff --git a/apps/web/src/components/calendar/event/event-context-menu.tsx b/apps/web/src/components/calendar/event/event-context-menu.tsx index 6dce8413..1c903fc1 100644 --- a/apps/web/src/components/calendar/event/event-context-menu.tsx +++ b/apps/web/src/components/calendar/event/event-context-menu.tsx @@ -76,12 +76,11 @@ function EventContextMenuCalendarList({ const updateAction = usePartialUpdateAction(); const moveEvent = React.useCallback( - (accountId: string, calendarId: string) => { + (calendar: { id: string; provider: { id: "google" | "microsoft"; accountId: string } }) => { updateAction({ changes: { id: event.id, - accountId, - calendarId, + calendar, type: event.type, }, notify: true, @@ -98,14 +97,14 @@ function EventContextMenuCalendarList({ moveEvent(calendar.accountId, calendar.id)} + onSelect={() => moveEvent({ id: calendar.id, provider: calendar.provider })} /> @@ -158,7 +157,7 @@ export function EventContextMenu({ event, children }: EventContextMenuProps) { {children} - + diff --git a/apps/web/src/components/calendar/event/event-item.tsx b/apps/web/src/components/calendar/event/event-item.tsx index 5cb0787e..2fa8cfb5 100644 --- a/apps/web/src/components/calendar/event/event-item.tsx +++ b/apps/web/src/components/calendar/event/event-item.tsx @@ -124,7 +124,7 @@ export function EventItem({ const color = item.event.color ?? - `var(${calendarColorVariable(item.event.accountId, item.event.calendarId)}, var(--color-muted-foreground))`; + `var(${calendarColorVariable(item.event.calendar.provider.accountId, item.event.calendar.id)}, var(--color-muted-foreground))`; if (view === "month") { return ( diff --git a/apps/web/src/components/calendar/flows/delete-event/delete-queue-provider.tsx b/apps/web/src/components/calendar/flows/delete-event/delete-queue-provider.tsx index fad003ed..50630ef9 100644 --- a/apps/web/src/components/calendar/flows/delete-event/delete-queue-provider.tsx +++ b/apps/web/src/components/calendar/flows/delete-event/delete-queue-provider.tsx @@ -41,8 +41,7 @@ export function DeleteQueueProvider({ children }: DeleteQueueProviderProps) { deleteMutation.mutate( { - accountId: item.event.accountId, - calendarId: item.event.calendarId, + calendar: item.event.calendar, eventId, sendUpdate: item.notify, }, diff --git a/apps/web/src/components/calendar/flows/update-event/utils.ts b/apps/web/src/components/calendar/flows/update-event/utils.ts index 74637ca8..0f764c7d 100644 --- a/apps/web/src/components/calendar/flows/update-event/utils.ts +++ b/apps/web/src/components/calendar/flows/update-event/utils.ts @@ -47,8 +47,8 @@ export function isMovedBetweenCalendars( previous: CalendarEvent, ) { return ( - updated.accountId !== previous.accountId || - updated.calendarId !== previous.calendarId + updated.calendar.provider.accountId !== previous.calendar.provider.accountId || + updated.calendar.id !== previous.calendar.id ); } @@ -76,8 +76,7 @@ export function buildUpdateEvent( ...event, ...(isCalendarChanged ? { - accountId: previous.accountId, - calendarId: previous.calendarId, + calendar: previous.calendar, } : {}), ...(options.sendUpdate @@ -92,14 +91,8 @@ export function buildUpdateEvent( ...(isCalendarChanged ? { move: { - source: { - accountId: previous.accountId, - calendarId: previous.calendarId, - }, - destination: { - accountId: event.accountId, - calendarId: event.calendarId, - }, + source: previous.calendar, + destination: event.calendar, }, } : {}), @@ -122,8 +115,7 @@ export function buildUpdateSeries( ...event, ...(isCalendarChanged ? { - accountId: previous.accountId, - calendarId: previous.calendarId, + calendar: previous.calendar, } : {}), ...(options.sendUpdate @@ -140,14 +132,8 @@ export function buildUpdateSeries( ...(isCalendarChanged ? { move: { - source: { - accountId: previous.accountId, - calendarId: previous.calendarId, - }, - destination: { - accountId: event.accountId, - calendarId: event.calendarId, - }, + source: previous.calendar, + destination: event.calendar, }, } : {}), diff --git a/apps/web/src/components/calendar/header/calendar-picker/calendar-delete-dialog.tsx b/apps/web/src/components/calendar/header/calendar-picker/calendar-delete-dialog.tsx index aff087e0..6503291c 100644 --- a/apps/web/src/components/calendar/header/calendar-picker/calendar-delete-dialog.tsx +++ b/apps/web/src/components/calendar/header/calendar-picker/calendar-delete-dialog.tsx @@ -44,7 +44,7 @@ export function CalendarDeleteDialog() { const onDelete = () => { mutate({ - accountId: calendar.accountId, + provider: calendar.provider, calendarId: calendar.id, }); }; diff --git a/apps/web/src/components/calendar/header/calendar-picker/calendar-picker-item-toggle.tsx b/apps/web/src/components/calendar/header/calendar-picker/calendar-picker-item-toggle.tsx index b13c2240..058a29b4 100644 --- a/apps/web/src/components/calendar/header/calendar-picker/calendar-picker-item-toggle.tsx +++ b/apps/web/src/components/calendar/header/calendar-picker/calendar-picker-item-toggle.tsx @@ -24,7 +24,7 @@ export function CalendarPickerItemToggle({ data-slot="checkbox" style={ { - "--calendar-color": `var(${calendarColorVariable(calendar.accountId, calendar.id)}, var(--color-muted-foreground))`, + "--calendar-color": `var(${calendarColorVariable(calendar.provider.accountId, calendar.id)}, var(--color-muted-foreground))`, } as React.CSSProperties } className={cn( diff --git a/apps/web/src/components/calendar/header/calendar-picker/calendar-picker-item.tsx b/apps/web/src/components/calendar/header/calendar-picker/calendar-picker-item.tsx index 9fcd200a..1a8336d8 100644 --- a/apps/web/src/components/calendar/header/calendar-picker/calendar-picker-item.tsx +++ b/apps/web/src/components/calendar/header/calendar-picker/calendar-picker-item.tsx @@ -87,7 +87,7 @@ function CalendarColorPicker() { const currentPreference = getCalendarPreference( calendarPreferences, - calendar.accountId, + calendar.provider.accountId, calendar.id, ); @@ -95,7 +95,7 @@ function CalendarColorPicker() { const onColorChange = (newColor: string) => { setCalendarPreferences((prev) => - setCalendarPreference(prev, calendar.accountId, calendar.id, { + setCalendarPreference(prev, calendar.provider.accountId, calendar.id, { color: newColor, }), ); @@ -126,7 +126,7 @@ function useCalendarVisibility() { const preference = getCalendarPreference( calendarPreferences, - calendar.accountId, + calendar.provider.accountId, calendar.id, ); @@ -135,12 +135,12 @@ function useCalendarVisibility() { const setVisible = React.useCallback( (visible: boolean) => { setCalendarPreferences((prev) => - setCalendarPreference(prev, calendar.accountId, calendar.id, { + setCalendarPreference(prev, calendar.provider.accountId, calendar.id, { hidden: !visible, }), ); }, - [setCalendarPreferences, calendar.accountId, calendar.id], + [setCalendarPreferences, calendar.provider.accountId, calendar.id], ); return { visible, setVisible }; diff --git a/apps/web/src/components/calendar/header/calendar-picker/calendar-picker.tsx b/apps/web/src/components/calendar/header/calendar-picker/calendar-picker.tsx index 4a141938..bc076165 100644 --- a/apps/web/src/components/calendar/header/calendar-picker/calendar-picker.tsx +++ b/apps/web/src/components/calendar/header/calendar-picker/calendar-picker.tsx @@ -52,7 +52,7 @@ function VisibleCalendarItem({ )} style={ { - "--calendar-color": `var(${calendarColorVariable(calendar.accountId, calendar.id)}, var(--color-muted-foreground))`, + "--calendar-color": `var(${calendarColorVariable(calendar.provider.accountId, calendar.id)}, var(--color-muted-foreground))`, } as React.CSSProperties } /> @@ -75,7 +75,7 @@ function VisibleCalendars({ className, calendars }: VisibleCalendarProps) {
{calendars.slice(0, Math.min(calendars.length, 3)).map((calendar) => ( ))} @@ -96,7 +96,7 @@ function CalendarPickerContent() { .filter((calendar) => { const preference = getCalendarPreference( calendarPreferences, - calendar.accountId, + calendar.provider.accountId, calendar.id, ); return !preference?.hidden; @@ -154,7 +154,7 @@ function CalendarPickerContent() { {account.calendars.map((calendar) => ( diff --git a/apps/web/src/components/calendar/header/calendar-picker/calendar-rename-dialog.tsx b/apps/web/src/components/calendar/header/calendar-picker/calendar-rename-dialog.tsx index fd7a2309..52b236b1 100644 --- a/apps/web/src/components/calendar/header/calendar-picker/calendar-rename-dialog.tsx +++ b/apps/web/src/components/calendar/header/calendar-picker/calendar-rename-dialog.tsx @@ -49,7 +49,7 @@ export function CalendarRenameDialog() { const onSubmit = () => { mutate({ id: calendar.id, - accountId: calendar.accountId, + provider: calendar.provider, name: name.trim(), }); }; diff --git a/apps/web/src/components/calendar/hooks/use-default-calendar.ts b/apps/web/src/components/calendar/hooks/use-default-calendar.ts index 073006c5..d799dc7a 100644 --- a/apps/web/src/components/calendar/hooks/use-default-calendar.ts +++ b/apps/web/src/components/calendar/hooks/use-default-calendar.ts @@ -12,7 +12,7 @@ export function useDefaultCalendar() { return "var(--color-muted-foreground)"; } - return `var(${calendarColorVariable(data?.defaultCalendar?.accountId, data?.defaultCalendar?.id)}, var(--color-muted-foreground))`; + return `var(${calendarColorVariable(data?.defaultCalendar?.provider.accountId, data?.defaultCalendar?.id)}, var(--color-muted-foreground))`; }, [data?.defaultCalendar]); return React.useMemo(() => { diff --git a/apps/web/src/components/calendar/hooks/use-event-mutations.ts b/apps/web/src/components/calendar/hooks/use-event-mutations.ts index c4d7f33a..5194b7b5 100644 --- a/apps/web/src/components/calendar/hooks/use-event-mutations.ts +++ b/apps/web/src/components/calendar/hooks/use-event-mutations.ts @@ -75,8 +75,7 @@ export function useUpdateEventMutation() { ...data, ...(move?.destination ? { - accountId: move.destination.accountId, - calendarId: move.destination.calendarId, + calendar: move.destination, } : {}), }; diff --git a/apps/web/src/components/calendar/hooks/use-optimistic-mutations.ts b/apps/web/src/components/calendar/hooks/use-optimistic-mutations.ts index b1e7a37b..d23724bf 100644 --- a/apps/web/src/components/calendar/hooks/use-optimistic-mutations.ts +++ b/apps/web/src/components/calendar/hooks/use-optimistic-mutations.ts @@ -40,9 +40,10 @@ export function useCreateDraftAction() { ...event, type: "draft", readOnly: false, - providerId: defaultCalendar.providerId, - accountId: defaultCalendar.accountId, - calendarId: defaultCalendar.id, + calendar: { + id: defaultCalendar.id, + provider: defaultCalendar.provider, + }, }, }); diff --git a/apps/web/src/components/calendar/utils/move.ts b/apps/web/src/components/calendar/utils/move.ts index 2ce061f0..16731fbb 100644 --- a/apps/web/src/components/calendar/utils/move.ts +++ b/apps/web/src/components/calendar/utils/move.ts @@ -5,8 +5,8 @@ export function canMoveBetweenCalendars( destination: Calendar, ): boolean { const isSameCalendar = - event.accountId === destination.accountId && - event.calendarId === destination.id; + event.calendar.provider.accountId === destination.provider.accountId && + event.calendar.id === destination.id; if (isSameCalendar) { return false; diff --git a/apps/web/src/components/command-bar/context-view.tsx b/apps/web/src/components/command-bar/context-view.tsx index dbb83b91..70fc2a7e 100644 --- a/apps/web/src/components/command-bar/context-view.tsx +++ b/apps/web/src/components/command-bar/context-view.tsx @@ -18,7 +18,7 @@ import { format, formatTime } from "@/lib/utils/format"; function eventColor(event: CalendarEvent) { return { - "--calendar-color": `var(${calendarColorVariable(event.accountId, event.calendarId)}, var(--color-muted-foreground))`, + "--calendar-color": `var(${calendarColorVariable(event.calendar.provider.accountId, event.calendar.id)}, var(--color-muted-foreground))`, } as React.CSSProperties; } diff --git a/apps/web/src/components/command-menu/event-search-commands.tsx b/apps/web/src/components/command-menu/event-search-commands.tsx index cfbef4eb..01180931 100644 --- a/apps/web/src/components/command-menu/event-search-commands.tsx +++ b/apps/web/src/components/command-menu/event-search-commands.tsx @@ -45,7 +45,7 @@ export function EventSearchCommands() { const color = item.event.color ?? - `var(${calendarColorVariable(item.event.accountId, item.event.calendarId)}, var(--color-muted-foreground))`; + `var(${calendarColorVariable(item.event.calendar.provider.accountId, item.event.calendar.id)}, var(--color-muted-foreground))`; return { ...item, diff --git a/apps/web/src/components/event-form/fields/calendar-field.tsx b/apps/web/src/components/event-form/fields/calendar-field.tsx index de088ff0..679848ef 100644 --- a/apps/web/src/components/event-form/fields/calendar-field.tsx +++ b/apps/web/src/components/event-form/fields/calendar-field.tsx @@ -1,6 +1,8 @@ import * as React from "react"; import { useQuery } from "@tanstack/react-query"; +import type { CalendarEventCalendar } from "@repo/providers/interfaces"; + import { Button } from "@/components/ui/button"; import { Command, @@ -24,8 +26,8 @@ import { cn } from "@/lib/utils"; interface CalendarFieldProps { id: string; className?: string; - value: { accountId: string; calendarId: string }; - onChange: (calendar: { accountId: string; calendarId: string }) => void; + value: CalendarEventCalendar; + onChange: (calendar: CalendarEventCalendar) => void; onBlur: () => void; disabled?: boolean; } @@ -47,7 +49,7 @@ export function CalendarField({ const onSelect = React.useCallback( (calendar: Calendar) => { - onChange({ accountId: calendar.accountId, calendarId: calendar.id }); + onChange({ id: calendar.id, provider: calendar.provider }); onBlur(); }, [onChange, onBlur], @@ -56,7 +58,7 @@ export function CalendarField({ const selected = React.useMemo(() => { return data?.accounts .flatMap((item) => item.calendars) - .find((item) => item.id === value.calendarId); + .find((item) => item.id === value.id); }, [data, value]); return ( @@ -77,7 +79,7 @@ export function CalendarField({ @@ -88,7 +90,7 @@ export function CalendarField({ interface CalendarColorIndicatorProps { primary: boolean; calendarId: string; - accountId: string; + providerAccountId: string; className?: string; disabled?: boolean; } @@ -96,11 +98,11 @@ interface CalendarColorIndicatorProps { export function CalendarColorIndicator({ primary, calendarId, - accountId, + providerAccountId, className, disabled, }: CalendarColorIndicatorProps) { - const color = `var(${calendarColorVariable(accountId, calendarId)}, var(--color-muted-foreground))`; + const color = `var(${calendarColorVariable(providerAccountId, calendarId)}, var(--color-muted-foreground))`; return (
@@ -131,7 +133,7 @@ export function CalendarListPickerItem({ calendar, onSelect, }: CalendarListPickerItemProps) { - const canMove = !calendar.readOnly && calendar.providerId === "google"; + const canMove = !calendar.readOnly && calendar.provider.id === "google"; return ( {calendar.name} @@ -190,7 +192,7 @@ export function CalendarListPicker({ > {account.calendars.map((calendar) => ( diff --git a/apps/web/src/components/event-form/utils/defaults.ts b/apps/web/src/components/event-form/utils/defaults.ts index cad0f17b..75f543e5 100644 --- a/apps/web/src/components/event-form/utils/defaults.ts +++ b/apps/web/src/components/event-form/utils/defaults.ts @@ -29,11 +29,13 @@ export const initialValues: FormValues = { recurringEventId: undefined, attendees: [], calendar: { - accountId: "", - calendarId: "", + id: "", + provider: { + id: "google", + accountId: "", + }, }, conference: undefined, - providerId: "google", visibility: "default", }; @@ -66,10 +68,9 @@ export function getDefaultValues({ availability: "busy", attendees: [], calendar: { - accountId: defaultCalendar.accountId, - calendarId: defaultCalendar.id, + id: defaultCalendar.id, + provider: defaultCalendar.provider, }, - providerId: defaultCalendar.providerId, visibility: "default", }; } diff --git a/apps/web/src/components/event-form/utils/schema.ts b/apps/web/src/components/event-form/utils/schema.ts index 86813200..330bfb8e 100644 --- a/apps/web/src/components/event-form/utils/schema.ts +++ b/apps/web/src/components/event-form/utils/schema.ts @@ -1,7 +1,7 @@ import { zZonedDateTimeInstance } from "temporal-zod"; import * as z from "zod"; -import { recurrenceSchema } from "@repo/schemas"; +import { eventCalendarSchema, recurrenceSchema } from "@repo/schemas"; export const conferenceEntryPointSchema = z.object({ joinUrl: z.object({ @@ -61,13 +61,9 @@ export const formSchema = z.object({ recurrence: recurrenceSchema.nullable().optional(), recurringEventId: z.string().optional(), description: z.string(), - calendar: z.object({ - accountId: z.string(), - calendarId: z.string(), - }), + calendar: eventCalendarSchema, attendees: z.array(attendeeSchema), conference: conferenceSchema.nullable().optional(), - providerId: z.enum(["google", "microsoft"]), visibility: z.enum(["default", "public", "private", "confidential"]), }); diff --git a/apps/web/src/components/event-form/utils/transform/input.ts b/apps/web/src/components/event-form/utils/transform/input.ts index 241af927..86696d48 100644 --- a/apps/web/src/components/event-form/utils/transform/input.ts +++ b/apps/web/src/components/event-form/utils/transform/input.ts @@ -70,11 +70,10 @@ export function parseDraftEvent({ recurrence: event.recurrence, recurringEventId: event.recurringEventId, attendees: parseAttendees(event), - calendar: { - accountId: event?.accountId ?? defaultCalendar.accountId, - calendarId: event?.calendarId ?? defaultCalendar.id, + calendar: event?.calendar ?? { + id: defaultCalendar.id, + provider: defaultCalendar.provider, }, - providerId: event?.providerId ?? defaultCalendar.providerId, conference: event.conference, visibility: event.visibility ?? "default", }; @@ -106,11 +105,7 @@ export function parseCalendarEvent({ recurrence: event.recurrence, recurringEventId: event.recurringEventId, attendees: parseAttendees(event), - calendar: { - accountId: event.accountId ?? "", - calendarId: event.calendarId ?? "", - }, - providerId: event.providerId, + calendar: event.calendar, conference: event.conference, visibility: event.visibility ?? "default", }; diff --git a/apps/web/src/components/event-form/utils/transform/output.ts b/apps/web/src/components/event-form/utils/transform/output.ts index 41249525..d5050359 100644 --- a/apps/web/src/components/event-form/utils/transform/output.ts +++ b/apps/web/src/components/event-form/utils/transform/output.ts @@ -32,9 +32,7 @@ export function toCalendarEvent({ description: values.description, availability: values.availability, allDay: values.isAllDay, - calendarId: values.calendar.calendarId, - accountId: values.calendar.accountId, - providerId: values.providerId, + calendar: values.calendar, start: values.isAllDay ? values.start.toPlainDate() : values.start, end: values.isAllDay ? values.end.toPlainDate() : values.end, readOnly: event?.readOnly ?? false, diff --git a/apps/web/src/components/event-form/utils/use-event-form.ts b/apps/web/src/components/event-form/utils/use-event-form.ts index 6f210870..51be73ce 100644 --- a/apps/web/src/components/event-form/utils/use-event-form.ts +++ b/apps/web/src/components/event-form/utils/use-event-form.ts @@ -142,7 +142,7 @@ export function useEventForm() { ]); React.useEffect(() => { - if (!defaultCalendar || form.state.values.calendar.calendarId !== "") { + if (!defaultCalendar || form.state.values.calendar.id !== "") { return; } diff --git a/apps/web/src/components/settings-dialog/tabs/accounts/connected-accounts-list.tsx b/apps/web/src/components/settings-dialog/tabs/accounts/connected-accounts-list.tsx index 2cdcf2fb..52b0f6a2 100644 --- a/apps/web/src/components/settings-dialog/tabs/accounts/connected-accounts-list.tsx +++ b/apps/web/src/components/settings-dialog/tabs/accounts/connected-accounts-list.tsx @@ -79,15 +79,15 @@ interface AccountListItemProps { function AccountListItem({ account }: AccountListItemProps) { return (
  • - +

    {account.email}

  • ); diff --git a/apps/web/src/lib/db.ts b/apps/web/src/lib/db.ts index f05fe5f5..95583fd7 100644 --- a/apps/web/src/lib/db.ts +++ b/apps/web/src/lib/db.ts @@ -4,16 +4,21 @@ import { useLiveQuery } from "dexie-react-hooks"; import { SuperJSONResult } from "superjson"; import { Temporal } from "temporal-polyfill"; +import type { ProviderId } from "@repo/providers/interfaces"; import { startOfDay } from "@repo/temporal"; import { useZonedDateTime } from "@/components/calendar/context/datetime-provider"; import { Calendar, CalendarEvent } from "./interfaces"; import { superjson } from "./trpc/superjson"; -export interface EventRow extends Omit< - CalendarEvent, - "start" | "end" | "createdAt" | "updatedAt" -> { +export interface EventRow + extends Omit< + CalendarEvent, + "start" | "end" | "createdAt" | "updatedAt" | "calendar" + > { + calendarId: string; + providerId: ProviderId; + providerAccountId: string; start: SuperJSONResult; end: SuperJSONResult; createdAt: SuperJSONResult | undefined; @@ -22,14 +27,19 @@ export interface EventRow extends Omit< endUnix: number; } +export interface CalendarRow extends Omit { + providerId: ProviderId; + providerAccountId: string; +} + export class Database extends Dexie { public events!: Table; - public calendars!: Table; + public calendars!: Table; constructor() { super("db"); - this.version(1).stores({ + this.version(2).stores({ calendars: [ "id", "providerId", @@ -38,7 +48,6 @@ export class Database extends Dexie { "etag", "timeZone", "primary", - "accountId", "providerAccountId", "color", "readOnly", @@ -48,7 +57,6 @@ export class Database extends Dexie { "start", "end", "calendarId", - "accountId", "providerId", "providerAccountId", "recurringEventId", @@ -69,9 +77,9 @@ export class Database extends Dexie { "response.status", "[calendarId+startUnix]", - "[accountId+startUnix]", + "[providerAccountId+startUnix]", "[providerId+calendarId]", - "[providerId+accountId]", + "[providerId+providerAccountId]", "[startUnix+endUnix]", ].join(","), }); @@ -104,8 +112,14 @@ export function mapEventQueryInput(event: CalendarEvent): EventRow { ? superjson.serialize(event.updatedAt) : undefined; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { calendar, ...rest } = event; + return { - ...event, + ...rest, + calendarId: event.calendar.id, + providerId: event.calendar.provider.id, + providerAccountId: event.calendar.provider.accountId, start, end, startUnix, @@ -116,8 +130,14 @@ export function mapEventQueryInput(event: CalendarEvent): EventRow { } export function mapEventQuery(row: EventRow): CalendarEvent { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { startUnix, endUnix, ...rest } = row; + const { + startUnix: _startUnix, + endUnix: _endUnix, + calendarId, + providerId, + providerAccountId, + ...rest + } = row; const start = superjson.deserialize(row.start) as | Temporal.PlainDate @@ -134,7 +154,41 @@ export function mapEventQuery(row: EventRow): CalendarEvent { ? (superjson.deserialize(row.updatedAt) as Temporal.Instant) : undefined; - return { ...rest, start, end, createdAt, updatedAt }; + return { + ...rest, + calendar: { + id: calendarId, + provider: { + id: providerId, + accountId: providerAccountId, + }, + }, + start, + end, + createdAt, + updatedAt, + }; +} + +export function mapCalendarQueryInput(calendar: Calendar): CalendarRow { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { provider, ...rest } = calendar; + return { + ...rest, + providerId: provider.id, + providerAccountId: provider.accountId, + }; +} + +export function mapCalendarQuery(row: CalendarRow): Calendar { + const { providerId, providerAccountId, ...rest } = row; + return { + ...rest, + provider: { + id: providerId, + accountId: providerAccountId, + }, + }; } export const db = new Database(); diff --git a/packages/api/src/routers/accounts.ts b/packages/api/src/routers/accounts.ts index 73d62a54..c2421b45 100644 --- a/packages/api/src/routers/accounts.ts +++ b/packages/api/src/routers/accounts.ts @@ -1,5 +1,7 @@ import { TRPCError } from "@trpc/server"; +import type { ProviderId } from "@repo/providers/interfaces"; + import { createTRPCRouter, protectedProcedure } from "../trpc"; import { getAccounts, getDefaultAccount } from "../utils/accounts"; @@ -10,8 +12,10 @@ export const accountsRouter = createTRPCRouter({ return { accounts: accounts.map((account) => ({ id: account.id, - providerAccountId: account.accountId, - providerId: account.providerId, + provider: { + id: account.providerId as ProviderId, + accountId: account.accountId, + }, name: account.name, email: account.email, image: account.image, @@ -33,8 +37,10 @@ export const accountsRouter = createTRPCRouter({ return { account: { id: account.id, - providerAccountId: account.accountId, - providerId: account.providerId, + provider: { + id: account.providerId as ProviderId, + accountId: account.accountId, + }, name: account.name, email: account.email, image: account.image, diff --git a/packages/api/src/routers/calendars.ts b/packages/api/src/routers/calendars.ts index 04b4f272..acd0f6d1 100644 --- a/packages/api/src/routers/calendars.ts +++ b/packages/api/src/routers/calendars.ts @@ -2,8 +2,9 @@ import { TRPCError } from "@trpc/server"; import * as z from "zod"; import { auth } from "@repo/auth/server"; +import type { ProviderId } from "@repo/providers/interfaces"; import { assignColor } from "@repo/providers/calendars/colors"; -import { createCalendarInputSchema } from "@repo/schemas"; +import { createCalendarInputSchema, providerSchema } from "@repo/schemas"; import { calendarProcedure, @@ -16,13 +17,13 @@ export const calendarsRouter = createTRPCRouter({ .input(createCalendarInputSchema) .mutation(async ({ ctx, input }) => { const provider = ctx.providers.find( - ({ account }) => account.accountId === input.accountId, + ({ account }) => account.accountId === input.provider.accountId, ); if (!provider?.client) { throw new TRPCError({ code: "NOT_FOUND", - message: `Calendar client not found for accountId: ${input.accountId}`, + message: `Calendar client not found for providerAccountId: ${input.provider.accountId}`, }); } @@ -49,8 +50,10 @@ export const calendarsRouter = createTRPCRouter({ return { id: account.id, - providerAccountId: account.accountId, - providerId: account.providerId, + provider: { + id: account.providerId as ProviderId, + accountId: account.accountId, + }, name: account.email, calendars, }; @@ -71,7 +74,7 @@ export const calendarsRouter = createTRPCRouter({ ) ?? calendars.find( (calendar) => - calendar.providerAccountId === defaultAccount.accountId && + calendar.provider.accountId === defaultAccount.accountId && calendar.primary, ); @@ -109,19 +112,19 @@ export const calendarsRouter = createTRPCRouter({ get: calendarProcedure .input( z.object({ - accountId: z.string(), + provider: providerSchema, calendarId: z.string(), }), ) .query(async ({ ctx, input }) => { const provider = ctx.providers.find( - ({ account }) => account.accountId === input.accountId, + ({ account }) => account.accountId === input.provider.accountId, ); if (!provider?.client) { throw new TRPCError({ code: "NOT_FOUND", - message: `Calendar client not found for accountId: ${input.accountId}`, + message: `Calendar client not found for providerAccountId: ${input.provider.accountId}`, }); } @@ -133,20 +136,20 @@ export const calendarsRouter = createTRPCRouter({ .input( z.object({ id: z.string(), - accountId: z.string(), + provider: providerSchema, name: z.string(), timeZone: z.string().optional(), }), ) .mutation(async ({ ctx, input }) => { const provider = ctx.providers.find( - ({ account }) => account.accountId === input.accountId, + ({ account }) => account.accountId === input.provider.accountId, ); if (!provider?.client) { throw new TRPCError({ code: "NOT_FOUND", - message: `Calendar client not found for accountId: ${input.accountId}`, + message: `Calendar client not found for providerAccountId: ${input.provider.accountId}`, }); } @@ -177,19 +180,19 @@ export const calendarsRouter = createTRPCRouter({ delete: calendarProcedure .input( z.object({ - accountId: z.string(), + provider: providerSchema, calendarId: z.string(), }), ) .mutation(async ({ ctx, input }) => { const provider = ctx.providers.find( - ({ account }) => account.accountId === input.accountId, + ({ account }) => account.accountId === input.provider.accountId, ); if (!provider?.client) { throw new TRPCError({ code: "NOT_FOUND", - message: `Calendar client not found for accountId: ${input.accountId}`, + message: `Calendar client not found for providerAccountId: ${input.provider.accountId}`, }); } diff --git a/packages/api/src/routers/conferencing.ts b/packages/api/src/routers/conferencing.ts index cc651ba2..1983c535 100644 --- a/packages/api/src/routers/conferencing.ts +++ b/packages/api/src/routers/conferencing.ts @@ -93,9 +93,10 @@ export const conferencingRouter = createTRPCRouter({ const event = await client.updateEvent(calendar, input.eventId, { id: input.eventId, title: input.agenda, - accountId: input.calendarAccountId, - calendarId: input.calendarId, - providerId: provider.account.providerId, + calendar: { + id: calendar.id, + provider: calendar.provider, + }, readOnly: calendar.readOnly, start, end, diff --git a/packages/api/src/routers/events.ts b/packages/api/src/routers/events.ts index adaa21cc..1be87c70 100644 --- a/packages/api/src/routers/events.ts +++ b/packages/api/src/routers/events.ts @@ -5,7 +5,12 @@ import { zZonedDateTimeInstance } from "temporal-zod"; import * as z from "zod"; import { CalendarEvent } from "@repo/providers/interfaces"; -import { createEventInputSchema, updateEventInputSchema } from "@repo/schemas"; +import { + createEventInputSchema, + eventCalendarSchema, + providerSchema, + updateEventInputSchema, +} from "@repo/schemas"; import { toInstant } from "@repo/temporal"; import { calendarProcedure, createTRPCRouter } from "../trpc"; @@ -23,7 +28,7 @@ export const eventsRouter = createTRPCRouter({ ) .query(async ({ ctx, input }) => { const results = await Promise.all( - ctx.providers.map(async ({ client, account }) => { + ctx.providers.map(async ({ client }) => { const calendars = await client.calendars(); const requestedCalendars = @@ -40,16 +45,8 @@ export const eventsRouter = createTRPCRouter({ input.defaultTimeZone, ); - const mapped = events.map((event) => ({ - ...event, - calendarId: calendar.id, - providerId: account.providerId, - accountId: account.accountId, - providerAccountId: account.accountId, - })); - return { - events: mapped, + events, recurringMasterEvents: Object.values(recurringMasterEvents), }; }), @@ -82,10 +79,7 @@ export const eventsRouter = createTRPCRouter({ z.object({ timeMin: zZonedDateTimeInstance.optional(), timeMax: zZonedDateTimeInstance.optional(), - calendar: z.object({ - providerId: z.enum(["google", "microsoft"]), - providerAccountId: z.string(), - calendarId: z.string(), + calendar: eventCalendarSchema.extend({ syncToken: z.string().optional(), }), timeZone: z.string().default("UTC"), @@ -93,26 +87,25 @@ export const eventsRouter = createTRPCRouter({ ) .query(async ({ ctx, input }) => { const provider = ctx.providers.find( - ({ account }) => account.accountId === input.calendar.providerAccountId, + ({ account }) => + account.accountId === input.calendar.provider.accountId, ); if (!provider?.client) { throw new TRPCError({ code: "NOT_FOUND", - message: `Calendar client not found for accountId: ${input.calendar.providerAccountId}`, + message: `Calendar client not found for providerAccountId: ${input.calendar.provider.accountId}`, }); } const calendars = await provider.client.calendars(); - const calendar = calendars.find( - (c) => c.id === input.calendar.calendarId, - ); + const calendar = calendars.find((c) => c.id === input.calendar.id); if (!calendar) { throw new TRPCError({ code: "NOT_FOUND", - message: `Calendar not found for accountId: ${input.calendar.providerAccountId}`, + message: `Calendar not found: ${input.calendar.id}`, }); } @@ -133,32 +126,32 @@ export const eventsRouter = createTRPCRouter({ get: calendarProcedure .input( z.object({ - accountId: z.string(), - calendarId: z.string(), + calendar: eventCalendarSchema, eventId: z.string(), timeZone: z.string().optional(), }), ) .query(async ({ ctx, input }) => { const provider = ctx.providers.find( - ({ account }) => account.accountId === input.accountId, + ({ account }) => + account.accountId === input.calendar.provider.accountId, ); if (!provider?.client) { throw new TRPCError({ code: "NOT_FOUND", - message: `Calendar client not found for accountId: ${input.accountId}`, + message: `Calendar client not found for providerAccountId: ${input.calendar.provider.accountId}`, }); } const calendars = await provider.client.calendars(); - const calendar = calendars.find((c) => c.id === input.calendarId); + const calendar = calendars.find((c) => c.id === input.calendar.id); if (!calendar) { throw new TRPCError({ code: "NOT_FOUND", - message: `Calendar not found for accountId: ${input.calendarId}`, + message: `Calendar not found: ${input.calendar.id}`, }); } @@ -174,24 +167,25 @@ export const eventsRouter = createTRPCRouter({ .input(createEventInputSchema) .mutation(async ({ ctx, input }) => { const provider = ctx.providers.find( - ({ account }) => account.accountId === input.accountId, + ({ account }) => + account.accountId === input.calendar.provider.accountId, ); if (!provider?.client) { throw new TRPCError({ code: "NOT_FOUND", - message: `Calendar client not found for accountId: ${input.accountId}`, + message: `Calendar client not found for providerAccountId: ${input.calendar.provider.accountId}`, }); } const calendars = await provider.client.calendars(); - const calendar = calendars.find((c) => c.id === input.calendarId); + const calendar = calendars.find((c) => c.id === input.calendar.id); if (!calendar) { throw new TRPCError({ code: "NOT_FOUND", - message: `Calendar not found for accountId: ${input.calendarId}`, + message: `Calendar not found: ${input.calendar.id}`, }); } @@ -205,14 +199,8 @@ export const eventsRouter = createTRPCRouter({ data: updateEventInputSchema, move: z .object({ - source: z.object({ - accountId: z.string(), - calendarId: z.string(), - }), - destination: z.object({ - accountId: z.string(), - calendarId: z.string(), - }), + source: eventCalendarSchema, + destination: eventCalendarSchema, }) .optional(), }), @@ -223,23 +211,24 @@ export const eventsRouter = createTRPCRouter({ // If no move provided, perform a regular update on the current calendar if (!move) { const provider = ctx.providers.find( - ({ account }) => account.accountId === data.accountId, + ({ account }) => + account.accountId === data.calendar.provider.accountId, ); if (!provider?.client) { throw new TRPCError({ code: "NOT_FOUND", - message: `Calendar client not found for accountId: ${data.accountId}`, + message: `Calendar client not found for providerAccountId: ${data.calendar.provider.accountId}`, }); } const calendars = await provider.client.calendars(); - const calendar = calendars.find((c) => c.id === data.calendarId); + const calendar = calendars.find((c) => c.id === data.calendar.id); if (!calendar) { throw new TRPCError({ code: "NOT_FOUND", - message: `Calendar not found for accountId: ${data.accountId}`, + message: `Calendar not found: ${data.calendar.id}`, }); } @@ -255,12 +244,12 @@ export const eventsRouter = createTRPCRouter({ // With move provided, move the event first, then apply updates if needed const sourceProvider = findProviderOrThrow( ctx.providers, - move.source.accountId, + move.source.provider.accountId, ); const destinationProvider = findProviderOrThrow( ctx.providers, - move.destination.accountId, + move.destination.provider.accountId, ); const [sourceCalendars, destinationCalendars] = await Promise.all([ @@ -268,19 +257,16 @@ export const eventsRouter = createTRPCRouter({ destinationProvider.client.calendars(), ]); - const sourceCalendar = findCalendarOrThrow( - sourceCalendars, - move.source.calendarId, - ); + const sourceCalendar = findCalendarOrThrow(sourceCalendars, move.source.id); const destinationCalendar = findCalendarOrThrow( destinationCalendars, - move.destination.calendarId, + move.destination.id, ); // If destination is the same as source, just update if ( - move.source.accountId === move.destination.accountId && - move.source.calendarId === move.destination.calendarId + move.source.provider.accountId === move.destination.provider.accountId && + move.source.id === move.destination.id ) { const event = await sourceProvider.client.updateEvent( sourceCalendar, @@ -302,7 +288,9 @@ export const eventsRouter = createTRPCRouter({ } // Same Google account → use native move then update - if (move.source.accountId === move.destination.accountId) { + if ( + move.source.provider.accountId === move.destination.provider.accountId + ) { const moved = await sourceProvider.client.moveEvent( sourceCalendar, destinationCalendar, @@ -316,8 +304,7 @@ export const eventsRouter = createTRPCRouter({ { ...data, id: moved.id, - accountId: move.destination.accountId, - calendarId: move.destination.calendarId, + calendar: move.destination, }, ); @@ -330,8 +317,10 @@ export const eventsRouter = createTRPCRouter({ { ...data, id: crypto.randomUUID(), - accountId: move.destination.accountId, - calendarId: destinationCalendar.id, + calendar: { + id: destinationCalendar.id, + provider: move.destination.provider, + }, }, ); @@ -346,26 +335,26 @@ export const eventsRouter = createTRPCRouter({ delete: calendarProcedure .input( z.object({ - accountId: z.string(), - calendarId: z.string(), + calendar: eventCalendarSchema, eventId: z.string(), sendUpdate: z.boolean().optional().default(true), }), ) .mutation(async ({ ctx, input }) => { const provider = ctx.providers.find( - ({ account }) => account.accountId === input.accountId, + ({ account }) => + account.accountId === input.calendar.provider.accountId, ); if (!provider?.client) { throw new TRPCError({ code: "NOT_FOUND", - message: `Calendar client not found for accountId: ${input.accountId}`, + message: `Calendar client not found for providerAccountId: ${input.calendar.provider.accountId}`, }); } await provider.client.deleteEvent( - input.calendarId, + input.calendar.id, input.eventId, input.sendUpdate, ); @@ -375,15 +364,15 @@ export const eventsRouter = createTRPCRouter({ move: calendarProcedure .input( z.object({ - source: z.object({ - providerId: z.enum(["google"]), - accountId: z.string(), - calendarId: z.string(), + source: eventCalendarSchema.extend({ + provider: providerSchema.extend({ + id: z.literal("google"), + }), }), - destination: z.object({ - providerId: z.enum(["google"]), - accountId: z.string(), - calendarId: z.string(), + destination: eventCalendarSchema.extend({ + provider: providerSchema.extend({ + id: z.literal("google"), + }), }), eventId: z.string(), sendUpdate: z.boolean().optional().default(true), @@ -392,11 +381,11 @@ export const eventsRouter = createTRPCRouter({ .mutation(async ({ ctx, input }) => { const sourceProvider = findProviderOrThrow( ctx.providers, - input.source.accountId, + input.source.provider.accountId, ); const destinationProvider = findProviderOrThrow( ctx.providers, - input.destination.accountId, + input.destination.provider.accountId, ); const [sourceCalendars, destinationCalendars] = await Promise.all([ @@ -406,16 +395,18 @@ export const eventsRouter = createTRPCRouter({ const sourceCalendar = findCalendarOrThrow( sourceCalendars, - input.source.calendarId, + input.source.id, ); const destinationCalendar = findCalendarOrThrow( destinationCalendars, - input.destination.calendarId, + input.destination.id, ); // Same Google account → use native move - if (input.source.accountId === input.destination.accountId) { + if ( + input.source.provider.accountId === input.destination.provider.accountId + ) { const event = await sourceProvider.client.moveEvent( sourceCalendar, destinationCalendar, @@ -438,9 +429,7 @@ export const eventsRouter = createTRPCRouter({ { ...sourceEvent, id: crypto.randomUUID(), - accountId: input.destination.accountId, - calendarId: input.destination.calendarId, - providerId: "google", + calendar: input.destination, }, ); @@ -455,8 +444,7 @@ export const eventsRouter = createTRPCRouter({ respondToInvite: calendarProcedure .input( z.object({ - accountId: z.string(), - calendarId: z.string(), + calendar: eventCalendarSchema, eventId: z.string(), response: z.object({ status: z.enum(["accepted", "tentative", "declined", "unknown"]), @@ -467,21 +455,26 @@ export const eventsRouter = createTRPCRouter({ ) .mutation(async ({ ctx, input }) => { const provider = ctx.providers.find( - ({ account }) => account.accountId === input.accountId, + ({ account }) => + account.accountId === input.calendar.provider.accountId, ); if (!provider?.client) { throw new TRPCError({ code: "NOT_FOUND", - message: `Calendar client not found for accountId: ${input.accountId}`, + message: `Calendar client not found for providerAccountId: ${input.calendar.provider.accountId}`, }); } - await provider.client.responseToEvent(input.calendarId, input.eventId, { - status: input.response.status, - comment: input.response.comment, - sendUpdate: input.response.sendUpdate, - }); + await provider.client.responseToEvent( + input.calendar.id, + input.eventId, + { + status: input.response.status, + comment: input.response.comment, + sendUpdate: input.response.sendUpdate, + }, + ); return { success: true }; }), diff --git a/packages/api/src/routers/tasks.ts b/packages/api/src/routers/tasks.ts index d9aaeda0..76d14d2b 100644 --- a/packages/api/src/routers/tasks.ts +++ b/packages/api/src/routers/tasks.ts @@ -1,5 +1,6 @@ import { TRPCError } from "@trpc/server"; +import type { ProviderId } from "@repo/providers/interfaces"; import { createTaskInputSchema } from "@repo/schemas"; import { createTRPCRouter, taskProcedure } from "../trpc"; @@ -9,13 +10,13 @@ export const tasksRouter = createTRPCRouter({ .input(createTaskInputSchema) .mutation(async ({ ctx, input }) => { const provider = ctx.providers.find( - ({ account }) => account.id === input.accountId, + ({ account }) => account.accountId === input.provider.accountId, ); if (!provider?.client) { throw new TRPCError({ code: "NOT_FOUND", - message: `Task client not found for accountId: ${input.accountId}`, + message: `Task client not found for providerAccountId: ${input.provider.accountId}`, }); } @@ -28,9 +29,11 @@ export const tasksRouter = createTRPCRouter({ const tasks = await client.tasks(); return { - accountId: account.id, - providerAccountId: account.accountId, - providerId: account.providerId, + id: account.id, + provider: { + id: account.providerId as ProviderId, + accountId: account.accountId, + }, name: account.email, tasks: tasks.map((task) => ({ ...task, diff --git a/packages/api/src/utils/index.ts b/packages/api/src/utils/index.ts index 31bb5ec6..5dc1b635 100644 --- a/packages/api/src/utils/index.ts +++ b/packages/api/src/utils/index.ts @@ -8,15 +8,18 @@ interface Provider { client: CalendarProvider; } -export function findProviderOrThrow(providers: Provider[], accountId: string) { +export function findProviderOrThrow( + providers: Provider[], + providerAccountId: string, +) { const provider = providers.find( - ({ account }) => account.accountId === accountId, + ({ account }) => account.accountId === providerAccountId, ); if (!provider?.client) { throw new TRPCError({ code: "NOT_FOUND", - message: `Could not find provider for account id: ${accountId}`, + message: `Could not find provider for providerAccountId: ${providerAccountId}`, }); } diff --git a/packages/providers/src/calendars/google-calendar.ts b/packages/providers/src/calendars/google-calendar.ts index b5ffcace..7d98cb5b 100644 --- a/packages/providers/src/calendars/google-calendar.ts +++ b/packages/providers/src/calendars/google-calendar.ts @@ -32,16 +32,16 @@ const MAX_EVENTS_PER_CALENDAR = 250; interface GoogleCalendarProviderOptions { accessToken: string; - accountId: string; + providerAccountId: string; } export class GoogleCalendarProvider implements CalendarProvider { public readonly providerId = "google" as const; - public readonly accountId: string; + public readonly providerAccountId: string; private client: GoogleCalendar; - constructor({ accessToken, accountId }: GoogleCalendarProviderOptions) { - this.accountId = accountId; + constructor({ accessToken, providerAccountId }: GoogleCalendarProviderOptions) { + this.providerAccountId = providerAccountId; this.client = new GoogleCalendar({ accessToken, }); @@ -55,7 +55,7 @@ export class GoogleCalendarProvider implements CalendarProvider { return items.map((calendar) => { const parsedCalendar = parseGoogleCalendarCalendarListEntry({ - accountId: this.accountId, + providerAccountId: this.providerAccountId, entry: calendar, }); @@ -70,7 +70,7 @@ export class GoogleCalendarProvider implements CalendarProvider { await this.client.users.me.calendarList.retrieve(calendarId); return parseGoogleCalendarCalendarListEntry({ - accountId: this.accountId, + providerAccountId: this.providerAccountId, entry: calendar, }); }); @@ -85,7 +85,7 @@ export class GoogleCalendarProvider implements CalendarProvider { }); return parseGoogleCalendarCalendarListEntry({ - accountId: this.accountId, + providerAccountId: this.providerAccountId, entry: createdCalendar, }); }); @@ -101,7 +101,7 @@ export class GoogleCalendarProvider implements CalendarProvider { }); return parseGoogleCalendarCalendarListEntry({ - accountId: this.accountId, + providerAccountId: this.providerAccountId, entry: updatedCalendar, }); }); @@ -135,7 +135,6 @@ export class GoogleCalendarProvider implements CalendarProvider { items?.map((event) => parseGoogleCalendarEvent({ calendar, - accountId: this.accountId, event, defaultTimeZone: timeZone ?? "UTC", }), @@ -219,10 +218,10 @@ export class GoogleCalendarProvider implements CalendarProvider { status: "deleted", event: { id: event.id!, - calendarId: calendar.id, - accountId: this.accountId, - providerId: this.providerId, - providerAccountId: this.accountId, + calendar: { + id: calendar.id, + provider: calendar.provider, + }, }, }); continue; @@ -230,7 +229,6 @@ export class GoogleCalendarProvider implements CalendarProvider { const parsedEvent = parseGoogleCalendarEvent({ calendar, - accountId: this.accountId, event, defaultTimeZone: timeZone, }); @@ -304,7 +302,6 @@ export class GoogleCalendarProvider implements CalendarProvider { return parseGoogleCalendarEvent({ calendar, - accountId: this.accountId, event, defaultTimeZone: timeZone ?? "UTC", }); @@ -324,7 +321,6 @@ export class GoogleCalendarProvider implements CalendarProvider { return parseGoogleCalendarEvent({ calendar, - accountId: this.accountId, event: createdEvent, }); } catch (error) { @@ -394,7 +390,6 @@ export class GoogleCalendarProvider implements CalendarProvider { return parseGoogleCalendarEvent({ calendar, - accountId: this.accountId, event: updatedEvent, }); }); @@ -428,7 +423,6 @@ export class GoogleCalendarProvider implements CalendarProvider { return parseGoogleCalendarEvent({ calendar: destinationCalendar, - accountId: this.accountId, event: moved, }); }); diff --git a/packages/providers/src/calendars/google-calendar/calendars.ts b/packages/providers/src/calendars/google-calendar/calendars.ts index 5d0874a9..985bfc79 100644 --- a/packages/providers/src/calendars/google-calendar/calendars.ts +++ b/packages/providers/src/calendars/google-calendar/calendars.ts @@ -2,12 +2,12 @@ import type { Calendar } from "../../interfaces"; import type { GoogleCalendarCalendarListEntry } from "./interfaces"; interface ParsedGoogleCalendarCalendarListEntryOptions { - accountId: string; + providerAccountId: string; entry: GoogleCalendarCalendarListEntry; } export function parseGoogleCalendarCalendarListEntry({ - accountId, + providerAccountId, entry, }: ParsedGoogleCalendarCalendarListEntryOptions): Calendar { return { @@ -20,9 +20,10 @@ export function parseGoogleCalendarCalendarListEntry({ primary: entry.primary!, readOnly: entry.accessRole === "reader" || entry.accessRole === "freeBusyReader", - providerId: "google", - accountId, - providerAccountId: accountId, + provider: { + id: "google", + accountId: providerAccountId, + }, color: entry.backgroundColor, syncToken: null, }; diff --git a/packages/providers/src/calendars/google-calendar/events.ts b/packages/providers/src/calendars/google-calendar/events.ts index 5e9f07ed..a3be7772 100644 --- a/packages/providers/src/calendars/google-calendar/events.ts +++ b/packages/providers/src/calendars/google-calendar/events.ts @@ -118,14 +118,12 @@ function parseRecurrence( interface ParsedGoogleCalendarEventOptions { calendar: Calendar; - accountId: string; event: GoogleCalendarEvent; defaultTimeZone?: string; } export function parseGoogleCalendarEvent({ calendar, - accountId, event, defaultTimeZone = "UTC", }: ParsedGoogleCalendarEventOptions): CalendarEvent { @@ -162,9 +160,10 @@ export function parseGoogleCalendarEvent({ | "private" | "confidential" | undefined, - providerId: "google", - accountId, - calendarId: calendar.id, + calendar: { + id: calendar.id, + provider: calendar.provider, + }, readOnly: calendar.readOnly || ["birthday", "focusTime", "outOfOffice", "workingLocation"].includes( @@ -303,7 +302,7 @@ export function updateEventParams( // TODO: how to handle recurrence when the time zone is changed (i.e. until, rDate, exDate). recurrence: recurrences(event), recurringEventId: event.recurringEventId, - calendarId: event.calendarId, + calendarId: event.calendar.id, }; } diff --git a/packages/providers/src/calendars/microsoft-calendar.ts b/packages/providers/src/calendars/microsoft-calendar.ts index cc7d924c..1b4fc7a5 100644 --- a/packages/providers/src/calendars/microsoft-calendar.ts +++ b/packages/providers/src/calendars/microsoft-calendar.ts @@ -43,16 +43,16 @@ const MAX_EVENTS_PER_CALENDAR = 250; interface MicrosoftCalendarProviderOptions { accessToken: string; - accountId: string; + providerAccountId: string; } export class MicrosoftCalendarProvider implements CalendarProvider { public readonly providerId = "microsoft" as const; - public readonly accountId: string; + public readonly providerAccountId: string; private graphClient: Client; - constructor({ accessToken, accountId }: MicrosoftCalendarProviderOptions) { - this.accountId = accountId; + constructor({ accessToken, providerAccountId }: MicrosoftCalendarProviderOptions) { + this.providerAccountId = providerAccountId; this.graphClient = Client.initWithMiddleware({ authProvider: { getAccessToken: async () => accessToken, @@ -70,7 +70,7 @@ export class MicrosoftCalendarProvider implements CalendarProvider { .get(); return (response.value as MicrosoftCalendar[]).map((calendar) => ({ - ...parseMicrosoftCalendar({ calendar, accountId: this.accountId }), + ...parseMicrosoftCalendar({ calendar, providerAccountId: this.providerAccountId }), })); }); } @@ -86,7 +86,7 @@ export class MicrosoftCalendarProvider implements CalendarProvider { return parseMicrosoftCalendar({ calendar, - accountId: this.accountId, + providerAccountId: this.providerAccountId, }); }); } @@ -101,7 +101,7 @@ export class MicrosoftCalendarProvider implements CalendarProvider { return parseMicrosoftCalendar({ calendar: createdCalendar, - accountId: this.accountId, + providerAccountId: this.providerAccountId, }); }); } @@ -117,7 +117,7 @@ export class MicrosoftCalendarProvider implements CalendarProvider { return parseMicrosoftCalendar({ calendar: updatedCalendar, - accountId: this.accountId, + providerAccountId: this.providerAccountId, }); }); } @@ -153,7 +153,7 @@ export class MicrosoftCalendarProvider implements CalendarProvider { const events = (response.value as MicrosoftEvent[]).map( (event: MicrosoftEvent) => - parseMicrosoftEvent({ event, accountId: this.accountId, calendar }), + parseMicrosoftEvent({ event, calendar }), ); return { events, recurringMasterEvents: [] }; @@ -218,10 +218,10 @@ export class MicrosoftCalendarProvider implements CalendarProvider { status: "deleted", event: { id: item.id, - calendarId: calendar.id, - accountId: this.accountId, - providerId: this.providerId, - providerAccountId: this.accountId, + calendar: { + id: calendar.id, + provider: calendar.provider, + }, }, }); @@ -232,7 +232,6 @@ export class MicrosoftCalendarProvider implements CalendarProvider { status: "updated", event: parseMicrosoftEvent({ event: item, - accountId: this.accountId, calendar, }), }); @@ -263,7 +262,6 @@ export class MicrosoftCalendarProvider implements CalendarProvider { return parseMicrosoftEvent({ event, - accountId: this.accountId, calendar, }); }); @@ -280,20 +278,11 @@ export class MicrosoftCalendarProvider implements CalendarProvider { return parseMicrosoftEvent({ event: createdEvent, - accountId: this.accountId, calendar, }); }); } - /** - * Updates an existing event - * - * @param calendarId - The calendar identifier - * @param eventId - The event identifier - * @param event - Partial event data for updates using UpdateEventInput interface - * @returns The updated transformed Event object - */ async updateEvent( calendar: Calendar, eventId: string, @@ -323,7 +312,6 @@ export class MicrosoftCalendarProvider implements CalendarProvider { return parseMicrosoftEvent({ event: updatedEvent, - accountId: this.accountId, calendar, }); }); @@ -363,8 +351,10 @@ export class MicrosoftCalendarProvider implements CalendarProvider { return { ...event, - calendarId: destinationCalendar.id, - // Mark as readOnly to signal as placeholder behavior if needed by callers + calendar: { + id: destinationCalendar.id, + provider: destinationCalendar.provider, + }, readOnly: event.readOnly, }; }); diff --git a/packages/providers/src/calendars/microsoft-calendar/calendars.ts b/packages/providers/src/calendars/microsoft-calendar/calendars.ts index af2f712d..d6610ef8 100644 --- a/packages/providers/src/calendars/microsoft-calendar/calendars.ts +++ b/packages/providers/src/calendars/microsoft-calendar/calendars.ts @@ -3,21 +3,22 @@ import type { Calendar as MicrosoftCalendar } from "@microsoft/microsoft-graph-t import type { Calendar } from "../../interfaces"; interface ParseMicrosoftCalendarOptions { - accountId: string; + providerAccountId: string; calendar: MicrosoftCalendar; } export function parseMicrosoftCalendar({ - accountId, + providerAccountId, calendar, }: ParseMicrosoftCalendarOptions): Calendar { return { id: calendar.id!, - providerId: "microsoft", name: calendar.name!, primary: calendar.isDefaultCalendar!, - accountId, - providerAccountId: accountId, + provider: { + id: "microsoft", + accountId: providerAccountId, + }, color: calendar.hexColor!, readOnly: !calendar.canEdit, syncToken: null, diff --git a/packages/providers/src/calendars/microsoft-calendar/events.ts b/packages/providers/src/calendars/microsoft-calendar/events.ts index 19a8bfa0..45795979 100644 --- a/packages/providers/src/calendars/microsoft-calendar/events.ts +++ b/packages/providers/src/calendars/microsoft-calendar/events.ts @@ -69,7 +69,6 @@ function parseDate(date: string) { } interface ParseMicrosoftEventOptions { - accountId: string; calendar: Calendar; event: MicrosoftEvent; } @@ -102,7 +101,6 @@ function parseResponseStatus( } export function parseMicrosoftEvent({ - accountId, calendar, event, }: ParseMicrosoftEventOptions): CalendarEvent { @@ -131,9 +129,10 @@ export function parseMicrosoftEvent({ url: event.webLink ?? undefined, // @ts-expect-error -- type from Graph API package is incorrect etag: event["@odata.etag"], - providerId: "microsoft", - accountId, - calendarId: calendar.id, + calendar: { + id: calendar.id, + provider: calendar.provider, + }, readOnly: calendar.readOnly, conference: parseMicrosoftConference(event), ...(responseStatus ? { response: { status: responseStatus } } : {}), diff --git a/packages/providers/src/conferencing/google-meet.ts b/packages/providers/src/conferencing/google-meet.ts index 47185470..7a5de7ca 100644 --- a/packages/providers/src/conferencing/google-meet.ts +++ b/packages/providers/src/conferencing/google-meet.ts @@ -6,16 +6,16 @@ import { ProviderError } from "../lib/provider-error"; interface GoogleMeetProviderOptions { accessToken: string; - accountId: string; + providerAccountId: string; } export class GoogleMeetProvider implements ConferencingProvider { public readonly providerId = "google" as const; - public readonly accountId: string; + public readonly providerAccountId: string; private client: GoogleCalendar; - constructor({ accessToken, accountId }: GoogleMeetProviderOptions) { - this.accountId = accountId; + constructor({ accessToken, providerAccountId }: GoogleMeetProviderOptions) { + this.providerAccountId = providerAccountId; this.client = new GoogleCalendar({ accessToken, }); diff --git a/packages/providers/src/conferencing/zoom.ts b/packages/providers/src/conferencing/zoom.ts index 7284dca0..27b4eded 100644 --- a/packages/providers/src/conferencing/zoom.ts +++ b/packages/providers/src/conferencing/zoom.ts @@ -5,7 +5,7 @@ import { ProviderError } from "../lib/provider-error"; interface ZoomProviderOptions { accessToken: string; - accountId?: string; // Unused but allows shared construction signature + providerAccountId?: string; } export class ZoomProvider implements ConferencingProvider { diff --git a/packages/providers/src/index.ts b/packages/providers/src/index.ts index 7a198017..9ee100d2 100644 --- a/packages/providers/src/index.ts +++ b/packages/providers/src/index.ts @@ -44,14 +44,14 @@ function accountToProvider< if (!activeAccount.accessToken) { throw new TRPCError({ code: "BAD_REQUEST", - message: `Invalid account: Missing access token for provider '${activeAccount.providerId}' (accountId: ${activeAccount.accountId})`, + message: `Invalid account: Missing access token for provider '${activeAccount.providerId}' (providerAccountId: ${activeAccount.accountId})`, }); } if (!activeAccount.refreshToken) { throw new TRPCError({ code: "BAD_REQUEST", - message: `Invalid account: Missing refresh token for provider '${activeAccount.providerId}' (accountId: ${activeAccount.accountId})`, + message: `Invalid account: Missing refresh token for provider '${activeAccount.providerId}' (providerAccountId: ${activeAccount.accountId})`, }); } @@ -60,13 +60,13 @@ function accountToProvider< if (!Provider) { throw new TRPCError({ code: "BAD_REQUEST", - message: `Provider not supported: '${activeAccount.providerId}' (accountId: ${activeAccount.accountId})`, + message: `Provider not supported: '${activeAccount.providerId}' (providerAccountId: ${activeAccount.accountId})`, }); } return new Provider({ accessToken: activeAccount.accessToken, - accountId: activeAccount.accountId, + providerAccountId: activeAccount.accountId, }); } @@ -95,14 +95,14 @@ export function accountToConferencingProvider( if (!activeAccount.accessToken) { throw new TRPCError({ code: "BAD_REQUEST", - message: `Invalid account: Missing access token for provider '${activeAccount.providerId}' (accountId: ${activeAccount.accountId})`, + message: `Invalid account: Missing access token for provider '${activeAccount.providerId}' (providerAccountId: ${activeAccount.accountId})`, }); } if (!activeAccount.refreshToken) { throw new TRPCError({ code: "BAD_REQUEST", - message: `Invalid account: Missing refresh token for provider '${activeAccount.providerId}' (accountId: ${activeAccount.accountId})`, + message: `Invalid account: Missing refresh token for provider '${activeAccount.providerId}' (providerAccountId: ${activeAccount.accountId})`, }); } @@ -111,13 +111,13 @@ export function accountToConferencingProvider( if (!Provider) { throw new TRPCError({ code: "BAD_REQUEST", - message: `Conferencing provider not supported: '${providerId}' for account '${activeAccount.providerId}' (accountId: ${activeAccount.accountId})`, + message: `Conferencing provider not supported: '${providerId}' for account '${activeAccount.providerId}' (providerAccountId: ${activeAccount.accountId})`, }); } return new Provider({ accessToken: activeAccount.accessToken, - accountId: activeAccount.accountId, + providerAccountId: activeAccount.accountId, }); } diff --git a/packages/providers/src/interfaces/calendars.ts b/packages/providers/src/interfaces/calendars.ts index 735a8606..ef55b358 100644 --- a/packages/providers/src/interfaces/calendars.ts +++ b/packages/providers/src/interfaces/calendars.ts @@ -1,15 +1,20 @@ import type { Temporal } from "temporal-polyfill"; +export type ProviderId = "google" | "microsoft"; + +export interface Provider { + id: ProviderId; + accountId: string; +} + export interface Calendar { id: string; - providerId: "google" | "microsoft"; + provider: Provider; name: string; description?: string; etag?: string; timeZone?: string; primary: boolean; - accountId: string; - providerAccountId: string; color?: string; readOnly: boolean; syncToken: string | null; diff --git a/packages/providers/src/interfaces/events.ts b/packages/providers/src/interfaces/events.ts index 14b08f20..d5c3fb1e 100644 --- a/packages/providers/src/interfaces/events.ts +++ b/packages/providers/src/interfaces/events.ts @@ -1,5 +1,12 @@ import type { Temporal } from "temporal-polyfill"; +import type { Provider } from "./calendars"; + +export interface CalendarEventCalendar { + id: string; + provider: Provider; +} + export interface CalendarEvent { id: string; title?: string; @@ -16,9 +23,7 @@ export interface CalendarEvent { color?: string | null; visibility?: "default" | "public" | "private" | "confidential"; readOnly: boolean; - providerId: "google" | "microsoft"; - accountId: string; - calendarId: string; + calendar: CalendarEventCalendar; createdAt?: Temporal.Instant; updatedAt?: Temporal.Instant; response?: { @@ -28,7 +33,6 @@ export interface CalendarEvent { metadata?: Record; conference?: Conference | null; recurrence?: Recurrence | null; - providerAccountId?: string; recurringEventId?: string; } @@ -41,10 +45,7 @@ export type CalendarEventSyncItem = status: "deleted"; event: { id: string; - calendarId: string; - accountId: string; - providerId: "google" | "microsoft"; - providerAccountId?: string; + calendar: CalendarEventCalendar; }; }; diff --git a/packages/providers/src/interfaces/providers/provider-config.ts b/packages/providers/src/interfaces/providers/provider-config.ts index a327afe5..8d8fa287 100644 --- a/packages/providers/src/interfaces/providers/provider-config.ts +++ b/packages/providers/src/interfaces/providers/provider-config.ts @@ -1,4 +1,4 @@ export interface ProviderConfig { accessToken: string; - accountId: string; + providerAccountId: string; } diff --git a/packages/providers/src/interfaces/tasks.ts b/packages/providers/src/interfaces/tasks.ts index f9ea1d02..2ac12388 100644 --- a/packages/providers/src/interfaces/tasks.ts +++ b/packages/providers/src/interfaces/tasks.ts @@ -5,7 +5,7 @@ export interface TaskCollection { providerId?: string; title?: string; updated?: string; - accountId: string; + providerAccountId: string; } export interface TaskCollectionWithTasks extends TaskCollection { @@ -14,7 +14,7 @@ export interface TaskCollectionWithTasks extends TaskCollection { export interface Task { id: string; - accountId: string; + providerAccountId: string; taskCollectionId: string; providerId?: string; title?: string; diff --git a/packages/providers/src/tasks/google-tasks.ts b/packages/providers/src/tasks/google-tasks.ts index be7efd43..482dd8a4 100644 --- a/packages/providers/src/tasks/google-tasks.ts +++ b/packages/providers/src/tasks/google-tasks.ts @@ -12,16 +12,16 @@ import { parseGoogleTask, toGoogleTask } from "./google-tasks/utils"; interface GoogleTasksProviderOptions { accessToken: string; - accountId: string; + providerAccountId: string; } export class GoogleTasksProvider implements TaskProvider { public readonly providerId = "google" as const; - public readonly accountId: string; + public readonly providerAccountId: string; private client: GoogleTasks; - constructor({ accessToken, accountId }: GoogleTasksProviderOptions) { - this.accountId = accountId; + constructor({ accessToken, providerAccountId }: GoogleTasksProviderOptions) { + this.providerAccountId = providerAccountId; this.client = new GoogleTasks({ accessToken, }); @@ -40,7 +40,7 @@ export class GoogleTasksProvider implements TaskProvider { providerId: "google", title: taskCollection.title, updated: taskCollection.updated, - accountId: this.accountId, + providerAccountId: this.providerAccountId, })); }); } @@ -57,14 +57,14 @@ export class GoogleTasksProvider implements TaskProvider { return { id: taskCollection.id!, title: taskCollection.title!, - accountId: this.accountId, + providerAccountId: this.providerAccountId, providerId: "google", tasks: tasks?.map((task) => parseGoogleTask({ task, collectionId: taskCollection.id!, - accountId: this.accountId, + providerAccountId: this.providerAccountId, }), ) ?? [], }; @@ -83,7 +83,7 @@ export class GoogleTasksProvider implements TaskProvider { return parseGoogleTask({ task: createdTask, collectionId: task.taskCollectionId, - accountId: this.accountId, + providerAccountId: this.providerAccountId, }); }); } @@ -97,7 +97,7 @@ export class GoogleTasksProvider implements TaskProvider { parseGoogleTask({ task, collectionId: taskCollectionId, - accountId: this.accountId, + providerAccountId: this.providerAccountId, }), ) ?? [] ); @@ -113,7 +113,7 @@ export class GoogleTasksProvider implements TaskProvider { return parseGoogleTask({ task: updatedTask, collectionId: task.taskCollectionId, - accountId: this.accountId, + providerAccountId: this.providerAccountId, }); }); } diff --git a/packages/providers/src/tasks/google-tasks/utils.ts b/packages/providers/src/tasks/google-tasks/utils.ts index 843470a7..87cd07fc 100644 --- a/packages/providers/src/tasks/google-tasks/utils.ts +++ b/packages/providers/src/tasks/google-tasks/utils.ts @@ -12,11 +12,11 @@ function parseGoogleTaskDate(date: string) { export function parseGoogleTask({ task, collectionId, - accountId, + providerAccountId, }: { task: GoogleTask; collectionId: string; - accountId: string; + providerAccountId: string; }): Task { return { id: task.id!, @@ -25,7 +25,7 @@ export function parseGoogleTask({ description: task.notes, due: task.due ? parseGoogleTaskDate(task.due) : undefined, providerId: "google", - accountId, + providerAccountId, taskCollectionId: collectionId, }; } diff --git a/packages/schemas/src/calendars.ts b/packages/schemas/src/calendars.ts index 53c253f1..5ad3e8ec 100644 --- a/packages/schemas/src/calendars.ts +++ b/packages/schemas/src/calendars.ts @@ -1,10 +1,12 @@ import * as z from "zod"; +import { providerSchema } from "./events"; + export const createCalendarInputSchema = z.object({ name: z.string(), description: z.string().optional(), timeZone: z.string().optional(), - accountId: z.string(), + provider: providerSchema, }); export const updateCalendarInputSchema = createCalendarInputSchema.extend({ @@ -14,9 +16,9 @@ export const updateCalendarInputSchema = createCalendarInputSchema.extend({ export type CreateCalendarInput = Omit< z.infer, - "accountId" + "provider" >; export type UpdateCalendarInput = Omit< z.infer, - "accountId" + "provider" >; diff --git a/packages/schemas/src/events.ts b/packages/schemas/src/events.ts index 0f66251d..b246f157 100644 --- a/packages/schemas/src/events.ts +++ b/packages/schemas/src/events.ts @@ -121,6 +121,16 @@ export const dateInputSchema = z.union([ zZonedDateTimeInstance, ]); +export const providerSchema = z.object({ + id: z.enum(["google", "microsoft"]), + accountId: z.string(), +}); + +export const eventCalendarSchema = z.object({ + id: z.string(), + provider: providerSchema, +}); + const attendeeSchema = z.object({ id: z.string().optional(), email: z.string().email(), @@ -220,9 +230,7 @@ export const createEventInputSchema = z.object({ visibility: z .enum(["default", "public", "private", "confidential"]) .optional(), - accountId: z.string(), - calendarId: z.string(), - providerId: z.enum(["google", "microsoft"]), + calendar: eventCalendarSchema, readOnly: z.boolean(), metadata: z.union([microsoftMetadataSchema, googleMetadataSchema]).optional(), attendees: z.array(attendeeSchema).optional(), @@ -257,9 +265,7 @@ export const patchEventInputSchema = z.object({ visibility: z .enum(["default", "public", "private", "confidential"]) .optional(), - accountId: z.string(), - calendarId: z.string(), - providerId: z.enum(["google", "microsoft"]), + calendar: eventCalendarSchema, readOnly: z.boolean(), attendees: z.array(attendeeSchema).optional(), conference: conferenceSchema.optional(), diff --git a/packages/schemas/src/tasks.ts b/packages/schemas/src/tasks.ts index 951c3b35..918e2670 100644 --- a/packages/schemas/src/tasks.ts +++ b/packages/schemas/src/tasks.ts @@ -1,6 +1,8 @@ import { Temporal } from "temporal-polyfill"; import * as z from "zod"; +import { providerSchema } from "./events"; + export const createTaskCollectionInputSchema = z.object({ title: z.string(), }); @@ -9,7 +11,7 @@ export const createTaskInputSchema = z.object({ title: z.string(), taskCollectionId: z.string(), description: z.string().optional(), - accountId: z.string(), + provider: providerSchema, completed: z .union([ z.instanceof(Temporal.PlainDate), From f81349e4901ee887174f5705b2559260887b69f7 Mon Sep 17 00:00:00 2001 From: "Jean P.D. Meijer" Date: Tue, 2 Dec 2025 00:33:30 +0100 Subject: [PATCH 2/3] wip --- .../calendar/event/event-context-menu.tsx | 13 ++- .../calendar/flows/update-event/utils.ts | 3 +- .../web/src/components/calendar/interfaces.ts | 4 - .../event-form/fields/calendar-field.tsx | 18 +++- .../src/components/event-form/utils/schema.ts | 10 ++- apps/web/src/lib/db.ts | 14 ++- apps/web/src/lib/interfaces.ts | 2 - packages/api/src/routers/accounts.ts | 6 +- packages/api/src/routers/calendars.ts | 20 +++-- packages/api/src/routers/events.ts | 88 +++++++++++++------ packages/api/src/routers/tasks.ts | 3 +- .../src/calendars/google-calendar.ts | 5 +- .../src/calendars/microsoft-calendar.ts | 13 ++- .../providers/src/interfaces/calendars.ts | 12 +-- packages/providers/src/interfaces/events.ts | 23 +++-- packages/schemas/src/calendars.ts | 7 +- packages/schemas/src/events.ts | 26 +++--- packages/schemas/src/tasks.ts | 7 +- 18 files changed, 171 insertions(+), 103 deletions(-) diff --git a/apps/web/src/components/calendar/event/event-context-menu.tsx b/apps/web/src/components/calendar/event/event-context-menu.tsx index 1c903fc1..bc72e24f 100644 --- a/apps/web/src/components/calendar/event/event-context-menu.tsx +++ b/apps/web/src/components/calendar/event/event-context-menu.tsx @@ -76,7 +76,10 @@ function EventContextMenuCalendarList({ const updateAction = usePartialUpdateAction(); const moveEvent = React.useCallback( - (calendar: { id: string; provider: { id: "google" | "microsoft"; accountId: string } }) => { + (calendar: { + id: string; + provider: { id: "google" | "microsoft"; accountId: string }; + }) => { updateAction({ changes: { id: event.id, @@ -104,7 +107,9 @@ function EventContextMenuCalendarList({ } as React.CSSProperties } disabled={!canMoveBetweenCalendars(event, calendar)} - onSelect={() => moveEvent({ id: calendar.id, provider: calendar.provider })} + onSelect={() => + moveEvent({ id: calendar.id, provider: calendar.provider }) + } /> @@ -157,7 +162,9 @@ export function EventContextMenu({ event, children }: EventContextMenuProps) { {children} - + diff --git a/apps/web/src/components/calendar/flows/update-event/utils.ts b/apps/web/src/components/calendar/flows/update-event/utils.ts index 0f764c7d..c7d35ae3 100644 --- a/apps/web/src/components/calendar/flows/update-event/utils.ts +++ b/apps/web/src/components/calendar/flows/update-event/utils.ts @@ -47,7 +47,8 @@ export function isMovedBetweenCalendars( previous: CalendarEvent, ) { return ( - updated.calendar.provider.accountId !== previous.calendar.provider.accountId || + updated.calendar.provider.accountId !== + previous.calendar.provider.accountId || updated.calendar.id !== previous.calendar.id ); } diff --git a/apps/web/src/components/calendar/interfaces.ts b/apps/web/src/components/calendar/interfaces.ts index 64f65062..a8e064c9 100644 --- a/apps/web/src/components/calendar/interfaces.ts +++ b/apps/web/src/components/calendar/interfaces.ts @@ -1,5 +1 @@ -import type { RouterOutputs } from "@/lib/trpc"; - export type CalendarView = "month" | "week" | "day" | "agenda"; - -export type CalendarEvent = RouterOutputs["events"]["list"]["events"][number]; diff --git a/apps/web/src/components/event-form/fields/calendar-field.tsx b/apps/web/src/components/event-form/fields/calendar-field.tsx index 679848ef..88ffcf64 100644 --- a/apps/web/src/components/event-form/fields/calendar-field.tsx +++ b/apps/web/src/components/event-form/fields/calendar-field.tsx @@ -1,8 +1,6 @@ import * as React from "react"; import { useQuery } from "@tanstack/react-query"; -import type { CalendarEventCalendar } from "@repo/providers/interfaces"; - import { Button } from "@/components/ui/button"; import { Command, @@ -26,8 +24,20 @@ import { cn } from "@/lib/utils"; interface CalendarFieldProps { id: string; className?: string; - value: CalendarEventCalendar; - onChange: (calendar: CalendarEventCalendar) => void; + value: { + id: string; + provider: { + id: "google" | "microsoft"; + accountId: string; + }; + }; + onChange: (calendar: { + id: string; + provider: { + id: "google" | "microsoft"; + accountId: string; + }; + }) => void; onBlur: () => void; disabled?: boolean; } diff --git a/apps/web/src/components/event-form/utils/schema.ts b/apps/web/src/components/event-form/utils/schema.ts index 330bfb8e..bb1cfd92 100644 --- a/apps/web/src/components/event-form/utils/schema.ts +++ b/apps/web/src/components/event-form/utils/schema.ts @@ -1,7 +1,7 @@ import { zZonedDateTimeInstance } from "temporal-zod"; import * as z from "zod"; -import { eventCalendarSchema, recurrenceSchema } from "@repo/schemas"; +import { recurrenceSchema } from "@repo/schemas"; export const conferenceEntryPointSchema = z.object({ joinUrl: z.object({ @@ -61,7 +61,13 @@ export const formSchema = z.object({ recurrence: recurrenceSchema.nullable().optional(), recurringEventId: z.string().optional(), description: z.string(), - calendar: eventCalendarSchema, + calendar: z.object({ + id: z.string(), + provider: z.object({ + id: z.enum(["google", "microsoft"]), + accountId: z.string(), + }), + }), attendees: z.array(attendeeSchema), conference: conferenceSchema.nullable().optional(), visibility: z.enum(["default", "public", "private", "confidential"]), diff --git a/apps/web/src/lib/db.ts b/apps/web/src/lib/db.ts index 95583fd7..be9f14e8 100644 --- a/apps/web/src/lib/db.ts +++ b/apps/web/src/lib/db.ts @@ -4,20 +4,18 @@ import { useLiveQuery } from "dexie-react-hooks"; import { SuperJSONResult } from "superjson"; import { Temporal } from "temporal-polyfill"; -import type { ProviderId } from "@repo/providers/interfaces"; import { startOfDay } from "@repo/temporal"; import { useZonedDateTime } from "@/components/calendar/context/datetime-provider"; import { Calendar, CalendarEvent } from "./interfaces"; import { superjson } from "./trpc/superjson"; -export interface EventRow - extends Omit< - CalendarEvent, - "start" | "end" | "createdAt" | "updatedAt" | "calendar" - > { +export interface EventRow extends Omit< + CalendarEvent, + "start" | "end" | "createdAt" | "updatedAt" | "calendar" +> { calendarId: string; - providerId: ProviderId; + providerId: "google" | "microsoft"; providerAccountId: string; start: SuperJSONResult; end: SuperJSONResult; @@ -28,7 +26,7 @@ export interface EventRow } export interface CalendarRow extends Omit { - providerId: ProviderId; + providerId: "google" | "microsoft"; providerAccountId: string; } diff --git a/apps/web/src/lib/interfaces.ts b/apps/web/src/lib/interfaces.ts index 8d35daaf..4dfa61c9 100644 --- a/apps/web/src/lib/interfaces.ts +++ b/apps/web/src/lib/interfaces.ts @@ -4,8 +4,6 @@ import type { CalendarEvent as ProviderCalendarEvent, } from "@repo/providers/interfaces"; -import type { RouterOutputs } from "./trpc"; - export type CalendarEvent = ProviderCalendarEvent & { type?: "draft" | "event"; }; diff --git a/packages/api/src/routers/accounts.ts b/packages/api/src/routers/accounts.ts index c2421b45..00bb0c36 100644 --- a/packages/api/src/routers/accounts.ts +++ b/packages/api/src/routers/accounts.ts @@ -1,7 +1,5 @@ import { TRPCError } from "@trpc/server"; -import type { ProviderId } from "@repo/providers/interfaces"; - import { createTRPCRouter, protectedProcedure } from "../trpc"; import { getAccounts, getDefaultAccount } from "../utils/accounts"; @@ -13,7 +11,7 @@ export const accountsRouter = createTRPCRouter({ accounts: accounts.map((account) => ({ id: account.id, provider: { - id: account.providerId as ProviderId, + id: account.providerId, accountId: account.accountId, }, name: account.name, @@ -38,7 +36,7 @@ export const accountsRouter = createTRPCRouter({ account: { id: account.id, provider: { - id: account.providerId as ProviderId, + id: account.providerId as "google" | "microsoft", accountId: account.accountId, }, name: account.name, diff --git a/packages/api/src/routers/calendars.ts b/packages/api/src/routers/calendars.ts index acd0f6d1..0decb901 100644 --- a/packages/api/src/routers/calendars.ts +++ b/packages/api/src/routers/calendars.ts @@ -2,9 +2,8 @@ import { TRPCError } from "@trpc/server"; import * as z from "zod"; import { auth } from "@repo/auth/server"; -import type { ProviderId } from "@repo/providers/interfaces"; import { assignColor } from "@repo/providers/calendars/colors"; -import { createCalendarInputSchema, providerSchema } from "@repo/schemas"; +import { createCalendarInputSchema } from "@repo/schemas"; import { calendarProcedure, @@ -51,7 +50,7 @@ export const calendarsRouter = createTRPCRouter({ return { id: account.id, provider: { - id: account.providerId as ProviderId, + id: account.providerId, accountId: account.accountId, }, name: account.email, @@ -112,7 +111,10 @@ export const calendarsRouter = createTRPCRouter({ get: calendarProcedure .input( z.object({ - provider: providerSchema, + provider: z.object({ + id: z.enum(["google", "microsoft"]), + accountId: z.string(), + }), calendarId: z.string(), }), ) @@ -136,7 +138,10 @@ export const calendarsRouter = createTRPCRouter({ .input( z.object({ id: z.string(), - provider: providerSchema, + provider: z.object({ + id: z.enum(["google", "microsoft"]), + accountId: z.string(), + }), name: z.string(), timeZone: z.string().optional(), }), @@ -180,7 +185,10 @@ export const calendarsRouter = createTRPCRouter({ delete: calendarProcedure .input( z.object({ - provider: providerSchema, + provider: z.object({ + id: z.enum(["google", "microsoft"]), + accountId: z.string(), + }), calendarId: z.string(), }), ) diff --git a/packages/api/src/routers/events.ts b/packages/api/src/routers/events.ts index 1be87c70..122e61da 100644 --- a/packages/api/src/routers/events.ts +++ b/packages/api/src/routers/events.ts @@ -5,12 +5,7 @@ import { zZonedDateTimeInstance } from "temporal-zod"; import * as z from "zod"; import { CalendarEvent } from "@repo/providers/interfaces"; -import { - createEventInputSchema, - eventCalendarSchema, - providerSchema, - updateEventInputSchema, -} from "@repo/schemas"; +import { createEventInputSchema, updateEventInputSchema } from "@repo/schemas"; import { toInstant } from "@repo/temporal"; import { calendarProcedure, createTRPCRouter } from "../trpc"; @@ -79,7 +74,12 @@ export const eventsRouter = createTRPCRouter({ z.object({ timeMin: zZonedDateTimeInstance.optional(), timeMax: zZonedDateTimeInstance.optional(), - calendar: eventCalendarSchema.extend({ + calendar: z.object({ + id: z.string(), + provider: z.object({ + id: z.enum(["google", "microsoft"]), + accountId: z.string(), + }), syncToken: z.string().optional(), }), timeZone: z.string().default("UTC"), @@ -126,7 +126,13 @@ export const eventsRouter = createTRPCRouter({ get: calendarProcedure .input( z.object({ - calendar: eventCalendarSchema, + calendar: z.object({ + id: z.string(), + provider: z.object({ + id: z.enum(["google", "microsoft"]), + accountId: z.string(), + }), + }), eventId: z.string(), timeZone: z.string().optional(), }), @@ -199,8 +205,20 @@ export const eventsRouter = createTRPCRouter({ data: updateEventInputSchema, move: z .object({ - source: eventCalendarSchema, - destination: eventCalendarSchema, + source: z.object({ + id: z.string(), + provider: z.object({ + id: z.enum(["google", "microsoft"]), + accountId: z.string(), + }), + }), + destination: z.object({ + id: z.string(), + provider: z.object({ + id: z.enum(["google", "microsoft"]), + accountId: z.string(), + }), + }), }) .optional(), }), @@ -257,7 +275,10 @@ export const eventsRouter = createTRPCRouter({ destinationProvider.client.calendars(), ]); - const sourceCalendar = findCalendarOrThrow(sourceCalendars, move.source.id); + const sourceCalendar = findCalendarOrThrow( + sourceCalendars, + move.source.id, + ); const destinationCalendar = findCalendarOrThrow( destinationCalendars, move.destination.id, @@ -265,7 +286,8 @@ export const eventsRouter = createTRPCRouter({ // If destination is the same as source, just update if ( - move.source.provider.accountId === move.destination.provider.accountId && + move.source.provider.accountId === + move.destination.provider.accountId && move.source.id === move.destination.id ) { const event = await sourceProvider.client.updateEvent( @@ -335,7 +357,13 @@ export const eventsRouter = createTRPCRouter({ delete: calendarProcedure .input( z.object({ - calendar: eventCalendarSchema, + calendar: z.object({ + id: z.string(), + provider: z.object({ + id: z.enum(["google", "microsoft"]), + accountId: z.string(), + }), + }), eventId: z.string(), sendUpdate: z.boolean().optional().default(true), }), @@ -364,14 +392,18 @@ export const eventsRouter = createTRPCRouter({ move: calendarProcedure .input( z.object({ - source: eventCalendarSchema.extend({ - provider: providerSchema.extend({ + source: z.object({ + id: z.string(), + provider: z.object({ id: z.literal("google"), + accountId: z.string(), }), }), - destination: eventCalendarSchema.extend({ - provider: providerSchema.extend({ + destination: z.object({ + id: z.string(), + provider: z.object({ id: z.literal("google"), + accountId: z.string(), }), }), eventId: z.string(), @@ -444,7 +476,13 @@ export const eventsRouter = createTRPCRouter({ respondToInvite: calendarProcedure .input( z.object({ - calendar: eventCalendarSchema, + calendar: z.object({ + id: z.string(), + provider: z.object({ + id: z.enum(["google", "microsoft"]), + accountId: z.string(), + }), + }), eventId: z.string(), response: z.object({ status: z.enum(["accepted", "tentative", "declined", "unknown"]), @@ -466,15 +504,11 @@ export const eventsRouter = createTRPCRouter({ }); } - await provider.client.responseToEvent( - input.calendar.id, - input.eventId, - { - status: input.response.status, - comment: input.response.comment, - sendUpdate: input.response.sendUpdate, - }, - ); + await provider.client.responseToEvent(input.calendar.id, input.eventId, { + status: input.response.status, + comment: input.response.comment, + sendUpdate: input.response.sendUpdate, + }); return { success: true }; }), diff --git a/packages/api/src/routers/tasks.ts b/packages/api/src/routers/tasks.ts index 76d14d2b..0728484e 100644 --- a/packages/api/src/routers/tasks.ts +++ b/packages/api/src/routers/tasks.ts @@ -1,6 +1,5 @@ import { TRPCError } from "@trpc/server"; -import type { ProviderId } from "@repo/providers/interfaces"; import { createTaskInputSchema } from "@repo/schemas"; import { createTRPCRouter, taskProcedure } from "../trpc"; @@ -31,7 +30,7 @@ export const tasksRouter = createTRPCRouter({ return { id: account.id, provider: { - id: account.providerId as ProviderId, + id: account.providerId, accountId: account.accountId, }, name: account.email, diff --git a/packages/providers/src/calendars/google-calendar.ts b/packages/providers/src/calendars/google-calendar.ts index 7d98cb5b..f633965f 100644 --- a/packages/providers/src/calendars/google-calendar.ts +++ b/packages/providers/src/calendars/google-calendar.ts @@ -40,7 +40,10 @@ export class GoogleCalendarProvider implements CalendarProvider { public readonly providerAccountId: string; private client: GoogleCalendar; - constructor({ accessToken, providerAccountId }: GoogleCalendarProviderOptions) { + constructor({ + accessToken, + providerAccountId, + }: GoogleCalendarProviderOptions) { this.providerAccountId = providerAccountId; this.client = new GoogleCalendar({ accessToken, diff --git a/packages/providers/src/calendars/microsoft-calendar.ts b/packages/providers/src/calendars/microsoft-calendar.ts index 1b4fc7a5..082e9085 100644 --- a/packages/providers/src/calendars/microsoft-calendar.ts +++ b/packages/providers/src/calendars/microsoft-calendar.ts @@ -51,7 +51,10 @@ export class MicrosoftCalendarProvider implements CalendarProvider { public readonly providerAccountId: string; private graphClient: Client; - constructor({ accessToken, providerAccountId }: MicrosoftCalendarProviderOptions) { + constructor({ + accessToken, + providerAccountId, + }: MicrosoftCalendarProviderOptions) { this.providerAccountId = providerAccountId; this.graphClient = Client.initWithMiddleware({ authProvider: { @@ -70,7 +73,10 @@ export class MicrosoftCalendarProvider implements CalendarProvider { .get(); return (response.value as MicrosoftCalendar[]).map((calendar) => ({ - ...parseMicrosoftCalendar({ calendar, providerAccountId: this.providerAccountId }), + ...parseMicrosoftCalendar({ + calendar, + providerAccountId: this.providerAccountId, + }), })); }); } @@ -152,8 +158,7 @@ export class MicrosoftCalendarProvider implements CalendarProvider { .get(); const events = (response.value as MicrosoftEvent[]).map( - (event: MicrosoftEvent) => - parseMicrosoftEvent({ event, calendar }), + (event: MicrosoftEvent) => parseMicrosoftEvent({ event, calendar }), ); return { events, recurringMasterEvents: [] }; diff --git a/packages/providers/src/interfaces/calendars.ts b/packages/providers/src/interfaces/calendars.ts index ef55b358..60b4a2b7 100644 --- a/packages/providers/src/interfaces/calendars.ts +++ b/packages/providers/src/interfaces/calendars.ts @@ -1,15 +1,11 @@ import type { Temporal } from "temporal-polyfill"; -export type ProviderId = "google" | "microsoft"; - -export interface Provider { - id: ProviderId; - accountId: string; -} - export interface Calendar { id: string; - provider: Provider; + provider: { + id: "google" | "microsoft"; + accountId: string; + }; name: string; description?: string; etag?: string; diff --git a/packages/providers/src/interfaces/events.ts b/packages/providers/src/interfaces/events.ts index d5c3fb1e..cb424e02 100644 --- a/packages/providers/src/interfaces/events.ts +++ b/packages/providers/src/interfaces/events.ts @@ -1,12 +1,5 @@ import type { Temporal } from "temporal-polyfill"; -import type { Provider } from "./calendars"; - -export interface CalendarEventCalendar { - id: string; - provider: Provider; -} - export interface CalendarEvent { id: string; title?: string; @@ -23,7 +16,13 @@ export interface CalendarEvent { color?: string | null; visibility?: "default" | "public" | "private" | "confidential"; readOnly: boolean; - calendar: CalendarEventCalendar; + calendar: { + id: string; + provider: { + id: "google" | "microsoft"; + accountId: string; + }; + }; createdAt?: Temporal.Instant; updatedAt?: Temporal.Instant; response?: { @@ -45,7 +44,13 @@ export type CalendarEventSyncItem = status: "deleted"; event: { id: string; - calendar: CalendarEventCalendar; + calendar: { + id: string; + provider: { + id: "google" | "microsoft"; + accountId: string; + }; + }; }; }; diff --git a/packages/schemas/src/calendars.ts b/packages/schemas/src/calendars.ts index 5ad3e8ec..55259863 100644 --- a/packages/schemas/src/calendars.ts +++ b/packages/schemas/src/calendars.ts @@ -1,12 +1,13 @@ import * as z from "zod"; -import { providerSchema } from "./events"; - export const createCalendarInputSchema = z.object({ name: z.string(), description: z.string().optional(), timeZone: z.string().optional(), - provider: providerSchema, + provider: z.object({ + id: z.enum(["google", "microsoft"]), + accountId: z.string(), + }), }); export const updateCalendarInputSchema = createCalendarInputSchema.extend({ diff --git a/packages/schemas/src/events.ts b/packages/schemas/src/events.ts index b246f157..75e32b72 100644 --- a/packages/schemas/src/events.ts +++ b/packages/schemas/src/events.ts @@ -121,16 +121,6 @@ export const dateInputSchema = z.union([ zZonedDateTimeInstance, ]); -export const providerSchema = z.object({ - id: z.enum(["google", "microsoft"]), - accountId: z.string(), -}); - -export const eventCalendarSchema = z.object({ - id: z.string(), - provider: providerSchema, -}); - const attendeeSchema = z.object({ id: z.string().optional(), email: z.string().email(), @@ -230,7 +220,13 @@ export const createEventInputSchema = z.object({ visibility: z .enum(["default", "public", "private", "confidential"]) .optional(), - calendar: eventCalendarSchema, + calendar: z.object({ + id: z.string(), + provider: z.object({ + id: z.enum(["google", "microsoft"]), + accountId: z.string(), + }), + }), readOnly: z.boolean(), metadata: z.union([microsoftMetadataSchema, googleMetadataSchema]).optional(), attendees: z.array(attendeeSchema).optional(), @@ -265,7 +261,13 @@ export const patchEventInputSchema = z.object({ visibility: z .enum(["default", "public", "private", "confidential"]) .optional(), - calendar: eventCalendarSchema, + calendar: z.object({ + id: z.string(), + provider: z.object({ + id: z.enum(["google", "microsoft"]), + accountId: z.string(), + }), + }), readOnly: z.boolean(), attendees: z.array(attendeeSchema).optional(), conference: conferenceSchema.optional(), diff --git a/packages/schemas/src/tasks.ts b/packages/schemas/src/tasks.ts index 918e2670..c9b28f57 100644 --- a/packages/schemas/src/tasks.ts +++ b/packages/schemas/src/tasks.ts @@ -1,8 +1,6 @@ import { Temporal } from "temporal-polyfill"; import * as z from "zod"; -import { providerSchema } from "./events"; - export const createTaskCollectionInputSchema = z.object({ title: z.string(), }); @@ -11,7 +9,10 @@ export const createTaskInputSchema = z.object({ title: z.string(), taskCollectionId: z.string(), description: z.string().optional(), - provider: providerSchema, + provider: z.object({ + id: z.enum(["google", "microsoft"]), + accountId: z.string(), + }), completed: z .union([ z.instanceof(Temporal.PlainDate), From dde369a460041f80d11ff05d8d548a5b9e606d1c Mon Sep 17 00:00:00 2001 From: "Jean P.D. Meijer" Date: Tue, 2 Dec 2025 00:40:27 +0100 Subject: [PATCH 3/3] wip --- apps/web/src/lib/trpc/client.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/lib/trpc/client.tsx b/apps/web/src/lib/trpc/client.tsx index 66a045a2..ea7e5b7e 100644 --- a/apps/web/src/lib/trpc/client.tsx +++ b/apps/web/src/lib/trpc/client.tsx @@ -98,7 +98,7 @@ export function TRPCReactProvider(props: Readonly) { return ( {props.children}