UNPKG

forerunnerdb

Version:

A NoSQL document store database for browsers and Node.js.

2,105 lines (1,691 loc) 84.2 kB
# ForerunnerDB - A NoSQL JSON Document DB ForerunnerDB is developed by [Irrelon Software Limited](http://www.irrelon.com/), a UK registered company. > ForerunnerDB is used in live projects that serve millions of users a day, is production ready and battle tested in real-world applications. ### Standout Features Include: * Views - Virtual collections that are built from existing collections and limited by live queries. * Joins - Query with joins across multiple collections. * Collection Groups - Add collections to a group and operate CRUD on them as a single entity. * Data Binding - (Browser Only) Bind data to your DOM and have it update your page in realtime as data changes. * Persistent Storage - (Browser & Node.js) Save your data and load it back at a later time, great for multi-page apps. * Compression & Encryption - Support for compressing and encrypting your persisted data. ## Version 1.3.357 [![npm version](https://badge.fury.io/js/forerunnerdb.svg)](https://www.npmjs.com/package/forerunnerdb) #### TravisCI Build Test Status <table> <tr> <th>Master</th> <th>Dev</th> </tr> <tr> <td><img src="https://travis-ci.org/Irrelon/ForerunnerDB.svg?branch=master" title="Master Branch Build Status" /></td> <td><img src="https://travis-ci.org/Irrelon/ForerunnerDB.svg?branch=dev" title="Dev Branch Build Status" /></td> </tr> </table> ## What is ForerunnerDB ForerunnerDB is a NoSQL JavaScript JSON database with a query language based on MongoDB (with some differences) and runs on browsers and Node.js. It is in use in many large production web applications and is transparently used by over 6 million clients. ForerunnerDB is the most advanced, battle-tested and production ready browser-based JSON database system available today. ## What is ForerunnerDB's Primary Use Case? ForerunnerDB was created primarily to allow web application developers to easily store, query and manipulate JSON data in the browser via a simple query language. It provides the ability to store data on the front-end and query it throughout your application making handling JSON data client-side significantly easier. It is designed to compliment a server-side database / API to allow your client to easily handle data and CRUD it in a fast and efficient way. While it can run on the server in Node.js, ForerunnerDB is NOT designed as a replacement or substitute for MongoDB on the server-side. It's use in server-side applications is geared more towards having a rich query language to query an in-memory store for your application. ForerunnerDB supports data persistence on both the client (via LocalForage) and in Node.js (by saving and loading JSON data files). If you build advanced web applications with AngularJS or perhaps your own framework or if you are looking to build a server application / API that needs a fast queryable in-memory store with file-based data persistence and a very easy setup ( simple installation via NPM and no requirements except Node.js) you will also find ForerunnerDB very useful. ## Demo You can see an interactive demo at [http://www.forerunnerdb.com/demo.html](http://www.forerunnerdb.com/demo.html) ## Tutorials [Tutorial 1: A Simple Todo List](http://www.forerunnerdb.com/tutorial/todoList.html) [Tutorial 2: Simple Chart](http://www.forerunnerdb.com/tutorial/simpleChart.html) ## Download If you are using Node.js (or have it installed) you can use NPM to download ForerunnerDB via: ``` npm install forerunnerdb ``` This will also work for browser-based development, however if you prefer a more traditional download, please click [here](https://github.com/irrelon/ForerunnerDB/archive/master.zip). ## License Please see licensing page for latest information: [http://www.forerunnerdb.com/licensing.html](http://www.forerunnerdb.com/licensing.html) ## Browser Compatibility ForerunnerDB works in all modern browsers (IE8+) * Android Browser 4 * Blackberry 7 * Chrome 23 * Chrome for Android 32 * Firefox 18 * Firefox for Android 25 * Firefox OS 1.0 * IE 8 * IE Mobile 10 * Opera 15 * Opera Mobile 11 * Phonegap/Apache Cordova 1.2.0 * Safari 4 (includes Mobile Safari) ## Use ForerunnerDB in Browser Include the fdb-all.min.js file in your HTML (change path to the location you put forerunner): <script src="./js/dist/fdb-all.min.js" type="text/javascript"></script> ## Distribution Files The DB comes with a few different files in the ./js/dist folder that are pre-built to help you use ForerunnerDB easily. * fdb-all - Contains the whole of ForerunnerDB * Collection - CRUD on collections (tables) * CollectionGroup - Create groups of collections that can be CRUD on as one entity * View - Virtual queried view of a collection (or other view) * HighChart - Highcharts module to create dynamic charts from view data * Persist - Persistent storage module for loading and saving in browser * Document - Single document with CRUD * Overview - Live aggregation of collection or view data * Grid - Generate and maintain an HTML grid with sort and filter columns from data * fdb-core - Contains only the core functionality * Collection - CRUD on collections (tables) * fdb-core+persist - Core functionality + persistent storage * Collection - CRUD on collections (tables) * Persist - Persistent storage module for loading and saving in browser * fdb-core+views - Core functionality + data views * Collection - CRUD on collections (tables) * View - Virtual queried view of a collection (or other view) * fdb-legacy - An old version of ForerunnerDB that some clients still require. Should not be used! This build will be removed in ForerunnerDB 2.0. The other files in ./js/dist are builds for various plugins that are part of the ForerunnerDB project but are entirely optional separate files that can be included in your project and added after the main ForerunnerDB dist file has been loaded. * fdb-angular - Adds data-binding to an angular scope back to ForerunnerDB * fdb-autobind - Adds data-binding for vanilla js projects to ForerunnerDB * fdb-infinilist - Adds the ability to create infinitely scrolling lists of huge amounts of data while only rendering the visible entities in the DOM for responsive UI even on a mobile device ### Chrome Extension: ForerunnerDB Explorer A chrome browser extension exists in the source repo as well as in the Chrome Web Store [available here](https://chrome.google.com/webstore/detail/forerunnerdb-explorer/gkgnafoehgghdeimbkaeeodnhbegfldm). You can inspect and explore your ForerunnerDB instance directly from Chrome's Dev Tools. 1. [Install the extension](https://chrome.google.com/webstore/detail/forerunnerdb-explorer/gkgnafoehgghdeimbkaeeodnhbegfldm) 2. Open Chrome's developer tools 3. Navigate to a url using ForerunnerDB (either local or remote) 4. Click the ForerunnerDB tab in dev tools to inspect instances 5. Click the Refresh button (the one in the ForerunnerDB explorer tab) to see any changes reflected ## Use ForerunnerDB in Node.js After installing via npm (see above) you can require ForerunnerDB in your code: var ForerunnerDB = require('forerunnerdb'); var fdb = new ForerunnerDB(); ## Create a Database var db = fdb.db('myDatabaseName'); > If you do not specify a database name a randomly generated one is provided instead. ## Collections (Tables) > Data Binding: Enabled To create or get a reference to a collection object call (where collectionName is the name of your collection): var collection = db.collection('collectionName'); In our examples we will use a collection called "item" which will store some fictitious items for sale: var itemCollection = db.collection('item'); ### Auto-Creation When you request a collection that does not yet exist it is automatically created. If it already exists you are given the reference to the existing collection. If you want ForerunnerDB to throw an error if a collection is requested that does not already exist you can pass an option to the *collection()* method instead: var collection = db.collection('collectionName', {autoCreate: false}); ### Specifying a Primary Key Up-Front On requesting a collection you can specify a primary key that the collection should be using. For instance to use a property called "name" as the primary key field: var collection = db.collection('collectionName', {primaryKey: 'name'}); You can also read or specify a primary key after instantiation via the primaryKey() method. ### Other Patterns The *collection()* method accepts a number of argument patterns including passing the primary key as a string in the second argument e.g. var collection = db.collection('collectionName', 'name'); > While this will work for legacy reasons it is best to use the options object as shown above to specify autoCreate and primaryKey option values as using an options object is considered the "standard". ## Setting Initial Data When you get a collection instance for the first time it will contain no data. To set data on the collection pass an array of objects to the setData() method: itemCollection.setData([{ _id: 1, name: 'Cat Litter', price: 200 }, { _id: 2, name: 'Dog Food', price: 100 }]); Setting data on a collection will empty any existing data from the collection. You do not *have* to use setData(). You can simply begin inserting data using the insert() method as soon as you have a collection reference. ## Inserting Documents You can either insert a single document object: itemCollection.insert({ _id: 3, price: 400, name: 'Fish Bones' }); or pass an array of documents: itemCollection.insert([{ _id: 4, price: 267, name:'Scooby Snacks' }, { _id: 5, price: 234, name: 'Chicken Yum Yum' }]); ## Searching the Collection > **PLEASE NOTE** While we have tried to remain as close to MongoDB's query language as possible, small differences are present in the query matching logic. The main difference is described here: [Find behaves differently from MongoDB](https://github.com/Irrelon/ForerunnerDB/issues/43) > See the *Special Considerations* section for details about how names of keys / properties in a query object can affect a query's operation. Much like MongoDB, searching for data in a collection is done using the find() method, which supports many of the same operators starting with a $ that MongoDB supports. For instance, finding documents in the collection where the price is greater than 90 but less than 150, would look like this: itemCollection.find({ price: { '$gt': 90, '$lt': 150 } }); And would return an array with all matching documents. If no documents match your search, an empty array is returned. ### Regular Expressions Searches support regular expressions for advanced text-based queries. Simply pass the regular expression object as the value for the key you wish to search, just like when using regular expressions with MongoDB. ### Query Operators ForerunnerDB supports many of the same query operators that MongoDB does, and adds some that are not available in MongoDB but which can help in browser-centric applications. * [$gt](#gt) Greater Than * [$gte](#gte) Greater Than / Equal To * [$lt](#lt) Less Than * [$lte](#lte) Less Than / Equal To * [$ne](#ne) Not Equal To (!=) * [$nee](#nee) Strict Not Equal To (!==) * [$in](#in) Match Any Value In An Array Of Values * [$nin](#nin) Match Any Value Not In An Array Of Values * [$distinct](#distinct) Match By Distinct Key/Value Pairs * [$count](#count) Match By Length Of Sub-Document Array * [$or](#or) Match any of the conditions inside the sub-query * [$and](#and) Match all conditions inside the sub-query * [$exists](#exists) Check that a key exists in the document * [$elemMatch](#elemMatch) Limit sub-array documents by query * [$elemsMatch](#elemsMatch) Multiple document version of $elemMatch #### $gt Selects those documents where the value of the field is greater than (i.e. >) the specified value. { field: {$gt: value} } ##### Usage var fdb = new ForerunnerDB(), db = fdb.db('test'), coll = db.collection('test'); coll.setData([{ _id: 1, val: 1 }, { _id: 2, val: 2 }, { _id: 3, val: 3 }]); result = coll.find({ val: { $gt: 1 } }); Result is: [{ _id: 2, val: 2 }, { _id: 3, val: 3 }] #### $gte Selects the documents where the value of the field is greater than or equal to (i.e. >=) the specified value. { field: {$gte: value} } ##### Usage var fdb = new ForerunnerDB(), db = fdb.db('test'), coll = db.collection('test'); coll.setData([{ _id: 1, val: 1 }, { _id: 2, val: 2 }, { _id: 3, val: 3 }]); result = coll.find({ val: { $gte: 1 } }); Result is: [{ _id: 1, val: 1 }, { _id: 2, val: 2 }, { _id: 3, val: 3 }] #### $lt Selects the documents where the value of the field is less than (i.e. <) the specified value. { field: { $lt: value} } ##### Usage var fdb = new ForerunnerDB(), db = fdb.db('test'), coll = db.collection('test'); coll.setData([{ _id: 1, val: 1 }, { _id: 2, val: 2 }, { _id: 3, val: 3 }]); result = coll.find({ val: { $lt: 2 } }); Result is: [{ _id: 1, val: 1 }] #### $lte Selects the documents where the value of the field is less than or equal to (i.e. <=) the specified value. { field: { $lte: value} } ##### Usage var fdb = new ForerunnerDB(), db = fdb.db('test'), coll = db.collection('test'); coll.setData([{ _id: 1, val: 1 }, { _id: 2, val: 2 }, { _id: 3, val: 3 }]); result = coll.find({ val: { $lte: 2 } }); Result is: [{ _id: 1, val: 1 }, { _id: 2, val: 2 }] #### $ne Selects the documents where the value of the field is not equal (i.e. !=) to the specified value. This includes documents that do not contain the field. {field: {$ne: value} } ##### Usage var fdb = new ForerunnerDB(), db = fdb.db('test'), coll = db.collection('test'); coll.setData([{ _id: 1, val: 1 }, { _id: 2, val: 2 }, { _id: 3, val: 3 }]); result = coll.find({ val: { $ne: 2 } }); Result is: [{ _id: 1, val: 1 }, { _id: 3, val: 3 }] #### $nee Selects the documents where the value of the field is not equal equal (i.e. !==) to the specified value. This allows for strict equality checks for instance zero will not be seen as false because 0 !== false and comparing a string with a number of the same value will also return false e.g. ('2' != 2) is false but ('2' !== 2) is true. This includes documents that do not contain the field. {field: {$nee: value} } ##### Usage var fdb = new ForerunnerDB(), db = fdb.db('test'), coll = db.collection('test'); coll.setData([{ _id: 1, val: 1 }, { _id: 2, val: 2 }, { _id: 3, val: 3 }]); result = coll.find({ val: { $nee: 2 } }); Result is: [{ _id: 1, val: 1 }, { _id: 3, val: 3 }] #### $in Selects documents where the value of a field equals any value in the specified array. { field: { $in: [<value1>, <value2>, ... <valueN> ] } } ##### Usage var fdb = new ForerunnerDB(), db = fdb.db('test'), coll = db.collection('test'); coll.setData([{ _id: 1, val: 1 }, { _id: 2, val: 2 }, { _id: 3, val: 3 }]); result = coll.find({ val: { $in: [1, 3] } }); Result is: [{ _id: 1, val: 1 }, { _id: 3, val: 3 }] #### $nin Selects documents where the value of a field does not equal any value in the specified array. { field: { $nin: [ <value1>, <value2> ... <valueN> ]} } ##### Usage var fdb = new ForerunnerDB(), db = fdb.db('test'), coll = db.collection('test'); coll.setData([{ _id: 1, val: 1 }, { _id: 2, val: 2 }, { _id: 3, val: 3 }]); result = coll.find({ val: { $nin: [1, 3] } }); Result is: [{ _id: 2, val: 2 }] #### $distinct Selects the first document matching a value of the specified field. If any further documents have the same value for the specified field they will not be returned. { $distinct: { field: 1 } } ##### Usage var fdb = new ForerunnerDB(), db = fdb.db('test'), coll = db.collection('test'); coll.setData([{ _id: 1, val: 1 }, { _id: 2, val: 1 }, { _id: 3, val: 1 }, { _id: 4, val: 2 }]); result = coll.find({ $distinct: { val: 1 } }); Result is: [{ _id: 1, val: 1 }, { _id: 4, val: 2 }] #### $count > Version >= 1.3.326 Selects documents based on the length (count) of items in an array inside a document. { $count: { field: <value> } } ##### Select Documents Where The "arr" Array Field Has Only 1 Item var fdb = new ForerunnerDB(), db = fdb.db('test'), coll = db.collection('test'); coll.setData([{ _id: 1, arr: [] }, { _id: 2, arr: [{ val: 1 }] }, { _id: 3, arr: [{ val: 1 }, { val: 2 }] }]); result = coll.find({ $count: { arr: 1 } }); Result is: [{ _id: 2, arr: [{ val: 1 }] }] ##### Select Documents Where The "arr" Array Field Has More Than 1 Item var fdb = new ForerunnerDB(), db = fdb.db('test'), coll = db.collection('test'); coll.setData([{ _id: 1, arr: [] }, { _id: 2, arr: [{ val: 1 }] }, { _id: 3, arr: [{ val: 1 }, { val: 2 }] }]); result = coll.find({ $count: { arr: { $gt: 1 } } }); Result is: [{ _id: 3, arr: [{ val: 1 }, { val: 2 }] }] #### $or The $or operator performs a logical OR operation on an array of two or more <expressions> and selects the documents that satisfy at least one of the <expressions>. { $or: [ { <expression1> }, { <expression2> }, ... , { <expressionN> } ] } ##### Usage var fdb = new ForerunnerDB(), db = fdb.db('test'), coll = db.collection('test'); coll.setData([{ _id: 1, val: 1 }, { _id: 2, val: 2 }, { _id: 3, val: 3 }]); result = coll.find({ $or: [{ val: 1 }, { val: { $gte: 3 } }] }); Result is: [{ _id: 1, val: 1 }, { _id: 3, val: 3 }] #### $and Performs a logical AND operation on an array of two or more expressions (e.g. <expression1>, <expression2>, etc.) and selects the documents that satisfy all the expressions in the array. The $and operator uses short-circuit evaluation. If the first expression (e.g. <expression1>) evaluates to false, ForerunnerDB will not evaluate the remaining expressions. { $and: [ { <expression1> }, { <expression2> } , ... , { <expressionN> } ] } ##### Usage var fdb = new ForerunnerDB(), db = fdb.db('test'), coll = db.collection('test'); coll.setData([{ _id: 1, val: 1 }, { _id: 2, val: 2 }, { _id: 3, val: 3 }]); result = coll.find({ $and: [{ _id: 3 }, { val: { $gte: 3 } }] }); Result is: [{ _id: 3, val: 3 }] #### $exists When <boolean> is true, $exists matches the documents that contain the field, including documents where the field value is null. If <boolean> is false, the query returns only the documents that do not contain the field. { field: { $exists: <boolean> } } ##### Usage var fdb = new ForerunnerDB(), db = fdb.db('test'), coll = db.collection('test'); coll.setData([{ _id: 1, val: 1 }, { _id: 2, val: 2, moo: 'hello' }, { _id: 3, val: 3 }]); result = coll.find({ moo: { $exists: true } }); Result is: [{ _id: 2, val: 2, moo: 'hello' }] ### Projection #### $elemMatch The $elemMatch operator limits the contents of an *array* field from the query results to contain only the first element matching the $elemMatch condition. The $elemMatch operator is specified in the *options* object of the find call rather than the query object. [MongoDB $elemMatch Documentation](http://docs.mongodb.org/manual/reference/operator/projection/elemMatch/) ##### Usage var fdb = new ForerunnerDB(), db = fdb.db('test'), coll = db.collection('test'); coll.setData({ names: [{ _id: 1, text: 'Jim' }, { _id: 2, text: 'Bob' }, { _id: 3, text: 'Bob' }, { _id: 4, text: 'Anne' }, { _id: 5, text: 'Simon' }, { _id: 6, text: 'Uber' }] }); result = coll.find({}, { $elemMatch: { names: { text: 'Bob' } } }); Result is: { names: [{ _id: 2, text: 'Bob' }] } Notice that only the FIRST item matching the $elemMatch clause is returned in the names array. If you require multiple matches use the ForerunnerDB-specific $elemsMatch operator instead. #### $elemsMatch The $elemsMatch operator limits the contents of an *array* field from the query results to contain only the elements matching the $elemMatch condition. The $elemsMatch operator is specified in the *options* object of the find call rather than the query object. ##### Usage var fdb = new ForerunnerDB(), db = fdb.db('test'), coll = db.collection('test'); coll.setData({ names: [{ _id: 1, text: 'Jim' }, { _id: 2, text: 'Bob' }, { _id: 3, text: 'Bob' }, { _id: 4, text: 'Anne' }, { _id: 5, text: 'Simon' }, { _id: 6, text: 'Uber' }] }); result = coll.find({}, { $elemsMatch: { names: { text: 'Bob' } } }); Result is: { names: [{ _id: 2, text: 'Bob' }, { _id: 3, text: 'Bob' }] } Notice that all items matching the $elemsMatch clause are returned in the names array. If you require match on ONLY the first item use the MongoDB-compliant $elemMatch operator instead. ### Ordering / Sorting Results You can specify an $orderBy option along with the find call to order/sort your results. This uses the same syntax as MongoDB: itemCollection.find({ price: { '$gt': 90, '$lt': 150 } }, { $orderBy: { price: 1 // Sort ascending or -1 for descending } }); ### Limiting Return Fields You can specify which fields are included in the return data for a query by adding them in the options object. This follows the same rules specified by MongoDB here: [MongoDB Documentation](http://docs.mongodb.org/manual/tutorial/project-fields-from-query-results/) > Please note that the primary key field will always be returned unless explicitly excluded from the results via "_id: 0". #### Usage var fdb = new ForerunnerDB(), db = fdb.db('test'), coll = db.collection('test'); coll.setData([{ _id: 1, text: 'Jim', val: 2131232 }, { _id: 2, text: 'Bob', val: 2425234321 }, { _id: 3, text: 'Bob', val: 54353454 }, { _id: 4, text: 'Anne', val: 1231432 }, { _id: 5, text: 'Simon', val: 87567455 }, { _id: 6, text: 'Uber', val: 93472834 }]); result = coll.find({}, { text: 1 }); Result is: [{ _id: 1, text: 'Jim' }, { _id: 2, text: 'Bob' }, { _id: 3, text: 'Bob' }, { _id: 4, text: 'Anne' }, { _id: 5, text: 'Simon' }, { _id: 6, text: 'Uber' }] ### Pagination / Paging Through Results > Version >= 1.3.55 It is often useful to limit the number of results and then page through the results one page at a time. ForerunnerDB supports an easy pagination system via the $page and $limit query options combination. #### Usage var fdb = new ForerunnerDB(), db = fdb.db('test'), coll = db.collection('test'), data = [], count = 100, result, i; // Generate random data for (i = 0; i < count; i++) { data.push({ _id: String(i), val: i }); } coll.insert(data); // Query the first 10 records (page indexes are zero-based // so the first page is page 0 not page 1) result = coll.find({}, { $page: 0, $limit: 10 }); // Query the next 10 records result = coll.find({}, { $page: 1, $limit: 10 }); ### Skipping Records in a Query > Version >= 1.3.55 You can skip records at the beginning of a query result by providing the $skip query option. This operates in a similar fashion to the MongoDB [skip()](http://docs.mongodb.org/manual/reference/method/cursor.skip/) method. #### Usage var fdb = new ForerunnerDB(), db = fdb.db('test'), coll = db.collection('test').truncate(), data = [], count = 100, result, i; // Generate random data for (i = 0; i < count; i++) { data.push({ _id: String(i), val: i }); } coll.insert(data); result = coll.find({}, { $skip: 50 }); ### Finding and Returning Sub-Documents When you have documents that contain arrays of sub-documents it can be useful to search and extract them. Consider this data structure: var fdb = new ForerunnerDB(), db = fdb.db('test'), coll = db.collection('test').truncate(), result, i; coll.insert({ _id: '1', arr: [{ _id: '332', val: 20, on: true }, { _id: '337', val: 15, on: false }] }); /** * Finds sub-documents from the collection's documents. * @param {Object} match The query object to use when matching parent documents * from which the sub-documents are queried. * @param {String} path The path string used to identify the key in which * sub-documents are stored in parent documents. * @param {Object=} subDocQuery The query to use when matching which sub-documents * to return. * @param {Object=} subDocOptions The options object to use when querying for * sub-documents. * @returns {*} */ result = coll.findSub({ _id: '1' }, 'arr', { on: false }, { //$stats: true, //$split: true }); The result of this query is an array containing the sub-documents that matched the query parameters: [{ _id: '337', val: 15, on: false }] > The result of findSub never returns a parent document's data. The fourth parameter (options object) allows you to specify if you wish to have stats and if you wish to split your results into separate arrays for each matching parent document. ## Updating the Collection This is one of the areas where ForerunnerDB and MongoDB are different. By default ForerunnerDB updates only the keys you specify in your update document, rather than outright *replacing* the matching documents like MongoDB does. In this sense ForerunnerDB behaves more like MySQL. In the call below, the update will find all documents where the price is greater than 90 and less than 150 and then update the documents' key "moo" with the value true. collection.update({ price: { '$gt': 90, '$lt': 150 } }, { moo: true }); If you want to replace a key's value you can use the $overwrite operator described in the *Update Operators* section below. ## Quick Updates You can target individual documents for update by their id (primary key) via a quick helper method: collection.updateById(1, {price: 180}); This will update the document with the _id field of 1 to a new price of 180. ### Update Operators * [$overwrite](#overwrite) * [$each](#each) * [$inc](#inc) * [$push](#push) * [$splicePush](#splicepush) * [$addToSet](#addtoset) * [$pull](#pull) * [$move](#move) * [$cast](#cast) * [Array Positional in Updates (.$)](#array-positional-in-updates) #### $overwrite The $overwrite operator replaces a key's value with the one passed, overwriting it completely. This operates the same way that MongoDB's default update behaviour works without using the $set operator. This operator is most useful when updating an array field to a new type such as an object. By default ForerunnerDB will detect an array and step into the array objects one at a time and apply the update to each object. When you use $overwrite you can replace the array instead of stepping into it. db.collection('test').update({ <query> }, { $overwrite: { <field>: <value> } }); In the following example the "arr" field (initially an array) is replaced by an object: db.collection('test').setData({ _id: "445324", arr: [{ foo: 1 }] }); db.collection('test').update({ _id: "445324" }, { $overwrite: { arr: { moo: 1 } } }); JSON.stringify(db.collection('test').find()); Result: [{ "_id": "445324", "arr": { "moo": 1 } }] #### $each > Version >= 1.3.34 $each allows you to iterate through multiple update operations on the same query result. Use $each when you wish to execute update operations in sequence or on the same query. Using $each is slightly more performant than running each update operation one after the other calling update(). Consider the following sequence of update calls that define a couple of nested arrays and then push a value to the inner-nested array: db.collection('test').setData({ _id: "445324", count: 5 }); db.collection('test').update({ _id: "445324" }, { $cast: { arr: "array", $data: [{}] } }); db.collection('test').update({ _id: "445324" }, { arr: { $cast: { secondArr: "array" } } }); db.collection('test').update({ _id: "445324" }, { arr: { $push: { secondArr: "moo" } } }); JSON.stringify(db.collection('test').find()); Result: [ { "_id": "445324", "count": 5, "arr": [{"secondArr": ["moo"]}] } ] These calls a wasteful because each update() call must query the collection for matching documents before running the update against them. With $each you can pass a sequence of update operations and they will be executed in order: db.collection('test').setData({ _id: "445324", count: 5 }); db.collection('test').update({ _id: "445324" }, { $each: [{ $cast: { arr: "array", $data: [{}] } }, { arr: { $cast: { secondArr: "array" } } }, { arr: { $push: { secondArr: "moo" } } }] }); JSON.stringify(db.collection('test').find()); Result: [ { "_id": "445324", "count": 5, "arr": [{"secondArr": ["moo"]}] } ] As you can see the single sequenced call produces the same output as the multiple update() calls but will run slightly faster and use fewer resources. #### $inc The $inc operator increments / decrements a field value by the given number. db.collection('test').update({ <query> }, { $inc: { <field>: <value> } }); In the following example, the "count" field is decremented by 1 in the document that matches the id "445324": db.collection('test').setData({ _id: "445324", count: 5 }); db.collection('test').update({ _id: "445324" }, { $inc: { count: -1 } }); JSON.stringify(db.collection('test').find()); Result: [{ "_id": "445324", "count": 4 }] Using a positive number will increment, using a negative number will decrement. #### $push The $push operator appends a specified value to an array. db.collection('test').update({ <query> }, { $push: { <field>: <value> } }); The following example appends "Milk" to the "shoppingList" array in the document with the id "23231": db.collection('test').setData({ _id: "23231", shoppingList: [] }); db.collection('test').update({ _id: "23231" }, { $push: { shoppingList: "Milk" } }); JSON.stringify(db.collection('test').find()); Result: [{ "_id": "23231", "shoppingList": [ "Milk" ] }] #### $splicePush The $splicePush operator adds an item into an array at a specified index. db.collection('test').update({ <query> }, { $splicePush: { <field>: <value> $index: <index> } }); The following example inserts "Milk" to the "shoppingList" array at index 1 in the document with the id "23231": db.collection('test').setData({ _id: "23231", shoppingList: [ "Sugar", "Tea", "Coffee" ] }); db.collection('test').update({ _id: "23231" }, { $splicePush: { shoppingList: "Milk", $index: 1 } }); JSON.stringify(db.collection('test').find()); Result: [ { "_id": "23231", "shoppingList": [ "Sugar", "Milk", "Tea", "Coffee" ] } ] #### $addToSet Adds an item into an array only if the item does not already exist in the array. ForerunnerDB supports the $addToSet operator as detailed in the MongoDB documentation. Unlike MongoDB, ForerunnerDB also allows you to specify a matching field / path to check uniqueness against by using the $key property. In the following example $addToSet is used to check uniqueness against the whole document being added: // Create a collection document db.collection('test').setData({ _id: "1", arr: [] }); // Update the document by adding an object to the "arr" array db.collection('test').update({ _id: "1" }, { $addToSet: { arr: { name: 'Fufu', test: '1' } } }); // Try and do it again... this will fail because a // matching item already exists in the array db.collection('test').update({ _id: "1" }, { $addToSet: { arr: { name: 'Fufu', test: '1' } } }); Now in the example below we specify which key to test uniqueness against: // Create a collection document db.collection('test').setData({ _id: "1", arr: [] }); // Update the document by adding an object to the "arr" array db.collection('test').update({ _id: "1" }, { $addToSet: { arr: { name: 'Fufu', test: '1' } } }); // Try and do it again... this will work because the // key "test" is different for the existing and new objects db.collection('test').update({ _id: "1" }, { $addToSet: { arr: { $key: 'test', name: 'Fufu', test: '2' } } }); You can also specify the key to check uniqueness against as an object path such as 'moo.foo'. #### $pull The $pull operator removes a specified value or values that match an input query. db.collection('test').update({ <query> }, { $pull: { <arrayField>: <value|query> } }); The following example removes the "Milk" entry from the "shoppingList" array: db.users.update({ _id: "23231" }, { $pull: { shoppingList: "Milk" } }); If an array element is an embedded document (JavaScript object), the $pull operator applies its specified query to the element as though it were a top-level object. #### $move The $move operator moves an item that exists inside a document's array from one index to another. db.collection('test').update({ <query> }, { $move: { <arrayField>: <value|query>, $index: <index> } }); The following example moves "Milk" in the "shoppingList" array to index 1 in the document with the id "23231": db.users.update({ _id: "23231" }, { $move: { shoppingList: "Milk" $index: 1 } }); #### $cast > Version >= 1.3.34 The $cast operator allows you to change a property's type within a document. If used to cast a property to an array or object the property is set to a new blank array or object respectively. This example changes the type of the "val" property from a string to a number: db.collection('test').setData({ val: "1.2" }); db.collection('test').update({}, { $cast: { val: "number" } }); JSON.stringify(db.collection('test').find()); Result: [{ "_id": "1d6fbf16e080de0", "val": 1.2 }] You can also use cast to ensure that an array or object exists on a property without overwriting that property if one already exists: db.collection('test').setData({ _id: "moo", arr: [{ test: true }] }); db.collection('test').update({ _id: "moo" }, { $cast: { arr: "array" } }); JSON.stringify(db.collection('test').find()); Result: [{ "_id": "moo", "arr": [{ "test": true }] }] Should you wish to initialise an array or object with specific data if the property is not currently of that type rather than initialising as a blank array / object, you can specify the data to use by including a $data property in your $cast operator object: db.collection('test').setData({ _id: "moo" }); db.collection('test').update({ _id: "moo" }, { $cast: { orders: "array", $data: [{ initial: true }] } }); JSON.stringify(db.collection('test').find()); Result: [{ "_id": "moo", "orders":[{ "initial": true }] }] #### Array Positional in Updates (.$) Often you want to update a sub-document stored inside an array. You can use the array positional operator to tell ForerunnerDB that you wish to update a sub-document that matches your query clause. The following example updates the sub-document in the array *"arr"* with the _id *"foo"* so that the *"name"* property is set to *"John"*: db.collection('test').setData({ _id: '2', arr: [{ _id: 'foo', name: 'Jim' }] }); var result = db.collection('test').update({ _id: '2', "arr": { "_id": "foo" } }, { "arr.$": { name: 'John' } }); Internally this operation checks the update for property's ending in ".$" and then looks at the query part of the call to see if a corresponding clause exists for it. In the example above the "arr.$" property in the update part has a corresponding "arr" in the query part which determines which sub-documents are to be updated based on if they match or not. ## Get Data Item By Reference JavaScript objects are passed around as references to the same object. By default when you query ForerunnerDB it will "decouple" the results from the internal objects stored in the collection. If you would prefer to get the reference instead of decoupled object you can specify this in the query options like so: var result = db.collection('item').find({}, { $decouple: false }); If you do not specify a decouple option, ForerunnerDB will default to true and return decoupled objects. Keep in mind that if you switch off decoupling for a query and then modify any object returned, it will also modify the internal object held in ForerunnerDB, which could result in incorrect index data as well as other anomalies. ## Primary Keys If your data uses different primary key fields from the default "_id" then you need to tell the collection. Simply call the primaryKey() method with the name of the field your primary key is stored in: collection.primaryKey('itemId'); When you change the primary key field name, methods like updateById will use this field automatically instead of the default one "_id". ## Removing Documents Removing is as simple as doing a normal find() call, but with the search for docs you want to remove. Remove all documents where the price is greater than or equal to 100: collection.remove({ price: { '$gte': 100 } }); ### Joins Sometimes you want to join two or more collections when running a query and return a single document with all the data you need from those multiple collections. ForerunnerDB supports collection joins via a simple options key "$join". For instance, let's setup a second collection called "purchase" in which we will store some details about users who have ordered items from the "item" collection we initialised above: var fdb = new ForerunnerDB(), db = fdb.db('test'), itemCollection = db.collection('item'), purchaseCollection = db.collection('purchase'); itemCollection.insert([{ _id: 1, name: 'Cat Litter', price: 200 }, { _id: 2, name: 'Dog Food', price: 100 }, { _id: 3, price: 400, name: 'Fish Bones' }, { _id: 4, price: 267, name:'Scooby Snacks' }, { _id: 5, price: 234, name: 'Chicken Yum Yum' }]); purchaseCollection.insert([{ itemId: 4, user: 'Fred Bloggs', quantity: 2 }, { itemId: 4, user: 'Jim Jones', quantity: 1 }]); Now, when we find data from the "item" collection we can grab all the users that ordered that item as well and store them in a key called "purchasedBy": itemCollection.find({}, { '$join': [{ 'purchase': { 'itemId': '_id', '$as': 'purchasedBy', '$require': false, '$multi': true } }] }); The "$join" key holds an array of joins to perform, each join object has a key which denotes the collection name to pull data from, then matching criteria which in this case is to match purchase.itemId with the item._id. The three other keys are special operations (start with $) and indicate: * $as tells the join what object key to store the join results in when returning the document * $require is a boolean that denotes if the join must be successful for the item to be returned in the final find result * $multi indicates if we should match just one item and then return, or match multiple items as an array The result of the call above is: [{ "_id":1, "name":"Cat Litter", "price":200, "purchasedBy":[] },{ "_id":2, "name":"Dog Food", "price":100, "purchasedBy":[] },{ "_id":3, "price":400, "name":"Fish Bones", "purchasedBy":[] },{ "_id":4, "price":267, "name":"Scooby Snacks", "purchasedBy": [{ "itemId":4, "user":"Fred Bloggs", "quantity":2 }, { "itemId":4, "user":"Jim Jones", "quantity":1 }] },{ "_id":5, "price":234, "name":"Chicken Yum Yum", "purchasedBy":[] }] ## Triggers > Version >= 1.3.12 ForerunnerDB currently supports triggers for inserts and updates at both the *before* and *after* operation phases. Triggers that fire on the *before* phase can also optionally modify the operation data and actually cancel the operation entirely allowing you to provide database-level data validation etc. Setting up triggers is very easy. ### Example 1: Cancel Operation Before Insert Trigger Here is an example of a *before insert* trigger that will cancel the insert operation before the data is inserted into the database: var fdb = new ForerunnerDB(), db = fdb.db('test'), collection = db.collection('test'); collection.addTrigger('myTrigger', db.TYPE_INSERT, db.PHASE_BEFORE, function (operation, oldData, newData) { // By returning false inside a "before" trigger we cancel the operation return false; }); collection.insert({test: true}); The trigger method passed to addTrigger() as parameter 4 should handle these arguments: |Argument|Data Type|Description| |--------------|---------|-----------------------------------------------------| |operation|object|Details about the operation being executed. In *before update* operations this also includes *query* and *update* objects which you can modify directly to alter the final update applied.| |oldData|object|The data before the operation is executed. In insert triggers this is always a blank object. In update triggers this will represent what the document that *will* be updated currently looks like. You cannot modify this object.| |newData|object|The data after the operation is executed. In insert triggers this is the new document being inserted. In update triggers this is what the document being updated *will* look like after the operation is run against it. You can update this object ONLY in *before* phase triggers.| ### Example 2: Modify a Document Before Update In this example we insert a document into the collection and then update it afterwards. When the update operation is run the *before update* trigger is fired and the document is modified before the update is applied. This allows you to make changes to an operation before the operation is carried out. var fdb = new ForerunnerDB(), db = fdb.db('test'), collection = db.collection('test'); collection.addTrigger('myTrigger', db.TYPE_UPDATE, db.PHASE_BEFORE, function (operation, oldData, newData) { newData.updated = String(new Date()); }); // Insert a document with the property "test" being true collection.insert({test: true}); // Now update that document to set "test" to false - this // will fire the trigger code registered above and cause the // final document to have a new property "updated" which // contains the date/time that the update occurred on that // document collection.update({test: true}, {test: false}); // Now inspect the document and it will show the "updated" // property that the trigger added! console.log(collection.find()); > Please keep in mind that you can only modify a document's data during a *before* phase trigger. Modifications to the document during an *after* phase trigger will simply be ignored and will not be applied to the document. This applies to insert and update trigger types. Remove triggers cannot modify the document at any time. ### Enabling / Disabling Triggers > Version >= 1.3.31 #### Enabling a Trigger You can enable a previously disabled trigger or multiple triggers using the enableTrigger() method on a collection. > If you specify a type or type and phase and do not specify an ID the method will affect all triggers that match the type / phase. ##### Enable a Trigger via Trigger ID db.collection('test').enableTrigger('myTriggerId'); ##### Enable a Trigger via Type db.collection('test').enableTrigger(db.TYPE_INSERT); ##### Enable a Trigger via Type and Phase db.collection('test').enableTrigger(db.TYPE_INSERT, db.PHASE_BEFORE); ##### Enable a Trigger via ID, Type and Phase db.collection('test').enableTrigger('myTriggerId', db.TYPE_INSERT, db.PHASE_BEFORE); #### Disabling a Trigger You can temporarily disable a trigger or multiple triggers using the disableTrigger() method on a collection. > If you specify a type or type and phase and do not specify an ID the method will affect all triggers that match the type / phase. ##### Disable a Trigger via Trigger ID db.collection('test').disableTrigger('myTriggerId'); ##### Disable a Trigger via Type db.collection('test').disableTrigger(db.TYPE_INSERT); ##### Disable a Trigger via Type and Phase db.collection('test').disableTrigger(db.TYPE_INSERT, db.PHASE_BEFORE); ##### Disable a Trigger via ID, Type and Phase db.collection('test').disableTrigger('myTriggerId', db.TYPE_INSERT, db.PHASE_BEFORE); ## Indices & Performance ForerunnerDB currently supports basic indexing for performance enhancements when querying a collection. You can create an index on a collection using the ensureIndex() method. ForerunnerDB will utilise the index that most closely matches the query you are executing. In the case where a query matches multiple indexes the most relevant index is automatically determined. Let's setup some data to index: var fdb = new ForerunnerDB(), db = fdb.db('test'), names = ['Jim', 'Bob', 'Bill', 'Max', 'Jane', 'Kim', 'Sally', 'Sam'], collection = db.collection('test'), tempName, tempAge, i; for (i = 0; i < 100000; i++) { tempName = names[Math.ceil(Math.random() * names.length) - 1]; tempAge = Math.ceil(Math.random() * 100); collection.insert({ name: tempName, age: tempAge }); } You can see that in our collection we have some random names and some random ages. If we ask Forerunner to explain the query plan for querying the name and age fields: collection.explain({ name: 'Bill', age: 17 }); The result shows that the largest amount of time was taken in the "tableScan" step: { analysis: Object, flag: Object, index: Object, log: Array[0], operation: "find", results: 128, // Will vary depending on your random entries inserted earlier steps: Array[4] // Lists the steps Forerunner took to generate the results [0]: Object name: "analyseQuery", totalMs: 0 [1]: Object name: "checkIndexes", totalMs: 0 [2]: Object name: "tableScan", totalMs: 54 [3]: Object name: "decouple", totalMs: 1, time: Object } From the explain output we can see that a large amount of time was taken up doing a table scan. This means that the database had to scan through every item in the collection and determine if it matched the query you passed. Let's speed this up by creating an index on the "name" field so that lookups against that field are very fast. In the index below we are indexing against the "name" field in ascending order, which is what the 1 denotes in name: 1. If we wish to index in descending order we would use name: -1 instead. collection.ensureIndex({ name: 1 }); The collection now contains an ascending index against the name field. Queries that check against the name field will now be optimised: collection.explain({ name: 'Bill', age: 17 }); Now the explain output has some different results: { analysis: Object, flag: Object, index: Object, log: Array[0], operation: "find", results: 128, // Will vary depending on your random entries inserted earlier steps: Array[6] // Lists the steps Forerunner took to generate the results [0]: Object name: "analyseQuery", totalMs: 1 [1]: Object name: "checkIndexes", totalMs: 1 [2]: Object name: "checkIndexMatch: name:1", totalMs: 0 [3]: Object name: "indexLookup", totalMs: 0, [4]: Object name: "tableScan", totalMs: 13, [5]: Object name: "decouple", totalMs: 1, time: Object } The query plan shows that the index was used because it has an "indexLookup" step, however we still have a "tableScan" step that took 13 milliseconds to execute. Why was this? If we delve into the query plan a little more by expanding the analysis object we can see why: { analysis: Object hasJoin: false, indexMatch: Array[1] [0]: Object index: Index, keyData: Object matchedKeyCount: 1, totalKeyCount: 2, matchedKeys: Object age: false, name: true lookup: Array[12353] joinQueries: Object, options: Object, queriesJoin: false, queriesOn: Array[1], query: Object flag: Object, index: Object, log: Array[0], operation: "find", results: 128, // Will vary depending on your random entries inserted earlier steps: Array[6] // Lists the steps Forerunner took to generate the results time: Object } In the selected index to use (indexMatch[0]) the keyData shows that the index only matched 1 out of the 2 query keys. In the case of the index and query above, Forerunner's process will be: * Query the index for all