paginated 1.0.0

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