How to refactor queries chain in Hanami?

174 Views Asked by At

How to refactor #filtered method?

In Hanami there is no way to make a chain of queries (filters) in ActiveRecord-style. I would like to get a methods like ActiveRecord filters.

Now: documents.filtered(genre: 'news', min_published_at: from, max_published_at: to, skip: 30)

What I want: documents.with_genre('news').published_between(from, to).skip(30)

class DocumentRepository < Hanami::Repository
  GENRES = DbSchema.current_schema.enum(:document_genre).values.map(&:to_s)
  DOCUMENTS_PER_PAGE = 30

  associations do
    has_many :boxes
    has_many :urls
  end

  # rubocop:disable Metrics/CyclomaticComplexity
  # rubocop:disable Metrics/AbcSize
  def filtered(params = {})
    result = ordered.limit(DOCUMENTS_PER_PAGE)

    result = result.where(genre: params[:genre]) if params.key?(:genre)

    if params.key?(:min_created_at) && params.key?(:max_created_at)
      date_range = params[:min_created_at]..params[:max_created_at]
      result = result.where(created_at: date_range)
    end

    if params.key?(:min_published_at) && params.key?(:max_published_at)
      date_range = params[:min_published_at]..params[:max_published_at]
      result = result.where(published_at: date_range)
    end

    result = result.offset(params[:skip]) if params.key?(:skip)

    result
  end
  # rubocop:enable Metrics/CyclomaticComplexity
  # rubocop:enable Metrics/AbcSize

  def ordered
    documents.order { created_at.desc }
  end
end
2

There are 2 best solutions below

0
On

Introduce query object:

class FilterDocuments
  DOCUMENTS_PER_PAGE = 30

  def initialize(documents)
    @documents = documents
  end

  def filter(params = {})
    result = apply_ordering(documents)
    result = apply_limit_and_offset(result, params)
    result = filter_by_genre(result, params)
    result = filter_by_created_at(result, params)
    result = filter_by_published_at(result, params)

    result
  end

private
  attr_reader :documents

  def apply_ordering(documents)
    documents.order { created_at.desc }
  end

  def apply_limit_and_offset(documents, params)
    if params.key?(:skip)
      documents.offset(params[:skip])
    else
      documents
    end.limit(DOCUMENTS_PER_PAGE)
  end

  def filter_by_genre(documents, params)
    if params.key?(:genre)
      documents.where(genre: params[:genre])
    else
      documents
    end
  end

  def filter_by_created_at(documents, params)
    if params.key?(:min_created_at) && params.key?(:max_created_at)
      range = params[:min_created_at]..params[:max_created_at]
      documents.where(created_at: range)
    else
      documents
    end
  end

  def filter_by_published_at(documents, params)
    if params.key?(:min_published_at) && params.key?(:max_published_at)
      range = params[:min_published_at]..params[:max_published_at]
      documents.where(published_at: range)
    else
      documents
    end
  end
end

How to use:

    def query
      FilterDocuments.new(DocumentRepository.new.documents)
    end

    filtered_documents = query.filter(params)
0
On

Something along these lines might work, but not sure how chaining these will poorly effect performance or results, but you can try it and it may lead you to the answer you want

UPDATED

If you really want chaining this is close to what you want.

class DocumentRepository < Hanami::Repository
  GENRES = DbSchema.current_schema.enum(:document_genre).values.map(&:to_s)
  DOCUMENTS_PER_PAGE = 30

  associations do
    has_many :boxes
    has_many :urls
  end

  attr_accessor :data

  def initialize
    @data = []
    super  
  end

  def data 
    @data.flatten!.uniq!
  end

  def with_genre(key)
    @data << documents.where(genre: key) 
    self
  end

  def published_between(arr)
    from, to = arr 
    @data << documents.where(created_at: [from..to])
    self
  end

  def skip(num)
    @data << documents.offset(num)
    self
  end

end

Call it like this assuming this is an instance variable of DocumentRepository

document_repository.with_genre('news')
                   .published_between([from, to])
                   .skip(30)
                   .data

By returning self in each instance method you're able to chain the calls on the instance.

Original answer

This way works but uses similar syntax in your current call.

class DocumentRepository < Hanami::Repository
  GENRES = DbSchema.current_schema.enum(:document_genre).values.map(&:to_s)
  DOCUMENTS_PER_PAGE = 30

  associations do
    has_many :boxes
    has_many :urls
  end

  def hack_where(opts={})
    data = []
    opts.each do |i|
      data << self.send(i[0],i[1]).call
    end
    data.flatten!.uniq!
  end

  def with_genre(key)
    lambda { |key| documents.where(genre: key) }
  end

  def published_between(arr)
    from = arr[0]
    to = arr[1]
    lambda { |from, to| documents.where(created_at: [from..to]) }
  end

  def skip(num)
    lambda { documents.offset(num) }
  end

end

You can call it like:

hack_where({with_genre: 'news', published_between: [from,to], skip: 30})