parse-stack-next 4.5.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 (178) hide show
  1. checksums.yaml +7 -0
  2. data/.bundle/config +2 -0
  3. data/.env.sample +112 -0
  4. data/.env.test +10 -0
  5. data/.github/workflows/ruby.yml +36 -0
  6. data/.gitignore +49 -0
  7. data/.ruby-version +1 -0
  8. data/.solargraph.yml +22 -0
  9. data/CHANGELOG.md +5816 -0
  10. data/Gemfile +30 -0
  11. data/Gemfile.lock +175 -0
  12. data/LICENSE.txt +23 -0
  13. data/Makefile +63 -0
  14. data/README.md +5655 -0
  15. data/Rakefile +573 -0
  16. data/bin/console +38 -0
  17. data/bin/parse-console +136 -0
  18. data/bin/server +17 -0
  19. data/bin/setup +7 -0
  20. data/config/parse-config.json +12 -0
  21. data/docs/TEST_SERVER.md +271 -0
  22. data/docs/_config.yml +1 -0
  23. data/docs/mcp_guide.md +3484 -0
  24. data/docs/mongodb_direct_guide.md +1348 -0
  25. data/docs/mongodb_index_optimization_guide.md +631 -0
  26. data/examples/transaction_example.rb +219 -0
  27. data/lib/parse/acl_scope.rb +728 -0
  28. data/lib/parse/agent/cancellation_token.rb +80 -0
  29. data/lib/parse/agent/constraint_translator.rb +480 -0
  30. data/lib/parse/agent/describe.rb +420 -0
  31. data/lib/parse/agent/errors.rb +133 -0
  32. data/lib/parse/agent/mcp_client.rb +557 -0
  33. data/lib/parse/agent/mcp_dispatcher.rb +1023 -0
  34. data/lib/parse/agent/mcp_rack_app.rb +1143 -0
  35. data/lib/parse/agent/mcp_server.rb +376 -0
  36. data/lib/parse/agent/metadata_audit.rb +259 -0
  37. data/lib/parse/agent/metadata_dsl.rb +733 -0
  38. data/lib/parse/agent/metadata_registry.rb +794 -0
  39. data/lib/parse/agent/pipeline_validator.rb +82 -0
  40. data/lib/parse/agent/prompts.rb +351 -0
  41. data/lib/parse/agent/rate_limiter.rb +158 -0
  42. data/lib/parse/agent/relation_graph.rb +162 -0
  43. data/lib/parse/agent/result_formatter.rb +453 -0
  44. data/lib/parse/agent/tools.rb +5489 -0
  45. data/lib/parse/agent.rb +3249 -0
  46. data/lib/parse/api/aggregate.rb +79 -0
  47. data/lib/parse/api/all.rb +26 -0
  48. data/lib/parse/api/analytics.rb +18 -0
  49. data/lib/parse/api/batch.rb +33 -0
  50. data/lib/parse/api/cloud_functions.rb +58 -0
  51. data/lib/parse/api/config.rb +125 -0
  52. data/lib/parse/api/files.rb +29 -0
  53. data/lib/parse/api/hooks.rb +117 -0
  54. data/lib/parse/api/objects.rb +146 -0
  55. data/lib/parse/api/path_segment.rb +75 -0
  56. data/lib/parse/api/push.rb +20 -0
  57. data/lib/parse/api/schema.rb +49 -0
  58. data/lib/parse/api/server.rb +50 -0
  59. data/lib/parse/api/sessions.rb +24 -0
  60. data/lib/parse/api/users.rb +250 -0
  61. data/lib/parse/atlas_search/index_manager.rb +353 -0
  62. data/lib/parse/atlas_search/result.rb +204 -0
  63. data/lib/parse/atlas_search/search_builder.rb +604 -0
  64. data/lib/parse/atlas_search/session.rb +253 -0
  65. data/lib/parse/atlas_search.rb +995 -0
  66. data/lib/parse/client/authentication.rb +97 -0
  67. data/lib/parse/client/batch.rb +234 -0
  68. data/lib/parse/client/body_builder.rb +240 -0
  69. data/lib/parse/client/caching.rb +203 -0
  70. data/lib/parse/client/logging.rb +293 -0
  71. data/lib/parse/client/profiling.rb +181 -0
  72. data/lib/parse/client/protocol.rb +91 -0
  73. data/lib/parse/client/request.rb +233 -0
  74. data/lib/parse/client/response.rb +208 -0
  75. data/lib/parse/client.rb +1104 -0
  76. data/lib/parse/clp_scope.rb +361 -0
  77. data/lib/parse/live_query/circuit_breaker.rb +256 -0
  78. data/lib/parse/live_query/client.rb +1001 -0
  79. data/lib/parse/live_query/configuration.rb +224 -0
  80. data/lib/parse/live_query/event.rb +115 -0
  81. data/lib/parse/live_query/event_queue.rb +272 -0
  82. data/lib/parse/live_query/health_monitor.rb +214 -0
  83. data/lib/parse/live_query/logging.rb +149 -0
  84. data/lib/parse/live_query/subscription.rb +294 -0
  85. data/lib/parse/live_query.rb +163 -0
  86. data/lib/parse/lookup_rewriter.rb +445 -0
  87. data/lib/parse/model/acl.rb +968 -0
  88. data/lib/parse/model/associations/belongs_to.rb +275 -0
  89. data/lib/parse/model/associations/collection_proxy.rb +435 -0
  90. data/lib/parse/model/associations/has_many.rb +597 -0
  91. data/lib/parse/model/associations/has_one.rb +158 -0
  92. data/lib/parse/model/associations/pointer_collection_proxy.rb +134 -0
  93. data/lib/parse/model/associations/relation_collection_proxy.rb +177 -0
  94. data/lib/parse/model/bytes.rb +62 -0
  95. data/lib/parse/model/classes/audience.rb +262 -0
  96. data/lib/parse/model/classes/installation.rb +363 -0
  97. data/lib/parse/model/classes/job_schedule.rb +153 -0
  98. data/lib/parse/model/classes/job_status.rb +264 -0
  99. data/lib/parse/model/classes/product.rb +75 -0
  100. data/lib/parse/model/classes/push_status.rb +263 -0
  101. data/lib/parse/model/classes/role.rb +751 -0
  102. data/lib/parse/model/classes/session.rb +201 -0
  103. data/lib/parse/model/classes/user.rb +943 -0
  104. data/lib/parse/model/clp.rb +544 -0
  105. data/lib/parse/model/core/actions.rb +1268 -0
  106. data/lib/parse/model/core/builder.rb +139 -0
  107. data/lib/parse/model/core/create_lock.rb +386 -0
  108. data/lib/parse/model/core/describe.rb +382 -0
  109. data/lib/parse/model/core/enhanced_change_tracking.rb +159 -0
  110. data/lib/parse/model/core/errors.rb +38 -0
  111. data/lib/parse/model/core/fetching.rb +566 -0
  112. data/lib/parse/model/core/field_guards.rb +220 -0
  113. data/lib/parse/model/core/indexing.rb +382 -0
  114. data/lib/parse/model/core/parse_reference.rb +407 -0
  115. data/lib/parse/model/core/properties.rb +809 -0
  116. data/lib/parse/model/core/querying.rb +491 -0
  117. data/lib/parse/model/core/schema.rb +202 -0
  118. data/lib/parse/model/core/search_indexing.rb +174 -0
  119. data/lib/parse/model/date.rb +88 -0
  120. data/lib/parse/model/email.rb +213 -0
  121. data/lib/parse/model/file.rb +527 -0
  122. data/lib/parse/model/geojson.rb +271 -0
  123. data/lib/parse/model/geopoint.rb +261 -0
  124. data/lib/parse/model/model.rb +260 -0
  125. data/lib/parse/model/object.rb +2068 -0
  126. data/lib/parse/model/phone.rb +520 -0
  127. data/lib/parse/model/pointer.rb +443 -0
  128. data/lib/parse/model/polygon.rb +406 -0
  129. data/lib/parse/model/push.rb +975 -0
  130. data/lib/parse/model/shortnames.rb +8 -0
  131. data/lib/parse/model/time_zone.rb +141 -0
  132. data/lib/parse/model/validations/uniqueness_validator.rb +97 -0
  133. data/lib/parse/model/validations.rb +96 -0
  134. data/lib/parse/mongodb.rb +2300 -0
  135. data/lib/parse/pipeline_security.rb +554 -0
  136. data/lib/parse/query/constraint.rb +198 -0
  137. data/lib/parse/query/constraints.rb +3279 -0
  138. data/lib/parse/query/cursor.rb +434 -0
  139. data/lib/parse/query/n_plus_one_detector.rb +445 -0
  140. data/lib/parse/query/operation.rb +104 -0
  141. data/lib/parse/query/ordering.rb +66 -0
  142. data/lib/parse/query.rb +7028 -0
  143. data/lib/parse/schema/index_migrator.rb +291 -0
  144. data/lib/parse/schema/search_index_migrator.rb +289 -0
  145. data/lib/parse/schema.rb +494 -0
  146. data/lib/parse/stack/generators/rails.rb +40 -0
  147. data/lib/parse/stack/generators/templates/model.erb +51 -0
  148. data/lib/parse/stack/generators/templates/model_installation.rb +4 -0
  149. data/lib/parse/stack/generators/templates/model_role.rb +4 -0
  150. data/lib/parse/stack/generators/templates/model_session.rb +4 -0
  151. data/lib/parse/stack/generators/templates/model_user.rb +11 -0
  152. data/lib/parse/stack/generators/templates/parse.rb +12 -0
  153. data/lib/parse/stack/generators/templates/webhooks.rb +10 -0
  154. data/lib/parse/stack/railtie.rb +18 -0
  155. data/lib/parse/stack/tasks.rb +563 -0
  156. data/lib/parse/stack/version.rb +11 -0
  157. data/lib/parse/stack.rb +455 -0
  158. data/lib/parse/two_factor_auth/user_extension.rb +449 -0
  159. data/lib/parse/two_factor_auth.rb +310 -0
  160. data/lib/parse/webhooks/payload.rb +360 -0
  161. data/lib/parse/webhooks/registration.rb +199 -0
  162. data/lib/parse/webhooks/replay_protection.rb +189 -0
  163. data/lib/parse/webhooks.rb +510 -0
  164. data/lib/parse-stack-next.rb +5 -0
  165. data/lib/parse-stack.rb +5 -0
  166. data/parse-stack-next.gemspec +82 -0
  167. data/parse-stack.png +0 -0
  168. data/scripts/debug-ips.js +35 -0
  169. data/scripts/docker/Dockerfile.parse +13 -0
  170. data/scripts/docker/atlas-init.js +284 -0
  171. data/scripts/docker/docker-compose.atlas.yml +76 -0
  172. data/scripts/docker/docker-compose.test.yml +106 -0
  173. data/scripts/docker/mongo-init.js +21 -0
  174. data/scripts/eval_mcp_with_lm_studio.rb +274 -0
  175. data/scripts/start-parse.sh +90 -0
  176. data/scripts/start_mcp_server.rb +78 -0
  177. data/scripts/test_server_connection.rb +82 -0
  178. metadata +377 -0
@@ -0,0 +1,2068 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+ require "active_model"
5
+ require "active_support"
6
+ require "active_support/inflector"
7
+ require "active_support/core_ext"
8
+ require "active_support/core_ext/object"
9
+ require "active_support/core_ext/string"
10
+ require "active_model/serializers/json"
11
+ require "time"
12
+ require "open-uri"
13
+
14
+ require_relative "../client"
15
+ require_relative "model"
16
+ require_relative "pointer"
17
+ require_relative "geopoint"
18
+ require_relative "polygon"
19
+ require_relative "geojson"
20
+ require_relative "file"
21
+ require_relative "bytes"
22
+ require_relative "date"
23
+ require_relative "time_zone"
24
+ require_relative "phone"
25
+ require_relative "email"
26
+ require_relative "acl"
27
+ require_relative "clp"
28
+ require_relative "push"
29
+ require_relative "core/actions"
30
+ require_relative "core/create_lock"
31
+ require_relative "core/fetching"
32
+ require_relative "core/querying"
33
+ require_relative "core/schema"
34
+ require_relative "core/describe"
35
+ require_relative "core/indexing"
36
+ require_relative "core/search_indexing"
37
+ require_relative "core/properties"
38
+ require_relative "core/errors"
39
+ require_relative "core/builder"
40
+ require_relative "core/enhanced_change_tracking"
41
+ require_relative "core/field_guards"
42
+ require_relative "core/parse_reference"
43
+ require_relative "validations"
44
+ require_relative "associations/has_one"
45
+ require_relative "associations/belongs_to"
46
+ require_relative "associations/has_many"
47
+
48
+ module Parse
49
+ # @return [Array] an array of registered Parse::Object subclasses.
50
+ def self.registered_classes
51
+ Parse::Object.descendants.map(&:parse_class).uniq
52
+ end
53
+
54
+ # @return [Array<Hash>] the list of all schemas for this application.
55
+ def self.schemas
56
+ client.schemas.results
57
+ end
58
+
59
+ # Fetch the schema for a specific collection name.
60
+ # @param className [String] the name collection
61
+ # @return [Hash] the schema document of this collection.
62
+ # @see Parse::Core::ClassBuilder.build!
63
+ def self.schema(className)
64
+ client.schema(className).result
65
+ end
66
+
67
+ # Perform a non-destructive upgrade of all your Parse schemas in the backend
68
+ # based on the property definitions of your local {Parse::Object} subclasses.
69
+ def self.auto_upgrade!
70
+ klassModels = Parse::Object.descendants
71
+ klassModels.sort_by(&:parse_class).each do |klass|
72
+ yield(klass) if block_given?
73
+ klass.auto_upgrade!
74
+ end
75
+ end
76
+
77
+ # Alias shorter names of core Parse class names.
78
+ # Ex, alias Parse::User to User, Parse::Installation to Installation, etc.
79
+ def self.use_shortnames!
80
+ require_relative "shortnames"
81
+ end
82
+
83
+ # This is the core class for all app specific Parse table subclasses. This class
84
+ # in herits from Parse::Pointer since an Object is a Parse::Pointer with additional fields,
85
+ # at a minimum, created_at, updated_at and ACLs. This class also handles all
86
+ # the relational types of associations in a Parse application and handles the main CRUD operations.
87
+ #
88
+ # As the parent class to all custom subclasses, this class provides the default property schema:
89
+ #
90
+ # class Parse::Object
91
+ # # All subclasses will inherit these properties by default.
92
+ #
93
+ # # the objectId column of a record.
94
+ # property :id, :string, field: :objectId
95
+ #
96
+ # # The the last updated date for a record (Parse::Date)
97
+ # property :updated_at, :date
98
+ #
99
+ # # The original creation date of a record (Parse::Date)
100
+ # property :created_at, :date
101
+ #
102
+ # # The Parse::ACL field
103
+ # property :acl, :acl, field: :ACL
104
+ #
105
+ # end
106
+ #
107
+ # Most Pointers and Object subclasses are treated the same. Therefore, defining a class Artist < Parse::Object
108
+ # that only has `id` set, will be treated as a pointer. Therefore a Parse::Object can be in a "pointer" state
109
+ # based on the data that it contains. Becasue of this, it is possible to take a Artist instance
110
+ # (in this example), that is in a pointer state, and fetch the rest of the data for that particular
111
+ # record without having to create a new object. Doing so would now mark it as not being a pointer anymore.
112
+ # This is important to the understanding on how relations and properties are handled.
113
+ #
114
+ # The implementation of this class is large and has been broken up into several modules.
115
+ #
116
+ # Properties:
117
+ #
118
+ # All columns in a Parse object are considered a type of property (ex. string, numbers, arrays, etc)
119
+ # except in two cases - Pointers and Relations. For a detailed discussion of properties, see
120
+ # The {https://github.com/modernistik/parse-stack#defining-properties Defining Properties} section.
121
+ #
122
+ # Associations:
123
+ #
124
+ # Parse supports a three main types of relational associations. One type of
125
+ # relation is the `One-to-One` association. This is implemented through a
126
+ # specific column in Parse with a Pointer data type. This pointer column,
127
+ # contains a local value that refers to a different record in a separate Parse
128
+ # table. This association is implemented using the `:belongs_to` feature. The
129
+ # second association is of `One-to-Many`. This is implemented is in Parse as a
130
+ # Array type column that contains a list of of Parse pointer objects. It is
131
+ # recommended by Parse that this array does not exceed 100 items for performance
132
+ # reasons. This feature is implemented using the `:has_many` operation with the
133
+ # plural name of the local Parse class. The last association type is a Parse
134
+ # Relation. These can be used to implement a large `Many-to-Many` association
135
+ # without requiring an explicit intermediary Parse table or class. This feature
136
+ # is also implemented using the `:has_many` method but passing the option of `:relation`.
137
+ #
138
+ # @see Associations::BelongsTo
139
+ # @see Associations::HasOne
140
+ # @see Associations::HasMany
141
+ class Object < Pointer
142
+ include Properties
143
+ include Core::EnhancedChangeTracking
144
+ include Core::FieldGuards
145
+ include Core::ParseReference
146
+ include Associations::HasOne
147
+ include Associations::BelongsTo
148
+ include Associations::HasMany
149
+ extend Core::Querying
150
+ extend Core::Schema
151
+ extend Core::Describe
152
+ extend Core::Indexing
153
+ extend Core::SearchIndexing
154
+ include Core::Fetching
155
+ include Core::Actions
156
+ # @!visibility private
157
+ BASE_OBJECT_CLASS = "Parse::Object".freeze
158
+
159
+ # @return [Model::TYPE_OBJECT]
160
+ def __type; Parse::Model::TYPE_OBJECT; end
161
+
162
+ # Default ActiveModel::Callbacks
163
+ # @!group Callbacks
164
+ #
165
+ # @!method before_validation
166
+ # A callback called before validations are run.
167
+ # @yield A block to execute for the callback.
168
+ # @see ActiveModel::Callbacks
169
+ # @!method after_validation
170
+ # A callback called after validations are run.
171
+ # @yield A block to execute for the callback.
172
+ # @see ActiveModel::Callbacks
173
+ # @!method around_validation
174
+ # A callback called around validations.
175
+ # @yield A block to execute for the callback.
176
+ # @see ActiveModel::Callbacks
177
+ # @!method before_create
178
+ # A callback called before the object has been created.
179
+ # @yield A block to execute for the callback.
180
+ # @see ActiveModel::Callbacks
181
+ # @!method after_create
182
+ # A callback called after the object has been created.
183
+ # @yield A block to execute for the callback.
184
+ # @see ActiveModel::Callbacks
185
+ # @!method around_create
186
+ # A callback called around object creation.
187
+ # @yield A block to execute for the callback.
188
+ # @see ActiveModel::Callbacks
189
+ # @!method before_update
190
+ # A callback called before the object is updated (not on create).
191
+ # @yield A block to execute for the callback.
192
+ # @see ActiveModel::Callbacks
193
+ # @!method after_update
194
+ # A callback called after the object has been updated.
195
+ # @yield A block to execute for the callback.
196
+ # @see ActiveModel::Callbacks
197
+ # @!method around_update
198
+ # A callback called around object update.
199
+ # @yield A block to execute for the callback.
200
+ # @see ActiveModel::Callbacks
201
+ # @!method before_save
202
+ # A callback called before the object is saved.
203
+ # @note This is not related to a Parse beforeSave webhook trigger.
204
+ # @yield A block to execute for the callback.
205
+ # @see ActiveModel::Callbacks
206
+ # @!method after_save
207
+ # A callback called after the object has been successfully saved.
208
+ # @note This is not related to a Parse afterSave webhook trigger.
209
+ # @yield A block to execute for the callback.
210
+ # @see ActiveModel::Callbacks
211
+ # @!method around_save
212
+ # A callback called around object save.
213
+ # @yield A block to execute for the callback.
214
+ # @see ActiveModel::Callbacks
215
+ # @!method before_destroy
216
+ # A callback called before the object is about to be deleted.
217
+ # @note This is not related to a Parse beforeDelete webhook trigger.
218
+ # @yield A block to execute for the callback.
219
+ # @see ActiveModel::Callbacks
220
+ # @!method after_destroy
221
+ # A callback called after the object has been successfully deleted.
222
+ # @note This is not related to a Parse afterDelete webhook trigger.
223
+ # @yield A block to execute for the callback.
224
+ # @see ActiveModel::Callbacks
225
+ # @!method around_destroy
226
+ # A callback called around object destruction.
227
+ # @yield A block to execute for the callback.
228
+ # @see ActiveModel::Callbacks
229
+ # @!endgroup
230
+
231
+ # Define all model callbacks with :before, :after, and :around support
232
+ # :validation - runs before/after/around validations
233
+ # :create - runs before/after/around creating a new object
234
+ # :update - runs before/after/around updating an existing object
235
+ # :save - runs before/after/around both create and update
236
+ # :destroy - runs before/after/around deleting an object
237
+ define_model_callbacks :validation, :create, :update, :save, :destroy, terminator: ->(target, result_lambda) { result_lambda.call == false }
238
+
239
+ # Add support for `on: :create` and `on: :update` options in validation callbacks
240
+ # This emulates ActiveRecord's callback behavior where you can specify:
241
+ # before_validation :method_name, on: :create
242
+ # before_validation :method_name, on: :update
243
+ #
244
+ # The `on:` option is transformed into an `if:` condition that checks new?
245
+ module ValidationCallbackOnSupport
246
+ %i[before_validation after_validation around_validation].each do |callback_method|
247
+ define_method(callback_method) do |*args, **options, &block|
248
+ # Extract the :on option and convert to :if condition
249
+ if options.key?(:on)
250
+ on_context = options.delete(:on)
251
+ case on_context
252
+ when :create
253
+ # Only run for new objects
254
+ existing_if = options[:if]
255
+ options[:if] = if existing_if
256
+ -> { new? && instance_exec(&existing_if) }
257
+ else
258
+ :new?
259
+ end
260
+ when :update
261
+ # Only run for existing objects
262
+ existing_if = options[:if]
263
+ options[:if] = if existing_if
264
+ -> { !new? && instance_exec(&existing_if) }
265
+ else
266
+ -> { !new? }
267
+ end
268
+ end
269
+ end
270
+
271
+ # Call the original callback method via super
272
+ if options.empty?
273
+ super(*args, &block)
274
+ else
275
+ super(*args, **options, &block)
276
+ end
277
+ end
278
+ end
279
+ end
280
+
281
+ singleton_class.prepend ValidationCallbackOnSupport
282
+
283
+ # Note: created_at, updated_at, and acl are defined via `property` declarations
284
+ # at the bottom of this file (lines ~870-878). Do not add attr_accessor here
285
+ # as it would be overwritten and cause "method redefined" warnings.
286
+
287
+ # All Parse Objects have a class-level and instance level `parse_class` method, in which the
288
+ # instance method is a convenience one for the class one. The default value for the parse_class is
289
+ # the name of the ruby class name. Therefore if you have an 'Artist' ruby class, then by default we will assume
290
+ # the remote Parse table is named 'Artist'. You may override this behavior by utilizing the `parse_class(<className>)` method
291
+ # to set it to something different.
292
+ class << self
293
+ attr_writer :parse_class
294
+
295
+ # @!attribute [rw] suppress_permissive_acl_warning
296
+ # When set on `Parse::Object` itself, suppresses the one-time per-class
297
+ # warning emitted when a class's effective {acl_policy_setting} is
298
+ # `:public` or `:owner_else_public`. Useful in test suites or apps that
299
+ # have deliberately reviewed and accepted permissive defaults.
300
+ # Defaults to `true` when `ENV["PARSE_SUPPRESS_PERMISSIVE_ACL_WARNING"]`
301
+ # is set to a truthy value (`1`, `true`, `yes`).
302
+ # @return [Boolean]
303
+ # @version 4.1.0
304
+ attr_writer :suppress_permissive_acl_warning
305
+
306
+ def suppress_permissive_acl_warning
307
+ return @suppress_permissive_acl_warning unless @suppress_permissive_acl_warning.nil?
308
+ env = ENV["PARSE_SUPPRESS_PERMISSIVE_ACL_WARNING"].to_s.downcase
309
+ %w[1 true yes].include?(env)
310
+ end
311
+
312
+ # @!attribute [rw] default_acl_private
313
+ # When set to true, new instances of this class will have a private ACL
314
+ # (no public access, master key only) instead of the default public read/write.
315
+ # @return [Boolean] whether new objects default to private ACLs.
316
+ # @version 3.1.3
317
+ # @example
318
+ # class PrivateDocument < Parse::Object
319
+ # self.default_acl_private = true
320
+ # end
321
+ #
322
+ # doc = PrivateDocument.new
323
+ # doc.acl.as_json # => {} (no permissions, master key only)
324
+ attr_accessor :default_acl_private
325
+
326
+ # Convenience method to set default ACL to private (no public access).
327
+ # Equivalent to `self.default_acl_private = true`.
328
+ # @version 3.1.3
329
+ # @example
330
+ # class PrivateDocument < Parse::Object
331
+ # private_acl!
332
+ # end
333
+ def private_acl!
334
+ self.default_acl_private = true
335
+ end
336
+
337
+ # The class method to override the implicitly assumed Parse collection name
338
+ # in your Parse database. The default Parse collection name is the singular form
339
+ # of the ruby Parse::Object subclass name. The Parse class value should match to
340
+ # the corresponding remote table in your database in order to properly store records and
341
+ # perform queries.
342
+ # @example
343
+ # class Song < Parse::Object; end;
344
+ # class Artist < Parse::Object
345
+ # parse_class "Musician" # remote collection name
346
+ # end
347
+ #
348
+ # Parse::User.parse_class # => '_User'
349
+ # Song.parse_class # => 'Song'
350
+ # Artist.parse_class # => 'Musician'
351
+ #
352
+ # @param remoteName [String] the name of the remote collection
353
+ # @return [String] the name of the Parse collection for this model.
354
+ def parse_class(remoteName = nil)
355
+ @parse_class ||= model_name.name
356
+ @parse_class = remoteName.to_s unless remoteName.nil?
357
+ @parse_class
358
+ end
359
+
360
+ # The set of default ACLs to be applied on newly created instances of this class.
361
+ # By default, public read and write are enabled unless {default_acl_private} is true.
362
+ # @see Parse::ACL.everyone
363
+ # @see Parse::ACL.private
364
+ # @return [Parse::ACL] the current default ACLs for this class.
365
+ def default_acls
366
+ @default_acls ||= case acl_policy_setting
367
+ when :public, :owner_else_public then Parse::ACL.everyone
368
+ when :private, :owner_else_private then Parse::ACL.private
369
+ else Parse::ACL.everyone
370
+ end
371
+ end
372
+
373
+ # A method to set default ACLs to be applied for newly created
374
+ # instances of this class. All subclasses have public read and write enabled
375
+ # by default.
376
+ # @example
377
+ # class AdminData < Parse::Object
378
+ #
379
+ # # Disable public read and write
380
+ # set_default_acl :public, read: false, write: false
381
+ #
382
+ # # but allow members of the Admin role to read and write
383
+ # set_default_acl 'Admin', role: true, read: true, write: true
384
+ #
385
+ # end
386
+ #
387
+ # data = AdminData.new
388
+ # data.acl # => ACL({"role:Admin"=>{"read"=>true, "write"=>true}})
389
+ #
390
+ # @param id [String|:public] The name for ACL entry. This can be an objectId, a role name or :public.
391
+ # @param read [Boolean] Whether to allow read permissions (default: false).
392
+ # @param write [Boolean] Whether to allow write permissions (default: false).
393
+ # @param role [Boolean] Whether the `id` argument should be applied as a role name.
394
+ # @see Parse::ACL#apply_role
395
+ # @see Parse::ACL#apply
396
+ # @version 1.7.0
397
+ def set_default_acl(id, read: false, write: false, role: false)
398
+ unless id.present?
399
+ raise ArgumentError, "Invalid argument applying #{self}.default_acls : must be either objectId, role or :public"
400
+ end
401
+ # Mixing the declarative `acl_policy` DSL with the legacy additive
402
+ # `set_default_acl` API on the same class produces ambiguous behavior
403
+ # (which one wins at save time? which fields get which permissions?).
404
+ # Pick one and stick with it.
405
+ if defined?(@acl_policy_setting) && @acl_policy_setting
406
+ raise ArgumentError,
407
+ "#{self}: cannot combine `set_default_acl` with `acl_policy`. " \
408
+ "This class already declares `acl_policy #{@acl_policy_setting.inspect}`. " \
409
+ "Use the declarative DSL for the entire ACL configuration, or remove " \
410
+ "`acl_policy` and use only `set_default_acl` (the legacy additive API)."
411
+ end
412
+ # Mark the class as using the legacy additive ACL API. The save-time
413
+ # policy resolver respects this and leaves the init-stamped default
414
+ # ACL alone, preserving pre-4.1 behavior for classes that customize
415
+ # via set_default_acl.
416
+ @acl_default_customized_by_set_default_acl = true
417
+ role ? default_acls.apply_role(id, read, write) : default_acls.apply(id, read, write)
418
+ end
419
+
420
+ # @!visibility private
421
+ # True when {set_default_acl} has been invoked on this class. Used by
422
+ # the save-time policy resolver to skip classes that have opted into
423
+ # the legacy additive default-ACL API.
424
+ def acl_default_customized_by_set_default_acl?
425
+ defined?(@acl_default_customized_by_set_default_acl) && @acl_default_customized_by_set_default_acl
426
+ end
427
+
428
+ # @!visibility private
429
+ def acl(acls, owner: nil)
430
+ raise "[#{self}.acl DEPRECATED] - Use `#{self}.default_acl` instead."
431
+ end
432
+
433
+ # Valid ACL policies that can be passed to {acl_policy}.
434
+ VALID_ACL_POLICIES = [:public, :private, :owner_else_public, :owner_else_private].freeze
435
+
436
+ # Declarative ACL policy applied to newly-created instances of this class.
437
+ # The policy is resolved at save time so that explicit ACL changes by the
438
+ # caller (`obj.acl = …`, `as:` kwarg, owner-field assignment after `.new`)
439
+ # always take precedence over the default.
440
+ #
441
+ # Resolution order at save (only when caller has not overridden):
442
+ # 1. Explicit `as: user` passed at construction → owner R/W only
443
+ # 2. Owner pointer resolved from the declared `owner:` field → owner R/W only
444
+ # 3. The else-half of the policy: `:public` → public R/W, `:private` → master-key only
445
+ #
446
+ # @param policy [Symbol] one of `:public`, `:private`, `:owner_else_public`, `:owner_else_private`.
447
+ # @param owner [Symbol,nil] the name of the property/belongs_to whose pointer designates the owner user.
448
+ # Only meaningful for `:owner_else_*` policies.
449
+ # @example
450
+ # class Post < Parse::Object
451
+ # acl_policy :owner_else_private, owner: :author
452
+ # end
453
+ #
454
+ # # server-side: no owner resolvable → master-key-only fallback
455
+ # Post.create!(title: "draft")
456
+ #
457
+ # # owner pointer set → ACL granting R/W to that user only
458
+ # Post.create!(title: "live", author: current_user)
459
+ #
460
+ # # explicit caller override (works regardless of `author` field)
461
+ # Post.create!({ title: "x" }, as: current_user)
462
+ # @raise [ArgumentError] if `policy` is not one of {VALID_ACL_POLICIES}.
463
+ # @see VALID_ACL_POLICIES
464
+ # @version 4.1.0
465
+ def acl_policy(policy, owner: nil)
466
+ unless VALID_ACL_POLICIES.include?(policy)
467
+ raise ArgumentError, "Invalid acl_policy #{policy.inspect}; must be one of #{VALID_ACL_POLICIES.inspect}"
468
+ end
469
+ # Symmetric to the guard in set_default_acl: pick one API per class.
470
+ if defined?(@acl_default_customized_by_set_default_acl) && @acl_default_customized_by_set_default_acl
471
+ raise ArgumentError,
472
+ "#{self}: cannot combine `acl_policy` with `set_default_acl`. " \
473
+ "This class already calls `set_default_acl`. Use the declarative " \
474
+ "DSL for the entire ACL configuration, or remove `acl_policy` and " \
475
+ "use only `set_default_acl`."
476
+ end
477
+ # `owner: :self` is a special marker meaning "the record itself is
478
+ # its own owner" — only meaningful for Parse::User and subclasses,
479
+ # where the record IS a user. The save-time resolver pre-generates
480
+ # `@id` via Parse::Core::ParseReference.generate_object_id when
481
+ # blank so the ACL can grant R/W to the record's own objectId in
482
+ # a single roundtrip. Non-User classes have no sensible
483
+ # interpretation (a Post's objectId is not a user id).
484
+ if owner == :self && !(self <= Parse::User)
485
+ raise ArgumentError,
486
+ "#{self}: `owner: :self` is only supported on Parse::User and " \
487
+ "its subclasses (the record IS the owner). For other classes, " \
488
+ "declare a belongs_to pointer to the owning user."
489
+ end
490
+ if owner && !policy.to_s.start_with?("owner_")
491
+ warn "[#{self}] `owner:` is ignored when acl_policy is #{policy.inspect}; only :owner_else_public and :owner_else_private use it."
492
+ end
493
+ if owner.nil? && policy.to_s.start_with?("owner_")
494
+ fallback = (policy == :owner_else_public) ? "public R/W" : "master-key-only"
495
+ warn "[#{self}] acl_policy #{policy.inspect} declared without `owner:` field; ACL resolution will always use the fallback (#{fallback}). Pass `as:` at construction to override."
496
+ end
497
+ @acl_policy_setting = policy
498
+ @acl_owner_field = owner
499
+ # Reset materialized default_acls so it picks up the new policy's fallback half.
500
+ @default_acls = nil
501
+ # Re-arm the permissive-default warning so a subsequent change is re-evaluated.
502
+ @_permissive_default_warned = nil
503
+ policy
504
+ end
505
+
506
+ # The effective ACL policy for this class. Inherits from the superclass
507
+ # when not explicitly declared. The gem-wide default is
508
+ # `:owner_else_private` — records grant read/write to the resolved
509
+ # owner (from `as:` or the class's `owner:` field) when one is
510
+ # supplied, and fall back to master-key-only when no owner is
511
+ # resolvable. `default_acl_private = true` is honored as `:private`.
512
+ # Classes that need public access for new records should declare
513
+ # `acl_policy :public` or `:owner_else_public` explicitly, or use
514
+ # the legacy `set_default_acl` additive API.
515
+ # @return [Symbol] one of {VALID_ACL_POLICIES}.
516
+ # @version 4.1.0
517
+ def acl_policy_setting
518
+ return @acl_policy_setting if defined?(@acl_policy_setting) && @acl_policy_setting
519
+ return :private if default_acl_private
520
+ if self == Parse::Object
521
+ :owner_else_private
522
+ elsif superclass.respond_to?(:acl_policy_setting)
523
+ superclass.acl_policy_setting
524
+ else
525
+ :owner_else_private
526
+ end
527
+ end
528
+
529
+ # The name of the property/belongs_to designating the owner user for
530
+ # `:owner_else_*` ACL policies. Inherited from the superclass when not
531
+ # explicitly declared via {acl_policy}.
532
+ # @return [Symbol,nil]
533
+ # @version 4.1.0
534
+ def acl_owner_field
535
+ return @acl_owner_field if defined?(@acl_owner_field) && @acl_owner_field
536
+ if self != Parse::Object && superclass.respond_to?(:acl_owner_field)
537
+ superclass.acl_owner_field
538
+ else
539
+ nil
540
+ end
541
+ end
542
+
543
+ # SDK-provided Parse model class names that the policy resolver and
544
+ # init-time default-ACL stamp both skip. Parse Server applies its own
545
+ # per-class defaults for these classes when the save body omits the
546
+ # `ACL` field — most importantly, `_User` gets `{"<user-id>": R/W,
547
+ # "*": R}` so the newly created user can edit their own profile.
548
+ # Stamping any ACL from the SDK side (even `{}`) overrides those
549
+ # server-side defaults and is almost always wrong.
550
+ BUILTIN_PARSE_CLASS_NAMES = %w[
551
+ Parse::User Parse::Installation Parse::Session Parse::Role
552
+ Parse::Product Parse::PushStatus Parse::Audience
553
+ Parse::JobStatus Parse::JobSchedule
554
+ ].freeze
555
+
556
+ # @!visibility private
557
+ # True when this class is one of the SDK's built-in Parse model
558
+ # classes ({BUILTIN_PARSE_CLASS_NAMES}). Mostly used internally to
559
+ # decide whether the SDK should bypass its default-ACL stamping.
560
+ def builtin_parse_class?
561
+ BUILTIN_PARSE_CLASS_NAMES.include?(name)
562
+ end
563
+
564
+ # @!visibility private
565
+ # True when this class is a built-in AND the application has not
566
+ # customized its ACL configuration via either `acl_policy` or
567
+ # `set_default_acl`. Under these conditions the SDK leaves `obj.acl`
568
+ # nil so the save body omits the `ACL` field and Parse Server applies
569
+ # its own per-class defaults (most importantly, `_User` → self R/W +
570
+ # public read). If the application has called `acl_policy` or
571
+ # `set_default_acl` on the built-in, the SDK respects that
572
+ # customization and runs the normal stamp / resolver path.
573
+ def builtin_acl_default_active?
574
+ return false unless builtin_parse_class?
575
+ return false if defined?(@acl_policy_setting) && @acl_policy_setting
576
+ return false if defined?(@acl_default_customized_by_set_default_acl) &&
577
+ @acl_default_customized_by_set_default_acl
578
+ true
579
+ end
580
+
581
+ # @!visibility private
582
+ # Emits a one-time warning per class when the effective default ACL policy
583
+ # is permissive (`:public` or `:owner_else_public`). Suppressed for the
584
+ # Parse::Object base class and the SDK's built-in Parse model classes.
585
+ # Set `Parse::Object.suppress_permissive_acl_warning = true` globally (or
586
+ # via the `PARSE_SUPPRESS_PERMISSIVE_ACL_WARNING` env var) to disable.
587
+ def _warn_permissive_acl_default_once
588
+ return if defined?(@_permissive_default_warned) && @_permissive_default_warned
589
+ @_permissive_default_warned = true
590
+ return if self == Parse::Object
591
+ return if BUILTIN_PARSE_CLASS_NAMES.include?(name)
592
+ return if Parse::Object.suppress_permissive_acl_warning
593
+ policy = acl_policy_setting
594
+ return unless policy == :public || policy == :owner_else_public
595
+ warn "[Parse::Stack security] #{self} uses permissive default ACL policy " \
596
+ "`#{policy}`. New records can be modified by anyone unless an owner " \
597
+ "is resolved at save. Call `acl_policy :owner_else_private` or " \
598
+ "`:private` in the class to silence this warning."
599
+ end
600
+
601
+ # @!group Class-Level Permissions (CLP)
602
+
603
+ # The Class-Level Permissions for this model.
604
+ # CLPs control access to the class at the schema level.
605
+ # @return [Parse::CLP] the CLP instance for this class
606
+ # @see Parse::CLP
607
+ def class_permissions
608
+ @class_permissions ||= Parse::CLP.new
609
+ end
610
+
611
+ alias_method :clp, :class_permissions
612
+
613
+ # Set default permissions for all CLP operations at once.
614
+ # This is useful for establishing a baseline before customizing specific operations.
615
+ #
616
+ # @param public [Boolean] whether public access is allowed for all operations
617
+ # @param roles [Array<String>] role names that have access to all operations
618
+ # @param requires_authentication [Boolean] whether authentication is required for all operations
619
+ #
620
+ # @example Public read, authenticated write
621
+ # class Document < Parse::Object
622
+ # # Start with public read access for all operations
623
+ # set_default_clp public: true
624
+ #
625
+ # # Then restrict write operations
626
+ # set_clp :create, requires_authentication: true
627
+ # set_clp :update, requires_authentication: true
628
+ # set_clp :delete, public: false, roles: ["Admin"]
629
+ # end
630
+ #
631
+ # @example Role-based access for everything
632
+ # class AdminReport < Parse::Object
633
+ # # Only admins can do anything
634
+ # set_default_clp public: false, roles: ["Admin"]
635
+ # end
636
+ #
637
+ # @example Authenticated users only
638
+ # class PrivateData < Parse::Object
639
+ # # Require authentication for all operations
640
+ # set_default_clp requires_authentication: true
641
+ # end
642
+ def set_default_clp(public: nil, roles: [], requires_authentication: false)
643
+ # Set the default permission on the CLP instance
644
+ # This will be used by as_json to fill in missing operations
645
+ class_permissions.set_default_permission(
646
+ public_access: public,
647
+ roles: Array(roles),
648
+ requires_authentication: requires_authentication
649
+ )
650
+
651
+ # Also explicitly set all operations to ensure they're included
652
+ Parse::CLP::OPERATIONS.each do |operation|
653
+ set_clp(operation, public: public, roles: roles, requires_authentication: requires_authentication)
654
+ end
655
+ end
656
+
657
+ # Set pointer-permission fields for read access.
658
+ # Users pointed to by these fields can read objects of this class.
659
+ # This is an alternative to ACLs for owner-based access control.
660
+ #
661
+ # @param fields [Array<Symbol, String>] pointer field names (snake_case supported)
662
+ # @example
663
+ # class Document < Parse::Object
664
+ # belongs_to :owner, as: :user
665
+ # belongs_to :editor, as: :user
666
+ #
667
+ # # Only owner and editor can read
668
+ # set_read_user_fields :owner, :editor
669
+ # end
670
+ def set_read_user_fields(*fields)
671
+ converted = fields.flatten.map do |f|
672
+ field_sym = f.to_sym
673
+ field_map[field_sym] || f.to_s.camelize(:lower)
674
+ end
675
+ class_permissions.set_read_user_fields(*converted)
676
+ end
677
+
678
+ # Set pointer-permission fields for write access.
679
+ # Users pointed to by these fields can write to objects of this class.
680
+ #
681
+ # @param fields [Array<Symbol, String>] pointer field names (snake_case supported)
682
+ # @example
683
+ # class Document < Parse::Object
684
+ # belongs_to :owner, as: :user
685
+ #
686
+ # # Only owner can write
687
+ # set_write_user_fields :owner
688
+ # end
689
+ def set_write_user_fields(*fields)
690
+ converted = fields.flatten.map do |f|
691
+ field_sym = f.to_sym
692
+ field_map[field_sym] || f.to_s.camelize(:lower)
693
+ end
694
+ class_permissions.set_write_user_fields(*converted)
695
+ end
696
+
697
+ # Set a class-level permission for a specific operation.
698
+ # This is the main DSL method for configuring CLPs in your model.
699
+ #
700
+ # @param operation [Symbol] the operation (:find, :get, :count, :create, :update, :delete, :addField)
701
+ # @param public [Boolean, nil] whether public access is allowed
702
+ # @param roles [Array<String>, String] role names that have access
703
+ # @param users [Array<String>, String] user objectIds that have access
704
+ # @param pointer_fields [Array<String>, String] pointer field names for userField access
705
+ # @param requires_authentication [Boolean] whether authentication is required
706
+ #
707
+ # @example Basic usage
708
+ # class Song < Parse::Object
709
+ # # Allow public read
710
+ # set_clp :find, public: true
711
+ # set_clp :get, public: true
712
+ #
713
+ # # Restrict write operations to specific roles
714
+ # set_clp :create, public: false, roles: ["Admin", "Editor"]
715
+ # set_clp :update, public: false, roles: ["Admin", "Editor"]
716
+ # set_clp :delete, public: false, roles: ["Admin"]
717
+ # end
718
+ #
719
+ # @example Requiring authentication
720
+ # class PrivateData < Parse::Object
721
+ # set_clp :find, requires_authentication: true
722
+ # set_clp :get, requires_authentication: true
723
+ # end
724
+ #
725
+ # @see Parse::CLP#set_permission
726
+ def set_clp(operation, public: nil, roles: [], users: [], pointer_fields: [], requires_authentication: false)
727
+ # Convert snake_case pointer field names to camelCase
728
+ converted_pointer_fields = Array(pointer_fields).map do |field|
729
+ field_sym = field.to_sym
730
+ field_map[field_sym] || field.to_s.camelize(:lower)
731
+ end
732
+
733
+ class_permissions.set_permission(
734
+ operation,
735
+ public_access: public,
736
+ roles: Array(roles),
737
+ users: Array(users),
738
+ pointer_fields: converted_pointer_fields,
739
+ requires_authentication: requires_authentication
740
+ )
741
+ end
742
+
743
+ alias_method :set_class_permission, :set_clp
744
+
745
+ # Lock every CLP operation to master-key access only. Use as a starting
746
+ # point when a class should be entirely hidden from clients; you can
747
+ # then selectively open specific operations with {set_clp} or
748
+ # {set_class_access} afterward.
749
+ #
750
+ # @example Hide a class entirely from clients
751
+ # class AuditLog < Parse::Object
752
+ # master_only_class!
753
+ # end
754
+ #
755
+ # @example Hide everything, then open create+get for clients
756
+ # class Invitation < Parse::Object
757
+ # master_only_class!
758
+ # set_clp :create, public: true
759
+ # set_clp :get, public: true
760
+ # end
761
+ #
762
+ # @return [void]
763
+ def master_only_class!
764
+ Parse::CLP::OPERATIONS.each { |op| set_clp(op) }
765
+ nil
766
+ end
767
+
768
+ # Restrict `find` and `count` to master-key only, leaving the other
769
+ # operations (`get`, `create`, `update`, `delete`, `addField`) at their
770
+ # current settings. This is the canonical "Installation-style" pattern:
771
+ # clients can interact with individual records but cannot enumerate or
772
+ # count them.
773
+ #
774
+ # @example Mirror _Installation semantics
775
+ # class Invitation < Parse::Object
776
+ # unlistable_class!
777
+ # # clients can still get/create/update/delete by objectId
778
+ # end
779
+ #
780
+ # @return [void]
781
+ def unlistable_class!
782
+ set_clp(:find)
783
+ set_clp(:count)
784
+ nil
785
+ end
786
+
787
+ # Set CLP for multiple operations in one call, choosing a coarse access
788
+ # mode per operation. Each value can be:
789
+ #
790
+ # * `:master` / `:master_only` / `nil` / `false` -- master key only
791
+ # (Parse Server's empty `{}` permission for that op)
792
+ # * `:public` / `true` -- wildcard `*` access
793
+ # * `:authenticated` -- requiresAuthentication
794
+ # * a String or Symbol -- a single role name
795
+ # (the `role:` prefix is added automatically)
796
+ # * an Array of Strings/Symbols -- multiple role names
797
+ #
798
+ # Operations not listed in the hash are left at their current setting.
799
+ # For finer control (mixed roles, users, pointer-fields,
800
+ # requires_authentication) use {set_clp} directly.
801
+ #
802
+ # @example The _Installation pattern -- get-by-id and create, but no listing
803
+ # class Invitation < Parse::Object
804
+ # set_class_access(
805
+ # find: :master, # nobody can list
806
+ # count: :master, # nobody can count
807
+ # get: :public, # anyone with the id can fetch
808
+ # create: :authenticated, # logged-in users may create
809
+ # update: :master, # only server may update
810
+ # delete: :master, # only server may delete
811
+ # )
812
+ # end
813
+ #
814
+ # @example Admin-only writes, public reads
815
+ # class Article < Parse::Object
816
+ # set_class_access(
817
+ # find: :public, get: :public,
818
+ # create: "Admin", update: "Admin", delete: "Admin",
819
+ # )
820
+ # end
821
+ #
822
+ # @param ops_to_access [Hash{Symbol => Symbol,String,Array,Boolean,nil}]
823
+ # @return [void]
824
+ def set_class_access(**ops_to_access)
825
+ ops_to_access.each do |op, access|
826
+ op = op.to_sym
827
+ unless Parse::CLP::OPERATIONS.include?(op)
828
+ raise ArgumentError,
829
+ "Unknown CLP operation #{op.inspect}. Allowed: #{Parse::CLP::OPERATIONS.inspect}"
830
+ end
831
+ case access
832
+ when :master, :master_only, nil, false
833
+ set_clp(op)
834
+ when :public, true
835
+ set_clp(op, public: true)
836
+ when :authenticated
837
+ set_clp(op, requires_authentication: true)
838
+ when Array
839
+ set_clp(op, roles: access.map(&:to_s))
840
+ when String, Symbol
841
+ set_clp(op, roles: [access.to_s])
842
+ else
843
+ raise ArgumentError,
844
+ "Unknown class_access value for :#{op}: #{access.inspect}. " \
845
+ "Use :master, :public, :authenticated, a role name, or an array of roles."
846
+ end
847
+ end
848
+ nil
849
+ end
850
+
851
+ # Define protected fields that should be hidden from certain users/roles.
852
+ # This is used to implement field-level security.
853
+ #
854
+ # Field names are automatically converted from snake_case (Ruby convention)
855
+ # to camelCase (Parse Server convention). You can use either format.
856
+ #
857
+ # @param pattern [String, Symbol] the pattern to apply protection for:
858
+ # - "*" or :public - applies to all users (public)
859
+ # - "role:RoleName" - applies to users in a specific role
860
+ # - "userField:fieldName" - applies to users referenced in a pointer field
861
+ # - user objectId - applies to a specific user
862
+ # @param fields [Array<String, Symbol>] field names to hide from this pattern.
863
+ # Use Ruby property names (snake_case) - they will be auto-converted.
864
+ # An empty array means the user can see all fields.
865
+ #
866
+ # @example Hide fields from public but allow admins to see everything
867
+ # class User < Parse::Object
868
+ # property :email, :string
869
+ # property :phone, :string
870
+ # property :internal_notes, :string
871
+ #
872
+ # # Hide sensitive fields from public (use snake_case Ruby names)
873
+ # protect_fields "*", [:email, :phone, :internal_notes]
874
+ #
875
+ # # Admins can see everything (empty array = no restrictions)
876
+ # protect_fields "role:Admin", []
877
+ #
878
+ # # Users can see their own data
879
+ # protect_fields "userField:objectId", []
880
+ # end
881
+ #
882
+ # @example Hide metadata from non-owners
883
+ # class Image < Parse::Object
884
+ # property :url, :string
885
+ # property :metadata, :object # GPS, camera info, etc.
886
+ # belongs_to :owner, as: :user
887
+ #
888
+ # # Hide metadata from everyone (auto-converts to "metadata" in Parse)
889
+ # protect_fields "*", [:metadata]
890
+ #
891
+ # # But owners can see their own image metadata
892
+ # protect_fields "userField:owner", []
893
+ # end
894
+ #
895
+ # @example Master key only fields
896
+ # class SensitiveDoc < Parse::Object
897
+ # property :admin_notes, :string
898
+ # property :internal_score, :integer
899
+ #
900
+ # # Only master key can see these fields
901
+ # # (converts to ["adminNotes", "internalScore"] for Parse Server)
902
+ # protect_fields "*", [:admin_notes, :internal_score]
903
+ # end
904
+ #
905
+ # @see Parse::CLP#set_protected_fields
906
+ def protect_fields(pattern, fields)
907
+ pattern = "*" if pattern.to_sym == :public rescue pattern
908
+
909
+ # Convert userField:field_name pattern to use camelCase field name
910
+ if pattern.to_s.start_with?("userField:")
911
+ field_name = pattern.to_s.sub("userField:", "")
912
+ field_sym = field_name.to_sym
913
+ converted_field = field_map[field_sym] || field_name.camelize(:lower)
914
+ pattern = "userField:#{converted_field}"
915
+ end
916
+
917
+ # Convert snake_case Ruby property names to camelCase Parse field names
918
+ converted_fields = Array(fields).map do |field|
919
+ field_sym = field.to_sym
920
+ # Use field_map if available, otherwise convert to camelCase
921
+ field_map[field_sym] || field.to_s.camelize(:lower)
922
+ end
923
+ class_permissions.set_protected_fields(pattern, converted_fields)
924
+ end
925
+
926
+ alias_method :set_protected_fields, :protect_fields
927
+
928
+ # Introspect the locally-configured access surface for this class.
929
+ # Combines the CLP operations, protectedFields read-side hiding, and
930
+ # the write-side protections installed via the field_guards DSL into
931
+ # a single hash, so it's easy to audit who can do what to which
932
+ # fields without reading three separate parts of the class body.
933
+ #
934
+ # The hash is built from the Parse-Stack model declarations only. It
935
+ # does NOT round-trip the Parse Server schema; if you've configured
936
+ # CLPs on the server side that haven't been mirrored locally, those
937
+ # won't appear here. Conversely, calling `update_clp!` pushes what
938
+ # this method reflects.
939
+ #
940
+ # @example
941
+ # class Post < Parse::Object
942
+ # property :title, :string
943
+ # property :owner, :string
944
+ # guard :owner, :master_only
945
+ # parse_reference
946
+ # set_class_access(find: :public, create: :authenticated, update: "Admin")
947
+ # end
948
+ #
949
+ # Post.describe_access
950
+ # # =>
951
+ # # {
952
+ # # operations: {
953
+ # # find: { "*" => true },
954
+ # # create: { "requiresAuthentication" => true },
955
+ # # update: { "role:Admin" => true },
956
+ # # ...
957
+ # # },
958
+ # # read_user_fields: [],
959
+ # # write_user_fields: [],
960
+ # # fields: {
961
+ # # title: { write: :open, read: :open },
962
+ # # owner: { write: :master_only, read: :open },
963
+ # # parse_reference: { write: :set_once, read: { hidden_from: ["*"] } },
964
+ # # },
965
+ # # }
966
+ #
967
+ # @return [Hash]
968
+ def describe_access
969
+ perms = class_permissions
970
+ protected_by_pattern = perms.respond_to?(:protected_fields) ? perms.protected_fields : {}
971
+ guards_map = respond_to?(:field_guards) && field_guards ? field_guards : {}
972
+
973
+ # Per-field access summary. Iterate `field_map` (local -> remote)
974
+ # rather than `fields`, because `fields` redundantly stores BOTH
975
+ # the local key (e.g. :full_name) and the remote key (:fullName)
976
+ # for every property. That redundancy would cause multi-word
977
+ # properties to appear twice in the output.
978
+ per_field = {}
979
+ field_map.each do |local_sym, remote_sym|
980
+ local_sym = local_sym.to_sym
981
+ next if Parse::Properties::CORE_FIELDS.key?(local_sym)
982
+ data_type = fields[local_sym]
983
+ remote = remote_sym.to_s
984
+
985
+ # Read protection -- collect every protectedFields pattern that
986
+ # lists this field (under either its local or remote name).
987
+ hidden_from = protected_by_pattern.each_with_object([]) do |(pattern, hidden_fields), acc|
988
+ acc << pattern if hidden_fields.include?(remote) || hidden_fields.include?(local_sym.to_s)
989
+ end
990
+
991
+ per_field[local_sym] = {
992
+ write: guards_map[local_sym] || :open,
993
+ read: hidden_from.empty? ? :open : { hidden_from: hidden_from },
994
+ type: data_type,
995
+ }
996
+ end
997
+
998
+ # Deep-copy the operations hash so callers mutating the result
999
+ # don't accidentally mutate the live class_permissions state.
1000
+ operations = if perms.respond_to?(:permissions)
1001
+ perms.permissions.transform_values { |v| v.is_a?(Hash) ? v.dup : v }
1002
+ else
1003
+ {}
1004
+ end
1005
+
1006
+ {
1007
+ operations: operations,
1008
+ read_user_fields: perms.respond_to?(:read_user_fields) ? perms.read_user_fields : [],
1009
+ write_user_fields: perms.respond_to?(:write_user_fields) ? perms.write_user_fields : [],
1010
+ fields: per_field,
1011
+ }
1012
+ end
1013
+
1014
+ # Fetch the current CLP from the Parse Server for this class.
1015
+ # @param client [Parse::Client] optional client to use
1016
+ # @return [Parse::CLP] the CLP from the server
1017
+ def fetch_clp(client: nil)
1018
+ client ||= self.client
1019
+ response = client.schema(parse_class)
1020
+ return Parse::CLP.new unless response.success?
1021
+
1022
+ clp_data = response.result["classLevelPermissions"] || {}
1023
+ Parse::CLP.new(clp_data)
1024
+ end
1025
+
1026
+ alias_method :fetch_class_permissions, :fetch_clp
1027
+
1028
+ # Update the CLP on the Parse Server for this class.
1029
+ # Merges local CLP with any existing server CLP.
1030
+ #
1031
+ # @param client [Parse::Client] optional client to use
1032
+ # @param replace [Boolean] if true, replaces server CLP entirely; otherwise merges
1033
+ # @return [Parse::Response] the response from the server
1034
+ #
1035
+ # @example Push local CLP to server
1036
+ # Song.update_clp!
1037
+ #
1038
+ # @example Replace server CLP entirely
1039
+ # Song.update_clp!(replace: true)
1040
+ def update_clp!(client: nil, replace: false)
1041
+ client ||= self.client
1042
+
1043
+ unless client.master_key.present?
1044
+ warn "[Parse] CLP changes for #{parse_class} require the master key!"
1045
+ return nil
1046
+ end
1047
+
1048
+ clp_data = class_permissions.as_json
1049
+ return nil if clp_data.empty?
1050
+
1051
+ schema_update = { "classLevelPermissions" => clp_data }
1052
+ client.update_schema(parse_class, schema_update)
1053
+ end
1054
+
1055
+ alias_method :update_class_permissions!, :update_clp!
1056
+
1057
+ # @!endgroup
1058
+
1059
+ end # << self
1060
+
1061
+ # @return [String] the Parse class for this object.
1062
+ # @see Parse::Object.parse_class
1063
+ def parse_class
1064
+ self.class.parse_class
1065
+ end
1066
+
1067
+ alias_method :className, :parse_class
1068
+
1069
+ # @return [Hash] the schema structure for this Parse collection from the server.
1070
+ # @see Parse::Core::Schema
1071
+ def schema
1072
+ self.class.schema
1073
+ end
1074
+
1075
+ # @!group Field Filtering (CLP)
1076
+
1077
+ # Filter this object's fields based on Class-Level Permissions for a user.
1078
+ # Uses the CLP configured on the model class to determine which fields
1079
+ # should be visible to the given user/roles context.
1080
+ #
1081
+ # This is useful for filtering webhook responses or API data before
1082
+ # sending to clients.
1083
+ #
1084
+ # @param user [Parse::User, String, nil] the user or user ID
1085
+ # @param roles [Array<String>] role names the user belongs to
1086
+ # @param authenticated [Boolean] whether the user is authenticated
1087
+ # @param clp [Parse::CLP, nil] optional CLP to use (defaults to class CLP)
1088
+ # @return [Hash] filtered data hash with protected fields removed
1089
+ #
1090
+ # @example Filter object for a specific user
1091
+ # song = Song.first
1092
+ # filtered = song.filter_for_user(current_user, roles: ["Member"])
1093
+ #
1094
+ # @example Filter for unauthenticated access
1095
+ # filtered = song.filter_for_user(nil)
1096
+ #
1097
+ # @see Parse::CLP#filter_fields
1098
+ def filter_for_user(user, roles: [], authenticated: nil, clp: nil)
1099
+ clp ||= self.class.class_permissions
1100
+ return as_json unless clp.present?
1101
+
1102
+ clp.filter_fields(as_json, user: user, roles: roles, authenticated: authenticated)
1103
+ end
1104
+
1105
+ # Filter an array of Parse objects or hashes for a user.
1106
+ # Class method that applies CLP filtering to multiple results.
1107
+ #
1108
+ # @param objects [Array<Parse::Object, Hash>] array of objects or hashes to filter
1109
+ # @param user [Parse::User, String, nil] the user or user ID
1110
+ # @param roles [Array<String>] role names the user belongs to
1111
+ # @param authenticated [Boolean] whether the user is authenticated
1112
+ # @param clp [Parse::CLP, nil] optional CLP to use (defaults to class CLP)
1113
+ # @return [Array<Hash>] filtered data hashes with protected fields removed
1114
+ #
1115
+ # @example Filter query results for a user
1116
+ # songs = Song.query(artist: "Beatles").results
1117
+ # filtered = Song.filter_results_for_user(songs, current_user, roles: user_roles)
1118
+ #
1119
+ # @see Parse::CLP#filter_fields
1120
+ def self.filter_results_for_user(objects, user, roles: [], authenticated: nil, clp: nil)
1121
+ clp ||= class_permissions
1122
+ return objects.map { |o| o.is_a?(Parse::Object) ? o.as_json : o } unless clp.present?
1123
+
1124
+ objects.map do |obj|
1125
+ data = obj.is_a?(Parse::Object) ? obj.as_json : obj
1126
+ clp.filter_fields(data, user: user, roles: roles, authenticated: authenticated)
1127
+ end
1128
+ end
1129
+
1130
+ # Fetch a user's roles for use with field filtering.
1131
+ # Convenience method to get role names that can be passed to filter methods.
1132
+ #
1133
+ # @param user [Parse::User] the user to get roles for
1134
+ # @return [Array<String>] role names (without "role:" prefix)
1135
+ #
1136
+ # @example Get roles and filter
1137
+ # roles = Song.roles_for_user(current_user)
1138
+ # filtered = song.filter_for_user(current_user, roles: roles)
1139
+ def self.roles_for_user(user)
1140
+ return [] unless user.is_a?(Parse::User) || user.is_a?(Parse::Pointer)
1141
+ return [] unless defined?(Parse::Role)
1142
+
1143
+ user_id = user.respond_to?(:id) ? user.id : user.to_s
1144
+ return [] if user_id.blank?
1145
+
1146
+ Parse::Role.all(users: user).map(&:name)
1147
+ rescue => e
1148
+ warn "[Parse] Error fetching roles for user: #{e.message}"
1149
+ []
1150
+ end
1151
+
1152
+ # @!endgroup
1153
+
1154
+ # Core identification fields that are always included in serialization
1155
+ # unless strict: true is specified
1156
+ IDENTIFICATION_FIELDS = %w[id objectId __type className].freeze
1157
+
1158
+ # @return [Hash] a json-hash representing this object.
1159
+ # @param opts [Hash] options for serialization
1160
+ # @option opts [Boolean] :only_fetched when true (or when Parse.serialize_only_fetched_fields
1161
+ # is true and this option is not explicitly set to false), only serialize fields that
1162
+ # were fetched for partially fetched objects. This prevents autofetch during serialization.
1163
+ # @option opts [Array<Symbol,String>] :only limit serialization to these fields. By default,
1164
+ # identification fields (objectId, className, __type, id) are always included for proper
1165
+ # object identification. Use strict: true to disable this behavior.
1166
+ # @option opts [Array<Symbol,String>] :except exclude these fields from serialization
1167
+ # @option opts [Array<Symbol,String>] :exclude_keys alias for :except
1168
+ # @option opts [Array<Symbol,String>] :exclude alias for :except
1169
+ # @option opts [Boolean] :strict when true with :only, performs strict filtering without
1170
+ # automatically including identification fields. Default is false.
1171
+ def as_json(opts = nil)
1172
+ opts ||= {}
1173
+
1174
+ # Normalize :exclude_keys and :exclude to :except (alias support)
1175
+ if !opts[:except]
1176
+ if opts[:exclude_keys]
1177
+ opts = opts.merge(except: opts[:exclude_keys])
1178
+ elsif opts[:exclude]
1179
+ opts = opts.merge(except: opts[:exclude])
1180
+ end
1181
+ end
1182
+
1183
+ # When :only is specified without :strict, automatically include identification fields
1184
+ # so the serialized object can be properly identified
1185
+ if opts[:only] && !opts[:strict]
1186
+ only_keys = Array(opts[:only]).map(&:to_s)
1187
+ only_keys |= IDENTIFICATION_FIELDS
1188
+ opts = opts.merge(only: only_keys)
1189
+ end
1190
+
1191
+ # For selectively fetched objects (partial fetch), serialize only the fetched fields.
1192
+ # This takes priority over pointer detection because a partial fetch has actual data
1193
+ # even if it lacks timestamps (which would otherwise make it look like a pointer).
1194
+ # This behavior is controlled by:
1195
+ # 1. Per-call: opts[:only_fetched] (explicit true/false)
1196
+ # 2. Global: Parse.serialize_only_fetched_fields (default true)
1197
+ if has_selective_keys?
1198
+ # Determine if we should serialize only fetched fields
1199
+ only_fetched = opts.fetch(:only_fetched) { Parse.serialize_only_fetched_fields }
1200
+
1201
+ if only_fetched && !opts.key?(:only)
1202
+ # Build the :only list from fetched keys
1203
+ # Use the local field names which match the attribute methods
1204
+ only_keys = fetched_keys.map(&:to_s)
1205
+ # Always include Parse metadata fields for proper object identification
1206
+ only_keys |= IDENTIFICATION_FIELDS
1207
+ only_keys |= %w[created_at updated_at]
1208
+ opts = opts.merge(only: only_keys)
1209
+ end
1210
+
1211
+ changed_fields = changed_attributes
1212
+ return super(opts).delete_if { |k, v| v.nil? && !changed_fields.has_key?(k) }
1213
+ end
1214
+
1215
+ # When in pointer state (no data fetched, just an objectId), return the serialized
1216
+ # pointer hash (with __type, className, objectId) for proper JSON serialization
1217
+ return pointer.as_json(opts) if pointer?
1218
+
1219
+ changed_fields = changed_attributes
1220
+ super(opts).delete_if { |k, v| v.nil? && !changed_fields.has_key?(k) }
1221
+ end
1222
+
1223
+ private
1224
+
1225
+ # Override to return string keys for compatibility with ActiveModel's serialization.
1226
+ # ActiveModel::Serialization#serializable_hash uses string comparison for :only/:except
1227
+ # options, but our attributes method returns symbol keys.
1228
+ # @return [Array<String>] attribute names as strings
1229
+ # @!visibility private
1230
+ def attribute_names_for_serialization
1231
+ attributes.keys.map(&:to_s)
1232
+ end
1233
+
1234
+ public
1235
+
1236
+ # The main constructor for subclasses. It can take different parameter types
1237
+ # including a String and a JSON hash. Assume a `Post` class that inherits
1238
+ # from Parse::Object:
1239
+ # @note Should only be called with Parse::Object subclasses.
1240
+ # @overload new(id)
1241
+ # Create a new object with an objectId. This method is useful for creating
1242
+ # an unfetched object (pointer-state).
1243
+ # @example
1244
+ # Post.new "1234"
1245
+ # @param id [String] The object id.
1246
+ # @overload new(hash = {})
1247
+ # Create a new object with Parse JSON hash.
1248
+ # @example
1249
+ # # JSON hash from Parse
1250
+ # Post.new({"className" => "Post", "objectId" => "1234", "title" => "My Title"})
1251
+ #
1252
+ # post = Post.new title: "My Title"
1253
+ # post.title # => "My Title"
1254
+ #
1255
+ # @param hash [Hash] the hash representing the object.
1256
+ # Untrusted by default: keys in
1257
+ # {Parse::Properties::PROTECTED_INITIALIZE_KEYS} (+sessionToken+,
1258
+ # +_rperm+, +_wperm+, +_hashed_password+, +authData+, +roles+)
1259
+ # are filtered out even when an +objectId+ is present. This
1260
+ # closes the mass-assignment hole where +klass.new(attacker_params)+
1261
+ # on a hash that happens to include +objectId+ would overwrite
1262
+ # session tokens, ACLs, and auth data. Use {Parse::Object.build}
1263
+ # for trusted hydration from server JSON; it bypasses the filter.
1264
+ # @return [Parse::Object] a the corresponding Parse::Object or subclass.
1265
+ def initialize(opts = {})
1266
+ # Trusted hydration is signalled by the +@_trusted_init+ instance
1267
+ # variable rather than by a +trusted:+ keyword argument. Using a
1268
+ # keyword would break subclasses that override +initialize(*args)+
1269
+ # and call +super+ — Ruby 3 keyword-arg semantics would convert the
1270
+ # kwarg into a positional Hash through the variadic +*args+ splat
1271
+ # and the subsequent +super+ would arrive at this method with two
1272
+ # positional args. The internal hydration paths
1273
+ # ({Parse::Object.build}, {Parse::Pointer} autofetch,
1274
+ # {Parse::User#session}) +allocate+ the object, set the ivar, then
1275
+ # invoke +initialize+ so subclass overrides still fire and pick up
1276
+ # the trust signal here.
1277
+ trusted = @_trusted_init == true
1278
+ @_trusted_init = nil
1279
+ acl_owner_override = nil
1280
+ if opts.is_a?(String) #then it's the objectId
1281
+ @id = opts.to_s
1282
+ elsif opts.is_a?(Hash)
1283
+ # Pop the `:as` option (also accepts string key) before applying
1284
+ # attributes so it is not mistaken for a model property. This holds
1285
+ # the caller-supplied owner user for save-time ACL resolution.
1286
+ acl_owner_override = opts.delete(:as) || opts.delete("as")
1287
+ #if the objectId is provided we will consider the object pristine
1288
+ #and not track dirty items
1289
+ dirty_track = opts[Parse::Model::OBJECT_ID] || opts[:objectId] || opts[:id]
1290
+ # Always filter the narrow PROTECTED_INITIALIZE_KEYS set unless
1291
+ # the caller is a trusted hydration path. Decoupled from
1292
+ # dirty_track so an objectId-bearing hash from a controller,
1293
+ # JSON params, or cache rehydrator cannot mass-assign
1294
+ # sessionToken / _rperm / _wperm / _hashed_password / authData /
1295
+ # roles. The narrow list deliberately allows createdAt /
1296
+ # updatedAt / className / __type through so the legitimate
1297
+ # +Klass.new("objectId" => id, "createdAt" => ts, …)+
1298
+ # cache-rehydrate pattern keeps working.
1299
+ apply_attributes!(opts,
1300
+ dirty_track: !dirty_track,
1301
+ filter_protected: !trusted,
1302
+ protected_set: Parse::Properties::PROTECTED_INITIALIZE_KEYS)
1303
+ end
1304
+
1305
+ # If the caller did not set an ACL via opts, stamp the class default ACL
1306
+ # (the policy's fallback half) so `obj.acl` reads sensibly pre-save.
1307
+ # We mark the object as "ACL-pristine": the save-time resolver
1308
+ # (#_resolve_default_acl) may upgrade this to an owner-only ACL if an
1309
+ # `as:` user or owner field is resolvable. Any explicit caller change
1310
+ # via `acl=` flips pristine off via #acl_will_change!.
1311
+ #
1312
+ # Built-in Parse classes (User, Installation, Session, Role, …) are
1313
+ # exempt: the SDK leaves their `acl` untouched (nil) so the save body
1314
+ # omits the `ACL` field and Parse Server applies its own per-class
1315
+ # defaults. Most importantly this lets `_User` get the standard
1316
+ # self-write-plus-public-read ACL on signup; stamping any value from
1317
+ # the SDK side (even `{}`) overrides that and locks the new user out
1318
+ # of editing their own profile without the master key.
1319
+ acl_was_user_supplied = !self.acl.nil?
1320
+ unless self.class.builtin_acl_default_active?
1321
+ self.acl = self.class.default_acls.as_json if self.acl.nil?
1322
+ end
1323
+ @_acl_pristine = !acl_was_user_supplied
1324
+ @_acl_owner_override = acl_owner_override
1325
+
1326
+ # One-time per-class permissive-default warning. Fires only when the
1327
+ # effective policy is :public or :owner_else_public.
1328
+ self.class._warn_permissive_acl_default_once
1329
+
1330
+ # do not apply defaults on a pointer because it will stop it from being
1331
+ # a pointer and will cause its field to be autofetched (for sync).
1332
+ # Note: apply_defaults! already skips unfetched fields on selectively fetched objects.
1333
+ if !pointer?
1334
+ apply_defaults!
1335
+ end
1336
+
1337
+ # clear changes AFTER applying defaults, so fields set by defaults
1338
+ # are not marked dirty when fetching with specific keys
1339
+ clear_changes! if @id.present? #then it was an import
1340
+ # do not call super since it is Pointer subclass
1341
+ end
1342
+
1343
+ # force apply default values for any properties defined with default values.
1344
+ # @return [Array] list of default fields
1345
+ def apply_defaults!
1346
+ self.class.defaults_list.each do |key|
1347
+ # Skip applying defaults to unfetched fields on selectively fetched objects.
1348
+ # This preserves the ability to autofetch when the field is accessed.
1349
+ next if has_selective_keys? && !field_was_fetched?(key)
1350
+
1351
+ send(key) # should call set default proc/values if nil
1352
+ end
1353
+ end
1354
+
1355
+ # Helper method to create a Parse::Pointer object for a given id.
1356
+ # @param id [String] The objectId
1357
+ # @return [Parse::Pointer] a pointer object corresponding to this class and id.
1358
+ def self.pointer(id)
1359
+ return nil if id.nil?
1360
+ Parse::Pointer.new self.parse_class, id
1361
+ end
1362
+
1363
+ # Determines if this object has been saved to the Parse database. If an object has
1364
+ # pending changes, then it is considered to not yet be persisted.
1365
+ # @return [Boolean] true if this object has not been saved.
1366
+ def persisted?
1367
+ changed? == false && !(@id.nil? || @created_at.nil? || @updated_at.nil? || @acl.nil?)
1368
+ end
1369
+
1370
+ # Force reload from the database and replace any local fields with data from
1371
+ # the persistent store. By default, bypasses cache reads but updates the cache
1372
+ # with fresh data (write-only mode) so future cached reads get the latest data.
1373
+ # @param opts [Hash] a set of options to send to fetch!
1374
+ # @option opts [Boolean, Symbol] :cache (:write_only) caching mode:
1375
+ # - :write_only (default) - skip cache read, but update cache with fresh data
1376
+ # - true - read from and write to cache
1377
+ # - false - completely bypass cache (no read or write)
1378
+ # @see Fetching#fetch!
1379
+ # @example Reload with fresh data (default - updates cache)
1380
+ # song.reload!
1381
+ # @example Reload with full caching (may return cached data)
1382
+ # song.reload!(cache: true)
1383
+ # @example Reload completely bypassing cache
1384
+ # song.reload!(cache: false)
1385
+ def reload!(**opts)
1386
+ # Default to write-only cache mode - reload always gets fresh data
1387
+ # but updates cache for future cached reads. Controlled by feature flag.
1388
+ unless opts.key?(:cache)
1389
+ opts[:cache] = Parse.cache_write_on_fetch ? :write_only : false
1390
+ end
1391
+ # get the values from the persistence layer
1392
+ fetch!(**opts)
1393
+ clear_changes!
1394
+ end
1395
+
1396
+ # clears all dirty tracking information
1397
+ def clear_changes!
1398
+ clear_changes_information
1399
+ # Clear the ACL snapshot used for proper acl_was tracking
1400
+ @_acl_snapshot_before_change = nil
1401
+ end
1402
+
1403
+ # An object is considered new until it has been successfully persisted to
1404
+ # the server. "Persisted" means the server has returned a `createdAt`
1405
+ # timestamp, which only happens after a successful create. Checking
1406
+ # @id alone is not sufficient: the `parse_reference precompute: true`
1407
+ # path assigns @id client-side in a `before_create` callback, so an
1408
+ # @id-only check would flip mid-callback-chain and confuse user code
1409
+ # (validation `on: :create / :update`, beforeSave handlers, etc.).
1410
+ # Treating an object as "new" until createdAt arrives keeps semantics
1411
+ # stable from the first `before_save` through the end of `after_create`.
1412
+ # @return [Boolean] true if the object has not yet been persisted.
1413
+ def new?
1414
+ @id.blank? || @created_at.nil?
1415
+ end
1416
+
1417
+ # Override valid? to run validation callbacks.
1418
+ # This wraps the standard ActiveModel validation with our custom :validation callbacks.
1419
+ # @param context [Symbol, nil] validation context (same as ActiveModel)
1420
+ # @return [Boolean] true if the object passes all validations
1421
+ def valid?(context = nil)
1422
+ result = true
1423
+ run_callbacks :validation do
1424
+ result = super(context)
1425
+ end
1426
+ result
1427
+ end
1428
+
1429
+ # Existed returns true if the object had existed before *its last save
1430
+ # operation*. This method returns false if the {Parse::Object#created_at}
1431
+ # and {Parse::Object#updated_at} dates of an object are equal, implyiny this
1432
+ # object has been newly created and saved (especially in an afterSave hook).
1433
+ #
1434
+ # This is a helper method in a webhook afterSave to know
1435
+ # if this object was recently saved in the beforeSave webhook. Checking for
1436
+ # {Parse::Object#existed?} == false in an afterSave hook, is equivalent to using
1437
+ # {Parse::Object#new?} in a beforeSave hook.
1438
+ # @note You should not use this method inside a beforeSave webhook.
1439
+ # @return [Boolean] true iff the last beforeSave successfully saved this object for the first time.
1440
+ def existed?
1441
+ if @id.blank? || @created_at.blank? || @updated_at.blank?
1442
+ return false
1443
+ end
1444
+ created_at != updated_at
1445
+ end
1446
+
1447
+ # Returns whether this object was fetched with specific keys (selective fetch).
1448
+ # When selectively fetched, accessing unfetched fields will trigger an autofetch.
1449
+ # This is an internal method used for autofetch logic.
1450
+ # @return [Boolean] true if the object was fetched with specific keys.
1451
+ # @api private
1452
+ def has_selective_keys?
1453
+ @_fetched_keys&.any? || false
1454
+ end
1455
+
1456
+ # Returns whether this object was fetched with specific keys (partial/selective fetch).
1457
+ # When partially fetched, only the specified keys are available and accessing other
1458
+ # fields will trigger an autofetch. Returns false for pointers and fully fetched objects.
1459
+ # @return [Boolean] true if the object was fetched with specific keys.
1460
+ def partially_fetched?
1461
+ !pointer? && has_selective_keys?
1462
+ end
1463
+
1464
+ # Returns whether this object is fully fetched with all fields available.
1465
+ # Returns false if the object is a pointer or was fetched with specific keys.
1466
+ # @return [Boolean] true if the object is fully fetched.
1467
+ def fully_fetched?
1468
+ !pointer? && !has_selective_keys?
1469
+ end
1470
+
1471
+ # Returns whether this object has been fetched from the server (fully or partially).
1472
+ # Overrides Pointer#fetched? to return true for any object with data.
1473
+ # @return [Boolean] true if the object has data (not just a pointer).
1474
+ def fetched?
1475
+ !pointer?
1476
+ end
1477
+
1478
+ # Returns the array of keys that were fetched for this object.
1479
+ # Empty array means the object was fully fetched.
1480
+ # Returns a frozen duplicate to prevent external mutation.
1481
+ # @return [Array<Symbol>] the keys that were fetched.
1482
+ def fetched_keys
1483
+ (@_fetched_keys || []).dup.freeze
1484
+ end
1485
+
1486
+ # Disables autofetch for this object instance.
1487
+ # Useful for preventing automatic network requests.
1488
+ # @return [void]
1489
+ def disable_autofetch!
1490
+ @_autofetch_disabled = true
1491
+ end
1492
+
1493
+ # Enables autofetch for this object instance (default behavior).
1494
+ # @return [void]
1495
+ def enable_autofetch!
1496
+ @_autofetch_disabled = false
1497
+ end
1498
+
1499
+ # Returns whether autofetch is disabled for this instance.
1500
+ # @return [Boolean] true if autofetch is disabled
1501
+ def autofetch_disabled?
1502
+ @_autofetch_disabled == true
1503
+ end
1504
+
1505
+ # Sets the fetched keys for this object. Used internally when building
1506
+ # objects from partial fetch queries.
1507
+ # @param keys [Array] the keys that were fetched
1508
+ # @return [Array] the stored keys
1509
+ def fetched_keys=(keys)
1510
+ if keys.nil? || keys.empty?
1511
+ @_fetched_keys = nil
1512
+ else
1513
+ # Always include :id and convert to symbols
1514
+ @_fetched_keys = keys.map { |k| Parse::Query.format_field(k).to_sym }
1515
+ @_fetched_keys << :id unless @_fetched_keys.include?(:id)
1516
+ @_fetched_keys << :objectId unless @_fetched_keys.include?(:objectId)
1517
+ @_fetched_keys.uniq!
1518
+ end
1519
+ @_fetched_keys
1520
+ end
1521
+
1522
+ # Returns whether a specific field was fetched for this object.
1523
+ # Base keys (id, created_at, updated_at) are always considered fetched.
1524
+ # @param key [Symbol, String] the field name to check
1525
+ # @return [Boolean] true if the field was fetched or if object is fully fetched.
1526
+ def field_was_fetched?(key)
1527
+ # If not partially fetched (i.e., still a pointer), all fields are NOT fetched
1528
+ return false if pointer?
1529
+
1530
+ # If no selective keys were specified, this is a fully fetched object
1531
+ # All fields are considered fetched
1532
+ return true unless has_selective_keys?
1533
+
1534
+ key = key.to_sym
1535
+ # Base keys are always considered fetched
1536
+ return true if Parse::Properties::BASE_KEYS.include?(key)
1537
+ return true if key == :acl || key == :ACL
1538
+
1539
+ # Check both local key and remote field name
1540
+ # Convert remote_key to symbol for consistent comparison
1541
+ remote_key = self.field_map[key]&.to_sym
1542
+ @_fetched_keys.include?(key) || (remote_key && @_fetched_keys.include?(remote_key))
1543
+ end
1544
+
1545
+ # Returns the nested fetched keys map for building nested objects.
1546
+ # @return [Hash] map of field names to their fetched keys
1547
+ def nested_fetched_keys
1548
+ @_nested_fetched_keys || {}
1549
+ end
1550
+
1551
+ # Sets the nested fetched keys map for building nested objects.
1552
+ # @param keys_map [Hash] map of field names to their fetched keys
1553
+ # @return [Hash] the stored map
1554
+ def nested_fetched_keys=(keys_map)
1555
+ @_nested_fetched_keys = keys_map.is_a?(Hash) ? keys_map : nil
1556
+ end
1557
+
1558
+ # Gets the fetched keys for a specific nested field.
1559
+ # @param field_name [Symbol, String] the field name
1560
+ # @return [Array, nil] the fetched keys for the nested object, or nil if not specified
1561
+ def nested_keys_for(field_name)
1562
+ return nil unless @_nested_fetched_keys.present?
1563
+ field_name = field_name.to_sym
1564
+ @_nested_fetched_keys[field_name]
1565
+ end
1566
+
1567
+ # Clears all partial fetch tracking state.
1568
+ # Called after successful save since server returns updated object.
1569
+ # @return [void]
1570
+ def clear_partial_fetch_state!
1571
+ @_fetched_keys = nil
1572
+ @_nested_fetched_keys = nil
1573
+ end
1574
+
1575
+ # Run after_create callbacks for this object.
1576
+ # This method is called by webhook handlers when an object is created.
1577
+ # @return [Boolean] true if callbacks executed successfully
1578
+ def run_after_create_callbacks
1579
+ run_callbacks_from_list(self.class._create_callbacks, :after)
1580
+ end
1581
+
1582
+ # Run after_save callbacks for this object.
1583
+ # This method is called by webhook handlers when an object is saved.
1584
+ # @return [Boolean] true if callbacks executed successfully
1585
+ def run_after_save_callbacks
1586
+ run_callbacks_from_list(self.class._save_callbacks, :after)
1587
+ end
1588
+
1589
+ # Run after_destroy callbacks for this object.
1590
+ # This method is called by webhook handlers when an object is deleted.
1591
+ # @return [Boolean] true if callbacks executed successfully
1592
+ def run_after_delete_callbacks
1593
+ run_callbacks_from_list(self.class._destroy_callbacks, :after)
1594
+ end
1595
+
1596
+ # Returns a hash of all the changes that have been made to the object. By default
1597
+ # changes to the Parse::Properties::BASE_KEYS are ignored unless you pass true as
1598
+ # an argument.
1599
+ # @param include_all [Boolean] whether to include all keys in result.
1600
+ # @return [Hash] a hash containing only the change information.
1601
+ # @see Properties::BASE_KEYS
1602
+ def updates(include_all = false)
1603
+ h = {}
1604
+ changed.each do |key|
1605
+ next if include_all == false && Parse::Properties::BASE_KEYS.include?(key.to_sym)
1606
+ # lookup the remote Parse field name incase it is different from the local attribute name
1607
+ remote_field = self.field_map[key.to_sym] || key
1608
+ h[remote_field] = send key
1609
+ # make an exception to Parse::Objects, we should return a pointer to them instead
1610
+ h[remote_field] = h[remote_field].parse_pointers if h[remote_field].is_a?(Parse::PointerCollectionProxy)
1611
+ h[remote_field] = h[remote_field].pointer if h[remote_field].respond_to?(:pointer)
1612
+ end
1613
+ h
1614
+ end
1615
+
1616
+ # Locally restores the previous state of the object and clears all dirty
1617
+ # tracking information.
1618
+ # @note This does not reload the object from the persistent store, for this use "reload!" instead.
1619
+ # @see #reload!
1620
+ def rollback!
1621
+ restore_attributes
1622
+ end
1623
+
1624
+ # Overrides ActiveModel::Validations#validate! instance method.
1625
+ # It runs all validations for this object. If validation fails,
1626
+ # it raises ActiveModel::ValidationError otherwise it returns the object.
1627
+ # @raise ActiveModel::ValidationError
1628
+ # @see ActiveModel::Validations#validate!
1629
+ # @return [self] self the object if validation passes.
1630
+ def validate!
1631
+ super
1632
+ self
1633
+ end
1634
+
1635
+ # This method creates a new object of the same instance type with a copy of
1636
+ # all the properties of the current instance. This is useful when you want
1637
+ # to create a duplicate record.
1638
+ # @return [Parse::Object] a twin copy of the object without the objectId
1639
+ def twin
1640
+ h = self.as_json
1641
+ h.delete(Parse::Model::OBJECT_ID)
1642
+ h.delete(:objectId)
1643
+ h.delete(:id)
1644
+ self.class.new h
1645
+ end
1646
+
1647
+ # @return [String] a pretty-formatted JSON string
1648
+ # @see JSON.pretty_generate
1649
+ def pretty
1650
+ JSON.pretty_generate(as_json)
1651
+ end
1652
+
1653
+ # clear all change and dirty tracking information.
1654
+ def clear_attribute_change!(atts)
1655
+ clear_attribute_changes(atts)
1656
+ end
1657
+
1658
+ # Method used for decoding JSON objects into their corresponding Object subclasses.
1659
+ # The first parameter is a hash containing the object data and the second parameter is the
1660
+ # name of the table / class if it is known. If it is not known, we we try and determine it
1661
+ # by checking the "className" or :className entries in the hash.
1662
+ # @note If a Parse class object hash is encoutered for which we don't have a
1663
+ # corresponding Parse::Object subclass for, a Parse::Pointer will be returned instead.
1664
+ #
1665
+ # @example
1666
+ # # assume you have defined Post subclass
1667
+ # post = Parse::Object.build({"className" => "Post", "objectId" => '1234'})
1668
+ # post # => #<Post:....>
1669
+ #
1670
+ # # if you know the table name
1671
+ # post = Parse::Object.build({"title" => "My Title"}, "Post")
1672
+ # # or
1673
+ # post = Post.build({"title" => "My Title"})
1674
+ # @param json [Hash] a JSON hash that contains a Parse object.
1675
+ # @param table [String] the Parse class for this hash. If not passed it will be detected.
1676
+ # @param fetched_keys [Array] optional array of keys that were fetched (for partial fetch tracking).
1677
+ # @param nested_fetched_keys [Hash] optional map of field names to their fetched keys for nested objects.
1678
+ # @return [Parse::Object] an instance of the Parse subclass
1679
+ def self.build(json, table = nil, fetched_keys: nil, nested_fetched_keys: nil)
1680
+ # Precedence (most → least authoritative):
1681
+ # 1. Caller-supplied +table+ — caller knows the expected class
1682
+ # (e.g. webhook payload routed to a typed handler, has_many that
1683
+ # knows its declared target class).
1684
+ # 2. The subclass +parse_class+ when invoked on a Parse::Object
1685
+ # subclass directly (Song.build(json)).
1686
+ # 3. The className inside the JSON — only trusted when neither of
1687
+ # the above is available (e.g. base-class +Parse::Object.build+
1688
+ # on untyped JSON).
1689
+ # Warn on mismatch between an explicit caller class and the
1690
+ # payload-supplied className so type-confusion attacks surface in
1691
+ # logs.
1692
+ incoming_class = nil
1693
+ if json.is_a?(Hash)
1694
+ incoming_class = json[Parse::Model::KEY_CLASS_NAME] || json[:className]
1695
+ end
1696
+ className = table
1697
+ if className.nil? && parse_class != BASE_OBJECT_CLASS
1698
+ className = parse_class
1699
+ end
1700
+ className ||= incoming_class
1701
+ if className && incoming_class && incoming_class != className
1702
+ warn "[Parse::Object.build] expected className=#{className.inspect}, ignoring incoming className=#{incoming_class.inspect}"
1703
+ end
1704
+ if json.is_a?(Hash) && json["error"].present? && json["code"].present?
1705
+ warn "[Parse::Object] Detected object hash with 'error' and 'code' set. : #{json}"
1706
+ end
1707
+ return if className.nil?
1708
+ # we should do a reverse lookup on who is registered for a different class type
1709
+ # than their name with parse_class
1710
+ klass = Parse::Model.find_class className
1711
+ o = nil
1712
+ if klass.present?
1713
+ # when creating objects from Parse JSON data, don't use dirty tracking since
1714
+ # we are considering these objects as "pristine"
1715
+ o = klass.allocate
1716
+
1717
+ # Set BOTH nested_fetched_keys AND fetched_keys BEFORE initialize
1718
+ # to ensure partially_fetched? returns correct value during attribute application
1719
+ o.instance_variable_set(:@_nested_fetched_keys, nested_fetched_keys) if nested_fetched_keys.present?
1720
+ if fetched_keys.present?
1721
+ # Process fetched_keys like the setter does - convert to symbols and include :id
1722
+ processed_keys = fetched_keys.map { |k| Parse::Query.format_field(k).to_sym }
1723
+ processed_keys << :id unless processed_keys.include?(:id)
1724
+ processed_keys << :objectId unless processed_keys.include?(:objectId)
1725
+ processed_keys.uniq!
1726
+ o.instance_variable_set(:@_fetched_keys, processed_keys)
1727
+ end
1728
+
1729
+ # Trusted hydration: this path runs on server-side JSON (response
1730
+ # bodies, webhook payloads that have already been scrubbed,
1731
+ # autofetch results). Server responses legitimately include
1732
+ # protected keys like +sessionToken+, +_rperm+ that must populate
1733
+ # the in-memory object. Untrusted +klass.new(hash)+ callers
1734
+ # default to filter those keys. The +@_trusted_init+ ivar is the
1735
+ # signal — see {#initialize} for why we don't use a kwarg.
1736
+ o.instance_variable_set(:@_trusted_init, true)
1737
+ o.send(:initialize, json)
1738
+ else
1739
+ o = Parse::Pointer.new className, (json[Parse::Model::OBJECT_ID] || json[:objectId])
1740
+ end
1741
+ return o
1742
+ # rescue NameError => e
1743
+ # puts "Parse::Object.build constant class error: #{e}"
1744
+ # rescue Exception => e
1745
+ # puts "Parse::Object.build error: #{e}"
1746
+ end
1747
+
1748
+ # @!attribute id
1749
+ # @return [String] the value of Parse "objectId" field.
1750
+ property :id, field: :objectId
1751
+
1752
+ # @!attribute [r] created_at
1753
+ # @return [Date] the created_at date of the record in UTC Zulu iso 8601 with 3 millisecond format.
1754
+ property :created_at, :date
1755
+
1756
+ # @!attribute [r] updated_at
1757
+ # @return [Date] the updated_at date of the record in UTC Zulu iso 8601 with 3 millisecond format.
1758
+ property :updated_at, :date
1759
+
1760
+ # @!attribute acl
1761
+ # @return [ACL] the access control list (permissions) object for this record.
1762
+ property :acl, :acl, field: :ACL
1763
+
1764
+ # Save-time resolver for the declarative {acl_policy} default ACL.
1765
+ # Runs as a `before_save` callback. If the caller has not overridden the
1766
+ # ACL (no `acl=` since the init-time default stamp), resolves an owner
1767
+ # from `@_acl_owner_override` (the `as:` kwarg) or from the class's
1768
+ # declared owner field, and applies an owner-only ACL. Falls back to
1769
+ # the policy's else-half (`:public` or `:private`) when no owner is
1770
+ # resolvable.
1771
+ # @api private
1772
+ def _resolve_default_acl
1773
+ return true unless defined?(@_acl_pristine) && @_acl_pristine
1774
+ # Legacy classes that customize defaults via set_default_acl opt out
1775
+ # of the policy resolver: the init-time stamp already reflects the
1776
+ # caller's intent and we must not overwrite it.
1777
+ return true if self.class.acl_default_customized_by_set_default_acl?
1778
+ # Built-in Parse classes (User, Installation, Session, …) are exempt
1779
+ # by default; see the matching guard in #initialize. Parse Server
1780
+ # applies its own ACL defaults when the save body omits the `ACL`
1781
+ # field, and those defaults (e.g. `_User` → self-write + public read)
1782
+ # are the right answer in nearly every case. Applications that need
1783
+ # to customize a built-in's ACL policy do so by calling `acl_policy`
1784
+ # or `set_default_acl` on the class — that flips
1785
+ # `builtin_acl_default_active?` to false and re-enables both the
1786
+ # init-time stamp and this resolver for that class.
1787
+ return true if self.class.builtin_acl_default_active?
1788
+ policy = self.class.acl_policy_setting
1789
+
1790
+ owner = @_acl_owner_override if defined?(@_acl_owner_override)
1791
+ if owner.nil? && (field = self.class.acl_owner_field)
1792
+ owner = if field == :self
1793
+ # Self-referential ownership (Parse::User only — enforced at
1794
+ # declaration time). Pre-generate a Parse-compatible objectId
1795
+ # client-side so the ACL grant can reference the record's own
1796
+ # id in the same POST body that creates it. Skipped when the id
1797
+ # is already set (e.g. when re-saving an existing user, or when
1798
+ # parse_reference precompute already ran).
1799
+ @id = Parse::Core::ParseReference.generate_object_id if @id.blank?
1800
+ @id
1801
+ elsif respond_to?(field)
1802
+ send(field)
1803
+ end
1804
+ end
1805
+ owner_id = _resolve_acl_owner_id(owner)
1806
+
1807
+ target_acl = case policy
1808
+ when :public
1809
+ Parse::ACL.everyone(true, true)
1810
+ when :private
1811
+ Parse::ACL.private
1812
+ when :owner_else_public
1813
+ if owner_id
1814
+ acl = Parse::ACL.new
1815
+ acl.apply(owner_id, true, true)
1816
+ acl
1817
+ else
1818
+ Parse::ACL.everyone(true, true)
1819
+ end
1820
+ when :owner_else_private
1821
+ if owner_id
1822
+ acl = Parse::ACL.new
1823
+ acl.apply(owner_id, true, true)
1824
+ acl
1825
+ else
1826
+ Parse::ACL.private
1827
+ end
1828
+ end
1829
+
1830
+ # Only re-stamp if the resolved ACL differs from the init-time stamp;
1831
+ # this avoids an unnecessary dirty mark on the acl field for `:public`
1832
+ # / `:private` policies where the init stamp already matches.
1833
+ if @acl.nil? || @acl.as_json != target_acl.as_json
1834
+ self.acl = target_acl.as_json
1835
+ end
1836
+ # @_acl_pristine is now false via #acl_will_change! (when re-stamped)
1837
+ # or it remains true (when nothing needed to change); either way the
1838
+ # resolver has done its job and need not run again. Return a non-false
1839
+ # value so the save callback chain is not halted by the model's
1840
+ # terminator (`result_lambda.call == false`).
1841
+ @_acl_pristine = false
1842
+ true
1843
+ end
1844
+
1845
+ # @api private
1846
+ # Resolves an `as:` value or owner-field pointer to an objectId string.
1847
+ # Strictly type-gated to Parse::User-shaped inputs to prevent accidental
1848
+ # ACL grants to non-user records (Roles use `role:` ACL keys, not raw
1849
+ # objectIds; pointers to non-User classes would silently grant access to
1850
+ # whatever record happens to share that objectId in the User collection).
1851
+ # Accepted forms:
1852
+ # - Parse::User instance
1853
+ # - Parse::Pointer with parse_class == "_User"
1854
+ # - Raw objectId String (caller's responsibility to ensure it is a user id)
1855
+ # Anything else returns nil and the policy falls through to its else-half.
1856
+ def _resolve_acl_owner_id(owner)
1857
+ return nil if owner.nil?
1858
+ return nil if owner.respond_to?(:empty?) && owner.empty?
1859
+ if owner.is_a?(Parse::Pointer)
1860
+ return nil unless owner.parse_class == Parse::Model::CLASS_USER
1861
+ return owner.id if owner.id.present?
1862
+ return nil
1863
+ end
1864
+ return owner if owner.is_a?(String) && owner.present?
1865
+ nil
1866
+ end
1867
+
1868
+ set_callback :save, :before, :_resolve_default_acl
1869
+
1870
+ # Override acl_will_change! to capture a snapshot of the ACL before modification.
1871
+ # This is necessary because ACL is a mutable object that can be modified in place
1872
+ # (via apply, apply_role, etc.). Without this, acl_was would return a reference
1873
+ # to the same object as acl, making them appear identical after in-place changes.
1874
+ #
1875
+ # Also clears the ACL-pristine flag so the save-time default-ACL resolver
1876
+ # leaves caller-set ACLs alone. The initial default stamp performed in
1877
+ # {#initialize} is excluded by re-asserting `@_acl_pristine = true` after
1878
+ # the stamp, so this hook can safely treat any subsequent change as a
1879
+ # caller intent to override.
1880
+ # @api private
1881
+ def acl_will_change!
1882
+ # Only capture snapshot on the first change (before any modifications)
1883
+ unless defined?(@_acl_snapshot_before_change) && @_acl_snapshot_before_change
1884
+ # Deep copy the ACL by creating a new one from its JSON representation
1885
+ @_acl_snapshot_before_change = @acl ? Parse::ACL.new(@acl.as_json) : Parse::ACL.new
1886
+ end
1887
+ @_acl_pristine = false if defined?(@_acl_pristine)
1888
+ super
1889
+ end
1890
+
1891
+ # EnhancedChangeTracking defines acl_was via define_method when
1892
+ # `property :acl` is processed above. Remove that definition so the
1893
+ # explicit override below does not emit "method redefined" under ruby -W.
1894
+ # The override is intentional - ACL needs snapshot-based dirty tracking
1895
+ # because it is a mutable object.
1896
+ remove_method(:acl_was) if method_defined?(:acl_was, false)
1897
+
1898
+ # Override acl_was to return the captured snapshot instead of the reference
1899
+ # stored by ActiveModel's dirty tracking.
1900
+ # @return [Parse::ACL] the ACL value before any changes were made.
1901
+ def acl_was
1902
+ # If we have a snapshot, return it; otherwise fall back to ActiveModel's behavior
1903
+ if defined?(@_acl_snapshot_before_change) && @_acl_snapshot_before_change
1904
+ @_acl_snapshot_before_change
1905
+ else
1906
+ super
1907
+ end
1908
+ end
1909
+
1910
+ # Override acl_changed? to compare actual ACL content, not just object references.
1911
+ # This ensures that setting an ACL to identical values doesn't mark it as changed.
1912
+ # @return [Boolean] true only if the ACL content has actually changed.
1913
+ def acl_changed?
1914
+ # First check if ActiveModel thinks it changed
1915
+ return false unless super
1916
+ # Then verify the content actually changed by comparing JSON representations
1917
+ acl_was_json = acl_was.respond_to?(:as_json) ? acl_was.as_json : acl_was
1918
+ acl_current_json = @acl&.respond_to?(:as_json) ? @acl.as_json : @acl
1919
+ acl_was_json != acl_current_json
1920
+ end
1921
+
1922
+ # Override changed to filter out ACL when its content hasn't actually changed.
1923
+ # This ensures dirty? returns false when ACL is rebuilt to identical values.
1924
+ # For new objects, ACL is always included since it needs to be sent to the server.
1925
+ # @return [Array<String>] list of changed attribute names.
1926
+ def changed
1927
+ result = super.dup
1928
+ # If ACL is in the changed list but content is identical, remove it
1929
+ # BUT keep it if the object is new (needs to be sent to server)
1930
+ if result.include?("acl") && !new? && !acl_changed?
1931
+ result.delete("acl")
1932
+ end
1933
+ result
1934
+ end
1935
+
1936
+ # Override changed? to use our filtered changed list.
1937
+ # ActiveModel's changed? uses internal tracking that doesn't account for
1938
+ # ACL content comparison.
1939
+ # @return [Boolean] true if any attributes have changed.
1940
+ def changed?
1941
+ changed.any?
1942
+ end
1943
+
1944
+ # Access the value for a defined property through hash accessor. This method
1945
+ # returns nil if the key is not one of the defined properties for this Parse::Object
1946
+ # subclass.
1947
+ # @param key [String] the name of the property. This key must be in the {fields} hash.
1948
+ # @return [Object] the value for this key.
1949
+ def [](key)
1950
+ return nil unless self.class.fields[key.to_sym].present?
1951
+ send(key)
1952
+ end
1953
+
1954
+ # Set the value for a specific property through a hash accessor. This method
1955
+ # does nothing if key is not one of the defined properties for this Parse::Object
1956
+ # subclass.
1957
+ # @param key (see Parse::Object#[])
1958
+ # @param value [Object] the value to set this property.
1959
+ # @return [Object] the value passed in.
1960
+ def []=(key, value)
1961
+ return unless self.class.fields[key.to_sym].present?
1962
+ send("#{key}=", value)
1963
+ end
1964
+
1965
+ # Returns an array of property names (keys) for this Parse::Object.
1966
+ # Similar to Hash#keys, this method returns all the defined field names
1967
+ # for this object's class.
1968
+ # @return [Array<String>] an array of property names as strings.
1969
+ def keys
1970
+ self.class.fields.keys.map(&:to_s)
1971
+ end
1972
+
1973
+ # Check if a field has a value (is present and not nil).
1974
+ # @param key [String, Symbol] the name of the field to check.
1975
+ # @return [Boolean] true if the field has a non-nil value, false otherwise.
1976
+ def has?(key)
1977
+ return false unless self.class.fields[key.to_sym].present?
1978
+ value = send(key)
1979
+ !value.nil?
1980
+ end
1981
+
1982
+ private
1983
+
1984
+ # Helper to run a set of callbacks of a certain kind (e.g., :after)
1985
+ def run_callbacks_from_list(callbacks, kind)
1986
+ callbacks.select { |cb| cb.kind == kind }.each do |callback|
1987
+ # 'filter' can be a Symbol (method name), String (code), or Proc.
1988
+ case callback.filter
1989
+ when Symbol
1990
+ send(callback.filter)
1991
+ when Proc
1992
+ instance_exec(&callback.filter)
1993
+ when String
1994
+ instance_eval(callback.filter)
1995
+ end
1996
+ end
1997
+ true
1998
+ end
1999
+ end
2000
+ end
2001
+
2002
+ class Hash
2003
+
2004
+ # Turns a Parse formatted JSON hash into a Parse-Stack class object, if one is found.
2005
+ # This is equivalent to calling `Parse::Object.build` on the hash object itself, but allows
2006
+ # for doing this in loops, such as `map` when using symbol to proc. However, you can also use
2007
+ # the Array extension `Array#parse_objects` for doing that more safely.
2008
+ # @return [Parse::Object] A Parse::Object subclass represented the built class.
2009
+ def parse_object
2010
+ Parse::Object.build(self)
2011
+ end
2012
+ end
2013
+
2014
+ class Array
2015
+ # This helper method selects or converts all objects in an array that are either inherit from
2016
+ # Parse::Pointer or are a JSON Parse hash. If it is a hash, a Pare::Object will be built from it
2017
+ # if it constrains the proper fields. Non-convertible objects will be removed.
2018
+ #
2019
+ # When +className+ is provided by the caller, it is treated as authoritative
2020
+ # — incoming hash +className+ values are ignored. This blocks attacker-
2021
+ # controlled type confusion when this helper is invoked from typed
2022
+ # associations (+has_many+, +belongs_to+) that already know the expected
2023
+ # class.
2024
+ #
2025
+ # When +className+ is +nil+ (caller is doing untyped array conversion),
2026
+ # the helper falls back to the hash-supplied className for compatibility
2027
+ # with raw JSON deserialization callers.
2028
+ #
2029
+ # @param className [String, nil] the authoritative Parse class name.
2030
+ # @return [Array<Parse::Object>] an array of Parse::Object subclasses.
2031
+ def parse_objects(className = nil)
2032
+ f = Parse::Model::KEY_CLASS_NAME
2033
+ map do |m|
2034
+ next m if m.is_a?(Parse::Pointer)
2035
+ if m.is_a?(Hash)
2036
+ resolved = if className
2037
+ # Caller knows the type; warn on mismatch but always
2038
+ # use the declared className.
2039
+ incoming = m[f] || m[:className]
2040
+ if incoming && incoming != className
2041
+ warn "[Parse::Array#parse_objects] expected className=#{className.inspect}, ignoring incoming className=#{incoming.inspect}"
2042
+ end
2043
+ className
2044
+ else
2045
+ m[f] || m[:className]
2046
+ end
2047
+ next Parse::Object.build(m, resolved) if resolved
2048
+ end
2049
+ nil
2050
+ end.compact
2051
+ end
2052
+
2053
+ # @return [Array<String>] an array of objectIds for all objects that are Parse::Objects.
2054
+ def parse_ids
2055
+ parse_objects.map(&:id)
2056
+ end
2057
+ end
2058
+
2059
+ # Load all the core classes.
2060
+ require_relative "classes/audience"
2061
+ require_relative "classes/installation"
2062
+ require_relative "classes/job_schedule"
2063
+ require_relative "classes/job_status"
2064
+ require_relative "classes/product"
2065
+ require_relative "classes/push_status"
2066
+ require_relative "classes/role"
2067
+ require_relative "classes/session"
2068
+ require_relative "classes/user"