parse-stack-next 5.7.5 → 5.8.0
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 +856 -0
- data/README.md +15 -4
- data/docs/TEST_SERVER.md +2 -2
- data/docs/acl_clp_guide.md +7 -0
- data/docs/atlas_vector_search_guide.md +190 -14
- data/docs/client_sdk_guide.md +11 -0
- data/docs/mcp_guide.md +318 -6
- data/docs/mongodb_direct_guide.md +27 -0
- data/docs/usage_guide.md +38 -0
- data/docs/webhooks_guide.md +74 -17
- data/lib/parse/acl_scope.rb +159 -41
- data/lib/parse/agent/approval_gate.rb +0 -0
- data/lib/parse/agent/constraint_translator.rb +42 -15
- data/lib/parse/agent/describe.rb +3 -1
- 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_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 +93 -1
- 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/protected_paths.rb +236 -0
- data/lib/parse/atlas_search.rb +95 -23
- data/lib/parse/authorization.rb +54 -1
- data/lib/parse/client/batch.rb +231 -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 +201 -38
- data/lib/parse/clp_scope.rb +281 -23
- 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/model/acl.rb +119 -21
- data/lib/parse/model/associations/belongs_to.rb +25 -3
- data/lib/parse/model/associations/collection_proxy.rb +138 -17
- data/lib/parse/model/associations/has_many.rb +38 -9
- data/lib/parse/model/associations/has_one.rb +3 -1
- data/lib/parse/model/associations/pointer_collection_proxy.rb +109 -17
- data/lib/parse/model/associations/relation_collection_proxy.rb +134 -28
- data/lib/parse/model/bytes.rb +13 -5
- data/lib/parse/model/classes/role.rb +72 -0
- data/lib/parse/model/classes/session.rb +43 -0
- data/lib/parse/model/classes/user.rb +78 -3
- data/lib/parse/model/core/actions.rb +269 -67
- 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 +21 -3
- 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 +265 -17
- data/lib/parse/model/phone.rb +54 -5
- data/lib/parse/model/pointer.rb +40 -6
- data/lib/parse/mongodb.rb +170 -60
- data/lib/parse/pipeline_security.rb +415 -26
- data/lib/parse/query/constraint.rb +30 -0
- data/lib/parse/query/constraints.rb +58 -32
- 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 +1100 -134
- data/lib/parse/retrieval/agent_tool.rb +290 -17
- 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 +72 -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 +36 -18
- data/lib/parse/vector_search/index_definition.rb +237 -0
- data/lib/parse/vector_search.rb +46 -17
- data/lib/parse/webhooks/payload.rb +93 -6
- data/lib/parse/webhooks/replay_protection.rb +58 -20
- data/lib/parse/webhooks.rb +412 -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).
|
|
@@ -511,13 +561,13 @@ module Parse
|
|
|
511
561
|
end
|
|
512
562
|
|
|
513
563
|
error_response = responses.find { |response| !response.success? }
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
if e.message.include?("251") && attempts < retries
|
|
564
|
+
error_code = error_response&.code.to_i
|
|
565
|
+
if error_code == TRANSACTION_CONFLICT_CODE && attempts < retries
|
|
517
566
|
sleep(0.1 * attempts)
|
|
518
|
-
|
|
567
|
+
next
|
|
519
568
|
end
|
|
520
|
-
|
|
569
|
+
detail = error_response&.error || "unknown error"
|
|
570
|
+
raise Parse::Error, "Transaction failed (code #{error_code}): #{detail}"
|
|
521
571
|
end
|
|
522
572
|
rescue StandardError
|
|
523
573
|
original_states.each_value do |state|
|
|
@@ -989,59 +1039,60 @@ module Parse
|
|
|
989
1039
|
iterator_block = nil
|
|
990
1040
|
if block_given?
|
|
991
1041
|
iterator_block = block
|
|
992
|
-
force ||= false
|
|
993
1042
|
else
|
|
994
1043
|
# if no block given, assume you want to just save all objects
|
|
995
1044
|
# regardless of modification.
|
|
996
1045
|
force = true
|
|
997
1046
|
end
|
|
998
|
-
# Only generate the comparison block once.
|
|
999
|
-
# updated_comparison_block = Proc.new { |x| x.updated_at }
|
|
1000
1047
|
|
|
1048
|
+
# Work on a copy so the caller's constraints hash is not mutated.
|
|
1049
|
+
base_constraints = constraints.dup
|
|
1001
1050
|
anchor_date = Parse::Date.now
|
|
1002
|
-
|
|
1003
|
-
|
|
1051
|
+
base_constraints[:updated_at.on_or_before] = anchor_date
|
|
1052
|
+
base_constraints[:cache] = false
|
|
1004
1053
|
# oldest first, so we create a reduction-cycle
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
cursor
|
|
1054
|
+
base_constraints[:order] = :updated_at.asc
|
|
1055
|
+
base_constraints[:limit] = batch_size
|
|
1056
|
+
|
|
1057
|
+
# Keyset cursor over (updated_at, objectId). Saved objects move past
|
|
1058
|
+
# the anchor date and drop out of the query. Objects the block left
|
|
1059
|
+
# unchanged keep their updated_at, so each page resumes at the last
|
|
1060
|
+
# seen updated_at and excludes the ids already visited at exactly
|
|
1061
|
+
# that timestamp. Every page therefore makes progress, and records
|
|
1062
|
+
# the block does not modify are visited once instead of ending the
|
|
1063
|
+
# run early.
|
|
1064
|
+
cursor_time = nil
|
|
1065
|
+
seen_at_cursor = []
|
|
1009
1066
|
has_errors = false
|
|
1010
1067
|
loop do
|
|
1011
|
-
|
|
1068
|
+
page_constraints = base_constraints.dup
|
|
1069
|
+
if cursor_time
|
|
1070
|
+
page_constraints[:updated_at.gte] = cursor_time
|
|
1071
|
+
page_constraints[:objectId.nin] = seen_at_cursor if seen_at_cursor.any?
|
|
1072
|
+
end
|
|
1073
|
+
results = query(page_constraints).results
|
|
1012
1074
|
|
|
1013
1075
|
break if results.empty?
|
|
1014
1076
|
|
|
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
1077
|
results.each(&iterator_block) if iterator_block.present?
|
|
1023
1078
|
# we don't need to refresh the objects in the array with the results
|
|
1024
1079
|
# since we will be throwing them away. Force determines whether
|
|
1025
1080
|
# to save these objects regardless of whether they are dirty.
|
|
1026
1081
|
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}"
|
|
1082
|
+
# Record failures before any break, so a failing last page is
|
|
1083
|
+
# reported.
|
|
1084
|
+
has_errors ||= batch.error?
|
|
1033
1085
|
|
|
1034
1086
|
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
1087
|
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1088
|
+
last_time = results.last.updated_at
|
|
1089
|
+
break if last_time.nil?
|
|
1090
|
+
if cursor_time && last_time == cursor_time
|
|
1091
|
+
seen_at_cursor.concat(results.map(&:id))
|
|
1092
|
+
else
|
|
1093
|
+
cursor_time = last_time
|
|
1094
|
+
seen_at_cursor = results.select { |r| r.updated_at == last_time }.map(&:id)
|
|
1042
1095
|
end
|
|
1043
|
-
|
|
1044
|
-
has_errors ||= batch.error?
|
|
1045
1096
|
end
|
|
1046
1097
|
not has_errors
|
|
1047
1098
|
end
|
|
@@ -1073,9 +1124,22 @@ module Parse
|
|
|
1073
1124
|
if response.error?
|
|
1074
1125
|
puts "[#{parse_class}:#{field} Operation] #{response.error}"
|
|
1075
1126
|
end
|
|
1127
|
+
# Keep the server's reply so a caller can adopt the field's new value
|
|
1128
|
+
# (Parse Server returns the whole array for Add/AddUnique/Remove).
|
|
1129
|
+
@_last_operation_result = response.success? && response.result.is_a?(Hash) ? response.result : nil
|
|
1076
1130
|
response.success?
|
|
1077
1131
|
end
|
|
1078
1132
|
|
|
1133
|
+
# @!visibility private
|
|
1134
|
+
# The value Parse Server returned for `field` from the last
|
|
1135
|
+
# {#operate_field!}, or nil when the reply did not include it.
|
|
1136
|
+
def _last_operation_value(field)
|
|
1137
|
+
result = @_last_operation_result
|
|
1138
|
+
return nil unless result.is_a?(Hash)
|
|
1139
|
+
wire = (self.field_map[field.to_sym] || field).to_s
|
|
1140
|
+
result[wire]
|
|
1141
|
+
end
|
|
1142
|
+
|
|
1079
1143
|
# Perform an atomic add operation to the array field.
|
|
1080
1144
|
# @param field [String] the name of the field in the Parse collection.
|
|
1081
1145
|
# @param objects [Array] the set of items to add to this field.
|
|
@@ -1150,20 +1214,27 @@ module Parse
|
|
|
1150
1214
|
operate_field! field, relation_action
|
|
1151
1215
|
end
|
|
1152
1216
|
|
|
1153
|
-
# Atomically increment or decrement a specific field.
|
|
1217
|
+
# Atomically increment or decrement a specific field. This is the one
|
|
1218
|
+
# place the local value is updated after the server applies the
|
|
1219
|
+
# increment; the `<field>_increment!` helpers delegate here.
|
|
1154
1220
|
# @param field [String] the name of the field in the Parse collection.
|
|
1155
|
-
# @param amount [
|
|
1221
|
+
# @param amount [Numeric] the amount to increment. Use negative values to decrement.
|
|
1222
|
+
# Integer amounts are sent as integers; any other Numeric is sent as a Float
|
|
1223
|
+
# so fractional increments on Number columns are not truncated.
|
|
1156
1224
|
# @see #operate_field!
|
|
1157
1225
|
def op_increment!(field, amount = 1)
|
|
1158
1226
|
unless amount.is_a?(Numeric)
|
|
1159
1227
|
raise ArgumentError, "Amount should be numeric"
|
|
1160
1228
|
end
|
|
1161
|
-
|
|
1229
|
+
amount = amount.is_a?(Integer) ? amount : amount.to_f
|
|
1230
|
+
result = operate_field! field, { __op: :Increment, amount: amount }.freeze
|
|
1162
1231
|
if result
|
|
1163
|
-
# Also update the local state to reflect the increment
|
|
1232
|
+
# Also update the local state to reflect the increment.
|
|
1233
|
+
# operate_field! does not touch local state.
|
|
1164
1234
|
field_sym = field.to_sym
|
|
1165
1235
|
current_value = self[field_sym] || 0
|
|
1166
|
-
|
|
1236
|
+
current_value = 0 unless current_value.is_a?(Numeric)
|
|
1237
|
+
new_value = current_value + amount
|
|
1167
1238
|
set_attribute_method = "#{field}_set_attribute!"
|
|
1168
1239
|
if respond_to?(set_attribute_method)
|
|
1169
1240
|
send(set_attribute_method, new_value, true) # Set new value with dirty tracking
|
|
@@ -1190,19 +1261,47 @@ module Parse
|
|
|
1190
1261
|
|
|
1191
1262
|
# Creates an array of all possible operations that need to be performed
|
|
1192
1263
|
# on this object. This includes all property and relational operation changes.
|
|
1264
|
+
#
|
|
1265
|
+
# This is the path batch saves ({Array#save}) and
|
|
1266
|
+
# {Parse::Object.transaction} use, so it applies the same save-time
|
|
1267
|
+
# rules as {#save} that do not depend on callbacks:
|
|
1268
|
+
# - a new object resolves its {Parse::Object.acl_policy} owner ACL
|
|
1269
|
+
# first, so batch-created records get the same ACL as single saves;
|
|
1270
|
+
# - a new object sends its relation additions in the create body,
|
|
1271
|
+
# since there is no objectId yet for a separate relation PUT;
|
|
1272
|
+
# - a reference to an unsaved object raises {Parse::RecordNotSaved}
|
|
1273
|
+
# instead of writing a pointer with a null objectId.
|
|
1193
1274
|
# @param force [Boolean] whether this object should be saved even if does not have
|
|
1194
1275
|
# pending changes.
|
|
1195
1276
|
# @return [Array<Parse::Request>] the list of API requests.
|
|
1277
|
+
# @raise [Parse::RecordNotSaved] if a pointer, array or relation field
|
|
1278
|
+
# references an object that has not been saved.
|
|
1196
1279
|
def change_requests(force = false)
|
|
1197
1280
|
requests = []
|
|
1198
1281
|
# get the URI path for this object.
|
|
1199
1282
|
uri = self.uri_path
|
|
1283
|
+
creating = new?
|
|
1284
|
+
|
|
1285
|
+
_resolve_default_acl if creating && respond_to?(:_resolve_default_acl, true)
|
|
1286
|
+
_assert_no_unsaved_references!
|
|
1287
|
+
|
|
1288
|
+
if creating
|
|
1289
|
+
body = attribute_updates
|
|
1290
|
+
relation_additions = relation_changes? ? relation_change_operations.first : {}
|
|
1291
|
+
if attribute_changes? || relation_additions.present? || force
|
|
1292
|
+
body.merge!(relation_additions)
|
|
1293
|
+
# Forward a client-assigned objectId, as {#create} does.
|
|
1294
|
+
body[Parse::Model::OBJECT_ID] = @id if @id.present?
|
|
1295
|
+
r = Request.new(:post, uri, body: body)
|
|
1296
|
+
r.tag = object_id
|
|
1297
|
+
requests << r
|
|
1298
|
+
end
|
|
1299
|
+
return requests
|
|
1300
|
+
end
|
|
1200
1301
|
|
|
1201
1302
|
# generate the request to update the object (PUT)
|
|
1202
1303
|
if attribute_changes? || force
|
|
1203
|
-
|
|
1204
|
-
method = new? ? :post : :put
|
|
1205
|
-
r = Request.new(method, uri, body: attribute_updates)
|
|
1304
|
+
r = Request.new(:put, uri, body: attribute_updates)
|
|
1206
1305
|
r.tag = object_id
|
|
1207
1306
|
requests << r
|
|
1208
1307
|
end
|
|
@@ -1220,6 +1319,52 @@ module Parse
|
|
|
1220
1319
|
requests
|
|
1221
1320
|
end
|
|
1222
1321
|
|
|
1322
|
+
# @!visibility private
|
|
1323
|
+
# Names of changed fields that reference a Parse object without an
|
|
1324
|
+
# objectId: a pointer field, an array of objects, or relation
|
|
1325
|
+
# additions. Saving such a field would store a pointer whose objectId
|
|
1326
|
+
# is null.
|
|
1327
|
+
# @return [Array<String>]
|
|
1328
|
+
def unsaved_reference_fields
|
|
1329
|
+
unsaved = ->(v) { v.is_a?(Parse::Pointer) && v.id.blank? }
|
|
1330
|
+
changed.each_with_object([]) do |key, list|
|
|
1331
|
+
sym = key.to_sym
|
|
1332
|
+
next unless fields[sym].present? || relations[sym].present?
|
|
1333
|
+
value = instance_variable_get(:"@#{key}")
|
|
1334
|
+
bad = if relations[sym].present?
|
|
1335
|
+
value.respond_to?(:additions) && value.additions.any?(&unsaved)
|
|
1336
|
+
elsif value.is_a?(Parse::CollectionProxy) || value.is_a?(Array)
|
|
1337
|
+
value.to_a.any?(&unsaved)
|
|
1338
|
+
else
|
|
1339
|
+
unsaved.call(value)
|
|
1340
|
+
end
|
|
1341
|
+
list << key.to_s if bad
|
|
1342
|
+
end
|
|
1343
|
+
end
|
|
1344
|
+
|
|
1345
|
+
# @!visibility private
|
|
1346
|
+
# @raise [Parse::RecordNotSaved] when {#unsaved_reference_fields} is not empty.
|
|
1347
|
+
def _assert_no_unsaved_references!
|
|
1348
|
+
fields_list = unsaved_reference_fields
|
|
1349
|
+
return if fields_list.empty?
|
|
1350
|
+
raise Parse::RecordNotSaved.new(self),
|
|
1351
|
+
"#{parse_class} references unsaved object(s) in #{fields_list.join(", ")}. " \
|
|
1352
|
+
"Save the referenced object(s) first."
|
|
1353
|
+
end
|
|
1354
|
+
|
|
1355
|
+
# @!visibility private
|
|
1356
|
+
# Refuses a write that would store a dangling pointer. Adds an error to
|
|
1357
|
+
# {#errors} and returns false (save turns that into false, or raises
|
|
1358
|
+
# with autoraise).
|
|
1359
|
+
def _check_unsaved_references
|
|
1360
|
+
fields_list = unsaved_reference_fields
|
|
1361
|
+
return true if fields_list.empty?
|
|
1362
|
+
fields_list.each do |f|
|
|
1363
|
+
errors.add(f.to_sym, "references an unsaved object; save it first")
|
|
1364
|
+
end
|
|
1365
|
+
false
|
|
1366
|
+
end
|
|
1367
|
+
|
|
1223
1368
|
# This methods sends an update request for this object with the any change
|
|
1224
1369
|
# information based on its local attributes. The bang implies that it will send
|
|
1225
1370
|
# the request even though it is possible no changes were performed. This is useful
|
|
@@ -1242,6 +1387,7 @@ module Parse
|
|
|
1242
1387
|
self.updated_at_will_change! if respond_to?(:updated_at_will_change!)
|
|
1243
1388
|
end
|
|
1244
1389
|
end
|
|
1390
|
+
return false unless _check_unsaved_references
|
|
1245
1391
|
response = client.update_object(parse_class, id, attribute_updates, session_token: _session_token)
|
|
1246
1392
|
@_last_response = response
|
|
1247
1393
|
if response.success?
|
|
@@ -1279,6 +1425,9 @@ module Parse
|
|
|
1279
1425
|
# @return [Boolean] true/false whether it was successful.
|
|
1280
1426
|
def create
|
|
1281
1427
|
run_callbacks :create do
|
|
1428
|
+
# Checked after the before_save / before_create callbacks so a
|
|
1429
|
+
# callback that saves the referenced object first still works.
|
|
1430
|
+
next false unless _check_unsaved_references
|
|
1282
1431
|
body = attribute_updates
|
|
1283
1432
|
# Forward a client-assigned objectId when a `before_create` callback
|
|
1284
1433
|
# set it (e.g. `parse_reference precompute: true`). attribute_updates
|
|
@@ -1293,9 +1442,14 @@ module Parse
|
|
|
1293
1442
|
unless res.error?
|
|
1294
1443
|
result = res.result
|
|
1295
1444
|
@id = result[Parse::Model::OBJECT_ID] || @id
|
|
1296
|
-
|
|
1445
|
+
# Store Parse::Date values, not the raw ISO strings. The :date
|
|
1446
|
+
# property reader converts a String on read and marks the field
|
|
1447
|
+
# dirty while doing so, which made a freshly created record
|
|
1448
|
+
# report unsaved changes (and send an empty PUT on the next
|
|
1449
|
+
# save) as soon as `updated_at` was read.
|
|
1450
|
+
@created_at = _server_date(result["createdAt"]) || @created_at
|
|
1297
1451
|
#if the object is created, updatedAt == createdAt
|
|
1298
|
-
@updated_at = result["updatedAt"] || result["createdAt"] || @updated_at
|
|
1452
|
+
@updated_at = _server_date(result["updatedAt"] || result["createdAt"]) || @updated_at
|
|
1299
1453
|
# Because beforeSave hooks can change the fields we are saving, any items that were
|
|
1300
1454
|
# changed, are returned to us and we should apply those locally to be in sync.
|
|
1301
1455
|
set_attributes!(result)
|
|
@@ -1305,6 +1459,18 @@ module Parse
|
|
|
1305
1459
|
end
|
|
1306
1460
|
end
|
|
1307
1461
|
|
|
1462
|
+
# @!visibility private
|
|
1463
|
+
# Parse a server timestamp (ISO string or Parse date hash) into a
|
|
1464
|
+
# Parse::Date. Returns nil for nil or unparseable input.
|
|
1465
|
+
def _server_date(value)
|
|
1466
|
+
return nil if value.nil?
|
|
1467
|
+
return value if value.is_a?(Parse::Date)
|
|
1468
|
+
value = value["iso"] || value[:iso] if value.is_a?(Hash)
|
|
1469
|
+
Parse::Date.parse(value.to_s)
|
|
1470
|
+
rescue ArgumentError
|
|
1471
|
+
nil
|
|
1472
|
+
end
|
|
1473
|
+
|
|
1308
1474
|
# @!visibility private
|
|
1309
1475
|
def _session_token
|
|
1310
1476
|
if @_session_token.respond_to?(:session_token)
|
|
@@ -1351,9 +1517,30 @@ module Parse
|
|
|
1351
1517
|
raise Parse::Error::ProtocolError, error_msg
|
|
1352
1518
|
end
|
|
1353
1519
|
|
|
1520
|
+
# A destroyed record is never recreated by a later save (ActiveRecord
|
|
1521
|
+
# returns false here too). Build a new object to write it again.
|
|
1522
|
+
if destroyed?
|
|
1523
|
+
if self.class.raise_on_save_failure || autoraise.present?
|
|
1524
|
+
raise Parse::RecordNotSaved.new(self), "Cannot save a destroyed #{parse_class} (id '#{@id}')."
|
|
1525
|
+
end
|
|
1526
|
+
return false
|
|
1527
|
+
end
|
|
1528
|
+
|
|
1354
1529
|
@_session_token = _validate_session_token! session, :save
|
|
1355
1530
|
return true unless changed? || force
|
|
1356
1531
|
|
|
1532
|
+
# A create may assign the objectId client-side from a callback
|
|
1533
|
+
# (parse_reference precompute, acl_owner :self). Until the create
|
|
1534
|
+
# returns createdAt, persisted? must stay false for such an object.
|
|
1535
|
+
@_creating_record = true if @id.blank?
|
|
1536
|
+
_save_with_callbacks(autoraise: autoraise, force: force, validate: validate)
|
|
1537
|
+
ensure
|
|
1538
|
+
remove_instance_variable(:@_creating_record) if defined?(@_creating_record)
|
|
1539
|
+
end
|
|
1540
|
+
|
|
1541
|
+
# @!visibility private
|
|
1542
|
+
# Body of {#save} after the deleted-object, session and no-change checks.
|
|
1543
|
+
def _save_with_callbacks(autoraise:, force:, validate:)
|
|
1357
1544
|
# Run validations (validation callbacks are now triggered by valid? method)
|
|
1358
1545
|
# Pass context so `on: :create` and `on: :update` options work with callbacks
|
|
1359
1546
|
if validate
|
|
@@ -1376,7 +1563,9 @@ module Parse
|
|
|
1376
1563
|
callback_executed = true
|
|
1377
1564
|
#first process the create/update action if any
|
|
1378
1565
|
#then perform any relation changes that need to be performed
|
|
1379
|
-
|
|
1566
|
+
was_new = new?
|
|
1567
|
+
success = was_new ? create : perform_update(force: force)
|
|
1568
|
+
saved_fields = changed.map(&:to_sym)
|
|
1380
1569
|
|
|
1381
1570
|
# if the save was successful and we have relational changes
|
|
1382
1571
|
# let's update send those next.
|
|
@@ -1388,13 +1577,13 @@ module Parse
|
|
|
1388
1577
|
success = update_relations
|
|
1389
1578
|
if success
|
|
1390
1579
|
changes_applied!
|
|
1391
|
-
|
|
1580
|
+
_after_save_partial_fetch_state!(was_new, saved_fields)
|
|
1392
1581
|
elsif self.class.raise_on_save_failure || autoraise.present?
|
|
1393
1582
|
raise Parse::RecordNotSaved.new(self), "Failed updating relations. #{self.parse_class} partially saved."
|
|
1394
1583
|
end
|
|
1395
1584
|
else
|
|
1396
1585
|
changes_applied!
|
|
1397
|
-
|
|
1586
|
+
_after_save_partial_fetch_state!(was_new, saved_fields)
|
|
1398
1587
|
end
|
|
1399
1588
|
elsif self.class.raise_on_save_failure || autoraise.present?
|
|
1400
1589
|
raise Parse::RecordNotSaved.new(self), "Failed to create or save attributes. #{self.parse_class} was not saved."
|
|
@@ -1427,6 +1616,11 @@ module Parse
|
|
|
1427
1616
|
|
|
1428
1617
|
# Delete this record from the Parse collection. Only valid if this object has an `id`.
|
|
1429
1618
|
# This will run all the `destroy` callbacks.
|
|
1619
|
+
#
|
|
1620
|
+
# After a successful destroy the object keeps its `id`, {Parse::Object#destroyed?}
|
|
1621
|
+
# returns true and {Parse::Object#persisted?} returns false. Saving a destroyed
|
|
1622
|
+
# object is refused: {#save} returns false and {#save!} raises
|
|
1623
|
+
# {Parse::RecordNotSaved}, as in ActiveRecord.
|
|
1430
1624
|
# @param session [String] a session token if you want to apply ACLs for a user in this operation.
|
|
1431
1625
|
# @raise ArgumentError if a non-nil value is passed to `session` that doesn't provide a session token string.
|
|
1432
1626
|
# @return [Boolean] whether the operation was successful.
|
|
@@ -1438,7 +1632,11 @@ module Parse
|
|
|
1438
1632
|
res = client.delete_object parse_class, id, session_token: _session_token
|
|
1439
1633
|
success = res.success?
|
|
1440
1634
|
if success
|
|
1441
|
-
|
|
1635
|
+
# Keep the objectId so `dom_id`, `to_key` and Turbo Stream
|
|
1636
|
+
# removals still identify the record, and mark it destroyed so
|
|
1637
|
+
# `persisted?` is false and a later `save` refuses rather than
|
|
1638
|
+
# recreating it.
|
|
1639
|
+
@_destroyed = true
|
|
1442
1640
|
changes_applied!
|
|
1443
1641
|
elsif self.class.raise_on_save_failure
|
|
1444
1642
|
raise Parse::RecordNotSaved.new(self), "Failed to create or save attributes. #{self.parse_class} was not saved."
|
|
@@ -1553,20 +1751,24 @@ module Parse
|
|
|
1553
1751
|
# Clears changes information on all collections (array and relations) and all
|
|
1554
1752
|
# local attributes.
|
|
1555
1753
|
def changes_applied!
|
|
1556
|
-
#
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
proxy = send(key)
|
|
1566
|
-
# clear changes if they support the method.
|
|
1754
|
+
# Settle the array and relation proxies that exist. Read the ivar
|
|
1755
|
+
# rather than the getter: the getter can autofetch a pointer or
|
|
1756
|
+
# build an empty proxy, and neither has changes to settle. A
|
|
1757
|
+
# relation proxy also drops its saved additions and removals here,
|
|
1758
|
+
# so a later save does not send them again.
|
|
1759
|
+
(fields(:array).keys + relations.keys).uniq.each do |key|
|
|
1760
|
+
ivar = :"@#{key}"
|
|
1761
|
+
next unless instance_variable_defined?(ivar)
|
|
1762
|
+
proxy = instance_variable_get(ivar)
|
|
1567
1763
|
proxy.changes_applied! if proxy.respond_to?(:changes_applied!)
|
|
1568
1764
|
end
|
|
1569
1765
|
changes_applied
|
|
1766
|
+
# The saved ACL is the new baseline. Without this, the snapshot from
|
|
1767
|
+
# before the first edit survived the save, so `acl_was` kept
|
|
1768
|
+
# reporting the pre-save ACL: revoking a grant after saving it
|
|
1769
|
+
# compared equal to that stale snapshot, `acl_changed?` was false,
|
|
1770
|
+
# and the revocation was silently dropped while save returned true.
|
|
1771
|
+
@_acl_snapshot_before_change = nil
|
|
1570
1772
|
end
|
|
1571
1773
|
end
|
|
1572
1774
|
end
|
|
@@ -89,6 +89,9 @@ module Parse
|
|
|
89
89
|
if klass.nil?
|
|
90
90
|
klass = ::Class.new(Parse::Object)
|
|
91
91
|
Parse::Generated.const_set(parse_class_name, klass)
|
|
92
|
+
# The default parse_class comes from the Ruby name, which here is
|
|
93
|
+
# namespaced ("Parse::Generated::Post"). Bind the server class name.
|
|
94
|
+
klass.parse_class(className.to_s)
|
|
92
95
|
end
|
|
93
96
|
unless klass.is_a?(Class) && klass <= Parse::Object
|
|
94
97
|
raise ArgumentError, "Resolved class #{klass.inspect} for #{className.inspect} is not a Parse::Object subclass"
|
|
@@ -106,22 +109,111 @@ module Parse
|
|
|
106
109
|
class_fields = klass.field_map.values + [:className]
|
|
107
110
|
fields.each do |field, type|
|
|
108
111
|
field = field.to_sym
|
|
109
|
-
key = field.to_s.underscore.to_sym
|
|
110
112
|
next if base_fields.include?(field) || class_fields.include?(field)
|
|
113
|
+
next unless type.respond_to?(:[]) && type[:type].present?
|
|
111
114
|
|
|
112
|
-
data_type = type[:type].downcase.to_sym
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
115
|
+
data_type = type[:type].to_s.downcase.to_sym
|
|
116
|
+
# A model's field registry shares one namespace between Ruby
|
|
117
|
+
# names and server columns, so a column whose name another column
|
|
118
|
+
# already claimed as its Ruby name (`foo_bar` after `fooBar`)
|
|
119
|
+
# cannot be mapped. Skip it instead of aborting the whole build.
|
|
120
|
+
if klass.fields.key?(field) || klass.field_map.key?(field)
|
|
121
|
+
builder_warn "skipping column #{className}.#{field}: its name is already " \
|
|
122
|
+
"used by another column of the same class"
|
|
123
|
+
next
|
|
124
|
+
end
|
|
125
|
+
key = safe_accessor_name(klass, field, data_type)
|
|
126
|
+
next if key.nil?
|
|
127
|
+
|
|
128
|
+
begin
|
|
129
|
+
if data_type == :pointer
|
|
130
|
+
klass.belongs_to key, as: safe_target_class(type[:targetClass]), field: field
|
|
131
|
+
elsif data_type == :relation
|
|
132
|
+
klass.has_many key, through: :relation, as: safe_target_class(type[:targetClass]), field: field
|
|
133
|
+
else
|
|
134
|
+
# A renamed accessor must not get an alias under the column
|
|
135
|
+
# name, which is the method it was renamed to avoid.
|
|
136
|
+
opts = { field: field }
|
|
137
|
+
opts[:alias] = false if key.to_s != field.to_s.underscore
|
|
138
|
+
klass.property key, data_type, **opts
|
|
139
|
+
end
|
|
140
|
+
rescue StandardError, SystemStackError => e
|
|
141
|
+
builder_warn "skipping column #{className}.#{field}: #{e.class}: #{e.message}"
|
|
142
|
+
next
|
|
143
|
+
end
|
|
144
|
+
# Hydration from server JSON dispatches on `<column>_set_attribute!`.
|
|
145
|
+
# The association and property DSLs only alias that hook when the
|
|
146
|
+
# column name itself is free as a method, so add it for a renamed
|
|
147
|
+
# accessor (`class` -> `class_field`).
|
|
148
|
+
setter = :"#{field}_set_attribute!"
|
|
149
|
+
target = :"#{key}_set_attribute!"
|
|
150
|
+
if setter != target && !klass.method_defined?(setter) && klass.method_defined?(target)
|
|
151
|
+
klass.send(:alias_method, setter, target)
|
|
119
152
|
end
|
|
120
153
|
class_fields.push(field)
|
|
121
154
|
end
|
|
122
155
|
klass
|
|
123
156
|
end
|
|
124
157
|
|
|
158
|
+
# Suffix appended to a column's Ruby accessor name when the natural
|
|
159
|
+
# (underscored) name is unusable.
|
|
160
|
+
RENAMED_ACCESSOR_SUFFIX = "_field"
|
|
161
|
+
|
|
162
|
+
# @!visibility private
|
|
163
|
+
# Chooses the Ruby accessor name for server column `field`. The natural
|
|
164
|
+
# name is `field.underscore`. It is replaced with `<name>_field` (then
|
|
165
|
+
# `<name>_field2`, ...) when it would shadow a method every
|
|
166
|
+
# Parse::Object relies on (`class`, `hash`, `save`, `send`,
|
|
167
|
+
# `object_id`, `changes`, ...), when a boolean column's class-level
|
|
168
|
+
# scope would shadow a class method (`freeze`, `name`), or when another
|
|
169
|
+
# column already claimed it (`fooBar` and `foo_bar` both underscore to
|
|
170
|
+
# `foo_bar`). The server column name is unchanged (it is kept with
|
|
171
|
+
# `field:`). Returns nil, after a warning, when no safe name exists.
|
|
172
|
+
# @return [Symbol, nil]
|
|
173
|
+
def self.safe_accessor_name(klass, field, data_type)
|
|
174
|
+
natural = field.to_s.underscore
|
|
175
|
+
unless natural.match?(/\A[a-z_][a-zA-Z0-9_]*\z/)
|
|
176
|
+
builder_warn "skipping column #{klass.parse_class}.#{field}: not a valid Ruby method name"
|
|
177
|
+
return nil
|
|
178
|
+
end
|
|
179
|
+
candidates = [natural, "#{natural}#{RENAMED_ACCESSOR_SUFFIX}"]
|
|
180
|
+
candidates.concat((2..9).map { |n| "#{natural}#{RENAMED_ACCESSOR_SUFFIX}#{n}" })
|
|
181
|
+
name = candidates.find { |c| !accessor_conflict?(klass, c.to_sym, data_type) }
|
|
182
|
+
if name.nil?
|
|
183
|
+
builder_warn "skipping column #{klass.parse_class}.#{field}: no free Ruby accessor name"
|
|
184
|
+
return nil
|
|
185
|
+
end
|
|
186
|
+
if name != natural
|
|
187
|
+
builder_warn "column #{klass.parse_class}.#{field} is exposed as ##{name} " \
|
|
188
|
+
"because ##{natural} would clash with an existing method or column"
|
|
189
|
+
end
|
|
190
|
+
name.to_sym
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# @!visibility private
|
|
194
|
+
# Whether defining accessor `key` on `klass` would replace an existing
|
|
195
|
+
# method or reuse a name another column already maps to.
|
|
196
|
+
def self.accessor_conflict?(klass, key, data_type)
|
|
197
|
+
return true if klass.fields.key?(key) || klass.field_map.key?(key)
|
|
198
|
+
return true if klass.respond_to?(:relations) && klass.relations.key?(key)
|
|
199
|
+
instance_names = [key, :"#{key}=", :"#{key}_changed?", :"#{key}_was"]
|
|
200
|
+
instance_names << :"#{key}?" if data_type == :boolean
|
|
201
|
+
return true if instance_names.any? { |m| method_taken?(klass, m) }
|
|
202
|
+
# Boolean columns also get a class-level scope named after the key.
|
|
203
|
+
return true if data_type == :boolean && klass.respond_to?(key, true)
|
|
204
|
+
false
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# @!visibility private
|
|
208
|
+
def self.method_taken?(klass, name)
|
|
209
|
+
klass.method_defined?(name) || klass.private_method_defined?(name)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# @!visibility private
|
|
213
|
+
def self.builder_warn(message)
|
|
214
|
+
warn "[Parse::Model::Builder] #{message}"
|
|
215
|
+
end
|
|
216
|
+
|
|
125
217
|
# @!visibility private
|
|
126
218
|
# Validates a server-returned `targetClass` string before forwarding
|
|
127
219
|
# it to `belongs_to`/`has_many`. Returns `nil` for missing or
|