rails-mermaid_erd 0.8.0 → 0.8.2
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/LICENSE +21 -0
- data/README.md +8 -1
- data/lib/rails-mermaid_erd/builder.rb +176 -104
- data/lib/rails-mermaid_erd/configuration.rb +54 -2
- data/lib/rails-mermaid_erd/mermaid_text.rb +14 -8
- data/lib/rails-mermaid_erd/version.rb +1 -1
- data/lib/tasks/mermaid_erd.rake +25 -1
- data/lib/templates/index.html.erb +402 -153
- data/lib/templates/vendor/CHECKSUMS.txt +2 -2
- data/lib/templates/vendor/LICENSES.md +757 -25
- data/lib/templates/vendor/README.md +77 -19
- data/lib/templates/vendor/tailwind.css +1 -0
- metadata +17 -3
- data/MIT-LICENSE +0 -20
- data/lib/templates/vendor/tailwindcss.js +0 -65
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1e8b99cba042397b3b3fb6e1db0480aeb839aaf1c4232e24be6ae5e9cac9cb3e
|
|
4
|
+
data.tar.gz: 1980c75216f10e0e6314626784c027c4aab1167f76f043e84c5358ccaa78beac
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 54387a81bc7df09d7dd723810848c2230ef31f043d27f14e8da8cb9184d1a00a997966f6cad191909e08ccfecbd69e101e655207fd580327e3d49d7f301ed103
|
|
7
|
+
data.tar.gz: 77047222cfdfc8bf8c814a9b5f2a9b8c06e8428fc23e1d4b81cad37ca4f6fc4af37afb0ccdd47774161ace5c8d7f725a5bc0f569e734aa294280babe45daf7f9
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022 肥溜め
|
|
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 all
|
|
13
|
+
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 THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
CHANGED
|
@@ -62,6 +62,8 @@ mermaid_erd
|
|
|
62
62
|
|
|
63
63
|
If you share this file, it can be used by those who do not have a Ruby on Rails environment. Or, you can upload the file to a web server and share it with the same URL.
|
|
64
64
|
|
|
65
|
+
The generated file contains your application's schema, including table and column names and comments. Share it, or host it, only as widely as you would share the schema itself.
|
|
66
|
+
|
|
65
67
|
It would be very smart to generate it automatically using CI.
|
|
66
68
|
|
|
67
69
|
### Print the Mermaid source to stdout
|
|
@@ -88,7 +90,7 @@ The Ruby × Rails combinations exercised by CI on every push and pull request:
|
|
|
88
90
|
| ----- | -------------------------------- |
|
|
89
91
|
| 5.2 | 2.7 |
|
|
90
92
|
| 6.0 | 2.7, 3.0 |
|
|
91
|
-
| 6.1 | 2.7, 3.0, 3.1, 3.2
|
|
93
|
+
| 6.1 | 2.7, 3.0, 3.1, 3.2, 3.3 |
|
|
92
94
|
| 7.0 | 2.7, 3.0, 3.1, 3.2, 3.3 |
|
|
93
95
|
| 7.1 | 3.1, 3.2, 3.3, 3.4 |
|
|
94
96
|
| 7.2 | 3.1, 3.2, 3.3, 3.4 |
|
|
@@ -108,6 +110,7 @@ The setting items are as follows.
|
|
|
108
110
|
| --- | --- | --- |
|
|
109
111
|
| `result_path` | Destination of generated files. | `mermaid_erd/index.html` |
|
|
110
112
|
| `ignore_tables` | Array of regular-expression strings. Tables whose `table_name` matches any pattern are dropped from the generated ERD, along with any relations that point at them. Useful for excluding audit-log models, soft-deleted/legacy tables, or other large noise you don't want to render. Patterns are compiled with `Regexp.new`, so escape backslashes inside YAML strings (e.g. `"\\Aaudit_"`). | `[]` |
|
|
113
|
+
| `viewer_defaults` | The view the viewer opens on when the URL carries no saved state, and the one **Reset** returns to; a link with its own state always wins. `models`: array of model names selected at first (a name that is not in the diagram is skipped with a warning when the file is generated). `columns`: `all`, `keys` (primary and foreign keys only) or `none`, the same choices as Columns in the sidebar. Useful for opening a large app on the models people usually start from. | `models: []` (nothing selected), `columns: all` |
|
|
111
114
|
|
|
112
115
|
<!--
|
|
113
116
|
TODO:
|
|
@@ -118,3 +121,7 @@ Contribution directions go here.
|
|
|
118
121
|
|
|
119
122
|
## License
|
|
120
123
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
124
|
+
|
|
125
|
+
## Trademarks
|
|
126
|
+
|
|
127
|
+
This project is not affiliated with, endorsed by, or sponsored by Rails, The Rails Foundation, the Mermaid project, or Mermaid Chart Inc. Rails and Ruby on Rails are registered trademarks of David Heinemeier Hansson. Mermaid Chart Inc. has registered or applied for the trademarks MERMAID CHART and MERMAID in the United States. All other trademarks are the property of their respective owners.
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
class RailsMermaidErd::Builder
|
|
2
|
+
# The order associations are read in, which is also the order their labels
|
|
3
|
+
# appear in a relation's comment.
|
|
4
|
+
RELATION_MACROS = [:has_many, :has_and_belongs_to_many, :belongs_to, :has_one].freeze
|
|
5
|
+
|
|
2
6
|
class << self
|
|
3
7
|
def model_data
|
|
4
8
|
result = {
|
|
@@ -24,10 +28,25 @@ class RailsMermaidErd::Builder
|
|
|
24
28
|
# pulling in `set` (which is autoloaded on Ruby 3.2+ but not earlier).
|
|
25
29
|
ignored_model_names = compute_ignored_model_names(ignore_patterns)
|
|
26
30
|
|
|
31
|
+
# Single table inheritance subclasses share their base class's table, so
|
|
32
|
+
# each hierarchy is drawn as the base class's entity. Associations that
|
|
33
|
+
# target a subclass are pointed at that entity too.
|
|
34
|
+
sti_base_names = compute_sti_base_names
|
|
35
|
+
|
|
36
|
+
# Associations grouped by the link they describe (see `link_key`). Each
|
|
37
|
+
# group becomes one relation line once every model has been read.
|
|
38
|
+
links = Hash.new { |hash, key| hash[key] = [] }
|
|
39
|
+
|
|
40
|
+
# Foreign key columns the associations name, by the model whose table
|
|
41
|
+
# holds them. Not every application backs its associations with foreign
|
|
42
|
+
# key constraints, so these are drawn as foreign keys too.
|
|
43
|
+
association_foreign_keys = Hash.new { |hash, model_name| hash[model_name] = [] }
|
|
44
|
+
|
|
27
45
|
::ActiveRecord::Base.descendants.sort_by(&:name).each do |defined_model|
|
|
28
46
|
next unless defined_model.table_exists?
|
|
29
47
|
next if defined_model.name.include?("HABTM_")
|
|
30
48
|
next if defined_model.table_name.blank?
|
|
49
|
+
next if sti_base_names.key?(defined_model.name)
|
|
31
50
|
next if ignored_model_names.key?(defined_model.name)
|
|
32
51
|
|
|
33
52
|
table_name = defined_model.table_name
|
|
@@ -40,10 +59,10 @@ class RailsMermaidErd::Builder
|
|
|
40
59
|
}
|
|
41
60
|
|
|
42
61
|
foreign_keys = ::ActiveRecord::Schema.foreign_keys(defined_model.table_name).map { |k| k.options[:column] }
|
|
43
|
-
|
|
62
|
+
primary_keys = Array(defined_model.primary_key)
|
|
44
63
|
defined_model.columns.each do |column|
|
|
45
64
|
key = ""
|
|
46
|
-
if column.name
|
|
65
|
+
if primary_keys.include?(column.name)
|
|
47
66
|
key = "PK"
|
|
48
67
|
elsif foreign_keys.include?(column.name)
|
|
49
68
|
key = "FK"
|
|
@@ -58,119 +77,46 @@ class RailsMermaidErd::Builder
|
|
|
58
77
|
|
|
59
78
|
result[:Models] << model
|
|
60
79
|
|
|
61
|
-
defined_model
|
|
62
|
-
|
|
63
|
-
|
|
80
|
+
hierarchy = [defined_model] + defined_model.descendants
|
|
81
|
+
.select { |descendant| sti_base_names[descendant.name] == defined_model.name }
|
|
82
|
+
.sort_by(&:name)
|
|
64
83
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
84
|
+
RELATION_MACROS.each do |macro|
|
|
85
|
+
hierarchy_reflections(hierarchy, macro, sti_base_names).each do |reflection, reflection_model_name|
|
|
86
|
+
# A `belongs_to` holds its foreign key itself, whatever it points at.
|
|
87
|
+
if macro == :belongs_to
|
|
88
|
+
association_foreign_keys[model[:ModelName]].concat(foreign_key_columns(reflection))
|
|
70
89
|
end
|
|
71
|
-
}
|
|
72
|
-
if reverse_relation
|
|
73
|
-
reverse_relation[:Comment] = if reflection.options[:through]
|
|
74
|
-
"#{reverse_relation[:Comment]}, HMT:#{reflection.name}"
|
|
75
|
-
else
|
|
76
|
-
"#{reverse_relation[:Comment]}, HM:#{reflection.name}"
|
|
77
|
-
end
|
|
78
|
-
else
|
|
79
|
-
result[:Relations] << {
|
|
80
|
-
LeftModelName: model[:ModelName],
|
|
81
|
-
LeftValue: reflection.options[:through] ? "}o" : "||",
|
|
82
|
-
Line: reflection.options[:through] ? ".." : "--",
|
|
83
|
-
RightModelName: reflection_model_name,
|
|
84
|
-
RightValue: "o{",
|
|
85
|
-
Comment: reflection.options[:through] ? "HMT:#{reflection.name}" : "HM:#{reflection.name}"
|
|
86
|
-
}
|
|
87
|
-
end
|
|
88
|
-
end
|
|
89
90
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
reverse_relation[:Comment] = "HABTM"
|
|
97
|
-
else
|
|
98
|
-
result[:Relations] << {
|
|
99
|
-
LeftModelName: model[:ModelName],
|
|
100
|
-
LeftValue: "}o",
|
|
101
|
-
Line: "..",
|
|
102
|
-
RightModelName: reflection_model_name,
|
|
103
|
-
RightValue: "o{",
|
|
104
|
-
Comment: "HABTM"
|
|
105
|
-
}
|
|
106
|
-
end
|
|
107
|
-
end
|
|
91
|
+
# Polymorphic `belongs_to` has no concrete target class — the target is
|
|
92
|
+
# decided at row level by the `*_type` column. Emitting an edge to the
|
|
93
|
+
# macro name (e.g. `"Imageable"`) would render an orphan node with no
|
|
94
|
+
# columns; the polymorphic parents express the relationship via their
|
|
95
|
+
# `has_many ..., as: :foo` reflections instead.
|
|
96
|
+
next if macro == :belongs_to && reflection.polymorphic?
|
|
108
97
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
next if reflection.polymorphic?
|
|
116
|
-
|
|
117
|
-
reflection_model_name = get_reflection_model_name(reflection)
|
|
118
|
-
next if ignored_model_names.key?(reflection_model_name)
|
|
119
|
-
|
|
120
|
-
reverse_relation = result[:Relations].find { |r| r[:RightModelName] == model[:ModelName] && r[:LeftModelName] == reflection_model_name }
|
|
121
|
-
if reverse_relation
|
|
122
|
-
if (::Rails.application.config.active_record.belongs_to_required_by_default && reflection.options[:optional]) || (!::Rails.application.config.active_record.belongs_to_required_by_default && !reflection.options[:required])
|
|
123
|
-
reverse_relation[:LeftValue] = "|o"
|
|
124
|
-
end
|
|
125
|
-
reverse_relation[:Comment] = "#{reverse_relation[:Comment]}, BT:#{reflection.name}"
|
|
126
|
-
else
|
|
127
|
-
right_value = if (::Rails.application.config.active_record.belongs_to_required_by_default && reflection.options[:optional]) || (!::Rails.application.config.active_record.belongs_to_required_by_default && !reflection.options[:required])
|
|
128
|
-
"o|"
|
|
129
|
-
else
|
|
130
|
-
"||"
|
|
98
|
+
next if ignored_model_names.key?(reflection_model_name)
|
|
99
|
+
|
|
100
|
+
# `has_many` / `has_one` leave the foreign key to the model they
|
|
101
|
+
# point at. A `:through` association stores nothing of its own.
|
|
102
|
+
if [:has_many, :has_one].include?(macro) && !reflection.options[:through]
|
|
103
|
+
association_foreign_keys[reflection_model_name].concat(foreign_key_columns(reflection))
|
|
131
104
|
end
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
Line: "--",
|
|
136
|
-
RightModelName: reflection_model_name,
|
|
137
|
-
RightValue: right_value,
|
|
138
|
-
Comment: "BT:#{reflection.name}"
|
|
139
|
-
}
|
|
105
|
+
|
|
106
|
+
association = {owner: model[:ModelName], macro: macro, reflection: reflection, target: reflection_model_name}
|
|
107
|
+
links[link_key(association)] << association
|
|
140
108
|
end
|
|
141
109
|
end
|
|
110
|
+
end
|
|
142
111
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
reverse_relation = result[:Relations].find { |r|
|
|
148
|
-
if reflection.options[:through]
|
|
149
|
-
r[:RightModelName] == model[:ModelName] && r[:LeftModelName] == reflection_model_name && r[:Line] == ".."
|
|
150
|
-
else
|
|
151
|
-
r[:RightModelName] == model[:ModelName] && r[:LeftModelName] == reflection_model_name && r[:Line] == "--"
|
|
152
|
-
end
|
|
153
|
-
}
|
|
154
|
-
if reverse_relation
|
|
155
|
-
reverse_relation[:LeftValue] = "|o"
|
|
156
|
-
reverse_relation[:Comment] = if reflection.options[:through]
|
|
157
|
-
"#{reverse_relation[:Comment]}, HOT:#{reflection.name}"
|
|
158
|
-
else
|
|
159
|
-
"#{reverse_relation[:Comment]}, HO:#{reflection.name}"
|
|
160
|
-
end
|
|
161
|
-
else
|
|
162
|
-
result[:Relations] << {
|
|
163
|
-
LeftModelName: model[:ModelName],
|
|
164
|
-
LeftValue: reflection.options[:through] ? "}o" : "||",
|
|
165
|
-
Line: reflection.options[:through] ? ".." : "--",
|
|
166
|
-
RightModelName: reflection_model_name,
|
|
167
|
-
RightValue: "o|",
|
|
168
|
-
Comment: reflection.options[:through] ? "HOT:#{reflection.name}" : "HO:#{reflection.name}"
|
|
169
|
-
}
|
|
170
|
-
end
|
|
112
|
+
result[:Models].each do |model|
|
|
113
|
+
model[:Columns].each do |column|
|
|
114
|
+
column[:key] = "FK" if column[:key].empty? && association_foreign_keys[model[:ModelName]].include?(column[:name])
|
|
171
115
|
end
|
|
172
116
|
end
|
|
173
117
|
|
|
118
|
+
result[:Relations] = links.map { |key, associations| relation_for(key, associations) }
|
|
119
|
+
|
|
174
120
|
result
|
|
175
121
|
end
|
|
176
122
|
|
|
@@ -191,6 +137,132 @@ class RailsMermaidErd::Builder
|
|
|
191
137
|
end
|
|
192
138
|
end
|
|
193
139
|
|
|
140
|
+
# Returns a Hash mapping each single table inheritance subclass name to its
|
|
141
|
+
# base class name. A subclass that sets its own `table_name` keeps its own
|
|
142
|
+
# entity, since it no longer shares the base class's table.
|
|
143
|
+
def compute_sti_base_names
|
|
144
|
+
::ActiveRecord::Base.descendants.each_with_object({}) do |defined_model, acc|
|
|
145
|
+
next unless defined_model.table_exists?
|
|
146
|
+
next if defined_model.name.include?("HABTM_")
|
|
147
|
+
base_class = defined_model.base_class
|
|
148
|
+
next if base_class == defined_model
|
|
149
|
+
next unless base_class.table_name == defined_model.table_name
|
|
150
|
+
acc[defined_model.name] = base_class.name
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Returns `[reflection, target model name]` pairs for the associations
|
|
155
|
+
# declared anywhere in an inheritance hierarchy, with targets resolved to
|
|
156
|
+
# their base class. A subclass inherits its base class's reflections, so
|
|
157
|
+
# pairs repeating a name and target are dropped rather than drawn twice.
|
|
158
|
+
def hierarchy_reflections(hierarchy, macro, sti_base_names)
|
|
159
|
+
hierarchy
|
|
160
|
+
.flat_map { |model| model.reflect_on_all_associations(macro) }
|
|
161
|
+
.map { |reflection|
|
|
162
|
+
model_name = get_reflection_model_name(reflection)
|
|
163
|
+
[reflection, sti_base_names.fetch(model_name, model_name)]
|
|
164
|
+
}
|
|
165
|
+
.uniq { |reflection, model_name| [reflection.name, model_name] }
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Identifies the link between two tables that an association describes, so
|
|
169
|
+
# that every association describing the same link is drawn as one line —
|
|
170
|
+
# whichever model declares it and whichever model sorts first — and
|
|
171
|
+
# associations describing different links are never folded together:
|
|
172
|
+
#
|
|
173
|
+
# - A foreign key column is one link: `belongs_to` on the model that holds
|
|
174
|
+
# the column, `has_many` / `has_one` on the model it references.
|
|
175
|
+
# - A join table is one link: both sides of `has_and_belongs_to_many`.
|
|
176
|
+
# - A `:through` association derives from other links and stores nothing
|
|
177
|
+
# of its own, so it is identified by the two models it connects.
|
|
178
|
+
def link_key(association)
|
|
179
|
+
owner, target, reflection = association.values_at(:owner, :target, :reflection)
|
|
180
|
+
|
|
181
|
+
if reflection.options[:through]
|
|
182
|
+
[:through, *[owner, target].sort]
|
|
183
|
+
elsif association[:macro] == :has_and_belongs_to_many
|
|
184
|
+
[:join_table, reflection.join_table.to_s, *[owner, target].sort]
|
|
185
|
+
elsif association[:macro] == :belongs_to
|
|
186
|
+
[:foreign_key, owner, foreign_key_columns(reflection), target]
|
|
187
|
+
else
|
|
188
|
+
[:foreign_key, target, foreign_key_columns(reflection), owner]
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Builds the relation line for one link from every association that
|
|
193
|
+
# describes it, so no glyph depends on the order they were read in.
|
|
194
|
+
def relation_for(key, associations)
|
|
195
|
+
case key.first
|
|
196
|
+
when :foreign_key
|
|
197
|
+
_, child, _, parent = key
|
|
198
|
+
macros = associations.map { |association| association[:macro] }
|
|
199
|
+
optional = associations.any? { |association| association[:macro] == :belongs_to && optional_belongs_to?(association[:reflection]) }
|
|
200
|
+
{
|
|
201
|
+
LeftModelName: parent,
|
|
202
|
+
LeftValue: optional ? "|o" : "||",
|
|
203
|
+
Line: "--",
|
|
204
|
+
RightModelName: child,
|
|
205
|
+
RightValue: (macros.include?(:has_one) && !macros.include?(:has_many)) ? "o|" : "o{",
|
|
206
|
+
Comment: association_labels(associations)
|
|
207
|
+
}
|
|
208
|
+
when :join_table
|
|
209
|
+
_, _, left, right = key
|
|
210
|
+
{LeftModelName: left, LeftValue: "}o", Line: "..", RightModelName: right, RightValue: "o{", Comment: "HABTM"}
|
|
211
|
+
when :through
|
|
212
|
+
_, left, right = key
|
|
213
|
+
# Each end is "at most one" only when every association reaching that
|
|
214
|
+
# end is a `has_one :through`.
|
|
215
|
+
at_most_one = lambda do |model_name|
|
|
216
|
+
reaching = associations.select { |association| association[:target] == model_name }
|
|
217
|
+
reaching.any? && reaching.all? { |association| association[:macro] == :has_one }
|
|
218
|
+
end
|
|
219
|
+
{
|
|
220
|
+
LeftModelName: left,
|
|
221
|
+
LeftValue: at_most_one.call(left) ? "|o" : "}o",
|
|
222
|
+
Line: "..",
|
|
223
|
+
RightModelName: right,
|
|
224
|
+
RightValue: at_most_one.call(right) ? "o|" : "o{",
|
|
225
|
+
Comment: association_labels(associations)
|
|
226
|
+
}
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def association_labels(associations)
|
|
231
|
+
associations.map { |association|
|
|
232
|
+
reflection = association[:reflection]
|
|
233
|
+
prefix = {has_many: "HM", has_one: "HO", belongs_to: "BT"}.fetch(association[:macro])
|
|
234
|
+
prefix = "#{prefix}T" if reflection.options[:through]
|
|
235
|
+
"#{prefix}:#{reflection.name}"
|
|
236
|
+
}.join(", ")
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# Rails may hand back a String or a Symbol, or an Array for a composite key.
|
|
240
|
+
#
|
|
241
|
+
# Rails 7.1+ derives an undeclared key from `inverse_of` or the model's
|
|
242
|
+
# `query_constraints`, and raises when those are misdeclared. The host app
|
|
243
|
+
# only hits that once it uses the association, so one such declaration
|
|
244
|
+
# must not abort the whole diagram: fall back to the naming convention.
|
|
245
|
+
def foreign_key_columns(reflection)
|
|
246
|
+
Array(reflection.foreign_key).map(&:to_s)
|
|
247
|
+
rescue NameError, ArgumentError
|
|
248
|
+
column = if reflection.macro == :belongs_to
|
|
249
|
+
"#{reflection.name}_id"
|
|
250
|
+
elsif reflection.options[:as]
|
|
251
|
+
"#{reflection.options[:as]}_id"
|
|
252
|
+
else
|
|
253
|
+
reflection.active_record.name.foreign_key
|
|
254
|
+
end
|
|
255
|
+
[column]
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def optional_belongs_to?(reflection)
|
|
259
|
+
if ::Rails.application.config.active_record.belongs_to_required_by_default
|
|
260
|
+
reflection.options[:optional]
|
|
261
|
+
else
|
|
262
|
+
!reflection.options[:required]
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
194
266
|
# Doc: https://guides.rubyonrails.org/association_basics.html
|
|
195
267
|
def get_reflection_model_name(reflection)
|
|
196
268
|
if reflection.options[:class_name]
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
require "yaml"
|
|
2
2
|
|
|
3
3
|
class RailsMermaidErd::Configuration
|
|
4
|
-
attr_accessor :result_path, :ignore_tables
|
|
4
|
+
attr_accessor :result_path, :ignore_tables, :viewer_defaults
|
|
5
|
+
|
|
6
|
+
# Mirrors the Columns choice in the viewer's sidebar.
|
|
7
|
+
VIEWER_COLUMNS = %w[all keys none].freeze
|
|
5
8
|
|
|
6
9
|
DEFAULTS = {
|
|
7
10
|
result_path: "mermaid_erd/index.html",
|
|
8
|
-
ignore_tables: []
|
|
11
|
+
ignore_tables: [],
|
|
12
|
+
viewer_defaults: {}
|
|
9
13
|
}.freeze
|
|
10
14
|
|
|
11
15
|
def initialize
|
|
@@ -23,6 +27,7 @@ class RailsMermaidErd::Configuration
|
|
|
23
27
|
|
|
24
28
|
@result_path = config[:result_path]
|
|
25
29
|
@ignore_tables = normalize_ignore_tables(config[:ignore_tables])
|
|
30
|
+
@viewer_defaults = normalize_viewer_defaults(config[:viewer_defaults])
|
|
26
31
|
end
|
|
27
32
|
|
|
28
33
|
private
|
|
@@ -42,4 +47,51 @@ class RailsMermaidErd::Configuration
|
|
|
42
47
|
|
|
43
48
|
value
|
|
44
49
|
end
|
|
50
|
+
|
|
51
|
+
# The two keys are read one by one rather than merged over a default hash,
|
|
52
|
+
# so setting only `columns` leaves `models` at its default. Unknown keys are
|
|
53
|
+
# rejected because a typo such as `column: keys` would otherwise be dropped
|
|
54
|
+
# without a trace and the viewer would open the way it always did.
|
|
55
|
+
def normalize_viewer_defaults(value)
|
|
56
|
+
value = {} if value.nil?
|
|
57
|
+
|
|
58
|
+
unless value.is_a?(Hash)
|
|
59
|
+
raise ArgumentError,
|
|
60
|
+
"config/mermaid_erd.yml: `viewer_defaults` must be a mapping with `models` and/or `columns`, got #{value.inspect}"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
unknown = value.keys.map(&:to_s) - %w[models columns]
|
|
64
|
+
unless unknown.empty?
|
|
65
|
+
raise ArgumentError,
|
|
66
|
+
"config/mermaid_erd.yml: unknown `viewer_defaults` key(s) #{unknown.join(", ")}; use `models` and/or `columns`"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
value = value.transform_keys(&:to_s)
|
|
70
|
+
{
|
|
71
|
+
models: normalize_viewer_models(value["models"]),
|
|
72
|
+
columns: normalize_viewer_columns(value["columns"])
|
|
73
|
+
}
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def normalize_viewer_models(value)
|
|
77
|
+
return [] if value.nil?
|
|
78
|
+
|
|
79
|
+
unless value.is_a?(Array) && value.all? { |entry| entry.is_a?(String) && !entry.empty? }
|
|
80
|
+
raise ArgumentError,
|
|
81
|
+
"config/mermaid_erd.yml: `viewer_defaults.models` must be an array of non-empty model names, got #{value.inspect}"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
value.uniq
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def normalize_viewer_columns(value)
|
|
88
|
+
return "all" if value.nil?
|
|
89
|
+
|
|
90
|
+
unless VIEWER_COLUMNS.include?(value)
|
|
91
|
+
raise ArgumentError,
|
|
92
|
+
"config/mermaid_erd.yml: `viewer_defaults.columns` must be one of #{VIEWER_COLUMNS.join(", ")}, got #{value.inspect}"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
value
|
|
96
|
+
end
|
|
45
97
|
end
|
|
@@ -6,13 +6,14 @@ class RailsMermaidErd::MermaidText
|
|
|
6
6
|
# (lib/templates/index.html.erb): the dump always shows every table, every
|
|
7
7
|
# column with its key marker and comment, and every relation label — i.e.
|
|
8
8
|
# the diagram the viewer produces with all of its detail toggles ("Show
|
|
9
|
-
# key", "Show comment", "Show
|
|
10
|
-
# model-selection filtering. The viewer
|
|
11
|
-
# dump is deliberately more detailed than
|
|
12
|
-
# thing it drops is the viewer's
|
|
13
|
-
# browser-only UI state and is
|
|
14
|
-
#
|
|
15
|
-
# it is byte-identical to the
|
|
9
|
+
# key", "Show column comment", "Show table comment", "Show relationship
|
|
10
|
+
# comment") enabled and with no model-selection filtering. The viewer
|
|
11
|
+
# defaults those toggles off, so the dump is deliberately more detailed than
|
|
12
|
+
# the viewer's initial view. The only thing it drops is the viewer's
|
|
13
|
+
# "Restore Hash" comment line, which encodes browser-only UI state and is
|
|
14
|
+
# meaningless on the command line. Apart from that line and a single trailing
|
|
15
|
+
# newline (added by the rake task's `puts`), it is byte-identical to the
|
|
16
|
+
# viewer's "all toggles on" output.
|
|
16
17
|
#
|
|
17
18
|
# Comment values come from arbitrary DB metadata, so they are sanitised the
|
|
18
19
|
# same way the viewer's renderer must (and now does): newlines collapse to a
|
|
@@ -39,7 +40,12 @@ class RailsMermaidErd::MermaidText
|
|
|
39
40
|
lines << " %% table comment: #{one_line(model[:TableComment])}"
|
|
40
41
|
# Mermaid entity names can't contain ":", so namespaced models like
|
|
41
42
|
# `Admin::User` are written as `Admin-User` (matches the front-end).
|
|
42
|
-
|
|
43
|
+
name = model[:ModelName].tr(":", "-")
|
|
44
|
+
# A model with a table comment gets Mermaid's entity alias, so its
|
|
45
|
+
# heading reads "Model / comment" while relations keep using the name.
|
|
46
|
+
comment = one_line(model[:TableComment])
|
|
47
|
+
alias_label = comment.empty? ? "" : %(["#{name} / #{comment.gsub('"', "#quot;")}"])
|
|
48
|
+
lines << " #{name}#{alias_label} {"
|
|
43
49
|
model[:Columns].each do |column|
|
|
44
50
|
lines << " #{column[:type]} #{column[:name]} #{column[:key]} #{quoted(column[:comment])}"
|
|
45
51
|
end
|
data/lib/tasks/mermaid_erd.rake
CHANGED
|
@@ -12,12 +12,36 @@ desc "Generate Mermaid ERD."
|
|
|
12
12
|
task mermaid_erd: :environment do
|
|
13
13
|
result = RailsMermaidErd::Builder.model_data
|
|
14
14
|
|
|
15
|
+
viewer_defaults = RailsMermaidErd.configuration.viewer_defaults
|
|
16
|
+
# A model that was renamed, or whose table is now in `ignore_tables`, would
|
|
17
|
+
# otherwise leave the viewer opening on a selection that quietly lost a
|
|
18
|
+
# model. Don't fail the build over it — the diagram is still usable — but say
|
|
19
|
+
# which names were dropped.
|
|
20
|
+
unknown_models = viewer_defaults[:models] - result[:Models].map { |model| model[:ModelName] }
|
|
21
|
+
unless unknown_models.empty?
|
|
22
|
+
warn "rails-mermaid_erd: `viewer_defaults.models` in config/mermaid_erd.yml lists " \
|
|
23
|
+
"#{unknown_models.join(", ")}, which #{unknown_models.one? ? "is" : "are"} not in the diagram; not preselecting."
|
|
24
|
+
viewer_defaults = viewer_defaults.merge(models: viewer_defaults[:models] - unknown_models)
|
|
25
|
+
end
|
|
26
|
+
|
|
15
27
|
version = RailsMermaidErd::VERSION
|
|
16
28
|
app_name = ::Rails.application.class.try(:parent_name) || ::Rails.application.class.try(:module_parent_name)
|
|
17
29
|
logo = RailsMermaidErd.read_gem_asset("./assets/logo.svg")
|
|
18
|
-
|
|
30
|
+
tailwind_css = RailsMermaidErd.read_gem_asset("./templates/vendor/tailwind.css")
|
|
19
31
|
mermaid_js = RailsMermaidErd.read_gem_asset("./templates/vendor/mermaid.min.js")
|
|
20
32
|
vue_js = RailsMermaidErd.read_gem_asset("./templates/vendor/vue.global.prod.min.js")
|
|
33
|
+
# Everything in the page is redistributed: the viewer under the gem's own
|
|
34
|
+
# license, the bundled libraries under theirs. Carrying the notices in the
|
|
35
|
+
# file keeps a shared copy within those licenses.
|
|
36
|
+
license_notices = <<~NOTICE.strip
|
|
37
|
+
This file was generated by rails-mermaid_erd, which is distributed under the MIT License:
|
|
38
|
+
|
|
39
|
+
#{RailsMermaidErd.read_gem_asset("../LICENSE").strip}
|
|
40
|
+
|
|
41
|
+
----------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
#{RailsMermaidErd.read_gem_asset("./templates/vendor/LICENSES.md").strip}
|
|
44
|
+
NOTICE
|
|
21
45
|
erb = ERB.new(RailsMermaidErd.read_gem_asset("./templates/index.html.erb"))
|
|
22
46
|
result_html = erb.result(binding)
|
|
23
47
|
|