tedb
Version:
TypeScript Embedded Database
909 lines • 36.1 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
/**
* Created by tsturzl on 4/11/17.
*/
const indices_1 = require("./indices");
const index_1 = require("./index");
const updateOperators_1 = require("./updateOperators");
const utils_1 = require("./utils");
const tedb_utils_1 = require("tedb-utils");
/**
* Datastore class
*
* Example:
* ~~~
* const UserStorage = new yourStorageClass("users");
* const Users = new Datastore({storage: UserStorage});
* ~~~
* Creates a new Datastore using a specified storageDriver
*/
class Datastore {
/**
* @param config - config object `{storage: IStorageDriver}`
*/
constructor(config) {
this.storage = config.storage;
this.generateId = true;
this.indices = new Map();
}
/**
* Insert a single document and insert any indices of document into
* its respective binary tree.
*
* ~~~
* Users.insert({name: "xyz", age: 30})
* .then((doc) => console.log(doc)) // {_id: "...", name: "xyz", age: 30}
* .catch((e) => console.log(e));
* ~~~
*
* @param doc - document to insert
* @returns {Promise<any>}
*/
insert(doc) {
return new Promise((resolve, reject) => {
if (tedb_utils_1.isEmpty(doc)) {
return reject(new Error("Cannot insert empty document"));
}
// doc._id = this.createId();
if (!doc.hasOwnProperty("_id")) {
doc._id = this.createId();
}
else if (doc._id.length !== 64) {
doc._id = this.createId();
}
const indexPromises = [];
this.indices.forEach((v) => {
indexPromises.push(v.insert(doc));
});
Promise.all(indexPromises)
.then(() => {
return this.storage.setItem(doc._id, doc);
})
.then(resolve)
.catch(reject);
});
}
/**
* Find documents
*
* Examples:
* ~~~
* Users.find()
* .sort({age: -1})
* .skip(1)
* .limit(10)
* .exec()
* .then((users) => console.log(users))
* .catch((e) => console.log(e));
*
* Users.find({$or: [{name: "a"}, {name: "b"}]})
* .then((docs) => console.log(docs.length)) // 2 if unique
* .catch((e) => console.log(e));
*
* return Users.find({age: {$gt: 0, $lte: 27, $ne: 5}});
*
* // sort from doc creation date. Auto generated by the _id
* Users.find({})
* .sort({$created_at: -1}) // descending
* .exec()
* .then(resolve)
* .catch(reject);
* ~~~
* @param query
* @returns {Cursor}
*/
find(query = {}) {
return new index_1.Cursor(this, query);
}
/**
* Count documents
* @param query
*/
count(query = {}) {
return new index_1.Cursor(this, query, true);
}
/**
* Update document/s
*
* Examples: -> lets say two users {name: "bob", age: 1},
* {name: "slydel", age: 45, companies: {name: "Initech", alternate: "Intertrode"}}
* ~~~
* Users.update({name: "bob"},{$rename: {name: "first"}, $set: {job: "consultant"}},
* {returnUpdatedDocs: true})
* .then((docs) => console.log(docs[0]) // {_id: "...", first: "bob", age: 1, job: "consultant"}
* .catch();
*
* Users.update({first: "bob"},{$unset: {"companies.alternate": ""}, $inc: {age: -44},
* $mul: {age: 5}}, {returnUpdatedDocs: true})
* .then((docs) => console.log(docs[0]) // {_id: "...", name: "bob", age: 5, companies: {name: "Initech"}}
* .catch();
*
* Users.update({name: "Charles", age: 22}, {$inc: {age: 4}}, {upser: true, returnUpdatedDocs: true}})
* .then((docs) => console.log(docs[0]) // {_id: "...", name: "Charles", age: 26}
* .catch();
*
* Users.update({age: {$gt: 0}},{$unset: {age: "", name: ""}},{ multi: true, returnUpdatedDocs: true})
* .then((docs) => console.log(docs)) // {_id: ".."}, {_id: ".."}, {_id: "..", companies: {name: "Initech"}}
* .catch();
* ~~~
* @param query - query document/s to update
* @param operation - update operation, either a new doc or modifies portions of a document(eg. `$set`)
* @param options - { fieldName, unique?, compareKeys?, checkKeyEquality? }
* @returns {Promise<any>}
*/
update(query, operation, options = {}) {
return new Promise((resolve, reject) => {
if (tedb_utils_1.isEmpty(operation)) {
return reject(new Error("No update without update operation"));
}
const promises = [];
const indexPromises = [];
const operators = ["$set", "$mul", "$inc", "$unset", "$rename"];
const multi = options.multi || false;
const upsert = options.upsert || false;
const exactObjectFind = options.exactObjectFind || false;
const returnUpdatedDocs = options.returnUpdatedDocs || false;
const operationKeys = Object.keys(operation);
if (exactObjectFind) {
// compresses all object + nested objects to "dot.notation";
const target = {};
tedb_utils_1.compressObj(query, target);
query = target;
}
if (multi) {
return this.find(query)
.exec()
.then((res) => {
res = res;
if (res.length === 0) {
if (upsert) {
if (exactObjectFind) {
query = tedb_utils_1.expandObj(query);
}
query._id = this.createId();
this.indices.forEach((v) => indexPromises.push(v.insert(query)));
this.updateDocsIndices([query], promises, indexPromises, operation, operators, operationKeys, reject);
}
else {
return [];
}
}
else {
// no return value, all are passed and used by reference.
this.updateDocsIndices(res, promises, indexPromises, operation, operators, operationKeys, reject);
}
// If any index changes - error, reject and do not update and save.
return Promise.all(indexPromises);
})
.then(() => {
return Promise.all(promises);
})
.then((docs) => tedb_utils_1.rmArrObjDups(docs, "_id"))
.then((docs) => {
const docPromises = [];
// save new docs to storage driver.
docs.forEach((doc) => {
docPromises.push(this.storage.setItem(doc._id, doc));
});
return Promise.all(docPromises);
})
.then((res) => {
if (returnUpdatedDocs) {
resolve(res);
}
else {
resolve();
}
})
.catch(reject);
}
else {
return this.find(query)
.limit(1)
.exec()
.then((res) => {
res = res;
if (res.length === 0) {
if (upsert) {
if (exactObjectFind) {
query = tedb_utils_1.expandObj(query);
}
query._id = this.createId();
this.indices.forEach((v) => {
indexPromises.push(v.insert(query));
});
this.updateDocsIndices([query], promises, indexPromises, operation, operators, operationKeys, reject);
}
else {
return [];
}
}
else {
this.updateDocsIndices(res, promises, indexPromises, operation, operators, operationKeys, reject);
}
return Promise.all(indexPromises);
})
.then(() => {
return Promise.all(promises);
})
.then((docs) => {
return tedb_utils_1.rmArrObjDups(docs, "_id");
})
.then((docs) => {
const docPromises = [];
// save new docs to storage driver.
docs.forEach((doc) => {
docPromises.push(this.storage.setItem(doc._id, doc));
});
return Promise.all(docPromises);
})
.then((res) => {
if (returnUpdatedDocs) {
resolve(res);
}
else {
resolve();
}
})
.catch(reject);
}
});
}
// public san(obj: any): Promise<any> {
san(fieldName, index) {
return new Promise((resolve, reject) => {
// const values: any[] = [];
return this.getIndices()
.then((indices) => indices.get(fieldName))
.then((INDEX) => {
const values = [];
// Upgrade here if you ever get an error from a user
// about there being a crash or slow down when they have an
// extremely large index. millions. thousands of levels
INDEX.traverse((ind) => {
// values.push({key: ind.key, value: ind.value});
ind.value.forEach((i) => {
if (i !== undefined && i !== null && i !== false) {
values.push({ key: ind.key, value: i });
}
});
});
return values;
})
.then((values) => {
return Promise.all(values.map((obj) => {
return this.storage.exists(obj, index, fieldName);
}));
})
.then((data) => {
return Promise.all(data.map((d) => {
if (d.doesExist) {
return new Promise((res) => res());
}
else {
if (d.key === null) {
return new Promise((res) => res());
}
else {
return d.index.removeByPair(d.key, d.value);
}
}
}));
})
.then(resolve)
.catch(reject);
});
}
storageSan(fieldName) {
return new Promise((resolve, reject) => {
return this.getIndices()
.then((indices) => indices.get(fieldName))
.then((INDEX) => {
const values = [];
// upgrade here as well
INDEX.traverse((ind) => {
values.push(...ind.value.filter((i) => {
if (i !== undefined && i !== null && i !== false) {
return i;
}
}));
});
return values;
})
.then((values) => {
return this.storage.collectionSanitize(values);
})
.then(resolve)
.catch(reject);
});
}
storageSanitize() {
return new Promise((resolve, reject) => {
if (this.indices.size !== 0) {
const fieldNames = [];
this.indices.forEach((i, fieldName) => {
fieldNames.push(this.storageSan(fieldName));
});
return Promise.all(fieldNames)
.then(resolve)
.catch(reject);
}
else {
// resolve, you have nothing to check against.
return resolve();
}
});
}
/**
* Method used after a remove depending on the want of the user
* to make sure that if an _id exists in the index that it should
* exist in the storage driver saved location. If not then the
* indexed item is removed from the index as to not cause lookup
* errors.
* @returns {Promise<any>}
*/
sanitize() {
return new Promise((resolve, reject) => {
const fieldNames = [];
this.indices.forEach((i, fieldName) => {
fieldNames.push(this.san(fieldName, i));
});
return Promise.all(fieldNames)
.then(resolve)
.catch(reject);
});
}
/**
* Removes document/s by query - uses find method to retrieve ids. multi always
* @param query
* @returns {Promise<number>}
*/
remove(query = {}) {
return new Promise((resolve, reject) => {
const uniqueIds = [];
this.find(query)
.exec()
.then((docs) => {
// Array of promises for index remove
if (this.indices.size !== 0) {
return Promise.all(docs.map((document) => {
return Promise.all(Array.from(this.indices).map(([key, value]) => {
return value.remove(document);
}));
}));
}
else {
return docs;
}
})
.then((docs) => {
docs = tedb_utils_1.flattenArr(docs);
return tedb_utils_1.rmArrObjDups(docs, "_id");
})
.then((docs) => {
docs.forEach((document) => {
if (uniqueIds.indexOf(document._id) === -1) {
uniqueIds.push(document._id);
}
});
return Promise.all(uniqueIds.map((id) => {
if (id && (Object.prototype.toString.call(id) === "[object String]")) {
return this.storage.removeItem(id);
}
else {
return new Promise((res) => res());
}
}));
})
.then(() => {
resolve(uniqueIds.length);
})
.catch(reject);
});
}
/**
* Ensure an index on the datastore
*
* Example:
* ~~~
* return Users.ensureIndex({fieldName: "username", unique: true});
* ~~~
* @param options
* @returns {Promise<null>}
*/
ensureIndex(options) {
return new Promise((resolve, reject) => {
try {
this.indices.set(options.fieldName, new indices_1.default(this, options));
}
catch (e) {
return reject(e);
}
resolve();
});
}
/**
* Remove index will delete the index from the Map which also
* holds the Btree of the indices. If you need to remove the
* index from the persisted version in from the storage driver,
* call the removeIndex from the storage driver from a different source.
* This method should not assume you saved the index.
* @param fieldName - Field that needs index removed
* @returns {Promise<null>}
*/
removeIndex(fieldName) {
return new Promise((resolve, reject) => {
try {
this.indices.delete(fieldName);
}
catch (e) {
return reject(e);
}
resolve();
});
}
/**
* Save the index currently in memory to the persisted version if need be
* through the storage driver.
* @param fieldName
* @returns {Promise<any>}
*/
saveIndex(fieldName) {
return new Promise((resolve, reject) => {
const index = this.indices.get(fieldName);
if (index) {
index.toJSON()
.then((res) => {
return this.storage.storeIndex(fieldName, res);
})
.then(resolve)
.catch(reject);
}
else {
return reject(new Error(`No index with name ${fieldName} for this datastore`));
}
});
}
/**
* Insert a stored index into the index of this datastore
* @param key - the index fieldName
* @param index - the key value pair obj
* @returns {Promise<null>}
*/
insertIndex(key, index) {
return new Promise((resolve, reject) => {
try {
const indices = this.indices.get(key);
if (indices !== undefined) {
indices.insertMany(key, index)
.then(resolve)
.catch((err) => {
return reject(err);
});
}
else {
return reject(new Error("No Index for this key was created on this datastore."));
}
}
catch (e) {
return reject(e);
}
});
}
/**
* Retrieve the indices of this datastore.
*
* Example:
* ~~~
* Users.getIndices()
* .then((indices) => {
* let usernameIndex = indices.get("username");
* if(usernameIndex) {
* return usernameIndex.toJSON(); // a method on index bTree
* }
* }); // no reject, will always resolve
* ~~~
* @returns {Promise<any>}
*/
getIndices() {
return new Promise((resolve, reject) => {
if (this.indices) {
resolve(this.indices);
}
else {
reject(`No indices found in TeDB: ${this.indices}`);
}
});
}
/**
* Get Document by ID/s
* Used internally
* @param options - sort limit skip options
* @param ids - ID or Array of IDs
* @returns {Promise<any>}
*/
getDocs(options, ids) {
return new Promise((resolve, reject) => {
let idsArr = (typeof ids === "string") ? [ids] : ids;
if (options.skip && options.limit) {
idsArr = idsArr.splice(options.skip, options.limit);
}
else if (options.skip && !options.limit) {
idsArr.splice(0, options.skip);
}
else if (options.limit && !options.skip) {
idsArr = idsArr.splice(0, options.limit);
}
return this.createIdsArray(idsArr)
.then(resolve)
.catch(reject);
});
}
/**
* Search for IDs, chooses best strategy. Handles logical operators($or, $and)
* Returns array of IDs
* Used Internally
* @param fieldName - element name or query start $or/$and
* @param value - string,number,date,null - or [{ field: value }, { field: value }]
* @returns {Promise<T>}
*/
search(fieldName, value) {
return new Promise((resolve, reject) => {
if (fieldName === "$or" && value instanceof Array) {
const promises = [];
value.forEach((query) => {
for (const field in query) {
if (typeof field === "string" && query.hasOwnProperty(field)) {
promises.push(this.searchField(field, query[field]));
}
}
});
Promise.all(promises)
.then((idsArr) => tedb_utils_1.flattenArr(idsArr))
.then(resolve)
.catch(reject);
}
else if (fieldName === "$and" && value instanceof Array) {
const promises = [];
value.forEach((query) => {
for (const field in query) {
if (typeof field === "string" && query.hasOwnProperty(field)) {
promises.push(this.searchField(field, query[field]));
}
}
});
Promise.all(promises)
.then((idsArr) => tedb_utils_1.saveArrDups(idsArr))
.then(resolve)
.catch(reject);
}
else if (fieldName !== "$or" && fieldName !== "$and" && fieldName) {
this.searchField(fieldName, value)
.then(resolve)
.catch(reject);
}
else if ((fieldName === undefined) && (value === undefined)) {
this.searchField()
.then(resolve)
.catch(reject);
}
else {
return reject(new Error("Logical operators expect an Array"));
}
});
}
/**
* Get Date from ID ... do we need this on the dataStore?
*
* Example:
* ~~~
* let id = "UE9UQVJWd0JBQUE9cmZ4Y2MxVzNlOFk9TXV4dmJ0WU5JUFk9d0FkMW1oSHY2SWs9"; // an ID
* Users.getIdDate(id); // date object -> 2017-05-26T17:14:48.252Z
* ~~~
* @param id - the `_id` of the document to get date of
* @returns {Date}
*/
getIdDate(id) {
return utils_1.getDate(id);
}
/**
* Search for IDs, chooses best strategy, preferring to search indices if they exist for the given field.
* Returns array of IDs
* @param fieldName
* @param value
* @returns {Promise<BTT.SNDBSA>}
*/
searchField(fieldName, value) {
if (fieldName && (value !== undefined)) {
return this.indices.has(fieldName) ?
this.searchIndices(fieldName, value) :
this.searchCollection(fieldName, value);
}
else {
return this.searchCollection();
}
}
/**
* Search indices by field
* Example 1: dbName.searchIndices("fieldName", "1234");
* will return the value id of that key as an array of one element.
* Example 2: dbName.searchIndices("fieldName", { $gte: 1, $lte 10, $ne: 3 }
* will return an array of ids from the given range.
* Returns array of IDs
* @param fieldName - field to search
* @param value - value to search by
* @returns {Promise<BTT.SNDBSA>}
*/
searchIndices(fieldName, value) {
return new Promise((resolve, reject) => {
const index = this.indices.get(fieldName);
if (!index) {
return resolve(undefined);
}
if (typeof value === "object") {
resolve(index.searchRange(value));
}
else {
resolve(index.search(value));
}
});
}
/**
* Search collection by field, essentially a collection scan
* Returns array of IDs
* @param fieldName - field to search
* @param value - value to search by
* @returns {Promise<T>}
*/
searchCollection(fieldName, value) {
return new Promise((resolve, reject) => {
const ids = [];
if (fieldName && (value !== undefined)) {
const queryObj = {};
let lt;
let lte;
let gt;
let gte;
let ne;
if (value !== null && Object.prototype.toString.call(value) === "[object Object]") {
lt = (value.hasOwnProperty("$lt") && value.$lt !== undefined) ? value.$lt : null;
lte = (value.hasOwnProperty("$lte") && value.$lte !== undefined) ? value.$lte : null;
gt = (value.hasOwnProperty("$gt") && value.$gt !== undefined) ? value.$gt : null;
gte = (value.hasOwnProperty("$gte") && value.$gte !== undefined) ? value.$gte : null;
ne = value.hasOwnProperty("$ne") ? value.$ne : null;
}
else {
lt = null;
lte = null;
gt = null;
gte = null;
ne = null;
}
const queryArray = [{ name: "lt", value: lt },
{ name: "lte", value: lte },
{ name: "gt", value: gt },
{ name: "gte", value: gte },
{ name: "ne", value: ne }];
queryArray.forEach((q) => {
if (q.value !== null) {
queryObj[q.name] = { value: q.value, flag: false };
}
});
const allTrue = (obj) => {
for (const o in obj) {
if (obj.hasOwnProperty(o)) {
if (!obj[o].flag) {
return false;
}
}
}
return true;
};
this.storage.iterate((v, k) => {
const field = tedb_utils_1.getObjValue(v, fieldName);
if (field !== undefined) {
if (lt === null && lte === null && gt === null &&
gte === null && ne === null && (k === value || field === value)) {
ids.push(k);
}
else {
let flag;
if (Object.prototype.toString.call(value) === "[object Object]") {
for (const item in queryObj) {
if (queryObj.hasOwnProperty(item)) {
switch (item) {
case "gt":
queryObj[item].flag = ((field > gt) && (gt !== null));
break;
case "gte":
queryObj[item].flag = ((field >= gte) && (gte !== null));
break;
case "lt":
queryObj[item].flag = ((field < lt) && (lt !== null));
break;
case "lte":
queryObj[item].flag = ((field <= lte) && (lte !== null));
break;
case "ne":
queryObj[item].flag = ((field !== ne) && (ne !== null));
break;
}
}
}
flag = allTrue(queryObj);
}
else {
flag = (((field < lt) && (lt !== null)) ||
((field <= lte) && (lte !== null)) ||
((field > gt) && (gt !== null)) ||
((field >= gte) && (gte !== null)) ||
((field !== ne) && (ne !== null)) ||
(field === value));
}
if (flag) {
ids.push(k);
}
}
}
})
.then(() => {
resolve(ids);
})
.catch((e) => {
reject(e);
});
}
else {
// here return keys from storage driver
this.storage.keys()
.then(resolve)
.catch(reject);
}
});
}
/**
* Actual method to update the documents and associated indices
* @param docs - an array of documents to be updated
* @param promises - reference to promise array to be resolved later
* @param indexPromises - reference to promise array to be resolved later
* @param operation - operation query from update method
* @param operators - array of operators passed by reference from the update method
* @param operationKeys - Each key from the query. could be less than operators array length
* @param reject - passed reference to reject from update method.
*/
updateDocsIndices(docs, promises, indexPromises, operation, operators, operationKeys, reject) {
docs.forEach((doc) => {
let mathed;
let preMath;
// update indices
this.indices.forEach((index, field) => {
operationKeys.forEach((k) => {
if (operationKeys.indexOf(k) !== -1) {
const setKeys = Object.keys(operation[k]);
switch (k) {
case "$set":
setKeys.forEach((sk) => {
if (field === sk) {
// update the index value with the new
// value if the index fieldname =
// the $set obj key.
indexPromises.push(index.updateKey(tedb_utils_1.getObjValue(doc, field), operation[k][sk]));
}
});
break;
case "$mul":
setKeys.forEach((mk) => {
if (field === mk) {
if (mathed) {
preMath = mathed;
mathed = mathed * operation[k][mk];
}
else {
mathed = tedb_utils_1.getObjValue(doc, field) * operation[k][mk];
}
const indexed = preMath ? preMath : tedb_utils_1.getObjValue(doc, field);
indexPromises.push(index.updateKey(indexed, mathed));
}
});
break;
case "$inc":
setKeys.forEach((ik) => {
if (field === ik) {
if (mathed) {
preMath = mathed;
mathed = mathed + operation[k][ik];
}
else {
mathed = tedb_utils_1.getObjValue(doc, field) + operation[k][ik];
}
const indexed = preMath ? preMath : tedb_utils_1.getObjValue(doc, field);
indexPromises.push(index.updateKey(indexed, mathed));
}
});
break;
case "$unset":
setKeys.forEach((ik) => {
if (field === ik) {
// To unset a field that is indexed
// remove the document. The index remove
// method has this ref and knows the value
indexPromises.push(index.remove(doc));
}
});
break;
case "$rename":
setKeys.forEach((rn) => {
if (field === rn) {
// save current index value
const indexValue = this.indices.get(field);
// delete current index
this.removeIndex(field)
.then(() => {
// create new index with old value new name
if (indexValue) {
this.indices.set(operation[k][rn], indexValue);
}
else {
return reject(new Error(`Cannot rename index of ${field} that does not exist`));
}
})
.catch((e) => reject(e));
}
});
break;
}
}
});
});
// Update Docs
operationKeys.forEach((k) => {
if (operators.indexOf(k) !== -1) {
switch (k) {
case "$set":
promises.push(updateOperators_1.$set(doc, operation[k]));
break;
case "$mul":
promises.push(updateOperators_1.$mul(doc, operation[k]));
break;
case "$inc":
promises.push(updateOperators_1.$inc(doc, operation[k]));
break;
case "$unset":
promises.push(updateOperators_1.$unset(doc, operation[k]));
break;
case "$rename":
promises.push(updateOperators_1.$rename(doc, operation[k]));
break;
}
}
});
});
}
/**
* Return fill provided promise array with promises from the storage driver
* @param promises
* @param ids
*/
createIdsArray(ids) {
return new Promise((resolve, reject) => {
return Promise.all(ids.map((id) => {
return this.storage.getItem(id);
}))
.then((docs) => {
docs = docs.filter((doc) => !tedb_utils_1.isEmpty(doc));
resolve(docs);
})
.catch(reject);
});
}
/**
* Create Unique ID that contains timestamp
* @returns {string}
*/
createId() {
return utils_1.getUUID();
}
}
exports.default = Datastore;
//# sourceMappingURL=datastore.js.map