parse-stack-next 5.6.0 → 5.7.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.
Files changed (118) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +409 -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 +167 -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/classes/role.rb +227 -28
  75. data/lib/parse/model/classes/user.rb +60 -1
  76. data/lib/parse/model/clp.rb +4 -4
  77. data/lib/parse/model/core/actions.rb +97 -15
  78. data/lib/parse/model/core/create_lock.rb +0 -2
  79. data/lib/parse/model/core/describe.rb +57 -57
  80. data/lib/parse/model/core/embed_managed.rb +21 -23
  81. data/lib/parse/model/core/fetching.rb +2 -0
  82. data/lib/parse/model/core/indexing.rb +14 -14
  83. data/lib/parse/model/core/parse_reference.rb +1 -1
  84. data/lib/parse/model/core/properties.rb +2 -2
  85. data/lib/parse/model/core/querying.rb +1 -1
  86. data/lib/parse/model/core/schema.rb +2 -2
  87. data/lib/parse/model/core/search_indexing.rb +2 -2
  88. data/lib/parse/model/core/vector_searchable.rb +4 -5
  89. data/lib/parse/model/file.rb +21 -15
  90. data/lib/parse/model/geojson.rb +2 -2
  91. data/lib/parse/model/geopoint.rb +1 -0
  92. data/lib/parse/model/object.rb +132 -66
  93. data/lib/parse/model/pointer.rb +2 -0
  94. data/lib/parse/model/polygon.rb +3 -6
  95. data/lib/parse/model/push.rb +2 -2
  96. data/lib/parse/model/vector.rb +3 -1
  97. data/lib/parse/mongodb.rb +512 -282
  98. data/lib/parse/pipeline_security.rb +3 -0
  99. data/lib/parse/query/constraints.rb +46 -46
  100. data/lib/parse/query.rb +146 -59
  101. data/lib/parse/retrieval/agent_tool.rb +34 -20
  102. data/lib/parse/retrieval/chunk.rb +1 -0
  103. data/lib/parse/retrieval/reranker/cohere.rb +11 -11
  104. data/lib/parse/retrieval/reranker.rb +3 -4
  105. data/lib/parse/retrieval/retriever.rb +4 -6
  106. data/lib/parse/schema/index_migrator.rb +26 -26
  107. data/lib/parse/schema/search_index_migrator.rb +19 -19
  108. data/lib/parse/stack/tasks.rb +6 -6
  109. data/lib/parse/stack/version.rb +1 -1
  110. data/lib/parse/stack.rb +10 -10
  111. data/lib/parse/vector_search/hybrid.rb +66 -41
  112. data/lib/parse/vector_search.rb +14 -11
  113. data/lib/parse/webhooks/payload.rb +1 -0
  114. data/lib/parse/webhooks/registration.rb +4 -4
  115. data/lib/parse/webhooks/trigger_audit.rb +50 -49
  116. data/lib/parse/webhooks.rb +22 -4
  117. data/parse-stack-next.gemspec +6 -6
  118. metadata +10 -1
@@ -51,12 +51,12 @@ module Parse
51
51
  # inside the save handler (Parse Server has no create trigger); the webhook
52
52
  # router runs the destroy chain inside the beforeDelete handler.
53
53
  CALLBACK_TRIGGER_MAP = {
54
- [:save, :before] => :before_save,
55
- [:create, :before] => :before_save,
56
- [:save, :after] => :after_save,
57
- [:create, :after] => :after_save,
54
+ [:save, :before] => :before_save,
55
+ [:create, :before] => :before_save,
56
+ [:save, :after] => :after_save,
57
+ [:create, :after] => :after_save,
58
58
  [:destroy, :before] => :before_delete,
59
- [:destroy, :after] => :after_delete,
59
+ [:destroy, :after] => :after_delete,
60
60
  }.freeze
61
61
 
62
62
  # ActiveModel callback chains + phases with NO server trigger that can run
@@ -67,10 +67,10 @@ module Parse
67
67
  # trigger registration changes that. Surfaced as an informational note, not
68
68
  # a fixable gap.
69
69
  LOCAL_ONLY_MAP = {
70
- [:update, :before] => :before_update,
71
- [:update, :after] => :after_update,
70
+ [:update, :before] => :before_update,
71
+ [:update, :after] => :after_update,
72
72
  [:validation, :before] => :before_validation,
73
- [:validation, :after] => :after_validation,
73
+ [:validation, :after] => :after_validation,
74
74
  }.freeze
75
75
 
76
76
  # The ActiveModel callback chains we introspect.
@@ -104,12 +104,12 @@ module Parse
104
104
 
105
105
  def initialize(parse_class:, callbacks:, local_routes:, server_triggers:,
106
106
  findings:, modeled:)
107
- @parse_class = parse_class
108
- @callbacks = callbacks
109
- @local_routes = local_routes
107
+ @parse_class = parse_class
108
+ @callbacks = callbacks
109
+ @local_routes = local_routes
110
110
  @server_triggers = server_triggers
111
- @findings = findings
112
- @modeled = modeled
111
+ @findings = findings
112
+ @modeled = modeled
113
113
  end
114
114
 
115
115
  # @return [Boolean] true when the class has at least one finding.
@@ -120,12 +120,12 @@ module Parse
120
120
  # @return [Hash] a JSON-safe representation of this row.
121
121
  def to_h
122
122
  {
123
- parse_class: parse_class,
124
- modeled: modeled,
125
- callbacks: callbacks,
126
- local_routes: local_routes,
123
+ parse_class: parse_class,
124
+ modeled: modeled,
125
+ callbacks: callbacks,
126
+ local_routes: local_routes,
127
127
  server_triggers: server_triggers,
128
- findings: findings,
128
+ findings: findings,
129
129
  }
130
130
  end
131
131
  end
@@ -144,11 +144,11 @@ module Parse
144
144
  # callbacks (e.g. the `_User` default-ACL callback). Off by default to keep
145
145
  # the report focused on app-defined logic.
146
146
  def initialize(network: true, client: nil, include_framework: false)
147
- @networked = network
147
+ @networked = network
148
148
  @include_framework = include_framework
149
- @client = client
150
- @server_lookup = network ? fetch_server_triggers : {}
151
- @classes = build_classes
149
+ @client = client
150
+ @server_lookup = network ? fetch_server_triggers : {}
151
+ @classes = build_classes
152
152
  end
153
153
 
154
154
  # @return [Array<Hash>] every finding across all classes, flattened, with the
@@ -164,10 +164,11 @@ module Parse
164
164
  def to_h
165
165
  {
166
166
  networked: networked,
167
- classes: @classes.map(&:to_h),
168
- summary: summary,
167
+ classes: @classes.map(&:to_h),
168
+ summary: summary,
169
169
  }
170
170
  end
171
+
171
172
  alias as_json to_h
172
173
 
173
174
  # @return [Hash] finding counts keyed by kind, plus class totals.
@@ -175,9 +176,9 @@ module Parse
175
176
  counts = Hash.new(0)
176
177
  gaps.each { |g| counts[g[:kind]] += 1 }
177
178
  {
178
- classes_audited: @classes.size,
179
+ classes_audited: @classes.size,
179
180
  classes_with_issues: @classes.count(&:issues?),
180
- findings: counts,
181
+ findings: counts,
181
182
  }
182
183
  end
183
184
 
@@ -212,6 +213,7 @@ module Parse
212
213
  s[:findings].sort.each { |kind, n| lines << " #{kind}: #{n}" }
213
214
  lines.join("\n")
214
215
  end
216
+
215
217
  alias to_s pretty
216
218
 
217
219
  private
@@ -231,7 +233,7 @@ module Parse
231
233
  lookup = Hash.new { |h, k| h[k] = {} }
232
234
  client.triggers.results.each do |t|
233
235
  next unless t["url"].present?
234
- name = t["triggerName"]
236
+ name = t["triggerName"]
235
237
  klass = t[Parse::Model::KEY_CLASS_NAME] || t["className"]
236
238
  next if name.blank? || klass.blank?
237
239
  lookup[klass.to_s][name.to_s.underscore.to_sym] = t["url"]
@@ -262,18 +264,18 @@ module Parse
262
264
  end
263
265
 
264
266
  def audit_class(name)
265
- klass = name == "*" ? nil : (Parse::Model.find_class(name) rescue nil)
267
+ klass = name == "*" ? nil : (Parse::Model.find_class(name) rescue nil)
266
268
  callbacks = klass ? collect_callbacks(klass) : {}
267
- routes = collect_local_routes(name)
268
- server = @server_lookup[name] || {}
269
- findings = analyze(name, callbacks, routes, server)
269
+ routes = collect_local_routes(name)
270
+ server = @server_lookup[name] || {}
271
+ findings = analyze(name, callbacks, routes, server)
270
272
  ClassAudit.new(
271
- parse_class: name,
272
- callbacks: callbacks,
273
- local_routes: routes,
273
+ parse_class: name,
274
+ callbacks: callbacks,
275
+ local_routes: routes,
274
276
  server_triggers: server,
275
- findings: findings,
276
- modeled: !klass.nil?,
277
+ findings: findings,
278
+ modeled: !klass.nil?,
277
279
  )
278
280
  end
279
281
 
@@ -373,7 +375,7 @@ module Parse
373
375
  end
374
376
 
375
377
  required.each do |trigger, cb_keys|
376
- has_route = routes.include?(trigger)
378
+ has_route = routes.include?(trigger)
377
379
  has_server = server.key?(trigger)
378
380
  missing = []
379
381
  missing << :route unless has_route
@@ -381,7 +383,7 @@ module Parse
381
383
  next if missing.empty?
382
384
 
383
385
  findings << {
384
- kind: :callbacks_inert,
386
+ kind: :callbacks_inert,
385
387
  trigger: trigger,
386
388
  missing: missing,
387
389
  callbacks: cb_keys.sort,
@@ -398,7 +400,7 @@ module Parse
398
400
  # Wildcard-only coverage is reported on the "*" row, not here.
399
401
  next unless Parse::Webhooks.routes[trigger]&.key?(name)
400
402
  findings << {
401
- kind: :route_not_registered,
403
+ kind: :route_not_registered,
402
404
  trigger: trigger,
403
405
  message: "Local `webhook :#{trigger}` block for #{name} is not " \
404
406
  "registered as a server trigger — run register_triggers! " \
@@ -410,7 +412,7 @@ module Parse
410
412
  server.each_key do |trigger|
411
413
  next if routes.include?(trigger)
412
414
  findings << {
413
- kind: :orphan_server_trigger,
415
+ kind: :orphan_server_trigger,
414
416
  trigger: trigger,
415
417
  message: "Server trigger #{trigger} is registered for #{name} but no " \
416
418
  "local webhook block handles it — every matching operation " \
@@ -426,7 +428,7 @@ module Parse
426
428
  end
427
429
  if local_only.any?
428
430
  findings << {
429
- kind: :local_only_callbacks,
431
+ kind: :local_only_callbacks,
430
432
  callbacks: local_only.sort,
431
433
  message: "#{name} has local-only callbacks (#{local_only.sort.join(", ")}) " \
432
434
  "that no server trigger can run — they fire for Ruby-initiated " \
@@ -451,25 +453,24 @@ module Parse
451
453
 
452
454
  def inert_message(name, trigger, missing, cb_keys)
453
455
  callbacks = cb_keys.sort.join(", ")
454
- reason =
455
- if missing == [:route, :server]
456
+ reason = if missing == [:route, :server]
456
457
  "neither a local `webhook :#{trigger}` block nor a server trigger is " \
457
- "registered"
458
+ "registered"
458
459
  elsif missing == [:route]
459
460
  "no local `webhook :#{trigger}` block is registered to handle it"
460
461
  else # [:server]
461
462
  "a local block exists but the #{trigger} server trigger is not registered"
462
463
  end
463
464
  "#{name} callbacks (#{callbacks}) will NOT run for non-Ruby clients: " \
464
- "#{reason}. Register `webhook :#{trigger}` and run register_triggers!."
465
+ "#{reason}. Register `webhook :#{trigger}` and run register_triggers!."
465
466
  end
466
467
 
467
468
  def finding_glyph(kind)
468
469
  case kind
469
- when :callbacks_inert then "GAP "
470
- when :route_not_registered then "GAP "
470
+ when :callbacks_inert then "GAP "
471
+ when :route_not_registered then "GAP "
471
472
  when :orphan_server_trigger then "WARN"
472
- when :local_only_callbacks then "note"
473
+ when :local_only_callbacks then "note"
473
474
  else " "
474
475
  end
475
476
  end
@@ -493,7 +494,7 @@ module Parse
493
494
  # `pretty: true`.
494
495
  def trigger_audit(pretty: false, network: true, client: nil, include_framework: false)
495
496
  audit = TriggerAudit.new(
496
- network: network, client: client, include_framework: include_framework
497
+ network: network, client: client, include_framework: include_framework,
497
498
  )
498
499
  pretty ? audit.pretty : audit.to_h
499
500
  end
@@ -182,8 +182,26 @@ module Parse
182
182
  raise ArgumentError, "Invalid Webhook registration trigger #{type} #{className}"
183
183
  end
184
184
 
185
- # AfterSave/AfterDelete hooks support more than one
186
- if type == :after_save || type == :after_delete
185
+ # Triggers whose handlers compose instead of replacing one another.
186
+ #
187
+ # `after_save` / `after_delete` have always accumulated. The remaining
188
+ # `after_*` triggers are added because the SDK itself now registers
189
+ # them for cache invalidation: without this, registering an internal
190
+ # `after_logout` handler would silently replace an application's own,
191
+ # with the winner decided by file load order and no warning.
192
+ #
193
+ # This is safe only for non-rejectable triggers. Parse Server ignores
194
+ # their response body, and {#call_route} normalizes their result to
195
+ # `true` regardless, so `.last` semantics are irrelevant.
196
+ # {REJECTABLE_NON_OBJECT_TRIGGERS} are deliberately excluded: a
197
+ # composite of those must deny if ANY handler denies, and folding with
198
+ # `.last` would discard an earlier rejection.
199
+ composable = type == :after_save || type == :after_delete ||
200
+ (NON_OBJECT_TRIGGERS.include?(type) &&
201
+ !REJECTABLE_NON_OBJECT_TRIGGERS.include?(type) &&
202
+ type.to_s.start_with?("after_"))
203
+
204
+ if composable
187
205
  routes[type][className] ||= []
188
206
  routes[type][className].push block
189
207
  else
@@ -353,7 +371,7 @@ module Parse
353
371
  if pre_obj.respond_to?(:apply_field_guards!)
354
372
  pre_obj.apply_field_guards!(
355
373
  master: payload.master? || false,
356
- is_new: payload.original.blank?
374
+ is_new: payload.original.blank?,
357
375
  )
358
376
  end
359
377
  end
@@ -777,7 +795,7 @@ module Parse
777
795
  else
778
796
  if self.logging.present?
779
797
  puts "[Webhooks] --> Could not find mapping route for " \
780
- "#{Parse::Middleware::BodyBuilder.redact(payload.to_json)}"
798
+ "#{Parse::Middleware::BodyBuilder.redact(payload.to_json)}"
781
799
  end
782
800
  end
783
801
 
@@ -6,7 +6,7 @@ require "parse/stack/version"
6
6
  Gem::Specification.new do |spec|
7
7
  spec.name = "parse-stack-next"
8
8
  spec.version = Parse::Stack::VERSION
9
- spec.authors = ["Adrian Curtin", "Anthony Persaud", "Henry Spindell"]
9
+ spec.authors = ["Adrian Curtin", "Anthony Persaud", "Henry Spindell"]
10
10
  spec.email = ["adrian+parse-stack@neurosynq.net"]
11
11
 
12
12
  spec.summary = %q{Parse Server SDK for Ruby — ORM, queries, auth, and MongoDB-direct access}
@@ -15,11 +15,11 @@ Gem::Specification.new do |spec|
15
15
  spec.license = "MIT"
16
16
 
17
17
  spec.metadata = {
18
- "homepage_uri" => "https://github.com/neurosynq/parse-stack-next",
19
- "source_code_uri" => "https://github.com/neurosynq/parse-stack-next",
20
- "changelog_uri" => "https://github.com/neurosynq/parse-stack-next/blob/main/CHANGELOG.md",
21
- "bug_tracker_uri" => "https://github.com/neurosynq/parse-stack-next/issues",
22
- "documentation_uri" => "https://neurosynq.github.io/parse-stack-next/",
18
+ "homepage_uri" => "https://github.com/neurosynq/parse-stack-next",
19
+ "source_code_uri" => "https://github.com/neurosynq/parse-stack-next",
20
+ "changelog_uri" => "https://github.com/neurosynq/parse-stack-next/blob/main/CHANGELOG.md",
21
+ "bug_tracker_uri" => "https://github.com/neurosynq/parse-stack-next/issues",
22
+ "documentation_uri" => "https://neurosynq.github.io/parse-stack-next/",
23
23
  "rubygems_mfa_required" => "true",
24
24
  }
25
25
 
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.6.0
4
+ version: 5.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Adrian Curtin
@@ -225,6 +225,7 @@ files:
225
225
  - docs/_config.yml
226
226
  - docs/acl_clp_guide.md
227
227
  - docs/atlas_vector_search_guide.md
228
+ - docs/caching.md
228
229
  - docs/client_sdk_guide.md
229
230
  - docs/mcp_guide.md
230
231
  - docs/mongodb_direct_guide.md
@@ -242,6 +243,7 @@ files:
242
243
  - examples/webhook_server.rb
243
244
  - lib/parse-stack-next.rb
244
245
  - lib/parse-stack.rb
246
+ - lib/parse/access.rb
245
247
  - lib/parse/acl_scope.rb
246
248
  - lib/parse/agent.rb
247
249
  - lib/parse/agent/approval_gate.rb
@@ -284,8 +286,15 @@ files:
284
286
  - lib/parse/atlas_search/result.rb
285
287
  - lib/parse/atlas_search/search_builder.rb
286
288
  - lib/parse/atlas_search/session.rb
289
+ - lib/parse/authorization.rb
290
+ - lib/parse/cache/invalidation.rb
291
+ - lib/parse/cache/keyspace.rb
292
+ - lib/parse/cache/moneta_surface.rb
287
293
  - lib/parse/cache/pool.rb
288
294
  - lib/parse/cache/redis.rb
295
+ - lib/parse/cache/scoped_view.rb
296
+ - lib/parse/cache/sub_cache.rb
297
+ - lib/parse/cache/upstream_roles.rb
289
298
  - lib/parse/client.rb
290
299
  - lib/parse/client/authentication.rb
291
300
  - lib/parse/client/batch.rb