lib/mongoid/criteria/queryable/selectable.rb
# frozen_string_literal: true
# rubocop:todo all
module Mongoid
class Criteria
module Queryable
# An queryable selectable is selectable, in that it has the ability to select
# document from the database. The selectable module brings all functionality
# to the selectable that has to do with building MongoDB selectors.
module Selectable
extend Macroable
# Constant for a LineString $geometry.
LINE_STRING = "LineString"
# Constant for a Point $geometry.
POINT = "Point"
# Constant for a Polygon $geometry.
POLYGON = "Polygon"
# @attribute [rw] negating If the next expression is negated.
# @attribute [rw] selector The query selector.
attr_accessor :negating, :selector
# Add the $all criterion.
#
# @example Add the criterion.
# selectable.all(field: [ 1, 2 ])
#
# @example Execute an $all in a where query.
# selectable.where(:field.all => [ 1, 2 ])
#
# @param [ Hash... ] *criteria The key value pair(s) for $all matching.
#
# @return [ Selectable ] The cloned selectable.
def all(*criteria)
if criteria.empty?
return clone.tap do |query|
query.reset_strategies!
end
end
criteria.inject(clone) do |query, condition|
if condition.nil?
raise Errors::CriteriaArgumentRequired, :all
end
condition = expand_condition_to_array_values(condition)
if strategy
send(strategy, condition, "$all")
else
condition.inject(query) do |_query, (field, value)|
v = {'$all' => value}
if negating?
v = {'$not' => v}
end
_query.add_field_expression(field.to_s, v)
end
end
end.reset_strategies!
end
alias :all_in :all
key :all, :union, "$all"
# Add the $and criterion.
#
# @example Add the criterion.
# selectable.and({ field: value }, { other: value })
#
# @param [ [ Hash | Criteria | Array<Hash | Criteria> ]... ] *criteria
# Multiple key/value pair matches or Criteria objects that all must
# match to return results.
#
# @return [ Selectable ] The new selectable.
def and(*criteria)
_mongoid_flatten_arrays(criteria).inject(self.clone) do |c, new_s|
if new_s.is_a?(Selectable)
new_s = new_s.selector
end
normalized = _mongoid_expand_keys(new_s)
normalized.each do |k, v|
k = k.to_s
if c.selector[k]
# There is already a condition on k.
# If v is an operator, and all existing conditions are
# also operators, and v isn't present in existing conditions,
# we can add to existing conditions.
# Otherwise use $and.
if v.is_a?(Hash) &&
v.length == 1 &&
(new_k = v.keys.first).start_with?('$') &&
(existing_kv = c.selector[k]).is_a?(Hash) &&
!existing_kv.key?(new_k) &&
existing_kv.keys.all? { |sub_k| sub_k.start_with?('$') }
then
merged_v = c.selector[k].merge(v)
c.selector.store(k, merged_v)
else
c = c.send(:__multi__, [k => v], '$and')
end
else
c.selector.store(k, v)
end
end
c
end
end
alias :all_of :and
# Add the range selection.
#
# @example Match on results within a single range.
# selectable.between(field: 1..2)
#
# @example Match on results between multiple ranges.
# selectable.between(field: 1..2, other: 5..7)
#
# @param [ Hash ] criterion Multiple key/range pairs.
#
# @return [ Selectable ] The cloned selectable.
def between(criterion)
if criterion.nil?
raise Errors::CriteriaArgumentRequired, :between
end
selection(criterion) do |selector, field, value|
selector.store(
field,
{ "$gte" => value.min, "$lte" => value.max }
)
end
end
# Select with an $elemMatch.
#
# @example Add criterion for a single match.
# selectable.elem_match(field: { name: "value" })
#
# @example Add criterion for multiple matches.
# selectable.elem_match(
# field: { name: "value" },
# other: { name: "value"}
# )
#
# @example Execute an $elemMatch in a where query.
# selectable.where(:field.elem_match => { name: "value" })
#
# @param [ Hash ] criterion The field/match pairs.
#
# @return [ Selectable ] The cloned selectable.
def elem_match(criterion)
if criterion.nil?
raise Errors::CriteriaArgumentRequired, :elem_match
end
and_with_operator(criterion, "$elemMatch")
end
key :elem_match, :override, "$elemMatch"
# Add the $exists selection.
#
# @example Add a single selection.
# selectable.exists(field: true)
#
# @example Add multiple selections.
# selectable.exists(field: true, other: false)
#
# @example Execute an $exists in a where query.
# selectable.where(:field.exists => true)
#
# @param [ Hash ] criterion The field/boolean existence checks.
#
# @return [ Selectable ] The cloned selectable.
def exists(criterion)
if criterion.nil?
raise Errors::CriteriaArgumentRequired, :exists
end
typed_override(criterion, "$exists") do |value|
Mongoid::Boolean.evolve(value)
end
end
key :exists, :override, "$exists" do |value|
Mongoid::Boolean.evolve(value)
end
# Add a $geoIntersects or $geoWithin selection. Symbol operators must
# be used as shown in the examples to expand the criteria.
#
# @note The only valid geometry shapes for a $geoIntersects are:
# :intersects_line, :intersects_point, and :intersects_polygon.
#
# @note The only valid options for a $geoWithin query are the geometry
# shape :within_polygon and the operator :within_box.
#
# @note The :within_box operator for the $geoWithin query expects the
# lower left (south west) coordinate pair as the first argument and
# the upper right (north east) as the second argument.
# Important: When latitude and longitude are passed, longitude is
# expected as the first element of the coordinate pair.
# Source: https://www.mongodb.com/docs/manual/reference/operator/query/box/
#
# @example Add a geo intersect criterion for a line.
# query.geo_spatial(:location.intersects_line => [[ 1, 10 ], [ 2, 10 ]])
#
# @example Add a geo intersect criterion for a point.
# query.geo_spatial(:location.intersects_point => [[ 1, 10 ]])
#
# @example Add a geo intersect criterion for a polygon.
# query.geo_spatial(:location.intersects_polygon => [[ 1, 10 ], [ 2, 10 ], [ 1, 10 ]])
#
# @example Add a geo within criterion for a polygon.
# query.geo_spatial(:location.within_polygon => [[ 1, 10 ], [ 2, 10 ], [ 1, 10 ]])
#
# @example Add a geo within criterion for a box.
# query.geo_spatial(:location.within_box => [[ 1, 10 ], [ 2, 10 ])
#
# @param [ Hash ] criterion The criterion.
#
# @return [ Selectable ] The cloned selectable.
def geo_spatial(criterion)
if criterion.nil?
raise Errors::CriteriaArgumentRequired, :geo_spatial
end
__merge__(criterion)
end
key :intersects_line, :override, "$geoIntersects", "$geometry" do |value|
{ "type" => LINE_STRING, "coordinates" => value }
end
key :intersects_point, :override, "$geoIntersects", "$geometry" do |value|
{ "type" => POINT, "coordinates" => value }
end
key :intersects_polygon, :override, "$geoIntersects", "$geometry" do |value|
{ "type" => POLYGON, "coordinates" => value }
end
key :within_polygon, :override, "$geoWithin", "$geometry" do |value|
{ "type" => POLYGON, "coordinates" => value }
end
key :within_box, :override, "$geoWithin", "$box"
# Add the $eq criterion to the selector.
#
# @example Add the $eq criterion.
# selectable.eq(age: 60)
#
# @example Execute an $eq in a where query.
# selectable.where(:field.eq => 10)
#
# @param [ Hash ] criterion The field/value pairs to check.
#
# @return [ Selectable ] The cloned selectable.
def eq(criterion)
if criterion.nil?
raise Errors::CriteriaArgumentRequired, :eq
end
and_with_operator(criterion, "$eq")
end
key :eq, :override, "$eq"
# Add the $gt criterion to the selector.
#
# @example Add the $gt criterion.
# selectable.gt(age: 60)
#
# @example Execute an $gt in a where query.
# selectable.where(:field.gt => 10)
#
# @param [ Hash ] criterion The field/value pairs to check.
#
# @return [ Selectable ] The cloned selectable.
def gt(criterion)
if criterion.nil?
raise Errors::CriteriaArgumentRequired, :gt
end
and_with_operator(criterion, "$gt")
end
key :gt, :override, "$gt"
# Add the $gte criterion to the selector.
#
# @example Add the $gte criterion.
# selectable.gte(age: 60)
#
# @example Execute an $gte in a where query.
# selectable.where(:field.gte => 10)
#
# @param [ Hash ] criterion The field/value pairs to check.
#
# @return [ Selectable ] The cloned selectable.
def gte(criterion)
if criterion.nil?
raise Errors::CriteriaArgumentRequired, :gte
end
and_with_operator(criterion, "$gte")
end
key :gte, :override, "$gte"
# Adds the $in selection to the selectable.
#
# @example Add $in selection on an array.
# selectable.in(age: [ 1, 2, 3 ])
#
# @example Add $in selection on a range.
# selectable.in(age: 18..24)
#
# @example Execute an $in in a where query.
# selectable.where(:field.in => [ 1, 2, 3 ])
#
# @param [ Hash ] condition The field/value criterion pairs.
#
# @return [ Selectable ] The cloned selectable.
def in(condition)
if condition.nil?
raise Errors::CriteriaArgumentRequired, :in
end
condition = expand_condition_to_array_values(condition)
if strategy
send(strategy, condition, "$in")
else
condition.inject(clone) do |query, (field, value)|
v = {'$in' => value}
if negating?
v = {'$not' => v}
end
query.add_field_expression(field.to_s, v)
end.reset_strategies!
end
end
alias :any_in :in
key :in, :intersect, "$in"
# Add the $lt criterion to the selector.
#
# @example Add the $lt criterion.
# selectable.lt(age: 60)
#
# @example Execute an $lt in a where query.
# selectable.where(:field.lt => 10)
#
# @param [ Hash ] criterion The field/value pairs to check.
#
# @return [ Selectable ] The cloned selectable.
def lt(criterion)
if criterion.nil?
raise Errors::CriteriaArgumentRequired, :lt
end
and_with_operator(criterion, "$lt")
end
key :lt, :override, "$lt"
# Add the $lte criterion to the selector.
#
# @example Add the $lte criterion.
# selectable.lte(age: 60)
#
# @example Execute an $lte in a where query.
# selectable.where(:field.lte => 10)
#
# @param [ Hash ] criterion The field/value pairs to check.
#
# @return [ Selectable ] The cloned selectable.
def lte(criterion)
if criterion.nil?
raise Errors::CriteriaArgumentRequired, :lte
end
and_with_operator(criterion, "$lte")
end
key :lte, :override, "$lte"
# Add a $maxDistance selection to the selectable.
#
# @example Add the $maxDistance selection.
# selectable.max_distance(location: 10)
#
# @param [ Hash ] criterion The field/distance pairs.
#
# @return [ Selectable ] The cloned selectable.
def max_distance(criterion)
if criterion.nil?
raise Errors::CriteriaArgumentRequired, :max_distance
end
# $maxDistance must be given together with $near
__add__(criterion, "$maxDistance")
end
# Adds $mod selection to the selectable.
#
# @example Add the $mod selection.
# selectable.mod(field: [ 10, 1 ])
#
# @example Execute an $mod in a where query.
# selectable.where(:field.mod => [ 10, 1 ])
#
# @param [ Hash ] criterion The field/mod selections.
#
# @return [ Selectable ] The cloned selectable.
def mod(criterion)
if criterion.nil?
raise Errors::CriteriaArgumentRequired, :mod
end
and_with_operator(criterion, "$mod")
end
key :mod, :override, "$mod"
# Adds $ne selection to the selectable.
#
# @example Query for a value $ne to something.
# selectable.ne(field: 10)
#
# @example Execute an $ne in a where query.
# selectable.where(:field.ne => "value")
#
# @param [ Hash ] criterion The field/ne selections.
#
# @return [ Selectable ] The cloned selectable.
def ne(criterion)
if criterion.nil?
raise Errors::CriteriaArgumentRequired, :ne
end
and_with_operator(criterion, "$ne")
end
alias :excludes :ne
key :ne, :override, "$ne"
# Adds a $near criterion to a geo selection.
#
# @example Add the $near selection.
# selectable.near(location: [ 23.1, 12.1 ])
#
# @example Execute an $near in a where query.
# selectable.where(:field.near => [ 23.2, 12.1 ])
#
# @param [ Hash ] criterion The field/location pair.
#
# @return [ Selectable ] The cloned selectable.
def near(criterion)
if criterion.nil?
raise Errors::CriteriaArgumentRequired, :near
end
and_with_operator(criterion, "$near")
end
key :near, :override, "$near"
# Adds a $nearSphere criterion to a geo selection.
#
# @example Add the $nearSphere selection.
# selectable.near_sphere(location: [ 23.1, 12.1 ])
#
# @example Execute an $nearSphere in a where query.
# selectable.where(:field.near_sphere => [ 10.11, 3.22 ])
#
# @param [ Hash ] criterion The field/location pair.
#
# @return [ Selectable ] The cloned selectable.
def near_sphere(criterion)
if criterion.nil?
raise Errors::CriteriaArgumentRequired, :near_sphere
end
and_with_operator(criterion, "$nearSphere")
end
key :near_sphere, :override, "$nearSphere"
# Adds the $nin selection to the selectable.
#
# @example Add $nin selection on an array.
# selectable.nin(age: [ 1, 2, 3 ])
#
# @example Add $nin selection on a range.
# selectable.nin(age: 18..24)
#
# @example Execute an $nin in a where query.
# selectable.where(:field.nin => [ 1, 2, 3 ])
#
# @param [ Hash ] condition The field/value criterion pairs.
#
# @return [ Selectable ] The cloned selectable.
def nin(condition)
if condition.nil?
raise Errors::CriteriaArgumentRequired, :nin
end
condition = expand_condition_to_array_values(condition)
if strategy
send(strategy, condition, "$nin")
else
condition.inject(clone) do |query, (field, value)|
v = {'$nin' => value}
if negating?
v = {'$not' => v}
end
query.add_field_expression(field.to_s, v)
end.reset_strategies!
end
end
alias :not_in :nin
key :nin, :intersect, "$nin"
# Adds $nor selection to the selectable.
#
# @example Add the $nor selection.
# selectable.nor(field: 1, field: 2)
#
# @param [ [ Hash | Criteria | Array<Hash | Criteria> ]... ] *criteria
# Multiple key/value pair matches or Criteria objects.
#
# @return [ Selectable ] The new selectable.
def nor(*criteria)
_mongoid_add_top_level_operation('$nor', criteria)
end
# Is the current selectable negating the next selection?
#
# @example Is the selectable negating?
# selectable.negating?
#
# @return [ true | false ] If the selectable is negating.
def negating?
!!negating
end
# Negate the arguments, or the next selection if no arguments are given.
#
# @example Negate the next selection.
# selectable.not.in(field: [ 1, 2 ])
#
# @example Add the $not criterion.
# selectable.not(name: /Bob/)
#
# @example Execute a $not in a where query.
# selectable.where(:field.not => /Bob/)
#
# @param [ [ Hash | Criteria ]... ] *criteria The key/value pair
# matches or Criteria objects to negate.
#
# @return [ Selectable ] The new selectable.
def not(*criteria)
if criteria.empty?
dup.tap { |query| query.negating = true }
else
criteria.compact.inject(self.clone) do |c, new_s|
if new_s.is_a?(Selectable)
new_s = new_s.selector
end
_mongoid_expand_keys(new_s).each do |k, v|
k = k.to_s
if c.selector[k] || k.start_with?('$')
c = c.send(:__multi__, [{'$nor' => [{k => v}]}], '$and')
else
if v.is_a?(Hash)
c = c.send(:__multi__, [{'$nor' => [{k => v}]}], '$and')
else
if v.is_a?(Regexp)
negated_operator = '$not'
else
negated_operator = '$ne'
end
c = c.send(:__override__, {k => v}, negated_operator)
end
end
end
c
end
end
end
key :not, :override, "$not"
# Negate the arguments, constraining the query to only those documents
# that do NOT match the arguments.
#
# @example Exclude a single criterion.
# selectable.none_of(name: /Bob/)
#
# @example Exclude multiple criteria.
# selectable.none_of(name: /Bob/, country: "USA")
#
# @example Exclude multiple criteria as an array.
# selectable.none_of([{ name: /Bob/ }, { country: "USA" }])
#
# @param [ [ Hash | Criteria ]... ] *criteria The key/value pair
# matches or Criteria objects to negate.
#
# @return [ Selectable ] The new selectable.
def none_of(*criteria)
criteria = _mongoid_flatten_arrays(criteria)
return dup if criteria.empty?
exprs = criteria.map do |criterion|
_mongoid_expand_keys(
criterion.is_a?(Selectable) ?
criterion.selector : criterion)
end
self.and('$nor' => exprs)
end
# Creates a disjunction using $or from the existing criteria in the
# receiver and the provided arguments.
#
# This behavior (receiver becoming one of the disjunction operands)
# matches ActiveRecord's +or+ behavior.
#
# Use +any_of+ to add a disjunction of the arguments as an additional
# constraint to the criteria already existing in the receiver.
#
# Each argument can be a Hash, a Criteria object, an array of
# Hash or Criteria objects, or a nested array. Nested arrays will be
# flattened and can be of any depth. Passing arrays is deprecated.
#
# @example Add the $or selection where both fields must have the specified values.
# selectable.or(field: 1, field: 2)
#
# @example Add the $or selection where either value match is sufficient.
# selectable.or({field: 1}, {field: 2})
#
# @example Same as previous example but using the deprecated array wrap.
# selectable.or([{field: 1}, {field: 2}])
#
# @example Same as previous example, also deprecated.
# selectable.or([{field: 1}], [{field: 2}])
#
# @param [ [ Hash | Criteria | Array<Hash | Criteria> ]... ] *criteria
# Multiple key/value pair matches or Criteria objects, or arrays
# thereof. Passing arrays is deprecated.
#
# @return [ Selectable ] The new selectable.
def or(*criteria)
_mongoid_add_top_level_operation('$or', criteria)
end
# Adds a disjunction of the arguments as an additional constraint
# to the criteria already existing in the receiver.
#
# Use +or+ to make the receiver one of the disjunction operands.
#
# Each argument can be a Hash, a Criteria object, an array of
# Hash or Criteria objects, or a nested array. Nested arrays will be
# flattened and can be of any depth. Passing arrays is deprecated.
#
# @example Add the $or selection where both fields must have the specified values.
# selectable.any_of(field: 1, field: 2)
#
# @example Add the $or selection where either value match is sufficient.
# selectable.any_of({field: 1}, {field: 2})
#
# @example Same as previous example but using the deprecated array wrap.
# selectable.any_of([{field: 1}, {field: 2}])
#
# @example Same as previous example, also deprecated.
# selectable.any_of([{field: 1}], [{field: 2}])
#
# @param [ [ Hash | Criteria | Array<Hash | Criteria> ]... ] *criteria
# Multiple key/value pair matches or Criteria objects, or arrays
# thereof. Passing arrays is deprecated.
#
# @return [ Selectable ] The new selectable.
def any_of(*criteria)
criteria = _mongoid_flatten_arrays(criteria)
case criteria.length
when 0
clone
when 1
# When we have a single criteria, any_of behaves like and.
# Note: criteria can be a Query object, which #where method does
# not support.
self.and(*criteria)
else
# When we have multiple criteria, combine them all with $or
# and add the result to self.
exprs = criteria.map do |criterion|
if criterion.is_a?(Selectable)
_mongoid_expand_keys(criterion.selector)
else
Hash[criterion.map do |k, v|
if k.is_a?(Symbol)
[k.to_s, v]
else
[k, v]
end
end]
end
end
self.and('$or' => exprs)
end
end
# Add a $size selection for array fields.
#
# @example Add the $size selection.
# selectable.with_size(field: 5)
#
# @note This method is named #with_size not to conflict with any existing
# #size method on enumerables or symbols.
#
# @example Execute an $size in a where query.
# selectable.where(:field.with_size => 10)
#
# @param [ Hash ] criterion The field/size pairs criterion.
#
# @return [ Selectable ] The cloned selectable.
def with_size(criterion)
if criterion.nil?
raise Errors::CriteriaArgumentRequired, :with_size
end
typed_override(criterion, "$size") do |value|
::Integer.evolve(value)
end
end
key :with_size, :override, "$size" do |value|
::Integer.evolve(value)
end
# Adds a $type selection to the selectable.
#
# @example Add the $type selection.
# selectable.with_type(field: 15)
#
# @example Execute an $type in a where query.
# selectable.where(:field.with_type => 15)
#
# @note http://vurl.me/PGOU contains a list of all types.
#
# @param [ Hash ] criterion The field/type pairs.
#
# @return [ Selectable ] The cloned selectable.
def with_type(criterion)
if criterion.nil?
raise Errors::CriteriaArgumentRequired, :with_type
end
typed_override(criterion, "$type") do |value|
::Integer.evolve(value)
end
end
key :with_type, :override, "$type" do |value|
::Integer.evolve(value)
end
# Construct a text search selector.
#
# @example Construct a text search selector.
# selectable.text_search("testing")
#
# @example Construct a text search selector with options.
# selectable.text_search("testing", :$language => "fr")
#
# @note Per https://www.mongodb.com/docs/manual/reference/operator/query/text/
# it is not currently possible to supply multiple text search
# conditions in a query. Mongoid will build such a query but the
# server will return an error when trying to execute it.
#
# @param [ String | Symbol ] terms A string of terms that MongoDB parses
# and uses to query the text index.
# @param [ Hash ] opts Text search options. See MongoDB documentation
# for options.
#
# @return [ Selectable ] The cloned selectable.
def text_search(terms, opts = nil)
if terms.nil?
raise Errors::CriteriaArgumentRequired, :terms
end
clone.tap do |query|
criterion = {'$text' => { '$search' => terms }}
criterion['$text'].merge!(opts) if opts
if query.selector['$text']
# Per https://www.mongodb.com/docs/manual/reference/operator/query/text/
# multiple $text expressions are not currently supported by
# MongoDB server, but build the query correctly instead of
# overwriting previous text search condition with the currently
# given one.
Mongoid.logger.warn('Multiple $text expressions per query are not currently supported by the server')
query.selector = {'$and' => [query.selector]}.merge(criterion)
else
query.selector = query.selector.merge(criterion)
end
end
end
# This is the general entry point for most MongoDB queries. This either
# creates a standard field: value selection, and expanded selection with
# the use of hash methods, or a $where selection if a string is provided.
#
# @example Add a standard selection.
# selectable.where(name: "syd")
#
# @example Add a javascript selection.
# selectable.where("this.name == 'syd'")
#
# @param [ [ Hash | String ]... ] *criterion The standard selection
# or javascript string.
#
# @return [ Selectable ] The cloned selectable.
def where(*criteria)
criteria.inject(clone) do |query, criterion|
if criterion.nil?
raise Errors::CriteriaArgumentRequired, :where
end
# We need to save the criterion in an instance variable so
# Modifiable methods know how to create a polymorphic object.
# Note that this method in principle accepts multiple criteria,
# but only the first one will be stored in @criterion. This
# works out to be fine because first_or_create etc. methods
# only ever specify one criterion to #where.
@criterion = criterion
if criterion.is_a?(String)
js_query(criterion)
else
expr_query(criterion)
end
end.reset_strategies!
end
private
# Adds the specified expression to the query.
#
# Criterion must be a hash in one of the following forms:
# - {field_name: value}
# - {'field_name' => value}
# - {key_instance: value}
# - {'$operator' => operator_value_expression}
#
# Field name and operator may be given as either strings or symbols.
#
# @example Create the selection.
# selectable.expr_query(age: 50)
#
# @param [ Hash ] criterion The field/value pairs.
#
# @return [ Selectable ] The cloned selectable.
# @api private
def expr_query(criterion)
if criterion.nil?
raise ArgumentError, 'Criterion cannot be nil here'
end
unless Hash === criterion
raise Errors::InvalidQuery, "Expression must be a Hash: #{Errors::InvalidQuery.truncate_expr(criterion)}"
end
normalized = _mongoid_expand_keys(criterion)
clone.tap do |query|
normalized.each do |field, value|
field_s = field.to_s
if field_s.start_with?('$')
# Query expression-level operator, like $and or $where
query.add_operator_expression(field_s, value)
else
query.add_field_expression(field, value)
end
end
query.reset_strategies!
end
end
# Force the values of the criterion to be evolved.
#
# @api private
#
# @example Force values to booleans.
# selectable.force_typing(criterion) do |val|
# Boolean.evolve(val)
# end
#
# @param [ Hash ] criterion The criterion.
def typed_override(criterion, operator)
if criterion
criterion.transform_values! do |value|
yield(value)
end
end
__override__(criterion, operator)
end
# Create a javascript selection.
#
# @api private
#
# @example Create the javascript selection.
# selectable.js_query("this.age == 50")
#
# @param [ String ] criterion The javascript as a string.
#
# @return [ Selectable ] The cloned selectable
def js_query(criterion)
clone.tap do |query|
if negating?
query.add_operator_expression('$and',
[{'$nor' => [{'$where' => criterion}]}])
else
query.add_operator_expression('$where', criterion)
end
query.reset_strategies!
end
end
# Take the provided criterion and store it as a selection in the query
# selector.
#
# @example Store the selection.
# selectable.selection({ field: "value" })
#
# @param [ Hash ] criterion The selection to store.
#
# @return [ Selectable ] The cloned selectable.
# @api private
def selection(criterion = nil)
clone.tap do |query|
if criterion
criterion.each_pair do |field, value|
yield(query.selector, field.is_a?(Key) ? field : field.to_s, value)
end
end
query.reset_strategies!
end
end
class << self
# Get the methods on the selectable that can be forwarded to from a model.
#
# @example Get the forwardable methods.
# Selectable.forwardables
#
# @return [ Array<Symbol> ] The names of the forwardable methods.
def forwardables
public_instance_methods(false) -
[ :negating, :negating=, :negating?, :selector, :selector= ]
end
end
end
end
end
end