Skip to content
File

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

typescript122 lines
1import { and, eq, sql } from "drizzle-orm";
2 
3import { calendarHref, calendarObjectHref } from "@/worker/caldav/paths";
4import type { CalDavDoDb } from "@/worker/db/cal-dav-do/client";
5import { calendarDeadProps, calendars, type CalendarRow } from "@/worker/db/cal-dav-do/schema";
6import {
7 calendarResource,
8 calendarSyncToken as syncToken,
9 ensureCalendar,
10 ensureDefaultCalendar,
11 homeResource,
12 listCalendars,
13 objectByName,
14 objectResource,
15 objectResources,
16} from "@/worker/db/cal-dav-do/repository";
17import {
18 calDavError as err,
19 type CalDavError,
20 type CalendarResource,
21 type DescribeInput,
22 type EnsureInitializedInput,
23 type ProppatchInput,
24} from "@/worker/objects/cal-dav/types";
25import { calendarWritePrecondition } from "@/worker/objects/cal-dav/preconditions";
26import { describeCollectionDomain, type CollectionDescribeSpec } from "@/worker/objects/common/collection-describe";
27import { proppatchCollectionDomain, type CollectionProppatchSpec } from "@/worker/objects/common/collection-proppatch";
28 
29export function ensureInitialized(db: CalDavDoDb, input: EnsureInitializedInput): { ok: true } {
30 ensureDefaultCalendar(db, input.subjectId, input.nowMs);
31 return { ok: true };
32}
33 
34export function describe(
35 db: CalDavDoDb,
36 input: DescribeInput,
37): { ok: true; resources: CalendarResource[]; syncToken: string } | CalDavError {
38 return describeCollectionDomain(db, { ...input, collectionName: input.calendarName }, calendarDescribeSpec);
39}
40 
41export function proppatch(db: CalDavDoDb, input: ProppatchInput): { ok: true } | CalDavError {
42 return proppatchCollectionDomain(db, { ...input, collectionName: input.calendarName }, calendarProppatchSpec);
43}
44 
45function targetHref(calendar: CalendarRow, kind: "home" | "calendar" | "object", objectName?: string): string {
46 if (kind === "home") return "/calendars/";
47 if (kind === "calendar") return calendarHref(calendar.name);
48 return calendarObjectHref(calendar.name, objectName ?? "");
49}
50 
51const calendarDescribeSpec = {
52 collectionKind: "calendar",
53 ensureCollection: ensureCalendar,
54 homeResource,
55 listCollections: listCalendars,
56 collectionResource: calendarResource,
57 objectByName,
58 objectResource,
59 objectResources,
60 syncToken,
61 collectionNotFound: () => err(404, "not_found", "Calendar not found"),
62 objectNotFound: () => err(404, "not_found", "Calendar object not found"),
63} satisfies CollectionDescribeSpec<
64 CalDavDoDb,
65 CalendarRow,
66 NonNullable<ReturnType<typeof objectByName>>,
67 CalendarResource,
68 CalDavError
69>;
70 
71const calendarProppatchSpec = {
72 collectionKind: "calendar",
73 ensureDefaultCollection: ensureDefaultCalendar,
74 ensureCollection: ensureCalendar,
75 objectByName,
76 targetHref,
77 writePrecondition: calendarWritePrecondition,
78 transaction: (db, callback) => db.transaction((tx) => callback(tx)),
79 updateCollectionDisplayName: (tx, collectionId, displayName, nowMs) => {
80 // RFC 4791 4.2: DAV:displayname on a calendar collection is a live
81 // property mapped to the calendar's displayName column.
82 tx.update(calendars)
83 .set({ displayName, modifiedAtMs: nowMs, syncSeq: sql`${calendars.syncSeq} + 1` })
84 .where(eq(calendars.id, collectionId))
85 .run();
86 },
87 upsertDeadProp: (tx, key, xmlValue) => {
88 tx.insert(calendarDeadProps)
89 .values({ ...key, resourceKind: key.resourceKind as "home" | "calendar" | "object", xmlValue })
90 .onConflictDoUpdate({
91 target: [
92 calendarDeadProps.resourceKind,
93 calendarDeadProps.resourceId,
94 calendarDeadProps.nsUri,
95 calendarDeadProps.localName,
96 ],
97 set: { xmlValue },
98 })
99 .run();
100 },
101 deleteDeadProp: (tx, key) => {
102 tx.delete(calendarDeadProps)
103 .where(
104 and(
105 eq(calendarDeadProps.resourceKind, key.resourceKind as "home" | "calendar" | "object"),
106 eq(calendarDeadProps.resourceId, key.resourceId),
107 eq(calendarDeadProps.nsUri, key.nsUri),
108 eq(calendarDeadProps.localName, key.localName),
109 ),
110 )
111 .run();
112 },
113 collectionNotFound: () => err(404, "not_found", "Calendar not found"),
114 resourceNotFound: () => err(404, "not_found", "Resource not found"),
115} satisfies CollectionProppatchSpec<
116 CalDavDoDb,
117 CalDavDoDb,
118 CalendarRow,
119 NonNullable<ReturnType<typeof objectByName>>,
120 CalDavError
121>;