lib/IndexedFTS.js
import IFTSTransaction from './Transaction';
import IFTSSchema from './Schema';
/**
* The database of IndexedFTS.
*
* Almost methods are the same interface as {@link IDBTransaction} and {@link IFTSArrayPromise}.
*/
export default class IndexedFTS {
/**
* Create or open IndexedFTS.
*
* Database has name and schema's version.
* The name is a name of the database in the storage.
*
* The schema is an object that key is column name and value is a definition of indexes. Schema can't change in same version database.
* If you want change schema of database, please change version number.
* Please be careful, all contents will remove when changing the version number.
*
* Index types are 'primary', 'unique', 'fulltext', 'ngram', 'word', or 'normal'.
*
* 'primary' is a primary key of the database. 'primary' can't set to multiple columns.
* 'unique' is columns that have a unique value in the database.
* The 'normal' will enable when not primary and not unique.
* 'primary', 'unique' and 'normal' column can numeric search (eg. {@link IndexedFTS#lower} or {@link IndexedFTS#between}).
*
* If set 'ngram' IndexedFTS will make 2-gram index table for full-text search.
* 'fulltext' is alias to 'ngram'.
*
* 'word' is word based index.
* The word index will split text with whitespaces and store those.
* Word index is faster than the 'ngram' index but can't find a partial match in the word.
*
* If you want to set some index types, please use object like `{unique: true, fulltext: true, normal: false}`.
*
* @param {string} name - name of new (or open) database.
* @param {number} version - schema's version of database.
* @param {object|IFTSSchema} schema - database schema.
* @param {object} [options] - other options.
* @param {string} [options.index_prefix='indexedfts_'] - prefix of indexes for full-text search.
* @param {object} [options.scope=window] - endpoints for IndexedDB API.
*
* @throws {InvalidSchemaError}
*/
constructor(name, version, schema, options={}) {
/** @type {string} */
this.index_prefix = options.index_prefix || 'indexedfts_';
/** @type {object} */
this.scope = options.scope || window;
/** @type {string} */
this.name = name;
/** @type {number} */
this.version = version;
/** @type {IFTSSchema} */
this.schema = schema instanceof IFTSSchema ? schema : new IFTSSchema(schema);
/** @type {IDBDatabase} */
this.db = null;
}
/**
* Delete database.
*
* Must be close all IndexedFTS before delete database.
*
* @param {string} name - name of target database. this method will success even if no such database.
* @param {object} [scope] - endpoints for IndexedDB API.
*
* @return {Promise<undefined>}
*/
static delete(name, scope=null) {
return new Promise((resolve, reject) => {
const req = (scope || window).indexedDB.deleteDatabase(name);
req.onsuccess = ev => resolve();
req.onerror = ev => reject(ev);
});
}
/**
* Open database.
*
* @return {Promise<undefined>}
*/
open() {
return new Promise((resolve, reject) => {
const request = this.scope.indexedDB.open(this.name, this.version);
request.onsuccess = ev => {
this.db = ev.target.result;
resolve(this);
};
request.onerror = reject;
request.onupgradeneeded = ev => {
this.db = ev.target.result;
const store = this.db.createObjectStore('data', this.schema._storeOption);
store.onerror = reject;
this.schema.uniqueIndexes.forEach(x => store.createIndex(x, x, {unique: true}));
this.schema.normalIndexes.forEach(x => store.createIndex(x, x, {unique: false}));
this.schema.ngramIndexes.forEach(column => {
const fts_store = this.db.createObjectStore(this.index_prefix + 'ngram_' + column, {autoIncrement: true});
fts_store.onerror = reject
fts_store.createIndex('key', 'key', {unique: false});
fts_store.createIndex('token', 'token', {unique: false});
fts_store.createIndex('lower', 'lower', {unique: false});
});
this.schema.wordIndexes.forEach(column => {
const fts_store = this.db.createObjectStore(this.index_prefix + 'word_' + column, {autoIncrement: true});
fts_store.onerror = reject
fts_store.createIndex('key', 'key', {unique: false});
fts_store.createIndex('word', 'word', {unique: false});
fts_store.createIndex('lower', 'lower', {unique: false});
});
};
});
}
/**
* Close database.
*/
close() {
this.db.close();
}
/**
* Make new {@link IFTSTransaction}.
*
* @param {"readonly"|"readwrite"} mode - mode of transaction.
* @param {string[]|null} target - open index targets. open for all if null.
*
* @return {IFTSTransaction}
*/
transaction(mode='readonly', target=null) {
if (target === null) {
const ngrams = [...this.schema.ngramIndexes].map(x => this.index_prefix + 'ngram_' + x);
const words = [...this.schema.wordIndexes].map(x => this.index_prefix + 'word_' + x);
target = ngrams.concat(words).concat(['data']);
}
return new IFTSTransaction(this, this.db.transaction(target, mode));
}
/**
* Put contents into database.
*
* @param {object} contents - contents for save. allowed multiple arguments.
*
* @return {Promise<IndexedFTS>} returns self for chain.
*/
put(...contents) {
return this.transaction('readwrite').put(...contents).then(() => this);
}
/**
* Delete contents from database.
*
* @param {object} keys - key of contents.
*
* @return {Promise<IndexedFTS>} returns self for chain. Will reject with {@link InvalidKeyError} if keys included null or undefined.
*/
delete(...keys) {
return this.transaction('readwrite').delete(...keys).then(() => this);
}
/**
* Get content by primary key.
*
* @param {object} key - the key of content.
*
* @return {Promise<object|undefined>} content. promise will reject with {@link InvalidKeyError} if keys included null or undefined. result value will be undefined if not found.
*/
get(key) {
return this.transaction('readonly', 'data').get(key);
}
/**
* Get filtered contents.
*
* @ignore
*/
_getFiltered(fun) {
return fun(this.transaction('readonly', 'data'));
}
/**
* Get all contents.
*
* @return {IFTSArrayPromise} contents.
*/
getAll() {
return this._getFiltered(x => x.getAll());
}
/**
* Do something process for each elements and returns {@link IFTSArrayPromise}.
*
* NOTE: This method doesn't fast. May better do filtering before doing map if need filtering.
*
* @param {function(content: object, index: Number): object} fun - function for processing element.
*
* @return {IFTSArrayPromise}
*/
map(fun) {
return this._getFiltered(x => x.map(fun));
}
/**
* Filtering elements by function and returns {@link IFTSArrayPromise}.
*
* WARNING: This method won't use the index. Other methods(eg. {@link IFTSTransaction#equals or @link IFTSTransaction#lower} may faster than this.
*
* @param {function(content: object, index: Number): object} fun - function for filtering element.
*
* @return {IFTSArrayPromise}
*/
filter(fun) {
return this._getFiltered(x => x.filter(fun));
}
/**
* Sort and get all contents.
*
* @param {object} column - the column for sorting.
* @param {'asc'|'desc'} [order='asc'] - sort order.
* @param {Number} [offset=0] - starting offset of the result.
* @param {Number} [limit] - maximum number of result length. will unlimited if omitted.
*
* @return {IFTSArrayPromise} sorted contents.
*/
sort(column, order='asc', offset=0, limit=undefined) {
return this._getFiltered(x => x.sort(column, order, offset, limit));
}
/**
* Get contents that have fully matched property.
*
* @param {object} column - column name for search.
* @param {object} value - value for search.
*
* @return {IFTSArrayPromise} matched contents. may reject with {@link NoSuchColumnError}.
*/
equals(column, value) {
return this._getFiltered(x => x.equals(column, value));
}
/**
* Get contents that have property lower than value.
*
* @param {object} column - column name for search.
* @param {object} value - value for search.
*
* @return {IFTSArrayPromise} matched contents. may reject with {@link NoSuchColumnError}.
*/
lower(column, value) {
return this._getFiltered(x => x.lower(column, value));
}
/**
* Get contents that have property greater than value.
*
* @param {object} column - column name for search.
* @param {object} value - value for search.
*
* @return {IFTSArrayPromise} matched contents. may reject with {@link NoSuchColumnError}.
*/
greater(column, value) {
return this._getFiltered(x => x.greater(column, value));
}
/**
* Get contents that have property lower than value or equals value.
*
* @param {object} column - column name for search.
* @param {object} value - value for search.
*
* @return {IFTSArrayPromise} matched contents. may reject with {@link NoSuchColumnError}.
*/
lowerOrEquals(column, value) {
return this._getFiltered(x => x.lowerOrEquals(column, value));
}
/**
* Get contents that have property greater than value or equals value.
*
* @param {object} column - column name for search.
* @param {object} value - value for search.
*
* @return {IFTSArrayPromise} matched contents. may reject with {@link NoSuchColumnError}.
*/
greaterOrEquals(column, value) {
return this._getFiltered(x => x.greaterOrEquals(column, value));
}
/**
* Get contents that have property is between argument values.
*
* @param {object} column - column name for search.
* @param {object} lower - minimal value.
* @param {object} upper - maximum value.
*
* @return {IFTSArrayPromise} matched contents. may reject with {@link NoSuchColumnError}.
*/
between(column, lower, upper) {
return this._getFiltered(x => x.between(column, lower, upper));
}
/**
* Get contents that have matched property by full-text search.
*
* All target columns have to made ngram index when created database.
* If you didn't made ngram index, you can use {@link IFTSArrayPromise#search} (but this way is very slow).
*
*
* @param {object|object[]} columns - column names for search.
* @param {string} query - query for search.
* @param {object} [options] - optional arguments.
* @param {boolean} [options.ignoreCase=false] - ignore case if true. default is false.
*
* @return {IFTSArrayPromise} matched contents. may reject with {@link NoSuchColumnError}.
*/
search(columns, query, options={}) {
return this.transaction().search(columns, query, options);
}
/**
* Find contents that have fully matched word in property.
*
* All target columns have to made word index when created database.
* If you didn't made word index, you can use {@link IFTSArrayPromise#searchWord} (but this way is very slow).
*
*
* @param {object|object[]} columns - column names for search.
* @param {string} query - query for search.
* @param {object} [options] - optional arguments.
* @param {boolean} [options.ignoreCase=false] - ignore case if true. default is false.
*
* @return {IFTSArrayPromise} matched contents. may reject with {@link NoSuchColumnError}.
*/
searchWord(columns, query, options={}) {
return this.transaction().searchWord(columns, query, options);
}
/**
* Get N-Gram set from index.
*
* @param {string} column - name of column.
* @param {object} [options] - optional arguments.
* @param {boolean} [options.ignoreCase=false] - ignore case when make result.
*
* @return {Promise<Map<string, number>>}
*/
getNGrams(column, options={}) {
return this.transaction().getNGrams(column, options);
}
/**
* Get word set from index.
*
* @param {string} column - name of column.
* @param {object} [options] - optional arguments.
* @param {boolean} [options.ignoreCase=false] - ignore case when make result.
*
* @return {Promise<Map<string, number>>}
*/
getWords(column, options={}) {
return this.transaction().getWords(column, options);
}
}