Skip to content
File

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

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