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/webhooks.rb
CHANGED
|
@@ -501,6 +501,8 @@ module Parse
|
|
|
501
501
|
master: payload.master? || false,
|
|
502
502
|
is_new: payload.original.blank?,
|
|
503
503
|
)
|
|
504
|
+
# A guard revert is not the handler assigning the ACL.
|
|
505
|
+
pre_obj.reset_webhook_handler_acl_assigned! if pre_obj.respond_to?(:reset_webhook_handler_acl_assigned!)
|
|
504
506
|
end
|
|
505
507
|
end
|
|
506
508
|
end
|
|
@@ -529,6 +531,7 @@ module Parse
|
|
|
529
531
|
# ran ActiveModel before_save callbacks locally. A client-spoofed
|
|
530
532
|
# `_RB_` without master falls through and runs them here.
|
|
531
533
|
unless trusted_ruby_initiated
|
|
534
|
+
adopt_request_user_as_acl_owner!(payload, result)
|
|
532
535
|
before_save_result = result.run_before_save_callbacks
|
|
533
536
|
# If a before_save callback halted the chain (returned false), reject the save.
|
|
534
537
|
if before_save_result == false
|
|
@@ -779,18 +782,23 @@ module Parse
|
|
|
779
782
|
#
|
|
780
783
|
# Fields are compared against a fresh build of the payload, so only the
|
|
781
784
|
# fields the handler actually changed are encoded from the Ruby object.
|
|
782
|
-
#
|
|
783
|
-
# sub-key (`"meta.count"`) reaches the webhook only
|
|
784
|
-
# sub-document,
|
|
785
|
-
#
|
|
786
|
-
#
|
|
785
|
+
# A field the handler rewrites is written as an absolute value. An
|
|
786
|
+
# operator on a dotted sub-key (`"meta.count"`) reaches the webhook only
|
|
787
|
+
# as its resulting sub-document, so on an update a changed sub-document
|
|
788
|
+
# is written back as dotted keys for just the sub-keys that differ from
|
|
789
|
+
# the stored one. A concurrent write to another sub-key survives; the
|
|
790
|
+
# changed sub-key itself is written as its resulting value. The split
|
|
791
|
+
# is one level deep (a changed sub-key is written whole at
|
|
792
|
+
# `field.sub`), and a field whose writes would carry a typed value is
|
|
793
|
+
# written whole.
|
|
787
794
|
#
|
|
788
795
|
# @param payload [Parse::Webhooks::Payload] the beforeSave payload.
|
|
789
796
|
# @param obj [Parse::Object, nil] the handler's object (nil when none).
|
|
790
797
|
# @param overrides [Hash, nil] field values a handler returned as a Hash.
|
|
791
798
|
# @param include_create_defaults [Boolean] on a create, also write the
|
|
792
|
-
# object's dirty fields the client did not send (declared defaults
|
|
793
|
-
#
|
|
799
|
+
# object's dirty fields the client did not send (declared defaults and
|
|
800
|
+
# the ACL the class policy resolves), matching what an SDK-side create
|
|
801
|
+
# sends.
|
|
794
802
|
# @return [Hash, nil] the reply object, or nil for "unchanged".
|
|
795
803
|
def before_save_reply(payload, obj, overrides: nil, include_create_defaults: false)
|
|
796
804
|
return nil unless payload && payload.object?
|
|
@@ -806,27 +814,238 @@ module Parse
|
|
|
806
814
|
if include_create_defaults && raw_original.nil?
|
|
807
815
|
client_keys = raw_object.keys.map(&:to_s)
|
|
808
816
|
dirty = obj.changed.map(&:to_sym) & snapshot_fields(obj)
|
|
817
|
+
# This includes the ACL. A create with no `ACL` key is stored by
|
|
818
|
+
# Parse Server as public read and write, so a class with an ACL
|
|
819
|
+
# policy always replies with the ACL that policy resolved (owned
|
|
820
|
+
# by the requesting user where the policy names an owner; see
|
|
821
|
+
# {adopt_request_user_as_acl_owner!}) or the ACL the handler set.
|
|
822
|
+
# An ACL the client sent is kept as sent.
|
|
809
823
|
wire_values(obj, dirty).each do |remote, value|
|
|
810
824
|
next if client_keys.include?(remote) || changes.key?(remote)
|
|
811
825
|
changes[remote] = value
|
|
812
826
|
end
|
|
813
827
|
end
|
|
828
|
+
# An ACL the handler assigned on a create is written even when it
|
|
829
|
+
# equals the default stamp or the client's ACL, whatever the handler
|
|
830
|
+
# returned (the object, `true`, `nil`, or a Hash, whose own `ACL`
|
|
831
|
+
# still wins below). Diffing alone misses `obj.acl = Parse::ACL.new`
|
|
832
|
+
# under a `{}` default, and a create with no `ACL` key is stored
|
|
833
|
+
# public read and write.
|
|
834
|
+
if handler_assigned_acl?(obj) && !changes.key?("ACL")
|
|
835
|
+
changes.merge!(wire_values(obj, [:acl]).slice("ACL"))
|
|
836
|
+
end
|
|
814
837
|
end
|
|
815
838
|
overrides = overrides.as_json if overrides.is_a?(Hash)
|
|
816
839
|
return nil if changes.empty? && drops.empty? && overrides.blank?
|
|
817
840
|
|
|
818
841
|
reply = client_write_data(raw_object, raw_original)
|
|
842
|
+
overrides = overrides.present? ? remote_override_keys(payload, overrides) : {}
|
|
843
|
+
# A field the handler wrote or dropped replaces the client's dotted
|
|
844
|
+
# sub-key writes for it; MongoDB refuses `meta` and `meta.x` together.
|
|
845
|
+
(drops + changes.keys + overrides.keys).each do |remote|
|
|
846
|
+
reply.delete_if { |key, _| key.start_with?("#{remote}.") }
|
|
847
|
+
end
|
|
819
848
|
drops.each { |remote| reply.delete(remote) }
|
|
820
849
|
reply.merge!(changes)
|
|
821
|
-
reply.merge!(overrides
|
|
850
|
+
reply.merge!(overrides)
|
|
851
|
+
fold_dotted_overrides!(reply, overrides.keys, raw_object: raw_object, create: raw_original.nil?)
|
|
852
|
+
reply
|
|
853
|
+
end
|
|
854
|
+
|
|
855
|
+
# A handler's Hash override keyed by Ruby names (`{acl: ...}`,
|
|
856
|
+
# `{author_name: ...}`) is mapped to the remote field names the reply
|
|
857
|
+
# uses, so it replaces the matching field instead of sitting beside it.
|
|
858
|
+
# A dotted key maps its first segment.
|
|
859
|
+
# @!visibility private
|
|
860
|
+
def remote_override_keys(payload, overrides)
|
|
861
|
+
klass = begin
|
|
862
|
+
name = payload.parse_class
|
|
863
|
+
name.present? ? Parse::Object.find_class(name) : nil
|
|
864
|
+
rescue StandardError
|
|
865
|
+
nil
|
|
866
|
+
end
|
|
867
|
+
map = klass.respond_to?(:field_map) ? klass.field_map : {}
|
|
868
|
+
overrides.each_with_object({}) do |(key, value), out|
|
|
869
|
+
key = key.to_s
|
|
870
|
+
head, rest = key.split(".", 2)
|
|
871
|
+
remote = map[head.to_sym]
|
|
872
|
+
head = remote.to_s if remote
|
|
873
|
+
out[rest ? "#{head}.#{rest}" : head] = value
|
|
874
|
+
end
|
|
875
|
+
end
|
|
876
|
+
|
|
877
|
+
# A handler's Hash override may name a sub-key (`"meta.y"`) of a field
|
|
878
|
+
# the reply writes whole (a create, or a field written whole on an
|
|
879
|
+
# update). MongoDB refuses `meta` and `meta.y` in one update, so the
|
|
880
|
+
# sub-key write is folded into the whole value. An override deeper
|
|
881
|
+
# than one level (`"meta.count.value"`) is folded into a `field.sub`
|
|
882
|
+
# write seeded from the pending object, because Parse Server rebuilds
|
|
883
|
+
# the afterSave object from a dotted key only one level deep. On a
|
|
884
|
+
# create every dotted override folds into its whole field.
|
|
885
|
+
#
|
|
886
|
+
# When the value it folds into is not a plain sub-document, a client's
|
|
887
|
+
# value is replaced by the handler's sub-key, and two of the handler's
|
|
888
|
+
# own overrides that conflict (`"meta.a" => 5` with `"meta.a.b" => 6`)
|
|
889
|
+
# raise {ResponseError}.
|
|
890
|
+
# @!visibility private
|
|
891
|
+
def fold_dotted_overrides!(reply, override_keys, raw_object: {}, create: false)
|
|
892
|
+
handler_keys = override_keys.to_set
|
|
893
|
+
override_keys.sort_by { |k| k.count(".") }.each do |key|
|
|
894
|
+
next unless key.include?(".") && reply.key?(key)
|
|
895
|
+
segments = key.split(".")
|
|
896
|
+
# The nearest ancestor path the reply writes whole, if any.
|
|
897
|
+
parent = (1...segments.length).map { |n| segments.first(n).join(".") }.reverse.find { |p| reply.key?(p) }
|
|
898
|
+
unless parent
|
|
899
|
+
next if !create && segments.length <= 2
|
|
900
|
+
parent = create ? segments.first : segments.first(2).join(".")
|
|
901
|
+
seed = parent.split(".").reduce(raw_object) { |cur, seg| cur.is_a?(Hash) ? cur[seg] : nil }
|
|
902
|
+
reply[parent] = plain_sub_document?(seed) ? seed.deep_dup : {}
|
|
903
|
+
end
|
|
904
|
+
value = reply.delete(key)
|
|
905
|
+
whole = reply[parent]
|
|
906
|
+
if plain_sub_document?(whole)
|
|
907
|
+
whole = whole.deep_dup
|
|
908
|
+
elsif handler_keys.include?(parent)
|
|
909
|
+
raise Parse::Webhooks::ResponseError,
|
|
910
|
+
"before_save reply: #{key} conflicts with #{parent} in the handler's reply"
|
|
911
|
+
else
|
|
912
|
+
whole = {}
|
|
913
|
+
end
|
|
914
|
+
*dirs, leaf = segments.drop(parent.count(".") + 1)
|
|
915
|
+
node = dirs.reduce(whole) do |cur, dir|
|
|
916
|
+
cur[dir] = {} unless plain_sub_document?(cur[dir])
|
|
917
|
+
cur[dir]
|
|
918
|
+
end
|
|
919
|
+
if value.is_a?(Hash) && value.key?("__op")
|
|
920
|
+
# A whole write stores its value as data, so an operator folded
|
|
921
|
+
# into it is applied here rather than written as a Hash.
|
|
922
|
+
result = fold_override_op(key, node[leaf], value)
|
|
923
|
+
if result.equal?(FOLD_DELETE)
|
|
924
|
+
node.delete(leaf)
|
|
925
|
+
else
|
|
926
|
+
node[leaf] = result
|
|
927
|
+
end
|
|
928
|
+
else
|
|
929
|
+
node[leaf] = value
|
|
930
|
+
end
|
|
931
|
+
reply[parent] = whole
|
|
932
|
+
end
|
|
822
933
|
reply
|
|
823
934
|
end
|
|
824
935
|
|
|
936
|
+
# @!visibility private
|
|
937
|
+
FOLD_DELETE = Object.new.freeze
|
|
938
|
+
|
|
939
|
+
# Apply a handler's sub-key operator to the value it replaces inside a
|
|
940
|
+
# sub-document the reply writes whole, with Parse Server's semantics
|
|
941
|
+
# for that operator. Raises {ResponseError} (an `{error}` reply, so
|
|
942
|
+
# Parse Server refuses the save) for an operator that cannot be applied
|
|
943
|
+
# to a plain value, rather than storing the operator Hash as data.
|
|
944
|
+
# @param key [String] the dotted override key, for the error message.
|
|
945
|
+
# @param current [Object] the value at that path in the whole write.
|
|
946
|
+
# @param op [Hash] the operator Hash (`{"__op" => ...}`).
|
|
947
|
+
# @return [Object] the new value, or {FOLD_DELETE} to remove the key.
|
|
948
|
+
# @!visibility private
|
|
949
|
+
def fold_override_op(key, current, op)
|
|
950
|
+
name = op["__op"]
|
|
951
|
+
case name
|
|
952
|
+
when "Delete"
|
|
953
|
+
FOLD_DELETE
|
|
954
|
+
when "Increment"
|
|
955
|
+
amount = op["amount"]
|
|
956
|
+
unless amount.is_a?(Numeric) && (current.nil? || current.is_a?(Numeric))
|
|
957
|
+
raise Parse::Webhooks::ResponseError,
|
|
958
|
+
"before_save reply: cannot apply Increment to #{key} (#{current.class})"
|
|
959
|
+
end
|
|
960
|
+
(current || 0) + amount
|
|
961
|
+
when "Add", "AddUnique", "Remove"
|
|
962
|
+
objects = op["objects"]
|
|
963
|
+
unless objects.is_a?(Array) && (current.nil? || current.is_a?(Array))
|
|
964
|
+
raise Parse::Webhooks::ResponseError,
|
|
965
|
+
"before_save reply: cannot apply #{name} to #{key} (#{current.class})"
|
|
966
|
+
end
|
|
967
|
+
base = current || []
|
|
968
|
+
case name
|
|
969
|
+
when "Add" then base + objects
|
|
970
|
+
when "AddUnique" then base + objects.reject { |o| base.include?(o) }.uniq
|
|
971
|
+
else base.reject { |o| objects.include?(o) }
|
|
972
|
+
end
|
|
973
|
+
else
|
|
974
|
+
raise Parse::Webhooks::ResponseError,
|
|
975
|
+
"before_save reply: operator #{name.inspect} on #{key} cannot be combined " \
|
|
976
|
+
"with a whole write of its parent field"
|
|
977
|
+
end
|
|
978
|
+
end
|
|
979
|
+
|
|
980
|
+
# @!visibility private
|
|
981
|
+
def handler_assigned_acl?(obj)
|
|
982
|
+
return true if obj.instance_variable_get(:@_webhook_reply_acl) == true
|
|
983
|
+
obj.respond_to?(:webhook_handler_acl_assigned?) && obj.webhook_handler_acl_assigned?
|
|
984
|
+
end
|
|
985
|
+
|
|
986
|
+
# On a client create, an owner-based ACL policy (`:owner_else_private`,
|
|
987
|
+
# the shipped default, `:owner_else_public`, `:owner_but_public_read`)
|
|
988
|
+
# resolves its owner the way an SDK create `as:` the requesting user
|
|
989
|
+
# does: when the declared owner field holds no value, the user who made
|
|
990
|
+
# the request owns the record. A request without a user (anonymous, or
|
|
991
|
+
# master key) gets the policy's fallback. Leaves the object alone when
|
|
992
|
+
# the handler or the client already set an ACL.
|
|
993
|
+
# @!visibility private
|
|
994
|
+
def adopt_request_user_as_acl_owner!(payload, obj)
|
|
995
|
+
return unless payload && payload.original.nil?
|
|
996
|
+
if handler_assigned_acl?(obj)
|
|
997
|
+
# The handler's ACL is final: keep the save-time policy resolver
|
|
998
|
+
# from replacing it, even when it equals the default stamp.
|
|
999
|
+
obj.instance_variable_set(:@_acl_pristine, false)
|
|
1000
|
+
return
|
|
1001
|
+
end
|
|
1002
|
+
return unless obj.is_a?(Parse::Object) && obj.instance_variable_get(:@_acl_pristine)
|
|
1003
|
+
return if obj.instance_variable_get(:@_acl_owner_override)
|
|
1004
|
+
klass = obj.class
|
|
1005
|
+
return if klass.respond_to?(:builtin_acl_default_active?) && klass.builtin_acl_default_active?
|
|
1006
|
+
return unless klass.respond_to?(:acl_policy_setting) && klass.acl_policy_setting.to_s.start_with?("owner_")
|
|
1007
|
+
field = klass.acl_owner_field
|
|
1008
|
+
if field == :self
|
|
1009
|
+
adopt_self_owned_acl!(obj, klass)
|
|
1010
|
+
return
|
|
1011
|
+
end
|
|
1012
|
+
# A master-key request is not the user's own write.
|
|
1013
|
+
return if payload.master?
|
|
1014
|
+
user = payload.user
|
|
1015
|
+
return unless user.is_a?(Parse::User) && user.id.present?
|
|
1016
|
+
if field && obj.respond_to?(field)
|
|
1017
|
+
return if obj.send(field).present?
|
|
1018
|
+
end
|
|
1019
|
+
obj.instance_variable_set(:@_acl_owner_override, user)
|
|
1020
|
+
end
|
|
1021
|
+
|
|
1022
|
+
# A self-owned user (`acl_policy ..., owner: :self`) owns its own
|
|
1023
|
+
# record, never the requesting user. Its objectId is assigned by Parse
|
|
1024
|
+
# Server after beforeSave, and Parse Server adds the new user's own
|
|
1025
|
+
# read and write to the ACL of every `_User` create. So the reply
|
|
1026
|
+
# carries the policy's ACL without the owner entry (`{}`, or public
|
|
1027
|
+
# read under `:owner_but_public_read`), which Parse Server completes
|
|
1028
|
+
# with the user's own grant. The save-time resolver is skipped, since
|
|
1029
|
+
# it would pre-generate an objectId the server does not use.
|
|
1030
|
+
# @!visibility private
|
|
1031
|
+
def adopt_self_owned_acl!(obj, klass)
|
|
1032
|
+
acl = if klass.acl_policy_setting == :owner_but_public_read
|
|
1033
|
+
Parse::ACL.everyone(true, false)
|
|
1034
|
+
else
|
|
1035
|
+
Parse::ACL.new
|
|
1036
|
+
end
|
|
1037
|
+
klass.instance_method(:acl=).bind_call(obj, acl)
|
|
1038
|
+
obj.instance_variable_set(:@_acl_pristine, false)
|
|
1039
|
+
obj.instance_variable_set(:@_webhook_reply_acl, true)
|
|
1040
|
+
end
|
|
1041
|
+
|
|
825
1042
|
# The client's write, rebuilt from a beforeSave payload. Parse Server
|
|
826
1043
|
# serializes the pending object with `toJSON()`, which reports every
|
|
827
1044
|
# pending top-level operator as its operator hash, so the operators
|
|
828
1045
|
# survive here as sent. On an update, a field whose value equals the
|
|
829
|
-
# stored one was not written by the client and is left out
|
|
1046
|
+
# stored one was not written by the client and is left out, and a
|
|
1047
|
+
# changed sub-document is split into dotted sub-key writes (see
|
|
1048
|
+
# {sub_document_write}).
|
|
830
1049
|
#
|
|
831
1050
|
# @param raw_object [Hash] the unscrubbed `object` hash.
|
|
832
1051
|
# @param raw_original [Hash, nil] the unscrubbed `original` hash.
|
|
@@ -841,12 +1060,96 @@ module Parse
|
|
|
841
1060
|
# Parse Server drops objectId from an update write itself.
|
|
842
1061
|
next if key == Parse::Model::OBJECT_ID
|
|
843
1062
|
next if raw_original.key?(key) && raw_original[key] == value
|
|
1063
|
+
dotted = sub_document_write(key, value, raw_original[key])
|
|
1064
|
+
if dotted
|
|
1065
|
+
data.merge!(dotted)
|
|
1066
|
+
next
|
|
1067
|
+
end
|
|
844
1068
|
end
|
|
845
1069
|
data[key] = value
|
|
846
1070
|
end
|
|
847
1071
|
data
|
|
848
1072
|
end
|
|
849
1073
|
|
|
1074
|
+
# Keys never split into dotted sub-key writes: their values are
|
|
1075
|
+
# replaced as a whole by Parse Server.
|
|
1076
|
+
# @!visibility private
|
|
1077
|
+
BEFORE_SAVE_REPLY_WHOLE_KEYS = %w[ACL authData].freeze
|
|
1078
|
+
|
|
1079
|
+
# Dotted sub-key writes for a sub-document the client changed.
|
|
1080
|
+
#
|
|
1081
|
+
# Parse Server applies a client's `"meta.count"` operator to its pending
|
|
1082
|
+
# object and sends the webhook only the resulting `meta`, which matches
|
|
1083
|
+
# a whole-object write of the same value. Writing just the sub-keys that
|
|
1084
|
+
# differ from the stored sub-document gives the same result as either
|
|
1085
|
+
# client write, without overwriting sub-keys another request changed in
|
|
1086
|
+
# the meantime. The split is one level deep: a changed sub-key is
|
|
1087
|
+
# written whole at `field.sub`, nested objects included, because Parse
|
|
1088
|
+
# Server rebuilds the afterSave object from a non-operator dotted key
|
|
1089
|
+
# only one level deep (`"meta.count.value"` would set `meta.count` to
|
|
1090
|
+
# the leaf). A sub-key missing from the new value is written as a
|
|
1091
|
+
# `Delete` operator.
|
|
1092
|
+
#
|
|
1093
|
+
# Parse Server stores a dotted value as sent, without the type transform
|
|
1094
|
+
# a whole write gets, so a Date would land as a plain sub-document
|
|
1095
|
+
# instead of a BSON date. When any write would carry a typed value
|
|
1096
|
+
# (`{"__type": ...}`), the field is written whole instead.
|
|
1097
|
+
#
|
|
1098
|
+
# @param key [String] the top-level field name.
|
|
1099
|
+
# @param value [Object] the field's value in the pending object.
|
|
1100
|
+
# @param stored [Object] the field's stored value.
|
|
1101
|
+
# @return [Hash, nil] the dotted writes, or nil to write the field whole.
|
|
1102
|
+
# @!visibility private
|
|
1103
|
+
def sub_document_write(key, value, stored)
|
|
1104
|
+
return nil if key.start_with?("_") || BEFORE_SAVE_REPLY_WHOLE_KEYS.include?(key)
|
|
1105
|
+
return nil unless splittable_level?(value, stored)
|
|
1106
|
+
writes = {}
|
|
1107
|
+
diff_sub_document(key, value, stored, writes)
|
|
1108
|
+
return nil if writes.empty? || writes.values.any? { |v| typed_value?(v) }
|
|
1109
|
+
writes
|
|
1110
|
+
end
|
|
1111
|
+
|
|
1112
|
+
# Collect the one-level dotted writes that turn `stored` into `value`
|
|
1113
|
+
# under `path`.
|
|
1114
|
+
# @!visibility private
|
|
1115
|
+
def diff_sub_document(path, value, stored, writes)
|
|
1116
|
+
(value.keys | stored.keys).each do |sub|
|
|
1117
|
+
new_present = value.key?(sub)
|
|
1118
|
+
old_present = stored.key?(sub)
|
|
1119
|
+
next if new_present && old_present && value[sub] == stored[sub]
|
|
1120
|
+
sub_path = "#{path}.#{sub}"
|
|
1121
|
+
writes[sub_path] = new_present ? value[sub] : { "__op" => "Delete" }
|
|
1122
|
+
end
|
|
1123
|
+
end
|
|
1124
|
+
|
|
1125
|
+
# Whether a pair of values can be diffed key by key: both plain JSON
|
|
1126
|
+
# objects, with sub-keys that are usable as path segments.
|
|
1127
|
+
# @!visibility private
|
|
1128
|
+
def splittable_level?(value, stored)
|
|
1129
|
+
return false unless plain_sub_document?(value) && plain_sub_document?(stored)
|
|
1130
|
+
keys = value.keys | stored.keys
|
|
1131
|
+
return false if keys.empty?
|
|
1132
|
+
keys.none? { |k| k.to_s.empty? || k.to_s.include?(".") || k.to_s.start_with?("$") }
|
|
1133
|
+
end
|
|
1134
|
+
|
|
1135
|
+
# Whether a JSON value is, or contains, a Parse typed value
|
|
1136
|
+
# (`{"__type": ...}`: Date, Bytes, Pointer, File, GeoPoint, ...).
|
|
1137
|
+
# @!visibility private
|
|
1138
|
+
def typed_value?(value)
|
|
1139
|
+
case value
|
|
1140
|
+
when Hash then value.key?("__type") || value.values.any? { |v| typed_value?(v) }
|
|
1141
|
+
when Array then value.any? { |v| typed_value?(v) }
|
|
1142
|
+
else false
|
|
1143
|
+
end
|
|
1144
|
+
end
|
|
1145
|
+
|
|
1146
|
+
# A JSON object field value: a Hash that is not a Parse operator or a
|
|
1147
|
+
# typed value (Pointer, Date, File, GeoPoint, ...).
|
|
1148
|
+
# @!visibility private
|
|
1149
|
+
def plain_sub_document?(value)
|
|
1150
|
+
value.is_a?(Hash) && !value.key?("__op") && !value.key?("__type")
|
|
1151
|
+
end
|
|
1152
|
+
|
|
850
1153
|
# Diff the handler's object against a fresh build of the same payload.
|
|
851
1154
|
#
|
|
852
1155
|
# Only fields that are dirty on either object can differ: a field the
|