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 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: []