parse-stack-next 5.7.6 → 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 (106) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1148 -0
  3. data/README.md +39 -23
  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/caching.md +50 -0
  8. data/docs/client_sdk_guide.md +11 -0
  9. data/docs/mcp_guide.md +332 -9
  10. data/docs/mongodb_direct_guide.md +27 -0
  11. data/docs/usage_guide.md +38 -0
  12. data/docs/webhooks_guide.md +126 -17
  13. data/lib/parse/acl_scope.rb +230 -50
  14. data/lib/parse/agent/approval_gate.rb +0 -0
  15. data/lib/parse/agent/constraint_translator.rb +42 -15
  16. data/lib/parse/agent/describe.rb +11 -3
  17. data/lib/parse/agent/field_names.rb +53 -0
  18. data/lib/parse/agent/field_policy.rb +74 -0
  19. data/lib/parse/agent/mcp_deployments.rb +426 -0
  20. data/lib/parse/agent/mcp_dispatcher.rb +8 -1
  21. data/lib/parse/agent/mcp_rack_app.rb +424 -45
  22. data/lib/parse/agent/mcp_server.rb +23 -1
  23. data/lib/parse/agent/mcp_subscriptions.rb +124 -6
  24. data/lib/parse/agent/metadata_registry.rb +67 -8
  25. data/lib/parse/agent/prompt_hardening.rb +9 -3
  26. data/lib/parse/agent/tools.rb +378 -29
  27. data/lib/parse/agent.rb +298 -21
  28. data/lib/parse/api/batch.rb +10 -1
  29. data/lib/parse/api/schema.rb +23 -4
  30. data/lib/parse/api/sessions.rb +6 -2
  31. data/lib/parse/api/users.rb +88 -14
  32. data/lib/parse/atlas_search/index_manager.rb +5 -1
  33. data/lib/parse/atlas_search/protected_paths.rb +236 -0
  34. data/lib/parse/atlas_search.rb +144 -24
  35. data/lib/parse/authorization.rb +289 -4
  36. data/lib/parse/cache/sub_cache.rb +26 -0
  37. data/lib/parse/client/authentication.rb +19 -1
  38. data/lib/parse/client/batch.rb +264 -35
  39. data/lib/parse/client/body_builder.rb +21 -0
  40. data/lib/parse/client/caching.rb +371 -27
  41. data/lib/parse/client/request.rb +26 -14
  42. data/lib/parse/client/response.rb +49 -6
  43. data/lib/parse/client.rb +227 -40
  44. data/lib/parse/clp_scope.rb +317 -25
  45. data/lib/parse/console.rb +2 -2
  46. data/lib/parse/embeddings/voyage.rb +181 -17
  47. data/lib/parse/graphql/type_generator.rb +3 -0
  48. data/lib/parse/live_query/client.rb +72 -2
  49. data/lib/parse/lock_backend.rb +4 -3
  50. data/lib/parse/model/acl.rb +119 -21
  51. data/lib/parse/model/associations/belongs_to.rb +25 -3
  52. data/lib/parse/model/associations/collection_proxy.rb +146 -17
  53. data/lib/parse/model/associations/has_many.rb +49 -9
  54. data/lib/parse/model/associations/has_one.rb +3 -1
  55. data/lib/parse/model/associations/pointer_collection_proxy.rb +145 -17
  56. data/lib/parse/model/associations/relation_collection_proxy.rb +229 -38
  57. data/lib/parse/model/bytes.rb +13 -5
  58. data/lib/parse/model/classes/role.rb +95 -0
  59. data/lib/parse/model/classes/session.rb +304 -1
  60. data/lib/parse/model/classes/user.rb +104 -8
  61. data/lib/parse/model/core/actions.rb +287 -70
  62. data/lib/parse/model/core/builder.rb +100 -8
  63. data/lib/parse/model/core/create_lock.rb +27 -2
  64. data/lib/parse/model/core/describe.rb +2 -0
  65. data/lib/parse/model/core/fetching.rb +50 -5
  66. data/lib/parse/model/core/field_guards.rb +16 -8
  67. data/lib/parse/model/core/pluralized_aliases.rb +8 -4
  68. data/lib/parse/model/core/properties.rb +488 -39
  69. data/lib/parse/model/core/querying.rb +7 -0
  70. data/lib/parse/model/core/schema.rb +5 -3
  71. data/lib/parse/model/core/search_indexing.rb +63 -0
  72. data/lib/parse/model/core/vector_searchable.rb +35 -6
  73. data/lib/parse/model/file.rb +9 -2
  74. data/lib/parse/model/geopoint.rb +61 -13
  75. data/lib/parse/model/model.rb +160 -9
  76. data/lib/parse/model/object.rb +317 -18
  77. data/lib/parse/model/phone.rb +54 -5
  78. data/lib/parse/model/pointer.rb +40 -6
  79. data/lib/parse/model/push.rb +11 -1
  80. data/lib/parse/mongodb.rb +189 -61
  81. data/lib/parse/pipeline_security.rb +415 -26
  82. data/lib/parse/query/constraint.rb +31 -1
  83. data/lib/parse/query/constraints.rb +89 -59
  84. data/lib/parse/query/cursor.rb +3 -1
  85. data/lib/parse/query/operation.rb +62 -8
  86. data/lib/parse/query/ordering.rb +34 -6
  87. data/lib/parse/query.rb +1871 -167
  88. data/lib/parse/retrieval/agent_tool.rb +225 -8
  89. data/lib/parse/retrieval/benchmark.rb +149 -0
  90. data/lib/parse/retrieval/profiles.rb +320 -0
  91. data/lib/parse/retrieval/retriever.rb +10 -1
  92. data/lib/parse/retrieval.rb +2 -0
  93. data/lib/parse/schema/search_index_migrator.rb +23 -5
  94. data/lib/parse/schema.rb +74 -18
  95. data/lib/parse/stack/tasks.rb +6 -4
  96. data/lib/parse/stack/version.rb +1 -1
  97. data/lib/parse/stack.rb +91 -14
  98. data/lib/parse/two_factor_auth/user_extension.rb +14 -2
  99. data/lib/parse/two_factor_auth.rb +11 -0
  100. data/lib/parse/vector_search/hybrid.rb +110 -33
  101. data/lib/parse/vector_search/index_definition.rb +237 -0
  102. data/lib/parse/vector_search.rb +259 -23
  103. data/lib/parse/webhooks/payload.rb +136 -6
  104. data/lib/parse/webhooks/replay_protection.rb +58 -20
  105. data/lib/parse/webhooks.rb +715 -40
  106. metadata +8 -1
@@ -64,12 +64,37 @@ 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`.
70
95
  # q.where :field => Parse::Pointer("Field", "someObjectId")
71
96
  # # alias, shorthand when we infer `:field` maps to `Field` parse class.
72
- # q.where :field.id => "someObjectId"
97
+ # q.where :field.pointer_id => "someObjectId"
73
98
  # # "field":{"__type":"Pointer","className":"Field","objectId":"someObjectId"}}
74
99
  #
75
100
  # class Artist < Parse::Object
@@ -89,19 +114,21 @@ module Parse
89
114
  # You can use the objectId in the query as follows:
90
115
  #
91
116
  # # shorthand if you are using convention. Will infer class `Artist`
92
- # Song.all :artist.id => artist_id
117
+ # Song.all :artist.pointer_id => artist_id
93
118
  #
94
119
  # # other approaches, same result
95
- # Song.all :artist.id => artist # safely supported Parse::Pointer
120
+ # Song.all :artist.pointer_id => artist # safely supported Parse::Pointer
96
121
  # Song.all :artist => Artist.pointer(artist_id)
97
122
  # Song.all :artist => Parse::Pointer.new("Artist", artist_id)
98
123
  #
99
124
  class ObjectIdConstraint < Constraint
100
- # @!method id
101
- # 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`.
102
129
  # @example
103
- # q.where :field.id => "someObjectId"
104
- # q.where :field.id => pointer # safely supported
130
+ # q.where :field.pointer_id => "someObjectId"
131
+ # q.where :field.pointer_id => pointer # safely supported
105
132
  # @return [ObjectIdConstraint]
106
133
  register :id
107
134
 
@@ -396,8 +423,7 @@ module Parse
396
423
 
397
424
  # @return [Hash] the compiled constraint.
398
425
  def build
399
- val = formatted_value
400
- val = [val].compact unless val.is_a?(Array)
426
+ val = list_value
401
427
 
402
428
  # Convert Parse objects to pointers for array contains queries
403
429
  if val.is_a?(Array)
@@ -447,8 +473,7 @@ module Parse
447
473
 
448
474
  # @return [Hash] the compiled constraint.
449
475
  def build
450
- val = formatted_value
451
- val = [val].compact unless val.is_a?(Array)
476
+ val = list_value
452
477
 
453
478
  # Convert Parse objects to pointers for array contains queries
454
479
  if val.is_a?(Array)
@@ -493,8 +518,7 @@ module Parse
493
518
 
494
519
  # @return [Hash] the compiled constraint.
495
520
  def build
496
- val = formatted_value
497
- val = [val].compact unless val.is_a?(Array)
521
+ val = list_value
498
522
  { @operation.operand => { key => val } }
499
523
  end
500
524
  end
@@ -522,8 +546,7 @@ module Parse
522
546
 
523
547
  # @return [Hash] the compiled constraint.
524
548
  def build
525
- val = formatted_value
526
- val = [val].compact unless val.is_a?(Array)
549
+ val = list_value
527
550
  { @operation.operand => { key => val } }
528
551
  end
529
552
  end
@@ -533,18 +556,18 @@ module Parse
533
556
  # MongoDB aggregation pipeline with $expr and $size to check array length.
534
557
  #
535
558
  # # Exact size match
536
- # q.where :field.size => 2
537
- # q.where :tags.size => 5
559
+ # q.where :field.array_size => 2
560
+ # q.where :tags.array_size => 5
538
561
  #
539
562
  # # Comparison operators via hash
540
- # q.where :tags.size => { gt: 3 } # size > 3
541
- # q.where :tags.size => { gte: 2 } # size >= 2
542
- # q.where :tags.size => { lt: 5 } # size < 5
543
- # q.where :tags.size => { lte: 4 } # size <= 4
544
- # 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
545
568
  #
546
569
  # # Combine for range
547
- # q.where :tags.size => { gte: 2, lt: 10 } # 2 <= size < 10
570
+ # q.where :tags.array_size => { gte: 2, lt: 10 } # 2 <= size < 10
548
571
  #
549
572
  # @note This constraint uses aggregation pipeline because Parse Server
550
573
  # does not support the $size query operator natively.
@@ -556,11 +579,13 @@ module Parse
556
579
  # @see ContainsAllConstraint
557
580
  # @see ArraySetEqualsConstraint
558
581
  class ArraySizeConstraint < Constraint
559
- # @!method size
560
- # 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`.
561
586
  # @example
562
- # q.where :field.size => 2
563
- # 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 }
564
589
  # @return [ArraySizeConstraint]
565
590
  register :size
566
591
 
@@ -864,8 +889,7 @@ module Parse
864
889
 
865
890
  # @return [Hash] the compiled constraint using aggregation pipeline.
866
891
  def build
867
- val = formatted_value
868
- val = [val].compact unless val.is_a?(Array)
892
+ val = list_value
869
893
 
870
894
  field_name = Parse::Query.format_field(@operation.operand)
871
895
 
@@ -960,8 +984,7 @@ module Parse
960
984
 
961
985
  # @return [Hash] the compiled constraint using aggregation pipeline.
962
986
  def build
963
- val = formatted_value
964
- val = [val].compact unless val.is_a?(Array)
987
+ val = list_value
965
988
 
966
989
  field_name = Parse::Query.format_field(@operation.operand)
967
990
 
@@ -1050,8 +1073,7 @@ module Parse
1050
1073
 
1051
1074
  # @return [Hash] the compiled constraint using aggregation pipeline.
1052
1075
  def build
1053
- val = formatted_value
1054
- val = [val].compact unless val.is_a?(Array)
1076
+ val = list_value
1055
1077
 
1056
1078
  field_name = Parse::Query.format_field(@operation.operand)
1057
1079
 
@@ -1140,8 +1162,7 @@ module Parse
1140
1162
 
1141
1163
  # @return [Hash] the compiled constraint using aggregation pipeline.
1142
1164
  def build
1143
- val = formatted_value
1144
- val = [val].compact unless val.is_a?(Array)
1165
+ val = list_value
1145
1166
 
1146
1167
  field_name = Parse::Query.format_field(@operation.operand)
1147
1168
 
@@ -1307,8 +1328,7 @@ module Parse
1307
1328
 
1308
1329
  # @return [Hash] the compiled constraint using aggregation pipeline.
1309
1330
  def build
1310
- val = formatted_value
1311
- val = [val].compact unless val.is_a?(Array)
1331
+ val = list_value
1312
1332
 
1313
1333
  field_name = Parse::Query.format_field(@operation.operand)
1314
1334
 
@@ -1568,10 +1588,10 @@ module Parse
1568
1588
  unless query.is_a?(Parse::Query)
1569
1589
  raise ArgumentError, "Invalid Parse::Query object provided in :query field of value: #{@operation.operand}.#{$dontSelect} => #{@value}"
1570
1590
  end
1571
- query = query.compile(encode: false, includeClassName: true)
1591
+ query = query.compile_subquery
1572
1592
  elsif @value.is_a?(Parse::Query)
1573
1593
  # if its a query, then assume dontSelect key is the same name as operand.
1574
- query = @value.compile(encode: false, includeClassName: true)
1594
+ query = @value.compile_subquery
1575
1595
  else
1576
1596
  raise ArgumentError, "Invalid `:select` query constraint. It should follow the format: :field.select => { key: 'key', query: '<Parse::Query>' }"
1577
1597
  end
@@ -1619,10 +1639,10 @@ module Parse
1619
1639
  unless query.is_a?(Parse::Query)
1620
1640
  raise ArgumentError, "Invalid Parse::Query object provided in :query field of value: #{@operation.operand}.#{$dontSelect} => #{@value}"
1621
1641
  end
1622
- query = query.compile(encode: false, includeClassName: true)
1642
+ query = query.compile_subquery
1623
1643
  elsif @value.is_a?(Parse::Query)
1624
1644
  # if its a query, then assume dontSelect key is the same name as operand.
1625
- query = @value.compile(encode: false, includeClassName: true)
1645
+ query = @value.compile_subquery
1626
1646
  else
1627
1647
  raise ArgumentError, "Invalid `:reject` query constraint. It should follow the format: :field.reject => { key: 'key', query: '<Parse::Query>' }"
1628
1648
  end
@@ -2269,10 +2289,10 @@ module Parse
2269
2289
  unless query.is_a?(Parse::Query)
2270
2290
  raise ArgumentError, "Invalid Parse::Query object provided in :query field of value: #{@operation.operand}.matches_key_in_query => #{@value}"
2271
2291
  end
2272
- query = query.compile(encode: false, includeClassName: true)
2292
+ query = query.compile_subquery
2273
2293
  elsif @value.is_a?(Parse::Query)
2274
2294
  # if its a query, then assume key is the same name as operand.
2275
- query = @value.compile(encode: false, includeClassName: true)
2295
+ query = @value.compile_subquery
2276
2296
  else
2277
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>' }"
2278
2298
  end
@@ -2319,10 +2339,10 @@ module Parse
2319
2339
  unless query.is_a?(Parse::Query)
2320
2340
  raise ArgumentError, "Invalid Parse::Query object provided in :query field of value: #{@operation.operand}.does_not_match_key_in_query => #{@value}"
2321
2341
  end
2322
- query = query.compile(encode: false, includeClassName: true)
2342
+ query = query.compile_subquery
2323
2343
  elsif @value.is_a?(Parse::Query)
2324
2344
  # if its a query, then assume key is the same name as operand.
2325
- query = @value.compile(encode: false, includeClassName: true)
2345
+ query = @value.compile_subquery
2326
2346
  else
2327
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>' }"
2328
2348
  end
@@ -3085,16 +3105,20 @@ module Parse
3085
3105
  target_field = @value[:field]
3086
3106
  local_field = @operation.operand
3087
3107
 
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
3108
  # Determine the target collection name from the through field
3095
3109
  # Use classify to convert field name to class name (e.g., :project -> "Project")
3096
3110
  target_collection = through_field.to_s.classify
3097
3111
 
3112
+ # Format field names according to Parse conventions
3113
+ # Pointer fields in MongoDB are stored with _p_ prefix. The local and
3114
+ # through fields are columns of the queried class; the target field is
3115
+ # a column of the linked class, so it uses that class's names.
3116
+ formatted_through = "_p_" + Parse::Query.format_field(through_field)
3117
+ formatted_target = "_p_" + Parse::Query.with_field_aliases(target_collection) do
3118
+ Parse::Query.format_field(target_field)
3119
+ end
3120
+ formatted_local = "_p_" + Parse::Query.format_field(local_field)
3121
+
3098
3122
  # Build the aggregation pipeline
3099
3123
  # Use clean alias name without _p_ prefix for readability
3100
3124
  lookup_alias = "#{through_field.to_s.camelize(:lower)}_data"
@@ -3168,15 +3192,20 @@ module Parse
3168
3192
  through_field = @value[:through]
3169
3193
  target_field = @value[:field]
3170
3194
 
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
3195
  # Determine the collection name for the lookup (Rails pluralization)
3177
3196
  through_class_name = through_field.to_s.classify
3178
3197
  lookup_collection = through_class_name
3179
3198
 
3199
+ # Convert field names to Parse format with _p_ prefix for pointers.
3200
+ # The local and through fields are columns of the queried class; the
3201
+ # target field is a column of the linked class, so it uses that
3202
+ # class's names.
3203
+ local_field_name = format_field_name(@operation.operand, is_pointer: true)
3204
+ through_field_name = format_field_name(through_field, is_pointer: true)
3205
+ target_field_name = Parse::Query.with_field_aliases(lookup_collection) do
3206
+ format_field_name(target_field, is_pointer: true)
3207
+ end
3208
+
3180
3209
  # Generate unique alias name for the joined data (use clean name without _p_ prefix)
3181
3210
  lookup_alias = "#{through_field.to_s.camelize(:lower)}_data"
3182
3211
 
@@ -3229,13 +3258,14 @@ module Parse
3229
3258
 
3230
3259
  private
3231
3260
 
3232
- # Converts field names from snake_case to camelCase for Parse Server compatibility
3233
- # and adds _p_ prefix for pointer fields in MongoDB
3261
+ # Formats a field name with {Parse::Query.format_field} (so declared
3262
+ # `field:` names in the current alias scope are honored) and adds the
3263
+ # _p_ prefix for pointer fields in MongoDB.
3234
3264
  # @param field [Symbol, String] the field name to format
3235
3265
  # @param is_pointer [Boolean] whether this field is a pointer field
3236
3266
  # @return [String] the formatted field name
3237
3267
  def format_field_name(field, is_pointer: true)
3238
- formatted = field.to_s.camelize(:lower)
3268
+ formatted = Parse::Query.format_field(field)
3239
3269
  # Add _p_ prefix for pointer fields as they're stored that way in MongoDB
3240
3270
  is_pointer ? "_p_#{formatted}" : formatted
3241
3271
  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)