paginated 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|