Skip to content

Built for people who want to own their automations. Join the waitlist for an invite.

Package listing

@kody/microsoft

src/calendar.ts

159 lines · 5.1 KB · TypeScript
import { resolveMicrosoftIntegration, type MicrosoftAccountParams } from './accounts.ts'
import { graphItems, graphRequest, requireConfirmOrDryRun, type JsonObject } from './core.ts'
import { boundedTop } from './validation.ts'

export type CalendarListParams = MicrosoftAccountParams & {
  top?: number
}

export type CalendarEventsParams = MicrosoftAccountParams & {
  calendarId?: string
  startDateTime?: string
  endDateTime?: string
  top?: number
  filter?: string
  search?: string
  orderBy?: string
  select?: string
}

export type CalendarEventGetParams = MicrosoftAccountParams & {
  eventId: string
  calendarId?: string
  select?: string
}

export type CalendarEventMutationParams = MicrosoftAccountParams & {
  calendarId?: string
  event: JsonObject
  dryRun?: boolean
  confirm?: boolean
}

export type CalendarEventUpdateParams = CalendarEventMutationParams & {
  eventId: string
}

const EVENT_SELECT =
  'id,subject,start,end,location,organizer,attendees,isAllDay,showAs,webLink,onlineMeeting,bodyPreview'

function eventsPath(calendarId?: string): string {
  return calendarId
    ? '/me/calendars/' + encodeURIComponent(calendarId) + '/events'
    : '/me/events'
}

function calendarViewPath(calendarId?: string): string {
  return calendarId
    ? '/me/calendars/' + encodeURIComponent(calendarId) + '/calendarView'
    : '/me/calendarView'
}

/** List calendars the signed-in user can access. */
export async function listCalendars(params: CalendarListParams = {}) {
  const data = await graphRequest<JsonObject>({
    integration: resolveMicrosoftIntegration(params),
    path: '/me/calendars',
    query: {
      $top: boundedTop(params.top, 20, 100),
      $select: 'id,name,color,isDefaultCalendar,canEdit,owner',
    },
  })
  return graphItems<JsonObject>(data)
}

/** List events. Pass startDateTime+endDateTime to use calendarView. */
export async function listEvents(params: CalendarEventsParams = {}) {
  const top = boundedTop(params.top, 10)
  const select = params.select ?? EVENT_SELECT
  const query: Record<string, string | number | undefined> = {
    $top: top,
    $select: select,
    $orderby: params.orderBy,
    $filter: params.filter,
  }
  if (params.search) query.$search = '"' + params.search.replace(/"/g, '\\"') + '"'
  const ranged = Boolean(params.startDateTime && params.endDateTime)
  if (ranged) {
    query.startDateTime = params.startDateTime
    query.endDateTime = params.endDateTime
  }
  const data = await graphRequest<JsonObject>({
    integration: resolveMicrosoftIntegration(params),
    path: ranged ? calendarViewPath(params.calendarId) : eventsPath(params.calendarId),
    query,
  })
  return graphItems<JsonObject>(data)
}

/** Read one calendar event. */
export async function getEvent(params: CalendarEventGetParams) {
  const eventId = params.eventId?.trim()
  if (!eventId) throw new Error('getEvent requires eventId.')
  const path = params.calendarId
    ? '/me/calendars/' + encodeURIComponent(params.calendarId) + '/events/' + encodeURIComponent(eventId)
    : '/me/events/' + encodeURIComponent(eventId)
  return graphRequest<JsonObject>({
    integration: resolveMicrosoftIntegration(params),
    path,
    query: { $select: params.select ?? EVENT_SELECT + ',body' },
  })
}

/** Preview or create a calendar event. Creating requires confirm: true. */
export async function createEvent(params: CalendarEventMutationParams) {
  if (!params.event || typeof params.event !== 'object') {
    throw new Error('createEvent requires event.')
  }
  const path = eventsPath(params.calendarId)
  const mode = requireConfirmOrDryRun({
    dryRun: params.dryRun,
    confirm: params.confirm,
    action: 'createEvent',
  })
  if (mode.dryRun) {
    return { dryRun: true as const, method: 'POST', path, body: params.event }
  }
  return graphRequest<JsonObject>({
    integration: resolveMicrosoftIntegration(params),
    method: 'POST',
    path,
    body: params.event,
  })
}

/** Preview or patch a calendar event. Updating requires confirm: true. */
export async function updateEvent(params: CalendarEventUpdateParams) {
  const eventId = params.eventId?.trim()
  if (!eventId) throw new Error('updateEvent requires eventId.')
  if (!params.event || typeof params.event !== 'object') {
    throw new Error('updateEvent requires event.')
  }
  const path = params.calendarId
    ? '/me/calendars/' + encodeURIComponent(params.calendarId) + '/events/' + encodeURIComponent(eventId)
    : '/me/events/' + encodeURIComponent(eventId)
  const mode = requireConfirmOrDryRun({
    dryRun: params.dryRun,
    confirm: params.confirm,
    action: 'updateEvent',
  })
  if (mode.dryRun) {
    return { dryRun: true as const, method: 'PATCH', path, body: params.event }
  }
  return graphRequest<JsonObject>({
    integration: resolveMicrosoftIntegration(params),
    method: 'PATCH',
    path,
    body: params.event,
  })
}

/**
 * Outlook calendar helpers for Microsoft Graph.
 * @example
 * import calendar from 'kody:@kody/microsoft/calendar'
 * const { items } = await calendar().listEvents({ startDateTime: '2026-08-22T00:00:00Z', endDateTime: '2026-08-23T00:00:00Z' })
 */
export default function calendar() {
  return { listCalendars, listEvents, getEvent, createEvent, updateEvent }
}