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.
Files changed (106) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1148 -0
  3. data/README.md +39 -23
  4. data/docs/TEST_SERVER.md +2 -2
  5. data/docs/acl_clp_guide.md +7 -0
  6. data/docs/atlas_vector_search_guide.md +181 -13
  7. data/docs/caching.md +50 -0
  8. data/docs/client_sdk_guide.md +11 -0
  9. data/docs/mcp_guide.md +332 -9
  10. data/docs/mongodb_direct_guide.md +27 -0
  11. data/docs/usage_guide.md +38 -0
  12. data/docs/webhooks_guide.md +126 -17
  13. data/lib/parse/acl_scope.rb +230 -50
  14. data/lib/parse/agent/approval_gate.rb +0 -0
  15. data/lib/parse/agent/constraint_translator.rb +42 -15
  16. data/lib/parse/agent/describe.rb +11 -3
  17. data/lib/parse/agent/field_names.rb +53 -0
  18. data/lib/parse/agent/field_policy.rb +74 -0
  19. data/lib/parse/agent/mcp_deployments.rb +426 -0
  20. data/lib/parse/agent/mcp_dispatcher.rb +8 -1
  21. data/lib/parse/agent/mcp_rack_app.rb +424 -45
  22. data/lib/parse/agent/mcp_server.rb +23 -1
  23. data/lib/parse/agent/mcp_subscriptions.rb +124 -6
  24. data/lib/parse/agent/metadata_registry.rb +67 -8
  25. data/lib/parse/agent/prompt_hardening.rb +9 -3
  26. data/lib/parse/agent/tools.rb +378 -29
  27. data/lib/parse/agent.rb +298 -21
  28. data/lib/parse/api/batch.rb +10 -1
  29. data/lib/parse/api/schema.rb +23 -4
  30. data/lib/parse/api/sessions.rb +6 -2
  31. data/lib/parse/api/users.rb +88 -14
  32. data/lib/parse/atlas_search/index_manager.rb +5 -1
  33. data/lib/parse/atlas_search/protected_paths.rb +236 -0
  34. data/lib/parse/atlas_search.rb +144 -24
  35. data/lib/parse/authorization.rb +289 -4
  36. data/lib/parse/cache/sub_cache.rb +26 -0
  37. data/lib/parse/client/authentication.rb +19 -1
  38. data/lib/parse/client/batch.rb +264 -35
  39. data/lib/parse/client/body_builder.rb +21 -0
  40. data/lib/parse/client/caching.rb +371 -27
  41. data/lib/parse/client/request.rb +26 -14
  42. data/lib/parse/client/response.rb +49 -6
  43. data/lib/parse/client.rb +227 -40
  44. data/lib/parse/clp_scope.rb +317 -25
  45. data/lib/parse/console.rb +2 -2
  46. data/lib/parse/embeddings/voyage.rb +181 -17
  47. data/lib/parse/graphql/type_generator.rb +3 -0
  48. data/lib/parse/live_query/client.rb +72 -2
  49. data/lib/parse/lock_backend.rb +4 -3
  50. data/lib/parse/model/acl.rb +119 -21
  51. data/lib/parse/model/associations/belongs_to.rb +25 -3
  52. data/lib/parse/model/associations/collection_proxy.rb +146 -17
  53. data/lib/parse/model/associations/has_many.rb +49 -9
  54. data/lib/parse/model/associations/has_one.rb +3 -1
  55. data/lib/parse/model/associations/pointer_collection_proxy.rb +145 -17
  56. data/lib/parse/model/associations/relation_collection_proxy.rb +229 -38
  57. data/lib/parse/model/bytes.rb +13 -5
  58. data/lib/parse/model/classes/role.rb +95 -0
  59. data/lib/parse/model/classes/session.rb +304 -1
  60. data/lib/parse/model/classes/user.rb +104 -8
  61. data/lib/parse/model/core/actions.rb +287 -70
  62. data/lib/parse/model/core/builder.rb +100 -8
  63. data/lib/parse/model/core/create_lock.rb +27 -2
  64. data/lib/parse/model/core/describe.rb +2 -0
  65. data/lib/parse/model/core/fetching.rb +50 -5
  66. data/lib/parse/model/core/field_guards.rb +16 -8
  67. data/lib/parse/model/core/pluralized_aliases.rb +8 -4
  68. data/lib/parse/model/core/properties.rb +488 -39
  69. data/lib/parse/model/core/querying.rb +7 -0
  70. data/lib/parse/model/core/schema.rb +5 -3
  71. data/lib/parse/model/core/search_indexing.rb +63 -0
  72. data/lib/parse/model/core/vector_searchable.rb +35 -6
  73. data/lib/parse/model/file.rb +9 -2
  74. data/lib/parse/model/geopoint.rb +61 -13
  75. data/lib/parse/model/model.rb +160 -9
  76. data/lib/parse/model/object.rb +317 -18
  77. data/lib/parse/model/phone.rb +54 -5
  78. data/lib/parse/model/pointer.rb +40 -6
  79. data/lib/parse/model/push.rb +11 -1
  80. data/lib/parse/mongodb.rb +189 -61
  81. data/lib/parse/pipeline_security.rb +415 -26
  82. data/lib/parse/query/constraint.rb +31 -1
  83. data/lib/parse/query/constraints.rb +89 -59
  84. data/lib/parse/query/cursor.rb +3 -1
  85. data/lib/parse/query/operation.rb +62 -8
  86. data/lib/parse/query/ordering.rb +34 -6
  87. data/lib/parse/query.rb +1871 -167
  88. data/lib/parse/retrieval/agent_tool.rb +225 -8
  89. data/lib/parse/retrieval/benchmark.rb +149 -0
  90. data/lib/parse/retrieval/profiles.rb +320 -0
  91. data/lib/parse/retrieval/retriever.rb +10 -1
  92. data/lib/parse/retrieval.rb +2 -0
  93. data/lib/parse/schema/search_index_migrator.rb +23 -5
  94. data/lib/parse/schema.rb +74 -18
  95. data/lib/parse/stack/tasks.rb +6 -4
  96. data/lib/parse/stack/version.rb +1 -1
  97. data/lib/parse/stack.rb +91 -14
  98. data/lib/parse/two_factor_auth/user_extension.rb +14 -2
  99. data/lib/parse/two_factor_auth.rb +11 -0
  100. data/lib/parse/vector_search/hybrid.rb +110 -33
  101. data/lib/parse/vector_search/index_definition.rb +237 -0
  102. data/lib/parse/vector_search.rb +259 -23
  103. data/lib/parse/webhooks/payload.rb +136 -6
  104. data/lib/parse/webhooks/replay_protection.rb +58 -20
  105. data/lib/parse/webhooks.rb +715 -40
  106. metadata +8 -1
@@ -83,7 +83,38 @@ module Parse
83
83
  class Webhooks
84
84
  # The error to be raised in registered trigger or function webhook blocks that
85
85
  # will trigger the Parse::Webhooks application to return the proper error response.
86
- class ResponseError < StandardError; end
86
+ #
87
+ # An optional numeric `code` is carried on the exception and written into
88
+ # the error body as `"code"`. Note that Parse Server's HTTP webhook adapter
89
+ # (as of 9.10) reports every webhook error to the client as code 141
90
+ # (`SCRIPT_FAILED`) and does not forward a custom code; the code remains
91
+ # visible to in-process callers such as {Parse::Webhooks.run_function}.
92
+ class ResponseError < StandardError
93
+ # @return [Integer, nil] the Parse error code requested by the handler.
94
+ attr_reader :code
95
+
96
+ # @param message [String] the error message.
97
+ # @param code [Integer, nil] an optional Parse error code.
98
+ def initialize(message = nil, code: nil)
99
+ super(message)
100
+ @code = code
101
+ end
102
+ end
103
+
104
+ # The reply that tells Parse Server to keep a beforeSave write, or the
105
+ # afterFind rows, exactly as they were: a JSON object with no `success`
106
+ # key, so the adapter's `body.success` is `undefined`. A `null` success
107
+ # is not safe for beforeSave (see {Parse::Webhooks.before_save_reply}).
108
+ PASS_THROUGH_BODY = "{}"
109
+
110
+ # Keys never echoed back to Parse Server in a beforeSave reply. These are
111
+ # server-managed (`className`, timestamps) or credential material that a
112
+ # client write cannot legitimately carry.
113
+ # @!visibility private
114
+ BEFORE_SAVE_REPLY_SKIP_KEYS = %w[
115
+ className createdAt updatedAt
116
+ sessionToken session_token _hashed_password _password_history
117
+ ].freeze
87
118
 
88
119
  # The authentication-side triggers (local underscore form). These carry a
89
120
  # `_User` / `_Session` as the payload object but are NOT object save/delete
@@ -470,6 +501,8 @@ module Parse
470
501
  master: payload.master? || false,
471
502
  is_new: payload.original.blank?,
472
503
  )
504
+ # A guard revert is not the handler assigning the ACL.
505
+ pre_obj.reset_webhook_handler_acl_assigned! if pre_obj.respond_to?(:reset_webhook_handler_acl_assigned!)
473
506
  end
474
507
  end
475
508
  end
@@ -483,6 +516,12 @@ module Parse
483
516
  result = invoke_handler(payload, registry)
484
517
  end
485
518
 
519
+ if type == :after_find
520
+ # Parse Server can only keep or deny afterFind rows from an HTTP
521
+ # webhook (see after_find_reply!); the reply always passes them through.
522
+ return after_find_reply!(payload, result)
523
+ end
524
+
486
525
  if result.is_a?(Parse::Object)
487
526
  # if it is a Parse::Object, we will call the registered ActiveModel callbacks
488
527
  if type == :before_save
@@ -492,6 +531,7 @@ module Parse
492
531
  # ran ActiveModel before_save callbacks locally. A client-spoofed
493
532
  # `_RB_` without master falls through and runs them here.
494
533
  unless trusted_ruby_initiated
534
+ adopt_request_user_as_acl_owner!(payload, result)
495
535
  before_save_result = result.run_before_save_callbacks
496
536
  # If a before_save callback halted the chain (returned false), reject the save.
497
537
  if before_save_result == false
@@ -510,18 +550,37 @@ module Parse
510
550
  end
511
551
  end
512
552
  end
513
- # For before_save, return the changes payload (what Parse Server expects)
514
- result = result.changes_payload
553
+ # Parse Server REPLACES the write with the object a beforeSave
554
+ # webhook returns, so reply with the client's full write plus the
555
+ # handler's changes (or `nil`, "unchanged", when there are none).
556
+ result = before_save_reply(payload, result, include_create_defaults: true)
515
557
  elsif type == :before_delete
516
- result.run_callbacks(:destroy) { false }
558
+ # Run only the BEFORE phase of the destroy chain: the object is not
559
+ # deleted yet, so after_destroy belongs to the afterDelete trigger.
560
+ # A halted chain must deny the delete, and Parse Server only treats
561
+ # an `{error}` body as a denial.
562
+ unless trusted_ruby_initiated
563
+ if result.send(:run_before_phase_callbacks, :destroy) == false
564
+ raise Parse::Webhooks::ResponseError, "Delete halted by before_destroy callback"
565
+ end
566
+ end
517
567
  result = true
518
568
  end
519
569
  elsif type == :before_save && result == false
520
570
  # If webhook block returns false, halt the save by throwing an error
521
571
  raise Parse::Webhooks::ResponseError, "Save halted by before_save webhook"
522
- elsif type == :before_save && (result == true || result.nil?)
523
- # Open Source Parse server does not accept true results on before_save hooks.
524
- result = {}
572
+ elsif type == :before_save
573
+ # `true` / `nil` (or any non-Hash value) means "allow the write as
574
+ # sent". A Hash is a set of field overrides. Either way the reply is
575
+ # built from the client's write so nothing the client sent is lost;
576
+ # in-place edits to `parse_object` and field-guard reverts are
577
+ # carried along. `nil` tells Parse Server to keep the write as is.
578
+ overrides = result.is_a?(Hash) ? result : nil
579
+ result = before_save_reply(payload, payload&.memoized_parse_object, overrides: overrides)
580
+ elsif type == :before_delete && result == false
581
+ # Parse Server ignores a `{success:false}` body and deletes anyway;
582
+ # only an `{error}` body denies the delete.
583
+ raise Parse::Webhooks::ResponseError, "Delete halted by before_delete webhook"
525
584
  end
526
585
 
527
586
  # Auth- and LiveQuery-trigger dispatch (beforeLogin/afterLogin/
@@ -548,29 +607,10 @@ module Parse
548
607
  result = true
549
608
  end
550
609
 
551
- # Guard-injection: when a handler returns a Hash (or true/nil normalized
552
- # to {}) for a class with field_guards, Parse Server would otherwise
553
- # merge the response with the client's original payload and persist
554
- # the client-supplied values for guarded fields. Inject the pre-built
555
- # parse_object's changes_payload entries for any guarded field so the
556
- # response carries the appropriate revert (Delete op on create, prior
557
- # value on update). The Parse::Object return path already runs through
558
- # changes_payload on the same memoized instance and therefore needs no
559
- # extra injection.
560
- if type == :before_save && result.is_a?(Hash) && payload && payload.object?
561
- guard_klass = (className.present? && className != "*") ? Parse::Object.find_class(className) : nil
562
- if guard_klass && guard_klass.respond_to?(:field_guards) && guard_klass.field_guards.any?
563
- pre_obj = payload.parse_object # same memoized instance the pre-block step mutated
564
- if pre_obj.respond_to?(:changes_payload)
565
- guard_payload = pre_obj.changes_payload
566
- field_map = guard_klass.respond_to?(:field_map) ? guard_klass.field_map : {}
567
- guard_klass.field_guards.each_key do |field|
568
- remote = (field_map[field.to_sym] || field).to_s
569
- result[remote] = guard_payload[remote] if guard_payload.key?(remote)
570
- end
571
- end
572
- end
573
- end
610
+ # Field guards need no separate injection step: the pre-block step
611
+ # reverted the guarded fields on the memoized parse_object, and
612
+ # before_save_reply diffs that same instance against the client's
613
+ # write, so every revert is already in the reply.
574
614
 
575
615
  if type == :after_save && payload&.parse_object.present? && payload.parse_object.is_a?(Parse::Object)
576
616
  # The chained ActiveModel after_save/after_create callbacks are NOT
@@ -681,6 +721,594 @@ module Parse
681
721
  nil
682
722
  end
683
723
 
724
+ # Fires the chained ActiveModel after_destroy callbacks for an afterDelete
725
+ # delivery, exactly once per request (after both the class route and the
726
+ # `"*"` route ran), mirroring {run_after_save_chain}. Skipped for
727
+ # trusted-Ruby-initiated deletes, whose callbacks already ran locally
728
+ # around `destroy`, and when no afterDelete route is registered. The
729
+ # object is already gone, so a raising callback is logged and swallowed.
730
+ #
731
+ # @param payload [Parse::Webhooks::Payload] the afterDelete payload.
732
+ # @return [void]
733
+ def run_after_delete_chain(payload)
734
+ obj = nil
735
+ return unless payload&.after_delete?
736
+ obj = payload.parse_object
737
+ return unless obj.is_a?(Parse::Object)
738
+ return unless route_registered?(:after_delete, payload.parse_class)
739
+ return if payload.ruby_initiated? && payload.master? == true
740
+ obj.run_after_delete_callbacks
741
+ nil
742
+ rescue => e
743
+ warn "[Parse::Webhooks] afterDelete after_destroy callback raised for " \
744
+ "#{obj ? obj.class : "UnknownObject"}##{Parse::TerminalSafe.sanitize_line(obj&.id)} -- the object is " \
745
+ "already deleted; logging and continuing: #{e.class}: " \
746
+ "#{Parse::TerminalSafe.sanitize_line(Parse::Middleware::BodyBuilder.redact(e.message))}"
747
+ nil
748
+ end
749
+
750
+ # Whether a handler is registered for a trigger on a class, either on the
751
+ # class itself or on the generic `"*"` route.
752
+ #
753
+ # @param type [Symbol, String] the trigger (or `:function`).
754
+ # @param class_name [String, nil] the class (or function) name.
755
+ # @return [Boolean]
756
+ def route_registered?(type, class_name)
757
+ type = type.to_s.underscore.to_sym
758
+ table = routes[type]
759
+ return false if table.blank?
760
+ return table[class_name.to_s].present? if type == :function
761
+ table[class_name.to_s].present? || table["*"].present?
762
+ end
763
+
764
+ # Build the object a beforeSave webhook replies with.
765
+ #
766
+ # Parse Server's HTTP webhook adapter REPLACES the pending write with
767
+ # whatever object a beforeSave webhook returns (`this.data =
768
+ # response.object`). Replying with only the handler's changes therefore
769
+ # erases every other field the client sent. This method returns:
770
+ #
771
+ # - `nil` when nothing differs from the client's write. The router
772
+ # replies with an empty object (`{}`, no `success` key), which Parse
773
+ # Server treats as "keep the write exactly as sent", preserving every
774
+ # atomic operator. (`{"success": null}` is NOT equivalent: Parse
775
+ # Server's beforeSave adapter checks `typeof result === "object"`,
776
+ # which is true for null, and then throws deleting a key from it.)
777
+ # - otherwise a Hash: the client's write (rebuilt from the payload, with
778
+ # `Increment` / `Add` / `Remove` / `Delete` / relation operators passed
779
+ # through untouched) with the handler's changes layered on top. A field
780
+ # the handler (or a field guard) returned to its stored value is
781
+ # dropped from the write rather than rewritten.
782
+ #
783
+ # Fields are compared against a fresh build of the payload, so only the
784
+ # fields the handler actually changed are encoded from the Ruby object.
785
+ # A field the handler rewrites is written as an absolute value. An
786
+ # operator on a dotted sub-key (`"meta.count"`) reaches the webhook only
787
+ # as its resulting sub-document, so on an update a changed sub-document
788
+ # is written back as dotted keys for just the sub-keys that differ from
789
+ # the stored one. A concurrent write to another sub-key survives; the
790
+ # changed sub-key itself is written as its resulting value. The split
791
+ # is one level deep (a changed sub-key is written whole at
792
+ # `field.sub`), and a field whose writes would carry a typed value is
793
+ # written whole.
794
+ #
795
+ # @param payload [Parse::Webhooks::Payload] the beforeSave payload.
796
+ # @param obj [Parse::Object, nil] the handler's object (nil when none).
797
+ # @param overrides [Hash, nil] field values a handler returned as a Hash.
798
+ # @param include_create_defaults [Boolean] on a create, also write the
799
+ # object's dirty fields the client did not send (declared defaults and
800
+ # the ACL the class policy resolves), matching what an SDK-side create
801
+ # sends.
802
+ # @return [Hash, nil] the reply object, or nil for "unchanged".
803
+ def before_save_reply(payload, obj, overrides: nil, include_create_defaults: false)
804
+ return nil unless payload && payload.object?
805
+ raw_object = payload.raw_object
806
+ return nil unless raw_object.is_a?(Hash)
807
+ raw_original = payload.raw_original
808
+ raw_original = nil unless raw_original.is_a?(Hash) && raw_original.present?
809
+
810
+ changes = {}
811
+ drops = []
812
+ if obj.is_a?(Parse::Object)
813
+ changes, drops = handler_field_changes(payload, obj, raw_original)
814
+ if include_create_defaults && raw_original.nil?
815
+ client_keys = raw_object.keys.map(&:to_s)
816
+ dirty = obj.changed.map(&:to_sym) & snapshot_fields(obj)
817
+ # This includes the ACL. A create with no `ACL` key is stored by
818
+ # Parse Server as public read and write, so a class with an ACL
819
+ # policy always replies with the ACL that policy resolved (owned
820
+ # by the requesting user where the policy names an owner; see
821
+ # {adopt_request_user_as_acl_owner!}) or the ACL the handler set.
822
+ # An ACL the client sent is kept as sent.
823
+ wire_values(obj, dirty).each do |remote, value|
824
+ next if client_keys.include?(remote) || changes.key?(remote)
825
+ changes[remote] = value
826
+ end
827
+ end
828
+ # An ACL the handler assigned on a create is written even when it
829
+ # equals the default stamp or the client's ACL, whatever the handler
830
+ # returned (the object, `true`, `nil`, or a Hash, whose own `ACL`
831
+ # still wins below). Diffing alone misses `obj.acl = Parse::ACL.new`
832
+ # under a `{}` default, and a create with no `ACL` key is stored
833
+ # public read and write.
834
+ if handler_assigned_acl?(obj) && !changes.key?("ACL")
835
+ changes.merge!(wire_values(obj, [:acl]).slice("ACL"))
836
+ end
837
+ end
838
+ overrides = overrides.as_json if overrides.is_a?(Hash)
839
+ return nil if changes.empty? && drops.empty? && overrides.blank?
840
+
841
+ reply = client_write_data(raw_object, raw_original)
842
+ overrides = overrides.present? ? remote_override_keys(payload, overrides) : {}
843
+ # A field the handler wrote or dropped replaces the client's dotted
844
+ # sub-key writes for it; MongoDB refuses `meta` and `meta.x` together.
845
+ (drops + changes.keys + overrides.keys).each do |remote|
846
+ reply.delete_if { |key, _| key.start_with?("#{remote}.") }
847
+ end
848
+ drops.each { |remote| reply.delete(remote) }
849
+ reply.merge!(changes)
850
+ reply.merge!(overrides)
851
+ fold_dotted_overrides!(reply, overrides.keys, raw_object: raw_object, create: raw_original.nil?)
852
+ reply
853
+ end
854
+
855
+ # A handler's Hash override keyed by Ruby names (`{acl: ...}`,
856
+ # `{author_name: ...}`) is mapped to the remote field names the reply
857
+ # uses, so it replaces the matching field instead of sitting beside it.
858
+ # A dotted key maps its first segment.
859
+ # @!visibility private
860
+ def remote_override_keys(payload, overrides)
861
+ klass = begin
862
+ name = payload.parse_class
863
+ name.present? ? Parse::Object.find_class(name) : nil
864
+ rescue StandardError
865
+ nil
866
+ end
867
+ map = klass.respond_to?(:field_map) ? klass.field_map : {}
868
+ overrides.each_with_object({}) do |(key, value), out|
869
+ key = key.to_s
870
+ head, rest = key.split(".", 2)
871
+ remote = map[head.to_sym]
872
+ head = remote.to_s if remote
873
+ out[rest ? "#{head}.#{rest}" : head] = value
874
+ end
875
+ end
876
+
877
+ # A handler's Hash override may name a sub-key (`"meta.y"`) of a field
878
+ # the reply writes whole (a create, or a field written whole on an
879
+ # update). MongoDB refuses `meta` and `meta.y` in one update, so the
880
+ # sub-key write is folded into the whole value. An override deeper
881
+ # than one level (`"meta.count.value"`) is folded into a `field.sub`
882
+ # write seeded from the pending object, because Parse Server rebuilds
883
+ # the afterSave object from a dotted key only one level deep. On a
884
+ # create every dotted override folds into its whole field.
885
+ #
886
+ # When the value it folds into is not a plain sub-document, a client's
887
+ # value is replaced by the handler's sub-key, and two of the handler's
888
+ # own overrides that conflict (`"meta.a" => 5` with `"meta.a.b" => 6`)
889
+ # raise {ResponseError}.
890
+ # @!visibility private
891
+ def fold_dotted_overrides!(reply, override_keys, raw_object: {}, create: false)
892
+ handler_keys = override_keys.to_set
893
+ override_keys.sort_by { |k| k.count(".") }.each do |key|
894
+ next unless key.include?(".") && reply.key?(key)
895
+ segments = key.split(".")
896
+ # The nearest ancestor path the reply writes whole, if any.
897
+ parent = (1...segments.length).map { |n| segments.first(n).join(".") }.reverse.find { |p| reply.key?(p) }
898
+ unless parent
899
+ next if !create && segments.length <= 2
900
+ parent = create ? segments.first : segments.first(2).join(".")
901
+ seed = parent.split(".").reduce(raw_object) { |cur, seg| cur.is_a?(Hash) ? cur[seg] : nil }
902
+ reply[parent] = plain_sub_document?(seed) ? seed.deep_dup : {}
903
+ end
904
+ value = reply.delete(key)
905
+ whole = reply[parent]
906
+ if plain_sub_document?(whole)
907
+ whole = whole.deep_dup
908
+ elsif handler_keys.include?(parent)
909
+ raise Parse::Webhooks::ResponseError,
910
+ "before_save reply: #{key} conflicts with #{parent} in the handler's reply"
911
+ else
912
+ whole = {}
913
+ end
914
+ *dirs, leaf = segments.drop(parent.count(".") + 1)
915
+ node = dirs.reduce(whole) do |cur, dir|
916
+ cur[dir] = {} unless plain_sub_document?(cur[dir])
917
+ cur[dir]
918
+ end
919
+ if value.is_a?(Hash) && value.key?("__op")
920
+ # A whole write stores its value as data, so an operator folded
921
+ # into it is applied here rather than written as a Hash.
922
+ result = fold_override_op(key, node[leaf], value)
923
+ if result.equal?(FOLD_DELETE)
924
+ node.delete(leaf)
925
+ else
926
+ node[leaf] = result
927
+ end
928
+ else
929
+ node[leaf] = value
930
+ end
931
+ reply[parent] = whole
932
+ end
933
+ reply
934
+ end
935
+
936
+ # @!visibility private
937
+ FOLD_DELETE = Object.new.freeze
938
+
939
+ # Apply a handler's sub-key operator to the value it replaces inside a
940
+ # sub-document the reply writes whole, with Parse Server's semantics
941
+ # for that operator. Raises {ResponseError} (an `{error}` reply, so
942
+ # Parse Server refuses the save) for an operator that cannot be applied
943
+ # to a plain value, rather than storing the operator Hash as data.
944
+ # @param key [String] the dotted override key, for the error message.
945
+ # @param current [Object] the value at that path in the whole write.
946
+ # @param op [Hash] the operator Hash (`{"__op" => ...}`).
947
+ # @return [Object] the new value, or {FOLD_DELETE} to remove the key.
948
+ # @!visibility private
949
+ def fold_override_op(key, current, op)
950
+ name = op["__op"]
951
+ case name
952
+ when "Delete"
953
+ FOLD_DELETE
954
+ when "Increment"
955
+ amount = op["amount"]
956
+ unless amount.is_a?(Numeric) && (current.nil? || current.is_a?(Numeric))
957
+ raise Parse::Webhooks::ResponseError,
958
+ "before_save reply: cannot apply Increment to #{key} (#{current.class})"
959
+ end
960
+ (current || 0) + amount
961
+ when "Add", "AddUnique", "Remove"
962
+ objects = op["objects"]
963
+ unless objects.is_a?(Array) && (current.nil? || current.is_a?(Array))
964
+ raise Parse::Webhooks::ResponseError,
965
+ "before_save reply: cannot apply #{name} to #{key} (#{current.class})"
966
+ end
967
+ base = current || []
968
+ case name
969
+ when "Add" then base + objects
970
+ when "AddUnique" then base + objects.reject { |o| base.include?(o) }.uniq
971
+ else base.reject { |o| objects.include?(o) }
972
+ end
973
+ else
974
+ raise Parse::Webhooks::ResponseError,
975
+ "before_save reply: operator #{name.inspect} on #{key} cannot be combined " \
976
+ "with a whole write of its parent field"
977
+ end
978
+ end
979
+
980
+ # @!visibility private
981
+ def handler_assigned_acl?(obj)
982
+ return true if obj.instance_variable_get(:@_webhook_reply_acl) == true
983
+ obj.respond_to?(:webhook_handler_acl_assigned?) && obj.webhook_handler_acl_assigned?
984
+ end
985
+
986
+ # On a client create, an owner-based ACL policy (`:owner_else_private`,
987
+ # the shipped default, `:owner_else_public`, `:owner_but_public_read`)
988
+ # resolves its owner the way an SDK create `as:` the requesting user
989
+ # does: when the declared owner field holds no value, the user who made
990
+ # the request owns the record. A request without a user (anonymous, or
991
+ # master key) gets the policy's fallback. Leaves the object alone when
992
+ # the handler or the client already set an ACL.
993
+ # @!visibility private
994
+ def adopt_request_user_as_acl_owner!(payload, obj)
995
+ return unless payload && payload.original.nil?
996
+ if handler_assigned_acl?(obj)
997
+ # The handler's ACL is final: keep the save-time policy resolver
998
+ # from replacing it, even when it equals the default stamp.
999
+ obj.instance_variable_set(:@_acl_pristine, false)
1000
+ return
1001
+ end
1002
+ return unless obj.is_a?(Parse::Object) && obj.instance_variable_get(:@_acl_pristine)
1003
+ return if obj.instance_variable_get(:@_acl_owner_override)
1004
+ klass = obj.class
1005
+ return if klass.respond_to?(:builtin_acl_default_active?) && klass.builtin_acl_default_active?
1006
+ return unless klass.respond_to?(:acl_policy_setting) && klass.acl_policy_setting.to_s.start_with?("owner_")
1007
+ field = klass.acl_owner_field
1008
+ if field == :self
1009
+ adopt_self_owned_acl!(obj, klass)
1010
+ return
1011
+ end
1012
+ # A master-key request is not the user's own write.
1013
+ return if payload.master?
1014
+ user = payload.user
1015
+ return unless user.is_a?(Parse::User) && user.id.present?
1016
+ if field && obj.respond_to?(field)
1017
+ return if obj.send(field).present?
1018
+ end
1019
+ obj.instance_variable_set(:@_acl_owner_override, user)
1020
+ end
1021
+
1022
+ # A self-owned user (`acl_policy ..., owner: :self`) owns its own
1023
+ # record, never the requesting user. Its objectId is assigned by Parse
1024
+ # Server after beforeSave, and Parse Server adds the new user's own
1025
+ # read and write to the ACL of every `_User` create. So the reply
1026
+ # carries the policy's ACL without the owner entry (`{}`, or public
1027
+ # read under `:owner_but_public_read`), which Parse Server completes
1028
+ # with the user's own grant. The save-time resolver is skipped, since
1029
+ # it would pre-generate an objectId the server does not use.
1030
+ # @!visibility private
1031
+ def adopt_self_owned_acl!(obj, klass)
1032
+ acl = if klass.acl_policy_setting == :owner_but_public_read
1033
+ Parse::ACL.everyone(true, false)
1034
+ else
1035
+ Parse::ACL.new
1036
+ end
1037
+ klass.instance_method(:acl=).bind_call(obj, acl)
1038
+ obj.instance_variable_set(:@_acl_pristine, false)
1039
+ obj.instance_variable_set(:@_webhook_reply_acl, true)
1040
+ end
1041
+
1042
+ # The client's write, rebuilt from a beforeSave payload. Parse Server
1043
+ # serializes the pending object with `toJSON()`, which reports every
1044
+ # pending top-level operator as its operator hash, so the operators
1045
+ # survive here as sent. On an update, a field whose value equals the
1046
+ # stored one was not written by the client and is left out, and a
1047
+ # changed sub-document is split into dotted sub-key writes (see
1048
+ # {sub_document_write}).
1049
+ #
1050
+ # @param raw_object [Hash] the unscrubbed `object` hash.
1051
+ # @param raw_original [Hash, nil] the unscrubbed `original` hash.
1052
+ # @return [Hash]
1053
+ # @!visibility private
1054
+ def client_write_data(raw_object, raw_original)
1055
+ data = {}
1056
+ raw_object.each do |key, value|
1057
+ key = key.to_s
1058
+ next if BEFORE_SAVE_REPLY_SKIP_KEYS.include?(key)
1059
+ if raw_original
1060
+ # Parse Server drops objectId from an update write itself.
1061
+ next if key == Parse::Model::OBJECT_ID
1062
+ next if raw_original.key?(key) && raw_original[key] == value
1063
+ dotted = sub_document_write(key, value, raw_original[key])
1064
+ if dotted
1065
+ data.merge!(dotted)
1066
+ next
1067
+ end
1068
+ end
1069
+ data[key] = value
1070
+ end
1071
+ data
1072
+ end
1073
+
1074
+ # Keys never split into dotted sub-key writes: their values are
1075
+ # replaced as a whole by Parse Server.
1076
+ # @!visibility private
1077
+ BEFORE_SAVE_REPLY_WHOLE_KEYS = %w[ACL authData].freeze
1078
+
1079
+ # Dotted sub-key writes for a sub-document the client changed.
1080
+ #
1081
+ # Parse Server applies a client's `"meta.count"` operator to its pending
1082
+ # object and sends the webhook only the resulting `meta`, which matches
1083
+ # a whole-object write of the same value. Writing just the sub-keys that
1084
+ # differ from the stored sub-document gives the same result as either
1085
+ # client write, without overwriting sub-keys another request changed in
1086
+ # the meantime. The split is one level deep: a changed sub-key is
1087
+ # written whole at `field.sub`, nested objects included, because Parse
1088
+ # Server rebuilds the afterSave object from a non-operator dotted key
1089
+ # only one level deep (`"meta.count.value"` would set `meta.count` to
1090
+ # the leaf). A sub-key missing from the new value is written as a
1091
+ # `Delete` operator.
1092
+ #
1093
+ # Parse Server stores a dotted value as sent, without the type transform
1094
+ # a whole write gets, so a Date would land as a plain sub-document
1095
+ # instead of a BSON date. When any write would carry a typed value
1096
+ # (`{"__type": ...}`), the field is written whole instead.
1097
+ #
1098
+ # @param key [String] the top-level field name.
1099
+ # @param value [Object] the field's value in the pending object.
1100
+ # @param stored [Object] the field's stored value.
1101
+ # @return [Hash, nil] the dotted writes, or nil to write the field whole.
1102
+ # @!visibility private
1103
+ def sub_document_write(key, value, stored)
1104
+ return nil if key.start_with?("_") || BEFORE_SAVE_REPLY_WHOLE_KEYS.include?(key)
1105
+ return nil unless splittable_level?(value, stored)
1106
+ writes = {}
1107
+ diff_sub_document(key, value, stored, writes)
1108
+ return nil if writes.empty? || writes.values.any? { |v| typed_value?(v) }
1109
+ writes
1110
+ end
1111
+
1112
+ # Collect the one-level dotted writes that turn `stored` into `value`
1113
+ # under `path`.
1114
+ # @!visibility private
1115
+ def diff_sub_document(path, value, stored, writes)
1116
+ (value.keys | stored.keys).each do |sub|
1117
+ new_present = value.key?(sub)
1118
+ old_present = stored.key?(sub)
1119
+ next if new_present && old_present && value[sub] == stored[sub]
1120
+ sub_path = "#{path}.#{sub}"
1121
+ writes[sub_path] = new_present ? value[sub] : { "__op" => "Delete" }
1122
+ end
1123
+ end
1124
+
1125
+ # Whether a pair of values can be diffed key by key: both plain JSON
1126
+ # objects, with sub-keys that are usable as path segments.
1127
+ # @!visibility private
1128
+ def splittable_level?(value, stored)
1129
+ return false unless plain_sub_document?(value) && plain_sub_document?(stored)
1130
+ keys = value.keys | stored.keys
1131
+ return false if keys.empty?
1132
+ keys.none? { |k| k.to_s.empty? || k.to_s.include?(".") || k.to_s.start_with?("$") }
1133
+ end
1134
+
1135
+ # Whether a JSON value is, or contains, a Parse typed value
1136
+ # (`{"__type": ...}`: Date, Bytes, Pointer, File, GeoPoint, ...).
1137
+ # @!visibility private
1138
+ def typed_value?(value)
1139
+ case value
1140
+ when Hash then value.key?("__type") || value.values.any? { |v| typed_value?(v) }
1141
+ when Array then value.any? { |v| typed_value?(v) }
1142
+ else false
1143
+ end
1144
+ end
1145
+
1146
+ # A JSON object field value: a Hash that is not a Parse operator or a
1147
+ # typed value (Pointer, Date, File, GeoPoint, ...).
1148
+ # @!visibility private
1149
+ def plain_sub_document?(value)
1150
+ value.is_a?(Hash) && !value.key?("__op") && !value.key?("__type")
1151
+ end
1152
+
1153
+ # Diff the handler's object against a fresh build of the same payload.
1154
+ #
1155
+ # Only fields that are dirty on either object can differ: a field the
1156
+ # client did not write and the handler did not touch is clean on both.
1157
+ # Comparing just those keeps the getters off fields the payload never
1158
+ # carried (which would otherwise autofetch).
1159
+ #
1160
+ # @return [Array(Hash, Array<String>)] the changed wire fields with their
1161
+ # encoded values, and the wire fields to drop from the write because
1162
+ # the handler returned them to their stored value.
1163
+ # @!visibility private
1164
+ def handler_field_changes(payload, obj, raw_original)
1165
+ fresh = payload.unmemoized_parse_object
1166
+ fresh = nil unless fresh.instance_of?(obj.class)
1167
+ candidates = obj.changed.map(&:to_sym)
1168
+ candidates |= fresh.changed.map(&:to_sym) if fresh
1169
+ candidates &= snapshot_fields(obj)
1170
+ after = wire_values(obj, candidates)
1171
+ before = fresh ? wire_values(fresh, candidates) : {}
1172
+
1173
+ changes = {}
1174
+ drops = []
1175
+ after.each do |remote, value|
1176
+ next if before.key?(remote) && before[remote] == value
1177
+ if raw_original
1178
+ stored = raw_original.key?(remote) ? raw_original[remote].as_json : nil
1179
+ unchanged = raw_original.key?(remote) ? stored == value : (value.is_a?(Hash) && value["__op"] == "Delete")
1180
+ if unchanged
1181
+ drops << remote
1182
+ next
1183
+ end
1184
+ end
1185
+ changes[remote] = value
1186
+ end
1187
+
1188
+ # Relation edits are tracked on the proxies, not as field values. A
1189
+ # relation field whose pending operation differs from the client's
1190
+ # was changed by the handler or reverted by a field guard: write the
1191
+ # handler's operation, or drop the client's when none is left.
1192
+ ops_after = relation_ops(obj)
1193
+ ops_before = fresh ? relation_ops(fresh) : {}
1194
+ (ops_after.keys | ops_before.keys).each do |remote|
1195
+ next if ops_after[remote] == ops_before[remote]
1196
+ if ops_after.key?(remote)
1197
+ changes[remote] = ops_after[remote]
1198
+ else
1199
+ drops << remote
1200
+ end
1201
+ end
1202
+ [changes, drops]
1203
+ end
1204
+
1205
+ # Pending relation operations of an object, keyed by remote field.
1206
+ # Additions win over removals for the same field, matching
1207
+ # `changes_payload`.
1208
+ # @!visibility private
1209
+ def relation_ops(obj)
1210
+ return {} unless obj.respond_to?(:relation_changes?) && obj.relation_changes?
1211
+ additions, removals = obj.relation_change_operations.as_json
1212
+ (removals || {}).merge(additions || {})
1213
+ end
1214
+
1215
+ # Declared, non-base, non-relation fields of an object.
1216
+ # @!visibility private
1217
+ def snapshot_fields(obj)
1218
+ klass = obj.class
1219
+ return [] unless klass.respond_to?(:fields)
1220
+ klass.fields.reject do |name, type|
1221
+ Parse::Properties::BASE_KEYS.include?(name.to_sym) || type == :relation
1222
+ end.keys.map(&:to_sym)
1223
+ end
1224
+
1225
+ # Encode the given fields of an object in Parse wire form, keyed by
1226
+ # remote field name, using the same encoder the SDK uses for a save
1227
+ # (`attribute_updates`). A nil value encodes as a Delete operator.
1228
+ # Autofetch is suspended so reading a field never reaches the network.
1229
+ # @!visibility private
1230
+ def wire_values(obj, fields)
1231
+ return {} if fields.empty?
1232
+ names = fields.map(&:to_s).freeze
1233
+ prior_autofetch = obj.instance_variable_get(:@_autofetch_disabled)
1234
+ obj.instance_variable_set(:@_autofetch_disabled, true)
1235
+ obj.define_singleton_method(:changed) { names }
1236
+ begin
1237
+ obj.attribute_updates.as_json
1238
+ ensure
1239
+ singleton = obj.singleton_class
1240
+ singleton.send(:remove_method, :changed) if singleton.method_defined?(:changed, false)
1241
+ obj.instance_variable_set(:@_autofetch_disabled, prior_autofetch)
1242
+ end
1243
+ end
1244
+
1245
+ # Resolve an afterFind handler result.
1246
+ #
1247
+ # Parse Server's afterFind handling maps each returned row through
1248
+ # `toJSONwithObjects`, which turns any plain JSON object (everything an
1249
+ # HTTP webhook can send) into `{}`, and crashes on a non-array body. A
1250
+ # missing result keeps the matched rows. So an HTTP afterFind can observe
1251
+ # the rows or deny the query, but it cannot rewrite them. The reply is
1252
+ # always `nil` (sent as `{}`, with no `success` key), and a handler that tries
1253
+ # to drop or add rows is refused with an error rather than having the
1254
+ # rows it meant to hide returned anyway.
1255
+ #
1256
+ # @param payload [Parse::Webhooks::Payload] the afterFind payload.
1257
+ # @param result [Object] the handler's return value.
1258
+ # @return [nil]
1259
+ # @raise [Parse::Webhooks::ResponseError] when the handler returned
1260
+ # `false` or a different set of rows.
1261
+ # @!visibility private
1262
+ def after_find_reply!(payload, result)
1263
+ if result == false
1264
+ raise Parse::Webhooks::ResponseError, "afterFind rejected by webhook handler"
1265
+ end
1266
+ if result.is_a?(Array) && payload
1267
+ returned = result.map { |row| after_find_row_id(row) }
1268
+ matched = Array(payload.objects).map { |row| after_find_row_id(row) }
1269
+ unless returned.size == matched.size && returned.sort_by(&:to_s) == matched.sort_by(&:to_s)
1270
+ warn "[Parse::Webhooks] an afterFind handler for " \
1271
+ "#{Parse::TerminalSafe.sanitize_line(payload.parse_class)} returned a different " \
1272
+ "set of rows. Parse Server cannot apply row changes from an HTTP afterFind " \
1273
+ "webhook, so the find is denied instead. Filter in beforeFind, or deny with error!."
1274
+ raise Parse::Webhooks::ResponseError,
1275
+ "afterFind webhooks cannot filter or replace results"
1276
+ end
1277
+ end
1278
+ nil
1279
+ end
1280
+
1281
+ # @!visibility private
1282
+ def after_find_row_id(row)
1283
+ case row
1284
+ when Hash then row["objectId"] || row[:objectId] || row["id"] || row[:id]
1285
+ else row.respond_to?(:id) ? row.id : row
1286
+ end
1287
+ end
1288
+
1289
+ # The value sent as `success` for a routed (or unrouted) trigger result.
1290
+ # `nil` for beforeSave / afterFind means "pass through": the Rack app
1291
+ # then replies `{}` (see {PASS_THROUGH_BODY}).
1292
+ #
1293
+ # @param payload [Parse::Webhooks::Payload] the request payload.
1294
+ # @param result [Object] the dispatch result.
1295
+ # @return [Object] the success value.
1296
+ # @!visibility private
1297
+ def trigger_success_value(payload, result)
1298
+ # beforeSave: nil means "keep the write as sent", a Hash replaces it.
1299
+ return (result.is_a?(Hash) ? result : nil) if payload.before_save?
1300
+ # afterFind: only "no result" is safe (keeps the rows); see after_find_reply!.
1301
+ return nil if payload.after_find?
1302
+ result.nil? ? true : result
1303
+ end
1304
+
1305
+ # Whether a dispatch result should be sent as {PASS_THROUGH_BODY}.
1306
+ # @!visibility private
1307
+ def pass_through_reply?(payload, result)
1308
+ result.nil? && payload.respond_to?(:trigger?) && payload.trigger? &&
1309
+ (payload.before_save? || payload.after_find?)
1310
+ end
1311
+
684
1312
  # Generates a success response for Parse Server.
685
1313
  # @param data [Object] the data to send back with the success.
686
1314
  # @return [Hash] a success data payload
@@ -690,9 +1318,12 @@ module Parse
690
1318
 
691
1319
  # Generates an error response for Parse Server.
692
1320
  # @param data [Object] the data to send back with the error.
1321
+ # @param code [Integer, nil] an optional Parse error code to include.
693
1322
  # @return [Hash] a error data payload
694
- def error(data = false)
695
- { error: data }.to_json
1323
+ def error(data = false, code = nil)
1324
+ body = { error: data }
1325
+ body[:code] = code unless code.nil?
1326
+ body.to_json
696
1327
  end
697
1328
 
698
1329
  # @!attribute key
@@ -862,6 +1493,14 @@ module Parse
862
1493
  return response.finish
863
1494
  end
864
1495
 
1496
+ # A trigger whose body names a different class than the one the
1497
+ # request was routed to (the URL path) is forged or misrouted. Refuse
1498
+ # it before any handler runs or any typed object is built from it.
1499
+ if payload.trigger? && payload.payload_class_mismatch?
1500
+ response.write error("Webhook payload class does not match the trigger class.")
1501
+ return response.finish
1502
+ end
1503
+
865
1504
  if self.logging.present?
866
1505
  # Everything interpolated below arrives in the webhook request body:
867
1506
  # the trigger/function names, the object id, and the whole payload are
@@ -887,7 +1526,14 @@ module Parse
887
1526
  begin
888
1527
  result = true
889
1528
  if payload.function? && payload.function_name.present?
1529
+ # An unknown function must fail. Answering success would tell the
1530
+ # caller that a function ran when nothing did.
1531
+ unless Parse::Webhooks.route_registered?(:function, payload.function_name)
1532
+ raise Parse::Webhooks::ResponseError,
1533
+ "Webhook function #{payload.function_name} is not registered."
1534
+ end
890
1535
  result = Parse::Webhooks.call_route(:function, payload.function_name, payload)
1536
+ result = true if result.nil?
891
1537
  elsif payload.trigger? && payload.parse_class.present? && payload.trigger_name.present?
892
1538
  # call hooks subscribed to the specific class
893
1539
  result = Parse::Webhooks.call_route(payload.trigger_name, payload.parse_class, payload)
@@ -896,25 +1542,42 @@ module Parse
896
1542
  generic_result = Parse::Webhooks.call_route(payload.trigger_name, "*", payload)
897
1543
  result = generic_result if generic_result.present? && result.nil?
898
1544
 
899
- # Fire the chained ActiveModel after_save/after_create callbacks
900
- # exactly once per delivery -- after BOTH route calls above -- so an
901
- # app that registers both a class route and a `"*"` route doesn't
902
- # double-fire them. No-op for every non-afterSave trigger.
1545
+ # Fire the chained ActiveModel after_save/after_create (or
1546
+ # after_destroy) callbacks exactly once per delivery -- after BOTH
1547
+ # route calls above -- so an app that registers both a class route
1548
+ # and a `"*"` route doesn't double-fire them. Each is a no-op for
1549
+ # every other trigger.
903
1550
  Parse::Webhooks.run_after_save_chain(payload)
1551
+ Parse::Webhooks.run_after_delete_chain(payload)
1552
+
1553
+ # An unrouted trigger (or a handler that returned nil) passes the
1554
+ # operation through unchanged, in the shape each trigger needs.
1555
+ result = Parse::Webhooks.trigger_success_value(payload, result)
1556
+ elsif payload.trigger?
1557
+ # A trigger the router cannot attribute to a class: pass it through
1558
+ # unchanged rather than failing the client's operation.
1559
+ if self.logging.present?
1560
+ puts "[Webhooks] --> Could not find mapping route for " \
1561
+ "#{Parse::TerminalSafe.sanitize_line(Parse::Middleware::BodyBuilder.redact(payload.to_json))}"
1562
+ end
1563
+ result = Parse::Webhooks.trigger_success_value(payload, nil)
904
1564
  else
905
1565
  if self.logging.present?
906
1566
  puts "[Webhooks] --> Could not find mapping route for " \
907
1567
  "#{Parse::TerminalSafe.sanitize_line(Parse::Middleware::BodyBuilder.redact(payload.to_json))}"
908
1568
  end
1569
+ raise Parse::Webhooks::ResponseError, "Unrecognized webhook request."
909
1570
  end
910
1571
 
911
- result = true if result.nil?
1572
+ body = pass_through_reply?(payload, result) ? PASS_THROUGH_BODY : success(result)
912
1573
  if self.logging.present?
1574
+ # The reply can echo the client's write (passwords, auth data,
1575
+ # tokens a handler returned), so it is redacted like the request.
913
1576
  puts "[Webhooks::Response] ----------------------------"
914
- puts Parse::TerminalSafe.sanitize(success(result))
1577
+ puts Parse::TerminalSafe.sanitize(Parse::Middleware::BodyBuilder.redact(body))
915
1578
  puts "----------------------------------------------------\n"
916
1579
  end
917
- response.write success(result)
1580
+ response.write body
918
1581
  # Schedule any after_response work to run once this reply is flushed,
919
1582
  # off the client's critical path. Registered on the success path so the
920
1583
  # deferred work overlaps a response Parse Server will act on.
@@ -930,7 +1593,19 @@ module Parse
930
1593
  puts "[Webhooks::ResponseError] >> #{Parse::TerminalSafe.sanitize_line(payload.function_name)}: " \
931
1594
  "#{Parse::TerminalSafe.sanitize_line(e.to_s)}"
932
1595
  end
933
- response.write error(e.to_s)
1596
+ code = e.respond_to?(:code) ? e.code : nil
1597
+ response.write error(e.to_s, code)
1598
+ return response.finish
1599
+ rescue StandardError => e
1600
+ # Anything else a handler (or the router) raised. Answer with a JSON
1601
+ # error so Parse Server denies the operation cleanly, and keep the
1602
+ # exception message out of the reply: it can quote record data or
1603
+ # internals. The log line carries the class and a redacted message.
1604
+ where = payload.trigger? ? "#{payload.trigger_name} #{payload.parse_class}" : payload.function_name
1605
+ warn "[Webhooks::Error] >> #{Parse::TerminalSafe.sanitize_line(where.to_s)}: #{e.class}: " \
1606
+ "#{Parse::TerminalSafe.sanitize_line(Parse::Middleware::BodyBuilder.redact(e.message.to_s))}"
1607
+ response = Rack::Response.new
1608
+ response.write error("Webhook handler failed.")
934
1609
  return response.finish
935
1610
  end
936
1611