const fs = require('fs').promises; const Table = require('./Table'); const utils = require('./utils'); /* API methods: openDb closeDb create drop open openAll close closeAll tableExists getDbInfo getDbSize select insert update delete esc */ class JembaDb { constructor() { this.opened = false; } /* query = { dbPath: String, //table open defaults inMemory: Boolean, false cacheSize: Number, 5 compressed: Number, {0..9}, 0 recreate: Boolean, false, autoRepair: Boolean, false, forceFileClosing: Boolean, false, lazyOpen: Boolean, false, } */ async openDb(query = {}) { if (this.opened) throw new Error(`Database ${this.dbPath} has already been opened`); if (!query.dbPath) throw new Error(`'query.dbPath' parameter is required`); this.dbPath = query.dbPath; await fs.mkdir(this.dbPath, { recursive: true }); this.table = new Map(); this.tableOpenDefaults = { inMemory: query.inMemory, cacheSize: query.cacheSize, compressed: query.compressed, recreate: query.recreate, autoRepair: query.autoRepair, forceFileClosing: query.forceFileClosing, lazyOpen: query.lazyOpen, }; this.opened = true; } async closeDb() { if (!this.opened) return; await this.closeAll(); this.opened = false; //console.log('closed'); } checkOpened() { if (!this.opened) throw new Error('Database closed'); } /* query = { table: 'tableName', quietIfExists: Boolean, inMemory: Boolean, false cacheSize: Number, 5 compressed: Number, {0..9}, 0 recreate: Boolean, false, autoRepair: Boolean, false, forceFileClosing: Boolean, false, lazyOpen: Boolean, false, in: 'tableName', flag: Object || Array, {name: 'flag1', check: '(r) => r.id > 10'} hash: Object || Array, {field: 'field1', type: 'string', depth: 11, allowUndef: false} index: Object || Array, {field: 'field1', type: 'string', depth: 11, allowUndef: false} } result = {} */ async create(query = {}) { this.checkOpened(); if ((!query.table && !query.in) || (query.table && query.in)) throw new Error(`One of 'query.table' or 'query.in' parameters is required, but not both`); let table; if (query.table) { if (await this.tableExists({table: query.table})) { if (!query.quietIfExists) throw new Error(`Table '${query.table}' already exists`); table = this.table.get(query.table); } else { table = new Table(); this.table.set(query.table, table); await this.open(query); } } else { if (await this.tableExists({table: query.in})) { table = this.table.get(query.in); } else { throw new Error(`Table '${query.in}' does not exist`); } } if (query.flag || query.hash || query.index) { await table.create({ quietIfExists: query.quietIfExists, flag: query.flag, hash: query.hash, index: query.index, }); } return {}; } /* query = { table: 'tableName', in: 'tableName', flag: Object || Array, {name: 'flag1'} hash: Object || Array, {field: 'field1'} index: Object || Array, {field: 'field1'} } result = {} */ async drop(query = {}) { this.checkOpened(); if ((!query.table && !query.in) || (query.table && query.in)) throw new Error(`One of 'query.table' or 'query.in' parameters is required, but not both`); if (query.table) { if (await this.tableExists({table: query.table})) { const table = this.table.get(query.table); if (table && table.opened) { await table.close(); } const basePath = `${this.dbPath}/${query.table}`; await fs.rmdir(basePath, { recursive: true }); this.table.delete(query.table); } else { throw new Error(`Table '${query.table}' does not exist`); } } else { if (await this.tableExists({table: query.in})) { const table = this.table.get(query.in); if (table) { if (query.flag || query.hash || query.index) { await table.drop({ flag: query.flag, hash: query.hash, index: query.index, }); } } else { throw new Error(`Table '${query.in}' has not been opened yet`); } } else { throw new Error(`Table '${query.in}' does not exist`); } } return {}; } /* query = { (!) table: 'tableName', inMemory: Boolean, false cacheSize: Number, 5 compressed: Number, {0..9}, 0 recreate: Boolean, false, autoRepair: Boolean, false, forceFileClosing: Boolean, false, lazyOpen: Boolean, false, } */ async open(query = {}) { this.checkOpened(); if (!query.table) throw new Error(`'query.table' parameter is required`); if (await this.tableExists({table: query.table})) { let table = this.table.get(query.table); if (!table) { table = new Table(); } if (!table.opened) { const opts = Object.assign({}, this.tableOpenDefaults, query); opts.tablePath = `${this.dbPath}/${query.table}`; await table.open(opts); } this.table.set(query.table, table); } else { throw new Error(`Table '${query.table}' does not exist`); } } async _getTableList() { const result = []; const files = await fs.readdir(this.dbPath, { withFileTypes: true }); for (const file of files) { if (file.isDirectory()) { if (file.name.indexOf('___temporary_recreating') >= 0) continue; result.push(file.name); } } return result; } /* query = { inMemory: Boolean, false cacheSize: Number, 5 compressed: Number, {0..9}, 0 recreate: Boolean, false, autoRepair: Boolean, false, forceFileClosing: Boolean, false, lazyOpen: Boolean, false, } */ async openAll(query = {}) { this.checkOpened(); const tables = await this._getTableList(); //sequentially for (const table of tables) { this.checkOpened(); await this.open(Object.assign({}, query, {table})); } /*const promises = []; for (const table of tables) { promises.push(this.open(Object.assign({}, query, {table}))); } await Promise.all(promises);*/ } /* query = { (!) table: 'tableName', } */ async close(query = {}) { this.checkOpened(); if (!query.table) throw new Error(`'query.table' parameter is required`); if (await this.tableExists({table: query.table})) { let table = this.table.get(query.table); if (table) { await table.close(); } this.table.delete(query.table); } else { throw new Error(`Table '${query.table}' does not exist`); } } async closeAll() { this.checkOpened(); const promises = []; for (const table of this.table.keys()) { promises.push(this.close({table})); } await Promise.all(promises); } /* query = { (!) table: 'tableName' }, result = Boolean */ async tableExists(query = {}) { this.checkOpened(); if (!query.table) throw new Error(`'query.table' parameter is required`); if (this.table.has(query.table)) return true; if (await utils.pathExists(`${this.dbPath}/${query.table}`)) return true; return false; } /* query = { table: 'tableName' }, result = { dbPath: String, tableName1: {opened: Boolean, ...}, tableName2: {opened: Boolean, ...}, ... } */ async getDbInfo(query = {}) { this.checkOpened(); const tables = await this._getTableList(); const result = {dbPath: this.dbPath}; for (const table of tables) { if (!query.table || (query.table && table == query.table)) { const tableInstance = this.table.get(table); if (tableInstance && tableInstance.opened) { result[table] = await tableInstance.getMeta(); result[table].opened = true; } else { result[table] = {opened: false}; } } } return result; } /* result = { total: Number, tables: { tableName1: Number, tableName2: Number, ... } } */ async getDbSize() { this.checkOpened(); const dirs = await fs.readdir(this.dbPath, { withFileTypes: true }); const result = {total: 0, tables: {}}; for (const dir of dirs) { if (dir.isDirectory()) { const table = dir.name; const tablePath = `${this.dbPath}/${table}`; const files = await fs.readdir(tablePath, { withFileTypes: true }); if (!result.tables[table]) result.tables[table] = 0; for (const file of files) { if (file.isFile()) { let size = 0; try { size = (await fs.stat(`${tablePath}/${file.name}`)).size; } catch(e) { // } result.tables[table] += size; result.total += size; } } } } return result; } /* query = { (!) table: 'tableName', distinct: 'fieldName' || Array, count: Boolean, map: '(r) => ({id1: r.id, ...})', where: `@@index('field1', 10, 20)`, sort: '(a, b) => a.id - b.id', limit: 10, offset: 10, } result = Array */ async select(query = {}) { this.checkOpened(); if (!query.table) throw new Error(`'query.table' parameter is required`); const table = this.table.get(query.table); if (table) { return await table.select(query); } else { if (await this.tableExists({table: query.table})) { throw new Error(`Table '${query.table}' has not been opened yet`); } else { throw new Error(`Table '${query.table}' does not exist`); } } } /* query = { (!) table: 'tableName', replace: Boolean, (!) rows: Array, } result = { (!) inserted: Number, (!) replaced: Number, } */ async insert(query = {}) { this.checkOpened(); if (!query.table) throw new Error(`'query.table' parameter is required`); const table = this.table.get(query.table); if (table) { return await table.insert(query); } else { if (await this.tableExists({table: query.table})) { throw new Error(`Table '${query.table}' has not been opened yet`); } else { throw new Error(`Table '${query.table}' does not exist`); } } } /* query = { (!) table: 'tableName', (!) mod: '(r) => r.count++', where: `@@index('field1', 10, 20)`, sort: '(a, b) => a.id - b.id', limit: 10, offset: 10, } result = { (!) updated: Number, } */ async update(query = {}) { this.checkOpened(); if (!query.table) throw new Error(`'query.table' parameter is required`); const table = this.table.get(query.table); if (table) { return await table.update(query); } else { if (await this.tableExists({table: query.table})) { throw new Error(`Table '${query.table}' has not been opened yet`); } else { throw new Error(`Table '${query.table}' does not exist`); } } } /* query = { (!) table: 'tableName', where: `@@index('field1', 10, 20)`, sort: '(a, b) => a.id - b.id', limit: 10, offset: 10, } result = { (!) deleted: Number, } */ async delete(query = {}) { this.checkOpened(); if (!query.table) throw new Error(`'query.table' parameter is required`); const table = this.table.get(query.table); if (table) { return await table.delete(query); } else { if (await this.tableExists({table: query.table})) { throw new Error(`Table '${query.table}' has not been opened yet`); } else { throw new Error(`Table '${query.table}' does not exist`); } } } esc(obj) { return utils.esc(obj); } } module.exports = JembaDb;