rails-mermaid_erd 0.8.1 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 232add69f0437070bb761d0b5d67ac76f16c31b61d8e795534badc027596ffcf
4
- data.tar.gz: 3d44a974b5b17b9ea267916929f1661de5197921f612eabad1f3772a53087b67
3
+ metadata.gz: 1e8b99cba042397b3b3fb6e1db0480aeb839aaf1c4232e24be6ae5e9cac9cb3e
4
+ data.tar.gz: 1980c75216f10e0e6314626784c027c4aab1167f76f043e84c5358ccaa78beac
5
5
  SHA512:
6
- metadata.gz: e245514f42a09847f7b424682cc1c908f5fdbff73eaade20ba034ca72cc6ee73a3bb243ede6fc69eb992dc0eeba5eea134eff3948e906cd7a0a1057a57291298
7
- data.tar.gz: 89ae729846b3cb31550f4576331b6dc88d0aaf3e5fbcd51510b36732405707b457a6e6efc1c94b47c32cbdff5720aaf9e473d492318d889134e6b74d8e3a0f08
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
- primary_key = defined_model.primary_key
62
+ primary_keys = Array(defined_model.primary_key)
44
63
  defined_model.columns.each do |column|
45
64
  key = ""
46
- if column.name == primary_key
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.reflect_on_all_associations(:has_many).each do |reflection|
62
- reflection_model_name = get_reflection_model_name(reflection)
63
- next if ignored_model_names.key?(reflection_model_name)
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
- reverse_relation = result[:Relations].find { |r|
66
- if reflection.options[:through]
67
- r[:RightModelName] == model[:ModelName] && r[:LeftModelName] == reflection_model_name && r[:Line] == ".."
68
- else
69
- r[:RightModelName] == model[:ModelName] && r[:LeftModelName] == reflection_model_name && r[:Line] == "--"
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
- defined_model.reflect_on_all_associations(:has_and_belongs_to_many).each do |reflection|
91
- reflection_model_name = get_reflection_model_name(reflection)
92
- next if ignored_model_names.key?(reflection_model_name)
93
-
94
- reverse_relation = result[:Relations].find { |r| r[:RightModelName] == model[:ModelName] && r[:LeftModelName] == reflection_model_name }
95
- if reverse_relation
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
- defined_model.reflect_on_all_associations(:belongs_to).each do |reflection|
110
- # Polymorphic `belongs_to` has no concrete target class — the target is
111
- # decided at row level by the `*_type` column. Emitting an edge to the
112
- # macro name (e.g. `"Imageable"`) would render an orphan node with no
113
- # columns; the polymorphic parents express the relationship via their
114
- # `has_many ..., as: :foo` reflections instead.
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
- result[:Relations] << {
133
- LeftModelName: model[:ModelName],
134
- LeftValue: "}o",
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
- defined_model.reflect_on_all_associations(:has_one).each do |reflection|
144
- reflection_model_name = get_reflection_model_name(reflection)
145
- next if ignored_model_names.key?(reflection_model_name)
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 relation comment") enabled and with no
10
- # model-selection filtering. The viewer defaults those toggles off, so the
11
- # dump is deliberately more detailed than the viewer's initial view. The only
12
- # thing it drops is the viewer's "Restore Hash" comment line, which encodes
13
- # browser-only UI state and is meaningless on the command line. Apart from
14
- # that line and a single trailing newline (added by the rake task's `puts`),
15
- # it is byte-identical to the viewer's "all toggles on" output.
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
- lines << " #{model[:ModelName].tr(":", "-")} {"
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
@@ -1,3 +1,3 @@
1
1
  module RailsMermaidErd
2
- VERSION = "0.8.1"
2
+ VERSION = "0.8.2"
3
3
  end
@@ -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
- tailwindcss_js = RailsMermaidErd.read_gem_asset("./templates/vendor/tailwindcss.js")
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