index_query_builder 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +50 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +61 -0
- data/README.md +37 -0
- data/Rakefile +53 -0
- data/config/database.yml +3 -0
- data/index_query_builder.gemspec +30 -0
- data/lib/index_query_builder.rb +103 -0
- data/lib/index_query_builder/query_builder.rb +40 -0
- data/lib/index_query_builder/query_definition.rb +130 -0
- data/lib/index_query_builder/version.rb +3 -0
- data/spec/index_query_builder_spec.rb +224 -0
- data/spec/integration_spec_helper.rb +26 -0
- data/spec/spec_helper.rb +32 -0
- metadata +164 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 8af5c5d595d2eadb2940a4f1aaea38103970502e
|
4
|
+
data.tar.gz: a7a3cec53023cfab0ca2fb6b3fd0f37d1c81a910
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: ca1644edcd841d1acd9b393538959bab3c5df58eababc66db5d8780ec32c4edf60c1b25cd4ae2f89d5e364af75a568b09b8c5c026c46d2deef7b1f06d8a27506
|
7
|
+
data.tar.gz: 559fa403af8cf4f64845f49d0c8f858bd4ab10746a5297272344f0bbcf8eb4e95dcb5990d8cae03baba79eeee11f9c928a572d40deb4f7096e3f391188a39f9a
|
data/.gitignore
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
/.config
|
4
|
+
/coverage/
|
5
|
+
/InstalledFiles
|
6
|
+
/pkg/
|
7
|
+
/spec/reports/
|
8
|
+
/spec/examples.txt
|
9
|
+
/test/tmp/
|
10
|
+
/test/version_tmp/
|
11
|
+
/tmp/
|
12
|
+
|
13
|
+
# Used by dotenv library to load environment variables.
|
14
|
+
# .env
|
15
|
+
|
16
|
+
## Specific to RubyMotion:
|
17
|
+
.dat*
|
18
|
+
.repl_history
|
19
|
+
build/
|
20
|
+
*.bridgesupport
|
21
|
+
build-iPhoneOS/
|
22
|
+
build-iPhoneSimulator/
|
23
|
+
|
24
|
+
## Specific to RubyMotion (use of CocoaPods):
|
25
|
+
#
|
26
|
+
# We recommend against adding the Pods directory to your .gitignore. However
|
27
|
+
# you should judge for yourself, the pros and cons are mentioned at:
|
28
|
+
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
|
29
|
+
#
|
30
|
+
# vendor/Pods/
|
31
|
+
|
32
|
+
## Documentation cache and generated files:
|
33
|
+
/.yardoc/
|
34
|
+
/_yardoc/
|
35
|
+
/doc/
|
36
|
+
/rdoc/
|
37
|
+
|
38
|
+
## Environment normalization:
|
39
|
+
/.bundle/
|
40
|
+
/vendor/bundle
|
41
|
+
/lib/bundler/man/
|
42
|
+
|
43
|
+
# for a library or gem, you might want to ignore these files since the code is
|
44
|
+
# intended to run in multiple environments; otherwise, check them in:
|
45
|
+
# Gemfile.lock
|
46
|
+
# .ruby-version
|
47
|
+
# .ruby-gemset
|
48
|
+
|
49
|
+
# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
|
50
|
+
.rvmrc
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
index_query_builder (0.0.1)
|
5
|
+
|
6
|
+
GEM
|
7
|
+
remote: https://rubygems.org/
|
8
|
+
specs:
|
9
|
+
activemodel (4.0.13)
|
10
|
+
activesupport (= 4.0.13)
|
11
|
+
builder (~> 3.1.0)
|
12
|
+
activerecord (4.0.13)
|
13
|
+
activemodel (= 4.0.13)
|
14
|
+
activerecord-deprecated_finders (~> 1.0.2)
|
15
|
+
activesupport (= 4.0.13)
|
16
|
+
arel (~> 4.0.0)
|
17
|
+
activerecord-deprecated_finders (1.0.4)
|
18
|
+
activesupport (4.0.13)
|
19
|
+
i18n (~> 0.6, >= 0.6.9)
|
20
|
+
minitest (~> 4.2)
|
21
|
+
multi_json (~> 1.3)
|
22
|
+
thread_safe (~> 0.1)
|
23
|
+
tzinfo (~> 0.3.37)
|
24
|
+
arel (4.0.2)
|
25
|
+
builder (3.1.4)
|
26
|
+
diff-lcs (1.2.5)
|
27
|
+
i18n (0.7.0)
|
28
|
+
minitest (4.7.5)
|
29
|
+
multi_json (1.12.1)
|
30
|
+
pg (0.18.1)
|
31
|
+
rake (10.4.2)
|
32
|
+
rspec (3.3.0)
|
33
|
+
rspec-core (~> 3.3.0)
|
34
|
+
rspec-expectations (~> 3.3.0)
|
35
|
+
rspec-mocks (~> 3.3.0)
|
36
|
+
rspec-core (3.3.1)
|
37
|
+
rspec-support (~> 3.3.0)
|
38
|
+
rspec-expectations (3.3.0)
|
39
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
40
|
+
rspec-support (~> 3.3.0)
|
41
|
+
rspec-mocks (3.3.1)
|
42
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
43
|
+
rspec-support (~> 3.3.0)
|
44
|
+
rspec-support (3.3.0)
|
45
|
+
thread_safe (0.3.5)
|
46
|
+
tzinfo (0.3.50)
|
47
|
+
|
48
|
+
PLATFORMS
|
49
|
+
ruby
|
50
|
+
|
51
|
+
DEPENDENCIES
|
52
|
+
activerecord (~> 4.0.0)
|
53
|
+
activesupport (~> 4.0.0)
|
54
|
+
bundler (~> 1.7)
|
55
|
+
index_query_builder!
|
56
|
+
pg (~> 0.18.1)
|
57
|
+
rake (~> 10.0)
|
58
|
+
rspec (~> 3.3.0)
|
59
|
+
|
60
|
+
BUNDLED WITH
|
61
|
+
1.11.2
|
data/README.md
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
# IndexQueryBuilder
|
2
|
+
|
3
|
+
TODO: Write a gem description
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
gem 'index_query_builder'
|
11
|
+
```
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
|
15
|
+
$ bundle
|
16
|
+
|
17
|
+
Or install it yourself as:
|
18
|
+
|
19
|
+
$ gem install index_query_builder
|
20
|
+
|
21
|
+
## Usage
|
22
|
+
|
23
|
+
TODO: Write usage instructions here
|
24
|
+
|
25
|
+
## Running tests
|
26
|
+
|
27
|
+
$ cd bounded_contexts/index_query_builder
|
28
|
+
$ rake db:test:setup
|
29
|
+
$ rspec
|
30
|
+
|
31
|
+
## Contributing
|
32
|
+
|
33
|
+
1. Fork it ( https://github.com/[my-github-username]/index_query_builder/fork )
|
34
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
35
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
36
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
37
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
require 'active_record'
|
3
|
+
require 'pg'
|
4
|
+
|
5
|
+
require 'index_query_builder'
|
6
|
+
|
7
|
+
namespace 'db:test' do
|
8
|
+
|
9
|
+
desc 'Drop and create new test db and load schema'
|
10
|
+
task :setup => [:drop, :create, :load_schema]
|
11
|
+
|
12
|
+
task :drop do
|
13
|
+
pg_connection = create_pg_connection
|
14
|
+
pg_connection.query("DROP DATABASE IF EXISTS #{db_config[:database]}")
|
15
|
+
pg_connection.close
|
16
|
+
end
|
17
|
+
|
18
|
+
task :create do
|
19
|
+
pg_connection = create_pg_connection
|
20
|
+
pg_connection.query("CREATE DATABASE #{db_config[:database]}")
|
21
|
+
pg_connection.close
|
22
|
+
end
|
23
|
+
|
24
|
+
task :load_schema do
|
25
|
+
ActiveRecord::Base.establish_connection(db_config)
|
26
|
+
ActiveRecord::Schema.define do
|
27
|
+
create_table :posts, force: true do |t|
|
28
|
+
t.text :title
|
29
|
+
t.integer :view_count
|
30
|
+
end
|
31
|
+
|
32
|
+
create_table :comments, force: true do |t|
|
33
|
+
t.integer :post_id
|
34
|
+
t.text :text
|
35
|
+
t.integer :likes
|
36
|
+
end
|
37
|
+
|
38
|
+
create_table :authors, force: true do |t|
|
39
|
+
t.integer :comment_id
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def create_pg_connection
|
45
|
+
PG::Connection.open(
|
46
|
+
host: db_config[:host], user: db_config[:username], password: db_config[:password], dbname: 'postgres'
|
47
|
+
)
|
48
|
+
end
|
49
|
+
|
50
|
+
def db_config
|
51
|
+
@db_config ||= IndexQueryBuilder.symbolize_keys(YAML.load_file('config/database.yml'))
|
52
|
+
end
|
53
|
+
end
|
data/config/database.yml
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "index_query_builder/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "index_query_builder"
|
8
|
+
spec.version = IndexQueryBuilder::VERSION
|
9
|
+
spec.authors = ["Arturo Pie"]
|
10
|
+
spec.email = ["arturop@nulogy.com"]
|
11
|
+
spec.summary = %q{DSL for getting data for index pages.}
|
12
|
+
spec.description = %q{This gem provides a DSL on top of ActiveRecord to get collection of models for index pages with filters.}
|
13
|
+
spec.homepage = "https://github.com/arturopie/index_query_builder"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.7"
|
22
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
23
|
+
|
24
|
+
spec.add_development_dependency "rspec", "~> 3.3", ">= 3.3.0"
|
25
|
+
|
26
|
+
spec.add_development_dependency "activerecord", "~> 4.0", ">= 4.0.0"
|
27
|
+
spec.add_development_dependency "activesupport", "~> 4.0", ">= 4.0.0"
|
28
|
+
|
29
|
+
spec.add_development_dependency "pg", "~> 0.18.1"
|
30
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
require "active_support/core_ext/string"
|
2
|
+
|
3
|
+
require "index_query_builder/query_definition"
|
4
|
+
require "index_query_builder/query_builder"
|
5
|
+
require "index_query_builder/version"
|
6
|
+
|
7
|
+
# Simple DSL for building queries using filters.
|
8
|
+
#
|
9
|
+
# This module makes it easy to fetch records from the database, specially
|
10
|
+
# for showing and filtering records in an index page
|
11
|
+
#
|
12
|
+
module IndexQueryBuilder
|
13
|
+
|
14
|
+
# Builds a query by calling arel methods on base_scope
|
15
|
+
#
|
16
|
+
# @param base_scope [Arel] used to build query on top of this scope
|
17
|
+
# @param options [Hash]
|
18
|
+
# @option :with [Hash] filters used to build query. Key is filter name, value is value for the filter
|
19
|
+
# @param &block yield to build query using IndexQueryBuilder's DSL
|
20
|
+
# @return [Arel] returns arel object to make it easy to extend query (e.g. add pagination, etc)
|
21
|
+
#
|
22
|
+
# ==== Example
|
23
|
+
#
|
24
|
+
# receive_orders = IndexQueryBuilder.query ReceiveOrder.where(:site_id => site.id), with: { sku_code: 'ABC' } do |query|
|
25
|
+
# query.filter_field :received
|
26
|
+
# query.filter_field :reference, contains: :reference
|
27
|
+
# query.filter_field :expected_delivery_at,
|
28
|
+
# greater_than_or_equal_to: :from_expected_delivery_at, less_than: :to_expected_delivery_at
|
29
|
+
# query.filter_field [:receive_order_items, :sku, :code], equal_to: :sku_code
|
30
|
+
#
|
31
|
+
# query.order_by "expected_delivery_at DESC, receive_orders.id DESC"
|
32
|
+
# end
|
33
|
+
#
|
34
|
+
def self.query(base_scope, options={}, &block)
|
35
|
+
query_definition = QueryDefinition.new
|
36
|
+
block.call(query_definition)
|
37
|
+
|
38
|
+
QueryBuilder.apply(base_scope, query_definition, filters(options))
|
39
|
+
end
|
40
|
+
|
41
|
+
# Builds a query by calling arel methods on base_scope, but it returns children of base scope.
|
42
|
+
# Use this method when using same filters as when querying the parent, but want to get back all children instead.
|
43
|
+
# This way, you can reuse same query definition you used for IndexQueryBuilder.query.
|
44
|
+
#
|
45
|
+
# @param child_association [Symbol] children's association name
|
46
|
+
# @param base_scope [Arel] used to build query on top of this scope
|
47
|
+
# @param options [Hash]
|
48
|
+
# @option options [Hash] :with filters used to build query. Key is filter name, value is value for the filter
|
49
|
+
# @param &block yield to build query using IndexQueryBuilder's DSL
|
50
|
+
# @return [Arel] returns arel object to make it easy to extend query (e.g. add pagination, etc)
|
51
|
+
#
|
52
|
+
# ==== Example
|
53
|
+
#
|
54
|
+
# receive_order_items = IndexQueryBuilder.query_children :receive_order_items, ReceiveOrder.scoped_by(site), with: filters do |query|
|
55
|
+
# query.filter_field :received
|
56
|
+
# query.filter_field :reference, contains: :reference
|
57
|
+
# query.filter_field :expected_delivery_at,
|
58
|
+
# greater_than_or_equal_to: :from_expected_delivery_at, less_than: :to_expected_delivery_at
|
59
|
+
# query.filter_field [:receive_order_items, :sku, :code], equal_to: :sku_code
|
60
|
+
#
|
61
|
+
# query.order_by "expected_delivery_at DESC, receive_orders.id DESC"
|
62
|
+
# end
|
63
|
+
#
|
64
|
+
def self.query_children(child_association, base_scope, options={}, &block)
|
65
|
+
parents = query(base_scope.eager_load(child_association), options, &block)
|
66
|
+
|
67
|
+
children_of(parents, child_association, base_scope)
|
68
|
+
end
|
69
|
+
|
70
|
+
# Exception raised when using Unknown Operator in query definition.
|
71
|
+
#
|
72
|
+
# ==== Example
|
73
|
+
#
|
74
|
+
# IndexQueryBuilder.query Post.where(nil) do |query|
|
75
|
+
# query.filter_field :view_count, unkown_operator: :view_count
|
76
|
+
# end
|
77
|
+
#
|
78
|
+
# will raise this exception.
|
79
|
+
#
|
80
|
+
class UnknownOperator < ArgumentError; end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
def self.filters(params)
|
85
|
+
symbolize_keys(params.fetch(:with) { {} })
|
86
|
+
end
|
87
|
+
|
88
|
+
def self.symbolize_keys(params)
|
89
|
+
Hash[params.map { |key, value| [key.to_sym, value] }]
|
90
|
+
end
|
91
|
+
|
92
|
+
def self.children_of(parents, child_association, base_scope)
|
93
|
+
initialize_parent_association(child_association, base_scope, parents).flat_map(&child_association)
|
94
|
+
end
|
95
|
+
|
96
|
+
def self.initialize_parent_association(child_association, base_scope, parents)
|
97
|
+
parents.each do |parent|
|
98
|
+
parent.public_send(child_association).each do |child|
|
99
|
+
child.public_send("#{base_scope.name.underscore}=", parent)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module IndexQueryBuilder
|
2
|
+
class QueryBuilder
|
3
|
+
def self.apply(base_scope, query_definition, filters)
|
4
|
+
new(query_definition, filters).apply(base_scope)
|
5
|
+
end
|
6
|
+
|
7
|
+
attr_reader :query_definition, :filters
|
8
|
+
|
9
|
+
def initialize(query_definition, filters)
|
10
|
+
@query_definition = query_definition
|
11
|
+
@filters = filters
|
12
|
+
end
|
13
|
+
|
14
|
+
def apply(base_scope)
|
15
|
+
apply_filters(apply_order_by(base_scope))
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def apply_order_by(arel)
|
21
|
+
query_definition.arel_ordering.reduce(arel) do |arel, ordering|
|
22
|
+
ordering.call(arel)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def apply_filters(arel)
|
27
|
+
filters.reduce(arel) do |arel, (filter_name, filter_value)|
|
28
|
+
apply_predicates_for_filter(arel, filter_name, filter_value)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def apply_predicates_for_filter(arel, filter_name, filter_value)
|
33
|
+
arel_predicates(filter_name).reduce(arel) { |arel, predicate| predicate.call(arel, filter_value) }
|
34
|
+
end
|
35
|
+
|
36
|
+
def arel_predicates(filter_name)
|
37
|
+
query_definition.arel_filters.fetch(filter_name) { [] }
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
module IndexQueryBuilder
|
2
|
+
|
3
|
+
# Provides a DSL to build a query definition
|
4
|
+
class QueryDefinition
|
5
|
+
attr_reader :arel_filters, :arel_ordering
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@arel_filters = {}
|
9
|
+
@arel_ordering = []
|
10
|
+
end
|
11
|
+
|
12
|
+
# Specifies how to filter a field
|
13
|
+
#
|
14
|
+
# @param field_name [Symbol | [Symbol]] database field name (if one element) or
|
15
|
+
# table names to join plus database field name (if an array of more than 1 element)
|
16
|
+
# @param predicates [Hash] of the form operator => filter_name. filter_name should match the key in filters hash
|
17
|
+
# that is passed to IndexQueryBuilder.query methods
|
18
|
+
#
|
19
|
+
# ==== Operators
|
20
|
+
#
|
21
|
+
# Operators will apply where clauses to query *only if* the filter_name is present in filters hash.
|
22
|
+
#
|
23
|
+
# [:equal_to]
|
24
|
+
# Applies field_name = filter_value
|
25
|
+
# [:contains]
|
26
|
+
# Applies substring (ILIKE '%filter_value%')
|
27
|
+
# [:greater_than_or_equal_to]
|
28
|
+
# Applies field_name >= filter_value
|
29
|
+
# [:less_than]
|
30
|
+
# Applies field_name < filter_value
|
31
|
+
# [:present_if]
|
32
|
+
# Applies
|
33
|
+
# field_name IS NOT NULL if filter_value
|
34
|
+
# field_name IS NULL if filter_value
|
35
|
+
#
|
36
|
+
# === Examples
|
37
|
+
#
|
38
|
+
# <tt>query.filter_field :received</tt>
|
39
|
+
# <tt>query.filter_field :reference, contains: :reference</tt>
|
40
|
+
# <tt>query.filter_field [:vendor, :name], equal_to: :vendor_name</tt>
|
41
|
+
# <tt>query.filter_field [:receive_order_items, :sku, :code], equal_to: :sku_code</tt>
|
42
|
+
# <tt>query.filter_field :expected_delivery_at, greater_than_or_equal_to: :from_expected_delivery_at, less_than: :to_expected_delivery_at</tt>
|
43
|
+
# <tt>query.filter_field :expected_delivery_at, less_than_or_equal_to: :to_expected_delivery_at</tt>
|
44
|
+
# <tt>query.filter_field :outbound_trailer_id, present_if: :has_trailer</tt>
|
45
|
+
#
|
46
|
+
def filter_field(field_name, predicates={equal_to: field_name})
|
47
|
+
predicates.each do |operator, filter|
|
48
|
+
@arel_filters[filter] = []
|
49
|
+
|
50
|
+
if operator == :contains
|
51
|
+
@arel_filters[filter] << ->(arel, value) do
|
52
|
+
table_name, field = apply_joins(arel, field_name, filter)
|
53
|
+
arel.where("#{table_name}.#{field} ILIKE ?", "%#{value}%")
|
54
|
+
end
|
55
|
+
elsif operator == :equal_to
|
56
|
+
@arel_filters[filter] << ->(arel, value) do
|
57
|
+
table_name, field = apply_joins(arel, field_name, filter)
|
58
|
+
arel.where(table_name => {field => value})
|
59
|
+
end
|
60
|
+
elsif operator == :greater_than_or_equal_to
|
61
|
+
@arel_filters[filter] << ->(arel, value) do
|
62
|
+
table_name, field = apply_joins(arel, field_name, filter)
|
63
|
+
arel.where("#{table_name}.#{field} >= ?", value)
|
64
|
+
end
|
65
|
+
elsif operator == :less_than
|
66
|
+
@arel_filters[filter] << ->(arel, value) do
|
67
|
+
table_name, field = apply_joins(arel, field_name, filter)
|
68
|
+
arel.where("#{table_name}.#{field} < ?", value)
|
69
|
+
end
|
70
|
+
elsif operator == :less_than_or_equal_to
|
71
|
+
@arel_filters[filter] << ->(arel, value) do
|
72
|
+
table_name, field = apply_joins(arel, field_name, filter)
|
73
|
+
arel.where("#{table_name}.#{field} <= ?", value)
|
74
|
+
end
|
75
|
+
elsif operator == :present_if
|
76
|
+
@arel_filters[filter] << ->(arel, value) do
|
77
|
+
table_name, field = apply_joins(arel, field_name, filter)
|
78
|
+
if value
|
79
|
+
arel.where("#{table_name}.#{field} IS NOT NULL")
|
80
|
+
else
|
81
|
+
arel.where("#{table_name}.#{field} IS NULL")
|
82
|
+
end
|
83
|
+
end
|
84
|
+
else
|
85
|
+
raise UnknownOperator.new("Unknown operator #{operator}.")
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# Specifies how to order the result.
|
91
|
+
# Uses same syntax as Arel#order (http://guides.rubyonrails.org/active_record_querying.html#ordering)
|
92
|
+
#
|
93
|
+
# === Examples
|
94
|
+
#
|
95
|
+
# <tt>query.order_by "expected_delivery_at DESC, receive_orders.id DESC"</tt>
|
96
|
+
#
|
97
|
+
def order_by(*args)
|
98
|
+
@arel_ordering << ->(arel) do
|
99
|
+
arel.order(*args)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def filter_names
|
104
|
+
arel_filters.keys
|
105
|
+
end
|
106
|
+
|
107
|
+
private
|
108
|
+
|
109
|
+
def apply_joins(arel, qualified_field_name, filter)
|
110
|
+
# IQB_TODO: refactor this
|
111
|
+
if qualified_field_name.is_a?(Array)
|
112
|
+
association_hash, field, table_name = transform(qualified_field_name)
|
113
|
+
@arel_filters[filter] << ->(arel, _) { arel.joins(association_hash) }
|
114
|
+
else
|
115
|
+
table_name = arel.table_name
|
116
|
+
field = qualified_field_name
|
117
|
+
end
|
118
|
+
|
119
|
+
return table_name.to_s.pluralize, field
|
120
|
+
end
|
121
|
+
|
122
|
+
def transform(qualified_field_name)
|
123
|
+
field = qualified_field_name[-1]
|
124
|
+
association_path = qualified_field_name[0..-2]
|
125
|
+
table_name = association_path[-1]
|
126
|
+
association_hash = association_path[0..-2].reverse.inject(association_path[-1]) { |memo, e| {e => memo} }
|
127
|
+
return association_hash, field, table_name
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
@@ -0,0 +1,224 @@
|
|
1
|
+
require 'rspec'
|
2
|
+
require 'integration_spec_helper'
|
3
|
+
|
4
|
+
RSpec.describe IndexQueryBuilder do
|
5
|
+
describe 'operator :contains' do
|
6
|
+
it 'returns records containing filter value' do
|
7
|
+
Post.create!(title: "Using Rubymine because it's awesome")
|
8
|
+
Post.create!(title: "VIM is useless")
|
9
|
+
|
10
|
+
posts = IndexQueryBuilder.query Post.where(nil), with: { title: 'Rubymine' } do |query|
|
11
|
+
query.filter_field :title, contains: :title
|
12
|
+
end
|
13
|
+
|
14
|
+
expect(posts.length).to eq(1)
|
15
|
+
expect(posts[0].title).to eq("Using Rubymine because it's awesome")
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'filters using association' do
|
19
|
+
Post.create!(comments: [Comment.create!(text: "This post is amazing.")])
|
20
|
+
|
21
|
+
posts = IndexQueryBuilder.query Post.where(nil), with: { comment_text: 'amazing' } do |query|
|
22
|
+
query.filter_field [:comments, :text], contains: :comment_text
|
23
|
+
end
|
24
|
+
|
25
|
+
expect(posts.length).to eq(1)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe 'operator :equal_to' do
|
30
|
+
it 'returns records equal to filter value' do
|
31
|
+
Post.create!(title: "Using Rubymine because it's awesome")
|
32
|
+
Post.create!(title: "VIM is useless")
|
33
|
+
|
34
|
+
posts = IndexQueryBuilder.query Post.where(nil), with: { title: "Using Rubymine because it's awesome" } do |query|
|
35
|
+
query.filter_field :title, equal_to: :title
|
36
|
+
end
|
37
|
+
|
38
|
+
expect(posts.length).to eq(1)
|
39
|
+
expect(posts[0].title).to eq("Using Rubymine because it's awesome")
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'filters using association' do
|
43
|
+
Post.create!(comments: [Comment.create!(text: "This post is amazing.")])
|
44
|
+
|
45
|
+
posts = IndexQueryBuilder.query Post.where(nil), with: { comment_text: "This post is amazing." } do |query|
|
46
|
+
query.filter_field [:comments, :text], equal_to: :comment_text
|
47
|
+
end
|
48
|
+
|
49
|
+
expect(posts.length).to eq(1)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
describe 'operator :greater_than_or_equal_to' do
|
54
|
+
it 'returns records greater than or equal to filter value' do
|
55
|
+
Post.create!(view_count: 10)
|
56
|
+
Post.create!(view_count: 1)
|
57
|
+
Post.create!(view_count: 5)
|
58
|
+
|
59
|
+
result = IndexQueryBuilder.query Post.where(nil), with: { view_count: 5 } do |query|
|
60
|
+
query.filter_field :view_count, greater_than_or_equal_to: :view_count
|
61
|
+
end
|
62
|
+
|
63
|
+
posts = result.sort_by(&:view_count)
|
64
|
+
expect(posts.length).to eq(2)
|
65
|
+
expect(posts[0].view_count).to eq(5)
|
66
|
+
expect(posts[1].view_count).to eq(10)
|
67
|
+
end
|
68
|
+
|
69
|
+
it 'filters using association' do
|
70
|
+
Post.create!(comments: [Comment.create!(likes: 5)])
|
71
|
+
posts = IndexQueryBuilder.query Post.where(nil), with: { likes: 3 } do |query|
|
72
|
+
query.filter_field [:comments, :likes], greater_than_or_equal_to: :likes
|
73
|
+
end
|
74
|
+
|
75
|
+
expect(posts.length).to eq(1)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
describe 'operator :less_than' do
|
80
|
+
it 'returns records less than filter value' do
|
81
|
+
Post.create!(view_count: 10)
|
82
|
+
Post.create!(view_count: 1)
|
83
|
+
Post.create!(view_count: 5)
|
84
|
+
|
85
|
+
posts = IndexQueryBuilder.query Post.where(nil), with: { view_count: 5 } do |query|
|
86
|
+
query.filter_field :view_count, less_than: :view_count
|
87
|
+
end
|
88
|
+
|
89
|
+
expect(posts.length).to eq(1)
|
90
|
+
expect(posts[0].view_count).to eq(1)
|
91
|
+
end
|
92
|
+
|
93
|
+
it 'filters using association' do
|
94
|
+
Post.create!(comments: [Comment.create!(likes: 5)])
|
95
|
+
posts = IndexQueryBuilder.query Post.where(nil), with: { likes: 6 } do |query|
|
96
|
+
query.filter_field [:comments, :likes], less_than: :likes
|
97
|
+
end
|
98
|
+
|
99
|
+
expect(posts.length).to eq(1)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
describe 'operator :less_than_or_equal_to' do
|
104
|
+
it 'returns records less than or equal to the filter value' do
|
105
|
+
Post.create!(view_count: 10)
|
106
|
+
Post.create!(view_count: 1)
|
107
|
+
Post.create!(view_count: 5)
|
108
|
+
|
109
|
+
posts = IndexQueryBuilder.query Post.where(nil), with: { view_count: 5 } do |query|
|
110
|
+
query.filter_field :view_count, less_than_or_equal_to: :view_count
|
111
|
+
end
|
112
|
+
|
113
|
+
expect(posts.length).to eq(2)
|
114
|
+
expect(posts[0].view_count).to eq(1)
|
115
|
+
expect(posts[1].view_count).to eq(5)
|
116
|
+
end
|
117
|
+
|
118
|
+
it 'filters using association' do
|
119
|
+
Post.create!(comments: [Comment.create!(likes: 5)])
|
120
|
+
posts = IndexQueryBuilder.query Post.where(nil), with: { likes: 5 } do |query|
|
121
|
+
query.filter_field [:comments, :likes], less_than_or_equal_to: :likes
|
122
|
+
end
|
123
|
+
|
124
|
+
expect(posts.length).to eq(1)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
describe 'operator :present_if' do
|
129
|
+
it 'returns records where field is not null' do
|
130
|
+
hit = Post.create!(view_count: 0)
|
131
|
+
Post.create!(view_count: nil)
|
132
|
+
|
133
|
+
posts = IndexQueryBuilder.query Post.where(nil), with: { has_view_count: true } do |query|
|
134
|
+
query.filter_field :view_count, present_if: :has_view_count
|
135
|
+
end
|
136
|
+
|
137
|
+
expect(posts.length).to eq(1)
|
138
|
+
expect(posts[0]).to eq(hit)
|
139
|
+
end
|
140
|
+
|
141
|
+
it 'returns records where field is null' do
|
142
|
+
hit = Post.create!(view_count: nil)
|
143
|
+
Post.create!(view_count: 0)
|
144
|
+
|
145
|
+
posts = IndexQueryBuilder.query Post.where(nil), with: { has_view_count: false } do |query|
|
146
|
+
query.filter_field :view_count, present_if: :has_view_count
|
147
|
+
end
|
148
|
+
|
149
|
+
expect(posts.length).to eq(1)
|
150
|
+
expect(posts[0]).to eq(hit)
|
151
|
+
end
|
152
|
+
|
153
|
+
it 'filters using association' do
|
154
|
+
Post.create!(comments: [Comment.create!(likes: nil)])
|
155
|
+
posts = IndexQueryBuilder.query Post.where(nil), with: { has_comment_likes: false } do |query|
|
156
|
+
query.filter_field [:comments, :likes], present_if: :has_comment_likes
|
157
|
+
end
|
158
|
+
|
159
|
+
expect(posts.length).to eq(1)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
it "raises UnknownOperator when passed operator is not supported" do
|
164
|
+
expect do
|
165
|
+
IndexQueryBuilder.query Post.where(nil) do |query|
|
166
|
+
query.filter_field :view_count, unkown_operator: :view_count
|
167
|
+
end
|
168
|
+
end.to raise_error(IndexQueryBuilder::UnknownOperator, /unkown_operator/)
|
169
|
+
end
|
170
|
+
|
171
|
+
it "does not error when passing unknown filter name" do
|
172
|
+
IndexQueryBuilder.query(Post.where(nil), with: {unknown_filter: "aaa"}) { |_| }
|
173
|
+
end
|
174
|
+
|
175
|
+
it "order_by works" do
|
176
|
+
Post.create(view_count: 5)
|
177
|
+
Post.create(view_count: 1)
|
178
|
+
Post.create(view_count: 10)
|
179
|
+
|
180
|
+
posts = IndexQueryBuilder.query Post.where(nil) do |query|
|
181
|
+
query.order_by "view_count DESC"
|
182
|
+
end
|
183
|
+
|
184
|
+
expect(posts.length).to eq(3)
|
185
|
+
expect(posts[0].view_count).to eq(10)
|
186
|
+
expect(posts[1].view_count).to eq(5)
|
187
|
+
expect(posts[2].view_count).to eq(1)
|
188
|
+
end
|
189
|
+
|
190
|
+
it "returns arel object" do
|
191
|
+
Post.create!(view_count: 10)
|
192
|
+
|
193
|
+
result = IndexQueryBuilder.query(Post.where(nil)) { |_| }
|
194
|
+
|
195
|
+
posts = result.where(view_count: 10)
|
196
|
+
expect(posts.length).to eq(1)
|
197
|
+
expect(posts[0].view_count).to eq(10)
|
198
|
+
end
|
199
|
+
|
200
|
+
it "works with filter names as string" do
|
201
|
+
Post.create!(view_count: 1)
|
202
|
+
|
203
|
+
posts = IndexQueryBuilder.query Post.where(nil), with: { "view_count" => 5 } do |query|
|
204
|
+
query.filter_field :view_count
|
205
|
+
end
|
206
|
+
|
207
|
+
expect(posts.length).to eq(0)
|
208
|
+
end
|
209
|
+
|
210
|
+
describe ".query_children" do
|
211
|
+
# IQB_TODO: write tests for number of queries
|
212
|
+
it "works" do
|
213
|
+
Post.create!(comments: [Comment.create!(likes: 5)])
|
214
|
+
Post.create!(comments: [Comment.create!(likes: 10)])
|
215
|
+
|
216
|
+
comments = IndexQueryBuilder.query_children(:comments, Post.where(nil), with: { likes: 10 }) do |query|
|
217
|
+
query.filter_field [:comments, :likes], equal_to: :likes
|
218
|
+
end
|
219
|
+
|
220
|
+
expect(comments.length).to eq(1)
|
221
|
+
expect(comments[0].likes).to eq(10)
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'index_query_builder'
|
3
|
+
require 'active_record'
|
4
|
+
|
5
|
+
class Post < ActiveRecord::Base
|
6
|
+
has_many :comments
|
7
|
+
end
|
8
|
+
|
9
|
+
class Comment < ActiveRecord::Base
|
10
|
+
belongs_to :post
|
11
|
+
has_one :author
|
12
|
+
end
|
13
|
+
|
14
|
+
class Author < ActiveRecord::Base
|
15
|
+
belongs_to :post
|
16
|
+
end
|
17
|
+
|
18
|
+
ActiveRecord::Base.establish_connection(YAML.load_file('config/database.yml'))
|
19
|
+
|
20
|
+
RSpec.configure do |config|
|
21
|
+
config.before(:each) do
|
22
|
+
Post.delete_all
|
23
|
+
Comment.delete_all
|
24
|
+
Author.delete_all
|
25
|
+
end
|
26
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
|
2
|
+
RSpec.configure do |config|
|
3
|
+
config.disable_monkey_patching!
|
4
|
+
# rspec-expectations config goes here. You can use an alternate
|
5
|
+
# assertion/expectation library such as wrong or the stdlib/minitest
|
6
|
+
# assertions if you prefer.
|
7
|
+
config.expect_with :rspec do |expectations|
|
8
|
+
# This option will default to `true` in RSpec 4. It makes the `description`
|
9
|
+
# and `failure_message` of custom matchers include text for helper methods
|
10
|
+
# defined using `chain`, e.g.:
|
11
|
+
# be_bigger_than(2).and_smaller_than(4).description
|
12
|
+
# # => "be bigger than 2 and smaller than 4"
|
13
|
+
# ...rather than:
|
14
|
+
# # => "be bigger than 2"
|
15
|
+
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
|
16
|
+
end
|
17
|
+
|
18
|
+
# rspec-mocks config goes here. You can use an alternate test double
|
19
|
+
# library (such as bogus or mocha) by changing the `mock_with` option here.
|
20
|
+
config.mock_with :rspec do |mocks|
|
21
|
+
# Prevents you from mocking or stubbing a method that does not exist on
|
22
|
+
# a real object. This is generally recommended, and will default to
|
23
|
+
# `true` in RSpec 4.
|
24
|
+
mocks.verify_partial_doubles = true
|
25
|
+
end
|
26
|
+
|
27
|
+
# Run specs in random order to surface order dependencies. If you find an
|
28
|
+
# order dependency and want to debug it, you can fix the order by providing
|
29
|
+
# the seed, which is printed after each run.
|
30
|
+
# --seed 1234
|
31
|
+
config.order = :random
|
32
|
+
end
|
metadata
ADDED
@@ -0,0 +1,164 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: index_query_builder
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Arturo Pie
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-12-09 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.7'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.7'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '3.3'
|
48
|
+
- - ">="
|
49
|
+
- !ruby/object:Gem::Version
|
50
|
+
version: 3.3.0
|
51
|
+
type: :development
|
52
|
+
prerelease: false
|
53
|
+
version_requirements: !ruby/object:Gem::Requirement
|
54
|
+
requirements:
|
55
|
+
- - "~>"
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
version: '3.3'
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: 3.3.0
|
61
|
+
- !ruby/object:Gem::Dependency
|
62
|
+
name: activerecord
|
63
|
+
requirement: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - "~>"
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '4.0'
|
68
|
+
- - ">="
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
version: 4.0.0
|
71
|
+
type: :development
|
72
|
+
prerelease: false
|
73
|
+
version_requirements: !ruby/object:Gem::Requirement
|
74
|
+
requirements:
|
75
|
+
- - "~>"
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '4.0'
|
78
|
+
- - ">="
|
79
|
+
- !ruby/object:Gem::Version
|
80
|
+
version: 4.0.0
|
81
|
+
- !ruby/object:Gem::Dependency
|
82
|
+
name: activesupport
|
83
|
+
requirement: !ruby/object:Gem::Requirement
|
84
|
+
requirements:
|
85
|
+
- - "~>"
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: '4.0'
|
88
|
+
- - ">="
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
version: 4.0.0
|
91
|
+
type: :development
|
92
|
+
prerelease: false
|
93
|
+
version_requirements: !ruby/object:Gem::Requirement
|
94
|
+
requirements:
|
95
|
+
- - "~>"
|
96
|
+
- !ruby/object:Gem::Version
|
97
|
+
version: '4.0'
|
98
|
+
- - ">="
|
99
|
+
- !ruby/object:Gem::Version
|
100
|
+
version: 4.0.0
|
101
|
+
- !ruby/object:Gem::Dependency
|
102
|
+
name: pg
|
103
|
+
requirement: !ruby/object:Gem::Requirement
|
104
|
+
requirements:
|
105
|
+
- - "~>"
|
106
|
+
- !ruby/object:Gem::Version
|
107
|
+
version: 0.18.1
|
108
|
+
type: :development
|
109
|
+
prerelease: false
|
110
|
+
version_requirements: !ruby/object:Gem::Requirement
|
111
|
+
requirements:
|
112
|
+
- - "~>"
|
113
|
+
- !ruby/object:Gem::Version
|
114
|
+
version: 0.18.1
|
115
|
+
description: This gem provides a DSL on top of ActiveRecord to get collection of models
|
116
|
+
for index pages with filters.
|
117
|
+
email:
|
118
|
+
- arturop@nulogy.com
|
119
|
+
executables: []
|
120
|
+
extensions: []
|
121
|
+
extra_rdoc_files: []
|
122
|
+
files:
|
123
|
+
- ".gitignore"
|
124
|
+
- Gemfile
|
125
|
+
- Gemfile.lock
|
126
|
+
- README.md
|
127
|
+
- Rakefile
|
128
|
+
- config/database.yml
|
129
|
+
- index_query_builder.gemspec
|
130
|
+
- lib/index_query_builder.rb
|
131
|
+
- lib/index_query_builder/query_builder.rb
|
132
|
+
- lib/index_query_builder/query_definition.rb
|
133
|
+
- lib/index_query_builder/version.rb
|
134
|
+
- spec/index_query_builder_spec.rb
|
135
|
+
- spec/integration_spec_helper.rb
|
136
|
+
- spec/spec_helper.rb
|
137
|
+
homepage: https://github.com/arturopie/index_query_builder
|
138
|
+
licenses:
|
139
|
+
- MIT
|
140
|
+
metadata: {}
|
141
|
+
post_install_message:
|
142
|
+
rdoc_options: []
|
143
|
+
require_paths:
|
144
|
+
- lib
|
145
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
146
|
+
requirements:
|
147
|
+
- - ">="
|
148
|
+
- !ruby/object:Gem::Version
|
149
|
+
version: '0'
|
150
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
151
|
+
requirements:
|
152
|
+
- - ">="
|
153
|
+
- !ruby/object:Gem::Version
|
154
|
+
version: '0'
|
155
|
+
requirements: []
|
156
|
+
rubyforge_project:
|
157
|
+
rubygems_version: 2.4.8
|
158
|
+
signing_key:
|
159
|
+
specification_version: 4
|
160
|
+
summary: DSL for getting data for index pages.
|
161
|
+
test_files:
|
162
|
+
- spec/index_query_builder_spec.rb
|
163
|
+
- spec/integration_spec_helper.rb
|
164
|
+
- spec/spec_helper.rb
|