UNPKG

memory-orm

Version:
355 lines (320 loc) 8.51 kB
import _cloneDeep from 'lodash/cloneDeep' import _merge from 'lodash/merge' import _union from 'lodash/union' import _uniq from 'lodash/uniq' import _get from 'lodash/get' import _set from 'lodash/set' import { State, PureObject, step, Metadata } from './mem' import { Datum } from './datum' import { DATA, Memory, Reduce, Filter, Cache, OrderCmd, ReduceLeaf, SetContext, Emitter, LeafCmd, NameBase, PATH, CLASS, DEFAULT_RULE_TYPE, } from './type' import { Model } from './model' import { Map } from './map' import { Struct } from './struct' import { List } from './list' import { Query } from './query' type IdProcess = (item: string) => void type PlainProcess<O> = (item: Partial<O>) => void type Emitters = { order: Emitter<OrderCmd> reduce: Emitter<LeafCmd> & { default: Emitter<LeafCmd>; default_origin: Emitter<LeafCmd> } } type ReduceContext<A extends DEFAULT_RULE_TYPE> = { map: typeof Map query: Query<A> memory: Memory cache: Cache['$format'] paths: { _reduce: Reduce } } function each_by_id<A extends DEFAULT_RULE_TYPE>({ from }: SetContext<A>, process: IdProcess) { if (from instanceof Array) { for (const item of from) { process((item as any).id || item) } } } function each<A extends DEFAULT_RULE_TYPE>({ from }: SetContext<A>, process: PlainProcess<A[0]>) { if (from instanceof Array) { for (const item of from) { process(item) } } else if (from instanceof Object) { for (const id in from) { const item = from[id] item._id = id process(item) } } } function validate(item: any, meta: Metadata, chklist: Filter[]): boolean { if (!item || !chklist) { return false } for (let chk of chklist) { if (!chk(item, meta)) { return false } } return true } export class Finder<A extends DEFAULT_RULE_TYPE> { $name!: NameBase all!: Query<A> model!: typeof Model | typeof Struct list!: CLASS<List<A>> map!: typeof Map constructor() {} join({ $name, model, all, map, list, }: { $name: NameBase all: Query<A> map: typeof Map list: CLASS<List<A>> model: typeof Model | typeof Struct }) { this.$name = $name this.all = all this.map = map this.list = list this.model = model State.notify(this.$name.list) } calculate(query: Query<A>, memory: Memory) { if (query._step >= State.step[this.$name.list]) { return } const base = State.base(this.$name.list) delete query._reduce query._step = step() const ctx: ReduceContext<A> = { map: this.map, query, memory, cache: _cloneDeep(base.$format), paths: { _reduce: { list: [], hash: {}, }, }, } if (query._all_ids) { let ids = query._all_ids if (query._is_uniq) { ids = _uniq(ids) } this.reduce(ctx, ids) } else if (query === query.all) { this.reduce(ctx, Object.keys(memory)) } else if (query._is_uniq) { let ids = [] for (const partition of query.$partition) { const tgt = _get(query.all, `reduce.${partition}`) ids = _union(ids, tgt) } this.reduce(ctx, ids) } else { for (const partition of query.$partition) { const tgt = _get(query.all, `reduce.${partition}`) this.reduce(ctx, tgt) } } this.finish(ctx) } reduce({ map, cache, paths, query, memory }: ReduceContext<A>, ids: string[]) { if (!ids) { return } for (let id of ids) { const o = memory[id] if (o) { const { meta, item, $group } = o if (!validate(item, meta, query._filters)) { continue } for (let [path, a] of $group) { const o = (paths[path] = cache[path]) map.reduce(query, path, item, o, a) } } } } finish({ map, paths, query }: ReduceContext<A>) { for (const path in paths) { const o = paths[path] map.finish(query, path, o, this.list) _set(query, path, o) } for (const path in query.$sort) { const cmd: OrderCmd = query.$sort[path] const from: ReduceLeaf = _get(query, path) if (from) { const sorted = map.order(query, path, from, from, cmd, this.list) const dashed = map.dash(query, path, sorted, from, cmd, this.list) const result = map.post_proc(query, path, dashed, from, cmd, this.list) this.list.bless(result as any, query) result.from = from _set(query, path, result) } } } data_set(type: string, from: DATA<A[0]>, parent: Object | undefined) { const meta = State.meta() const base = State.base(this.$name.list) const journal = State.journal(this.$name.list) const { deploys } = this.$name return this[type]({ base, journal, meta, model: this.model, all: this.all, deploys, from, parent, }) } data_emitter({ base, journal }: SetContext<A>, { item, $group }): Emitters { if (!base.$format) { throw new Error('bad context.') } const order = (keys: PATH, cmd: OrderCmd) => { if ('string' === typeof keys) { keys = [keys] } const path = [`_reduce`, ...keys].join('.') base.$sort[path] = cmd journal.$sort[path] = cmd } const reduce = (keys: PATH, cmd: LeafCmd) => { if ('string' === typeof keys) { keys = [keys] } const path = [`_reduce`, ...keys].join('.') cmd = reduce.default(keys, cmd) $group.push([path, cmd]) const map = base.$format[path] || (base.$format[path] = {}) const map_j = journal.$format[path] || (journal.$format[path] = {}) this.map.init(map, cmd) this.map.init(map_j, cmd) } reduce.default = reduce.default_origin = function (keys: PATH, cmd: LeafCmd) { if (keys.length) { return cmd } reduce.default = (_keys, cmd) => cmd const bare = { set: item.id, list: true, } return Object.assign(bare, cmd) } return ({ reduce, order } as any) as Emitters } data_init( { model, parent, deploys }: SetContext<A>, { item }: Datum, { reduce, order }: Emitters ) { model.bless(item as any) parent && _merge(item, parent) model.deploy.call(item, model) for (const deploy of deploys) { deploy.call(item, { o: item, model, reduce, order }) } } data_entry({ model }: SetContext<A>, { item }: Datum, { reduce, order }: Emitters) { model.map_partition(item, reduce) model.map_reduce(item, reduce) if (reduce.default === reduce.default_origin) { reduce([], {}) } model.order(item, order) } reset(ctx: SetContext<A>) { ctx.journal.$memory = PureObject() const news = (ctx.base.$memory = ctx.all.$memory = PureObject()) this.merge(ctx) for (let key in ctx.base.$memory) { const old = ctx.base.$memory[key] const item = news[key] if (item == null) { ctx.model.delete(old) } } return true } merge(ctx: SetContext<A>) { let is_hit = false each(ctx, (item) => { const o = new Datum(ctx.meta, item as any) const emit = this.data_emitter(ctx, o) this.data_init(ctx, o, emit) this.data_entry(ctx, o, emit) const id = item.id if (!id) { throw new Error(`detect bad data: ${JSON.stringify(item)}`) } ctx.journal.$memory[id] = o ctx.base.$memory[id] = o const old = ctx.base.$memory[item.id!] if (old != null) { ctx.model.update(item, old.item) } else { ctx.model.create(item) } return (is_hit = true) }) return is_hit } remove(ctx: SetContext<A>) { let is_hit = false each_by_id(ctx, (id) => { const old = ctx.base.$memory[id] if (old != null) { ctx.model.delete(old.item) delete ctx.journal.$memory[id] delete ctx.base.$memory[id] is_hit = true } }) return is_hit } update(ctx: SetContext<A>, parent: Object) { let is_hit = false each_by_id(ctx, (id) => { const old = ctx.base.$memory[id] if (!old) { return } _merge(old.item, parent) old.$group = [] const emit = this.data_emitter(ctx, old) this.data_entry(ctx, old, emit) ctx.model.update(old.item, old.item) is_hit = true }) return is_hit } }