Новый движок БД
This commit is contained in:
534
server/db/JembaDb/JembaDb.js
Normal file
534
server/db/JembaDb/JembaDb.js
Normal file
@@ -0,0 +1,534 @@
|
||||
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}))) {
|
||||
table = new Table();
|
||||
this.table.set(query.table, table);
|
||||
|
||||
await this.open(query);
|
||||
} else {
|
||||
if (!query.quietIfExists)
|
||||
throw new Error(`Table '${query.table}' already exists`);
|
||||
}
|
||||
} 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;
|
||||
Reference in New Issue
Block a user