parse-stack-next 5.7.6 → 5.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +830 -0
  3. data/README.md +14 -4
  4. data/docs/TEST_SERVER.md +2 -2
  5. data/docs/acl_clp_guide.md +7 -0
  6. data/docs/atlas_vector_search_guide.md +181 -13
  7. data/docs/client_sdk_guide.md +11 -0
  8. data/docs/mcp_guide.md +317 -6
  9. data/docs/mongodb_direct_guide.md +27 -0
  10. data/docs/usage_guide.md +38 -0
  11. data/docs/webhooks_guide.md +74 -17
  12. data/lib/parse/acl_scope.rb +159 -41
  13. data/lib/parse/agent/approval_gate.rb +0 -0
  14. data/lib/parse/agent/constraint_translator.rb +42 -15
  15. data/lib/parse/agent/describe.rb +3 -1
  16. data/lib/parse/agent/field_names.rb +53 -0
  17. data/lib/parse/agent/field_policy.rb +74 -0
  18. data/lib/parse/agent/mcp_deployments.rb +426 -0
  19. data/lib/parse/agent/mcp_rack_app.rb +424 -45
  20. data/lib/parse/agent/mcp_server.rb +23 -1
  21. data/lib/parse/agent/mcp_subscriptions.rb +124 -6
  22. data/lib/parse/agent/metadata_registry.rb +67 -8
  23. data/lib/parse/agent/prompt_hardening.rb +9 -3
  24. data/lib/parse/agent/tools.rb +378 -29
  25. data/lib/parse/agent.rb +93 -1
  26. data/lib/parse/api/batch.rb +10 -1
  27. data/lib/parse/api/schema.rb +23 -4
  28. data/lib/parse/api/sessions.rb +6 -2
  29. data/lib/parse/api/users.rb +88 -14
  30. data/lib/parse/atlas_search/protected_paths.rb +236 -0
  31. data/lib/parse/atlas_search.rb +95 -23
  32. data/lib/parse/authorization.rb +54 -1
  33. data/lib/parse/client/batch.rb +231 -35
  34. data/lib/parse/client/body_builder.rb +21 -0
  35. data/lib/parse/client/caching.rb +371 -27
  36. data/lib/parse/client/request.rb +26 -14
  37. data/lib/parse/client/response.rb +49 -6
  38. data/lib/parse/client.rb +201 -38
  39. data/lib/parse/clp_scope.rb +281 -23
  40. data/lib/parse/console.rb +2 -2
  41. data/lib/parse/embeddings/voyage.rb +181 -17
  42. data/lib/parse/graphql/type_generator.rb +3 -0
  43. data/lib/parse/model/acl.rb +119 -21
  44. data/lib/parse/model/associations/belongs_to.rb +25 -3
  45. data/lib/parse/model/associations/collection_proxy.rb +138 -17
  46. data/lib/parse/model/associations/has_many.rb +38 -9
  47. data/lib/parse/model/associations/has_one.rb +3 -1
  48. data/lib/parse/model/associations/pointer_collection_proxy.rb +109 -17
  49. data/lib/parse/model/associations/relation_collection_proxy.rb +134 -28
  50. data/lib/parse/model/bytes.rb +13 -5
  51. data/lib/parse/model/classes/role.rb +72 -0
  52. data/lib/parse/model/classes/session.rb +43 -0
  53. data/lib/parse/model/classes/user.rb +78 -3
  54. data/lib/parse/model/core/actions.rb +269 -67
  55. data/lib/parse/model/core/builder.rb +100 -8
  56. data/lib/parse/model/core/create_lock.rb +27 -2
  57. data/lib/parse/model/core/describe.rb +2 -0
  58. data/lib/parse/model/core/fetching.rb +21 -3
  59. data/lib/parse/model/core/pluralized_aliases.rb +8 -4
  60. data/lib/parse/model/core/properties.rb +488 -39
  61. data/lib/parse/model/core/querying.rb +7 -0
  62. data/lib/parse/model/core/schema.rb +5 -3
  63. data/lib/parse/model/core/search_indexing.rb +63 -0
  64. data/lib/parse/model/core/vector_searchable.rb +35 -6
  65. data/lib/parse/model/file.rb +9 -2
  66. data/lib/parse/model/geopoint.rb +61 -13
  67. data/lib/parse/model/model.rb +160 -9
  68. data/lib/parse/model/object.rb +265 -17
  69. data/lib/parse/model/phone.rb +54 -5
  70. data/lib/parse/model/pointer.rb +40 -6
  71. data/lib/parse/mongodb.rb +170 -60
  72. data/lib/parse/pipeline_security.rb +415 -26
  73. data/lib/parse/query/constraint.rb +30 -0
  74. data/lib/parse/query/constraints.rb +58 -32
  75. data/lib/parse/query/cursor.rb +3 -1
  76. data/lib/parse/query/operation.rb +62 -8
  77. data/lib/parse/query/ordering.rb +34 -6
  78. data/lib/parse/query.rb +1100 -134
  79. data/lib/parse/retrieval/agent_tool.rb +225 -8
  80. data/lib/parse/retrieval/benchmark.rb +149 -0
  81. data/lib/parse/retrieval/profiles.rb +320 -0
  82. data/lib/parse/retrieval/retriever.rb +10 -1
  83. data/lib/parse/retrieval.rb +2 -0
  84. data/lib/parse/schema/search_index_migrator.rb +23 -5
  85. data/lib/parse/schema.rb +74 -18
  86. data/lib/parse/stack/tasks.rb +6 -4
  87. data/lib/parse/stack/version.rb +1 -1
  88. data/lib/parse/stack.rb +72 -14
  89. data/lib/parse/two_factor_auth/user_extension.rb +14 -2
  90. data/lib/parse/two_factor_auth.rb +11 -0
  91. data/lib/parse/vector_search/hybrid.rb +36 -18
  92. data/lib/parse/vector_search/index_definition.rb +237 -0
  93. data/lib/parse/vector_search.rb +46 -17
  94. data/lib/parse/webhooks/payload.rb +93 -6
  95. data/lib/parse/webhooks/replay_protection.rb +58 -20
  96. data/lib/parse/webhooks.rb +412 -40
  97. metadata +8 -1
@@ -64,6 +64,31 @@ module Parse
64
64
  end
65
65
 
66
66
  class Constraint
67
+ # @!visibility private
68
+ # The constraint value as a list for the array operators (`$in`, `$nin`,
69
+ # `$all`, `$containedBy`). A Set or a finite Range is expanded into its
70
+ # members; before, it was wrapped as a single element (`[Set[1, 2]]`,
71
+ # `["1..3"]`) and matched nothing. A Range that cannot be enumerated
72
+ # (endless, beginless, or Float bounds) raises ArgumentError; use
73
+ # `:field.between` for a numeric interval.
74
+ # @return [Array]
75
+ def list_value
76
+ raw = @value
77
+ if raw.is_a?(Set)
78
+ raw = raw.to_a
79
+ elsif raw.is_a?(Range)
80
+ unless raw.begin.respond_to?(:succ) && !raw.end.nil?
81
+ raise ArgumentError,
82
+ "Cannot expand #{raw.inspect} into a list for #{key}. " \
83
+ "Pass an Array, or use :field.between for an interval."
84
+ end
85
+ raw = raw.to_a
86
+ end
87
+ val = self.class.formatted_value(raw)
88
+ val = [val].compact unless val.is_a?(Array)
89
+ val
90
+ end
91
+
67
92
  # A constraint for matching by a specific objectId value.
68
93
  #
69
94
  # # where this Parse object equals the object in the column `field`.
@@ -396,8 +421,7 @@ module Parse
396
421
 
397
422
  # @return [Hash] the compiled constraint.
398
423
  def build
399
- val = formatted_value
400
- val = [val].compact unless val.is_a?(Array)
424
+ val = list_value
401
425
 
402
426
  # Convert Parse objects to pointers for array contains queries
403
427
  if val.is_a?(Array)
@@ -447,8 +471,7 @@ module Parse
447
471
 
448
472
  # @return [Hash] the compiled constraint.
449
473
  def build
450
- val = formatted_value
451
- val = [val].compact unless val.is_a?(Array)
474
+ val = list_value
452
475
 
453
476
  # Convert Parse objects to pointers for array contains queries
454
477
  if val.is_a?(Array)
@@ -493,8 +516,7 @@ module Parse
493
516
 
494
517
  # @return [Hash] the compiled constraint.
495
518
  def build
496
- val = formatted_value
497
- val = [val].compact unless val.is_a?(Array)
519
+ val = list_value
498
520
  { @operation.operand => { key => val } }
499
521
  end
500
522
  end
@@ -522,8 +544,7 @@ module Parse
522
544
 
523
545
  # @return [Hash] the compiled constraint.
524
546
  def build
525
- val = formatted_value
526
- val = [val].compact unless val.is_a?(Array)
547
+ val = list_value
527
548
  { @operation.operand => { key => val } }
528
549
  end
529
550
  end
@@ -864,8 +885,7 @@ module Parse
864
885
 
865
886
  # @return [Hash] the compiled constraint using aggregation pipeline.
866
887
  def build
867
- val = formatted_value
868
- val = [val].compact unless val.is_a?(Array)
888
+ val = list_value
869
889
 
870
890
  field_name = Parse::Query.format_field(@operation.operand)
871
891
 
@@ -960,8 +980,7 @@ module Parse
960
980
 
961
981
  # @return [Hash] the compiled constraint using aggregation pipeline.
962
982
  def build
963
- val = formatted_value
964
- val = [val].compact unless val.is_a?(Array)
983
+ val = list_value
965
984
 
966
985
  field_name = Parse::Query.format_field(@operation.operand)
967
986
 
@@ -1050,8 +1069,7 @@ module Parse
1050
1069
 
1051
1070
  # @return [Hash] the compiled constraint using aggregation pipeline.
1052
1071
  def build
1053
- val = formatted_value
1054
- val = [val].compact unless val.is_a?(Array)
1072
+ val = list_value
1055
1073
 
1056
1074
  field_name = Parse::Query.format_field(@operation.operand)
1057
1075
 
@@ -1140,8 +1158,7 @@ module Parse
1140
1158
 
1141
1159
  # @return [Hash] the compiled constraint using aggregation pipeline.
1142
1160
  def build
1143
- val = formatted_value
1144
- val = [val].compact unless val.is_a?(Array)
1161
+ val = list_value
1145
1162
 
1146
1163
  field_name = Parse::Query.format_field(@operation.operand)
1147
1164
 
@@ -1307,8 +1324,7 @@ module Parse
1307
1324
 
1308
1325
  # @return [Hash] the compiled constraint using aggregation pipeline.
1309
1326
  def build
1310
- val = formatted_value
1311
- val = [val].compact unless val.is_a?(Array)
1327
+ val = list_value
1312
1328
 
1313
1329
  field_name = Parse::Query.format_field(@operation.operand)
1314
1330
 
@@ -3085,16 +3101,20 @@ module Parse
3085
3101
  target_field = @value[:field]
3086
3102
  local_field = @operation.operand
3087
3103
 
3088
- # Format field names according to Parse conventions
3089
- # Pointer fields in MongoDB are stored with _p_ prefix
3090
- formatted_through = "_p_" + Parse::Query.format_field(through_field)
3091
- formatted_target = "_p_" + Parse::Query.format_field(target_field)
3092
- formatted_local = "_p_" + Parse::Query.format_field(local_field)
3093
-
3094
3104
  # Determine the target collection name from the through field
3095
3105
  # Use classify to convert field name to class name (e.g., :project -> "Project")
3096
3106
  target_collection = through_field.to_s.classify
3097
3107
 
3108
+ # Format field names according to Parse conventions
3109
+ # Pointer fields in MongoDB are stored with _p_ prefix. The local and
3110
+ # through fields are columns of the queried class; the target field is
3111
+ # a column of the linked class, so it uses that class's names.
3112
+ formatted_through = "_p_" + Parse::Query.format_field(through_field)
3113
+ formatted_target = "_p_" + Parse::Query.with_field_aliases(target_collection) do
3114
+ Parse::Query.format_field(target_field)
3115
+ end
3116
+ formatted_local = "_p_" + Parse::Query.format_field(local_field)
3117
+
3098
3118
  # Build the aggregation pipeline
3099
3119
  # Use clean alias name without _p_ prefix for readability
3100
3120
  lookup_alias = "#{through_field.to_s.camelize(:lower)}_data"
@@ -3168,15 +3188,20 @@ module Parse
3168
3188
  through_field = @value[:through]
3169
3189
  target_field = @value[:field]
3170
3190
 
3171
- # Convert field names to Parse format (snake_case to camelCase) with _p_ prefix for pointers
3172
- local_field_name = format_field_name(@operation.operand, is_pointer: true)
3173
- through_field_name = format_field_name(through_field, is_pointer: true)
3174
- target_field_name = format_field_name(target_field, is_pointer: true)
3175
-
3176
3191
  # Determine the collection name for the lookup (Rails pluralization)
3177
3192
  through_class_name = through_field.to_s.classify
3178
3193
  lookup_collection = through_class_name
3179
3194
 
3195
+ # Convert field names to Parse format with _p_ prefix for pointers.
3196
+ # The local and through fields are columns of the queried class; the
3197
+ # target field is a column of the linked class, so it uses that
3198
+ # class's names.
3199
+ local_field_name = format_field_name(@operation.operand, is_pointer: true)
3200
+ through_field_name = format_field_name(through_field, is_pointer: true)
3201
+ target_field_name = Parse::Query.with_field_aliases(lookup_collection) do
3202
+ format_field_name(target_field, is_pointer: true)
3203
+ end
3204
+
3180
3205
  # Generate unique alias name for the joined data (use clean name without _p_ prefix)
3181
3206
  lookup_alias = "#{through_field.to_s.camelize(:lower)}_data"
3182
3207
 
@@ -3229,13 +3254,14 @@ module Parse
3229
3254
 
3230
3255
  private
3231
3256
 
3232
- # Converts field names from snake_case to camelCase for Parse Server compatibility
3233
- # and adds _p_ prefix for pointer fields in MongoDB
3257
+ # Formats a field name with {Parse::Query.format_field} (so declared
3258
+ # `field:` names in the current alias scope are honored) and adds the
3259
+ # _p_ prefix for pointer fields in MongoDB.
3234
3260
  # @param field [Symbol, String] the field name to format
3235
3261
  # @param is_pointer [Boolean] whether this field is a pointer field
3236
3262
  # @return [String] the formatted field name
3237
3263
  def format_field_name(field, is_pointer: true)
3238
- formatted = field.to_s.camelize(:lower)
3264
+ formatted = Parse::Query.format_field(field)
3239
3265
  # Add _p_ prefix for pointer fields as they're stored that way in MongoDB
3240
3266
  is_pointer ? "_p_#{formatted}" : formatted
3241
3267
  end
@@ -365,7 +365,9 @@ module Parse
365
365
  # Returns: (field < last_value) OR (field = last_value AND objectId < last_id)
366
366
  # for descending order, or the inverse for ascending.
367
367
  def build_cursor_constraint
368
- formatted_field = Parse::Query.format_field(@order_field)
368
+ formatted_field = Parse::Query.with_field_aliases(@query.table) do
369
+ Parse::Query.format_field(@order_field)
370
+ end
369
371
 
370
372
  if @order_direction == :desc
371
373
  # Descending: (field < last_value) OR (field = last_value AND objectId < last_id)
@@ -10,7 +10,8 @@ module Parse
10
10
  # queries. It contains an operand (the Parse field) and an operator (the Parse
11
11
  # operation). These combined with a value, provide you with a constraint.
12
12
  #
13
- # All operation registrations add methods to the Symbol class.
13
+ # All operation registrations add methods to the Symbol class, through
14
+ # the {Operation::SymbolMethods} module.
14
15
  class Operation
15
16
 
16
17
  # @!attribute operand
@@ -87,18 +88,71 @@ module Parse
87
88
  handler.new(self, value)
88
89
  end
89
90
 
91
+ # Operator names that are never installed as `Symbol` methods because
92
+ # they would replace or shadow behavior other code relies on:
93
+ #
94
+ # - `:size` is Ruby's own `Symbol#size`; replacing it broke
95
+ # `sort_by(&:size)` and any code that measures a symbol.
96
+ # - `:id` makes every symbol `respond_to?(:id)`, which ActiveRecord
97
+ # treats as a record. `where(status: :draft)` then raised
98
+ # `TypeError: can't cast Parse::Operation` (issue #83).
99
+ #
100
+ # Each one is installed under the alternate name given here instead.
101
+ # The original name stays registered in {operators}, so an explicit
102
+ # `Parse::Operation.new(:tags, :size)` still resolves.
103
+ SYMBOL_METHOD_RENAMES = { id: :pointer_id, size: :array_size }.freeze
104
+
105
+ # The module that carries the query DSL methods (`:plays.gt`,
106
+ # `:tags.in`, ...). It is included into `Symbol` rather than defining
107
+ # methods on `Symbol` directly, so a method another library defines on
108
+ # `Symbol` (Mongoid's `Symbol#gt`, Sequel's `Symbol#desc`) is never
109
+ # replaced by Parse, whichever library loads first.
110
+ module SymbolMethods; end
111
+
112
+ class << self
113
+ # Operator names that were not installed on `Symbol` because another
114
+ # library already defines a method of that name. Use the explicit
115
+ # form `Parse::Operation.new(:field, :op) => value` for these.
116
+ # @return [Array<Symbol>]
117
+ def symbol_conflicts
118
+ @symbol_conflicts ||= []
119
+ end
120
+ end
121
+
90
122
  # Register a new symbol operator method mapped to a specific {Parse::Constraint}.
123
+ # @param op [Symbol] the operator name.
124
+ # @param klass [Class] the {Parse::Constraint} subclass that handles it.
91
125
  def self.register(op, klass)
92
- Operation.operators[op.to_sym] = klass
93
- # Some operator names (e.g. :size) collide with existing Symbol methods.
94
- # The override is intentional - the query DSL repurposes these for
95
- # constraint building. Remove the prior definition so define_method
96
- # does not emit "method redefined" under ruby -W.
97
- Symbol.send(:remove_method, op) if Symbol.method_defined?(op, false)
98
- Symbol.send :define_method, op do |value = nil|
126
+ op = op.to_sym
127
+ Operation.operators[op] = klass
128
+ method_name = SYMBOL_METHOD_RENAMES.fetch(op, op)
129
+ Operation.operators[method_name] = klass
130
+ install_symbol_method(method_name)
131
+ end
132
+
133
+ # Defines `method_name` on {SymbolMethods} unless `Symbol` already has
134
+ # a method of that name from somewhere other than Parse.
135
+ # @!visibility private
136
+ def self.install_symbol_method(method_name)
137
+ existing = ::Symbol.method_defined?(method_name) &&
138
+ ::Symbol.instance_method(method_name).owner
139
+ if existing && existing != SymbolMethods
140
+ symbol_conflicts << method_name unless symbol_conflicts.include?(method_name)
141
+ return false
142
+ end
143
+ if SymbolMethods.method_defined?(method_name, false)
144
+ SymbolMethods.send(:remove_method, method_name)
145
+ end
146
+ op = method_name
147
+ SymbolMethods.send :define_method, method_name do |value = nil|
99
148
  operation = Operation.new self, op
100
149
  value.nil? ? operation : operation.constraint(value)
101
150
  end
151
+ true
102
152
  end
103
153
  end
104
154
  end
155
+
156
+ # Included (not prepended) so methods defined on Symbol itself, by Ruby or
157
+ # by another library, keep precedence over the Parse query DSL.
158
+ Symbol.include(Parse::Operation::SymbolMethods)
@@ -56,11 +56,39 @@ module Parse
56
56
  end # Order
57
57
  end
58
58
 
59
- # Extension to add all the operator instance methods to the Symbol classe
60
- class Symbol
61
- Parse::Order::ORDERING.keys.each do |sym|
62
- define_method(sym) do
63
- Parse::Order.new self, sym
59
+ module Parse
60
+ class Order
61
+ # Carries the `:field.asc` / `:field.desc` sort helpers. Included into
62
+ # `Symbol` rather than defined on it, and skipped for any name another
63
+ # library already defines on `Symbol` (Mongoid, Sequel core extensions),
64
+ # so Parse never replaces another library's `Symbol#asc` or `#desc`.
65
+ # Use `Parse::Order.new(:field, :desc)` when such a library is loaded.
66
+ module SymbolMethods; end
67
+
68
+ # Converts another library's sort key (Mongoid's `:field.desc` when
69
+ # Mongoid owns `Symbol#desc`) into a {Parse::Order}.
70
+ # @param value [Object]
71
+ # @return [Parse::Order, nil] nil when `value` is not a known foreign sort key.
72
+ def self.from_foreign(value)
73
+ return value if value.is_a?(Parse::Order)
74
+ if defined?(::Mongoid::Criteria::Queryable::Key) &&
75
+ value.is_a?(::Mongoid::Criteria::Queryable::Key)
76
+ case value.operator
77
+ when 1, "1" then return new(value.name, :asc)
78
+ when -1, "-1" then return new(value.name, :desc)
79
+ end
80
+ end
81
+ nil
64
82
  end
65
- end # each
83
+
84
+ ORDERING.keys.each do |sym|
85
+ existing = ::Symbol.method_defined?(sym) && ::Symbol.instance_method(sym).owner
86
+ next if existing && existing != SymbolMethods
87
+ SymbolMethods.send(:define_method, sym) do
88
+ Parse::Order.new self, sym
89
+ end
90
+ end
91
+ end
66
92
  end
93
+
94
+ Symbol.include(Parse::Order::SymbolMethods)