@memsdb/core
Version:
A simple embedded document based database with advanced querying, advanced population/tree creation, and multiple storage and backup providers.
306 lines • 11.4 kB
JavaScript
import { v4 } from 'uuid';
import debug from 'debug';
import { VoidBackup } from '@memsdb/backup-void';
import { MemoryStorage } from '@memsdb/storage-memory';
const memsdb_ = debug('memsdb');
/**
* Database constructor containing all the initialised collections
* @category Core
*/
export class DB {
/**
* Construct a new in memory db with the provided collection references
* @param name Name of database
* @param opts Options object to modify DB behaviour (mostly unused)
*/
constructor(name = v4(), opts = {}) {
/** Key based object containing all the collections */
this.name = 'memsdb';
this.collections = new Map();
/**
* @ignore
* List of event handlers
*/
this.eventHandlers = new Map();
this.queryHandlers = new Map();
this.name = name;
const _ = this.db_ = memsdb_.extend(`<db>${name}`);
const { useDynamicIndexes = false, backupProvider = new VoidBackup(), eventHandlers, storageEngine = new MemoryStorage(), } = opts;
this.options = {
useDynamicIndexes,
backupProvider,
};
this.storageEngine = storageEngine;
if (eventHandlers) {
/* DEBUG */ _('Adding event handlers');
const eventTypes = Object.keys(eventHandlers);
const addEventHandler = (type, handler) => {
const evHandler = this.eventHandlers.get(type);
if (evHandler) {
evHandler.push(handler);
}
else
this.eventHandlers.set(type, [handler]);
};
eventTypes.forEach(handlerType => {
if (eventHandlers) {
const handlers = eventHandlers[handlerType];
if (!handlers)
return;
if (Array.isArray(handlers))
handlers.forEach(handler => addEventHandler(handlerType, handler));
else
addEventHandler(handlerType, handlers);
}
});
}
}
/**
* Add custom query handlers to allow for custom filtering
* @param queryHandler QueryHandler or QueryHandler array to add to the map.
*/
addQueryHandler(queryHandler) {
if (Array.isArray(queryHandler))
queryHandler.forEach(this.addQueryHandler);
else
this.queryHandlers.set(queryHandler.operator, queryHandler.func);
}
/**
* Add an EventHandler class to the DB
* @param eventHandler EventHandler or array of EventHandler classes to add to the DB
*/
addEventHandler(eventHandler) {
const addHandler = (handler) => {
const handlerType = handler.eventType;
const evHandlers = this.eventHandlers.get(handlerType);
if (evHandlers) {
evHandlers.push(handler.func);
}
else
this.eventHandlers.set(handlerType, [handler.func]);
};
if (Array.isArray(eventHandler)) {
eventHandler.forEach(addHandler);
}
else {
addHandler(eventHandler);
this.emitEvent({
event: 'EventDBHandlerAdded',
db: this,
handler: eventHandler,
});
}
}
/**
* Loop over the EventHandlers and emit the event to the provided function
* @param event Event to emit
*/
emitEvent(event) {
const evHandlers = this.eventHandlers.get(event.event);
if (evHandlers) {
evHandlers.forEach(handler => handler(event));
}
}
/**
* Return a specified collection by name
* @param name Collection name to select
*/
c(name) {
/* DEBUG */ this.db_('Finding and returning collection with name/key of `%s`', name);
return this.collections.get(name);
}
/**
* Alias of this.c() - Returns a specified collection
* @param name Name of collection to retrieve
*/
collection(name) {
return this.c(name);
}
/**
* Add a new collection to the DB. It won't replace a collection unless you specify to
* @param collection Collection to add to the db
* @param replace Replace the specified collection if it exists
*/
addCollection(collection, opts = { replace: false }) {
const _ = this.db_.extend('addCollection');
/* DEBUG */ _('Adding collection `%s` to DB. Replace if it already exists:', collection.name, opts.replace ? 'true' : 'false');
/* DEBUG */ _('Emitting event "EventDBAddCollection"');
this.emitEvent({
event: 'EventDBAddCollection',
collection,
opts,
});
const hasCollection = this.collections.has(collection.name);
if (!hasCollection || opts.replace) {
/* DEBUG */ _('Replacing or setting collection');
this.collections.set(collection.name, collection);
}
return collection;
}
/**
* Delete a collection and all its documents
* @param name Collection name to delete
*/
deleteCollection(name) {
const _ = this.db_.extend('deleteCollection');
try {
/* DEBUG */ _('Removing collection `%s` from DB', name);
const collection = this.collections.get(name);
if (!collection)
return this;
/* DEBUG */ _('Emitting event "EventDBAddCollection"');
this.emitEvent({
event: 'EventDBDeleteCollection',
collection: collection,
});
collection.docs.forEach(doc => doc.delete());
this.collections.delete(name);
/* DEBUG */ _('Emitting event "EventDBDeleteCollectionComplete"');
this.emitEvent({
event: 'EventDBDeleteCollectionComplete',
name,
success: true,
});
}
catch (err) {
/* DEBUG */ _("Collection deletion failed successfully, collection `%s` doesn't exist", name);
/* DEBUG */ _('Emitting event "EventDBDeleteCollectionComplete" with error');
this.emitEvent({
event: 'EventDBDeleteCollectionComplete',
name,
success: false,
error: err,
});
}
finally {
return this;
}
}
/**
* Empty out a collection, deleting the documents but leaving the collection
* structure intact
* @param name Empty out a specified collection
*/
emptyCollection(name) {
const _ = this.db_.extend('emptyCollection');
const collection = this.collection(name);
if (!collection)
return this;
try {
/* DEBUG */ _('Emptying collection `%s`. Current document count: %d', name, collection.docs.length);
/* DEBUG */ _('Emitting event "EventDBEmptyCollection"');
this.emitEvent({
event: 'EventDBEmptyCollection',
collection: collection,
});
collection.docs.forEach(doc => doc.delete());
collection.docs.length = 0;
/* DEBUG */ _('Emptying collection `%s` completed. Current document count: %d', name, collection.docs.length);
/* DEBUG */ _('Emitting event "EventDBEmptyCollection"');
this.emitEvent({
event: 'EventDBEmptyCollectionComplete',
collection: collection,
success: true,
});
}
catch (err) {
/* DEBUG */ _('Emptying collection `%s` failed as it does not exist.', name);
/* DEBUG */ _('Emitting event "EventDBEmptyCollection" with error');
this.emitEvent({
event: 'EventDBEmptyCollectionComplete',
collection,
success: false,
error: err,
});
}
finally {
return this;
}
}
/**
* Backup collection data to the provided BackupProvider.
*/
backup() {
const _ = this.db_.extend('backup');
/* DEBUG */ _('Starting backup');
const backup = {};
/* DEBUG */ _('Serialising collections');
this.collections.forEach((collection, key) => {
const keys = Object.keys(collection.schema);
const values = collection.docs.map(doc => [
doc.id,
doc._createdAt,
doc._updatedAt,
...keys.map(key => doc.data[key]),
]);
keys.unshift('id', '_createdAt', '_updatedAt');
backup[key] = {
keys,
values,
};
});
/* DEBUG */ _('Data structure created for backup');
/* DEBUG */ _('Emitting event "EventDBBackup"');
this.emitEvent({
event: 'EventDBBackup',
backup,
});
/* DEBUG */ _('Backing up database to BackupProvider');
const backedUp = this.options.backupProvider.save(backup);
/* DEBUG */ _('Backed up to BackupProvider, status: %s', backedUp ? 'success' : 'failed');
/* DEBUG */ _('Emitting event "EventDBBackupComplete"');
this.emitEvent({
event: 'EventDBBackupComplete',
backup,
status: backedUp ? 'success' : 'failed',
});
/* DEBUG */ _('Backup finished, result: %s', backedUp ? 'Data backed up' : 'Data NOT backed up');
}
/**
* Restore collection documents from a backup using the provided
* BackupProvider. This won't overwrite any documents
*/
restore() {
const _ = this.db_.extend('restore');
/* DEBUG */ _('Restoring database collections from BackupProvider');
const backup = this.options.backupProvider.load();
/* DEBUG */ _('Emitting event "EventDBRestore"');
this.emitEvent({
event: 'EventDBRestore',
backup,
});
/* DEBUG */ _('Collection data loaded from BackupProvider');
const collectionKeys = Object.keys(backup);
/* DEBUG */ _('%d collections to restore', collectionKeys.length);
collectionKeys.forEach(colKey => {
const col = this.collection(colKey);
if (!col)
return;
const data = backup[colKey];
data.values.forEach(docData => {
const doc = {};
data.keys.forEach((key, i) => {
// Skip the ID, _createdAt, and _updatedAt
if (i < 3)
return;
doc[key] = docData[i];
});
const newDoc = col.insert({
doc,
id: docData[0],
_createdAt: docData[1],
_updatedAt: docData[2],
});
newDoc._createdAt = docData[1];
newDoc._updatedAt = docData[2];
});
});
/* DEBUG */ _('Emitting event "EventDBRestoreComplete"');
this.emitEvent({
event: 'EventDBRestoreComplete',
backup,
});
/* DEBUG */ _('Database restored');
}
}
//# sourceMappingURL=DB.js.map