github.com/SaurabhDubey-Groww/go-cloud@v0.0.0-20221124105541-b26c29285fd8/docstore/gcpfirestore/fs.go (about) 1 // Copyright 2019 The Go Cloud Development Kit Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // https://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Package gcpfirestore provides a docstore implementation backed by Google Cloud 16 // Firestore. 17 // Use OpenCollection to construct a *docstore.Collection. 18 // 19 // Docstore types not supported by the Go firestore client, cloud.google.com/go/firestore: 20 // - unsigned integers: encoded is int64s 21 // - arrays: encoded as Firestore array values 22 // 23 // Firestore types not supported by Docstore: 24 // - Document reference (a pointer to another Firestore document) 25 // 26 // # URLs 27 // 28 // For docstore.OpenCollection, gcpfirestore registers for the scheme 29 // "firestore". 30 // The default URL opener will create a connection using default credentials 31 // from the environment, as described in 32 // https://cloud.google.com/docs/authentication/production. 33 // To customize the URL opener, or for more details on the URL format, 34 // see URLOpener. 35 // See https://gocloud.dev/concepts/urls/ for background information. 36 // 37 // # As 38 // 39 // gcpfirestore exposes the following types for as functions. 40 // The pb package is google.golang.org/genproto/googleapis/firestore/v1. 41 // The firestore package is cloud.google.com/go/firestore/apiv1. 42 // - Collection.As: *firestore.Client 43 // - ActionList.BeforeDo: *pb.BatchGetDocumentsRequest or *pb.CommitRequest. 44 // - Query.BeforeQuery: *pb.RunQueryRequest 45 // - DocumentIterator: firestore.Firestore_RunQueryClient 46 // - Error: *google.golang.org/grpc/status.Status 47 // 48 // # Queries 49 // 50 // Firestore allows only one field in a query to be compared with an inequality 51 // operator (one other than "="). This driver selects the first field in a Where 52 // clause with an inequality to pass to Firestore and handles the rest locally. For 53 // example, if the query specifies the three clauses A > 1, B > 2 and A < 3, then 54 // A > 1 and A < 3 will be sent to Firestore, and the results will be filtered by 55 // B > 2 in this driver. 56 // 57 // Firestore requires a composite index if a query contains both an equality and an 58 // inequality comparison. This driver returns an error if the necessary index does 59 // not exist. You must create the index manually. See 60 // https://cloud.google.com/firestore/docs/query-data/indexing for details. 61 // 62 // See https://cloud.google.com/firestore/docs/query-data/queries for more information on Firestore queries. 63 package gcpfirestore // import "gocloud.dev/docstore/gcpfirestore" 64 65 import ( 66 "bytes" 67 "context" 68 "fmt" 69 "io" 70 "os" 71 "reflect" 72 "regexp" 73 "strings" 74 75 vkit "cloud.google.com/go/firestore/apiv1" 76 "github.com/google/wire" 77 "gocloud.dev/docstore" 78 "gocloud.dev/docstore/driver" 79 "gocloud.dev/gcerrors" 80 "gocloud.dev/gcp" 81 "gocloud.dev/internal/gcerr" 82 "gocloud.dev/internal/useragent" 83 "google.golang.org/api/option" 84 pb "google.golang.org/genproto/googleapis/firestore/v1" 85 "google.golang.org/grpc" 86 "google.golang.org/grpc/metadata" 87 "google.golang.org/grpc/status" 88 "google.golang.org/protobuf/proto" 89 tspb "google.golang.org/protobuf/types/known/timestamppb" 90 ) 91 92 // Dial returns a client to use with Firestore and a clean-up function to close 93 // the client after used. 94 // If the 'FIRESTORE_EMULATOR_HOST' environment variable is set the client connects 95 // to the GCP firestore emulator by overriding the default endpoint. 96 func Dial(ctx context.Context, ts gcp.TokenSource) (*vkit.Client, func(), error) { 97 opts := []option.ClientOption{ 98 useragent.ClientOption("docstore"), 99 } 100 if host := os.Getenv("FIRESTORE_EMULATOR_HOST"); host != "" { 101 conn, err := grpc.DialContext(ctx, host, grpc.WithInsecure()) 102 if err != nil { 103 return nil, nil, err 104 } 105 opts = append(opts, 106 option.WithEndpoint(host), 107 option.WithGRPCConn(conn), 108 ) 109 } else { 110 opts = append(opts, option.WithTokenSource(ts)) 111 } 112 c, err := vkit.NewClient(ctx, opts...) 113 return c, func() { c.Close() }, err 114 } 115 116 // Set holds Wire providers for this package. 117 var Set = wire.NewSet( 118 Dial, 119 wire.Struct(new(URLOpener), "Client"), 120 ) 121 122 type collection struct { 123 nameField string 124 nameFunc func(docstore.Document) string 125 client *vkit.Client 126 dbPath string // e.g. "projects/P/databases/(default)" 127 collPath string // e.g. "projects/P/databases/(default)/documents/States/Wisconsin/cities" 128 opts *Options 129 } 130 131 // Options contains optional arguments to the OpenCollection functions. 132 type Options struct { 133 // If true, allow queries that require client-side evaluation of filters (Where clauses) 134 // to run. 135 AllowLocalFilters bool 136 // The name of the field holding the document revision. 137 // Defaults to docstore.DefaultRevisionField. 138 RevisionField string 139 140 // The maximum number of RPCs that can be in progress for a single call to 141 // ActionList.Do. 142 // If less than 1, there is no limit. 143 MaxOutstandingActionRPCs int 144 } 145 146 // CollectionResourceID constructs a resource ID for a collection from the project ID and the collection path. 147 // See the OpenCollection example for use. 148 func CollectionResourceID(projectID, collPath string) string { 149 return fmt.Sprintf("projects/%s/databases/(default)/documents/%s", projectID, collPath) 150 } 151 152 // OpenCollection creates a *docstore.Collection representing a Firestore collection. 153 // 154 // collResourceID must be of the form "project/<projectID>/databases/(default)/documents/<collPath>". 155 // <collPath> may be a top-level collection, like "States", or it may be a path to a nested 156 // collection, like "States/Wisconsin/Cities". 157 // See https://cloud.google.com/firestore/docs/reference/rest/ for more detail. 158 // 159 // gcpfirestore requires that a single field, nameField, be designated the primary 160 // key. Its values must be strings, and must be unique over all documents in the 161 // collection. The primary key must be provided to retrieve a document. 162 func OpenCollection(client *vkit.Client, collResourceID, nameField string, opts *Options) (*docstore.Collection, error) { 163 c, err := newCollection(client, collResourceID, nameField, nil, opts) 164 if err != nil { 165 return nil, err 166 } 167 return docstore.NewCollection(c), nil 168 } 169 170 // OpenCollectionWithNameFunc creates a *docstore.Collection representing a Firestore collection. 171 // 172 // collResourceID must be of the form "project/<projectID>/databases/(default)/documents/<collPath>". 173 // <collPath> may be a top-level collection, like "States", or it may be a path to a nested 174 // collection, like "States/Wisconsin/Cities". 175 // 176 // The nameFunc argument is function that accepts a document and returns the value to 177 // be used for the document's primary key. It should return the empty string if the 178 // document is missing the information to construct a name. This will cause all 179 // actions, even Create, to fail. 180 // 181 // Providing a function to construct the primary key is useful in two situations: if 182 // your desired primary key field is not a string, or if there is more than one field 183 // you want to use as a primary key. 184 // 185 // For the collection to be usable with Query.Delete and Query.Update, nameFunc 186 // must work with both map and struct types representing the same underlying 187 // data structure. See gocloud.dev/docstore/drivertest.HighScoreKey for an example. 188 func OpenCollectionWithNameFunc(client *vkit.Client, collResourceID string, nameFunc func(docstore.Document) string, opts *Options) (*docstore.Collection, error) { 189 c, err := newCollection(client, collResourceID, "", nameFunc, opts) 190 if err != nil { 191 return nil, err 192 } 193 return docstore.NewCollection(c), nil 194 } 195 196 var resourceIDRE = regexp.MustCompile(`^(projects/[^/]+/databases/\(default\))/documents/.+`) 197 198 func newCollection(client *vkit.Client, collResourceID, nameField string, nameFunc func(docstore.Document) string, opts *Options) (*collection, error) { 199 if nameField == "" && nameFunc == nil { 200 return nil, gcerr.Newf(gcerr.InvalidArgument, nil, "one of nameField or nameFunc must be provided") 201 } 202 matches := resourceIDRE.FindStringSubmatch(collResourceID) 203 if matches == nil { 204 return nil, gcerr.Newf(gcerr.InvalidArgument, nil, "bad collection resource ID %q; must match %v", 205 collResourceID, resourceIDRE) 206 } 207 dbPath := matches[1] 208 if opts == nil { 209 opts = &Options{} 210 } 211 if opts.RevisionField == "" { 212 opts.RevisionField = docstore.DefaultRevisionField 213 } 214 return &collection{ 215 client: client, 216 nameField: nameField, 217 nameFunc: nameFunc, 218 dbPath: dbPath, 219 collPath: collResourceID, 220 opts: opts, 221 }, nil 222 } 223 224 // Key returns the document key, if present. This is either the value of the field 225 // called c.nameField, or the result of calling c.nameFunc. 226 func (c *collection) Key(doc driver.Document) (interface{}, error) { 227 if c.nameField != "" { 228 name, err := doc.GetField(c.nameField) 229 vn := reflect.ValueOf(name) 230 if err != nil || name == nil || driver.IsEmptyValue(vn) { 231 // missing field is not an error 232 return nil, nil 233 } 234 // Check that the reflect kind is String so we can support any type whose underlying type 235 // is string. E.g. "type DocName string". 236 if vn.Kind() != reflect.String { 237 return nil, gcerr.Newf(gcerr.InvalidArgument, nil, "key field %q with value %v is not a string", 238 c.nameField, name) 239 } 240 sname := vn.String() 241 if sname == "" { // empty string is the same as missing 242 return nil, nil 243 } 244 return sname, nil 245 } 246 sname := c.nameFunc(doc.Origin) 247 if sname == "" { 248 return nil, gcerr.Newf(gcerr.InvalidArgument, nil, "missing document key") 249 } 250 return sname, nil 251 } 252 253 func (c *collection) RevisionField() string { 254 return c.opts.RevisionField 255 } 256 257 // RunActions implements driver.RunActions. 258 func (c *collection) RunActions(ctx context.Context, actions []*driver.Action, opts *driver.RunActionsOptions) driver.ActionListError { 259 errs := make([]error, len(actions)) 260 beforeGets, gets, writes, afterGets := driver.GroupActions(actions) 261 calls := c.buildCommitCalls(writes, errs) 262 // runGets does not issue concurrent RPCs, so it doesn't need a throttle. 263 c.runGets(ctx, beforeGets, errs, opts) 264 t := driver.NewThrottle(c.opts.MaxOutstandingActionRPCs) 265 for _, call := range calls { 266 call := call 267 t.Acquire() 268 go func() { 269 defer t.Release() 270 c.doCommitCall(ctx, call, errs, opts) 271 }() 272 } 273 t.Acquire() 274 c.runGets(ctx, gets, errs, opts) 275 t.Release() 276 t.Wait() 277 c.runGets(ctx, afterGets, errs, opts) 278 return driver.NewActionListError(errs) 279 } 280 281 // runGets executes a group of Get actions by calling the BatchGetDocuments RPC. 282 // It may make several calls, because all gets in a single RPC must have the same set of field paths. 283 func (c *collection) runGets(ctx context.Context, actions []*driver.Action, errs []error, opts *driver.RunActionsOptions) { 284 for _, group := range driver.GroupByFieldPath(actions) { 285 c.batchGet(ctx, group, errs, opts) 286 } 287 } 288 289 // Run a single BatchGet RPC with the given Get actions, all of which have the same set of field paths. 290 // Populate errs, a slice of per-action errors indexed by the original action list position. 291 func (c *collection) batchGet(ctx context.Context, gets []*driver.Action, errs []error, opts *driver.RunActionsOptions) { 292 setErr := func(err error) { 293 for _, g := range gets { 294 errs[g.Index] = err 295 } 296 } 297 298 req, err := c.newGetRequest(gets) 299 if err != nil { 300 setErr(err) 301 return 302 } 303 indexByPath := map[string]int{} // from document path to index in gets 304 for i, path := range req.Documents { 305 indexByPath[path] = i 306 } 307 if opts.BeforeDo != nil { 308 if err := opts.BeforeDo(driver.AsFunc(req)); err != nil { 309 setErr(err) 310 return 311 } 312 } 313 streamClient, err := c.client.BatchGetDocuments(withResourceHeader(ctx, req.Database), req) 314 if err != nil { 315 setErr(err) 316 return 317 } 318 for { 319 resp, err := streamClient.Recv() 320 if err == io.EOF { 321 break 322 } 323 if err != nil { 324 setErr(err) 325 return 326 } 327 switch r := resp.Result.(type) { 328 case *pb.BatchGetDocumentsResponse_Found: 329 pdoc := r.Found 330 i, ok := indexByPath[pdoc.Name] 331 if !ok { 332 setErr(gcerr.Newf(gcerr.Internal, nil, "no index for path %s", pdoc.Name)) 333 } else { 334 errs[gets[i].Index] = decodeDoc(pdoc, gets[i].Doc, c.nameField, c.opts.RevisionField) 335 } 336 case *pb.BatchGetDocumentsResponse_Missing: 337 i := indexByPath[r.Missing] 338 errs[gets[i].Index] = gcerr.Newf(gcerr.NotFound, nil, "document at path %q is missing", r.Missing) 339 default: 340 setErr(gcerr.Newf(gcerr.Internal, nil, "unknown BatchGetDocumentsResponse result type")) 341 return 342 } 343 } 344 } 345 346 func (c *collection) newGetRequest(gets []*driver.Action) (*pb.BatchGetDocumentsRequest, error) { 347 req := &pb.BatchGetDocumentsRequest{Database: c.dbPath} 348 for _, a := range gets { 349 req.Documents = append(req.Documents, c.collPath+"/"+a.Key.(string)) 350 } 351 // groupActions has already made sure that all the actions have the same field paths, 352 // so just use the first one. 353 var fps []string // field paths that will go in the mask 354 for _, fp := range gets[0].FieldPaths { 355 fps = append(fps, toServiceFieldPath(fp)) 356 } 357 if fps != nil { 358 req.Mask = &pb.DocumentMask{FieldPaths: fps} 359 } 360 return req, nil 361 } 362 363 // commitCall holds information needed to make a Commit RPC and to follow up after it is done. 364 type commitCall struct { 365 writes []*pb.Write // writes to commit 366 actions []*driver.Action // actions corresponding to those writes 367 newNames []string // new names for Create; parallel to actions 368 } 369 370 // Construct a set of concurrently runnable calls to Commit. 371 func (c *collection) buildCommitCalls(actions []*driver.Action, errs []error) []*commitCall { 372 // Convert each action to one or more writes, collecting names for newly created 373 // documents along the way. Divide writes into those with preconditions and those without. 374 // Writes without preconditions can't fail, so we can execute them all in one Commit RPC. 375 // All other writes must be run as separate Commits. 376 var ( 377 nCall = &commitCall{} // for writes without preconditions 378 pCalls []*commitCall // for writes with preconditions 379 ) 380 for _, a := range actions { 381 ws, nn, err := c.actionToWrites(a) 382 if err != nil { 383 errs[a.Index] = err 384 } else if ws[0].CurrentDocument == nil { // no precondition 385 nCall.writes = append(nCall.writes, ws...) 386 nCall.actions = append(nCall.actions, a) 387 nCall.newNames = append(nCall.newNames, nn) 388 } else { // writes have a precondition 389 pCalls = append(pCalls, &commitCall{ 390 writes: ws, 391 actions: []*driver.Action{a}, 392 newNames: []string{nn}, 393 }) 394 } 395 } 396 if len(nCall.writes) == 0 { 397 return pCalls 398 } 399 return append(pCalls, nCall) 400 } 401 402 // Convert an action to one or more Firestore Write protos. 403 func (c *collection) actionToWrites(a *driver.Action) ([]*pb.Write, string, error) { 404 var ( 405 w *pb.Write 406 ws []*pb.Write 407 err error 408 docName string 409 newName string // for Create with no name 410 ) 411 if a.Key != nil { 412 docName = a.Key.(string) 413 } 414 switch a.Kind { 415 case driver.Create: 416 // Make a name for this document if it doesn't have one. 417 if a.Key == nil { 418 docName = driver.UniqueString() 419 newName = docName 420 } 421 w, err = c.putWrite(a.Doc, docName, &pb.Precondition{ConditionType: &pb.Precondition_Exists{Exists: false}}) 422 423 case driver.Replace: 424 // If the given document has a revision, use it as the precondition (it implies existence). 425 pc, perr := c.revisionPrecondition(a.Doc) 426 if perr != nil { 427 return nil, "", perr 428 } 429 // Otherwise, just require that the document exists. 430 if pc == nil { 431 pc = &pb.Precondition{ConditionType: &pb.Precondition_Exists{Exists: true}} 432 } 433 w, err = c.putWrite(a.Doc, docName, pc) 434 435 case driver.Put: 436 pc, perr := c.revisionPrecondition(a.Doc) 437 if perr != nil { 438 return nil, "", perr 439 } 440 w, err = c.putWrite(a.Doc, docName, pc) 441 442 case driver.Update: 443 ws, err = c.updateWrites(a.Doc, docName, a.Mods) 444 445 case driver.Delete: 446 w, err = c.deleteWrite(a.Doc, docName) 447 448 default: 449 err = gcerr.Newf(gcerr.Internal, nil, "bad action %+v", a) 450 } 451 if err != nil { 452 return nil, "", err 453 } 454 if ws == nil { 455 ws = []*pb.Write{w} 456 } 457 return ws, newName, nil 458 } 459 460 func (c *collection) putWrite(doc driver.Document, docName string, pc *pb.Precondition) (*pb.Write, error) { 461 pdoc, err := encodeDoc(doc, c.nameField) 462 if err != nil { 463 return nil, err 464 } 465 pdoc.Name = c.collPath + "/" + docName 466 return &pb.Write{ 467 Operation: &pb.Write_Update{Update: pdoc}, 468 CurrentDocument: pc, 469 }, nil 470 } 471 472 func (c *collection) deleteWrite(doc driver.Document, docName string) (*pb.Write, error) { 473 pc, err := c.revisionPrecondition(doc) 474 if err != nil { 475 return nil, err 476 } 477 return &pb.Write{ 478 Operation: &pb.Write_Delete{Delete: c.collPath + "/" + docName}, 479 CurrentDocument: pc, 480 }, nil 481 } 482 483 // updateWrites returns a slice of writes because we may need two: one for setting 484 // and deleting values, the other for transforms. 485 func (c *collection) updateWrites(doc driver.Document, docName string, mods []driver.Mod) ([]*pb.Write, error) { 486 ts, err := c.revisionTimestamp(doc) 487 if err != nil { 488 return nil, err 489 } 490 fields, paths, transforms, err := processMods(mods) 491 if err != nil { 492 return nil, err 493 } 494 return newUpdateWrites(c.collPath+"/"+docName, ts, fields, paths, transforms) 495 } 496 497 func newUpdateWrites(docPath string, ts *tspb.Timestamp, fields map[string]*pb.Value, paths []string, transforms []*pb.DocumentTransform_FieldTransform) ([]*pb.Write, error) { 498 pc := preconditionFromTimestamp(ts) 499 // If there is no revision in the document, add a precondition that the document exists. 500 if pc == nil { 501 pc = &pb.Precondition{ConditionType: &pb.Precondition_Exists{Exists: true}} 502 } 503 var ws []*pb.Write 504 if len(fields) > 0 || len(paths) > 0 { 505 ws = []*pb.Write{{ 506 Operation: &pb.Write_Update{Update: &pb.Document{ 507 Name: docPath, 508 Fields: fields, 509 }}, 510 UpdateMask: &pb.DocumentMask{FieldPaths: paths}, 511 CurrentDocument: pc, 512 }} 513 pc = nil // If the precondition is in the write, we don't need it in the transform. 514 } 515 if len(transforms) > 0 { 516 ws = append(ws, &pb.Write{ 517 Operation: &pb.Write_Transform{ 518 Transform: &pb.DocumentTransform{ 519 Document: docPath, 520 FieldTransforms: transforms, 521 }, 522 }, 523 CurrentDocument: pc, 524 }) 525 } 526 return ws, nil 527 } 528 529 // To update a document, we need to send: 530 // - A document with all the fields we want to add or change. 531 // - A mask with the field paths of all the fields we want to add, change or delete. 532 // processMods converts the mods into the fields for the document, and a list of 533 // valid Firestore field paths for the mask. 534 func processMods(mods []driver.Mod) (fields map[string]*pb.Value, maskPaths []string, transforms []*pb.DocumentTransform_FieldTransform, err error) { 535 fields = map[string]*pb.Value{} 536 for _, m := range mods { 537 sfp := toServiceFieldPath(m.FieldPath) 538 // If m.Value is nil, we want to delete it. In that case, we put the field in 539 // the mask but not in the doc. 540 if inc, ok := m.Value.(driver.IncOp); ok { 541 pv, err := encodeValue(inc.Amount) 542 if err != nil { 543 return nil, nil, nil, err 544 } 545 transforms = append(transforms, &pb.DocumentTransform_FieldTransform{ 546 FieldPath: sfp, 547 TransformType: &pb.DocumentTransform_FieldTransform_Increment{ 548 Increment: pv, 549 }, 550 }) 551 } else { 552 // The field path of every other mod belongs in the mask. 553 maskPaths = append(maskPaths, sfp) 554 if m.Value != nil { 555 pv, err := encodeValue(m.Value) 556 if err != nil { 557 return nil, nil, nil, err 558 } 559 if err := setAtFieldPath(fields, m.FieldPath, pv); err != nil { 560 return nil, nil, nil, err 561 } 562 } 563 } 564 } 565 return fields, maskPaths, transforms, nil 566 } 567 568 // doCommitCall Calls the Commit RPC with a list of writes, and handles the results. 569 func (c *collection) doCommitCall(ctx context.Context, call *commitCall, errs []error, opts *driver.RunActionsOptions) { 570 wrs, err := c.commit(ctx, call.writes, opts) 571 if err != nil { 572 for _, a := range call.actions { 573 errs[a.Index] = err 574 } 575 return 576 } 577 // Set the revision fields of the documents. 578 // The actions and writes may not correspond, because Update actions may require 579 // two writes. We can tell which writes correspond to actions by the type of write. 580 j := 0 581 for i, a := range call.actions { 582 wr := wrs[j] 583 if a.Doc.HasField(c.opts.RevisionField) { 584 if err := a.Doc.SetField(c.opts.RevisionField, wr.UpdateTime); err != nil { 585 errs[a.Index] = err 586 } 587 } 588 if call.newNames[i] != "" { 589 // c.nameField should not be empty since we only create new names when there 590 // is a nameField. 591 _ = a.Doc.SetField(c.nameField, call.newNames[i]) 592 } 593 if hasFollowingTransform(call.writes, j) { 594 j = j + 2 595 } else { 596 j++ 597 } 598 } 599 return 600 } 601 602 func hasFollowingTransform(writes []*pb.Write, i int) bool { 603 if i >= len(writes)-1 { 604 return false 605 } 606 curr, ok := writes[i].Operation.(*pb.Write_Update) 607 if !ok { 608 return false 609 } 610 next, ok := writes[i+1].Operation.(*pb.Write_Transform) 611 if !ok { 612 return false 613 } 614 return curr.Update.Name == next.Transform.Document 615 } 616 617 func (c *collection) commit(ctx context.Context, ws []*pb.Write, opts *driver.RunActionsOptions) ([]*pb.WriteResult, error) { 618 req := &pb.CommitRequest{ 619 Database: c.dbPath, 620 Writes: ws, 621 } 622 if opts.BeforeDo != nil { 623 if err := opts.BeforeDo(driver.AsFunc(req)); err != nil { 624 return nil, err 625 } 626 } 627 res, err := c.client.Commit(withResourceHeader(ctx, req.Database), req) 628 if err != nil { 629 return nil, err 630 } 631 if len(res.WriteResults) != len(ws) { 632 return nil, gcerr.Newf(gcerr.Internal, nil, "wrong number of WriteResults from firestore commit") 633 } 634 return res.WriteResults, nil 635 } 636 637 /////////////// 638 // From memdocstore/mem.go. 639 640 // setAtFieldPath sets m's value at fp to val. It creates intermediate maps as 641 // needed. It returns an error if a non-final component of fp does not denote a map. 642 func setAtFieldPath(m map[string]*pb.Value, fp []string, val *pb.Value) error { 643 m2, err := getParentMap(m, fp, true) 644 if err != nil { 645 return err 646 } 647 m2[fp[len(fp)-1]] = val 648 return nil 649 } 650 651 // getParentMap returns the map that directly contains the given field path; 652 // that is, the value of m at the field path that excludes the last component 653 // of fp. If a non-map is encountered along the way, an InvalidArgument error is 654 // returned. If nil is encountered, nil is returned unless create is true, in 655 // which case a map is added at that point. 656 func getParentMap(m map[string]*pb.Value, fp []string, create bool) (map[string]*pb.Value, error) { 657 for _, k := range fp[:len(fp)-1] { 658 if m[k] == nil { 659 if !create { 660 return nil, nil 661 } 662 m[k] = &pb.Value{ValueType: &pb.Value_MapValue{&pb.MapValue{Fields: map[string]*pb.Value{}}}} 663 } 664 mv := m[k].GetMapValue() 665 if mv == nil { 666 return nil, gcerr.Newf(gcerr.InvalidArgument, nil, "invalid field path %q at %q", strings.Join(fp, "."), k) 667 } 668 m = mv.Fields 669 } 670 return m, nil 671 } 672 673 //////////////// 674 // From fieldpath.go in cloud.google.com/go/firestore. 675 676 // Convert a docstore field path, which is a []string, into the kind of field path 677 // that the Firestore service expects: a string of dot-separated components, some of 678 // which may be quoted. 679 func toServiceFieldPath(fp []string) string { 680 cs := make([]string, len(fp)) 681 for i, c := range fp { 682 cs[i] = toServiceFieldPathComponent(c) 683 } 684 return strings.Join(cs, ".") 685 } 686 687 // Google SQL syntax for an unquoted field. 688 var unquotedFieldRE = regexp.MustCompile("^[A-Za-z_][A-Za-z_0-9]*$") 689 690 // toServiceFieldPathComponent returns a string that represents key and is a valid 691 // Firestore field path component. Components must be quoted with backticks if 692 // they don't match the above regexp. 693 func toServiceFieldPathComponent(key string) string { 694 if unquotedFieldRE.MatchString(key) { 695 return key 696 } 697 var buf bytes.Buffer 698 buf.WriteRune('`') 699 for _, r := range key { 700 if r == '`' || r == '\\' { 701 buf.WriteRune('\\') 702 } 703 buf.WriteRune(r) 704 } 705 buf.WriteRune('`') 706 return buf.String() 707 } 708 709 // revisionPrecondition returns a Firestore precondition that asserts that the stored document's 710 // revision matches the revision of doc. 711 func (c *collection) revisionPrecondition(doc driver.Document) (*pb.Precondition, error) { 712 rev, err := c.revisionTimestamp(doc) 713 if err != nil { 714 return nil, err 715 } 716 return preconditionFromTimestamp(rev), nil 717 } 718 719 // revisionTimestamp extracts the timestamp from the revision field of doc, if there is one. 720 // It only returns an error if the revision field is present and does not contain the right type. 721 func (c *collection) revisionTimestamp(doc driver.Document) (*tspb.Timestamp, error) { 722 v, err := doc.GetField(c.opts.RevisionField) 723 if err != nil { // revision field not present 724 return nil, nil 725 } 726 if v == nil { // revision field is present, but nil 727 return nil, nil 728 } 729 rev, ok := v.(*tspb.Timestamp) 730 if !ok { 731 return nil, gcerr.Newf(gcerr.InvalidArgument, nil, 732 "%s field contains wrong type: got %T, want proto Timestamp", 733 c.opts.RevisionField, v) 734 } 735 return rev, nil 736 } 737 738 func preconditionFromTimestamp(ts *tspb.Timestamp) *pb.Precondition { 739 if ts == nil || (ts.Seconds == 0 && ts.Nanos == 0) { // ignore a missing or zero revision 740 return nil 741 } 742 return &pb.Precondition{ConditionType: &pb.Precondition_UpdateTime{ts}} 743 } 744 745 func (c *collection) ErrorCode(err error) gcerrors.ErrorCode { 746 return gcerr.GRPCCode(err) 747 } 748 749 // resourcePrefixHeader is the name of the metadata header used to indicate 750 // the resource being operated on. 751 const resourcePrefixHeader = "google-cloud-resource-prefix" 752 753 // withResourceHeader returns a new context that includes resource in a special header. 754 // Firestore uses the resource header for routing. 755 func withResourceHeader(ctx context.Context, resource string) context.Context { 756 md, _ := metadata.FromOutgoingContext(ctx) 757 md = md.Copy() 758 md[resourcePrefixHeader] = []string{resource} 759 return metadata.NewOutgoingContext(ctx, md) 760 } 761 762 // RevisionToBytes implements driver.RevisionToBytes. 763 func (c *collection) RevisionToBytes(rev interface{}) ([]byte, error) { 764 r, ok := rev.(*tspb.Timestamp) 765 if !ok { 766 return nil, gcerr.Newf(gcerr.InvalidArgument, nil, "revision %v of type %[1]T is not a proto Timestamp", rev) 767 } 768 return proto.Marshal(r) 769 } 770 771 // BytesToRevision implements driver.BytesToRevision. 772 func (c *collection) BytesToRevision(b []byte) (interface{}, error) { 773 var ts tspb.Timestamp 774 if err := proto.Unmarshal(b, &ts); err != nil { 775 return nil, err 776 } 777 return &ts, nil 778 } 779 780 func (c *collection) As(i interface{}) bool { 781 p, ok := i.(**vkit.Client) 782 if !ok { 783 return false 784 } 785 *p = c.client 786 return true 787 } 788 789 // ErrorAs implements driver.Collection.ErrorAs. 790 func (c *collection) ErrorAs(err error, i interface{}) bool { 791 s, ok := status.FromError(err) 792 if !ok { 793 return false 794 } 795 p, ok := i.(**status.Status) 796 if !ok { 797 return false 798 } 799 *p = s 800 return true 801 } 802 803 // Close implements driver.Collection.Close. 804 func (c *collection) Close() error { return nil }