paginated 1.0.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/README.md +57 -0
- data/lib/paginated/search.rb +109 -0
- data/lib/paginated/serialization.rb +79 -0
- data/lib/paginated.rb +58 -0
- metadata +60 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 25d6612ba23cb5271d1e3f372038e6561367ca5d89fa3b3954459bb12d745e69
|
4
|
+
data.tar.gz: 3a8251026ddc43e08426263df219db39fabbf8127a71059cc21a165afa2649e2
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 1a16eda994f3d8ce357367ecb016a0aeacb0b45c16c10519c1be03865a382fa42b2239014b1dbc6d8453ee58db0fc9065d1a2c319f90f568e4a3b26bf5130747
|
7
|
+
data.tar.gz: 7007c621aa8deb8a343cf8aca640d1fcb7bc8aa4710a2ef7fb142c317b04b86b00bfb9c1009fe4df16d6490541ff24915ae422d104d44d51ed84c3274bdae9f5
|
data/README.md
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
# Paginated
|
2
|
+
|
3
|
+
Гем для пагинации и фильтрации записей.
|
4
|
+
|
5
|
+
## Установка
|
6
|
+
|
7
|
+
Добавить в Gemfile
|
8
|
+
|
9
|
+
```
|
10
|
+
gem 'paginated'
|
11
|
+
```
|
12
|
+
|
13
|
+
И выполнить:
|
14
|
+
|
15
|
+
```
|
16
|
+
bundle
|
17
|
+
```
|
18
|
+
|
19
|
+
## Использование
|
20
|
+
|
21
|
+
Использование с помощью вызова метода `.collection` и передачи в него необходимых опций:
|
22
|
+
```
|
23
|
+
Paginated.new.collection()
|
24
|
+
```
|
25
|
+
|
26
|
+
Опции (с примерами):
|
27
|
+
|
28
|
+
`model: 'User'` - название модели, записи которой нужно обработать.
|
29
|
+
|
30
|
+
`fields: %i[...]` - список полей, которые должны присутствовать в списке записей (передается вместо `serializer`).
|
31
|
+
|
32
|
+
`serializer: 'UserSerializer'` - название сериалайзера, который обработает каждую запись (передается вместо `fields`).
|
33
|
+
|
34
|
+
`params:` - передаваемые параметры для фильтрации записей.
|
35
|
+
|
36
|
+
`order: :name` - название поля, по которому должна осуществляться сортировка.
|
37
|
+
|
38
|
+
`search: %w[...]` - список полей, по которым должна осуществляться фильтрация (поиск).
|
39
|
+
|
40
|
+
Например:
|
41
|
+
|
42
|
+
```
|
43
|
+
Paginated.new.collection(
|
44
|
+
model: 'User',
|
45
|
+
fields: %i[
|
46
|
+
email
|
47
|
+
name
|
48
|
+
],
|
49
|
+
search: %w[
|
50
|
+
email
|
51
|
+
name|ilike
|
52
|
+
role|custom_search.for_role
|
53
|
+
],
|
54
|
+
params:,
|
55
|
+
order: :name,
|
56
|
+
)
|
57
|
+
```
|
@@ -0,0 +1,109 @@
|
|
1
|
+
module Search
|
2
|
+
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
private
|
6
|
+
|
7
|
+
def search
|
8
|
+
@attrs[:search]&.each do |field|
|
9
|
+
# коллекция записей для дальнейшей фильтрации
|
10
|
+
entity = @objects || @model.unscoped.merge(@scopes)
|
11
|
+
|
12
|
+
# оператор и значение для поиска
|
13
|
+
operator, value = search_operands(field)
|
14
|
+
|
15
|
+
# пропускаем, если параметр для поиска не передан
|
16
|
+
next if value.blank? || value == 'null'
|
17
|
+
|
18
|
+
# кастомный поиск
|
19
|
+
if operator.include?('custom_search') && value.present?
|
20
|
+
# название скоупа из модели, который нужно применить
|
21
|
+
scope_name = operator.split('.')[1]
|
22
|
+
# применение скоупа
|
23
|
+
@objects = entity.send(scope_name, value)
|
24
|
+
@objects_count = @objects.count
|
25
|
+
next
|
26
|
+
end
|
27
|
+
|
28
|
+
# форматирование кавычек
|
29
|
+
if operator != 'IN' && !@model.defined_enums.key?(field)
|
30
|
+
value = quotations_formatting(value)
|
31
|
+
end
|
32
|
+
|
33
|
+
if field.include?('.')
|
34
|
+
# нужно приджойнить таблицу
|
35
|
+
join_association, field = field.split('.')
|
36
|
+
table = join_association.tableize
|
37
|
+
entity = entity.joins(join_association.to_sym)
|
38
|
+
else
|
39
|
+
table = @model.table_name
|
40
|
+
field = field.split('|')[0]
|
41
|
+
end
|
42
|
+
|
43
|
+
query = where_query(table, field, operator, value)
|
44
|
+
@objects = entity.where(query)
|
45
|
+
@objects_count = @objects.count
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def search_operands(field)
|
50
|
+
value = default_param_value(field)
|
51
|
+
|
52
|
+
# WHERE query operator
|
53
|
+
operator = field.split('|').size > 1 ? field.split('|').last : '='
|
54
|
+
|
55
|
+
# check for ILIKE operator
|
56
|
+
value = "%#{value}%" if operator == 'ilike' && value.present?
|
57
|
+
|
58
|
+
# check if multi-filter search enabled
|
59
|
+
if operator == 'multi' && !value.blank?
|
60
|
+
operator = 'IN'
|
61
|
+
value = "(#{value.join(', ')})"
|
62
|
+
end
|
63
|
+
|
64
|
+
# проверка диапазона (MIN/MAX)
|
65
|
+
if %w[min max].include?(operator)
|
66
|
+
range_field = field.split('.').last.split(':')[0].try { |p| p.split('|')[0] }
|
67
|
+
value = @params["#{range_field}_#{operator}"]
|
68
|
+
operator = operator == 'min' ? '>=' : '<='
|
69
|
+
end
|
70
|
+
|
71
|
+
# check array operator
|
72
|
+
if operator == 'array' && !value.blank?
|
73
|
+
operator = '='
|
74
|
+
value = "{#{value}}"
|
75
|
+
end
|
76
|
+
|
77
|
+
[operator, value]
|
78
|
+
end
|
79
|
+
|
80
|
+
def default_param_value(field)
|
81
|
+
key =
|
82
|
+
if field.include?('|')
|
83
|
+
# название ключа-поля без оператора (напр. ilike)
|
84
|
+
field.split('|')[0]
|
85
|
+
else
|
86
|
+
field
|
87
|
+
end
|
88
|
+
|
89
|
+
@params[key]
|
90
|
+
end
|
91
|
+
|
92
|
+
def where_query(table, field, operator, value)
|
93
|
+
if field.include?('_at')
|
94
|
+
# фильтрация по дате/времени
|
95
|
+
"EXTRACT(epoch FROM #{field}) #{operator} #{value}"
|
96
|
+
else
|
97
|
+
"#{table}.#{field} #{operator} #{value}"
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def quotations_formatting(string)
|
102
|
+
# удаление одинарных кавычек (напр. внутри строки)
|
103
|
+
value = string.to_s.gsub(/'/, '')
|
104
|
+
|
105
|
+
# добавление одинарных кавычек вокруг строки
|
106
|
+
"'#{value}'"
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module Serialization
|
2
|
+
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
private
|
6
|
+
|
7
|
+
def serialize
|
8
|
+
# коллекция записей / сущность для применения аггрегирования
|
9
|
+
entity = @objects || @model.unscoped.merge(@scopes)
|
10
|
+
|
11
|
+
# применение сортировки и пагинации (если необходимо) к коллекции записей
|
12
|
+
@objects =
|
13
|
+
if @paginate
|
14
|
+
entity.offset(@offset).merge(sort_proc).limit(@limit)
|
15
|
+
else
|
16
|
+
entity.merge(sort_proc)
|
17
|
+
end
|
18
|
+
|
19
|
+
# сериализация с помощью переданных полей или сериалайзера
|
20
|
+
if @fields
|
21
|
+
serialize_with_fields
|
22
|
+
elsif @attrs[:serializer]
|
23
|
+
serialize_with_serializer
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def serialize_with_fields
|
28
|
+
# добавление ID в список полей по умолчанию
|
29
|
+
@fields.push(:id)
|
30
|
+
|
31
|
+
# список массивов значений
|
32
|
+
@objects = @objects.map { |i| obtain_fields_values(i) }
|
33
|
+
|
34
|
+
# трансформация массивов значений в объекты (ключ-значение)
|
35
|
+
@objects = @objects.map { |i| @fields.zip(i).to_h }
|
36
|
+
end
|
37
|
+
|
38
|
+
def serialize_with_serializer
|
39
|
+
serializer = @attrs[:serializer].constantize
|
40
|
+
@objects = @objects.map { |i| serializer.new(i, root: false) }
|
41
|
+
end
|
42
|
+
|
43
|
+
def sort_proc
|
44
|
+
# сортировка не задана явно, не передан параметр
|
45
|
+
# или значение переданного параметра не совпадает с ожидаемым
|
46
|
+
if @sort.blank? || @sort.map { |i| i.split('|')[0] }.exclude?(@params[:sort])
|
47
|
+
field = @order
|
48
|
+
return proc { order("#{field} DESC") }
|
49
|
+
end
|
50
|
+
|
51
|
+
@sort.each do |sort_option|
|
52
|
+
sort, field = sort_option.split('|')
|
53
|
+
|
54
|
+
# пропускаем, если поле для сортировки не совпадает с переданным параметром
|
55
|
+
next if @params[:sort] != sort
|
56
|
+
|
57
|
+
# если поле для сортировки совпадает с ключом переданного параметра
|
58
|
+
field = sort if field.blank?
|
59
|
+
|
60
|
+
sort_order = "#{@params[:sort_order]} NULLS LAST"
|
61
|
+
|
62
|
+
order_proc =
|
63
|
+
if field.split('.').count > 1
|
64
|
+
# сортировка по связанной таблице
|
65
|
+
relation, column = field.split('.')
|
66
|
+
proc { eager_load(relation).order("#{relation.tableize}.#{column} #{sort_order}") }
|
67
|
+
else
|
68
|
+
proc { order("#{field} #{sort_order}") }
|
69
|
+
end
|
70
|
+
|
71
|
+
return order_proc
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def obtain_fields_values(object)
|
76
|
+
@fields.map { |f| object.send(f) }
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
data/lib/paginated.rb
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'paginated/search'
|
2
|
+
require 'paginated/serialization'
|
3
|
+
|
4
|
+
class Paginated
|
5
|
+
|
6
|
+
include Search
|
7
|
+
include Serialization
|
8
|
+
|
9
|
+
def collection(**attrs)
|
10
|
+
@attrs = attrs
|
11
|
+
@fields = attrs[:fields]
|
12
|
+
@scopes = attrs[:scopes] || {}
|
13
|
+
@params = attrs[:params].stringify_keys
|
14
|
+
@offset = @params['offset'].to_i
|
15
|
+
@limit = @params['limit'] || 20
|
16
|
+
@order = attrs[:order] || :id
|
17
|
+
@sort = attrs[:sort]
|
18
|
+
@model = attrs[:model].constantize
|
19
|
+
@root = @params.key?('root') ? @params['root'] : false
|
20
|
+
@paginate = @params.key?('paginate') ? @params['paginate'].to_s == 'true' : true
|
21
|
+
@objects_count = records_count(attrs[:model])
|
22
|
+
|
23
|
+
if @objects_count > 0
|
24
|
+
search
|
25
|
+
serialize
|
26
|
+
else
|
27
|
+
@objects = []
|
28
|
+
end
|
29
|
+
|
30
|
+
if @root
|
31
|
+
# записи в корне
|
32
|
+
@objects
|
33
|
+
else
|
34
|
+
# результат с пагинацией
|
35
|
+
{ count: @objects_count, objects: @objects }
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def records_count(model_name)
|
42
|
+
# получение кол-ва записей из кеша
|
43
|
+
cache_key = "#{model_name}_records_count"
|
44
|
+
cached_value = Rails.cache.read(cache_key)
|
45
|
+
return cached_value if cached_value
|
46
|
+
|
47
|
+
# получение кол-ва записей из запроса к БД
|
48
|
+
db_value = @model.unscoped.merge(@scopes).count(:id)
|
49
|
+
|
50
|
+
# кеширование кол-ва записей при превышении порогового значения
|
51
|
+
if db_value >= 1_000
|
52
|
+
Rails.cache.write(cache_key, db_value, expires_in: 30.minutes)
|
53
|
+
end
|
54
|
+
|
55
|
+
db_value
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
metadata
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: paginated
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Павел Бабин
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2023-11-03 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rails
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '6.0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '6.0'
|
27
|
+
description: Простая пагинация записей и поиск для Rails-приложений
|
28
|
+
email: babin359@gmail.com
|
29
|
+
executables: []
|
30
|
+
extensions: []
|
31
|
+
extra_rdoc_files: []
|
32
|
+
files:
|
33
|
+
- README.md
|
34
|
+
- lib/paginated.rb
|
35
|
+
- lib/paginated/search.rb
|
36
|
+
- lib/paginated/serialization.rb
|
37
|
+
homepage:
|
38
|
+
licenses:
|
39
|
+
- MIT
|
40
|
+
metadata: {}
|
41
|
+
post_install_message:
|
42
|
+
rdoc_options: []
|
43
|
+
require_paths:
|
44
|
+
- lib
|
45
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - ">="
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: 2.5.0
|
50
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
requirements: []
|
56
|
+
rubygems_version: 3.0.6
|
57
|
+
signing_key:
|
58
|
+
specification_version: 4
|
59
|
+
summary: Пагинация для Rails-приложений
|
60
|
+
test_files: []
|