@minatojs/driver-memory
Version:
In-memory Driver for Minato
226 lines (194 loc) • 7.3 kB
text/typescript
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