--- a/services/common/KintoCertificateBlocklist.js
+++ b/services/common/KintoCertificateBlocklist.js
@@ -3,17 +3,17 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
this.EXPORTED_SYMBOLS = ["OneCRLClient"];
const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
-Cu.import("resource://services-common/moz-kinto-client.js");
+Cu.import("resource://services-common/kinto-offline-client.js");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyServiceGetter(this, "uuidgen",
"@mozilla.org/uuid-generator;1",
"nsIUUIDGenerator");
new file mode 100644
--- /dev/null
+++ b/services/common/kinto-http-client.js
@@ -0,0 +1,1866 @@
+/*
+ *
+ * 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.
+ */
+
+/*
+ * This file is generated from kinto-client.js - do not modify directly.
+ */
+
+this.EXPORTED_SYMBOLS = ["KintoHttpClient"];
+
+/*
+ * Version 0.6.0 - 6b6c736
+ */
+
+(function(f){if(typeof 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.KintoHttpClient = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({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 = undefined;
+
+var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
+
+var _base = require("../src/base");
+
+var _base2 = _interopRequireDefault(_base);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/Timer.jsm");
+Cu.importGlobalProperties(['fetch']);
+const { EventEmitter } = Cu.import("resource://devtools/shared/event-emitter.js", {});
+
+let KintoHttpClient = class KintoHttpClient extends _base2.default {
+ constructor(remote, options = {}) {
+ const events = {};
+ EventEmitter.decorate(events);
+ super(remote, _extends({ events }, options));
+ }
+};
+
+// 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 = KintoHttpClient;
+if (typeof module === "object") {
+ module.exports = KintoHttpClient;
+}
+
+},{"../src/base":2}],2:[function(require,module,exports){
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.default = exports.SUPPORTED_PROTOCOL_VERSION = undefined;
+
+var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
+
+var _dec, _dec2, _dec3, _dec4, _dec5, _dec6, _desc, _value, _class;
+
+var _utils = require("./utils");
+
+var _http = require("./http");
+
+var _http2 = _interopRequireDefault(_http);
+
+var _endpoint = require("./endpoint");
+
+var _endpoint2 = _interopRequireDefault(_endpoint);
+
+var _requests = require("./requests");
+
+var requests = _interopRequireWildcard(_requests);
+
+var _batch = require("./batch");
+
+var _bucket2 = require("./bucket");
+
+var _bucket3 = _interopRequireDefault(_bucket2);
+
+function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+function _applyDecoratedDescriptor(target, property, decorators, descriptor, context) {
+ var desc = {};
+ Object['ke' + 'ys'](descriptor).forEach(function (key) {
+ desc[key] = descriptor[key];
+ });
+ desc.enumerable = !!desc.enumerable;
+ desc.configurable = !!desc.configurable;
+
+ if ('value' in desc || desc.initializer) {
+ desc.writable = true;
+ }
+
+ desc = decorators.slice().reverse().reduce(function (desc, decorator) {
+ return decorator(target, property, desc) || desc;
+ }, desc);
+
+ if (context && desc.initializer !== void 0) {
+ desc.value = desc.initializer ? desc.initializer.call(context) : void 0;
+ desc.initializer = undefined;
+ }
+
+ if (desc.initializer === void 0) {
+ Object['define' + 'Property'](target, property, desc);
+ desc = null;
+ }
+
+ return desc;
+}
+
+/**
+ * Currently supported protocol version.
+ * @type {String}
+ */
+const SUPPORTED_PROTOCOL_VERSION = exports.SUPPORTED_PROTOCOL_VERSION = "v1";
+
+/**
+ * High level HTTP client for the Kinto API.
+ *
+ * @example
+ * const client = new KintoClient("https://kinto.dev.mozaws.net/v1");
+ * client.bucket("default")
+* .collection("my-blog")
+* .createRecord({title: "First article"})
+ * .then(console.log.bind(console))
+ * .catch(console.error.bind(console));
+ */
+let KintoClientBase = (_dec = (0, _utils.nobatch)("This operation is not supported within a batch operation."), _dec2 = (0, _utils.nobatch)("This operation is not supported within a batch operation."), _dec3 = (0, _utils.nobatch)("This operation is not supported within a batch operation."), _dec4 = (0, _utils.nobatch)("This operation is not supported within a batch operation."), _dec5 = (0, _utils.nobatch)("Can't use batch within a batch!"), _dec6 = (0, _utils.support)("1.4", "2.0"), (_class = class KintoClientBase {
+ /**
+ * Constructor.
+ *
+ * @param {String} remote The remote URL.
+ * @param {Object} options The options object.
+ * @param {Boolean} options.safe Adds concurrency headers to every
+ * requests (default: `true`).
+ * @param {EventEmitter} options.events The events handler. If none provided
+ * an `EventEmitter` instance will be created.
+ * @param {Object} options.headers The key-value headers to pass to each
+ * request (default: `{}`).
+ * @param {String} options.bucket The default bucket to use (default:
+ * `"default"`)
+ * @param {String} options.requestMode The HTTP request mode (from ES6 fetch
+ * spec).
+ */
+ constructor(remote, 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;
+
+ /**
+ * Default request options container.
+ * @private
+ * @type {Object}
+ */
+ this.defaultReqOptions = {
+ bucket: options.bucket || "default",
+ headers: options.headers || {},
+ safe: !!options.safe
+ };
+
+ this._options = options;
+ this._requests = [];
+ this._isBatch = !!options.batch;
+
+ // public properties
+ /**
+ * The remote server base URL.
+ * @type {String}
+ */
+ this.remote = remote;
+ /**
+ * Current server information.
+ * @ignore
+ * @type {Object|null}
+ */
+ this.serverInfo = null;
+ /**
+ * The event emitter instance. Should comply with the `EventEmitter`
+ * interface.
+ * @ignore
+ * @type {Class}
+ */
+ this.events = options.events;
+
+ /**
+ * The HTTP instance.
+ * @ignore
+ * @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;
+ }
+
+ /**
+ * @ignore
+ */
+ 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.
+ *
+ * @type {Number}
+ */
+ get backoff() {
+ const currentTime = new Date().getTime();
+ if (this._backoffReleaseTime && currentTime < this._backoffReleaseTime) {
+ return this._backoffReleaseTime - currentTime;
+ }
+ return 0;
+ }
+
+ /**
+ * Registers HTTP events.
+ * @private
+ */
+ _registerHTTPEvents() {
+ this.events.on("backoff", backoffMs => {
+ this._backoffReleaseTime = backoffMs;
+ });
+ }
+
+ /**
+ * Retrieve a bucket object to perform operations on it.
+ *
+ * @param {String} name The bucket name.
+ * @param {Object} options The request options.
+ * @param {Boolean} safe The resulting safe option.
+ * @param {String} bucket The resulting bucket name option.
+ * @param {Object} headers The extended headers object option.
+ * @return {Bucket}
+ */
+ bucket(name, options = {}) {
+ const bucketOptions = (0, _utils.omit)(this._getRequestOptions(options), "bucket");
+ return new _bucket3.default(this, name, bucketOptions);
+ }
+
+ /**
+ * Generates a request options object, deeply merging the client configured
+ * defaults with the ones provided as argument.
+ *
+ * Note: Headers won't be overriden but merged with instance default ones.
+ *
+ * @private
+ * @param {Object} options The request options.
+ * @return {Object}
+ * @property {Boolean} safe The resulting safe option.
+ * @property {String} bucket The resulting bucket name option.
+ * @property {Object} headers The extended headers object option.
+ */
+ _getRequestOptions(options = {}) {
+ return _extends({}, this.defaultReqOptions, options, {
+ batch: this._isBatch,
+ // Note: headers should never be overriden but extended
+ headers: _extends({}, this.defaultReqOptions.headers, options.headers)
+ });
+ }
+
+ /**
+ * Retrieves server information and persist them locally. This operation is
+ * usually performed a single time during the instance lifecycle.
+ *
+ * @return {Promise<Object, Error>}
+ */
+ fetchServerInfo() {
+ if (this.serverInfo) {
+ return Promise.resolve(this.serverInfo);
+ }
+ return this.http.request(this.remote + (0, _endpoint2.default)("root"), {
+ headers: this.defaultReqOptions.headers
+ }).then(({ json }) => {
+ this.serverInfo = json;
+ return this.serverInfo;
+ });
+ }
+
+ /**
+ * Retrieves Kinto server settings.
+ *
+ * @return {Promise<Object, Error>}
+ */
+
+ fetchServerSettings() {
+ return this.fetchServerInfo().then(({ settings }) => settings);
+ }
+
+ /**
+ * Retrieve server capabilities information.
+ *
+ * @return {Promise<Object, Error>}
+ */
+
+ fetchServerCapabilities() {
+ return this.fetchServerInfo().then(({ capabilities }) => capabilities);
+ }
+
+ /**
+ * Retrieve authenticated user information.
+ *
+ * @return {Promise<Object, Error>}
+ */
+
+ fetchUser() {
+ return this.fetchServerInfo().then(({ user }) => user);
+ }
+
+ /**
+ * Retrieve authenticated user information.
+ *
+ * @return {Promise<Object, Error>}
+ */
+
+ fetchHTTPApiVersion() {
+ return this.fetchServerInfo().then(({ http_api_version }) => {
+ return http_api_version;
+ });
+ }
+
+ /**
+ * Process batch requests, chunking them according to the batch_max_requests
+ * server setting when needed.
+ *
+ * @param {Array} requests The list of batch subrequests to perform.
+ * @param {Object} options The options object.
+ * @return {Promise<Object, Error>}
+ */
+ _batchRequests(requests, options = {}) {
+ const headers = _extends({}, this.defaultReqOptions.headers, options.headers);
+ if (!requests.length) {
+ return Promise.resolve([]);
+ }
+ return this.fetchServerSettings().then(serverSettings => {
+ const maxRequests = serverSettings["batch_max_requests"];
+ if (maxRequests && requests.length > maxRequests) {
+ const chunks = (0, _utils.partition)(requests, maxRequests);
+ return (0, _utils.pMap)(chunks, chunk => this._batchRequests(chunk, options));
+ }
+ return this.execute({
+ path: (0, _endpoint2.default)("batch"),
+ method: "POST",
+ headers: headers,
+ body: {
+ defaults: { headers },
+ requests: requests
+ }
+ })
+ // we only care about the responses
+ .then(({ responses }) => responses);
+ });
+ }
+
+ /**
+ * Sends batch requests to the remote server.
+ *
+ * Note: Reserved for internal use only.
+ *
+ * @ignore
+ * @param {Function} fn The function to use for describing batch ops.
+ * @param {Object} options The options object.
+ * @param {Boolean} options.safe The safe option.
+ * @param {String} options.bucket The bucket name option.
+ * @param {Object} options.headers The headers object option.
+ * @param {Boolean} options.aggregate Produces an aggregated result object
+ * (default: `false`).
+ * @return {Promise<Object, Error>}
+ */
+
+ batch(fn, options = {}) {
+ const rootBatch = new KintoClientBase(this.remote, _extends({}, this._options, this._getRequestOptions(options), {
+ batch: true
+ }));
+ let bucketBatch, collBatch;
+ if (options.bucket) {
+ bucketBatch = rootBatch.bucket(options.bucket);
+ if (options.collection) {
+ collBatch = bucketBatch.collection(options.collection);
+ }
+ }
+ const batchClient = collBatch || bucketBatch || rootBatch;
+ try {
+ fn(batchClient);
+ } catch (err) {
+ return Promise.reject(err);
+ }
+ return this._batchRequests(rootBatch._requests, options).then(responses => {
+ if (options.aggregate) {
+ return (0, _batch.aggregate)(responses, rootBatch._requests);
+ }
+ return responses;
+ });
+ }
+
+ /**
+ * Executes an atomic HTTP request.
+ *
+ * @private
+ * @param {Object} request The request object.
+ * @param {Object} options The options object.
+ * @param {Boolean} options.raw Resolve with full response object, including
+ * json body and headers (Default: `false`, so only the json body is
+ * retrieved).
+ * @return {Promise<Object, Error>}
+ */
+ execute(request, options = { raw: false }) {
+ // If we're within a batch, add the request to the stack to send at once.
+ if (this._isBatch) {
+ this._requests.push(request);
+ // Resolve with a message in case people attempt at consuming the result
+ // from within a batch operation.
+ const msg = "This result is generated from within a batch " + "operation and should not be consumed.";
+ return Promise.resolve(options.raw ? { json: msg } : msg);
+ }
+ const promise = this.fetchServerSettings().then(_ => {
+ return this.http.request(this.remote + request.path, _extends({}, request, {
+ body: JSON.stringify(request.body)
+ }));
+ });
+ return options.raw ? promise : promise.then(({ json }) => json);
+ }
+
+ /**
+ * Retrieves the list of buckets.
+ *
+ * @param {Object} options The options object.
+ * @param {Object} options.headers The headers object option.
+ * @return {Promise<Object[], Error>}
+ */
+ listBuckets(options = {}) {
+ return this.execute({
+ path: (0, _endpoint2.default)("buckets"),
+ headers: _extends({}, this.defaultReqOptions.headers, options.headers)
+ });
+ }
+
+ /**
+ * Creates a new bucket on the server.
+ *
+ * @param {String} bucketName The bucket name.
+ * @param {Object} options The options object.
+ * @param {Boolean} options.safe The safe option.
+ * @param {Object} options.headers The headers object option.
+ * @return {Promise<Object, Error>}
+ */
+ createBucket(bucketName, options = {}) {
+ const reqOptions = this._getRequestOptions(options);
+ return this.execute(requests.createBucket(bucketName, reqOptions));
+ }
+
+ /**
+ * Deletes a bucket from the server.
+ *
+ * @ignore
+ * @param {Object|String} bucket The bucket to delete.
+ * @param {Object} options The options object.
+ * @param {Boolean} options.safe The safe option.
+ * @param {Object} options.headers The headers object option.
+ * @return {Promise<Object, Error>}
+ */
+ deleteBucket(bucket, options = {}) {
+ const _bucket = typeof bucket === "object" ? bucket : { id: bucket };
+ const reqOptions = this._getRequestOptions(options);
+ return this.execute(requests.deleteBucket(_bucket, reqOptions));
+ }
+
+ /**
+ * Deletes all buckets on the server.
+ *
+ * @ignore
+ * @param {Object} options The options object.
+ * @param {Boolean} options.safe The safe option.
+ * @param {Object} options.headers The headers object option.
+ * @return {Promise<Object, Error>}
+ */
+
+ deleteBuckets(options = {}) {
+ const reqOptions = this._getRequestOptions(options);
+ return this.execute(requests.deleteBuckets(reqOptions));
+ }
+}, (_applyDecoratedDescriptor(_class.prototype, "fetchServerSettings", [_dec], Object.getOwnPropertyDescriptor(_class.prototype, "fetchServerSettings"), _class.prototype), _applyDecoratedDescriptor(_class.prototype, "fetchServerCapabilities", [_dec2], Object.getOwnPropertyDescriptor(_class.prototype, "fetchServerCapabilities"), _class.prototype), _applyDecoratedDescriptor(_class.prototype, "fetchUser", [_dec3], Object.getOwnPropertyDescriptor(_class.prototype, "fetchUser"), _class.prototype), _applyDecoratedDescriptor(_class.prototype, "fetchHTTPApiVersion", [_dec4], Object.getOwnPropertyDescriptor(_class.prototype, "fetchHTTPApiVersion"), _class.prototype), _applyDecoratedDescriptor(_class.prototype, "batch", [_dec5], Object.getOwnPropertyDescriptor(_class.prototype, "batch"), _class.prototype), _applyDecoratedDescriptor(_class.prototype, "deleteBuckets", [_dec6], Object.getOwnPropertyDescriptor(_class.prototype, "deleteBuckets"), _class.prototype)), _class));
+exports.default = KintoClientBase;
+
+},{"./batch":3,"./bucket":4,"./endpoint":6,"./http":8,"./requests":9,"./utils":10}],3:[function(require,module,exports){
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.aggregate = aggregate;
+/**
+ * Exports batch responses as a result object.
+ *
+ * @private
+ * @param {Array} responses The batch subrequest responses.
+ * @param {Array} requests The initial issued requests.
+ * @return {Object}
+ */
+function aggregate(responses = [], requests = []) {
+ if (responses.length !== requests.length) {
+ throw new Error("Responses length should match requests one.");
+ }
+ const results = {
+ errors: [],
+ published: [],
+ conflicts: [],
+ skipped: []
+ };
+ return responses.reduce((acc, response, index) => {
+ const { status } = response;
+ if (status >= 200 && status < 400) {
+ acc.published.push(response.body);
+ } else if (status === 404) {
+ acc.skipped.push(response.body);
+ } else if (status === 412) {
+ acc.conflicts.push({
+ // XXX: specifying the type is probably superfluous
+ type: "outgoing",
+ local: requests[index].body,
+ remote: response.body.details && response.body.details.existing || null
+ });
+ } else {
+ acc.errors.push({
+ path: response.path,
+ sent: requests[index],
+ error: response.body
+ });
+ }
+ return acc;
+ }, results);
+}
+
+},{}],4:[function(require,module,exports){
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.default = undefined;
+
+var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
+
+var _utils = require("./utils");
+
+var _collection = require("./collection");
+
+var _collection2 = _interopRequireDefault(_collection);
+
+var _requests = require("./requests");
+
+var requests = _interopRequireWildcard(_requests);
+
+var _endpoint = require("./endpoint");
+
+var _endpoint2 = _interopRequireDefault(_endpoint);
+
+function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+/**
+ * Abstract representation of a selected bucket.
+ *
+ */
+let Bucket = class Bucket {
+ /**
+ * Constructor.
+ *
+ * @param {KintoClient} client The client instance.
+ * @param {String} name The bucket name.
+ * @param {Object} options.headers The headers object option.
+ * @param {Boolean} options.safe The safe option.
+ */
+ constructor(client, name, options = {}) {
+ /**
+ * @ignore
+ */
+ this.client = client;
+ /**
+ * The bucket name.
+ * @type {String}
+ */
+ this.name = name;
+ /**
+ * The default options object.
+ * @ignore
+ * @type {Object}
+ */
+ this.options = options;
+ /**
+ * @ignore
+ */
+ this._isBatch = !!options.batch;
+ }
+
+ /**
+ * Merges passed request options with default bucket ones, if any.
+ *
+ * @private
+ * @param {Object} options The options to merge.
+ * @return {Object} The merged options.
+ */
+ _bucketOptions(options = {}) {
+ const headers = _extends({}, this.options && this.options.headers, options.headers);
+ return _extends({}, this.options, options, {
+ headers,
+ bucket: this.name,
+ batch: this._isBatch
+ });
+ }
+
+ /**
+ * Selects a collection.
+ *
+ * @param {String} name The collection name.
+ * @param {Object} options The options object.
+ * @param {Object} options.headers The headers object option.
+ * @param {Boolean} options.safe The safe option.
+ * @return {Collection}
+ */
+ collection(name, options) {
+ return new _collection2.default(this.client, this, name, this._bucketOptions(options));
+ }
+
+ /**
+ * Retrieves bucket properties.
+ *
+ * @param {Object} options The options object.
+ * @param {Object} options.headers The headers object option.
+ * @return {Promise<Object, Error>}
+ */
+ getAttributes(options = {}) {
+ return this.client.execute({
+ path: (0, _endpoint2.default)("bucket", this.name),
+ headers: _extends({}, this.options.headers, options.headers)
+ });
+ }
+
+ /**
+ * Retrieves the list of collections in the current bucket.
+ *
+ * @param {Object} options The options object.
+ * @param {Object} options.headers The headers object option.
+ * @return {Promise<Array<Object>, Error>}
+ */
+ listCollections(options = {}) {
+ return this.client.execute({
+ path: (0, _endpoint2.default)("collections", this.name),
+ headers: _extends({}, this.options.headers, options.headers)
+ });
+ }
+
+ /**
+ * Creates a new collection in current bucket.
+ *
+ * @param {String|undefined} id The collection id.
+ * @param {Object} options The options object.
+ * @param {Boolean} options.safe The safe option.
+ * @param {Object} options.headers The headers object option.
+ * @param {Object} options.permissions The permissions object.
+ * @param {Object} options.data The metadadata object.
+ * @param {Object} options.schema The JSONSchema object.
+ * @return {Promise<Object, Error>}
+ */
+ createCollection(id, options) {
+ const reqOptions = this._bucketOptions(options);
+ const request = requests.createCollection(id, reqOptions);
+ return this.client.execute(request);
+ }
+
+ /**
+ * Deletes a collection from the current bucket.
+ *
+ * @param {Object|String} collection The collection to delete.
+ * @param {Object} options The options object.
+ * @param {Object} options.headers The headers object option.
+ * @param {Boolean} options.safe The safe option.
+ * @return {Promise<Object, Error>}
+ */
+ deleteCollection(collection, options) {
+ const reqOptions = this._bucketOptions(options);
+ const request = requests.deleteCollection((0, _utils.toDataBody)(collection), reqOptions);
+ return this.client.execute(request);
+ }
+
+ /**
+ * Retrieves the list of permissions for this bucket.
+ *
+ * @param {Object} options The options object.
+ * @param {Object} options.headers The headers object option.
+ * @return {Promise<Object, Error>}
+ */
+ getPermissions(options) {
+ return this.getAttributes(this._bucketOptions(options)).then(res => res.permissions);
+ }
+
+ /**
+ * Recplaces all existing bucket permissions with the ones provided.
+ *
+ * @param {Object} permissions The permissions object.
+ * @param {Object} options The options object
+ * @param {Object} options The options object.
+ * @param {Boolean} options.safe The safe option.
+ * @param {Object} options.headers The headers object option.
+ * @param {Object} options.last_modified The last_modified option.
+ * @return {Promise<Object, Error>}
+ */
+ setPermissions(permissions, options = {}) {
+ return this.client.execute(requests.updateBucket({
+ id: this.name,
+ last_modified: options.last_modified
+ }, _extends({}, this._bucketOptions(options), { permissions })));
+ }
+
+ /**
+ * Performs batch operations at the current bucket level.
+ *
+ * @param {Function} fn The batch operation function.
+ * @param {Object} options The options object.
+ * @param {Object} options.headers The headers object option.
+ * @param {Boolean} options.safe The safe option.
+ * @param {Boolean} options.aggregate Produces a grouped result object.
+ * @return {Promise<Object, Error>}
+ */
+ batch(fn, options) {
+ return this.client.batch(fn, this._bucketOptions(options));
+ }
+};
+exports.default = Bucket;
+
+},{"./collection":5,"./endpoint":6,"./requests":9,"./utils":10}],5:[function(require,module,exports){
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.default = undefined;
+
+var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
+
+var _utils = require("./utils");
+
+var _requests = require("./requests");
+
+var requests = _interopRequireWildcard(_requests);
+
+var _endpoint = require("./endpoint");
+
+var _endpoint2 = _interopRequireDefault(_endpoint);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }
+
+/**
+ * Abstract representation of a selected collection.
+ *
+ */
+let Collection = class Collection {
+ /**
+ * Constructor.
+ *
+ * @param {KintoClient} client The client instance.
+ * @param {Bucket} bucket The bucket instance.
+ * @param {String} name The collection name.
+ * @param {Object} options.headers The headers object option.
+ * @param {Boolean} options.safe The safe option.
+ */
+ constructor(client, bucket, name, options = {}) {
+ /**
+ * @ignore
+ */
+ this.client = client;
+ /**
+ * @ignore
+ */
+ this.bucket = bucket;
+ /**
+ * The collection name.
+ * @type {String}
+ */
+ this.name = name;
+
+ /**
+ * The default collection options object, embedding the default bucket ones.
+ * @ignore
+ * @type {Object}
+ */
+ this.options = _extends({}, this.bucket.options, options, {
+ headers: _extends({}, this.bucket.options && this.bucket.options.headers, options.headers)
+ });
+ /**
+ * @ignore
+ */
+ this._isBatch = !!options.batch;
+ }
+
+ /**
+ * Merges passed request options with default bucket and collection ones, if
+ * any.
+ *
+ * @private
+ * @param {Object} options The options to merge.
+ * @return {Object} The merged options.
+ */
+ _collOptions(options = {}) {
+ const headers = _extends({}, this.options && this.options.headers, options.headers);
+ return _extends({}, this.options, options, {
+ headers,
+ // XXX soon to be removed once we've migrated everything from KintoClient
+ bucket: this.bucket.name
+ });
+ }
+
+ /**
+ * Updates current collection properties.
+ *
+ * @private
+ * @param {Object} options The request options.
+ * @return {Promise<Object, Error>}
+ */
+ _updateAttributes(options = {}) {
+ const collection = (0, _utils.toDataBody)(this.name);
+ const reqOptions = this._collOptions(options);
+ const request = requests.updateCollection(collection, reqOptions);
+ return this.client.execute(request);
+ }
+
+ /**
+ * Retrieves collection properties.
+ *
+ * @param {Object} options The options object.
+ * @param {Object} options.headers The headers object option.
+ * @return {Promise<Object, Error>}
+ */
+ getAttributes(options) {
+ const { headers } = this._collOptions(options);
+ return this.client.execute({
+ path: (0, _endpoint2.default)("collection", this.bucket.name, this.name),
+ headers
+ });
+ }
+
+ /**
+ * Retrieves the list of permissions for this collection.
+ *
+ * @param {Object} options The options object.
+ * @param {Object} options.headers The headers object option.
+ * @return {Promise<Object, Error>}
+ */
+ getPermissions(options) {
+ return this.getAttributes(options).then(res => res.permissions);
+ }
+
+ /**
+ * Replaces all existing collection permissions with the ones provided.
+ *
+ * @param {Object} permissions The permissions object.
+ * @param {Object} options The options object
+ * @param {Object} options.headers The headers object option.
+ * @param {Boolean} options.safe The safe option.
+ * @param {Number} options.last_modified The last_modified option.
+ * @return {Promise<Object, Error>}
+ */
+ setPermissions(permissions, options) {
+ return this._updateAttributes(_extends({}, options, { permissions }));
+ }
+
+ /**
+ * Retrieves the JSON schema for this collection, if any.
+ *
+ * @param {Object} options The options object.
+ * @param {Object} options.headers The headers object option.
+ * @return {Promise<Object|null, Error>}
+ */
+ getSchema(options) {
+ return this.getAttributes(options).then(res => res.data && res.data.schema || null);
+ }
+
+ /**
+ * Sets the JSON schema for this collection.
+ *
+ * @param {Object} schema The JSON schema object.
+ * @param {Object} options The options object.
+ * @param {Object} options.headers The headers object option.
+ * @param {Boolean} options.safe The safe option.
+ * @param {Number} options.last_modified The last_modified option.
+ * @return {Promise<Object|null, Error>}
+ */
+ setSchema(schema, options) {
+ return this._updateAttributes(_extends({}, options, { schema }));
+ }
+
+ /**
+ * Retrieves metadata attached to current collection.
+ *
+ * @param {Object} options The options object.
+ * @param {Object} options.headers The headers object option.
+ * @return {Promise<Object, Error>}
+ */
+ getMetadata(options) {
+ return this.getAttributes(options).then(({ data }) => (0, _utils.omit)(data, "schema"));
+ }
+
+ /**
+ * Sets metadata for current collection.
+ *
+ * @param {Object} metadata The metadata object.
+ * @param {Object} options The options object.
+ * @param {Object} options.headers The headers object option.
+ * @param {Boolean} options.safe The safe option.
+ * @param {Number} options.last_modified The last_modified option.
+ * @return {Promise<Object, Error>}
+ */
+ setMetadata(metadata, options) {
+ // Note: patching allows preventing overridding the schema, which lives
+ // within the "data" namespace.
+ return this._updateAttributes(_extends({}, options, { metadata, patch: true }));
+ }
+
+ /**
+ * Creates a record in current collection.
+ *
+ * @param {Object} record The record to create.
+ * @param {Object} options The options object.
+ * @param {Object} options.headers The headers object option.
+ * @param {Boolean} options.safe The safe option.
+ * @return {Promise<Object, Error>}
+ */
+ createRecord(record, options) {
+ const reqOptions = this._collOptions(options);
+ const request = requests.createRecord(this.name, record, reqOptions);
+ return this.client.execute(request);
+ }
+
+ /**
+ * Updates a record in current collection.
+ *
+ * @param {Object} record The record to update.
+ * @param {Object} options The options object.
+ * @param {Object} options.headers The headers object option.
+ * @param {Boolean} options.safe The safe option.
+ * @param {Number} options.last_modified The last_modified option.
+ * @return {Promise<Object, Error>}
+ */
+ updateRecord(record, options) {
+ const reqOptions = this._collOptions(options);
+ const request = requests.updateRecord(this.name, record, reqOptions);
+ return this.client.execute(request);
+ }
+
+ /**
+ * Deletes a record from the current collection.
+ *
+ * @param {Object|String} record The record to delete.
+ * @param {Object} options The options object.
+ * @param {Object} options.headers The headers object option.
+ * @param {Boolean} options.safe The safe option.
+ * @param {Number} options.last_modified The last_modified option.
+ * @return {Promise<Object, Error>}
+ */
+ deleteRecord(record, options) {
+ const reqOptions = this._collOptions(options);
+ const request = requests.deleteRecord(this.name, (0, _utils.toDataBody)(record), reqOptions);
+ return this.client.execute(request);
+ }
+
+ /**
+ * Retrieves a record from the current collection.
+ *
+ * @param {String} id The record id to retrieve.
+ * @param {Object} options The options object.
+ * @param {Object} options.headers The headers object option.
+ * @return {Promise<Object, Error>}
+ */
+ getRecord(id, options) {
+ return this.client.execute(_extends({
+ path: (0, _endpoint2.default)("record", this.bucket.name, this.name, id)
+ }, this._collOptions(options)));
+ }
+
+ /**
+ * Lists records from the current collection.
+ *
+ * Sorting is done by passing a `sort` string option:
+ *
+ * - The field to order the results by, prefixed with `-` for descending.
+ * Default: `-last_modified`.
+ *
+ * @see http://kinto.readthedocs.org/en/latest/api/1.x/cliquet/resource.html#sorting
+ *
+ * Filtering is done by passing a `filters` option object:
+ *
+ * - `{fieldname: "value"}`
+ * - `{min_fieldname: 4000}`
+ * - `{in_fieldname: "1,2,3"}`
+ * - `{not_fieldname: 0}`
+ * - `{exclude_fieldname: "0,1"}`
+ *
+ * @see http://kinto.readthedocs.org/en/latest/api/1.x/cliquet/resource.html#filtering
+ *
+ * Paginating is done by passing a `limit` option, then calling the `next()`
+ * method from the resolved result object to fetch the next page, if any.
+ *
+ * @param {Object} options The options object.
+ * @param {Object} options.headers The headers object option.
+ * @param {Object} options.filters The filters object.
+ * @param {String} options.sort The sort field.
+ * @param {String} options.limit The limit field.
+ * @param {String} options.pages The number of result pages to aggregate.
+ * @param {Number} options.since Only retrieve records modified since the
+ * provided timestamp.
+ * @return {Promise<Object, Error>}
+ */
+ listRecords(options = {}) {
+ const { http } = this.client;
+ const { sort, filters, limit, pages, since } = _extends({
+ sort: "-last_modified"
+ }, options);
+ const collHeaders = this.options.headers;
+ const path = (0, _endpoint2.default)("records", this.bucket.name, this.name);
+ const querystring = (0, _utils.qsify)(_extends({}, filters, {
+ _sort: sort,
+ _limit: limit,
+ _since: since
+ }));
+ let results = [],
+ current = 0;
+
+ const next = function (nextPage) {
+ if (!nextPage) {
+ throw new Error("Pagination exhausted.");
+ }
+ return processNextPage(nextPage);
+ };
+
+ const processNextPage = nextPage => {
+ return http.request(nextPage, { headers: collHeaders }).then(handleResponse);
+ };
+
+ const pageResults = (results, nextPage, etag) => {
+ return {
+ last_modified: etag,
+ data: results,
+ next: next.bind(null, nextPage)
+ };
+ };
+
+ const handleResponse = ({ headers, json }) => {
+ const nextPage = headers.get("Next-Page");
+ // ETag are supposed to be opaque and stored «as-is».
+ const etag = headers.get("ETag");
+ if (!pages) {
+ return pageResults(json.data, nextPage, etag);
+ }
+ // Aggregate new results with previous ones
+ results = results.concat(json.data);
+ current += 1;
+ if (current >= pages || !nextPage) {
+ // Pagination exhausted
+ return pageResults(results, nextPage, etag);
+ }
+ // Follow next page
+ return processNextPage(nextPage);
+ };
+
+ return this.client.execute(_extends({
+ path: path + "?" + querystring
+ }, this._collOptions(options)), { raw: true }).then(handleResponse);
+ }
+
+ /**
+ * Performs batch operations at the current collection level.
+ *
+ * @param {Function} fn The batch operation function.
+ * @param {Object} options The options object.
+ * @param {Object} options.headers The headers object option.
+ * @param {Boolean} options.safe The safe option.
+ * @param {Boolean} options.aggregate Produces a grouped result object.
+ * @return {Promise<Object, Error>}
+ */
+ batch(fn, options) {
+ const reqOptions = this._collOptions(options);
+ return this.client.batch(fn, _extends({}, reqOptions, {
+ collection: this.name
+ }));
+ }
+};
+exports.default = Collection;
+
+},{"./endpoint":6,"./requests":9,"./utils":10}],6:[function(require,module,exports){
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.default = endpoint;
+/**
+ * Endpoints templates.
+ * @type {Object}
+ */
+const ENDPOINTS = {
+ root: () => "/",
+ batch: () => "/batch",
+ buckets: () => "/buckets",
+ bucket: bucket => `/buckets/${ bucket }`,
+ collections: bucket => `${ ENDPOINTS.bucket(bucket) }/collections`,
+ collection: (bucket, coll) => `${ ENDPOINTS.bucket(bucket) }/collections/${ coll }`,
+ records: (bucket, coll) => `${ ENDPOINTS.collection(bucket, coll) }/records`,
+ record: (bucket, coll, id) => `${ ENDPOINTS.records(bucket, coll) }/${ id }`
+};
+
+/**
+ * Retrieves a server enpoint by its name.
+ *
+ * @private
+ * @param {String} name The endpoint name.
+ * @param {...string} args The endpoint parameters.
+ * @return {String}
+ */
+function endpoint(name, ...args) {
+ return ENDPOINTS[name](...args);
+}
+
+},{}],7:[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"
+};
+
+},{}],8:[function(require,module,exports){
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.default = undefined;
+
+var _errors = require("./errors");
+
+var _errors2 = _interopRequireDefault(_errors);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+/**
+ * Enhanced HTTP client for the Kinto protocol.
+ * @private
+ */
+let HTTP = 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, 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) => {
+ const _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 } ${ json.error || "" }: `;
+ if (json.errno && json.errno in _errors2.default) {
+ const errnoMsg = _errors2.default[json.errno];
+ message += errnoMsg;
+ if (json.message && json.message !== errnoMsg) {
+ 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":7}],9:[function(require,module,exports){
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+
+var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
+
+exports.createBucket = createBucket;
+exports.updateBucket = updateBucket;
+exports.deleteBucket = deleteBucket;
+exports.deleteBuckets = deleteBuckets;
+exports.createCollection = createCollection;
+exports.updateCollection = updateCollection;
+exports.deleteCollection = deleteCollection;
+exports.createRecord = createRecord;
+exports.updateRecord = updateRecord;
+exports.deleteRecord = deleteRecord;
+
+var _endpoint = require("./endpoint");
+
+var _endpoint2 = _interopRequireDefault(_endpoint);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+const requestDefaults = {
+ safe: false,
+ // check if we should set default content type here
+ headers: {},
+ bucket: "default",
+ permissions: {},
+ data: {},
+ patch: false
+};
+
+function safeHeader(safe, last_modified) {
+ if (!safe) {
+ return {};
+ }
+ if (last_modified) {
+ return { "If-Match": `"${ last_modified }"` };
+ }
+ return { "If-None-Match": "*" };
+}
+
+/**
+ * @private
+ */
+function createBucket(bucketName, options = {}) {
+ if (!bucketName) {
+ throw new Error("A bucket name is required.");
+ }
+ // Note that we simply ignore any "bucket" option passed here, as the one
+ // we're interested in is the one provided as a required argument.
+ const { headers, permissions, safe } = _extends({}, requestDefaults, options);
+ return {
+ method: "PUT",
+ path: (0, _endpoint2.default)("bucket", bucketName),
+ headers: _extends({}, headers, safeHeader(safe)),
+ body: {
+ // XXX We can't pass the data option just yet, see Kinto/kinto/issues/239
+ permissions
+ }
+ };
+}
+
+/**
+ * @private
+ */
+function updateBucket(bucket, options = {}) {
+ if (typeof bucket !== "object") {
+ throw new Error("A bucket object is required.");
+ }
+ if (!bucket.id) {
+ throw new Error("A bucket id is required.");
+ }
+ const { headers, permissions, safe, patch, last_modified } = _extends({}, requestDefaults, options);
+ return {
+ method: patch ? "PATCH" : "PUT",
+ path: (0, _endpoint2.default)("bucket", bucket.id),
+ headers: _extends({}, headers, safeHeader(safe, last_modified || bucket.last_modified)),
+ body: {
+ data: bucket,
+ permissions
+ }
+ };
+}
+
+/**
+ * @private
+ */
+function deleteBucket(bucket, options = {}) {
+ if (typeof bucket !== "object") {
+ throw new Error("A bucket object is required.");
+ }
+ if (!bucket.id) {
+ throw new Error("A bucket id is required.");
+ }
+ const { headers, safe, last_modified } = _extends({}, requestDefaults, {
+ last_modified: bucket.last_modified
+ }, options);
+ if (safe && !last_modified) {
+ throw new Error("Safe concurrency check requires a last_modified value.");
+ }
+ return {
+ method: "DELETE",
+ path: (0, _endpoint2.default)("bucket", bucket.id),
+ headers: _extends({}, headers, safeHeader(safe, last_modified))
+ };
+}
+
+/**
+ * @private
+ */
+function deleteBuckets(options = {}) {
+ const { headers, safe, last_modified } = _extends({}, requestDefaults, options);
+ if (safe && !last_modified) {
+ throw new Error("Safe concurrency check requires a last_modified value.");
+ }
+ return {
+ method: "DELETE",
+ path: (0, _endpoint2.default)("buckets"),
+ headers: _extends({}, headers, safeHeader(safe, last_modified))
+ };
+}
+
+/**
+ * @private
+ */
+function createCollection(id, options = {}) {
+ const { bucket, headers, permissions, data, safe } = _extends({}, requestDefaults, options);
+ // XXX checks that provided data can't override schema when provided
+ const path = id ? (0, _endpoint2.default)("collection", bucket, id) : (0, _endpoint2.default)("collections", bucket);
+ return {
+ method: id ? "PUT" : "POST",
+ path,
+ headers: _extends({}, headers, safeHeader(safe)),
+ body: { data, permissions }
+ };
+}
+
+/**
+ * @private
+ */
+function updateCollection(collection, options = {}) {
+ if (typeof collection !== "object") {
+ throw new Error("A collection object is required.");
+ }
+ if (!collection.id) {
+ throw new Error("A collection id is required.");
+ }
+ const {
+ bucket,
+ headers,
+ permissions,
+ schema,
+ metadata,
+ safe,
+ patch,
+ last_modified
+ } = _extends({}, requestDefaults, options);
+ const collectionData = _extends({}, metadata, collection);
+ if (options.schema) {
+ collectionData.schema = schema;
+ }
+ return {
+ method: patch ? "PATCH" : "PUT",
+ path: (0, _endpoint2.default)("collection", bucket, collection.id),
+ headers: _extends({}, headers, safeHeader(safe, last_modified || collection.last_modified)),
+ body: {
+ data: collectionData,
+ permissions
+ }
+ };
+}
+
+/**
+ * @private
+ */
+function deleteCollection(collection, options = {}) {
+ if (typeof collection !== "object") {
+ throw new Error("A collection object is required.");
+ }
+ if (!collection.id) {
+ throw new Error("A collection id is required.");
+ }
+ const { bucket, headers, safe, last_modified } = _extends({}, requestDefaults, {
+ last_modified: collection.last_modified
+ }, options);
+ if (safe && !last_modified) {
+ throw new Error("Safe concurrency check requires a last_modified value.");
+ }
+ return {
+ method: "DELETE",
+ path: (0, _endpoint2.default)("collection", bucket, collection.id),
+ headers: _extends({}, headers, safeHeader(safe, last_modified))
+ };
+}
+
+/**
+ * @private
+ */
+function createRecord(collName, record, options = {}) {
+ if (!collName) {
+ throw new Error("A collection name is required.");
+ }
+ const { bucket, headers, permissions, safe } = _extends({}, requestDefaults, options);
+ return {
+ // Note: Safe POST using a record id would fail.
+ // see https://github.com/Kinto/kinto/issues/489
+ method: record.id ? "PUT" : "POST",
+ path: record.id ? (0, _endpoint2.default)("record", bucket, collName, record.id) : (0, _endpoint2.default)("records", bucket, collName),
+ headers: _extends({}, headers, safeHeader(safe)),
+ body: {
+ data: record,
+ permissions
+ }
+ };
+}
+
+/**
+ * @private
+ */
+function updateRecord(collName, record, options = {}) {
+ if (!collName) {
+ throw new Error("A collection name is required.");
+ }
+ if (!record.id) {
+ throw new Error("A record id is required.");
+ }
+ const { bucket, headers, permissions, safe, patch, last_modified } = _extends({}, requestDefaults, options);
+ return {
+ method: patch ? "PATCH" : "PUT",
+ path: (0, _endpoint2.default)("record", bucket, collName, record.id),
+ headers: _extends({}, headers, safeHeader(safe, last_modified || record.last_modified)),
+ body: {
+ data: record,
+ permissions
+ }
+ };
+}
+
+/**
+ * @private
+ */
+function deleteRecord(collName, record, options = {}) {
+ if (!collName) {
+ throw new Error("A collection name is required.");
+ }
+ if (typeof record !== "object") {
+ throw new Error("A record object is required.");
+ }
+ if (!record.id) {
+ throw new Error("A record id is required.");
+ }
+ const { bucket, headers, safe, last_modified } = _extends({}, requestDefaults, {
+ last_modified: record.last_modified
+ }, options);
+ if (safe && !last_modified) {
+ throw new Error("Safe concurrency check requires a last_modified value.");
+ }
+ return {
+ method: "DELETE",
+ path: (0, _endpoint2.default)("record", bucket, collName, record.id),
+ headers: _extends({}, headers, safeHeader(safe, last_modified))
+ };
+}
+
+},{"./endpoint":6}],10:[function(require,module,exports){
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.partition = partition;
+exports.pMap = pMap;
+exports.omit = omit;
+exports.toDataBody = toDataBody;
+exports.qsify = qsify;
+exports.checkVersion = checkVersion;
+exports.support = support;
+exports.nobatch = nobatch;
+/**
+ * Chunks an array into n pieces.
+ *
+ * @private
+ * @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;
+ }, []);
+}
+
+/**
+ * Maps a list to promises using the provided mapping function, executes them
+ * sequentially then returns a Promise resolving with ordered results obtained.
+ * Think of this as a sequential Promise.all.
+ *
+ * @private
+ * @param {Array} list The list to map.
+ * @param {Function} fn The mapping function.
+ * @return {Promise}
+ */
+function pMap(list, fn) {
+ let results = [];
+ return list.reduce((promise, entry) => {
+ return promise.then(() => {
+ return Promise.resolve(fn(entry)).then(result => results = results.concat(result));
+ });
+ }, Promise.resolve()).then(() => results);
+}
+
+/**
+ * Takes an object and returns a copy of it with the provided keys omitted.
+ *
+ * @private
+ * @param {Object} obj The source object.
+ * @param {...String} keys The keys to omit.
+ * @return {Object}
+ */
+function omit(obj, ...keys) {
+ return Object.keys(obj).reduce((acc, key) => {
+ if (keys.indexOf(key) === -1) {
+ acc[key] = obj[key];
+ }
+ return acc;
+ }, {});
+}
+
+/**
+ * Always returns a resource data object from the provided argument.
+ *
+ * @private
+ * @param {Object|String} value
+ * @return {Object}
+ */
+function toDataBody(value) {
+ if (typeof value === "object") {
+ return value;
+ }
+ if (typeof value === "string") {
+ return { id: value };
+ }
+ throw new Error("Invalid collection argument.");
+}
+
+/**
+ * Transforms an object into an URL query string, stripping out any undefined
+ * values.
+ *
+ * @param {Object} obj
+ * @return {String}
+ */
+function qsify(obj) {
+ const sep = "&";
+ const encode = v => encodeURIComponent(typeof v === "boolean" ? String(v) : v);
+ const stripUndefined = o => JSON.parse(JSON.stringify(o));
+ const stripped = stripUndefined(obj);
+ return Object.keys(stripped).map(k => {
+ const ks = encode(k) + "=";
+ if (Array.isArray(stripped[k])) {
+ return stripped[k].map(v => ks + encode(v)).join(sep);
+ } else {
+ return ks + encode(stripped[k]);
+ }
+ }).join(sep);
+}
+
+/**
+ * Checks if a version is within the provided range.
+ *
+ * @param {String} version The version to check.
+ * @param {String} minVersion The minimum supported version (inclusive).
+ * @param {String} maxVersion The minimum supported version (exclusive).
+ * @throws {Error} If the version is outside of the provided range.
+ */
+function checkVersion(version, minVersion, maxVersion) {
+ const extract = str => str.split(".").map(x => parseInt(x, 10));
+ const [verMajor, verMinor] = extract(version);
+ const [minMajor, minMinor] = extract(minVersion);
+ const [maxMajor, maxMinor] = extract(maxVersion);
+ const checks = [verMajor < minMajor, verMajor === minMajor && verMinor < minMinor, verMajor > maxMajor, verMajor === maxMajor && verMinor >= maxMinor];
+ if (checks.some(x => x)) {
+ throw new Error(`Version ${ version } doesn't satisfy ` + `${ minVersion } <= x < ${ maxVersion }`);
+ }
+}
+
+/**
+ * Generates a decorator function ensuring a version check is performed against
+ * the provided requirements before executing it.
+ *
+ * @param {String} min The required min version (inclusive).
+ * @param {String} max The required max version (inclusive).
+ * @return {Function}
+ */
+function support(min, max) {
+ return function (target, key, descriptor) {
+ const fn = descriptor.value;
+ return {
+ configurable: true,
+ get() {
+ const wrappedMethod = (...args) => {
+ // "this" is the current instance which its method is decorated.
+ const client = "client" in this ? this.client : this;
+ return client.fetchHTTPApiVersion().then(version => checkVersion(version, min, max)).then(Promise.resolve(fn.apply(this, args)));
+ };
+ Object.defineProperty(this, key, {
+ value: wrappedMethod,
+ configurable: true,
+ writable: true
+ });
+ return wrappedMethod;
+ }
+ };
+ };
+}
+
+/**
+ * Generates a decorator function ensuring an operation is not performed from
+ * within a batch request.
+ *
+ * @param {String} message The error message to throw.
+ * @return {Function}
+ */
+function nobatch(message) {
+ return function (target, key, descriptor) {
+ const fn = descriptor.value;
+ return {
+ configurable: true,
+ get() {
+ const wrappedMethod = (...args) => {
+ // "this" is the current instance which its method is decorated.
+ if (this._isBatch) {
+ throw new Error(message);
+ }
+ return fn.apply(this, args);
+ };
+ Object.defineProperty(this, key, {
+ value: wrappedMethod,
+ configurable: true,
+ writable: true
+ });
+ return wrappedMethod;
+ }
+ };
+ };
+}
+
+},{}]},{},[1])(1)
+});
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/services/common/kinto-offline-client.js
@@ -0,0 +1,1903 @@
+/*
+ *
+ * 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.
+ */
+
+/*
+ * This file is generated from kinto.js - do not modify directly.
+ */
+
+this.EXPORTED_SYMBOLS = ["loadKinto"];
+
+/*
+ * Version 2.0.0 - 8b846f8
+ */
+
+(function(f){if(typeof 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.loadKinto = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
+"use strict";
+
+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 }; }
+
+/*
+ * 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
+ );`,
+
+ "createCollectionMetadata": `
+ CREATE TABLE collection_metadata (
+ collection_name TEXT PRIMARY KEY,
+ last_modified INTEGER
+ ) WITHOUT ROWID;`,
+
+ "createCollectionDataRecordIdIndex": `
+ CREATE UNIQUE INDEX unique_collection_record
+ ON collection_data(collection_name, record_id);`,
+
+ "clearData": `
+ DELETE FROM collection_data
+ WHERE collection_name = :collection_name;`,
+
+ "createData": `
+ INSERT INTO collection_data (collection_name, record_id, record)
+ VALUES (:collection_name, :record_id, :record);`,
+
+ "updateData": `
+ UPDATE collection_data
+ SET record = :record
+ WHERE collection_name = :collection_name
+ AND record_id = :record_id;`,
+
+ "deleteData": `
+ DELETE FROM collection_data
+ WHERE collection_name = :collection_name
+ AND record_id = :record_id;`,
+
+ "saveLastModified": `
+ REPLACE INTO collection_metadata (collection_name, last_modified)
+ VALUES (:collection_name, :last_modified);`,
+
+ "getLastModified": `
+ SELECT last_modified
+ FROM collection_metadata
+ WHERE collection_name = :collection_name;`,
+
+ "getRecord": `
+ SELECT record
+ FROM collection_data
+ WHERE collection_name = :collection_name
+ AND record_id = :record_id;`,
+
+ "listRecords": `
+ SELECT record
+ FROM collection_data
+ WHERE collection_name = :collection_name;`,
+
+ "importData": `
+ REPLACE INTO collection_data (collection_name, record_id, record)
+ VALUES (:collection_name, :record_id, :record);`
+
+};
+
+const createStatements = ["createCollectionData", "createCollectionMetadata", "createCollectionDataRecordIdIndex"];
+
+const currentSchemaVersion = 1;
+
+class FirefoxAdapter extends _base2.default {
+ constructor(collection) {
+ super();
+ this.collection = collection;
+ }
+
+ _init(connection) {
+ return Task.spawn(function* () {
+ yield connection.executeTransaction(function* doSetup() {
+ const schema = yield connection.getSchemaVersion();
+
+ if (schema == 0) {
+
+ 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;
+ });
+ }
+
+ _executeStatement(statement, params) {
+ if (!this._connection) {
+ throw new Error("The storage adapter is not open");
+ }
+ return this._connection.executeCached(statement, params);
+ }
+
+ open() {
+ const self = this;
+ return Task.spawn(function* () {
+ const opts = { path: "kinto.sqlite", sharedMemoryCache: false };
+ if (!self._connection) {
+ self._connection = yield Sqlite.openConnection(opts).then(self._init);
+ }
+ });
+ }
+
+ close() {
+ if (this._connection) {
+ const promise = this._connection.close();
+ this._connection = null;
+ return promise;
+ }
+ return Promise.resolve();
+ }
+
+ clear() {
+ const params = { collection_name: this.collection };
+ return this._executeStatement(statements.clearData, params);
+ }
+
+ execute(callback, options = { preload: [] }) {
+ if (!this._connection) {
+ throw new Error("The storage adapter is not open");
+ }
+ const preloaded = options.preload.reduce((acc, record) => {
+ acc[record.id] = record;
+ return acc;
+ }, {});
+
+ const proxy = transactionProxy(this.collection, preloaded);
+ let result;
+ try {
+ result = callback(proxy);
+ } catch (e) {
+ return Promise.reject(e);
+ }
+ const conn = this._connection;
+ return conn.executeTransaction(function* doExecuteTransaction() {
+ 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
+ };
+ return this._executeStatement(statements.getRecord, params).then(result => {
+ if (result.length == 0) {
+ return;
+ }
+ return JSON.parse(result[0].getResultByName("record"));
+ });
+ }
+
+ list(params = { filters: {}, order: "" }) {
+ const parameters = {
+ collection_name: this.collection
+ };
+ 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
+ * between every adapters.
+ *
+ * @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() {
+ 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.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);
+ }
+ });
+ return records;
+ });
+ }
+
+ saveLastModified(lastModified) {
+ const parsedLastModified = parseInt(lastModified, 10) || null;
+ const params = {
+ collection_name: this.collection,
+ last_modified: parsedLastModified
+ };
+ return this._executeStatement(statements.saveLastModified, params).then(() => parsedLastModified);
+ }
+
+ getLastModified() {
+ const params = {
+ collection_name: this.collection
+ };
+ return this._executeStatement(statements.getLastModified, params).then(result => {
+ if (result.length == 0) {
+ return 0;
+ }
+ return result[0].getResultByName("last_modified");
+ });
+ }
+}
+
+exports.default = FirefoxAdapter;
+function transactionProxy(collection, preloaded) {
+ const _operations = [];
+
+ return {
+ get operations() {
+ return _operations;
+ },
+
+ create(record) {
+ _operations.push({
+ statement: statements.createData,
+ params: {
+ collection_name: collection,
+ record_id: record.id,
+ record: JSON.stringify(record)
+ }
+ });
+ },
+
+ update(record) {
+ _operations.push({
+ statement: statements.updateData,
+ params: {
+ collection_name: collection,
+ record_id: record.id,
+ record: JSON.stringify(record)
+ }
+ });
+ },
+
+ delete(id) {
+ _operations.push({
+ statement: statements.deleteData,
+ params: {
+ collection_name: collection,
+ record_id: id
+ }
+ });
+ },
+
+ get(id) {
+ // Gecko JS engine outputs undesired warnings if id is not in preloaded.
+ return id in preloaded ? preloaded[id] : undefined;
+ }
+ };
+}
+
+},{"../src/adapters/base":6,"../src/utils":8}],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
+ *
+ * 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 = loadKinto;
+
+var _base = require("../src/adapters/base");
+
+var _base2 = _interopRequireDefault(_base);
+
+var _KintoBase = require("../src/KintoBase");
+
+var _KintoBase2 = _interopRequireDefault(_KintoBase);
+
+var _FirefoxStorage = require("./FirefoxStorage");
+
+var _FirefoxStorage2 = _interopRequireDefault(_FirefoxStorage);
+
+var _utils = require("../src/utils");
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+function loadKinto() {
+ const { EventEmitter } = Cu.import("resource://devtools/shared/event-emitter.js", {});
+ const { generateUUID } = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
+
+ // Use standalone kinto-client module landed in FFx.
+ const { KintoHttpClient } = Cu.import("resource://services-common/kinto-http-client.js");
+
+ Cu.import("resource://gre/modules/Timer.jsm");
+ Cu.importGlobalProperties(['fetch']);
+
+ // Leverage Gecko service to generate UUIDs.
+ function makeIDSchema() {
+ return {
+ validate: _utils.RE_UUID.test.bind(_utils.RE_UUID),
+ generate: function () {
+ return generateUUID().toString().replace(/[{}]/g, "");
+ }
+ };
+ }
+
+ class KintoFX extends _KintoBase2.default {
+ static get adapters() {
+ return {
+ BaseAdapter: _base2.default,
+ FirefoxAdapter: _FirefoxStorage2.default
+ };
+ }
+
+ constructor(options = {}) {
+ const emitter = {};
+ EventEmitter.decorate(emitter);
+
+ const defaults = {
+ events: emitter,
+ ApiClass: KintoHttpClient
+ };
+
+ const expandedOptions = Object.assign(defaults, options);
+ super(expandedOptions);
+ }
+
+ collection(collName, options = {}) {
+ const idSchema = makeIDSchema();
+ const expandedOptions = Object.assign({ idSchema }, options);
+ return super.collection(collName, expandedOptions);
+ }
+ }
+
+ return KintoFX;
+}
+
+// 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":5,"../src/adapters/base":6,"../src/utils":8,"./FirefoxStorage":1}],3:[function(require,module,exports){
+
+},{}],4:[function(require,module,exports){
+'use strict'
+
+function isArguments (object) {
+ return Object.prototype.toString.call(object) === '[object Arguments]'
+}
+
+function deeper (a, b) {
+ return deeper_(a, b, [], [])
+}
+
+module.exports = deeper
+
+try {
+ deeper.fastEqual = require('buffertools').equals
+} catch (e) {
+ // whoops, nobody told buffertools it wasn't installed
+}
+
+/**
+ * This is a Node-specific version of a structural equality test, modeled on
+ * bits and pieces of loads of other implementations of this algorithm, most
+ * notably the one in the Node.js source and the Underscore library. It doesn't
+ * throw and handles cycles.
+ *
+ * Everybody who writes one of these functions puts the documentation
+ * inline, which makes it incredibly hard to follow. Here's what this version
+ * of the algorithm does, in order:
+ *
+ * 1. `===` only tests objects and functions by reference. `null` is an object.
+ * Any pairs of identical entities failing this test are therefore objects
+ * (including `null`), which need to be recursed into and compared attribute by
+ * attribute.
+ * 2. Since the only entities to get to this test must be objects, if `a` or `b`
+ * is not an object, they're clearly not the same. All unfiltered `a` and `b`
+ * getting past this are objects (including `null`).
+ * 3. `null` is an object, but `null === null.` All unfiltered `a` and `b` are
+ * non-null `Objects`.
+ * 4. Buffers need to be special-cased because they live partially on the wrong
+ * side of the C++ / JavaScript barrier. Still, calling this on structures
+ * that can contain Buffers is a bad idea, because they can contain
+ * multiple megabytes of data and comparing them byte-by-byte is hella
+ * expensive.
+ * 5. It's much faster to compare dates by numeric value (`.getTime()`) than by
+ * lexical value.
+ * 6. Compare `RegExps` by their components, not the objects themselves.
+ * 7. Treat argumens objects like arrays. The parts of an arguments list most
+ * people care about are the arguments themselves, not `callee`, which you
+ * shouldn't be looking at anyway.
+ * 8. Objects are more complex:
+ * 1. Ensure that `a` and `b` are on the same constructor chain.
+ * 2. Ensure that `a` and `b` have the same number of own properties (which is
+ * what `Object.keys()` returns).
+ * 3. Ensure that cyclical references don't blow up the stack.
+ * 4. Ensure that all the key names match (faster).
+ * 5. Ensure that all of the associated values match, recursively (slower).
+ *
+ * (somewhat untested) assumptions:
+ *
+ * - Functions are only considered identical if they unify to the same
+ * reference. To anything else is to invite the wrath of the halting problem.
+ * - V8 is smart enough to optimize treating an Array like any other kind of
+ * object.
+ * - Users of this function are cool with mutually recursive data structures
+ * that are otherwise identical being treated as the same.
+ */
+function deeper_ (a, b, ca, cb) {
+ if (a === b) {
+ return true
+ } else if (typeof a !== 'object' || typeof b !== 'object') {
+ return false
+ } else if (a === null || b === null) {
+ return false
+ } else if (Buffer.isBuffer(a) && Buffer.isBuffer(b)) {
+ if (a.equals) {
+ return a.equals(b)
+ } else if (deeper.fastEqual) {
+ return deeper.fastEqual.call(a, b)
+ } else {
+ if (a.length !== b.length) return false
+
+ for (var i = 0; i < a.length; i++) if (a[i] !== b[i]) return false
+
+ return true
+ }
+ } else if (a instanceof Date && b instanceof Date) {
+ return a.getTime() === b.getTime()
+ } else if (a instanceof RegExp && b instanceof RegExp) {
+ return a.source === b.source &&
+ a.global === b.global &&
+ a.multiline === b.multiline &&
+ a.lastIndex === b.lastIndex &&
+ a.ignoreCase === b.ignoreCase
+ } else if (isArguments(a) || isArguments(b)) {
+ if (!(isArguments(a) && isArguments(b))) return false
+
+ var slice = Array.prototype.slice
+ return deeper_(slice.call(a), slice.call(b), ca, cb)
+ } else {
+ if (a.constructor !== b.constructor) return false
+
+ var ka = Object.keys(a)
+ var kb = Object.keys(b)
+ // don't bother with stack acrobatics if there's nothing there
+ if (ka.length === 0 && kb.length === 0) return true
+ if (ka.length !== kb.length) return false
+
+ var cal = ca.length
+ while (cal--) if (ca[cal] === a) return cb[cal] === b
+ ca.push(a); cb.push(b)
+
+ ka.sort(); kb.sort()
+ for (var j = ka.length - 1; j >= 0; j--) if (ka[j] !== kb[j]) return false
+
+ var key
+ for (var k = ka.length - 1; k >= 0; k--) {
+ key = ka[k]
+ if (!deeper_(a[key], b[key], ca, cb)) return false
+ }
+
+ ca.pop(); cb.pop()
+
+ return true
+ }
+}
+
+},{"buffertools":3}],5:[function(require,module,exports){
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+
+var _collection = require("./collection");
+
+var _collection2 = _interopRequireDefault(_collection);
+
+var _base = require("./adapters/base");
+
+var _base2 = _interopRequireDefault(_base);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+const DEFAULT_BUCKET_NAME = "default";
+const DEFAULT_REMOTE = "http://localhost:8888/v1";
+
+/**
+ * KintoBase class.
+ */
+class KintoBase {
+ /**
+ * Provides a public access to the base adapter class. Users can create a
+ * custom DB adapter by extending {@link BaseAdapter}.
+ *
+ * @type {Object}
+ */
+ static get adapters() {
+ return {
+ BaseAdapter: _base2.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}
+ */
+ static get syncStrategy() {
+ return _collection2.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.
+ * - `{String}` `dbPrefix` The DB name prefix.
+ * - `{Object}` `headers` The HTTP headers to use.
+ * - `{String}` `requestMode` The HTTP CORS mode to use.
+ *
+ * @param {Object} options The options object.
+ */
+ constructor(options = {}) {
+ const defaults = {
+ bucket: DEFAULT_BUCKET_NAME,
+ remote: DEFAULT_REMOTE
+ };
+ this._options = Object.assign(defaults, options);
+ if (!this._options.adapter) {
+ throw new Error("No adapter provided");
+ }
+
+ const { remote, events, headers, requestMode, ApiClass } = this._options;
+ this._api = new ApiClass(remote, { events, headers, requestMode });
+
+ // public properties
+ /**
+ * 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 May contain the following fields:
+ * remoteTransformers: Array<RemoteTransformer>
+ * @return {Collection}
+ */
+ collection(collName, options = {}) {
+ if (!collName) {
+ throw new Error("missing collection name");
+ }
+
+ const bucket = this._options.bucket;
+ return new _collection2.default(bucket, collName, this._api, {
+ events: this._options.events,
+ adapter: this._options.adapter,
+ dbPrefix: this._options.dbPrefix,
+ idSchema: options.idSchema,
+ remoteTransformers: options.remoteTransformers,
+ hooks: options.hooks
+ });
+ }
+}
+exports.default = KintoBase;
+
+},{"./adapters/base":6,"./collection":7}],6:[function(require,module,exports){
+"use strict";
+
+/**
+ * Base db adapter.
+ *
+ * @abstract
+ */
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+class BaseAdapter {
+ /**
+ * Opens a connection to the database.
+ *
+ * @abstract
+ * @return {Promise}
+ */
+ open() {
+ return Promise.resolve();
+ }
+
+ /**
+ * Closes current connection to the database.
+ *
+ * @abstract
+ * @return {Promise}
+ */
+ close() {
+ return Promise.resolve();
+ }
+
+ /**
+ * Deletes every records present in the database.
+ *
+ * @abstract
+ * @return {Promise}
+ */
+ 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}
+ */
+ execute(callback, options = { 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}
+ */
+ 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(params = { filters: {}, order: "" }) {
+ throw new Error("Not Implemented.");
+ }
+
+ /**
+ * Store the lastModified value.
+ *
+ * @abstract
+ * @param {Number} lastModified
+ * @return {Promise}
+ */
+ saveLastModified(lastModified) {
+ throw new Error("Not Implemented.");
+ }
+
+ /**
+ * Retrieve saved lastModified value.
+ *
+ * @abstract
+ * @return {Promise}
+ */
+ getLastModified() {
+ throw new Error("Not Implemented.");
+ }
+
+ /**
+ * Load a dump of records exported from a server.
+ *
+ * @abstract
+ * @return {Promise}
+ */
+ loadDump(records) {
+ throw new Error("Not Implemented.");
+ }
+}
+exports.default = BaseAdapter;
+
+},{}],7:[function(require,module,exports){
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.SyncResultObject = undefined;
+exports.cleanRecord = cleanRecord;
+
+var _base = require("./adapters/base");
+
+var _base2 = _interopRequireDefault(_base);
+
+var _utils = require("./utils");
+
+var _uuid = require("uuid");
+
+var _deeper = require("deeper");
+
+var _deeper2 = _interopRequireDefault(_deeper);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+const RECORD_FIELDS_TO_CLEAN = ["_status", "last_modified"];
+const AVAILABLE_HOOKS = ["incoming-changes"];
+
+/**
+ * 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.
+ */
+function cleanRecord(record, excludeFields = RECORD_FIELDS_TO_CLEAN) {
+ return Object.keys(record).reduce((acc, key) => {
+ if (excludeFields.indexOf(key) === -1) {
+ acc[key] = record[key];
+ }
+ return acc;
+ }, {});
+}
+
+/**
+ * Synchronization result object.
+ */
+class SyncResultObject {
+ /**
+ * Object default values.
+ * @type {Object}
+ */
+ static get defaults() {
+ return {
+ ok: true,
+ lastModified: null,
+ errors: [],
+ created: [],
+ updated: [],
+ deleted: [],
+ published: [],
+ conflicts: [],
+ skipped: [],
+ resolved: []
+ };
+ }
+
+ /**
+ * Public constructor.
+ */
+ constructor() {
+ /**
+ * 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}
+ */
+ add(type, entries) {
+ if (!Array.isArray(this[type])) {
+ return;
+ }
+ this[type] = this[type].concat(entries);
+ 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}
+ */
+ reset(type) {
+ this[type] = SyncResultObject.defaults[type];
+ this.ok = this.errors.length + this.conflicts.length === 0;
+ return this;
+ }
+}
+
+exports.SyncResultObject = SyncResultObject;
+function createUUIDSchema() {
+ return {
+ generate() {
+ return (0, _uuid.v4)();
+ },
+
+ validate(id) {
+ return (0, _utils.isUUID)(id);
+ }
+ };
+}
+
+function markStatus(record, status) {
+ return Object.assign({}, 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.
+ * @return {Object}
+ */
+function importChange(transaction, remote) {
+ const 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 };
+ }
+ const synced = markSynced(remote);
+ transaction.create(synced);
+ return { type: "created", data: synced };
+ }
+ const identical = (0, _deeper2.default)(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
+ // status to "synced".
+ const synced = markSynced(remote);
+ transaction.update(synced);
+ return { type: "updated", data: synced, previous: local };
+ }
+ return {
+ type: "conflicts",
+ data: { type: "incoming", local: local, remote: remote }
+ };
+ }
+ if (remote.deleted) {
+ transaction.delete(remote.id);
+ return { type: "deleted", data: { id: local.id } };
+ }
+ const synced = markSynced(remote);
+ transaction.update(synced);
+ // if identical, simply exclude it from all lists
+ const type = identical ? "void" : "updated";
+ return { type, data: synced };
+}
+
+/**
+ * Abstracts a collection of records stored in the local database, providing
+ * CRUD operations and synchronization helpers.
+ */
+class Collection {
+ /**
+ * Constructor.
+ *
+ * Options:
+ * - `{BaseAdapter} adapter` The DB adapter (default: `IDB`)
+ * - `{String} dbPrefix` The DB name prefix (default: `""`)
+ *
+ * @param {String} bucket The bucket identifier.
+ * @param {String} name The collection name.
+ * @param {Api} api The Api instance.
+ * @param {Object} options The options object.
+ */
+ constructor(bucket, name, api, options = {}) {
+ this._bucket = bucket;
+ this._name = name;
+ this._lastModified = null;
+
+ const DBAdapter = options.adapter;
+ if (!DBAdapter) {
+ throw new Error("No adapter provided");
+ }
+ const dbPrefix = options.dbPrefix || "";
+ const db = new DBAdapter(`${ dbPrefix }${ bucket }/${ name }`);
+ if (!(db instanceof _base2.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;
+ this._apiCollection = this.api.bucket(this.bucket).collection(this.name);
+ /**
+ * 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 collection name.
+ * @type {String}
+ */
+ get name() {
+ return this._name;
+ }
+
+ /**
+ * The bucket name.
+ * @type {String}
+ */
+ get bucket() {
+ return this._bucket;
+ }
+
+ /**
+ * The last modified timestamp.
+ * @type {Number}
+ */
+ get lastModified() {
+ 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}
+ */
+ static get strategy() {
+ return {
+ CLIENT_WINS: "client_wins",
+ SERVER_WINS: "server_wins",
+ MANUAL: "manual"
+ };
+ }
+
+ /**
+ * Validates an idSchema.
+ *
+ * @param {Object|undefined} idSchema
+ * @return {Object}
+ */
+ _validateIdSchema(idSchema) {
+ if (typeof idSchema === "undefined") {
+ return createUUIDSchema();
+ }
+ if (typeof 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}
+ */
+ _validateRemoteTransformers(remoteTransformers) {
+ if (typeof remoteTransformers === "undefined") {
+ return [];
+ }
+ if (!Array.isArray(remoteTransformers)) {
+ throw new Error("remoteTransformers should be an array.");
+ }
+ return remoteTransformers.map(transformer => {
+ if (typeof 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}
+ **/
+ _validateHook(hook) {
+ if (!Array.isArray(hook)) {
+ throw new Error("A hook definition should be an array of functions.");
+ }
+ return hook.map(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}
+ */
+ _validateHooks(hooks) {
+ if (typeof hooks === "undefined") {
+ return {};
+ }
+ if (Array.isArray(hooks)) {
+ throw new Error("hooks should be an object, not an array.");
+ }
+ if (typeof hooks !== "object") {
+ throw new Error("hooks should be an object.");
+ }
+
+ const validatedHooks = {};
+
+ for (let hook in hooks) {
+ if (AVAILABLE_HOOKS.indexOf(hook) === -1) {
+ 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}
+ */
+ clear() {
+ return this.db.clear().then(_ => this.db.saveLastModified(null)).then(_ => ({ data: [], permissions: {} }));
+ }
+
+ /**
+ * Encodes a record.
+ *
+ * @param {String} type Either "remote" or "local".
+ * @param {Object} record The record object to encode.
+ * @return {Promise}
+ */
+ _encodeRecord(type, record) {
+ if (!this[`${ type }Transformers`].length) {
+ return Promise.resolve(record);
+ }
+ return (0, _utils.waterfall)(this[`${ type }Transformers`].map(transformer => {
+ return record => transformer.encode(record);
+ }), record);
+ }
+
+ /**
+ * Decodes a record.
+ *
+ * @param {String} type Either "remote" or "local".
+ * @param {Object} record The record object to decode.
+ * @return {Promise}
+ */
+ _decodeRecord(type, record) {
+ if (!this[`${ type }Transformers`].length) {
+ return Promise.resolve(record);
+ }
+ return (0, _utils.waterfall)(this[`${ type }Transformers`].reverse().map(transformer => {
+ return record => transformer.decode(record);
+ }), record);
+ }
+
+ /**
+ * Adds a record to the local database.
+ *
+ * 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}
+ */
+ create(record, options = { useRecordId: false, synced: false }) {
+ const reject = msg => Promise.reject(new Error(msg));
+ if (typeof record !== "object") {
+ return reject("Record is not an object.");
+ }
+ if ((options.synced || options.useRecordId) && !record.id) {
+ return reject("Missing required Id; synced and useRecordId options require one");
+ }
+ if (!options.synced && !options.useRecordId && record.id) {
+ return reject("Extraneous Id; can't create a record having one set.");
+ }
+ const newRecord = Object.assign({}, record, {
+ id: options.synced || options.useRecordId ? record.id : this.idSchema.generate(),
+ _status: options.synced ? "synced" : "created"
+ });
+ if (!this.idSchema.validate(newRecord.id)) {
+ return reject(`Invalid Id: ${ newRecord.id }`);
+ }
+ return this.db.execute(transaction => {
+ transaction.create(newRecord);
+ return { data: newRecord, permissions: {} };
+ }).catch(err => {
+ if (options.useRecordId) {
+ throw new Error("Couldn't create record. It may have been virtually deleted.");
+ }
+ throw err;
+ });
+ }
+
+ /**
+ * 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 {Promise}
+ */
+ update(record, options = { synced: false, patch: false }) {
+ if (typeof record !== "object") {
+ return Promise.reject(new Error("Record is not an object."));
+ }
+ 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;
+ 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: {} };
+ });
+ });
+ }
+
+ /**
+ * Retrieve a record by its id from the local database.
+ *
+ * @param {String} id
+ * @param {Object} options
+ * @return {Promise}
+ */
+ get(id, options = { includeDeleted: false }) {
+ if (!this.idSchema.validate(id)) {
+ return Promise.reject(Error(`Invalid Id: ${ id }`));
+ }
+ return this.db.get(id).then(record => {
+ if (!record || !options.includeDeleted && record._status === "deleted") {
+ throw new Error(`Record with id=${ id } not found.`);
+ } else {
+ return { data: record, permissions: {} };
+ }
+ });
+ }
+
+ /**
+ * 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 {Promise}
+ */
+ delete(id, options = { virtual: true }) {
+ if (!this.idSchema.validate(id)) {
+ return Promise.reject(new Error(`Invalid Id: ${ id }`));
+ }
+ // Ensure the record actually exists.
+ return this.get(id, { includeDeleted: true }).then(res => {
+ const existing = res.data;
+ return this.db.execute(transaction => {
+ // Virtual updates status.
+ if (options.virtual) {
+ transaction.update(markDeleted(existing));
+ } else {
+ // Delete for real.
+ transaction.delete(id);
+ }
+ return { data: { id: id }, permissions: {} };
+ });
+ });
+ }
+
+ /**
+ * 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}
+ */
+ list(params = {}, options = { includeDeleted: false }) {
+ params = Object.assign({ order: "-last_modified", filters: {} }, params);
+ return this.db.list(params).then(results => {
+ let data = results;
+ if (!options.includeDeleted) {
+ data = results.filter(record => record._status !== "deleted");
+ }
+ return { data, permissions: {} };
+ });
+ }
+
+ /**
+ * Import changes into the local database.
+ *
+ * @param {SyncResultObject} syncResultObject The sync result object.
+ * @param {Object} changeObject The change object.
+ * @return {Promise}
+ */
+ importChanges(syncResultObject, changeObject) {
+ return Promise.all(changeObject.changes.map(change => {
+ if (change.deleted) {
+ return Promise.resolve(change);
+ }
+ return this._decodeRecord("remote", change);
+ })).then(decodedChanges => {
+ // 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);
+ }
+ }
+ 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 => {
+ this._lastModified = lastModified;
+ return syncResultObject;
+ });
+ });
+ }
+
+ /**
+ * 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.
+ */
+ resetSyncStatus() {
+ let _count;
+ return this.list({ filters: { _status: ["deleted", "synced"] }, order: "" }, { includeDeleted: true }).then(unsynced => {
+ return this.db.execute(transaction => {
+ _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({}, record, {
+ last_modified: undefined,
+ _status: "created"
+ }));
+ }
+ });
+ });
+ }).then(() => this.db.saveLastModified(null)).then(() => _count);
+ }
+
+ /**
+ * Returns an object containing two lists:
+ *
+ * - `toDelete`: unsynced deleted records we can safely delete;
+ * - `toSync`: local updates to send to the server.
+ *
+ * @return {Object}
+ */
+ gatherLocalChanges() {
+ let _toDelete;
+ 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.
+ *
+ * Options:
+ * - {String} strategy: The selected sync strategy.
+ *
+ * @param {SyncResultObject} syncResultObject
+ * @param {Object} options
+ * @return {Promise}
+ */
+ pullChanges(syncResultObject, options = {}) {
+ if (!syncResultObject.ok) {
+ return Promise.resolve(syncResultObject);
+ }
+ options = Object.assign({
+ strategy: Collection.strategy.MANUAL,
+ lastModified: this.lastModified,
+ headers: {}
+ }, options);
+ // First fetch remote changes from the server
+ return this._apiCollection.listRecords({
+ since: options.lastModified || undefined,
+ headers: options.headers
+ }).then(({ data, last_modified }) => {
+ // last_modified is the ETag header value (string).
+ // For retro-compatibility with first kinto.js versions
+ // parse it to integer.
+ const unquoted = last_modified ? parseInt(last_modified.replace(/"/g, ""), 10) : undefined;
+
+ // Check if server was flushed.
+ // This is relevant for the Kinto demo server
+ // (and thus for many new comers).
+ const localSynced = options.lastModified;
+ const serverChanged = unquoted > options.lastModified;
+ const emptyCollection = data.length === 0;
+ if (localSynced && serverChanged && emptyCollection) {
+ throw Error("Server has been flushed.");
+ }
+
+ const payload = { lastModified: unquoted, changes: data };
+ return this.applyHook("incoming-changes", payload);
+ })
+ // Reflect these changes locally
+ .then(changes => this.importChanges(syncResultObject, changes))
+ // Handle conflicts, if any
+ .then(result => this._handleConflicts(result, options.strategy));
+ }
+
+ applyHook(hookName, payload) {
+ if (typeof this.hooks[hookName] == "undefined") {
+ return Promise.resolve(payload);
+ }
+ return (0, _utils.waterfall)(this.hooks[hookName].map(hook => {
+ return record => hook(payload, this);
+ }), payload);
+ }
+
+ /**
+ * Publish local changes to the remote server and updates the passed
+ * {@link SyncResultObject} with publication results.
+ *
+ * @param {SyncResultObject} syncResultObject The sync result object.
+ * @param {Object} options The options object.
+ * @return {Promise}
+ */
+ pushChanges(syncResultObject, options = {}) {
+ 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 }) => {
+ // Send batch update requests
+ return this._apiCollection.batch(batch => {
+ toDelete.forEach(r => {
+ // never published locally deleted records should not be pusblished
+ if (r.last_modified) {
+ batch.deleteRecord(r);
+ }
+ });
+ toSync.forEach(r => {
+ const isCreated = r._status === "created";
+ // Do not store status on server.
+ // XXX: cleanRecord() removes last_modified, required by safe.
+ delete r._status;
+ if (isCreated) {
+ batch.createRecord(r);
+ } else {
+ batch.updateRecord(r);
+ }
+ });
+ }, { headers: options.headers, safe: true, aggregate: true });
+ })
+ // Update published local records
+ .then(synced => {
+ // Merge outgoing errors into sync result object
+ syncResultObject.add("errors", synced.errors.map(error => {
+ error.type = "outgoing";
+ return error;
+ }));
+
+ // The result of a batch returns data and permissions.
+ // XXX: permissions are ignored currently.
+ const conflicts = synced.conflicts.map(c => {
+ return { type: c.type, local: c.local.data, remote: c.remote };
+ });
+ const published = synced.published.map(c => c.data);
+ const skipped = synced.skipped.map(c => c.data);
+
+ // 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 => {
+ return this.db.execute(transaction => {
+ const updated = results.map(record => {
+ const synced = markSynced(record);
+ transaction.update(synced);
+ return { data: synced };
+ });
+ const deleted = toDeleteLocally.map(record => {
+ transaction.delete(record.id);
+ // Amend result data with the deleted attribute set
+ return { data: { id: record.id, deleted: true } };
+ });
+ return updated.concat(deleted);
+ });
+ }).then(published => {
+ syncResultObject.add("published", published.map(res => res.data));
+ return syncResultObject;
+ });
+ })
+ // Handle conflicts, if any
+ .then(result => this._handleConflicts(result, options.strategy)).then(result => {
+ const resolvedUnsynced = result.resolved.filter(record => record._status !== "synced");
+ // No resolved conflict to reflect anywhere
+ if (resolvedUnsynced.length === 0 || options.resolved) {
+ return result;
+ } else if (options.strategy === Collection.strategy.CLIENT_WINS && !options.resolved) {
+ // We need to push local versions of the records to the server
+ return this.pushChanges(result, Object.assign({}, options, { resolved: true }));
+ } else if (options.strategy === Collection.strategy.SERVER_WINS) {
+ // If records have been automatically resolved according to strategy and
+ // are in non-synced status, mark them as synced.
+ return this.db.execute(transaction => {
+ resolvedUnsynced.forEach(record => {
+ transaction.update(markSynced(record));
+ });
+ return result;
+ });
+ }
+ });
+ }
+
+ /**
+ * 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}
+ */
+ resolve(conflict, resolution) {
+ return this.update(Object.assign({}, resolution, {
+ // Ensure local record has the latest authoritative timestamp
+ last_modified: conflict.remote.last_modified
+ }));
+ }
+
+ /**
+ * Handles synchronization conflicts according to specified strategy.
+ *
+ * @param {SyncResultObject} result The sync result object.
+ * @param {String} strategy The {@link Collection.strategy}.
+ * @return {Promise}
+ */
+ _handleConflicts(result, strategy = Collection.strategy.MANUAL) {
+ if (strategy === Collection.strategy.MANUAL || result.conflicts.length === 0) {
+ return Promise.resolve(result);
+ }
+ return Promise.all(result.conflicts.map(conflict => {
+ const resolution = strategy === Collection.strategy.CLIENT_WINS ? conflict.local : conflict.remote;
+ return this.resolve(conflict, resolution);
+ })).then(imports => {
+ return result.reset("conflicts").add("resolved", imports.map(res => res.data));
+ });
+ }
+
+ /**
+ * 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.
+ * - {Collection.strategy} strategy: See {@link Collection.strategy}.
+ * - {Boolean} ignoreBackoff: Force synchronization even if server is currently
+ * backed off.
+ * - {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.
+ */
+ sync(options = {
+ strategy: Collection.strategy.MANUAL,
+ headers: {},
+ ignoreBackoff: false,
+ remote: null
+ }) {
+ const 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) {
+ const seconds = Math.ceil(this.api.backoff / 1000);
+ return Promise.reject(new Error(`Server is asking clients to back off; retry in ${ seconds }s or use the ignoreBackoff option.`));
+ }
+ const result = new SyncResultObject();
+ const syncPromise = this.db.getLastModified().then(lastModified => this._lastModified = lastModified).then(_ => this.pullChanges(result, options)).then(result => this.pushChanges(result, options)).then(result => {
+ // Avoid performing a last pull if nothing has been published.
+ if (result.published.length === 0) {
+ return result;
+ }
+ return this.pullChanges(result, options);
+ });
+ // Ensure API default remote is reverted if a custom one's been used
+ return (0, _utils.pFinally)(syncPromise, () => this.api.remote = previousRemote);
+ }
+
+ /**
+ * 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.
+ */
+ loadDump(records) {
+ const reject = msg => Promise.reject(new Error(msg));
+ if (!Array.isArray(records)) {
+ return reject("Records is not an array.");
+ }
+
+ for (let record of records) {
+ 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));
+ }
+ }
+
+ // 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];
+ const 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;
+ });
+ }).then(newRecords => newRecords.map(markSynced)).then(newRecords => this.db.loadDump(newRecords));
+ }
+}
+exports.default = Collection;
+
+},{"./adapters/base":6,"./utils":8,"deeper":4,"uuid":3}],8:[function(require,module,exports){
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.sortObjects = sortObjects;
+exports.filterObjects = filterObjects;
+exports.reduceRecords = reduceRecords;
+exports.isUUID = isUUID;
+exports.waterfall = waterfall;
+exports.pFinally = pFinally;
+const RE_UUID = exports.RE_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
+
+/**
+ * Checks if a value is undefined.
+ * @param {Any} value
+ * @return {Boolean}
+ */
+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) {
+ const hasDash = order[0] === "-";
+ const field = hasDash ? order.slice(1) : order;
+ const direction = hasDash ? -1 : 1;
+ return list.slice().sort((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;
+ });
+}
+
+/**
+ * Filters records in a list matching all given filters.
+ *
+ * @param {String} filters The filters object.
+ * @param {Array} list The collection to filter.
+ * @return {Array}
+ */
+function filterObjects(filters, list) {
+ return list.filter(entry => {
+ return Object.keys(filters).every(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.
+ * @param {String} order The order to apply.
+ * @param {Array} list The list to reduce.
+ * @return {Array}
+ */
+function reduceRecords(filters, order, list) {
+ const filtered = filters ? filterObjects(filters, list) : list;
+ return order ? sortObjects(order, filtered) : filtered;
+}
+
+/**
+ * Checks if a string is an UUID.
+ *
+ * @param {String} uuid The uuid to validate.
+ * @return {Boolean}
+ */
+function isUUID(uuid) {
+ return RE_UUID.test(uuid);
+}
+
+/**
+ * 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((promise, nextFn) => {
+ return promise.then(nextFn);
+ }, Promise.resolve(init));
+}
+
+/**
+ * Ensure a callback is always executed at the end of the passed promise flow.
+ *
+ * @link https://github.com/domenic/promises-unwrapping/issues/18
+ * @param {Promise} promise The promise.
+ * @param {Function} fn The callback.
+ * @return {Promise}
+ */
+function pFinally(promise, fn) {
+ return promise.then(value => Promise.resolve(fn()).then(() => value), reason => Promise.resolve(fn()).then(() => {
+ throw reason;
+ }));
+}
+
+},{}]},{},[2])(2)
+});
\ No newline at end of file
deleted file mode 100644
--- a/services/common/moz-kinto-client.js
+++ /dev/null
@@ -1,3653 +0,0 @@
-/*
- *
- * 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.
- */
-
-/*
- * This file is generated from kinto.js - do not modify directly.
- */
-
-this.EXPORTED_SYMBOLS = ["loadKinto"];
-(function(f){if(typeof 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.loadKinto = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
-"use strict";
-
-Object.defineProperty(exports, "__esModule", {
- value: true
-});
-
-var _base = require("../src/adapters/base");
-
-var _base2 = _interopRequireDefault(_base);
-
-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.
- */
-
-Components.utils.import("resource://gre/modules/Task.jsm");
-
-const statements = {
- "createCollectionData": `
- CREATE TABLE collection_data (
- collection_name TEXT,
- record_id TEXT,
- record TEXT
- );`,
-
- "createCollectionMetadata": `
- CREATE TABLE collection_metadata (
- collection_name TEXT PRIMARY KEY,
- last_modified INTEGER
- ) WITHOUT ROWID;`,
-
- "createCollectionDataRecordIdIndex": `
- CREATE UNIQUE INDEX unique_collection_record
- ON collection_data(collection_name, record_id);`,
-
- "clearData": `
- DELETE FROM collection_data
- WHERE collection_name = :collection_name;`,
-
- "createData": `
- INSERT INTO collection_data (collection_name, record_id, record)
- VALUES (:collection_name, :record_id, :record);`,
-
- "updateData": `
- UPDATE collection_data
- SET record = :record
- WHERE collection_name = :collection_name
- AND record_id = :record_id;`,
-
- "deleteData": `
- DELETE FROM collection_data
- WHERE collection_name = :collection_name
- AND record_id = :record_id;`,
-
- "saveLastModified": `
- REPLACE INTO collection_metadata (collection_name, last_modified)
- VALUES (:collection_name, :last_modified);`,
-
- "getLastModified": `
- SELECT last_modified
- FROM collection_metadata
- WHERE collection_name = :collection_name;`,
-
- "getRecord": `
- SELECT record
- FROM collection_data
- WHERE collection_name = :collection_name
- AND record_id = :record_id;`,
-
- "listRecords": `
- SELECT record
- FROM collection_data
- WHERE collection_name = :collection_name;`,
-
- "importData": `
- REPLACE INTO collection_data (collection_name, record_id, record)
- VALUES (:collection_name, :record_id, :record);`
-
-};
-
-const createStatements = ["createCollectionData", "createCollectionMetadata", "createCollectionDataRecordIdIndex"];
-
-const currentSchemaVersion = 1;
-
-class FirefoxAdapter extends _base2.default {
- constructor(collection) {
- super();
- this.collection = collection;
- }
-
- _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;
- }
- }
- }
-
- yield connection.setSchemaVersion(currentSchemaVersion);
- } else if (schema != 1) {
- throw new Error("Unknown database schema: " + schema);
- }
- });
- return connection;
- });
- }
-
- _executeStatement(statement, params) {
- if (!this._connection) {
- throw new Error("The storage adapter is not open");
- }
- return this._connection.executeCached(statement, params);
- }
-
- open() {
- const self = this;
- return Task.spawn(function* () {
- const opts = { path: "kinto.sqlite", sharedMemoryCache: false };
- if (!self._connection) {
- self._connection = yield Sqlite.openConnection(opts).then(self._init);
- }
- });
- }
-
- close() {
- if (this._connection) {
- const promise = this._connection.close();
- this._connection = null;
- return promise;
- }
- return Promise.resolve();
- }
-
- clear() {
- const params = { collection_name: this.collection };
- return this._executeStatement(statements.clearData, params);
- }
-
- execute(callback, options = { preload: [] }) {
- if (!this._connection) {
- throw new Error("The storage adapter is not open");
- }
- const preloaded = options.preload.reduce((acc, record) => {
- acc[record.id] = record;
- return acc;
- }, {});
-
- const proxy = transactionProxy(this.collection, preloaded);
- 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;
- }
- }
- }
- }).then(_ => result);
- }
-
- get(id) {
- const params = {
- collection_name: this.collection,
- record_id: id
- };
- return this._executeStatement(statements.getRecord, params).then(result => {
- if (result.length == 0) {
- return;
- }
- return JSON.parse(result[0].getResultByName("record"));
- });
- }
-
- list() {
- const params = {
- collection_name: this.collection
- };
- return this._executeStatement(statements.listRecords, params).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;
- });
- }
-
- /**
- * 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
- * between every adapters.
- *
- * @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;
- }
- }
- }
-
- 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;
- });
- if (lastModified > previousLastModified) {
- const params = {
- collection_name: collection_name,
- last_modified: lastModified
- };
- yield connection.execute(statements.saveLastModified, params);
- }
- });
- return records;
- });
- }
-
- saveLastModified(lastModified) {
- const parsedLastModified = parseInt(lastModified, 10) || null;
- const params = {
- collection_name: this.collection,
- last_modified: parsedLastModified
- };
- return this._executeStatement(statements.saveLastModified, params).then(() => parsedLastModified);
- }
-
- getLastModified() {
- const params = {
- collection_name: this.collection
- };
- return this._executeStatement(statements.getLastModified, params).then(result => {
- if (result.length == 0) {
- return 0;
- }
- return result[0].getResultByName("last_modified");
- });
- }
-}
-
-exports.default = FirefoxAdapter;
-function transactionProxy(collection, preloaded) {
- const _operations = [];
-
- return {
- get operations() {
- return _operations;
- },
-
- create(record) {
- _operations.push({
- statement: statements.createData,
- params: {
- collection_name: collection,
- record_id: record.id,
- record: JSON.stringify(record)
- }
- });
- },
-
- update(record) {
- _operations.push({
- statement: statements.updateData,
- params: {
- collection_name: collection,
- record_id: record.id,
- record: JSON.stringify(record)
- }
- });
- },
-
- delete(id) {
- _operations.push({
- statement: statements.deleteData,
- params: {
- collection_name: collection,
- record_id: id
- }
- });
- },
-
- 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){
-/*
- *
- * 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 = loadKinto;
-
-var _base = require("../src/adapters/base");
-
-var _base2 = _interopRequireDefault(_base);
-
-var _KintoBase = require("../src/KintoBase");
-
-var _KintoBase2 = _interopRequireDefault(_KintoBase);
-
-var _FirefoxStorage = require("./FirefoxStorage");
-
-var _FirefoxStorage2 = _interopRequireDefault(_FirefoxStorage);
-
-function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
-
-const Cu = Components.utils;
-
-function loadKinto() {
- const { EventEmitter } = Cu.import("resource://devtools/shared/event-emitter.js", {});
-
- Cu.import("resource://gre/modules/Timer.jsm");
- Cu.importGlobalProperties(['fetch']);
-
- class KintoFX extends _KintoBase2.default {
- static get adapters() {
- return {
- BaseAdapter: _base2.default,
- FirefoxAdapter: _FirefoxStorage2.default
- };
- }
-
- constructor(options = {}) {
- const emitter = {};
- EventEmitter.decorate(emitter);
-
- const defaults = {
- events: emitter
- };
-
- const expandedOptions = Object.assign(defaults, options);
- super(expandedOptions);
- }
- }
-
- return KintoFX;
-}
-
-// 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){
-// 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
-// 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 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.
-
-// when used in node, this will actually load the util module we depend on
-// versus loading the builtin util module as happens otherwise
-// this is a bug in node module loading as far as I am concerned
-var util = require('util/');
-
-var pSlice = Array.prototype.slice;
-var hasOwn = Object.prototype.hasOwnProperty;
-
-// 1. The assert module provides functions that throw
-// AssertionError's when particular conditions are not met. The
-// assert module must conform to the following interface.
-
-var assert = module.exports = ok;
-
-// 2. The AssertionError is defined in assert.
-// new assert.AssertionError({ message: message,
-// actual: actual,
-// expected: expected })
-
-assert.AssertionError = function AssertionError(options) {
- this.name = 'AssertionError';
- this.actual = options.actual;
- this.expected = options.expected;
- this.operator = options.operator;
- if (options.message) {
- this.message = options.message;
- this.generatedMessage = false;
- } else {
- this.message = getMessage(this);
- this.generatedMessage = true;
- }
- var stackStartFunction = options.stackStartFunction || fail;
-
- if (Error.captureStackTrace) {
- Error.captureStackTrace(this, stackStartFunction);
- }
- else {
- // non v8 browsers so we can have a stacktrace
- var err = new Error();
- if (err.stack) {
- var out = err.stack;
-
- // try to strip useless frames
- var fn_name = stackStartFunction.name;
- var idx = out.indexOf('\n' + fn_name);
- if (idx >= 0) {
- // once we have located the function frame
- // we need to strip out everything before it (and its line)
- var next_line = out.indexOf('\n', idx + 1);
- out = out.substring(next_line + 1);
- }
-
- this.stack = out;
- }
- }
-};
-
-// assert.AssertionError instanceof Error
-util.inherits(assert.AssertionError, Error);
-
-function replacer(key, value) {
- if (util.isUndefined(value)) {
- return '' + value;
- }
- if (util.isNumber(value) && !isFinite(value)) {
- return value.toString();
- }
- if (util.isFunction(value) || util.isRegExp(value)) {
- return value.toString();
- }
- return value;
-}
-
-function truncate(s, n) {
- if (util.isString(s)) {
- return s.length < n ? s : s.slice(0, n);
- } else {
- return s;
- }
-}
-
-function getMessage(self) {
- return truncate(JSON.stringify(self.actual, replacer), 128) + ' ' +
- self.operator + ' ' +
- truncate(JSON.stringify(self.expected, replacer), 128);
-}
-
-// At present only the three keys mentioned above are used and
-// understood by the spec. Implementations or sub modules can pass
-// other keys to the AssertionError's constructor - they will be
-// ignored.
-
-// 3. All of the following functions must throw an AssertionError
-// when a corresponding condition is not met, with a message that
-// may be undefined if not provided. All assertion methods provide
-// both the actual and expected values to the assertion error for
-// display purposes.
-
-function fail(actual, expected, message, operator, stackStartFunction) {
- throw new assert.AssertionError({
- message: message,
- actual: actual,
- expected: expected,
- operator: operator,
- stackStartFunction: stackStartFunction
- });
-}
-
-// EXTENSION! allows for well behaved errors defined elsewhere.
-assert.fail = fail;
-
-// 4. Pure assertion tests whether a value is truthy, as determined
-// by !!guard.
-// assert.ok(guard, message_opt);
-// This statement is equivalent to assert.equal(true, !!guard,
-// message_opt);. To test strictly for the value true, use
-// assert.strictEqual(true, guard, message_opt);.
-
-function ok(value, message) {
- if (!value) fail(value, true, message, '==', assert.ok);
-}
-assert.ok = ok;
-
-// 5. The equality assertion tests shallow, coercive equality with
-// ==.
-// assert.equal(actual, expected, message_opt);
-
-assert.equal = function equal(actual, expected, message) {
- if (actual != expected) fail(actual, expected, message, '==', assert.equal);
-};
-
-// 6. The non-equality assertion tests for whether two objects are not equal
-// with != assert.notEqual(actual, expected, message_opt);
-
-assert.notEqual = function notEqual(actual, expected, message) {
- if (actual == expected) {
- fail(actual, expected, message, '!=', assert.notEqual);
- }
-};
-
-// 7. The equivalence assertion tests a deep equality relation.
-// assert.deepEqual(actual, expected, message_opt);
-
-assert.deepEqual = function deepEqual(actual, expected, message) {
- if (!_deepEqual(actual, expected)) {
- fail(actual, expected, message, 'deepEqual', assert.deepEqual);
- }
-};
-
-function _deepEqual(actual, expected) {
- // 7.1. All identical values are equivalent, as determined by ===.
- if (actual === expected) {
- return true;
-
- } else if (util.isBuffer(actual) && util.isBuffer(expected)) {
- if (actual.length != expected.length) return false;
-
- for (var i = 0; i < actual.length; i++) {
- if (actual[i] !== expected[i]) return false;
- }
-
- return true;
-
- // 7.2. If the expected value is a Date object, the actual value is
- // equivalent if it is also a Date object that refers to the same time.
- } else if (util.isDate(actual) && util.isDate(expected)) {
- return actual.getTime() === expected.getTime();
-
- // 7.3 If the expected value is a RegExp object, the actual value is
- // equivalent if it is also a RegExp object with the same source and
- // properties (`global`, `multiline`, `lastIndex`, `ignoreCase`).
- } else if (util.isRegExp(actual) && util.isRegExp(expected)) {
- return actual.source === expected.source &&
- actual.global === expected.global &&
- actual.multiline === expected.multiline &&
- actual.lastIndex === expected.lastIndex &&
- actual.ignoreCase === expected.ignoreCase;
-
- // 7.4. Other pairs that do not both pass typeof value == 'object',
- // equivalence is determined by ==.
- } else if (!util.isObject(actual) && !util.isObject(expected)) {
- return actual == expected;
-
- // 7.5 For all other Object pairs, including Array objects, equivalence is
- // determined by having the same number of owned properties (as verified
- // with Object.prototype.hasOwnProperty.call), the same set of keys
- // (although not necessarily the same order), equivalent values for every
- // corresponding key, and an identical 'prototype' property. Note: this
- // accounts for both named and indexed properties on Arrays.
- } else {
- return objEquiv(actual, expected);
- }
-}
-
-function isArguments(object) {
- return Object.prototype.toString.call(object) == '[object Arguments]';
-}
-
-function objEquiv(a, b) {
- if (util.isNullOrUndefined(a) || util.isNullOrUndefined(b))
- return false;
- // an identical 'prototype' property.
- if (a.prototype !== b.prototype) return false;
- // if one is a primitive, the other must be same
- if (util.isPrimitive(a) || util.isPrimitive(b)) {
- return a === b;
- }
- var aIsArgs = isArguments(a),
- bIsArgs = isArguments(b);
- if ((aIsArgs && !bIsArgs) || (!aIsArgs && bIsArgs))
- return false;
- if (aIsArgs) {
- a = pSlice.call(a);
- b = pSlice.call(b);
- return _deepEqual(a, b);
- }
- var ka = objectKeys(a),
- kb = objectKeys(b),
- key, i;
- // having the same number of owned properties (keys incorporates
- // hasOwnProperty)
- if (ka.length != kb.length)
- return false;
- //the same set of keys (although not necessarily the same order),
- ka.sort();
- kb.sort();
- //~~~cheap key test
- for (i = ka.length - 1; i >= 0; i--) {
- if (ka[i] != kb[i])
- return false;
- }
- //equivalent values for every corresponding key, and
- //~~~possibly expensive deep test
- for (i = ka.length - 1; i >= 0; i--) {
- key = ka[i];
- if (!_deepEqual(a[key], b[key])) return false;
- }
- return true;
-}
-
-// 8. The non-equivalence assertion tests for any deep inequality.
-// assert.notDeepEqual(actual, expected, message_opt);
-
-assert.notDeepEqual = function notDeepEqual(actual, expected, message) {
- if (_deepEqual(actual, expected)) {
- fail(actual, expected, message, 'notDeepEqual', assert.notDeepEqual);
- }
-};
-
-// 9. The strict equality assertion tests strict equality, as determined by ===.
-// assert.strictEqual(actual, expected, message_opt);
-
-assert.strictEqual = function strictEqual(actual, expected, message) {
- if (actual !== expected) {
- fail(actual, expected, message, '===', assert.strictEqual);
- }
-};
-
-// 10. The strict non-equality assertion tests for strict inequality, as
-// determined by !==. assert.notStrictEqual(actual, expected, message_opt);
-
-assert.notStrictEqual = function notStrictEqual(actual, expected, message) {
- if (actual === expected) {
- fail(actual, expected, message, '!==', assert.notStrictEqual);
- }
-};
-
-function expectedException(actual, expected) {
- if (!actual || !expected) {
- return false;
- }
-
- if (Object.prototype.toString.call(expected) == '[object RegExp]') {
- return expected.test(actual);
- } else if (actual instanceof expected) {
- return true;
- } else if (expected.call({}, actual) === true) {
- return true;
- }
-
- return false;
-}
-
-function _throws(shouldThrow, block, expected, message) {
- var actual;
-
- if (util.isString(expected)) {
- message = expected;
- expected = null;
- }
-
- try {
- block();
- } catch (e) {
- actual = e;
- }
-
- message = (expected && expected.name ? ' (' + expected.name + ').' : '.') +
- (message ? ' ' + message : '.');
-
- if (shouldThrow && !actual) {
- fail(actual, expected, 'Missing expected exception' + message);
- }
-
- if (!shouldThrow && expectedException(actual, expected)) {
- fail(actual, expected, 'Got unwanted exception' + message);
- }
-
- if ((shouldThrow && actual && expected &&
- !expectedException(actual, expected)) || (!shouldThrow && actual)) {
- throw actual;
- }
-}
-
-// 11. Expected to throw an error:
-// assert.throws(block, Error_opt, message_opt);
-
-assert.throws = function(block, /*optional*/error, /*optional*/message) {
- _throws.apply(this, [true].concat(pSlice.call(arguments)));
-};
-
-// EXTENSION! This is annoying to write outside this module.
-assert.doesNotThrow = function(block, /*optional*/message) {
- _throws.apply(this, [false].concat(pSlice.call(arguments)));
-};
-
-assert.ifError = function(err) { if (err) {throw 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){
-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,
- writable: true,
- configurable: true
- }
- });
- };
-} else {
- // old school shim for old browsers
- module.exports = function inherits(ctor, superCtor) {
- ctor.super_ = superCtor
- var TempCtor = function () {}
- TempCtor.prototype = superCtor.prototype
- ctor.prototype = new TempCtor()
- ctor.prototype.constructor = ctor
- }
-}
-
-},{}],5:[function(require,module,exports){
-// shim for using process in browser
-
-var process = module.exports = {};
-var queue = [];
-var draining = false;
-var currentQueue;
-var queueIndex = -1;
-
-function cleanUpNextTick() {
- draining = false;
- if (currentQueue.length) {
- queue = currentQueue.concat(queue);
- } else {
- queueIndex = -1;
- }
- if (queue.length) {
- drainQueue();
- }
-}
-
-function drainQueue() {
- if (draining) {
- return;
- }
- var timeout = setTimeout(cleanUpNextTick);
- draining = true;
-
- var len = queue.length;
- while(len) {
- currentQueue = queue;
- queue = [];
- while (++queueIndex < len) {
- if (currentQueue) {
- currentQueue[queueIndex].run();
- }
- }
- queueIndex = -1;
- len = queue.length;
- }
- currentQueue = null;
- draining = false;
- clearTimeout(timeout);
-}
-
-process.nextTick = function (fun) {
- var args = new Array(arguments.length - 1);
- if (arguments.length > 1) {
- for (var i = 1; i < arguments.length; i++) {
- args[i - 1] = arguments[i];
- }
- }
- queue.push(new Item(fun, args));
- if (queue.length === 1 && !draining) {
- setTimeout(drainQueue, 0);
- }
-};
-
-// v8 likes predictible objects
-function Item(fun, array) {
- this.fun = fun;
- this.array = array;
-}
-Item.prototype.run = function () {
- this.fun.apply(null, this.array);
-};
-process.title = 'browser';
-process.browser = true;
-process.env = {};
-process.argv = [];
-process.version = ''; // empty string to avoid regexp issues
-process.versions = {};
-
-function noop() {}
-
-process.on = noop;
-process.addListener = noop;
-process.once = noop;
-process.off = noop;
-process.removeListener = noop;
-process.removeAllListeners = noop;
-process.emit = noop;
-
-process.binding = function (name) {
- throw new Error('process.binding is not supported');
-};
-
-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){
-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){
-(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
-// 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.
-
-var formatRegExp = /%[sdj%]/g;
-exports.format = function(f) {
- if (!isString(f)) {
- var objects = [];
- for (var i = 0; i < arguments.length; i++) {
- objects.push(inspect(arguments[i]));
- }
- return objects.join(' ');
- }
-
- var i = 1;
- var args = arguments;
- var len = args.length;
- var str = String(f).replace(formatRegExp, function(x) {
- if (x === '%%') return '%';
- if (i >= len) return x;
- switch (x) {
- case '%s': return String(args[i++]);
- case '%d': return Number(args[i++]);
- case '%j':
- try {
- return JSON.stringify(args[i++]);
- } catch (_) {
- return '[Circular]';
- }
- default:
- return x;
- }
- });
- for (var x = args[i]; i < len; x = args[++i]) {
- if (isNull(x) || !isObject(x)) {
- str += ' ' + x;
- } else {
- str += ' ' + inspect(x);
- }
- }
- return str;
-};
-
-
-// Mark that a method should not be used.
-// Returns a modified function which warns once by default.
-// If --no-deprecation is set, then it is a no-op.
-exports.deprecate = function(fn, msg) {
- // Allow for deprecating things in the process of starting up.
- if (isUndefined(global.process)) {
- return function() {
- return exports.deprecate(fn, msg).apply(this, arguments);
- };
- }
-
- if (process.noDeprecation === true) {
- return fn;
- }
-
- var warned = false;
- function deprecated() {
- if (!warned) {
- if (process.throwDeprecation) {
- throw new Error(msg);
- } else if (process.traceDeprecation) {
- console.trace(msg);
- } else {
- console.error(msg);
- }
- warned = true;
- }
- return fn.apply(this, arguments);
- }
-
- return deprecated;
-};
-
-
-var debugs = {};
-var debugEnviron;
-exports.debuglog = function(set) {
- if (isUndefined(debugEnviron))
- debugEnviron = process.env.NODE_DEBUG || '';
- set = set.toUpperCase();
- if (!debugs[set]) {
- if (new RegExp('\\b' + set + '\\b', 'i').test(debugEnviron)) {
- var pid = process.pid;
- debugs[set] = function() {
- var msg = exports.format.apply(exports, arguments);
- console.error('%s %d: %s', set, pid, msg);
- };
- } else {
- debugs[set] = function() {};
- }
- }
- return debugs[set];
-};
-
-
-/**
- * Echos the value of a value. Trys to print the value out
- * in the best way possible given the different types.
- *
- * @param {Object} obj The object to print out.
- * @param {Object} opts Optional options object that alters the output.
- */
-/* legacy: obj, showHidden, depth, colors*/
-function inspect(obj, opts) {
- // default options
- var ctx = {
- seen: [],
- stylize: stylizeNoColor
- };
- // legacy...
- if (arguments.length >= 3) ctx.depth = arguments[2];
- if (arguments.length >= 4) ctx.colors = arguments[3];
- if (isBoolean(opts)) {
- // legacy...
- ctx.showHidden = opts;
- } else if (opts) {
- // got an "options" object
- exports._extend(ctx, opts);
- }
- // set default options
- if (isUndefined(ctx.showHidden)) ctx.showHidden = false;
- if (isUndefined(ctx.depth)) ctx.depth = 2;
- if (isUndefined(ctx.colors)) ctx.colors = false;
- if (isUndefined(ctx.customInspect)) ctx.customInspect = true;
- if (ctx.colors) ctx.stylize = stylizeWithColor;
- return formatValue(ctx, obj, ctx.depth);
-}
-exports.inspect = inspect;
-
-
-// http://en.wikipedia.org/wiki/ANSI_escape_code#graphics
-inspect.colors = {
- 'bold' : [1, 22],
- 'italic' : [3, 23],
- 'underline' : [4, 24],
- 'inverse' : [7, 27],
- 'white' : [37, 39],
- 'grey' : [90, 39],
- 'black' : [30, 39],
- 'blue' : [34, 39],
- 'cyan' : [36, 39],
- 'green' : [32, 39],
- 'magenta' : [35, 39],
- 'red' : [31, 39],
- 'yellow' : [33, 39]
-};
-
-// Don't use 'blue' not visible on cmd.exe
-inspect.styles = {
- 'special': 'cyan',
- 'number': 'yellow',
- 'boolean': 'yellow',
- 'undefined': 'grey',
- 'null': 'bold',
- 'string': 'green',
- 'date': 'magenta',
- // "name": intentionally not styling
- 'regexp': 'red'
-};
-
-
-function stylizeWithColor(str, styleType) {
- var style = inspect.styles[styleType];
-
- if (style) {
- return '\u001b[' + inspect.colors[style][0] + 'm' + str +
- '\u001b[' + inspect.colors[style][1] + 'm';
- } else {
- return str;
- }
-}
-
-
-function stylizeNoColor(str, styleType) {
- return str;
-}
-
-
-function arrayToHash(array) {
- var hash = {};
-
- array.forEach(function(val, idx) {
- hash[val] = true;
- });
-
- return hash;
-}
-
-
-function formatValue(ctx, value, recurseTimes) {
- // Provide a hook for user-specified inspect functions.
- // Check that value is an object with an inspect function on it
- if (ctx.customInspect &&
- value &&
- isFunction(value.inspect) &&
- // Filter out the util module, it's inspect function is special
- value.inspect !== exports.inspect &&
- // Also filter out any prototype objects using the circular check.
- !(value.constructor && value.constructor.prototype === value)) {
- var ret = value.inspect(recurseTimes, ctx);
- if (!isString(ret)) {
- ret = formatValue(ctx, ret, recurseTimes);
- }
- return ret;
- }
-
- // Primitive types cannot have properties
- var primitive = formatPrimitive(ctx, value);
- if (primitive) {
- return primitive;
- }
-
- // Look up the keys of the object.
- var keys = Object.keys(value);
- var visibleKeys = arrayToHash(keys);
-
- if (ctx.showHidden) {
- keys = Object.getOwnPropertyNames(value);
- }
-
- // IE doesn't make error fields non-enumerable
- // http://msdn.microsoft.com/en-us/library/ie/dww52sbt(v=vs.94).aspx
- if (isError(value)
- && (keys.indexOf('message') >= 0 || keys.indexOf('description') >= 0)) {
- return formatError(value);
- }
-
- // Some type of object without properties can be shortcutted.
- if (keys.length === 0) {
- if (isFunction(value)) {
- var name = value.name ? ': ' + value.name : '';
- return ctx.stylize('[Function' + name + ']', 'special');
- }
- if (isRegExp(value)) {
- return ctx.stylize(RegExp.prototype.toString.call(value), 'regexp');
- }
- if (isDate(value)) {
- return ctx.stylize(Date.prototype.toString.call(value), 'date');
- }
- if (isError(value)) {
- return formatError(value);
- }
- }
-
- var base = '', array = false, braces = ['{', '}'];
-
- // Make Array say that they are Array
- if (isArray(value)) {
- array = true;
- braces = ['[', ']'];
- }
-
- // Make functions say that they are functions
- if (isFunction(value)) {
- var n = value.name ? ': ' + value.name : '';
- base = ' [Function' + n + ']';
- }
-
- // Make RegExps say that they are RegExps
- if (isRegExp(value)) {
- base = ' ' + RegExp.prototype.toString.call(value);
- }
-
- // Make dates with properties first say the date
- if (isDate(value)) {
- base = ' ' + Date.prototype.toUTCString.call(value);
- }
-
- // Make error with message first say the error
- if (isError(value)) {
- base = ' ' + formatError(value);
- }
-
- if (keys.length === 0 && (!array || value.length == 0)) {
- return braces[0] + base + braces[1];
- }
-
- if (recurseTimes < 0) {
- if (isRegExp(value)) {
- return ctx.stylize(RegExp.prototype.toString.call(value), 'regexp');
- } else {
- return ctx.stylize('[Object]', 'special');
- }
- }
-
- ctx.seen.push(value);
-
- var output;
- if (array) {
- output = formatArray(ctx, value, recurseTimes, visibleKeys, keys);
- } else {
- output = keys.map(function(key) {
- return formatProperty(ctx, value, recurseTimes, visibleKeys, key, array);
- });
- }
-
- ctx.seen.pop();
-
- return reduceToSingleString(output, base, braces);
-}
-
-
-function formatPrimitive(ctx, value) {
- if (isUndefined(value))
- return ctx.stylize('undefined', 'undefined');
- if (isString(value)) {
- var simple = '\'' + JSON.stringify(value).replace(/^"|"$/g, '')
- .replace(/'/g, "\\'")
- .replace(/\\"/g, '"') + '\'';
- return ctx.stylize(simple, 'string');
- }
- if (isNumber(value))
- return ctx.stylize('' + value, 'number');
- if (isBoolean(value))
- return ctx.stylize('' + value, 'boolean');
- // For some reason typeof null is "object", so special case here.
- if (isNull(value))
- return ctx.stylize('null', 'null');
-}
-
-
-function formatError(value) {
- return '[' + Error.prototype.toString.call(value) + ']';
-}
-
-
-function formatArray(ctx, value, recurseTimes, visibleKeys, keys) {
- var output = [];
- for (var i = 0, l = value.length; i < l; ++i) {
- if (hasOwnProperty(value, String(i))) {
- output.push(formatProperty(ctx, value, recurseTimes, visibleKeys,
- String(i), true));
- } else {
- output.push('');
- }
- }
- keys.forEach(function(key) {
- if (!key.match(/^\d+$/)) {
- output.push(formatProperty(ctx, value, recurseTimes, visibleKeys,
- key, true));
- }
- });
- return output;
-}
-
-
-function formatProperty(ctx, value, recurseTimes, visibleKeys, key, array) {
- var name, str, desc;
- desc = Object.getOwnPropertyDescriptor(value, key) || { value: value[key] };
- if (desc.get) {
- if (desc.set) {
- str = ctx.stylize('[Getter/Setter]', 'special');
- } else {
- str = ctx.stylize('[Getter]', 'special');
- }
- } else {
- if (desc.set) {
- str = ctx.stylize('[Setter]', 'special');
- }
- }
- if (!hasOwnProperty(visibleKeys, key)) {
- name = '[' + key + ']';
- }
- if (!str) {
- if (ctx.seen.indexOf(desc.value) < 0) {
- if (isNull(recurseTimes)) {
- str = formatValue(ctx, desc.value, null);
- } else {
- str = formatValue(ctx, desc.value, recurseTimes - 1);
- }
- if (str.indexOf('\n') > -1) {
- if (array) {
- str = str.split('\n').map(function(line) {
- return ' ' + line;
- }).join('\n').substr(2);
- } else {
- str = '\n' + str.split('\n').map(function(line) {
- return ' ' + line;
- }).join('\n');
- }
- }
- } else {
- str = ctx.stylize('[Circular]', 'special');
- }
- }
- if (isUndefined(name)) {
- if (array && key.match(/^\d+$/)) {
- return str;
- }
- name = JSON.stringify('' + key);
- if (name.match(/^"([a-zA-Z_][a-zA-Z_0-9]*)"$/)) {
- name = name.substr(1, name.length - 2);
- name = ctx.stylize(name, 'name');
- } else {
- name = name.replace(/'/g, "\\'")
- .replace(/\\"/g, '"')
- .replace(/(^"|"$)/g, "'");
- name = ctx.stylize(name, 'string');
- }
- }
-
- return name + ': ' + str;
-}
-
-
-function reduceToSingleString(output, base, braces) {
- var numLinesEst = 0;
- var length = output.reduce(function(prev, cur) {
- numLinesEst++;
- if (cur.indexOf('\n') >= 0) numLinesEst++;
- return prev + cur.replace(/\u001b\[\d\d?m/g, '').length + 1;
- }, 0);
-
- if (length > 60) {
- return braces[0] +
- (base === '' ? '' : base + '\n ') +
- ' ' +
- output.join(',\n ') +
- ' ' +
- braces[1];
- }
-
- return braces[0] + base + ' ' + output.join(', ') + ' ' + braces[1];
-}
-
-
-// NOTE: These type checking functions intentionally don't use `instanceof`
-// because it is fragile and can be easily faked with `Object.create()`.
-function isArray(ar) {
- return Array.isArray(ar);
-}
-exports.isArray = isArray;
-
-function isBoolean(arg) {
- return typeof arg === 'boolean';
-}
-exports.isBoolean = isBoolean;
-
-function isNull(arg) {
- return arg === null;
-}
-exports.isNull = isNull;
-
-function isNullOrUndefined(arg) {
- return arg == null;
-}
-exports.isNullOrUndefined = isNullOrUndefined;
-
-function isNumber(arg) {
- return typeof arg === 'number';
-}
-exports.isNumber = isNumber;
-
-function isString(arg) {
- return typeof arg === 'string';
-}
-exports.isString = isString;
-
-function isSymbol(arg) {
- return typeof arg === 'symbol';
-}
-exports.isSymbol = isSymbol;
-
-function isUndefined(arg) {
- return arg === void 0;
-}
-exports.isUndefined = isUndefined;
-
-function isRegExp(re) {
- return isObject(re) && objectToString(re) === '[object RegExp]';
-}
-exports.isRegExp = isRegExp;
-
-function isObject(arg) {
- return typeof arg === 'object' && arg !== null;
-}
-exports.isObject = isObject;
-
-function isDate(d) {
- return isObject(d) && objectToString(d) === '[object Date]';
-}
-exports.isDate = isDate;
-
-function isError(e) {
- return isObject(e) &&
- (objectToString(e) === '[object Error]' || e instanceof Error);
-}
-exports.isError = isError;
-
-function isFunction(arg) {
- return typeof arg === 'function';
-}
-exports.isFunction = isFunction;
-
-function isPrimitive(arg) {
- return arg === null ||
- typeof arg === 'boolean' ||
- typeof arg === 'number' ||
- typeof arg === 'string' ||
- typeof arg === 'symbol' || // ES6 symbol
- typeof arg === 'undefined';
-}
-exports.isPrimitive = isPrimitive;
-
-exports.isBuffer = require('./support/isBuffer');
-
-function objectToString(o) {
- return Object.prototype.toString.call(o);
-}
-
-
-function pad(n) {
- return n < 10 ? '0' + n.toString(10) : n.toString(10);
-}
-
-
-var months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep',
- 'Oct', 'Nov', 'Dec'];
-
-// 26 Feb 16:19:34
-function timestamp() {
- var d = new Date();
- var time = [pad(d.getHours()),
- pad(d.getMinutes()),
- pad(d.getSeconds())].join(':');
- return [d.getDate(), months[d.getMonth()], time].join(' ');
-}
-
-
-// log is just a thin wrapper to console.log that prepends a timestamp
-exports.log = function() {
- console.log('%s - %s', timestamp(), exports.format.apply(exports, arguments));
-};
-
-
-/**
- * Inherit the prototype methods from one constructor into another.
- *
- * The Function.prototype.inherits from lang.js rewritten as a standalone
- * function (not on Function.prototype). NOTE: If this file is to be loaded
- * during bootstrapping this function needs to be rewritten using some native
- * functions as prototype setup using normal JavaScript does not work as
- * expected during bootstrapping (see mirror.js in r114903).
- *
- * @param {function} ctor Constructor function which needs to inherit the
- * prototype.
- * @param {function} superCtor Constructor function to inherit prototype from.
- */
-exports.inherits = require('inherits');
-
-exports._extend = function(origin, add) {
- // Don't do anything if add isn't an object
- if (!add || !isObject(add)) return origin;
-
- var keys = Object.keys(add);
- var i = keys.length;
- while (i--) {
- origin[keys[i]] = add[keys[i]];
- }
- 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){
-(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);
- rng = function whatwgRNG() {
- crypto.getRandomValues(_rnds8);
- return _rnds8;
- };
-}
-
-if (!rng) {
- // Math.random()-based (RNG)
- //
- // If all else fails, use Math.random(). It's fast, but is of unspecified
- // quality.
- var _rnds = new Array(16);
- rng = function() {
- for (var i = 0, r; i < 16; i++) {
- if ((i & 0x03) === 0) r = Math.random() * 0x100000000;
- _rnds[i] = r >>> ((i & 0x03) << 3) & 0xff;
- }
-
- return _rnds;
- };
-}
-
-module.exports = rng;
-
-
-}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
-},{}],9:[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
-var _rng = require('./rng');
-
-// Maps for number <-> hex string conversion
-var _byteToHex = [];
-var _hexToByte = {};
-for (var i = 0; i < 256; i++) {
- _byteToHex[i] = (i + 0x100).toString(16).substr(1);
- _hexToByte[_byteToHex[i]] = i;
-}
-
-// **`parse()` - Parse a UUID into it's component bytes**
-function parse(s, buf, offset) {
- var i = (buf && offset) || 0, ii = 0;
-
- buf = buf || [];
- s.toLowerCase().replace(/[0-9a-f]{2}/g, function(oct) {
- if (ii < 16) { // Don't overflow!
- buf[i + ii++] = _hexToByte[oct];
- }
- });
-
- // Zero out remaining bytes if string was short
- while (ii < 16) {
- buf[i + ii++] = 0;
- }
-
- return buf;
-}
-
-// **`unparse()` - Convert UUID byte array (ala parse()) into a string**
-function unparse(buf, offset) {
- var i = offset || 0, bth = _byteToHex;
- return bth[buf[i++]] + bth[buf[i++]] +
- bth[buf[i++]] + bth[buf[i++]] + '-' +
- bth[buf[i++]] + bth[buf[i++]] + '-' +
- bth[buf[i++]] + bth[buf[i++]] + '-' +
- bth[buf[i++]] + bth[buf[i++]] + '-' +
- bth[buf[i++]] + bth[buf[i++]] +
- bth[buf[i++]] + bth[buf[i++]] +
- bth[buf[i++]] + bth[buf[i++]];
-}
-
-// **`v1()` - Generate time-based UUID**
-//
-// Inspired by https://github.com/LiosK/UUID.js
-// and http://docs.python.org/library/uuid.html
-
-// random #'s we need to init node and clockseq
-var _seedBytes = _rng();
-
-// Per 4.5, create and 48-bit node id, (47 random bits + multicast bit = 1)
-var _nodeId = [
- _seedBytes[0] | 0x01,
- _seedBytes[1], _seedBytes[2], _seedBytes[3], _seedBytes[4], _seedBytes[5]
-];
-
-// Per 4.2.2, randomize (14 bit) clockseq
-var _clockseq = (_seedBytes[6] << 8 | _seedBytes[7]) & 0x3fff;
-
-// Previous uuid creation time
-var _lastMSecs = 0, _lastNSecs = 0;
-
-// See https://github.com/broofa/node-uuid for API details
-function v1(options, buf, offset) {
- var i = buf && offset || 0;
- var b = buf || [];
-
- options = options || {};
-
- var clockseq = options.clockseq !== undefined ? options.clockseq : _clockseq;
-
- // UUID timestamps are 100 nano-second units since the Gregorian epoch,
- // (1582-10-15 00:00). JSNumbers aren't precise enough for this, so
- // time is handled internally as 'msecs' (integer milliseconds) and 'nsecs'
- // (100-nanoseconds offset from msecs) since unix epoch, 1970-01-01 00:00.
- var msecs = options.msecs !== undefined ? options.msecs : new Date().getTime();
-
- // Per 4.2.1.2, use count of uuid's generated during the current clock
- // cycle to simulate higher resolution clock
- var nsecs = options.nsecs !== undefined ? options.nsecs : _lastNSecs + 1;
-
- // Time since last uuid creation (in msecs)
- var dt = (msecs - _lastMSecs) + (nsecs - _lastNSecs)/10000;
-
- // Per 4.2.1.2, Bump clockseq on clock regression
- if (dt < 0 && options.clockseq === undefined) {
- clockseq = clockseq + 1 & 0x3fff;
- }
-
- // Reset nsecs if clock regresses (new clockseq) or we've moved onto a new
- // time interval
- if ((dt < 0 || msecs > _lastMSecs) && options.nsecs === undefined) {
- nsecs = 0;
- }
-
- // Per 4.2.1.2 Throw error if too many uuids are requested
- if (nsecs >= 10000) {
- throw new Error('uuid.v1(): Can\'t create more than 10M uuids/sec');
- }
-
- _lastMSecs = msecs;
- _lastNSecs = nsecs;
- _clockseq = clockseq;
-
- // Per 4.1.4 - Convert from unix epoch to Gregorian epoch
- msecs += 12219292800000;
-
- // `time_low`
- var tl = ((msecs & 0xfffffff) * 10000 + nsecs) % 0x100000000;
- b[i++] = tl >>> 24 & 0xff;
- b[i++] = tl >>> 16 & 0xff;
- b[i++] = tl >>> 8 & 0xff;
- b[i++] = tl & 0xff;
-
- // `time_mid`
- var tmh = (msecs / 0x100000000 * 10000) & 0xfffffff;
- b[i++] = tmh >>> 8 & 0xff;
- b[i++] = tmh & 0xff;
-
- // `time_high_and_version`
- b[i++] = tmh >>> 24 & 0xf | 0x10; // include version
- b[i++] = tmh >>> 16 & 0xff;
-
- // `clock_seq_hi_and_reserved` (Per 4.2.2 - include variant)
- b[i++] = clockseq >>> 8 | 0x80;
-
- // `clock_seq_low`
- b[i++] = clockseq & 0xff;
-
- // `node`
- var node = options.node || _nodeId;
- for (var n = 0; n < 6; n++) {
- b[i + n] = node[n];
- }
-
- return buf ? buf : unparse(b);
-}
-
-// **`v4()` - Generate random UUID**
-
-// See https://github.com/broofa/node-uuid for API details
-function v4(options, buf, offset) {
- // Deprecated - 'format' argument, as supported in v1.2
- var i = buf && offset || 0;
-
- if (typeof(options) == 'string') {
- buf = options == 'binary' ? new Array(16) : null;
- options = null;
- }
- options = options || {};
-
- var rnds = options.random || (options.rng || _rng)();
-
- // Per 4.4, set bits for version and `clock_seq_hi_and_reserved`
- rnds[6] = (rnds[6] & 0x0f) | 0x40;
- rnds[8] = (rnds[8] & 0x3f) | 0x80;
-
- // Copy bytes to buffer, if provided
- if (buf) {
- for (var ii = 0; ii < 16; ii++) {
- buf[i + ii] = rnds[ii];
- }
- }
-
- return buf || unparse(rnds);
-}
-
-// Export public API
-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){
-"use strict";
-
-Object.defineProperty(exports, "__esModule", {
- value: true
-});
-
-var _api = require("./api");
-
-var _api2 = _interopRequireDefault(_api);
-
-var _collection = require("./collection");
-
-var _collection2 = _interopRequireDefault(_collection);
-
-var _base = require("./adapters/base");
-
-var _base2 = _interopRequireDefault(_base);
-
-function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
-
-const DEFAULT_BUCKET_NAME = "default";
-const DEFAULT_REMOTE = "http://localhost:8888/v1";
-
-/**
- * KintoBase class.
- */
-class KintoBase {
- /**
- * Provides a public access to the base adapter class. Users can create a
- * custom DB adapter by extending {@link BaseAdapter}.
- *
- * @type {Object}
- */
- static get adapters() {
- return {
- BaseAdapter: _base2.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}
- */
- static get syncStrategy() {
- return _collection2.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.
- * - `{String}` `dbPrefix` The DB name prefix.
- * - `{Object}` `headers` The HTTP headers to use.
- * - `{String}` `requestMode` The HTTP CORS mode to use.
- *
- * @param {Object} options The options object.
- */
- constructor(options = {}) {
- const defaults = {
- bucket: DEFAULT_BUCKET_NAME,
- 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 });
-
- // public properties
- /**
- * 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 May contain the following fields:
- * remoteTransformers: Array<RemoteTransformer>
- * @return {Collection}
- */
- collection(collName, options = {}) {
- if (!collName) {
- throw new Error("missing collection name");
- }
-
- const bucket = this._options.bucket;
- return new _collection2.default(bucket, collName, this._api, {
- events: this._options.events,
- adapter: this._options.adapter,
- 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){
-"use strict";
-
-/**
- * Base db adapter.
- *
- * @abstract
- */
-
-Object.defineProperty(exports, "__esModule", {
- value: true
-});
-class BaseAdapter {
- /**
- * Opens a connection to the database.
- *
- * @abstract
- * @return {Promise}
- */
- open() {
- return Promise.resolve();
- }
-
- /**
- * Closes current connection to the database.
- *
- * @abstract
- * @return {Promise}
- */
- close() {
- return Promise.resolve();
- }
-
- /**
- * Deletes every records present in the database.
- *
- * @abstract
- * @return {Promise}
- */
- 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}
- */
- execute(callback, options = { 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}
- */
- get(id) {
- throw new Error("Not Implemented.");
- }
-
- /**
- * Lists all records from the database.
- *
- * @abstract
- * @return {Promise}
- */
- list() {
- throw new Error("Not Implemented.");
- }
-
- /**
- * Store the lastModified value.
- *
- * @abstract
- * @param {Number} lastModified
- * @return {Promise}
- */
- saveLastModified(lastModified) {
- throw new Error("Not Implemented.");
- }
-
- /**
- * Retrieve saved lastModified value.
- *
- * @abstract
- * @return {Promise}
- */
- getLastModified() {
- throw new Error("Not Implemented.");
- }
-
- /**
- * Load a dump of records exported from a server.
- *
- * @abstract
- * @return {Promise}
- */
- loadDump(records) {
- throw new Error("Not Implemented.");
- }
-}
-exports.default = BaseAdapter;
-
-},{}],12:[function(require,module,exports){
-"use strict";
-
-Object.defineProperty(exports, "__esModule", {
- value: true
-});
-exports.SUPPORTED_PROTOCOL_VERSION = undefined;
-exports.cleanRecord = cleanRecord;
-
-var _utils = require("./utils.js");
-
-var _http = require("./http.js");
-
-var _http2 = _interopRequireDefault(_http);
-
-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.
- */
-function cleanRecord(record, excludeFields = RECORD_FIELDS_TO_CLEAN) {
- return Object.keys(record).reduce((acc, key) => {
- 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() {
- return {
- ok: true,
- lastModified: null,
- errors: [],
- created: [],
- updated: [],
- deleted: [],
- published: [],
- conflicts: [],
- skipped: [],
- resolved: []
- };
- }
-
- /**
- * Public constructor.
- */
- constructor() {
- /**
- * 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}
- */
- add(type, entries) {
- if (!Array.isArray(this[type])) {
- return;
- }
- this[type] = this[type].concat(entries);
- 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}
- */
- reset(type) {
- this[type] = SyncResultObject.defaults[type];
- this.ok = this.errors.length + this.conflicts.length === 0;
- return this;
- }
-}
-
-exports.SyncResultObject = SyncResultObject;
-function createUUIDSchema() {
- return {
- generate() {
- return (0, _uuid.v4)();
- },
-
- validate(id) {
- return (0, _utils.isUUID)(id);
- }
- };
-}
-
-function markStatus(record, status) {
- return Object.assign({}, 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.
- * @return {Object}
- */
-function importChange(transaction, remote) {
- const 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 };
- }
- 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));
- 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
- // status to "synced".
- const synced = markSynced(remote);
- transaction.update(synced);
- return { type: "updated", data: synced };
- }
- return {
- type: "conflicts",
- data: { type: "incoming", local: local, remote: remote }
- };
- }
- if (remote.deleted) {
- transaction.delete(remote.id);
- return { type: "deleted", data: { id: local.id } };
- }
- const synced = markSynced(remote);
- transaction.update(synced);
- // if identical, simply exclude it from all lists
- const type = identical ? "void" : "updated";
- return { type, data: synced };
-}
-
-/**
- * Abstracts a collection of records stored in the local database, providing
- * CRUD operations and synchronization helpers.
- */
-class Collection {
- /**
- * Constructor.
- *
- * Options:
- * - `{BaseAdapter} adapter` The DB adapter (default: `IDB`)
- * - `{String} dbPrefix` The DB name prefix (default: `""`)
- *
- * @param {String} bucket The bucket identifier.
- * @param {String} name The collection name.
- * @param {Api} api The Api instance.
- * @param {Object} options The options object.
- */
- constructor(bucket, name, api, options = {}) {
- this._bucket = bucket;
- this._name = name;
- this._lastModified = null;
-
- const DBAdapter = options.adapter;
- if (!DBAdapter) {
- throw new Error("No adapter provided");
- }
- const dbPrefix = options.dbPrefix || "";
- const db = new DBAdapter(`${ dbPrefix }${ bucket }/${ name }`);
- if (!(db instanceof _base2.default)) {
- throw new Error("Unsupported adapter.");
- }
- // public properties
- /**
- * The db adapter instance
- * @type {BaseAdapter}
- */
- this.db = db;
- /**
- * The Api instance.
- * @type {Api}
- */
- 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 collection name.
- * @type {String}
- */
- get name() {
- return this._name;
- }
-
- /**
- * The bucket name.
- * @type {String}
- */
- get bucket() {
- return this._bucket;
- }
-
- /**
- * The last modified timestamp.
- * @type {Number}
- */
- get lastModified() {
- 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}
- */
- static get strategy() {
- return {
- CLIENT_WINS: "client_wins",
- SERVER_WINS: "server_wins",
- MANUAL: "manual"
- };
- }
-
- /**
- * Validates an idSchema.
- *
- * @param {Object|undefined} idSchema
- * @return {Object}
- */
- _validateIdSchema(idSchema) {
- if (typeof idSchema === "undefined") {
- return createUUIDSchema();
- }
- if (typeof 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}
- */
- _validateRemoteTransformers(remoteTransformers) {
- if (typeof remoteTransformers === "undefined") {
- return [];
- }
- if (!Array.isArray(remoteTransformers)) {
- throw new Error("remoteTransformers should be an array.");
- }
- return remoteTransformers.map(transformer => {
- if (typeof 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;
- });
- }
-
- /**
- * Deletes every records in the current collection and marks the collection as
- * never synced.
- *
- * @return {Promise}
- */
- clear() {
- return this.db.clear().then(_ => this.db.saveLastModified(null)).then(_ => ({ data: [], permissions: {} }));
- }
-
- /**
- * Encodes a record.
- *
- * @param {String} type Either "remote" or "local".
- * @param {Object} record The record object to encode.
- * @return {Promise}
- */
- _encodeRecord(type, record) {
- if (!this[`${ type }Transformers`].length) {
- return Promise.resolve(record);
- }
- return (0, _utils.waterfall)(this[`${ type }Transformers`].map(transformer => {
- return record => transformer.encode(record);
- }), record);
- }
-
- /**
- * Decodes a record.
- *
- * @param {String} type Either "remote" or "local".
- * @param {Object} record The record object to decode.
- * @return {Promise}
- */
- _decodeRecord(type, record) {
- if (!this[`${ type }Transformers`].length) {
- return Promise.resolve(record);
- }
- return (0, _utils.waterfall)(this[`${ type }Transformers`].reverse().map(transformer => {
- return record => transformer.decode(record);
- }), record);
- }
-
- /**
- * Adds a record to the local database.
- *
- * 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}
- */
- create(record, options = { useRecordId: false, synced: false }) {
- const reject = msg => Promise.reject(new Error(msg));
- if (typeof record !== "object") {
- return reject("Record is not an object.");
- }
- if ((options.synced || options.useRecordId) && !record.id) {
- return reject("Missing required Id; synced and useRecordId options require one");
- }
- if (!options.synced && !options.useRecordId && record.id) {
- return reject("Extraneous Id; can't create a record having one set.");
- }
- const newRecord = Object.assign({}, record, {
- id: options.synced || options.useRecordId ? record.id : this.idSchema.generate(),
- _status: options.synced ? "synced" : "created"
- });
- if (!this.idSchema.validate(newRecord.id)) {
- return reject(`Invalid Id: ${ newRecord.id }`);
- }
- return this.db.execute(transaction => {
- transaction.create(newRecord);
- return { data: newRecord, permissions: {} };
- }).catch(err => {
- if (options.useRecordId) {
- throw new Error("Couldn't create record. It may have been virtually deleted.");
- }
- throw err;
- });
- }
-
- /**
- * 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 {Promise}
- */
- update(record, options = { synced: false, patch: false }) {
- if (typeof record !== "object") {
- return Promise.reject(new Error("Record is not an object."));
- }
- 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";
- }
- 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: {} };
- });
- });
- }
-
- /**
- * Retrieve a record by its id from the local database.
- *
- * @param {String} id
- * @param {Object} options
- * @return {Promise}
- */
- get(id, options = { includeDeleted: false }) {
- if (!this.idSchema.validate(id)) {
- return Promise.reject(Error(`Invalid Id: ${ id }`));
- }
- return this.db.get(id).then(record => {
- if (!record || !options.includeDeleted && record._status === "deleted") {
- throw new Error(`Record with id=${ id } not found.`);
- } else {
- return { data: record, permissions: {} };
- }
- });
- }
-
- /**
- * 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 {Promise}
- */
- delete(id, options = { virtual: true }) {
- if (!this.idSchema.validate(id)) {
- return Promise.reject(new Error(`Invalid Id: ${ id }`));
- }
- // Ensure the record actually exists.
- return this.get(id, { includeDeleted: true }).then(res => {
- const existing = res.data;
- return this.db.execute(transaction => {
- // Virtual updates status.
- if (options.virtual) {
- transaction.update(markDeleted(existing));
- } else {
- // Delete for real.
- transaction.delete(id);
- }
- return { data: { id: id }, permissions: {} };
- });
- });
- }
-
- /**
- * Lists records from the local database.
- *
- * Params:
- * - {Object} filters The filters to apply (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);
- if (!options.includeDeleted) {
- reduced = reduced.filter(record => record._status !== "deleted");
- }
- return { data: reduced, permissions: {} };
- });
- }
-
- /**
- * Import changes into the local database.
- *
- * @param {SyncResultObject} syncResultObject The sync result object.
- * @param {Object} changeObject The change object.
- * @return {Promise}
- */
- 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;
-
- 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;
- }).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 => {
- this._lastModified = lastModified;
- return syncResultObject;
- });
- });
- }
-
- /**
- * 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.
- */
- resetSyncStatus() {
- let _count;
- // XXX filter by status
- return this.list({}, { includeDeleted: true }).then(result => {
- 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);
- } else {
- // Records that were synced become «created».
- transaction.update(Object.assign({}, r, {
- last_modified: undefined,
- _status: "created"
- }));
- }
- });
- });
- }).then(() => this.db.saveLastModified(null)).then(() => _count);
- }
-
- /**
- * Returns an object containing two lists:
- *
- * - `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")));
- }).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.
- *
- * Options:
- * - {String} strategy: The selected sync strategy.
- *
- * @param {SyncResultObject} syncResultObject
- * @param {Object} options
- * @return {Promise}
- */
- pullChanges(syncResultObject, options = {}) {
- if (!syncResultObject.ok) {
- return Promise.resolve(syncResultObject);
- }
- options = Object.assign({
- strategy: Collection.strategy.MANUAL,
- lastModified: this.lastModified,
- headers: {}
- }, options);
- // First fetch remote changes from the server
- return this.api.fetchChangesSince(this.bucket, this.name, {
- lastModified: options.lastModified,
- headers: options.headers
- })
- // Reflect these changes locally
- .then(changes => this.importChanges(syncResultObject, changes))
- // Handle conflicts, if any
- .then(result => this._handleConflicts(result, options.strategy));
- }
-
- /**
- * Publish local changes to the remote server and updates the passed
- * {@link SyncResultObject} with publication results.
- *
- * @param {SyncResultObject} syncResultObject The sync result object.
- * @param {Object} options The options object.
- * @return {Promise}
- */
- pushChanges(syncResultObject, options = {}) {
- 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);
- });
- }),
- // Send batch update requests
- this.api.batch(this.bucket, this.name, toSync, options)]);
- })
- // Update published local records
- .then(([deleted, 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
- 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 => {
- return this.db.execute(transaction => {
- const updated = results.map(record => {
- const synced = markSynced(record);
- transaction.update(synced);
- return { data: synced };
- });
- const deleted = toDeleteLocally.map(record => {
- transaction.delete(record.id);
- // Amend result data with the deleted attribute set
- return { data: { id: record.id, deleted: true } };
- });
- return updated.concat(deleted);
- });
- }).then(published => {
- syncResultObject.add("published", published.map(res => res.data));
- return syncResultObject;
- });
- })
- // Handle conflicts, if any
- .then(result => this._handleConflicts(result, options.strategy)).then(result => {
- const resolvedUnsynced = result.resolved.filter(record => record._status !== "synced");
- // No resolved conflict to reflect anywhere
- if (resolvedUnsynced.length === 0 || options.resolved) {
- return result;
- } else if (options.strategy === Collection.strategy.CLIENT_WINS && !options.resolved) {
- // We need to push local versions of the records to the server
- return this.pushChanges(result, Object.assign({}, options, { resolved: true }));
- } else if (options.strategy === Collection.strategy.SERVER_WINS) {
- // If records have been automatically resolved according to strategy and
- // are in non-synced status, mark them as synced.
- return this.db.execute(transaction => {
- resolvedUnsynced.forEach(record => {
- transaction.update(markSynced(record));
- });
- return result;
- });
- }
- });
- }
-
- /**
- * 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}
- */
- resolve(conflict, resolution) {
- return this.update(Object.assign({}, resolution, {
- // Ensure local record has the latest authoritative timestamp
- last_modified: conflict.remote.last_modified
- }));
- }
-
- /**
- * Handles synchronization conflicts according to specified strategy.
- *
- * @param {SyncResultObject} result The sync result object.
- * @param {String} strategy The {@link Collection.strategy}.
- * @return {Promise}
- */
- _handleConflicts(result, strategy = Collection.strategy.MANUAL) {
- if (strategy === Collection.strategy.MANUAL || result.conflicts.length === 0) {
- return Promise.resolve(result);
- }
- return Promise.all(result.conflicts.map(conflict => {
- const resolution = strategy === Collection.strategy.CLIENT_WINS ? conflict.local : conflict.remote;
- return this.resolve(conflict, resolution);
- })).then(imports => {
- return result.reset("conflicts").add("resolved", imports.map(res => res.data));
- });
- }
-
- /**
- * 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.
- * - {Collection.strategy} strategy: See {@link Collection.strategy}.
- * - {Boolean} ignoreBackoff: Force synchronization even if server is currently
- * backed off.
- * - {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.
- */
- sync(options = {
- strategy: Collection.strategy.MANUAL,
- headers: {},
- ignoreBackoff: false,
- remote: null
- }) {
- const 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) {
- const seconds = Math.ceil(this.api.backoff / 1000);
- return Promise.reject(new Error(`Server is backed off; retry in ${ seconds }s or use the ignoreBackoff option.`));
- }
- const result = new SyncResultObject();
- const syncPromise = this.db.getLastModified().then(lastModified => this._lastModified = lastModified).then(_ => this.pullChanges(result, options)).then(result => this.pushChanges(result, options)).then(result => {
- // Avoid performing a last pull if nothing has been published.
- if (result.published.length === 0) {
- return result;
- }
- return this.pullChanges(result, options);
- });
- // Ensure API default remote is reverted if a custom one's been used
- return (0, _utils.pFinally)(syncPromise, () => this.api.remote = previousRemote);
- }
-
- /**
- * 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.
- */
- 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));
- }
- }
-
- // 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;
- }
- }
- }
-
- 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];
- const 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;
- });
- }).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){
-"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;
-
-/**
- * Deeply checks if two structures are equals.
- *
- * @param {Any} a
- * @param {Any} b
- * @return {Boolean}
- */
-function deepEquals(a, b) {
- try {
- (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";
-}
-
-/**
- * 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) {
- const hasDash = order[0] === "-";
- const field = hasDash ? order.slice(1) : order;
- const direction = hasDash ? -1 : 1;
- return list.slice().sort((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;
- });
-}
-
-/**
- * Filters records in a list matching all given filters.
- *
- * @param {String} filters The filters object.
- * @param {Array} list The collection to order.
- * @return {Array}
- */
-function filterObjects(filters, list) {
- return list.filter(entry => {
- return Object.keys(filters).every(filter => {
- return entry[filter] === filters[filter];
- });
- });
-}
-
-/**
- * Filter and sort list against provided filters and order.
- *
- * @param {Object} filters The filters to apply.
- * @param {String} order The order to apply.
- * @param {Array} list The list to reduce.
- * @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);
-}
-
-/**
- * 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((promise, nextFn) => {
- return promise.then(nextFn);
- }, Promise.resolve(init));
-}
-
-/**
- * Ensure a callback is always executed at the end of the passed promise flow.
- *
- * @link https://github.com/domenic/promises-unwrapping/issues/18
- * @param {Promise} promise The promise.
- * @param {Function} fn The callback.
- * @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)
-});
\ No newline at end of file
--- a/services/common/moz.build
+++ b/services/common/moz.build
@@ -10,20 +10,21 @@ with Files('**'):
TEST_DIRS += ['tests']
EXTRA_COMPONENTS += [
'servicesComponents.manifest',
]
EXTRA_JS_MODULES['services-common'] += [
'async.js',
+ 'kinto-http-client.js',
+ 'kinto-offline-client.js',
'kinto-updater.js',
'KintoCertificateBlocklist.js',
'logmanager.js',
- 'moz-kinto-client.js',
'observers.js',
'rest.js',
'stringbundle.js',
'utils.js',
]
if CONFIG['MOZ_WIDGET_TOOLKIT'] != 'android':
EXTRA_JS_MODULES['services-common'] += [
--- a/services/common/tests/unit/test_kinto.js
+++ b/services/common/tests/unit/test_kinto.js
@@ -1,12 +1,12 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
-Cu.import("resource://services-common/moz-kinto-client.js")
+Cu.import("resource://services-common/kinto-offline-client.js");
Cu.import("resource://testing-common/httpd.js");
const BinaryInputStream = Components.Constructor("@mozilla.org/binaryinputstream;1",
"nsIBinaryInputStream", "setInputStream");
var server;
// set up what we need to make storage adapters
@@ -354,28 +354,28 @@ function getSampleResponse(req, port) {
"Access-Control-Allow-Origin: *",
"Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
"Content-Type: application/json; charset=UTF-8",
"Server: waitress"
],
"status": {status: 200, statusText: "OK"},
"responseBody": JSON.stringify({"settings":{"cliquet.batch_max_requests":25}, "url":`http://localhost:${port}/v1/`, "documentation":"https://kinto.readthedocs.org/", "version":"1.5.1", "commit":"cbc6f58", "hello":"kinto"})
},
- "GET:/v1/buckets/default/collections/test_collection/records?": {
+ "GET:/v1/buckets/default/collections/test_collection/records?_sort=-last_modified": {
"sampleHeaders": [
"Access-Control-Allow-Origin: *",
"Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
"Content-Type: application/json; charset=UTF-8",
"Server: waitress",
"Etag: \"1445606341071\""
],
"status": {status: 200, statusText: "OK"},
"responseBody": JSON.stringify({"data":[{"last_modified":1445606341071, "done":false, "id":"68db8313-686e-4fff-835e-07d78ad6f2af", "title":"New test"}]})
},
- "GET:/v1/buckets/default/collections/test_collection/records?_since=1445606341071": {
+ "GET:/v1/buckets/default/collections/test_collection/records?_sort=-last_modified&_since=1445606341071": {
"sampleHeaders": [
"Access-Control-Allow-Origin: *",
"Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
"Content-Type: application/json; charset=UTF-8",
"Server: waitress",
"Etag: \"1445607941223\""
],
"status": {status: 200, statusText: "OK"},
--- a/services/common/tests/unit/test_kintoCertBlocklist.js
+++ b/services/common/tests/unit/test_kintoCertBlocklist.js
@@ -1,15 +1,15 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
const { Constructor: CC } = Components;
Cu.import("resource://services-common/KintoCertificateBlocklist.js");
-Cu.import("resource://services-common/moz-kinto-client.js")
+Cu.import("resource://services-common/kinto-offline-client.js");
Cu.import("resource://testing-common/httpd.js");
const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1",
"nsIBinaryInputStream", "setInputStream");
var server;
// set up what we need to make storage adapters
@@ -140,33 +140,33 @@ function getSampleResponse(req, port) {
"Access-Control-Allow-Origin: *",
"Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
"Content-Type: application/json; charset=UTF-8",
"Server: waitress"
],
"status": {status: 200, statusText: "OK"},
"responseBody": JSON.stringify({"settings":{"cliquet.batch_max_requests":25}, "url":`http://localhost:${port}/v1/`, "documentation":"https://kinto.readthedocs.org/", "version":"1.5.1", "commit":"cbc6f58", "hello":"kinto"})
},
- "GET:/v1/buckets/blocklists/collections/certificates/records?": {
+ "GET:/v1/buckets/blocklists/collections/certificates/records?_sort=-last_modified": {
"sampleHeaders": [
"Access-Control-Allow-Origin: *",
"Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
"Content-Type: application/json; charset=UTF-8",
"Server: waitress",
"Etag: \"3000\""
],
"status": {status: 200, statusText: "OK"},
"responseBody": JSON.stringify({"data":[{
"issuerName": "MEQxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwx0aGF3dGUsIEluYy4xHjAcBgNVBAMTFXRoYXd0ZSBFViBTU0wgQ0EgLSBHMw==",
"serialNumber":"CrTHPEE6AZSfI3jysin2bA==",
"id":"78cf8900-fdea-4ce5-f8fb-b78710617718",
"last_modified":3000
}]})
},
- "GET:/v1/buckets/blocklists/collections/certificates/records?_since=3000": {
+ "GET:/v1/buckets/blocklists/collections/certificates/records?_sort=-last_modified&_since=3000": {
"sampleHeaders": [
"Access-Control-Allow-Origin: *",
"Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
"Content-Type: application/json; charset=UTF-8",
"Server: waitress",
"Etag: \"4000\""
],
"status": {status: 200, statusText: "OK"},
--- a/services/common/tests/unit/test_storage_adapter.js
+++ b/services/common/tests/unit/test_storage_adapter.js
@@ -1,12 +1,12 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
-Cu.import("resource://services-common/moz-kinto-client.js");
+Cu.import("resource://services-common/kinto-offline-client.js");
// set up what we need to make storage adapters
const Kinto = loadKinto();
const FirefoxAdapter = Kinto.adapters.FirefoxAdapter;
const kintoFilename = "kinto.sqlite";
let gFirefoxAdapter = null;