UNPKG

kinto

Version:

An Offline-First JavaScript client for Kinto.

2,895 lines 91.2 kB
"use strict";

var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.recordsEqual = recordsEqual;
exports.createKeyValueStoreIdSchema = createKeyValueStoreIdSchema;
exports.CollectionTransaction = exports.default = exports.ServerWasFlushedError = exports.SyncResultObject = void 0;

var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator"));

var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator"));

var _typeof2 = _interopRequireDefault(require("@babel/runtime/helpers/typeof"));

var _objectSpread2 = _interopRequireDefault(require("@babel/runtime/helpers/objectSpread"));

var _possibleConstructorReturn2 = _interopRequireDefault(require("@babel/runtime/helpers/possibleConstructorReturn"));

var _getPrototypeOf2 = _interopRequireDefault(require("@babel/runtime/helpers/getPrototypeOf"));

var _inherits2 = _interopRequireDefault(require("@babel/runtime/helpers/inherits"));

var _assertThisInitialized2 = _interopRequireDefault(require("@babel/runtime/helpers/assertThisInitialized"));

var _wrapNativeSuper2 = _interopRequireDefault(require("@babel/runtime/helpers/wrapNativeSuper"));

var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));

var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));

var _base = _interopRequireDefault(require("./adapters/base"));

var _IDB = _interopRequireDefault(require("./adapters/IDB"));

var _utils = require("./utils");

var _uuid = require("uuid");

var RECORD_FIELDS_TO_CLEAN = ["_status"];
var AVAILABLE_HOOKS = ["incoming-changes"];
var IMPORT_CHUNK_SIZE = 200;
/**
 * Compare two records omitting local fields and synchronization
 * attributes (like _status and last_modified)
 * @param {Object} a    A record to compare.
 * @param {Object} b    A record to compare.
 * @param {Array} localFields Additional fields to ignore during the comparison
 * @return {boolean}
 */

function recordsEqual(a, b) {
  var localFields = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : [];
  var fieldsToClean = RECORD_FIELDS_TO_CLEAN.concat(["last_modified"]).concat(localFields);

  var cleanLocal = function cleanLocal(r) {
    return (0, _utils.omitKeys)(r, fieldsToClean);
  };

  return (0, _utils.deepEqual)(cleanLocal(a), cleanLocal(b));
}
/**
 * Synchronization result object.
 */


var SyncResultObject =
/*#__PURE__*/
function () {
  /**
   * Public constructor.
   */
  function SyncResultObject() {
    var _this = this;

    (0, _classCallCheck2.default)(this, SyncResultObject);

    /**
     * Current synchronization result status; becomes `false` when conflicts or
     * errors are registered.
     * @type {Boolean}
     */
    this.lastModified = null;
    this._lists = {};
    ["errors", "created", "updated", "deleted", "published", "conflicts", "skipped", "resolved", "void"].forEach(function (l) {
      return _this._lists[l] = [];
    });
    this._cached = {};
  }
  /**
   * Adds entries for a given result type.
   *
   * @param {String} type    The result type.
   * @param {Array}  entries The result entries.
   * @return {SyncResultObject}
   */


  (0, _createClass2.default)(SyncResultObject, [{
    key: "add",
    value: function add(type, entries) {
      if (!Array.isArray(this._lists[type])) {
        console.warn("Unknown type \"".concat(type, "\""));
        return;
      }

      if (!Array.isArray(entries)) {
        entries = [entries];
      }

      this._lists[type] = this._lists[type].concat(entries);
      delete this._cached[type];
      return this;
    }
  }, {
    key: "_deduplicate",
    value: function _deduplicate(list) {
      if (!(list in this._cached)) {
        // Deduplicate entries by id. If the values don't have `id` attribute, just
        // keep all.
        var recordsWithoutId = new Set();
        var recordsById = new Map();

        this._lists[list].forEach(function (record) {
          if (!record.id) {
            recordsWithoutId.add(record);
          } else {
            recordsById.set(record.id, record);
          }
        });

        this._cached[list] = Array.from(recordsById.values()).concat(Array.from(recordsWithoutId));
      }

      return this._cached[list];
    }
    /**
     * Reinitializes result entries for a given result type.
     *
     * @param  {String} type The result type.
     * @return {SyncResultObject}
     */

  }, {
    key: "reset",
    value: function reset(type) {
      this._lists[type] = [];
      delete this._cached[type];
      return this;
    }
  }, {
    key: "toObject",
    value: function toObject() {
      // Only used in tests.
      return {
        ok: this.ok,
        lastModified: this.lastModified,
        errors: this.errors,
        created: this.created,
        updated: this.updated,
        deleted: this.deleted,
        skipped: this.skipped,
        published: this.published,
        conflicts: this.conflicts,
        resolved: this.resolved
      };
    }
  }, {
    key: "ok",
    get: function get() {
      return this.errors.length + this.conflicts.length === 0;
    }
  }, {
    key: "errors",
    get: function get() {
      return this._lists["errors"];
    }
  }, {
    key: "conflicts",
    get: function get() {
      return this._lists["conflicts"];
    }
  }, {
    key: "skipped",
    get: function get() {
      return this._deduplicate("skipped");
    }
  }, {
    key: "resolved",
    get: function get() {
      return this._deduplicate("resolved");
    }
  }, {
    key: "created",
    get: function get() {
      return this._deduplicate("created");
    }
  }, {
    key: "updated",
    get: function get() {
      return this._deduplicate("updated");
    }
  }, {
    key: "deleted",
    get: function get() {
      return this._deduplicate("deleted");
    }
  }, {
    key: "published",
    get: function get() {
      return this._deduplicate("published");
    }
  }]);
  return SyncResultObject;
}();

exports.SyncResultObject = SyncResultObject;

var ServerWasFlushedError =
/*#__PURE__*/
function (_Error) {
  (0, _inherits2.default)(ServerWasFlushedError, _Error);

  function ServerWasFlushedError(clientTimestamp, serverTimestamp, message) {
    var _this2;

    (0, _classCallCheck2.default)(this, ServerWasFlushedError);
    _this2 = (0, _possibleConstructorReturn2.default)(this, (0, _getPrototypeOf2.default)(ServerWasFlushedError).call(this, message));

    if (Error.captureStackTrace) {
      Error.captureStackTrace((0, _assertThisInitialized2.default)((0, _assertThisInitialized2.default)(_this2)), ServerWasFlushedError);
    }

    _this2.clientTimestamp = clientTimestamp;
    _this2.serverTimestamp = serverTimestamp;
    return _this2;
  }

  return ServerWasFlushedError;
}((0, _wrapNativeSuper2.default)(Error));

exports.ServerWasFlushedError = ServerWasFlushedError;

function createUUIDSchema() {
  return {
    generate: function generate() {
      return (0, _uuid.v4)();
    },
    validate: function validate(id) {
      return typeof id == "string" && _utils.RE_RECORD_ID.test(id);
    }
  };
}
/**
 * IDSchema for when using kinto.js as a key-value store.
 * Using this IDSchema requires you to set a property as the id.
 * This will be the property used to retrieve this record.
 *
 * @example
 * const exampleCollection = db.collection("example", { idSchema: createKeyValueStoreIdSchema() })
 * await exampleCollection.create({ title: "How to tie a tie", favoriteColor: "blue", id: "user123" }, { useRecordId: true })
 * await exampleCollection.getAny("user123")
 */


function createKeyValueStoreIdSchema() {
  return {
    generate: function generate() {
      throw new Error("createKeyValueStoreIdSchema() does not generate an id");
    },
    validate: function validate() {
      return true;
    }
  };
}

function markStatus(record, status) {
  return (0, _objectSpread2.default)({}, record, {
    _status: status
  });
}

function markDeleted(record) {
  return markStatus(record, "deleted");
}

function markSynced(record) {
  return markStatus(record, "synced");
}
/**
 * Import a remote change into the local database.
 *
 * @param  {IDBTransactionProxy} transaction The transaction handler.
 * @param  {Object}              remote      The remote change object to import.
 * @param  {Array<String>}       localFields The list of fields that remain local.
 * @param  {String}              strategy    The {@link Collection.strategy}.
 * @return {Object}
 */


function importChange(transaction, remote, localFields, strategy) {
  var local = transaction.get(remote.id);

  if (!local) {
    // Not found locally but remote change is marked as deleted; skip to
    // avoid recreation.
    if (remote.deleted) {
      return {
        type: "skipped",
        data: remote
      };
    }

    var _synced = markSynced(remote);

    transaction.create(_synced);
    return {
      type: "created",
      data: _synced
    };
  } // Apply remote changes on local record.


  var synced = (0, _objectSpread2.default)({}, local, markSynced(remote)); // With pull only, we don't need to compare records since we override them.

  if (strategy === Collection.strategy.PULL_ONLY) {
    if (remote.deleted) {
      transaction.delete(remote.id);
      return {
        type: "deleted",
        data: local
      };
    }

    transaction.update(synced);
    return {
      type: "updated",
      data: {
        old: local,
        new: synced
      }
    };
  } // With other sync strategies, we detect conflicts,
  // by comparing local and remote, ignoring local fields.


  var isIdentical = recordsEqual(local, remote, localFields); // Detect or ignore conflicts if record has also been modified locally.

  if (local._status !== "synced") {
    // Locally deleted, unsynced: scheduled for remote deletion.
    if (local._status === "deleted") {
      return {
        type: "skipped",
        data: local
      };
    }

    if (isIdentical) {
      // If records are identical, import anyway, so we bump the
      // local last_modified value from the server and set record
      // status to "synced".
      transaction.update(synced);
      return {
        type: "updated",
        data: {
          old: local,
          new: synced
        }
      };
    }

    if (local.last_modified !== undefined && local.last_modified === remote.last_modified) {
      // If our local version has the same last_modified as the remote
      // one, this represents an object that corresponds to a resolved
      // conflict. Our local version represents the final output, so
      // we keep that one. (No transaction operation to do.)
      // But if our last_modified is undefined,
      // that means we've created the same object locally as one on
      // the server, which *must* be a conflict.
      return {
        type: "void"
      };
    }

    return {
      type: "conflicts",
      data: {
        type: "incoming",
        local: local,
        remote: remote
      }
    };
  } // Local record was synced.


  if (remote.deleted) {
    transaction.delete(remote.id);
    return {
      type: "deleted",
      data: local
    };
  } // Import locally.


  transaction.update(synced); // if identical, simply exclude it from all SyncResultObject lists

  var type = isIdentical ? "void" : "updated";
  return {
    type: type,
    data: {
      old: local,
      new: synced
    }
  };
}
/**
 * Abstracts a collection of records stored in the local database, providing
 * CRUD operations and synchronization helpers.
 */


var Collection =
/*#__PURE__*/
function () {
  /**
   * Constructor.
   *
   * Options:
   * - `{BaseAdapter} adapter` The DB adapter (default: `IDB`)
   *
   * @param  {String} bucket  The bucket identifier.
   * @param  {String} name    The collection name.
   * @param  {Api}    api     The Api instance.
   * @param  {Object} options The options object.
   */
  function Collection(bucket, name, api) {
    var options = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {};
    (0, _classCallCheck2.default)(this, Collection);
    this._bucket = bucket;
    this._name = name;
    this._lastModified = null;
    var DBAdapter = options.adapter || _IDB.default;

    if (!DBAdapter) {
      throw new Error("No adapter provided");
    }

    var db = new DBAdapter("".concat(bucket, "/").concat(name), options.adapterOptions);

    if (!(db instanceof _base.default)) {
      throw new Error("Unsupported adapter.");
    } // public properties

    /**
     * The db adapter instance
     * @type {BaseAdapter}
     */


    this.db = db;
    /**
     * The Api instance.
     * @type {KintoClient}
     */

    this.api = api;
    /**
     * The event emitter instance.
     * @type {EventEmitter}
     */

    this.events = options.events;
    /**
     * The IdSchema instance.
     * @type {Object}
     */

    this.idSchema = this._validateIdSchema(options.idSchema);
    /**
     * The list of remote transformers.
     * @type {Array}
     */

    this.remoteTransformers = this._validateRemoteTransformers(options.remoteTransformers);
    /**
     * The list of hooks.
     * @type {Object}
     */

    this.hooks = this._validateHooks(options.hooks);
    /**
     * The list of fields names that will remain local.
     * @type {Array}
     */

    this.localFields = options.localFields || [];
  }
  /**
   * The collection name.
   * @type {String}
   */


  (0, _createClass2.default)(Collection, [{
    key: "_validateIdSchema",

    /**
     * Validates an idSchema.
     *
     * @param  {Object|undefined} idSchema
     * @return {Object}
     */
    value: function _validateIdSchema(idSchema) {
      if (typeof idSchema === "undefined") {
        return createUUIDSchema();
      }

      if ((0, _typeof2.default)(idSchema) !== "object") {
        throw new Error("idSchema must be an object.");
      } else if (typeof idSchema.generate !== "function") {
        throw new Error("idSchema must provide a generate function.");
      } else if (typeof idSchema.validate !== "function") {
        throw new Error("idSchema must provide a validate function.");
      }

      return idSchema;
    }
    /**
     * Validates a list of remote transformers.
     *
     * @param  {Array|undefined} remoteTransformers
     * @return {Array}
     */

  }, {
    key: "_validateRemoteTransformers",
    value: function _validateRemoteTransformers(remoteTransformers) {
      if (typeof remoteTransformers === "undefined") {
        return [];
      }

      if (!Array.isArray(remoteTransformers)) {
        throw new Error("remoteTransformers should be an array.");
      }

      return remoteTransformers.map(function (transformer) {
        if ((0, _typeof2.default)(transformer) !== "object") {
          throw new Error("A transformer must be an object.");
        } else if (typeof transformer.encode !== "function") {
          throw new Error("A transformer must provide an encode function.");
        } else if (typeof transformer.decode !== "function") {
          throw new Error("A transformer must provide a decode function.");
        }

        return transformer;
      });
    }
    /**
     * Validate the passed hook is correct.
     *
     * @param {Array|undefined} hook.
     * @return {Array}
     **/

  }, {
    key: "_validateHook",
    value: function _validateHook(hook) {
      if (!Array.isArray(hook)) {
        throw new Error("A hook definition should be an array of functions.");
      }

      return hook.map(function (fn) {
        if (typeof fn !== "function") {
          throw new Error("A hook definition should be an array of functions.");
        }

        return fn;
      });
    }
    /**
     * Validates a list of hooks.
     *
     * @param  {Object|undefined} hooks
     * @return {Object}
     */

  }, {
    key: "_validateHooks",
    value: function _validateHooks(hooks) {
      if (typeof hooks === "undefined") {
        return {};
      }

      if (Array.isArray(hooks)) {
        throw new Error("hooks should be an object, not an array.");
      }

      if ((0, _typeof2.default)(hooks) !== "object") {
        throw new Error("hooks should be an object.");
      }

      var validatedHooks = {};

      for (var hook in hooks) {
        if (!AVAILABLE_HOOKS.includes(hook)) {
          throw new Error("The hook should be one of " + AVAILABLE_HOOKS.join(", "));
        }

        validatedHooks[hook] = this._validateHook(hooks[hook]);
      }

      return validatedHooks;
    }
    /**
     * Deletes every records in the current collection and marks the collection as
     * never synced.
     *
     * @return {Promise}
     */

  }, {
    key: "clear",
    value: function () {
      var _clear = (0, _asyncToGenerator2.default)(
      /*#__PURE__*/
      _regenerator.default.mark(function _callee() {
        return _regenerator.default.wrap(function _callee$(_context) {
          while (1) {
            switch (_context.prev = _context.next) {
              case 0:
                _context.next = 2;
                return this.db.clear();

              case 2:
                _context.next = 4;
                return this.db.saveMetadata(null);

              case 4:
                _context.next = 6;
                return this.db.saveLastModified(null);

              case 6:
                return _context.abrupt("return", {
                  data: [],
                  permissions: {}
                });

              case 7:
              case "end":
                return _context.stop();
            }
          }
        }, _callee, this);
      }));

      return function clear() {
        return _clear.apply(this, arguments);
      };
    }()
    /**
     * Encodes a record.
     *
     * @param  {String} type   Either "remote" or "local".
     * @param  {Object} record The record object to encode.
     * @return {Promise}
     */

  }, {
    key: "_encodeRecord",
    value: function _encodeRecord(type, record) {
      if (!this["".concat(type, "Transformers")].length) {
        return Promise.resolve(record);
      }

      return (0, _utils.waterfall)(this["".concat(type, "Transformers")].map(function (transformer) {
        return function (record) {
          return transformer.encode(record);
        };
      }), record);
    }
    /**
     * Decodes a record.
     *
     * @param  {String} type   Either "remote" or "local".
     * @param  {Object} record The record object to decode.
     * @return {Promise}
     */

  }, {
    key: "_decodeRecord",
    value: function _decodeRecord(type, record) {
      if (!this["".concat(type, "Transformers")].length) {
        return Promise.resolve(record);
      }

      return (0, _utils.waterfall)(this["".concat(type, "Transformers")].reverse().map(function (transformer) {
        return function (record) {
          return transformer.decode(record);
        };
      }), record);
    }
    /**
     * Adds a record to the local database, asserting that none
     * already exist with this ID.
     *
     * Note: If either the `useRecordId` or `synced` options are true, then the
     * record object must contain the id field to be validated. If none of these
     * options are true, an id is generated using the current IdSchema; in this
     * case, the record passed must not have an id.
     *
     * Options:
     * - {Boolean} synced       Sets record status to "synced" (default: `false`).
     * - {Boolean} useRecordId  Forces the `id` field from the record to be used,
     *                          instead of one that is generated automatically
     *                          (default: `false`).
     *
     * @param  {Object} record
     * @param  {Object} options
     * @return {Promise}
     */

  }, {
    key: "create",
    value: function create(record) {
      var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {
        useRecordId: false,
        synced: false
      };

      // Validate the record and its ID (if any), even though this
      // validation is also done in the CollectionTransaction method,
      // because we need to pass the ID to preloadIds.
      var reject = function reject(msg) {
        return Promise.reject(new Error(msg));
      };

      if ((0, _typeof2.default)(record) !== "object") {
        return reject("Record is not an object.");
      }

      if ((options.synced || options.useRecordId) && !Object.prototype.hasOwnProperty.call(record, "id")) {
        return reject("Missing required Id; synced and useRecordId options require one");
      }

      if (!options.synced && !options.useRecordId && Object.prototype.hasOwnProperty.call(record, "id")) {
        return reject("Extraneous Id; can't create a record having one set.");
      }

      var newRecord = (0, _objectSpread2.default)({}, record, {
        id: options.synced || options.useRecordId ? record.id : this.idSchema.generate(record),
        _status: options.synced ? "synced" : "created"
      });

      if (!this.idSchema.validate(newRecord.id)) {
        return reject("Invalid Id: ".concat(newRecord.id));
      }

      return this.execute(function (txn) {
        return txn.create(newRecord);
      }, {
        preloadIds: [newRecord.id]
      }).catch(function (err) {
        if (options.useRecordId) {
          throw new Error("Couldn't create record. It may have been virtually deleted.");
        }

        throw err;
      });
    }
    /**
     * Like {@link CollectionTransaction#update}, but wrapped in its own transaction.
     *
     * Options:
     * - {Boolean} synced: Sets record status to "synced" (default: false)
     * - {Boolean} patch:  Extends the existing record instead of overwriting it
     *   (default: false)
     *
     * @param  {Object} record
     * @param  {Object} options
     * @return {Promise}
     */

  }, {
    key: "update",
    value: function update(record) {
      var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {
        synced: false,
        patch: false
      };

      // Validate the record and its ID, even though this validation is
      // also done in the CollectionTransaction method, because we need
      // to pass the ID to preloadIds.
      if ((0, _typeof2.default)(record) !== "object") {
        return Promise.reject(new Error("Record is not an object."));
      }

      if (!Object.prototype.hasOwnProperty.call(record, "id")) {
        return Promise.reject(new Error("Cannot update a record missing id."));
      }

      if (!this.idSchema.validate(record.id)) {
        return Promise.reject(new Error("Invalid Id: ".concat(record.id)));
      }

      return this.execute(function (txn) {
        return txn.update(record, options);
      }, {
        preloadIds: [record.id]
      });
    }
    /**
     * Like {@link CollectionTransaction#upsert}, but wrapped in its own transaction.
     *
     * @param  {Object} record
     * @return {Promise}
     */

  }, {
    key: "upsert",
    value: function upsert(record) {
      // Validate the record and its ID, even though this validation is
      // also done in the CollectionTransaction method, because we need
      // to pass the ID to preloadIds.
      if ((0, _typeof2.default)(record) !== "object") {
        return Promise.reject(new Error("Record is not an object."));
      }

      if (!Object.prototype.hasOwnProperty.call(record, "id")) {
        return Promise.reject(new Error("Cannot update a record missing id."));
      }

      if (!this.idSchema.validate(record.id)) {
        return Promise.reject(new Error("Invalid Id: ".concat(record.id)));
      }

      return this.execute(function (txn) {
        return txn.upsert(record);
      }, {
        preloadIds: [record.id]
      });
    }
    /**
     * Like {@link CollectionTransaction#get}, but wrapped in its own transaction.
     *
     * Options:
     * - {Boolean} includeDeleted: Include virtually deleted records.
     *
     * @param  {String} id
     * @param  {Object} options
     * @return {Promise}
     */

  }, {
    key: "get",
    value: function get(id) {
      var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {
        includeDeleted: false
      };
      return this.execute(function (txn) {
        return txn.get(id, options);
      }, {
        preloadIds: [id]
      });
    }
    /**
     * Like {@link CollectionTransaction#getAny}, but wrapped in its own transaction.
     *
     * @param  {String} id
     * @return {Promise}
     */

  }, {
    key: "getAny",
    value: function getAny(id) {
      return this.execute(function (txn) {
        return txn.getAny(id);
      }, {
        preloadIds: [id]
      });
    }
    /**
     * Same as {@link Collection#delete}, but wrapped in its own transaction.
     *
     * Options:
     * - {Boolean} virtual: When set to `true`, doesn't actually delete the record,
     *   update its `_status` attribute to `deleted` instead (default: true)
     *
     * @param  {String} id       The record's Id.
     * @param  {Object} options  The options object.
     * @return {Promise}
     */

  }, {
    key: "delete",
    value: function _delete(id) {
      var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {
        virtual: true
      };
      return this.execute(function (transaction) {
        return transaction.delete(id, options);
      }, {
        preloadIds: [id]
      });
    }
    /**
     * Same as {@link Collection#deleteAll}, but wrapped in its own transaction, execulding the parameter.
     *
     * @return {Promise}
     */

  }, {
    key: "deleteAll",
    value: function () {
      var _deleteAll = (0, _asyncToGenerator2.default)(
      /*#__PURE__*/
      _regenerator.default.mark(function _callee2() {
        var _ref, data, recordIds;

        return _regenerator.default.wrap(function _callee2$(_context2) {
          while (1) {
            switch (_context2.prev = _context2.next) {
              case 0:
                _context2.next = 2;
                return this.list({}, {
                  includeDeleted: false
                });

              case 2:
                _ref = _context2.sent;
                data = _ref.data;
                recordIds = data.map(function (record) {
                  return record.id;
                });
                return _context2.abrupt("return", this.execute(function (transaction) {
                  return transaction.deleteAll(recordIds);
                }, {
                  preloadIds: recordIds
                }));

              case 6:
              case "end":
                return _context2.stop();
            }
          }
        }, _callee2, this);
      }));

      return function deleteAll() {
        return _deleteAll.apply(this, arguments);
      };
    }()
    /**
     * The same as {@link CollectionTransaction#deleteAny}, but wrapped
     * in its own transaction.
     *
     * @param  {String} id       The record's Id.
     * @return {Promise}
     */

  }, {
    key: "deleteAny",
    value: function deleteAny(id) {
      return this.execute(function (txn) {
        return txn.deleteAny(id);
      }, {
        preloadIds: [id]
      });
    }
    /**
     * Lists records from the local database.
     *
     * Params:
     * - {Object} filters Filter the results (default: `{}`).
     * - {String} order   The order to apply   (default: `-last_modified`).
     *
     * Options:
     * - {Boolean} includeDeleted: Include virtually deleted records.
     *
     * @param  {Object} params  The filters and order to apply to the results.
     * @param  {Object} options The options object.
     * @return {Promise}
     */

  }, {
    key: "list",
    value: function () {
      var _list = (0, _asyncToGenerator2.default)(
      /*#__PURE__*/
      _regenerator.default.mark(function _callee3() {
        var params,
            options,
            results,
            data,
            _args3 = arguments;
        return _regenerator.default.wrap(function _callee3$(_context3) {
          while (1) {
            switch (_context3.prev = _context3.next) {
              case 0:
                params = _args3.length > 0 && _args3[0] !== undefined ? _args3[0] : {};
                options = _args3.length > 1 && _args3[1] !== undefined ? _args3[1] : {
                  includeDeleted: false
                };
                params = (0, _objectSpread2.default)({
                  order: "-last_modified",
                  filters: {}
                }, params);
                _context3.next = 5;
                return this.db.list(params);

              case 5:
                results = _context3.sent;
                data = results;

                if (!options.includeDeleted) {
                  data = results.filter(function (record) {
                    return record._status !== "deleted";
                  });
                }

                return _context3.abrupt("return", {
                  data: data,
                  permissions: {}
                });

              case 9:
              case "end":
                return _context3.stop();
            }
          }
        }, _callee3, this);
      }));

      return function list() {
        return _list.apply(this, arguments);
      };
    }()
    /**
     * Imports remote changes into the local database.
     * This method is in charge of detecting the conflicts, and resolve them
     * according to the specified strategy.
     * @param  {SyncResultObject} syncResultObject The sync result object.
     * @param  {Array}            decodedChanges   The list of changes to import in the local database.
     * @param  {String}           strategy         The {@link Collection.strategy} (default: MANUAL)
     * @return {Promise}
     */

  }, {
    key: "importChanges",
    value: function () {
      var _importChanges = (0, _asyncToGenerator2.default)(
      /*#__PURE__*/
      _regenerator.default.mark(function _callee4(syncResultObject, decodedChanges) {
        var _this3 = this;

        var strategy,
            _loop,
            i,
            data,
            _args5 = arguments;

        return _regenerator.default.wrap(function _callee4$(_context5) {
          while (1) {
            switch (_context5.prev = _context5.next) {
              case 0:
                strategy = _args5.length > 2 && _args5[2] !== undefined ? _args5[2] : Collection.strategy.MANUAL;
                _context5.prev = 1;
                _loop =
                /*#__PURE__*/
                _regenerator.default.mark(function _loop(i) {
                  var slice, _ref2, imports, resolved;

                  return _regenerator.default.wrap(function _loop$(_context4) {
                    while (1) {
                      switch (_context4.prev = _context4.next) {
                        case 0:
                          slice = decodedChanges.slice(i, i + IMPORT_CHUNK_SIZE);
                          _context4.next = 3;
                          return _this3.db.execute(function (transaction) {
                            var imports = slice.map(function (remote) {
                              // Store remote change into local database.
                              return importChange(transaction, remote, _this3.localFields, strategy);
                            });
                            var conflicts = imports.filter(function (i) {
                              return i.type === "conflicts";
                            }).map(function (i) {
                              return i.data;
                            });

                            var resolved = _this3._handleConflicts(transaction, conflicts, strategy);

                            return {
                              imports: imports,
                              resolved: resolved
                            };
                          }, {
                            preload: slice.map(function (record) {
                              return record.id;
                            })
                          });

                        case 3:
                          _ref2 = _context4.sent;
                          imports = _ref2.imports;
                          resolved = _ref2.resolved;
                          // Lists of created/updated/deleted records
                          imports.forEach(function (_ref3) {
                            var type = _ref3.type,
                                data = _ref3.data;
                            return syncResultObject.add(type, data);
                          }); // Automatically resolved conflicts (if not manual)

                          if (resolved.length > 0) {
                            syncResultObject.reset("conflicts").add("resolved", resolved);
                          }

                        case 8:
                        case "end":
                          return _context4.stop();
                      }
                    }
                  }, _loop, this);
                });
                i = 0;

              case 4:
                if (!(i < decodedChanges.length)) {
                  _context5.next = 9;
                  break;
                }

                return _context5.delegateYield(_loop(i), "t0", 6);

              case 6:
                i += IMPORT_CHUNK_SIZE;
                _context5.next = 4;
                break;

              case 9:
                _context5.next = 15;
                break;

              case 11:
                _context5.prev = 11;
                _context5.t1 = _context5["catch"](1);
                data = {
                  type: "incoming",
                  message: _context5.t1.message,
                  stack: _context5.t1.stack
                }; // XXX one error of the whole transaction instead of per atomic op

                syncResultObject.add("errors", data);

              case 15:
                return _context5.abrupt("return", syncResultObject);

              case 16:
              case "end":
                return _context5.stop();
            }
          }
        }, _callee4, this, [[1, 11]]);
      }));

      return function importChanges(_x, _x2) {
        return _importChanges.apply(this, arguments);
      };
    }()
    /**
     * Imports the responses of pushed changes into the local database.
     * Basically it stores the timestamp assigned by the server into the local
     * database.
     * @param  {SyncResultObject} syncResultObject The sync result object.
     * @param  {Array}            toApplyLocally   The list of changes to import in the local database.
     * @param  {Array}            conflicts        The list of conflicts that have to be resolved.
     * @param  {String}           strategy         The {@link Collection.strategy}.
     * @return {Promise}
     */

  }, {
    key: "_applyPushedResults",
    value: function () {
      var _applyPushedResults2 = (0, _asyncToGenerator2.default)(
      /*#__PURE__*/
      _regenerator.default.mark(function _callee5(syncResultObject, toApplyLocally, conflicts) {
        var _this4 = this;

        var strategy,
            toDeleteLocally,
            toUpdateLocally,
            _ref4,
            published,
            resolved,
            _args6 = arguments;

        return _regenerator.default.wrap(function _callee5$(_context6) {
          while (1) {
            switch (_context6.prev = _context6.next) {
              case 0:
                strategy = _args6.length > 3 && _args6[3] !== undefined ? _args6[3] : Collection.strategy.MANUAL;
                toDeleteLocally = toApplyLocally.filter(function (r) {
                  return r.deleted;
                });
                toUpdateLocally = toApplyLocally.filter(function (r) {
                  return !r.deleted;
                });
                _context6.next = 5;
                return this.db.execute(function (transaction) {
                  var updated = toUpdateLocally.map(function (record) {
                    var synced = markSynced(record);
                    transaction.update(synced);
                    return synced;
                  });
                  var deleted = toDeleteLocally.map(function (record) {
                    transaction.delete(record.id); // Amend result data with the deleted attribute set

                    return {
                      id: record.id,
                      deleted: true
                    };
                  });
                  var published = updated.concat(deleted); // Handle conflicts, if any

                  var resolved = _this4._handleConflicts(transaction, conflicts, strategy);

                  return {
                    published: published,
                    resolved: resolved
                  };
                });

              case 5:
                _ref4 = _context6.sent;
                published = _ref4.published;
                resolved = _ref4.resolved;
                syncResultObject.add("published", published);

                if (resolved.length > 0) {
                  syncResultObject.reset("conflicts").reset("resolved").add("resolved", resolved);
                }

                return _context6.abrupt("return", syncResultObject);

              case 11:
              case "end":
                return _context6.stop();
            }
          }
        }, _callee5, this);
      }));

      return function _applyPushedResults(_x3, _x4, _x5) {
        return _applyPushedResults2.apply(this, arguments);
      };
    }()
    /**
     * Handles synchronization conflicts according to specified strategy.
     *
     * @param  {SyncResultObject} result    The sync result object.
     * @param  {String}           strategy  The {@link Collection.strategy}.
     * @return {Promise<Array<Object>>} The resolved conflicts, as an
     *    array of {accepted, rejected} objects
     */

  }, {
    key: "_handleConflicts",
    value: function _handleConflicts(transaction, conflicts, strategy) {
      var _this5 = this;

      if (strategy === Collection.strategy.MANUAL) {
        return [];
      }

      return conflicts.map(function (conflict) {
        var resolution = strategy === Collection.strategy.CLIENT_WINS ? conflict.local : conflict.remote;
        var rejected = strategy === Collection.strategy.CLIENT_WINS ? conflict.remote : conflict.local;
        var accepted, status, id;

        if (resolution === null) {
          // We "resolved" with the server-side deletion. Delete locally.
          // This only happens during SERVER_WINS because the local
          // version of a record can never be null.
          // We can get "null" from the remote side if we got a conflict
          // and there is no remote version available; see kinto-http.js
          // batch.js:aggregate.
          transaction.delete(conflict.local.id);
          accepted = null; // The record was deleted, but that status is "synced" with
          // the server, so we don't need to push the change.

          status = "synced";
          id = conflict.local.id;
        } else {
          var updated = _this5._resolveRaw(conflict, resolution);

          transaction.update(updated);
          accepted = updated;
          status = updated._status;
          id = updated.id;
        }

        return {
          rejected: rejected,
          accepted: accepted,
          id: id,
          _status: status
        };
      });
    }
    /**
     * Execute a bunch of operations in a transaction.
     *
     * This transaction should be atomic -- either all of its operations
     * will succeed, or none will.
     *
     * The argument to this function is itself a function which will be
     * called with a {@link CollectionTransaction}. Collection methods
     * are available on this transaction, but instead of returning
     * promises, they are synchronous. execute() returns a Promise whose
     * value will be the return value of the provided function.
     *
     * Most operations will require access to the record itself, which
     * must be preloaded by passing its ID in the preloadIds option.
     *
     * Options:
     * - {Array} preloadIds: list of IDs to fetch at the beginning of
     *   the transaction
     *
     * @return {Promise} Resolves with the result of the given function
     *    when the transaction commits.
     */

  }, {
    key: "execute",
    value: function execute(doOperations) {
      var _this6 = this;

      var _ref5 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {},
          _ref5$preloadIds = _ref5.preloadIds,
          preloadIds = _ref5$preloadIds === void 0 ? [] : _ref5$preloadIds;

      var _iteratorNormalCompletion = true;
      var _didIteratorError = false;
      var _iteratorError = undefined;

      try {
        for (var _iterator = preloadIds[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
          var id = _step.value;

          if (!this.idSchema.validate(id)) {
            return Promise.reject(Error("Invalid Id: ".concat(id)));
          }
        }
      } catch (err) {
        _didIteratorError = true;
        _iteratorError = err;
      } finally {
        try {
          if (!_iteratorNormalCompletion && _iterator.return != null) {
            _iterator.return();
          }
        } finally {
          if (_didIteratorError) {
            throw _iteratorError;
          }
        }
      }

      return this.db.execute(function (transaction) {
        var txn = new CollectionTransaction(_this6, transaction);
        var result = doOperations(txn);
        txn.emitEvents();
        return result;
      }, {
        preload: preloadIds
      });
    }
    /**
     * Resets the local records as if they were never synced; existing records are
     * marked as newly created, deleted records are dropped.
     *
     * A next call to {@link Collection.sync} will thus republish the whole
     * content of the local collection to the server.
     *
     * @return {Promise} Resolves with the number of processed records.
     */

  }, {
    key: "resetSyncStatus",
    value: function () {
      var _resetSyncStatus = (0, _asyncToGenerator2.default)(
      /*#__PURE__*/
      _regenerator.default.mark(function _callee6() {
        var unsynced;
        return _regenerator.default.wrap(function _callee6$(_context7) {
          while (1) {
            switch (_context7.prev = _context7.next) {
              case 0:
                _context7.next = 2;
                return this.list({
                  filters: {
                    _status: ["deleted", "synced"]
                  },
                  order: ""
                }, {
                  includeDeleted: true
                });

              case 2:
                unsynced = _context7.sent;
                _context7.next = 5;
                return this.db.execute(function (transaction) {
                  unsynced.data.forEach(function (record) {
                    if (record._status === "deleted") {
                      // Garbage collect deleted records.
                      transaction.delete(record.id);
                    } else {
                      // Records that were synced become «created».
                      transaction.update((0, _objectSpread2.default)({}, record, {
                        last_modified: undefined,
                        _status: "created"
                      }));
                    }
                  });
                });

              case 5:
                this._lastModified = null;
                _context7.next = 8;
                return this.db.saveLastModified(null);

              case 8:
                return _context7.abrupt("return", unsynced.data.length);

              case 9:
              case "end":
                return _context7.stop();
            }
          }
        }, _callee6, this);
      }));

      return function resetSyncStatus() {
        return _resetSyncStatus.apply(this, arguments);
      };
    }()
    /**
     * Returns an object containing two lists:
     *
     * - `toDelete`: unsynced deleted records we can safely delete;
     * - `toSync`: local updates to send to the server.
     *
     * @return {Promise}
     */

  }, {
    key: "gatherLocalChanges",
    value: function () {
      var _gatherLocalChanges = (0, _asyncToGenerator2.default)(
      /*#__PURE__*/
      _regenerator.default.mark(function _callee7() {
        var unsynced, deleted;
        return _regenerator.default.wrap(function _callee7$(_context8) {
          while (1) {
            switch (_context8.prev = _context8.next) {
              case 0:
                _context8.next = 2;
                return this.list({
                  filters: {
                    _status: ["created", "updated"]
                  },
                  order: ""
                });

              case 2:
                unsynced = _context8.sent;
                _context8.next = 5;
                return this.list({
                  filters: {
                    _status: "deleted"
                  },
                  order: ""
                }, {
                  includeDeleted: true
                });

              case 5:
                deleted = _context8.sent;
                _context8.next = 8;
                return Promise.all(unsynced.data.concat(deleted.data).map(this._encodeRecord.bind(this, "remote")));

              case 8:
                return _context8.abrupt("return", _context8.sent);

              case 9:
              case "end":
                return _context8.stop();
            }
          }
        }, _callee7, this);
      }));

      return function gatherLocalChanges() {
        return _gatherLocalChanges.apply(this, arguments);
      };
    }()
    /**
     * Fetch remote changes, import them to the local database, and handle
     * conflicts according to `options.strategy`. Then, updates the passed
     * {@link SyncResultObject} with import results.
     *
     * Options:
     * - {String} strategy: The selected sync strategy.
     * - {String} expectedTimestamp: A timestamp to use as a "cache busting" query parameter.
     * - {Array<String>} exclude: A list of record ids to exclude from pull.
     * - {Object} headers: The HTTP headers to use in the request.
     * - {int} retry: The number of retries to do if the HTTP request fails.
     * - {int} lastModified: The timestamp to use in `?_since` query.
     *
     * @param  {KintoClient.Collection} client           Kinto client Collection instance.
     * @param  {SyncResultObject}       syncResultObject The sync result object.
     * @param  {Object}                 options          The options object.
     * @return {Promise}
     */

  }, {
    key: "pullChanges",
    value: function () {
      var _pullChanges = (0, _asyncToGenerator2.default)(
      /*#__PURE__*/
      _regenerator.default.mark(function _callee8(client, syncResultObject) {
        var _this7 = this;

        var options,
            since,
            filters,
            exclude_id,
            _ref6,
            data,
            last_modified,
            unquoted,
            localSynced,
            serverChanged,
            emptyCollection,
            e,
            decodedChanges,
            payload,
            afterHooks,
            _args9 = arguments;

        return _regenerator.default.wrap(function _callee8$(_context9) {
          while (1) {
            switch (_context9.prev = _context9.next) {
              case 0:
                options = _args9.length > 2 && _args9[2] !== undefined ? _args9[2] : {};

                if (syncResultObject.ok) {
                  _context9.next = 3;
                  break;
                }

                return _context9.abrupt("return", syncResultObject);

              case 3:
                if (!this.lastModified) {
                  _context9.next = 7;
                  break;
                }

                _context9.t0 = this.lastModified;
                _context9.next = 10;
                break;

              case 7:
                _context9.next = 9;
                return this.db.getLastModified();

              case 9:
                _context9.t0 = _context9.sent;

              case 10:
                since = _context9.t0;
                options = (0, _objectSpread2.default)({
                  strategy: Collection.strategy.MANUAL,
                  lastModified: since,
                  headers: {}
                }, options); // Optionally ignore some records when pulling for changes.
                // (avoid redownloading our own changes on last step of #sync())

                if (options.exclude) {
                  // Limit the list of excluded records to the first 50 records in order
                  // to remain under de-facto URL size limit (~2000 chars).
                  // http://stackoverflow.com/questions/417142/what-is-the-maximum-length-of-a-url-in-different-browsers/417184#417184
                  exclude_id = options.exclude.slice(0, 50).map(function (r) {
                    return r.id;
                  }).join(",");
                  filters = {
                    exclude_id: exclude_id
                  };
                }

                if (options.expectedTimestamp) {
                  filters = (0, _objectSpread2.default)({}, filters, {
                    _expected: options.expectedTimestamp
                  });
                } // First fetch remote changes from the server


                _context9.next = 16;
                return client.listRecords({
                  // Since should be ETag (see https://github.com/Kinto/kinto.js/issues/356)
                  since: options.lastModified ? "".concat(options.lastModified) : undefined,
                  headers: options.headers,
                  retry: options.retry,
                  // Fetch every page by default (FIXME: option to limit pages, see #277)
                  pages: Infinity,
                  filters: filters
                });

              case 16:
                _ref6 = _context9.sent;
                data = _ref6.data;
                last_modified = _ref6.last_modified;
                // last_modified is the ETag header value (string).
                // For retro-compatibility with first kinto.js versions
                // parse it to integer.
                unquoted = last_modified ? parseInt(last_modified, 10) : undefined; // Check if server was flushed.
                // This is relevant for the Kinto demo server
                // (and thus for many new comers).

                localSynced = options.lastModified;
                serverChanged = unquoted > options.lastModified;
                emptyCollection = data.length === 0;

                if (!(!options.exclude && localSynced && serverChanged && emptyCollection)) {
                  _context9.next = 26;
                  break;
                }

                e = new ServerWasFlushedError(localSynced, unquoted, "Server has been flushed. Client Side Timestamp: " + localSynced + " Server Side Timestamp: " + unquoted);
                throw e;

              case 26:
                // Atomic updates are not sensible here because unquoted is not
                // computed as a function of syncResultObject.lastModified.
                // eslint-disable-next-line require-atomic-updates
                syncResultObject.lastModified = unquoted; // Decode incoming changes.

                _context9.next = 29;
                return Promise.all(data.map(function (change) {
                  return _this7._decodeRecord("remote", change);
                }));

              case 29:
                decodedChanges = _context9.sent;
                // Hook receives decoded records.
                payload = {
                  lastModified: unquoted,
                  changes: decodedChanges
                };
                _context9.next = 33;
                return this.applyHook("incoming-changes", payload);

              case 33:
                afterHooks = _context9.sent;

                if (!(afterHooks.changes.length > 0)) {
                  _context9.next = 37;
                  break;
                }

                _context9.next = 37;
                return this.importChanges(syncResultObject, afterHooks.changes, options.strategy);

              case 37:
                return _context9.abrupt("return", syncResultObject);

              case 38:
              case "end":
                return _context9.stop();
            }
          }
        }, _callee8, this);
      }));

      return function pullChanges(_x6, _x7) {
        return _pullChanges.apply(this, arguments);
      };
    }()
  }, {
    key: "applyHook",
    value: function applyHook(hookName, payload) {
      var _this8 = this;

      if (typeof this.hooks[hookName] == "undefined") {
        return Promise.resolve(payload);
      }

      return (0, _utils.waterfall)(this.hooks[hookName].map(function (hook) {
        return function (record) {
          var result = hook(payload, _this8);
          var resultThenable = result && typeof result.then === "function";
          var resultChanges = result && Object.prototype.hasOwnProperty.call(result, "changes");

          if (!(resultThenable || resultChanges)) {
            throw new Error("Invalid return value for hook: ".concat(JSON.stringify(result), " has no 'then()' or 'changes' properties"));
          }

          return result;
        };
      }), payload);
    }
    /**
     * Publish local changes to the remote server and updates the passed
     * {@link SyncResultObject} with publication results.
     *
     * Options:
     * - {String} strategy: The selected sync strategy.
     * - {Object} headers: The HTTP headers to use in the request.
     * - {int} retry: The number of retries to do if the HTTP request fails.
     *
     * @param  {KintoClient.Collection} client           Kinto client Collection instance.
     * @param  {SyncResultObject}       syncResultObject The sync result object.
     * @param  {Object}                 changes          The change object.
     * @param  {Array}                  changes.toDelete The list of records to delete.
     * @param  {Array}                  changes.toSync   The list of records to create/update.
     * @param  {Object}                 options          The options object.
     * @return {Promise}
     */

  }, {
    key: "pushChanges",
    value: function () {
      var _pushChanges = (0, _asyncToGenerator2.default)(
      /*#__PURE__*/
      _regenerator.default.mark(function _callee9(client, changes, syncResultObject) {
        var _this9 = this;

        var options,
            safe,
            toDelete,
            toSync,
            synced,
            conflicts,
            _iteratorNormalCompletion2,
            _didIteratorError2,
            _iteratorError2,
            _iterator2,
            _step2,
            _step2$value,
            type,
            local,
            remote,
            safeLocal,
            realLocal,
            realRemote,
            conflict,
            missingRemotely,
            published,
            toApplyLocally,
            decoded,
            _args10 = arguments;

        return _regenerator.default.wrap(function _callee9$(_context10) {
          while (1) {
            switch (_context10.prev = _context10.next) {
              case 0:
                options = _args10.length > 3 && _args10[3] !== undefined ? _args10[3] : {};

                if (syncResultObject.ok) {
                  _context10.next = 3;
                  break;
                }

                return _context10.abrupt("return", syncResultObject);

              case 3:
                safe = !options.strategy || options.strategy !== Collection.CLIENT_WINS;
                toDelete = changes.filter(function (r) {
                  return r._status == "deleted";
                });
                toSync = changes.filter(function (r) {
                  return r._status != "deleted";
                }); // Perform a batch request with every changes.

                _context10.next = 8;
                return client.batch(function (batch) {
                  toDelete.forEach(function (r) {
                    // never published locally deleted records should not be pusblished
                    if (r.last_modified) {
                      batch.deleteRecord(r);
                    }
                  });
                  toSync.forEach(function (r) {
                    // Clean local fields (like _status) before sending to server.
                    var published = _this9.cleanLocalFields(r);

                    if (r._status === "created") {
                      batch.createRecord(published);
                    } else {
                      batch.updateRecord(published);
                    }
                  });
                }, {
                  headers: options.headers,
                  retry: options.retry,
                  safe: safe,
                  aggregate: true
                });

              case 8:
                synced = _context10.sent;
                // Store outgoing errors into sync result object
                syncResultObject.add("errors", synced.errors.map(function (e) {
                  return (0, _objectSpread2.default)({}, e, {
                    type: "outgoing"
                  });
                })); // Store outgoing conflicts into sync result object

                conflicts = [];
                _iteratorNormalCompletion2 = true;
                _didIteratorError2 = false;
                _iteratorError2 = undefined;
                _context10.prev = 14;
                _iterator2 = synced.conflicts[Symbol.iterator]();

              case 16:
                if (_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done) {
                  _context10.next = 33;
                  break;
                }

                _step2$value = _step2.value, type = _step2$value.type, local = _step2$value.local, remote = _step2$value.remote;
                // Note: we ensure that local data are actually available, as they may
                // be missing in the case of a published deletion.
                safeLocal = local && local.data || {
                  id: remote.id
                };
                _context10.next = 21;
                return this._decodeRecord("remote", safeLocal);

              case 21:
                realLocal = _context10.sent;
                _context10.t0 = remote;

                if (!_context10.t0) {
                  _context10.next = 27;
                  break;
                }

                _context10.next = 26;
                return this._decodeRecord("remote", remote);

              case 26:
                _context10.t0 = _context10.sent;

              case 27:
                realRemote = _context10.t0;
                conflict = {
                  type: type,
                  local: realLocal,
                  remote: realRemote
                };
                conflicts.push(conflict);

              case 30:
                _iteratorNormalCompletion2 = true;
                _context10.next = 16;
                break;

              case 33:
                _context10.next = 39;
                break;

              case 35:
                _context10.prev = 35;
                _context10.t1 = _context10["catch"](14);
                _didIteratorError2 = true;
                _iteratorError2 = _context10.t1;

              case 39:
                _context10.prev = 39;
                _context10.prev = 40;

                if (!_iteratorNormalCompletion2 && _iterator2.return != null) {
                  _iterator2.return();
                }

              case 42:
                _context10.prev = 42;

                if (!_didIteratorError2) {
                  _context10.next = 45;
                  break;
                }

                throw _iteratorError2;

              case 45:
                return _context10.finish(42);

              case 46:
                return _context10.finish(39);

              case 47:
                syncResultObject.add("conflicts", conflicts); // Records that must be deleted are either deletions that were pushed
                // to server (published) or deleted records that were never pushed (skipped).

                missingRemotely = synced.skipped.map(function (r) {
                  return (0, _objectSpread2.default)({}, r, {
                    deleted: true
                  });
                }); // For created and updated records, the last_modified coming from server
                // will be stored locally.
                // Reflect publication results locally using the response from
                // the batch request.

                published = synced.published.map(function (c) {
                  return c.data;
                });
                toApplyLocally = published.concat(missingRemotely); // Apply the decode transformers, if any

                _context10.next = 53;
                return Promise.all(toApplyLocally.map(function (record) {
                  return _this9._decodeRecord("remote", record);
                }));

              case 53:
                decoded = _context10.sent;

                if (!(decoded.length > 0 || conflicts.length > 0)) {
                  _context10.next = 57;
                  break;
                }

                _context10.next = 57;
                return this._applyPushedResults(syncResultObject, decoded, conflicts, options.strategy);

              case 57:
                return _context10.abrupt("return", syncResultObject);

              case 58:
              case "end":
                return _context10.stop();
            }
          }
        }, _callee9, this, [[14, 35, 39, 47], [40,, 42, 46]]);
      }));

      return function pushChanges(_x8, _x9, _x10) {
        return _pushChanges.apply(this, arguments);
      };
    }()
    /**
     * Return a copy of the specified record without the local fields.
     *
     * @param  {Object} record  A record with potential local fields.
     * @return {Object}
     */

  }, {
    key: "cleanLocalFields",
    value: function cleanLocalFields(record) {
      var localKeys = RECORD_FIELDS_TO_CLEAN.concat(this.localFields);
      return (0, _utils.omitKeys)(record, localKeys);
    }
    /**
     * Resolves a conflict, updating local record according to proposed
     * resolution — keeping remote record `last_modified` value as a reference for
     * further batch sending.
     *
     * @param  {Object} conflict   The conflict object.
     * @param  {Object} resolution The proposed record.
     * @return {Promise}
     */

  }, {
    key: "resolve",
    value: function resolve(conflict, resolution) {
      var _this10 = this;

      return this.db.execute(function (transaction) {
        var updated = _this10._resolveRaw(conflict, resolution);

        transaction.update(updated);
        return {
          data: updated,
          permissions: {}
        };
      });
    }
    /**
     * @private
     */

  }, {
    key: "_resolveRaw",
    value: function _resolveRaw(conflict, resolution) {
      var resolved = (0, _objectSpread2.default)({}, resolution, {
        // Ensure local record has the latest authoritative timestamp
        last_modified: conflict.remote && conflict.remote.last_modified
      }); // If the resolution object is strictly equal to the
      // remote record, then we can mark it as synced locally.
      // Otherwise, mark it as updated (so that the resolution is pushed).

      var synced = (0, _utils.deepEqual)(resolved, conflict.remote);
      return markStatus(resolved, synced ? "synced" : "updated");
    }
    /**
     * Synchronize remote and local data. The promise will resolve with a
     * {@link SyncResultObject}, though will reject:
     *
     * - if the server is currently backed off;
     * - if the server has been detected flushed.
     *
     * Options:
     * - {Object} headers: HTTP headers to attach to outgoing requests.
     * - {String} expectedTimestamp: A timestamp to use as a "cache busting" query parameter.
     * - {Number} retry: Number of retries when server fails to process the request (default: 1).
     * - {Collection.strategy} strategy: See {@link Collection.strategy}.
     * - {Boolean} ignoreBackoff: Force synchronization even if server is currently
     *   backed off.
     * - {String} bucket: The remove bucket id to use (default: null)
     * - {String} collection: The remove collection id to use (default: null)
     * - {String} remote The remote Kinto server endpoint to use (default: null).
     *
     * @param  {Object} options Options.
     * @return {Promise}
     * @throws {Error} If an invalid remote option is passed.
     */

  }, {
    key: "sync",
    value: function () {
      var _sync = (0, _asyncToGenerator2.default)(
      /*#__PURE__*/
      _regenerator.default.mark(function _callee10() {
        var _this11 = this;

        var options,
            previousRemote,
            seconds,
            client,
            result,
            lastModified,
            toSync,
            resolvedUnsynced,
            resolvedEncoded,
            pullOpts,
            _args11 = arguments;
        return _regenerator.default.wrap(function _callee10$(_context11) {
          while (1) {
            switch (_context11.prev = _context11.next) {
              case 0:
                options = _args11.length > 0 && _args11[0] !== undefined ? _args11[0] : {
                  strategy: Collection.strategy.MANUAL,
                  headers: {},
                  retry: 1,
                  ignoreBackoff: false,
                  bucket: null,
                  collection: null,
                  remote: null,
                  expectedTimestamp: null
                };
                options = (0, _objectSpread2.default)({}, options, {
                  bucket: options.bucket || this.bucket,
                  collection: options.collection || this.name
                });
                previousRemote = this.api.remote;

                if (options.remote) {
                  // Note: setting the remote ensures it's valid, throws when invalid.
                  this.api.remote = options.remote;
                }

                if (!(!options.ignoreBackoff && this.api.backoff > 0)) {
                  _context11.next = 7;
                  break;
                }

                seconds = Math.ceil(this.api.backoff / 1000);
                return _context11.abrupt("return", Promise.reject(new Error("Server is asking clients to back off; retry in ".concat(seconds, "s or use the ignoreBackoff option."))));

              case 7:
                client = this.api.bucket(options.bucket).collection(options.collection);
                result = new SyncResultObject();
                _context11.prev = 9;
                _context11.next = 12;
                return this.pullMetadata(client, options);

              case 12:
                _context11.next = 14;
                return this.pullChanges(client, result, options);

              case 14:
                lastModified = result.lastModified;

                if (!(options.strategy != Collection.strategy.PULL_ONLY)) {
                  _context11.next = 32;
                  break;
                }

                _context11.next = 18;
                return this.gatherLocalChanges();

              case 18:
                toSync = _context11.sent;
                _context11.next = 21;
                return this.pushChanges(client, toSync, result, options);

              case 21:
                // Publish local resolution of push conflicts to server (on CLIENT_WINS)
                resolvedUnsynced = result.resolved.filter(function (r) {
                  return r._status !== "synced";
                });

                if (!(resolvedUnsynced.length > 0)) {
                  _context11.next = 28;
                  break;
                }

                _context11.next = 25;
                return Promise.all(resolvedUnsynced.map(function (resolution) {
                  var record = resolution.accepted;

                  if (record === null) {
                    record = {
                      id: resolution.id,
                      _status: resolution._status
                    };
                  }

                  return _this11._encodeRecord("remote", record);
                }));

              case 25:
                resolvedEncoded = _context11.sent;
                _context11.next = 28;
                return this.pushChanges(client, resolvedEncoded, result, options);

              case 28:
                if (!(result.published.length > 0)) {
                  _context11.next = 32;
                  break;
                }

                // Avoid redownloading our own changes during the last pull.
                pullOpts = (0, _objectSpread2.default)({}, options, {
                  lastModified: lastModified,
                  exclude: result.published
                });
                _context11.next = 32;
                return this.pullChanges(client, result, pullOpts);

              case 32:
                if (!result.ok) {
                  _context11.next = 36;
                  break;
                }

                _context11.next = 35;
                return this.db.saveLastModified(result.lastModified);

              case 35:
                this._lastModified = _context11.sent;

              case 36:
                _context11.next = 42;
                break;

              case 38:
                _context11.prev = 38;
                _context11.t0 = _context11["catch"](9);
                this.events.emit("sync:error", (0, _objectSpread2.default)({}, options, {
                  error: _context11.t0
                }));
                throw _context11.t0;

              case 42:
                _context11.prev = 42;
                // Ensure API default remote is reverted if a custom one's been used
                this.api.remote = previousRemote;
                return _context11.finish(42);

              case 45:
                this.events.emit("sync:success", (0, _objectSpread2.default)({}, options, {
                  result: result
                }));
                return _context11.abrupt("return", result);

              case 47:
              case "end":
                return _context11.stop();
            }
          }
        }, _callee10, this, [[9, 38, 42, 45]]);
      }));

      return function sync() {
        return _sync.apply(this, arguments);
      };
    }()
    /**
     * Load a list of records already synced with the remote server.
     *
     * The local records which are unsynced or whose timestamp is either missing
     * or superior to those being loaded will be ignored.
     *
     * @deprecated Use {@link importBulk} instead.
     * @param  {Array} records The previously exported list of records to load.
     * @return {Promise} with the effectively imported records.
     */

  }, {
    key: "loadDump",
    value: function () {
      var _loadDump = (0, _asyncToGenerator2.default)(
      /*#__PURE__*/
      _regenerator.default.mark(function _callee11(records) {
        return _regenerator.default.wrap(function _callee11$(_context12) {
          while (1) {
            switch (_context12.prev = _context12.next) {
              case 0:
                return _context12.abrupt("return", this.importBulk(records));

              case 1:
              case "end":
                return _context12.stop();
            }
          }
        }, _callee11, this);
      }));

      return function loadDump(_x11) {
        return _loadDump.apply(this, arguments);
      };
    }()
    /**
     * Load a list of records already synced with the remote server.
     *
     * The local records which are unsynced or whose timestamp is either missing
     * or superior to those being loaded will be ignored.
     *
     * @param  {Array} records The previously exported list of records to load.
     * @return {Promise} with the effectively imported records.
     */

  }, {
    key: "importBulk",
    value: function () {
      var _importBulk = (0, _asyncToGenerator2.default)(
      /*#__PURE__*/
      _regenerator.default.mark(function _callee12(records) {
        var _iteratorNormalCompletion3, _didIteratorError3, _iteratorError3, _iterator3, _step3, record, _ref7, data, existingById, newRecords;

        return _regenerator.default.wrap(function _callee12$(_context13) {
          while (1) {
            switch (_context13.prev = _context13.next) {
              case 0:
                if (Array.isArray(records)) {
                  _context13.next = 2;
                  break;
                }

                throw new Error("Records is not an array.");

              case 2:
                _iteratorNormalCompletion3 = true;
                _didIteratorError3 = false;
                _iteratorError3 = undefined;
                _context13.prev = 5;
                _iterator3 = records[Symbol.iterator]();

              case 7:
                if (_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done) {
                  _context13.next = 16;
                  break;
                }

                record = _step3.value;

                if (!(!Object.prototype.hasOwnProperty.call(record, "id") || !this.idSchema.validate(record.id))) {
                  _context13.next = 11;
                  break;
                }

                throw new Error("Record has invalid ID: " + JSON.stringify(record));

              case 11:
                if (record.last_modified) {
                  _context13.next = 13;
                  break;
                }

                throw new Error("Record has no last_modified value: " + JSON.stringify(record));

              case 13:
                _iteratorNormalCompletion3 = true;
                _context13.next = 7;
                break;

              case 16:
                _context13.next = 22;
                break;

              case 18:
                _context13.prev = 18;
                _context13.t0 = _context13["catch"](5);
                _didIteratorError3 = true;
                _iteratorError3 = _context13.t0;

              case 22:
                _context13.prev = 22;
                _context13.prev = 23;

                if (!_iteratorNormalCompletion3 && _iterator3.return != null) {
                  _iterator3.return();
                }

              case 25:
                _context13.prev = 25;

                if (!_didIteratorError3) {
                  _context13.next = 28;
                  break;
                }

                throw _iteratorError3;

              case 28:
                return _context13.finish(25);

              case 29:
                return _context13.finish(22);

              case 30:
                _context13.next = 32;
                return this.list({}, {
                  includeDeleted: true
                });

              case 32:
                _ref7 = _context13.sent;
                data = _ref7.data;
                existingById = data.reduce(function (acc, record) {
                  acc[record.id] = record;
                  return acc;
                }, {});
                newRecords = records.filter(function (record) {
                  var localRecord = existingById[record.id];
                  var shouldKeep = // No local record with this id.
                  localRecord === undefined || // Or local record is synced
                  localRecord._status === "synced" && // And was synced from server
                  localRecord.last_modified !== undefined && // And is older than imported one.
                  record.last_modified > localRecord.last_modified;
                  return shouldKeep;
                });
                _context13.next = 38;
                return this.db.importBulk(newRecords.map(markSynced));

              case 38:
                return _context13.abrupt("return", _context13.sent);

              case 39:
              case "end":
                return _context13.stop();
            }
          }
        }, _callee12, this, [[5, 18, 22, 30], [23,, 25, 29]]);
      }));

      return function importBulk(_x12) {
        return _importBulk.apply(this, arguments);
      };
    }()
  }, {
    key: "pullMetadata",
    value: function () {
      var _pullMetadata = (0, _asyncToGenerator2.default)(
      /*#__PURE__*/
      _regenerator.default.mark(function _callee13(client) {
        var options,
            expectedTimestamp,
            headers,
            query,
            metadata,
            _args14 = arguments;
        return _regenerator.default.wrap(function _callee13$(_context14) {
          while (1) {
            switch (_context14.prev = _context14.next) {
              case 0:
                options = _args14.length > 1 && _args14[1] !== undefined ? _args14[1] : {};
                expectedTimestamp = options.expectedTimestamp, headers = options.headers;
                query = expectedTimestamp ? {
                  query: {
                    _expected: expectedTimestamp
                  }
                } : undefined;
                _context14.next = 5;
                return client.getData((0, _objectSpread2.default)({}, query, {
                  headers: headers
                }));

              case 5:
                metadata = _context14.sent;
                return _context14.abrupt("return", this.db.saveMetadata(metadata));

              case 7:
              case "end":
                return _context14.stop();
            }
          }
        }, _callee13, this);
      }));

      return function pullMetadata(_x13) {
        return _pullMetadata.apply(this, arguments);
      };
    }()
  }, {
    key: "metadata",
    value: function () {
      var _metadata = (0, _asyncToGenerator2.default)(
      /*#__PURE__*/
      _regenerator.default.mark(function _callee14() {
        return _regenerator.default.wrap(function _callee14$(_context15) {
          while (1) {
            switch (_context15.prev = _context15.next) {
              case 0:
                return _context15.abrupt("return", this.db.getMetadata());

              case 1:
              case "end":
                return _context15.stop();
            }
          }
        }, _callee14, this);
      }));

      return function metadata() {
        return _metadata.apply(this, arguments);
      };
    }()
  }, {
    key: "name",
    get: function get() {
      return this._name;
    }
    /**
     * The bucket name.
     * @type {String}
     */

  }, {
    key: "bucket",
    get: function get() {
      return this._bucket;
    }
    /**
     * The last modified timestamp.
     * @type {Number}
     */

  }, {
    key: "lastModified",
    get: function get() {
      return this._lastModified;
    }
    /**
     * Synchronization strategies. Available strategies are:
     *
     * - `MANUAL`: Conflicts will be reported in a dedicated array.
     * - `SERVER_WINS`: Conflicts are resolved using remote data.
     * - `CLIENT_WINS`: Conflicts are resolved using local data.
     *
     * @type {Object}
     */

  }], [{
    key: "strategy",
    get: function get() {
      return {
        CLIENT_WINS: "client_wins",
        SERVER_WINS: "server_wins",
        PULL_ONLY: "pull_only",
        MANUAL: "manual"
      };
    }
  }]);
  return Collection;
}();
/**
 * A Collection-oriented wrapper for an adapter's transaction.
 *
 * This defines the high-level functions available on a collection.
 * The collection itself offers functions of the same name. These will
 * perform just one operation in its own transaction.
 */


exports.default = Collection;

var CollectionTransaction =
/*#__PURE__*/
function () {
  function CollectionTransaction(collection, adapterTransaction) {
    (0, _classCallCheck2.default)(this, CollectionTransaction);
    this.collection = collection;
    this.adapterTransaction = adapterTransaction;
    this._events = [];
  }

  (0, _createClass2.default)(CollectionTransaction, [{
    key: "_queueEvent",
    value: function _queueEvent(action, payload) {
      this._events.push({
        action: action,
        payload: payload
      });
    }
    /**
     * Emit queued events, to be called once every transaction operations have
     * been executed successfully.
     */

  }, {
    key: "emitEvents",
    value: function emitEvents() {
      var _iteratorNormalCompletion4 = true;
      var _didIteratorError4 = false;
      var _iteratorError4 = undefined;

      try {
        for (var _iterator4 = this._events[Symbol.iterator](), _step4; !(_iteratorNormalCompletion4 = (_step4 = _iterator4.next()).done); _iteratorNormalCompletion4 = true) {
          var _step4$value = _step4.value,
              action = _step4$value.action,
              payload = _step4$value.payload;
          this.collection.events.emit(action, payload);
        }
      } catch (err) {
        _didIteratorError4 = true;
        _iteratorError4 = err;
      } finally {
        try {
          if (!_iteratorNormalCompletion4 && _iterator4.return != null) {
            _iterator4.return();
          }
        } finally {
          if (_didIteratorError4) {
            throw _iteratorError4;
          }
        }
      }

      if (this._events.length > 0) {
        var targets = this._events.map(function (_ref8) {
          var action = _ref8.action,
              payload = _ref8.payload;
          return (0, _objectSpread2.default)({
            action: action
          }, payload);
        });

        this.collection.events.emit("change", {
          targets: targets
        });
      }

      this._events = [];
    }
    /**
     * Retrieve a record by its id from the local database, or
     * undefined if none exists.
     *
     * This will also return virtually deleted records.
     *
     * @param  {String} id
     * @return {Object}
     */

  }, {
    key: "getAny",
    value: function getAny(id) {
      var record = this.adapterTransaction.get(id);
      return {
        data: record,
        permissions: {}
      };
    }
    /**
     * Retrieve a record by its id from the local database.
     *
     * Options:
     * - {Boolean} includeDeleted: Include virtually deleted records.
     *
     * @param  {String} id
     * @param  {Object} options
     * @return {Object}
     */

  }, {
    key: "get",
    value: function get(id) {
      var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {
        includeDeleted: false
      };
      var res = this.getAny(id);

      if (!res.data || !options.includeDeleted && res.data._status === "deleted") {
        throw new Error("Record with id=".concat(id, " not found."));
      }

      return res;
    }
    /**
     * Deletes a record from the local database.
     *
     * Options:
     * - {Boolean} virtual: When set to `true`, doesn't actually delete the record,
     *   update its `_status` attribute to `deleted` instead (default: true)
     *
     * @param  {String} id       The record's Id.
     * @param  {Object} options  The options object.
     * @return {Object}
     */

  }, {
    key: "delete",
    value: function _delete(id) {
      var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {
        virtual: true
      };
      // Ensure the record actually exists.
      var existing = this.adapterTransaction.get(id);
      var alreadyDeleted = existing && existing._status == "deleted";

      if (!existing || alreadyDeleted && options.virtual) {
        throw new Error("Record with id=".concat(id, " not found."));
      } // Virtual updates status.


      if (options.virtual) {
        this.adapterTransaction.update(markDeleted(existing));
      } else {
        // Delete for real.
        this.adapterTransaction.delete(id);
      }

      this._queueEvent("delete", {
        data: existing
      });

      return {
        data: existing,
        permissions: {}
      };
    }
    /**
     * Soft delete all records from the local database.
     *
     * @param  {Array} ids        Array of non-deleted Record Ids.
     * @return {Object}
     */

  }, {
    key: "deleteAll",
    value: function deleteAll(ids) {
      var _this12 = this;

      var existingRecords = [];
      ids.forEach(function (id) {
        existingRecords.push(_this12.adapterTransaction.get(id));

        _this12.delete(id);
      });

      this._queueEvent("deleteAll", {
        data: existingRecords
      });

      return {
        data: existingRecords,
        permissions: {}
      };
    }
    /**
     * Deletes a record from the local database, if any exists.
     * Otherwise, do nothing.
     *
     * @param  {String} id       The record's Id.
     * @return {Object}
     */

  }, {
    key: "deleteAny",
    value: function deleteAny(id) {
      var existing = this.adapterTransaction.get(id);

      if (existing) {
        this.adapterTransaction.update(markDeleted(existing));

        this._queueEvent("delete", {
          data: existing
        });
      }

      return {
        data: (0, _objectSpread2.default)({
          id: id
        }, existing),
        deleted: !!existing,
        permissions: {}
      };
    }
    /**
     * Adds a record to the local database, asserting that none
     * already exist with this ID.
     *
     * @param  {Object} record, which must contain an ID
     * @return {Object}
     */

  }, {
    key: "create",
    value: function create(record) {
      if ((0, _typeof2.default)(record) !== "object") {
        throw new Error("Record is not an object.");
      }

      if (!Object.prototype.hasOwnProperty.call(record, "id")) {
        throw new Error("Cannot create a record missing id");
      }

      if (!this.collection.idSchema.validate(record.id)) {
        throw new Error("Invalid Id: ".concat(record.id));
      }

      this.adapterTransaction.create(record);

      this._queueEvent("create", {
        data: record
      });

      return {
        data: record,
        permissions: {}
      };
    }
    /**
     * Updates a record from the local database.
     *
     * Options:
     * - {Boolean} synced: Sets record status to "synced" (default: false)
     * - {Boolean} patch:  Extends the existing record instead of overwriting it
     *   (default: false)
     *
     * @param  {Object} record
     * @param  {Object} options
     * @return {Object}
     */

  }, {
    key: "update",
    value: function update(record) {
      var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {
        synced: false,
        patch: false
      };

      if ((0, _typeof2.default)(record) !== "object") {
        throw new Error("Record is not an object.");
      }

      if (!Object.prototype.hasOwnProperty.call(record, "id")) {
        throw new Error("Cannot update a record missing id.");
      }

      if (!this.collection.idSchema.validate(record.id)) {
        throw new Error("Invalid Id: ".concat(record.id));
      }

      var oldRecord = this.adapterTransaction.get(record.id);

      if (!oldRecord) {
        throw new Error("Record with id=".concat(record.id, " not found."));
      }

      var newRecord = options.patch ? (0, _objectSpread2.default)({}, oldRecord, record) : record;

      var updated = this._updateRaw(oldRecord, newRecord, options);

      this.adapterTransaction.update(updated);

      this._queueEvent("update", {
        data: updated,
        oldRecord: oldRecord
      });

      return {
        data: updated,
        oldRecord: oldRecord,
        permissions: {}
      };
    }
    /**
     * Lower-level primitive for updating a record while respecting
     * _status and last_modified.
     *
     * @param  {Object} oldRecord: the record retrieved from the DB
     * @param  {Object} newRecord: the record to replace it with
     * @return {Object}
     */

  }, {
    key: "_updateRaw",
    value: function _updateRaw(oldRecord, newRecord) {
      var _ref9 = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {},
          _ref9$synced = _ref9.synced,
          synced = _ref9$synced === void 0 ? false : _ref9$synced;

      var updated = (0, _objectSpread2.default)({}, newRecord); // Make sure to never loose the existing timestamp.

      if (oldRecord && oldRecord.last_modified && !updated.last_modified) {
        updated.last_modified = oldRecord.last_modified;
      } // If only local fields have changed, then keep record as synced.
      // If status is created, keep record as created.
      // If status is deleted, mark as updated.


      var isIdentical = oldRecord && recordsEqual(oldRecord, updated, this.collection.localFields);
      var keepSynced = isIdentical && oldRecord._status == "synced";
      var neverSynced = !oldRecord || oldRecord && oldRecord._status == "created";
      var newStatus = keepSynced || synced ? "synced" : neverSynced ? "created" : "updated";
      return markStatus(updated, newStatus);
    }
    /**
     * Upsert a record into the local database.
     *
     * This record must have an ID.
     *
     * If a record with this ID already exists, it will be replaced.
     * Otherwise, this record will be inserted.
     *
     * @param  {Object} record
     * @return {Object}
     */

  }, {
    key: "upsert",
    value: function upsert(record) {
      if ((0, _typeof2.default)(record) !== "object") {
        throw new Error("Record is not an object.");
      }

      if (!Object.prototype.hasOwnProperty.call(record, "id")) {
        throw new Error("Cannot update a record missing id.");
      }

      if (!this.collection.idSchema.validate(record.id)) {
        throw new Error("Invalid Id: ".concat(record.id));
      }

      var oldRecord = this.adapterTransaction.get(record.id);

      var updated = this._updateRaw(oldRecord, record);

      this.adapterTransaction.update(updated); // Don't return deleted records -- pretend they are gone

      if (oldRecord && oldRecord._status == "deleted") {
        oldRecord = undefined;
      }

      if (oldRecord) {
        this._queueEvent("update", {
          data: updated,
          oldRecord: oldRecord
        });
      } else {
        this._queueEvent("create", {
          data: updated
        });
      }

      return {
        data: updated,
        oldRecord: oldRecord,
        permissions: {}
      };
    }
  }]);
  return CollectionTransaction;
}();

exports.CollectionTransaction = CollectionTransaction;