active_queryable 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 85318f4379a335f5d2398c1b85ab19ddb63cfbfa6955882c4dcd9e7a24a5168f
4
+ data.tar.gz: d9dbb94f9c174af1a34b680152b8add196cf1ddf1a38a895fc0f32370855a985
5
+ SHA512:
6
+ metadata.gz: f64dda266de6dd21f1c6a21fae9bfba014a72a16f7acf6c560f6feea3771b4663db82b11b97c97fc751379963aa2b0b29b26a6a34339d2f8dade38576ee5868b
7
+ data.tar.gz: 89bad17c363651ddbc6c75ca6eeda418d436682c9376a1c9670e81bbc959e491b5a3b0acc1345cf71879ea01ad5ccf623fc8624a00e698c34a8131e1f1730c63
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_queryable'
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/concern'
4
+ require 'kaminari/activerecord'
5
+
6
+ module ActiveQueryable
7
+ extend ActiveSupport::Concern
8
+
9
+ QUERYABLE_VALID_PARAMS = [:filter, :sort, :page, :per].freeze
10
+
11
+ included do
12
+ class_attribute :_queryable_default_order
13
+ class_attribute :_queryable_default_page
14
+ class_attribute :_queryable_default_per
15
+ class_attribute :_queryable_filter_keys
16
+ end
17
+
18
+ module Initializer
19
+ def as_queryable
20
+ send :include, ActiveQueryable
21
+ end
22
+ end
23
+
24
+ module ClassMethods
25
+ def queryable(options)
26
+ self._queryable_default_order = options[:order] || { id: :asc }
27
+ self._queryable_default_page = options[:page] || 1
28
+ self._queryable_default_per = options[:per] || 25
29
+ self._queryable_filter_keys = ((options[:filter] || []) + ['not']).map(&:to_sym)
30
+
31
+ scope :query_by, ->(params) { queryable_scope(params) }
32
+ scope :of_not, ->(ids) { where.not(id: ids) }
33
+ end
34
+
35
+ def queryable_scope(params)
36
+ params = params.to_unsafe_h if params.respond_to? :to_unsafe_h
37
+ params = params.with_indifferent_access if params.respond_to?(:with_indifferent_access)
38
+ params.each_key { |k| QUERYABLE_VALID_PARAMS.include?(k.to_sym) || Rails.logger.error("Invalid key #{k} in queryable") }
39
+
40
+ order_params = queryable_validate_order_params(params[:sort])
41
+ query = queryable_parse_order_scope(order_params, self)
42
+
43
+ queryable_filtered_scope(params, query)
44
+ end
45
+
46
+ private
47
+
48
+ def queryable_filtered_scope(params, query)
49
+ filter_params = queryable_validate_filter_params(params[:filter])
50
+
51
+ page_params = queryable_validate_page_params(params)
52
+
53
+ scope = queryable_parse_filter_scope(filter_params, query)
54
+
55
+ unless page_params[:per] == 'all'
56
+ scope = scope
57
+ .page(page_params[:page])
58
+ .per(page_params[:per])
59
+ end
60
+
61
+ scope
62
+ end
63
+
64
+ def queryable_validate_order_params(params)
65
+ queryable_parse_order_params(params) || _queryable_default_order
66
+ end
67
+
68
+ def queryable_validate_page_params(params)
69
+ page_params = {}
70
+ if params[:page].respond_to?(:dig)
71
+ page_params[:page] = params.dig(:page, :number) || _queryable_default_page
72
+ page_params[:per] = params.dig(:page, :size) || params[:per] || _queryable_default_per
73
+ else
74
+ page_params[:page] = params[:page] || _queryable_default_page
75
+ page_params[:per] = params[:per] || _queryable_default_per
76
+ end
77
+ page_params
78
+ end
79
+
80
+ def queryable_validate_filter_params(filter_params)
81
+ return nil if filter_params.nil?
82
+
83
+ unpermitted = filter_params.except(*_queryable_filter_keys)
84
+ Rails.logger.warn("Unpermitted queryable parameters: #{unpermitted.keys.join(', ')}") if unpermitted.present?
85
+
86
+ filter_params.slice(*_queryable_filter_keys)
87
+ end
88
+
89
+ def queryable_parse_order_params(params)
90
+ return nil unless params.is_a? String
91
+
92
+ params.split(',').map! do |param|
93
+ clean_param = param.start_with?('-') ? param[1..-1] : param
94
+ [clean_param, clean_param == param ? :asc : :desc]
95
+ end.to_h
96
+ end
97
+
98
+ def queryable_parse_order_scope(params, query)
99
+ return query unless params
100
+
101
+ params.inject(query) do |current_query, (k, v)|
102
+ scope = "by_#{k}"
103
+
104
+ if current_query.respond_to?(scope, true)
105
+ current_query.public_send(scope, v)
106
+ else
107
+ current_query.order(params)
108
+ end
109
+ end || query
110
+ end
111
+
112
+ def queryable_parse_filter_scope(params, query)
113
+ return query unless params
114
+
115
+ params.inject(query) do |current_query, (k, v)|
116
+ scope = "of_#{k}"
117
+
118
+
119
+ if current_query.respond_to?(scope, true)
120
+ current_query.public_send(scope, v)
121
+ else
122
+ current_query.where(k => v)
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end
128
+ ActiveRecord::Base.send :extend, ActiveQueryable::Initializer
@@ -0,0 +1,3 @@
1
+ module ActiveQueryable
2
+ VERSION = '0.2.0'
3
+ end
@@ -0,0 +1,29 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'Filters' do
4
+ context 'default filters' do
5
+ it 'applies column name-based filters' do
6
+ query = Person.query_by(filter: { name: 'john doe' }, per: 'all')
7
+ expect(query.to_sql).to include('"people"."name" = \'john doe\'')
8
+ expect(query).to include(Person.first)
9
+ end
10
+
11
+ it 'applies id-exclusion filters' do
12
+ query = Person.query_by(filter: { not: [1, 2] }, per: 'all')
13
+ expect(query.to_sql).to include('NOT IN (1, 2)')
14
+ expect(query).to be_empty
15
+ end
16
+ end
17
+
18
+ it 'applies an explicit name filter' do
19
+ query = Article.query_by(filter: { title: 'sOME ARTICLE 1' }, per: 'all')
20
+ expect(query.to_sql).to include('lower(')
21
+ expect(query).to include(Article.find_by_title!('Some article 1'))
22
+ end
23
+
24
+ it 'applies an explicit article title filter' do
25
+ query = Person.query_by(filter: { article_title: 'Some article 1' }, per: 'all')
26
+ expect(query.to_sql).to include('INNER JOIN "articles"')
27
+ expect(query).to include(Person.first)
28
+ end
29
+ end
@@ -0,0 +1,18 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'Ordering' do
4
+ it 'default order' do
5
+ query = Person.query_by(per: 'all')
6
+ expect(query.to_sql).to include('ORDER BY "people"."name" ASC')
7
+ end
8
+
9
+ it 'order by column' do
10
+ query = Person.query_by(sort: '-name', per: 'all')
11
+ expect(query.to_sql).to include('ORDER BY "people"."name" DESC')
12
+ end
13
+
14
+ it 'order by scope' do
15
+ query = Person.query_by(sort: '-article_title', per: 'all')
16
+ expect(query.to_sql).to include('ORDER BY "articles"."title" DESC')
17
+ end
18
+ end
@@ -0,0 +1,23 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'Pagination' do
4
+ it 'paginates using `per` and `page`' do
5
+ query = Article.query_by(per: 2, page: 2)
6
+ expect(query.to_sql).to include('LIMIT 2 OFFSET 2')
7
+ end
8
+
9
+ it 'paginates using `page[number]` and `page[size]`' do
10
+ query = Person.query_by(page: { number: 2, size: 2 })
11
+ expect(query.to_sql).to include('LIMIT 2 OFFSET 2')
12
+ end
13
+
14
+ it 'accepts strings and numbers' do
15
+ query = Person.query_by(page: { number: '2', size: '2' })
16
+ expect(query.to_sql).to include('LIMIT 2 OFFSET 2')
17
+ end
18
+
19
+ it 'ignores invalid values of page' do
20
+ query = Person.query_by(page: {}, per: 20)
21
+ expect(query.to_sql).to include('LIMIT 20 OFFSET 0')
22
+ end
23
+ end
@@ -0,0 +1,27 @@
1
+ require 'active_support'
2
+ require 'rspec'
3
+ require 'active_record'
4
+ require 'active_queryable'
5
+ # require 'kaminari-activerecord'
6
+
7
+ I18n.enforce_available_locales = false
8
+ RSpec::Expectations.configuration.warn_about_potential_false_positives = false
9
+
10
+ Dir[File.expand_path('../support/*.rb', __FILE__)].each { |f| require f }
11
+
12
+ RSpec.configure do |config|
13
+
14
+ config.before(:suite) do
15
+ Schema.create
16
+ end
17
+
18
+
19
+ config.around(:each) do |example|
20
+ ActiveRecord::Base.transaction do
21
+ example.run
22
+ raise ActiveRecord::Rollback
23
+ end
24
+ end
25
+ end
26
+
27
+
@@ -0,0 +1,9 @@
1
+ def within_query
2
+ $__instrumentation = ActiveSupport::Notifications.subscribe 'sql.active_record' do |_, _, _, _, data|
3
+ yield data[:sql]
4
+ end
5
+ end
6
+
7
+ def query_clear
8
+ ActiveSupport::Notifications.unsubscribe($__instrumentation) if $__instrumentation
9
+ end
@@ -0,0 +1,54 @@
1
+ require 'active_record'
2
+
3
+ ActiveRecord::Base.establish_connection(
4
+ adapter: 'sqlite3',
5
+ database: ':memory:'
6
+ )
7
+
8
+ class Person < ActiveRecord::Base
9
+ as_queryable
10
+ queryable order: { name: :asc }, filter: ['name', 'article_title']
11
+
12
+ has_many :articles
13
+
14
+ scope :of_article_title, ->(title) { joins(:articles).where(articles: { title: title }) }
15
+ scope :by_article_title, ->(direction) { joins(:articles).order(:'articles.title' => direction) }
16
+
17
+ end
18
+
19
+ class Article < ActiveRecord::Base
20
+ as_queryable
21
+ queryable order: { title: :asc }, filter: ['title']
22
+
23
+ scope :of_title, ->(title) { where('lower(title) = ?', title.downcase) }
24
+
25
+ belongs_to :person
26
+ end
27
+
28
+ module Schema
29
+ def self.create
30
+ ActiveRecord::Migration.verbose = false
31
+
32
+ ActiveRecord::Schema.define do
33
+ create_table :people, force: true do |t|
34
+ t.string :name
35
+ t.string :email
36
+ t.boolean :terms_and_conditions, default: false
37
+ t.timestamps null: false
38
+ end
39
+
40
+ create_table :articles, force: true do |t|
41
+ t.integer :person_id
42
+ t.string :title
43
+ t.text :body
44
+ t.integer :status
45
+ t.timestamps null: false
46
+ end
47
+ end
48
+
49
+ person = Person.create!(name: 'john doe', email: 'e@mail.com', terms_and_conditions: true)
50
+ 10.times do |i|
51
+ article = Article.create!(person: person, title: "Some article #{i}", body: 'hello!', status: 0)
52
+ end
53
+ end
54
+ end
metadata ADDED
@@ -0,0 +1,119 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: active_queryable
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - Mònade
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2019-12-16 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '5'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '7'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '5'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '7'
33
+ - !ruby/object:Gem::Dependency
34
+ name: kaminari-activerecord
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ type: :runtime
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ - !ruby/object:Gem::Dependency
48
+ name: rspec
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '3'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '3'
61
+ - !ruby/object:Gem::Dependency
62
+ name: rubocop
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ type: :development
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ description: Gem to make easier model's filtering, sorting and pagination
76
+ email: team@monade.io
77
+ executables: []
78
+ extensions: []
79
+ extra_rdoc_files: []
80
+ files:
81
+ - lib/active-queryable.rb
82
+ - lib/active_queryable.rb
83
+ - lib/active_queryable/version.rb
84
+ - spec/active_queryable/filters_spec.rb
85
+ - spec/active_queryable/ordering_spec.rb
86
+ - spec/active_queryable/pagination_spec.rb
87
+ - spec/spec_helper.rb
88
+ - spec/support/match_queries.rb
89
+ - spec/support/schema.rb
90
+ homepage: https://rubygems.org/gems/active_queryable
91
+ licenses:
92
+ - MIT
93
+ metadata: {}
94
+ post_install_message:
95
+ rdoc_options: []
96
+ require_paths:
97
+ - lib
98
+ required_ruby_version: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: 2.3.0
103
+ required_rubygems_version: !ruby/object:Gem::Requirement
104
+ requirements:
105
+ - - ">="
106
+ - !ruby/object:Gem::Version
107
+ version: '0'
108
+ requirements: []
109
+ rubygems_version: 3.0.3
110
+ signing_key:
111
+ specification_version: 4
112
+ summary: Gem to make easier model's filtering, sorting and pagination
113
+ test_files:
114
+ - spec/spec_helper.rb
115
+ - spec/active_queryable/ordering_spec.rb
116
+ - spec/active_queryable/pagination_spec.rb
117
+ - spec/active_queryable/filters_spec.rb
118
+ - spec/support/schema.rb
119
+ - spec/support/match_queries.rb