activekit 0.5.0.dev8 → 0.5.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7fe66b1b57a945fa39f8d3daad8daa1dd3d564d190fb302453e1e1d9b4bc64e8
4
- data.tar.gz: '0026541966d64d4fb509ae32dbd69715cf86d8dd79abd51537aee335bb0da393'
3
+ metadata.gz: 71b9eb7bd5fc27d2794cd15b8ec08a68e9baae06e37e072188260ddbaaa25473
4
+ data.tar.gz: 33f906a96b417c86b8048a2f663dbac5d4a6b0595f9cea23b6a0db92f98982cb
5
5
  SHA512:
6
- metadata.gz: ebc4d83438c610266f4de8c72395199f71282f5e8b00cfdebcf9e532950094997f520b0dd3e65a85e05f5e62fae365e5eaebc770691c4fcb2131a0ce3225bbc1
7
- data.tar.gz: bbb51f533e437c2de942dcc5a8d9120b3f1cf0317b138c0b924cd433a48bbe6c145b4c77e3eaf0160c2a37d47d0d35df07916a090fbd237bc4929c7de8b0d790
6
+ metadata.gz: 5172f9cb6b0aa0623a9e6f9a0263de7a5fadc016a25c6b92514f9916fca13653f8326479f4bc7fa8c54314c77c8d9d75435034b3c0e1e40054f7f002bf969d90
7
+ data.tar.gz: 65cb7a16747fb2281042dced7e1781bf0709437c470e8621b7ee232b6b205ab1ad021d8b1287a147abba85ddb96fe4f020e1ebb455b00a532d45488fbdf6d47f
data/README.md CHANGED
@@ -3,6 +3,46 @@ Add the essential kit for rails ActiveRecord models and be happy.
3
3
 
4
4
  ## Usage
5
5
 
6
+ ### Search Attribute
7
+
8
+ Add searching to your ActiveRecord models.
9
+ Search Attribute provides full searching functionality for your model database records using redis search including search suggestions.
10
+
11
+ You can define any number of model attributes in one model to search together.
12
+
13
+ Define the search attributes in accordance with the column name in your model like below.
14
+ ```ruby
15
+ class Product < ApplicationRecord
16
+ search_attribute :name, type: :text
17
+ search_attribute :permalink, type: :tag
18
+ search_attribute :short_description, type: :text
19
+ search_attribute :published, type: :tag, sortable: true
20
+ end
21
+ ```
22
+
23
+ You can also define a search_describer to describe the details of the search instead of using the defaults.
24
+ ```ruby
25
+ class Product < ApplicationRecord
26
+ # search_describer method_name, database: -> { ActiveRecord::Base.connection_db_config.database }
27
+ search_describer :limit_by_search, database: -> { System::Current.tenant.database }
28
+ search_attribute :name, type: :text
29
+ search_attribute :permalink, type: :tag
30
+ search_attribute :short_description, type: :text
31
+ search_attribute :published, type: :tag, sortable: true
32
+ end
33
+ ```
34
+
35
+ The following class methods will be added to your model class to use in accordance with details provided for search_describer:
36
+ ```ruby
37
+ Product.limit_by_search(term: "term", tags: { published: true }, order: "name asc", page: 1)
38
+ Product.searcher.for(:limit_by_search).current_page
39
+ Product.searcher.for(:limit_by_search).previous_page?
40
+ Product.searcher.for(:limit_by_search).previous_page
41
+ Product.searcher.for(:limit_by_search).next_page?
42
+ Product.searcher.for(:limit_by_search).next_page
43
+ Product.searcher.for(:limit_by_search).suggestions(prefix: "prefix_term").keys
44
+ ```
45
+
6
46
  ### Export Attribute
7
47
 
8
48
  Add exporting to your ActiveRecord models.
@@ -23,8 +63,8 @@ end
23
63
  You can also define an export_describer to describe the details of the export instead of using the defaults.
24
64
  ```ruby
25
65
  class Product < ApplicationRecord
26
- # export_describer method_name, kind: :csv, database: -> { ActiveRecord::Base.connection_db_config.database.to_sym }
27
- export_describer :to_csv, kind: :csv, database: -> { System::Current.tenant.database.to_sym }
66
+ # export_describer method_name, kind: :csv, database: -> { ActiveRecord::Base.connection_db_config.database }
67
+ export_describer :to_csv, kind: :csv, database: -> { System::Current.tenant.database }
28
68
  export_attribute :name
29
69
  export_attribute :sku, heading: "SKU No."
30
70
  export_attribute :image_name, value: lambda { |record| record.image&.name }, includes: :image
@@ -8,12 +8,12 @@ module ActiveKit
8
8
 
9
9
  base.module_eval <<-CODE, __FILE__, __LINE__ + 1
10
10
  module ClassMethods
11
- private
12
-
13
11
  def #{current_component}er
14
12
  @#{current_component}er ||= ActiveKit::#{current_component.to_s.titleize}::#{current_component.to_s.titleize}er.new(current_component: :#{current_component}, current_class: self)
15
13
  end
16
14
 
15
+ private
16
+
17
17
  def #{current_component}_describer(name, **options)
18
18
  #{current_component}er.create_describer(name, options)
19
19
  end
@@ -21,10 +21,6 @@ module ActiveKit
21
21
  def #{current_component}_attribute(name, **options)
22
22
  #{current_component}er.create_attribute(name, options)
23
23
  end
24
-
25
- def #{current_component}_describer_method(describer)
26
- raise NotImplementedError
27
- end
28
24
  end
29
25
  CODE
30
26
  end
@@ -15,10 +15,8 @@ module ActiveKit
15
15
  options.store(:attributes, {})
16
16
  @describers.store(name, options)
17
17
  @current_class.class_eval <<-CODE, __FILE__, __LINE__ + 1
18
- def self.#{name}
19
- if describer = #{@current_component}er.find_describer_by(name: "#{name}".to_sym)
20
- #{@current_component}_describer_method(describer)
21
- end
18
+ def self.#{name}(**params)
19
+ #{@current_component}er.run_describer_method("#{name}", params)
22
20
  end
23
21
  CODE
24
22
  end
@@ -37,6 +35,45 @@ module ActiveKit
37
35
  describer_options[:attributes].store(name, options)
38
36
  end
39
37
  end
38
+
39
+ describer_names
40
+ end
41
+
42
+ def run_describer_method(describer_name, params)
43
+ raise "Could not find describer while creating describer method." unless describer = find_describer_by(name: describer_name.to_sym)
44
+ describer_method(describer, params)
45
+ end
46
+
47
+ def describer_method(describer, params)
48
+ raise NotImplementedError
49
+ end
50
+
51
+ def for(describer_name)
52
+ describer_name = @describers.keys[0] if describer_name.nil?
53
+ raise "Could not find any describer name in #{@current_class.name}." if describer_name.blank?
54
+
55
+ describer_name = describer_name.to_sym
56
+ raise "Could not find describer '#{describer_name}' in #{@current_class.name}." unless @describers.dig(describer_name)
57
+ componenting = @describers.dig(describer_name, :componenting)
58
+ return componenting if componenting
59
+
60
+ @describers[describer_name][:componenting] = "ActiveKit::#{@current_component.to_s.titleize}::#{@current_component.to_s.titleize}ing".constantize.new(describer: find_describer_by(name: describer_name), current_class: @current_class)
61
+ @describers[describer_name][:componenting]
62
+ end
63
+
64
+ def get_describer_names
65
+ @describers.keys.map(&:to_s)
66
+ end
67
+
68
+ private
69
+
70
+ def create_default_describer
71
+ case @current_component
72
+ when :export
73
+ create_describer(:to_csv, kind: :csv, database: -> { ActiveRecord::Base.connection_db_config.database })
74
+ when :search
75
+ create_describer(:limit_by_search, database: -> { ActiveRecord::Base.connection_db_config.database })
76
+ end
40
77
  end
41
78
 
42
79
  def find_describer_by(name:)
@@ -57,8 +94,6 @@ module ActiveKit
57
94
  OpenStruct.new(hash)
58
95
  end
59
96
 
60
- private
61
-
62
97
  def build_describer_fields(describer_attributes)
63
98
  describer_attributes.inject({}) do |fields_hash, (name, options)|
64
99
  enclosed_attributes = Array(options.dig(:attributes))
@@ -108,13 +143,6 @@ module ActiveKit
108
143
  def get_heading(options_heading)
109
144
  options_heading.is_a?(Proc) ? options_heading.call(@current_class) : options_heading
110
145
  end
111
-
112
- def create_default_describer
113
- case @current_component
114
- when :export
115
- create_describer(:to_csv, kind: :csv, database: -> { ActiveRecord::Base.connection_db_config.database.to_sym })
116
- end
117
- end
118
146
  end
119
147
  end
120
148
  end
@@ -0,0 +1,10 @@
1
+ module ActiveKit
2
+ module Bedrock
3
+ class Bedrocking
4
+ def initialize(describer:, current_class:)
5
+ @describer = describer
6
+ @current_class = current_class
7
+ end
8
+ end
9
+ end
10
+ end
@@ -3,5 +3,6 @@ module ActiveKit
3
3
  extend ActiveSupport::Autoload
4
4
 
5
5
  autoload :Bedrocker
6
+ autoload :Bedrocking
6
7
  end
7
8
  end
@@ -10,30 +10,6 @@ module ActiveKit
10
10
  end
11
11
 
12
12
  class_methods do
13
- def export_describer_method(describer)
14
- case describer.kind
15
- when :csv
16
- # The 'all' relation must be captured outside the Enumerator,
17
- # else it will get reset to all the records of the class.
18
- all_activerecord_relation = all.includes(describer.includes)
19
-
20
- Enumerator.new do |yielder|
21
- ActiveRecord::Base.connected_to(role: :writing, shard: describer.database.call) do
22
- exporting = exporter.new_exporting(describer: describer)
23
-
24
- # Add the headings.
25
- yielder << CSV.generate_line(exporting.headings) if exporting.headings?
26
-
27
- # Add the values.
28
- # find_each will ignore any order if set earlier.
29
- all_activerecord_relation.find_each do |record|
30
- lines = exporting.lines_for(record: record)
31
- lines.each { |line| yielder << CSV.generate_line(line) }
32
- end
33
- end
34
- end
35
- end
36
- end
37
13
  end
38
14
  end
39
15
  end
@@ -1,8 +1,29 @@
1
1
  module ActiveKit
2
2
  module Export
3
3
  class Exporter < Bedrock::Bedrocker
4
- def new_exporting(describer:)
5
- Exporting.new(describer: describer)
4
+ def describer_method(describer, params)
5
+ case describer.kind
6
+ when :csv
7
+ # The 'all' relation must be captured outside the Enumerator,
8
+ # else it will get reset to all the records of the class.
9
+ all_activerecord_relation = @current_class.all.includes(describer.includes)
10
+
11
+ Enumerator.new do |yielder|
12
+ ActiveRecord::Base.connected_to(role: :writing, shard: describer.database.call.to_sym) do
13
+ exporting = self.for(describer.name)
14
+
15
+ # Add the headings.
16
+ yielder << CSV.generate_line(exporting.headings) if exporting.headings?
17
+
18
+ # Add the values.
19
+ # find_each will ignore any order if set earlier.
20
+ all_activerecord_relation.find_each do |record|
21
+ lines = exporting.lines_for(record: record)
22
+ lines.each { |line| yielder << CSV.generate_line(line) }
23
+ end
24
+ end
25
+ end
26
+ end
6
27
  end
7
28
  end
8
29
  end
@@ -1,11 +1,6 @@
1
1
  module ActiveKit
2
2
  module Export
3
- class Exporting
4
-
5
- def initialize(describer:)
6
- @describer = describer
7
- end
8
-
3
+ class Exporting < Bedrock::Bedrocking
9
4
  def headings
10
5
  @headings ||= @describer.fields.keys.flatten
11
6
  end
@@ -3,19 +3,21 @@ module ActiveKit
3
3
  class Index
4
4
  attr_reader :prefix, :schema, :attribute_value_parser
5
5
 
6
- def initialize(current_class:)
6
+ def initialize(current_class:, describer:)
7
7
  @redis = ActiveKit::Search.redis
8
8
  @current_class = current_class
9
+ @describer = describer
9
10
 
10
11
  current_class_name = current_class.to_s.parameterize.pluralize
11
- @name = "activekit:search:index:#{current_class_name}"
12
- @prefix = "activekit:search:#{current_class_name}"
12
+ describer_name = describer.name.to_s
13
+ @name = "activekit:search:index:#{current_class_name}:#{describer_name}"
14
+ @prefix = "activekit:search:#{current_class_name}:#{describer_name}"
13
15
  @schema = {}
14
16
  @attribute_value_parser = {}
15
17
  end
16
18
 
17
19
  def add_attribute_to_schema(name:, options:)
18
- raise "Error: No type specified for the search attribute #{name}." unless options[:type].present?
20
+ raise "Error: No type specified for the search attribute '#{name}'." unless options[:type].present?
19
21
 
20
22
  attribute_schema = []
21
23
 
@@ -137,7 +139,7 @@ module ActiveKit
137
139
  end
138
140
  end
139
141
 
140
- query = "@database:{#{escape_separators(System::Current.tenant.database, include_space: true)}}#{term}#{tags}#{modifiers}"
142
+ query = "@database:{#{escape_separators(@describer.database.call, include_space: true)}}#{term}#{tags}#{modifiers}"
141
143
  command = [
142
144
  "FT.SEARCH",
143
145
  @name,
@@ -1,16 +1,17 @@
1
1
  module ActiveKit
2
2
  module Search
3
3
  class Key
4
- def initialize(index:)
4
+ def initialize(index:, describer:)
5
5
  @redis = ActiveKit::Search.redis
6
6
  @index = index
7
+ @describer = describer
7
8
  end
8
9
 
9
10
  def reload(record:)
10
11
  clear(record: record)
11
12
 
12
13
  hash_key = key(record: record)
13
- hash_value = { "database" => System::Current.tenant.database, "id" => record.id }
14
+ hash_value = { "database" => @describer.database.call, "id" => record.id }
14
15
  @index.schema.each do |field_name, field_value|
15
16
  attribute_name = field_name
16
17
  attribute_value = @index.attribute_value_parser[field_name]&.call(record) || record.public_send(field_name)
@@ -37,7 +38,7 @@ module ActiveKit
37
38
  private
38
39
 
39
40
  def key(record:)
40
- "#{@index.prefix}:#{System::Current.tenant.database}:#{record.id}"
41
+ "#{@index.prefix}:#{@describer.database.call}:#{record.id}"
41
42
  end
42
43
  end
43
44
  end
@@ -0,0 +1,44 @@
1
+ module ActiveKit
2
+ module Search
3
+ class Manager
4
+ def initialize(given_class:, given_describer:)
5
+ @given_class = given_class
6
+ @given_describer = given_describer
7
+
8
+ Rails.application.eager_load!
9
+ end
10
+
11
+ def reload
12
+ task(name: :reload, log_name: "Reloading")
13
+ end
14
+
15
+ def clear
16
+ task(name: :clear, log_name: "Clearing")
17
+ end
18
+
19
+ def drop
20
+ task(name: :drop, log_name: "Dropping")
21
+ end
22
+
23
+ private
24
+
25
+ def task(name:, log_name: "Reloading")
26
+ models = @given_class.present? ? [@given_class] : ActiveRecord::Base.descendants.collect(&:name)
27
+ # Removing these models for efficiency as they will never contain searcher.
28
+ models -= ["ApplicationRecord", "ActionText::Record", "ActionText::RichText", "ActiveKit::ApplicationRecord", "ActionMailbox::Record", "ActionMailbox::InboundEmail", "ActiveStorage::Record", "ActiveStorage::Blob", "ActiveStorage::VariantRecord", "ActiveStorage::Attachment"]
29
+ models.each do |model|
30
+ model_const = model.constantize
31
+ if model_const.try(:searcher)
32
+ describer_names = @given_describer.present? ? [@given_describer] : model_const.searcher.get_describer_names
33
+ describer_names.each do |describer_name|
34
+ if model_const.searcher.for(describer_name).attributes_present?
35
+ puts "ActiveKit::Search | #{log_name}: #{model}"
36
+ model_const.searcher.for(describer_name).public_send(name)
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -3,124 +3,14 @@ require 'active_support/concern'
3
3
  module ActiveKit
4
4
  module Search
5
5
  module Searchable
6
+ extend Bedrock::Bedrockable
6
7
  extend ActiveSupport::Concern
7
8
 
8
9
  included do
9
10
  end
10
11
 
11
12
  class_methods do
12
- def searcher
13
- @searcher ||= ActiveKit::Search::Search.new(current_class: self)
14
- end
15
-
16
- def searching(term: nil, **options)
17
- options[:page] = 1 if options.key?(:page) && options[:page].blank?
18
- searcher.fetch(term: term, **options).records
19
- end
20
-
21
- def search_attribute(name, **options)
22
- options.deep_symbolize_keys!
23
-
24
- set_activekit_search_callbacks unless searcher.attributes_present?
25
- depends_on = options.delete(:depends_on) || {}
26
- set_activekit_search_depends_on_callbacks(depends_on: depends_on) unless depends_on.empty?
27
-
28
- searcher.add_attribute(name: name, options: options)
29
- end
30
-
31
- def set_activekit_search_callbacks
32
- after_commit do
33
- self.class.searcher.reload(record: self)
34
- logger.info "ActiveKit::Search | Indexing from #{self.class.name}: Done."
35
- end
36
- end
37
-
38
- def set_activekit_search_depends_on_callbacks(depends_on:)
39
- depends_on.each do |depends_on_association, depends_on_inverse|
40
- klass = self.reflect_on_all_associations.map { |assoc| [assoc.name, assoc.klass.name] }.to_h[depends_on_association]
41
- klass.constantize.class_eval do
42
- after_commit do
43
- inverse_assoc = self.public_send(depends_on_inverse)
44
- if inverse_assoc.respond_to?(:each)
45
- inverse_assoc.each { |instance| instance.class.searcher.reload(record: instance) }
46
- else
47
- inverse_assoc.class.searcher.reload(record: inverse_assoc)
48
- end
49
- logger.info "ActiveKit::Search | Indexing from #{self.class.name}: Done."
50
- end
51
- end
52
- end
53
- end
54
13
  end
55
14
  end
56
15
  end
57
16
  end
58
-
59
-
60
-
61
-
62
- # require 'active_support/concern'
63
-
64
- # module ActiveKit
65
- # module Search
66
- # module Searchable
67
- # extend ActiveSupport::Concern
68
-
69
- # included do
70
- # end
71
-
72
- # class_methods do
73
- # def searcher
74
- # @searcher ||= ActiveKit::Search::Searcher.new(current_class: self)
75
- # end
76
-
77
- # def search_describer(name, **options)
78
- # name = name.to_sym
79
- # options.deep_symbolize_keys!
80
-
81
- # unless searcher.find_describer_by(describer_name: name)
82
- # searcher.new_describer(name: name, options: options)
83
- # define_search_describer_method(kind: options[:kind], name: name)
84
- # end
85
- # end
86
-
87
- # def search_attribute(name, **options)
88
- # search_describer(:to_csv, kind: :csv, database: -> { ActiveRecord::Base.connection_db_config.database.to_sym }) unless searcher.describers?
89
-
90
- # options.deep_symbolize_keys!
91
- # searcher.new_attribute(name: name.to_sym, options: options)
92
- # end
93
-
94
- # def define_search_describer_method(kind:, name:)
95
- # case kind
96
- # when :csv
97
- # define_singleton_method name do
98
- # describer = exporter.find_describer_by(describer_name: name)
99
- # raise "could not find describer for the describer name '#{name}'" unless describer.present?
100
-
101
- # # The 'all' relation must be captured outside the Enumerator,
102
- # # else it will get reset to all the records of the class.
103
- # all_activerecord_relation = all.includes(describer.includes)
104
-
105
- # Enumerator.new do |yielder|
106
- # ActiveRecord::Base.connected_to(role: :writing, shard: describer.database.call) do
107
- # exporting = exporter.new_exporting(describer: describer)
108
-
109
- # # Add the headings.
110
- # yielder << CSV.generate_line(exporting.headings) if exporting.headings?
111
-
112
- # # Add the values.
113
- # # find_each will ignore any order if set earlier.
114
- # all_activerecord_relation.find_each do |record|
115
- # lines = exporting.lines_for(record: record)
116
- # lines.each { |line| yielder << CSV.generate_line(line) }
117
- # end
118
- # end
119
- # end
120
- # end
121
- # end
122
- # end
123
- # end
124
- # end
125
- # end
126
- # end
@@ -1,106 +1,51 @@
1
- # module ActiveKit
2
- # module Search
3
- # class Searcher
4
- # attr_reader :describers
5
-
6
- # def initialize(current_class:)
7
- # @current_class = current_class
8
- # @describers = {}
9
- # end
10
-
11
- # def find_describer_by(describer_name:)
12
- # describer_options = @describers.dig(describer_name)
13
- # return nil unless describer_options.present?
14
-
15
- # describer_attributes = describer_options[:attributes]
16
- # includes = describer_attributes.values.map { |options| options.dig(:includes) }.compact.flatten(1).uniq
17
- # fields = build_describer_fields(describer_attributes)
18
- # hash = {
19
- # name: describer_name,
20
- # kind: describer_options[:kind],
21
- # database: describer_options[:database],
22
- # attributes: describer_attributes,
23
- # includes: includes,
24
- # fields: fields
25
- # }
26
- # OpenStruct.new(hash)
27
- # end
28
-
29
- # def new_describer(name:, options:)
30
- # options.store(:attributes, {})
31
- # @describers.store(name, options)
32
- # end
33
-
34
- # def describers?
35
- # @describers.present?
36
- # end
37
-
38
- # def new_attribute(name:, options:)
39
- # describer_names = Array(options.delete(:describers))
40
- # describer_names = @describers.keys if describer_names.blank?
41
-
42
- # describer_names.each do |describer_name|
43
- # if describer_options = @describers.dig(describer_name)
44
- # describer_options[:attributes].store(name, options)
45
- # end
46
- # end
47
- # end
48
-
49
- # def new_exporting(describer:)
50
- # Exporting.new(describer: describer)
51
- # end
52
-
53
- # private
54
-
55
- # def build_describer_fields(describer_attributes)
56
- # describer_attributes.inject({}) do |fields_hash, (name, options)|
57
- # enclosed_attributes = Array(options.dig(:attributes))
58
-
59
- # if enclosed_attributes.blank?
60
- # field_key, field_value = (get_heading(options.dig(:heading))&.to_s || name.to_s.titleize), (options.dig(:value) || name)
61
- # else
62
- # field_key, field_value = get_nested_field(name, options, enclosed_attributes)
63
- # end
64
- # fields_hash.store(field_key, field_value)
65
-
66
- # fields_hash
67
- # end
68
- # end
69
-
70
- # def get_nested_field(name, options, enclosed_attributes, ancestor_heading = nil)
71
- # parent_heading = ancestor_heading.present? ? ancestor_heading : ""
72
- # parent_heading += (get_heading(options.dig(:heading))&.to_s || name.to_s.singularize.titleize) + " "
73
- # parent_value = options.dig(:value) || name
74
-
75
- # enclosed_attributes.inject([[], [parent_value]]) do |nested_field, enclosed_attribute|
76
- # unless enclosed_attribute.is_a? Hash
77
- # nested_field_key = parent_heading + enclosed_attribute.to_s.titleize
78
- # nested_field_val = enclosed_attribute
79
-
80
- # nested_field[0].push(nested_field_key)
81
- # nested_field[1].push(nested_field_val)
82
- # else
83
- # enclosed_attribute.each do |enclosed_attribute_key, enclosed_attribute_value|
84
- # wrapped_attributes = Array(enclosed_attribute_value.dig(:attributes))
85
- # if wrapped_attributes.blank?
86
- # nested_field_key = parent_heading + (get_heading(enclosed_attribute_value.dig(:heading))&.to_s || enclosed_attribute_key.to_s.titleize)
87
- # nested_field_val = enclosed_attribute_value.dig(:value) || enclosed_attribute_key
88
- # else
89
- # nested_field_key, nested_field_val = get_nested_field(enclosed_attribute_key, enclosed_attribute_value, wrapped_attributes, parent_heading)
90
- # end
91
-
92
- # nested_field[0].push(nested_field_key)
93
- # nested_field[1].push(nested_field_val)
94
- # end
95
- # end
96
-
97
- # nested_field
98
- # end
99
- # end
100
-
101
- # def get_heading(options_heading)
102
- # options_heading.is_a?(Proc) ? options_heading.call(@current_class) : options_heading
103
- # end
104
- # end
105
- # end
106
- # end
1
+ module ActiveKit
2
+ module Search
3
+ class Searcher < Bedrock::Bedrocker
4
+ def create_attribute(name, options)
5
+ describer_names = super
6
+
7
+ depends_on = options.delete(:depends_on) || {}
8
+ describer_names.each do |describer_name|
9
+ set_reload_callbacks(depends_on, describer_name)
10
+ self.for(describer_name).add_attribute(name: name, options: options.deep_dup)
11
+ end
12
+ end
13
+
14
+ def describer_method(describer, params)
15
+ params[:page] = 1 if params.key?(:page) && params[:page].blank?
16
+ self.for(describer.name).fetch(term: params.delete(:term), **params).records
17
+ end
18
+
19
+ private
20
+
21
+ # Set callbacks for current class and depending classes.
22
+ def set_reload_callbacks(depends_on, describer_name)
23
+ @current_class.class_eval do
24
+ unless searcher.for(describer_name).attributes_present?
25
+ after_commit do
26
+ self.class.searcher.for(describer_name).reload(record: self)
27
+ logger.info "ActiveKit::Search | Indexing from #{self.class.name}: Done."
28
+ end
29
+ end
30
+
31
+ unless depends_on.empty?
32
+ depends_on.each do |depends_on_association, depends_on_inverse|
33
+ klass = self.reflect_on_all_associations.map { |assoc| [assoc.name, assoc.klass.name] }.to_h[depends_on_association]
34
+ klass.constantize.class_eval do
35
+ after_commit do
36
+ inverse_assoc = self.public_send(depends_on_inverse)
37
+ if inverse_assoc.respond_to?(:each)
38
+ inverse_assoc.each { |instance| instance.class.searcher.for(describer_name).reload(record: instance) }
39
+ else
40
+ inverse_assoc.class.searcher.for(describer_name).reload(record: inverse_assoc)
41
+ end
42
+ logger.info "ActiveKit::Search | Indexing from #{self.class.name}: Done."
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -1,104 +1,68 @@
1
- # module ActiveKit
2
- # module Search
3
- # class Searching
4
-
5
- # def initialize(describer:)
6
- # @describer = describer
7
- # end
8
-
9
- # def headings
10
- # @headings ||= @describer.fields.keys.flatten
11
- # end
12
-
13
- # def headings?
14
- # headings.present?
15
- # end
16
-
17
- # def lines_for(record:)
18
- # row_counter, column_counter = 1, 0
19
-
20
- # @describer.fields.inject([[]]) do |rows, (heading, value)|
21
- # if value.is_a? Proc
22
- # rows[0].push(value.call(record))
23
- # column_counter += 1
24
- # elsif value.is_a?(Symbol) || value.is_a?(String)
25
- # rows[0].push(record.public_send(value))
26
- # column_counter += 1
27
- # elsif value.is_a? Array
28
- # deeprows = get_deeprows(record, heading, value, column_counter)
29
- # deeprows.each do |deeprow|
30
- # rows[row_counter] = deeprow
31
- # row_counter += 1
32
- # end
33
-
34
- # column_count = get_column_count_for(value)
35
- # column_count.times { |i| rows[0].push(nil) }
36
- # column_counter += column_count
37
- # else
38
- # raise "Could not identify '#{value}' for '#{heading}'."
39
- # end
40
-
41
- # rows
42
- # end
43
- # end
44
-
45
- # private
46
-
47
- # def get_deeprows(record, heading, value, column_counter)
48
- # value_clone = value.clone
49
- # assoc_value = value_clone.shift
50
-
51
- # if assoc_value.is_a? Proc
52
- # assoc_records = assoc_value.call(record)
53
- # elsif assoc_value.is_a?(Symbol) || assoc_value.is_a?(String)
54
- # assoc_records = record.public_send(assoc_value)
55
- # else
56
- # raise "Count not identity '#{assoc_value}' for '#{heading}'."
57
- # end
58
-
59
- # subrows = []
60
- # assoc_records.each do |assoc_record|
61
- # subrow, subrow_column_counter, deeprows = [], 0, []
62
- # column_counter.times { |i| subrow.push(nil) }
63
-
64
- # subrow = value_clone.inject(subrow) do |subrow, v|
65
- # if v.is_a? Proc
66
- # subrow.push(v.call(assoc_record))
67
- # subrow_column_counter += 1
68
- # elsif v.is_a?(Symbol) || v.is_a?(String)
69
- # subrow.push(assoc_record.public_send(v))
70
- # subrow_column_counter += 1
71
- # elsif v.is_a? Array
72
- # deeprows = get_deeprows(assoc_record, heading, v, (column_counter + subrow_column_counter))
73
-
74
- # column_count = get_column_count_for(v)
75
- # column_count.times { |i| subrow.push(nil) }
76
- # subrow_column_counter += column_count
77
- # end
78
-
79
- # subrow
80
- # end
81
-
82
- # subrows.push(subrow)
83
- # deeprows.each { |deeprow| subrows.push(deeprow) }
84
- # end
85
-
86
- # subrows
87
- # end
88
-
89
- # def get_column_count_for(value)
90
- # count = 0
91
-
92
- # value.each do |v|
93
- # unless v.is_a? Array
94
- # count += 1
95
- # else
96
- # count += get_column_count_for(v)
97
- # end
98
- # end
99
-
100
- # count - 1
101
- # end
102
- # end
103
- # end
104
- # end
1
+ module ActiveKit
2
+ module Search
3
+ class Searching < Bedrock::Bedrocking
4
+ attr_reader :current_page, :previous_page, :next_page
5
+
6
+ def initialize(describer:, current_class:)
7
+ super
8
+
9
+ @index = Index.new(current_class: @current_class, describer: describer)
10
+ @key = Key.new(index: @index, describer: describer)
11
+ @suggestion = Suggestion.new(current_class: @current_class, describer: describer)
12
+ end
13
+
14
+ def reload(record: nil)
15
+ record ? @key.reload(record: record) : @current_class.all.each { |rec| @key.reload(record: rec) }
16
+ @index.reload
17
+ end
18
+
19
+ def clear(record: nil)
20
+ record ? @key.clear(record: record) : @current_class.all.each { |rec| @key.clear(record: rec) }
21
+ @index.reload
22
+ end
23
+
24
+ def fetch(**options)
25
+ search_result = @index.fetch(**options)
26
+
27
+ if search_result.keys.any?
28
+ @suggestion.add(term: search_result.term)
29
+ else
30
+ @suggestion.del(term: search_result.term)
31
+ end
32
+
33
+ @current_page = search_result.current_page
34
+ @previous_page = search_result.previous_page
35
+ @next_page = search_result.next_page
36
+
37
+ search_result
38
+ end
39
+
40
+ def suggestions(prefix:)
41
+ @suggestion.fetch(prefix: prefix)
42
+ end
43
+
44
+ def drop
45
+ total_count = @index.fetch(offset: 0, limit: 0).count
46
+ keys = @index.fetch(offset: 0, limit: total_count).keys
47
+ @key.drop(keys: keys)
48
+ @index.drop
49
+ end
50
+
51
+ def previous_page?
52
+ !!@previous_page
53
+ end
54
+
55
+ def next_page?
56
+ !!@next_page
57
+ end
58
+
59
+ def add_attribute(name:, options:)
60
+ @index.add_attribute_to_schema(name: name, options: options)
61
+ end
62
+
63
+ def attributes_present?
64
+ @index.schema.present?
65
+ end
66
+ end
67
+ end
68
+ end
@@ -1,10 +1,12 @@
1
1
  module ActiveKit
2
2
  module Search
3
3
  class Suggestion
4
- def initialize(current_class:)
4
+ def initialize(current_class:, describer:)
5
5
  @redis = ActiveKit::Search.redis
6
6
  @current_class = current_class
7
+ @describer = describer
7
8
  @current_class_name = current_class.to_s.parameterize.pluralize
9
+ @describer_name = describer.name.to_s
8
10
  end
9
11
 
10
12
  def add(term:, score: 1, increment: true)
@@ -32,7 +34,7 @@ module ActiveKit
32
34
  private
33
35
 
34
36
  def key
35
- "activekit:search:suggestions:#{@current_class_name}:#{System::Current.tenant.database}"
37
+ "activekit:search:suggestions:#{@current_class_name}:#{@describer_name}:#{@describer.database.call}"
36
38
  end
37
39
  end
38
40
  end
@@ -4,7 +4,7 @@ module ActiveKit
4
4
 
5
5
  autoload :Index
6
6
  autoload :Key
7
- autoload :Search
7
+ autoload :Manager
8
8
  autoload :SearchResult
9
9
  autoload :Searcher
10
10
  autoload :Searching
@@ -1,3 +1,3 @@
1
1
  module ActiveKit
2
- VERSION = '0.5.0.dev8'
2
+ VERSION = '0.5.0'
3
3
  end
@@ -1,84 +1,21 @@
1
1
  namespace :active_kit do
2
- desc "bundle exec rails active_kit:boot DOMAIN='www.yourdomain.com'"
3
- task boot: [:environment] do
4
- # Database & Preferences
5
- domain = ENV['DOMAIN'] || raise("DOMAIN not specified")
6
-
7
- tenant = nil
8
- shard_name = nil
9
-
10
- # Returns the first db config for a specific environment where it is assumed that all tenants data is stored.
11
- default_shard_name = ActiveRecord::Base.configurations.find_db_config(Rails.env).name
12
-
13
- ActiveRecord::Base.connected_to(role: :writing, shard: default_shard_name.to_sym) do
14
- tenant = System::Tenant.where(domain: domain).or(System::Tenant.where(custom_domain: domain)).select(:database, :storage, :domain, :custom_domain).first
15
- shard_name = tenant.database
16
- raise RuntimeError, 'Could not set shard name.' unless shard_name.present? # TODO: In future, redirect this to "Nothing Here Yet" page
17
- end
18
-
19
- ApplicationRecord.connects_to database: { writing: "#{shard_name}".to_sym }
20
-
21
- System::Current.tenant = OpenStruct.new(tenant.serializable_hash)
22
- System::Current.preferences = System::Preference.revealed
23
- System::Current.integrations = System::Integration.revealed
24
- end
25
-
26
2
  namespace :search do
27
- desc "bundle exec rails active_kit:search:reload CLASS='Article' DOMAIN='www.yourdomain.com'"
28
- task reload: [:boot] do
29
- if ENV['CLASS']
30
- if ENV['CLASS'].constantize.searcher.attributes_present?
31
- puts "ActiveKit::Search | Reloading: #{ENV['CLASS']}"
32
- ENV['CLASS'].constantize.searcher.reload
33
- end
34
- else
35
- Rails.application.eager_load!
36
- models = ApplicationRecord.descendants.collect(&:name)
37
- models.each do |model|
38
- if model.constantize.searcher.attributes_present?
39
- puts "ActiveKit::Search | Reloading: #{model}"
40
- model.constantize.searcher.reload
41
- end
42
- end
43
- end
3
+ desc "bundle exec rails active_kit:search:reload CLASS='Article' DESCRIBER='limit_by_search'"
4
+ task :reload do
5
+ manager = ActiveKit::Search::Manager.new(given_class: ENV['CLASS'], given_describer: ENV['DESCRIBER'])
6
+ manager.reload
44
7
  end
45
8
 
46
- desc "bundle exec rails active_kit:search:clear CLASS='Article' DOMAIN='www.yourdomain.com'"
47
- task clear: [:boot] do
48
- if ENV['CLASS']
49
- if ENV['CLASS'].constantize.searcher.attributes_present?
50
- puts "ActiveKit::Search | Clearing: #{ENV['CLASS']}"
51
- ENV['CLASS'].constantize.searcher.clear
52
- end
53
- else
54
- Rails.application.eager_load!
55
- models = ApplicationRecord.descendants.collect(&:name)
56
- models.each do |model|
57
- if model.constantize.searcher.attributes_present?
58
- puts "ActiveKit::Search | Clearing: #{model}"
59
- model.constantize.searcher.clear
60
- end
61
- end
62
- end
9
+ desc "bundle exec rails active_kit:search:clear CLASS='Article' DESCRIBER='limit_by_search'"
10
+ task :clear do
11
+ manager = ActiveKit::Search::Manager.new(given_class: ENV['CLASS'], given_describer: ENV['DESCRIBER'])
12
+ manager.clear
63
13
  end
64
14
 
65
- desc "bundle exec rails active_kit:search:drop CLASS='Article' DOMAIN='www.yourdomain.com'"
66
- task drop: [:boot] do
67
- if ENV['CLASS']
68
- if ENV['CLASS'].constantize.searcher.attributes_present?
69
- puts "ActiveKit::Search | Dropping: #{ENV['CLASS']}"
70
- ENV['CLASS'].constantize.searcher.drop
71
- end
72
- else
73
- Rails.application.eager_load!
74
- models = ApplicationRecord.descendants.collect(&:name)
75
- models.each do |model|
76
- if model.constantize.searcher.attributes_present?
77
- puts "ActiveKit::Search | Dropping: #{model}"
78
- model.constantize.searcher.drop
79
- end
80
- end
81
- end
15
+ desc "bundle exec rails active_kit:search:drop CLASS='Article' DESCRIBER='limit_by_search'"
16
+ task :drop do
17
+ manager = ActiveKit::Search::Manager.new(given_class: ENV['CLASS'], given_describer: ENV['DESCRIBER'])
18
+ manager.drop
82
19
  end
83
20
  end
84
21
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activekit
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0.dev8
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - plainsource
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-04-23 00:00:00.000000000 Z
11
+ date: 2024-04-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -53,6 +53,7 @@ files:
53
53
  - lib/active_kit/bedrock.rb
54
54
  - lib/active_kit/bedrock/bedrockable.rb
55
55
  - lib/active_kit/bedrock/bedrocker.rb
56
+ - lib/active_kit/bedrock/bedrocking.rb
56
57
  - lib/active_kit/engine.rb
57
58
  - lib/active_kit/export.rb
58
59
  - lib/active_kit/export/exportable.rb
@@ -67,7 +68,7 @@ files:
67
68
  - lib/active_kit/search.rb
68
69
  - lib/active_kit/search/index.rb
69
70
  - lib/active_kit/search/key.rb
70
- - lib/active_kit/search/search.rb
71
+ - lib/active_kit/search/manager.rb
71
72
  - lib/active_kit/search/search_result.rb
72
73
  - lib/active_kit/search/searchable.rb
73
74
  - lib/active_kit/search/searcher.rb
@@ -94,9 +95,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
94
95
  version: '0'
95
96
  required_rubygems_version: !ruby/object:Gem::Requirement
96
97
  requirements:
97
- - - ">"
98
+ - - ">="
98
99
  - !ruby/object:Gem::Version
99
- version: 1.3.1
100
+ version: '0'
100
101
  requirements: []
101
102
  rubygems_version: 3.1.2
102
103
  signing_key:
@@ -1,68 +0,0 @@
1
- module ActiveKit
2
- module Search
3
- class Search
4
- attr_reader :current_page, :previous_page, :next_page
5
-
6
- def initialize(current_class:)
7
- @current_class = current_class
8
-
9
- @index = Index.new(current_class: @current_class)
10
- @key = Key.new(index: @index)
11
- @suggestion = Suggestion.new(current_class: @current_class)
12
- end
13
-
14
- def reload(record: nil)
15
- record ? @key.reload(record: record) : @current_class.all.each { |rec| @key.reload(record: rec) }
16
- @index.reload
17
- end
18
-
19
- def clear(record: nil)
20
- record ? @key.clear(record: record) : @current_class.all.each { |rec| @key.clear(record: rec) }
21
- @index.reload
22
- end
23
-
24
- def fetch(**options)
25
- search_result = @index.fetch(**options)
26
-
27
- if search_result.keys.any?
28
- @suggestion.add(term: search_result.term)
29
- else
30
- @suggestion.del(term: search_result.term)
31
- end
32
-
33
- @current_page = search_result.current_page
34
- @previous_page = search_result.previous_page
35
- @next_page = search_result.next_page
36
-
37
- search_result
38
- end
39
-
40
- def suggestions(prefix:)
41
- @suggestion.fetch(prefix: prefix)
42
- end
43
-
44
- def drop
45
- total_count = @index.fetch(offset: 0, limit: 0).count
46
- keys = @index.fetch(offset: 0, limit: total_count).keys
47
- @key.drop(keys: keys)
48
- @index.drop
49
- end
50
-
51
- def previous_page?
52
- !!@previous_page
53
- end
54
-
55
- def next_page?
56
- !!@next_page
57
- end
58
-
59
- def add_attribute(name:, options:)
60
- @index.add_attribute_to_schema(name: name, options: options)
61
- end
62
-
63
- def attributes_present?
64
- @index.schema.present?
65
- end
66
- end
67
- end
68
- end