1package kivik
2
3import (
4	"context"
5	"encoding/json"
6	"io"
7	"io/ioutil"
8	"reflect"
9	"strings"
10
11	"github.com/go-kivik/kivik/driver"
12	"github.com/go-kivik/kivik/errors"
13)
14
15// DB is a handle to a specific database.
16type DB struct {
17	client   *Client
18	name     string
19	driverDB driver.DB
20}
21
22// Client returns the Client used to connect to the database.
23func (db *DB) Client() *Client {
24	return db.client
25}
26
27// Name returns the database name as passed when creating the DB connection.
28func (db *DB) Name() string {
29	return db.name
30}
31
32// AllDocs returns a list of all documents in the database.
33func (db *DB) AllDocs(ctx context.Context, options ...Options) (*Rows, error) {
34	opts, err := mergeOptions(options...)
35	if err != nil {
36		return nil, err
37	}
38	rowsi, err := db.driverDB.AllDocs(ctx, opts)
39	if err != nil {
40		return nil, err
41	}
42	return newRows(ctx, rowsi), nil
43}
44
45// Query executes the specified view function from the specified design
46// document. ddoc and view may or may not be be prefixed with '_design/'
47// and '_view/' respectively. No other
48func (db *DB) Query(ctx context.Context, ddoc, view string, options ...Options) (*Rows, error) {
49	opts, err := mergeOptions(options...)
50	if err != nil {
51		return nil, err
52	}
53	ddoc = strings.TrimPrefix(ddoc, "_design/")
54	view = strings.TrimPrefix(view, "_view/")
55	rowsi, err := db.driverDB.Query(ctx, ddoc, view, opts)
56	if err != nil {
57		return nil, err
58	}
59	return newRows(ctx, rowsi), nil
60}
61
62// Row contains the result of calling Get for a single document. For most uses,
63// it is sufficient just to call the ScanDoc method. For more advanced uses, the
64// fields may be accessed directly.
65type Row struct {
66	// ContentLength records the size of the JSON representation of the document
67	// as requestd. The value -1 indicates that the length is unknown. Values
68	// >= 0 indicate that the given number of bytes may be read from Body.
69	ContentLength int64
70
71	// Rev is the revision ID of the returned document.
72	Rev string
73
74	// Body represents the document's content.
75	//
76	// Kivik will always return a non-nil Body, except when Err is non-nil. The
77	// ScanDoc method will close Body. When not using ScanDoc, it is the
78	// caller's responsibility to close Body
79	Body io.ReadCloser
80
81	// Err contains any error that occurred while fetching the document. It is
82	// typically returned by ScanDoc.
83	Err error
84
85	// Attachments is experimental
86	Attachments *AttachmentsIterator
87}
88
89// ScanDoc unmarshals the data from the fetched row into dest. It is an
90// intelligent wrapper around json.Unmarshal which also handles
91// multipart/related responses. When done, the underlying reader is closed.
92func (r *Row) ScanDoc(dest interface{}) error {
93	if r.Err != nil {
94		return r.Err
95	}
96	if reflect.TypeOf(dest).Kind() != reflect.Ptr {
97		return errNonPtr
98	}
99	defer r.Body.Close() // nolint: errcheck
100	return errors.WrapStatus(StatusBadResponse, json.NewDecoder(r.Body).Decode(dest))
101}
102
103// Get fetches the requested document. Any errors are deferred until the
104// row.ScanDoc call.
105func (db *DB) Get(ctx context.Context, docID string, options ...Options) *Row {
106	opts, err := mergeOptions(options...)
107	if err != nil {
108		return &Row{Err: err}
109	}
110	doc, err := db.driverDB.Get(ctx, docID, opts)
111	if err != nil {
112		return &Row{Err: err}
113	}
114	row := &Row{
115		ContentLength: doc.ContentLength,
116		Rev:           doc.Rev,
117		Body:          doc.Body,
118	}
119	if doc.Attachments != nil {
120		row.Attachments = &AttachmentsIterator{atti: doc.Attachments}
121	}
122	return row
123}
124
125// GetMeta returns the size and rev of the specified document. GetMeta accepts
126// the same options as the Get method.
127func (db *DB) GetMeta(ctx context.Context, docID string, options ...Options) (size int64, rev string, err error) {
128	opts, err := mergeOptions(options...)
129	if err != nil {
130		return 0, "", err
131	}
132	if r, ok := db.driverDB.(driver.MetaGetter); ok {
133		return r.GetMeta(ctx, docID, opts)
134	}
135	row := db.Get(ctx, docID, nil)
136	if row.Err != nil {
137		return 0, "", row.Err
138	}
139	if row.Rev != "" {
140		_ = row.Body.Close()
141		return row.ContentLength, row.Rev, nil
142	}
143	var doc struct {
144		Rev string `json:"_rev"`
145	}
146	// These last two lines cannot be combined for GopherJS due to a bug.
147	// See https://github.com/gopherjs/gopherjs/issues/608
148	err = row.ScanDoc(&doc)
149	return row.ContentLength, doc.Rev, err
150}
151
152// CreateDoc creates a new doc with an auto-generated unique ID. The generated
153// docID and new rev are returned.
154func (db *DB) CreateDoc(ctx context.Context, doc interface{}, options ...Options) (docID, rev string, err error) {
155	opts, err := mergeOptions(options...)
156	if err != nil {
157		return "", "", err
158	}
159	return db.driverDB.CreateDoc(ctx, doc, opts)
160}
161
162// normalizeFromJSON unmarshals a []byte, json.RawMessage or io.Reader to a
163// map[string]interface{}, or passed through any other types.
164func normalizeFromJSON(i interface{}) (interface{}, error) {
165	var body []byte
166	switch t := i.(type) {
167	case []byte:
168		body = t
169	case json.RawMessage:
170		body = t
171	default:
172		r, ok := i.(io.Reader)
173		if !ok {
174			return i, nil
175		}
176		var err error
177		body, err = ioutil.ReadAll(r)
178		if err != nil {
179			return nil, errors.WrapStatus(StatusUnknownError, err)
180		}
181	}
182	var x map[string]interface{}
183	if err := json.Unmarshal(body, &x); err != nil {
184		return nil, errors.WrapStatus(StatusBadRequest, err)
185	}
186	return x, nil
187}
188
189func extractDocID(i interface{}) (string, bool) {
190	if i == nil {
191		return "", false
192	}
193	var id string
194	var ok bool
195	switch t := i.(type) {
196	case map[string]interface{}:
197		id, ok = t["_id"].(string)
198	case map[string]string:
199		id, ok = t["_id"]
200	default:
201		data, err := json.Marshal(i)
202		if err != nil {
203			return "", false
204		}
205		var result struct {
206			ID string `json:"_id"`
207		}
208		if err := json.Unmarshal(data, &result); err != nil {
209			return "", false
210		}
211		id = result.ID
212		ok = result.ID != ""
213	}
214	if !ok {
215		return "", false
216	}
217	return id, true
218}
219
220// Put creates a new doc or updates an existing one, with the specified docID.
221// If the document already exists, the current revision must be included in doc,
222// with JSON key '_rev', otherwise a conflict will occur. The new rev is
223// returned.
224//
225// doc may be one of:
226//
227//  - An object to be marshaled to JSON. The resulting JSON structure must
228//    conform to CouchDB standards.
229//  - A []byte value, containing a valid JSON document
230//  - A json.RawMessage value containing a valid JSON document
231//  - An io.Reader, from which a valid JSON document may be read.
232func (db *DB) Put(ctx context.Context, docID string, doc interface{}, options ...Options) (rev string, err error) {
233	if docID == "" {
234		return "", missingArg("docID")
235	}
236	i, err := normalizeFromJSON(doc)
237	if err != nil {
238		return "", err
239	}
240	opts, err := mergeOptions(options...)
241	if err != nil {
242		return "", err
243	}
244	return db.driverDB.Put(ctx, docID, i, opts)
245}
246
247// Delete marks the specified document as deleted.
248func (db *DB) Delete(ctx context.Context, docID, rev string, options ...Options) (newRev string, err error) {
249	if docID == "" {
250		return "", missingArg("docID")
251	}
252	opts, err := mergeOptions(options...)
253	if err != nil {
254		return "", err
255	}
256	return db.driverDB.Delete(ctx, docID, rev, opts)
257}
258
259// Flush requests a flush of disk cache to disk or other permanent storage.
260//
261// See http://docs.couchdb.org/en/2.0.0/api/database/compact.html#db-ensure-full-commit
262func (db *DB) Flush(ctx context.Context) error {
263	if flusher, ok := db.driverDB.(driver.Flusher); ok {
264		return flusher.Flush(ctx)
265	}
266	return errors.Status(StatusNotImplemented, "kivik: flush not supported by driver")
267}
268
269// DBStats contains database statistics..
270type DBStats struct {
271	// Name is the name of the database.
272	Name string `json:"db_name"`
273	// CompactRunning is true if the database is currently being compacted.
274	CompactRunning bool `json:"compact_running"`
275	// DocCount is the number of documents are currently stored in the database.
276	DocCount int64 `json:"doc_count"`
277	// DeletedCount is a count of documents which have been deleted from the
278	// database.
279	DeletedCount int64 `json:"doc_del_count"`
280	// UpdateSeq is the current update sequence for the database.
281	UpdateSeq string `json:"update_seq"`
282	// DiskSize is the number of bytes used on-disk to store the database.
283	DiskSize int64 `json:"disk_size"`
284	// ActiveSize is the number of bytes used on-disk to store active documents.
285	// If this number is lower than DiskSize, then compaction would free disk
286	// space.
287	ActiveSize int64 `json:"data_size"`
288	// ExternalSize is the size of the documents in the database, as represented
289	// as JSON, before compression.
290	ExternalSize int64 `json:"-"`
291}
292
293// Stats returns database statistics.
294func (db *DB) Stats(ctx context.Context) (*DBStats, error) {
295	i, err := db.driverDB.Stats(ctx)
296	if err != nil {
297		return nil, err
298	}
299	stats := DBStats(*i)
300	return &stats, nil
301}
302
303// Compact begins compaction of the database. Check the CompactRunning field
304// returned by Info() to see if the compaction has completed.
305// See http://docs.couchdb.org/en/2.0.0/api/database/compact.html#db-compact
306func (db *DB) Compact(ctx context.Context) error {
307	return db.driverDB.Compact(ctx)
308}
309
310// CompactView compats the view indexes associated with the specified design
311// document.
312// See http://docs.couchdb.org/en/2.0.0/api/database/compact.html#db-compact-design-doc
313func (db *DB) CompactView(ctx context.Context, ddocID string) error {
314	return db.driverDB.CompactView(ctx, ddocID)
315}
316
317// ViewCleanup removes view index files that are no longer required as a result
318// of changed views within design documents.
319// See http://docs.couchdb.org/en/2.0.0/api/database/compact.html#db-view-cleanup
320func (db *DB) ViewCleanup(ctx context.Context) error {
321	return db.driverDB.ViewCleanup(ctx)
322}
323
324// Security returns the database's security document.
325// See http://couchdb.readthedocs.io/en/latest/api/database/security.html#get--db-_security
326func (db *DB) Security(ctx context.Context) (*Security, error) {
327	s, err := db.driverDB.Security(ctx)
328	if err != nil {
329		return nil, err
330	}
331	return &Security{
332		Admins:  Members(s.Admins),
333		Members: Members(s.Members),
334	}, err
335}
336
337// SetSecurity sets the database's security document.
338// See http://couchdb.readthedocs.io/en/latest/api/database/security.html#put--db-_security
339func (db *DB) SetSecurity(ctx context.Context, security *Security) error {
340	if security == nil {
341		return missingArg("security")
342	}
343	sec := &driver.Security{
344		Admins:  driver.Members(security.Admins),
345		Members: driver.Members(security.Members),
346	}
347	return db.driverDB.SetSecurity(ctx, sec)
348}
349
350// Copy copies the source document to a new document with an ID of targetID. If
351// the database backend does not support COPY directly, the operation will be
352// emulated with a Get followed by Put. The target will be an exact copy of the
353// source, with only the ID and revision changed.
354//
355// See http://docs.couchdb.org/en/2.0.0/api/document/common.html#copy--db-docid
356func (db *DB) Copy(ctx context.Context, targetID, sourceID string, options ...Options) (targetRev string, err error) {
357	if targetID == "" {
358		return "", missingArg("targetID")
359	}
360	if sourceID == "" {
361		return "", missingArg("sourceID")
362	}
363	opts, err := mergeOptions(options...)
364	if err != nil {
365		return "", err
366	}
367	if copier, ok := db.driverDB.(driver.Copier); ok {
368		return copier.Copy(ctx, targetID, sourceID, opts)
369	}
370	var doc map[string]interface{}
371	if err = db.Get(ctx, sourceID, opts).ScanDoc(&doc); err != nil {
372		return "", err
373	}
374	delete(doc, "_rev")
375	doc["_id"] = targetID
376	delete(opts, "rev") // rev has a completely different meaning for Copy and Put
377	return db.Put(ctx, targetID, doc, opts)
378}
379
380// PutAttachment uploads the supplied content as an attachment to the specified
381// document.
382func (db *DB) PutAttachment(ctx context.Context, docID, rev string, att *Attachment, options ...Options) (newRev string, err error) {
383	if docID == "" {
384		return "", missingArg("docID")
385	}
386	if e := att.validate(); e != nil {
387		return "", e
388	}
389	opts, err := mergeOptions(options...)
390	if err != nil {
391		return "", err
392	}
393	a := driver.Attachment(*att)
394	return db.driverDB.PutAttachment(ctx, docID, rev, &a, opts)
395}
396
397// GetAttachment returns a file attachment associated with the document.
398func (db *DB) GetAttachment(ctx context.Context, docID, rev, filename string, options ...Options) (*Attachment, error) {
399	if docID == "" {
400		return nil, missingArg("docID")
401	}
402	if filename == "" {
403		return nil, missingArg("filename")
404	}
405	opts, e := mergeOptions(options...)
406	if e != nil {
407		return nil, e
408	}
409	att, err := db.driverDB.GetAttachment(ctx, docID, rev, filename, opts)
410	if err != nil {
411		return nil, err
412	}
413	a := Attachment(*att)
414	return &a, nil
415}
416
417type nilContentReader struct{}
418
419var _ io.ReadCloser = &nilContentReader{}
420
421func (c nilContentReader) Read(_ []byte) (int, error) { return 0, io.EOF }
422func (c nilContentReader) Close() error               { return nil }
423
424var nilContent = nilContentReader{}
425
426// GetAttachmentMeta returns meta data about an attachment. The attachment
427// content returned will be empty.
428func (db *DB) GetAttachmentMeta(ctx context.Context, docID, rev, filename string, options ...Options) (*Attachment, error) {
429	if docID == "" {
430		return nil, missingArg("docID")
431	}
432	if filename == "" {
433		return nil, missingArg("filename")
434	}
435	var att *Attachment
436	if metaer, ok := db.driverDB.(driver.AttachmentMetaGetter); ok {
437		opts, err := mergeOptions(options...)
438		if err != nil {
439			return nil, err
440		}
441		a, err := metaer.GetAttachmentMeta(ctx, docID, rev, filename, opts)
442		if err != nil {
443			return nil, err
444		}
445		att = new(Attachment)
446		*att = Attachment(*a)
447	} else {
448		var err error
449		att, err = db.GetAttachment(ctx, docID, rev, filename, options...)
450		if err != nil {
451			return nil, err
452		}
453	}
454	if att.Content != nil {
455		_ = att.Content.Close() // Ensure this is closed
456	}
457	att.Content = nilContent
458	return att, nil
459}
460
461// DeleteAttachment delets an attachment from a document, returning the
462// document's new revision.
463func (db *DB) DeleteAttachment(ctx context.Context, docID, rev, filename string, options ...Options) (newRev string, err error) {
464	if docID == "" {
465		return "", missingArg("docID")
466	}
467	if filename == "" {
468		return "", missingArg("filename")
469	}
470	opts, err := mergeOptions(options...)
471	if err != nil {
472		return "", err
473	}
474	return db.driverDB.DeleteAttachment(ctx, docID, rev, filename, opts)
475}
476