dynamic-active-model 1.1.0 → 1.2.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/bin/dynamic-db-explorer +7 -1
- data/lib/dynamic-active-model/associations.rb +8 -0
- data/lib/dynamic-active-model/explorer.rb +11 -5
- data/lib/dynamic-active-model/has_many_through.rb +85 -0
- data/lib/dynamic-active-model/setup.rb +12 -1
- data/lib/dynamic-active-model/template_class_file.rb +20 -1
- data/lib/dynamic-active-model/version.rb +1 -1
- data/lib/dynamic-active-model.rb +3 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b13a9d41753ab707e9a414bb36ffd043c4362d09764717f4b43770ba8c0e0731
|
|
4
|
+
data.tar.gz: 2ed3a9b28b4e9a0825b19acf97f9b95deb8cd99bf312ed6006112f8bbd2573d1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 86c16133679c95f1e79a24745f2d474c5b5645aadcd6609dcddfe0a347e0545ef618a24ee96b9d674f742b75d03fa0cc90c964400c4ca3e8c89019de8988214a
|
|
7
|
+
data.tar.gz: acb55a31da056e1f4d5b5d816a61cd45464262b7b6bf4df84b9f56e3400e4d0767561c933e8bce43bc3718e4de7088bd108fa5124a6ecb8f66ec4ef03d9b3134
|
data/bin/dynamic-db-explorer
CHANGED
|
@@ -70,6 +70,10 @@ OptionParser.new do |opts|
|
|
|
70
70
|
options[:foreign_key_constraints] = true
|
|
71
71
|
end
|
|
72
72
|
|
|
73
|
+
opts.on('--has-many-through', 'Also add has_many :through across join models') do
|
|
74
|
+
options[:has_many_through] = true
|
|
75
|
+
end
|
|
76
|
+
|
|
73
77
|
opts.on('-c', '--config FILE', "config file, default #{DEFAULT_CONFIG_FILE}") do |v|
|
|
74
78
|
options[:config_file] = v
|
|
75
79
|
end
|
|
@@ -96,6 +100,7 @@ if options[:config_file]
|
|
|
96
100
|
options[:skip_tables] = config.delete('skip_tables') || []
|
|
97
101
|
options[:relationships] = config.delete('relationships') || {}
|
|
98
102
|
options[:foreign_key_constraints] ||= config.delete('foreign_key_constraints') || false
|
|
103
|
+
options[:has_many_through] ||= config.delete('has_many_through') || false
|
|
99
104
|
if (module_name = config.delete('module'))
|
|
100
105
|
raise ArgumentError, "Invalid module name: #{module_name}" unless module_name.match?(/\A[A-Z][A-Za-z0-9_]*\z/)
|
|
101
106
|
|
|
@@ -110,7 +115,8 @@ DYNAMIC_DATABASE = DynamicActiveModel::Explorer.explore(
|
|
|
110
115
|
options[:connection_options],
|
|
111
116
|
options[:skip_tables],
|
|
112
117
|
options[:relationships],
|
|
113
|
-
foreign_key_constraints: options[:foreign_key_constraints] || false
|
|
118
|
+
foreign_key_constraints: options[:foreign_key_constraints] || false,
|
|
119
|
+
has_many_through: options[:has_many_through] || false
|
|
114
120
|
)
|
|
115
121
|
|
|
116
122
|
if options[:class_files_dir]
|
|
@@ -60,6 +60,12 @@ module DynamicActiveModel
|
|
|
60
60
|
@constraints = ForeignKeyConstraints.new(@database)
|
|
61
61
|
end
|
|
62
62
|
|
|
63
|
+
# Also adds has_many :through associations across join models (see HasManyThrough)
|
|
64
|
+
# @return [void]
|
|
65
|
+
def use_has_many_through!
|
|
66
|
+
@has_many_through = true
|
|
67
|
+
end
|
|
68
|
+
|
|
63
69
|
# Builds all relationships between models based on foreign keys and constraints
|
|
64
70
|
# This method:
|
|
65
71
|
# 1. Maps foreign keys to their corresponding models
|
|
@@ -80,6 +86,8 @@ module DynamicActiveModel
|
|
|
80
86
|
end.compact
|
|
81
87
|
add_has_and_belongs_to_many(join_table_model, models) if models.size == 2
|
|
82
88
|
end
|
|
89
|
+
|
|
90
|
+
HasManyThrough.new(@database.models, table_indexes).build! if @has_many_through
|
|
83
91
|
end
|
|
84
92
|
|
|
85
93
|
private
|
|
@@ -14,8 +14,9 @@ module DynamicActiveModel
|
|
|
14
14
|
# skip_tables = ['temporary_data', 'audit_logs']
|
|
15
15
|
# DynamicActiveModel::Explorer.explore(DB, database_config, skip_tables)
|
|
16
16
|
#
|
|
17
|
-
# @example With Foreign Key Constraints
|
|
18
|
-
# DynamicActiveModel::Explorer.explore(DB, database_config, foreign_key_constraints: true
|
|
17
|
+
# @example With Foreign Key Constraints and has_many :through
|
|
18
|
+
# DynamicActiveModel::Explorer.explore(DB, database_config, foreign_key_constraints: true,
|
|
19
|
+
# has_many_through: true)
|
|
19
20
|
#
|
|
20
21
|
# @example With Custom Relationships
|
|
21
22
|
# relationships = {
|
|
@@ -35,13 +36,16 @@ module DynamicActiveModel
|
|
|
35
36
|
# @param parent_class [Class, nil] Optional superclass for the base class (see Factory)
|
|
36
37
|
# @param foreign_key_constraints [Boolean] Also relate columns through the database's
|
|
37
38
|
# foreign key constraints (see ForeignKeyConstraints)
|
|
39
|
+
# @param has_many_through [Boolean] Also add has_many :through across join models
|
|
40
|
+
# (see HasManyThrough)
|
|
38
41
|
# @return [Database] The configured database instance
|
|
39
42
|
# @raise [ClassNameConflict] If two tables map to the same class name
|
|
40
43
|
def self.explore(base_module, connection_options, skip_tables = [], relationships = {}, table_class_names = {},
|
|
41
|
-
parent_class: nil, foreign_key_constraints: false)
|
|
44
|
+
parent_class: nil, foreign_key_constraints: false, has_many_through: false)
|
|
42
45
|
database = create_models!(base_module, connection_options, skip_tables, table_class_names,
|
|
43
46
|
parent_class: parent_class)
|
|
44
|
-
build_relationships!(database, relationships, foreign_key_constraints: foreign_key_constraints
|
|
47
|
+
build_relationships!(database, relationships, foreign_key_constraints: foreign_key_constraints,
|
|
48
|
+
has_many_through: has_many_through)
|
|
45
49
|
database
|
|
46
50
|
end
|
|
47
51
|
|
|
@@ -73,10 +77,12 @@ module DynamicActiveModel
|
|
|
73
77
|
# @param database [Database] The database instance containing the models
|
|
74
78
|
# @param relationships [Hash] Custom foreign key relationships to add
|
|
75
79
|
# @param foreign_key_constraints [Boolean] Also relate columns through foreign key constraints
|
|
80
|
+
# @param has_many_through [Boolean] Also add has_many :through across join models
|
|
76
81
|
# @return [void]
|
|
77
|
-
def self.build_relationships!(database, relationships, foreign_key_constraints: false)
|
|
82
|
+
def self.build_relationships!(database, relationships, foreign_key_constraints: false, has_many_through: false)
|
|
78
83
|
relations = Associations.new(database)
|
|
79
84
|
relations.use_foreign_key_constraints! if foreign_key_constraints
|
|
85
|
+
relations.use_has_many_through! if has_many_through
|
|
80
86
|
relationships.each do |table_name, foreign_keys|
|
|
81
87
|
foreign_keys.each do |foreign_key, relationship_name|
|
|
82
88
|
relations.add_foreign_key(table_name, foreign_key, relationship_name)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DynamicActiveModel
|
|
4
|
+
# Adds has_many :through associations across join models. A join model is a
|
|
5
|
+
# table with a primary key and a unique index on exactly two columns, each a
|
|
6
|
+
# belongs_to to a different model; other columns (timestamps, audit columns,
|
|
7
|
+
# extra data) don't matter. For user_roles with a unique (user_id, role_id):
|
|
8
|
+
#
|
|
9
|
+
# User has_many :roles, through: :user_roles, source: :role
|
|
10
|
+
# Role has_many :users, through: :user_roles, source: :user
|
|
11
|
+
#
|
|
12
|
+
# A name already taken on the model (association, method or column) is skipped.
|
|
13
|
+
# Tables without a primary key are has_and_belongs_to_many join tables instead.
|
|
14
|
+
class HasManyThrough
|
|
15
|
+
# @param models [Array<Class>] Models with their belongs_to/has_many already built
|
|
16
|
+
# @param table_indexes [Hash] Indexes by table name (see Associations#table_indexes)
|
|
17
|
+
def initialize(models, table_indexes)
|
|
18
|
+
@models = models
|
|
19
|
+
@table_indexes = table_indexes
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @return [void]
|
|
23
|
+
def build!
|
|
24
|
+
@models.each { |model| add_through_associations(model) }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
# @param join_model [Class]
|
|
30
|
+
def add_through_associations(join_model)
|
|
31
|
+
pair = join_belongs_to(join_model)
|
|
32
|
+
return unless pair
|
|
33
|
+
|
|
34
|
+
first, second = pair
|
|
35
|
+
add_through(join_model, first, second)
|
|
36
|
+
add_through(join_model, second, first)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# @param model [Class]
|
|
40
|
+
# @return [Array<ActiveRecord::Reflection::BelongsToReflection>, nil] The two
|
|
41
|
+
# belongs_to a unique two-column index covers, if the model is a join model
|
|
42
|
+
def join_belongs_to(model)
|
|
43
|
+
return unless model.primary_key.is_a?(String)
|
|
44
|
+
|
|
45
|
+
@table_indexes.fetch(model.table_name, []).each do |index|
|
|
46
|
+
next unless index.unique && index.columns.is_a?(Array) && index.columns.size == 2
|
|
47
|
+
|
|
48
|
+
reflections = index.columns.map { |column| belongs_to_for(model, column) }
|
|
49
|
+
return reflections if reflections.all? && reflections.map(&:klass).uniq.size == 2
|
|
50
|
+
end
|
|
51
|
+
nil
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# @return [ActiveRecord::Reflection::BelongsToReflection, nil]
|
|
55
|
+
def belongs_to_for(model, column)
|
|
56
|
+
model.reflect_on_all_associations(:belongs_to).find { |reflection| reflection.foreign_key.to_s == column }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Gives the owner side of the join a has_many :through to the target side
|
|
60
|
+
# @param join_model [Class]
|
|
61
|
+
# @param owner [ActiveRecord::Reflection::BelongsToReflection] e.g. user_roles.user
|
|
62
|
+
# @param target [ActiveRecord::Reflection::BelongsToReflection] e.g. user_roles.role
|
|
63
|
+
def add_through(join_model, owner, target)
|
|
64
|
+
through = join_association(owner.klass, join_model, owner.foreign_key.to_s)
|
|
65
|
+
name = target.name.to_s.pluralize.to_sym
|
|
66
|
+
return if through.nil? || taken?(owner.klass, name)
|
|
67
|
+
|
|
68
|
+
owner.klass.has_many(name, through: through.name, source: target.name)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# @return [ActiveRecord::Reflection::HasManyReflection, nil] The owner's has_many
|
|
72
|
+
# to the join model over that foreign key, e.g. User has_many :user_roles
|
|
73
|
+
def join_association(owner_model, join_model, foreign_key)
|
|
74
|
+
owner_model.reflect_on_all_associations(:has_many).find do |reflection|
|
|
75
|
+
reflection.options[:class_name] == join_model.name && reflection.options[:foreign_key].to_s == foreign_key
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# @return [Boolean] Whether the name is already an association, method or column
|
|
80
|
+
def taken?(model, name)
|
|
81
|
+
!model.reflect_on_association(name).nil? || model.method_defined?(name) ||
|
|
82
|
+
model.column_names.include?(name.to_s)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -54,6 +54,7 @@ module DynamicActiveModel
|
|
|
54
54
|
connection_options: nil,
|
|
55
55
|
parent_class: nil,
|
|
56
56
|
foreign_key_constraints: false,
|
|
57
|
+
has_many_through: false,
|
|
57
58
|
skip_tables: [],
|
|
58
59
|
relationships: {},
|
|
59
60
|
table_class_names: {},
|
|
@@ -94,6 +95,15 @@ module DynamicActiveModel
|
|
|
94
95
|
dynamic_active_model_config[:foreign_key_constraints]
|
|
95
96
|
end
|
|
96
97
|
|
|
98
|
+
# Sets or gets whether has_many :through associations are added across join
|
|
99
|
+
# models (see HasManyThrough)
|
|
100
|
+
# @param enabled [Boolean, nil]
|
|
101
|
+
# @return [Boolean] The current setting
|
|
102
|
+
def has_many_through(enabled = nil) # rubocop:disable Naming/PredicatePrefix -- DSL setter/getter
|
|
103
|
+
update_config(:has_many_through, enabled) unless enabled.nil?
|
|
104
|
+
dynamic_active_model_config[:has_many_through]
|
|
105
|
+
end
|
|
106
|
+
|
|
97
107
|
# Sets or gets the list of tables to skip
|
|
98
108
|
# @param tables [Array<String>, nil] Tables to skip
|
|
99
109
|
# @return [Array<String>] The current list of skipped tables
|
|
@@ -176,7 +186,8 @@ module DynamicActiveModel
|
|
|
176
186
|
relationships,
|
|
177
187
|
table_class_names,
|
|
178
188
|
parent_class: parent_class,
|
|
179
|
-
foreign_key_constraints: foreign_key_constraints
|
|
189
|
+
foreign_key_constraints: foreign_key_constraints,
|
|
190
|
+
has_many_through: has_many_through
|
|
180
191
|
)
|
|
181
192
|
)
|
|
182
193
|
database.update_all_models(extensions_path, extensions_suffix) if extensions_path
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
|
|
3
5
|
module DynamicActiveModel
|
|
4
6
|
# The TemplateClassFile class generates Ruby source files for ActiveRecord models.
|
|
5
7
|
# It creates properly formatted class definitions that include:
|
|
@@ -26,11 +28,13 @@ module DynamicActiveModel
|
|
|
26
28
|
@model = model
|
|
27
29
|
end
|
|
28
30
|
|
|
29
|
-
# Creates a Ruby source file for the model
|
|
31
|
+
# Creates a Ruby source file for the model, at dir/<namespace>/<model>.rb
|
|
30
32
|
# @param dir [String] Directory to create the file in
|
|
31
33
|
# @return [void]
|
|
32
34
|
def create_template!(dir)
|
|
33
35
|
file = "#{dir}/#{@model.name.underscore}.rb"
|
|
36
|
+
# the namespace directory (e.g. db/ for DB::User) may not exist yet
|
|
37
|
+
FileUtils.mkdir_p(File.dirname(file))
|
|
34
38
|
File.binwrite(file, to_s)
|
|
35
39
|
end
|
|
36
40
|
|
|
@@ -44,6 +48,7 @@ module DynamicActiveModel
|
|
|
44
48
|
all_has_many_relationships.each do |assoc|
|
|
45
49
|
append_association!(str, 'has_many', assoc, has_many_association_options(assoc))
|
|
46
50
|
end
|
|
51
|
+
all_has_many_through_relationships.each { |assoc| append_through_association!(str, assoc) }
|
|
47
52
|
all_belongs_to_relationships.each do |assoc|
|
|
48
53
|
append_association!(str, 'belongs_to', assoc, belongs_to_association_options(assoc))
|
|
49
54
|
end
|
|
@@ -66,6 +71,12 @@ module DynamicActiveModel
|
|
|
66
71
|
@model.reflect_on_all_associations.grep(ActiveRecord::Reflection::HasManyReflection)
|
|
67
72
|
end
|
|
68
73
|
|
|
74
|
+
# Gets all has_many :through relationships for the model
|
|
75
|
+
# @return [Array] List of has_many :through associations
|
|
76
|
+
def all_has_many_through_relationships
|
|
77
|
+
@model.reflect_on_all_associations(:has_many).grep(ActiveRecord::Reflection::ThroughReflection)
|
|
78
|
+
end
|
|
79
|
+
|
|
69
80
|
# Gets all belongs_to relationships for the model
|
|
70
81
|
# @return [Array<ActiveRecord::Reflection::BelongsToReflection>]
|
|
71
82
|
def all_belongs_to_relationships
|
|
@@ -84,6 +95,14 @@ module DynamicActiveModel
|
|
|
84
95
|
@model.reflect_on_all_associations.grep(ActiveRecord::Reflection::HasAndBelongsToManyReflection)
|
|
85
96
|
end
|
|
86
97
|
|
|
98
|
+
# Appends a has_many :through declaration; through and source are symbols
|
|
99
|
+
# @param str [String] The source string being built
|
|
100
|
+
# @param assoc [ActiveRecord::Reflection::ThroughReflection] The association to add
|
|
101
|
+
def append_through_association!(str, assoc)
|
|
102
|
+
str << " has_many #{assoc.name.inspect}, through: #{assoc.options[:through].to_sym.inspect}, " \
|
|
103
|
+
"source: #{assoc.options[:source].to_sym.inspect}\n"
|
|
104
|
+
end
|
|
105
|
+
|
|
87
106
|
# Appends an association definition to the source string
|
|
88
107
|
# @param str [String] The source string being built
|
|
89
108
|
# @param assoc_type [String] The association macro, e.g. 'has_many'
|
data/lib/dynamic-active-model.rb
CHANGED
|
@@ -40,6 +40,9 @@ module DynamicActiveModel
|
|
|
40
40
|
# Resolves the database's foreign key constraints to models
|
|
41
41
|
autoload :ForeignKeyConstraints, 'dynamic-active-model/foreign_key_constraints'
|
|
42
42
|
|
|
43
|
+
# Adds has_many :through associations across join models
|
|
44
|
+
autoload :HasManyThrough, 'dynamic-active-model/has_many_through'
|
|
45
|
+
|
|
43
46
|
# Manages automatic discovery and setup of model relationships
|
|
44
47
|
autoload :Associations, 'dynamic-active-model/associations'
|
|
45
48
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: dynamic-active-model
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Doug Youch
|
|
@@ -57,6 +57,7 @@ files:
|
|
|
57
57
|
- lib/dynamic-active-model/factory.rb
|
|
58
58
|
- lib/dynamic-active-model/foreign_key.rb
|
|
59
59
|
- lib/dynamic-active-model/foreign_key_constraints.rb
|
|
60
|
+
- lib/dynamic-active-model/has_many_through.rb
|
|
60
61
|
- lib/dynamic-active-model/setup.rb
|
|
61
62
|
- lib/dynamic-active-model/template_class_file.rb
|
|
62
63
|
- lib/dynamic-active-model/version.rb
|