google-cloud-firestore 0.22.0 → 0.23.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +1 -0
  3. data/README.md +8 -8
  4. data/lib/google-cloud-firestore.rb +1 -1
  5. data/lib/google/cloud/firestore.rb +46 -0
  6. data/lib/google/cloud/firestore/batch.rb +1 -1
  7. data/lib/google/cloud/firestore/client.rb +18 -13
  8. data/lib/google/cloud/firestore/convert.rb +78 -35
  9. data/lib/google/cloud/firestore/credentials.rb +2 -12
  10. data/lib/google/cloud/firestore/document_change.rb +124 -0
  11. data/lib/google/cloud/firestore/document_listener.rb +125 -0
  12. data/lib/google/cloud/firestore/document_reference.rb +35 -0
  13. data/lib/google/cloud/firestore/document_snapshot.rb +91 -9
  14. data/lib/google/cloud/firestore/field_path.rb +23 -13
  15. data/lib/google/cloud/firestore/query.rb +513 -69
  16. data/lib/google/cloud/firestore/query_listener.rb +118 -0
  17. data/lib/google/cloud/firestore/query_snapshot.rb +121 -0
  18. data/lib/google/cloud/firestore/service.rb +8 -0
  19. data/lib/google/cloud/firestore/transaction.rb +2 -2
  20. data/lib/google/cloud/firestore/v1beta1.rb +62 -37
  21. data/lib/google/cloud/firestore/v1beta1/credentials.rb +41 -0
  22. data/lib/google/cloud/firestore/v1beta1/doc/google/firestore/v1beta1/common.rb +1 -1
  23. data/lib/google/cloud/firestore/v1beta1/doc/google/firestore/v1beta1/document.rb +5 -4
  24. data/lib/google/cloud/firestore/v1beta1/doc/google/firestore/v1beta1/firestore.rb +1 -12
  25. data/lib/google/cloud/firestore/v1beta1/doc/google/firestore/v1beta1/query.rb +4 -1
  26. data/lib/google/cloud/firestore/v1beta1/doc/google/firestore/v1beta1/write.rb +37 -8
  27. data/lib/google/cloud/firestore/v1beta1/doc/google/protobuf/any.rb +1 -1
  28. data/lib/google/cloud/firestore/v1beta1/doc/google/protobuf/empty.rb +28 -0
  29. data/lib/google/cloud/firestore/v1beta1/doc/google/protobuf/timestamp.rb +1 -1
  30. data/lib/google/cloud/firestore/v1beta1/doc/google/protobuf/wrappers.rb +1 -1
  31. data/lib/google/cloud/firestore/v1beta1/doc/google/rpc/status.rb +1 -1
  32. data/lib/google/cloud/firestore/v1beta1/firestore_client.rb +124 -56
  33. data/lib/google/cloud/firestore/v1beta1/firestore_client_config.json +2 -2
  34. data/lib/google/cloud/firestore/version.rb +1 -1
  35. data/lib/google/cloud/firestore/watch/enumerator_queue.rb +47 -0
  36. data/lib/google/cloud/firestore/watch/inventory.rb +280 -0
  37. data/lib/google/cloud/firestore/watch/listener.rb +298 -0
  38. data/lib/google/cloud/firestore/watch/order.rb +98 -0
  39. data/lib/google/firestore/v1beta1/firestore_services_pb.rb +2 -4
  40. data/lib/google/firestore/v1beta1/query_pb.rb +1 -0
  41. data/lib/google/firestore/v1beta1/write_pb.rb +2 -0
  42. metadata +40 -3
  43. data/lib/google/cloud/firestore/v1beta1/doc/overview.rb +0 -53
@@ -0,0 +1,125 @@
1
+ # Copyright 2018 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/watch/listener"
17
+
18
+ module Google
19
+ module Cloud
20
+ module Firestore
21
+ ##
22
+ # An ongoing listen operation on a document reference. This is returned by
23
+ # calling {DocumentReference#listen}.
24
+ #
25
+ # @example
26
+ # require "google/cloud/firestore"
27
+ #
28
+ # firestore = Google::Cloud::Firestore.new
29
+ #
30
+ # # Get a document reference
31
+ # nyc_ref = firestore.doc "cities/NYC"
32
+ #
33
+ # listener = nyc_ref.listen do |snapshot|
34
+ # puts "The population of #{snapshot[:name]} "
35
+ # puts "is #{snapshot[:population]}."
36
+ # end
37
+ #
38
+ # # When ready, stop the listen operation and close the stream.
39
+ # listener.stop
40
+ #
41
+ class DocumentListener
42
+ ##
43
+ # @private
44
+ # Creates the watch stream and listener object.
45
+ def initialize doc_ref, &callback
46
+ @doc_ref = doc_ref
47
+ raise ArgumentError if @doc_ref.nil?
48
+
49
+ @callback = callback
50
+ raise ArgumentError if @callback.nil?
51
+
52
+ @listener = Watch::Listener.for_doc_ref doc_ref do |query_snp|
53
+ doc_snp = query_snp.docs.find { |doc| doc.path == @doc_ref.path }
54
+
55
+ if doc_snp.nil?
56
+ doc_snp = DocumentSnapshot.missing \
57
+ @doc_ref, read_at: query_snp.read_at
58
+ end
59
+
60
+ @callback.call doc_snp
61
+ end
62
+ end
63
+
64
+ ##
65
+ # @private
66
+ def start
67
+ @listener.start
68
+ self
69
+ end
70
+
71
+ ##
72
+ # Stops the client listening for changes.
73
+ #
74
+ # @example
75
+ # require "google/cloud/firestore"
76
+ #
77
+ # firestore = Google::Cloud::Firestore.new
78
+ #
79
+ # # Get a document reference
80
+ # nyc_ref = firestore.doc "cities/NYC"
81
+ #
82
+ # listener = nyc_ref.listen do |snapshot|
83
+ # puts "The population of #{snapshot[:name]} "
84
+ # puts "is #{snapshot[:population]}."
85
+ # end
86
+ #
87
+ # # When ready, stop the listen operation and close the stream.
88
+ # listener.stop
89
+ #
90
+ def stop
91
+ @listener.stop
92
+ end
93
+
94
+ ##
95
+ # Whether the client has stopped listening for changes.
96
+ #
97
+ # @example
98
+ # require "google/cloud/firestore"
99
+ #
100
+ # firestore = Google::Cloud::Firestore.new
101
+ #
102
+ # # Get a document reference
103
+ # nyc_ref = firestore.doc "cities/NYC"
104
+ #
105
+ # listener = nyc_ref.listen do |snapshot|
106
+ # puts "The population of #{snapshot[:name]} "
107
+ # puts "is #{snapshot[:population]}."
108
+ # end
109
+ #
110
+ # # Checks if the listener is stopped.
111
+ # listener.stopped? #=> false
112
+ #
113
+ # # When ready, stop the listen operation and close the stream.
114
+ # listener.stop
115
+ #
116
+ # # Checks if the listener is stopped.
117
+ # listener.stopped? #=> true
118
+ #
119
+ def stopped?
120
+ @listener.stopped?
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
@@ -16,6 +16,7 @@
16
16
  require "google/cloud/firestore/v1beta1"
17
17
  require "google/cloud/firestore/document_snapshot"
18
18
  require "google/cloud/firestore/collection_reference"
19
+ require "google/cloud/firestore/document_listener"
19
20
 
20
21
  module Google
21
22
  module Cloud
@@ -147,6 +148,40 @@ module Google
147
148
  client.get_all([self]).first
148
149
  end
149
150
 
151
+ ##
152
+ # Listen to this document reference for changes.
153
+ #
154
+ # @yield [callback] The block for accessing the document snapshot.
155
+ # @yieldparam [DocumentSnapshot] snapshot A document snapshot.
156
+ #
157
+ # @return [DocumentListener] The ongoing listen operation on the
158
+ # document reference.
159
+ #
160
+ # @example
161
+ # require "google/cloud/firestore"
162
+ #
163
+ # firestore = Google::Cloud::Firestore.new
164
+ #
165
+ # # Get a document reference
166
+ # nyc_ref = firestore.doc "cities/NYC"
167
+ #
168
+ # listener = nyc_ref.listen do |snapshot|
169
+ # puts "The population of #{snapshot[:name]} "
170
+ # puts "is #{snapshot[:population]}."
171
+ # end
172
+ #
173
+ # # When ready, stop the listen operation and close the stream.
174
+ # listener.stop
175
+ #
176
+ def listen &callback
177
+ raise ArgumentError, "callback required" if callback.nil?
178
+
179
+ ensure_client!
180
+
181
+ DocumentListener.new(self, &callback).start
182
+ end
183
+ alias on_snapshot listen
184
+
150
185
  ##
151
186
  # The collection the document reference belongs to.
152
187
  #
@@ -17,6 +17,7 @@ require "google/cloud/firestore/v1beta1"
17
17
  require "google/cloud/firestore/document_reference"
18
18
  require "google/cloud/firestore/collection_reference"
19
19
  require "google/cloud/firestore/convert"
20
+ require "google/cloud/firestore/watch/order"
20
21
 
21
22
  module Google
22
23
  module Cloud
@@ -29,6 +30,9 @@ module Google
29
30
  #
30
31
  # The snapshot can reference a non-existing document.
31
32
  #
33
+ # See {DocumentReference#get}, {DocumentReference#listen},
34
+ # {Query#get}, {Query#listen}, and {QuerySnapshot#docs}.
35
+ #
32
36
  # @example
33
37
  # require "google/cloud/firestore"
34
38
  #
@@ -40,6 +44,22 @@ module Google
40
44
  # # Get the document data
41
45
  # nyc_snap[:population] #=> 1000000
42
46
  #
47
+ # @example Listen to a document reference for changes:
48
+ # require "google/cloud/firestore"
49
+ #
50
+ # firestore = Google::Cloud::Firestore.new
51
+ #
52
+ # # Get a document reference
53
+ # nyc_ref = firestore.doc "cities/NYC"
54
+ #
55
+ # listener = nyc_ref.listen do |snapshot|
56
+ # puts "The population of #{snapshot[:name]} "
57
+ # puts "is #{snapshot[:population]}."
58
+ # end
59
+ #
60
+ # # When ready, stop the listen operation and close the stream.
61
+ # listener.stop
62
+ #
43
63
  class DocumentSnapshot
44
64
  ##
45
65
  # @private The Google::Firestore::V1beta1::Document object.
@@ -118,9 +138,11 @@ module Google
118
138
  # @!group Data
119
139
 
120
140
  ##
121
- # Retrieves the document data.
141
+ # Retrieves the document data. When the document exists the data hash is
142
+ # frozen and will not allow any changes. When the document does not
143
+ # exist `nil` will be returned.
122
144
  #
123
- # @return [Hash] The document data.
145
+ # @return [Hash, nil] The document data.
124
146
  #
125
147
  # @example
126
148
  # require "google/cloud/firestore"
@@ -134,7 +156,7 @@ module Google
134
156
  #
135
157
  def data
136
158
  return nil if missing?
137
- Convert.fields_to_hash grpc.fields, ref.client
159
+ @data ||= Convert.fields_to_hash(grpc.fields, ref.client).freeze
138
160
  end
139
161
  alias fields data
140
162
 
@@ -192,8 +214,9 @@ module Google
192
214
  end
193
215
 
194
216
  nodes = field_path.fields.map(&:to_sym)
195
- selected_data = data
217
+ return ref if nodes == [:__name__]
196
218
 
219
+ selected_data = data
197
220
  nodes.each do |node|
198
221
  unless selected_data.is_a? Hash
199
222
  err_msg = "#{field_path.formatted_string} is not " \
@@ -285,11 +308,46 @@ module Google
285
308
  grpc.nil?
286
309
  end
287
310
 
311
+ ##
312
+ # @private
313
+ def <=> other
314
+ return nil unless other.is_a? DocumentSnapshot
315
+ return data <=> other.data if path == other.path
316
+ path <=> other.path
317
+ end
318
+
319
+ ##
320
+ # @private
321
+ def eql? other
322
+ return nil unless other.is_a? DocumentSnapshot
323
+ return data.eql? other.data if path == other.path
324
+ path.eql? other.path
325
+ end
326
+
327
+ ##
328
+ # @private
329
+ def hash
330
+ @hash ||= [path, data].hash
331
+ end
332
+
333
+ ##
334
+ # @private
335
+ def query_comparisons_for query_grpc
336
+ @memoized_comps ||= {}
337
+ if @memoized_comps.key? query_grpc.hash
338
+ return @memoized_comps[query_grpc.hash]
339
+ end
340
+
341
+ @memoized_comps[query_grpc.hash] = query_grpc.order_by.map do |order|
342
+ Watch::Order.field_comparison get(order.field.field_path)
343
+ end
344
+ end
345
+
288
346
  ##
289
347
  # @private New DocumentSnapshot from a
290
348
  # Google::Firestore::V1beta1::RunQueryResponse object.
291
- def self.from_query_result result, context
292
- ref = DocumentReference.from_path result.document.name, context
349
+ def self.from_query_result result, client
350
+ ref = DocumentReference.from_path result.document.name, client
293
351
  read_at = Convert.timestamp_to_time result.read_time
294
352
 
295
353
  new.tap do |s|
@@ -299,17 +357,30 @@ module Google
299
357
  end
300
358
  end
301
359
 
360
+ ##
361
+ # @private New DocumentSnapshot from a
362
+ # Google::Firestore::V1beta1::DocumentChange object.
363
+ def self.from_document document, client, read_at: nil
364
+ ref = DocumentReference.from_path document.name, client
365
+
366
+ new.tap do |s|
367
+ s.grpc = document
368
+ s.instance_variable_set :@ref, ref
369
+ s.instance_variable_set :@read_at, read_at
370
+ end
371
+ end
372
+
302
373
  ##
303
374
  # @private New DocumentSnapshot from a
304
375
  # Google::Firestore::V1beta1::BatchGetDocumentsResponse object.
305
- def self.from_batch_result result, context
376
+ def self.from_batch_result result, client
306
377
  ref = nil
307
378
  grpc = nil
308
379
  if result.result == :found
309
380
  grpc = result.found
310
- ref = DocumentReference.from_path grpc.name, context
381
+ ref = DocumentReference.from_path grpc.name, client
311
382
  else
312
- ref = DocumentReference.from_path result.missing, context
383
+ ref = DocumentReference.from_path result.missing, client
313
384
  end
314
385
  read_at = Convert.timestamp_to_time result.read_time
315
386
 
@@ -319,6 +390,17 @@ module Google
319
390
  s.instance_variable_set :@read_at, read_at
320
391
  end
321
392
  end
393
+
394
+ ##
395
+ # @private New non-existant DocumentSnapshot from a
396
+ # DocumentReference object.
397
+ def self.missing doc_ref, read_at: nil
398
+ new.tap do |s|
399
+ s.grpc = nil
400
+ s.instance_variable_set :@ref, doc_ref
401
+ s.instance_variable_set :@read_at, read_at
402
+ end
403
+ end
322
404
  end
323
405
  end
324
406
  end
@@ -61,15 +61,15 @@ module Google
61
61
  # user_snap.get(nested_field_path) #=> "Pizza"
62
62
  #
63
63
  def initialize *fields
64
- @fields = fields.flatten.map(&:to_s)
65
- @fields.each do |field|
66
- raise ArgumentError, "empty paths not allowed" if field.empty?
67
- end
64
+ @fields = fields.flatten.map(&:to_s).freeze
65
+
66
+ invalid_fields = @fields.detect(&:empty?)
67
+ raise ArgumentError, "empty paths not allowed" if invalid_fields
68
68
  end
69
69
 
70
70
  ##
71
71
  # @private The individual fields representing the nested field path for
72
- # document data.
72
+ # document data. The fields are frozen.
73
73
  #
74
74
  # @return [Array<String>] The fields.
75
75
  #
@@ -129,9 +129,9 @@ module Google
129
129
  # cities_col = firestore.col "cities"
130
130
  #
131
131
  # # Create a query
132
- # query = cities_col.start_at("NYC").order(
132
+ # query = cities_col.order(
133
133
  # Google::Cloud::Firestore::FieldPath.document_id
134
- # )
134
+ # ).start_at("NYC")
135
135
  #
136
136
  # query.get do |city|
137
137
  # puts "#{city.document_id} has #{city[:population]} residents."
@@ -145,6 +145,8 @@ module Google
145
145
  # @private Creates a field path object representing the nested fields
146
146
  # for document data.
147
147
  #
148
+ # The values are memoized to increase performance.
149
+ #
148
150
  # @param [String] dotted_string A string representing the path of the
149
151
  # document data. The string can represent as a string of individual
150
152
  # fields joined by ".". Fields containing `~`, `*`, `/`, `[`, `]`, and
@@ -166,15 +168,23 @@ module Google
166
168
  # field_path.fields #=> ["favorites", "food"]
167
169
  #
168
170
  def self.parse dotted_string
169
- return new dotted_string if dotted_string.is_a? Array
171
+ # Memoize parsed field paths
172
+ @memoized_field_paths ||= {}
173
+ if @memoized_field_paths.key? dotted_string
174
+ return @memoized_field_paths[dotted_string]
175
+ end
176
+
177
+ if dotted_string.is_a? Array
178
+ return @memoized_field_paths[dotted_string] = new(dotted_string)
179
+ end
170
180
 
171
181
  fields = String(dotted_string).split(".")
172
- fields.each do |field|
173
- if INVALID_FIELD_PATH_CHARS.match field
174
- raise ArgumentError, "invalid character, use FieldPath instead"
175
- end
182
+
183
+ if fields.grep(INVALID_FIELD_PATH_CHARS).any?
184
+ raise ArgumentError, "invalid character, use FieldPath instead"
176
185
  end
177
- new fields
186
+
187
+ @memoized_field_paths[dotted_string] = new(fields)
178
188
  end
179
189
 
180
190
  ##
@@ -15,6 +15,7 @@
15
15
 
16
16
  require "google/cloud/firestore/v1beta1"
17
17
  require "google/cloud/firestore/document_snapshot"
18
+ require "google/cloud/firestore/query_listener"
18
19
  require "google/cloud/firestore/convert"
19
20
 
20
21
  module Google
@@ -40,6 +41,22 @@ module Google
40
41
  # puts "#{city.document_id} has #{city[:population]} residents."
41
42
  # end
42
43
  #
44
+ # @example Listen to a query for changes:
45
+ # require "google/cloud/firestore"
46
+ #
47
+ # firestore = Google::Cloud::Firestore.new
48
+ #
49
+ # # Create a query
50
+ # query = firestore.col(:cities).order(:population, :desc)
51
+ #
52
+ # listener = query.listen do |snapshot|
53
+ # puts "The query snapshot has #{snapshot.docs.count} documents "
54
+ # puts "and has #{snapshot.changes.count} changes."
55
+ # end
56
+ #
57
+ # # When ready, stop the listen operation and close the stream.
58
+ # listener.stop
59
+ #
43
60
  class Query
44
61
  ##
45
62
  # @private The parent path for the query.
@@ -88,13 +105,15 @@ module Google
88
105
  new_query = @query.dup
89
106
  new_query ||= StructuredQuery.new
90
107
 
108
+ fields = Array(fields).flatten.compact
109
+ fields = [FieldPath.document_id] if fields.empty?
91
110
  field_refs = fields.flatten.compact.map do |field|
92
111
  field = FieldPath.parse field unless field.is_a? FieldPath
93
112
  StructuredQuery::FieldReference.new \
94
113
  field_path: field.formatted_string
95
114
  end
96
115
 
97
- new_query.select ||= StructuredQuery::Projection.new
116
+ new_query.select = StructuredQuery::Projection.new
98
117
  field_refs.each do |field_ref|
99
118
  new_query.select.fields << field_ref
100
119
  end
@@ -130,7 +149,7 @@ module Google
130
149
  new_query ||= StructuredQuery.new
131
150
 
132
151
  if new_query.from.empty?
133
- raise "missing collection_id to specify descendants."
152
+ raise "missing collection_id to specify descendants"
134
153
  end
135
154
 
136
155
  new_query.from.last.all_descendants = true
@@ -166,7 +185,7 @@ module Google
166
185
  new_query ||= StructuredQuery.new
167
186
 
168
187
  if new_query.from.empty?
169
- raise "missing collection_id to specify descendants."
188
+ raise "missing collection_id to specify descendants"
170
189
  end
171
190
 
172
191
  new_query.from.last.all_descendants = false
@@ -213,14 +232,18 @@ module Google
213
232
  # end
214
233
  #
215
234
  def where field, operator, value
235
+ if query_has_cursors?
236
+ raise "cannot call where after calling " \
237
+ "start_at, start_after, end_before, or end_at"
238
+ end
239
+
216
240
  new_query = @query.dup
217
241
  new_query ||= StructuredQuery.new
218
242
 
219
243
  field = FieldPath.parse field unless field.is_a? FieldPath
220
244
 
221
- new_query.where ||= default_filter
222
- new_query.where.composite_filter.filters << \
223
- filter(field.formatted_string, operator, value)
245
+ new_filter = filter field.formatted_string, operator, value
246
+ add_filters_to_query new_query, new_filter
224
247
 
225
248
  Query.start new_query, parent_path, client
226
249
  end
@@ -274,6 +297,11 @@ module Google
274
297
  # end
275
298
  #
276
299
  def order field, direction = :asc
300
+ if query_has_cursors?
301
+ raise "cannot call order after calling " \
302
+ "start_at, start_after, end_before, or end_at"
303
+ end
304
+
277
305
  new_query = @query.dup
278
306
  new_query ||= StructuredQuery.new
279
307
 
@@ -355,108 +383,285 @@ module Google
355
383
  end
356
384
 
357
385
  ##
358
- # Starts query results at a set of field values. The result set will
386
+ # Starts query results at a set of field values. The field values can be
387
+ # specified explicitly as arguments, or can be specified implicitly by
388
+ # providing a {DocumentSnapshot} object instead. The result set will
359
389
  # include the document specified by `values`.
360
390
  #
361
391
  # If the current query already has specified `start_at` or
362
392
  # `start_after`, this will overwrite it.
363
393
  #
364
- # The values provided here are for the field paths provides to `order`.
365
- # Values provided to `start_at` without an associated field path
366
- # provided to `order` will result in an error.
394
+ # The values are associated with the field paths that have been provided
395
+ # to `order`, and must match the same sort order. An ArgumentError will
396
+ # be raised if more explicit values are given than are present in
397
+ # `order`.
367
398
  #
368
- # @param [Object, Array<Object>] values The field value to start the
369
- # query at.
399
+ # @param [DocumentSnapshot, Object, Array<Object>] values The field
400
+ # values to start the query at.
370
401
  #
371
402
  # @return [Query] New query with `start_at` called on it.
372
403
  #
373
- # @example
404
+ # @example Starting a query at a document reference id
374
405
  # require "google/cloud/firestore"
375
406
  #
376
407
  # firestore = Google::Cloud::Firestore.new
377
408
  #
378
409
  # # Get a collection reference
379
410
  # cities_col = firestore.col "cities"
411
+ # nyc_doc_id = "NYC"
380
412
  #
381
413
  # # Create a query
382
- # query = cities_col.start_at("NYC").order(firestore.document_id)
414
+ # query = cities_col.order(firestore.document_id)
415
+ # .start_at(nyc_doc_id)
416
+ #
417
+ # query.get do |city|
418
+ # puts "#{city.document_id} has #{city[:population]} residents."
419
+ # end
420
+ #
421
+ # @example Starting a query at a document reference object
422
+ # require "google/cloud/firestore"
423
+ #
424
+ # firestore = Google::Cloud::Firestore.new
425
+ #
426
+ # # Get a collection reference
427
+ # cities_col = firestore.col "cities"
428
+ # nyc_doc_id = "NYC"
429
+ # nyc_ref = cities_col.doc nyc_doc_id
430
+ #
431
+ # # Create a query
432
+ # query = cities_col.order(firestore.document_id)
433
+ # .start_at(nyc_ref)
434
+ #
435
+ # query.get do |city|
436
+ # puts "#{city.document_id} has #{city[:population]} residents."
437
+ # end
438
+ #
439
+ # @example Starting a query at multiple explicit values
440
+ # require "google/cloud/firestore"
441
+ #
442
+ # firestore = Google::Cloud::Firestore.new
443
+ #
444
+ # # Get a collection reference
445
+ # cities_col = firestore.col "cities"
446
+ #
447
+ # # Create a query
448
+ # query = cities_col.order(:population, :desc)
449
+ # .order(:name)
450
+ # .start_at(1000000, "New York City")
451
+ #
452
+ # query.get do |city|
453
+ # puts "#{city.document_id} has #{city[:population]} residents."
454
+ # end
455
+ #
456
+ # @example Starting a query at a DocumentSnapshot
457
+ # require "google/cloud/firestore"
458
+ #
459
+ # firestore = Google::Cloud::Firestore.new
460
+ #
461
+ # # Get a collection reference
462
+ # cities_col = firestore.col "cities"
463
+ #
464
+ # # Get a document snapshot
465
+ # nyc_snap = firestore.doc("cities/NYC").get
466
+ #
467
+ # # Create a query
468
+ # query = cities_col.order(:population, :desc)
469
+ # .order(:name)
470
+ # .start_at(nyc_snap)
383
471
  #
384
472
  # query.get do |city|
385
473
  # puts "#{city.document_id} has #{city[:population]} residents."
386
474
  # end
387
475
  #
388
476
  def start_at *values
477
+ raise ArgumentError, "must provide values" if values.empty?
478
+
389
479
  new_query = @query.dup
390
480
  new_query ||= StructuredQuery.new
391
481
 
392
- values = values.flatten.map { |value| Convert.raw_to_value value }
393
- new_query.start_at = Google::Firestore::V1beta1::Cursor.new(
394
- values: values, before: true
395
- )
482
+ cursor = values_to_cursor values, new_query
483
+ cursor.before = true
484
+ new_query.start_at = cursor
396
485
 
397
486
  Query.start new_query, parent_path, client
398
487
  end
399
488
 
400
489
  ##
401
- # Starts query results after a set of field values. The result set will
490
+ # Starts query results after a set of field values. The field values can
491
+ # be specified explicitly as arguments, or can be specified implicitly
492
+ # by providing a {DocumentSnapshot} object instead. The result set will
402
493
  # not include the document specified by `values`.
403
494
  #
404
495
  # If the current query already has specified `start_at` or
405
496
  # `start_after`, this will overwrite it.
406
497
  #
407
- # The values provided here are for the field paths provides to `order`.
408
- # Values provided to `start_after` without an associated field path
409
- # provided to `order` will result in an error.
498
+ # The values are associated with the field paths that have been provided
499
+ # to `order`, and must match the same sort order. An ArgumentError will
500
+ # be raised if more explicit values are given than are present in
501
+ # `order`.
410
502
  #
411
- # @param [Object, Array<Object>] values The field value to start the
412
- # query after.
503
+ # @param [DocumentSnapshot, Object, Array<Object>] values The field
504
+ # values to start the query after.
413
505
  #
414
506
  # @return [Query] New query with `start_after` called on it.
415
507
  #
416
- # @example
508
+ # @example Starting a query after a document reference id
509
+ # require "google/cloud/firestore"
510
+ #
511
+ # firestore = Google::Cloud::Firestore.new
512
+ #
513
+ # # Get a collection reference
514
+ # cities_col = firestore.col "cities"
515
+ # nyc_doc_id = "NYC"
516
+ #
517
+ # # Create a query
518
+ # query = cities_col.order(firestore.document_id)
519
+ # .start_after(nyc_doc_id)
520
+ #
521
+ # query.get do |city|
522
+ # puts "#{city.document_id} has #{city[:population]} residents."
523
+ # end
524
+ #
525
+ # @example Starting a query after a document reference object
417
526
  # require "google/cloud/firestore"
418
527
  #
419
528
  # firestore = Google::Cloud::Firestore.new
420
529
  #
421
530
  # # Get a collection reference
422
531
  # cities_col = firestore.col "cities"
532
+ # nyc_doc_id = "NYC"
533
+ # nyc_ref = cities_col.doc nyc_doc_id
423
534
  #
424
535
  # # Create a query
425
- # query = cities_col.start_after("NYC").order(firestore.document_id)
536
+ # query = cities_col.order(firestore.document_id)
537
+ # .start_after(nyc_ref)
538
+ #
539
+ # query.get do |city|
540
+ # puts "#{city.document_id} has #{city[:population]} residents."
541
+ # end
542
+ #
543
+ # @example Starting a query after multiple explicit values
544
+ # require "google/cloud/firestore"
545
+ #
546
+ # firestore = Google::Cloud::Firestore.new
547
+ #
548
+ # # Get a collection reference
549
+ # cities_col = firestore.col "cities"
550
+ #
551
+ # # Create a query
552
+ # query = cities_col.order(:population, :desc)
553
+ # .order(:name)
554
+ # .start_after(1000000, "New York City")
555
+ #
556
+ # query.get do |city|
557
+ # puts "#{city.document_id} has #{city[:population]} residents."
558
+ # end
559
+ #
560
+ # @example Starting a query after a DocumentSnapshot
561
+ # require "google/cloud/firestore"
562
+ #
563
+ # firestore = Google::Cloud::Firestore.new
564
+ #
565
+ # # Get a collection reference
566
+ # cities_col = firestore.col "cities"
567
+ #
568
+ # # Get a document snapshot
569
+ # nyc_snap = firestore.doc("cities/NYC").get
570
+ #
571
+ # # Create a query
572
+ # query = cities_col.order(:population, :desc)
573
+ # .order(:name)
574
+ # .start_after(nyc_snap)
426
575
  #
427
576
  # query.get do |city|
428
577
  # puts "#{city.document_id} has #{city[:population]} residents."
429
578
  # end
430
579
  #
431
580
  def start_after *values
581
+ raise ArgumentError, "must provide values" if values.empty?
582
+
432
583
  new_query = @query.dup
433
584
  new_query ||= StructuredQuery.new
434
585
 
435
- values = values.flatten.map { |value| Convert.raw_to_value value }
436
- new_query.start_at = Google::Firestore::V1beta1::Cursor.new(
437
- values: values, before: false
438
- )
586
+ cursor = values_to_cursor values, new_query
587
+ cursor.before = false
588
+ new_query.start_at = cursor
439
589
 
440
590
  Query.start new_query, parent_path, client
441
591
  end
442
592
 
443
593
  ##
444
- # Ends query results before a set of field values. The result set will
594
+ # Ends query results before a set of field values. The field values can
595
+ # be specified explicitly as arguments, or can be specified implicitly
596
+ # by providing a {DocumentSnapshot} object instead. The result set will
445
597
  # not include the document specified by `values`.
446
598
  #
447
599
  # If the current query already has specified `end_before` or
448
600
  # `end_at`, this will overwrite it.
449
601
  #
450
- # The values provided here are for the field paths provides to `order`.
451
- # Values provided to `end_before` without an associated field path
452
- # provided to `order` will result in an error.
602
+ # The values are associated with the field paths that have been provided
603
+ # to `order`, and must match the same sort order. An ArgumentError will
604
+ # be raised if more explicit values are given than are present in
605
+ # `order`.
453
606
  #
454
- # @param [Object, Array<Object>] values The field value to end the query
455
- # before.
607
+ # @param [DocumentSnapshot, Object, Array<Object>] values The field
608
+ # values to end the query before.
456
609
  #
457
610
  # @return [Query] New query with `end_before` called on it.
458
611
  #
459
- # @example
612
+ # @example Ending a query before a document reference id
613
+ # require "google/cloud/firestore"
614
+ #
615
+ # firestore = Google::Cloud::Firestore.new
616
+ #
617
+ # # Get a collection reference
618
+ # cities_col = firestore.col "cities"
619
+ # nyc_doc_id = "NYC"
620
+ #
621
+ # # Create a query
622
+ # query = cities_col.order(firestore.document_id)
623
+ # .end_before(nyc_doc_id)
624
+ #
625
+ # query.get do |city|
626
+ # puts "#{city.document_id} has #{city[:population]} residents."
627
+ # end
628
+ #
629
+ # @example Ending a query before a document reference object
630
+ # require "google/cloud/firestore"
631
+ #
632
+ # firestore = Google::Cloud::Firestore.new
633
+ #
634
+ # # Get a collection reference
635
+ # cities_col = firestore.col "cities"
636
+ # nyc_doc_id = "NYC"
637
+ # nyc_ref = cities_col.doc nyc_doc_id
638
+ #
639
+ # # Create a query
640
+ # query = cities_col.order(firestore.document_id)
641
+ # .end_before(nyc_ref)
642
+ #
643
+ # query.get do |city|
644
+ # puts "#{city.document_id} has #{city[:population]} residents."
645
+ # end
646
+ #
647
+ # @example Ending a query before multiple explicit values
648
+ # require "google/cloud/firestore"
649
+ #
650
+ # firestore = Google::Cloud::Firestore.new
651
+ #
652
+ # # Get a collection reference
653
+ # cities_col = firestore.col "cities"
654
+ #
655
+ # # Create a query
656
+ # query = cities_col.order(:population, :desc)
657
+ # .order(:name)
658
+ # .end_before(1000000, "New York City")
659
+ #
660
+ # query.get do |city|
661
+ # puts "#{city.document_id} has #{city[:population]} residents."
662
+ # end
663
+ #
664
+ # @example Ending a query before a DocumentSnapshot
460
665
  # require "google/cloud/firestore"
461
666
  #
462
667
  # firestore = Google::Cloud::Firestore.new
@@ -464,64 +669,131 @@ module Google
464
669
  # # Get a collection reference
465
670
  # cities_col = firestore.col "cities"
466
671
  #
672
+ # # Get a document snapshot
673
+ # nyc_snap = firestore.doc("cities/NYC").get
674
+ #
467
675
  # # Create a query
468
- # query = cities_col.end_before("NYC").order(firestore.document_id)
676
+ # query = cities_col.order(:population, :desc)
677
+ # .order(:name)
678
+ # .end_before(nyc_snap)
469
679
  #
470
680
  # query.get do |city|
471
681
  # puts "#{city.document_id} has #{city[:population]} residents."
472
682
  # end
473
683
  #
474
684
  def end_before *values
685
+ raise ArgumentError, "must provide values" if values.empty?
686
+
475
687
  new_query = @query.dup
476
688
  new_query ||= StructuredQuery.new
477
689
 
478
- values = values.flatten.map { |value| Convert.raw_to_value value }
479
- new_query.end_at = Google::Firestore::V1beta1::Cursor.new(
480
- values: values, before: true
481
- )
690
+ cursor = values_to_cursor values, new_query
691
+ cursor.before = true
692
+ new_query.end_at = cursor
482
693
 
483
694
  Query.start new_query, parent_path, client
484
695
  end
485
696
 
486
697
  ##
487
- # Ends query results at a set of field values. The result set will
698
+ # Ends query results at a set of field values. The field values can
699
+ # be specified explicitly as arguments, or can be specified implicitly
700
+ # by providing a {DocumentSnapshot} object instead. The result set will
488
701
  # include the document specified by `values`.
489
702
  #
490
703
  # If the current query already has specified `end_before` or
491
704
  # `end_at`, this will overwrite it.
492
705
  #
493
- # The values provided here are for the field paths provides to `order`.
494
- # Values provided to `end_at` without an associated field path provided
495
- # to `order` will result in an error.
706
+ # The values are associated with the field paths that have been provided
707
+ # to `order`, and must match the same sort order. An ArgumentError will
708
+ # be raised if more explicit values are given than are present in
709
+ # `order`.
496
710
  #
497
- # @param [Object, Array<Object>] values The field value to end the query
498
- # at.
711
+ # @param [DocumentSnapshot, Object, Array<Object>] values The field
712
+ # values to end the query at.
499
713
  #
500
714
  # @return [Query] New query with `end_at` called on it.
501
715
  #
502
- # @example
716
+ # @example Ending a query at a document reference id
503
717
  # require "google/cloud/firestore"
504
718
  #
505
719
  # firestore = Google::Cloud::Firestore.new
506
720
  #
507
721
  # # Get a collection reference
508
722
  # cities_col = firestore.col "cities"
723
+ # nyc_doc_id = "NYC"
509
724
  #
510
725
  # # Create a query
511
- # query = cities_col.end_at("NYC").order(firestore.document_id)
726
+ # query = cities_col.order(firestore.document_id)
727
+ # .end_at(nyc_doc_id)
728
+ #
729
+ # query.get do |city|
730
+ # puts "#{city.document_id} has #{city[:population]} residents."
731
+ # end
732
+ #
733
+ # @example Ending a query at a document reference object
734
+ # require "google/cloud/firestore"
735
+ #
736
+ # firestore = Google::Cloud::Firestore.new
737
+ #
738
+ # # Get a collection reference
739
+ # cities_col = firestore.col "cities"
740
+ # nyc_doc_id = "NYC"
741
+ # nyc_ref = cities_col.doc nyc_doc_id
742
+ #
743
+ # # Create a query
744
+ # query = cities_col.order(firestore.document_id)
745
+ # .end_at(nyc_ref)
746
+ #
747
+ # query.get do |city|
748
+ # puts "#{city.document_id} has #{city[:population]} residents."
749
+ # end
750
+ #
751
+ # @example Ending a query at multiple explicit values
752
+ # require "google/cloud/firestore"
753
+ #
754
+ # firestore = Google::Cloud::Firestore.new
755
+ #
756
+ # # Get a collection reference
757
+ # cities_col = firestore.col "cities"
758
+ #
759
+ # # Create a query
760
+ # query = cities_col.order(:population, :desc)
761
+ # .order(:name)
762
+ # .end_at(1000000, "New York City")
763
+ #
764
+ # query.get do |city|
765
+ # puts "#{city.document_id} has #{city[:population]} residents."
766
+ # end
767
+ #
768
+ # @example Ending a query at a DocumentSnapshot
769
+ # require "google/cloud/firestore"
770
+ #
771
+ # firestore = Google::Cloud::Firestore.new
772
+ #
773
+ # # Get a collection reference
774
+ # cities_col = firestore.col "cities"
775
+ #
776
+ # # Get a document snapshot
777
+ # nyc_snap = firestore.doc("cities/NYC").get
778
+ #
779
+ # # Create a query
780
+ # query = cities_col.order(:population, :desc)
781
+ # .order(:name)
782
+ # .end_at(nyc_snap)
512
783
  #
513
784
  # query.get do |city|
514
785
  # puts "#{city.document_id} has #{city[:population]} residents."
515
786
  # end
516
787
  #
517
788
  def end_at *values
789
+ raise ArgumentError, "must provide values" if values.empty?
790
+
518
791
  new_query = @query.dup
519
792
  new_query ||= StructuredQuery.new
520
793
 
521
- values = values.flatten.map { |value| Convert.raw_to_value value }
522
- new_query.end_at = Google::Firestore::V1beta1::Cursor.new(
523
- values: values, before: false
524
- )
794
+ cursor = values_to_cursor values, new_query
795
+ cursor.before = false
796
+ new_query.end_at = cursor
525
797
 
526
798
  Query.start new_query, parent_path, client
527
799
  end
@@ -552,7 +824,7 @@ module Google
552
824
  def get
553
825
  ensure_service!
554
826
 
555
- return enum_for(:run) unless block_given?
827
+ return enum_for(:get) unless block_given?
556
828
 
557
829
  results = service.run_query parent_path, @query
558
830
  results.each do |result|
@@ -562,6 +834,39 @@ module Google
562
834
  end
563
835
  alias run get
564
836
 
837
+ ##
838
+ # Listen to this query for changes.
839
+ #
840
+ # @yield [callback] The block for accessing the query snapshot.
841
+ # @yieldparam [QuerySnapshot] snapshot A query snapshot.
842
+ #
843
+ # @return [QueryListener] The ongoing listen operation on the query.
844
+ #
845
+ # @example
846
+ # require "google/cloud/firestore"
847
+ #
848
+ # firestore = Google::Cloud::Firestore.new
849
+ #
850
+ # # Create a query
851
+ # query = firestore.col(:cities).order(:population, :desc)
852
+ #
853
+ # listener = query.listen do |snapshot|
854
+ # puts "The query snapshot has #{snapshot.docs.count} documents "
855
+ # puts "and has #{snapshot.changes.count} changes."
856
+ # end
857
+ #
858
+ # # When ready, stop the listen operation and close the stream.
859
+ # listener.stop
860
+ #
861
+ def listen &callback
862
+ raise ArgumentError, "callback required" if callback.nil?
863
+
864
+ ensure_service!
865
+
866
+ QueryListener.new(self, &callback).start
867
+ end
868
+ alias on_snapshot listen
869
+
565
870
  ##
566
871
  # @private Start a new Query.
567
872
  def self.start query, parent_path, client
@@ -608,41 +913,180 @@ module Google
608
913
 
609
914
  def filter name, op, value
610
915
  field = StructuredQuery::FieldReference.new field_path: name.to_s
611
- op = FILTER_OPS[op.to_s.downcase] || :EQUAL
916
+ operator = FILTER_OPS[op.to_s.downcase]
917
+ raise ArgumentError, "unknown operator #{op}" if operator.nil?
612
918
 
613
919
  is_value_nan = value.respond_to?(:nan?) && value.nan?
614
920
  if UNARY_VALUES.include?(value) || is_value_nan
615
- if op != :EQUAL
921
+ if operator != :EQUAL
616
922
  raise ArgumentError,
617
- "can only check equality for #{value} values."
923
+ "can only check equality for #{value} values"
618
924
  end
619
925
 
620
- op = :IS_NULL
621
- op = :IS_NAN if UNARY_NAN_VALUES.include?(value) || is_value_nan
926
+ operator = :IS_NULL
927
+ if UNARY_NAN_VALUES.include?(value) || is_value_nan
928
+ operator = :IS_NAN
929
+ end
622
930
 
623
931
  return StructuredQuery::Filter.new(unary_filter:
624
- StructuredQuery::UnaryFilter.new(field: field, op: op))
932
+ StructuredQuery::UnaryFilter.new(field: field, op: operator))
625
933
  end
626
934
 
627
935
  value = Convert.raw_to_value value
628
936
  StructuredQuery::Filter.new(field_filter:
629
- StructuredQuery::FieldFilter.new(field: field, op: op,
937
+ StructuredQuery::FieldFilter.new(field: field, op: operator,
630
938
  value: value))
631
939
  end
632
940
 
633
- def default_filter
941
+ def composite_filter
634
942
  StructuredQuery::Filter.new(composite_filter:
635
943
  StructuredQuery::CompositeFilter.new(op: :AND))
636
944
  end
637
945
 
638
- def order_direction direction
639
- if direction.to_s.downcase.start_with? "a"
640
- :ASCENDING
641
- elsif direction.to_s.downcase.start_with? "d"
642
- :DESCENDING
946
+ def add_filters_to_query query, filter
947
+ if query.where.nil?
948
+ query.where = filter
949
+ elsif query.where.filter_type == :composite_filter
950
+ query.where.composite_filter.filters << filter
643
951
  else
644
- :DIRECTION_UNSPECIFIED
952
+ old_filter = query.where
953
+ query.where = composite_filter
954
+ query.where.composite_filter.filters << old_filter
955
+ query.where.composite_filter.filters << filter
956
+ end
957
+ end
958
+
959
+ def order_direction direction
960
+ return :DESCENDING if direction.to_s.downcase.start_with? "d".freeze
961
+ :ASCENDING
962
+ end
963
+
964
+ def query_has_cursors?
965
+ query.start_at || query.end_at
966
+ end
967
+
968
+ def values_to_cursor values, query
969
+ if values.count == 1 && values.first.is_a?(DocumentSnapshot)
970
+ return snapshot_to_cursor(values.first, query)
971
+ end
972
+
973
+ # pair values with their field_paths to ensure correct formatting
974
+ order_field_paths = order_by_field_paths query
975
+ if values.count > order_field_paths.count
976
+ # raise if too many values provided for the cursor
977
+ raise ArgumentError, "too many values"
978
+ end
979
+
980
+ values = values.zip(order_field_paths).map do |value, field_path|
981
+ if field_path == doc_id_path && !value.is_a?(DocumentReference)
982
+ value = document_reference value
983
+ end
984
+ Convert.raw_to_value value
985
+ end
986
+
987
+ Google::Firestore::V1beta1::Cursor.new values: values
988
+ end
989
+
990
+ def snapshot_to_cursor snapshot, query
991
+ if snapshot.parent.path != query_collection_path
992
+ raise ArgumentError, "cursor snapshot must belong to collection"
993
+ end
994
+
995
+ # first, add any inequality filters missing from existing order_by
996
+ ensure_inequality_field_paths_in_order_by! query
997
+
998
+ # second, make sure __name__ is present in order_by
999
+ ensure_document_id_in_order_by! query
1000
+
1001
+ # lastly, create cursor for all field_paths in order_by
1002
+ values = order_by_field_paths(query).map do |field_path|
1003
+ if field_path == doc_id_path
1004
+ snapshot.ref
1005
+ else
1006
+ snapshot[field_path]
1007
+ end
645
1008
  end
1009
+
1010
+ values_to_cursor values, query
1011
+ end
1012
+
1013
+ def ensure_inequality_field_paths_in_order_by! query
1014
+ inequality_paths = inequality_filter_field_paths query
1015
+ orig_order = order_by_field_paths query
1016
+
1017
+ inequality_paths.reverse.each do |field_path|
1018
+ next if orig_order.include? field_path
1019
+
1020
+ query.order_by.unshift StructuredQuery::Order.new(
1021
+ field: StructuredQuery::FieldReference.new(
1022
+ field_path: field_path
1023
+ ),
1024
+ direction: :ASCENDING
1025
+ )
1026
+ end
1027
+ end
1028
+
1029
+ def ensure_document_id_in_order_by! query
1030
+ return if order_by_field_paths(query).include? doc_id_path
1031
+
1032
+ query.order_by.push StructuredQuery::Order.new(
1033
+ field: StructuredQuery::FieldReference.new(
1034
+ field_path: doc_id_path
1035
+ ),
1036
+ direction: last_order_direction(query)
1037
+ )
1038
+ end
1039
+
1040
+ def inequality_filter_field_paths query
1041
+ return [] if query.where.nil?
1042
+
1043
+ # The way we construct a query, where is always a CompositeFilter
1044
+ filters = if query.where.filter_type == :composite_filter
1045
+ query.where.composite_filter.filters
1046
+ else
1047
+ [query.where]
1048
+ end
1049
+ ineq_filters = filters.select do |filter|
1050
+ if filter.filter_type == :field_filter
1051
+ filter.field_filter.op != :EQUAL
1052
+ end
1053
+ end
1054
+ ineq_filters.map { |filter| filter.field_filter.field.field_path }
1055
+ end
1056
+
1057
+ def order_by_field_paths query
1058
+ query.order_by.map { |order_by| order_by.field.field_path }
1059
+ end
1060
+
1061
+ def last_order_direction query
1062
+ last_order_by = query.order_by.last
1063
+ return :ASCENDING if last_order_by.nil?
1064
+ last_order_by.direction
1065
+ end
1066
+
1067
+ def document_reference document_path
1068
+ if document_path.to_s.split("/").count.even?
1069
+ raise ArgumentError, "document_path must refer to a document"
1070
+ end
1071
+
1072
+ DocumentReference.from_path(
1073
+ "#{query_collection_path}/#{document_path}", client
1074
+ )
1075
+ end
1076
+
1077
+ def query_collection_path
1078
+ "#{parent_path}/#{query_collection_id}"
1079
+ end
1080
+
1081
+ def query_collection_id
1082
+ # We trust that query.from is always set, since Query cannot be
1083
+ # created without it.
1084
+ return nil if query.from.empty?
1085
+ query.from.first.collection_id
1086
+ end
1087
+
1088
+ def doc_id_path
1089
+ "__name__".freeze
646
1090
  end
647
1091
 
648
1092
  ##