/ src / lib / server / persistence / repository-utils.ts
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  }