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.
- checksums.yaml +7 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +77 -0
- data/LICENSE +20 -0
- data/README.md +1281 -0
- data/Rakefile +12 -0
- data/bin/console +20 -0
- data/bin/server +10 -0
- data/bin/setup +7 -0
- data/lib/parse/api/all.rb +13 -0
- data/lib/parse/api/analytics.rb +16 -0
- data/lib/parse/api/apps.rb +37 -0
- data/lib/parse/api/batch.rb +148 -0
- data/lib/parse/api/cloud_functions.rb +18 -0
- data/lib/parse/api/config.rb +22 -0
- data/lib/parse/api/files.rb +21 -0
- data/lib/parse/api/hooks.rb +68 -0
- data/lib/parse/api/objects.rb +77 -0
- data/lib/parse/api/push.rb +16 -0
- data/lib/parse/api/schemas.rb +25 -0
- data/lib/parse/api/sessions.rb +11 -0
- data/lib/parse/api/users.rb +43 -0
- data/lib/parse/client.rb +225 -0
- data/lib/parse/client/authentication.rb +59 -0
- data/lib/parse/client/body_builder.rb +69 -0
- data/lib/parse/client/caching.rb +103 -0
- data/lib/parse/client/protocol.rb +15 -0
- data/lib/parse/client/request.rb +43 -0
- data/lib/parse/client/response.rb +116 -0
- data/lib/parse/model/acl.rb +182 -0
- data/lib/parse/model/associations/belongs_to.rb +121 -0
- data/lib/parse/model/associations/collection_proxy.rb +202 -0
- data/lib/parse/model/associations/has_many.rb +218 -0
- data/lib/parse/model/associations/pointer_collection_proxy.rb +71 -0
- data/lib/parse/model/associations/relation_collection_proxy.rb +134 -0
- data/lib/parse/model/bytes.rb +50 -0
- data/lib/parse/model/core/actions.rb +499 -0
- data/lib/parse/model/core/properties.rb +377 -0
- data/lib/parse/model/core/querying.rb +100 -0
- data/lib/parse/model/core/schema.rb +92 -0
- data/lib/parse/model/date.rb +50 -0
- data/lib/parse/model/file.rb +127 -0
- data/lib/parse/model/geopoint.rb +98 -0
- data/lib/parse/model/model.rb +120 -0
- data/lib/parse/model/object.rb +347 -0
- data/lib/parse/model/pointer.rb +106 -0
- data/lib/parse/model/push.rb +99 -0
- data/lib/parse/query.rb +378 -0
- data/lib/parse/query/constraint.rb +130 -0
- data/lib/parse/query/constraints.rb +176 -0
- data/lib/parse/query/operation.rb +66 -0
- data/lib/parse/query/ordering.rb +49 -0
- data/lib/parse/stack.rb +11 -0
- data/lib/parse/stack/version.rb +5 -0
- data/lib/parse/webhooks.rb +228 -0
- data/lib/parse/webhooks/payload.rb +115 -0
- data/lib/parse/webhooks/registration.rb +139 -0
- data/parse-stack.gemspec +45 -0
- 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
|