parse-stack-next 5.7.6 → 5.8.1
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 +4 -4
- data/CHANGELOG.md +1148 -0
- data/README.md +39 -23
- data/docs/TEST_SERVER.md +2 -2
- data/docs/acl_clp_guide.md +7 -0
- data/docs/atlas_vector_search_guide.md +181 -13
- data/docs/caching.md +50 -0
- data/docs/client_sdk_guide.md +11 -0
- data/docs/mcp_guide.md +332 -9
- data/docs/mongodb_direct_guide.md +27 -0
- data/docs/usage_guide.md +38 -0
- data/docs/webhooks_guide.md +126 -17
- data/lib/parse/acl_scope.rb +230 -50
- data/lib/parse/agent/approval_gate.rb +0 -0
- data/lib/parse/agent/constraint_translator.rb +42 -15
- data/lib/parse/agent/describe.rb +11 -3
- data/lib/parse/agent/field_names.rb +53 -0
- data/lib/parse/agent/field_policy.rb +74 -0
- data/lib/parse/agent/mcp_deployments.rb +426 -0
- data/lib/parse/agent/mcp_dispatcher.rb +8 -1
- data/lib/parse/agent/mcp_rack_app.rb +424 -45
- data/lib/parse/agent/mcp_server.rb +23 -1
- data/lib/parse/agent/mcp_subscriptions.rb +124 -6
- data/lib/parse/agent/metadata_registry.rb +67 -8
- data/lib/parse/agent/prompt_hardening.rb +9 -3
- data/lib/parse/agent/tools.rb +378 -29
- data/lib/parse/agent.rb +298 -21
- data/lib/parse/api/batch.rb +10 -1
- data/lib/parse/api/schema.rb +23 -4
- data/lib/parse/api/sessions.rb +6 -2
- data/lib/parse/api/users.rb +88 -14
- data/lib/parse/atlas_search/index_manager.rb +5 -1
- data/lib/parse/atlas_search/protected_paths.rb +236 -0
- data/lib/parse/atlas_search.rb +144 -24
- data/lib/parse/authorization.rb +289 -4
- data/lib/parse/cache/sub_cache.rb +26 -0
- data/lib/parse/client/authentication.rb +19 -1
- data/lib/parse/client/batch.rb +264 -35
- data/lib/parse/client/body_builder.rb +21 -0
- data/lib/parse/client/caching.rb +371 -27
- data/lib/parse/client/request.rb +26 -14
- data/lib/parse/client/response.rb +49 -6
- data/lib/parse/client.rb +227 -40
- data/lib/parse/clp_scope.rb +317 -25
- data/lib/parse/console.rb +2 -2
- data/lib/parse/embeddings/voyage.rb +181 -17
- data/lib/parse/graphql/type_generator.rb +3 -0
- data/lib/parse/live_query/client.rb +72 -2
- data/lib/parse/lock_backend.rb +4 -3
- data/lib/parse/model/acl.rb +119 -21
- data/lib/parse/model/associations/belongs_to.rb +25 -3
- data/lib/parse/model/associations/collection_proxy.rb +146 -17
- data/lib/parse/model/associations/has_many.rb +49 -9
- data/lib/parse/model/associations/has_one.rb +3 -1
- data/lib/parse/model/associations/pointer_collection_proxy.rb +145 -17
- data/lib/parse/model/associations/relation_collection_proxy.rb +229 -38
- data/lib/parse/model/bytes.rb +13 -5
- data/lib/parse/model/classes/role.rb +95 -0
- data/lib/parse/model/classes/session.rb +304 -1
- data/lib/parse/model/classes/user.rb +104 -8
- data/lib/parse/model/core/actions.rb +287 -70
- data/lib/parse/model/core/builder.rb +100 -8
- data/lib/parse/model/core/create_lock.rb +27 -2
- data/lib/parse/model/core/describe.rb +2 -0
- data/lib/parse/model/core/fetching.rb +50 -5
- data/lib/parse/model/core/field_guards.rb +16 -8
- data/lib/parse/model/core/pluralized_aliases.rb +8 -4
- data/lib/parse/model/core/properties.rb +488 -39
- data/lib/parse/model/core/querying.rb +7 -0
- data/lib/parse/model/core/schema.rb +5 -3
- data/lib/parse/model/core/search_indexing.rb +63 -0
- data/lib/parse/model/core/vector_searchable.rb +35 -6
- data/lib/parse/model/file.rb +9 -2
- data/lib/parse/model/geopoint.rb +61 -13
- data/lib/parse/model/model.rb +160 -9
- data/lib/parse/model/object.rb +317 -18
- data/lib/parse/model/phone.rb +54 -5
- data/lib/parse/model/pointer.rb +40 -6
- data/lib/parse/model/push.rb +11 -1
- data/lib/parse/mongodb.rb +189 -61
- data/lib/parse/pipeline_security.rb +415 -26
- data/lib/parse/query/constraint.rb +31 -1
- data/lib/parse/query/constraints.rb +89 -59
- data/lib/parse/query/cursor.rb +3 -1
- data/lib/parse/query/operation.rb +62 -8
- data/lib/parse/query/ordering.rb +34 -6
- data/lib/parse/query.rb +1871 -167
- data/lib/parse/retrieval/agent_tool.rb +225 -8
- data/lib/parse/retrieval/benchmark.rb +149 -0
- data/lib/parse/retrieval/profiles.rb +320 -0
- data/lib/parse/retrieval/retriever.rb +10 -1
- data/lib/parse/retrieval.rb +2 -0
- data/lib/parse/schema/search_index_migrator.rb +23 -5
- data/lib/parse/schema.rb +74 -18
- data/lib/parse/stack/tasks.rb +6 -4
- data/lib/parse/stack/version.rb +1 -1
- data/lib/parse/stack.rb +91 -14
- data/lib/parse/two_factor_auth/user_extension.rb +14 -2
- data/lib/parse/two_factor_auth.rb +11 -0
- data/lib/parse/vector_search/hybrid.rb +110 -33
- data/lib/parse/vector_search/index_definition.rb +237 -0
- data/lib/parse/vector_search.rb +259 -23
- data/lib/parse/webhooks/payload.rb +136 -6
- data/lib/parse/webhooks/replay_protection.rb +58 -20
- data/lib/parse/webhooks.rb +715 -40
- metadata +8 -1
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
4
|
require "digest"
|
|
5
|
+
require "securerandom"
|
|
5
6
|
require "set"
|
|
6
7
|
|
|
7
8
|
module Parse
|
|
@@ -163,6 +164,25 @@ module Parse
|
|
|
163
164
|
value
|
|
164
165
|
end
|
|
165
166
|
|
|
167
|
+
# A random marker replaced on every identity invalidation, read by
|
|
168
|
+
# `Parse::Authorization` before and after it caches a token so an
|
|
169
|
+
# invalidation from any process in between is detected. A nonce rather
|
|
170
|
+
# than a counter: a plane clear drops it, and a counter restarting at
|
|
171
|
+
# 0 could repeat a value an in-flight lookup captured.
|
|
172
|
+
# @return [String, nil] nil when never set or cleared.
|
|
173
|
+
def invalidation_nonce
|
|
174
|
+
value = @store[invalidation_nonce_key]
|
|
175
|
+
value.nil? ? nil : value.to_s
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Replace the invalidation nonce.
|
|
179
|
+
# @return [String] the new nonce.
|
|
180
|
+
def bump_invalidation_nonce
|
|
181
|
+
nonce = SecureRandom.hex(16)
|
|
182
|
+
@store.store(invalidation_nonce_key, nonce, {})
|
|
183
|
+
nonce
|
|
184
|
+
end
|
|
185
|
+
|
|
166
186
|
# @return [Float] the last invalidation epoch, 0.0 when never set.
|
|
167
187
|
def epoch
|
|
168
188
|
@store[epoch_key].to_f
|
|
@@ -253,6 +273,12 @@ module Parse
|
|
|
253
273
|
@keyspace.key(@family, "meta", "epoch")
|
|
254
274
|
end
|
|
255
275
|
|
|
276
|
+
# Sits beside the epoch under the reserved `meta` segment; a token key
|
|
277
|
+
# is a digest, so no token can name it.
|
|
278
|
+
def invalidation_nonce_key
|
|
279
|
+
@keyspace.key(@family, "meta", "invalidation")
|
|
280
|
+
end
|
|
281
|
+
|
|
256
282
|
# Generations live in the same family so a plane clear takes them with it,
|
|
257
283
|
# and are namespaced under `gen:` so they cannot collide with an entry
|
|
258
284
|
# whose logical key happens to be a user id.
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
4
|
require "faraday"
|
|
5
|
+
require "securerandom"
|
|
5
6
|
require "active_support"
|
|
6
7
|
require "active_support/core_ext"
|
|
7
8
|
|
|
@@ -15,6 +16,18 @@ module Parse
|
|
|
15
16
|
include Parse::Protocol
|
|
16
17
|
# @!visibility private
|
|
17
18
|
DISABLE_MASTER_KEY = "X-Disable-Parse-Master-Key".freeze
|
|
19
|
+
# SDK-internal marker for a request that reads metadata (a class
|
|
20
|
+
# schema, the role graph) while enforcing a scope. Such a request keeps
|
|
21
|
+
# the master key inside {Parse.without_master_key}, which governs row
|
|
22
|
+
# access. The header carries a random per-process token and is removed
|
|
23
|
+
# before the request is logged or sent, so a header copied from user
|
|
24
|
+
# input cannot set it. {Parse::Client#request} adds it only for the
|
|
25
|
+
# `metadata_master:` request option set to
|
|
26
|
+
# {Parse::Client::METADATA_MASTER_REQUEST}.
|
|
27
|
+
# @!visibility private
|
|
28
|
+
METADATA_MASTER = "X-Parse-Stack-Metadata-Master".freeze
|
|
29
|
+
# @!visibility private
|
|
30
|
+
METADATA_MASTER_TOKEN = SecureRandom.hex(32).freeze
|
|
18
31
|
# @return [String] the application id for this Parse endpoint.
|
|
19
32
|
attr_accessor :application_id
|
|
20
33
|
# @return [String] the REST API Key for this Parse endpoint.
|
|
@@ -67,7 +80,12 @@ module Parse
|
|
|
67
80
|
# 3. A session-token-authenticated request (the existing check
|
|
68
81
|
# below; session token wins over master key).
|
|
69
82
|
header_disable = env[:request_headers][DISABLE_MASTER_KEY].present?
|
|
70
|
-
|
|
83
|
+
# An SDK metadata read keeps the master key inside the block. Always
|
|
84
|
+
# remove the marker so it never reaches the logger or the server.
|
|
85
|
+
metadata_value = env[:request_headers].delete(METADATA_MASTER)
|
|
86
|
+
metadata_master = metadata_value.is_a?(String) &&
|
|
87
|
+
ActiveSupport::SecurityUtils.secure_compare(metadata_value, METADATA_MASTER_TOKEN)
|
|
88
|
+
fiber_disable = Parse.master_key_disabled? && !metadata_master
|
|
71
89
|
unless @master_key.blank? || header_disable || fiber_disable
|
|
72
90
|
headers[MASTER_KEY] = @master_key
|
|
73
91
|
end
|
data/lib/parse/client/batch.rb
CHANGED
|
@@ -70,12 +70,19 @@ module Parse
|
|
|
70
70
|
def initialize(reqs = nil, transaction: false)
|
|
71
71
|
@requests = []
|
|
72
72
|
@responses = []
|
|
73
|
+
@submitted = false
|
|
73
74
|
@transaction = transaction
|
|
74
75
|
reqs = [reqs] unless reqs.is_a?(Enumerable)
|
|
75
76
|
reqs.each { |r| add(r) } if reqs.is_a?(Enumerable)
|
|
76
77
|
end
|
|
77
78
|
|
|
78
79
|
# Add an additional request to this batch.
|
|
80
|
+
#
|
|
81
|
+
# A request tagged to a Parse object (see {Parse::Request#tag}) is skipped
|
|
82
|
+
# when the batch already holds an identical request for that same object,
|
|
83
|
+
# so adding an object twice does not send its changes twice. Untagged
|
|
84
|
+
# requests are always kept: two identical raw requests (for example two
|
|
85
|
+
# Increment operations) are two writes and are both sent.
|
|
79
86
|
# @overload add(req)
|
|
80
87
|
# @param req [Parse::Request] the request to append.
|
|
81
88
|
# @return [Array<Parse::Request>] the set of requests.
|
|
@@ -83,16 +90,19 @@ module Parse
|
|
|
83
90
|
# @param req [Parse::BatchOperation] add all the requests from this batch operation.
|
|
84
91
|
# @return [Array<Parse::Request>] the set of requests.
|
|
85
92
|
def add(req)
|
|
86
|
-
if req.
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
93
|
+
incoming = if req.is_a?(BatchOperation)
|
|
94
|
+
req.requests
|
|
95
|
+
elsif req.respond_to?(:change_requests)
|
|
96
|
+
req.change_requests
|
|
97
|
+
elsif req.is_a?(Array)
|
|
98
|
+
req
|
|
99
|
+
else
|
|
100
|
+
[req]
|
|
101
|
+
end
|
|
102
|
+
incoming.each do |r|
|
|
103
|
+
next unless r.is_a?(Parse::Request)
|
|
104
|
+
next if duplicate_object_request?(r)
|
|
105
|
+
@requests.push(r)
|
|
96
106
|
end
|
|
97
107
|
@requests
|
|
98
108
|
end
|
|
@@ -127,13 +137,15 @@ module Parse
|
|
|
127
137
|
@requests.clear
|
|
128
138
|
end
|
|
129
139
|
|
|
130
|
-
# @return [Boolean] true if the
|
|
140
|
+
# @return [Boolean] true if every response in the batch succeeded. A
|
|
141
|
+
# batch that was submitted with no requests is successful; one that has
|
|
142
|
+
# not been submitted is not.
|
|
131
143
|
def success?
|
|
132
|
-
return
|
|
133
|
-
@responses.
|
|
144
|
+
return @submitted == true if @responses.empty?
|
|
145
|
+
@responses.all? { |r| r.respond_to?(:success?) && r.success? }
|
|
134
146
|
end
|
|
135
147
|
|
|
136
|
-
# @return [Boolean] true if
|
|
148
|
+
# @return [Boolean] true if at least one response in the batch failed.
|
|
137
149
|
def error?
|
|
138
150
|
return false if @responses.empty?
|
|
139
151
|
!success?
|
|
@@ -143,57 +155,202 @@ module Parse
|
|
|
143
155
|
# Parse limits requests in each batch to 50 and it is possible that a {BatchOperation}
|
|
144
156
|
# instance contains more than 50 requests. This method will slice up the array of
|
|
145
157
|
# request and send them based on the `segment` amount until they have all been submitted.
|
|
158
|
+
#
|
|
159
|
+
# The returned array always has one response per request, in request
|
|
160
|
+
# order. When a chunk fails as a whole (an HTTP error, or a response that
|
|
161
|
+
# cannot be matched to its requests), every request in that chunk gets a
|
|
162
|
+
# failed response, so a failure never shifts results onto other requests.
|
|
163
|
+
#
|
|
164
|
+
# A transactional batch (`transaction: true`) is never split. All of its
|
|
165
|
+
# requests go to Parse Server in one `POST /batch` with `transaction: true`,
|
|
166
|
+
# which commits or rolls back as a unit, regardless of `segment`. Parse
|
|
167
|
+
# Server has no fixed sub-request limit; a server configured with
|
|
168
|
+
# `requestComplexity.batchRequestLimit` rejects an oversized transaction
|
|
169
|
+
# for non-master callers, and nothing is written.
|
|
170
|
+
#
|
|
171
|
+
# When a chunk raises (for example a 5xx or a dropped connection), the
|
|
172
|
+
# other chunks are still processed and the block still sees every
|
|
173
|
+
# request, with failed responses for the chunk that raised. The first
|
|
174
|
+
# exception is then re-raised.
|
|
146
175
|
# @param segment [Integer] the number of requests to send in each batch. Default 50.
|
|
147
176
|
# @param parallelism [Integer] the number of segments dispatched in
|
|
148
177
|
# parallel. Defaults to `Parse::BatchOperation.parallelism` (2).
|
|
178
|
+
# @yieldparam request [Parse::Request] a submitted request.
|
|
179
|
+
# @yieldparam response [Parse::Response] the response for that request.
|
|
149
180
|
# @return [Array<Parse::Response>] the corresponding set of responses for
|
|
150
181
|
# each request in the batch.
|
|
151
182
|
def submit(segment = 50, parallelism: self.class.parallelism, &block)
|
|
152
183
|
@responses = []
|
|
153
|
-
@
|
|
154
|
-
|
|
155
|
-
@responses
|
|
156
|
-
|
|
184
|
+
@submitted = true
|
|
185
|
+
failure = nil
|
|
186
|
+
return @responses if @requests.empty?
|
|
187
|
+
|
|
188
|
+
if @transaction
|
|
189
|
+
# One request, one transaction. Exceptions propagate unchanged so the
|
|
190
|
+
# caller can roll back its local state.
|
|
191
|
+
@responses = align_responses(@requests, client.batch_request(self))
|
|
192
|
+
else
|
|
193
|
+
segment = 50 if segment.nil? || segment < 1
|
|
194
|
+
parallelism = 1 if parallelism.nil? || parallelism < 1
|
|
195
|
+
slices = @requests.each_slice(segment).to_a
|
|
196
|
+
outcomes = slices.threaded_map(parallelism) do |slice|
|
|
197
|
+
begin
|
|
198
|
+
[align_responses(slice, client.batch_request(BatchOperation.new(slice))), nil]
|
|
199
|
+
rescue StandardError => e
|
|
200
|
+
[Array.new(slice.size) { exception_response(e) }, e]
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
@responses = outcomes.flat_map(&:first)
|
|
204
|
+
failure = outcomes.map(&:last).compact.first
|
|
157
205
|
end
|
|
158
|
-
|
|
206
|
+
|
|
159
207
|
@requests.zip(@responses).each(&block) if block_given?
|
|
208
|
+
raise failure if failure
|
|
160
209
|
@responses
|
|
161
210
|
end
|
|
162
211
|
|
|
163
212
|
alias_method :save, :submit
|
|
213
|
+
|
|
214
|
+
private
|
|
215
|
+
|
|
216
|
+
# Whether `req` repeats a request already in the batch for the same
|
|
217
|
+
# tagged object.
|
|
218
|
+
def duplicate_object_request?(req)
|
|
219
|
+
tag = req.tag
|
|
220
|
+
return false if tag.nil? || tag == 0
|
|
221
|
+
sig = req.signature
|
|
222
|
+
@requests.any? { |r| r.tag == tag && r.signature == sig }
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Pair a chunk's requests with the result of its batch call, one
|
|
226
|
+
# response per request.
|
|
227
|
+
# @param slice [Array<Parse::Request>] the requests that were sent.
|
|
228
|
+
# @param result [Array<Parse::Response>, Parse::Response] the result of
|
|
229
|
+
# {Parse::API::Batch#batch_request}.
|
|
230
|
+
# @return [Array<Parse::Response>]
|
|
231
|
+
def align_responses(slice, result)
|
|
232
|
+
if result.is_a?(Array)
|
|
233
|
+
slice.each_with_index.map do |_req, i|
|
|
234
|
+
entry = result[i]
|
|
235
|
+
next entry if entry.is_a?(Parse::Response)
|
|
236
|
+
Parse::Response.error_response(
|
|
237
|
+
Parse::Response::ERROR_INTERNAL,
|
|
238
|
+
"Batch response had #{result.size} results for #{slice.size} requests",
|
|
239
|
+
)
|
|
240
|
+
end
|
|
241
|
+
elsif result.is_a?(Parse::Response) && result.error?
|
|
242
|
+
slice.map do
|
|
243
|
+
Parse::Response.error_response(result.code, result.error, http_status: result.http_status)
|
|
244
|
+
end
|
|
245
|
+
else
|
|
246
|
+
slice.map do
|
|
247
|
+
Parse::Response.error_response(Parse::Response::ERROR_INTERNAL, "Malformed batch response")
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# A failed response standing in for a request whose chunk raised.
|
|
253
|
+
def exception_response(error)
|
|
254
|
+
code = Parse::Response::ERROR_INTERNAL
|
|
255
|
+
inner = error.respond_to?(:response) ? error.response : nil
|
|
256
|
+
code = inner.code if inner.respond_to?(:code) && inner.code.is_a?(Integer)
|
|
257
|
+
Parse::Response.error_response(code, "#{error.class}: #{error.message}")
|
|
258
|
+
end
|
|
164
259
|
end
|
|
165
260
|
end
|
|
166
261
|
|
|
167
262
|
class Array
|
|
168
263
|
|
|
169
264
|
# Submit a batch request for deleting a set of Parse::Objects.
|
|
265
|
+
#
|
|
266
|
+
# Each object whose delete succeeds has its local state updated the same
|
|
267
|
+
# way {Parse::Object#destroy} updates it. Objects whose delete fails are
|
|
268
|
+
# left untouched; inspect the returned batch's responses to find them.
|
|
269
|
+
# Destroy callbacks are not run. Every {Parse::Session} and {Parse::User}
|
|
270
|
+
# whose delete succeeded, or reported "object not found" (the row is
|
|
271
|
+
# already gone, for example revoked elsewhere), is dropped from its
|
|
272
|
+
# client's identity plane, as their single-object destroy does.
|
|
170
273
|
# @example
|
|
171
274
|
# # assume Post and Author are Parse models
|
|
172
275
|
# author = Author.first
|
|
173
276
|
# posts = Post.all author: author
|
|
174
277
|
# posts.destroy # batch destroy request
|
|
175
278
|
# @return [Parse::BatchOperation] the batch operation performed.
|
|
279
|
+
# @raise ArgumentError if the array is not empty and holds no Parse objects.
|
|
176
280
|
# @see Parse::BatchOperation
|
|
177
281
|
def destroy
|
|
282
|
+
targets = select { |o| o.respond_to?(:destroy_request) }
|
|
283
|
+
if targets.empty? && !empty?
|
|
284
|
+
raise ArgumentError, "Array#destroy requires Parse::Object elements; " \
|
|
285
|
+
"this array holds none (#{first.class})"
|
|
286
|
+
end
|
|
287
|
+
# A session deleted from a pointer or a partial fetch carries no token or
|
|
288
|
+
# owner to drop from the identity cache; look them up first, in one
|
|
289
|
+
# query per client.
|
|
290
|
+
Parse::Session.send(:_preload_identity_for_destroy!, targets) if defined?(Parse::Session)
|
|
291
|
+
_destroy_batch(targets)
|
|
292
|
+
ensure
|
|
293
|
+
# A looked-up session token is a live credential; never leave it on an
|
|
294
|
+
# object whose delete was skipped or whose batch raised.
|
|
295
|
+
targets&.each { |o| o.send(:_clear_identity_for_destroy!) if _batch_identity_hook?(o, :_clear_identity_for_destroy!) }
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
# @!visibility private
|
|
299
|
+
def _destroy_batch(targets)
|
|
178
300
|
batch = Parse::BatchOperation.new
|
|
179
|
-
|
|
180
|
-
|
|
301
|
+
objects = {}
|
|
302
|
+
targets.each do |o|
|
|
303
|
+
next if objects.key?(o.object_id)
|
|
181
304
|
r = o.destroy_request
|
|
182
|
-
|
|
305
|
+
next if r.nil?
|
|
306
|
+
objects[o.object_id] = o
|
|
307
|
+
batch.add(r)
|
|
308
|
+
end
|
|
309
|
+
batch.submit do |request, response|
|
|
310
|
+
o = objects[request.tag]
|
|
311
|
+
next unless o
|
|
312
|
+
# Sessions and users drop their identity-plane entries when the delete
|
|
313
|
+
# applied or the row is already gone ("object not found"), so a revoked
|
|
314
|
+
# token stops resolving now. They decide from the response; a denied
|
|
315
|
+
# delete leaves the cache alone.
|
|
316
|
+
o.send(:_after_batch_destroy, response) if _batch_identity_hook?(o, :_after_batch_destroy)
|
|
317
|
+
next unless response.respond_to?(:success?) && response.success?
|
|
318
|
+
# Mirror Parse::Object#destroy: keep the id and mark the object
|
|
319
|
+
# destroyed so it reports `destroyed?` and a later save refuses.
|
|
320
|
+
o.instance_variable_set(:@_destroyed, true)
|
|
321
|
+
o.changes_applied! if o.respond_to?(:changes_applied!)
|
|
183
322
|
end
|
|
184
|
-
batch.submit
|
|
185
323
|
batch
|
|
186
324
|
end
|
|
325
|
+
private :_destroy_batch
|
|
326
|
+
|
|
327
|
+
# Whether `o`'s class defines the internal (non-public) identity hook
|
|
328
|
+
# `name`. Checked on the class rather than with `respond_to?(name, true)`
|
|
329
|
+
# so a duck-typed element overriding `respond_to?` cannot break the batch.
|
|
330
|
+
# @!visibility private
|
|
331
|
+
def _batch_identity_hook?(o, name)
|
|
332
|
+
klass = o.class
|
|
333
|
+
klass.private_method_defined?(name) || klass.method_defined?(name)
|
|
334
|
+
end
|
|
335
|
+
private :_batch_identity_hook?
|
|
187
336
|
|
|
188
337
|
# Do not alias method as :delete is already part of array.
|
|
189
338
|
# alias_method :delete, :destroy
|
|
190
339
|
|
|
191
|
-
# Submit a batch request for
|
|
340
|
+
# Submit a batch request for saving a set of Parse::Objects.
|
|
192
341
|
# Batch requests are supported implicitly and intelligently through an
|
|
193
342
|
# extension of array. When an array of Parse::Object subclasses is saved,
|
|
194
343
|
# Parse-Stack will batch all possible save operations for the objects in the
|
|
195
344
|
# array that have changed. It will also batch save 50 at a time until all items
|
|
196
345
|
# in the array are saved. Note: Parse does not allow batch saving Parse::User objects.
|
|
346
|
+
#
|
|
347
|
+
# Each object is updated only from its own responses. An object whose
|
|
348
|
+
# requests all succeed gets its id and timestamps and has its changes
|
|
349
|
+
# cleared. When an object has several requests (an attribute update plus
|
|
350
|
+
# relation updates) and only some succeed, the fields written by the
|
|
351
|
+
# successful requests are cleared and the rest stay dirty, so a later save
|
|
352
|
+
# resends only what failed. An object listed more than once is saved once.
|
|
353
|
+
# Elements that are not Parse objects are skipped.
|
|
197
354
|
# @note The objects of the array to be saved do not all have to be of the same collection.
|
|
198
355
|
# @param merge [Boolean] whether to merge the updated changes to the series of
|
|
199
356
|
# objects back to the original ones submitted. If you don't need the original objects
|
|
@@ -206,12 +363,18 @@ class Array
|
|
|
206
363
|
# posts.each { |post| post.author = author }
|
|
207
364
|
# posts.save # batch save
|
|
208
365
|
# @return [Parse::BatchOperation] the batch operation performed.
|
|
366
|
+
# @raise ArgumentError if the array is not empty and holds no Parse objects.
|
|
209
367
|
# @see Parse::BatchOperation
|
|
210
368
|
def save(merge: true, force: false)
|
|
369
|
+
targets = select { |o| o.is_a?(Parse::Object) }
|
|
370
|
+
if targets.empty? && !empty?
|
|
371
|
+
raise ArgumentError, "Array#save requires Parse::Object elements; " \
|
|
372
|
+
"this array holds none (#{first.class})"
|
|
373
|
+
end
|
|
211
374
|
batch = Parse::BatchOperation.new
|
|
212
375
|
objects = {}
|
|
213
|
-
each do |o|
|
|
214
|
-
next
|
|
376
|
+
targets.each do |o|
|
|
377
|
+
next if objects.key?(o.object_id)
|
|
215
378
|
objects[o.object_id] = o
|
|
216
379
|
batch.add o.change_requests(force)
|
|
217
380
|
end
|
|
@@ -219,16 +382,82 @@ class Array
|
|
|
219
382
|
batch.submit
|
|
220
383
|
return batch
|
|
221
384
|
end
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
385
|
+
outcomes = Hash.new { |h, k| h[k] = [] }
|
|
386
|
+
begin
|
|
387
|
+
batch.submit do |request, response|
|
|
388
|
+
outcomes[request.tag] << [request, response] if objects.key?(request.tag)
|
|
389
|
+
end
|
|
390
|
+
ensure
|
|
391
|
+
# Apply what landed even when a chunk raised, so a successful create is
|
|
392
|
+
# never left looking new (which would create it again on the next save).
|
|
393
|
+
outcomes.each do |tag, pairs|
|
|
394
|
+
Parse::BatchOperation.apply_save_outcome(objects[tag], pairs)
|
|
395
|
+
end
|
|
231
396
|
end
|
|
232
397
|
batch
|
|
233
398
|
end #save!
|
|
234
399
|
end
|
|
400
|
+
|
|
401
|
+
module Parse
|
|
402
|
+
class BatchOperation
|
|
403
|
+
# @!visibility private
|
|
404
|
+
# Apply the batch responses for one object's requests to that object.
|
|
405
|
+
# @param obj [Parse::Object] the object that was saved.
|
|
406
|
+
# @param pairs [Array<Array(Parse::Request, Parse::Response)>] its
|
|
407
|
+
# requests and their responses.
|
|
408
|
+
def self.apply_save_outcome(obj, pairs)
|
|
409
|
+
return unless obj.is_a?(Parse::Object)
|
|
410
|
+
ok, failed = pairs.partition { |_req, res| res.respond_to?(:success?) && res.success? }
|
|
411
|
+
|
|
412
|
+
ok.each do |_req, res|
|
|
413
|
+
result = res.result
|
|
414
|
+
next unless result.is_a?(Hash)
|
|
415
|
+
if obj.id.blank? && result[Parse::Model::OBJECT_ID].present?
|
|
416
|
+
obj.instance_variable_set(:@id, result[Parse::Model::OBJECT_ID])
|
|
417
|
+
end
|
|
418
|
+
created = result["createdAt"]
|
|
419
|
+
updated = result["updatedAt"] || created
|
|
420
|
+
obj.instance_variable_set(:@created_at, Parse::Date.parse(created)) if created
|
|
421
|
+
obj.instance_variable_set(:@updated_at, Parse::Date.parse(updated)) if updated
|
|
422
|
+
# beforeSave triggers can change saved fields; apply what came back.
|
|
423
|
+
obj.set_attributes!(result)
|
|
424
|
+
end
|
|
425
|
+
return if ok.empty?
|
|
426
|
+
|
|
427
|
+
if failed.empty?
|
|
428
|
+
obj.changes_applied!
|
|
429
|
+
# Settle array proxies explicitly as well: their dirty state lives on
|
|
430
|
+
# the proxy, and a proxy left dirty resends the whole array on the
|
|
431
|
+
# next save.
|
|
432
|
+
settle_collections(obj, obj.class.fields(:array).keys)
|
|
433
|
+
else
|
|
434
|
+
clear_written_fields(obj, ok.map(&:first), failed.map(&:first))
|
|
435
|
+
end
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
# @!visibility private
|
|
439
|
+
# Clear dirty tracking only for the fields written by successful
|
|
440
|
+
# requests that no failed request also touched.
|
|
441
|
+
def self.clear_written_fields(obj, ok_requests, failed_requests)
|
|
442
|
+
remote = ->(reqs) { reqs.flat_map { |r| r.body.is_a?(Hash) ? r.body.keys.map(&:to_s) : [] } }
|
|
443
|
+
written = remote.call(ok_requests) - remote.call(failed_requests)
|
|
444
|
+
return if written.empty?
|
|
445
|
+
field_map = obj.class.respond_to?(:field_map) ? obj.class.field_map : {}
|
|
446
|
+
local = written.map do |key|
|
|
447
|
+
(field_map.find { |_local, rem| rem.to_s == key }&.first || key).to_s
|
|
448
|
+
end
|
|
449
|
+
settle_collections(obj, local)
|
|
450
|
+
obj.clear_attribute_changes(local) if obj.respond_to?(:clear_attribute_changes)
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
# @!visibility private
|
|
454
|
+
# Mark the collection proxies behind the named fields as saved.
|
|
455
|
+
def self.settle_collections(obj, names)
|
|
456
|
+
names.each do |name|
|
|
457
|
+
next unless obj.respond_to?(name)
|
|
458
|
+
value = obj.send(name)
|
|
459
|
+
value.changes_applied! if value.is_a?(Parse::CollectionProxy) && value.respond_to?(:changes_applied!)
|
|
460
|
+
end
|
|
461
|
+
end
|
|
462
|
+
end
|
|
463
|
+
end
|
|
@@ -346,15 +346,36 @@ module Parse
|
|
|
346
346
|
puts "[[Response]] --------------------------------------\n"
|
|
347
347
|
end
|
|
348
348
|
|
|
349
|
+
parsed = true
|
|
349
350
|
begin
|
|
350
351
|
r = Parse::Response.new(response_env.body)
|
|
351
352
|
rescue => e
|
|
353
|
+
parsed = false
|
|
352
354
|
r = Parse::Response.new
|
|
353
355
|
r.code = response_env.status
|
|
354
356
|
r.error = "Invalid response for #{env[:method]} #{env[:url]}: #{e}"
|
|
355
357
|
end
|
|
358
|
+
status = response_env[:status].to_i
|
|
356
359
|
r.http_status = response_env[:status]
|
|
357
360
|
r.headers = response_env[:response_headers]
|
|
361
|
+
if parsed && status.between?(200, 299)
|
|
362
|
+
# Parse Server never reports an error with a 2xx status, so a
|
|
363
|
+
# `code` or `error` key in a 2xx body is object data (for example
|
|
364
|
+
# a column set by a beforeSave trigger), not an error envelope.
|
|
365
|
+
r.code = nil
|
|
366
|
+
r.error = nil
|
|
367
|
+
elsif status > 0 && !status.between?(200, 299) && r.error.blank?
|
|
368
|
+
# A non-2xx response is a failure even without a Parse `error`
|
|
369
|
+
# field. It may come from something in front of Parse Server (an
|
|
370
|
+
# API gateway or load balancer returning 502/504 with
|
|
371
|
+
# `{"message": ...}`), or from Parse Server itself (a failed
|
|
372
|
+
# transaction returns `{"code": 1, "message": ...}`). Record the
|
|
373
|
+
# HTTP status as the code when there is none, so the request is
|
|
374
|
+
# never mistaken for a success.
|
|
375
|
+
detail = r.result.is_a?(Hash) ? r.result["message"] : nil
|
|
376
|
+
r.code ||= status
|
|
377
|
+
r.error = detail.present? ? detail.to_s : "HTTP #{status} response without a Parse error body"
|
|
378
|
+
end
|
|
358
379
|
r.code ||= response_env[:status] if r.error.present?
|
|
359
380
|
response_env[:body] = r
|
|
360
381
|
end
|