care 0.1.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/.gitignore +12 -0
- data/.rspec +0 -0
- data/.rubocop.yml +10 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +0 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/Gemfile +27 -0
- data/Gemfile.lock +206 -0
- data/README.md +40 -0
- data/Rakefile +12 -0
- data/bin/console +0 -0
- data/bin/setup +0 -0
- data/care.gemspec +49 -0
- data/exe/care +0 -0
- data/lib/care.rb +27 -0
- data/lib/care/auto_finder/by_ids.rb +24 -0
- data/lib/care/auto_finder/findable.rb +68 -0
- data/lib/care/auto_finder/finder_methods.rb +28 -0
- data/lib/care/auto_finder/paginateble.rb +30 -0
- data/lib/care/auto_finder/searchable.rb +56 -0
- data/lib/care/auto_finder/searcher.rb +101 -0
- data/lib/care/auto_finder/sortable.rb +41 -0
- data/lib/care/finder.rb +17 -0
- data/lib/care/jwt_service.rb +24 -0
- data/lib/care/rspec.rb +15 -0
- data/lib/care/seed.rb +21 -0
- data/lib/care/support/authorization_helper.rb +20 -0
- data/lib/care/support/error_collection.rb +43 -0
- data/lib/care/support/factory_bot.rb +5 -0
- data/lib/care/support/pagination.rb +9 -0
- data/lib/care/support/parameters.rb +39 -0
- data/lib/care/support/request_helper.rb +11 -0
- data/lib/care/version.rb +5 -0
- data/lib/generators/care/install/USAGE +19 -0
- data/lib/generators/care/install/install_generator.rb +33 -0
- data/lib/generators/care/install/templates/.env.local.example +62 -0
- data/lib/generators/care/install/templates/active_model_serializer.rb +1 -0
- data/lib/generators/care/install/templates/centrifuge.rb +4 -0
- data/lib/generators/care/install/templates/rswag-ui.rb +3 -0
- data/lib/generators/care/install/templates/rswag_api.rb +3 -0
- data/lib/generators/care/install/templates/swagger.yml +76 -0
- data/lib/generators/care/install/templates/swagger_helper.rb +24 -0
- data/lib/patch/action_controller/api.rb +17 -0
- data/lib/patch/action_controller/concerns/authentication.rb +50 -0
- data/lib/patch/action_controller/concerns/authorization.rb +46 -0
- data/lib/patch/action_controller/concerns/exception_handler.rb +67 -0
- data/lib/patch/action_controller/concerns/filter_sort_pagination.rb +98 -0
- metadata +321 -0
data/exe/care
ADDED
File without changes
|
data/lib/care.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "care/version"
|
3
|
+
require "care/seed"
|
4
|
+
require 'care/jwt_service'
|
5
|
+
|
6
|
+
require 'rswag/specs'
|
7
|
+
require 'rswag/api'
|
8
|
+
require 'rswag/ui'
|
9
|
+
|
10
|
+
|
11
|
+
require 'rails/generators'
|
12
|
+
require 'jwt'
|
13
|
+
require 'patch/action_controller/api'
|
14
|
+
require 'cancancan'
|
15
|
+
|
16
|
+
require 'care/finder'
|
17
|
+
require 'activerecord-import'
|
18
|
+
|
19
|
+
require 'active_model_serializers'
|
20
|
+
ActiveModel::Serializer.config.adapter = :json
|
21
|
+
|
22
|
+
require 'kaminari'
|
23
|
+
|
24
|
+
module Care
|
25
|
+
class Error < StandardError; end
|
26
|
+
# Your code goes here...
|
27
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Care::AutoFinder
|
4
|
+
#
|
5
|
+
# Содержит методы для фильтрации по ids
|
6
|
+
#
|
7
|
+
# @example
|
8
|
+
# class Finder
|
9
|
+
# include ByIds
|
10
|
+
#
|
11
|
+
# attr_accessor :params
|
12
|
+
#
|
13
|
+
# def call
|
14
|
+
# params = { ids: [1, 2, 3] }
|
15
|
+
# by_ids(Document)
|
16
|
+
# end
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
module ByIds
|
20
|
+
def ids(items)
|
21
|
+
params[:ids].present? ? items.where(id: params[:ids]) : items
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Care::AutoFinder
|
4
|
+
module Findable
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
attr_accessor :chain_filter
|
9
|
+
# attr_accessor :relation
|
10
|
+
|
11
|
+
def set_relation(relation)
|
12
|
+
@relation = relation
|
13
|
+
end
|
14
|
+
|
15
|
+
def entity
|
16
|
+
self.relation = yield
|
17
|
+
end
|
18
|
+
|
19
|
+
def filter_by(*filters)
|
20
|
+
self.chain_filter = filters
|
21
|
+
end
|
22
|
+
|
23
|
+
def relation
|
24
|
+
@relation ||= /(.*)Finder/.match(name)[1].singularize.constantize
|
25
|
+
end
|
26
|
+
|
27
|
+
#
|
28
|
+
# @param [Hash] options параметры для работы файдера
|
29
|
+
# @option options [ActiveRecord] :relation модель, для которой будет вестись поиск
|
30
|
+
# @option options [Hash] :params опции для поиска
|
31
|
+
#
|
32
|
+
# @example
|
33
|
+
# FooBarFinder
|
34
|
+
# .call(
|
35
|
+
# relation: FooBar,
|
36
|
+
# params: { page: 10, limit: 2, name: 'Lorem' }
|
37
|
+
# )
|
38
|
+
#
|
39
|
+
def call(options = {})
|
40
|
+
new(options).call
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def initialize(options = {})
|
45
|
+
options = options.symbolize_keys
|
46
|
+
@relation = options[:relation] || self.class.relation || options[:params][:relation]
|
47
|
+
@params = options[:params] || options || {}
|
48
|
+
end
|
49
|
+
|
50
|
+
def call
|
51
|
+
collection = relation || self.class.relation
|
52
|
+
|
53
|
+
chain_filter.each do |filter|
|
54
|
+
collection = send(filter, collection)
|
55
|
+
end
|
56
|
+
|
57
|
+
collection.distinct
|
58
|
+
end
|
59
|
+
|
60
|
+
protected
|
61
|
+
|
62
|
+
attr_reader :relation, :params
|
63
|
+
|
64
|
+
def chain_filter
|
65
|
+
self.class.chain_filter || [:ids, :paginate, :search, :sort]
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Care::AutoFinder
|
4
|
+
module FinderMethods
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
include Care::AutoFinder::Paginateble
|
9
|
+
include Care::AutoFinder::Searchable
|
10
|
+
include Care::AutoFinder::Sortable
|
11
|
+
include Care::AutoFinder::ByIds
|
12
|
+
end
|
13
|
+
|
14
|
+
def range_filter(options)
|
15
|
+
options = Struct.new(*options.keys).new(*options.values)
|
16
|
+
|
17
|
+
if options.start.present? && options.end.present?
|
18
|
+
options.collection.where(options.column => options.start..options.end)
|
19
|
+
elsif options.start.present?
|
20
|
+
options.collection.where("#{options.column} >= ?", options.start)
|
21
|
+
elsif options.end.present?
|
22
|
+
options.collection.where("#{options.column} <= ?", options.end)
|
23
|
+
else
|
24
|
+
options.collection
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Care::AutoFinder
|
4
|
+
#
|
5
|
+
# Содержит методы для пагинации коллекции
|
6
|
+
#
|
7
|
+
# @example
|
8
|
+
# class Finder
|
9
|
+
# include Paginateble
|
10
|
+
#
|
11
|
+
# attr_reader :params
|
12
|
+
#
|
13
|
+
# def call
|
14
|
+
# params = { page: 2, limit: 10 }
|
15
|
+
# by_page(Document)
|
16
|
+
# end
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
module Paginateble
|
20
|
+
DEFAULT_LIMIT = 100
|
21
|
+
|
22
|
+
def by_page(items)
|
23
|
+
params[:page].present? ? items.page(params[:page]).per(params[:limit] || DEFAULT_LIMIT) : items
|
24
|
+
end
|
25
|
+
|
26
|
+
def paginate(items)
|
27
|
+
by_page(items)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Care::AutoFinder
|
4
|
+
#
|
5
|
+
# Содержит методы для полнотекстового поиска в коллекции
|
6
|
+
#
|
7
|
+
# @example
|
8
|
+
# class Finder
|
9
|
+
# include Care::AutoFinder::Searchable
|
10
|
+
#
|
11
|
+
# attr_reader :params
|
12
|
+
#
|
13
|
+
# search_by :name, :version
|
14
|
+
# search_by :name, address: %i[locality street house flat]
|
15
|
+
# # columns => [:name, {:address=>[:locality, :street, :house, :flat]}]
|
16
|
+
#
|
17
|
+
# def call
|
18
|
+
# params = { search: 'сзи' }
|
19
|
+
# search(Document)
|
20
|
+
# end
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
module Searchable
|
24
|
+
extend ActiveSupport::Concern
|
25
|
+
|
26
|
+
def search(items)
|
27
|
+
if params[:search].present?
|
28
|
+
Searcher.call(items: items, columns: columns, search: params[:search])
|
29
|
+
else
|
30
|
+
items
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def search_into_array_field(column)
|
35
|
+
relation.where("array_to_string(#{column}, '||') LIKE ?", "%#{params[:search]}%")
|
36
|
+
end
|
37
|
+
|
38
|
+
def search_into_joined_field(field_path)
|
39
|
+
relation.where("#{field_path} LIKE ?", "%#{params[:search]}%")
|
40
|
+
end
|
41
|
+
|
42
|
+
module ClassMethods
|
43
|
+
attr_accessor :columns
|
44
|
+
|
45
|
+
def search_by(*columns)
|
46
|
+
self.columns = columns
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
protected
|
51
|
+
|
52
|
+
def columns
|
53
|
+
self.class.columns
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Care::AutoFinder
|
4
|
+
class Searcher
|
5
|
+
attr_reader :relation
|
6
|
+
attr_reader :query
|
7
|
+
attr_reader :columns
|
8
|
+
attr_reader :attributes
|
9
|
+
attr_reader :arel_table
|
10
|
+
attr_reader :table
|
11
|
+
attr_reader :pattern
|
12
|
+
|
13
|
+
def initialize(params)
|
14
|
+
@relation = params[:items]
|
15
|
+
@columns = params[:columns]
|
16
|
+
@attributes = params[:columns]
|
17
|
+
@query = params[:search]
|
18
|
+
@arel_table = relation.arel_table
|
19
|
+
@table = relation.arel_table
|
20
|
+
@pattern = "%#{sanitaze(query)}%"
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.call(params)
|
24
|
+
new(params).call
|
25
|
+
end
|
26
|
+
|
27
|
+
def call
|
28
|
+
predicate = complile_node(relation, attributes).reduce { |memo, expression| memo ? memo.or(expression) : memo }
|
29
|
+
relation.where(predicate)
|
30
|
+
end
|
31
|
+
|
32
|
+
protected
|
33
|
+
|
34
|
+
def complile_node(relation, attributes)
|
35
|
+
expression = [*attributes].map { |attribute|
|
36
|
+
if attribute.is_a?(Symbol)
|
37
|
+
compile(relation, attribute)
|
38
|
+
elsif attribute.is_a?(Hash)
|
39
|
+
attribute.map do |child_relation, child_attributes|
|
40
|
+
complile_node(inner_relation(relation, child_relation), child_attributes)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
}
|
44
|
+
|
45
|
+
expression.flatten
|
46
|
+
end
|
47
|
+
|
48
|
+
#
|
49
|
+
# rubocop:disable Metrics/AbcSize
|
50
|
+
#
|
51
|
+
def compile(relation, attribute)
|
52
|
+
if relation.columns_hash[attribute.to_s] && relation.columns_hash[attribute.to_s].type == :date
|
53
|
+
Arel.sql("to_char(#{attribute}, 'DD.MM.YYYY')").matches(pattern)
|
54
|
+
elsif relation.columns_hash[attribute.to_s] && relation.columns_hash[attribute.to_s].type == :datetime
|
55
|
+
Arel.sql("to_char(#{attribute}, 'DD.MM.YYYY')").matches(pattern)
|
56
|
+
elsif relation.columns_hash[attribute.to_s] && relation.columns_hash[attribute.to_s].type == :integer
|
57
|
+
Arel.sql("to_char(#{attribute}, '999')").matches(pattern)
|
58
|
+
elsif relation.columns_hash[attribute.to_s]&.array?
|
59
|
+
Arel.sql("array_to_string(#{attribute}, ', ')").matches(pattern)
|
60
|
+
elsif relation.respond_to?(:custom_columns) && relation.custom_columns[attribute]
|
61
|
+
Arel.sql(relation.custom_columns[attribute]).matches(pattern)
|
62
|
+
else
|
63
|
+
relation.arel_table[attribute].matches(pattern)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
#
|
67
|
+
# rubocop:enable Metrics/AbcSize
|
68
|
+
#
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def sanitaze(string, escape_character = '\\')
|
73
|
+
pattern = Regexp.union(escape_character, "%", "_")
|
74
|
+
string.gsub(pattern) { |x| [escape_character, x].join }
|
75
|
+
end
|
76
|
+
|
77
|
+
def inner_relation(relation, target_relation)
|
78
|
+
target = relation.reflections[target_relation.to_s]
|
79
|
+
target ? target.klass : ArtificalRelation.new(target_relation)
|
80
|
+
end
|
81
|
+
|
82
|
+
#
|
83
|
+
# Исскуственая модель, имеющая функцию arel_table
|
84
|
+
#
|
85
|
+
class ArtificalRelation
|
86
|
+
attr_accessor :table_name
|
87
|
+
|
88
|
+
def initialize(table_name)
|
89
|
+
@table_name = table_name
|
90
|
+
end
|
91
|
+
|
92
|
+
def arel_table
|
93
|
+
Arel::Table.new(table_name)
|
94
|
+
end
|
95
|
+
|
96
|
+
def columns_hash
|
97
|
+
{}
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Care::AutoFinder
|
4
|
+
#
|
5
|
+
# Содержит методы для сортировки коллекции
|
6
|
+
#
|
7
|
+
# @example
|
8
|
+
# class Finder
|
9
|
+
# include Sortable
|
10
|
+
#
|
11
|
+
# attr_reader :params
|
12
|
+
#
|
13
|
+
# def call
|
14
|
+
# params = { sort: 'name_asc' }
|
15
|
+
# sort(Document)
|
16
|
+
# end
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
module Sortable
|
20
|
+
# def sort(items)
|
21
|
+
# params[:sort].present? ? items.order(order_expiration) : items.order(created_at: :asc)
|
22
|
+
# end
|
23
|
+
|
24
|
+
def sort(items)
|
25
|
+
if params[:sort].present?
|
26
|
+
items.order(order_expiration)
|
27
|
+
elsif items.methods.include?(:default_sort)
|
28
|
+
items.default_sort
|
29
|
+
else
|
30
|
+
items.order(created_at: :asc)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def order_expiration
|
37
|
+
order_match_parts = params[:sort].rpartition("_")
|
38
|
+
{order_match_parts.first => order_match_parts.last}
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
data/lib/care/finder.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require_relative 'auto_finder/findable'
|
2
|
+
require_relative 'auto_finder/paginateble'
|
3
|
+
require_relative 'auto_finder/by_ids'
|
4
|
+
require_relative 'auto_finder/sortable'
|
5
|
+
require_relative 'auto_finder/searchable'
|
6
|
+
require_relative 'auto_finder/finder_methods'
|
7
|
+
|
8
|
+
module Care
|
9
|
+
class Finder
|
10
|
+
include Care::AutoFinder::Findable
|
11
|
+
include Care::AutoFinder::FinderMethods
|
12
|
+
|
13
|
+
def self.call(relation, params)
|
14
|
+
new(relation: relation, params: params).call
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Care
|
2
|
+
class JwtService
|
3
|
+
def decode(token)
|
4
|
+
decoded_token = JWT.decode(token, public_key, true, algorithm: 'RS256')
|
5
|
+
HashWithIndifferentAccess.new(decoded_token[0])
|
6
|
+
end
|
7
|
+
|
8
|
+
def encode(payload)
|
9
|
+
JWT.encode(payload, secret_key, 'RS256')
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def public_key
|
15
|
+
key = File.read(ENV['JWT_SECRET_PATH'])
|
16
|
+
OpenSSL::PKey::RSA.new(key)
|
17
|
+
end
|
18
|
+
|
19
|
+
def secret_key
|
20
|
+
key = File.read(ENV['JWT_SECRET_PATH'])
|
21
|
+
OpenSSL::PKey::RSA.new(key)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
data/lib/care/rspec.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'factory_bot_rails'
|
2
|
+
require 'database_cleaner-active_record'
|
3
|
+
require 'faker'
|
4
|
+
require 'rspec-rails'
|
5
|
+
require 'shoulda-matchers'
|
6
|
+
require 'dotenv-rails'
|
7
|
+
|
8
|
+
|
9
|
+
require_relative 'support/authorization_helper'
|
10
|
+
require_relative 'support/error_collection'
|
11
|
+
require_relative 'support/factory_bot'
|
12
|
+
require_relative 'support/pagination'
|
13
|
+
require_relative 'support/parameters'
|
14
|
+
require_relative 'support/request_helper'
|
15
|
+
|