forked from fjl/go-couchdb
-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathdb.go
More file actions
371 lines (338 loc) · 11.1 KB
/
db.go
File metadata and controls
371 lines (338 loc) · 11.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
// Package couchdb implements wrappers for the CouchDB HTTP API.
//
// Unless otherwise noted, all functions in this package
// can be called from more than one goroutine at the same time.
package couchdb
import (
"bytes"
"context"
"encoding/json"
"net/http"
"reflect"
"strings"
)
// DB represents a remote CouchDB database.
type DB struct {
*transport
name string
ctx context.Context
}
// WithContext returns a new copy of the database object with the
// new context set. Use like:
//
// db.WithContext(ctx).Post(doc)
func (db *DB) WithContext(ctx context.Context) *DB {
db2 := new(DB)
*db2 = *db
db2.ctx = ctx
return db2
}
// Context provides the database objects current context.
func (db *DB) Context() context.Context {
return db.ctx
}
// Name returns the name of a database.
func (db *DB) Name() string {
return db.name
}
var getJsonKeys = []string{"open_revs", "atts_since"}
// Get retrieves a document from the given database.
// The document is unmarshalled into the given object.
// Some fields (like _conflicts) will only be returned if the
// options require it. Please refer to the CouchDB HTTP API documentation
// for more information.
//
// http://docs.couchdb.org/en/latest/api/document/common.html?highlight=doc#get--db-docid
func (db *DB) Get(id string, doc interface{}, opts Options) error {
path, err := optpath(opts, getJsonKeys, db.name, id)
if err != nil {
return err
}
resp, err := db.request(db.ctx, "GET", path, nil)
if err != nil {
return err
}
return readBody(resp, &doc)
}
// BulkGet retrieves several documents by their ID.
// It accepts a list of ID, a struct acting as a response type and an Options struct.
// It returns the list of found docs as a []interface{}, the list of docs not found as a []string and an eventual error.
// The found docs should be casted to the same type of docType.
func (db *DB) BulkGet(ids []string, docType interface{}, opts Options) (docs []interface{}, notFound []string, err error) {
path, err := optpath(opts, getJsonKeys, db.name, "_bulk_get")
if err != nil {
return nil, nil, err
}
request := &BulkGet{}
for _, id := range ids {
request.Docs = append(request.Docs, bulkID{ID: id})
}
bodyJson, err := json.Marshal(request)
body := bytes.NewReader(bodyJson)
resp, err := db.request(db.ctx, "POST", path, body)
if err != nil {
return nil, nil, err
}
response := bulkGetResp{}
err = readBody(resp, &response)
if err != nil {
return nil, nil, err
}
docTypeType := reflect.TypeOf(docType)
if docTypeType.Kind() == reflect.Ptr {
docTypeType = docTypeType.Elem()
}
for _, result := range response.Results {
if len(result.Docs) > 0 {
wrapper := result.Docs[0]
if wrapper.Error != nil || wrapper.Ok == nil {
notFound = append(notFound, result.Id)
} else if wrapper.Ok != nil {
foundDoc := reflect.New(docTypeType)
err := json.Unmarshal(wrapper.Ok, foundDoc.Interface())
if err != nil {
return nil, nil, err
}
docs = append(docs, foundDoc.Elem().Interface())
}
}
}
return docs, notFound, nil
}
// Rev fetches the current revision of a document.
// It is faster than an equivalent Get request because no body
// has to be parsed.
func (db *DB) Rev(id string) (string, error) {
return responseRev(db.closedRequest(db.ctx, "HEAD", path(db.name, id), nil))
}
// Post stores a new document into the given database.
func (db *DB) Post(doc interface{}) (id, rev string, err error) {
path := revpath("", db.name)
// TODO: make it possible to stream encoder output somehow
json, err := json.Marshal(doc)
if err != nil {
return "", "", err
}
b := bytes.NewReader(json)
resp, err := db.request(db.ctx, "POST", path, b)
if err != nil {
return "", "", err
}
return responseIDRev(resp)
}
// Put stores a document into the given database.
func (db *DB) Put(id string, doc interface{}, rev string) (newrev string, err error) {
path := revpath(rev, db.name, id)
// TODO: make it possible to stream encoder output somehow
json, err := json.Marshal(doc)
if err != nil {
return "", err
}
b := bytes.NewReader(json)
return responseRev(db.closedRequest(db.ctx, "PUT", path, b))
}
// BulkDocs allows to create, update and/or delete multiple documents in a single request.
// The basic operations are similar to creating or updating a single document,
// except that they are batched into one request.
//
// BulkDocs accepts an array of documents to be processed.
// Documents may contain _id, _rev and _deleted,
// depending on the wanted operation,
// as well as the corresponding document fields if needed.
//
// It returns a slice of results with the outcome of every operation or an error.
// The only mandatory field is the ID.
// The rest of the structure of the result depends if it was successful or not.
// Note that no error will be returned if an operation or more fail.
//
// Observe that behaviour of two or more operations in a single document is undetermined.
// There are no guarantees that the operations will be processed in any given order.
//
// Reference: https://cloud.ibm.com/docs/Cloudant?topic=Cloudant-documents#bulk-operations
func (db *DB) BulkDocs(docs ...interface{}) (res []BulkDocsResp, err error) {
path := revpath("", db.name, "_bulk_docs")
var req BulkDocsReq
req.Docs = make([]interface{}, 0, len(docs))
for _, doc := range docs {
req.Docs = append(req.Docs, doc)
}
bodyJSON, err := json.Marshal(req)
if err != nil {
return nil, err
}
body := bytes.NewReader(bodyJSON)
httpResp, err := db.request(db.ctx, http.MethodPost, path, body)
err = readBody(httpResp, &res)
if err != nil {
return nil, err
}
return res, nil
}
// Delete marks a document revision as deleted.
func (db *DB) Delete(id, rev string) (newrev string, err error) {
path := revpath(rev, db.name, id)
return responseRev(db.closedRequest(db.ctx, "DELETE", path, nil))
}
// Security represents database security objects.
type Security struct {
Admins Members `json:"admins"`
Members Members `json:"members"`
}
// Members represents member lists in database security objects.
type Members struct {
Names []string `json:"names,omitempty"`
Roles []string `json:"roles,omitempty"`
}
// Security retrieves the security object of a database.
func (db *DB) Security() (*Security, error) {
secobj := new(Security)
resp, err := db.request(db.ctx, "GET", path(db.name, "_security"), nil)
if err != nil {
return nil, err
}
if resp.ContentLength == 0 {
// empty reply means defaults
return secobj, nil
}
if err = readBody(resp, secobj); err != nil {
return nil, err
}
return secobj, nil
}
// PutSecurity sets the database security object.
func (db *DB) PutSecurity(secobj *Security) error {
json, _ := json.Marshal(secobj)
body := bytes.NewReader(json)
_, err := db.request(db.ctx, "PUT", path(db.name, "_security"), body)
return err
}
var viewJsonKeys = []string{"startkey", "start_key", "key", "endkey", "end_key", "keys"}
// View invokes a view.
// The ddoc parameter must be the name of the design document
// containing the view, but excluding the _design/ prefix.
//
// The output of the query is unmarshalled into the given result.
// The format of the result depends on the options. Please
// refer to the CouchDB HTTP API documentation for all the possible
// options that can be set.
//
// http://docs.couchdb.org/en/latest/api/ddoc/views.html
func (db *DB) View(ddoc, view string, result interface{}, opts Options) error {
ddoc = strings.Replace(ddoc, "_design/", "", 1)
path, err := optpath(opts, viewJsonKeys, db.name, "_design", ddoc, "_view", view)
if err != nil {
return err
}
resp, err := db.request(db.ctx, "GET", path, nil)
if err != nil {
return err
}
return readBody(resp, &result)
}
// PostView invokes a view.
// The ddoc parameter must be the name of the design document
// containing the view, but excluding the _design/ prefix.
//
// PostView functionality supports identical parameters and behavior
// as specified in the View function but allows for the query string
// parameters to be supplied as keys in a JSON object in the body of
// the POST request.
//
// The output of the query is unmarshalled into the given result.
// The format of the result depends on the options. Please
// refer to the CouchDB HTTP API documentation for all the possible
// options that can be set.
//
// Note it seems that only `keys` property is recognize on the payload
// the rest must go as query parameters
//
// http://docs.couchdb.org/en/latest/api/ddoc/views.html
func (db *DB) PostView(ddoc, view string, result interface{}, opts Options, payload Payload) error {
ddoc = strings.Replace(ddoc, "_design/", "", 1)
path, err := optpath(opts, viewJsonKeys, db.name, "_design", ddoc, "_view", view)
if err != nil {
return err
}
json, err := json.Marshal(payload)
if err != nil {
return err
}
body := bytes.NewReader(json)
resp, err := db.request(db.ctx, "POST", path, body)
if err != nil {
return err
}
return readBody(resp, &result)
}
// PostSearchIndex invokes a Search Index
// The ddoc parameter must be the name of the design document
// containing the index, but excluding the _design/ prefix.
//
// The output of the query is unmarshalled into the given result.
// The format of the result depends on the options. Please
// refer to the CouchDB HTTP API documentation for all the possible
// options that can be set.
//
// https://docs.couchdb.org/en/stable/ddocs/search.html
func (db *DB) PostSearchIndex(ddoc, index string, result interface{}, opts Options, payload Payload) error {
ddoc = strings.Replace(ddoc, "_design/", "", 1)
path, err := optpath(opts, viewJsonKeys, db.name, "_design", ddoc, "_search", index)
if err != nil {
return err
}
json, err := json.Marshal(payload)
if err != nil {
return err
}
body := bytes.NewReader(json)
resp, err := db.request(db.ctx, "POST", path, body)
if err != nil {
return err
}
return readBody(resp, &result)
}
// AllDocs invokes the _all_docs view of a database.
//
// The output of the query is unmarshalled into the given result.
// The format of the result depends on the options. Please
// refer to the CouchDB HTTP API documentation for all the possible
// options that can be set.
//
// http://docs.couchdb.org/en/latest/api/database/bulk-api.html#db-all-docs
func (db *DB) AllDocs(result interface{}, opts Options) error {
path, err := optpath(opts, viewJsonKeys, db.name, "_all_docs")
if err != nil {
return err
}
resp, err := db.request(db.ctx, "GET", path, nil)
if err != nil {
return err
}
return readBody(resp, &result)
}
// SyncDesign will attempt to create or update a design document on the provided
// database. This can be called multiple times for different databases,
// the latest Rev will always be fetched before storing the design.
func (db *DB) SyncDesign(d *Design) error {
// Get the previous design doc so we can compare and extract Rev if needed
prev := &Design{}
if err := db.Get(d.ID, prev, nil); err != nil {
if !NotFound(err) {
return err
}
}
if prev.Rev != "" {
if d.ViewChecksum() == prev.ViewChecksum() {
// nothing to do!
d.Rev = prev.Rev
return nil
}
}
d.Rev = "" // Prevent conflicts when switching databases
if rev, err := db.Put(d.ID, d, prev.Rev); err != nil {
return err
} else {
d.Rev = rev
}
return nil
}