parse-stack 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|