--- a/services/common/moz-kinto-client.js
+++ b/services/common/moz-kinto-client.js
@@ -24,32 +24,35 @@ this.EXPORTED_SYMBOLS = ["loadKinto"];
Object.defineProperty(exports, "__esModule", {
value: true
});
var _base = require("../src/adapters/base");
var _base2 = _interopRequireDefault(_base);
+var _utils = require("../src/utils");
+
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
-Components.utils.import("resource://gre/modules/Sqlite.jsm"); /*
- * 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.
- */
-
+/*
+ * 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.
+ */
+
+Components.utils.import("resource://gre/modules/Sqlite.jsm");
Components.utils.import("resource://gre/modules/Task.jsm");
const statements = {
"createCollectionData": `
CREATE TABLE collection_data (
collection_name TEXT,
record_id TEXT,
record TEXT
@@ -121,40 +124,19 @@ class FirefoxAdapter extends _base2.defa
}
_init(connection) {
return Task.spawn(function* () {
yield connection.executeTransaction(function* doSetup() {
const schema = yield connection.getSchemaVersion();
if (schema == 0) {
- var _iteratorNormalCompletion = true;
- var _didIteratorError = false;
- var _iteratorError = undefined;
-
- try {
-
- for (var _iterator = createStatements[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
- const statementName = _step.value;
-
- yield connection.execute(statements[statementName]);
- }
- } catch (err) {
- _didIteratorError = true;
- _iteratorError = err;
- } finally {
- try {
- if (!_iteratorNormalCompletion && _iterator.return) {
- _iterator.return();
- }
- } finally {
- if (_didIteratorError) {
- throw _iteratorError;
- }
- }
+
+ for (let statementName of createStatements) {
+ yield connection.execute(statements[statementName]);
}
yield connection.setSchemaVersion(currentSchemaVersion);
} else if (schema != 1) {
throw new Error("Unknown database schema: " + schema);
}
});
return connection;
@@ -205,39 +187,18 @@ class FirefoxAdapter extends _base2.defa
let result;
try {
result = callback(proxy);
} catch (e) {
return Promise.reject(e);
}
const conn = this._connection;
return conn.executeTransaction(function* doExecuteTransaction() {
- var _iteratorNormalCompletion2 = true;
- var _didIteratorError2 = false;
- var _iteratorError2 = undefined;
-
- try {
- for (var _iterator2 = proxy.operations[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) {
- const { statement, params } = _step2.value;
-
- yield conn.executeCached(statement, params);
- }
- } catch (err) {
- _didIteratorError2 = true;
- _iteratorError2 = err;
- } finally {
- try {
- if (!_iteratorNormalCompletion2 && _iterator2.return) {
- _iterator2.return();
- }
- } finally {
- if (_didIteratorError2) {
- throw _iteratorError2;
- }
- }
+ for (let { statement, params } of proxy.operations) {
+ yield conn.executeCached(statement, params);
}
}).then(_ => result);
}
get(id) {
const params = {
collection_name: this.collection,
record_id: id
@@ -245,27 +206,31 @@ class FirefoxAdapter extends _base2.defa
return this._executeStatement(statements.getRecord, params).then(result => {
if (result.length == 0) {
return;
}
return JSON.parse(result[0].getResultByName("record"));
});
}
- list() {
- const params = {
+ list(params = { filters: {}, order: "" }) {
+ const parameters = {
collection_name: this.collection
};
- return this._executeStatement(statements.listRecords, params).then(result => {
+ return this._executeStatement(statements.listRecords, parameters).then(result => {
const records = [];
for (let k = 0; k < result.length; k++) {
const row = result[k];
records.push(JSON.parse(row.getResultByName("record")));
}
return records;
+ }).then(results => {
+ // The resulting list of records is filtered and sorted.
+ // XXX: with some efforts, this could be implemented using SQL.
+ return (0, _utils.reduceRecords)(params.filters, params.order, results);
});
}
/**
* Load a list of records into the local database.
*
* Note: The adapter is not in charge of filtering the already imported
* records. This is done in `Collection#loadDump()`, as a common behaviour
@@ -274,52 +239,30 @@ class FirefoxAdapter extends _base2.defa
* @param {Array} records.
* @return {Array} imported records.
*/
loadDump(records) {
const connection = this._connection;
const collection_name = this.collection;
return Task.spawn(function* () {
yield connection.executeTransaction(function* doImport() {
- var _iteratorNormalCompletion3 = true;
- var _didIteratorError3 = false;
- var _iteratorError3 = undefined;
-
- try {
- for (var _iterator3 = records[Symbol.iterator](), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) {
- const record = _step3.value;
-
- const params = {
- collection_name: collection_name,
- record_id: record.id,
- record: JSON.stringify(record)
- };
- yield connection.execute(statements.importData, params);
- }
- } catch (err) {
- _didIteratorError3 = true;
- _iteratorError3 = err;
- } finally {
- try {
- if (!_iteratorNormalCompletion3 && _iterator3.return) {
- _iterator3.return();
- }
- } finally {
- if (_didIteratorError3) {
- throw _iteratorError3;
- }
- }
+ for (let record of records) {
+ const params = {
+ collection_name: collection_name,
+ record_id: record.id,
+ record: JSON.stringify(record)
+ };
+ yield connection.execute(statements.importData, params);
}
-
const lastModified = Math.max(...records.map(record => record.last_modified));
const params = {
collection_name: collection_name
};
const previousLastModified = yield connection.execute(statements.getLastModified, params).then(result => {
- return result ? result[0].getResultByName('last_modified') : -1;
+ return result.length > 0 ? result[0].getResultByName('last_modified') : -1;
});
if (lastModified > previousLastModified) {
const params = {
collection_name: collection_name,
last_modified: lastModified
};
yield connection.execute(statements.saveLastModified, params);
}
@@ -393,17 +336,17 @@ function transactionProxy(collection, pr
get(id) {
// Gecko JS engine outputs undesired warnings if id is not in preloaded.
return id in preloaded ? preloaded[id] : undefined;
}
};
}
-},{"../src/adapters/base":11}],2:[function(require,module,exports){
+},{"../src/adapters/base":17,"../src/utils":19}],2:[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
*
@@ -468,17 +411,19 @@ function loadKinto() {
}
// This fixes compatibility with CommonJS required by browserify.
// See http://stackoverflow.com/questions/33505992/babel-6-changes-how-it-exports-default/33683495#33683495
if (typeof module === "object") {
module.exports = loadKinto;
}
-},{"../src/KintoBase":10,"../src/adapters/base":11,"./FirefoxStorage":1}],3:[function(require,module,exports){
+},{"../src/KintoBase":16,"../src/adapters/base":17,"./FirefoxStorage":1}],3:[function(require,module,exports){
+
+},{}],4:[function(require,module,exports){
// http://wiki.commonjs.org/wiki/Unit_Testing/1.0
//
// THIS IS NOT TESTED NOR LIKELY TO WORK OUTSIDE V8!
//
// Originally from narwhal.js (http://narwhaljs.org)
// Copyright (c) 2009 Thomas Robinson <280north.com>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -829,17 +774,317 @@ assert.ifError = function(err) { if (err
var objectKeys = Object.keys || function (obj) {
var keys = [];
for (var key in obj) {
if (hasOwn.call(obj, key)) keys.push(key);
}
return keys;
};
-},{"util/":7}],4:[function(require,module,exports){
+},{"util/":9}],5:[function(require,module,exports){
+// Copyright Joyent, Inc. and other Node contributors.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to permit
+// persons to whom the Software is furnished to do so, subject to the
+// following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+// USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+function EventEmitter() {
+ this._events = this._events || {};
+ this._maxListeners = this._maxListeners || undefined;
+}
+module.exports = EventEmitter;
+
+// Backwards-compat with node 0.10.x
+EventEmitter.EventEmitter = EventEmitter;
+
+EventEmitter.prototype._events = undefined;
+EventEmitter.prototype._maxListeners = undefined;
+
+// By default EventEmitters will print a warning if more than 10 listeners are
+// added to it. This is a useful default which helps finding memory leaks.
+EventEmitter.defaultMaxListeners = 10;
+
+// Obviously not all Emitters should be limited to 10. This function allows
+// that to be increased. Set to zero for unlimited.
+EventEmitter.prototype.setMaxListeners = function(n) {
+ if (!isNumber(n) || n < 0 || isNaN(n))
+ throw TypeError('n must be a positive number');
+ this._maxListeners = n;
+ return this;
+};
+
+EventEmitter.prototype.emit = function(type) {
+ var er, handler, len, args, i, listeners;
+
+ if (!this._events)
+ this._events = {};
+
+ // If there is no 'error' event listener then throw.
+ if (type === 'error') {
+ if (!this._events.error ||
+ (isObject(this._events.error) && !this._events.error.length)) {
+ er = arguments[1];
+ if (er instanceof Error) {
+ throw er; // Unhandled 'error' event
+ }
+ throw TypeError('Uncaught, unspecified "error" event.');
+ }
+ }
+
+ handler = this._events[type];
+
+ if (isUndefined(handler))
+ return false;
+
+ if (isFunction(handler)) {
+ switch (arguments.length) {
+ // fast cases
+ case 1:
+ handler.call(this);
+ break;
+ case 2:
+ handler.call(this, arguments[1]);
+ break;
+ case 3:
+ handler.call(this, arguments[1], arguments[2]);
+ break;
+ // slower
+ default:
+ args = Array.prototype.slice.call(arguments, 1);
+ handler.apply(this, args);
+ }
+ } else if (isObject(handler)) {
+ args = Array.prototype.slice.call(arguments, 1);
+ listeners = handler.slice();
+ len = listeners.length;
+ for (i = 0; i < len; i++)
+ listeners[i].apply(this, args);
+ }
+
+ return true;
+};
+
+EventEmitter.prototype.addListener = function(type, listener) {
+ var m;
+
+ if (!isFunction(listener))
+ throw TypeError('listener must be a function');
+
+ if (!this._events)
+ this._events = {};
+
+ // To avoid recursion in the case that type === "newListener"! Before
+ // adding it to the listeners, first emit "newListener".
+ if (this._events.newListener)
+ this.emit('newListener', type,
+ isFunction(listener.listener) ?
+ listener.listener : listener);
+
+ if (!this._events[type])
+ // Optimize the case of one listener. Don't need the extra array object.
+ this._events[type] = listener;
+ else if (isObject(this._events[type]))
+ // If we've already got an array, just append.
+ this._events[type].push(listener);
+ else
+ // Adding the second element, need to change to array.
+ this._events[type] = [this._events[type], listener];
+
+ // Check for listener leak
+ if (isObject(this._events[type]) && !this._events[type].warned) {
+ if (!isUndefined(this._maxListeners)) {
+ m = this._maxListeners;
+ } else {
+ m = EventEmitter.defaultMaxListeners;
+ }
+
+ if (m && m > 0 && this._events[type].length > m) {
+ this._events[type].warned = true;
+ console.error('(node) warning: possible EventEmitter memory ' +
+ 'leak detected. %d listeners added. ' +
+ 'Use emitter.setMaxListeners() to increase limit.',
+ this._events[type].length);
+ if (typeof console.trace === 'function') {
+ // not supported in IE 10
+ console.trace();
+ }
+ }
+ }
+
+ return this;
+};
+
+EventEmitter.prototype.on = EventEmitter.prototype.addListener;
+
+EventEmitter.prototype.once = function(type, listener) {
+ if (!isFunction(listener))
+ throw TypeError('listener must be a function');
+
+ var fired = false;
+
+ function g() {
+ this.removeListener(type, g);
+
+ if (!fired) {
+ fired = true;
+ listener.apply(this, arguments);
+ }
+ }
+
+ g.listener = listener;
+ this.on(type, g);
+
+ return this;
+};
+
+// emits a 'removeListener' event iff the listener was removed
+EventEmitter.prototype.removeListener = function(type, listener) {
+ var list, position, length, i;
+
+ if (!isFunction(listener))
+ throw TypeError('listener must be a function');
+
+ if (!this._events || !this._events[type])
+ return this;
+
+ list = this._events[type];
+ length = list.length;
+ position = -1;
+
+ if (list === listener ||
+ (isFunction(list.listener) && list.listener === listener)) {
+ delete this._events[type];
+ if (this._events.removeListener)
+ this.emit('removeListener', type, listener);
+
+ } else if (isObject(list)) {
+ for (i = length; i-- > 0;) {
+ if (list[i] === listener ||
+ (list[i].listener && list[i].listener === listener)) {
+ position = i;
+ break;
+ }
+ }
+
+ if (position < 0)
+ return this;
+
+ if (list.length === 1) {
+ list.length = 0;
+ delete this._events[type];
+ } else {
+ list.splice(position, 1);
+ }
+
+ if (this._events.removeListener)
+ this.emit('removeListener', type, listener);
+ }
+
+ return this;
+};
+
+EventEmitter.prototype.removeAllListeners = function(type) {
+ var key, listeners;
+
+ if (!this._events)
+ return this;
+
+ // not listening for removeListener, no need to emit
+ if (!this._events.removeListener) {
+ if (arguments.length === 0)
+ this._events = {};
+ else if (this._events[type])
+ delete this._events[type];
+ return this;
+ }
+
+ // emit removeListener for all listeners on all events
+ if (arguments.length === 0) {
+ for (key in this._events) {
+ if (key === 'removeListener') continue;
+ this.removeAllListeners(key);
+ }
+ this.removeAllListeners('removeListener');
+ this._events = {};
+ return this;
+ }
+
+ listeners = this._events[type];
+
+ if (isFunction(listeners)) {
+ this.removeListener(type, listeners);
+ } else if (listeners) {
+ // LIFO order
+ while (listeners.length)
+ this.removeListener(type, listeners[listeners.length - 1]);
+ }
+ delete this._events[type];
+
+ return this;
+};
+
+EventEmitter.prototype.listeners = function(type) {
+ var ret;
+ if (!this._events || !this._events[type])
+ ret = [];
+ else if (isFunction(this._events[type]))
+ ret = [this._events[type]];
+ else
+ ret = this._events[type].slice();
+ return ret;
+};
+
+EventEmitter.prototype.listenerCount = function(type) {
+ if (this._events) {
+ var evlistener = this._events[type];
+
+ if (isFunction(evlistener))
+ return 1;
+ else if (evlistener)
+ return evlistener.length;
+ }
+ return 0;
+};
+
+EventEmitter.listenerCount = function(emitter, type) {
+ return emitter.listenerCount(type);
+};
+
+function isFunction(arg) {
+ return typeof arg === 'function';
+}
+
+function isNumber(arg) {
+ return typeof arg === 'number';
+}
+
+function isObject(arg) {
+ return typeof arg === 'object' && arg !== null;
+}
+
+function isUndefined(arg) {
+ return arg === void 0;
+}
+
+},{}],6:[function(require,module,exports){
if (typeof Object.create === 'function') {
// implementation from standard node.js 'util' module
module.exports = function inherits(ctor, superCtor) {
ctor.super_ = superCtor
ctor.prototype = Object.create(superCtor.prototype, {
constructor: {
value: ctor,
enumerable: false,
@@ -854,17 +1099,17 @@ if (typeof Object.create === 'function')
ctor.super_ = superCtor
var TempCtor = function () {}
TempCtor.prototype = superCtor.prototype
ctor.prototype = new TempCtor()
ctor.prototype.constructor = ctor
}
}
-},{}],5:[function(require,module,exports){
+},{}],7:[function(require,module,exports){
// shim for using process in browser
var process = module.exports = {};
var queue = [];
var draining = false;
var currentQueue;
var queueIndex = -1;
@@ -947,24 +1192,24 @@ process.binding = function (name) {
};
process.cwd = function () { return '/' };
process.chdir = function (dir) {
throw new Error('process.chdir is not supported');
};
process.umask = function() { return 0; };
-},{}],6:[function(require,module,exports){
+},{}],8:[function(require,module,exports){
module.exports = function isBuffer(arg) {
return arg && typeof arg === 'object'
&& typeof arg.copy === 'function'
&& typeof arg.fill === 'function'
&& typeof arg.readUInt8 === 'function';
}
-},{}],7:[function(require,module,exports){
+},{}],9:[function(require,module,exports){
(function (process,global){
// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
@@ -1544,17 +1789,713 @@ exports._extend = function(origin, add)
return origin;
};
function hasOwnProperty(obj, prop) {
return Object.prototype.hasOwnProperty.call(obj, prop);
}
}).call(this,require('_process'),typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
-},{"./support/isBuffer":6,"_process":5,"inherits":4}],8:[function(require,module,exports){
+},{"./support/isBuffer":8,"_process":7,"inherits":6}],10:[function(require,module,exports){
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+/**
+ * Kinto server error code descriptors.
+ * @type {Object}
+ */
+exports.default = {
+ 104: "Missing Authorization Token",
+ 105: "Invalid Authorization Token",
+ 106: "Request body was not valid JSON",
+ 107: "Invalid request parameter",
+ 108: "Missing request parameter",
+ 109: "Invalid posted data",
+ 110: "Invalid Token / id",
+ 111: "Missing Token / id",
+ 112: "Content-Length header was not provided",
+ 113: "Request body too large",
+ 114: "Resource was modified meanwhile",
+ 115: "Method not allowed on this end point",
+ 116: "Requested version not available on this server",
+ 117: "Client has sent too many requests",
+ 121: "Resource access is forbidden for this user",
+ 122: "Another resource violates constraint",
+ 201: "Service Temporary unavailable due to high load",
+ 202: "Service deprecated",
+ 999: "Internal Server Error"
+};
+},{}],11:[function(require,module,exports){
+"use strict";
+
+var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+
+var _errors = require("./errors.js");
+
+var _errors2 = _interopRequireDefault(_errors);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
+
+/**
+ * Enhanced HTTP client for the Kinto protocol.
+ */
+
+var HTTP = function () {
+ _createClass(HTTP, null, [{
+ key: "DEFAULT_REQUEST_HEADERS",
+
+ /**
+ * Default HTTP request headers applied to each outgoing request.
+ *
+ * @type {Object}
+ */
+ get: function get() {
+ return {
+ "Accept": "application/json",
+ "Content-Type": "application/json"
+ };
+ }
+
+ /**
+ * Default options.
+ *
+ * @type {Object}
+ */
+
+ }, {
+ key: "defaultOptions",
+ get: function get() {
+ return { timeout: 5000, requestMode: "cors" };
+ }
+
+ /**
+ * Constructor.
+ *
+ * Options:
+ * - {Number} timeout The request timeout in ms (default: `5000`).
+ * - {String} requestMode The HTTP request mode (default: `"cors"`).
+ *
+ * @param {EventEmitter} events The event handler.
+ * @param {Object} options The options object.
+ */
+
+ }]);
+
+ function HTTP(events) {
+ var options = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1];
+
+ _classCallCheck(this, HTTP);
+
+ // public properties
+ /**
+ * The event emitter instance.
+ * @type {EventEmitter}
+ */
+ if (!events) {
+ throw new Error("No events handler provided");
+ }
+ this.events = events;
+
+ options = Object.assign({}, HTTP.defaultOptions, options);
+
+ /**
+ * The request mode.
+ * @see https://fetch.spec.whatwg.org/#requestmode
+ * @type {String}
+ */
+ this.requestMode = options.requestMode;
+
+ /**
+ * The request timeout.
+ * @type {Number}
+ */
+ this.timeout = options.timeout;
+ }
+
+ /**
+ * Performs an HTTP request to the Kinto server.
+ *
+ * Options:
+ * - `{Object} headers` The request headers object (default: {})
+ *
+ * Resolves with an objet containing the following HTTP response properties:
+ * - `{Number} status` The HTTP status code.
+ * - `{Object} json` The JSON response body.
+ * - `{Headers} headers` The response headers object; see the ES6 fetch() spec.
+ *
+ * @param {String} url The URL.
+ * @param {Object} options The fetch() options object.
+ * @return {Promise}
+ */
+
+ _createClass(HTTP, [{
+ key: "request",
+ value: function request(url) {
+ var _this = this;
+
+ var options = arguments.length <= 1 || arguments[1] === undefined ? { headers: {} } : arguments[1];
+
+ var response = undefined,
+ status = undefined,
+ statusText = undefined,
+ headers = undefined,
+ _timeoutId = undefined,
+ hasTimedout = undefined;
+ // Ensure default request headers are always set
+ options.headers = Object.assign({}, HTTP.DEFAULT_REQUEST_HEADERS, options.headers);
+ options.mode = this.requestMode;
+ return new Promise(function (resolve, reject) {
+ _timeoutId = setTimeout(function () {
+ hasTimedout = true;
+ reject(new Error("Request timeout."));
+ }, _this.timeout);
+ fetch(url, options).then(function (res) {
+ if (!hasTimedout) {
+ clearTimeout(_timeoutId);
+ resolve(res);
+ }
+ }).catch(function (err) {
+ if (!hasTimedout) {
+ clearTimeout(_timeoutId);
+ reject(err);
+ }
+ });
+ }).then(function (res) {
+ response = res;
+ headers = res.headers;
+ status = res.status;
+ statusText = res.statusText;
+ _this._checkForDeprecationHeader(headers);
+ _this._checkForBackoffHeader(status, headers);
+ return res.text();
+ })
+ // Check if we have a body; if so parse it as JSON.
+ .then(function (text) {
+ if (text.length === 0) {
+ return null;
+ }
+ // Note: we can't consume the response body twice.
+ return JSON.parse(text);
+ }).catch(function (err) {
+ var error = new Error("HTTP " + (status || 0) + "; " + err);
+ error.response = response;
+ error.stack = err.stack;
+ throw error;
+ }).then(function (json) {
+ if (json && status >= 400) {
+ var message = "HTTP " + status + "; ";
+ if (json.errno && json.errno in _errors2.default) {
+ message += _errors2.default[json.errno];
+ if (json.message) {
+ message += ": " + json.message;
+ }
+ } else {
+ message += statusText || "";
+ }
+ var error = new Error(message.trim());
+ error.response = response;
+ error.data = json;
+ throw error;
+ }
+ return { status: status, json: json, headers: headers };
+ });
+ }
+ }, {
+ key: "_checkForDeprecationHeader",
+ value: function _checkForDeprecationHeader(headers) {
+ var alertHeader = headers.get("Alert");
+ if (!alertHeader) {
+ return;
+ }
+ var alert = undefined;
+ try {
+ alert = JSON.parse(alertHeader);
+ } catch (err) {
+ console.warn("Unable to parse Alert header message", alertHeader);
+ return;
+ }
+ console.warn(alert.message, alert.url);
+ this.events.emit("deprecated", alert);
+ }
+ }, {
+ key: "_checkForBackoffHeader",
+ value: function _checkForBackoffHeader(status, headers) {
+ var backoffMs = undefined;
+ var backoffSeconds = parseInt(headers.get("Backoff"), 10);
+ if (backoffSeconds > 0) {
+ backoffMs = new Date().getTime() + backoffSeconds * 1000;
+ } else {
+ backoffMs = 0;
+ }
+ this.events.emit("backoff", backoffMs);
+ }
+ }]);
+
+ return HTTP;
+}();
+
+exports.default = HTTP;
+},{"./errors.js":10}],12:[function(require,module,exports){
+"use strict";
+
+var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.SUPPORTED_PROTOCOL_VERSION = undefined;
+
+require("isomorphic-fetch");
+
+var _events = require("events");
+
+var _utils = require("./utils.js");
+
+var _http = require("./http.js");
+
+var _http2 = _interopRequireDefault(_http);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
+
+/**
+ * Currently supported protocol version.
+ * @type {String}
+ */
+var SUPPORTED_PROTOCOL_VERSION = exports.SUPPORTED_PROTOCOL_VERSION = "v1";
+
+/**
+ * High level HTTP client for the Kinto API.
+ */
+
+var KintoApi = function () {
+ /**
+ * Constructor.
+ *
+ * Options:
+ * - {Object} headers The key-value headers to pass to each request.
+ * - {String} requestMode The HTTP request mode.
+ *
+ * @param {String} remote The remote URL.
+ * @param {EventEmitter} events The events handler
+ * @param {Object} options The options object.
+ */
+
+ function KintoApi(remote, events) {
+ var options = arguments.length <= 2 || arguments[2] === undefined ? {} : arguments[2];
+
+ _classCallCheck(this, KintoApi);
+
+ if (typeof remote !== "string" || !remote.length) {
+ throw new Error("Invalid remote URL: " + remote);
+ }
+ if (remote[remote.length - 1] === "/") {
+ remote = remote.slice(0, -1);
+ }
+ this._backoffReleaseTime = null;
+ this.remote = remote;
+
+ // public properties
+ /**
+ * The optional generic headers.
+ * @type {Object}
+ */
+ this.optionHeaders = options.headers || {};
+ /**
+ * Current server settings, retrieved from the server.
+ * @type {Object}
+ */
+ this.serverSettings = null;
+ /**
+ * The even emitter instance.
+ * @type {EventEmitter}
+ */
+ this.events = events || new _events.EventEmitter();
+
+ /**
+ * The HTTP instance.
+ * @type {HTTP}
+ */
+ this.http = new _http2.default(this.events, { requestMode: options.requestMode });
+ this._registerHTTPEvents();
+ }
+
+ /**
+ * The remote endpoint base URL. Setting the value will also extract and
+ * validate the version.
+ * @type {String}
+ */
+
+ _createClass(KintoApi, [{
+ key: "_registerHTTPEvents",
+
+ /**
+ * Registers HTTP events.
+ */
+ value: function _registerHTTPEvents() {
+ var _this = this;
+
+ this.events.on("backoff", function (backoffMs) {
+ _this._backoffReleaseTime = backoffMs;
+ });
+ }
+
+ /**
+ * Retrieves available server enpoints.
+ *
+ * Options:
+ * - {Boolean} fullUrl: Retrieve a fully qualified URL (default: true).
+ *
+ * @param {Object} options Options object.
+ * @return {String}
+ */
+
+ }, {
+ key: "endpoints",
+ value: function endpoints() {
+ var options = arguments.length <= 0 || arguments[0] === undefined ? { fullUrl: true } : arguments[0];
+
+ var _root = options.fullUrl ? this.remote : "/" + this.version;
+ var urls = {
+ root: function root() {
+ return _root + "/";
+ },
+ batch: function batch() {
+ return _root + "/batch";
+ },
+ bucket: function bucket(_bucket) {
+ return _root + "/buckets/" + _bucket;
+ },
+ collection: function collection(bucket, coll) {
+ return urls.bucket(bucket) + "/collections/" + coll;
+ },
+ records: function records(bucket, coll) {
+ return urls.collection(bucket, coll) + "/records";
+ },
+ record: function record(bucket, coll, id) {
+ return urls.records(bucket, coll) + "/" + id;
+ }
+ };
+ return urls;
+ }
+
+ /**
+ * Retrieves Kinto server settings.
+ *
+ * @return {Promise}
+ */
+
+ }, {
+ key: "fetchServerSettings",
+ value: function fetchServerSettings() {
+ var _this2 = this;
+
+ if (this.serverSettings) {
+ return Promise.resolve(this.serverSettings);
+ }
+ return this.http.request(this.endpoints().root()).then(function (res) {
+ _this2.serverSettings = res.json.settings;
+ return _this2.serverSettings;
+ });
+ }
+
+ /**
+ * Fetches latest changes from the remote server.
+ *
+ * @param {String} bucketName The bucket name.
+ * @param {String} collName The collection name.
+ * @param {Object} options The options object.
+ * @return {Promise}
+ */
+
+ }, {
+ key: "fetchChangesSince",
+ value: function fetchChangesSince(bucketName, collName) {
+ var _this3 = this;
+
+ var options = arguments.length <= 2 || arguments[2] === undefined ? { lastModified: null, headers: {} } : arguments[2];
+
+ var recordsUrl = this.endpoints().records(bucketName, collName);
+ var queryString = "";
+ var headers = Object.assign({}, this.optionHeaders, options.headers);
+
+ if (options.lastModified) {
+ queryString = "?_since=" + options.lastModified;
+ headers["If-None-Match"] = (0, _utils.quote)(options.lastModified);
+ }
+
+ return this.fetchServerSettings().then(function (_) {
+ return _this3.http.request(recordsUrl + queryString, { headers: headers });
+ }).then(function (res) {
+ // If HTTP 304, nothing has changed
+ if (res.status === 304) {
+ return {
+ lastModified: options.lastModified,
+ changes: []
+ };
+ }
+ // XXX: ETag are supposed to be opaque and stored «as-is».
+ // Extract response data
+ var etag = res.headers.get("ETag"); // e.g. '"42"'
+ etag = etag ? parseInt((0, _utils.unquote)(etag), 10) : options.lastModified;
+ var records = res.json.data;
+
+ // Check if server was flushed
+ var localSynced = options.lastModified;
+ var serverChanged = etag > options.lastModified;
+ var emptyCollection = records ? records.length === 0 : true;
+ if (localSynced && serverChanged && emptyCollection) {
+ throw Error("Server has been flushed.");
+ }
+
+ return { lastModified: etag, changes: records };
+ });
+ }
+
+ /**
+ * Builds an individual record batch request body.
+ *
+ * @param {Object} record The record object.
+ * @param {String} path The record endpoint URL.
+ * @param {Boolean} safe Safe update?
+ * @return {Object} The request body object.
+ */
+
+ }, {
+ key: "_buildRecordBatchRequest",
+ value: function _buildRecordBatchRequest(record, path, safe) {
+ var method = record.deleted ? "DELETE" : "PUT";
+ // Prepare the record body to send with any last_modified property dropped
+ var cleanedRecord = Object.assign({}, record, { last_modified: undefined });
+ var body = record.deleted ? undefined : { data: cleanedRecord };
+ var headers = {};
+ if (safe) {
+ if (record.last_modified) {
+ // Safe replace.
+ headers["If-Match"] = (0, _utils.quote)(record.last_modified);
+ } else if (!record.deleted) {
+ // Safe creation.
+ headers["If-None-Match"] = "*";
+ }
+ }
+ return { method: method, headers: headers, path: path, body: body };
+ }
+
+ /**
+ * Process a batch request response.
+ *
+ * @param {Object} results The results object.
+ * @param {Array} records The initial records list.
+ * @param {Object} response The response HTTP object.
+ * @return {Promise}
+ */
+
+ }, {
+ key: "_processBatchResponses",
+ value: function _processBatchResponses(results, records, response) {
+ // Handle individual batch subrequests responses
+ response.json.responses.forEach(function (response, index) {
+ // TODO: handle 409 when unicity rule is violated (ex. POST with
+ // existing id, unique field, etc.)
+ if (response.status && response.status >= 200 && response.status < 400) {
+ results.published.push(response.body.data);
+ } else if (response.status === 404) {
+ results.skipped.push(records[index]);
+ } else if (response.status === 412) {
+ results.conflicts.push({
+ type: "outgoing",
+ local: records[index],
+ remote: response.body.details && response.body.details.existing || null
+ });
+ } else {
+ results.errors.push({
+ path: response.path,
+ sent: records[index],
+ error: response.body
+ });
+ }
+ });
+ return results;
+ }
+
+ /**
+ * Sends batch update requests to the remote server.
+ *
+ * Options:
+ * - {Object} headers Headers to attach to main and all subrequests.
+ * - {Boolean} safe Safe update (default: `true`)
+ *
+ * @param {String} bucketName The bucket name.
+ * @param {String} collName The collection name.
+ * @param {Array} records The list of record updates to send.
+ * @param {Object} options The options object.
+ * @return {Promise}
+ */
+
+ }, {
+ key: "batch",
+ value: function batch(bucketName, collName, records) {
+ var _this4 = this;
+
+ var options = arguments.length <= 3 || arguments[3] === undefined ? { headers: {} } : arguments[3];
+
+ var safe = options.safe || true;
+ var headers = Object.assign({}, this.optionHeaders, options.headers);
+ var results = {
+ errors: [],
+ published: [],
+ conflicts: [],
+ skipped: []
+ };
+ if (!records.length) {
+ return Promise.resolve(results);
+ }
+ return this.fetchServerSettings().then(function (serverSettings) {
+ // Kinto 1.6.1 possibly exposes multiple setting prefixes
+ var maxRequests = serverSettings["batch_max_requests"] || serverSettings["cliquet.batch_max_requests"];
+ if (maxRequests && records.length > maxRequests) {
+ return Promise.all((0, _utils.partition)(records, maxRequests).map(function (chunk) {
+ return _this4.batch(bucketName, collName, chunk, options);
+ })).then(function (batchResults) {
+ // Assemble responses of chunked batch results into one single
+ // result object
+ return batchResults.reduce(function (acc, batchResult) {
+ Object.keys(batchResult).forEach(function (key) {
+ acc[key] = results[key].concat(batchResult[key]);
+ });
+ return acc;
+ }, results);
+ });
+ }
+ return _this4.http.request(_this4.endpoints().batch(), {
+ method: "POST",
+ headers: headers,
+ body: JSON.stringify({
+ defaults: { headers: headers },
+ requests: records.map(function (record) {
+ var path = _this4.endpoints({ full: false }).record(bucketName, collName, record.id);
+ return _this4._buildRecordBatchRequest(record, path, safe);
+ })
+ })
+ }).then(function (res) {
+ return _this4._processBatchResponses(results, records, res);
+ });
+ });
+ }
+ }, {
+ key: "remote",
+ get: function get() {
+ return this._remote;
+ },
+ set: function set(url) {
+ var version = undefined;
+ try {
+ version = url.match(/\/(v\d+)\/?$/)[1];
+ } catch (err) {
+ throw new Error("The remote URL must contain the version: " + url);
+ }
+ if (version !== SUPPORTED_PROTOCOL_VERSION) {
+ throw new Error("Unsupported protocol version: " + version);
+ }
+ this._remote = url;
+ this._version = version;
+ }
+
+ /**
+ * The current server protocol version, eg. `v1`.
+ * @type {String}
+ */
+
+ }, {
+ key: "version",
+ get: function get() {
+ return this._version;
+ }
+
+ /**
+ * Backoff remaining time, in milliseconds. Defaults to zero if no backoff is
+ * ongoing.
+ *
+ * @return {Number}
+ */
+
+ }, {
+ key: "backoff",
+ get: function get() {
+ var currentTime = new Date().getTime();
+ if (this._backoffReleaseTime && currentTime < this._backoffReleaseTime) {
+ return this._backoffReleaseTime - currentTime;
+ }
+ return 0;
+ }
+ }]);
+
+ return KintoApi;
+}();
+
+exports.default = KintoApi;
+},{"./http.js":11,"./utils.js":13,"events":5,"isomorphic-fetch":3}],13:[function(require,module,exports){
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.quote = quote;
+exports.unquote = unquote;
+exports.partition = partition;
+/**
+ * Returns the specified string with double quotes.
+ *
+ * @param {String} str A string to quote.
+ * @return {String}
+ */
+function quote(str) {
+ return "\"" + str + "\"";
+}
+
+/**
+ * Trim double quotes from specified string.
+ *
+ * @param {String} str A string to unquote.
+ * @return {String}
+ */
+function unquote(str) {
+ return str.replace(/^"/, "").replace(/"$/, "");
+}
+
+/**
+ * Chunks an array into n pieces.
+ *
+ * @param {Array} array
+ * @param {Number} n
+ * @return {Array}
+ */
+function partition(array, n) {
+ if (n <= 0) {
+ return array;
+ }
+ return array.reduce(function (acc, x, i) {
+ if (i === 0 || i % n === 0) {
+ acc.push([x]);
+ } else {
+ acc[acc.length - 1].push(x);
+ }
+ return acc;
+ }, []);
+}
+},{}],14:[function(require,module,exports){
(function (global){
var rng;
if (global.crypto && crypto.getRandomValues) {
// WHATWG crypto-based RNG - http://wiki.whatwg.org/wiki/Crypto
// Moderately fast, high quality
var _rnds8 = new Uint8Array(16);
@@ -1579,17 +2520,17 @@ if (!rng) {
return _rnds;
};
}
module.exports = rng;
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
-},{}],9:[function(require,module,exports){
+},{}],15:[function(require,module,exports){
// uuid.js
//
// Copyright (c) 2010-2012 Robert Kieffer
// MIT License - http://opensource.org/licenses/mit-license.php
// Unique ID creation requires a high quality random # generator. We feature
// detect to determine the best RNG source, normalizing to a function that
// returns 128-bits of randomness, since that's what's usually required
@@ -1764,26 +2705,26 @@ function v4(options, buf, offset) {
var uuid = v4;
uuid.v1 = v1;
uuid.v4 = v4;
uuid.parse = parse;
uuid.unparse = unparse;
module.exports = uuid;
-},{"./rng":8}],10:[function(require,module,exports){
+},{"./rng":14}],16:[function(require,module,exports){
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
-var _api = require("./api");
-
-var _api2 = _interopRequireDefault(_api);
+var _kintoClient = require("kinto-client");
+
+var _kintoClient2 = _interopRequireDefault(_kintoClient);
var _collection = require("./collection");
var _collection2 = _interopRequireDefault(_collection);
var _base = require("./adapters/base");
var _base2 = _interopRequireDefault(_base);
@@ -1842,17 +2783,17 @@ class KintoBase {
remote: DEFAULT_REMOTE
};
this._options = Object.assign(defaults, options);
if (!this._options.adapter) {
throw new Error("No adapter provided");
}
const { remote, events, headers, requestMode } = this._options;
- this._api = new _api2.default(remote, events, { headers, requestMode });
+ this._api = new _kintoClient2.default(remote, events, { headers, requestMode });
// public properties
/**
* The event emitter instance.
* @type {EventEmitter}
*/
this.events = this._options.events;
}
@@ -1878,17 +2819,17 @@ class KintoBase {
dbPrefix: this._options.dbPrefix,
idSchema: options.idSchema,
remoteTransformers: options.remoteTransformers
});
}
}
exports.default = KintoBase;
-},{"./adapters/base":11,"./api":12,"./collection":13}],11:[function(require,module,exports){
+},{"./adapters/base":17,"./collection":18,"kinto-client":12}],17:[function(require,module,exports){
"use strict";
/**
* Base db adapter.
*
* @abstract
*/
@@ -1948,19 +2889,20 @@ class BaseAdapter {
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}
*/
- list() {
+ list(params = { filters: {}, order: "" }) {
throw new Error("Not Implemented.");
}
/**
* Store the lastModified value.
*
* @abstract
* @param {Number} lastModified
@@ -1987,39 +2929,36 @@ class BaseAdapter {
* @return {Promise}
*/
loadDump(records) {
throw new Error("Not Implemented.");
}
}
exports.default = BaseAdapter;
-},{}],12:[function(require,module,exports){
+},{}],18:[function(require,module,exports){
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
-exports.SUPPORTED_PROTOCOL_VERSION = undefined;
+exports.SyncResultObject = undefined;
exports.cleanRecord = cleanRecord;
-var _utils = require("./utils.js");
-
-var _http = require("./http.js");
-
-var _http2 = _interopRequireDefault(_http);
+var _base = require("./adapters/base");
+
+var _base2 = _interopRequireDefault(_base);
+
+var _utils = require("./utils");
+
+var _uuid = require("uuid");
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
const RECORD_FIELDS_TO_CLEAN = ["_status", "last_modified"];
-/**
- * Currently supported protocol version.
- * @type {String}
- */
-const SUPPORTED_PROTOCOL_VERSION = exports.SUPPORTED_PROTOCOL_VERSION = "v1";
/**
* Cleans a record object, excluding passed keys.
*
* @param {Object} record The record object.
* @param {Array} excludeFields The list of keys to exclude.
* @return {Object} A clean copy of source record object.
*/
@@ -2028,341 +2967,16 @@ function cleanRecord(record, excludeFiel
if (excludeFields.indexOf(key) === -1) {
acc[key] = record[key];
}
return acc;
}, {});
}
/**
- * High level HTTP client for the Kinto API.
- */
-class Api {
- /**
- * Constructor.
- *
- * Options:
- * - {Object} headers The key-value headers to pass to each request.
- * - {String} requestMode The HTTP request mode.
- *
- * @param {String} remote The remote URL.
- * @param {EventEmitter} events The events handler
- * @param {Object} options The options object.
- */
- constructor(remote, events, options = {}) {
- if (typeof remote !== "string" || !remote.length) {
- throw new Error("Invalid remote URL: " + remote);
- }
- if (remote[remote.length - 1] === "/") {
- remote = remote.slice(0, -1);
- }
- this._backoffReleaseTime = null;
- this.remote = remote;
-
- // public properties
- /**
- * The optional generic headers.
- * @type {Object}
- */
- this.optionHeaders = options.headers || {};
- /**
- * Current server settings, retrieved from the server.
- * @type {Object}
- */
- this.serverSettings = null;
- /**
- * The even emitter instance.
- * @type {EventEmitter}
- */
- if (!events) {
- throw new Error("No events handler provided");
- }
- this.events = events;
-
- /**
- * The HTTP instance.
- * @type {HTTP}
- */
- this.http = new _http2.default(this.events, { requestMode: options.requestMode });
- this._registerHTTPEvents();
- }
-
- /**
- * The remote endpoint base URL. Setting the value will also extract and
- * validate the version.
- * @type {String}
- */
- get remote() {
- return this._remote;
- }
-
- set remote(url) {
- let version;
- try {
- version = url.match(/\/(v\d+)\/?$/)[1];
- } catch (err) {
- throw new Error("The remote URL must contain the version: " + url);
- }
- if (version !== SUPPORTED_PROTOCOL_VERSION) {
- throw new Error(`Unsupported protocol version: ${ version }`);
- }
- this._remote = url;
- this._version = version;
- }
-
- /**
- * The current server protocol version, eg. `v1`.
- * @type {String}
- */
- get version() {
- return this._version;
- }
-
- /**
- * Backoff remaining time, in milliseconds. Defaults to zero if no backoff is
- * ongoing.
- *
- * @return {Number}
- */
- get backoff() {
- const currentTime = new Date().getTime();
- if (this._backoffReleaseTime && currentTime < this._backoffReleaseTime) {
- return this._backoffReleaseTime - currentTime;
- }
- return 0;
- }
-
- /**
- * Registers HTTP events.
- */
- _registerHTTPEvents() {
- this.events.on("backoff", backoffMs => {
- this._backoffReleaseTime = backoffMs;
- });
- }
-
- /**
- * Retrieves available server enpoints.
- *
- * Options:
- * - {Boolean} fullUrl: Retrieve a fully qualified URL (default: true).
- *
- * @param {Object} options Options object.
- * @return {String}
- */
- endpoints(options = { fullUrl: true }) {
- const root = options.fullUrl ? this.remote : `/${ this.version }`;
- const urls = {
- root: () => `${ root }/`,
- batch: () => `${ root }/batch`,
- bucket: bucket => `${ root }/buckets/${ bucket }`,
- collection: (bucket, coll) => `${ urls.bucket(bucket) }/collections/${ coll }`,
- records: (bucket, coll) => `${ urls.collection(bucket, coll) }/records`,
- record: (bucket, coll, id) => `${ urls.records(bucket, coll) }/${ id }`
- };
- return urls;
- }
-
- /**
- * Retrieves Kinto server settings.
- *
- * @return {Promise}
- */
- fetchServerSettings() {
- if (this.serverSettings) {
- return Promise.resolve(this.serverSettings);
- }
- return this.http.request(this.endpoints().root()).then(res => {
- this.serverSettings = res.json.settings;
- return this.serverSettings;
- });
- }
-
- /**
- * Fetches latest changes from the remote server.
- *
- * @param {String} bucketName The bucket name.
- * @param {String} collName The collection name.
- * @param {Object} options The options object.
- * @return {Promise}
- */
- fetchChangesSince(bucketName, collName, options = { lastModified: null, headers: {} }) {
- const recordsUrl = this.endpoints().records(bucketName, collName);
- let queryString = "";
- const headers = Object.assign({}, this.optionHeaders, options.headers);
-
- if (options.lastModified) {
- queryString = "?_since=" + options.lastModified;
- headers["If-None-Match"] = (0, _utils.quote)(options.lastModified);
- }
-
- return this.fetchServerSettings().then(_ => this.http.request(recordsUrl + queryString, { headers })).then(res => {
- // If HTTP 304, nothing has changed
- if (res.status === 304) {
- return {
- lastModified: options.lastModified,
- changes: []
- };
- }
- // XXX: ETag are supposed to be opaque and stored «as-is».
- // Extract response data
- let etag = res.headers.get("ETag"); // e.g. '"42"'
- etag = etag ? parseInt((0, _utils.unquote)(etag), 10) : options.lastModified;
- const records = res.json.data;
-
- // Check if server was flushed
- const localSynced = options.lastModified;
- const serverChanged = etag > options.lastModified;
- const emptyCollection = records ? records.length === 0 : true;
- if (localSynced && serverChanged && emptyCollection) {
- throw Error("Server has been flushed.");
- }
-
- return { lastModified: etag, changes: records };
- });
- }
-
- /**
- * Builds an individual record batch request body.
- *
- * @param {Object} record The record object.
- * @param {String} path The record endpoint URL.
- * @param {Boolean} safe Safe update?
- * @return {Object} The request body object.
- */
- _buildRecordBatchRequest(record, path, safe) {
- const isDeletion = record._status === "deleted";
- const method = isDeletion ? "DELETE" : "PUT";
- const body = isDeletion ? undefined : { data: cleanRecord(record) };
- const headers = {};
- if (safe) {
- if (record.last_modified) {
- // Safe replace.
- headers["If-Match"] = (0, _utils.quote)(record.last_modified);
- } else if (!isDeletion) {
- // Safe creation.
- headers["If-None-Match"] = "*";
- }
- }
- return { method, headers, path, body };
- }
-
- /**
- * Process a batch request response.
- *
- * @param {Object} results The results object.
- * @param {Array} records The initial records list.
- * @param {Object} response The response HTTP object.
- * @return {Promise}
- */
- _processBatchResponses(results, records, response) {
- // Handle individual batch subrequests responses
- response.json.responses.forEach((response, index) => {
- // TODO: handle 409 when unicity rule is violated (ex. POST with
- // existing id, unique field, etc.)
- if (response.status && response.status >= 200 && response.status < 400) {
- results.published.push(response.body.data);
- } else if (response.status === 404) {
- results.skipped.push(records[index]);
- } else if (response.status === 412) {
- results.conflicts.push({
- type: "outgoing",
- local: records[index],
- remote: response.body.details && response.body.details.existing || null
- });
- } else {
- results.errors.push({
- path: response.path,
- sent: records[index],
- error: response.body
- });
- }
- });
- return results;
- }
-
- /**
- * Sends batch update requests to the remote server.
- *
- * Options:
- * - {Object} headers Headers to attach to main and all subrequests.
- * - {Boolean} safe Safe update (default: `true`)
- *
- * @param {String} bucketName The bucket name.
- * @param {String} collName The collection name.
- * @param {Array} records The list of record updates to send.
- * @param {Object} options The options object.
- * @return {Promise}
- */
- batch(bucketName, collName, records, options = { headers: {} }) {
- const safe = options.safe || true;
- const headers = Object.assign({}, this.optionHeaders, options.headers);
- const results = {
- errors: [],
- published: [],
- conflicts: [],
- skipped: []
- };
- if (!records.length) {
- return Promise.resolve(results);
- }
- return this.fetchServerSettings().then(serverSettings => {
- // Kinto 1.6.1 possibly exposes multiple setting prefixes
- const maxRequests = serverSettings["batch_max_requests"] || serverSettings["cliquet.batch_max_requests"];
- if (maxRequests && records.length > maxRequests) {
- return Promise.all((0, _utils.partition)(records, maxRequests).map(chunk => {
- return this.batch(bucketName, collName, chunk, options);
- })).then(batchResults => {
- // Assemble responses of chunked batch results into one single
- // result object
- return batchResults.reduce((acc, batchResult) => {
- Object.keys(batchResult).forEach(key => {
- acc[key] = results[key].concat(batchResult[key]);
- });
- return acc;
- }, results);
- });
- }
- return this.http.request(this.endpoints().batch(), {
- method: "POST",
- headers: headers,
- body: JSON.stringify({
- defaults: { headers },
- requests: records.map(record => {
- const path = this.endpoints({ full: false }).record(bucketName, collName, record.id);
- return this._buildRecordBatchRequest(record, path, safe);
- })
- })
- }).then(res => this._processBatchResponses(results, records, res));
- });
- }
-}
-exports.default = Api;
-
-},{"./http.js":15,"./utils.js":16}],13:[function(require,module,exports){
-"use strict";
-
-Object.defineProperty(exports, "__esModule", {
- value: true
-});
-exports.SyncResultObject = undefined;
-
-var _base = require("./adapters/base");
-
-var _base2 = _interopRequireDefault(_base);
-
-var _utils = require("./utils");
-
-var _api = require("./api");
-
-var _uuid = require("uuid");
-
-function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
-
-/**
* Synchronization result object.
*/
class SyncResultObject {
/**
* Object default values.
* @type {Object}
*/
static get defaults() {
@@ -2461,17 +3075,17 @@ function importChange(transaction, remot
// avoid recreation.
if (remote.deleted) {
return { type: "skipped", data: remote };
}
const synced = markSynced(remote);
transaction.create(synced);
return { type: "created", data: synced };
}
- const identical = (0, _utils.deepEquals)((0, _api.cleanRecord)(local), (0, _api.cleanRecord)(remote));
+ const identical = (0, _utils.deepEquals)(cleanRecord(local), cleanRecord(remote));
if (local._status !== "synced") {
// Locally deleted, unsynced: scheduled for remote deletion.
if (local._status === "deleted") {
return { type: "skipped", data: local };
}
if (identical) {
// If records are identical, import anyway, so we bump the
// local last_modified value from the server and set record
@@ -2749,22 +3363,17 @@ class Collection {
if (!record.id) {
return Promise.reject(new Error("Cannot update a record missing id."));
}
if (!this.idSchema.validate(record.id)) {
return Promise.reject(new Error(`Invalid Id: ${ record.id }`));
}
return this.get(record.id).then(res => {
const existing = res.data;
- let newStatus = "updated";
- if (record._status === "deleted") {
- newStatus = "deleted";
- } else if (options.synced) {
- newStatus = "synced";
- }
+ const newStatus = options.synced ? "synced" : "updated";
return this.db.execute(transaction => {
const source = options.patch ? Object.assign({}, existing, record) : record;
const updated = markStatus(source, newStatus);
if (existing.last_modified && !updated.last_modified) {
updated.last_modified = existing.last_modified;
}
transaction.update(updated);
return { data: updated, permissions: {} };
@@ -2822,34 +3431,34 @@ class Collection {
});
});
}
/**
* Lists records from the local database.
*
* Params:
- * - {Object} filters The filters to apply (default: `{}`).
+ * - {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}
*/
list(params = {}, options = { includeDeleted: false }) {
params = Object.assign({ order: "-last_modified", filters: {} }, params);
- return this.db.list().then(results => {
- let reduced = (0, _utils.reduceRecords)(params.filters, params.order, results);
+ return this.db.list(params).then(results => {
+ let data = results;
if (!options.includeDeleted) {
- reduced = reduced.filter(record => record._status !== "deleted");
+ data = results.filter(record => record._status !== "deleted");
}
- return { data: reduced, permissions: {} };
+ return { data, permissions: {} };
});
}
/**
* Import changes into the local database.
*
* @param {SyncResultObject} syncResultObject The sync result object.
* @param {Object} changeObject The change object.
@@ -2857,61 +3466,42 @@ class Collection {
*/
importChanges(syncResultObject, changeObject) {
return Promise.all(changeObject.changes.map(change => {
if (change.deleted) {
return Promise.resolve(change);
}
return this._decodeRecord("remote", change);
})).then(decodedChanges => {
- // XXX: list() should filter only ids in changes.
- return this.list({ order: "" }, { includeDeleted: true }).then(res => {
- return { decodedChanges, existingRecords: res.data };
- });
- }).then(({ decodedChanges, existingRecords }) => {
- return this.db.execute(transaction => {
- return decodedChanges.map(remote => {
- // Store remote change into local database.
- return importChange(transaction, remote);
- });
- }, { preload: existingRecords });
- }).catch(err => {
- // XXX todo
- err.type = "incoming";
- // XXX one error of the whole transaction instead of one per atomic op
- return [{ type: "errors", data: err }];
- }).then(imports => {
- var _iteratorNormalCompletion = true;
- var _didIteratorError = false;
- var _iteratorError = undefined;
-
- try {
- for (var _iterator = imports[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
- const imported = _step.value;
-
+ // No change, nothing to import.
+ if (decodedChanges.length === 0) {
+ return Promise.resolve(syncResultObject);
+ }
+ // Retrieve records matching change ids.
+ const remoteIds = decodedChanges.map(change => change.id);
+ return this.list({ filters: { id: remoteIds }, order: "" }, { includeDeleted: true }).then(res => ({ decodedChanges, existingRecords: res.data })).then(({ decodedChanges, existingRecords }) => {
+ return this.db.execute(transaction => {
+ return decodedChanges.map(remote => {
+ // Store remote change into local database.
+ return importChange(transaction, remote);
+ });
+ }, { preload: existingRecords });
+ }).catch(err => {
+ // XXX todo
+ err.type = "incoming";
+ // XXX one error of the whole transaction instead of per atomic op
+ return [{ type: "errors", data: err }];
+ }).then(imports => {
+ for (let imported of imports) {
if (imported.type !== "void") {
syncResultObject.add(imported.type, imported.data);
}
}
- } catch (err) {
- _didIteratorError = true;
- _iteratorError = err;
- } finally {
- try {
- if (!_iteratorNormalCompletion && _iterator.return) {
- _iterator.return();
- }
- } finally {
- if (_didIteratorError) {
- throw _iteratorError;
- }
- }
- }
-
- return syncResultObject;
+ return syncResultObject;
+ });
}).then(syncResultObject => {
syncResultObject.lastModified = changeObject.lastModified;
// Don't persist lastModified value if any conflict or error occured
if (!syncResultObject.ok) {
return syncResultObject;
}
// No conflict occured, persist collection's lastModified value
return this.db.saveLastModified(syncResultObject.lastModified).then(lastModified => {
@@ -2920,34 +3510,33 @@ class Collection {
});
});
}
/**
* 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.
+ * 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.
*/
resetSyncStatus() {
let _count;
- // XXX filter by status
- return this.list({}, { includeDeleted: true }).then(result => {
+ return this.list({ filters: { _status: ["deleted", "synced"] }, order: "" }, { includeDeleted: true }).then(unsynced => {
return this.db.execute(transaction => {
- _count = result.data.length;
- result.data.forEach(r => {
- // Garbage collect deleted records.
- if (r._status === "deleted") {
- transaction.delete(r.id);
+ _count = unsynced.data.length;
+ unsynced.data.forEach(record => {
+ if (record._status === "deleted") {
+ // Garbage collect deleted records.
+ transaction.delete(record.id);
} else {
// Records that were synced become «created».
- transaction.update(Object.assign({}, r, {
+ transaction.update(Object.assign({}, record, {
last_modified: undefined,
_status: "created"
}));
}
});
});
}).then(() => this.db.saveLastModified(null)).then(() => _count);
}
@@ -2957,30 +3546,20 @@ class Collection {
*
* - `toDelete`: unsynced deleted records we can safely delete;
* - `toSync`: local updates to send to the server.
*
* @return {Object}
*/
gatherLocalChanges() {
let _toDelete;
- // XXX filter by status
- return this.list({}, { includeDeleted: true }).then(res => {
- return res.data.reduce((acc, record) => {
- if (record._status === "deleted" && !record.last_modified) {
- acc.toDelete.push(record);
- } else if (record._status !== "synced") {
- acc.toSync.push(record);
- }
- return acc;
- // rename toSync to toPush or toPublish
- }, { toDelete: [], toSync: [] });
- }).then(({ toDelete, toSync }) => {
- _toDelete = toDelete;
- return Promise.all(toSync.map(this._encodeRecord.bind(this, "remote")));
+ return Promise.all([this.list({ filters: { _status: ["created", "updated"] }, order: "" }), this.list({ filters: { _status: "deleted" }, order: "" }, { includeDeleted: true })]).then(([unsynced, deleted]) => {
+ _toDelete = deleted.data;
+ // Encode unsynced records.
+ return Promise.all(unsynced.data.map(this._encodeRecord.bind(this, "remote")));
}).then(toSync => ({ toDelete: _toDelete, toSync }));
}
/**
* 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.
*
@@ -3023,40 +3602,39 @@ class Collection {
if (!syncResultObject.ok) {
return Promise.resolve(syncResultObject);
}
const safe = options.strategy === Collection.SERVER_WINS;
options = Object.assign({ safe }, options);
// Fetch local changes
return this.gatherLocalChanges().then(({ toDelete, toSync }) => {
- return Promise.all([
- // Delete never synced records marked for deletion
- this.db.execute(transaction => {
- toDelete.forEach(record => {
- transaction.delete(record.id);
- });
- }),
+ const batchChanges = toSync.concat(toDelete.map(record => {
+ return cleanRecord(Object.assign({}, record, { deleted: true }));
+ }));
// Send batch update requests
- this.api.batch(this.bucket, this.name, toSync, options)]);
+ return this.api.batch(this.bucket, this.name, batchChanges, options);
})
// Update published local records
- .then(([deleted, synced]) => {
+ .then(synced => {
const { errors, conflicts, published, skipped } = synced;
// Merge outgoing errors into sync result object
syncResultObject.add("errors", errors.map(error => {
error.type = "outgoing";
return error;
}));
// Merge outgoing conflicts into sync result object
syncResultObject.add("conflicts", conflicts);
// Reflect publication results locally
const missingRemotely = skipped.map(r => Object.assign({}, r, { deleted: true }));
const toApplyLocally = published.concat(missingRemotely);
// Deleted records are distributed accross local and missing records
+ // XXX: When tackling the issue to avoid downloading our own changes
+ // from the server. `toDeleteLocally` should be obtained from local db.
+ // See https://github.com/Kinto/kinto.js/issues/144
const toDeleteLocally = toApplyLocally.filter(r => r.deleted);
const toUpdateLocally = toApplyLocally.filter(r => !r.deleted);
// First, apply the decode transformers, if any
return Promise.all(toUpdateLocally.map(record => {
return this._decodeRecord("remote", record);
}))
// Process everything within a single transaction
.then(results => {
@@ -3190,52 +3768,31 @@ class Collection {
* @return {Promise} with the effectively imported records.
*/
loadDump(records) {
const reject = msg => Promise.reject(new Error(msg));
if (!Array.isArray(records)) {
return reject("Records is not an array.");
}
- var _iteratorNormalCompletion2 = true;
- var _didIteratorError2 = false;
- var _iteratorError2 = undefined;
-
- try {
- for (var _iterator2 = records[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) {
- const record = _step2.value;
-
- if (!record.id || !this.idSchema.validate(record.id)) {
- return reject("Record has invalid ID: " + JSON.stringify(record));
- }
-
- if (!record.last_modified) {
- return reject("Record has no last_modified value: " + JSON.stringify(record));
- }
+ for (let record of records) {
+ if (!record.id || !this.idSchema.validate(record.id)) {
+ return reject("Record has invalid ID: " + JSON.stringify(record));
}
- // Fetch all existing records from local database,
- // and skip those who are newer or not marked as synced.
-
- // XXX filter by status / ids in records
- } catch (err) {
- _didIteratorError2 = true;
- _iteratorError2 = err;
- } finally {
- try {
- if (!_iteratorNormalCompletion2 && _iterator2.return) {
- _iterator2.return();
- }
- } finally {
- if (_didIteratorError2) {
- throw _iteratorError2;
- }
+ if (!record.last_modified) {
+ return reject("Record has no last_modified value: " + JSON.stringify(record));
}
}
+ // Fetch all existing records from local database,
+ // and skip those who are newer or not marked as synced.
+
+ // XXX filter by status / ids in records
+
return this.list({}, { includeDeleted: true }).then(res => {
return res.data.reduce((acc, record) => {
acc[record.id] = record;
return acc;
}, {});
}).then(existingById => {
return records.filter(record => {
const localRecord = existingById[record.id];
@@ -3250,242 +3807,26 @@ class Collection {
record.last_modified > localRecord.last_modified;
return shouldKeep;
});
}).then(newRecords => newRecords.map(markSynced)).then(newRecords => this.db.loadDump(newRecords));
}
}
exports.default = Collection;
-},{"./adapters/base":11,"./api":12,"./utils":16,"uuid":9}],14:[function(require,module,exports){
-"use strict";
-
-Object.defineProperty(exports, "__esModule", {
- value: true
-});
-/**
- * Kinto server error code descriptors.
- * @type {Object}
- */
-exports.default = {
- 104: "Missing Authorization Token",
- 105: "Invalid Authorization Token",
- 106: "Request body was not valid JSON",
- 107: "Invalid request parameter",
- 108: "Missing request parameter",
- 109: "Invalid posted data",
- 110: "Invalid Token / id",
- 111: "Missing Token / id",
- 112: "Content-Length header was not provided",
- 113: "Request body too large",
- 114: "Resource was modified meanwhile",
- 115: "Method not allowed on this end point",
- 116: "Requested version not available on this server",
- 117: "Client has sent too many requests",
- 121: "Resource access is forbidden for this user",
- 122: "Another resource violates constraint",
- 201: "Service Temporary unavailable due to high load",
- 202: "Service deprecated",
- 999: "Internal Server Error"
-};
-
-},{}],15:[function(require,module,exports){
-"use strict";
-
-Object.defineProperty(exports, "__esModule", {
- value: true
-});
-
-var _errors = require("./errors.js");
-
-var _errors2 = _interopRequireDefault(_errors);
-
-function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
-
-/**
- * Enhanced HTTP client for the Kinto protocol.
- */
-class HTTP {
- /**
- * Default HTTP request headers applied to each outgoing request.
- *
- * @type {Object}
- */
- static get DEFAULT_REQUEST_HEADERS() {
- return {
- "Accept": "application/json",
- "Content-Type": "application/json"
- };
- }
-
- /**
- * Default options.
- *
- * @type {Object}
- */
- static get defaultOptions() {
- return { timeout: 5000, requestMode: "cors" };
- }
-
- /**
- * Constructor.
- *
- * Options:
- * - {Number} timeout The request timeout in ms (default: `5000`).
- * - {String} requestMode The HTTP request mode (default: `"cors"`).
- *
- * @param {EventEmitter} events The event handler.
- * @param {Object} options The options object.
- */
- constructor(events, options = {}) {
- // public properties
- /**
- * The event emitter instance.
- * @type {EventEmitter}
- */
- if (!events) {
- throw new Error("No events handler provided");
- }
- this.events = events;
-
- options = Object.assign({}, HTTP.defaultOptions, options);
-
- /**
- * The request mode.
- * @see https://fetch.spec.whatwg.org/#requestmode
- * @type {String}
- */
- this.requestMode = options.requestMode;
-
- /**
- * The request timeout.
- * @type {Number}
- */
- this.timeout = options.timeout;
- }
-
- /**
- * Performs an HTTP request to the Kinto server.
- *
- * Options:
- * - `{Object} headers` The request headers object (default: {})
- *
- * Resolves with an objet containing the following HTTP response properties:
- * - `{Number} status` The HTTP status code.
- * - `{Object} json` The JSON response body.
- * - `{Headers} headers` The response headers object; see the ES6 fetch() spec.
- *
- * @param {String} url The URL.
- * @param {Object} options The fetch() options object.
- * @return {Promise}
- */
- request(url, options = { headers: {} }) {
- let response, status, statusText, headers, _timeoutId, hasTimedout;
- // Ensure default request headers are always set
- options.headers = Object.assign({}, HTTP.DEFAULT_REQUEST_HEADERS, options.headers);
- options.mode = this.requestMode;
- return new Promise((resolve, reject) => {
- _timeoutId = setTimeout(() => {
- hasTimedout = true;
- reject(new Error("Request timeout."));
- }, this.timeout);
- fetch(url, options).then(res => {
- if (!hasTimedout) {
- clearTimeout(_timeoutId);
- resolve(res);
- }
- }).catch(err => {
- if (!hasTimedout) {
- clearTimeout(_timeoutId);
- reject(err);
- }
- });
- }).then(res => {
- response = res;
- headers = res.headers;
- status = res.status;
- statusText = res.statusText;
- this._checkForDeprecationHeader(headers);
- this._checkForBackoffHeader(status, headers);
- return res.text();
- })
- // Check if we have a body; if so parse it as JSON.
- .then(text => {
- if (text.length === 0) {
- return null;
- }
- // Note: we can't consume the response body twice.
- return JSON.parse(text);
- }).catch(err => {
- const error = new Error(`HTTP ${ status || 0 }; ${ err }`);
- error.response = response;
- error.stack = err.stack;
- throw error;
- }).then(json => {
- if (json && status >= 400) {
- let message = `HTTP ${ status }; `;
- if (json.errno && json.errno in _errors2.default) {
- message += _errors2.default[json.errno];
- if (json.message) {
- message += `: ${ json.message }`;
- }
- } else {
- message += statusText || "";
- }
- const error = new Error(message.trim());
- error.response = response;
- error.data = json;
- throw error;
- }
- return { status, json, headers };
- });
- }
-
- _checkForDeprecationHeader(headers) {
- const alertHeader = headers.get("Alert");
- if (!alertHeader) {
- return;
- }
- let alert;
- try {
- alert = JSON.parse(alertHeader);
- } catch (err) {
- console.warn("Unable to parse Alert header message", alertHeader);
- return;
- }
- console.warn(alert.message, alert.url);
- this.events.emit("deprecated", alert);
- }
-
- _checkForBackoffHeader(status, headers) {
- let backoffMs;
- const backoffSeconds = parseInt(headers.get("Backoff"), 10);
- if (backoffSeconds > 0) {
- backoffMs = new Date().getTime() + backoffSeconds * 1000;
- } else {
- backoffMs = 0;
- }
- this.events.emit("backoff", backoffMs);
- }
-}
-exports.default = HTTP;
-
-},{"./errors.js":14}],16:[function(require,module,exports){
+},{"./adapters/base":17,"./utils":19,"uuid":15}],19:[function(require,module,exports){
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.deepEquals = deepEquals;
-exports.quote = quote;
-exports.unquote = unquote;
exports.sortObjects = sortObjects;
exports.filterObjects = filterObjects;
exports.reduceRecords = reduceRecords;
-exports.partition = partition;
exports.isUUID = isUUID;
exports.waterfall = waterfall;
exports.pFinally = pFinally;
var _assert = require("assert");
const RE_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
@@ -3501,36 +3842,16 @@ function deepEquals(a, b) {
(0, _assert.deepEqual)(a, b);
} catch (err) {
return false;
}
return true;
}
/**
- * Returns the specified string with double quotes.
- *
- * @param {String} str A string to quote.
- * @return {String}
- */
-function quote(str) {
- return `"${ str }"`;
-}
-
-/**
- * Trim double quotes from specified string.
- *
- * @param {String} str A string to unquote.
- * @return {String}
- */
-function unquote(str) {
- return str.replace(/^"/, "").replace(/"$/, "");
-}
-
-/**
* Checks if a value is undefined.
* @param {Any} value
* @return {Boolean}
*/
function _isUndefined(value) {
return typeof value === "undefined";
}
@@ -3558,23 +3879,27 @@ function sortObjects(order, list) {
return a[field] > b[field] ? direction : -direction;
});
}
/**
* Filters records in a list matching all given filters.
*
* @param {String} filters The filters object.
- * @param {Array} list The collection to order.
+ * @param {Array} list The collection to filter.
* @return {Array}
*/
function filterObjects(filters, list) {
return list.filter(entry => {
return Object.keys(filters).every(filter => {
- return entry[filter] === filters[filter];
+ const value = filters[filter];
+ if (Array.isArray(value)) {
+ return value.some(candidate => candidate === entry[filter]);
+ }
+ return entry[filter] === value;
});
});
}
/**
* Filter and sort list against provided filters and order.
*
* @param {Object} filters The filters to apply.
@@ -3583,37 +3908,16 @@ function filterObjects(filters, list) {
* @return {Array}
*/
function reduceRecords(filters, order, list) {
const filtered = filters ? filterObjects(filters, list) : list;
return order ? sortObjects(order, filtered) : filtered;
}
/**
- * Chunks an array into n pieces.
- *
- * @param {Array} array
- * @param {Number} n
- * @return {Array}
- */
-function partition(array, n) {
- if (n <= 0) {
- return array;
- }
- return array.reduce((acc, x, i) => {
- if (i === 0 || i % n === 0) {
- acc.push([x]);
- } else {
- acc[acc.length - 1].push(x);
- }
- return acc;
- }, []);
-}
-
-/**
* Checks if a string is an UUID.
*
* @param {String} uuid The uuid to validate.
* @return {Boolean}
*/
function isUUID(uuid) {
return RE_UUID.test(uuid);
}
@@ -3644,10 +3948,10 @@ function waterfall(fns, init) {
* @return {Promise}
*/
function pFinally(promise, fn) {
return promise.then(value => Promise.resolve(fn()).then(() => value), reason => Promise.resolve(fn()).then(() => {
throw reason;
}));
}
-},{"assert":3}]},{},[2])(2)
+},{"assert":4}]},{},[2])(2)
});
\ No newline at end of file