google-cloud-firestore 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|