restic/vendor/cloud.google.com/go/firestore/docref.go

611 lines
19 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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"
"reflect"
"sort"
"golang.org/x/net/context"
"google.golang.org/api/iterator"
vkit "cloud.google.com/go/firestore/apiv1beta1"
pb "google.golang.org/genproto/googleapis/firestore/v1beta1"
)
var errNilDocRef = errors.New("firestore: nil DocumentRef")
// A DocumentRef is a reference to a Firestore document.
type DocumentRef struct {
// The CollectionRef that this document is a part of. Never nil.
Parent *CollectionRef
// The full resource path of the document: "projects/P/databases/D/documents..."
Path string
// The ID of the document: the last component of the resource path.
ID string
}
func newDocRef(parent *CollectionRef, id string) *DocumentRef {
return &DocumentRef{
Parent: parent,
ID: id,
Path: parent.Path + "/" + id,
}
}
func (d1 *DocumentRef) equal(d2 *DocumentRef) bool {
if d1 == nil || d2 == nil {
return d1 == d2
}
return d1.Parent.equal(d2.Parent) && d1.Path == d2.Path && d1.ID == d2.ID
}
// Collection returns a reference to sub-collection of this document.
func (d *DocumentRef) Collection(id string) *CollectionRef {
return newCollRefWithParent(d.Parent.c, d, id)
}
// Get retrieves the document. It returns an error if the document does not exist.
func (d *DocumentRef) Get(ctx context.Context) (*DocumentSnapshot, error) {
if err := checkTransaction(ctx); err != nil {
return nil, err
}
if d == nil {
return nil, errNilDocRef
}
doc, err := d.Parent.c.c.GetDocument(withResourceHeader(ctx, d.Parent.c.path()),
&pb.GetDocumentRequest{Name: d.Path})
// TODO(jba): verify that GetDocument returns NOT_FOUND.
if err != nil {
return nil, err
}
return newDocumentSnapshot(d, doc, d.Parent.c)
}
// Create creates the document with the given data.
// It returns an error if a document with the same ID already exists.
//
// The data argument can be a map with string keys, a struct, or a pointer to a
// struct. The map keys or exported struct fields become the fields of the firestore
// document.
// The values of data are converted to Firestore values as follows:
//
// - bool converts to Bool.
// - string converts to String.
// - int, int8, int16, int32 and int64 convert to Integer.
// - uint8, uint16 and uint32 convert to Integer. uint64 is disallowed,
// because it can represent values that cannot be represented in an int64, which
// is the underlying type of a Integer.
// - float32 and float64 convert to Double.
// - []byte converts to Bytes.
// - time.Time converts to Timestamp.
// - latlng.LatLng converts to GeoPoint. latlng is the package
// "google.golang.org/genproto/googleapis/type/latlng".
// - Slices convert to Array.
// - Maps and structs convert to Map.
// - nils of any type convert to Null.
//
// Pointers and interface{} are also permitted, and their elements processed
// recursively.
//
// Struct fields can have tags like those used by the encoding/json package. Tags
// begin with "firestore:" and are followed by "-", meaning "ignore this field," or
// an alternative name for the field. Following the name, these comma-separated
// options may be provided:
//
// - omitempty: Do not encode this field if it is empty. A value is empty
// if it is a zero value, or an array, slice or map of length zero.
// - serverTimestamp: The field must be of type time.Time. When writing, if
// the field has the zero value, the server will populate the stored document with
// the time that the request is processed.
func (d *DocumentRef) Create(ctx context.Context, data interface{}) (*WriteResult, error) {
ws, err := d.newReplaceWrites(data, nil, Exists(false))
if err != nil {
return nil, err
}
return d.Parent.c.commit(ctx, ws)
}
// Set creates or overwrites the document with the given data. See DocumentRef.Create
// for the acceptable values of data. Without options, Set overwrites the document
// completely. Specify one of the Merge options to preserve an existing document's
// fields.
func (d *DocumentRef) Set(ctx context.Context, data interface{}, opts ...SetOption) (*WriteResult, error) {
ws, err := d.newReplaceWrites(data, opts, nil)
if err != nil {
return nil, err
}
return d.Parent.c.commit(ctx, ws)
}
// Delete deletes the document. If the document doesn't exist, it does nothing
// and returns no error.
func (d *DocumentRef) Delete(ctx context.Context, preconds ...Precondition) (*WriteResult, error) {
ws, err := d.newDeleteWrites(preconds)
if err != nil {
return nil, err
}
return d.Parent.c.commit(ctx, ws)
}
func (d *DocumentRef) newReplaceWrites(data interface{}, opts []SetOption, p Precondition) ([]*pb.Write, error) {
if d == nil {
return nil, errNilDocRef
}
origFieldPaths, allPaths, err := processSetOptions(opts)
if err != nil {
return nil, err
}
isMerge := len(origFieldPaths) > 0 || allPaths // was some Merge option specified?
doc, serverTimestampPaths, err := toProtoDocument(data)
if err != nil {
return nil, err
}
if len(origFieldPaths) > 0 {
// Keep only data fields corresponding to the given field paths.
doc.Fields = applyFieldPaths(doc.Fields, origFieldPaths, nil)
}
doc.Name = d.Path
var fieldPaths []FieldPath
if allPaths {
// MergeAll was passed. Check that the data is a map, and extract its field paths.
v := reflect.ValueOf(data)
if v.Kind() != reflect.Map {
return nil, errors.New("firestore: MergeAll can only be specified with map data")
}
fieldPaths = fieldPathsFromMap(v, nil)
} else if len(origFieldPaths) > 0 {
// Remove server timestamp paths that are not in the list of paths to merge.
// Note: this is technically O(n^2), but it is unlikely that there is more
// than one server timestamp path.
serverTimestampPaths = removePathsIf(serverTimestampPaths, func(fp FieldPath) bool {
return !fp.in(origFieldPaths)
})
// Remove server timestamp fields from fieldPaths. Those fields were removed
// from the document by toProtoDocument, so they should not be in the update
// mask.
// Note: this is technically O(n^2), but it is unlikely that there is
// more than one server timestamp path.
fieldPaths = removePathsIf(origFieldPaths, func(fp FieldPath) bool {
return fp.in(serverTimestampPaths)
})
// Check that all the remaining field paths in the merge option are in the document.
for _, fp := range fieldPaths {
if _, err := valueAtPath(fp, doc.Fields); err != nil {
return nil, err
}
}
}
var pc *pb.Precondition
if p != nil {
pc, err = p.preconditionProto()
if err != nil {
return nil, err
}
}
var w *pb.Write
switch {
case len(fieldPaths) > 0:
// There are field paths, so we need an update mask.
sfps := toServiceFieldPaths(fieldPaths)
sort.Strings(sfps) // TODO(jba): make tests pass without this
w = &pb.Write{
Operation: &pb.Write_Update{doc},
UpdateMask: &pb.DocumentMask{FieldPaths: sfps},
CurrentDocument: pc,
}
case isMerge && pc != nil:
// There were field paths, but they all got removed.
// The write does nothing but enforce the precondition.
w = &pb.Write{CurrentDocument: pc}
case !isMerge && (pc != nil || doc.Fields != nil):
// Set without merge, so no update mask.
w = &pb.Write{
Operation: &pb.Write_Update{doc},
CurrentDocument: pc,
}
}
return d.writeWithTransform(w, serverTimestampPaths), nil
}
// Create a new map that contains only the field paths in fps.
func applyFieldPaths(fields map[string]*pb.Value, fps []FieldPath, root FieldPath) map[string]*pb.Value {
r := map[string]*pb.Value{}
for k, v := range fields {
kpath := root.with(k)
if kpath.in(fps) {
r[k] = v
} else if mv := v.GetMapValue(); mv != nil {
if m2 := applyFieldPaths(mv.Fields, fps, kpath); m2 != nil {
r[k] = &pb.Value{&pb.Value_MapValue{&pb.MapValue{m2}}}
}
}
}
if len(r) == 0 {
return nil
}
return r
}
func fieldPathsFromMap(vmap reflect.Value, prefix FieldPath) []FieldPath {
// vmap is a map and its keys are strings.
// Each map key denotes a field; no splitting or escaping.
var fps []FieldPath
for _, k := range vmap.MapKeys() {
v := vmap.MapIndex(k)
fp := prefix.with(k.String())
if vm := extractMap(v); vm.IsValid() {
fps = append(fps, fieldPathsFromMap(vm, fp)...)
} else if v.Interface() != ServerTimestamp {
// ServerTimestamp fields do not go into the update mask.
fps = append(fps, fp)
}
}
return fps
}
func extractMap(v reflect.Value) reflect.Value {
switch v.Kind() {
case reflect.Map:
return v
case reflect.Interface:
return extractMap(v.Elem())
default:
return reflect.Value{}
}
}
// removePathsIf creates a new slice of FieldPaths that contains
// exactly those elements of fps for which pred returns false.
func removePathsIf(fps []FieldPath, pred func(FieldPath) bool) []FieldPath {
var result []FieldPath
for _, fp := range fps {
if !pred(fp) {
result = append(result, fp)
}
}
return result
}
func (d *DocumentRef) newDeleteWrites(preconds []Precondition) ([]*pb.Write, error) {
if d == nil {
return nil, errNilDocRef
}
pc, err := processPreconditionsForDelete(preconds)
if err != nil {
return nil, err
}
return []*pb.Write{{
Operation: &pb.Write_Delete{d.Path},
CurrentDocument: pc,
}}, nil
}
func (d *DocumentRef) newUpdateMapWrites(data map[string]interface{}, preconds []Precondition) ([]*pb.Write, error) {
// Collect all the (top-level) keys map; they will comprise the update mask.
// Also, translate the map into a sequence of FieldPathUpdates.
var fps []FieldPath
var fpus []FieldPathUpdate
for k, v := range data {
fp, err := parseDotSeparatedString(k)
if err != nil {
return nil, err
}
fps = append(fps, fp)
fpus = append(fpus, FieldPathUpdate{Path: fp, Value: v})
}
// Check that there are no duplicate field paths, and that no field
// path is a prefix of another.
if err := checkNoDupOrPrefix(fps); err != nil {
return nil, err
}
// Re-create the map from the field paths and their corresponding values. A field path
// with a Delete value will not appear in the map but it will appear in the
// update mask, which will cause it to be deleted.
m := createMapFromFieldPathUpdates(fpus)
return d.newUpdateWrites(m, fps, preconds)
}
func (d *DocumentRef) newUpdateStructWrites(fieldPaths []string, data interface{}, preconds []Precondition) ([]*pb.Write, error) {
if !isStructOrStructPtr(data) {
return nil, errors.New("firestore: data is not struct or struct pointer")
}
fps, err := parseDotSeparatedStrings(fieldPaths)
if err != nil {
return nil, err
}
if err := checkNoDupOrPrefix(fps); err != nil {
return nil, err
}
return d.newUpdateWrites(data, fps, preconds)
}
func (d *DocumentRef) newUpdatePathWrites(data []FieldPathUpdate, preconds []Precondition) ([]*pb.Write, error) {
var fps []FieldPath
for _, fpu := range data {
if err := fpu.Path.validate(); err != nil {
return nil, err
}
fps = append(fps, fpu.Path)
}
if err := checkNoDupOrPrefix(fps); err != nil {
return nil, err
}
m := createMapFromFieldPathUpdates(data)
return d.newUpdateWrites(m, fps, preconds)
}
// newUpdateWrites creates Write operations for an update.
func (d *DocumentRef) newUpdateWrites(data interface{}, fieldPaths []FieldPath, preconds []Precondition) ([]*pb.Write, error) {
if len(fieldPaths) == 0 {
return nil, errors.New("firestore: no paths to update")
}
if d == nil {
return nil, errNilDocRef
}
pc, err := processPreconditionsForUpdate(preconds)
if err != nil {
return nil, err
}
doc, serverTimestampPaths, err := toProtoDocument(data)
if err != nil {
return nil, err
}
sfps := toServiceFieldPaths(fieldPaths)
doc.Name = d.Path
return d.writeWithTransform(&pb.Write{
Operation: &pb.Write_Update{doc},
UpdateMask: &pb.DocumentMask{FieldPaths: sfps},
CurrentDocument: pc,
}, serverTimestampPaths), nil
}
var requestTimeTransform = &pb.DocumentTransform_FieldTransform_SetToServerValue{
pb.DocumentTransform_FieldTransform_REQUEST_TIME,
}
func (d *DocumentRef) writeWithTransform(w *pb.Write, serverTimestampFieldPaths []FieldPath) []*pb.Write {
var ws []*pb.Write
if w != nil {
ws = append(ws, w)
}
if len(serverTimestampFieldPaths) > 0 {
ws = append(ws, d.newTransform(serverTimestampFieldPaths))
}
return ws
}
func (d *DocumentRef) newTransform(serverTimestampFieldPaths []FieldPath) *pb.Write {
sort.Sort(byPath(serverTimestampFieldPaths)) // TODO(jba): make tests pass without this
var fts []*pb.DocumentTransform_FieldTransform
for _, p := range serverTimestampFieldPaths {
fts = append(fts, &pb.DocumentTransform_FieldTransform{
FieldPath: p.toServiceFieldPath(),
TransformType: requestTimeTransform,
})
}
return &pb.Write{
Operation: &pb.Write_Transform{
&pb.DocumentTransform{
Document: d.Path,
FieldTransforms: fts,
// TODO(jba): should the transform have the same preconditions as the write?
},
},
}
}
type sentinel int
const (
// Delete is used as a value in a call to UpdateMap to indicate that the
// corresponding key should be deleted.
Delete sentinel = iota
// ServerTimestamp is used as a value in a call to UpdateMap to indicate that the
// key's value should be set to the time at which the server processed
// the request.
ServerTimestamp
)
func (s sentinel) String() string {
switch s {
case Delete:
return "Delete"
case ServerTimestamp:
return "ServerTimestamp"
default:
return "<?sentinel?>"
}
}
// UpdateMap updates the document using the given data. Map keys replace the stored
// values, but other fields of the stored document are untouched.
// See DocumentRef.Create for acceptable map values.
//
// If a map key is a multi-element field path, like "a.b", then only key "b" of
// the map value at "a" is changed; the rest of the map is preserved.
// For example, if the stored data is
// {"a": {"b": 1, "c": 2}}
// then
// UpdateMap({"a": {"b": 3}}) => {"a": {"b": 3}}
// while
// UpdateMap({"a.b": 3}) => {"a": {"b": 3, "c": 2}}
//
// To delete a key, specify it in the input with a value of firestore.Delete.
//
// Field paths expressed as map keys must not contain any of the runes "˜*/[]".
// Use UpdatePaths instead for such paths.
//
// UpdateMap returns an error if the document does not exist.
func (d *DocumentRef) UpdateMap(ctx context.Context, data map[string]interface{}, preconds ...Precondition) (*WriteResult, error) {
ws, err := d.newUpdateMapWrites(data, preconds)
if err != nil {
return nil, err
}
return d.Parent.c.commit(ctx, ws)
}
func isStructOrStructPtr(x interface{}) bool {
v := reflect.ValueOf(x)
if v.Kind() == reflect.Struct {
return true
}
if v.Kind() == reflect.Ptr && v.Elem().Kind() == reflect.Struct {
return true
}
return false
}
// UpdateStruct updates the given field paths of the stored document from the fields
// of data, which must be a struct or a pointer to a struct. Other fields of the
// stored document are untouched.
// See DocumentRef.Create for the acceptable values of the struct's fields.
//
// Each element of fieldPaths is a single field or a dot-separated sequence of
// fields, none of which contain the runes "˜*/[]".
//
// If an element of fieldPaths does not have a corresponding field in the struct,
// that key is deleted from the stored document.
//
// UpdateStruct returns an error if the document does not exist.
func (d *DocumentRef) UpdateStruct(ctx context.Context, fieldPaths []string, data interface{}, preconds ...Precondition) (*WriteResult, error) {
ws, err := d.newUpdateStructWrites(fieldPaths, data, preconds)
if err != nil {
return nil, err
}
return d.Parent.c.commit(ctx, ws)
}
// A FieldPathUpdate describes an update to a value referred to by a FieldPath.
// See DocumentRef.Create for acceptable values.
// To delete a field, specify firestore.Delete as the value.
type FieldPathUpdate struct {
Path FieldPath
Value interface{}
}
// UpdatePaths updates the document using the given data. The values at the given
// field paths are replaced, but other fields of the stored document are untouched.
func (d *DocumentRef) UpdatePaths(ctx context.Context, data []FieldPathUpdate, preconds ...Precondition) (*WriteResult, error) {
ws, err := d.newUpdatePathWrites(data, preconds)
if err != nil {
return nil, err
}
return d.Parent.c.commit(ctx, ws)
}
// Collections returns an interator over the immediate sub-collections of the document.
func (d *DocumentRef) Collections(ctx context.Context) *CollectionIterator {
client := d.Parent.c
it := &CollectionIterator{
err: checkTransaction(ctx),
client: client,
parent: d,
it: client.c.ListCollectionIds(
withResourceHeader(ctx, client.path()),
&pb.ListCollectionIdsRequest{Parent: d.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
}
// CollectionIterator is an iterator over sub-collections of a document.
type CollectionIterator struct {
client *Client
parent *DocumentRef
it *vkit.StringIterator
pageInfo *iterator.PageInfo
nextFunc func() error
items []*CollectionRef
err error
}
// PageInfo supports pagination. See the google.golang.org/api/iterator package for details.
func (it *CollectionIterator) PageInfo() *iterator.PageInfo { return it.pageInfo }
// Next returns the next result. Its second return value is iterator.Done if there
// are no more results. Once Next returns Done, all subsequent calls will return
// Done.
func (it *CollectionIterator) Next() (*CollectionRef, error) {
if err := it.nextFunc(); err != nil {
return nil, err
}
item := it.items[0]
it.items = it.items[1:]
return item, nil
}
func (it *CollectionIterator) fetch(pageSize int, pageToken string) (string, error) {
if it.err != nil {
return "", it.err
}
return iterFetch(pageSize, pageToken, it.it.PageInfo(), func() error {
id, err := it.it.Next()
if err != nil {
return err
}
var cr *CollectionRef
if it.parent == nil {
cr = newTopLevelCollRef(it.client, it.client.path(), id)
} else {
cr = newCollRefWithParent(it.client, it.parent, id)
}
it.items = append(it.items, cr)
return nil
})
}
// GetAll returns all the collections remaining from the iterator.
func (it *CollectionIterator) GetAll() ([]*CollectionRef, error) {
var crs []*CollectionRef
for {
cr, err := it.Next()
if err == iterator.Done {
break
}
if err != nil {
return nil, err
}
crs = append(crs, cr)
}
return crs, nil
}
// Common fetch code for iterators that are backed by vkit iterators.
// TODO(jba): dedup with same function in logging/logadmin.
func iterFetch(pageSize int, pageToken string, pi *iterator.PageInfo, next func() error) (string, error) {
pi.MaxSize = pageSize
pi.Token = pageToken
// Get one item, which will fill the buffer.
if err := next(); err != nil {
return "", err
}
// Collect the rest of the buffer.
for pi.Remaining() > 0 {
if err := next(); err != nil {
return "", err
}
}
return pi.Token, nil
}