import { extractPropTextValue } from "@/worker/db/dav-collections"; import { DAV_NS } from "@/worker/xml/namespaces"; import type { CollectionDomainError, DomainCollection, DomainConditionalInput, DomainProppatchInput, } from "@/worker/objects/common/collection-types"; export interface CollectionProppatchSpec< TDb, TTx, TCollection extends DomainCollection, TObject extends { id: string; etag: string }, TError extends CollectionDomainError, > { collectionKind: string; ensureDefaultCollection(db: TDb, subjectId: string, nowMs: number): TCollection; ensureCollection(db: TDb, subjectId: string, nowMs: number, collectionName: string): TCollection | null; objectByName(db: TDb, collectionId: string, objectName: string): TObject | undefined; targetHref(collection: TCollection, kind: string, objectName?: string): string; writePrecondition( db: TDb, input: DomainConditionalInput, targetHref: string, targetEtag: string | null, ): TError | null; transaction(db: TDb, callback: (tx: TTx) => void): void; updateCollectionDisplayName(tx: TTx, collectionId: string, displayName: string, nowMs: number): void; upsertDeadProp( tx: TTx, key: { resourceKind: string; resourceId: string; nsUri: string; localName: string }, xmlValue: string, ): void; deleteDeadProp(tx: TTx, key: { resourceKind: string; resourceId: string; nsUri: string; localName: string }): void; collectionNotFound(): TError; resourceNotFound(): TError; } export function proppatchCollectionDomain< TDb, TTx, TCollection extends DomainCollection, TObject extends { id: string; etag: string }, TError extends CollectionDomainError, >( db: TDb, input: DomainProppatchInput, spec: CollectionProppatchSpec, ): { ok: true } | TError { const collection = input.kind === "home" ? spec.ensureDefaultCollection(db, input.subjectId, input.nowMs) : spec.ensureCollection(db, input.subjectId, input.nowMs, input.collectionName ?? ""); if (!collection) return spec.collectionNotFound(); const resourceId = input.resourceId ?? resolveDomainResourceId(db, input, collection, spec); if (!resourceId) return spec.resourceNotFound(); const object = input.kind === "object" ? spec.objectByName(db, collection.id, input.objectName ?? "") : undefined; const precondition = spec.writePrecondition( db, input, spec.targetHref(collection, input.kind, input.objectName), object?.etag ?? null, ); if (precondition) return precondition; spec.transaction(db, (tx) => { for (const instruction of input.instructions) { if (instruction.kind === "set") { if (isMappedDisplayName(input.kind, spec.collectionKind, instruction)) { spec.updateCollectionDisplayName(tx, resourceId, extractPropTextValue(instruction.xmlValue), input.nowMs); continue; } spec.upsertDeadProp( tx, { resourceKind: input.kind, resourceId, nsUri: instruction.nsUri, localName: instruction.localName, }, instruction.xmlValue, ); continue; } if (isMappedDisplayName(input.kind, spec.collectionKind, instruction)) { spec.updateCollectionDisplayName(tx, resourceId, collection.name, input.nowMs); continue; } spec.deleteDeadProp(tx, { resourceKind: input.kind, resourceId, nsUri: instruction.nsUri, localName: instruction.localName, }); } }); return { ok: true }; } function resolveDomainResourceId< TDb, TTx, TCollection extends DomainCollection, TObject extends { id: string; etag: string }, TError extends CollectionDomainError, >( db: TDb, input: DomainProppatchInput, collection: TCollection, spec: CollectionProppatchSpec, ): string | null { if (input.kind === "home") return "home"; if (input.kind === spec.collectionKind) return collection.id; return spec.objectByName(db, collection.id, input.objectName ?? "")?.id ?? null; } function isMappedDisplayName( kind: string, collectionKind: string, prop: { nsUri: string; localName: string }, ): boolean { return kind === collectionKind && prop.nsUri === DAV_NS && prop.localName === "displayname"; }