// Copyright 2017 Google Inc. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package firestore import ( "errors" "fmt" "io" "strings" "time" "google.golang.org/api/iterator" vkit "cloud.google.com/go/firestore/apiv1beta1" "cloud.google.com/go/internal/version" pb "google.golang.org/genproto/googleapis/firestore/v1beta1" "github.com/golang/protobuf/ptypes" "golang.org/x/net/context" "google.golang.org/api/option" "google.golang.org/grpc/metadata" ) // resourcePrefixHeader is the name of the metadata header used to indicate // the resource being operated on. const resourcePrefixHeader = "google-cloud-resource-prefix" // A Client provides access to the Firestore service. type Client struct { c *vkit.Client projectID string databaseID string // A client is tied to a single database. } // NewClient creates a new Firestore client that uses the given project. func NewClient(ctx context.Context, projectID string, opts ...option.ClientOption) (*Client, error) { vc, err := vkit.NewClient(ctx, opts...) if err != nil { return nil, err } vc.SetGoogleClientInfo("gccl", version.Repo) c := &Client{ c: vc, projectID: projectID, databaseID: "(default)", // always "(default)", for now } return c, nil } // Close closes any resources held by the client. // // Close need not be called at program exit. func (c *Client) Close() error { return c.c.Close() } func (c *Client) path() string { return fmt.Sprintf("projects/%s/databases/%s", c.projectID, c.databaseID) } func withResourceHeader(ctx context.Context, resource string) context.Context { md, _ := metadata.FromOutgoingContext(ctx) md = md.Copy() md[resourcePrefixHeader] = []string{resource} return metadata.NewOutgoingContext(ctx, md) } // Collection creates a reference to a collection with the given path. // A path is a sequence of IDs separated by slashes. // // Collection returns nil if path contains an even number of IDs or any ID is empty. func (c *Client) Collection(path string) *CollectionRef { coll, _ := c.idsToRef(strings.Split(path, "/"), c.path()) return coll } // Doc creates a reference to a document with the given path. // A path is a sequence of IDs separated by slashes. // // Doc returns nil if path contains an odd number of IDs or any ID is empty. func (c *Client) Doc(path string) *DocumentRef { _, doc := c.idsToRef(strings.Split(path, "/"), c.path()) return doc } func (c *Client) idsToRef(IDs []string, dbPath string) (*CollectionRef, *DocumentRef) { if len(IDs) == 0 { return nil, nil } for _, id := range IDs { if id == "" { return nil, nil } } coll := newTopLevelCollRef(c, dbPath, IDs[0]) i := 1 for i < len(IDs) { doc := newDocRef(coll, IDs[i]) i++ if i == len(IDs) { return nil, doc } coll = newCollRefWithParent(c, doc, IDs[i]) i++ } return coll, nil } // GetAll retrieves multiple documents with a single call. The DocumentSnapshots are // returned in the order of the given DocumentRefs. // // If a document is not present, the corresponding DocumentSnapshot will be nil. func (c *Client) GetAll(ctx context.Context, docRefs []*DocumentRef) ([]*DocumentSnapshot, error) { if err := checkTransaction(ctx); err != nil { return nil, err } return c.getAll(ctx, docRefs, nil) } func (c *Client) getAll(ctx context.Context, docRefs []*DocumentRef, tid []byte) ([]*DocumentSnapshot, error) { var docNames []string for _, dr := range docRefs { if dr == nil { return nil, errNilDocRef } docNames = append(docNames, dr.Path) } req := &pb.BatchGetDocumentsRequest{ Database: c.path(), Documents: docNames, } if tid != nil { req.ConsistencySelector = &pb.BatchGetDocumentsRequest_Transaction{tid} } streamClient, err := c.c.BatchGetDocuments(withResourceHeader(ctx, req.Database), req) if err != nil { return nil, err } // Read results from the stream and add them to a map. docMap := map[string]*pb.Document{} for { res, err := streamClient.Recv() if err == io.EOF { break } if err != nil { return nil, err } switch x := res.Result.(type) { case *pb.BatchGetDocumentsResponse_Found: docMap[x.Found.Name] = x.Found case *pb.BatchGetDocumentsResponse_Missing: if docMap[x.Missing] != nil { return nil, fmt.Errorf("firestore: %q both missing and present", x.Missing) } docMap[x.Missing] = nil default: return nil, errors.New("firestore: unknown BatchGetDocumentsResponse result type") } } // Put the documents we've gathered in the same order as the requesting slice of // DocumentRefs. docs := make([]*DocumentSnapshot, len(docNames)) for i, name := range docNames { pbDoc, ok := docMap[name] if !ok { return nil, fmt.Errorf("firestore: passed %q to BatchGetDocuments but never saw response", name) } if pbDoc != nil { doc, err := newDocumentSnapshot(docRefs[i], pbDoc, c) if err != nil { return nil, err } docs[i] = doc } } return docs, nil } // Collections returns an interator over the top-level collections. func (c *Client) Collections(ctx context.Context) *CollectionIterator { it := &CollectionIterator{ err: checkTransaction(ctx), client: c, it: c.c.ListCollectionIds( withResourceHeader(ctx, c.path()), &pb.ListCollectionIdsRequest{Parent: c.path()}), } it.pageInfo, it.nextFunc = iterator.NewPageInfo( it.fetch, func() int { return len(it.items) }, func() interface{} { b := it.items; it.items = nil; return b }) return it } // Batch returns a WriteBatch. func (c *Client) Batch() *WriteBatch { return &WriteBatch{c: c} } // commit calls the Commit RPC outside of a transaction. func (c *Client) commit(ctx context.Context, ws []*pb.Write) ([]*WriteResult, error) { if err := checkTransaction(ctx); err != nil { return nil, err } req := &pb.CommitRequest{ Database: c.path(), Writes: ws, } res, err := c.c.Commit(withResourceHeader(ctx, req.Database), req) if err != nil { return nil, err } if len(res.WriteResults) == 0 { return nil, errors.New("firestore: missing WriteResult") } var wrs []*WriteResult for _, pwr := range res.WriteResults { wr, err := writeResultFromProto(pwr) if err != nil { return nil, err } wrs = append(wrs, wr) } return wrs, nil } func (c *Client) commit1(ctx context.Context, ws []*pb.Write) (*WriteResult, error) { wrs, err := c.commit(ctx, ws) if err != nil { return nil, err } return wrs[0], nil } // A WriteResult is returned by methods that write documents. type WriteResult struct { // The time at which the document was updated, or created if it did not // previously exist. Writes that do not actually change the document do // not change the update time. UpdateTime time.Time } func writeResultFromProto(wr *pb.WriteResult) (*WriteResult, error) { t, err := ptypes.Timestamp(wr.UpdateTime) if err != nil { t = time.Time{} // TODO(jba): Follow up if Delete is supposed to return a nil timestamp. } return &WriteResult{UpdateTime: t}, nil }