Skip to content
File

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

typescript77 lines
1import { davConditionalFailure } from "@/worker/dav/conditional";
2import type { ParsedIfHeader } from "@/worker/dav/if-header";
3import { parseCollectionObjectPath } from "@/worker/dav/paths";
4 
5export interface CollectionConditionalInput {
6 subjectId: string;
7 nowMs: number;
8 ifMatch: string | null;
9 ifNoneMatch: string | null;
10 ifHeader: ParsedIfHeader;
11}
12 
13export interface CollectionPreconditionSpec<TDb, TCollection extends { id: string }, TError> {
14 basePath: string;
15 collectionHref: (collectionName: string) => string;
16 ensureCollection: (db: TDb, subjectId: string, nowMs: number, collectionName: string) => TCollection | null;
17 objectEtag: (db: TDb, collectionId: string, objectName: string) => string | null;
18 latestChangeSeq: (db: TDb) => number;
19 syncToken: (seq: number) => string;
20 preconditionFailed: (message: string) => TError;
21}
22 
23export function collectionWritePrecondition<TDb, TCollection extends { id: string }, TError>(
24 db: TDb,
25 input: CollectionConditionalInput,
26 targetHref: string,
27 targetEtag: string | null,
28 spec: CollectionPreconditionSpec<TDb, TCollection, TError>,
29): TError | null {
30 const stateHref = collectionStateHrefForTarget(targetHref, spec.basePath, spec.collectionHref);
31 const failure = davConditionalFailure({
32 ifMatch: input.ifMatch,
33 ifNoneMatch: input.ifNoneMatch,
34 ifHeader: input.ifHeader,
35 targetHref,
36 targetEtag,
37 getEtag: (href) => collectionEtagForHref(db, input, href, spec),
38 getStateToken: (href) => (href === stateHref ? collectionStateTokenForHref(db, input, href, spec) : null),
39 });
40 return failure ? spec.preconditionFailed(failure) : null;
41}
42 
43function collectionStateHrefForTarget(
44 targetHref: string,
45 basePath: string,
46 collectionHref: (collectionName: string) => string,
47): string | null {
48 const path = parseCollectionObjectPath(targetHref, basePath);
49 if (!path || path.kind === "home") return null;
50 return collectionHref(path.collectionName);
51}
52 
53function collectionEtagForHref<TDb, TCollection extends { id: string }, TError>(
54 db: TDb,
55 input: CollectionConditionalInput,
56 href: string,
57 spec: CollectionPreconditionSpec<TDb, TCollection, TError>,
58): string | null {
59 const path = parseCollectionObjectPath(href, spec.basePath);
60 if (!path || path.kind !== "object") return null;
61 const collection = spec.ensureCollection(db, input.subjectId, input.nowMs, path.collectionName);
62 if (!collection) return null;
63 return spec.objectEtag(db, collection.id, path.objectName);
64}
65 
66function collectionStateTokenForHref<TDb, TCollection extends { id: string }, TError>(
67 db: TDb,
68 input: CollectionConditionalInput,
69 href: string,
70 spec: CollectionPreconditionSpec<TDb, TCollection, TError>,
71): string | null {
72 const path = parseCollectionObjectPath(href, spec.basePath);
73 if (!path || path.kind !== "collection") return null;
74 const collection = spec.ensureCollection(db, input.subjectId, input.nowMs, path.collectionName);
75 return collection ? spec.syncToken(spec.latestChangeSeq(db)) : null;
76}