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.
- checksums.yaml +7 -0
- data/lib/active-queryable.rb +3 -0
- data/lib/active_queryable.rb +128 -0
- data/lib/active_queryable/version.rb +3 -0
- data/spec/active_queryable/filters_spec.rb +29 -0
- data/spec/active_queryable/ordering_spec.rb +18 -0
- data/spec/active_queryable/pagination_spec.rb +23 -0
- data/spec/spec_helper.rb +27 -0
- data/spec/support/match_queries.rb +9 -0
- data/spec/support/schema.rb +54 -0
- metadata +119 -0
checksums.yaml
ADDED
@@ -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,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,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
|
data/spec/spec_helper.rb
ADDED
@@ -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,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
|