stretchy-model 0.2.0 → 0.4.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: a1709205b5fe75817fe79c5047b6f4d3531257cf8a3118f84d8fa4b92adb8932
4
- data.tar.gz: 1add1b8c78f9c27362d31e0f1f8c141a429af4b6176b7d521e32e8a3e9230039
3
+ metadata.gz: d7dff610329ad21128c58429c6f9d08b62138a7fad9adb0f610984bd744db1cf
4
+ data.tar.gz: 915256c1b413dd34d4777097b878f0c2c7886342b5d8dcfedc2159a405b3bdf2
5
5
  SHA512:
6
- metadata.gz: cf690ed5affe0224370f50e5208a6fcd0a91e611460fecf3f9a024464b0c87fa9b2718932fefca49c4ad9d587c4cc956b3490738fd794176f35d8f180dc16998
7
- data.tar.gz: bc518187e999e3e989332612294ec8152bcc673fd5c025683e1170e8549491d8f8f3b578d4feb82a8fc4b564811f625eaae21fa5b754f0cd8fc7e36ae136c744
6
+ metadata.gz: 66f98b878a8e78d9d79b53f05edf89a8d4a61d4fd3a5b98d93ef349491c7c87f36d4cadb857b8be5e9e992308609c22d1abc45703eb39b95e78240d87c6ae081
7
+ data.tar.gz: 0eabc3b84d0aecb0f8051bf53238ec6da5d431c7ff5d0f7c3be20888a20252504e3324c7b558fe455d90c2cf0db827b1d0440e6d03d211569ed8ed7d60dade01
data/README.md CHANGED
@@ -98,16 +98,6 @@ Model.bulk_in_batches(records, size: 100) do |batch|
98
98
  end
99
99
  ```
100
100
 
101
-
102
- ## Instrumentation
103
- ```ruby
104
- Blanket.first
105
- ```
106
-
107
- ```sh
108
- Blanket (6.322ms) curl -X GET 'http://localhost:9200/blankets/_search?size=1' -d '{"sort":{"date":"desc"}}'
109
- ```
110
-
111
101
  ## Installation
112
102
 
113
103
  Install the gem and add to the application's Gemfile by executing:
@@ -118,6 +108,38 @@ If bundler is not being used to manage dependencies, install the gem by executin
118
108
 
119
109
  $ gem install stretchy-model
120
110
 
111
+ <details>
112
+ <summary>Rails Configuration</summary>
113
+
114
+
115
+
116
+ ```sh
117
+ rails credentials:edit
118
+ ```
119
+
120
+ #### Add elasticsearch credentials
121
+ ```yaml
122
+ elasticsearch:
123
+ url: localhost:9200
124
+
125
+ # or opensearch
126
+ # opensearch:
127
+ # host: https://localhost:9200
128
+ # user: admin
129
+ # password: admin
130
+ ```
131
+
132
+ #### Create an initializer
133
+ <p><sub><em>config/initializers/stretchy.rb</em></sub></p>
134
+
135
+ ```ruby {file=config/initializers/stretchy.rb}
136
+ Stretchy.configure do |config|
137
+ config.client = Elasticsearch::Client.new url: Rails.application.credentials.elasticsearch.url, log: true
138
+ end
139
+ ```
140
+ </details>
141
+
142
+
121
143
  ## Development
122
144
 
123
145
  After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
@@ -128,6 +150,25 @@ After checking out the repo, run `bin/setup` to install dependencies. You can al
128
150
  > Full documentation on [Elasticsearch Query DSL and Aggregation options](https://github.com/elastic/elasticsearch-rails/tree/main/elasticsearch-persistence)
129
151
 
130
152
  ## Testing
153
+ <details>
154
+ <summary>Act</summary>
155
+
156
+ Run github action workflow locally
157
+
158
+ ```sh
159
+ brew install act --HEAD
160
+ ```
161
+
162
+ ```sh
163
+ act -P ubuntu-latest=ghcr.io/catthehacker/ubuntu:runner-latest
164
+ ```
165
+
166
+ </details>
167
+
168
+ <details>
169
+ <summary>Elasticsearch</summary>
170
+
171
+
131
172
  ```
132
173
  docker-compose up elasticsearch
133
174
  ```
@@ -136,6 +177,21 @@ docker-compose up elasticsearch
136
177
  bundle exec rspec
137
178
  ```
138
179
 
180
+ </details>
181
+
182
+ <details>
183
+ <summary>Opensearch</summary>
184
+
185
+
186
+ ```
187
+ docker-compose up opensearch
188
+ ```
189
+
190
+ ```
191
+ ENV['BACKEND']=opensearch bundle rspec
192
+ ```
193
+ </details>
194
+
139
195
  ## Contributing
140
196
 
141
197
  Bug reports and pull requests are welcome on GitHub at https://github.com/theablefew/stretchy. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/theablefew/stretchy/blob/master/CODE_OF_CONDUCT.md).
data/Rakefile CHANGED
@@ -2,3 +2,95 @@
2
2
 
3
3
  require "bundler/gem_tasks"
4
4
  task default: %i[]
5
+
6
+ require 'octokit'
7
+ require 'versionomy'
8
+ require 'rainbow'
9
+
10
+ def determine_current_version
11
+ # Load current version
12
+ load 'lib/stretchy/version.rb'
13
+ current_version = Versionomy.parse(Stretchy::VERSION)
14
+ end
15
+
16
+ def determine_new_version(version)
17
+ # Load current version
18
+ current_version = determine_current_version
19
+
20
+ # Determine new version
21
+ case version.to_sym
22
+ when :major
23
+ current_version.bump(:major)
24
+ when :minor
25
+ current_version.bump(:minor)
26
+ when :patch
27
+ current_version.bump(:tiny)
28
+ else
29
+ version =~ /\Av?\d+\.\d+\.\d+\z/ ? Versionomy.parse(version).to_s.gsub(/v/,'') : current_version
30
+ end
31
+ end
32
+
33
+ def create_release_branch(new_version, base_branch)
34
+ system("git stash save 'Changes before creating release branch'")
35
+ system("git fetch origin #{base_branch}")
36
+ branch_name = "release/v#{new_version}"
37
+ system("git checkout -b #{branch_name} #{base_branch}")
38
+ branch_name
39
+ end
40
+
41
+ def update_version_file(new_version)
42
+ # Update lib/stretchy/version.rb
43
+ File.open('lib/stretchy/version.rb', 'w') do |file|
44
+ file.puts "module Stretchy\n VERSION = '#{new_version}'\nend"
45
+ end
46
+ end
47
+
48
+ def commit_and_push_changes(new_version, branch_name)
49
+ system("git add lib/stretchy/version.rb")
50
+ system("git commit -m 'Bump version to v#{new_version}'")
51
+ system("git tag v#{new_version}")
52
+ system("git push origin #{branch_name} --tags -f")
53
+ end
54
+
55
+ def create_pull_request(new_version, base_branch, branch_name)
56
+ # Create a pull request
57
+ client = Octokit::Client.new(access_token: ENV['GH_TOKEN'])
58
+ client.create_pull_request('theablefew/stretchy', base_branch, branch_name, "Release v#{new_version}")
59
+ end
60
+
61
+ namespace :publish do
62
+ desc "Create a release"
63
+ task :release, [:version, :base_branch] do |t, args|
64
+ args.with_defaults(version: :patch, base_branch: 'main')
65
+ version = args[:version]
66
+ base_branch = args[:base_branch]
67
+
68
+ old_version = determine_current_version
69
+ new_version = determine_new_version(version)
70
+ puts Rainbow("Bumping version from #{old_version} to #{new_version}").green
71
+ branch_name = create_release_branch(new_version, base_branch)
72
+ begin
73
+ update_version_file(new_version)
74
+ commit_and_push_changes(new_version, branch_name)
75
+ create_pull_request(new_version, base_branch, branch_name)
76
+ rescue => e
77
+ puts "Error: #{e.message}"
78
+ puts "Rolling back changes"
79
+ system("git tag -d v#{new_version}")
80
+ system("git checkout #{base_branch}")
81
+ system("git branch -D #{branch_name}")
82
+ end
83
+ end
84
+
85
+ task :major do
86
+ Rake::Task['publish:release'].invoke('major')
87
+ end
88
+
89
+ task :minor do
90
+ Rake::Task['publish:release'].invoke('minor')
91
+ end
92
+
93
+ task :patch do
94
+ Rake::Task['publish:release'].invoke('patch')
95
+ end
96
+ end
@@ -0,0 +1,85 @@
1
+ module Stretchy
2
+ module Attributes
3
+ module Transformers
4
+ class KeywordTransformer
5
+
6
+ KEYWORD_AGGREGATION_KEYS = [:terms, :rare_terms, :significant_terms, :cardinality, :string_stats]
7
+
8
+ attr_reader :attribute_types
9
+
10
+ def initialize(attribute_types)
11
+ @attribute_types = attribute_types
12
+ end
13
+
14
+ def cast_value_keys
15
+ values.transform_values do |value|
16
+ case value
17
+ when Array
18
+ value.map { |item| transform_keys_for_item(item) }
19
+ when Hash
20
+ transform_keys_for_item(value)
21
+ else
22
+ value
23
+ end
24
+ end
25
+ end
26
+
27
+ def keyword?(arg)
28
+ attr = @attribute_types[arg.to_s]
29
+ return false unless attr
30
+ attr.is_a?(Stretchy::Attributes::Type::Keyword)
31
+ end
32
+
33
+ def protected?(arg)
34
+ return false if arg.nil?
35
+ Stretchy::Relations::AggregationMethods::AGGREGATION_METHODS.include?(arg.to_sym)
36
+ end
37
+
38
+ def transform(item, *ignore)
39
+ item.each_with_object({}) do |(k, v), new_item|
40
+ if ignore && ignore.include?(k)
41
+ new_item[k] = v
42
+ next
43
+ end
44
+ new_key = (!protected?(k) && keyword?(k)) ? "#{k}.keyword" : k
45
+
46
+ new_value = v
47
+
48
+ if new_value.is_a?(Hash)
49
+ new_value = transform(new_value)
50
+ elsif new_value.is_a?(Array)
51
+ new_value = new_value.map { |i| i.is_a?(Hash) ? transform(i) : i }
52
+ elsif new_value.is_a?(String) || new_value.is_a?(Symbol)
53
+ new_value = "#{new_value}.keyword" if keyword?(new_value)
54
+ end
55
+
56
+ new_item[new_key] = new_value
57
+ end
58
+ end
59
+
60
+ # If terms are used, we assume that the field is a keyword field
61
+ # and append .keyword to the field name
62
+ # {terms: {field: 'gender'}}
63
+ # or nested aggs
64
+ # {terms: {field: 'gender'}, aggs: {name: {terms: {field: 'position.name'}}}}
65
+ # should be converted to
66
+ # {terms: {field: 'gender.keyword'}, aggs: {name: {terms: {field: 'position.name.keyword'}}}}
67
+ # {date_histogram: {field: 'created_at', interval: 'day'}}
68
+ # TODO: There may be cases where we don't want to add .keyword to the field and there should be a way to override this
69
+ def assume_keyword_field(args={}, parent_match=false)
70
+ if args.is_a?(Hash)
71
+ args.each do |k, v|
72
+ if v.is_a?(Hash)
73
+ assume_keyword_field(v, KEYWORD_AGGREGATION_FIELDS.include?(k))
74
+ else
75
+ next unless v.is_a?(String) || v.is_a?(Symbol)
76
+ args[k] = ([:field, :fields].include?(k.to_sym) && v !~ /\.keyword$/ && parent_match) ? "#{v}.keyword" : v.to_s
77
+ end
78
+ end
79
+ end
80
+ end
81
+
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,11 @@
1
+ module Stretchy
2
+ module Attributes
3
+ module Type
4
+ class Keyword < ActiveModel::Type::String # :nodoc:
5
+ def type
6
+ :keyword
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,10 @@
1
+ module Stretchy
2
+ module Attributes
3
+
4
+ def self.register!
5
+ ActiveModel::Type.register(:array, ActiveModel::Type::Array)
6
+ ActiveModel::Type.register(:hash, ActiveModel::Type::Hash)
7
+ ActiveModel::Type.register(:keyword, Stretchy::Attributes::Type::Keyword)
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,86 @@
1
+ module Stretchy
2
+ module OpenSearchCompatibility
3
+ extend ActiveSupport::Concern
4
+
5
+ # Patches the Elasticsearch::Persistence::Repository::Search module to remove the
6
+ # document type from the request for compatability with OpenSearch
7
+ def self.opensearch_patch!
8
+ patch = Module.new do
9
+ def search(query_or_definition, options={})
10
+ request = { index: index_name }
11
+
12
+ if query_or_definition.respond_to?(:to_hash)
13
+ request[:body] = query_or_definition.to_hash
14
+ elsif query_or_definition.is_a?(String)
15
+ request[:q] = query_or_definition
16
+ else
17
+ raise ArgumentError, "[!] Pass the search definition as a Hash-like object or pass the query as a String" +
18
+ " -- #{query_or_definition.class} given."
19
+ end
20
+
21
+ Elasticsearch::Persistence::Repository::Response::Results.new(self, client.search(request.merge(options)))
22
+ end
23
+
24
+ def count(query_or_definition=nil, options={})
25
+ query_or_definition ||= { query: { match_all: {} } }
26
+ request = { index: index_name}
27
+
28
+ if query_or_definition.respond_to?(:to_hash)
29
+ request[:body] = query_or_definition.to_hash
30
+ elsif query_or_definition.is_a?(String)
31
+ request[:q] = query_or_definition
32
+ else
33
+ raise ArgumentError, "[!] Pass the search definition as a Hash-like object or pass the query as a String" +
34
+ " -- #{query_or_definition.class} given."
35
+ end
36
+
37
+ client.count(request.merge(options))['count']
38
+ end
39
+ end
40
+
41
+ store = Module.new do
42
+ def save(document, options={})
43
+ serialized = serialize(document)
44
+ id = __get_id_from_document(serialized)
45
+ request = { index: index_name,
46
+ id: id,
47
+ body: serialized }
48
+ client.index(request.merge(options))
49
+ end
50
+
51
+
52
+ def update(document_or_id, options = {})
53
+ if document_or_id.is_a?(String) || document_or_id.is_a?(Integer)
54
+ id = document_or_id
55
+ body = options
56
+ else
57
+ document = serialize(document_or_id)
58
+ id = __extract_id_from_document(document)
59
+ if options[:script]
60
+ body = options
61
+ else
62
+ body = { doc: document }.merge(options)
63
+ end
64
+ end
65
+ client.update(index: index_name, id: id, body: body)
66
+ end
67
+
68
+ def delete(document_or_id, options = {})
69
+ if document_or_id.is_a?(String) || document_or_id.is_a?(Integer)
70
+ id = document_or_id
71
+ else
72
+ serialized = serialize(document_or_id)
73
+ id = __get_id_from_document(serialized)
74
+ end
75
+ client.delete({ index: index_name, id: id }.merge(options))
76
+ end
77
+ end
78
+
79
+
80
+ ::Elasticsearch::Persistence::Repository.send(:include, patch)
81
+ ::Elasticsearch::Persistence::Repository.send(:include, store)
82
+ end
83
+
84
+
85
+ end
86
+ end
@@ -1,11 +1,12 @@
1
1
  module Stretchy
2
2
  module Querying
3
3
  delegate :first, :first!, :last, :last!, :exists?, :has_field, :any?, :many?, to: :all
4
- delegate :order, :limit, :size, :sort, :where, :rewhere, :eager_load, :includes, :create_with, :none, :unscope, to: :all
5
- delegate :or_filter, :filter, :fields, :source, :highlight, :aggregation, to: :all
6
- delegate :skip_callbacks, :routing, to: :all
7
- delegate :search_options, :routing, to: :all
8
- delegate :must, :must_not, :should, :where_not, :query_string, to: :all
4
+ delegate :order, :limit, :size, :sort, :rewhere, :eager_load, :includes, :create_with, :none, :unscope, to: :all
5
+ delegate :or_filter, :fields, :source, :highlight, to: :all
6
+ delegate *Stretchy::Relations::AggregationMethods::AGGREGATION_METHODS, to: :all
7
+
8
+ delegate :skip_callbacks, :routing, :search_options, to: :all
9
+ delegate :must, :must_not, :should, :where_not, :where, :filter_query, :query_string, to: :all
9
10
 
10
11
  def fetch_results(es)
11
12
  unless es.count?
@@ -4,7 +4,7 @@ module Stretchy
4
4
  class Relation
5
5
 
6
6
  # These methods can accept multiple values.
7
- MULTI_VALUE_METHODS = [:order, :where, :or_filter, :filter, :bind, :extending, :unscope, :skip_callbacks]
7
+ MULTI_VALUE_METHODS = [:order, :where, :or_filter, :filter_query, :bind, :extending, :unscope, :skip_callbacks]
8
8
 
9
9
  # These methods can accept a single value.
10
10
  SINGLE_VALUE_METHODS = [:limit, :offset, :routing, :size]
@@ -16,7 +16,7 @@ module Stretchy
16
16
  VALUE_METHODS = MULTI_VALUE_METHODS + SINGLE_VALUE_METHODS
17
17
 
18
18
  # Include modules.
19
- include Relations::FinderMethods, Relations::SpawnMethods, Relations::QueryMethods, Relations::SearchOptionMethods, Delegation
19
+ include Relations::FinderMethods, Relations::SpawnMethods, Relations::QueryMethods, Relations::AggregationMethods, Relations::SearchOptionMethods, Delegation
20
20
 
21
21
  # Getters.
22
22
  attr_reader :klass, :loaded
@@ -162,7 +162,7 @@ module Stretchy
162
162
  #
163
163
  # @return [QueryBuilder] The query builder for the relation.
164
164
  def query_builder
165
- Relations::QueryBuilder.new(values)
165
+ Relations::QueryBuilder.new(values, klass.attribute_types)
166
166
  end
167
167
 
168
168
  end