indexeddbshim
Version:
A polyfill for IndexedDB using WebSql
960 lines (898 loc) • 38.4 kB
JavaScript
import SyncPromise from 'sync-promise-expanded';
import {createDOMException} from './DOMException.js';
import {IDBCursor, IDBCursorWithValue} from './IDBCursor.js';
import * as util from './util.js';
import * as Key from './Key.js';
import {setSQLForKeyRange, IDBKeyRange, convertValueToKeyRange} from './IDBKeyRange.js';
import IDBTransaction from './IDBTransaction.js';
import * as Sca from './Sca.js';
import CFG from './CFG.js';
import IDBObjectStore from './IDBObjectStore.js';
const readonlyProperties = ['objectStore', 'keyPath', 'multiEntry', 'unique'];
/**
* @typedef {number} Integer
*/
/**
* @typedef {{
* columnName: string,
* keyPath: import('./Key.js').KeyPath,
* optionalParams: {
* unique: boolean,
* multiEntry: boolean
* }
* deleted?: boolean,
* __deleted?: boolean,
* cursors?: import('./IDBCursor.js').IDBCursorWithValueFull[],
* }} IDBIndexProperties
*/
/**
* IDB Index.
* @see http://www.w3.org/TR/IndexedDB/#idl-def-IDBIndex
* @class
*/
function IDBIndex () {
throw new TypeError('Illegal constructor');
}
const IDBIndexAlias = IDBIndex;
/**
* @typedef {IDBIndex & {
* name: string,
* keyPath: import('./Key.js').KeyPath,
* multiEntry: boolean,
* unique: boolean,
* objectStore: import('./IDBObjectStore.js').IDBObjectStoreFull,
* __pendingCreate?: boolean,
* __deleted?: boolean,
* __originalName: string,
* __currentName: string,
* __pendingName?: string,
* __pendingDelete?: boolean,
* __name: string,
* __multiEntry: boolean,
* __unique: boolean,
* __objectStore: import('./IDBObjectStore.js').IDBObjectStoreFull,
* __keyPath: import('./Key.js').KeyPath,
* __recreated?: boolean
* }} IDBIndexFull
*/
/**
*
* @param {import('./IDBObjectStore.js').IDBObjectStoreFull} store
* @param {IDBIndexProperties} indexProperties
* @returns {IDBIndexFull}
*/
IDBIndex.__createInstance = function (store, indexProperties) {
/**
* @class
* @this {IDBIndexFull}
*/
function IDBIndex () {
const me = this;
// @ts-expect-error It's ok
me[Symbol.toStringTag] = 'IDBIndex';
util.defineReadonlyProperties(me, readonlyProperties);
me.__objectStore = store;
me.__name = me.__originalName = indexProperties.columnName;
me.__keyPath = Array.isArray(indexProperties.keyPath) ? indexProperties.keyPath.slice() : indexProperties.keyPath;
const {optionalParams} = indexProperties;
me.__multiEntry = Boolean(optionalParams && optionalParams.multiEntry);
me.__unique = Boolean(optionalParams && optionalParams.unique);
me.__deleted = Boolean(indexProperties.__deleted);
me.__objectStore.__cursors = indexProperties.cursors || [];
Object.defineProperty(me, '__currentName', {
/**
* @this {IDBIndexFull}
* @returns {string}
*/
get () {
return '__pendingName' in me
? /** @type {string} */ (me.__pendingName)
: me.name;
}
});
Object.defineProperty(me, 'name', {
enumerable: false,
configurable: false,
/**
* @this {IDBIndexFull}
* @returns {string}
*/
get () {
return this.__name;
},
/**
* @param {string} newName
* @this {IDBIndexFull}
* @returns {void}
*/
set (newName) {
const me = this;
newName = util.convertToDOMString(newName);
const oldName = me.name;
IDBTransaction.__assertVersionChange(me.objectStore.transaction);
IDBTransaction.__assertActive(me.objectStore.transaction);
IDBIndexAlias.__invalidStateIfDeleted(me);
IDBObjectStore.__invalidStateIfDeleted(me);
if (newName === oldName) {
return;
}
if (me.objectStore.__indexes[newName] && !me.objectStore.__indexes[newName].__deleted &&
!me.objectStore.__indexes[newName].__pendingDelete) {
throw createDOMException('ConstraintError', 'Index "' + newName + '" already exists on ' + me.objectStore.__currentName);
}
me.__name = newName;
const {objectStore} = me;
delete objectStore.__indexes[oldName];
objectStore.__indexes[newName] = me;
objectStore.indexNames.splice(objectStore.indexNames.indexOf(oldName), 1, newName);
const storeHandle = /** @type {import('./IDBTransaction.js').IDBTransactionFull} */ (
objectStore.transaction
).__storeHandles[objectStore.name];
const oldIndexHandle = storeHandle.__indexHandles[oldName];
oldIndexHandle.__name = newName; // Fix old references
storeHandle.__indexHandles[newName] = oldIndexHandle; // Ensure new reference accessible
me.__pendingName = oldName;
const colInfoToPreserveArr = [
['key', 'BLOB ' + (objectStore.autoIncrement ? 'UNIQUE, inc INTEGER PRIMARY KEY AUTOINCREMENT' : 'PRIMARY KEY')],
['value', 'BLOB']
].concat(
// @ts-expect-error Has numeric indexes instead of iterator
[...objectStore.indexNames]
.filter((indexName) => indexName !== newName)
.map((indexName) => [util.escapeIndexNameForSQL(indexName), 'BLOB'])
);
me.__renameIndex(
objectStore, oldName, newName, colInfoToPreserveArr,
function (tx, success) {
IDBIndexAlias.__updateIndexList(store, tx, function (store) {
delete storeHandle.__pendingName;
success(store);
});
}
);
}
});
}
IDBIndex.prototype = IDBIndexAlias.prototype;
// @ts-expect-error It's ok
return new IDBIndex();
};
/**
*
* @param {IDBIndexFull} index
* @param {string} [msg]
* @throws {DOMException}
* @returns {void}
*/
IDBIndex.__invalidStateIfDeleted = function (index, msg) {
if (index.__deleted || index.__pendingDelete || (
index.__pendingCreate && index.objectStore.transaction && index.objectStore.transaction.__errored
)) {
throw createDOMException('InvalidStateError', msg || 'This index has been deleted');
}
};
/**
* Clones an IDBIndex instance for a different IDBObjectStore instance.
* @param {IDBIndexFull} index
* @param {import('./IDBObjectStore.js').IDBObjectStoreFull} store
* @returns {IDBIndexFull}
*/
IDBIndex.__clone = function (index, store) {
const idx = IDBIndex.__createInstance(store, {
columnName: index.name,
keyPath: index.keyPath,
optionalParams: {
multiEntry: index.multiEntry,
unique: index.unique
}
});
/** @type {const} */ ([
'__pendingCreate', '__pendingDelete', '__deleted',
'__originalName', '__recreated'
]).forEach((p) => {
// @ts-expect-error Why is this type "never"?
idx[p] = index[p];
});
return idx;
};
/**
* Creates a new index on an object store.
* @param {import('./IDBObjectStore.js').IDBObjectStoreFull} store
* @param {IDBIndexFull} index
* @returns {void}
*/
IDBIndex.__createIndex = function (store, index) {
const indexName = index.name;
const storeName = store.__currentName;
const idx = store.__indexes[indexName];
index.__pendingCreate = true;
// Add the index to the IDBObjectStore
store.indexNames.push(indexName);
store.__indexes[indexName] = index; // We add to indexes as needs to be available, e.g., if there is a subsequent deleteIndex call
let indexHandle = store.__indexHandles[indexName];
if (!indexHandle ||
index.__pendingDelete ||
index.__deleted ||
indexHandle.__pendingDelete ||
indexHandle.__deleted
) {
indexHandle = store.__indexHandles[indexName] = IDBIndex.__clone(index, store);
}
// Create the index in WebSQL
const {transaction} = store;
/** @type {import('./IDBTransaction.js').IDBTransactionFull} */ (
transaction
).__addNonRequestToTransactionQueue(function createIndex (tx, args, success, failure) {
const columnExists = idx && (idx.__deleted || idx.__recreated); // This check must occur here rather than earlier as properties may not have been set yet otherwise
/** @type {{[key: string]: boolean}} */
let indexValues = {};
/**
* @param {SQLTransaction} tx
* @param {SQLError} err
* @returns {void}
*/
function error (tx, err) {
failure(createDOMException('UnknownError', 'Could not create index "' + indexName + '"' + err.code + '::' + err.message, err));
}
/**
* @param {SQLTransaction} tx
* @returns {void}
*/
function applyIndex (tx) {
// Update the object store's index list
IDBIndex.__updateIndexList(store, tx, function () {
// Add index entries for all existing records
tx.executeSql('SELECT "key", "value" FROM ' + util.escapeStoreNameForSQL(storeName), [], function (tx, data) {
if (CFG.DEBUG) { console.log('Adding existing ' + storeName + ' records to the ' + indexName + ' index'); }
addIndexEntry(0);
/**
* @param {Integer} i
* @returns {void}
*/
function addIndexEntry (i) {
if (i < data.rows.length) {
try {
const value = Sca.decode(util.unescapeSQLiteResponse(data.rows.item(i).value));
const indexKey = Key.extractKeyValueDecodedFromValueUsingKeyPath(value, index.keyPath, index.multiEntry); // Todo: Do we need this stricter error checking?
if (
('invalid' in indexKey && indexKey.invalid) ||
('failure' in indexKey && indexKey.failure)
) { // Todo: Do we need invalid checks and should we instead treat these as being duplicates?
throw new Error('Go to catch; ignore bad indexKey');
}
const indexKeyStr = /** @type {string} */ (
Key.encode(indexKey.value, index.multiEntry)
);
if (index.unique) {
if (indexValues[indexKeyStr]) {
indexValues = {};
failure(createDOMException(
'ConstraintError',
'Duplicate values already exist within the store'
));
return;
}
indexValues[indexKeyStr] = true;
}
tx.executeSql(
'UPDATE ' + util.escapeStoreNameForSQL(storeName) + ' SET ' +
util.escapeIndexNameForSQL(indexName) + ' = ? WHERE "key" = ?',
[util.escapeSQLiteStatement(indexKeyStr), data.rows.item(i).key],
function () {
addIndexEntry(i + 1);
},
/** @type {SQLStatementErrorCallback} */ (error)
);
} catch (err) {
// Not a valid value to insert into index, so just continue
addIndexEntry(i + 1);
}
} else {
delete index.__pendingCreate;
delete indexHandle.__pendingCreate;
if (index.__deleted) {
delete index.__deleted;
delete indexHandle.__deleted;
index.__recreated = true;
indexHandle.__recreated = true;
}
indexValues = {};
success(store);
}
}
}, /** @type {SQLStatementErrorCallback} */ (error));
}, /** @type {SQLStatementErrorCallback} */ (error));
}
const escapedStoreNameSQL = util.escapeStoreNameForSQL(storeName);
const escapedIndexNameSQL = util.escapeIndexNameForSQL(index.name);
/**
* @param {SQLTransaction} tx
* @returns {void}
*/
function addIndexSQL (tx) {
if (!CFG.useSQLiteIndexes) {
applyIndex(tx);
return;
}
tx.executeSql(
'CREATE INDEX IF NOT EXISTS "' +
// The escaped index name must be unique among indexes in the whole database;
// so we prefix with store name; as prefixed, will also not conflict with
// index on `key`
// Avoid quotes and separate with special escape sequence
escapedStoreNameSQL.slice(1, -1) + '^5' + escapedIndexNameSQL.slice(1, -1) +
'" ON ' + escapedStoreNameSQL + '(' + escapedIndexNameSQL + ')',
[],
applyIndex,
/** @type {SQLStatementErrorCallback} */ (error)
);
}
if (columnExists) {
// For a previously existing index, just update the index entries in the existing column;
// no need to add SQLite index to it either as should already exist
applyIndex(tx);
} else {
// For a new index, add a new column to the object store, then apply the index
const sql = ['ALTER TABLE', escapedStoreNameSQL, 'ADD', escapedIndexNameSQL, 'BLOB'].join(' ');
if (CFG.DEBUG) { console.log(sql); }
tx.executeSql(
sql, [], addIndexSQL, /** @type {SQLStatementErrorCallback} */ (error)
);
}
});
};
/**
* Deletes an index from an object store.
* @param {import('./IDBObjectStore.js').IDBObjectStoreFull} store
* @param {IDBIndexFull} index
* @returns {void}
*/
IDBIndex.__deleteIndex = function (store, index) {
// Remove the index from the IDBObjectStore
index.__pendingDelete = true;
const indexHandle = store.__indexHandles[index.name];
if (indexHandle) {
indexHandle.__pendingDelete = true;
}
store.indexNames.splice(store.indexNames.indexOf(index.name), 1);
// Remove the index in WebSQL
const {transaction} = store;
/** @type {import('./IDBTransaction.js').IDBTransactionFull} */ (
transaction
).__addNonRequestToTransactionQueue(function deleteIndex (tx, args, success, failure) {
/**
* @param {SQLTransaction} tx
* @param {SQLError} err
* @returns {void}
*/
function error (tx, err) {
failure(createDOMException('UnknownError', 'Could not delete index "' + index.name + '"', err));
}
/**
* @returns {void}
*/
function finishDeleteIndex () {
// Update the object store's index list
IDBIndex.__updateIndexList(store, tx, function (store) {
delete index.__pendingDelete;
delete index.__recreated;
index.__deleted = true;
if (indexHandle) {
indexHandle.__deleted = true;
delete indexHandle.__pendingDelete;
}
success(store);
}, /** @type {SQLStatementErrorCallback} */ (error));
}
if (!CFG.useSQLiteIndexes) {
finishDeleteIndex();
return;
}
tx.executeSql(
'DROP INDEX IF EXISTS ' +
util.sqlQuote(
util.escapeStoreNameForSQL(store.name).slice(1, -1) + '^5' +
util.escapeIndexNameForSQL(index.name).slice(1, -1)
),
[],
finishDeleteIndex,
/** @type {SQLStatementErrorCallback} */ (error)
);
});
};
/**
* @typedef {{[key: string]: IDBIndexProperties}} IndexList
*/
/**
* Updates index list for the given object store.
* @param {import('./IDBObjectStore.js').IDBObjectStoreFull} store
* @param {SQLTransaction} tx
* @param {(store: IDBObjectStore) => void} success
* @param {(
* tx: SQLTransaction,
* err: SQLError
* ) => boolean} [failure]
* @returns {void}
*/
IDBIndex.__updateIndexList = function (store, tx, success, failure) {
/** @type {IndexList} **/
const indexList = {};
for (let i = 0; i < store.indexNames.length; i++) {
const idx = store.__indexes[store.indexNames[i]];
indexList[idx.name] = {
columnName: idx.name,
keyPath: idx.keyPath,
optionalParams: {
unique: idx.unique,
multiEntry: idx.multiEntry
},
deleted: Boolean(idx.__deleted)
};
}
if (CFG.DEBUG) { console.log('Updating the index list for ' + store.__currentName, indexList); }
tx.executeSql('UPDATE __sys__ SET "indexList" = ? WHERE "name" = ?', [JSON.stringify(indexList), util.escapeSQLiteStatement(store.__currentName)], function () {
success(store);
}, /** @type {SQLStatementErrorCallback} */ (failure));
};
/**
* @typedef {any|IDBKeyRange} Query
*/
/**
* Retrieves index data for the given key.
* @param {Query} range
* @param {"value"|"key"|"count"} opType
* @param {boolean} nullDisallowed
* @param {number} [count]
* @this {IDBIndexFull}
* @returns {import('./IDBRequest.js').IDBRequestFull}
*/
IDBIndex.prototype.__fetchIndexData = function (range, opType, nullDisallowed, count) {
const me = this;
if (count !== undefined) {
count = util.enforceRange(count, 'unsigned long');
}
IDBIndex.__invalidStateIfDeleted(me);
IDBObjectStore.__invalidStateIfDeleted(me.objectStore);
if (me.objectStore.__deleted) {
throw createDOMException('InvalidStateError', "This index's object store has been deleted");
}
IDBTransaction.__assertActive(me.objectStore.transaction);
if (nullDisallowed && util.isNullish(range)) {
throw createDOMException('DataError', 'No key or range was specified');
}
const fetchArgs = buildFetchIndexDataSQL(nullDisallowed, me, range, opType, false);
return /** @type {import('./IDBTransaction.js').IDBTransactionFull} */ (
me.objectStore.transaction
).__addToTransactionQueue(function (...args) {
executeFetchIndexData(
count,
...fetchArgs,
// @ts-expect-error It's ok
...args
);
}, undefined, me);
};
/**
* Opens a cursor over the given key range.
* @this {IDBIndexFull}
* @returns {import('./IDBRequest.js').IDBRequestFull}
*/
IDBIndex.prototype.openCursor = function (/* query, direction */) {
const me = this;
// eslint-disable-next-line prefer-rest-params -- API
const [query, direction] = arguments;
const cursor = IDBCursorWithValue.__createInstance(query, direction, me.objectStore, me, util.escapeIndexNameForSQLKeyColumn(me.name), 'value');
me.__objectStore.__cursors.push(cursor);
return cursor.__request;
};
/**
* Opens a cursor over the given key range. The cursor only includes key values, not data.
* @this {IDBIndexFull}
* @returns {import('./IDBRequest.js').IDBRequestFull}
*/
IDBIndex.prototype.openKeyCursor = function (/* query, direction */) {
const me = this;
// eslint-disable-next-line prefer-rest-params -- API
const [query, direction] = arguments;
const cursor = IDBCursor.__createInstance(query, direction, me.objectStore, me, util.escapeIndexNameForSQLKeyColumn(me.name), 'key');
me.__objectStore.__cursors.push(cursor);
return cursor.__request;
};
/**
*
* @param {Query} query
* @throws {TypeError}
* @this {IDBIndexFull}
* @returns {import('./IDBRequest.js').IDBRequestFull}
*/
IDBIndex.prototype.get = function (query) {
if (!arguments.length) { // Per https://heycam.github.io/webidl/
throw new TypeError('A parameter was missing for `IDBIndex.get`.');
}
return this.__fetchIndexData(query, 'value', true);
};
/**
*
* @param {Query} query
* @throws {TypeError}
* @this {IDBIndexFull}
* @returns {import('./IDBRequest.js').IDBRequestFull}
*/
IDBIndex.prototype.getKey = function (query) {
if (!arguments.length) { // Per https://heycam.github.io/webidl/
throw new TypeError('A parameter was missing for `IDBIndex.getKey`.');
}
return this.__fetchIndexData(query, 'key', true);
};
/**
* @this {IDBIndexFull}
* @returns {import('./IDBRequest.js').IDBRequestFull}
*/
IDBIndex.prototype.getAll = function (/* query, count */) {
// eslint-disable-next-line prefer-rest-params -- API
const [query, count] = arguments;
return this.__fetchIndexData(query, 'value', false, count);
};
/**
* @this {IDBIndexFull}
* @returns {import('./IDBRequest.js').IDBRequestFull}
*/
IDBIndex.prototype.getAllKeys = function (/* query, count */) {
// eslint-disable-next-line prefer-rest-params -- API
const [query, count] = arguments;
return this.__fetchIndexData(query, 'key', false, count);
};
/**
* @this {IDBIndexFull}
* @returns {import('./IDBRequest.js').IDBRequestFull}
*/
IDBIndex.prototype.count = function (/* query */) {
const me = this;
// eslint-disable-next-line prefer-rest-params -- API
const query = arguments[0];
// With the exception of needing to check whether the index has been
// deleted, we could, for greater spec parity (if not accuracy),
// just call:
// `return me.__objectStore.count(query);`
if (util.instanceOf(query, IDBKeyRange)) { // Todo: Do we need this block?
// We don't need to add to cursors array since has the count parameter which won't cache
return IDBCursorWithValue.__createInstance(query, 'next', me.objectStore, me, util.escapeIndexNameForSQLKeyColumn(me.name), 'value', true).__request;
}
return me.__fetchIndexData(query, 'count', false);
};
/**
*
* @param {import('./IDBObjectStore.js').IDBObjectStoreFull} store
* @param {string} oldName
* @param {string} newName
* @param {string[][]} colInfoToPreserveArr
* @param {null|((
* tx: SQLTransaction,
* success: ((store: IDBObjectStore) => void)
* ) => void)} cb
* @this {IDBIndexFull}
* @returns {void}
*/
IDBIndex.prototype.__renameIndex = function (store, oldName, newName, colInfoToPreserveArr = [], cb = null) {
const newNameType = 'BLOB';
const storeName = store.__currentName;
const escapedStoreNameSQL = util.escapeStoreNameForSQL(storeName);
const escapedNewIndexNameSQL = util.escapeIndexNameForSQL(newName);
const escapedTmpStoreNameSQL = util.sqlQuote('tmp_' + util.escapeStoreNameForSQL(storeName).slice(1, -1));
const colNamesToPreserve = colInfoToPreserveArr.map((colInfo) => colInfo[0]);
const colInfoToPreserve = colInfoToPreserveArr.map((colInfo) => colInfo.join(' '));
const listColInfoToPreserve = (colInfoToPreserve.length ? (colInfoToPreserve.join(', ') + ', ') : '');
const listColsToPreserve = (colNamesToPreserve.length ? (colNamesToPreserve.join(', ') + ', ') : '');
// We could adapt the approach at http://stackoverflow.com/a/8430746/271577
// to make the approach reusable without passing column names, but it is a bit fragile
/** @type {import('./IDBTransaction.js').IDBTransactionFull} */ (
store.transaction
).__addNonRequestToTransactionQueue(function renameIndex (tx, args, success, error) {
/**
* @param {SQLTransaction} tx
* @param {SQLError} err
* @returns {void}
*/
function sqlError (tx, err) {
error(err);
}
/**
* @returns {void}
*/
function finish () {
if (cb) {
cb(tx, success);
return;
}
success();
}
// See https://www.sqlite.org/lang_altertable.html#otheralter
// We don't query for indexes as we already have the info
// This approach has the advantage of auto-deleting indexes via the DROP TABLE
const sql = 'CREATE TABLE ' + escapedTmpStoreNameSQL +
'(' + listColInfoToPreserve + escapedNewIndexNameSQL + ' ' + newNameType + ')';
if (CFG.DEBUG) { console.log(sql); }
tx.executeSql(sql, [], function () {
const sql = 'INSERT INTO ' + escapedTmpStoreNameSQL + '(' +
listColsToPreserve + escapedNewIndexNameSQL +
') SELECT ' + listColsToPreserve + util.escapeIndexNameForSQL(oldName) + ' FROM ' + escapedStoreNameSQL;
if (CFG.DEBUG) { console.log(sql); }
tx.executeSql(sql, [], function () {
const sql = 'DROP TABLE ' + escapedStoreNameSQL;
if (CFG.DEBUG) { console.log(sql); }
tx.executeSql(sql, [], function () {
const sql = 'ALTER TABLE ' + escapedTmpStoreNameSQL + ' RENAME TO ' + escapedStoreNameSQL;
if (CFG.DEBUG) { console.log(sql); }
tx.executeSql(sql, [], function (tx) {
if (!CFG.useSQLiteIndexes) {
finish();
return;
}
const indexCreations = colNamesToPreserve
.slice(2) // Doing `key` separately and no need for index on `value`
.map((escapedIndexNameSQL) => new SyncPromise(function (resolve, reject) {
const escapedIndexToRecreate = util.sqlQuote(
escapedStoreNameSQL.slice(1, -1) + '^5' + escapedIndexNameSQL.slice(1, -1)
);
// const sql = 'DROP INDEX IF EXISTS ' + escapedIndexToRecreate;
// if (CFG.DEBUG) { console.log(sql); }
// tx.executeSql(sql, [], function () {
const sql = 'CREATE INDEX ' +
escapedIndexToRecreate + ' ON ' + escapedStoreNameSQL + '(' + escapedIndexNameSQL + ')';
if (CFG.DEBUG) { console.log(sql); }
tx.executeSql(
sql,
[],
resolve,
/** @type {SQLStatementErrorCallback} */
(function (tx, err) {
reject(err);
})
);
// }, function (tx, err) {
// reject(err);
// });
}));
indexCreations.push(
new SyncPromise(function (resolve, reject) {
const escapedIndexToRecreate = util.sqlQuote('sk_' + escapedStoreNameSQL.slice(1, -1));
// Chrome erring here if not dropped first; Node does not
const sql = 'DROP INDEX IF EXISTS ' + escapedIndexToRecreate;
if (CFG.DEBUG) { console.log(sql); }
tx.executeSql(
sql, [], function () {
const sql = 'CREATE INDEX ' + escapedIndexToRecreate +
' ON ' + escapedStoreNameSQL + '("key")';
if (CFG.DEBUG) { console.log(sql); }
tx.executeSql(
sql, [], resolve,
/** @type {SQLStatementErrorCallback} */
(function (tx, err) {
reject(err);
})
);
},
/** @type {SQLStatementErrorCallback} */
(function (tx, err) {
reject(err);
})
);
})
);
SyncPromise.all(indexCreations).then(
finish,
/** @type {(reason: any) => PromiseLike<never>} */
(error)
).catch((err) => {
console.log('Index rename error');
throw err;
});
}, /** @type {SQLStatementErrorCallback} */ (sqlError));
}, /** @type {SQLStatementErrorCallback} */ (sqlError));
}, /** @type {SQLStatementErrorCallback} */ (sqlError));
}, /** @type {SQLStatementErrorCallback} */ (sqlError));
});
};
/**
* @typedef {any} AnyValue
*/
Object.defineProperty(IDBIndex, Symbol.hasInstance, {
/**
* @param {AnyValue} obj
* @returns {boolean}
*/
value: (obj) => util.isObj(obj) &&
'openCursor' in obj &&
typeof obj.openCursor === 'function' &&
'multiEntry' in obj &&
typeof obj.multiEntry === 'boolean'
});
util.defineReadonlyOuterInterface(IDBIndex.prototype, readonlyProperties);
util.defineOuterInterface(IDBIndex.prototype, ['name']);
IDBIndex.prototype[Symbol.toStringTag] = 'IDBIndexPrototype';
Object.defineProperty(IDBIndex, 'prototype', {
writable: false
});
/**
* @param {number|null} count
* @param {boolean} unboundedDisallowed
* @param {IDBIndexFull} index
* @param {boolean} hasKey
* @param {import('./Key.js').Value|import('./Key.js').Key} range
* @param {"value"|"key"|"count"} opType
* @param {boolean} multiChecks
* @param {string[]} sql
* @param {string[]} sqlValues
* @param {SQLTransaction} tx
* @param {null|undefined} args
* @param {(result: number|undefined|[]|AnyValue|AnyValue[]) => void} success
* @param {(tx: SQLTransaction, err: SQLError) => void} error
* @returns {void}
*/
function executeFetchIndexData (
count, unboundedDisallowed, index, hasKey, range, opType,
multiChecks, sql, sqlValues, tx, args, success, error
) {
if (unboundedDisallowed) {
count = 1;
}
if (count) {
sql.push('LIMIT', String(count));
}
const isCount = opType === 'count';
if (CFG.DEBUG) { console.log('Trying to fetch data for Index', sql.join(' '), sqlValues); }
tx.executeSql(sql.join(' '), sqlValues, function (tx, data) {
const records = [];
let recordCount = 0;
const decode = isCount
? () => { /* */ }
: (opType === 'key'
// eslint-disable-next-line @stylistic/operator-linebreak -- JSDoc
?
/**
* @param {{
* key: string
* }} record
* @returns {import('./Key.js').ValueType|undefined}
*/
(record) => {
// Key.convertValueToKey(record.key); // Already validated before storage
return Key.decode(util.unescapeSQLiteResponse(record.key));
}
// eslint-disable-next-line @stylistic/operator-linebreak -- JSDoc
:
/**
* @param {{
* value: string
* }} record
* @returns {AnyValue}
*/
(record) => { // when opType is value
return Sca.decode(util.unescapeSQLiteResponse(record.value));
});
if (index.multiEntry) {
const escapedIndexNameForKeyCol = util.escapeIndexNameForSQLKeyColumn(index.name);
const encodedKey = Key.encode(range, index.multiEntry);
for (let i = 0; i < data.rows.length; i++) {
const row = data.rows.item(i);
const rowKey = /** @type {import('./Key.js').ValueTypeArray} */ (
Key.decode(row[escapedIndexNameForKeyCol])
);
let record;
if (hasKey && (
(multiChecks && range.some(
/**
* @param {string} check
* @returns {boolean}
*/
(check) => rowKey.includes(check)
)) || // More precise than our SQL
Key.isMultiEntryMatch(
// Added `JSON.stringify` as was having problems with
// `JSON.stringify` encoding added to nested
// array keys
JSON.stringify(encodedKey).slice(1, -1),
row[escapedIndexNameForKeyCol]
)
)) {
recordCount++;
record = row;
} else if (!hasKey && !multiChecks) {
if (rowKey !== undefined) {
recordCount += (Array.isArray(rowKey) ? rowKey.length : 1);
record = row;
}
}
if (record) {
records.push(decode(record));
if (unboundedDisallowed) {
break;
}
}
}
} else {
for (let i = 0; i < data.rows.length; i++) {
const record = data.rows.item(i);
if (record) {
records.push(decode(record));
}
}
recordCount = records.length;
}
if (isCount) {
success(recordCount);
} else if (recordCount === 0) {
success(unboundedDisallowed ? undefined : []);
} else {
success(unboundedDisallowed ? records[0] : records);
}
}, /** @type {SQLStatementErrorCallback} */ (error));
}
/**
* @param {boolean} nullDisallowed
* @param {IDBIndexFull} index
* @param {import('./Key.js').Value|import('./Key.js').Key} range
* @param {"value"|"key"|"count"} opType
* @param {boolean} multiChecks
* @returns {[
* nullDisallowed: boolean,
* index: IDBIndexFull,
* hasRange: boolean,
* range: import('./Key.js').Value|import('./Key.js').Key,
* opType: "value"|"key"|"count",
* multiChecks: boolean,
* sql: string[],
* sqlValues: string[]
* ]}
*/
function buildFetchIndexDataSQL (
nullDisallowed, index, range, opType, multiChecks
) {
const hasRange = nullDisallowed || !util.isNullish(range);
const col = opType === 'count' ? 'key' : opType; // It doesn't matter which column we use for 'count' as long as it is valid
const sql = [
'SELECT', util.sqlQuote(col) + (
index.multiEntry ? ', ' + util.escapeIndexNameForSQL(index.name) : ''
),
'FROM', util.escapeStoreNameForSQL(index.objectStore.__currentName),
'WHERE', util.escapeIndexNameForSQL(index.name), 'NOT NULL'
];
/** @type {string[]} */
const sqlValues = [];
if (hasRange) {
if (multiChecks) {
sql.push('AND (');
/** @type {import('./Key.js').KeyPathArray} */ (
range
).forEach((innerKey, i) => {
if (i > 0) { sql.push('OR'); }
sql.push(util.escapeIndexNameForSQL(index.name), "LIKE ? ESCAPE '^' ");
sqlValues.push('%' + util.sqlLIKEEscape(
/** @type {string} */ (Key.encode(innerKey, index.multiEntry))
) + '%');
});
sql.push(')');
} else if (index.multiEntry) {
sql.push('AND', util.escapeIndexNameForSQL(index.name), "LIKE ? ESCAPE '^'");
if (Array.isArray(range)) {
// Todo: For nesting deeper than one level, we probably need to
// run `JSON.stringify` again
sqlValues.push('%' + util.sqlLIKEEscape(
JSON.stringify(
/** @type {string} */ (Key.encode(range, index.multiEntry))
).slice(1, -1)
) + '%');
} else {
sqlValues.push('%' + util.sqlLIKEEscape(
/** @type {string} */ (Key.encode(range, index.multiEntry))
) + '%');
}
} else {
const convertedRange = convertValueToKeyRange(range, nullDisallowed);
setSQLForKeyRange(convertedRange, util.escapeIndexNameForSQL(index.name), sql, sqlValues, true, false);
}
}
return [nullDisallowed, index, hasRange, range, opType, multiChecks, sql, sqlValues];
}
// eslint-disable-next-line unicorn/no-named-default -- Had some reason
export {buildFetchIndexDataSQL, executeFetchIndexData, IDBIndex, IDBIndex as default};