index_query_builder 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in index_query_builder.gemspec
4
+ gemspec
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
@@ -0,0 +1,3 @@
1
+ adapter: postgresql
2
+ host: localhost
3
+ database: index_query_builder_test
@@ -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,3 @@
1
+ module IndexQueryBuilder
2
+ VERSION = "0.0.1"
3
+ 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
@@ -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