sqlite-tree-store
Version:
Simple document store over SQLite
231 lines (208 loc) • 7.21 kB
JavaScript
/**
* sqlite-tree-store
* Copyright(c) 2019 Dennis B. <denesey@gmail.com>
* MIT Licensed
*/
const sqlite = require('better-sqlite3')
/**
* @typedef {import('better-sqlite3')} BetterSqlite3
*/
/**
* The the main and only module export
* @param {(string|BetterSqlite3)} db Full/relative database file name, or BetterSqlite3.Database instance
* @param {string} tableName Name of the table, where opened/created model is stored
* @return {tree} result
*/
module.exports = function (db, tableName = 'tree') {
if (typeof db === 'string') {
db = sqlite(db)
}
(function () { // self-calling at startup, ensures db has proper structures
let existance = (name, type) => `SELECT name FROM sqlite_master WHERE type='${type}' AND name='${name}'`
let entities = [
{
exists: existance(tableName, 'table'),
definitions: [
`CREATE TABLE ${tableName} (id INTEGER PRIMARY KEY ASC AUTOINCREMENT, idp INTEGER REFERENCES ${tableName} (id) ON DELETE CASCADE, name STRING NOT NULL, type STRING, value STRING)`,
`CREATE UNIQUE INDEX ${tableName}_unique ON ${tableName} (idp, name)`,
`INSERT INTO ${tableName} (id, idp, name) values (0, 0, '_root_')`
]
},
{
exists: existance('v_' + tableName, 'view'),
definitions: [`CREATE VIEW v_${tableName} AS\n` +
`WITH RECURSIVE nested (id, level, path)\n` +
`AS (\n` +
` SELECT \n` +
` id,\n` +
` 0,\n` +
` name\n` +
` FROM \n` +
` ${tableName}\n` +
` WHERE \n` +
` idp = 0 and id != idp\n` +
` UNION\n` +
` SELECT \n` +
` n.id,\n` +
` nested.level + 1,\n` +
` nested.path || '/' || name\n` +
` FROM\n` +
` ${tableName} n,\n` +
` nested\n` +
` WHERE\n` +
` n.idp = nested.id\n` +
`)\n` +
`SELECT \n` +
` nested.path,\n` +
` n.id,\n` +
` replace(hex(zeroblob(level * 6)), '00', ' ') || n.name AS name,\n` +
` n.type,\n` +
` n.value\n` +
`FROM \n` +
` nested,\n` +
` ${tableName} n\n` +
`WHERE\n` +
` n.id = nested.id\n` +
`ORDER BY \n` +
` nested.path`
]
}
]
for (let entity of entities) {
if (!db.prepare(entity.exists).get()) {
for (let statement of entity.definitions) db.prepare(statement).run()
}
}
})()
let selectNode = db.prepare(`select * from ${tableName} where idp = ? and name = ?`)
let selectNodes = db.prepare(`select * from ${tableName} where idp = ? and id != idp`)
let insertNode = db.prepare(`insert into ${tableName} (idp, name, type, value) values ($idp, $name, $type, $value)`)
let deleteNode = db.prepare(`delete from ${tableName} where id = $id`)
let updateValue = db.prepare(`update ${tableName} set value = $value, type = $type where id = $id`)
let renameNode = db.prepare(`update ${tableName} set name = $name where id = $id`)
function _setHiddenProperty (target, key, value) {
return Object.defineProperty(target, key, { enumerable: false, configurable: true, value: value })
}
function _renameProperty (node, meta, oldKey, newKey) {
if (newKey in node || !(oldKey in node) || !('id' in meta[oldKey])) return null
node[newKey] = node[oldKey]
_setHiddenProperty(meta, newKey, meta[oldKey])
renameNode.run({ id: meta[oldKey].id, name: newKey })
delete node[oldKey]
delete meta[oldKey]
return node[newKey]
}
// to db value & type conversion helper
function _toDbType (value) {
let type = typeof value
let primitive = null // for container types (objects, arrays) primitive stay's as null
if (value === null) return { primitive, type }
switch (value.constructor) {
case Array: type = 'array'; break
case Object: break
case Buffer: type = 'buffer'; primitive = value; break
case Function: primitive = value.toString(); break
case Date: type = 'date'; primitive = value.toISOString(); break
case Boolean: type = 'boolean'; primitive = (value) ? 1 : 0; break
case String:
case Number: primitive = value
}
return { primitive, type }
}
// from db value & type conversion helper
function _fromDbType ({ type, value, id }) {
switch (type) {
case 'array': return createNode(id, [])
case 'object': return createNode(id, {})
case 'buffer': return Buffer.from(value)
case 'date': return new Date(value)
case 'function': return String(value) // -- dummy
case 'boolean': return Boolean(value)
case 'string': return String(value)
default: return value
}
}
function createNode (id, source = {}) {
// meta-data for id, type, rename and other maybe...
const meta = new Proxy({}, {
set (target, key, value, receiver) {
return Reflect.set(source, key, value) // note: set original node <source> (not target or receiver)
},
get (target, key, receiver) {
if (!Reflect.has(target, key)) { // bypass non-existent meta keys
_setHiddenProperty(target, key, {
id: null,
rename: (newKey) => _renameProperty(source, target, key, newKey)
})
}
return Reflect.get(target, key)
}
})
return new Proxy(source, {
set (target, key, value, receiver) {
let { primitive, type } = _toDbType(value)
let node
if (!Reflect.has(target, key)) {
meta[key].id = insertNode.run({ idp: id, name: key, type: type, value: primitive }).lastInsertRowid
if (primitive === null) node = _fromDbType({ type, id: meta[key].id })
} else {
updateValue.run({ id: meta[key].id, type: type, value: primitive })
if (primitive === null) node = receiver[key]
}
meta[key].type = type
if (node) {
for (let subKey in value) {
node[subKey] = value[subKey] // note: (re)assigning proxified node causes recursive set() calling
}
value = node
}
return Reflect.set(target, key, value)
},
get (target, key, receiver) {
if (Reflect.has(target, key)) {
return Reflect.get(target, key)
} else if (key === '_') {
return meta
}
return undefined
},
deleteProperty (target, key) {
if (Reflect.has(target, key)) {
deleteNode.run({ id: meta[key].id })
delete meta[key]
return Reflect.deleteProperty(target, key)
}
return false
}
})
}
/**
* Build tree store from database
*
* @param {string[]} [path] (optional) array of strings representing path to node
* @param {number} [depth] (optional) build to desired depth
* @return {Proxy} result
*/
function tree (path = [], depth) {
let root = { id: 0, type: 'object' }
for (let name of path) {
root = selectNode.get(root.id, name)
if (!root) throw new Error('The specified path does not exist')
}
function _build (node, children, level) {
for (let child of children) {
let nodeValue = _fromDbType(child)
node._[child.name].id = child.id
if ((child.type === 'object' || child.type === 'array') && !(depth <= level + 1)) {
node._[child.name] = _build(nodeValue, selectNodes.all(child.id), level + 1)
} else {
node._[child.name] = nodeValue
}
}
return node
}
return _build(_fromDbType(root), selectNodes.all(root.id), 0)
}
return tree
}