1/// 2/// EJDB2 Dart VM native API binding. 3/// 4/// See https://github.com/Softmotions/ejdb/blob/master/README.md 5/// 6/// For API usage examples look into `/example` and `/test` folders. 7/// 8 9library ejdb2_dart; 10 11import 'dart:async'; 12import 'dart:convert'; 13import 'dart:convert' as convert_lib; 14import 'dart:io'; 15import 'dart:isolate'; 16import 'dart:nativewrappers' show NativeFieldWrapperClass2; 17 18import 'package:path/path.dart' as path_lib; 19import 'package:quiver/core.dart'; 20import 'package:json_at/json_at.dart'; 21 22import 'dart-ext:ejdb2dart'; 23 24String ejdb2ExplainRC(int rc) native 'explain_rc'; 25 26/// EJDB specific exception 27class EJDB2Error implements Exception { 28 EJDB2Error(this.code, this.message); 29 30 EJDB2Error.fromCode(int code) : this(code, ejdb2ExplainRC(code)); 31 32 EJDB2Error.invalidState() : this.fromCode(EJD_ERROR_INVALID_STATE); 33 34 EJDB2Error.notFound() : this.fromCode(IWKV_ERROR_NOTFOUND); 35 36 static int EJD_ERROR_CREATE_PORT = 89001; 37 static int EJD_ERROR_POST_PORT = 89002; 38 static int EJD_ERROR_INVALID_NATIVE_CALL_ARGS = 89003; 39 static int EJD_ERROR_INVALID_STATE = 89004; 40 static int IWKV_ERROR_NOTFOUND = 75001; 41 42 final int code; 43 44 final String message; 45 46 bool get notFound => code == IWKV_ERROR_NOTFOUND; 47 48 bool get invalidQuery => code == 87001; 49 50 @override 51 String toString() => '$runtimeType: $code $message'; 52} 53 54/// EJDB document item 55class JBDOC { 56 JBDOC(this.id, this._json); 57 JBDOC._fromList(List list) : this(list[0] as int, list[1] as String?); 58 59 /// Document identifier 60 final int id; 61 62 /// Document body as JSON string 63 String get json => _json ?? convert_lib.jsonEncode(_object); 64 65 /// Document body as parsed JSON object. 66 dynamic get object { 67 if (_json == null) { 68 return _object; 69 } else { 70 _object = convert_lib.jsonDecode(_json!); 71 _json = null; // Release memory used to store JSON string data 72 return _object; 73 } 74 } 75 76 /// Gets subset of document using RFC 6901 JSON [pointer]. 77 Optional<dynamic> at(String pointer) => jsonAt(object, pointer); 78 79 /// Gets subset of document using RFC 6901 JSON [pointer]. 80 Optional<dynamic> operator [](String pointer) => at(pointer); 81 82 String? _json; 83 84 dynamic _object; 85 86 @override 87 String toString() => '$runtimeType: $id $json'; 88} 89 90/// Represents query on ejdb collection. 91/// Instance can be reused for multiple queries reusing 92/// placeholder parameters. 93class JQL extends NativeFieldWrapperClass2 { 94 JQL._(this.db, this.query, this.collection); 95 96 final String query; 97 final String collection; 98 final EJDB2 db; 99 100 StreamController<JBDOC>? _controller; 101 RawReceivePort? _replyPort; 102 103 /// Execute query and returns a stream of matched documents. 104 /// 105 /// [explainCallback] Used to get query execution log. 106 /// [limit] Overrides `limit` set by query text for this execution session. 107 /// 108 Stream<JBDOC> execute({void explainCallback(String log)?, int limit = 0}) { 109 abort(); 110 var execHandle = 0; 111 _controller = StreamController<JBDOC>(); 112 _replyPort = RawReceivePort(); 113 _replyPort!.handler = (dynamic reply) { 114 if (reply is int) { 115 _exec_check(execHandle, true); 116 _replyPort!.close(); 117 _controller!.addError(EJDB2Error.fromCode(reply)); 118 return; 119 } else if (reply is List) { 120 _exec_check(execHandle, false); 121 if (reply[2] != null && explainCallback != null) { 122 explainCallback(reply[2] as String); 123 } 124 _controller!.add(JBDOC._fromList(reply)); 125 } else { 126 _exec_check(execHandle, true); 127 if (reply != null && explainCallback != null) { 128 explainCallback(reply as String); 129 } 130 abort(); 131 } 132 }; 133 execHandle = _exec(_replyPort!.sendPort, explainCallback != null, limit); 134 return _controller!.stream; 135 } 136 137 /// Returns optional element for first record in result set. 138 Future<Optional<JBDOC>> first({void explainCallback(String log)?}) async { 139 await for (final doc in execute(explainCallback: explainCallback, limit: 1)) { 140 return Optional.of(doc); 141 } 142 return const Optional.absent(); 143 } 144 145 /// Return first record in result set or throw not found [EJDB2Error] error. 146 Future<JBDOC> firstRequired({void explainCallback(String log)?}) async { 147 await for (final doc in execute(explainCallback: explainCallback, limit: 1)) { 148 return doc; 149 } 150 throw EJDB2Error.notFound(); 151 } 152 153 /// Collects up to [n] elements from result set into array. 154 Future<List<JBDOC>> firstN(int n, {void explainCallback(String log)?}) async { 155 final ret = <JBDOC>[]; 156 await for (final doc in execute(explainCallback: explainCallback, limit: n)) { 157 if (n-- <= 0) break; 158 ret.add(doc); 159 } 160 return ret; 161 } 162 163 /// Abort query execution. 164 void abort() { 165 _replyPort?.close(); 166 _replyPort = null; 167 _controller?.close(); 168 _controller = null; 169 } 170 171 /// Return scalar integer value as result of query execution. 172 /// For example execution of count query: `/... | count` 173 Future<int> scalarInt({void explainCallback(String log)?}) { 174 return execute(explainCallback: explainCallback).map((d) => d.id).first; 175 } 176 177 /// Set [json] at the specified [placeholder]. 178 /// [placeholder] can be either `string` or `int` 179 JQL setJson(dynamic placeholder, dynamic json) { 180 _checkPlaceholder(placeholder); 181 ArgumentError.checkNotNull(json); 182 _set(placeholder, _asJsonString(json), 1); 183 return this; 184 } 185 186 /// Set [regexp] at the specified [placeholder]. 187 /// [placeholder] can be either `string` or `int` 188 JQL setRegExp(dynamic placeholder, RegExp regexp) { 189 _checkPlaceholder(placeholder); 190 ArgumentError.checkNotNull(regexp); 191 _set(placeholder, regexp.pattern, 2); 192 return this; 193 } 194 195 /// Set integer [val] at the specified [placeholder]. 196 /// [placeholder] can be either `string` or `int` 197 JQL setInt(dynamic placeholder, int val) { 198 _checkPlaceholder(placeholder); 199 ArgumentError.checkNotNull(val); 200 _set(placeholder, val); 201 return this; 202 } 203 204 /// Set double [val] at the specified [placeholder]. 205 /// [placeholder] can be either `string` or `int` 206 JQL setDouble(dynamic placeholder, double val) { 207 _checkPlaceholder(placeholder); 208 ArgumentError.checkNotNull(val); 209 _set(placeholder, val); 210 return this; 211 } 212 213 /// Set boolean [val] at the specified [placeholder]. 214 /// [placeholder] can be either `string` or `int` 215 JQL setBoolean(dynamic placeholder, bool val) { 216 _checkPlaceholder(placeholder); 217 ArgumentError.checkNotNull(val); 218 _set(placeholder, val); 219 return this; 220 } 221 222 /// Set string [val] at the specified [placeholder]. 223 /// [placeholder] can be either `string` or `int` 224 JQL setString(dynamic placeholder, String val) { 225 _checkPlaceholder(placeholder); 226 ArgumentError.checkNotNull(val); 227 _set(placeholder, val); 228 return this; 229 } 230 231 /// Set `null` at the specified [placeholder]. 232 /// [placeholder] can be either `string` or `int` 233 JQL setNull(dynamic placeholder) { 234 _checkPlaceholder(placeholder); 235 _set(placeholder, null); 236 return this; 237 } 238 239 /// Get current `limit` encoded in query. 240 int get limit native 'jql_get_limit'; 241 242 void _checkPlaceholder(dynamic placeholder) { 243 if (!(placeholder is String) && !(placeholder is int)) { 244 ArgumentError.value(placeholder, 'placeholder'); 245 } 246 } 247 248 void _set(dynamic placeholder, dynamic value, [int type]) native 'jql_set'; 249 250 int _exec(SendPort sendPort, bool explain, int limit) native 'exec'; 251 252 void _exec_check(int execHandle, bool terminate) native 'check_exec'; 253} 254 255/// Database wrapper 256class EJDB2 extends NativeFieldWrapperClass2 { 257 EJDB2._(); 258 259 static bool _checkCompleterPortError(Completer<dynamic> completer, dynamic reply) { 260 if (reply is int) { 261 completer.completeError(EJDB2Error(reply, ejdb2ExplainRC(reply))); 262 return true; 263 } else if (reply is! List) { 264 completer.completeError(EJDB2Error(0, 'Invalid port response')); 265 return true; 266 } 267 return false; 268 } 269 270 /// Open EJDB2 database 271 /// See https://github.com/Softmotions/ejdb/blob/master/src/ejdb2.h#L104 272 /// for description of options. 273 static Future<EJDB2> open(String path, 274 {bool truncate = false, 275 bool readonly = false, 276 bool http_enabled = false, 277 bool http_read_anon = false, 278 bool wal_enabled = true, 279 bool wal_check_crc_on_checkpoint = false, 280 int? wal_checkpoint_buffer_sz, 281 int? wal_checkpoint_timeout_sec, 282 int? wal_savepoint_timeout_sec, 283 int? wal_wal_buffer_sz, 284 int? document_buffer_sz, 285 int? sort_buffer_sz, 286 String? http_access_token, 287 String? http_bind, 288 int? http_max_body_size, 289 int? http_port}) { 290 final completer = Completer<EJDB2>(); 291 final replyPort = RawReceivePort(); 292 final jb = EJDB2._(); 293 294 path = path_lib.canonicalize(File(path).absolute.path); 295 296 replyPort.handler = (dynamic reply) { 297 replyPort.close(); 298 if (_checkCompleterPortError(completer, reply)) { 299 return; 300 } 301 try { 302 jb._set_handle((reply as List).first as int); 303 } catch (e) { 304 completer.completeError(e); 305 return; 306 } 307 completer.complete(jb); 308 }; 309 310 // Open 311 var oflags = 0; 312 if (readonly) { 313 oflags |= 0x02; 314 } else if (truncate) { 315 oflags |= 0x04; 316 } 317 318 jb._port().send([ 319 replyPort.sendPort, 320 'open', 321 path, 322 oflags, 323 wal_enabled, 324 wal_check_crc_on_checkpoint, 325 wal_checkpoint_buffer_sz as dynamic, 326 wal_checkpoint_timeout_sec as dynamic, 327 wal_savepoint_timeout_sec as dynamic, 328 wal_wal_buffer_sz as dynamic, 329 document_buffer_sz as dynamic, 330 sort_buffer_sz as dynamic, 331 http_enabled, 332 http_access_token as dynamic, 333 http_bind as dynamic, 334 http_max_body_size as dynamic, 335 http_port as dynamic, 336 http_read_anon 337 ]); 338 return completer.future; 339 } 340 341 /// Closes database instance. 342 Future<void> close() { 343 final hdb = _get_handle(); 344 if (hdb == null) { 345 return Future.value(); 346 } 347 final completer = Completer<void>(); 348 final replyPort = RawReceivePort(); 349 replyPort.handler = (dynamic reply) { 350 replyPort.close(); 351 if (_checkCompleterPortError(completer, reply)) { 352 return; 353 } 354 completer.complete(); 355 }; 356 _set_handle(null); 357 _port().send([replyPort.sendPort, 'close', hdb as dynamic]); 358 return completer.future; 359 } 360 361 /// Save [json] document under specified [id] or create a document 362 /// with new generated `id`. Returns future holding actual document `id`. 363 Future<int> put(String collection, dynamic json, [int? id]) { 364 final hdb = _get_handle(); 365 if (hdb == null) { 366 return Future.error(EJDB2Error.invalidState()); 367 } 368 final completer = Completer<int>(); 369 final replyPort = RawReceivePort(); 370 replyPort.handler = (dynamic reply) { 371 replyPort.close(); 372 if (_checkCompleterPortError(completer, reply)) { 373 return; 374 } 375 completer.complete((reply as List).first as int); 376 }; 377 _port().send([ 378 replyPort.sendPort, 379 'put', 380 hdb as dynamic, 381 collection, 382 _asJsonString(json), 383 id as dynamic 384 ]); 385 return completer.future; 386 } 387 388 /// Apply rfc6902/rfc7386 JSON [patch] to the document identified by [id]. 389 Future<void> patch(String collection, dynamic patchObj, int id, [bool upsert = false]) { 390 final hdb = _get_handle(); 391 if (hdb == null) { 392 return Future.error(EJDB2Error.invalidState()); 393 } 394 final completer = Completer<void>(); 395 final replyPort = RawReceivePort(); 396 replyPort.handler = (dynamic reply) { 397 replyPort.close(); 398 if (_checkCompleterPortError(completer, reply)) { 399 return; 400 } 401 completer.complete(); 402 }; 403 _port().send([ 404 replyPort.sendPort, 405 'patch', 406 hdb as dynamic, 407 collection, 408 _asJsonString(patchObj), 409 id as dynamic, 410 upsert 411 ]); 412 return completer.future; 413 } 414 415 /// Apply JSON merge patch (rfc7396) to the document identified by `id` or 416 /// insert new document under specified `id`. 417 Future<void> patchOrPut(String collection, dynamic patchObj, int id) => 418 patch(collection, patch, id, true); 419 420 /// Get json body of document identified by [id] and stored in [collection]. 421 /// Throws [EJDB2Error] not found exception if document is not found. 422 Future<String> get(String collection, int id) { 423 final hdb = _get_handle(); 424 if (hdb == null) { 425 return Future.error(EJDB2Error.invalidState()); 426 } 427 final completer = Completer<String>(); 428 final replyPort = RawReceivePort(); 429 replyPort.handler = (dynamic reply) { 430 replyPort.close(); 431 if (_checkCompleterPortError(completer, reply)) { 432 return; 433 } 434 completer.complete((reply as List).first as String); 435 }; 436 _port().send([replyPort.sendPort, 'get', hdb as dynamic, collection, id]); 437 return completer.future; 438 } 439 440 /// Get json body of database metadata. 441 Future<String> info() { 442 final hdb = _get_handle(); 443 if (hdb == null) { 444 return Future.error(EJDB2Error.invalidState()); 445 } 446 final completer = Completer<String>(); 447 final replyPort = RawReceivePort(); 448 replyPort.handler = (dynamic reply) { 449 replyPort.close(); 450 if (_checkCompleterPortError(completer, reply)) { 451 return; 452 } 453 completer.complete((reply as List).first as String); 454 }; 455 _port().send([replyPort.sendPort, 'info', hdb as dynamic]); 456 return completer.future; 457 } 458 459 /// Remove document idenfied by [id] from [collection]. 460 Future<void> del(String collection, int id) { 461 final hdb = _get_handle(); 462 if (hdb == null) { 463 return Future.error(EJDB2Error.invalidState()); 464 } 465 final completer = Completer<void>(); 466 final replyPort = RawReceivePort(); 467 replyPort.handler = (dynamic reply) { 468 replyPort.close(); 469 if (_checkCompleterPortError(completer, reply)) { 470 return; 471 } 472 completer.complete(); 473 }; 474 _port().send([replyPort.sendPort, 'del', hdb as dynamic, collection, id]); 475 return completer.future; 476 } 477 478 /// Remove document idenfied by [id] from [collection]. 479 /// Doesn't raise error if document is not found. 480 Future<void> delIgnoreNotFound(String collection, int id) => 481 del(collection, id).catchError((err) { 482 if (err is EJDB2Error && err.notFound) { 483 return Future.value(); 484 } else { 485 return Future.error(err as Object); 486 } 487 }); 488 489 Future<void> renameCollection(String oldCollection, String newCollectionName) { 490 final hdb = _get_handle(); 491 if (hdb == null) { 492 return Future.error(EJDB2Error.invalidState()); 493 } 494 final completer = Completer<void>(); 495 final replyPort = RawReceivePort(); 496 replyPort.handler = (dynamic reply) { 497 replyPort.close(); 498 if (_checkCompleterPortError(completer, reply)) { 499 return; 500 } 501 completer.complete(); 502 }; 503 _port().send([replyPort.sendPort, 'rename', hdb as dynamic, oldCollection, newCollectionName]); 504 return completer.future; 505 } 506 507 /// Ensures json document database index specified by [path] json pointer to string data type. 508 Future<void> ensureStringIndex(String collection, String path, {bool unique = false}) { 509 return _idx(collection, path, 0x04 | (unique ? 0x01 : 0)); 510 } 511 512 /// Removes specified database index. 513 Future<void> removeStringIndex(String collection, String path, {bool unique = false}) { 514 return _rmi(collection, path, 0x04 | (unique ? 0x01 : 0)); 515 } 516 517 /// Ensures json document database index specified by [path] json pointer to integer data type. 518 Future<void> ensureIntIndex(String collection, String path, {bool unique = false}) { 519 return _idx(collection, path, 0x08 | (unique ? 0x01 : 0)); 520 } 521 522 /// Removes specified database index. 523 Future<void> removeIntIndex(String collection, String path, {bool unique = false}) { 524 return _rmi(collection, path, 0x08 | (unique ? 0x01 : 0)); 525 } 526 527 /// Ensures json document database index specified by [path] json pointer to floating point data type. 528 Future<void> ensureFloatIndex(String collection, String path, {bool unique = false}) { 529 return _idx(collection, path, 0x10 | (unique ? 0x01 : 0)); 530 } 531 532 /// Removes specified database index. 533 Future<void> removeFloatIndex(String collection, String path, {bool unique = false}) { 534 return _rmi(collection, path, 0x10 | (unique ? 0x01 : 0)); 535 } 536 537 /// Removes database [collection]. 538 Future<void> removeCollection(String collection) { 539 final hdb = _get_handle(); 540 if (hdb == null) { 541 return Future.error(EJDB2Error.invalidState()); 542 } 543 final completer = Completer<void>(); 544 final replyPort = RawReceivePort(); 545 replyPort.handler = (dynamic reply) { 546 replyPort.close(); 547 if (_checkCompleterPortError(completer, reply)) { 548 return; 549 } 550 completer.complete(); 551 }; 552 _port().send([replyPort.sendPort, 'rmc', hdb as dynamic, collection]); 553 return completer.future; 554 } 555 556 /// Creates an online database backup image and copies it into the specified [fileName]. 557 /// During online backup phase read/write database operations are allowed and not 558 /// blocked for significant amount of time. Returns future with backup 559 /// finish time as number of milliseconds since epoch. 560 Future<int> onlineBackup(String fileName) { 561 final hdb = _get_handle(); 562 if (hdb == null) { 563 return Future.error(EJDB2Error.invalidState()); 564 } 565 final completer = Completer<int>(); 566 final replyPort = RawReceivePort(); 567 replyPort.handler = (dynamic reply) { 568 replyPort.close(); 569 if (_checkCompleterPortError(completer, reply)) { 570 return; 571 } 572 completer.complete((reply as List).first as int); 573 }; 574 _port().send([replyPort.sendPort, 'bkp', hdb as dynamic, fileName]); 575 return completer.future; 576 } 577 578 /// Create instance of [query] specified for [collection]. 579 /// If [collection] is not specified a [query] spec must contain collection name, 580 /// eg: `@mycollection/[foo=bar]` 581 JQL createQuery(String query, [String collection]) native 'create_query'; 582 583 Future<void> _idx(String collection, String path, int mode) { 584 final hdb = _get_handle(); 585 if (hdb == null) { 586 return Future.error(EJDB2Error.invalidState()); 587 } 588 final completer = Completer<void>(); 589 final replyPort = RawReceivePort(); 590 replyPort.handler = (dynamic reply) { 591 replyPort.close(); 592 if (_checkCompleterPortError(completer, reply)) { 593 return; 594 } 595 completer.complete(); 596 }; 597 _port().send([replyPort.sendPort, 'idx', hdb as dynamic, collection, path, mode]); 598 return completer.future; 599 } 600 601 Future<void> _rmi(String collection, String path, int mode) { 602 final hdb = _get_handle(); 603 if (hdb == null) { 604 return Future.error(EJDB2Error.invalidState()); 605 } 606 final completer = Completer<void>(); 607 final replyPort = RawReceivePort(); 608 replyPort.handler = (dynamic reply) { 609 replyPort.close(); 610 if (_checkCompleterPortError(completer, reply)) { 611 return; 612 } 613 completer.complete(); 614 }; 615 _port().send([replyPort.sendPort, 'rmi', hdb as dynamic, collection, path, mode]); 616 return completer.future; 617 } 618 619 SendPort _port() native 'port'; 620 621 void _set_handle(int? handle) native 'set_handle'; 622 623 int? _get_handle() native 'get_handle'; 624} 625 626String _asJsonString(dynamic val) { 627 if (val is String) { 628 return val; 629 } else { 630 return jsonEncode(val); 631 } 632} 633