Persistent database for Node.js/NW.js/Electron with MongoDB/Mongoose-like features and interface on top of LevelUp
Persistent database for Node.js/NW.js/Electron with MongoDB/Mongoose-like features and interface on top of LevelUp
LinvoDB is a Node.js/NW.js/Electron persistent DB with MongoDB / Mongoose-like features and interface.
LinvoDB is based on NeDB, the most significant core change is that it uses LevelUP as a back-end, meaning it doesn't have to keep the whole dataset in memory. LinvoDB also can do a query entirely by indexes, meaning it doesn't have to scan the full database on a query.
In general:
Install:
npm install linvodb3 level-js # For NW.js, using level-js
npm install linvodb3 leveldown # For pure node.js, using LevelDB
Initialize:
var LinvoDB = require("linvodb3");
// The following two lines are very important
// Initialize the default store to level-js - which is a JS-only store which will work without recompiling in NW.js / Electron
LinvoDB.defaults.store = { db: require("level-js") }; // Comment out to use LevelDB instead of level-js
// Set dbPath - this should be done explicitly and will be the dir where each model's store is saved
LinvoDB.dbPath = process.cwd();
var Doc = new LinvoDB("doc", { /* schema, can be empty */ })
Initialization, detailed:
var LinvoDB = require("linvodb3");
var modelName = "doc";
var schema = { }; // Non-strict always, can be left empty
var options = { };
// options.filename = "./test.db"; // Path to database - not necessary
// options.store = { db: require("level-js") }; // Options passed to LevelUP constructor
var Doc = new LinvoDB(modelName, schema, options); // New model; Doc is the constructor
LinvoDB.dbPath // default path where data files are stored for each model
LinvoDB.defaults // default options for every model
The native types are String, Number, Boolean, Date and null. You can also use
arrays and subdocuments (objects). If a field is undefined, it will not be saved.
If the document does not contain an _id field, one will be automatically generated (a 16-characters alphanumerical string). The _id of a document, once set, cannot be modified.
…
Use find to look for multiple documents matching you query, or findOne to look for one specific document. You can select documents based on field equality or use comparison operators ($lt, $lte, $gt, $gte, $in, $nin, $ne, $regex, $exists). You can also use logical operators $or, $and and $not. See below for the syntax.
…
The syntax is { field: { $op: value } } where $op is any comparison operator:
$lt, $lte: less than, less than or equal$gt, $gte: greater than, greater than or equal$in: member of. value must be an array of values$ne, $nin: not equal, not a member of$exists: checks whether the document posses the property field. value should be true or false$regex: checks whether a string is matched by the regular expression. Contrary to MongoDB, the use of $options with $regex is not supported, because it doesn't give you more power than regex flags. Basic queries are more readable so only use the $regex operator when you need to use another operator with it (see example below)…
When a field in a document is an array the query is treated as a query on every element and there is a match if at least one element matches.
…
You can combine queries using logical operators:
$or and $and, the syntax is { $op: [query1, query2, ...] }.$not, the syntax is { $not: query }Planet.find({ $or: [{ planet: 'Earth' }, { planet: 'Mars' }] }, function (err, docs) {
// docs contains Earth and Mars
});
Planet.find({ $not: { planet: 'Earth' } }, function (err, docs) {
// docs contains Mars, Jupiter, Omicron Persei 8
});
// You can mix normal queries, comparison queries and logical operators
Planet.find({ $or: [{ planet: 'Earth' }, { planet: 'Mars' }], inhabited: true }, function (err, docs) {
// docs contains Earth
});
If you don't specify a callback to find, findOne or count, a Cursor object is returned. You can modify the cursor with sort, skip and limit and then execute it with exec(callback).
…
You can use count to count documents. It has the same syntax as find. For example:
// Count all planets in the solar system
Planet.count({ system: 'solar' }, function (err, count) {
// count equals to 3
});
// Count all documents via cursor
Planet.find({}).count(function (err, count) {
// count equals to 4
});
Besides the standard pagination and sorting Cursor methods, we have the filter, map and reduce modifiers.
Before seeing the examples, you should know that you can combine any of these modifiers in any order/way and all will be executed. For example, you can run a regular query with .find and then run a reduce on it.
No matter how you combine those modifiers, the order of execution is: query, filter, sort, limit/skip, map, reduce, aggregate.
The basic syntax is:
Cursor.map(function(val){ return val })
Cursor.reduce(function reducer(a,b), initial);
Cursor.filter(function(val) { return true /* or false*/ }); // truthy / falsy values accepted
Cursor.aggregate(function(res) { /* do something to the result of the query right before serving */ return res })
…
Once you have a Cursor object, returned by calling find without a callback, you can turn it into a live query, meaning the .res property will always be up-to-date results from the query. Of course, all modifiers, such as limit, skip, sort, map, reduce, filter and aggregate will still apply.
An event will be emitted when the result is updated - liveQueryUpdate on the model itself.
Seriously consider if live queries can be utilized in your application - if you need particular results continuously, using live queries is extremely efficient, since you don't have to re-read the database but results are kept up-to-date as you update the documents.
…
If you plan to use Live Queries with AngularJS and update scope on the liveQueryUpdated event please be careful. First, I recommend using $digest when possible instead of $apply (dirty-check only the current scope). Second, I recommend debouncing the event before running the $scope.$apply() event to avoid $apply being called many times because of heavy DB use at a moment.
doc.save() - you can use save on a document instance to re-save it, therefore updating it.
// Let's use the same example collection as in the "finding document" part
// { _id: 'id1', planet: 'Mars', system: 'solar', inhabited: false }
// { _id: 'id2', planet: 'Earth', system: 'solar', inhabited: true }
// { _id: 'id3', planet: 'Jupiter', system: 'solar', inhabited: false }
// { _id: 'id4', planet: 'Omicron Persia 8', system: 'futurama', inhabited: true }
Planet.findOne({ planet: 'Earth' }, function(err, doc) {
doc.inhabited = false;
doc.save(function(err) { /* we have updated the Earth doc */ });
});
Doc.update(query, update, options, callback) will update all documents matching query according to the update rules:
query is the same kind of finding query you use with find and findOneupdate specifies how the documents should be modified. It is either a new document or a set of modifiers (you cannot use both together, it doesn't make sense!)$set to change a field's value, $unset to delete a field and $inc to increment a field's value. To work on arrays, you have $push, $pop, $addToSet, $pull, and the special $each. See examples below for the syntax.options is an object with two possible parametersmulti (defaults to false) which allows the modification of several documents if set to trueupsert (defaults to false) if you want to insert a new document corresponding to the update rules if your query doesn't match anything. If your update is a simple object with no modifiers, it is the inserted document. In the other case, the query is stripped from all operator recursively, and the update is applied to it.callback (optional) signature: err, numReplaced, newDocnumReplaced is the number of documents replacednewDoc is the created document if the upsert mode was chosen and a document was insertedNote: you can't change a document's _id.
…
// if you have the document instance at hand, you can just
Doc.findOne({ planet: 'Mars' }, function(err, doc) {
doc.remove(function() {
// done
});
});
Doc.remove(query, options, callback) will remove all documents matching query according to options
query is the same as the ones used for finding and updatingoptions only one option for now: multi which allows the removal of multiple documents if set to true. Default is falsecallback is optional, signature: err, numRemoved…
…
You can define a schema for a model, allowing you to enforce certain properties to types (String, Number, Date), set defaults and also define properties with getter/setter. Since schema support is implemented deep in LinvoDB, you can query on fields which are getter/setter-based and rely that types/defaults are always going to be enforced.
NOTE: when constructing a model with a schema, please specify options object after the schema, otherwise schema will be treated as options: new LinvoDB(name, schema, options)
Schemas are defined as an object of specs for each property. The spec can have properties:
type - the type to be enforced, can be String, Number, Date along with "string", "number", "date" alternative syntax. Can also be a RegExp instance in case you want to validate against that expressioNo open issues yet, or sync has not completed.