Ruby on Rails Graphiti file attachment/file upload
Tags: activestorage, shrine, rails, graphiti
Published: 2019-12-29 15:09:25

I have been using [Graphiti](https://www.graphiti.dev/guides/) gem to build the API for the upcoming product in PostCo. While it is great that it makes building [JSON:API](https://jsonapi.org/) compliant API endpoints a breeze, and made API resource the first-class citizen, it still quite lacks some important features, which includes file attachment support. I will talk about Graphiti in some other time but right now lets dive into today's topic.

In this article, you will understand how to add the file attachment functionality into the Graphiti resource. I will cover 2 of the most popular file attachment libraries recently, [Active Storage](https://edgeguides.rubyonrails.org/active_storage_overview.html) and [Shrine](https://shrinerb.com/), and I will be uploading the file in Base64 string format.

Assuming the User model file looks something like this, where it contains 2 attachments, an avatar photo, and an ID card photo.
``` ruby
class User < ApplicationRecord
  # some other codes
  User::ATTACHMENT_ATTRS = [:avatar, :id_card].freeze

  ## Active Storage
  User::ATTACHMENT_ATTRS.each do |attachment_attr|  
    has_one_attached :"#{attachment_attr}"
  end
  # or
  # has_one_attached :avatar
  # has_one_attached :id_card

  ## Shrine
  include ImageUploader::Attachment(:avatar)
  include ImageUploader::Attachment(:id_card)

  # some more codes
end


## Shrine
class ImageUploader < Shrine
  plugin :data_uri
end
```

### Write

During *POST* and *PUT* API call, the Graphiti Resource will assign all the submitted attributes and save it. But both file attachment libraries do not support attaching files by assigning the attribute.

Therefore, we need to extract the attachment data out from the submitted attributes and attach them separately. Luckily, Graphiti exposes the [persistence lifecycle hooks](https://www.graphiti.dev/guides/concepts/resources#persistence-lifecycle-hooks) to allow developers to add in additional logic during persisting the data.

To achieve the goal, we overwrite the `assign_attributes` to do exactly what we need to, extract the attachment data and assign them separately!

``` ruby
class UserResource < ApplicationResource
  
  # some other codes...

  def assign_attributes(model_instance, attributes)
    attachments = extract_attributes(attributes)
    attach_data(model_instance, attachments)

    attributes.each_pair { |key, value| model_instance.send(:"#{key}=", value) }
  end

  private

  def attach_data(model_instance, attachments)
    attachments.each do |attribute, data|
      ## Active Storage
      model_instance.send(:"#{attribute}").attach(data: data)

      ## Shrine
      model_instance.send(:"#{attribute}_data_uri=", data)
    end
  end

  def extract_attachments(attrs)
    attachments = {}
    User::ATTACHMENT_ATTRS.each do |attachment_attr|
      attachment = attrs.delete(attachment_attr)
      attachments[attachment_attr] = attachment if attachment.present?
    end
    attachments
  end

  # some more codes

end
```
### Read

To return the attachment's URL when reading, we simply check each attachment and return the URL if it is available.
``` ruby
class UserResource < ApplicationResource

  User::ATTACHMENT_ATTRS.each do |attachment_attr|
    attribute attachment_attr, :string do
      ## Active Storage
      attachment = @object.send(:"#{attachment_attr}")
      attachment.attached? ? rails_blob_path(attachment, only_path: true) : ''
      
      ## Shrine
      @object.send(:"#{attachment_attr}_url") || ''
    end
  end
  # other attributes...
  attribute :created_at, :datetime, writable: false
  attribute :updated_at, :datetime, writable: false

  # some other codes...

end
```
----------------------
Here's what the final Resource code will look like:
``` ruby
class UserResource < ApplicationResource

  User::ATTACHMENT_ATTRS.each do |attachment_attr|
    attribute attachment_attr, :string do
      ## Active Storage
      attachment = @object.send(:"#{attachment_attr}")
      attachment.attached? ? rails_blob_path(attachment, only_path: true) : ''
      
      ## Shrine
      @object.send(:"#{attachment_attr}_url") || ''
    end
  end
  # other attributes...
  attribute :created_at, :datetime, writable: false
  attribute :updated_at, :datetime, writable: false

  def assign_attributes(model_instance, attributes)
    attachments = extract_attributes(attributes)
    attach_data(model_instance, attachments)

    attributes.each_pair { |key, value| model_instance.send(:"#{key}=", value) }
  end

  private

  def attach_data(model_instance, attachments)
    attachments.each do |attribute, data|
      ## Active Storage
      model_instance.send(:"#{attribute}").attach(data: data)

      ## Shrine
      model_instance.send(:"#{attribute}_data_uri=", data)
    end
  end

  def extract_attachments(attrs)
    attachments = {}
    User::ATTACHMENT_ATTRS.each do |attachment_attr|
      attachment = attrs.delete(attachment_attr)
      attachments[attachment_attr] = attachment if attachment.present?
    end
    attachments
  end
end
```

And Voilà! Now you can upload attachments through the Graphiti API!

If you find this article is useful, please do give a ❤️! If you have any questions or suggestions, please do comment in the discussion area below!