parse-stack 1.5.3 → 1.6.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 (55) hide show
  1. checksums.yaml +4 -4
  2. data/.github/parse-ruby-sdk.png +0 -0
  3. data/Changes.md +25 -1
  4. data/Gemfile.lock +4 -4
  5. data/README.md +37 -31
  6. data/bin/console +3 -0
  7. data/lib/parse/api/all.rb +2 -1
  8. data/lib/parse/api/apps.rb +12 -0
  9. data/lib/parse/api/config.rb +5 -1
  10. data/lib/parse/api/files.rb +1 -0
  11. data/lib/parse/api/hooks.rb +1 -0
  12. data/lib/parse/api/objects.rb +4 -1
  13. data/lib/parse/api/push.rb +1 -0
  14. data/lib/parse/api/{schemas.rb → schema.rb} +7 -0
  15. data/lib/parse/api/server.rb +44 -0
  16. data/lib/parse/api/sessions.rb +1 -0
  17. data/lib/parse/api/users.rb +4 -1
  18. data/lib/parse/client.rb +109 -73
  19. data/lib/parse/client/authentication.rb +2 -1
  20. data/lib/parse/client/batch.rb +9 -1
  21. data/lib/parse/client/body_builder.rb +16 -1
  22. data/lib/parse/client/caching.rb +15 -13
  23. data/lib/parse/client/protocol.rb +27 -15
  24. data/lib/parse/client/response.rb +26 -8
  25. data/lib/parse/model/acl.rb +1 -1
  26. data/lib/parse/model/associations/belongs_to.rb +18 -19
  27. data/lib/parse/model/associations/collection_proxy.rb +6 -0
  28. data/lib/parse/model/associations/has_many.rb +5 -6
  29. data/lib/parse/model/bytes.rb +4 -1
  30. data/lib/parse/model/classes/user.rb +46 -44
  31. data/lib/parse/model/core/actions.rb +508 -460
  32. data/lib/parse/model/core/builder.rb +75 -0
  33. data/lib/parse/model/core/errors.rb +9 -0
  34. data/lib/parse/model/core/fetching.rb +42 -38
  35. data/lib/parse/model/core/properties.rb +46 -27
  36. data/lib/parse/model/core/querying.rb +231 -228
  37. data/lib/parse/model/core/schema.rb +76 -74
  38. data/lib/parse/model/date.rb +10 -2
  39. data/lib/parse/model/file.rb +16 -2
  40. data/lib/parse/model/geopoint.rb +9 -2
  41. data/lib/parse/model/model.rb +38 -7
  42. data/lib/parse/model/object.rb +60 -19
  43. data/lib/parse/model/pointer.rb +22 -1
  44. data/lib/parse/model/push.rb +6 -2
  45. data/lib/parse/query.rb +57 -11
  46. data/lib/parse/query/constraint.rb +5 -2
  47. data/lib/parse/query/constraints.rb +588 -589
  48. data/lib/parse/query/ordering.rb +2 -2
  49. data/lib/parse/stack.rb +1 -0
  50. data/lib/parse/stack/version.rb +1 -1
  51. data/lib/parse/webhooks.rb +30 -29
  52. data/lib/parse/webhooks/payload.rb +181 -168
  53. data/lib/parse/webhooks/registration.rb +1 -1
  54. data/parse-stack.gemspec +9 -9
  55. metadata +9 -12
@@ -10,13 +10,56 @@ require 'parallel'
10
10
  require_relative '../../client/request'
11
11
  require_relative 'fetching'
12
12
 
13
+
13
14
  module Parse
15
+
16
+ class Query
17
+
18
+
19
+ # Supporting the `all` class method to be used in scope chaining with queries.
20
+ # @!visibility private
21
+ def all(expressions = {limit: :max})
22
+ conditions(expressions)
23
+ return results(&Proc.new) if block_given?
24
+ results
25
+ end
26
+
27
+ # Supporting the `first_or_create` class method to be used in scope chaining with queries.
28
+ # @!visibility private
29
+ def first_or_create(query_attrs = {}, resource_attrs = {})
30
+ conditions(query_attrs)
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
+ klass.first_or_create(hash_constraints, resource_attrs)
37
+ end
38
+
39
+ # Supporting the `save_all` method to be used in scope chaining with queries.
40
+ # @!visibility private
41
+ def save_all(expressions = {})
42
+ conditions(expressions)
43
+ klass = Parse::Model.find_class self.table
44
+ if klass.blank?
45
+ raise ArgumentError, "Parse model with class name #{self.table} is not registered."
46
+ end
47
+ hash_constraints = constraints(true)
48
+
49
+ klass.save_all(hash_constraints, &Proc.new) if block_given?
50
+ klass.save_all(hash_constraints)
51
+ end
52
+
53
+ end
54
+
14
55
  # A Parse::RelationAction is special operation that adds one object to a relational
15
56
  # table as to another. Depending on the polarity of the action, the objects are
16
57
  # either added or removed from the relation. This class is used to generate the proper
17
58
  # hash request formats Parse needs in order to modify relational information for classes.
18
59
  class RelationAction
60
+ # @!visibility private
19
61
  ADD = "AddRelation"
62
+ # @!visibility private
20
63
  REMOVE = "RemoveRelation"
21
64
  # @!attribute polarity
22
65
  # @return [Boolean] whether it is an addition (true) or removal (false) action.
@@ -55,7 +98,7 @@ end
55
98
  module Parse
56
99
 
57
100
  # An error raised when a save failure occurs.
58
- class SaveFailureError < StandardError
101
+ class RecordNotSaved < StandardError
59
102
  # @return [Parse::Object] the Parse::Object that failed to save.
60
103
  attr_reader :object
61
104
 
@@ -65,533 +108,538 @@ module Parse
65
108
  end
66
109
  end
67
110
 
68
- # Defines some of the save, update and destroy operations for Parse objects.
69
- module Actions
70
- # @!visibility private
71
- def self.included(base)
72
- base.extend(ClassMethods)
73
- end
74
-
75
- # Class methods applied to Parse::Object subclasses.
76
- module ClassMethods
77
- # @!attribute raise_on_save_failure
78
- # By default, we return `true` or `false` for save and destroy operations.
79
- # If you prefer to have `Parse::Object` raise an exception instead, you
80
- # can tell to do so either globally or on a per-model basis. When a save
81
- # fails, it will raise a {Parse::SaveFailureError}.
82
- #
83
- # When enabled, if an error is returned by Parse due to saving or
84
- # destroying a record, due to your `before_save` or `before_delete`
85
- # validation cloud code triggers, `Parse::Object` will return the a
86
- # {Parse::SaveFailureError} exception type. This exception has an instance
87
- # method of `#object` which contains the object that failed to save.
88
- # @example
89
- # # globally across all models
90
- # Parse::Model.raise_on_save_failure = true
91
- # Song.raise_on_save_failure = true # per-model
92
- #
93
- # # or per-instance raise on failure
94
- # song.save!
95
- #
96
- # @return [Boolean] whether to raise a {Parse::SaveFailureError}
97
- # when an object fails to save.
98
- attr_accessor :raise_on_save_failure
99
-
100
- def raise_on_save_failure
101
- return @raise_on_save_failure unless @raise_on_save_failure.nil?
102
- Parse::Model.raise_on_save_failure
111
+ module Core
112
+ # Defines some of the save, update and destroy operations for Parse objects.
113
+ module Actions
114
+ # @!visibility private
115
+ def self.included(base)
116
+ base.extend(ClassMethods)
103
117
  end
104
118
 
105
- # Finds the first object matching the query conditions, or creates a new
106
- # unsaved object with the attributes.
107
- # the provided query attributes, otherwise create
108
- # @param query_attrs [Hash] a set of query constraints that also are applied.
109
- # @param resource_attrs [Hash] a set of attribute values to be applied if an object was not found.
110
- # @return [Parse::Object] a Parse::Object, whether found by the query or newly created.
111
- def first_or_create(query_attrs = {}, resource_attrs = {})
112
-
113
- query_attrs.symbolize_keys!
114
- resource_attrs.symbolize_keys!
115
- obj = query(query_attrs).first
116
-
117
- if obj.blank?
118
- obj = self.new query_attrs
119
- obj.apply_attributes!(resource_attrs, dirty_track: false)
119
+ # Class methods applied to Parse::Object subclasses.
120
+ module ClassMethods
121
+ # @!attribute raise_on_save_failure
122
+ # By default, we return `true` or `false` for save and destroy operations.
123
+ # If you prefer to have `Parse::Object` raise an exception instead, you
124
+ # can tell to do so either globally or on a per-model basis. When a save
125
+ # fails, it will raise a {Parse::RecordNotSaved}.
126
+ #
127
+ # When enabled, if an error is returned by Parse due to saving or
128
+ # destroying a record, due to your `before_save` or `before_delete`
129
+ # validation cloud code triggers, `Parse::Object` will return the a
130
+ # {Parse::RecordNotSaved} exception type. This exception has an instance
131
+ # method of `#object` which contains the object that failed to save.
132
+ # @example
133
+ # # globally across all models
134
+ # Parse::Model.raise_on_save_failure = true
135
+ # Song.raise_on_save_failure = true # per-model
136
+ #
137
+ # # or per-instance raise on failure
138
+ # song.save!
139
+ #
140
+ # @return [Boolean] whether to raise a {Parse::RecordNotSaved}
141
+ # when an object fails to save.
142
+ attr_accessor :raise_on_save_failure
143
+
144
+ def raise_on_save_failure
145
+ return @raise_on_save_failure unless @raise_on_save_failure.nil?
146
+ Parse::Model.raise_on_save_failure
120
147
  end
121
- obj.save if obj.new? && Parse::Model.autosave_on_create
122
- obj
123
- end
124
148
 
125
- # Auto save all objects matching the query constraints. This method is
126
- # meant to be used with a block. Any objects that are modified in the block
127
- # will be batched for a save operation. This uses the `updated_at` field to
128
- # continue to query for all matching objects that have not been updated.
129
- # @param constraints [Hash] a set of query constraints.
130
- # @yield a block which will iterate through each matching object.
131
- # @example
132
- #
133
- # post = Post.first
134
- # Comments.save_all( post: post) do |comment|
135
- # # .. modify comment ...
136
- # # it will automatically be saved
137
- # end
138
- # @return [Boolean] whether there were any errors.
139
- def save_all(constraints = {})
140
- force = false
141
-
142
- iterator_block = nil
143
- if block_given?
144
- iterator_block = Proc.new
145
- force ||= false
146
- else
147
- # if no block given, assume you want to just save all objects
148
- # regardless of modification.
149
- force = true
149
+ # Finds the first object matching the query conditions, or creates a new
150
+ # unsaved object with the attributes. This method takes the possibility of two hashes,
151
+ # therefore make sure you properly wrap the contents of the input with `{}`.
152
+ # Parse::User.first_or_create({ ..query conditions..})
153
+ # Parse::User.first_or_create({ ..query conditions..}, {.. resrouce_attrs ..})
154
+ # @param query_attrs [Hash] a set of query constraints that also are applied.
155
+ # @param resource_attrs [Hash] a set of attribute values to be applied if an object was not found.
156
+ # @return [Parse::Object] a Parse::Object, whether found by the query or newly created.
157
+ # @see Parse::Model.autosave_on_create
158
+ def first_or_create(query_attrs = {}, resource_attrs = {})
159
+
160
+ query_attrs.symbolize_keys!
161
+ resource_attrs.symbolize_keys!
162
+ obj = query(query_attrs).first
163
+
164
+ if obj.blank?
165
+ obj = self.new query_attrs
166
+ obj.apply_attributes!(resource_attrs, dirty_track: false)
167
+ end
168
+ obj.save if obj.new? && Parse::Model.autosave_on_create
169
+ obj
150
170
  end
151
- # Only generate the comparison block once.
152
- # updated_comparison_block = Proc.new { |x| x.updated_at }
153
-
154
- anchor_date = Parse::Date.now
155
- constraints.merge! :updated_at.on_or_before => anchor_date
156
- constraints.merge! cache: false
157
- # oldest first, so we create a reduction-cycle
158
- constraints.merge! order: :updated_at.asc, limit: 100
159
- update_query = query(constraints)
160
- #puts "Setting Anchor Date: #{anchor_date}"
161
- cursor = nil
162
- has_errors = false
163
- loop do
164
- results = update_query.results
165
-
166
- break if results.empty?
167
-
168
- # verify we didn't get duplicates fetches
169
- if cursor.is_a?(Parse::Object) && results.any? { |x| x.id == cursor.id }
170
- warn "[Parse::SaveAll] Unbounded update detected with id #{cursor.id}."
171
- has_errors = true
172
- break cursor
171
+
172
+ # Auto save all objects matching the query constraints. This method is
173
+ # meant to be used with a block. Any objects that are modified in the block
174
+ # will be batched for a save operation. This uses the `updated_at` field to
175
+ # continue to query for all matching objects that have not been updated.
176
+ # @param constraints [Hash] a set of query constraints.
177
+ # @yield a block which will iterate through each matching object.
178
+ # @example
179
+ #
180
+ # post = Post.first
181
+ # Comments.save_all( post: post) do |comment|
182
+ # # .. modify comment ...
183
+ # # it will automatically be saved
184
+ # end
185
+ # @return [Boolean] whether there were any errors.
186
+ def save_all(constraints = {})
187
+ force = false
188
+
189
+ iterator_block = nil
190
+ if block_given?
191
+ iterator_block = Proc.new
192
+ force ||= false
193
+ else
194
+ # if no block given, assume you want to just save all objects
195
+ # regardless of modification.
196
+ force = true
173
197
  end
198
+ # Only generate the comparison block once.
199
+ # updated_comparison_block = Proc.new { |x| x.updated_at }
200
+
201
+ anchor_date = Parse::Date.now
202
+ constraints.merge! :updated_at.on_or_before => anchor_date
203
+ constraints.merge! cache: false
204
+ # oldest first, so we create a reduction-cycle
205
+ constraints.merge! order: :updated_at.asc, limit: 100
206
+ update_query = query(constraints)
207
+ #puts "Setting Anchor Date: #{anchor_date}"
208
+ cursor = nil
209
+ has_errors = false
210
+ loop do
211
+ results = update_query.results
212
+
213
+ break if results.empty?
214
+
215
+ # verify we didn't get duplicates fetches
216
+ if cursor.is_a?(Parse::Object) && results.any? { |x| x.id == cursor.id }
217
+ warn "[Parse::SaveAll] Unbounded update detected with id #{cursor.id}."
218
+ has_errors = true
219
+ break cursor
220
+ end
174
221
 
175
- results.each(&iterator_block) if iterator_block.present?
176
- # we don't need to refresh the objects in the array with the results
177
- # since we will be throwing them away. Force determines whether
178
- # to save these objects regardless of whether they are dirty.
179
- batch = results.save(merge: false, force: force)
222
+ results.each(&iterator_block) if iterator_block.present?
223
+ # we don't need to refresh the objects in the array with the results
224
+ # since we will be throwing them away. Force determines whether
225
+ # to save these objects regardless of whether they are dirty.
226
+ batch = results.save(merge: false, force: force)
180
227
 
181
- # faster version assuming sorting order wasn't messed up
182
- cursor = results.last
183
- # slower version, but more accurate
184
- # cursor_item = results.max_by(&updated_comparison_block).updated_at
185
- # puts "[Parse::SaveAll] Updated #{results.count} records updated <= #{cursor.updated_at}"
228
+ # faster version assuming sorting order wasn't messed up
229
+ cursor = results.last
230
+ # slower version, but more accurate
231
+ # cursor_item = results.max_by(&updated_comparison_block).updated_at
232
+ # puts "[Parse::SaveAll] Updated #{results.count} records updated <= #{cursor.updated_at}"
186
233
 
187
- if cursor.is_a?(Parse::Object)
188
- update_query.where :updated_at.gte => cursor.updated_at
234
+ if cursor.is_a?(Parse::Object)
235
+ update_query.where :updated_at.gte => cursor.updated_at
236
+
237
+ if cursor.updated_at.present? && cursor.updated_at > anchor_date
238
+ warn "[Parse::SaveAll] Reached anchor date #{anchor_date} < #{cursor.updated_at}"
239
+ break cursor
240
+ end
189
241
 
190
- if cursor.updated_at.present? && cursor.updated_at > anchor_date
191
- warn "[Parse::SaveAll] Reached anchor date #{anchor_date} < #{cursor.updated_at}"
192
- break cursor
193
242
  end
194
243
 
244
+ has_errors ||= batch.error?
195
245
  end
246
+ has_errors
247
+ end
248
+
249
+ end # ClassMethods
196
250
 
197
- has_errors ||= batch.error?
251
+ # Perform an atomic operation on this field. This operation is done on the
252
+ # Parse server which guarantees the atomicity of the operation. This is the low-level
253
+ # API on performing atomic operations on properties for classes. These methods do not
254
+ # update the current instance with any changes the server may have made to satisfy this
255
+ # operation.
256
+ #
257
+ # @param field [String] the name of the field in the Parse collection.
258
+ # @param op_hash [Hash] The operation hash. It may also be of type {Parse::RelationAction}.
259
+ # @return [Boolean] whether the operation was successful.
260
+ def operate_field!(field, op_hash)
261
+ field = field.to_sym
262
+ field = self.field_map[field] || field
263
+ if op_hash.is_a?(Parse::RelationAction)
264
+ op_hash = op_hash.as_json
265
+ else
266
+ op_hash = { field => op_hash }.as_json
198
267
  end
199
- has_errors
200
- end
201
268
 
202
- end # ClassMethods
203
-
204
- # Perform an atomic operation on this field. This operation is done on the
205
- # Parse server which guarantees the atomicity of the operation. This is the low-level
206
- # API on performing atomic operations on properties for classes. These methods do not
207
- # update the current instance with any changes the server may have made to satisfy this
208
- # operation.
209
- #
210
- # @param field [String] the name of the field in the Parse collection.
211
- # @param op_hash [Hash] The operation hash. It may also be of type {Parse::RelationAction}.
212
- # @return [Boolean] whether the operation was successful.
213
- def operate_field!(field, op_hash)
214
- field = field.to_sym
215
- field = self.field_map[field] || field
216
- if op_hash.is_a?(Parse::RelationAction)
217
- op_hash = op_hash.as_json
218
- else
219
- op_hash = { field => op_hash }.as_json
269
+ response = client.update_object(parse_class, id, op_hash, session_token: _session_token )
270
+ if response.error?
271
+ puts "[#{parse_class}:#{field} Operation] #{response.error}"
272
+ end
273
+ response.success?
220
274
  end
221
275
 
222
- response = client.update_object(parse_class, id, op_hash, session_token: _session_token )
223
- if response.error?
224
- puts "[#{parse_class}:#{field} Operation] #{response.error}"
276
+ # Perform an atomic add operation to the array field.
277
+ # @param field [String] the name of the field in the Parse collection.
278
+ # @param objects [Array] the set of items to add to this field.
279
+ # @return [Boolean] whether it was successful
280
+ # @see #operate_field!
281
+ def op_add!(field,objects)
282
+ operate_field! field, { __op: :Add, objects: objects }
225
283
  end
226
- response.success?
227
- end
228
-
229
- # Perform an atomic add operation to the array field.
230
- # @param field [String] the name of the field in the Parse collection.
231
- # @param objects [Array] the set of items to add to this field.
232
- # @return [Boolean] whether it was successful
233
- # @see #operate_field!
234
- def op_add!(field,objects)
235
- operate_field! field, { __op: :Add, objects: objects }
236
- end
237
-
238
- # Perform an atomic add unique operation to the array field. The objects will
239
- # only be added if they don't already exists in the array for that particular field.
240
- # @param field [String] the name of the field in the Parse collection.
241
- # @param objects [Array] the set of items to add uniquely to this field.
242
- # @return [Boolean] whether it was successful
243
- # @see #operate_field!
244
- def op_add_unique!(field,objects)
245
- operate_field! field, { __op: :AddUnique, objects: objects }
246
- end
247
-
248
- # Perform an atomic remove operation to the array field.
249
- # @param field [String] the name of the field in the Parse collection.
250
- # @param objects [Array] the set of items to remove to this field.
251
- # @return [Boolean] whether it was successful
252
- # @see #operate_field!
253
- def op_remove!(field, objects)
254
- operate_field! field, { __op: :Remove, objects: objects }
255
- end
256
284
 
257
- # Perform an atomic delete operation on this field.
258
- # @param field [String] the name of the field in the Parse collection.
259
- # @return [Boolean] whether it was successful
260
- # @see #operate_field!
261
- def op_destroy!(field)
262
- operate_field! field, { __op: :Delete }.freeze
263
- end
285
+ # Perform an atomic add unique operation to the array field. The objects will
286
+ # only be added if they don't already exists in the array for that particular field.
287
+ # @param field [String] the name of the field in the Parse collection.
288
+ # @param objects [Array] the set of items to add uniquely to this field.
289
+ # @return [Boolean] whether it was successful
290
+ # @see #operate_field!
291
+ def op_add_unique!(field,objects)
292
+ operate_field! field, { __op: :AddUnique, objects: objects }
293
+ end
264
294
 
265
- # Perform an atomic add operation on this relational field.
266
- # @param field [String] the name of the field in the Parse collection.
267
- # @param objects [Array<Parse::Object>] the set of objects to add to this relational field.
268
- # @return [Boolean] whether it was successful
269
- # @see #operate_field!
270
- def op_add_relation!(field, objects = [])
271
- objects = [objects] unless objects.is_a?(Array)
272
- return false if objects.empty?
273
- relation_action = Parse::RelationAction.new(field, polarity: true, objects: objects)
274
- operate_field! field, relation_action
275
- end
295
+ # Perform an atomic remove operation to the array field.
296
+ # @param field [String] the name of the field in the Parse collection.
297
+ # @param objects [Array] the set of items to remove to this field.
298
+ # @return [Boolean] whether it was successful
299
+ # @see #operate_field!
300
+ def op_remove!(field, objects)
301
+ operate_field! field, { __op: :Remove, objects: objects }
302
+ end
276
303
 
277
- # Perform an atomic remove operation on this relational field.
278
- # @param field [String] the name of the field in the Parse collection.
279
- # @param objects [Array<Parse::Object>] the set of objects to remove to this relational field.
280
- # @return [Boolean] whether it was successful
281
- # @see #operate_field!
282
- def op_remove_relation!(field, objects = [])
283
- objects = [objects] unless objects.is_a?(Array)
284
- return false if objects.empty?
285
- relation_action = Parse::RelationAction.new(field, polarity: false, objects: objects)
286
- operate_field! field, relation_action
287
- end
304
+ # Perform an atomic delete operation on this field.
305
+ # @param field [String] the name of the field in the Parse collection.
306
+ # @return [Boolean] whether it was successful
307
+ # @see #operate_field!
308
+ def op_destroy!(field)
309
+ operate_field! field, { __op: :Delete }.freeze
310
+ end
288
311
 
289
- # Atomically increment or decrement a specific field.
290
- # @param field [String] the name of the field in the Parse collection.
291
- # @param amount [Integer] the amoun to increment. Use negative values to decrement.
292
- # @see #operate_field!
293
- def op_increment!(field, amount = 1)
294
- unless amount.is_a?(Numeric)
295
- raise ArgumentError, "Amount should be numeric"
312
+ # Perform an atomic add operation on this relational field.
313
+ # @param field [String] the name of the field in the Parse collection.
314
+ # @param objects [Array<Parse::Object>] the set of objects to add to this relational field.
315
+ # @return [Boolean] whether it was successful
316
+ # @see #operate_field!
317
+ def op_add_relation!(field, objects = [])
318
+ objects = [objects] unless objects.is_a?(Array)
319
+ return false if objects.empty?
320
+ relation_action = Parse::RelationAction.new(field, polarity: true, objects: objects)
321
+ operate_field! field, relation_action
296
322
  end
297
- operate_field! field, { __op: :Increment, amount: amount.to_i }.freeze
298
- end
299
323
 
300
- # @return [Parse::Request] a destroy_request for the current object.
301
- def destroy_request
302
- return nil unless @id.present?
303
- uri = self.uri_path
304
- r = Request.new( :delete, uri )
305
- r.tag = object_id
306
- r
307
- end
324
+ # Perform an atomic remove operation on this relational field.
325
+ # @param field [String] the name of the field in the Parse collection.
326
+ # @param objects [Array<Parse::Object>] the set of objects to remove to this relational field.
327
+ # @return [Boolean] whether it was successful
328
+ # @see #operate_field!
329
+ def op_remove_relation!(field, objects = [])
330
+ objects = [objects] unless objects.is_a?(Array)
331
+ return false if objects.empty?
332
+ relation_action = Parse::RelationAction.new(field, polarity: false, objects: objects)
333
+ operate_field! field, relation_action
334
+ end
308
335
 
309
- # @return [String] the API uri path for this class.
310
- def uri_path
311
- self.client.url_prefix.path + Client.uri_path(self)
312
- end
336
+ # Atomically increment or decrement a specific field.
337
+ # @param field [String] the name of the field in the Parse collection.
338
+ # @param amount [Integer] the amoun to increment. Use negative values to decrement.
339
+ # @see #operate_field!
340
+ def op_increment!(field, amount = 1)
341
+ unless amount.is_a?(Numeric)
342
+ raise ArgumentError, "Amount should be numeric"
343
+ end
344
+ operate_field! field, { __op: :Increment, amount: amount.to_i }.freeze
345
+ end
313
346
 
314
- # Creates an array of all possible operations that need to be performed
315
- # on this object. This includes all property and relational operation changes.
316
- # @param force [Boolean] whether this object should be saved even if does not have
317
- # pending changes.
318
- # @return [Array<Parse::Request>] the list of API requests.
319
- def change_requests(force = false)
320
- requests = []
321
- # get the URI path for this object.
322
- uri = self.uri_path
323
-
324
- # generate the request to update the object (PUT)
325
- if attribute_changes? || force
326
- # if it's new, then we should call :post for creating the object.
327
- method = new? ? :post : :put
328
- r = Request.new( method, uri, body: attribute_updates)
347
+ # @return [Parse::Request] a destroy_request for the current object.
348
+ def destroy_request
349
+ return nil unless @id.present?
350
+ uri = self.uri_path
351
+ r = Request.new( :delete, uri )
329
352
  r.tag = object_id
330
- requests << r
353
+ r
354
+ end
355
+
356
+ # @return [String] the API uri path for this class.
357
+ def uri_path
358
+ self.client.url_prefix.path + Client.uri_path(self)
331
359
  end
332
360
 
333
- # if the object is not new, then we can also add all the relational changes
334
- # we need to perform.
335
- if @id.present? && relation_changes?
336
- relation_change_operations.each do |ops|
337
- next if ops.empty?
338
- r = Request.new( :put, uri, body: ops)
361
+ # Creates an array of all possible operations that need to be performed
362
+ # on this object. This includes all property and relational operation changes.
363
+ # @param force [Boolean] whether this object should be saved even if does not have
364
+ # pending changes.
365
+ # @return [Array<Parse::Request>] the list of API requests.
366
+ def change_requests(force = false)
367
+ requests = []
368
+ # get the URI path for this object.
369
+ uri = self.uri_path
370
+
371
+ # generate the request to update the object (PUT)
372
+ if attribute_changes? || force
373
+ # if it's new, then we should call :post for creating the object.
374
+ method = new? ? :post : :put
375
+ r = Request.new( method, uri, body: attribute_updates)
339
376
  r.tag = object_id
340
377
  requests << r
341
378
  end
342
- end
343
- requests
344
- end
345
379
 
346
- # This methods sends an update request for this object with the any change
347
- # information based on its local attributes. The bang implies that it will send
348
- # the request even though it is possible no changes were performed. This is useful
349
- # in kicking-off an beforeSave / afterSave hooks
350
- # Save the object regardless of whether there are changes. This would call
351
- # any beforeSave and afterSave cloud code hooks you have registered for this class.
352
- # @return [Boolean] true/false whether it was successful.
353
- def update!(raw: false)
354
- if valid? == false
355
- errors.full_messages.each do |msg|
356
- warn "[#{parse_class}] warning: #{msg}"
380
+ # if the object is not new, then we can also add all the relational changes
381
+ # we need to perform.
382
+ if @id.present? && relation_changes?
383
+ relation_change_operations.each do |ops|
384
+ next if ops.empty?
385
+ r = Request.new( :put, uri, body: ops)
386
+ r.tag = object_id
387
+ requests << r
388
+ end
357
389
  end
390
+ requests
358
391
  end
359
- response = client.update_object(parse_class, id, attribute_updates, session_token: _session_token)
360
- if response.success?
361
- result = response.result
362
- # Because beforeSave hooks can change the fields we are saving, any items that were
363
- # changed, are returned to us and we should apply those locally to be in sync.
364
- set_attributes!(result)
365
- end
366
- puts "Error updating #{self.parse_class}: #{response.error}" if response.error?
367
- return response if raw
368
- response.success?
369
- end
370
-
371
- # Save all the changes related to this object.
372
- # @return [Boolean] true/false whether it was successful.
373
- def update
374
- return true unless attribute_changes?
375
- update!
376
- end
377
392
 
378
- # Save the object as a new record, running all callbacks.
379
- # @return [Boolean] true/false whether it was successful.
380
- def create
381
- run_callbacks :create do
382
- res = client.create_object(parse_class, attribute_updates, session_token: _session_token)
383
- unless res.error?
384
- result = res.result
385
- @id = result[Parse::Model::OBJECT_ID] || @id
386
- @created_at = result["createdAt"] || @created_at
387
- #if the object is created, updatedAt == createdAt
388
- @updated_at = result["updatedAt"] || result["createdAt"] || @updated_at
393
+ # This methods sends an update request for this object with the any change
394
+ # information based on its local attributes. The bang implies that it will send
395
+ # the request even though it is possible no changes were performed. This is useful
396
+ # in kicking-off an beforeSave / afterSave hooks
397
+ # Save the object regardless of whether there are changes. This would call
398
+ # any beforeSave and afterSave cloud code hooks you have registered for this class.
399
+ # @return [Boolean] true/false whether it was successful.
400
+ def update!(raw: false)
401
+ if valid? == false
402
+ errors.full_messages.each do |msg|
403
+ warn "[#{parse_class}] warning: #{msg}"
404
+ end
405
+ end
406
+ response = client.update_object(parse_class, id, attribute_updates, session_token: _session_token)
407
+ if response.success?
408
+ result = response.result
389
409
  # Because beforeSave hooks can change the fields we are saving, any items that were
390
410
  # changed, are returned to us and we should apply those locally to be in sync.
391
411
  set_attributes!(result)
392
412
  end
393
- puts "Error creating #{self.parse_class}: #{res.error}" if res.error?
394
- res.success?
413
+ puts "Error updating #{self.parse_class}: #{response.error}" if response.error?
414
+ return response if raw
415
+ response.success?
395
416
  end
396
- end
397
417
 
398
- # @!visibility private
399
- def _session_token
400
- if @_session_token.respond_to?(:session_token)
401
- @_session_token = @_session_token.session_token
418
+ # Save all the changes related to this object.
419
+ # @return [Boolean] true/false whether it was successful.
420
+ def update
421
+ return true unless attribute_changes?
422
+ update!
423
+ end
424
+
425
+ # Save the object as a new record, running all callbacks.
426
+ # @return [Boolean] true/false whether it was successful.
427
+ def create
428
+ run_callbacks :create do
429
+ res = client.create_object(parse_class, attribute_updates, session_token: _session_token)
430
+ unless res.error?
431
+ result = res.result
432
+ @id = result[Parse::Model::OBJECT_ID] || @id
433
+ @created_at = result["createdAt"] || @created_at
434
+ #if the object is created, updatedAt == createdAt
435
+ @updated_at = result["updatedAt"] || result["createdAt"] || @updated_at
436
+ # Because beforeSave hooks can change the fields we are saving, any items that were
437
+ # changed, are returned to us and we should apply those locally to be in sync.
438
+ set_attributes!(result)
439
+ end
440
+ puts "Error creating #{self.parse_class}: #{res.error}" if res.error?
441
+ res.success?
442
+ end
402
443
  end
403
- @_session_token
404
- end
405
444
 
406
- # saves the object. If the object has not changed, it is a noop. If it is new,
407
- # we will create the object. If the object has an id, we will update the record.
408
- # You can define before and after :save callbacks
409
- # autoraise: set to true will automatically raise an exception if the save fails
410
- # @raise Parse::SaveFailureError if the save fails
411
- # @param autoraise [Boolean] whether to raise an exception if the save fails.
412
- # @param session [String] a session token in order to apply ACLs to this operation.
413
- # @return [Boolean] whether the save was successful.
414
- def save(autoraise: false, session: nil)
415
- return true unless changed?
416
- success = false
417
- @_session_token = session
418
- run_callbacks :save do
419
- #first process the create/update action if any
420
- #then perform any relation changes that need to be performed
421
- success = new? ? create : update
422
-
423
- # if the save was successful and we have relational changes
424
- # let's update send those next.
425
- if success
426
- if relation_changes?
427
- # get the list of changed keys
428
- changed_attribute_keys = changed - relations.keys.map(&:to_s)
429
- clear_attribute_changes( changed_attribute_keys )
430
- success = update_relations
431
- if success
445
+ # @!visibility private
446
+ def _session_token
447
+ if @_session_token.respond_to?(:session_token)
448
+ @_session_token = @_session_token.session_token
449
+ end
450
+ @_session_token
451
+ end
452
+
453
+ # saves the object. If the object has not changed, it is a noop. If it is new,
454
+ # we will create the object. If the object has an id, we will update the record.
455
+ # You can define before and after :save callbacks
456
+ # autoraise: set to true will automatically raise an exception if the save fails
457
+ # @raise Parse::RecordNotSaved if the save fails
458
+ # @param autoraise [Boolean] whether to raise an exception if the save fails.
459
+ # @param session [String] a session token in order to apply ACLs to this operation.
460
+ # @return [Boolean] whether the save was successful.
461
+ def save(autoraise: false, session: nil)
462
+ return true unless changed?
463
+ success = false
464
+ @_session_token = session
465
+ run_callbacks :save do
466
+ #first process the create/update action if any
467
+ #then perform any relation changes that need to be performed
468
+ success = new? ? create : update
469
+
470
+ # if the save was successful and we have relational changes
471
+ # let's update send those next.
472
+ if success
473
+ if relation_changes?
474
+ # get the list of changed keys
475
+ changed_attribute_keys = changed - relations.keys.map(&:to_s)
476
+ clear_attribute_changes( changed_attribute_keys )
477
+ success = update_relations
478
+ if success
479
+ changes_applied!
480
+ elsif self.class.raise_on_save_failure || autoraise.present?
481
+ raise Parse::RecordNotSaved.new(self), "Failed updating relations. #{self.parse_class} partially saved."
482
+ end
483
+ else
432
484
  changes_applied!
433
- elsif self.class.raise_on_save_failure || autoraise.present?
434
- raise Parse::SaveFailureError.new(self), "Failed updating relations. #{self.parse_class} partially saved."
435
485
  end
436
- else
437
- changes_applied!
486
+ elsif self.class.raise_on_save_failure || autoraise.present?
487
+ raise Parse::RecordNotSaved.new(self), "Failed to create or save attributes. #{self.parse_class} was not saved."
438
488
  end
439
- elsif self.class.raise_on_save_failure || autoraise.present?
440
- raise Parse::SaveFailureError.new(self), "Failed to create or save attributes. #{self.parse_class} was not saved."
441
- end
442
489
 
443
- end #callbacks
444
- @_session_token = nil
445
- success
446
- end
490
+ end #callbacks
491
+ @_session_token = nil
492
+ success
493
+ end
447
494
 
448
- # Save this object and raise an exception if it fails.
449
- # @raise Parse::SaveFailureError if the save fails
450
- # @param session (see #save)
451
- # @return (see #save)
452
- def save!(session: nil)
453
- save(autoraise: true, session: session)
454
- end
495
+ # Save this object and raise an exception if it fails.
496
+ # @raise Parse::RecordNotSaved if the save fails
497
+ # @param session (see #save)
498
+ # @return (see #save)
499
+ def save!(session: nil)
500
+ save(autoraise: true, session: session)
501
+ end
455
502
 
456
503
 
457
- # Delete this record from the Parse collection. Only valid if this object has an `id`.
458
- # This will run all the `destroy` callbacks.
459
- # @param session [String] a session token if you want to apply ACLs for a user in this operation.
460
- # @return [Boolean] whether the operation was successful.
461
- def destroy(session: nil)
462
- return false if new?
463
- @_session_token = session
464
- success = false
465
- run_callbacks :destroy do
466
- res = client.delete_object parse_class, id, session_token: _session_token
467
- success = res.success?
468
- if success
469
- @id = nil
470
- changes_applied!
471
- elsif self.class.raise_on_save_failure
472
- raise Parse::SaveFailureError.new(self), "Failed to create or save attributes. #{self.parse_class} was not saved."
504
+ # Delete this record from the Parse collection. Only valid if this object has an `id`.
505
+ # This will run all the `destroy` callbacks.
506
+ # @param session [String] a session token if you want to apply ACLs for a user in this operation.
507
+ # @return [Boolean] whether the operation was successful.
508
+ def destroy(session: nil)
509
+ return false if new?
510
+ @_session_token = session
511
+ success = false
512
+ run_callbacks :destroy do
513
+ res = client.delete_object parse_class, id, session_token: _session_token
514
+ success = res.success?
515
+ if success
516
+ @id = nil
517
+ changes_applied!
518
+ elsif self.class.raise_on_save_failure
519
+ raise Parse::RecordNotSaved.new(self), "Failed to create or save attributes. #{self.parse_class} was not saved."
520
+ end
521
+ # Your create action methods here
473
522
  end
474
- # Your create action methods here
523
+ @_session_token = nil
524
+ success
475
525
  end
476
- @_session_token = nil
477
- success
478
- end
479
-
480
- # Runs all the registered `before_save` related callbacks.
481
- def prepare_save!
482
- run_callbacks(:save) { false }
483
- end
484
526
 
485
- # @return [Hash] a hash of the list of changes made to this instance.
486
- def changes_payload
487
- h = attribute_updates
488
- if relation_changes?
489
- r = relation_change_operations.select { |s| s.present? }.first
490
- h.merge!(r) if r.present?
527
+ # Runs all the registered `before_save` related callbacks.
528
+ def prepare_save!
529
+ run_callbacks(:save) { false }
491
530
  end
492
- h.merge!(className: parse_class) unless h.empty?
493
- h.as_json
494
- end
495
- alias_method :update_payload, :changes_payload
496
-
497
- # Generates an array with two entries for addition and removal operations. The first entry
498
- # of the array will contain a hash of all the change operations regarding adding new relational
499
- # objects. The second entry in the array is a hash of all the change operations regarding removing
500
- # relation objects from this field.
501
- # @return [Array] an array with two hashes; the first is a hash of all the addition operations and
502
- # the second hash, all the remove operations.
503
- def relation_change_operations
504
- return [{},{}] unless relation_changes?
505
-
506
- additions = []
507
- removals = []
508
- # go through all the additions of a collection and generate an action to add.
509
- relation_updates.each do |field,collection|
510
- if collection.additions.count > 0
511
- additions.push Parse::RelationAction.new(field, objects: collection.additions, polarity: true)
531
+
532
+ # @return [Hash] a hash of the list of changes made to this instance.
533
+ def changes_payload
534
+ h = attribute_updates
535
+ if relation_changes?
536
+ r = relation_change_operations.select { |s| s.present? }.first
537
+ h.merge!(r) if r.present?
512
538
  end
513
- # go through all the additions of a collection and generate an action to remove.
514
- if collection.removals.count > 0
515
- removals.push Parse::RelationAction.new(field, objects: collection.removals, polarity: false)
539
+ #h.merge!(className: parse_class) unless h.empty?
540
+ h.as_json
541
+ end
542
+ alias_method :update_payload, :changes_payload
543
+
544
+ # Generates an array with two entries for addition and removal operations. The first entry
545
+ # of the array will contain a hash of all the change operations regarding adding new relational
546
+ # objects. The second entry in the array is a hash of all the change operations regarding removing
547
+ # relation objects from this field.
548
+ # @return [Array] an array with two hashes; the first is a hash of all the addition operations and
549
+ # the second hash, all the remove operations.
550
+ def relation_change_operations
551
+ return [{},{}] unless relation_changes?
552
+
553
+ additions = []
554
+ removals = []
555
+ # go through all the additions of a collection and generate an action to add.
556
+ relation_updates.each do |field,collection|
557
+ if collection.additions.count > 0
558
+ additions.push Parse::RelationAction.new(field, objects: collection.additions, polarity: true)
559
+ end
560
+ # go through all the additions of a collection and generate an action to remove.
561
+ if collection.removals.count > 0
562
+ removals.push Parse::RelationAction.new(field, objects: collection.removals, polarity: false)
563
+ end
516
564
  end
565
+ # merge all additions and removals into one large hash
566
+ additions = additions.reduce({}) { |m,v| m.merge! v.as_json }
567
+ removals = removals.reduce({}) { |m,v| m.merge! v.as_json }
568
+ [additions, removals]
517
569
  end
518
- # merge all additions and removals into one large hash
519
- additions = additions.reduce({}) { |m,v| m.merge! v.as_json }
520
- removals = removals.reduce({}) { |m,v| m.merge! v.as_json }
521
- [additions, removals]
522
- end
523
570
 
524
- # Saves and updates all the relational changes for made to this object.
525
- # @return [Boolean] whether all the save or update requests were successful.
526
- def update_relations
527
- # relational saves require an id
528
- return false unless @id.present?
529
- # verify we have relational changes before we do work.
530
- return true unless relation_changes?
531
- raise "Unable to update relations for a new object." if new?
532
- # get all the relational changes (both additions and removals)
533
- additions, removals = relation_change_operations
534
-
535
- responses = []
536
- # Send parallel Parse requests for each of the items to update.
537
- # since we will have multiple responses, we will track it in array
538
- [removals, additions].threaded_each do |ops|
539
- next if ops.empty? #if no operations to be performed, then we are done
540
- responses << client.update_object(parse_class, @id, ops, session_token: _session_token)
571
+ # Saves and updates all the relational changes for made to this object.
572
+ # @return [Boolean] whether all the save or update requests were successful.
573
+ def update_relations
574
+ # relational saves require an id
575
+ return false unless @id.present?
576
+ # verify we have relational changes before we do work.
577
+ return true unless relation_changes?
578
+ raise "Unable to update relations for a new object." if new?
579
+ # get all the relational changes (both additions and removals)
580
+ additions, removals = relation_change_operations
581
+
582
+ responses = []
583
+ # Send parallel Parse requests for each of the items to update.
584
+ # since we will have multiple responses, we will track it in array
585
+ [removals, additions].threaded_each do |ops|
586
+ next if ops.empty? #if no operations to be performed, then we are done
587
+ responses << client.update_object(parse_class, @id, ops, session_token: _session_token)
588
+ end
589
+ # check if any of them ended up in error
590
+ has_error = responses.any? { |response| response.error? }
591
+ # if everything was ok, find the last response to be returned and update
592
+ #their fields in case beforeSave made any changes.
593
+ unless has_error || responses.empty?
594
+ result = responses.last.result #last result to come back
595
+ set_attributes!(result)
596
+ end #unless
597
+ has_error == false
541
598
  end
542
- # check if any of them ended up in error
543
- has_error = responses.any? { |response| response.error? }
544
- # if everything was ok, find the last response to be returned and update
545
- #their fields in case beforeSave made any changes.
546
- unless has_error || responses.empty?
547
- result = responses.last.result #last result to come back
548
- set_attributes!(result)
549
- end #unless
550
- has_error == false
551
- end
552
599
 
553
- # Performs mass assignment using a hash with the ability to modify dirty tracking.
554
- # This is an internal method used to set properties on the object while controlling
555
- # whether they are dirty tracked. Each defined property has a method defined with the
556
- # suffix `_set_attribute!` that can will be called if it is contained in the hash.
557
- # @example
558
- # object.set_attributes!( {"myField" => value}, false)
559
- #
560
- # # equivalent to calling the specific method.
561
- # object.myField_set_attribute!(value, false)
562
- # @param hash [Hash] the hash containing all the attribute names and values.
563
- # @param dirty_track [Boolean] whether the assignment should be tracked in the change tracking
564
- # system.
565
- # @return [Hash]
566
- def set_attributes!(hash, dirty_track = false)
567
- return unless hash.is_a?(Hash)
568
- hash.each do |k,v|
569
- next if k == Parse::Model::OBJECT_ID || k == Parse::Model::ID
570
- method = "#{k}_set_attribute!"
571
- send(method, v, dirty_track) if respond_to?(method)
600
+ # Performs mass assignment using a hash with the ability to modify dirty tracking.
601
+ # This is an internal method used to set properties on the object while controlling
602
+ # whether they are dirty tracked. Each defined property has a method defined with the
603
+ # suffix `_set_attribute!` that can will be called if it is contained in the hash.
604
+ # @example
605
+ # object.set_attributes!( {"myField" => value}, false)
606
+ #
607
+ # # equivalent to calling the specific method.
608
+ # object.myField_set_attribute!(value, false)
609
+ # @param hash [Hash] the hash containing all the attribute names and values.
610
+ # @param dirty_track [Boolean] whether the assignment should be tracked in the change tracking
611
+ # system.
612
+ # @return [Hash]
613
+ def set_attributes!(hash, dirty_track = false)
614
+ return unless hash.is_a?(Hash)
615
+ hash.each do |k,v|
616
+ next if k == Parse::Model::OBJECT_ID || k == Parse::Model::ID
617
+ method = "#{k}_set_attribute!"
618
+ send(method, v, dirty_track) if respond_to?(method)
619
+ end
572
620
  end
573
- end
574
621
 
575
- # Clears changes information on all collections (array and relations) and all
576
- # local attributes.
577
- def changes_applied!
578
- # find all fields that are of type :array
579
- fields(:array) do |key,v|
580
- proxy = send(key)
581
- # clear changes
582
- proxy.changes_applied! if proxy.respond_to?(:changes_applied!)
583
- end
622
+ # Clears changes information on all collections (array and relations) and all
623
+ # local attributes.
624
+ def changes_applied!
625
+ # find all fields that are of type :array
626
+ fields(:array) do |key,v|
627
+ proxy = send(key)
628
+ # clear changes
629
+ proxy.changes_applied! if proxy.respond_to?(:changes_applied!)
630
+ end
584
631
 
585
- # for all relational fields,
586
- relations.each do |key,v|
587
- proxy = send(key)
588
- # clear changes if they support the method.
589
- proxy.changes_applied! if proxy.respond_to?(:changes_applied!)
632
+ # for all relational fields,
633
+ relations.each do |key,v|
634
+ proxy = send(key)
635
+ # clear changes if they support the method.
636
+ proxy.changes_applied! if proxy.respond_to?(:changes_applied!)
637
+ end
638
+ changes_applied
590
639
  end
591
- changes_applied
592
- end
593
640
 
594
641
 
642
+ end
595
643
  end
596
644
 
597
645
  end