google-cloud-firestore 0.20.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/.yardopts +8 -0
  3. data/LICENSE +201 -0
  4. data/README.md +30 -0
  5. data/lib/google-cloud-firestore.rb +106 -0
  6. data/lib/google/cloud/firestore.rb +514 -0
  7. data/lib/google/cloud/firestore/batch.rb +462 -0
  8. data/lib/google/cloud/firestore/client.rb +449 -0
  9. data/lib/google/cloud/firestore/collection_reference.rb +249 -0
  10. data/lib/google/cloud/firestore/commit_response.rb +145 -0
  11. data/lib/google/cloud/firestore/convert.rb +561 -0
  12. data/lib/google/cloud/firestore/credentials.rb +35 -0
  13. data/lib/google/cloud/firestore/document_reference.rb +468 -0
  14. data/lib/google/cloud/firestore/document_snapshot.rb +324 -0
  15. data/lib/google/cloud/firestore/field_path.rb +216 -0
  16. data/lib/google/cloud/firestore/field_value.rb +113 -0
  17. data/lib/google/cloud/firestore/generate.rb +35 -0
  18. data/lib/google/cloud/firestore/query.rb +651 -0
  19. data/lib/google/cloud/firestore/service.rb +176 -0
  20. data/lib/google/cloud/firestore/transaction.rb +726 -0
  21. data/lib/google/cloud/firestore/v1beta1.rb +121 -0
  22. data/lib/google/cloud/firestore/v1beta1/doc/google/firestore/v1beta1/common.rb +63 -0
  23. data/lib/google/cloud/firestore/v1beta1/doc/google/firestore/v1beta1/document.rb +134 -0
  24. data/lib/google/cloud/firestore/v1beta1/doc/google/firestore/v1beta1/firestore.rb +584 -0
  25. data/lib/google/cloud/firestore/v1beta1/doc/google/firestore/v1beta1/query.rb +215 -0
  26. data/lib/google/cloud/firestore/v1beta1/doc/google/firestore/v1beta1/write.rb +167 -0
  27. data/lib/google/cloud/firestore/v1beta1/doc/google/protobuf/any.rb +124 -0
  28. data/lib/google/cloud/firestore/v1beta1/doc/google/protobuf/timestamp.rb +106 -0
  29. data/lib/google/cloud/firestore/v1beta1/doc/google/protobuf/wrappers.rb +89 -0
  30. data/lib/google/cloud/firestore/v1beta1/doc/google/rpc/status.rb +83 -0
  31. data/lib/google/cloud/firestore/v1beta1/doc/overview.rb +53 -0
  32. data/lib/google/cloud/firestore/v1beta1/firestore_client.rb +974 -0
  33. data/lib/google/cloud/firestore/v1beta1/firestore_client_config.json +100 -0
  34. data/lib/google/cloud/firestore/version.rb +22 -0
  35. data/lib/google/firestore/v1beta1/common_pb.rb +44 -0
  36. data/lib/google/firestore/v1beta1/document_pb.rb +49 -0
  37. data/lib/google/firestore/v1beta1/firestore_pb.rb +219 -0
  38. data/lib/google/firestore/v1beta1/firestore_services_pb.rb +87 -0
  39. data/lib/google/firestore/v1beta1/query_pb.rb +103 -0
  40. data/lib/google/firestore/v1beta1/write_pb.rb +73 -0
  41. metadata +251 -0
@@ -0,0 +1,176 @@
1
+ # Copyright 2017 Google LLC
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
+
16
+ require "google/cloud/env"
17
+ require "google/cloud/errors"
18
+ require "google/cloud/firestore/credentials"
19
+ require "google/cloud/firestore/version"
20
+ require "google/cloud/firestore/v1beta1"
21
+
22
+ module Google
23
+ module Cloud
24
+ module Firestore
25
+ ##
26
+ # @private Represents the gRPC Firestore service, including all the API
27
+ # methods.
28
+ class Service
29
+ attr_accessor :project, :credentials, :timeout, :client_config
30
+
31
+ ##
32
+ # @private Default project.
33
+ def self.default_project_id
34
+ ENV["FIRESTORE_PROJECT"] ||
35
+ ENV["GOOGLE_CLOUD_PROJECT"] ||
36
+ ENV["GCLOUD_PROJECT"] ||
37
+ Google::Cloud.env.project_id
38
+ end
39
+
40
+ ##
41
+ # Creates a new Service instance.
42
+ def initialize project, credentials, timeout: nil, client_config: nil
43
+ @project = project
44
+ @credentials = credentials
45
+ @timeout = timeout
46
+ @client_config = client_config || {}
47
+ end
48
+
49
+ def firestore
50
+ @firestore ||= \
51
+ V1beta1::FirestoreClient.new(
52
+ credentials: credentials,
53
+ timeout: timeout,
54
+ client_config: client_config,
55
+ lib_name: "gccl",
56
+ lib_version: Google::Cloud::Firestore::VERSION)
57
+ end
58
+
59
+ def get_documents document_paths, mask: nil, transaction: nil
60
+ batch_get_args = { mask: document_mask(mask) }
61
+ if transaction.is_a? String
62
+ batch_get_args[:transaction] = transaction
63
+ elsif transaction
64
+ batch_get_args[:new_transaction] = transaction
65
+ end
66
+ batch_get_args[:options] = call_options parent: database_path
67
+
68
+ execute do
69
+ firestore.batch_get_documents database_path, document_paths,
70
+ batch_get_args
71
+ end
72
+ end
73
+
74
+ def list_collections parent, transaction: nil
75
+ list_args = {}
76
+ if transaction.is_a? String
77
+ list_args[:transaction] = transaction
78
+ elsif transaction
79
+ list_args[:new_transaction] = transaction
80
+ end
81
+ list_args[:options] = call_options parent: database_path
82
+
83
+ execute do
84
+ firestore.list_collection_ids parent, list_args
85
+ end
86
+ end
87
+
88
+ def run_query path, query_grpc, transaction: nil
89
+ run_query_args = { structured_query: query_grpc }
90
+ if transaction.is_a? String
91
+ run_query_args[:transaction] = transaction
92
+ elsif transaction
93
+ run_query_args[:new_transaction] = transaction
94
+ end
95
+ run_query_args[:options] = call_options parent: database_path
96
+
97
+ execute do
98
+ firestore.run_query path, run_query_args
99
+ end
100
+ end
101
+
102
+ def begin_transaction transaction_opt
103
+ options = call_options parent: database_path
104
+
105
+ execute do
106
+ firestore.begin_transaction database_path,
107
+ options_: transaction_opt,
108
+ options: options
109
+ end
110
+ end
111
+
112
+ def commit writes, transaction: nil
113
+ commit_args = {}
114
+ commit_args[:transaction] = transaction if transaction
115
+ commit_args[:options] = call_options parent: database_path
116
+
117
+ execute do
118
+ firestore.commit database_path, writes, commit_args
119
+ end
120
+ end
121
+
122
+ def rollback transaction
123
+ options = call_options parent: database_path
124
+
125
+ execute do
126
+ firestore.rollback database_path, transaction, options: options
127
+ end
128
+ end
129
+
130
+ def database_path project_id: project, database_id: "(default)"
131
+ V1beta1::FirestoreClient.database_root_path project_id, database_id
132
+ end
133
+
134
+ def documents_path project_id: project, database_id: "(default)"
135
+ V1beta1::FirestoreClient.document_root_path project_id, database_id
136
+ end
137
+
138
+ def inspect
139
+ "#{self.class}(#{@project})"
140
+ end
141
+
142
+ protected
143
+
144
+ def default_headers parent = nil
145
+ parent ||= database_path
146
+ { "google-cloud-resource-prefix" => parent }
147
+ end
148
+
149
+ def call_options parent: nil, token: nil
150
+ Google::Gax::CallOptions.new({
151
+ kwargs: default_headers(parent),
152
+ page_token: token
153
+ }.delete_if { |_, v| v.nil? })
154
+ end
155
+
156
+ def document_mask mask
157
+ return nil if mask.nil?
158
+
159
+ mask = Array(mask).map(&:to_s).reject(&:nil?).reject(&:empty?)
160
+ return nil if mask.empty?
161
+
162
+ Google::Firestore::V1beta1::DocumentMask.new field_paths: mask
163
+ end
164
+
165
+ def execute
166
+ yield
167
+ rescue Google::Gax::GaxError => e
168
+ # GaxError wraps BadStatus, but exposes it as #cause
169
+ raise Google::Cloud::Error.from_error(e.cause)
170
+ rescue GRPC::BadStatus => e
171
+ raise Google::Cloud::Error.from_error(e)
172
+ end
173
+ end
174
+ end
175
+ end
176
+ end
@@ -0,0 +1,726 @@
1
+ # Copyright 2017 Google LLC
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
+
16
+ require "google/cloud/firestore/collection_reference"
17
+ require "google/cloud/firestore/document_reference"
18
+ require "google/cloud/firestore/document_snapshot"
19
+ require "google/cloud/firestore/commit_response"
20
+ require "google/cloud/firestore/convert"
21
+
22
+ module Google
23
+ module Cloud
24
+ module Firestore
25
+ ##
26
+ # # Transaction
27
+ #
28
+ # A transaction in Cloud Firestore is a set of reads and writes that
29
+ # execute atomically at a single logical point in time.
30
+ #
31
+ # All changes are accumulated in memory until the block passed to
32
+ # {Database#transaction} completes. Transactions will be automatically
33
+ # retried when documents change before the transaction is committed. See
34
+ # {Database#transaction}.
35
+ #
36
+ # @example
37
+ # require "google/cloud/firestore"
38
+ #
39
+ # firestore = Google::Cloud::Firestore.new
40
+ #
41
+ # city = firestore.col("cities").doc("SF")
42
+ # city.set({ name: "San Francisco",
43
+ # state: "CA",
44
+ # country: "USA",
45
+ # capital: false,
46
+ # population: 860000 })
47
+ #
48
+ # firestore.transaction do |tx|
49
+ # new_population = tx.get(city).data[:population] + 1
50
+ # tx.update(city, { population: new_population })
51
+ # end
52
+ #
53
+ class Transaction
54
+ ##
55
+ # @private New Transaction object.
56
+ def initialize
57
+ @writes = []
58
+ @transaction_id = nil
59
+ @previous_transaction = nil
60
+ end
61
+
62
+ ##
63
+ # The transaction identifier.
64
+ #
65
+ # @return [String] transaction identifier.
66
+ def transaction_id
67
+ @transaction_id
68
+ end
69
+
70
+ ##
71
+ # The client the Cloud Firestore transaction belongs to.
72
+ #
73
+ # @return [Client] firestore client.
74
+ def firestore
75
+ @client
76
+ end
77
+ alias_method :client, :firestore
78
+
79
+ # @!group Access
80
+
81
+ ##
82
+ # Retrieves a list of document snapshots.
83
+ #
84
+ # @param [String, DocumentReference] docs One or more strings
85
+ # representing the path of the document, or document reference
86
+ # objects.
87
+ #
88
+ # @yield [documents] The block for accessing the document snapshots.
89
+ # @yieldparam [DocumentSnapshot] document A document snapshot.
90
+ #
91
+ # @return [Enumerator<DocumentSnapshot>] document snapshots list.
92
+ #
93
+ # @example
94
+ # require "google/cloud/firestore"
95
+ #
96
+ # firestore = Google::Cloud::Firestore.new
97
+ #
98
+ # firestore.transaction do |tx|
99
+ # # Get and print city documents
100
+ # tx.get_all("cities/NYC", "cities/SF", "cities/LA").each do |city|
101
+ # puts "#{city.document_id} has #{city[:population]} residents."
102
+ # end
103
+ # end
104
+ #
105
+ def get_all *docs
106
+ ensure_not_closed!
107
+ ensure_service!
108
+
109
+ return enum_for(:get_all, docs) unless block_given?
110
+
111
+ doc_paths = Array(docs).flatten.map do |doc_path|
112
+ coalesce_doc_path_argument doc_path
113
+ end
114
+
115
+ results = service.get_documents \
116
+ doc_paths, transaction: transaction_or_create
117
+ results.each do |result|
118
+ extract_transaction_from_result! result
119
+ next if result.result.nil?
120
+ yield DocumentSnapshot.from_batch_result(result, self)
121
+ end
122
+ end
123
+ alias_method :get_docs, :get_all
124
+ alias_method :get_documents, :get_all
125
+ alias_method :find, :get_all
126
+
127
+ ##
128
+ # Retrieves document snapshots for the given value. Valid values can be
129
+ # a string representing either a document or a collection of documents,
130
+ # a document reference object, a collection reference object, or a query
131
+ # to be run.
132
+ #
133
+ # @param [String, DocumentReference, CollectionReference, Query] obj
134
+ # A string representing the path of a document or collection, a
135
+ # document reference object, a collection reference object, or a query
136
+ # to run.
137
+ #
138
+ # @yield [documents] The block for accessing the document snapshots.
139
+ # @yieldparam [DocumentReference] document A document snapshot.
140
+ #
141
+ # @return [DocumentReference, Enumerator<DocumentReference>] A
142
+ # single document snapshot when passed a document path a document
143
+ # reference, or a list of document snapshots when passed other valid
144
+ # values.
145
+ #
146
+ # @example Get a document snapshot given a document path:
147
+ # require "google/cloud/firestore"
148
+ #
149
+ # firestore = Google::Cloud::Firestore.new
150
+ #
151
+ # firestore.transaction do |tx|
152
+ # # Get a document snapshot
153
+ # nyc_snap = tx.get "cities/NYC"
154
+ # end
155
+ #
156
+ # @example Get a document snapshot given a document reference:
157
+ # require "google/cloud/firestore"
158
+ #
159
+ # firestore = Google::Cloud::Firestore.new
160
+ #
161
+ # # Get a document reference
162
+ # nyc_ref = firestore.doc "cities/NYC"
163
+ #
164
+ # firestore.transaction do |tx|
165
+ # # Get a document snapshot
166
+ # nyc_snap = tx.get nyc_ref
167
+ # end
168
+ #
169
+ # @example Get document snapshots given a collection path:
170
+ # require "google/cloud/firestore"
171
+ #
172
+ # firestore = Google::Cloud::Firestore.new
173
+ #
174
+ # firestore.transaction do |tx|
175
+ # # Get documents for a collection path
176
+ # tx.get("cities").each do |city|
177
+ # # Update the city population by 1
178
+ # tx.update(city, { population: city[:population] + 1})
179
+ # end
180
+ # end
181
+ #
182
+ # @example Get document snapshots given a collection reference:
183
+ # require "google/cloud/firestore"
184
+ #
185
+ # firestore = Google::Cloud::Firestore.new
186
+ #
187
+ # # Get a collection reference
188
+ # cities_col = firestore.col :cities
189
+ #
190
+ # firestore.transaction do |tx|
191
+ # # Get documents for a collection
192
+ # tx.get(cities_col).each do |city|
193
+ # # Update the city population by 1
194
+ # tx.update(city, { population: city[:population] + 1})
195
+ # end
196
+ # end
197
+ #
198
+ # @example Get document snapshots given a query:
199
+ # require "google/cloud/firestore"
200
+ #
201
+ # firestore = Google::Cloud::Firestore.new
202
+ #
203
+ # # Create a query
204
+ # query = firestore.col(:cities).select(:population)
205
+ #
206
+ # firestore.transaction do |tx|
207
+ # # Get/run a query
208
+ # tx.get(query).each do |city|
209
+ # # Update the city population by 1
210
+ # tx.update(city, { population: city[:population] + 1})
211
+ # end
212
+ # end
213
+ #
214
+ def get obj
215
+ ensure_not_closed!
216
+ ensure_service!
217
+
218
+ obj = coalesce_get_argument obj
219
+
220
+ if obj.is_a?(DocumentReference)
221
+ doc = get_all([obj]).first
222
+ yield doc if block_given?
223
+ return doc
224
+ end
225
+
226
+ return enum_for(:get, obj) unless block_given?
227
+
228
+ results = service.run_query obj.parent_path, obj.query,
229
+ transaction: transaction_or_create
230
+ results.each do |result|
231
+ extract_transaction_from_result! result
232
+ next if result.document.nil?
233
+ yield DocumentSnapshot.from_query_result(result, self)
234
+ end
235
+ end
236
+ alias_method :run, :get
237
+
238
+ # @!endgroup
239
+
240
+ # @!group Modifications
241
+
242
+ ##
243
+ # Creates a document with the provided data (fields and values).
244
+ #
245
+ # The operation will fail if the document already exists.
246
+ #
247
+ # @param [String, DocumentReference] doc A string representing the
248
+ # path of the document, or a document reference object.
249
+ # @param [Hash] data The document's fields and values.
250
+ #
251
+ # @example Create a document using a document path:
252
+ # require "google/cloud/firestore"
253
+ #
254
+ # firestore = Google::Cloud::Firestore.new
255
+ #
256
+ # firestore.transaction do |tx|
257
+ # tx.create("cities/NYC", { name: "New York City" })
258
+ # end
259
+ #
260
+ # @example Create a document using a document reference:
261
+ # require "google/cloud/firestore"
262
+ #
263
+ # firestore = Google::Cloud::Firestore.new
264
+ #
265
+ # # Get a document reference
266
+ # nyc_ref = firestore.doc "cities/NYC"
267
+ #
268
+ # firestore.transaction do |tx|
269
+ # tx.create(nyc_ref, { name: "New York City" })
270
+ # end
271
+ #
272
+ # @example Create a document and set a field to server_time:
273
+ # require "google/cloud/firestore"
274
+ #
275
+ # firestore = Google::Cloud::Firestore.new
276
+ #
277
+ # # Get a document reference
278
+ # nyc_ref = firestore.doc "cities/NYC"
279
+ #
280
+ # firestore.transaction do |tx|
281
+ # tx.create(nyc_ref, { name: "New York City",
282
+ # updated_at: firestore.field_server_time })
283
+ # end
284
+ #
285
+ def create doc, data
286
+ ensure_not_closed!
287
+
288
+ doc_path = coalesce_doc_path_argument doc
289
+
290
+ @writes << Convert.writes_for_create(doc_path, data)
291
+
292
+ nil
293
+ end
294
+
295
+ ##
296
+ # Writes the provided data (fields and values) to the provided document.
297
+ # If the document does not exist, it will be created. By default, the
298
+ # provided data overwrites existing data, but the provided data can be
299
+ # merged into the existing document using the `merge` argument.
300
+ #
301
+ # If you're not sure whether the document exists, use the `merge`
302
+ # argument to merge the new data with any existing document data to
303
+ # avoid overwriting entire documents.
304
+ #
305
+ # @param [String, DocumentReference] doc A string representing the
306
+ # path of the document, or a document reference object.
307
+ # @param [Hash] data The document's fields and values.
308
+ # @param [Boolean, FieldPath, String, Symbol] merge When
309
+ # `true`, all provided data is merged with the existing document data.
310
+ # When the argument is one or more field path, only the data for
311
+ # fields in this argument is merged with the existing document data.
312
+ # The default is to not merge, but to instead overwrite the existing
313
+ # document data.
314
+ #
315
+ # @example Set a document using a document path:
316
+ # require "google/cloud/firestore"
317
+ #
318
+ # firestore = Google::Cloud::Firestore.new
319
+ #
320
+ # firestore.transaction do |tx|
321
+ # # Update a document
322
+ # tx.set("cities/NYC", { name: "New York City" })
323
+ # end
324
+ #
325
+ # @example Create a document using a document reference:
326
+ # require "google/cloud/firestore"
327
+ #
328
+ # firestore = Google::Cloud::Firestore.new
329
+ #
330
+ # # Get a document reference
331
+ # nyc_ref = firestore.doc "cities/NYC"
332
+ #
333
+ # firestore.transaction do |tx|
334
+ # # Update a document
335
+ # tx.set(nyc_ref, { name: "New York City" })
336
+ # end
337
+ #
338
+ # @example Set a document and merge all data:
339
+ # require "google/cloud/firestore"
340
+ #
341
+ # firestore = Google::Cloud::Firestore.new
342
+ #
343
+ # firestore.transaction do |tx|
344
+ # tx.set("cities/NYC", { name: "New York City" }, merge: true)
345
+ # end
346
+ #
347
+ # @example Set a document and merge only name:
348
+ # require "google/cloud/firestore"
349
+ #
350
+ # firestore = Google::Cloud::Firestore.new
351
+ #
352
+ # firestore.transaction do |tx|
353
+ # tx.set("cities/NYC", { name: "New York City" }, merge: :name)
354
+ # end
355
+ #
356
+ # @example Set a document and deleting a field using merge:
357
+ # require "google/cloud/firestore"
358
+ #
359
+ # firestore = Google::Cloud::Firestore.new
360
+ #
361
+ # # Get a document reference
362
+ # nyc_ref = firestore.doc "cities/NYC"
363
+ #
364
+ # nyc_data = { name: "New York City",
365
+ # trash: firestore.field_delete }
366
+ #
367
+ # firestore.transaction do |tx|
368
+ # tx.set(nyc_ref, nyc_data, merge: true)
369
+ # end
370
+ #
371
+ # @example Set a document and set a field to server_time:
372
+ # require "google/cloud/firestore"
373
+ #
374
+ # firestore = Google::Cloud::Firestore.new
375
+ #
376
+ # # Get a document reference
377
+ # nyc_ref = firestore.doc "cities/NYC"
378
+ #
379
+ # nyc_data = { name: "New York City",
380
+ # updated_at: firestore.field_server_time }
381
+ #
382
+ # firestore.transaction do |tx|
383
+ # tx.set(nyc_ref, nyc_data, merge: true)
384
+ # end
385
+ #
386
+ def set doc, data, merge: nil
387
+ ensure_not_closed!
388
+
389
+ doc_path = coalesce_doc_path_argument doc
390
+
391
+ @writes << Convert.writes_for_set(doc_path, data, merge: merge)
392
+
393
+ nil
394
+ end
395
+
396
+ ##
397
+ # Updates the document with the provided data (fields and values). The
398
+ # provided data is merged into the existing document data.
399
+ #
400
+ # The operation will fail if the document does not exist.
401
+ #
402
+ # @param [String, DocumentReference] doc A string representing the
403
+ # path of the document, or a document reference object.
404
+ # @param [Hash<FieldPath|String|Symbol, Object>] data The document's
405
+ # fields and values.
406
+ #
407
+ # The top-level keys in the data hash are considered field paths, and
408
+ # can either be a FieldPath object, or a string representing the
409
+ # nested fields. In other words the string represents individual
410
+ # fields joined by ".". Fields containing `~`, `*`, `/`, `[`, `]`, and
411
+ # `.` cannot be in a dotted string, and should provided using a
412
+ # {FieldPath} object instead.
413
+ # @param [Time] update_time When set, the document must have been last
414
+ # updated at that time. Optional.
415
+ #
416
+ # @example Update a document using a document path:
417
+ # require "google/cloud/firestore"
418
+ #
419
+ # firestore = Google::Cloud::Firestore.new
420
+ #
421
+ # firestore.transaction do |tx|
422
+ # tx.update("cities/NYC", { name: "New York City" })
423
+ # end
424
+ #
425
+ # @example Directly update a deeply-nested field with a `FieldPath`:
426
+ # require "google/cloud/firestore"
427
+ #
428
+ # firestore = Google::Cloud::Firestore.new
429
+ #
430
+ # nested_field_path = Google::Cloud::Firestore::FieldPath.new(
431
+ # :favorites, :food
432
+ # )
433
+ #
434
+ # firestore.transaction do |tx|
435
+ # tx.update("users/frank", { nested_field_path: "Pasta" })
436
+ # end
437
+ #
438
+ # @example Update a document using a document reference:
439
+ # require "google/cloud/firestore"
440
+ #
441
+ # firestore = Google::Cloud::Firestore.new
442
+ #
443
+ # # Get a document reference
444
+ # nyc_ref = firestore.doc "cities/NYC"
445
+ #
446
+ # firestore.transaction do |tx|
447
+ # tx.update(nyc_ref, { name: "New York City" })
448
+ # end
449
+ #
450
+ # @example Update a document using the `update_time` precondition:
451
+ # require "google/cloud/firestore"
452
+ #
453
+ # firestore = Google::Cloud::Firestore.new
454
+ #
455
+ # last_updated_at = Time.now - 42 # 42 seconds ago
456
+ #
457
+ # firestore.transaction do |tx|
458
+ # tx.update("cities/NYC", { name: "New York City" },
459
+ # update_time: last_updated_at)
460
+ # end
461
+ #
462
+ # @example Update a document and deleting a field:
463
+ # require "google/cloud/firestore"
464
+ #
465
+ # firestore = Google::Cloud::Firestore.new
466
+ #
467
+ # # Get a document reference
468
+ # nyc_ref = firestore.doc "cities/NYC"
469
+ #
470
+ # nyc_data = { name: "New York City",
471
+ # trash: firestore.field_delete }
472
+ #
473
+ # firestore.transaction do |tx|
474
+ # tx.update(nyc_ref, nyc_data)
475
+ # end
476
+ #
477
+ # @example Update a document and set a field to server_time:
478
+ # require "google/cloud/firestore"
479
+ #
480
+ # firestore = Google::Cloud::Firestore.new
481
+ #
482
+ # # Get a document reference
483
+ # nyc_ref = firestore.doc "cities/NYC"
484
+ #
485
+ # nyc_data = { name: "New York City",
486
+ # updated_at: firestore.field_server_time }
487
+ #
488
+ # firestore.transaction do |tx|
489
+ # tx.update(nyc_ref, nyc_data)
490
+ # end
491
+ #
492
+ def update doc, data, update_time: nil
493
+ ensure_not_closed!
494
+
495
+ doc_path = coalesce_doc_path_argument doc
496
+
497
+ @writes << Convert.writes_for_update(doc_path, data,
498
+ update_time: update_time)
499
+
500
+ nil
501
+ end
502
+
503
+ ##
504
+ # Deletes a document from the database.
505
+ #
506
+ # @param [String, DocumentReference] doc A string representing the
507
+ # path of the document, or a document reference object.
508
+ # @param [Boolean] exists Whether the document must exist. When `true`,
509
+ # the document must exist or an error is raised. Default is `false`.
510
+ # Optional.
511
+ # @param [Time] update_time When set, the document must have been last
512
+ # updated at that time. Optional.
513
+ #
514
+ # @example Delete a document using a document path:
515
+ # require "google/cloud/firestore"
516
+ #
517
+ # firestore = Google::Cloud::Firestore.new
518
+ #
519
+ # firestore.transaction do |tx|
520
+ # # Delete a document
521
+ # tx.delete "cities/NYC"
522
+ # end
523
+ #
524
+ # @example Delete a document using a document reference:
525
+ # require "google/cloud/firestore"
526
+ #
527
+ # firestore = Google::Cloud::Firestore.new
528
+ #
529
+ # # Get a document reference
530
+ # nyc_ref = firestore.doc "cities/NYC"
531
+ #
532
+ # firestore.transaction do |tx|
533
+ # # Delete a document
534
+ # tx.delete nyc_ref
535
+ # end
536
+ #
537
+ # @example Delete a document using `exists`:
538
+ # require "google/cloud/firestore"
539
+ #
540
+ # firestore = Google::Cloud::Firestore.new
541
+ #
542
+ # firestore.transaction do |tx|
543
+ # # Delete a document
544
+ # tx.delete "cities/NYC", exists: true
545
+ # end
546
+ #
547
+ # @example Delete a document using the `update_time` precondition:
548
+ # require "google/cloud/firestore"
549
+ #
550
+ # firestore = Google::Cloud::Firestore.new
551
+ #
552
+ # last_updated_at = Time.now - 42 # 42 seconds ago
553
+ #
554
+ # firestore.transaction do |tx|
555
+ # # Delete a document
556
+ # tx.delete "cities/NYC", update_time: last_updated_at
557
+ # end
558
+ #
559
+ def delete doc, exists: nil, update_time: nil
560
+ ensure_not_closed!
561
+
562
+ doc_path = coalesce_doc_path_argument doc
563
+
564
+ @writes << Convert.write_for_delete(
565
+ doc_path, exists: exists, update_time: update_time)
566
+
567
+ nil
568
+ end
569
+
570
+ # @!endgroup
571
+
572
+ ##
573
+ # @private commit the transaction
574
+ def commit
575
+ ensure_not_closed!
576
+
577
+ if @transaction_id.nil? && @writes.empty?
578
+ @closed = true
579
+ return CommitResponse.from_grpc nil, @writes
580
+ end
581
+
582
+ ensure_transaction_id!
583
+
584
+ resp = service.commit @writes.flatten, transaction: transaction_id
585
+ @closed = true
586
+ CommitResponse.from_grpc resp, @writes
587
+ end
588
+
589
+ ##
590
+ # @private rollback and close the transaction
591
+ def rollback
592
+ ensure_not_closed!
593
+
594
+ if @transaction_id.nil? && @writes.empty?
595
+ @closed = true
596
+ return
597
+ end
598
+
599
+ service.rollback @transaction_id
600
+ @closed = true
601
+ nil
602
+ end
603
+
604
+ ##
605
+ # @private the transaction is complete and closed
606
+ def closed?
607
+ @closed
608
+ end
609
+
610
+ ##
611
+ # @private New Transaction reference object from a path.
612
+ def self.from_client client, previous_transaction: nil
613
+ new.tap do |s|
614
+ s.instance_variable_set :@client, client
615
+ s.instance_variable_set :@previous_transaction, previous_transaction
616
+ end
617
+ end
618
+
619
+ ##
620
+ # @private The Service object.
621
+ def service
622
+ ensure_client!
623
+
624
+ firestore.service
625
+ end
626
+
627
+ protected
628
+
629
+ ##
630
+ # @private The full Database path for the Cloud Firestore transaction.
631
+ #
632
+ # @return [String] database resource path.
633
+ def path
634
+ @client.path
635
+ end
636
+
637
+ ##
638
+ # @private
639
+ def coalesce_get_argument obj
640
+ if obj.is_a?(String) || obj.is_a?(Symbol)
641
+ if obj.to_s.split("/").count.even?
642
+ return client.doc obj # Convert a DocumentReference
643
+ else
644
+ return client.col obj # Convert to CollectionReference
645
+ end
646
+ end
647
+
648
+ return obj.ref if obj.is_a? DocumentSnapshot
649
+
650
+ obj
651
+ end
652
+
653
+ ##
654
+ # @private
655
+ def coalesce_doc_path_argument doc_path
656
+ return doc_path.path if doc_path.respond_to? :path
657
+
658
+ client.doc(doc_path).path
659
+ end
660
+
661
+ ##
662
+ # @private
663
+ def transaction_or_create
664
+ return @transaction_id if @transaction_id
665
+
666
+ transaction_opt
667
+ end
668
+
669
+ ##
670
+ # @private
671
+ def transaction_opt
672
+ read_write = \
673
+ Google::Firestore::V1beta1::TransactionOptions::ReadWrite.new
674
+
675
+ if @previous_transaction
676
+ read_write.retry_transaction = @previous_transaction
677
+ @previous_transaction = nil
678
+ end
679
+
680
+ Google::Firestore::V1beta1::TransactionOptions.new(
681
+ read_write: read_write
682
+ )
683
+ end
684
+
685
+ ##
686
+ # @private
687
+ def extract_transaction_from_result! result
688
+ return if @transaction_id
689
+ return if result.transaction.nil?
690
+ return if result.transaction.empty?
691
+
692
+ @transaction_id = result.transaction
693
+ end
694
+
695
+ ##
696
+ # @private
697
+ def ensure_not_closed!
698
+ fail "transaction is closed" if closed?
699
+ end
700
+
701
+ ##
702
+ # @private Raise an error unless an database available.
703
+ def ensure_transaction_id!
704
+ ensure_service!
705
+
706
+ return unless @transaction_id.nil?
707
+ resp = service.begin_transaction transaction_opt
708
+ @transaction_id = resp.transaction
709
+ end
710
+
711
+ ##
712
+ # @private Raise an error unless an database available.
713
+ def ensure_client!
714
+ fail "Must have active connection to service" unless firestore
715
+ end
716
+
717
+ ##
718
+ # @private Raise an error unless an active connection to the service
719
+ # is available.
720
+ def ensure_service!
721
+ fail "Must have active connection to service" unless service
722
+ end
723
+ end
724
+ end
725
+ end
726
+ end