/*! MonguitoDB v0.0.1 | Thu May 14 2015 00:54:07 | (c) Juan Cuartas | MIT license */
(function (exports) {
/**
* Provides some utility functions (array manipulators, validators, etc).
*
* @author Juan Cuartas
* @version 0.0.1, Jun 2014
*
* @namespace
* @ignore
*/
var util = util || {};
// MANIPULATING ARRAYS:
// ----------------------------------------------------------------------------
/**
* Filters the array given as argument and returns a reference to the new
* filtered array.
*
* NOTE: passed-in array is not modified, a new copied array is returned.
*
* @param {object[]} array - Array of objects to be filtered.
* @param {?(object|function)} filter - Filter object or function.
* E.g. 1: {status: "Delivered", seller: "Armani"}
* E.g. 2: function (e) { return e.total > 700; }
* @return {object[]}
*/
util.filterArray = function (array, filter) {
if (Object.prototype.toString.call(array) !== "[object Array]") {
throw new TypeError(
"util.filterArray(), invalid arg[0]:array, expecting array."
);
}
var newArray = array.slice(0);
if (typeof filter === "function") {
for (var i = 0; i < newArray.length; i++) {
if (filter(newArray[i]) !== true) {
newArray.splice(i--, 1);
}
}
} else if (typeof filter === "object" && filter !== null) {
var isMatch = function (element) {
for (var property in filter) {
if (element[property] !== filter[property]) {
return false;
}
}
return true;
};
for (var j = 0; j < newArray.length; j++) {
if (!isMatch(newArray[j])) {
newArray.splice(j--, 1);
}
}
} else if (typeof filter !== "undefined") {
throw new TypeError(
"util.filterArray(), invalid arg[1]:filter, " +
"expecting object or function."
);
}
return newArray;
};
/**
* Sorts the array given as argument and returns a reference to the new sorted
* array.
*
* NOTE: passed-in array is not modified, a new copied array is returned.
*
* @param {object[]} array - Array of objects to be sortered.
* @param {string} sort - Sort expression.
* E.g. 1: "seller".
* E.g. 2: "seller, total".
* E.g. 3: "seller ASC, total DESC".
* @return {object[]}
*/
util.sortArray = function (array, sort) {
if (Object.prototype.toString.call(array) !== "[object Array]") {
throw new TypeError(
"util.sortArray(), invalid arg[0]:array, expecting array."
);
}
if (typeof sort !== "string") {
throw new TypeError(
"util.sortArray(), invalid arg[1]:sort, expecting string."
);
}
var newArray = array.slice(0);
sort = sort.replace(/ +(?= )/g, "").split(",");
newArray.sort(function (obj1, obj2) {
for (var i = 0; i < sort.length; i++) {
var tokens = sort[i].trim().split(" ");
var property = tokens[0];
var direction = "ASC";
if (tokens.length > 1) {
direction = tokens[1].toUpperCase();
}
if (direction === "DESC") {
if (obj1[property] > obj2[property]) {
return -1;
} else if (obj1[property] < obj2[property]) {
return 1;
}
} else {
if (obj1[property] < obj2[property]) {
return -1;
} else if (obj1[property] > obj2[property]) {
return 1;
}
}
}
return 0;
});
return newArray;
};
/**
* Gets the first element in the given array, null if the array is empty.
*
* @param {object[]} array - Array to get its first element.
* @return {object|null}
*/
util.firstInArray = function (array) {
if (Object.prototype.toString.call(array) !== "[object Array]") {
throw new TypeError(
"util.firstInArray(), invalid arg[0]:array, expecting array."
);
}
if (array.length) {
return array[0];
} else {
return null;
}
};
/**
* Gets the last element in the given array, null if the array is empty.
*
* @param {object[]} array - Array to get its last element.
* @return {object|null}
*/
util.lastInArray = function (array) {
if (Object.prototype.toString.call(array) !== "[object Array]") {
throw new TypeError(
"util.lastInArray(), invalid arg[0]:array, expecting array."
);
}
if (array.length) {
return array[array.length - 1];
} else {
return null;
}
};
// VALIDATORS:
// ----------------------------------------------------------------------------
/**
* Indicates if the given argument is a valid natural number {0, 1, 2, ...}.
*
* @param {number} num - Number to validate.
* @return {boolean}
*/
util.isValidNaturalNumber = function (num) {
return (typeof num === "number") && (num % 1 === 0) && (num >= 0);
};
/**
* Indicates if the given argument is a valid JavaScript variable name.
*
* @param {string} variableName - Variable to validate.
* @return {boolean}
*/
util.isValidVariableName = function (variableName) {
return typeof variableName === "string" &&
/^[$A-Z_][0-9A-Z_$]*$/i.test(variableName);
};
/**
* Indicates if the given argument is a valid Universally Unique Identifier,
* according to the RFC4122 V4 specification.
*
* @see http://en.wikipedia.org/wiki/Universally_unique_identifier
* @param {string} uuid - Identifier to validate.
* @return {boolean}
*/
util.isValidUUID = function (uuid) {
if (typeof uuid !== "string" || uuid.length !== 36) {
return false;
}
uuid = uuid.toUpperCase();
var validateDigit = function (digit, index) {
if ([8, 13, 18, 23].indexOf(index) >= 0) {
return digit === "-";
} else if (index === 14) {
return digit === "4";
} else if (index === 19) {
return ["8", "9", "A", "B"].indexOf(digit) >= 0;
} else {
return ["0", "1", "2", "3", "4", "5", "6", "7",
"8", "9", "A", "B", "C", "D", "E", "F"].indexOf(digit) >= 0;
}
};
for (var i = 0; i < uuid.length; i++) {
if (!validateDigit(uuid[i], i)) {
return false;
}
}
return true;
};
/**
* Indicates if the given argument is a valid document _id (a document _id is
* valid when it is a Natural Number or an UUID).
*
* @param {number} documentId - Document _id.
* @return {boolean}
*/
util.isValidDocumentId = function (documentId) {
return util.isValidNaturalNumber(documentId) ||
util.isValidUUID(documentId);
};
/**
* Indicates if the given argument implements the Storage Interface defined by
* the W3C.
*
* @see http://www.w3.org/TR/webstorage/#the-storage-interface
* @param {object} storage - Object to validate.
* @return {boolean}
*/
util.isValidStorage = function (storage) {
var key = "1E7B9A3B-9D53-469F-BF4E-D056A3BE403C";
var value = "3220B380-2E7A-49BD-9A51-44D6408ED989";
try {
storage.removeItem(key);
var length = storage.length;
if (!util.isValidNaturalNumber(length)) {
return false;
}
storage.setItem(key, value);
if (storage.getItem(key) !== value) {
return false;
}
if (storage.length !== (length + 1)) {
return false;
}
storage.removeItem(key);
if (storage.getItem(key) !== null) {
return false;
}
if (storage.length !== length) {
return false;
}
if (typeof storage.key !== "function") {
return false;
}
if (typeof storage.clear !== "function") {
return false;
}
} catch (err) {
return false;
}
return true;
};
/**
* Validates that args.length === expected. If args.length !== expected,
* an exception will be thrown.
*
* @param {object[]} args - The arguments to validate.
* @param {number} expected - The expected lenght.
* @param {string} path - Path where the validation was done.
*/
util.validateArguments = function (args, expected, path) {
if (args.length !== expected) {
var msg = null;
switch (expected) {
case 0:
msg = "arguments are not allowed.";
break;
case 1:
msg = "expecting 1 arg, " + args.length + " received.";
break;
default:
msg = "expecting " + expected + " args, " + args.length +
" received.";
break;
}
throw new Error(path + ", " + msg);
}
};
/**
* Validates that arg is a valid string. If arg is null or is not an string,
* an exception will be thrown.
*
* @param {object} arg - The argument to validate.
* @param {string} name - The argument name.
* @param {string} path - Path where the validation was done.
*/
util.validateString = function (arg, name, path) {
if (arg === null) {
throw new TypeError(
path + ", " + name + " can't be null."
);
}
if (typeof arg !== "string") {
throw new TypeError(
path + ", invalid " + name + ", expecting string."
);
}
};
/**
* Validates that arg is a valid object. If arg is null or is not an object,
* an exception will be thrown.
*
* @param {object} arg - The argument to validate.
* @param {string} name - The argument name.
* @param {string} path - Path where the validation was done.
*/
util.validateObject = function (arg, name, path) {
if (arg === null) {
throw new TypeError(
path + ", " + name + " can't be null."
);
}
if (Object.prototype.toString.call(arg) !== "[object Object]") {
throw new TypeError(
path + ", invalid " + name + ", expecting object."
);
}
};
/**
* Validates that arg is a valid object or function. If arg is null or is not
* an object or a function, an exception will be thrown.
*
* @param {object} arg - The argument to validate.
* @param {string} name - The argument name.
* @param {string} path - Path where the validation was done.
*/
util.validateObjectOrFunction = function (arg, name, path) {
if (arg === null) {
throw new TypeError(
path + ", " + name + " can't be null."
);
}
if (typeof arg !== "function" &&
Object.prototype.toString.call(arg) !== "[object Object]") {
throw new TypeError(
path + ", invalid " + name + ", expecting object or function."
);
}
};
/**
* Validates that arg is a valid document _id (a document _id is
* valid when it is a Natural Number or an UUID.). If arg is null or is not
* a valid document _id, an exception will be thrown.
*
* @param {object} arg - The argument to validate.
* @param {string} name - The argument name.
* @param {string} path - Path where the validation was done.
*/
util.validateDocumentId = function (arg, name, path) {
if (arg === null) {
throw new TypeError(
path + ", " + name + " can't be null."
);
}
if (!util.isValidDocumentId(arg)) {
throw new TypeError(
path + ", invalid arg[0]:documentId, expecting number or UUID."
);
}
};
// OTHER UTILITIES:
// ----------------------------------------------------------------------------
/**
* Gets a "pretty" JSON representation of the given object.
*
* @param {object} obj - Object to get its "pretty" JSON representation.
* @return {string}
*/
util.prettyJSON = function (obj) {
switch (typeof obj) {
case "string":
return obj;
case "undefined":
return "undefined";
default:
return JSON.stringify(obj, null, "\t");
}
};
/**
* Generates an Universally Unique Identifier, according to the RFC4122 V4
* specification.
*
* @see http://en.wikipedia.org/wiki/Universally_unique_identifier
* @return {string}
*/
util.generateUUID = function () {
var format = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx";
var replaceableChars = /[xy]/g;
var randomDigit = function (digitType) {
var random = Math.random() * 16 | 0;
if (digitType === "y") {
random = random & 0x3 | 0x8;
}
return random.toString(16).toUpperCase();
};
return format.replace(replaceableChars, randomDigit);
};
/**
* Custom implementation of the Storage Interface defined by the W3C. This
* implementation stores key/value pairs in memory.
*
* @see http://www.w3.org/TR/webstorage/#the-storage-interface
* @author Juan Cuartas
* @version 0.0.1, Jun 2014
*
* @constructor
* @ignore
*/
function MemoryStorage() {
// PRIVATE ATTRIBUTES:
// ------------------------------------------------------------------------
/**
* Stores the list of key/value pairs.
*
* @private
*/
var _items = {};
// PUBLIC ATTRIBUTES:
// ------------------------------------------------------------------------
/**
* Number of key/value pairs currently stored.
*
* @public
*/
this.length = 0;
// PUBLIC METHODS:
// ------------------------------------------------------------------------
/**
* Gets the key associated with the given index.
*
* @param {number} index - Item index (starting from zero).
* @return {string|null}
* @public
*/
this.key = function (index) {
var counter = 0;
for (var key in _items) {
if (counter++ === index) {
return key;
}
}
return null;
};
/**
* Gets the value associated with the given key.
*
* @param {string} key - Item key.
* @return {string|null}
* @public
*/
this.getItem = function (key) {
return _items[key + ""] || null;
};
/**
* Sets the value associated with the given key.
*
* @param {string} key - Item key.
* @param {string} value - Item value.
* @public
*/
this.setItem = function (key, value) {
key = key + "";
value = value + "";
var oldValue = this.getItem(key);
if (oldValue !== value) {
_items[key] = value;
if (oldValue === null) {
this.length++;
}
}
};
/**
* Removes an item from the storage.
*
* @param {string} key - Item key.
* @public
*/
this.removeItem = function (key) {
key = key + "";
var oldValue = this.getItem(key);
if (oldValue !== null) {
delete _items[key];
this.length--;
}
};
/**
* Removes all items from the storage.
*
* @public
*/
this.clear = function () {
if (this.length) {
_items = {};
this.length = 0;
}
};
/**
* Returns a string that represents the current object.
*
* @return {string}
* @public
*/
this.toString = function () {
return "[object Storage]";
};
}
/**
* A document belongs to a {@link Collection}. It is a JSON structure composed
* by key-value pairs. It can be thought as a "record" belonging to a
* table in the relational world.
*
* The following actions can be performed on a document: update, remove,
* pretty.
*
* Documents are retrieved through methods in the {@link Collection} class,
* so, don't initialize documents by yourself!
*
* @example
* var db = new MonguitoDB(localStorage, "orders");
*
* // You get a reference to a Document when you insert one.
* var order = db.orders.insert({recipient: "Juan", total: 50});
*
* // You can get a Document with get() if you know its _id.
* var order = db.orders.get(1);
*
* // Individual elements retrieved from find() are of type Document.
* var order = db.orders.find({recipient: "Juan"})[0];
*
* // By using findOne you can get the first Document retrieved from a query.
* var order = db.orders.findOne({recipient: "Juan"});
*
* // You can also get a reference to a Document by using first() or last().
* var order = db.orders.find().first();
*
* @author Juan Cuartas
* @version 0.0.1, Jun 2014
*
* @constructor
*/
function Document(doc, storage, getConfig, getKey) {
// PRIVATE METHODS:
// ------------------------------------------------------------------------
/**
* Adds functions to manipulate the passed-in doc, specifically:
* update, remove, pretty.
*
* @private
*/
var _addBehaviour = function () {
if (doc) {
doc.update = _update;
doc.remove = _remove;
doc.pretty = _pretty;
}
return doc;
};
// PUBLIC METHODS:
// ------------------------------------------------------------------------
/**
* Updates the document in the storage.
*
* NOTE: _id property can't be modified.
*
* @example
* var db = new MonguitoDB(localStorage, "orders");
* var order = db.orders.get(1);
*
* // Update with arguments.
* order.update({status: "Delivered"});
*
* // Update without arguments.
* order.status = "Delivered";
* order.update();
*
* @param {?object} obj - The modifications to apply (_id is omitted).
* @return {Document}
* @alias Document#update
* @public
*/
var _update = function (obj) {
var path = "MonguitoDB.Document.update()";
if (obj !== undefined) {
util.validateArguments(arguments, 1, path);
util.validateObject(obj, "arg[0]:obj", path);
for (var property in obj) {
if (property !== "_id") {
doc[property] = obj[property];
}
}
}
var config = getConfig();
var index = config.ids.indexOf(doc._id);
if (index >= 0) {
var key = getKey(doc._id);
var json = JSON.stringify(doc);
storage.setItem(key, json);
return doc;
} else {
throw new Error(
path + ", _id:" + doc._id + " doesn't exist."
);
}
};
/**
* Removes the document from the storage.
*
* NOTE: Once you call remove() on a document, you can't perform remove()
* or update() on the document anymore, because an exception will be thrown.
*
* @example
* var db = new MonguitoDB(localStorage, "orders");
* var order = db.orders.get(1);
*
* order.remove();
*
* @alias Document#remove
* @public
*/
var _remove = function () {
var path = "MonguitoDB.Document.remove()";
util.validateArguments(arguments, 0, path);
var config = getConfig();
var index = config.ids.indexOf(doc._id);
if (index >= 0) {
storage.removeItem(getKey(doc._id));
config.ids.splice(index, 1);
config.save();
} else {
throw new Error(
path + ", _id:" + doc._id + " doesn't exist."
);
}
};
/**
* Gets a "pretty" JSON representation of this document.
*
* @example
* var db = new MonguitoDB(localStorage, "orders");
* var order = db.orders.get(1);
*
* console.log(order.pretty());
*
* @alias Document#pretty
* @public
*/
var _pretty = function () {
var path = "MonguitoDB.Document.pretty()";
util.validateArguments(arguments, 0, path);
return util.prettyJSON(doc);
};
return _addBehaviour();
}
/**
* A cursor is a set of documents. It can be manipulated as an array plus the
* following actions: update, remove, get, find, findOne, sort, first, last,
* pretty, count.
*
* Cursors are initialized by {@link Collection}.find(), so, don't initialize
* cursors by yourself!
*
* @example
* var db = new MonguitoDB(localStorage, "orders");
* var cursor = db.orders.find({recipient: "Juan"});
*
* @author Juan Cuartas
* @version 0.0.1, Jun 2014
*
* @constructor
*/
function Cursor(collection) {
// PRIVATE METHODS:
// ------------------------------------------------------------------------
/**
* Adds functions to manipulate the passed-in collection, specifically:
* update, remove, get, find, findOne, sort, first, last, pretty, count.
*
* @private
*/
var _addBehaviour = function () {
if (collection) {
collection.update = _update;
collection.remove = _remove;
collection.get = _get;
collection.find = _find;
collection.findOne = _findOne;
collection.sort = _sort;
collection.first = _first;
collection.last = _last;
collection.pretty = _pretty;
collection.count = _count;
}
return collection;
};
// PUBLIC METHODS:
// ------------------------------------------------------------------------
/**
* Updates all documents in this cursor (changes are applied both in the
* cursor and the storage).
*
* NOTE: _id property can't be modified.
*
* @example
* var db = new MonguitoDB(localStorage, "orders");
* var cursor = db.orders.find({recipient: "Juan"});
*
* // Updates all orders belonging to "Juan" with status "Delivered".
* cursor.update({status: "Delivered"});
*
* @param {?object} obj - The modifications to apply (_id is omitted).
* @return {Cursor}
* @alias Cursor#update
* @public
*/
var _update = function (obj) {
if (obj !== undefined) {
var path = "MonguitoDB.Cursor.update()";
util.validateArguments(arguments, 1, path);
util.validateObject(obj, "arg[0]:obj", path);
}
collection.forEach(function (doc) {
doc.update(obj);
});
return collection;
};
/**
* Removes all documents in this cursor (changes are applied both in the
* cursor and the storage).
*
* NOTE: Once you call remove() on a cursor the cursor will be empty.
*
* @example
* var db = new MonguitoDB(localStorage, "orders");
* var cursor = db.orders.find({recipient: "Juan"});
*
* // Removes all orders belonging to "Juan".
* cursor.remove();
*
* @alias Cursor#remove
* @public
*/
var _remove = function () {
var path = "MonguitoDB.Cursor.remove()";
util.validateArguments(arguments, 0, path);
collection.forEach(function (doc) {
doc.remove();
});
collection.length = 0;
};
/**
* Gets the {@link Document} within this cursor matching the specified _id.
* If there is no matching document within this cursor, it will be returned
* null.
*
* @example
* var db = new MonguitoDB(localStorage, "orders");
* var cursor = db.orders.find({recipient: "Juan"});
*
* // Gets the order belonging to "Juan" with _id = 1.
* var order = cursor.get(1);
*
* @param {number|string} documentId - Document _id.
* @return {Document|null}
* @alias Cursor#get
* @public
*/
var _get = function (documentId) {
var path = "MonguitoDB.Cursor.get()";
util.validateArguments(arguments, 1, path);
util.validateDocumentId(documentId, "arg[0]:documentId", path);
return collection.findOne({_id: documentId});
};
/**
* Retrieves all documents within this cursor that match the specified
* query.
*
* NOTE: This function creates and returns a new cursor (the original one
* won't be modified).
*
* @example
* var db = new MonguitoDB(localStorage, "orders");
* var cursor = db.orders.find({recipient: "Juan"});
*
* // Gets all orders belonging to "Juan" with status "Pending".
* var pending = cursor.find({status: "Pending"});
*
* // Gets all orders belonging to "Juan" with total >= 1000.
* var expensive = cursor.find(function (e) { return e.total >= 1000; });
*
* @param {?(object|function)} query - Specifies selection criteria.
* @return {Cursor}
* @alias Cursor#find
* @public
*/
var _find = function (query) {
if (query !== undefined) {
var path = "MonguitoDB.Cursor.find()";
util.validateArguments(arguments, 1, path);
util.validateObjectOrFunction(query, "arg[0]:query", path);
}
return Cursor(util.filterArray(collection, query));
};
/**
* Returns a {@link Document} within this cursor matching the specified
* query. If multiple documents satisfy the query, it will be returned the
* first document found. If there is no matching document within this
* cursor, it will be returned null.
*
* @example
* var db = new MonguitoDB(localStorage, "orders");
* var cursor = db.orders.find({recipient: "Juan"});
*
* // Gets the order belonging to "Juan" with number "107-1".
* var order = cursor.findOne({number: "107-1"});
*
* // Another way to do the same above.
* var order = cursor.findOne(function (e) { return e.number === "107-1"});
*
* @param {?(object|function)} query - Specifies selection criteria.
* @return {Document|null}
* @alias Cursor#findOne
* @public
*/
var _findOne = function (query) {
if (query !== undefined) {
var path = "MonguitoDB.Cursor.findOne()";
util.validateArguments(arguments, 1, path);
util.validateObjectOrFunction(query, "arg[0]:query", path);
}
return util.firstInArray(
util.filterArray(collection, query)
);
};
/**
* Sorts the current cursor and returns a reference to the new sorted
* cursor.
*
* NOTE: This function creates and returns a new cursor (the original one
* won't be modified).
*
* @example
* var db = new MonguitoDB(localStorage, "orders");
* var cursor = db.orders.find({recipient: "Juan"});
*
* // Sorts the documents by seller (Default sort direction is ascending).
* orders = cursor.sort("seller");
*
* // Sorts the documents by seller and total.
* orders = cursor.sort("seller, total");
*
* // Sorts the documents by seller (ascending) and total (descending).
* orders = cursor.sort("seller ASC, total DESC");
*
* @param {string} sortExpression - Sort expression (You can use ASC or
* DESC to specify sort direction.
* @return {Cursor}
* @alias Cursor#sort
* @public
*/
var _sort = function (sortExpression) {
var path = "MonguitoDB.Cursor.sort()";
util.validateArguments(arguments, 1, path);
util.validateString(sortExpression, "arg[0]:sortExpression", path);
return Cursor(util.sortArray(collection, sortExpression));
};
/**
* Returns the first {@link Document} within this cursor. If the cursor
* is empty, it will be returned null.
*
* @example
* var db = new MonguitoDB(localStorage, "orders");
* var cursor = db.orders.find({recipient: "Juan"});
*
* // Gets the first order belonging to "Juan".
* var order = cursor.first();
*
* @return {Document|null}
* @alias Cursor#first
* @public
*/
var _first = function () {
var path = "MonguitoDB.Cursor.first()";
util.validateArguments(arguments, 0, path);
return util.firstInArray(collection);
};
/**
* Returns the last {@link Document} within this cursor. If the cursor
* is empty, it will be returned null.
*
* @example
* var db = new MonguitoDB(localStorage, "orders");
* var cursor = db.orders.find({recipient: "Juan"});
*
* // Gets the last order belonging to "Juan".
* var order = cursor.last();
*
* @return {Document|null}
* @alias Cursor#last
* @public
*/
var _last = function () {
var path = "MonguitoDB.Cursor.last()";
util.validateArguments(arguments, 0, path);
return util.lastInArray(collection);
};
/**
* Gets a "pretty" JSON representation of this cursor.
*
* @example
* var db = new MonguitoDB(localStorage, "orders");
* var cursor = db.orders.find({recipient: "Juan"});
*
* // Prints all orders belonging to "Juan".
* console.log(cursor.pretty());
*
* @alias Cursor#pretty
* @public
*/
var _pretty = function () {
var path = "MonguitoDB.Cursor.pretty()";
util.validateArguments(arguments, 0, path);
return util.prettyJSON(collection);
};
/**
* Counts the number of documents within this cursor.
*
* @example
* var db = new MonguitoDB(localStorage, "orders");
* var cursor = db.orders.find({recipient: "Juan"});
*
* // Counts the number of orders belonging to "Juan"
* var count = cursor.count();
*
* @return {number}
* @alias Cursor#count
* @public
*/
var _count = function () {
var path = "MonguitoDB.Cursor.count()";
util.validateArguments(arguments, 0, path);
return collection.length;
};
return _addBehaviour();
}
/**
* A collection is a set of documents. It is equivalent to a table in the
* relational world, with the difference that a collection does not enforce
* a schema.
*
* The following actions can be performed on a Collection: insert, update,
* remove, get, find, findOne, count.
*
* Collections are initialized by MonguitoDB() constructor, so, don't
* initialize collections by yourself!
*
* @example
* // The following code shows how to reference collections initialized by
* // MonguitoDB() constructor.
* var db = new MonguitoDB(localStorage, ["orders", "users"]);
*
* var ordersCollection = db.orders;
* var usersCollection = db.users;
*
* @author Juan Cuartas
* @see Document
* @see Cursor
* @version 0.0.1, Jun 2014
*
* @constructor
*/
function Collection(storage, collectionName) {
// PRIVATE METHODS:
// ------------------------------------------------------------------------
/**
* Gets the key used to store a document in this collection.
*
* @param {number} documentId - Document _id.
* @return {string}
* @private
*/
var _getKey = function (documentId) {
if (util.isValidUUID(documentId)) {
return documentId;
} else {
return collectionName + "-" + documentId;
}
};
/**
* Gets the configuration regarding to this collection.
*
* @return {{identity: number, ids: object[]}}
* @private
*/
var _getConfig = function () {
var config = storage.getItem(collectionName);
if (config === null) {
config = {
identity: 1,
ids: []
};
} else {
config = JSON.parse(config);
}
config.save = function () {
var json = JSON.stringify(this);
storage.setItem(collectionName, json);
};
return config;
};
// PUBLIC METHODS:
// ------------------------------------------------------------------------
/**
* Inserts a new object into this collection and returns a reference to
* the inserted {@link Document}.
*
* NOTE: _id property is the document's "primary key" wich is automatically
* assigned. It can be of two types:
* 1) Auto-numeric: when _id is omitted in the passed-in obj.
* 2) UUID: When _id is set to "uuid" in the passed-in obj.
*
* @example
* var db = new MonguitoDB(localStorage, "orders");
*
* // case 1) _id will be automatically assigned as an auto-numeric value.
* var order = db.orders.insert({recipient: "Juan", total: 50});
* var documentId = order._id;
*
* // case 2) _id will be automatically assigned as an UUID.
* var order = db.orders.insert({_id: "uuid", recipient: "Juan", total: 50});
* var documentId = order._id;
*
* @param {object} obj - Document to insert into the collection.
* @return {Document}
* @alias Collection#insert
* @public
*/
var _insert = function (obj) {
var path = "MonguitoDB.Collection.insert()";
util.validateArguments(arguments, 1, path);
util.validateObject(obj, "arg[0]:obj", path);
var config = _getConfig();
if (obj.hasOwnProperty("_id")) {
if (obj._id === "uuid") {
obj._id = util.generateUUID();
} else {
throw new TypeError(
path + ", invalid _id value, only 'uuid' is allowed."
);
}
} else {
obj._id = config.identity++;
}
var key = _getKey(obj._id);
var json = JSON.stringify(obj);
storage.setItem(key, json);
config.ids.push(obj._id);
config.save();
return Document(obj, storage, _getConfig, _getKey);
};
/**
* Gets the {@link Document} that matches the specified _id. If there is
* no matching document within this collection, it will be returned
* null.
*
* The following actions can be performed on the returned document:
* update, remove, pretty.
*
* NOTE: get() is faster than find() and findOne().
*
* @example
* var db = new MonguitoDB(localStorage, "orders");
* var order = db.orders.get(1);
*
* console.log(order.pretty()); // Prints the document.
* order.update({status: "Delivered"}); // Updates the document.
* order.remove(); // Removes the document.
*
* @param {number|string} documentId - Document _id.
* @return {Document|null}
* @alias Collection#get
* @public
*/
var _get = function (documentId) {
var path = "MonguitoDB.Collection.get()";
util.validateArguments(arguments, 1, path);
util.validateDocumentId(documentId, "arg[0]:documentId", path);
var key = _getKey(documentId);
var json = storage.getItem(key);
if (json !== null) {
var obj = JSON.parse(json);
return Document(obj, storage, _getConfig, _getKey);
} else {
return null;
}
};
/**
* Retrieves all documents in this collection matching the specified query.
* If no query is passed-in, all documents will be returned.
*
* This function returns a {@link Cursor} that can be manipulated as an
* array plus the following actions: update, remove, get, find, findOne,
* sort, first, last, pretty, count.
*
* @example
* var db = new MonguitoDB(localStorage, "orders");
* var orders = db.orders.find();
*
* // Each element in the cursor is of type {@link Document}
* orders.forEach(function (order) {
* console.log(order.pretty());
* });
*
* // Applying conditions to retrieve the data.
* orders = db.orders.find({status: "Delivered"});
* orders = db.orders.find({status: "Delivered", seller: "Armani"});
* orders = db.orders.find(function (e) { return e.total > 700; });
*
* // Sorting the data.
* orders = db.orders.find().sort("seller");
* orders = db.orders.find().sort("seller, total");
* orders = db.orders.find().sort("seller ASC, total DESC");
*
* // Executing many actions in cascade.
* var firstOrder = db.orders.find().sort("total").first();
* var lastOrder = db.orders.find().sort("total").last();
*
* // Printing the whole collection.
* console.log(db.orders.find().pretty());
*
* @param {?(object|function)} query - Specifies selection criteria.
* @return {Cursor}
* @alias Collection#find
* @public
*/
var _find = function (query) {
if (query !== undefined) {
var path = "MonguitoDB.Collection.find()";
util.validateArguments(arguments, 1, path);
util.validateObjectOrFunction(query, "arg[0]:query", path);
}
var documents = [];
_getConfig().ids.forEach(function (documentId) {
documents.push(_get(documentId));
});
if (typeof query !== "undefined") {
documents = util.filterArray(documents, query);
}
return Cursor(documents);
};
/**
* Returns a {@link Document} that satisfies the specified query. If
* multiple documents satisfy the query, it will be returned the first
* document found (according to insertion order). If there is
* no matching document within this collection, it will be returned
* null.
*
* The following actions can be performed on the returned document:
* update, remove, pretty.
*
* @example
* var db = new MonguitoDB(localStorage, "orders");
* var order = db.orders.findOne({recipient: "Juan"});
*
* console.log(order.pretty()); // Prints the document.
* order.update({status: "Delivered"}); // Updates the document.
* order.remove(); // Removes the document.
*
* // Using findOne() with a function criteria.
* var order = db.orders.findOne(function (e) { return e.total > 0; });
*
* @param {?(object|function)} query - Specifies selection criteria.
* @return {Document|null}
* @alias Collection#findOne
* @public
*/
var _findOne = function (query) {
if (query !== undefined) {
var path = "MonguitoDB.Collection.findOne()";
util.validateArguments(arguments, 1, path);
util.validateObjectOrFunction(query, "arg[0]:query", path);
}
return _find(query).first();
};
/**
* Counts the number of documents in this collection.
*
* @example
* var db = new MonguitoDB(localStorage, "orders");
* var count = db.orders.count();
*
* @return {number}
* @alias Collection#count
* @public
*/
var _count = function () {
util.validateArguments(arguments, 0, "MonguitoDB.Collection.count()");
return _getConfig().ids.length;
};
/**
* Updates one or several documents in the current collection and returns
* a {@link Cursor} containing the updated documents.
*
* NOTE: _id property can't be modified.
*
* @example
* var db = new MonguitoDB(localStorage, "orders");
*
* // Updates a single document (that one matching _id = 1).
* db.orders.update({_id: 1}, {status: "Delivered"});
*
* // Updates several documents (those matching recipient = "Juan").
* db.orders.update({recipient: "Juan"}, {status: "Delivered"});
*
* @param {object|function} query - Selection criteria for the update.
* @param {object} obj - The modifications to apply (_id is omitted).
* @return {Cursor}
* @alias Collection#update
* @public
*/
var _update = function (query, obj) {
var path = "MonguitoDB.Collection.update()";
util.validateArguments(arguments, 2, path);
util.validateObjectOrFunction(query, "arg[0]:query", path);
util.validateObject(obj, "arg[1]:obj", path);
var documents = _find(query);
documents.forEach(function (doc) {
doc.update(obj);
});
return documents;
};
/**
* Removes one or several documents from the current collection.
*
* NOTE: If no query is passed-in, all documents will be removed.
*
* @example
* var db = new MonguitoDB(localStorage, "orders");
*
* // Removes a single document (that one matching _id = 1).
* db.orders.remove({_id: 1});
*
* // Removes several documents (those matching recipient = "Juan").
* db.orders.remove({recipient: "Juan"});
*
* // Removes all documents.
* db.orders.remove();
*
* @param {?(object|function)} query - Selection criteria for the remove.
* @alias Collection#remove
* @public
*/
var _remove = function (query) {
if (query !== undefined) {
var path = "MonguitoDB.Collection.remove()";
util.validateArguments(arguments, 1, path);
util.validateObjectOrFunction(query, "arg[0]:query", path);
}
_find(query).forEach(function (doc) {
doc.remove();
});
};
return {
insert: _insert,
get: _get,
find: _find,
findOne: _findOne,
count: _count,
update: _update,
remove: _remove
};
}
/**
* Utility to perform CRUD operations over the localStorage, sessionStorage, or
* any object implementing the Storage Interface defined by the W3C.
*
* This library was inspired by MongoDB, and some of its functions are
* syntactically similar to how they are in Mongo, with some differences
* and limitations.
*
* NOTE: This utility works synchronously.
*
* @example
* // An object to perform CRUD operations over the HTML5 localStorage.
* var db = new MonguitoDB(localStorage, "orders");
*
* // An object to perform CRUD operations over the HTML5 sessionStorage.
* var db = new MonguitoDB(sessionStorage, ["orders", "users"]);
*
* // An object to perform CRUD operations over collections stored in memory.
* var db = new MonguitoDB(null, ["orders", "users"]);
*
* @param {object|null} storage - Object providing the storage mechanism. You
* can send localStorage or sessionStorage if the browser supports HTML5.
* Send null to use the default mechanism which stores collections in memory.
* Alternatively, you can send your own object implementing the Storage
* Interface defined by the W3C.
* @param {string|string[]} collections - Array containing the names of the
* collections to be manipulated (analogous to tables in a relational
* database). You can send a single name, or an array of names if you want
* to manipulate several collections.
*
* @see http://www.w3.org/TR/webstorage/#the-storage-interface
* @see Collection
* @author Juan Cuartas
* @version 0.0.1, Jun 2014
*
* @constructor
*/
exports.MonguitoDB = function(storage, collections) {
util.validateArguments(arguments, 2, "MonguitoDB()");
if (storage !== null && !util.isValidStorage(storage)) {
throw new TypeError(
"MonguitoDB(), invalid arg[0]:storage, expecting storage object."
);
}
collections = [].concat(collections);
if (!collections.length) {
throw new TypeError(
"MonguitoDB(), invalid arg[1]:collections, array can not be empty."
);
}
for (var i = 0; i < collections.length; i++) {
if (typeof collections[i] !== "string") {
throw new TypeError(
"MonguitoDB(), invalid arg[1]:collections, " +
"expecting string or string[]."
);
}
}
for (i = 0; i < collections.length; i++) {
if (!util.isValidVariableName(collections[i])) {
throw new TypeError(
"MonguitoDB(), invalid arg[1]:collections '" + collections[i] +
"' is an invalid collection's name."
);
}
}
if (storage === null) {
storage = new MemoryStorage();
}
for (i = 0; i < collections.length; i++) {
if (!this[collections[i]]) {
this[collections[i]] = new Collection(storage, collections[i]);
}
}
};
})(window);