rails-schema 0.1.5 → 0.1.7
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 +17 -0
- data/CLAUDE.md +20 -7
- data/README.md +64 -2
- data/docs/index.html +686 -55
- data/docs/screenshot.png +0 -0
- data/lib/rails/schema/assets/app.js +548 -51
- data/lib/rails/schema/assets/style.css +123 -0
- data/lib/rails/schema/assets/template.html.erb +13 -2
- data/lib/rails/schema/configuration.rb +16 -1
- data/lib/rails/schema/extractor/model_scanner.rb +19 -20
- 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/graph_builder.rb +6 -2
- 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
|
@@ -142,11 +142,65 @@ body {
|
|
|
142
142
|
margin: 0 4px;
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
+
#toolbar label.toolbar-label {
|
|
146
|
+
display: flex;
|
|
147
|
+
align-items: center;
|
|
148
|
+
gap: 4px;
|
|
149
|
+
font-size: 13px;
|
|
150
|
+
color: var(--text-secondary);
|
|
151
|
+
cursor: pointer;
|
|
152
|
+
white-space: nowrap;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
#toolbar label.toolbar-label input[type="checkbox"] {
|
|
156
|
+
margin: 0;
|
|
157
|
+
cursor: pointer;
|
|
158
|
+
accent-color: var(--accent);
|
|
159
|
+
}
|
|
160
|
+
|
|
145
161
|
#toolbar .shortcuts-hint {
|
|
146
162
|
font-size: 11px;
|
|
147
163
|
color: var(--text-muted);
|
|
148
164
|
}
|
|
149
165
|
|
|
166
|
+
.toolbar-dropdown {
|
|
167
|
+
position: relative;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.toolbar-dropdown-menu {
|
|
171
|
+
display: none;
|
|
172
|
+
position: absolute;
|
|
173
|
+
top: 100%;
|
|
174
|
+
right: 0;
|
|
175
|
+
margin-top: 4px;
|
|
176
|
+
background: var(--bg-primary);
|
|
177
|
+
border: 1px solid var(--border-color);
|
|
178
|
+
border-radius: 6px;
|
|
179
|
+
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
|
180
|
+
z-index: 300;
|
|
181
|
+
min-width: 160px;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.toolbar-dropdown-menu.open {
|
|
185
|
+
display: flex;
|
|
186
|
+
flex-direction: column;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.toolbar-dropdown-menu button {
|
|
190
|
+
border: none;
|
|
191
|
+
border-radius: 0;
|
|
192
|
+
text-align: left;
|
|
193
|
+
padding: 8px 12px;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.toolbar-dropdown-menu button:first-child {
|
|
197
|
+
border-radius: 6px 6px 0 0;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.toolbar-dropdown-menu button:last-of-type {
|
|
201
|
+
border-radius: 0 0 6px 6px;
|
|
202
|
+
}
|
|
203
|
+
|
|
150
204
|
/* Sidebar */
|
|
151
205
|
#sidebar {
|
|
152
206
|
width: var(--sidebar-width);
|
|
@@ -232,6 +286,10 @@ body {
|
|
|
232
286
|
background: var(--bg-tertiary);
|
|
233
287
|
}
|
|
234
288
|
|
|
289
|
+
#collapse-groups-btn {
|
|
290
|
+
margin-left: auto;
|
|
291
|
+
}
|
|
292
|
+
|
|
235
293
|
#model-list {
|
|
236
294
|
flex: 1;
|
|
237
295
|
overflow-y: auto;
|
|
@@ -545,6 +603,15 @@ body {
|
|
|
545
603
|
gap: 4px;
|
|
546
604
|
}
|
|
547
605
|
|
|
606
|
+
.legend-toggle {
|
|
607
|
+
cursor: pointer;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
.legend-toggle input[type="checkbox"] {
|
|
611
|
+
margin: 0;
|
|
612
|
+
cursor: pointer;
|
|
613
|
+
}
|
|
614
|
+
|
|
548
615
|
.legend-line {
|
|
549
616
|
width: 20px;
|
|
550
617
|
height: 2px;
|
|
@@ -591,3 +658,59 @@ body {
|
|
|
591
658
|
);
|
|
592
659
|
background-color: transparent;
|
|
593
660
|
}
|
|
661
|
+
|
|
662
|
+
/* Sidebar group headers */
|
|
663
|
+
.group-header {
|
|
664
|
+
display: flex;
|
|
665
|
+
align-items: center;
|
|
666
|
+
padding: 6px 16px;
|
|
667
|
+
font-size: 12px;
|
|
668
|
+
font-weight: 700;
|
|
669
|
+
border-bottom: 1px solid var(--border-color);
|
|
670
|
+
color: var(--text-secondary);
|
|
671
|
+
text-transform: uppercase;
|
|
672
|
+
letter-spacing: 0.5px;
|
|
673
|
+
cursor: pointer;
|
|
674
|
+
user-select: none;
|
|
675
|
+
gap: 6px;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
.group-header:hover {
|
|
679
|
+
background: var(--bg-tertiary);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
.group-header.active {
|
|
683
|
+
background: var(--accent-light);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
.group-count {
|
|
687
|
+
font-size: 11px;
|
|
688
|
+
color: var(--text-secondary);
|
|
689
|
+
font-weight: normal;
|
|
690
|
+
margin-left: auto;
|
|
691
|
+
flex-shrink: 0;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
.group-toggle {
|
|
695
|
+
font-size: 10px;
|
|
696
|
+
width: 12px;
|
|
697
|
+
flex-shrink: 0;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
.group-color-dot {
|
|
701
|
+
width: 10px;
|
|
702
|
+
height: 10px;
|
|
703
|
+
border-radius: 50%;
|
|
704
|
+
display: inline-block;
|
|
705
|
+
flex-shrink: 0;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
.group-header-label {
|
|
709
|
+
overflow: hidden;
|
|
710
|
+
text-overflow: ellipsis;
|
|
711
|
+
white-space: nowrap;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
.group-models {
|
|
715
|
+
border-bottom: 1px solid var(--border-color);
|
|
716
|
+
}
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
<div id="sidebar-actions">
|
|
20
20
|
<button id="select-all-btn">Select All</button>
|
|
21
21
|
<button id="deselect-all-btn">Deselect All</button>
|
|
22
|
+
<button id="collapse-groups-btn" style="display:none" title="Collapse all groups">▼</button>
|
|
22
23
|
</div>
|
|
23
24
|
<div id="model-list"></div>
|
|
24
25
|
</div>
|
|
@@ -31,7 +32,7 @@
|
|
|
31
32
|
<span class="legend-item"><span class="legend-line habtm"></span> habtm (M:M)</span>
|
|
32
33
|
<span class="legend-item" data-mode="mongoid"><span class="legend-line embeds_many"></span> embeds_many</span>
|
|
33
34
|
<span class="legend-item" data-mode="mongoid"><span class="legend-line embeds_one"></span> embeds_one</span>
|
|
34
|
-
<
|
|
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>
|
|
35
36
|
<span class="legend-item"><span class="legend-line polymorphic"></span> polymorphic</span>
|
|
36
37
|
</div>
|
|
37
38
|
<div class="spacer"></div>
|
|
@@ -41,7 +42,17 @@
|
|
|
41
42
|
<button id="fit-btn" title="Fit to screen (F)">Fit</button>
|
|
42
43
|
<button id="theme-btn" title="Toggle theme">Theme</button>
|
|
43
44
|
<span class="toolbar-divider"></span>
|
|
44
|
-
<
|
|
45
|
+
<label class="toolbar-label" for="manual-positioning"><input type="checkbox" id="manual-positioning">Manual Positioning</label>
|
|
46
|
+
<span class="toolbar-divider"></span>
|
|
47
|
+
<div class="toolbar-dropdown">
|
|
48
|
+
<button id="file-menu-btn">File ▼</button>
|
|
49
|
+
<div class="toolbar-dropdown-menu" id="file-menu">
|
|
50
|
+
<button id="export-mermaid-btn">Export Mermaid</button>
|
|
51
|
+
<button id="save-layout-btn">Save Layout</button>
|
|
52
|
+
<button id="load-layout-btn">Load Layout</button>
|
|
53
|
+
<input type="file" id="load-layout-input" accept=".json" style="display:none">
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
45
56
|
<span class="shortcuts-hint">/ search · Esc deselect · F fit</span>
|
|
46
57
|
</div>
|
|
47
58
|
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
module Rails
|
|
4
4
|
module Schema
|
|
5
5
|
class Configuration
|
|
6
|
-
attr_accessor :output_path, :exclude_models, :exclude_model_if, :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"
|
|
@@ -13,6 +14,20 @@ module Rails
|
|
|
13
14
|
@theme = :auto
|
|
14
15
|
@expand_columns = false
|
|
15
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
|
|
16
31
|
end
|
|
17
32
|
end
|
|
18
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)
|
|
@@ -89,11 +92,7 @@ module Rails
|
|
|
89
92
|
return true if @configuration.exclude_model_if&.call(model)
|
|
90
93
|
|
|
91
94
|
@configuration.exclude_models.any? do |pattern|
|
|
92
|
-
|
|
93
|
-
model.name.start_with?(pattern.delete_suffix("*"))
|
|
94
|
-
else
|
|
95
|
-
model.name == pattern
|
|
96
|
-
end
|
|
95
|
+
pattern.end_with?("*") ? model.name.start_with?(pattern.delete_suffix("*")) : model.name == pattern
|
|
97
96
|
end
|
|
98
97
|
end
|
|
99
98
|
|
|
@@ -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
|
|
@@ -6,9 +6,11 @@ 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)
|
|
@@ -40,10 +42,12 @@ module Rails
|
|
|
40
42
|
end
|
|
41
43
|
|
|
42
44
|
def build_node(model, unique_id)
|
|
45
|
+
group = @group_proc ? Array(@group_proc.call(model)) : []
|
|
43
46
|
Node.new(
|
|
44
47
|
id: unique_id,
|
|
45
48
|
table_name: model.table_name,
|
|
46
|
-
columns: @column_reader.read(model)
|
|
49
|
+
columns: @column_reader.read(model),
|
|
50
|
+
group: group
|
|
47
51
|
)
|
|
48
52
|
end
|
|
49
53
|
|
|
@@ -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.7
|
|
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: []
|