UNPKG

@furystack/core

Version:
105 lines (91 loc) 3.71 kB
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 } }