| // 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" | |
| vkit "cloud.google.com/go/firestore/apiv1" | |
| "cloud.google.com/go/internal/trace" | |
| "google.golang.org/api/iterator" | |
| pb "google.golang.org/genproto/googleapis/firestore/v1" | |
| "google.golang.org/grpc/codes" | |
| "google.golang.org/grpc/status" | |
| ) | |
| 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. 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 | |
| } | |
| func newDocRef(parent *CollectionRef, id string) *DocumentRef { | |
| return &DocumentRef{ | |
| Parent: parent, | |
| ID: id, | |
| Path: parent.Path + "/" + id, | |
| shortPath: parent.selfPath + "/" + 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. 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 d == nil { | |
| return nil, errNilDocRef | |
| } | |
| docsnaps, err := d.Parent.c.getAll(ctx, []*DocumentRef{d}, nil) | |
| 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 | |
| // "github.com/golang/protobuf/ptypes/timestamp". | |
| // - *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 d == nil { | |
| return nil, errNilDocRef | |
| } | |
| 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 d == nil { | |
| return nil, errNilDocRef | |
| } | |
| 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 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) 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 followed by a transform. | |
| // | |
| // 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 | |
| if updateOnEmpty || len(doc.Fields) > 0 || | |
| len(updatePaths) > 0 || (pc != nil && len(transforms) == 0) { | |
| 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 := &pb.Write{ | |
| Operation: &pb.Write_Update{doc}, | |
| UpdateMask: mask, | |
| CurrentDocument: pc, | |
| } | |
| ws = append(ws, w) | |
| pc = nil // If the precondition is in the write, we don't need it in the transform. | |
| } | |
| if len(transforms) > 0 || pc != nil { | |
| ws = append(ws, &pb.Write{ | |
| Operation: &pb.Write_Transform{ | |
| Transform: &pb.DocumentTransform{ | |
| Document: d.Path, | |
| FieldTransforms: transforms, | |
| }, | |
| }, | |
| CurrentDocument: pc, | |
| }) | |
| } | |
| 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 | |
| } | |
| // 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 | |
| } | |
| ft := *ar.t | |
| 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{} | |
| } | |
| // 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, _, readTime, 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: readTime}, 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() | |
| } |