open_source

A 4-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.

Introducing KittyEvents

During the Christmas break me and Mike were discussing a new feature at Product Hunt. The feature required scheduling an ActiveJobs when a user signs up, votes or submits a comment.

There is SignUp object which handles user registration. So scheduling a new background job there is quite simple:

module SignUp  
  # ... handle user sign up

  def after_sign_up(user)
    WelcomeEmailWorker.perform_later(user)
    WelcomeTweetWorker.perform_later(user)
    SyncProfileImageWorker.perform_later(user)
    NewFancyFeatureWorker.perform_later(user) # <- new worker
  end
end  

Unfortunately after_sign_up method was becoming quite large ☚ī¸
Now imagine having to add NewFancyFeatureWorker to 10 other places đŸ˜Ģ

Those issues pushed us to create a simple wrapper around ActiveJobs, which we call KittyEvents.

Now in SignUp there is just one trigger for an "event":

module SignUp  
  # ... handle user sign up

  def after_sign_up(user)
    ApplicationEvents.trigger(:user_signup, user)
  end
end  

And there is a central place, where events are mapped to ActiveJobs Workers:

# config/initializers/application_events.rb
module ApplicationEvents  
  extend KittyEvents

  event :user_signup, [
    WelcomeEmailWorker,
    WelcomeTweetWorker,
    SyncProfileImageWorker,
    NewFancyFeatureWorker, # <- new worker
  ]

  # ... other events
end  

When an event is triggered, all ActiveJobs Workers for this events are scheduled and executed.

Another bonus, is when using KittyEvents, you only make a single Redis call to trigger any number of events. This shaves off precious milliseconds when using KittyEvents in request.

Introducing MiniForm

My last blog post - Dealing with form objects, got me thinking about form objects. I also had a 12 hours flight to San Francisco, where I was meeting my teammates at Product Hunt.

During that flight, I went back and checked all of my custom form implementations. I gathered them together in a folder and started combining and extracting the common parts. By the end of the flight I almost had what now is MiniForm. (I had to change its name several times since I'm not very good with names and most of the decent names are not available).

MiniForm allows the following code:

 class RegistrationForm  
  include ActiveModel::Model

  attr_reader :user, :account

  attr_accessor :first_name, :last_name, :email, :name, :plan, :terms_of_service

  # user validation
  validates :first_name, presence: true
  validates :last_name, presence: true
  validates :email, presence: true, email: true

  # account validation
  validates :account_name, presence: true

  # form custom validation
  validates :plan, inclusion: {in AccountPlan::VALUES}
  validates :terms_of_service, acceptance: true

  # ensure uniqueness
  validate :ensure_unique_user_email
  validate :ensure_unique_account_name

  def initialize
    @user    = User.new
    @account = Account.new owner: @user
  end

  def update(attributes)
    attributes.each do |name, value|
      public_send "#{name}=", value
    end

    if valid? & user.update(user_attributes) && account.update(account_attributes)
      account.users << user
      AccountWasCreated.perform_later(account)
    else
      false
    end
  end

  private

  def ensure_unique_user_email
    errors.add :email, 'already taken' if User.where(email: email).any?
  end

  def ensure_unique_account_name
    errors.add :name, 'already taken' if Account.where(name: name).any?
  end

  def user_attributes
    {first_name: first_name, last_name: last_name, email: email}
  end

  def account_attributes
    {plan: plan, name: name}
  end
end  

To become:

class RegistrationForm  
  include MiniForm::Model

  # `model` delegates attributes to models 
  # and copies the validations from them
  model :user, attributes: %i(first_name last_name email), save: true
  model :account, attributes: %i(name plan), save: true

  attributes :terms_of_service

  validates :plan, inclusion: {in AccountPlan::VALUES}
  validates :terms_of_service, acceptance: true

  def initialize
    @user    = User.new
    @account = Account.new owner: @user
  end

  # `update` calls `perform` if model is valid
  # and models are saved
  def perform
    account.users << user
    AccountWasCreated.perform_later(account)
  end
end  

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