{"id":17159258,"url":"https://github.com/akuzko/zen-query","last_synced_at":"2025-04-13T13:31:02.375Z","repository":{"id":56887447,"uuid":"58372703","full_name":"akuzko/zen-query","owner":"akuzko","description":"parascope gem for param-based scope generation","archived":false,"fork":false,"pushed_at":"2021-05-11T07:14:42.000Z","size":73,"stargazers_count":25,"open_issues_count":0,"forks_count":1,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-03-25T21:36:23.467Z","etag":null,"topics":["activerecord","parameters","ruby","scope"],"latest_commit_sha":null,"homepage":null,"language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/akuzko.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG","contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2016-05-09T11:51:34.000Z","updated_at":"2021-10-18T16:33:20.000Z","dependencies_parsed_at":"2022-08-20T15:20:51.249Z","dependency_job_id":null,"html_url":"https://github.com/akuzko/zen-query","commit_stats":null,"previous_names":["akuzko/parascope"],"tags_count":11,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/akuzko%2Fzen-query","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/akuzko%2Fzen-query/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/akuzko%2Fzen-query/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/akuzko%2Fzen-query/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/akuzko","download_url":"https://codeload.github.com/akuzko/zen-query/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248441543,"owners_count":21104014,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["activerecord","parameters","ruby","scope"],"created_at":"2024-10-14T22:13:48.338Z","updated_at":"2025-04-13T13:31:02.029Z","avatar_url":"https://github.com/akuzko.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Zen::Query\n\nParam-based scope (relation, dataset) generation.\n\n[![build status](https://secure.travis-ci.org/akuzko/zen-query.png)](http://travis-ci.org/akuzko/zen-query)\n[![github release](https://img.shields.io/github/release/akuzko/zen-query.svg)](https://github.com/akuzko/zen-query/releases)\n\n---\n\nThis gem provides a `Zen::Query` class with a declarative and convenient API\nto build scopes (ActiveRecord relations or arbitrary objects) dynamically, based\non parameters passed to query object on initialization.\n\n## Installation\n\nAdd this line to your application's Gemfile:\n\n```ruby\ngem 'zen-query'\n```\n\nAnd then execute:\n\n    $ bundle\n\nOr install it yourself as:\n\n    $ gem install zen-query\n\n## Usage\n\nDespite the fact `zen-query` was intended to help building ActiveRecord relations\nvia scopes or query methods, it's usage is not limited to ActiveRecord cases and\nmay be used with any arbitrary classes and objects. In fact, for development and\ntesting, `OpenStruct` instance is used as a generic subject. However, ActiveRecord\nexamples should illustrate gem's usage in the best way.\n\nFor most examples in this README, `scope` method is used as accessor to\ncurrent subject value. This behavior is easily achieved via `Query.alias_subject_name(:scope)`\nmethod call.\n\n### API\n\n`zen-query` provides `Zen::Query` class, descendants of which should declare\nscope manipulations using `query_by`, `sift_by` and other class methods bellow.\n\n#### Class Methods\n\n- `query_by(*presence_fields, **value_fields, \u0026block)` declares a scope-generation query\n  block that will be executed if, and only if all values of query params at the keys of\n  `presence_fields` are present in activesupport's definition of presence and all `value_fields`\n  are present in query params as is. The block is executed in context of query\n  object. All values of specified params are yielded to the block. If the block\n  returns a non-nil value, it becomes a new scope for subsequent processing. Of course,\n  there can be multiple `query_by` block definitions. Methods accepts additional options:\n  - `:index` - allows to specify order of query block applications. By default all query\n    blocks have index of 0. This option also accepts special values `:first` and `:last` for\n    more convenient usage. Queries with the same value of `:index` option are applied in\n    order of declaration.\n  - `:if` - specifies condition according to which query should be applied. If Symbol\n    or String is passed, calls corresponding method. If Proc is passed, it is executed\n    in context of query object. Note that this is optional condition, and does not\n    overwrite original param-based condition for a query block that should always be met.\n  - `:unless` - the same as `:if` option, but with reversed boolean check.\n\n- `query_by!(*fields, \u0026block)` declares scope-generation block that is always executed\n  (unless `:if` and/or `:unless` options are used). All values in params at `fields` keys are\n  yielded to the block. As `query_by`, accepts `:index`, `:if` and `:unless` options.\n\n- `query(\u0026block)` declares scope-generation block that is always executed (unless `:if`\n  and/or `:unless` options are used). As `query_by`, accepts `:index`, `:if` and `:unless`\n  options.\n\n*Examples:*\n\n```ruby\n# executes block only when params[:department_id] is non-empty:\nquery_by(:department_id) { |id| scope.where(department_id: id) }\n\n# executes block only when params[:only_active] == 'true':\nquery_by(only_active: 'true') { scope.active }\n\n# executes block only when *both* params[:first_name] and params[:last_name]\n# are present:\nquery_by(:first_name, :last_name) do |first_name, last_name|\n  scope.where(first_name: first_name, last_name: last_name)\nend\n\n# if query block returns nil, scope will remain intact:\nquery { scope.active if only_active? }\n\n# conditional example:\nquery(if: :include_inactive?) { scope.with_inactive }\n\ndef include_inactive?\n  company.settings.include_inactive?\nend\n```\n\n- `sift_by(*presence_fields, **value_fields, \u0026block)` method is used to hoist sets of\n  query definitions that should be applied if, and only if, all specified values\n  match criteria in the same way as in `query_by` method. Just like `query_by` method,\n  values of specified fields are yielded to the block. Accepts the same options as\n  it's `query_by` counterpart. Such `sift_by` definitions may be nested in any depth.\n\n- `sift_by!(*fields, \u0026block)` declares a sifter block that is always applied (unless\n  `:if` and/or `:unless` options are used). All values in params at specified `fields`\n  are yielded to the block.\n\n- `sifter` alias for `sift_by`. Results in a more readable construct when a single\n  presence field is passed. For example, `sifter(:paginated)`.\n\n*Examples:*\n\n```ruby\nsift_by(:search_value, :search_type) do |value|\n  # definitions in this block will be applied only if *both* params[:search_value]\n  # and params[:search_type] are present\n\n  search_value = \"%#{value}%\"\n\n  query_by(search_type: 'name') { scope.name_like(value) }\n  query_by(search_type: 'email') { scope.where(\"users.email LIKE ?\", search_value) }\nend\n\nsifter :paginated do\n  query_by(:page, :per_page) do |page, per|\n    scope.page(page).per(per)\n  end\nend\n\ndef paginated_records\n  resolve(:paginated)\nend\n```\n\n- `subject(\u0026block)` method is used to define a base subject as a starting point\n  of subject-generating process. Note that `subject` will not be evaluated if\n  query is initialized with a given subject.\n\n*Examples:*\n\n```ruby\nsubject { User.all }\n```\n\n- `defaults(\u0026block)` method is used to declare default query params that are\n  reverse merged with params passed on query initialization. When used in `sift_by`\n  block, hashes are merged altogether. Accepts a `block`, it's return value\n  will be evaluated and merged on query object instantiation, allowing to have\n  dynamic default params values.\n\n*Examples:*\n\n```ruby\ndefaults { { later_than: 1.week.ago } }\n\nsifter :paginated do\n  # sifter defaults are merged with higher-level defaults:\n  defaults { { page: 1, per_page: 25 } }\nend\n```\n\n- `guard(message = nil, \u0026block)` defines a guard instance method block (see instance methods\n  bellow). All such blocks are executed before query object resolves scope via\n  `resolve_scope` method. Optional `message` may be supplied to provide more informative\n  error message.\n\n*Examples:*\n\n```ruby\nsift_by(:sort_col, :sort_dir) do |scol, sdir|\n  # will raise Zen::Query::GuardViolationError on scope resolution if\n  # params[:sort_dir] is not 'asc' or 'desc'\n  guard(':sort_dir should be \"asc\" or \"desc\"') do\n    sdir.downcase.in?(%w(asc desc))\n  end\n\n  query { scope.order(scol =\u003e sdir) }\nend\n```\n\n- `raise_on_guard_violation(value)` allows to specify whether or not exception should be raised\n  whenever any guard block is violated during scope resolution. When set to `false`, in case\n  of any violation, `resolve` will return `nil`, and query will have `violation` property\n  set with value corresponding to the message of violated block. Default option value is `true`.\n\n*Examples:*\n\n```ruby\nraise_on_guard_violation false\n\nsift_by(:sort_col, :sort_dir) do |scol, sdir|\n  guard(':sort_dir should be \"asc\" or \"desc\"') do\n    sdir.downcase.in?(%w(asc desc))\n  end\n\n  query { scope.order(scol =\u003e sdir) }\nend\n```\n\n```ruby\nquery = UsersQuery.new(sort_col: 'id', sort_dir: 'there')\nquery.resolve # =\u003e nil\nquery.violation # =\u003e \":sort_dir should be \\\"asc\\\" or \\\"desc\\\"\"\n```\n\n- `attributes(*attribute_names)` allows to specify additional attributes that can be passed\n  to query object on initialization. For each given attribute name, reader method is generated.\n\n#### Instance Methods\n\n- `initialize(params: {}, subject: nil, **attributes)` initializes a query with\n  `params`, an optional subject and attributes. If subject is aliased, corresponding\n  key should be used instead. The rest of attributes are only accepted if they were\n  declared via `attributes` class method call.\n\n*Examples:*\n\n```ruby\nquery = UsersQuery.new(params: query_params, company: company)\n```\n\n- `params` returns a parameters passed in initialization, reverse merged with query\n  defaults.\n\n- `subject` \"current\" subject of query object. For an initialized query object corresponds\n  to base subject. Primary usage is to call this method in `query_by` blocks and return\n  it's mutated version corresponding to passed `query_by` arguments.\n\n  Can be aliased to more suitable name with `Query.alias_subject_name` class method.\n\n- `guard(\u0026block)` executes a passed `block`. If this execution returns falsy value,\n  `GuardViolationError` is raised. You can use this method to ensure safety of param\n  values interpolation to a SQL string in a `query_by` block for example.\n\n*Examples:*\n\n```ruby\nquery_by(:sort_col, :sort_dir) do |scol, sdir|\n  # will raise Zen::Query::GuardViolationError on scope resolution if\n  # params[:sort_dir] is not 'asc' or 'desc'\n  guard { sdir.downcase.in?(%w(asc desc)) }\n\n  scope.order(scol =\u003e sdir)\nend\n```\n\n- `resolve(*presence_keys, override_params = {})` returns a resulting scope\n  generated by all queries and sifted queries that fit to query params applied to\n  base scope. Optionally, additional params may be passed to override the ones passed on\n  initialization. For convinience, you may pass list of keys that should be resolved\n  to `true` with params (for example, `resolve(:with_projects)` instead of\n  `resolve(with_projects: true)`). It's the main `Query` instance method that\n  returns the sole purpose of it's instances.\n\n*Examples:*\n\n```ruby\ndefaults { { only_active: true } }\n\nsubject { company.users }\n\nquery_by(:only_active) { subject.active }\n\nsifter :with_departments do\n  query { subject.joins(:departments) }\n\n  query_by(:department_name) do |name|\n    subject.where(departments: { name: name })\n  end\nend\n\ndef users\n  @users ||= resolve\nend\n\n# you can use options to overwrite defaults:\ndef all_users\n  resolve(only_active: false)\nend\n\n# or to apply a sifter with additional params:\ndef managers\n  resolve(:with_departments, department_name: 'managers')\nend\n```\n\n### Composite usage example with ActiveRecord Relation as a subject, aliased as `:relation`\n\n```ruby\nclass UserQuery \u003c Zen::Query\n  alias_subject_name :relation\n\n  attributes :company\n\n  defaults { { only_active: true } }\n\n  relation { company.users }\n\n  query_by(:only_active) { relation.active }\n\n  query_by(:birthdate) { |date| relation.by_birtdate(date) }\n\n  query_by :name do |name|\n    relation.where(\"CONCAT(first_name, ' ', last_name) LIKE :name\", name: \"%#{name}%\")\n  end\n\n  sift_by :sort_column, :sort_direction do |scol, sdir|\n    guard { sdir.to_s.downcase.in?(%w(asc desc)) }\n\n    query { relation.order(scol =\u003e sdir) }\n\n    query_by(sort_column: 'name') do\n      relation.reorder(\"CONCAT(first_name, ' ', last_name) #{sdir}\")\n    end\n  end\n\n  sifter :with_projects do\n    query { relation.joins(:projects) }\n\n    query_by :project_name do |name|\n      scope.where(projects: { name: name })\n    end\n  end\n\n  def users\n    @users ||= resolve\n  end\n\n  def project_users\n    @project_users ||= resolve(:with_projects)\n  end\nend\n\nparams = { name: 'John', sort_column: 'name', sort_direction: 'DESC', project_name: 'ExampleApp' }\n\nquery = UserQuery.new(params: params, company: some_company)\n\nquery.project_users # =\u003e this is the same as:\n# some_company.users\n#   .active\n#   .joins(:projects)\n#   .where(\"CONCAT(first_name, ' ', last_name) LIKE ?\", \"%John%\")\n#   .where(projects: { name: 'ExampleApp' })\n#   .order(\"CONCAT(first_name, ' ', last_name) DESC\")\n```\n\n### Hints and Tips\n\n- Keep in mind that query classes are just plain Ruby classes. All `sifter`,\n`query_by` and `guard` declarations are inherited, as well as default params\ndeclared by `defaults` method. Thus, you can define a BaseQuery with common\ndefinitions as a base class for queries in your application. Or you can define\nquery API blocks in some module's `included` callback to share common definitions\nvia module inclusion.\n\n- Being plain Ruby classes also means you can easily extend default functionality\nfor your needs. For example, if you're querying ActiveRecord relations, and your\nprimary use case looks like\n\n```ruby\nquery_by(:some_field_id) { |id| scope.where(some_field_id: id) }\n```\nyou can do the following to make things more DRY:\n\n```ruby\nclass ApplicationQuery \u003c Zen::Query\n  def self.query_by(*fields, \u0026block)\n    block ||= default_query_block(fields)\n    super(*fields, \u0026block)\n  end\n\n  def self.default_query_block(fields)\n    -\u003e(*values){ scope.where(Hash[fields.zip(values)]) }\n  end\n  private_class_method :default_query_block\nend\n```\n\nand then you can simply call\n\n```ruby\nclass UsersQuery \u003c ApplicationQuery\n  base_scope { company.users }\n\n  query_by :first_name\n  query_by :last_name\n  query_by :city, :street_address\nend\n```\n\nOr you can go a little further and declare a class method\n\n```ruby\nclass ApplicationQuery\n  def self.query_by_fields(*fields)\n    fields.each do |field|\n      query_by field\n    end\n  end\nend\n```\n\nand then\n\n```ruby\nclass UserQuery \u003c ApplicationQuery\n  query_by_fields :first_name, :last_name, :department_id\nend\n```\n\n## Development\n\nAfter checking out the repo, run `bin/setup` to install dependencies. Then, run\n`rake spec` to run the tests. You can also run `bin/console` for an interactive\nprompt that will allow you to experiment.\n\n## Contributing\n\nBug reports and pull requests are welcome on GitHub at https://github.com/akuzko/zen-query.\n\n\n## License\n\nThe gem is available as open source under the terms of the\n[MIT License](http://opensource.org/licenses/MIT).\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fakuzko%2Fzen-query","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fakuzko%2Fzen-query","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fakuzko%2Fzen-query/lists"}