Kinto.js typescript diff

Created Diff never expires
25 removals
Lines
Total
Removed
Words
Total
Removed
To continue using this feature, upgrade to
Diffchecker logo
Diffchecker Pro
755 lines
29 additions
Lines
Total
Added
Words
Total
Added
To continue using this feature, upgrade to
Diffchecker logo
Diffchecker Pro
758 lines
/*
/*
*
*
* 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