File
Blob: src/worker/objects/common/collection-preconditions.ts
| 1 | import { davConditionalFailure } from "@/worker/dav/conditional"; |
| 2 | import type { ParsedIfHeader } from "@/worker/dav/if-header"; |
| 3 | import { parseCollectionObjectPath } from "@/worker/dav/paths"; |
| 4 | |
| 5 | export interface CollectionConditionalInput { |
| 6 | subjectId: string; |
| 7 | nowMs: number; |
| 8 | ifMatch: string | null; |
| 9 | ifNoneMatch: string | null; |
| 10 | ifHeader: ParsedIfHeader; |
| 11 | } |
| 12 | |
| 13 | export 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 | |
| 23 | export 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 | |
| 43 | function 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 | |
| 53 | function 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 | |
| 66 | function 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 | } |