parse-stack 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +6 -0
  3. data/Gemfile.lock +77 -0
  4. data/LICENSE +20 -0
  5. data/README.md +1281 -0
  6. data/Rakefile +12 -0
  7. data/bin/console +20 -0
  8. data/bin/server +10 -0
  9. data/bin/setup +7 -0
  10. data/lib/parse/api/all.rb +13 -0
  11. data/lib/parse/api/analytics.rb +16 -0
  12. data/lib/parse/api/apps.rb +37 -0
  13. data/lib/parse/api/batch.rb +148 -0
  14. data/lib/parse/api/cloud_functions.rb +18 -0
  15. data/lib/parse/api/config.rb +22 -0
  16. data/lib/parse/api/files.rb +21 -0
  17. data/lib/parse/api/hooks.rb +68 -0
  18. data/lib/parse/api/objects.rb +77 -0
  19. data/lib/parse/api/push.rb +16 -0
  20. data/lib/parse/api/schemas.rb +25 -0
  21. data/lib/parse/api/sessions.rb +11 -0
  22. data/lib/parse/api/users.rb +43 -0
  23. data/lib/parse/client.rb +225 -0
  24. data/lib/parse/client/authentication.rb +59 -0
  25. data/lib/parse/client/body_builder.rb +69 -0
  26. data/lib/parse/client/caching.rb +103 -0
  27. data/lib/parse/client/protocol.rb +15 -0
  28. data/lib/parse/client/request.rb +43 -0
  29. data/lib/parse/client/response.rb +116 -0
  30. data/lib/parse/model/acl.rb +182 -0
  31. data/lib/parse/model/associations/belongs_to.rb +121 -0
  32. data/lib/parse/model/associations/collection_proxy.rb +202 -0
  33. data/lib/parse/model/associations/has_many.rb +218 -0
  34. data/lib/parse/model/associations/pointer_collection_proxy.rb +71 -0
  35. data/lib/parse/model/associations/relation_collection_proxy.rb +134 -0
  36. data/lib/parse/model/bytes.rb +50 -0
  37. data/lib/parse/model/core/actions.rb +499 -0
  38. data/lib/parse/model/core/properties.rb +377 -0
  39. data/lib/parse/model/core/querying.rb +100 -0
  40. data/lib/parse/model/core/schema.rb +92 -0
  41. data/lib/parse/model/date.rb +50 -0
  42. data/lib/parse/model/file.rb +127 -0
  43. data/lib/parse/model/geopoint.rb +98 -0
  44. data/lib/parse/model/model.rb +120 -0
  45. data/lib/parse/model/object.rb +347 -0
  46. data/lib/parse/model/pointer.rb +106 -0
  47. data/lib/parse/model/push.rb +99 -0
  48. data/lib/parse/query.rb +378 -0
  49. data/lib/parse/query/constraint.rb +130 -0
  50. data/lib/parse/query/constraints.rb +176 -0
  51. data/lib/parse/query/operation.rb +66 -0
  52. data/lib/parse/query/ordering.rb +49 -0
  53. data/lib/parse/stack.rb +11 -0
  54. data/lib/parse/stack/version.rb +5 -0
  55. data/lib/parse/webhooks.rb +228 -0
  56. data/lib/parse/webhooks/payload.rb +115 -0
  57. data/lib/parse/webhooks/registration.rb +139 -0
  58. data/parse-stack.gemspec +45 -0
  59. metadata +340 -0
@@ -0,0 +1,71 @@
1
+ require 'active_model'
2
+ require 'active_support'
3
+ require 'active_support/inflector'
4
+ require 'active_support/core_ext/object'
5
+ require_relative 'collection_proxy'
6
+
7
+ # A PointerCollectionProxy is a collection proxy that only allows Parse Pointers (Objects)
8
+ # to be part of the collection. This is done by typecasting the collection to a particular
9
+ # Parse class. Ex. An Artist may have several Song objects. Therefore an Artist could have a
10
+ # column :songs, that is an array (collection) of Song (Parse::Object) objects.
11
+ # Because this collection is typecasted, we can do some more interesting things.
12
+ module Parse
13
+
14
+ class PointerCollectionProxy < CollectionProxy
15
+
16
+ def collection=(c)
17
+ notify_will_change!
18
+ @collection = c
19
+ end
20
+ # When we add items, we will verify that they are of type Parse::Pointer at a minimum.
21
+ # If they are not, and it is a hash, we check to see if it is a Parse hash.
22
+ def add(*items)
23
+ notify_will_change! if items.count > 0
24
+ items.flatten.parse_pointers.each do |item|
25
+ collection.push(item)
26
+ end
27
+ @collection
28
+ end
29
+
30
+ # removes items from the collection
31
+ def remove(*items)
32
+ notify_will_change! if items.count > 0
33
+ items.flatten.parse_pointers.each do |item|
34
+ collection.delete item
35
+ end
36
+ @collection
37
+ end
38
+
39
+ def add!(*items)
40
+ super(items.flatten.parse_pointers)
41
+ end
42
+
43
+ def add_unique!(*items)
44
+ super(items.flatten.parse_pointers)
45
+ end
46
+
47
+ def remove!(*items)
48
+ super(items.flatten.parse_pointers)
49
+ end
50
+
51
+ # We define a fetch and fetch! methods on array
52
+ # that contain pointer objects. This will make requests for each object
53
+ # in the array that is of pointer state (object with unfetch data) and fetch
54
+ # them in parallel.
55
+
56
+ def fetch!
57
+ collection.fetch!
58
+ end
59
+
60
+ def fetch
61
+ collection.fetch
62
+ end
63
+ # Even though we may have full Parse Objects in the collection, when updating
64
+ # or storing them in Parse, we actually just want Parse::Pointer objects.
65
+ def as_json(*args)
66
+ collection.parse_pointers.as_json
67
+ end
68
+
69
+ end
70
+
71
+ end
@@ -0,0 +1,134 @@
1
+ require 'active_support/inflector'
2
+ require 'active_support/core_ext/object'
3
+ require_relative 'pointer_collection_proxy'
4
+
5
+ # The RelationCollectionProxy is similar to a PointerCollectionProxy except that
6
+ # there is no actual "array" object in Parse. Parse treats relation through an
7
+ # intermediary table (a.k.a. join table). Whenever a developer wants the
8
+ # contents of a collection, the foreign table needs to be queried instead.
9
+ # In this scenario, the parse_class: initializer argument should be passed in order to
10
+ # know which remote table needs to be queried in order to fetch the items of the collection.
11
+ #
12
+ # Unlike managing an array of Pointers, relations in Parse are done throug atomic operations,
13
+ # which have a specific API. The design of this proxy is to maintain two sets of lists,
14
+ # items to be added to the relation and a separate list of items to be removed from the
15
+ # relation.
16
+ #
17
+ # Because this relationship is based on queryable Parse table, we are also able to
18
+ # not just get all the items in a collection, but also provide additional constraints to
19
+ # get matching items within the relation collection.
20
+ #
21
+ # When creating a Relation proxy, all the delegate methods defined in the superclasses
22
+ # need to be implemented, in addition to a few others with the key parameter:
23
+ # _relation_query and _commit_relation_updates . :'key'_relation_query should return a
24
+ # Parse::Query object that is properly tied to the foreign table class related to this object column.
25
+ # Example, if an Artist has many Song objects, then the query to be returned by this method
26
+ # should be a Parse::Query for the class 'Song'.
27
+ # Because relation changes are separate from object changes, you can call save on a
28
+ # relation collection to save the current add and remove operations. Because the delegate needs
29
+ # to be informed of the changes being committed, it will be notified
30
+ # through :'key'_commit_relation_updates message. The delegate is also in charge of
31
+ # clearing out the change information for the collection if saved successfully.
32
+
33
+ module Parse
34
+
35
+ class RelationCollectionProxy < PointerCollectionProxy
36
+
37
+ define_attribute_methods :additions, :removals
38
+ attr_reader :additions, :removals
39
+
40
+ def initialize(collection = nil, delegate: nil, key: nil, parse_class: nil)
41
+ super
42
+ @additions = []
43
+ @removals = []
44
+ end
45
+
46
+ # You can get items within the collection relation filtered by a specific set
47
+ # of query constraints.
48
+ def all(constraints = {})
49
+ q = query( {limit: :max}.merge(constraints) )
50
+ if block_given?
51
+ # if we have a query, then use the Proc with it (more efficient)
52
+ return q.present? ? q.results(&Proc.new) : collection.each(&Proc.new)
53
+ end
54
+ # if no block given, get all the results
55
+ q.present? ? q.results : collection
56
+ end
57
+
58
+ # Ask the delegate to return a query for this collection type
59
+ def query(constraints = {})
60
+ q = forward :"#{@key}_relation_query"
61
+ end
62
+
63
+
64
+ # add the current items to the relation. The process of adding it
65
+ # is adding it to the @additions array and making sure it is
66
+ # removed from the @removals array.
67
+ def add(*items)
68
+ items = items.flatten.parse_pointers
69
+ return @collection if items.empty?
70
+
71
+ notify_will_change!
72
+ additions_will_change!
73
+ removals_will_change!
74
+ # take all the items
75
+ items.each do |item|
76
+ @additions.push item
77
+ @collection.push item
78
+ #cleanup
79
+ @removals.delete item
80
+ end
81
+ @collection
82
+ end
83
+
84
+ # removes the current items from the relation.
85
+ # The process of removing is deleting it from the @removals array,
86
+ # and adding it to the @additions array.
87
+ def remove(*items)
88
+ items = items.flatten.parse_pointers
89
+ return @collection if items.empty?
90
+ notify_will_change!
91
+ additions_will_change!
92
+ removals_will_change!
93
+ items.each do |item|
94
+ @removals.push item
95
+ @collection.delete item
96
+ # remove it from any add operations
97
+ @additions.delete item
98
+ end
99
+ @collection
100
+ end
101
+
102
+ def add!(*items)
103
+ return false unless @delegate.respond_to?(:op_add_relation!)
104
+ items = items.flatten.parse_pointers
105
+ @delegate.send :op_add_relation!, @key, items
106
+ end
107
+
108
+ def add_unique!(*items)
109
+ return false unless @delegate.respond_to?(:op_add_relation!)
110
+ items = items.flatten.parse_pointers
111
+ @delegate.send :op_add_relation!, @key, items
112
+ end
113
+
114
+ def remove!(*items)
115
+ return false unless @delegate.respond_to?(:op_remove_relation!)
116
+ items = items.flatten.parse_pointers
117
+ @delegate.send :op_remove_relation!, @key, items
118
+ end
119
+
120
+ # save the changes if any
121
+ def save
122
+ unless @removals.empty? && @additions.empty?
123
+ forward :"#{@key}_commit_relation_updates"
124
+ end
125
+ end
126
+
127
+ def <<(*list)
128
+ list.each { |d| add(d) }
129
+ @collection
130
+ end
131
+
132
+ end
133
+
134
+ end
@@ -0,0 +1,50 @@
1
+ require 'active_support'
2
+ require 'active_support/core_ext/object'
3
+ require_relative "model"
4
+ require 'base64'
5
+
6
+ # Support for Bytes type in Parse
7
+ module Parse
8
+
9
+ class Bytes < Model
10
+ attr_accessor :base64
11
+ def parse_class; TYPE_BYTES; end;
12
+ def parse_class; self.class.parse_class; end;
13
+ alias_method :__type, :parse_class
14
+
15
+ # initialize with a base64 string or a Bytes object
16
+ def initialize(bytes = "")
17
+ @base64 = (bytes.is_a?(Bytes) ? bytes.base64 : bytes).dup
18
+ end
19
+
20
+ def attributes
21
+ {__type: :string, base64: :string }.freeze
22
+ end
23
+
24
+ # takes a string and base64 encodes it
25
+ def encode(s)
26
+ @base64 = Base64.encode64(s)
27
+ end
28
+
29
+ # decode the internal data
30
+ def decoded
31
+ Base64.decode64(@base64 || "")
32
+ end
33
+
34
+ def attributes=(a)
35
+ if a.is_a?(String)
36
+ @bytes = a
37
+ elsif a.is_a?(Hash)
38
+ @bytes = a["base64".freeze] || @bytes
39
+ end
40
+ end
41
+
42
+ # two Bytes objects are equal if they have the same base64 signature
43
+ def ==(u)
44
+ return false unless u.is_a?(self.class)
45
+ @base64 == u.base64
46
+ end
47
+
48
+ end
49
+
50
+ end
@@ -0,0 +1,499 @@
1
+ require 'active_model'
2
+ require 'active_support'
3
+ require 'active_support/inflector'
4
+ require 'active_support/core_ext/object'
5
+ require 'time'
6
+ require 'parallel'
7
+ require_relative '../../client/request'
8
+
9
+ #This module provides many of the CRUD operations on Parse::Object.
10
+
11
+ # A Parse::RelationAction is special operation that adds one object to a relational
12
+ # table as to another. Depending on the polarity of the action, the objects are
13
+ # either added or removed from the relation. This class is used to generate the proper
14
+ # hash request format Parse needs in order to modify relational information for classes.
15
+ module Parse
16
+ class RelationAction
17
+ ADD = "AddRelation".freeze
18
+ REMOVE = "RemoveRelation".freeze
19
+ attr_accessor :polarity, :key, :objects
20
+ # provide the column name of the field, polarity (true = add, false = remove) and the
21
+ # list of objects.
22
+ def initialize(field, polarity: true, objects: [])
23
+ @key = field.to_s
24
+ self.polarity = polarity
25
+ @objects = [objects].flatten.compact
26
+ end
27
+
28
+ # generate the proper Parse hash-format operation
29
+ def as_json(*args)
30
+ { @key =>
31
+ {
32
+ "__op" => ( @polarity == true ? ADD : REMOVE ),
33
+ "objects" => objects.parse_pointers
34
+ }
35
+ }.as_json
36
+ end
37
+
38
+ end
39
+
40
+ end
41
+
42
+ # This module is mainly all the basic orm operations. To support batching actions,
43
+ # we use temporary Request objects have contain the operation to be performed (in some cases).
44
+ # This allows to group a list of Request methods, into a batch for sending all at once to Parse.
45
+ module Parse
46
+ class SaveFailureError < StandardError
47
+ attr_reader :object
48
+ def initialize(object)
49
+ @object = object
50
+ end
51
+ end
52
+
53
+ module Actions
54
+
55
+ def self.included(base)
56
+ base.extend(ClassMethods)
57
+ end
58
+
59
+ module ClassMethods
60
+ attr_accessor :raise_on_save_failure
61
+
62
+ def raise_on_save_failure
63
+ return @raise_on_save_failure unless @raise_on_save_failure.nil?
64
+ Parse::Model.raise_on_save_failure
65
+ end
66
+
67
+ def first_or_create(query_attrs = {}, resource_attrs = {})
68
+ # force only one result
69
+ query_attrs.symbolize_keys!
70
+ resource_attrs.symbolize_keys!
71
+ obj = query(query_attrs).first
72
+
73
+ if obj.blank?
74
+ obj = self.new query_attrs
75
+ obj.apply_attributes!(resource_attrs, dirty_track: false)
76
+ end
77
+ obj.save if obj.new? && Parse::Model.autosave_on_create
78
+ obj
79
+ end
80
+
81
+ # not quite sure if I like the name of this API.
82
+ def save_all(constraints = {})
83
+ force = false
84
+
85
+ iterator_block = nil
86
+ if block_given?
87
+ iterator_block = Proc.new
88
+ force ||= false
89
+ else
90
+ # if no block given, assume you want to just save all objects
91
+ # regardless of modification.
92
+ force = true
93
+ end
94
+ # Only generate the comparison block once.
95
+ # updated_comparison_block = Proc.new { |x| x.updated_at }
96
+
97
+ anchor_date = Parse::Date.now
98
+ constraints.merge! :updated_at.lte => anchor_date
99
+ # oldest first, so we create a reduction-cycle
100
+ constraints.merge! order: :updated_at.asc, limit: 100
101
+ update_query = query(constraints)
102
+ puts "Setting Anchor Date: #{anchor_date}"
103
+ cursor = nil
104
+ has_errors = false
105
+ loop do
106
+ results = update_query.results
107
+
108
+ break if results.empty?
109
+
110
+ # verify we didn't get duplicates fetches
111
+ if cursor.is_a?(Parse::Object) && results.any? { |x| x.id == cursor.id }
112
+ warn "Unbounded update detected - stopping."
113
+ has_errors = true
114
+ break cursor
115
+ end
116
+
117
+ results.each(&iterator_block) if iterator_block.present?
118
+ # we don't need to refresh the objects in the array with the results
119
+ # since we will be throwing them away. Force determines whether
120
+ # to save these objects regardless of whether they are dirty.
121
+ batch = results.save(merge: false, force: force)
122
+
123
+ # faster version assuming sorting order wasn't messed up
124
+ cursor = results.last
125
+ # slower version, but more accurate
126
+ # cursor_item = results.max_by(&updated_comparison_block).updated_at
127
+ puts "Updated #{results.count} records updated <= #{cursor.updated_at}"
128
+
129
+ if cursor.is_a?(Parse::Object)
130
+ update_query.where :updated_at.gte => cursor.updated_at
131
+
132
+ if cursor.updated_at.present? && cursor.updated_at > anchor_date
133
+ warn "Reached anchor date limit - stopping."
134
+ break cursor
135
+ end
136
+
137
+ end
138
+
139
+ has_errors ||= batch.error?
140
+ end
141
+ has_errors
142
+ end
143
+
144
+ end # ClassMethods
145
+
146
+ def operate_field!(field, op_hash)
147
+ if op_hash.is_a?(Parse::RelationAction)
148
+ op_hash = op_hash.as_json
149
+ else
150
+ op_hash = { field => op_hash }.as_json
151
+ end
152
+
153
+ response = client.update_object(parse_class, id, op_hash )
154
+ if response.error?
155
+ puts "[#{parse_class}:#{field} Operation] #{response.error}"
156
+ end
157
+ response.success?
158
+ end
159
+
160
+ def op_add!(field,objects)
161
+ operate_field field, { __op: :Add, objects: objects }
162
+ end
163
+
164
+ def op_add_unique!(field,objects)
165
+ operate_field field, { __op: :AddUnique, objects: objects }
166
+ end
167
+
168
+ def op_remove!(field, objects)
169
+ operate_field field, { __op: :Remove, objects: objects }
170
+ end
171
+
172
+ def op_destroy!(field)
173
+ operate_field field, { __op: :Delete }
174
+ end
175
+
176
+ def op_add_relation!(field, objects = [])
177
+ objects = [objects] unless objects.is_a?(Array)
178
+ return false if objects.empty?
179
+ relation_action = Parse::RelationAction.new(field, polarity: true, objects: objects)
180
+ operate_field field, relation_action
181
+ end
182
+
183
+ def op_remove_relation!(field, objects = [])
184
+ objects = [objects] unless objects.is_a?(Array)
185
+ return false if objects.empty?
186
+ relation_action = Parse::RelationAction.new(field, polarity: false, objects: objects)
187
+ operate_field field, relation_action
188
+ end
189
+
190
+ # This creates a destroy_request for the current object.
191
+ def destroy_request
192
+ return nil unless @id.present?
193
+ uri = Client.uri_path(self)
194
+ r = Request.new( :delete, uri )
195
+ r.tag = object_id
196
+ r
197
+ end
198
+
199
+ # Creates an array of all possible PUT operations that need to be performed
200
+ # on this local object. The reason it is a list is because attribute operations,
201
+ # relational add operations and relational remove operations are treated as separate
202
+ # Parse requests.
203
+ def change_requests(force = false)
204
+ requests = []
205
+ # get the URI path for this object.
206
+ uri = Client.uri_path(self)
207
+
208
+ # generate the request to update the object (PUT)
209
+ if attribute_changes? || force
210
+ # if it's new, then we should call :post for creating the object.
211
+ method = new? ? :post : :put
212
+ r = Request.new( method, uri, body: attribute_updates)
213
+ r.tag = object_id
214
+ requests << r
215
+ end
216
+
217
+ # if the object is not new, then we can also add all the relational changes
218
+ # we need to perform.
219
+ if @id.present? && relation_changes?
220
+ relation_change_operations.each do |ops|
221
+ next if ops.empty?
222
+ r = Request.new( :put, uri, body: ops)
223
+ r.tag = object_id
224
+ requests << r
225
+ end
226
+ end
227
+ requests
228
+ end
229
+
230
+ # This methods sends an update request for this object with the any change
231
+ # information based on its local attributes. The bang implies that it will send
232
+ # the request even though it is possible no changes were performed. This is useful
233
+ # in kicking-off an beforeSave / afterSave hooks
234
+ def update!(raw: false)
235
+ if valid? == false
236
+ errors.full_messages.each do |msg|
237
+ warn "[#{parse_class}] warning: #{msg}"
238
+ end
239
+ end
240
+ response = client.update_object(parse_class, id, attribute_updates)
241
+ if response.success?
242
+ result = response.result
243
+ # Because beforeSave hooks can change the fields we are saving, any items that were
244
+ # changed, are returned to us and we should apply those locally to be in sync.
245
+ set_attributes!(result)
246
+ end
247
+ puts "Error updating #{self.parse_class}: #{response.error}" if response.error?
248
+ return response if raw
249
+ response.success?
250
+ end
251
+
252
+ # save the updates on the objects, if any
253
+ def update
254
+ return true unless attribute_changes?
255
+ update!
256
+ end
257
+
258
+ # create this object in Parse
259
+ def create
260
+ res = client.create_object(parse_class, attribute_updates )
261
+ unless res.error?
262
+ result = res.result
263
+ @id = result["objectId"] || @id
264
+ @created_at = result["createdAt"] || @created_at
265
+ #if the object is created, updatedAt == createdAt
266
+ @updated_at = result["updatedAt"] || result["createdAt"] || @updated_at
267
+ # Because beforeSave hooks can change the fields we are saving, any items that were
268
+ # changed, are returned to us and we should apply those locally to be in sync.
269
+ set_attributes!(result)
270
+ end
271
+ puts "Error creating #{self.parse_class}: #{res.error}" if res.error?
272
+ res.success?
273
+ end
274
+
275
+ # saves the object. If the object has not changed, it is a noop. If it is new,
276
+ # we will create the object. If the object has an id, we will update the record.
277
+ # You can define before and after :save callbacks
278
+ def save
279
+ return true unless changed?
280
+ success = false
281
+ run_callbacks :save do
282
+ #first process the create/update action if any
283
+ #then perform any relation changes that need to be performed
284
+ success = new? ? create : update
285
+
286
+ # if the save was successful and we have relational changes
287
+ # let's update send those next.
288
+ if success
289
+ if relation_changes?
290
+ # get the list of changed keys
291
+ changed_attribute_keys = changed - relations.keys.map(&:to_s)
292
+ clear_attribute_changes( changed_attribute_keys )
293
+ success = update_relations
294
+ if success
295
+ changes_applied!
296
+ elsif self.class.raise_on_save_failure
297
+ raise Parse::SaveFailureError.new(self), "Failed updating relations. #{self.parse_class} partially saved."
298
+ end
299
+ else
300
+ changes_applied!
301
+ end
302
+ elsif self.class.raise_on_save_failure
303
+ raise Parse::SaveFailureError.new(self), "Failed to create or save attributes. #{self.parse_class} was not saved."
304
+ end
305
+
306
+ end #callbacks
307
+ success
308
+ end
309
+
310
+ # only destroy the object if it has an id. You can setup before and after
311
+ #callback hooks on :destroy
312
+ def destroy
313
+ return false if new?
314
+ success = false
315
+ run_callbacks :destroy do
316
+ res = client.delete_object parse_class, id
317
+ success = res.success?
318
+ if success
319
+ @id = nil
320
+ changes_applied!
321
+ elsif self.class.raise_on_save_failure
322
+ raise Parse::SaveFailureError.new(self), "Failed to create or save attributes. #{self.parse_class} was not saved."
323
+ end
324
+ # Your create action methods here
325
+ end
326
+ success
327
+ end
328
+
329
+ # this method is useful to generate an array of additions and removals to a relational
330
+ # column.
331
+ def relation_change_operations
332
+ return [{},{}] unless relation_changes?
333
+
334
+ additions = []
335
+ removals = []
336
+ # go through all the additions of a collection and generate an action to add.
337
+ relation_updates.each do |field,collection|
338
+ if collection.additions.count > 0
339
+ additions.push Parse::RelationAction.new(field, objects: collection.additions, polarity: true)
340
+ end
341
+ # go through all the additions of a collection and generate an action to remove.
342
+ if collection.removals.count > 0
343
+ removals.push Parse::RelationAction.new(field, objects: collection.removals, polarity: false)
344
+ end
345
+ end
346
+ # merge all additions and removals into one large hash
347
+ additions = additions.reduce({}) { |m,v| m.merge! v.as_json }
348
+ removals = removals.reduce({}) { |m,v| m.merge! v.as_json }
349
+ [additions, removals]
350
+ end
351
+
352
+ # update relations updates all the relational data that needs to be updated.
353
+ def update_relations
354
+ # relational saves require an id
355
+ return false unless @id.present?
356
+ # verify we have relational changes before we do work.
357
+ return true unless relation_changes?
358
+ raise "Unable to update relations for a new object." if new?
359
+ # get all the relational changes (both additions and removals)
360
+ additions, removals = relation_change_operations
361
+ # removal_response = client.update_object(parse_class, id, removals)
362
+ # addition_response = client.update_object(parse_class, id, additions)
363
+ responses = []
364
+ # Send parallel Parse requests for each of the items to update.
365
+ # since we will have multiple responses, we will track it in array
366
+ [removals, additions].threaded_each do |ops|
367
+ next if ops.empty? #if no operations to be performed, then we are done
368
+ responses << client.update_object(parse_class, @id, ops)
369
+ end
370
+ #response = client.update_object(parse_class, id, relation_updates)
371
+ # check if any of them ended up in error
372
+ has_error = responses.any? { |response| response.error? }
373
+ # if everything was ok, find the last response to be returned and update
374
+ #their fields in case beforeSave made any changes.
375
+ unless has_error || responses.empty?
376
+ result = responses.last.result #last result to come back
377
+ set_attributes!(result)
378
+ end #unless
379
+ has_error == false
380
+ end
381
+
382
+ def set_attributes!(hash, dirty_track = false)
383
+ return unless hash.is_a?(Hash)
384
+ hash.each do |k,v|
385
+ next if k == "objectId".freeze || k == "id".freeze
386
+ method = "#{k}_set_attribute!"
387
+ send(method, v, dirty_track) if respond_to?(method)
388
+ end
389
+ end
390
+
391
+ # clears changes information on all collections (array and relations) and all
392
+ # local attributes.
393
+ def changes_applied!
394
+ # find all fields that are of type :array
395
+ fields(:array) do |key,v|
396
+ proxy = send(key)
397
+ # clear changes
398
+ proxy.changes_applied! if proxy.respond_to?(:changes_applied!)
399
+ end
400
+
401
+ # for all relational fields,
402
+ relations.each do |key,v|
403
+ proxy = send(key)
404
+ # clear changes if they support the method.
405
+ proxy.changes_applied! if proxy.respond_to?(:changes_applied!)
406
+ end
407
+ changes_applied
408
+ end
409
+
410
+
411
+ end
412
+
413
+ module Fetching
414
+
415
+ # force fetches the current object with the data contained in Parse.
416
+ def fetch!
417
+ response = client.fetch_object(parse_class, id)
418
+ if response.error?
419
+ puts "[Fetch Error] #{response.code}: #{response.error}"
420
+ end
421
+ # take the result hash and apply it to the attributes.
422
+ apply_attributes!(response.result, dirty_track: false)
423
+ clear_changes!
424
+ self
425
+ end
426
+
427
+ # fetches the object if needed
428
+ def fetch
429
+ # if it is a pointer, then let's go fetch the rest of the content
430
+ pointer? ? fetch! : self
431
+ end
432
+
433
+ # autofetches the object based on a key. If the key is not a Parse standard
434
+ # key, the current object is a pointer, then fetch the object - but only if
435
+ # the current object is currently autofetching.
436
+ def autofetch!(key)
437
+ key = key.to_sym
438
+ @fetch_lock ||= false
439
+ if @fetch_lock != true && pointer? && Parse::Properties::BASE_KEYS.include?(key) == false && respond_to?(:fetch)
440
+ @fetch_lock = true
441
+ send :fetch
442
+ @fetch_lock = false
443
+ end
444
+
445
+ end
446
+
447
+ end
448
+
449
+ end
450
+
451
+ class Array
452
+
453
+ # Support for threaded operations on array items
454
+ def threaded_each(threads = 2)
455
+ Parallel.each(self, {in_threads: threads}, &Proc.new)
456
+ end
457
+
458
+ def threaded_map(threads = 2)
459
+ Parallel.map(self, {in_threads: threads}, &Proc.new)
460
+ end
461
+
462
+ def self.threaded_select(threads = 2)
463
+ Parallel.select(self, {in_threads: threads}, &Proc.new)
464
+ end
465
+
466
+ # fetches all the objects in the array (force)
467
+ # a parameter symbol can be passed indicating the lookup methodology. Default
468
+ # is parallel which fetches all objects in parallel HTTP requests.
469
+ # If nil is passed in, then all the fetching happens sequentially.
470
+ def fetch!(lookup = :parallel)
471
+ # this gets all valid parse objects from the array
472
+ items = valid_parse_objects
473
+
474
+ # make parallel requests.
475
+ unless lookup == :parallel
476
+ # force fetch all objects
477
+ items.threaded_each { |o| o.fetch! }
478
+ else
479
+ # serially fetch each object
480
+ items.each { |o| o.fetch! }
481
+ end
482
+ self #return for chaining.
483
+ end
484
+
485
+ # fetches all pointer objects in the array. You can pass a symbol argument
486
+ # that provides the lookup methodology, default is :parallel. Objects that have
487
+ # already been fetched (not in a pointer state) are skipped.
488
+ def fetch(lookup = :parallel)
489
+ items = valid_parse_objects
490
+ if lookup == :parallel
491
+ items.threaded_each { |o| o.fetch }
492
+ else
493
+ items.each { |e| e.fetch }
494
+ end
495
+ #self.replace items
496
+ self
497
+ end
498
+
499
+ end