revisionstorage.js 33.4 KB
Newer Older
1 2 3
/*jslint indent: 2, maxlen: 80, nomen: true */
/*global jIO, hex_sha256, setTimeout, define */

4
/**
5
 * JIO Revision Storage.
6
 * It manages document version and can generate conflicts.
7 8 9
 * Description:
 * {
 *     "type": "revision",
10
 *     "sub_storage": <sub storage description>
11
 * }
12
 */
13 14
// define([module_name], [dependencies], module);
(function (dependencies, module) {
Sven Franck's avatar
Sven Franck committed
15
  "use strict";
16 17 18
  if (typeof define === 'function' && define.amd) {
    return define(dependencies, module);
  }
19 20
  module(jIO, {hex_sha256: hex_sha256});
}(['jio', 'sha256'], function (jIO, sha256) {
21 22
  "use strict";

23 24 25 26 27
  var tool = {
    "readBlobAsBinaryString": jIO.util.readBlobAsBinaryString,
    "uniqueJSONStringify": jIO.util.uniqueJSONStringify
  };

28
  jIO.addStorage("revision", function (spec) {
29

30
    var that = this, priv = {};
31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
    spec = spec || {};
    // ATTRIBUTES //
    priv.doc_tree_suffix = ".revision_tree.json";
    priv.sub_storage = spec.sub_storage;
    // METHODS //
    /**
     * Clones an object in deep (without functions)
     * @method clone
     * @param  {any} object The object to clone
     * @return {any} The cloned object
     */
    priv.clone = function (object) {
      var tmp = JSON.stringify(object);
      if (tmp === undefined) {
        return undefined;
46
      }
47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
      return JSON.parse(tmp);
    };

    /**
     * Generate a new uuid
     * @method generateUuid
     * @return {string} The new uuid
     */
    priv.generateUuid = function () {
      var S4 = function () {
        /* 65536 */
        var i, string = Math.floor(
          Math.random() * 0x10000
        ).toString(16);
        for (i = string.length; i < 4; i += 1) {
          string = '0' + string;
63
        }
64 65 66 67
        return string;
      };
      return S4() + S4() + "-" + S4() + "-" + S4() + "-" + S4() + "-" + S4() +
        S4() + S4();
68
    };
69 70 71 72 73 74 75 76

    /**
     * Generates a hash code of a string
     * @method hashCode
     * @param  {string} string The string to hash
     * @return {string} The string hash code
     */
    priv.hashCode = function (string) {
77
      return sha256.hex_sha256(string);
78 79 80 81 82 83 84 85 86 87 88
    };

    /**
     * Checks a revision format
     * @method checkDocumentRevisionFormat
     * @param  {object} doc The document object
     * @return {object} null if ok, else error object
     */
    priv.checkDocumentRevisionFormat = function (doc) {
      var send_error = function (message) {
        return {
89
          "status": 409,
90
          "message": message,
91
          "reason": "Wrong revision"
92 93 94 95 96 97
        };
      };
      if (typeof doc._rev === "string") {
        if (/^[0-9]+-[0-9a-zA-Z]+$/.test(doc._rev) === false) {
          return send_error("The document revision does not match " +
                            "^[0-9]+-[0-9a-zA-Z]+$");
98
        }
Sven Franck's avatar
Sven Franck committed
99
      }
100 101 102 103 104 105 106
      if (typeof doc._revs === "object") {
        if (typeof doc._revs.start !== "number" ||
            typeof doc._revs.ids !== "object" ||
            typeof doc._revs.ids.length !== "number") {
          return send_error(
            "The document revision history is not well formated"
          );
107
        }
Sven Franck's avatar
Sven Franck committed
108
      }
109 110 111 112 113
      if (typeof doc._revs_info === "object") {
        if (typeof doc._revs_info.length !== "number") {
          return send_error("The document revision information " +
                            "is not well formated");
        }
114
      }
115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139
    };

    /**
     * Creates a new document tree
     * @method newDocTree
     * @return {object} The new document tree
     */
    priv.newDocTree = function () {
      return {"children": []};
    };

    /**
     * Convert revs_info to a simple revisions history
     * @method revsInfoToHistory
     * @param  {array} revs_info The revs info
     * @return {object} The revisions history
     */
    priv.revsInfoToHistory = function (revs_info) {
      var i, revisions = {
        "start": 0,
        "ids": []
      };
      revs_info = revs_info || [];
      if (revs_info.length > 0) {
        revisions.start = parseInt(revs_info[0].rev.split('-')[0], 10);
Sven Franck's avatar
Sven Franck committed
140
      }
141 142
      for (i = 0; i < revs_info.length; i += 1) {
        revisions.ids.push(revs_info[i].rev.split('-')[1]);
Sven Franck's avatar
Sven Franck committed
143
      }
144 145 146 147 148 149 150 151 152 153 154 155 156
      return revisions;
    };

    /**
     * Convert the revision history object to an array of revisions.
     * @method revisionHistoryToList
     * @param  {object} revs The revision history
     * @return {array} The revision array
     */
    priv.revisionHistoryToList = function (revs) {
      var i, start = revs.start, new_list = [];
      for (i = 0; i < revs.ids.length; i += 1, start -= 1) {
        new_list.push(start + "-" + revs.ids[i]);
Sven Franck's avatar
Sven Franck committed
157
      }
158 159 160 161 162 163 164 165 166 167 168 169 170 171
      return new_list;
    };

    /**
     * Convert revision list to revs info.
     * @method revisionListToRevsInfo
     * @param  {array} revision_list The revision list
     * @param  {object} doc_tree The document tree
     * @return {array} The document revs info
     */
    priv.revisionListToRevsInfo = function (revision_list, doc_tree) {
      var revisionListToRevsInfoRec, revs_info = [], j;
      for (j = 0; j < revision_list.length; j += 1) {
        revs_info.push({"rev": revision_list[j], "status": "missing"});
Sven Franck's avatar
Sven Franck committed
172
      }
173 174 175 176 177 178 179 180 181 182 183
      revisionListToRevsInfoRec = function (index, doc_tree) {
        var child, i;
        if (index < 0) {
          return;
        }
        for (i = 0; i < doc_tree.children.length; i += 1) {
          child = doc_tree.children[i];
          if (child.rev === revision_list[index]) {
            revs_info[index].status = child.status;
            revisionListToRevsInfoRec(index - 1, child);
          }
184
        }
185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209
      };
      revisionListToRevsInfoRec(revision_list.length - 1, doc_tree);
      return revs_info;
    };

    /**
     * Update a document metadata revision properties
     * @method fillDocumentRevisionProperties
     * @param  {object} doc The document object
     * @param  {object} doc_tree The document tree
     */
    priv.fillDocumentRevisionProperties = function (doc, doc_tree) {
      if (doc._revs_info) {
        doc._revs = priv.revsInfoToHistory(doc._revs_info);
      } else if (doc._revs) {
        doc._revs_info = priv.revisionListToRevsInfo(
          priv.revisionHistoryToList(doc._revs),
          doc_tree
        );
      } else if (doc._rev) {
        doc._revs_info = priv.getRevisionInfo(doc._rev, doc_tree);
        doc._revs = priv.revsInfoToHistory(doc._revs_info);
      } else {
        doc._revs_info = [];
        doc._revs = {"start": 0, "ids": []};
Sven Franck's avatar
Sven Franck committed
210
      }
211 212 213 214
      if (doc._revs.start > 0) {
        doc._rev = doc._revs.start + "-" + doc._revs.ids[0];
      } else {
        delete doc._rev;
Sven Franck's avatar
Sven Franck committed
215
      }
216
    };
217 218 219 220 221 222 223 224 225

    /**
     * Generates the next revision of a document.
     * @methode generateNextRevision
     * @param  {object} doc The document metadata
     * @param  {boolean} deleted_flag The deleted flag
     * @return {array} 0:The next revision number and 1:the hash code
     */
    priv.generateNextRevision = function (doc, deleted_flag) {
226
      var string, revision_history, revs_info;
227 228 229 230 231 232
      doc = priv.clone(doc) || {};
      revision_history = doc._revs;
      revs_info = doc._revs_info;
      delete doc._rev;
      delete doc._revs;
      delete doc._revs_info;
233 234
      string = tool.uniqueJSONStringify(doc) +
        tool.uniqueJSONStringify(revision_history) +
235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275
        JSON.stringify(deleted_flag ? true : false);
      revision_history.start += 1;
      revision_history.ids.unshift(priv.hashCode(string));
      doc._revs = revision_history;
      doc._rev = revision_history.start + "-" + revision_history.ids[0];
      revs_info.unshift({
        "rev": doc._rev,
        "status": deleted_flag ? "deleted" : "available"
      });
      doc._revs_info = revs_info;
      return doc;
    };

    /**
     * Gets the revs info from the document tree
     * @method getRevisionInfo
     * @param  {string} revision The revision to search for
     * @param  {object} doc_tree The document tree
     * @return {array} The revs info
     */
    priv.getRevisionInfo = function (revision, doc_tree) {
      var getRevisionInfoRec;
      getRevisionInfoRec = function (doc_tree) {
        var i, child, revs_info;
        for (i = 0; i < doc_tree.children.length; i += 1) {
          child = doc_tree.children[i];
          if (child.rev === revision) {
            return [{"rev": child.rev, "status": child.status}];
          }
          revs_info = getRevisionInfoRec(child);
          if (revs_info.length > 0 || revision === undefined) {
            revs_info.push({"rev": child.rev, "status": child.status});
            return revs_info;
          }
        }
        return [];
      };
      return getRevisionInfoRec(doc_tree);
    };

    priv.updateDocumentTree = function (doc, doc_tree) {
276
      var revs_info, updateDocumentTreeRec;
277 278 279 280 281
      doc = priv.clone(doc);
      revs_info = doc._revs_info;
      updateDocumentTreeRec = function (doc_tree, revs_info) {
        var i, child, info;
        if (revs_info.length === 0) {
Sven Franck's avatar
Sven Franck committed
282
          return;
283
        }
284 285 286 287 288
        info = revs_info.pop();
        for (i = 0; i < doc_tree.children.length; i += 1) {
          child = doc_tree.children[i];
          if (child.rev === info.rev) {
            return updateDocumentTreeRec(child, revs_info);
289
          }
290
        }
291 292 293 294
        doc_tree.children.unshift({
          "rev": info.rev,
          "status": info.status,
          "children": []
295
        });
296 297 298 299 300
        updateDocumentTreeRec(doc_tree.children[0], revs_info);
      };
      updateDocumentTreeRec(doc_tree, priv.clone(revs_info));
    };

301
    priv.send = function (command, method, doc, option, callback) {
302
      var storage = command.storage(priv.sub_storage);
303 304 305 306 307 308 309
      function onSuccess(success) {
        callback(undefined, success);
      }
      function onError(err) {
        callback(err, undefined);
      }
      if (method === 'allDocs') {
310
        storage.allDocs(option).then(onSuccess, onError);
311
      } else {
312
        storage[method](doc, option).then(onSuccess, onError);
313
      }
314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338
    };

    priv.getWinnerRevsInfo = function (doc_tree) {
      var revs_info = [], getWinnerRevsInfoRec;
      getWinnerRevsInfoRec = function (doc_tree, tmp_revs_info) {
        var i;
        if (doc_tree.rev) {
          tmp_revs_info.unshift({
            "rev": doc_tree.rev,
            "status": doc_tree.status
          });
        }
        if (doc_tree.children.length === 0) {
          if (revs_info.length === 0 ||
              (revs_info[0].status !== "available" &&
               tmp_revs_info[0].status === "available") ||
              (tmp_revs_info[0].status === "available" &&
               revs_info.length < tmp_revs_info.length)) {
            revs_info = priv.clone(tmp_revs_info);
          }
        }
        for (i = 0; i < doc_tree.children.length; i += 1) {
          getWinnerRevsInfoRec(doc_tree.children[i], tmp_revs_info);
        }
        tmp_revs_info.shift();
339
      };
340 341
      getWinnerRevsInfoRec(doc_tree, []);
      return revs_info;
342
    };
343 344 345 346 347 348

    priv.getConflicts = function (revision, doc_tree) {
      var conflicts = [], getConflictsRec;
      getConflictsRec = function (doc_tree) {
        var i;
        if (doc_tree.rev === revision) {
349 350
          return;
        }
351 352 353 354
        if (doc_tree.children.length === 0) {
          if (doc_tree.status !== "deleted") {
            conflicts.push(doc_tree.rev);
          }
355
        }
356 357
        for (i = 0; i < doc_tree.children.length; i += 1) {
          getConflictsRec(doc_tree.children[i]);
358 359
        }
      };
360 361
      getConflictsRec(doc_tree);
      return conflicts.length === 0 ? undefined : conflicts;
362
    };
363

364 365
    priv.get = function (command, doc, option, callback) {
      priv.send(command, "get", doc, option, callback);
366
    };
367 368
    priv.put = function (command, doc, option, callback) {
      priv.send(command, "put", doc, option, callback);
369
    };
370 371
    priv.remove = function (command, doc, option, callback) {
      priv.send(command, "remove", doc, option, callback);
372
    };
373 374
    priv.getAttachment = function (command, attachment, option, callback) {
      priv.send(command, "getAttachment", attachment, option, callback);
375
    };
376 377
    priv.putAttachment = function (command, attachment, option, callback) {
      priv.send(command, "putAttachment", attachment, option, callback);
378
    };
379 380
    priv.removeAttachment = function (command, attachment, option, callback) {
      priv.send(command, "removeAttachment", attachment, option, callback);
381 382
    };

383
    priv.getDocument = function (command, doc, option, callback) {
384 385 386 387 388 389
      doc = priv.clone(doc);
      doc._id = doc._id + "." + doc._rev;
      delete doc._attachment;
      delete doc._rev;
      delete doc._revs;
      delete doc._revs_info;
390
      priv.get(command, doc, option, callback);
391
    };
392
    priv.putDocument = function (command, doc, option, callback) {
393 394 395 396 397
      doc = priv.clone(doc);
      doc._id = doc._id + "." + doc._rev;
      delete doc._attachment;
      delete doc._data;
      delete doc._mimetype;
398
      delete doc._content_type;
399 400 401
      delete doc._rev;
      delete doc._revs;
      delete doc._revs_info;
402
      priv.put(command, doc, option, callback);
403 404
    };

405
    priv.getRevisionTree = function (command, doc, option, callback) {
406 407
      doc = priv.clone(doc);
      doc._id = doc._id + priv.doc_tree_suffix;
408 409 410 411 412 413 414 415 416
      priv.get(command, doc, option, function (err, response) {
        if (err) {
          return callback(err, response);
        }
        if (response.data && response.data.children) {
          response.data.children = JSON.parse(response.data.children);
        }
        return callback(err, response);
      });
417 418
    };

419
    priv.getAttachmentList = function (command, doc, option, callback) {
420 421
      var attachment_id, dealResults, state = "ok", result_list = [], count = 0;
      dealResults = function (attachment_id, attachment_meta) {
422
        return function (err, response) {
423 424
          if (state !== "ok") {
            return;
Sven Franck's avatar
Sven Franck committed
425
          }
426 427 428 429 430 431 432 433
          count -= 1;
          if (err) {
            if (err.status === 404) {
              result_list.push(undefined);
            } else {
              state = "error";
              return callback(err, undefined);
            }
434
          }
435 436
          result_list.push({
            "_attachment": attachment_id,
437
            "_data": response.data,
438
            "_content_type": attachment_meta.content_type
439 440 441
          });
          if (count === 0) {
            state = "finished";
442
            callback(undefined, {"data": result_list});
443 444 445 446 447 448 449
          }
        };
      };
      for (attachment_id in doc._attachments) {
        if (doc._attachments.hasOwnProperty(attachment_id)) {
          count += 1;
          priv.getAttachment(
450
            command,
451 452 453 454
            {"_id": doc._id, "_attachment": attachment_id},
            option,
            dealResults(attachment_id, doc._attachments[attachment_id])
          );
455 456
        }
      }
457
      if (count === 0) {
458
        callback(undefined, {"data": []});
459
      }
460 461
    };

462 463
    priv.putAttachmentList = function (command, doc, option,
                                       attachment_list, callback) {
464 465
      var i, dealResults, state = "ok", count = 0, attachment;
      attachment_list = attachment_list || [];
466 467
      dealResults = function () {
        return function (err) {
468 469 470 471 472 473 474 475 476 477
          if (state !== "ok") {
            return;
          }
          count -= 1;
          if (err) {
            state = "error";
            return callback(err, undefined);
          }
          if (count === 0) {
            state = "finished";
478
            callback(undefined, {});
479
          }
480
        };
481 482 483 484 485 486
      };
      for (i = 0; i < attachment_list.length; i += 1) {
        attachment = attachment_list[i];
        if (attachment !== undefined) {
          count += 1;
          attachment._id = doc._id + "." + doc._rev;
487
          priv.putAttachment(command, attachment, option, dealResults(i));
488
        }
489
      }
490
      if (count === 0) {
491
        return callback(undefined, {});
492 493 494
      }
    };

495
    priv.putDocumentTree = function (command, doc, option, doc_tree, callback) {
496 497
      doc_tree = priv.clone(doc_tree);
      doc_tree._id = doc._id + priv.doc_tree_suffix;
498 499 500
      if (doc_tree.children) {
        doc_tree.children = JSON.stringify(doc_tree.children);
      }
501
      priv.put(command, doc_tree, option, callback);
502 503 504 505
    };

    priv.notFoundError = function (message, reason) {
      return {
506
        "status": 404,
507 508 509 510 511 512 513
        "message": message,
        "reason": reason
      };
    };

    priv.conflictError = function (message, reason) {
      return {
514
        "status": 409,
515 516 517 518 519
        "message": message,
        "reason": reason
      };
    };

520
    priv.revisionGenericRequest = function (command, doc, option,
521 522 523 524
                                            specific_parameter, onEnd) {
      var prev_doc, doc_tree, attachment_list, callback = {};
      if (specific_parameter.doc_id) {
        doc._id = specific_parameter.doc_id;
525
      }
526 527
      if (specific_parameter.attachment_id) {
        doc._attachment = specific_parameter.attachment_id;
528
      }
529 530
      callback.begin = function () {
        var check_error;
531
        doc._id = doc._id || priv.generateUuid(); // XXX should not generate id
532 533 534 535 536
        if (specific_parameter.revision_needed && !doc._rev) {
          return onEnd(priv.conflictError(
            "Document update conflict",
            "No document revision was provided"
          ), undefined);
537
        }
538 539 540 541 542
        // check revision format
        check_error = priv.checkDocumentRevisionFormat(doc);
        if (check_error !== undefined) {
          return onEnd(check_error, undefined);
        }
543
        priv.getRevisionTree(command, doc, option, callback.getRevisionTree);
544 545 546
      };
      callback.getRevisionTree = function (err, response) {
        var winner_info, previous_revision, generate_new_revision;
547
        previous_revision = doc._rev;
548 549 550 551 552
        generate_new_revision = doc._revs || doc._revs_info ? false : true;
        if (err) {
          if (err.status !== 404) {
            err.message = "Cannot get document revision tree";
            return onEnd(err, undefined);
553
          }
554
        }
555
        doc_tree = (response && response.data) || priv.newDocTree();
556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573
        if (specific_parameter.get || specific_parameter.getAttachment) {
          if (!doc._rev) {
            winner_info = priv.getWinnerRevsInfo(doc_tree);
            if (winner_info.length === 0) {
              return onEnd(priv.notFoundError(
                "Document not found",
                "missing"
              ), undefined);
            }
            if (winner_info[0].status === "deleted") {
              return onEnd(priv.notFoundError(
                "Document not found",
                "deleted"
              ), undefined);
            }
            doc._rev = winner_info[0].rev;
          }
          priv.fillDocumentRevisionProperties(doc, doc_tree);
574
          return priv.getDocument(command, doc, option, callback.getDocument);
575
        }
576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591
        priv.fillDocumentRevisionProperties(doc, doc_tree);
        if (generate_new_revision) {
          if (previous_revision && doc._revs_info.length === 0) {
            // the document history has changed, it means that the document
            // revision was wrong. Add a pseudo history to the document
            doc._rev = previous_revision;
            doc._revs = {
              "start": parseInt(previous_revision.split("-")[0], 10),
              "ids": [previous_revision.split("-")[1]]
            };
            doc._revs_info = [{"rev": previous_revision, "status": "missing"}];
          }
          doc = priv.generateNextRevision(
            doc,
            specific_parameter.remove
          );
592
        }
593 594 595 596 597 598 599
        if (doc._revs_info.length > 1) {
          prev_doc = {
            "_id": doc._id,
            "_rev": doc._revs_info[1].rev
          };
          if (!generate_new_revision && specific_parameter.putAttachment) {
            prev_doc._rev = doc._revs_info[0].rev;
600 601
          }
        }
602 603 604 605 606
        // force revs_info status
        doc._revs_info[0].status = (specific_parameter.remove ?
                                    "deleted" : "available");
        priv.updateDocumentTree(doc, doc_tree);
        if (prev_doc) {
607 608
          return priv.getDocument(command, prev_doc,
                                  option, callback.getDocument);
609 610
        }
        if (specific_parameter.remove || specific_parameter.removeAttachment) {
611
          return onEnd(priv.notFoundError(
612
            "Unable to remove an inexistent document",
613 614
            "missing"
          ), undefined);
615
        }
616
        priv.putDocument(command, doc, option, callback.putDocument);
617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634
      };
      callback.getDocument = function (err, res_doc) {
        var k, conflicts;
        if (err) {
          if (err.status === 404) {
            if (specific_parameter.remove ||
                specific_parameter.removeAttachment) {
              return onEnd(priv.conflictError(
                "Document update conflict",
                "Document is missing"
              ), undefined);
            }
            if (specific_parameter.get) {
              return onEnd(priv.notFoundError(
                "Unable to find the document",
                "missing"
              ), undefined);
            }
635
            res_doc = {"data": {}};
636 637 638
          } else {
            err.message = "Cannot get document";
            return onEnd(err, undefined);
639 640
          }
        }
641
        res_doc = res_doc.data;
642 643 644 645 646 647 648 649 650 651 652 653 654 655 656
        if (specific_parameter.get) {
          res_doc._id = doc._id;
          res_doc._rev = doc._rev;
          if (option.conflicts === true) {
            conflicts = priv.getConflicts(doc._rev, doc_tree);
            if (conflicts) {
              res_doc._conflicts = conflicts;
            }
          }
          if (option.revs === true) {
            res_doc._revisions = doc._revs;
          }
          if (option.revs_info === true) {
            res_doc._revs_info = doc._revs_info;
          }
657
          return onEnd(undefined, {"data": res_doc});
658 659 660 661 662 663 664 665 666 667 668
        }
        if (specific_parameter.putAttachment ||
            specific_parameter.removeAttachment) {
          // copy metadata (not beginning by "_" to document
          for (k in res_doc) {
            if (res_doc.hasOwnProperty(k) && !k.match("^_")) {
              doc[k] = res_doc[k];
            }
          }
        }
        if (specific_parameter.remove) {
669 670
          priv.putDocumentTree(command, doc, option,
                               doc_tree, callback.putDocumentTree);
671
        } else {
672 673
          priv.getAttachmentList(command, res_doc, option,
                                 callback.getAttachmentList);
674
        }
675
      };
676 677
      callback.getAttachmentList = function (err, res_list) {
        var i, attachment_found = false;
678
        if (err) {
679 680
          err.message = "Cannot get attachment";
          return onEnd(err, undefined);
Sven Franck's avatar
Sven Franck committed
681
        }
682
        res_list = res_list.data;
683 684 685 686 687 688 689
        attachment_list = res_list || [];
        if (specific_parameter.getAttachment) {
          // getting specific attachment
          for (i = 0; i < attachment_list.length; i += 1) {
            if (attachment_list[i] &&
                doc._attachment ===
                attachment_list[i]._attachment) {
690
              return onEnd(undefined, {"data": attachment_list[i]._data});
691 692 693 694 695 696
            }
          }
          return onEnd(priv.notFoundError(
            "Unable to get an inexistent attachment",
            "missing"
          ), undefined);
697
        }
698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714
        if (specific_parameter.remove_from_attachment_list) {
          // removing specific attachment
          for (i = 0; i < attachment_list.length; i += 1) {
            if (attachment_list[i] &&
                specific_parameter.remove_from_attachment_list._attachment ===
                attachment_list[i]._attachment) {
              attachment_found = true;
              attachment_list[i] = undefined;
              break;
            }
          }
          if (!attachment_found) {
            return onEnd(priv.notFoundError(
              "Unable to remove an inexistent attachment",
              "missing"
            ), undefined);
          }
Sven Franck's avatar
Sven Franck committed
715
        }
716
        priv.putDocument(command, doc, option, callback.putDocument);
717
      };
718
      callback.putDocument = function (err) {
719
        var i, attachment_found = false;
720
        if (err) {
721 722
          err.message = "Cannot post the document";
          return onEnd(err, undefined);
723
        }
724 725 726 727 728 729 730 731 732 733 734 735 736 737 738
        if (specific_parameter.add_to_attachment_list) {
          // adding specific attachment
          attachment_list = attachment_list || [];
          for (i = 0; i < attachment_list.length; i += 1) {
            if (attachment_list[i] &&
                specific_parameter.add_to_attachment_list._attachment ===
                attachment_list[i]._attachment) {
              attachment_found = true;
              attachment_list[i] = specific_parameter.add_to_attachment_list;
              break;
            }
          }
          if (!attachment_found) {
            attachment_list.unshift(specific_parameter.add_to_attachment_list);
          }
Sven Franck's avatar
Sven Franck committed
739
        }
740
        priv.putAttachmentList(
741
          command,
742 743 744 745 746 747
          doc,
          option,
          attachment_list,
          callback.putAttachmentList
        );
      };
748
      callback.putAttachmentList = function (err) {
749
        if (err) {
750 751
          err.message = "Cannot copy attacments to the document";
          return onEnd(err, undefined);
752
        }
753 754
        priv.putDocumentTree(command, doc, option,
                             doc_tree, callback.putDocumentTree);
755
      };
756
      callback.putDocumentTree = function (err) {
757
        var response_object;
758
        if (err) {
759 760
          err.message = "Cannot update the document history";
          return onEnd(err, undefined);
761
        }
762 763 764 765 766 767 768 769
        response_object = {
          "id": doc._id,
          "rev": doc._rev
        };
        if (specific_parameter.putAttachment ||
            specific_parameter.removeAttachment ||
            specific_parameter.getAttachment) {
          response_object.attachment = doc._attachment;
770
        }
771 772
        onEnd(undefined, response_object);
        // if (option.keep_revision_history !== true) {
773
        //   // priv.remove(command, prev_doc, option, function () {
774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790
        //   //   - change "available" status to "deleted"
        //   //   - remove attachments
        //   //   - done, no callback
        //   // });
        // }
      };
      callback.begin();
    };

    /**
     * Post the document metadata and create or update a document tree.
     * Options:
     * - {boolean} keep_revision_history To keep the previous revisions
     *                                   (false by default) (NYI).
     * @method post
     * @param  {object} command The JIO command
     */
791
    that.post = function (command, metadata, option) {
792
      priv.revisionGenericRequest(
793 794 795
        command,
        metadata,
        option,
796 797 798
        {},
        function (err, response) {
          if (err) {
799
            return command.error(err);
800
          }
801
          command.success({"id": response.id, "rev": response.rev});
802
        }
803 804 805 806 807 808 809 810 811 812 813
      );
    };

    /**
     * Put the document metadata and create or update a document tree.
     * Options:
     * - {boolean} keep_revision_history To keep the previous revisions
     *                                   (false by default) (NYI).
     * @method put
     * @param  {object} command The JIO command
     */
814
    that.put = function (command, metadata, option) {
815
      priv.revisionGenericRequest(
816 817 818
        command,
        metadata,
        option,
819
        {},
820
        function (err, response) {
821
          if (err) {
822
            return command.error(err);
823
          }
824
          command.success({"rev": response.rev});
825 826 827 828 829
        }
      );
    };


830
    that.putAttachment = function (command, param, option) {
831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847
      tool.readBlobAsBinaryString(param._blob).then(function (event) {
        param._content_type = param._blob.type;
        param._data = event.target.result;
        delete param._blob;
        priv.revisionGenericRequest(
          command,
          param,
          option,
          {
            "doc_id": param._id,
            "attachment_id": param._attachment,
            "add_to_attachment_list": {
              "_attachment": param._attachment,
              "_content_type": param._content_type,
              "_data": param._data
            },
            "putAttachment": true
848
          },
849 850 851 852 853
          function (err, response) {
            if (err) {
              return command.error(err);
            }
            command.success({"rev": response.rev});
854
          }
855 856 857 858
        );
      }, function () {
        command.error("conflict", "broken blob", "Cannot read data to put");
      });
859
    };
860

861
    that.remove = function (command, param, option) {
862
      priv.revisionGenericRequest(
863 864 865
        command,
        param,
        option,
866 867 868 869
        {
          "revision_needed": true,
          "remove": true
        },
870
        function (err) {
871
          if (err) {
872
            return command.error(err);
873
          }
874
          command.success();
875 876 877 878
        }
      );
    };

879
    that.removeAttachment = function (command, param, option) {
880
      priv.revisionGenericRequest(
881 882 883
        command,
        param,
        option,
884
        {
885 886
          "doc_id": param._id,
          "attachment_id": param._attachment,
887 888 889
          "revision_needed": true,
          "removeAttachment": true,
          "remove_from_attachment_list": {
890
            "_attachment": param._attachment
891 892
          }
        },
893
        function (err) {
894
          if (err) {
895
            return command.error(err);
896
          }
897
          command.success();
898 899
        }
      );
900
    };
901

902
    that.get = function (command, param, option) {
903
      priv.revisionGenericRequest(
904 905 906
        command,
        param,
        option,
907 908
        {
          "get": true
909
        },
910 911
        function (err, response) {
          if (err) {
912
            return command.error(err);
913
          }
914
          command.success({"data": response.data});
915
        }
916 917 918
      );
    };

919
    that.getAttachment = function (command, param, option) {
920
      priv.revisionGenericRequest(
921 922 923
        command,
        param,
        option,
924
        {
925 926
          "doc_id": param._id,
          "attachment_id": param._attachment,
927 928 929 930
          "getAttachment": true
        },
        function (err, response) {
          if (err) {
931
            return command.error(err);
932
          }
933
          command.success({"data": response.data});
934 935 936 937
        }
      );
    };

938 939
    that.allDocs = function (command, param, option) {
      /*jslint unparam: true */
940 941 942 943
      var rows, result = {"total_rows": 0, "rows": []}, functions = {};
      functions.finished = 0;
      functions.falseResponseGenerator = function (response, callback) {
        callback(undefined, response);
944
      };
945 946 947 948
      functions.fillResultGenerator = function (doc_id) {
        return function (err, doc_tree) {
          var document_revision, row, revs_info;
          if (err) {
949
            return command.error(err);
950
          }
951 952 953 954 955 956 957 958 959 960 961
          revs_info = priv.getWinnerRevsInfo(doc_tree);
          document_revision =
            rows.document_revisions[doc_id + "." + revs_info[0].rev];
          if (document_revision) {
            row = {
              "id": doc_id,
              "key": doc_id,
              "value": {
                "rev": revs_info[0].rev
              }
            };
962
            if (document_revision.doc && option.include_docs) {
963 964 965 966 967 968
              document_revision.doc._id = doc_id;
              document_revision.doc._rev = revs_info[0].rev;
              row.doc = document_revision.doc;
            }
            result.rows.push(row);
            result.total_rows += 1;
969
          }
970 971 972 973 974 975
          functions.success();
        };
      };
      functions.success = function () {
        functions.finished -= 1;
        if (functions.finished === 0) {
976
          command.success(result);
977
        }
978
      };
979 980
      priv.send(command, "allDocs", null, option, function (err, response) {
        var i, row, selector, selected;
981
        if (err) {
982
          return command.error(err);
983
        }
984
        response = response.data;
985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013
        selector = /\.revision_tree\.json$/;
        rows = {
          "revision_trees": {
            // id.revision_tree.json: {
            //   id: blabla
            //   doc: {...}
            // }
          },
          "document_revisions": {
            // id.rev: {
            //   id: blabla
            //   rev: 1-1
            //   doc: {...}
            // }
          }
        };
        while (response.rows.length > 0) {
          // filling rows
          row = response.rows.shift();
          selected = selector.exec(row.id);
          if (selected) {
            selected = selected.input.substring(0, selected.index);
            // this is a revision tree
            rows.revision_trees[row.id] = {
              "id": selected
            };
            if (row.doc) {
              rows.revision_trees[row.id].doc = row.doc;
            }
1014
          } else {
1015 1016 1017 1018 1019 1020 1021 1022
            // this is a simple revision
            rows.document_revisions[row.id] = {
              "id": row.id.split(".").slice(0, -1),
              "rev": row.id.split(".").slice(-1)
            };
            if (row.doc) {
              rows.document_revisions[row.id].doc = row.doc;
            }
1023 1024
          }
        }
1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035
        functions.finished += 1;
        for (i in rows.revision_trees) {
          if (rows.revision_trees.hasOwnProperty(i)) {
            functions.finished += 1;
            if (rows.revision_trees[i].doc) {
              functions.falseResponseGenerator(
                rows.revision_trees[i].doc,
                functions.fillResultGenerator(rows.revision_trees[i].id)
              );
            } else {
              priv.getRevisionTree(
1036
                command,
1037
                {"_id": rows.revision_trees[i].id},
1038
                option,
1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049
                functions.fillResultGenerator(rows.revision_trees[i].id)
              );
            }
          }
        }
        functions.success();
      });
    };
  }); // end RevisionStorage

}));