parse-stack-next 5.8.0 → 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 +318 -0
- data/README.md +25 -19
- data/docs/caching.md +50 -0
- data/docs/mcp_guide.md +15 -3
- data/docs/webhooks_guide.md +59 -7
- data/lib/parse/acl_scope.rb +71 -9
- data/lib/parse/agent/describe.rb +8 -2
- data/lib/parse/agent/mcp_dispatcher.rb +8 -1
- data/lib/parse/agent.rb +205 -20
- data/lib/parse/atlas_search/index_manager.rb +5 -1
- data/lib/parse/atlas_search.rb +49 -1
- data/lib/parse/authorization.rb +235 -3
- data/lib/parse/cache/sub_cache.rb +26 -0
- data/lib/parse/client/authentication.rb +19 -1
- data/lib/parse/client/batch.rb +35 -2
- data/lib/parse/client.rb +26 -2
- data/lib/parse/clp_scope.rb +37 -3
- data/lib/parse/live_query/client.rb +72 -2
- data/lib/parse/lock_backend.rb +4 -3
- data/lib/parse/model/associations/collection_proxy.rb +19 -11
- data/lib/parse/model/associations/has_many.rb +11 -0
- data/lib/parse/model/associations/pointer_collection_proxy.rb +36 -0
- data/lib/parse/model/associations/relation_collection_proxy.rb +97 -12
- data/lib/parse/model/classes/role.rb +23 -0
- data/lib/parse/model/classes/session.rb +279 -19
- data/lib/parse/model/classes/user.rb +33 -12
- data/lib/parse/model/core/actions.rb +19 -4
- data/lib/parse/model/core/fetching.rb +29 -2
- data/lib/parse/model/core/field_guards.rb +16 -8
- data/lib/parse/model/object.rb +52 -1
- data/lib/parse/model/push.rb +11 -1
- data/lib/parse/mongodb.rb +19 -1
- data/lib/parse/query/constraint.rb +1 -1
- data/lib/parse/query/constraints.rb +31 -27
- data/lib/parse/query.rb +793 -55
- data/lib/parse/stack/version.rb +1 -1
- data/lib/parse/stack.rb +19 -0
- data/lib/parse/vector_search/hybrid.rb +74 -15
- data/lib/parse/vector_search.rb +214 -7
- data/lib/parse/webhooks/payload.rb +43 -0
- data/lib/parse/webhooks.rb +312 -9
- metadata +1 -1
|
@@ -90,7 +90,9 @@ module Parse
|
|
|
90
90
|
opts.delete(:session_token)
|
|
91
91
|
response = client.fetch_session(token, **opts)
|
|
92
92
|
if response.success?
|
|
93
|
-
|
|
93
|
+
built = Parse::Session.build response.result
|
|
94
|
+
_remember_owners!([built], client)
|
|
95
|
+
return built
|
|
94
96
|
end
|
|
95
97
|
nil
|
|
96
98
|
end
|
|
@@ -205,29 +207,218 @@ module Parse
|
|
|
205
207
|
destroy
|
|
206
208
|
end
|
|
207
209
|
|
|
208
|
-
# Deletes the session and
|
|
209
|
-
#
|
|
210
|
-
#
|
|
211
|
-
#
|
|
212
|
-
#
|
|
213
|
-
#
|
|
210
|
+
# Deletes the session and forgets it in the client's identity plane, so
|
|
211
|
+
# mongo-direct and Atlas Search reads stop resolving the token at once
|
|
212
|
+
# instead of when the cached entry expires. Both the token and the owning
|
|
213
|
+
# user's entries are dropped. A session that does not carry them (built
|
|
214
|
+
# from its objectId, or fetched without the master key or with `keys:`
|
|
215
|
+
# leaving them out) has them looked up first: with the master key as SDK
|
|
216
|
+
# metadata when the client has one, otherwise with the `session:` passed
|
|
217
|
+
# here. A lookup that finds no row leaves nothing to forget. Only a
|
|
218
|
+
# lookup that fails outright drops every cached identity on the client,
|
|
219
|
+
# at most once every {RESET_INTERVAL} seconds per client.
|
|
220
|
+
#
|
|
221
|
+
# The entries are dropped when the delete succeeds or reports the row
|
|
222
|
+
# is already gone (or not visible to the caller). A delete that raises
|
|
223
|
+
# still drops the known token, which is harmless, but does not touch the
|
|
224
|
+
# owner's entries.
|
|
225
|
+
#
|
|
226
|
+
# A batch delete through `Array#destroy` drops the same entries for
|
|
227
|
+
# each session in it. With an identity plane that cannot drop a
|
|
228
|
+
# single user's entries (a custom plane without `bump_generation` or
|
|
229
|
+
# `invalidate_value`), a revoked token keeps resolving until its cached
|
|
230
|
+
# entry expires.
|
|
214
231
|
# @param session [String] (see Parse::Object#destroy)
|
|
215
232
|
# @return [Boolean] whether the operation was successful.
|
|
216
233
|
def destroy(session: nil)
|
|
217
|
-
|
|
218
|
-
#
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
owner_id =
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
234
|
+
# A session never saved is not deleted (see Parse::Object#destroy), so
|
|
235
|
+
# there is nothing to forget either.
|
|
236
|
+
return super if new?
|
|
237
|
+
token = nil
|
|
238
|
+
owner_id = nil
|
|
239
|
+
begin
|
|
240
|
+
Parse::Session.send(:_preload_identity_for_destroy!, [self], session_token: session)
|
|
241
|
+
token, owner_id = _identity_for_destroy
|
|
242
|
+
ensure
|
|
243
|
+
# The looked-up token is a live credential: keep it in locals only.
|
|
244
|
+
_clear_identity_for_destroy!
|
|
245
|
+
end
|
|
246
|
+
result = nil
|
|
247
|
+
begin
|
|
248
|
+
result = super
|
|
249
|
+
ensure
|
|
250
|
+
# `false` covers "object not found", which is either a session
|
|
251
|
+
# already revoked elsewhere or one the caller cannot see; dropping
|
|
252
|
+
# cached entries is idempotent in both cases. A raised delete still
|
|
253
|
+
# drops the token it named but leaves the owner alone. A single
|
|
254
|
+
# delete cannot tell "already gone" from "denied", so an absent row
|
|
255
|
+
# only uses the recorded owner, never the reset fallback.
|
|
256
|
+
if result.nil?
|
|
257
|
+
_forget_identity!(token.is_a?(String) ? token : nil, nil)
|
|
258
|
+
else
|
|
259
|
+
_forget_identity!(token, owner_id, reset_fallback: false)
|
|
260
|
+
end
|
|
227
261
|
end
|
|
228
|
-
success
|
|
229
262
|
end
|
|
230
263
|
|
|
264
|
+
# Seconds between two full identity-cache resets triggered by a failed
|
|
265
|
+
# session lookup on the same client.
|
|
266
|
+
RESET_INTERVAL = 5
|
|
267
|
+
|
|
268
|
+
# Process-local record of the last lookup-triggered reset per client.
|
|
269
|
+
@identity_reset_at = {}
|
|
270
|
+
@identity_reset_mutex = Mutex.new
|
|
271
|
+
|
|
272
|
+
class << self
|
|
273
|
+
# Record, in the identity context of `cl` (the client that fetched
|
|
274
|
+
# them), which user owns each session that carries both its objectId
|
|
275
|
+
# and its owner, so a later delete can drop the owner's cached
|
|
276
|
+
# identities after the row is gone. Never raises: it runs while query
|
|
277
|
+
# results are decoded.
|
|
278
|
+
# @param sessions [Array<Parse::Object>]
|
|
279
|
+
# @param cl [Parse::Client]
|
|
280
|
+
# @!visibility private
|
|
281
|
+
def _remember_owners!(sessions, cl)
|
|
282
|
+
auth = cl.respond_to?(:authorization) ? cl.authorization : nil
|
|
283
|
+
return unless auth.respond_to?(:remember_session_owner)
|
|
284
|
+
Array(sessions).each do |o|
|
|
285
|
+
next unless o.is_a?(Parse::Session)
|
|
286
|
+
owner_id = o.send(:_identity_owner_id)
|
|
287
|
+
id = o.id
|
|
288
|
+
auth.remember_session_owner(id, owner_id) if owner_id && id.present?
|
|
289
|
+
end
|
|
290
|
+
nil
|
|
291
|
+
rescue StandardError
|
|
292
|
+
nil
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
private
|
|
296
|
+
|
|
297
|
+
# Reset `cl`'s identity and role caches, at most once every
|
|
298
|
+
# {RESET_INTERVAL} seconds per client.
|
|
299
|
+
# @!visibility private
|
|
300
|
+
def _rate_limited_identity_reset!(cl)
|
|
301
|
+
auth = cl.respond_to?(:authorization) ? cl.authorization : nil
|
|
302
|
+
return unless auth.respond_to?(:reset_caches!)
|
|
303
|
+
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
304
|
+
due = @identity_reset_mutex.synchronize do
|
|
305
|
+
last = @identity_reset_at[cl.object_id]
|
|
306
|
+
next false if last && now - last < RESET_INTERVAL
|
|
307
|
+
@identity_reset_at[cl.object_id] = now
|
|
308
|
+
true
|
|
309
|
+
end
|
|
310
|
+
auth.reset_caches! if due
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
# Look up the token and owner of every session about to be deleted that
|
|
314
|
+
# does not carry them, so the delete can drop their identity entries.
|
|
315
|
+
# One `_Session` query per client. A client with a master key reads it
|
|
316
|
+
# as SDK metadata (it works inside `Parse.without_master_key`); one
|
|
317
|
+
# without reads it with `session_token`, or skips the lookup when there
|
|
318
|
+
# is none. The query never uses the response cache: its rows carry live
|
|
319
|
+
# session tokens.
|
|
320
|
+
#
|
|
321
|
+
# A row the lookup did not return is marked absent: there is nothing
|
|
322
|
+
# to forget for it. Only a lookup that raised marks its sessions
|
|
323
|
+
# `:unknown`, which makes their delete reset the client's identity
|
|
324
|
+
# cache (rate limited).
|
|
325
|
+
# @param sessions [Array<Parse::Object>]
|
|
326
|
+
# @param session_token [String, nil] the delete's own session.
|
|
327
|
+
# @!visibility private
|
|
328
|
+
def _preload_identity_for_destroy!(sessions, session_token: nil)
|
|
329
|
+
pending = sessions.select { |o| o.is_a?(Parse::Session) && o.send(:_identity_lookup_needed?) }
|
|
330
|
+
return if pending.empty?
|
|
331
|
+
pending.group_by(&:client).each do |cl, group|
|
|
332
|
+
has_master = cl.respond_to?(:master_key) && cl.master_key.present?
|
|
333
|
+
token = session_token.respond_to?(:session_token) ? session_token.session_token : session_token
|
|
334
|
+
token = nil unless token.is_a?(String) && !token.strip.empty?
|
|
335
|
+
unless has_master || token
|
|
336
|
+
group.each { |o| o.instance_variable_set(:@_identity_for_destroy, :absent) }
|
|
337
|
+
next
|
|
338
|
+
end
|
|
339
|
+
ids = group.map(&:id).uniq
|
|
340
|
+
found = begin
|
|
341
|
+
query = Parse::Session.query(:objectId.in => ids, limit: ids.size)
|
|
342
|
+
query.keys(:session_token, :user)
|
|
343
|
+
query.client = cl
|
|
344
|
+
query.cache = false
|
|
345
|
+
if has_master
|
|
346
|
+
query.instance_variable_set(:@_metadata_master, true)
|
|
347
|
+
else
|
|
348
|
+
query.session_token = token
|
|
349
|
+
end
|
|
350
|
+
query.results.to_h do |row|
|
|
351
|
+
owner = row.instance_variable_get(:@user)
|
|
352
|
+
[row.id, [row.instance_variable_get(:@session_token), owner.respond_to?(:id) ? owner.id : nil]]
|
|
353
|
+
end
|
|
354
|
+
rescue StandardError => e
|
|
355
|
+
warn "[Parse::Session] could not look up the token and owner of " \
|
|
356
|
+
"#{ids.size} session(s) before deleting them (#{e.class}); " \
|
|
357
|
+
"dropping the whole identity cache instead."
|
|
358
|
+
nil
|
|
359
|
+
end
|
|
360
|
+
group.each do |o|
|
|
361
|
+
mark = if found.nil?
|
|
362
|
+
:unknown
|
|
363
|
+
elsif (row = found[o.id]) && (row[0].is_a?(String) || row[1])
|
|
364
|
+
row
|
|
365
|
+
else
|
|
366
|
+
:absent
|
|
367
|
+
end
|
|
368
|
+
o.instance_variable_set(:@_identity_for_destroy, mark)
|
|
369
|
+
end
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
# Called by `Array#destroy` for each session in the batch with its
|
|
375
|
+
# response. The entries are dropped only when the delete succeeded or
|
|
376
|
+
# reported the row gone ("object not found").
|
|
377
|
+
# @!visibility private
|
|
378
|
+
def _after_batch_destroy(response = nil)
|
|
379
|
+
identity = _identity_for_destroy
|
|
380
|
+
_clear_identity_for_destroy!
|
|
381
|
+
return unless Parse::Session.send(:_destroy_applied?, response)
|
|
382
|
+
# The batch response tells success and "object not found" apart from
|
|
383
|
+
# a denial, so an absent row whose owner was never recorded may fall
|
|
384
|
+
# back to the rate-limited reset here.
|
|
385
|
+
_forget_identity!(*identity, reset_fallback: true)
|
|
386
|
+
end
|
|
387
|
+
private :_after_batch_destroy
|
|
388
|
+
|
|
389
|
+
# Whether a batch delete response means the row is gone: success, or
|
|
390
|
+
# "object not found". A missing response (older callers) counts as
|
|
391
|
+
# applied.
|
|
392
|
+
# @!visibility private
|
|
393
|
+
def self._destroy_applied?(response)
|
|
394
|
+
return true if response.nil?
|
|
395
|
+
(response.respond_to?(:success?) && response.success?) ||
|
|
396
|
+
(response.respond_to?(:object_not_found?) && response.object_not_found?)
|
|
397
|
+
end
|
|
398
|
+
private_class_method :_destroy_applied?
|
|
399
|
+
|
|
400
|
+
# Remove the token and owner {._preload_identity_for_destroy!} looked
|
|
401
|
+
# up. The token is a live credential and must not outlive the delete
|
|
402
|
+
# (it would show in `inspect` or `instance_variables`).
|
|
403
|
+
# @!visibility private
|
|
404
|
+
def _clear_identity_for_destroy!
|
|
405
|
+
remove_instance_variable(:@_identity_for_destroy) if instance_variable_defined?(:@_identity_for_destroy)
|
|
406
|
+
end
|
|
407
|
+
private :_clear_identity_for_destroy!
|
|
408
|
+
|
|
409
|
+
# Whether this session lacks the token or the owner its identity-plane
|
|
410
|
+
# entries are keyed by: a fetch without the master key (no token) or
|
|
411
|
+
# with `keys:` leaving them out.
|
|
412
|
+
# @!visibility private
|
|
413
|
+
def _identity_lookup_needed?
|
|
414
|
+
# Keyed on the objectId, not `new?`: `Array#destroy` deletes by id
|
|
415
|
+
# alone, so a session built from an id (no timestamps loaded) is still
|
|
416
|
+
# deleted and its token must still be dropped.
|
|
417
|
+
return false if @id.blank?
|
|
418
|
+
!@session_token.is_a?(String) || @session_token.empty? || _identity_owner_id.nil?
|
|
419
|
+
end
|
|
420
|
+
private :_identity_lookup_needed?
|
|
421
|
+
|
|
231
422
|
# Serialization omits `sessionToken` unless `include_session_token: true`
|
|
232
423
|
# is passed. A session token is a bearer credential, and `as_json` is the
|
|
233
424
|
# surface that reaches logs, API responses and agent tool output.
|
|
@@ -245,7 +436,76 @@ module Parse
|
|
|
245
436
|
# Redacts the session token from the default inspect output.
|
|
246
437
|
# @return [String]
|
|
247
438
|
def inspect
|
|
248
|
-
|
|
439
|
+
looked_up = @_identity_for_destroy
|
|
440
|
+
Parse::User.redact_session_token(super, @session_token, looked_up.is_a?(Array) ? looked_up[0] : nil)
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
private
|
|
444
|
+
|
|
445
|
+
# The token and owner id to forget for this delete: what this instance
|
|
446
|
+
# carries, completed by {._preload_identity_for_destroy!} when it did
|
|
447
|
+
# not, or `:unknown` when that lookup failed.
|
|
448
|
+
# @!visibility private
|
|
449
|
+
def _identity_for_destroy
|
|
450
|
+
looked_up = @_identity_for_destroy
|
|
451
|
+
token = @session_token.is_a?(String) && !@session_token.empty? ? @session_token : nil
|
|
452
|
+
owner_id = _identity_owner_id
|
|
453
|
+
return [:unknown, owner_id] if looked_up == :unknown
|
|
454
|
+
if looked_up == :absent
|
|
455
|
+
# No row to read: the session is gone or not visible. Forget what
|
|
456
|
+
# this instance carries; with nothing at all, mark it absent so the
|
|
457
|
+
# delete can fall back to the recorded owner.
|
|
458
|
+
return token || owner_id ? [token, owner_id] : [:absent, nil]
|
|
459
|
+
end
|
|
460
|
+
if looked_up.is_a?(Array)
|
|
461
|
+
token ||= looked_up[0]
|
|
462
|
+
owner_id ||= looked_up[1]
|
|
463
|
+
end
|
|
464
|
+
[token, owner_id]
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
# The owning user's id, read from the association ivar directly:
|
|
468
|
+
# calling `user` on a partially fetched session could trigger an
|
|
469
|
+
# autofetch just to learn the owner.
|
|
470
|
+
# @!visibility private
|
|
471
|
+
def _identity_owner_id
|
|
472
|
+
owner = @user
|
|
473
|
+
owner.respond_to?(:id) ? owner.id : nil
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
# Drop the token and the owner's entries from this session's client's
|
|
477
|
+
# identity plane.
|
|
478
|
+
# @!visibility private
|
|
479
|
+
def _forget_identity!(token, owner_id, reset_fallback: false)
|
|
480
|
+
cl = client
|
|
481
|
+
if token == :absent
|
|
482
|
+
# The row was not readable before the delete. Use the owner recorded
|
|
483
|
+
# when this session was last loaded; without one, and only when the
|
|
484
|
+
# delete is known to have applied or found the row gone, fall back to
|
|
485
|
+
# the rate-limited reset. Never on a denied delete.
|
|
486
|
+
token = nil
|
|
487
|
+
auth = cl.respond_to?(:authorization) ? cl.authorization : nil
|
|
488
|
+
recorded = auth.respond_to?(:session_owner) ? auth.session_owner(@id) : nil
|
|
489
|
+
if recorded
|
|
490
|
+
owner_id ||= recorded
|
|
491
|
+
elsif owner_id.nil? && reset_fallback
|
|
492
|
+
Parse::Session.send(:_rate_limited_identity_reset!, cl)
|
|
493
|
+
end
|
|
494
|
+
end
|
|
495
|
+
if token == :unknown
|
|
496
|
+
# The lookup failed, so the token's entry cannot be named. Drop every
|
|
497
|
+
# cached identity on this client (the next read of each token
|
|
498
|
+
# re-resolves it, which is safe), at most once every RESET_INTERVAL
|
|
499
|
+
# seconds so repeated failures cannot flush a shared plane on every
|
|
500
|
+
# request. The owner's entries go as well when the owner is known.
|
|
501
|
+
Parse::Session.send(:_rate_limited_identity_reset!, cl)
|
|
502
|
+
token = nil
|
|
503
|
+
end
|
|
504
|
+
cl.invalidate_session_identity(token) if token.is_a?(String) && cl.respond_to?(:invalidate_session_identity)
|
|
505
|
+
cl.invalidate_user_identity(owner_id) if owner_id && cl.respond_to?(:invalidate_user_identity)
|
|
506
|
+
rescue StandardError
|
|
507
|
+
# Runs from an `ensure`: never replace the delete's own outcome.
|
|
508
|
+
nil
|
|
249
509
|
end
|
|
250
510
|
end
|
|
251
511
|
end
|
|
@@ -712,7 +712,7 @@ module Parse
|
|
|
712
712
|
|
|
713
713
|
if response.success?
|
|
714
714
|
result = response.result || {}
|
|
715
|
-
@updated_at = result["updatedAt"] || @updated_at
|
|
715
|
+
@updated_at = _server_date(result["updatedAt"]) || @updated_at
|
|
716
716
|
# Parse Server may rotate the session token on a credential
|
|
717
717
|
# change; apply it narrowly if present without going through the
|
|
718
718
|
# full property writer chain.
|
|
@@ -826,8 +826,8 @@ module Parse
|
|
|
826
826
|
# save-as-signup path had already addressed.
|
|
827
827
|
result = response.result
|
|
828
828
|
@id = result[Parse::Model::OBJECT_ID] || @id
|
|
829
|
-
@created_at = result["createdAt"] || @created_at
|
|
830
|
-
@updated_at = result["updatedAt"] || result["createdAt"] || @updated_at
|
|
829
|
+
@created_at = _server_date(result["createdAt"]) || @created_at
|
|
830
|
+
@updated_at = _server_date(result["updatedAt"] || result["createdAt"]) || @updated_at
|
|
831
831
|
set_attributes!(result.slice(*SIGNUP_RESPONSE_APPLY_KEYS))
|
|
832
832
|
# Drop the plaintext password from memory now that the server
|
|
833
833
|
# has it hashed and we no longer need it. Matches the Parse JS
|
|
@@ -956,19 +956,40 @@ module Parse
|
|
|
956
956
|
result
|
|
957
957
|
end
|
|
958
958
|
|
|
959
|
-
# Deletes the user.
|
|
960
|
-
#
|
|
961
|
-
#
|
|
959
|
+
# Deletes the user. Every cached identity entry for the user is dropped
|
|
960
|
+
# from this client's identity plane, since Parse Server removes the
|
|
961
|
+
# user's sessions with the account. The entries are dropped when the
|
|
962
|
+
# delete returns, successful or not (a failure is usually "object not
|
|
963
|
+
# found": the account is already gone or not visible to the caller, and
|
|
964
|
+
# dropping cached entries is idempotent). A delete that raises leaves
|
|
965
|
+
# them alone.
|
|
962
966
|
# @param session [String] (see Parse::Object#destroy)
|
|
963
967
|
# @return [Boolean] whether the operation was successful.
|
|
964
968
|
def destroy(session: nil)
|
|
965
969
|
user_id = id
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
client.invalidate_user_identity(user_id)
|
|
970
|
+
result = super
|
|
971
|
+
begin
|
|
972
|
+
client.invalidate_user_identity(user_id) if user_id.present? && client.respond_to?(:invalidate_user_identity)
|
|
973
|
+
rescue StandardError
|
|
974
|
+
# Never replace the delete's own outcome.
|
|
975
|
+
nil
|
|
969
976
|
end
|
|
970
|
-
|
|
977
|
+
result
|
|
978
|
+
end
|
|
979
|
+
|
|
980
|
+
# Called by `Array#destroy` for each user in the batch with its
|
|
981
|
+
# response. Drops the user's identity entries, as {#destroy} does, when
|
|
982
|
+
# the delete succeeded or the row is already gone; a denied delete
|
|
983
|
+
# leaves them alone.
|
|
984
|
+
# @!visibility private
|
|
985
|
+
def _after_batch_destroy(response = nil)
|
|
986
|
+
applied = response.nil? ||
|
|
987
|
+
(response.respond_to?(:success?) && response.success?) ||
|
|
988
|
+
(response.respond_to?(:object_not_found?) && response.object_not_found?)
|
|
989
|
+
return unless applied
|
|
990
|
+
client.invalidate_user_identity(id) if id.present? && client.respond_to?(:invalidate_user_identity)
|
|
971
991
|
end
|
|
992
|
+
private :_after_batch_destroy
|
|
972
993
|
|
|
973
994
|
# Invalid the current session token for this logged in user.
|
|
974
995
|
# @return [Boolean] True/false if successful
|
|
@@ -1729,8 +1750,8 @@ module Parse
|
|
|
1729
1750
|
unless res.error?
|
|
1730
1751
|
result = res.result
|
|
1731
1752
|
@id = result[Parse::Model::OBJECT_ID] || @id
|
|
1732
|
-
@created_at = result["createdAt"] || @created_at
|
|
1733
|
-
@updated_at = result["updatedAt"] || result["createdAt"] || @updated_at
|
|
1753
|
+
@created_at = _server_date(result["createdAt"]) || @created_at
|
|
1754
|
+
@updated_at = _server_date(result["updatedAt"] || result["createdAt"]) || @updated_at
|
|
1734
1755
|
# Plaintext password is no longer needed locally; the server
|
|
1735
1756
|
# has it hashed. Direct ivar assignment avoids re-dirtying the
|
|
1736
1757
|
# field.
|
|
@@ -554,7 +554,13 @@ module Parse
|
|
|
554
554
|
|
|
555
555
|
# Apply any additional attributes returned by beforeSave hooks.
|
|
556
556
|
obj.set_attributes!(result) if obj.respond_to?(:set_attributes!)
|
|
557
|
-
|
|
557
|
+
# The transaction carried the record's relation ops too, so
|
|
558
|
+
# settle them as a save does rather than discarding them.
|
|
559
|
+
if obj.respond_to?(:changes_applied!, true)
|
|
560
|
+
obj.send(:changes_applied!)
|
|
561
|
+
elsif obj.respond_to?(:clear_changes!, true)
|
|
562
|
+
obj.send(:clear_changes!)
|
|
563
|
+
end
|
|
558
564
|
end
|
|
559
565
|
|
|
560
566
|
return responses
|
|
@@ -1108,6 +1114,8 @@ module Parse
|
|
|
1108
1114
|
# @param op_hash [Hash] The operation hash. It may also be of type {Parse::RelationAction}.
|
|
1109
1115
|
# @return [Boolean] whether the operation was successful.
|
|
1110
1116
|
def operate_field!(field, op_hash)
|
|
1117
|
+
# A reply from an earlier operation must never be read as this one's.
|
|
1118
|
+
@_last_operation_result = nil
|
|
1111
1119
|
field = field.to_sym
|
|
1112
1120
|
field = self.field_map[field] || field
|
|
1113
1121
|
if op_hash.is_a?(Parse::RelationAction)
|
|
@@ -1132,9 +1140,12 @@ module Parse
|
|
|
1132
1140
|
|
|
1133
1141
|
# @!visibility private
|
|
1134
1142
|
# The value Parse Server returned for `field` from the last
|
|
1135
|
-
# {#operate_field!}, or nil when the reply did not include it.
|
|
1143
|
+
# {#operate_field!}, or nil when the reply did not include it. The
|
|
1144
|
+
# reply is read once: it is forgotten here so a later read cannot
|
|
1145
|
+
# pick up a stale value.
|
|
1136
1146
|
def _last_operation_value(field)
|
|
1137
1147
|
result = @_last_operation_result
|
|
1148
|
+
@_last_operation_result = nil
|
|
1138
1149
|
return nil unless result.is_a?(Hash)
|
|
1139
1150
|
wire = (self.field_map[field.to_sym] || field).to_s
|
|
1140
1151
|
result[wire]
|
|
@@ -1198,7 +1209,9 @@ module Parse
|
|
|
1198
1209
|
def op_add_relation!(field, objects = [])
|
|
1199
1210
|
objects = [objects] unless objects.is_a?(Array)
|
|
1200
1211
|
return false if objects.empty?
|
|
1201
|
-
|
|
1212
|
+
# The operation is keyed by the remote column, as a save sends it.
|
|
1213
|
+
remote_field = self.field_map[field.to_sym] || field
|
|
1214
|
+
relation_action = Parse::RelationAction.new(remote_field, polarity: true, objects: objects)
|
|
1202
1215
|
operate_field! field, relation_action
|
|
1203
1216
|
end
|
|
1204
1217
|
|
|
@@ -1210,7 +1223,9 @@ module Parse
|
|
|
1210
1223
|
def op_remove_relation!(field, objects = [])
|
|
1211
1224
|
objects = [objects] unless objects.is_a?(Array)
|
|
1212
1225
|
return false if objects.empty?
|
|
1213
|
-
|
|
1226
|
+
# The operation is keyed by the remote column, as a save sends it.
|
|
1227
|
+
remote_field = self.field_map[field.to_sym] || field
|
|
1228
|
+
relation_action = Parse::RelationAction.new(remote_field, polarity: false, objects: objects)
|
|
1214
1229
|
operate_field! field, relation_action
|
|
1215
1230
|
end
|
|
1216
1231
|
|
|
@@ -55,6 +55,9 @@ module Parse
|
|
|
55
55
|
# @param preserve_changes [Boolean] if true, re-apply local dirty values to fetched fields.
|
|
56
56
|
# By default (false), fetched fields accept server values and local changes are discarded.
|
|
57
57
|
# Unfetched fields always preserve their dirty state regardless of this setting.
|
|
58
|
+
# A relation's staged additions and removals are kept in both modes (they are
|
|
59
|
+
# operations, not field values) and the relation stays marked changed, so the
|
|
60
|
+
# next save sends them. Use {Parse::Object#reload!} or `clear_changes!` to drop them.
|
|
58
61
|
# @param opts [Hash] a set of options to pass to the client request.
|
|
59
62
|
# @option opts [Boolean, Symbol] :cache (:write_only) caching mode:
|
|
60
63
|
# - :write_only (default) - skip cache read, but update cache with fresh data
|
|
@@ -220,11 +223,13 @@ module Parse
|
|
|
220
223
|
respond_to?(:record_authorization_hydration!)
|
|
221
224
|
|
|
222
225
|
begin
|
|
223
|
-
clear_changes
|
|
226
|
+
# Not clear_changes!: that also drops a relation's staged
|
|
227
|
+
# additions and removals, which a fetch must keep.
|
|
228
|
+
clear_dirty_tracking!
|
|
224
229
|
rescue => e
|
|
225
230
|
# Log the error for debugging purposes
|
|
226
231
|
warn "[Parse::Fetch] Warning: clear_changes! failed: #{e.class}: #{e.message}"
|
|
227
|
-
# If
|
|
232
|
+
# If clearing fails, manually reset change tracking
|
|
228
233
|
@changed_attributes = {} if instance_variable_defined?(:@changed_attributes)
|
|
229
234
|
@mutations_from_database = nil if instance_variable_defined?(:@mutations_from_database)
|
|
230
235
|
@mutations_before_last_save = nil if instance_variable_defined?(:@mutations_before_last_save)
|
|
@@ -266,9 +271,31 @@ module Parse
|
|
|
266
271
|
end
|
|
267
272
|
end
|
|
268
273
|
|
|
274
|
+
# A relation whose proxy still holds staged additions or removals
|
|
275
|
+
# must stay dirty, or the next save sends nothing. The loop above
|
|
276
|
+
# cannot guarantee it: when the response carries the relation's
|
|
277
|
+
# descriptor, re-applying the same proxy is not a change.
|
|
278
|
+
remark_staged_relations_dirty!
|
|
279
|
+
|
|
269
280
|
self
|
|
270
281
|
end
|
|
271
282
|
|
|
283
|
+
# @!visibility private
|
|
284
|
+
# Marks dirty every relation whose proxy holds staged additions or
|
|
285
|
+
# removals. Used after a fetch has cleared the record's dirty tracking.
|
|
286
|
+
def remark_staged_relations_dirty!
|
|
287
|
+
return unless respond_to?(:relations)
|
|
288
|
+
relations.each_key do |key|
|
|
289
|
+
ivar = :"@#{key}"
|
|
290
|
+
next unless instance_variable_defined?(ivar)
|
|
291
|
+
proxy = instance_variable_get(ivar)
|
|
292
|
+
next unless proxy.respond_to?(:staged_changes?) && proxy.staged_changes?
|
|
293
|
+
next if changed.include?(key.to_s)
|
|
294
|
+
will_change_method = "#{key}_will_change!"
|
|
295
|
+
send(will_change_method) if respond_to?(will_change_method)
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
|
|
272
299
|
# Fetches the object with explicit caching enabled.
|
|
273
300
|
# This is a convenience method that calls fetch! with cache: true.
|
|
274
301
|
# Use this when you want to leverage cached responses for better performance.
|
|
@@ -197,20 +197,28 @@ module Parse
|
|
|
197
197
|
|
|
198
198
|
if respond_to?(:relations) && relations[field_sym]
|
|
199
199
|
proxy = public_send(field_sym)
|
|
200
|
-
#
|
|
201
|
-
# relation_change_operations
|
|
202
|
-
#
|
|
203
|
-
#
|
|
204
|
-
#
|
|
205
|
-
|
|
206
|
-
proxy.instance_variable_set(:@removals, []) if proxy.instance_variable_defined?(:@removals)
|
|
200
|
+
# Drop the pending add/remove ledger that backs
|
|
201
|
+
# relation_change_operations, and the proxy's dirty markers. The
|
|
202
|
+
# relation proxy's clear_changes! also reloads its list on next
|
|
203
|
+
# access, since the loaded list included the discarded items. A
|
|
204
|
+
# handler that reads the relation after the revert therefore
|
|
205
|
+
# triggers one query for the stored members (on a saved owner).
|
|
207
206
|
proxy.clear_changes! if proxy.respond_to?(:clear_changes!)
|
|
208
207
|
clear_attribute_changes([field_str])
|
|
209
208
|
return
|
|
210
209
|
end
|
|
211
210
|
|
|
212
211
|
if is_new
|
|
213
|
-
|
|
212
|
+
if field_sym == :acl && !self.class.builtin_acl_default_active?
|
|
213
|
+
# The class's ACL policy still decides a new record's ACL: put
|
|
214
|
+
# back the init-time default stamp and let the save-time
|
|
215
|
+
# resolver run, instead of a nil ACL that a reply would send
|
|
216
|
+
# as `{}` (master-key only).
|
|
217
|
+
public_send(:acl=, self.class.default_acls.as_json)
|
|
218
|
+
@_acl_pristine = true
|
|
219
|
+
else
|
|
220
|
+
public_send("#{field_str}=", nil)
|
|
221
|
+
end
|
|
214
222
|
else
|
|
215
223
|
restore_attributes([field_str])
|
|
216
224
|
end
|
data/lib/parse/model/object.rb
CHANGED
|
@@ -1614,8 +1614,54 @@ module Parse
|
|
|
1614
1614
|
clear_changes!
|
|
1615
1615
|
end
|
|
1616
1616
|
|
|
1617
|
-
# clears all dirty tracking information
|
|
1617
|
+
# clears all dirty tracking information. Array and relation proxies are
|
|
1618
|
+
# cleared too, so a relation's staged additions and removals are dropped
|
|
1619
|
+
# rather than sent by a later save.
|
|
1618
1620
|
def clear_changes!
|
|
1621
|
+
clear_proxy_changes!
|
|
1622
|
+
clear_dirty_tracking!
|
|
1623
|
+
end
|
|
1624
|
+
|
|
1625
|
+
# @!visibility private
|
|
1626
|
+
# Instance variable names of the array and relation proxies a class can
|
|
1627
|
+
# hold. Cached per class because {#clear_changes!} runs for every record
|
|
1628
|
+
# built with an id (every query row). The cache is rebuilt when a field
|
|
1629
|
+
# or relation is declared after it was computed.
|
|
1630
|
+
# @return [Array<Symbol>]
|
|
1631
|
+
def self.proxy_change_ivars
|
|
1632
|
+
all_fields = fields
|
|
1633
|
+
all_relations = relations
|
|
1634
|
+
signature = [all_fields.size, all_relations.size]
|
|
1635
|
+
return @_proxy_change_ivars if @_proxy_change_ivars_signature == signature
|
|
1636
|
+
ivars = (all_fields.select { |_, type| type == :array }.keys + all_relations.keys)
|
|
1637
|
+
.uniq.map { |key| :"@#{key}" }.freeze
|
|
1638
|
+
# Assign the list before its signature so a concurrent reader that
|
|
1639
|
+
# sees the new signature also sees the new list.
|
|
1640
|
+
@_proxy_change_ivars = ivars
|
|
1641
|
+
@_proxy_change_ivars_signature = signature
|
|
1642
|
+
ivars
|
|
1643
|
+
end
|
|
1644
|
+
|
|
1645
|
+
# @!visibility private
|
|
1646
|
+
# Clears the dirty state of every array and relation proxy this record
|
|
1647
|
+
# holds, dropping a relation's staged additions and removals.
|
|
1648
|
+
def clear_proxy_changes!
|
|
1649
|
+
ivars = self.class.proxy_change_ivars
|
|
1650
|
+
return if ivars.empty?
|
|
1651
|
+
ivars.each do |ivar|
|
|
1652
|
+
next unless instance_variable_defined?(ivar)
|
|
1653
|
+
proxy = instance_variable_get(ivar)
|
|
1654
|
+
proxy.clear_changes! if proxy.respond_to?(:clear_changes!)
|
|
1655
|
+
end
|
|
1656
|
+
end
|
|
1657
|
+
private :clear_proxy_changes!
|
|
1658
|
+
|
|
1659
|
+
# @!visibility private
|
|
1660
|
+
# Clears the record's own dirty tracking but leaves array and relation
|
|
1661
|
+
# proxies alone. A fetch uses this: it replaces field values from the
|
|
1662
|
+
# server, but a relation's staged additions and removals are not field
|
|
1663
|
+
# values and must survive it (the fetch re-marks the field dirty).
|
|
1664
|
+
def clear_dirty_tracking!
|
|
1619
1665
|
clear_changes_information
|
|
1620
1666
|
# Clear the ACL snapshot used for proper acl_was tracking
|
|
1621
1667
|
@_acl_snapshot_before_change = nil
|
|
@@ -1926,6 +1972,11 @@ module Parse
|
|
|
1926
1972
|
# so a later edit captures a fresh baseline.
|
|
1927
1973
|
@acl = Parse::ACL.typecast(snapshot.as_json, self) if snapshot && !@acl.nil?
|
|
1928
1974
|
@_acl_snapshot_before_change = nil
|
|
1975
|
+
# A relation's staged additions and removals live on its proxy, not in
|
|
1976
|
+
# the attribute ActiveModel restores (after a fetch the restored value
|
|
1977
|
+
# is the proxy with the operations still staged). Drop them so a later
|
|
1978
|
+
# save does not send what was rolled back.
|
|
1979
|
+
clear_proxy_changes!
|
|
1929
1980
|
end
|
|
1930
1981
|
|
|
1931
1982
|
# Keys that mass assignment never applies to an object: the objectId
|
data/lib/parse/model/push.rb
CHANGED
|
@@ -352,7 +352,17 @@ module Parse
|
|
|
352
352
|
if @channels.is_a?(Array) && @channels.empty? == false
|
|
353
353
|
q.where :channels.in => @channels
|
|
354
354
|
end
|
|
355
|
-
|
|
355
|
+
unless q.where.empty?
|
|
356
|
+
compiled = q.compile_rest_where
|
|
357
|
+
# Constraints that compile to nothing would target every
|
|
358
|
+
# installation; refuse instead of widening the audience.
|
|
359
|
+
if compiled.empty?
|
|
360
|
+
raise ArgumentError,
|
|
361
|
+
"The push targeting query has constraints but compiles to no `where`, " \
|
|
362
|
+
"which would reach every installation. Check the targeting constraints."
|
|
363
|
+
end
|
|
364
|
+
msg[:where] = compiled
|
|
365
|
+
end
|
|
356
366
|
elsif @channels.is_a?(Array) && @channels.empty? == false
|
|
357
367
|
msg[:channels] = @channels
|
|
358
368
|
end
|