parse-stack-next 5.8.0 → 5.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +318 -0
  3. data/README.md +25 -19
  4. data/docs/caching.md +50 -0
  5. data/docs/mcp_guide.md +15 -3
  6. data/docs/webhooks_guide.md +59 -7
  7. data/lib/parse/acl_scope.rb +71 -9
  8. data/lib/parse/agent/describe.rb +8 -2
  9. data/lib/parse/agent/mcp_dispatcher.rb +8 -1
  10. data/lib/parse/agent.rb +205 -20
  11. data/lib/parse/atlas_search/index_manager.rb +5 -1
  12. data/lib/parse/atlas_search.rb +49 -1
  13. data/lib/parse/authorization.rb +235 -3
  14. data/lib/parse/cache/sub_cache.rb +26 -0
  15. data/lib/parse/client/authentication.rb +19 -1
  16. data/lib/parse/client/batch.rb +35 -2
  17. data/lib/parse/client.rb +26 -2
  18. data/lib/parse/clp_scope.rb +37 -3
  19. data/lib/parse/live_query/client.rb +72 -2
  20. data/lib/parse/lock_backend.rb +4 -3
  21. data/lib/parse/model/associations/collection_proxy.rb +19 -11
  22. data/lib/parse/model/associations/has_many.rb +11 -0
  23. data/lib/parse/model/associations/pointer_collection_proxy.rb +36 -0
  24. data/lib/parse/model/associations/relation_collection_proxy.rb +97 -12
  25. data/lib/parse/model/classes/role.rb +23 -0
  26. data/lib/parse/model/classes/session.rb +279 -19
  27. data/lib/parse/model/classes/user.rb +33 -12
  28. data/lib/parse/model/core/actions.rb +19 -4
  29. data/lib/parse/model/core/fetching.rb +29 -2
  30. data/lib/parse/model/core/field_guards.rb +16 -8
  31. data/lib/parse/model/object.rb +52 -1
  32. data/lib/parse/model/push.rb +11 -1
  33. data/lib/parse/mongodb.rb +19 -1
  34. data/lib/parse/query/constraint.rb +1 -1
  35. data/lib/parse/query/constraints.rb +31 -27
  36. data/lib/parse/query.rb +793 -55
  37. data/lib/parse/stack/version.rb +1 -1
  38. data/lib/parse/stack.rb +19 -0
  39. data/lib/parse/vector_search/hybrid.rb +74 -15
  40. data/lib/parse/vector_search.rb +214 -7
  41. data/lib/parse/webhooks/payload.rb +43 -0
  42. data/lib/parse/webhooks.rb +312 -9
  43. metadata +1 -1
data/lib/parse/mongodb.rb CHANGED
@@ -1481,6 +1481,15 @@ module Parse
1481
1481
  "They are mutually exclusive."
1482
1482
  end
1483
1483
 
1484
+ if master == true && Parse::ACLScope.master_key_suppressed?
1485
+ # The role graph has no public mode to fall back to, so a master
1486
+ # call inside `Parse.without_master_key` is refused outright.
1487
+ raise Parse::ACLScope::ACLRequired,
1488
+ "Parse::MongoDB.#{method_name}: master: true is dropped inside " \
1489
+ "Parse.without_master_key. Pass `as: <Parse::User|Parse::Pointer>`, " \
1490
+ "or run the call inside Parse.with_master_key."
1491
+ end
1492
+
1484
1493
  if master == true
1485
1494
  return Parse::ACLScope::Resolution.new(
1486
1495
  mode: :master, permission_strings: nil, user_id: nil, session: nil,
@@ -1782,6 +1791,10 @@ module Parse
1782
1791
  # database with this application's permission strings.
1783
1792
  verify_client!(resolution.client)
1784
1793
  payload[:scope] = __scope_label(resolution)
1794
+ # `master: true` inside Parse.without_master_key, downgraded to the
1795
+ # public scope. Lets a subscriber explain a master-key caller that
1796
+ # sees only public rows inside the block.
1797
+ payload[:master_dropped] = true if resolution.respond_to?(:master_dropped?) && resolution.master_dropped?
1785
1798
 
1786
1799
  # Validate BEFORE rewrite so the security denylist is applied to the
1787
1800
  # caller's original pipeline (which an attacker controls), not to
@@ -2303,7 +2316,12 @@ module Parse
2303
2316
  "the caller is authorized. Callers without master scope (e.g. agent " \
2304
2317
  "tools, request handlers) must not invoke this method."
2305
2318
  end
2306
- results = aggregate(collection_name, [{ "$indexStats" => {} }], master: true)
2319
+ # Index metadata, not rows: keep master mode inside
2320
+ # `Parse.without_master_key`, which only governs row access. The
2321
+ # sentinel elevates scope resolution only; it leaves the block's
2322
+ # fiber state alone, so notification subscribers still see it.
2323
+ results = aggregate(collection_name, [{ "$indexStats" => {} }],
2324
+ master: Parse::ACLScope::METADATA_MASTER)
2307
2325
  results.each_with_object({}) do |row, h|
2308
2326
  name = row["name"] || row[:name]
2309
2327
  next unless name
@@ -151,7 +151,7 @@ module Parse
151
151
  # d = d.pointer if d.is_a?(Parse::Object) #simplified query object
152
152
  # d = d.compile
153
153
  if d.is_a?(Parse::Query)
154
- compiled = d.compile(encode: false, includeClassName: true)
154
+ compiled = d.compile_subquery
155
155
  # compiled["className"] = d.table
156
156
  d = compiled
157
157
  end
@@ -94,7 +94,7 @@ module Parse
94
94
  # # where this Parse object equals the object in the column `field`.
95
95
  # q.where :field => Parse::Pointer("Field", "someObjectId")
96
96
  # # alias, shorthand when we infer `:field` maps to `Field` parse class.
97
- # q.where :field.id => "someObjectId"
97
+ # q.where :field.pointer_id => "someObjectId"
98
98
  # # "field":{"__type":"Pointer","className":"Field","objectId":"someObjectId"}}
99
99
  #
100
100
  # class Artist < Parse::Object
@@ -114,19 +114,21 @@ module Parse
114
114
  # You can use the objectId in the query as follows:
115
115
  #
116
116
  # # shorthand if you are using convention. Will infer class `Artist`
117
- # Song.all :artist.id => artist_id
117
+ # Song.all :artist.pointer_id => artist_id
118
118
  #
119
119
  # # other approaches, same result
120
- # Song.all :artist.id => artist # safely supported Parse::Pointer
120
+ # Song.all :artist.pointer_id => artist # safely supported Parse::Pointer
121
121
  # Song.all :artist => Artist.pointer(artist_id)
122
122
  # Song.all :artist => Parse::Pointer.new("Artist", artist_id)
123
123
  #
124
124
  class ObjectIdConstraint < Constraint
125
- # @!method id
126
- # A registered method on a symbol to create the constraint.
125
+ # @!method pointer_id
126
+ # A registered method on a symbol to create the constraint. The
127
+ # operator is registered as `:id` and installed on `Symbol` as
128
+ # `pointer_id`.
127
129
  # @example
128
- # q.where :field.id => "someObjectId"
129
- # q.where :field.id => pointer # safely supported
130
+ # q.where :field.pointer_id => "someObjectId"
131
+ # q.where :field.pointer_id => pointer # safely supported
130
132
  # @return [ObjectIdConstraint]
131
133
  register :id
132
134
 
@@ -554,18 +556,18 @@ module Parse
554
556
  # MongoDB aggregation pipeline with $expr and $size to check array length.
555
557
  #
556
558
  # # Exact size match
557
- # q.where :field.size => 2
558
- # q.where :tags.size => 5
559
+ # q.where :field.array_size => 2
560
+ # q.where :tags.array_size => 5
559
561
  #
560
562
  # # Comparison operators via hash
561
- # q.where :tags.size => { gt: 3 } # size > 3
562
- # q.where :tags.size => { gte: 2 } # size >= 2
563
- # q.where :tags.size => { lt: 5 } # size < 5
564
- # q.where :tags.size => { lte: 4 } # size <= 4
565
- # q.where :tags.size => { ne: 0 } # size != 0
563
+ # q.where :tags.array_size => { gt: 3 } # size > 3
564
+ # q.where :tags.array_size => { gte: 2 } # size >= 2
565
+ # q.where :tags.array_size => { lt: 5 } # size < 5
566
+ # q.where :tags.array_size => { lte: 4 } # size <= 4
567
+ # q.where :tags.array_size => { ne: 0 } # size != 0
566
568
  #
567
569
  # # Combine for range
568
- # q.where :tags.size => { gte: 2, lt: 10 } # 2 <= size < 10
570
+ # q.where :tags.array_size => { gte: 2, lt: 10 } # 2 <= size < 10
569
571
  #
570
572
  # @note This constraint uses aggregation pipeline because Parse Server
571
573
  # does not support the $size query operator natively.
@@ -577,11 +579,13 @@ module Parse
577
579
  # @see ContainsAllConstraint
578
580
  # @see ArraySetEqualsConstraint
579
581
  class ArraySizeConstraint < Constraint
580
- # @!method size
581
- # A registered method on a symbol to create the constraint.
582
+ # @!method array_size
583
+ # A registered method on a symbol to create the constraint. The
584
+ # operator is registered as `:size` and installed on `Symbol` as
585
+ # `array_size`.
582
586
  # @example
583
- # q.where :field.size => 2
584
- # q.where :field.size => { gt: 3, lte: 10 }
587
+ # q.where :field.array_size => 2
588
+ # q.where :field.array_size => { gt: 3, lte: 10 }
585
589
  # @return [ArraySizeConstraint]
586
590
  register :size
587
591
 
@@ -1584,10 +1588,10 @@ module Parse
1584
1588
  unless query.is_a?(Parse::Query)
1585
1589
  raise ArgumentError, "Invalid Parse::Query object provided in :query field of value: #{@operation.operand}.#{$dontSelect} => #{@value}"
1586
1590
  end
1587
- query = query.compile(encode: false, includeClassName: true)
1591
+ query = query.compile_subquery
1588
1592
  elsif @value.is_a?(Parse::Query)
1589
1593
  # if its a query, then assume dontSelect key is the same name as operand.
1590
- query = @value.compile(encode: false, includeClassName: true)
1594
+ query = @value.compile_subquery
1591
1595
  else
1592
1596
  raise ArgumentError, "Invalid `:select` query constraint. It should follow the format: :field.select => { key: 'key', query: '<Parse::Query>' }"
1593
1597
  end
@@ -1635,10 +1639,10 @@ module Parse
1635
1639
  unless query.is_a?(Parse::Query)
1636
1640
  raise ArgumentError, "Invalid Parse::Query object provided in :query field of value: #{@operation.operand}.#{$dontSelect} => #{@value}"
1637
1641
  end
1638
- query = query.compile(encode: false, includeClassName: true)
1642
+ query = query.compile_subquery
1639
1643
  elsif @value.is_a?(Parse::Query)
1640
1644
  # if its a query, then assume dontSelect key is the same name as operand.
1641
- query = @value.compile(encode: false, includeClassName: true)
1645
+ query = @value.compile_subquery
1642
1646
  else
1643
1647
  raise ArgumentError, "Invalid `:reject` query constraint. It should follow the format: :field.reject => { key: 'key', query: '<Parse::Query>' }"
1644
1648
  end
@@ -2285,10 +2289,10 @@ module Parse
2285
2289
  unless query.is_a?(Parse::Query)
2286
2290
  raise ArgumentError, "Invalid Parse::Query object provided in :query field of value: #{@operation.operand}.matches_key_in_query => #{@value}"
2287
2291
  end
2288
- query = query.compile(encode: false, includeClassName: true)
2292
+ query = query.compile_subquery
2289
2293
  elsif @value.is_a?(Parse::Query)
2290
2294
  # if its a query, then assume key is the same name as operand.
2291
- query = @value.compile(encode: false, includeClassName: true)
2295
+ query = @value.compile_subquery
2292
2296
  else
2293
2297
  raise ArgumentError, "Invalid `:matches_key_in_query` query constraint. It should follow the format: :field.matches_key_in_query => { key: 'key', query: '<Parse::Query>' }"
2294
2298
  end
@@ -2335,10 +2339,10 @@ module Parse
2335
2339
  unless query.is_a?(Parse::Query)
2336
2340
  raise ArgumentError, "Invalid Parse::Query object provided in :query field of value: #{@operation.operand}.does_not_match_key_in_query => #{@value}"
2337
2341
  end
2338
- query = query.compile(encode: false, includeClassName: true)
2342
+ query = query.compile_subquery
2339
2343
  elsif @value.is_a?(Parse::Query)
2340
2344
  # if its a query, then assume key is the same name as operand.
2341
- query = @value.compile(encode: false, includeClassName: true)
2345
+ query = @value.compile_subquery
2342
2346
  else
2343
2347
  raise ArgumentError, "Invalid `:does_not_match_key_in_query` query constraint. It should follow the format: :field.does_not_match_key_in_query => { key: 'key', query: '<Parse::Query>' }"
2344
2348
  end