[![Build Status](https://travis-ci.org/sitrox/inquery.svg?branch=master)](https://travis-ci.org/sitrox/inquery) [![Gem Version](https://badge.fury.io/rb/inquery.svg)](https://badge.fury.io/rb/inquery) # Inquery A skeleton that allows extracting queries into atomic, reusable classes. ## Installation To install the **Inquery** gem: ```sh $ gem install inquery ``` To install it using `bundler` (recommended for any application), add it to your `Gemfile`: ```ruby gem 'inquery' ``` ## Compatibility Inquery is tested with the following ruby versions: * 2.5.1 * 2.6.2 * 2.7.1 * 3.0.0 Other ruby versions might work but are not covered by our Travis tests. ## Basic usage ```ruby class FetchUsersWithACar < Inquery::Query schema do req :color, :symbol end def call User.joins(:cars).where(cars: { color: osparams.color }) end end FetchUsersWithACar.run # => [ 0, it will automatically select the given field. This option defaults to `:id`. Use `nil` to disable this behavior. ### Using query classes as regular scopes Chainable queries can also be used as regular AR model scopes: ```ruby class User < ActiveRecord::Base scope :active, Queries::User::FetchActive end class Queries::User::FetchActive < Inquery::Query::Chainable # Note that specifying either `class` or `default` is mandatory when using # this query class as a scope. The reason for this is that, if the scope is # otherwise empty, the class will receive `nil` from AR and therefore has no # way of knowing which default class to take. relation class: 'User' def call relation.where(active: 1) end end ``` This approach allows to you use short and descriptive code like `User.active` but have the possibly complex query code hidden in a separate, reusable class. Note that when using classes as scopes, the `process` method will be ignored. ### Using the given relation as subquery In simple cases and all the examples above, we just extend the given relation and return it again. It is also possible however to just use the given relation as a subquery and return a completely new relation: ```ruby class FetchUsersInGroup < Inquery::Query::Chainable # Here we do not specify any specific class, as we don't care for it as long # as the relation returns exactly one field. relation fields: 1 def call return ::User.where(%( id IN ( SELECT user_id FROM GROUPS_USERS WHERE group_id IN ( #{relation.to_sql} ) ) )) end end ``` This query could then be called in the following ways: ```ruby FetchUsersInGroup.run( GroupsUser.where(user_id: 1).select(:group_id) ) # In this example, we're not specifying any select for the relation we pass to # the query class. This is fine because the query automatically defaults to # selecting `id` if exactly one field is required (`fields: 1`) and no select is # specifyed. You can control this further with the option `default_select`. FetchUsersInGroup.run(Group.where(color: 'red')) ``` ## Parameters Both query classes can be parameterized using a hash called `params`. It is recommended to specify and validate input parameters in every query. For this purpose, Inquery provides the `schema` method witch integrates the [Schemacop](https://github.com/sitrox/schemacop) validation Gem: ```ruby class SomeQueryClass < Inquery::Query schema do req :some_param, :integer opt :some_other_param, :hash do req :some_field, :string end end # ... end ``` The schema is validated at query class instantiation. An exception will be raised if the given params do not match the schema specified. See documentation of the Schemacop Gem for more information on how to specify schemas. Parameters can be accessed using either `params` or `osparams`. The method `osparams` automatically wraps `params` in an `OpenStruct` for more convenient access. ```ruby class SomeQueryClass < Inquery::Query def run User.where( active: params[:active], username: osparams.search ) end end ``` Inquery supports both schemacop specification versions 2 and 3 using the methods `schema` / `schema2` for version 2 and method `schema3` for version 3. ## Rails integration While it is optional, Inquery has been written from the ground up to be perfectly integrated into any Rails application. It has proven to be a winning concept to extract all complex queries into separate classes that are independently executable and testable. ### Directory structure While not enforced, it is encouraged to use the following structure for storing your query classes: * All domain-specific query classes reside in `app/queries`. * They're in the module `Queries`. * Queries are further grouped by the model they return (and not the model they receive). For instance, a class fetching all active users could be located at `Queries::User::FetchActive` and would reside under `app/queries/user/fetch_active.rb`. There are some key benefits to this approach: * As it should, domain-specific code is located within `app/`. * As queries are grouped by the model they return and consistently named, they're easy to locate and it does not take much thought where to put and how to name new query classes. * As there is a single file per query class, it's a breeze to list all queries, i.e. to check their naming for consistency. * If you're using the same layout for your unit tests, it is absolutely clear where to find the corresponding unit tests for each one of your query classes. ## Contributors Thanks to Jeroen Weeink for his insights regarding using query classes as scopes in his [blog post](http://craftingruby.com/posts/2015/06/29/query-objects-through-scopes.html). ## Copyright Copyright © 2016 - 2021 Sitrox. See `LICENSE` for further details.