Kinto.js typescript diff

Created Diff never expires
/*
/*
*
*
* Licensed under the Apache License, Version 2.0 (the "License");
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* See the License for the specific language governing permissions and
* limitations under the License.
* limitations under the License.
*/
*/
"use strict";
"use strict";


/*
/*
* This file is generated from kinto.js - do not modify directly.
* This file is generated from kinto.js - do not modify directly.
*/
*/


// This is required because with Babel compiles ES2015 modules into a
// This is required because with Babel compiles ES2015 modules into a
// require() form that tries to keep its modules on "this", but
// require() form that tries to keep its modules on "this", but
// doesn't specify "this", leaving it to default to the global
// doesn't specify "this", leaving it to default to the global
// object. However, in strict mode, "this" no longer defaults to the
// object. However, in strict mode, "this" no longer defaults to the
// global object, so expose the global object explicitly. Babel's
// global object, so expose the global object explicitly. Babel's
// compiled output will use a variable called "global" if one is
// compiled output will use a variable called "global" if one is
// present.
// present.
//
//
// See https://bugzilla.mozilla.org/show_bug.cgi?id=1394556#c3 for
// See https://bugzilla.mozilla.org/show_bug.cgi?id=1394556#c3 for
// more details.
// more details.
const global = this;
const global = this;


var EXPORTED_SYMBOLS = ["Kinto"];
var EXPORTED_SYMBOLS = ["Kinto"];


/*
/*
* Version 12.7.0 - 57a2440
* Version 12.7.0 - cf865b6
*/
*/


(function(global, factory) {
(function(global, factory) {
typeof exports === "object" && typeof module !== "undefined"
typeof exports === "object" && typeof module !== "undefined"
? (module.exports = factory())
? (module.exports = factory(require("events")))
: typeof define === "function" && define.amd
: typeof define === "function" && define.amd
? define(factory)
? define(["events"], factory)
: ((global = global || self), (global.Kinto = factory()));
: ((global = global || self), (global.Kinto = factory(global.events)));
})(this, function() {
})(this, function(events) {
"use strict";
"use strict";


/**
/**
* Base db adapter.
* Base db adapter.
*
*
* @abstract
* @abstract
*/
*/
class BaseAdapter {
class BaseAdapter {
/**
/**
* Deletes every records present in the database.
* Deletes every records present in the database.
*
*
* @abstract
* @abstract
* @return {Promise}
* @return {Promise}
*/
*/
clear() {
clear() {
throw new Error("Not Implemented.");
throw new Error("Not Implemented.");
}
}
/**
/**
* Executes a batch of operations within a single transaction.
* Executes a batch of operations within a single transaction.
*
*
* @abstract
* @abstract
* @param {Function} callback The operation callback.
* @param {Function} callback The operation callback.
* @param {Object} options The options object.
* @param {Object} options The options object.
* @return {Promise}
* @return {Promise}
*/
*/
execute(callback, options = { preload: [] }) {
execute(callback, options = { preload: [] }) {
throw new Error("Not Implemented.");
throw new Error("Not Implemented.");
}
}
/**
/**
* Retrieve a record by its primary key from the database.
* Retrieve a record by its primary key from the database.
*
*
* @abstract
* @abstract
* @param {String} id The record id.
* @param {String} id The record id.
* @return {Promise}
* @return {Promise}
*/
*/
get(id) {
get(id) {
throw new Error("Not Implemented.");
throw new Error("Not Implemented.");
}
}
/**
/**
* Lists all records from the database.
* Lists all records from the database.
*
*
* @abstract
* @abstract
* @param {Object} params The filters and order to apply to the results.
* @param {Object} params The filters and order to apply to the results.
* @return {Promise}
* @return {Promise}
*/
*/
list(params = { filters: {}, order: "" }) {
list(
params = {
filters: {},
order: "",
}
) {
throw new Error("Not Implemented.");
throw new Error("Not Implemented.");
}
}
/**
/**
* Store the lastModified value.
* Store the lastModified value.
*
*
* @abstract
* @abstract
* @param {Number} lastModified
* @param {Number} lastModified
* @return {Promise}
* @return {Promise}
*/
*/
saveLastModified(lastModified) {
saveLastModified(lastModified) {
throw new Error("Not Implemented.");
throw new Error("Not Implemented.");
}
}
/**
/**
* Retrieve saved lastModified value.
* Retrieve saved lastModified value.
*
*
* @abstract
* @abstract
* @return {Promise}
* @return {Promise}
*/
*/
getLastModified() {
getLastModified() {
throw new Error("Not Implemented.");
throw new Error("Not Implemented.");
}
}
/**
/**
* Load records in bulk that were exported from a server.
* Load records in bulk that were exported from a server.
*
*
* @abstract
* @abstract
* @param {Array} records The records to load.
* @param {Array} records The records to load.
* @return {Promise}
* @return {Promise}
*/
*/
importBulk(records) {
importBulk(records) {
throw new Error("Not Implemented.");
throw new Error("Not Implemented.");
}
}
/**
/**
* Load a dump of records exported from a server.
* Load a dump of records exported from a server.
*
*
* @deprecated Use {@link importBulk} instead.
* @deprecated Use {@link importBulk} instead.
* @abstract
* @abstract
* @param {Array} records The records to load.
* @param {Array} records The records to load.
* @return {Promise}
* @return {Promise}
*/
*/
loadDump(records) {
loadDump(records) {
throw new Error("Not Implemented.");
throw new Error("Not Implemented.");
}
}
saveMetadata(metadata) {
saveMetadata(metadata) {
throw new Error("Not Implemented.");
throw new Error("Not Implemented.");
}
}
getMetadata() {
getMetadata() {
throw new Error("Not Implemented.");
throw new Error("Not Implemented.");
}
}
}
}


const RE_RECORD_ID = /^[a-zA-Z0-9][a-zA-Z0-9_-]*$/;
const RE_RECORD_ID = /^[a-zA-Z0-9][a-zA-Z0-9_-]*$/;
/**
/**
* Checks if a value is undefined.
* Checks if a value is undefined.
* @param {Any} value
* @param {Any} value
* @return {Boolean}
* @return {Boolean}
*/
*/
function _isUndefined(value) {
function _isUndefined(value) {
return typeof value === "undefined";
return typeof value === "undefined";
}
}
/**
/**
* Sorts records in a list according to a given ordering.
* Sorts records in a list according to a given ordering.
*
*
* @param {String} order The ordering, eg. `-last_modified`.
* @param {String} order The ordering, eg. `-last_modified`.
* @param {Array} list The collection to order.
* @param {Array} list The collection to order.
* @return {Array}
* @return {Array}
*/
*/
function sortObjects(order, list) {
function sortObjects(order, list) {
const hasDash = order[0] === "-";
const hasDash = order[0] === "-";
const field = hasDash ? order.slice(1) : order;
const field = hasDash ? order.slice(1) : order;
const direction = hasDash ? -1 : 1;
const direction = hasDash ? -1 : 1;
return list.slice().sort((a, b) => {
return list.slice().sort((a, b) => {
if (a[field] && _isUndefined(b[field])) {
if (a[field] && _isUndefined(b[field])) {
return direction;
return direction;
}
}
if (b[field] && _isUndefined(a[field])) {
if (b[field] && _isUndefined(a[field])) {
return -direction;
return -direction;
}
}
if (_isUndefined(a[field]) && _isUndefined(b[field])) {
if (_isUndefined(a[field]) && _isUndefined(b[field])) {
return 0;
return 0;
}
}
return a[field] > b[field] ? direction : -direction;
return a[field] > b[field] ? direction : -direction;
});
});
}
}
/**
/**
* Test if a single object matches all given filters.
* Test if a single object matches all given filters.
*
*
* @param {Object} filters The filters object.
* @param {Object} filters The filters object.
* @param {Object} entry The object to filter.
* @param {Object} entry The object to filter.
* @return {Boolean}
* @return {Boolean}
*/
*/
function filterObject(filters, entry) {
function filterObject(filters, entry) {
return Object.keys(filters).every(filter => {
return Object.keys(filters).every(filter => {
const value = filters[filter];
const value = filters[filter];
if (Array.isArray(value)) {
if (Array.isArray(value)) {
return value.some(candidate => candidate === entry[filter]);
return value.some(candidate => candidate === entry[filter]);
} else if (typeof value === "object") {
} else if (typeof value === "object") {
return filterObject(value, entry[filter]);
return filterObject(value, entry[filter]);
} else if (!Object.prototype.hasOwnProperty.call(entry, filter)) {
} else if (!Object.prototype.hasOwnProperty.call(entry, filter)) {
console.error(`The property ${filter} does not exist`);
console.error(`The property ${filter} does not exist`);
return false;
return false;
}
}
return entry[filter] === value;
return entry[filter] === value;
});
});
}
}
/**
/**
* Resolves a list of functions sequentially, which can be sync or async; in
* Resolves a list of functions sequentially, which can be sync or async; in
* case of async, functions must return a promise.
* case of async, functions must return a promise.
*
*
* @param {Array} fns The list of functions.
* @param {Array} fns The list of functions.
* @param {Any} init The initial value.
* @param {Any} init The initial value.
* @return {Promise}
* @return {Promise}
*/
*/
function waterfall(fns, init) {
function waterfall(fns, init) {
if (!fns.length) {
if (!fns.length) {
return Promise.resolve(init);
return Promise.resolve(init);
}
}
return fns.reduce((promise, nextFn) => {
return fns.reduce((promise, nextFn) => {
return promise.then(nextFn);
return promise.then(nextFn);
}, Promise.resolve(init));
}, Promise.resolve(init));
}
}
/**
/**
* Simple deep object comparison function. This only supports comparison of
* Simple deep object comparison function. This only supports comparison of
* serializable JavaScript objects.
* serializable JavaScript objects.
*
*
* @param {Object} a The source object.
* @param {Object} a The source object.
* @param {Object} b The compared object.
* @param {Object} b The compared object.
* @return {Boolean}
* @return {Boolean}
*/
*/
function deepEqual(a, b) {
function deepEqual(a, b) {
if (a === b) {
if (a === b) {
return true;
return true;
}
}
if (typeof a !== typeof b) {
if (typeof a !== typeof b) {
return false;
return false;
}
}
if (!(a && typeof a == "object") || !(b && typeof b == "object")) {
if (!(a && typeof a === "object") || !(b && typeof b === "object")) {
return false;
return false;
}
}
if (Object.keys(a).length !== Object.keys(b).length) {
if (Object.keys(a).length !== Object.keys(b).length) {
return false;
return false;
}
}
for (const k in a) {
for (const k in a) {
if (!deepEqual(a[k], b[k])) {
if (!deepEqual(a[k], b[k])) {
return false;
return false;
}
}
}
}
return true;
return true;
}
}
/**
/**
* Return an object without the specified keys.
* Return an object without the specified keys.
*
*
* @param {Object} obj The original object.
* @param {Object} obj The original object.
* @param {Array} keys The list of keys to exclude.
* @param {Array} keys The list of keys to exclude.
* @return {Object} A copy without the specified keys.
* @return {Object} A copy without the specified keys.
*/
*/
function omitKeys(obj, keys = []) {
function omitKeys(obj, keys = []) {
const result = Object.assign({}, obj);
const result = Object.assign({}, obj);
for (const key of keys) {
for (const key of keys) {
delete result[key];
delete result[key];
}
}
return result;
return result;
}
}
function arrayEqual(a, b) {
function arrayEqual(a, b) {
if (a.length !== b.length) {
if (a.length !== b.length) {
return false;
return false;
}
}
for (let i = a.length; i--; ) {
for (let i = a.length; i--; ) {
if (a[i] !== b[i]) {
if (a[i] !== b[i]) {
return false;
return false;
}
}
}
}
return true;
return true;
}
}
function makeNestedObjectFromArr(arr, val, nestedFiltersObj) {
function makeNestedObjectFromArr(arr, val, nestedFiltersObj) {
const last = arr.length - 1;
const last = arr.length - 1;
return arr.reduce((acc, cv, i) => {
return arr.reduce((acc, cv, i) => {
if (i === last) {
if (i === last) {
return (acc[cv] = val);
return (acc[cv] = val);
} else if (Object.prototype.hasOwnProperty.call(acc, cv)) {
} else if (Object.prototype.hasOwnProperty.call(acc, cv)) {
return acc[cv];
return acc[cv];
} else {
} else {
return (acc[cv] = {});
return (acc[cv] = {});
}
}
}, nestedFiltersObj);
}, nestedFiltersObj);
}
}
function transformSubObjectFilters(filtersObj) {
function transformSubObjectFilters(filtersObj) {
const transformedFilters = {};
const transformedFilters = {};
for (const key in filtersObj) {
for (const key in filtersObj) {
const keysArr = key.split(".");
const keysArr = key.split(".");
const val = filtersObj[key];
const val = filtersObj[key];
makeNestedObjectFromArr(keysArr, val, transformedFilters);
makeNestedObjectFromArr(keysArr, val, transformedFilters);
}
}
return transformedFilters;
return transformedFilters;
}
}


const INDEXED_FIELDS = ["id", "_status", "last_modified"];
const INDEXED_FIELDS = ["id", "_status", "last_modified"];
/**
/**
* Small helper that wraps the opening of an IndexedDB into a Promise.
* Small helper that wraps the opening of an IndexedDB into a Promise.
*
*
* @param dbname {String} The database name.
* @param dbname {String} The database name.
* @param version {Integer} Schema version
* @param version {Integer} Schema version
* @param onupgradeneeded {Function} The callback to execute if schema is
* @param onupgradeneeded {Function} The callback to execute if schema is
* missing or different.
* missing or different.
* @return {Promise<IDBDatabase>}
* @return {Promise<IDBDatabase>}
*/
*/
async function open(dbname, { version, onupgradeneeded }) {
async function open(dbname, { version, onupgradeneeded }) {
return new Promise((resolve, reject) => {
return new Promise((resolve, reject) => {
const request = indexedDB.open(dbname, version);
const request = indexedDB.open(dbname, version);
request.onupgradeneeded = event => {
request.onupgradeneeded = event => {
const db = event.target.result;
const db = request.result;
db.onerror = event => reject(event.target.error);
db.onerror = event => reject(request.error);
// When an upgrade is needed, a transaction is started.
// When an upgrade is needed, a transaction is started.
const transaction = event.target.transaction;
const transaction = request.transaction;
transaction.onabort = event => {
transaction.onabort = event => {
const error =
const error =
event.target.error ||
request.error ||
transaction.error ||
transaction.error ||
new DOMException("The operation has been aborted", "AbortError");
new DOMException("The operation has been aborted", "AbortError");
reject(error);
reject(error);
};
};
// Callback for store creation etc.
// Callback for store creation etc.
return onupgradeneeded(event);
return onupgradeneeded(event);
};
};
request.onerror = event => {
request.onerror = event => {
reject(event.target.error);
reject(event.target.error);
};
};
request.onsuccess = event => {
request.onsuccess = event => {
const db = event.target.result;
const db = request.result;
resolve(db);
resolve(db);
};
};
});
});
}
}
/**
/**
* Helper to run the specified callback in a single transaction on the
* Helper to run the specified callback in a single transaction on the
* specified store.
* specified store.
* The helper focuses on transaction wrapping into a promise.
* The helper focuses on transaction wrapping into a promise.
*
*
* @param db {IDBDatabase} The database instance.
* @param db {IDBDatabase} The database instance.
* @param name {String} The store name.
* @param name {String} The store name.
* @param callback {Function} The piece of code to execute in the transaction.
* @param callback {Function} The piece of code to execute in the transaction.
* @param options {Object} Options.
* @param options {Object} Options.
* @param options.mode {String} Transaction mode (default: read).
* @param options.mode {String} Transaction mode (default: read).
* @return {Promise} any value returned by the callback.
* @return {Promise} any value returned by the callback.
*/
*/
async function execute(db, name, callback, options = {}) {
async function execute(db, name, callback, options = {}) {
const { mode } = options;
const { mode } = options;
return new Promise((resolve, reject) => {
return new Promise((resolve, reject) => {
// On Safari, calling IDBDatabase.transaction with mode == undefined raises
// On Safari, calling IDBDatabase.transaction with mode == undefined raises
// a TypeError.
// a TypeError.
const transaction = mode
const transaction = mode
? db.transaction([name], mode)
? db.transaction([name], mode)
: db.transaction([name]);
: db.transaction([name]);
const store = transaction.objectStore(name);
const store = transaction.objectStore(name);
// Let the callback abort this transaction.
// Let the callback abort this transaction.
const abort = e => {
const abort = e => {
transaction.abort();
transaction.abort();
reject(e);
reject(e);
};
};
// Execute the specified callback **synchronously**.
// Execute the specified callback **synchronously**.
let result;
let result;
try {
try {
result = callback(store, abort);
result = callback(store, abort);
} catch (e) {
} catch (e) {
abort(e);
abort(e);
}
}
transaction.onerror = event => reject(event.target.error);
transaction.onerror = event => reject(event.target.error);
transaction.oncomplete = event => resolve(result);
transaction.oncomplete = event => resolve(result);
transaction.onabort = event => {
transaction.onabort = event => {
const error =
const error =
event.target.error ||
event.target.error ||
transaction.error ||
transaction.error ||
new DOMException("The operation has been aborted", "AbortError");
new DOMException("The operation has been aborted", "AbortError");
reject(error);
reject(error);
};
};
});
});
}
}
/**
/**
* Helper to wrap the deletion of an IndexedDB database into a promise.
* Helper to wrap the deletion of an IndexedDB database into a promise.
*
*
* @param dbName {String} the database to delete
* @param dbName {String} the database to delete
* @return {Promise}
* @return {Promise}
*/
*/
async function deleteDatabase(dbName) {
async function deleteDatabase(dbName) {
return new Promise((resolve, reject) => {
return new Promise((resolve, reject) => {
const request = indexedDB.deleteDatabase(dbName);
const request = indexedDB.deleteDatabase(dbName);
request.onsuccess = event => resolve(event.target);
request.onsuccess = event => resolve(event.target);
request.onerror = event => reject(event.target.error);
request.onerror = event => reject(event.target.error);
});
});
}
}
/**
/**
* IDB cursor handlers.
* IDB cursor handlers.
* @type {Object}
* @type {Object}
*/
*/
const cursorHandlers = {
const cursorHandlers = {
all(filters, done) {
all(filters, done) {
const results = [];
const results = [];
return event => {
return event => {
const cursor = event.target.result;
const cursor = event.target.result;
if (cursor) {
if (cursor) {
const { value } = cursor;
const { value } = cursor;
if (filterObject(filters, value)) {
if (filterObject(filters, value)) {
results.push(value);
results.push(value);
}
}
cursor.continue();
cursor.continue();
} else {
} else {
done(results);
done(results);
}
}
};
};
},
},
in(values, filters, done) {
in(values, filters, done) {
const results = [];
const results = [];
let i = 0;
let i = 0;
return function(event) {
return function(event) {
const cursor = event.target.result;
const cursor = event.target.result;
if (!cursor) {
if (!cursor) {
done(results);
done(results);
return;
return;
}
}
const { key, value } = cursor;
const { key, value } = cursor;
// `key` can be an array of two values (see `keyPath` in indices definitions).
// `key` can be an array of two values (see `keyPath` in indices definitions).
// `values` can be an array of arrays if we filter using an index whose key path
// `values` can be an array of arrays if we filter using an index whose key path
// is an array (eg. `cursorHandlers.in([["bid/cid", 42], ["bid/cid", 43]], ...)`)
// is an array (eg. `cursorHandlers.in([["bid/cid", 42], ["bid/cid", 43]], ...)`)
while (key > values[i]) {
while (key > values[i]) {
// The cursor has passed beyond this key. Check next.
// The cursor has passed beyond this key. Check next.
++i;
++i;
if (i === values.length) {
if (i === values.length) {
done(results); // There is no next. Stop searching.
done(results); // There is no next. Stop searching.
return;
return;
}
}
}
}
const isEqual = Array.isArray(key)
const isEqual = Array.isArray(key)
? arrayEqual(key, values[i])
? arrayEqual(key, values[i])
: key === values[i];
: key === values[i];
if (isEqual) {
if (isEqual) {
if (filterObject(filters, value)) {
if (filterObject(filters, value)) {
results.push(value);
results.push(value);
}
}
cursor.continue();
cursor.continue();
} else {
} else {
cursor.continue(values[i]);
cursor.continue(values[i]);
}
}
};
};
},
},
};
};
/**
/**
* Creates an IDB request and attach it the appropriate cursor event handler to
* Creates an IDB request and attach it the appropriate cursor event handler to
* perform a list query.
* perform a list query.
*
*
* Multiple matching values are handled by passing an array.
* Multiple matching values are handled by passing an array.
*
*
* @param {String} cid The collection id (ie. `{bid}/{cid}`)
* @param {String} cid The collection id (ie. `{bid}/{cid}`)
* @param {IDBStore} store The IDB store.
* @param {IDBStore} store The IDB store.
* @param {Object} filters Filter the records by field.
* @param {Object} filters Filter the records by field.
* @param {Function} done The operation completion handler.
* @param {Function} done The operation completion handler.
* @return {IDBRequest}
* @return {IDBRequest}
*/
*/
function createListRequest(cid, store, filters, done) {
function createListRequest(cid, store, filters, done) {
const filterFields = Object.keys(filters);
const filterFields = Object.keys(filters);
// If no filters, get all results in one bulk.
// If no filters, get all results in one bulk.
if (filterFields.length == 0) {
if (filterFields.length === 0) {
const request = store.index("cid").getAll(IDBKeyRange.only(cid));
const request = store.index("cid").getAll(IDBKeyRange.only(cid));
request.onsuccess = event => done(event.target.result);
request.onsuccess = event => done(event.target.result);
return request;
return request;
}
}
// Introspect filters and check if they leverage an indexed field.
// Introspect filters and check if they leverage an indexed field.
const indexField = filterFields.find(field => {
const indexField = filterFields.find(field => {
return INDEXED_FIELDS.includes(field);
return INDEXED_FIELDS.includes(field);
});
});
if (!indexField) {
if (!indexField) {
// Iterate on all records for this collection (ie. cid)
// Iterate on all records for this collection (ie. cid)
const isSubQuery = Object.keys(filters).some(key => key.includes(".")); // (ie. filters: {"article.title": "hello"})
const isSubQuery = Object.keys(filters).some(key => key.includes(".")); // (ie. filters: {"article.title": "hello"})
if (isSubQuery) {
if (isSubQuery) {
const newFilter = transformSubObjectFilters(filters);
const newFilter = transformSubObjectFilters(filters);
const request = store.index("cid").openCursor(IDBKeyRange.only(cid));
const request = store.index("cid").openCursor(IDBKeyRange.only(cid));
request.onsuccess = cursorHandlers.all(newFilter, done);
request.onsuccess = cursorHandlers.all(newFilter, done);
return request;
return request;
}
}
const request = store.index("cid").openCursor(IDBKeyRange.only(cid));
const request = store.index("cid").openCursor(IDBKeyRange.only(cid));
request.onsuccess = cursorHandlers.all(filters, done);
request.onsuccess = cursorHandlers.all(filters, done);
return request;
return request;
}
}
// If `indexField` was used already, don't filter again.
// If `indexField` was used already, don't filter again.
const remainingFilters = omitKeys(filters, [indexField]);
const remainingFilters = omitKeys(filters, [indexField]);
// value specified in the filter (eg. `filters: { _status: ["created", "updated"] }`)
// value specified in the filter (eg. `filters: { _status: ["created", "updated"] }`)
const value = filters[indexField];
const value = filters[indexField];
// For the "id" field, use the primary key.
// For the "id" field, use the primary key.
const indexStore = indexField == "id" ? store : store.index(indexField);
const indexStore = indexField === "id" ? store : store.index(indexField);
// WHERE IN equivalent clause
// WHERE IN equivalent clause
if (Array.isArray(value)) {
if (Array.isArray(value)) {
if (value.length === 0) {
if (value.length === 0) {
return done([]);
return done([]);
}
}
const values = value.map(i => [cid, i]).sort();
const values = value.map(i => [cid, i]).sort();
const range = IDBKeyRange.bound(values[0], values[values.length - 1]);
const range = IDBKeyRange.bound(values[0], values[values.length - 1]);
const request = indexStore.openCursor(range);
const request = indexStore.openCursor(range);
request.onsuccess = cursorHandlers.in(values, remainingFilters, done);
request.onsuccess = cursorHandlers.in(values, remainingFilters, done);
return request;
return request;
}
}
// If no filters on custom attribute, get all results in one bulk.
// If no filters on custom attribute, get all results in one bulk.
if (remainingFilters.length == 0) {
if (remainingFilters.length === 0) {
const request = indexStore.getAll(IDBKeyRange.only([cid, value]));
const request = indexStore.getAll(IDBKeyRange.only([cid, value]));
request.onsuccess = event => done(event.target.result);
request.onsuccess = event => done(event.target.result);
return request;
return request;
}
}
// WHERE field = value clause
// WHERE field = value clause
const request = indexStore.openCursor(IDBKeyRange.only([cid, value]));
const request = indexStore.openCursor(IDBKeyRange.only([cid, value]));
request.onsuccess = cursorHandlers.all(remainingFilters, done);
request.onsuccess = cursorHandlers.all(remainingFilters, done);
return request;
return request;
}
}
/**
/**
* IndexedDB adapter.
* IndexedDB adapter.
*
*
* This adapter doesn't support any options.
* This adapter doesn't support any options.
*/
*/
class IDB extends BaseAdapter {
class IDB extends BaseAdapter {
/**
/**
* Constructor.
* Constructor.
*
*
* @param {String} cid The key base for this collection (eg. `bid/cid`)
* @param {String} cid The key base for this collection (eg. `bid/cid`)
* @param {Object} options
* @param {Object} options
* @param {String} options.dbName The IndexedDB name (default: `"KintoDB"`)
* @param {String} options.dbName The IndexedDB name (default: `"KintoDB"`)
* @param {String} options.migrateOldData Whether old database data should be migrated (default: `false`)
* @param {String} options.migrateOldData Whether old database data should be migrated (default: `false`)
*/
*/
constructor(cid, options = {}) {
constructor(cid, options = {}) {
super();
super();
this.cid = cid;
this.cid = cid;
this.dbName = options.dbName || "KintoDB";
this.dbName = options.dbName || "KintoDB";
this._options = options;
this._options = options;
this._db = null;
this._db = null;
}
}
_handleError(method, err) {
_handleError(method, err) {
const error = new Error(`IndexedDB ${method}() ${err.message}`);
const error = new Error(`IndexedDB ${method}() ${err.message}`);
error.stack = err.stack;
error.stack = err.stack;
throw error;
throw error;
}
}
/**
/**
* Ensures a connection to the IndexedDB database has been opened.
* Ensures a connection to the IndexedDB database has been opened.
*
*
* @override
* @override
* @return {Promise}
* @return {Promise}
*/
*/
async open() {
async open() {
if (this._db) {
if (this._db) {
return this;
return this;
}
}
// In previous versions, we used to have a database with name `${bid}/${cid}`.
// In previous versions, we used to have a database with name `${bid}/${cid}`.
// Check if it exists, and migrate data once new schema is in place.
// Check if it exists, and migrate data once new schema is in place.
// Note: the built-in migrations from IndexedDB can only be used if the
// Note: the built-in migrations from IndexedDB can only be used if the
// database name does not change.
// database name does not change.
const dataToMigrate = this._options.migrateOldData
const dataToMigrate = this._options.migrateOldData
? await migrationRequired(this.cid)
? await migrationRequired(this.cid)
: null;
: null;
this._db = await open(this.dbName, {
this._db = await open(this.dbName, {
version: 2,
version: 2,
onupgradeneeded: event => {
onupgradeneeded: event => {
const db = event.target.result;
const db = event.target.result;
if (event.oldVersion < 1) {
if (event.oldVersion < 1) {
// Records store
// Records store
const recordsStore = db.createObjectStore("records", {
const recordsStore = db.createObjectStore("records", {
keyPath: ["_cid", "id"],
keyPath: ["_cid", "id"],
});
});
// An index to obtain all the records in a collection.
// An index to obtain all the records in a collection.
recordsStore.createIndex("cid", "_cid");
recordsStore.createIndex("cid", "_cid");
// Here we create indices for every known field in records by collection.
// Here we create indices for every known field in records by collection.
// Local record status ("synced", "created", "updated", "deleted")
// Local record status ("synced", "created", "updated", "deleted")
recordsStore.createIndex("_status", ["_cid", "_status"]);
recordsStore.createIndex("_status", ["_cid", "_status"]);
// Last modified field
// Last modified field
recordsStore.createIndex("last_modified", [
recordsStore.createIndex("last_modified", [
"_cid",
"_cid",
"last_modified",
"last_modified",
]);
]);
// Timestamps store
// Timestamps store
db.createObjectStore("timestamps", {
db.createObjectStore("timestamps", {
keyPath: "cid",
keyPath: "cid",
});
});
}
}
if (event.oldVersion < 2) {
if (event.oldVersion < 2) {
// Collections store
// Collections store
db.createObjectStore("collections", {
db.createObjectStore("collections", {
keyPath: "cid",
keyPath: "cid",
});
});
}
}
},
},
});
});
if (dataToMigrate) {
if (dataToMigrate) {
const { records, timestamp } = dataToMigrate;
const { records, timestamp } = dataToMigrate;
await this.importBulk(records);
await this.importBulk(records);
await this.saveLastModified(timestamp);
await this.saveLastModified(
timestamp !== null && timestamp !== void 0 ? timestamp : 0
);
console.log(`${this.cid}: data was migrated successfully.`);
console.log(`${this.cid}: data was migrated successfully.`);
// Delete the old database.
// Delete the old database.
await deleteDatabase(this.cid);
await deleteDatabase(this.cid);
console.warn(`${this.cid}: old database was deleted.`);
console.warn(`${this.cid}: old database was deleted.`);
}
}
return this;
return this;
}
}
/**
/**
* Closes current connection to the database.
* Closes current connection to the database.
*
*
* @override
* @override
* @return {Promise}
* @return {Promise}
*/
*/
close() {
close() {
if (this._db) {
if (this._db) {
this._db.close(); // indexedDB.close is synchronous
this._db.close(); // indexedDB.close is synchronous
this._db = null;
this._db = null;
}
}
return Promise.resolve();
return Promise.resolve();
}
}
/**
/**
* Returns a transaction and an object store for a store name.
* Returns a transaction and an object store for a store name.
*
*
* To determine if a transaction has completed successfully, we should rather
* To determine if a transaction has completed successfully, we should rather
* listen to the transaction’s complete event rather than the IDBObjectStore
* listen to the transaction’s complete event rather than the IDBObjectStore
* request’s success event, because the transaction may still fail after the
* request’s success event, because the transaction may still fail after the
* success event fires.
* success event fires.
*
*
* @param {String} name Store name
* @param {String} name Store name
* @param {Function} callback to execute
* @param {Function} callback to execute
* @param {Object} options Options
* @param {Object} options Options
* @param {String} options.mode Transaction mode ("readwrite" or undefined)
* @param {String} options.mode Transaction mode ("readwrite" or undefined)
* @return {Object}
* @return {Object}
*/
*/
async prepare(name, callback, options) {
async prepare(name, callback, options) {
await this.open();
await this.open();
await execute(this._db, name, callback, options);
await execute(this._db, name, callback, options);
}
}
/**
/**
* Deletes every records in the current collection.
* Deletes every records in the current collection.
*
*
* @override
* @override
* @return {Promise}
* @return {Promise}
*/
*/
async clear() {
async clear() {
try {
try {
await this.prepare(
await this.prepare(
"records",
"records",
store => {
store => {
const range = IDBKeyRange.only(this.cid);
const range = IDBKeyRange.only(this.cid);
const request = store.index("cid").openKeyCursor(range);
const request = store.index("cid").openKeyCursor(range);
request.onsuccess = event => {
request.onsuccess = event => {
const cursor = event.target.result;
const cursor = event.target.result;
if (cursor) {
if (cursor) {
store.delete(cursor.primaryKey);
store.delete(cursor.primaryKey);
cursor.continue();
cursor.continue();
}
}
};
};
return request;
return request;
},
},
{ mode: "readwrite" }
{ mode: "readwrite" }
);
);
} catch (e) {
} catch (e) {
this._handleError("clear", e);
this._handleError("clear", e);
}
}
}
}
/**
/**
* Executes the set of synchronous CRUD operations described in the provided
* Executes the set of synchronous CRUD operations described in the provided
* callback within an IndexedDB transaction, for current db store.
* callback within an IndexedDB transaction, for current db store.
*
*
* The callback will be provided an object exposing the following synchronous
* The callback will be provided an object exposing the following synchronous
* CRUD operation methods: get, create, update, delete.
* CRUD operation methods: get, create, update, delete.
*
*
* Important note: because limitations in IndexedDB implementations, no
* Important note: because limitations in IndexedDB implementations, no
* asynchronous code should be performed within the provided callback; the
* asynchronous code should be performed within the provided callback; the
* promise will therefore be rejected if the callback returns a Promise.
* promise will therefore be rejected if the callback returns a Promise.
*
*
* Options:
* Options:
* - {Array} preload: The list of record IDs to fetch and make available to
* - {Array} preload: The list of record IDs to fetch and make available to
* the transaction object get() method (default: [])
* the transaction object get() method (default: [])
*
*
* @example
* @example
* const db = new IDB("example");
* const db = new IDB("example");
* const result = await db.execute(transaction => {
* const result = await db.execute(transaction => {
* transaction.create({id: 1, title: "foo"});
* transaction.create({id: 1, title: "foo"});
* transaction.update({id: 2, title: "bar"});
* transaction.update({id: 2, title: "bar"});
* transaction.delete(3);
* transaction.delete(3);
* return "foo";
* return "foo";
* });
* });
*
*
* @override
* @override
* @param {Function} callback The operation description callback.
* @param {Function} callback The operation description callback.
* @param {Object} options The options object.
* @param {Object} options The options object.
* @return {Promise}
* @return {Promise}
*/
*/
async execute(callback, options = { preload: [] }) {
async execute(callback, options = { preload: [] }) {
// Transactions in IndexedDB are autocommited when a callback does not
// Transactions in IndexedDB are autocommited when a callback does not
// perform any additional operation.
// perform any additional operation.
// The way Promises are implemented in Firefox (see https://bugzilla.mozilla.org/show_bug.cgi?id=1193394)
// The way Promises are implemented in Firefox (see https://bugzilla.mozilla.org/show_bug.cgi?id=1193394)
// prevents using within an opened transaction.
// prevents using within an opened transaction.
// To avoid managing asynchronocity in the specified `callback`, we preload
// To avoid managing asynchronocity in the specified `callback`, we preload
// a list of record in order to execute the `callback` synchronously.
// a list of record in order to execute the `callback` synchronously.
// See also:
// See also:
// - http://stackoverflow.com/a/28388805/330911
// - http://stackoverflow.com/a/28388805/330911
// - http://stackoverflow.com/a/10405196
// - http://stackoverflow.com/a/10405196
// - https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/
// - https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/
let result;
let result;
await this.prepare(
await this.prepare(
"records",
"records",
(store, abort) => {
(store, abort) => {
const runCallback = (preloaded = []) => {
const runCallback = (preloaded = {}) => {
// Expose a consistent API for every adapter instead of raw store methods.
// Expose a consistent API for every adapter instead of raw store methods.
const proxy = transactionProxy(this, store, preloaded);
const proxy = transactionProxy(this, store, preloaded);
// The callback is executed synchronously within the same transaction.
// The callback is executed synchronously within the same transaction.
try {
try {
const returned = callback(proxy);
const returned = callback(proxy);
if (returned instanceof Promise) {
if (returned instanceof Promise) {
// XXX: investigate how to provide documentation details in error.
// XXX: investigate how to provide documentation details in error.
throw new Error(
throw new Error(
"execute() callback should not return a Promise."
"execute() callback should not return a Promise."
);
);
}
}
// Bring to scope that will be returned (once promise awaited).
// Bring to scope that will be returned (once promise awaited).
result = returned;
result = returned;
} catch (e) {
} catch (e) {
// The callback has thrown an error explicitly. Abort transaction cleanly.
// The callback has thrown an error explicitly. Abort transaction cleanly.
abort(e);
abort && abort(e);
}
}
};
};
// No option to preload records, go straight to `callback`.
// No option to preload records, go straight to `callback`.
if (!options.preload.length) {
if (!options.preload) {
return runCallback();
return runCallback();
}
}
// Preload specified records using a list request.
// Preload specified records using a list request.
const filters = { id: options.preload };
const filters = { id: options.preload };
createListRequest(this.cid, store, filters, records => {
createListRequest(this.cid, store, filters, records => {
// Store obtained records by id.
// Store obtained records by id.
const preloaded = {};
const preloaded = {};
for (const record of records) {
for (const record of records) {
delete record["_cid"];
delete record["_cid"];
preloaded[record.id] = record;
preloaded[record.id] = record;
}
}
runCallback(preloaded);
runCallback(preloaded);
});
});
},
},
{ mode: "readwrite" }
{ mode: "readwrite" }
);
);
return result;
return result;
}
}
/**
/**
* Retrieve a record by its primary key from the IndexedDB database.
* Retrieve a record by its primary key from the IndexedDB database.
*
*
* @override
* @override
* @param {String} id The record id.
* @param {String} id The record id.
* @return {Promise}
* @return {Promise}
*/
*/
async get(id) {
async get(id) {
try {
try {
let record;
let record;
await this.prepare("records", store => {
await this.prepare("records", store => {
store.get([this.cid, id]).onsuccess = e => (record = e.target.result);
store.get([this.cid, id]).onsuccess = e => (record = e.target.result);
});
});
return record;
return record;
} catch (e) {
} catch (e) {
this._handleError("get", e);
this._handleError("get", e);
}
}
}
}
/**
/**
* Lists all records from the IndexedDB database.
* Lists all records from the IndexedDB database.
*
*
* @override
* @override
* @param {Object} params The filters and order to apply to the results.
* @param {Object} params
* @return {Promise}
*/
async list(params = { filters: {} }) {
cons