import { and, eq, sql } from "drizzle-orm"; import { addressbookHref, addressObjectHref } from "@/worker/carddav/paths"; import type { CardDavDoDb } from "@/worker/db/card-dav-do/client"; import { addressbooks, addressDeadProps, type AddressbookRow } from "@/worker/db/card-dav-do/schema"; import { bookResource, addressSyncToken as syncToken, ensureAddressbook, ensureDefaultAddressbook, homeResource, listAddressbooks, objectByName, objectResource, objectResources, } from "@/worker/db/card-dav-do/repository"; import { cardDavError as err, type CardDavError, type CardDavResource, type DescribeInput, type EnsureInitializedInput, type ProppatchInput, } from "@/worker/objects/card-dav/types"; import { addressWritePrecondition } from "@/worker/objects/card-dav/preconditions"; import { describeCollectionDomain, type CollectionDescribeSpec } from "@/worker/objects/common/collection-describe"; import { proppatchCollectionDomain, type CollectionProppatchSpec } from "@/worker/objects/common/collection-proppatch"; export function ensureInitialized(db: CardDavDoDb, input: EnsureInitializedInput): { ok: true } { ensureDefaultAddressbook(db, input.subjectId, input.nowMs); return { ok: true }; } export function describe( db: CardDavDoDb, input: DescribeInput, ): { ok: true; resources: CardDavResource[]; syncToken: string } | CardDavError { return describeCollectionDomain(db, { ...input, collectionName: input.bookName }, addressDescribeSpec); } export function proppatch(db: CardDavDoDb, input: ProppatchInput): { ok: true } | CardDavError { return proppatchCollectionDomain(db, { ...input, collectionName: input.bookName }, addressProppatchSpec); } function targetHref(book: AddressbookRow, kind: "home" | "addressbook" | "object", objectName?: string): string { if (kind === "home") return "/addressbooks/"; if (kind === "addressbook") return addressbookHref(book.name); return addressObjectHref(book.name, objectName ?? ""); } const addressDescribeSpec = { collectionKind: "addressbook", ensureCollection: ensureAddressbook, homeResource, listCollections: listAddressbooks, collectionResource: bookResource, objectByName, objectResource, objectResources, syncToken, collectionNotFound: () => err(404, "not_found", "Address book not found"), objectNotFound: () => err(404, "not_found", "Address object not found"), } satisfies CollectionDescribeSpec< CardDavDoDb, AddressbookRow, NonNullable>, CardDavResource, CardDavError >; const addressProppatchSpec = { collectionKind: "addressbook", ensureDefaultCollection: ensureDefaultAddressbook, ensureCollection: ensureAddressbook, objectByName, targetHref, writePrecondition: addressWritePrecondition, transaction: (db, callback) => db.transaction((tx) => callback(tx)), updateCollectionDisplayName: (tx, collectionId, displayName, nowMs) => { // RFC 6352 5.2: DAV:displayname on an address book collection is a live // property mapped to the address book's displayName column. tx.update(addressbooks) .set({ displayName, modifiedAtMs: nowMs, syncSeq: sql`${addressbooks.syncSeq} + 1` }) .where(eq(addressbooks.id, collectionId)) .run(); }, upsertDeadProp: (tx, key, xmlValue) => { tx.insert(addressDeadProps) .values({ ...key, resourceKind: key.resourceKind as "home" | "addressbook" | "object", xmlValue }) .onConflictDoUpdate({ target: [ addressDeadProps.resourceKind, addressDeadProps.resourceId, addressDeadProps.nsUri, addressDeadProps.localName, ], set: { xmlValue }, }) .run(); }, deleteDeadProp: (tx, key) => { tx.delete(addressDeadProps) .where( and( eq(addressDeadProps.resourceKind, key.resourceKind as "home" | "addressbook" | "object"), eq(addressDeadProps.resourceId, key.resourceId), eq(addressDeadProps.nsUri, key.nsUri), eq(addressDeadProps.localName, key.localName), ), ) .run(); }, collectionNotFound: () => err(404, "not_found", "Address book not found"), resourceNotFound: () => err(404, "not_found", "Resource not found"), } satisfies CollectionProppatchSpec< CardDavDoDb, CardDavDoDb, AddressbookRow, NonNullable>, CardDavError >;