// 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 ( "errors" "fmt" "reflect" "time" "github.com/golang/protobuf/ptypes" tspb "github.com/golang/protobuf/ptypes/timestamp" pb "google.golang.org/genproto/googleapis/firestore/v1" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) // A DocumentSnapshot contains document data and metadata. type DocumentSnapshot struct { // The DocumentRef for this document. Ref *DocumentRef // Read-only. The time at which the document was created. // Increases monotonically when a document is deleted then // recreated. It can also be compared to values from other documents and // the read time of a query. CreateTime time.Time // Read-only. The time at which the document was last changed. This value // is initially set to CreateTime then increases monotonically with each // change to the document. It can also be compared to values from other // documents and the read time of a query. UpdateTime time.Time // Read-only. The time at which the document was read. ReadTime time.Time c *Client proto *pb.Document } // Exists reports whether the DocumentSnapshot represents an existing document. // Even if Exists returns false, the Ref and ReadTime fields of the DocumentSnapshot // are valid. func (d *DocumentSnapshot) Exists() bool { return d.proto != nil } // Data returns the DocumentSnapshot's fields as a map. // It is equivalent to // var m map[string]interface{} // d.DataTo(&m) // except that it returns nil if the document does not exist. func (d *DocumentSnapshot) Data() map[string]interface{} { if !d.Exists() { return nil } m, err := createMapFromValueMap(d.proto.Fields, d.c) // Any error here is a bug in the client. if err != nil { panic(fmt.Sprintf("firestore: %v", err)) } return m } // DataTo uses the document's fields to populate p, which can be a pointer to a // map[string]interface{} or a pointer to a struct. // // Firestore field values are converted to Go values as follows: // - Null converts to nil. // - Bool converts to bool. // - String converts to string. // - Integer converts int64. When setting a struct field, any signed or unsigned // integer type is permitted except uint, uint64 or uintptr. Overflow is detected // and results in an error. // - Double converts to float64. When setting a struct field, float32 is permitted. // Overflow is detected and results in an error. // - Bytes is converted to []byte. // - Timestamp converts to time.Time. // - GeoPoint converts to *latlng.LatLng, where latlng is the package // "google.golang.org/genproto/googleapis/type/latlng". // - Arrays convert to []interface{}. When setting a struct field, the field // may be a slice or array of any type and is populated recursively. // Slices are resized to the incoming value's size, while arrays that are too // long have excess elements filled with zero values. If the array is too short, // excess incoming values will be dropped. // - Maps convert to map[string]interface{}. When setting a struct field, // maps of key type string and any value type are permitted, and are populated // recursively. // - References are converted to *firestore.DocumentRefs. // // Field names given by struct field tags are observed, as described in // DocumentRef.Create. // // Only the fields actually present in the document are used to populate p. Other fields // of p are left unchanged. // // If the document does not exist, DataTo returns a NotFound error. func (d *DocumentSnapshot) DataTo(p interface{}) error { if !d.Exists() { return status.Errorf(codes.NotFound, "document %s does not exist", d.Ref.Path) } return setFromProtoValue(p, &pb.Value{ValueType: &pb.Value_MapValue{&pb.MapValue{Fields: d.proto.Fields}}}, d.c) } // DataAt returns the data value denoted by path. // // The path argument can be a single field or a dot-separated sequence of // fields, and must not contain any of the runes "˜*/[]". Use DataAtPath instead for // such a path. // // See DocumentSnapshot.DataTo for how Firestore values are converted to Go values. // // If the document does not exist, DataAt returns a NotFound error. func (d *DocumentSnapshot) DataAt(path string) (interface{}, error) { if !d.Exists() { return nil, status.Errorf(codes.NotFound, "document %s does not exist", d.Ref.Path) } fp, err := parseDotSeparatedString(path) if err != nil { return nil, err } return d.DataAtPath(fp) } // DataAtPath returns the data value denoted by the FieldPath fp. // If the document does not exist, DataAtPath returns a NotFound error. func (d *DocumentSnapshot) DataAtPath(fp FieldPath) (interface{}, error) { if !d.Exists() { return nil, status.Errorf(codes.NotFound, "document %s does not exist", d.Ref.Path) } v, err := valueAtPath(fp, d.proto.Fields) if err != nil { return nil, err } return createFromProtoValue(v, d.c) } // valueAtPath returns the value of m referred to by fp. func valueAtPath(fp FieldPath, m map[string]*pb.Value) (*pb.Value, error) { for _, k := range fp[:len(fp)-1] { v := m[k] if v == nil { return nil, fmt.Errorf("firestore: no field %q", k) } mv := v.GetMapValue() if mv == nil { return nil, fmt.Errorf("firestore: value for field %q is not a map", k) } m = mv.Fields } k := fp[len(fp)-1] v := m[k] if v == nil { return nil, fmt.Errorf("firestore: no field %q", k) } return v, nil } // toProtoDocument converts a Go value to a Document proto. // Valid values are: map[string]T, struct, or pointer to a valid value. // It also returns a list DocumentTransforms. func toProtoDocument(x interface{}) (*pb.Document, []*pb.DocumentTransform_FieldTransform, error) { if x == nil { return nil, nil, errors.New("firestore: nil document contents") } v := reflect.ValueOf(x) pv, _, err := toProtoValue(v) if err != nil { return nil, nil, err } var transforms []*pb.DocumentTransform_FieldTransform transforms, err = extractTransforms(v, nil) if err != nil { return nil, nil, err } var fields map[string]*pb.Value if pv != nil { m := pv.GetMapValue() if m == nil { return nil, nil, fmt.Errorf("firestore: cannot convert value of type %T into a map", x) } fields = m.Fields } return &pb.Document{Fields: fields}, transforms, nil } func extractTransforms(v reflect.Value, prefix FieldPath) ([]*pb.DocumentTransform_FieldTransform, error) { switch v.Kind() { case reflect.Map: return extractTransformsFromMap(v, prefix) case reflect.Struct: return extractTransformsFromStruct(v, prefix) case reflect.Ptr: if v.IsNil() { return nil, nil } return extractTransforms(v.Elem(), prefix) case reflect.Interface: if v.NumMethod() == 0 { // empty interface: recurse on its contents return extractTransforms(v.Elem(), prefix) } return nil, nil default: return nil, nil } } func extractTransformsFromMap(v reflect.Value, prefix FieldPath) ([]*pb.DocumentTransform_FieldTransform, error) { var transforms []*pb.DocumentTransform_FieldTransform for _, k := range v.MapKeys() { sk := k.Interface().(string) // assume keys are strings; checked in toProtoValue path := prefix.with(sk) mi := v.MapIndex(k) if mi.Interface() == ServerTimestamp { transforms = append(transforms, serverTimestamp(path.toServiceFieldPath())) } else if au, ok := mi.Interface().(arrayUnion); ok { t, err := arrayUnionTransform(au, path) if err != nil { return nil, err } transforms = append(transforms, t) } else if ar, ok := mi.Interface().(arrayRemove); ok { t, err := arrayRemoveTransform(ar, path) if err != nil { return nil, err } transforms = append(transforms, t) } else { ps, err := extractTransforms(mi, path) if err != nil { return nil, err } transforms = append(transforms, ps...) } } return transforms, nil } func extractTransformsFromStruct(v reflect.Value, prefix FieldPath) ([]*pb.DocumentTransform_FieldTransform, error) { var transforms []*pb.DocumentTransform_FieldTransform fields, err := fieldCache.Fields(v.Type()) if err != nil { return nil, err } for _, f := range fields { fv := v.FieldByIndex(f.Index) path := prefix.with(f.Name) opts := f.ParsedTag.(tagOptions) if opts.serverTimestamp { var isZero bool switch f.Type { case typeOfGoTime: isZero = fv.Interface().(time.Time).IsZero() case reflect.PtrTo(typeOfGoTime): isZero = fv.IsNil() || fv.Elem().Interface().(time.Time).IsZero() default: return nil, fmt.Errorf("firestore: field %s of struct %s with serverTimestamp tag must be of type time.Time or *time.Time", f.Name, v.Type()) } if isZero { transforms = append(transforms, serverTimestamp(path.toServiceFieldPath())) } } else { ps, err := extractTransforms(fv, path) if err != nil { return nil, err } transforms = append(transforms, ps...) } } return transforms, nil } func newDocumentSnapshot(ref *DocumentRef, proto *pb.Document, c *Client, readTime *tspb.Timestamp) (*DocumentSnapshot, error) { d := &DocumentSnapshot{ Ref: ref, c: c, proto: proto, } if proto != nil { ts, err := ptypes.Timestamp(proto.CreateTime) if err != nil { return nil, err } d.CreateTime = ts ts, err = ptypes.Timestamp(proto.UpdateTime) if err != nil { return nil, err } d.UpdateTime = ts } if readTime != nil { ts, err := ptypes.Timestamp(readTime) if err != nil { return nil, err } d.ReadTime = ts } return d, nil } func serverTimestamp(path string) *pb.DocumentTransform_FieldTransform { return &pb.DocumentTransform_FieldTransform{ FieldPath: path, TransformType: &pb.DocumentTransform_FieldTransform_SetToServerValue{ SetToServerValue: pb.DocumentTransform_FieldTransform_REQUEST_TIME, }, } }