parse-stack-next 5.7.6 → 5.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +1148 -0
- data/README.md +39 -23
- data/docs/TEST_SERVER.md +2 -2
- data/docs/acl_clp_guide.md +7 -0
- data/docs/atlas_vector_search_guide.md +181 -13
- data/docs/caching.md +50 -0
- data/docs/client_sdk_guide.md +11 -0
- data/docs/mcp_guide.md +332 -9
- data/docs/mongodb_direct_guide.md +27 -0
- data/docs/usage_guide.md +38 -0
- data/docs/webhooks_guide.md +126 -17
- data/lib/parse/acl_scope.rb +230 -50
- data/lib/parse/agent/approval_gate.rb +0 -0
- data/lib/parse/agent/constraint_translator.rb +42 -15
- data/lib/parse/agent/describe.rb +11 -3
- data/lib/parse/agent/field_names.rb +53 -0
- data/lib/parse/agent/field_policy.rb +74 -0
- data/lib/parse/agent/mcp_deployments.rb +426 -0
- data/lib/parse/agent/mcp_dispatcher.rb +8 -1
- data/lib/parse/agent/mcp_rack_app.rb +424 -45
- data/lib/parse/agent/mcp_server.rb +23 -1
- data/lib/parse/agent/mcp_subscriptions.rb +124 -6
- data/lib/parse/agent/metadata_registry.rb +67 -8
- data/lib/parse/agent/prompt_hardening.rb +9 -3
- data/lib/parse/agent/tools.rb +378 -29
- data/lib/parse/agent.rb +298 -21
- data/lib/parse/api/batch.rb +10 -1
- data/lib/parse/api/schema.rb +23 -4
- data/lib/parse/api/sessions.rb +6 -2
- data/lib/parse/api/users.rb +88 -14
- data/lib/parse/atlas_search/index_manager.rb +5 -1
- data/lib/parse/atlas_search/protected_paths.rb +236 -0
- data/lib/parse/atlas_search.rb +144 -24
- data/lib/parse/authorization.rb +289 -4
- data/lib/parse/cache/sub_cache.rb +26 -0
- data/lib/parse/client/authentication.rb +19 -1
- data/lib/parse/client/batch.rb +264 -35
- data/lib/parse/client/body_builder.rb +21 -0
- data/lib/parse/client/caching.rb +371 -27
- data/lib/parse/client/request.rb +26 -14
- data/lib/parse/client/response.rb +49 -6
- data/lib/parse/client.rb +227 -40
- data/lib/parse/clp_scope.rb +317 -25
- data/lib/parse/console.rb +2 -2
- data/lib/parse/embeddings/voyage.rb +181 -17
- data/lib/parse/graphql/type_generator.rb +3 -0
- data/lib/parse/live_query/client.rb +72 -2
- data/lib/parse/lock_backend.rb +4 -3
- data/lib/parse/model/acl.rb +119 -21
- data/lib/parse/model/associations/belongs_to.rb +25 -3
- data/lib/parse/model/associations/collection_proxy.rb +146 -17
- data/lib/parse/model/associations/has_many.rb +49 -9
- data/lib/parse/model/associations/has_one.rb +3 -1
- data/lib/parse/model/associations/pointer_collection_proxy.rb +145 -17
- data/lib/parse/model/associations/relation_collection_proxy.rb +229 -38
- data/lib/parse/model/bytes.rb +13 -5
- data/lib/parse/model/classes/role.rb +95 -0
- data/lib/parse/model/classes/session.rb +304 -1
- data/lib/parse/model/classes/user.rb +104 -8
- data/lib/parse/model/core/actions.rb +287 -70
- data/lib/parse/model/core/builder.rb +100 -8
- data/lib/parse/model/core/create_lock.rb +27 -2
- data/lib/parse/model/core/describe.rb +2 -0
- data/lib/parse/model/core/fetching.rb +50 -5
- data/lib/parse/model/core/field_guards.rb +16 -8
- data/lib/parse/model/core/pluralized_aliases.rb +8 -4
- data/lib/parse/model/core/properties.rb +488 -39
- data/lib/parse/model/core/querying.rb +7 -0
- data/lib/parse/model/core/schema.rb +5 -3
- data/lib/parse/model/core/search_indexing.rb +63 -0
- data/lib/parse/model/core/vector_searchable.rb +35 -6
- data/lib/parse/model/file.rb +9 -2
- data/lib/parse/model/geopoint.rb +61 -13
- data/lib/parse/model/model.rb +160 -9
- data/lib/parse/model/object.rb +317 -18
- data/lib/parse/model/phone.rb +54 -5
- data/lib/parse/model/pointer.rb +40 -6
- data/lib/parse/model/push.rb +11 -1
- data/lib/parse/mongodb.rb +189 -61
- data/lib/parse/pipeline_security.rb +415 -26
- data/lib/parse/query/constraint.rb +31 -1
- data/lib/parse/query/constraints.rb +89 -59
- data/lib/parse/query/cursor.rb +3 -1
- data/lib/parse/query/operation.rb +62 -8
- data/lib/parse/query/ordering.rb +34 -6
- data/lib/parse/query.rb +1871 -167
- data/lib/parse/retrieval/agent_tool.rb +225 -8
- data/lib/parse/retrieval/benchmark.rb +149 -0
- data/lib/parse/retrieval/profiles.rb +320 -0
- data/lib/parse/retrieval/retriever.rb +10 -1
- data/lib/parse/retrieval.rb +2 -0
- data/lib/parse/schema/search_index_migrator.rb +23 -5
- data/lib/parse/schema.rb +74 -18
- data/lib/parse/stack/tasks.rb +6 -4
- data/lib/parse/stack/version.rb +1 -1
- data/lib/parse/stack.rb +91 -14
- data/lib/parse/two_factor_auth/user_extension.rb +14 -2
- data/lib/parse/two_factor_auth.rb +11 -0
- data/lib/parse/vector_search/hybrid.rb +110 -33
- data/lib/parse/vector_search/index_definition.rb +237 -0
- data/lib/parse/vector_search.rb +259 -23
- data/lib/parse/webhooks/payload.rb +136 -6
- data/lib/parse/webhooks/replay_protection.rb +58 -20
- data/lib/parse/webhooks.rb +715 -40
- metadata +8 -1
|
@@ -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
|
|
@@ -542,6 +555,17 @@ module Parse
|
|
|
542
555
|
warn "[#{self.class}] has_many :#{key} expected className=#{klassName.inspect}, ignoring incoming className=#{val[Parse::Model::KEY_CLASS_NAME].inspect}"
|
|
543
556
|
end
|
|
544
557
|
if val.is_a?(Hash) && val["__type"] == "Relation"
|
|
558
|
+
current = instance_variable_get(ivar)
|
|
559
|
+
# A server descriptor carries no membership. When the current
|
|
560
|
+
# proxy holds staged additions or removals (a fetch of a record
|
|
561
|
+
# with unsaved relation changes), keep it so they are not lost.
|
|
562
|
+
if track != true && current.is_a?(Parse::RelationCollectionProxy) &&
|
|
563
|
+
current.staged_changes?
|
|
564
|
+
# The loaded list predates this fetch. Unload it so the next
|
|
565
|
+
# read queries the server and re-applies the staged changes.
|
|
566
|
+
current.reset! if current.loaded?
|
|
567
|
+
return current
|
|
568
|
+
end
|
|
545
569
|
relation_objects = val["objects"] || []
|
|
546
570
|
val = Parse::RelationCollectionProxy.new relation_objects, delegate: self, key: key, parse_class: klassName
|
|
547
571
|
elsif val.is_a?(Hash) && val["__op"] == "AddRelation" && val["objects"].present?
|
|
@@ -554,10 +578,21 @@ module Parse
|
|
|
554
578
|
_collection.loaded = true
|
|
555
579
|
_collection.remove val["objects"].parse_objects(klassName)
|
|
556
580
|
val = _collection
|
|
557
|
-
elsif val.is_a?(Array)
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
581
|
+
elsif val.is_a?(Array) || (track == true && val.is_a?(Parse::CollectionProxy) && !val.is_a?(proxyKlass))
|
|
582
|
+
_collection = proxyKlass.new [], delegate: self, key: key, parse_class: klassName
|
|
583
|
+
items = if track == true
|
|
584
|
+
# Assignment from application code: validate every item.
|
|
585
|
+
# An item of another class, nil, or a value that is not an
|
|
586
|
+
# object, pointer or objectId raises ArgumentError rather
|
|
587
|
+
# than being stored under the declared class.
|
|
588
|
+
_collection.send(:typecast_items, val.to_a)
|
|
589
|
+
else
|
|
590
|
+
# Server data: coerce elements to the declared class.
|
|
591
|
+
val.parse_objects(klassName)
|
|
592
|
+
end
|
|
593
|
+
_collection.set_collection!(items)
|
|
594
|
+
_collection.loaded = true
|
|
595
|
+
val = _collection
|
|
561
596
|
end
|
|
562
597
|
|
|
563
598
|
# send dirty tracking if set
|
|
@@ -579,11 +614,16 @@ module Parse
|
|
|
579
614
|
# for more information.
|
|
580
615
|
if data_type == :relation
|
|
581
616
|
# return a query given the foreign table class name.
|
|
617
|
+
# The $relatedTo key is the remote column, which differs from the
|
|
618
|
+
# local name when `field:` is given.
|
|
582
619
|
define_method("#{key}_relation_query") do
|
|
583
|
-
Parse::Query.new(klassName,
|
|
620
|
+
Parse::Query.new(klassName, parse_field.related_to => self.pointer, limit: :max)
|
|
584
621
|
end
|
|
585
|
-
# fetch the contents of the relation
|
|
622
|
+
# fetch the contents of the relation. An owner without an
|
|
623
|
+
# objectId has nothing on the server; a query for it would send
|
|
624
|
+
# a null objectId, which Parse Server rejects.
|
|
586
625
|
define_method("#{key}_fetch!") do
|
|
626
|
+
next [] if @id.blank?
|
|
587
627
|
q = self.send :"#{key}_relation_query"
|
|
588
628
|
q.results || []
|
|
589
629
|
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
|
|
|
@@ -32,15 +32,50 @@ module Parse
|
|
|
32
32
|
# @overload add(parse_objects)
|
|
33
33
|
# Add an array of Parse::Objects or Parse::Pointers to this collection.
|
|
34
34
|
# @param parse_objects [Array<Parse::Object,Parse::Pointer>] the array to append.
|
|
35
|
+
# An objectId String is accepted and becomes a pointer of the declared
|
|
36
|
+
# class.
|
|
37
|
+
# @raise [ArgumentError] if an item is nil, of another Parse class, or
|
|
38
|
+
# cannot be turned into a pointer.
|
|
35
39
|
# @return [Array<Parse::Object>] the collection
|
|
36
40
|
def add(*items)
|
|
37
|
-
|
|
38
|
-
items.
|
|
39
|
-
|
|
40
|
-
|
|
41
|
+
items = typecast_items(items)
|
|
42
|
+
return @collection if items.empty?
|
|
43
|
+
notify_will_change!
|
|
44
|
+
items.each { |item| collection.push(item) }
|
|
41
45
|
@collection
|
|
42
46
|
end
|
|
43
47
|
|
|
48
|
+
alias_method :push, :add
|
|
49
|
+
|
|
50
|
+
# Add items that are not already part of the collection.
|
|
51
|
+
# @param items [Array<Parse::Object,Parse::Pointer,String>] items to uniquely add
|
|
52
|
+
# @raise [ArgumentError] (see #add)
|
|
53
|
+
# @return [Array<Parse::Object>] the collection
|
|
54
|
+
def add_unique(*items)
|
|
55
|
+
items = typecast_items(items)
|
|
56
|
+
return @collection if items.empty?
|
|
57
|
+
notify_will_change!
|
|
58
|
+
@collection = collection | items
|
|
59
|
+
@collection
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
alias_method :push_unique, :add_unique
|
|
63
|
+
|
|
64
|
+
# @see #add
|
|
65
|
+
def <<(*list)
|
|
66
|
+
add(*list)
|
|
67
|
+
self
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Replace the contents of the collection. The items are validated as in
|
|
71
|
+
# {#add}.
|
|
72
|
+
# @param items [Array<Parse::Object,Parse::Pointer,String>] the new contents.
|
|
73
|
+
# @raise [ArgumentError] (see #add)
|
|
74
|
+
# @return [self]
|
|
75
|
+
def replace(items)
|
|
76
|
+
super(typecast_items(Array(items.is_a?(Parse::CollectionProxy) ? items.to_a : items)))
|
|
77
|
+
end
|
|
78
|
+
|
|
44
79
|
# Removes Parse::Objects from the collection.
|
|
45
80
|
# @overload remove(parse_object)
|
|
46
81
|
# Remove a Parse::Object or Parse::Pointer to this collection.
|
|
@@ -48,40 +83,37 @@ module Parse
|
|
|
48
83
|
# @overload remove(parse_objects)
|
|
49
84
|
# Remove an array of Parse::Objects or Parse::Pointers from this collection.
|
|
50
85
|
# @param parse_objects [Array<Parse::Object,Parse::Pointer>] the array of objects to remove.
|
|
86
|
+
# An objectId String removes the object of the declared class with that id.
|
|
51
87
|
# @return [Array<Parse::Object>] the collection
|
|
52
88
|
def remove(*items)
|
|
53
|
-
|
|
54
|
-
items.
|
|
55
|
-
|
|
56
|
-
|
|
89
|
+
items = typecast_items(items, strict: false)
|
|
90
|
+
return @collection if items.empty?
|
|
91
|
+
notify_will_change!
|
|
92
|
+
items.each { |item| collection.delete item }
|
|
57
93
|
@collection
|
|
58
94
|
end
|
|
59
95
|
|
|
96
|
+
alias_method :delete, :remove
|
|
97
|
+
|
|
60
98
|
# Atomically add a set of Parse::Objects to this collection.
|
|
61
|
-
# This is done by making the API request directly with Parse server; the
|
|
62
|
-
# local object is not updated with changes.
|
|
63
99
|
# @see CollectionProxy#add!
|
|
64
100
|
# @see #add_unique!
|
|
65
101
|
def add!(*items)
|
|
66
|
-
super(items
|
|
102
|
+
super(*typecast_items(items))
|
|
67
103
|
end
|
|
68
104
|
|
|
69
105
|
# Atomically add a set of Parse::Objects to this collection for those not already
|
|
70
106
|
# in the collection.
|
|
71
|
-
# This is done by making the API request directly with Parse server; the
|
|
72
|
-
# local object is not updated with changes.
|
|
73
107
|
# @see CollectionProxy#add_unique!
|
|
74
108
|
# @see #add!
|
|
75
109
|
def add_unique!(*items)
|
|
76
|
-
super(items
|
|
110
|
+
super(*typecast_items(items))
|
|
77
111
|
end
|
|
78
112
|
|
|
79
113
|
# Atomically remove a set of Parse::Objects to this collection.
|
|
80
|
-
# This is done by making the API request directly with Parse server; the
|
|
81
|
-
# local object is not updated with changes.
|
|
82
114
|
# @see CollectionProxy#remove!
|
|
83
115
|
def remove!(*items)
|
|
84
|
-
super(items
|
|
116
|
+
super(*typecast_items(items, strict: false))
|
|
85
117
|
end
|
|
86
118
|
|
|
87
119
|
# Force fetch the set of pointer objects in this collection.
|
|
@@ -130,5 +162,101 @@ module Parse
|
|
|
130
162
|
opts = defaults.merge(opts)
|
|
131
163
|
super(opts)
|
|
132
164
|
end
|
|
165
|
+
|
|
166
|
+
private
|
|
167
|
+
|
|
168
|
+
# Convert the given items to Parse objects of the collection's class.
|
|
169
|
+
# Parse objects and pointers are kept, pointer hashes are built, and an
|
|
170
|
+
# objectId String becomes a pointer of the declared class.
|
|
171
|
+
# @param items [Array] the items to convert (nested arrays are flattened).
|
|
172
|
+
# @param strict [Boolean] when true, an item that cannot be converted, or
|
|
173
|
+
# belongs to another Parse class, raises. When false it is skipped.
|
|
174
|
+
# @raise [ArgumentError] in strict mode, for an invalid item.
|
|
175
|
+
# @return [Array<Parse::Pointer>]
|
|
176
|
+
def typecast_items(items, strict: true)
|
|
177
|
+
items.flatten.each_with_object([]) do |item, list|
|
|
178
|
+
obj = typecast_item(item)
|
|
179
|
+
if obj.nil?
|
|
180
|
+
next unless strict
|
|
181
|
+
raise ArgumentError, "Invalid item #{item.inspect} for #{collection_label}: " \
|
|
182
|
+
"expected a Parse::Object, Parse::Pointer or objectId String."
|
|
183
|
+
end
|
|
184
|
+
unless item_class_allowed?(obj.parse_class)
|
|
185
|
+
next unless strict
|
|
186
|
+
raise ArgumentError, "Invalid item for #{collection_label}: expected a " \
|
|
187
|
+
"#{@parse_class} object, got #{obj.parse_class}."
|
|
188
|
+
end
|
|
189
|
+
list << obj
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# The server returns the field's array as pointer hashes. Each entry
|
|
194
|
+
# becomes an object of the declared class, reusing the local object with
|
|
195
|
+
# the same id so fetched data is kept. The array is adopted only when
|
|
196
|
+
# the declared class is a registered model and every entry is a pointer
|
|
197
|
+
# to that class; anything else (another class, a bare string, a value
|
|
198
|
+
# that is not a pointer) makes the operation apply locally instead,
|
|
199
|
+
# rather than relabeling the entry as the declared class.
|
|
200
|
+
# @return [Array<Parse::Pointer>, nil]
|
|
201
|
+
def adopt_server_items(value)
|
|
202
|
+
return nil if @parse_class.blank? || Parse::Model.find_class(@parse_class).nil?
|
|
203
|
+
local_by_id = {}
|
|
204
|
+
collection.to_a.each do |item|
|
|
205
|
+
id = item.respond_to?(:id) ? item.id : nil
|
|
206
|
+
local_by_id[id] ||= item if id.present?
|
|
207
|
+
end
|
|
208
|
+
value.map do |entry|
|
|
209
|
+
class_name, object_id = server_pointer_parts(entry)
|
|
210
|
+
return nil unless object_id.is_a?(String) && object_id.present?
|
|
211
|
+
return nil unless Parse::Model.same_parse_class?(class_name, @parse_class)
|
|
212
|
+
local_by_id[object_id] || typecast_item(entry) || (return nil)
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# @return [Array(String, String), nil] the className and objectId of a
|
|
217
|
+
# pointer entry from a server reply, or nil when it is not a pointer.
|
|
218
|
+
def server_pointer_parts(entry)
|
|
219
|
+
case entry
|
|
220
|
+
when Parse::Pointer
|
|
221
|
+
[entry.parse_class, entry.id]
|
|
222
|
+
when Hash
|
|
223
|
+
type = entry["__type"] || entry[:__type]
|
|
224
|
+
return nil unless %w[Pointer Object].include?(type.to_s)
|
|
225
|
+
[entry["className"] || entry[:className], entry["objectId"] || entry[:objectId]]
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# @return [Parse::Pointer, nil] the item as a Parse object, or nil.
|
|
230
|
+
def typecast_item(item)
|
|
231
|
+
case item
|
|
232
|
+
when Parse::Pointer
|
|
233
|
+
item
|
|
234
|
+
when Hash
|
|
235
|
+
type = item["__type"] || item[:__type]
|
|
236
|
+
return nil if type.present? && !%w[Pointer Object].include?(type.to_s)
|
|
237
|
+
# A pointer hash is wire data: with a declared class its className
|
|
238
|
+
# is ignored (with a warning), as for server data.
|
|
239
|
+
[item].parse_objects(@parse_class.presence).first
|
|
240
|
+
when String
|
|
241
|
+
return nil if item.blank? || @parse_class.blank?
|
|
242
|
+
Parse::Object.build({ Parse::Model::TYPE_FIELD => Parse::Model::TYPE_POINTER,
|
|
243
|
+
Parse::Model::KEY_CLASS_NAME => @parse_class,
|
|
244
|
+
Parse::Model::OBJECT_ID => item }, @parse_class)
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# @return [Boolean] whether an item of `klass` may be part of this
|
|
249
|
+
# collection. The class is checked when the declared class is a
|
|
250
|
+
# registered model; a declaration naming no model (for example a
|
|
251
|
+
# has_many whose `as:` was left to default) keeps accepting any class.
|
|
252
|
+
def item_class_allowed?(klass)
|
|
253
|
+
return true if @parse_class.blank? || Parse::Model.find_class(@parse_class).nil?
|
|
254
|
+
klass.present? && Parse::Model.same_parse_class?(klass, @parse_class)
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def collection_label
|
|
258
|
+
owner = @delegate.respond_to?(:parse_class) ? @delegate.parse_class : @delegate.class
|
|
259
|
+
@key ? "#{owner}##{@key}" : self.class.name
|
|
260
|
+
end
|
|
133
261
|
end
|
|
134
262
|
end
|
|
@@ -24,18 +24,19 @@ module Parse
|
|
|
24
24
|
# get matching items within the relation collection.
|
|
25
25
|
#
|
|
26
26
|
# When creating a Relation proxy, all the delegate methods defined in the superclasses
|
|
27
|
-
# need to be implemented, in addition to
|
|
28
|
-
# _relation_query and _commit_relation_updates . :'key'_relation_query should return a
|
|
27
|
+
# need to be implemented, in addition to :'key'_relation_query, which should return a
|
|
29
28
|
# Parse::Query object that is properly tied to the foreign table class related to this object column.
|
|
30
29
|
# Example, if an Artist has many Song objects, then the query to be returned by this method
|
|
31
30
|
# should be a Parse::Query for the class 'Song'.
|
|
32
31
|
# Because relation changes are separate from object changes, you can call save on a
|
|
33
|
-
# relation collection to
|
|
34
|
-
#
|
|
35
|
-
# through :'key'_commit_relation_updates message. The delegate is also in charge of
|
|
36
|
-
# clearing out the change information for the collection if saved successfully.
|
|
32
|
+
# relation collection to send only its staged add and remove operations, through the
|
|
33
|
+
# owner's atomic relation operations.
|
|
37
34
|
# @see PointerCollectionProxy
|
|
38
35
|
class RelationCollectionProxy < PointerCollectionProxy
|
|
36
|
+
# @!visibility private
|
|
37
|
+
# Default for {#save}'s `session:`: keep the owner's session as it is.
|
|
38
|
+
SESSION_UNSET = Object.new.freeze
|
|
39
|
+
|
|
39
40
|
define_attribute_methods :additions, :removals
|
|
40
41
|
# @!attribute [r] removals
|
|
41
42
|
# The objects that have been newly removed to this collection
|
|
@@ -51,9 +52,29 @@ module Parse
|
|
|
51
52
|
@removals = []
|
|
52
53
|
end
|
|
53
54
|
|
|
55
|
+
# The items of the relation. The first access queries the server for the
|
|
56
|
+
# related objects, then applies the additions and removals that have not
|
|
57
|
+
# been saved yet. An owner without an objectId has nothing on the server,
|
|
58
|
+
# so no query is sent for it.
|
|
59
|
+
# @return [Array<Parse::Object>]
|
|
60
|
+
def collection
|
|
61
|
+
unless @loaded
|
|
62
|
+
fetched = owner_saved? ? forward(:"#{@key}_fetch!") : nil
|
|
63
|
+
list = fetched.to_a.reject { |item| @removals.include?(item) }
|
|
64
|
+
@additions.each { |item| list.push(item) unless list.include?(item) }
|
|
65
|
+
@collection = list
|
|
66
|
+
@loaded = true
|
|
67
|
+
end
|
|
68
|
+
@collection
|
|
69
|
+
end
|
|
70
|
+
|
|
54
71
|
# You can get items within the collection relation filtered by a specific set
|
|
55
72
|
# of query constraints.
|
|
56
73
|
def all(constraints = {}, &block)
|
|
74
|
+
# An unsaved owner has no relation on the server yet.
|
|
75
|
+
unless owner_saved?
|
|
76
|
+
return block_given? ? collection.each(&block) : collection
|
|
77
|
+
end
|
|
57
78
|
q = query({ limit: :max }.merge(constraints))
|
|
58
79
|
if block_given?
|
|
59
80
|
# if we have a query, then use the Proc with it (more efficient)
|
|
@@ -86,32 +107,44 @@ module Parse
|
|
|
86
107
|
query(limit: count)
|
|
87
108
|
end
|
|
88
109
|
|
|
89
|
-
# Add Parse::Objects to the relation.
|
|
110
|
+
# Add Parse::Objects to the relation. The change is staged and sent with
|
|
111
|
+
# the next save of the owner. Staging does not query the relation.
|
|
112
|
+
# Adding an item that is already staged has no further effect.
|
|
90
113
|
# @overload add(parse_object)
|
|
91
114
|
# Add a Parse::Object or Parse::Pointer to this relation.
|
|
92
115
|
# @param parse_object [Parse::Object,Parse::Pointer] the object to add
|
|
93
116
|
# @overload add(parse_objects)
|
|
94
117
|
# Add an array of Parse::Objects or Parse::Pointers to this relation.
|
|
95
118
|
# @param parse_objects [Array<Parse::Object,Parse::Pointer>] the array to append.
|
|
119
|
+
# @raise [ArgumentError] if an item is nil, of another Parse class, or
|
|
120
|
+
# cannot be turned into a pointer.
|
|
96
121
|
# @return [Array<Parse::Object>] the collection
|
|
97
122
|
def add(*items)
|
|
98
|
-
items = items
|
|
123
|
+
items = typecast_items(items)
|
|
99
124
|
return @collection if items.empty?
|
|
100
125
|
|
|
101
126
|
notify_will_change!
|
|
102
127
|
additions_will_change!
|
|
103
128
|
removals_will_change!
|
|
104
|
-
# take all the items
|
|
105
129
|
items.each do |item|
|
|
106
|
-
@additions.push item
|
|
107
|
-
@
|
|
108
|
-
|
|
109
|
-
@removals.delete item
|
|
130
|
+
@additions.push(item) unless @additions.include?(item)
|
|
131
|
+
@removals.delete(item)
|
|
132
|
+
@collection.push(item) if @loaded && !@collection.include?(item)
|
|
110
133
|
end
|
|
111
134
|
@collection
|
|
112
135
|
end
|
|
113
136
|
|
|
114
|
-
|
|
137
|
+
alias_method :push, :add
|
|
138
|
+
|
|
139
|
+
# Same as {#add}: a relation never holds an object twice.
|
|
140
|
+
def add_unique(*items)
|
|
141
|
+
add(*items)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
alias_method :push_unique, :add_unique
|
|
145
|
+
|
|
146
|
+
# Removes Parse::Objects from the relation. The change is staged and sent
|
|
147
|
+
# with the next save of the owner. Staging does not query the relation.
|
|
115
148
|
# @overload remove(parse_object)
|
|
116
149
|
# Remove a Parse::Object or Parse::Pointer to this relation.
|
|
117
150
|
# @param parse_object [Parse::Object,Parse::Pointer] the object to remove
|
|
@@ -120,52 +153,165 @@ module Parse
|
|
|
120
153
|
# @param parse_objects [Array<Parse::Object,Parse::Pointer>] the array of objects to remove.
|
|
121
154
|
# @return [Array<Parse::Object>] the collection
|
|
122
155
|
def remove(*items)
|
|
123
|
-
items = items
|
|
156
|
+
items = typecast_items(items, strict: false)
|
|
124
157
|
return @collection if items.empty?
|
|
125
158
|
notify_will_change!
|
|
126
159
|
additions_will_change!
|
|
127
160
|
removals_will_change!
|
|
161
|
+
# An unsaved owner has nothing on the server to remove.
|
|
162
|
+
saved = owner_saved?
|
|
128
163
|
items.each do |item|
|
|
129
|
-
@removals.push item
|
|
130
|
-
@
|
|
131
|
-
|
|
132
|
-
@additions.delete item
|
|
164
|
+
@removals.push(item) if saved && !@removals.include?(item)
|
|
165
|
+
@additions.delete(item)
|
|
166
|
+
@collection.delete(item)
|
|
133
167
|
end
|
|
134
168
|
@collection
|
|
135
169
|
end
|
|
136
170
|
|
|
171
|
+
alias_method :delete, :remove
|
|
172
|
+
|
|
173
|
+
# Stage the removal of every object in the relation. This loads the
|
|
174
|
+
# relation to know what to remove.
|
|
175
|
+
# @return [Array<Parse::Object>] the (now empty) collection.
|
|
176
|
+
def clear
|
|
177
|
+
remove(*collection.to_a)
|
|
178
|
+
@collection
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Stage the changes that make the relation hold exactly `items`.
|
|
182
|
+
# @return [self]
|
|
183
|
+
def replace(items)
|
|
184
|
+
items = typecast_items(Array(items.is_a?(Parse::CollectionProxy) ? items.to_a : items))
|
|
185
|
+
current = collection.to_a
|
|
186
|
+
stale = current.reject { |item| items.include?(item) }
|
|
187
|
+
remove(*stale) if stale.any?
|
|
188
|
+
fresh = items.reject { |item| current.include?(item) }
|
|
189
|
+
add(*fresh) if fresh.any?
|
|
190
|
+
self
|
|
191
|
+
end
|
|
192
|
+
|
|
137
193
|
# Atomically add a set of Parse::Objects to this relation.
|
|
138
|
-
# This is done by making the API request directly with Parse server
|
|
139
|
-
#
|
|
194
|
+
# This is done by making the API request directly with Parse server. On
|
|
195
|
+
# success the loaded collection includes the items, and a staged
|
|
196
|
+
# removal of the same items is dropped so a later save does not undo it.
|
|
197
|
+
# On an owner that has not been saved yet the items are staged as in
|
|
198
|
+
# {#add} and sent with the next save.
|
|
199
|
+
# @return [Boolean] whether the operation succeeded.
|
|
140
200
|
def add!(*items)
|
|
141
201
|
return false unless @delegate.respond_to?(:op_add_relation!)
|
|
142
|
-
items = items
|
|
143
|
-
|
|
202
|
+
items = typecast_items(items)
|
|
203
|
+
return true if items.empty?
|
|
204
|
+
return add(*items) && true unless owner_persisted?
|
|
205
|
+
return false unless @delegate.send(:op_add_relation!, @key, items.parse_pointers)
|
|
206
|
+
items.each do |item|
|
|
207
|
+
@removals.delete(item)
|
|
208
|
+
@collection.push(item) if @loaded && !@collection.include?(item)
|
|
209
|
+
end
|
|
210
|
+
true
|
|
144
211
|
end
|
|
145
212
|
|
|
146
|
-
#
|
|
147
|
-
# This is done by making the API request directly with Parse server; the
|
|
148
|
-
# local object is not updated with changes.
|
|
213
|
+
# @see #add!
|
|
149
214
|
def add_unique!(*items)
|
|
150
|
-
|
|
151
|
-
items = items.flatten.parse_pointers
|
|
152
|
-
@delegate.send :op_add_relation!, @key, items
|
|
215
|
+
add!(*items)
|
|
153
216
|
end
|
|
154
217
|
|
|
155
|
-
# Atomically remove a set of Parse::Objects
|
|
156
|
-
# This is done by making the API request directly with Parse server
|
|
157
|
-
#
|
|
218
|
+
# Atomically remove a set of Parse::Objects from this relation.
|
|
219
|
+
# This is done by making the API request directly with Parse server. On
|
|
220
|
+
# success the items leave the loaded collection, and a staged addition
|
|
221
|
+
# of the same items is dropped.
|
|
222
|
+
# @return [Boolean] whether the operation succeeded.
|
|
158
223
|
def remove!(*items)
|
|
159
224
|
return false unless @delegate.respond_to?(:op_remove_relation!)
|
|
160
|
-
items = items
|
|
161
|
-
|
|
225
|
+
items = typecast_items(items, strict: false)
|
|
226
|
+
return true if items.empty?
|
|
227
|
+
return remove(*items) && true unless owner_persisted?
|
|
228
|
+
return false unless @delegate.send(:op_remove_relation!, @key, items.parse_pointers)
|
|
229
|
+
items.each do |item|
|
|
230
|
+
@additions.delete(item)
|
|
231
|
+
@collection.delete(item)
|
|
232
|
+
end
|
|
233
|
+
true
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Marks the staged additions and removals as saved. Called after the
|
|
237
|
+
# owner is saved successfully, so the operations are not sent again by a
|
|
238
|
+
# later save.
|
|
239
|
+
def changes_applied!
|
|
240
|
+
@additions = []
|
|
241
|
+
@removals = []
|
|
242
|
+
super
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# Drops the staged additions and removals. The items are reloaded from
|
|
246
|
+
# the server on the next access.
|
|
247
|
+
def rollback!
|
|
248
|
+
super
|
|
249
|
+
@additions = []
|
|
250
|
+
@removals = []
|
|
251
|
+
reset!
|
|
252
|
+
@collection
|
|
162
253
|
end
|
|
163
254
|
|
|
164
|
-
#
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
255
|
+
# @return [Boolean] true when additions or removals are staged and not
|
|
256
|
+
# yet sent.
|
|
257
|
+
def staged_changes?
|
|
258
|
+
@additions.any? || @removals.any?
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# Drops the staged additions and removals and clears the dirty tracking,
|
|
262
|
+
# without sending anything. When operations were staged, the items are
|
|
263
|
+
# reloaded from the server on the next access, since the loaded list
|
|
264
|
+
# included them.
|
|
265
|
+
def clear_changes!
|
|
266
|
+
staged = staged_changes?
|
|
267
|
+
@additions = []
|
|
268
|
+
@removals = []
|
|
269
|
+
super
|
|
270
|
+
reset! if staged
|
|
271
|
+
@collection
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
# Sends the staged additions and removals of this relation without saving
|
|
275
|
+
# the rest of the owner. The owner must already be saved: an unsaved owner
|
|
276
|
+
# sends its relation additions when it is created.
|
|
277
|
+
#
|
|
278
|
+
# Removals and additions go out as two requests, removals first (each
|
|
279
|
+
# through the owner's atomic relation operation, so {Parse::Role} cache
|
|
280
|
+
# invalidation still runs). The two are not atomic: if the removals
|
|
281
|
+
# succeed and the additions fail, the removals stay applied, they are no
|
|
282
|
+
# longer staged, and the additions stay staged for a retry. An owner
|
|
283
|
+
# save sends relation changes the same way.
|
|
284
|
+
#
|
|
285
|
+
# The requests use the owner's session token from its last
|
|
286
|
+
# `save(session:)`, or the client's default credentials. Pass `session:`
|
|
287
|
+
# to send them as a specific user (a session token String or a
|
|
288
|
+
# {Parse::User}), or `session: nil` to send them with no session token.
|
|
289
|
+
# @param session [String, Parse::User, nil] the session to send the
|
|
290
|
+
# operations with, when given.
|
|
291
|
+
# @return [Boolean] whether the staged operations were applied. False
|
|
292
|
+
# when the owner is not saved yet or a request failed; operations that
|
|
293
|
+
# did not succeed stay staged.
|
|
294
|
+
def save(session: SESSION_UNSET)
|
|
295
|
+
return true if @additions.empty? && @removals.empty?
|
|
296
|
+
return false unless owner_persisted?
|
|
297
|
+
return false unless @delegate.respond_to?(:op_add_relation!) && @delegate.respond_to?(:op_remove_relation!)
|
|
298
|
+
applied = with_owner_session(session) do
|
|
299
|
+
if @removals.any?
|
|
300
|
+
next false unless @delegate.send(:op_remove_relation!, @key, @removals.parse_pointers)
|
|
301
|
+
@removals = []
|
|
302
|
+
end
|
|
303
|
+
if @additions.any?
|
|
304
|
+
next false unless @delegate.send(:op_add_relation!, @key, @additions.parse_pointers)
|
|
305
|
+
@additions = []
|
|
306
|
+
end
|
|
307
|
+
true
|
|
308
|
+
end
|
|
309
|
+
return false unless applied
|
|
310
|
+
clear_changes_information
|
|
311
|
+
if @delegate.respond_to?(:clear_attribute_changes, true)
|
|
312
|
+
@delegate.send(:clear_attribute_changes, [@key.to_s])
|
|
168
313
|
end
|
|
314
|
+
true
|
|
169
315
|
end
|
|
170
316
|
|
|
171
317
|
# @see #add
|
|
@@ -173,5 +319,50 @@ module Parse
|
|
|
173
319
|
list.each { |d| add(d) }
|
|
174
320
|
@collection
|
|
175
321
|
end
|
|
322
|
+
|
|
323
|
+
private
|
|
324
|
+
|
|
325
|
+
attr_writer :additions, :removals
|
|
326
|
+
|
|
327
|
+
# @return [Boolean] whether the owner has an objectId to query by.
|
|
328
|
+
def owner_saved?
|
|
329
|
+
!(@delegate.respond_to?(:id) && @delegate.id.blank?)
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
# @return [Boolean] whether the owner exists on the server, so a write to
|
|
333
|
+
# its relation can be sent. Follows {Parse::Object#persisted?}: an
|
|
334
|
+
# id-only handle to a stored record counts, but an objectId assigned
|
|
335
|
+
# client-side during a create (`parse_reference precompute:`) does not
|
|
336
|
+
# until the create returns, and neither does a destroyed owner.
|
|
337
|
+
def owner_persisted?
|
|
338
|
+
return false unless owner_saved?
|
|
339
|
+
!@delegate.respond_to?(:persisted?) || @delegate.persisted?
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
# Run the block with the owner's session token set to `session`, then
|
|
343
|
+
# restore the owner's previous token. {SESSION_UNSET} runs it unchanged.
|
|
344
|
+
def with_owner_session(session)
|
|
345
|
+
return yield if session.equal?(SESSION_UNSET)
|
|
346
|
+
token = @delegate.send(:_validate_session_token!, session, :save)
|
|
347
|
+
had = @delegate.instance_variable_defined?(:@_session_token)
|
|
348
|
+
previous = @delegate.instance_variable_get(:@_session_token)
|
|
349
|
+
@delegate.instance_variable_set(:@_session_token, token)
|
|
350
|
+
begin
|
|
351
|
+
yield
|
|
352
|
+
ensure
|
|
353
|
+
if had
|
|
354
|
+
@delegate.instance_variable_set(:@_session_token, previous)
|
|
355
|
+
else
|
|
356
|
+
@delegate.remove_instance_variable(:@_session_token)
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
# ActiveModel reads the current value when a change starts. Read the
|
|
362
|
+
# items directly: going through {#collection} would query the whole
|
|
363
|
+
# relation just to stage an add or remove.
|
|
364
|
+
def _read_attribute(attr)
|
|
365
|
+
attr.to_s == "collection" ? @collection : super
|
|
366
|
+
end
|
|
176
367
|
end
|
|
177
368
|
end
|