metastocle
Version:
Decentralized data storage
439 lines (365 loc) • 14.4 kB
JavaScript
import { merge, camelCase, capitalize, get, pickBy, set } from "lodash-es";
import sizeof from "object-sizeof";
import loki from "spreadable/src/db/transports/loki/index.js";
import { v1 as uuidv1 } from "uuid";
import errors from "../../../errors.js";
import utils from "../../../utils.js";
import database from "../database/index.js";
const DatabaseMetastocle = database();
const DatabaseLoki = loki(DatabaseMetastocle);
export default (Parent) => {
/**
* Lokijs database transport
*/
return class DatabaseLokiMetastocle extends (Parent || DatabaseLoki) {
constructor(options = {}) {
options = merge({
metaPrefix: 'meta'
}, options);
super(options);
}
/**
* @see DatabaseMetastocle.prototype.createCollectionName
*/
createCollectionName(name) {
return this.options.metaPrefix + capitalize(name);
}
/**
* @see DatabaseMetastocle.prototype.createDocumentPrimaryKey
*/
createDocumentPrimaryKey() {
return uuidv1();
}
/**
* @see DatabaseMetastocle.prototype.createDocumentDuplicationKey
*/
createDocumentDuplicationKey() {
return uuidv1();
}
/**
* @see DatabaseMetastocle.prototype.removeDocumentSystemFields
*/
removeDocumentSystemFields(document, exclude = []) {
return pickBy(document, (v, k) => !k.startsWith('$') || (exclude.length && exclude.includes(k)));
}
/**
* @see DatabaseMetastocle.prototype.addCollection
*/
async addCollection(name, options = {}) {
const lokiOptions = Object.assign({}, options.loki || {});
if (options.pk) {
!lokiOptions.unique && (lokiOptions.unique = []);
!lokiOptions.unique.includes(options.pk) && (lokiOptions.unique.push(options.pk));
}
const fullName = this.createCollectionName(name);
this.col[fullName] = this.prepareCollection(fullName, lokiOptions);
return this.col[fullName];
}
/**
* @see DatabaseMetastocle.prototype.getCollection
*/
async getCollection(name) {
return this.col[this.createCollectionName(name)] || null;
}
/**
* @see DatabaseMetastocle.prototype.removeCollection
*/
async removeCollection(name) {
const fullName = this.createCollectionName(name);
this.loki.removeCollection(fullName);
delete this.col[fullName];
}
/**
* @see DatabaseMetastocle.prototype.emptyCollection
*/
async emptyCollection(name) {
this.col[this.createCollectionName(name)].clear();
}
/**
* @see DatabaseMetastocle.prototype.normalizeCollections
*/
async normalizeCollections() {
const collections = this.loki.listCollections();
for (let i = 0; i < collections.length; i++) {
const collection = collections[i];
if (!collection.name.startsWith(this.options.metaPrefix)) {
continue;
}
const name = camelCase(collection.name.substring(this.options.metaPrefix.length));
const nodeCollection = await this.node.getCollection(name);
if (!nodeCollection) {
await this.removeCollection(name);
continue;
}
const pk = nodeCollection.pk;
if (pk) {
this.col[collection.name]
.chain()
.where(doc => !doc[pk] || (typeof doc[pk] != 'string' && typeof doc[pk] != 'number'))
.remove();
}
await this.removeCollectionExcessDocuments(name);
}
}
/**
* @see DatabaseMetastocle.prototype.removeCollectionExcessDocuments
*/
async removeCollectionExcessDocuments(name) {
const collection = await this.node.getCollection(name);
let order = collection.limitationOrder || '$accessedAt';
!Array.isArray(order) && (order = [order]);
const newOrder = [];
for (let i = 0; i < order.length; i++) {
let item = order[i];
item = Array.isArray(item) ? [item[0], item[1] == 'desc'] : [item, false];
newOrder.push(item);
}
await this.removeCollectionExcessDocumentsByLimit(name, newOrder);
await this.removeCollectionExcessDocumentsBySize(name, newOrder);
}
/**
* @see DatabaseMetastocle.prototype.removeCollectionExcessDocumentsByLimit
*/
async removeCollectionExcessDocumentsByLimit(name, order) {
const fullName = this.createCollectionName(name);
const collection = await this.node.getCollection(name);
const count = await this.getCollectionSize(name);
if (!collection.limit || count <= collection.limit) {
return;
}
this.col[fullName].chain().find().compoundsort(order).limit(count - collection.limit).remove();
}
/**
* @see DatabaseMetastocle.prototype.removeCollectionExcessDocumentsBySize
*/
async removeCollectionExcessDocumentsBySize(name, order) {
const fullName = this.createCollectionName(name);
const collection = await this.node.getCollection(name);
if (!collection.maxSize) {
return;
}
let size = sizeof(this.col[fullName].data);
if (size <= collection.maxSize) {
return;
}
const docs = this.col[fullName].chain().find().compoundsort(order).data();
for (let i = 0; i < docs.length; i++) {
const doc = docs[i];
let docSize = sizeof(doc);
this.col[fullName].remove(doc);
size -= docSize;
if (size <= collection.maxSize) {
break;
}
}
}
/**
* @see DatabaseMetastocle.prototype.getCollectionSize
*/
async getCollectionSize(name) {
const fullName = this.createCollectionName(name);
const collection = this.loki.listCollections().find(c => c.name == fullName);
return collection.count;
}
/**
* @see DatabaseMetastocle.prototype.addDocument
*/
async addDocument(name, document) {
const fullName = this.createCollectionName(name);
const collection = await this.node.getCollection(name);
const count = await this.getCollectionSize(name);
const colData = this.col[fullName].data;
const errMsg = `Too much documents are in collection "${name}", you can't add new one`;
if (collection.limit && !collection.queue && count >= collection.limit) {
throw new errors.WorkError(errMsg, 'ERR_METASTOCLE_DOCUMENTS_LIMIT');
}
delete document.$loki;
document.$createdAt = document.$updatedAt = document.$accessedAt = Date.now();
document[collection.duplicationKey] = document[collection.duplicationKey] || this.createDocumentDuplicationKey(document);
document.$collection = name;
document = await this.handleDocument(document);
if (collection.maxSize && !collection.queue && sizeof(colData) + sizeof(document) > collection.maxSize) {
throw new errors.WorkError(errMsg, 'ERR_METASTOCLE_DOCUMENTS_MAX_SIZE');
}
document = this.col[fullName].insert(document);
await this.removeCollectionExcessDocuments(name);
return await this.prepareDocumentToGet(document);
}
/**
* @see DatabaseMetastocle.prototype.prepareDocumentToSet
*/
async prepareDocumentToSet(document, prevDocument = null) {
if (!document.$collection) {
const msg = `Document must have "$collection" field`;
throw new errors.WorkError(msg, 'ERR_METASTOCLE_INVALID_DOCUMENT_COLLECTION');
}
const collection = await this.node.getCollection(document.$collection);
if (collection.defaults) {
for (let key in collection.defaults) {
const handler = collection.defaults[key];
if (get(document, key) !== undefined) {
continue;
}
set(document, key, typeof handler == 'function' ? await handler(key, document, prevDocument) : handler);
}
}
if (collection.setters) {
for (let key in collection.setters) {
const handler = collection.setters[key];
const current = get(document, key);
const value = typeof handler == 'function' ? await handler(current, key, document, prevDocument) : handler;
set(document, key, value);
}
}
return document;
}
/**
* @see DatabaseMetastocle.prototype.prepareDocumentToGet
*/
async prepareDocumentToGet(document) {
if (!document.$collection) {
const msg = `Document must have "$collection" field`;
throw new errors.WorkError(msg, 'ERR_METASTOCLE_INVALID_DOCUMENT_COLLECTION');
}
const collection = await this.node.getCollection(document.$collection);
if (collection.getters) {
for (let key in collection.getters) {
const handler = collection.getters[key];
const value = get(document, key);
set(document, key, typeof handler == 'function' ? await handler(value, key, document) : handler);
}
}
return document;
}
/**
* @see DatabaseMetastocle.prototype.handleDocument
*/
async handleDocument(document, options = {}) {
if (!document.$collection) {
const msg = `Document must have "$collection" field`;
throw new errors.WorkError(msg, 'ERR_METASTOCLE_INVALID_DOCUMENT_COLLECTION');
}
const fullName = this.createCollectionName(document.$collection);
const collection = await this.node.getCollection(document.$collection);
document = await this.prepareDocumentToSet(document, options.prevState || null);
if (collection.schema) {
utils.validateSchema(collection.schema, document);
}
if (collection.pk) {
const pkValue = document[collection.pk];
const pkCheckOptions = { [collection.pk]: pkValue };
document.$loki && (pkCheckOptions.$loki = { $ne: document.$loki });
if (pkValue && typeof pkValue !== 'string' && typeof pkValue !== 'number') {
const msg = `Primary key for "${collection.pk}" must be a string or a number`;
throw new errors.WorkError(msg, 'ERR_METASTOCLE_INVALID_DOCUMENT_PK_TYPE');
}
else if (pkValue && ((options.pks && options.pks[pkValue] &&
(!document.$loki || (options.pks[pkValue].$loki != document.$loki))) ||
(!options.pks && this.col[fullName].chain().find(pkCheckOptions).count()))
) {
const msg = `Primary key "${pkValue}" for "${collection.pk}" already exists`;
throw new errors.WorkError(msg, 'ERR_METASTOCLE_DOCUMENT_PK_EXISTS');
}
else if (!pkValue) {
document[collection.pk] = this.createDocumentPrimaryKey(document);
}
}
if (!document.$loki && this.col[fullName].chain().find({ [collection.duplicationKey]: document[collection.duplicationKey] }).count()) {
const msg = `The duplicate key "${document[collection.duplicationKey]}" already exists`;
throw new errors.WorkError(msg, 'ERR_METASTOCLE_DOCUMENT_DUPLICATE_EXISTS');
}
return document;
}
/**
* @see DatabaseMetastocle.prototype.getDocumentByPk
*/
async getDocumentByPk(name, value) {
const fullName = this.createCollectionName(name);
const collection = await this.node.getCollection(name);
const document = this.col[fullName].by(collection.pk, value);
return document ? await this.prepareDocumentToGet(document) : null;
}
/**
* @see DatabaseMetastocle.prototype.getDocuments
*/
async getDocuments(name) {
const fullName = this.createCollectionName(name);
const documents = this.col[fullName].find();
for (let i = 0; i < documents.length; i++) {
documents[i] = await this.prepareDocumentToGet(documents[i]);
}
return documents;
}
/**
* @see DatabaseMetastocle.prototype.accessDocument
*/
async accessDocument(document) {
const fullName = this.createCollectionName(document.$collection);
document.$accessedAt = Date.now();
this.col[fullName].update(document);
return await this.prepareDocumentToGet(document);
}
/**
* @see DatabaseMetastocle.prototype.documents
*/
async accessDocuments(name, documents) {
for (let i = 0; i < documents.length; i++) {
documents[i] = await this.prepareDocumentToGet(await this.accessDocument(documents[i]));
}
return documents;
}
/**
* @see DatabaseMetastocle.prototype.updateDocument
*/
async updateDocument(document, options = {}) {
options = Object.assign({}, options);
const fullName = this.createCollectionName(document.$collection);
document.$updatedAt = document.$accessedAt = Date.now();
options.prevState = this.col[fullName].get(document.$loki);
document = await this.handleDocument(document, options);
this.col[fullName].update(document);
return await this.prepareDocumentToGet(document);
}
/**
* @see DatabaseMetastocle.prototype.updateDocuments
*/
async updateDocuments(name, documents) {
const fullName = this.createCollectionName(name);
const collection = await this.node.getCollection(name);
const pks = {};
if (collection.pk) {
const docs = this.col[fullName].find();
docs.forEach(d => pks[d[collection.pk]] = d);
}
for (let i = 0; i < documents.length; i++) {
let prevPkValue;
if (collection.pk) {
prevPkValue = documents[i][collection.pk];
}
documents[i] = await this.updateDocument(documents[i], { pks });
if (collection.pk) {
delete pks[prevPkValue];
pks[documents[i][collection.pk]] = documents[i];
}
documents[i] = await this.prepareDocumentToGet(documents[i]);
}
return documents;
}
/**
* @see DatabaseMetastocle.prototype.deleteDocument
*/
async deleteDocument(document) {
const fullName = this.createCollectionName(document.$collection);
this.col[fullName].remove(document);
return document;
}
/**
* @see DatabaseMetastocle.prototype.deleteDocuments
*/
async deleteDocuments(name, documents) {
for (let i = 0; i < documents.length; i++) {
documents[i] = await this.deleteDocument(documents[i]);
}
}
};
};