huginn_datatable 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/CHANGELOG.md +33 -0
- data/LICENSE.txt +21 -0
- data/README.md +219 -0
- data/README.pt-BR.md +219 -0
- data/lib/huginn/configuration.rb +32 -0
- data/lib/huginn/datatable/allowed_paths.rb +129 -0
- data/lib/huginn/datatable/association_path.rb +170 -0
- data/lib/huginn/datatable/datatable.rb +308 -0
- data/lib/huginn/datatable/filter_normalizer.rb +53 -0
- data/lib/huginn/datatable/paginator.rb +91 -0
- data/lib/huginn/datatable/validator.rb +134 -0
- data/lib/huginn/datatable.rb +13 -0
- data/lib/huginn/railtie.rb +12 -0
- data/lib/huginn/searchable/fuzzy.rb +88 -0
- data/lib/huginn/searchable/query.rb +129 -0
- data/lib/huginn/searchable/searchable.rb +55 -0
- data/lib/huginn/searchable.rb +10 -0
- data/lib/huginn/version.rb +5 -0
- data/lib/huginn.rb +42 -0
- data/logo.png +0 -0
- metadata +182 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Huginn
|
|
4
|
+
module Datatable
|
|
5
|
+
# Resolves and validates column references used by datatable filters,
|
|
6
|
+
# orders and ranges.
|
|
7
|
+
#
|
|
8
|
+
# A field is either a plain column ("status"), an association-scoped
|
|
9
|
+
# column ("pessoa.nome", "company.people.age") or — when the model
|
|
10
|
+
# declares `huginn_attributes` — a *public* API alias that maps to the
|
|
11
|
+
# real column.
|
|
12
|
+
#
|
|
13
|
+
# Fields are never interpolated into SQL strings: scoped fields are
|
|
14
|
+
# resolved through `AssociationPath` (direction aware FK/PK linking) and
|
|
15
|
+
# authorization is enforced against the `allowed_paths` allowlist. When
|
|
16
|
+
# the model configures `huginn_attributes(strict: true)` only the
|
|
17
|
+
# declared aliases are accepted, so the schema stays hidden from callers.
|
|
18
|
+
class Validator
|
|
19
|
+
FIELD_PATTERN = /\A[a-z_][a-z0-9_]*\z/
|
|
20
|
+
|
|
21
|
+
def self.call(model, field)
|
|
22
|
+
new(model, field)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def initialize(model, field)
|
|
26
|
+
@model = model
|
|
27
|
+
@strict_denied = false
|
|
28
|
+
@field = resolve_alias(field)
|
|
29
|
+
@field = @field.split(".").drop(1).join(".") if own_table_prefix?(@field)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def segments
|
|
33
|
+
@segments ||= @field.split(".")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def plain?
|
|
37
|
+
segments.size == 1
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def scoped?
|
|
41
|
+
!plain? && segments.size >= 2
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def valid?
|
|
45
|
+
return false if strict_denied?
|
|
46
|
+
return false unless segments.all? { |segment| segment.match?(FIELD_PATTERN) }
|
|
47
|
+
|
|
48
|
+
if plain?
|
|
49
|
+
model.columns_hash.key?(column)
|
|
50
|
+
else
|
|
51
|
+
association_path&.valid? && association_path.target_klass.columns_hash.key?(column)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Canonical chain of association reflection names (e.g. ["company",
|
|
56
|
+
# "people"]) used to match the allowlist. Table names and public aliases
|
|
57
|
+
# are normalized through the reflection.
|
|
58
|
+
def authorization_path
|
|
59
|
+
return [] if plain?
|
|
60
|
+
|
|
61
|
+
association_path&.association_names || []
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def arel_attribute
|
|
65
|
+
return nil unless valid?
|
|
66
|
+
|
|
67
|
+
plain? ? model.arel_table[column] : association_path.target_table[column]
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def column_type
|
|
71
|
+
return nil unless valid?
|
|
72
|
+
|
|
73
|
+
klass = plain? ? model : association_path.target_klass
|
|
74
|
+
klass.columns_hash[column].type
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# The AssociationPath for scoped fields (builds filter/order
|
|
78
|
+
# subqueries), or nil for plain columns.
|
|
79
|
+
def association_path
|
|
80
|
+
@association_path ||= (AssociationPath.call(model, @field) if scoped?)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# First association name of the chain (backwards compatible alias used
|
|
84
|
+
# by specs), or nil for plain columns.
|
|
85
|
+
def association_name
|
|
86
|
+
return nil if plain?
|
|
87
|
+
|
|
88
|
+
association_path&.association_names&.first&.to_sym
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def correlated_order_expression
|
|
92
|
+
association_path&.correlated_order_expression
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def allowlist_key
|
|
96
|
+
return nil if plain?
|
|
97
|
+
|
|
98
|
+
joined = authorization_path.join(".")
|
|
99
|
+
joined.empty? ? nil : joined
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
private
|
|
103
|
+
|
|
104
|
+
attr_reader :model
|
|
105
|
+
|
|
106
|
+
# "people.name" addressed via the model's own table_name is a plain
|
|
107
|
+
# column of the base relation (model.table_name is, actually, "people"
|
|
108
|
+
# for Person).
|
|
109
|
+
def own_table_prefix?(field)
|
|
110
|
+
segments = field.split(".")
|
|
111
|
+
segments.size >= 2 && segments.first == model.table_name
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def strict_denied?
|
|
115
|
+
@strict_denied
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def column
|
|
119
|
+
segments.last
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def resolve_alias(field)
|
|
123
|
+
mapping = model.respond_to?(:huginn_attribute_mapping) ? model.huginn_attribute_mapping : nil
|
|
124
|
+
return field.to_s if mapping.blank?
|
|
125
|
+
|
|
126
|
+
key = field.to_s
|
|
127
|
+
return mapping[key] if mapping.key?(key)
|
|
128
|
+
|
|
129
|
+
@strict_denied = model.huginn_strict_mapping
|
|
130
|
+
key
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Huginn
|
|
4
|
+
module Datatable
|
|
5
|
+
end
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
require_relative "datatable/association_path"
|
|
9
|
+
require_relative "datatable/allowed_paths"
|
|
10
|
+
require_relative "datatable/validator"
|
|
11
|
+
require_relative "datatable/filter_normalizer"
|
|
12
|
+
require_relative "datatable/paginator"
|
|
13
|
+
require_relative "datatable/datatable"
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Huginn
|
|
4
|
+
class Railtie < Rails::Railtie
|
|
5
|
+
initializer "huginn.configure" do |app|
|
|
6
|
+
ActiveSupport.on_load(:active_record) do
|
|
7
|
+
include Huginn::Datatable if Huginn.configuration.auto_include_datatable?
|
|
8
|
+
include Huginn::Searchable if Huginn.configuration.auto_include_searchable?
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Huginn
|
|
4
|
+
module Searchable
|
|
5
|
+
# Builds an Arel predicate for a single searchable column.
|
|
6
|
+
#
|
|
7
|
+
# Strategy chain (driven by Huginn.configuration.search_strategy):
|
|
8
|
+
#
|
|
9
|
+
# :pg_trgm -> trigram similarity OR unaccent+ILIKE (best for typos)
|
|
10
|
+
# :unaccent -> unaccent + ILIKE (case/accents insensitive)
|
|
11
|
+
# :simple -> plain LIKE
|
|
12
|
+
module Fuzzy
|
|
13
|
+
# Gentle helpers to wrap a node in UNACCENT(...).
|
|
14
|
+
module Unaccentable
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def unaccent(node)
|
|
18
|
+
Arel::Nodes::NamedFunction.new("UNACCENT", [node])
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
class Simple
|
|
23
|
+
def initialize(column, value)
|
|
24
|
+
@column = column
|
|
25
|
+
@value = value
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def predicate
|
|
29
|
+
@column.matches("%#{escape_like(@value)}%")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def escape_like(value)
|
|
35
|
+
value.to_s.gsub(/[\\%_]/) { |char| "\\#{char}" }
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
class Unaccent
|
|
40
|
+
include Unaccentable
|
|
41
|
+
|
|
42
|
+
def initialize(column, value)
|
|
43
|
+
@column = column
|
|
44
|
+
@value = value
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def predicate
|
|
48
|
+
Arel::Nodes::InfixOperation.new(
|
|
49
|
+
"ILIKE",
|
|
50
|
+
unaccent(@column),
|
|
51
|
+
unaccent(Arel::Nodes.build_quoted("%#{escape_like(@value)}%", @column))
|
|
52
|
+
)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def escape_like(value)
|
|
58
|
+
value.to_s.gsub(/[\\%_]/) { |char| "\\#{char}" }
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
class Trigram
|
|
63
|
+
include Unaccentable
|
|
64
|
+
|
|
65
|
+
def initialize(column, value, threshold)
|
|
66
|
+
@column = column
|
|
67
|
+
@value = value
|
|
68
|
+
@threshold = threshold
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def predicate
|
|
72
|
+
similarity = Arel::Nodes::NamedFunction.new("similarity", [
|
|
73
|
+
unaccent(@column),
|
|
74
|
+
unaccent(Arel::Nodes.build_quoted(@value, @column))
|
|
75
|
+
])
|
|
76
|
+
threshold = Arel::Nodes.build_quoted(@threshold)
|
|
77
|
+
Arel::Nodes::InfixOperation.new(">", similarity, threshold).or(fallback)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
def fallback
|
|
83
|
+
Unaccent.new(@column, @value).predicate
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Huginn
|
|
4
|
+
module Searchable
|
|
5
|
+
# Builds a tolerant full-text search relation over a model's searchable
|
|
6
|
+
# columns, optionally crossing associations through left_joins.
|
|
7
|
+
#
|
|
8
|
+
# All matched columns are fused into a single Arel OR predicate, so the
|
|
9
|
+
# whole search stays one query regardless of how many columns (or
|
|
10
|
+
# associations) are involved.
|
|
11
|
+
class Query
|
|
12
|
+
def self.call(model, value, options = {})
|
|
13
|
+
new(model, value, options).call
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def initialize(model, value, options = {})
|
|
17
|
+
@model = model
|
|
18
|
+
@value = value.to_s
|
|
19
|
+
@options = options
|
|
20
|
+
@distinct = options.fetch(:distinct, true)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def call
|
|
24
|
+
relation = @model.all
|
|
25
|
+
return apply_distinct(relation) if @value.strip.empty?
|
|
26
|
+
|
|
27
|
+
resolution = resolve_columns
|
|
28
|
+
return apply_distinct(relation) if resolution.fetch(:attrs).empty?
|
|
29
|
+
|
|
30
|
+
joins = resolution.fetch(:joins)
|
|
31
|
+
relation = relation.left_joins(*joins) if joins.any?
|
|
32
|
+
|
|
33
|
+
predicate = resolution.fetch(:attrs).map { |attr| fuzzy_predicate(attr) }.reduce(&:or)
|
|
34
|
+
apply_distinct(relation.where(predicate))
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def resolve_columns
|
|
40
|
+
columns = @model.searchable_columns_config_resolved
|
|
41
|
+
joins = []
|
|
42
|
+
attrs = []
|
|
43
|
+
|
|
44
|
+
Array(columns[:columns]).map(&:to_s).reject(&:blank?).each do |field|
|
|
45
|
+
if field.include?(".")
|
|
46
|
+
name, col = field.split(".", 2)
|
|
47
|
+
push_association(joins, attrs, name, col)
|
|
48
|
+
else
|
|
49
|
+
attrs << @model.arel_table[field] if @model.columns_hash.key?(field)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
columns[:associations].each do |name, cols|
|
|
54
|
+
Array(cols).each { |col| push_association(joins, attrs, name.to_s, col.to_s) }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
{ joins: joins.uniq, attrs: attrs }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def push_association(joins, attrs, name, col)
|
|
61
|
+
association = @model.reflect_on_association(name.to_sym)
|
|
62
|
+
return unless association
|
|
63
|
+
|
|
64
|
+
table = association.klass.arel_table
|
|
65
|
+
attrs << table[col] if association.klass.columns_hash.key?(col)
|
|
66
|
+
joins << name.to_sym unless joins.include?(name.to_sym)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def fuzzy_predicate(attr)
|
|
70
|
+
case strategy
|
|
71
|
+
when :pg_trgm
|
|
72
|
+
if pg_trgm_available?
|
|
73
|
+
Fuzzy::Trigram.new(attr, @value, Huginn.configuration.fuzzy_threshold).predicate
|
|
74
|
+
else
|
|
75
|
+
fallback_fuzzy(attr)
|
|
76
|
+
end
|
|
77
|
+
when :unaccent
|
|
78
|
+
Fuzzy::Unaccent.new(attr, @value).predicate
|
|
79
|
+
else
|
|
80
|
+
Fuzzy::Simple.new(attr, @value).predicate
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Even with :pg_trgm configured, degrade gracefully to accent/case
|
|
85
|
+
# insensitive matching when the extension is not installed.
|
|
86
|
+
def fallback_fuzzy(attr)
|
|
87
|
+
if unaccent_available?
|
|
88
|
+
Fuzzy::Unaccent.new(attr, @value).predicate
|
|
89
|
+
else
|
|
90
|
+
Fuzzy::Simple.new(attr, @value).predicate
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def strategy
|
|
95
|
+
Huginn.configuration.search_strategy
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def pg_trgm_available?
|
|
99
|
+
return false unless postgresql?
|
|
100
|
+
@pg_trgm_available ||= extension_installed?("pg_trgm")
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def unaccent_available?
|
|
104
|
+
return false unless postgresql?
|
|
105
|
+
@unaccent_available ||= extension_installed?("unaccent")
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def extension_installed?(name)
|
|
109
|
+
@model.connection
|
|
110
|
+
.select_all(sanitize_extension_query(name))
|
|
111
|
+
.any?
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def sanitize_extension_query(name)
|
|
115
|
+
ActiveRecord::Base.sanitize_sql_array(
|
|
116
|
+
["SELECT 1 AS one FROM pg_extension WHERE extname = ?", name]
|
|
117
|
+
)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def postgresql?
|
|
121
|
+
@model.connection.adapter_name.downcase.include?("postgres")
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def apply_distinct(relation)
|
|
125
|
+
@distinct ? relation.distinct : relation
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Huginn
|
|
4
|
+
module Searchable
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
included do
|
|
8
|
+
class_attribute :searchable_columns_config, default: nil
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
class_methods do
|
|
12
|
+
# Declares which columns (own or association-scoped) are searched.
|
|
13
|
+
#
|
|
14
|
+
# searchable_columns :name, :document
|
|
15
|
+
# searchable_columns :name, person: [:name, :email]
|
|
16
|
+
# searchable_columns "person.name"
|
|
17
|
+
#
|
|
18
|
+
# When nothing is declared, every :string/:text column of the model is
|
|
19
|
+
# searched automatically. Columns may be association-scoped ("person.name")
|
|
20
|
+
# or declared via keyword arguments; scoped columns are reached through a
|
|
21
|
+
# left_join.
|
|
22
|
+
def searchable_columns(*columns, **associations)
|
|
23
|
+
self.searchable_columns_config = {
|
|
24
|
+
columns: columns.flatten.compact.map(&:to_s),
|
|
25
|
+
associations: associations
|
|
26
|
+
}
|
|
27
|
+
searchable_columns_config
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Tolerant full-text search (pg_trgm / unaccent / LIKE chain).
|
|
31
|
+
#
|
|
32
|
+
# @param value [String] the raw search term from the frontend
|
|
33
|
+
# @param options [Hash] :distinct (default true), :columns override
|
|
34
|
+
def search(value, options = {})
|
|
35
|
+
Searchable::Query.call(self, value, options)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def searchable_columns_config_resolved
|
|
39
|
+
config = searchable_columns_config || {}
|
|
40
|
+
{
|
|
41
|
+
columns: config[:columns].presence || default_searchable_columns,
|
|
42
|
+
associations: config[:associations] || {}
|
|
43
|
+
}
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def default_searchable_columns
|
|
49
|
+
columns_hash.each_with_object([]) do |(name, column), acc|
|
|
50
|
+
acc << name if %i[string text].include?(column.type)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
data/lib/huginn.rb
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support"
|
|
4
|
+
require "active_support/concern"
|
|
5
|
+
require "active_support/notifications"
|
|
6
|
+
require "active_record"
|
|
7
|
+
|
|
8
|
+
require "logger"
|
|
9
|
+
|
|
10
|
+
module Huginn
|
|
11
|
+
NAMESPACE = "huginn"
|
|
12
|
+
|
|
13
|
+
class << self
|
|
14
|
+
def configuration
|
|
15
|
+
@configuration ||= Configuration.new
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def configure
|
|
19
|
+
yield(configuration)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def instrument(event, payload = {})
|
|
23
|
+
ActiveSupport::Notifications.instrument("#{event}.#{NAMESPACE}", payload) do
|
|
24
|
+
yield if block_given?
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def logger
|
|
29
|
+
@logger ||= if defined?(Rails) && Rails.respond_to?(:logger)
|
|
30
|
+
Rails.logger
|
|
31
|
+
else
|
|
32
|
+
::Logger.new($stdout)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
require_relative "huginn/version"
|
|
39
|
+
require_relative "huginn/configuration"
|
|
40
|
+
require_relative "huginn/datatable"
|
|
41
|
+
require_relative "huginn/searchable"
|
|
42
|
+
require_relative "huginn/railtie" if defined?(Rails::Railtie)
|
data/logo.png
ADDED
|
Binary file
|
metadata
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: huginn_datatable
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Kayky Marcelo
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: activesupport
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '7.1'
|
|
19
|
+
- - "<"
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: '9'
|
|
22
|
+
type: :runtime
|
|
23
|
+
prerelease: false
|
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
25
|
+
requirements:
|
|
26
|
+
- - ">="
|
|
27
|
+
- !ruby/object:Gem::Version
|
|
28
|
+
version: '7.1'
|
|
29
|
+
- - "<"
|
|
30
|
+
- !ruby/object:Gem::Version
|
|
31
|
+
version: '9'
|
|
32
|
+
- !ruby/object:Gem::Dependency
|
|
33
|
+
name: activerecord
|
|
34
|
+
requirement: !ruby/object:Gem::Requirement
|
|
35
|
+
requirements:
|
|
36
|
+
- - ">="
|
|
37
|
+
- !ruby/object:Gem::Version
|
|
38
|
+
version: '7.1'
|
|
39
|
+
- - "<"
|
|
40
|
+
- !ruby/object:Gem::Version
|
|
41
|
+
version: '9'
|
|
42
|
+
type: :runtime
|
|
43
|
+
prerelease: false
|
|
44
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
45
|
+
requirements:
|
|
46
|
+
- - ">="
|
|
47
|
+
- !ruby/object:Gem::Version
|
|
48
|
+
version: '7.1'
|
|
49
|
+
- - "<"
|
|
50
|
+
- !ruby/object:Gem::Version
|
|
51
|
+
version: '9'
|
|
52
|
+
- !ruby/object:Gem::Dependency
|
|
53
|
+
name: actionpack
|
|
54
|
+
requirement: !ruby/object:Gem::Requirement
|
|
55
|
+
requirements:
|
|
56
|
+
- - ">="
|
|
57
|
+
- !ruby/object:Gem::Version
|
|
58
|
+
version: '7.1'
|
|
59
|
+
- - "<"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '9'
|
|
62
|
+
type: :runtime
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - ">="
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '7.1'
|
|
69
|
+
- - "<"
|
|
70
|
+
- !ruby/object:Gem::Version
|
|
71
|
+
version: '9'
|
|
72
|
+
- !ruby/object:Gem::Dependency
|
|
73
|
+
name: pagy
|
|
74
|
+
requirement: !ruby/object:Gem::Requirement
|
|
75
|
+
requirements:
|
|
76
|
+
- - ">="
|
|
77
|
+
- !ruby/object:Gem::Version
|
|
78
|
+
version: '6.0'
|
|
79
|
+
type: :runtime
|
|
80
|
+
prerelease: false
|
|
81
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
82
|
+
requirements:
|
|
83
|
+
- - ">="
|
|
84
|
+
- !ruby/object:Gem::Version
|
|
85
|
+
version: '6.0'
|
|
86
|
+
- !ruby/object:Gem::Dependency
|
|
87
|
+
name: rspec
|
|
88
|
+
requirement: !ruby/object:Gem::Requirement
|
|
89
|
+
requirements:
|
|
90
|
+
- - "~>"
|
|
91
|
+
- !ruby/object:Gem::Version
|
|
92
|
+
version: '3.12'
|
|
93
|
+
type: :development
|
|
94
|
+
prerelease: false
|
|
95
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
96
|
+
requirements:
|
|
97
|
+
- - "~>"
|
|
98
|
+
- !ruby/object:Gem::Version
|
|
99
|
+
version: '3.12'
|
|
100
|
+
- !ruby/object:Gem::Dependency
|
|
101
|
+
name: pry
|
|
102
|
+
requirement: !ruby/object:Gem::Requirement
|
|
103
|
+
requirements:
|
|
104
|
+
- - ">="
|
|
105
|
+
- !ruby/object:Gem::Version
|
|
106
|
+
version: '0'
|
|
107
|
+
type: :development
|
|
108
|
+
prerelease: false
|
|
109
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
110
|
+
requirements:
|
|
111
|
+
- - ">="
|
|
112
|
+
- !ruby/object:Gem::Version
|
|
113
|
+
version: '0'
|
|
114
|
+
- !ruby/object:Gem::Dependency
|
|
115
|
+
name: appraisal
|
|
116
|
+
requirement: !ruby/object:Gem::Requirement
|
|
117
|
+
requirements:
|
|
118
|
+
- - ">="
|
|
119
|
+
- !ruby/object:Gem::Version
|
|
120
|
+
version: '0'
|
|
121
|
+
type: :development
|
|
122
|
+
prerelease: false
|
|
123
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
124
|
+
requirements:
|
|
125
|
+
- - ">="
|
|
126
|
+
- !ruby/object:Gem::Version
|
|
127
|
+
version: '0'
|
|
128
|
+
description: 'Huginn is the raven of Odin that represents thought and remembrance.
|
|
129
|
+
It offers a lightweight, highly performant query layer for ActiveRecord datatables:
|
|
130
|
+
filtered/ordered eager-loaded pagination with an enxuto count and a PostgreSQL fuzzy-search
|
|
131
|
+
builder (pg_trgm, unaccent, ILIKE).'
|
|
132
|
+
email:
|
|
133
|
+
- kaykymarcelo2411@gmail.com
|
|
134
|
+
executables: []
|
|
135
|
+
extensions: []
|
|
136
|
+
extra_rdoc_files: []
|
|
137
|
+
files:
|
|
138
|
+
- CHANGELOG.md
|
|
139
|
+
- LICENSE.txt
|
|
140
|
+
- README.md
|
|
141
|
+
- README.pt-BR.md
|
|
142
|
+
- lib/huginn.rb
|
|
143
|
+
- lib/huginn/configuration.rb
|
|
144
|
+
- lib/huginn/datatable.rb
|
|
145
|
+
- lib/huginn/datatable/allowed_paths.rb
|
|
146
|
+
- lib/huginn/datatable/association_path.rb
|
|
147
|
+
- lib/huginn/datatable/datatable.rb
|
|
148
|
+
- lib/huginn/datatable/filter_normalizer.rb
|
|
149
|
+
- lib/huginn/datatable/paginator.rb
|
|
150
|
+
- lib/huginn/datatable/validator.rb
|
|
151
|
+
- lib/huginn/railtie.rb
|
|
152
|
+
- lib/huginn/searchable.rb
|
|
153
|
+
- lib/huginn/searchable/fuzzy.rb
|
|
154
|
+
- lib/huginn/searchable/query.rb
|
|
155
|
+
- lib/huginn/searchable/searchable.rb
|
|
156
|
+
- lib/huginn/version.rb
|
|
157
|
+
- logo.png
|
|
158
|
+
homepage: https://github.com/KaykyM2411/huginn
|
|
159
|
+
licenses:
|
|
160
|
+
- MIT
|
|
161
|
+
metadata:
|
|
162
|
+
homepage_uri: https://github.com/KaykyM2411/huginn
|
|
163
|
+
source_code_uri: https://github.com/KaykyM2411/huginn
|
|
164
|
+
changelog_uri: https://github.com/KaykyM2411/huginn/blob/main/CHANGELOG.md
|
|
165
|
+
rdoc_options: []
|
|
166
|
+
require_paths:
|
|
167
|
+
- lib
|
|
168
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
169
|
+
requirements:
|
|
170
|
+
- - ">="
|
|
171
|
+
- !ruby/object:Gem::Version
|
|
172
|
+
version: '3.0'
|
|
173
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
174
|
+
requirements:
|
|
175
|
+
- - ">="
|
|
176
|
+
- !ruby/object:Gem::Version
|
|
177
|
+
version: '0'
|
|
178
|
+
requirements: []
|
|
179
|
+
rubygems_version: 4.0.6
|
|
180
|
+
specification_version: 4
|
|
181
|
+
summary: Performant and elegant ActiveRecord datatables/data-grids with tolerant search
|
|
182
|
+
test_files: []
|