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