Active Storage meets GraphQL Pt. 2: Exposing attachment URLs
Tags: ruby, rails, graphql
Published: 2019-04-23 14:39:29

---
title: Active Storage meets GraphQL Pt. 2: Exposing attachment URLs
published: true
canonical_url: https://evilmartians.com/chronicles/active-storage-meets-graphql-pt-2-exposing-attachment-urls
description: Serving files with Active Storage and GraphQL 
tags: ruby, rails, graphql
cover_image: https://thepracticaldev.s3.amazonaws.com/i/ve7bfdg1ecea86eudoc8.png
---

In the [previous post](https://dev.to/evilmartians/active-storage-meets-graphql-direct-uploads-3n38) I shared some tips on adding Active Storage's direct uploads to Rails+GraphQL applications.

So, now we know how to upload files. What's next? Let's move to the next step: **exposing attachments URLs via GraphQL API**.


Seems like an easy task, right? Not exactly.

These are the challenges we faced when doing it in our own application:

- Dealing with N+1 queries
- Making it possible for clients to request a specific image variant.
 
## N+1 problem: batch loading to the rescue

Let's first try to add the `avatarUrl` field to our `User` type in a naïve way:

```ruby
module Types
  class User < GraphQL::Schema::Object
    field :id, ID, null: false
    field :name, String, null: false
    field :avatar_url, String, null: true

    def avatar_url
      # That's an official way for generating
      # Active Storage blobs URLs outside of controllers 😕
      Rails.application.routes.url_helpers
           .rails_blob_url(user.avatar)
    end
  end
end
```

Assume that we have an endpoint which returns all the users, e.g., `{ users { name avatarUrl } }`. If you run this query in development and take a look at the Rails server logs in your console, you will see something like this:

```
D, [2019-04-15T22:46:45.916467 #2500] DEBUG -- :   User Load (0.9ms)  SELECT users".* FROM "users"
D, [2019-04-15T22:46:45.919362 #2500] DEBUG -- :   ActiveStorage::Attachment Load (0.9ms)  SELECT "active_storage_attachments".* FROM "active_storage_attachments" WHERE "active_storage_attachments"."record_type" = $1 AND "active_storage_attachments"."name" = $2 AND "active_storage_attachments"."record_id" = $3 [["record_type", "User"], ["name", "avatar"], ["record_id", 12]]
D, [2019-04-15T22:46:45.922420 #2500] DEBUG -- :   ActiveStorage::Blob Load (1.0ms)  SELECT "active_storage_blobs".* FROM "active_storage_blobs" WHERE "active_storage_blobs"."id" = $1  [["id", 9]]
D, [2019-04-15T22:46:45.919362 #2500] DEBUG -- :   ActiveStorage::Attachment Load (0.9ms)  SELECT "active_storage_attachments".* FROM "active_storage_attachments" WHERE "active_storage_attachments"."record_type" = $1 AND "active_storage_attachments"."name" = $2 AND "active_storage_attachments"."record_id" = $3 [["record_type", "User"], ["name", "avatar"], ["record_id", 13]]
D, [2019-04-15T22:46:45.922420 #2500] DEBUG -- :   ActiveStorage::Blob Load (1.0ms)  SELECT "active_storage_blobs".* FROM "active_storage_blobs" WHERE "active_storage_blobs"."id" = $1  [["id", 10]]
D, [2019-04-15T22:46:45.919362 #2500] DEBUG -- :   ActiveStorage::Attachment Load (0.9ms)  SELECT "active_storage_attachments".* FROM "active_storage_attachments" WHERE "active_storage_attachments"."record_type" = $1 AND "active_storage_attachments"."name" = $2 AND "active_storage_attachments"."record_id" = $3 [["record_type", "User"], ["name", "avatar"], ["record_id", 14]]
D, [2019-04-15T22:46:45.922420 #2500] DEBUG -- :   ActiveStorage::Blob Load (1.0ms)  SELECT "active_storage_blobs".* FROM "active_storage_blobs" WHERE "active_storage_blobs"."id" = $1  [["id", 15]]
```

For each user we load an `ActiveStorage::Attachment` and an `ActiveStorage::Blob` record: **2*N + 1** records (where N is the number of users).


We already discussed this problem in the ["Rails 5.2: Active Storage and beyond"](https://evilmartians.com/chronicles/rails-5-2-active-storage-and-beyond) post, so, I'm not going to repeat the technical details here.

**tl;dr** For _classic_ Rails apps we have a built-in scope for preloading attachments (e.g. `User.with_attached_avatar`) or can generate the scope ourselves knowing the way Active Storage names internal associations.

GraphQL makes preloading data a little bit trickier–we don't know beforehand which data is needed by the client and cannot just add `with_attached_<smth>` to every Active Record collection ('cause that would add an additional overhead when we don't need this data).

That's why classic preloading approaches (`includes`, `eager_load`, etc.) are not very helpful for building GraphQL APIs. Instead, most of the applications use the [_batch loading_ technique](https://engineering.universe.com/batching-a-powerful-way-to-solve-n-1-queries-every-rubyist-should-know-24e20c6e7b94).

One of the ways to do that in a Ruby app is to add the [`graphql-batch` gem](https://github.com/Shopify/graphql-batch) by Shopify. It provides a core API for writing _batch loaders_ with a [Promise-like](http://promisesaplus.com/) interface.

Although no batch loaders included into the gem by default, there is an [`association_loader`](https://github.com/Shopify/graphql-batch/blob/master/examples/association_loader.rb) example which we can use for our task (more precisely, we use [this enhanced version](https://gist.github.com/palkan/03eb5306a1a3e8addbe8df97a298a466) which supports scopes and nested associations).

Let's use it to solve our N+1 issue:


```ruby
def avatar_url
  AssociationLoader.for(
    object.class,
    # We should provide the same arguments as
    # the `preload` or `includes` call when do a classic preloading
    avatar_attachment: :blob
  ).load(object).then do |avatar|        
    next if avatar.nil?        
    Rails.application.routes.url_helpers.rails_blob_url(avatar)        
  end
end
```

**NOTE:** the `then` method we use above is not a [`#yield_self`](https://ruby-doc.org/core-2.6.2/Object.html#method-i-yield_self) alias, it's an API provided by the [`promise.rb` gem](https://github.com/lgierth/promise.rb).

The code looks a little bit overloaded, but it works and makes only 3 queries independently on the number of users. Keep on reading to see how we can transform this into a human-friendly API.

## Dealing with variants

We want to leverage the power of GraphQL and allow clients to specify the desired image variants (e.g., thumbs, covers, etc.):

![API example](https://thepracticaldev.s3.amazonaws.com/i/mulczhx009kds5c57ix4.png)

From the code perspective we want to do the following:

```ruby
user.avatar.variant(:thumb) # == user.avatar.variant(resize_to_fill: [64, 64])
```

Unfortunately, Active Storage doesn't have a concept of _variants_ (predefined, named transformations) yet. That will likely be included in Rails 6.x (where x > 0) when [this PR](https://github.com/rails/rails/pull/35290) (or its variation) gets merged.

We decided not to wait and implement this functionality ourselves: this [small patch](https://gist.github.com/palkan/ac4a2eca053a80047c91160c191b4701) by [@bibendi](https://github.com/bibendi) adds the ability to define named variants in a YAML file:

```yml
# config/transformations.yml

thumb:
  convert: jpg
  resize_to_fill: [64, 64]

medium:
  convert: jpg
  resize_to_fill: [200, 200]
```

Since we have the same transformation settings for all the attachments in the app, this global configuration works for us well.

Now we need to integrate this functionality into our API.

First, we add an [_enum type_](https://graphql-ruby.org/type_definitions/enums.html) to our schema representing a particular variant from the `transformations.yml`:

```ruby
class ImageVariant < GraphQL::Schema::Enum
  description <<~DESC
    Image variant generated with libvips via the image_processing gem.
    Read more about options here https://github.com/janko/image_processing/blob/master/doc/vips.md#methods
  DESC

  ActiveStorage.transformations.each do |key, options|
    value key.to_s, options.map { |k, v| "#{k}: #{v}" }.join("\n"), value: key
  end
end
```

Thanks to Ruby's metaprogramming nature we can define our type dynamically using the configuration object–our `transfromations.yml` and the `ImageVariant` enum will always be in sync!

![ImageVariant type](https://thepracticaldev.s3.amazonaws.com/i/zyucz9hf42o3qx5zj4ym.png)

Finally, let's update our field definition to support variants:

```ruby
module Types
  class User < GraphQL::Schema::Object
    field :avatar_url, String, null: true do
      argument :variant, ImageVariant, required: false
    end

    def avatar_url(variant: nil)
      AssociationLoader.for(
        object.class,
        avatar_attachment: :blob
      ).load(object).then do |avatar|
        next if avatar.nil?
        avatar = avatar.variant(variant) if variant
        Rails.application.routes.url_helpers.url_for(avatar)
      end
    end
  end
end
```


## Bonus: adding a field extension

Adding this amount of code every time we want to add an attachment url field to a type doesn't seem to be an elegant solution, does it?

While looking for a better option, I found a [Field Extensions](https://graphql-ruby.org/type_definitions/field_extensions.html) API for `graphql-ruby`. "Looks like exactly what I was looking for!", I thought.

Let me first show you the final field definition:

```ruby
field :avatar_url, String, null: true, extensions: [ImageUrlField]
```

That's it! No more `argument`-s and loaders. Adding the extension makes everything work the way we want!


And here is the annotated code for the extension:

```ruby
class ImageUrlField < GraphQL::Schema::FieldExtension
  attr_reader :attachment_assoc

  def apply
    # Here we try to define the attachment name:
    #  - it could be set explicitly via extension options
    #  - or we imply that is the same as the field name w/o "_url"
    # suffix (e.g., "avatar_url" => "avatar") 
    attachment = options&.[](:attachment) ||
                  field.original_name.to_s.sub(/_url$/, "")

    # that's the name of the Active Record association
    @attachment_assoc = "#{attachment}_attachment"

    # Defining an argument for the field
    field.argument(
      :variant,
      ImageVariant,
      required: false
    )
  end

  # This method resolves (as it states) the field itself
  # (it's the same as defining a method within a type)
  def resolve(object:, arguments:, **rest)
    AssociationLoader.for(
      object.class,
      # that's where we use our association name
      attachment_assoc => :blob
    )
  end

  # This method is called if the result of the `resolve`
  # is a lazy value (e.g., a Promise – like in our case)
  def after_resolve(value:, arguments:, object:, **rest)
    return if value.nil?
    
    variant = arguments.fetch(:variant, :medium)
    value = value.variant(variant) if variant

    Rails.application.routes.url_helpers.url_for(value)
  end
end
```

Happy _graphQLing_ and active _storing_!

---

Read more dev articles on [https://evilmartians.com/chronicles](https://evilmartians.com/chronicles)!