File
Blob: src/worker/objects/card-dav/address-objects.ts
| 1 | import { eq } from "drizzle-orm"; |
| 2 | |
| 3 | import { addressObjectHref as objectHref } from "@/worker/carddav/paths"; |
| 4 | import { parseVCardBytes, type ParsedVCard } from "@/worker/carddav/vcard"; |
| 5 | import type { CardDavDoDb } from "@/worker/db/card-dav-do/client"; |
| 6 | import { addressIndex, addressObjects, type AddressbookRow } from "@/worker/db/card-dav-do/schema"; |
| 7 | import { |
| 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"; |
| 21 | import { |
| 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"; |
| 30 | import { addressWritePrecondition } from "@/worker/objects/card-dav/preconditions"; |
| 31 | import { |
| 32 | collectionMultiget, |
| 33 | deleteCollectionObject, |
| 34 | getCollectionObject, |
| 35 | putCollectionObject, |
| 36 | syncCollectionDomain, |
| 37 | type CollectionObjectSpec, |
| 38 | } from "@/worker/objects/common/collection-object-domain"; |
| 39 | |
| 40 | export function getObject(db: CardDavDoDb, input: GetAddressObjectInput) { |
| 41 | return getCollectionObject(db, { ...input, collectionName: input.bookName }, addressObjectSpec); |
| 42 | } |
| 43 | |
| 44 | export async function putObject(db: CardDavDoDb, input: PutAddressObjectInput) { |
| 45 | const putInput: AddressPutObjectDomainInput = { ...input, collectionName: input.bookName }; |
| 46 | return await putCollectionObject(db, putInput, addressObjectSpec); |
| 47 | } |
| 48 | |
| 49 | export function deleteObject(db: CardDavDoDb, input: DeleteAddressObjectInput) { |
| 50 | return deleteCollectionObject(db, { ...input, collectionName: input.bookName }, addressObjectSpec); |
| 51 | } |
| 52 | |
| 53 | export 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 | |
| 60 | export function addressbookMultiget(db: CardDavDoDb, input: AddressbookMultigetInput) { |
| 61 | return collectionMultiget(db, { ...input, collectionName: input.bookName }, addressObjectSpec); |
| 62 | } |
| 63 | |
| 64 | export function syncCollection(db: CardDavDoDb, input: SyncCollectionInput) { |
| 65 | return syncCollectionDomain(db, { ...input, collectionName: input.bookName }, addressObjectSpec); |
| 66 | } |
| 67 | |
| 68 | type AddressObjectRow = NonNullable<ReturnType<typeof objectByName>>; |
| 69 | type AddressPutObjectDomainInput = PutAddressObjectInput & { collectionName: string }; |
| 70 | type AddressObjectSpec = CollectionObjectSpec< |
| 71 | CardDavDoDb, |
| 72 | CardDavDoDb, |
| 73 | AddressbookRow, |
| 74 | AddressObjectRow, |
| 75 | AddressPutObjectDomainInput, |
| 76 | ParsedVCard, |
| 77 | ReturnType<typeof err> |
| 78 | >; |
| 79 | |
| 80 | function parseAddressObject(input: AddressPutObjectDomainInput): ParsedVCard { |
| 81 | return parseVCardBytes(input.body, input.maxBytes); |
| 82 | } |
| 83 | |
| 84 | const 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; |