parse-stack-next 5.3.0 → 5.4.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/.gitignore +2 -0
- data/CHANGELOG.md +461 -0
- data/Gemfile +7 -0
- data/Gemfile.lock +12 -4
- data/README.md +160 -3
- data/Rakefile +52 -3
- data/docs/atlas_vector_search_guide.md +86 -2
- data/docs/client_sdk_guide.md +5 -0
- data/docs/mcp_guide.md +59 -4
- data/docs/mongodb_direct_guide.md +93 -1
- data/docs/usage_guide.md +11 -1
- data/docs/webhooks_guide.md +418 -0
- data/examples/README.md +46 -0
- data/examples/basic_client.rb +93 -0
- data/examples/basic_server.rb +109 -0
- data/examples/live_query_listener.rb +98 -0
- data/examples/rag_chatbot.rb +221 -0
- data/examples/webhook_server.rb +111 -0
- data/lib/parse/agent/mcp_rack_app.rb +285 -62
- data/lib/parse/agent/tools.rb +45 -5
- data/lib/parse/api/aggregate.rb +7 -1
- data/lib/parse/api/cloud_functions.rb +12 -4
- data/lib/parse/api/hooks.rb +46 -9
- data/lib/parse/api/objects.rb +16 -2
- data/lib/parse/api/path_segment.rb +33 -0
- data/lib/parse/api/server.rb +94 -0
- data/lib/parse/api/users.rb +58 -2
- data/lib/parse/atlas_search.rb +7 -7
- data/lib/parse/client/body_builder.rb +5 -0
- data/lib/parse/client/protocol.rb +4 -0
- data/lib/parse/client.rb +55 -2
- data/lib/parse/embeddings/spend_cap.rb +255 -0
- data/lib/parse/embeddings.rb +1 -0
- data/lib/parse/live_query/client.rb +3 -1
- data/lib/parse/live_query/subscription.rb +32 -5
- data/lib/parse/model/acl.rb +4 -2
- data/lib/parse/model/classes/audience.rb +52 -4
- data/lib/parse/model/classes/user.rb +180 -3
- data/lib/parse/model/core/embed_managed.rb +113 -0
- data/lib/parse/model/core/querying.rb +3 -1
- data/lib/parse/model/core/vector_searchable.rb +161 -0
- data/lib/parse/model/object.rb +28 -5
- data/lib/parse/mongodb.rb +7 -1
- data/lib/parse/pipeline_security.rb +5 -3
- data/lib/parse/query/constraints.rb +29 -0
- data/lib/parse/query.rb +265 -27
- data/lib/parse/retrieval/agent_tool.rb +49 -0
- data/lib/parse/retrieval/reranker/cohere.rb +218 -0
- data/lib/parse/retrieval/reranker.rb +157 -0
- data/lib/parse/retrieval/retriever.rb +110 -23
- data/lib/parse/stack/version.rb +1 -1
- data/lib/parse/stack.rb +17 -0
- data/lib/parse/two_factor_auth/user_extension.rb +123 -31
- data/lib/parse/vector_search/hybrid.rb +578 -0
- data/lib/parse/webhooks/payload.rb +252 -7
- data/lib/parse/webhooks/trigger_audit.rb +502 -0
- data/lib/parse/webhooks.rb +215 -3
- data/scripts/docker/Dockerfile.parse +5 -1
- data/scripts/docker/docker-compose.test.yml +31 -0
- data/scripts/docker/docker-compose.verifyemail.yml +4 -0
- data/scripts/docker/preflight.sh +76 -0
- data/scripts/start-parse.sh +52 -4
- metadata +15 -1
data/lib/parse/query.rb
CHANGED
|
@@ -370,6 +370,19 @@ module Parse
|
|
|
370
370
|
# @param where [Array] an array of {Parse::Constraint} objects.
|
|
371
371
|
# @return [Hash] a hash representing the compiled query, with
|
|
372
372
|
# internal routing markers stripped.
|
|
373
|
+
# One-shot process latch so {#warn_if_public_explain_restricted!} emits
|
|
374
|
+
# the allowPublicExplain guidance at most once per process rather than on
|
|
375
|
+
# every explain call.
|
|
376
|
+
# @!visibility private
|
|
377
|
+
def public_explain_warned?
|
|
378
|
+
@public_explain_warned == true
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
# @!visibility private
|
|
382
|
+
def public_explain_warned!
|
|
383
|
+
@public_explain_warned = true
|
|
384
|
+
end
|
|
385
|
+
|
|
373
386
|
def compile_where(where)
|
|
374
387
|
constraint_reduce(where).reject { |k, _| k.is_a?(String) && k.start_with?("__") }
|
|
375
388
|
end
|
|
@@ -480,6 +493,7 @@ module Parse
|
|
|
480
493
|
@where = []
|
|
481
494
|
@order = []
|
|
482
495
|
@keys = []
|
|
496
|
+
@exclude_keys = []
|
|
483
497
|
@includes = []
|
|
484
498
|
@limit = nil
|
|
485
499
|
@skip = 0
|
|
@@ -495,6 +509,7 @@ module Parse
|
|
|
495
509
|
# unless the caller said otherwise.
|
|
496
510
|
@use_master_key = nil
|
|
497
511
|
@verbose_aggregate = false
|
|
512
|
+
@hint = nil
|
|
498
513
|
conditions constraints
|
|
499
514
|
end # initialize
|
|
500
515
|
|
|
@@ -616,6 +631,50 @@ module Parse
|
|
|
616
631
|
|
|
617
632
|
alias_method :select_fields, :keys
|
|
618
633
|
|
|
634
|
+
# Set a server-side field denylist for this query.
|
|
635
|
+
# When set, Parse Server excludes the named fields from each returned
|
|
636
|
+
# object, complementing the {#keys} allowlist. The two options can be
|
|
637
|
+
# combined: Parse Server first applies the {#keys} allowlist, then
|
|
638
|
+
# strips any field names listed in +excludeKeys+.
|
|
639
|
+
#
|
|
640
|
+
# @note On the REST query path (+encode: true+ in {#compile}) this maps to
|
|
641
|
+
# Parse Server's path-scoped +excludeKeys+. On the mongo-direct path
|
|
642
|
+
# (explicit +.results_direct+, an auto-route, or an aggregation that
|
|
643
|
+
# auto-promotes — e.g. an +$inQuery+ pointer constraint that rewrites to
|
|
644
|
+
# a +$lookup+) the pipeline can only project the {#keys} allowlist, so
|
|
645
|
+
# the SDK honors the denylist as a post-fetch sanitize over the returned
|
|
646
|
+
# results instead. That mongo-direct sanitize is recursive by name: it
|
|
647
|
+
# strips EVERY key with a matching name at any depth, so excluding a
|
|
648
|
+
# field also removes a same-named field inside included/nested objects —
|
|
649
|
+
# broader than the REST path's top-level/dotted scoping. Reserved
|
|
650
|
+
# envelope fields (+objectId+, +className+, +__type+, +createdAt+,
|
|
651
|
+
# +updatedAt+, +ACL+ and their Mongo storage-form names) are never
|
|
652
|
+
# stripped, so object reconstruction is unaffected. The raw aggregation
|
|
653
|
+
# accessor (`aggregate(...).raw`) returns unredacted documents — the
|
|
654
|
+
# sanitize applies to the object/decoded result paths. +excludeKeys+ is a
|
|
655
|
+
# projection convenience, not an ACL/CLP boundary, so it does not affect
|
|
656
|
+
# access control.
|
|
657
|
+
#
|
|
658
|
+
# @example Omit a single sensitive field
|
|
659
|
+
# Post.query.exclude_keys(:secret_token).results
|
|
660
|
+
#
|
|
661
|
+
# @example Omit multiple fields
|
|
662
|
+
# Post.query.exclude_keys(:secret_token, :internal_notes).results
|
|
663
|
+
#
|
|
664
|
+
# @param fields [Array<Symbol, String>] the field names to exclude.
|
|
665
|
+
# @return [self]
|
|
666
|
+
def exclude_keys(*fields)
|
|
667
|
+
@exclude_keys ||= []
|
|
668
|
+
fields.flatten.each do |field|
|
|
669
|
+
if field.nil? == false && field.respond_to?(:to_s)
|
|
670
|
+
@exclude_keys.push Query.format_field(field).to_sym
|
|
671
|
+
end
|
|
672
|
+
end
|
|
673
|
+
@exclude_keys.uniq!
|
|
674
|
+
@results = nil if fields.count > 0
|
|
675
|
+
self # chaining
|
|
676
|
+
end
|
|
677
|
+
|
|
619
678
|
# Extract values for a specific field from all matching objects.
|
|
620
679
|
# This is similar to keys() but returns an array of the actual field values
|
|
621
680
|
# instead of objects with only those fields selected.
|
|
@@ -792,6 +851,31 @@ module Parse
|
|
|
792
851
|
self
|
|
793
852
|
end
|
|
794
853
|
|
|
854
|
+
# Set a MongoDB index hint for this query.
|
|
855
|
+
# Forces Parse Server (and the underlying MongoDB driver) to use the
|
|
856
|
+
# named index instead of the query planner's choice. Useful for
|
|
857
|
+
# benchmarking or for working around sub-optimal plan selection.
|
|
858
|
+
# The hint is emitted in the compiled REST query body as the +hint+
|
|
859
|
+
# parameter (supported by Parse Server 7.4.0+) AND forwarded to the
|
|
860
|
+
# mongo-direct path — +results_direct+ / +count_direct+ / +distinct_direct+
|
|
861
|
+
# pass it to {Parse::MongoDB.aggregate}/{Parse::MongoDB.find} as the Mongo
|
|
862
|
+
# +hint+ option, so a plan diagnosed with {#explain} can be corrected on
|
|
863
|
+
# either path.
|
|
864
|
+
#
|
|
865
|
+
# @example Force a specific index
|
|
866
|
+
# Post.query(:status => "published").hint("status_1_created_at_-1").results
|
|
867
|
+
#
|
|
868
|
+
# @param index_name [String, nil, :_read_] the index name or key pattern to use,
|
|
869
|
+
# or +nil+ to clear a previously set hint. Called with no arguments acts as a
|
|
870
|
+
# reader and returns the current hint value.
|
|
871
|
+
# @return [String, nil, self]
|
|
872
|
+
HINT_UNSET = :_hint_unset_ # @!visibility private
|
|
873
|
+
def hint(index_name = HINT_UNSET)
|
|
874
|
+
return @hint if index_name.equal?(HINT_UNSET)
|
|
875
|
+
@hint = index_name
|
|
876
|
+
self
|
|
877
|
+
end
|
|
878
|
+
|
|
795
879
|
def related_to(field, pointer)
|
|
796
880
|
raise ArgumentError, "Object value must be a Parse::Pointer type" unless pointer.is_a?(Parse::Pointer)
|
|
797
881
|
add_constraint field.to_sym.related_to, pointer
|
|
@@ -1462,24 +1546,125 @@ module Parse
|
|
|
1462
1546
|
# Build headers for the query request
|
|
1463
1547
|
def _headers
|
|
1464
1548
|
headers = {}
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1549
|
+
pref = normalized_read_preference
|
|
1550
|
+
headers[Parse::Protocol::READ_PREFERENCE] = pref if pref
|
|
1551
|
+
headers
|
|
1552
|
+
end
|
|
1553
|
+
|
|
1554
|
+
# Normalize the query's `read_pref` value to the canonical Parse Server
|
|
1555
|
+
# token (`PRIMARY`, `PRIMARY_PREFERRED`, `SECONDARY`, `SECONDARY_PREFERRED`,
|
|
1556
|
+
# `NEAREST`). Parse Server's `_parseReadPreference` upcases the incoming
|
|
1557
|
+
# string and matches exactly these forms, so the SDK emits them verbatim.
|
|
1558
|
+
# @return [String, nil] the canonical token, or nil when no preference is
|
|
1559
|
+
# set. Warns and returns nil on an unrecognized value.
|
|
1560
|
+
# @!visibility private
|
|
1561
|
+
def normalized_read_preference
|
|
1562
|
+
return nil unless read_preference.present?
|
|
1563
|
+
pref = read_preference.to_s.upcase.gsub("_", " ").split.join("_")
|
|
1564
|
+
pref = case pref
|
|
1565
|
+
when "PRIMARY" then "PRIMARY"
|
|
1566
|
+
when "PRIMARY_PREFERRED", "PRIMARYPREFERRED" then "PRIMARY_PREFERRED"
|
|
1567
|
+
when "SECONDARY" then "SECONDARY"
|
|
1568
|
+
when "SECONDARY_PREFERRED", "SECONDARYPREFERRED" then "SECONDARY_PREFERRED"
|
|
1569
|
+
when "NEAREST" then "NEAREST"
|
|
1570
|
+
else pref
|
|
1480
1571
|
end
|
|
1572
|
+
return pref if Parse::Protocol::READ_PREFERENCES.include?(pref)
|
|
1573
|
+
warn "[ParseQuery] Invalid read preference: #{read_preference}. Valid values: #{Parse::Protocol::READ_PREFERENCES.join(", ")}"
|
|
1574
|
+
nil
|
|
1575
|
+
end
|
|
1576
|
+
|
|
1577
|
+
# Proactive guidance for {#explain} on Parse Server 9.0+. PS 9.0 defaults
|
|
1578
|
+
# `allowPublicExplain` to false, so a NON-master explain is rejected unless
|
|
1579
|
+
# the operator re-enabled it server-side. That flag is not surfaced in
|
|
1580
|
+
# `/serverInfo`, so we cannot know for certain whether the call will be
|
|
1581
|
+
# allowed — we therefore WARN (one-shot) and still run the call:
|
|
1582
|
+
# `allowPublicExplain: true` servers return the plan; restricted servers
|
|
1583
|
+
# fail and {#explain}'s reactive enrichment explains why.
|
|
1584
|
+
#
|
|
1585
|
+
# We warn only when the query is clearly non-master (explicit
|
|
1586
|
+
# `use_master_key: false`, or a session-token scope) AND the server version
|
|
1587
|
+
# is known to restrict it — so a master-default explain (the common case)
|
|
1588
|
+
# and unknown-version servers don't get spurious noise.
|
|
1589
|
+
# @!visibility private
|
|
1590
|
+
def warn_if_public_explain_restricted!
|
|
1591
|
+
non_master = use_master_key == false ||
|
|
1592
|
+
(session_token.present? && use_master_key != true)
|
|
1593
|
+
return unless non_master
|
|
1594
|
+
return unless client.respond_to?(:server_supports?) && client.respond_to?(:server_version)
|
|
1595
|
+
return if client.server_version.to_s.empty? # known version only
|
|
1596
|
+
return if client.server_supports?(:public_explain)
|
|
1597
|
+
return if Parse::Query.public_explain_warned?
|
|
1598
|
+
Parse::Query.public_explain_warned!
|
|
1599
|
+
message = "[ParseQuery:Explain] Parse Server #{client.server_version} defaults " \
|
|
1600
|
+
"`allowPublicExplain` to false; a non-master explain will be rejected " \
|
|
1601
|
+
"unless the server enables it. Run explain with use_master_key: true, or " \
|
|
1602
|
+
"set `allowPublicExplain: true` in the server's databaseOptions."
|
|
1603
|
+
if defined?(Parse) && Parse.respond_to?(:logger) && Parse.logger
|
|
1604
|
+
Parse.logger.warn(message)
|
|
1605
|
+
else
|
|
1606
|
+
warn message
|
|
1481
1607
|
end
|
|
1482
|
-
|
|
1608
|
+
end
|
|
1609
|
+
|
|
1610
|
+
# Honor the `exclude_keys` denylist on the mongo-direct path by redacting
|
|
1611
|
+
# the matching fields from the fetched results in Ruby — the mongo-direct
|
|
1612
|
+
# pipeline projects only the `keys` allowlist (Parse Server's REST
|
|
1613
|
+
# `excludeKeys` has no mongo-direct equivalent), so without this the
|
|
1614
|
+
# denylist would silently have no effect. This is a pure post-fetch
|
|
1615
|
+
# sanitize over the Parse-format result hashes; it does NOT change the
|
|
1616
|
+
# MongoDB query or pipeline.
|
|
1617
|
+
#
|
|
1618
|
+
# Semantics differ from the REST path: `excludeKeys` on REST is
|
|
1619
|
+
# path-scoped (top-level / dotted), whereas this drops EVERY key with a
|
|
1620
|
+
# matching name at ANY depth — so excluding `:name` also strips `name`
|
|
1621
|
+
# from included/nested objects. This matches the "recursively drop all
|
|
1622
|
+
# keys with that name" contract for the mongo-direct path.
|
|
1623
|
+
#
|
|
1624
|
+
# `exclude_keys` is a projection convenience, NOT an ACL/CLP boundary, so
|
|
1625
|
+
# this redaction is about returned-object shape, not access control.
|
|
1626
|
+
#
|
|
1627
|
+
# Decode-critical structural keys are never stripped, so a query can ask
|
|
1628
|
+
# to exclude e.g. `:objectId` without breaking object reconstruction.
|
|
1629
|
+
# @param results [Array<Hash>] Parse-format result hashes (mutated in place)
|
|
1630
|
+
# @return [Array<Hash>] the same array, with excluded keys removed
|
|
1631
|
+
# @!visibility private
|
|
1632
|
+
def redact_excluded_keys!(results)
|
|
1633
|
+
return results unless @exclude_keys&.any?
|
|
1634
|
+
names = @exclude_keys.map(&:to_s) - RESERVED_EXCLUDE_KEYS
|
|
1635
|
+
return results if names.empty?
|
|
1636
|
+
drop = names.to_set
|
|
1637
|
+
results.each { |row| recursively_drop_keys!(row, drop) }
|
|
1638
|
+
results
|
|
1639
|
+
end
|
|
1640
|
+
|
|
1641
|
+
# Reserved fields that {#redact_excluded_keys!} never strips: dropping these
|
|
1642
|
+
# would break {#decode} (objectId / className / __type) or remove the
|
|
1643
|
+
# required Parse envelope. Both the Parse-format names (objectId, createdAt,
|
|
1644
|
+
# updatedAt, ACL) and their Mongo storage-form counterparts (_id,
|
|
1645
|
+
# _created_at, _updated_at, _acl) are guarded, so the redaction is safe even
|
|
1646
|
+
# if it is ever pointed at a raw Mongo document, and a caller can't break
|
|
1647
|
+
# reconstruction by excluding e.g. `:_id`. This is an SDK safety choice, not
|
|
1648
|
+
# an assertion about which fields Parse Server's REST `excludeKeys` strips.
|
|
1649
|
+
RESERVED_EXCLUDE_KEYS = %w[
|
|
1650
|
+
objectId className __type createdAt updatedAt ACL
|
|
1651
|
+
_id _created_at _updated_at _acl
|
|
1652
|
+
].freeze
|
|
1653
|
+
|
|
1654
|
+
# Recursively delete every key named in +names+ from a nested
|
|
1655
|
+
# Hash/Array structure, in place. Symbol and string keys both match.
|
|
1656
|
+
# @param value [Object] a Hash, Array, or scalar
|
|
1657
|
+
# @param names [Set<String>] the key names to drop
|
|
1658
|
+
# @!visibility private
|
|
1659
|
+
def recursively_drop_keys!(value, names)
|
|
1660
|
+
case value
|
|
1661
|
+
when Hash
|
|
1662
|
+
value.reject! { |k, _| names.include?(k.to_s) }
|
|
1663
|
+
value.each_value { |v| recursively_drop_keys!(v, names) }
|
|
1664
|
+
when Array
|
|
1665
|
+
value.each { |v| recursively_drop_keys!(v, names) }
|
|
1666
|
+
end
|
|
1667
|
+
value
|
|
1483
1668
|
end
|
|
1484
1669
|
|
|
1485
1670
|
# Performs the fetch request for the query.
|
|
@@ -1921,11 +2106,18 @@ module Parse
|
|
|
1921
2106
|
master: master,
|
|
1922
2107
|
acl_user: acl_user,
|
|
1923
2108
|
acl_role: acl_role,
|
|
1924
|
-
read_preference: @read_preference
|
|
2109
|
+
read_preference: @read_preference,
|
|
2110
|
+
hint: @hint)
|
|
1925
2111
|
|
|
1926
2112
|
# Convert MongoDB documents to Parse format
|
|
1927
2113
|
parse_results = Parse::MongoDB.convert_documents_to_parse(raw_results, @table)
|
|
1928
2114
|
|
|
2115
|
+
# Honor exclude_keys on the mongo-direct path: the pipeline can only
|
|
2116
|
+
# project the keys allowlist, so apply the denylist here as a post-fetch
|
|
2117
|
+
# sanitize over the Parse-format hashes (before the raw/decode fork so
|
|
2118
|
+
# both shapes are redacted). Does not alter the MongoDB query.
|
|
2119
|
+
redact_excluded_keys!(parse_results)
|
|
2120
|
+
|
|
1929
2121
|
if raw
|
|
1930
2122
|
return parse_results.each(&block) if block_given?
|
|
1931
2123
|
return parse_results
|
|
@@ -2042,7 +2234,8 @@ module Parse
|
|
|
2042
2234
|
master: master,
|
|
2043
2235
|
acl_user: acl_user,
|
|
2044
2236
|
acl_role: acl_role,
|
|
2045
|
-
read_preference: @read_preference
|
|
2237
|
+
read_preference: @read_preference,
|
|
2238
|
+
hint: @hint)
|
|
2046
2239
|
|
|
2047
2240
|
# Extract count from result
|
|
2048
2241
|
return 0 if raw_results.empty?
|
|
@@ -2121,6 +2314,7 @@ module Parse
|
|
|
2121
2314
|
raw_results = Parse::MongoDB.aggregate(@table, pipeline,
|
|
2122
2315
|
allow_internal_fields: true,
|
|
2123
2316
|
read_preference: @read_preference,
|
|
2317
|
+
hint: @hint,
|
|
2124
2318
|
session_token: session_token,
|
|
2125
2319
|
master: master,
|
|
2126
2320
|
acl_user: acl_user,
|
|
@@ -2239,7 +2433,8 @@ module Parse
|
|
|
2239
2433
|
# SDK-built pipeline only — see results_direct for rationale.
|
|
2240
2434
|
raw_results = Parse::MongoDB.aggregate(@table, pipeline,
|
|
2241
2435
|
allow_internal_fields: true,
|
|
2242
|
-
read_preference: @read_preference
|
|
2436
|
+
read_preference: @read_preference,
|
|
2437
|
+
hint: @hint)
|
|
2243
2438
|
|
|
2244
2439
|
# Convert results
|
|
2245
2440
|
if options[:raw]
|
|
@@ -3032,7 +3227,7 @@ module Parse
|
|
|
3032
3227
|
# events can arrive. Optional.
|
|
3033
3228
|
# @return [Parse::LiveQuery::Subscription] the subscription object
|
|
3034
3229
|
# @see Parse::LiveQuery::Subscription
|
|
3035
|
-
def subscribe(fields: nil, session_token: nil, client: nil, use_master_key: false, &block)
|
|
3230
|
+
def subscribe(fields: nil, keys: nil, watch: nil, session_token: nil, client: nil, use_master_key: false, &block)
|
|
3036
3231
|
require_relative "live_query"
|
|
3037
3232
|
|
|
3038
3233
|
lq_client = client || Parse::LiveQuery.client
|
|
@@ -3040,6 +3235,8 @@ module Parse
|
|
|
3040
3235
|
@table,
|
|
3041
3236
|
where: compile_where,
|
|
3042
3237
|
fields: fields,
|
|
3238
|
+
keys: keys,
|
|
3239
|
+
watch: watch,
|
|
3043
3240
|
session_token: session_token || @session_token,
|
|
3044
3241
|
use_master_key: use_master_key,
|
|
3045
3242
|
&block
|
|
@@ -3063,11 +3260,22 @@ module Parse
|
|
|
3063
3260
|
# @note This feature requires MongoDB explain support in Parse Server.
|
|
3064
3261
|
# The format of the returned plan depends on the MongoDB version.
|
|
3065
3262
|
def explain
|
|
3263
|
+
warn_if_public_explain_restricted!
|
|
3066
3264
|
compiled_query = compile
|
|
3067
3265
|
compiled_query[:explain] = true
|
|
3068
|
-
response = client.find_objects(@table, compiled_query.as_json, **_opts)
|
|
3266
|
+
response = client.find_objects(@table, compiled_query.as_json, headers: _headers, **_opts)
|
|
3069
3267
|
if response.error?
|
|
3070
|
-
|
|
3268
|
+
# Parse Server 9.0+ defaults `allowPublicExplain` to false, so a
|
|
3269
|
+
# non-master explain that worked on 8.x now returns a permission
|
|
3270
|
+
# error. Surface that as actionable guidance instead of a bare 403.
|
|
3271
|
+
if response.respond_to?(:permission_denied?) && response.permission_denied?
|
|
3272
|
+
puts "[ParseQuery:Explain] #{response.error} — Parse Server 9.0+ defaults " \
|
|
3273
|
+
"`allowPublicExplain` to false; query explain now requires the master key " \
|
|
3274
|
+
"(use_master_key: true) or `allowPublicExplain: true` in the server's " \
|
|
3275
|
+
"databaseOptions."
|
|
3276
|
+
else
|
|
3277
|
+
puts "[ParseQuery:Explain] #{response.error}"
|
|
3278
|
+
end
|
|
3071
3279
|
return {}
|
|
3072
3280
|
end
|
|
3073
3281
|
response.result
|
|
@@ -3131,7 +3339,7 @@ module Parse
|
|
|
3131
3339
|
# at the top-level stage.
|
|
3132
3340
|
BLOCKED_PIPELINE_STAGES = Parse::PipelineSecurity::DENIED_OPERATORS
|
|
3133
3341
|
|
|
3134
|
-
def aggregate(pipeline, verbose: nil, mongo_direct: nil, rewrite_lookups: nil)
|
|
3342
|
+
def aggregate(pipeline, verbose: nil, mongo_direct: nil, rewrite_lookups: nil, raw_values: false, raw_field_names: false)
|
|
3135
3343
|
validate_pipeline!(pipeline)
|
|
3136
3344
|
|
|
3137
3345
|
# Auto-rewrite LLM-style $lookup stages against logical Parse class
|
|
@@ -3275,7 +3483,8 @@ module Parse
|
|
|
3275
3483
|
complete_pipeline = translate_pipeline_for_direct_mongodb(complete_pipeline)
|
|
3276
3484
|
end
|
|
3277
3485
|
|
|
3278
|
-
Aggregation.new(self, complete_pipeline, verbose: verbose, mongo_direct: use_mongo_direct || false
|
|
3486
|
+
Aggregation.new(self, complete_pipeline, verbose: verbose, mongo_direct: use_mongo_direct || false,
|
|
3487
|
+
raw_values: raw_values, raw_field_names: raw_field_names)
|
|
3279
3488
|
end
|
|
3280
3489
|
|
|
3281
3490
|
# Apply the direct-MongoDB stage converter to every stage in a pipeline.
|
|
@@ -3938,6 +4147,7 @@ module Parse
|
|
|
3938
4147
|
|
|
3939
4148
|
q[:include] = @includes.join(",") unless @includes.empty?
|
|
3940
4149
|
q[:keys] = @keys.join(",") unless @keys.empty?
|
|
4150
|
+
q[:excludeKeys] = @exclude_keys.join(",") if encode && @exclude_keys&.any?
|
|
3941
4151
|
q[:order] = @order.join(",") unless @order.empty?
|
|
3942
4152
|
unless @where.empty?
|
|
3943
4153
|
q[:where] = Parse::Query.compile_where(@where)
|
|
@@ -3949,6 +4159,17 @@ module Parse
|
|
|
3949
4159
|
q[:limit] = 0
|
|
3950
4160
|
q[:count] = 1
|
|
3951
4161
|
end
|
|
4162
|
+
# Read preference must ride the REST query body (restOptions), NOT a
|
|
4163
|
+
# header: Parse Server's middleware does not map any
|
|
4164
|
+
# `X-Parse-Read-Preference` header into request options, so the
|
|
4165
|
+
# header alone is silently ignored and the read always hits the
|
|
4166
|
+
# primary. `RestQuery` reads `readPreference` from restOptions, so
|
|
4167
|
+
# emitting it here is what actually routes the read. (The header is
|
|
4168
|
+
# still sent for any intermediary that honors it; it is harmless.)
|
|
4169
|
+
if encode && (pref = normalized_read_preference)
|
|
4170
|
+
q[:readPreference] = pref
|
|
4171
|
+
end
|
|
4172
|
+
q[:hint] = @hint if @hint
|
|
3952
4173
|
if includeClassName
|
|
3953
4174
|
q[:className] = @table
|
|
3954
4175
|
end
|
|
@@ -5207,7 +5428,7 @@ module Parse
|
|
|
5207
5428
|
cloned_query = Parse::Query.new(self.instance_variable_get(:@table))
|
|
5208
5429
|
# Note: :client is intentionally excluded - it contains non-serializable objects
|
|
5209
5430
|
# (Redis connections, Faraday connections) and should be obtained lazily
|
|
5210
|
-
[:count, :where, :order, :keys, :includes, :limit, :skip, :cache, :use_master_key].each do |param|
|
|
5431
|
+
[:count, :where, :order, :keys, :exclude_keys, :includes, :limit, :skip, :cache, :use_master_key, :hint].each do |param|
|
|
5211
5432
|
if instance_variable_defined?(:"@#{param}")
|
|
5212
5433
|
value = instance_variable_get(:"@#{param}")
|
|
5213
5434
|
if value.is_a?(Array) || value.is_a?(Hash)
|
|
@@ -5503,12 +5724,19 @@ module Parse
|
|
|
5503
5724
|
# @param mongo_direct [Boolean] if true, uses MongoDB directly bypassing Parse Server (required for $literal)
|
|
5504
5725
|
# @param max_time_ms [Integer, nil] optional server-side time limit in milliseconds passed to
|
|
5505
5726
|
# {Parse::MongoDB.aggregate} when mongo_direct is true. Pass +nil+ (the default) for no cap.
|
|
5506
|
-
|
|
5727
|
+
# @param raw_values [Boolean] when true, passes +rawValues: true+ to the Parse Server REST
|
|
5728
|
+
# aggregate endpoint (PS 9.9.0+). Has no effect on the mongo-direct path.
|
|
5729
|
+
# @param raw_field_names [Boolean] when true, passes +rawFieldNames: true+ to the Parse Server
|
|
5730
|
+
# REST aggregate endpoint (PS 9.9.0+). Has no effect on the mongo-direct path.
|
|
5731
|
+
def initialize(query, pipeline, verbose: nil, mongo_direct: false, max_time_ms: nil,
|
|
5732
|
+
raw_values: false, raw_field_names: false)
|
|
5507
5733
|
@query = query
|
|
5508
5734
|
@pipeline = pipeline
|
|
5509
5735
|
@cached_response = nil
|
|
5510
5736
|
@mongo_direct = mongo_direct
|
|
5511
5737
|
@max_time_ms = max_time_ms
|
|
5738
|
+
@raw_values = raw_values
|
|
5739
|
+
@raw_field_names = raw_field_names
|
|
5512
5740
|
# Use provided verbose setting, or fall back to query's verbose_aggregate setting
|
|
5513
5741
|
@verbose = verbose.nil? ? @query.instance_variable_get(:@verbose_aggregate) : verbose
|
|
5514
5742
|
end
|
|
@@ -5532,6 +5760,8 @@ module Parse
|
|
|
5532
5760
|
@query.instance_variable_get(:@table),
|
|
5533
5761
|
@pipeline,
|
|
5534
5762
|
headers: {},
|
|
5763
|
+
raw_values: @raw_values,
|
|
5764
|
+
raw_field_names: @raw_field_names,
|
|
5535
5765
|
**@query.send(:_opts),
|
|
5536
5766
|
)
|
|
5537
5767
|
end
|
|
@@ -5555,7 +5785,11 @@ module Parse
|
|
|
5555
5785
|
def execute_direct!(max_time_ms: @max_time_ms)
|
|
5556
5786
|
table = @query.instance_variable_get(:@table)
|
|
5557
5787
|
auth_kwargs = @query.send(:mongo_direct_auth_kwargs)
|
|
5558
|
-
|
|
5788
|
+
# Forward the parent query's index hint so `query.hint(...).aggregate(...)`
|
|
5789
|
+
# honors it on the mongo-direct path too (parity with results_direct /
|
|
5790
|
+
# count_direct / distinct_direct).
|
|
5791
|
+
hint = @query.instance_variable_get(:@hint)
|
|
5792
|
+
Parse::MongoDB.aggregate(table, @pipeline, max_time_ms: max_time_ms, hint: hint, **auth_kwargs)
|
|
5559
5793
|
end
|
|
5560
5794
|
|
|
5561
5795
|
# Returns processed results from the aggregation.
|
|
@@ -5607,6 +5841,10 @@ module Parse
|
|
|
5607
5841
|
def convert_direct_aggregation_item(raw, table)
|
|
5608
5842
|
if raw_is_parse_document?(raw)
|
|
5609
5843
|
parse_doc = Parse::MongoDB.convert_document_to_parse(raw, table)
|
|
5844
|
+
# Honor exclude_keys on this mongo-direct aggregation path (e.g. the
|
|
5845
|
+
# $inQuery -> $lookup rewrite) by redacting the denylisted fields from
|
|
5846
|
+
# the converted document before decode. Mirrors results_direct.
|
|
5847
|
+
@query.send(:redact_excluded_keys!, [parse_doc])
|
|
5610
5848
|
@query.send(:decode, [parse_doc]).first
|
|
5611
5849
|
else
|
|
5612
5850
|
AggregationResult.new(Parse::MongoDB.convert_aggregation_document(raw))
|
|
@@ -94,6 +94,14 @@ module Parse
|
|
|
94
94
|
# AccessDenied for an un-bound agent on a scoped class).
|
|
95
95
|
scope = Parse::Agent::Tools.resolve_tenant_scope!(agent, cname)
|
|
96
96
|
|
|
97
|
+
# Per-tenant embedding spend cap (§16.10 — agent-tool exposure
|
|
98
|
+
# mitigation). semantic_search embeds attacker-controlled query
|
|
99
|
+
# text on every call; charge the estimated query tokens against
|
|
100
|
+
# the tenant's budget BEFORE embedding. HARD-REFUSES once the
|
|
101
|
+
# tenant is over cap. No-op when no limit is configured or for
|
|
102
|
+
# trusted admin agents.
|
|
103
|
+
charge_spend_cap!(agent, scope, query)
|
|
104
|
+
|
|
97
105
|
# Non-admin agents get quantized scores (membership-inference
|
|
98
106
|
# defense); admin agents get full precision. Keyed on the
|
|
99
107
|
# permission tier, not master-key posture.
|
|
@@ -144,6 +152,47 @@ module Parse
|
|
|
144
152
|
envelope
|
|
145
153
|
end
|
|
146
154
|
|
|
155
|
+
# @!visibility private
|
|
156
|
+
# Charge the estimated query-embedding token cost against the
|
|
157
|
+
# tenant's spend cap. The tenant key is the resolved tenant-scope
|
|
158
|
+
# value (so each tenant has its own budget); unscoped non-admin
|
|
159
|
+
# calls charge the shared default bucket. Admin agents are trusted
|
|
160
|
+
# and skip the cap entirely (mirrors the score-quantize tier check).
|
|
161
|
+
#
|
|
162
|
+
# A cap hit is surfaced as a structured error rather than the raw
|
|
163
|
+
# {Parse::Embeddings::SpendCap::Exceeded} — otherwise the agent's
|
|
164
|
+
# generic-error rescue would collapse it to an opaque "internal
|
|
165
|
+
# error" and the model couldn't self-correct. Two distinct cases:
|
|
166
|
+
#
|
|
167
|
+
# * Transient (`retry_after` non-nil): the window will roll off
|
|
168
|
+
# enough tokens to admit this charge. Surface as
|
|
169
|
+
# {Parse::Agent::RateLimitExceeded} (wire `error_code:
|
|
170
|
+
# :rate_limited`) carrying the real backoff hint so the model
|
|
171
|
+
# waits and retries.
|
|
172
|
+
# * Permanent (`retry_after` nil): the request alone exceeds the cap
|
|
173
|
+
# (`requested > limit`) and can NEVER fit, no matter how long the
|
|
174
|
+
# caller waits. Mapping that to a RateLimitExceeded would tell the
|
|
175
|
+
# model to back off and retry an unsatisfiable request — and it
|
|
176
|
+
# would also crash, since RateLimitExceeded#initialize calls
|
|
177
|
+
# `retry_after.round`. Surface as {Parse::Agent::ValidationError}
|
|
178
|
+
# so the model shrinks the query (or the operator raises the cap).
|
|
179
|
+
def charge_spend_cap!(agent, scope, query)
|
|
180
|
+
return if agent.permissions == :admin
|
|
181
|
+
tenant_id = scope && (scope[:value] || scope["value"])
|
|
182
|
+
tokens = Parse::Embeddings::SpendCap.estimate_tokens(query)
|
|
183
|
+
Parse::Embeddings::SpendCap.charge!(tenant_id: tenant_id, tokens: tokens)
|
|
184
|
+
rescue Parse::Embeddings::SpendCap::Exceeded => e
|
|
185
|
+
if e.retry_after.nil?
|
|
186
|
+
raise Parse::Agent::ValidationError,
|
|
187
|
+
"semantic_search: query too large for the embedding spend cap " \
|
|
188
|
+
"(#{e.requested} tokens requested, limit #{e.limit}/#{e.window}s). " \
|
|
189
|
+
"Shorten the query or raise the cap."
|
|
190
|
+
end
|
|
191
|
+
raise Parse::Agent::RateLimitExceeded.new(
|
|
192
|
+
retry_after: e.retry_after, limit: e.limit, window: e.window,
|
|
193
|
+
)
|
|
194
|
+
end
|
|
195
|
+
|
|
147
196
|
# @!visibility private
|
|
148
197
|
# nil -> DEFAULT_MAX_TOTAL_TOKENS; <=0 -> nil (unlimited); else the int.
|
|
149
198
|
def resolve_token_budget(max_total_tokens)
|