Home > Mobile >  How can I return variants of an ActiveStorage image over GraphQL in Ruby on Rails?
How can I return variants of an ActiveStorage image over GraphQL in Ruby on Rails?

Time:01-10

Summary

For a given GraphQL query against a Ruby on Rails 7 endpoint:

query {
    posts(limit: 1) {
        id
        caption
        image {
            web # <- this is a variant name
            thumb # <- another variant name
        }
    }
}

I want a response that looks like this:

{
    "data": {
        "posts": [
            {
                "id": "8d59b87c-c33b-4c29-b18f-a4d6aa2abf45",
                "caption": "Nam ad incidunt. Nisi sequi quibusdam. Nihil quis veritatis. Beatae consequatur excepturi. Et at sapiente. Delectus deleniti unde. Id a quo. Qui aut quod. Est qui quo. Dolores beatae facilis. Sed error vero. Est est at. Alias aut sit. Consequuntur consequ."
                "image": {
                    "web": "some-url-here",
                    "thumb": "another-url-here"
                }
            }
        ]
    }
}

Where the object has an image property with multiple variants defined.

Details

I'm working on a specific GraphQL query endpoint using Rails 7, the graphql-rails, and my intermediate knowledge of Ruby, but I've happened upon a stumbling block.

I have a project that accepts user uploads of photos, which then receive multiple modifications in the form of ActiveStorage variants. Each post a user makes includes one (and only one) attached image to be processed. After upload, a background job generates 5 different variants based on some model-specified dimensions.

For the sake of simplicity, here's the relevant part of the Post model:

class Post < ApplicationRecord
  belongs_to :user
  has_many :comments, dependent: :destroy
  has_one_attached :image, dependent: :destroy do |attachable|
    attachable.variant :web, resize_to_fill: [1200, 1200, { crop: :centre }]
    attachable.variant :opengraph, resize_to_fill: [1200, 630, { crop: :attention }]
    attachable.variant :twitter, resize_to_fill: [1024, 512, { crop: :attention }]
    attachable.variant :mobile, resize_to_fill: [600, 600, { crop: :centre }]
    attachable.variant :thumb, resize_to_fill: [100, 100, { crop: :centre }]
  end

...

The Post type for GraphQL is set to return a field named image of type VariantType, which I've included below in my current, inelegant version:

module Types
  class VariantType < Types::BaseObject
    field :web, String, null: false
    field :opengraph, String, null: false
    field :twitter, String, null: false
    field :mobile, String, null: false
    field :thumb, String, null: false
  end

  def web
    url_for(:web)
  end

  def opengraph
    url_for(:opengraph)
  end

  def twitter
    url_for(:twitter)
  end

  def mobile
    url_for(:mobile)
  end

  def thumb
    url_for(:thumb)
  end

  ... (method definition for url_for removed for clarity)
end

When I attempted to query the endpoint, instead of the JSON response above, I got the below error:

Failed to implement Variant.web, tried:
     
  - `Types::VariantType#web`, which did not exist
  - `ActiveStorage::Attached::One#web`, which did not exist
  - Looking up hash key `:web` or `"web"` on `<ActiveStorage::Attached::One:0x000000012511c5f8>`, but it wasn't a Hash
     
To implement this field, define one of the methods above (and check for typos)

Update 1

The url_for method is defined as:

  def url_for(variant) # rubocop:disable Metrics/MethodLength
    img = object.image.variant(variant)

    # Add protocol if CDN_HOST doesn't have it
    cdn_host = ENV.fetch('CDN_HOST', nil)
    cdn_host = "https://#{cdn_host}" unless cdn_host.nil? || cdn_host.start_with?('http')
    if cdn_host.nil?
      blob_type = if img.is_a?(ActiveStorage::Variant) || img.is_a?(ActiveStorage::VariantWithRecord)
                    :rails_representation
                  else
                    :rails_blob
                  end

      Rails.application.routes.url_helpers.route_for(blob_type, img, only_path: true)
    else
      File.join(cdn_host, img.key) # Returns CDN path for blob
    end

It's a hack, I know, in lieu of a better approach to having my asset URLs appear as https://cdn.example.com/asset.jpg instead of the ugly ActiveStorage proxy URLs.

CodePudding user response:

One way to fix this issue would be to use the variant method provided by ActiveStorage to retrieve the variant you want. You can pass the variant name as a symbol to the variant method, like this:

def web
  object.image.variant(:web).service_url
end

def opengraph
  object.image.variant(:opengraph).service_url
end

def twitter
  object.image.variant(:twitter).service_url
end

def mobile
  object.image.variant(:mobile).service_url
end

def thumb
  object.image.variant(:thumb).service_url
end

This will retrieve the variant you want and return its URL using the service_url method.

Alternatively, you could define your VariantType like this:

module Types
  class VariantType < Types::BaseObject
    field :web, String, null: false, method: :web_url
    field :opengraph, String, null: false, method: :opengraph_url
    field :twitter, String, null: false, method: :twitter_url
    field :mobile, String, null: false, method: :mobile_url
    field :thumb, String, null: false, method: :thumb_url

    def web_url
      object.image.variant(:web).service_url
    end

    def opengraph_url
      object.image.variant(:opengraph).service_url
    end

    def twitter_url
      object.image.variant(:twitter).service_url
    end

    def mobile_url
      object.image.variant(:mobile).service_url
    end

    def thumb_url
      object.image.variant(:thumb).service_url
    end
  end
end

This way, you can define your fields with the method option, which allows you to specify a method to be called to resolve the field's value.

  • Related