dynamic-active-model 1.0.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 +13 -1
- data/lib/dynamic-active-model/associations.rb +55 -19
- data/lib/dynamic-active-model/explorer.rb +16 -3
- data/lib/dynamic-active-model/foreign_key_constraints.rb +66 -0
- data/lib/dynamic-active-model/has_many_through.rb +85 -0
- data/lib/dynamic-active-model/setup.rb +23 -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 +6 -0
- metadata +3 -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
|
@@ -66,6 +66,14 @@ OptionParser.new do |opts|
|
|
|
66
66
|
options[:id_suffix] = v
|
|
67
67
|
end
|
|
68
68
|
|
|
69
|
+
opts.on('--foreign-key-constraints', 'Also relate columns through foreign key constraints') do
|
|
70
|
+
options[:foreign_key_constraints] = true
|
|
71
|
+
end
|
|
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
|
+
|
|
69
77
|
opts.on('-c', '--config FILE', "config file, default #{DEFAULT_CONFIG_FILE}") do |v|
|
|
70
78
|
options[:config_file] = v
|
|
71
79
|
end
|
|
@@ -91,6 +99,8 @@ if options[:config_file]
|
|
|
91
99
|
config = YAML.safe_load_file(options[:config_file], permitted_classes: [Symbol])[options[:config_section]]
|
|
92
100
|
options[:skip_tables] = config.delete('skip_tables') || []
|
|
93
101
|
options[:relationships] = config.delete('relationships') || {}
|
|
102
|
+
options[:foreign_key_constraints] ||= config.delete('foreign_key_constraints') || false
|
|
103
|
+
options[:has_many_through] ||= config.delete('has_many_through') || false
|
|
94
104
|
if (module_name = config.delete('module'))
|
|
95
105
|
raise ArgumentError, "Invalid module name: #{module_name}" unless module_name.match?(/\A[A-Z][A-Za-z0-9_]*\z/)
|
|
96
106
|
|
|
@@ -104,7 +114,9 @@ DYNAMIC_DATABASE = DynamicActiveModel::Explorer.explore(
|
|
|
104
114
|
options[:base_module],
|
|
105
115
|
options[:connection_options],
|
|
106
116
|
options[:skip_tables],
|
|
107
|
-
options[:relationships]
|
|
117
|
+
options[:relationships],
|
|
118
|
+
foreign_key_constraints: options[:foreign_key_constraints] || false,
|
|
119
|
+
has_many_through: options[:has_many_through] || false
|
|
108
120
|
)
|
|
109
121
|
|
|
110
122
|
if options[:class_files_dir]
|
|
@@ -53,6 +53,19 @@ module DynamicActiveModel
|
|
|
53
53
|
.add(foreign_key, relationship_name)
|
|
54
54
|
end
|
|
55
55
|
|
|
56
|
+
# Relates columns through the database's foreign key constraints as well as by
|
|
57
|
+
# naming convention. A column with a constraint follows the constraint.
|
|
58
|
+
# @return [void]
|
|
59
|
+
def use_foreign_key_constraints!
|
|
60
|
+
@constraints = ForeignKeyConstraints.new(@database)
|
|
61
|
+
end
|
|
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
|
+
|
|
56
69
|
# Builds all relationships between models based on foreign keys and constraints
|
|
57
70
|
# This method:
|
|
58
71
|
# 1. Maps foreign keys to their corresponding models
|
|
@@ -64,15 +77,7 @@ module DynamicActiveModel
|
|
|
64
77
|
foreign_key_to_models = create_foreign_key_to_model_map
|
|
65
78
|
|
|
66
79
|
@database.models.each do |model|
|
|
67
|
-
model.column_names.each
|
|
68
|
-
next unless foreign_key_to_models[column_name.downcase]
|
|
69
|
-
|
|
70
|
-
foreign_key_to_models[column_name.downcase].each do |foreign_model, relationship_name|
|
|
71
|
-
next if foreign_model == model
|
|
72
|
-
|
|
73
|
-
add_relationships(relationship_name, model, foreign_model, column_name)
|
|
74
|
-
end
|
|
75
|
-
end
|
|
80
|
+
model.column_names.each { |column_name| add_column_relationships(model, column_name, foreign_key_to_models) }
|
|
76
81
|
end
|
|
77
82
|
|
|
78
83
|
@join_tables.each do |join_table_model|
|
|
@@ -81,10 +86,36 @@ module DynamicActiveModel
|
|
|
81
86
|
end.compact
|
|
82
87
|
add_has_and_belongs_to_many(join_table_model, models) if models.size == 2
|
|
83
88
|
end
|
|
89
|
+
|
|
90
|
+
HasManyThrough.new(@database.models, table_indexes).build! if @has_many_through
|
|
84
91
|
end
|
|
85
92
|
|
|
86
93
|
private
|
|
87
94
|
|
|
95
|
+
# Relates a column through its foreign key constraint when it has one (and
|
|
96
|
+
# constraints are in use), otherwise by naming convention
|
|
97
|
+
# @param model [Class] The model with the column
|
|
98
|
+
# @param column_name [String]
|
|
99
|
+
# @param foreign_key_to_models [Hash] See #create_foreign_key_to_model_map
|
|
100
|
+
def add_column_relationships(model, column_name, foreign_key_to_models)
|
|
101
|
+
constraint = @constraints&.find(model, column_name)
|
|
102
|
+
return add_constraint_relationships(constraint) if constraint
|
|
103
|
+
|
|
104
|
+
foreign_key_to_models.fetch(column_name.downcase, []).each do |foreign_model, relationship_name|
|
|
105
|
+
next if foreign_model == model
|
|
106
|
+
|
|
107
|
+
add_relationships(relationship_name, model, foreign_model, column_name)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Adds the relationships a foreign key constraint describes; unlike the naming
|
|
112
|
+
# convention, these may reference the same model (employees.manager_id)
|
|
113
|
+
# @param constraint [ForeignKeyConstraints::Constraint]
|
|
114
|
+
def add_constraint_relationships(constraint)
|
|
115
|
+
add_relationships(constraint.relationship_name, constraint.model, constraint.referenced_model,
|
|
116
|
+
constraint.column, constraint.primary_key)
|
|
117
|
+
end
|
|
118
|
+
|
|
88
119
|
# Adds has_and_belongs_to_many relationships between two models
|
|
89
120
|
# @param join_table_model [Class] The join table model
|
|
90
121
|
# @param models [Array<Class>] The two models to be related
|
|
@@ -101,12 +132,14 @@ module DynamicActiveModel
|
|
|
101
132
|
# @param model [Class] The model with the foreign key
|
|
102
133
|
# @param belongs_to_model [Class] The model being referenced
|
|
103
134
|
# @param foreign_key [String] The foreign key column name
|
|
104
|
-
|
|
105
|
-
|
|
135
|
+
# @param primary_key [String] The referenced column, normally the primary key
|
|
136
|
+
def add_relationships(relationship_name, model, belongs_to_model, foreign_key,
|
|
137
|
+
primary_key = belongs_to_model.primary_key)
|
|
138
|
+
add_belongs_to(relationship_name, model, belongs_to_model, foreign_key, primary_key)
|
|
106
139
|
if unique_index?(model, foreign_key)
|
|
107
|
-
add_has_one(relationship_name, belongs_to_model, model, foreign_key)
|
|
140
|
+
add_has_one(relationship_name, belongs_to_model, model, foreign_key, primary_key)
|
|
108
141
|
else
|
|
109
|
-
add_has_many(relationship_name, belongs_to_model, model, foreign_key)
|
|
142
|
+
add_has_many(relationship_name, belongs_to_model, model, foreign_key, primary_key)
|
|
110
143
|
end
|
|
111
144
|
end
|
|
112
145
|
|
|
@@ -115,12 +148,13 @@ module DynamicActiveModel
|
|
|
115
148
|
# @param model [Class] The model with the foreign key
|
|
116
149
|
# @param belongs_to_model [Class] The model being referenced
|
|
117
150
|
# @param foreign_key [String] The foreign key column name
|
|
118
|
-
|
|
151
|
+
# @param primary_key [String] The referenced column
|
|
152
|
+
def add_belongs_to(relationship_name, model, belongs_to_model, foreign_key, primary_key)
|
|
119
153
|
model.belongs_to(
|
|
120
154
|
relationship_name.singularize.to_sym,
|
|
121
155
|
class_name: belongs_to_model.name,
|
|
122
156
|
foreign_key: foreign_key,
|
|
123
|
-
primary_key:
|
|
157
|
+
primary_key: primary_key
|
|
124
158
|
)
|
|
125
159
|
end
|
|
126
160
|
|
|
@@ -129,12 +163,13 @@ module DynamicActiveModel
|
|
|
129
163
|
# @param model [Class] The referenced (parent) model that gets the association
|
|
130
164
|
# @param has_many_model [Class] The model with the foreign key
|
|
131
165
|
# @param foreign_key [String] The foreign key column name
|
|
132
|
-
|
|
166
|
+
# @param primary_key [String] The referenced column on the parent model
|
|
167
|
+
def add_has_many(relationship_name, model, has_many_model, foreign_key, primary_key)
|
|
133
168
|
model.has_many(
|
|
134
169
|
generate_has_many_association_name(relationship_name, model, has_many_model),
|
|
135
170
|
class_name: has_many_model.name,
|
|
136
171
|
foreign_key: foreign_key,
|
|
137
|
-
primary_key:
|
|
172
|
+
primary_key: primary_key
|
|
138
173
|
)
|
|
139
174
|
end
|
|
140
175
|
|
|
@@ -143,12 +178,13 @@ module DynamicActiveModel
|
|
|
143
178
|
# @param model [Class] The referenced (parent) model that gets the association
|
|
144
179
|
# @param has_one_model [Class] The model with the foreign key
|
|
145
180
|
# @param foreign_key [String] The foreign key column name
|
|
146
|
-
|
|
181
|
+
# @param primary_key [String] The referenced column on the parent model
|
|
182
|
+
def add_has_one(relationship_name, model, has_one_model, foreign_key, primary_key)
|
|
147
183
|
model.has_one(
|
|
148
184
|
generate_has_one_association_name(relationship_name, model, has_one_model),
|
|
149
185
|
class_name: has_one_model.name,
|
|
150
186
|
foreign_key: foreign_key,
|
|
151
|
-
primary_key:
|
|
187
|
+
primary_key: primary_key
|
|
152
188
|
)
|
|
153
189
|
end
|
|
154
190
|
|
|
@@ -14,6 +14,10 @@ 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 and has_many :through
|
|
18
|
+
# DynamicActiveModel::Explorer.explore(DB, database_config, foreign_key_constraints: true,
|
|
19
|
+
# has_many_through: true)
|
|
20
|
+
#
|
|
17
21
|
# @example With Custom Relationships
|
|
18
22
|
# relationships = {
|
|
19
23
|
# 'users' => {
|
|
@@ -30,13 +34,18 @@ module DynamicActiveModel
|
|
|
30
34
|
# @param relationships [Hash] Custom foreign key relationships to add
|
|
31
35
|
# @param table_class_names [Hash] Custom class names by table name
|
|
32
36
|
# @param parent_class [Class, nil] Optional superclass for the base class (see Factory)
|
|
37
|
+
# @param foreign_key_constraints [Boolean] Also relate columns through the database's
|
|
38
|
+
# foreign key constraints (see ForeignKeyConstraints)
|
|
39
|
+
# @param has_many_through [Boolean] Also add has_many :through across join models
|
|
40
|
+
# (see HasManyThrough)
|
|
33
41
|
# @return [Database] The configured database instance
|
|
34
42
|
# @raise [ClassNameConflict] If two tables map to the same class name
|
|
35
43
|
def self.explore(base_module, connection_options, skip_tables = [], relationships = {}, table_class_names = {},
|
|
36
|
-
parent_class: nil)
|
|
44
|
+
parent_class: nil, foreign_key_constraints: false, has_many_through: false)
|
|
37
45
|
database = create_models!(base_module, connection_options, skip_tables, table_class_names,
|
|
38
46
|
parent_class: parent_class)
|
|
39
|
-
build_relationships!(database, relationships
|
|
47
|
+
build_relationships!(database, relationships, foreign_key_constraints: foreign_key_constraints,
|
|
48
|
+
has_many_through: has_many_through)
|
|
40
49
|
database
|
|
41
50
|
end
|
|
42
51
|
|
|
@@ -67,9 +76,13 @@ module DynamicActiveModel
|
|
|
67
76
|
# Sets up relationships between created models
|
|
68
77
|
# @param database [Database] The database instance containing the models
|
|
69
78
|
# @param relationships [Hash] Custom foreign key relationships to add
|
|
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
|
|
70
81
|
# @return [void]
|
|
71
|
-
def self.build_relationships!(database, relationships)
|
|
82
|
+
def self.build_relationships!(database, relationships, foreign_key_constraints: false, has_many_through: false)
|
|
72
83
|
relations = Associations.new(database)
|
|
84
|
+
relations.use_foreign_key_constraints! if foreign_key_constraints
|
|
85
|
+
relations.use_has_many_through! if has_many_through
|
|
73
86
|
relationships.each do |table_name, foreign_keys|
|
|
74
87
|
foreign_keys.each do |foreign_key, relationship_name|
|
|
75
88
|
relations.add_foreign_key(table_name, foreign_key, relationship_name)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DynamicActiveModel
|
|
4
|
+
# Reads the database's foreign key constraints and resolves them to models, so
|
|
5
|
+
# Associations can relate columns that don't follow the <table>_id naming
|
|
6
|
+
# convention (posts.author_id => users) and self-references (employees.manager_id).
|
|
7
|
+
#
|
|
8
|
+
# Skipped: composite constraints, columns without the id suffix (a belongs_to
|
|
9
|
+
# named after the column itself would hide it), and constraints whose table or
|
|
10
|
+
# referenced table has no model.
|
|
11
|
+
#
|
|
12
|
+
# @example
|
|
13
|
+
# constraints = DynamicActiveModel::ForeignKeyConstraints.new(database)
|
|
14
|
+
# constraints.find(DB::Post, 'author_id') # => #<struct referenced_model=DB::User, ...>
|
|
15
|
+
class ForeignKeyConstraints
|
|
16
|
+
# A constraint resolved to models
|
|
17
|
+
Constraint = Struct.new(:model, :column, :referenced_model, :primary_key, :relationship_name)
|
|
18
|
+
|
|
19
|
+
# @param database [Database] The database whose models' constraints to read
|
|
20
|
+
def initialize(database)
|
|
21
|
+
@database = database
|
|
22
|
+
@constraints = database.models.flat_map { |model| constraints_for(model) }
|
|
23
|
+
.to_h { |constraint| [[constraint.model, constraint.column], constraint] }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# @param model [Class] The model with the column
|
|
27
|
+
# @param column [String] The column name
|
|
28
|
+
# @return [Constraint, nil] The column's constraint, if it has one
|
|
29
|
+
def find(model, column)
|
|
30
|
+
@constraints[[model, column]]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# @return [Integer] Number of usable constraints
|
|
34
|
+
def size
|
|
35
|
+
@constraints.size
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
# @param model [Class]
|
|
41
|
+
# @return [Array<Constraint>] The model's usable constraints
|
|
42
|
+
def constraints_for(model)
|
|
43
|
+
model.connection.foreign_keys(model.table_name).filter_map do |foreign_key|
|
|
44
|
+
column = foreign_key.column
|
|
45
|
+
next unless column.is_a?(String) && column.end_with?(ForeignKey.id_suffix)
|
|
46
|
+
|
|
47
|
+
referenced_model = @database.get_model(foreign_key.to_table)
|
|
48
|
+
next unless referenced_model
|
|
49
|
+
|
|
50
|
+
Constraint.new(model, column, referenced_model, foreign_key.primary_key,
|
|
51
|
+
relationship_name(column, referenced_model))
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Names the relationship like the naming convention does: author_id => "author",
|
|
56
|
+
# but user_id referencing users => "users", so its has_many stays :posts
|
|
57
|
+
# @param column [String]
|
|
58
|
+
# @param referenced_model [Class]
|
|
59
|
+
# @return [String]
|
|
60
|
+
def relationship_name(column, referenced_model)
|
|
61
|
+
name = column.delete_suffix(ForeignKey.id_suffix)
|
|
62
|
+
table_name = referenced_model.table_name.underscore
|
|
63
|
+
name == table_name.singularize ? table_name : name
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -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
|
|
@@ -53,6 +53,8 @@ module DynamicActiveModel
|
|
|
53
53
|
{
|
|
54
54
|
connection_options: nil,
|
|
55
55
|
parent_class: nil,
|
|
56
|
+
foreign_key_constraints: false,
|
|
57
|
+
has_many_through: false,
|
|
56
58
|
skip_tables: [],
|
|
57
59
|
relationships: {},
|
|
58
60
|
table_class_names: {},
|
|
@@ -84,6 +86,24 @@ module DynamicActiveModel
|
|
|
84
86
|
dynamic_active_model_config[:parent_class]
|
|
85
87
|
end
|
|
86
88
|
|
|
89
|
+
# Sets or gets whether columns are also related through the database's foreign
|
|
90
|
+
# key constraints (see ForeignKeyConstraints)
|
|
91
|
+
# @param enabled [Boolean, nil]
|
|
92
|
+
# @return [Boolean] The current setting
|
|
93
|
+
def foreign_key_constraints(enabled = nil)
|
|
94
|
+
update_config(:foreign_key_constraints, enabled) unless enabled.nil?
|
|
95
|
+
dynamic_active_model_config[:foreign_key_constraints]
|
|
96
|
+
end
|
|
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
|
+
|
|
87
107
|
# Sets or gets the list of tables to skip
|
|
88
108
|
# @param tables [Array<String>, nil] Tables to skip
|
|
89
109
|
# @return [Array<String>] The current list of skipped tables
|
|
@@ -165,7 +185,9 @@ module DynamicActiveModel
|
|
|
165
185
|
skip_tables,
|
|
166
186
|
relationships,
|
|
167
187
|
table_class_names,
|
|
168
|
-
parent_class: parent_class
|
|
188
|
+
parent_class: parent_class,
|
|
189
|
+
foreign_key_constraints: foreign_key_constraints,
|
|
190
|
+
has_many_through: has_many_through
|
|
169
191
|
)
|
|
170
192
|
)
|
|
171
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
|
@@ -37,6 +37,12 @@ module DynamicActiveModel
|
|
|
37
37
|
# Handles foreign key relationships and constraints
|
|
38
38
|
autoload :ForeignKey, 'dynamic-active-model/foreign_key'
|
|
39
39
|
|
|
40
|
+
# Resolves the database's foreign key constraints to models
|
|
41
|
+
autoload :ForeignKeyConstraints, 'dynamic-active-model/foreign_key_constraints'
|
|
42
|
+
|
|
43
|
+
# Adds has_many :through associations across join models
|
|
44
|
+
autoload :HasManyThrough, 'dynamic-active-model/has_many_through'
|
|
45
|
+
|
|
40
46
|
# Manages automatic discovery and setup of model relationships
|
|
41
47
|
autoload :Associations, 'dynamic-active-model/associations'
|
|
42
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
|
|
@@ -56,6 +56,8 @@ files:
|
|
|
56
56
|
- lib/dynamic-active-model/explorer.rb
|
|
57
57
|
- lib/dynamic-active-model/factory.rb
|
|
58
58
|
- lib/dynamic-active-model/foreign_key.rb
|
|
59
|
+
- lib/dynamic-active-model/foreign_key_constraints.rb
|
|
60
|
+
- lib/dynamic-active-model/has_many_through.rb
|
|
59
61
|
- lib/dynamic-active-model/setup.rb
|
|
60
62
|
- lib/dynamic-active-model/template_class_file.rb
|
|
61
63
|
- lib/dynamic-active-model/version.rb
|