302 lines
9.1 KiB
Go
302 lines
9.1 KiB
Go
|
// 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"
|
|||
|
"fmt"
|
|||
|
"reflect"
|
|||
|
"time"
|
|||
|
|
|||
|
pb "google.golang.org/genproto/googleapis/firestore/v1beta1"
|
|||
|
"google.golang.org/grpc/codes"
|
|||
|
"google.golang.org/grpc/status"
|
|||
|
|
|||
|
"github.com/golang/protobuf/ptypes"
|
|||
|
tspb "github.com/golang/protobuf/ptypes/timestamp"
|
|||
|
)
|
|||
|
|
|||
|
// 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 initally 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 DocumentRefs.
|
|||
|
//
|
|||
|
// Field names given by struct field tags are observed, as described in
|
|||
|
// DocumentRef.Create.
|
|||
|
//
|
|||
|
// 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{&pb.Value_MapValue{&pb.MapValue{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 of field paths for DocumentTransform (server timestamp).
|
|||
|
func toProtoDocument(x interface{}) (*pb.Document, []FieldPath, error) {
|
|||
|
if x == nil {
|
|||
|
return nil, nil, errors.New("firestore: nil document contents")
|
|||
|
}
|
|||
|
v := reflect.ValueOf(x)
|
|||
|
pv, sawTransform, err := toProtoValue(v)
|
|||
|
if err != nil {
|
|||
|
return nil, nil, err
|
|||
|
}
|
|||
|
var fieldPaths []FieldPath
|
|||
|
if sawTransform {
|
|||
|
fieldPaths, err = extractTransformPaths(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 covert value of type %T into a map", x)
|
|||
|
}
|
|||
|
fields = m.Fields
|
|||
|
}
|
|||
|
return &pb.Document{Fields: fields}, fieldPaths, nil
|
|||
|
}
|
|||
|
|
|||
|
func extractTransformPaths(v reflect.Value, prefix FieldPath) ([]FieldPath, error) {
|
|||
|
switch v.Kind() {
|
|||
|
case reflect.Map:
|
|||
|
return extractTransformPathsFromMap(v, prefix)
|
|||
|
case reflect.Struct:
|
|||
|
return extractTransformPathsFromStruct(v, prefix)
|
|||
|
case reflect.Ptr:
|
|||
|
if v.IsNil() {
|
|||
|
return nil, nil
|
|||
|
}
|
|||
|
return extractTransformPaths(v.Elem(), prefix)
|
|||
|
case reflect.Interface:
|
|||
|
if v.NumMethod() == 0 { // empty interface: recurse on its contents
|
|||
|
return extractTransformPaths(v.Elem(), prefix)
|
|||
|
}
|
|||
|
return nil, nil
|
|||
|
default:
|
|||
|
return nil, nil
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
func extractTransformPathsFromMap(v reflect.Value, prefix FieldPath) ([]FieldPath, error) {
|
|||
|
var paths []FieldPath
|
|||
|
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 {
|
|||
|
paths = append(paths, path)
|
|||
|
} else {
|
|||
|
ps, err := extractTransformPaths(mi, path)
|
|||
|
if err != nil {
|
|||
|
return nil, err
|
|||
|
}
|
|||
|
paths = append(paths, ps...)
|
|||
|
}
|
|||
|
}
|
|||
|
return paths, nil
|
|||
|
}
|
|||
|
|
|||
|
func extractTransformPathsFromStruct(v reflect.Value, prefix FieldPath) ([]FieldPath, error) {
|
|||
|
var paths []FieldPath
|
|||
|
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 {
|
|||
|
paths = append(paths, path)
|
|||
|
}
|
|||
|
} else {
|
|||
|
ps, err := extractTransformPaths(fv, path)
|
|||
|
if err != nil {
|
|||
|
return nil, err
|
|||
|
}
|
|||
|
paths = append(paths, ps...)
|
|||
|
}
|
|||
|
}
|
|||
|
return paths, 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
|
|||
|
}
|