# 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 # # https://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. require "google/cloud/firestore/v1beta1" require "google/cloud/firestore/service" require "google/cloud/firestore/field_path" require "google/cloud/firestore/field_value" require "google/cloud/firestore/collection_reference" require "google/cloud/firestore/document_reference" require "google/cloud/firestore/document_snapshot" require "google/cloud/firestore/batch" require "google/cloud/firestore/transaction" module Google module Cloud module Firestore ## # # Client # # The Cloud Firestore Client used is to access and manipulate the # collections and documents in the Firestore database. # # @example # require "google/cloud/firestore" # # firestore = Google::Cloud::Firestore.new # # # Get a document reference # nyc_ref = firestore.doc "cities/NYC" # # firestore.batch do |b| # b.update(nyc_ref, { name: "New York City" }) # end # class Client ## # @private The Service object. attr_accessor :service ## # @private Creates a new Firestore Database instance. def initialize service @service = service end ## # The project identifier for the Cloud Firestore database. # # @return [String] project identifier. def project_id service.project end ## # The database identifier for the Cloud Firestore database. # # @return [String] database identifier. def database_id "(default)" end ## # @private The full Database path for the Cloud Firestore database. # # @return [String] database resource path. def path service.database_path end # @!group Access ## # Retrieves a list of collections. # # @yield [collections] The block for accessing the collections. # @yieldparam [CollectionReference] collection A collection. # # @return [Enumerator<CollectionReference>] collection list. # # @example # require "google/cloud/firestore" # # firestore = Google::Cloud::Firestore.new # # # Get the root collections # firestore.cols.each do |col| # puts col.collection_id # end # def cols ensure_service! return enum_for(:cols) unless block_given? collection_ids = service.list_collections "#{path}/documents" collection_ids.each { |collection_id| yield col(collection_id) } end alias collections cols ## # Retrieves a collection. # # @param [String] collection_path A string representing the path of the # collection, relative to the document root of the database. # # @return [CollectionReference] A collection. # # @example # require "google/cloud/firestore" # # firestore = Google::Cloud::Firestore.new # # # Get the cities collection # cities_col = firestore.col "cities" # # # Get the document for NYC # nyc_ref = cities_col.doc "NYC" # def col collection_path if collection_path.to_s.split("/").count.even? raise ArgumentError, "collection_path must refer to a collection." end CollectionReference.from_path \ "#{path}/documents/#{collection_path}", self end alias collection col ## # Retrieves a document reference. # # @param [String] document_path A string representing the path of the # document, relative to the document root of the database. # # @return [DocumentReference] A document. # # @example # require "google/cloud/firestore" # # firestore = Google::Cloud::Firestore.new # # # Get a document # nyc_ref = firestore.doc "cities/NYC" # # puts nyc_ref.document_id # def doc document_path if document_path.to_s.split("/").count.odd? raise ArgumentError, "document_path must refer to a document." end doc_path = "#{path}/documents/#{document_path}" DocumentReference.from_path doc_path, self end alias document doc ## # Retrieves a list of document snapshots. # # @param [String, DocumentReference, Array<String|DocumentReference>] # docs One or more strings representing the path of the document, or # document reference objects. # # @yield [documents] The block for accessing the document snapshots. # @yieldparam [DocumentSnapshot] document A document snapshot. # # @return [Enumerator<DocumentSnapshot>] document snapshots list. # # @example # require "google/cloud/firestore" # # firestore = Google::Cloud::Firestore.new # # # Get and print city documents # cities = ["cities/NYC", "cities/SF", "cities/LA"] # firestore.get_all(cities).each do |city| # puts "#{city.document_id} has #{city[:population]} residents." # end # def get_all *docs ensure_service! return enum_for(:get_all, docs) unless block_given? doc_paths = Array(docs).flatten.map do |doc_path| coalesce_doc_path_argument doc_path end results = service.get_documents doc_paths results.each do |result| next if result.result.nil? yield DocumentSnapshot.from_batch_result(result, self) end end alias get_docs get_all alias get_documents get_all alias find get_all ## # Creates a field path object representing the sentinel ID of a # document. It can be used in queries to sort or filter by the document # ID. See {Client#document_id}. # # @return [FieldPath] The field path object. # # @example # require "google/cloud/firestore" # # firestore = Google::Cloud::Firestore.new # # # Get a collection reference # cities_col = firestore.col "cities" # # # Create a query # query = cities_col.start_at("NYC").order( # Google::Cloud::Firestore::FieldPath.document_id # ) # # query.get do |city| # puts "#{city.document_id} has #{city[:population]} residents." # end # def document_id FieldPath.document_id end ## # Creates a field path object representing a nested field for # document data. # # @param [String, Symbol, Array<String|Symbol>] fields One or more # strings representing the path of the data to select. Each field must # be provided separately. # # @return [FieldPath] The field path object. # # @example # require "google/cloud/firestore" # # firestore = Google::Cloud::Firestore.new # # user_snap = firestore.doc("users/frank").get # # nested_field_path = firestore.field_path :favorites, :food # user_snap.get(nested_field_path) #=> "Pizza" # def field_path *fields FieldPath.new(*fields) end ## # Creates a field value object representing the deletion of a field in # document data. # # @return [FieldValue] The delete field value object. # # @example # require "google/cloud/firestore" # # firestore = Google::Cloud::Firestore.new # # # Get a document reference # nyc_ref = firestore.doc "cities/NYC" # # nyc_ref.update({ name: "New York City", # trash: firestore.field_delete }) # def field_delete FieldValue.delete end ## # Creates a field value object representing set a field's value to # the server timestamp when accessing the document data. # # @return [FieldValue] The server time field value object. # # @example # require "google/cloud/firestore" # # firestore = Google::Cloud::Firestore.new # # # Get a document reference # nyc_ref = firestore.doc "cities/NYC" # # nyc_ref.update({ name: "New York City", # updated_at: firestore.field_server_time }) # def field_server_time FieldValue.server_time end # @!endgroup # @!group Operations ## # Perform multiple changes at the same time. # # All changes are accumulated in memory until the block completes. # Unlike transactions, batches don't lock on document reads, should only # fail if users provide preconditions, and are not automatically # retried. See {Batch}. # # @see https://firebase.google.com/docs/firestore/manage-data/transactions # Transactions and Batched Writes # # @yield [batch] The block for reading data and making changes. # @yieldparam [Batch] batch The write batch object for making changes. # # @return [CommitResponse] The response from committing the changes. # # @example # require "google/cloud/firestore" # # firestore = Google::Cloud::Firestore.new # # firestore.batch do |b| # # Set the data for NYC # b.set("cities/NYC", { name: "New York City" }) # # # Update the population for SF # b.update("cities/SF", { population: 1000000 }) # # # Delete LA # b.delete("cities/LA") # end # def batch batch = Batch.from_client self yield batch batch.commit end ## # Create a transaction to perform multiple reads and writes that are # executed atomically at a single logical point in time in a database. # # All changes are accumulated in memory until the block completes. # Transactions will be automatically retried when documents change # before the transaction is committed. See {Transaction}. # # @see https://firebase.google.com/docs/firestore/manage-data/transactions # Transactions and Batched Writes # # @param [Integer] max_retries The maximum number of retries for # transactions failed due to errors. Default is 5. Optional. # # @yield [transaction] The block for reading data and making changes. # @yieldparam [Transaction] transaction The transaction object for # making changes. # # @return [CommitResponse] The response from committing the changes. # # @example # require "google/cloud/firestore" # # firestore = Google::Cloud::Firestore.new # # firestore.transaction do |tx| # # Set the data for NYC # tx.set("cities/NYC", { name: "New York City" }) # # # Update the population for SF # tx.update("cities/SF", { population: 1000000 }) # # # Delete LA # tx.delete("cities/LA") # end # def transaction max_retries: nil max_retries = 5 unless max_retries.is_a? Integer retries = 0 backoff = 1.0 transaction = Transaction.from_client self begin yield transaction transaction.commit rescue Google::Cloud::UnavailableError => err # Re-raise if deadline has passed raise err if retries >= max_retries # Sleep with incremental backoff sleep(backoff *= 1.3) # Create new transaction and retry transaction = Transaction.from_client \ self, previous_transaction: transaction.transaction_id retries += 1 retry rescue Google::Cloud::InvalidArgumentError => err # Return if a previous call has succeeded return nil if retries > 0 # Re-raise error. raise err rescue StandardError => err # Rollback transaction when handling unexpected error transaction.rollback rescue nil # Re-raise error. raise err end end # @!endgroup protected ## # @private def coalesce_get_argument obj return obj.ref if obj.is_a? DocumentSnapshot return obj unless obj.is_a?(String) || obj.is_a?(Symbol) return doc obj if obj.to_s.split("/").count.even? col obj # Convert to CollectionReference end ## # @private def coalesce_doc_path_argument doc_path return doc_path.path if doc_path.respond_to? :path doc(doc_path).path end ## # @private Raise an error unless an active connection to the service is # available. def ensure_service! raise "Must have active connection to service" unless service end end end end end