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