packages/memory/src/index.ts
import { BadRequest, MethodNotAllowed, NotFound } from '@feathersjs/errors'
import { _ } from '@feathersjs/commons'
import {
sorter,
select,
AdapterBase,
AdapterServiceOptions,
PaginationOptions,
AdapterParams
} from '@feathersjs/adapter-commons'
import sift from 'sift'
import { NullableId, Id, Params, Paginated } from '@feathersjs/feathers'
export interface MemoryServiceStore<T> {
[key: string]: T
}
export interface MemoryServiceOptions<T = any> extends AdapterServiceOptions {
store?: MemoryServiceStore<T>
startId?: number
matcher?: (query: any) => any
sorter?: (sort: any) => any
}
const _select = (data: any, params: any, ...args: string[]) => {
const base = select(params, ...args)
return base(JSON.parse(JSON.stringify(data)))
}
export class MemoryAdapter<
Result = any,
Data = Partial<Result>,
ServiceParams extends Params = Params,
PatchData = Partial<Data>
> extends AdapterBase<Result, Data, PatchData, ServiceParams, MemoryServiceOptions<Result>> {
store: MemoryServiceStore<Result>
_uId: number
constructor(options: MemoryServiceOptions<Result> = {}) {
super({
id: 'id',
matcher: sift,
sorter,
store: {},
startId: 0,
...options
})
this._uId = this.options.startId
this.store = { ...this.options.store }
}
async getEntries(_params?: ServiceParams) {
const params = _params || ({} as ServiceParams)
return this._find({
...params,
paginate: false
})
}
getQuery(params: ServiceParams) {
const { $skip, $sort, $limit, $select, ...query } = params.query || {}
return {
query,
filters: { $skip, $sort, $limit, $select }
}
}
async _find(_params?: ServiceParams & { paginate?: PaginationOptions }): Promise<Paginated<Result>>
async _find(_params?: ServiceParams & { paginate: false }): Promise<Result[]>
async _find(_params?: ServiceParams): Promise<Paginated<Result> | Result[]>
async _find(params: ServiceParams = {} as ServiceParams): Promise<Paginated<Result> | Result[]> {
const { paginate } = this.getOptions(params)
const { query, filters } = this.getQuery(params)
let values = _.values(this.store)
const hasSkip = filters.$skip !== undefined
const hasSort = filters.$sort !== undefined
const hasLimit = filters.$limit !== undefined
const hasQuery = _.keys(query).length > 0
if (hasSort) {
values.sort(this.options.sorter(filters.$sort))
}
if (paginate) {
if (hasQuery) {
values = values.filter(this.options.matcher(query))
}
const total = values.length
if (hasSkip) {
values = values.slice(filters.$skip)
}
if (hasLimit) {
values = values.slice(0, filters.$limit)
}
const result: Paginated<Result> = {
total,
limit: filters.$limit,
skip: filters.$skip || 0,
data: values.map((value) => _select(value, params, this.id))
}
return result
}
/* Without pagination, we don't have to match every result and gain considerable performance improvements with a breaking for loop. */
if (hasQuery || hasLimit || hasSkip) {
let skipped = 0
const matcher = this.options.matcher(query)
const matched = []
if (hasLimit && filters.$limit === 0) {
return []
}
for (let index = 0, length = values.length; index < length; index++) {
const value = values[index]
if (hasQuery && !matcher(value, index, values)) {
continue
}
if (hasSkip && filters.$skip > skipped) {
skipped++
continue
}
matched.push(_select(value, params, this.id))
if (hasLimit && filters.$limit === matched.length) {
break
}
}
return matched
}
return values.map((value) => _select(value, params, this.id))
}
async _get(id: Id, params: ServiceParams = {} as ServiceParams): Promise<Result> {
const { query } = this.getQuery(params)
if (id in this.store) {
const value = this.store[id]
if (this.options.matcher(query)(value)) {
return _select(value, params, this.id)
}
}
throw new NotFound(`No record found for id '${id}'`)
}
async _create(data: Partial<Data>, params?: ServiceParams): Promise<Result>
async _create(data: Partial<Data>[], params?: ServiceParams): Promise<Result[]>
async _create(data: Partial<Data> | Partial<Data>[], _params?: ServiceParams): Promise<Result | Result[]>
async _create(
data: Partial<Data> | Partial<Data>[],
params: ServiceParams = {} as ServiceParams
): Promise<Result | Result[]> {
if (Array.isArray(data)) {
return Promise.all(data.map((current) => this._create(current, params)))
}
const id = (data as any)[this.id] || this._uId++
const current = _.extend({}, data, { [this.id]: id })
const result = (this.store[id] = current)
return _select(result, params, this.id) as Result
}
async _update(id: Id, data: Data, params: ServiceParams = {} as ServiceParams): Promise<Result> {
if (id === null || Array.isArray(data)) {
throw new BadRequest("You can not replace multiple instances. Did you mean 'patch'?")
}
const oldEntry = await this._get(id)
// We don't want our id to change type if it can be coerced
const oldId = (oldEntry as any)[this.id]
// eslint-disable-next-line eqeqeq
id = oldId == id ? oldId : id
this.store[id] = _.extend({}, data, { [this.id]: id })
return this._get(id, params)
}
async _patch(id: null, data: PatchData | Partial<Result>, params?: ServiceParams): Promise<Result[]>
async _patch(id: Id, data: PatchData | Partial<Result>, params?: ServiceParams): Promise<Result>
async _patch(
id: NullableId,
data: PatchData | Partial<Result>,
_params?: ServiceParams
): Promise<Result | Result[]>
async _patch(
id: NullableId,
data: PatchData | Partial<Result>,
params: ServiceParams = {} as ServiceParams
): Promise<Result | Result[]> {
if (id === null && !this.allowsMulti('patch', params)) {
throw new MethodNotAllowed('Can not patch multiple entries')
}
const { query } = this.getQuery(params)
const patchEntry = (entry: Result) => {
const currentId = (entry as any)[this.id]
this.store[currentId] = _.extend(this.store[currentId], _.omit(data, this.id))
return _select(this.store[currentId], params, this.id)
}
if (id === null) {
const entries = await this.getEntries({
...params,
query
})
return entries.map(patchEntry)
}
return patchEntry(await this._get(id, params)) // Will throw an error if not found
}
async _remove(id: null, params?: ServiceParams): Promise<Result[]>
async _remove(id: Id, params?: ServiceParams): Promise<Result>
async _remove(id: NullableId, _params?: ServiceParams): Promise<Result | Result[]>
async _remove(id: NullableId, params: ServiceParams = {} as ServiceParams): Promise<Result | Result[]> {
if (id === null && !this.allowsMulti('remove', params)) {
throw new MethodNotAllowed('Can not remove multiple entries')
}
const { query } = this.getQuery(params)
if (id === null) {
const entries = await this.getEntries({
...params,
query
})
return Promise.all(entries.map((current: any) => this._remove(current[this.id] as Id, params)))
}
const entry = await this._get(id, params)
delete this.store[id]
return entry
}
}
export class MemoryService<
Result = any,
Data = Partial<Result>,
ServiceParams extends AdapterParams = AdapterParams,
PatchData = Partial<Data>
> extends MemoryAdapter<Result, Data, ServiceParams, PatchData> {
async find(params?: ServiceParams & { paginate?: PaginationOptions }): Promise<Paginated<Result>>
async find(params?: ServiceParams & { paginate: false }): Promise<Result[]>
async find(params?: ServiceParams): Promise<Paginated<Result> | Result[]>
async find(params?: ServiceParams): Promise<Paginated<Result> | Result[]> {
return this._find({
...params,
query: await this.sanitizeQuery(params)
})
}
async get(id: Id, params?: ServiceParams): Promise<Result> {
return this._get(id, {
...params,
query: await this.sanitizeQuery(params)
})
}
async create(data: Data, params?: ServiceParams): Promise<Result>
async create(data: Data[], params?: ServiceParams): Promise<Result[]>
async create(data: Data | Data[], params?: ServiceParams): Promise<Result | Result[]> {
if (Array.isArray(data) && !this.allowsMulti('create', params)) {
throw new MethodNotAllowed('Can not create multiple entries')
}
return this._create(data, params)
}
async update(id: Id, data: Data, params?: ServiceParams): Promise<Result> {
return this._update(id, data, {
...params,
query: await this.sanitizeQuery(params)
})
}
async patch(id: Id, data: PatchData, params?: ServiceParams): Promise<Result>
async patch(id: null, data: PatchData, params?: ServiceParams): Promise<Result[]>
async patch(id: NullableId, data: PatchData, params?: ServiceParams): Promise<Result | Result[]> {
const { $limit, ...query } = await this.sanitizeQuery(params)
return this._patch(id, data, {
...params,
query
})
}
async remove(id: Id, params?: ServiceParams): Promise<Result>
async remove(id: null, params?: ServiceParams): Promise<Result[]>
async remove(id: NullableId, params?: ServiceParams): Promise<Result | Result[]> {
const { $limit, ...query } = await this.sanitizeQuery(params)
return this._remove(id, {
...params,
query
})
}
}
export function memory<T = any, D = Partial<T>, P extends Params = Params>(
options: Partial<MemoryServiceOptions<T>> = {}
) {
return new MemoryService<T, D, P>(options)
}