@furystack/core
Version:
Core FuryStack package
105 lines (91 loc) • 3.71 kB
text/typescript
import { EventHub } from '@furystack/utils'
import { NotFoundError } from './errors/not-found-error.js'
import { filterItems } from './filter-items.js'
import type { Constructable } from './models/constructable.js'
import type { CreateResult, FilterType, FindOptions, PartialResult, PhysicalStore } from './models/physical-store.js'
import { selectFields } from './models/physical-store.js'
/**
* In-memory {@link PhysicalStore} backed by a `Map`. Intended for tests, dev
* fixtures and small reference datasets — entries are lost on process exit.
* Production deployments should bind a real adapter (filesystem, MongoDB,
* Sequelize, Redis) via `defineXxxStore`.
*/
export class InMemoryStore<T, TPrimaryKey extends keyof T>
extends EventHub<{
onEntityAdded: { entity: T }
onEntityUpdated: { id: T[TPrimaryKey]; change: Partial<T> }
onEntityRemoved: { key: T[TPrimaryKey] }
}>
implements PhysicalStore<T, TPrimaryKey, T>
{
public async remove(...keys: Array<T[TPrimaryKey]>): Promise<void> {
keys.forEach((key) => {
this.cache.delete(key)
this.emit('onEntityRemoved', { key })
})
}
public async add(...entries: T[]): Promise<CreateResult<T>> {
const created = entries.map((e) => {
const entry = { ...e }
if (this.cache.has(entry[this.primaryKey])) {
throw new Error('Item with the primary key already exists.')
}
this.cache.set(entry[this.primaryKey], entry)
this.emit('onEntityAdded', { entity: entry })
return entry
})
return { created }
}
public cache: Map<T[TPrimaryKey], T> = new Map()
public get = (key: T[TPrimaryKey], select?: Array<keyof T>) => {
const item = this.cache.get(key)
return Promise.resolve(item && select ? selectFields(item, ...select) : item)
}
public async find<TFields extends Array<keyof T>>(searchOptions: FindOptions<T, TFields>) {
let value: Array<PartialResult<T, TFields>> = filterItems([...this.cache.values()], searchOptions.filter)
if (searchOptions.order) {
const orderRecord = searchOptions.order as Record<string, 'ASC' | 'DESC'>
for (const fieldName of Object.keys(searchOptions.order) as Array<keyof T>) {
value = value.sort((a, b) => {
const order = orderRecord[fieldName as string]
if (a[fieldName] < b[fieldName]) return order === 'ASC' ? -1 : 1
if (a[fieldName] > b[fieldName]) return order === 'ASC' ? 1 : -1
return 0
})
}
}
if (searchOptions.top || searchOptions.skip) {
value = value.slice(searchOptions.skip, (searchOptions.skip || 0) + (searchOptions.top || this.cache.size))
}
if (searchOptions.select) {
value = value.map((item) => {
return selectFields(item, ...(searchOptions.select as TFields))
})
}
return value
}
public async count(filter?: FilterType<T>) {
return filterItems([...this.cache.values()], filter).length
}
public async update(id: T[TPrimaryKey], data: T) {
if (!this.cache.has(id)) {
throw new NotFoundError(`Entity not found with id '${id as string}', cannot update`)
}
this.cache.set(id, {
...this.cache.get(id),
...data,
})
this.emit('onEntityUpdated', { id, change: data })
}
public [Symbol.dispose]() {
this.cache.clear()
super[Symbol.dispose]()
}
public readonly primaryKey: TPrimaryKey
public readonly model: Constructable<T>
constructor(options: { primaryKey: TPrimaryKey; model: Constructable<T> }) {
super()
this.primaryKey = options.primaryKey
this.model = options.model
}
}