repository-utils.ts
1 import { perf } from '@/lib/server/runtime/perf' 2 3 export interface RecordRepository< 4 T, 5 ListOptions = void, 6 UpsertValue = T | Record<string, unknown>, 7 > { 8 get(id: string, options?: ListOptions): T | null 9 getMany(ids: string[], options?: ListOptions): Record<string, T> 10 list(options?: ListOptions): Record<string, T> 11 upsert(id: string, value: UpsertValue): void 12 upsertMany(entries: Array<[string, UpsertValue]>): void 13 patch(id: string, updater: (current: T | null) => T | null, options?: ListOptions): T | null 14 replace(data: Record<string, UpsertValue>): void 15 delete(id: string): void 16 } 17 18 interface RecordRepositoryOps< 19 T, 20 ListOptions = void, 21 UpsertValue = T | Record<string, unknown>, 22 > { 23 get(id: string, options?: ListOptions): T | null 24 list(options?: ListOptions): Record<string, T> 25 upsert(id: string, value: UpsertValue): void 26 upsertMany?: (entries: Array<[string, UpsertValue]>) => void 27 patch?: (id: string, updater: (current: T | null) => T | null) => T | null 28 replace?: (data: Record<string, UpsertValue>) => void 29 delete?: (id: string) => void 30 } 31 32 export interface SingletonRepository< 33 T, 34 SaveValue = T | Record<string, unknown>, 35 > { 36 get(): T 37 save(value: SaveValue): void 38 patch(updater: (current: T) => SaveValue): T 39 } 40 41 interface SingletonRepositoryOps< 42 T, 43 SaveValue = T | Record<string, unknown>, 44 > { 45 get(): T 46 save(value: SaveValue): void 47 } 48 49 function uniqueIds(ids: string[]): string[] { 50 const out: string[] = [] 51 const seen = new Set<string>() 52 for (const id of ids) { 53 const normalized = typeof id === 'string' ? id.trim() : '' 54 if (!normalized || seen.has(normalized)) continue 55 seen.add(normalized) 56 out.push(normalized) 57 } 58 return out 59 } 60 61 export function createRecordRepository< 62 T, 63 ListOptions = void, 64 UpsertValue = T | Record<string, unknown>, 65 >( 66 name: string, 67 ops: RecordRepositoryOps<T, ListOptions, UpsertValue>, 68 ): RecordRepository<T, ListOptions, UpsertValue> { 69 return { 70 get(id, options) { 71 return perf.measureSync('repository', `${name}.get`, () => ops.get(id, options), { id }) 72 }, 73 getMany(ids, options) { 74 return perf.measureSync('repository', `${name}.getMany`, () => { 75 const result: Record<string, T> = {} 76 for (const id of uniqueIds(ids)) { 77 const item = ops.get(id, options) 78 if (item) result[id] = item 79 } 80 return result 81 }, { count: ids.length }) 82 }, 83 list(options) { 84 return perf.measureSync('repository', `${name}.list`, () => ops.list(options)) 85 }, 86 upsert(id, value) { 87 perf.measureSync('repository', `${name}.upsert`, () => ops.upsert(id, value), { id }) 88 }, 89 upsertMany(entries) { 90 perf.measureSync('repository', `${name}.upsertMany`, () => { 91 if (ops.upsertMany) { 92 ops.upsertMany(entries) 93 return 94 } 95 for (const [id, value] of entries) ops.upsert(id, value) 96 }, { count: entries.length }) 97 }, 98 patch(id, updater, options) { 99 return perf.measureSync('repository', `${name}.patch`, () => { 100 if (ops.patch) return ops.patch(id, updater) 101 const current = ops.get(id, options) 102 const next = updater(current) 103 if (next === null) { 104 if (!ops.delete) return null 105 ops.delete(id) 106 return null 107 } 108 ops.upsert(id, next as UpsertValue) 109 return next 110 }, { id }) 111 }, 112 replace(data) { 113 perf.measureSync('repository', `${name}.replace`, () => { 114 if (ops.replace) { 115 ops.replace(data) 116 return 117 } 118 const entries = Object.entries(data) 119 if (ops.upsertMany) ops.upsertMany(entries) 120 else for (const [id, value] of entries) ops.upsert(id, value) 121 }, { count: Object.keys(data).length }) 122 }, 123 delete(id) { 124 perf.measureSync('repository', `${name}.delete`, () => { 125 if (!ops.delete) return 126 ops.delete(id) 127 }, { id }) 128 }, 129 } 130 } 131 132 export function createSingletonRepository< 133 T, 134 SaveValue = T | Record<string, unknown>, 135 >( 136 name: string, 137 ops: SingletonRepositoryOps<T, SaveValue>, 138 ): SingletonRepository<T, SaveValue> { 139 return { 140 get() { 141 return perf.measureSync('repository', `${name}.get`, () => ops.get()) 142 }, 143 save(value) { 144 perf.measureSync('repository', `${name}.save`, () => ops.save(value)) 145 }, 146 patch(updater) { 147 return perf.measureSync('repository', `${name}.patch`, () => { 148 const next = updater(ops.get()) 149 ops.save(next) 150 return ops.get() 151 }) 152 }, 153 } 154 }