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.
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)
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.
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.