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,1268 @@
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 "time"
9
+ require_relative "../../client/request"
10
+ require_relative "fetching"
11
+
12
+ module Parse
13
+ class Query
14
+
15
+ # Supporting the `first_or_create` class method to be used in scope chaining with queries.
16
+ # @!visibility private
17
+ def first_or_create(query_attrs = {}, resource_attrs = {})
18
+ conditions(query_attrs)
19
+ klass = Parse::Model.find_class self.table
20
+ if klass.blank?
21
+ raise ArgumentError, "Parse model with class name #{self.table} is not registered."
22
+ end
23
+ hash_constraints = constraints(true)
24
+ klass.first_or_create(hash_constraints, resource_attrs)
25
+ end
26
+
27
+ # Supporting the `save_all` method to be used in scope chaining with queries.
28
+ # @!visibility private
29
+ def save_all(expressions = {}, &block)
30
+ conditions(expressions)
31
+ klass = Parse::Model.find_class self.table
32
+ if klass.blank?
33
+ raise ArgumentError, "Parse model with class name #{self.table} is not registered."
34
+ end
35
+ hash_constraints = constraints(true)
36
+
37
+ klass.save_all(hash_constraints, &block) if block_given?
38
+ klass.save_all(hash_constraints)
39
+ end
40
+ end
41
+
42
+ # A Parse::RelationAction is special operation that adds one object to a relational
43
+ # table as to another. Depending on the polarity of the action, the objects are
44
+ # either added or removed from the relation. This class is used to generate the proper
45
+ # hash request formats Parse needs in order to modify relational information for classes.
46
+ class RelationAction
47
+ # @!visibility private
48
+ ADD = "AddRelation"
49
+ # @!visibility private
50
+ REMOVE = "RemoveRelation"
51
+ # @!attribute polarity
52
+ # @return [Boolean] whether it is an addition (true) or removal (false) action.
53
+ # @!attribute key
54
+ # @return [String] the name of the Parse field (column).
55
+ # @!attribute objects
56
+ # @return [Array<Parse::Object>] the set of objects in this relation action.
57
+ attr_accessor :polarity, :key, :objects
58
+
59
+ # @param field [String] the name of the Parse field tied to this relation.
60
+ # @param polarity [Boolean] whether this is an addition (true) or removal (false) action.
61
+ # @param objects [Array<Parse::Object>] the set of objects tied to this relation action.
62
+ def initialize(field, polarity: true, objects: [])
63
+ @key = field.to_s
64
+ self.polarity = polarity
65
+ @objects = Array.wrap(objects).compact
66
+ end
67
+
68
+ # @return [Hash] a hash representing a relation operation.
69
+ def as_json(*args)
70
+ { @key => {
71
+ "__op" => (@polarity == true ? ADD : REMOVE),
72
+ "objects" => objects.parse_pointers,
73
+ } }.as_json
74
+ end
75
+ end
76
+ end
77
+
78
+ # This module is mainly all the basic orm operations. To support batching actions,
79
+ # we use temporary Request objects have contain the operation to be performed (in some cases).
80
+ # This allows to group a list of Request methods, into a batch for sending all at once to Parse.
81
+ module Parse
82
+
83
+ # An error raised when a save failure occurs.
84
+ class RecordNotSaved < StandardError
85
+ # @return [Parse::Object] the Parse::Object that failed to save.
86
+ attr_reader :object
87
+
88
+ # @param object [Parse::Object] the object that failed.
89
+ def initialize(object)
90
+ @object = object
91
+ end
92
+ end
93
+
94
+ module Core
95
+ # Defines some of the save, update and destroy operations for Parse objects.
96
+ module Actions
97
+ # @!visibility private
98
+ def self.included(base)
99
+ base.extend(ClassMethods)
100
+ # Per-class override for the synchronize-create lock. `nil` means
101
+ # "inherit from `Parse.synchronize_create_default`". Set to true/false
102
+ # on a subclass to force-on or force-off for that class and its
103
+ # descendants. Use ActiveSupport::class_attribute so inheritance works
104
+ # naturally; mirrors `signup_on_save` (user.rb:178) and `field_guards`
105
+ # (field_guards.rb:55).
106
+ if base.respond_to?(:class_attribute)
107
+ base.class_attribute :synchronize_create_default, instance_writer: false
108
+ base.synchronize_create_default = nil
109
+ end
110
+ end
111
+
112
+ # Class methods applied to Parse::Object subclasses.
113
+ module ClassMethods
114
+
115
+ # Execute a set of operations as an atomic transaction.
116
+ # All operations will be executed in sequence, and if any fail,
117
+ # the entire transaction will be rolled back.
118
+ #
119
+ # @example Basic transaction
120
+ # Parse::Object.transaction do |batch|
121
+ # user = User.first
122
+ # user.username = "new_username"
123
+ # batch.add(user)
124
+ #
125
+ # post = Post.new(author: user, title: "New Post")
126
+ # batch.add(post)
127
+ # end
128
+ #
129
+ # @example Using the block return for automatic batching
130
+ # results = Parse::Object.transaction do
131
+ # user1 = User.first
132
+ # user1.score = 100
133
+ #
134
+ # user2 = User.first(username: "player2")
135
+ # user2.score = 200
136
+ #
137
+ # [user1, user2] # Return array of objects to save
138
+ # end
139
+ #
140
+ # @param retries [Integer] number of times to retry on transaction conflict (error 251)
141
+ # @yield [Parse::BatchOperation] the batch operation to add requests to
142
+ # @return [Array<Parse::Response>] the responses from the transaction
143
+ # @raise [Parse::Error] if the transaction fails
144
+ def transaction(retries: 5, &block)
145
+ raise ArgumentError, "Block required for transaction" unless block_given?
146
+
147
+ batch = Parse::BatchOperation.new(nil, transaction: true)
148
+
149
+ # Store original state of objects for rollback
150
+ original_states = {}
151
+ tracked_objects = []
152
+
153
+ # Wrap the batch to capture objects being added
154
+ batch_wrapper = Object.new
155
+ batch_wrapper.define_singleton_method(:is_a?) do |klass|
156
+ klass == Parse::BatchOperation || super(klass)
157
+ end
158
+ batch_wrapper.define_singleton_method(:kind_of?) do |klass|
159
+ klass == Parse::BatchOperation || super(klass)
160
+ end
161
+ batch_wrapper.define_singleton_method(:instance_of?) do |klass|
162
+ klass == Parse::BatchOperation
163
+ end
164
+ batch_wrapper.define_singleton_method(:add) do |obj|
165
+ # Store original state when object is first added to transaction.
166
+ # Use obj.object_id (Ruby identity) as the key because Parse::Object#hash
167
+ # and #eql? treat all unsaved objects (nil id) as equal, which would cause
168
+ # only the first unsaved object to be tracked.
169
+ if obj.respond_to?(:attributes) && obj.respond_to?(:id) && !original_states.key?(obj.object_id)
170
+ original_states[obj.object_id] = {
171
+ object: obj,
172
+ attributes: obj.attributes.dup,
173
+ changed_attributes: obj.instance_variable_get(:@changed_attributes)&.dup || {},
174
+ id: obj.id,
175
+ mutations_from_database: obj.instance_variable_get(:@mutations_from_database),
176
+ mutations_before_last_save: obj.instance_variable_get(:@mutations_before_last_save),
177
+ }
178
+ tracked_objects << obj
179
+ end
180
+ batch.add(obj)
181
+ end
182
+
183
+ # Forward other methods to the real batch
184
+ batch_wrapper.define_singleton_method(:method_missing) do |method, *args, &block|
185
+ batch.send(method, *args, &block)
186
+ end
187
+
188
+ result = yield(batch_wrapper)
189
+
190
+ # If block returns objects, add them to batch
191
+ if result.respond_to?(:change_requests)
192
+ batch_wrapper.add(result)
193
+ elsif result.is_a?(Array)
194
+ result.each { |obj| batch_wrapper.add(obj) if obj.respond_to?(:change_requests) }
195
+ end
196
+
197
+ # Submit with retry logic for transaction conflicts
198
+ attempts = 0
199
+ begin
200
+ attempts += 1
201
+ responses = batch.submit
202
+
203
+ # Check for success
204
+ if responses.all?(&:success?)
205
+ # Update tracked objects with data from successful responses
206
+ # Match responses to objects using the request tag (Ruby object_id)
207
+ # Build hash lookup once for O(n) instead of O(n²) linear search
208
+ objects_by_id = tracked_objects.each_with_object({}) { |o, h| h[o.object_id] = o }
209
+ requests = batch.requests
210
+ requests.zip(responses).each do |request, response|
211
+ next unless request && response && response.success?
212
+ result = response.result
213
+ next unless result.is_a?(Hash)
214
+
215
+ # Find the object matching this request's tag
216
+ obj = objects_by_id[request.tag]
217
+ next unless obj
218
+
219
+ # Update object with response data (objectId, createdAt, updatedAt)
220
+ if result["objectId"]
221
+ obj.instance_variable_set(:@id, result["objectId"])
222
+ end
223
+ if result["createdAt"]
224
+ obj.instance_variable_set(:@created_at, Parse::Date.parse(result["createdAt"]))
225
+ end
226
+ if result["updatedAt"]
227
+ obj.instance_variable_set(:@updated_at, Parse::Date.parse(result["updatedAt"]))
228
+ elsif result["createdAt"]
229
+ obj.instance_variable_set(:@updated_at, Parse::Date.parse(result["createdAt"]))
230
+ end
231
+
232
+ # Apply any additional attributes returned by beforeSave hooks
233
+ obj.set_attributes!(result) if obj.respond_to?(:set_attributes!)
234
+
235
+ # Clear change tracking since save was successful
236
+ obj.send(:clear_changes!) if obj.respond_to?(:clear_changes!, true)
237
+ end
238
+
239
+ return responses
240
+ else
241
+ # Find first error
242
+ error_response = responses.find { |r| !r.success? }
243
+
244
+ # Rollback local object states
245
+ original_states.each_value do |state|
246
+ obj = state[:object]
247
+ obj.instance_variable_set(:@attributes, state[:attributes])
248
+ obj.instance_variable_set(:@changed_attributes, state[:changed_attributes])
249
+ obj.instance_variable_set(:@id, state[:id])
250
+ # Restore change tracking state
251
+ obj.instance_variable_set(:@mutations_from_database, state[:mutations_from_database])
252
+ obj.instance_variable_set(:@mutations_before_last_save, state[:mutations_before_last_save])
253
+ end
254
+
255
+ raise Parse::Error, "Transaction failed: #{error_response.error}"
256
+ end
257
+ rescue Parse::Error => e
258
+ # Retry on transaction conflict (error code 251)
259
+ if e.message.include?("251") && attempts < retries
260
+ sleep(0.1 * attempts) # Exponential backoff
261
+ retry
262
+ end
263
+
264
+ # Rollback local object states on final failure
265
+ original_states.each_value do |state|
266
+ obj = state[:object]
267
+ obj.instance_variable_set(:@attributes, state[:attributes])
268
+ obj.instance_variable_set(:@changed_attributes, state[:changed_attributes])
269
+ obj.instance_variable_set(:@id, state[:id])
270
+ # Restore change tracking state
271
+ obj.instance_variable_set(:@mutations_from_database, state[:mutations_from_database])
272
+ obj.instance_variable_set(:@mutations_before_last_save, state[:mutations_before_last_save])
273
+ end
274
+
275
+ raise e
276
+ end
277
+ end
278
+
279
+ # @!attribute raise_on_save_failure
280
+ # By default, we return `true` or `false` for save and destroy operations.
281
+ # If you prefer to have `Parse::Object` raise an exception instead, you
282
+ # can tell to do so either globally or on a per-model basis. When a save
283
+ # fails, it will raise a {Parse::RecordNotSaved}.
284
+ #
285
+ # When enabled, if an error is returned by Parse due to saving or
286
+ # destroying a record, due to your `before_save` or `before_delete`
287
+ # validation cloud code triggers, `Parse::Object` will return the a
288
+ # {Parse::RecordNotSaved} exception type. This exception has an instance
289
+ # method of `#object` which contains the object that failed to save.
290
+ # @example
291
+ # # globally across all models
292
+ # Parse::Model.raise_on_save_failure = true
293
+ # Song.raise_on_save_failure = true # per-model
294
+ #
295
+ # # or per-instance raise on failure
296
+ # song.save!
297
+ #
298
+ # @return [Boolean] whether to raise a {Parse::RecordNotSaved}
299
+ # when an object fails to save.
300
+ attr_writer :raise_on_save_failure
301
+
302
+ def raise_on_save_failure
303
+ return @raise_on_save_failure unless @raise_on_save_failure.nil?
304
+ Parse::Model.raise_on_save_failure
305
+ end
306
+
307
+ # Finds the first object matching the query conditions, or creates a new
308
+ # unsaved object with the attributes. This method takes the possibility of two hashes,
309
+ # therefore make sure you properly wrap the contents of the input with `{}`.
310
+ # @example
311
+ # Parse::User.first_or_create({ ..query conditions..})
312
+ # Parse::User.first_or_create({ ..query conditions..}, {.. resource_attrs ..})
313
+ # @param query_attrs [Hash] a set of query constraints that also are applied.
314
+ # @param resource_attrs [Hash] a set of additional attribute values to be applied only if an object was not found.
315
+ # @return [Parse::Object] a Parse::Object, whether found by the query or newly created.
316
+ def first_or_create(query_attrs = {}, resource_attrs = {})
317
+ query_attrs = query_attrs.symbolize_keys
318
+ resource_attrs = resource_attrs.symbolize_keys
319
+ obj = query(query_attrs).first
320
+
321
+ if obj.blank?
322
+ # Object not found, create new one with query_attrs + resource_attrs
323
+ merged_attrs = query_attrs.merge(resource_attrs)
324
+ obj = self.new merged_attrs
325
+ end
326
+ # If object exists, return it as-is without any modifications
327
+
328
+ obj
329
+ end
330
+
331
+ # Finds the first object matching the query conditions, or creates a new
332
+ # *saved* object with the attributes. This method is similar to {first_or_create}
333
+ # but will also {save!} the object if it was newly created.
334
+ #
335
+ # When `synchronize:` is enabled (per-call, per-class via
336
+ # `synchronize_create_default`, or globally via
337
+ # `Parse.synchronize_create_default`), the find→create→save sequence is
338
+ # serialized through {Parse::CreateLock} so concurrent callers
339
+ # with identical `query_attrs` cannot both create. The lock requires a
340
+ # Moneta cache store (Redis recommended); on a process-local store the
341
+ # lock degrades to a per-key Mutex. A MongoDB unique index on the
342
+ # constrained fields is the correctness floor — on Parse code 137
343
+ # (DuplicateValue) the wrapper re-queries inside the held lock and
344
+ # returns the winner.
345
+ #
346
+ # @example
347
+ # obj = Parse::User.first_or_create!({ ..query conditions..})
348
+ # obj = Parse::User.first_or_create!({ ..query conditions..}, {.. resource_attrs ..})
349
+ # @example Per-call lock opt-in
350
+ # User.first_or_create!({ email: e }, { name: n }, synchronize: true)
351
+ # @example Per-call with tuning
352
+ # User.first_or_create!({ email: e }, {}, synchronize: { ttl: 5, wait: 1.0 })
353
+ # @example Auth-context threading
354
+ # User.first_or_create!({ email: e }, {}, session: current_user.session_token)
355
+ # @param query_attrs [Hash] a set of query constraints that also are applied.
356
+ # @param resource_attrs [Hash] a set of attribute values to be applied if an object was not found.
357
+ # @param synchronize [Boolean, Hash, nil] override the synchronize-create lock. `nil` (default) defers
358
+ # to the per-class `synchronize_create_default` or the module-level `Parse.synchronize_create_default`.
359
+ # `true` enables with defaults; `false` opts out; a Hash enables with custom options merged over
360
+ # `Parse.synchronize_create_options`.
361
+ # @param session [String, Parse::User, nil] session token (or object answering :session_token) threaded
362
+ # through both the query and the save so the entire find→create flow runs under one auth identity.
363
+ # @param master_key [Boolean, nil] when explicitly `false`, disables master key for both halves.
364
+ # @return [Parse::Object] a Parse::Object, whether found by the query or newly created.
365
+ # @raise {Parse::RecordNotSaved} if the save fails
366
+ # @raise {Parse::CreateLockTimeoutError} when synchronized and the wait budget is exceeded
367
+ # @raise {Parse::CreateLockInvalidKey} when query_attrs cannot be canonicalized for a stable lock key
368
+ # @see #first_or_create
369
+ def first_or_create!(query_attrs = {}, resource_attrs = {}, synchronize: nil, session: nil, master_key: nil)
370
+ query_attrs = query_attrs.symbolize_keys
371
+ resource_attrs = resource_attrs.symbolize_keys
372
+
373
+ enabled, sync_opts = _resolve_synchronize_flag(synchronize)
374
+ return _first_or_create_unsynchronized!(query_attrs, resource_attrs, session: session, master_key: master_key) unless enabled
375
+
376
+ _assert_synchronize_class_allowed!
377
+ options = _merged_synchronize_options(sync_opts)
378
+ session_token = _extract_session_token(session)
379
+
380
+ # Split query_attrs into the constraint subset (what
381
+ # determines lock identity) and the query-shape options
382
+ # (`:cache`, `:limit`, `:order`, ACL helpers, …) that
383
+ # `Parse::Query#conditions` absorbs as query parameters.
384
+ # Without this, a caller passing the documented `cache:
385
+ # 30.seconds` escape hatch alongside their constraints
386
+ # tripped `Parse::CreateLock.canonicalize_value` on the
387
+ # `ActiveSupport::Duration` — see 4.4.2 changelog. The
388
+ # original `query_attrs` is still forwarded to
389
+ # `_scoped_first` below; `conditions()` extracts the option
390
+ # keys on the find side, so the cache TTL still applies.
391
+ lock_attrs = query_attrs.reject { |k, _| Parse::Query.option_key?(k) }
392
+ _assert_lock_attrs_have_constraints!(query_attrs, lock_attrs)
393
+
394
+ Parse::CreateLock.synchronize(
395
+ parse_class: parse_class,
396
+ query_attrs: lock_attrs,
397
+ options: options,
398
+ session_token: session_token,
399
+ master_key: master_key,
400
+ ) do
401
+ obj = _scoped_first(query_attrs, session: session, master_key: master_key)
402
+ next obj if obj
403
+
404
+ obj = self.new query_attrs.merge(resource_attrs)
405
+ begin
406
+ session ? obj.save!(session: session) : obj.save!
407
+ obj
408
+ rescue Parse::RecordNotSaved => e
409
+ winner = _recover_from_duplicate_value(e, query_attrs, session: session, master_key: master_key)
410
+ raise unless winner
411
+ winner
412
+ end
413
+ end
414
+ end
415
+
416
+ # Creates a new object with the given attributes and saves it.
417
+ # This is equivalent to calling `new(attrs).save!`.
418
+ # @example
419
+ # song = Song.create!(title: "New Song", artist: "Artist")
420
+ # @param attrs [Hash] the attributes for the new object.
421
+ # @return [Parse::Object] the newly created and saved object.
422
+ # @raise {Parse::RecordNotSaved} if the save fails
423
+ def create!(attrs = {})
424
+ obj = new(attrs)
425
+ obj.save!
426
+ obj
427
+ end
428
+
429
+ # Finds the first object matching the query conditions and updates it with the attributes,
430
+ # or creates a new *saved* object with the attributes. Saves new objects or existing objects with changes.
431
+ # See {#first_or_create!} for the synchronize-create lock semantics — they apply identically here.
432
+ # @example
433
+ # Parse::User.create_or_update!({ ..query conditions..}, {.. resource_attrs ..})
434
+ # @param query_attrs [Hash] a set of query constraints that also are applied.
435
+ # @param resource_attrs [Hash] a set of attribute values to be applied to found objects or used for creation.
436
+ # @param synchronize (see #first_or_create!)
437
+ # @param session (see #first_or_create!)
438
+ # @param master_key (see #first_or_create!)
439
+ # @return [Parse::Object] a Parse::Object, whether found by the query or newly created.
440
+ # @raise {Parse::RecordNotSaved} if the save fails
441
+ def create_or_update!(query_attrs = {}, resource_attrs = {}, synchronize: nil, session: nil, master_key: nil)
442
+ query_attrs = query_attrs.symbolize_keys
443
+ resource_attrs = resource_attrs.symbolize_keys
444
+
445
+ enabled, sync_opts = _resolve_synchronize_flag(synchronize)
446
+ return _create_or_update_unsynchronized!(query_attrs, resource_attrs, session: session, master_key: master_key) unless enabled
447
+
448
+ _assert_synchronize_class_allowed!
449
+ options = _merged_synchronize_options(sync_opts)
450
+ session_token = _extract_session_token(session)
451
+
452
+ # See #first_or_create! for the partition rationale — strip
453
+ # Parse::Query option keys before lock canonicalization.
454
+ lock_attrs = query_attrs.reject { |k, _| Parse::Query.option_key?(k) }
455
+ _assert_lock_attrs_have_constraints!(query_attrs, lock_attrs)
456
+
457
+ Parse::CreateLock.synchronize(
458
+ parse_class: parse_class,
459
+ query_attrs: lock_attrs,
460
+ options: options,
461
+ session_token: session_token,
462
+ master_key: master_key,
463
+ ) do
464
+ obj = _scoped_first(query_attrs, session: session, master_key: master_key)
465
+
466
+ if obj.nil?
467
+ obj = self.new query_attrs.merge(resource_attrs)
468
+ begin
469
+ session ? obj.save!(session: session) : obj.save!
470
+ rescue Parse::RecordNotSaved => e
471
+ winner = _recover_from_duplicate_value(e, query_attrs, session: session, master_key: master_key)
472
+ raise unless winner
473
+ obj = winner
474
+ end
475
+ end
476
+
477
+ if !obj.new? && !resource_attrs.empty?
478
+ has_changes = resource_attrs.any? do |key, value|
479
+ obj.respond_to?(key) && obj.send(key) != value
480
+ end
481
+ if has_changes
482
+ obj.apply_attributes!(resource_attrs, dirty_track: true)
483
+ session ? obj.save!(session: session) : obj.save!
484
+ end
485
+ end
486
+
487
+ obj
488
+ end
489
+ end
490
+
491
+ # @!visibility private
492
+ # Resolves the per-call synchronize kwarg against per-class and module
493
+ # defaults. Returns [enabled?, options_hash].
494
+ #
495
+ # Precedence (most specific wins):
496
+ # per-call true/false → per-class default → Parse.synchronize_create_default
497
+ # A Hash kwarg implies `true` with custom options. `nil` defers up the chain.
498
+ def _resolve_synchronize_flag(synchronize)
499
+ case synchronize
500
+ when true
501
+ [true, {}]
502
+ when false
503
+ [false, {}]
504
+ when Hash
505
+ [true, synchronize]
506
+ when nil
507
+ cls_default = respond_to?(:synchronize_create_default) ? synchronize_create_default : nil
508
+ case cls_default
509
+ when true
510
+ [true, {}]
511
+ when false
512
+ [false, {}]
513
+ when Hash
514
+ [true, cls_default]
515
+ else
516
+ [Parse.synchronize_create_default ? true : false, {}]
517
+ end
518
+ else
519
+ raise ArgumentError, "synchronize: must be true, false, nil, or an options Hash (got #{synchronize.class})"
520
+ end
521
+ end
522
+
523
+ # @!visibility private
524
+ def _merged_synchronize_options(per_call)
525
+ base = Parse.synchronize_create_options || {}
526
+ base.merge(per_call || {})
527
+ end
528
+
529
+ # @!visibility private
530
+ # Enforce {Parse.synchronize_classes} allowlist. Inheritance is
531
+ # **transitive** for Class entries (`self <= entry`): allowlisting
532
+ # `User` automatically allowlists every subclass of `User`. To gate
533
+ # per-class without inheritance, pass entries as Strings — they
534
+ # match only `self.name` / `parse_class` literally. See the
535
+ # `Parse.synchronize_classes` docstring in lib/parse/stack.rb.
536
+ def _assert_synchronize_class_allowed!
537
+ allowlist = Parse.synchronize_classes
538
+ return if allowlist.nil? || allowlist.empty?
539
+ allowed = allowlist.any? do |entry|
540
+ (entry.is_a?(Class) && self <= entry) ||
541
+ entry.to_s == self.name ||
542
+ entry.to_s == parse_class
543
+ end
544
+ return if allowed
545
+ raise Parse::CreateLockUnavailableError,
546
+ "#{self} is not in Parse.synchronize_classes allowlist; either add it or pass synchronize: false"
547
+ end
548
+
549
+ # @!visibility private
550
+ # Confirm that after partitioning query_attrs into
551
+ # constraints + query-shape options, at least one constraint
552
+ # remained. If not, raise a specific error explaining the
553
+ # likely mistake before `Parse::CreateLock.synchronize` does
554
+ # — its generic "non-empty query_attrs" message would mislead
555
+ # the caller who can see a non-empty `query_attrs` argument
556
+ # right there in their code.
557
+ #
558
+ # Two distinguished cases:
559
+ # - `query_attrs` was empty to begin with → generic empty
560
+ # error (the user really did pass nothing).
561
+ # - `query_attrs` was non-empty but every key was a query
562
+ # option (`:cache`, `:limit`, …) → specific error naming the
563
+ # partitioned-out keys so the user can fix their call.
564
+ def _assert_lock_attrs_have_constraints!(query_attrs, lock_attrs)
565
+ return unless lock_attrs.empty?
566
+ if query_attrs.empty?
567
+ raise Parse::CreateLockInvalidKey,
568
+ "synchronize requires at least one constraint key in query_attrs (got an empty Hash)"
569
+ end
570
+ option_keys = query_attrs.keys.select { |k| Parse::Query.option_key?(k) }
571
+ raise Parse::CreateLockInvalidKey,
572
+ "synchronize requires at least one constraint key in query_attrs; " \
573
+ "every key passed (#{option_keys.inspect}) is a Parse::Query option " \
574
+ "(`:cache`, `:limit`, `:order`, ACL helpers, …) and is partitioned " \
575
+ "out of the lock identity. Add a constraint key (e.g. the unique " \
576
+ "field your callsite is finding-or-creating against), or pass " \
577
+ "`synchronize: false` if you don't need cross-process locking."
578
+ end
579
+
580
+ # @!visibility private
581
+ # Extract a session token string from either a String or an object
582
+ # answering :session_token (e.g. Parse::User, Parse::Session). Returns
583
+ # nil when session is nil so the canonical lock key picks the
584
+ # "default" auth-context marker.
585
+ def _extract_session_token(session)
586
+ return nil if session.nil?
587
+ return session if session.is_a?(String)
588
+ return session.session_token if session.respond_to?(:session_token)
589
+ raise ArgumentError, "session: must be a String token or an object responding to :session_token (got #{session.class})"
590
+ end
591
+
592
+ # @!visibility private
593
+ # Run `query(constraints).first` with explicit auth scoping so the
594
+ # synchronized find runs under the same identity as the subsequent
595
+ # save. When session/master_key are unset, falls through to the
596
+ # client default exactly as the legacy non-synchronized path.
597
+ def _scoped_first(query_attrs, session: nil, master_key: nil)
598
+ q = query(query_attrs)
599
+ if session
600
+ q.session_token = session.respond_to?(:session_token) ? session.session_token : session
601
+ end
602
+ q.use_master_key = master_key unless master_key.nil?
603
+ q.first
604
+ end
605
+
606
+ # @!visibility private
607
+ # When a save inside the lock fails with Parse code 137 (DuplicateValue),
608
+ # re-query inside the still-held lock and return the row that won the
609
+ # race. Returns nil when the error was something other than 137 or the
610
+ # winner cannot be located. The caller raises the original exception in
611
+ # the nil case.
612
+ def _recover_from_duplicate_value(error, query_attrs, session: nil, master_key: nil)
613
+ obj = error.respond_to?(:object) ? error.object : nil
614
+ return nil unless obj
615
+ res = obj.instance_variable_get(:@_last_response)
616
+ return nil unless res && res.respond_to?(:code) && res.code == Parse::Client::DuplicateValueError::CODE
617
+ _scoped_first(query_attrs, session: session, master_key: master_key)
618
+ end
619
+
620
+ # @!visibility private
621
+ # The pre-synchronize behavior of `first_or_create!`, factored out so
622
+ # the synchronize wrapper can short-circuit when disabled. Preserves
623
+ # the legacy contract: query → build → save! if new.
624
+ def _first_or_create_unsynchronized!(query_attrs, resource_attrs, session: nil, master_key: nil)
625
+ obj = _scoped_first(query_attrs, session: session, master_key: master_key)
626
+ if obj.nil?
627
+ obj = self.new query_attrs.merge(resource_attrs)
628
+ end
629
+ if obj.new?
630
+ session ? obj.save!(session: session) : obj.save!
631
+ end
632
+ obj
633
+ end
634
+
635
+ # @!visibility private
636
+ def _create_or_update_unsynchronized!(query_attrs, resource_attrs, session: nil, master_key: nil)
637
+ obj = _scoped_first(query_attrs, session: session, master_key: master_key)
638
+ if obj.nil?
639
+ obj = self.new query_attrs.merge(resource_attrs)
640
+ session ? obj.save!(session: session) : obj.save!
641
+ elsif !resource_attrs.empty?
642
+ has_changes = resource_attrs.any? do |key, value|
643
+ obj.respond_to?(key) && obj.send(key) != value
644
+ end
645
+ if has_changes
646
+ obj.apply_attributes!(resource_attrs, dirty_track: true)
647
+ session ? obj.save!(session: session) : obj.save!
648
+ end
649
+ end
650
+ obj
651
+ end
652
+
653
+ # Auto save all objects matching the query constraints. This method is
654
+ # meant to be used with a block. Any objects that are modified in the block
655
+ # will be batched for a save operation. This uses the `updated_at` field to
656
+ # continue to query for all matching objects that have not been updated.
657
+ # If you need to use `:updated_at` in your constraints, consider using {Parse::Core::Querying#all} or
658
+ # {Parse::Core::Querying#each}
659
+ # @param constraints [Hash] a set of query constraints.
660
+ # @yield a block which will iterate through each matching object.
661
+ # @example
662
+ #
663
+ # post = Post.first
664
+ # Comments.save_all( post: post) do |comment|
665
+ # # .. modify comment ...
666
+ # # it will automatically be saved
667
+ # end
668
+ # @note You cannot use *:updated_at* as a constraint.
669
+ # @return [Boolean] true if all saves succeeded and there were no errors.
670
+ def save_all(constraints = {}, &block)
671
+ invalid_constraints = constraints.keys.any? do |k|
672
+ (k == :updated_at || k == :updatedAt) ||
673
+ (k.is_a?(Parse::Operation) && (k.operand == :updated_at || k.operand == :updatedAt))
674
+ end
675
+ if invalid_constraints
676
+ raise ArgumentError,
677
+ "[#{self}] Special method save_all() cannot be used with an :updated_at constraint."
678
+ end
679
+
680
+ force = false
681
+ batch_size = 250
682
+ iterator_block = nil
683
+ if block_given?
684
+ iterator_block = block
685
+ force ||= false
686
+ else
687
+ # if no block given, assume you want to just save all objects
688
+ # regardless of modification.
689
+ force = true
690
+ end
691
+ # Only generate the comparison block once.
692
+ # updated_comparison_block = Proc.new { |x| x.updated_at }
693
+
694
+ anchor_date = Parse::Date.now
695
+ constraints.merge! :updated_at.on_or_before => anchor_date
696
+ constraints.merge! cache: false
697
+ # oldest first, so we create a reduction-cycle
698
+ constraints.merge! order: :updated_at.asc, limit: batch_size
699
+ update_query = query(constraints)
700
+ #puts "Setting Anchor Date: #{anchor_date}"
701
+ cursor = nil
702
+ has_errors = false
703
+ loop do
704
+ results = update_query.results
705
+
706
+ break if results.empty?
707
+
708
+ # verify we didn't get duplicates fetches
709
+ if cursor.is_a?(Parse::Object) && results.any? { |x| x.id == cursor.id }
710
+ warn "[#{self}.save_all] Unbounded update detected with id #{cursor.id}."
711
+ has_errors = true
712
+ break cursor
713
+ end
714
+
715
+ results.each(&iterator_block) if iterator_block.present?
716
+ # we don't need to refresh the objects in the array with the results
717
+ # since we will be throwing them away. Force determines whether
718
+ # to save these objects regardless of whether they are dirty.
719
+ batch = results.save(merge: false, force: force)
720
+
721
+ # faster version assuming sorting order wasn't messed up
722
+ cursor = results.last
723
+ # slower version, but more accurate
724
+ # cursor_item = results.max_by(&updated_comparison_block).updated_at
725
+ # puts "[Parse::SaveAll] Updated #{results.count} records updated <= #{cursor.updated_at}"
726
+
727
+ break if results.count < batch_size # we didn't hit a cap on results.
728
+ if cursor.is_a?(Parse::Object)
729
+ update_query.where :updated_at.gte => cursor.updated_at
730
+
731
+ if cursor.updated_at.present? && cursor.updated_at > anchor_date
732
+ warn "[#{self}.save_all] Reached anchor date #{anchor_date} < #{cursor.updated_at}"
733
+ break cursor
734
+ end
735
+ end
736
+
737
+ has_errors ||= batch.error?
738
+ end
739
+ not has_errors
740
+ end
741
+ end # ClassMethods
742
+
743
+ # Perform an atomic operation on this field. This operation is done on the
744
+ # Parse server which guarantees the atomicity of the operation. This is the low-level
745
+ # API on performing atomic operations on properties for classes. These methods do not
746
+ # update the current instance with any changes the server may have made to satisfy this
747
+ # operation.
748
+ #
749
+ # @param field [String] the name of the field in the Parse collection.
750
+ # @param op_hash [Hash] The operation hash. It may also be of type {Parse::RelationAction}.
751
+ # @return [Boolean] whether the operation was successful.
752
+ def operate_field!(field, op_hash)
753
+ field = field.to_sym
754
+ field = self.field_map[field] || field
755
+ if op_hash.is_a?(Parse::RelationAction)
756
+ op_hash = op_hash.as_json
757
+ else
758
+ op_hash = { field => op_hash }.as_json
759
+ end
760
+
761
+ # If the object hasn't been saved yet (no id), we can't make field operations
762
+ # Return true to indicate the operation was "successful" locally
763
+ return true if id.nil?
764
+
765
+ response = client.update_object(parse_class, id, op_hash, session_token: _session_token)
766
+ if response.error?
767
+ puts "[#{parse_class}:#{field} Operation] #{response.error}"
768
+ end
769
+ response.success?
770
+ end
771
+
772
+ # Perform an atomic add operation to the array field.
773
+ # @param field [String] the name of the field in the Parse collection.
774
+ # @param objects [Array] the set of items to add to this field.
775
+ # @return [Boolean] whether it was successful
776
+ # @see #operate_field!
777
+ def op_add!(field, objects)
778
+ operate_field! field, { __op: :Add, objects: objects }
779
+ end
780
+
781
+ # Perform an atomic add unique operation to the array field. The objects will
782
+ # only be added if they don't already exists in the array for that particular field.
783
+ # @param field [String] the name of the field in the Parse collection.
784
+ # @param objects [Array] the set of items to add uniquely to this field.
785
+ # @return [Boolean] whether it was successful
786
+ # @see #operate_field!
787
+ def op_add_unique!(field, objects)
788
+ operate_field! field, { __op: :AddUnique, objects: objects }
789
+ end
790
+
791
+ # Perform an atomic remove operation to the array field.
792
+ # @param field [String] the name of the field in the Parse collection.
793
+ # @param objects [Array] the set of items to remove to this field.
794
+ # @return [Boolean] whether it was successful
795
+ # @see #operate_field!
796
+ def op_remove!(field, objects)
797
+ operate_field! field, { __op: :Remove, objects: objects }
798
+ end
799
+
800
+ # Perform an atomic delete operation on this field.
801
+ # @param field [String] the name of the field in the Parse collection.
802
+ # @return [Boolean] whether it was successful
803
+ # @see #operate_field!
804
+ def op_destroy!(field)
805
+ result = operate_field! field, { __op: :Delete }.freeze
806
+ if result
807
+ # Also update the local state to reflect the deletion
808
+ field_sym = field.to_sym
809
+ if self.class.fields[field_sym].present?
810
+ set_attribute_method = "#{field}_set_attribute!"
811
+ if respond_to?(set_attribute_method)
812
+ send(set_attribute_method, nil, true) # Set to nil with dirty tracking
813
+ else
814
+ instance_variable_set(:"@#{field}", nil)
815
+ send("#{field}_will_change!") if respond_to?("#{field}_will_change!")
816
+ end
817
+ end
818
+ end
819
+ result
820
+ end
821
+
822
+ # Perform an atomic add operation on this relational field.
823
+ # @param field [String] the name of the field in the Parse collection.
824
+ # @param objects [Array<Parse::Object>] the set of objects to add to this relational field.
825
+ # @return [Boolean] whether it was successful
826
+ # @see #operate_field!
827
+ def op_add_relation!(field, objects = [])
828
+ objects = [objects] unless objects.is_a?(Array)
829
+ return false if objects.empty?
830
+ relation_action = Parse::RelationAction.new(field, polarity: true, objects: objects)
831
+ operate_field! field, relation_action
832
+ end
833
+
834
+ # Perform an atomic remove operation on this relational field.
835
+ # @param field [String] the name of the field in the Parse collection.
836
+ # @param objects [Array<Parse::Object>] the set of objects to remove to this relational field.
837
+ # @return [Boolean] whether it was successful
838
+ # @see #operate_field!
839
+ def op_remove_relation!(field, objects = [])
840
+ objects = [objects] unless objects.is_a?(Array)
841
+ return false if objects.empty?
842
+ relation_action = Parse::RelationAction.new(field, polarity: false, objects: objects)
843
+ operate_field! field, relation_action
844
+ end
845
+
846
+ # Atomically increment or decrement a specific field.
847
+ # @param field [String] the name of the field in the Parse collection.
848
+ # @param amount [Integer] the amoun to increment. Use negative values to decrement.
849
+ # @see #operate_field!
850
+ def op_increment!(field, amount = 1)
851
+ unless amount.is_a?(Numeric)
852
+ raise ArgumentError, "Amount should be numeric"
853
+ end
854
+ result = operate_field! field, { __op: :Increment, amount: amount.to_i }.freeze
855
+ if result
856
+ # Also update the local state to reflect the increment
857
+ field_sym = field.to_sym
858
+ current_value = self[field_sym] || 0
859
+ new_value = current_value + amount.to_i
860
+ set_attribute_method = "#{field}_set_attribute!"
861
+ if respond_to?(set_attribute_method)
862
+ send(set_attribute_method, new_value, true) # Set new value with dirty tracking
863
+ else
864
+ self[field_sym] = new_value
865
+ end
866
+ end
867
+ result
868
+ end
869
+
870
+ # @return [Parse::Request] a destroy_request for the current object.
871
+ def destroy_request
872
+ return nil unless @id.present?
873
+ uri = self.uri_path
874
+ r = Request.new(:delete, uri)
875
+ r.tag = object_id
876
+ r
877
+ end
878
+
879
+ # @return [String] the API uri path for this class.
880
+ def uri_path
881
+ self.client.url_prefix.path + Client.uri_path(self)
882
+ end
883
+
884
+ # Creates an array of all possible operations that need to be performed
885
+ # on this object. This includes all property and relational operation changes.
886
+ # @param force [Boolean] whether this object should be saved even if does not have
887
+ # pending changes.
888
+ # @return [Array<Parse::Request>] the list of API requests.
889
+ def change_requests(force = false)
890
+ requests = []
891
+ # get the URI path for this object.
892
+ uri = self.uri_path
893
+
894
+ # generate the request to update the object (PUT)
895
+ if attribute_changes? || force
896
+ # if it's new, then we should call :post for creating the object.
897
+ method = new? ? :post : :put
898
+ r = Request.new(method, uri, body: attribute_updates)
899
+ r.tag = object_id
900
+ requests << r
901
+ end
902
+
903
+ # if the object is not new, then we can also add all the relational changes
904
+ # we need to perform.
905
+ if @id.present? && relation_changes?
906
+ relation_change_operations.each do |ops|
907
+ next if ops.empty?
908
+ r = Request.new(:put, uri, body: ops)
909
+ r.tag = object_id
910
+ requests << r
911
+ end
912
+ end
913
+ requests
914
+ end
915
+
916
+ # This methods sends an update request for this object with the any change
917
+ # information based on its local attributes. The bang implies that it will send
918
+ # the request even though it is possible no changes were performed. This is useful
919
+ # in kicking-off an beforeSave / afterSave hooks
920
+ # Save the object regardless of whether there are changes. This would call
921
+ # any beforeSave and afterSave cloud code hooks you have registered for this class.
922
+ # @return [Boolean] true/false whether it was successful.
923
+ def update!(raw: false, force: false)
924
+ if valid? == false
925
+ errors.full_messages.each do |msg|
926
+ warn "[#{parse_class}] warning: #{msg}"
927
+ end
928
+ end
929
+ if force == true && attribute_changes?.blank? && !new?
930
+ # if we are forcing an update, but there are no attribute changes,
931
+ # we should still mark the updated_at field as changed so that
932
+ # the server updates it.
933
+ if self.class.fields[:updated_at].present?
934
+ self.updated_at = Time.now.utc
935
+ self.updated_at_will_change! if respond_to?(:updated_at_will_change!)
936
+ end
937
+ end
938
+ response = client.update_object(parse_class, id, attribute_updates, session_token: _session_token)
939
+ @_last_response = response
940
+ if response.success?
941
+ result = response.result
942
+ # Because beforeSave hooks can change the fields we are saving, any items that were
943
+ # changed, are returned to us and we should apply those locally to be in sync.
944
+ set_attributes!(result)
945
+ end
946
+ puts "Error updating #{self.parse_class}: #{response.error}" if response.error?
947
+ return response if raw
948
+ response.success?
949
+ end
950
+
951
+ # Save all the changes related to this object.
952
+ # @param force [Boolean] whether to send the update even if there are no changes.
953
+ # @return [Boolean] true/false whether it was successful.
954
+ def update(force: false)
955
+ return true unless attribute_changes? || force
956
+ update!(force: force)
957
+ end
958
+
959
+ # Internal method to perform update with :update callbacks.
960
+ # Called from save() for existing objects.
961
+ # @param force [Boolean] whether to send the update even if there are no changes.
962
+ # @return [Boolean] true/false whether it was successful.
963
+ # @!visibility private
964
+ def perform_update(force: false)
965
+ return true unless attribute_changes? || force
966
+ run_callbacks :update do
967
+ update!(force: force)
968
+ end
969
+ end
970
+
971
+ # Save the object as a new record, running all callbacks.
972
+ # @return [Boolean] true/false whether it was successful.
973
+ def create
974
+ run_callbacks :create do
975
+ body = attribute_updates
976
+ # Forward a client-assigned objectId when a `before_create` callback
977
+ # set it (e.g. `parse_reference precompute: true`). attribute_updates
978
+ # excludes BASE_KEYS, so @id must be merged explicitly. Parse Server
979
+ # accepts an objectId in the create POST body and rejects duplicates
980
+ # with a typed error rather than silently overwriting.
981
+ body[Parse::Model::OBJECT_ID] = @id if @id.present?
982
+ res = client.create_object(parse_class, body, session_token: _session_token)
983
+ # Retain the response so wrappers (e.g. synchronize_create) can
984
+ # inspect the Parse error code on failure (notably 137 DuplicateValue).
985
+ @_last_response = res
986
+ unless res.error?
987
+ result = res.result
988
+ @id = result[Parse::Model::OBJECT_ID] || @id
989
+ @created_at = result["createdAt"] || @created_at
990
+ #if the object is created, updatedAt == createdAt
991
+ @updated_at = result["updatedAt"] || result["createdAt"] || @updated_at
992
+ # Because beforeSave hooks can change the fields we are saving, any items that were
993
+ # changed, are returned to us and we should apply those locally to be in sync.
994
+ set_attributes!(result)
995
+ end
996
+ puts "Error creating #{self.parse_class}: #{res.error}" if res.error?
997
+ res.success?
998
+ end
999
+ end
1000
+
1001
+ # @!visibility private
1002
+ def _session_token
1003
+ if @_session_token.respond_to?(:session_token)
1004
+ @_session_token = @_session_token.session_token
1005
+ end
1006
+ @_session_token
1007
+ end
1008
+
1009
+ # @!visibility private
1010
+ def _validate_session_token!(token, action = :save)
1011
+ return nil if token.nil? # user explicitly requests no session token
1012
+ token = token.session_token if token.respond_to?(:session_token)
1013
+ return token if token.is_a?(String) && token.present?
1014
+ raise ArgumentError, "#{self.class}##{action} error: Invalid session token passed (#{token})"
1015
+ end
1016
+
1017
+ # saves the object. If the object has not changed, it is a noop. If it is new,
1018
+ # we will create the object. If the object has an id, we will update the record.
1019
+ #
1020
+ # You may pass a session token to the `session` argument to perform this actions
1021
+ # with the privileges of a certain user.
1022
+ #
1023
+ # Callback order:
1024
+ # 1. before_validation / around_validation / after_validation
1025
+ # 2. before_save / around_save
1026
+ # 3. before_create or before_update / around_create or around_update
1027
+ # 4. [actual save operation]
1028
+ # 5. after_create or after_update
1029
+ # 6. after_save
1030
+ #
1031
+ # You can define before and after :save callbacks
1032
+ # autoraise: set to true will automatically raise an exception if the save fails
1033
+ # @raise {Parse::RecordNotSaved} if the save fails
1034
+ # @raise ArgumentError if a non-nil value is passed to `session` that doesn't provide a session token string.
1035
+ # @param session [String] a session token in order to apply ACLs to this operation.
1036
+ # @param autoraise [Boolean] whether to raise an exception if the save fails.
1037
+ # @param force [Boolean] whether to run callbacks and send request even if there are no changes.
1038
+ # @param validate [Boolean] whether to run validations (default: true).
1039
+ # @return [Boolean] whether the save was successful.
1040
+ def save(session: nil, autoraise: false, force: false, validate: true)
1041
+ # Prevent saving objects that have been fetched and found to be deleted
1042
+ if _deleted?
1043
+ error_msg = "Cannot save deleted object. Object with id '#{@id}' no longer exists on the server."
1044
+ raise Parse::Error::ProtocolError, error_msg
1045
+ end
1046
+
1047
+ @_session_token = _validate_session_token! session, :save
1048
+ return true unless changed? || force
1049
+
1050
+ # Run validations (validation callbacks are now triggered by valid? method)
1051
+ # Pass context so `on: :create` and `on: :update` options work with callbacks
1052
+ if validate
1053
+ validation_context = new? ? :create : :update
1054
+ validation_passed = valid?(validation_context)
1055
+
1056
+ unless validation_passed
1057
+ if self.class.raise_on_save_failure || autoraise.present?
1058
+ raise Parse::RecordNotSaved.new(self), "Validation failed: #{errors.full_messages.join(", ")}"
1059
+ end
1060
+ return false
1061
+ end
1062
+ end
1063
+
1064
+ success = false
1065
+
1066
+ # Track if callbacks are halted by a before_save hook returning false
1067
+ callback_executed = false
1068
+ run_callbacks :save do
1069
+ callback_executed = true
1070
+ #first process the create/update action if any
1071
+ #then perform any relation changes that need to be performed
1072
+ success = new? ? create : perform_update(force: force)
1073
+
1074
+ # if the save was successful and we have relational changes
1075
+ # let's update send those next.
1076
+ if success
1077
+ if relation_changes?
1078
+ # get the list of changed keys
1079
+ changed_attribute_keys = changed - relations.keys.map(&:to_s)
1080
+ clear_attribute_changes(changed_attribute_keys)
1081
+ success = update_relations
1082
+ if success
1083
+ changes_applied!
1084
+ clear_partial_fetch_state!
1085
+ elsif self.class.raise_on_save_failure || autoraise.present?
1086
+ raise Parse::RecordNotSaved.new(self), "Failed updating relations. #{self.parse_class} partially saved."
1087
+ end
1088
+ else
1089
+ changes_applied!
1090
+ clear_partial_fetch_state!
1091
+ end
1092
+ elsif self.class.raise_on_save_failure || autoraise.present?
1093
+ raise Parse::RecordNotSaved.new(self), "Failed to create or save attributes. #{self.parse_class} was not saved."
1094
+ end
1095
+ end #callbacks
1096
+
1097
+ # If callbacks were halted (before_save returned false), return false
1098
+ return false unless callback_executed
1099
+
1100
+ @_session_token = nil
1101
+ success
1102
+ end
1103
+
1104
+ # Save this object and raise an exception if it fails.
1105
+ # @raise {Parse::RecordNotSaved} if the save fails
1106
+ # @raise ArgumentError if a non-nil value is passed to `session` that doesn't provide a session token string.
1107
+ # @param session (see #save)
1108
+ # @param force (see #save)
1109
+ # @return (see #save)
1110
+ def save!(session: nil, force: false)
1111
+ save(autoraise: true, session: session, force: force)
1112
+ end
1113
+
1114
+ # Returns true if this object has been fetched and found to be deleted from the server.
1115
+ # Deleted objects cannot be saved.
1116
+ # @return [Boolean] true if the object is marked as deleted
1117
+ def _deleted?
1118
+ @_deleted == true
1119
+ end
1120
+
1121
+ # Delete this record from the Parse collection. Only valid if this object has an `id`.
1122
+ # This will run all the `destroy` callbacks.
1123
+ # @param session [String] a session token if you want to apply ACLs for a user in this operation.
1124
+ # @raise ArgumentError if a non-nil value is passed to `session` that doesn't provide a session token string.
1125
+ # @return [Boolean] whether the operation was successful.
1126
+ def destroy(session: nil)
1127
+ @_session_token = _validate_session_token! session, :destroy
1128
+ return false if new?
1129
+ success = false
1130
+ run_callbacks :destroy do
1131
+ res = client.delete_object parse_class, id, session_token: _session_token
1132
+ success = res.success?
1133
+ if success
1134
+ @id = nil
1135
+ changes_applied!
1136
+ elsif self.class.raise_on_save_failure
1137
+ raise Parse::RecordNotSaved.new(self), "Failed to create or save attributes. #{self.parse_class} was not saved."
1138
+ end
1139
+ # Your create action methods here
1140
+ end
1141
+ @_session_token = nil
1142
+ success
1143
+ end
1144
+
1145
+ # Runs all the registered `before_save` related callbacks.
1146
+ def prepare_save!
1147
+ # With terminator configured, run_callbacks will return false if any callback returns false
1148
+ # We track if the block executes to know if callbacks were halted
1149
+ callback_success = false
1150
+ run_callbacks(:save) do
1151
+ callback_success = true
1152
+ true
1153
+ end
1154
+ callback_success
1155
+ end
1156
+
1157
+ # @return [Hash] a hash of the list of changes made to this instance.
1158
+ def changes_payload
1159
+ h = attribute_updates
1160
+ if relation_changes?
1161
+ r = relation_change_operations.select { |s| s.present? }.first
1162
+ h.merge!(r) if r.present?
1163
+ end
1164
+ #h.merge!(className: parse_class) unless h.empty?
1165
+ h.as_json
1166
+ end
1167
+
1168
+ alias_method :update_payload, :changes_payload
1169
+
1170
+ # Generates an array with two entries for addition and removal operations. The first entry
1171
+ # of the array will contain a hash of all the change operations regarding adding new relational
1172
+ # objects. The second entry in the array is a hash of all the change operations regarding removing
1173
+ # relation objects from this field.
1174
+ # @return [Array] an array with two hashes; the first is a hash of all the addition operations and
1175
+ # the second hash, all the remove operations.
1176
+ def relation_change_operations
1177
+ return [{}, {}] unless relation_changes?
1178
+
1179
+ additions = []
1180
+ removals = []
1181
+ # go through all the additions of a collection and generate an action to add.
1182
+ relation_updates.each do |field, collection|
1183
+ if collection.additions.count > 0
1184
+ additions.push Parse::RelationAction.new(field, objects: collection.additions, polarity: true)
1185
+ end
1186
+ # go through all the additions of a collection and generate an action to remove.
1187
+ if collection.removals.count > 0
1188
+ removals.push Parse::RelationAction.new(field, objects: collection.removals, polarity: false)
1189
+ end
1190
+ end
1191
+ # merge all additions and removals into one large hash
1192
+ additions = additions.reduce({}) { |m, v| m.merge! v.as_json }
1193
+ removals = removals.reduce({}) { |m, v| m.merge! v.as_json }
1194
+ [additions, removals]
1195
+ end
1196
+
1197
+ # Saves and updates all the relational changes for made to this object.
1198
+ # @return [Boolean] whether all the save or update requests were successful.
1199
+ def update_relations
1200
+ # relational saves require an id
1201
+ return false unless @id.present?
1202
+ # verify we have relational changes before we do work.
1203
+ return true unless relation_changes?
1204
+ raise "Unable to update relations for a new object." if new?
1205
+ # get all the relational changes (both additions and removals)
1206
+ additions, removals = relation_change_operations
1207
+
1208
+ responses = []
1209
+ # Send parallel Parse requests for each of the items to update.
1210
+ # since we will have multiple responses, we will track it in array
1211
+ [removals, additions].threaded_each do |ops|
1212
+ next if ops.empty? #if no operations to be performed, then we are done
1213
+ responses << client.update_object(parse_class, @id, ops, session_token: _session_token)
1214
+ end
1215
+ # check if any of them ended up in error
1216
+ has_error = responses.any? { |response| response.error? }
1217
+ # if everything was ok, find the last response to be returned and update
1218
+ #their fields in case beforeSave made any changes.
1219
+ unless has_error || responses.empty?
1220
+ result = responses.last.result #last result to come back
1221
+ set_attributes!(result)
1222
+ end #unless
1223
+ has_error == false
1224
+ end
1225
+
1226
+ # Performs mass assignment using a hash with the ability to modify dirty tracking.
1227
+ # This is an internal method used to set properties on the object while controlling
1228
+ # whether they are dirty tracked. Each defined property has a method defined with the
1229
+ # suffix `_set_attribute!` that can will be called if it is contained in the hash.
1230
+ # @example
1231
+ # object.set_attributes!( {"myField" => value}, false)
1232
+ #
1233
+ # # equivalent to calling the specific method.
1234
+ # object.myField_set_attribute!(value, false)
1235
+ # @param hash [Hash] the hash containing all the attribute names and values.
1236
+ # @param dirty_track [Boolean] whether the assignment should be tracked in the change tracking
1237
+ # system.
1238
+ # @return [Hash]
1239
+ def set_attributes!(hash, dirty_track = false)
1240
+ return unless hash.is_a?(Hash)
1241
+ hash.each do |k, v|
1242
+ next if k == Parse::Model::OBJECT_ID || k == Parse::Model::ID
1243
+ method = "#{k}_set_attribute!"
1244
+ send(method, v, dirty_track) if respond_to?(method)
1245
+ end
1246
+ end
1247
+
1248
+ # Clears changes information on all collections (array and relations) and all
1249
+ # local attributes.
1250
+ def changes_applied!
1251
+ # find all fields that are of type :array
1252
+ fields(:array) do |key, v|
1253
+ proxy = send(key)
1254
+ # clear changes
1255
+ proxy.changes_applied! if proxy.respond_to?(:changes_applied!)
1256
+ end
1257
+
1258
+ # for all relational fields,
1259
+ relations.each do |key, v|
1260
+ proxy = send(key)
1261
+ # clear changes if they support the method.
1262
+ proxy.changes_applied! if proxy.respond_to?(:changes_applied!)
1263
+ end
1264
+ changes_applied
1265
+ end
1266
+ end
1267
+ end
1268
+ end