• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// @dart=2.5
2
3import 'dart:async';
4import 'dart:convert' as convert_lib;
5
6import 'package:flutter/services.dart';
7import 'package:json_at/json_at.dart';
8import 'package:pedantic/pedantic.dart';
9import 'package:quiver/core.dart';
10
11const MethodChannel _mc = MethodChannel('ejdb2');
12
13const EventChannel _qc = EventChannel('ejdb2/query');
14
15Map<String, StreamController<JBDOC>> _ecm;
16
17void _execute(String hook, dynamic args, StreamController<JBDOC> sctl) {
18  if (_ecm == null) {
19    _ecm = {};
20    _qc.receiveBroadcastStream().listen(_onQueryData);
21  }
22  unawaited(_mc.invokeMethod('executeQuery', args).catchError((err, StackTrace s) {
23    _ecm.remove(hook);
24    dynamic streamError = err;
25    if (err is PlatformException && err.code.startsWith('@ejdb')) {
26      streamError = EJDB2Error(err.code, err.message);
27    }
28    sctl.addError(streamError, s);
29  }));
30  _ecm[hook] = sctl;
31}
32
33void _onQueryData(dynamic dataArg) {
34  final data = dataArg as List;
35  if (data.isEmpty) {
36    return;
37  }
38  final qhook = data[0] as String;
39  final sctl = _ecm[qhook];
40  if (sctl == null) {
41    return;
42  }
43  final last = data.last == true;
44  final l = last ? data.length - 1 : data.length;
45  for (int i = 1; i < l; i += 2) {
46    sctl.add(JBDOC(data[i] as int, data[i + 1] as String));
47  }
48  if (last) {
49    _ecm.remove(qhook);
50    sctl.close();
51  }
52}
53
54/// EJDB2 Instance builder.
55///
56class EJDB2Builder {
57  EJDB2Builder(this.path);
58
59  final String path;
60
61  bool readonly;
62
63  bool truncate;
64
65  bool walEnabled;
66
67  bool walCheckCRCOnCheckpoint;
68
69  int walCheckpointBufferSize;
70
71  int walCheckpointTimeout;
72
73  int walSavepointTimeout;
74
75  int walBufferSize;
76
77  int documentBufferSize;
78
79  int sortBufferSize;
80
81  /// Open EJDB2 database
82  /// See https://github.com/Softmotions/ejdb/blob/master/src/ejdb2.h#L104
83  /// for description of options.
84  Future<EJDB2> open() => EJDB2._open(this);
85
86  Map<String, dynamic> _asOpts() => <String, dynamic>{
87        'readonly': readonly ?? false,
88        'truncate': truncate ?? false,
89        'wal_enabled': walEnabled ?? true,
90        'wal_check_crc_on_checkpoint': walCheckCRCOnCheckpoint ?? false,
91        ...walCheckpointBufferSize != null
92            ? {'wal_checkpoint_buffer_sz': walCheckpointBufferSize}
93            : {},
94        ...walCheckpointTimeout != null ? {'wal_checkpoint_timeout_sec': walCheckpointTimeout} : {},
95        ...walSavepointTimeout != null ? {'wal_savepoint_timeout_sec': walSavepointTimeout} : {},
96        ...walBufferSize != null ? {'wal_wal_buffer_sz': walBufferSize} : {},
97        ...documentBufferSize != null ? {'document_buffer_sz': documentBufferSize} : {},
98        ...sortBufferSize != null ? {'sort_buffer_sz': sortBufferSize} : {}
99      };
100}
101
102FutureOr<T> Function(Object, StackTrace) _errorHandler<T>() =>
103  (Object err, StackTrace s) {
104    if (err is PlatformException && err.code.startsWith('@ejdb')) {
105    return Future<T>.error(EJDB2Error(err.code, err.message), s);
106    }
107    return Future<T>.error(err, s);
108  };
109
110class EJDB2Error implements Exception {
111  EJDB2Error(this.code, this.message);
112  EJDB2Error.notFound() : this('@ejdb IWRC:75001', 'Not found');
113
114  final String code;
115  final String message;
116
117  bool isNotFound() => (code ?? '').startsWith('@ejdb IWRC:75001');
118
119  bool isInvalidQuery() => (code ?? '').startsWith('@ejdb IWRC:87001');
120
121  @override
122  String toString() => '$runtimeType: $code $message';
123}
124
125/// EJDB document item.
126///
127class JBDOC {
128  JBDOC(this.id, this._json);
129  JBDOC._fromList(List list) : this(list[0] as int, list[1] as String);
130
131  /// Document identifier
132  final int id;
133
134  /// Document body as JSON string
135  String get json => _json ?? convert_lib.jsonEncode(_object);
136
137  /// Document body as parsed JSON object.
138  dynamic get object {
139    if (_json == null) {
140      return _object;
141    } else {
142      _object = convert_lib.jsonDecode(_json);
143      _json = null; // Release memory used to store JSON string data
144      return _object;
145    }
146  }
147
148  /// Gets subset of document using RFC 6901 JSON [pointer].
149  Optional<dynamic> at(String pointer) => jsonAt(object, pointer);
150
151  /// Gets subset of document using RFC 6901 JSON [pointer].
152  Optional<dynamic> operator [](String pointer) => at(pointer);
153
154  String _json;
155
156  dynamic _object;
157
158  @override
159  String toString() => '$runtimeType: $id $json';
160}
161
162/// EJDB2 query builder/executor.
163///
164class JQL {
165  JQL._(this._jb, this.collection, this.qtext);
166
167  static int _qhandle = 0;
168
169  final EJDB2 _jb;
170
171  final _qspec = <String, dynamic>{};
172
173  final _params = <dynamic>[];
174
175  final String collection;
176
177  final String qtext;
178
179  int get limit => _qspec['l'] as int ?? 0;
180
181  set limit(int limit) => _qspec['l'] = limit;
182
183  int get skip => _qspec['s'] as int ?? 0;
184
185  set skip(int skip) => _qspec['s'] = skip;
186
187  int get _handle => _jb._handle;
188
189  /// Set string [val] at the specified [placeholder].
190  /// [placeholder] can be either `string` or `int`
191  JQL setString(dynamic placeholder, String val) {
192    _checkPlaceholder(placeholder);
193    ArgumentError.checkNotNull(val);
194    _params.add([1, placeholder, val]);
195    return this;
196  }
197
198  /// Set integer [val] at the specified [placeholder].
199  /// [placeholder] can be either `string` or `int`
200  JQL setInt(dynamic placeholder, int val) {
201    _checkPlaceholder(placeholder);
202    ArgumentError.checkNotNull(val);
203    _params.add([2, placeholder, val]);
204    return this;
205  }
206
207  /// Set double [val] at the specified [placeholder].
208  /// [placeholder] can be either `string` or `int`
209  JQL setDouble(dynamic placeholder, double val) {
210    _checkPlaceholder(placeholder);
211    ArgumentError.checkNotNull(val);
212    _params.add([3, placeholder, val]);
213    return this;
214  }
215
216  /// Set bool [val] at the specified [placeholder].
217  /// [placeholder] can be either `string` or `int`
218  JQL setBoolean(dynamic placeholder, bool val) {
219    _checkPlaceholder(placeholder);
220    ArgumentError.checkNotNull(val);
221    _params.add([4, placeholder, val]);
222    return this;
223  }
224
225  /// Set RegExp [val] at the specified [placeholder].
226  /// [placeholder] can be either `string` or `int`
227  JQL setRegexp(dynamic placeholder, RegExp val) {
228    _checkPlaceholder(placeholder);
229    ArgumentError.checkNotNull(val);
230    _params.add([5, placeholder, val.pattern]);
231    return this;
232  }
233
234  /// Set [json] at the specified [placeholder].
235  /// [placeholder] can be either `string` or `int`
236  JQL setJson(dynamic placeholder, Object json) {
237    _checkPlaceholder(placeholder);
238    ArgumentError.checkNotNull(json);
239    if (json is! String) {
240      json = convert_lib.jsonEncode(json);
241    }
242    _params.add([6, placeholder, json]);
243    return this;
244  }
245
246  /// Execute query and returns a stream of matched documents.
247  Stream<JBDOC> execute() {
248    final qh = '${++JQL._qhandle}';
249    final sctl = StreamController<JBDOC>(onCancel: () {
250      _ecm.remove(qh);
251    });
252    final args = [_handle, qh, collection, qtext, _qspec, _params];
253    _execute(qh, args, sctl);
254    return sctl.stream;
255  }
256
257  /// Executes query then listen event stream for first event/error
258  /// to eagerly fetch pending error if available.
259  Future<void> executeTouch() {
260    StreamSubscription subscription;
261    final Completer completer = Completer();
262    final stream = execute();
263    subscription = stream.listen((val) {
264      if (!completer.isCompleted) {
265        completer.complete();
266      }
267      final sf = subscription.cancel();
268      if (sf != null) {
269        unawaited(sf.catchError(() {}));
270      }
271    }, onError: (e, StackTrace s) {
272      if (!completer.isCompleted) {
273        completer.completeError(e, s);
274      }
275    }, onDone: () {
276      if (!completer.isCompleted) {
277        completer.complete();
278      }
279    }, cancelOnError: true);
280    return completer.future;
281  }
282
283  /// Return scalar integer value as result of query execution.
284  /// For example execution of count query: `/... | count`
285  Future<int> executeScalarInt() => _mc.invokeMethod('executeScalarInt',
286      [_handle, null, collection, qtext, _qspec, _params]).then((v) => v as int);
287
288  /// Returns list of matched documents.
289  /// Use it with care to avoid wasting of memory.
290  Future<List<JBDOC>> list([int limitArg]) async {
291    var qspec = _qspec;
292    if (limitArg != null) {
293      // Copy on write to be safe
294      qspec = {
295        ..._qspec,
296        ...{'l': limitArg}
297      };
298    }
299    final list = await _mc
300        .invokeListMethod('executeList', [_handle, null, collection, qtext, qspec, _params]);
301    final res = <JBDOC>[];
302    for (int i = 0; i < list.length; i += 2) {
303      res.add(JBDOC(list[i] as int, list[i + 1] as String));
304    }
305    return res;
306  }
307
308  /// Returns optional element for first record in result set.
309  Future<Optional<JBDOC>> first() async {
310    final l = await list(limit = 1);
311    return l.isNotEmpty ? Optional.of(l.first) : const Optional.absent();
312  }
313
314  /// Return first record in result set or resolve to notfound [EJDB2Error] error.
315  Future<JBDOC> firstRequired() async {
316    final f = await first();
317    if (f.isPresent) {
318      return f.value;
319    }
320    return Future.error(EJDB2Error.notFound());
321  }
322
323  /// Returns list of first [n] matched documents.
324  /// Use it with care to avoid wasting of memory.
325  Future<List<JBDOC>> firstN(int n) => list(n);
326
327  void _checkPlaceholder(dynamic placeholder) {
328    if (!(placeholder is String) && !(placeholder is int)) {
329      ArgumentError.value(placeholder, 'placeholder');
330    }
331  }
332}
333
334class EJDB2 {
335  EJDB2._(this._handle);
336
337  final int _handle;
338
339  static Future<EJDB2> _open(EJDB2Builder b) async {
340    final hdl =
341        await _mc.invokeMethod<int>('open', [null, b.path, b._asOpts()]).catchError(_errorHandler<int>());
342    return EJDB2._(hdl);
343  }
344
345  /// Closes database instance.
346  Future<void> close() {
347    if (_handle == null) {
348      throw StateError('Closed');
349    }
350    return _mc.invokeMethod('close', [_handle]).catchError(_errorHandler());
351  }
352
353  /// Create instance of [query] specified for [collection].
354  /// If [collection] is not specified a [query] spec must contain collection name,
355  /// eg: `@mycollection/[foo=bar]`
356  JQL createQuery(String query, [String collection]) => JQL._(this, collection, query);
357
358  /// Create instance of [query].
359  JQL operator [](String query) => createQuery(query);
360
361  /// Save [json] document under specified [id] or create a new document
362  /// with new generated `id`. Returns future holding actual document `id`.
363  Future<int> put(String collection, dynamic json, [int id]) => Future.sync(() => _mc
364      .invokeMethod('put', [
365        _handle,
366        collection,
367        (json is! String) ? convert_lib.jsonEncode(json) : json as String,
368        id
369      ])
370      .catchError(_errorHandler<int>())
371      .then((v) => v as int));
372
373  /// Apply rfc6902/rfc6901 JSON [patch] to the document identified by [id].
374  Future<void> patch(String collection, dynamic json, int id, [bool upsert = false]) =>
375      Future.sync(() => _mc.invokeMethod('patch', [
376            _handle,
377            collection,
378            (json is! String) ? convert_lib.jsonEncode(json) : json as String,
379            id,
380            upsert
381          ]).catchError(_errorHandler()));
382
383  /// Apply JSON merge patch (rfc7396) to the document identified by `id` or
384  /// insert new document under specified `id`.
385  Future<void> patchOrPut(String collection, dynamic json, int id) =>
386      patch(collection, json, id, true);
387
388  /// Get json body of document identified by [id] and stored in [collection].
389  /// Throws [EJDB2Error] not found exception if document is not found.
390  Future<JBDOC> get(String collection, int id) => _mc
391      .invokeMethod('get', [_handle, collection, id])
392      .catchError(_errorHandler<JBDOC>())
393      .then((v) => JBDOC(id, v as String));
394
395  /// Get json body of document identified by [id] and stored in [collection].
396  Future<Optional<JBDOC>> getOptional(String collection, int id) {
397    return get(collection, id).then((doc) => Optional.of(doc)).catchError((err) {
398      if (err is EJDB2Error && err.isNotFound()) {
399        return Future.value(const Optional<JBDOC>.absent());
400      }
401      return Future.error(err);
402    });
403  }
404
405  /// Remove document identified by [id] from specified [collection].
406  Future<void> del(String collection, int id) =>
407      _mc.invokeMethod('del', [_handle, collection, id]).catchError(_errorHandler());
408
409  /// Remove document identified by [id] from specified [collection].
410  /// Doesn't raise error if document is not found.
411  Future<void> delIgnoreNotFound(String collection, int id) =>
412      del(collection, id).catchError((err) {
413        if (err is EJDB2Error && err.isNotFound()) {
414          return Future.value();
415        } else {
416          return Future.error(err);
417        }
418      });
419
420  /// Get json body of database metadata.
421  Future<dynamic> info() => _mc
422      .invokeMethod('info', [_handle])
423      .catchError(_errorHandler())
424      .then((v) => convert_lib.jsonDecode(v as String));
425
426  /// Removes database [collection].
427  Future<void> removeCollection(String collection) =>
428      _mc.invokeMethod('removeCollection', [_handle, collection]).catchError(_errorHandler());
429
430  /// Renames database collection.
431  Future<void> renameCollection(String oldName, String newName) {
432    return _mc
433        .invokeMethod('renameCollection', [_handle, oldName, newName]).catchError(_errorHandler());
434  }
435
436  Future<void> ensureStringIndex(String coll, String path, [bool unique]) =>
437      _mc.invokeMethod('ensureStringIndex', [_handle, coll, path, unique ?? false]);
438
439  Future<void> removeStringIndex(String coll, String path, [bool unique]) =>
440      _mc.invokeMethod('removeStringIndex', [_handle, coll, path, unique ?? false]);
441
442  Future<void> ensureIntIndex(String coll, String path, [bool unique]) =>
443      _mc.invokeMethod('ensureIntIndex', [_handle, coll, path, unique ?? false]);
444
445  Future<void> removeIntIndex(String coll, String path, [bool unique]) =>
446      _mc.invokeMethod('removeIntIndex', [_handle, coll, path, unique ?? false]);
447
448  Future<void> ensureFloatIndex(String coll, String path, [bool unique]) =>
449      _mc.invokeMethod('ensureFloatIndex', [_handle, coll, path, unique ?? false]);
450
451  Future<void> removeFloatIndex(String coll, String path, [bool unique]) =>
452      _mc.invokeMethod('removeFloatIndex', [_handle, coll, path, unique ?? false]);
453
454  /// Creates an online database backup image and copies it into the specified [fileName].
455  /// During online backup phase read/write database operations are allowed and not
456  /// blocked for significant amount of time. Returns future with backup
457  /// finish time as number of milliseconds since epoch.
458  Future<int> onlineBackup(String fileName) =>
459      _mc.invokeMethod('onlineBackup', [_handle, fileName]).then((v) => v as int);
460}
461