Skip to content
File

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

typescript126 lines
1import type { DomainCollection, SubjectTimeInput } from "@/worker/objects/common/collection-types";
2 
3export interface CollectionCrudSpec<TDb, TCollection extends DomainCollection, TRecord, TError> {
4 ensureDefaultCollection(db: TDb, subjectId: string, nowMs: number): TCollection;
5 listCollections(db: TDb, subjectId: string, nowMs: number): TCollection[];
6 collectionById(db: TDb, id: string): TCollection | undefined;
7 collectionByName(db: TDb, name: string): TCollection | undefined;
8 validName(name: string): boolean;
9 deleteCollection(db: TDb, id: string): boolean;
10 record(row: TCollection): TRecord;
11 invalidName(): TError;
12 nameConflict(): TError;
13 collectionNotFound(): TError;
14 emptyDisplayName(): TError;
15 defaultDeleteForbidden(): TError;
16}
17 
18export function listCollectionRecords<TDb, TCollection extends DomainCollection, TRecord, TError>(
19 db: TDb,
20 input: SubjectTimeInput,
21 spec: Pick<CollectionCrudSpec<TDb, TCollection, TRecord, TError>, "listCollections" | "record">,
22): { ok: true; records: TRecord[] } {
23 return { ok: true, records: spec.listCollections(db, input.subjectId, input.nowMs).map(spec.record) };
24}
25 
26export function getCollectionRecord<TDb, TCollection extends DomainCollection, TRecord, TError>(
27 db: TDb,
28 input: SubjectTimeInput & { id: string },
29 spec: Pick<
30 CollectionCrudSpec<TDb, TCollection, TRecord, TError>,
31 "ensureDefaultCollection" | "collectionById" | "record" | "collectionNotFound"
32 >,
33): { ok: true; record: TRecord } | TError {
34 spec.ensureDefaultCollection(db, input.subjectId, input.nowMs);
35 const row = spec.collectionById(db, input.id);
36 return row ? { ok: true, record: spec.record(row) } : spec.collectionNotFound();
37}
38 
39export function createCollectionRecord<
40 TDb,
41 TCollection extends DomainCollection,
42 TRecord,
43 TError,
44 TInput extends SubjectTimeInput & { name: string; displayName?: string },
45>(
46 db: TDb,
47 input: TInput,
48 spec: Pick<
49 CollectionCrudSpec<TDb, TCollection, TRecord, TError>,
50 "validName" | "invalidName" | "ensureDefaultCollection" | "collectionByName" | "nameConflict" | "record"
51 > & {
52 createCollection(db: TDb, input: TInput, displayName: string): TCollection;
53 },
54): { ok: true; record: TRecord } | TError {
55 if (!spec.validName(input.name)) return spec.invalidName();
56 spec.ensureDefaultCollection(db, input.subjectId, input.nowMs);
57 if (spec.collectionByName(db, input.name)) return spec.nameConflict();
58 const row = spec.createCollection(db, input, input.displayName ?? input.name);
59 return { ok: true, record: spec.record(row) };
60}
61 
62export function updateCollectionRecord<
63 TDb,
64 TCollection extends DomainCollection,
65 TRecord,
66 TError,
67 TInput extends SubjectTimeInput & { id: string; displayName?: string },
68>(
69 db: TDb,
70 input: TInput,
71 spec: Pick<
72 CollectionCrudSpec<TDb, TCollection, TRecord, TError>,
73 "ensureDefaultCollection" | "record" | "collectionNotFound" | "emptyDisplayName"
74 > & {
75 updateCollection(db: TDb, input: TInput, displayName: string | undefined): TCollection | undefined;
76 },
77): { ok: true; record: TRecord } | TError {
78 spec.ensureDefaultCollection(db, input.subjectId, input.nowMs);
79 const displayName = input.displayName?.trim();
80 if (displayName !== undefined && displayName.length === 0) return spec.emptyDisplayName();
81 const row = spec.updateCollection(db, input, displayName);
82 return row ? { ok: true, record: spec.record(row) } : spec.collectionNotFound();
83}
84 
85export function deleteCollectionRecord<TDb, TCollection extends DomainCollection, TRecord, TError>(
86 db: TDb,
87 input: SubjectTimeInput & { id: string },
88 spec: Pick<
89 CollectionCrudSpec<TDb, TCollection, TRecord, TError>,
90 "ensureDefaultCollection" | "collectionById" | "deleteCollection" | "collectionNotFound" | "defaultDeleteForbidden"
91 >,
92): { ok: true } | TError {
93 spec.ensureDefaultCollection(db, input.subjectId, input.nowMs);
94 return deleteExistingCollectionRecord(db, spec.collectionById(db, input.id), spec);
95}
96 
97export function deleteCollectionRecordByName<TDb, TCollection extends DomainCollection, TRecord, TError>(
98 db: TDb,
99 input: SubjectTimeInput & { name: string },
100 spec: Pick<
101 CollectionCrudSpec<TDb, TCollection, TRecord, TError>,
102 | "ensureDefaultCollection"
103 | "collectionByName"
104 | "deleteCollection"
105 | "collectionNotFound"
106 | "defaultDeleteForbidden"
107 >,
108): { ok: true } | TError {
109 spec.ensureDefaultCollection(db, input.subjectId, input.nowMs);
110 return deleteExistingCollectionRecord(db, spec.collectionByName(db, input.name), spec);
111}
112 
113function deleteExistingCollectionRecord<TDb, TCollection extends DomainCollection, TRecord, TError>(
114 db: TDb,
115 collection: TCollection | undefined,
116 spec: Pick<
117 CollectionCrudSpec<TDb, TCollection, TRecord, TError>,
118 "deleteCollection" | "collectionNotFound" | "defaultDeleteForbidden"
119 >,
120): { ok: true } | TError {
121 if (!collection) return spec.collectionNotFound();
122 if (collection.name === "default") return spec.defaultDeleteForbidden();
123 spec.deleteCollection(db, collection.id);
124 return { ok: true };
125}