has-meta 0.8.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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 928b405da392f2e0c10011167dfaff2d42b9a9fce7dba0cefd3266ebca9d05d9
4
+ data.tar.gz: 6501c857bf540e800b5de03027f47987e96c2beecab80ef385e7a3c91f036b45
5
+ SHA512:
6
+ metadata.gz: 2da0e9dd873bff4d454e58cd58650b22692c9cacf504421be84ebcfc00b7551f1c4b165d910d8c8e56d44bb75b62e011be11d817bae4f70897df18ed800c1205
7
+ data.tar.gz: d57dd3db2819d4b6027378d48ab21c5a086dcd89b7c6395c8ac984813bd2f72d4759ffbb3d8f12a3017765ed18ee4c3ccdd669ef49283d9227932d8404f4cc82
@@ -0,0 +1,13 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ .rspec_status
12
+ *.swp
13
+ spec/debug.log
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm use ruby-2.5.0@has-meta
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.5.0
5
+ before_install: gem install bundler -v 1.16.1
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in has-meta.gemspec
6
+ gemspec
@@ -0,0 +1,105 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ has-meta (0.8.0)
5
+ activerecord (>= 4.2.8)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ actionpack (4.2.10)
11
+ actionview (= 4.2.10)
12
+ activesupport (= 4.2.10)
13
+ rack (~> 1.6)
14
+ rack-test (~> 0.6.2)
15
+ rails-dom-testing (~> 1.0, >= 1.0.5)
16
+ rails-html-sanitizer (~> 1.0, >= 1.0.2)
17
+ actionview (4.2.10)
18
+ activesupport (= 4.2.10)
19
+ builder (~> 3.1)
20
+ erubis (~> 2.7.0)
21
+ rails-dom-testing (~> 1.0, >= 1.0.5)
22
+ rails-html-sanitizer (~> 1.0, >= 1.0.3)
23
+ activemodel (4.2.10)
24
+ activesupport (= 4.2.10)
25
+ builder (~> 3.1)
26
+ activerecord (4.2.10)
27
+ activemodel (= 4.2.10)
28
+ activesupport (= 4.2.10)
29
+ arel (~> 6.0)
30
+ activesupport (4.2.10)
31
+ i18n (~> 0.7)
32
+ minitest (~> 5.1)
33
+ thread_safe (~> 0.3, >= 0.3.4)
34
+ tzinfo (~> 1.1)
35
+ arel (6.0.4)
36
+ builder (3.2.3)
37
+ coderay (1.1.2)
38
+ concurrent-ruby (1.0.5)
39
+ crass (1.0.3)
40
+ diff-lcs (1.3)
41
+ erubis (2.7.0)
42
+ i18n (0.9.1)
43
+ concurrent-ruby (~> 1.0)
44
+ loofah (2.1.1)
45
+ crass (~> 1.0.2)
46
+ nokogiri (>= 1.5.9)
47
+ method_source (0.9.0)
48
+ mini_portile2 (2.3.0)
49
+ minitest (5.11.1)
50
+ nokogiri (1.8.1)
51
+ mini_portile2 (~> 2.3.0)
52
+ pry (0.11.3)
53
+ coderay (~> 1.1.0)
54
+ method_source (~> 0.9.0)
55
+ rack (1.6.8)
56
+ rack-test (0.6.3)
57
+ rack (>= 1.0)
58
+ rails-deprecated_sanitizer (1.0.3)
59
+ activesupport (>= 4.2.0.alpha)
60
+ rails-dom-testing (1.0.9)
61
+ activesupport (>= 4.2.0, < 5.0)
62
+ nokogiri (~> 1.6)
63
+ rails-deprecated_sanitizer (>= 1.0.1)
64
+ rails-html-sanitizer (1.0.3)
65
+ loofah (~> 2.0)
66
+ railties (4.2.10)
67
+ actionpack (= 4.2.10)
68
+ activesupport (= 4.2.10)
69
+ rake (>= 0.8.7)
70
+ thor (>= 0.18.1, < 2.0)
71
+ rake (10.5.0)
72
+ rspec-core (3.7.1)
73
+ rspec-support (~> 3.7.0)
74
+ rspec-expectations (3.7.0)
75
+ diff-lcs (>= 1.2.0, < 2.0)
76
+ rspec-support (~> 3.7.0)
77
+ rspec-mocks (3.7.0)
78
+ diff-lcs (>= 1.2.0, < 2.0)
79
+ rspec-support (~> 3.7.0)
80
+ rspec-rails (3.7.2)
81
+ actionpack (>= 3.0)
82
+ activesupport (>= 3.0)
83
+ railties (>= 3.0)
84
+ rspec-core (~> 3.7.0)
85
+ rspec-expectations (~> 3.7.0)
86
+ rspec-mocks (~> 3.7.0)
87
+ rspec-support (~> 3.7.0)
88
+ rspec-support (3.7.0)
89
+ sqlite3 (1.3.13)
90
+ thor (0.20.0)
91
+ thread_safe (0.3.6)
92
+ tzinfo (1.2.4)
93
+ thread_safe (~> 0.1)
94
+
95
+ PLATFORMS
96
+ ruby
97
+
98
+ DEPENDENCIES
99
+ has-meta!
100
+ pry
101
+ rspec-rails
102
+ sqlite3
103
+
104
+ BUNDLED WITH
105
+ 1.16.1
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2017 ProTrainings, LLC
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,169 @@
1
+ # Has-Meta
2
+ A key/value store solution for Rails apps with bloated tables
3
+
4
+ [![Build Status](https://travis-ci.org/ProTrainings/has-meta.svg?branch=master)](https://travis-ci.org/ProTrainings/has-meta)
5
+
6
+ ## Installation
7
+
8
+ Add this line to your application's Gemfile:
9
+
10
+ ```ruby
11
+ gem 'has-meta'
12
+ ```
13
+
14
+ And then execute:
15
+
16
+ $ bundle
17
+
18
+ Or install it yourself as:
19
+
20
+ $ gem install has-meta
21
+
22
+ Then, install migrations:
23
+
24
+ ```ruby
25
+ rake has_meta_engine:install:migrations
26
+ ```
27
+
28
+ Finally, review the migrations and migrate:
29
+
30
+ ```ruby
31
+ rails db:migrate
32
+ ```
33
+
34
+ ## Usage
35
+
36
+ ### Declaring Meta Attributes
37
+
38
+ Suppose we have a model `Part` and only a minority of our parts records have the attribute `catalog_number` populated. We want to move `catalog_number` to our key/value store.
39
+
40
+ To create a new meta attribute on an Active Record model, add this line to your model:
41
+
42
+ ```ruby
43
+ has_meta :catalog_number
44
+ ```
45
+
46
+ Or specify multiple meta attributes on the model:
47
+
48
+ ```ruby
49
+ has_meta :catalog_number, :other_attribute
50
+ ```
51
+
52
+ You may also choose to migrate existing data from a table:
53
+
54
+ $ rake "has_meta_engine:data_mover[parts, catalog_number, integer, catalog_number]"
55
+
56
+ Once that data is moved generate a migration to remove the column and run the migration:
57
+
58
+ $ rake generate migration RevmoveCatalogNumberFromParts catalog_number:integer
59
+ $ rake db:migrate
60
+
61
+ And finally, declare the meta attribute in the model
62
+
63
+ ```ruby
64
+ has_meta :catalog_number
65
+ ```
66
+
67
+ ### Getting and Setting Meta Attributes
68
+
69
+ Now, we can use normal getters and setters to access the attribute:
70
+
71
+ ```ruby
72
+ new_part = Part.create name: 'Fancy new part'
73
+ new_part.catalog_number = 12345
74
+ new_part.save
75
+
76
+ new_part.catalog_number
77
+ # => 12345
78
+ ```
79
+
80
+ You can update the attribute any way you would with other attributes managed by Active Record:
81
+
82
+ ```ruby
83
+ new_part.update catalog_number: 67890
84
+ new_part.catalog_number # => 67890
85
+
86
+ new_part.attributes = {catalog_number: 12345}
87
+ new_part.catalog_number # => 12345
88
+ ```
89
+
90
+ **NB**: _Declaring a meta attribute on a model creates a polymorphic relationship between the model and the MetaData model. Therefore, the parent model must be saved before assigning meta attributes._
91
+
92
+ Meta attributes may also represent an Active Record model. Perhaps some of our parts may conform to a uniform standard represented by class `Standard`. Just declare the meta attribute `:standard` and `has-meta` will treat the meta attribute as a one-to-one relation if the attribute corresponds to an Active Record model in your app.
93
+
94
+ ```ruby
95
+ has_meta :catalog_number, :standard
96
+ ```
97
+
98
+ Now you can get or set the attribute using either object or the object id as you would with any other attribute:
99
+
100
+ ```ruby
101
+ new_standard = Standard.create name: 'Some great standard'
102
+ new_part.standard = new_standard
103
+ new_part.standard # => #<Standard id: 1, name: "Some great standard">
104
+ new_part.standard_id # => 1
105
+
106
+ newer_standard = Standard.create name 'An even better standard'
107
+ new_part.standard.id = newer_standard.id
108
+ new_part.standard # => #<Standard id: 2, name: "An even better standard">
109
+ new_part.standard_id # => 2
110
+ ```
111
+
112
+ ### Finding by meta attributes
113
+
114
+ `find_by_attribute_name` methods are provided for meta attributes. For attributes representing an Active Record model, use `find_by_attribute_id`:
115
+
116
+ ```ruby
117
+ Part.find_by_catalog_number 12345
118
+ # => #<Part id: 1, name: "Fancy new part">
119
+
120
+ Part.find_by_standard_id 2
121
+ # => #<Part id: 1, name: "Fancy new part">
122
+ ```
123
+
124
+ You may also use `with_meta` method to return a scope of parts with correspoding meta attribute values:
125
+
126
+ ```ruby
127
+ another_part = Part.create name: 'Another fancy new part'
128
+ another_part.update standard: new_standard
129
+
130
+ Part.with_meta standard: new_standard
131
+ # => #<ActiveRecord::Relation [#<Part id: 1, name: "Fancy new part">,
132
+ #<Part id: 2, name: "Another fancy new part">]>
133
+ ```
134
+
135
+ `with_meta` accepts the `any: true` option to match any condition provided:
136
+
137
+ ```ruby
138
+ yet_another_part = Part.create name: 'Yet another fancy new part'
139
+ yet_another_part.update catalog_number: 12345
140
+
141
+
142
+ Part.with_meta({standard: new_standard, catalog_number: 12345}, any: true)
143
+ # => #<ActiveRecord::Relation [#<Part id: 1, name: "Fancy new part">,
144
+ #<Part id: 2, name: "Another fancy new part">,
145
+ #<Part id: 3, name: "Yet another fancy new part">]>
146
+ ```
147
+
148
+ Calling `excluding_meta` will return all records not meeting the criteria:
149
+
150
+ ```ruby
151
+ Part.excluding_meta catalog_number: 12345
152
+ # => #<ActiveRecord::Relation [#<Part id: 2, name: "Another fancy new part">]>
153
+ ```
154
+
155
+ ## TODO/Known Issues
156
+ `has-meta` was developed for Active Record 4.2+ and MySQL 5.5. PRs for supporting earlier versions of Active Record and/or PostgreSQL are welcome!
157
+
158
+ ## Contributing
159
+
160
+ Bug reports and pull requests are welcome on GitHub at https://github.com/protrainings/has-meta.
161
+
162
+ ## License
163
+
164
+ Has-Meta is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
165
+
166
+ ## About ProTrainings
167
+ Has-Meta was written by [Dan Drust](https://www.github.com/dandrust). It is maintained and funded by Protrainings, LLC. Has-Meta, names, and logos are copyright ProTrainings, LLC.
168
+
169
+
@@ -0,0 +1,8 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ import "./lib/tasks/data_mover.rake"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task :default => :spec
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "has/meta"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,26 @@
1
+ class HasMetaMigration < ActiveRecord::Migration[4.2]
2
+
3
+ def self.up
4
+ create_table :meta_data do |t|
5
+
6
+ t.references :meta_model, polymorphic: true
7
+
8
+ t.string :key
9
+ t.string :text_value
10
+ t.integer :integer_value
11
+ t.decimal :decimal_value,
12
+ :precision => 6,
13
+ :scale => 2
14
+ t.date :date_value
15
+
16
+ t.timestamps
17
+ end
18
+
19
+ add_index :meta_data, [:meta_model_type, :meta_model_id, :key]
20
+ end
21
+
22
+ def self.down
23
+ drop_table :meta_data
24
+ end
25
+
26
+ end
@@ -0,0 +1,28 @@
1
+ # Use ruby 2.5.0 with default gemset
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "has_meta/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "has-meta"
8
+ spec.version = HasMeta::VERSION
9
+ spec.authors = ["Dan Drust"]
10
+ spec.email = ["dan.drust@protrainings.com"]
11
+
12
+ spec.summary = "A key/value store solution for Rails apps with bloated tables"
13
+ spec.homepage = 'https://www.github.com/protrainings/has-meta'
14
+ spec.license = "MIT"
15
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
16
+ f.match(%r{^(test|spec|features)/})
17
+ end
18
+ spec.bindir = "exe"
19
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
20
+ spec.require_paths = ["lib"]
21
+
22
+
23
+ spec.add_dependency 'activerecord', ['>= 4.2.8']
24
+
25
+ spec.add_development_dependency 'pry'
26
+ spec.add_development_dependency 'sqlite3'
27
+ spec.add_development_dependency 'rspec-rails'
28
+ end
@@ -0,0 +1,53 @@
1
+ # require 'active_record'
2
+ # require 'active_record/version'
3
+ # require 'active_support/core_ext/module'
4
+
5
+ require 'pry'
6
+
7
+ begin
8
+ require 'rails/engine'
9
+ require 'has_meta/engine'
10
+ rescue
11
+ LoadError
12
+ end
13
+
14
+ module HasMeta
15
+
16
+ extend ActiveSupport::Autoload
17
+
18
+ autoload :MetaData
19
+ autoload :DataMover
20
+ autoload :MetaQuery
21
+ autoload :QueryMethods
22
+ autoload :DynamicMethods
23
+ autoload :InstanceMethods
24
+ autoload :VERSION
25
+
26
+ def meta_attributes
27
+ nil
28
+ end
29
+
30
+ def has_meta(*attributes)
31
+ options = attributes.pop if attributes.last.is_a? Hash
32
+ attributes = attributes.to_a.flatten.compact.map(&:to_sym)
33
+
34
+ if self.meta_attributes.present?
35
+ self.meta_attributes += attributes
36
+ else
37
+ class_attribute :meta_attributes, instance_predicate: false, instance_writer: false
38
+ self.meta_attributes = attributes
39
+ end
40
+
41
+ class_eval do
42
+ has_many :meta_data, as: :meta_model, dependent: :destroy, class_name: '::HasMeta::MetaData'
43
+ include HasMeta::InstanceMethods
44
+ include HasMeta::DynamicMethods
45
+ include HasMeta::QueryMethods
46
+ end
47
+ end
48
+
49
+ end
50
+
51
+ ActiveSupport.on_load :active_record do
52
+ extend HasMeta
53
+ end
@@ -0,0 +1,109 @@
1
+ module HasMeta
2
+ class DataMover
3
+ def initialize table, attribute, type, key
4
+ @table = table
5
+ @attribute = attribute
6
+ @key = key
7
+ @type = type
8
+ resolve_type!
9
+ end
10
+
11
+ def execute
12
+
13
+ insert = Arel::Nodes::InsertStatement.new
14
+ insert.relation = destination_table
15
+ insert.columns = destination_columns
16
+ insert.values = Arel::Nodes::SqlLiteral.new source_values
17
+ # There seems to be a bug with earlier versions of Arel where multiple values aren't supported
18
+ # insert.select = source_select.where(source_conditions)
19
+
20
+ ActiveRecord::Base.connection.execute insert.to_sql if migrateable?
21
+
22
+ self
23
+ end
24
+
25
+ private
26
+
27
+ attr_accessor :abort
28
+ attr_reader :table, :attribute, :key, :type
29
+
30
+ def resolve_type!
31
+ @type = 'text' if @type == 'string'
32
+ end
33
+
34
+ def source_values
35
+ migrateable_source_values
36
+ .reduce(" VALUES ") {|acc, row| acc += format_values row}[0...-1]
37
+ end
38
+
39
+ def migrateable_source_values
40
+ if type.to_s == 'text'
41
+ source_model.where.not(:"#{attribute}" => [nil, ''])
42
+ else
43
+ source_model.where(source_table[:"#{attribute}"].not_eq(nil))
44
+ end
45
+ end
46
+
47
+ def migrateable?
48
+ migrateable_source_values.present?
49
+ end
50
+
51
+ def format_values row
52
+ "('#{table.classify.constantize.name}', #{row.id}, '#{key}', #{escape(row.send(:"#{attribute}"))}, #{timestamp}),"
53
+ end
54
+
55
+ def escape value
56
+ if ['text', 'date'].include? type.to_s
57
+ "'#{value}'"
58
+ else
59
+ value
60
+ end
61
+ end
62
+
63
+ def timestamp
64
+ case ::ActiveRecord::Base.connection.adapter_name
65
+ when 'Mysql2'
66
+ 'NOW()'
67
+ when 'SQLite'
68
+ "'#{Time.now}'"
69
+ end
70
+ end
71
+
72
+ def source_model
73
+ table.classify.constantize
74
+ end
75
+
76
+ def source_table
77
+ @source_table ||= Arel::Table.new(table)
78
+ end
79
+
80
+ def source_select
81
+ source_table.project(
82
+ Arel.sql("\'#{table.classify.constantize.name}\'"), # :meta_model_type
83
+ source_table[:id], # :meta_model_id
84
+ Arel.sql("\'#{key}\'"), # :key
85
+ source_table[:"#{attribute}"], # :integer_value (ex.)
86
+ Arel::Nodes::NamedFunction.new("NOW", []) # :created_at
87
+ )
88
+ end
89
+
90
+ def source_conditions
91
+ source_table[:"#{attribute}"].not_in([nil, ''])
92
+ end
93
+
94
+ def destination_table
95
+ @destination_table ||= HasMeta::MetaData.arel_table
96
+ end
97
+
98
+ def destination_columns
99
+ [
100
+ destination_table[:meta_model_type],
101
+ destination_table[:meta_model_id],
102
+ destination_table[:key],
103
+ destination_table[:"#{type}_value"],
104
+ destination_table[:created_at],
105
+ ]
106
+ end
107
+
108
+ end
109
+ end
@@ -0,0 +1,80 @@
1
+ module HasMeta
2
+ module DynamicMethods
3
+ def self.included base
4
+ base.extend ClassMethods
5
+ end
6
+
7
+ #TODO: refactor this
8
+ def respond_to? method, include_private=false
9
+ attribute = self.meta_attributes.select { |x| method.match /^#{x}(_id)?=?$/ }.pop
10
+ if attribute
11
+ self.class.find_object_from(attribute) ? true : !method.match(/^#{attribute}=?$/).nil?
12
+ else
13
+ super
14
+ end
15
+ end
16
+
17
+ def method_missing method, *args, &block
18
+ attribute = self.meta_attributes.select { |x| method.match /^#{x}(_id)?=?$/ }.pop
19
+ if attribute
20
+ object = self.class.find_object_from attribute
21
+ if method =~ /=$/ # setter
22
+ object ? meta_set!(:"#{attribute}_id", args.first.try(:id) || args.first) : meta_set!(attribute, args.first)
23
+ else # getter
24
+ if object
25
+ method =~ /_id$/ ? meta_get(:"#{attribute}_id") : object.find_by_id(meta_get(:"#{attribute}_id"))
26
+ else
27
+ meta_get(attribute)
28
+ end
29
+ end
30
+ else
31
+ super
32
+ end
33
+ end
34
+
35
+ module ClassMethods
36
+
37
+ def respond_to? method, include_private=false
38
+ attribute = self.meta_attributes.select { |x| method.match(/(?<=^find_by_)#{x}(?=$|(?=_id$))/) }.pop
39
+ if attribute
40
+ find_object_from(attribute) ? !method.match(/_id$/).nil? : !method.match(/#{attribute}$/).nil?
41
+ else
42
+ super
43
+ end
44
+ end
45
+
46
+ def method_missing method, *args, &block
47
+ # TODO: refactor this to not be as cluttery and dense
48
+ attribute = self.meta_attributes.select { |x| method.match /(?<=^find_by_)#{x}(?=$|(?=_id$))/ }.pop
49
+ if attribute
50
+ object = find_object_from(attribute)
51
+ if object and method =~ /_id$/
52
+ conditions = {key: "#{attribute}_id", meta_model_type: self}.
53
+ merge! MetaData.generate_value_hash(args.first)
54
+ MetaData.where(conditions).map do |x|
55
+ self.find_by_id(x.meta_model_id)
56
+ end
57
+ elsif !object
58
+ conditions = {key: "#{attribute}", meta_model_type: self}.
59
+ merge! MetaData.generate_value_hash(args.first)
60
+ MetaData.where(conditions).map do |x|
61
+ self.find_by_id(x.meta_model_id)
62
+ end
63
+ else
64
+ super
65
+ end
66
+ else
67
+ super
68
+ end
69
+ end
70
+
71
+ def find_object_from attribute
72
+ begin
73
+ attribute.to_s.classify.constantize
74
+ rescue
75
+ nil
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,4 @@
1
+ module HasMeta
2
+ class Engine < Rails::Engine
3
+ end
4
+ end
@@ -0,0 +1,34 @@
1
+ module HasMeta
2
+ module InstanceMethods
3
+
4
+ def meta_get key
5
+ self.meta_data.where(key: key.to_s).try(:first).try(:value)
6
+ end
7
+
8
+ def meta_set key, value
9
+ return meta_destroy key if value.nil? or value == ''
10
+
11
+ meta = self.meta_data.where(key: key.to_s).first_or_create
12
+ meta.value = value
13
+ meta
14
+ end
15
+
16
+ def meta_set! key, value
17
+ meta_set(key, value).save
18
+ end
19
+
20
+ def meta_destroy key
21
+ self.meta_data.where(key: key).destroy_all
22
+ end
23
+
24
+ # TODO: Test these
25
+ def list_meta_keys
26
+ self.meta_data.pluck(:keys).uniq
27
+ end
28
+
29
+ def remove_meta(key)
30
+ self.send(key.to_sym, nil)
31
+ end
32
+
33
+ end
34
+ end
@@ -0,0 +1,92 @@
1
+ module HasMeta
2
+ class MetaData < ::ActiveRecord::Base
3
+
4
+ belongs_to :meta_model, polymorphic: true
5
+
6
+ attr_accessor :value
7
+ attr_reader :data_type
8
+
9
+ def value
10
+ @value ||= value_attributes.compact.values.pop
11
+ end
12
+
13
+ def value= value
14
+ @data_type, @value = resolve_data_type! value
15
+ set_attribute
16
+ end
17
+
18
+ def self.generate_value_hash *values
19
+ # values = 'some text'
20
+ # values = ['some text', 'some other text']
21
+ # values = ['some text', 'some other text', 9]
22
+ if values.size == 1
23
+ # {text_value: 'some text'}
24
+ value_hash_for values.pop
25
+ else
26
+ # ['some text', 'some other text', 9]
27
+ # [{text_value: 'some text'}, {text_value: 'some other text'}, {integer_value: 9}]
28
+ # {:text_value=>[{:text_value=>"some text"}, {:text_value=>"some other text"}], :integer_value=>[{:integer_value=>9}]}
29
+ # values.map { |value| value_hash_for value }.group_by { |x| x.keys.first }.map {|k, v| {k => v.map {|x| x.values.first}}}
30
+ values
31
+ .map { |value| value_hash_for value }
32
+ .group_by { |x| x.keys.first }
33
+ .reduce({}) do |acc, hash|
34
+ key, value = *hash
35
+ acc.merge({key => value.size == 1 ? value.first.values.first : value.map {|x| x.values.first}})
36
+ end
37
+ # .map {|k, v| {k => v.map {|x| x.values.first}}}
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def self.value_hash_for value
44
+ data_type, value = resolve_data_type! value
45
+ {"#{data_type}_value": value}
46
+ end
47
+
48
+ def self.resolve_data_type! value
49
+ case value
50
+ when ->(x) {x.kind_of? Integer}
51
+ # TODO: dynamically check for a range error (is this a ruby thing or mysql thing?)
52
+ if value < 2000000000
53
+ return :integer, value
54
+ else
55
+ return :text, value.to_s
56
+ end
57
+ when ->(x) {x.kind_of? Float}
58
+ return :decimal, value
59
+ when ->(x) {x.kind_of? Date}
60
+ return :date, value
61
+ when ->(x) {x.respond_to? :id}
62
+ return :integer, value.id
63
+ else
64
+ return :integer, value.to_i if value =~ /^-?\d+$/
65
+ return :decimal, value.to_f if value =~ /^-?\d*\.\d+$/
66
+ return :text, value
67
+ end
68
+ end
69
+
70
+ def resolve_data_type! value
71
+ self.class.resolve_data_type! value
72
+ end
73
+
74
+ def value_attributes
75
+ self.attributes.select do |k, _|
76
+ k =~ /_value/
77
+ end
78
+ end
79
+
80
+ def reset_values
81
+ value_attributes.keys.each do |attribute|
82
+ self[attribute] = nil
83
+ end
84
+ end
85
+
86
+ def set_attribute
87
+ reset_values
88
+ self[:"#{@data_type}_value"] = @value
89
+ end
90
+
91
+ end
92
+ end
@@ -0,0 +1,85 @@
1
+ module HasMeta
2
+ class MetaQuery
3
+ def initialize meta_model, meta_data, conditions, options={}
4
+ @meta_model = meta_model
5
+ @meta_data = meta_data
6
+ @conditions = conditions
7
+ @options = options
8
+ end
9
+
10
+ def build
11
+ @meta_model
12
+ .joins(for_each_meta_key)
13
+ .where(conditions_for_keys_and_values)
14
+ end
15
+
16
+ private
17
+
18
+ attr_reader :meta_model, :meta_data, :conditions, :options
19
+
20
+ def meta_model_arel_table
21
+ @meta_model_arel_table ||= @meta_model.arel_table
22
+ end
23
+
24
+ def meta_data_arel_table
25
+ @meta_data_arel_table ||= @meta_data.arel_table
26
+ end
27
+
28
+ def meta_data_aliases
29
+ @meta_data_aliases = @conditions.keys
30
+ .map.with_index do |key, i|
31
+ {key => meta_data_arel_table.alias("#{key}_join")}
32
+ end
33
+ end
34
+
35
+ def for_each_meta_key
36
+ meta_data_aliases.reduce(meta_model_arel_table) { |acc, meta_data_alias_hash|
37
+ key, meta_data_alias = *meta_data_alias_hash.first
38
+
39
+ acc
40
+ .join(meta_data_alias, join_type)
41
+ .on(on_conditions meta_data_alias, key)
42
+ }.join_sources
43
+ end
44
+
45
+ def join_type
46
+ if options[:exclude] or options[:any]
47
+ Arel::Nodes::OuterJoin
48
+ else
49
+ Arel::Nodes::InnerJoin
50
+ end
51
+ end
52
+
53
+ def on_conditions table_alias, key
54
+ type_condition = table_alias[:meta_model_type].eq(meta_model)
55
+ id_condition = table_alias[:meta_model_id].eq(meta_model_arel_table[:id])
56
+ key_condition = table_alias[:key].eq(resolve_key key)
57
+
58
+ type_condition.and(id_condition).and(key_condition)
59
+ end
60
+
61
+ def conditions_for_keys_and_values
62
+ @conditions.values.map.with_index do |values, i|
63
+
64
+ conditions_for_values(meta_data_aliases[i].values.pop, values)
65
+ .reduce { |acc, x| acc.or(x) }
66
+
67
+ end
68
+ .reduce { |acc, x| options[:any] ? acc.or(x) : acc.and(x) }
69
+ end
70
+
71
+ def resolve_key key
72
+ meta_model.find_object_from(key) ? "#{key}_id" : key
73
+ end
74
+
75
+ def conditions_for_values meta_data_alias, values
76
+ MetaData.generate_value_hash(*values).map do |column, value|
77
+ if options[:exclude]
78
+ meta_data_alias[column].not_in(value)
79
+ else
80
+ meta_data_alias[column].in(value)
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,18 @@
1
+ module HasMeta
2
+ module QueryMethods
3
+ def self.included base
4
+ base.extend ClassMethods
5
+ end
6
+
7
+ module ClassMethods
8
+
9
+ def with_meta args=nil, options={}
10
+ HasMeta::MetaQuery.new(self, MetaData, args, options).build
11
+ end
12
+
13
+ def excluding_meta args=nil, options={}
14
+ HasMeta::MetaQuery.new(self, MetaData, args, options.merge(exclude: true)).build
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,3 @@
1
+ module HasMeta
2
+ VERSION = "0.8.0"
3
+ end
@@ -0,0 +1,10 @@
1
+ namespace :has_meta_engine do
2
+
3
+ desc 'Move data from existing table to meta_data table'
4
+ task :data_mover, [:table, :attribute, :type, :key] => [:environment] do |t, args|
5
+
6
+ HasMeta::DataMover.new(*args).execute
7
+
8
+ end
9
+
10
+ end
metadata ADDED
@@ -0,0 +1,123 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: has-meta
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.8.0
5
+ platform: ruby
6
+ authors:
7
+ - Dan Drust
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2018-02-02 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activerecord
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 4.2.8
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 4.2.8
27
+ - !ruby/object:Gem::Dependency
28
+ name: pry
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: sqlite3
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec-rails
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ description:
70
+ email:
71
+ - dan.drust@protrainings.com
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - ".gitignore"
77
+ - ".rspec"
78
+ - ".rvmrc"
79
+ - ".travis.yml"
80
+ - Gemfile
81
+ - Gemfile.lock
82
+ - LICENSE.txt
83
+ - README.md
84
+ - Rakefile
85
+ - bin/console
86
+ - bin/setup
87
+ - db/migrate/1_has_meta_migration.rb
88
+ - has-meta.gemspec
89
+ - lib/has-meta.rb
90
+ - lib/has_meta/data_mover.rb
91
+ - lib/has_meta/dynamic_methods.rb
92
+ - lib/has_meta/engine.rb
93
+ - lib/has_meta/instance_methods.rb
94
+ - lib/has_meta/meta_data.rb
95
+ - lib/has_meta/meta_query.rb
96
+ - lib/has_meta/query_methods.rb
97
+ - lib/has_meta/version.rb
98
+ - lib/tasks/data_mover.rake
99
+ homepage: https://www.github.com/protrainings/has-meta
100
+ licenses:
101
+ - MIT
102
+ metadata: {}
103
+ post_install_message:
104
+ rdoc_options: []
105
+ require_paths:
106
+ - lib
107
+ required_ruby_version: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - ">="
110
+ - !ruby/object:Gem::Version
111
+ version: '0'
112
+ required_rubygems_version: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: '0'
117
+ requirements: []
118
+ rubyforge_project:
119
+ rubygems_version: 2.7.3
120
+ signing_key:
121
+ specification_version: 4
122
+ summary: A key/value store solution for Rails apps with bloated tables
123
+ test_files: []