Module: HasHelpers::SyncUtils

Defined in:
app/utils/has_helpers/sync_utils.rb

Class Method Summary collapse

Class Method Details

.apply_mapping(resource, data, resource_key:, is_destroyed: false) ⇒ Object

Applies a custom mapping to a resource, updating it with values from the provided data. It now supports handling resource destruction.

Arguments:

- resource (ApplicationRecord): The record to update, e.g., a User.
- data (Hash): The external data to sync with the resource.
- resource_key (Symbol): The key for the resource in the sync configuration.
- is_destroyed (Boolean): Whether the resource is being destroyed (defaults to false).

Returns:

- The updated resource or nil if no updates are made.
- When the resource is destroyed, it triggers a sync for deletion.


50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
# File 'app/utils/has_helpers/sync_utils.rb', line 50

def self.apply_mapping(resource, data, resource_key:, is_destroyed: false)
  return unless resource && (data.present? || is_destroyed)

  if is_destroyed
    # Logic for when the resource is destroyed, you may want to sync this deletion
    # Example: Mark the corresponding remote record as deleted
    return handle_destruction(resource)
  end

  # Transform the data keys to snake_case if provided
  data = self.deep_transform_keys_to_snake_case(data) if data

  # Fetch the custom mapping for the resource
  mapping = HasHelpers.config.sync_custom_mapping[resource_key.to_sym]

  # If mapping is nil, create or update resource directly with the data
  if mapping.nil?
    # Update the resource with external data directly
    resource.assign_attributes(data.deep_symbolize_keys)
    return save_with_result(resource)
  end

  # Symbolize the keys of the external data to match the mapping
  data = data.deep_symbolize_keys if data
  attrs = {}

  # Iterate over the mapping and apply it to the resource
  mapping.each do |model_key, mapper|
    # Add the mapped value to the attributes hash
    case mapper
    when Proc
      # Call the proc with the external data
      attrs[model_key] = mapper.call(data)
    when Symbol
      # Map the symbol directly from the external data
      attrs[model_key] = data[mapper]
    end
  end

  resource.assign_attributes(attrs)

  save_with_result(resource)
end

.deep_transform_keys_to_snake_case(data) ⇒ Object

Transforms the keys of a nested hash or array to snake_case (recursive transformation for deep structures)

Arguments:

- data (Hash, Array, or other): The data structure to transform.

Returns:

- The transformed data structure with all keys in snake_case.


127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
# File 'app/utils/has_helpers/sync_utils.rb', line 127

def self.deep_transform_keys_to_snake_case(data)
  case data
  when Hash
    # if data is serialized, deserialize
    if data["_aj_serialized"].present?
      return ActiveJob::Serializers.deserialize(data)
    end

    data.transform_keys { |key| key.to_s.underscore.to_sym }.each_with_object({}) do |(key, value), result|
      result[key] = deep_transform_keys_to_snake_case(value)
    end
  when Array
    data.map { |element| deep_transform_keys_to_snake_case(element) }
  else
    data
  end
end

.dependencies_satisfied?(data, dependencies) ⇒ Boolean

Checks whether all declared sync dependencies are satisfied.

Arguments:

- data (Hash): The payload being synced.
- dependencies (Array<String, Symbol>): Dependency names (e.g. "user", "organization").

Returns:

- true if all dependencies exist
- false if any dependency is missing

Returns:

  • (Boolean)


167
168
169
170
171
172
173
174
175
176
177
178
179
180
# File 'app/utils/has_helpers/sync_utils.rb', line 167

def self.dependencies_satisfied?(data, dependencies)
  Array(dependencies).compact.each do |dependency|
    next if dependency.blank?

    dependency_class = resolve_dependency_class(dependency)
    next unless dependency_class

    dependency_id = extract_dependency_id(data, dependency)

    return false unless dependency_id && dependency_class.exists?(id: dependency_id)
  end

  true
end

.handle_destruction(resource) ⇒ Object

Handles the logic when a resource is destroyed

For example, sync with an external system to delete the resource or mark it as inactive.

Arguments:

- resource (ApplicationRecord): The record to sync (which is being destroyed).

Returns:

- `true` if the sync was successful, or an error message if it wasn't.


104
105
106
107
108
109
110
111
112
113
114
115
116
# File 'app/utils/has_helpers/sync_utils.rb', line 104

def self.handle_destruction(resource)
  unless resource.respond_to?(:id) && resource.id.present?
    message = "Failed to destroy #{resource.class.name}: missing resource id"
    Rails.logger.error(message)
    return message
  end

  return true if resource.destroy

  message = "Failed to destroy resource #{resource.class.name} with ID #{resource.id}: #{resource.errors.full_messages.join(', ')}"
  Rails.logger.error(message)
  message
end

.save_with_result(resource) ⇒ Object

Attempts to save the resource and returns a success or error message

Arguments:

- resource (ApplicationRecord): The resource to save.

Returns:

- `true` if the resource was saved successfully, or an error message if it failed.


153
154
155
# File 'app/utils/has_helpers/sync_utils.rb', line 153

def self.save_with_result(resource)
  resource.save ? true : resource.errors.full_messages.join(", ")
end

.touch_dependencies!(data, dependencies) ⇒ Object

Touches declared dependency records so their updated_at reflects related sync changes.

Arguments:

- data (Hash): The payload being synced.
- dependencies (Array<String, Symbol>): Dependency names.

Returns:

- nil (side-effect only)


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
# File 'app/utils/has_helpers/sync_utils.rb', line 191

def self.touch_dependencies!(data, dependencies)
  Array(dependencies).compact.each do |dependency|
    next if dependency.blank?

    dependency_class = resolve_dependency_class(dependency)
    next unless dependency_class

    dependency_id = extract_dependency_id(data, dependency)
    unless dependency_id
      Rails.logger.warn("Could not touch dependency #{dependency}: missing dependency id in payload")
      next
    end

    dependency_record = dependency_class.find_by(id: dependency_id)
    unless dependency_record
      Rails.logger.warn("Could not touch dependency #{dependency}: #{dependency_class.name} with id=#{dependency_id} was not found")
      next
    end

    dependency_record.touch # rubocop:disable Rails/SkipsModelValidations -- touch required to bump updated_at on sync dependencies without running callbacks/validations
  rescue => e
    Rails.logger.warn("Could not touch dependency #{dependency} with id=#{dependency_id}: #{e.message}")
  end

  nil
end