import { eq } from "drizzle-orm"; import { parseICalendarBytes } from "@/worker/caldav/ical/parse"; import type { ParsedICalendar } from "@/worker/caldav/ical/types"; import { calendarObjectHref as objectHref } from "@/worker/caldav/paths"; import type { CalDavDoDb } from "@/worker/db/cal-dav-do/client"; import { calendarIndex, calendarObjects, type CalendarRow } from "@/worker/db/cal-dav-do/schema"; import { calendarIndexRow, calendarObjectId, calendarSyncToken as syncToken, changesSince, ensureCalendar, latestChangeSeq, objectByName, objectByUid, objectResourceFromHref, objectResources, recordChange, syncTokenSeq, } from "@/worker/db/cal-dav-do/repository"; import { calDavError as err, type CalendarMultigetInput, type CalendarQueryInput, type DeleteCalendarObjectInput, type GetCalendarObjectInput, type PutCalendarObjectInput, type SyncCollectionInput, } from "@/worker/objects/cal-dav/types"; import { calendarWritePrecondition } from "@/worker/objects/cal-dav/preconditions"; import { collectionMultiget, deleteCollectionObject, getCollectionObject, putCollectionObject, syncCollectionDomain, type CollectionObjectSpec, } from "@/worker/objects/common/collection-object-domain"; export function getObject(db: CalDavDoDb, input: GetCalendarObjectInput) { return getCollectionObject(db, { ...input, collectionName: input.calendarName }, calendarObjectSpec); } export async function putObject(db: CalDavDoDb, input: PutCalendarObjectInput) { const putInput: CalendarPutObjectDomainInput = { ...input, collectionName: input.calendarName }; return await putCollectionObject(db, putInput, calendarObjectSpec); } export function deleteObject(db: CalDavDoDb, input: DeleteCalendarObjectInput) { return deleteCollectionObject(db, { ...input, collectionName: input.calendarName }, calendarObjectSpec); } export function calendarQuery(db: CalDavDoDb, input: CalendarQueryInput) { const calendar = ensureCalendar(db, input.subjectId, input.nowMs, input.calendarName); if (!calendar) return err(404, "not_found", "Calendar not found"); const { resources, truncated } = objectResources(db, calendar, input.maxResults, input.filters, { maxYears: input.maxRecurrenceYears, maxInstances: input.maxRecurrenceInstances, }); return { ok: true as const, resources, truncated }; } export function calendarMultiget(db: CalDavDoDb, input: CalendarMultigetInput) { return collectionMultiget(db, { ...input, collectionName: input.calendarName }, calendarObjectSpec); } export function syncCollection(db: CalDavDoDb, input: SyncCollectionInput) { return syncCollectionDomain(db, { ...input, collectionName: input.calendarName }, calendarObjectSpec); } type CalendarObjectRow = NonNullable>; type CalendarPutObjectDomainInput = PutCalendarObjectInput & { collectionName: string }; type CalendarObjectSpec = CollectionObjectSpec< CalDavDoDb, CalDavDoDb, CalendarRow, CalendarObjectRow, CalendarPutObjectDomainInput, ParsedICalendar, ReturnType >; function parseCalendarObject(input: CalendarPutObjectDomainInput): ParsedICalendar { return parseICalendarBytes(input.body, input.maxBytes, { maxYears: input.maxRecurrenceYears, maxInstances: input.maxRecurrenceInstances, }); } const calendarObjectSpec = { ensureCollection: ensureCalendar, objectByName, objectByUid, objectResourceFromHref, objectResources, changesSince, latestChangeSeq, syncTokenSeq, syncToken, objectHref, parseObject: parseCalendarObject, createObjectId: calendarObjectId, writePrecondition: calendarWritePrecondition, transaction: (db, callback) => db.transaction((tx) => callback(tx)), saveObject: (tx, input) => { if (input.existing) { tx.update(calendarObjects) .set({ uid: input.parsed.uid, componentType: input.parsed.componentType, body: input.parsed.body, etag: input.etag, size: input.parsed.size, modifiedAtMs: input.nowMs, version: input.version, }) .where(eq(calendarObjects.id, input.existing.id)) .run(); tx.delete(calendarIndex).where(eq(calendarIndex.objectId, input.existing.id)).run(); } else { tx.insert(calendarObjects) .values({ id: input.objectId, calendarId: input.collection.id, name: input.objectName, uid: input.parsed.uid, componentType: input.parsed.componentType, body: input.parsed.body, etag: input.etag, size: input.parsed.size, createdAtMs: input.nowMs, modifiedAtMs: input.nowMs, version: input.version, }) .run(); } tx.insert(calendarIndex) .values(calendarIndexRow(input.objectId, input.collection.id, input.parsed)) .run(); }, deleteObject: (tx, object) => { tx.delete(calendarObjects).where(eq(calendarObjects.id, object.id)).run(); }, recordChange, collectionNotFound: () => err(404, "not_found", "Calendar not found"), objectNotFound: () => err(404, "not_found", "Calendar object not found"), parseFailure: (input, cause) => err( input.body.byteLength > input.maxBytes ? 413 : 400, input.body.byteLength > input.maxBytes ? "payload_too_large" : "bad_request", cause instanceof Error ? cause.message : "Invalid iCalendar", ), uidAlreadyExists: () => err(409, "conflict", "iCalendar UID already exists"), uidCannotChange: () => err(409, "conflict", "iCalendar UID cannot change at an existing href"), multigetLimitExceeded: () => err(403, "forbidden", "calendar-multiget exceeds result limit"), invalidSyncToken: () => err(400, "bad_request", "Invalid sync token"), } satisfies CalendarObjectSpec;