google-cloud-firestore 0.20.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.yardopts +8 -0
- data/LICENSE +201 -0
- data/README.md +30 -0
- data/lib/google-cloud-firestore.rb +106 -0
- data/lib/google/cloud/firestore.rb +514 -0
- data/lib/google/cloud/firestore/batch.rb +462 -0
- data/lib/google/cloud/firestore/client.rb +449 -0
- data/lib/google/cloud/firestore/collection_reference.rb +249 -0
- data/lib/google/cloud/firestore/commit_response.rb +145 -0
- data/lib/google/cloud/firestore/convert.rb +561 -0
- data/lib/google/cloud/firestore/credentials.rb +35 -0
- data/lib/google/cloud/firestore/document_reference.rb +468 -0
- data/lib/google/cloud/firestore/document_snapshot.rb +324 -0
- data/lib/google/cloud/firestore/field_path.rb +216 -0
- data/lib/google/cloud/firestore/field_value.rb +113 -0
- data/lib/google/cloud/firestore/generate.rb +35 -0
- data/lib/google/cloud/firestore/query.rb +651 -0
- data/lib/google/cloud/firestore/service.rb +176 -0
- data/lib/google/cloud/firestore/transaction.rb +726 -0
- data/lib/google/cloud/firestore/v1beta1.rb +121 -0
- data/lib/google/cloud/firestore/v1beta1/doc/google/firestore/v1beta1/common.rb +63 -0
- data/lib/google/cloud/firestore/v1beta1/doc/google/firestore/v1beta1/document.rb +134 -0
- data/lib/google/cloud/firestore/v1beta1/doc/google/firestore/v1beta1/firestore.rb +584 -0
- data/lib/google/cloud/firestore/v1beta1/doc/google/firestore/v1beta1/query.rb +215 -0
- data/lib/google/cloud/firestore/v1beta1/doc/google/firestore/v1beta1/write.rb +167 -0
- data/lib/google/cloud/firestore/v1beta1/doc/google/protobuf/any.rb +124 -0
- data/lib/google/cloud/firestore/v1beta1/doc/google/protobuf/timestamp.rb +106 -0
- data/lib/google/cloud/firestore/v1beta1/doc/google/protobuf/wrappers.rb +89 -0
- data/lib/google/cloud/firestore/v1beta1/doc/google/rpc/status.rb +83 -0
- data/lib/google/cloud/firestore/v1beta1/doc/overview.rb +53 -0
- data/lib/google/cloud/firestore/v1beta1/firestore_client.rb +974 -0
- data/lib/google/cloud/firestore/v1beta1/firestore_client_config.json +100 -0
- data/lib/google/cloud/firestore/version.rb +22 -0
- data/lib/google/firestore/v1beta1/common_pb.rb +44 -0
- data/lib/google/firestore/v1beta1/document_pb.rb +49 -0
- data/lib/google/firestore/v1beta1/firestore_pb.rb +219 -0
- data/lib/google/firestore/v1beta1/firestore_services_pb.rb +87 -0
- data/lib/google/firestore/v1beta1/query_pb.rb +103 -0
- data/lib/google/firestore/v1beta1/write_pb.rb +73 -0
- metadata +251 -0
@@ -0,0 +1,145 @@
|
|
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/v1beta1"
|
17
|
+
require "google/cloud/firestore/convert"
|
18
|
+
|
19
|
+
module Google
|
20
|
+
module Cloud
|
21
|
+
module Firestore
|
22
|
+
##
|
23
|
+
# # CommitResponse
|
24
|
+
#
|
25
|
+
# The response for a commit.
|
26
|
+
#
|
27
|
+
# @example
|
28
|
+
# require "google/cloud/firestore"
|
29
|
+
#
|
30
|
+
# firestore = Google::Cloud::Firestore.new
|
31
|
+
#
|
32
|
+
# commit_response = firestore.batch do |b|
|
33
|
+
# # Set the data for NYC
|
34
|
+
# b.set("cities/NYC", { name: "New York City" })
|
35
|
+
#
|
36
|
+
# # Update the population for SF
|
37
|
+
# b.update("cities/SF", { population: 1000000 })
|
38
|
+
#
|
39
|
+
# # Delete LA
|
40
|
+
# b.delete("cities/LA")
|
41
|
+
# end
|
42
|
+
#
|
43
|
+
# puts commit_response.commit_time
|
44
|
+
# commit_response.write_results.each do |write_result|
|
45
|
+
# puts write_result.update_time
|
46
|
+
# end
|
47
|
+
#
|
48
|
+
class CommitResponse
|
49
|
+
##
|
50
|
+
# @private
|
51
|
+
def initialize
|
52
|
+
@commit_time = nil
|
53
|
+
@write_results = []
|
54
|
+
end
|
55
|
+
|
56
|
+
##
|
57
|
+
# The time at which the commit occurred.
|
58
|
+
#
|
59
|
+
# @return [Time] The commit time.
|
60
|
+
attr_accessor :commit_time
|
61
|
+
|
62
|
+
##
|
63
|
+
# The result of applying the writes.
|
64
|
+
#
|
65
|
+
# This i-th write result corresponds to the i-th write in the request.
|
66
|
+
#
|
67
|
+
# @return [Array<CommitResponse::WriteResult>] The write results.
|
68
|
+
attr_accessor :write_results
|
69
|
+
|
70
|
+
##
|
71
|
+
# @private
|
72
|
+
def self.from_grpc grpc, writes
|
73
|
+
return new if grpc.nil?
|
74
|
+
|
75
|
+
commit_time = Convert.timestamp_to_time grpc.commit_time
|
76
|
+
|
77
|
+
all_write_results = Array(grpc.write_results)
|
78
|
+
|
79
|
+
write_results = writes.map do |write|
|
80
|
+
update_time = nil
|
81
|
+
Array(write).count.times do
|
82
|
+
write_grpc = all_write_results.shift
|
83
|
+
if write_grpc
|
84
|
+
update_time ||= Convert.timestamp_to_time write_grpc.update_time
|
85
|
+
end
|
86
|
+
end
|
87
|
+
update_time ||= commit_time
|
88
|
+
WriteResult.new.tap do |write_result|
|
89
|
+
write_result.instance_variable_set :@update_time, update_time
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
new.tap do |resp|
|
94
|
+
resp.instance_variable_set :@commit_time, commit_time
|
95
|
+
resp.instance_variable_set :@write_results, write_results
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
##
|
100
|
+
# # WriteResult
|
101
|
+
#
|
102
|
+
# Represents the result of applying a write.
|
103
|
+
#
|
104
|
+
# @example
|
105
|
+
# require "google/cloud/firestore"
|
106
|
+
#
|
107
|
+
# firestore = Google::Cloud::Firestore.new
|
108
|
+
#
|
109
|
+
# commit_response = firestore.batch do |b|
|
110
|
+
# # Set the data for NYC
|
111
|
+
# b.set("cities/NYC", { name: "New York City" })
|
112
|
+
#
|
113
|
+
# # Update the population for SF
|
114
|
+
# b.update("cities/SF", { population: 1000000 })
|
115
|
+
#
|
116
|
+
# # Delete LA
|
117
|
+
# b.delete("cities/LA")
|
118
|
+
# end
|
119
|
+
#
|
120
|
+
# puts commit_response.commit_time
|
121
|
+
# commit_response.write_results.each do |write_result|
|
122
|
+
# puts write_result.update_time
|
123
|
+
# end
|
124
|
+
#
|
125
|
+
class WriteResult
|
126
|
+
##
|
127
|
+
# @private
|
128
|
+
def initialize
|
129
|
+
@update_time = nil
|
130
|
+
end
|
131
|
+
|
132
|
+
##
|
133
|
+
# The last update time of the document after applying the write. Not
|
134
|
+
# set after a +delete+.
|
135
|
+
#
|
136
|
+
# If the write did not actually change the document, this will be
|
137
|
+
# the previous update_time.
|
138
|
+
#
|
139
|
+
# @return [Time] The last update time.
|
140
|
+
attr_accessor :update_time
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
@@ -0,0 +1,561 @@
|
|
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/v1beta1"
|
17
|
+
require "google/cloud/firestore/field_path"
|
18
|
+
require "time"
|
19
|
+
require "stringio"
|
20
|
+
|
21
|
+
module Google
|
22
|
+
module Cloud
|
23
|
+
module Firestore
|
24
|
+
##
|
25
|
+
# @private Helper module for converting Protobuf values.
|
26
|
+
module Convert
|
27
|
+
# rubocop:disable all
|
28
|
+
module ClassMethods
|
29
|
+
def time_to_timestamp time
|
30
|
+
return nil if time.nil?
|
31
|
+
|
32
|
+
# Force the object to be a Time object.
|
33
|
+
time = time.to_time
|
34
|
+
|
35
|
+
Google::Protobuf::Timestamp.new \
|
36
|
+
seconds: time.to_i,
|
37
|
+
nanos: time.nsec
|
38
|
+
end
|
39
|
+
|
40
|
+
def timestamp_to_time timestamp
|
41
|
+
return nil if timestamp.nil?
|
42
|
+
|
43
|
+
Time.at timestamp.seconds, Rational(timestamp.nanos, 1000)
|
44
|
+
end
|
45
|
+
|
46
|
+
def fields_to_hash fields, context
|
47
|
+
Hash[fields.map do |key, value|
|
48
|
+
[key.to_sym, value_to_raw(value, context)]
|
49
|
+
end]
|
50
|
+
end
|
51
|
+
|
52
|
+
def hash_to_fields hash
|
53
|
+
Hash[hash.map do |key, value|
|
54
|
+
[String(key), raw_to_value(value)]
|
55
|
+
end]
|
56
|
+
end
|
57
|
+
|
58
|
+
def value_to_raw value, context
|
59
|
+
case value.value_type
|
60
|
+
when :null_value
|
61
|
+
nil
|
62
|
+
when :boolean_value
|
63
|
+
value.boolean_value
|
64
|
+
when :integer_value
|
65
|
+
Integer value.integer_value
|
66
|
+
when :double_value
|
67
|
+
value.double_value
|
68
|
+
when :timestamp_value
|
69
|
+
timestamp_to_time value.timestamp_value
|
70
|
+
when :string_value
|
71
|
+
value.string_value
|
72
|
+
when :bytes_value
|
73
|
+
StringIO.new Base64.decode64 value.bytes_value
|
74
|
+
when :reference_value
|
75
|
+
Google::Cloud::Firestore::DocumentReference.from_path \
|
76
|
+
value.reference_value, context
|
77
|
+
when :geo_point_value
|
78
|
+
value.geo_point_value.to_hash
|
79
|
+
when :array_value
|
80
|
+
value.array_value.values.map { |v| value_to_raw v, context }
|
81
|
+
when :map_value
|
82
|
+
fields_to_hash value.map_value.fields, context
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def raw_to_value obj
|
87
|
+
if NilClass === obj
|
88
|
+
Google::Firestore::V1beta1::Value.new null_value: :NULL_VALUE
|
89
|
+
elsif TrueClass === obj || FalseClass === obj
|
90
|
+
Google::Firestore::V1beta1::Value.new boolean_value: obj
|
91
|
+
elsif Integer === obj
|
92
|
+
Google::Firestore::V1beta1::Value.new integer_value: obj
|
93
|
+
elsif Numeric === obj # Any number not an integer is a double
|
94
|
+
Google::Firestore::V1beta1::Value.new double_value: obj.to_f
|
95
|
+
elsif Time === obj || DateTime === obj || Date === obj
|
96
|
+
Google::Firestore::V1beta1::Value.new \
|
97
|
+
timestamp_value: time_to_timestamp(obj.to_time)
|
98
|
+
elsif String === obj || Symbol === obj
|
99
|
+
Google::Firestore::V1beta1::Value.new string_value: obj.to_s
|
100
|
+
elsif Google::Cloud::Firestore::DocumentReference === obj
|
101
|
+
Google::Firestore::V1beta1::Value.new reference_value: obj.path
|
102
|
+
elsif Array === obj
|
103
|
+
values = obj.map { |o| raw_to_value(o) }
|
104
|
+
Google::Firestore::V1beta1::Value.new(array_value:
|
105
|
+
Google::Firestore::V1beta1::ArrayValue.new(values: values))
|
106
|
+
elsif Hash === obj
|
107
|
+
if obj.keys.sort == [:latitude, :longitude]
|
108
|
+
Google::Firestore::V1beta1::Value.new(geo_point_value:
|
109
|
+
Google::Type::LatLng.new(obj))
|
110
|
+
else
|
111
|
+
fields = hash_to_fields obj
|
112
|
+
Google::Firestore::V1beta1::Value.new(map_value:
|
113
|
+
Google::Firestore::V1beta1::MapValue.new(fields: fields))
|
114
|
+
end
|
115
|
+
elsif obj.respond_to?(:read) && obj.respond_to?(:rewind)
|
116
|
+
obj.rewind
|
117
|
+
content = obj.read.force_encoding "ASCII-8BIT"
|
118
|
+
encoded_content = Base64.strict_encode64 content
|
119
|
+
Google::Firestore::V1beta1::Value.new bytes_value: encoded_content
|
120
|
+
else
|
121
|
+
fail ArgumentError,
|
122
|
+
"A value of type #{obj.class} is not supported."
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def writes_for_create doc_path, data
|
127
|
+
writes = []
|
128
|
+
|
129
|
+
if is_field_value_nested data, :delete
|
130
|
+
fail ArgumentError, "DELETE not allowed on create"
|
131
|
+
end
|
132
|
+
fail ArgumentError, "data is required" unless data.is_a? Hash
|
133
|
+
|
134
|
+
data, server_time_paths = remove_field_value_from data, :server_time
|
135
|
+
|
136
|
+
if data.any? || server_time_paths.empty?
|
137
|
+
write = Google::Firestore::V1beta1::Write.new(
|
138
|
+
update: Google::Firestore::V1beta1::Document.new(
|
139
|
+
name: doc_path,
|
140
|
+
fields: hash_to_fields(data)),
|
141
|
+
current_document: Google::Firestore::V1beta1::Precondition.new(
|
142
|
+
exists: false)
|
143
|
+
)
|
144
|
+
writes << write
|
145
|
+
end
|
146
|
+
|
147
|
+
if server_time_paths.any?
|
148
|
+
transform_write = transform_write doc_path, server_time_paths
|
149
|
+
|
150
|
+
if data.empty?
|
151
|
+
transform_write.current_document = \
|
152
|
+
Google::Firestore::V1beta1::Precondition.new(exists: false)
|
153
|
+
end
|
154
|
+
|
155
|
+
writes << transform_write
|
156
|
+
end
|
157
|
+
|
158
|
+
writes
|
159
|
+
end
|
160
|
+
|
161
|
+
def writes_for_set doc_path, data, merge: nil
|
162
|
+
fail ArgumentError, "data is required" unless data.is_a? Hash
|
163
|
+
|
164
|
+
if merge
|
165
|
+
if merge == true
|
166
|
+
# extract the leaf node field paths from data
|
167
|
+
field_paths = identify_leaf_nodes data
|
168
|
+
else
|
169
|
+
field_paths = Array(merge).map do |field_path|
|
170
|
+
field_path = FieldPath.parse field_path unless field_path.is_a? FieldPath
|
171
|
+
field_path
|
172
|
+
end
|
173
|
+
end
|
174
|
+
return writes_for_set_merge doc_path, data, field_paths
|
175
|
+
end
|
176
|
+
|
177
|
+
writes = []
|
178
|
+
|
179
|
+
data, delete_paths = remove_field_value_from data, :delete
|
180
|
+
if delete_paths.any?
|
181
|
+
fail ArgumentError, "DELETE not allowed on set"
|
182
|
+
end
|
183
|
+
|
184
|
+
data, server_time_paths = remove_field_value_from data, :server_time
|
185
|
+
|
186
|
+
writes << Google::Firestore::V1beta1::Write.new(
|
187
|
+
update: Google::Firestore::V1beta1::Document.new(
|
188
|
+
name: doc_path,
|
189
|
+
fields: hash_to_fields(data))
|
190
|
+
)
|
191
|
+
|
192
|
+
if server_time_paths.any?
|
193
|
+
writes << transform_write(doc_path, server_time_paths)
|
194
|
+
end
|
195
|
+
|
196
|
+
writes
|
197
|
+
end
|
198
|
+
|
199
|
+
def writes_for_set_merge doc_path, data, field_paths
|
200
|
+
fail ArgumentError, "data is required" unless data.is_a? Hash
|
201
|
+
|
202
|
+
writes = []
|
203
|
+
|
204
|
+
# Ensure provided field paths are valid.
|
205
|
+
all_valid = identify_leaf_nodes data
|
206
|
+
all_valid_check = field_paths.map do |verify_path|
|
207
|
+
if all_valid.include?(verify_path)
|
208
|
+
true
|
209
|
+
else
|
210
|
+
found_in_all_valid = all_valid.select do |fp|
|
211
|
+
fp.formatted_string.start_with? "#{verify_path.formatted_string}."
|
212
|
+
end
|
213
|
+
found_in_all_valid.any?
|
214
|
+
end
|
215
|
+
end
|
216
|
+
all_valid_check = all_valid_check.include? false
|
217
|
+
fail ArgumentError, "all fields must be in data" if all_valid_check
|
218
|
+
|
219
|
+
data, delete_paths = remove_field_value_from data, :delete
|
220
|
+
data, server_time_paths = remove_field_value_from data, :server_time
|
221
|
+
|
222
|
+
delete_valid_check = delete_paths.map do |delete_path|
|
223
|
+
if field_paths.include?(delete_path)
|
224
|
+
true
|
225
|
+
else
|
226
|
+
found_in_field_paths = field_paths.select do |fp|
|
227
|
+
fp.formatted_string.start_with? "#{delete_path.formatted_string}."
|
228
|
+
end
|
229
|
+
found_in_field_paths.any?
|
230
|
+
end
|
231
|
+
end
|
232
|
+
delete_valid_check = delete_valid_check.include? false
|
233
|
+
fail ArgumentError, "deleted field not included in merge" if delete_valid_check
|
234
|
+
|
235
|
+
# Choose only the data there are field paths for
|
236
|
+
field_paths -= delete_paths
|
237
|
+
field_paths -= server_time_paths
|
238
|
+
data = select_by_field_paths data, field_paths
|
239
|
+
|
240
|
+
if data.empty?
|
241
|
+
if server_time_paths.empty?
|
242
|
+
fail ArgumentError, "data required for set with merge"
|
243
|
+
end
|
244
|
+
else
|
245
|
+
writes << Google::Firestore::V1beta1::Write.new(
|
246
|
+
update: Google::Firestore::V1beta1::Document.new(
|
247
|
+
name: doc_path,
|
248
|
+
fields: hash_to_fields(data)),
|
249
|
+
update_mask: Google::Firestore::V1beta1::DocumentMask.new(
|
250
|
+
field_paths: field_paths.map(&:formatted_string))
|
251
|
+
)
|
252
|
+
end
|
253
|
+
|
254
|
+
if server_time_paths.any?
|
255
|
+
writes << transform_write(doc_path, server_time_paths)
|
256
|
+
end
|
257
|
+
|
258
|
+
writes
|
259
|
+
end
|
260
|
+
|
261
|
+
def writes_for_update doc_path, data, update_time: nil
|
262
|
+
writes = []
|
263
|
+
|
264
|
+
fail ArgumentError, "data is required" unless data.is_a? Hash
|
265
|
+
|
266
|
+
# Convert data to use FieldPath
|
267
|
+
new_data_pairs = data.map do |key, value|
|
268
|
+
key = FieldPath.parse key unless key.is_a? FieldPath
|
269
|
+
[key, value]
|
270
|
+
end
|
271
|
+
|
272
|
+
# Duplicate field paths check
|
273
|
+
dup_keys = new_data_pairs.map(&:first).map(&:formatted_string)
|
274
|
+
if dup_keys.size != dup_keys.uniq.size
|
275
|
+
fail ArgumentError, "duplicate field paths"
|
276
|
+
end
|
277
|
+
dup_keys.each do |field_path|
|
278
|
+
prefix_check = dup_keys.select do |this_path|
|
279
|
+
this_path.start_with? "#{field_path}."
|
280
|
+
end
|
281
|
+
if prefix_check.any?
|
282
|
+
fail ArgumentError, "one field cannot be a prefix of another"
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
286
|
+
delete_paths, new_data_pairs = new_data_pairs.partition do |field_path, value|
|
287
|
+
value.is_a?(FieldValue) && value.type == :delete
|
288
|
+
end
|
289
|
+
|
290
|
+
root_server_time_paths, new_data_pairs = new_data_pairs.partition do |field_path, value|
|
291
|
+
value.is_a?(FieldValue) && value.type == :server_time
|
292
|
+
end
|
293
|
+
|
294
|
+
data = build_hash_from_field_paths_and_values new_data_pairs
|
295
|
+
field_paths = new_data_pairs.map(&:first)
|
296
|
+
|
297
|
+
delete_paths.map!(&:first)
|
298
|
+
root_server_time_paths.map!(&:first)
|
299
|
+
|
300
|
+
data, nested_deletes = remove_field_value_from data, :delete
|
301
|
+
fail ArgumentError, "DELETE cannot be nested" if nested_deletes.any?
|
302
|
+
|
303
|
+
data, nested_server_time_paths = remove_field_value_from data, :server_time
|
304
|
+
|
305
|
+
server_time_paths = root_server_time_paths + nested_server_time_paths
|
306
|
+
server_time_paths = root_server_time_paths + nested_server_time_paths
|
307
|
+
|
308
|
+
field_paths = (field_paths - (field_paths - identify_all_file_paths(data)) + delete_paths).uniq
|
309
|
+
field_paths.each do |field_path|
|
310
|
+
fail ArgumentError, "empty paths not allowed" if field_path.fields.empty?
|
311
|
+
end
|
312
|
+
|
313
|
+
if data.empty? && delete_paths.empty? && server_time_paths.empty?
|
314
|
+
fail ArgumentError, "data is required"
|
315
|
+
end
|
316
|
+
|
317
|
+
if data.any? || delete_paths.any?
|
318
|
+
write = Google::Firestore::V1beta1::Write.new(
|
319
|
+
update: Google::Firestore::V1beta1::Document.new(
|
320
|
+
name: doc_path,
|
321
|
+
fields: hash_to_fields(data)),
|
322
|
+
update_mask: Google::Firestore::V1beta1::DocumentMask.new(
|
323
|
+
field_paths: field_paths.map(&:formatted_string)),
|
324
|
+
current_document: Google::Firestore::V1beta1::Precondition.new(
|
325
|
+
exists: true)
|
326
|
+
)
|
327
|
+
if update_time
|
328
|
+
write.current_document = \
|
329
|
+
Google::Firestore::V1beta1::Precondition.new(
|
330
|
+
update_time: time_to_timestamp(update_time))
|
331
|
+
end
|
332
|
+
writes << write
|
333
|
+
end
|
334
|
+
|
335
|
+
if server_time_paths.any?
|
336
|
+
transform_write = transform_write doc_path, server_time_paths
|
337
|
+
if data.empty?
|
338
|
+
transform_write.current_document = \
|
339
|
+
Google::Firestore::V1beta1::Precondition.new(exists: true)
|
340
|
+
end
|
341
|
+
writes << transform_write
|
342
|
+
end
|
343
|
+
|
344
|
+
writes
|
345
|
+
end
|
346
|
+
|
347
|
+
def write_for_delete doc_path, exists: nil, update_time: nil
|
348
|
+
if !exists.nil? && !update_time.nil?
|
349
|
+
fail ArgumentError, "cannot specify both exists and update_time"
|
350
|
+
end
|
351
|
+
|
352
|
+
write = Google::Firestore::V1beta1::Write.new(
|
353
|
+
delete: doc_path
|
354
|
+
)
|
355
|
+
|
356
|
+
unless exists.nil? && update_time.nil?
|
357
|
+
write.current_document = \
|
358
|
+
Google::Firestore::V1beta1::Precondition.new({
|
359
|
+
exists: exists, update_time: time_to_timestamp(update_time)
|
360
|
+
}.delete_if { |_, v| v.nil? })
|
361
|
+
end
|
362
|
+
|
363
|
+
write
|
364
|
+
end
|
365
|
+
|
366
|
+
def is_field_value_nested obj, field_value_type
|
367
|
+
return true if obj.is_a?(FieldValue) && obj.type == field_value_type
|
368
|
+
|
369
|
+
if obj.is_a? Array
|
370
|
+
obj.each { |o| val = is_field_value_nested o, field_value_type; return true if val }
|
371
|
+
elsif obj.is_a? Hash
|
372
|
+
obj.each { |_k, v| val = is_field_value_nested v, field_value_type; return true if val }
|
373
|
+
end
|
374
|
+
false
|
375
|
+
end
|
376
|
+
|
377
|
+
def remove_field_value_from obj, field_value_type
|
378
|
+
return [nil, []] unless obj.is_a? Hash
|
379
|
+
|
380
|
+
paths = []
|
381
|
+
new_pairs = obj.map do |key, value|
|
382
|
+
if value.is_a?(FieldValue) && value.type == field_value_type
|
383
|
+
paths << [key]
|
384
|
+
nil # will be removed by calling compact
|
385
|
+
else
|
386
|
+
if value.is_a? Hash
|
387
|
+
unless value.empty?
|
388
|
+
nested_hash, nested_paths = remove_field_value_from value, field_value_type
|
389
|
+
if nested_paths.any?
|
390
|
+
nested_paths.each do |nested_path|
|
391
|
+
paths << (([key] + nested_path.fields).flatten)
|
392
|
+
end
|
393
|
+
end
|
394
|
+
if nested_hash.empty?
|
395
|
+
nil # will be removed by calling compact
|
396
|
+
else
|
397
|
+
[String(key), nested_hash]
|
398
|
+
end
|
399
|
+
else
|
400
|
+
[String(key), value]
|
401
|
+
end
|
402
|
+
else
|
403
|
+
if value.is_a? Array
|
404
|
+
if is_field_value_nested value, field_value_type
|
405
|
+
fail ArgumentError, "cannot nest #{field_value_type} under arrays"
|
406
|
+
end
|
407
|
+
end
|
408
|
+
|
409
|
+
[String(key), value]
|
410
|
+
end
|
411
|
+
end
|
412
|
+
end
|
413
|
+
|
414
|
+
paths.map! { |path| FieldPath.new *path }
|
415
|
+
|
416
|
+
# return a new hash and paths
|
417
|
+
[Hash[new_pairs.compact], paths]
|
418
|
+
end
|
419
|
+
|
420
|
+
def identify_leaf_nodes hash
|
421
|
+
paths = []
|
422
|
+
|
423
|
+
hash.map do |key, value|
|
424
|
+
if value.is_a? Hash
|
425
|
+
nested_paths = identify_leaf_nodes value
|
426
|
+
nested_paths.each do |nested_path|
|
427
|
+
paths << (([key] + nested_path.fields).flatten)
|
428
|
+
end
|
429
|
+
else
|
430
|
+
paths << [key]
|
431
|
+
end
|
432
|
+
end
|
433
|
+
|
434
|
+
paths.map { |path| FieldPath.new *path }
|
435
|
+
end
|
436
|
+
|
437
|
+
def identify_all_file_paths hash
|
438
|
+
paths = []
|
439
|
+
|
440
|
+
hash.map do |key, value|
|
441
|
+
paths << [key]
|
442
|
+
|
443
|
+
if value.is_a? Hash
|
444
|
+
nested_paths = identify_all_file_paths value
|
445
|
+
nested_paths.each do |nested_path|
|
446
|
+
paths << (([key] + nested_path.fields).flatten)
|
447
|
+
end
|
448
|
+
end
|
449
|
+
end
|
450
|
+
|
451
|
+
paths.map { |path| FieldPath.new *path }
|
452
|
+
end
|
453
|
+
|
454
|
+
def select_by_field_paths hash, field_paths
|
455
|
+
new_hash = {}
|
456
|
+
field_paths.map do |field_path|
|
457
|
+
selected_hash = select_field_path hash, field_path
|
458
|
+
deep_merge_hashes new_hash, selected_hash
|
459
|
+
end
|
460
|
+
new_hash
|
461
|
+
end
|
462
|
+
|
463
|
+
def select_field_path hash, field_path
|
464
|
+
ret_hash = {}
|
465
|
+
tmp_hash = ret_hash
|
466
|
+
prev_hash = ret_hash
|
467
|
+
dup_hash = hash.dup
|
468
|
+
fields = field_path.fields
|
469
|
+
last_field = nil
|
470
|
+
|
471
|
+
# squash fields until the key exists?
|
472
|
+
until dup_hash.key? fields.first
|
473
|
+
fields.unshift "#{fields.shift}.#{fields.shift}"
|
474
|
+
break if fields.count <= 1
|
475
|
+
end
|
476
|
+
|
477
|
+
fields.each do |field|
|
478
|
+
prev_hash[last_field] = tmp_hash unless last_field.nil?
|
479
|
+
last_field = field
|
480
|
+
tmp_hash[field] = {}
|
481
|
+
prev_hash = tmp_hash
|
482
|
+
tmp_hash = tmp_hash[field]
|
483
|
+
dup_hash = dup_hash[field]
|
484
|
+
end
|
485
|
+
prev_hash[last_field] = dup_hash
|
486
|
+
ret_hash
|
487
|
+
end
|
488
|
+
|
489
|
+
def deep_merge_hashes left_hash, right_hash
|
490
|
+
right_hash.each_pair do |key, right_value|
|
491
|
+
left_value = left_hash[key]
|
492
|
+
|
493
|
+
if left_value.is_a?(Hash) && right_value.is_a?(Hash)
|
494
|
+
left_hash[key] = deep_merge_hashes left_value, right_value
|
495
|
+
else
|
496
|
+
left_hash[key] = right_value
|
497
|
+
end
|
498
|
+
end
|
499
|
+
|
500
|
+
left_hash
|
501
|
+
end
|
502
|
+
|
503
|
+
START_FIELD_PATH_CHARS = /\A[a-zA-Z_]/
|
504
|
+
INVALID_FIELD_PATH_CHARS = /[\~\*\/\[\]]/
|
505
|
+
ESCAPED_FIELD_PATH = /\A\`(.*)\`\z/
|
506
|
+
|
507
|
+
def build_hash_from_field_paths_and_values pairs
|
508
|
+
pairs.each do |field_path, _value|
|
509
|
+
raise ArgumentError unless field_path.is_a? FieldPath
|
510
|
+
end
|
511
|
+
|
512
|
+
dup_hash = {}
|
513
|
+
|
514
|
+
pairs.each do |field_path, value|
|
515
|
+
tmp_dup = dup_hash
|
516
|
+
last_field = nil
|
517
|
+
field_path.fields.map(&:to_sym).each do |field|
|
518
|
+
fail ArgumentError, "empty paths not allowed" if field.empty?
|
519
|
+
tmp_dup = tmp_dup[last_field] unless last_field.nil?
|
520
|
+
last_field = field
|
521
|
+
tmp_dup[field] ||= {}
|
522
|
+
end
|
523
|
+
tmp_dup[last_field] = value
|
524
|
+
end
|
525
|
+
|
526
|
+
dup_hash
|
527
|
+
end
|
528
|
+
|
529
|
+
def escape_field_path str
|
530
|
+
str = String str
|
531
|
+
|
532
|
+
return "`#{str}`" if INVALID_FIELD_PATH_CHARS.match str
|
533
|
+
return "`#{str}`" if str["."] # contains "."
|
534
|
+
return str if START_FIELD_PATH_CHARS.match str
|
535
|
+
|
536
|
+
"`#{str}`"
|
537
|
+
end
|
538
|
+
|
539
|
+
def transform_write doc_path, paths, server_value: :REQUEST_TIME
|
540
|
+
field_transforms = paths.map do |path|
|
541
|
+
Google::Firestore::V1beta1::DocumentTransform::FieldTransform.new(
|
542
|
+
field_path: path.formatted_string,
|
543
|
+
set_to_server_value: server_value
|
544
|
+
)
|
545
|
+
end
|
546
|
+
|
547
|
+
Google::Firestore::V1beta1::Write.new(
|
548
|
+
transform: Google::Firestore::V1beta1::DocumentTransform.new(
|
549
|
+
document: doc_path,
|
550
|
+
field_transforms: field_transforms
|
551
|
+
)
|
552
|
+
)
|
553
|
+
end
|
554
|
+
end
|
555
|
+
# rubocop:enable all
|
556
|
+
|
557
|
+
extend ClassMethods
|
558
|
+
end
|
559
|
+
end
|
560
|
+
end
|
561
|
+
end
|