parse-stack-next 5.7.5 → 5.8.0
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 +856 -0
- data/README.md +15 -4
- data/docs/TEST_SERVER.md +2 -2
- data/docs/acl_clp_guide.md +7 -0
- data/docs/atlas_vector_search_guide.md +190 -14
- data/docs/client_sdk_guide.md +11 -0
- data/docs/mcp_guide.md +318 -6
- data/docs/mongodb_direct_guide.md +27 -0
- data/docs/usage_guide.md +38 -0
- data/docs/webhooks_guide.md +74 -17
- data/lib/parse/acl_scope.rb +159 -41
- data/lib/parse/agent/approval_gate.rb +0 -0
- data/lib/parse/agent/constraint_translator.rb +42 -15
- data/lib/parse/agent/describe.rb +3 -1
- data/lib/parse/agent/field_names.rb +53 -0
- data/lib/parse/agent/field_policy.rb +74 -0
- data/lib/parse/agent/mcp_deployments.rb +426 -0
- data/lib/parse/agent/mcp_rack_app.rb +424 -45
- data/lib/parse/agent/mcp_server.rb +23 -1
- data/lib/parse/agent/mcp_subscriptions.rb +124 -6
- data/lib/parse/agent/metadata_registry.rb +67 -8
- data/lib/parse/agent/prompt_hardening.rb +9 -3
- data/lib/parse/agent/tools.rb +378 -29
- data/lib/parse/agent.rb +93 -1
- data/lib/parse/api/batch.rb +10 -1
- data/lib/parse/api/schema.rb +23 -4
- data/lib/parse/api/sessions.rb +6 -2
- data/lib/parse/api/users.rb +88 -14
- data/lib/parse/atlas_search/protected_paths.rb +236 -0
- data/lib/parse/atlas_search.rb +95 -23
- data/lib/parse/authorization.rb +54 -1
- data/lib/parse/client/batch.rb +231 -35
- data/lib/parse/client/body_builder.rb +21 -0
- data/lib/parse/client/caching.rb +371 -27
- data/lib/parse/client/request.rb +26 -14
- data/lib/parse/client/response.rb +49 -6
- data/lib/parse/client.rb +201 -38
- data/lib/parse/clp_scope.rb +281 -23
- data/lib/parse/console.rb +2 -2
- data/lib/parse/embeddings/voyage.rb +181 -17
- data/lib/parse/graphql/type_generator.rb +3 -0
- data/lib/parse/model/acl.rb +119 -21
- data/lib/parse/model/associations/belongs_to.rb +25 -3
- data/lib/parse/model/associations/collection_proxy.rb +138 -17
- data/lib/parse/model/associations/has_many.rb +38 -9
- data/lib/parse/model/associations/has_one.rb +3 -1
- data/lib/parse/model/associations/pointer_collection_proxy.rb +109 -17
- data/lib/parse/model/associations/relation_collection_proxy.rb +134 -28
- data/lib/parse/model/bytes.rb +13 -5
- data/lib/parse/model/classes/role.rb +72 -0
- data/lib/parse/model/classes/session.rb +43 -0
- data/lib/parse/model/classes/user.rb +78 -3
- data/lib/parse/model/core/actions.rb +269 -67
- data/lib/parse/model/core/builder.rb +100 -8
- data/lib/parse/model/core/create_lock.rb +27 -2
- data/lib/parse/model/core/describe.rb +2 -0
- data/lib/parse/model/core/fetching.rb +21 -3
- data/lib/parse/model/core/pluralized_aliases.rb +8 -4
- data/lib/parse/model/core/properties.rb +488 -39
- data/lib/parse/model/core/querying.rb +7 -0
- data/lib/parse/model/core/schema.rb +5 -3
- data/lib/parse/model/core/search_indexing.rb +63 -0
- data/lib/parse/model/core/vector_searchable.rb +35 -6
- data/lib/parse/model/file.rb +9 -2
- data/lib/parse/model/geopoint.rb +61 -13
- data/lib/parse/model/model.rb +160 -9
- data/lib/parse/model/object.rb +265 -17
- data/lib/parse/model/phone.rb +54 -5
- data/lib/parse/model/pointer.rb +40 -6
- data/lib/parse/mongodb.rb +170 -60
- data/lib/parse/pipeline_security.rb +415 -26
- data/lib/parse/query/constraint.rb +30 -0
- data/lib/parse/query/constraints.rb +58 -32
- data/lib/parse/query/cursor.rb +3 -1
- data/lib/parse/query/operation.rb +62 -8
- data/lib/parse/query/ordering.rb +34 -6
- data/lib/parse/query.rb +1100 -134
- data/lib/parse/retrieval/agent_tool.rb +290 -17
- data/lib/parse/retrieval/benchmark.rb +149 -0
- data/lib/parse/retrieval/profiles.rb +320 -0
- data/lib/parse/retrieval/retriever.rb +10 -1
- data/lib/parse/retrieval.rb +2 -0
- data/lib/parse/schema/search_index_migrator.rb +23 -5
- data/lib/parse/schema.rb +74 -18
- data/lib/parse/stack/tasks.rb +6 -4
- data/lib/parse/stack/version.rb +1 -1
- data/lib/parse/stack.rb +72 -14
- data/lib/parse/two_factor_auth/user_extension.rb +14 -2
- data/lib/parse/two_factor_auth.rb +11 -0
- data/lib/parse/vector_search/hybrid.rb +36 -18
- data/lib/parse/vector_search/index_definition.rb +237 -0
- data/lib/parse/vector_search.rb +46 -17
- data/lib/parse/webhooks/payload.rb +93 -6
- data/lib/parse/webhooks/replay_protection.rb +58 -20
- data/lib/parse/webhooks.rb +412 -40
- metadata +8 -1
data/lib/parse/model/acl.rb
CHANGED
|
@@ -147,6 +147,25 @@ module Parse
|
|
|
147
147
|
@delegate = owner
|
|
148
148
|
end
|
|
149
149
|
|
|
150
|
+
# Deep-copies the permissions table so a `dup`/`clone` of an ACL never
|
|
151
|
+
# shares mutable state with the original. ActiveModel dirty tracking
|
|
152
|
+
# stores a clone of the ACL as the "before" value on the first change;
|
|
153
|
+
# with a shallow copy, later in-place edits (`apply`, `delete`,
|
|
154
|
+
# `Permission#no_read!`) also rewrote that saved value. `rollback!`
|
|
155
|
+
# then restored the edited ACL and the change history reported the old
|
|
156
|
+
# and new values as identical.
|
|
157
|
+
# @!visibility private
|
|
158
|
+
def initialize_copy(other)
|
|
159
|
+
super
|
|
160
|
+
src = other.instance_variable_get(:@permissions)
|
|
161
|
+
@permissions = nil
|
|
162
|
+
return if src.nil?
|
|
163
|
+
@permissions = {}
|
|
164
|
+
src.each do |key, perm|
|
|
165
|
+
@permissions[key] = perm.is_a?(Permission) ? adopt_permission(perm.dup) : perm
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
150
169
|
# Create a new ACL with default Public read/write permissions and any
|
|
151
170
|
# overrides from the input hash format.
|
|
152
171
|
# @param read [Boolean] the read permissions for PUBLIC (default: true)
|
|
@@ -268,6 +287,21 @@ module Parse
|
|
|
268
287
|
as_json == other_acl.as_json
|
|
269
288
|
end
|
|
270
289
|
|
|
290
|
+
# Strict equality used by Hash keys and `Array#uniq`. Unlike {#==}, a
|
|
291
|
+
# plain Hash is never `eql?` to an ACL, since the two cannot share a
|
|
292
|
+
# {#hash} value. Two ACLs granting the same privileges are `eql?` and
|
|
293
|
+
# hash alike. The hash follows the current permissions, so an ACL
|
|
294
|
+
# mutated after use as a Hash key must be rehashed like any Hash key.
|
|
295
|
+
# @return [Boolean]
|
|
296
|
+
def eql?(other_acl)
|
|
297
|
+
other_acl.is_a?(Parse::ACL) && as_json == other_acl.as_json
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
# @return [Integer] a hash value consistent with {#eql?}.
|
|
301
|
+
def hash
|
|
302
|
+
[Parse::ACL, as_json].hash
|
|
303
|
+
end
|
|
304
|
+
|
|
271
305
|
# Set the public read and write permissions.
|
|
272
306
|
# @param read [Boolean] the read permission state.
|
|
273
307
|
# @param write [Boolean] the write permission state.
|
|
@@ -285,17 +319,23 @@ module Parse
|
|
|
285
319
|
@delegate.acl_will_change! if @delegate.respond_to?(:acl_will_change!)
|
|
286
320
|
end
|
|
287
321
|
|
|
288
|
-
# Removes
|
|
322
|
+
# Removes every permission for a user, role or the public entry.
|
|
289
323
|
# @overload delete(object)
|
|
290
|
-
# @param object [Parse::User] the user to revoke permissions.
|
|
324
|
+
# @param object [Parse::User, Parse::Pointer] the user to revoke permissions.
|
|
325
|
+
# @overload delete(role)
|
|
326
|
+
# @param role [Parse::Role] the role to revoke permissions.
|
|
291
327
|
# @overload delete(id)
|
|
292
|
-
# @param id [String]
|
|
328
|
+
# @param id [String, Symbol] a user objectId, a role key (`"role:Admin"`),
|
|
329
|
+
# a role name whose `role:` entry exists, or `:public` / `"*"`.
|
|
330
|
+
# @return [Parse::ACL::Permission, nil] the removed permission, or nil
|
|
331
|
+
# when there was no matching entry.
|
|
293
332
|
def delete(id)
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
333
|
+
key = normalize_permission_key(id)
|
|
334
|
+
return nil unless key.present? && permissions.has_key?(key)
|
|
335
|
+
will_change!
|
|
336
|
+
perm = permissions.delete(key)
|
|
337
|
+
perm.acl = nil if perm.is_a?(Permission) && perm.acl.equal?(self)
|
|
338
|
+
perm
|
|
299
339
|
end
|
|
300
340
|
|
|
301
341
|
# Apply a new permission with a given objectId, tag or :public.
|
|
@@ -331,7 +371,7 @@ module Parse
|
|
|
331
371
|
if permission.is_a?(ACL::Permission)
|
|
332
372
|
if permissions[id.to_s] != permission
|
|
333
373
|
will_change! # dirty track
|
|
334
|
-
permissions[id.to_s] = permission
|
|
374
|
+
permissions[id.to_s] = adopt_permission(permission)
|
|
335
375
|
end
|
|
336
376
|
end
|
|
337
377
|
|
|
@@ -351,6 +391,10 @@ module Parse
|
|
|
351
391
|
# @param write [Boolean] the write permission.
|
|
352
392
|
def apply_role(name, read = nil, write = nil)
|
|
353
393
|
name = name.name if name.is_a?(Parse::Role)
|
|
394
|
+
name = name.to_s
|
|
395
|
+
# Accept an already-prefixed key so "role:Admin" does not become
|
|
396
|
+
# "role:role:Admin".
|
|
397
|
+
name = name.delete_prefix("role:") if name.start_with?("role:")
|
|
354
398
|
apply("role:#{name}", read, write)
|
|
355
399
|
end
|
|
356
400
|
|
|
@@ -822,6 +866,17 @@ module Parse
|
|
|
822
866
|
|
|
823
867
|
private
|
|
824
868
|
|
|
869
|
+
# Points a Permission at this ACL so its own mutators can mark the ACL
|
|
870
|
+
# (and the owning object) dirty. A Permission already owned by a
|
|
871
|
+
# different ACL is copied first, so one ACL's edit never reaches another.
|
|
872
|
+
# @return [Permission]
|
|
873
|
+
def adopt_permission(perm)
|
|
874
|
+
owner = perm.acl
|
|
875
|
+
perm = perm.dup if owner && !owner.equal?(self)
|
|
876
|
+
perm.acl = self
|
|
877
|
+
perm
|
|
878
|
+
end
|
|
879
|
+
|
|
825
880
|
# Normalizes a user or role input to the appropriate permission key format.
|
|
826
881
|
# @param user_or_role [String, Parse::User, Parse::Role] the input to normalize
|
|
827
882
|
# @return [String, nil] the normalized key or nil if invalid
|
|
@@ -885,6 +940,27 @@ module Parse
|
|
|
885
940
|
# @return [Boolean] whether this permission is allowed.
|
|
886
941
|
attr_reader :write
|
|
887
942
|
|
|
943
|
+
# @!visibility private
|
|
944
|
+
# The ACL this permission belongs to. Its mutators notify this ACL so
|
|
945
|
+
# the change is dirty tracked on the owning object.
|
|
946
|
+
attr_accessor :acl
|
|
947
|
+
|
|
948
|
+
# Converts a permission flag to a strict boolean. Only `true`, the
|
|
949
|
+
# strings "true" and "1" (any case, surrounding whitespace ignored) and
|
|
950
|
+
# non-zero numbers grant. Everything else, including the string
|
|
951
|
+
# "false", denies.
|
|
952
|
+
# @!visibility private
|
|
953
|
+
# @return [Boolean]
|
|
954
|
+
def self.grant?(value)
|
|
955
|
+
case value
|
|
956
|
+
when true then true
|
|
957
|
+
when false, nil then false
|
|
958
|
+
when String then %w[true 1].include?(value.strip.downcase)
|
|
959
|
+
when Numeric then !value.zero?
|
|
960
|
+
else value.present?
|
|
961
|
+
end
|
|
962
|
+
end
|
|
963
|
+
|
|
888
964
|
# Create a new permission with the given read and write privileges.
|
|
889
965
|
# @overload new(read = nil, write = nil)
|
|
890
966
|
# @param read [Boolean] whether reading is allowed.
|
|
@@ -899,20 +975,34 @@ module Parse
|
|
|
899
975
|
def initialize(r_perm = nil, w_perm = nil)
|
|
900
976
|
if r_perm.is_a?(Hash)
|
|
901
977
|
r_perm = r_perm.symbolize_keys
|
|
902
|
-
@read = r_perm[:read]
|
|
903
|
-
@write = r_perm[:write]
|
|
978
|
+
@read = self.class.grant?(r_perm[:read])
|
|
979
|
+
@write = self.class.grant?(r_perm[:write])
|
|
904
980
|
else
|
|
905
|
-
@read =
|
|
906
|
-
@write =
|
|
981
|
+
@read = self.class.grant?(r_perm)
|
|
982
|
+
@write = self.class.grant?(w_perm)
|
|
907
983
|
end
|
|
908
984
|
end
|
|
909
985
|
|
|
986
|
+
# A copy is detached from the original's ACL; the copying ACL adopts it.
|
|
987
|
+
# @!visibility private
|
|
988
|
+
def initialize_copy(other)
|
|
989
|
+
super
|
|
990
|
+
@acl = nil
|
|
991
|
+
end
|
|
992
|
+
|
|
910
993
|
# @return [Boolean] whether two permission instances have the same permissions.
|
|
911
994
|
def ==(per)
|
|
912
995
|
return false unless per.is_a?(self.class)
|
|
913
996
|
@read == per.read && @write == per.write
|
|
914
997
|
end
|
|
915
998
|
|
|
999
|
+
alias_method :eql?, :==
|
|
1000
|
+
|
|
1001
|
+
# @return [Integer] a hash value consistent with {#eql?}.
|
|
1002
|
+
def hash
|
|
1003
|
+
[self.class, @read, @write].hash
|
|
1004
|
+
end
|
|
1005
|
+
|
|
916
1006
|
# @return [Hash] A Parse-compatible ACL-hash. Omission or false on a
|
|
917
1007
|
# priviledge means don't include it
|
|
918
1008
|
def as_json(*args)
|
|
@@ -940,19 +1030,27 @@ module Parse
|
|
|
940
1030
|
@read.present? || @write.present?
|
|
941
1031
|
end
|
|
942
1032
|
|
|
943
|
-
# Sets the *read* value of the permission. Defaults to true.
|
|
944
|
-
#
|
|
1033
|
+
# Sets the *read* value of the permission. Defaults to true. When the
|
|
1034
|
+
# permission belongs to an ACL, a real change marks that ACL (and its
|
|
1035
|
+
# owning object) dirty so the change is sent on the next save.
|
|
945
1036
|
# @version 1.7.2
|
|
946
|
-
# @return [
|
|
1037
|
+
# @return [Boolean] the new read value.
|
|
947
1038
|
def read!(value = true)
|
|
1039
|
+
value = self.class.grant?(value)
|
|
1040
|
+
return value if value == @read
|
|
1041
|
+
@acl&.will_change!
|
|
948
1042
|
@read = value
|
|
949
1043
|
end
|
|
950
1044
|
|
|
951
|
-
# Sets the *write* value of the permission. Defaults to true.
|
|
952
|
-
#
|
|
1045
|
+
# Sets the *write* value of the permission. Defaults to true. When the
|
|
1046
|
+
# permission belongs to an ACL, a real change marks that ACL (and its
|
|
1047
|
+
# owning object) dirty so the change is sent on the next save.
|
|
953
1048
|
# @version 1.7.2
|
|
954
|
-
# @return [
|
|
1049
|
+
# @return [Boolean] the new write value.
|
|
955
1050
|
def write!(value = true)
|
|
1051
|
+
value = self.class.grant?(value)
|
|
1052
|
+
return value if value == @write
|
|
1053
|
+
@acl&.will_change!
|
|
956
1054
|
@write = value
|
|
957
1055
|
end
|
|
958
1056
|
|
|
@@ -960,14 +1058,14 @@ module Parse
|
|
|
960
1058
|
# @version 1.7.2
|
|
961
1059
|
# @return [void]
|
|
962
1060
|
def no_read!
|
|
963
|
-
|
|
1061
|
+
read!(false)
|
|
964
1062
|
end
|
|
965
1063
|
|
|
966
1064
|
# Sets the *write* value of the permission to false.
|
|
967
1065
|
# @version 1.7.2
|
|
968
1066
|
# @return [void]
|
|
969
1067
|
def no_write!
|
|
970
|
-
|
|
1068
|
+
write!(false)
|
|
971
1069
|
end
|
|
972
1070
|
end
|
|
973
1071
|
end
|
|
@@ -183,6 +183,7 @@ module Parse
|
|
|
183
183
|
self.fields.merge!(key => :pointer, parse_field => :pointer)
|
|
184
184
|
# Mapping between local attribute name and the remote column name
|
|
185
185
|
self.field_map.merge!(key => parse_field)
|
|
186
|
+
Parse::Model.model_registry_changed!
|
|
186
187
|
|
|
187
188
|
# Agent metadata: a belongs_to pointer can carry a semantic description
|
|
188
189
|
# (and per-value enum descriptions) just like a `property` can. This
|
|
@@ -229,8 +230,10 @@ module Parse
|
|
|
229
230
|
# hash, lets try to build a Pointer of that type.
|
|
230
231
|
|
|
231
232
|
if val.is_a?(Hash) && (val["__type"] == "Pointer" || val["__type"] == "Object")
|
|
232
|
-
# Get nested fetched keys for this field if available
|
|
233
|
-
|
|
233
|
+
# Get nested fetched keys for this field if available. Query
|
|
234
|
+
# keys are recorded under the remote column name, which differs
|
|
235
|
+
# from the local name for multi-word fields.
|
|
236
|
+
nested_keys = nested_keys_for(association_key) || nested_keys_for(parse_field)
|
|
234
237
|
# Always trust the declared klassName — never the className the
|
|
235
238
|
# server (or attacker-controlled mass assignment) supplied. This
|
|
236
239
|
# prevents type confusion where a pointer to a different class
|
|
@@ -272,21 +275,40 @@ module Parse
|
|
|
272
275
|
end
|
|
273
276
|
|
|
274
277
|
# We only support pointers, either by object or by transforming a hash.
|
|
278
|
+
# Assignment from application code (track == true) also accepts an
|
|
279
|
+
# objectId String, which becomes a pointer of the declared class,
|
|
280
|
+
# and refuses an object or pointer of another class. A pointer
|
|
281
|
+
# hash is wire data: its className is ignored in favor of the
|
|
282
|
+
# declared class, as before.
|
|
275
283
|
define_method(set_attribute_method) do |val, track = true|
|
|
276
284
|
if val == Parse::Properties::DELETE_OP
|
|
277
285
|
val = nil
|
|
278
286
|
elsif val.is_a?(Hash) && (val["__type"] == "Pointer" || val["__type"] == "Object")
|
|
279
287
|
# Get nested fetched keys for this field if available
|
|
280
|
-
nested_keys = nested_keys_for(key)
|
|
288
|
+
nested_keys = nested_keys_for(key) || nested_keys_for(parse_field)
|
|
281
289
|
# Always trust declared klassName over incoming hash className.
|
|
282
290
|
incoming_class = val[Parse::Model::KEY_CLASS_NAME]
|
|
283
291
|
if incoming_class && !Parse::Model.same_parse_class?(incoming_class, klassName)
|
|
284
292
|
warn "[#{self.class}] belongs_to :#{key} expected className=#{klassName.inspect}, ignoring incoming className=#{incoming_class.inspect}"
|
|
285
293
|
end
|
|
286
294
|
val = Parse::Object.build val, klassName, fetched_keys: nested_keys
|
|
295
|
+
elsif track == true && val.is_a?(String)
|
|
296
|
+
# A blank String (an empty form select) clears the pointer.
|
|
297
|
+
val = val.blank? ? nil : Parse::Object.build({ Parse::Model::TYPE_FIELD => Parse::Model::TYPE_POINTER,
|
|
298
|
+
Parse::Model::KEY_CLASS_NAME => klassName,
|
|
299
|
+
Parse::Model::OBJECT_ID => val }, klassName)
|
|
287
300
|
end
|
|
288
301
|
|
|
289
302
|
if track == true
|
|
303
|
+
unless val.nil? || val.is_a?(Parse::Pointer)
|
|
304
|
+
raise ArgumentError, "#{self.class}##{key} expects a #{klassName} object, pointer or objectId, got #{val.class}."
|
|
305
|
+
end
|
|
306
|
+
# The class is checked when the declared class is a registered
|
|
307
|
+
# model, matching has_many arrays.
|
|
308
|
+
if val.is_a?(Parse::Pointer) && Parse::Model.find_class(klassName) &&
|
|
309
|
+
!Parse::Model.same_parse_class?(val.parse_class, klassName)
|
|
310
|
+
raise ArgumentError, "#{self.class}##{key} expects a #{klassName} object, got #{val.parse_class}."
|
|
311
|
+
end
|
|
290
312
|
prepare_for_dirty_tracking!(key)
|
|
291
313
|
send will_change_method unless val == instance_variable_get(ivar)
|
|
292
314
|
else
|
|
@@ -67,6 +67,25 @@ module Parse
|
|
|
67
67
|
@parse_class = parse_class
|
|
68
68
|
end
|
|
69
69
|
|
|
70
|
+
# Copies (`dup` and `clone`) get their own backing array and their own
|
|
71
|
+
# dirty-tracking state. ActiveModel records a property's previous value
|
|
72
|
+
# by cloning it; with a shared array the "previous" proxy changed along
|
|
73
|
+
# with the live one, so change history showed the new value twice and
|
|
74
|
+
# `rollback!` restored nothing.
|
|
75
|
+
# @!visibility private
|
|
76
|
+
def initialize_copy(other)
|
|
77
|
+
super
|
|
78
|
+
@collection = other.instance_variable_get(:@collection).dup
|
|
79
|
+
# Pending relation operations (RelationCollectionProxy) are copied too.
|
|
80
|
+
%i[@additions @removals].each do |ivar|
|
|
81
|
+
next unless instance_variable_defined?(ivar)
|
|
82
|
+
value = instance_variable_get(ivar)
|
|
83
|
+
instance_variable_set(ivar, value.dup) if value.is_a?(Array)
|
|
84
|
+
end
|
|
85
|
+
@mutations_from_database = nil
|
|
86
|
+
@mutations_before_last_save = nil
|
|
87
|
+
end
|
|
88
|
+
|
|
70
89
|
# true if the collection has been loaded
|
|
71
90
|
def loaded?
|
|
72
91
|
@loaded
|
|
@@ -81,10 +100,12 @@ module Parse
|
|
|
81
100
|
params.nil? ? @delegate.send(method) : @delegate.send(method, params)
|
|
82
101
|
end
|
|
83
102
|
|
|
84
|
-
# Reset the state of the collection.
|
|
103
|
+
# Reset the state of the collection. The items are dropped and the
|
|
104
|
+
# collection is marked as not loaded. This is not a change to the
|
|
105
|
+
# field, so it is not dirty tracked; use {#clear} to empty the field.
|
|
85
106
|
def reset!
|
|
86
107
|
@loaded = false
|
|
87
|
-
clear
|
|
108
|
+
@collection.clear
|
|
88
109
|
end
|
|
89
110
|
|
|
90
111
|
# @return [Boolean] true if two collection proxies have similar items.
|
|
@@ -102,9 +123,25 @@ module Parse
|
|
|
102
123
|
collection #force reload
|
|
103
124
|
end
|
|
104
125
|
|
|
105
|
-
#
|
|
126
|
+
# Remove all items from the collection. The field is marked as changed,
|
|
127
|
+
# so the next save stores the empty array.
|
|
128
|
+
# @return [Array] the (now empty) collection.
|
|
106
129
|
def clear
|
|
130
|
+
notify_will_change!
|
|
107
131
|
@collection.clear
|
|
132
|
+
@loaded = true
|
|
133
|
+
@collection
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Replace the contents of the collection with a new set of items, as
|
|
137
|
+
# Array#replace does. The field is marked as changed.
|
|
138
|
+
# @param items [Array] the new contents.
|
|
139
|
+
# @return [self]
|
|
140
|
+
def replace(items)
|
|
141
|
+
notify_will_change!
|
|
142
|
+
@collection = Array(items.is_a?(Parse::CollectionProxy) ? items.to_a : items).dup
|
|
143
|
+
@loaded = true
|
|
144
|
+
self
|
|
108
145
|
end
|
|
109
146
|
|
|
110
147
|
# @return [Array]
|
|
@@ -233,36 +270,59 @@ module Parse
|
|
|
233
270
|
|
|
234
271
|
alias_method :delete, :remove
|
|
235
272
|
|
|
236
|
-
# Atomically adds all items
|
|
237
|
-
#
|
|
238
|
-
#
|
|
273
|
+
# Atomically adds all items to the array field. The request is sent
|
|
274
|
+
# directly to the Parse backend. On success the local collection is
|
|
275
|
+
# updated to match (the items are appended) without marking the field
|
|
276
|
+
# as changed, so a later save does not overwrite the server array. On
|
|
277
|
+
# an owner that has not been saved yet there is nothing to update on
|
|
278
|
+
# the server, so the items are added locally as a normal change and
|
|
279
|
+
# sent with the next save.
|
|
280
|
+
# @param items [Array] items to add
|
|
239
281
|
# @note Parse objects are automatically converted to pointer format
|
|
282
|
+
# @return [Boolean] whether the operation succeeded.
|
|
240
283
|
# @see #add_unique!
|
|
241
284
|
def add!(*items)
|
|
285
|
+
items = items.flatten
|
|
242
286
|
return false unless @delegate.respond_to?(:op_add!)
|
|
243
|
-
|
|
244
|
-
|
|
287
|
+
return true if items.empty?
|
|
288
|
+
return add(*items) && true if unsaved_delegate?
|
|
289
|
+
apply_atomic_op(:op_add!, items) do |list, objs|
|
|
290
|
+
list + objs
|
|
291
|
+
end
|
|
245
292
|
end
|
|
246
293
|
|
|
247
|
-
# Atomically adds
|
|
248
|
-
#
|
|
294
|
+
# Atomically adds the items that are not already part of the array
|
|
295
|
+
# field. The request is sent directly to the Parse backend. The local
|
|
296
|
+
# collection is updated as in {#add!}.
|
|
249
297
|
# @param items [Array] items to uniquely add
|
|
250
298
|
# @note Parse objects are automatically converted to pointer format
|
|
299
|
+
# @return [Boolean] whether the operation succeeded.
|
|
251
300
|
# @see #add!
|
|
252
301
|
def add_unique!(*items)
|
|
302
|
+
items = items.flatten
|
|
253
303
|
return false unless @delegate.respond_to?(:op_add_unique!)
|
|
254
|
-
|
|
255
|
-
|
|
304
|
+
return true if items.empty?
|
|
305
|
+
return add_unique(*items) && true if unsaved_delegate?
|
|
306
|
+
apply_atomic_op(:op_add_unique!, items) do |list, objs|
|
|
307
|
+
objs.each { |o| list << o unless list.include?(o) }
|
|
308
|
+
list
|
|
309
|
+
end
|
|
256
310
|
end
|
|
257
311
|
|
|
258
|
-
# Atomically
|
|
259
|
-
# directly to the Parse backend.
|
|
312
|
+
# Atomically removes the items from the array field. The request is
|
|
313
|
+
# sent directly to the Parse backend. The local collection is updated
|
|
314
|
+
# as in {#add!}.
|
|
260
315
|
# @param items [Array] items to remove
|
|
261
316
|
# @note Parse objects are automatically converted to pointer format
|
|
317
|
+
# @return [Boolean] whether the operation succeeded.
|
|
262
318
|
def remove!(*items)
|
|
319
|
+
items = items.flatten
|
|
263
320
|
return false unless @delegate.respond_to?(:op_remove!)
|
|
264
|
-
|
|
265
|
-
|
|
321
|
+
return true if items.empty?
|
|
322
|
+
return remove(*items) && true if unsaved_delegate?
|
|
323
|
+
apply_atomic_op(:op_remove!, items) do |list, objs|
|
|
324
|
+
list.reject { |x| objs.include?(x) }
|
|
325
|
+
end
|
|
266
326
|
end
|
|
267
327
|
|
|
268
328
|
# Atomically deletes all items in the array, and marks the field as `undefined` directly
|
|
@@ -276,8 +336,15 @@ module Parse
|
|
|
276
336
|
end
|
|
277
337
|
|
|
278
338
|
# Locally restores previous attributes (not from the persistent store)
|
|
339
|
+
# and clears the dirty tracking of the collection. The values are
|
|
340
|
+
# restored directly: going through the public writers would report a
|
|
341
|
+
# new change to the owner while undoing one.
|
|
279
342
|
def rollback!
|
|
280
|
-
|
|
343
|
+
changed.each do |attr|
|
|
344
|
+
instance_variable_set(:"@#{attr}", attribute_was(attr))
|
|
345
|
+
end
|
|
346
|
+
clear_changes_information
|
|
347
|
+
@collection
|
|
281
348
|
end
|
|
282
349
|
|
|
283
350
|
# clears all dirty tracked information.
|
|
@@ -416,6 +483,60 @@ module Parse
|
|
|
416
483
|
|
|
417
484
|
private
|
|
418
485
|
|
|
486
|
+
# @return [Boolean] true when the owner has no objectId yet, so an
|
|
487
|
+
# atomic server operation has nothing to apply to.
|
|
488
|
+
def unsaved_delegate?
|
|
489
|
+
@delegate.respond_to?(:id) && @delegate.id.blank?
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
# @return [Boolean] whether the owner reports this field as changed.
|
|
493
|
+
def delegate_field_dirty?
|
|
494
|
+
return false unless @delegate.respond_to?(:changed)
|
|
495
|
+
@delegate.changed.include?(@key.to_s)
|
|
496
|
+
rescue StandardError
|
|
497
|
+
false
|
|
498
|
+
end
|
|
499
|
+
|
|
500
|
+
# Run an atomic operation on the owner and, when it succeeds, apply the
|
|
501
|
+
# same change to the local collection without marking the field dirty.
|
|
502
|
+
# The block receives a copy of the current items and the operation's
|
|
503
|
+
# items and returns the new contents.
|
|
504
|
+
# @return [Boolean] whether the operation succeeded.
|
|
505
|
+
def apply_atomic_op(op, items)
|
|
506
|
+
return true if items.empty?
|
|
507
|
+
success = @delegate.send(op, @key, items_to_pointers(items))
|
|
508
|
+
return false unless success
|
|
509
|
+
# Read the owner's dirty state before touching the items, so a change
|
|
510
|
+
# that was already pending is still sent by the next save.
|
|
511
|
+
was_dirty = delegate_field_dirty?
|
|
512
|
+
# A clean plain array adopts the array the server returned, so a local
|
|
513
|
+
# copy that was already out of date is corrected. With unsaved local
|
|
514
|
+
# edits pending, adopting it would discard them, so the operation is
|
|
515
|
+
# applied locally instead (as it is for pointer collections, or a reply
|
|
516
|
+
# without the field) and the pending edits are still sent on save.
|
|
517
|
+
server = was_dirty ? nil : server_array_after_op
|
|
518
|
+
@collection = server || yield(collection.to_a.dup, items)
|
|
519
|
+
@loaded = true
|
|
520
|
+
unless was_dirty
|
|
521
|
+
# Plain array properties detect in-place edits by comparing against
|
|
522
|
+
# a snapshot; take a new one so this update is not seen as an edit.
|
|
523
|
+
if @delegate.respond_to?(:_rebaseline_mutable_value!, true)
|
|
524
|
+
@delegate.send(:_rebaseline_mutable_value!, @key)
|
|
525
|
+
end
|
|
526
|
+
end
|
|
527
|
+
true
|
|
528
|
+
end
|
|
529
|
+
|
|
530
|
+
# The field's new array from the delegate's last atomic operation, when
|
|
531
|
+
# this is a plain array proxy and the server returned it.
|
|
532
|
+
# @return [Array, nil]
|
|
533
|
+
def server_array_after_op
|
|
534
|
+
return nil unless instance_of?(Parse::CollectionProxy)
|
|
535
|
+
return nil unless @delegate.respond_to?(:_last_operation_value, true)
|
|
536
|
+
value = @delegate.send(:_last_operation_value, @key)
|
|
537
|
+
value.is_a?(Array) ? Parse::Properties.deep_copy_value(value) : nil
|
|
538
|
+
end
|
|
539
|
+
|
|
419
540
|
# Convert items to pointer format for atomic operations.
|
|
420
541
|
# Parse objects/pointers are converted to pointer hashes, other items pass through.
|
|
421
542
|
# @param items [Array] items to convert
|
|
@@ -378,10 +378,22 @@ module Parse
|
|
|
378
378
|
}
|
|
379
379
|
|
|
380
380
|
define_method(key) do |*args, &block|
|
|
381
|
-
return [] if @id.nil?
|
|
382
381
|
query = Parse::Query.new(klassName, limit: :max)
|
|
383
382
|
|
|
384
|
-
|
|
383
|
+
unless opts[:scope_only] == true
|
|
384
|
+
if @id.blank?
|
|
385
|
+
# An unsaved owner has no related records. Return a query
|
|
386
|
+
# that matches nothing, so chaining still works, without
|
|
387
|
+
# sending a constraint on a pointer with a null objectId.
|
|
388
|
+
query.where(:objectId.in => [])
|
|
389
|
+
query.define_singleton_method(:results) do |*_a, **_o, &blk|
|
|
390
|
+
blk ? [].each(&blk) : []
|
|
391
|
+
end
|
|
392
|
+
query.define_singleton_method(:count) { |*_a, **_o| 0 }
|
|
393
|
+
else
|
|
394
|
+
query.where(foreign_field => self)
|
|
395
|
+
end
|
|
396
|
+
end
|
|
385
397
|
|
|
386
398
|
if scope.is_a?(Proc)
|
|
387
399
|
# magic, override the singleton method_missing with accessing object level methods
|
|
@@ -483,6 +495,7 @@ module Parse
|
|
|
483
495
|
}
|
|
484
496
|
|
|
485
497
|
self.field_map.merge!(key => parse_field)
|
|
498
|
+
Parse::Model.model_registry_changed!
|
|
486
499
|
# dirty tracking
|
|
487
500
|
define_attribute_methods key
|
|
488
501
|
|
|
@@ -510,7 +523,7 @@ module Parse
|
|
|
510
523
|
unless val.is_a?(Parse::PointerCollectionProxy)
|
|
511
524
|
results = []
|
|
512
525
|
#results = val.parse_objects if val.respond_to?(:parse_objects)
|
|
513
|
-
val = proxyKlass.new results, delegate: self, key: key
|
|
526
|
+
val = proxyKlass.new results, delegate: self, key: key, parse_class: klassName
|
|
514
527
|
instance_variable_set(ivar, val)
|
|
515
528
|
end
|
|
516
529
|
val
|
|
@@ -554,10 +567,21 @@ module Parse
|
|
|
554
567
|
_collection.loaded = true
|
|
555
568
|
_collection.remove val["objects"].parse_objects(klassName)
|
|
556
569
|
val = _collection
|
|
557
|
-
elsif val.is_a?(Array)
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
570
|
+
elsif val.is_a?(Array) || (track == true && val.is_a?(Parse::CollectionProxy) && !val.is_a?(proxyKlass))
|
|
571
|
+
_collection = proxyKlass.new [], delegate: self, key: key, parse_class: klassName
|
|
572
|
+
items = if track == true
|
|
573
|
+
# Assignment from application code: validate every item.
|
|
574
|
+
# An item of another class, nil, or a value that is not an
|
|
575
|
+
# object, pointer or objectId raises ArgumentError rather
|
|
576
|
+
# than being stored under the declared class.
|
|
577
|
+
_collection.send(:typecast_items, val.to_a)
|
|
578
|
+
else
|
|
579
|
+
# Server data: coerce elements to the declared class.
|
|
580
|
+
val.parse_objects(klassName)
|
|
581
|
+
end
|
|
582
|
+
_collection.set_collection!(items)
|
|
583
|
+
_collection.loaded = true
|
|
584
|
+
val = _collection
|
|
561
585
|
end
|
|
562
586
|
|
|
563
587
|
# send dirty tracking if set
|
|
@@ -579,11 +603,16 @@ module Parse
|
|
|
579
603
|
# for more information.
|
|
580
604
|
if data_type == :relation
|
|
581
605
|
# return a query given the foreign table class name.
|
|
606
|
+
# The $relatedTo key is the remote column, which differs from the
|
|
607
|
+
# local name when `field:` is given.
|
|
582
608
|
define_method("#{key}_relation_query") do
|
|
583
|
-
Parse::Query.new(klassName,
|
|
609
|
+
Parse::Query.new(klassName, parse_field.related_to => self.pointer, limit: :max)
|
|
584
610
|
end
|
|
585
|
-
# fetch the contents of the relation
|
|
611
|
+
# fetch the contents of the relation. An owner without an
|
|
612
|
+
# objectId has nothing on the server; a query for it would send
|
|
613
|
+
# a null objectId, which Parse Server rejects.
|
|
586
614
|
define_method("#{key}_fetch!") do
|
|
615
|
+
next [] if @id.blank?
|
|
587
616
|
q = self.send :"#{key}_relation_query"
|
|
588
617
|
q.results || []
|
|
589
618
|
end
|
|
@@ -142,7 +142,9 @@ module Parse
|
|
|
142
142
|
end
|
|
143
143
|
|
|
144
144
|
define_method(key) do |*args, &block|
|
|
145
|
-
|
|
145
|
+
# An unsaved owner has no related record, unless the scope alone
|
|
146
|
+
# selects it (`scope_only: true` does not use the owner id).
|
|
147
|
+
return nil if @id.blank? && opts[:scope_only] != true
|
|
146
148
|
query = Parse::Query.new(klassName, limit: 1)
|
|
147
149
|
query.where(foreign_field => self) unless opts[:scope_only] == true
|
|
148
150
|
|