stretchy-model 0.3.0 → 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 +4 -4
- data/.rspec +1 -1
- data/README.md +40 -94
- data/Rakefile +92 -0
- data/lib/stretchy/associations.rb +155 -15
- data/lib/stretchy/attributes/transformers/keyword_transformer.rb +85 -0
- data/lib/stretchy/attributes/type/array.rb +15 -0
- data/lib/stretchy/attributes/type/hash.rb +17 -0
- data/lib/stretchy/attributes/type/keyword.rb +11 -0
- data/lib/stretchy/attributes/type/text.rb +12 -0
- data/lib/stretchy/attributes.rb +30 -0
- data/lib/stretchy/common.rb +2 -3
- data/lib/stretchy/delegation/gateway_delegation.rb +7 -1
- data/lib/stretchy/model/serialization.rb +1 -0
- data/lib/stretchy/querying.rb +6 -5
- data/lib/stretchy/record.rb +8 -9
- data/lib/stretchy/relation.rb +11 -17
- data/lib/stretchy/relations/aggregation_methods.rb +758 -0
- data/lib/stretchy/relations/finder_methods.rb +21 -3
- data/lib/stretchy/relations/merger.rb +11 -7
- data/lib/stretchy/relations/query_builder.rb +48 -11
- data/lib/stretchy/relations/query_methods.rb +76 -38
- data/lib/stretchy/shared_scopes.rb +1 -1
- data/lib/stretchy/utils.rb +21 -0
- data/lib/stretchy/version.rb +1 -3
- data/lib/stretchy.rb +21 -11
- data/lib/stretchy_model.rb +9 -0
- metadata +39 -5
- data/lib/active_model/type/array.rb +0 -13
- data/lib/active_model/type/hash.rb +0 -15
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: dd18b424c18bda352233d72af113e9bbc944277e03a02cfb950780f0fab9a405
|
4
|
+
data.tar.gz: 6eb86522e7bc91012cc4c743d0a9fccc54c7e75b14ff4d6f4fca5bdb9fa31626
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0fd2a0fb25439d79c799f43f43bb01df7c4dfe0ce9eb577d445b6d415eb9c38ecdc0f45c73d8255d2e079ff502e943f6491b874e3bb9c41fc9c853efed84d252
|
7
|
+
data.tar.gz: 8ea4cfa584ec825ac30aa4aa5dea88b04c709cbf7cc75089e28cd1e850318f150f1f68ead89bc571535739aa8ff8db777102d5dd05173087d4af499bac8d8bbb
|
data/.rspec
CHANGED
@@ -1 +1 @@
|
|
1
|
-
--require spec_helper
|
1
|
+
--require spec_helper
|
data/README.md
CHANGED
@@ -1,6 +1,5 @@
|
|
1
1
|
stretchy-model
|
2
2
|
===
|
3
|
-
|
4
3
|
<p>
|
5
4
|
<a href="https://stretchy.io/" target="_blank"><img src="./stretchy.logo.png" alt="Gum Image" width="450" /></a>
|
6
5
|
<br><br>
|
@@ -9,114 +8,42 @@ stretchy-model
|
|
9
8
|
|
10
9
|
</p>
|
11
10
|
|
12
|
-
Stretchy provides Elasticsearch models in a Rails environment with an integrated ActiveRecord-like interface and features.
|
13
11
|
|
14
12
|
## Features
|
15
13
|
Stretchy simplifies the process of querying, aggregating, and managing Elasticsearch-backed models, allowing Rails developers to work with search indices as comfortably as they would with traditional Rails models.
|
16
14
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
attribute :flagged, :boolean, default: false
|
25
|
-
attribute :author, :hash
|
26
|
-
attribute :tags, :array, default: []
|
27
|
-
|
28
|
-
end
|
29
|
-
```
|
30
|
-
>[!NOTE]
|
31
|
-
>`created_at`, `:updated_at` and `:id` are automatically added to all `Stretchy::Records`
|
32
|
-
|
33
|
-
|
34
|
-
## Query
|
35
|
-
```ruby
|
36
|
-
Post.where('author.name': "Jadzia", flagged: true).first
|
37
|
-
#=> <Post id: aW02w3092, title: "Fun Cats", body: "...", flagged: true,
|
38
|
-
# author: {name: "Jadzia", age: 20}, tags: ["cat", "amusing"]>
|
39
|
-
```
|
40
|
-
|
41
|
-
## Aggregations
|
42
|
-
```ruby
|
43
|
-
|
44
|
-
result = Post.filter(:range, 'author.age': {gte: 18})
|
45
|
-
.aggregation(:post_frequency, date_histogram: {
|
46
|
-
field: :created_at,
|
47
|
-
calender_interval: :month
|
48
|
-
})
|
15
|
+
* Model fully back by Elasticsearch/Opensearch
|
16
|
+
* Chain queries, scopes and aggregations
|
17
|
+
* Reduce Elasticsearch query complexity
|
18
|
+
* Support for time-based indices and aliases
|
19
|
+
* Associations to both ActiveRecord models and Stretchy::Record
|
20
|
+
* Bulk Operations made easy
|
21
|
+
* Validations, custom attributes, and more...
|
49
22
|
|
50
|
-
|
51
|
-
#=> {buckets: [{key_as_string: "2024-01-01", doc_count: 20}, ...]}
|
52
|
-
```
|
53
|
-
|
54
|
-
## Scopes
|
55
|
-
|
56
|
-
```ruby
|
57
|
-
class Post < Stretchy::Record
|
58
|
-
# ...attributes
|
59
|
-
|
60
|
-
# Scopes
|
61
|
-
scope :flagged, -> { where(flagged: true) }
|
62
|
-
scope :top_links, lambda do |size=10, url=".com"|
|
63
|
-
aggregation(:links,
|
64
|
-
terms: {
|
65
|
-
field: :links,
|
66
|
-
size: size,
|
67
|
-
include: ".*#{url}.*"
|
68
|
-
})
|
69
|
-
end
|
70
|
-
end
|
71
|
-
|
72
|
-
# Returns flagged posts and includes the top 10 'youtube.com'
|
73
|
-
# links in results.aggregations.links
|
74
|
-
result = Post.flagged.top_links(10, "youtube.com")
|
75
|
-
|
76
|
-
```
|
23
|
+
Follow the guides to learn more about:
|
77
24
|
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
Model.bulk(records_as_bulk_operations)
|
83
|
-
```
|
84
|
-
|
85
|
-
#### Bulk helper
|
86
|
-
Generates structure for the bulk operation
|
87
|
-
```ruby
|
88
|
-
record.to_bulk # default to_bulk(:index)
|
89
|
-
record.to_bulk(:delete)
|
90
|
-
record.to_bulk(:update)
|
91
|
-
```
|
25
|
+
* [Models](https://theablefew.github.io/stretchy/#/guides/models?id=models)
|
26
|
+
* [Querying](https://theablefew.github.io/stretchy/#/guides/querying?id=querying)
|
27
|
+
* [Aggregations](https://theablefew.github.io/stretchy/#/guides/aggregations?id=aggregations)
|
28
|
+
* [Scopes](https://theablefew.github.io/stretchy/#/guides/scopes?id=scopes)
|
92
29
|
|
93
|
-
#### In batches
|
94
|
-
Run bulk operations in batches specified by `size`
|
95
|
-
```ruby
|
96
|
-
Model.bulk_in_batches(records, size: 100) do |batch|
|
97
|
-
batch.map! { |record| Model.new(record).to_bulk }
|
98
|
-
end
|
99
|
-
```
|
100
30
|
|
31
|
+
[Read the Documentation](https://theablefew.github.io/stretchy/#/) or walk through of a simple [Data Analysis](https://theablefew.github.io/stretchy/#/examples/data_analysis?id=data-analysis) example.
|
101
32
|
|
102
|
-
## Instrumentation
|
103
|
-
```ruby
|
104
|
-
Blanket.first
|
105
|
-
```
|
106
33
|
|
107
|
-
```sh
|
108
|
-
Blanket (6.322ms) curl -X GET 'http://localhost:9200/blankets/_search?size=1' -d '{"sort":{"date":"desc"}}'
|
109
|
-
```
|
110
34
|
|
111
35
|
## Installation
|
112
36
|
|
113
37
|
Install the gem and add to the application's Gemfile by executing:
|
114
38
|
|
115
|
-
|
39
|
+
```sh
|
40
|
+
bundle add stretchy-model
|
41
|
+
```
|
116
42
|
|
117
43
|
If bundler is not being used to manage dependencies, install the gem by executing:
|
118
|
-
|
119
|
-
|
44
|
+
```sh
|
45
|
+
gem install stretchy-model
|
46
|
+
```
|
120
47
|
|
121
48
|
<details>
|
122
49
|
<summary>Rails Configuration</summary>
|
@@ -131,6 +58,12 @@ rails credentials:edit
|
|
131
58
|
```yaml
|
132
59
|
elasticsearch:
|
133
60
|
url: localhost:9200
|
61
|
+
|
62
|
+
# or opensearch
|
63
|
+
# opensearch:
|
64
|
+
# host: https://localhost:9200
|
65
|
+
# user: admin
|
66
|
+
# password: admin
|
134
67
|
```
|
135
68
|
|
136
69
|
#### Create an initializer
|
@@ -149,11 +82,24 @@ end
|
|
149
82
|
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.
|
150
83
|
|
151
84
|
>[!TIP]
|
152
|
-
>This library is built on top of the excellent [elasticsearch-persistence](https://github.com/elastic/elasticsearch-rails/tree/main/elasticsearch-persistence) gem.
|
153
|
-
>
|
154
85
|
> Full documentation on [Elasticsearch Query DSL and Aggregation options](https://github.com/elastic/elasticsearch-rails/tree/main/elasticsearch-persistence)
|
155
86
|
|
156
87
|
## Testing
|
88
|
+
<details>
|
89
|
+
<summary>Act</summary>
|
90
|
+
|
91
|
+
Run github action workflow locally
|
92
|
+
|
93
|
+
```sh
|
94
|
+
brew install act --HEAD
|
95
|
+
```
|
96
|
+
|
97
|
+
```sh
|
98
|
+
act -P ubuntu-latest=ghcr.io/catthehacker/ubuntu:runner-latest
|
99
|
+
```
|
100
|
+
|
101
|
+
</details>
|
102
|
+
|
157
103
|
<details>
|
158
104
|
<summary>Elasticsearch</summary>
|
159
105
|
|
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
|
@@ -3,7 +3,11 @@ module Stretchy
|
|
3
3
|
extend ActiveSupport::Concern
|
4
4
|
|
5
5
|
def save!
|
6
|
+
if valid?
|
6
7
|
self.save
|
8
|
+
else
|
9
|
+
raise "Record is invalid"
|
10
|
+
end
|
7
11
|
end
|
8
12
|
|
9
13
|
# Required for Elasticsearch < 7
|
@@ -34,7 +38,7 @@ module Stretchy
|
|
34
38
|
end
|
35
39
|
|
36
40
|
def association_reflection(association)
|
37
|
-
|
41
|
+
Stretchy::Relation.new @@_associations[association], (dirty[association.to_sym] || [])
|
38
42
|
end
|
39
43
|
|
40
44
|
def _destroy=(bool)
|
@@ -48,6 +52,7 @@ module Stretchy
|
|
48
52
|
def save_associations
|
49
53
|
@_after_save_objects.each_pair do |association, collection|
|
50
54
|
collection.each do |instance|
|
55
|
+
# TODO: bulk update
|
51
56
|
instance.send("#{@@_association_options[association.to_sym][:foreign_key]}=", self.id)
|
52
57
|
instance.save
|
53
58
|
end
|
@@ -59,59 +64,194 @@ module Stretchy
|
|
59
64
|
@@_associations ||= {}
|
60
65
|
@@_association_options ||= {}
|
61
66
|
|
67
|
+
# The belongs_to method is used to set up a one-to-one connection with another model.
|
68
|
+
# This indicates that this model has exactly one instance of another model.
|
69
|
+
# For example, if your application includes authors and books, and each book can be assigned exactly one author,
|
70
|
+
# you'd declare the book model to belong to the author model.
|
71
|
+
#
|
72
|
+
# association:: [Symbol] the name of the association
|
73
|
+
# options:: [Hash] a hash to set up options for the association
|
74
|
+
# :foreign_key - the foreign key used for the association. Defaults to "#{association}_id"
|
75
|
+
# :primary_key - the primary key used for the association. Defaults to "id"
|
76
|
+
# :class_name - the name of the associated object's class. Defaults to the name of the association
|
77
|
+
#
|
78
|
+
# Example:
|
79
|
+
# belongs_to :author
|
80
|
+
#
|
81
|
+
# This creates a book.author method that returns the author of the book.
|
82
|
+
# It also creates an author= method that allows you to assign the author of the book.
|
83
|
+
#
|
62
84
|
def belongs_to(association, options = {})
|
63
85
|
@@_association_options[association] = {
|
64
86
|
foreign_key: "#{association}_id",
|
65
87
|
primary_key: "id",
|
66
88
|
class_name: association
|
67
|
-
}.
|
89
|
+
}.merge(options)
|
68
90
|
|
69
91
|
klass = @@_association_options[association][:class_name].to_s.singularize.classify.constantize
|
70
92
|
@@_associations[association] = klass
|
71
93
|
|
72
94
|
define_method(association.to_sym) do
|
73
|
-
|
95
|
+
instance_variable_get("@#{association}") ||
|
96
|
+
klass.where(_id: self.send(@@_association_options[association][:foreign_key].to_sym)).first
|
74
97
|
end
|
75
98
|
|
76
99
|
define_method("#{association}=".to_sym) do |val|
|
77
100
|
options = @@_association_options[association]
|
78
|
-
|
101
|
+
self.send("#{options[:foreign_key]}=", val.send(options[:primary_key]))
|
102
|
+
instance_variable_set("@#{association}", val)
|
103
|
+
end
|
104
|
+
|
105
|
+
define_method("build_#{association}") do |*args|
|
106
|
+
associated_object = klass.new(*args)
|
107
|
+
instance_variable_set("@#{association}", associated_object)
|
108
|
+
associated_object
|
109
|
+
end
|
110
|
+
|
111
|
+
before_save do
|
112
|
+
associated_object = instance_variable_get("@#{association}")
|
113
|
+
if associated_object && associated_object.new_record?
|
114
|
+
if associated_object.save!
|
115
|
+
self.send("#{@@_association_options[association][:foreign_key]}=", associated_object.id)
|
116
|
+
end
|
117
|
+
end
|
79
118
|
end
|
80
119
|
end
|
81
120
|
|
82
|
-
def has_one(association, class_name: nil, foreign_key: nil, dependent: :destroy)
|
83
121
|
|
84
|
-
|
85
|
-
|
122
|
+
|
123
|
+
|
124
|
+
|
125
|
+
|
126
|
+
|
127
|
+
|
128
|
+
|
129
|
+
|
130
|
+
# The has_one method is used to set up a one-to-one connection with another model.
|
131
|
+
# This indicates that this model contains the foreign key.
|
132
|
+
#
|
133
|
+
# association:: [Symbol] The name of the association.
|
134
|
+
# options:: [Hash] A hash to set up options for the association.
|
135
|
+
# :class_name - The name of the associated model. If not provided, it's derived from +association+.
|
136
|
+
# :foreign_key - The name of the foreign key on the associated model. If not provided, it's derived from the name of this model.
|
137
|
+
# :dependent - If set to +:destroy+, the associated object will be destroyed when this object is destroyed. This is the default behavior.
|
138
|
+
# :primary_key - The name of the primary key on the associated model. If not provided, it's assumed to be +id+.
|
139
|
+
#
|
140
|
+
#
|
141
|
+
# Example:
|
142
|
+
# has_one :profile
|
143
|
+
#
|
144
|
+
# This creates a user.profile method that returns the profile of the user.
|
145
|
+
# It also creates a profile= method that allows you to assign the profile of the user.
|
146
|
+
#
|
147
|
+
def has_one(association, options = {})
|
148
|
+
|
149
|
+
@@_association_options[association] = {
|
150
|
+
foreign_key: "#{self.name.underscore}_id",
|
151
|
+
primary_key: "id",
|
152
|
+
class_name: association
|
153
|
+
}.merge(options)
|
154
|
+
|
155
|
+
klass = @@_association_options[association][:class_name].to_s.singularize.classify.constantize
|
86
156
|
@@_associations[association] = klass
|
87
157
|
|
158
|
+
foreign_key = @@_association_options[association][:foreign_key]
|
159
|
+
|
88
160
|
define_method(association.to_sym) do
|
89
|
-
|
161
|
+
instance_variable_get("@#{association}") ||
|
162
|
+
klass.where("#{foreign_key}": self.id).first
|
163
|
+
end
|
164
|
+
|
165
|
+
define_method("#{association}=".to_sym) do |val|
|
166
|
+
instance_variable_set("@#{association}", val)
|
167
|
+
save!
|
168
|
+
end
|
169
|
+
|
170
|
+
before_save do
|
171
|
+
associated_object = instance_variable_get("@#{association}")
|
172
|
+
if associated_object
|
173
|
+
associated_object.send("#{foreign_key}=", self.id)
|
174
|
+
associated_object.save!
|
175
|
+
end
|
90
176
|
end
|
91
177
|
end
|
92
178
|
|
93
|
-
def has_many(association, klass, options = {})
|
94
|
-
@@_associations[association] = klass
|
95
179
|
|
96
|
-
opt_fk = options.delete(:foreign_key)
|
97
|
-
foreign_key = opt_fk ? opt_fk : "#{self.name.split("::").last.tableize.singularize}_id"
|
98
180
|
|
99
|
-
|
181
|
+
|
182
|
+
|
183
|
+
|
184
|
+
|
185
|
+
|
186
|
+
|
187
|
+
# The has_many method is used to set up a one-to-many connection with another model.
|
188
|
+
# This indicates that this model can be matched with zero or more instances of another model.
|
189
|
+
# For example, if your application includes authors and books, and each author can have many books,
|
190
|
+
# you'd declare the author model to have many books.
|
191
|
+
#
|
192
|
+
# association:: [Symbol] the name of the association
|
193
|
+
# options:: [Hash] a hash to set up options for the association
|
194
|
+
# :foreign_key - the foreign key used for the association. Defaults to "#{self.name.downcase}_id"
|
195
|
+
# :primary_key - the primary key used for the association. Defaults to "id"
|
196
|
+
# :class_name - the name of the associated object's class. Defaults to the name of the association
|
197
|
+
# :dependent - if set to :destroy, the associated object will be destroyed when this object is destroyed. This is the default behavior.
|
198
|
+
#
|
199
|
+
#
|
200
|
+
# Example:
|
201
|
+
# has_many :books
|
202
|
+
#
|
203
|
+
# This creates an author.books method that returns a collection of books for the author.
|
204
|
+
# It also creates a books= method that allows you to assign the books for the author.
|
205
|
+
#
|
206
|
+
def has_many(association, options = {})
|
207
|
+
@@_association_options[association] = {
|
208
|
+
foreign_key: "#{self.name.underscore}_id",
|
209
|
+
primary_key: "id",
|
210
|
+
class_name: association.to_s.singularize.to_sym
|
211
|
+
}.merge(options)
|
212
|
+
|
213
|
+
klass = @@_association_options[association][:class_name].to_s.classify.constantize
|
214
|
+
foreign_key = @@_association_options[association][:foreign_key]
|
215
|
+
primary_key = @@_association_options[association][:primary_key]
|
216
|
+
@@_associations[association] = klass
|
100
217
|
|
101
218
|
define_method(association.to_sym) do
|
102
219
|
args = {}
|
103
|
-
args[
|
220
|
+
args["_#{primary_key}"] = self.send("#{association.to_s.singularize}_ids")
|
104
221
|
self.new_record? ? association_reflection(association) : klass.where(args)
|
105
222
|
end
|
106
223
|
|
224
|
+
define_method("#{association.to_s.singularize}_ids") do
|
225
|
+
instance_variable_get("@#{association.to_s.singularize}_ids".to_sym)
|
226
|
+
end
|
227
|
+
|
228
|
+
define_method("#{association.to_s.singularize}_ids=") do |val|
|
229
|
+
instance_variable_set("@#{association.to_s.singularize}_ids".to_sym, val)
|
230
|
+
end
|
231
|
+
|
232
|
+
define_method("#{association}=".to_sym) do |val|
|
233
|
+
val.each { |v| after_save_objects(v.attributes, association)}
|
234
|
+
self.send("#{association.to_s.singularize}_ids=", val.map(&:id))
|
235
|
+
dirty
|
236
|
+
end
|
237
|
+
|
107
238
|
define_method("build_#{association}".to_sym) do |*args|
|
108
239
|
opts = {}
|
109
240
|
opts[foreign_key] = self.id
|
110
241
|
args.first.merge! opts
|
111
242
|
klass.new *args
|
112
243
|
end
|
244
|
+
|
245
|
+
after_save do
|
246
|
+
save_associations
|
247
|
+
end
|
113
248
|
end
|
114
249
|
|
250
|
+
|
251
|
+
|
252
|
+
|
253
|
+
|
254
|
+
|
115
255
|
def validates_associated(*attr_names)
|
116
256
|
validates_with AssociatedValidator, _merge_attributes(attr_names)
|
117
257
|
end
|
@@ -131,7 +271,7 @@ module Stretchy
|
|
131
271
|
end
|
132
272
|
|
133
273
|
def reflect_on_association(association)
|
134
|
-
|
274
|
+
Stretchy::Relation.new @@_associations[association]
|
135
275
|
end
|
136
276
|
|
137
277
|
def update_all(records, **attributes)
|
@@ -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
|