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
|
@@ -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) }
|
|
45
|
+
@collection
|
|
46
|
+
end
|
|
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
|
|
41
59
|
@collection
|
|
42
60
|
end
|
|
43
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,65 @@ 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
|
+
# @return [Parse::Pointer, nil] the item as a Parse object, or nil.
|
|
194
|
+
def typecast_item(item)
|
|
195
|
+
case item
|
|
196
|
+
when Parse::Pointer
|
|
197
|
+
item
|
|
198
|
+
when Hash
|
|
199
|
+
type = item["__type"] || item[:__type]
|
|
200
|
+
return nil if type.present? && !%w[Pointer Object].include?(type.to_s)
|
|
201
|
+
# A pointer hash is wire data: with a declared class its className
|
|
202
|
+
# is ignored (with a warning), as for server data.
|
|
203
|
+
[item].parse_objects(@parse_class.presence).first
|
|
204
|
+
when String
|
|
205
|
+
return nil if item.blank? || @parse_class.blank?
|
|
206
|
+
Parse::Object.build({ Parse::Model::TYPE_FIELD => Parse::Model::TYPE_POINTER,
|
|
207
|
+
Parse::Model::KEY_CLASS_NAME => @parse_class,
|
|
208
|
+
Parse::Model::OBJECT_ID => item }, @parse_class)
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# @return [Boolean] whether an item of `klass` may be part of this
|
|
213
|
+
# collection. The class is checked when the declared class is a
|
|
214
|
+
# registered model; a declaration naming no model (for example a
|
|
215
|
+
# has_many whose `as:` was left to default) keeps accepting any class.
|
|
216
|
+
def item_class_allowed?(klass)
|
|
217
|
+
return true if @parse_class.blank? || Parse::Model.find_class(@parse_class).nil?
|
|
218
|
+
klass.present? && Parse::Model.same_parse_class?(klass, @parse_class)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def collection_label
|
|
222
|
+
owner = @delegate.respond_to?(:parse_class) ? @delegate.parse_class : @delegate.class
|
|
223
|
+
@key ? "#{owner}##{@key}" : self.class.name
|
|
224
|
+
end
|
|
133
225
|
end
|
|
134
226
|
end
|
|
@@ -51,9 +51,29 @@ module Parse
|
|
|
51
51
|
@removals = []
|
|
52
52
|
end
|
|
53
53
|
|
|
54
|
+
# The items of the relation. The first access queries the server for the
|
|
55
|
+
# related objects, then applies the additions and removals that have not
|
|
56
|
+
# been saved yet. An owner without an objectId has nothing on the server,
|
|
57
|
+
# so no query is sent for it.
|
|
58
|
+
# @return [Array<Parse::Object>]
|
|
59
|
+
def collection
|
|
60
|
+
unless @loaded
|
|
61
|
+
fetched = owner_saved? ? forward(:"#{@key}_fetch!") : nil
|
|
62
|
+
list = fetched.to_a.reject { |item| @removals.include?(item) }
|
|
63
|
+
@additions.each { |item| list.push(item) unless list.include?(item) }
|
|
64
|
+
@collection = list
|
|
65
|
+
@loaded = true
|
|
66
|
+
end
|
|
67
|
+
@collection
|
|
68
|
+
end
|
|
69
|
+
|
|
54
70
|
# You can get items within the collection relation filtered by a specific set
|
|
55
71
|
# of query constraints.
|
|
56
72
|
def all(constraints = {}, &block)
|
|
73
|
+
# An unsaved owner has no relation on the server yet.
|
|
74
|
+
unless owner_saved?
|
|
75
|
+
return block_given? ? collection.each(&block) : collection
|
|
76
|
+
end
|
|
57
77
|
q = query({ limit: :max }.merge(constraints))
|
|
58
78
|
if block_given?
|
|
59
79
|
# if we have a query, then use the Proc with it (more efficient)
|
|
@@ -86,32 +106,44 @@ module Parse
|
|
|
86
106
|
query(limit: count)
|
|
87
107
|
end
|
|
88
108
|
|
|
89
|
-
# Add Parse::Objects to the relation.
|
|
109
|
+
# Add Parse::Objects to the relation. The change is staged and sent with
|
|
110
|
+
# the next save of the owner. Staging does not query the relation.
|
|
111
|
+
# Adding an item that is already staged has no further effect.
|
|
90
112
|
# @overload add(parse_object)
|
|
91
113
|
# Add a Parse::Object or Parse::Pointer to this relation.
|
|
92
114
|
# @param parse_object [Parse::Object,Parse::Pointer] the object to add
|
|
93
115
|
# @overload add(parse_objects)
|
|
94
116
|
# Add an array of Parse::Objects or Parse::Pointers to this relation.
|
|
95
117
|
# @param parse_objects [Array<Parse::Object,Parse::Pointer>] the array to append.
|
|
118
|
+
# @raise [ArgumentError] if an item is nil, of another Parse class, or
|
|
119
|
+
# cannot be turned into a pointer.
|
|
96
120
|
# @return [Array<Parse::Object>] the collection
|
|
97
121
|
def add(*items)
|
|
98
|
-
items = items
|
|
122
|
+
items = typecast_items(items)
|
|
99
123
|
return @collection if items.empty?
|
|
100
124
|
|
|
101
125
|
notify_will_change!
|
|
102
126
|
additions_will_change!
|
|
103
127
|
removals_will_change!
|
|
104
|
-
# take all the items
|
|
105
128
|
items.each do |item|
|
|
106
|
-
@additions.push item
|
|
107
|
-
@
|
|
108
|
-
|
|
109
|
-
@removals.delete item
|
|
129
|
+
@additions.push(item) unless @additions.include?(item)
|
|
130
|
+
@removals.delete(item)
|
|
131
|
+
@collection.push(item) if @loaded && !@collection.include?(item)
|
|
110
132
|
end
|
|
111
133
|
@collection
|
|
112
134
|
end
|
|
113
135
|
|
|
114
|
-
|
|
136
|
+
alias_method :push, :add
|
|
137
|
+
|
|
138
|
+
# Same as {#add}: a relation never holds an object twice.
|
|
139
|
+
def add_unique(*items)
|
|
140
|
+
add(*items)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
alias_method :push_unique, :add_unique
|
|
144
|
+
|
|
145
|
+
# Removes Parse::Objects from the relation. The change is staged and sent
|
|
146
|
+
# with the next save of the owner. Staging does not query the relation.
|
|
115
147
|
# @overload remove(parse_object)
|
|
116
148
|
# Remove a Parse::Object or Parse::Pointer to this relation.
|
|
117
149
|
# @param parse_object [Parse::Object,Parse::Pointer] the object to remove
|
|
@@ -120,45 +152,103 @@ module Parse
|
|
|
120
152
|
# @param parse_objects [Array<Parse::Object,Parse::Pointer>] the array of objects to remove.
|
|
121
153
|
# @return [Array<Parse::Object>] the collection
|
|
122
154
|
def remove(*items)
|
|
123
|
-
items = items
|
|
155
|
+
items = typecast_items(items, strict: false)
|
|
124
156
|
return @collection if items.empty?
|
|
125
157
|
notify_will_change!
|
|
126
158
|
additions_will_change!
|
|
127
159
|
removals_will_change!
|
|
160
|
+
# An unsaved owner has nothing on the server to remove.
|
|
161
|
+
saved = owner_saved?
|
|
128
162
|
items.each do |item|
|
|
129
|
-
@removals.push item
|
|
130
|
-
@
|
|
131
|
-
|
|
132
|
-
@additions.delete item
|
|
163
|
+
@removals.push(item) if saved && !@removals.include?(item)
|
|
164
|
+
@additions.delete(item)
|
|
165
|
+
@collection.delete(item)
|
|
133
166
|
end
|
|
134
167
|
@collection
|
|
135
168
|
end
|
|
136
169
|
|
|
170
|
+
alias_method :delete, :remove
|
|
171
|
+
|
|
172
|
+
# Stage the removal of every object in the relation. This loads the
|
|
173
|
+
# relation to know what to remove.
|
|
174
|
+
# @return [Array<Parse::Object>] the (now empty) collection.
|
|
175
|
+
def clear
|
|
176
|
+
remove(*collection.to_a)
|
|
177
|
+
@collection
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Stage the changes that make the relation hold exactly `items`.
|
|
181
|
+
# @return [self]
|
|
182
|
+
def replace(items)
|
|
183
|
+
items = typecast_items(Array(items.is_a?(Parse::CollectionProxy) ? items.to_a : items))
|
|
184
|
+
current = collection.to_a
|
|
185
|
+
stale = current.reject { |item| items.include?(item) }
|
|
186
|
+
remove(*stale) if stale.any?
|
|
187
|
+
fresh = items.reject { |item| current.include?(item) }
|
|
188
|
+
add(*fresh) if fresh.any?
|
|
189
|
+
self
|
|
190
|
+
end
|
|
191
|
+
|
|
137
192
|
# Atomically add a set of Parse::Objects to this relation.
|
|
138
|
-
# This is done by making the API request directly with Parse server
|
|
139
|
-
#
|
|
193
|
+
# This is done by making the API request directly with Parse server. On
|
|
194
|
+
# success the loaded collection includes the items, and a staged
|
|
195
|
+
# removal of the same items is dropped so a later save does not undo it.
|
|
196
|
+
# On an owner that has not been saved yet the items are staged as in
|
|
197
|
+
# {#add} and sent with the next save.
|
|
198
|
+
# @return [Boolean] whether the operation succeeded.
|
|
140
199
|
def add!(*items)
|
|
141
200
|
return false unless @delegate.respond_to?(:op_add_relation!)
|
|
142
|
-
items = items
|
|
143
|
-
|
|
201
|
+
items = typecast_items(items)
|
|
202
|
+
return true if items.empty?
|
|
203
|
+
return add(*items) && true unless owner_saved?
|
|
204
|
+
return false unless @delegate.send(:op_add_relation!, @key, items.parse_pointers)
|
|
205
|
+
items.each do |item|
|
|
206
|
+
@removals.delete(item)
|
|
207
|
+
@collection.push(item) if @loaded && !@collection.include?(item)
|
|
208
|
+
end
|
|
209
|
+
true
|
|
144
210
|
end
|
|
145
211
|
|
|
146
|
-
#
|
|
147
|
-
# This is done by making the API request directly with Parse server; the
|
|
148
|
-
# local object is not updated with changes.
|
|
212
|
+
# @see #add!
|
|
149
213
|
def add_unique!(*items)
|
|
150
|
-
|
|
151
|
-
items = items.flatten.parse_pointers
|
|
152
|
-
@delegate.send :op_add_relation!, @key, items
|
|
214
|
+
add!(*items)
|
|
153
215
|
end
|
|
154
216
|
|
|
155
|
-
# Atomically remove a set of Parse::Objects
|
|
156
|
-
# This is done by making the API request directly with Parse server
|
|
157
|
-
#
|
|
217
|
+
# Atomically remove a set of Parse::Objects from this relation.
|
|
218
|
+
# This is done by making the API request directly with Parse server. On
|
|
219
|
+
# success the items leave the loaded collection, and a staged addition
|
|
220
|
+
# of the same items is dropped.
|
|
221
|
+
# @return [Boolean] whether the operation succeeded.
|
|
158
222
|
def remove!(*items)
|
|
159
223
|
return false unless @delegate.respond_to?(:op_remove_relation!)
|
|
160
|
-
items = items
|
|
161
|
-
|
|
224
|
+
items = typecast_items(items, strict: false)
|
|
225
|
+
return true if items.empty?
|
|
226
|
+
return remove(*items) && true unless owner_saved?
|
|
227
|
+
return false unless @delegate.send(:op_remove_relation!, @key, items.parse_pointers)
|
|
228
|
+
items.each do |item|
|
|
229
|
+
@additions.delete(item)
|
|
230
|
+
@collection.delete(item)
|
|
231
|
+
end
|
|
232
|
+
true
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Marks the staged additions and removals as saved. Called after the
|
|
236
|
+
# owner is saved successfully, so the operations are not sent again by a
|
|
237
|
+
# later save.
|
|
238
|
+
def changes_applied!
|
|
239
|
+
@additions = []
|
|
240
|
+
@removals = []
|
|
241
|
+
super
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# Drops the staged additions and removals. The items are reloaded from
|
|
245
|
+
# the server on the next access.
|
|
246
|
+
def rollback!
|
|
247
|
+
super
|
|
248
|
+
@additions = []
|
|
249
|
+
@removals = []
|
|
250
|
+
reset!
|
|
251
|
+
@collection
|
|
162
252
|
end
|
|
163
253
|
|
|
164
254
|
# Save the changes to the relation
|
|
@@ -173,5 +263,21 @@ module Parse
|
|
|
173
263
|
list.each { |d| add(d) }
|
|
174
264
|
@collection
|
|
175
265
|
end
|
|
266
|
+
|
|
267
|
+
private
|
|
268
|
+
|
|
269
|
+
attr_writer :additions, :removals
|
|
270
|
+
|
|
271
|
+
# @return [Boolean] whether the owner has an objectId to query by.
|
|
272
|
+
def owner_saved?
|
|
273
|
+
!(@delegate.respond_to?(:id) && @delegate.id.blank?)
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# ActiveModel reads the current value when a change starts. Read the
|
|
277
|
+
# items directly: going through {#collection} would query the whole
|
|
278
|
+
# relation just to stage an add or remove.
|
|
279
|
+
def _read_attribute(attr)
|
|
280
|
+
attr.to_s == "collection" ? @collection : super
|
|
281
|
+
end
|
|
176
282
|
end
|
|
177
283
|
end
|
data/lib/parse/model/bytes.rb
CHANGED
|
@@ -34,10 +34,12 @@ module Parse
|
|
|
34
34
|
ATTRIBUTES
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
-
# Base64 encode and set the instance contents
|
|
37
|
+
# Base64 encode and set the instance contents. Uses strict (RFC 4648)
|
|
38
|
+
# encoding: `Base64.encode64` inserts a newline every 60 characters,
|
|
39
|
+
# which Parse Server stores verbatim in the Bytes column.
|
|
38
40
|
# @param str the string to encode
|
|
39
41
|
def encode(str)
|
|
40
|
-
@base64 = Base64.
|
|
42
|
+
@base64 = Base64.strict_encode64(str.to_s)
|
|
41
43
|
end
|
|
42
44
|
|
|
43
45
|
# Get the content as decoded base64 bytes
|
|
@@ -45,11 +47,17 @@ module Parse
|
|
|
45
47
|
Base64.decode64(@base64 || "")
|
|
46
48
|
end
|
|
47
49
|
|
|
50
|
+
# Mass-assign the base64 content from a base64 String, a Parse Bytes
|
|
51
|
+
# hash (`{"__type" => "Bytes", "base64" => "..."}`), or another
|
|
52
|
+
# {Parse::Bytes}.
|
|
53
|
+
# @param a [String, Hash, Parse::Bytes]
|
|
48
54
|
def attributes=(a)
|
|
49
|
-
if a.is_a?(
|
|
50
|
-
@
|
|
55
|
+
if a.is_a?(Bytes)
|
|
56
|
+
@base64 = a.base64.dup
|
|
57
|
+
elsif a.is_a?(String)
|
|
58
|
+
@base64 = a.dup
|
|
51
59
|
elsif a.is_a?(Hash)
|
|
52
|
-
@
|
|
60
|
+
@base64 = a["base64"] || a[:base64] || @base64
|
|
53
61
|
end
|
|
54
62
|
end
|
|
55
63
|
|
|
@@ -62,6 +62,78 @@ module Parse
|
|
|
62
62
|
# @return [RelationCollectionProxy<User>] a Parse relation of users belonging to this role.
|
|
63
63
|
has_many :users, through: :relation
|
|
64
64
|
|
|
65
|
+
# Grants and revocations reach mongo-direct ACL resolution immediately
|
|
66
|
+
# rather than after the role-cache TTL: the users whose membership a save
|
|
67
|
+
# changes have their cached role closures dropped, and a change to the
|
|
68
|
+
# role hierarchy drops them all.
|
|
69
|
+
before_save :_capture_role_membership_changes
|
|
70
|
+
after_save :_invalidate_role_caches
|
|
71
|
+
|
|
72
|
+
# Atomic relation operations (`role.users.add!`, `role.roles.remove!`)
|
|
73
|
+
# write immediately without a save, so they invalidate the same caches.
|
|
74
|
+
# @!visibility private
|
|
75
|
+
def op_add_relation!(field, objects = [])
|
|
76
|
+
result = super
|
|
77
|
+
_invalidate_after_relation_op(field, objects) if result
|
|
78
|
+
result
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# @!visibility private
|
|
82
|
+
def op_remove_relation!(field, objects = [])
|
|
83
|
+
result = super
|
|
84
|
+
_invalidate_after_relation_op(field, objects) if result
|
|
85
|
+
result
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# @!visibility private
|
|
89
|
+
def _invalidate_after_relation_op(field, objects)
|
|
90
|
+
key = field.to_s
|
|
91
|
+
if %w[roles].include?(key)
|
|
92
|
+
@_role_hierarchy_changed = true
|
|
93
|
+
elsif %w[users].include?(key)
|
|
94
|
+
@_role_membership_changed_ids = Array(objects).filter_map do |u|
|
|
95
|
+
id = u.respond_to?(:id) ? u.id : (u.is_a?(Hash) ? (u["objectId"] || u[:objectId]) : u)
|
|
96
|
+
id.to_s if id.present?
|
|
97
|
+
end
|
|
98
|
+
else
|
|
99
|
+
return
|
|
100
|
+
end
|
|
101
|
+
_invalidate_role_caches
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# @!visibility private
|
|
105
|
+
def _capture_role_membership_changes
|
|
106
|
+
users_proxy = instance_variable_get(:@users)
|
|
107
|
+
ids = []
|
|
108
|
+
if users_proxy.respond_to?(:additions)
|
|
109
|
+
(Array(users_proxy.additions) + Array(users_proxy.removals)).each do |u|
|
|
110
|
+
id = u.respond_to?(:id) ? u.id : u
|
|
111
|
+
ids << id.to_s if id.present?
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
roles_proxy = instance_variable_get(:@roles)
|
|
115
|
+
@_role_hierarchy_changed = roles_proxy.respond_to?(:additions) &&
|
|
116
|
+
(Array(roles_proxy.additions).any? || Array(roles_proxy.removals).any?)
|
|
117
|
+
@_role_membership_changed_ids = ids.uniq
|
|
118
|
+
nil
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# @!visibility private
|
|
122
|
+
def _invalidate_role_caches
|
|
123
|
+
auth = client.respond_to?(:authorization) ? client.authorization : nil
|
|
124
|
+
return unless auth
|
|
125
|
+
if @_role_hierarchy_changed && auth.respond_to?(:invalidate_all_roles)
|
|
126
|
+
auth.invalidate_all_roles
|
|
127
|
+
elsif auth.respond_to?(:invalidate_user_roles)
|
|
128
|
+
Array(@_role_membership_changed_ids).each { |id| auth.invalidate_user_roles(id) }
|
|
129
|
+
end
|
|
130
|
+
rescue StandardError => e
|
|
131
|
+
warn "[Parse::Role] role cache invalidation failed: #{e.class}"
|
|
132
|
+
ensure
|
|
133
|
+
@_role_membership_changed_ids = nil
|
|
134
|
+
@_role_hierarchy_changed = nil
|
|
135
|
+
end
|
|
136
|
+
|
|
65
137
|
# Names of Mongo driver errors that mean "the server is momentarily
|
|
66
138
|
# unreachable", for which falling back to the Parse Server walk is right.
|
|
67
139
|
#
|
|
@@ -204,5 +204,48 @@ module Parse
|
|
|
204
204
|
def revoke!
|
|
205
205
|
destroy
|
|
206
206
|
end
|
|
207
|
+
|
|
208
|
+
# Deletes the session and, on success, forgets it in the client's
|
|
209
|
+
# identity plane so mongo-direct and Atlas Search reads stop resolving
|
|
210
|
+
# the token at once instead of when the cached entry expires. The token
|
|
211
|
+
# is dropped when this instance carries it; the owning user's entries
|
|
212
|
+
# are dropped as well, since a session fetched without the master key
|
|
213
|
+
# does not carry its token.
|
|
214
|
+
# @param session [String] (see Parse::Object#destroy)
|
|
215
|
+
# @return [Boolean] whether the operation was successful.
|
|
216
|
+
def destroy(session: nil)
|
|
217
|
+
token = @session_token
|
|
218
|
+
# Read the association ivar directly: calling `user` on a partially
|
|
219
|
+
# fetched session could trigger an autofetch just to learn the owner.
|
|
220
|
+
owner = @user
|
|
221
|
+
owner_id = owner.respond_to?(:id) ? owner.id : nil
|
|
222
|
+
success = super
|
|
223
|
+
if success
|
|
224
|
+
cl = client
|
|
225
|
+
cl.invalidate_session_identity(token) if token.is_a?(String) && cl.respond_to?(:invalidate_session_identity)
|
|
226
|
+
cl.invalidate_user_identity(owner_id) if owner_id && cl.respond_to?(:invalidate_user_identity)
|
|
227
|
+
end
|
|
228
|
+
success
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Serialization omits `sessionToken` unless `include_session_token: true`
|
|
232
|
+
# is passed. A session token is a bearer credential, and `as_json` is the
|
|
233
|
+
# surface that reaches logs, API responses and agent tool output.
|
|
234
|
+
# @param opts [Hash] see Parse::Object#as_json.
|
|
235
|
+
# @option opts [Boolean] :include_session_token include the token.
|
|
236
|
+
# @return [Hash]
|
|
237
|
+
def as_json(opts = nil)
|
|
238
|
+
opts = (opts || {}).dup
|
|
239
|
+
include_token = opts.delete(:include_session_token) == true
|
|
240
|
+
json = super(opts)
|
|
241
|
+
return json if include_token || !json.is_a?(Hash)
|
|
242
|
+
json.except("session_token", "sessionToken", :session_token, :sessionToken)
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# Redacts the session token from the default inspect output.
|
|
246
|
+
# @return [String]
|
|
247
|
+
def inspect
|
|
248
|
+
Parse::User.redact_session_token(super, @session_token)
|
|
249
|
+
end
|
|
207
250
|
end
|
|
208
251
|
end
|
|
@@ -202,6 +202,17 @@ module Parse
|
|
|
202
202
|
class User < Parse::Object
|
|
203
203
|
parse_class Parse::Model::CLASS_USER
|
|
204
204
|
|
|
205
|
+
# ActiveModel naming relative to the `Parse` namespace, so Rails forms,
|
|
206
|
+
# params, routes, and i18n see `Parse::User` as `user`
|
|
207
|
+
# (`params[:user]`, `users_path`) rather than `parse_user`. `name` stays
|
|
208
|
+
# "Parse::User". A subclass outside the `Parse` namespace keeps the
|
|
209
|
+
# default naming.
|
|
210
|
+
# @return [ActiveModel::Name]
|
|
211
|
+
def self.model_name
|
|
212
|
+
return super if name.nil? || !name.start_with?("Parse::")
|
|
213
|
+
@_relative_model_name ||= ActiveModel::Name.new(self, Parse)
|
|
214
|
+
end
|
|
215
|
+
|
|
205
216
|
# When true (default), saving a new {Parse::User} that has a `password`
|
|
206
217
|
# value routes through Parse Server's signup endpoint (`POST /parse/users`)
|
|
207
218
|
# with the `X-Parse-Revocable-Session` header set, so the signup response
|
|
@@ -908,6 +919,57 @@ module Parse
|
|
|
908
919
|
self.session_token.present?
|
|
909
920
|
end
|
|
910
921
|
|
|
922
|
+
# Replace each non-empty token in `text` with `[FILTERED]`. Used by the
|
|
923
|
+
# `inspect` overrides on {Parse::User} and {Parse::Session} so a session
|
|
924
|
+
# token does not land in logs, exception messages or console history.
|
|
925
|
+
# @!visibility private
|
|
926
|
+
# @param text [String] the inspect output.
|
|
927
|
+
# @param tokens [Array<String, nil>] tokens to redact.
|
|
928
|
+
# @return [String]
|
|
929
|
+
def self.redact_session_token(text, *tokens)
|
|
930
|
+
out = text.to_s
|
|
931
|
+
tokens.each do |t|
|
|
932
|
+
next unless t.is_a?(String) && !t.strip.empty?
|
|
933
|
+
out = out.gsub(t, "[FILTERED]")
|
|
934
|
+
end
|
|
935
|
+
out
|
|
936
|
+
end
|
|
937
|
+
|
|
938
|
+
# Default inspect output with the session token redacted.
|
|
939
|
+
# @return [String]
|
|
940
|
+
def inspect
|
|
941
|
+
self.class.redact_session_token(super, @session_token, @_session_token)
|
|
942
|
+
end
|
|
943
|
+
|
|
944
|
+
# Saves pending changes. When the save changes the password, Parse Server
|
|
945
|
+
# revokes the user's other sessions, so this client's identity plane
|
|
946
|
+
# drops every cached entry for the user to make the revocation visible
|
|
947
|
+
# to mongo-direct reads at once.
|
|
948
|
+
# @!visibility private
|
|
949
|
+
def update!(raw: false, force: false)
|
|
950
|
+
password_changing = changed.include?("password")
|
|
951
|
+
result = super
|
|
952
|
+
ok = raw ? (result.respond_to?(:success?) && result.success?) : result == true
|
|
953
|
+
if ok && password_changing && client.respond_to?(:invalidate_user_identity)
|
|
954
|
+
client.invalidate_user_identity(id)
|
|
955
|
+
end
|
|
956
|
+
result
|
|
957
|
+
end
|
|
958
|
+
|
|
959
|
+
# Deletes the user. On success every cached identity entry for the user
|
|
960
|
+
# is dropped from this client's identity plane, since Parse Server
|
|
961
|
+
# removes the user's sessions with the account.
|
|
962
|
+
# @param session [String] (see Parse::Object#destroy)
|
|
963
|
+
# @return [Boolean] whether the operation was successful.
|
|
964
|
+
def destroy(session: nil)
|
|
965
|
+
user_id = id
|
|
966
|
+
success = super
|
|
967
|
+
if success && client.respond_to?(:invalidate_user_identity)
|
|
968
|
+
client.invalidate_user_identity(user_id)
|
|
969
|
+
end
|
|
970
|
+
success
|
|
971
|
+
end
|
|
972
|
+
|
|
911
973
|
# Invalid the current session token for this logged in user.
|
|
912
974
|
# @return [Boolean] True/false if successful
|
|
913
975
|
def logout
|
|
@@ -1016,7 +1078,10 @@ module Parse
|
|
|
1016
1078
|
# the request.
|
|
1017
1079
|
#
|
|
1018
1080
|
# @param body [Hash] The hash containing the Parse::User fields. The field `username` and `password` are required.
|
|
1019
|
-
# @option opts [Boolean] :
|
|
1081
|
+
# @option opts [Boolean] :use_master_key send the master key with the
|
|
1082
|
+
# signup. Off by default: a master-key signup returns no session token
|
|
1083
|
+
# and bypasses the `_User` create CLP. Pass `true` only for admin
|
|
1084
|
+
# provisioning that needs that bypass.
|
|
1020
1085
|
# @raise [ArgumentError] If `body` contains `authData`/`auth_data`/`objectId` — use {.autologin_service} for federated flows.
|
|
1021
1086
|
# @raise [Parse::Error::UsernameMissingError] If username is missing.
|
|
1022
1087
|
# @raise [Parse::Error::PasswordMissingError] If password is missing.
|
|
@@ -1145,6 +1210,8 @@ module Parse
|
|
|
1145
1210
|
# @param username [String] the user's username
|
|
1146
1211
|
# @param password [String] the user's password
|
|
1147
1212
|
# @return [User] a logged in user for the provided username. Returns nil otherwise.
|
|
1213
|
+
# @raise [Parse::MFA::RequiredError] when the account has MFA enabled; use
|
|
1214
|
+
# {.login_with_mfa} with the user's code.
|
|
1148
1215
|
# @see .login!
|
|
1149
1216
|
def self.login(username, password)
|
|
1150
1217
|
response = client.login(username.to_s, password.to_s)
|
|
@@ -1350,6 +1417,13 @@ module Parse
|
|
|
1350
1417
|
end
|
|
1351
1418
|
n
|
|
1352
1419
|
end
|
|
1420
|
+
# Every session but (optionally) the current one is gone server-side.
|
|
1421
|
+
# Drop the user's identity entries on this client and on the default
|
|
1422
|
+
# client (the logout above went through `Parse.client`). A kept
|
|
1423
|
+
# current token simply re-resolves on its next use.
|
|
1424
|
+
[client, Parse.client].uniq.each do |cl|
|
|
1425
|
+
cl.invalidate_user_identity(id) if cl.respond_to?(:invalidate_user_identity)
|
|
1426
|
+
end
|
|
1353
1427
|
@session_token = nil unless keep_current
|
|
1354
1428
|
@session = nil unless keep_current
|
|
1355
1429
|
count
|
|
@@ -1648,8 +1722,9 @@ module Parse
|
|
|
1648
1722
|
# Cloud Code `beforeSave(_User)` see `request.user = caller`,
|
|
1649
1723
|
# which an integrator can mistake for "the new user". The signup
|
|
1650
1724
|
# endpoint authenticates by the signup itself, not by a prior
|
|
1651
|
-
# session
|
|
1652
|
-
#
|
|
1725
|
+
# session, so pass `nil` explicitly. The master key is not sent
|
|
1726
|
+
# either (see Parse::API::Users#create_user): Parse Server mints no
|
|
1727
|
+
# session token for a master-key signup.
|
|
1653
1728
|
res = client.create_user(body, session_token: nil)
|
|
1654
1729
|
unless res.error?
|
|
1655
1730
|
result = res.result
|