package http import ( "context" "fmt" "net/http" "path" "github.com/influxdata/httprouter" "github.com/influxdata/influxdb/v2" "github.com/influxdata/influxdb/v2/kit/platform" "github.com/influxdata/influxdb/v2/kit/platform/errors" "github.com/influxdata/influxdb/v2/kit/tracing" "github.com/influxdata/influxdb/v2/pkg/httpc" "go.uber.org/zap" ) const prefixDocuments = "/api/v2/documents" // DocumentService is an interface HTTP-exposed portion of the document service. type DocumentService interface { GetDocuments(ctx context.Context, namespace string, orgID platform.ID) ([]*influxdb.Document, error) } // DocumentBackend is all services and associated parameters required to construct // the DocumentHandler. type DocumentBackend struct { log *zap.Logger errors.HTTPErrorHandler DocumentService influxdb.DocumentService } // NewDocumentBackend returns a new instance of DocumentBackend. func NewDocumentBackend(log *zap.Logger, b *APIBackend) *DocumentBackend { return &DocumentBackend{ HTTPErrorHandler: b.HTTPErrorHandler, log: log, DocumentService: b.DocumentService, } } // DocumentHandler represents an HTTP API handler for documents. type DocumentHandler struct { *httprouter.Router log *zap.Logger errors.HTTPErrorHandler DocumentService influxdb.DocumentService LabelService influxdb.LabelService } const ( documentsPath = "/api/v2/documents/:ns" ) // NewDocumentHandler returns a new instance of DocumentHandler. // TODO(desa): this should probably take a namespace func NewDocumentHandler(b *DocumentBackend) *DocumentHandler { h := &DocumentHandler{ Router: NewRouter(b.HTTPErrorHandler), HTTPErrorHandler: b.HTTPErrorHandler, log: b.log, DocumentService: b.DocumentService, } h.HandlerFunc("GET", documentsPath, h.handleGetDocuments) return h } type documentResponse struct { Links map[string]string `json:"links"` *influxdb.Document } func newDocumentResponse(ns string, d *influxdb.Document) *documentResponse { if d.Labels == nil { d.Labels = []*influxdb.Label{} } return &documentResponse{ Links: map[string]string{ "self": fmt.Sprintf("/api/v2/documents/%s/%s", ns, d.ID), }, Document: d, } } type documentsResponse struct { Documents []*documentResponse `json:"documents"` } func newDocumentsResponse(ns string, docs []*influxdb.Document) *documentsResponse { ds := make([]*documentResponse, 0, len(docs)) for _, doc := range docs { ds = append(ds, newDocumentResponse(ns, doc)) } return &documentsResponse{ Documents: ds, } } // handleGetDocuments is the HTTP handler for the GET /api/v2/documents/:ns route. func (h *DocumentHandler) handleGetDocuments(w http.ResponseWriter, r *http.Request) { ctx := r.Context() req, err := decodeGetDocumentsRequest(ctx, r) if err != nil { h.HandleHTTPError(ctx, err, w) return } s, err := h.DocumentService.FindDocumentStore(ctx, req.Namespace) if err != nil { h.HandleHTTPError(ctx, err, w) return } ds, err := s.FindDocuments(ctx, req.OrgID) if err != nil { h.HandleHTTPError(ctx, err, w) return } h.log.Debug("Documents retrieved", zap.String("documents", fmt.Sprint(ds))) if err := encodeResponse(ctx, w, http.StatusOK, newDocumentsResponse(req.Namespace, ds)); err != nil { logEncodingError(h.log, r, err) return } } type getDocumentsRequest struct { Namespace string Org string OrgID platform.ID } func decodeGetDocumentsRequest(ctx context.Context, r *http.Request) (*getDocumentsRequest, error) { params := httprouter.ParamsFromContext(ctx) ns := params.ByName("ns") if ns == "" { return nil, &errors.Error{ Code: errors.EInvalid, Msg: "url missing namespace", } } qp := r.URL.Query() req := &getDocumentsRequest{ Namespace: ns, Org: qp.Get("org"), } if oidStr := qp.Get("orgID"); oidStr != "" { oid, err := platform.IDFromString(oidStr) if err != nil { return nil, &errors.Error{ Code: errors.EInvalid, Msg: "Invalid orgID", } } req.OrgID = *oid } return req, nil } type documentService struct { Client *httpc.Client } // NewDocumentService creates a client to connect to Influx via HTTP to manage documents. func NewDocumentService(client *httpc.Client) DocumentService { return &documentService{ Client: client, } } func buildDocumentsPath(namespace string) string { return path.Join(prefixDocuments, namespace) } // GetDocuments returns the documents for a `namespace` and an `orgID`. // Returned documents do not contain their content. func (s *documentService) GetDocuments(ctx context.Context, namespace string, orgID platform.ID) ([]*influxdb.Document, error) { span, _ := tracing.StartSpanFromContext(ctx) defer span.Finish() var resp documentsResponse r := s.Client. Get(buildDocumentsPath(namespace)). DecodeJSON(&resp) r = r.QueryParams([2]string{"orgID", orgID.String()}) if err := r.Do(ctx); err != nil { return nil, err } docs := make([]*influxdb.Document, len(resp.Documents)) for i := 0; i < len(docs); i++ { docs[i] = resp.Documents[i].Document } return docs, nil }