Home Reference Source

lib/IndexedFTS.js

  1. import IFTSTransaction from './Transaction';
  2. import IFTSSchema from './Schema';
  3.  
  4.  
  5. /**
  6. * The database of IndexedFTS.
  7. *
  8. * Almost methods are the same interface as {@link IDBTransaction} and {@link IFTSArrayPromise}.
  9. */
  10. export default class IndexedFTS {
  11. /**
  12. * Create or open IndexedFTS.
  13. *
  14. * Database has name and schema's version.
  15. * The name is a name of the database in the storage.
  16. *
  17. * 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.
  18. * If you want change schema of database, please change version number.
  19. * Please be careful, all contents will remove when changing the version number.
  20. *
  21. * Index types are 'primary', 'unique', 'fulltext', 'ngram', 'word', or 'normal'.
  22. *
  23. * 'primary' is a primary key of the database. 'primary' can't set to multiple columns.
  24. * 'unique' is columns that have a unique value in the database.
  25. * The 'normal' will enable when not primary and not unique.
  26. * 'primary', 'unique' and 'normal' column can numeric search (eg. {@link IndexedFTS#lower} or {@link IndexedFTS#between}).
  27. *
  28. * If set 'ngram' IndexedFTS will make 2-gram index table for full-text search.
  29. * 'fulltext' is alias to 'ngram'.
  30. *
  31. * 'word' is word based index.
  32. * The word index will split text with whitespaces and store those.
  33. * Word index is faster than the 'ngram' index but can't find a partial match in the word.
  34. *
  35. * If you want to set some index types, please use object like `{unique: true, fulltext: true, normal: false}`.
  36. *
  37. * @param {string} name - name of new (or open) database.
  38. * @param {number} version - schema's version of database.
  39. * @param {object|IFTSSchema} schema - database schema.
  40. * @param {object} [options] - other options.
  41. * @param {string} [options.index_prefix='indexedfts_'] - prefix of indexes for full-text search.
  42. * @param {object} [options.scope=window] - endpoints for IndexedDB API.
  43. *
  44. * @throws {InvalidSchemaError}
  45. */
  46. constructor(name, version, schema, options={}) {
  47. /** @type {string} */
  48. this.index_prefix = options.index_prefix || 'indexedfts_';
  49.  
  50. /** @type {object} */
  51. this.scope = options.scope || window;
  52.  
  53. /** @type {string} */
  54. this.name = name;
  55.  
  56. /** @type {number} */
  57. this.version = version;
  58.  
  59. /** @type {IFTSSchema} */
  60. this.schema = schema instanceof IFTSSchema ? schema : new IFTSSchema(schema);
  61.  
  62.  
  63. /** @type {IDBDatabase} */
  64. this.db = null;
  65. }
  66.  
  67. /**
  68. * Delete database.
  69. *
  70. * Must be close all IndexedFTS before delete database.
  71. *
  72. * @param {string} name - name of target database. this method will success even if no such database.
  73. * @param {object} [scope] - endpoints for IndexedDB API.
  74. *
  75. * @return {Promise<undefined>}
  76. */
  77. static delete(name, scope=null) {
  78. return new Promise((resolve, reject) => {
  79. const req = (scope || window).indexedDB.deleteDatabase(name);
  80. req.onsuccess = ev => resolve();
  81. req.onerror = ev => reject(ev);
  82. });
  83. }
  84.  
  85. /**
  86. * Open database.
  87. *
  88. * @return {Promise<undefined>}
  89. */
  90. open() {
  91. return new Promise((resolve, reject) => {
  92. const request = this.scope.indexedDB.open(this.name, this.version);
  93.  
  94. request.onsuccess = ev => {
  95. this.db = ev.target.result;
  96. resolve(this);
  97. };
  98. request.onerror = reject;
  99.  
  100. request.onupgradeneeded = ev => {
  101. this.db = ev.target.result;
  102.  
  103. const store = this.db.createObjectStore('data', this.schema._storeOption);
  104.  
  105. store.onerror = reject;
  106.  
  107. this.schema.uniqueIndexes.forEach(x => store.createIndex(x, x, {unique: true}));
  108.  
  109. this.schema.normalIndexes.forEach(x => store.createIndex(x, x, {unique: false}));
  110.  
  111. this.schema.ngramIndexes.forEach(column => {
  112. const fts_store = this.db.createObjectStore(this.index_prefix + 'ngram_' + column, {autoIncrement: true});
  113. fts_store.onerror = reject
  114. fts_store.createIndex('key', 'key', {unique: false});
  115. fts_store.createIndex('token', 'token', {unique: false});
  116. fts_store.createIndex('lower', 'lower', {unique: false});
  117. });
  118.  
  119. this.schema.wordIndexes.forEach(column => {
  120. const fts_store = this.db.createObjectStore(this.index_prefix + 'word_' + column, {autoIncrement: true});
  121. fts_store.onerror = reject
  122. fts_store.createIndex('key', 'key', {unique: false});
  123. fts_store.createIndex('word', 'word', {unique: false});
  124. fts_store.createIndex('lower', 'lower', {unique: false});
  125. });
  126. };
  127. });
  128. }
  129.  
  130. /**
  131. * Close database.
  132. */
  133. close() {
  134. this.db.close();
  135. }
  136.  
  137. /**
  138. * Make new {@link IFTSTransaction}.
  139. *
  140. * @param {"readonly"|"readwrite"} mode - mode of transaction.
  141. * @param {string[]|null} target - open index targets. open for all if null.
  142. *
  143. * @return {IFTSTransaction}
  144. */
  145. transaction(mode='readonly', target=null) {
  146. if (target === null) {
  147. const ngrams = [...this.schema.ngramIndexes].map(x => this.index_prefix + 'ngram_' + x);
  148. const words = [...this.schema.wordIndexes].map(x => this.index_prefix + 'word_' + x);
  149. target = ngrams.concat(words).concat(['data']);
  150. }
  151. return new IFTSTransaction(this, this.db.transaction(target, mode));
  152. }
  153.  
  154. /**
  155. * Put contents into database.
  156. *
  157. * @param {object} contents - contents for save. allowed multiple arguments.
  158. *
  159. * @return {Promise<IndexedFTS>} returns self for chain.
  160. */
  161. put(...contents) {
  162. return this.transaction('readwrite').put(...contents).then(() => this);
  163. }
  164.  
  165. /**
  166. * Delete contents from database.
  167. *
  168. * @param {object} keys - key of contents.
  169. *
  170. * @return {Promise<IndexedFTS>} returns self for chain. Will reject with {@link InvalidKeyError} if keys included null or undefined.
  171. */
  172. delete(...keys) {
  173. return this.transaction('readwrite').delete(...keys).then(() => this);
  174. }
  175.  
  176. /**
  177. * Get content by primary key.
  178. *
  179. * @param {object} key - the key of content.
  180. *
  181. * @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.
  182. */
  183. get(key) {
  184. return this.transaction('readonly', 'data').get(key);
  185. }
  186.  
  187. /**
  188. * Get filtered contents.
  189. *
  190. * @ignore
  191. */
  192. _getFiltered(fun) {
  193. return fun(this.transaction('readonly', 'data'));
  194. }
  195.  
  196. /**
  197. * Get all contents.
  198. *
  199. * @return {IFTSArrayPromise} contents.
  200. */
  201. getAll() {
  202. return this._getFiltered(x => x.getAll());
  203. }
  204.  
  205. /**
  206. * Do something process for each elements and returns {@link IFTSArrayPromise}.
  207. *
  208. * NOTE: This method doesn't fast. May better do filtering before doing map if need filtering.
  209. *
  210. * @param {function(content: object, index: Number): object} fun - function for processing element.
  211. *
  212. * @return {IFTSArrayPromise}
  213. */
  214. map(fun) {
  215. return this._getFiltered(x => x.map(fun));
  216. }
  217.  
  218. /**
  219. * Filtering elements by function and returns {@link IFTSArrayPromise}.
  220. *
  221. * WARNING: This method won't use the index. Other methods(eg. {@link IFTSTransaction#equals or @link IFTSTransaction#lower} may faster than this.
  222. *
  223. * @param {function(content: object, index: Number): object} fun - function for filtering element.
  224. *
  225. * @return {IFTSArrayPromise}
  226. */
  227. filter(fun) {
  228. return this._getFiltered(x => x.filter(fun));
  229. }
  230.  
  231. /**
  232. * Sort and get all contents.
  233. *
  234. * @param {object} column - the column for sorting.
  235. * @param {'asc'|'desc'} [order='asc'] - sort order.
  236. * @param {Number} [offset=0] - starting offset of the result.
  237. * @param {Number} [limit] - maximum number of result length. will unlimited if omitted.
  238. *
  239. * @return {IFTSArrayPromise} sorted contents.
  240. */
  241. sort(column, order='asc', offset=0, limit=undefined) {
  242. return this._getFiltered(x => x.sort(column, order, offset, limit));
  243. }
  244.  
  245. /**
  246. * Get contents that have fully matched property.
  247. *
  248. * @param {object} column - column name for search.
  249. * @param {object} value - value for search.
  250. *
  251. * @return {IFTSArrayPromise} matched contents. may reject with {@link NoSuchColumnError}.
  252. */
  253. equals(column, value) {
  254. return this._getFiltered(x => x.equals(column, value));
  255. }
  256.  
  257. /**
  258. * Get contents that have property lower than value.
  259. *
  260. * @param {object} column - column name for search.
  261. * @param {object} value - value for search.
  262. *
  263. * @return {IFTSArrayPromise} matched contents. may reject with {@link NoSuchColumnError}.
  264. */
  265. lower(column, value) {
  266. return this._getFiltered(x => x.lower(column, value));
  267. }
  268.  
  269. /**
  270. * Get contents that have property greater than value.
  271. *
  272. * @param {object} column - column name for search.
  273. * @param {object} value - value for search.
  274. *
  275. * @return {IFTSArrayPromise} matched contents. may reject with {@link NoSuchColumnError}.
  276. */
  277. greater(column, value) {
  278. return this._getFiltered(x => x.greater(column, value));
  279. }
  280.  
  281. /**
  282. * Get contents that have property lower than value or equals value.
  283. *
  284. * @param {object} column - column name for search.
  285. * @param {object} value - value for search.
  286. *
  287. * @return {IFTSArrayPromise} matched contents. may reject with {@link NoSuchColumnError}.
  288. */
  289. lowerOrEquals(column, value) {
  290. return this._getFiltered(x => x.lowerOrEquals(column, value));
  291. }
  292.  
  293. /**
  294. * Get contents that have property greater than value or equals value.
  295. *
  296. * @param {object} column - column name for search.
  297. * @param {object} value - value for search.
  298. *
  299. * @return {IFTSArrayPromise} matched contents. may reject with {@link NoSuchColumnError}.
  300. */
  301. greaterOrEquals(column, value) {
  302. return this._getFiltered(x => x.greaterOrEquals(column, value));
  303. }
  304.  
  305. /**
  306. * Get contents that have property is between argument values.
  307. *
  308. * @param {object} column - column name for search.
  309. * @param {object} lower - minimal value.
  310. * @param {object} upper - maximum value.
  311. *
  312. * @return {IFTSArrayPromise} matched contents. may reject with {@link NoSuchColumnError}.
  313. */
  314. between(column, lower, upper) {
  315. return this._getFiltered(x => x.between(column, lower, upper));
  316. }
  317.  
  318. /**
  319. * Get contents that have matched property by full-text search.
  320. *
  321. * All target columns have to made ngram index when created database.
  322. * If you didn't made ngram index, you can use {@link IFTSArrayPromise#search} (but this way is very slow).
  323. *
  324. *
  325. * @param {object|object[]} columns - column names for search.
  326. * @param {string} query - query for search.
  327. * @param {object} [options] - optional arguments.
  328. * @param {boolean} [options.ignoreCase=false] - ignore case if true. default is false.
  329. *
  330. * @return {IFTSArrayPromise} matched contents. may reject with {@link NoSuchColumnError}.
  331. */
  332. search(columns, query, options={}) {
  333. return this.transaction().search(columns, query, options);
  334. }
  335.  
  336. /**
  337. * Find contents that have fully matched word in property.
  338. *
  339. * All target columns have to made word index when created database.
  340. * If you didn't made word index, you can use {@link IFTSArrayPromise#searchWord} (but this way is very slow).
  341. *
  342. *
  343. * @param {object|object[]} columns - column names for search.
  344. * @param {string} query - query for search.
  345. * @param {object} [options] - optional arguments.
  346. * @param {boolean} [options.ignoreCase=false] - ignore case if true. default is false.
  347. *
  348. * @return {IFTSArrayPromise} matched contents. may reject with {@link NoSuchColumnError}.
  349. */
  350. searchWord(columns, query, options={}) {
  351. return this.transaction().searchWord(columns, query, options);
  352. }
  353.  
  354. /**
  355. * Get N-Gram set from index.
  356. *
  357. * @param {string} column - name of column.
  358. * @param {object} [options] - optional arguments.
  359. * @param {boolean} [options.ignoreCase=false] - ignore case when make result.
  360. *
  361. * @return {Promise<Map<string, number>>}
  362. */
  363. getNGrams(column, options={}) {
  364. return this.transaction().getNGrams(column, options);
  365. }
  366.  
  367. /**
  368. * Get word set from index.
  369. *
  370. * @param {string} column - name of column.
  371. * @param {object} [options] - optional arguments.
  372. * @param {boolean} [options.ignoreCase=false] - ignore case when make result.
  373. *
  374. * @return {Promise<Map<string, number>>}
  375. */
  376. getWords(column, options={}) {
  377. return this.transaction().getWords(column, options);
  378. }
  379. }