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 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