search_object

A 2-post collection

Introducing SearchObject GraphQL Plugin

When I started using GraphQL, I immediately saw, that SearchObject would be a perfect fit for search resolvers.

Having a GraphQL query to fetch the first 10 most recent published news posts would look something like this:

query {  
  posts(first: 10, categoryName: "News", order: "RECENT", published: true) {    
    id  
    title  
    body  
    author {      
       id
       name
    }
  }
}

And it would have a corresponding SearchObject:

class Resolvers::PostSearch  
  include SearchObject.module  

  scope { Post.all }    

  option :categoryName, with: :apply_category_name_filter
  option :published, with: :apply_published_filter
  option :order, enum: %i(RECENT VIEWS LIKES)  

  # ... definitions of the option methods
end  

So clean. ☀️

But then, PostSearch have to be connected with GraphQL Ruby gem:

PostOrderEnum = GraphQL::EnumType.define do  
  name 'PostOrder'

  value 'RECENT'
  value 'VIEWS'
  value 'LIKES'
end  

Types::QueryType = GraphQL::ObjectType.define do  
  name 'Query'

  field :posts, types[Types::PostType] do
    argument :categoryName, types.String  
    argument :published, types.Boolean  
    argument :order, PostOrderEnum
    resolve ->(_obj, args, _ctx) { Resolvers::PostSearch.results(filters: args.to_h) } 
  end
end  

That isn't so bad. 🤔

But then, thinking about how can this code can change in the future:

  • adding/removing options would involve going to both files
  • adding a new order option would mean searching for the PostOrderEnum and manually sync it with PostSearch enum
  • reusing PostSearch in other types, for queries like: query { user(id: 1) { posts(published: true) } }
    • requires copy and paste argument/type definitions
    • which makes updating the resolver even harder

Yikes! 😤 😷

This is where SearchObject::Plugin::GraphQL comes in. It puts type definitions and the resolver itself:

class Resolvers::PostSearch  
  # include the the plugin
  include SearchObject.module(:graphql)

  # documentation and type about this resolver
  # can be provided into the resolver itself
  type types[Types::PostType]
  description 'Lists posts'

  # enums or other types can also be nested
  OrderEnum = GraphQL::EnumType.define do
    name 'PostOrder'

    value 'RECENT'
    value 'VIEWS'
    value 'LIKES'
  end

  scope { Post.all }    

  # options just need to have a their type specifed.
  option :categoryName, type: types.String, with: :apply_category_name_filter
  option :published, type: types.Boolean, with: :apply_published_filter  
  # enums are automatically handled
  option :order, type: OrderEnum

  # ... definitions of the option methods
end  

Then PostSearch can be used just as GraphQL::Function:

Types::QueryType = GraphQL::ObjectType.define do  
  name 'Query'

  field :posts, function: Resolvers::PostSearch
end  

Now, changing filter options requires changing only a single file. PostSearch can be reused in other types, by just adding function: Resolvers::PostSearch.

For more information check SearchObject::Plugin::GraphQL example.

SearchObject 1.1

Today I release version 1.1 of my Search Object. It has two major new features.

Using instance method for straight dispatch

Suggested and developed by Genadi Samokovarov:

class ProductSearch  
  include SearchObject.module

  scope { Product.all }

  option :date, with: :parse_dates

  private

  def parse_dates(scope, value)
    # some "magic" method to parse dates
  end
end  
Classes mixed with SearchObject can be inherited

I had this setting on a branch for a long time. In one of the projects I worked on it would have been usefull to have a search base object:

class BaseSearch  
  include SearchObject.module

  # ... options and configuration
end

# and used as 
class ProductSearch < BaseSearch  
  scope { Product }
end  

Its implementation actually might push me into an interesting refactoring.

Other minor features
  • I added Rubocop for verifying the project
  • I started testing against Ruby 2.2
  • I stopped testing against Ruby 1.9