kinto
Version:
An Offline-First JavaScript client for Kinto.
4,408 lines • 156 kB
JavaScript
/*
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
"use strict";
/*
* This file is generated from kinto.js - do not modify directly.
*/
// This is required because with Babel compiles ES2015 modules into a
// require() form that tries to keep its modules on "this", but
// doesn't specify "this", leaving it to default to the global
// object. However, in strict mode, "this" no longer defaults to the
// global object, so expose the global object explicitly. Babel's
// compiled output will use a variable called "global" if one is
// present.
//
// See https://bugzilla.mozilla.org/show_bug.cgi?id=1394556#c3 for
// more details.
var _interopRequireDefault2 = require("@babel/runtime/helpers/interopRequireDefault");
var _toConsumableArray2 = _interopRequireDefault2(require("@babel/runtime/helpers/toConsumableArray"));
var _regenerator = _interopRequireDefault2(require("@babel/runtime/regenerator"));
var _asyncToGenerator2 = _interopRequireDefault2(require("@babel/runtime/helpers/asyncToGenerator"));
var _objectSpread2 = _interopRequireDefault2(require("@babel/runtime/helpers/objectSpread"));
var _classCallCheck2 = _interopRequireDefault2(require("@babel/runtime/helpers/classCallCheck"));
var _possibleConstructorReturn2 = _interopRequireDefault2(require("@babel/runtime/helpers/possibleConstructorReturn"));
var _getPrototypeOf2 = _interopRequireDefault2(require("@babel/runtime/helpers/getPrototypeOf"));
var _get3 = _interopRequireDefault2(require("@babel/runtime/helpers/get"));
var _createClass2 = _interopRequireDefault2(require("@babel/runtime/helpers/createClass"));
var _inherits2 = _interopRequireDefault2(require("@babel/runtime/helpers/inherits"));
var _typeof2 = _interopRequireDefault2(require("@babel/runtime/helpers/typeof"));
var global = void 0;
var EXPORTED_SYMBOLS = ["Kinto"];
/*
* Version 12.0.1 - 75cb4fe
*/
(function (f) {
if ((typeof exports === "undefined" ? "undefined" : (0, _typeof2.default)(exports)) === "object" && typeof module !== "undefined") {
module.exports = f();
} else if (typeof define === "function" && define.amd) {
define([], f);
} else {
var g;
if (typeof window !== "undefined") {
g = window;
} else if (typeof global !== "undefined") {
g = global;
} else if (typeof self !== "undefined") {
g = self;
} else {
g = this;
}
g.Kinto = f();
}
})(function () {
var define, module, exports;
return function () {
function r(e, n, t) {
function o(i, f) {
if (!n[i]) {
if (!e[i]) {
var c = "function" == typeof require && require;
if (!f && c) return c(i, !0);
if (u) return u(i, !0);
var a = new Error("Cannot find module '" + i + "'");
throw a.code = "MODULE_NOT_FOUND", a;
}
var p = n[i] = {
exports: {}
};
e[i][0].call(p.exports, function (r) {
var n = e[i][1][r];
return o(n || r);
}, p, p.exports, r, e, n, t);
}
return n[i].exports;
}
for (var u = "function" == typeof require && require, i = 0; i < t.length; i++) {
o(t[i]);
}
return o;
}
return r;
}()({
1: [function (require, module, exports) {
/*
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _KintoBase = _interopRequireDefault(require("../src/KintoBase"));
var _base = _interopRequireDefault(require("../src/adapters/base"));
var _IDB = _interopRequireDefault(require("../src/adapters/IDB"));
var _utils = require("../src/utils");
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : {
default: obj
};
}
ChromeUtils.import("resource://gre/modules/Timer.jsm");
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyGlobalGetters(global, ["fetch", "indexedDB"]);
var _ChromeUtils$import = ChromeUtils.import("resource://gre/modules/EventEmitter.jsm", {}),
EventEmitter = _ChromeUtils$import.EventEmitter;
var _Cc$MozillaOrgUuid = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator),
generateUUID = _Cc$MozillaOrgUuid.generateUUID; // Use standalone kinto-http module landed in FFx.
var _ChromeUtils$import2 = ChromeUtils.import("resource://services-common/kinto-http-client.js"),
KintoHttpClient = _ChromeUtils$import2.KintoHttpClient;
var Kinto =
/*#__PURE__*/
function (_KintoBase$default) {
(0, _inherits2.default)(Kinto, _KintoBase$default);
(0, _createClass2.default)(Kinto, null, [{
key: "adapters",
get: function get() {
return {
BaseAdapter: _base.default,
IDB: _IDB.default
};
}
}]);
function Kinto() {
var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
(0, _classCallCheck2.default)(this, Kinto);
var events = {};
EventEmitter.decorate(events);
var defaults = {
adapter: _IDB.default,
events: events,
ApiClass: KintoHttpClient
};
return (0, _possibleConstructorReturn2.default)(this, (0, _getPrototypeOf2.default)(Kinto).call(this, (0, _objectSpread2.default)({}, defaults, options)));
}
(0, _createClass2.default)(Kinto, [{
key: "collection",
value: function collection(collName) {
var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
var idSchema = {
validate: function validate(id) {
return typeof id == "string" && _utils.RE_RECORD_ID.test(id);
},
generate: function generate() {
return generateUUID().toString().replace(/[{}]/g, "");
}
};
return (0, _get3.default)((0, _getPrototypeOf2.default)(Kinto.prototype), "collection", this).call(this, collName, (0, _objectSpread2.default)({
idSchema: idSchema
}, options));
}
}]);
return Kinto;
}(_KintoBase.default); // This fixes compatibility with CommonJS required by browserify.
// See http://stackoverflow.com/questions/33505992/babel-6-changes-how-it-exports-default/33683495#33683495
exports.default = Kinto;
if ((0, _typeof2.default)(module) === "object") {
module.exports = Kinto;
}
}, {
"../src/KintoBase": 3,
"../src/adapters/IDB": 4,
"../src/adapters/base": 5,
"../src/utils": 7
}],
2: [function (require, module, exports) {}, {}],
3: [function (require, module, exports) {
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _collection = _interopRequireDefault(require("./collection"));
var _base = _interopRequireDefault(require("./adapters/base"));
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : {
default: obj
};
}
var DEFAULT_BUCKET_NAME = "default";
var DEFAULT_REMOTE = "http://localhost:8888/v1";
var DEFAULT_RETRY = 1;
/**
* KintoBase class.
*/
var KintoBase =
/*#__PURE__*/
function () {
(0, _createClass2.default)(KintoBase, null, [{
key: "adapters",
/**
* Provides a public access to the base adapter class. Users can create a
* custom DB adapter by extending {@link BaseAdapter}.
*
* @type {Object}
*/
get: function get() {
return {
BaseAdapter: _base.default
};
}
/**
* 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: "syncStrategy",
get: function get() {
return _collection.default.strategy;
}
/**
* Constructor.
*
* Options:
* - `{String}` `remote` The server URL to use.
* - `{String}` `bucket` The collection bucket name.
* - `{EventEmitter}` `events` Events handler.
* - `{BaseAdapter}` `adapter` The base DB adapter class.
* - `{Object}` `adapterOptions` Options given to the adapter.
* - `{Object}` `headers` The HTTP headers to use.
* - `{Object}` `retry` Number of retries when the server fails to process the request (default: `1`)
* - `{String}` `requestMode` The HTTP CORS mode to use.
* - `{Number}` `timeout` The requests timeout in ms (default: `5000`).
*
* @param {Object} options The options object.
*/
}]);
function KintoBase() {
var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
(0, _classCallCheck2.default)(this, KintoBase);
var defaults = {
bucket: DEFAULT_BUCKET_NAME,
remote: DEFAULT_REMOTE,
retry: DEFAULT_RETRY
};
this._options = (0, _objectSpread2.default)({}, defaults, options);
if (!this._options.adapter) {
throw new Error("No adapter provided");
}
var _this$_options = this._options,
ApiClass = _this$_options.ApiClass,
events = _this$_options.events,
headers = _this$_options.headers,
remote = _this$_options.remote,
requestMode = _this$_options.requestMode,
retry = _this$_options.retry,
timeout = _this$_options.timeout; // public properties
/**
* The kinto HTTP client instance.
* @type {KintoClient}
*/
this.api = new ApiClass(remote, {
events: events,
headers: headers,
requestMode: requestMode,
retry: retry,
timeout: timeout
});
/**
* The event emitter instance.
* @type {EventEmitter}
*/
this.events = this._options.events;
}
/**
* Creates a {@link Collection} instance. The second (optional) parameter
* will set collection-level options like e.g. `remoteTransformers`.
*
* @param {String} collName The collection name.
* @param {Object} [options={}] Extra options or override client's options.
* @param {Object} [options.idSchema] IdSchema instance (default: UUID)
* @param {Object} [options.remoteTransformers] Array<RemoteTransformer> (default: `[]`])
* @param {Object} [options.hooks] Array<Hook> (default: `[]`])
* @param {Object} [options.localFields] Array<Field> (default: `[]`])
* @return {Collection}
*/
(0, _createClass2.default)(KintoBase, [{
key: "collection",
value: function collection(collName) {
var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
if (!collName) {
throw new Error("missing collection name");
}
var _this$_options$option = (0, _objectSpread2.default)({}, this._options, options),
bucket = _this$_options$option.bucket,
events = _this$_options$option.events,
adapter = _this$_options$option.adapter,
adapterOptions = _this$_options$option.adapterOptions;
var idSchema = options.idSchema,
remoteTransformers = options.remoteTransformers,
hooks = options.hooks,
localFields = options.localFields;
return new _collection.default(bucket, collName, this.api, {
events: events,
adapter: adapter,
adapterOptions: adapterOptions,
idSchema: idSchema,
remoteTransformers: remoteTransformers,
hooks: hooks,
localFields: localFields
});
}
}]);
return KintoBase;
}();
exports.default = KintoBase;
}, {
"./adapters/base": 5,
"./collection": 6
}],
4: [function (require, module, exports) {
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.open = _open2;
exports.execute = execute;
exports.default = void 0;
var _base = _interopRequireDefault(require("./base.js"));
var _utils = require("../utils");
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : {
default: obj
};
}
var INDEXED_FIELDS = ["id", "_status", "last_modified"];
/**
* Small helper that wraps the opening of an IndexedDB into a Promise.
*
* @param dbname {String} The database name.
* @param version {Integer} Schema version
* @param onupgradeneeded {Function} The callback to execute if schema is
* missing or different.
* @return {Promise<IDBDatabase>}
*/
function _open2(_x, _x2) {
return _open.apply(this, arguments);
}
/**
* Helper to run the specified callback in a single transaction on the
* specified store.
* The helper focuses on transaction wrapping into a promise.
*
* @param db {IDBDatabase} The database instance.
* @param name {String} The store name.
* @param callback {Function} The piece of code to execute in the transaction.
* @param options {Object} Options.
* @param options.mode {String} Transaction mode (default: read).
* @return {Promise} any value returned by the callback.
*/
function _open() {
_open = (0, _asyncToGenerator2.default)(
/*#__PURE__*/
_regenerator.default.mark(function _callee10(dbname, _ref) {
var version, onupgradeneeded;
return _regenerator.default.wrap(function _callee10$(_context10) {
while (1) {
switch (_context10.prev = _context10.next) {
case 0:
version = _ref.version, onupgradeneeded = _ref.onupgradeneeded;
return _context10.abrupt("return", new Promise(function (resolve, reject) {
var request = indexedDB.open(dbname, version);
request.onupgradeneeded = function (event) {
var db = event.target.result;
db.onerror = function (event) {
return reject(event.target.error);
};
return onupgradeneeded(event);
};
request.onerror = function (event) {
reject(event.target.error);
};
request.onsuccess = function (event) {
var db = event.target.result;
resolve(db);
};
}));
case 2:
case "end":
return _context10.stop();
}
}
}, _callee10, this);
}));
return _open.apply(this, arguments);
}
function execute(_x3, _x4, _x5) {
return _execute.apply(this, arguments);
}
/**
* Helper to wrap the deletion of an IndexedDB database into a promise.
*
* @param dbName {String} the database to delete
* @return {Promise}
*/
function _execute() {
_execute = (0, _asyncToGenerator2.default)(
/*#__PURE__*/
_regenerator.default.mark(function _callee11(db, name, callback) {
var options,
mode,
_args11 = arguments;
return _regenerator.default.wrap(function _callee11$(_context11) {
while (1) {
switch (_context11.prev = _context11.next) {
case 0:
options = _args11.length > 3 && _args11[3] !== undefined ? _args11[3] : {};
mode = options.mode;
return _context11.abrupt("return", new Promise(function (resolve, reject) {
// On Safari, calling IDBDatabase.transaction with mode == undefined raises
// a TypeError.
var transaction = mode ? db.transaction([name], mode) : db.transaction([name]);
var store = transaction.objectStore(name); // Let the callback abort this transaction.
var abort = function abort(e) {
transaction.abort();
reject(e);
}; // Execute the specified callback **synchronously**.
var result;
try {
result = callback(store, abort);
} catch (e) {
abort(e);
}
transaction.onerror = function (event) {
return reject(event.target.error);
};
transaction.oncomplete = function (event) {
return resolve(result);
};
}));
case 3:
case "end":
return _context11.stop();
}
}
}, _callee11, this);
}));
return _execute.apply(this, arguments);
}
function deleteDatabase(_x6) {
return _deleteDatabase.apply(this, arguments);
}
/**
* IDB cursor handlers.
* @type {Object}
*/
function _deleteDatabase() {
_deleteDatabase = (0, _asyncToGenerator2.default)(
/*#__PURE__*/
_regenerator.default.mark(function _callee12(dbName) {
return _regenerator.default.wrap(function _callee12$(_context12) {
while (1) {
switch (_context12.prev = _context12.next) {
case 0:
return _context12.abrupt("return", new Promise(function (resolve, reject) {
var request = indexedDB.deleteDatabase(dbName);
request.onsuccess = function (event) {
return resolve(event.target);
};
request.onerror = function (event) {
return reject(event.target.error);
};
}));
case 1:
case "end":
return _context12.stop();
}
}
}, _callee12, this);
}));
return _deleteDatabase.apply(this, arguments);
}
var cursorHandlers = {
all: function all(filters, done) {
var results = [];
return function (event) {
var cursor = event.target.result;
if (cursor) {
var value = cursor.value;
if ((0, _utils.filterObject)(filters, value)) {
results.push(value);
}
cursor.continue();
} else {
done(results);
}
};
},
in: function _in(values, filters, done) {
var results = [];
return function (event) {
var cursor = event.target.result;
if (!cursor) {
done(results);
return;
}
var key = cursor.key,
value = cursor.value; // `key` can be an array of two values (see `keyPath` in indices definitions).
var i = 0; // `values` can be an array of arrays if we filter using an index whose key path
// is an array (eg. `cursorHandlers.in([["bid/cid", 42], ["bid/cid", 43]], ...)`)
while (key > values[i]) {
// The cursor has passed beyond this key. Check next.
++i;
if (i === values.length) {
done(results); // There is no next. Stop searching.
return;
}
}
var isEqual = Array.isArray(key) ? (0, _utils.arrayEqual)(key, values[i]) : key === values[i];
if (isEqual) {
if ((0, _utils.filterObject)(filters, value)) {
results.push(value);
}
cursor.continue();
} else {
cursor.continue(values[i]);
}
};
}
};
/**
* Creates an IDB request and attach it the appropriate cursor event handler to
* perform a list query.
*
* Multiple matching values are handled by passing an array.
*
* @param {String} cid The collection id (ie. `{bid}/{cid}`)
* @param {IDBStore} store The IDB store.
* @param {Object} filters Filter the records by field.
* @param {Function} done The operation completion handler.
* @return {IDBRequest}
*/
function createListRequest(cid, store, filters, done) {
// Introspect filters and check if they leverage an indexed field.
var indexField = Object.keys(filters).find(function (field) {
return INDEXED_FIELDS.includes(field);
});
if (!indexField) {
// Get all records for this collection (ie. cid)
var _request = store.index("cid").openCursor(IDBKeyRange.only(cid));
_request.onsuccess = cursorHandlers.all(filters, done);
return _request;
} // If `indexField` was used already, don't filter again.
var remainingFilters = (0, _utils.omitKeys)(filters, indexField); // value specified in the filter (eg. `filters: { _status: ["created", "updated"] }`)
var value = filters[indexField]; // WHERE IN equivalent clause
if (Array.isArray(value)) {
if (value.length === 0) {
return done([]);
}
var values = value.map(function (i) {
return [cid, i];
}).sort();
var range = IDBKeyRange.bound(values[0], values[values.length - 1]);
var _request2 = store.index(indexField).openCursor(range);
_request2.onsuccess = cursorHandlers.in(values, remainingFilters, done);
return _request2;
} // WHERE field = value clause
var request = store.index(indexField).openCursor(IDBKeyRange.only([cid, value]));
request.onsuccess = cursorHandlers.all(remainingFilters, done);
return request;
}
/**
* IndexedDB adapter.
*
* This adapter doesn't support any options.
*/
var IDB =
/*#__PURE__*/
function (_base$default) {
(0, _inherits2.default)(IDB, _base$default);
/**
* Constructor.
*
* @param {String} cid The key base for this collection (eg. `bid/cid`)
* @param {Object} options
* @param {String} options.dbName The IndexedDB name (default: `"KintoDB"`)
* @param {String} options.migrateOldData Whether old database data should be migrated (default: `false`)
*/
function IDB(cid) {
var _this;
var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
(0, _classCallCheck2.default)(this, IDB);
_this = (0, _possibleConstructorReturn2.default)(this, (0, _getPrototypeOf2.default)(IDB).call(this));
_this.cid = cid;
_this.dbName = options.dbName || "KintoDB";
_this._options = options;
_this._db = null;
return _this;
}
(0, _createClass2.default)(IDB, [{
key: "_handleError",
value: function _handleError(method, err) {
var error = new Error(method + "() " + err.message);
error.stack = err.stack;
throw error;
}
/**
* Ensures a connection to the IndexedDB database has been opened.
*
* @override
* @return {Promise}
*/
}, {
key: "open",
value: function () {
var _open3 = (0, _asyncToGenerator2.default)(
/*#__PURE__*/
_regenerator.default.mark(function _callee() {
var dataToMigrate, records, timestamp;
return _regenerator.default.wrap(function _callee$(_context) {
while (1) {
switch (_context.prev = _context.next) {
case 0:
if (!this._db) {
_context.next = 2;
break;
}
return _context.abrupt("return", this);
case 2:
if (!this._options.migrateOldData) {
_context.next = 8;
break;
}
_context.next = 5;
return migrationRequired(this.cid);
case 5:
_context.t0 = _context.sent;
_context.next = 9;
break;
case 8:
_context.t0 = null;
case 9:
dataToMigrate = _context.t0;
_context.next = 12;
return _open2(this.dbName, {
version: 1,
onupgradeneeded: function onupgradeneeded(event) {
var db = event.target.result; // Records store
var recordsStore = db.createObjectStore("records", {
keyPath: ["_cid", "id"]
}); // An index to obtain all the records in a collection.
recordsStore.createIndex("cid", "_cid"); // Here we create indices for every known field in records by collection.
// Record id (generated by IdSchema, UUID by default)
recordsStore.createIndex("id", ["_cid", "id"]); // Local record status ("synced", "created", "updated", "deleted")
recordsStore.createIndex("_status", ["_cid", "_status"]); // Last modified field
recordsStore.createIndex("last_modified", ["_cid", "last_modified"]); // Timestamps store
db.createObjectStore("timestamps", {
keyPath: "cid"
});
}
});
case 12:
this._db = _context.sent;
if (!dataToMigrate) {
_context.next = 23;
break;
}
records = dataToMigrate.records, timestamp = dataToMigrate.timestamp;
_context.next = 17;
return this.loadDump(records);
case 17:
_context.next = 19;
return this.saveLastModified(timestamp);
case 19:
console.log("".concat(this.cid, ": data was migrated successfully.")); // Delete the old database.
_context.next = 22;
return deleteDatabase(this.cid);
case 22:
console.warn("".concat(this.cid, ": old database was deleted."));
case 23:
return _context.abrupt("return", this);
case 24:
case "end":
return _context.stop();
}
}
}, _callee, this);
}));
return function open() {
return _open3.apply(this, arguments);
};
}()
/**
* Closes current connection to the database.
*
* @override
* @return {Promise}
*/
}, {
key: "close",
value: function close() {
if (this._db) {
this._db.close(); // indexedDB.close is synchronous
this._db = null;
}
return Promise.resolve();
}
/**
* Returns a transaction and an object store for a store name.
*
* To determine if a transaction has completed successfully, we should rather
* listen to the transaction’s complete event rather than the IDBObjectStore
* request’s success event, because the transaction may still fail after the
* success event fires.
*
* @param {String} name Store name
* @param {Function} callback to execute
* @param {Object} options Options
* @param {String} options.mode Transaction mode ("readwrite" or undefined)
* @return {Object}
*/
}, {
key: "prepare",
value: function () {
var _prepare = (0, _asyncToGenerator2.default)(
/*#__PURE__*/
_regenerator.default.mark(function _callee2(name, callback, options) {
return _regenerator.default.wrap(function _callee2$(_context2) {
while (1) {
switch (_context2.prev = _context2.next) {
case 0:
_context2.next = 2;
return this.open();
case 2:
_context2.next = 4;
return execute(this._db, name, callback, options);
case 4:
case "end":
return _context2.stop();
}
}
}, _callee2, this);
}));
return function prepare(_x7, _x8, _x9) {
return _prepare.apply(this, arguments);
};
}()
/**
* Deletes every records in the current collection.
*
* @override
* @return {Promise}
*/
}, {
key: "clear",
value: function () {
var _clear = (0, _asyncToGenerator2.default)(
/*#__PURE__*/
_regenerator.default.mark(function _callee3() {
var _this2 = this;
return _regenerator.default.wrap(function _callee3$(_context3) {
while (1) {
switch (_context3.prev = _context3.next) {
case 0:
_context3.prev = 0;
_context3.next = 3;
return this.prepare("records", function (store) {
var range = IDBKeyRange.only(_this2.cid);
var request = store.index("cid").openKeyCursor(range);
request.onsuccess = function (event) {
var cursor = event.target.result;
if (cursor) {
store.delete(cursor.primaryKey);
cursor.continue();
}
};
return request;
}, {
mode: "readwrite"
});
case 3:
_context3.next = 8;
break;
case 5:
_context3.prev = 5;
_context3.t0 = _context3["catch"](0);
this._handleError("clear", _context3.t0);
case 8:
case "end":
return _context3.stop();
}
}
}, _callee3, this, [[0, 5]]);
}));
return function clear() {
return _clear.apply(this, arguments);
};
}()
/**
* Executes the set of synchronous CRUD operations described in the provided
* callback within an IndexedDB transaction, for current db store.
*
* The callback will be provided an object exposing the following synchronous
* CRUD operation methods: get, create, update, delete.
*
* Important note: because limitations in IndexedDB implementations, no
* asynchronous code should be performed within the provided callback; the
* promise will therefore be rejected if the callback returns a Promise.
*
* Options:
* - {Array} preload: The list of record IDs to fetch and make available to
* the transaction object get() method (default: [])
*
* @example
* const db = new IDB("example");
* const result = await db.execute(transaction => {
* transaction.create({id: 1, title: "foo"});
* transaction.update({id: 2, title: "bar"});
* transaction.delete(3);
* return "foo";
* });
*
* @override
* @param {Function} callback The operation description callback.
* @param {Object} options The options object.
* @return {Promise}
*/
}, {
key: "execute",
value: function () {
var _execute2 = (0, _asyncToGenerator2.default)(
/*#__PURE__*/
_regenerator.default.mark(function _callee4(callback) {
var _this3 = this;
var options,
result,
_args4 = arguments;
return _regenerator.default.wrap(function _callee4$(_context4) {
while (1) {
switch (_context4.prev = _context4.next) {
case 0:
options = _args4.length > 1 && _args4[1] !== undefined ? _args4[1] : {
preload: []
};
_context4.next = 3;
return this.prepare("records", function (store, abort) {
var runCallback = function runCallback() {
var preloaded = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
// Expose a consistent API for every adapter instead of raw store methods.
var proxy = transactionProxy(_this3, store, preloaded); // The callback is executed synchronously within the same transaction.
try {
var returned = callback(proxy);
if (returned instanceof Promise) {
// XXX: investigate how to provide documentation details in error.
throw new Error("execute() callback should not return a Promise.");
} // Bring to scope that will be returned (once promise awaited).
result = returned;
} catch (e) {
// The callback has thrown an error explicitly. Abort transaction cleanly.
abort(e);
}
}; // No option to preload records, go straight to `callback`.
if (!options.preload.length) {
return runCallback();
} // Preload specified records using a list request.
var filters = {
id: options.preload
};
createListRequest(_this3.cid, store, filters, function (records) {
// Store obtained records by id.
var preloaded = records.reduce(function (acc, record) {
acc[record.id] = (0, _utils.omitKeys)(record, ["_cid"]);
return acc;
}, {});
runCallback(preloaded);
});
}, {
mode: "readwrite"
});
case 3:
return _context4.abrupt("return", result);
case 4:
case "end":
return _context4.stop();
}
}
}, _callee4, this);
}));
return function execute(_x10) {
return _execute2.apply(this, arguments);
};
}()
/**
* Retrieve a record by its primary key from the IndexedDB database.
*
* @override
* @param {String} id The record id.
* @return {Promise}
*/
}, {
key: "get",
value: function () {
var _get2 = (0, _asyncToGenerator2.default)(
/*#__PURE__*/
_regenerator.default.mark(function _callee5(id) {
var _this4 = this;
var record;
return _regenerator.default.wrap(function _callee5$(_context5) {
while (1) {
switch (_context5.prev = _context5.next) {
case 0:
_context5.prev = 0;
_context5.next = 3;
return this.prepare("records", function (store) {
store.get([_this4.cid, id]).onsuccess = function (e) {
return record = e.target.result;
};
});
case 3:
return _context5.abrupt("return", record);
case 6:
_context5.prev = 6;
_context5.t0 = _context5["catch"](0);
this._handleError("get", _context5.t0);
case 9:
case "end":
return _context5.stop();
}
}
}, _callee5, this, [[0, 6]]);
}));
return function get(_x11) {
return _get2.apply(this, arguments);
};
}()
/**
* Lists all records from the IndexedDB database.
*
* @override
* @param {Object} params The filters and order to apply to the results.
* @return {Promise}
*/
}, {
key: "list",
value: function () {
var _list = (0, _asyncToGenerator2.default)(
/*#__PURE__*/
_regenerator.default.mark(function _callee6() {
var _this5 = this;
var params,
filters,
results,
_args6 = arguments;
return _regenerator.default.wrap(function _callee6$(_context6) {
while (1) {
switch (_context6.prev = _context6.next) {
case 0:
params = _args6.length > 0 && _args6[0] !== undefined ? _args6[0] : {
filters: {}
};
filters = params.filters;
_context6.prev = 2;
results = [];
_context6.next = 6;
return this.prepare("records", function (store) {
createListRequest(_this5.cid, store, filters, function (_results) {
// we have received all requested records that match the filters,
// we now park them within current scope and hide the `_cid` attribute.
results = _results.map(function (r) {
return (0, _utils.omitKeys)(r, ["_cid"]);
});
});
});
case 6:
return _context6.abrupt("return", params.order ? (0, _utils.sortObjects)(params.order, results) : results);
case 9:
_context6.prev = 9;
_context6.t0 = _context6["catch"](2);
this._handleError("list", _context6.t0);
case 12:
case "end":
return _context6.stop();
}
}
}, _callee6, this, [[2, 9]]);
}));
return function list() {
return _list.apply(this, arguments);
};
}()
/**
* Store the lastModified value into metadata store.
*
* @override
* @param {Number} lastModified
* @return {Promise}
*/
}, {
key: "saveLastModified",
value: function () {
var _saveLastModified = (0, _asyncToGenerator2.default)(
/*#__PURE__*/
_regenerator.default.mark(function _callee7(lastModified) {
var _this6 = this;
var value;
return _regenerator.default.wrap(function _callee7$(_context7) {
while (1) {
switch (_context7.prev = _context7.next) {
case 0:
value = parseInt(lastModified, 10) || null;
_context7.prev = 1;
_context7.next = 4;
return this.prepare("timestamps", function (store) {
return store.put({
cid: _this6.cid,
value: value
});
}, {
mode: "readwrite"
});
case 4:
return _context7.abrupt("return", value);
case 7:
_context7.prev = 7;
_context7.t0 = _context7["catch"](1);
this._handleError("saveLastModified", _context7.t0);
case 10:
case "end":
return _context7.stop();
}
}
}, _callee7, this, [[1, 7]]);
}));
return function saveLastModified(_x12) {
return _saveLastModified.apply(this, arguments);
};
}()
/**
* Retrieve saved lastModified value.
*
* @override
* @return {Promise}
*/
}, {
key: "getLastModified",
value: function () {
var _getLastModified = (0, _asyncToGenerator2.default)(
/*#__PURE__*/
_regenerator.default.mark(function _callee8() {
var _this7 = this;
var entry;
return _regenerator.default.wrap(function _callee8$(_context8) {
while (1) {
switch (_context8.prev = _context8.next) {
case 0:
_context8.prev = 0;
entry = null;
_context8.next = 4;
return this.prepare("timestamps", function (store) {
store.get(_this7.cid).onsuccess = function (e) {
return entry = e.target.result;
};
});
case 4:
return _context8.abrupt("return", entry ? entry.value : null);
case 7:
_context8.prev = 7;
_context8.t0 = _context8["catch"](0);
this._handleError("getLastModified", _context8.t0);
case 10:
case "end":
return _context8.stop();
}
}
}, _callee8, this, [[0, 7]]);
}));
return function getLastModified() {
return _getLastModified.apply(this, arguments);
};
}()
/**
* Load a dump of records exported from a server.
*
* @abstract
* @param {Array} records The records to load.
* @return {Promise}
*/
}, {
key: "loadDump",
value: function () {
var _loadDump = (0, _asyncToGenerator2.default)(
/*#__PURE__*/
_regenerator.default.mark(function _callee9(records) {
var previousLastModified, lastModified;
return _regenerator.default.wrap(function _callee9$(_context9) {
while (1) {
switch (_context9.prev = _context9.next) {
case 0:
_context9.prev = 0;
_context9.next = 3;
return this.execute(function (transaction) {
records.forEach(function (record) {
return transaction.update(record);
});
});
case 3:
_context9.next = 5;
return this.getLastModified();
case 5:
previousLastModified = _context9.sent;
lastModified = Math.max.apply(Math, (0, _toConsumableArray2.default)(records.map(function (record) {
return record.last_modified;
})));
if (!(lastModified > previousLastModified)) {
_context9.next = 10;
break;
}
_context9.next = 10;
return this.saveLastModified(lastModified);
case 10:
return _context9.abrupt("return", records);
case 13:
_context9.prev = 13;
_context9.t0 = _context9["catch"](0);
this._handleError("loadDump", _context9.t0);
case 16:
case "end":
return _context9.stop();
}
}
}, _callee9, this, [[0, 13]]);
}));
return function loadDump(_x13) {
return _loadDump.apply(this, arguments);
};
}()
}]);
return IDB;
}(_base.default);
/**
* IDB transaction proxy.
*
* @param {IDB} adapter The call IDB adapter
* @param {IDBStore} store The IndexedDB database store.
* @param {Array} preloaded The list of records to make available to
* get() (default: []).
* @return {Object}
*/
exports.default = IDB;
function transactionProxy(adapter, store) {
var preloaded = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : [];
var _cid = adapter.cid;
return {
create: function create(record) {
store.add((0, _objectSpread2.default)({}, record, {
_cid: _cid
}));
},
update: function update(record) {
store.put((0, _objectSpread2.default)({}, record, {
_cid: _cid
}));
},
delete: function _delete(id) {
store.delete([_cid, id]);
},
get: function get(id) {
return preloaded[id];
}
};
}
/**
* Up to version 10.X of kinto.js, each collection had its own collection.
* The database name was `${bid}/${cid}` (eg. `"blocklists/certificates"`)
* and contained only one store with the same name.
*/
function migrationRequired(_x14) {
return _migrationRequired.apply(this, arguments);
}
function _migrationRequired() {
_migrationRequired = (0, _asyncToGenerator2.default)(
/*#__PURE__*/
_regenerator.default.mark(function _callee13(dbName) {
var exists, db, records, timestamp;
return _regenerator.default.wrap(function _callee13$(_context13) {
while (1) {
switch (_context13.prev = _context13.next) {
case 0:
exists = true;
_context13.next = 3;
return _open2(dbName, {
version: 1,
onupgradeneeded: function onupgradeneeded(event) {
exists = false;
}
});
case 3:
db = _context13.sent;
// Check that the DB we're looking at is really a legacy one,
// and not some remainder of the open() operation above.
exists &= db.objectStoreNames.contains("__meta__");
if (exists) {
_context13.next = 10;
break;
}
db.close(); // Testing the existence creates it, so delete it :)
_context13.next = 9;
return deleteDatabase(dbName);
case 9:
return _context13.abrupt("return", null);
case 10:
console.warn("".concat(dbName, ": old IndexedDB database found."));
_context13.prev = 11;
_context13.next = 14;
return execute(db, dbName, function (store) {
store.openCursor().onsuccess = cursorHandlers.all({}, function (res) {
return records = res;
});
});
case 14:
console.log("".concat(dbName, ": found ").concat(records.length, " records.")); // Check if there's a entry for this.
timestamp = null;
_context13.next = 18;
return execute(db, "__meta__", function (store) {
store.get("".concat(dbName, "-lastModified")).onsuccess = function (e) {
timestamp = e.target.result ? e.target.result.value : null;
};
});
case 18:
if (timestamp) {
_context13.next = 21;
break;
}
_context13.next = 21;
return execute(db, "__meta__", function (store) {
store.get("lastModified").onsuccess = function (e) {
timestamp = e.target.result ? e.target.result.value : null;
};
});
case 21:
console.log("".concat(dbName, ": ").concat(timestamp ? "found" : "no", " timestamp.")); // Those will be inserted in the new database/schema.
return _context13.abrupt("return", {
records: records,
timestamp: timestamp
});
case 25:
_context13.prev = 25;
_context13.t0 = _context13["catch"](11);
console.error("Error occured during migration", _context13.t0);
return _context13.abrupt("return", null);
case 29:
_context13.prev = 29;
db.close();
return _context13.finish(29);
case 32:
case "end":
return _context13.stop();
}
}
}, _callee13, this, [[11, 25, 29, 32]]);
}));
return _migrationRequired.apply(this, arguments);
}
}, {
"../utils": 7,
"./base.js": 5
}],
5: [function (require, module, exports) {
"use strict";
/**
* Base db adapter.
*
* @abstract
*/
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var BaseAdapter =
/*#__PURE__*/
function () {
function BaseAdapter() {
(0, _classCallCheck2.default)(this, BaseAdapter);
}
(0, _createClass2.default)(BaseAdapter, [{
key: "clear",
/**
* Deletes every records present in the database.
*
* @abstract
* @return {Promise}
*/
value: function clear() {
throw new Error("Not Implemented.");
}
/**
* Executes a batch of operations within a single transaction.
*
* @abstract
* @param {Function} callback The operation callback.
* @param {Object} options The options object.
* @return {Promise}
*/
}, {
key: "execute",
value: function execute(callback) {
var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {
preload: []
};
throw new Error("Not Implemented.");
}
/**
* Retrieve a record by its primary key from the database.
*
* @abstract
* @param {String} id The record id.
* @return {Promise}
*/
}, {
key: "get",
value: function get(id) {
throw new Error("Not Implemented.");
}
/**
* Lists all records from the database.
*
* @abstract
* @param {Object} params The filters and order to apply to the results.
* @return {Promise}
*/
}, {
key: "list",
value: function list() {
var params = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {
filters: {},
order: ""
};
throw new Error("Not Implemented.");
}
/**
* Store the lastModified value.
*
* @abstract
* @param {Number} lastModified
* @return {Promise}
*/
}, {
key: "saveLastModified",
value: function saveLastModified(lastModified) {
throw new Error("Not Implemented.");
}
/**
* Retrieve saved lastModified value.
*
* @abstract
* @return {Promise}
*/
}, {
key: "getLastModified",
value: function getLastModified() {
throw new Error("Not Implemented.");
}
/**
* Load a dump of records exported from a server.
*
* @abstract
* @param {Array} records The records to load.
* @return {Promise}
*/
}, {
key: "loadDump",
value: function loadDump(records) {
throw new Error("Not Implemented.");
}
}]);
return BaseAdapter;
}();
exports.default = BaseAdapter;
}, {}],
6: [function (require, module, exports) {
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.recordsEqual = recordsEqual;
exports.CollectionTransaction = exports.default = exports.SyncResultObject = void 0;
var _base = _interopRequireDefault(require("./adapters/base"));
var _IDB = _interopRequireDefault(require("./adapters/IDB"));
var _utils = require("./utils");
var _uuid = require("uuid");
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : {
default: obj
};
}
var RECORD_FIELDS_TO_CLEAN = ["_status"];
var AVAILABLE_HOOKS = ["incoming-changes"];
/**
* 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 () {
(0, _createClass2.default)(SyncResultObject, null, [{
key: "defaults",
/**
* Object default values.
* @type {Object}
*/
get: function get() {
return {
ok: true,
lastModified: null,
errors: [],
created: [],
updated: [],
deleted: [],
published: [],
conflicts: [],
skipped: [],
resolved: []
};
}
/**
* Public constructor.
*/
}]);
function SyncResultObject() {
(0, _classCallCheck2.default)(this, SyncResultObject);
/**
* Current synchronization result status; becomes `false` when conflicts or
* errors are registered.
* @type {Boolean}
*/
this.ok = true;
Object.assign(this, SyncResultObject.defaults);
}
/**
* 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[type])) {
return;
}
if (!Array.isArray(entries)) {
entries = [entries];
} // Deduplicate entries by id. If the values don't have `id` attribute, just
// keep all.
var recordsWithoutId = new Set();
var recordsById = new Map();
function addOneRecord(record) {
if (!record.id) {
recordsWithoutId.add(record);
} else {
recordsById.set(record.id, record);
}
}
this[type].forEach(addOneRecord);
entries.forEach(addOneRecord);
this[type] = Array.from(recordsById.values()).concat(Array.from(recordsWithoutId));
this.ok = this.errors.length + this.conflicts.length === 0;
return this;
}
/**
* Reinitializes result entries for a given result type.
*
* @param {String} type The result type.
* @return {SyncResultObject}
*/
}, {
key: "reset",
value: function reset(type) {
this[type] = SyncResultObject.defaults[type];
this.ok = this.errors.length + this.conflicts.length === 0;
return this;
}
}]);
return SyncResultObject;
}();
exports.SyncResultObject = SyncResultObject;
function createUUIDSchema() {
return {
generate: function generate() {
return (0, _uuid.v4)();
},
validate: function validate(id) {
return typeof id == "string" && _utils.RE_RECORD_ID.test(id);
}
};
}
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.
* @return {Object}
*/
function importChange(transaction, remote, localFields) {
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
};
} // Compare local and remote, ignoring local fields.
var isIdentical = recordsEqual(local, remote, localFields); // Apply remote changes on local record.
var synced = (0, _objectSpread2.default)({}, local, markSynced(remote)); // 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 _clear2 = (0, _asyncToGenerator2.default)(
/*#__PURE__*/
_regenerator.default.mark(function _callee14() {
return _regenerator.default.wrap(function _callee14$(_context14) {
while (1) {
switch (_context14.prev = _context14.next) {
case 0:
_context14.next = 2;
return this.db.clear();
case 2:
_context14.next = 4;
return this.db.saveLastModified(null);
case 4:
return _context14.abrupt("return", {
data: [],
permissions: {}
});
case 5:
case "end":
return _context14.stop();
}
}
}, _callee14, this);
}));
return function clear() {
return _clear2.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) && !record.hasOwnProperty("id")) {
return reject("Missing required Id; synced and useRecordId options require one");
}
if (!options.synced && !options.useRecordId && record.hasOwnProperty("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 (!record.hasOwnProperty("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 (!record.hasOwnProperty("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 _callee15() {
var _ref2, data, recordIds;
return _regenerator.default.wrap(function _callee15$(_context15) {
while (1) {
switch (_context15.prev = _context15.next) {
case 0:
_context15.next = 2;
return this.list({}, {
includeDeleted: false
});
case 2:
_ref2 = _context15.sent;
data = _ref2.data;
recordIds = data.map(function (record) {
return record.id;
});
return _context15.abrupt("return", this.execute(function (transaction) {
return transaction.deleteAll(recordIds);
}, {
preloadIds: recordIds
}));
case 6:
case "end":
return _context15.stop();
}
}
}, _callee15, 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 _list2 = (0, _asyncToGenerator2.default)(
/*#__PURE__*/
_regenerator.default.mark(function _callee16() {
var params,
options,
results,
data,
_args16 = arguments;
return _regenerator.default.wrap(function _callee16$(_context16) {
while (1) {
switch (_context16.prev = _context16.next) {
case 0:
params = _args16.length > 0 && _args16[0] !== undefined ? _args16[0] : {};
options = _args16.length > 1 && _args16[1] !== undefined ? _args16[1] : {
includeDeleted: false
};
params = (0, _objectSpread2.default)({
order: "-last_modified",
filters: {}
}, params);
_context16.next = 5;
return this.db.list(params);
case 5:
results = _context16.sent;
data = results;
if (!options.includeDeleted) {
data = results.filter(function (record) {
return record._status !== "deleted";
});
}
return _context16.abrupt("return", {
data: data,
permissions: {}
});
case 9:
case "end":
return _context16.stop();
}
}
}, _callee16, this);
}));
return function list() {
return _list2.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 _callee17(syncResultObject, decodedChanges) {
var _this8 = this;
var strategy,
_ref3,
imports,
resolved,
data,
_args17 = arguments;
return _regenerator.default.wrap(function _callee17$(_context17) {
while (1) {
switch (_context17.prev = _context17.next) {
case 0:
strategy = _args17.length > 2 && _args17[2] !== undefined ? _args17[2] : Collection.strategy.MANUAL;
_context17.prev = 1;
_context17.next = 4;
return this.db.execute(function (transaction) {
var imports = decodedChanges.map(function (remote) {
// Store remote change into local database.
return importChange(transaction, remote, _this8.localFields);
});
var conflicts = imports.filter(function (i) {
return i.type === "conflicts";
}).map(function (i) {
return i.data;
});
var resolved = _this8._handleConflicts(transaction, conflicts, strategy);
return {
imports: imports,
resolved: resolved
};
}, {
preload: decodedChanges.map(function (record) {
return record.id;
})
});
case 4:
_ref3 = _context17.sent;
imports = _ref3.imports;
resolved = _ref3.resolved;
// Lists of created/updated/deleted records
imports.forEach(function (_ref4) {
var type = _ref4.type,
data = _ref4.data;
return syncResultObject.add(type, data);
}); // Automatically resolved conflicts (if not manual)
if (resolved.length > 0) {
syncResultObject.reset("conflicts").add("resolved", resolved);
}
_context17.next = 15;
break;
case 11:
_context17.prev = 11;
_context17.t0 = _context17["catch"](1);
data = {
type: "incoming",
message: _context17.t0.message,
stack: _context17.t0.stack
}; // XXX one error of the whole transaction instead of per atomic op
syncResultObject.add("errors", data);
case 15:
return _context17.abrupt("return", syncResultObject);
case 16:
case "end":
return _context17.stop();
}
}
}, _callee17, this, [[1, 11]]);
}));
return function importChanges(_x15, _x16) {
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 _callee18(syncResultObject, toApplyLocally, conflicts) {
var _this9 = this;
var strategy,
toDeleteLocally,
toUpdateLocally,
_ref5,
published,
resolved,
_args18 = arguments;
return _regenerator.default.wrap(function _callee18$(_context18) {
while (1) {
switch (_context18.prev = _context18.next) {
case 0:
strategy = _args18.length > 3 && _args18[3] !== undefined ? _args18[3] : Collection.strategy.MANUAL;
toDeleteLocally = toApplyLocally.filter(function (r) {
return r.deleted;
});
toUpdateLocally = toApplyLocally.filter(function (r) {
return !r.deleted;
});
_context18.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 = _this9._handleConflicts(transaction, conflicts, strategy);
return {
published: published,
resolved: resolved
};
});
case 5:
_ref5 = _context18.sent;
published = _ref5.published;
resolved = _ref5.resolved;
syncResultObject.add("published", published);
if (resolved.length > 0) {
syncResultObject.reset("conflicts").reset("resolved").add("resolved", resolved);
}
return _context18.abrupt("return", syncResultObject);
case 11:
case "end":
return _context18.stop();
}
}
}, _callee18, this);
}));
return function _applyPushedResults(_x17, _x18, _x19) {
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 _this10 = 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 = _this10._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 _this11 = this;
var _ref6 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {},
_ref6$preloadIds = _ref6.preloadIds,
preloadIds = _ref6$preloadIds === void 0 ? [] : _ref6$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(_this11, 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 _callee19() {
var unsynced;
return _regenerator.default.wrap(function _callee19$(_context19) {
while (1) {
switch (_context19.prev = _context19.next) {
case 0:
_context19.next = 2;
return this.list({
filters: {
_status: ["deleted", "synced"]
},
order: ""
}, {
includeDeleted: true
});
case 2:
unsynced = _context19.sent;
_context19.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;
_context19.next = 8;
return this.db.saveLastModified(null);
case 8:
return _context19.abrupt("return", unsynced.data.length);
case 9:
case "end":
return _context19.stop();
}
}
}, _callee19, 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 _callee20() {
var unsynced, deleted;
return _regenerator.default.wrap(function _callee20$(_context20) {
while (1) {
switch (_context20.prev = _context20.next) {
case 0:
_context20.next = 2;
return this.list({
filters: {
_status: ["created", "updated"]
},
order: ""
});
case 2:
unsynced = _context20.sent;
_context20.next = 5;
return this.list({
filters: {
_status: "deleted"
},
order: ""
}, {
includeDeleted: true
});
case 5:
deleted = _context20.sent;
_context20.next = 8;
return Promise.all(unsynced.data.concat(deleted.data).map(this._encodeRecord.bind(this, "remote")));
case 8:
return _context20.abrupt("return", _context20.sent);
case 9:
case "end":
return _context20.stop();
}
}
}, _callee20, 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.
*
* @param {KintoClient.Collection} client Kinto client Collection instance.
* @param {SyncResultObject} syncResultObject The sync result object.
* @param {Object} options
* @return {Promise}
*/
}, {
key: "pullChanges",
value: function () {
var _pullChanges = (0, _asyncToGenerator2.default)(
/*#__PURE__*/
_regenerator.default.mark(function _callee21(client, syncResultObject) {
var _this12 = this;
var options,
since,
filters,
exclude_id,
_ref7,
data,
last_modified,
unquoted,
localSynced,
serverChanged,
emptyCollection,
decodedChanges,
payload,
afterHooks,
_args21 = arguments;
return _regenerator.default.wrap(function _callee21$(_context21) {
while (1) {
switch (_context21.prev = _context21.next) {
case 0:
options = _args21.length > 2 && _args21[2] !== undefined ? _args21[2] : {};
if (syncResultObject.ok) {
_context21.next = 3;
break;
}
return _context21.abrupt("return", syncResultObject);
case 3:
if (!this.lastModified) {
_context21.next = 7;
break;
}
_context21.t0 = this.lastModified;
_context21.next = 10;
break;
case 7:
_context21.next = 9;
return this.db.getLastModified();
case 9:
_context21.t0 = _context21.sent;
case 10:
since = _context21.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
};
} // First fetch remote changes from the server
_context21.next = 15;
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 15:
_ref7 = _context21.sent;
data = _ref7.data;
last_modified = _ref7.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)) {
_context21.next = 24;
break;
}
throw Error("Server has been flushed.");
case 24:
syncResultObject.lastModified = unquoted; // Decode incoming changes.
_context21.next = 27;
return Promise.all(data.map(function (change) {
return _this12._decodeRecord("remote", change);
}));
case 27:
decodedChanges = _context21.sent;
// Hook receives decoded records.
payload = {
lastModified: unquoted,
changes: decodedChanges
};
_context21.next = 31;
return this.applyHook("incoming-changes", payload);
case 31:
afterHooks = _context21.sent;
if (!(afterHooks.changes.length > 0)) {
_context21.next = 35;
break;
}
_context21.next = 35;
return this.importChanges(syncResultObject, afterHooks.changes, options.strategy);
case 35:
return _context21.abrupt("return", syncResultObject);
case 36:
case "end":
return _context21.stop();
}
}
}, _callee21, this);
}));
return function pullChanges(_x20, _x21) {
return _pullChanges.apply(this, arguments);
};
}()
}, {
key: "applyHook",
value: function applyHook(hookName, payload) {
var _this13 = 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, _this13);
var resultThenable = result && typeof result.then === "function";
var resultChanges = result && result.hasOwnProperty("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.
*
* @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 _callee22(client, changes, syncResultObject) {
var _this14 = 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,
_args22 = arguments;
return _regenerator.default.wrap(function _callee22$(_context22) {
while (1) {
switch (_context22.prev = _context22.next) {
case 0:
options = _args22.length > 3 && _args22[3] !== undefined ? _args22[3] : {};
if (syncResultObject.ok) {
_context22.next = 3;
break;
}
return _context22.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.
_context22.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 = _this14.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 = _context22.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;
_context22.prev = 14;
_iterator2 = synced.conflicts[Symbol.iterator]();
case 16:
if (_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done) {
_context22.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
};
_context22.next = 21;
return this._decodeRecord("remote", safeLocal);
case 21:
realLocal = _context22.sent;
_context22.t0 = remote;
if (!_context22.t0) {
_context22.next = 27;
break;
}
_context22.next = 26;
return this._decodeRecord("remote", remote);
case 26:
_context22.t0 = _context22.sent;
case 27:
realRemote = _context22.t0;
conflict = {
type: type,
local: realLocal,
remote: realRemote
};
conflicts.push(conflict);
case 30:
_iteratorNormalCompletion2 = true;
_context22.next = 16;
break;
case 33:
_context22.next = 39;
break;
case 35:
_context22.prev = 35;
_context22.t1 = _context22["catch"](14);
_didIteratorError2 = true;
_iteratorError2 = _context22.t1;
case 39:
_context22.prev = 39;
_context22.prev = 40;
if (!_iteratorNormalCompletion2 && _iterator2.return != null) {
_iterator2.return();
}
case 42:
_context22.prev = 42;
if (!_didIteratorError2) {
_context22.next = 45;
break;
}
throw _iteratorError2;
case 45:
return _context22.finish(42);
case 46:
return _context22.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
_context22.next = 53;
return Promise.all(toApplyLocally.map(function (record) {
return _this14._decodeRecord("remote", record);
}));
case 53:
decoded = _context22.sent;
if (!(decoded.length > 0 || conflicts.length > 0)) {
_context22.next = 57;
break;
}
_context22.next = 57;
return this._applyPushedResults(syncResultObject, decoded, conflicts, options.strategy);
case 57:
return _context22.abrupt("return", syncResultObject);
case 58:
case "end":
return _context22.stop();
}
}
}, _callee22, this, [[14, 35, 39, 47], [40,, 42, 46]]);
}));
return function pushChanges(_x22, _x23, _x24) {
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 _this15 = this;
return this.db.execute(function (transaction) {
var updated = _this15._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.
* - {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 _callee23() {
var _this16 = this;
var options,
previousRemote,
seconds,
client,
result,
lastModified,
toSync,
resolvedUnsynced,
resolvedEncoded,
pullOpts,
_args23 = arguments;
return _regenerator.default.wrap(function _callee23$(_context23) {
while (1) {
switch (_context23.prev = _context23.next) {
case 0:
options = _args23.length > 0 && _args23[0] !== undefined ? _args23[0] : {
strategy: Collection.strategy.MANUAL,
headers: {},
retry: 1,
ignoreBackoff: false,
bucket: null,
collection: null,
remote: 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)) {
_context23.next = 7;
break;
}
seconds = Math.ceil(this.api.backoff / 1000);
return _context23.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();
_context23.prev = 9;
_context23.next = 12;
return this.pullChanges(client, result, options);
case 12:
lastModified = result.lastModified; // Fetch local changes
_context23.next = 15;
return this.gatherLocalChanges();
case 15:
toSync = _context23.sent;
_context23.next = 18;
return this.pushChanges(client, toSync, result, options);
case 18:
// 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)) {
_context23.next = 25;
break;
}
_context23.next = 22;
return Promise.all(resolvedUnsynced.map(function (resolution) {
var record = resolution.accepted;
if (record === null) {
record = {
id: resolution.id,
_status: resolution._status
};
}
return _this16._encodeRecord("remote", record);
}));
case 22:
resolvedEncoded = _context23.sent;
_context23.next = 25;
return this.pushChanges(client, resolvedEncoded, result, options);
case 25:
if (!(result.published.length > 0)) {
_context23.next = 29;
break;
}
// Avoid redownloading our own changes during the last pull.
pullOpts = (0, _objectSpread2.default)({}, options, {
lastModified: lastModified,
exclude: result.published
});
_context23.next = 29;
return this.pullChanges(client, result, pullOpts);
case 29:
if (!result.ok) {
_context23.next = 33;
break;
}
_context23.next = 32;
return this.db.saveLastModified(result.lastModified);
case 32:
this._lastModified = _context23.sent;
case 33:
_context23.next = 39;
break;
case 35:
_context23.prev = 35;
_context23.t0 = _context23["catch"](9);
this.events.emit("sync:error", (0, _objectSpread2.default)({}, options, {
error: _context23.t0
}));
throw _context23.t0;
case 39:
_context23.prev = 39;
// Ensure API default remote is reverted if a custom one's been used
this.api.remote = previousRemote;
return _context23.finish(39);
case 42:
this.events.emit("sync:success", (0, _objectSpread2.default)({}, options, {
result: result
}));
return _context23.abrupt("return", result);
case 44:
case "end":
return _context23.stop();
}
}
}, _callee23, this, [[9, 35, 39, 42]]);
}));
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.
*
* @param {Array} records The previously exported list of records to load.
* @return {Promise} with the effectively imported records.
*/
}, {
key: "loadDump",
value: function () {
var _loadDump2 = (0, _asyncToGenerator2.default)(
/*#__PURE__*/
_regenerator.default.mark(function _callee24(records) {
var _iteratorNormalCompletion3, _didIteratorError3, _iteratorError3, _iterator3, _step3, record, _ref8, data, existingById, newRecords;
return _regenerator.default.wrap(function _callee24$(_context24) {
while (1) {
switch (_context24.prev = _context24.next) {
case 0:
if (Array.isArray(records)) {
_context24.next = 2;
break;
}
throw new Error("Records is not an array.");
case 2:
_iteratorNormalCompletion3 = true;
_didIteratorError3 = false;
_iteratorError3 = undefined;
_context24.prev = 5;
_iterator3 = records[Symbol.iterator]();
case 7:
if (_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done) {
_context24.next = 16;
break;
}
record = _step3.value;
if (!(!record.hasOwnProperty("id") || !this.idSchema.validate(record.id))) {
_context24.next = 11;
break;
}
throw new Error("Record has invalid ID: " + JSON.stringify(record));
case 11:
if (record.last_modified) {
_context24.next = 13;
break;
}
throw new Error("Record has no last_modified value: " + JSON.stringify(record));
case 13:
_iteratorNormalCompletion3 = true;
_context24.next = 7;
break;
case 16:
_context24.next = 22;
break;
case 18:
_context24.prev = 18;
_context24.t0 = _context24["catch"](5);
_didIteratorError3 = true;
_iteratorError3 = _context24.t0;
case 22:
_context24.prev = 22;
_context24.prev = 23;
if (!_iteratorNormalCompletion3 && _iterator3.return != null) {
_iterator3.return();
}
case 25:
_context24.prev = 25;
if (!_didIteratorError3) {
_context24.next = 28;
break;
}
throw _iteratorError3;
case 28:
return _context24.finish(25);
case 29:
return _context24.finish(22);
case 30:
_context24.next = 32;
return this.list({}, {
includeDeleted: true
});
case 32:
_ref8 = _context24.sent;
data = _ref8.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;
});
_context24.next = 38;
return this.db.loadDump(newRecords.map(markSynced));
case 38:
return _context24.abrupt("return", _context24.sent);
case 39:
case "end":
return _context24.stop();
}
}
}, _callee24, this, [[5, 18, 22, 30], [23,, 25, 29]]);
}));
return function loadDump(_x25) {
return _loadDump2.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",
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 (_ref9) {
var action = _ref9.action,
payload = _ref9.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 _this17 = this;
var existingRecords = [];
ids.forEach(function (id) {
existingRecords.push(_this17.adapterTransaction.get(id));
_this17.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 (!record.hasOwnProperty("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 (!record.hasOwnProperty("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 _ref10 = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {},
_ref10$synced = _ref10.synced,
synced = _ref10$synced === void 0 ? false : _ref10$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.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 (!record.hasOwnProperty("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;
}, {
"./adapters/IDB": 4,
"./adapters/base": 5,
"./utils": 7,
"uuid": 2
}],
7: [function (require, module, exports) {
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.sortObjects = sortObjects;
exports.filterObject = filterObject;
exports.filterObjects = filterObjects;
exports.waterfall = waterfall;
exports.deepEqual = deepEqual;
exports.omitKeys = omitKeys;
exports.arrayEqual = arrayEqual;
exports.RE_RECORD_ID = void 0;
var RE_RECORD_ID = /^[a-zA-Z0-9][a-zA-Z0-9_-]*$/;
/**
* Checks if a value is undefined.
* @param {Any} value
* @return {Boolean}
*/
exports.RE_RECORD_ID = RE_RECORD_ID;
function _isUndefined(value) {
return typeof value === "undefined";
}
/**
* Sorts records in a list according to a given ordering.
*
* @param {String} order The ordering, eg. `-last_modified`.
* @param {Array} list The collection to order.
* @return {Array}
*/
function sortObjects(order, list) {
var hasDash = order[0] === "-";
var field = hasDash ? order.slice(1) : order;
var direction = hasDash ? -1 : 1;
return list.slice().sort(function (a, b) {
if (a[field] && _isUndefined(b[field])) {
return direction;
}
if (b[field] && _isUndefined(a[field])) {
return -direction;
}
if (_isUndefined(a[field]) && _isUndefined(b[field])) {
return 0;
}
return a[field] > b[field] ? direction : -direction;
});
}
/**
* Test if a single object matches all given filters.
*
* @param {Object} filters The filters object.
* @param {Object} entry The object to filter.
* @return {Boolean}
*/
function filterObject(filters, entry) {
return Object.keys(filters).every(function (filter) {
var value = filters[filter];
if (Array.isArray(value)) {
return value.some(function (candidate) {
return candidate === entry[filter];
});
}
return entry[filter] === value;
});
}
/**
* Filters records in a list matching all given filters.
*
* @param {Object} filters The filters object.
* @param {Array} list The collection to filter.
* @return {Array}
*/
function filterObjects(filters, list) {
return list.filter(function (entry) {
return filterObject(filters, entry);
});
}
/**
* Resolves a list of functions sequentially, which can be sync or async; in
* case of async, functions must return a promise.
*
* @param {Array} fns The list of functions.
* @param {Any} init The initial value.
* @return {Promise}
*/
function waterfall(fns, init) {
if (!fns.length) {
return Promise.resolve(init);
}
return fns.reduce(function (promise, nextFn) {
return promise.then(nextFn);
}, Promise.resolve(init));
}
/**
* Simple deep object comparison function. This only supports comparison of
* serializable JavaScript objects.
*
* @param {Object} a The source object.
* @param {Object} b The compared object.
* @return {Boolean}
*/
function deepEqual(a, b) {
if (a === b) {
return true;
}
if ((0, _typeof2.default)(a) !== (0, _typeof2.default)(b)) {
return false;
}
if (!(a && (0, _typeof2.default)(a) == "object") || !(b && (0, _typeof2.default)(b) == "object")) {
return false;
}
if (Object.keys(a).length !== Object.keys(b).length) {
return false;
}
for (var k in a) {
if (!deepEqual(a[k], b[k])) {
return false;
}
}
return true;
}
/**
* Return an object without the specified keys.
*
* @param {Object} obj The original object.
* @param {Array} keys The list of keys to exclude.
* @return {Object} A copy without the specified keys.
*/
function omitKeys(obj) {
var keys = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : [];
return Object.keys(obj).reduce(function (acc, key) {
if (!keys.includes(key)) {
acc[key] = obj[key];
}
return acc;
}, {});
}
function arrayEqual(a, b) {
if (a.length !== b.length) {
return false;
}
for (var i = a.length; i--;) {
if (a[i] !== b[i]) {
return false;
}
}
return true;
}
}, {}]
}, {}, [1])(1);
});