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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +318 -0
- data/README.md +25 -19
- data/docs/caching.md +50 -0
- data/docs/mcp_guide.md +15 -3
- data/docs/webhooks_guide.md +59 -7
- data/lib/parse/acl_scope.rb +71 -9
- data/lib/parse/agent/describe.rb +8 -2
- data/lib/parse/agent/mcp_dispatcher.rb +8 -1
- data/lib/parse/agent.rb +205 -20
- data/lib/parse/atlas_search/index_manager.rb +5 -1
- data/lib/parse/atlas_search.rb +49 -1
- data/lib/parse/authorization.rb +235 -3
- data/lib/parse/cache/sub_cache.rb +26 -0
- data/lib/parse/client/authentication.rb +19 -1
- data/lib/parse/client/batch.rb +35 -2
- data/lib/parse/client.rb +26 -2
- data/lib/parse/clp_scope.rb +37 -3
- data/lib/parse/live_query/client.rb +72 -2
- data/lib/parse/lock_backend.rb +4 -3
- data/lib/parse/model/associations/collection_proxy.rb +19 -11
- data/lib/parse/model/associations/has_many.rb +11 -0
- data/lib/parse/model/associations/pointer_collection_proxy.rb +36 -0
- data/lib/parse/model/associations/relation_collection_proxy.rb +97 -12
- data/lib/parse/model/classes/role.rb +23 -0
- data/lib/parse/model/classes/session.rb +279 -19
- data/lib/parse/model/classes/user.rb +33 -12
- data/lib/parse/model/core/actions.rb +19 -4
- data/lib/parse/model/core/fetching.rb +29 -2
- data/lib/parse/model/core/field_guards.rb +16 -8
- data/lib/parse/model/object.rb +52 -1
- data/lib/parse/model/push.rb +11 -1
- data/lib/parse/mongodb.rb +19 -1
- data/lib/parse/query/constraint.rb +1 -1
- data/lib/parse/query/constraints.rb +31 -27
- data/lib/parse/query.rb +793 -55
- data/lib/parse/stack/version.rb +1 -1
- data/lib/parse/stack.rb +19 -0
- data/lib/parse/vector_search/hybrid.rb +74 -15
- data/lib/parse/vector_search.rb +214 -7
- data/lib/parse/webhooks/payload.rb +43 -0
- data/lib/parse/webhooks.rb +312 -9
- metadata +1 -1
data/lib/parse/query.rb
CHANGED
|
@@ -209,8 +209,8 @@ module Parse
|
|
|
209
209
|
# # Valid values: :primary, :primary_preferred, :secondary, :secondary_preferred, :nearest
|
|
210
210
|
# @return [Symbol, String] the read preference for this query.
|
|
211
211
|
attr_reader :table, :session_token
|
|
212
|
-
|
|
213
|
-
|
|
212
|
+
attr_accessor :key, :cache, :verbose_aggregate, :read_preference
|
|
213
|
+
attr_reader :use_master_key
|
|
214
214
|
|
|
215
215
|
# We have a special class method to handle field formatting. This turns
|
|
216
216
|
# the symbol keys in an operand from one key to another. For example, we can
|
|
@@ -570,6 +570,58 @@ module Parse
|
|
|
570
570
|
constraint_reduce(where)
|
|
571
571
|
end
|
|
572
572
|
|
|
573
|
+
# SDK routing markers that a REST `$or` branch cannot carry.
|
|
574
|
+
# @!visibility private
|
|
575
|
+
OR_BRANCH_MARKERS = %w[__aggregation_pipeline __mongo_direct_only].freeze
|
|
576
|
+
|
|
577
|
+
# @!visibility private
|
|
578
|
+
# Compile one branch of an OR group. A constraint that only works
|
|
579
|
+
# through the aggregation pipeline or mongo-direct (`readable_by`,
|
|
580
|
+
# `:ACL.readable_by`, `:tags.array_size`, the mongo-direct geo
|
|
581
|
+
# operators) compiles to a routing marker that {compile_where}
|
|
582
|
+
# strips. Inside an OR that would turn the branch into `{}`, which
|
|
583
|
+
# matches every row, or silently drop part of it. Such a branch is
|
|
584
|
+
# refused instead.
|
|
585
|
+
# @param where [Array<Parse::Constraint>]
|
|
586
|
+
# @return [Hash] the compiled branch.
|
|
587
|
+
# @raise [ArgumentError] when the branch carries a routing marker.
|
|
588
|
+
def compile_or_branch(where)
|
|
589
|
+
markers = compile_markers(where)
|
|
590
|
+
if or_branch_marker?(markers)
|
|
591
|
+
names = pipeline_only_constraint_names(where)
|
|
592
|
+
raise ArgumentError,
|
|
593
|
+
"This constraint is not supported inside an OR " \
|
|
594
|
+
"(#{names.empty? ? "pipeline-only constraint" : names.join(", ")}): it needs the " \
|
|
595
|
+
"aggregation pipeline or mongo-direct, and a REST `$or` branch cannot carry it. " \
|
|
596
|
+
"Apply it outside the OR, or run the OR branches as separate queries."
|
|
597
|
+
end
|
|
598
|
+
compile_where(where)
|
|
599
|
+
end
|
|
600
|
+
|
|
601
|
+
# @!visibility private
|
|
602
|
+
# Names (`field.operator`) of the constraints in a where list that
|
|
603
|
+
# compile to a pipeline or mongo-direct routing marker.
|
|
604
|
+
# @return [Array<String>]
|
|
605
|
+
def pipeline_only_constraint_names(where)
|
|
606
|
+
Array(where).select { |c| c.is_a?(Parse::Constraint) && or_branch_marker?(c.as_json) }
|
|
607
|
+
.map do |c|
|
|
608
|
+
field = c.operand.to_s.casecmp?("acl") ? "ACL" : c.operand.to_s
|
|
609
|
+
c.operation.respond_to?(:operator) ? "#{field}.#{c.operation.operator}" : field
|
|
610
|
+
end.uniq
|
|
611
|
+
end
|
|
612
|
+
|
|
613
|
+
# @!visibility private
|
|
614
|
+
def or_branch_marker?(value)
|
|
615
|
+
case value
|
|
616
|
+
when Hash
|
|
617
|
+
value.any? { |k, v| OR_BRANCH_MARKERS.include?(k.to_s) || or_branch_marker?(v) }
|
|
618
|
+
when Array
|
|
619
|
+
value.any? { |v| or_branch_marker?(v) }
|
|
620
|
+
else
|
|
621
|
+
false
|
|
622
|
+
end
|
|
623
|
+
end
|
|
624
|
+
|
|
573
625
|
# @!visibility private
|
|
574
626
|
# Reduce a list of constraints into one where hash. Constraints on the
|
|
575
627
|
# same field are combined so every one of them still applies:
|
|
@@ -688,6 +740,14 @@ module Parse
|
|
|
688
740
|
@client ||= self.class.client
|
|
689
741
|
end
|
|
690
742
|
|
|
743
|
+
# Set the client this query runs through. A client from
|
|
744
|
+
# {Parse::Client#become} carries a session, so on a query combined
|
|
745
|
+
# with OR the change is checked like any other change of authority.
|
|
746
|
+
# @param value [Parse::Client]
|
|
747
|
+
def client=(value)
|
|
748
|
+
guard_or_scope_change { @client = value }
|
|
749
|
+
end
|
|
750
|
+
|
|
691
751
|
# Clear a specific clause of this query. This can be one of: :where, :order,
|
|
692
752
|
# :includes, :skip, :limit, :count, :keys or :results.
|
|
693
753
|
# @param item [:Symbol] the clause to clear.
|
|
@@ -768,6 +828,11 @@ module Parse
|
|
|
768
828
|
# and their value.
|
|
769
829
|
# @return [self]
|
|
770
830
|
def conditions(expressions = {})
|
|
831
|
+
# Apply `:or` last, so a `:session` or `:use_master_key` in the same
|
|
832
|
+
# Hash is in place before the branches' authority is compared.
|
|
833
|
+
if expressions.is_a?(Hash) && expressions.key?(:or) && expressions.keys.last != :or
|
|
834
|
+
expressions = expressions.reject { |k, _| k == :or }.merge(or: expressions[:or])
|
|
835
|
+
end
|
|
771
836
|
expressions.each do |expression, value|
|
|
772
837
|
# Normalize to symbol for comparison (handles both string and symbol keys)
|
|
773
838
|
expr_sym = expression.respond_to?(:to_sym) ? expression.to_sym : expression
|
|
@@ -835,6 +900,9 @@ module Parse
|
|
|
835
900
|
not_publicly_readable if value
|
|
836
901
|
elsif expr_sym == :not_publicly_writable
|
|
837
902
|
not_publicly_writable if value
|
|
903
|
+
elsif expression == :or
|
|
904
|
+
# Symbol key only: a string "or" stays a literal field name.
|
|
905
|
+
add_or_branches(value)
|
|
838
906
|
else
|
|
839
907
|
add_constraint(expression, value)
|
|
840
908
|
end
|
|
@@ -842,6 +910,114 @@ module Parse
|
|
|
842
910
|
self #chaining
|
|
843
911
|
end
|
|
844
912
|
|
|
913
|
+
# @!visibility private
|
|
914
|
+
# Handle the `:or => [branch, ...]` condition. Each branch is a
|
|
915
|
+
# constraint Hash, a {Parse::Query}, or an Array of {Parse::Constraint}.
|
|
916
|
+
# The branches are ORed together and the group is ANDed with the
|
|
917
|
+
# query's other constraints. A branch with no constraints matches every
|
|
918
|
+
# row, so the group adds no constraint. An empty list matches no rows.
|
|
919
|
+
# @param branches [Array]
|
|
920
|
+
def add_or_branches(branches)
|
|
921
|
+
unless branches.is_a?(Array)
|
|
922
|
+
raise ArgumentError, ":or expects an Array of constraint hashes or queries, got #{branches.class}."
|
|
923
|
+
end
|
|
924
|
+
with_or_rollback { add_or_branches_unguarded(branches) }
|
|
925
|
+
end
|
|
926
|
+
|
|
927
|
+
# @!visibility private
|
|
928
|
+
def add_or_branches_unguarded(branches)
|
|
929
|
+
wheres = branches.map do |branch|
|
|
930
|
+
case branch
|
|
931
|
+
when Parse::Query
|
|
932
|
+
unless branch.table == @table
|
|
933
|
+
raise ArgumentError, ":or branch queries must be of the same class #{@table} (got #{branch.table})."
|
|
934
|
+
end
|
|
935
|
+
# The branch's authority travels with its conditions, or raises
|
|
936
|
+
# when it differs from this query's.
|
|
937
|
+
merge_or_scope!(branch)
|
|
938
|
+
branch.where
|
|
939
|
+
when Hash then or_branch_hash_where(branch)
|
|
940
|
+
when Array
|
|
941
|
+
# A branch given as a list: constraints, or constraint hashes that
|
|
942
|
+
# are expanded the same way a Hash branch is. Anything else is
|
|
943
|
+
# refused, so a typo cannot empty the branch and turn it into
|
|
944
|
+
# match-all.
|
|
945
|
+
branch.flat_map do |item|
|
|
946
|
+
case item
|
|
947
|
+
when Parse::Constraint then [item]
|
|
948
|
+
when Hash then or_branch_hash_where(item)
|
|
949
|
+
else
|
|
950
|
+
raise ArgumentError, ":or Array branches may only hold Parse::Constraint objects or constraint hashes, got #{item.class}."
|
|
951
|
+
end
|
|
952
|
+
end
|
|
953
|
+
else
|
|
954
|
+
raise ArgumentError, ":or branches must be constraint hashes or Parse::Query objects, got #{branch.class}."
|
|
955
|
+
end
|
|
956
|
+
end
|
|
957
|
+
if wheres.empty?
|
|
958
|
+
# `$or: []` is rejected by MongoDB. Keep the "no rows" meaning.
|
|
959
|
+
add_constraint(:objectId.in, [])
|
|
960
|
+
return
|
|
961
|
+
end
|
|
962
|
+
# Refuse pipeline-only constraints in any branch before deciding
|
|
963
|
+
# whether the group is match-all.
|
|
964
|
+
compiled = wheres.map { |w| Parse::Query.compile_or_branch(w) }
|
|
965
|
+
return if wheres.any? { |w| Parse::Query.match_all?(w) }
|
|
966
|
+
add_constraint(Parse::Constraint::CompoundQueryConstraint.new(:or, compiled), nil, filter: false)
|
|
967
|
+
end
|
|
968
|
+
private :add_or_branches_unguarded
|
|
969
|
+
|
|
970
|
+
# @!visibility private
|
|
971
|
+
# Query options that shape a whole query and mean nothing inside one OR
|
|
972
|
+
# branch.
|
|
973
|
+
OR_BRANCH_OPTION_KEYS = %i[order keys key skip limit include includes cache read_preference].freeze
|
|
974
|
+
|
|
975
|
+
# @!visibility private
|
|
976
|
+
# The constraints of a Hash OR branch. The branch is built as its own
|
|
977
|
+
# query so a `session:` / `use_master_key:` key or a nested `:or` with a
|
|
978
|
+
# scoped query is applied there, and its authority then has to agree
|
|
979
|
+
# with this query's (adopted when this query has none, refused when it
|
|
980
|
+
# differs). Query-shaping options inside a branch are refused.
|
|
981
|
+
# @param hash [Hash]
|
|
982
|
+
# @return [Array<Parse::Constraint>]
|
|
983
|
+
def or_branch_hash_where(hash)
|
|
984
|
+
options = hash.keys.select { |k| k.is_a?(Symbol) && OR_BRANCH_OPTION_KEYS.include?(k) }
|
|
985
|
+
if options.any?
|
|
986
|
+
raise ArgumentError,
|
|
987
|
+
"#{options.map(&:inspect).join(", ")} cannot be set inside an OR branch; " \
|
|
988
|
+
"set it on the query itself."
|
|
989
|
+
end
|
|
990
|
+
branch = Parse::Query.new(@table, hash)
|
|
991
|
+
merge_or_scope!(branch)
|
|
992
|
+
branch.where
|
|
993
|
+
end
|
|
994
|
+
private :or_branch_hash_where
|
|
995
|
+
|
|
996
|
+
# @!visibility private
|
|
997
|
+
# Run an OR or subquery merge so that a raise leaves this query exactly
|
|
998
|
+
# as it was: constraints, authority, client, and the pinned authority.
|
|
999
|
+
def with_or_rollback
|
|
1000
|
+
# Captured before the guarded block: a failure here changes nothing,
|
|
1001
|
+
# so there is nothing to restore.
|
|
1002
|
+
ivars = OR_SCOPE_STATE_IVARS + %i[@_or_branch_scope @_or_branch_app @read_preference]
|
|
1003
|
+
saved = ivars.to_h { |iv| [iv, [instance_variable_defined?(iv), instance_variable_get(iv)]] }
|
|
1004
|
+
saved_where = @where.dup
|
|
1005
|
+
begin
|
|
1006
|
+
yield
|
|
1007
|
+
rescue StandardError
|
|
1008
|
+
saved.each do |iv, (defined, value)|
|
|
1009
|
+
if defined
|
|
1010
|
+
instance_variable_set(iv, value)
|
|
1011
|
+
elsif instance_variable_defined?(iv)
|
|
1012
|
+
remove_instance_variable(iv)
|
|
1013
|
+
end
|
|
1014
|
+
end
|
|
1015
|
+
@where = saved_where
|
|
1016
|
+
raise
|
|
1017
|
+
end
|
|
1018
|
+
end
|
|
1019
|
+
private :with_or_rollback
|
|
1020
|
+
|
|
845
1021
|
alias_method :query, :conditions
|
|
846
1022
|
alias_method :append, :conditions
|
|
847
1023
|
|
|
@@ -854,11 +1030,16 @@ module Parse
|
|
|
854
1030
|
value = value.session_token
|
|
855
1031
|
end
|
|
856
1032
|
|
|
857
|
-
|
|
858
|
-
@session_token = value
|
|
859
|
-
else
|
|
1033
|
+
unless value.nil? || (value.is_a?(String) && value.present?)
|
|
860
1034
|
raise ArgumentError, "Invalid session token passed to query."
|
|
861
1035
|
end
|
|
1036
|
+
guard_or_scope_change { @session_token = value }
|
|
1037
|
+
end
|
|
1038
|
+
|
|
1039
|
+
# @param value [Boolean, nil] whether to send the master key (nil
|
|
1040
|
+
# leaves the decision to the request layer).
|
|
1041
|
+
def use_master_key=(value)
|
|
1042
|
+
guard_or_scope_change { @use_master_key = value }
|
|
862
1043
|
end
|
|
863
1044
|
|
|
864
1045
|
# returns the query clause for the particular clause
|
|
@@ -1008,11 +1189,16 @@ module Parse
|
|
|
1008
1189
|
case entry
|
|
1009
1190
|
when Order
|
|
1010
1191
|
entry.field = Query.format_field(entry.field)
|
|
1011
|
-
|
|
1012
|
-
when
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1192
|
+
push_order(entry)
|
|
1193
|
+
when String
|
|
1194
|
+
if entry.include?(",")
|
|
1195
|
+
# The REST sort string lists several fields: "title,-plays".
|
|
1196
|
+
order(*entry.split(",").map(&:strip).reject(&:empty?))
|
|
1197
|
+
next
|
|
1198
|
+
end
|
|
1199
|
+
push_order(parse_order_string(entry))
|
|
1200
|
+
when Symbol
|
|
1201
|
+
push_order(parse_order_string(entry))
|
|
1016
1202
|
when Hash
|
|
1017
1203
|
entry.each do |field, direction|
|
|
1018
1204
|
dir_sym = direction.is_a?(String) ? direction.downcase.to_sym : direction
|
|
@@ -1023,7 +1209,7 @@ module Parse
|
|
|
1023
1209
|
end
|
|
1024
1210
|
o = Order.new(field, dir_sym)
|
|
1025
1211
|
o.field = Query.format_field(o.field)
|
|
1026
|
-
|
|
1212
|
+
push_order(o)
|
|
1027
1213
|
end
|
|
1028
1214
|
else
|
|
1029
1215
|
raise ArgumentError,
|
|
@@ -1036,6 +1222,38 @@ module Parse
|
|
|
1036
1222
|
self #chaining
|
|
1037
1223
|
end #order
|
|
1038
1224
|
|
|
1225
|
+
# @!visibility private
|
|
1226
|
+
# One field of the REST sort-string form: "-title" is descending and
|
|
1227
|
+
# "+title" ascending. Parsing the sign here keeps a repeated field
|
|
1228
|
+
# deduplicated and gives the direct `$sort` stage a real field name.
|
|
1229
|
+
# @return [Parse::Order]
|
|
1230
|
+
def parse_order_string(entry)
|
|
1231
|
+
name = entry.to_s
|
|
1232
|
+
o = if name.start_with?("-") && name.length > 1
|
|
1233
|
+
Order.new(name[1..], :desc)
|
|
1234
|
+
elsif name.start_with?("+") && name.length > 1
|
|
1235
|
+
Order.new(name[1..], :asc)
|
|
1236
|
+
else
|
|
1237
|
+
Order.new(entry)
|
|
1238
|
+
end
|
|
1239
|
+
o.field = Query.format_field(o.field)
|
|
1240
|
+
o
|
|
1241
|
+
end
|
|
1242
|
+
private :parse_order_string
|
|
1243
|
+
|
|
1244
|
+
# @!visibility private
|
|
1245
|
+
# Add one sort key. A field that is already in the order keeps its
|
|
1246
|
+
# position and takes the new direction, so it is sent once. Parse Server
|
|
1247
|
+
# and MongoDB both resolve a repeated sort key the same way.
|
|
1248
|
+
def push_order(order)
|
|
1249
|
+
index = @order.index { |existing| existing.field.to_s == order.field.to_s }
|
|
1250
|
+
if index
|
|
1251
|
+
@order[index] = order
|
|
1252
|
+
else
|
|
1253
|
+
@order.push order
|
|
1254
|
+
end
|
|
1255
|
+
end
|
|
1256
|
+
|
|
1039
1257
|
# Use with limit to paginate through results. Default is 0.
|
|
1040
1258
|
# @example
|
|
1041
1259
|
# # get the next 3 songs after the first 10
|
|
@@ -1184,6 +1402,7 @@ module Parse
|
|
|
1184
1402
|
# @return [self]
|
|
1185
1403
|
def add_constraints(list)
|
|
1186
1404
|
list = Array.wrap(list).select { |m| m.is_a?(Parse::Constraint) }
|
|
1405
|
+
list.each { |c| merge_subquery_scopes!(c.value) }
|
|
1187
1406
|
@where = @where + list
|
|
1188
1407
|
self
|
|
1189
1408
|
end
|
|
@@ -1222,11 +1441,31 @@ module Parse
|
|
|
1222
1441
|
constraint.operand = Query.format_field(constraint.operand)
|
|
1223
1442
|
end
|
|
1224
1443
|
reject_vector_constraint!(constraint)
|
|
1444
|
+
merge_subquery_scopes!(constraint.value)
|
|
1225
1445
|
@where.push constraint
|
|
1226
1446
|
@results = nil
|
|
1227
1447
|
self #chaining
|
|
1228
1448
|
end
|
|
1229
1449
|
|
|
1450
|
+
# @!visibility private
|
|
1451
|
+
# Parse Server runs a subquery (`$inQuery`, `$notInQuery`, `$select`,
|
|
1452
|
+
# `$dontSelect`) under the outer request's authority, so a subquery's
|
|
1453
|
+
# own session or scope has to agree with this query's: it is adopted
|
|
1454
|
+
# when this query has none and refused when it differs, the same rule
|
|
1455
|
+
# as an OR branch.
|
|
1456
|
+
def merge_subquery_scopes!(value)
|
|
1457
|
+
subqueries = case value
|
|
1458
|
+
when Parse::Query then [value]
|
|
1459
|
+
when Hash
|
|
1460
|
+
q = value[:query] || value["query"]
|
|
1461
|
+
q.is_a?(Parse::Query) ? [q] : []
|
|
1462
|
+
else []
|
|
1463
|
+
end
|
|
1464
|
+
return if subqueries.empty?
|
|
1465
|
+
with_or_rollback { subqueries.each { |q| merge_or_scope!(q) } }
|
|
1466
|
+
end
|
|
1467
|
+
private :merge_subquery_scopes!
|
|
1468
|
+
|
|
1230
1469
|
# @!visibility private
|
|
1231
1470
|
# Raise {Parse::VectorSearch::ConstraintNotSupported} when a
|
|
1232
1471
|
# constraint targets a declared `:vector` property with an operator
|
|
@@ -1325,8 +1564,22 @@ module Parse
|
|
|
1325
1564
|
# @param where_clauses [Array<Parse::Constraint>] a list of Parse::Constraint objects to combine.
|
|
1326
1565
|
# @return [Query] the combined query with an OR clause.
|
|
1327
1566
|
def or_where(where_clauses = [])
|
|
1328
|
-
|
|
1329
|
-
|
|
1567
|
+
with_or_rollback { or_where_unguarded(where_clauses) }
|
|
1568
|
+
end
|
|
1569
|
+
|
|
1570
|
+
# @!visibility private
|
|
1571
|
+
def or_where_unguarded(where_clauses)
|
|
1572
|
+
if where_clauses.is_a?(Parse::Query)
|
|
1573
|
+
unless where_clauses.table == @table
|
|
1574
|
+
raise ArgumentError, "Parse queries must be of the same class #{@table}."
|
|
1575
|
+
end
|
|
1576
|
+
merge_or_scope!(where_clauses)
|
|
1577
|
+
# A query with no constraints is a branch that matches every row, so
|
|
1578
|
+
# the whole OR does too.
|
|
1579
|
+
return match_all! if Parse::Query.match_all?(where_clauses.where)
|
|
1580
|
+
where_clauses = where_clauses.where
|
|
1581
|
+
end
|
|
1582
|
+
where_clauses = or_branch_hash_where(where_clauses) if where_clauses.is_a?(Hash)
|
|
1330
1583
|
return self if where_clauses.blank?
|
|
1331
1584
|
# Reuse the existing OR only when it is the query's sole constraint.
|
|
1332
1585
|
# When other constraints sit beside it, the current where is
|
|
@@ -1339,13 +1592,13 @@ module Parse
|
|
|
1339
1592
|
# if we don't have a OR clause to reuse, then create a new one with then
|
|
1340
1593
|
# current set of constraints
|
|
1341
1594
|
if compound.blank?
|
|
1342
|
-
initial_constraints = Parse::Query.
|
|
1595
|
+
initial_constraints = Parse::Query.compile_or_branch(@where)
|
|
1343
1596
|
# Only include initial constraints if they're not empty
|
|
1344
1597
|
initial_values = initial_constraints.empty? ? [] : [initial_constraints]
|
|
1345
1598
|
compound = Parse::Constraint::CompoundQueryConstraint.new :or, initial_values
|
|
1346
1599
|
end
|
|
1347
1600
|
# then take the where clauses from the second query and append them.
|
|
1348
|
-
new_constraints = Parse::Query.
|
|
1601
|
+
new_constraints = Parse::Query.compile_or_branch(where_clauses)
|
|
1349
1602
|
# Only add new constraints if they're not empty
|
|
1350
1603
|
unless new_constraints.empty?
|
|
1351
1604
|
compound.value.push new_constraints
|
|
@@ -1354,16 +1607,330 @@ module Parse
|
|
|
1354
1607
|
@where = [compound]
|
|
1355
1608
|
self #chaining
|
|
1356
1609
|
end
|
|
1610
|
+
private :or_where_unguarded
|
|
1357
1611
|
|
|
1612
|
+
# Combine two queries with OR. The result is a copy of the receiver
|
|
1613
|
+
# (its limit, order, keys and includes) whose where is the OR of both.
|
|
1614
|
+
#
|
|
1615
|
+
# * A receiver with no constraints is a starting point, the same as
|
|
1616
|
+
# {#or_where}: `Model.query | q` gives an OR whose only branch is `q`,
|
|
1617
|
+
# so `[q1, q2].reduce(Model.query, :|)` gives `q1 OR q2`.
|
|
1618
|
+
# * A right-hand side with no constraints is a branch that matches
|
|
1619
|
+
# every row, so `q | Model.query` has no constraints.
|
|
1620
|
+
#
|
|
1621
|
+
# The auth scope (session token, master-key flag, `scope_to_user`,
|
|
1622
|
+
# `scope_to_role`) carries over from whichever side set it. Two
|
|
1623
|
+
# different scopes raise `ArgumentError`, since the OR could only run
|
|
1624
|
+
# under one of them.
|
|
1358
1625
|
# @see #or_where
|
|
1359
1626
|
# @return [Query] the combined query with an OR clause.
|
|
1627
|
+
# @raise [ArgumentError] when the classes or auth scopes differ, or a
|
|
1628
|
+
# branch holds a pipeline-only constraint.
|
|
1360
1629
|
def |(other_query)
|
|
1361
1630
|
raise ArgumentError, "Parse queries must be of the same class #{@table}." unless @table == other_query.table
|
|
1362
1631
|
copy_query = self.clone
|
|
1363
|
-
copy_query.
|
|
1632
|
+
copy_query.send(:merge_or_scope!, other_query)
|
|
1633
|
+
if Parse::Query.match_all?(other_query.where)
|
|
1634
|
+
# Still refuse a pipeline-only receiver rather than silently
|
|
1635
|
+
# widening it.
|
|
1636
|
+
Parse::Query.compile_or_branch(@where)
|
|
1637
|
+
copy_query.match_all!
|
|
1638
|
+
else
|
|
1639
|
+
copy_query.or_where other_query.where
|
|
1640
|
+
end
|
|
1364
1641
|
copy_query
|
|
1365
1642
|
end
|
|
1366
1643
|
|
|
1644
|
+
# @!visibility private
|
|
1645
|
+
# Auth scope ivars an OR must agree on.
|
|
1646
|
+
OR_SCOPE_IVARS = %i[session_token use_master_key acl_user acl_role].freeze
|
|
1647
|
+
|
|
1648
|
+
# @!visibility private
|
|
1649
|
+
# The authority this query runs under, as one comparable Hash: the
|
|
1650
|
+
# session token, the scoped user id, the scoped role name, and an
|
|
1651
|
+
# explicit master-key flag. An explicit `use_master_key = false` beside
|
|
1652
|
+
# a session, user, or role adds nothing, since those already run without
|
|
1653
|
+
# the master key. An empty Hash is the default authority.
|
|
1654
|
+
# @return [Hash]
|
|
1655
|
+
def or_effective_scope
|
|
1656
|
+
scope = {}
|
|
1657
|
+
scope[:session] = @session_token unless @session_token.nil?
|
|
1658
|
+
# A client from Parse::Client#become sends its bound session on every
|
|
1659
|
+
# request that names no token and does not ask for the master key,
|
|
1660
|
+
# so it counts as that session. A non-default client with neither a
|
|
1661
|
+
# session nor a master key runs without the master key.
|
|
1662
|
+
own_client = or_explicit_client
|
|
1663
|
+
if own_client && @session_token.nil? && @use_master_key != true
|
|
1664
|
+
bound = own_client.respond_to?(:session_token) ? own_client.session_token : nil
|
|
1665
|
+
if bound.is_a?(String) && !bound.strip.empty?
|
|
1666
|
+
scope[:session] = bound
|
|
1667
|
+
elsif !(own_client.respond_to?(:master_key) && own_client.master_key.present?)
|
|
1668
|
+
scope[:master] = false
|
|
1669
|
+
end
|
|
1670
|
+
end
|
|
1671
|
+
scope[:user] = Parse::Query.or_scope_value(:acl_user, @acl_user) unless @acl_user.nil?
|
|
1672
|
+
scope[:role] = Parse::Query.or_scope_value(:acl_role, @acl_role) unless @acl_role.nil?
|
|
1673
|
+
unless @use_master_key.nil? || (@use_master_key == false && !scope.empty?)
|
|
1674
|
+
scope[:master] = @use_master_key
|
|
1675
|
+
end
|
|
1676
|
+
scope.delete(:master) if scope[:master] == false && scope.size > 1
|
|
1677
|
+
scope
|
|
1678
|
+
end
|
|
1679
|
+
protected :or_effective_scope
|
|
1680
|
+
|
|
1681
|
+
# @!visibility private
|
|
1682
|
+
# The client set on this query when it is not the default client, or
|
|
1683
|
+
# nil. Read from the instance variable so it never memoizes the default.
|
|
1684
|
+
# @return [Parse::Client, nil]
|
|
1685
|
+
def or_explicit_client
|
|
1686
|
+
own = @client
|
|
1687
|
+
return nil if own.nil?
|
|
1688
|
+
default = begin
|
|
1689
|
+
self.class.client
|
|
1690
|
+
rescue StandardError
|
|
1691
|
+
nil
|
|
1692
|
+
end
|
|
1693
|
+
return nil if default && own.equal?(default)
|
|
1694
|
+
own
|
|
1695
|
+
end
|
|
1696
|
+
protected :or_explicit_client
|
|
1697
|
+
|
|
1698
|
+
# @!visibility private
|
|
1699
|
+
# Instance variables that make up a query's authority.
|
|
1700
|
+
OR_SCOPE_STATE_IVARS = %i[@session_token @use_master_key @acl_user @acl_role @client].freeze
|
|
1701
|
+
|
|
1702
|
+
# @!visibility private
|
|
1703
|
+
# Run an assignment that may change this query's authority. On a query
|
|
1704
|
+
# combined with OR, a change that leaves the pinned authority raises
|
|
1705
|
+
# and the previous values are restored, so a rescued error never
|
|
1706
|
+
# leaves the query running under a different principal.
|
|
1707
|
+
def guard_or_scope_change
|
|
1708
|
+
return yield if @_or_branch_scope.nil?
|
|
1709
|
+
saved = OR_SCOPE_STATE_IVARS.to_h { |iv| [iv, instance_variable_get(iv)] }
|
|
1710
|
+
begin
|
|
1711
|
+
yield
|
|
1712
|
+
enforce_or_branch_scope!
|
|
1713
|
+
rescue StandardError
|
|
1714
|
+
saved.each { |iv, v| instance_variable_set(iv, v) }
|
|
1715
|
+
raise
|
|
1716
|
+
end
|
|
1717
|
+
end
|
|
1718
|
+
private :guard_or_scope_change
|
|
1719
|
+
|
|
1720
|
+
# @!visibility private
|
|
1721
|
+
# Combine `other`'s authority with this query's for an OR. Both must
|
|
1722
|
+
# run under the same authority: a query with the default authority
|
|
1723
|
+
# adopts the other's, and two different authorities (a session and a
|
|
1724
|
+
# scoped user, a session and the master key, two users) raise. The
|
|
1725
|
+
# resulting authority is then pinned whichever side carried it, so a
|
|
1726
|
+
# later change to this query's
|
|
1727
|
+
# session, master-key flag, or scope raises instead of running the
|
|
1728
|
+
# other query's rows under a different principal. Read preference is
|
|
1729
|
+
# carried over but a mismatch keeps this query's value.
|
|
1730
|
+
# @param other [Parse::Query]
|
|
1731
|
+
def merge_or_scope!(other)
|
|
1732
|
+
merge_or_application!(other)
|
|
1733
|
+
theirs = other.or_effective_scope
|
|
1734
|
+
if theirs.empty?
|
|
1735
|
+
# The other query has the default authority: pin this query's own
|
|
1736
|
+
# authority, so the result does not depend on operand order.
|
|
1737
|
+
mine = or_effective_scope
|
|
1738
|
+
unless mine.empty?
|
|
1739
|
+
@_or_branch_scope = mine
|
|
1740
|
+
@_or_branch_app = or_application_identity
|
|
1741
|
+
end
|
|
1742
|
+
else
|
|
1743
|
+
mine = or_effective_scope
|
|
1744
|
+
if mine.empty?
|
|
1745
|
+
OR_SCOPE_IVARS.each do |ivar|
|
|
1746
|
+
instance_variable_set(:"@#{ivar}", other.instance_variable_get(:"@#{ivar}"))
|
|
1747
|
+
end
|
|
1748
|
+
adopt_or_client!(other, theirs)
|
|
1749
|
+
# Fail closed if the adopted state still does not reproduce the
|
|
1750
|
+
# other query's authority.
|
|
1751
|
+
unless or_effective_scope == theirs
|
|
1752
|
+
raise ArgumentError,
|
|
1753
|
+
"Cannot combine with OR a query running under #{Parse::Query.describe_or_scope(theirs)} " \
|
|
1754
|
+
"through its own client. Run the queries separately."
|
|
1755
|
+
end
|
|
1756
|
+
elsif mine != theirs
|
|
1757
|
+
raise ArgumentError,
|
|
1758
|
+
"Queries combined with OR must run under the same authority " \
|
|
1759
|
+
"(#{Parse::Query.describe_or_scope(mine)} vs #{Parse::Query.describe_or_scope(theirs)}). " \
|
|
1760
|
+
"Run them separately, or set the same scope on each."
|
|
1761
|
+
end
|
|
1762
|
+
@_or_branch_scope = theirs
|
|
1763
|
+
@_or_branch_app = or_application_identity
|
|
1764
|
+
end
|
|
1765
|
+
@read_preference ||= other.read_preference
|
|
1766
|
+
self
|
|
1767
|
+
end
|
|
1768
|
+
protected :merge_or_scope!
|
|
1769
|
+
|
|
1770
|
+
# @!visibility private
|
|
1771
|
+
# The client set on this query when it is a real Parse client other
|
|
1772
|
+
# than the default one, or nil.
|
|
1773
|
+
# @return [Parse::Client, nil]
|
|
1774
|
+
def or_application_client
|
|
1775
|
+
own = or_explicit_client
|
|
1776
|
+
return nil unless own.respond_to?(:application_id) && own.respond_to?(:server_url)
|
|
1777
|
+
own
|
|
1778
|
+
end
|
|
1779
|
+
protected :or_application_client
|
|
1780
|
+
|
|
1781
|
+
# @!visibility private
|
|
1782
|
+
# The application this query runs against, as [application id, server
|
|
1783
|
+
# URL], from its own client or the default client. nil when no client
|
|
1784
|
+
# can be resolved.
|
|
1785
|
+
# @return [Array<String>, nil]
|
|
1786
|
+
def or_application_identity
|
|
1787
|
+
target = or_application_client || begin
|
|
1788
|
+
self.class.client
|
|
1789
|
+
rescue StandardError
|
|
1790
|
+
nil
|
|
1791
|
+
end
|
|
1792
|
+
return nil unless target.respond_to?(:application_id) && target.respond_to?(:server_url)
|
|
1793
|
+
[target.application_id.to_s, target.server_url.to_s.chomp("/")]
|
|
1794
|
+
end
|
|
1795
|
+
protected :or_application_identity
|
|
1796
|
+
|
|
1797
|
+
# @!visibility private
|
|
1798
|
+
# Queries combined with OR must target the same application, so a
|
|
1799
|
+
# session token or master-key authority from one app is never sent to
|
|
1800
|
+
# another. An empty receiver on the default client (no constraints, no
|
|
1801
|
+
# authority) takes the other query's client, as it takes its authority;
|
|
1802
|
+
# any other mismatch raises.
|
|
1803
|
+
def merge_or_application!(other)
|
|
1804
|
+
mine = or_application_identity
|
|
1805
|
+
theirs = other.or_application_identity
|
|
1806
|
+
return if mine.nil? || theirs.nil? || mine == theirs
|
|
1807
|
+
their_client = other.or_application_client
|
|
1808
|
+
if their_client && or_application_client.nil? && @_or_branch_scope.nil? &&
|
|
1809
|
+
or_effective_scope.empty? && Parse::Query.match_all?(@where)
|
|
1810
|
+
@client = their_client
|
|
1811
|
+
return
|
|
1812
|
+
end
|
|
1813
|
+
raise ArgumentError,
|
|
1814
|
+
"Queries combined with OR must run against the same Parse application " \
|
|
1815
|
+
"(#{mine.first} at #{mine.last} vs #{theirs.first} at #{theirs.last}). " \
|
|
1816
|
+
"Run them separately on their own clients."
|
|
1817
|
+
end
|
|
1818
|
+
private :merge_or_application!
|
|
1819
|
+
|
|
1820
|
+
# @!visibility private
|
|
1821
|
+
# Carry `other`'s client-bound authority onto this query. A query with
|
|
1822
|
+
# no client of its own takes `other`'s client, which keeps its server
|
|
1823
|
+
# and session. A query with its own client takes the derived session
|
|
1824
|
+
# token (or the no-master-key flag) explicitly instead.
|
|
1825
|
+
def adopt_or_client!(other, theirs)
|
|
1826
|
+
their_client = other.or_explicit_client
|
|
1827
|
+
return if their_client.nil?
|
|
1828
|
+
if or_explicit_client.nil?
|
|
1829
|
+
@client = their_client
|
|
1830
|
+
elsif other.session_token.nil? && theirs.key?(:session)
|
|
1831
|
+
@session_token = theirs[:session]
|
|
1832
|
+
elsif theirs[:master] == false
|
|
1833
|
+
@use_master_key = false
|
|
1834
|
+
end
|
|
1835
|
+
end
|
|
1836
|
+
private :adopt_or_client!
|
|
1837
|
+
|
|
1838
|
+
# @!visibility private
|
|
1839
|
+
# Raise when this query's authority no longer matches the one an OR
|
|
1840
|
+
# branch was combined under.
|
|
1841
|
+
def enforce_or_branch_scope!
|
|
1842
|
+
return if @_or_branch_scope.nil?
|
|
1843
|
+
if @_or_branch_app && (app = or_application_identity) && app != @_or_branch_app
|
|
1844
|
+
raise ArgumentError,
|
|
1845
|
+
"This query was combined with OR against Parse application #{@_or_branch_app.first}; " \
|
|
1846
|
+
"running it against #{app.first} would send its authority to another application. " \
|
|
1847
|
+
"Build a new query instead."
|
|
1848
|
+
end
|
|
1849
|
+
current = or_effective_scope
|
|
1850
|
+
return if current == @_or_branch_scope
|
|
1851
|
+
raise ArgumentError,
|
|
1852
|
+
"This query was combined with OR under #{Parse::Query.describe_or_scope(@_or_branch_scope)}; " \
|
|
1853
|
+
"changing it to #{Parse::Query.describe_or_scope(current)} would run the combined " \
|
|
1854
|
+
"branches under a different authority. Build a new query instead."
|
|
1855
|
+
end
|
|
1856
|
+
private :enforce_or_branch_scope!
|
|
1857
|
+
|
|
1858
|
+
# @!visibility private
|
|
1859
|
+
# On a query combined with OR, auth kwargs passed straight to a direct
|
|
1860
|
+
# or Atlas terminal (`results_direct(master: true)`) must name the same
|
|
1861
|
+
# authority the query was pinned to, and an explicit client the same
|
|
1862
|
+
# application. A narrower `master: false` is allowed.
|
|
1863
|
+
def enforce_explicit_auth_against_or_scope!(session_token: nil, master: nil, acl_user: nil, acl_role: nil, client: nil)
|
|
1864
|
+
return if @_or_branch_scope.nil?
|
|
1865
|
+
if client && @_or_branch_app && client.respond_to?(:application_id) && client.respond_to?(:server_url)
|
|
1866
|
+
app = [client.application_id.to_s, client.server_url.to_s.chomp("/")]
|
|
1867
|
+
unless app == @_or_branch_app
|
|
1868
|
+
raise ArgumentError,
|
|
1869
|
+
"This query was combined with OR against Parse application #{@_or_branch_app.first}; " \
|
|
1870
|
+
"the client: passed here targets #{app.first}."
|
|
1871
|
+
end
|
|
1872
|
+
end
|
|
1873
|
+
given = {}
|
|
1874
|
+
token = session_token.respond_to?(:session_token) ? session_token.session_token : session_token
|
|
1875
|
+
given[:session] = token unless token.nil?
|
|
1876
|
+
given[:user] = Parse::Query.or_scope_value(:acl_user, acl_user) unless acl_user.nil?
|
|
1877
|
+
given[:role] = Parse::Query.or_scope_value(:acl_role, acl_role) unless acl_role.nil?
|
|
1878
|
+
given[:master] = true if master && master != false
|
|
1879
|
+
return if given.empty? || given == @_or_branch_scope
|
|
1880
|
+
raise ArgumentError,
|
|
1881
|
+
"This query was combined with OR under #{Parse::Query.describe_or_scope(@_or_branch_scope)}; " \
|
|
1882
|
+
"the auth passed here (#{Parse::Query.describe_or_scope(given)}) would run it under a " \
|
|
1883
|
+
"different authority."
|
|
1884
|
+
end
|
|
1885
|
+
private :enforce_explicit_auth_against_or_scope!
|
|
1886
|
+
|
|
1887
|
+
# @!visibility private
|
|
1888
|
+
# A redacted, human-readable form of an authority Hash for errors.
|
|
1889
|
+
def self.describe_or_scope(scope)
|
|
1890
|
+
return "the default authority" if scope.empty?
|
|
1891
|
+
scope.map do |kind, value|
|
|
1892
|
+
case kind
|
|
1893
|
+
when :session then "a session token"
|
|
1894
|
+
when :user then "scoped user #{value}"
|
|
1895
|
+
when :role then "scoped role #{value}"
|
|
1896
|
+
when :master then value ? "the master key" : "no master key"
|
|
1897
|
+
end
|
|
1898
|
+
end.join(" + ")
|
|
1899
|
+
end
|
|
1900
|
+
|
|
1901
|
+
# @!visibility private
|
|
1902
|
+
# A comparable form of one auth scope value.
|
|
1903
|
+
def self.or_scope_value(ivar, value)
|
|
1904
|
+
case ivar
|
|
1905
|
+
when :acl_user
|
|
1906
|
+
value.respond_to?(:id) ? value.id : value.to_s
|
|
1907
|
+
when :acl_role
|
|
1908
|
+
name = value.respond_to?(:name) ? value.name : value.to_s
|
|
1909
|
+
name.to_s.delete_prefix("role:")
|
|
1910
|
+
else
|
|
1911
|
+
value
|
|
1912
|
+
end
|
|
1913
|
+
end
|
|
1914
|
+
|
|
1915
|
+
# @!visibility private
|
|
1916
|
+
# Drop every where constraint, so the query matches all rows.
|
|
1917
|
+
# @return [self]
|
|
1918
|
+
def match_all!
|
|
1919
|
+
@where = []
|
|
1920
|
+
@results = nil
|
|
1921
|
+
self
|
|
1922
|
+
end
|
|
1923
|
+
protected :match_all!
|
|
1924
|
+
|
|
1925
|
+
# @!visibility private
|
|
1926
|
+
# Whether a list of constraints places no restriction on the rows (an
|
|
1927
|
+
# empty list, or constraints that compile to nothing).
|
|
1928
|
+
# @param where [Array<Parse::Constraint>]
|
|
1929
|
+
# @return [Boolean]
|
|
1930
|
+
def self.match_all?(where)
|
|
1931
|
+
where.blank? || compile_markers(where).empty?
|
|
1932
|
+
end
|
|
1933
|
+
|
|
1367
1934
|
# Add a "field is NOT between" condition — the logical negation of
|
|
1368
1935
|
# `.where(field.between => value)`: `field < min OR field > max` for a
|
|
1369
1936
|
# fully-bounded Range/Array, or a single one-sided comparison when the
|
|
@@ -1937,6 +2504,7 @@ module Parse
|
|
|
1937
2504
|
|
|
1938
2505
|
# @!visibility private
|
|
1939
2506
|
def _opts
|
|
2507
|
+
enforce_or_branch_scope!
|
|
1940
2508
|
opts = {}
|
|
1941
2509
|
opts[:cache] = self.cache || false
|
|
1942
2510
|
# Only forward `use_master_key` when the caller actually set it.
|
|
@@ -1946,6 +2514,14 @@ module Parse
|
|
|
1946
2514
|
# init-block comment on `@use_master_key`.
|
|
1947
2515
|
opts[:use_master_key] = self.use_master_key unless self.use_master_key.nil?
|
|
1948
2516
|
opts[:session_token] = self.session_token unless self.session_token.nil?
|
|
2517
|
+
# SDK-internal metadata read (the role-graph walk used for scope
|
|
2518
|
+
# enforcement): keeps the master key inside `Parse.without_master_key`.
|
|
2519
|
+
# Set only by the SDK through an instance variable, never a public option.
|
|
2520
|
+
if @_metadata_master == true
|
|
2521
|
+
opts[:use_master_key] = true
|
|
2522
|
+
opts.delete(:session_token)
|
|
2523
|
+
opts[:metadata_master] = Parse::Client::METADATA_MASTER_REQUEST
|
|
2524
|
+
end
|
|
1949
2525
|
# for now, don't cache requests where we disable master_key or provide session token
|
|
1950
2526
|
# if opts[:use_master_key] == false || opts[:session_token].present?
|
|
1951
2527
|
# opts[:cache] = false
|
|
@@ -2291,7 +2867,7 @@ module Parse
|
|
|
2291
2867
|
# @return [self]
|
|
2292
2868
|
def scope_to_user(user)
|
|
2293
2869
|
raise ArgumentError, "[Parse::Query] scope_to_user requires a Parse::User or User Pointer." unless user.respond_to?(:id) && user.id.is_a?(String)
|
|
2294
|
-
@acl_user = user
|
|
2870
|
+
guard_or_scope_change { @acl_user = user }
|
|
2295
2871
|
self
|
|
2296
2872
|
end
|
|
2297
2873
|
|
|
@@ -2331,7 +2907,7 @@ module Parse
|
|
|
2331
2907
|
# Parse::ACLScope#resolve_for_role only ever sees Parse::Role or
|
|
2332
2908
|
# String. Without normalization, any String-only operation
|
|
2333
2909
|
# (e.g. #start_with?, #sub) silently NoMethodErrors on Symbol.
|
|
2334
|
-
@acl_role = role.is_a?(Symbol) ? role.to_s : role
|
|
2910
|
+
guard_or_scope_change { @acl_role = role.is_a?(Symbol) ? role.to_s : role }
|
|
2335
2911
|
self
|
|
2336
2912
|
end
|
|
2337
2913
|
|
|
@@ -2477,6 +3053,10 @@ module Parse
|
|
|
2477
3053
|
|
|
2478
3054
|
# @!visibility private
|
|
2479
3055
|
def mongo_direct_scope_kwargs
|
|
3056
|
+
enforce_or_branch_scope!
|
|
3057
|
+
# SDK-internal metadata read (see `_opts`): master mode on the direct
|
|
3058
|
+
# path too, whatever `Parse.without_master_key` says.
|
|
3059
|
+
return { master: Parse::ACLScope::METADATA_MASTER } if @_metadata_master == true
|
|
2480
3060
|
if @acl_user
|
|
2481
3061
|
# Pre-resolved User pointer. Hand it to Parse::ACLScope as
|
|
2482
3062
|
# acl_user: so the same three-layer simulation runs (top-level
|
|
@@ -2544,9 +3124,10 @@ module Parse
|
|
|
2544
3124
|
# @return [Boolean]
|
|
2545
3125
|
# @!visibility private
|
|
2546
3126
|
def mongo_direct_master_posture?
|
|
2547
|
-
# `Parse.without_master_key`
|
|
2548
|
-
#
|
|
2549
|
-
|
|
3127
|
+
# Inside `Parse.without_master_key` this still answers true: the
|
|
3128
|
+
# resulting `master: true` is dropped by Parse::ACLScope.resolve!,
|
|
3129
|
+
# which runs the read in the public scope (or raises under
|
|
3130
|
+
# `require_session_token`) with the block-specific message.
|
|
2550
3131
|
c = begin
|
|
2551
3132
|
client
|
|
2552
3133
|
rescue StandardError
|
|
@@ -2566,15 +3147,6 @@ module Parse
|
|
|
2566
3147
|
Parse.respond_to?(:master_key_disabled?) && Parse.master_key_disabled?
|
|
2567
3148
|
end
|
|
2568
3149
|
|
|
2569
|
-
# An explicit `master: true` passed to a direct terminal, dropped inside
|
|
2570
|
-
# a `Parse.without_master_key` block. REST strips the master key there
|
|
2571
|
-
# even when a call asks for it, so the direct read falls back to the
|
|
2572
|
-
# public scope as REST would.
|
|
2573
|
-
# @!visibility private
|
|
2574
|
-
def direct_master_kwarg(master)
|
|
2575
|
-
master == true && master_key_suppressed? ? nil : master
|
|
2576
|
-
end
|
|
2577
|
-
|
|
2578
3150
|
# Auth kwargs for the Atlas Search bridge (`#atlas_search` builder
|
|
2579
3151
|
# block). Explicit `atlas_search(...)` auth kwargs win; otherwise
|
|
2580
3152
|
# derive from the query's own scope (`#scope_to_user`, an explicit
|
|
@@ -2606,9 +3178,11 @@ module Parse
|
|
|
2606
3178
|
end
|
|
2607
3179
|
|
|
2608
3180
|
explicit = %i[session_token master acl_user acl_role].select { |k| options.key?(k) }
|
|
2609
|
-
if explicit.any?
|
|
3181
|
+
if explicit.any? || options.key?(:client)
|
|
2610
3182
|
given = explicit.to_h { |k| [k, options[k]] }
|
|
2611
|
-
|
|
3183
|
+
enforce_explicit_auth_against_or_scope!(client: options[:client], **given)
|
|
3184
|
+
end
|
|
3185
|
+
if explicit.any?
|
|
2612
3186
|
return client_kwarg.merge(given)
|
|
2613
3187
|
end
|
|
2614
3188
|
|
|
@@ -2617,6 +3191,7 @@ module Parse
|
|
|
2617
3191
|
|
|
2618
3192
|
# @!visibility private
|
|
2619
3193
|
def atlas_search_scope_kwargs
|
|
3194
|
+
enforce_or_branch_scope!
|
|
2620
3195
|
if @acl_user
|
|
2621
3196
|
{ acl_user: @acl_user }
|
|
2622
3197
|
elsif @acl_role
|
|
@@ -2625,9 +3200,9 @@ module Parse
|
|
|
2625
3200
|
{ session_token: @session_token }
|
|
2626
3201
|
elsif use_master_key == true
|
|
2627
3202
|
# An explicit master request skips the ambient session, as on REST.
|
|
2628
|
-
# Inside `Parse.without_master_key`
|
|
2629
|
-
# search runs in the public scope.
|
|
2630
|
-
|
|
3203
|
+
# Inside `Parse.without_master_key` Parse::AtlasSearch drops it and
|
|
3204
|
+
# the search runs in the public scope.
|
|
3205
|
+
{ master: true }
|
|
2631
3206
|
elsif (ambient = ambient_session_token)
|
|
2632
3207
|
{ session_token: ambient }
|
|
2633
3208
|
elsif anonymous_session_block?
|
|
@@ -2729,6 +3304,8 @@ module Parse
|
|
|
2729
3304
|
# @note This is a read-only operation. Direct MongoDB queries cannot modify data.
|
|
2730
3305
|
# @see Parse::MongoDB.configure
|
|
2731
3306
|
def results_direct(raw: false, max_time_ms: nil, session_token: nil, master: nil, acl_user: nil, acl_role: nil, client: nil, &block)
|
|
3307
|
+
enforce_explicit_auth_against_or_scope!(session_token: session_token, master: master,
|
|
3308
|
+
acl_user: acl_user, acl_role: acl_role, client: client)
|
|
2732
3309
|
# `limit(0)` asks for no rows. MongoDB rejects `$limit: 0`, and
|
|
2733
3310
|
# omitting the stage would return every row.
|
|
2734
3311
|
return [] if @limit == 0
|
|
@@ -2770,7 +3347,6 @@ module Parse
|
|
|
2770
3347
|
acl_user = auth[:acl_user]
|
|
2771
3348
|
acl_role = auth[:acl_role]
|
|
2772
3349
|
end
|
|
2773
|
-
master = direct_master_kwarg(master)
|
|
2774
3350
|
|
|
2775
3351
|
# Execute the aggregation directly on MongoDB. The pipeline was built
|
|
2776
3352
|
# entirely from SDK constraint translation (no user-supplied stages),
|
|
@@ -2878,6 +3454,8 @@ module Parse
|
|
|
2878
3454
|
# @note This is a read-only operation. Direct MongoDB queries cannot modify data.
|
|
2879
3455
|
# @see Parse::MongoDB.configure
|
|
2880
3456
|
def count_direct(session_token: nil, master: nil, acl_user: nil, acl_role: nil, client: nil)
|
|
3457
|
+
enforce_explicit_auth_against_or_scope!(session_token: session_token, master: master,
|
|
3458
|
+
acl_user: acl_user, acl_role: acl_role, client: client)
|
|
2881
3459
|
require_relative "mongodb"
|
|
2882
3460
|
Parse::MongoDB.require_gem!
|
|
2883
3461
|
|
|
@@ -2916,7 +3494,6 @@ module Parse
|
|
|
2916
3494
|
acl_user = auth[:acl_user]
|
|
2917
3495
|
acl_role = auth[:acl_role]
|
|
2918
3496
|
end
|
|
2919
|
-
master = direct_master_kwarg(master)
|
|
2920
3497
|
|
|
2921
3498
|
# SDK-built pipeline only — see results_direct for rationale.
|
|
2922
3499
|
# ACL simulation runs inside Parse::MongoDB.aggregate when
|
|
@@ -2957,6 +3534,8 @@ module Parse
|
|
|
2957
3534
|
def distinct_direct(field, return_pointers: false, order: nil,
|
|
2958
3535
|
session_token: nil, master: nil, acl_user: nil, acl_role: nil,
|
|
2959
3536
|
client: nil)
|
|
3537
|
+
enforce_explicit_auth_against_or_scope!(session_token: session_token, master: master,
|
|
3538
|
+
acl_user: acl_user, acl_role: acl_role, client: client)
|
|
2960
3539
|
require_relative "mongodb"
|
|
2961
3540
|
Parse::MongoDB.require_gem!
|
|
2962
3541
|
|
|
@@ -3017,7 +3596,6 @@ module Parse
|
|
|
3017
3596
|
acl_user = auth[:acl_user]
|
|
3018
3597
|
acl_role = auth[:acl_role]
|
|
3019
3598
|
end
|
|
3020
|
-
master = direct_master_kwarg(master)
|
|
3021
3599
|
raw_results = Parse::MongoDB.aggregate(@table, pipeline,
|
|
3022
3600
|
allow_internal_fields: true,
|
|
3023
3601
|
read_preference: @read_preference,
|
|
@@ -4255,11 +4833,11 @@ module Parse
|
|
|
4255
4833
|
lq_client = client || Parse::LiveQuery.client
|
|
4256
4834
|
lq_client.subscribe(
|
|
4257
4835
|
@table,
|
|
4258
|
-
where:
|
|
4836
|
+
where: compile_rest_where,
|
|
4259
4837
|
fields: fields,
|
|
4260
4838
|
keys: keys,
|
|
4261
4839
|
watch: watch,
|
|
4262
|
-
session_token: session_token
|
|
4840
|
+
session_token: live_query_session_token(session_token, live_query_client: lq_client),
|
|
4263
4841
|
use_master_key: use_master_key,
|
|
4264
4842
|
&block
|
|
4265
4843
|
)
|
|
@@ -5145,7 +5723,12 @@ module Parse
|
|
|
5145
5723
|
# Keys like ["project.name", "project.status"] define which subfields to fetch on nested objects
|
|
5146
5724
|
nested_keys = Parse::Query.parse_keys_to_nested_keys(@keys) if @keys.present?
|
|
5147
5725
|
|
|
5148
|
-
list.map { |m| Parse::Object.build(m, @table, fetched_keys: fetch_keys, nested_fetched_keys: nested_keys) }.compact
|
|
5726
|
+
built = list.map { |m| Parse::Object.build(m, @table, fetched_keys: fetch_keys, nested_fetched_keys: nested_keys) }.compact
|
|
5727
|
+
# Record session owners against the client that fetched them, so a
|
|
5728
|
+
# later delete of an already-gone session can still drop its owner's
|
|
5729
|
+
# cached identities.
|
|
5730
|
+
Parse::Session._remember_owners!(built, client) if @table == Parse::Model::CLASS_SESSION && defined?(Parse::Session)
|
|
5731
|
+
built
|
|
5149
5732
|
end
|
|
5150
5733
|
|
|
5151
5734
|
# Validates includes against keys and field types, printing debug warnings for:
|
|
@@ -5295,7 +5878,7 @@ module Parse
|
|
|
5295
5878
|
q[:excludeKeys] = @exclude_keys.join(",") if encode && @exclude_keys&.any?
|
|
5296
5879
|
q[:order] = @order.join(",") unless @order.empty?
|
|
5297
5880
|
unless @where.empty?
|
|
5298
|
-
q[:where] = Parse::Query.compile_where(@where)
|
|
5881
|
+
q[:where] = coerce_rest_pointer_ids(Parse::Query.compile_where(@where))
|
|
5299
5882
|
q[:where] = q[:where].to_json if encode
|
|
5300
5883
|
end
|
|
5301
5884
|
|
|
@@ -5328,6 +5911,94 @@ module Parse
|
|
|
5328
5911
|
self.class.compile_where(@where || [])
|
|
5329
5912
|
end
|
|
5330
5913
|
|
|
5914
|
+
# @return [Hash] the where clause as sent to Parse Server over REST or
|
|
5915
|
+
# LiveQuery: {#compile_where} with bare objectIds on declared pointer
|
|
5916
|
+
# fields rewritten into Pointer hashes, so they match the stored
|
|
5917
|
+
# pointer.
|
|
5918
|
+
def compile_rest_where
|
|
5919
|
+
refuse_pipeline_only!("a LiveQuery subscription or push target")
|
|
5920
|
+
coerce_rest_pointer_ids(compile_where)
|
|
5921
|
+
end
|
|
5922
|
+
|
|
5923
|
+
# @!visibility private
|
|
5924
|
+
# Compile this query as a subquery of another (`$inQuery`,
|
|
5925
|
+
# `$notInQuery`, `$select`, `$dontSelect`). Parse Server evaluates it
|
|
5926
|
+
# from the REST form, so a pipeline-only constraint, which that form
|
|
5927
|
+
# drops, is refused rather than turning the subquery into match-all.
|
|
5928
|
+
# @return [Hash]
|
|
5929
|
+
def compile_subquery
|
|
5930
|
+
refuse_pipeline_only!("a subquery")
|
|
5931
|
+
enforce_or_branch_scope!
|
|
5932
|
+
compile(encode: false, includeClassName: true)
|
|
5933
|
+
end
|
|
5934
|
+
|
|
5935
|
+
# @!visibility private
|
|
5936
|
+
# Raise when this query holds a constraint that only the aggregation
|
|
5937
|
+
# pipeline or mongo-direct can run, for a target that receives only
|
|
5938
|
+
# the REST where form.
|
|
5939
|
+
def refuse_pipeline_only!(target)
|
|
5940
|
+
markers = compile_markers
|
|
5941
|
+
return unless Parse::Query.or_branch_marker?(markers)
|
|
5942
|
+
names = Parse::Query.pipeline_only_constraint_names(@where)
|
|
5943
|
+
raise ArgumentError,
|
|
5944
|
+
"This query cannot be used as #{target}: " \
|
|
5945
|
+
"#{names.empty? ? "a pipeline-only constraint" : names.join(", ")} needs the aggregation " \
|
|
5946
|
+
"pipeline or mongo-direct, and Parse Server receives only the REST where form, " \
|
|
5947
|
+
"which would drop it."
|
|
5948
|
+
end
|
|
5949
|
+
private :refuse_pipeline_only!
|
|
5950
|
+
|
|
5951
|
+
# @!visibility private
|
|
5952
|
+
# The session a LiveQuery subscription built from this query must use:
|
|
5953
|
+
# the query's own session token or the session of its `become` client.
|
|
5954
|
+
# LiveQuery has no equivalent of `scope_to_user` / `scope_to_role`, so
|
|
5955
|
+
# those raise, and an explicit token that differs from the query's
|
|
5956
|
+
# authority raises too.
|
|
5957
|
+
#
|
|
5958
|
+
# A query that carries any authority must target the same Parse
|
|
5959
|
+
# application as the LiveQuery client, compared by application id, so
|
|
5960
|
+
# one app's session token is never sent to another app's LiveQuery
|
|
5961
|
+
# server. A query with the default authority sends no query-derived
|
|
5962
|
+
# credential, so it is not checked.
|
|
5963
|
+
# @param explicit [String, nil] a `session_token:` passed by the caller.
|
|
5964
|
+
# @param live_query_client [Parse::LiveQuery::Client, nil] the client
|
|
5965
|
+
# the subscription will be sent on.
|
|
5966
|
+
# @return [String, nil]
|
|
5967
|
+
def live_query_session_token(explicit = nil, live_query_client: nil)
|
|
5968
|
+
enforce_or_branch_scope!
|
|
5969
|
+
scope = or_effective_scope
|
|
5970
|
+
if scope.key?(:user) || scope.key?(:role)
|
|
5971
|
+
raise ArgumentError,
|
|
5972
|
+
"A query scoped with scope_to_user / scope_to_role cannot be subscribed to: " \
|
|
5973
|
+
"LiveQuery authorizes by session token only. Subscribe with that user's session instead."
|
|
5974
|
+
end
|
|
5975
|
+
explicit = explicit.session_token if explicit.respond_to?(:session_token)
|
|
5976
|
+
own = scope[:session]
|
|
5977
|
+
if explicit.is_a?(String) && !explicit.empty? && own && explicit != own
|
|
5978
|
+
raise ArgumentError,
|
|
5979
|
+
"The session_token: passed to subscribe differs from the session this query runs under."
|
|
5980
|
+
end
|
|
5981
|
+
ensure_live_query_application!(live_query_client) unless scope.empty?
|
|
5982
|
+
(explicit.is_a?(String) && !explicit.empty?) ? explicit : own
|
|
5983
|
+
end
|
|
5984
|
+
|
|
5985
|
+
# @!visibility private
|
|
5986
|
+
# Raise when a LiveQuery client targets a different Parse application
|
|
5987
|
+
# than this query, so a credential bound to one app never reaches
|
|
5988
|
+
# another. Compared by application id: the LiveQuery URL is a WebSocket
|
|
5989
|
+
# endpoint and never matches the REST server URL.
|
|
5990
|
+
def ensure_live_query_application!(live_query_client)
|
|
5991
|
+
return if live_query_client.nil? || !live_query_client.respond_to?(:application_id)
|
|
5992
|
+
query_app = or_application_identity&.first
|
|
5993
|
+
lq_app = live_query_client.application_id.to_s
|
|
5994
|
+
return if query_app.nil? || query_app.empty? || lq_app.empty? || query_app == lq_app
|
|
5995
|
+
raise ArgumentError,
|
|
5996
|
+
"This query runs under an authority bound to application #{query_app.inspect}, but the " \
|
|
5997
|
+
"LiveQuery client targets #{lq_app.inspect}. Its credentials are not sent to another " \
|
|
5998
|
+
"application; subscribe on a LiveQuery client for the same application."
|
|
5999
|
+
end
|
|
6000
|
+
private :ensure_live_query_application!
|
|
6001
|
+
|
|
5331
6002
|
# @return [Hash] the un-stripped reduced where hash, including any
|
|
5332
6003
|
# SDK-internal markers like `"__mongo_direct_only"` and
|
|
5333
6004
|
# `"__aggregation_pipeline"`. Used by the routing layer to decide
|
|
@@ -6168,6 +6839,56 @@ module Parse
|
|
|
6168
6839
|
end
|
|
6169
6840
|
end
|
|
6170
6841
|
|
|
6842
|
+
# @!visibility private
|
|
6843
|
+
# Rewrite bare objectId strings compared to a declared pointer column
|
|
6844
|
+
# into Pointer hashes, for the REST where clause. Parse Server compares
|
|
6845
|
+
# a pointer column against its "Class$objectId" storage form, so a bare
|
|
6846
|
+
# objectId never matched (and `$ne` matched every row). Same rules as
|
|
6847
|
+
# {#coerce_bare_pointer_ids}: only declared pointers with a known target
|
|
6848
|
+
# class, only `$eq` / `$ne` / `$in` / `$nin` / `$all` and plain
|
|
6849
|
+
# equality, recursing into `$or` / `$and` / `$nor`.
|
|
6850
|
+
# @param where [Hash] a compiled where clause.
|
|
6851
|
+
# @return [Hash]
|
|
6852
|
+
def coerce_rest_pointer_ids(where)
|
|
6853
|
+
return where unless where.is_a?(Hash) && !where.empty?
|
|
6854
|
+
klass = table_model_class
|
|
6855
|
+
return where unless klass
|
|
6856
|
+
where.each_with_object({}) do |(field, value), out|
|
|
6857
|
+
name = field.to_s
|
|
6858
|
+
out[field] = if %w[$or $and $nor].include?(name) && value.is_a?(Array)
|
|
6859
|
+
value.map { |branch| coerce_rest_pointer_ids(branch) }
|
|
6860
|
+
elsif name.start_with?("$") || name.include?(".")
|
|
6861
|
+
value
|
|
6862
|
+
elsif (target = get_pointer_target_class_for(klass, name))
|
|
6863
|
+
rest_pointer_value(target, value)
|
|
6864
|
+
else
|
|
6865
|
+
value
|
|
6866
|
+
end
|
|
6867
|
+
end
|
|
6868
|
+
end
|
|
6869
|
+
|
|
6870
|
+
# @!visibility private
|
|
6871
|
+
def rest_pointer_value(target, value)
|
|
6872
|
+
bare = ->(v) { v.is_a?(String) && !v.empty? && !v.include?("$") }
|
|
6873
|
+
to_pointer = ->(v) { bare.(v) ? { "__type" => "Pointer", "className" => target, "objectId" => v } : v }
|
|
6874
|
+
case value
|
|
6875
|
+
when String
|
|
6876
|
+
to_pointer.(value)
|
|
6877
|
+
when Hash
|
|
6878
|
+
return value if value.key?("__type") || value.key?(:__type)
|
|
6879
|
+
value.each_with_object({}) do |(op, op_value), out|
|
|
6880
|
+
out[op] = case op.to_s
|
|
6881
|
+
when "$eq", "$ne" then to_pointer.(op_value)
|
|
6882
|
+
when "$in", "$nin", "$all"
|
|
6883
|
+
op_value.is_a?(Array) ? op_value.map(&to_pointer) : op_value
|
|
6884
|
+
else op_value
|
|
6885
|
+
end
|
|
6886
|
+
end
|
|
6887
|
+
else
|
|
6888
|
+
value
|
|
6889
|
+
end
|
|
6890
|
+
end
|
|
6891
|
+
|
|
6171
6892
|
# Handle a constraint value that is a bare String inside `$in`/`$nin`
|
|
6172
6893
|
# against a column positively identified as a pointer, when the
|
|
6173
6894
|
# target class cannot be resolved (no local belongs_to AND no peer
|
|
@@ -6571,20 +7292,25 @@ module Parse
|
|
|
6571
7292
|
raise ArgumentError, "All queries passed to Parse::Query.or must be for the same Parse class."
|
|
6572
7293
|
end
|
|
6573
7294
|
|
|
6574
|
-
# Start with an empty query for this table
|
|
7295
|
+
# Start with an empty query for this table, carrying the members'
|
|
7296
|
+
# common auth scope (raises when two members set different scopes).
|
|
6575
7297
|
result = self.new(table)
|
|
7298
|
+
# Run against the members' application: seed the client from the
|
|
7299
|
+
# first member that sets one (the members are then checked to agree).
|
|
7300
|
+
seed = queries.lazy.map { |q| q.send(:or_application_client) }.find(&:itself)
|
|
7301
|
+
result.instance_variable_set(:@client, seed) if seed
|
|
7302
|
+
queries.each { |query| result.send(:merge_or_scope!, query) }
|
|
6576
7303
|
|
|
6577
|
-
#
|
|
6578
|
-
|
|
7304
|
+
# Refuse pipeline-only constraints in any member first, so one is
|
|
7305
|
+
# never dropped or widened.
|
|
7306
|
+
queries.each { |query| compile_or_branch(query.where) }
|
|
7307
|
+
|
|
7308
|
+
# A query with no constraints matches every row, so an OR that
|
|
7309
|
+
# includes one does too.
|
|
7310
|
+
return result if queries.any? { |q| match_all?(q.where) }
|
|
6579
7311
|
|
|
6580
7312
|
# Add each query's complete constraint set as an OR branch
|
|
6581
|
-
queries.each
|
|
6582
|
-
# Compile the where constraints to check if they result in empty conditions
|
|
6583
|
-
compiled_where = Parse::Query.compile_where(query.where)
|
|
6584
|
-
unless compiled_where.empty?
|
|
6585
|
-
result.or_where(query.where)
|
|
6586
|
-
end
|
|
6587
|
-
end
|
|
7313
|
+
queries.each { |query| result.or_where(query.where) }
|
|
6588
7314
|
|
|
6589
7315
|
result
|
|
6590
7316
|
end
|
|
@@ -6631,13 +7357,19 @@ module Parse
|
|
|
6631
7357
|
|
|
6632
7358
|
# Creates a deep copy of this query object, allowing independent modifications
|
|
6633
7359
|
# @return [Parse::Query] a new query object with the same constraints
|
|
6634
|
-
# @note The @
|
|
6635
|
-
#
|
|
7360
|
+
# @note The @results instance variable is not cloned. A client that sets
|
|
7361
|
+
# the query's authority (session-bound or without a master key) is kept
|
|
7362
|
+
# by reference; otherwise the copy uses the default client.
|
|
6636
7363
|
def clone
|
|
6637
7364
|
cloned_query = Parse::Query.new(self.instance_variable_get(:@table))
|
|
6638
7365
|
# Note: :client is intentionally excluded - it contains non-serializable objects
|
|
6639
7366
|
# (Redis connections, Faraday connections) and should be obtained lazily
|
|
6640
|
-
|
|
7367
|
+
# The auth scope and read routing travel with the copy, so a cloned or
|
|
7368
|
+
# combined query never runs under a broader authority than the
|
|
7369
|
+
# original.
|
|
7370
|
+
[:count, :where, :order, :keys, :exclude_keys, :includes, :limit, :skip, :cache, :use_master_key, :hint,
|
|
7371
|
+
:session_token, :acl_user, :acl_role, :read_preference, :key, :acl_query_mongo_direct,
|
|
7372
|
+
:_or_branch_scope, :_or_branch_app].each do |param|
|
|
6641
7373
|
if instance_variable_defined?(:"@#{param}")
|
|
6642
7374
|
value = instance_variable_get(:"@#{param}")
|
|
6643
7375
|
if value.is_a?(Array) || value.is_a?(Hash)
|
|
@@ -6655,6 +7387,12 @@ module Parse
|
|
|
6655
7387
|
cloned_query.instance_variable_set(:"@#{param}", cloned_value)
|
|
6656
7388
|
end
|
|
6657
7389
|
end
|
|
7390
|
+
# A client set on the query (one from Parse::Client#become or
|
|
7391
|
+
# #anonymous, or a client for another application) decides where the
|
|
7392
|
+
# query's session token and authority are sent, so the copy keeps it.
|
|
7393
|
+
# Only the memoized default client is resolved lazily again.
|
|
7394
|
+
own_client = or_application_client
|
|
7395
|
+
cloned_query.instance_variable_set(:@client, own_client) if own_client
|
|
6658
7396
|
cloned_query.instance_variable_set(:@results, nil)
|
|
6659
7397
|
cloned_query
|
|
6660
7398
|
end
|