import { eq } from "drizzle-orm"; import { addressObjectHref as objectHref } from "@/worker/carddav/paths"; import { parseVCardBytes, type ParsedVCard } from "@/worker/carddav/vcard"; import type { CardDavDoDb } from "@/worker/db/card-dav-do/client"; import { addressIndex, addressObjects, type AddressbookRow } from "@/worker/db/card-dav-do/schema"; import { addressIndexRow, addressObjectId, addressSyncToken as syncToken, changesSince, ensureAddressbook, latestChangeSeq, objectByName, objectByUid, objectResourceFromHref, objectResources, recordChange, syncTokenSeq, } from "@/worker/db/card-dav-do/repository"; import { cardDavError as err, type AddressbookMultigetInput, type AddressbookQueryInput, type DeleteAddressObjectInput, type GetAddressObjectInput, type PutAddressObjectInput, type SyncCollectionInput, } from "@/worker/objects/card-dav/types"; import { addressWritePrecondition } from "@/worker/objects/card-dav/preconditions"; import { collectionMultiget, deleteCollectionObject, getCollectionObject, putCollectionObject, syncCollectionDomain, type CollectionObjectSpec, } from "@/worker/objects/common/collection-object-domain"; export function getObject(db: CardDavDoDb, input: GetAddressObjectInput) { return getCollectionObject(db, { ...input, collectionName: input.bookName }, addressObjectSpec); } export async function putObject(db: CardDavDoDb, input: PutAddressObjectInput) { const putInput: AddressPutObjectDomainInput = { ...input, collectionName: input.bookName }; return await putCollectionObject(db, putInput, addressObjectSpec); } export function deleteObject(db: CardDavDoDb, input: DeleteAddressObjectInput) { return deleteCollectionObject(db, { ...input, collectionName: input.bookName }, addressObjectSpec); } export function addressbookQuery(db: CardDavDoDb, input: AddressbookQueryInput) { const book = ensureAddressbook(db, input.subjectId, input.nowMs, input.bookName); if (!book) return err(404, "not_found", "Address book not found"); const { resources, truncated } = objectResources(db, book, input.maxResults, input.filters, input.filterTest); return { ok: true as const, resources, truncated }; } export function addressbookMultiget(db: CardDavDoDb, input: AddressbookMultigetInput) { return collectionMultiget(db, { ...input, collectionName: input.bookName }, addressObjectSpec); } export function syncCollection(db: CardDavDoDb, input: SyncCollectionInput) { return syncCollectionDomain(db, { ...input, collectionName: input.bookName }, addressObjectSpec); } type AddressObjectRow = NonNullable>; type AddressPutObjectDomainInput = PutAddressObjectInput & { collectionName: string }; type AddressObjectSpec = CollectionObjectSpec< CardDavDoDb, CardDavDoDb, AddressbookRow, AddressObjectRow, AddressPutObjectDomainInput, ParsedVCard, ReturnType >; function parseAddressObject(input: AddressPutObjectDomainInput): ParsedVCard { return parseVCardBytes(input.body, input.maxBytes); } const addressObjectSpec = { ensureCollection: ensureAddressbook, objectByName, objectByUid, objectResourceFromHref, objectResources, changesSince, latestChangeSeq, syncTokenSeq, syncToken, objectHref, parseObject: parseAddressObject, validateParsed: (parsed, input) => { return input.expectedVersion && input.expectedVersion !== parsed.version ? err(415, "bad_request", "vCard VERSION does not match Content-Type version parameter") : null; }, createObjectId: addressObjectId, writePrecondition: addressWritePrecondition, transaction: (db, callback) => db.transaction((tx) => callback(tx)), saveObject: (tx, input) => { if (input.existing) { tx.update(addressObjects) .set({ uid: input.parsed.uid, body: input.parsed.body, etag: input.etag, size: input.parsed.size, modifiedAtMs: input.nowMs, version: input.version, }) .where(eq(addressObjects.id, input.existing.id)) .run(); tx.delete(addressIndex).where(eq(addressIndex.objectId, input.existing.id)).run(); } else { tx.insert(addressObjects) .values({ id: input.objectId, addressbookId: input.collection.id, name: input.objectName, uid: input.parsed.uid, body: input.parsed.body, etag: input.etag, size: input.parsed.size, createdAtMs: input.nowMs, modifiedAtMs: input.nowMs, version: input.version, }) .run(); } tx.insert(addressIndex) .values(addressIndexRow(input.objectId, input.collection.id, input.parsed)) .run(); }, deleteObject: (tx, object) => { tx.delete(addressObjects).where(eq(addressObjects.id, object.id)).run(); }, recordChange, collectionNotFound: () => err(404, "not_found", "Address book not found"), objectNotFound: () => err(404, "not_found", "Address object not found"), parseFailure: (input, cause) => err( input.body.byteLength > input.maxBytes ? 413 : 400, input.body.byteLength > input.maxBytes ? "payload_too_large" : "bad_request", cause instanceof Error ? cause.message : "Invalid vCard", ), uidAlreadyExists: () => err(409, "conflict", "vCard UID already exists"), uidCannotChange: () => err(409, "conflict", "vCard UID cannot change at an existing href"), multigetLimitExceeded: () => err(403, "forbidden", "addressbook-multiget exceeds result limit"), invalidSyncToken: () => err(400, "bad_request", "Invalid sync token"), } satisfies AddressObjectSpec;