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
data/lib/parse/webhooks.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
|
@@ -483,6 +514,12 @@ module Parse
|
|
|
483
514
|
result = invoke_handler(payload, registry)
|
|
484
515
|
end
|
|
485
516
|
|
|
517
|
+
if type == :after_find
|
|
518
|
+
# Parse Server can only keep or deny afterFind rows from an HTTP
|
|
519
|
+
# webhook (see after_find_reply!); the reply always passes them through.
|
|
520
|
+
return after_find_reply!(payload, result)
|
|
521
|
+
end
|
|
522
|
+
|
|
486
523
|
if result.is_a?(Parse::Object)
|
|
487
524
|
# if it is a Parse::Object, we will call the registered ActiveModel callbacks
|
|
488
525
|
if type == :before_save
|
|
@@ -510,18 +547,37 @@ module Parse
|
|
|
510
547
|
end
|
|
511
548
|
end
|
|
512
549
|
end
|
|
513
|
-
#
|
|
514
|
-
|
|
550
|
+
# Parse Server REPLACES the write with the object a beforeSave
|
|
551
|
+
# webhook returns, so reply with the client's full write plus the
|
|
552
|
+
# handler's changes (or `nil`, "unchanged", when there are none).
|
|
553
|
+
result = before_save_reply(payload, result, include_create_defaults: true)
|
|
515
554
|
elsif type == :before_delete
|
|
516
|
-
|
|
555
|
+
# Run only the BEFORE phase of the destroy chain: the object is not
|
|
556
|
+
# deleted yet, so after_destroy belongs to the afterDelete trigger.
|
|
557
|
+
# A halted chain must deny the delete, and Parse Server only treats
|
|
558
|
+
# an `{error}` body as a denial.
|
|
559
|
+
unless trusted_ruby_initiated
|
|
560
|
+
if result.send(:run_before_phase_callbacks, :destroy) == false
|
|
561
|
+
raise Parse::Webhooks::ResponseError, "Delete halted by before_destroy callback"
|
|
562
|
+
end
|
|
563
|
+
end
|
|
517
564
|
result = true
|
|
518
565
|
end
|
|
519
566
|
elsif type == :before_save && result == false
|
|
520
567
|
# If webhook block returns false, halt the save by throwing an error
|
|
521
568
|
raise Parse::Webhooks::ResponseError, "Save halted by before_save webhook"
|
|
522
|
-
elsif type == :before_save
|
|
523
|
-
#
|
|
524
|
-
|
|
569
|
+
elsif type == :before_save
|
|
570
|
+
# `true` / `nil` (or any non-Hash value) means "allow the write as
|
|
571
|
+
# sent". A Hash is a set of field overrides. Either way the reply is
|
|
572
|
+
# built from the client's write so nothing the client sent is lost;
|
|
573
|
+
# in-place edits to `parse_object` and field-guard reverts are
|
|
574
|
+
# carried along. `nil` tells Parse Server to keep the write as is.
|
|
575
|
+
overrides = result.is_a?(Hash) ? result : nil
|
|
576
|
+
result = before_save_reply(payload, payload&.memoized_parse_object, overrides: overrides)
|
|
577
|
+
elsif type == :before_delete && result == false
|
|
578
|
+
# Parse Server ignores a `{success:false}` body and deletes anyway;
|
|
579
|
+
# only an `{error}` body denies the delete.
|
|
580
|
+
raise Parse::Webhooks::ResponseError, "Delete halted by before_delete webhook"
|
|
525
581
|
end
|
|
526
582
|
|
|
527
583
|
# Auth- and LiveQuery-trigger dispatch (beforeLogin/afterLogin/
|
|
@@ -548,29 +604,10 @@ module Parse
|
|
|
548
604
|
result = true
|
|
549
605
|
end
|
|
550
606
|
|
|
551
|
-
#
|
|
552
|
-
#
|
|
553
|
-
#
|
|
554
|
-
#
|
|
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
|
|
607
|
+
# Field guards need no separate injection step: the pre-block step
|
|
608
|
+
# reverted the guarded fields on the memoized parse_object, and
|
|
609
|
+
# before_save_reply diffs that same instance against the client's
|
|
610
|
+
# write, so every revert is already in the reply.
|
|
574
611
|
|
|
575
612
|
if type == :after_save && payload&.parse_object.present? && payload.parse_object.is_a?(Parse::Object)
|
|
576
613
|
# The chained ActiveModel after_save/after_create callbacks are NOT
|
|
@@ -681,6 +718,294 @@ module Parse
|
|
|
681
718
|
nil
|
|
682
719
|
end
|
|
683
720
|
|
|
721
|
+
# Fires the chained ActiveModel after_destroy callbacks for an afterDelete
|
|
722
|
+
# delivery, exactly once per request (after both the class route and the
|
|
723
|
+
# `"*"` route ran), mirroring {run_after_save_chain}. Skipped for
|
|
724
|
+
# trusted-Ruby-initiated deletes, whose callbacks already ran locally
|
|
725
|
+
# around `destroy`, and when no afterDelete route is registered. The
|
|
726
|
+
# object is already gone, so a raising callback is logged and swallowed.
|
|
727
|
+
#
|
|
728
|
+
# @param payload [Parse::Webhooks::Payload] the afterDelete payload.
|
|
729
|
+
# @return [void]
|
|
730
|
+
def run_after_delete_chain(payload)
|
|
731
|
+
obj = nil
|
|
732
|
+
return unless payload&.after_delete?
|
|
733
|
+
obj = payload.parse_object
|
|
734
|
+
return unless obj.is_a?(Parse::Object)
|
|
735
|
+
return unless route_registered?(:after_delete, payload.parse_class)
|
|
736
|
+
return if payload.ruby_initiated? && payload.master? == true
|
|
737
|
+
obj.run_after_delete_callbacks
|
|
738
|
+
nil
|
|
739
|
+
rescue => e
|
|
740
|
+
warn "[Parse::Webhooks] afterDelete after_destroy callback raised for " \
|
|
741
|
+
"#{obj ? obj.class : "UnknownObject"}##{Parse::TerminalSafe.sanitize_line(obj&.id)} -- the object is " \
|
|
742
|
+
"already deleted; logging and continuing: #{e.class}: " \
|
|
743
|
+
"#{Parse::TerminalSafe.sanitize_line(Parse::Middleware::BodyBuilder.redact(e.message))}"
|
|
744
|
+
nil
|
|
745
|
+
end
|
|
746
|
+
|
|
747
|
+
# Whether a handler is registered for a trigger on a class, either on the
|
|
748
|
+
# class itself or on the generic `"*"` route.
|
|
749
|
+
#
|
|
750
|
+
# @param type [Symbol, String] the trigger (or `:function`).
|
|
751
|
+
# @param class_name [String, nil] the class (or function) name.
|
|
752
|
+
# @return [Boolean]
|
|
753
|
+
def route_registered?(type, class_name)
|
|
754
|
+
type = type.to_s.underscore.to_sym
|
|
755
|
+
table = routes[type]
|
|
756
|
+
return false if table.blank?
|
|
757
|
+
return table[class_name.to_s].present? if type == :function
|
|
758
|
+
table[class_name.to_s].present? || table["*"].present?
|
|
759
|
+
end
|
|
760
|
+
|
|
761
|
+
# Build the object a beforeSave webhook replies with.
|
|
762
|
+
#
|
|
763
|
+
# Parse Server's HTTP webhook adapter REPLACES the pending write with
|
|
764
|
+
# whatever object a beforeSave webhook returns (`this.data =
|
|
765
|
+
# response.object`). Replying with only the handler's changes therefore
|
|
766
|
+
# erases every other field the client sent. This method returns:
|
|
767
|
+
#
|
|
768
|
+
# - `nil` when nothing differs from the client's write. The router
|
|
769
|
+
# replies with an empty object (`{}`, no `success` key), which Parse
|
|
770
|
+
# Server treats as "keep the write exactly as sent", preserving every
|
|
771
|
+
# atomic operator. (`{"success": null}` is NOT equivalent: Parse
|
|
772
|
+
# Server's beforeSave adapter checks `typeof result === "object"`,
|
|
773
|
+
# which is true for null, and then throws deleting a key from it.)
|
|
774
|
+
# - otherwise a Hash: the client's write (rebuilt from the payload, with
|
|
775
|
+
# `Increment` / `Add` / `Remove` / `Delete` / relation operators passed
|
|
776
|
+
# through untouched) with the handler's changes layered on top. A field
|
|
777
|
+
# the handler (or a field guard) returned to its stored value is
|
|
778
|
+
# dropped from the write rather than rewritten.
|
|
779
|
+
#
|
|
780
|
+
# Fields are compared against a fresh build of the payload, so only the
|
|
781
|
+
# fields the handler actually changed are encoded from the Ruby object.
|
|
782
|
+
# Two limits come from what Parse Server sends: an operator on a dotted
|
|
783
|
+
# sub-key (`"meta.count"`) reaches the webhook only as its resulting
|
|
784
|
+
# sub-document, and is written back that way when the reply is not
|
|
785
|
+
# `nil`; and a field the handler rewrites is written as an absolute
|
|
786
|
+
# value.
|
|
787
|
+
#
|
|
788
|
+
# @param payload [Parse::Webhooks::Payload] the beforeSave payload.
|
|
789
|
+
# @param obj [Parse::Object, nil] the handler's object (nil when none).
|
|
790
|
+
# @param overrides [Hash, nil] field values a handler returned as a Hash.
|
|
791
|
+
# @param include_create_defaults [Boolean] on a create, also write the
|
|
792
|
+
# object's dirty fields the client did not send (declared defaults,
|
|
793
|
+
# default ACL), matching what an SDK-side create sends.
|
|
794
|
+
# @return [Hash, nil] the reply object, or nil for "unchanged".
|
|
795
|
+
def before_save_reply(payload, obj, overrides: nil, include_create_defaults: false)
|
|
796
|
+
return nil unless payload && payload.object?
|
|
797
|
+
raw_object = payload.raw_object
|
|
798
|
+
return nil unless raw_object.is_a?(Hash)
|
|
799
|
+
raw_original = payload.raw_original
|
|
800
|
+
raw_original = nil unless raw_original.is_a?(Hash) && raw_original.present?
|
|
801
|
+
|
|
802
|
+
changes = {}
|
|
803
|
+
drops = []
|
|
804
|
+
if obj.is_a?(Parse::Object)
|
|
805
|
+
changes, drops = handler_field_changes(payload, obj, raw_original)
|
|
806
|
+
if include_create_defaults && raw_original.nil?
|
|
807
|
+
client_keys = raw_object.keys.map(&:to_s)
|
|
808
|
+
dirty = obj.changed.map(&:to_sym) & snapshot_fields(obj)
|
|
809
|
+
wire_values(obj, dirty).each do |remote, value|
|
|
810
|
+
next if client_keys.include?(remote) || changes.key?(remote)
|
|
811
|
+
changes[remote] = value
|
|
812
|
+
end
|
|
813
|
+
end
|
|
814
|
+
end
|
|
815
|
+
overrides = overrides.as_json if overrides.is_a?(Hash)
|
|
816
|
+
return nil if changes.empty? && drops.empty? && overrides.blank?
|
|
817
|
+
|
|
818
|
+
reply = client_write_data(raw_object, raw_original)
|
|
819
|
+
drops.each { |remote| reply.delete(remote) }
|
|
820
|
+
reply.merge!(changes)
|
|
821
|
+
reply.merge!(overrides.transform_keys(&:to_s)) if overrides.present?
|
|
822
|
+
reply
|
|
823
|
+
end
|
|
824
|
+
|
|
825
|
+
# The client's write, rebuilt from a beforeSave payload. Parse Server
|
|
826
|
+
# serializes the pending object with `toJSON()`, which reports every
|
|
827
|
+
# pending top-level operator as its operator hash, so the operators
|
|
828
|
+
# survive here as sent. On an update, a field whose value equals the
|
|
829
|
+
# stored one was not written by the client and is left out.
|
|
830
|
+
#
|
|
831
|
+
# @param raw_object [Hash] the unscrubbed `object` hash.
|
|
832
|
+
# @param raw_original [Hash, nil] the unscrubbed `original` hash.
|
|
833
|
+
# @return [Hash]
|
|
834
|
+
# @!visibility private
|
|
835
|
+
def client_write_data(raw_object, raw_original)
|
|
836
|
+
data = {}
|
|
837
|
+
raw_object.each do |key, value|
|
|
838
|
+
key = key.to_s
|
|
839
|
+
next if BEFORE_SAVE_REPLY_SKIP_KEYS.include?(key)
|
|
840
|
+
if raw_original
|
|
841
|
+
# Parse Server drops objectId from an update write itself.
|
|
842
|
+
next if key == Parse::Model::OBJECT_ID
|
|
843
|
+
next if raw_original.key?(key) && raw_original[key] == value
|
|
844
|
+
end
|
|
845
|
+
data[key] = value
|
|
846
|
+
end
|
|
847
|
+
data
|
|
848
|
+
end
|
|
849
|
+
|
|
850
|
+
# Diff the handler's object against a fresh build of the same payload.
|
|
851
|
+
#
|
|
852
|
+
# Only fields that are dirty on either object can differ: a field the
|
|
853
|
+
# client did not write and the handler did not touch is clean on both.
|
|
854
|
+
# Comparing just those keeps the getters off fields the payload never
|
|
855
|
+
# carried (which would otherwise autofetch).
|
|
856
|
+
#
|
|
857
|
+
# @return [Array(Hash, Array<String>)] the changed wire fields with their
|
|
858
|
+
# encoded values, and the wire fields to drop from the write because
|
|
859
|
+
# the handler returned them to their stored value.
|
|
860
|
+
# @!visibility private
|
|
861
|
+
def handler_field_changes(payload, obj, raw_original)
|
|
862
|
+
fresh = payload.unmemoized_parse_object
|
|
863
|
+
fresh = nil unless fresh.instance_of?(obj.class)
|
|
864
|
+
candidates = obj.changed.map(&:to_sym)
|
|
865
|
+
candidates |= fresh.changed.map(&:to_sym) if fresh
|
|
866
|
+
candidates &= snapshot_fields(obj)
|
|
867
|
+
after = wire_values(obj, candidates)
|
|
868
|
+
before = fresh ? wire_values(fresh, candidates) : {}
|
|
869
|
+
|
|
870
|
+
changes = {}
|
|
871
|
+
drops = []
|
|
872
|
+
after.each do |remote, value|
|
|
873
|
+
next if before.key?(remote) && before[remote] == value
|
|
874
|
+
if raw_original
|
|
875
|
+
stored = raw_original.key?(remote) ? raw_original[remote].as_json : nil
|
|
876
|
+
unchanged = raw_original.key?(remote) ? stored == value : (value.is_a?(Hash) && value["__op"] == "Delete")
|
|
877
|
+
if unchanged
|
|
878
|
+
drops << remote
|
|
879
|
+
next
|
|
880
|
+
end
|
|
881
|
+
end
|
|
882
|
+
changes[remote] = value
|
|
883
|
+
end
|
|
884
|
+
|
|
885
|
+
# Relation edits are tracked on the proxies, not as field values. A
|
|
886
|
+
# relation field whose pending operation differs from the client's
|
|
887
|
+
# was changed by the handler or reverted by a field guard: write the
|
|
888
|
+
# handler's operation, or drop the client's when none is left.
|
|
889
|
+
ops_after = relation_ops(obj)
|
|
890
|
+
ops_before = fresh ? relation_ops(fresh) : {}
|
|
891
|
+
(ops_after.keys | ops_before.keys).each do |remote|
|
|
892
|
+
next if ops_after[remote] == ops_before[remote]
|
|
893
|
+
if ops_after.key?(remote)
|
|
894
|
+
changes[remote] = ops_after[remote]
|
|
895
|
+
else
|
|
896
|
+
drops << remote
|
|
897
|
+
end
|
|
898
|
+
end
|
|
899
|
+
[changes, drops]
|
|
900
|
+
end
|
|
901
|
+
|
|
902
|
+
# Pending relation operations of an object, keyed by remote field.
|
|
903
|
+
# Additions win over removals for the same field, matching
|
|
904
|
+
# `changes_payload`.
|
|
905
|
+
# @!visibility private
|
|
906
|
+
def relation_ops(obj)
|
|
907
|
+
return {} unless obj.respond_to?(:relation_changes?) && obj.relation_changes?
|
|
908
|
+
additions, removals = obj.relation_change_operations.as_json
|
|
909
|
+
(removals || {}).merge(additions || {})
|
|
910
|
+
end
|
|
911
|
+
|
|
912
|
+
# Declared, non-base, non-relation fields of an object.
|
|
913
|
+
# @!visibility private
|
|
914
|
+
def snapshot_fields(obj)
|
|
915
|
+
klass = obj.class
|
|
916
|
+
return [] unless klass.respond_to?(:fields)
|
|
917
|
+
klass.fields.reject do |name, type|
|
|
918
|
+
Parse::Properties::BASE_KEYS.include?(name.to_sym) || type == :relation
|
|
919
|
+
end.keys.map(&:to_sym)
|
|
920
|
+
end
|
|
921
|
+
|
|
922
|
+
# Encode the given fields of an object in Parse wire form, keyed by
|
|
923
|
+
# remote field name, using the same encoder the SDK uses for a save
|
|
924
|
+
# (`attribute_updates`). A nil value encodes as a Delete operator.
|
|
925
|
+
# Autofetch is suspended so reading a field never reaches the network.
|
|
926
|
+
# @!visibility private
|
|
927
|
+
def wire_values(obj, fields)
|
|
928
|
+
return {} if fields.empty?
|
|
929
|
+
names = fields.map(&:to_s).freeze
|
|
930
|
+
prior_autofetch = obj.instance_variable_get(:@_autofetch_disabled)
|
|
931
|
+
obj.instance_variable_set(:@_autofetch_disabled, true)
|
|
932
|
+
obj.define_singleton_method(:changed) { names }
|
|
933
|
+
begin
|
|
934
|
+
obj.attribute_updates.as_json
|
|
935
|
+
ensure
|
|
936
|
+
singleton = obj.singleton_class
|
|
937
|
+
singleton.send(:remove_method, :changed) if singleton.method_defined?(:changed, false)
|
|
938
|
+
obj.instance_variable_set(:@_autofetch_disabled, prior_autofetch)
|
|
939
|
+
end
|
|
940
|
+
end
|
|
941
|
+
|
|
942
|
+
# Resolve an afterFind handler result.
|
|
943
|
+
#
|
|
944
|
+
# Parse Server's afterFind handling maps each returned row through
|
|
945
|
+
# `toJSONwithObjects`, which turns any plain JSON object (everything an
|
|
946
|
+
# HTTP webhook can send) into `{}`, and crashes on a non-array body. A
|
|
947
|
+
# missing result keeps the matched rows. So an HTTP afterFind can observe
|
|
948
|
+
# the rows or deny the query, but it cannot rewrite them. The reply is
|
|
949
|
+
# always `nil` (sent as `{}`, with no `success` key), and a handler that tries
|
|
950
|
+
# to drop or add rows is refused with an error rather than having the
|
|
951
|
+
# rows it meant to hide returned anyway.
|
|
952
|
+
#
|
|
953
|
+
# @param payload [Parse::Webhooks::Payload] the afterFind payload.
|
|
954
|
+
# @param result [Object] the handler's return value.
|
|
955
|
+
# @return [nil]
|
|
956
|
+
# @raise [Parse::Webhooks::ResponseError] when the handler returned
|
|
957
|
+
# `false` or a different set of rows.
|
|
958
|
+
# @!visibility private
|
|
959
|
+
def after_find_reply!(payload, result)
|
|
960
|
+
if result == false
|
|
961
|
+
raise Parse::Webhooks::ResponseError, "afterFind rejected by webhook handler"
|
|
962
|
+
end
|
|
963
|
+
if result.is_a?(Array) && payload
|
|
964
|
+
returned = result.map { |row| after_find_row_id(row) }
|
|
965
|
+
matched = Array(payload.objects).map { |row| after_find_row_id(row) }
|
|
966
|
+
unless returned.size == matched.size && returned.sort_by(&:to_s) == matched.sort_by(&:to_s)
|
|
967
|
+
warn "[Parse::Webhooks] an afterFind handler for " \
|
|
968
|
+
"#{Parse::TerminalSafe.sanitize_line(payload.parse_class)} returned a different " \
|
|
969
|
+
"set of rows. Parse Server cannot apply row changes from an HTTP afterFind " \
|
|
970
|
+
"webhook, so the find is denied instead. Filter in beforeFind, or deny with error!."
|
|
971
|
+
raise Parse::Webhooks::ResponseError,
|
|
972
|
+
"afterFind webhooks cannot filter or replace results"
|
|
973
|
+
end
|
|
974
|
+
end
|
|
975
|
+
nil
|
|
976
|
+
end
|
|
977
|
+
|
|
978
|
+
# @!visibility private
|
|
979
|
+
def after_find_row_id(row)
|
|
980
|
+
case row
|
|
981
|
+
when Hash then row["objectId"] || row[:objectId] || row["id"] || row[:id]
|
|
982
|
+
else row.respond_to?(:id) ? row.id : row
|
|
983
|
+
end
|
|
984
|
+
end
|
|
985
|
+
|
|
986
|
+
# The value sent as `success` for a routed (or unrouted) trigger result.
|
|
987
|
+
# `nil` for beforeSave / afterFind means "pass through": the Rack app
|
|
988
|
+
# then replies `{}` (see {PASS_THROUGH_BODY}).
|
|
989
|
+
#
|
|
990
|
+
# @param payload [Parse::Webhooks::Payload] the request payload.
|
|
991
|
+
# @param result [Object] the dispatch result.
|
|
992
|
+
# @return [Object] the success value.
|
|
993
|
+
# @!visibility private
|
|
994
|
+
def trigger_success_value(payload, result)
|
|
995
|
+
# beforeSave: nil means "keep the write as sent", a Hash replaces it.
|
|
996
|
+
return (result.is_a?(Hash) ? result : nil) if payload.before_save?
|
|
997
|
+
# afterFind: only "no result" is safe (keeps the rows); see after_find_reply!.
|
|
998
|
+
return nil if payload.after_find?
|
|
999
|
+
result.nil? ? true : result
|
|
1000
|
+
end
|
|
1001
|
+
|
|
1002
|
+
# Whether a dispatch result should be sent as {PASS_THROUGH_BODY}.
|
|
1003
|
+
# @!visibility private
|
|
1004
|
+
def pass_through_reply?(payload, result)
|
|
1005
|
+
result.nil? && payload.respond_to?(:trigger?) && payload.trigger? &&
|
|
1006
|
+
(payload.before_save? || payload.after_find?)
|
|
1007
|
+
end
|
|
1008
|
+
|
|
684
1009
|
# Generates a success response for Parse Server.
|
|
685
1010
|
# @param data [Object] the data to send back with the success.
|
|
686
1011
|
# @return [Hash] a success data payload
|
|
@@ -690,9 +1015,12 @@ module Parse
|
|
|
690
1015
|
|
|
691
1016
|
# Generates an error response for Parse Server.
|
|
692
1017
|
# @param data [Object] the data to send back with the error.
|
|
1018
|
+
# @param code [Integer, nil] an optional Parse error code to include.
|
|
693
1019
|
# @return [Hash] a error data payload
|
|
694
|
-
def error(data = false)
|
|
695
|
-
{ error: data }
|
|
1020
|
+
def error(data = false, code = nil)
|
|
1021
|
+
body = { error: data }
|
|
1022
|
+
body[:code] = code unless code.nil?
|
|
1023
|
+
body.to_json
|
|
696
1024
|
end
|
|
697
1025
|
|
|
698
1026
|
# @!attribute key
|
|
@@ -862,6 +1190,14 @@ module Parse
|
|
|
862
1190
|
return response.finish
|
|
863
1191
|
end
|
|
864
1192
|
|
|
1193
|
+
# A trigger whose body names a different class than the one the
|
|
1194
|
+
# request was routed to (the URL path) is forged or misrouted. Refuse
|
|
1195
|
+
# it before any handler runs or any typed object is built from it.
|
|
1196
|
+
if payload.trigger? && payload.payload_class_mismatch?
|
|
1197
|
+
response.write error("Webhook payload class does not match the trigger class.")
|
|
1198
|
+
return response.finish
|
|
1199
|
+
end
|
|
1200
|
+
|
|
865
1201
|
if self.logging.present?
|
|
866
1202
|
# Everything interpolated below arrives in the webhook request body:
|
|
867
1203
|
# the trigger/function names, the object id, and the whole payload are
|
|
@@ -887,7 +1223,14 @@ module Parse
|
|
|
887
1223
|
begin
|
|
888
1224
|
result = true
|
|
889
1225
|
if payload.function? && payload.function_name.present?
|
|
1226
|
+
# An unknown function must fail. Answering success would tell the
|
|
1227
|
+
# caller that a function ran when nothing did.
|
|
1228
|
+
unless Parse::Webhooks.route_registered?(:function, payload.function_name)
|
|
1229
|
+
raise Parse::Webhooks::ResponseError,
|
|
1230
|
+
"Webhook function #{payload.function_name} is not registered."
|
|
1231
|
+
end
|
|
890
1232
|
result = Parse::Webhooks.call_route(:function, payload.function_name, payload)
|
|
1233
|
+
result = true if result.nil?
|
|
891
1234
|
elsif payload.trigger? && payload.parse_class.present? && payload.trigger_name.present?
|
|
892
1235
|
# call hooks subscribed to the specific class
|
|
893
1236
|
result = Parse::Webhooks.call_route(payload.trigger_name, payload.parse_class, payload)
|
|
@@ -896,25 +1239,42 @@ module Parse
|
|
|
896
1239
|
generic_result = Parse::Webhooks.call_route(payload.trigger_name, "*", payload)
|
|
897
1240
|
result = generic_result if generic_result.present? && result.nil?
|
|
898
1241
|
|
|
899
|
-
# Fire the chained ActiveModel after_save/after_create
|
|
900
|
-
# exactly once per delivery -- after BOTH
|
|
901
|
-
# app that registers both a class route
|
|
902
|
-
# double-fire them.
|
|
1242
|
+
# Fire the chained ActiveModel after_save/after_create (or
|
|
1243
|
+
# after_destroy) callbacks exactly once per delivery -- after BOTH
|
|
1244
|
+
# route calls above -- so an app that registers both a class route
|
|
1245
|
+
# and a `"*"` route doesn't double-fire them. Each is a no-op for
|
|
1246
|
+
# every other trigger.
|
|
903
1247
|
Parse::Webhooks.run_after_save_chain(payload)
|
|
1248
|
+
Parse::Webhooks.run_after_delete_chain(payload)
|
|
1249
|
+
|
|
1250
|
+
# An unrouted trigger (or a handler that returned nil) passes the
|
|
1251
|
+
# operation through unchanged, in the shape each trigger needs.
|
|
1252
|
+
result = Parse::Webhooks.trigger_success_value(payload, result)
|
|
1253
|
+
elsif payload.trigger?
|
|
1254
|
+
# A trigger the router cannot attribute to a class: pass it through
|
|
1255
|
+
# unchanged rather than failing the client's operation.
|
|
1256
|
+
if self.logging.present?
|
|
1257
|
+
puts "[Webhooks] --> Could not find mapping route for " \
|
|
1258
|
+
"#{Parse::TerminalSafe.sanitize_line(Parse::Middleware::BodyBuilder.redact(payload.to_json))}"
|
|
1259
|
+
end
|
|
1260
|
+
result = Parse::Webhooks.trigger_success_value(payload, nil)
|
|
904
1261
|
else
|
|
905
1262
|
if self.logging.present?
|
|
906
1263
|
puts "[Webhooks] --> Could not find mapping route for " \
|
|
907
1264
|
"#{Parse::TerminalSafe.sanitize_line(Parse::Middleware::BodyBuilder.redact(payload.to_json))}"
|
|
908
1265
|
end
|
|
1266
|
+
raise Parse::Webhooks::ResponseError, "Unrecognized webhook request."
|
|
909
1267
|
end
|
|
910
1268
|
|
|
911
|
-
|
|
1269
|
+
body = pass_through_reply?(payload, result) ? PASS_THROUGH_BODY : success(result)
|
|
912
1270
|
if self.logging.present?
|
|
1271
|
+
# The reply can echo the client's write (passwords, auth data,
|
|
1272
|
+
# tokens a handler returned), so it is redacted like the request.
|
|
913
1273
|
puts "[Webhooks::Response] ----------------------------"
|
|
914
|
-
puts Parse::TerminalSafe.sanitize(
|
|
1274
|
+
puts Parse::TerminalSafe.sanitize(Parse::Middleware::BodyBuilder.redact(body))
|
|
915
1275
|
puts "----------------------------------------------------\n"
|
|
916
1276
|
end
|
|
917
|
-
response.write
|
|
1277
|
+
response.write body
|
|
918
1278
|
# Schedule any after_response work to run once this reply is flushed,
|
|
919
1279
|
# off the client's critical path. Registered on the success path so the
|
|
920
1280
|
# deferred work overlaps a response Parse Server will act on.
|
|
@@ -930,7 +1290,19 @@ module Parse
|
|
|
930
1290
|
puts "[Webhooks::ResponseError] >> #{Parse::TerminalSafe.sanitize_line(payload.function_name)}: " \
|
|
931
1291
|
"#{Parse::TerminalSafe.sanitize_line(e.to_s)}"
|
|
932
1292
|
end
|
|
933
|
-
|
|
1293
|
+
code = e.respond_to?(:code) ? e.code : nil
|
|
1294
|
+
response.write error(e.to_s, code)
|
|
1295
|
+
return response.finish
|
|
1296
|
+
rescue StandardError => e
|
|
1297
|
+
# Anything else a handler (or the router) raised. Answer with a JSON
|
|
1298
|
+
# error so Parse Server denies the operation cleanly, and keep the
|
|
1299
|
+
# exception message out of the reply: it can quote record data or
|
|
1300
|
+
# internals. The log line carries the class and a redacted message.
|
|
1301
|
+
where = payload.trigger? ? "#{payload.trigger_name} #{payload.parse_class}" : payload.function_name
|
|
1302
|
+
warn "[Webhooks::Error] >> #{Parse::TerminalSafe.sanitize_line(where.to_s)}: #{e.class}: " \
|
|
1303
|
+
"#{Parse::TerminalSafe.sanitize_line(Parse::Middleware::BodyBuilder.redact(e.message.to_s))}"
|
|
1304
|
+
response = Rack::Response.new
|
|
1305
|
+
response.write error("Webhook handler failed.")
|
|
934
1306
|
return response.finish
|
|
935
1307
|
end
|
|
936
1308
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: parse-stack-next
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 5.
|
|
4
|
+
version: 5.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Adrian Curtin
|
|
@@ -251,8 +251,11 @@ files:
|
|
|
251
251
|
- lib/parse/agent/constraint_translator.rb
|
|
252
252
|
- lib/parse/agent/describe.rb
|
|
253
253
|
- lib/parse/agent/errors.rb
|
|
254
|
+
- lib/parse/agent/field_names.rb
|
|
255
|
+
- lib/parse/agent/field_policy.rb
|
|
254
256
|
- lib/parse/agent/log_levels.rb
|
|
255
257
|
- lib/parse/agent/mcp_client.rb
|
|
258
|
+
- lib/parse/agent/mcp_deployments.rb
|
|
256
259
|
- lib/parse/agent/mcp_dispatcher.rb
|
|
257
260
|
- lib/parse/agent/mcp_rack_app.rb
|
|
258
261
|
- lib/parse/agent/mcp_server.rb
|
|
@@ -284,6 +287,7 @@ files:
|
|
|
284
287
|
- lib/parse/api/users.rb
|
|
285
288
|
- lib/parse/atlas_search.rb
|
|
286
289
|
- lib/parse/atlas_search/index_manager.rb
|
|
290
|
+
- lib/parse/atlas_search/protected_paths.rb
|
|
287
291
|
- lib/parse/atlas_search/result.rb
|
|
288
292
|
- lib/parse/atlas_search/search_builder.rb
|
|
289
293
|
- lib/parse/atlas_search/session.rb
|
|
@@ -403,8 +407,10 @@ files:
|
|
|
403
407
|
- lib/parse/query/ordering.rb
|
|
404
408
|
- lib/parse/retrieval.rb
|
|
405
409
|
- lib/parse/retrieval/agent_tool.rb
|
|
410
|
+
- lib/parse/retrieval/benchmark.rb
|
|
406
411
|
- lib/parse/retrieval/chunk.rb
|
|
407
412
|
- lib/parse/retrieval/chunker.rb
|
|
413
|
+
- lib/parse/retrieval/profiles.rb
|
|
408
414
|
- lib/parse/retrieval/reranker.rb
|
|
409
415
|
- lib/parse/retrieval/reranker/cohere.rb
|
|
410
416
|
- lib/parse/retrieval/reranker/voyage.rb
|
|
@@ -429,6 +435,7 @@ files:
|
|
|
429
435
|
- lib/parse/two_factor_auth/user_extension.rb
|
|
430
436
|
- lib/parse/vector_search.rb
|
|
431
437
|
- lib/parse/vector_search/hybrid.rb
|
|
438
|
+
- lib/parse/vector_search/index_definition.rb
|
|
432
439
|
- lib/parse/webhooks.rb
|
|
433
440
|
- lib/parse/webhooks/payload.rb
|
|
434
441
|
- lib/parse/webhooks/registration.rb
|