• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1import { NativeEventEmitter, NativeModules } from 'react-native';
2
3const { EJDB2DB: ejdb2, EJDB2JQL: ejdb2jql } = NativeModules;
4
5/**
6 * EJDB2 Error helpers.
7 */
8class JBE {
9  /**
10   * Returns `true` if given error [err] is `IWKV_ERROR_NOTFOUND`
11   * @param {Error} err
12   * @returns {boolean}
13   */
14  static isNotFound(err) {
15    const msg = (err.message || '').toString();
16    return msg.indexOf('@ejdb IWRC:75001') != -1;
17  }
18
19  static isInvalidQuery(err) {
20    const msg = (err.message || '').toString();
21    return msg.indexOf('@ejdb IWRC:87001') != -1;
22  }
23}
24
25/**
26 * EJDB document.
27 */
28class JBDOC {
29  /**
30   * Get document JSON object
31   */
32  get json() {
33    if (this._json != null) {
34      return this._json;
35    }
36    this._json = JSON.parse(this._raw);
37    this._raw = null;
38    return this._json;
39  }
40
41  /**
42   * @param {number} id Document ID
43   * @param {string} raw Document JSON as string
44   */
45  constructor(id, raw) {
46    this.id = id;
47    this._raw = raw;
48    this._json = null;
49  }
50
51  toString() {
52    return `JBDOC: ${this.id} ${this._raw != null ? this._raw : JSON.stringify(this.json)}`;
53  }
54}
55
56/**
57 * EJDB Query.
58 */
59class JQL {
60  /**
61   * @param db {EJDB2}
62   * @param jql
63   */
64  constructor(db, jql) {
65    this._db = db;
66    this._jql = jql;
67    this._nee = new NativeEventEmitter(ejdb2jql);
68    this._eid = 0;
69    this._explain = null;
70    this._withExplain = false;
71  }
72
73  /**
74   * Get database instance associated with this query.
75   * @returns {EJDB2}
76   */
77  get db() {
78    return this._db;
79  }
80
81  /**
82   * Get query text
83   * @return {string}
84   */
85  get query() {
86    return ejdb2jql.getQuery(this._jql);
87  }
88
89  /**
90   * Get documents collection name used in query.
91   * @return {string}
92   */
93  get collection() {
94    return ejdb2jql.getCollection(this._jql);
95  }
96
97  /**
98   * Set collection to use with query.
99   * Value overrides collection encoded in query.
100   * @param coll
101   * @return {JQL}
102   */
103  withCollection(coll) {
104    ejdb2jql.setCollection(this._jql, coll);
105    return this;
106  }
107
108  /**
109   * Retrieve query execution plan.
110   * @return {string|null}
111   */
112  get explainLog() {
113    return this._explainLog || ejdb2jql.getExplainLog(this._jql);
114  }
115
116  /**
117   * Get number of documents to skip.
118   * @return {number}
119   */
120  get skip() {
121    return parseInt(ejdb2jql.getSkip(this._jql));
122  }
123
124  /**
125   * Set number of documents to skip.
126   * Specified value overrides `skip` option encoded in query.
127   * @param {number} skip
128   * @return {JQL}
129   */
130  withSkip(skip) {
131    if (!this._isInteger(skip)) {
132      throw new Error('`skip` argument must an Integer');
133    }
134    ejdb2jql.setSkip(this._jql, skip.toString());
135    return this;
136  }
137
138  /**
139   * Get maximum number of documents to fetch.
140   * @return {number}
141   */
142  get limit() {
143    return parseInt(ejdb2jql.getLimit(this._jql));
144  }
145
146  /**
147   * Set maximum number of documents to fetch.
148   * Specified value overrides `limit` option encoded in query.
149   * @param {number} limit Limit
150   * @return {JQL}
151   */
152  withLimit(limit) {
153    if (!this._isInteger(limit)) {
154      new Error('`limit` argument must an Integer');
155    }
156    ejdb2jql.setLimit(this._jql, limit.toString());
157    return this;
158  }
159
160  /**
161   * Turn on explain query mode
162   * @return {JQL}
163   */
164  withExplain() {
165    ejdb2jql.withExplain(this._jql);
166    this._withExplain = true;
167    this._explainLog = null;
168    return this;
169  }
170
171  /**
172   * Set string [val] at the specified [placeholder].
173   * @param {string|number} placeholder
174   * @param {string} val
175   * @return {JQL}
176   */
177  setString(placeholder, val) {
178    if (val != null && typeof val !== 'string') {
179      val = val.toString();
180    }
181    this._p('setString', placeholder)(this._jql, placeholder, val);
182    return this;
183  }
184
185  /**
186   * Set [json] at the specified [placeholder].
187   * @param {string|number} placeholder
188   * @param {string|object} val
189   * @return {JQL}
190   */
191  setJSON(placeholder, val) {
192    if (typeof val !== 'string') {
193      val = JSON.stringify(val);
194    }
195    this._p('setJSON', placeholder)(this._jql, placeholder, val);
196    return this;
197  }
198
199  /**
200   * Set [regexp] string at the specified [placeholder].
201   * @param {string|number} placeholder
202   * @param {string|RegExp} val
203   * @return {JQL}
204   */
205  setRegexp(placeholder, val) {
206    if (val instanceof RegExp) {
207      const sval = val.toString();
208      val = sval.substring(1, sval.lastIndexOf('/'));
209    } else if (typeof val !== 'string') {
210      throw new Error('Regexp argument must be a string or RegExp object');
211    }
212    this._p('setRegexp', placeholder)(this._jql, placeholder, val);
213    return this;
214  }
215
216  /**
217   * Set number [val] at the specified [placeholder].
218   * @param {string|number} placeholder
219   * @param {number} val
220   * @returns {JQL}
221   */
222  setNumber(placeholder, val) {
223    if (typeof val !== 'number') {
224      throw new Error('Value must be a number');
225    }
226    if (this._isInteger(val)) {
227      this._p('setLong', placeholder)(this._jql, placeholder, val.toString());
228    } else {
229      this._p('setDouble', placeholder)(this._jql, placeholder, val);
230    }
231    return this;
232  }
233
234  /**
235   * Set boolean [val] at the specified [placeholder].
236   * @param {string|number} placeholder
237   * @param {boolean} val
238   * @return {JQL}
239   */
240  setBoolean(placeholder, val) {
241    this._p('setBoolean', placeholder)(this._jql, placeholder, !!val);
242    return this;
243  }
244
245  /**
246   * Set `null` at the specified [placeholder].
247   * @param {string|number} placeholder
248   * @return {JQL}
249   */
250  setNull(placeholder) {
251    this._p('setNull', placeholder)(this._jql, placeholder);
252    return this;
253  }
254
255  /**
256   * @callback JQLExecuteCallback
257   * @param {JBDOC} doc
258   * @param {JQL} jql
259   */
260  /**
261   * Execute this query.
262   *
263   * @param {boolean} dispose Dispose this query after execution
264   * @param {JQLExecuteCallback} callback
265   * @return {Promise<void>}
266   */
267  execute(dispose, callback) {
268    const eid = this._nextEventId();
269    const reg = this._nee.addListener(eid, data => {
270      data.forEach(row => {
271        if (row != null) {
272          callback && callback(new JBDOC(row.id, row.raw), this);
273        }
274      });
275      if (data.length && data[data.length - 1] == null) {
276        // EOF last element is null
277        reg.remove();
278      }
279    });
280    this._explainLog = null;
281    return ejdb2jql
282      .execute(this._jql, eid)
283      .catch(err => {
284        reg.remove();
285        return err;
286      })
287      .then(err => {
288        if (this._withExplain) {
289          // Save explain log before close
290          this._explainLog = ejdb2jql.getExplainLog(this._jql);
291        }
292        if (dispose) {
293          this.close();
294        }
295        if (err) {
296          return Promise.reject(err);
297        }
298      });
299  }
300
301  /**
302   * Execute this query then dispose it.
303   *
304   * @param {JQLExecuteCallback?} callback
305   * @return {Promise<void>}
306   */
307  useExecute(callback) {
308    return this.execute(true, callback);
309  }
310
311  /**
312   * Uses query in `callback` then closes it.
313   * @param {UseQueryCallback} callback Function accepts query object
314   * @return {Promise<*>}
315   */
316  async use(callback) {
317    let ret;
318    try {
319      this._explainLog = null;
320      ret = await callback(this);
321    } finally {
322      if (this._withExplain) {
323        // Save explain log before close
324        this._explainLog = ejdb2jql.getExplainLog(this._jql);
325      }
326      this.close();
327    }
328    return ret;
329  }
330
331  /**
332   *
333   * @param {object?} opts Optional options object.
334   *        - `limit` Override maximum number of documents in result set
335   *        - `skip` Override skip number of documents to skip
336   * @return {Promise<JBDOC[]>}
337   */
338  list(opts_ = {}) {
339    const opts = { ...opts_ };
340    if (opts.limit != null && typeof opts.limit !== 'string') {
341      opts.limit = `${opts.limit}`;
342    }
343    if (opts.skip != null && typeof opts.skip !== 'string') {
344      opts.skip = `${opts.skip}`;
345    }
346    return ejdb2jql.list(this._jql, opts).then(l => l.map(data => new JBDOC(data.id, data.raw)));
347  }
348
349  /**
350   * Collects up to [n] documents from result set into array.
351   * @param {number|string} n Upper limit of documents in result set
352   * @return {Promise<JBDOC[]>}
353   */
354  firstN(n = 1) {
355    return this.list({ limit: n });
356  }
357
358  /**
359   * Returns a first record in result set.
360   * If no reconds found `null` resolved promise will be returned.
361   * @returns {Promise<JBDOC|null>}
362   */
363  first() {
364    return ejdb2jql.first(this._jql).then(data => (data ? new JBDOC(data.id, data.raw) : null));
365  }
366
367  /**
368   * Returns a scalar integer value as result of query execution.
369   * Eg.: A count query: `/... | count`
370   * @return {Promise<number>}
371   */
372  scalarInt() {
373    return ejdb2jql.executeScalarInt(this._jql).then(r => parseInt(r));
374  }
375
376  /**
377   * Reset query parameters.
378   * @returns {Promise<JQL>}
379   */
380  reset() {
381    return ejdb2jql.reset(this._jql).then(_ => this);
382  }
383
384  /**
385   * Disposes JQL instance and releases all underlying resources.
386   */
387  close() {
388    return ejdb2jql.close(this._jql);
389  }
390
391  /**
392   * @private
393   */
394  _isInteger(n) {
395    return n === +n && n === (n | 0);
396  }
397
398  /**
399   * @private
400   */
401  _p(name, placeholder) {
402    const t = typeof placeholder;
403    if (!(t === 'number' || t === 'string')) {
404      throw new Error('Invalid placeholder specified, must be either string or number');
405    }
406    return ejdb2jql[(this._isInteger(placeholder) ? 'p' : '') + name];
407  }
408
409  /**
410   * Generate next event id.
411   * @private
412   * @return {string}
413   */
414  _nextEventId() {
415    return `jql${this._eid++}`;
416  }
417}
418
419/**
420 * EJDB2 React Native wrapper.
421 */
422class EJDB2 {
423  /**
424   * Open database instance.
425   *
426   * @param {string} path Path to database
427   * @param {Object} [opts]
428   * @return {Promise<EJDB2>} EJDB2 instance promise
429   */
430  static open(path, opts = {}) {
431    return ejdb2.open(path, opts).then(db => new EJDB2(db));
432  }
433
434  constructor(db) {
435    this._db = db;
436  }
437
438  /**
439   * Closes database instance.
440   * @return {Promise<void>}
441   */
442  close() {
443    return ejdb2.close(this._db);
444  }
445
446  /**
447   * Get json body with database metadata.
448   *
449   * @return {Promise<object>}
450   */
451  info() {
452    return ejdb2.info(this._db).then(JSON.parse);
453  }
454
455  /**
456   * @callback UseQueryCallback
457   * @param {JQL} query
458   * @return {Promise<*>}
459   */
460
461  /**
462   * Create instance of [query] specified for [collection].
463   * If [collection] is not specified a [query] spec must contain collection name,
464   * eg: `@mycollection/[foo=bar]`
465   *
466   * @note WARNING In order to avoid memory leaks, dispose created query object by {@link JQL#close}
467   *       or use {@link JQL#use}
468   *
469   * @param {string} query Query text
470   * @param {string} [collection]
471   * @returns {JQL}
472   */
473  createQuery(query, collection) {
474    const qh = ejdb2.createQuery(this._db, query, collection);
475    return new JQL(this, qh);
476  }
477
478  /**
479   * Saves [json] document under specified [id] or create a document
480   * with new generated `id`. Returns promise holding actual document `id`.
481   *
482   * @param {string} collection
483   * @param {Object|string} json
484   * @param {number} [id]
485   * @returns {Promise<number>}
486   */
487  put(collection, json, id = 0) {
488    json = typeof json === 'string' ? json : JSON.stringify(json);
489    return ejdb2.put(this._db, collection, json, id).then(v => parseInt(v));
490  }
491
492  /**
493   * Apply rfc6902/rfc7386 JSON [patch] to the document identified by [id].
494   *
495   * @param {string} collection
496   * @param {Object|string} json
497   * @param {number} id
498   * @return {Promise<void>}
499   */
500  patch(collection, json, id) {
501    json = typeof json === 'string' ? json : JSON.stringify(json);
502    return ejdb2.patch(this._db, collection, json, id);
503  }
504
505  /**
506   * Apply JSON merge patch (rfc7396) to the document identified by `id` or
507   * insert new document under specified `id`.
508   *
509   * @param {String} collection
510   * @param {Object|string} json
511   * @param {number} id
512   * @return {Promise<void>}
513   */
514  patchOrPut(collection, json, id) {
515    json = typeof json === 'string' ? json : JSON.stringify(json);
516    return ejdb2.patchOrPut(this._db, collection, json, id);
517  }
518
519  /**
520   * Get json body of document identified by [id] and stored in [collection].
521   *
522   * @param {string} collection
523   * @param {number} id
524   * @return {Promise<object>} JSON object
525   */
526  get(collection, id) {
527    return ejdb2.get(this._db, collection, id).then(JSON.parse);
528  }
529
530  /**
531   * Get json body of document identified by [id] and stored in [collection].
532   * If document with given `id` is not found then `null` will be resoved.
533   *
534   * @param {string} collection
535   * @param {number} id
536   * @return {Promise<object|null>} JSON object
537   */
538  getOrNull(collection, id) {
539    return this.get(collection, id).catch((err) => {
540      if (JBE.isNotFound(err)) {
541        return null;
542      } else {
543        return Promise.reject(err);
544      }
545    });
546  }
547
548  /**
549   * Removes document idenfied by [id] from [collection].
550   *
551   * @param {string} collection
552   * @param {number} id
553   * @return {Promise<void>}
554   */
555  del(collection, id) {
556    return ejdb2.del(this._db, collection, id);
557  }
558
559  /**
560   * Renames collection.
561   *
562   * @param {string} oldCollectionName Collection to be renamed
563   * @param {string} newCollectionName New name of collection
564   * @return {Promise<void>}
565   */
566  renameCollection(oldCollectionName, newCollectionName) {
567    return ejdb2.renameCollection(this._db, oldCollectionName, newCollectionName);
568  }
569
570  /**
571   * Removes database [collection].
572   *
573   * @param {string} collection
574   * @return {Promise<void>}
575   */
576  removeCollection(collection) {
577    return ejdb2.removeCollection(this._db, collection).then(_ => this);
578  }
579
580  /**
581   * Ensures json document database index specified by [path] json pointer to string data type.
582   *
583   * @param {string} collection
584   * @param {string} path
585   * @param {boolean} [unique=false]
586   * @return {Promise<void>}
587   */
588  ensureStringIndex(collection, path, unique) {
589    return ejdb2.ensureStringIndex(this._db, collection, path, unique).then(_ => this);
590  }
591
592  /**
593   * Removes specified database index.
594   *
595   * @param {string} collection
596   * @param {string} path
597   * @param {boolean} [unique=false]
598   * @return {Promise<void>}
599   */
600  removeStringIndex(collection, path, unique) {
601    return ejdb2.removeStringIndex(this._db, collection, path, unique).then(_ => this);
602  }
603
604  /**
605   * Ensures json document database index specified by [path] json pointer to integer data type.
606   *
607   * @param {string} collection
608   * @param {string} path
609   * @param {boolean} [unique=false]
610   * @return {Promise<void>}
611   */
612  ensureIntIndex(collection, path, unique) {
613    return ejdb2.ensureIntIndex(this._db, collection, path, unique).then(_ => this);
614  }
615
616  /**
617   * Removes specified database index.
618   *
619   * @param {string} collection
620   * @param {string} path
621   * @param {boolean} [unique=false]
622   * @return {Promise<void>}
623   */
624  removeIntIndex(collection, path, unique) {
625    return ejdb2.removeIntIndex(this._db, collection, path, unique).then(_ => this);
626  }
627
628  /**
629   * Ensures json document database index specified by [path] json pointer to floating point data type.
630   *
631   * @param {string} collection
632   * @param {string} path
633   * @param {boolean} [unique=false]
634   * @return {Promise<void>}
635   */
636  ensureFloatIndex(collection, path, unique) {
637    return ejdb2.ensureFloatIndex(this._db, collection, path, unique).then(_ => this);
638  }
639
640  /**
641   * Removes specified database index.
642   *
643   * @param {string} collection
644   * @param {string} path
645   * @param {boolean} [unique=false]
646   * @return {Promise<void>}
647   */
648  removeFloatIndex(collection, path, unique) {
649    return ejdb2.removeFloatIndex(this._db, collection, path, unique).then(_ => this);
650  }
651
652  /**
653   * Creates an online database backup image and copies it into the specified [fileName].
654   * During online backup phase read/write database operations are allowed and not
655   * blocked for significant amount of time. Returns promise with backup
656   * finish time as number of milliseconds since epoch.
657   *
658   * @param {string} fileName Backup image file path.
659   * @returns {Promise<number>}
660   */
661  onlineBackup(fileName) {
662    return ejdb2.onlineBackup(this._db, fileName);
663  }
664}
665
666module.exports = {
667  EJDB2,
668  JBE
669};
670