Skip to content

Instantly share code, notes, and snippets.

@Zooip
Last active June 11, 2021 19:04
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save Zooip/4770a33ffbc030fbf22f248560101413 to your computer and use it in GitHub Desktop.
Save Zooip/4770a33ffbc030fbf22f248560101413 to your computer and use it in GitHub Desktop.
Graphiti ActiveStorage attachment
# app/resources/application_resource.rb
# ApplicationResource is similar to ApplicationRecord - a base class that
# holds configuration/methods for subclasses.
# All Resources should inherit from ApplicationResource.
class ApplicationResource < Graphiti::Resource
# Use the ActiveRecord Adapter for all subclasses.
# Subclasses can still override this default.
self.abstract_class = true
self.adapter = Graphiti::Adapters::ActiveRecord
self.base_url = Rails.application.routes.default_url_options[:host]
self.endpoint_namespace = '/api/v1'
around_persistence :handle_attachment_attributes
protected
# Extract attachment from attributes before saving model
# Attach attachments after save
def handle_attachment_attributes(attrs)
attachments_params = extract_attachments_params(attrs)
model = yield
save_attachments(model, attachments_params)
end
#@return [Hash] Attributes definitions matching type "attachment"
def attachment_attributes
self.attributes.select{|_name, opts| opts[:type].to_s == "attachment"}
end
#@return [Hash] Extracted attachment params
def extract_attachments_params(attrs)
Hash.new.tap do |attachments_params|
attachment_attributes.each do |name, _opts|
attachments_params[name] = attrs.delete(name.to_sym)
end
end
end
# Add attachments to saved model
def save_attachments(model, attachments_params)
attachments_params.each do |name, params|
# ActiveStorage attachment name is inferred from the param name
# but can also be overridden with option `:attached_as`
type = attachment_attributes[name].fetch(:attached_as){name}
attach(model, type, params)
end
end
# Attach a single attachment
def attach(record, type, params)
file = tempfile_for(params[:data])
record.send(type).attach(io: file, **params.slice(:filename, :content_type))
end
def tempfile_for(data)
file = Tempfile.new('temp')
file.binmode
file.write(Base64.decode64(data))
file.close
file.open
file
end
end
# app/models/types/attachment.rb
module Types
module Attachment
# validate attachment format
# Use Types::Hash.strict to avoid sending unwanted params to ActiveStorage
WriteAttachment = Types::Hash.schema(
data: Types::Strict::String,
filename: Types::Strict::String,
content_type?: Types::Strict::String
).with_key_transform(&:to_sym).strict
# Serialize to Hash
ReadAttachment = Types::Nominal::Hash.constructor do |attachment|
if attachment.attached?
{
url: Rails.application.routes.url_helpers.rails_blob_path(attachment, only_path: true),
filename: attachment.filename,
content_type: attachment.content_type
}
else
nil
end
end
end
end
{
"data": {
"id": "2235",
"type": "places",
"attributes": {
"name": "Test name",
"coordinates": {
"lon": 5.11135,
"lat": 45.41
},
"image": {
"url": "/rails/active_storage/blobs/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBBcmNDIiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--6ba858d6903afb5290a959befe3fda6481c6332b/present.png",
"filename": "image.png",
"content_type": "image/png"
}
}
},
"meta": {}
}
{
"data":{
"type": "places",
"attributes":{
"name": "Test name",
"coordinates": {
"lon": 5.11135,
"lat": 45.41
},
"image": {
"filename": "image.png",
"data": "iVBORw0KGgoAAAANSUh...truncated base64...5QzFVEtFUveAAAAAElFTkSuQmCC"
}
}
}
}
# config/initializers/graphiti_types.rb
Graphiti::Types[:attachment] = {
params: Types::Attachment::WriteAttachment,
read: Types::Attachment::ReadAttachment,
write: Types::Attachment::WriteAttachment,
kind: 'record',
canonical_name: :attachment,
description: 'Active Storage file attached to the resource model'
}
# app/models/place.rb:7
class Place < ApplicationRecord
validates :name, presence: true
validates :coordinates, presence: true
has_one_attached :photo
end
# app/resources/place_resource.rb
class PlaceResource < ApplicationResource
attribute :name, :string
attribute :coordinates, :geo_point
attribute :image, :attachment, attached_as: "photo"
def base_scope
Place.with_attached_photo
end
end
@Zooip
Copy link
Author

Zooip commented May 28, 2020

This will only works for has_one_attached attachement

@kumaresan-rails
Copy link

Thanks! Any suggestions/plan to update it for has_many_attached?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment