/* * Copyright 2013, Nexedi SA * Released under the LGPL license. * http://www.gnu.org/licenses/lgpl.html */ /*jslint indent:2, maxlen: 80, nomen: true */ /*global jIO: true, exports: true, define: true */ /** * Provides a split storage for JIO. This storage splits data * and store them in the sub storages defined on the description. * * { * "type": "split", * "storage_list": [<storage description>, ...] * } */ (function () { "use strict"; var queries; /** * Get the real type of an object * * @param {Any} value The value to check * @return {String} The value type */ function type(value) { // returns "String", "Object", "Array", "RegExp", ... return (/^\[object ([a-zA-Z]+)\]$/).exec( Object.prototype.toString.call(value) )[1]; } /** * Generate a new uuid * * @method generateUuid * @private * @return {String} The new uuid */ function generateUuid() { function S4() { /* 65536 */ var i, string = Math.floor( Math.random() * 0x10000 ).toString(16); for (i = string.length; i < 4; i += 1) { string = '0' + string; } return string; } return S4() + S4() + "-" + S4() + "-" + S4() + "-" + S4() + "-" + S4() + S4() + S4(); } /** * Class to merge allDocs responses from several sub storages. * * @class AllDocsResponseMerger * @constructor */ function AllDocsResponseMerger() { /** * A list of allDocs response. * * @attribute response_list * @type {Array} Contains allDocs responses * @default [] */ this.response_list = []; } AllDocsResponseMerger.prototype.constructor = AllDocsResponseMerger; /** * Add an allDocs response to the response list. * * @method addResponse * @param {Object} response The allDocs response. * @return {AllDocsResponseMerger} This */ AllDocsResponseMerger.prototype.addResponse = function (response) { this.response_list.push(response); return this; }; /** * Add several allDocs responses to the response list. * * @method addResponseList * @param {Array} response_list An array of allDocs responses. * @return {AllDocsResponseMerger} This */ AllDocsResponseMerger.prototype.addResponseList = function (response_list) { var i; for (i = 0; i < response_list.length; i += 1) { this.response_list.push(response_list[i]); } return this; }; /** * Merge the response_list to one allDocs response. * * The merger will find rows with the same id in order to merge them, thanks * to the onRowToMerge method. If no row correspond to an id, rows with the * same id will be ignored. * * @method merge * @param {Object} [option={}] The merge options * @param {Boolean} [option.include_docs=false] Tell the merger to also * merge metadata if true. * @return {Object} The merged allDocs response. */ AllDocsResponseMerger.prototype.merge = function (option) { var result = [], row, to_merge = [], tmp, i; if (this.response_list.length === 0) { return []; } while ((row = this.response_list[0].rows.shift()) !== undefined) { console.log('row', row); to_merge[0] = row; for (i = 1; i < this.response_list.length; i += 1) { to_merge[i] = AllDocsResponseMerger.listPopFromRowId( this.response_list[i].rows, row.id ); if (to_merge[i] === undefined) { break; } } console.log('to merge', to_merge); tmp = this.onRowToMerge(to_merge, option || {}); if (tmp !== undefined) { result[result.length] = tmp; } } this.response_list = []; return {"total_rows": result.length, "rows": result}; }; /** * This method is called when the merger want to merge several rows with the * same id. * * @method onRowToMerge * @param {Array} row_list An array of rows. * @param {Object} [option={}] The merge option. * @param {Boolean} [option.include_docs=false] Also merge the metadata if * true * @return {Object} The merged row */ AllDocsResponseMerger.prototype.onRowToMerge = function (row_list, option) { var i, k, new_row = {"value": {}}, data = ""; option = option || {}; for (i = 0; i < row_list.length; i += 1) { new_row.id = row_list[i].id; if (row_list[i].key) { new_row.key = row_list[i].key; } if (option.include_docs) { new_row.doc = new_row.doc || {}; for (k in row_list[i].doc) { if (row_list[i].doc.hasOwnProperty(k)) { if (k[0] === "_") { new_row.doc[k] = row_list[i].doc[k]; } } } data += row_list[i].doc.data; } } if (option.include_docs) { try { data = JSON.parse(data); } catch (e) { return undefined; } for (k in data) { if (data.hasOwnProperty(k)) { new_row.doc[k] = data[k]; } } } return new_row; }; /** * Search for a specific row and pop it. During the search operation, all * parsed rows are stored on a dictionnary in order to be found instantly * later. * * @method listPopFromRowId * @param {Array} rows The row list * @param {String} doc_id The document/row id * @return {Object/undefined} The poped row */ AllDocsResponseMerger.listPopFromRowId = function (rows, doc_id) { var row; if (!rows.dict) { rows.dict = {}; } if (rows.dict[doc_id]) { row = rows.dict[doc_id]; delete rows.dict[doc_id]; return row; } while ((row = rows.shift()) !== undefined) { if (row.id === doc_id) { return row; } rows.dict[row.id] = row; } }; /** * The split storage class used by JIO. * * A split storage instance is able to i/o on several sub storages with * split documents. * * @class splitStorage */ function splitStorage(spec, my) { var that = my.basicStorage(spec, my), priv = {}; /** * The list of sub storages we want to use to store part of documents. * * @attribute storage_list * @private * @type {Array} Array of storage descriptions */ priv.storage_list = spec.storage_list; ////////////////////////////////////////////////////////////////////// // Overrides /** * Overrides the original {{#crossLink "storage/specToStore:method"}} * specToStore method{{/crossLink}}. * * @method specToStore * @return {Object} The specificities to store */ that.specToStore = function () { return {"storage_list": priv.storage_list}; }; /** * TODO validateState */ ////////////////////////////////////////////////////////////////////// // Tools /** * Send a command to all sub storages. All the response are returned * in a list. The index of the response correspond to the storage_list * index. If an error occurs during operation, the callback is called with * `callback(err, undefined)`. The response is given with * `callback(undefined, response_list)`. * * `doc` is the document informations but can also be a list of dedicated * document informations. In this case, each document is associated to one * sub storage. * * @method send * @private * @param {String} method The command method * @param {Object,Array} doc The document information to send to each sub * storages or a list of dedicated document * @param {Object} option The command option * @param {Function} callback Called at the end */ priv.send = function (method, doc, option, callback) { var i, answer_list = [], failed = false; function onEnd() { i += 1; if (i === priv.storage_list.length) { callback(undefined, answer_list); } } function onSuccess(i) { return function (response) { if (!failed) { answer_list[i] = response; } onEnd(); }; } function onError(i) { return function (err) { if (!failed) { failed = true; err.index = i; callback(err, undefined); } }; } if (type(doc) !== "Array") { for (i = 0; i < priv.storage_list.length; i += 1) { that.addJob( method, priv.storage_list[i], doc, option, onSuccess(i), onError(i) ); } } else { for (i = 0; i < priv.storage_list.length; i += 1) { that.addJob( method, priv.storage_list[i], doc[i], option, onSuccess(i), onError(i) ); } } i = 0; }; /** * Split document metadata then store them to the sub storages. * * @method postOrPut * @private * @param {Object} doc A serialized document object * @param {Object} option Command option properties * @param {String} method The command method ('post' or 'put') */ priv.postOrPut = function (doc, option, method) { var i, data, doc_list = [], doc_underscores = {}; if (!doc._id) { doc._id = generateUuid(); } for (i in doc) { if (doc.hasOwnProperty(i)) { if (i[0] === "_") { doc_underscores[i] = doc[i]; delete doc[i]; } } } data = JSON.stringify(doc); for (i = 0; i < priv.storage_list.length; i += 1) { doc_list[i] = JSON.parse(JSON.stringify(doc_underscores)); doc_list[i].data = data.slice( (data.length / priv.storage_list.length) * i, (data.length / priv.storage_list.length) * (i + 1) ); } priv.send(method, doc_list, option, function (err, response) { if (err) { err.message = "Unable to " + method + " document"; delete err.index; return that.error(err); } that.success({"ok": true, "id": doc_underscores._id}); }); }; ////////////////////////////////////////////////////////////////////// // JIO commands /** * Split document metadata then store them to the sub storages. * * @method post * @param {Command} command The JIO command */ that.post = function (command) { priv.postOrPut(command.cloneDoc(), command.cloneOption(), 'post'); }; /** * Split document metadata then store them to the sub storages. * * @method put * @param {Command} command The JIO command */ that.put = function (command) { priv.postOrPut(command.cloneDoc(), command.cloneOption(), 'put'); }; /** * Puts an attachment to the sub storages. * * @method putAttachment * @param {Command} command The JIO command */ that.putAttachment = function (command) { var i, attachment_list = [], data = command.getAttachmentData(); for (i = 0; i < priv.storage_list.length; i += 1) { attachment_list[i] = command.cloneDoc(); attachment_list[i]._data = data.slice( (data.length / priv.storage_list.length) * i, (data.length / priv.storage_list.length) * (i + 1) ); } priv.send( 'putAttachment', attachment_list, command.cloneOption(), function (err, response) { if (err) { err.message = "Unable to put attachment"; delete err.index; return that.error(err); } that.success({ "ok": true, "id": command.getDocId(), "attachment": command.getAttachmentId() }); } ); }; /** * Gets splited document metadata then returns real document. * * @method get * @param {Command} command The JIO command */ that.get = function (command) { var doc, option, data, attachments; doc = command.cloneDoc(); option = command.cloneOption(); priv.send('get', doc, option, function (err, response) { var i, k; if (err) { err.message = "Unable to get document"; delete err.index; return that.error(err); } doc = ''; for (i = 0; i < response.length; i += 1) { doc += response[i].data; } doc = JSON.parse(doc); for (i = 0; i < response.length; i += 1) { for (k in response[i]) { if (response[i].hasOwnProperty(k)) { if (k[0] === "_") { doc[k] = response[i][k]; } } } if (response[i]._attachments) { doc._attachments = doc._attachments || {}; for (k in response[i]._attachments) { if (response[i]._attachments.hasOwnProperty(k)) { doc._attachments[k] = doc._attachments[k] || { "length": 0, "content_type": "", }; doc._attachments[k].length += response[i]._attachments[k]. length; doc._attachments[k].content_type = response[i]._attachments[k]. content_type; } } } } doc._id = command.getDocId(); that.success(doc); }); }; /** * Gets splited document attachment then returns real attachment data. * * @method getAttachment * @param {Command} command The JIO command */ that.getAttachment = function (command) { var doc, option; doc = command.cloneDoc(); option = command.cloneOption(); priv.send('getAttachment', doc, option, function (err, response) { var i, k; if (err) { err.message = "Unable to get attachment"; delete err.index; return that.error(err); } doc = ''; for (i = 0; i < response.length; i += 1) { doc += response[i]; } that.success(doc); }); }; /** * Removes a document from the sub storages. * * @method remove * @param {Command} command The JIO command */ that.remove = function (command) { priv.send( 'remove', command.cloneDoc(), command.cloneOption(), function (err, response_list) { if (err) { err.message = "Unable to remove document"; delete err.index; return that.error(err); } that.success({"id": command.getDocId(), "ok": true}); } ); }; /** * Removes an attachment from the sub storages. * * @method removeAttachment * @param {Command} command The JIO command */ that.removeAttachment = function (command) { var doc = command.cloneDoc(); priv.send( 'removeAttachment', doc, command.cloneOption(), function (err, response_list) { if (err) { err.message = "Unable to remove attachment"; delete err.index; return that.error(err); } that.success({ "id": doc._id, "attachment": doc._attachment, "ok": true }); } ); }; /** * Retreive a list of all document in the sub storages. * * If include_docs option is false, then it returns the document list from * the first sub storage. Else, it will merge results and return. * * @method allDocs * @param {Command} command The JIO command */ that.allDocs = function (command) { var option = command.cloneOption(); option = {"include_docs": option.include_docs}; priv.send( 'allDocs', command.cloneDoc(), option, function (err, response_list) { var all_docs_merger; if (err) { err.message = "Unable to retrieve document list"; delete err.index; return that.error(err); } all_docs_merger = new AllDocsResponseMerger(); all_docs_merger.addResponseList(response_list); return that.success(all_docs_merger.merge(option)); } ); }; return that; } // end of splitStorage ////////////////////////////// // exports to JIO if (typeof define === "function" && define.amd) { define(['jio'], function (jio) { try { queries = require('complex_queries'); } catch (e) {} jio.addStorageType('split', splitStorage); }); } else if (typeof require === "function") { require('jio').addStorageType('split', splitStorage); } else if (typeof jIO === "object") { jIO.addStorageType('split', splitStorage); } else { throw new Error("Unable to export splitStorage to JIO."); } }());