dynamic-active-model 1.0.0 → 1.1.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 +47 -19
- data/lib/dynamic-active-model/explorer.rb +10 -3
- data/lib/dynamic-active-model/foreign_key_constraints.rb +66 -0
- data/lib/dynamic-active-model/setup.rb +12 -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: 4b97bc188a4c9b06a6cfa87bf1af11ef77bd1e80dbf795b61992d90a940202f8
|
|
4
|
+
data.tar.gz: add2284f1e7f247f47bb3146b145bb8fe8880baa95acc37b904c791d4077c24d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9a185c609164a98c20d755c47d45db2ed059aebe42811d15c987ebb8f3e18d8dd6b7101e714252d046b85ef0f42cad1997d557ead86d0fac534a18314751bac5
|
|
7
|
+
data.tar.gz: 6dc01aefd21b1dce01a1b4bc9926bd4424a25da6fb0bbcaeb728b6692fff88de984567f2fa988e4dbde917d996b3a2166bfa6d19671fa1831438e745700f25f7
|
data/bin/dynamic-db-explorer
CHANGED
|
@@ -66,6 +66,10 @@ 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
|
+
|
|
69
73
|
opts.on('-c', '--config FILE', "config file, default #{DEFAULT_CONFIG_FILE}") do |v|
|
|
70
74
|
options[:config_file] = v
|
|
71
75
|
end
|
|
@@ -91,6 +95,7 @@ if options[:config_file]
|
|
|
91
95
|
config = YAML.safe_load_file(options[:config_file], permitted_classes: [Symbol])[options[:config_section]]
|
|
92
96
|
options[:skip_tables] = config.delete('skip_tables') || []
|
|
93
97
|
options[:relationships] = config.delete('relationships') || {}
|
|
98
|
+
options[:foreign_key_constraints] ||= config.delete('foreign_key_constraints') || false
|
|
94
99
|
if (module_name = config.delete('module'))
|
|
95
100
|
raise ArgumentError, "Invalid module name: #{module_name}" unless module_name.match?(/\A[A-Z][A-Za-z0-9_]*\z/)
|
|
96
101
|
|
|
@@ -104,7 +109,8 @@ DYNAMIC_DATABASE = DynamicActiveModel::Explorer.explore(
|
|
|
104
109
|
options[:base_module],
|
|
105
110
|
options[:connection_options],
|
|
106
111
|
options[:skip_tables],
|
|
107
|
-
options[:relationships]
|
|
112
|
+
options[:relationships],
|
|
113
|
+
foreign_key_constraints: options[:foreign_key_constraints] || false
|
|
108
114
|
)
|
|
109
115
|
|
|
110
116
|
if options[:class_files_dir]
|
|
@@ -53,6 +53,13 @@ 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
|
+
|
|
56
63
|
# Builds all relationships between models based on foreign keys and constraints
|
|
57
64
|
# This method:
|
|
58
65
|
# 1. Maps foreign keys to their corresponding models
|
|
@@ -64,15 +71,7 @@ module DynamicActiveModel
|
|
|
64
71
|
foreign_key_to_models = create_foreign_key_to_model_map
|
|
65
72
|
|
|
66
73
|
@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
|
|
74
|
+
model.column_names.each { |column_name| add_column_relationships(model, column_name, foreign_key_to_models) }
|
|
76
75
|
end
|
|
77
76
|
|
|
78
77
|
@join_tables.each do |join_table_model|
|
|
@@ -85,6 +84,30 @@ module DynamicActiveModel
|
|
|
85
84
|
|
|
86
85
|
private
|
|
87
86
|
|
|
87
|
+
# Relates a column through its foreign key constraint when it has one (and
|
|
88
|
+
# constraints are in use), otherwise by naming convention
|
|
89
|
+
# @param model [Class] The model with the column
|
|
90
|
+
# @param column_name [String]
|
|
91
|
+
# @param foreign_key_to_models [Hash] See #create_foreign_key_to_model_map
|
|
92
|
+
def add_column_relationships(model, column_name, foreign_key_to_models)
|
|
93
|
+
constraint = @constraints&.find(model, column_name)
|
|
94
|
+
return add_constraint_relationships(constraint) if constraint
|
|
95
|
+
|
|
96
|
+
foreign_key_to_models.fetch(column_name.downcase, []).each do |foreign_model, relationship_name|
|
|
97
|
+
next if foreign_model == model
|
|
98
|
+
|
|
99
|
+
add_relationships(relationship_name, model, foreign_model, column_name)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Adds the relationships a foreign key constraint describes; unlike the naming
|
|
104
|
+
# convention, these may reference the same model (employees.manager_id)
|
|
105
|
+
# @param constraint [ForeignKeyConstraints::Constraint]
|
|
106
|
+
def add_constraint_relationships(constraint)
|
|
107
|
+
add_relationships(constraint.relationship_name, constraint.model, constraint.referenced_model,
|
|
108
|
+
constraint.column, constraint.primary_key)
|
|
109
|
+
end
|
|
110
|
+
|
|
88
111
|
# Adds has_and_belongs_to_many relationships between two models
|
|
89
112
|
# @param join_table_model [Class] The join table model
|
|
90
113
|
# @param models [Array<Class>] The two models to be related
|
|
@@ -101,12 +124,14 @@ module DynamicActiveModel
|
|
|
101
124
|
# @param model [Class] The model with the foreign key
|
|
102
125
|
# @param belongs_to_model [Class] The model being referenced
|
|
103
126
|
# @param foreign_key [String] The foreign key column name
|
|
104
|
-
|
|
105
|
-
|
|
127
|
+
# @param primary_key [String] The referenced column, normally the primary key
|
|
128
|
+
def add_relationships(relationship_name, model, belongs_to_model, foreign_key,
|
|
129
|
+
primary_key = belongs_to_model.primary_key)
|
|
130
|
+
add_belongs_to(relationship_name, model, belongs_to_model, foreign_key, primary_key)
|
|
106
131
|
if unique_index?(model, foreign_key)
|
|
107
|
-
add_has_one(relationship_name, belongs_to_model, model, foreign_key)
|
|
132
|
+
add_has_one(relationship_name, belongs_to_model, model, foreign_key, primary_key)
|
|
108
133
|
else
|
|
109
|
-
add_has_many(relationship_name, belongs_to_model, model, foreign_key)
|
|
134
|
+
add_has_many(relationship_name, belongs_to_model, model, foreign_key, primary_key)
|
|
110
135
|
end
|
|
111
136
|
end
|
|
112
137
|
|
|
@@ -115,12 +140,13 @@ module DynamicActiveModel
|
|
|
115
140
|
# @param model [Class] The model with the foreign key
|
|
116
141
|
# @param belongs_to_model [Class] The model being referenced
|
|
117
142
|
# @param foreign_key [String] The foreign key column name
|
|
118
|
-
|
|
143
|
+
# @param primary_key [String] The referenced column
|
|
144
|
+
def add_belongs_to(relationship_name, model, belongs_to_model, foreign_key, primary_key)
|
|
119
145
|
model.belongs_to(
|
|
120
146
|
relationship_name.singularize.to_sym,
|
|
121
147
|
class_name: belongs_to_model.name,
|
|
122
148
|
foreign_key: foreign_key,
|
|
123
|
-
primary_key:
|
|
149
|
+
primary_key: primary_key
|
|
124
150
|
)
|
|
125
151
|
end
|
|
126
152
|
|
|
@@ -129,12 +155,13 @@ module DynamicActiveModel
|
|
|
129
155
|
# @param model [Class] The referenced (parent) model that gets the association
|
|
130
156
|
# @param has_many_model [Class] The model with the foreign key
|
|
131
157
|
# @param foreign_key [String] The foreign key column name
|
|
132
|
-
|
|
158
|
+
# @param primary_key [String] The referenced column on the parent model
|
|
159
|
+
def add_has_many(relationship_name, model, has_many_model, foreign_key, primary_key)
|
|
133
160
|
model.has_many(
|
|
134
161
|
generate_has_many_association_name(relationship_name, model, has_many_model),
|
|
135
162
|
class_name: has_many_model.name,
|
|
136
163
|
foreign_key: foreign_key,
|
|
137
|
-
primary_key:
|
|
164
|
+
primary_key: primary_key
|
|
138
165
|
)
|
|
139
166
|
end
|
|
140
167
|
|
|
@@ -143,12 +170,13 @@ module DynamicActiveModel
|
|
|
143
170
|
# @param model [Class] The referenced (parent) model that gets the association
|
|
144
171
|
# @param has_one_model [Class] The model with the foreign key
|
|
145
172
|
# @param foreign_key [String] The foreign key column name
|
|
146
|
-
|
|
173
|
+
# @param primary_key [String] The referenced column on the parent model
|
|
174
|
+
def add_has_one(relationship_name, model, has_one_model, foreign_key, primary_key)
|
|
147
175
|
model.has_one(
|
|
148
176
|
generate_has_one_association_name(relationship_name, model, has_one_model),
|
|
149
177
|
class_name: has_one_model.name,
|
|
150
178
|
foreign_key: foreign_key,
|
|
151
|
-
primary_key:
|
|
179
|
+
primary_key: primary_key
|
|
152
180
|
)
|
|
153
181
|
end
|
|
154
182
|
|
|
@@ -14,6 +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)
|
|
19
|
+
#
|
|
17
20
|
# @example With Custom Relationships
|
|
18
21
|
# relationships = {
|
|
19
22
|
# 'users' => {
|
|
@@ -30,13 +33,15 @@ module DynamicActiveModel
|
|
|
30
33
|
# @param relationships [Hash] Custom foreign key relationships to add
|
|
31
34
|
# @param table_class_names [Hash] Custom class names by table name
|
|
32
35
|
# @param parent_class [Class, nil] Optional superclass for the base class (see Factory)
|
|
36
|
+
# @param foreign_key_constraints [Boolean] Also relate columns through the database's
|
|
37
|
+
# foreign key constraints (see ForeignKeyConstraints)
|
|
33
38
|
# @return [Database] The configured database instance
|
|
34
39
|
# @raise [ClassNameConflict] If two tables map to the same class name
|
|
35
40
|
def self.explore(base_module, connection_options, skip_tables = [], relationships = {}, table_class_names = {},
|
|
36
|
-
parent_class: nil)
|
|
41
|
+
parent_class: nil, foreign_key_constraints: false)
|
|
37
42
|
database = create_models!(base_module, connection_options, skip_tables, table_class_names,
|
|
38
43
|
parent_class: parent_class)
|
|
39
|
-
build_relationships!(database, relationships)
|
|
44
|
+
build_relationships!(database, relationships, foreign_key_constraints: foreign_key_constraints)
|
|
40
45
|
database
|
|
41
46
|
end
|
|
42
47
|
|
|
@@ -67,9 +72,11 @@ module DynamicActiveModel
|
|
|
67
72
|
# Sets up relationships between created models
|
|
68
73
|
# @param database [Database] The database instance containing the models
|
|
69
74
|
# @param relationships [Hash] Custom foreign key relationships to add
|
|
75
|
+
# @param foreign_key_constraints [Boolean] Also relate columns through foreign key constraints
|
|
70
76
|
# @return [void]
|
|
71
|
-
def self.build_relationships!(database, relationships)
|
|
77
|
+
def self.build_relationships!(database, relationships, foreign_key_constraints: false)
|
|
72
78
|
relations = Associations.new(database)
|
|
79
|
+
relations.use_foreign_key_constraints! if foreign_key_constraints
|
|
73
80
|
relationships.each do |table_name, foreign_keys|
|
|
74
81
|
foreign_keys.each do |foreign_key, relationship_name|
|
|
75
82
|
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
|
|
@@ -53,6 +53,7 @@ module DynamicActiveModel
|
|
|
53
53
|
{
|
|
54
54
|
connection_options: nil,
|
|
55
55
|
parent_class: nil,
|
|
56
|
+
foreign_key_constraints: false,
|
|
56
57
|
skip_tables: [],
|
|
57
58
|
relationships: {},
|
|
58
59
|
table_class_names: {},
|
|
@@ -84,6 +85,15 @@ module DynamicActiveModel
|
|
|
84
85
|
dynamic_active_model_config[:parent_class]
|
|
85
86
|
end
|
|
86
87
|
|
|
88
|
+
# Sets or gets whether columns are also related through the database's foreign
|
|
89
|
+
# key constraints (see ForeignKeyConstraints)
|
|
90
|
+
# @param enabled [Boolean, nil]
|
|
91
|
+
# @return [Boolean] The current setting
|
|
92
|
+
def foreign_key_constraints(enabled = nil)
|
|
93
|
+
update_config(:foreign_key_constraints, enabled) unless enabled.nil?
|
|
94
|
+
dynamic_active_model_config[:foreign_key_constraints]
|
|
95
|
+
end
|
|
96
|
+
|
|
87
97
|
# Sets or gets the list of tables to skip
|
|
88
98
|
# @param tables [Array<String>, nil] Tables to skip
|
|
89
99
|
# @return [Array<String>] The current list of skipped tables
|
|
@@ -165,7 +175,8 @@ module DynamicActiveModel
|
|
|
165
175
|
skip_tables,
|
|
166
176
|
relationships,
|
|
167
177
|
table_class_names,
|
|
168
|
-
parent_class: parent_class
|
|
178
|
+
parent_class: parent_class,
|
|
179
|
+
foreign_key_constraints: foreign_key_constraints
|
|
169
180
|
)
|
|
170
181
|
)
|
|
171
182
|
database.update_all_models(extensions_path, extensions_suffix) if extensions_path
|
data/lib/dynamic-active-model.rb
CHANGED
|
@@ -37,6 +37,9 @@ 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
|
+
|
|
40
43
|
# Manages automatic discovery and setup of model relationships
|
|
41
44
|
autoload :Associations, 'dynamic-active-model/associations'
|
|
42
45
|
|
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.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Doug Youch
|
|
@@ -56,6 +56,7 @@ 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
|
|
59
60
|
- lib/dynamic-active-model/setup.rb
|
|
60
61
|
- lib/dynamic-active-model/template_class_file.rb
|
|
61
62
|
- lib/dynamic-active-model/version.rb
|