parse-stack-next 5.8.0 → 5.8.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 +630 -0
- data/README.md +31 -19
- data/docs/caching.md +90 -5
- data/docs/mcp_guide.md +22 -5
- data/docs/mongodb_direct_guide.md +29 -0
- data/docs/webhooks_guide.md +94 -9
- data/lib/parse/acl_scope.rb +71 -9
- data/lib/parse/agent/constraint_translator.rb +22 -21
- data/lib/parse/agent/describe.rb +8 -2
- data/lib/parse/agent/errors.rb +17 -0
- data/lib/parse/agent/mcp_dispatcher.rb +8 -1
- data/lib/parse/agent/mcp_server.rb +47 -6
- data/lib/parse/agent.rb +521 -70
- data/lib/parse/api/batch.rb +70 -2
- data/lib/parse/atlas_search/index_manager.rb +5 -1
- data/lib/parse/atlas_search.rb +50 -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 +263 -11
- data/lib/parse/client/body_builder.rb +100 -15
- data/lib/parse/client/caching.rb +53 -3
- data/lib/parse/client/request.rb +59 -0
- data/lib/parse/client/response.rb +63 -5
- data/lib/parse/client.rb +173 -33
- data/lib/parse/clp_scope.rb +37 -3
- data/lib/parse/live_query/client.rb +142 -11
- 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 +307 -19
- data/lib/parse/model/classes/user.rb +33 -12
- data/lib/parse/model/core/actions.rb +41 -5
- data/lib/parse/model/core/fetching.rb +29 -2
- data/lib/parse/model/core/field_guards.rb +16 -8
- data/lib/parse/model/file.rb +96 -19
- data/lib/parse/model/object.rb +52 -1
- data/lib/parse/model/push.rb +11 -1
- data/lib/parse/mongodb.rb +43 -2
- data/lib/parse/pipeline_security.rb +157 -21
- data/lib/parse/query/constraint.rb +1 -1
- data/lib/parse/query/constraints.rb +665 -41
- data/lib/parse/query.rb +802 -60
- data/lib/parse/stack/tasks.rb +1 -1
- data/lib/parse/stack/version.rb +1 -1
- data/lib/parse/stack.rb +23 -0
- data/lib/parse/vector_search/hybrid.rb +78 -16
- data/lib/parse/vector_search.rb +215 -7
- data/lib/parse/webhooks/payload.rb +82 -8
- data/lib/parse/webhooks/registration.rb +26 -6
- data/lib/parse/webhooks/replay_protection.rb +19 -9
- data/lib/parse/webhooks.rb +329 -13
- 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,235 @@ 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 that returns false cannot tell "already gone" from
|
|
255
|
+
# "denied", so it never resets and forgets nothing for a row the
|
|
256
|
+
# caller could not read; one that returns true removed the row, so
|
|
257
|
+
# the recorded owner (or the rate-limited reset) applies.
|
|
258
|
+
if result.nil?
|
|
259
|
+
_forget_identity!(token.is_a?(String) ? token : nil, nil)
|
|
260
|
+
else
|
|
261
|
+
deleted = result == true
|
|
262
|
+
_forget_identity!(token, owner_id, reset_fallback: deleted, deleted: deleted)
|
|
263
|
+
end
|
|
227
264
|
end
|
|
228
|
-
success
|
|
229
265
|
end
|
|
230
266
|
|
|
267
|
+
# Seconds between two full identity-cache resets triggered by a failed
|
|
268
|
+
# session lookup on the same client.
|
|
269
|
+
RESET_INTERVAL = 5
|
|
270
|
+
|
|
271
|
+
# Process-local record of the last lookup-triggered reset per client.
|
|
272
|
+
@identity_reset_at = {}
|
|
273
|
+
@identity_reset_mutex = Mutex.new
|
|
274
|
+
|
|
275
|
+
class << self
|
|
276
|
+
# Record, in the identity context of `cl` (the client that fetched
|
|
277
|
+
# them), which user owns each session that carries both its objectId
|
|
278
|
+
# and its owner, so a later delete can drop the owner's cached
|
|
279
|
+
# identities after the row is gone. Never raises: it runs while query
|
|
280
|
+
# results are decoded.
|
|
281
|
+
# @param sessions [Array<Parse::Object>]
|
|
282
|
+
# @param cl [Parse::Client]
|
|
283
|
+
# @!visibility private
|
|
284
|
+
def _remember_owners!(sessions, cl)
|
|
285
|
+
auth = cl.respond_to?(:authorization) ? cl.authorization : nil
|
|
286
|
+
return unless auth.respond_to?(:remember_session_owner)
|
|
287
|
+
Array(sessions).each do |o|
|
|
288
|
+
next unless o.is_a?(Parse::Session)
|
|
289
|
+
owner_id = o.send(:_identity_owner_id)
|
|
290
|
+
id = o.id
|
|
291
|
+
auth.remember_session_owner(id, owner_id) if owner_id && id.present?
|
|
292
|
+
end
|
|
293
|
+
nil
|
|
294
|
+
rescue StandardError
|
|
295
|
+
nil
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
private
|
|
299
|
+
|
|
300
|
+
# Reset `cl`'s identity and role caches, at most once every
|
|
301
|
+
# {RESET_INTERVAL} seconds per client.
|
|
302
|
+
# @!visibility private
|
|
303
|
+
def _rate_limited_identity_reset!(cl)
|
|
304
|
+
auth = cl.respond_to?(:authorization) ? cl.authorization : nil
|
|
305
|
+
return unless auth.respond_to?(:reset_caches!)
|
|
306
|
+
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
307
|
+
due = @identity_reset_mutex.synchronize do
|
|
308
|
+
last = @identity_reset_at[cl.object_id]
|
|
309
|
+
next false if last && now - last < RESET_INTERVAL
|
|
310
|
+
@identity_reset_at[cl.object_id] = now
|
|
311
|
+
true
|
|
312
|
+
end
|
|
313
|
+
auth.reset_caches! if due
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
# Look up the token and owner of every session about to be deleted that
|
|
317
|
+
# does not carry them, so the delete can drop their identity entries.
|
|
318
|
+
# One `_Session` query per client. When the delete runs as a user
|
|
319
|
+
# (`session_token`), the lookup runs as that user too, so it only
|
|
320
|
+
# reads sessions that user can see. Otherwise a client with a master
|
|
321
|
+
# key reads it as SDK metadata (it works inside
|
|
322
|
+
# `Parse.without_master_key`), and one without skips the lookup. The
|
|
323
|
+
# query never uses the response cache: its rows carry live session
|
|
324
|
+
# tokens.
|
|
325
|
+
#
|
|
326
|
+
# A row the lookup did not return is marked absent: there is nothing
|
|
327
|
+
# to forget for it. A row a session-scoped lookup could not see is
|
|
328
|
+
# marked `:invisible`: a denied or not-found delete of it forgets
|
|
329
|
+
# nothing and never resets, so a user deleting session ids they cannot
|
|
330
|
+
# read cannot evict other users' cached identities. A delete of it
|
|
331
|
+
# that succeeds removed a session the caller was allowed to delete, so
|
|
332
|
+
# its recorded owner (or the rate-limited reset) is used. Only
|
|
333
|
+
# a lookup that raised marks its sessions `:unknown`, which makes
|
|
334
|
+
# their delete reset the client's identity cache (rate limited).
|
|
335
|
+
# @param sessions [Array<Parse::Object>]
|
|
336
|
+
# @param session_token [String, nil] the delete's own session.
|
|
337
|
+
# @!visibility private
|
|
338
|
+
def _preload_identity_for_destroy!(sessions, session_token: nil)
|
|
339
|
+
pending = sessions.select { |o| o.is_a?(Parse::Session) && o.send(:_identity_lookup_needed?) }
|
|
340
|
+
return if pending.empty?
|
|
341
|
+
pending.group_by(&:client).each do |cl, group|
|
|
342
|
+
has_master = cl.respond_to?(:master_key) && cl.master_key.present?
|
|
343
|
+
token = session_token.respond_to?(:session_token) ? session_token.session_token : session_token
|
|
344
|
+
token = nil unless token.is_a?(String) && !token.strip.empty?
|
|
345
|
+
unless has_master || token
|
|
346
|
+
group.each { |o| o.instance_variable_set(:@_identity_for_destroy, :absent) }
|
|
347
|
+
next
|
|
348
|
+
end
|
|
349
|
+
ids = group.map(&:id).uniq
|
|
350
|
+
scoped = !token.nil?
|
|
351
|
+
found = begin
|
|
352
|
+
query = Parse::Session.query(:objectId.in => ids, limit: ids.size)
|
|
353
|
+
query.keys(:session_token, :user)
|
|
354
|
+
query.client = cl
|
|
355
|
+
query.cache = false
|
|
356
|
+
if scoped
|
|
357
|
+
query.session_token = token
|
|
358
|
+
else
|
|
359
|
+
query.instance_variable_set(:@_metadata_master, true)
|
|
360
|
+
end
|
|
361
|
+
query.results.to_h do |row|
|
|
362
|
+
owner = row.instance_variable_get(:@user)
|
|
363
|
+
[row.id, [row.instance_variable_get(:@session_token), owner.respond_to?(:id) ? owner.id : nil]]
|
|
364
|
+
end
|
|
365
|
+
rescue StandardError => e
|
|
366
|
+
warn "[Parse::Session] could not look up the token and owner of " \
|
|
367
|
+
"#{ids.size} session(s) before deleting them (#{e.class}); " \
|
|
368
|
+
"dropping the whole identity cache instead."
|
|
369
|
+
nil
|
|
370
|
+
end
|
|
371
|
+
group.each do |o|
|
|
372
|
+
mark = if found.nil?
|
|
373
|
+
:unknown
|
|
374
|
+
elsif (row = found[o.id]) && (row[0].is_a?(String) || row[1])
|
|
375
|
+
row
|
|
376
|
+
elsif scoped
|
|
377
|
+
:invisible
|
|
378
|
+
else
|
|
379
|
+
:absent
|
|
380
|
+
end
|
|
381
|
+
o.instance_variable_set(:@_identity_for_destroy, mark)
|
|
382
|
+
end
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
# Called by `Array#destroy` for each session in the batch with its
|
|
388
|
+
# response. The entries are dropped only when the delete succeeded or
|
|
389
|
+
# reported the row gone ("object not found").
|
|
390
|
+
# @!visibility private
|
|
391
|
+
def _after_batch_destroy(response = nil)
|
|
392
|
+
identity = _identity_for_destroy
|
|
393
|
+
_clear_identity_for_destroy!
|
|
394
|
+
return unless Parse::Session.send(:_destroy_applied?, response)
|
|
395
|
+
deleted = response.nil? || (response.respond_to?(:success?) && response.success?)
|
|
396
|
+
# A row the caller could not read counts only when the delete itself
|
|
397
|
+
# succeeded: "object not found" there may mean "not yours".
|
|
398
|
+
return if identity[0] == :invisible && !deleted
|
|
399
|
+
# The batch response tells success and "object not found" apart from
|
|
400
|
+
# a denial, so an absent row whose owner was never recorded may fall
|
|
401
|
+
# back to the rate-limited reset here.
|
|
402
|
+
_forget_identity!(*identity, reset_fallback: true, deleted: deleted)
|
|
403
|
+
end
|
|
404
|
+
private :_after_batch_destroy
|
|
405
|
+
|
|
406
|
+
# Whether a batch delete response means the row is gone: success, or
|
|
407
|
+
# "object not found". A missing response (older callers) counts as
|
|
408
|
+
# applied.
|
|
409
|
+
# @!visibility private
|
|
410
|
+
def self._destroy_applied?(response)
|
|
411
|
+
return true if response.nil?
|
|
412
|
+
(response.respond_to?(:success?) && response.success?) ||
|
|
413
|
+
(response.respond_to?(:object_not_found?) && response.object_not_found?)
|
|
414
|
+
end
|
|
415
|
+
private_class_method :_destroy_applied?
|
|
416
|
+
|
|
417
|
+
# Remove the token and owner {._preload_identity_for_destroy!} looked
|
|
418
|
+
# up. The token is a live credential and must not outlive the delete
|
|
419
|
+
# (it would show in `inspect` or `instance_variables`).
|
|
420
|
+
# @!visibility private
|
|
421
|
+
def _clear_identity_for_destroy!
|
|
422
|
+
remove_instance_variable(:@_identity_for_destroy) if instance_variable_defined?(:@_identity_for_destroy)
|
|
423
|
+
end
|
|
424
|
+
private :_clear_identity_for_destroy!
|
|
425
|
+
|
|
426
|
+
# Whether this session lacks the token or the owner its identity-plane
|
|
427
|
+
# entries are keyed by: a fetch without the master key (no token) or
|
|
428
|
+
# with `keys:` leaving them out.
|
|
429
|
+
# @!visibility private
|
|
430
|
+
def _identity_lookup_needed?
|
|
431
|
+
# Keyed on the objectId, not `new?`: `Array#destroy` deletes by id
|
|
432
|
+
# alone, so a session built from an id (no timestamps loaded) is still
|
|
433
|
+
# deleted and its token must still be dropped.
|
|
434
|
+
return false if @id.blank?
|
|
435
|
+
!@session_token.is_a?(String) || @session_token.empty? || _identity_owner_id.nil?
|
|
436
|
+
end
|
|
437
|
+
private :_identity_lookup_needed?
|
|
438
|
+
|
|
231
439
|
# Serialization omits `sessionToken` unless `include_session_token: true`
|
|
232
440
|
# is passed. A session token is a bearer credential, and `as_json` is the
|
|
233
441
|
# surface that reaches logs, API responses and agent tool output.
|
|
@@ -245,7 +453,87 @@ module Parse
|
|
|
245
453
|
# Redacts the session token from the default inspect output.
|
|
246
454
|
# @return [String]
|
|
247
455
|
def inspect
|
|
248
|
-
|
|
456
|
+
looked_up = @_identity_for_destroy
|
|
457
|
+
Parse::User.redact_session_token(super, @session_token, looked_up.is_a?(Array) ? looked_up[0] : nil)
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
private
|
|
461
|
+
|
|
462
|
+
# The token and owner id to forget for this delete: what this instance
|
|
463
|
+
# carries, completed by {._preload_identity_for_destroy!} when it did
|
|
464
|
+
# not, or `:unknown` when that lookup failed.
|
|
465
|
+
# @!visibility private
|
|
466
|
+
def _identity_for_destroy
|
|
467
|
+
looked_up = @_identity_for_destroy
|
|
468
|
+
token = @session_token.is_a?(String) && !@session_token.empty? ? @session_token : nil
|
|
469
|
+
owner_id = _identity_owner_id
|
|
470
|
+
return [:unknown, owner_id] if looked_up == :unknown
|
|
471
|
+
# A session-scoped lookup could not see the row: forget what this
|
|
472
|
+
# instance carries; with nothing at all, mark it invisible so only a
|
|
473
|
+
# delete that succeeds may use the recorded owner or the reset.
|
|
474
|
+
return (token || owner_id ? [token, owner_id] : [:invisible, nil]) if looked_up == :invisible
|
|
475
|
+
if looked_up == :absent
|
|
476
|
+
# No row to read: the session is gone or not visible. Forget what
|
|
477
|
+
# this instance carries; with nothing at all, mark it absent so the
|
|
478
|
+
# delete can fall back to the recorded owner.
|
|
479
|
+
return token || owner_id ? [token, owner_id] : [:absent, nil]
|
|
480
|
+
end
|
|
481
|
+
if looked_up.is_a?(Array)
|
|
482
|
+
token ||= looked_up[0]
|
|
483
|
+
owner_id ||= looked_up[1]
|
|
484
|
+
end
|
|
485
|
+
[token, owner_id]
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
# The owning user's id, read from the association ivar directly:
|
|
489
|
+
# calling `user` on a partially fetched session could trigger an
|
|
490
|
+
# autofetch just to learn the owner.
|
|
491
|
+
# @!visibility private
|
|
492
|
+
def _identity_owner_id
|
|
493
|
+
owner = @user
|
|
494
|
+
owner.respond_to?(:id) ? owner.id : nil
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
# Drop the token and the owner's entries from this session's client's
|
|
498
|
+
# identity plane.
|
|
499
|
+
# @!visibility private
|
|
500
|
+
def _forget_identity!(token, owner_id, reset_fallback: false, deleted: false)
|
|
501
|
+
cl = client
|
|
502
|
+
if token == :invisible
|
|
503
|
+
# The caller could not read the row. Only a delete that succeeded
|
|
504
|
+
# touches other identities: the recorded owner, else the
|
|
505
|
+
# rate-limited reset. A denied or not-found delete forgets nothing.
|
|
506
|
+
return unless deleted
|
|
507
|
+
token = :absent
|
|
508
|
+
end
|
|
509
|
+
if token == :absent
|
|
510
|
+
# The row was not readable before the delete. Use the owner recorded
|
|
511
|
+
# when this session was last loaded; without one, and only when the
|
|
512
|
+
# delete is known to have applied or found the row gone, fall back to
|
|
513
|
+
# the rate-limited reset. Never on a denied delete.
|
|
514
|
+
token = nil
|
|
515
|
+
auth = cl.respond_to?(:authorization) ? cl.authorization : nil
|
|
516
|
+
recorded = auth.respond_to?(:session_owner) ? auth.session_owner(@id) : nil
|
|
517
|
+
if recorded
|
|
518
|
+
owner_id ||= recorded
|
|
519
|
+
elsif owner_id.nil? && reset_fallback
|
|
520
|
+
Parse::Session.send(:_rate_limited_identity_reset!, cl)
|
|
521
|
+
end
|
|
522
|
+
end
|
|
523
|
+
if token == :unknown
|
|
524
|
+
# The lookup failed, so the token's entry cannot be named. Drop every
|
|
525
|
+
# cached identity on this client (the next read of each token
|
|
526
|
+
# re-resolves it, which is safe), at most once every RESET_INTERVAL
|
|
527
|
+
# seconds so repeated failures cannot flush a shared plane on every
|
|
528
|
+
# request. The owner's entries go as well when the owner is known.
|
|
529
|
+
Parse::Session.send(:_rate_limited_identity_reset!, cl)
|
|
530
|
+
token = nil
|
|
531
|
+
end
|
|
532
|
+
cl.invalidate_session_identity(token) if token.is_a?(String) && cl.respond_to?(:invalidate_session_identity)
|
|
533
|
+
cl.invalidate_user_identity(owner_id) if owner_id && cl.respond_to?(:invalidate_user_identity)
|
|
534
|
+
rescue StandardError
|
|
535
|
+
# Runs from an `ensure`: never replace the delete's own outcome.
|
|
536
|
+
nil
|
|
249
537
|
end
|
|
250
538
|
end
|
|
251
539
|
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.
|
|
@@ -432,11 +432,20 @@ module Parse
|
|
|
432
432
|
# default; enable it only for writes that are safe to repeat (it
|
|
433
433
|
# works around Parse Server running a transaction's requests
|
|
434
434
|
# concurrently, which MongoDB intermittently rejects).
|
|
435
|
+
# @param session [String, #session_token, nil] run the transaction as
|
|
436
|
+
# this user (ACL and CLP enforced). Without it, the transaction runs
|
|
437
|
+
# with the credentials of its objects' class client, as a single
|
|
438
|
+
# save does.
|
|
435
439
|
# @yield [Parse::BatchOperation] the batch operation to add requests to
|
|
436
440
|
# @return [Array<Parse::Response>] the responses from the transaction
|
|
437
441
|
# @raise [Parse::Error] if the transaction fails
|
|
438
|
-
|
|
442
|
+
# @raise [Parse::BatchOperation::MixedAuthorityError] if the objects
|
|
443
|
+
# are bound to clients with different credentials. Parse Server runs
|
|
444
|
+
# a transaction under one credential, so it is refused before
|
|
445
|
+
# anything is sent.
|
|
446
|
+
def transaction(retries: 5, retry_server_errors: false, session: nil, &block)
|
|
439
447
|
raise ArgumentError, "Block required for transaction" unless block_given?
|
|
448
|
+
session_token = Parse::BatchOperation.session_token_for!(session)
|
|
440
449
|
|
|
441
450
|
previous_context = Fiber[TRANSACTION_CONTEXT_KEY]
|
|
442
451
|
transaction_context = { snapshots: {}, created_objects: {} }
|
|
@@ -490,6 +499,10 @@ module Parse
|
|
|
490
499
|
result.each { |obj| batch_wrapper.add(obj) if obj.respond_to?(:change_requests) }
|
|
491
500
|
end
|
|
492
501
|
|
|
502
|
+
if session_token
|
|
503
|
+
batch.requests.each { |r| r.opts[:session_token] = session_token if r.opts.is_a?(Hash) }
|
|
504
|
+
end
|
|
505
|
+
|
|
493
506
|
# Submit with retry logic for transaction conflicts.
|
|
494
507
|
# Parse Server reports a write conflict inside a transaction as
|
|
495
508
|
# error code 251, after rolling it back. Retry on that structured
|
|
@@ -554,7 +567,13 @@ module Parse
|
|
|
554
567
|
|
|
555
568
|
# Apply any additional attributes returned by beforeSave hooks.
|
|
556
569
|
obj.set_attributes!(result) if obj.respond_to?(:set_attributes!)
|
|
557
|
-
|
|
570
|
+
# The transaction carried the record's relation ops too, so
|
|
571
|
+
# settle them as a save does rather than discarding them.
|
|
572
|
+
if obj.respond_to?(:changes_applied!, true)
|
|
573
|
+
obj.send(:changes_applied!)
|
|
574
|
+
elsif obj.respond_to?(:clear_changes!, true)
|
|
575
|
+
obj.send(:clear_changes!)
|
|
576
|
+
end
|
|
558
577
|
end
|
|
559
578
|
|
|
560
579
|
return responses
|
|
@@ -1108,6 +1127,8 @@ module Parse
|
|
|
1108
1127
|
# @param op_hash [Hash] The operation hash. It may also be of type {Parse::RelationAction}.
|
|
1109
1128
|
# @return [Boolean] whether the operation was successful.
|
|
1110
1129
|
def operate_field!(field, op_hash)
|
|
1130
|
+
# A reply from an earlier operation must never be read as this one's.
|
|
1131
|
+
@_last_operation_result = nil
|
|
1111
1132
|
field = field.to_sym
|
|
1112
1133
|
field = self.field_map[field] || field
|
|
1113
1134
|
if op_hash.is_a?(Parse::RelationAction)
|
|
@@ -1132,9 +1153,12 @@ module Parse
|
|
|
1132
1153
|
|
|
1133
1154
|
# @!visibility private
|
|
1134
1155
|
# The value Parse Server returned for `field` from the last
|
|
1135
|
-
# {#operate_field!}, or nil when the reply did not include it.
|
|
1156
|
+
# {#operate_field!}, or nil when the reply did not include it. The
|
|
1157
|
+
# reply is read once: it is forgotten here so a later read cannot
|
|
1158
|
+
# pick up a stale value.
|
|
1136
1159
|
def _last_operation_value(field)
|
|
1137
1160
|
result = @_last_operation_result
|
|
1161
|
+
@_last_operation_result = nil
|
|
1138
1162
|
return nil unless result.is_a?(Hash)
|
|
1139
1163
|
wire = (self.field_map[field.to_sym] || field).to_s
|
|
1140
1164
|
result[wire]
|
|
@@ -1198,7 +1222,9 @@ module Parse
|
|
|
1198
1222
|
def op_add_relation!(field, objects = [])
|
|
1199
1223
|
objects = [objects] unless objects.is_a?(Array)
|
|
1200
1224
|
return false if objects.empty?
|
|
1201
|
-
|
|
1225
|
+
# The operation is keyed by the remote column, as a save sends it.
|
|
1226
|
+
remote_field = self.field_map[field.to_sym] || field
|
|
1227
|
+
relation_action = Parse::RelationAction.new(remote_field, polarity: true, objects: objects)
|
|
1202
1228
|
operate_field! field, relation_action
|
|
1203
1229
|
end
|
|
1204
1230
|
|
|
@@ -1210,7 +1236,9 @@ module Parse
|
|
|
1210
1236
|
def op_remove_relation!(field, objects = [])
|
|
1211
1237
|
objects = [objects] unless objects.is_a?(Array)
|
|
1212
1238
|
return false if objects.empty?
|
|
1213
|
-
|
|
1239
|
+
# The operation is keyed by the remote column, as a save sends it.
|
|
1240
|
+
remote_field = self.field_map[field.to_sym] || field
|
|
1241
|
+
relation_action = Parse::RelationAction.new(remote_field, polarity: false, objects: objects)
|
|
1214
1242
|
operate_field! field, relation_action
|
|
1215
1243
|
end
|
|
1216
1244
|
|
|
@@ -1251,6 +1279,9 @@ module Parse
|
|
|
1251
1279
|
uri = self.uri_path
|
|
1252
1280
|
r = Request.new(:delete, uri)
|
|
1253
1281
|
r.tag = object_id
|
|
1282
|
+
# A batch sends this through the class's client (and its bound
|
|
1283
|
+
# session), as a single destroy does.
|
|
1284
|
+
r.client = client
|
|
1254
1285
|
r
|
|
1255
1286
|
end
|
|
1256
1287
|
|
|
@@ -1261,6 +1292,8 @@ module Parse
|
|
|
1261
1292
|
|
|
1262
1293
|
# Creates an array of all possible operations that need to be performed
|
|
1263
1294
|
# on this object. This includes all property and relational operation changes.
|
|
1295
|
+
# Each request carries this class's client, so a batch sends it with the
|
|
1296
|
+
# same credentials as a single {#save}.
|
|
1264
1297
|
#
|
|
1265
1298
|
# This is the path batch saves ({Array#save}) and
|
|
1266
1299
|
# {Parse::Object.transaction} use, so it applies the same save-time
|
|
@@ -1294,6 +1327,7 @@ module Parse
|
|
|
1294
1327
|
body[Parse::Model::OBJECT_ID] = @id if @id.present?
|
|
1295
1328
|
r = Request.new(:post, uri, body: body)
|
|
1296
1329
|
r.tag = object_id
|
|
1330
|
+
r.client = client
|
|
1297
1331
|
requests << r
|
|
1298
1332
|
end
|
|
1299
1333
|
return requests
|
|
@@ -1303,6 +1337,7 @@ module Parse
|
|
|
1303
1337
|
if attribute_changes? || force
|
|
1304
1338
|
r = Request.new(:put, uri, body: attribute_updates)
|
|
1305
1339
|
r.tag = object_id
|
|
1340
|
+
r.client = client
|
|
1306
1341
|
requests << r
|
|
1307
1342
|
end
|
|
1308
1343
|
|
|
@@ -1313,6 +1348,7 @@ module Parse
|
|
|
1313
1348
|
next if ops.empty?
|
|
1314
1349
|
r = Request.new(:put, uri, body: ops)
|
|
1315
1350
|
r.tag = object_id
|
|
1351
|
+
r.client = client
|
|
1316
1352
|
requests << r
|
|
1317
1353
|
end
|
|
1318
1354
|
end
|
|
@@ -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
|