parse-stack-next 5.7.0 → 5.7.2
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 +167 -1
- data/lib/parse/agent/mcp_dispatcher.rb +2 -2
- data/lib/parse/agent.rb +0 -1
- data/lib/parse/cache/invalidation.rb +102 -50
- data/lib/parse/client/request.rb +16 -4
- data/lib/parse/client.rb +25 -4
- data/lib/parse/embeddings/image_fetch.rb +9 -1
- data/lib/parse/embeddings.rb +17 -4
- 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/core/actions.rb +294 -123
- data/lib/parse/model/core/embed_managed.rb +41 -4
- data/lib/parse/model/core/fetching.rb +4 -0
- data/lib/parse/model/core/properties.rb +4 -0
- data/lib/parse/model/core/querying.rb +0 -4
- data/lib/parse/model/object.rb +4 -1
- data/lib/parse/query/constraints.rb +61 -5
- data/lib/parse/query.rb +79 -5
- data/lib/parse/stack/version.rb +1 -1
- data/lib/parse/webhooks.rb +98 -2
- metadata +1 -1
|
@@ -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)
|
|
@@ -133,10 +155,16 @@ module Parse
|
|
|
133
155
|
# cannot corrupt the saved copy.
|
|
134
156
|
def self.snapshot_property_values(obj)
|
|
135
157
|
fields = obj.class.respond_to?(:fields) ? obj.class.fields.keys : []
|
|
136
|
-
|
|
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|
|
|
137
162
|
ivar = :"@#{key}"
|
|
138
|
-
|
|
139
|
-
|
|
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
|
|
140
168
|
end
|
|
141
169
|
end
|
|
142
170
|
|
|
@@ -147,25 +175,144 @@ module Parse
|
|
|
147
175
|
# `:array` and association properties hold a {Parse::CollectionProxy},
|
|
148
176
|
# and duplicating the proxy still shares the underlying `@collection`
|
|
149
177
|
# array, so `widget.tags << "b"` would mutate the snapshot too. The
|
|
150
|
-
# proxy's
|
|
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.
|
|
151
181
|
#
|
|
152
182
|
# @param value [Object] the live property value.
|
|
153
183
|
# @return [Object] a copy safe to hold across the transaction.
|
|
154
|
-
def self.dup_for_snapshot(value)
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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))
|
|
162
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
|
|
163
234
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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))
|
|
167
256
|
end
|
|
168
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)
|
|
169
316
|
end
|
|
170
317
|
|
|
171
318
|
# Restore the values captured by {snapshot_property_values}.
|
|
@@ -180,6 +327,25 @@ module Parse
|
|
|
180
327
|
def self.restore_property_values(obj, snapshot)
|
|
181
328
|
return unless snapshot.is_a?(Hash)
|
|
182
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
|
|
183
349
|
obj.instance_variable_set(ivar, value)
|
|
184
350
|
end
|
|
185
351
|
end
|
|
@@ -193,16 +359,35 @@ module Parse
|
|
|
193
359
|
obj = state[:object]
|
|
194
360
|
return if obj.nil?
|
|
195
361
|
restore_property_values(obj, state[:property_values])
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
|
204
387
|
end
|
|
205
388
|
|
|
389
|
+
private :_capture_transaction_state!, :_read_attribute
|
|
390
|
+
|
|
206
391
|
# Class methods applied to Parse::Object subclasses.
|
|
207
392
|
module ClassMethods
|
|
208
393
|
|
|
@@ -238,123 +423,109 @@ module Parse
|
|
|
238
423
|
def transaction(retries: 5, &block)
|
|
239
424
|
raise ArgumentError, "Block required for transaction" unless block_given?
|
|
240
425
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
426
|
+
previous_context = Fiber[TRANSACTION_CONTEXT_KEY]
|
|
427
|
+
transaction_context = { snapshots: {}, created_objects: {} }
|
|
428
|
+
Fiber[TRANSACTION_CONTEXT_KEY] = transaction_context
|
|
244
429
|
original_states = {}
|
|
245
430
|
tracked_objects = []
|
|
246
431
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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)
|
|
273
459
|
end
|
|
274
|
-
batch.add(obj)
|
|
275
|
-
end
|
|
276
460
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
|
281
468
|
|
|
282
|
-
|
|
469
|
+
result = yield(batch_wrapper)
|
|
283
470
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
|
290
477
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
obj.instance_variable_set(:@updated_at, Parse::Date.parse(result["updatedAt"]))
|
|
322
|
-
elsif result["createdAt"]
|
|
323
|
-
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)
|
|
324
508
|
end
|
|
325
509
|
|
|
326
|
-
|
|
327
|
-
obj.set_attributes!(result) if obj.respond_to?(:set_attributes!)
|
|
328
|
-
|
|
329
|
-
# Clear change tracking since save was successful
|
|
330
|
-
obj.send(:clear_changes!) if obj.respond_to?(:clear_changes!, true)
|
|
331
|
-
end
|
|
332
|
-
|
|
333
|
-
return responses
|
|
334
|
-
else
|
|
335
|
-
# Find first error
|
|
336
|
-
error_response = responses.find { |r| !r.success? }
|
|
337
|
-
|
|
338
|
-
# Rollback local object states
|
|
339
|
-
original_states.each_value do |state|
|
|
340
|
-
Parse::Core::Actions.rollback_object_state(state)
|
|
510
|
+
return responses
|
|
341
511
|
end
|
|
342
512
|
|
|
513
|
+
error_response = responses.find { |response| !response.success? }
|
|
343
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
|
|
344
521
|
end
|
|
345
|
-
rescue
|
|
346
|
-
# Retry on transaction conflict (error code 251)
|
|
347
|
-
if e.message.include?("251") && attempts < retries
|
|
348
|
-
sleep(0.1 * attempts) # Exponential backoff
|
|
349
|
-
retry
|
|
350
|
-
end
|
|
351
|
-
|
|
352
|
-
# Rollback local object states on final failure
|
|
522
|
+
rescue StandardError
|
|
353
523
|
original_states.each_value do |state|
|
|
354
524
|
Parse::Core::Actions.rollback_object_state(state)
|
|
355
525
|
end
|
|
356
|
-
|
|
357
|
-
|
|
526
|
+
raise
|
|
527
|
+
ensure
|
|
528
|
+
Fiber[TRANSACTION_CONTEXT_KEY] = previous_context
|
|
358
529
|
end
|
|
359
530
|
end
|
|
360
531
|
|
|
@@ -687,7 +687,7 @@ module Parse
|
|
|
687
687
|
return if stored_digest == digest && target_present
|
|
688
688
|
|
|
689
689
|
provider = Parse::Embeddings.provider(directive.provider_name)
|
|
690
|
-
vectors = call_provider(provider, directive, input)
|
|
690
|
+
vectors = call_provider(provider, directive, input, record)
|
|
691
691
|
unless vectors.is_a?(Array) && vectors.length == 1 && vectors.first.is_a?(Array)
|
|
692
692
|
raise Parse::Embeddings::InvalidResponseError,
|
|
693
693
|
"Parse::Core::EmbedManaged (#{record.class}##{directive.into}): provider " \
|
|
@@ -774,16 +774,27 @@ module Parse
|
|
|
774
774
|
# provider a {Parse::Embeddings::ImageFetch::FetchedImage}; `:url`
|
|
775
775
|
# mode forwards the raw URL String (the provider validates and
|
|
776
776
|
# fetches it itself).
|
|
777
|
-
|
|
777
|
+
#
|
|
778
|
+
# `input` is the bare canonical URL used for the digest (see
|
|
779
|
+
# {.build_source_input}). It stays stable across saves, so an
|
|
780
|
+
# unsigned re-read of the same file location does not force a
|
|
781
|
+
# re-embed.
|
|
782
|
+
# The actual fetch/forward target prefers the file's presigned
|
|
783
|
+
# URL ({Parse::File#presigned_url}) when one is currently valid,
|
|
784
|
+
# since a private-bucket adapter's bare `file.url` is stripped of
|
|
785
|
+
# its signature and will not resolve for the provider or for the
|
|
786
|
+
# SDK's own `:bytes`-mode download.
|
|
787
|
+
def self.call_provider(provider, directive, input, record)
|
|
778
788
|
if directive.image?
|
|
789
|
+
fetch_url = presigned_fetch_url(record, directive, input)
|
|
779
790
|
source = if directive.bytes_mode?
|
|
780
791
|
Parse::Embeddings::ImageFetch.fetch!(
|
|
781
|
-
|
|
792
|
+
fetch_url,
|
|
782
793
|
allow_insecure: directive.allow_insecure ? true : false,
|
|
783
794
|
exif_strip: directive.exif_strip != false,
|
|
784
795
|
)
|
|
785
796
|
else
|
|
786
|
-
|
|
797
|
+
fetch_url
|
|
787
798
|
end
|
|
788
799
|
provider.embed_image([source],
|
|
789
800
|
input_type: directive.input_type,
|
|
@@ -793,6 +804,32 @@ module Parse
|
|
|
793
804
|
end
|
|
794
805
|
end
|
|
795
806
|
|
|
807
|
+
# @!visibility private
|
|
808
|
+
# Resolve the URL to actually fetch/forward for an image
|
|
809
|
+
# directive: the source file's currently-valid presigned URL if
|
|
810
|
+
# it has one, otherwise the bare canonical `fallback` (the same
|
|
811
|
+
# string used for the digest). Never used for text directives, so
|
|
812
|
+
# `directive.sources.first` is always a `:file` property here.
|
|
813
|
+
#
|
|
814
|
+
# Checks validity with a zero safety buffer rather than
|
|
815
|
+
# {Parse::File#presigned_url_valid?}'s default 60-second one. That
|
|
816
|
+
# default exists so a browser has time to render before a
|
|
817
|
+
# presigned URL goes stale; here it would instead spend the last
|
|
818
|
+
# 60 seconds of a perfectly usable presigned URL falling back to
|
|
819
|
+
# `fallback`, which on a private-bucket adapter is not fetchable
|
|
820
|
+
# at all. A fetch that starts immediately after this check has no
|
|
821
|
+
# meaningful use for that margin, and a 403 from a URL that
|
|
822
|
+
# expired mid-request is strictly better than a guaranteed 403
|
|
823
|
+
# from a URL known unfetchable in advance.
|
|
824
|
+
def self.presigned_fetch_url(record, directive, fallback)
|
|
825
|
+
file = record.public_send(directive.sources.first)
|
|
826
|
+
if file.respond_to?(:presigned_url_valid?) && file.presigned_url_valid?(buffer: 0)
|
|
827
|
+
file.presigned_url
|
|
828
|
+
else
|
|
829
|
+
fallback
|
|
830
|
+
end
|
|
831
|
+
end
|
|
832
|
+
|
|
796
833
|
# @!visibility private
|
|
797
834
|
# Concatenate source-field string values. `nil` and blank entries
|
|
798
835
|
# are skipped; remaining values are joined with a double newline.
|
|
@@ -512,6 +512,10 @@ module Parse
|
|
|
512
512
|
@_fetched_keys ||= []
|
|
513
513
|
@_fetched_keys << key unless @_fetched_keys.include?(key)
|
|
514
514
|
end
|
|
515
|
+
|
|
516
|
+
# A transaction must capture the value before the setter marks or
|
|
517
|
+
# replaces it. `batch.add` happens after mutation in the public API.
|
|
518
|
+
send(:_capture_transaction_state!) if respond_to?(:_capture_transaction_state!, true)
|
|
515
519
|
end
|
|
516
520
|
end
|
|
517
521
|
end
|
|
@@ -573,6 +573,10 @@ module Parse
|
|
|
573
573
|
value = instance_variable_get ivar
|
|
574
574
|
end
|
|
575
575
|
|
|
576
|
+
# Capture after any implicit fetch, but before a default value or
|
|
577
|
+
# mutable collection is materialized by this getter.
|
|
578
|
+
send(:_capture_transaction_state!) if respond_to?(:_capture_transaction_state!, true)
|
|
579
|
+
|
|
576
580
|
# if value is nil (even after fetching), then lets see if the developer
|
|
577
581
|
# set a default value for this attribute.
|
|
578
582
|
if value.nil? && respond_to?("#{key}_default")
|
|
@@ -253,7 +253,6 @@ module Parse
|
|
|
253
253
|
# same created_at date (down to the microsecond). This prevents getting the same
|
|
254
254
|
# record in the next query request.
|
|
255
255
|
exclusion_set = results.select { |r| r.created_at == next_cursor.created_at }.map(&:id)
|
|
256
|
-
results = nil
|
|
257
256
|
cursor = next_cursor
|
|
258
257
|
end
|
|
259
258
|
end
|
|
@@ -356,7 +355,6 @@ module Parse
|
|
|
356
355
|
# Object.latest(:user.eq => user, limit: 5) # => 5 most recent for user
|
|
357
356
|
# @return [Parse::Object] the most recently created object matching constraints.
|
|
358
357
|
def latest(constraints = {})
|
|
359
|
-
fetch_count = 1
|
|
360
358
|
if constraints.is_a?(Numeric)
|
|
361
359
|
fetch_count = constraints.to_i
|
|
362
360
|
constraints = {}
|
|
@@ -385,7 +383,6 @@ module Parse
|
|
|
385
383
|
# Object.last_updated(:user.eq => user, limit: 3) # => 3 most recently updated for user
|
|
386
384
|
# @return [Parse::Object] the most recently updated object matching constraints.
|
|
387
385
|
def last_updated(constraints = {})
|
|
388
|
-
fetch_count = 1
|
|
389
386
|
if constraints.is_a?(Numeric)
|
|
390
387
|
fetch_count = constraints.to_i
|
|
391
388
|
constraints = {}
|
|
@@ -600,7 +597,6 @@ module Parse
|
|
|
600
597
|
parse_ids.compact!
|
|
601
598
|
# determines if the result back to the call site is an array or a single result
|
|
602
599
|
as_array = parse_ids.count > 1
|
|
603
|
-
results = []
|
|
604
600
|
|
|
605
601
|
# Default to write-only cache mode - find always gets fresh data
|
|
606
602
|
# but updates cache for future cached reads. Controlled by feature flag.
|
data/lib/parse/model/object.rb
CHANGED
|
@@ -1337,6 +1337,8 @@ module Parse
|
|
|
1337
1337
|
# for trusted hydration from server JSON; it bypasses the filter.
|
|
1338
1338
|
# @return [Parse::Object] a the corresponding Parse::Object or subclass.
|
|
1339
1339
|
def initialize(opts = {})
|
|
1340
|
+
Parse::Core::Actions.mark_transaction_object_created(self)
|
|
1341
|
+
|
|
1340
1342
|
# Trusted hydration is signalled by the `@_trusted_init` instance
|
|
1341
1343
|
# variable rather than by a `trusted:` keyword argument. Using a
|
|
1342
1344
|
# keyword would break subclasses that override `initialize(*args)`
|
|
@@ -1865,7 +1867,6 @@ module Parse
|
|
|
1865
1867
|
# we should do a reverse lookup on who is registered for a different class type
|
|
1866
1868
|
# than their name with parse_class
|
|
1867
1869
|
klass = Parse::Model.find_class className
|
|
1868
|
-
o = nil
|
|
1869
1870
|
if klass.present?
|
|
1870
1871
|
# when creating objects from Parse JSON data, don't use dirty tracking since
|
|
1871
1872
|
# we are considering these objects as "pristine"
|
|
@@ -2042,6 +2043,8 @@ module Parse
|
|
|
2042
2043
|
# caller intent to override.
|
|
2043
2044
|
# @api private
|
|
2044
2045
|
def acl_will_change!
|
|
2046
|
+
_capture_transaction_state!
|
|
2047
|
+
|
|
2045
2048
|
# Only capture snapshot on the first change (before any modifications)
|
|
2046
2049
|
unless defined?(@_acl_snapshot_before_change) && @_acl_snapshot_before_change
|
|
2047
2050
|
# Deep copy the ACL by creating a new one from its JSON representation
|