parse-stack-next 5.7.6 → 5.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +1148 -0
- data/README.md +39 -23
- data/docs/TEST_SERVER.md +2 -2
- data/docs/acl_clp_guide.md +7 -0
- data/docs/atlas_vector_search_guide.md +181 -13
- data/docs/caching.md +50 -0
- data/docs/client_sdk_guide.md +11 -0
- data/docs/mcp_guide.md +332 -9
- data/docs/mongodb_direct_guide.md +27 -0
- data/docs/usage_guide.md +38 -0
- data/docs/webhooks_guide.md +126 -17
- data/lib/parse/acl_scope.rb +230 -50
- data/lib/parse/agent/approval_gate.rb +0 -0
- data/lib/parse/agent/constraint_translator.rb +42 -15
- data/lib/parse/agent/describe.rb +11 -3
- data/lib/parse/agent/field_names.rb +53 -0
- data/lib/parse/agent/field_policy.rb +74 -0
- data/lib/parse/agent/mcp_deployments.rb +426 -0
- data/lib/parse/agent/mcp_dispatcher.rb +8 -1
- data/lib/parse/agent/mcp_rack_app.rb +424 -45
- data/lib/parse/agent/mcp_server.rb +23 -1
- data/lib/parse/agent/mcp_subscriptions.rb +124 -6
- data/lib/parse/agent/metadata_registry.rb +67 -8
- data/lib/parse/agent/prompt_hardening.rb +9 -3
- data/lib/parse/agent/tools.rb +378 -29
- data/lib/parse/agent.rb +298 -21
- data/lib/parse/api/batch.rb +10 -1
- data/lib/parse/api/schema.rb +23 -4
- data/lib/parse/api/sessions.rb +6 -2
- data/lib/parse/api/users.rb +88 -14
- data/lib/parse/atlas_search/index_manager.rb +5 -1
- data/lib/parse/atlas_search/protected_paths.rb +236 -0
- data/lib/parse/atlas_search.rb +144 -24
- data/lib/parse/authorization.rb +289 -4
- data/lib/parse/cache/sub_cache.rb +26 -0
- data/lib/parse/client/authentication.rb +19 -1
- data/lib/parse/client/batch.rb +264 -35
- data/lib/parse/client/body_builder.rb +21 -0
- data/lib/parse/client/caching.rb +371 -27
- data/lib/parse/client/request.rb +26 -14
- data/lib/parse/client/response.rb +49 -6
- data/lib/parse/client.rb +227 -40
- data/lib/parse/clp_scope.rb +317 -25
- data/lib/parse/console.rb +2 -2
- data/lib/parse/embeddings/voyage.rb +181 -17
- data/lib/parse/graphql/type_generator.rb +3 -0
- data/lib/parse/live_query/client.rb +72 -2
- data/lib/parse/lock_backend.rb +4 -3
- data/lib/parse/model/acl.rb +119 -21
- data/lib/parse/model/associations/belongs_to.rb +25 -3
- data/lib/parse/model/associations/collection_proxy.rb +146 -17
- data/lib/parse/model/associations/has_many.rb +49 -9
- data/lib/parse/model/associations/has_one.rb +3 -1
- data/lib/parse/model/associations/pointer_collection_proxy.rb +145 -17
- data/lib/parse/model/associations/relation_collection_proxy.rb +229 -38
- data/lib/parse/model/bytes.rb +13 -5
- data/lib/parse/model/classes/role.rb +95 -0
- data/lib/parse/model/classes/session.rb +304 -1
- data/lib/parse/model/classes/user.rb +104 -8
- data/lib/parse/model/core/actions.rb +287 -70
- data/lib/parse/model/core/builder.rb +100 -8
- data/lib/parse/model/core/create_lock.rb +27 -2
- data/lib/parse/model/core/describe.rb +2 -0
- data/lib/parse/model/core/fetching.rb +50 -5
- data/lib/parse/model/core/field_guards.rb +16 -8
- data/lib/parse/model/core/pluralized_aliases.rb +8 -4
- data/lib/parse/model/core/properties.rb +488 -39
- data/lib/parse/model/core/querying.rb +7 -0
- data/lib/parse/model/core/schema.rb +5 -3
- data/lib/parse/model/core/search_indexing.rb +63 -0
- data/lib/parse/model/core/vector_searchable.rb +35 -6
- data/lib/parse/model/file.rb +9 -2
- data/lib/parse/model/geopoint.rb +61 -13
- data/lib/parse/model/model.rb +160 -9
- data/lib/parse/model/object.rb +317 -18
- data/lib/parse/model/phone.rb +54 -5
- data/lib/parse/model/pointer.rb +40 -6
- data/lib/parse/model/push.rb +11 -1
- data/lib/parse/mongodb.rb +189 -61
- data/lib/parse/pipeline_security.rb +415 -26
- data/lib/parse/query/constraint.rb +31 -1
- data/lib/parse/query/constraints.rb +89 -59
- data/lib/parse/query/cursor.rb +3 -1
- data/lib/parse/query/operation.rb +62 -8
- data/lib/parse/query/ordering.rb +34 -6
- data/lib/parse/query.rb +1871 -167
- data/lib/parse/retrieval/agent_tool.rb +225 -8
- data/lib/parse/retrieval/benchmark.rb +149 -0
- data/lib/parse/retrieval/profiles.rb +320 -0
- data/lib/parse/retrieval/retriever.rb +10 -1
- data/lib/parse/retrieval.rb +2 -0
- data/lib/parse/schema/search_index_migrator.rb +23 -5
- data/lib/parse/schema.rb +74 -18
- data/lib/parse/stack/tasks.rb +6 -4
- data/lib/parse/stack/version.rb +1 -1
- data/lib/parse/stack.rb +91 -14
- data/lib/parse/two_factor_auth/user_extension.rb +14 -2
- data/lib/parse/two_factor_auth.rb +11 -0
- data/lib/parse/vector_search/hybrid.rb +110 -33
- data/lib/parse/vector_search/index_definition.rb +237 -0
- data/lib/parse/vector_search.rb +259 -23
- data/lib/parse/webhooks/payload.rb +136 -6
- data/lib/parse/webhooks/replay_protection.rb +58 -20
- data/lib/parse/webhooks.rb +715 -40
- metadata +8 -1
|
@@ -34,8 +34,10 @@ module Parse
|
|
|
34
34
|
end
|
|
35
35
|
hash_constraints = constraints(true)
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
# One call: with a block it saves what the block modifies; without one
|
|
38
|
+
# it force-saves every match. (Previously a block call also ran a
|
|
39
|
+
# second, forced pass over every matching record.)
|
|
40
|
+
klass.save_all(hash_constraints, &block)
|
|
39
41
|
end
|
|
40
42
|
end
|
|
41
43
|
|
|
@@ -99,6 +101,8 @@ module Parse
|
|
|
99
101
|
# late for this: the public API documents mutating an object and adding it
|
|
100
102
|
# afterwards.
|
|
101
103
|
TRANSACTION_CONTEXT_KEY = :__parse_transaction_context__
|
|
104
|
+
# Parse Server error code for a transaction write conflict.
|
|
105
|
+
TRANSACTION_CONFLICT_CODE = 251
|
|
102
106
|
|
|
103
107
|
# Distinguishes a property whose ivar did not exist from one explicitly
|
|
104
108
|
# set to nil. Rollback removes the former instead of defining it as nil.
|
|
@@ -416,11 +420,22 @@ module Parse
|
|
|
416
420
|
# [user1, user2] # Return array of objects to save
|
|
417
421
|
# end
|
|
418
422
|
#
|
|
419
|
-
#
|
|
423
|
+
# Transactions need MongoDB running as a replica set (or mongos) on
|
|
424
|
+
# the Parse Server side.
|
|
425
|
+
#
|
|
426
|
+
# @param retries [Integer] number of attempts on a transaction conflict
|
|
427
|
+
# (error 251), which Parse Server reports only after rolling back.
|
|
428
|
+
# @param retry_server_errors [Boolean] also resend after a bare HTTP 500
|
|
429
|
+
# from Parse Server. Parse Server answers both an aborted transaction
|
|
430
|
+
# and a failed commit this way, and a commit can fail after it was
|
|
431
|
+
# applied, so a resend can apply the transaction twice. Off by
|
|
432
|
+
# default; enable it only for writes that are safe to repeat (it
|
|
433
|
+
# works around Parse Server running a transaction's requests
|
|
434
|
+
# concurrently, which MongoDB intermittently rejects).
|
|
420
435
|
# @yield [Parse::BatchOperation] the batch operation to add requests to
|
|
421
436
|
# @return [Array<Parse::Response>] the responses from the transaction
|
|
422
437
|
# @raise [Parse::Error] if the transaction fails
|
|
423
|
-
def transaction(retries: 5, &block)
|
|
438
|
+
def transaction(retries: 5, retry_server_errors: false, &block)
|
|
424
439
|
raise ArgumentError, "Block required for transaction" unless block_given?
|
|
425
440
|
|
|
426
441
|
previous_context = Fiber[TRANSACTION_CONTEXT_KEY]
|
|
@@ -476,10 +491,45 @@ module Parse
|
|
|
476
491
|
end
|
|
477
492
|
|
|
478
493
|
# Submit with retry logic for transaction conflicts.
|
|
494
|
+
# Parse Server reports a write conflict inside a transaction as
|
|
495
|
+
# error code 251, after rolling it back. Retry on that structured
|
|
496
|
+
# code only (a per-request response code, or the code of the
|
|
497
|
+
# response an error carries).
|
|
479
498
|
attempts = 0
|
|
480
|
-
|
|
499
|
+
loop do
|
|
481
500
|
attempts += 1
|
|
482
|
-
|
|
501
|
+
begin
|
|
502
|
+
responses = batch.submit
|
|
503
|
+
rescue Parse::Error => e
|
|
504
|
+
# A conflict is recognized only from the structured Parse
|
|
505
|
+
# error code in the response, never from message text, so an
|
|
506
|
+
# unrelated "251" in an error message cannot trigger a resend.
|
|
507
|
+
conflict = e.respond_to?(:response) && e.response.respond_to?(:code) &&
|
|
508
|
+
e.response.code.to_i == TRANSACTION_CONFLICT_CODE
|
|
509
|
+
# A bare 500 does not say whether the transaction was applied:
|
|
510
|
+
# Parse Server answers an aborted transaction and a failed
|
|
511
|
+
# commit (which may have been applied) the same way, and a
|
|
512
|
+
# 502/503/504 comes from a gateway that may have timed out
|
|
513
|
+
# after the commit. Only a 251 conflict is a confirmed
|
|
514
|
+
# rollback; a 500 is resent only when the caller opted in.
|
|
515
|
+
server_500 = e.is_a?(Parse::Error::ServiceUnavailableError) && e.http_status == 500
|
|
516
|
+
if (conflict || (server_500 && retry_server_errors)) && attempts < retries
|
|
517
|
+
sleep(0.1 * attempts)
|
|
518
|
+
next
|
|
519
|
+
end
|
|
520
|
+
# Name the likely causes rather than surfacing only "Internal
|
|
521
|
+
# server error". The original error is the cause.
|
|
522
|
+
if server_500
|
|
523
|
+
raise Parse::Error,
|
|
524
|
+
"Transaction failed: Parse Server answered 500 (#{e.message}) after " \
|
|
525
|
+
"#{attempts} attempt(s). The transaction may or may not have been applied; " \
|
|
526
|
+
"check before resending. Parse Server transactions need MongoDB running as " \
|
|
527
|
+
"a replica set or mongos, and on a standalone server every transaction fails " \
|
|
528
|
+
"this way; otherwise see the Parse Server log. For a non-atomic batch, save " \
|
|
529
|
+
"the objects with Array#save instead."
|
|
530
|
+
end
|
|
531
|
+
raise
|
|
532
|
+
end
|
|
483
533
|
|
|
484
534
|
if responses.all?(&:success?)
|
|
485
535
|
# Match responses to objects using the request tag (Ruby object_id).
|
|
@@ -504,20 +554,26 @@ module Parse
|
|
|
504
554
|
|
|
505
555
|
# Apply any additional attributes returned by beforeSave hooks.
|
|
506
556
|
obj.set_attributes!(result) if obj.respond_to?(:set_attributes!)
|
|
507
|
-
|
|
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
|
|
508
564
|
end
|
|
509
565
|
|
|
510
566
|
return responses
|
|
511
567
|
end
|
|
512
568
|
|
|
513
569
|
error_response = responses.find { |response| !response.success? }
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
if e.message.include?("251") && attempts < retries
|
|
570
|
+
error_code = error_response&.code.to_i
|
|
571
|
+
if error_code == TRANSACTION_CONFLICT_CODE && attempts < retries
|
|
517
572
|
sleep(0.1 * attempts)
|
|
518
|
-
|
|
573
|
+
next
|
|
519
574
|
end
|
|
520
|
-
|
|
575
|
+
detail = error_response&.error || "unknown error"
|
|
576
|
+
raise Parse::Error, "Transaction failed (code #{error_code}): #{detail}"
|
|
521
577
|
end
|
|
522
578
|
rescue StandardError
|
|
523
579
|
original_states.each_value do |state|
|
|
@@ -989,59 +1045,60 @@ module Parse
|
|
|
989
1045
|
iterator_block = nil
|
|
990
1046
|
if block_given?
|
|
991
1047
|
iterator_block = block
|
|
992
|
-
force ||= false
|
|
993
1048
|
else
|
|
994
1049
|
# if no block given, assume you want to just save all objects
|
|
995
1050
|
# regardless of modification.
|
|
996
1051
|
force = true
|
|
997
1052
|
end
|
|
998
|
-
# Only generate the comparison block once.
|
|
999
|
-
# updated_comparison_block = Proc.new { |x| x.updated_at }
|
|
1000
1053
|
|
|
1054
|
+
# Work on a copy so the caller's constraints hash is not mutated.
|
|
1055
|
+
base_constraints = constraints.dup
|
|
1001
1056
|
anchor_date = Parse::Date.now
|
|
1002
|
-
|
|
1003
|
-
|
|
1057
|
+
base_constraints[:updated_at.on_or_before] = anchor_date
|
|
1058
|
+
base_constraints[:cache] = false
|
|
1004
1059
|
# oldest first, so we create a reduction-cycle
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
cursor
|
|
1060
|
+
base_constraints[:order] = :updated_at.asc
|
|
1061
|
+
base_constraints[:limit] = batch_size
|
|
1062
|
+
|
|
1063
|
+
# Keyset cursor over (updated_at, objectId). Saved objects move past
|
|
1064
|
+
# the anchor date and drop out of the query. Objects the block left
|
|
1065
|
+
# unchanged keep their updated_at, so each page resumes at the last
|
|
1066
|
+
# seen updated_at and excludes the ids already visited at exactly
|
|
1067
|
+
# that timestamp. Every page therefore makes progress, and records
|
|
1068
|
+
# the block does not modify are visited once instead of ending the
|
|
1069
|
+
# run early.
|
|
1070
|
+
cursor_time = nil
|
|
1071
|
+
seen_at_cursor = []
|
|
1009
1072
|
has_errors = false
|
|
1010
1073
|
loop do
|
|
1011
|
-
|
|
1074
|
+
page_constraints = base_constraints.dup
|
|
1075
|
+
if cursor_time
|
|
1076
|
+
page_constraints[:updated_at.gte] = cursor_time
|
|
1077
|
+
page_constraints[:objectId.nin] = seen_at_cursor if seen_at_cursor.any?
|
|
1078
|
+
end
|
|
1079
|
+
results = query(page_constraints).results
|
|
1012
1080
|
|
|
1013
1081
|
break if results.empty?
|
|
1014
1082
|
|
|
1015
|
-
# verify we didn't get duplicates fetches
|
|
1016
|
-
if cursor.is_a?(Parse::Object) && results.any? { |x| x.id == cursor.id }
|
|
1017
|
-
warn "[#{self}.save_all] Unbounded update detected with id #{cursor.id}."
|
|
1018
|
-
has_errors = true
|
|
1019
|
-
break cursor
|
|
1020
|
-
end
|
|
1021
|
-
|
|
1022
1083
|
results.each(&iterator_block) if iterator_block.present?
|
|
1023
1084
|
# we don't need to refresh the objects in the array with the results
|
|
1024
1085
|
# since we will be throwing them away. Force determines whether
|
|
1025
1086
|
# to save these objects regardless of whether they are dirty.
|
|
1026
1087
|
batch = results.save(merge: false, force: force)
|
|
1027
|
-
|
|
1028
|
-
#
|
|
1029
|
-
|
|
1030
|
-
# slower version, but more accurate
|
|
1031
|
-
# cursor_item = results.max_by(&updated_comparison_block).updated_at
|
|
1032
|
-
# puts "[Parse::SaveAll] Updated #{results.count} records updated <= #{cursor.updated_at}"
|
|
1088
|
+
# Record failures before any break, so a failing last page is
|
|
1089
|
+
# reported.
|
|
1090
|
+
has_errors ||= batch.error?
|
|
1033
1091
|
|
|
1034
1092
|
break if results.count < batch_size # we didn't hit a cap on results.
|
|
1035
|
-
if cursor.is_a?(Parse::Object)
|
|
1036
|
-
update_query.where :updated_at.gte => cursor.updated_at
|
|
1037
1093
|
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1094
|
+
last_time = results.last.updated_at
|
|
1095
|
+
break if last_time.nil?
|
|
1096
|
+
if cursor_time && last_time == cursor_time
|
|
1097
|
+
seen_at_cursor.concat(results.map(&:id))
|
|
1098
|
+
else
|
|
1099
|
+
cursor_time = last_time
|
|
1100
|
+
seen_at_cursor = results.select { |r| r.updated_at == last_time }.map(&:id)
|
|
1042
1101
|
end
|
|
1043
|
-
|
|
1044
|
-
has_errors ||= batch.error?
|
|
1045
1102
|
end
|
|
1046
1103
|
not has_errors
|
|
1047
1104
|
end
|
|
@@ -1057,6 +1114,8 @@ module Parse
|
|
|
1057
1114
|
# @param op_hash [Hash] The operation hash. It may also be of type {Parse::RelationAction}.
|
|
1058
1115
|
# @return [Boolean] whether the operation was successful.
|
|
1059
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
|
|
1060
1119
|
field = field.to_sym
|
|
1061
1120
|
field = self.field_map[field] || field
|
|
1062
1121
|
if op_hash.is_a?(Parse::RelationAction)
|
|
@@ -1073,9 +1132,25 @@ module Parse
|
|
|
1073
1132
|
if response.error?
|
|
1074
1133
|
puts "[#{parse_class}:#{field} Operation] #{response.error}"
|
|
1075
1134
|
end
|
|
1135
|
+
# Keep the server's reply so a caller can adopt the field's new value
|
|
1136
|
+
# (Parse Server returns the whole array for Add/AddUnique/Remove).
|
|
1137
|
+
@_last_operation_result = response.success? && response.result.is_a?(Hash) ? response.result : nil
|
|
1076
1138
|
response.success?
|
|
1077
1139
|
end
|
|
1078
1140
|
|
|
1141
|
+
# @!visibility private
|
|
1142
|
+
# The value Parse Server returned for `field` from the last
|
|
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.
|
|
1146
|
+
def _last_operation_value(field)
|
|
1147
|
+
result = @_last_operation_result
|
|
1148
|
+
@_last_operation_result = nil
|
|
1149
|
+
return nil unless result.is_a?(Hash)
|
|
1150
|
+
wire = (self.field_map[field.to_sym] || field).to_s
|
|
1151
|
+
result[wire]
|
|
1152
|
+
end
|
|
1153
|
+
|
|
1079
1154
|
# Perform an atomic add operation to the array field.
|
|
1080
1155
|
# @param field [String] the name of the field in the Parse collection.
|
|
1081
1156
|
# @param objects [Array] the set of items to add to this field.
|
|
@@ -1134,7 +1209,9 @@ module Parse
|
|
|
1134
1209
|
def op_add_relation!(field, objects = [])
|
|
1135
1210
|
objects = [objects] unless objects.is_a?(Array)
|
|
1136
1211
|
return false if objects.empty?
|
|
1137
|
-
|
|
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)
|
|
1138
1215
|
operate_field! field, relation_action
|
|
1139
1216
|
end
|
|
1140
1217
|
|
|
@@ -1146,24 +1223,33 @@ module Parse
|
|
|
1146
1223
|
def op_remove_relation!(field, objects = [])
|
|
1147
1224
|
objects = [objects] unless objects.is_a?(Array)
|
|
1148
1225
|
return false if objects.empty?
|
|
1149
|
-
|
|
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)
|
|
1150
1229
|
operate_field! field, relation_action
|
|
1151
1230
|
end
|
|
1152
1231
|
|
|
1153
|
-
# Atomically increment or decrement a specific field.
|
|
1232
|
+
# Atomically increment or decrement a specific field. This is the one
|
|
1233
|
+
# place the local value is updated after the server applies the
|
|
1234
|
+
# increment; the `<field>_increment!` helpers delegate here.
|
|
1154
1235
|
# @param field [String] the name of the field in the Parse collection.
|
|
1155
|
-
# @param amount [
|
|
1236
|
+
# @param amount [Numeric] the amount to increment. Use negative values to decrement.
|
|
1237
|
+
# Integer amounts are sent as integers; any other Numeric is sent as a Float
|
|
1238
|
+
# so fractional increments on Number columns are not truncated.
|
|
1156
1239
|
# @see #operate_field!
|
|
1157
1240
|
def op_increment!(field, amount = 1)
|
|
1158
1241
|
unless amount.is_a?(Numeric)
|
|
1159
1242
|
raise ArgumentError, "Amount should be numeric"
|
|
1160
1243
|
end
|
|
1161
|
-
|
|
1244
|
+
amount = amount.is_a?(Integer) ? amount : amount.to_f
|
|
1245
|
+
result = operate_field! field, { __op: :Increment, amount: amount }.freeze
|
|
1162
1246
|
if result
|
|
1163
|
-
# Also update the local state to reflect the increment
|
|
1247
|
+
# Also update the local state to reflect the increment.
|
|
1248
|
+
# operate_field! does not touch local state.
|
|
1164
1249
|
field_sym = field.to_sym
|
|
1165
1250
|
current_value = self[field_sym] || 0
|
|
1166
|
-
|
|
1251
|
+
current_value = 0 unless current_value.is_a?(Numeric)
|
|
1252
|
+
new_value = current_value + amount
|
|
1167
1253
|
set_attribute_method = "#{field}_set_attribute!"
|
|
1168
1254
|
if respond_to?(set_attribute_method)
|
|
1169
1255
|
send(set_attribute_method, new_value, true) # Set new value with dirty tracking
|
|
@@ -1190,19 +1276,47 @@ module Parse
|
|
|
1190
1276
|
|
|
1191
1277
|
# Creates an array of all possible operations that need to be performed
|
|
1192
1278
|
# on this object. This includes all property and relational operation changes.
|
|
1279
|
+
#
|
|
1280
|
+
# This is the path batch saves ({Array#save}) and
|
|
1281
|
+
# {Parse::Object.transaction} use, so it applies the same save-time
|
|
1282
|
+
# rules as {#save} that do not depend on callbacks:
|
|
1283
|
+
# - a new object resolves its {Parse::Object.acl_policy} owner ACL
|
|
1284
|
+
# first, so batch-created records get the same ACL as single saves;
|
|
1285
|
+
# - a new object sends its relation additions in the create body,
|
|
1286
|
+
# since there is no objectId yet for a separate relation PUT;
|
|
1287
|
+
# - a reference to an unsaved object raises {Parse::RecordNotSaved}
|
|
1288
|
+
# instead of writing a pointer with a null objectId.
|
|
1193
1289
|
# @param force [Boolean] whether this object should be saved even if does not have
|
|
1194
1290
|
# pending changes.
|
|
1195
1291
|
# @return [Array<Parse::Request>] the list of API requests.
|
|
1292
|
+
# @raise [Parse::RecordNotSaved] if a pointer, array or relation field
|
|
1293
|
+
# references an object that has not been saved.
|
|
1196
1294
|
def change_requests(force = false)
|
|
1197
1295
|
requests = []
|
|
1198
1296
|
# get the URI path for this object.
|
|
1199
1297
|
uri = self.uri_path
|
|
1298
|
+
creating = new?
|
|
1299
|
+
|
|
1300
|
+
_resolve_default_acl if creating && respond_to?(:_resolve_default_acl, true)
|
|
1301
|
+
_assert_no_unsaved_references!
|
|
1302
|
+
|
|
1303
|
+
if creating
|
|
1304
|
+
body = attribute_updates
|
|
1305
|
+
relation_additions = relation_changes? ? relation_change_operations.first : {}
|
|
1306
|
+
if attribute_changes? || relation_additions.present? || force
|
|
1307
|
+
body.merge!(relation_additions)
|
|
1308
|
+
# Forward a client-assigned objectId, as {#create} does.
|
|
1309
|
+
body[Parse::Model::OBJECT_ID] = @id if @id.present?
|
|
1310
|
+
r = Request.new(:post, uri, body: body)
|
|
1311
|
+
r.tag = object_id
|
|
1312
|
+
requests << r
|
|
1313
|
+
end
|
|
1314
|
+
return requests
|
|
1315
|
+
end
|
|
1200
1316
|
|
|
1201
1317
|
# generate the request to update the object (PUT)
|
|
1202
1318
|
if attribute_changes? || force
|
|
1203
|
-
|
|
1204
|
-
method = new? ? :post : :put
|
|
1205
|
-
r = Request.new(method, uri, body: attribute_updates)
|
|
1319
|
+
r = Request.new(:put, uri, body: attribute_updates)
|
|
1206
1320
|
r.tag = object_id
|
|
1207
1321
|
requests << r
|
|
1208
1322
|
end
|
|
@@ -1220,6 +1334,52 @@ module Parse
|
|
|
1220
1334
|
requests
|
|
1221
1335
|
end
|
|
1222
1336
|
|
|
1337
|
+
# @!visibility private
|
|
1338
|
+
# Names of changed fields that reference a Parse object without an
|
|
1339
|
+
# objectId: a pointer field, an array of objects, or relation
|
|
1340
|
+
# additions. Saving such a field would store a pointer whose objectId
|
|
1341
|
+
# is null.
|
|
1342
|
+
# @return [Array<String>]
|
|
1343
|
+
def unsaved_reference_fields
|
|
1344
|
+
unsaved = ->(v) { v.is_a?(Parse::Pointer) && v.id.blank? }
|
|
1345
|
+
changed.each_with_object([]) do |key, list|
|
|
1346
|
+
sym = key.to_sym
|
|
1347
|
+
next unless fields[sym].present? || relations[sym].present?
|
|
1348
|
+
value = instance_variable_get(:"@#{key}")
|
|
1349
|
+
bad = if relations[sym].present?
|
|
1350
|
+
value.respond_to?(:additions) && value.additions.any?(&unsaved)
|
|
1351
|
+
elsif value.is_a?(Parse::CollectionProxy) || value.is_a?(Array)
|
|
1352
|
+
value.to_a.any?(&unsaved)
|
|
1353
|
+
else
|
|
1354
|
+
unsaved.call(value)
|
|
1355
|
+
end
|
|
1356
|
+
list << key.to_s if bad
|
|
1357
|
+
end
|
|
1358
|
+
end
|
|
1359
|
+
|
|
1360
|
+
# @!visibility private
|
|
1361
|
+
# @raise [Parse::RecordNotSaved] when {#unsaved_reference_fields} is not empty.
|
|
1362
|
+
def _assert_no_unsaved_references!
|
|
1363
|
+
fields_list = unsaved_reference_fields
|
|
1364
|
+
return if fields_list.empty?
|
|
1365
|
+
raise Parse::RecordNotSaved.new(self),
|
|
1366
|
+
"#{parse_class} references unsaved object(s) in #{fields_list.join(", ")}. " \
|
|
1367
|
+
"Save the referenced object(s) first."
|
|
1368
|
+
end
|
|
1369
|
+
|
|
1370
|
+
# @!visibility private
|
|
1371
|
+
# Refuses a write that would store a dangling pointer. Adds an error to
|
|
1372
|
+
# {#errors} and returns false (save turns that into false, or raises
|
|
1373
|
+
# with autoraise).
|
|
1374
|
+
def _check_unsaved_references
|
|
1375
|
+
fields_list = unsaved_reference_fields
|
|
1376
|
+
return true if fields_list.empty?
|
|
1377
|
+
fields_list.each do |f|
|
|
1378
|
+
errors.add(f.to_sym, "references an unsaved object; save it first")
|
|
1379
|
+
end
|
|
1380
|
+
false
|
|
1381
|
+
end
|
|
1382
|
+
|
|
1223
1383
|
# This methods sends an update request for this object with the any change
|
|
1224
1384
|
# information based on its local attributes. The bang implies that it will send
|
|
1225
1385
|
# the request even though it is possible no changes were performed. This is useful
|
|
@@ -1242,6 +1402,7 @@ module Parse
|
|
|
1242
1402
|
self.updated_at_will_change! if respond_to?(:updated_at_will_change!)
|
|
1243
1403
|
end
|
|
1244
1404
|
end
|
|
1405
|
+
return false unless _check_unsaved_references
|
|
1245
1406
|
response = client.update_object(parse_class, id, attribute_updates, session_token: _session_token)
|
|
1246
1407
|
@_last_response = response
|
|
1247
1408
|
if response.success?
|
|
@@ -1279,6 +1440,9 @@ module Parse
|
|
|
1279
1440
|
# @return [Boolean] true/false whether it was successful.
|
|
1280
1441
|
def create
|
|
1281
1442
|
run_callbacks :create do
|
|
1443
|
+
# Checked after the before_save / before_create callbacks so a
|
|
1444
|
+
# callback that saves the referenced object first still works.
|
|
1445
|
+
next false unless _check_unsaved_references
|
|
1282
1446
|
body = attribute_updates
|
|
1283
1447
|
# Forward a client-assigned objectId when a `before_create` callback
|
|
1284
1448
|
# set it (e.g. `parse_reference precompute: true`). attribute_updates
|
|
@@ -1293,9 +1457,14 @@ module Parse
|
|
|
1293
1457
|
unless res.error?
|
|
1294
1458
|
result = res.result
|
|
1295
1459
|
@id = result[Parse::Model::OBJECT_ID] || @id
|
|
1296
|
-
|
|
1460
|
+
# Store Parse::Date values, not the raw ISO strings. The :date
|
|
1461
|
+
# property reader converts a String on read and marks the field
|
|
1462
|
+
# dirty while doing so, which made a freshly created record
|
|
1463
|
+
# report unsaved changes (and send an empty PUT on the next
|
|
1464
|
+
# save) as soon as `updated_at` was read.
|
|
1465
|
+
@created_at = _server_date(result["createdAt"]) || @created_at
|
|
1297
1466
|
#if the object is created, updatedAt == createdAt
|
|
1298
|
-
@updated_at = result["updatedAt"] || result["createdAt"] || @updated_at
|
|
1467
|
+
@updated_at = _server_date(result["updatedAt"] || result["createdAt"]) || @updated_at
|
|
1299
1468
|
# Because beforeSave hooks can change the fields we are saving, any items that were
|
|
1300
1469
|
# changed, are returned to us and we should apply those locally to be in sync.
|
|
1301
1470
|
set_attributes!(result)
|
|
@@ -1305,6 +1474,18 @@ module Parse
|
|
|
1305
1474
|
end
|
|
1306
1475
|
end
|
|
1307
1476
|
|
|
1477
|
+
# @!visibility private
|
|
1478
|
+
# Parse a server timestamp (ISO string or Parse date hash) into a
|
|
1479
|
+
# Parse::Date. Returns nil for nil or unparseable input.
|
|
1480
|
+
def _server_date(value)
|
|
1481
|
+
return nil if value.nil?
|
|
1482
|
+
return value if value.is_a?(Parse::Date)
|
|
1483
|
+
value = value["iso"] || value[:iso] if value.is_a?(Hash)
|
|
1484
|
+
Parse::Date.parse(value.to_s)
|
|
1485
|
+
rescue ArgumentError
|
|
1486
|
+
nil
|
|
1487
|
+
end
|
|
1488
|
+
|
|
1308
1489
|
# @!visibility private
|
|
1309
1490
|
def _session_token
|
|
1310
1491
|
if @_session_token.respond_to?(:session_token)
|
|
@@ -1351,9 +1532,30 @@ module Parse
|
|
|
1351
1532
|
raise Parse::Error::ProtocolError, error_msg
|
|
1352
1533
|
end
|
|
1353
1534
|
|
|
1535
|
+
# A destroyed record is never recreated by a later save (ActiveRecord
|
|
1536
|
+
# returns false here too). Build a new object to write it again.
|
|
1537
|
+
if destroyed?
|
|
1538
|
+
if self.class.raise_on_save_failure || autoraise.present?
|
|
1539
|
+
raise Parse::RecordNotSaved.new(self), "Cannot save a destroyed #{parse_class} (id '#{@id}')."
|
|
1540
|
+
end
|
|
1541
|
+
return false
|
|
1542
|
+
end
|
|
1543
|
+
|
|
1354
1544
|
@_session_token = _validate_session_token! session, :save
|
|
1355
1545
|
return true unless changed? || force
|
|
1356
1546
|
|
|
1547
|
+
# A create may assign the objectId client-side from a callback
|
|
1548
|
+
# (parse_reference precompute, acl_owner :self). Until the create
|
|
1549
|
+
# returns createdAt, persisted? must stay false for such an object.
|
|
1550
|
+
@_creating_record = true if @id.blank?
|
|
1551
|
+
_save_with_callbacks(autoraise: autoraise, force: force, validate: validate)
|
|
1552
|
+
ensure
|
|
1553
|
+
remove_instance_variable(:@_creating_record) if defined?(@_creating_record)
|
|
1554
|
+
end
|
|
1555
|
+
|
|
1556
|
+
# @!visibility private
|
|
1557
|
+
# Body of {#save} after the deleted-object, session and no-change checks.
|
|
1558
|
+
def _save_with_callbacks(autoraise:, force:, validate:)
|
|
1357
1559
|
# Run validations (validation callbacks are now triggered by valid? method)
|
|
1358
1560
|
# Pass context so `on: :create` and `on: :update` options work with callbacks
|
|
1359
1561
|
if validate
|
|
@@ -1376,7 +1578,9 @@ module Parse
|
|
|
1376
1578
|
callback_executed = true
|
|
1377
1579
|
#first process the create/update action if any
|
|
1378
1580
|
#then perform any relation changes that need to be performed
|
|
1379
|
-
|
|
1581
|
+
was_new = new?
|
|
1582
|
+
success = was_new ? create : perform_update(force: force)
|
|
1583
|
+
saved_fields = changed.map(&:to_sym)
|
|
1380
1584
|
|
|
1381
1585
|
# if the save was successful and we have relational changes
|
|
1382
1586
|
# let's update send those next.
|
|
@@ -1388,13 +1592,13 @@ module Parse
|
|
|
1388
1592
|
success = update_relations
|
|
1389
1593
|
if success
|
|
1390
1594
|
changes_applied!
|
|
1391
|
-
|
|
1595
|
+
_after_save_partial_fetch_state!(was_new, saved_fields)
|
|
1392
1596
|
elsif self.class.raise_on_save_failure || autoraise.present?
|
|
1393
1597
|
raise Parse::RecordNotSaved.new(self), "Failed updating relations. #{self.parse_class} partially saved."
|
|
1394
1598
|
end
|
|
1395
1599
|
else
|
|
1396
1600
|
changes_applied!
|
|
1397
|
-
|
|
1601
|
+
_after_save_partial_fetch_state!(was_new, saved_fields)
|
|
1398
1602
|
end
|
|
1399
1603
|
elsif self.class.raise_on_save_failure || autoraise.present?
|
|
1400
1604
|
raise Parse::RecordNotSaved.new(self), "Failed to create or save attributes. #{self.parse_class} was not saved."
|
|
@@ -1427,6 +1631,11 @@ module Parse
|
|
|
1427
1631
|
|
|
1428
1632
|
# Delete this record from the Parse collection. Only valid if this object has an `id`.
|
|
1429
1633
|
# This will run all the `destroy` callbacks.
|
|
1634
|
+
#
|
|
1635
|
+
# After a successful destroy the object keeps its `id`, {Parse::Object#destroyed?}
|
|
1636
|
+
# returns true and {Parse::Object#persisted?} returns false. Saving a destroyed
|
|
1637
|
+
# object is refused: {#save} returns false and {#save!} raises
|
|
1638
|
+
# {Parse::RecordNotSaved}, as in ActiveRecord.
|
|
1430
1639
|
# @param session [String] a session token if you want to apply ACLs for a user in this operation.
|
|
1431
1640
|
# @raise ArgumentError if a non-nil value is passed to `session` that doesn't provide a session token string.
|
|
1432
1641
|
# @return [Boolean] whether the operation was successful.
|
|
@@ -1438,7 +1647,11 @@ module Parse
|
|
|
1438
1647
|
res = client.delete_object parse_class, id, session_token: _session_token
|
|
1439
1648
|
success = res.success?
|
|
1440
1649
|
if success
|
|
1441
|
-
|
|
1650
|
+
# Keep the objectId so `dom_id`, `to_key` and Turbo Stream
|
|
1651
|
+
# removals still identify the record, and mark it destroyed so
|
|
1652
|
+
# `persisted?` is false and a later `save` refuses rather than
|
|
1653
|
+
# recreating it.
|
|
1654
|
+
@_destroyed = true
|
|
1442
1655
|
changes_applied!
|
|
1443
1656
|
elsif self.class.raise_on_save_failure
|
|
1444
1657
|
raise Parse::RecordNotSaved.new(self), "Failed to create or save attributes. #{self.parse_class} was not saved."
|
|
@@ -1553,20 +1766,24 @@ module Parse
|
|
|
1553
1766
|
# Clears changes information on all collections (array and relations) and all
|
|
1554
1767
|
# local attributes.
|
|
1555
1768
|
def changes_applied!
|
|
1556
|
-
#
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
proxy = send(key)
|
|
1566
|
-
# clear changes if they support the method.
|
|
1769
|
+
# Settle the array and relation proxies that exist. Read the ivar
|
|
1770
|
+
# rather than the getter: the getter can autofetch a pointer or
|
|
1771
|
+
# build an empty proxy, and neither has changes to settle. A
|
|
1772
|
+
# relation proxy also drops its saved additions and removals here,
|
|
1773
|
+
# so a later save does not send them again.
|
|
1774
|
+
(fields(:array).keys + relations.keys).uniq.each do |key|
|
|
1775
|
+
ivar = :"@#{key}"
|
|
1776
|
+
next unless instance_variable_defined?(ivar)
|
|
1777
|
+
proxy = instance_variable_get(ivar)
|
|
1567
1778
|
proxy.changes_applied! if proxy.respond_to?(:changes_applied!)
|
|
1568
1779
|
end
|
|
1569
1780
|
changes_applied
|
|
1781
|
+
# The saved ACL is the new baseline. Without this, the snapshot from
|
|
1782
|
+
# before the first edit survived the save, so `acl_was` kept
|
|
1783
|
+
# reporting the pre-save ACL: revoking a grant after saving it
|
|
1784
|
+
# compared equal to that stale snapshot, `acl_changed?` was false,
|
|
1785
|
+
# and the revocation was silently dropped while save returned true.
|
|
1786
|
+
@_acl_snapshot_before_change = nil
|
|
1570
1787
|
end
|
|
1571
1788
|
end
|
|
1572
1789
|
end
|