github.com/go-kivik/kivik/v4@v4.3.2/pouchdb/bindings/pouchdb.go (about) 1 // Licensed under the Apache License, Version 2.0 (the "License"); you may not 2 // use this file except in compliance with the License. You may obtain a copy of 3 // the License at 4 // 5 // http://www.apache.org/licenses/LICENSE-2.0 6 // 7 // Unless required by applicable law or agreed to in writing, software 8 // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 // License for the specific language governing permissions and limitations under 11 // the License. 12 13 //go:build js 14 15 // Package bindings provides minimal GopherJS bindings around the PouchDB 16 // library. (https://pouchdb.com/api.html) 17 package bindings 18 19 import ( 20 "bytes" 21 "context" 22 "encoding/json" 23 "errors" 24 "io" 25 "net/http" 26 "reflect" 27 "time" 28 29 "github.com/gopherjs/gopherjs/js" 30 "github.com/gopherjs/jsbuiltin" 31 32 internal "github.com/go-kivik/kivik/v4/int/errors" 33 ) 34 35 // DB is a PouchDB database object. 36 type DB struct { 37 *js.Object 38 } 39 40 // PouchDB represents a PouchDB constructor. 41 type PouchDB struct { 42 *js.Object 43 } 44 45 // GlobalPouchDB returns the global PouchDB object. 46 func GlobalPouchDB() *PouchDB { 47 return &PouchDB{Object: js.Global.Get("PouchDB")} 48 } 49 50 // Defaults returns a new PouchDB constructor with the specified default options. 51 // See https://pouchdb.com/api.html#defaults 52 func Defaults(options map[string]interface{}) *PouchDB { 53 return &PouchDB{Object: js.Global.Get("PouchDB").Call("defaults", options)} 54 } 55 56 // New creates a database or opens an existing one. 57 // 58 // See https://pouchdb.com/api.html#create_database 59 func (p *PouchDB) New(dbName string, options map[string]interface{}) *DB { 60 db := &DB{Object: p.Object.New(dbName, options)} 61 if db.indexeddb() { 62 /* Without blocking here, we get the following error. This may be related 63 to a sleep in PouchDB, that has a mysterious note about why it exists. 64 https://github.com/pouchdb/pouchdb/blob/27ab3b27a6673038b449313d9700b3a7977ac091/packages/node_modules/pouchdb-adapter-indexeddb/src/index.js#L156-L160 65 66 /home/jonhall/src/kivik/pouchdb/node_modules/pouchdb-adapter-indexeddb/lib/index.js:1597 67 doc.rev_tree = pouchdbMerge.removeLeafFromTree(doc.rev_tree, rev); 68 ^ 69 TypeError: Cannot read properties of undefined (reading 'rev_tree') 70 at FDBRequest.docStore.get.onsuccess (/home/jonhall/src/kivik/pouchdb/node_modules/pouchdb-adapter-indexeddb/lib/index.js:1597:58) 71 at invokeEventListeners (/home/jonhall/src/kivik/pouchdb/node_modules/fake-indexeddb/build/cjs/lib/FakeEventTarget.js:55:25) 72 at FDBRequest.dispatchEvent (/home/jonhall/src/kivik/pouchdb/node_modules/fake-indexeddb/build/cjs/lib/FakeEventTarget.js:99:7) 73 at FDBTransaction._start (/home/jonhall/src/kivik/pouchdb/node_modules/fake-indexeddb/build/cjs/FDBTransaction.js:210:19) 74 at Immediate.<anonymous> (/home/jonhall/src/kivik/pouchdb/node_modules/fake-indexeddb/build/cjs/lib/Database.js:38:16) 75 at processImmediate (node:internal/timers:466:21) 76 */ 77 time.Sleep(0) 78 } 79 return db 80 } 81 82 // Version returns the version of the currently running PouchDB library. 83 func (p *PouchDB) Version() string { 84 return p.Get("version").String() 85 } 86 87 func setTimeout(ctx context.Context, options map[string]interface{}) map[string]interface{} { 88 if ctx == nil { // Just to be safe 89 return options 90 } 91 deadline, ok := ctx.Deadline() 92 if !ok { 93 return options 94 } 95 if options == nil { 96 options = make(map[string]interface{}) 97 } 98 if _, ok := options["ajax"]; !ok { 99 options["ajax"] = make(map[string]interface{}) 100 } 101 ajax := options["ajax"].(map[string]interface{}) 102 timeout := int(time.Until(deadline) * 1000) //nolint:gomnd 103 // Used by ajax calls 104 ajax["timeout"] = timeout 105 // Used by changes and replications 106 options["timeout"] = timeout 107 return options 108 } 109 110 type caller interface { 111 Call(string, ...interface{}) *js.Object 112 } 113 114 // prepareArgs trims any trailing nil values, since JavaScript treats null as 115 // distinct from an omitted value. 116 func prepareArgs(args []interface{}) []interface{} { 117 for len(args) > 0 { 118 if !omitNil(args[len(args)-1]) { 119 break 120 } 121 args = args[:len(args)-1] 122 } 123 return args 124 } 125 126 // omitNil returns true if a is a nil value that should be omitted as an 127 // argument to a JavaScript function. 128 func omitNil(a interface{}) bool { 129 if a == nil { 130 // a literal nil value should be converted to a null, so we don't omit 131 return false 132 } 133 v := reflect.ValueOf(a) 134 switch v.Kind() { 135 case reflect.Slice, reflect.Interface, reflect.Map, reflect.Ptr: 136 // nil slices, interfaces, maps, and pointers in our context mean that 137 // we have a nil option that in JS idioms would just be omitted as an 138 // argument, so return true. 139 return v.IsNil() 140 } 141 return false 142 } 143 144 // callBack executes the 'method' of 'o' as a callback, setting result to the 145 // callback's return value. An error is returned if either the callback returns 146 // an error, or if the context is cancelled. No attempt is made to abort the 147 // callback in the case that the context is cancelled. 148 func callBack(ctx context.Context, o caller, method string, args ...interface{}) (r *js.Object, e error) { 149 defer RecoverError(&e) 150 resultCh := make(chan *js.Object) 151 var err error 152 o.Call(method, prepareArgs(args)...).Call("then", func(r *js.Object) { 153 go func() { resultCh <- r }() 154 }).Call("catch", func(e *js.Object) { 155 err = NewPouchError(e) 156 close(resultCh) 157 }) 158 select { 159 case <-ctx.Done(): 160 return nil, ctx.Err() 161 case result := <-resultCh: 162 return result, err 163 } 164 } 165 166 // AllDBs returns the list of all existing (undeleted) databases. 167 func (p *PouchDB) AllDBs(ctx context.Context) ([]string, error) { 168 if jsbuiltin.TypeOf(p.Get("allDbs")) != jsbuiltin.TypeFunction { 169 return nil, errors.New("pouchdb-all-dbs plugin not loaded") 170 } 171 result, err := callBack(ctx, p, "allDbs") 172 if err != nil { 173 return nil, err 174 } 175 if result == js.Undefined { 176 return nil, nil 177 } 178 allDBs := make([]string, result.Length()) 179 for i := range allDBs { 180 allDBs[i] = result.Index(i).String() 181 } 182 return allDBs, nil 183 } 184 185 // DBInfo is a struct representing information about a specific database. 186 type DBInfo struct { 187 *js.Object 188 Name string `js:"db_name"` 189 DocCount int64 `js:"doc_count"` 190 UpdateSeq string `js:"update_seq"` 191 } 192 193 // Info returns info about the database. 194 func (db *DB) Info(ctx context.Context) (*DBInfo, error) { 195 result, err := callBack(ctx, db, "info") 196 return &DBInfo{Object: result}, err 197 } 198 199 // Put creates a new document or update an existing document. 200 // See https://pouchdb.com/api.html#create_document 201 func (db *DB) Put(ctx context.Context, doc interface{}, opts map[string]interface{}) (rev string, err error) { 202 result, err := callBack(ctx, db, "put", doc, setTimeout(ctx, opts)) 203 if err != nil { 204 return "", err 205 } 206 return result.Get("rev").String(), nil 207 } 208 209 // Post creates a new document and lets PouchDB auto-generate the ID. 210 // See https://pouchdb.com/api.html#using-dbpost 211 func (db *DB) Post(ctx context.Context, doc interface{}, opts map[string]interface{}) (docID, rev string, err error) { 212 result, err := callBack(ctx, db, "post", doc, setTimeout(ctx, opts)) 213 if err != nil { 214 return "", "", err 215 } 216 return result.Get("id").String(), result.Get("rev").String(), nil 217 } 218 219 // Get fetches the requested document from the database. 220 // See https://pouchdb.com/api.html#fetch_document 221 func (db *DB) Get(ctx context.Context, docID string, opts map[string]interface{}) (doc []byte, rev string, err error) { 222 result, err := callBack(ctx, db, "get", docID, setTimeout(ctx, opts)) 223 if err != nil { 224 return nil, "", err 225 } 226 resultJSON := js.Global.Get("JSON").Call("stringify", result).String() 227 return []byte(resultJSON), result.Get("_rev").String(), err 228 } 229 230 // Delete marks a document as deleted. 231 // See https://pouchdb.com/api.html#delete_document 232 func (db *DB) Delete(ctx context.Context, docID, rev string, opts map[string]interface{}) (newRev string, err error) { 233 result, err := callBack(ctx, db, "remove", docID, rev, setTimeout(ctx, opts)) 234 if err != nil { 235 return "", err 236 } 237 return result.Get("rev").String(), nil 238 } 239 240 func (db *DB) indexeddb() bool { 241 return db.Object.Get("__opts").Get("adapter").String() == "indexeddb" 242 } 243 244 // Purge purges a specific document revision. It returns a list of successfully 245 // purged revisions. This method is only supported by the IndexedDB adaptor, and 246 // all others return an error. 247 func (db *DB) Purge(ctx context.Context, docID, rev string) ([]string, error) { 248 if db.Object.Get("purge") == js.Undefined { 249 return nil, &internal.Error{Status: http.StatusNotImplemented, Message: "kivik: purge supported by PouchDB 8 or newer"} 250 } 251 if !db.indexeddb() { 252 return nil, &internal.Error{Status: http.StatusNotImplemented, Message: "kivik: purge only supported with indexedDB adapter"} 253 } 254 result, err := callBack(ctx, db, "purge", docID, rev, setTimeout(ctx, nil)) 255 if err != nil { 256 return nil, err 257 } 258 delRevs := result.Get("deletedRevs") 259 revs := make([]string, delRevs.Length()) 260 for i := range revs { 261 revs[i] = delRevs.Index(i).String() 262 } 263 return revs, nil 264 } 265 266 // Destroy destroys the database. 267 func (db *DB) Destroy(ctx context.Context, options map[string]interface{}) error { 268 _, err := callBack(ctx, db, "destroy", setTimeout(ctx, options)) 269 return err 270 } 271 272 // AllDocs returns a list of all documents in the database. 273 func (db *DB) AllDocs(ctx context.Context, options map[string]interface{}) (*js.Object, error) { 274 return callBack(ctx, db, "allDocs", setTimeout(ctx, options)) 275 } 276 277 // Query queries a map/reduce function. 278 func (db *DB) Query(ctx context.Context, ddoc, view string, options map[string]interface{}) (*js.Object, error) { 279 o := setTimeout(ctx, options) 280 return callBack(ctx, db, "query", ddoc+"/"+view, o) 281 } 282 283 const errFindPluginNotLoaded = internal.CompositeError("501 pouchdb-find plugin not loaded") 284 285 // Find executes a MongoDB-style find query with the pouchdb-find plugin, if it 286 // is installed. If the plugin is not installed, a NotImplemented error will be 287 // returned. 288 // 289 // See https://github.com/pouchdb/pouchdb/tree/master/packages/node_modules/pouchdb-find#dbfindrequest--callback 290 func (db *DB) Find(ctx context.Context, query interface{}) (*js.Object, error) { 291 if jsbuiltin.TypeOf(db.Object.Get("find")) != jsbuiltin.TypeFunction { 292 return nil, errFindPluginNotLoaded 293 } 294 queryObj, err := Objectify(query) 295 if err != nil { 296 return nil, err 297 } 298 return callBack(ctx, db, "find", queryObj) 299 } 300 301 // Objectify unmarshals a string, []byte, or json.RawMessage into an interface{}. 302 // All other types are just passed through. 303 func Objectify(i interface{}) (interface{}, error) { 304 var buf []byte 305 switch t := i.(type) { 306 case string: 307 buf = []byte(t) 308 case []byte: 309 buf = t 310 case json.RawMessage: 311 buf = t 312 default: 313 return i, nil 314 } 315 var x interface{} 316 err := json.Unmarshal(buf, &x) 317 if err != nil { 318 err = &internal.Error{Status: http.StatusBadRequest, Err: err} 319 } 320 return x, err 321 } 322 323 // Compact compacts the database, and waits for it to complete. This may take 324 // a long time! Please wrap this call in a goroutine. 325 func (db *DB) Compact() error { 326 _, err := callBack(context.Background(), db, "compact") 327 return err 328 } 329 330 // ViewCleanup cleans up views, and waits for it to complete. This may take a 331 // long time! Please wrap this call in a goroutine. 332 func (db *DB) ViewCleanup() error { 333 _, err := callBack(context.Background(), db, "viewCleanup") 334 return err 335 } 336 337 var jsJSON = js.Global.Get("JSON") 338 339 // BulkDocs creates, updates, or deletes docs in bulk. 340 // See https://pouchdb.com/api.html#batch_create 341 func (db *DB) BulkDocs(ctx context.Context, docs []interface{}, options map[string]interface{}) (result *js.Object, err error) { 342 defer RecoverError(&err) 343 jsDocs := make([]*js.Object, len(docs)) 344 for i, doc := range docs { 345 jsonDoc, err := json.Marshal(doc) 346 if err != nil { 347 return nil, err 348 } 349 jsDocs[i] = jsJSON.Call("parse", string(jsonDoc)) 350 } 351 if options == nil { 352 return callBack(ctx, db, "bulkDocs", jsDocs, setTimeout(ctx, nil)) 353 } 354 return callBack(ctx, db, "bulkDocs", jsDocs, options, setTimeout(ctx, nil)) 355 } 356 357 // Changes returns an event emitter object. 358 // 359 // See https://pouchdb.com/api.html#changes 360 func (db *DB) Changes(ctx context.Context, options map[string]interface{}) (changes *js.Object, e error) { 361 defer RecoverError(&e) 362 return db.Call("changes", setTimeout(ctx, options)), nil 363 } 364 365 // PutAttachment attaches a binary object to a document. 366 // 367 // See https://pouchdb.com/api.html#save_attachment 368 func (db *DB) PutAttachment(ctx context.Context, docID, filename, rev string, body io.Reader, ctype string) (*js.Object, error) { 369 att, err := attachmentObject(ctype, body) 370 if err != nil { 371 return nil, err 372 } 373 if rev == "" { 374 return callBack(ctx, db, "putAttachment", docID, filename, att, ctype) 375 } 376 return callBack(ctx, db, "putAttachment", docID, filename, rev, att, ctype) 377 } 378 379 // attachmentObject converts an io.Reader to a JavaScript Buffer in node, or 380 // a Blob in the browser 381 func attachmentObject(contentType string, content io.Reader) (att *js.Object, err error) { 382 RecoverError(&err) 383 buf := new(bytes.Buffer) 384 if _, err := buf.ReadFrom(content); err != nil { 385 return nil, err 386 } 387 if buffer := js.Global.Get("Buffer"); jsbuiltin.TypeOf(buffer) == jsbuiltin.TypeFunction { 388 // The Buffer type is supported, so we'll use that 389 if jsbuiltin.TypeOf(buffer.Get("from")) == jsbuiltin.TypeFunction { 390 // For newer versions of Node.js. See https://nodejs.org/fa/docs/guides/buffer-constructor-deprecation/ 391 return buffer.Call("from", buf.String()), nil 392 } 393 // Fall back to legacy Buffer constructor. 394 return buffer.New(buf.String()), nil 395 } 396 if js.Global.Get("Blob") != js.Undefined { 397 // We have Blob support, must be in a browser 398 return js.Global.Get("Blob").New([]interface{}{buf.Bytes()}, map[string]string{"type": contentType}), nil 399 } 400 // Not sure what to do 401 return nil, errors.New("No Blob or Buffer support?!?") 402 } 403 404 // GetAttachment returns attachment data. 405 // 406 // See https://pouchdb.com/api.html#get_attachment 407 func (db *DB) GetAttachment(ctx context.Context, docID, filename string, options map[string]interface{}) (*js.Object, error) { 408 return callBack(ctx, db, "getAttachment", docID, filename, setTimeout(ctx, options)) 409 } 410 411 // RemoveAttachment deletes an attachment from a document. 412 // 413 // See https://pouchdb.com/api.html#delete_attachment 414 func (db *DB) RemoveAttachment(ctx context.Context, docID, filename, rev string) (*js.Object, error) { 415 return callBack(ctx, db, "removeAttachment", docID, filename, rev) 416 } 417 418 // CreateIndex creates an index to be used by MongoDB-style queries with the 419 // pouchdb-find plugin, if it is installed. If the plugin is not installed, a 420 // NotImplemented error will be returned. 421 // 422 // See https://github.com/pouchdb/pouchdb/tree/master/packages/node_modules/pouchdb-find#dbcreateindexindex--callback 423 func (db *DB) CreateIndex(ctx context.Context, index interface{}) (*js.Object, error) { 424 if jsbuiltin.TypeOf(db.Object.Get("find")) != jsbuiltin.TypeFunction { 425 return nil, errFindPluginNotLoaded 426 } 427 return callBack(ctx, db, "createIndex", index) 428 } 429 430 // GetIndexes returns the list of currently defined indexes on the database. 431 // 432 // See https://github.com/pouchdb/pouchdb/tree/master/packages/node_modules/pouchdb-find#dbgetindexescallback 433 func (db *DB) GetIndexes(ctx context.Context) (*js.Object, error) { 434 if jsbuiltin.TypeOf(db.Object.Get("find")) != jsbuiltin.TypeFunction { 435 return nil, errFindPluginNotLoaded 436 } 437 return callBack(ctx, db, "getIndexes") 438 } 439 440 // DeleteIndex deletes an index used by the MongoDB-style queries with the 441 // pouchdb-find plugin, if it is installed. If the plugin is not installed, a 442 // NotImplemented error will be returned. 443 // 444 // See: https://github.com/pouchdb/pouchdb/tree/master/packages/node_modules/pouchdb-find#dbdeleteindexindex--callback 445 func (db *DB) DeleteIndex(ctx context.Context, index interface{}) (*js.Object, error) { 446 if jsbuiltin.TypeOf(db.Object.Get("find")) != jsbuiltin.TypeFunction { 447 return nil, errFindPluginNotLoaded 448 } 449 return callBack(ctx, db, "deleteIndex", index) 450 } 451 452 // Replication events 453 const ( 454 ReplicationEventChange = "change" 455 ReplicationEventComplete = "complete" 456 ReplicationEventPaused = "paused" 457 ReplicationEventActive = "active" 458 ReplicationEventDenied = "denied" 459 ReplicationEventError = "error" 460 ) 461 462 // Replicate initiates a replication. 463 // See https://pouchdb.com/api.html#replication 464 func (p *PouchDB) Replicate(source, target interface{}, options map[string]interface{}) (result *js.Object, err error) { 465 defer RecoverError(&err) 466 return p.Call("replicate", source, target, options), nil 467 } 468 469 // Explain the query plan for a given query 470 // 471 // See https://pouchdb.com/api.html#explain_index 472 func (db *DB) Explain(ctx context.Context, query interface{}) (*js.Object, error) { 473 if jsbuiltin.TypeOf(db.Object.Get("find")) != jsbuiltin.TypeFunction { 474 return nil, errFindPluginNotLoaded 475 } 476 queryObj, err := Objectify(query) 477 if err != nil { 478 return nil, err 479 } 480 return callBack(ctx, db, "explain", queryObj) 481 } 482 483 // Close closes the underlying db object. 484 func (db *DB) Close() error { 485 // I'm not sure when DB.close() was added to PouchDB, so guard against 486 // it missing, just in case. 487 if jsbuiltin.TypeOf(db.Object.Get("close")) != jsbuiltin.TypeFunction { 488 return nil 489 } 490 _, err := callBack(context.Background(), db, "close") 491 return err 492 }