active_queryable 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|