indexstorage.js 18.6 KB
Newer Older
Sven Franck's avatar
Sven Franck committed
1 2
/*jslint indent: 2, maxlen: 80, sloppy: true, nomen: true */
/*global jIO: true, localStorage: true, setTimeout: true */
3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
/**
 * JIO Index Storage.
 * Manages indexes for specified storages.
 * Description:
 * {
 *     "type": "index",
 *     "indices": [
 *        {"indexA",["field_A"]},
 *        {"indexAB",["field_A","field_B"]}
 *     ],
 *     "storage": [
 *         <sub storage description>,
 *         ...
 *     ]
 * }
 * Index file will contain
 * {
 *   "_id": "ipost_indices.json",
 *   "indexA": {
 *       "keyword_abc": ["some_id","some_other_id",...]
 *   },
 *   "indexAB": {
 *       "keyword_abc": ["some_id"],
 *       "keyword_def": ["some_id"]
 *   }
 * }
 */
Sven Franck's avatar
Sven Franck committed
30
jIO.addStorageType('indexed', function (spec, my) {
31 32

  "use strict";
Sven Franck's avatar
Sven Franck committed
33
  var that, priv = {};
34

Sven Franck's avatar
Sven Franck committed
35
  spec = spec || {};
36
  that = my.basicStorage(spec, my);
Sven Franck's avatar
Sven Franck committed
37

38 39 40 41 42
  priv.indices = spec.indices;
  priv.substorage_key = "sub_storage";
  priv.substorage = spec[priv.substorage_key];
  priv.index_indicator = spec.sub_storage.application_name || "index";
  priv.index_suffix = priv.index_indicator + "_indices.json";
Sven Franck's avatar
Sven Franck committed
43

44
  my.env = my.env || spec.env || {};
Sven Franck's avatar
Sven Franck committed
45

46 47 48 49
  that.specToStore = function () {
    var o = {};
    o[priv.substorage_key] = priv.substorage;
    o.env = my.env;
Sven Franck's avatar
Sven Franck committed
50 51 52
    return o;
  };

53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
  /**
   * Generate a new uuid
   * @method generateUuid
   * @return {string} The new uuid
   */
  priv.generateUuid = function () {
    var S4 = function () {
      var i, string = Math.floor(
        Math.random() * 0x10000 /* 65536 */
      ).toString(16);
      for (i = string.length; i < 4; i += 1) {
        string = "0" + string;
      }
      return string;
    };
    return S4() + S4() + "-" +
      S4() + "-" +
      S4() + "-" +
      S4() + "-" +
      S4() + S4() + S4();
  };

  /**
  * Get number of elements in object
  * @method getObjectSize
  * @param  {object} obj The object to check
  * @return {number} size The amount of elements in the object
  */
  priv.getObjectSize = function (obj) {
    var size = 0, key;
    for (key in obj) {
Sven Franck's avatar
Sven Franck committed
84 85 86
      if (obj.hasOwnProperty(key)) {
        size += 1;
      }
Sven Franck's avatar
Sven Franck committed
87
    }
88
    return size;
Sven Franck's avatar
Sven Franck committed
89 90
  };

91 92 93 94 95 96 97
  /**
   * Creates an empty indices array
   * @method createEmptyIndexArray
   * @param  {array} indices An array of indices (optional)
   * @return {object} The new index array
   */
  priv.createEmptyIndexArray = function (indices) {
Sven Franck's avatar
Sven Franck committed
98
    var i, j = priv.indices.length,
99 100 101 102
      new_index_object = {}, new_index_name;

    if (indices === undefined) {
      for (i = 0; i < j; i += 1) {
Sven Franck's avatar
Sven Franck committed
103
        new_index_name = priv.indices[i].name;
104 105
        new_index_object[new_index_name] = {};
      }
Sven Franck's avatar
Sven Franck committed
106
    }
107 108 109
    return new_index_object;
  };

110 111 112 113 114 115 116 117 118 119 120 121
  /**
   * Determine if a key/value pair exists in an object by VALUE
   * @method searchObjectByValue
   * @param  {object} indexToSearch The index to search
   * @param  {string} docid The document id to find
   * @param  {string} passback The value that should be returned
   * @return {boolean} true/false
   */
  priv.searchIndexByValue = function (indexToSearch, docid, passback) {
    var key, obj, prop;

    for (key in indexToSearch) {
Sven Franck's avatar
Sven Franck committed
122
      if (indexToSearch.hasOwnProperty(key)) {
123 124 125 126 127 128 129
        obj = indexToSearch[key];
        for (prop in obj) {
          if (obj[prop] === docid) {
            return passback === "bool" ? true : key;
          }
        }
      }
Sven Franck's avatar
Sven Franck committed
130
    }
131
    return false;
Sven Franck's avatar
Sven Franck committed
132
  };
133

134 135
  /**
   * Find id in indices
136
   * @method isDocidInIndex
137 138 139 140
   * @param  {object} indices The file containing the indeces
   * @param  {object} doc The document which should be added to the index
   * @return {boolean} true/false
   */
141 142
  priv.isDocidInIndex = function (indices, doc) {
    var index, i, l = priv.indices.length;
143 144 145

    // loop indices
    for (i = 0; i < l; i += 1) {
146 147
      index = {};
      index.reference = priv.indices[i];
Sven Franck's avatar
Sven Franck committed
148
      index.name = index.reference.name;
149
      index.size = priv.getObjectSize(indices[index.name]);
150

151 152
      if (index.size > 0) {
        if (priv.searchIndexByValue(indices[index.name], doc._id, "bool")) {
Sven Franck's avatar
Sven Franck committed
153 154
          return true;
        }
Sven Franck's avatar
Sven Franck committed
155 156
      }
    }
157
    return false;
Sven Franck's avatar
Sven Franck committed
158 159 160 161 162 163 164 165 166
  };

  /**
   * Clean up indexes when removing a file
   * @method cleanIndices
   * @param  {object} indices The file containing the indeces
   * @param  {object} doc The document which should be added to the index
   * @return {object} indices The cleaned up file
   */
167
  priv.cleanIndices = function (indices, doc) {
Sven Franck's avatar
Sven Franck committed
168
    var i, j, k, index, key, l = priv.indices.length;
169 170 171 172 173 174

    // loop indices (indexA, indexAB...)
    for (i = 0; i < l; i += 1) {
      // index object (reference and current-iteration)
      index = {};
      index.reference = priv.indices[i];
Sven Franck's avatar
Sven Franck committed
175
      index.current = indices[index.reference.name];
176 177
      index.current_size = priv.getObjectSize(index.current);

Sven Franck's avatar
Sven Franck committed
178
      for (j = 0; j < index.current_size; j += 1) {
179 180 181 182 183 184
        key = priv.searchIndexByValue(index.current, doc._id, "key");
        index.result_array = index.current[key];
        if (!!key) {
          // if there is more than one docid in the result array,
          // just remove this one and not the whole array
          if (index.result_array.length > 1) {
Sven Franck's avatar
Sven Franck committed
185
            index.result_array.splice(k, 1);
186 187 188 189 190 191 192
          } else {
            delete index.current[key];
          }
        }
      }
    }
    return indices;
Sven Franck's avatar
Sven Franck committed
193
  };
194 195 196 197 198 199
  /**
   * Adds entries to indices
   * @method createEmptyIndexArray
   * @param  {object} indices The file containing the indeces
   * @param  {object} doc The document which should be added to the index
   */
200
  priv.updateIndices = function (indices, doc) {
Sven Franck's avatar
Sven Franck committed
201
    var i, j, k, m, index, value, label, key, l = priv.indices.length;
202 203 204

    // loop indices
    for (i = 0; i < l; i += 1) {
205 206 207 208 209
      // index object (reference and current iteration)
      index = {};
      index.reference = priv.indices[i];
      index.reference_size = index.reference.fields.length;
      index.field_array = [];
Sven Franck's avatar
Sven Franck committed
210
      index.current = indices[index.reference.name];
211
      index.current_size = priv.getObjectSize(index.current);
212

213 214
      // build array of values to create entries in index
      for (j = 0; j < index.reference_size; j += 1) {
Sven Franck's avatar
Sven Franck committed
215
        label = index.reference.fields[j];
216
        value = doc[label];
217
        if (value !== undefined) {
218 219 220 221 222 223
          // add a new entry
          index.field_array.push(value);

          // remove existing entries with same docid
          // because items are stored as "keyword:id" pairs this is tricky
          if (index.current_size > 0) {
Sven Franck's avatar
Sven Franck committed
224 225
            key = priv.searchIndexByValue(indices[index.reference.name],
              doc._id, "key");
226 227 228 229
            if (!!key) {
              delete index.current[key];
            }
          }
230 231
        }
      }
232 233 234 235 236 237 238 239
      // create keyword entries
      if (index.current !== undefined) {
        m = index.field_array.length;
        if (m) {
          for (k = 0; k < m; k += 1) {
            index.current_keyword = [index.field_array[k]];
            if (index.current[index.current_keyword] === undefined) {
              index.current[index.current_keyword] = [];
240
            }
241
            index.current[index.current_keyword].push(doc._id);
242 243 244 245 246
          }
        }
      }
    }
    return indices;
Sven Franck's avatar
Sven Franck committed
247 248
  };

Sven Franck's avatar
Sven Franck committed
249 250 251
  priv.getDocContent = function () {

  };
252 253 254 255 256 257 258 259 260
  /**
   * Build the alldocs response from the index file (overriding substorage)
   * @method allDocsResponseFromIndex
   * @param  {object} command The JIO command
   * @param  {boolean} include_docs Whether to also supply the document
   * @param  {object} option The options set for this method
   * @returns {object} response The allDocs response
   */
  priv.allDocsResponseFromIndex = function (indices, include_docs, option) {
Sven Franck's avatar
Sven Franck committed
261
    var i, j, k, m, n = 0, l = priv.indices.length,
262
      index, key, obj, prop, found, file,
Sven Franck's avatar
Sven Franck committed
263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285
      unique_count = 0, unique_docids = [], all_doc_response = {},
      success = function (content) {
        file = { value: {} };
        file.id = unique_docids[n];
        file.key = unique_docids[n];
        file.doc = content;
        all_doc_response.rows.push(file);
        // async counter, must be in callback
        n += 1;
        if (n === unique_count) {
          that.success(all_doc_response);
        }
      },
      error = function () {
        that.error({
          "status": 404,
          "statusText": "Not Found",
          "error": "not_found",
          "message": "Cannot find the document",
          "reason": "Cannot get a document from substorage"
        });
        return;
      };
286 287 288 289 290

    // loop indices
    for (i = 0; i < l; i += 1) {
      index = {};
      index.reference = priv.indices[i];
Sven Franck's avatar
Sven Franck committed
291
      index.current = indices[index.reference.name];
292 293 294 295 296
      index.current_size = priv.getObjectSize(index.current);

      // a lot of loops, not sure this is the fastest way
      for (j = 0; j < index.current_size; j += 1) {
        for (key in index.current) {
Sven Franck's avatar
Sven Franck committed
297 298 299 300 301 302 303 304 305 306 307 308 309 310
          if (index.current.hasOwnProperty(key)) {
            obj = index.current[key];
            for (prop in obj) {
              if (obj.hasOwnProperty(prop)) {
                for (k = 0; k < unique_docids.length; k += 1) {
                  if (obj[prop] === unique_docids[k]) {
                    found = true;
                    break;
                  }
                }
                if (!found) {
                  unique_docids.push(obj[prop]);
                  unique_count += 1;
                }
311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327
              }
            }
          }
        }
      }
    }
    // construct allDocs response
    all_doc_response.total_rows = unique_count;
    all_doc_response.rows = [];
    for (m = 0; m < unique_count; m += 1) {
      // include_docs
      if (include_docs) {
        that.addJob(
          "get",
          priv.substorage,
          unique_docids[m],
          option,
Sven Franck's avatar
Sven Franck committed
328 329
          success,
          error
330 331 332 333 334 335
        );
      } else {
        file = { value: {} };
        file.id = unique_docids[m];
        file.key = unique_docids[m];
        all_doc_response.rows.push(file);
Sven Franck's avatar
Sven Franck committed
336
        if (m === (unique_count - 1)) {
337 338 339 340 341 342
          return all_doc_response;
        }
      }
    }
  };

343 344 345 346
  /**
   * Post document to substorage and create/update index file(s)
   * @method post
   * @param  {object} command The JIO command
347
   * @param  {string} source The source of the function call
348
   */
349
  priv.postOrput = function (command, source) {
350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373
    var f = {}, indices, doc, docid;
    doc = command.cloneDoc();
    docid = command.getDocId();
    if (typeof docid !== "string") {
      doc._id = priv.generateUuid();
      docid = doc._id;
    }
    f.getIndices = function () {
      var option = command.cloneOption();
      if (option.max_retry === 0) {
        option.max_retry = 3;
      }
      that.addJob(
        "get",
        priv.substorage,
        priv.index_suffix,
        option,
        function (response) {
          indices = response;
          f.postDocument("put");
        },
        function (err) {
          switch (err.status) {
          case 404:
374 375 376 377 378 379 380 381 382 383 384
            if (source !== 'PUTATTACHMENT') {
              indices = priv.createEmptyIndexArray();
              f.postDocument("post");
            } else {
              that.error({
                "status": 404,
                "statusText": "Not Found",
                "error": "not found",
                "message": "Document not found",
                "reason": "Document not found"
              });
Sven Franck's avatar
Sven Franck committed
385
              return;
386
            }
387 388 389 390 391 392 393 394 395 396
            break;
          default:
            err.message = "Cannot retrieve index array";
            that.error(err);
            break;
          }
        }
      );
    };
    f.postDocument = function (index_update_method) {
397
      if (priv.isDocidInIndex(indices, doc) && source === 'POST') {
398 399 400 401 402 403 404 405 406
        // POST the document already exists
        that.error({
          "status": 409,
          "statusText": "Conflicts",
          "error": "conflicts",
          "message": "Cannot create a new document",
          "reason": "Document already exists"
        });
        return;
Sven Franck's avatar
Sven Franck committed
407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430
      }
      if (source !== 'PUTATTACHMENT') {
        indices = priv.updateIndices(indices, doc);
      }
      that.addJob(
        source === 'PUTATTACHMENT' ? "putAttachment" : "post",
        priv.substorage,
        doc,
        command.cloneOption(),
        function () {
          if (source !== 'PUTATTACHMENT') {
            f.sendIndices(index_update_method);
          } else {
            docid = docid + '/' + command.getAttachmentId();
            that.success({
              "ok": true,
              "id": docid
            });
          }
        },
        function (err) {
          switch (err.status) {
          case 409:
            // file already exists
431 432 433 434 435 436 437 438
            if (source !== 'PUTATTACHMENT') {
              f.sendIndices(index_update_method);
            } else {
              that.success({
                "ok": true,
                "id": docid
              });
            }
Sven Franck's avatar
Sven Franck committed
439 440 441 442 443
            break;
          default:
            err.message = "Cannot upload document";
            that.error(err);
            break;
444
          }
Sven Franck's avatar
Sven Franck committed
445 446
        }
      );
447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468
    };
    f.sendIndices = function (method) {
      indices._id = priv.index_suffix;
      that.addJob(
        method,
        priv.substorage,
        indices,
        command.cloneOption(),
        function () {
          that.success({
            "ok": true,
            "id": docid
          });
        },
        function (err) {
          // xxx do we try to delete the posted document ?
          err.message = "Cannot save index file";
          that.error(err);
        }
      );
    };
    f.getIndices();
Sven Franck's avatar
Sven Franck committed
469 470
  };

471 472 473 474 475
  /**
   * Update the document metadata and update the index
   * @method put
   * @param  {object} command The JIO command
   */
476 477
  that.post = function (command) {
    priv.postOrput(command, 'POST');
478
  };
479

480
  /**
481 482 483 484 485 486
   * Update the document metadata and update the index
   * @method put
   * @param  {object} command The JIO command
   */
  that.put = function (command) {
    priv.postOrput(command, 'PUT');
Sven Franck's avatar
Sven Franck committed
487
  };
488

489
  /**
490 491 492 493 494 495
   * Add an attachment to a document (no index modification)
   * @method putAttachment
   * @param  {object} command The JIO command
   */
  that.putAttachment = function (command) {
    priv.postOrput(command, 'PUTATTACHMENT');
Sven Franck's avatar
Sven Franck committed
496
  };
497

498
  /**
499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516
   * Get the document metadata or attachment.
   * Options:
   * - {boolean} revs Add simple revision history (false by default).
   * - {boolean} revs_info Add revs info (false by default).
   * - {boolean} conflicts Add conflict object (false by default).
   * @method get
   * @param  {object} command The JIO command
   */
  that.get = function (command) {
    var option, docid;
    option = command.cloneOption();
    if (option.max_retry === 0) {
      option.max_retry = 3;
    }
    if (command.getAttachmentId() !== undefined) {
      docid = command.getDocId() + '/' + command.getAttachmentId();
    } else {
      docid = command.getDocId();
Sven Franck's avatar
Sven Franck committed
517
    }
518
    that.addJob(
519 520 521 522
      "get",
      priv.substorage,
      docid,
      option,
523 524
      function (response) {
        that.success(response);
Sven Franck's avatar
Sven Franck committed
525
      },
Sven Franck's avatar
Sven Franck committed
526
      function () {
527 528 529 530 531 532 533
        that.error({
          "status": 404,
          "statusText": "Not Found",
          "error": "not_found",
          "message": "Cannot find the attachment",
          "reason": "Document/Attachment not found"
        });
534 535 536
      }
    );
  };
537 538 539

  /**
   * Remove document or attachment - removing documents updates index!.
540
   * @method remove
541 542
   * @param  {object} command The JIO command
   */
543
  that.remove = function (command) {
544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565
    var f = {}, indices, doc, docid, option;

    doc = command.cloneDoc();
    option = command.cloneOption();
    if (option.max_retry === 0) {
      option.max_retry = 3;
    }

    f.removeDocument = function (type) {
      if (type === 'doc') {
        docid = command.getDocId();
      } else {
        docid = command.getDocId() + '/' + command.getAttachmentId();
      }
      that.addJob(
        "remove",
        priv.substorage,
        docid,
        option,
        function (response) {
          that.success(response);
        },
Sven Franck's avatar
Sven Franck committed
566
        function () {
567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584
          that.error({
            "status": 409,
            "statusText": "Conflict",
            "error": "conflict",
            "message": "Document Update Conflict",
            "reason": "Could not delete document or attachment"
          });
        }
      );
    };
    f.getIndices = function () {
      that.addJob(
        "get",
        priv.substorage,
        priv.index_suffix,
        option,
        function (response) {
          // if deleting an attachment
Sven Franck's avatar
Sven Franck committed
585 586
          if (typeof command.getAttachmentId() === 'string') {
            f.removeDocument('attachment');
587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605
          } else {
            indices = priv.cleanIndices(response, doc);
            // store update index file
            that.addJob(
              "put",
              priv.substorage,
              indices,
              command.cloneOption(),
              function () {
                // remove actual document
                f.removeDocument('doc');
              },
              function (err) {
                err.message = "Cannot save index file";
                that.error(err);
              }
            );
          }
        },
Sven Franck's avatar
Sven Franck committed
606
        function () {
607 608 609 610 611 612 613
          that.error({
            "status": 404,
            "statusText": "Not Found",
            "error": "not_found",
            "message": "Document index not found, please check document ID",
            "reason": "Incorrect document ID"
          });
Sven Franck's avatar
Sven Franck committed
614
          return;
615 616 617 618
        }
      );
    };
    f.getIndices();
619
  };
620

621 622 623 624
  /**
   * Gets a document list from the substorage
   * Options:
   * - {boolean} include_docs Also retrieve the actual document content.
Sven Franck's avatar
Sven Franck committed
625
   * @method allDocs
626
   * @param  {object} command The JIO command
Sven Franck's avatar
Sven Franck committed
627
   */
628 629 630 631 632 633 634 635 636 637 638 639
  //{
  // "total_rows": 4,
  // "rows": [
  //    {
  //    "id": "otherdoc",
  //    "key": "otherdoc",
  //    "value": {
  //      "rev": "1-3753476B70A49EA4D8C9039E7B04254C"
  //    }
  //  },{...}
  // ]
  //}
Sven Franck's avatar
Sven Franck committed
640
  that.allDocs = function (command) {
Sven Franck's avatar
Sven Franck committed
641
    var f = {}, option, all_docs_response;
642 643 644
    option = command.cloneOption();
    if (option.max_retry === 0) {
      option.max_retry = 3;
Sven Franck's avatar
Sven Franck committed
645
    }
646 647 648 649 650 651 652 653 654 655 656 657 658

    f.getIndices = function () {
      that.addJob(
        "get",
        priv.substorage,
        priv.index_suffix,
        option,
        function (response) {
          if (command.getOption('include_docs')) {
            priv.allDocsResponseFromIndex(response, true, option);
          } else {
            all_docs_response =
              priv.allDocsResponseFromIndex(response, false, option);
Sven Franck's avatar
Sven Franck committed
659
            that.success(all_docs_response);
660 661
          }
        },
Sven Franck's avatar
Sven Franck committed
662
        function () {
663 664 665 666 667 668 669
          that.error({
            "status": 404,
            "statusText": "Not Found",
            "error": "not_found",
            "message": "Document index not found",
            "reason": "There are no documents in the storage"
          });
Sven Franck's avatar
Sven Franck committed
670
          return;
671 672 673 674 675
        }
      );
    };
    f.getIndices();
  };
Sven Franck's avatar
Sven Franck committed
676
  return that;
677
});