• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 import Flutter
2 import EJDB2
3 import PathKit
4 
5 typealias MethodInvocation = (_: DbEntry) -> Void
6 
7 typealias DbQueryHandler = (_: SWJQL, _: String?) throws -> Void
8 
9 extension Dictionary {
containsKeynull10   func containsKey(_ key: Key) -> Bool {
11     self[key] != nil
12   }
13 }
14 
asBooleannull15 func asBoolean(_ v: Any?, _ def: Bool = false) -> Bool {
16   if let c = v as? NSNumber {
17     return c.intValue > 0
18   }
19   if let c = v as? NSString {
20     return c.isEqual(to: "true")
21   }
22   if let c = v as? Bool {
23     return c
24   }
25   return def
26 }
27 
asNumbernull28 func asNumber(_ v: Any?) -> NSNumber? {
29   if let c = v as? NSNumber {
30     return c
31   }
32   if let c = v as? String {
33     return NSNumber(value: Int64(c) ?? 0)
34   }
35   return nil
36 }
37 
asDoublenull38 func asDouble(_ v: Any?) -> Double? {
39   if let c = v as? NSNumber {
40     return c.doubleValue
41   }
42   if let c = v as? String {
43     return Double(c)
44   }
45   return nil
46 }
47 
asStringnull48 func asString(_ v: Any?, _ def: String? = nil) -> String? {
49   if let c = v as? String {
50     return c
51   }
52   if v != nil {
53     return String(describing: v!)
54   }
55   return def
56 }
57 
58 class DbEntry {
59   let db: EJDB2DB
60   let handle: UInt32
61   let path: String
62   var counter: Int = 0
63 
64   init(_ db: EJDB2DB, _ handle: UInt32, _ path: String) {
65     self.db = db
66     self.handle = handle
67     self.path = path
68   }
69 
countOpennull70   func countOpen() {
71     counter += 1
72   }
73 
closenull74   func close() throws -> Bool {
75     counter -= 1
76     if counter <= 0 {
77       try db.close()
78     }
79     return counter <= 0
80   }
81 }
82 
83 struct DbMethodCall {
84   let dbe: DbEntry?
85   let args: [Any?]
86   let result: FlutterResult
87 
88   var db: EJDB2DB {
89     dbe!.db
90   }
91 
successOnMainThreadnull92   func successOnMainThread(_ val: Any? = nil) {
93     DispatchQueue.main.async {
94       self.result(val)
95     }
96   }
97 
errorOnMainThreadnull98   func errorOnMainThread(_ code: String, _ message: String?, _ details: Any? = nil) {
99     DispatchQueue.main.async {
100       self.result(FlutterError(code: code, message: message, details: details))
101     }
102   }
103 }
104 
105 public class SwiftEjdb2FlutterPlugin: NSObject, FlutterPlugin, FlutterStreamHandler {
106 
registernull107   public static func register(with registrar: FlutterPluginRegistrar) {
108     let _ = SwiftEjdb2FlutterPlugin(registrar)
109   }
110 
111   let methodChannel: FlutterMethodChannel
112   let eventChannel: FlutterEventChannel
113   var dbmap: [UInt32: DbEntry] = [:]
114   var dbkeys: UInt32 = 0
115   var events: FlutterEventSink?
116 
117   init(_ registrar: FlutterPluginRegistrar) {
118     self.methodChannel = FlutterMethodChannel(name: "ejdb2", binaryMessenger: registrar.messenger())
119     self.eventChannel = FlutterEventChannel(name: "ejdb2/query", binaryMessenger: registrar.messenger())
120     super.init()
121     registrar.addMethodCallDelegate(self, channel: methodChannel)
122     eventChannel.setStreamHandler(self)
123   }
124 
125   func execute(_ mc: DbMethodCall, _ block: @escaping (_: DbMethodCall) throws -> Void) {
126     DispatchQueue.global().async {
127       do {
128         try block(mc)
129       } catch let error as EJDB2Error {
130         mc.errorOnMainThread("@ejdb IWRC:\(error.code)", error.message)
131       } catch {
132         mc.errorOnMainThread("@ejdb", "\(error)")
133       }
134     }
135   }
136 
handlenull137   public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
138     // NSLog("\nCall method \(call.method)")
139     let args = call.arguments as? [Any?] ?? []
140     var dbe: DbEntry?
141     if let dbid = args.first as? NSNumber {
142       dbe = dbmap[dbid.uint32Value]
143       if dbe == nil {
144         result(FlutterError(code: "@ejdb", message: "Database handle already disposed", details: nil))
145         return
146       }
147     }
148 
149     let mc = DbMethodCall(dbe: dbe, args: args, result: result)
150 
151     switch call.method {
152     case "get":
153       execute(mc, get)
154       break
155     case "put":
156       execute(mc, put)
157       break;
158     case "patch":
159       execute(mc, patch)
160       break
161     case "del":
162       execute(mc, del)
163       break
164     case "info":
165       execute(mc, info)
166       break
167     case "executeList":
168       execute(mc, executeList)
169       break
170     case "executeFirst":
171       execute(mc, executeFirst)
172       break
173     case "executeScalarInt":
174       execute(mc, executeScalarInt)
175       break
176     case "executeQuery":
177       execute(mc, executeQuery)
178       break
179     case "renameCollection":
180       execute(mc, renameCollection)
181       break
182     case "removeCollection":
183       execute(mc, removeCollection)
184       break
185     case "ensureFloatIndex":
186       execute(mc, ensureFloatIndex)
187       break
188     case "ensureStringIndex":
189       execute(mc, ensureStringIndex)
190       break
191     case "ensureIntIndex":
192       execute(mc, ensureIntIndex)
193       break
194     case "removeFloatIndex":
195       execute(mc, removeFloatIndex)
196       break
197     case "removeStringIndex":
198       execute(mc, removeStringIndex)
199       break
200     case "removeIntIndex":
201       execute(mc, removeIntIndex)
202       break
203     case "onlineBackup":
204       execute(mc, onlineBackup)
205       break
206     case "open":
207       execute(mc, open)
208       break
209     case "close":
210       execute(mc, close)
211       break
212     default:
213       NSLog("\nMethod call: \(call.method) not implemented")
214       result(FlutterMethodNotImplemented)
215       break
216     }
217   }
218 
executeFirstnull219   func executeFirst(_ mc: DbMethodCall) throws {
220     return try executeListImpl(mc, 1)
221   }
222 
executeListnull223   func executeList(_ mc: DbMethodCall) throws {
224     return try executeListImpl(mc)
225   }
226 
executeListImplnull227   func executeListImpl(_ mc: DbMethodCall, _ limit: Int64? = nil) throws {
228     try prepareQuery(mc) { q, _ in
229       if limit != nil {
230         q.setLimit(limit!)
231       }
232       var res: [Any?] = []
233       try q.execute() { doc in
234         res.append(doc.id)
235         res.append(doc.json)
236         return true
237       }
238       mc.successOnMainThread(res)
239     }
240   }
241 
executeScalarIntnull242   func executeScalarInt(_ mc: DbMethodCall) throws {
243     try prepareQuery(mc) { q, _ in
244       let res = try q.executeScalarInt()
245       mc.successOnMainThread(res)
246     }
247   }
248 
executeQuerynull249   func executeQuery(_ mc: DbMethodCall) throws {
250     try prepareQuery(mc) { q, hook in
251       var batch: [Any?] = []
252       try q.execute() { doc in
253         if batch.count >= 128 && batch.count % 2 == 0 {
254           batch.insert(hook, at: 0)
255           let res = batch
256           DispatchQueue.main.sync {
257             self.events?(res)
258           }
259           batch = []
260         }
261         batch.append(doc.id)
262         batch.append(doc.json)
263         return true
264       }
265       batch.insert(hook, at: 0)
266       batch.append(true)
267       DispatchQueue.main.sync {
268         self.events?(batch)
269       }
270     }
271   }
272 
prepareQuerynull273   func prepareQuery(_ mc: DbMethodCall, _ qh: DbQueryHandler) throws {
274     let db = mc.db
275     let hook = asString(mc.args[1])
276     let coll = asString(mc.args[2])
277     let qtext = asString(mc.args[3], "")!
278     let qspec = mc.args[4] as! [String: Any?]
279     let params = mc.args[5] as! [[Any?]]
280 
281     let q = try db.createQuery(qtext, coll)
282     qspec.forEach { k, v in
283       switch (k) {
284       case "l": // limit
285         q.setLimit(asNumber(v)?.int64Value ?? 0)
286         break
287       case "s": // skip
288         q.setSkip(asNumber(v)?.int64Value ?? 0)
289         break
290       default:
291         break
292       }
293     }
294     for pslot in params {
295       let type = asNumber(pslot[0])?.intValue
296       let plh = pslot[1]
297       let val = pslot[2]
298       if type == 0 || val == nil {
299         if let v = plh as? NSNumber {
300           try q.setNull(v.int32Value)
301         } else {
302           try q.setNull(plh as! String)
303         }
304       } else {
305         switch (type) {
306         case 1: // String
307           if let v = plh as? NSNumber {
308             try q.setString(v.int32Value, asString(val))
309           } else {
310             try q.setString(plh as! String, asString(val))
311           }
312           break
313         case 2: // Long
314           if let v = plh as? NSNumber {
315             try q.setInt64(v.int32Value, asNumber(val)?.int64Value)
316           } else {
317             try q.setInt64(plh as! String, asNumber(val)?.int64Value)
318           }
319           break
320         case 3: // Double
321           if let v = plh as? NSNumber {
322             try q.setDouble(v.int32Value, asDouble(val))
323           } else {
324             try q.setDouble(plh as! String, asDouble(val))
325           }
326           break
327         case 4: // Boolean
328           if let v = plh as? NSNumber {
329             try q.setBool(v.int32Value, asBoolean(val))
330           } else {
331             try q.setBool(plh as! String, asBoolean(val))
332           }
333           break
334         case 5: // Regexp
335           if let v = plh as? NSNumber {
336             try q.setRegexp(v.int32Value, asString(val))
337           } else {
338             try q.setRegexp(plh as! String, asString(val))
339           }
340           break
341         case 6: // JSON
342           if let v = plh as? NSNumber {
343             try q.setJson(v.int32Value, val!)
344           } else {
345             try q.setJson(plh as! String, val!)
346           }
347           break
348         default:
349           break
350         }
351       }
352     }
353     try qh(q, hook)
354   }
355 
onlineBackupnull356   func onlineBackup(_ mc: DbMethodCall) throws {
357     var path = Path(asString(mc.args[1])!)
358     if !path.isAbsolute {
359       let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
360       path = Path(documentsPath) + path
361     }
362     NSLog("\nOnline backup into: \(path)")
363     mc.successOnMainThread(try mc.db.onlineBackup(path.string))
364   }
365 
removeFloatIndexnull366   func removeFloatIndex(_ mc: DbMethodCall) throws {
367     let coll = asString(mc.args[1])!
368     let path = asString(mc.args[2])!
369     let unique = asBoolean(mc.args[3])
370     try mc.db.removeFloatIndex(coll, path, unique: unique)
371     mc.successOnMainThread()
372   }
373 
ensureFloatIndexnull374   func ensureFloatIndex(_ mc: DbMethodCall) throws {
375     let coll = asString(mc.args[1])!
376     let path = asString(mc.args[2])!
377     let unique = asBoolean(mc.args[3])
378     try mc.db.ensureFloatIndex(coll, path, unique: unique)
379     mc.successOnMainThread()
380   }
381 
removeIntIndexnull382   func removeIntIndex(_ mc: DbMethodCall) throws {
383     let coll = asString(mc.args[1])!
384     let path = asString(mc.args[2])!
385     let unique = asBoolean(mc.args[3])
386     try mc.db.removeIntIndex(coll, path, unique: unique)
387     mc.successOnMainThread()
388   }
389 
ensureIntIndexnull390   func ensureIntIndex(_ mc: DbMethodCall) throws {
391     let coll = asString(mc.args[1])!
392     let path = asString(mc.args[2])!
393     let unique = asBoolean(mc.args[3])
394     try mc.db.ensureIntIndex(coll, path, unique: unique)
395     mc.successOnMainThread()
396   }
397 
removeStringIndexnull398   func removeStringIndex(_ mc: DbMethodCall) throws {
399     let coll = asString(mc.args[1])!
400     let path = asString(mc.args[2])!
401     let unique = asBoolean(mc.args[3])
402     try mc.db.removeStringIndex(coll, path, unique: unique)
403     mc.successOnMainThread()
404   }
405 
ensureStringIndexnull406   func ensureStringIndex(_ mc: DbMethodCall) throws {
407     let coll = asString(mc.args[1])!
408     let path = asString(mc.args[2])!
409     let unique = asBoolean(mc.args[3])
410     try mc.db.ensureStringIndex(coll, path, unique: unique)
411     mc.successOnMainThread()
412   }
413 
removeCollectionnull414   func removeCollection(_ mc: DbMethodCall) throws {
415     let coll = asString(mc.args[1])!
416     try mc.db.removeCollection(coll)
417     mc.successOnMainThread()
418   }
419 
renameCollectionnull420   func renameCollection(_ mc: DbMethodCall) throws {
421     let oldName = asString(mc.args[1])!
422     let newName = asString(mc.args[2])!
423     try mc.db.renameCollection(oldName, newName)
424     mc.successOnMainThread()
425   }
426 
delnull427   func del(_ mc: DbMethodCall) throws {
428     let coll = asString(mc.args[1])!
429     let id = asNumber(mc.args[2])!.int64Value
430     try mc.db.del(coll, id)
431     mc.successOnMainThread()
432   }
433 
patchnull434   func patch(_ mc: DbMethodCall) throws {
435     let db = mc.db
436     let coll = asString(mc.args[1])!
437     let json = asString(mc.args[2])!
438     let id = asNumber(mc.args[3])!.int64Value
439     let upsert = asBoolean(mc.args[4])
440     if upsert {
441       try db.put(coll, json, id, merge: true)
442     } else {
443       try db.patch(coll, json, id)
444     }
445     mc.successOnMainThread()
446   }
447 
getnull448   func get(_ mc: DbMethodCall) throws {
449     let db = mc.db
450     let coll = asString(mc.args[1])!
451     let id = asNumber(mc.args[2])!.int64Value
452     let doc = try db.get(coll, id)
453     mc.successOnMainThread(doc.json)
454   }
455 
putnull456   func put(_ mc: DbMethodCall) throws {
457     let db = mc.db
458     let coll = asString(mc.args[1])!
459     let json = asString(mc.args[2])!
460     let id = asNumber(mc.args[3])?.int64Value ?? 0
461     let res = try db.put(coll, json, id)
462     mc.successOnMainThread(res)
463   }
464 
infonull465   func info(_ mc: DbMethodCall) throws {
466     let info = try mc.db.info()
467     let data = String(data: try JSONEncoder().encode(info), encoding: .utf8)
468     mc.successOnMainThread(data)
469   }
470 
opennull471   func open(_ mc: DbMethodCall) throws {
472     var path = Path(mc.args[1] as! String)
473     if !path.isAbsolute {
474       let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
475       path = Path(documentsPath) + path
476     }
477     path = path.normalize()
478     let dbe = dbmap.values.first(where: { $0.path == path.string })
479     if dbe != nil {
480       dbe!.countOpen()
481       mc.successOnMainThread(dbe!.handle)
482       return
483     }
484     let cfg = mc.args[2] as? [String: Any] ?? [:]
485     var b = EJDB2Builder(path.string)
486     if cfg.containsKey("truncate") {
487       b = b.withTruncate(asBoolean(cfg["truncate"]))
488     }
489     if cfg.containsKey("readonly") {
490       b = b.withReadonly(asBoolean(cfg["readonly"]))
491     }
492     if cfg.containsKey("wal_enabled") {
493       b.withWalDisabled(!asBoolean(cfg["wal_enabled"], true))
494     }
495     if cfg.containsKey("wal_check_crc_on_checkpoint") {
496       b.withWalCheckCRCOnCheckpoint(asBoolean(cfg["wal_check_crc_on_checkpoint"]))
497     }
498     if cfg.containsKey("wal_checkpoint_buffer_sz") {
499       b.withWalCheckpointBufferSize(asNumber(cfg["wal_checkpoint_buffer_sz"])?.uint64Value ?? 0)
500     }
501     if cfg.containsKey("wal_checkpoint_timeout_sec") {
502       b.withWalCheckpointTimeout(asNumber(cfg["wal_checkpoint_timeout_sec"])?.uint32Value ?? 0)
503     }
504     if cfg.containsKey("wal_savepoint_timeout_sec") {
505       b.withWalSavepointTimeout(asNumber(cfg["wal_checkpoint_timeout_sec"])?.uint32Value ?? 0)
506     }
507     if cfg.containsKey("wal_wal_buffer_sz") {
508       b.withWalBufferSize(asNumber(cfg["wal_wal_buffer_sz"])?.intValue ?? 0)
509     }
510     if cfg.containsKey("document_buffer_sz") {
511       b.withDocumentBufferSize(asNumber(cfg["document_buffer_sz"])?.uint32Value ?? 0)
512     }
513     if cfg.containsKey("sort_buffer_sz") {
514       b.withSortBufferSize(asNumber(cfg["sort_buffer_sz"])?.uint32Value ?? 0)
515     }
516     dbkeys += 1
517     let key = dbkeys
518     dbmap[key] = try DbEntry(b.open(), key, path.string)
519     mc.successOnMainThread(key as NSNumber)
520   }
521 
closenull522   func close(_ mc: DbMethodCall) throws {
523     if try mc.dbe?.close() ?? false {
524       dbmap.removeValue(forKey: mc.dbe?.handle ?? 0)
525     }
526     mc.successOnMainThread()
527   }
528 
onListennull529   public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
530     self.events = events
531     return nil
532   }
533 
onCancelnull534   public func onCancel(withArguments arguments: Any?) -> FlutterError? {
535     self.events = nil
536     return nil
537   }
538 }
539