Skip to content
File

Blob: src/worker/objects/common/collection-proppatch.ts

typescript128 lines
1import { extractPropTextValue } from "@/worker/db/dav-collections";
2import { DAV_NS } from "@/worker/xml/namespaces";
3import type {
4 CollectionDomainError,
5 DomainCollection,
6 DomainConditionalInput,
7 DomainProppatchInput,
8} from "@/worker/objects/common/collection-types";
9 
10export interface CollectionProppatchSpec<
11 TDb,
12 TTx,
13 TCollection extends DomainCollection,
14 TObject extends { id: string; etag: string },
15 TError extends CollectionDomainError,
16> {
17 collectionKind: string;
18 ensureDefaultCollection(db: TDb, subjectId: string, nowMs: number): TCollection;
19 ensureCollection(db: TDb, subjectId: string, nowMs: number, collectionName: string): TCollection | null;
20 objectByName(db: TDb, collectionId: string, objectName: string): TObject | undefined;
21 targetHref(collection: TCollection, kind: string, objectName?: string): string;
22 writePrecondition(
23 db: TDb,
24 input: DomainConditionalInput,
25 targetHref: string,
26 targetEtag: string | null,
27 ): TError | null;
28 transaction(db: TDb, callback: (tx: TTx) => void): void;
29 updateCollectionDisplayName(tx: TTx, collectionId: string, displayName: string, nowMs: number): void;
30 upsertDeadProp(
31 tx: TTx,
32 key: { resourceKind: string; resourceId: string; nsUri: string; localName: string },
33 xmlValue: string,
34 ): void;
35 deleteDeadProp(tx: TTx, key: { resourceKind: string; resourceId: string; nsUri: string; localName: string }): void;
36 collectionNotFound(): TError;
37 resourceNotFound(): TError;
38}
39 
40export function proppatchCollectionDomain<
41 TDb,
42 TTx,
43 TCollection extends DomainCollection,
44 TObject extends { id: string; etag: string },
45 TError extends CollectionDomainError,
46>(
47 db: TDb,
48 input: DomainProppatchInput,
49 spec: CollectionProppatchSpec<TDb, TTx, TCollection, TObject, TError>,
50): { ok: true } | TError {
51 const collection =
52 input.kind === "home"
53 ? spec.ensureDefaultCollection(db, input.subjectId, input.nowMs)
54 : spec.ensureCollection(db, input.subjectId, input.nowMs, input.collectionName ?? "");
55 if (!collection) return spec.collectionNotFound();
56 
57 const resourceId = input.resourceId ?? resolveDomainResourceId(db, input, collection, spec);
58 if (!resourceId) return spec.resourceNotFound();
59 
60 const object = input.kind === "object" ? spec.objectByName(db, collection.id, input.objectName ?? "") : undefined;
61 const precondition = spec.writePrecondition(
62 db,
63 input,
64 spec.targetHref(collection, input.kind, input.objectName),
65 object?.etag ?? null,
66 );
67 if (precondition) return precondition;
68 
69 spec.transaction(db, (tx) => {
70 for (const instruction of input.instructions) {
71 if (instruction.kind === "set") {
72 if (isMappedDisplayName(input.kind, spec.collectionKind, instruction)) {
73 spec.updateCollectionDisplayName(tx, resourceId, extractPropTextValue(instruction.xmlValue), input.nowMs);
74 continue;
75 }
76 spec.upsertDeadProp(
77 tx,
78 {
79 resourceKind: input.kind,
80 resourceId,
81 nsUri: instruction.nsUri,
82 localName: instruction.localName,
83 },
84 instruction.xmlValue,
85 );
86 continue;
87 }
88 if (isMappedDisplayName(input.kind, spec.collectionKind, instruction)) {
89 spec.updateCollectionDisplayName(tx, resourceId, collection.name, input.nowMs);
90 continue;
91 }
92 spec.deleteDeadProp(tx, {
93 resourceKind: input.kind,
94 resourceId,
95 nsUri: instruction.nsUri,
96 localName: instruction.localName,
97 });
98 }
99 });
100 
101 return { ok: true };
102}
103 
104function resolveDomainResourceId<
105 TDb,
106 TTx,
107 TCollection extends DomainCollection,
108 TObject extends { id: string; etag: string },
109 TError extends CollectionDomainError,
110>(
111 db: TDb,
112 input: DomainProppatchInput,
113 collection: TCollection,
114 spec: CollectionProppatchSpec<TDb, TTx, TCollection, TObject, TError>,
115): string | null {
116 if (input.kind === "home") return "home";
117 if (input.kind === spec.collectionKind) return collection.id;
118 return spec.objectByName(db, collection.id, input.objectName ?? "")?.id ?? null;
119}
120 
121function isMappedDisplayName(
122 kind: string,
123 collectionKind: string,
124 prop: { nsUri: string; localName: string },
125): boolean {
126 return kind === collectionKind && prop.nsUri === DAV_NS && prop.localName === "displayname";
127}