| // 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/v1beta1" |
| "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 uint64. 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, |
| }, |
| } |
| } |