parse-stack 1.0.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 (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