DEV Community

Andy Chong for PostCo

Posted on • Edited on

Ruby on Rails Graphiti file attachment/file upload

I have been using Graphiti gem to build the API for the upcoming product in PostCo. While it is great that it makes building JSON:API 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 and Shrine, 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.

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 
Enter fullscreen mode Exit fullscreen mode

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 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!

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 
Enter fullscreen mode Exit fullscreen mode

Read

To return the attachment's URL when reading, we simply check each attachment and return the URL if it is available.

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 
Enter fullscreen mode Exit fullscreen mode

Here's what the final Resource code will look like:

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 
Enter fullscreen mode Exit fullscreen mode

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!

Top comments (2)

Collapse
 
pedrohgrandin profile image
Pedro Henrique Grandin • Edited

Congrats on the article!

Here it's raising an error at my Resource:
NoMethodError: undefined method `extract_image_attributes'

Am I missing something?
I'm using with Active Storage

Collapse
 
andychongyz profile image
Andy Chong PostCo

Hey man, sorry for the late reply, it was a typo, it should be calling extract_attributes. But thanks!