google-cloud-firestore 2.3.0 → 2.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -18,6 +18,7 @@ require "google/cloud/firestore/document_snapshot"
18
18
  require "google/cloud/firestore/collection_reference"
19
19
  require "google/cloud/firestore/document_listener"
20
20
  require "google/cloud/firestore/document_reference/list"
21
+ require "google/cloud/firestore/resource_path"
21
22
 
22
23
  module Google
23
24
  module Cloud
@@ -90,11 +91,11 @@ module Google
90
91
  # puts col.collection_id
91
92
  # end
92
93
  #
93
- def cols
94
+ def cols &block
94
95
  ensure_service!
95
96
  grpc = service.list_collections path
96
97
  cols_enum = CollectionReferenceList.from_grpc(grpc, client, path).all
97
- cols_enum.each { |c| yield c } if block_given?
98
+ cols_enum.each(&block) if block_given?
98
99
  cols_enum
99
100
  end
100
101
  alias collections cols
@@ -459,6 +460,12 @@ module Google
459
460
 
460
461
  # @!endgroup
461
462
 
463
+ # @private
464
+ def <=> other
465
+ return nil unless other.is_a? self.class
466
+ ResourcePath.from_path(path) <=> ResourcePath.from_path(other.path)
467
+ end
468
+
462
469
  ##
463
470
  # @private New DocumentReference object from a path.
464
471
  def self.from_path path, client
@@ -86,8 +86,7 @@ module Google
86
86
  def next
87
87
  return nil unless next?
88
88
  ensure_client!
89
- options = { token: token, max: @max }
90
- grpc = @client.service.list_documents @parent, @collection_id, options
89
+ grpc = @client.service.list_documents @parent, @collection_id, token: token, max: @max
91
90
  self.class.from_grpc grpc, @client, @parent, @collection_id, @max
92
91
  end
93
92
 
@@ -110,7 +109,7 @@ module Google
110
109
  #
111
110
  # @return [Enumerator]
112
111
  #
113
- # @example Iterating each document reference by passing a block:
112
+ # @example Iterating each document reference by passing a block or proc:
114
113
  # require "google/cloud/firestore"
115
114
  #
116
115
  # firestore = Google::Cloud::Firestore.new
@@ -143,17 +142,17 @@ module Google
143
142
  # puts doc_ref.document_id
144
143
  # end
145
144
  #
146
- def all request_limit: nil
145
+ def all request_limit: nil, &block
147
146
  request_limit = request_limit.to_i if request_limit
148
147
  unless block_given?
149
148
  return enum_for :all, request_limit: request_limit
150
149
  end
151
150
  results = self
152
151
  loop do
153
- results.each { |r| yield r }
152
+ results.each(&block)
154
153
  if request_limit
155
154
  request_limit -= 1
156
- break if request_limit < 0
155
+ break if request_limit.negative?
157
156
  end
158
157
  break unless results.next?
159
158
  results = results.next
@@ -210,14 +210,14 @@ module Google
210
210
  protected
211
211
 
212
212
  START_FIELD_PATH_CHARS = /\A[a-zA-Z_]/.freeze
213
- INVALID_FIELD_PATH_CHARS = %r{[\~\*\/\[\]]}.freeze
213
+ INVALID_FIELD_PATH_CHARS = %r{[~*/\[\]]}.freeze
214
214
 
215
215
  def escape_field_for_path field
216
216
  field = String field
217
217
 
218
218
  if INVALID_FIELD_PATH_CHARS.match(field) ||
219
219
  field["."] || field["`"] || field["\\"]
220
- escaped_field = field.gsub(/[\`\\]/, "`" => "\\\`", "\\" => "\\\\")
220
+ escaped_field = field.gsub(/[`\\]/, "`" => "\\\`", "\\" => "\\\\")
221
221
  return "`#{escaped_field}`"
222
222
  end
223
223
 
@@ -27,9 +27,14 @@ module Google
27
27
  #
28
28
  # firestore = Google::Cloud::Firestore.new
29
29
  #
30
- # user_snap = firestore.doc("users/frank").get
30
+ # # Get a document reference
31
+ # nyc_ref = firestore.doc "cities/NYC"
31
32
  #
32
- # # TODO
33
+ # # Set the population to increment by 1.
34
+ # increment_value = Google::Cloud::Firestore::FieldValue.increment 1
35
+ #
36
+ # nyc_ref.update({ name: "New York City",
37
+ # population: increment_value })
33
38
  #
34
39
  class FieldValue
35
40
  ##
@@ -17,6 +17,7 @@ require "google/cloud/firestore/v1"
17
17
  require "google/cloud/firestore/document_snapshot"
18
18
  require "google/cloud/firestore/query_listener"
19
19
  require "google/cloud/firestore/convert"
20
+ require "json"
20
21
 
21
22
  module Google
22
23
  module Cloud
@@ -74,6 +75,16 @@ module Google
74
75
  # @private The firestore client object.
75
76
  attr_accessor :client
76
77
 
78
+ ##
79
+ # @private Creates a new Query.
80
+ def initialize query, parent_path, client, limit_type: nil
81
+ query ||= StructuredQuery.new
82
+ @query = query
83
+ @parent_path = parent_path
84
+ @limit_type = limit_type
85
+ @client = client
86
+ end
87
+
77
88
  ##
78
89
  # Restricts documents matching the query to return only data for the
79
90
  # provided fields.
@@ -216,6 +227,9 @@ module Google
216
227
  # * greater than: `>`, `gt`
217
228
  # * greater than or equal: `>=`, `gte`
218
229
  # * equal: `=`, `==`, `eq`, `eql`, `is`
230
+ # * not equal: `!=`
231
+ # * in: `in`
232
+ # * not in: `not-in`, `not_in`
219
233
  # * array contains: `array-contains`, `array_contains`
220
234
  # @param [Object] value A value the field is compared to.
221
235
  #
@@ -959,16 +973,71 @@ module Google
959
973
  end
960
974
  alias on_snapshot listen
961
975
 
976
+ ##
977
+ # Serializes the instance to a JSON text string. See also {Query.from_json}.
978
+ #
979
+ # @return [String] A JSON text string.
980
+ #
981
+ # @example
982
+ # require "google/cloud/firestore"
983
+ #
984
+ # firestore = Google::Cloud::Firestore.new
985
+ # query = firestore.col(:cities).select(:population)
986
+ #
987
+ # json = query.to_json
988
+ #
989
+ # new_query = Google::Cloud::Firestore::Query.from_json json, firestore
990
+ #
991
+ # new_query.get do |city|
992
+ # puts "#{city.document_id} has #{city[:population]} residents."
993
+ # end
994
+ #
995
+ def to_json options = nil
996
+ query_json = Google::Cloud::Firestore::V1::StructuredQuery.encode_json query
997
+ {
998
+ "query" => JSON.parse(query_json),
999
+ "parent_path" => parent_path,
1000
+ "limit_type" => limit_type
1001
+ }.to_json options
1002
+ end
1003
+
1004
+ ##
1005
+ # Deserializes a JSON text string serialized from this class and returns it as a new instance. See also
1006
+ # {#to_json}.
1007
+ #
1008
+ # @param [String] json A JSON text string serialized using {#to_json}.
1009
+ # @param [Google::Cloud::Firestore::Client] client A connected client instance.
1010
+ #
1011
+ # @return [Query] A new query equal to the original query used to create the JSON text string.
1012
+ #
1013
+ # @example
1014
+ # require "google/cloud/firestore"
1015
+ #
1016
+ # firestore = Google::Cloud::Firestore.new
1017
+ # query = firestore.col(:cities).select(:population)
1018
+ #
1019
+ # json = query.to_json
1020
+ #
1021
+ # new_query = Google::Cloud::Firestore::Query.from_json json, firestore
1022
+ #
1023
+ # new_query.get do |city|
1024
+ # puts "#{city.document_id} has #{city[:population]} residents."
1025
+ # end
1026
+ #
1027
+ def self.from_json json, client
1028
+ raise ArgumentError, "client is required" unless client
1029
+
1030
+ json = JSON.parse json
1031
+ query_json = json["query"]
1032
+ raise ArgumentError, "Field 'query' is required" unless query_json
1033
+ query = Google::Cloud::Firestore::V1::StructuredQuery.decode_json query_json.to_json
1034
+ start query, json["parent_path"], client, limit_type: json["limit_type"]&.to_sym
1035
+ end
1036
+
962
1037
  ##
963
1038
  # @private Start a new Query.
964
1039
  def self.start query, parent_path, client, limit_type: nil
965
- query ||= StructuredQuery.new
966
- Query.new.tap do |q|
967
- q.instance_variable_set :@query, query
968
- q.instance_variable_set :@parent_path, parent_path
969
- q.instance_variable_set :@limit_type, limit_type
970
- q.instance_variable_set :@client, client
971
- end
1040
+ new query, parent_path, client, limit_type: limit_type
972
1041
  end
973
1042
 
974
1043
  protected
@@ -993,28 +1062,25 @@ module Google
993
1062
  "eq" => :EQUAL,
994
1063
  "eql" => :EQUAL,
995
1064
  "is" => :EQUAL,
1065
+ "!=" => :NOT_EQUAL,
996
1066
  "array_contains" => :ARRAY_CONTAINS,
997
1067
  "array-contains" => :ARRAY_CONTAINS,
998
1068
  "include" => :ARRAY_CONTAINS,
999
1069
  "include?" => :ARRAY_CONTAINS,
1000
1070
  "has" => :ARRAY_CONTAINS,
1001
1071
  "in" => :IN,
1072
+ "not_in" => :NOT_IN,
1073
+ "not-in" => :NOT_IN,
1002
1074
  "array_contains_any" => :ARRAY_CONTAINS_ANY,
1003
1075
  "array-contains-any" => :ARRAY_CONTAINS_ANY
1004
1076
  }.freeze
1005
1077
  ##
1006
1078
  # @private
1007
- EQUALITY_FILTERS = %i[
1008
- EQUAL
1009
- ARRAY_CONTAINS
1010
- ].freeze
1011
- ##
1012
- # @private
1013
- INEQUALITY_FILTERS = %i[
1014
- LESS_THAN
1015
- LESS_THAN_OR_EQUAL
1016
- GREATER_THAN
1017
- GREATER_THAN_OR_EQUAL
1079
+ INEQUALITY_FILTERS = [
1080
+ :LESS_THAN,
1081
+ :LESS_THAN_OR_EQUAL,
1082
+ :GREATER_THAN,
1083
+ :GREATER_THAN_OR_EQUAL
1018
1084
  ].freeze
1019
1085
 
1020
1086
  def value_nil? value
@@ -1031,18 +1097,20 @@ module Google
1031
1097
  value_nil?(value) || value_nan?(value)
1032
1098
  end
1033
1099
 
1034
- def filter name, op, value
1100
+ def filter name, op_key, value
1035
1101
  field = StructuredQuery::FieldReference.new field_path: name.to_s
1036
- operator = FILTER_OPS[op.to_s.downcase]
1037
- raise ArgumentError, "unknown operator #{op}" if operator.nil?
1102
+ operator = FILTER_OPS[op_key.to_s.downcase]
1103
+ raise ArgumentError, "unknown operator #{op_key}" if operator.nil?
1038
1104
 
1039
1105
  if value_unary? value
1040
- if operator != :EQUAL
1041
- raise ArgumentError,
1042
- "can only check equality for #{value} values"
1043
- end
1044
-
1045
- operator = value_nan?(value) ? :IS_NAN : :IS_NULL
1106
+ operator = case operator
1107
+ when :EQUAL
1108
+ value_nan?(value) ? :IS_NAN : :IS_NULL
1109
+ when :NOT_EQUAL
1110
+ value_nan?(value) ? :IS_NOT_NAN : :IS_NOT_NULL
1111
+ else
1112
+ raise ArgumentError, "can only perform '==' and '!=' comparisons on #{value} values"
1113
+ end
1046
1114
 
1047
1115
  return StructuredQuery::Filter.new(
1048
1116
  unary_filter: StructuredQuery::UnaryFilter.new(
@@ -1092,11 +1160,14 @@ module Google
1092
1160
  return snapshot_to_cursor values.first, query
1093
1161
  end
1094
1162
 
1163
+ # The *values param in start_at, start_after, etc. will wrap an array argument in an array, so unwrap it here.
1164
+ values = values.first if values.count == 1 && values.first.is_a?(Array)
1165
+
1095
1166
  # pair values with their field_paths to ensure correct formatting
1096
1167
  order_field_paths = order_by_field_paths query
1097
1168
  if values.count > order_field_paths.count
1098
1169
  # raise if too many values provided for the cursor
1099
- raise ArgumentError, "too many values"
1170
+ raise ArgumentError, "There cannot be more cursor values than order by fields"
1100
1171
  end
1101
1172
 
1102
1173
  values = values.zip(order_field_paths).map do |value, field_path|
@@ -1128,7 +1199,6 @@ module Google
1128
1199
  snapshot[field_path]
1129
1200
  end
1130
1201
  end
1131
-
1132
1202
  values_to_cursor values, query
1133
1203
  end
1134
1204
 
@@ -0,0 +1,80 @@
1
+ # Copyright 2021 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
+ module Google
17
+ module Cloud
18
+ module Firestore
19
+ ##
20
+ # # QueryPartition
21
+ #
22
+ # Represents a split point that can be used in a query as a starting and/or end point for the query results.
23
+ #
24
+ # The cursors returned by {#start_at} and {#end_before} can only be used in a query that matches the constraint of
25
+ # the query that produced this partition.
26
+ #
27
+ # See {CollectionGroup#partitions} and {Query}.
28
+ #
29
+ # @!attribute [r] start_at
30
+ # The cursor values that define the first result for this partition, or `nil` if this is the first partition.
31
+ # Returns an array of values that represent a position, in the order they appear in the order by clause of the
32
+ # query. Can contain fewer values than specified in the order by clause. Will be used in the query returned by
33
+ # {#to_query}.
34
+ # @return [Array<Object>, nil] Typically, the values are {DocumentReference} objects.
35
+ # @!attribute [r] end_before
36
+ # The cursor values that define the first result after this partition, or `nil` if this is the last partition.
37
+ # Returns an array of values that represent a position, in the order they appear in the order by clause of the
38
+ # query. Can contain fewer values than specified in the order by clause. Will be used in the query returned by
39
+ # {#to_query}.
40
+ # @return [Array<Object>, nil] Typically, the values are {DocumentReference} objects.
41
+ #
42
+ # @example
43
+ # require "google/cloud/firestore"
44
+ #
45
+ # firestore = Google::Cloud::Firestore.new
46
+ #
47
+ # col_group = firestore.col_group "cities"
48
+ #
49
+ # partitions = col_group.partitions 3
50
+ #
51
+ # queries = partitions.map(&:to_query)
52
+ #
53
+ class QueryPartition
54
+ attr_reader :start_at
55
+ attr_reader :end_before
56
+
57
+ ##
58
+ # @private New QueryPartition from query and Cursor
59
+ def initialize query, start_at, end_before
60
+ @query = query
61
+ @start_at = start_at
62
+ @end_before = end_before
63
+ end
64
+
65
+ ##
66
+ # Creates a new query that only returns the documents for this partition, using the cursor values from
67
+ # {#start_at} and {#end_before}.
68
+ #
69
+ # @return [Query] The query for the partition.
70
+ #
71
+ def to_query
72
+ base_query = @query
73
+ base_query = base_query.start_at start_at if start_at
74
+ base_query = base_query.end_before end_before if end_before
75
+ base_query
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,58 @@
1
+ # Copyright 2021 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
+ module Google
17
+ module Cloud
18
+ module Firestore
19
+ ##
20
+ # @private
21
+ #
22
+ # Represents a resource path to the Firestore API.
23
+ #
24
+ class ResourcePath
25
+ include Comparable
26
+
27
+ RESOURCE_PATH_RE = %r{^projects/([^/]*)/databases/([^/]*)(?:/documents/)?([\s\S]*)$}.freeze
28
+
29
+ attr_reader :project_id
30
+ attr_reader :database_id
31
+ attr_reader :segments
32
+
33
+ ##
34
+ # Creates a resource path object.
35
+ #
36
+ # @param [Array<String>] segments One or more strings representing the resource path.
37
+ #
38
+ # @return [ResourcePath] The resource path object.
39
+ #
40
+ def initialize project_id, database_id, segments
41
+ @project_id = project_id
42
+ @database_id = database_id
43
+ @segments = segments.split "/"
44
+ end
45
+
46
+ def <=> other
47
+ return nil unless other.is_a? ResourcePath
48
+ [project_id, database_id, segments] <=> [other.project_id, other.database_id, other.segments]
49
+ end
50
+
51
+ def self.from_path path
52
+ data = RESOURCE_PATH_RE.match path
53
+ new data[1], data[2], data[3]
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end