File
Blob: src/worker/objects/cal-dav/calendar-objects.ts
| 1 | import { eq } from "drizzle-orm"; |
| 2 | |
| 3 | import { parseICalendarBytes } from "@/worker/caldav/ical/parse"; |
| 4 | import type { ParsedICalendar } from "@/worker/caldav/ical/types"; |
| 5 | import { calendarObjectHref as objectHref } from "@/worker/caldav/paths"; |
| 6 | import type { CalDavDoDb } from "@/worker/db/cal-dav-do/client"; |
| 7 | import { calendarIndex, calendarObjects, type CalendarRow } from "@/worker/db/cal-dav-do/schema"; |
| 8 | import { |
| 9 | calendarIndexRow, |
| 10 | calendarObjectId, |
| 11 | calendarSyncToken as syncToken, |
| 12 | changesSince, |
| 13 | ensureCalendar, |
| 14 | latestChangeSeq, |
| 15 | objectByName, |
| 16 | objectByUid, |
| 17 | objectResourceFromHref, |
| 18 | objectResources, |
| 19 | recordChange, |
| 20 | syncTokenSeq, |
| 21 | } from "@/worker/db/cal-dav-do/repository"; |
| 22 | import { |
| 23 | calDavError as err, |
| 24 | type CalendarMultigetInput, |
| 25 | type CalendarQueryInput, |
| 26 | type DeleteCalendarObjectInput, |
| 27 | type GetCalendarObjectInput, |
| 28 | type PutCalendarObjectInput, |
| 29 | type SyncCollectionInput, |
| 30 | } from "@/worker/objects/cal-dav/types"; |
| 31 | import { calendarWritePrecondition } from "@/worker/objects/cal-dav/preconditions"; |
| 32 | import { |
| 33 | collectionMultiget, |
| 34 | deleteCollectionObject, |
| 35 | getCollectionObject, |
| 36 | putCollectionObject, |
| 37 | syncCollectionDomain, |
| 38 | type CollectionObjectSpec, |
| 39 | } from "@/worker/objects/common/collection-object-domain"; |
| 40 | |
| 41 | export function getObject(db: CalDavDoDb, input: GetCalendarObjectInput) { |
| 42 | return getCollectionObject(db, { ...input, collectionName: input.calendarName }, calendarObjectSpec); |
| 43 | } |
| 44 | |
| 45 | export async function putObject(db: CalDavDoDb, input: PutCalendarObjectInput) { |
| 46 | const putInput: CalendarPutObjectDomainInput = { ...input, collectionName: input.calendarName }; |
| 47 | return await putCollectionObject(db, putInput, calendarObjectSpec); |
| 48 | } |
| 49 | |
| 50 | export function deleteObject(db: CalDavDoDb, input: DeleteCalendarObjectInput) { |
| 51 | return deleteCollectionObject(db, { ...input, collectionName: input.calendarName }, calendarObjectSpec); |
| 52 | } |
| 53 | |
| 54 | export function calendarQuery(db: CalDavDoDb, input: CalendarQueryInput) { |
| 55 | const calendar = ensureCalendar(db, input.subjectId, input.nowMs, input.calendarName); |
| 56 | if (!calendar) return err(404, "not_found", "Calendar not found"); |
| 57 | const { resources, truncated } = objectResources(db, calendar, input.maxResults, input.filters, { |
| 58 | maxYears: input.maxRecurrenceYears, |
| 59 | maxInstances: input.maxRecurrenceInstances, |
| 60 | }); |
| 61 | return { ok: true as const, resources, truncated }; |
| 62 | } |
| 63 | |
| 64 | export function calendarMultiget(db: CalDavDoDb, input: CalendarMultigetInput) { |
| 65 | return collectionMultiget(db, { ...input, collectionName: input.calendarName }, calendarObjectSpec); |
| 66 | } |
| 67 | |
| 68 | export function syncCollection(db: CalDavDoDb, input: SyncCollectionInput) { |
| 69 | return syncCollectionDomain(db, { ...input, collectionName: input.calendarName }, calendarObjectSpec); |
| 70 | } |
| 71 | |
| 72 | type CalendarObjectRow = NonNullable<ReturnType<typeof objectByName>>; |
| 73 | type CalendarPutObjectDomainInput = PutCalendarObjectInput & { collectionName: string }; |
| 74 | type CalendarObjectSpec = CollectionObjectSpec< |
| 75 | CalDavDoDb, |
| 76 | CalDavDoDb, |
| 77 | CalendarRow, |
| 78 | CalendarObjectRow, |
| 79 | CalendarPutObjectDomainInput, |
| 80 | ParsedICalendar, |
| 81 | ReturnType<typeof err> |
| 82 | >; |
| 83 | |
| 84 | function parseCalendarObject(input: CalendarPutObjectDomainInput): ParsedICalendar { |
| 85 | return parseICalendarBytes(input.body, input.maxBytes, { |
| 86 | maxYears: input.maxRecurrenceYears, |
| 87 | maxInstances: input.maxRecurrenceInstances, |
| 88 | }); |
| 89 | } |
| 90 | |
| 91 | const calendarObjectSpec = { |
| 92 | ensureCollection: ensureCalendar, |
| 93 | objectByName, |
| 94 | objectByUid, |
| 95 | objectResourceFromHref, |
| 96 | objectResources, |
| 97 | changesSince, |
| 98 | latestChangeSeq, |
| 99 | syncTokenSeq, |
| 100 | syncToken, |
| 101 | objectHref, |
| 102 | parseObject: parseCalendarObject, |
| 103 | createObjectId: calendarObjectId, |
| 104 | writePrecondition: calendarWritePrecondition, |
| 105 | transaction: (db, callback) => db.transaction((tx) => callback(tx)), |
| 106 | saveObject: (tx, input) => { |
| 107 | if (input.existing) { |
| 108 | tx.update(calendarObjects) |
| 109 | .set({ |
| 110 | uid: input.parsed.uid, |
| 111 | componentType: input.parsed.componentType, |
| 112 | body: input.parsed.body, |
| 113 | etag: input.etag, |
| 114 | size: input.parsed.size, |
| 115 | modifiedAtMs: input.nowMs, |
| 116 | version: input.version, |
| 117 | }) |
| 118 | .where(eq(calendarObjects.id, input.existing.id)) |
| 119 | .run(); |
| 120 | tx.delete(calendarIndex).where(eq(calendarIndex.objectId, input.existing.id)).run(); |
| 121 | } else { |
| 122 | tx.insert(calendarObjects) |
| 123 | .values({ |
| 124 | id: input.objectId, |
| 125 | calendarId: input.collection.id, |
| 126 | name: input.objectName, |
| 127 | uid: input.parsed.uid, |
| 128 | componentType: input.parsed.componentType, |
| 129 | body: input.parsed.body, |
| 130 | etag: input.etag, |
| 131 | size: input.parsed.size, |
| 132 | createdAtMs: input.nowMs, |
| 133 | modifiedAtMs: input.nowMs, |
| 134 | version: input.version, |
| 135 | }) |
| 136 | .run(); |
| 137 | } |
| 138 | tx.insert(calendarIndex) |
| 139 | .values(calendarIndexRow(input.objectId, input.collection.id, input.parsed)) |
| 140 | .run(); |
| 141 | }, |
| 142 | deleteObject: (tx, object) => { |
| 143 | tx.delete(calendarObjects).where(eq(calendarObjects.id, object.id)).run(); |
| 144 | }, |
| 145 | recordChange, |
| 146 | collectionNotFound: () => err(404, "not_found", "Calendar not found"), |
| 147 | objectNotFound: () => err(404, "not_found", "Calendar object not found"), |
| 148 | parseFailure: (input, cause) => |
| 149 | err( |
| 150 | input.body.byteLength > input.maxBytes ? 413 : 400, |
| 151 | input.body.byteLength > input.maxBytes ? "payload_too_large" : "bad_request", |
| 152 | cause instanceof Error ? cause.message : "Invalid iCalendar", |
| 153 | ), |
| 154 | uidAlreadyExists: () => err(409, "conflict", "iCalendar UID already exists"), |
| 155 | uidCannotChange: () => err(409, "conflict", "iCalendar UID cannot change at an existing href"), |
| 156 | multigetLimitExceeded: () => err(403, "forbidden", "calendar-multiget exceeds result limit"), |
| 157 | invalidSyncToken: () => err(400, "bad_request", "Invalid sync token"), |
| 158 | } satisfies CalendarObjectSpec; |