parse-stack-next 5.8.0 → 5.8.2
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 +630 -0
- data/README.md +31 -19
- data/docs/caching.md +90 -5
- data/docs/mcp_guide.md +22 -5
- data/docs/mongodb_direct_guide.md +29 -0
- data/docs/webhooks_guide.md +94 -9
- data/lib/parse/acl_scope.rb +71 -9
- data/lib/parse/agent/constraint_translator.rb +22 -21
- data/lib/parse/agent/describe.rb +8 -2
- data/lib/parse/agent/errors.rb +17 -0
- data/lib/parse/agent/mcp_dispatcher.rb +8 -1
- data/lib/parse/agent/mcp_server.rb +47 -6
- data/lib/parse/agent.rb +521 -70
- data/lib/parse/api/batch.rb +70 -2
- data/lib/parse/atlas_search/index_manager.rb +5 -1
- data/lib/parse/atlas_search.rb +50 -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 +263 -11
- data/lib/parse/client/body_builder.rb +100 -15
- data/lib/parse/client/caching.rb +53 -3
- data/lib/parse/client/request.rb +59 -0
- data/lib/parse/client/response.rb +63 -5
- data/lib/parse/client.rb +173 -33
- data/lib/parse/clp_scope.rb +37 -3
- data/lib/parse/live_query/client.rb +142 -11
- 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 +307 -19
- data/lib/parse/model/classes/user.rb +33 -12
- data/lib/parse/model/core/actions.rb +41 -5
- data/lib/parse/model/core/fetching.rb +29 -2
- data/lib/parse/model/core/field_guards.rb +16 -8
- data/lib/parse/model/file.rb +96 -19
- data/lib/parse/model/object.rb +52 -1
- data/lib/parse/model/push.rb +11 -1
- data/lib/parse/mongodb.rb +43 -2
- data/lib/parse/pipeline_security.rb +157 -21
- data/lib/parse/query/constraint.rb +1 -1
- data/lib/parse/query/constraints.rb +665 -41
- data/lib/parse/query.rb +802 -60
- data/lib/parse/stack/tasks.rb +1 -1
- data/lib/parse/stack/version.rb +1 -1
- data/lib/parse/stack.rb +23 -0
- data/lib/parse/vector_search/hybrid.rb +78 -16
- data/lib/parse/vector_search.rb +215 -7
- data/lib/parse/webhooks/payload.rb +82 -8
- data/lib/parse/webhooks/registration.rb +26 -6
- data/lib/parse/webhooks/replay_protection.rb +19 -9
- data/lib/parse/webhooks.rb +329 -13
- 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:
|
|
@@ -586,11 +638,16 @@ module Parse
|
|
|
586
638
|
#
|
|
587
639
|
# Identical constraints collapse to one.
|
|
588
640
|
def constraint_reduce(clauses)
|
|
589
|
-
clauses.reduce({}) do |clause, subclause|
|
|
641
|
+
reduced = clauses.reduce({}) do |clause, subclause|
|
|
590
642
|
subclause_json = subclause.as_json || {}
|
|
591
643
|
subclause_json.each { |key, value| merge_constraint_entry!(clause, key, value) }
|
|
592
644
|
clause
|
|
593
645
|
end
|
|
646
|
+
# A raw `{ "$regex" => ... }` in a where hash (or under `$not`,
|
|
647
|
+
# `$elemMatch`, or an OR branch) never passes through the regex
|
|
648
|
+
# constraint's own check, so every compiled pattern is checked here.
|
|
649
|
+
Parse::RegexSecurity.validate_where!(reduced)
|
|
650
|
+
reduced
|
|
594
651
|
end
|
|
595
652
|
|
|
596
653
|
# Operators whose meaning depends on a partner operator in the same
|
|
@@ -688,6 +745,14 @@ module Parse
|
|
|
688
745
|
@client ||= self.class.client
|
|
689
746
|
end
|
|
690
747
|
|
|
748
|
+
# Set the client this query runs through. A client from
|
|
749
|
+
# {Parse::Client#become} carries a session, so on a query combined
|
|
750
|
+
# with OR the change is checked like any other change of authority.
|
|
751
|
+
# @param value [Parse::Client]
|
|
752
|
+
def client=(value)
|
|
753
|
+
guard_or_scope_change { @client = value }
|
|
754
|
+
end
|
|
755
|
+
|
|
691
756
|
# Clear a specific clause of this query. This can be one of: :where, :order,
|
|
692
757
|
# :includes, :skip, :limit, :count, :keys or :results.
|
|
693
758
|
# @param item [:Symbol] the clause to clear.
|
|
@@ -768,6 +833,11 @@ module Parse
|
|
|
768
833
|
# and their value.
|
|
769
834
|
# @return [self]
|
|
770
835
|
def conditions(expressions = {})
|
|
836
|
+
# Apply `:or` last, so a `:session` or `:use_master_key` in the same
|
|
837
|
+
# Hash is in place before the branches' authority is compared.
|
|
838
|
+
if expressions.is_a?(Hash) && expressions.key?(:or) && expressions.keys.last != :or
|
|
839
|
+
expressions = expressions.reject { |k, _| k == :or }.merge(or: expressions[:or])
|
|
840
|
+
end
|
|
771
841
|
expressions.each do |expression, value|
|
|
772
842
|
# Normalize to symbol for comparison (handles both string and symbol keys)
|
|
773
843
|
expr_sym = expression.respond_to?(:to_sym) ? expression.to_sym : expression
|
|
@@ -835,6 +905,9 @@ module Parse
|
|
|
835
905
|
not_publicly_readable if value
|
|
836
906
|
elsif expr_sym == :not_publicly_writable
|
|
837
907
|
not_publicly_writable if value
|
|
908
|
+
elsif expression == :or
|
|
909
|
+
# Symbol key only: a string "or" stays a literal field name.
|
|
910
|
+
add_or_branches(value)
|
|
838
911
|
else
|
|
839
912
|
add_constraint(expression, value)
|
|
840
913
|
end
|
|
@@ -842,6 +915,114 @@ module Parse
|
|
|
842
915
|
self #chaining
|
|
843
916
|
end
|
|
844
917
|
|
|
918
|
+
# @!visibility private
|
|
919
|
+
# Handle the `:or => [branch, ...]` condition. Each branch is a
|
|
920
|
+
# constraint Hash, a {Parse::Query}, or an Array of {Parse::Constraint}.
|
|
921
|
+
# The branches are ORed together and the group is ANDed with the
|
|
922
|
+
# query's other constraints. A branch with no constraints matches every
|
|
923
|
+
# row, so the group adds no constraint. An empty list matches no rows.
|
|
924
|
+
# @param branches [Array]
|
|
925
|
+
def add_or_branches(branches)
|
|
926
|
+
unless branches.is_a?(Array)
|
|
927
|
+
raise ArgumentError, ":or expects an Array of constraint hashes or queries, got #{branches.class}."
|
|
928
|
+
end
|
|
929
|
+
with_or_rollback { add_or_branches_unguarded(branches) }
|
|
930
|
+
end
|
|
931
|
+
|
|
932
|
+
# @!visibility private
|
|
933
|
+
def add_or_branches_unguarded(branches)
|
|
934
|
+
wheres = branches.map do |branch|
|
|
935
|
+
case branch
|
|
936
|
+
when Parse::Query
|
|
937
|
+
unless branch.table == @table
|
|
938
|
+
raise ArgumentError, ":or branch queries must be of the same class #{@table} (got #{branch.table})."
|
|
939
|
+
end
|
|
940
|
+
# The branch's authority travels with its conditions, or raises
|
|
941
|
+
# when it differs from this query's.
|
|
942
|
+
merge_or_scope!(branch)
|
|
943
|
+
branch.where
|
|
944
|
+
when Hash then or_branch_hash_where(branch)
|
|
945
|
+
when Array
|
|
946
|
+
# A branch given as a list: constraints, or constraint hashes that
|
|
947
|
+
# are expanded the same way a Hash branch is. Anything else is
|
|
948
|
+
# refused, so a typo cannot empty the branch and turn it into
|
|
949
|
+
# match-all.
|
|
950
|
+
branch.flat_map do |item|
|
|
951
|
+
case item
|
|
952
|
+
when Parse::Constraint then [item]
|
|
953
|
+
when Hash then or_branch_hash_where(item)
|
|
954
|
+
else
|
|
955
|
+
raise ArgumentError, ":or Array branches may only hold Parse::Constraint objects or constraint hashes, got #{item.class}."
|
|
956
|
+
end
|
|
957
|
+
end
|
|
958
|
+
else
|
|
959
|
+
raise ArgumentError, ":or branches must be constraint hashes or Parse::Query objects, got #{branch.class}."
|
|
960
|
+
end
|
|
961
|
+
end
|
|
962
|
+
if wheres.empty?
|
|
963
|
+
# `$or: []` is rejected by MongoDB. Keep the "no rows" meaning.
|
|
964
|
+
add_constraint(:objectId.in, [])
|
|
965
|
+
return
|
|
966
|
+
end
|
|
967
|
+
# Refuse pipeline-only constraints in any branch before deciding
|
|
968
|
+
# whether the group is match-all.
|
|
969
|
+
compiled = wheres.map { |w| Parse::Query.compile_or_branch(w) }
|
|
970
|
+
return if wheres.any? { |w| Parse::Query.match_all?(w) }
|
|
971
|
+
add_constraint(Parse::Constraint::CompoundQueryConstraint.new(:or, compiled), nil, filter: false)
|
|
972
|
+
end
|
|
973
|
+
private :add_or_branches_unguarded
|
|
974
|
+
|
|
975
|
+
# @!visibility private
|
|
976
|
+
# Query options that shape a whole query and mean nothing inside one OR
|
|
977
|
+
# branch.
|
|
978
|
+
OR_BRANCH_OPTION_KEYS = %i[order keys key skip limit include includes cache read_preference].freeze
|
|
979
|
+
|
|
980
|
+
# @!visibility private
|
|
981
|
+
# The constraints of a Hash OR branch. The branch is built as its own
|
|
982
|
+
# query so a `session:` / `use_master_key:` key or a nested `:or` with a
|
|
983
|
+
# scoped query is applied there, and its authority then has to agree
|
|
984
|
+
# with this query's (adopted when this query has none, refused when it
|
|
985
|
+
# differs). Query-shaping options inside a branch are refused.
|
|
986
|
+
# @param hash [Hash]
|
|
987
|
+
# @return [Array<Parse::Constraint>]
|
|
988
|
+
def or_branch_hash_where(hash)
|
|
989
|
+
options = hash.keys.select { |k| k.is_a?(Symbol) && OR_BRANCH_OPTION_KEYS.include?(k) }
|
|
990
|
+
if options.any?
|
|
991
|
+
raise ArgumentError,
|
|
992
|
+
"#{options.map(&:inspect).join(", ")} cannot be set inside an OR branch; " \
|
|
993
|
+
"set it on the query itself."
|
|
994
|
+
end
|
|
995
|
+
branch = Parse::Query.new(@table, hash)
|
|
996
|
+
merge_or_scope!(branch)
|
|
997
|
+
branch.where
|
|
998
|
+
end
|
|
999
|
+
private :or_branch_hash_where
|
|
1000
|
+
|
|
1001
|
+
# @!visibility private
|
|
1002
|
+
# Run an OR or subquery merge so that a raise leaves this query exactly
|
|
1003
|
+
# as it was: constraints, authority, client, and the pinned authority.
|
|
1004
|
+
def with_or_rollback
|
|
1005
|
+
# Captured before the guarded block: a failure here changes nothing,
|
|
1006
|
+
# so there is nothing to restore.
|
|
1007
|
+
ivars = OR_SCOPE_STATE_IVARS + %i[@_or_branch_scope @_or_branch_app @read_preference]
|
|
1008
|
+
saved = ivars.to_h { |iv| [iv, [instance_variable_defined?(iv), instance_variable_get(iv)]] }
|
|
1009
|
+
saved_where = @where.dup
|
|
1010
|
+
begin
|
|
1011
|
+
yield
|
|
1012
|
+
rescue StandardError
|
|
1013
|
+
saved.each do |iv, (defined, value)|
|
|
1014
|
+
if defined
|
|
1015
|
+
instance_variable_set(iv, value)
|
|
1016
|
+
elsif instance_variable_defined?(iv)
|
|
1017
|
+
remove_instance_variable(iv)
|
|
1018
|
+
end
|
|
1019
|
+
end
|
|
1020
|
+
@where = saved_where
|
|
1021
|
+
raise
|
|
1022
|
+
end
|
|
1023
|
+
end
|
|
1024
|
+
private :with_or_rollback
|
|
1025
|
+
|
|
845
1026
|
alias_method :query, :conditions
|
|
846
1027
|
alias_method :append, :conditions
|
|
847
1028
|
|
|
@@ -854,11 +1035,16 @@ module Parse
|
|
|
854
1035
|
value = value.session_token
|
|
855
1036
|
end
|
|
856
1037
|
|
|
857
|
-
|
|
858
|
-
@session_token = value
|
|
859
|
-
else
|
|
1038
|
+
unless value.nil? || (value.is_a?(String) && value.present?)
|
|
860
1039
|
raise ArgumentError, "Invalid session token passed to query."
|
|
861
1040
|
end
|
|
1041
|
+
guard_or_scope_change { @session_token = value }
|
|
1042
|
+
end
|
|
1043
|
+
|
|
1044
|
+
# @param value [Boolean, nil] whether to send the master key (nil
|
|
1045
|
+
# leaves the decision to the request layer).
|
|
1046
|
+
def use_master_key=(value)
|
|
1047
|
+
guard_or_scope_change { @use_master_key = value }
|
|
862
1048
|
end
|
|
863
1049
|
|
|
864
1050
|
# returns the query clause for the particular clause
|
|
@@ -1008,11 +1194,16 @@ module Parse
|
|
|
1008
1194
|
case entry
|
|
1009
1195
|
when Order
|
|
1010
1196
|
entry.field = Query.format_field(entry.field)
|
|
1011
|
-
|
|
1012
|
-
when
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1197
|
+
push_order(entry)
|
|
1198
|
+
when String
|
|
1199
|
+
if entry.include?(",")
|
|
1200
|
+
# The REST sort string lists several fields: "title,-plays".
|
|
1201
|
+
order(*entry.split(",").map(&:strip).reject(&:empty?))
|
|
1202
|
+
next
|
|
1203
|
+
end
|
|
1204
|
+
push_order(parse_order_string(entry))
|
|
1205
|
+
when Symbol
|
|
1206
|
+
push_order(parse_order_string(entry))
|
|
1016
1207
|
when Hash
|
|
1017
1208
|
entry.each do |field, direction|
|
|
1018
1209
|
dir_sym = direction.is_a?(String) ? direction.downcase.to_sym : direction
|
|
@@ -1023,7 +1214,7 @@ module Parse
|
|
|
1023
1214
|
end
|
|
1024
1215
|
o = Order.new(field, dir_sym)
|
|
1025
1216
|
o.field = Query.format_field(o.field)
|
|
1026
|
-
|
|
1217
|
+
push_order(o)
|
|
1027
1218
|
end
|
|
1028
1219
|
else
|
|
1029
1220
|
raise ArgumentError,
|
|
@@ -1036,6 +1227,38 @@ module Parse
|
|
|
1036
1227
|
self #chaining
|
|
1037
1228
|
end #order
|
|
1038
1229
|
|
|
1230
|
+
# @!visibility private
|
|
1231
|
+
# One field of the REST sort-string form: "-title" is descending and
|
|
1232
|
+
# "+title" ascending. Parsing the sign here keeps a repeated field
|
|
1233
|
+
# deduplicated and gives the direct `$sort` stage a real field name.
|
|
1234
|
+
# @return [Parse::Order]
|
|
1235
|
+
def parse_order_string(entry)
|
|
1236
|
+
name = entry.to_s
|
|
1237
|
+
o = if name.start_with?("-") && name.length > 1
|
|
1238
|
+
Order.new(name[1..], :desc)
|
|
1239
|
+
elsif name.start_with?("+") && name.length > 1
|
|
1240
|
+
Order.new(name[1..], :asc)
|
|
1241
|
+
else
|
|
1242
|
+
Order.new(entry)
|
|
1243
|
+
end
|
|
1244
|
+
o.field = Query.format_field(o.field)
|
|
1245
|
+
o
|
|
1246
|
+
end
|
|
1247
|
+
private :parse_order_string
|
|
1248
|
+
|
|
1249
|
+
# @!visibility private
|
|
1250
|
+
# Add one sort key. A field that is already in the order keeps its
|
|
1251
|
+
# position and takes the new direction, so it is sent once. Parse Server
|
|
1252
|
+
# and MongoDB both resolve a repeated sort key the same way.
|
|
1253
|
+
def push_order(order)
|
|
1254
|
+
index = @order.index { |existing| existing.field.to_s == order.field.to_s }
|
|
1255
|
+
if index
|
|
1256
|
+
@order[index] = order
|
|
1257
|
+
else
|
|
1258
|
+
@order.push order
|
|
1259
|
+
end
|
|
1260
|
+
end
|
|
1261
|
+
|
|
1039
1262
|
# Use with limit to paginate through results. Default is 0.
|
|
1040
1263
|
# @example
|
|
1041
1264
|
# # get the next 3 songs after the first 10
|
|
@@ -1184,6 +1407,7 @@ module Parse
|
|
|
1184
1407
|
# @return [self]
|
|
1185
1408
|
def add_constraints(list)
|
|
1186
1409
|
list = Array.wrap(list).select { |m| m.is_a?(Parse::Constraint) }
|
|
1410
|
+
list.each { |c| merge_subquery_scopes!(c.value) }
|
|
1187
1411
|
@where = @where + list
|
|
1188
1412
|
self
|
|
1189
1413
|
end
|
|
@@ -1222,11 +1446,31 @@ module Parse
|
|
|
1222
1446
|
constraint.operand = Query.format_field(constraint.operand)
|
|
1223
1447
|
end
|
|
1224
1448
|
reject_vector_constraint!(constraint)
|
|
1449
|
+
merge_subquery_scopes!(constraint.value)
|
|
1225
1450
|
@where.push constraint
|
|
1226
1451
|
@results = nil
|
|
1227
1452
|
self #chaining
|
|
1228
1453
|
end
|
|
1229
1454
|
|
|
1455
|
+
# @!visibility private
|
|
1456
|
+
# Parse Server runs a subquery (`$inQuery`, `$notInQuery`, `$select`,
|
|
1457
|
+
# `$dontSelect`) under the outer request's authority, so a subquery's
|
|
1458
|
+
# own session or scope has to agree with this query's: it is adopted
|
|
1459
|
+
# when this query has none and refused when it differs, the same rule
|
|
1460
|
+
# as an OR branch.
|
|
1461
|
+
def merge_subquery_scopes!(value)
|
|
1462
|
+
subqueries = case value
|
|
1463
|
+
when Parse::Query then [value]
|
|
1464
|
+
when Hash
|
|
1465
|
+
q = value[:query] || value["query"]
|
|
1466
|
+
q.is_a?(Parse::Query) ? [q] : []
|
|
1467
|
+
else []
|
|
1468
|
+
end
|
|
1469
|
+
return if subqueries.empty?
|
|
1470
|
+
with_or_rollback { subqueries.each { |q| merge_or_scope!(q) } }
|
|
1471
|
+
end
|
|
1472
|
+
private :merge_subquery_scopes!
|
|
1473
|
+
|
|
1230
1474
|
# @!visibility private
|
|
1231
1475
|
# Raise {Parse::VectorSearch::ConstraintNotSupported} when a
|
|
1232
1476
|
# constraint targets a declared `:vector` property with an operator
|
|
@@ -1325,8 +1569,22 @@ module Parse
|
|
|
1325
1569
|
# @param where_clauses [Array<Parse::Constraint>] a list of Parse::Constraint objects to combine.
|
|
1326
1570
|
# @return [Query] the combined query with an OR clause.
|
|
1327
1571
|
def or_where(where_clauses = [])
|
|
1328
|
-
|
|
1329
|
-
|
|
1572
|
+
with_or_rollback { or_where_unguarded(where_clauses) }
|
|
1573
|
+
end
|
|
1574
|
+
|
|
1575
|
+
# @!visibility private
|
|
1576
|
+
def or_where_unguarded(where_clauses)
|
|
1577
|
+
if where_clauses.is_a?(Parse::Query)
|
|
1578
|
+
unless where_clauses.table == @table
|
|
1579
|
+
raise ArgumentError, "Parse queries must be of the same class #{@table}."
|
|
1580
|
+
end
|
|
1581
|
+
merge_or_scope!(where_clauses)
|
|
1582
|
+
# A query with no constraints is a branch that matches every row, so
|
|
1583
|
+
# the whole OR does too.
|
|
1584
|
+
return match_all! if Parse::Query.match_all?(where_clauses.where)
|
|
1585
|
+
where_clauses = where_clauses.where
|
|
1586
|
+
end
|
|
1587
|
+
where_clauses = or_branch_hash_where(where_clauses) if where_clauses.is_a?(Hash)
|
|
1330
1588
|
return self if where_clauses.blank?
|
|
1331
1589
|
# Reuse the existing OR only when it is the query's sole constraint.
|
|
1332
1590
|
# When other constraints sit beside it, the current where is
|
|
@@ -1339,13 +1597,13 @@ module Parse
|
|
|
1339
1597
|
# if we don't have a OR clause to reuse, then create a new one with then
|
|
1340
1598
|
# current set of constraints
|
|
1341
1599
|
if compound.blank?
|
|
1342
|
-
initial_constraints = Parse::Query.
|
|
1600
|
+
initial_constraints = Parse::Query.compile_or_branch(@where)
|
|
1343
1601
|
# Only include initial constraints if they're not empty
|
|
1344
1602
|
initial_values = initial_constraints.empty? ? [] : [initial_constraints]
|
|
1345
1603
|
compound = Parse::Constraint::CompoundQueryConstraint.new :or, initial_values
|
|
1346
1604
|
end
|
|
1347
1605
|
# then take the where clauses from the second query and append them.
|
|
1348
|
-
new_constraints = Parse::Query.
|
|
1606
|
+
new_constraints = Parse::Query.compile_or_branch(where_clauses)
|
|
1349
1607
|
# Only add new constraints if they're not empty
|
|
1350
1608
|
unless new_constraints.empty?
|
|
1351
1609
|
compound.value.push new_constraints
|
|
@@ -1354,16 +1612,330 @@ module Parse
|
|
|
1354
1612
|
@where = [compound]
|
|
1355
1613
|
self #chaining
|
|
1356
1614
|
end
|
|
1615
|
+
private :or_where_unguarded
|
|
1357
1616
|
|
|
1617
|
+
# Combine two queries with OR. The result is a copy of the receiver
|
|
1618
|
+
# (its limit, order, keys and includes) whose where is the OR of both.
|
|
1619
|
+
#
|
|
1620
|
+
# * A receiver with no constraints is a starting point, the same as
|
|
1621
|
+
# {#or_where}: `Model.query | q` gives an OR whose only branch is `q`,
|
|
1622
|
+
# so `[q1, q2].reduce(Model.query, :|)` gives `q1 OR q2`.
|
|
1623
|
+
# * A right-hand side with no constraints is a branch that matches
|
|
1624
|
+
# every row, so `q | Model.query` has no constraints.
|
|
1625
|
+
#
|
|
1626
|
+
# The auth scope (session token, master-key flag, `scope_to_user`,
|
|
1627
|
+
# `scope_to_role`) carries over from whichever side set it. Two
|
|
1628
|
+
# different scopes raise `ArgumentError`, since the OR could only run
|
|
1629
|
+
# under one of them.
|
|
1358
1630
|
# @see #or_where
|
|
1359
1631
|
# @return [Query] the combined query with an OR clause.
|
|
1632
|
+
# @raise [ArgumentError] when the classes or auth scopes differ, or a
|
|
1633
|
+
# branch holds a pipeline-only constraint.
|
|
1360
1634
|
def |(other_query)
|
|
1361
1635
|
raise ArgumentError, "Parse queries must be of the same class #{@table}." unless @table == other_query.table
|
|
1362
1636
|
copy_query = self.clone
|
|
1363
|
-
copy_query.
|
|
1637
|
+
copy_query.send(:merge_or_scope!, other_query)
|
|
1638
|
+
if Parse::Query.match_all?(other_query.where)
|
|
1639
|
+
# Still refuse a pipeline-only receiver rather than silently
|
|
1640
|
+
# widening it.
|
|
1641
|
+
Parse::Query.compile_or_branch(@where)
|
|
1642
|
+
copy_query.match_all!
|
|
1643
|
+
else
|
|
1644
|
+
copy_query.or_where other_query.where
|
|
1645
|
+
end
|
|
1364
1646
|
copy_query
|
|
1365
1647
|
end
|
|
1366
1648
|
|
|
1649
|
+
# @!visibility private
|
|
1650
|
+
# Auth scope ivars an OR must agree on.
|
|
1651
|
+
OR_SCOPE_IVARS = %i[session_token use_master_key acl_user acl_role].freeze
|
|
1652
|
+
|
|
1653
|
+
# @!visibility private
|
|
1654
|
+
# The authority this query runs under, as one comparable Hash: the
|
|
1655
|
+
# session token, the scoped user id, the scoped role name, and an
|
|
1656
|
+
# explicit master-key flag. An explicit `use_master_key = false` beside
|
|
1657
|
+
# a session, user, or role adds nothing, since those already run without
|
|
1658
|
+
# the master key. An empty Hash is the default authority.
|
|
1659
|
+
# @return [Hash]
|
|
1660
|
+
def or_effective_scope
|
|
1661
|
+
scope = {}
|
|
1662
|
+
scope[:session] = @session_token unless @session_token.nil?
|
|
1663
|
+
# A client from Parse::Client#become sends its bound session on every
|
|
1664
|
+
# request that names no token and does not ask for the master key,
|
|
1665
|
+
# so it counts as that session. A non-default client with neither a
|
|
1666
|
+
# session nor a master key runs without the master key.
|
|
1667
|
+
own_client = or_explicit_client
|
|
1668
|
+
if own_client && @session_token.nil? && @use_master_key != true
|
|
1669
|
+
bound = own_client.respond_to?(:session_token) ? own_client.session_token : nil
|
|
1670
|
+
if bound.is_a?(String) && !bound.strip.empty?
|
|
1671
|
+
scope[:session] = bound
|
|
1672
|
+
elsif !(own_client.respond_to?(:master_key) && own_client.master_key.present?)
|
|
1673
|
+
scope[:master] = false
|
|
1674
|
+
end
|
|
1675
|
+
end
|
|
1676
|
+
scope[:user] = Parse::Query.or_scope_value(:acl_user, @acl_user) unless @acl_user.nil?
|
|
1677
|
+
scope[:role] = Parse::Query.or_scope_value(:acl_role, @acl_role) unless @acl_role.nil?
|
|
1678
|
+
unless @use_master_key.nil? || (@use_master_key == false && !scope.empty?)
|
|
1679
|
+
scope[:master] = @use_master_key
|
|
1680
|
+
end
|
|
1681
|
+
scope.delete(:master) if scope[:master] == false && scope.size > 1
|
|
1682
|
+
scope
|
|
1683
|
+
end
|
|
1684
|
+
protected :or_effective_scope
|
|
1685
|
+
|
|
1686
|
+
# @!visibility private
|
|
1687
|
+
# The client set on this query when it is not the default client, or
|
|
1688
|
+
# nil. Read from the instance variable so it never memoizes the default.
|
|
1689
|
+
# @return [Parse::Client, nil]
|
|
1690
|
+
def or_explicit_client
|
|
1691
|
+
own = @client
|
|
1692
|
+
return nil if own.nil?
|
|
1693
|
+
default = begin
|
|
1694
|
+
self.class.client
|
|
1695
|
+
rescue StandardError
|
|
1696
|
+
nil
|
|
1697
|
+
end
|
|
1698
|
+
return nil if default && own.equal?(default)
|
|
1699
|
+
own
|
|
1700
|
+
end
|
|
1701
|
+
protected :or_explicit_client
|
|
1702
|
+
|
|
1703
|
+
# @!visibility private
|
|
1704
|
+
# Instance variables that make up a query's authority.
|
|
1705
|
+
OR_SCOPE_STATE_IVARS = %i[@session_token @use_master_key @acl_user @acl_role @client].freeze
|
|
1706
|
+
|
|
1707
|
+
# @!visibility private
|
|
1708
|
+
# Run an assignment that may change this query's authority. On a query
|
|
1709
|
+
# combined with OR, a change that leaves the pinned authority raises
|
|
1710
|
+
# and the previous values are restored, so a rescued error never
|
|
1711
|
+
# leaves the query running under a different principal.
|
|
1712
|
+
def guard_or_scope_change
|
|
1713
|
+
return yield if @_or_branch_scope.nil?
|
|
1714
|
+
saved = OR_SCOPE_STATE_IVARS.to_h { |iv| [iv, instance_variable_get(iv)] }
|
|
1715
|
+
begin
|
|
1716
|
+
yield
|
|
1717
|
+
enforce_or_branch_scope!
|
|
1718
|
+
rescue StandardError
|
|
1719
|
+
saved.each { |iv, v| instance_variable_set(iv, v) }
|
|
1720
|
+
raise
|
|
1721
|
+
end
|
|
1722
|
+
end
|
|
1723
|
+
private :guard_or_scope_change
|
|
1724
|
+
|
|
1725
|
+
# @!visibility private
|
|
1726
|
+
# Combine `other`'s authority with this query's for an OR. Both must
|
|
1727
|
+
# run under the same authority: a query with the default authority
|
|
1728
|
+
# adopts the other's, and two different authorities (a session and a
|
|
1729
|
+
# scoped user, a session and the master key, two users) raise. The
|
|
1730
|
+
# resulting authority is then pinned whichever side carried it, so a
|
|
1731
|
+
# later change to this query's
|
|
1732
|
+
# session, master-key flag, or scope raises instead of running the
|
|
1733
|
+
# other query's rows under a different principal. Read preference is
|
|
1734
|
+
# carried over but a mismatch keeps this query's value.
|
|
1735
|
+
# @param other [Parse::Query]
|
|
1736
|
+
def merge_or_scope!(other)
|
|
1737
|
+
merge_or_application!(other)
|
|
1738
|
+
theirs = other.or_effective_scope
|
|
1739
|
+
if theirs.empty?
|
|
1740
|
+
# The other query has the default authority: pin this query's own
|
|
1741
|
+
# authority, so the result does not depend on operand order.
|
|
1742
|
+
mine = or_effective_scope
|
|
1743
|
+
unless mine.empty?
|
|
1744
|
+
@_or_branch_scope = mine
|
|
1745
|
+
@_or_branch_app = or_application_identity
|
|
1746
|
+
end
|
|
1747
|
+
else
|
|
1748
|
+
mine = or_effective_scope
|
|
1749
|
+
if mine.empty?
|
|
1750
|
+
OR_SCOPE_IVARS.each do |ivar|
|
|
1751
|
+
instance_variable_set(:"@#{ivar}", other.instance_variable_get(:"@#{ivar}"))
|
|
1752
|
+
end
|
|
1753
|
+
adopt_or_client!(other, theirs)
|
|
1754
|
+
# Fail closed if the adopted state still does not reproduce the
|
|
1755
|
+
# other query's authority.
|
|
1756
|
+
unless or_effective_scope == theirs
|
|
1757
|
+
raise ArgumentError,
|
|
1758
|
+
"Cannot combine with OR a query running under #{Parse::Query.describe_or_scope(theirs)} " \
|
|
1759
|
+
"through its own client. Run the queries separately."
|
|
1760
|
+
end
|
|
1761
|
+
elsif mine != theirs
|
|
1762
|
+
raise ArgumentError,
|
|
1763
|
+
"Queries combined with OR must run under the same authority " \
|
|
1764
|
+
"(#{Parse::Query.describe_or_scope(mine)} vs #{Parse::Query.describe_or_scope(theirs)}). " \
|
|
1765
|
+
"Run them separately, or set the same scope on each."
|
|
1766
|
+
end
|
|
1767
|
+
@_or_branch_scope = theirs
|
|
1768
|
+
@_or_branch_app = or_application_identity
|
|
1769
|
+
end
|
|
1770
|
+
@read_preference ||= other.read_preference
|
|
1771
|
+
self
|
|
1772
|
+
end
|
|
1773
|
+
protected :merge_or_scope!
|
|
1774
|
+
|
|
1775
|
+
# @!visibility private
|
|
1776
|
+
# The client set on this query when it is a real Parse client other
|
|
1777
|
+
# than the default one, or nil.
|
|
1778
|
+
# @return [Parse::Client, nil]
|
|
1779
|
+
def or_application_client
|
|
1780
|
+
own = or_explicit_client
|
|
1781
|
+
return nil unless own.respond_to?(:application_id) && own.respond_to?(:server_url)
|
|
1782
|
+
own
|
|
1783
|
+
end
|
|
1784
|
+
protected :or_application_client
|
|
1785
|
+
|
|
1786
|
+
# @!visibility private
|
|
1787
|
+
# The application this query runs against, as [application id, server
|
|
1788
|
+
# URL], from its own client or the default client. nil when no client
|
|
1789
|
+
# can be resolved.
|
|
1790
|
+
# @return [Array<String>, nil]
|
|
1791
|
+
def or_application_identity
|
|
1792
|
+
target = or_application_client || begin
|
|
1793
|
+
self.class.client
|
|
1794
|
+
rescue StandardError
|
|
1795
|
+
nil
|
|
1796
|
+
end
|
|
1797
|
+
return nil unless target.respond_to?(:application_id) && target.respond_to?(:server_url)
|
|
1798
|
+
[target.application_id.to_s, target.server_url.to_s.chomp("/")]
|
|
1799
|
+
end
|
|
1800
|
+
protected :or_application_identity
|
|
1801
|
+
|
|
1802
|
+
# @!visibility private
|
|
1803
|
+
# Queries combined with OR must target the same application, so a
|
|
1804
|
+
# session token or master-key authority from one app is never sent to
|
|
1805
|
+
# another. An empty receiver on the default client (no constraints, no
|
|
1806
|
+
# authority) takes the other query's client, as it takes its authority;
|
|
1807
|
+
# any other mismatch raises.
|
|
1808
|
+
def merge_or_application!(other)
|
|
1809
|
+
mine = or_application_identity
|
|
1810
|
+
theirs = other.or_application_identity
|
|
1811
|
+
return if mine.nil? || theirs.nil? || mine == theirs
|
|
1812
|
+
their_client = other.or_application_client
|
|
1813
|
+
if their_client && or_application_client.nil? && @_or_branch_scope.nil? &&
|
|
1814
|
+
or_effective_scope.empty? && Parse::Query.match_all?(@where)
|
|
1815
|
+
@client = their_client
|
|
1816
|
+
return
|
|
1817
|
+
end
|
|
1818
|
+
raise ArgumentError,
|
|
1819
|
+
"Queries combined with OR must run against the same Parse application " \
|
|
1820
|
+
"(#{mine.first} at #{mine.last} vs #{theirs.first} at #{theirs.last}). " \
|
|
1821
|
+
"Run them separately on their own clients."
|
|
1822
|
+
end
|
|
1823
|
+
private :merge_or_application!
|
|
1824
|
+
|
|
1825
|
+
# @!visibility private
|
|
1826
|
+
# Carry `other`'s client-bound authority onto this query. A query with
|
|
1827
|
+
# no client of its own takes `other`'s client, which keeps its server
|
|
1828
|
+
# and session. A query with its own client takes the derived session
|
|
1829
|
+
# token (or the no-master-key flag) explicitly instead.
|
|
1830
|
+
def adopt_or_client!(other, theirs)
|
|
1831
|
+
their_client = other.or_explicit_client
|
|
1832
|
+
return if their_client.nil?
|
|
1833
|
+
if or_explicit_client.nil?
|
|
1834
|
+
@client = their_client
|
|
1835
|
+
elsif other.session_token.nil? && theirs.key?(:session)
|
|
1836
|
+
@session_token = theirs[:session]
|
|
1837
|
+
elsif theirs[:master] == false
|
|
1838
|
+
@use_master_key = false
|
|
1839
|
+
end
|
|
1840
|
+
end
|
|
1841
|
+
private :adopt_or_client!
|
|
1842
|
+
|
|
1843
|
+
# @!visibility private
|
|
1844
|
+
# Raise when this query's authority no longer matches the one an OR
|
|
1845
|
+
# branch was combined under.
|
|
1846
|
+
def enforce_or_branch_scope!
|
|
1847
|
+
return if @_or_branch_scope.nil?
|
|
1848
|
+
if @_or_branch_app && (app = or_application_identity) && app != @_or_branch_app
|
|
1849
|
+
raise ArgumentError,
|
|
1850
|
+
"This query was combined with OR against Parse application #{@_or_branch_app.first}; " \
|
|
1851
|
+
"running it against #{app.first} would send its authority to another application. " \
|
|
1852
|
+
"Build a new query instead."
|
|
1853
|
+
end
|
|
1854
|
+
current = or_effective_scope
|
|
1855
|
+
return if current == @_or_branch_scope
|
|
1856
|
+
raise ArgumentError,
|
|
1857
|
+
"This query was combined with OR under #{Parse::Query.describe_or_scope(@_or_branch_scope)}; " \
|
|
1858
|
+
"changing it to #{Parse::Query.describe_or_scope(current)} would run the combined " \
|
|
1859
|
+
"branches under a different authority. Build a new query instead."
|
|
1860
|
+
end
|
|
1861
|
+
private :enforce_or_branch_scope!
|
|
1862
|
+
|
|
1863
|
+
# @!visibility private
|
|
1864
|
+
# On a query combined with OR, auth kwargs passed straight to a direct
|
|
1865
|
+
# or Atlas terminal (`results_direct(master: true)`) must name the same
|
|
1866
|
+
# authority the query was pinned to, and an explicit client the same
|
|
1867
|
+
# application. A narrower `master: false` is allowed.
|
|
1868
|
+
def enforce_explicit_auth_against_or_scope!(session_token: nil, master: nil, acl_user: nil, acl_role: nil, client: nil)
|
|
1869
|
+
return if @_or_branch_scope.nil?
|
|
1870
|
+
if client && @_or_branch_app && client.respond_to?(:application_id) && client.respond_to?(:server_url)
|
|
1871
|
+
app = [client.application_id.to_s, client.server_url.to_s.chomp("/")]
|
|
1872
|
+
unless app == @_or_branch_app
|
|
1873
|
+
raise ArgumentError,
|
|
1874
|
+
"This query was combined with OR against Parse application #{@_or_branch_app.first}; " \
|
|
1875
|
+
"the client: passed here targets #{app.first}."
|
|
1876
|
+
end
|
|
1877
|
+
end
|
|
1878
|
+
given = {}
|
|
1879
|
+
token = session_token.respond_to?(:session_token) ? session_token.session_token : session_token
|
|
1880
|
+
given[:session] = token unless token.nil?
|
|
1881
|
+
given[:user] = Parse::Query.or_scope_value(:acl_user, acl_user) unless acl_user.nil?
|
|
1882
|
+
given[:role] = Parse::Query.or_scope_value(:acl_role, acl_role) unless acl_role.nil?
|
|
1883
|
+
given[:master] = true if master && master != false
|
|
1884
|
+
return if given.empty? || given == @_or_branch_scope
|
|
1885
|
+
raise ArgumentError,
|
|
1886
|
+
"This query was combined with OR under #{Parse::Query.describe_or_scope(@_or_branch_scope)}; " \
|
|
1887
|
+
"the auth passed here (#{Parse::Query.describe_or_scope(given)}) would run it under a " \
|
|
1888
|
+
"different authority."
|
|
1889
|
+
end
|
|
1890
|
+
private :enforce_explicit_auth_against_or_scope!
|
|
1891
|
+
|
|
1892
|
+
# @!visibility private
|
|
1893
|
+
# A redacted, human-readable form of an authority Hash for errors.
|
|
1894
|
+
def self.describe_or_scope(scope)
|
|
1895
|
+
return "the default authority" if scope.empty?
|
|
1896
|
+
scope.map do |kind, value|
|
|
1897
|
+
case kind
|
|
1898
|
+
when :session then "a session token"
|
|
1899
|
+
when :user then "scoped user #{value}"
|
|
1900
|
+
when :role then "scoped role #{value}"
|
|
1901
|
+
when :master then value ? "the master key" : "no master key"
|
|
1902
|
+
end
|
|
1903
|
+
end.join(" + ")
|
|
1904
|
+
end
|
|
1905
|
+
|
|
1906
|
+
# @!visibility private
|
|
1907
|
+
# A comparable form of one auth scope value.
|
|
1908
|
+
def self.or_scope_value(ivar, value)
|
|
1909
|
+
case ivar
|
|
1910
|
+
when :acl_user
|
|
1911
|
+
value.respond_to?(:id) ? value.id : value.to_s
|
|
1912
|
+
when :acl_role
|
|
1913
|
+
name = value.respond_to?(:name) ? value.name : value.to_s
|
|
1914
|
+
name.to_s.delete_prefix("role:")
|
|
1915
|
+
else
|
|
1916
|
+
value
|
|
1917
|
+
end
|
|
1918
|
+
end
|
|
1919
|
+
|
|
1920
|
+
# @!visibility private
|
|
1921
|
+
# Drop every where constraint, so the query matches all rows.
|
|
1922
|
+
# @return [self]
|
|
1923
|
+
def match_all!
|
|
1924
|
+
@where = []
|
|
1925
|
+
@results = nil
|
|
1926
|
+
self
|
|
1927
|
+
end
|
|
1928
|
+
protected :match_all!
|
|
1929
|
+
|
|
1930
|
+
# @!visibility private
|
|
1931
|
+
# Whether a list of constraints places no restriction on the rows (an
|
|
1932
|
+
# empty list, or constraints that compile to nothing).
|
|
1933
|
+
# @param where [Array<Parse::Constraint>]
|
|
1934
|
+
# @return [Boolean]
|
|
1935
|
+
def self.match_all?(where)
|
|
1936
|
+
where.blank? || compile_markers(where).empty?
|
|
1937
|
+
end
|
|
1938
|
+
|
|
1367
1939
|
# Add a "field is NOT between" condition — the logical negation of
|
|
1368
1940
|
# `.where(field.between => value)`: `field < min OR field > max` for a
|
|
1369
1941
|
# fully-bounded Range/Array, or a single one-sided comparison when the
|
|
@@ -1937,6 +2509,7 @@ module Parse
|
|
|
1937
2509
|
|
|
1938
2510
|
# @!visibility private
|
|
1939
2511
|
def _opts
|
|
2512
|
+
enforce_or_branch_scope!
|
|
1940
2513
|
opts = {}
|
|
1941
2514
|
opts[:cache] = self.cache || false
|
|
1942
2515
|
# Only forward `use_master_key` when the caller actually set it.
|
|
@@ -1946,10 +2519,17 @@ module Parse
|
|
|
1946
2519
|
# init-block comment on `@use_master_key`.
|
|
1947
2520
|
opts[:use_master_key] = self.use_master_key unless self.use_master_key.nil?
|
|
1948
2521
|
opts[:session_token] = self.session_token unless self.session_token.nil?
|
|
1949
|
-
#
|
|
1950
|
-
#
|
|
1951
|
-
#
|
|
1952
|
-
|
|
2522
|
+
# SDK-internal metadata read (the role-graph walk used for scope
|
|
2523
|
+
# enforcement): keeps the master key inside `Parse.without_master_key`.
|
|
2524
|
+
# Set only by the SDK through an instance variable, never a public option.
|
|
2525
|
+
if @_metadata_master == true
|
|
2526
|
+
opts[:use_master_key] = true
|
|
2527
|
+
opts.delete(:session_token)
|
|
2528
|
+
opts[:metadata_master] = Parse::Client::METADATA_MASTER_REQUEST
|
|
2529
|
+
end
|
|
2530
|
+
# Session reads are kept out of the response cache by the caching
|
|
2531
|
+
# middleware, which sees the effective session whichever way it was
|
|
2532
|
+
# supplied. See Parse::Middleware::Caching.cache_session_requests.
|
|
1953
2533
|
opts
|
|
1954
2534
|
end
|
|
1955
2535
|
|
|
@@ -2291,7 +2871,7 @@ module Parse
|
|
|
2291
2871
|
# @return [self]
|
|
2292
2872
|
def scope_to_user(user)
|
|
2293
2873
|
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
|
|
2874
|
+
guard_or_scope_change { @acl_user = user }
|
|
2295
2875
|
self
|
|
2296
2876
|
end
|
|
2297
2877
|
|
|
@@ -2331,7 +2911,7 @@ module Parse
|
|
|
2331
2911
|
# Parse::ACLScope#resolve_for_role only ever sees Parse::Role or
|
|
2332
2912
|
# String. Without normalization, any String-only operation
|
|
2333
2913
|
# (e.g. #start_with?, #sub) silently NoMethodErrors on Symbol.
|
|
2334
|
-
@acl_role = role.is_a?(Symbol) ? role.to_s : role
|
|
2914
|
+
guard_or_scope_change { @acl_role = role.is_a?(Symbol) ? role.to_s : role }
|
|
2335
2915
|
self
|
|
2336
2916
|
end
|
|
2337
2917
|
|
|
@@ -2477,6 +3057,10 @@ module Parse
|
|
|
2477
3057
|
|
|
2478
3058
|
# @!visibility private
|
|
2479
3059
|
def mongo_direct_scope_kwargs
|
|
3060
|
+
enforce_or_branch_scope!
|
|
3061
|
+
# SDK-internal metadata read (see `_opts`): master mode on the direct
|
|
3062
|
+
# path too, whatever `Parse.without_master_key` says.
|
|
3063
|
+
return { master: Parse::ACLScope::METADATA_MASTER } if @_metadata_master == true
|
|
2480
3064
|
if @acl_user
|
|
2481
3065
|
# Pre-resolved User pointer. Hand it to Parse::ACLScope as
|
|
2482
3066
|
# acl_user: so the same three-layer simulation runs (top-level
|
|
@@ -2544,9 +3128,10 @@ module Parse
|
|
|
2544
3128
|
# @return [Boolean]
|
|
2545
3129
|
# @!visibility private
|
|
2546
3130
|
def mongo_direct_master_posture?
|
|
2547
|
-
# `Parse.without_master_key`
|
|
2548
|
-
#
|
|
2549
|
-
|
|
3131
|
+
# Inside `Parse.without_master_key` this still answers true: the
|
|
3132
|
+
# resulting `master: true` is dropped by Parse::ACLScope.resolve!,
|
|
3133
|
+
# which runs the read in the public scope (or raises under
|
|
3134
|
+
# `require_session_token`) with the block-specific message.
|
|
2550
3135
|
c = begin
|
|
2551
3136
|
client
|
|
2552
3137
|
rescue StandardError
|
|
@@ -2566,15 +3151,6 @@ module Parse
|
|
|
2566
3151
|
Parse.respond_to?(:master_key_disabled?) && Parse.master_key_disabled?
|
|
2567
3152
|
end
|
|
2568
3153
|
|
|
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
3154
|
# Auth kwargs for the Atlas Search bridge (`#atlas_search` builder
|
|
2579
3155
|
# block). Explicit `atlas_search(...)` auth kwargs win; otherwise
|
|
2580
3156
|
# derive from the query's own scope (`#scope_to_user`, an explicit
|
|
@@ -2606,9 +3182,11 @@ module Parse
|
|
|
2606
3182
|
end
|
|
2607
3183
|
|
|
2608
3184
|
explicit = %i[session_token master acl_user acl_role].select { |k| options.key?(k) }
|
|
2609
|
-
if explicit.any?
|
|
3185
|
+
if explicit.any? || options.key?(:client)
|
|
2610
3186
|
given = explicit.to_h { |k| [k, options[k]] }
|
|
2611
|
-
|
|
3187
|
+
enforce_explicit_auth_against_or_scope!(client: options[:client], **given)
|
|
3188
|
+
end
|
|
3189
|
+
if explicit.any?
|
|
2612
3190
|
return client_kwarg.merge(given)
|
|
2613
3191
|
end
|
|
2614
3192
|
|
|
@@ -2617,6 +3195,7 @@ module Parse
|
|
|
2617
3195
|
|
|
2618
3196
|
# @!visibility private
|
|
2619
3197
|
def atlas_search_scope_kwargs
|
|
3198
|
+
enforce_or_branch_scope!
|
|
2620
3199
|
if @acl_user
|
|
2621
3200
|
{ acl_user: @acl_user }
|
|
2622
3201
|
elsif @acl_role
|
|
@@ -2625,9 +3204,9 @@ module Parse
|
|
|
2625
3204
|
{ session_token: @session_token }
|
|
2626
3205
|
elsif use_master_key == true
|
|
2627
3206
|
# 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
|
-
|
|
3207
|
+
# Inside `Parse.without_master_key` Parse::AtlasSearch drops it and
|
|
3208
|
+
# the search runs in the public scope.
|
|
3209
|
+
{ master: true }
|
|
2631
3210
|
elsif (ambient = ambient_session_token)
|
|
2632
3211
|
{ session_token: ambient }
|
|
2633
3212
|
elsif anonymous_session_block?
|
|
@@ -2729,6 +3308,8 @@ module Parse
|
|
|
2729
3308
|
# @note This is a read-only operation. Direct MongoDB queries cannot modify data.
|
|
2730
3309
|
# @see Parse::MongoDB.configure
|
|
2731
3310
|
def results_direct(raw: false, max_time_ms: nil, session_token: nil, master: nil, acl_user: nil, acl_role: nil, client: nil, &block)
|
|
3311
|
+
enforce_explicit_auth_against_or_scope!(session_token: session_token, master: master,
|
|
3312
|
+
acl_user: acl_user, acl_role: acl_role, client: client)
|
|
2732
3313
|
# `limit(0)` asks for no rows. MongoDB rejects `$limit: 0`, and
|
|
2733
3314
|
# omitting the stage would return every row.
|
|
2734
3315
|
return [] if @limit == 0
|
|
@@ -2770,7 +3351,6 @@ module Parse
|
|
|
2770
3351
|
acl_user = auth[:acl_user]
|
|
2771
3352
|
acl_role = auth[:acl_role]
|
|
2772
3353
|
end
|
|
2773
|
-
master = direct_master_kwarg(master)
|
|
2774
3354
|
|
|
2775
3355
|
# Execute the aggregation directly on MongoDB. The pipeline was built
|
|
2776
3356
|
# entirely from SDK constraint translation (no user-supplied stages),
|
|
@@ -2878,6 +3458,8 @@ module Parse
|
|
|
2878
3458
|
# @note This is a read-only operation. Direct MongoDB queries cannot modify data.
|
|
2879
3459
|
# @see Parse::MongoDB.configure
|
|
2880
3460
|
def count_direct(session_token: nil, master: nil, acl_user: nil, acl_role: nil, client: nil)
|
|
3461
|
+
enforce_explicit_auth_against_or_scope!(session_token: session_token, master: master,
|
|
3462
|
+
acl_user: acl_user, acl_role: acl_role, client: client)
|
|
2881
3463
|
require_relative "mongodb"
|
|
2882
3464
|
Parse::MongoDB.require_gem!
|
|
2883
3465
|
|
|
@@ -2916,7 +3498,6 @@ module Parse
|
|
|
2916
3498
|
acl_user = auth[:acl_user]
|
|
2917
3499
|
acl_role = auth[:acl_role]
|
|
2918
3500
|
end
|
|
2919
|
-
master = direct_master_kwarg(master)
|
|
2920
3501
|
|
|
2921
3502
|
# SDK-built pipeline only — see results_direct for rationale.
|
|
2922
3503
|
# ACL simulation runs inside Parse::MongoDB.aggregate when
|
|
@@ -2957,6 +3538,8 @@ module Parse
|
|
|
2957
3538
|
def distinct_direct(field, return_pointers: false, order: nil,
|
|
2958
3539
|
session_token: nil, master: nil, acl_user: nil, acl_role: nil,
|
|
2959
3540
|
client: nil)
|
|
3541
|
+
enforce_explicit_auth_against_or_scope!(session_token: session_token, master: master,
|
|
3542
|
+
acl_user: acl_user, acl_role: acl_role, client: client)
|
|
2960
3543
|
require_relative "mongodb"
|
|
2961
3544
|
Parse::MongoDB.require_gem!
|
|
2962
3545
|
|
|
@@ -3017,7 +3600,6 @@ module Parse
|
|
|
3017
3600
|
acl_user = auth[:acl_user]
|
|
3018
3601
|
acl_role = auth[:acl_role]
|
|
3019
3602
|
end
|
|
3020
|
-
master = direct_master_kwarg(master)
|
|
3021
3603
|
raw_results = Parse::MongoDB.aggregate(@table, pipeline,
|
|
3022
3604
|
allow_internal_fields: true,
|
|
3023
3605
|
read_preference: @read_preference,
|
|
@@ -4255,11 +4837,11 @@ module Parse
|
|
|
4255
4837
|
lq_client = client || Parse::LiveQuery.client
|
|
4256
4838
|
lq_client.subscribe(
|
|
4257
4839
|
@table,
|
|
4258
|
-
where:
|
|
4840
|
+
where: compile_rest_where,
|
|
4259
4841
|
fields: fields,
|
|
4260
4842
|
keys: keys,
|
|
4261
4843
|
watch: watch,
|
|
4262
|
-
session_token: session_token
|
|
4844
|
+
session_token: live_query_session_token(session_token, live_query_client: lq_client),
|
|
4263
4845
|
use_master_key: use_master_key,
|
|
4264
4846
|
&block
|
|
4265
4847
|
)
|
|
@@ -5145,7 +5727,12 @@ module Parse
|
|
|
5145
5727
|
# Keys like ["project.name", "project.status"] define which subfields to fetch on nested objects
|
|
5146
5728
|
nested_keys = Parse::Query.parse_keys_to_nested_keys(@keys) if @keys.present?
|
|
5147
5729
|
|
|
5148
|
-
list.map { |m| Parse::Object.build(m, @table, fetched_keys: fetch_keys, nested_fetched_keys: nested_keys) }.compact
|
|
5730
|
+
built = list.map { |m| Parse::Object.build(m, @table, fetched_keys: fetch_keys, nested_fetched_keys: nested_keys) }.compact
|
|
5731
|
+
# Record session owners against the client that fetched them, so a
|
|
5732
|
+
# later delete of an already-gone session can still drop its owner's
|
|
5733
|
+
# cached identities.
|
|
5734
|
+
Parse::Session._remember_owners!(built, client) if @table == Parse::Model::CLASS_SESSION && defined?(Parse::Session)
|
|
5735
|
+
built
|
|
5149
5736
|
end
|
|
5150
5737
|
|
|
5151
5738
|
# Validates includes against keys and field types, printing debug warnings for:
|
|
@@ -5295,7 +5882,7 @@ module Parse
|
|
|
5295
5882
|
q[:excludeKeys] = @exclude_keys.join(",") if encode && @exclude_keys&.any?
|
|
5296
5883
|
q[:order] = @order.join(",") unless @order.empty?
|
|
5297
5884
|
unless @where.empty?
|
|
5298
|
-
q[:where] = Parse::Query.compile_where(@where)
|
|
5885
|
+
q[:where] = coerce_rest_pointer_ids(Parse::Query.compile_where(@where))
|
|
5299
5886
|
q[:where] = q[:where].to_json if encode
|
|
5300
5887
|
end
|
|
5301
5888
|
|
|
@@ -5328,6 +5915,94 @@ module Parse
|
|
|
5328
5915
|
self.class.compile_where(@where || [])
|
|
5329
5916
|
end
|
|
5330
5917
|
|
|
5918
|
+
# @return [Hash] the where clause as sent to Parse Server over REST or
|
|
5919
|
+
# LiveQuery: {#compile_where} with bare objectIds on declared pointer
|
|
5920
|
+
# fields rewritten into Pointer hashes, so they match the stored
|
|
5921
|
+
# pointer.
|
|
5922
|
+
def compile_rest_where
|
|
5923
|
+
refuse_pipeline_only!("a LiveQuery subscription or push target")
|
|
5924
|
+
coerce_rest_pointer_ids(compile_where)
|
|
5925
|
+
end
|
|
5926
|
+
|
|
5927
|
+
# @!visibility private
|
|
5928
|
+
# Compile this query as a subquery of another (`$inQuery`,
|
|
5929
|
+
# `$notInQuery`, `$select`, `$dontSelect`). Parse Server evaluates it
|
|
5930
|
+
# from the REST form, so a pipeline-only constraint, which that form
|
|
5931
|
+
# drops, is refused rather than turning the subquery into match-all.
|
|
5932
|
+
# @return [Hash]
|
|
5933
|
+
def compile_subquery
|
|
5934
|
+
refuse_pipeline_only!("a subquery")
|
|
5935
|
+
enforce_or_branch_scope!
|
|
5936
|
+
compile(encode: false, includeClassName: true)
|
|
5937
|
+
end
|
|
5938
|
+
|
|
5939
|
+
# @!visibility private
|
|
5940
|
+
# Raise when this query holds a constraint that only the aggregation
|
|
5941
|
+
# pipeline or mongo-direct can run, for a target that receives only
|
|
5942
|
+
# the REST where form.
|
|
5943
|
+
def refuse_pipeline_only!(target)
|
|
5944
|
+
markers = compile_markers
|
|
5945
|
+
return unless Parse::Query.or_branch_marker?(markers)
|
|
5946
|
+
names = Parse::Query.pipeline_only_constraint_names(@where)
|
|
5947
|
+
raise ArgumentError,
|
|
5948
|
+
"This query cannot be used as #{target}: " \
|
|
5949
|
+
"#{names.empty? ? "a pipeline-only constraint" : names.join(", ")} needs the aggregation " \
|
|
5950
|
+
"pipeline or mongo-direct, and Parse Server receives only the REST where form, " \
|
|
5951
|
+
"which would drop it."
|
|
5952
|
+
end
|
|
5953
|
+
private :refuse_pipeline_only!
|
|
5954
|
+
|
|
5955
|
+
# @!visibility private
|
|
5956
|
+
# The session a LiveQuery subscription built from this query must use:
|
|
5957
|
+
# the query's own session token or the session of its `become` client.
|
|
5958
|
+
# LiveQuery has no equivalent of `scope_to_user` / `scope_to_role`, so
|
|
5959
|
+
# those raise, and an explicit token that differs from the query's
|
|
5960
|
+
# authority raises too.
|
|
5961
|
+
#
|
|
5962
|
+
# A query that carries any authority must target the same Parse
|
|
5963
|
+
# application as the LiveQuery client, compared by application id, so
|
|
5964
|
+
# one app's session token is never sent to another app's LiveQuery
|
|
5965
|
+
# server. A query with the default authority sends no query-derived
|
|
5966
|
+
# credential, so it is not checked.
|
|
5967
|
+
# @param explicit [String, nil] a `session_token:` passed by the caller.
|
|
5968
|
+
# @param live_query_client [Parse::LiveQuery::Client, nil] the client
|
|
5969
|
+
# the subscription will be sent on.
|
|
5970
|
+
# @return [String, nil]
|
|
5971
|
+
def live_query_session_token(explicit = nil, live_query_client: nil)
|
|
5972
|
+
enforce_or_branch_scope!
|
|
5973
|
+
scope = or_effective_scope
|
|
5974
|
+
if scope.key?(:user) || scope.key?(:role)
|
|
5975
|
+
raise ArgumentError,
|
|
5976
|
+
"A query scoped with scope_to_user / scope_to_role cannot be subscribed to: " \
|
|
5977
|
+
"LiveQuery authorizes by session token only. Subscribe with that user's session instead."
|
|
5978
|
+
end
|
|
5979
|
+
explicit = explicit.session_token if explicit.respond_to?(:session_token)
|
|
5980
|
+
own = scope[:session]
|
|
5981
|
+
if explicit.is_a?(String) && !explicit.empty? && own && explicit != own
|
|
5982
|
+
raise ArgumentError,
|
|
5983
|
+
"The session_token: passed to subscribe differs from the session this query runs under."
|
|
5984
|
+
end
|
|
5985
|
+
ensure_live_query_application!(live_query_client) unless scope.empty?
|
|
5986
|
+
(explicit.is_a?(String) && !explicit.empty?) ? explicit : own
|
|
5987
|
+
end
|
|
5988
|
+
|
|
5989
|
+
# @!visibility private
|
|
5990
|
+
# Raise when a LiveQuery client targets a different Parse application
|
|
5991
|
+
# than this query, so a credential bound to one app never reaches
|
|
5992
|
+
# another. Compared by application id: the LiveQuery URL is a WebSocket
|
|
5993
|
+
# endpoint and never matches the REST server URL.
|
|
5994
|
+
def ensure_live_query_application!(live_query_client)
|
|
5995
|
+
return if live_query_client.nil? || !live_query_client.respond_to?(:application_id)
|
|
5996
|
+
query_app = or_application_identity&.first
|
|
5997
|
+
lq_app = live_query_client.application_id.to_s
|
|
5998
|
+
return if query_app.nil? || query_app.empty? || lq_app.empty? || query_app == lq_app
|
|
5999
|
+
raise ArgumentError,
|
|
6000
|
+
"This query runs under an authority bound to application #{query_app.inspect}, but the " \
|
|
6001
|
+
"LiveQuery client targets #{lq_app.inspect}. Its credentials are not sent to another " \
|
|
6002
|
+
"application; subscribe on a LiveQuery client for the same application."
|
|
6003
|
+
end
|
|
6004
|
+
private :ensure_live_query_application!
|
|
6005
|
+
|
|
5331
6006
|
# @return [Hash] the un-stripped reduced where hash, including any
|
|
5332
6007
|
# SDK-internal markers like `"__mongo_direct_only"` and
|
|
5333
6008
|
# `"__aggregation_pipeline"`. Used by the routing layer to decide
|
|
@@ -6168,6 +6843,56 @@ module Parse
|
|
|
6168
6843
|
end
|
|
6169
6844
|
end
|
|
6170
6845
|
|
|
6846
|
+
# @!visibility private
|
|
6847
|
+
# Rewrite bare objectId strings compared to a declared pointer column
|
|
6848
|
+
# into Pointer hashes, for the REST where clause. Parse Server compares
|
|
6849
|
+
# a pointer column against its "Class$objectId" storage form, so a bare
|
|
6850
|
+
# objectId never matched (and `$ne` matched every row). Same rules as
|
|
6851
|
+
# {#coerce_bare_pointer_ids}: only declared pointers with a known target
|
|
6852
|
+
# class, only `$eq` / `$ne` / `$in` / `$nin` / `$all` and plain
|
|
6853
|
+
# equality, recursing into `$or` / `$and` / `$nor`.
|
|
6854
|
+
# @param where [Hash] a compiled where clause.
|
|
6855
|
+
# @return [Hash]
|
|
6856
|
+
def coerce_rest_pointer_ids(where)
|
|
6857
|
+
return where unless where.is_a?(Hash) && !where.empty?
|
|
6858
|
+
klass = table_model_class
|
|
6859
|
+
return where unless klass
|
|
6860
|
+
where.each_with_object({}) do |(field, value), out|
|
|
6861
|
+
name = field.to_s
|
|
6862
|
+
out[field] = if %w[$or $and $nor].include?(name) && value.is_a?(Array)
|
|
6863
|
+
value.map { |branch| coerce_rest_pointer_ids(branch) }
|
|
6864
|
+
elsif name.start_with?("$") || name.include?(".")
|
|
6865
|
+
value
|
|
6866
|
+
elsif (target = get_pointer_target_class_for(klass, name))
|
|
6867
|
+
rest_pointer_value(target, value)
|
|
6868
|
+
else
|
|
6869
|
+
value
|
|
6870
|
+
end
|
|
6871
|
+
end
|
|
6872
|
+
end
|
|
6873
|
+
|
|
6874
|
+
# @!visibility private
|
|
6875
|
+
def rest_pointer_value(target, value)
|
|
6876
|
+
bare = ->(v) { v.is_a?(String) && !v.empty? && !v.include?("$") }
|
|
6877
|
+
to_pointer = ->(v) { bare.(v) ? { "__type" => "Pointer", "className" => target, "objectId" => v } : v }
|
|
6878
|
+
case value
|
|
6879
|
+
when String
|
|
6880
|
+
to_pointer.(value)
|
|
6881
|
+
when Hash
|
|
6882
|
+
return value if value.key?("__type") || value.key?(:__type)
|
|
6883
|
+
value.each_with_object({}) do |(op, op_value), out|
|
|
6884
|
+
out[op] = case op.to_s
|
|
6885
|
+
when "$eq", "$ne" then to_pointer.(op_value)
|
|
6886
|
+
when "$in", "$nin", "$all"
|
|
6887
|
+
op_value.is_a?(Array) ? op_value.map(&to_pointer) : op_value
|
|
6888
|
+
else op_value
|
|
6889
|
+
end
|
|
6890
|
+
end
|
|
6891
|
+
else
|
|
6892
|
+
value
|
|
6893
|
+
end
|
|
6894
|
+
end
|
|
6895
|
+
|
|
6171
6896
|
# Handle a constraint value that is a bare String inside `$in`/`$nin`
|
|
6172
6897
|
# against a column positively identified as a pointer, when the
|
|
6173
6898
|
# target class cannot be resolved (no local belongs_to AND no peer
|
|
@@ -6571,20 +7296,25 @@ module Parse
|
|
|
6571
7296
|
raise ArgumentError, "All queries passed to Parse::Query.or must be for the same Parse class."
|
|
6572
7297
|
end
|
|
6573
7298
|
|
|
6574
|
-
# Start with an empty query for this table
|
|
7299
|
+
# Start with an empty query for this table, carrying the members'
|
|
7300
|
+
# common auth scope (raises when two members set different scopes).
|
|
6575
7301
|
result = self.new(table)
|
|
7302
|
+
# Run against the members' application: seed the client from the
|
|
7303
|
+
# first member that sets one (the members are then checked to agree).
|
|
7304
|
+
seed = queries.lazy.map { |q| q.send(:or_application_client) }.find(&:itself)
|
|
7305
|
+
result.instance_variable_set(:@client, seed) if seed
|
|
7306
|
+
queries.each { |query| result.send(:merge_or_scope!, query) }
|
|
6576
7307
|
|
|
6577
|
-
#
|
|
6578
|
-
|
|
7308
|
+
# Refuse pipeline-only constraints in any member first, so one is
|
|
7309
|
+
# never dropped or widened.
|
|
7310
|
+
queries.each { |query| compile_or_branch(query.where) }
|
|
7311
|
+
|
|
7312
|
+
# A query with no constraints matches every row, so an OR that
|
|
7313
|
+
# includes one does too.
|
|
7314
|
+
return result if queries.any? { |q| match_all?(q.where) }
|
|
6579
7315
|
|
|
6580
7316
|
# 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
|
|
7317
|
+
queries.each { |query| result.or_where(query.where) }
|
|
6588
7318
|
|
|
6589
7319
|
result
|
|
6590
7320
|
end
|
|
@@ -6631,13 +7361,19 @@ module Parse
|
|
|
6631
7361
|
|
|
6632
7362
|
# Creates a deep copy of this query object, allowing independent modifications
|
|
6633
7363
|
# @return [Parse::Query] a new query object with the same constraints
|
|
6634
|
-
# @note The @
|
|
6635
|
-
#
|
|
7364
|
+
# @note The @results instance variable is not cloned. A client that sets
|
|
7365
|
+
# the query's authority (session-bound or without a master key) is kept
|
|
7366
|
+
# by reference; otherwise the copy uses the default client.
|
|
6636
7367
|
def clone
|
|
6637
7368
|
cloned_query = Parse::Query.new(self.instance_variable_get(:@table))
|
|
6638
7369
|
# Note: :client is intentionally excluded - it contains non-serializable objects
|
|
6639
7370
|
# (Redis connections, Faraday connections) and should be obtained lazily
|
|
6640
|
-
|
|
7371
|
+
# The auth scope and read routing travel with the copy, so a cloned or
|
|
7372
|
+
# combined query never runs under a broader authority than the
|
|
7373
|
+
# original.
|
|
7374
|
+
[:count, :where, :order, :keys, :exclude_keys, :includes, :limit, :skip, :cache, :use_master_key, :hint,
|
|
7375
|
+
:session_token, :acl_user, :acl_role, :read_preference, :key, :acl_query_mongo_direct,
|
|
7376
|
+
:_or_branch_scope, :_or_branch_app].each do |param|
|
|
6641
7377
|
if instance_variable_defined?(:"@#{param}")
|
|
6642
7378
|
value = instance_variable_get(:"@#{param}")
|
|
6643
7379
|
if value.is_a?(Array) || value.is_a?(Hash)
|
|
@@ -6655,6 +7391,12 @@ module Parse
|
|
|
6655
7391
|
cloned_query.instance_variable_set(:"@#{param}", cloned_value)
|
|
6656
7392
|
end
|
|
6657
7393
|
end
|
|
7394
|
+
# A client set on the query (one from Parse::Client#become or
|
|
7395
|
+
# #anonymous, or a client for another application) decides where the
|
|
7396
|
+
# query's session token and authority are sent, so the copy keeps it.
|
|
7397
|
+
# Only the memoized default client is resolved lazily again.
|
|
7398
|
+
own_client = or_application_client
|
|
7399
|
+
cloned_query.instance_variable_set(:@client, own_client) if own_client
|
|
6658
7400
|
cloned_query.instance_variable_set(:@results, nil)
|
|
6659
7401
|
cloned_query
|
|
6660
7402
|
end
|