Module: Parse::Core::Actions

Included in:
Object
Defined in:
lib/parse/model/core/actions.rb

Overview

Defines some of the save, update and destroy operations for Parse objects.

Defined Under Namespace

Modules: ClassMethods

Constant Summary collapse

TRANSACTION_CONTEXT_KEY =

Fiber-local context used to capture an object's state before its first mutation inside a transaction block. batch.add is intentionally too late for this: the public API documents mutating an object and adding it afterwards.

:__parse_transaction_context__
UNDEFINED_PROPERTY =

Distinguishes a property whose ivar did not exist from one explicitly set to nil. Rollback removes the former instead of defining it as nil.

Object.new.freeze
ROLLBACK_STATE_IVARS =

Non-property state that can change while building/submitting a batch and must be restored along with the @<field> property ivars.

%i[
  @changed_attributes
  @id
  @mutations_from_database
  @mutations_before_last_save
  @_acl_snapshot_before_change
  @_acl_pristine
  @_authorization_acl_state
].freeze

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.capture_transaction_state(obj, context = , include_created: false) ⇒ Hash?

Capture an object's state once, before its first transaction mutation. Objects created inside the transaction defer capture until batch.add.

Parameters:

  • obj (Parse::Object)

    object whose state should be captured.

  • context (Hash, nil) (defaults to: )

    transaction context; defaults to the current fiber.

  • include_created (Boolean) (defaults to: false)

    capture a newly created object at add time.

Returns:

  • (Hash, nil)

    the object's rollback state.



310
311
312
313
314
315
316
# File 'lib/parse/model/core/actions.rb', line 310

def self.capture_transaction_state(obj, context = Fiber[TRANSACTION_CONTEXT_KEY], include_created: false)
  return unless context && obj

  object_id = obj.object_id
  return if context[:created_objects].key?(object_id) && !include_created
  context[:snapshots][object_id] ||= snapshot_object_state(obj)
end

.dup_for_snapshot(value, seen = {}) ⇒ Object

Copy a property value deeply enough that later in-place mutation of the live value cannot reach the snapshot.

A plain dup is not sufficient for collection-backed properties: :array and association properties hold a Parse::CollectionProxy, and duplicating the proxy still shares the underlying @collection array, so widget.tags << "b" would mutate the snapshot too. The proxy's mutable backing arrays and nested values are duplicated as well. Parse objects nested inside values remain references: a pointer property should restore the same object, not manufacture a clone.

Parameters:

  • value (Object)

    the live property value.

Returns:

  • (Object)

    a copy safe to hold across the transaction.



184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
# File 'lib/parse/model/core/actions.rb', line 184

def self.dup_for_snapshot(value, seen = {})
  return value if value.nil? || value == true || value == false || value.is_a?(Symbol) || value.is_a?(Numeric)
  return value if defined?(Parse::Pointer) && value.is_a?(Parse::Pointer)

  object_id = value.object_id
  return seen[object_id] if seen.key?(object_id)

  case value
  when Array
    copy = value.dup
    copy.clear
    seen[object_id] = copy
    value.each { |item| copy << dup_for_snapshot(item, seen) }
    copy
  when Hash
    copy = value.dup
    copy.clear
    seen[object_id] = copy
    value.each do |key, item|
      copy[dup_for_snapshot(key, seen)] = dup_for_snapshot(item, seen)
    end
    copy
  when Parse::CollectionProxy
    copy = value.dup
    seen[object_id] = copy
    %i[@collection @additions @removals @changed_attributes].each do |ivar|
      next unless value.instance_variable_defined?(ivar)
      copy.instance_variable_set(ivar, dup_for_snapshot(value.instance_variable_get(ivar), seen))
    end
    %i[@mutations_from_database @mutations_before_last_save].each do |ivar|
      next unless value.instance_variable_defined?(ivar)
      tracker = value.instance_variable_get(ivar)
      copy.instance_variable_set(ivar, dup_mutation_tracker(tracker, copy, seen))
    end
    copy
  when Parse::ACL
    copy = Parse::ACL.new(dup_for_snapshot(value.as_json, seen), owner: value.delegate)
    seen[object_id] = copy
    copy
  else
    copy = begin
        value.dup
      rescue TypeError
        # Immutable values are safe to share with the snapshot.
        return value
      end
    seen[object_id] = copy
    copy
  end
end

.dup_mutation_tracker(tracker, owner, seen = {}) ⇒ Object?

Duplicate ActiveModel's mutation tracker without sharing its mutable forced/finalized change hashes. Forced trackers retain their owning Parse object (or copied collection proxy) so dirty reads keep working.

Parameters:

  • tracker (Object, nil)

    ActiveModel mutation tracker.

  • owner (Object)

    object whose attributes the copy should read.

  • seen (Hash) (defaults to: {})

    identity map used by dup_for_snapshot.

Returns:

  • (Object, nil)

    isolated tracker copy.



243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
# File 'lib/parse/model/core/actions.rb', line 243

def self.dup_mutation_tracker(tracker, owner, seen = {})
  return nil if tracker.nil?
  return tracker if defined?(ActiveModel::NullMutationTracker) && tracker.is_a?(ActiveModel::NullMutationTracker)
  return seen[tracker.object_id] if seen.key?(tracker.object_id)

  copy = tracker.dup
  seen[tracker.object_id] = copy
  if defined?(ActiveModel::ForcedMutationTracker) && tracker.is_a?(ActiveModel::ForcedMutationTracker)
    copy.instance_variable_set(:@attributes, owner)
  end
  %i[@forced_changes @finalized_changes].each do |ivar|
    next unless tracker.instance_variable_defined?(ivar)
    copy.instance_variable_set(ivar, dup_for_snapshot(tracker.instance_variable_get(ivar), seen))
  end
  copy
rescue TypeError
  tracker
end

.mark_transaction_object_created(obj) ⇒ void

This method returns an undefined value.

Record that an object was initialized inside the active transaction. Its first rollback snapshot is intentionally taken when it is added to the batch, after initialization, so a failed create stays usable as an initialized unsaved object.

Parameters:



296
297
298
299
300
301
# File 'lib/parse/model/core/actions.rb', line 296

def self.mark_transaction_object_created(obj)
  context = Fiber[TRANSACTION_CONTEXT_KEY]
  return unless context
  context[:created_objects][obj.object_id] = true
  context[:snapshots].delete(obj.object_id)
end

.restore_instance_variable(obj, ivar, value) ⇒ void

This method returns an undefined value.

Restore one instance variable while preserving whether it originally existed. Used for dirty/ACL bookkeeping outside the property schema.

Parameters:



345
346
347
348
349
350
351
# File 'lib/parse/model/core/actions.rb', line 345

def self.restore_instance_variable(obj, ivar, value)
  if value.equal?(UNDEFINED_PROPERTY)
    obj.remove_instance_variable(ivar) if obj.instance_variable_defined?(ivar)
  else
    obj.instance_variable_set(ivar, value)
  end
end

.restore_property_values(obj, snapshot) ⇒ void

This method returns an undefined value.

Restore the values captured by snapshot_property_values.

Writes ivars directly rather than going through the property setters, since the setters mark the object dirty and the caller restores the dirty state itself immediately afterwards.

Parameters:



327
328
329
330
331
332
333
334
335
336
# File 'lib/parse/model/core/actions.rb', line 327

def self.restore_property_values(obj, snapshot)
  return unless snapshot.is_a?(Hash)
  snapshot.each do |ivar, value|
    if value.equal?(UNDEFINED_PROPERTY)
      obj.remove_instance_variable(ivar) if obj.instance_variable_defined?(ivar)
    else
      obj.instance_variable_set(ivar, value)
    end
  end
end

.rollback_object_state(state) ⇒ void

This method returns an undefined value.

Roll a single tracked object back to the state captured when it was added to the transaction.

Parameters:

  • state (Hash)

    one entry of the transaction's original_states.



358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
# File 'lib/parse/model/core/actions.rb', line 358

def self.rollback_object_state(state)
  obj = state[:object]
  return if obj.nil?
  restore_property_values(obj, state[:property_values])
  if state[:instance_variables]
    state[:instance_variables].each do |ivar, value|
      restore_instance_variable(obj, ivar, value)
    end
  else
    # Compatibility with rollback states produced before the complete
    # snapshot format was introduced.
    obj.instance_variable_set(:@changed_attributes, state[:changed_attributes])
    obj.instance_variable_set(:@id, state[:id])
    obj.instance_variable_set(:@mutations_from_database, state[:mutations_from_database])
    obj.instance_variable_set(:@mutations_before_last_save, state[:mutations_before_last_save])
  end
end

.snapshot_object_state(obj) ⇒ Hash

Capture all local state needed to restore an object after a failed transaction. This is separate from #attributes, which is a schema.

Parameters:

Returns:

  • (Hash)

    rollback state.



267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
# File 'lib/parse/model/core/actions.rb', line 267

def self.snapshot_object_state(obj)
  seen = {}
  instance_variables = ROLLBACK_STATE_IVARS.each_with_object({}) do |ivar, snapshot|
    snapshot[ivar] = if obj.instance_variable_defined?(ivar)
        value = obj.instance_variable_get(ivar)
        if %i[@mutations_from_database @mutations_before_last_save].include?(ivar)
          dup_mutation_tracker(value, obj, seen)
        else
          dup_for_snapshot(value, seen)
        end
      else
        UNDEFINED_PROPERTY
      end
  end

  {
    object: obj,
    property_values: snapshot_property_values(obj),
    instance_variables: instance_variables,
  }
end

.snapshot_property_values(obj) ⇒ Hash

Capture the property values a transaction rollback must restore.

Property values live in @<field> instance variables (see Properties::ClassMethods#property), so those are what gets snapshotted. Object#attributes is deliberately NOT captured: it returns a SCHEMA map (field name to type symbol), not values, so restoring it never rolled anything back.

Restoring it was also actively harmful. @attributes is the ivar ActiveModel::Dirty keys its behavior on: mutations_from_database builds an AttributeMutationTracker over @attributes the moment that ivar is defined (and a ForcedMutationTracker otherwise), while forget_attribute_assignments calls map(&:forgetting_assignment) on it. Handing either one a schema Hash raised NoMethodError on a type symbol and on the [key, value] pair Hash#map yields. Both sites rescue and warn, so every rollback silently downgraded the object to broken change tracking instead of failing.

Parameters:

Returns:

  • (Hash)

    field name to a snapshot of its value. Mutable values are duplicated so in-place mutation between snapshot and rollback cannot corrupt the saved copy.



156
157
158
159
160
161
162
163
164
165
166
167
168
169
# File 'lib/parse/model/core/actions.rb', line 156

def self.snapshot_property_values(obj)
  fields = obj.class.respond_to?(:fields) ? obj.class.fields.keys : []
  relations = obj.class.respond_to?(:relations) ? obj.class.relations.keys : []
  property_keys = (fields + relations).uniq
  seen = {}
  property_keys.each_with_object({}) do |key, snapshot|
    ivar = :"@#{key}"
    snapshot[ivar] = if obj.instance_variable_defined?(ivar)
        dup_for_snapshot(obj.instance_variable_get(ivar), seen)
      else
        UNDEFINED_PROPERTY
      end
  end
end

Instance Method Details

#_deleted?Boolean

Returns true if this object has been fetched and found to be deleted from the server. Deleted objects cannot be saved.

Returns:

  • (Boolean)

    true if the object is marked as deleted



1424
1425
1426
# File 'lib/parse/model/core/actions.rb', line 1424

def _deleted?
  @_deleted == true
end

#change_requests(force = false) ⇒ Array<Parse::Request>

Creates an array of all possible operations that need to be performed on this object. This includes all property and relational operation changes.

Parameters:

  • force (Boolean) (defaults to: false)

    whether this object should be saved even if does not have pending changes.

Returns:



1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
# File 'lib/parse/model/core/actions.rb', line 1196

def change_requests(force = false)
  requests = []
  # get the URI path for this object.
  uri = self.uri_path

  # generate the request to update the object (PUT)
  if attribute_changes? || force
    # if it's new, then we should call :post for creating the object.
    method = new? ? :post : :put
    r = Request.new(method, uri, body: attribute_updates)
    r.tag = object_id
    requests << r
  end

  # if the object is not new, then we can also add all the relational changes
  # we need to perform.
  if @id.present? && relation_changes?
    relation_change_operations.each do |ops|
      next if ops.empty?
      r = Request.new(:put, uri, body: ops)
      r.tag = object_id
      requests << r
    end
  end
  requests
end

#changes_applied!Object

Clears changes information on all collections (array and relations) and all local attributes.



1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
# File 'lib/parse/model/core/actions.rb', line 1555

def changes_applied!
  # find all fields that are of type :array
  fields(:array) do |key, v|
    proxy = send(key)
    # clear changes
    proxy.changes_applied! if proxy.respond_to?(:changes_applied!)
  end

  # for all relational fields,
  relations.each do |key, v|
    proxy = send(key)
    # clear changes if they support the method.
    proxy.changes_applied! if proxy.respond_to?(:changes_applied!)
  end
  changes_applied
end

#changes_payloadHash Also known as: update_payload

Returns a hash of the list of changes made to this instance.

Returns:

  • (Hash)

    a hash of the list of changes made to this instance.



1463
1464
1465
1466
1467
1468
1469
1470
1471
# File 'lib/parse/model/core/actions.rb', line 1463

def changes_payload
  h = attribute_updates
  if relation_changes?
    r = relation_change_operations.select { |s| s.present? }.first
    h.merge!(r) if r.present?
  end
  #h.merge!(className: parse_class) unless h.empty?
  h.as_json
end

#createBoolean

Save the object as a new record, running all callbacks.

Returns:

  • (Boolean)

    true/false whether it was successful.



1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
# File 'lib/parse/model/core/actions.rb', line 1280

def create
  run_callbacks :create do
    body = attribute_updates
    # Forward a client-assigned objectId when a `before_create` callback
    # set it (e.g. `parse_reference precompute: true`). attribute_updates
    # excludes BASE_KEYS, so @id must be merged explicitly. Parse Server
    # accepts an objectId in the create POST body and rejects duplicates
    # with a typed error rather than silently overwriting.
    body[Parse::Model::OBJECT_ID] = @id if @id.present?
    res = client.create_object(parse_class, body, session_token: _session_token)
    # Retain the response so wrappers (e.g. synchronize_create) can
    # inspect the Parse error code on failure (notably 137 DuplicateValue).
    @_last_response = res
    unless res.error?
      result = res.result
      @id = result[Parse::Model::OBJECT_ID] || @id
      @created_at = result["createdAt"] || @created_at
      #if the object is created, updatedAt == createdAt
      @updated_at = result["updatedAt"] || result["createdAt"] || @updated_at
      # Because beforeSave hooks can change the fields we are saving, any items that were
      # changed, are returned to us and we should apply those locally to be in sync.
      set_attributes!(result)
    end
    puts "Error creating #{self.parse_class}: #{res.error}" if res.error?
    res.success?
  end
end

#destroy(session: nil) ⇒ Boolean

Delete this record from the Parse collection. Only valid if this object has an id. This will run all the destroy callbacks.

Parameters:

  • session (String) (defaults to: nil)

    a session token if you want to apply ACLs for a user in this operation.

Returns:

  • (Boolean)

    whether the operation was successful.

Raises:

  • ArgumentError if a non-nil value is passed to session that doesn't provide a session token string.



1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
# File 'lib/parse/model/core/actions.rb', line 1433

def destroy(session: nil)
  @_session_token = _validate_session_token! session, :destroy
  return false if new?
  success = false
  run_callbacks :destroy do
    res = client.delete_object parse_class, id, session_token: _session_token
    success = res.success?
    if success
      @id = nil
      changes_applied!
    elsif self.class.raise_on_save_failure
      raise Parse::RecordNotSaved.new(self), "Failed to create or save attributes. #{self.parse_class} was not saved."
    end
    # Your create action methods here
  end
  @_session_token = nil
  success
end

#destroy_requestParse::Request

Returns a destroy_request for the current object.

Returns:



1178
1179
1180
1181
1182
1183
1184
# File 'lib/parse/model/core/actions.rb', line 1178

def destroy_request
  return nil unless @id.present?
  uri = self.uri_path
  r = Request.new(:delete, uri)
  r.tag = object_id
  r
end

#op_add!(field, objects) ⇒ Boolean

Perform an atomic add operation to the array field.

Parameters:

  • field (String)

    the name of the field in the Parse collection.

  • objects (Array)

    the set of items to add to this field.

Returns:

  • (Boolean)

    whether it was successful

See Also:



1084
1085
1086
# File 'lib/parse/model/core/actions.rb', line 1084

def op_add!(field, objects)
  operate_field! field, { __op: :Add, objects: objects }
end

#op_add_relation!(field, objects = []) ⇒ Boolean

Perform an atomic add operation on this relational field.

Parameters:

  • field (String)

    the name of the field in the Parse collection.

  • objects (Array<Parse::Object>) (defaults to: [])

    the set of objects to add to this relational field.

Returns:

  • (Boolean)

    whether it was successful

See Also:



1134
1135
1136
1137
1138
1139
# File 'lib/parse/model/core/actions.rb', line 1134

def op_add_relation!(field, objects = [])
  objects = [objects] unless objects.is_a?(Array)
  return false if objects.empty?
  relation_action = Parse::RelationAction.new(field, polarity: true, objects: objects)
  operate_field! field, relation_action
end

#op_add_unique!(field, objects) ⇒ Boolean

Perform an atomic add unique operation to the array field. The objects will only be added if they don't already exists in the array for that particular field.

Parameters:

  • field (String)

    the name of the field in the Parse collection.

  • objects (Array)

    the set of items to add uniquely to this field.

Returns:

  • (Boolean)

    whether it was successful

See Also:



1094
1095
1096
# File 'lib/parse/model/core/actions.rb', line 1094

def op_add_unique!(field, objects)
  operate_field! field, { __op: :AddUnique, objects: objects }
end

#op_destroy!(field) ⇒ Boolean

Perform an atomic delete operation on this field.

Parameters:

  • field (String)

    the name of the field in the Parse collection.

Returns:

  • (Boolean)

    whether it was successful

See Also:



1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
# File 'lib/parse/model/core/actions.rb', line 1111

def op_destroy!(field)
  result = operate_field! field, { __op: :Delete }.freeze
  if result
    # Also update the local state to reflect the deletion
    field_sym = field.to_sym
    if self.class.fields[field_sym].present?
      set_attribute_method = "#{field}_set_attribute!"
      if respond_to?(set_attribute_method)
        send(set_attribute_method, nil, true) # Set to nil with dirty tracking
      else
        instance_variable_set(:"@#{field}", nil)
        send("#{field}_will_change!") if respond_to?("#{field}_will_change!")
      end
    end
  end
  result
end

#op_increment!(field, amount = 1) ⇒ Object

Atomically increment or decrement a specific field.

Parameters:

  • field (String)

    the name of the field in the Parse collection.

  • amount (Integer) (defaults to: 1)

    the amoun to increment. Use negative values to decrement.

See Also:



1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
# File 'lib/parse/model/core/actions.rb', line 1157

def op_increment!(field, amount = 1)
  unless amount.is_a?(Numeric)
    raise ArgumentError, "Amount should be numeric"
  end
  result = operate_field! field, { __op: :Increment, amount: amount.to_i }.freeze
  if result
    # Also update the local state to reflect the increment
    field_sym = field.to_sym
    current_value = self[field_sym] || 0
    new_value = current_value + amount.to_i
    set_attribute_method = "#{field}_set_attribute!"
    if respond_to?(set_attribute_method)
      send(set_attribute_method, new_value, true) # Set new value with dirty tracking
    else
      self[field_sym] = new_value
    end
  end
  result
end

#op_remove!(field, objects) ⇒ Boolean

Perform an atomic remove operation to the array field.

Parameters:

  • field (String)

    the name of the field in the Parse collection.

  • objects (Array)

    the set of items to remove to this field.

Returns:

  • (Boolean)

    whether it was successful

See Also:



1103
1104
1105
# File 'lib/parse/model/core/actions.rb', line 1103

def op_remove!(field, objects)
  operate_field! field, { __op: :Remove, objects: objects }
end

#op_remove_relation!(field, objects = []) ⇒ Boolean

Perform an atomic remove operation on this relational field.

Parameters:

  • field (String)

    the name of the field in the Parse collection.

  • objects (Array<Parse::Object>) (defaults to: [])

    the set of objects to remove to this relational field.

Returns:

  • (Boolean)

    whether it was successful

See Also:



1146
1147
1148
1149
1150
1151
# File 'lib/parse/model/core/actions.rb', line 1146

def op_remove_relation!(field, objects = [])
  objects = [objects] unless objects.is_a?(Array)
  return false if objects.empty?
  relation_action = Parse::RelationAction.new(field, polarity: false, objects: objects)
  operate_field! field, relation_action
end

#operate_field!(field, op_hash) ⇒ Boolean

Perform an atomic operation on this field. This operation is done on the Parse server which guarantees the atomicity of the operation. This is the low-level API on performing atomic operations on properties for classes. These methods do not update the current instance with any changes the server may have made to satisfy this operation.

Parameters:

  • field (String)

    the name of the field in the Parse collection.

  • op_hash (Hash)

    The operation hash. It may also be of type RelationAction.

Returns:

  • (Boolean)

    whether the operation was successful.



1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
# File 'lib/parse/model/core/actions.rb', line 1059

def operate_field!(field, op_hash)
  field = field.to_sym
  field = self.field_map[field] || field
  if op_hash.is_a?(Parse::RelationAction)
    op_hash = op_hash.as_json
  else
    op_hash = { field => op_hash }.as_json
  end

  # If the object hasn't been saved yet (no id), we can't make field operations
  # Return true to indicate the operation was "successful" locally
  return true if id.nil?

  response = client.update_object(parse_class, id, op_hash, session_token: _session_token)
  if response.error?
    puts "[#{parse_class}:#{field} Operation] #{response.error}"
  end
  response.success?
end

#prepare_save!Boolean

Back-compat alias for Object#run_before_save_callbacks. The canonical name spells out exactly what runs (the before_save callbacks, before phase only) and is symmetric with run_after_save_callbacks / run_before_create_callbacks / run_after_create_callbacks. Retained so existing callers of prepare_save! keep working.

Returns:

  • (Boolean)

    false if a before_save callback halted the chain, else true.



1458
1459
1460
# File 'lib/parse/model/core/actions.rb', line 1458

def prepare_save!
  run_before_save_callbacks
end

#relation_change_operationsArray

Generates an array with two entries for addition and removal operations. The first entry of the array will contain a hash of all the change operations regarding adding new relational objects. The second entry in the array is a hash of all the change operations regarding removing relation objects from this field.

Returns:

  • (Array)

    an array with two hashes; the first is a hash of all the addition operations and the second hash, all the remove operations.



1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
# File 'lib/parse/model/core/actions.rb', line 1481

def relation_change_operations
  return [{}, {}] unless relation_changes?

  additions = []
  removals = []
  # go through all the additions of a collection and generate an action to add.
  relation_updates.each do |field, collection|
    if collection.additions.count > 0
      additions.push Parse::RelationAction.new(field, objects: collection.additions, polarity: true)
    end
    # go through all the additions of a collection and generate an action to remove.
    if collection.removals.count > 0
      removals.push Parse::RelationAction.new(field, objects: collection.removals, polarity: false)
    end
  end
  # merge all additions and removals into one large hash
  additions = additions.reduce({}) { |m, v| m.merge! v.as_json }
  removals = removals.reduce({}) { |m, v| m.merge! v.as_json }
  [additions, removals]
end

#save(session: nil, autoraise: false, force: false, validate: true) ⇒ Boolean

saves the object. If the object has not changed, it is a noop. If it is new, we will create the object. If the object has an id, we will update the record.

You may pass a session token to the session argument to perform this actions with the privileges of a certain user.

Callback order:

  1. before_validation / around_validation / after_validation
  2. before_save / around_save
  3. before_create or before_update / around_create or around_update
  4. [actual save operation]
  5. after_create or after_update
  6. after_save

You can define before and after :save callbacks autoraise: set to true will automatically raise an exception if the save fails

Parameters:

  • session (String) (defaults to: nil)

    a session token in order to apply ACLs to this operation.

  • autoraise (Boolean) (defaults to: false)

    whether to raise an exception if the save fails.

  • force (Boolean) (defaults to: false)

    whether to run callbacks and send request even if there are no changes.

  • validate (Boolean) (defaults to: true)

    whether to run validations (default: true).

Returns:

  • (Boolean)

    whether the save was successful.

Raises:

  • (Parse::RecordNotSaved)

    if the save fails

  • ArgumentError if a non-nil value is passed to session that doesn't provide a session token string.



1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
# File 'lib/parse/model/core/actions.rb', line 1347

def save(session: nil, autoraise: false, force: false, validate: true)
  # Prevent saving objects that have been fetched and found to be deleted
  if _deleted?
    error_msg = "Cannot save deleted object. Object with id '#{@id}' no longer exists on the server."
    raise Parse::Error::ProtocolError, error_msg
  end

  @_session_token = _validate_session_token! session, :save
  return true unless changed? || force

  # Run validations (validation callbacks are now triggered by valid? method)
  # Pass context so `on: :create` and `on: :update` options work with callbacks
  if validate
    validation_context = new? ? :create : :update
    validation_passed = valid?(validation_context)

    unless validation_passed
      if self.class.raise_on_save_failure || autoraise.present?
        raise Parse::RecordNotSaved.new(self), "Validation failed: #{errors.full_messages.join(", ")}"
      end
      return false
    end
  end

  success = false

  # Track if callbacks are halted by a before_save hook returning false
  callback_executed = false
  run_callbacks :save do
    callback_executed = true
    #first process the create/update action if any
    #then perform any relation changes that need to be performed
    success = new? ? create : perform_update(force: force)

    # if the save was successful and we have relational changes
    # let's update send those next.
    if success
      if relation_changes?
        # get the list of changed keys
        changed_attribute_keys = changed - relations.keys.map(&:to_s)
        clear_attribute_changes(changed_attribute_keys)
        success = update_relations
        if success
          changes_applied!
          clear_partial_fetch_state!
        elsif self.class.raise_on_save_failure || autoraise.present?
          raise Parse::RecordNotSaved.new(self), "Failed updating relations. #{self.parse_class} partially saved."
        end
      else
        changes_applied!
        clear_partial_fetch_state!
      end
    elsif self.class.raise_on_save_failure || autoraise.present?
      raise Parse::RecordNotSaved.new(self), "Failed to create or save attributes. #{self.parse_class} was not saved."
    end
  end #callbacks

  # If callbacks were halted (before_save returned false), return false
  return false unless callback_executed

  @_session_token = nil
  success
end

#save!(session: nil, force: false) ⇒ Boolean

Save this object and raise an exception if it fails.

Parameters:

  • session (String) (defaults to: nil)

    a session token in order to apply ACLs to this operation.

  • force (Boolean) (defaults to: false)

    whether to run callbacks and send request even if there are no changes.

Returns:

  • (Boolean)

    whether the save was successful.

Raises:

  • (Parse::RecordNotSaved)

    if the save fails

  • ArgumentError if a non-nil value is passed to session that doesn't provide a session token string.



1417
1418
1419
# File 'lib/parse/model/core/actions.rb', line 1417

def save!(session: nil, force: false)
  save(autoraise: true, session: session, force: force)
end

#set_attributes!(hash, dirty_track = false) ⇒ Hash

Performs mass assignment using a hash with the ability to modify dirty tracking. This is an internal method used to set properties on the object while controlling whether they are dirty tracked. Each defined property has a method defined with the suffix _set_attribute! that can will be called if it is contained in the hash.

Examples:

object.set_attributes!( {"myField" => value}, false)

# equivalent to calling the specific method.
object.myField_set_attribute!(value, false)

Parameters:

  • hash (Hash)

    the hash containing all the attribute names and values.

  • dirty_track (Boolean) (defaults to: false)

    whether the assignment should be tracked in the change tracking system.

Returns:



1544
1545
1546
1547
1548
1549
1550
1551
# File 'lib/parse/model/core/actions.rb', line 1544

def set_attributes!(hash, dirty_track = false)
  return unless hash.is_a?(Hash)
  hash.each do |k, v|
    next if k == Parse::Model::OBJECT_ID || k == Parse::Model::ID
    method = "#{k}_set_attribute!"
    send(method, v, dirty_track) if respond_to?(method)
  end
end

#update(force: false) ⇒ Boolean

Save all the changes related to this object.

Parameters:

  • force (Boolean) (defaults to: false)

    whether to send the update even if there are no changes.

Returns:

  • (Boolean)

    true/false whether it was successful.



1261
1262
1263
1264
# File 'lib/parse/model/core/actions.rb', line 1261

def update(force: false)
  return true unless attribute_changes? || force
  update!(force: force)
end

#update!(raw: false, force: false) ⇒ Boolean

This methods sends an update request for this object with the any change information based on its local attributes. The bang implies that it will send the request even though it is possible no changes were performed. This is useful in kicking-off an beforeSave / afterSave hooks Save the object regardless of whether there are changes. This would call any beforeSave and afterSave cloud code hooks you have registered for this class.

Returns:

  • (Boolean)

    true/false whether it was successful.



1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
# File 'lib/parse/model/core/actions.rb', line 1230

def update!(raw: false, force: false)
  if valid? == false
    errors.full_messages.each do |msg|
      warn "[#{parse_class}] warning: #{msg}"
    end
  end
  if force == true && attribute_changes?.blank? && !new?
    # if we are forcing an update, but there are no attribute changes,
    # we should still mark the updated_at field as changed so that
    # the server updates it.
    if self.class.fields[:updated_at].present?
      self.updated_at = Time.now.utc
      self.updated_at_will_change! if respond_to?(:updated_at_will_change!)
    end
  end
  response = client.update_object(parse_class, id, attribute_updates, session_token: _session_token)
  @_last_response = response
  if response.success?
    result = response.result
    # Because beforeSave hooks can change the fields we are saving, any items that were
    # changed, are returned to us and we should apply those locally to be in sync.
    set_attributes!(result)
  end
  puts "Error updating #{self.parse_class}: #{response.error}" if response.error?
  return response if raw
  response.success?
end

#update_relationsBoolean

Saves and updates all the relational changes for made to this object.

Returns:

  • (Boolean)

    whether all the save or update requests were successful.



1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
# File 'lib/parse/model/core/actions.rb', line 1504

def update_relations
  # relational saves require an id
  return false unless @id.present?
  # verify we have relational changes before we do work.
  return true unless relation_changes?
  raise "Unable to update relations for a new object." if new?
  # get all the relational changes (both additions and removals)
  additions, removals = relation_change_operations

  responses = []
  # Send parallel Parse requests for each of the items to update.
  # since we will have multiple responses, we will track it in array
  [removals, additions].threaded_each do |ops|
    next if ops.empty? #if no operations to be performed, then we are done
    responses << client.update_object(parse_class, @id, ops, session_token: _session_token)
  end
  # check if any of them ended up in error
  has_error = responses.any? { |response| response.error? }
  # if everything was ok, find the last response to be returned and update
  #their fields in case beforeSave made any changes.
  unless has_error || responses.empty?
    result = responses.last.result #last result to come back
    set_attributes!(result)
  end #unless
  has_error == false
end

#uri_pathString

Returns the API uri path for this class.

Returns:

  • (String)

    the API uri path for this class.



1187
1188
1189
# File 'lib/parse/model/core/actions.rb', line 1187

def uri_path
  self.client.url_prefix.path + Client.uri_path(self)
end