parse-stack-next 5.7.6 → 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.
Files changed (97) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +830 -0
  3. data/README.md +14 -4
  4. data/docs/TEST_SERVER.md +2 -2
  5. data/docs/acl_clp_guide.md +7 -0
  6. data/docs/atlas_vector_search_guide.md +181 -13
  7. data/docs/client_sdk_guide.md +11 -0
  8. data/docs/mcp_guide.md +317 -6
  9. data/docs/mongodb_direct_guide.md +27 -0
  10. data/docs/usage_guide.md +38 -0
  11. data/docs/webhooks_guide.md +74 -17
  12. data/lib/parse/acl_scope.rb +159 -41
  13. data/lib/parse/agent/approval_gate.rb +0 -0
  14. data/lib/parse/agent/constraint_translator.rb +42 -15
  15. data/lib/parse/agent/describe.rb +3 -1
  16. data/lib/parse/agent/field_names.rb +53 -0
  17. data/lib/parse/agent/field_policy.rb +74 -0
  18. data/lib/parse/agent/mcp_deployments.rb +426 -0
  19. data/lib/parse/agent/mcp_rack_app.rb +424 -45
  20. data/lib/parse/agent/mcp_server.rb +23 -1
  21. data/lib/parse/agent/mcp_subscriptions.rb +124 -6
  22. data/lib/parse/agent/metadata_registry.rb +67 -8
  23. data/lib/parse/agent/prompt_hardening.rb +9 -3
  24. data/lib/parse/agent/tools.rb +378 -29
  25. data/lib/parse/agent.rb +93 -1
  26. data/lib/parse/api/batch.rb +10 -1
  27. data/lib/parse/api/schema.rb +23 -4
  28. data/lib/parse/api/sessions.rb +6 -2
  29. data/lib/parse/api/users.rb +88 -14
  30. data/lib/parse/atlas_search/protected_paths.rb +236 -0
  31. data/lib/parse/atlas_search.rb +95 -23
  32. data/lib/parse/authorization.rb +54 -1
  33. data/lib/parse/client/batch.rb +231 -35
  34. data/lib/parse/client/body_builder.rb +21 -0
  35. data/lib/parse/client/caching.rb +371 -27
  36. data/lib/parse/client/request.rb +26 -14
  37. data/lib/parse/client/response.rb +49 -6
  38. data/lib/parse/client.rb +201 -38
  39. data/lib/parse/clp_scope.rb +281 -23
  40. data/lib/parse/console.rb +2 -2
  41. data/lib/parse/embeddings/voyage.rb +181 -17
  42. data/lib/parse/graphql/type_generator.rb +3 -0
  43. data/lib/parse/model/acl.rb +119 -21
  44. data/lib/parse/model/associations/belongs_to.rb +25 -3
  45. data/lib/parse/model/associations/collection_proxy.rb +138 -17
  46. data/lib/parse/model/associations/has_many.rb +38 -9
  47. data/lib/parse/model/associations/has_one.rb +3 -1
  48. data/lib/parse/model/associations/pointer_collection_proxy.rb +109 -17
  49. data/lib/parse/model/associations/relation_collection_proxy.rb +134 -28
  50. data/lib/parse/model/bytes.rb +13 -5
  51. data/lib/parse/model/classes/role.rb +72 -0
  52. data/lib/parse/model/classes/session.rb +43 -0
  53. data/lib/parse/model/classes/user.rb +78 -3
  54. data/lib/parse/model/core/actions.rb +269 -67
  55. data/lib/parse/model/core/builder.rb +100 -8
  56. data/lib/parse/model/core/create_lock.rb +27 -2
  57. data/lib/parse/model/core/describe.rb +2 -0
  58. data/lib/parse/model/core/fetching.rb +21 -3
  59. data/lib/parse/model/core/pluralized_aliases.rb +8 -4
  60. data/lib/parse/model/core/properties.rb +488 -39
  61. data/lib/parse/model/core/querying.rb +7 -0
  62. data/lib/parse/model/core/schema.rb +5 -3
  63. data/lib/parse/model/core/search_indexing.rb +63 -0
  64. data/lib/parse/model/core/vector_searchable.rb +35 -6
  65. data/lib/parse/model/file.rb +9 -2
  66. data/lib/parse/model/geopoint.rb +61 -13
  67. data/lib/parse/model/model.rb +160 -9
  68. data/lib/parse/model/object.rb +265 -17
  69. data/lib/parse/model/phone.rb +54 -5
  70. data/lib/parse/model/pointer.rb +40 -6
  71. data/lib/parse/mongodb.rb +170 -60
  72. data/lib/parse/pipeline_security.rb +415 -26
  73. data/lib/parse/query/constraint.rb +30 -0
  74. data/lib/parse/query/constraints.rb +58 -32
  75. data/lib/parse/query/cursor.rb +3 -1
  76. data/lib/parse/query/operation.rb +62 -8
  77. data/lib/parse/query/ordering.rb +34 -6
  78. data/lib/parse/query.rb +1100 -134
  79. data/lib/parse/retrieval/agent_tool.rb +225 -8
  80. data/lib/parse/retrieval/benchmark.rb +149 -0
  81. data/lib/parse/retrieval/profiles.rb +320 -0
  82. data/lib/parse/retrieval/retriever.rb +10 -1
  83. data/lib/parse/retrieval.rb +2 -0
  84. data/lib/parse/schema/search_index_migrator.rb +23 -5
  85. data/lib/parse/schema.rb +74 -18
  86. data/lib/parse/stack/tasks.rb +6 -4
  87. data/lib/parse/stack/version.rb +1 -1
  88. data/lib/parse/stack.rb +72 -14
  89. data/lib/parse/two_factor_auth/user_extension.rb +14 -2
  90. data/lib/parse/two_factor_auth.rb +11 -0
  91. data/lib/parse/vector_search/hybrid.rb +36 -18
  92. data/lib/parse/vector_search/index_definition.rb +237 -0
  93. data/lib/parse/vector_search.rb +46 -17
  94. data/lib/parse/webhooks/payload.rb +93 -6
  95. data/lib/parse/webhooks/replay_protection.rb +58 -20
  96. data/lib/parse/webhooks.rb +412 -40
  97. 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
- notify_will_change! if items.count > 0
38
- items.flatten.parse_objects.each do |item|
39
- collection.push(item) if item.is_a?(Parse::Pointer)
40
- end
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
- notify_will_change! if items.count > 0
54
- items.flatten.parse_objects.each do |item|
55
- collection.delete item
56
- end
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.flatten.parse_pointers)
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.flatten.parse_pointers)
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.flatten.parse_pointers)
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.flatten.parse_objects
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
- @collection.push item
108
- #cleanup
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
- # Removes Parse::Objects from the relation.
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.flatten.parse_objects
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
- @collection.delete item
131
- # remove it from any add operations
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; the
139
- # local object is not updated with changes.
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.flatten.parse_pointers
143
- @delegate.send :op_add_relation!, @key, items
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
- # Atomically add a set of Parse::Objects to this relation.
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
- return false unless @delegate.respond_to?(:op_add_relation!)
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 to this relation.
156
- # This is done by making the API request directly with Parse server; the
157
- # local object is not updated with changes.
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.flatten.parse_pointers
161
- @delegate.send :op_remove_relation!, @key, items
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
@@ -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.encode64(str)
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?(String)
50
- @bytes = a
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
- @bytes = a["base64"] || @bytes
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] :master_key Whether the master key should be used for this request.
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 — pass `nil` explicitly. Master key continues to flow
1652
- # via the normal authentication middleware when configured.
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