parse-stack-next 4.5.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/.bundle/config +2 -0
- data/.env.sample +112 -0
- data/.env.test +10 -0
- data/.github/workflows/ruby.yml +36 -0
- data/.gitignore +49 -0
- data/.ruby-version +1 -0
- data/.solargraph.yml +22 -0
- data/CHANGELOG.md +5816 -0
- data/Gemfile +30 -0
- data/Gemfile.lock +175 -0
- data/LICENSE.txt +23 -0
- data/Makefile +63 -0
- data/README.md +5655 -0
- data/Rakefile +573 -0
- data/bin/console +38 -0
- data/bin/parse-console +136 -0
- data/bin/server +17 -0
- data/bin/setup +7 -0
- data/config/parse-config.json +12 -0
- data/docs/TEST_SERVER.md +271 -0
- data/docs/_config.yml +1 -0
- data/docs/mcp_guide.md +3484 -0
- data/docs/mongodb_direct_guide.md +1348 -0
- data/docs/mongodb_index_optimization_guide.md +631 -0
- data/examples/transaction_example.rb +219 -0
- data/lib/parse/acl_scope.rb +728 -0
- data/lib/parse/agent/cancellation_token.rb +80 -0
- data/lib/parse/agent/constraint_translator.rb +480 -0
- data/lib/parse/agent/describe.rb +420 -0
- data/lib/parse/agent/errors.rb +133 -0
- data/lib/parse/agent/mcp_client.rb +557 -0
- data/lib/parse/agent/mcp_dispatcher.rb +1023 -0
- data/lib/parse/agent/mcp_rack_app.rb +1143 -0
- data/lib/parse/agent/mcp_server.rb +376 -0
- data/lib/parse/agent/metadata_audit.rb +259 -0
- data/lib/parse/agent/metadata_dsl.rb +733 -0
- data/lib/parse/agent/metadata_registry.rb +794 -0
- data/lib/parse/agent/pipeline_validator.rb +82 -0
- data/lib/parse/agent/prompts.rb +351 -0
- data/lib/parse/agent/rate_limiter.rb +158 -0
- data/lib/parse/agent/relation_graph.rb +162 -0
- data/lib/parse/agent/result_formatter.rb +453 -0
- data/lib/parse/agent/tools.rb +5489 -0
- data/lib/parse/agent.rb +3249 -0
- data/lib/parse/api/aggregate.rb +79 -0
- data/lib/parse/api/all.rb +26 -0
- data/lib/parse/api/analytics.rb +18 -0
- data/lib/parse/api/batch.rb +33 -0
- data/lib/parse/api/cloud_functions.rb +58 -0
- data/lib/parse/api/config.rb +125 -0
- data/lib/parse/api/files.rb +29 -0
- data/lib/parse/api/hooks.rb +117 -0
- data/lib/parse/api/objects.rb +146 -0
- data/lib/parse/api/path_segment.rb +75 -0
- data/lib/parse/api/push.rb +20 -0
- data/lib/parse/api/schema.rb +49 -0
- data/lib/parse/api/server.rb +50 -0
- data/lib/parse/api/sessions.rb +24 -0
- data/lib/parse/api/users.rb +250 -0
- data/lib/parse/atlas_search/index_manager.rb +353 -0
- data/lib/parse/atlas_search/result.rb +204 -0
- data/lib/parse/atlas_search/search_builder.rb +604 -0
- data/lib/parse/atlas_search/session.rb +253 -0
- data/lib/parse/atlas_search.rb +995 -0
- data/lib/parse/client/authentication.rb +97 -0
- data/lib/parse/client/batch.rb +234 -0
- data/lib/parse/client/body_builder.rb +240 -0
- data/lib/parse/client/caching.rb +203 -0
- data/lib/parse/client/logging.rb +293 -0
- data/lib/parse/client/profiling.rb +181 -0
- data/lib/parse/client/protocol.rb +91 -0
- data/lib/parse/client/request.rb +233 -0
- data/lib/parse/client/response.rb +208 -0
- data/lib/parse/client.rb +1104 -0
- data/lib/parse/clp_scope.rb +361 -0
- data/lib/parse/live_query/circuit_breaker.rb +256 -0
- data/lib/parse/live_query/client.rb +1001 -0
- data/lib/parse/live_query/configuration.rb +224 -0
- data/lib/parse/live_query/event.rb +115 -0
- data/lib/parse/live_query/event_queue.rb +272 -0
- data/lib/parse/live_query/health_monitor.rb +214 -0
- data/lib/parse/live_query/logging.rb +149 -0
- data/lib/parse/live_query/subscription.rb +294 -0
- data/lib/parse/live_query.rb +163 -0
- data/lib/parse/lookup_rewriter.rb +445 -0
- data/lib/parse/model/acl.rb +968 -0
- data/lib/parse/model/associations/belongs_to.rb +275 -0
- data/lib/parse/model/associations/collection_proxy.rb +435 -0
- data/lib/parse/model/associations/has_many.rb +597 -0
- data/lib/parse/model/associations/has_one.rb +158 -0
- data/lib/parse/model/associations/pointer_collection_proxy.rb +134 -0
- data/lib/parse/model/associations/relation_collection_proxy.rb +177 -0
- data/lib/parse/model/bytes.rb +62 -0
- data/lib/parse/model/classes/audience.rb +262 -0
- data/lib/parse/model/classes/installation.rb +363 -0
- data/lib/parse/model/classes/job_schedule.rb +153 -0
- data/lib/parse/model/classes/job_status.rb +264 -0
- data/lib/parse/model/classes/product.rb +75 -0
- data/lib/parse/model/classes/push_status.rb +263 -0
- data/lib/parse/model/classes/role.rb +751 -0
- data/lib/parse/model/classes/session.rb +201 -0
- data/lib/parse/model/classes/user.rb +943 -0
- data/lib/parse/model/clp.rb +544 -0
- data/lib/parse/model/core/actions.rb +1268 -0
- data/lib/parse/model/core/builder.rb +139 -0
- data/lib/parse/model/core/create_lock.rb +386 -0
- data/lib/parse/model/core/describe.rb +382 -0
- data/lib/parse/model/core/enhanced_change_tracking.rb +159 -0
- data/lib/parse/model/core/errors.rb +38 -0
- data/lib/parse/model/core/fetching.rb +566 -0
- data/lib/parse/model/core/field_guards.rb +220 -0
- data/lib/parse/model/core/indexing.rb +382 -0
- data/lib/parse/model/core/parse_reference.rb +407 -0
- data/lib/parse/model/core/properties.rb +809 -0
- data/lib/parse/model/core/querying.rb +491 -0
- data/lib/parse/model/core/schema.rb +202 -0
- data/lib/parse/model/core/search_indexing.rb +174 -0
- data/lib/parse/model/date.rb +88 -0
- data/lib/parse/model/email.rb +213 -0
- data/lib/parse/model/file.rb +527 -0
- data/lib/parse/model/geojson.rb +271 -0
- data/lib/parse/model/geopoint.rb +261 -0
- data/lib/parse/model/model.rb +260 -0
- data/lib/parse/model/object.rb +2068 -0
- data/lib/parse/model/phone.rb +520 -0
- data/lib/parse/model/pointer.rb +443 -0
- data/lib/parse/model/polygon.rb +406 -0
- data/lib/parse/model/push.rb +975 -0
- data/lib/parse/model/shortnames.rb +8 -0
- data/lib/parse/model/time_zone.rb +141 -0
- data/lib/parse/model/validations/uniqueness_validator.rb +97 -0
- data/lib/parse/model/validations.rb +96 -0
- data/lib/parse/mongodb.rb +2300 -0
- data/lib/parse/pipeline_security.rb +554 -0
- data/lib/parse/query/constraint.rb +198 -0
- data/lib/parse/query/constraints.rb +3279 -0
- data/lib/parse/query/cursor.rb +434 -0
- data/lib/parse/query/n_plus_one_detector.rb +445 -0
- data/lib/parse/query/operation.rb +104 -0
- data/lib/parse/query/ordering.rb +66 -0
- data/lib/parse/query.rb +7028 -0
- data/lib/parse/schema/index_migrator.rb +291 -0
- data/lib/parse/schema/search_index_migrator.rb +289 -0
- data/lib/parse/schema.rb +494 -0
- data/lib/parse/stack/generators/rails.rb +40 -0
- data/lib/parse/stack/generators/templates/model.erb +51 -0
- data/lib/parse/stack/generators/templates/model_installation.rb +4 -0
- data/lib/parse/stack/generators/templates/model_role.rb +4 -0
- data/lib/parse/stack/generators/templates/model_session.rb +4 -0
- data/lib/parse/stack/generators/templates/model_user.rb +11 -0
- data/lib/parse/stack/generators/templates/parse.rb +12 -0
- data/lib/parse/stack/generators/templates/webhooks.rb +10 -0
- data/lib/parse/stack/railtie.rb +18 -0
- data/lib/parse/stack/tasks.rb +563 -0
- data/lib/parse/stack/version.rb +11 -0
- data/lib/parse/stack.rb +455 -0
- data/lib/parse/two_factor_auth/user_extension.rb +449 -0
- data/lib/parse/two_factor_auth.rb +310 -0
- data/lib/parse/webhooks/payload.rb +360 -0
- data/lib/parse/webhooks/registration.rb +199 -0
- data/lib/parse/webhooks/replay_protection.rb +189 -0
- data/lib/parse/webhooks.rb +510 -0
- data/lib/parse-stack-next.rb +5 -0
- data/lib/parse-stack.rb +5 -0
- data/parse-stack-next.gemspec +82 -0
- data/parse-stack.png +0 -0
- data/scripts/debug-ips.js +35 -0
- data/scripts/docker/Dockerfile.parse +13 -0
- data/scripts/docker/atlas-init.js +284 -0
- data/scripts/docker/docker-compose.atlas.yml +76 -0
- data/scripts/docker/docker-compose.test.yml +106 -0
- data/scripts/docker/mongo-init.js +21 -0
- data/scripts/eval_mcp_with_lm_studio.rb +274 -0
- data/scripts/start-parse.sh +90 -0
- data/scripts/start_mcp_server.rb +78 -0
- data/scripts/test_server_connection.rb +82 -0
- metadata +377 -0
|
@@ -0,0 +1,1268 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "active_model"
|
|
5
|
+
require "active_support"
|
|
6
|
+
require "active_support/inflector"
|
|
7
|
+
require "active_support/core_ext"
|
|
8
|
+
require "time"
|
|
9
|
+
require_relative "../../client/request"
|
|
10
|
+
require_relative "fetching"
|
|
11
|
+
|
|
12
|
+
module Parse
|
|
13
|
+
class Query
|
|
14
|
+
|
|
15
|
+
# Supporting the `first_or_create` class method to be used in scope chaining with queries.
|
|
16
|
+
# @!visibility private
|
|
17
|
+
def first_or_create(query_attrs = {}, resource_attrs = {})
|
|
18
|
+
conditions(query_attrs)
|
|
19
|
+
klass = Parse::Model.find_class self.table
|
|
20
|
+
if klass.blank?
|
|
21
|
+
raise ArgumentError, "Parse model with class name #{self.table} is not registered."
|
|
22
|
+
end
|
|
23
|
+
hash_constraints = constraints(true)
|
|
24
|
+
klass.first_or_create(hash_constraints, resource_attrs)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Supporting the `save_all` method to be used in scope chaining with queries.
|
|
28
|
+
# @!visibility private
|
|
29
|
+
def save_all(expressions = {}, &block)
|
|
30
|
+
conditions(expressions)
|
|
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
|
+
|
|
37
|
+
klass.save_all(hash_constraints, &block) if block_given?
|
|
38
|
+
klass.save_all(hash_constraints)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# A Parse::RelationAction is special operation that adds one object to a relational
|
|
43
|
+
# table as to another. Depending on the polarity of the action, the objects are
|
|
44
|
+
# either added or removed from the relation. This class is used to generate the proper
|
|
45
|
+
# hash request formats Parse needs in order to modify relational information for classes.
|
|
46
|
+
class RelationAction
|
|
47
|
+
# @!visibility private
|
|
48
|
+
ADD = "AddRelation"
|
|
49
|
+
# @!visibility private
|
|
50
|
+
REMOVE = "RemoveRelation"
|
|
51
|
+
# @!attribute polarity
|
|
52
|
+
# @return [Boolean] whether it is an addition (true) or removal (false) action.
|
|
53
|
+
# @!attribute key
|
|
54
|
+
# @return [String] the name of the Parse field (column).
|
|
55
|
+
# @!attribute objects
|
|
56
|
+
# @return [Array<Parse::Object>] the set of objects in this relation action.
|
|
57
|
+
attr_accessor :polarity, :key, :objects
|
|
58
|
+
|
|
59
|
+
# @param field [String] the name of the Parse field tied to this relation.
|
|
60
|
+
# @param polarity [Boolean] whether this is an addition (true) or removal (false) action.
|
|
61
|
+
# @param objects [Array<Parse::Object>] the set of objects tied to this relation action.
|
|
62
|
+
def initialize(field, polarity: true, objects: [])
|
|
63
|
+
@key = field.to_s
|
|
64
|
+
self.polarity = polarity
|
|
65
|
+
@objects = Array.wrap(objects).compact
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# @return [Hash] a hash representing a relation operation.
|
|
69
|
+
def as_json(*args)
|
|
70
|
+
{ @key => {
|
|
71
|
+
"__op" => (@polarity == true ? ADD : REMOVE),
|
|
72
|
+
"objects" => objects.parse_pointers,
|
|
73
|
+
} }.as_json
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# This module is mainly all the basic orm operations. To support batching actions,
|
|
79
|
+
# we use temporary Request objects have contain the operation to be performed (in some cases).
|
|
80
|
+
# This allows to group a list of Request methods, into a batch for sending all at once to Parse.
|
|
81
|
+
module Parse
|
|
82
|
+
|
|
83
|
+
# An error raised when a save failure occurs.
|
|
84
|
+
class RecordNotSaved < StandardError
|
|
85
|
+
# @return [Parse::Object] the Parse::Object that failed to save.
|
|
86
|
+
attr_reader :object
|
|
87
|
+
|
|
88
|
+
# @param object [Parse::Object] the object that failed.
|
|
89
|
+
def initialize(object)
|
|
90
|
+
@object = object
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
module Core
|
|
95
|
+
# Defines some of the save, update and destroy operations for Parse objects.
|
|
96
|
+
module Actions
|
|
97
|
+
# @!visibility private
|
|
98
|
+
def self.included(base)
|
|
99
|
+
base.extend(ClassMethods)
|
|
100
|
+
# Per-class override for the synchronize-create lock. `nil` means
|
|
101
|
+
# "inherit from `Parse.synchronize_create_default`". Set to true/false
|
|
102
|
+
# on a subclass to force-on or force-off for that class and its
|
|
103
|
+
# descendants. Use ActiveSupport::class_attribute so inheritance works
|
|
104
|
+
# naturally; mirrors `signup_on_save` (user.rb:178) and `field_guards`
|
|
105
|
+
# (field_guards.rb:55).
|
|
106
|
+
if base.respond_to?(:class_attribute)
|
|
107
|
+
base.class_attribute :synchronize_create_default, instance_writer: false
|
|
108
|
+
base.synchronize_create_default = nil
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Class methods applied to Parse::Object subclasses.
|
|
113
|
+
module ClassMethods
|
|
114
|
+
|
|
115
|
+
# Execute a set of operations as an atomic transaction.
|
|
116
|
+
# All operations will be executed in sequence, and if any fail,
|
|
117
|
+
# the entire transaction will be rolled back.
|
|
118
|
+
#
|
|
119
|
+
# @example Basic transaction
|
|
120
|
+
# Parse::Object.transaction do |batch|
|
|
121
|
+
# user = User.first
|
|
122
|
+
# user.username = "new_username"
|
|
123
|
+
# batch.add(user)
|
|
124
|
+
#
|
|
125
|
+
# post = Post.new(author: user, title: "New Post")
|
|
126
|
+
# batch.add(post)
|
|
127
|
+
# end
|
|
128
|
+
#
|
|
129
|
+
# @example Using the block return for automatic batching
|
|
130
|
+
# results = Parse::Object.transaction do
|
|
131
|
+
# user1 = User.first
|
|
132
|
+
# user1.score = 100
|
|
133
|
+
#
|
|
134
|
+
# user2 = User.first(username: "player2")
|
|
135
|
+
# user2.score = 200
|
|
136
|
+
#
|
|
137
|
+
# [user1, user2] # Return array of objects to save
|
|
138
|
+
# end
|
|
139
|
+
#
|
|
140
|
+
# @param retries [Integer] number of times to retry on transaction conflict (error 251)
|
|
141
|
+
# @yield [Parse::BatchOperation] the batch operation to add requests to
|
|
142
|
+
# @return [Array<Parse::Response>] the responses from the transaction
|
|
143
|
+
# @raise [Parse::Error] if the transaction fails
|
|
144
|
+
def transaction(retries: 5, &block)
|
|
145
|
+
raise ArgumentError, "Block required for transaction" unless block_given?
|
|
146
|
+
|
|
147
|
+
batch = Parse::BatchOperation.new(nil, transaction: true)
|
|
148
|
+
|
|
149
|
+
# Store original state of objects for rollback
|
|
150
|
+
original_states = {}
|
|
151
|
+
tracked_objects = []
|
|
152
|
+
|
|
153
|
+
# Wrap the batch to capture objects being added
|
|
154
|
+
batch_wrapper = Object.new
|
|
155
|
+
batch_wrapper.define_singleton_method(:is_a?) do |klass|
|
|
156
|
+
klass == Parse::BatchOperation || super(klass)
|
|
157
|
+
end
|
|
158
|
+
batch_wrapper.define_singleton_method(:kind_of?) do |klass|
|
|
159
|
+
klass == Parse::BatchOperation || super(klass)
|
|
160
|
+
end
|
|
161
|
+
batch_wrapper.define_singleton_method(:instance_of?) do |klass|
|
|
162
|
+
klass == Parse::BatchOperation
|
|
163
|
+
end
|
|
164
|
+
batch_wrapper.define_singleton_method(:add) do |obj|
|
|
165
|
+
# Store original state when object is first added to transaction.
|
|
166
|
+
# Use obj.object_id (Ruby identity) as the key because Parse::Object#hash
|
|
167
|
+
# and #eql? treat all unsaved objects (nil id) as equal, which would cause
|
|
168
|
+
# only the first unsaved object to be tracked.
|
|
169
|
+
if obj.respond_to?(:attributes) && obj.respond_to?(:id) && !original_states.key?(obj.object_id)
|
|
170
|
+
original_states[obj.object_id] = {
|
|
171
|
+
object: obj,
|
|
172
|
+
attributes: obj.attributes.dup,
|
|
173
|
+
changed_attributes: obj.instance_variable_get(:@changed_attributes)&.dup || {},
|
|
174
|
+
id: obj.id,
|
|
175
|
+
mutations_from_database: obj.instance_variable_get(:@mutations_from_database),
|
|
176
|
+
mutations_before_last_save: obj.instance_variable_get(:@mutations_before_last_save),
|
|
177
|
+
}
|
|
178
|
+
tracked_objects << obj
|
|
179
|
+
end
|
|
180
|
+
batch.add(obj)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Forward other methods to the real batch
|
|
184
|
+
batch_wrapper.define_singleton_method(:method_missing) do |method, *args, &block|
|
|
185
|
+
batch.send(method, *args, &block)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
result = yield(batch_wrapper)
|
|
189
|
+
|
|
190
|
+
# If block returns objects, add them to batch
|
|
191
|
+
if result.respond_to?(:change_requests)
|
|
192
|
+
batch_wrapper.add(result)
|
|
193
|
+
elsif result.is_a?(Array)
|
|
194
|
+
result.each { |obj| batch_wrapper.add(obj) if obj.respond_to?(:change_requests) }
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Submit with retry logic for transaction conflicts
|
|
198
|
+
attempts = 0
|
|
199
|
+
begin
|
|
200
|
+
attempts += 1
|
|
201
|
+
responses = batch.submit
|
|
202
|
+
|
|
203
|
+
# Check for success
|
|
204
|
+
if responses.all?(&:success?)
|
|
205
|
+
# Update tracked objects with data from successful responses
|
|
206
|
+
# Match responses to objects using the request tag (Ruby object_id)
|
|
207
|
+
# Build hash lookup once for O(n) instead of O(n²) linear search
|
|
208
|
+
objects_by_id = tracked_objects.each_with_object({}) { |o, h| h[o.object_id] = o }
|
|
209
|
+
requests = batch.requests
|
|
210
|
+
requests.zip(responses).each do |request, response|
|
|
211
|
+
next unless request && response && response.success?
|
|
212
|
+
result = response.result
|
|
213
|
+
next unless result.is_a?(Hash)
|
|
214
|
+
|
|
215
|
+
# Find the object matching this request's tag
|
|
216
|
+
obj = objects_by_id[request.tag]
|
|
217
|
+
next unless obj
|
|
218
|
+
|
|
219
|
+
# Update object with response data (objectId, createdAt, updatedAt)
|
|
220
|
+
if result["objectId"]
|
|
221
|
+
obj.instance_variable_set(:@id, result["objectId"])
|
|
222
|
+
end
|
|
223
|
+
if result["createdAt"]
|
|
224
|
+
obj.instance_variable_set(:@created_at, Parse::Date.parse(result["createdAt"]))
|
|
225
|
+
end
|
|
226
|
+
if result["updatedAt"]
|
|
227
|
+
obj.instance_variable_set(:@updated_at, Parse::Date.parse(result["updatedAt"]))
|
|
228
|
+
elsif result["createdAt"]
|
|
229
|
+
obj.instance_variable_set(:@updated_at, Parse::Date.parse(result["createdAt"]))
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# Apply any additional attributes returned by beforeSave hooks
|
|
233
|
+
obj.set_attributes!(result) if obj.respond_to?(:set_attributes!)
|
|
234
|
+
|
|
235
|
+
# Clear change tracking since save was successful
|
|
236
|
+
obj.send(:clear_changes!) if obj.respond_to?(:clear_changes!, true)
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
return responses
|
|
240
|
+
else
|
|
241
|
+
# Find first error
|
|
242
|
+
error_response = responses.find { |r| !r.success? }
|
|
243
|
+
|
|
244
|
+
# Rollback local object states
|
|
245
|
+
original_states.each_value do |state|
|
|
246
|
+
obj = state[:object]
|
|
247
|
+
obj.instance_variable_set(:@attributes, state[:attributes])
|
|
248
|
+
obj.instance_variable_set(:@changed_attributes, state[:changed_attributes])
|
|
249
|
+
obj.instance_variable_set(:@id, state[:id])
|
|
250
|
+
# Restore change tracking state
|
|
251
|
+
obj.instance_variable_set(:@mutations_from_database, state[:mutations_from_database])
|
|
252
|
+
obj.instance_variable_set(:@mutations_before_last_save, state[:mutations_before_last_save])
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
raise Parse::Error, "Transaction failed: #{error_response.error}"
|
|
256
|
+
end
|
|
257
|
+
rescue Parse::Error => e
|
|
258
|
+
# Retry on transaction conflict (error code 251)
|
|
259
|
+
if e.message.include?("251") && attempts < retries
|
|
260
|
+
sleep(0.1 * attempts) # Exponential backoff
|
|
261
|
+
retry
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# Rollback local object states on final failure
|
|
265
|
+
original_states.each_value do |state|
|
|
266
|
+
obj = state[:object]
|
|
267
|
+
obj.instance_variable_set(:@attributes, state[:attributes])
|
|
268
|
+
obj.instance_variable_set(:@changed_attributes, state[:changed_attributes])
|
|
269
|
+
obj.instance_variable_set(:@id, state[:id])
|
|
270
|
+
# Restore change tracking state
|
|
271
|
+
obj.instance_variable_set(:@mutations_from_database, state[:mutations_from_database])
|
|
272
|
+
obj.instance_variable_set(:@mutations_before_last_save, state[:mutations_before_last_save])
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
raise e
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# @!attribute raise_on_save_failure
|
|
280
|
+
# By default, we return `true` or `false` for save and destroy operations.
|
|
281
|
+
# If you prefer to have `Parse::Object` raise an exception instead, you
|
|
282
|
+
# can tell to do so either globally or on a per-model basis. When a save
|
|
283
|
+
# fails, it will raise a {Parse::RecordNotSaved}.
|
|
284
|
+
#
|
|
285
|
+
# When enabled, if an error is returned by Parse due to saving or
|
|
286
|
+
# destroying a record, due to your `before_save` or `before_delete`
|
|
287
|
+
# validation cloud code triggers, `Parse::Object` will return the a
|
|
288
|
+
# {Parse::RecordNotSaved} exception type. This exception has an instance
|
|
289
|
+
# method of `#object` which contains the object that failed to save.
|
|
290
|
+
# @example
|
|
291
|
+
# # globally across all models
|
|
292
|
+
# Parse::Model.raise_on_save_failure = true
|
|
293
|
+
# Song.raise_on_save_failure = true # per-model
|
|
294
|
+
#
|
|
295
|
+
# # or per-instance raise on failure
|
|
296
|
+
# song.save!
|
|
297
|
+
#
|
|
298
|
+
# @return [Boolean] whether to raise a {Parse::RecordNotSaved}
|
|
299
|
+
# when an object fails to save.
|
|
300
|
+
attr_writer :raise_on_save_failure
|
|
301
|
+
|
|
302
|
+
def raise_on_save_failure
|
|
303
|
+
return @raise_on_save_failure unless @raise_on_save_failure.nil?
|
|
304
|
+
Parse::Model.raise_on_save_failure
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# Finds the first object matching the query conditions, or creates a new
|
|
308
|
+
# unsaved object with the attributes. This method takes the possibility of two hashes,
|
|
309
|
+
# therefore make sure you properly wrap the contents of the input with `{}`.
|
|
310
|
+
# @example
|
|
311
|
+
# Parse::User.first_or_create({ ..query conditions..})
|
|
312
|
+
# Parse::User.first_or_create({ ..query conditions..}, {.. resource_attrs ..})
|
|
313
|
+
# @param query_attrs [Hash] a set of query constraints that also are applied.
|
|
314
|
+
# @param resource_attrs [Hash] a set of additional attribute values to be applied only if an object was not found.
|
|
315
|
+
# @return [Parse::Object] a Parse::Object, whether found by the query or newly created.
|
|
316
|
+
def first_or_create(query_attrs = {}, resource_attrs = {})
|
|
317
|
+
query_attrs = query_attrs.symbolize_keys
|
|
318
|
+
resource_attrs = resource_attrs.symbolize_keys
|
|
319
|
+
obj = query(query_attrs).first
|
|
320
|
+
|
|
321
|
+
if obj.blank?
|
|
322
|
+
# Object not found, create new one with query_attrs + resource_attrs
|
|
323
|
+
merged_attrs = query_attrs.merge(resource_attrs)
|
|
324
|
+
obj = self.new merged_attrs
|
|
325
|
+
end
|
|
326
|
+
# If object exists, return it as-is without any modifications
|
|
327
|
+
|
|
328
|
+
obj
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
# Finds the first object matching the query conditions, or creates a new
|
|
332
|
+
# *saved* object with the attributes. This method is similar to {first_or_create}
|
|
333
|
+
# but will also {save!} the object if it was newly created.
|
|
334
|
+
#
|
|
335
|
+
# When `synchronize:` is enabled (per-call, per-class via
|
|
336
|
+
# `synchronize_create_default`, or globally via
|
|
337
|
+
# `Parse.synchronize_create_default`), the find→create→save sequence is
|
|
338
|
+
# serialized through {Parse::CreateLock} so concurrent callers
|
|
339
|
+
# with identical `query_attrs` cannot both create. The lock requires a
|
|
340
|
+
# Moneta cache store (Redis recommended); on a process-local store the
|
|
341
|
+
# lock degrades to a per-key Mutex. A MongoDB unique index on the
|
|
342
|
+
# constrained fields is the correctness floor — on Parse code 137
|
|
343
|
+
# (DuplicateValue) the wrapper re-queries inside the held lock and
|
|
344
|
+
# returns the winner.
|
|
345
|
+
#
|
|
346
|
+
# @example
|
|
347
|
+
# obj = Parse::User.first_or_create!({ ..query conditions..})
|
|
348
|
+
# obj = Parse::User.first_or_create!({ ..query conditions..}, {.. resource_attrs ..})
|
|
349
|
+
# @example Per-call lock opt-in
|
|
350
|
+
# User.first_or_create!({ email: e }, { name: n }, synchronize: true)
|
|
351
|
+
# @example Per-call with tuning
|
|
352
|
+
# User.first_or_create!({ email: e }, {}, synchronize: { ttl: 5, wait: 1.0 })
|
|
353
|
+
# @example Auth-context threading
|
|
354
|
+
# User.first_or_create!({ email: e }, {}, session: current_user.session_token)
|
|
355
|
+
# @param query_attrs [Hash] a set of query constraints that also are applied.
|
|
356
|
+
# @param resource_attrs [Hash] a set of attribute values to be applied if an object was not found.
|
|
357
|
+
# @param synchronize [Boolean, Hash, nil] override the synchronize-create lock. `nil` (default) defers
|
|
358
|
+
# to the per-class `synchronize_create_default` or the module-level `Parse.synchronize_create_default`.
|
|
359
|
+
# `true` enables with defaults; `false` opts out; a Hash enables with custom options merged over
|
|
360
|
+
# `Parse.synchronize_create_options`.
|
|
361
|
+
# @param session [String, Parse::User, nil] session token (or object answering :session_token) threaded
|
|
362
|
+
# through both the query and the save so the entire find→create flow runs under one auth identity.
|
|
363
|
+
# @param master_key [Boolean, nil] when explicitly `false`, disables master key for both halves.
|
|
364
|
+
# @return [Parse::Object] a Parse::Object, whether found by the query or newly created.
|
|
365
|
+
# @raise {Parse::RecordNotSaved} if the save fails
|
|
366
|
+
# @raise {Parse::CreateLockTimeoutError} when synchronized and the wait budget is exceeded
|
|
367
|
+
# @raise {Parse::CreateLockInvalidKey} when query_attrs cannot be canonicalized for a stable lock key
|
|
368
|
+
# @see #first_or_create
|
|
369
|
+
def first_or_create!(query_attrs = {}, resource_attrs = {}, synchronize: nil, session: nil, master_key: nil)
|
|
370
|
+
query_attrs = query_attrs.symbolize_keys
|
|
371
|
+
resource_attrs = resource_attrs.symbolize_keys
|
|
372
|
+
|
|
373
|
+
enabled, sync_opts = _resolve_synchronize_flag(synchronize)
|
|
374
|
+
return _first_or_create_unsynchronized!(query_attrs, resource_attrs, session: session, master_key: master_key) unless enabled
|
|
375
|
+
|
|
376
|
+
_assert_synchronize_class_allowed!
|
|
377
|
+
options = _merged_synchronize_options(sync_opts)
|
|
378
|
+
session_token = _extract_session_token(session)
|
|
379
|
+
|
|
380
|
+
# Split query_attrs into the constraint subset (what
|
|
381
|
+
# determines lock identity) and the query-shape options
|
|
382
|
+
# (`:cache`, `:limit`, `:order`, ACL helpers, …) that
|
|
383
|
+
# `Parse::Query#conditions` absorbs as query parameters.
|
|
384
|
+
# Without this, a caller passing the documented `cache:
|
|
385
|
+
# 30.seconds` escape hatch alongside their constraints
|
|
386
|
+
# tripped `Parse::CreateLock.canonicalize_value` on the
|
|
387
|
+
# `ActiveSupport::Duration` — see 4.4.2 changelog. The
|
|
388
|
+
# original `query_attrs` is still forwarded to
|
|
389
|
+
# `_scoped_first` below; `conditions()` extracts the option
|
|
390
|
+
# keys on the find side, so the cache TTL still applies.
|
|
391
|
+
lock_attrs = query_attrs.reject { |k, _| Parse::Query.option_key?(k) }
|
|
392
|
+
_assert_lock_attrs_have_constraints!(query_attrs, lock_attrs)
|
|
393
|
+
|
|
394
|
+
Parse::CreateLock.synchronize(
|
|
395
|
+
parse_class: parse_class,
|
|
396
|
+
query_attrs: lock_attrs,
|
|
397
|
+
options: options,
|
|
398
|
+
session_token: session_token,
|
|
399
|
+
master_key: master_key,
|
|
400
|
+
) do
|
|
401
|
+
obj = _scoped_first(query_attrs, session: session, master_key: master_key)
|
|
402
|
+
next obj if obj
|
|
403
|
+
|
|
404
|
+
obj = self.new query_attrs.merge(resource_attrs)
|
|
405
|
+
begin
|
|
406
|
+
session ? obj.save!(session: session) : obj.save!
|
|
407
|
+
obj
|
|
408
|
+
rescue Parse::RecordNotSaved => e
|
|
409
|
+
winner = _recover_from_duplicate_value(e, query_attrs, session: session, master_key: master_key)
|
|
410
|
+
raise unless winner
|
|
411
|
+
winner
|
|
412
|
+
end
|
|
413
|
+
end
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
# Creates a new object with the given attributes and saves it.
|
|
417
|
+
# This is equivalent to calling `new(attrs).save!`.
|
|
418
|
+
# @example
|
|
419
|
+
# song = Song.create!(title: "New Song", artist: "Artist")
|
|
420
|
+
# @param attrs [Hash] the attributes for the new object.
|
|
421
|
+
# @return [Parse::Object] the newly created and saved object.
|
|
422
|
+
# @raise {Parse::RecordNotSaved} if the save fails
|
|
423
|
+
def create!(attrs = {})
|
|
424
|
+
obj = new(attrs)
|
|
425
|
+
obj.save!
|
|
426
|
+
obj
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
# Finds the first object matching the query conditions and updates it with the attributes,
|
|
430
|
+
# or creates a new *saved* object with the attributes. Saves new objects or existing objects with changes.
|
|
431
|
+
# See {#first_or_create!} for the synchronize-create lock semantics — they apply identically here.
|
|
432
|
+
# @example
|
|
433
|
+
# Parse::User.create_or_update!({ ..query conditions..}, {.. resource_attrs ..})
|
|
434
|
+
# @param query_attrs [Hash] a set of query constraints that also are applied.
|
|
435
|
+
# @param resource_attrs [Hash] a set of attribute values to be applied to found objects or used for creation.
|
|
436
|
+
# @param synchronize (see #first_or_create!)
|
|
437
|
+
# @param session (see #first_or_create!)
|
|
438
|
+
# @param master_key (see #first_or_create!)
|
|
439
|
+
# @return [Parse::Object] a Parse::Object, whether found by the query or newly created.
|
|
440
|
+
# @raise {Parse::RecordNotSaved} if the save fails
|
|
441
|
+
def create_or_update!(query_attrs = {}, resource_attrs = {}, synchronize: nil, session: nil, master_key: nil)
|
|
442
|
+
query_attrs = query_attrs.symbolize_keys
|
|
443
|
+
resource_attrs = resource_attrs.symbolize_keys
|
|
444
|
+
|
|
445
|
+
enabled, sync_opts = _resolve_synchronize_flag(synchronize)
|
|
446
|
+
return _create_or_update_unsynchronized!(query_attrs, resource_attrs, session: session, master_key: master_key) unless enabled
|
|
447
|
+
|
|
448
|
+
_assert_synchronize_class_allowed!
|
|
449
|
+
options = _merged_synchronize_options(sync_opts)
|
|
450
|
+
session_token = _extract_session_token(session)
|
|
451
|
+
|
|
452
|
+
# See #first_or_create! for the partition rationale — strip
|
|
453
|
+
# Parse::Query option keys before lock canonicalization.
|
|
454
|
+
lock_attrs = query_attrs.reject { |k, _| Parse::Query.option_key?(k) }
|
|
455
|
+
_assert_lock_attrs_have_constraints!(query_attrs, lock_attrs)
|
|
456
|
+
|
|
457
|
+
Parse::CreateLock.synchronize(
|
|
458
|
+
parse_class: parse_class,
|
|
459
|
+
query_attrs: lock_attrs,
|
|
460
|
+
options: options,
|
|
461
|
+
session_token: session_token,
|
|
462
|
+
master_key: master_key,
|
|
463
|
+
) do
|
|
464
|
+
obj = _scoped_first(query_attrs, session: session, master_key: master_key)
|
|
465
|
+
|
|
466
|
+
if obj.nil?
|
|
467
|
+
obj = self.new query_attrs.merge(resource_attrs)
|
|
468
|
+
begin
|
|
469
|
+
session ? obj.save!(session: session) : obj.save!
|
|
470
|
+
rescue Parse::RecordNotSaved => e
|
|
471
|
+
winner = _recover_from_duplicate_value(e, query_attrs, session: session, master_key: master_key)
|
|
472
|
+
raise unless winner
|
|
473
|
+
obj = winner
|
|
474
|
+
end
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
if !obj.new? && !resource_attrs.empty?
|
|
478
|
+
has_changes = resource_attrs.any? do |key, value|
|
|
479
|
+
obj.respond_to?(key) && obj.send(key) != value
|
|
480
|
+
end
|
|
481
|
+
if has_changes
|
|
482
|
+
obj.apply_attributes!(resource_attrs, dirty_track: true)
|
|
483
|
+
session ? obj.save!(session: session) : obj.save!
|
|
484
|
+
end
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
obj
|
|
488
|
+
end
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
# @!visibility private
|
|
492
|
+
# Resolves the per-call synchronize kwarg against per-class and module
|
|
493
|
+
# defaults. Returns [enabled?, options_hash].
|
|
494
|
+
#
|
|
495
|
+
# Precedence (most specific wins):
|
|
496
|
+
# per-call true/false → per-class default → Parse.synchronize_create_default
|
|
497
|
+
# A Hash kwarg implies `true` with custom options. `nil` defers up the chain.
|
|
498
|
+
def _resolve_synchronize_flag(synchronize)
|
|
499
|
+
case synchronize
|
|
500
|
+
when true
|
|
501
|
+
[true, {}]
|
|
502
|
+
when false
|
|
503
|
+
[false, {}]
|
|
504
|
+
when Hash
|
|
505
|
+
[true, synchronize]
|
|
506
|
+
when nil
|
|
507
|
+
cls_default = respond_to?(:synchronize_create_default) ? synchronize_create_default : nil
|
|
508
|
+
case cls_default
|
|
509
|
+
when true
|
|
510
|
+
[true, {}]
|
|
511
|
+
when false
|
|
512
|
+
[false, {}]
|
|
513
|
+
when Hash
|
|
514
|
+
[true, cls_default]
|
|
515
|
+
else
|
|
516
|
+
[Parse.synchronize_create_default ? true : false, {}]
|
|
517
|
+
end
|
|
518
|
+
else
|
|
519
|
+
raise ArgumentError, "synchronize: must be true, false, nil, or an options Hash (got #{synchronize.class})"
|
|
520
|
+
end
|
|
521
|
+
end
|
|
522
|
+
|
|
523
|
+
# @!visibility private
|
|
524
|
+
def _merged_synchronize_options(per_call)
|
|
525
|
+
base = Parse.synchronize_create_options || {}
|
|
526
|
+
base.merge(per_call || {})
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
# @!visibility private
|
|
530
|
+
# Enforce {Parse.synchronize_classes} allowlist. Inheritance is
|
|
531
|
+
# **transitive** for Class entries (`self <= entry`): allowlisting
|
|
532
|
+
# `User` automatically allowlists every subclass of `User`. To gate
|
|
533
|
+
# per-class without inheritance, pass entries as Strings — they
|
|
534
|
+
# match only `self.name` / `parse_class` literally. See the
|
|
535
|
+
# `Parse.synchronize_classes` docstring in lib/parse/stack.rb.
|
|
536
|
+
def _assert_synchronize_class_allowed!
|
|
537
|
+
allowlist = Parse.synchronize_classes
|
|
538
|
+
return if allowlist.nil? || allowlist.empty?
|
|
539
|
+
allowed = allowlist.any? do |entry|
|
|
540
|
+
(entry.is_a?(Class) && self <= entry) ||
|
|
541
|
+
entry.to_s == self.name ||
|
|
542
|
+
entry.to_s == parse_class
|
|
543
|
+
end
|
|
544
|
+
return if allowed
|
|
545
|
+
raise Parse::CreateLockUnavailableError,
|
|
546
|
+
"#{self} is not in Parse.synchronize_classes allowlist; either add it or pass synchronize: false"
|
|
547
|
+
end
|
|
548
|
+
|
|
549
|
+
# @!visibility private
|
|
550
|
+
# Confirm that after partitioning query_attrs into
|
|
551
|
+
# constraints + query-shape options, at least one constraint
|
|
552
|
+
# remained. If not, raise a specific error explaining the
|
|
553
|
+
# likely mistake before `Parse::CreateLock.synchronize` does
|
|
554
|
+
# — its generic "non-empty query_attrs" message would mislead
|
|
555
|
+
# the caller who can see a non-empty `query_attrs` argument
|
|
556
|
+
# right there in their code.
|
|
557
|
+
#
|
|
558
|
+
# Two distinguished cases:
|
|
559
|
+
# - `query_attrs` was empty to begin with → generic empty
|
|
560
|
+
# error (the user really did pass nothing).
|
|
561
|
+
# - `query_attrs` was non-empty but every key was a query
|
|
562
|
+
# option (`:cache`, `:limit`, …) → specific error naming the
|
|
563
|
+
# partitioned-out keys so the user can fix their call.
|
|
564
|
+
def _assert_lock_attrs_have_constraints!(query_attrs, lock_attrs)
|
|
565
|
+
return unless lock_attrs.empty?
|
|
566
|
+
if query_attrs.empty?
|
|
567
|
+
raise Parse::CreateLockInvalidKey,
|
|
568
|
+
"synchronize requires at least one constraint key in query_attrs (got an empty Hash)"
|
|
569
|
+
end
|
|
570
|
+
option_keys = query_attrs.keys.select { |k| Parse::Query.option_key?(k) }
|
|
571
|
+
raise Parse::CreateLockInvalidKey,
|
|
572
|
+
"synchronize requires at least one constraint key in query_attrs; " \
|
|
573
|
+
"every key passed (#{option_keys.inspect}) is a Parse::Query option " \
|
|
574
|
+
"(`:cache`, `:limit`, `:order`, ACL helpers, …) and is partitioned " \
|
|
575
|
+
"out of the lock identity. Add a constraint key (e.g. the unique " \
|
|
576
|
+
"field your callsite is finding-or-creating against), or pass " \
|
|
577
|
+
"`synchronize: false` if you don't need cross-process locking."
|
|
578
|
+
end
|
|
579
|
+
|
|
580
|
+
# @!visibility private
|
|
581
|
+
# Extract a session token string from either a String or an object
|
|
582
|
+
# answering :session_token (e.g. Parse::User, Parse::Session). Returns
|
|
583
|
+
# nil when session is nil so the canonical lock key picks the
|
|
584
|
+
# "default" auth-context marker.
|
|
585
|
+
def _extract_session_token(session)
|
|
586
|
+
return nil if session.nil?
|
|
587
|
+
return session if session.is_a?(String)
|
|
588
|
+
return session.session_token if session.respond_to?(:session_token)
|
|
589
|
+
raise ArgumentError, "session: must be a String token or an object responding to :session_token (got #{session.class})"
|
|
590
|
+
end
|
|
591
|
+
|
|
592
|
+
# @!visibility private
|
|
593
|
+
# Run `query(constraints).first` with explicit auth scoping so the
|
|
594
|
+
# synchronized find runs under the same identity as the subsequent
|
|
595
|
+
# save. When session/master_key are unset, falls through to the
|
|
596
|
+
# client default exactly as the legacy non-synchronized path.
|
|
597
|
+
def _scoped_first(query_attrs, session: nil, master_key: nil)
|
|
598
|
+
q = query(query_attrs)
|
|
599
|
+
if session
|
|
600
|
+
q.session_token = session.respond_to?(:session_token) ? session.session_token : session
|
|
601
|
+
end
|
|
602
|
+
q.use_master_key = master_key unless master_key.nil?
|
|
603
|
+
q.first
|
|
604
|
+
end
|
|
605
|
+
|
|
606
|
+
# @!visibility private
|
|
607
|
+
# When a save inside the lock fails with Parse code 137 (DuplicateValue),
|
|
608
|
+
# re-query inside the still-held lock and return the row that won the
|
|
609
|
+
# race. Returns nil when the error was something other than 137 or the
|
|
610
|
+
# winner cannot be located. The caller raises the original exception in
|
|
611
|
+
# the nil case.
|
|
612
|
+
def _recover_from_duplicate_value(error, query_attrs, session: nil, master_key: nil)
|
|
613
|
+
obj = error.respond_to?(:object) ? error.object : nil
|
|
614
|
+
return nil unless obj
|
|
615
|
+
res = obj.instance_variable_get(:@_last_response)
|
|
616
|
+
return nil unless res && res.respond_to?(:code) && res.code == Parse::Client::DuplicateValueError::CODE
|
|
617
|
+
_scoped_first(query_attrs, session: session, master_key: master_key)
|
|
618
|
+
end
|
|
619
|
+
|
|
620
|
+
# @!visibility private
|
|
621
|
+
# The pre-synchronize behavior of `first_or_create!`, factored out so
|
|
622
|
+
# the synchronize wrapper can short-circuit when disabled. Preserves
|
|
623
|
+
# the legacy contract: query → build → save! if new.
|
|
624
|
+
def _first_or_create_unsynchronized!(query_attrs, resource_attrs, session: nil, master_key: nil)
|
|
625
|
+
obj = _scoped_first(query_attrs, session: session, master_key: master_key)
|
|
626
|
+
if obj.nil?
|
|
627
|
+
obj = self.new query_attrs.merge(resource_attrs)
|
|
628
|
+
end
|
|
629
|
+
if obj.new?
|
|
630
|
+
session ? obj.save!(session: session) : obj.save!
|
|
631
|
+
end
|
|
632
|
+
obj
|
|
633
|
+
end
|
|
634
|
+
|
|
635
|
+
# @!visibility private
|
|
636
|
+
def _create_or_update_unsynchronized!(query_attrs, resource_attrs, session: nil, master_key: nil)
|
|
637
|
+
obj = _scoped_first(query_attrs, session: session, master_key: master_key)
|
|
638
|
+
if obj.nil?
|
|
639
|
+
obj = self.new query_attrs.merge(resource_attrs)
|
|
640
|
+
session ? obj.save!(session: session) : obj.save!
|
|
641
|
+
elsif !resource_attrs.empty?
|
|
642
|
+
has_changes = resource_attrs.any? do |key, value|
|
|
643
|
+
obj.respond_to?(key) && obj.send(key) != value
|
|
644
|
+
end
|
|
645
|
+
if has_changes
|
|
646
|
+
obj.apply_attributes!(resource_attrs, dirty_track: true)
|
|
647
|
+
session ? obj.save!(session: session) : obj.save!
|
|
648
|
+
end
|
|
649
|
+
end
|
|
650
|
+
obj
|
|
651
|
+
end
|
|
652
|
+
|
|
653
|
+
# Auto save all objects matching the query constraints. This method is
|
|
654
|
+
# meant to be used with a block. Any objects that are modified in the block
|
|
655
|
+
# will be batched for a save operation. This uses the `updated_at` field to
|
|
656
|
+
# continue to query for all matching objects that have not been updated.
|
|
657
|
+
# If you need to use `:updated_at` in your constraints, consider using {Parse::Core::Querying#all} or
|
|
658
|
+
# {Parse::Core::Querying#each}
|
|
659
|
+
# @param constraints [Hash] a set of query constraints.
|
|
660
|
+
# @yield a block which will iterate through each matching object.
|
|
661
|
+
# @example
|
|
662
|
+
#
|
|
663
|
+
# post = Post.first
|
|
664
|
+
# Comments.save_all( post: post) do |comment|
|
|
665
|
+
# # .. modify comment ...
|
|
666
|
+
# # it will automatically be saved
|
|
667
|
+
# end
|
|
668
|
+
# @note You cannot use *:updated_at* as a constraint.
|
|
669
|
+
# @return [Boolean] true if all saves succeeded and there were no errors.
|
|
670
|
+
def save_all(constraints = {}, &block)
|
|
671
|
+
invalid_constraints = constraints.keys.any? do |k|
|
|
672
|
+
(k == :updated_at || k == :updatedAt) ||
|
|
673
|
+
(k.is_a?(Parse::Operation) && (k.operand == :updated_at || k.operand == :updatedAt))
|
|
674
|
+
end
|
|
675
|
+
if invalid_constraints
|
|
676
|
+
raise ArgumentError,
|
|
677
|
+
"[#{self}] Special method save_all() cannot be used with an :updated_at constraint."
|
|
678
|
+
end
|
|
679
|
+
|
|
680
|
+
force = false
|
|
681
|
+
batch_size = 250
|
|
682
|
+
iterator_block = nil
|
|
683
|
+
if block_given?
|
|
684
|
+
iterator_block = block
|
|
685
|
+
force ||= false
|
|
686
|
+
else
|
|
687
|
+
# if no block given, assume you want to just save all objects
|
|
688
|
+
# regardless of modification.
|
|
689
|
+
force = true
|
|
690
|
+
end
|
|
691
|
+
# Only generate the comparison block once.
|
|
692
|
+
# updated_comparison_block = Proc.new { |x| x.updated_at }
|
|
693
|
+
|
|
694
|
+
anchor_date = Parse::Date.now
|
|
695
|
+
constraints.merge! :updated_at.on_or_before => anchor_date
|
|
696
|
+
constraints.merge! cache: false
|
|
697
|
+
# oldest first, so we create a reduction-cycle
|
|
698
|
+
constraints.merge! order: :updated_at.asc, limit: batch_size
|
|
699
|
+
update_query = query(constraints)
|
|
700
|
+
#puts "Setting Anchor Date: #{anchor_date}"
|
|
701
|
+
cursor = nil
|
|
702
|
+
has_errors = false
|
|
703
|
+
loop do
|
|
704
|
+
results = update_query.results
|
|
705
|
+
|
|
706
|
+
break if results.empty?
|
|
707
|
+
|
|
708
|
+
# verify we didn't get duplicates fetches
|
|
709
|
+
if cursor.is_a?(Parse::Object) && results.any? { |x| x.id == cursor.id }
|
|
710
|
+
warn "[#{self}.save_all] Unbounded update detected with id #{cursor.id}."
|
|
711
|
+
has_errors = true
|
|
712
|
+
break cursor
|
|
713
|
+
end
|
|
714
|
+
|
|
715
|
+
results.each(&iterator_block) if iterator_block.present?
|
|
716
|
+
# we don't need to refresh the objects in the array with the results
|
|
717
|
+
# since we will be throwing them away. Force determines whether
|
|
718
|
+
# to save these objects regardless of whether they are dirty.
|
|
719
|
+
batch = results.save(merge: false, force: force)
|
|
720
|
+
|
|
721
|
+
# faster version assuming sorting order wasn't messed up
|
|
722
|
+
cursor = results.last
|
|
723
|
+
# slower version, but more accurate
|
|
724
|
+
# cursor_item = results.max_by(&updated_comparison_block).updated_at
|
|
725
|
+
# puts "[Parse::SaveAll] Updated #{results.count} records updated <= #{cursor.updated_at}"
|
|
726
|
+
|
|
727
|
+
break if results.count < batch_size # we didn't hit a cap on results.
|
|
728
|
+
if cursor.is_a?(Parse::Object)
|
|
729
|
+
update_query.where :updated_at.gte => cursor.updated_at
|
|
730
|
+
|
|
731
|
+
if cursor.updated_at.present? && cursor.updated_at > anchor_date
|
|
732
|
+
warn "[#{self}.save_all] Reached anchor date #{anchor_date} < #{cursor.updated_at}"
|
|
733
|
+
break cursor
|
|
734
|
+
end
|
|
735
|
+
end
|
|
736
|
+
|
|
737
|
+
has_errors ||= batch.error?
|
|
738
|
+
end
|
|
739
|
+
not has_errors
|
|
740
|
+
end
|
|
741
|
+
end # ClassMethods
|
|
742
|
+
|
|
743
|
+
# Perform an atomic operation on this field. This operation is done on the
|
|
744
|
+
# Parse server which guarantees the atomicity of the operation. This is the low-level
|
|
745
|
+
# API on performing atomic operations on properties for classes. These methods do not
|
|
746
|
+
# update the current instance with any changes the server may have made to satisfy this
|
|
747
|
+
# operation.
|
|
748
|
+
#
|
|
749
|
+
# @param field [String] the name of the field in the Parse collection.
|
|
750
|
+
# @param op_hash [Hash] The operation hash. It may also be of type {Parse::RelationAction}.
|
|
751
|
+
# @return [Boolean] whether the operation was successful.
|
|
752
|
+
def operate_field!(field, op_hash)
|
|
753
|
+
field = field.to_sym
|
|
754
|
+
field = self.field_map[field] || field
|
|
755
|
+
if op_hash.is_a?(Parse::RelationAction)
|
|
756
|
+
op_hash = op_hash.as_json
|
|
757
|
+
else
|
|
758
|
+
op_hash = { field => op_hash }.as_json
|
|
759
|
+
end
|
|
760
|
+
|
|
761
|
+
# If the object hasn't been saved yet (no id), we can't make field operations
|
|
762
|
+
# Return true to indicate the operation was "successful" locally
|
|
763
|
+
return true if id.nil?
|
|
764
|
+
|
|
765
|
+
response = client.update_object(parse_class, id, op_hash, session_token: _session_token)
|
|
766
|
+
if response.error?
|
|
767
|
+
puts "[#{parse_class}:#{field} Operation] #{response.error}"
|
|
768
|
+
end
|
|
769
|
+
response.success?
|
|
770
|
+
end
|
|
771
|
+
|
|
772
|
+
# Perform an atomic add operation to the array field.
|
|
773
|
+
# @param field [String] the name of the field in the Parse collection.
|
|
774
|
+
# @param objects [Array] the set of items to add to this field.
|
|
775
|
+
# @return [Boolean] whether it was successful
|
|
776
|
+
# @see #operate_field!
|
|
777
|
+
def op_add!(field, objects)
|
|
778
|
+
operate_field! field, { __op: :Add, objects: objects }
|
|
779
|
+
end
|
|
780
|
+
|
|
781
|
+
# Perform an atomic add unique operation to the array field. The objects will
|
|
782
|
+
# only be added if they don't already exists in the array for that particular field.
|
|
783
|
+
# @param field [String] the name of the field in the Parse collection.
|
|
784
|
+
# @param objects [Array] the set of items to add uniquely to this field.
|
|
785
|
+
# @return [Boolean] whether it was successful
|
|
786
|
+
# @see #operate_field!
|
|
787
|
+
def op_add_unique!(field, objects)
|
|
788
|
+
operate_field! field, { __op: :AddUnique, objects: objects }
|
|
789
|
+
end
|
|
790
|
+
|
|
791
|
+
# Perform an atomic remove operation to the array field.
|
|
792
|
+
# @param field [String] the name of the field in the Parse collection.
|
|
793
|
+
# @param objects [Array] the set of items to remove to this field.
|
|
794
|
+
# @return [Boolean] whether it was successful
|
|
795
|
+
# @see #operate_field!
|
|
796
|
+
def op_remove!(field, objects)
|
|
797
|
+
operate_field! field, { __op: :Remove, objects: objects }
|
|
798
|
+
end
|
|
799
|
+
|
|
800
|
+
# Perform an atomic delete operation on this field.
|
|
801
|
+
# @param field [String] the name of the field in the Parse collection.
|
|
802
|
+
# @return [Boolean] whether it was successful
|
|
803
|
+
# @see #operate_field!
|
|
804
|
+
def op_destroy!(field)
|
|
805
|
+
result = operate_field! field, { __op: :Delete }.freeze
|
|
806
|
+
if result
|
|
807
|
+
# Also update the local state to reflect the deletion
|
|
808
|
+
field_sym = field.to_sym
|
|
809
|
+
if self.class.fields[field_sym].present?
|
|
810
|
+
set_attribute_method = "#{field}_set_attribute!"
|
|
811
|
+
if respond_to?(set_attribute_method)
|
|
812
|
+
send(set_attribute_method, nil, true) # Set to nil with dirty tracking
|
|
813
|
+
else
|
|
814
|
+
instance_variable_set(:"@#{field}", nil)
|
|
815
|
+
send("#{field}_will_change!") if respond_to?("#{field}_will_change!")
|
|
816
|
+
end
|
|
817
|
+
end
|
|
818
|
+
end
|
|
819
|
+
result
|
|
820
|
+
end
|
|
821
|
+
|
|
822
|
+
# Perform an atomic add operation on this relational field.
|
|
823
|
+
# @param field [String] the name of the field in the Parse collection.
|
|
824
|
+
# @param objects [Array<Parse::Object>] the set of objects to add to this relational field.
|
|
825
|
+
# @return [Boolean] whether it was successful
|
|
826
|
+
# @see #operate_field!
|
|
827
|
+
def op_add_relation!(field, objects = [])
|
|
828
|
+
objects = [objects] unless objects.is_a?(Array)
|
|
829
|
+
return false if objects.empty?
|
|
830
|
+
relation_action = Parse::RelationAction.new(field, polarity: true, objects: objects)
|
|
831
|
+
operate_field! field, relation_action
|
|
832
|
+
end
|
|
833
|
+
|
|
834
|
+
# Perform an atomic remove operation on this relational field.
|
|
835
|
+
# @param field [String] the name of the field in the Parse collection.
|
|
836
|
+
# @param objects [Array<Parse::Object>] the set of objects to remove to this relational field.
|
|
837
|
+
# @return [Boolean] whether it was successful
|
|
838
|
+
# @see #operate_field!
|
|
839
|
+
def op_remove_relation!(field, objects = [])
|
|
840
|
+
objects = [objects] unless objects.is_a?(Array)
|
|
841
|
+
return false if objects.empty?
|
|
842
|
+
relation_action = Parse::RelationAction.new(field, polarity: false, objects: objects)
|
|
843
|
+
operate_field! field, relation_action
|
|
844
|
+
end
|
|
845
|
+
|
|
846
|
+
# Atomically increment or decrement a specific field.
|
|
847
|
+
# @param field [String] the name of the field in the Parse collection.
|
|
848
|
+
# @param amount [Integer] the amoun to increment. Use negative values to decrement.
|
|
849
|
+
# @see #operate_field!
|
|
850
|
+
def op_increment!(field, amount = 1)
|
|
851
|
+
unless amount.is_a?(Numeric)
|
|
852
|
+
raise ArgumentError, "Amount should be numeric"
|
|
853
|
+
end
|
|
854
|
+
result = operate_field! field, { __op: :Increment, amount: amount.to_i }.freeze
|
|
855
|
+
if result
|
|
856
|
+
# Also update the local state to reflect the increment
|
|
857
|
+
field_sym = field.to_sym
|
|
858
|
+
current_value = self[field_sym] || 0
|
|
859
|
+
new_value = current_value + amount.to_i
|
|
860
|
+
set_attribute_method = "#{field}_set_attribute!"
|
|
861
|
+
if respond_to?(set_attribute_method)
|
|
862
|
+
send(set_attribute_method, new_value, true) # Set new value with dirty tracking
|
|
863
|
+
else
|
|
864
|
+
self[field_sym] = new_value
|
|
865
|
+
end
|
|
866
|
+
end
|
|
867
|
+
result
|
|
868
|
+
end
|
|
869
|
+
|
|
870
|
+
# @return [Parse::Request] a destroy_request for the current object.
|
|
871
|
+
def destroy_request
|
|
872
|
+
return nil unless @id.present?
|
|
873
|
+
uri = self.uri_path
|
|
874
|
+
r = Request.new(:delete, uri)
|
|
875
|
+
r.tag = object_id
|
|
876
|
+
r
|
|
877
|
+
end
|
|
878
|
+
|
|
879
|
+
# @return [String] the API uri path for this class.
|
|
880
|
+
def uri_path
|
|
881
|
+
self.client.url_prefix.path + Client.uri_path(self)
|
|
882
|
+
end
|
|
883
|
+
|
|
884
|
+
# Creates an array of all possible operations that need to be performed
|
|
885
|
+
# on this object. This includes all property and relational operation changes.
|
|
886
|
+
# @param force [Boolean] whether this object should be saved even if does not have
|
|
887
|
+
# pending changes.
|
|
888
|
+
# @return [Array<Parse::Request>] the list of API requests.
|
|
889
|
+
def change_requests(force = false)
|
|
890
|
+
requests = []
|
|
891
|
+
# get the URI path for this object.
|
|
892
|
+
uri = self.uri_path
|
|
893
|
+
|
|
894
|
+
# generate the request to update the object (PUT)
|
|
895
|
+
if attribute_changes? || force
|
|
896
|
+
# if it's new, then we should call :post for creating the object.
|
|
897
|
+
method = new? ? :post : :put
|
|
898
|
+
r = Request.new(method, uri, body: attribute_updates)
|
|
899
|
+
r.tag = object_id
|
|
900
|
+
requests << r
|
|
901
|
+
end
|
|
902
|
+
|
|
903
|
+
# if the object is not new, then we can also add all the relational changes
|
|
904
|
+
# we need to perform.
|
|
905
|
+
if @id.present? && relation_changes?
|
|
906
|
+
relation_change_operations.each do |ops|
|
|
907
|
+
next if ops.empty?
|
|
908
|
+
r = Request.new(:put, uri, body: ops)
|
|
909
|
+
r.tag = object_id
|
|
910
|
+
requests << r
|
|
911
|
+
end
|
|
912
|
+
end
|
|
913
|
+
requests
|
|
914
|
+
end
|
|
915
|
+
|
|
916
|
+
# This methods sends an update request for this object with the any change
|
|
917
|
+
# information based on its local attributes. The bang implies that it will send
|
|
918
|
+
# the request even though it is possible no changes were performed. This is useful
|
|
919
|
+
# in kicking-off an beforeSave / afterSave hooks
|
|
920
|
+
# Save the object regardless of whether there are changes. This would call
|
|
921
|
+
# any beforeSave and afterSave cloud code hooks you have registered for this class.
|
|
922
|
+
# @return [Boolean] true/false whether it was successful.
|
|
923
|
+
def update!(raw: false, force: false)
|
|
924
|
+
if valid? == false
|
|
925
|
+
errors.full_messages.each do |msg|
|
|
926
|
+
warn "[#{parse_class}] warning: #{msg}"
|
|
927
|
+
end
|
|
928
|
+
end
|
|
929
|
+
if force == true && attribute_changes?.blank? && !new?
|
|
930
|
+
# if we are forcing an update, but there are no attribute changes,
|
|
931
|
+
# we should still mark the updated_at field as changed so that
|
|
932
|
+
# the server updates it.
|
|
933
|
+
if self.class.fields[:updated_at].present?
|
|
934
|
+
self.updated_at = Time.now.utc
|
|
935
|
+
self.updated_at_will_change! if respond_to?(:updated_at_will_change!)
|
|
936
|
+
end
|
|
937
|
+
end
|
|
938
|
+
response = client.update_object(parse_class, id, attribute_updates, session_token: _session_token)
|
|
939
|
+
@_last_response = response
|
|
940
|
+
if response.success?
|
|
941
|
+
result = response.result
|
|
942
|
+
# Because beforeSave hooks can change the fields we are saving, any items that were
|
|
943
|
+
# changed, are returned to us and we should apply those locally to be in sync.
|
|
944
|
+
set_attributes!(result)
|
|
945
|
+
end
|
|
946
|
+
puts "Error updating #{self.parse_class}: #{response.error}" if response.error?
|
|
947
|
+
return response if raw
|
|
948
|
+
response.success?
|
|
949
|
+
end
|
|
950
|
+
|
|
951
|
+
# Save all the changes related to this object.
|
|
952
|
+
# @param force [Boolean] whether to send the update even if there are no changes.
|
|
953
|
+
# @return [Boolean] true/false whether it was successful.
|
|
954
|
+
def update(force: false)
|
|
955
|
+
return true unless attribute_changes? || force
|
|
956
|
+
update!(force: force)
|
|
957
|
+
end
|
|
958
|
+
|
|
959
|
+
# Internal method to perform update with :update callbacks.
|
|
960
|
+
# Called from save() for existing objects.
|
|
961
|
+
# @param force [Boolean] whether to send the update even if there are no changes.
|
|
962
|
+
# @return [Boolean] true/false whether it was successful.
|
|
963
|
+
# @!visibility private
|
|
964
|
+
def perform_update(force: false)
|
|
965
|
+
return true unless attribute_changes? || force
|
|
966
|
+
run_callbacks :update do
|
|
967
|
+
update!(force: force)
|
|
968
|
+
end
|
|
969
|
+
end
|
|
970
|
+
|
|
971
|
+
# Save the object as a new record, running all callbacks.
|
|
972
|
+
# @return [Boolean] true/false whether it was successful.
|
|
973
|
+
def create
|
|
974
|
+
run_callbacks :create do
|
|
975
|
+
body = attribute_updates
|
|
976
|
+
# Forward a client-assigned objectId when a `before_create` callback
|
|
977
|
+
# set it (e.g. `parse_reference precompute: true`). attribute_updates
|
|
978
|
+
# excludes BASE_KEYS, so @id must be merged explicitly. Parse Server
|
|
979
|
+
# accepts an objectId in the create POST body and rejects duplicates
|
|
980
|
+
# with a typed error rather than silently overwriting.
|
|
981
|
+
body[Parse::Model::OBJECT_ID] = @id if @id.present?
|
|
982
|
+
res = client.create_object(parse_class, body, session_token: _session_token)
|
|
983
|
+
# Retain the response so wrappers (e.g. synchronize_create) can
|
|
984
|
+
# inspect the Parse error code on failure (notably 137 DuplicateValue).
|
|
985
|
+
@_last_response = res
|
|
986
|
+
unless res.error?
|
|
987
|
+
result = res.result
|
|
988
|
+
@id = result[Parse::Model::OBJECT_ID] || @id
|
|
989
|
+
@created_at = result["createdAt"] || @created_at
|
|
990
|
+
#if the object is created, updatedAt == createdAt
|
|
991
|
+
@updated_at = result["updatedAt"] || result["createdAt"] || @updated_at
|
|
992
|
+
# Because beforeSave hooks can change the fields we are saving, any items that were
|
|
993
|
+
# changed, are returned to us and we should apply those locally to be in sync.
|
|
994
|
+
set_attributes!(result)
|
|
995
|
+
end
|
|
996
|
+
puts "Error creating #{self.parse_class}: #{res.error}" if res.error?
|
|
997
|
+
res.success?
|
|
998
|
+
end
|
|
999
|
+
end
|
|
1000
|
+
|
|
1001
|
+
# @!visibility private
|
|
1002
|
+
def _session_token
|
|
1003
|
+
if @_session_token.respond_to?(:session_token)
|
|
1004
|
+
@_session_token = @_session_token.session_token
|
|
1005
|
+
end
|
|
1006
|
+
@_session_token
|
|
1007
|
+
end
|
|
1008
|
+
|
|
1009
|
+
# @!visibility private
|
|
1010
|
+
def _validate_session_token!(token, action = :save)
|
|
1011
|
+
return nil if token.nil? # user explicitly requests no session token
|
|
1012
|
+
token = token.session_token if token.respond_to?(:session_token)
|
|
1013
|
+
return token if token.is_a?(String) && token.present?
|
|
1014
|
+
raise ArgumentError, "#{self.class}##{action} error: Invalid session token passed (#{token})"
|
|
1015
|
+
end
|
|
1016
|
+
|
|
1017
|
+
# saves the object. If the object has not changed, it is a noop. If it is new,
|
|
1018
|
+
# we will create the object. If the object has an id, we will update the record.
|
|
1019
|
+
#
|
|
1020
|
+
# You may pass a session token to the `session` argument to perform this actions
|
|
1021
|
+
# with the privileges of a certain user.
|
|
1022
|
+
#
|
|
1023
|
+
# Callback order:
|
|
1024
|
+
# 1. before_validation / around_validation / after_validation
|
|
1025
|
+
# 2. before_save / around_save
|
|
1026
|
+
# 3. before_create or before_update / around_create or around_update
|
|
1027
|
+
# 4. [actual save operation]
|
|
1028
|
+
# 5. after_create or after_update
|
|
1029
|
+
# 6. after_save
|
|
1030
|
+
#
|
|
1031
|
+
# You can define before and after :save callbacks
|
|
1032
|
+
# autoraise: set to true will automatically raise an exception if the save fails
|
|
1033
|
+
# @raise {Parse::RecordNotSaved} if the save fails
|
|
1034
|
+
# @raise ArgumentError if a non-nil value is passed to `session` that doesn't provide a session token string.
|
|
1035
|
+
# @param session [String] a session token in order to apply ACLs to this operation.
|
|
1036
|
+
# @param autoraise [Boolean] whether to raise an exception if the save fails.
|
|
1037
|
+
# @param force [Boolean] whether to run callbacks and send request even if there are no changes.
|
|
1038
|
+
# @param validate [Boolean] whether to run validations (default: true).
|
|
1039
|
+
# @return [Boolean] whether the save was successful.
|
|
1040
|
+
def save(session: nil, autoraise: false, force: false, validate: true)
|
|
1041
|
+
# Prevent saving objects that have been fetched and found to be deleted
|
|
1042
|
+
if _deleted?
|
|
1043
|
+
error_msg = "Cannot save deleted object. Object with id '#{@id}' no longer exists on the server."
|
|
1044
|
+
raise Parse::Error::ProtocolError, error_msg
|
|
1045
|
+
end
|
|
1046
|
+
|
|
1047
|
+
@_session_token = _validate_session_token! session, :save
|
|
1048
|
+
return true unless changed? || force
|
|
1049
|
+
|
|
1050
|
+
# Run validations (validation callbacks are now triggered by valid? method)
|
|
1051
|
+
# Pass context so `on: :create` and `on: :update` options work with callbacks
|
|
1052
|
+
if validate
|
|
1053
|
+
validation_context = new? ? :create : :update
|
|
1054
|
+
validation_passed = valid?(validation_context)
|
|
1055
|
+
|
|
1056
|
+
unless validation_passed
|
|
1057
|
+
if self.class.raise_on_save_failure || autoraise.present?
|
|
1058
|
+
raise Parse::RecordNotSaved.new(self), "Validation failed: #{errors.full_messages.join(", ")}"
|
|
1059
|
+
end
|
|
1060
|
+
return false
|
|
1061
|
+
end
|
|
1062
|
+
end
|
|
1063
|
+
|
|
1064
|
+
success = false
|
|
1065
|
+
|
|
1066
|
+
# Track if callbacks are halted by a before_save hook returning false
|
|
1067
|
+
callback_executed = false
|
|
1068
|
+
run_callbacks :save do
|
|
1069
|
+
callback_executed = true
|
|
1070
|
+
#first process the create/update action if any
|
|
1071
|
+
#then perform any relation changes that need to be performed
|
|
1072
|
+
success = new? ? create : perform_update(force: force)
|
|
1073
|
+
|
|
1074
|
+
# if the save was successful and we have relational changes
|
|
1075
|
+
# let's update send those next.
|
|
1076
|
+
if success
|
|
1077
|
+
if relation_changes?
|
|
1078
|
+
# get the list of changed keys
|
|
1079
|
+
changed_attribute_keys = changed - relations.keys.map(&:to_s)
|
|
1080
|
+
clear_attribute_changes(changed_attribute_keys)
|
|
1081
|
+
success = update_relations
|
|
1082
|
+
if success
|
|
1083
|
+
changes_applied!
|
|
1084
|
+
clear_partial_fetch_state!
|
|
1085
|
+
elsif self.class.raise_on_save_failure || autoraise.present?
|
|
1086
|
+
raise Parse::RecordNotSaved.new(self), "Failed updating relations. #{self.parse_class} partially saved."
|
|
1087
|
+
end
|
|
1088
|
+
else
|
|
1089
|
+
changes_applied!
|
|
1090
|
+
clear_partial_fetch_state!
|
|
1091
|
+
end
|
|
1092
|
+
elsif self.class.raise_on_save_failure || autoraise.present?
|
|
1093
|
+
raise Parse::RecordNotSaved.new(self), "Failed to create or save attributes. #{self.parse_class} was not saved."
|
|
1094
|
+
end
|
|
1095
|
+
end #callbacks
|
|
1096
|
+
|
|
1097
|
+
# If callbacks were halted (before_save returned false), return false
|
|
1098
|
+
return false unless callback_executed
|
|
1099
|
+
|
|
1100
|
+
@_session_token = nil
|
|
1101
|
+
success
|
|
1102
|
+
end
|
|
1103
|
+
|
|
1104
|
+
# Save this object and raise an exception if it fails.
|
|
1105
|
+
# @raise {Parse::RecordNotSaved} if the save fails
|
|
1106
|
+
# @raise ArgumentError if a non-nil value is passed to `session` that doesn't provide a session token string.
|
|
1107
|
+
# @param session (see #save)
|
|
1108
|
+
# @param force (see #save)
|
|
1109
|
+
# @return (see #save)
|
|
1110
|
+
def save!(session: nil, force: false)
|
|
1111
|
+
save(autoraise: true, session: session, force: force)
|
|
1112
|
+
end
|
|
1113
|
+
|
|
1114
|
+
# Returns true if this object has been fetched and found to be deleted from the server.
|
|
1115
|
+
# Deleted objects cannot be saved.
|
|
1116
|
+
# @return [Boolean] true if the object is marked as deleted
|
|
1117
|
+
def _deleted?
|
|
1118
|
+
@_deleted == true
|
|
1119
|
+
end
|
|
1120
|
+
|
|
1121
|
+
# Delete this record from the Parse collection. Only valid if this object has an `id`.
|
|
1122
|
+
# This will run all the `destroy` callbacks.
|
|
1123
|
+
# @param session [String] a session token if you want to apply ACLs for a user in this operation.
|
|
1124
|
+
# @raise ArgumentError if a non-nil value is passed to `session` that doesn't provide a session token string.
|
|
1125
|
+
# @return [Boolean] whether the operation was successful.
|
|
1126
|
+
def destroy(session: nil)
|
|
1127
|
+
@_session_token = _validate_session_token! session, :destroy
|
|
1128
|
+
return false if new?
|
|
1129
|
+
success = false
|
|
1130
|
+
run_callbacks :destroy do
|
|
1131
|
+
res = client.delete_object parse_class, id, session_token: _session_token
|
|
1132
|
+
success = res.success?
|
|
1133
|
+
if success
|
|
1134
|
+
@id = nil
|
|
1135
|
+
changes_applied!
|
|
1136
|
+
elsif self.class.raise_on_save_failure
|
|
1137
|
+
raise Parse::RecordNotSaved.new(self), "Failed to create or save attributes. #{self.parse_class} was not saved."
|
|
1138
|
+
end
|
|
1139
|
+
# Your create action methods here
|
|
1140
|
+
end
|
|
1141
|
+
@_session_token = nil
|
|
1142
|
+
success
|
|
1143
|
+
end
|
|
1144
|
+
|
|
1145
|
+
# Runs all the registered `before_save` related callbacks.
|
|
1146
|
+
def prepare_save!
|
|
1147
|
+
# With terminator configured, run_callbacks will return false if any callback returns false
|
|
1148
|
+
# We track if the block executes to know if callbacks were halted
|
|
1149
|
+
callback_success = false
|
|
1150
|
+
run_callbacks(:save) do
|
|
1151
|
+
callback_success = true
|
|
1152
|
+
true
|
|
1153
|
+
end
|
|
1154
|
+
callback_success
|
|
1155
|
+
end
|
|
1156
|
+
|
|
1157
|
+
# @return [Hash] a hash of the list of changes made to this instance.
|
|
1158
|
+
def changes_payload
|
|
1159
|
+
h = attribute_updates
|
|
1160
|
+
if relation_changes?
|
|
1161
|
+
r = relation_change_operations.select { |s| s.present? }.first
|
|
1162
|
+
h.merge!(r) if r.present?
|
|
1163
|
+
end
|
|
1164
|
+
#h.merge!(className: parse_class) unless h.empty?
|
|
1165
|
+
h.as_json
|
|
1166
|
+
end
|
|
1167
|
+
|
|
1168
|
+
alias_method :update_payload, :changes_payload
|
|
1169
|
+
|
|
1170
|
+
# Generates an array with two entries for addition and removal operations. The first entry
|
|
1171
|
+
# of the array will contain a hash of all the change operations regarding adding new relational
|
|
1172
|
+
# objects. The second entry in the array is a hash of all the change operations regarding removing
|
|
1173
|
+
# relation objects from this field.
|
|
1174
|
+
# @return [Array] an array with two hashes; the first is a hash of all the addition operations and
|
|
1175
|
+
# the second hash, all the remove operations.
|
|
1176
|
+
def relation_change_operations
|
|
1177
|
+
return [{}, {}] unless relation_changes?
|
|
1178
|
+
|
|
1179
|
+
additions = []
|
|
1180
|
+
removals = []
|
|
1181
|
+
# go through all the additions of a collection and generate an action to add.
|
|
1182
|
+
relation_updates.each do |field, collection|
|
|
1183
|
+
if collection.additions.count > 0
|
|
1184
|
+
additions.push Parse::RelationAction.new(field, objects: collection.additions, polarity: true)
|
|
1185
|
+
end
|
|
1186
|
+
# go through all the additions of a collection and generate an action to remove.
|
|
1187
|
+
if collection.removals.count > 0
|
|
1188
|
+
removals.push Parse::RelationAction.new(field, objects: collection.removals, polarity: false)
|
|
1189
|
+
end
|
|
1190
|
+
end
|
|
1191
|
+
# merge all additions and removals into one large hash
|
|
1192
|
+
additions = additions.reduce({}) { |m, v| m.merge! v.as_json }
|
|
1193
|
+
removals = removals.reduce({}) { |m, v| m.merge! v.as_json }
|
|
1194
|
+
[additions, removals]
|
|
1195
|
+
end
|
|
1196
|
+
|
|
1197
|
+
# Saves and updates all the relational changes for made to this object.
|
|
1198
|
+
# @return [Boolean] whether all the save or update requests were successful.
|
|
1199
|
+
def update_relations
|
|
1200
|
+
# relational saves require an id
|
|
1201
|
+
return false unless @id.present?
|
|
1202
|
+
# verify we have relational changes before we do work.
|
|
1203
|
+
return true unless relation_changes?
|
|
1204
|
+
raise "Unable to update relations for a new object." if new?
|
|
1205
|
+
# get all the relational changes (both additions and removals)
|
|
1206
|
+
additions, removals = relation_change_operations
|
|
1207
|
+
|
|
1208
|
+
responses = []
|
|
1209
|
+
# Send parallel Parse requests for each of the items to update.
|
|
1210
|
+
# since we will have multiple responses, we will track it in array
|
|
1211
|
+
[removals, additions].threaded_each do |ops|
|
|
1212
|
+
next if ops.empty? #if no operations to be performed, then we are done
|
|
1213
|
+
responses << client.update_object(parse_class, @id, ops, session_token: _session_token)
|
|
1214
|
+
end
|
|
1215
|
+
# check if any of them ended up in error
|
|
1216
|
+
has_error = responses.any? { |response| response.error? }
|
|
1217
|
+
# if everything was ok, find the last response to be returned and update
|
|
1218
|
+
#their fields in case beforeSave made any changes.
|
|
1219
|
+
unless has_error || responses.empty?
|
|
1220
|
+
result = responses.last.result #last result to come back
|
|
1221
|
+
set_attributes!(result)
|
|
1222
|
+
end #unless
|
|
1223
|
+
has_error == false
|
|
1224
|
+
end
|
|
1225
|
+
|
|
1226
|
+
# Performs mass assignment using a hash with the ability to modify dirty tracking.
|
|
1227
|
+
# This is an internal method used to set properties on the object while controlling
|
|
1228
|
+
# whether they are dirty tracked. Each defined property has a method defined with the
|
|
1229
|
+
# suffix `_set_attribute!` that can will be called if it is contained in the hash.
|
|
1230
|
+
# @example
|
|
1231
|
+
# object.set_attributes!( {"myField" => value}, false)
|
|
1232
|
+
#
|
|
1233
|
+
# # equivalent to calling the specific method.
|
|
1234
|
+
# object.myField_set_attribute!(value, false)
|
|
1235
|
+
# @param hash [Hash] the hash containing all the attribute names and values.
|
|
1236
|
+
# @param dirty_track [Boolean] whether the assignment should be tracked in the change tracking
|
|
1237
|
+
# system.
|
|
1238
|
+
# @return [Hash]
|
|
1239
|
+
def set_attributes!(hash, dirty_track = false)
|
|
1240
|
+
return unless hash.is_a?(Hash)
|
|
1241
|
+
hash.each do |k, v|
|
|
1242
|
+
next if k == Parse::Model::OBJECT_ID || k == Parse::Model::ID
|
|
1243
|
+
method = "#{k}_set_attribute!"
|
|
1244
|
+
send(method, v, dirty_track) if respond_to?(method)
|
|
1245
|
+
end
|
|
1246
|
+
end
|
|
1247
|
+
|
|
1248
|
+
# Clears changes information on all collections (array and relations) and all
|
|
1249
|
+
# local attributes.
|
|
1250
|
+
def changes_applied!
|
|
1251
|
+
# find all fields that are of type :array
|
|
1252
|
+
fields(:array) do |key, v|
|
|
1253
|
+
proxy = send(key)
|
|
1254
|
+
# clear changes
|
|
1255
|
+
proxy.changes_applied! if proxy.respond_to?(:changes_applied!)
|
|
1256
|
+
end
|
|
1257
|
+
|
|
1258
|
+
# for all relational fields,
|
|
1259
|
+
relations.each do |key, v|
|
|
1260
|
+
proxy = send(key)
|
|
1261
|
+
# clear changes if they support the method.
|
|
1262
|
+
proxy.changes_applied! if proxy.respond_to?(:changes_applied!)
|
|
1263
|
+
end
|
|
1264
|
+
changes_applied
|
|
1265
|
+
end
|
|
1266
|
+
end
|
|
1267
|
+
end
|
|
1268
|
+
end
|