parse-stack-next 5.6.0 → 5.7.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 +489 -0
- data/README.md +378 -6
- data/docs/caching.md +748 -0
- data/examples/basic_client.rb +3 -3
- data/examples/basic_server.rb +3 -3
- data/examples/live_query_listener.rb +12 -12
- data/examples/rag_chatbot.rb +14 -14
- data/examples/transaction_example.rb +44 -45
- data/examples/webhook_server.rb +3 -3
- data/lib/parse/access.rb +493 -0
- data/lib/parse/acl_scope.rb +125 -52
- data/lib/parse/agent/approval_gate.rb +0 -0
- data/lib/parse/agent/cancellation_token.rb +3 -3
- data/lib/parse/agent/constraint_translator.rb +4 -4
- data/lib/parse/agent/describe.rb +34 -34
- data/lib/parse/agent/errors.rb +9 -9
- data/lib/parse/agent/mcp_client.rb +61 -59
- data/lib/parse/agent/mcp_dispatcher.rb +89 -101
- data/lib/parse/agent/mcp_rack_app.rb +92 -93
- data/lib/parse/agent/mcp_server.rb +1 -1
- data/lib/parse/agent/mcp_subscriptions.rb +22 -22
- data/lib/parse/agent/metadata_audit.rb +1 -2
- data/lib/parse/agent/metadata_dsl.rb +7 -7
- data/lib/parse/agent/metadata_registry.rb +12 -12
- data/lib/parse/agent/prompt_hardening.rb +4 -4
- data/lib/parse/agent/prompts.rb +10 -10
- data/lib/parse/agent/result_formatter.rb +4 -4
- data/lib/parse/agent/tools.rb +493 -400
- data/lib/parse/agent.rb +157 -116
- data/lib/parse/api/hooks.rb +1 -1
- data/lib/parse/api/server.rb +2 -2
- data/lib/parse/api/users.rb +2 -2
- data/lib/parse/atlas_search/index_manager.rb +1 -1
- data/lib/parse/atlas_search/session.rb +40 -218
- data/lib/parse/atlas_search.rb +181 -60
- data/lib/parse/authorization.rb +466 -0
- data/lib/parse/cache/invalidation.rb +219 -0
- data/lib/parse/cache/keyspace.rb +306 -0
- data/lib/parse/cache/moneta_surface.rb +126 -0
- data/lib/parse/cache/pool.rb +48 -5
- data/lib/parse/cache/redis.rb +425 -10
- data/lib/parse/cache/scoped_view.rb +443 -0
- data/lib/parse/cache/sub_cache.rb +264 -0
- data/lib/parse/cache/upstream_roles.rb +230 -0
- data/lib/parse/client/authentication.rb +1 -1
- data/lib/parse/client/body_builder.rb +1 -2
- data/lib/parse/client/caching.rb +84 -5
- data/lib/parse/client.rb +327 -189
- data/lib/parse/clp_scope.rb +225 -28
- data/lib/parse/console.rb +3 -3
- data/lib/parse/embeddings/batch_embedder.rb +3 -3
- data/lib/parse/embeddings/cache.rb +17 -17
- data/lib/parse/embeddings/cohere.rb +21 -22
- data/lib/parse/embeddings/image_fetch.rb +22 -21
- data/lib/parse/embeddings/jina.rb +21 -21
- data/lib/parse/embeddings/local_http.rb +6 -7
- data/lib/parse/embeddings/media_file.rb +5 -4
- data/lib/parse/embeddings/openai.rb +5 -5
- data/lib/parse/embeddings/provider.rb +1 -1
- data/lib/parse/embeddings/qwen.rb +10 -10
- data/lib/parse/embeddings/spend_cap.rb +2 -3
- data/lib/parse/embeddings/streaming_body.rb +11 -11
- data/lib/parse/embeddings/video_source.rb +7 -6
- data/lib/parse/embeddings/voyage.rb +59 -61
- data/lib/parse/embeddings.rb +28 -27
- data/lib/parse/graphql/type_generator.rb +1 -1
- data/lib/parse/graphql.rb +5 -5
- data/lib/parse/live_query/client.rb +1 -1
- data/lib/parse/live_query.rb +1 -1
- data/lib/parse/lock.rb +15 -18
- data/lib/parse/lock_backend.rb +1 -1
- data/lib/parse/lookup_rewriter.rb +6 -6
- data/lib/parse/model/associations/belongs_to.rb +4 -0
- data/lib/parse/model/associations/collection_proxy.rb +3 -0
- data/lib/parse/model/associations/has_many.rb +4 -0
- data/lib/parse/model/classes/role.rb +227 -28
- data/lib/parse/model/classes/user.rb +60 -1
- data/lib/parse/model/clp.rb +4 -4
- data/lib/parse/model/core/actions.rb +366 -113
- data/lib/parse/model/core/create_lock.rb +0 -2
- data/lib/parse/model/core/describe.rb +57 -57
- data/lib/parse/model/core/embed_managed.rb +21 -23
- data/lib/parse/model/core/fetching.rb +6 -0
- data/lib/parse/model/core/indexing.rb +14 -14
- data/lib/parse/model/core/parse_reference.rb +1 -1
- data/lib/parse/model/core/properties.rb +6 -2
- data/lib/parse/model/core/querying.rb +1 -1
- data/lib/parse/model/core/schema.rb +2 -2
- data/lib/parse/model/core/search_indexing.rb +2 -2
- data/lib/parse/model/core/vector_searchable.rb +4 -5
- data/lib/parse/model/file.rb +21 -15
- data/lib/parse/model/geojson.rb +2 -2
- data/lib/parse/model/geopoint.rb +1 -0
- data/lib/parse/model/object.rb +136 -66
- data/lib/parse/model/pointer.rb +2 -0
- data/lib/parse/model/polygon.rb +3 -6
- data/lib/parse/model/push.rb +2 -2
- data/lib/parse/model/vector.rb +3 -1
- data/lib/parse/mongodb.rb +512 -282
- data/lib/parse/pipeline_security.rb +3 -0
- data/lib/parse/query/constraints.rb +46 -46
- data/lib/parse/query.rb +146 -59
- data/lib/parse/retrieval/agent_tool.rb +34 -20
- data/lib/parse/retrieval/chunk.rb +1 -0
- data/lib/parse/retrieval/reranker/cohere.rb +11 -11
- data/lib/parse/retrieval/reranker.rb +3 -4
- data/lib/parse/retrieval/retriever.rb +4 -6
- data/lib/parse/schema/index_migrator.rb +26 -26
- data/lib/parse/schema/search_index_migrator.rb +19 -19
- data/lib/parse/stack/tasks.rb +6 -6
- data/lib/parse/stack/version.rb +1 -1
- data/lib/parse/stack.rb +10 -10
- data/lib/parse/vector_search/hybrid.rb +66 -41
- data/lib/parse/vector_search.rb +14 -11
- data/lib/parse/webhooks/payload.rb +1 -0
- data/lib/parse/webhooks/registration.rb +4 -4
- data/lib/parse/webhooks/trigger_audit.rb +50 -49
- data/lib/parse/webhooks.rb +120 -5
- data/parse-stack-next.gemspec +6 -6
- metadata +10 -1
data/lib/parse/model/clp.rb
CHANGED
|
@@ -324,10 +324,10 @@ module Parse
|
|
|
324
324
|
# Determine if we should include defaults
|
|
325
325
|
# Auto-enable if any CLP settings exist and no explicit choice made
|
|
326
326
|
should_include_defaults = if include_defaults.nil?
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
327
|
+
present? && @default_permission
|
|
328
|
+
else
|
|
329
|
+
include_defaults
|
|
330
|
+
end
|
|
331
331
|
|
|
332
332
|
# Determine the default permission to use
|
|
333
333
|
# Use explicit default_permission if set, otherwise fall back to public
|
|
@@ -94,6 +94,28 @@ module Parse
|
|
|
94
94
|
module Core
|
|
95
95
|
# Defines some of the save, update and destroy operations for Parse objects.
|
|
96
96
|
module Actions
|
|
97
|
+
# Fiber-local context used to capture an object's state before its first
|
|
98
|
+
# mutation inside a transaction block. `batch.add` is intentionally too
|
|
99
|
+
# late for this: the public API documents mutating an object and adding it
|
|
100
|
+
# afterwards.
|
|
101
|
+
TRANSACTION_CONTEXT_KEY = :__parse_transaction_context__
|
|
102
|
+
|
|
103
|
+
# Distinguishes a property whose ivar did not exist from one explicitly
|
|
104
|
+
# set to nil. Rollback removes the former instead of defining it as nil.
|
|
105
|
+
UNDEFINED_PROPERTY = Object.new.freeze
|
|
106
|
+
|
|
107
|
+
# Non-property state that can change while building/submitting a batch
|
|
108
|
+
# and must be restored along with the `@<field>` property ivars.
|
|
109
|
+
ROLLBACK_STATE_IVARS = %i[
|
|
110
|
+
@changed_attributes
|
|
111
|
+
@id
|
|
112
|
+
@mutations_from_database
|
|
113
|
+
@mutations_before_last_save
|
|
114
|
+
@_acl_snapshot_before_change
|
|
115
|
+
@_acl_pristine
|
|
116
|
+
@_authorization_acl_state
|
|
117
|
+
].freeze
|
|
118
|
+
|
|
97
119
|
# @!visibility private
|
|
98
120
|
def self.included(base)
|
|
99
121
|
base.extend(ClassMethods)
|
|
@@ -109,6 +131,263 @@ module Parse
|
|
|
109
131
|
end
|
|
110
132
|
end
|
|
111
133
|
|
|
134
|
+
# Capture the property values a transaction rollback must restore.
|
|
135
|
+
#
|
|
136
|
+
# Property values live in `@<field>` instance variables (see
|
|
137
|
+
# {Parse::Properties::ClassMethods#property}), so those are what gets
|
|
138
|
+
# snapshotted. {Parse::Object#attributes} is deliberately NOT captured:
|
|
139
|
+
# it returns a SCHEMA map (field name to type symbol), not values, so
|
|
140
|
+
# restoring it never rolled anything back.
|
|
141
|
+
#
|
|
142
|
+
# Restoring it was also actively harmful. `@attributes` is the ivar
|
|
143
|
+
# `ActiveModel::Dirty` keys its behavior on: `mutations_from_database`
|
|
144
|
+
# builds an `AttributeMutationTracker` over `@attributes` the moment
|
|
145
|
+
# that ivar is defined (and a `ForcedMutationTracker` otherwise), while
|
|
146
|
+
# `forget_attribute_assignments` calls `map(&:forgetting_assignment)`
|
|
147
|
+
# on it. Handing either one a schema Hash raised `NoMethodError` on a
|
|
148
|
+
# type symbol and on the `[key, value]` pair `Hash#map` yields. Both
|
|
149
|
+
# sites rescue and warn, so every rollback silently downgraded the
|
|
150
|
+
# object to broken change tracking instead of failing.
|
|
151
|
+
#
|
|
152
|
+
# @param obj [Parse::Object] the object being tracked.
|
|
153
|
+
# @return [Hash] field name to a snapshot of its value. Mutable values
|
|
154
|
+
# are duplicated so in-place mutation between snapshot and rollback
|
|
155
|
+
# cannot corrupt the saved copy.
|
|
156
|
+
def self.snapshot_property_values(obj)
|
|
157
|
+
fields = obj.class.respond_to?(:fields) ? obj.class.fields.keys : []
|
|
158
|
+
relations = obj.class.respond_to?(:relations) ? obj.class.relations.keys : []
|
|
159
|
+
property_keys = (fields + relations).uniq
|
|
160
|
+
seen = {}
|
|
161
|
+
property_keys.each_with_object({}) do |key, snapshot|
|
|
162
|
+
ivar = :"@#{key}"
|
|
163
|
+
snapshot[ivar] = if obj.instance_variable_defined?(ivar)
|
|
164
|
+
dup_for_snapshot(obj.instance_variable_get(ivar), seen)
|
|
165
|
+
else
|
|
166
|
+
UNDEFINED_PROPERTY
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Copy a property value deeply enough that later in-place mutation of
|
|
172
|
+
# the live value cannot reach the snapshot.
|
|
173
|
+
#
|
|
174
|
+
# A plain `dup` is not sufficient for collection-backed properties:
|
|
175
|
+
# `:array` and association properties hold a {Parse::CollectionProxy},
|
|
176
|
+
# and duplicating the proxy still shares the underlying `@collection`
|
|
177
|
+
# array, so `widget.tags << "b"` would mutate the snapshot too. The
|
|
178
|
+
# proxy's mutable backing arrays and nested values are duplicated as
|
|
179
|
+
# well. Parse objects nested inside values remain references: a pointer
|
|
180
|
+
# property should restore the same object, not manufacture a clone.
|
|
181
|
+
#
|
|
182
|
+
# @param value [Object] the live property value.
|
|
183
|
+
# @return [Object] a copy safe to hold across the transaction.
|
|
184
|
+
def self.dup_for_snapshot(value, seen = {})
|
|
185
|
+
return value if value.nil? || value == true || value == false || value.is_a?(Symbol) || value.is_a?(Numeric)
|
|
186
|
+
return value if defined?(Parse::Pointer) && value.is_a?(Parse::Pointer)
|
|
187
|
+
|
|
188
|
+
object_id = value.object_id
|
|
189
|
+
return seen[object_id] if seen.key?(object_id)
|
|
190
|
+
|
|
191
|
+
case value
|
|
192
|
+
when Array
|
|
193
|
+
copy = value.dup
|
|
194
|
+
copy.clear
|
|
195
|
+
seen[object_id] = copy
|
|
196
|
+
value.each { |item| copy << dup_for_snapshot(item, seen) }
|
|
197
|
+
copy
|
|
198
|
+
when Hash
|
|
199
|
+
copy = value.dup
|
|
200
|
+
copy.clear
|
|
201
|
+
seen[object_id] = copy
|
|
202
|
+
value.each do |key, item|
|
|
203
|
+
copy[dup_for_snapshot(key, seen)] = dup_for_snapshot(item, seen)
|
|
204
|
+
end
|
|
205
|
+
copy
|
|
206
|
+
when Parse::CollectionProxy
|
|
207
|
+
copy = value.dup
|
|
208
|
+
seen[object_id] = copy
|
|
209
|
+
%i[@collection @additions @removals @changed_attributes].each do |ivar|
|
|
210
|
+
next unless value.instance_variable_defined?(ivar)
|
|
211
|
+
copy.instance_variable_set(ivar, dup_for_snapshot(value.instance_variable_get(ivar), seen))
|
|
212
|
+
end
|
|
213
|
+
%i[@mutations_from_database @mutations_before_last_save].each do |ivar|
|
|
214
|
+
next unless value.instance_variable_defined?(ivar)
|
|
215
|
+
tracker = value.instance_variable_get(ivar)
|
|
216
|
+
copy.instance_variable_set(ivar, dup_mutation_tracker(tracker, copy, seen))
|
|
217
|
+
end
|
|
218
|
+
copy
|
|
219
|
+
when Parse::ACL
|
|
220
|
+
copy = Parse::ACL.new(dup_for_snapshot(value.as_json, seen), owner: value.delegate)
|
|
221
|
+
seen[object_id] = copy
|
|
222
|
+
copy
|
|
223
|
+
else
|
|
224
|
+
copy = begin
|
|
225
|
+
value.dup
|
|
226
|
+
rescue TypeError
|
|
227
|
+
# Immutable values are safe to share with the snapshot.
|
|
228
|
+
return value
|
|
229
|
+
end
|
|
230
|
+
seen[object_id] = copy
|
|
231
|
+
copy
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Duplicate ActiveModel's mutation tracker without sharing its mutable
|
|
236
|
+
# forced/finalized change hashes. Forced trackers retain their owning
|
|
237
|
+
# Parse object (or copied collection proxy) so dirty reads keep working.
|
|
238
|
+
#
|
|
239
|
+
# @param tracker [Object, nil] ActiveModel mutation tracker.
|
|
240
|
+
# @param owner [Object] object whose attributes the copy should read.
|
|
241
|
+
# @param seen [Hash] identity map used by {dup_for_snapshot}.
|
|
242
|
+
# @return [Object, nil] isolated tracker copy.
|
|
243
|
+
def self.dup_mutation_tracker(tracker, owner, seen = {})
|
|
244
|
+
return nil if tracker.nil?
|
|
245
|
+
return tracker if defined?(ActiveModel::NullMutationTracker) && tracker.is_a?(ActiveModel::NullMutationTracker)
|
|
246
|
+
return seen[tracker.object_id] if seen.key?(tracker.object_id)
|
|
247
|
+
|
|
248
|
+
copy = tracker.dup
|
|
249
|
+
seen[tracker.object_id] = copy
|
|
250
|
+
if defined?(ActiveModel::ForcedMutationTracker) && tracker.is_a?(ActiveModel::ForcedMutationTracker)
|
|
251
|
+
copy.instance_variable_set(:@attributes, owner)
|
|
252
|
+
end
|
|
253
|
+
%i[@forced_changes @finalized_changes].each do |ivar|
|
|
254
|
+
next unless tracker.instance_variable_defined?(ivar)
|
|
255
|
+
copy.instance_variable_set(ivar, dup_for_snapshot(tracker.instance_variable_get(ivar), seen))
|
|
256
|
+
end
|
|
257
|
+
copy
|
|
258
|
+
rescue TypeError
|
|
259
|
+
tracker
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# Capture all local state needed to restore an object after a failed
|
|
263
|
+
# transaction. This is separate from `#attributes`, which is a schema.
|
|
264
|
+
#
|
|
265
|
+
# @param obj [Parse::Object] object being transacted.
|
|
266
|
+
# @return [Hash] rollback state.
|
|
267
|
+
def self.snapshot_object_state(obj)
|
|
268
|
+
seen = {}
|
|
269
|
+
instance_variables = ROLLBACK_STATE_IVARS.each_with_object({}) do |ivar, snapshot|
|
|
270
|
+
snapshot[ivar] = if obj.instance_variable_defined?(ivar)
|
|
271
|
+
value = obj.instance_variable_get(ivar)
|
|
272
|
+
if %i[@mutations_from_database @mutations_before_last_save].include?(ivar)
|
|
273
|
+
dup_mutation_tracker(value, obj, seen)
|
|
274
|
+
else
|
|
275
|
+
dup_for_snapshot(value, seen)
|
|
276
|
+
end
|
|
277
|
+
else
|
|
278
|
+
UNDEFINED_PROPERTY
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
{
|
|
283
|
+
object: obj,
|
|
284
|
+
property_values: snapshot_property_values(obj),
|
|
285
|
+
instance_variables: instance_variables,
|
|
286
|
+
}
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# Record that an object was initialized inside the active transaction.
|
|
290
|
+
# Its first rollback snapshot is intentionally taken when it is added to
|
|
291
|
+
# the batch, after initialization, so a failed create stays usable as an
|
|
292
|
+
# initialized unsaved object.
|
|
293
|
+
#
|
|
294
|
+
# @param obj [Parse::Object] newly initialized object.
|
|
295
|
+
# @return [void]
|
|
296
|
+
def self.mark_transaction_object_created(obj)
|
|
297
|
+
context = Fiber[TRANSACTION_CONTEXT_KEY]
|
|
298
|
+
return unless context
|
|
299
|
+
context[:created_objects][obj.object_id] = true
|
|
300
|
+
context[:snapshots].delete(obj.object_id)
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# Capture an object's state once, before its first transaction mutation.
|
|
304
|
+
# Objects created inside the transaction defer capture until `batch.add`.
|
|
305
|
+
#
|
|
306
|
+
# @param obj [Parse::Object] object whose state should be captured.
|
|
307
|
+
# @param context [Hash, nil] transaction context; defaults to the current fiber.
|
|
308
|
+
# @param include_created [Boolean] capture a newly created object at add time.
|
|
309
|
+
# @return [Hash, nil] the object's rollback state.
|
|
310
|
+
def self.capture_transaction_state(obj, context = Fiber[TRANSACTION_CONTEXT_KEY], include_created: false)
|
|
311
|
+
return unless context && obj
|
|
312
|
+
|
|
313
|
+
object_id = obj.object_id
|
|
314
|
+
return if context[:created_objects].key?(object_id) && !include_created
|
|
315
|
+
context[:snapshots][object_id] ||= snapshot_object_state(obj)
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# Restore the values captured by {snapshot_property_values}.
|
|
319
|
+
#
|
|
320
|
+
# Writes ivars directly rather than going through the property setters,
|
|
321
|
+
# since the setters mark the object dirty and the caller restores the
|
|
322
|
+
# dirty state itself immediately afterwards.
|
|
323
|
+
#
|
|
324
|
+
# @param obj [Parse::Object] the object being rolled back.
|
|
325
|
+
# @param snapshot [Hash] the return value of {snapshot_property_values}.
|
|
326
|
+
# @return [void]
|
|
327
|
+
def self.restore_property_values(obj, snapshot)
|
|
328
|
+
return unless snapshot.is_a?(Hash)
|
|
329
|
+
snapshot.each do |ivar, value|
|
|
330
|
+
if value.equal?(UNDEFINED_PROPERTY)
|
|
331
|
+
obj.remove_instance_variable(ivar) if obj.instance_variable_defined?(ivar)
|
|
332
|
+
else
|
|
333
|
+
obj.instance_variable_set(ivar, value)
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
# Restore one instance variable while preserving whether it originally
|
|
339
|
+
# existed. Used for dirty/ACL bookkeeping outside the property schema.
|
|
340
|
+
#
|
|
341
|
+
# @param obj [Object] target object.
|
|
342
|
+
# @param ivar [Symbol] instance variable name.
|
|
343
|
+
# @param value [Object] snapshotted value or {UNDEFINED_PROPERTY}.
|
|
344
|
+
# @return [void]
|
|
345
|
+
def self.restore_instance_variable(obj, ivar, value)
|
|
346
|
+
if value.equal?(UNDEFINED_PROPERTY)
|
|
347
|
+
obj.remove_instance_variable(ivar) if obj.instance_variable_defined?(ivar)
|
|
348
|
+
else
|
|
349
|
+
obj.instance_variable_set(ivar, value)
|
|
350
|
+
end
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
# Roll a single tracked object back to the state captured when it was
|
|
354
|
+
# added to the transaction.
|
|
355
|
+
#
|
|
356
|
+
# @param state [Hash] one entry of the transaction's `original_states`.
|
|
357
|
+
# @return [void]
|
|
358
|
+
def self.rollback_object_state(state)
|
|
359
|
+
obj = state[:object]
|
|
360
|
+
return if obj.nil?
|
|
361
|
+
restore_property_values(obj, state[:property_values])
|
|
362
|
+
if state[:instance_variables]
|
|
363
|
+
state[:instance_variables].each do |ivar, value|
|
|
364
|
+
restore_instance_variable(obj, ivar, value)
|
|
365
|
+
end
|
|
366
|
+
else
|
|
367
|
+
# Compatibility with rollback states produced before the complete
|
|
368
|
+
# snapshot format was introduced.
|
|
369
|
+
obj.instance_variable_set(:@changed_attributes, state[:changed_attributes])
|
|
370
|
+
obj.instance_variable_set(:@id, state[:id])
|
|
371
|
+
obj.instance_variable_set(:@mutations_from_database, state[:mutations_from_database])
|
|
372
|
+
obj.instance_variable_set(:@mutations_before_last_save, state[:mutations_before_last_save])
|
|
373
|
+
end
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
# Hook used by generated property accessors and collection proxies.
|
|
377
|
+
# @api private
|
|
378
|
+
def _capture_transaction_state!
|
|
379
|
+
Parse::Core::Actions.capture_transaction_state(self)
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
# ActiveModel calls this immediately after `<field>_will_change!`; the
|
|
383
|
+
# hook also covers callers that explicitly mark a mutable value dirty.
|
|
384
|
+
def _read_attribute(attr_name)
|
|
385
|
+
_capture_transaction_state!
|
|
386
|
+
super
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
private :_capture_transaction_state!, :_read_attribute
|
|
390
|
+
|
|
112
391
|
# Class methods applied to Parse::Object subclasses.
|
|
113
392
|
module ClassMethods
|
|
114
393
|
|
|
@@ -144,135 +423,109 @@ module Parse
|
|
|
144
423
|
def transaction(retries: 5, &block)
|
|
145
424
|
raise ArgumentError, "Block required for transaction" unless block_given?
|
|
146
425
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
426
|
+
previous_context = Fiber[TRANSACTION_CONTEXT_KEY]
|
|
427
|
+
transaction_context = { snapshots: {}, created_objects: {} }
|
|
428
|
+
Fiber[TRANSACTION_CONTEXT_KEY] = transaction_context
|
|
150
429
|
original_states = {}
|
|
151
430
|
tracked_objects = []
|
|
152
431
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
432
|
+
begin
|
|
433
|
+
batch = Parse::BatchOperation.new(nil, transaction: true)
|
|
434
|
+
|
|
435
|
+
# Wrap the batch to associate the pre-mutation snapshot with each
|
|
436
|
+
# object that actually participates in the transaction.
|
|
437
|
+
batch_wrapper = Object.new
|
|
438
|
+
batch_wrapper.define_singleton_method(:is_a?) do |klass|
|
|
439
|
+
klass == Parse::BatchOperation || super(klass)
|
|
440
|
+
end
|
|
441
|
+
batch_wrapper.define_singleton_method(:kind_of?) do |klass|
|
|
442
|
+
klass == Parse::BatchOperation || super(klass)
|
|
443
|
+
end
|
|
444
|
+
batch_wrapper.define_singleton_method(:instance_of?) do |klass|
|
|
445
|
+
klass == Parse::BatchOperation
|
|
446
|
+
end
|
|
447
|
+
batch_wrapper.define_singleton_method(:add) do |obj|
|
|
448
|
+
# Ruby identity is required because all unsaved Parse objects
|
|
449
|
+
# compare equal while their ids are nil.
|
|
450
|
+
if obj.respond_to?(:attributes) && obj.respond_to?(:id) && !original_states.key?(obj.object_id)
|
|
451
|
+
original_states[obj.object_id] = Parse::Core::Actions.capture_transaction_state(
|
|
452
|
+
obj,
|
|
453
|
+
transaction_context,
|
|
454
|
+
include_created: true,
|
|
455
|
+
)
|
|
456
|
+
tracked_objects << obj
|
|
457
|
+
end
|
|
458
|
+
batch.add(obj)
|
|
179
459
|
end
|
|
180
|
-
batch.add(obj)
|
|
181
|
-
end
|
|
182
460
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
461
|
+
# Forward other methods to the real batch.
|
|
462
|
+
batch_wrapper.define_singleton_method(:method_missing) do |method, *args, &method_block|
|
|
463
|
+
batch.send(method, *args, &method_block)
|
|
464
|
+
end
|
|
465
|
+
batch_wrapper.define_singleton_method(:respond_to_missing?) do |method, include_private = false|
|
|
466
|
+
batch.respond_to?(method, include_private)
|
|
467
|
+
end
|
|
187
468
|
|
|
188
|
-
|
|
469
|
+
result = yield(batch_wrapper)
|
|
189
470
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
471
|
+
# If block returns objects, add them to batch.
|
|
472
|
+
if result.respond_to?(:change_requests)
|
|
473
|
+
batch_wrapper.add(result)
|
|
474
|
+
elsif result.is_a?(Array)
|
|
475
|
+
result.each { |obj| batch_wrapper.add(obj) if obj.respond_to?(:change_requests) }
|
|
476
|
+
end
|
|
196
477
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
obj.instance_variable_set(:@updated_at, Parse::Date.parse(result["updatedAt"]))
|
|
228
|
-
elsif result["createdAt"]
|
|
229
|
-
obj.instance_variable_set(:@updated_at, Parse::Date.parse(result["createdAt"]))
|
|
478
|
+
# Submit with retry logic for transaction conflicts.
|
|
479
|
+
attempts = 0
|
|
480
|
+
begin
|
|
481
|
+
attempts += 1
|
|
482
|
+
responses = batch.submit
|
|
483
|
+
|
|
484
|
+
if responses.all?(&:success?)
|
|
485
|
+
# Match responses to objects using the request tag (Ruby object_id).
|
|
486
|
+
objects_by_id = tracked_objects.each_with_object({}) { |o, h| h[o.object_id] = o }
|
|
487
|
+
batch.requests.zip(responses).each do |request, response|
|
|
488
|
+
next unless request && response && response.success?
|
|
489
|
+
result = response.result
|
|
490
|
+
next unless result.is_a?(Hash)
|
|
491
|
+
|
|
492
|
+
obj = objects_by_id[request.tag]
|
|
493
|
+
next unless obj
|
|
494
|
+
|
|
495
|
+
obj.instance_variable_set(:@id, result["objectId"]) if result["objectId"]
|
|
496
|
+
if result["createdAt"]
|
|
497
|
+
obj.instance_variable_set(:@created_at, Parse::Date.parse(result["createdAt"]))
|
|
498
|
+
end
|
|
499
|
+
if result["updatedAt"]
|
|
500
|
+
obj.instance_variable_set(:@updated_at, Parse::Date.parse(result["updatedAt"]))
|
|
501
|
+
elsif result["createdAt"]
|
|
502
|
+
obj.instance_variable_set(:@updated_at, Parse::Date.parse(result["createdAt"]))
|
|
503
|
+
end
|
|
504
|
+
|
|
505
|
+
# Apply any additional attributes returned by beforeSave hooks.
|
|
506
|
+
obj.set_attributes!(result) if obj.respond_to?(:set_attributes!)
|
|
507
|
+
obj.send(:clear_changes!) if obj.respond_to?(:clear_changes!, true)
|
|
230
508
|
end
|
|
231
509
|
|
|
232
|
-
|
|
233
|
-
obj.set_attributes!(result) if obj.respond_to?(:set_attributes!)
|
|
234
|
-
|
|
235
|
-
# Clear change tracking since save was successful
|
|
236
|
-
obj.send(:clear_changes!) if obj.respond_to?(:clear_changes!, true)
|
|
237
|
-
end
|
|
238
|
-
|
|
239
|
-
return responses
|
|
240
|
-
else
|
|
241
|
-
# Find first error
|
|
242
|
-
error_response = responses.find { |r| !r.success? }
|
|
243
|
-
|
|
244
|
-
# Rollback local object states
|
|
245
|
-
original_states.each_value do |state|
|
|
246
|
-
obj = state[:object]
|
|
247
|
-
obj.instance_variable_set(:@attributes, state[:attributes])
|
|
248
|
-
obj.instance_variable_set(:@changed_attributes, state[:changed_attributes])
|
|
249
|
-
obj.instance_variable_set(:@id, state[:id])
|
|
250
|
-
# Restore change tracking state
|
|
251
|
-
obj.instance_variable_set(:@mutations_from_database, state[:mutations_from_database])
|
|
252
|
-
obj.instance_variable_set(:@mutations_before_last_save, state[:mutations_before_last_save])
|
|
510
|
+
return responses
|
|
253
511
|
end
|
|
254
512
|
|
|
513
|
+
error_response = responses.find { |response| !response.success? }
|
|
255
514
|
raise Parse::Error, "Transaction failed: #{error_response.error}"
|
|
515
|
+
rescue Parse::Error => e
|
|
516
|
+
if e.message.include?("251") && attempts < retries
|
|
517
|
+
sleep(0.1 * attempts)
|
|
518
|
+
retry
|
|
519
|
+
end
|
|
520
|
+
raise
|
|
256
521
|
end
|
|
257
|
-
rescue
|
|
258
|
-
# Retry on transaction conflict (error code 251)
|
|
259
|
-
if e.message.include?("251") && attempts < retries
|
|
260
|
-
sleep(0.1 * attempts) # Exponential backoff
|
|
261
|
-
retry
|
|
262
|
-
end
|
|
263
|
-
|
|
264
|
-
# Rollback local object states on final failure
|
|
522
|
+
rescue StandardError
|
|
265
523
|
original_states.each_value do |state|
|
|
266
|
-
|
|
267
|
-
obj.instance_variable_set(:@attributes, state[:attributes])
|
|
268
|
-
obj.instance_variable_set(:@changed_attributes, state[:changed_attributes])
|
|
269
|
-
obj.instance_variable_set(:@id, state[:id])
|
|
270
|
-
# Restore change tracking state
|
|
271
|
-
obj.instance_variable_set(:@mutations_from_database, state[:mutations_from_database])
|
|
272
|
-
obj.instance_variable_set(:@mutations_before_last_save, state[:mutations_before_last_save])
|
|
524
|
+
Parse::Core::Actions.rollback_object_state(state)
|
|
273
525
|
end
|
|
274
|
-
|
|
275
|
-
|
|
526
|
+
raise
|
|
527
|
+
ensure
|
|
528
|
+
Fiber[TRANSACTION_CONTEXT_KEY] = previous_context
|
|
276
529
|
end
|
|
277
530
|
end
|
|
278
531
|
|