Trailblazer Finder
Provides DSL for creating Trailblazer based Finder Objects. But it is designed to be used on its own as a separate gem. It was influenced by popular Ransack gem, but in addition to ActiveRecord, it can be used with DataMapper or Sequel. It also integrates with Kaminari or Will Paginate, as well as FriendlyId
Table of Contents
- Installation
- Dependencies
- Usage
- Finder
- Finder Example
- Operation
- Operation Example
- Usable without Trailblazer
- Example without Trailblazer
- Example Project
- Features
- Predicates
- Predicates Example
- Paging
- Paging Example
- Sorting
- Sorting Example
- Predicates
- Adapters
- Adapters Example
- ActiveRecord
- Active Record Example
- Sequel
- Sequel Example
- Tips & Tricks
- ORM's are not required
- Passing Entity as Argument
- Finder
- Contributing
- License
Installation
Add this line to your application's Gemfile:
gem 'trailblazer-finder'
And then execute:
$ bundle
Or install it yourself as:
$ gem install trailblazer-finder
Dependencies
Usage
Finder
Just inherit from the Trailblazer::Finder
class.
Features and adapters are optional. However.
NOTE: features are only applied if their options are specified in your finder
An Entity (model) is required, but can:
- be defined in the inherited Finder class
- be given as an option in the call
Be sure to specify features you wish to use in your finder, before specifying adapters.
Basically for most use cases, Entity is the entity/model/array of hashes you wish to use finder on
Finder Example
class Post::Finder < Trailblazer::Finder
# Without defining an ORM everything defaults to dealing with hash objects
adapter :ActiveRecord
# Optional if you use it as option in the caller, Model/Entity or Array with Hashes
entity { Post }
# Pagination settings (don't use it if you don't wish to use paging)
paging per_page: 5, min_per_page: 1, max_per_page: 100
# Property
# Set the properties with their respective options
#
# Params:
# * +name+:: The property name (attribute /column / field - name) (required)
# * +type:+:: Dry Types type, for future validations (required)
#
# * Following is optional (don't use it if you do not wish to use sorting)
# * +sortable:+:: Can this property be sorted on? (default: false)
# * +sort_direction:+:: Specify default sort direction (only usable if sortable is true)
# (default: :desc)
property :id, type: Types::Integer
property :body, type: Types::String, sortable: true
property :title, type: Types::String, sortable: true, sort_direction: :asc
# Filter By
# A simple way to make custom filter methods
#
# Params:
# * +parameter+:: The parameter to use to call this filter (required)
#
# * Following is optional, matches exact value if not specified
# * +with:+:: Filter method defined in Finder class
# * block:: A block can be given with the code to filter with
filter_by :created_after, with: :apply_created_after
filter_by(:created_before) { |entity, _attribute, value| entity.where('DATE(created_at) <= ?', value) if value.present? }
private
def apply_created_after(entity, _attribute, value)
entity.where('DATE(created_at) >= ?', value) if value.present?
end
end
Operation
The only tie this has to Trailblazer 2.1, is this part to be honest, which doesn't get loaded in case Trailblazer isn't loaded in your environment. The idea was actually to create it specifically for use with Trailblazer 2.1, hence the name Trailblazer-Finder.
Operation Example using Finder Macro
class Post::Index < Trailblazer::Operation
# Runs a Trailblazer Task with a Finder object
# Params:
# +finder_class+:: Finder class to be used
# +action+:: :all, :single (optional, defaults to :all)
# +entity+:: Entity/Model or array (optional if specified in Finder Class, overwrites Finder class entity)
step Finder(Post::Finder, :all, Post)
end
Operation Example using custom step
class Post::Index < Trailblazer::Operation
step :finder!
# Find all matching results and extend model object with required methods
def finder!(options, params:, **)
options[:finder] = Post::Finder.new(params: params)
end
# Find first matching row, no method extension on model object
def single_finder!(options, params:, **)
# Since ID's are usually given directly, patch it into filter
apply_id(params)
# No paging, sorting, no methods and only returns the first matched result
options[:finder] = Post::Finder.new(params: params).result.first
end
# Since ID's are usually given directly, patch it into filter
def apply_id(params)
return if params[:id].nil?
params[:id_eq] = params[:id] unless params.key?("id")
end
end
When using this, result[:finder] will be extended with (not available for :single row)
# accessing results
.count # => number of found results
.result? # => are there any results found
.result # => fetched results
# params for url generations
.params # => filter values
Usable without Trailblazer
It's not really tied to Trailblazer, you can actually use it anywhere by calling the below example directly. If Trailblazer isn't loaded, the ties to Trailblazer won't be executed.
Example without Trailblazer
# If entity is specified in your finder
result = Post::Finder.new(params: params)
# If entity ism't specified in your finder
result = Post::Finder.new(entity: Post, params: params)
When using this, result will be extended with (not available for :single row)
# accessing results
.count # => number of found results
.result? # => are there any results found
.result # => fetched results
# params for url generations
.params # => filter values
Example Project
Coming soon!
Features
Aside of the default filtering behavior, it offers the following optional features as well.
NOTE: FEATURES NEED TO BE SPECIFIED ON TOP OF YOUR CLASS
Predicates
Simple predicate feature, that enables you to have default predicate filters available for the specified fields.
At the moment we support:
- eq: equals to
- not_eq: not equals to
- blank: blank (empty/nil/nul)
- not_blank: not blank (empty/nil/null)
- lt: less than (value converts to float)
- lte: less than or equal to (value converts to float)
- gt: greater than (value converts to float)
- gte: greater than or equal to (value converts to float)
- cont: contains specified value
- not_cont: does not contain specified value
- sw: starts with specified value
- not_sw: does not start with specified value
- ew: end with specified value
- not_ew: does not end with specified value
Predicates Example
class Post::Finder < Trailblazer::Finder
entity { Post }
property :id, type: Types::Integer
property :body, type: Types::String
property :title, type: Types::String
end
This feature extends the result[:finder] object with the following methods
# Available Predicate filters with the above example
id_eq # => id equals value
id_not_eq # => id not equals value
id_blank # => id blank value
id_not_blank # => id not blank value
id_lt # => id less than value (converts value to float)
id_lte # => id less than or equal to value (converts value to float)
id_gt # => id greater than value (converts value to float)
id_gte # => id greater than or equal to value (converts value to float)
id_cont # => id contains value
id_not_cont # => id does not contain value
id_sw # => id starts with
id_not_sw # => id does not start with
id_ew # => id ends with
id_not_ew # => id does not end with
body_eq # => body equals value
body_not_eq # => body not equals value
body_blank # => body blank value
body_not_blank # => body not blank value
body_lt # => body less than value (converts value to float)
body_lte # => body less than or equal to value (converts value to float)
body_gt # => body greater than value (converts value to float)
body_gte # => body greater than or equal to value (converts value to float)
body_cont # => body contains value
body_not_cont # => body does not contain value
body_sw # => body starts with
body_not_sw # => body does not start with
body_ew # => body ends with
body_not_ew # => body does not end with
title_eq # => title equals value
title_not_eq # => title not equals value
title_blank # => title blank value
title_not_blank # => title not blank value
title_lt # => title less than value (converts value to float)
title_lte # => title less than or equal to value (converts value to float)
title_gt # => title greater than value (converts value to float)
title_gte # => title greater than or equal to value (converts value to float)
title_cont # => title contains value
title_not_cont # => title does not contain value
title_sw # => title starts with
title_not_sw # => title does not start with
title_ew # => title ends with
title_not_ew # => title does not end with
Paging
Really simple pagination feature, which uses the plain .limit
and .offset
methods.
Paging Example
class Post::Finder < Trailblazer::Finder
entity { Post }
paging per_page: 5, min_per_page: 1, max_per_page: 100
end
Sorting
Really simple sorting feature, fixing the pain of dealing with sorting attributes and directions. Can sort by multiple columns/directions.
Sorting Example
class Post::Finder < Trailblazer::Finder
entity { Post }
# Property
# Set the properties with their respective options
#
# Params:
# * +name+:: The property name (attribute /column / field - name) (required)
# * +type:+:: Dry Types type, for future validations (required)
#
# * Following is optional (don't use it if you do not wish to use sorting)
# * +sortable:+:: Can this property be sorted on? (default: false)
# * +sort_direction:+:: Specify default sort direction (only usable if sortable is true)
# (default: :desc)
property :id, type: Types::Integer
property :body, type: Types::String, sortable: true
property :title, type: Types::String, sortable: true, sort_direction: :asc
end
This feature extends the result[:finder] object with the following methods
.result # => Posts sorted by title DESC
# Smart sort checking
.sort?('title') # => true
# Helpers for seeing current sort direction
.sort_direction_for('title') # => 'asc'
.sort_direction_for('body') # => 'desc'
# Helpers for seeing reversing sort direction
.reverse_sort_direction_for('title') # => 'desc'
.reverse_sort_direction_for('body') # => 'asc'
# Params for sorting links (new if none exists, existing params if exists)
.sort_params_for('title')
# Add Params for sorting links (add to existing / replace with different direction if exists)
.add_sort_params_for('title')
# Remove Params for sorting links (remove from existing)
.remove_sort_params_for('title')
# New Params for sorting links (reset)
.new_sort_params_for('title')
Adapters
By default, everything works with an array of hashes. You can change the default behaviour by using adapters.
Currently supported ORM's:
You can specify the adapters you wish to use inside your enherited Finder class.
Adapters Example
class Post::Finder < Trailblazer::Finder
adapter "ActiveRecord"
end
ActiveRecord
The only thing the ActiveRecord adapter does, is overwrite one specific method for Paging (limit/offset) and Sorting (order) each, as well as change the default filter behaviour (from select to where). These are overwritten by the adapter to match the specific use case of ActiveRecord.
Active Record Example
class Post::Finder < Trailblazer::Finder
adapter "ActiveRecord"
end
Sequel
The only thing the Sequel adapter does, is overwrite one specific method for Paging (limit/offset) and Sorting (order) each, as well as change the default filter behaviour (from select to where). These are overwritten by the adapter to match the specific use case of Sequel.
Sequel Example
class Post::Finder < Trailblazer::Finder
adapter "Sequel"
end
Tips & Tricks
ORM's are not required
Not even for the Paging, Predicates and Sorting features.
class Post::Finder < Trailblazer::Finder
entity { fetch_product_as_hashes }
filter_by(:name) { |entity, _attribute, value| entity.select { |product| product[:name] == value } }
filter_by(:category) { |entity, _attribute, value| entity.select { |product| product[:category] == value } }
end
Overwriting Methods
You can have fine grained entity, by overwriting initialize
method:
class Post::Finder < Trailblazer::Finder
property :name, type: Types::String
def initialize(options = {})
super options.merge(entity: Product.visible_to(user))
end
end
You can have fine grained result set, by overwriting fetch_result
method:
class Post::Finder < Trailblazer::Finder
property :name, type: Types::String
def fetch_result
super
result.merge! test: "I added this"
end
end
Contributing
- Fork it
- Create your feature branch
- Commit your changes
- Push to the branch
- Run the tests (
rake
) - Make sure all tests pass, rubocop has no offenses and coverage is 100%
- Create new Pull Request
Bugs
Please report them on the Github issue tracker for this project.
If you have a bug to report, please include the following information:
- Version information for Trailblazer-Finder, Trailblazer, used Adapters and Ruby.
- Full stack trace and error message (if you have them).
- Any snippets of relevant model, view or controller code that shows how you are using Trailblazer-Finder.
If you are able to, it helps even more if you can fork Trailblazer-Finder on Github, and add a test that reproduces the error you are experiencing.
For more info on how to report bugs, please see this article.