UNPKG

@wxn0brp/db

Version:

A simple file-based database management system with support for CRUD operations, custom queries, and graph structures.

156 lines (155 loc) 6 kB
function pickByPath(obj, paths) { const result = {}; for (const path of paths) { let src = obj; let dst = result; for (let i = 0; i < path.length; i++) { const k = path[i]; if (src == null) break; if (i === path.length - 1) { dst[k] = src[k]; } else { dst[k] ||= {}; dst = dst[k]; src = src[k]; } } } return result; } function autoSelect(rel, key) { const select = Array.isArray(rel.select) ? [...rel.select] : undefined; const shouldDelete = select && !select.includes(key); if (shouldDelete) select.push(key); return [select, shouldDelete]; } function convertSearchObjToSearchArray(obj, parentKeys = []) { return Object.entries(obj).reduce((acc, [key, value]) => { const currentPath = [...parentKeys, key]; if (!value) { return acc; } else if (typeof value === 'object' && value !== null && !Array.isArray(value)) { return [...acc, ...convertSearchObjToSearchArray(value, currentPath)]; } else { return [...acc, currentPath]; } }, []); } async function processRelations(dbs, cfg, data, parentList = null) { if (!data && !parentList) return; const batchMode = Array.isArray(parentList); const targets = batchMode ? parentList : [data]; for (const key in cfg) { if (!cfg.hasOwnProperty(key)) continue; const rel = cfg[key]; const { pk = "_id", fk = "_id", type = "1", path, as = key, select, findOpts = {}, through } = rel; const [dbKey, coll] = path; const db = dbs[dbKey]; if (type === "1") { const keys = [...new Set(targets.map(i => i[pk]))]; const [selectSafe, deleteSelect] = autoSelect(rel, fk); const results = await db.find(coll, { $in: { [fk]: keys } }, {}, {}, { select: selectSafe }); const map = new Map(results.map(row => [row[fk], row])); for (const item of targets) { const result = map.get(item[pk]) || null; if (result && rel.relations) { await processRelations(dbs, rel.relations, result); } if (deleteSelect && result) delete result[fk]; item[as] = result; } } else if (type === "11") { const cache = new Map(); const [selectSafe, deleteSelect] = autoSelect(rel, fk); for (const item of targets) { const id = item[pk]; if (!cache.has(id)) { cache.set(id, await db.findOne(coll, { [fk]: id }, {}, { select: selectSafe })); } const result = cache.get(id) || null; if (result && rel.relations) { await processRelations(dbs, rel.relations, result); } if (deleteSelect && result) delete result[fk]; item[as] = result; } } else if (type === "1n") { const ids = targets.map(i => i[pk]); const [selectSafe, deleteSelect] = autoSelect(rel, fk); const results = await db.find(coll, { $in: { [fk]: ids } }, {}, findOpts || {}, { select: selectSafe }); const grouped = results.reduce((acc, row) => { const id = row[fk]; (acc[id] ||= []).push(row); return acc; }, {}); for (const item of targets) { item[as] = grouped[item[pk]] || []; } if (rel.relations) { await Promise.all(results.map(row => processRelations(dbs, rel.relations, row))); } if (deleteSelect) for (const r of results) delete r[fk]; } else if (type === "nm") { if (!through || !through.table || !through.pk || !through.fk) { throw new Error(`Relation type "nm" requires a defined 'through' in '${key}'`); } for (const item of targets) { const pivotDb = dbs[through.db || dbKey]; const pivots = await pivotDb.find(through.table, { [through.pk]: item[pk] }); const ids = pivots.map(p => p[through.fk]); const related = await db.find(coll, { $in: { [fk]: ids } }, {}, {}, { select }); item[as] = related; if (rel.relations) { await Promise.all(related.map(row => processRelations(dbs, rel.relations, row))); } } } else { throw new Error(`Unknown relation type: ${type}`); } } } class Relation { dbs; constructor(dbs) { this.dbs = dbs; } async findOne(path, search, relations, select) { const [dbKey, coll] = path; const db = this.dbs[dbKey]; const data = await db.findOne(coll, search); if (!data) return null; if (typeof select === "object" && !Array.isArray(select)) { select = convertSearchObjToSearchArray(select); } await processRelations(this.dbs, relations, data); return select ? pickByPath(data, select) : data; } async find(path, search, relations, select, findOpts = {}) { const [dbKey, coll] = path; const db = this.dbs[dbKey]; const data = await db.find(coll, search, {}, findOpts); if (relations) await processRelations(this.dbs, relations, null, data); if (typeof select === "object" && !Array.isArray(select)) { select = convertSearchObjToSearchArray(select); } return select ? data.map(d => pickByPath(d, select)) : data; } } export default Relation;