Skip to content
File

Blob: src/worker/objects/cal-dav/calendar-objects.ts

typescript159 lines
1import { eq } from "drizzle-orm";
2 
3import { parseICalendarBytes } from "@/worker/caldav/ical/parse";
4import type { ParsedICalendar } from "@/worker/caldav/ical/types";
5import { calendarObjectHref as objectHref } from "@/worker/caldav/paths";
6import type { CalDavDoDb } from "@/worker/db/cal-dav-do/client";
7import { calendarIndex, calendarObjects, type CalendarRow } from "@/worker/db/cal-dav-do/schema";
8import {
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";
22import {
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";
31import { calendarWritePrecondition } from "@/worker/objects/cal-dav/preconditions";
32import {
33 collectionMultiget,
34 deleteCollectionObject,
35 getCollectionObject,
36 putCollectionObject,
37 syncCollectionDomain,
38 type CollectionObjectSpec,
39} from "@/worker/objects/common/collection-object-domain";
40 
41export function getObject(db: CalDavDoDb, input: GetCalendarObjectInput) {
42 return getCollectionObject(db, { ...input, collectionName: input.calendarName }, calendarObjectSpec);
43}
44 
45export async function putObject(db: CalDavDoDb, input: PutCalendarObjectInput) {
46 const putInput: CalendarPutObjectDomainInput = { ...input, collectionName: input.calendarName };
47 return await putCollectionObject(db, putInput, calendarObjectSpec);
48}
49 
50export function deleteObject(db: CalDavDoDb, input: DeleteCalendarObjectInput) {
51 return deleteCollectionObject(db, { ...input, collectionName: input.calendarName }, calendarObjectSpec);
52}
53 
54export 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 
64export function calendarMultiget(db: CalDavDoDb, input: CalendarMultigetInput) {
65 return collectionMultiget(db, { ...input, collectionName: input.calendarName }, calendarObjectSpec);
66}
67 
68export function syncCollection(db: CalDavDoDb, input: SyncCollectionInput) {
69 return syncCollectionDomain(db, { ...input, collectionName: input.calendarName }, calendarObjectSpec);
70}
71 
72type CalendarObjectRow = NonNullable<ReturnType<typeof objectByName>>;
73type CalendarPutObjectDomainInput = PutCalendarObjectInput & { collectionName: string };
74type CalendarObjectSpec = CollectionObjectSpec<
75 CalDavDoDb,
76 CalDavDoDb,
77 CalendarRow,
78 CalendarObjectRow,
79 CalendarPutObjectDomainInput,
80 ParsedICalendar,
81 ReturnType<typeof err>
82>;
83 
84function parseCalendarObject(input: CalendarPutObjectDomainInput): ParsedICalendar {
85 return parseICalendarBytes(input.body, input.maxBytes, {
86 maxYears: input.maxRecurrenceYears,
87 maxInstances: input.maxRecurrenceInstances,
88 });
89}
90 
91const 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;