feat: Inital commit
This commit is contained in:
+875
@@ -0,0 +1,875 @@
|
||||
// Copyright 2017 Google LLC
|
||||
//
|
||||
// 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 (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"sort"
|
||||
"unicode/utf8"
|
||||
|
||||
vkit "cloud.google.com/go/firestore/apiv1"
|
||||
pb "cloud.google.com/go/firestore/apiv1/firestorepb"
|
||||
"cloud.google.com/go/internal/trace"
|
||||
"google.golang.org/api/iterator"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
var (
|
||||
errNilDocRef = errors.New("firestore: nil DocumentRef")
|
||||
errInvalidUtf8DocRef = errors.New("firestore: ID in DocumentRef contains invalid UTF-8 characters")
|
||||
)
|
||||
|
||||
// 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. A document "doc-1" in collection
|
||||
// "coll-1" would be: "projects/P/databases/D/documents/coll-1/doc-1".
|
||||
Path string
|
||||
|
||||
// The shorter resource path of the document. A document "doc-1" in
|
||||
// collection "coll-1" would be: "coll-1/doc-1".
|
||||
shortPath string
|
||||
|
||||
// The ID of the document: the last component of the resource path.
|
||||
ID string
|
||||
|
||||
// The options (only read time currently supported) for reading this document
|
||||
readSettings *readSettings
|
||||
}
|
||||
|
||||
func newDocRef(parent *CollectionRef, id string) *DocumentRef {
|
||||
return &DocumentRef{
|
||||
Parent: parent,
|
||||
ID: id,
|
||||
Path: parent.Path + "/" + id,
|
||||
shortPath: parent.selfPath + "/" + id,
|
||||
readSettings: &readSettings{},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DocumentRef) isValid() error {
|
||||
if d == nil {
|
||||
return errNilDocRef
|
||||
}
|
||||
if !utf8.ValidString(d.ID) {
|
||||
return errInvalidUtf8DocRef
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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. If the document does not exist, Get return a NotFound error, which
|
||||
// can be checked with
|
||||
//
|
||||
// status.Code(err) == codes.NotFound
|
||||
//
|
||||
// In that case, Get returns a non-nil DocumentSnapshot whose Exists method return false and whose
|
||||
// ReadTime is the time of the failed read operation.
|
||||
func (d *DocumentRef) Get(ctx context.Context) (_ *DocumentSnapshot, err error) {
|
||||
ctx = trace.StartSpan(ctx, "cloud.google.com/go/firestore.DocumentRef.Get")
|
||||
defer func() { trace.EndSpan(ctx, err) }()
|
||||
|
||||
if err := d.isValid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
docsnaps, err := d.Parent.c.getAll(ctx, []*DocumentRef{d}, nil, d.readSettings)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ds := docsnaps[0]
|
||||
if !ds.Exists() {
|
||||
return ds, status.Errorf(codes.NotFound, "%q not found", d.Path)
|
||||
}
|
||||
return ds, nil
|
||||
}
|
||||
|
||||
// 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. uint, uint64 and uintptr are disallowed,
|
||||
// because they may be able to 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 and *ts.Timestamp convert to Timestamp. ts is the package
|
||||
// "google.golang.org/protobuf/types/known/timestamppb".
|
||||
// - *latlng.LatLng converts to GeoPoint. latlng is the package
|
||||
// "google.golang.org/genproto/googleapis/type/latlng". You should always use
|
||||
// a pointer to a LatLng.
|
||||
// - Slices convert to Array.
|
||||
// - *firestore.DocumentRef converts to Reference.
|
||||
// - 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. serverTimestamp
|
||||
// is a sentinel token that tells Firestore to substitute the server time
|
||||
// into that field. When writing, if the field has the zero value, the
|
||||
// server will populate the stored document with the time that the request
|
||||
// is processed. However, if the field value is non-zero it won't be saved.
|
||||
func (d *DocumentRef) Create(ctx context.Context, data interface{}) (_ *WriteResult, err error) {
|
||||
ctx = trace.StartSpan(ctx, "cloud.google.com/go/firestore.DocumentRef.Create")
|
||||
defer func() { trace.EndSpan(ctx, err) }()
|
||||
|
||||
ws, err := d.newCreateWrites(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d.Parent.c.commit1(ctx, ws)
|
||||
}
|
||||
|
||||
func (d *DocumentRef) newCreateWrites(data interface{}) ([]*pb.Write, error) {
|
||||
if err := d.isValid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
doc, transforms, err := toProtoDocument(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
doc.Name = d.Path
|
||||
pc, err := exists(false).preconditionProto()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d.newUpdateWithTransform(doc, nil, pc, transforms, false), nil
|
||||
}
|
||||
|
||||
// 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. To delete some fields, use a Merge option with firestore.Delete as the
|
||||
// field value.
|
||||
func (d *DocumentRef) Set(ctx context.Context, data interface{}, opts ...SetOption) (_ *WriteResult, err error) {
|
||||
ctx = trace.StartSpan(ctx, "cloud.google.com/go/firestore.DocumentRef.Set")
|
||||
defer func() { trace.EndSpan(ctx, err) }()
|
||||
|
||||
ws, err := d.newSetWrites(data, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d.Parent.c.commit1(ctx, ws)
|
||||
}
|
||||
|
||||
func (d *DocumentRef) newSetWrites(data interface{}, opts []SetOption) ([]*pb.Write, error) {
|
||||
if err := d.isValid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if data == nil {
|
||||
return nil, errors.New("firestore: nil document contents")
|
||||
}
|
||||
if len(opts) == 0 { // Set without merge
|
||||
doc, serverTimestampPaths, err := toProtoDocument(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
doc.Name = d.Path
|
||||
return d.newUpdateWithTransform(doc, nil, nil, serverTimestampPaths, true), nil
|
||||
}
|
||||
// Set with merge.
|
||||
// This is just like Update, except for the existence precondition.
|
||||
// So we turn data into a list of (FieldPath, interface{}) pairs (fpv's), as we do
|
||||
// for Update.
|
||||
fieldPaths, allPaths, err := processSetOptions(opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var fpvs []fpv
|
||||
v := reflect.ValueOf(data)
|
||||
if allPaths {
|
||||
// Set with MergeAll. Collect all the leaves of the map.
|
||||
if v.Kind() != reflect.Map {
|
||||
return nil, errors.New("firestore: MergeAll can only be specified with map data")
|
||||
}
|
||||
if v.Len() == 0 {
|
||||
// Special case: MergeAll with an empty map.
|
||||
return d.newUpdateWithTransform(&pb.Document{Name: d.Path}, []FieldPath{}, nil, nil, true), nil
|
||||
}
|
||||
fpvsFromData(v, nil, &fpvs)
|
||||
} else {
|
||||
// Set with merge paths. Collect only the values at the given paths.
|
||||
for _, fp := range fieldPaths {
|
||||
val, err := getAtPath(v, fp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fpvs = append(fpvs, fpv{fp, val})
|
||||
}
|
||||
}
|
||||
return d.fpvsToWrites(fpvs, nil)
|
||||
}
|
||||
|
||||
// fpvsFromData converts v into a list of (FieldPath, value) pairs.
|
||||
func fpvsFromData(v reflect.Value, prefix FieldPath, fpvs *[]fpv) {
|
||||
switch v.Kind() {
|
||||
case reflect.Map:
|
||||
for _, k := range v.MapKeys() {
|
||||
fpvsFromData(v.MapIndex(k), prefix.with(k.String()), fpvs)
|
||||
}
|
||||
case reflect.Interface:
|
||||
fpvsFromData(v.Elem(), prefix, fpvs)
|
||||
|
||||
default:
|
||||
var val interface{}
|
||||
if v.IsValid() {
|
||||
val = v.Interface()
|
||||
}
|
||||
*fpvs = append(*fpvs, fpv{prefix, val})
|
||||
}
|
||||
}
|
||||
|
||||
// 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, err error) {
|
||||
ctx = trace.StartSpan(ctx, "cloud.google.com/go/firestore.DocumentRef.Delete")
|
||||
defer func() { trace.EndSpan(ctx, err) }()
|
||||
|
||||
ws, err := d.newDeleteWrites(preconds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d.Parent.c.commit1(ctx, ws)
|
||||
}
|
||||
|
||||
func (d *DocumentRef) newDeleteWrites(preconds []Precondition) ([]*pb.Write, error) {
|
||||
if err := d.isValid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pc, err := processPreconditionsForDelete(preconds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []*pb.Write{{
|
||||
Operation: &pb.Write_Delete{Delete: d.Path},
|
||||
CurrentDocument: pc,
|
||||
}}, nil
|
||||
}
|
||||
|
||||
func (d *DocumentRef) newUpdatePathWrites(updates []Update, preconds []Precondition) ([]*pb.Write, error) {
|
||||
if len(updates) == 0 {
|
||||
return nil, errors.New("firestore: no paths to update")
|
||||
}
|
||||
var fpvs []fpv
|
||||
for _, u := range updates {
|
||||
v, err := u.process()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fpvs = append(fpvs, v)
|
||||
}
|
||||
pc, err := processPreconditionsForUpdate(preconds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d.fpvsToWrites(fpvs, pc)
|
||||
}
|
||||
|
||||
func (d *DocumentRef) fpvsToWrites(fpvs []fpv, pc *pb.Precondition) ([]*pb.Write, error) {
|
||||
// Make sure there are no duplications or prefixes among the field paths.
|
||||
var fps []FieldPath
|
||||
for _, fpv := range fpvs {
|
||||
fps = append(fps, fpv.fieldPath)
|
||||
}
|
||||
if err := checkNoDupOrPrefix(fps); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Process each fpv.
|
||||
var updatePaths []FieldPath
|
||||
var transforms []*pb.DocumentTransform_FieldTransform
|
||||
doc := &pb.Document{
|
||||
Name: d.Path,
|
||||
Fields: map[string]*pb.Value{},
|
||||
}
|
||||
for _, fpv := range fpvs {
|
||||
switch fpv.value.(type) {
|
||||
case arrayUnion:
|
||||
au := fpv.value.(arrayUnion)
|
||||
t, err := arrayUnionTransform(au, fpv.fieldPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
transforms = append(transforms, t)
|
||||
case arrayRemove:
|
||||
ar := fpv.value.(arrayRemove)
|
||||
t, err := arrayRemoveTransform(ar, fpv.fieldPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
transforms = append(transforms, t)
|
||||
case transform:
|
||||
t, err := fieldTransform(fpv.value.(transform), fpv.fieldPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
transforms = append(transforms, t)
|
||||
|
||||
default:
|
||||
switch fpv.value {
|
||||
case Delete:
|
||||
// Send the field path without a corresponding value.
|
||||
updatePaths = append(updatePaths, fpv.fieldPath)
|
||||
|
||||
case ServerTimestamp:
|
||||
// Use the path in a transform operation.
|
||||
transforms = append(transforms, serverTimestamp(fpv.fieldPath.toServiceFieldPath()))
|
||||
|
||||
default:
|
||||
updatePaths = append(updatePaths, fpv.fieldPath)
|
||||
// Convert the value to a proto and put it into the document.
|
||||
v := reflect.ValueOf(fpv.value)
|
||||
|
||||
pv, _, err := toProtoValue(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
setAtPath(doc.Fields, fpv.fieldPath, pv)
|
||||
// Also accumulate any transforms within the value.
|
||||
ts, err := extractTransforms(v, fpv.fieldPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
transforms = append(transforms, ts...)
|
||||
}
|
||||
}
|
||||
}
|
||||
return d.newUpdateWithTransform(doc, updatePaths, pc, transforms, false), nil
|
||||
}
|
||||
|
||||
// newUpdateWithTransform constructs operations for a commit. Most generally, it
|
||||
// returns an update operation with update transforms.
|
||||
//
|
||||
// If there are no serverTimestampPaths, the transform is omitted.
|
||||
//
|
||||
// If doc.Fields is empty, there are no updatePaths, and there is no precondition,
|
||||
// the update is omitted, unless updateOnEmpty is true.
|
||||
func (d *DocumentRef) newUpdateWithTransform(doc *pb.Document, updatePaths []FieldPath, pc *pb.Precondition, transforms []*pb.DocumentTransform_FieldTransform, updateOnEmpty bool) []*pb.Write {
|
||||
var ws []*pb.Write
|
||||
var w *pb.Write
|
||||
initializedW := &pb.Write{
|
||||
Operation: &pb.Write_Update{
|
||||
Update: doc,
|
||||
},
|
||||
CurrentDocument: pc,
|
||||
// If the mask is not set for an `update` and the document exists, any
|
||||
// existing data will be overwritten.
|
||||
UpdateMask: &pb.DocumentMask{},
|
||||
}
|
||||
if updateOnEmpty || len(doc.Fields) > 0 ||
|
||||
len(updatePaths) > 0 || (pc != nil && len(transforms) == 0) {
|
||||
w = initializedW
|
||||
var mask *pb.DocumentMask
|
||||
if updatePaths != nil {
|
||||
sfps := toServiceFieldPaths(updatePaths)
|
||||
sort.Strings(sfps) // TODO(jba): make tests pass without this
|
||||
mask = &pb.DocumentMask{FieldPaths: sfps}
|
||||
}
|
||||
w.UpdateMask = mask
|
||||
}
|
||||
if len(transforms) > 0 || pc != nil {
|
||||
if w == nil {
|
||||
w = initializedW
|
||||
}
|
||||
w.UpdateTransforms = transforms
|
||||
}
|
||||
if w != nil {
|
||||
ws = append(ws, w)
|
||||
}
|
||||
return ws
|
||||
}
|
||||
|
||||
// arrayUnion is a special type in firestore. It instructs the server to add its
|
||||
// elements to whatever array already exists, or to create an array if no value
|
||||
// exists.
|
||||
type arrayUnion struct {
|
||||
elems []interface{}
|
||||
}
|
||||
|
||||
// ArrayUnion specifies elements to be added to whatever array already exists in
|
||||
// the server, or to create an array if no value exists.
|
||||
//
|
||||
// If a value exists and it's an array, values are appended to it. Any duplicate
|
||||
// value is ignored.
|
||||
// If a value exists and it's not an array, the value is replaced by an array of
|
||||
// the values in the ArrayUnion.
|
||||
// If a value does not exist, an array of the values in the ArrayUnion is created.
|
||||
//
|
||||
// ArrayUnion must be the value of a field directly; it cannot appear in
|
||||
// array or struct values, or in any value that is itself inside an array or
|
||||
// struct.
|
||||
func ArrayUnion(elems ...interface{}) arrayUnion {
|
||||
return arrayUnion{elems: elems}
|
||||
}
|
||||
|
||||
// This helper converts an arrayUnion into a proto object.
|
||||
func arrayUnionTransform(au arrayUnion, fp FieldPath) (*pb.DocumentTransform_FieldTransform, error) {
|
||||
var elems []*pb.Value
|
||||
for _, v := range au.elems {
|
||||
pv, _, err := toProtoValue(reflect.ValueOf(v))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
elems = append(elems, pv)
|
||||
}
|
||||
return &pb.DocumentTransform_FieldTransform{
|
||||
FieldPath: fp.toServiceFieldPath(),
|
||||
TransformType: &pb.DocumentTransform_FieldTransform_AppendMissingElements{
|
||||
AppendMissingElements: &pb.ArrayValue{Values: elems},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// arrayRemove is a special type in firestore. It instructs the server to remove
|
||||
// the specified values.
|
||||
type arrayRemove struct {
|
||||
elems []interface{}
|
||||
}
|
||||
|
||||
// ArrayRemove specifies elements to be removed from whatever array already
|
||||
// exists in the server.
|
||||
//
|
||||
// If a value exists and it's an array, values are removed from it. All
|
||||
// duplicate values are removed.
|
||||
// If a value exists and it's not an array, the value is replaced by an empty
|
||||
// array.
|
||||
// If a value does not exist, an empty array is created.
|
||||
//
|
||||
// ArrayRemove must be the value of a field directly; it cannot appear in
|
||||
// array or struct values, or in any value that is itself inside an array or
|
||||
// struct.
|
||||
func ArrayRemove(elems ...interface{}) arrayRemove {
|
||||
return arrayRemove{elems: elems}
|
||||
}
|
||||
|
||||
// This helper converts an arrayRemove into a proto object.
|
||||
func arrayRemoveTransform(ar arrayRemove, fp FieldPath) (*pb.DocumentTransform_FieldTransform, error) {
|
||||
var elems []*pb.Value
|
||||
for _, v := range ar.elems {
|
||||
// ServerTimestamp cannot occur in an array, so we ignore transformations here.
|
||||
pv, _, err := toProtoValue(reflect.ValueOf(v))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
elems = append(elems, pv)
|
||||
}
|
||||
return &pb.DocumentTransform_FieldTransform{
|
||||
FieldPath: fp.toServiceFieldPath(),
|
||||
TransformType: &pb.DocumentTransform_FieldTransform_RemoveAllFromArray{
|
||||
RemoveAllFromArray: &pb.ArrayValue{Values: elems},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
type transform struct {
|
||||
t *pb.DocumentTransform_FieldTransform
|
||||
|
||||
// For v2 of this package, we may want to remove this field and
|
||||
// return an error directly from the FieldTransformX functions.
|
||||
err error
|
||||
}
|
||||
|
||||
func (t transform) String() string {
|
||||
if t.t == nil {
|
||||
return ""
|
||||
}
|
||||
return t.t.String()
|
||||
}
|
||||
|
||||
// FieldTransformIncrement returns a special value that can be used with Set, Create, or
|
||||
// Update that tells the server to transform the field's current value
|
||||
// by the given value.
|
||||
//
|
||||
// The supported values are:
|
||||
//
|
||||
// int, int8, int16, int32, int64
|
||||
// uint8, uint16, uint32
|
||||
// float32, float64
|
||||
//
|
||||
// If the field does not yet exist, the transformation will set the field to
|
||||
// the given value.
|
||||
func FieldTransformIncrement(n interface{}) transform {
|
||||
v, err := numericTransformValue(n)
|
||||
return transform{
|
||||
t: &pb.DocumentTransform_FieldTransform{
|
||||
TransformType: &pb.DocumentTransform_FieldTransform_Increment{
|
||||
Increment: v,
|
||||
},
|
||||
},
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
// Increment is an alias for FieldTransformIncrement.
|
||||
func Increment(n interface{}) transform {
|
||||
return FieldTransformIncrement(n)
|
||||
}
|
||||
|
||||
// FieldTransformMaximum returns a special value that can be used with Set, Create, or
|
||||
// Update that tells the server to set the field to the maximum of the
|
||||
// field's current value and the given value.
|
||||
//
|
||||
// The supported values are:
|
||||
//
|
||||
// int, int8, int16, int32, int64
|
||||
// uint8, uint16, uint32
|
||||
// float32, float64
|
||||
//
|
||||
// If the field is not an integer or double, or if the field does not yet
|
||||
// exist, the transformation will set the field to the given value. If a
|
||||
// maximum operation is applied where the field and the input value are of
|
||||
// mixed types (that is - one is an integer and one is a double) the field
|
||||
// takes on the type of the larger operand. If the operands are equivalent
|
||||
// (e.g. 3 and 3.0), the field does not change. 0, 0.0, and -0.0 are all zero.
|
||||
// The maximum of a zero stored value and zero input value is always the
|
||||
// stored value. The maximum of any numeric value x and NaN is NaN.
|
||||
func FieldTransformMaximum(n interface{}) transform {
|
||||
v, err := numericTransformValue(n)
|
||||
return transform{
|
||||
t: &pb.DocumentTransform_FieldTransform{
|
||||
TransformType: &pb.DocumentTransform_FieldTransform_Maximum{
|
||||
Maximum: v,
|
||||
},
|
||||
},
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
// FieldTransformMinimum returns a special value that can be used with Set, Create, or
|
||||
// Update that tells the server to set the field to the minimum of the
|
||||
// field's current value and the given value.
|
||||
//
|
||||
// The supported values are:
|
||||
//
|
||||
// int, int8, int16, int32, int64
|
||||
// uint8, uint16, uint32
|
||||
// float32, float64
|
||||
//
|
||||
// If the field is not an integer or double, or if the field does not yet
|
||||
// exist, the transformation will set the field to the given value. If a
|
||||
// minimum operation is applied where the field and the input value are of
|
||||
// mixed types (that is - one is an integer and one is a double) the field
|
||||
// takes on the type of the smaller operand. If the operands are equivalent
|
||||
// (e.g. 3 and 3.0), the field does not change. 0, 0.0, and -0.0 are all zero.
|
||||
// The minimum of a zero stored value and zero input value is always the
|
||||
// stored value. The minimum of any numeric value x and NaN is NaN.
|
||||
func FieldTransformMinimum(n interface{}) transform {
|
||||
v, err := numericTransformValue(n)
|
||||
return transform{
|
||||
t: &pb.DocumentTransform_FieldTransform{
|
||||
TransformType: &pb.DocumentTransform_FieldTransform_Minimum{
|
||||
Minimum: v,
|
||||
},
|
||||
},
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
func numericTransformValue(n interface{}) (*pb.Value, error) {
|
||||
switch n.(type) {
|
||||
case int, int8, int16, int32, int64,
|
||||
uint8, uint16, uint32,
|
||||
float32, float64:
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported type %T for Increment; supported values include int, int8, int16, int32, int64, uint8, uint16, uint32, float32, float64", n)
|
||||
}
|
||||
|
||||
v, _, err := toProtoValue(reflect.ValueOf(n))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func fieldTransform(ar transform, fp FieldPath) (*pb.DocumentTransform_FieldTransform, error) {
|
||||
if ar.err != nil {
|
||||
return nil, ar.err
|
||||
}
|
||||
newFt := proto.Clone(ar.t)
|
||||
ft := newFt.(*pb.DocumentTransform_FieldTransform)
|
||||
ft.FieldPath = fp.toServiceFieldPath()
|
||||
return ft, nil
|
||||
}
|
||||
|
||||
type sentinel int
|
||||
|
||||
const (
|
||||
// Delete is used as a value in a call to Update or Set with merge to indicate
|
||||
// that the corresponding key should be deleted.
|
||||
Delete sentinel = iota
|
||||
|
||||
// ServerTimestamp is used as a value in a call to Update to indicate that the
|
||||
// key's value should be set to the time at which the server processed
|
||||
// the request.
|
||||
//
|
||||
// ServerTimestamp must be the value of a field directly; it cannot appear in
|
||||
// array or struct values, or in any value that is itself inside an array or
|
||||
// struct.
|
||||
ServerTimestamp
|
||||
)
|
||||
|
||||
func (s sentinel) String() string {
|
||||
switch s {
|
||||
case Delete:
|
||||
return "Delete"
|
||||
case ServerTimestamp:
|
||||
return "ServerTimestamp"
|
||||
default:
|
||||
return "<?sentinel?>"
|
||||
}
|
||||
}
|
||||
|
||||
// An Update describes an update to a value referred to by a path.
|
||||
// An Update should have either a non-empty Path or a non-empty FieldPath,
|
||||
// but not both.
|
||||
//
|
||||
// See DocumentRef.Create for acceptable values.
|
||||
// To delete a field, specify firestore.Delete as the value.
|
||||
type Update struct {
|
||||
Path string // Will be split on dots, and must not contain any of "˜*/[]".
|
||||
FieldPath FieldPath
|
||||
Value interface{}
|
||||
}
|
||||
|
||||
// String returns string representation of firestore.Update
|
||||
func (u Update) String() string {
|
||||
t, ok := u.Value.(transform)
|
||||
if !ok {
|
||||
return fmt.Sprintf("Path: %s FieldPath: %s Value: %s", u.Path, u.FieldPath, u.Value)
|
||||
}
|
||||
return fmt.Sprintf("Path: %s FieldPath: %s Value: %s", u.Path, u.FieldPath, t.String())
|
||||
}
|
||||
|
||||
// An fpv is a pair of validated FieldPath and value.
|
||||
type fpv struct {
|
||||
fieldPath FieldPath
|
||||
value interface{}
|
||||
}
|
||||
|
||||
func (u *Update) process() (fpv, error) {
|
||||
if (u.Path != "") == (u.FieldPath != nil) {
|
||||
return fpv{}, fmt.Errorf("firestore: update %+v should have exactly one of Path or FieldPath", u)
|
||||
}
|
||||
fp := u.FieldPath
|
||||
var err error
|
||||
if fp == nil {
|
||||
fp, err = parseDotSeparatedString(u.Path)
|
||||
if err != nil {
|
||||
return fpv{}, err
|
||||
}
|
||||
}
|
||||
if err := fp.validate(); err != nil {
|
||||
return fpv{}, err
|
||||
}
|
||||
return fpv{fp, u.Value}, nil
|
||||
}
|
||||
|
||||
// Update updates the document. The values at the given
|
||||
// field paths are replaced, but other fields of the stored document are untouched.
|
||||
func (d *DocumentRef) Update(ctx context.Context, updates []Update, preconds ...Precondition) (_ *WriteResult, err error) {
|
||||
ctx = trace.StartSpan(ctx, "cloud.google.com/go/firestore.DocumentRef.Update")
|
||||
defer func() { trace.EndSpan(ctx, err) }()
|
||||
|
||||
ws, err := d.newUpdatePathWrites(updates, preconds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d.Parent.c.commit1(ctx, ws)
|
||||
}
|
||||
|
||||
// Collections returns an iterator over the immediate sub-collections of the document.
|
||||
func (d *DocumentRef) Collections(ctx context.Context) *CollectionIterator {
|
||||
ctx = trace.StartSpan(ctx, "cloud.google.com/go/firestore.DocumentRef.ListCollectionIds")
|
||||
defer func() { trace.EndSpan(ctx, nil) }()
|
||||
|
||||
client := d.Parent.c
|
||||
it := &CollectionIterator{
|
||||
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
|
||||
}
|
||||
|
||||
// Snapshots returns an iterator over snapshots of the document. Each time the document
|
||||
// changes or is added or deleted, a new snapshot will be generated.
|
||||
func (d *DocumentRef) Snapshots(ctx context.Context) *DocumentSnapshotIterator {
|
||||
return &DocumentSnapshotIterator{
|
||||
docref: d,
|
||||
ws: newWatchStreamForDocument(ctx, d),
|
||||
}
|
||||
}
|
||||
|
||||
// DocumentSnapshotIterator is an iterator over snapshots of a document.
|
||||
// Call Next on the iterator to get a snapshot of the document each time it changes.
|
||||
// Call Stop on the iterator when done.
|
||||
//
|
||||
// For an example, see DocumentRef.Snapshots.
|
||||
type DocumentSnapshotIterator struct {
|
||||
docref *DocumentRef
|
||||
ws *watchStream
|
||||
}
|
||||
|
||||
// Next blocks until the document changes, then returns the DocumentSnapshot for
|
||||
// the current state of the document. If the document has been deleted, Next
|
||||
// returns a DocumentSnapshot whose Exists method returns false.
|
||||
//
|
||||
// Next is not expected to return iterator.Done unless it is called after Stop.
|
||||
// Rarely, networking issues may also cause iterator.Done to be returned.
|
||||
func (it *DocumentSnapshotIterator) Next() (*DocumentSnapshot, error) {
|
||||
btree, _, rt, err := it.ws.nextSnapshot()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
err = iterator.Done
|
||||
}
|
||||
// watchStream's error is sticky, so SnapshotIterator does not need to remember it.
|
||||
return nil, err
|
||||
}
|
||||
if btree.Len() == 0 { // document deleted
|
||||
return &DocumentSnapshot{Ref: it.docref, ReadTime: rt}, nil
|
||||
}
|
||||
snap, _ := btree.At(0)
|
||||
return snap.(*DocumentSnapshot), nil
|
||||
}
|
||||
|
||||
// Stop stops receiving snapshots. You should always call Stop when you are done with
|
||||
// a DocumentSnapshotIterator, to free up resources. It is not safe to call Stop
|
||||
// concurrently with Next.
|
||||
func (it *DocumentSnapshotIterator) Stop() {
|
||||
it.ws.stop()
|
||||
}
|
||||
|
||||
// WithReadOptions specifies constraints for accessing documents from the database,
|
||||
// e.g. at what time snapshot to read the documents.
|
||||
func (d *DocumentRef) WithReadOptions(opts ...ReadOption) *DocumentRef {
|
||||
for _, ro := range opts {
|
||||
ro.apply(d.readSettings)
|
||||
}
|
||||
return d
|
||||
}
|
||||
Reference in New Issue
Block a user