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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +1148 -0
- data/README.md +39 -23
- data/docs/TEST_SERVER.md +2 -2
- data/docs/acl_clp_guide.md +7 -0
- data/docs/atlas_vector_search_guide.md +181 -13
- data/docs/caching.md +50 -0
- data/docs/client_sdk_guide.md +11 -0
- data/docs/mcp_guide.md +332 -9
- data/docs/mongodb_direct_guide.md +27 -0
- data/docs/usage_guide.md +38 -0
- data/docs/webhooks_guide.md +126 -17
- data/lib/parse/acl_scope.rb +230 -50
- data/lib/parse/agent/approval_gate.rb +0 -0
- data/lib/parse/agent/constraint_translator.rb +42 -15
- data/lib/parse/agent/describe.rb +11 -3
- data/lib/parse/agent/field_names.rb +53 -0
- data/lib/parse/agent/field_policy.rb +74 -0
- data/lib/parse/agent/mcp_deployments.rb +426 -0
- data/lib/parse/agent/mcp_dispatcher.rb +8 -1
- data/lib/parse/agent/mcp_rack_app.rb +424 -45
- data/lib/parse/agent/mcp_server.rb +23 -1
- data/lib/parse/agent/mcp_subscriptions.rb +124 -6
- data/lib/parse/agent/metadata_registry.rb +67 -8
- data/lib/parse/agent/prompt_hardening.rb +9 -3
- data/lib/parse/agent/tools.rb +378 -29
- data/lib/parse/agent.rb +298 -21
- data/lib/parse/api/batch.rb +10 -1
- data/lib/parse/api/schema.rb +23 -4
- data/lib/parse/api/sessions.rb +6 -2
- data/lib/parse/api/users.rb +88 -14
- data/lib/parse/atlas_search/index_manager.rb +5 -1
- data/lib/parse/atlas_search/protected_paths.rb +236 -0
- data/lib/parse/atlas_search.rb +144 -24
- data/lib/parse/authorization.rb +289 -4
- data/lib/parse/cache/sub_cache.rb +26 -0
- data/lib/parse/client/authentication.rb +19 -1
- data/lib/parse/client/batch.rb +264 -35
- data/lib/parse/client/body_builder.rb +21 -0
- data/lib/parse/client/caching.rb +371 -27
- data/lib/parse/client/request.rb +26 -14
- data/lib/parse/client/response.rb +49 -6
- data/lib/parse/client.rb +227 -40
- data/lib/parse/clp_scope.rb +317 -25
- data/lib/parse/console.rb +2 -2
- data/lib/parse/embeddings/voyage.rb +181 -17
- data/lib/parse/graphql/type_generator.rb +3 -0
- data/lib/parse/live_query/client.rb +72 -2
- data/lib/parse/lock_backend.rb +4 -3
- data/lib/parse/model/acl.rb +119 -21
- data/lib/parse/model/associations/belongs_to.rb +25 -3
- data/lib/parse/model/associations/collection_proxy.rb +146 -17
- data/lib/parse/model/associations/has_many.rb +49 -9
- data/lib/parse/model/associations/has_one.rb +3 -1
- data/lib/parse/model/associations/pointer_collection_proxy.rb +145 -17
- data/lib/parse/model/associations/relation_collection_proxy.rb +229 -38
- data/lib/parse/model/bytes.rb +13 -5
- data/lib/parse/model/classes/role.rb +95 -0
- data/lib/parse/model/classes/session.rb +304 -1
- data/lib/parse/model/classes/user.rb +104 -8
- data/lib/parse/model/core/actions.rb +287 -70
- data/lib/parse/model/core/builder.rb +100 -8
- data/lib/parse/model/core/create_lock.rb +27 -2
- data/lib/parse/model/core/describe.rb +2 -0
- data/lib/parse/model/core/fetching.rb +50 -5
- data/lib/parse/model/core/field_guards.rb +16 -8
- data/lib/parse/model/core/pluralized_aliases.rb +8 -4
- data/lib/parse/model/core/properties.rb +488 -39
- data/lib/parse/model/core/querying.rb +7 -0
- data/lib/parse/model/core/schema.rb +5 -3
- data/lib/parse/model/core/search_indexing.rb +63 -0
- data/lib/parse/model/core/vector_searchable.rb +35 -6
- data/lib/parse/model/file.rb +9 -2
- data/lib/parse/model/geopoint.rb +61 -13
- data/lib/parse/model/model.rb +160 -9
- data/lib/parse/model/object.rb +317 -18
- data/lib/parse/model/phone.rb +54 -5
- data/lib/parse/model/pointer.rb +40 -6
- data/lib/parse/model/push.rb +11 -1
- data/lib/parse/mongodb.rb +189 -61
- data/lib/parse/pipeline_security.rb +415 -26
- data/lib/parse/query/constraint.rb +31 -1
- data/lib/parse/query/constraints.rb +89 -59
- data/lib/parse/query/cursor.rb +3 -1
- data/lib/parse/query/operation.rb +62 -8
- data/lib/parse/query/ordering.rb +34 -6
- data/lib/parse/query.rb +1871 -167
- data/lib/parse/retrieval/agent_tool.rb +225 -8
- data/lib/parse/retrieval/benchmark.rb +149 -0
- data/lib/parse/retrieval/profiles.rb +320 -0
- data/lib/parse/retrieval/retriever.rb +10 -1
- data/lib/parse/retrieval.rb +2 -0
- data/lib/parse/schema/search_index_migrator.rb +23 -5
- data/lib/parse/schema.rb +74 -18
- data/lib/parse/stack/tasks.rb +6 -4
- data/lib/parse/stack/version.rb +1 -1
- data/lib/parse/stack.rb +91 -14
- data/lib/parse/two_factor_auth/user_extension.rb +14 -2
- data/lib/parse/two_factor_auth.rb +11 -0
- data/lib/parse/vector_search/hybrid.rb +110 -33
- data/lib/parse/vector_search/index_definition.rb +237 -0
- data/lib/parse/vector_search.rb +259 -23
- data/lib/parse/webhooks/payload.rb +136 -6
- data/lib/parse/webhooks/replay_protection.rb +58 -20
- data/lib/parse/webhooks.rb +715 -40
- 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.
|
|
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.
|
|
117
|
+
# Song.all :artist.pointer_id => artist_id
|
|
93
118
|
#
|
|
94
119
|
# # other approaches, same result
|
|
95
|
-
# Song.all :artist.
|
|
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
|
|
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.
|
|
104
|
-
# q.where :field.
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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.
|
|
537
|
-
# q.where :tags.
|
|
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.
|
|
541
|
-
# q.where :tags.
|
|
542
|
-
# q.where :tags.
|
|
543
|
-
# q.where :tags.
|
|
544
|
-
# q.where :tags.
|
|
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.
|
|
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
|
|
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.
|
|
563
|
-
# q.where :field.
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
#
|
|
3233
|
-
#
|
|
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 =
|
|
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
|
data/lib/parse/query/cursor.rb
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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)
|
data/lib/parse/query/ordering.rb
CHANGED
|
@@ -56,11 +56,39 @@ module Parse
|
|
|
56
56
|
end # Order
|
|
57
57
|
end
|
|
58
58
|
|
|
59
|
-
|
|
60
|
-
class
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
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)
|