parse-stack-next 5.6.0 → 5.7.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 (121) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +489 -0
  3. data/README.md +378 -6
  4. data/docs/caching.md +748 -0
  5. data/examples/basic_client.rb +3 -3
  6. data/examples/basic_server.rb +3 -3
  7. data/examples/live_query_listener.rb +12 -12
  8. data/examples/rag_chatbot.rb +14 -14
  9. data/examples/transaction_example.rb +44 -45
  10. data/examples/webhook_server.rb +3 -3
  11. data/lib/parse/access.rb +493 -0
  12. data/lib/parse/acl_scope.rb +125 -52
  13. data/lib/parse/agent/approval_gate.rb +0 -0
  14. data/lib/parse/agent/cancellation_token.rb +3 -3
  15. data/lib/parse/agent/constraint_translator.rb +4 -4
  16. data/lib/parse/agent/describe.rb +34 -34
  17. data/lib/parse/agent/errors.rb +9 -9
  18. data/lib/parse/agent/mcp_client.rb +61 -59
  19. data/lib/parse/agent/mcp_dispatcher.rb +89 -101
  20. data/lib/parse/agent/mcp_rack_app.rb +92 -93
  21. data/lib/parse/agent/mcp_server.rb +1 -1
  22. data/lib/parse/agent/mcp_subscriptions.rb +22 -22
  23. data/lib/parse/agent/metadata_audit.rb +1 -2
  24. data/lib/parse/agent/metadata_dsl.rb +7 -7
  25. data/lib/parse/agent/metadata_registry.rb +12 -12
  26. data/lib/parse/agent/prompt_hardening.rb +4 -4
  27. data/lib/parse/agent/prompts.rb +10 -10
  28. data/lib/parse/agent/result_formatter.rb +4 -4
  29. data/lib/parse/agent/tools.rb +493 -400
  30. data/lib/parse/agent.rb +157 -116
  31. data/lib/parse/api/hooks.rb +1 -1
  32. data/lib/parse/api/server.rb +2 -2
  33. data/lib/parse/api/users.rb +2 -2
  34. data/lib/parse/atlas_search/index_manager.rb +1 -1
  35. data/lib/parse/atlas_search/session.rb +40 -218
  36. data/lib/parse/atlas_search.rb +181 -60
  37. data/lib/parse/authorization.rb +466 -0
  38. data/lib/parse/cache/invalidation.rb +219 -0
  39. data/lib/parse/cache/keyspace.rb +306 -0
  40. data/lib/parse/cache/moneta_surface.rb +126 -0
  41. data/lib/parse/cache/pool.rb +48 -5
  42. data/lib/parse/cache/redis.rb +425 -10
  43. data/lib/parse/cache/scoped_view.rb +443 -0
  44. data/lib/parse/cache/sub_cache.rb +264 -0
  45. data/lib/parse/cache/upstream_roles.rb +230 -0
  46. data/lib/parse/client/authentication.rb +1 -1
  47. data/lib/parse/client/body_builder.rb +1 -2
  48. data/lib/parse/client/caching.rb +84 -5
  49. data/lib/parse/client.rb +327 -189
  50. data/lib/parse/clp_scope.rb +225 -28
  51. data/lib/parse/console.rb +3 -3
  52. data/lib/parse/embeddings/batch_embedder.rb +3 -3
  53. data/lib/parse/embeddings/cache.rb +17 -17
  54. data/lib/parse/embeddings/cohere.rb +21 -22
  55. data/lib/parse/embeddings/image_fetch.rb +22 -21
  56. data/lib/parse/embeddings/jina.rb +21 -21
  57. data/lib/parse/embeddings/local_http.rb +6 -7
  58. data/lib/parse/embeddings/media_file.rb +5 -4
  59. data/lib/parse/embeddings/openai.rb +5 -5
  60. data/lib/parse/embeddings/provider.rb +1 -1
  61. data/lib/parse/embeddings/qwen.rb +10 -10
  62. data/lib/parse/embeddings/spend_cap.rb +2 -3
  63. data/lib/parse/embeddings/streaming_body.rb +11 -11
  64. data/lib/parse/embeddings/video_source.rb +7 -6
  65. data/lib/parse/embeddings/voyage.rb +59 -61
  66. data/lib/parse/embeddings.rb +28 -27
  67. data/lib/parse/graphql/type_generator.rb +1 -1
  68. data/lib/parse/graphql.rb +5 -5
  69. data/lib/parse/live_query/client.rb +1 -1
  70. data/lib/parse/live_query.rb +1 -1
  71. data/lib/parse/lock.rb +15 -18
  72. data/lib/parse/lock_backend.rb +1 -1
  73. data/lib/parse/lookup_rewriter.rb +6 -6
  74. data/lib/parse/model/associations/belongs_to.rb +4 -0
  75. data/lib/parse/model/associations/collection_proxy.rb +3 -0
  76. data/lib/parse/model/associations/has_many.rb +4 -0
  77. data/lib/parse/model/classes/role.rb +227 -28
  78. data/lib/parse/model/classes/user.rb +60 -1
  79. data/lib/parse/model/clp.rb +4 -4
  80. data/lib/parse/model/core/actions.rb +366 -113
  81. data/lib/parse/model/core/create_lock.rb +0 -2
  82. data/lib/parse/model/core/describe.rb +57 -57
  83. data/lib/parse/model/core/embed_managed.rb +21 -23
  84. data/lib/parse/model/core/fetching.rb +6 -0
  85. data/lib/parse/model/core/indexing.rb +14 -14
  86. data/lib/parse/model/core/parse_reference.rb +1 -1
  87. data/lib/parse/model/core/properties.rb +6 -2
  88. data/lib/parse/model/core/querying.rb +1 -1
  89. data/lib/parse/model/core/schema.rb +2 -2
  90. data/lib/parse/model/core/search_indexing.rb +2 -2
  91. data/lib/parse/model/core/vector_searchable.rb +4 -5
  92. data/lib/parse/model/file.rb +21 -15
  93. data/lib/parse/model/geojson.rb +2 -2
  94. data/lib/parse/model/geopoint.rb +1 -0
  95. data/lib/parse/model/object.rb +136 -66
  96. data/lib/parse/model/pointer.rb +2 -0
  97. data/lib/parse/model/polygon.rb +3 -6
  98. data/lib/parse/model/push.rb +2 -2
  99. data/lib/parse/model/vector.rb +3 -1
  100. data/lib/parse/mongodb.rb +512 -282
  101. data/lib/parse/pipeline_security.rb +3 -0
  102. data/lib/parse/query/constraints.rb +46 -46
  103. data/lib/parse/query.rb +146 -59
  104. data/lib/parse/retrieval/agent_tool.rb +34 -20
  105. data/lib/parse/retrieval/chunk.rb +1 -0
  106. data/lib/parse/retrieval/reranker/cohere.rb +11 -11
  107. data/lib/parse/retrieval/reranker.rb +3 -4
  108. data/lib/parse/retrieval/retriever.rb +4 -6
  109. data/lib/parse/schema/index_migrator.rb +26 -26
  110. data/lib/parse/schema/search_index_migrator.rb +19 -19
  111. data/lib/parse/stack/tasks.rb +6 -6
  112. data/lib/parse/stack/version.rb +1 -1
  113. data/lib/parse/stack.rb +10 -10
  114. data/lib/parse/vector_search/hybrid.rb +66 -41
  115. data/lib/parse/vector_search.rb +14 -11
  116. data/lib/parse/webhooks/payload.rb +1 -0
  117. data/lib/parse/webhooks/registration.rb +4 -4
  118. data/lib/parse/webhooks/trigger_audit.rb +50 -49
  119. data/lib/parse/webhooks.rb +120 -5
  120. data/parse-stack-next.gemspec +6 -6
  121. metadata +10 -1
data/lib/parse/query.rb CHANGED
@@ -795,10 +795,9 @@ module Parse
795
795
  # @param amount [Integer] The number of records to skip.
796
796
  # @return [self]
797
797
  def skip(amount)
798
- coerced =
799
- case amount
800
- when nil then 0
801
- when Numeric then amount.to_i
798
+ coerced = case amount
799
+ when nil then 0
800
+ when Numeric then amount.to_i
802
801
  when String
803
802
  unless amount =~ /\A-?\d+\z/
804
803
  raise ArgumentError,
@@ -889,6 +888,7 @@ module Parse
889
888
  # reader and returns the current hint value.
890
889
  # @return [String, nil, self]
891
890
  HINT_UNSET = :_hint_unset_ # @!visibility private
891
+
892
892
  def hint(index_name = HINT_UNSET)
893
893
  return @hint if index_name.equal?(HINT_UNSET)
894
894
  @hint = index_name
@@ -1004,8 +1004,7 @@ module Parse
1004
1004
  # `:bodyEmbedding`) depending on whether Query.format_field has
1005
1005
  # already run. Resolve both shapes against the local set.
1006
1006
  operand_sym = constraint.operand.to_sym
1007
- local_field =
1008
- if vec_fields.key?(operand_sym)
1007
+ local_field = if vec_fields.key?(operand_sym)
1009
1008
  operand_sym
1010
1009
  elsif klass.respond_to?(:field_map)
1011
1010
  klass.field_map.find { |_local, remote| remote.to_sym == operand_sym }&.first
@@ -1136,7 +1135,7 @@ module Parse
1136
1135
  # Explicit opt-in to direct MongoDB
1137
1136
  if mongo_direct
1138
1137
  return distinct_direct(field, return_pointers: return_pointers, order: order,
1139
- **mongo_direct_auth_kwargs)
1138
+ **mongo_direct_auth_kwargs)
1140
1139
  end
1141
1140
 
1142
1141
  # Auto-route to mongo-direct when the compiled where contains a
@@ -1144,7 +1143,7 @@ module Parse
1144
1143
  if requires_mongo_direct?
1145
1144
  assert_mongo_direct_routable!
1146
1145
  return distinct_direct(field, return_pointers: return_pointers, order: order,
1147
- **mongo_direct_auth_kwargs)
1146
+ **mongo_direct_auth_kwargs)
1148
1147
  end
1149
1148
 
1150
1149
  # Auto-route scoped queries (session_token / acl_user / acl_role) to
@@ -1155,7 +1154,7 @@ module Parse
1155
1154
  # `$group`, so distinct values reflect only ACL-readable rows.
1156
1155
  if distinct_query_is_scoped? && defined?(Parse::MongoDB) && Parse::MongoDB.enabled?
1157
1156
  return distinct_direct(field, return_pointers: return_pointers, order: order,
1158
- **mongo_direct_auth_kwargs)
1157
+ **mongo_direct_auth_kwargs)
1159
1158
  end
1160
1159
 
1161
1160
  if field.nil? || !field.respond_to?(:to_s) || field.is_a?(Hash) || field.is_a?(Array)
@@ -1295,8 +1294,8 @@ module Parse
1295
1294
 
1296
1295
  # Execute aggregation
1297
1296
  aggregation = Aggregation.new(self, pipeline, verbose: @verbose_aggregate,
1298
- mongo_direct: use_mongo_direct,
1299
- allow_internal_fields: uses_internal_fields)
1297
+ mongo_direct: use_mongo_direct,
1298
+ allow_internal_fields: uses_internal_fields)
1300
1299
  response = aggregation.execute!
1301
1300
 
1302
1301
  # Extract count from aggregation result
@@ -1420,8 +1419,7 @@ module Parse
1420
1419
  @results = nil if @limit != fetch_count
1421
1420
  @limit = fetch_count
1422
1421
  else
1423
- fetch_count =
1424
- case limit_or_constraints
1422
+ fetch_count = case limit_or_constraints
1425
1423
  when Numeric then limit_or_constraints.to_i
1426
1424
  when String
1427
1425
  unless limit_or_constraints =~ /\A-?\d+\z/
@@ -1910,8 +1908,7 @@ module Parse
1910
1908
  # @param user [Parse::User, Parse::Pointer] the principal to scope by.
1911
1909
  # @return [self]
1912
1910
  def scope_to_user(user)
1913
- raise ArgumentError, "[Parse::Query] scope_to_user requires a Parse::User or User Pointer." \
1914
- unless user.respond_to?(:id) && user.id.is_a?(String)
1911
+ raise ArgumentError, "[Parse::Query] scope_to_user requires a Parse::User or User Pointer." unless user.respond_to?(:id) && user.id.is_a?(String)
1915
1912
  @acl_user = user
1916
1913
  self
1917
1914
  end
@@ -2021,11 +2018,11 @@ module Parse
2021
2018
  # path would have sent; the SDK should not force callers to repeat
2022
2019
  # `use_master_key: true` on every direct query.
2023
2020
  client_has_master_key = begin
2024
- c = client
2025
- c.respond_to?(:master_key) && !c.master_key.to_s.empty?
2026
- rescue StandardError
2027
- false
2028
- end
2021
+ c = client
2022
+ c.respond_to?(:master_key) && !c.master_key.to_s.empty?
2023
+ rescue StandardError
2024
+ false
2025
+ end
2029
2026
  server_mode_master = (use_master_key != false) && !Parse.client_mode && client_has_master_key
2030
2027
  unless use_master_key || server_mode_master || @acl_user || @acl_role || has_session || has_ambient_session
2031
2028
  raise MongoDirectRequired,
@@ -2070,6 +2067,30 @@ module Parse
2070
2067
  # * Otherwise (master-key path) → forward `master: true`.
2071
2068
  # @!visibility private
2072
2069
  def mongo_direct_auth_kwargs
2070
+ # Every branch below is merged with the query's own client, so a query
2071
+ # built from a non-default client authorizes against THAT client rather
2072
+ # than silently falling back to Parse.client at the ACLScope boundary.
2073
+ # Note that a client is bound with `query.client = other`, NOT by
2074
+ # passing `client:` to `Post.query(...)`, which builds a constraint on
2075
+ # a field literally named `client` and silently matches nothing.
2076
+ mongo_direct_client_kwarg.merge(mongo_direct_scope_kwargs)
2077
+ end
2078
+
2079
+ # @!visibility private
2080
+ # @return [Hash] `{ client: ... }`, or empty when this query uses the
2081
+ # default client. Omitted rather than passed as the default client so
2082
+ # the downstream fallback stays observable in one place.
2083
+ def mongo_direct_client_kwarg
2084
+ resolved = client
2085
+ return {} if resolved.nil?
2086
+ return {} if Parse::Client.client? && resolved.equal?(Parse::Client.client)
2087
+ { client: resolved }
2088
+ rescue StandardError
2089
+ {}
2090
+ end
2091
+
2092
+ # @!visibility private
2093
+ def mongo_direct_scope_kwargs
2073
2094
  if @acl_user
2074
2095
  # Pre-resolved User pointer. Hand it to Parse::ACLScope as
2075
2096
  # acl_user: so the same three-layer simulation runs (top-level
@@ -2113,9 +2134,29 @@ module Parse
2113
2134
  # @return [Hash] zero-or-one-of { session_token:/master:/acl_user:/acl_role: }
2114
2135
  # @!visibility private
2115
2136
  def atlas_search_auth_kwargs(options)
2137
+ # The client travels with the identity on this path too. Atlas Search
2138
+ # runs its own pipelines against the driver rather than going through
2139
+ # Parse::MongoDB.aggregate, so a `q.client = other; q.atlas_search(...)`
2140
+ # would otherwise resolve and read as the default application while the
2141
+ # query it came from was bound to another.
2142
+ #
2143
+ # An explicitly passed client wins, matching how the identity kwargs
2144
+ # below behave, and is dropped when it is already the default so the
2145
+ # common single-application call carries nothing extra.
2146
+ client_kwarg = if options.key?(:client)
2147
+ { client: options[:client] }
2148
+ else
2149
+ mongo_direct_client_kwarg
2150
+ end
2151
+
2116
2152
  explicit = %i[session_token master acl_user acl_role].select { |k| options.key?(k) }
2117
- return explicit.to_h { |k| [k, options[k]] } if explicit.any?
2153
+ return client_kwarg.merge(explicit.to_h { |k| [k, options[k]] }) if explicit.any?
2154
+
2155
+ client_kwarg.merge(atlas_search_scope_kwargs)
2156
+ end
2118
2157
 
2158
+ # @!visibility private
2159
+ def atlas_search_scope_kwargs
2119
2160
  if @acl_user
2120
2161
  { acl_user: @acl_user }
2121
2162
  elsif @acl_role
@@ -2201,7 +2242,7 @@ module Parse
2201
2242
  # @raise [Parse::MongoDB::ExecutionTimeout] if the query exceeds max_time_ms
2202
2243
  # @note This is a read-only operation. Direct MongoDB queries cannot modify data.
2203
2244
  # @see Parse::MongoDB.configure
2204
- def results_direct(raw: false, max_time_ms: nil, session_token: nil, master: nil, acl_user: nil, acl_role: nil, &block)
2245
+ def results_direct(raw: false, max_time_ms: nil, session_token: nil, master: nil, acl_user: nil, acl_role: nil, client: nil, &block)
2205
2246
  require_relative "mongodb"
2206
2247
  Parse::MongoDB.require_gem!
2207
2248
 
@@ -2211,6 +2252,17 @@ module Parse
2211
2252
  "Call Parse::MongoDB.configure(uri: 'mongodb://...', enabled: true) first."
2212
2253
  end
2213
2254
 
2255
+ # A query already owns a client. Defaulting to it means a query built
2256
+ # from a secondary client does not have to repeat itself, and cannot
2257
+ # silently authorize against the default application by omission.
2258
+ #
2259
+ # Resolved defensively: `#client` lazily constructs the DEFAULT client
2260
+ # and raises ConnectionError when none is configured. Argument
2261
+ # validation below must still be able to raise ArgumentError in a
2262
+ # process that never called Parse.setup, so a missing client is left
2263
+ # nil here and resolved at the authorization boundary instead.
2264
+ client ||= (self.client rescue nil)
2265
+
2214
2266
  # Build the aggregation pipeline for direct MongoDB execution
2215
2267
  pipeline = build_direct_mongodb_pipeline
2216
2268
 
@@ -2225,9 +2277,9 @@ module Parse
2225
2277
  if session_token.nil? && master.nil? && acl_user.nil? && acl_role.nil?
2226
2278
  auth = mongo_direct_auth_kwargs
2227
2279
  session_token = auth[:session_token]
2228
- master = auth[:master]
2229
- acl_user = auth[:acl_user]
2230
- acl_role = auth[:acl_role]
2280
+ master = auth[:master]
2281
+ acl_user = auth[:acl_user]
2282
+ acl_role = auth[:acl_role]
2231
2283
  end
2232
2284
 
2233
2285
  # Execute the aggregation directly on MongoDB. The pipeline was built
@@ -2245,6 +2297,7 @@ module Parse
2245
2297
  master: master,
2246
2298
  acl_user: acl_user,
2247
2299
  acl_role: acl_role,
2300
+ client: client,
2248
2301
  read_preference: @read_preference,
2249
2302
  hint: @hint)
2250
2303
 
@@ -2291,8 +2344,7 @@ module Parse
2291
2344
  limit_or_constraints = 1
2292
2345
  end
2293
2346
 
2294
- count =
2295
- case limit_or_constraints
2347
+ count = case limit_or_constraints
2296
2348
  when Numeric then limit_or_constraints.to_i
2297
2349
  when String
2298
2350
  unless limit_or_constraints =~ /\A-?\d+\z/
@@ -2335,7 +2387,7 @@ module Parse
2335
2387
  # @raise [Parse::MongoDB::NotEnabled] if direct MongoDB is not configured
2336
2388
  # @note This is a read-only operation. Direct MongoDB queries cannot modify data.
2337
2389
  # @see Parse::MongoDB.configure
2338
- def count_direct(session_token: nil, master: nil, acl_user: nil, acl_role: nil)
2390
+ def count_direct(session_token: nil, master: nil, acl_user: nil, acl_role: nil, client: nil)
2339
2391
  require_relative "mongodb"
2340
2392
  Parse::MongoDB.require_gem!
2341
2393
 
@@ -2345,6 +2397,17 @@ module Parse
2345
2397
  "Call Parse::MongoDB.configure(uri: 'mongodb://...', enabled: true) first."
2346
2398
  end
2347
2399
 
2400
+ # A query already owns a client. Defaulting to it means a query built
2401
+ # from a secondary client does not have to repeat itself, and cannot
2402
+ # silently authorize against the default application by omission.
2403
+ #
2404
+ # Resolved defensively: `#client` lazily constructs the DEFAULT client
2405
+ # and raises ConnectionError when none is configured. Argument
2406
+ # validation below must still be able to raise ArgumentError in a
2407
+ # process that never called Parse.setup, so a missing client is left
2408
+ # nil here and resolved at the authorization boundary instead.
2409
+ client ||= (self.client rescue nil)
2410
+
2348
2411
  # Build the aggregation pipeline for direct MongoDB execution
2349
2412
  pipeline = build_direct_mongodb_pipeline
2350
2413
 
@@ -2359,9 +2422,9 @@ module Parse
2359
2422
  if session_token.nil? && master.nil? && acl_user.nil? && acl_role.nil?
2360
2423
  auth = mongo_direct_auth_kwargs
2361
2424
  session_token = auth[:session_token]
2362
- master = auth[:master]
2363
- acl_user = auth[:acl_user]
2364
- acl_role = auth[:acl_role]
2425
+ master = auth[:master]
2426
+ acl_user = auth[:acl_user]
2427
+ acl_role = auth[:acl_role]
2365
2428
  end
2366
2429
 
2367
2430
  # SDK-built pipeline only — see results_direct for rationale.
@@ -2373,6 +2436,7 @@ module Parse
2373
2436
  master: master,
2374
2437
  acl_user: acl_user,
2375
2438
  acl_role: acl_role,
2439
+ client: client,
2376
2440
  read_preference: @read_preference,
2377
2441
  hint: @hint)
2378
2442
 
@@ -2400,7 +2464,8 @@ module Parse
2400
2464
  # @note This is a read-only operation. Direct MongoDB queries cannot modify data.
2401
2465
  # @see Parse::MongoDB.configure
2402
2466
  def distinct_direct(field, return_pointers: false, order: nil,
2403
- session_token: nil, master: nil, acl_user: nil, acl_role: nil)
2467
+ session_token: nil, master: nil, acl_user: nil, acl_role: nil,
2468
+ client: nil)
2404
2469
  require_relative "mongodb"
2405
2470
  Parse::MongoDB.require_gem!
2406
2471
 
@@ -2410,6 +2475,17 @@ module Parse
2410
2475
  "Call Parse::MongoDB.configure(uri: 'mongodb://...', enabled: true) first."
2411
2476
  end
2412
2477
 
2478
+ # A query already owns a client. Defaulting to it means a query built
2479
+ # from a secondary client does not have to repeat itself, and cannot
2480
+ # silently authorize against the default application by omission.
2481
+ #
2482
+ # Resolved defensively: `#client` lazily constructs the DEFAULT client
2483
+ # and raises ConnectionError when none is configured. Argument
2484
+ # validation below must still be able to raise ArgumentError in a
2485
+ # process that never called Parse.setup, so a missing client is left
2486
+ # nil here and resolved at the authorization boundary instead.
2487
+ client ||= (self.client rescue nil)
2488
+
2413
2489
  if field.nil? || !field.respond_to?(:to_s) || field.is_a?(Hash) || field.is_a?(Array)
2414
2490
  raise ArgumentError, "Invalid field name passed to `distinct_direct`."
2415
2491
  end
@@ -2446,9 +2522,9 @@ module Parse
2446
2522
  if session_token.nil? && master.nil? && acl_user.nil? && acl_role.nil?
2447
2523
  auth = mongo_direct_auth_kwargs
2448
2524
  session_token = auth[:session_token]
2449
- master = auth[:master]
2450
- acl_user = auth[:acl_user]
2451
- acl_role = auth[:acl_role]
2525
+ master = auth[:master]
2526
+ acl_user = auth[:acl_user]
2527
+ acl_role = auth[:acl_role]
2452
2528
  end
2453
2529
  raw_results = Parse::MongoDB.aggregate(@table, pipeline,
2454
2530
  allow_internal_fields: true,
@@ -2457,7 +2533,8 @@ module Parse
2457
2533
  session_token: session_token,
2458
2534
  master: master,
2459
2535
  acl_user: acl_user,
2460
- acl_role: acl_role)
2536
+ acl_role: acl_role,
2537
+ client: client)
2461
2538
 
2462
2539
  # Extract values from results
2463
2540
  values = raw_results.map { |doc| doc["value"] }.compact
@@ -2484,10 +2561,11 @@ module Parse
2484
2561
  # @return [Array] array of distinct values, with pointer fields as Parse::Pointer objects
2485
2562
  # @see #distinct_direct
2486
2563
  def distinct_direct_pointers(field, order: nil,
2487
- session_token: nil, master: nil, acl_user: nil, acl_role: nil)
2564
+ session_token: nil, master: nil, acl_user: nil, acl_role: nil,
2565
+ client: nil)
2488
2566
  distinct_direct(field, return_pointers: true, order: order,
2489
- session_token: session_token, master: master,
2490
- acl_user: acl_user, acl_role: acl_role)
2567
+ session_token: session_token, master: master,
2568
+ acl_user: acl_user, acl_role: acl_role, client: client)
2491
2569
  end
2492
2570
 
2493
2571
  #----------------------------------------------------------------
@@ -2615,6 +2693,10 @@ module Parse
2615
2693
  options[:class_name] = @table
2616
2694
  options[:limit] = limit
2617
2695
  options[:skip] = skip_val
2696
+ # The query's client travels with its identity here too. Without it a
2697
+ # `q.client = other` query reached Atlas Search as the default
2698
+ # application. See #atlas_search_auth_kwargs.
2699
+ options.merge!(mongo_direct_client_kwarg) unless options.key?(:client)
2618
2700
  # Forward the query's read_preference (set via `#read_pref`).
2619
2701
  # Without this, Atlas Search calls reached through the Query
2620
2702
  # bridge silently fall back to the client default even though
@@ -2682,6 +2764,10 @@ module Parse
2682
2764
  # Use query limit if set and no explicit limit provided
2683
2765
  options[:limit] ||= (@limit.is_a?(Numeric) && @limit > 0 ? @limit : 10)
2684
2766
  options[:class_name] = @table
2767
+ # The query's client travels with its identity here too. Without it a
2768
+ # `q.client = other` query reached Atlas Search as the default
2769
+ # application. See #atlas_search_auth_kwargs.
2770
+ options.merge!(mongo_direct_client_kwarg) unless options.key?(:client)
2685
2771
  # Forward the query's read_preference (set via `#read_pref`).
2686
2772
  # See #atlas_search for the parity rationale.
2687
2773
  if @read_preference && !options.key?(:read_preference)
@@ -2739,6 +2825,10 @@ module Parse
2739
2825
  options[:limit] ||= (@limit.is_a?(Numeric) && @limit > 0 ? @limit : 100)
2740
2826
  options[:skip] ||= (@skip > 0 ? @skip : 0)
2741
2827
  options[:class_name] = @table
2828
+ # The query's client travels with its identity here too. Without it a
2829
+ # `q.client = other` query reached Atlas Search as the default
2830
+ # application. See #atlas_search_auth_kwargs.
2831
+ options.merge!(mongo_direct_client_kwarg) unless options.key?(:client)
2742
2832
  # Forward the query's read_preference (set via `#read_pref`).
2743
2833
  # See #atlas_search for the parity rationale.
2744
2834
  if @read_preference && !options.key?(:read_preference)
@@ -3692,8 +3782,8 @@ module Parse
3692
3782
  end
3693
3783
 
3694
3784
  Aggregation.new(self, complete_pipeline, verbose: verbose, mongo_direct: use_mongo_direct || false,
3695
- allow_internal_fields: uses_internal_fields,
3696
- raw_values: raw_values, raw_field_names: raw_field_names)
3785
+ allow_internal_fields: uses_internal_fields,
3786
+ raw_values: raw_values, raw_field_names: raw_field_names)
3697
3787
  end
3698
3788
 
3699
3789
  # Apply the direct-MongoDB stage converter to every stage in a pipeline.
@@ -3796,7 +3886,7 @@ module Parse
3796
3886
 
3797
3887
  # Create Aggregation directly to avoid double-applying constraints
3798
3888
  Aggregation.new(self, pipeline, verbose: verbose, mongo_direct: use_mongo_direct || false,
3799
- allow_internal_fields: uses_internal_fields)
3889
+ allow_internal_fields: uses_internal_fields)
3800
3890
  end
3801
3891
 
3802
3892
  private
@@ -3976,7 +4066,7 @@ module Parse
3976
4066
  # Create Aggregation directly to avoid double-applying constraints
3977
4067
  # The aggregate() method would redundantly add where constraints again
3978
4068
  Aggregation.new(self, pipeline, verbose: @verbose_aggregate, mongo_direct: use_mongo_direct,
3979
- allow_internal_fields: uses_internal_fields)
4069
+ allow_internal_fields: uses_internal_fields)
3980
4070
  end
3981
4071
 
3982
4072
  # Check if the pipeline references internal Parse fields that require MongoDB direct access
@@ -4330,6 +4420,7 @@ module Parse
4330
4420
  end
4331
4421
  @keys.uniq!
4332
4422
  end
4423
+
4333
4424
  private :merge_includes_into_keys!
4334
4425
 
4335
4426
  # Builds Parse::Pointer objects based on the set of Parse JSON hashes in an array.
@@ -6057,7 +6148,7 @@ module Parse
6057
6148
  # auth data) is what `allow_internal_fields` relaxes, so it must never be
6058
6149
  # set on a pipeline that interpolates user input. Defaults to `false`.
6059
6150
  def initialize(query, pipeline, verbose: nil, mongo_direct: false, max_time_ms: nil,
6060
- raw_values: false, raw_field_names: false, allow_internal_fields: false)
6151
+ raw_values: false, raw_field_names: false, allow_internal_fields: false)
6061
6152
  @query = query
6062
6153
  @pipeline = pipeline
6063
6154
  @cached_response = nil
@@ -6127,7 +6218,7 @@ module Parse
6127
6218
  # count_direct / distinct_direct).
6128
6219
  hint = @query.instance_variable_get(:@hint)
6129
6220
  Parse::MongoDB.aggregate(table, @pipeline, max_time_ms: max_time_ms, hint: hint,
6130
- allow_internal_fields: @allow_internal_fields, **auth_kwargs)
6221
+ allow_internal_fields: @allow_internal_fields, **auth_kwargs)
6131
6222
  end
6132
6223
 
6133
6224
  # Returns processed results from the aggregation.
@@ -6339,8 +6430,7 @@ module Parse
6339
6430
  # @example Groups with the most members first
6340
6431
  # Document.group_by(:category).order(size: :desc).list
6341
6432
  def order(spec)
6342
- target, direction =
6343
- case spec
6433
+ target, direction = case spec
6344
6434
  when Symbol
6345
6435
  [:key, spec]
6346
6436
  when Hash
@@ -6841,8 +6931,7 @@ module Parse
6841
6931
  # `count` field (for :value / :size).
6842
6932
  def sort_stage
6843
6933
  return nil unless @sort_target
6844
- field =
6845
- case @sort_target
6934
+ field = case @sort_target
6846
6935
  when :key then "_id"
6847
6936
  when :value then "count"
6848
6937
  when :size then "__order_size"
@@ -7012,13 +7101,13 @@ module Parse
7012
7101
  # @return [String] the default second-column header
7013
7102
  def default_value_header
7014
7103
  case @operation&.to_s
7015
- when "count" then "Count"
7016
- when "sum" then "Sum"
7104
+ when "count" then "Count"
7105
+ when "sum" then "Sum"
7017
7106
  when "average", "avg" then "Average"
7018
- when "min" then "Min"
7019
- when "max" then "Max"
7020
- when "list" then "Items"
7021
- else "Count"
7107
+ when "min" then "Min"
7108
+ when "max" then "Max"
7109
+ when "list" then "Items"
7110
+ else "Count"
7022
7111
  end
7023
7112
  end
7024
7113
 
@@ -7193,8 +7282,7 @@ module Parse
7193
7282
  # @example Busiest day first
7194
7283
  # Post.group_by_date(:created_at, :day).order(value: :desc).count
7195
7284
  def order(spec)
7196
- target, direction =
7197
- case spec
7285
+ target, direction = case spec
7198
7286
  when Symbol
7199
7287
  [:key, spec]
7200
7288
  when Hash
@@ -7626,8 +7714,7 @@ module Parse
7626
7714
  # `.order(...)` has been called.
7627
7715
  def sort_stage
7628
7716
  field = @sort_target == :value ? "count" : "_id"
7629
- dir =
7630
- if @sort_target.nil?
7717
+ dir = if @sort_target.nil?
7631
7718
  1
7632
7719
  else
7633
7720
  @sort_direction == :desc ? -1 : 1
@@ -27,6 +27,20 @@ module Parse
27
27
  module AgentTool
28
28
  module_function
29
29
 
30
+ # The agent's auth kwargs for the direct path, including its client
31
+ # when the agent can supply one.
32
+ #
33
+ # Agents are duck-typed at this boundary, so an agent-shaped object
34
+ # that predates `direct_auth_kwargs` still works and simply resolves as
35
+ # an unidentified caller, which Parse::MongoDB's binding guard already
36
+ # models. Requiring the new method of every stand-in would break each
37
+ # one that has not been updated, to gain a field that is optional by
38
+ # construction.
39
+ def retrieval_auth_kwargs(agent)
40
+ return agent.direct_auth_kwargs if agent.respond_to?(:direct_auth_kwargs)
41
+ agent.acl_scope_kwargs
42
+ end
43
+
30
44
  # Upper bound on `k` (mirrors the registered parameter schema).
31
45
  MAX_K = 20
32
46
  # Default neighbour count for the agent tool. Intentionally lower than
@@ -60,18 +74,18 @@ module Parse
60
74
  # token budget trims the result, `budget_truncated: true` and
61
75
  # `budget_dropped: <n>` are added.
62
76
  def semantic_search(agent, class_name: nil, query: nil, k: DEFAULT_K,
63
- filter: nil, vector_filter: nil, text_field: nil,
64
- chunk_size: nil, chunk_overlap: nil, chunk_by: nil,
65
- max_chunks_per_document: nil, max_total_tokens: nil,
66
- # Back-compat / ergonomic aliases for direct callers:
67
- # `klass:`/`class:` for class_name, and the chunker's
68
- # own `size:`/`overlap:`/`by:` names.
69
- klass: nil, size: nil, overlap: nil, by: nil,
77
+ filter: nil, vector_filter: nil, text_field: nil,
78
+ chunk_size: nil, chunk_overlap: nil, chunk_by: nil,
79
+ max_chunks_per_document: nil, max_total_tokens: nil,
80
+ # Back-compat / ergonomic aliases for direct callers:
81
+ # `klass:`/`class:` for class_name, and the chunker's
82
+ # own `size:`/`overlap:`/`by:` names.
83
+ klass: nil, size: nil, overlap: nil, by: nil,
70
84
  **rest)
71
- class_name ||= klass || rest.delete(:class)
72
- chunk_size ||= size
85
+ class_name ||= klass || rest.delete(:class)
86
+ chunk_size ||= size
73
87
  chunk_overlap ||= overlap
74
- chunk_by ||= by
88
+ chunk_by ||= by
75
89
 
76
90
  klass = Parse::Agent::MetadataRegistry.resolve_searchable!(class_name)
77
91
  cname = klass.parse_class
@@ -126,7 +140,7 @@ module Parse
126
140
  tenant_scope: scope,
127
141
  score_quantize: score_quantize,
128
142
  source_transform: source_projector(agent, cname, scope),
129
- **agent.acl_scope_kwargs,
143
+ **retrieval_auth_kwargs(agent),
130
144
  )
131
145
  end
132
146
 
@@ -352,15 +366,15 @@ module Parse
352
366
  PARAMETERS = {
353
367
  "type" => "object",
354
368
  "properties" => {
355
- "class_name" => { "type" => "string", "description" => "Parse class name (must be agent_searchable)." },
356
- "query" => { "type" => "string", "description" => "Natural-language query." },
357
- "k" => { "type" => "integer", "default" => DEFAULT_K, "minimum" => 1, "maximum" => MAX_K },
358
- "filter" => { "type" => "object", "description" => "Post-search field filter (allowlisted fields only)." },
369
+ "class_name" => { "type" => "string", "description" => "Parse class name (must be agent_searchable)." },
370
+ "query" => { "type" => "string", "description" => "Natural-language query." },
371
+ "k" => { "type" => "integer", "default" => DEFAULT_K, "minimum" => 1, "maximum" => MAX_K },
372
+ "filter" => { "type" => "object", "description" => "Post-search field filter (allowlisted fields only)." },
359
373
  "vector_filter" => { "type" => "object", "description" => "Atlas pre-search filter (allowlisted fields only)." },
360
- "text_field" => { "type" => "string", "description" => "Which embedded text source to chunk and return as content. Required only when the class embeds more than one text field; must name one of those sources." },
361
- "chunk_size" => { "type" => "integer", "description" => "Override chunk window size." },
374
+ "text_field" => { "type" => "string", "description" => "Which embedded text source to chunk and return as content. Required only when the class embeds more than one text field; must name one of those sources." },
375
+ "chunk_size" => { "type" => "integer", "description" => "Override chunk window size." },
362
376
  "chunk_overlap" => { "type" => "integer", "description" => "Override chunk overlap." },
363
- "chunk_by" => { "type" => "string", "enum" => %w[chars tokens], "description" => "Chunk unit." },
377
+ "chunk_by" => { "type" => "string", "enum" => %w[chars tokens], "description" => "Chunk unit." },
364
378
  "max_chunks_per_document" => { "type" => "integer", "minimum" => 1, "description" => "Cap on chunks emitted per matched document." },
365
379
  "max_total_tokens" => { "type" => "integer", "minimum" => 0, "description" => "Ceiling on total returned chunk-content tokens (approx chars/4). Trims lowest-ranked chunks first and sets budget_truncated. 0 disables." },
366
380
  },
@@ -379,8 +393,8 @@ module Parse
379
393
  "items" => {
380
394
  "type" => "object",
381
395
  "properties" => {
382
- "id" => { "type" => "string" },
383
- "score" => { "type" => %w[number null] },
396
+ "id" => { "type" => "string" },
397
+ "score" => { "type" => %w[number null] },
384
398
  "content" => { "type" => "string" },
385
399
  "metadata" => { "type" => "object" },
386
400
  },
@@ -64,6 +64,7 @@ module Parse
64
64
  other.score == @score &&
65
65
  other.content == @content
66
66
  end
67
+
67
68
  alias eql? ==
68
69
 
69
70
  def hash
@@ -35,10 +35,10 @@ module Parse
35
35
  class BadRequestError < Error; end
36
36
 
37
37
  DEFAULT_BASE_URL = "https://api.cohere.com/v2"
38
- DEFAULT_MODEL = "rerank-v3.5"
39
- DEFAULT_TIMEOUT = 30
38
+ DEFAULT_MODEL = "rerank-v3.5"
39
+ DEFAULT_TIMEOUT = 30
40
40
  DEFAULT_OPEN_TIMEOUT = 5
41
- DEFAULT_MAX_RETRIES = 2
41
+ DEFAULT_MAX_RETRIES = 2
42
42
 
43
43
  # Cohere documents a cap of 1000 documents per rerank call; the
44
44
  # {Base::MAX_DOCUMENTS} cap (1000) already enforces this.
@@ -75,7 +75,7 @@ module Parse
75
75
 
76
76
  def inspect
77
77
  "#<#{self.class} model=#{@model.inspect} base=#{safe_base_host.inspect} " \
78
- "retries=#{@max_retries} api_key=[REDACTED]>"
78
+ "retries=#{@max_retries} api_key=[REDACTED]>"
79
79
  end
80
80
 
81
81
  protected
@@ -83,10 +83,10 @@ module Parse
83
83
  def rerank_scores(query, documents, top_n)
84
84
  require_faraday!
85
85
  body = {
86
- "model" => @model,
87
- "query" => query,
86
+ "model" => @model,
87
+ "query" => query,
88
88
  "documents" => documents,
89
- "top_n" => top_n,
89
+ "top_n" => top_n,
90
90
  }
91
91
  payload = post_rerank(body)
92
92
  extract_results!(payload, documents.length)
@@ -158,9 +158,9 @@ module Parse
158
158
  require_faraday!
159
159
  headers = {
160
160
  "Authorization" => "Bearer #{@api_key}",
161
- "Content-Type" => "application/json",
162
- "Accept" => "application/json",
163
- "User-Agent" => "parse-stack-reranker/#{Parse::Stack::VERSION rescue "0"}",
161
+ "Content-Type" => "application/json",
162
+ "Accept" => "application/json",
163
+ "User-Agent" => "parse-stack-reranker/#{Parse::Stack::VERSION rescue "0"}",
164
164
  }
165
165
  # base_url must end with a trailing slash so Faraday resolves the
166
166
  # relative "rerank" path under /v2/ rather than replacing it.
@@ -177,7 +177,7 @@ module Parse
177
177
  end
178
178
 
179
179
  def backoff_seconds(attempt)
180
- [0.5 * (2**(attempt - 1)), 30.0].min
180
+ [0.5 * (2 ** (attempt - 1)), 30.0].min
181
181
  end
182
182
 
183
183
  def retry_after_seconds(response)
@@ -100,11 +100,10 @@ module Parse
100
100
 
101
101
  def normalize_results(pairs, doc_count, top_n)
102
102
  results = Array(pairs).map do |p|
103
- idx, score =
104
- case p
103
+ idx, score = case p
105
104
  when Result then [p.index, p.relevance_score]
106
- when Array then [p[0], p[1]]
107
- when Hash then [p[:index] || p["index"], p[:relevance_score] || p["relevance_score"]]
105
+ when Array then [p[0], p[1]]
106
+ when Hash then [p[:index] || p["index"], p[:relevance_score] || p["relevance_score"]]
108
107
  else
109
108
  raise InvalidResponseError, "#{self.class}: unexpected rerank result element #{p.inspect}."
110
109
  end