Skip to content
File

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

typescript155 lines
1import { eq } from "drizzle-orm";
2 
3import { addressObjectHref as objectHref } from "@/worker/carddav/paths";
4import { parseVCardBytes, type ParsedVCard } from "@/worker/carddav/vcard";
5import type { CardDavDoDb } from "@/worker/db/card-dav-do/client";
6import { addressIndex, addressObjects, type AddressbookRow } from "@/worker/db/card-dav-do/schema";
7import {
8 addressIndexRow,
9 addressObjectId,
10 addressSyncToken as syncToken,
11 changesSince,
12 ensureAddressbook,
13 latestChangeSeq,
14 objectByName,
15 objectByUid,
16 objectResourceFromHref,
17 objectResources,
18 recordChange,
19 syncTokenSeq,
20} from "@/worker/db/card-dav-do/repository";
21import {
22 cardDavError as err,
23 type AddressbookMultigetInput,
24 type AddressbookQueryInput,
25 type DeleteAddressObjectInput,
26 type GetAddressObjectInput,
27 type PutAddressObjectInput,
28 type SyncCollectionInput,
29} from "@/worker/objects/card-dav/types";
30import { addressWritePrecondition } from "@/worker/objects/card-dav/preconditions";
31import {
32 collectionMultiget,
33 deleteCollectionObject,
34 getCollectionObject,
35 putCollectionObject,
36 syncCollectionDomain,
37 type CollectionObjectSpec,
38} from "@/worker/objects/common/collection-object-domain";
39 
40export function getObject(db: CardDavDoDb, input: GetAddressObjectInput) {
41 return getCollectionObject(db, { ...input, collectionName: input.bookName }, addressObjectSpec);
42}
43 
44export async function putObject(db: CardDavDoDb, input: PutAddressObjectInput) {
45 const putInput: AddressPutObjectDomainInput = { ...input, collectionName: input.bookName };
46 return await putCollectionObject(db, putInput, addressObjectSpec);
47}
48 
49export function deleteObject(db: CardDavDoDb, input: DeleteAddressObjectInput) {
50 return deleteCollectionObject(db, { ...input, collectionName: input.bookName }, addressObjectSpec);
51}
52 
53export function addressbookQuery(db: CardDavDoDb, input: AddressbookQueryInput) {
54 const book = ensureAddressbook(db, input.subjectId, input.nowMs, input.bookName);
55 if (!book) return err(404, "not_found", "Address book not found");
56 const { resources, truncated } = objectResources(db, book, input.maxResults, input.filters, input.filterTest);
57 return { ok: true as const, resources, truncated };
58}
59 
60export function addressbookMultiget(db: CardDavDoDb, input: AddressbookMultigetInput) {
61 return collectionMultiget(db, { ...input, collectionName: input.bookName }, addressObjectSpec);
62}
63 
64export function syncCollection(db: CardDavDoDb, input: SyncCollectionInput) {
65 return syncCollectionDomain(db, { ...input, collectionName: input.bookName }, addressObjectSpec);
66}
67 
68type AddressObjectRow = NonNullable<ReturnType<typeof objectByName>>;
69type AddressPutObjectDomainInput = PutAddressObjectInput & { collectionName: string };
70type AddressObjectSpec = CollectionObjectSpec<
71 CardDavDoDb,
72 CardDavDoDb,
73 AddressbookRow,
74 AddressObjectRow,
75 AddressPutObjectDomainInput,
76 ParsedVCard,
77 ReturnType<typeof err>
78>;
79 
80function parseAddressObject(input: AddressPutObjectDomainInput): ParsedVCard {
81 return parseVCardBytes(input.body, input.maxBytes);
82}
83 
84const addressObjectSpec = {
85 ensureCollection: ensureAddressbook,
86 objectByName,
87 objectByUid,
88 objectResourceFromHref,
89 objectResources,
90 changesSince,
91 latestChangeSeq,
92 syncTokenSeq,
93 syncToken,
94 objectHref,
95 parseObject: parseAddressObject,
96 validateParsed: (parsed, input) => {
97 return input.expectedVersion && input.expectedVersion !== parsed.version
98 ? err(415, "bad_request", "vCard VERSION does not match Content-Type version parameter")
99 : null;
100 },
101 createObjectId: addressObjectId,
102 writePrecondition: addressWritePrecondition,
103 transaction: (db, callback) => db.transaction((tx) => callback(tx)),
104 saveObject: (tx, input) => {
105 if (input.existing) {
106 tx.update(addressObjects)
107 .set({
108 uid: input.parsed.uid,
109 body: input.parsed.body,
110 etag: input.etag,
111 size: input.parsed.size,
112 modifiedAtMs: input.nowMs,
113 version: input.version,
114 })
115 .where(eq(addressObjects.id, input.existing.id))
116 .run();
117 tx.delete(addressIndex).where(eq(addressIndex.objectId, input.existing.id)).run();
118 } else {
119 tx.insert(addressObjects)
120 .values({
121 id: input.objectId,
122 addressbookId: input.collection.id,
123 name: input.objectName,
124 uid: input.parsed.uid,
125 body: input.parsed.body,
126 etag: input.etag,
127 size: input.parsed.size,
128 createdAtMs: input.nowMs,
129 modifiedAtMs: input.nowMs,
130 version: input.version,
131 })
132 .run();
133 }
134 tx.insert(addressIndex)
135 .values(addressIndexRow(input.objectId, input.collection.id, input.parsed))
136 .run();
137 },
138 deleteObject: (tx, object) => {
139 tx.delete(addressObjects).where(eq(addressObjects.id, object.id)).run();
140 },
141 recordChange,
142 collectionNotFound: () => err(404, "not_found", "Address book not found"),
143 objectNotFound: () => err(404, "not_found", "Address object not found"),
144 parseFailure: (input, cause) =>
145 err(
146 input.body.byteLength > input.maxBytes ? 413 : 400,
147 input.body.byteLength > input.maxBytes ? "payload_too_large" : "bad_request",
148 cause instanceof Error ? cause.message : "Invalid vCard",
149 ),
150 uidAlreadyExists: () => err(409, "conflict", "vCard UID already exists"),
151 uidCannotChange: () => err(409, "conflict", "vCard UID cannot change at an existing href"),
152 multigetLimitExceeded: () => err(403, "forbidden", "addressbook-multiget exceeds result limit"),
153 invalidSyncToken: () => err(400, "bad_request", "Invalid sync token"),
154} satisfies AddressObjectSpec;