...

Source file src/github.com/go-kivik/kivik/v4/pouchdb/bindings/pouchdb.go

Documentation: github.com/go-kivik/kivik/v4/pouchdb/bindings

     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  }
   493  

View as plain text