Keep your codebase clean with Rails API and Graphql Connection Helpers

Yannis Kolovos
5 min readApr 27, 2020

--

Abstract: This article is about Rails API, Graphql and Search Object Connection Helpers like: filter_by, order_by, total_count and search.

For each project you will probably need something like order_by, filter_by, total_count maybe search for your listing scopes. Any client will require this functionality so it becomes a core part of your API.

Search Object is a great gem when it comes to Graphql Relay Connections. You can use an inherited resolver from Resolvers::BaseSearchResolver and resolve your connections using filters, order or any other complicated queries. This is the place where we are able to manipulate our scope.

For each model we have to create and use a new resolver. To avoid rewriting all these methods on each resolver we will demonstrate how can we abstract re-usable helpers!

Note: We are using Rails API (6.0) and these are the gems we will need. For search we will use searchkick (it uses Elasticsearch) but you can change that based on the search engine you use.

gem 'rails', '~> 6.0.2', '>= 6.0.2.1'
gem 'graphql'
gem 'search_object'
gem 'search_object_graphql'
gem 'searchkick', '~> 4.3'
gem 'bonsai-elasticsearch-rails', '~> 7.0.1'

Once we have our project up and running we continue with the implementation of our helpers:

Order by:

One of the most common functionality we need in any API is to order the scope based on an attribute and a direction. For example if we have a users scope and we want to order by created_at desc

scope.order(created_at: :desc)

To avoid writing code for each attribute lets create a method that we can order by desc, asc any of the model’s attribute.

Implementation on the BaseSearchResolver:

This will generate a direction (Types::BaseEnum) field with values of desc and asc, and a collection with all model’s attributes so we will be able to sort by.

Use it on the UsersResolver:

It will generate something similar to this:

The dropdown field is a collection with all user’s attributes so we can combine the order_by with any attribute, asc or desc!

Note: As you can see both fields are required (direction and field)

Filter by datetime fields:

Another great helper is when you want to filter a scope by an datetime field using a datetime range.

For example we want to filter all the users that created from the beginning of the month until now:

scope.where(created_at: 1.month.ago.beginning_of_day..Time.now)

Would it be great if we can generate a filter_by method with all models’s datetime fields?

To do this, first we need to select only the datetime attributes from the model so we can after create a range on the scope.

Lets do that on the ApplicationRecord class so the method will be available to any ActiveRecord model.

Implementation on the BaseSearchResolver:

Use it on the UsersResolver:

This will generate something similar to this:

Note: both fields (from, to) are required so we can create the range on the scope.

Note: When you’re sending data from the graphiql-explorer or a query, the fields from and to are GraphQL::Types::ISO8601DateTime so it has to be send in iso8601 format:

Time.now.iso8601(9).to_s

The output should be a valid ISO 8601 format:

“2020–04–22T17:45:23.548844000+03:00”

Search scope:

Another great helper, if you are using Searchkick, is to expose the model’s search on the connection so the scope can be searchable.

Implementation on the BaseSearchResolver:

Update: The default type on search is option(:search, type: types.String) but if you want to use it with variables in your query, you will get a Type mismatch error so you have to change it to option(:search, type: String)

Note: We filter the scope with the search’s matched ids, in this way we can apply any other method on the scope without changing the collection plus we use execute: false to avoid unnecessary loading.

ids = klass.search(value, {execute: false, select: [:id]}).map(&:id)          scope.where(id: ids)

Use it on the UsersResolver:

The user needs to be a valid Searckick model.

The search input on the user’s connection:

Total Count:

For each connection we might need a total_count to count all the scoped items. When we use {edge {node}} and first in the query these are the paginated items not the actual size of the scope’s collection, so this method its a kid of a required. This is not supported by default so we have to implement it.

As seen above we use:

type Types::UserType.connection_type, null: false

This is default UserType's Connection generated by Graphql so to implement the total_count we have to create a new GraphQL::Types::Relay::BaseConnection connection and implement the total_count method.

Implementation on the BaseSearchResolver:

Use it on the UsersResolver:

The easy way:

The above implementation is if you want to use total_count only to specific connections. To use total_count to all connections you have specifying the connection_type_class:

On resolver type Types::UserType.connection_type, null: false

The total_count is now displayed in our connection!

Finally expose the UsersResolver to QueryType:

All together:

So if we combine all together will have powerful filter, order and search conditions plus the total_count on the users connection, also will be able to use all these with the existing Graphql’s build-in methods like slicing (last, first) or any pagination methods!

Conclusion:

GraphQL is strongly-typed language and oftentimes hard when you have to declare everything, review execution semantics and static validations, but in the end once you set up all neat and organized it gives you so much flexibility and a stable environment.

The combination of the GraphQL’s connections with searching, filtering total_count and ordering a scope and the flexibility of the GraphQL query language is probably the best approach to any modern API.

The reason I created this article is because with this approach I was able to delete so many files (Scalars and InputTypes) and keep my project clean and elegant.

All the helpers can be found combined here

Thank you for reading!

--

--

Yannis Kolovos
Yannis Kolovos

Written by Yannis Kolovos

Backend Engineer at Humid Research

No responses yet