github.com/go-kivik/kivik/v4@v4.3.2/pouchdb/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 pouchdb 16 17 import ( 18 "context" 19 "encoding/json" 20 "errors" 21 "fmt" 22 "net/http" 23 "net/url" 24 "strings" 25 "sync" 26 27 kivik "github.com/go-kivik/kivik/v4" 28 "github.com/go-kivik/kivik/v4/driver" 29 internal "github.com/go-kivik/kivik/v4/int/errors" 30 "github.com/go-kivik/kivik/v4/pouchdb/bindings" 31 ) 32 33 type pouchDriver struct{} 34 35 var _ driver.Driver = &pouchDriver{} 36 37 func init() { 38 kivik.Register("pouch", &pouchDriver{}) 39 } 40 41 // NewClient returns a PouchDB client handle. Provide a dsn only for remote 42 // databases. Otherwise specify "" 43 func (d *pouchDriver) NewClient(dsn string, options driver.Options) (driver.Client, error) { 44 var u *url.URL 45 var user *url.Userinfo 46 if dsn != "" { 47 var err error 48 u, err = url.Parse(dsn) 49 if err != nil { 50 return nil, fmt.Errorf("Invalid DSN URL '%s' provided: %s", dsn, err) 51 } 52 user = u.User 53 u.User = nil 54 } 55 pouch := bindings.GlobalPouchDB() 56 client := &client{ 57 dsn: u, 58 pouch: pouch, 59 opts: make(map[string]Options), 60 } 61 if user != nil { 62 pass, _ := user.Password() 63 client.setAuth(user.Username(), pass) 64 } 65 options.Apply(client) 66 return client, nil 67 } 68 69 func (c *client) setAuth(username, password string) { 70 c.opts["authenticator"] = Options{ 71 "auth": map[string]interface{}{ 72 "username": username, 73 "password": password, 74 }, 75 } 76 } 77 78 type client struct { 79 dsn *url.URL 80 opts map[string]Options 81 pouch *bindings.PouchDB 82 83 // This maintains a list of running replications 84 replications []*replication 85 replicationsMU sync.RWMutex 86 } 87 88 var _ driver.Client = &client{} 89 90 // AllDBs returns the list of all existing databases. This function depends on 91 // the pouchdb-all-dbs plugin being loaded. 92 func (c *client) AllDBs(ctx context.Context, _ driver.Options) ([]string, error) { 93 if c.dsn == nil { 94 return c.pouch.AllDBs(ctx) 95 } 96 return nil, errors.New("AllDBs() not implemented for remote PouchDB databases") 97 } 98 99 func (c *client) Version(context.Context) (*driver.Version, error) { 100 ver := c.pouch.Version() 101 return &driver.Version{ 102 Version: ver, 103 Vendor: "PouchDB", 104 RawResponse: json.RawMessage(fmt.Sprintf(`{"version":"%s","vendor":{"name":"PouchDB"}}`, ver)), 105 }, nil 106 } 107 108 func (c *client) dbURL(db string) string { 109 if c.dsn == nil { 110 // No transformation for local databases 111 return db 112 } 113 myURL := *c.dsn // Make a copy 114 myURL.Path += strings.TrimLeft(db, "/") 115 return myURL.String() 116 } 117 118 // Options is a struct of options, as documented in the PouchDB API. 119 type Options map[string]interface{} 120 121 func (c *client) options(options ...Options) Options { 122 o := Options{} 123 for _, defOpts := range c.opts { 124 for k, v := range defOpts { 125 o[k] = v 126 } 127 } 128 for _, opts := range options { 129 for k, v := range opts { 130 o[k] = v 131 } 132 } 133 return o 134 } 135 136 func (c *client) isRemote() bool { 137 return c.dsn != nil 138 } 139 140 // DBExists returns true if the requested DB exists. This function only works 141 // for remote databases. For local databases, it creates the database. 142 // Silly PouchDB. 143 func (c *client) DBExists(ctx context.Context, dbName string, options driver.Options) (bool, error) { 144 pouchOpts := map[string]interface{}{"skip_setup": true} 145 options.Apply(pouchOpts) 146 _, err := c.pouch.New(c.dbURL(dbName), pouchOpts).Info(ctx) 147 if err == nil { 148 return true, nil 149 } 150 if kivik.HTTPStatus(err) == http.StatusNotFound { 151 return false, nil 152 } 153 return false, err 154 } 155 156 func (c *client) CreateDB(ctx context.Context, dbName string, options driver.Options) error { 157 if c.isRemote() { 158 if exists, _ := c.DBExists(ctx, dbName, options); exists { 159 return &internal.Error{Status: http.StatusPreconditionFailed, Message: "database exists"} 160 } 161 } 162 pouchOpts := map[string]interface{}{} 163 options.Apply(pouchOpts) 164 _, err := c.pouch.New(c.dbURL(dbName), pouchOpts).Info(ctx) 165 return err 166 } 167 168 func (c *client) DestroyDB(ctx context.Context, dbName string, options driver.Options) error { 169 exists, err := c.DBExists(ctx, dbName, options) 170 if err != nil { 171 return err 172 } 173 if !exists { 174 // This will only ever do anything for a remote database 175 return &internal.Error{Status: http.StatusNotFound, Message: "database does not exist"} 176 } 177 pouchOpts := map[string]interface{}{} 178 options.Apply(pouchOpts) 179 return c.pouch.New(c.dbURL(dbName), pouchOpts).Destroy(ctx, nil) 180 } 181 182 func (c *client) DB(dbName string, options driver.Options) (driver.DB, error) { 183 pouchOpts := map[string]interface{}{} 184 options.Apply(pouchOpts) 185 return &db{ 186 // TODO: #68 Consider deferring this pouch.New call until the first use, 187 // so ctx can be used. 188 db: c.pouch.New(c.dbURL(dbName), pouchOpts), 189 client: c, 190 }, nil 191 }