UNPKG

@minatojs/driver-memory

Version:
226 lines (194 loc) 7.3 kB
import { clone, deepEqual, Dict, makeArray, mapValues, noop, omit, pick } from 'cosmokit' import { Driver, Eval, executeEval, executeQuery, executeSort, executeUpdate, Field, isAggrExpr, RuntimeError, Selection, z } from 'minato' export class MemoryDriver extends Driver<MemoryDriver.Config> { static name = 'memory' _store: Dict<any[]> = { _fields: [], } _indexes: Dict<Dict<any>> = {} async prepare(name: string) {} async start() { // await this.#loader?.start(this.#store) } async $save(name: string) { // await this.#loader?.save(name, this.#store[name]) } async stop() { // await this.#loader?.stop(this.#store) } table(sel: string | Selection.Immutable | Dict<string | Selection.Immutable>, env: any = {}): any[] { if (typeof sel === 'string') { return this._store[sel] ||= [] } if (!Selection.is(sel)) { throw new Error('Should not reach here') } const { ref, query, table, args, model } = sel const { fields, group, having, optional = {} } = sel.args[0] let data: any[] if (typeof table === 'object' && !Selection.is(table)) { const entries = Object.entries(table).map(([name, sel]) => [name, this.table(sel, env)] as const) const catesian = (entries: (readonly [string, any[]])[]): any[] => { if (!entries.length) return [] const [[name, rows], ...tail] = entries if (!tail.length) return rows.map(row => ({ [name]: row })) return rows.flatMap(row => { let res = catesian(tail).map(tail => ({ ...tail, [name]: row })) if (Object.keys(table).length === tail.length + 1) { res = res.map(row => ({ ...env, [ref]: row })).filter(data => executeEval(data, having)).map(x => x[ref]) } return !optional[tail[0]?.[0]] || res.length ? res : [{ [name]: row }] }) } data = catesian(entries) } else { data = this.table(table, env).filter(row => executeQuery(row, query, ref, env)) } env[ref] = data const branches: { index: Dict; table: any[] }[] = [] const groupFields = group ? pick(fields!, group) : fields for (let row of executeSort(data, args[0], ref)) { row = model.format(row, false) for (const key in model.fields) { if (!Field.available(model.fields[key])) continue row[key] ??= null } let index = row if (fields) { index = mapValues(groupFields!, (expr) => executeEval({ ...env, [ref]: row }, expr)) } let branch = branches.find((branch) => { if (!group || !groupFields) return false for (const key in groupFields) { if (!deepEqual(branch.index[key], index[key])) return false } return true }) if (!branch) { branch = { index, table: [] } branches.push(branch) } branch.table.push(row) } return branches.map(({ index, table }) => { if (group) { if (having) { const value = executeEval(table.map(row => ({ ...env, [ref]: row, _: row })), having) if (!value) return } for (const key in omit(fields!, group)) { index[key] = executeEval(table.map(row => ({ ...env, [ref]: row, _: row })), fields![key]) } } return model.parse(index, false) }).filter(Boolean) } async drop(table: string) { delete this._store[table] } async dropAll() { this._store = { _fields: [] } } async stats() { return { tables: mapValues(this._store, (rows, name) => ({ name, count: rows.length, size: 0 })), size: 0 } } async get(sel: Selection.Immutable) { return this.table(sel as Selection) } async eval(sel: Selection.Immutable, expr: Eval.Expr) { const { query, table } = sel const ref = typeof table === 'string' ? sel.ref : table.ref as string const data = this.table(table).filter(row => executeQuery(row, query, ref)) return executeEval(data.map(row => ({ [ref]: row, _: row })), expr) } async set(sel: Selection.Mutable, data: {}) { const { table, ref, query } = sel const matched = this.table(table) .filter(row => executeQuery(row, query, ref)) .map(row => executeUpdate(row, data, ref)) .length this.$save(table) return { matched } } async remove(sel: Selection.Mutable) { const { ref, query, table } = sel const data = this.table(table) this._store[table] = data.filter(row => !executeQuery(row, query, ref)) this.$save(table) const count = data.length - this._store[table].length return { removed: count, matched: count } } async create(sel: Selection.Mutable, data: any) { const { table, model } = sel const { primary, autoInc } = model const store = this.table(table) if (!Array.isArray(primary) && autoInc && !(primary in data)) { let meta = this._store._fields.find(row => row.table === table && row.field === primary) if (!meta) { meta = { table, field: primary, autoInc: 0 } this._store._fields.push(meta) } meta.autoInc += 1 data[primary] = meta.autoInc } else { const duplicated = await this.database.get(table, pick(model.format(data), makeArray(primary))) if (duplicated.length) { throw new RuntimeError('duplicate-entry') } } store.push(clone(data)) this.$save(table) return clone(clone(data)) } async upsert(sel: Selection.Mutable, data: any, keys: string[]) { const { table, model, ref } = sel const result = { inserted: 0, matched: 0 } for (const update of data) { const row = this.table(table).find(row => { return keys.every(key => row[key] === update[key]) }) if (row) { executeUpdate(row, update, ref) result.matched++ } else { const data = executeUpdate(model.create(), update, ref) await this.create(sel, data).catch(noop) result.inserted++ } } this.$save(table) return result } executeSelection(sel: Selection.Immutable, env: any = {}) { const expr = sel.args[0], table = sel.table as Selection if (Array.isArray(env)) env = { [sel.ref]: env } const data = this.table(sel.table, env) const res = isAggrExpr(expr) ? data.map(row => executeEval({ ...env, [table.ref]: row, _: row }, expr)) : executeEval(Object.assign(data.map(row => ({ [table.ref]: row, _: row })), env), expr) return res } async withTransaction(callback: () => Promise<void>) { const data = clone(this._store) await callback().catch((e) => { this._store = data throw e }) } async getIndexes(table: string) { return Object.values(this._indexes[table] ?? {}) } async createIndex(table: string, index: Driver.Index) { const name = index.name ?? 'index:' + Object.entries(index.keys).map(([key, direction]) => `${key}_${direction}`).join('+') this._indexes[table] ??= {} this._indexes[table][name] = { name, unique: false, ...index } } async dropIndex(table: string, name: string) { this._indexes[table] ??= {} delete this._indexes[table][name] } } export namespace MemoryDriver { export interface Config {} export const Config: z<Config> = z.object({}) } export default MemoryDriver