github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/pkg/couchdb/changes.go (about)

     1  package couchdb
     2  
     3  import (
     4  	"encoding/json"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"net/http"
     9  
    10  	"github.com/cozy/cozy-stack/pkg/prefixer"
    11  	"github.com/google/go-querystring/query"
    12  )
    13  
    14  // ChangesFeedMode is a value for the feed parameter of a ChangesRequest
    15  type ChangesFeedMode string
    16  
    17  // ChangesFeedStyle is a value for the style parameter of a ChangesRequest
    18  type ChangesFeedStyle string
    19  
    20  const (
    21  	// ChangesModeNormal is the only mode supported by cozy-stack
    22  	ChangesModeNormal ChangesFeedMode = "normal"
    23  	// ChangesStyleAllDocs pass all revisions including conflicts
    24  	ChangesStyleAllDocs ChangesFeedStyle = "all_docs"
    25  	// ChangesStyleMainOnly only pass the winning revision
    26  	ChangesStyleMainOnly ChangesFeedStyle = "main_only"
    27  )
    28  
    29  // ValidChangesMode convert any string into a ChangesFeedMode or gives an error
    30  // if the string is invalid.
    31  func ValidChangesMode(feed string) (ChangesFeedMode, error) {
    32  	if feed == "" || feed == string(ChangesModeNormal) {
    33  		return ChangesModeNormal, nil
    34  	}
    35  
    36  	err := fmt.Errorf("Unsuported feed value '%s'", feed)
    37  	return ChangesModeNormal, err
    38  }
    39  
    40  // ValidChangesStyle convert any string into a ChangesFeedStyle or gives an
    41  // error if the string is invalid.
    42  func ValidChangesStyle(style string) (ChangesFeedStyle, error) {
    43  	if style == "" || style == string(ChangesStyleMainOnly) {
    44  		return ChangesStyleMainOnly, nil
    45  	}
    46  	if style == string(ChangesStyleAllDocs) {
    47  		return ChangesStyleAllDocs, nil
    48  	}
    49  	err := fmt.Errorf("Unsuported style value '%s'", style)
    50  	return ChangesStyleMainOnly, err
    51  }
    52  
    53  // StaticChangesFilter checks that the filter is static (ie doesn't depend of a
    54  // design doc)
    55  func StaticChangesFilter(filter string) (string, error) {
    56  	switch filter {
    57  	case "", "_doc_ids", "_selector", "_design":
    58  		return filter, nil
    59  	default:
    60  		return "", fmt.Errorf("Unsuported filter value '%s'", filter)
    61  	}
    62  }
    63  
    64  // A ChangesRequest are all parameters than can be passed to a changes feed
    65  type ChangesRequest struct {
    66  	DocType string `url:"-"`
    67  	// see Changes Feeds. Default is normal.
    68  	Feed ChangesFeedMode `url:"feed,omitempty"`
    69  	// Maximum period in milliseconds to wait for a change before the response
    70  	// is sent, even if there are no results. Only applicable for longpoll or
    71  	// continuous feeds. Default value is specified by httpd/changes_timeout
    72  	// configuration option. Note that 60000 value is also the default maximum
    73  	// timeout to prevent undetected dead connections.
    74  	Timeout int `url:"timeout,omitempty"`
    75  	// Period in milliseconds after which an empty line is sent in the results.
    76  	// Only applicable for longpoll, continuous, and eventsource feeds. Overrides
    77  	// any timeout to keep the feed alive indefinitely. Default is 60000. May be
    78  	// true to use default value.
    79  	Heartbeat int `url:"heartbeat,omitempty"`
    80  	// Includes conflicts information in response. Ignored if include_docs isn’t
    81  	// true. Default is false.
    82  	Conflicts bool `url:"conflicts,omitempty"`
    83  	// Return the change results in descending sequence order (most recent change
    84  	// first). Default is false.
    85  	Descending bool `url:"descending,omitempty"`
    86  	// Include the associated document with each result. If there are conflicts,
    87  	// only the winning revision is returned. Default is false.
    88  	IncludeDocs bool `url:"include_docs,omitempty"`
    89  	// Include the Base64-encoded content of attachments in the documents that
    90  	// are included if include_docs is true. Ignored if include_docs isn’t true.
    91  	// Default is false.
    92  	Attachments bool `url:"attachments,omitempty"`
    93  	// Include encoding information in attachment stubs if include_docs is true
    94  	// and the particular attachment is compressed. Ignored if include_docs isn’t
    95  	// true. Default is false.
    96  	AttEncodingInfo bool `url:"att_encoding_info,omitempty"`
    97  	// Alias of Last-Event-ID header.
    98  	LastEventID int `url:"last,omitempty"`
    99  	// Limit number of result rows to the specified value (note that using 0 here
   100  	//  has the same effect as 1).
   101  	Limit int `url:"limit,omitempty"`
   102  	// Start the results from the change immediately after the given update
   103  	// sequence. Can be valid update sequence or now value. Default is 0.
   104  	Since string `url:"since,omitempty"`
   105  	// Specifies how many revisions are returned in the changes array. The
   106  	// default, main_only, will only return the current “winning” revision;
   107  	// all_docs will return all leaf revisions (including conflicts and deleted
   108  	// former conflicts).
   109  	Style ChangesFeedStyle `url:"style,omitempty"`
   110  	// Reference to a filter function from a design document that will filter
   111  	// whole stream emitting only filtered events. See the section Change
   112  	// Notifications in the book CouchDB The Definitive Guide for more
   113  	// information.
   114  	Filter string `url:"filter,omitempty"`
   115  	// Allows to use view functions as filters. Documents counted as “passed” for
   116  	// view filter in case if map function emits at least one record for them.
   117  	// See _view for more info.
   118  	View string `url:"view,omitempty"`
   119  	// SeqInterval tells CouchDB to only calculate the update seq with every
   120  	// Nth result returned. It is used by PouchDB replication, and helps to
   121  	// lower the load on a CouchDB cluster.
   122  	SeqInterval int `url:"seq_interval,omitempty"`
   123  }
   124  
   125  // A ChangesResponse is the response provided by a GetChanges call
   126  type ChangesResponse struct {
   127  	LastSeq string   `json:"last_seq"` // Last change update sequence
   128  	Pending int      `json:"pending"`  // Count of remaining items in the feed
   129  	Results []Change `json:"results"`  // Changes made to a database
   130  }
   131  
   132  // A Change is an atomic change in couchdb
   133  type Change struct {
   134  	DocID   string  `json:"id"`
   135  	Seq     string  `json:"seq"`
   136  	Doc     JSONDoc `json:"doc,omitempty"`
   137  	Deleted bool    `json:"deleted,omitempty"`
   138  	Changes []struct {
   139  		Rev string `json:"rev"`
   140  	} `json:"changes"`
   141  }
   142  
   143  // GetChanges returns a list of changes in couchdb
   144  func GetChanges(db prefixer.Prefixer, req *ChangesRequest) (*ChangesResponse, error) {
   145  	if req.DocType == "" {
   146  		return nil, errors.New("Empty doctype in GetChanges")
   147  	}
   148  
   149  	v, err := query.Values(req)
   150  	if err != nil {
   151  		return nil, err
   152  	}
   153  
   154  	var response ChangesResponse
   155  	url := "_changes?" + v.Encode()
   156  	err = makeRequest(db, req.DocType, http.MethodGet, url, nil, &response)
   157  
   158  	if err != nil {
   159  		return nil, err
   160  	}
   161  	return &response, nil
   162  }
   163  
   164  // PostChanges returns a list of changes in couchdb
   165  func PostChanges(db prefixer.Prefixer, req *ChangesRequest, body io.ReadCloser) (*ChangesResponse, error) {
   166  	var payload json.RawMessage
   167  	if err := json.NewDecoder(body).Decode(&payload); err != nil {
   168  		return nil, err
   169  	}
   170  	if err := body.Close(); err != nil {
   171  		return nil, err
   172  	}
   173  
   174  	if req.DocType == "" {
   175  		return nil, errors.New("Empty doctype in GetChanges")
   176  	}
   177  
   178  	v, err := query.Values(req)
   179  	if err != nil {
   180  		return nil, err
   181  	}
   182  
   183  	var response ChangesResponse
   184  	url := "_changes?" + v.Encode()
   185  	err = makeRequest(db, req.DocType, http.MethodPost, url, payload, &response)
   186  
   187  	if err != nil {
   188  		return nil, err
   189  	}
   190  	return &response, nil
   191  }