UNPKG

tedb

Version:

TypeScript Embedded Database

909 lines 36.1 kB
"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