rails-schema 0.1.4 → 0.1.6
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/CHANGELOG.md +25 -0
- data/CLAUDE.md +18 -7
- data/README.md +67 -2
- data/docs/index.html +936 -76
- data/docs/screenshot.png +0 -0
- data/lib/rails/schema/assets/app.js +496 -41
- data/lib/rails/schema/assets/style.css +119 -4
- data/lib/rails/schema/assets/template.html.erb +8 -2
- data/lib/rails/schema/configuration.rb +17 -1
- data/lib/rails/schema/extractor/model_scanner.rb +21 -20
- data/lib/rails/schema/extractor/mongoid/model_scanner.rb +2 -0
- data/lib/rails/schema/extractor/packwerk_discovery.rb +41 -0
- data/lib/rails/schema/renderer/html_generator.rb +4 -1
- data/lib/rails/schema/transformer/edge.rb +6 -1
- data/lib/rails/schema/transformer/graph_builder.rb +57 -5
- data/lib/rails/schema/transformer/node.rb +6 -7
- data/lib/rails/schema/version.rb +1 -1
- data/lib/rails/schema.rb +1 -0
- metadata +4 -6
|
@@ -99,7 +99,7 @@ body {
|
|
|
99
99
|
#toolbar {
|
|
100
100
|
position: fixed;
|
|
101
101
|
top: 0;
|
|
102
|
-
left:
|
|
102
|
+
left: 0;
|
|
103
103
|
right: 0;
|
|
104
104
|
height: var(--toolbar-height);
|
|
105
105
|
background: var(--bg-secondary);
|
|
@@ -108,7 +108,7 @@ body {
|
|
|
108
108
|
align-items: center;
|
|
109
109
|
padding: 0 16px;
|
|
110
110
|
gap: 8px;
|
|
111
|
-
z-index:
|
|
111
|
+
z-index: 200;
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
#toolbar button {
|
|
@@ -135,6 +135,13 @@ body {
|
|
|
135
135
|
text-align: center;
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
+
.toolbar-divider {
|
|
139
|
+
width: 1px;
|
|
140
|
+
height: 24px;
|
|
141
|
+
background: var(--border-color);
|
|
142
|
+
margin: 0 4px;
|
|
143
|
+
}
|
|
144
|
+
|
|
138
145
|
#toolbar .shortcuts-hint {
|
|
139
146
|
font-size: 11px;
|
|
140
147
|
color: var(--text-muted);
|
|
@@ -148,7 +155,8 @@ body {
|
|
|
148
155
|
border-right: 1px solid var(--border-color);
|
|
149
156
|
display: flex;
|
|
150
157
|
flex-direction: column;
|
|
151
|
-
|
|
158
|
+
margin-top: var(--toolbar-height);
|
|
159
|
+
height: calc(100vh - var(--toolbar-height));
|
|
152
160
|
z-index: 101;
|
|
153
161
|
}
|
|
154
162
|
|
|
@@ -163,15 +171,39 @@ body {
|
|
|
163
171
|
margin-bottom: 12px;
|
|
164
172
|
}
|
|
165
173
|
|
|
174
|
+
#search-wrapper {
|
|
175
|
+
position: relative;
|
|
176
|
+
}
|
|
177
|
+
|
|
166
178
|
#search-input {
|
|
167
179
|
width: 100%;
|
|
168
|
-
padding: 8px 12px;
|
|
180
|
+
padding: 8px 28px 8px 12px;
|
|
169
181
|
border: 1px solid var(--border-color);
|
|
170
182
|
border-radius: 6px;
|
|
171
183
|
background: var(--bg-primary);
|
|
172
184
|
color: var(--text-primary);
|
|
173
185
|
font-size: 13px;
|
|
174
186
|
outline: none;
|
|
187
|
+
box-sizing: border-box;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
#search-clear {
|
|
191
|
+
position: absolute;
|
|
192
|
+
right: 4px;
|
|
193
|
+
top: 50%;
|
|
194
|
+
transform: translateY(-50%);
|
|
195
|
+
background: none;
|
|
196
|
+
border: none;
|
|
197
|
+
color: var(--text-secondary);
|
|
198
|
+
font-size: 16px;
|
|
199
|
+
cursor: pointer;
|
|
200
|
+
padding: 2px 6px;
|
|
201
|
+
line-height: 1;
|
|
202
|
+
display: none;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
#search-clear:hover {
|
|
206
|
+
color: var(--text-primary);
|
|
175
207
|
}
|
|
176
208
|
|
|
177
209
|
#search-input:focus {
|
|
@@ -200,6 +232,10 @@ body {
|
|
|
200
232
|
background: var(--bg-tertiary);
|
|
201
233
|
}
|
|
202
234
|
|
|
235
|
+
#collapse-groups-btn {
|
|
236
|
+
margin-left: auto;
|
|
237
|
+
}
|
|
238
|
+
|
|
203
239
|
#model-list {
|
|
204
240
|
flex: 1;
|
|
205
241
|
overflow-y: auto;
|
|
@@ -346,6 +382,14 @@ body {
|
|
|
346
382
|
pointer-events: none;
|
|
347
383
|
}
|
|
348
384
|
|
|
385
|
+
.edge-label.belongs_to { fill: var(--edge-belongs-to); }
|
|
386
|
+
.edge-label.has_many { fill: var(--edge-has-many); }
|
|
387
|
+
.edge-label.has_one { fill: var(--edge-has-one); }
|
|
388
|
+
.edge-label.has_and_belongs_to_many { fill: var(--edge-habtm); }
|
|
389
|
+
.edge-label.embeds_many { fill: var(--edge-embeds-many); }
|
|
390
|
+
.edge-label.embeds_one { fill: var(--edge-embeds-one); }
|
|
391
|
+
.edge-label.embedded_in { fill: var(--edge-embedded-in); }
|
|
392
|
+
|
|
349
393
|
.edge-label-bg {
|
|
350
394
|
fill: var(--bg-primary);
|
|
351
395
|
opacity: 0.85;
|
|
@@ -430,6 +474,10 @@ body {
|
|
|
430
474
|
.column-list .col-name {
|
|
431
475
|
font-family: "SF Mono", "Fira Code", monospace;
|
|
432
476
|
font-weight: 500;
|
|
477
|
+
overflow: hidden;
|
|
478
|
+
text-overflow: ellipsis;
|
|
479
|
+
white-space: nowrap;
|
|
480
|
+
min-width: 0;
|
|
433
481
|
}
|
|
434
482
|
|
|
435
483
|
.column-list .col-name.pk {
|
|
@@ -440,6 +488,8 @@ body {
|
|
|
440
488
|
color: var(--text-muted);
|
|
441
489
|
font-family: "SF Mono", "Fira Code", monospace;
|
|
442
490
|
font-size: 11px;
|
|
491
|
+
flex-shrink: 0;
|
|
492
|
+
margin-left: 8px;
|
|
443
493
|
}
|
|
444
494
|
|
|
445
495
|
.assoc-list {
|
|
@@ -499,6 +549,15 @@ body {
|
|
|
499
549
|
gap: 4px;
|
|
500
550
|
}
|
|
501
551
|
|
|
552
|
+
.legend-toggle {
|
|
553
|
+
cursor: pointer;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
.legend-toggle input[type="checkbox"] {
|
|
557
|
+
margin: 0;
|
|
558
|
+
cursor: pointer;
|
|
559
|
+
}
|
|
560
|
+
|
|
502
561
|
.legend-line {
|
|
503
562
|
width: 20px;
|
|
504
563
|
height: 2px;
|
|
@@ -545,3 +604,59 @@ body {
|
|
|
545
604
|
);
|
|
546
605
|
background-color: transparent;
|
|
547
606
|
}
|
|
607
|
+
|
|
608
|
+
/* Sidebar group headers */
|
|
609
|
+
.group-header {
|
|
610
|
+
display: flex;
|
|
611
|
+
align-items: center;
|
|
612
|
+
padding: 6px 16px;
|
|
613
|
+
font-size: 12px;
|
|
614
|
+
font-weight: 700;
|
|
615
|
+
border-bottom: 1px solid var(--border-color);
|
|
616
|
+
color: var(--text-secondary);
|
|
617
|
+
text-transform: uppercase;
|
|
618
|
+
letter-spacing: 0.5px;
|
|
619
|
+
cursor: pointer;
|
|
620
|
+
user-select: none;
|
|
621
|
+
gap: 6px;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
.group-header:hover {
|
|
625
|
+
background: var(--bg-tertiary);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
.group-header.active {
|
|
629
|
+
background: var(--accent-light);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
.group-count {
|
|
633
|
+
font-size: 11px;
|
|
634
|
+
color: var(--text-secondary);
|
|
635
|
+
font-weight: normal;
|
|
636
|
+
margin-left: auto;
|
|
637
|
+
flex-shrink: 0;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
.group-toggle {
|
|
641
|
+
font-size: 10px;
|
|
642
|
+
width: 12px;
|
|
643
|
+
flex-shrink: 0;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
.group-color-dot {
|
|
647
|
+
width: 10px;
|
|
648
|
+
height: 10px;
|
|
649
|
+
border-radius: 50%;
|
|
650
|
+
display: inline-block;
|
|
651
|
+
flex-shrink: 0;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
.group-header-label {
|
|
655
|
+
overflow: hidden;
|
|
656
|
+
text-overflow: ellipsis;
|
|
657
|
+
white-space: nowrap;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
.group-models {
|
|
661
|
+
border-bottom: 1px solid var(--border-color);
|
|
662
|
+
}
|
|
@@ -11,11 +11,15 @@
|
|
|
11
11
|
<div id="sidebar">
|
|
12
12
|
<div id="sidebar-header">
|
|
13
13
|
<h1><%= title %></h1>
|
|
14
|
-
<
|
|
14
|
+
<div id="search-wrapper">
|
|
15
|
+
<input type="text" id="search-input" placeholder="Search models... ( / )" autocomplete="off">
|
|
16
|
+
<button id="search-clear" type="button" aria-label="Clear search">×</button>
|
|
17
|
+
</div>
|
|
15
18
|
</div>
|
|
16
19
|
<div id="sidebar-actions">
|
|
17
20
|
<button id="select-all-btn">Select All</button>
|
|
18
21
|
<button id="deselect-all-btn">Deselect All</button>
|
|
22
|
+
<button id="collapse-groups-btn" style="display:none" title="Collapse all groups">▼</button>
|
|
19
23
|
</div>
|
|
20
24
|
<div id="model-list"></div>
|
|
21
25
|
</div>
|
|
@@ -28,7 +32,7 @@
|
|
|
28
32
|
<span class="legend-item"><span class="legend-line habtm"></span> habtm (M:M)</span>
|
|
29
33
|
<span class="legend-item" data-mode="mongoid"><span class="legend-line embeds_many"></span> embeds_many</span>
|
|
30
34
|
<span class="legend-item" data-mode="mongoid"><span class="legend-line embeds_one"></span> embeds_one</span>
|
|
31
|
-
<
|
|
35
|
+
<label class="legend-item legend-toggle" data-mode="active_record"><input type="checkbox" id="toggle-through" checked><span class="legend-line through"></span> :through</label>
|
|
32
36
|
<span class="legend-item"><span class="legend-line polymorphic"></span> polymorphic</span>
|
|
33
37
|
</div>
|
|
34
38
|
<div class="spacer"></div>
|
|
@@ -37,6 +41,8 @@
|
|
|
37
41
|
<button id="zoom-out-btn" title="Zoom out (-)">−</button>
|
|
38
42
|
<button id="fit-btn" title="Fit to screen (F)">Fit</button>
|
|
39
43
|
<button id="theme-btn" title="Toggle theme">Theme</button>
|
|
44
|
+
<span class="toolbar-divider"></span>
|
|
45
|
+
<button id="export-mermaid-btn" title="Export as Mermaid (Markdown)">Export Mermaid</button>
|
|
40
46
|
<span class="shortcuts-hint">/ search · Esc deselect · F fit</span>
|
|
41
47
|
</div>
|
|
42
48
|
|
|
@@ -3,15 +3,31 @@
|
|
|
3
3
|
module Rails
|
|
4
4
|
module Schema
|
|
5
5
|
class Configuration
|
|
6
|
-
attr_accessor :output_path, :exclude_models, :title, :theme, :expand_columns, :schema_format
|
|
6
|
+
attr_accessor :output_path, :exclude_models, :exclude_model_if, :title, :theme, :expand_columns, :schema_format,
|
|
7
|
+
:model_schema_group, :collapse_groups, :show_through_edges
|
|
7
8
|
|
|
8
9
|
def initialize
|
|
9
10
|
@output_path = "docs/schema.html"
|
|
10
11
|
@exclude_models = []
|
|
12
|
+
@exclude_model_if = nil
|
|
11
13
|
@title = "Database Schema"
|
|
12
14
|
@theme = :auto
|
|
13
15
|
@expand_columns = false
|
|
14
16
|
@schema_format = :auto
|
|
17
|
+
@model_schema_group = nil
|
|
18
|
+
@collapse_groups = true
|
|
19
|
+
@show_through_edges = true
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def resolved_group_proc
|
|
23
|
+
case @model_schema_group
|
|
24
|
+
when nil then nil
|
|
25
|
+
when :namespaces
|
|
26
|
+
->(model) { model.name.split("::")[0..-2] }
|
|
27
|
+
when Proc then @model_schema_group
|
|
28
|
+
else
|
|
29
|
+
raise ArgumentError, "model_schema_group must be nil, :namespaces, or a Proc"
|
|
30
|
+
end
|
|
15
31
|
end
|
|
16
32
|
end
|
|
17
33
|
end
|
|
@@ -36,13 +36,9 @@ module Rails
|
|
|
36
36
|
|
|
37
37
|
def eager_load_via_zeitwerk!
|
|
38
38
|
loader = ::Rails.autoloaders.main
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
loader.eager_load_dir(models_path)
|
|
43
|
-
else
|
|
44
|
-
loader.eager_load
|
|
45
|
-
end
|
|
39
|
+
dirs = model_directories
|
|
40
|
+
dirs.each { |dir| loader.eager_load_dir(dir) } if dirs.any? && loader.respond_to?(:eager_load_dir)
|
|
41
|
+
loader.eager_load if no_concrete_descendants?
|
|
46
42
|
rescue StandardError, LoadError => e
|
|
47
43
|
warn "[rails-schema] Zeitwerk eager_load failed (#{e.class}: #{e.message}), " \
|
|
48
44
|
"trying Rails.application.eager_load!"
|
|
@@ -60,22 +56,29 @@ module Rails
|
|
|
60
56
|
def eager_load_model_files!
|
|
61
57
|
return unless defined?(::Rails.root) && ::Rails.root
|
|
62
58
|
|
|
63
|
-
|
|
64
|
-
return
|
|
59
|
+
dirs = model_directories
|
|
60
|
+
return if dirs.empty?
|
|
65
61
|
|
|
66
|
-
Dir.glob(
|
|
62
|
+
dirs.flat_map { |d| Dir.glob(File.join(d, "**/*.rb")) }.sort.each do |file|
|
|
67
63
|
require file
|
|
68
64
|
rescue StandardError, LoadError => e
|
|
69
65
|
warn "[rails-schema] Could not load #{file}: #{e.class}: #{e.message}"
|
|
70
66
|
end
|
|
71
67
|
end
|
|
72
68
|
|
|
69
|
+
def model_directories
|
|
70
|
+
dirs = PackwerkDiscovery.new.model_paths
|
|
71
|
+
default_path = ::Rails.root&.join("app", "models")&.to_s
|
|
72
|
+
dirs << default_path if default_path && !dirs.include?(default_path)
|
|
73
|
+
dirs.select { |d| d && File.directory?(d) }.uniq
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def no_concrete_descendants?
|
|
77
|
+
ActiveRecord::Base.descendants.none? { |m| !m.abstract_class? && m.name }
|
|
78
|
+
end
|
|
79
|
+
|
|
73
80
|
def table_known?(model)
|
|
74
|
-
|
|
75
|
-
@schema_data.key?(model.table_name)
|
|
76
|
-
else
|
|
77
|
-
safe_table_exists?(model)
|
|
78
|
-
end
|
|
81
|
+
@schema_data ? @schema_data.key?(model.table_name) : safe_table_exists?(model)
|
|
79
82
|
end
|
|
80
83
|
|
|
81
84
|
def safe_table_exists?(model)
|
|
@@ -86,12 +89,10 @@ module Rails
|
|
|
86
89
|
end
|
|
87
90
|
|
|
88
91
|
def excluded?(model)
|
|
92
|
+
return true if @configuration.exclude_model_if&.call(model)
|
|
93
|
+
|
|
89
94
|
@configuration.exclude_models.any? do |pattern|
|
|
90
|
-
|
|
91
|
-
model.name.start_with?(pattern.delete_suffix("*"))
|
|
92
|
-
else
|
|
93
|
-
model.name == pattern
|
|
94
|
-
end
|
|
95
|
+
pattern.end_with?("*") ? model.name.start_with?(pattern.delete_suffix("*")) : model.name == pattern
|
|
95
96
|
end
|
|
96
97
|
end
|
|
97
98
|
|
|
@@ -120,6 +120,8 @@ module Rails
|
|
|
120
120
|
end
|
|
121
121
|
|
|
122
122
|
def excluded?(model)
|
|
123
|
+
return true if @configuration.exclude_model_if&.call(model)
|
|
124
|
+
|
|
123
125
|
@configuration.exclude_models.any? do |pattern|
|
|
124
126
|
if pattern.end_with?("*")
|
|
125
127
|
model.name.start_with?(pattern.delete_suffix("*"))
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rails
|
|
4
|
+
module Schema
|
|
5
|
+
module Extractor
|
|
6
|
+
class PackwerkDiscovery
|
|
7
|
+
def model_paths
|
|
8
|
+
return [] unless defined?(::Rails.root) && ::Rails.root
|
|
9
|
+
|
|
10
|
+
find_package_directories.flat_map do |dir|
|
|
11
|
+
%w[app/models app/public].map { |sub| File.join(dir, sub) }
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def find_package_directories
|
|
18
|
+
patterns = package_paths
|
|
19
|
+
return [] if patterns.empty?
|
|
20
|
+
|
|
21
|
+
patterns.flat_map do |pattern|
|
|
22
|
+
Dir.glob(::Rails.root.join(pattern).to_s).select do |path|
|
|
23
|
+
File.directory?(path) && File.exist?(File.join(path, "package.yml"))
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def package_paths
|
|
29
|
+
packwerk_yml = ::Rails.root.join("packwerk.yml")
|
|
30
|
+
return [] unless File.exist?(packwerk_yml)
|
|
31
|
+
|
|
32
|
+
require "yaml"
|
|
33
|
+
config = YAML.safe_load(File.read(packwerk_yml)) || {}
|
|
34
|
+
config["package_paths"] || ["**/"]
|
|
35
|
+
rescue StandardError
|
|
36
|
+
[]
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -61,7 +61,10 @@ module Rails
|
|
|
61
61
|
def config_json
|
|
62
62
|
config = {
|
|
63
63
|
expand_columns: @configuration.expand_columns,
|
|
64
|
-
theme: @configuration.theme.to_s
|
|
64
|
+
theme: @configuration.theme.to_s,
|
|
65
|
+
grouping_enabled: !@configuration.model_schema_group.nil?,
|
|
66
|
+
collapse_groups: @configuration.collapse_groups,
|
|
67
|
+
show_through_edges: @configuration.show_through_edges
|
|
65
68
|
}
|
|
66
69
|
JSON.generate(config).gsub("</", '<\/')
|
|
67
70
|
end
|
|
@@ -5,6 +5,7 @@ module Rails
|
|
|
5
5
|
module Transformer
|
|
6
6
|
class Edge
|
|
7
7
|
attr_reader :from, :to, :association_type, :label, :foreign_key, :through, :polymorphic
|
|
8
|
+
attr_accessor :reverse_label, :reverse_association_type
|
|
8
9
|
|
|
9
10
|
def initialize(from:, to:, association_type:, label:, foreign_key: nil, through: nil, polymorphic: false)
|
|
10
11
|
@from = from
|
|
@@ -14,6 +15,8 @@ module Rails
|
|
|
14
15
|
@foreign_key = foreign_key
|
|
15
16
|
@through = through
|
|
16
17
|
@polymorphic = polymorphic
|
|
18
|
+
@reverse_label = nil
|
|
19
|
+
@reverse_association_type = nil
|
|
17
20
|
end
|
|
18
21
|
|
|
19
22
|
def to_h
|
|
@@ -24,7 +27,9 @@ module Rails
|
|
|
24
27
|
label: @label,
|
|
25
28
|
foreign_key: @foreign_key,
|
|
26
29
|
through: @through,
|
|
27
|
-
polymorphic: @polymorphic
|
|
30
|
+
polymorphic: @polymorphic,
|
|
31
|
+
reverse_label: @reverse_label,
|
|
32
|
+
reverse_association_type: @reverse_association_type
|
|
28
33
|
}
|
|
29
34
|
end
|
|
30
35
|
end
|
|
@@ -6,18 +6,19 @@ module Rails
|
|
|
6
6
|
module Schema
|
|
7
7
|
module Transformer
|
|
8
8
|
class GraphBuilder
|
|
9
|
-
def initialize(column_reader: Extractor::ColumnReader.new, association_reader: Extractor::AssociationReader.new
|
|
9
|
+
def initialize(column_reader: Extractor::ColumnReader.new, association_reader: Extractor::AssociationReader.new,
|
|
10
|
+
configuration: ::Rails::Schema.configuration)
|
|
10
11
|
@column_reader = column_reader
|
|
11
12
|
@association_reader = association_reader
|
|
13
|
+
@group_proc = configuration.resolved_group_proc
|
|
12
14
|
end
|
|
13
15
|
|
|
14
16
|
def build(models)
|
|
15
17
|
model_ids = assign_unique_ids(models)
|
|
16
|
-
name_to_id =
|
|
17
|
-
model_ids.each { |m, uid| name_to_id[m.name] ||= uid }
|
|
18
|
+
name_to_id = build_name_to_id(model_ids)
|
|
18
19
|
|
|
19
20
|
nodes = model_ids.map { |m, uid| build_node(m, uid) }
|
|
20
|
-
edges = model_ids.flat_map { |m, uid| build_edges(m, uid, name_to_id) }
|
|
21
|
+
edges = deduplicate_edges(model_ids.flat_map { |m, uid| build_edges(m, uid, name_to_id) })
|
|
21
22
|
|
|
22
23
|
{
|
|
23
24
|
nodes: nodes.map(&:to_h),
|
|
@@ -28,6 +29,10 @@ module Rails
|
|
|
28
29
|
|
|
29
30
|
private
|
|
30
31
|
|
|
32
|
+
def build_name_to_id(model_ids)
|
|
33
|
+
model_ids.each_with_object({}) { |(m, uid), map| map[m.name] ||= uid }
|
|
34
|
+
end
|
|
35
|
+
|
|
31
36
|
def assign_unique_ids(models)
|
|
32
37
|
counts = models.group_by(&:name).transform_values(&:size)
|
|
33
38
|
models.map do |m|
|
|
@@ -37,10 +42,12 @@ module Rails
|
|
|
37
42
|
end
|
|
38
43
|
|
|
39
44
|
def build_node(model, unique_id)
|
|
45
|
+
group = @group_proc ? Array(@group_proc.call(model)) : []
|
|
40
46
|
Node.new(
|
|
41
47
|
id: unique_id,
|
|
42
48
|
table_name: model.table_name,
|
|
43
|
-
columns: @column_reader.read(model)
|
|
49
|
+
columns: @column_reader.read(model),
|
|
50
|
+
group: group
|
|
44
51
|
)
|
|
45
52
|
end
|
|
46
53
|
|
|
@@ -60,6 +67,51 @@ module Rails
|
|
|
60
67
|
end
|
|
61
68
|
end
|
|
62
69
|
|
|
70
|
+
def deduplicate_edges(edges)
|
|
71
|
+
edges = deduplicate_habtm(edges)
|
|
72
|
+
deduplicate_has_many_belongs_to(edges)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def deduplicate_habtm(edges)
|
|
76
|
+
habtm_first = {}
|
|
77
|
+
|
|
78
|
+
edges.each_with_object([]) do |edge, result|
|
|
79
|
+
next result << edge unless edge.association_type == "has_and_belongs_to_many"
|
|
80
|
+
|
|
81
|
+
pair = [edge.from, edge.to].sort
|
|
82
|
+
if habtm_first.key?(pair)
|
|
83
|
+
habtm_first[pair].reverse_label = edge.label
|
|
84
|
+
else
|
|
85
|
+
habtm_first[pair] = edge
|
|
86
|
+
result << edge
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def deduplicate_has_many_belongs_to(edges)
|
|
92
|
+
hm_lookup = build_has_many_lookup(edges)
|
|
93
|
+
|
|
94
|
+
edges.each_with_object([]) do |edge, result|
|
|
95
|
+
if edge.association_type == "belongs_to"
|
|
96
|
+
hm_edge = hm_lookup[[edge.from, edge.to, edge.foreign_key]]
|
|
97
|
+
if hm_edge
|
|
98
|
+
hm_edge.reverse_label = edge.label
|
|
99
|
+
hm_edge.reverse_association_type = edge.association_type
|
|
100
|
+
next
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
result << edge
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def build_has_many_lookup(edges)
|
|
108
|
+
edges.each_with_object({}) do |edge, lookup|
|
|
109
|
+
next unless %w[has_many has_one].include?(edge.association_type)
|
|
110
|
+
|
|
111
|
+
lookup[[edge.to, edge.from, edge.foreign_key]] = edge
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
63
115
|
def build_metadata(models)
|
|
64
116
|
{
|
|
65
117
|
generated_at: Time.now.utc.iso8601,
|
|
@@ -4,20 +4,19 @@ module Rails
|
|
|
4
4
|
module Schema
|
|
5
5
|
module Transformer
|
|
6
6
|
class Node
|
|
7
|
-
attr_reader :id, :table_name, :columns
|
|
7
|
+
attr_reader :id, :table_name, :columns, :group
|
|
8
8
|
|
|
9
|
-
def initialize(id:, table_name:, columns: [])
|
|
9
|
+
def initialize(id:, table_name:, columns: [], group: [])
|
|
10
10
|
@id = id
|
|
11
11
|
@table_name = table_name
|
|
12
12
|
@columns = columns
|
|
13
|
+
@group = group
|
|
13
14
|
end
|
|
14
15
|
|
|
15
16
|
def to_h
|
|
16
|
-
{
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
columns: @columns
|
|
20
|
-
}
|
|
17
|
+
h = { id: @id, table_name: @table_name, columns: @columns }
|
|
18
|
+
h[:group] = @group unless @group.empty?
|
|
19
|
+
h
|
|
21
20
|
end
|
|
22
21
|
end
|
|
23
22
|
end
|
data/lib/rails/schema/version.rb
CHANGED
data/lib/rails/schema.rb
CHANGED
|
@@ -6,6 +6,7 @@ require_relative "schema/transformer/node"
|
|
|
6
6
|
require_relative "schema/transformer/edge"
|
|
7
7
|
require_relative "schema/extractor/schema_file_parser"
|
|
8
8
|
require_relative "schema/extractor/structure_sql_parser"
|
|
9
|
+
require_relative "schema/extractor/packwerk_discovery"
|
|
9
10
|
require_relative "schema/extractor/model_scanner"
|
|
10
11
|
require_relative "schema/extractor/column_reader"
|
|
11
12
|
require_relative "schema/extractor/association_reader"
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rails-schema
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.6
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andrei Kislichenko
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: exe
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: activerecord
|
|
@@ -80,6 +79,7 @@ files:
|
|
|
80
79
|
- lib/rails/schema/extractor/mongoid/column_reader.rb
|
|
81
80
|
- lib/rails/schema/extractor/mongoid/model_adapter.rb
|
|
82
81
|
- lib/rails/schema/extractor/mongoid/model_scanner.rb
|
|
82
|
+
- lib/rails/schema/extractor/packwerk_discovery.rb
|
|
83
83
|
- lib/rails/schema/extractor/schema_file_parser.rb
|
|
84
84
|
- lib/rails/schema/extractor/structure_sql_parser.rb
|
|
85
85
|
- lib/rails/schema/railtie.rb
|
|
@@ -97,7 +97,6 @@ metadata:
|
|
|
97
97
|
source_code_uri: https://github.com/andrew2net/rails-schema
|
|
98
98
|
changelog_uri: https://github.com/andrew2net/rails-schema/blob/main/CHANGELOG.md
|
|
99
99
|
rubygems_mfa_required: 'true'
|
|
100
|
-
post_install_message:
|
|
101
100
|
rdoc_options: []
|
|
102
101
|
require_paths:
|
|
103
102
|
- lib
|
|
@@ -112,8 +111,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
112
111
|
- !ruby/object:Gem::Version
|
|
113
112
|
version: '0'
|
|
114
113
|
requirements: []
|
|
115
|
-
rubygems_version: 3.
|
|
116
|
-
signing_key:
|
|
114
|
+
rubygems_version: 3.6.9
|
|
117
115
|
specification_version: 4
|
|
118
116
|
summary: Interactive HTML visualization of your Rails database schema
|
|
119
117
|
test_files: []
|