active_queryable 0.2.0

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