rails-realtime-erd 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,18 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title><%= @app_name %> - Rails Realtime ERD</title>
6
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
7
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
8
+ <%= csrf_meta_tags %>
9
+ <%= inline_stylesheet "app/assets/stylesheets/rails_realtime_erd/erd.css" %>
10
+ </head>
11
+ <body>
12
+ <%= yield %>
13
+
14
+ <script src="<%= engine_asset_path("stimulus.js") %>"></script>
15
+ <script src="<%= engine_asset_path("mermaid.js") %>"></script>
16
+ <script src="<%= engine_asset_path("application.js") %>"></script>
17
+ </body>
18
+ </html>
@@ -0,0 +1,40 @@
1
+ <div class="space-x-2 inline-flex p-4">
2
+ <button type="button" data-tab-target="erdButton" data-action="click->tab#showErd" class="text-xs py-1 px-2 rounded hover:bg-white focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-gray-900 bg-white text-gray-900">ERD</button>
3
+ <button type="button" data-tab-target="codeButton" data-action="click->tab#showCode" class="text-xs py-1 px-2 rounded hover:bg-white focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-gray-900 bg-gray-400 text-gray-900">Code</button>
4
+ </div>
5
+
6
+ <div data-tab-target="erdPane" class="px-4 w-full min-h-[calc(100vh-56px-32px-56px)] relative overflow-hidden" data-zoom-pan-target="canvas">
7
+ <div data-zoom-pan-target="area">
8
+ <div id="rre-preview" data-diagram-target="preview"></div>
9
+ </div>
10
+
11
+ <div class="absolute bottom-0 left-1/2 -translate-x-1/2 p-4 pointer-events-none">
12
+ <div class="bg-gray-800 text-white text-[10px] px-3 py-1.5 rounded inline-flex items-center space-x-6">
13
+ <div class="inline-flex items-center">
14
+ <span class="font-medium">Movement:</span>
15
+ <span class="ml-1 text-white/60">Space + Mouse Drag / Middle Click + Drag</span>
16
+ </div>
17
+ <div class="inline-flex items-center">
18
+ <span class="font-medium">Zoom:</span>
19
+ <span class="ml-1 text-white/60">Mouse Wheel / Pinch In/Out</span>
20
+ </div>
21
+ </div>
22
+ </div>
23
+
24
+ <div class="absolute bottom-0 right-0 p-4 space-x-4 flex">
25
+ <div class="space-x-2 flex items-center">
26
+ <button type="button" data-action="click->zoom-pan#zoomIn" class="text-xs py-1 px-2 rounded hover:bg-white focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-gray-900 bg-gray-400 text-gray-900">+</button>
27
+ <button type="button" data-action="click->zoom-pan#zoomOut" class="text-xs py-1 px-2 rounded hover:bg-white focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-gray-900 bg-gray-400 text-gray-900">-</button>
28
+ </div>
29
+ <div class="flex items-center space-x-2">
30
+ <button type="button" data-action="click->zoom-pan#moveLeft" class="text-xs py-1 px-2 rounded hover:bg-white focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-gray-900 bg-gray-400 text-gray-900">&larr;</button>
31
+ <div class="flex flex-col space-y-8">
32
+ <button type="button" data-action="click->zoom-pan#moveUp" class="text-xs py-1 px-2 rounded hover:bg-white focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-gray-900 bg-gray-400 text-gray-900">&uarr;</button>
33
+ <button type="button" data-action="click->zoom-pan#moveDown" class="text-xs py-1 px-2 rounded hover:bg-white focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-gray-900 bg-gray-400 text-gray-900">&darr;</button>
34
+ </div>
35
+ <button type="button" data-action="click->zoom-pan#moveRight" class="text-xs py-1 px-2 rounded hover:bg-white focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-gray-900 bg-gray-400 text-gray-900">&rarr;</button>
36
+ </div>
37
+ </div>
38
+ </div>
39
+
40
+ <textarea data-tab-target="codePane" data-rre-hidden data-diagram-target="codeOutput" readonly class="px-4 bg-gray-900 text-gray-300 font-mono w-full text-xs min-h-[calc(100vh-56px-32px-56px)] border-0 focus:ring-0 block"></textarea>
@@ -0,0 +1,106 @@
1
+ <div class="p-2">
2
+ <h2 class="font-medium text-gray-900 mb-1">Actions</h2>
3
+
4
+ <div class="space-y-2">
5
+ <button data-action="click->filter#reset" type="button" class="text-xs border border-gray-300 rounded py-1 w-full hover:bg-gray-100 flex justify-center items-center focus:ring focus:ring-red-600 focus:ring-offset-2">
6
+ <svg class="w-4 h-4 mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M9 15L3 9m0 0l6-6M3 9h12a6 6 0 010 12h-3"/></svg>
7
+ <span>Reset</span>
8
+ </button>
9
+
10
+ <button data-action="click->clipboard#copyUrl" data-clipboard-target="copyUrlButton" type="button" class="text-xs border border-gray-300 rounded py-1 w-full hover:bg-gray-100 flex justify-center items-center focus:ring focus:ring-red-600 focus:ring-offset-2">
11
+ <svg class="w-4 h-4 mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12h3.75M9 15h3.75M9 18h3.75m3 .75H18a2.25 2.25 0 002.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48.424 48.424 0 00-1.123-.08m-5.801 0c-.065.21-.1.433-.1.664 0 .414.336.75.75.75h4.5a.75.75 0 00.75-.75 2.25 2.25 0 00-.1-.664m-5.8 0A2.251 2.251 0 0113.5 2.25H15c1.012 0 1.867.668 2.15 1.586m-5.8 0c-.376.023-.75.05-1.124.08C9.095 4.01 8.25 4.973 8.25 6.108V8.25m0 0H4.875c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V9.375c0-.621-.504-1.125-1.125-1.125H8.25z"/></svg>
12
+ <span data-clipboard-target="copyUrlLabel">Copy Link for Sharing</span>
13
+ </button>
14
+
15
+ <button data-action="click->clipboard#copyMermaid" data-clipboard-target="copyMermaidButton" type="button" class="text-xs border border-gray-300 rounded py-1 w-full hover:bg-gray-100 flex justify-center items-center focus:ring focus:ring-red-600 focus:ring-offset-2">
16
+ <svg class="w-4 h-4 mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12h3.75M9 15h3.75M9 18h3.75m3 .75H18a2.25 2.25 0 002.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48.424 48.424 0 00-1.123-.08m-5.801 0c-.065.21-.1.433-.1.664 0 .414.336.75.75.75h4.5a.75.75 0 00.75-.75 2.25 2.25 0 00-.1-.664m-5.8 0A2.251 2.251 0 0113.5 2.25H15c1.012 0 1.867.668 2.15 1.586m-5.8 0c-.376.023-.75.05-1.124.08C9.095 4.01 8.25 4.973 8.25 6.108V8.25m0 0H4.875c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V9.375c0-.621-.504-1.125-1.125-1.125H8.25z"/></svg>
17
+ <span data-clipboard-target="copyMermaidLabel">Copy Mermaid Code</span>
18
+ </button>
19
+
20
+ <button data-action="click->clipboard#copyMarkdown" data-clipboard-target="copyMarkdownButton" type="button" class="text-xs border border-gray-300 rounded py-1 w-full hover:bg-gray-100 flex justify-center items-center focus:ring focus:ring-red-600 focus:ring-offset-2">
21
+ <svg class="w-4 h-4 mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12h3.75M9 15h3.75M9 18h3.75m3 .75H18a2.25 2.25 0 002.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48.424 48.424 0 00-1.123-.08m-5.801 0c-.065.21-.1.433-.1.664 0 .414.336.75.75.75h4.5a.75.75 0 00.75-.75 2.25 2.25 0 00-.1-.664m-5.8 0A2.251 2.251 0 0113.5 2.25H15c1.012 0 1.867.668 2.15 1.586m-5.8 0c-.376.023-.75.05-1.124.08C9.095 4.01 8.25 4.973 8.25 6.108V8.25m0 0H4.875c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V9.375c0-.621-.504-1.125-1.125-1.125H8.25z"/></svg>
22
+ <span data-clipboard-target="copyMarkdownLabel">Copy Markdown Code</span>
23
+ </button>
24
+
25
+ <button data-action="click->download#downloadSvg" type="button" class="text-xs border border-gray-300 rounded py-1 w-full hover:bg-gray-100 flex justify-center items-center focus:ring focus:ring-red-600 focus:ring-offset-2">
26
+ <svg class="w-4 h-4 mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3"/></svg>
27
+ <span>Download SVG File</span>
28
+ </button>
29
+
30
+ <button data-action="click->download#downloadPng" type="button" class="text-xs border border-gray-300 rounded py-1 w-full hover:bg-gray-100 flex justify-center items-center focus:ring focus:ring-red-600 focus:ring-offset-2">
31
+ <svg class="w-4 h-4 mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3"/></svg>
32
+ <span>Download PNG File</span>
33
+ </button>
34
+ </div>
35
+ </div>
36
+
37
+ <div class="p-2">
38
+ <h2 class="font-medium text-gray-900 mb-1">Options</h2>
39
+
40
+ <div class="space-y-1">
41
+ <label class="relative flex items-start cursor-pointer hover:bg-gray-100 rounded text-sm">
42
+ <input type="checkbox" class="h-4 w-4 rounded border-gray-300 text-red-600 mr-2 focus:ring-red-600" data-filter-target="previewRelations" data-action="change->filter#onOptionChange">
43
+ <span class="text-xs text-gray-900">Preview Relationships</span>
44
+ </label>
45
+
46
+ <label class="relative flex items-start cursor-pointer hover:bg-gray-100 rounded text-sm">
47
+ <input type="checkbox" class="h-4 w-4 rounded border-gray-300 text-red-600 mr-2 focus:ring-red-600" data-filter-target="showRelationComment" data-action="change->filter#onOptionChange">
48
+ <span class="text-xs text-gray-900">Show Relationship Comment</span>
49
+ </label>
50
+
51
+ <label class="relative flex items-start cursor-pointer hover:bg-gray-100 rounded text-sm">
52
+ <input type="checkbox" class="h-4 w-4 rounded border-gray-300 text-red-600 mr-2 focus:ring-red-600" data-filter-target="hideColumns" data-action="change->filter#onOptionChange">
53
+ <span class="text-xs text-gray-900">Hide Columns</span>
54
+ </label>
55
+
56
+ <label class="relative flex items-start hover:bg-gray-100 rounded text-sm" data-filter-target="showKeyLabel">
57
+ <input type="checkbox" class="h-4 w-4 rounded border-gray-300 text-red-600 mr-2 focus:ring-red-600" data-filter-target="showKey" data-action="change->filter#onOptionChange">
58
+ <span class="text-xs text-gray-900">Show Key</span>
59
+ </label>
60
+
61
+ <label class="relative flex items-start hover:bg-gray-100 rounded text-sm" data-filter-target="showCommentLabel">
62
+ <input type="checkbox" class="h-4 w-4 rounded border-gray-300 text-red-600 mr-2 focus:ring-red-600" data-filter-target="showComment" data-action="change->filter#onOptionChange">
63
+ <span class="text-xs text-gray-900">Show Column Comment</span>
64
+ </label>
65
+
66
+ <label class="relative flex items-start hover:bg-gray-100 rounded text-sm" data-filter-target="showOnlyKeysLabel">
67
+ <input type="checkbox" class="h-4 w-4 rounded border-gray-300 text-red-600 mr-2 focus:ring-red-600" data-filter-target="showOnlyKeys" data-action="change->filter#onOptionChange">
68
+ <span class="text-xs text-gray-900">Only Show Keys</span>
69
+ </label>
70
+ </div>
71
+ </div>
72
+
73
+ <div class="p-2">
74
+ <h2 class="font-medium text-gray-900 mb-1">Models</h2>
75
+
76
+ <div>
77
+ <input type="search" placeholder="Filter" data-filter-target="search" data-action="input->filter#onSearchChange" class="rounded text-xs w-full py-1 px-2 border border-gray-300 bg-gray-100 focus:border-red-600 focus:ring-red-600">
78
+ </div>
79
+
80
+ <div class="space-x-1 text-xs text-gray-900 my-1">
81
+ <span>Select</span>
82
+ <button type="button" data-action="click->filter#selectAll" class="text-red-600 font-bold hover:text-red-900 rounded focus:ring-2 focus:ring-red-600 focus:ring-offset-1">All</button>
83
+ <span>/</span>
84
+ <button type="button" data-action="click->filter#unselectAll" class="text-red-600 font-bold hover:text-red-900 rounded focus:ring-2 focus:ring-red-600 focus:ring-offset-1">None</button>
85
+ </div>
86
+
87
+ <div class="space-y-1" data-filter-target="modelList">
88
+ <% @schema[:Models].sort_by { |m| m[:ModelName] }.each do |model| %>
89
+ <label class="relative flex items-start cursor-pointer hover:bg-gray-100 rounded" data-filter-target="modelRow" data-model-name="<%= model[:ModelName] %>" data-table-name="<%= model[:TableName] %>">
90
+ <div class="min-w-0 flex-1 text-xs text-gray-900 inline-flex items-center">
91
+ <input
92
+ type="checkbox"
93
+ class="h-4 w-4 rounded border-gray-300 text-red-600 mr-2 focus:ring-red-600"
94
+ data-filter-target="modelCheckbox"
95
+ data-action="change->filter#onModelToggle"
96
+ value="<%= model[:ModelName] %>"
97
+ >
98
+ <% unless model[:IsModelExist] %>
99
+ <svg class="text-orange-900 mr-1 h-3 w-3" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z"/></svg>
100
+ <% end %>
101
+ <span><%= model[:ModelName] %></span>
102
+ </div>
103
+ </label>
104
+ <% end %>
105
+ </div>
106
+ </div>
@@ -0,0 +1,44 @@
1
+ <%= schema_data_tag(@schema) %>
2
+
3
+ <div
4
+ id="rre-app"
5
+ data-controller="hash-state filter diagram clipboard download tab zoom-pan"
6
+ data-hash-state-filter-outlet="[data-controller~='filter']"
7
+ data-filter-diagram-outlet="[data-controller~='diagram']"
8
+ data-clipboard-diagram-outlet="[data-controller~='diagram']"
9
+ data-download-diagram-outlet="[data-controller~='diagram']"
10
+ >
11
+ <header class="bg-red-600">
12
+ <nav class="mx-auto px-4">
13
+ <div class="flex w-full items-center justify-between py-4">
14
+ <h1 class="text-white inline-flex space-x-2 items-center">
15
+ <svg class="h-6 w-6" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
16
+ <path d="M3 4h18v4H3zM3 10h18v4H3zM3 16h18v4H3z"/>
17
+ </svg>
18
+ <span class="font-bold">Rails Realtime ERD</span>
19
+ </h1>
20
+ <div class="space-x-4 inline-flex items-center">
21
+ <span class="text-white/80 text-xs"><%= @app_name %></span>
22
+ </div>
23
+ </div>
24
+ </nav>
25
+ </header>
26
+
27
+ <div class="flex">
28
+ <aside class="w-[250px] border-r border-gray-300 flex-none p-2">
29
+ <%= render "sidebar" %>
30
+ </aside>
31
+
32
+ <main class="flex-1 bg-gray-900 min-h-[calc(100vh-56px-32px)]">
33
+ <%= render "main" %>
34
+ </main>
35
+ </div>
36
+
37
+ <footer class="bg-gray-100">
38
+ <p class="text-center text-xs text-gray-600 py-2">
39
+ <a href="https://github.com/" class="hover:text-gray-400" target="_blank" rel="noopener noreferrer">
40
+ Rails Realtime ERD v<%= @version %>
41
+ </a>
42
+ </p>
43
+ </footer>
44
+ </div>
data/config/routes.rb ADDED
@@ -0,0 +1,4 @@
1
+ RailsRealtimeErd::Engine.routes.draw do
2
+ root to: "erd#show", as: :erd
3
+ get "assets/:name", to: "assets#show", as: :engine_asset, constraints: {name: /[\w.\-]+/}
4
+ end
@@ -0,0 +1,167 @@
1
+ module RailsRealtimeErd
2
+ class Builder
3
+ class << self
4
+ def model_data
5
+ result = {Models: [], Relations: []}
6
+
7
+ ::Rails.application.eager_load!
8
+ ::ActiveRecord::Base.descendants.sort_by(&:name).each do |defined_model|
9
+ next unless defined_model.table_exists?
10
+ next if defined_model.name.nil?
11
+ next if defined_model.name.include?("HABTM_")
12
+ next if defined_model.table_name.blank?
13
+ next if defined_model.abstract_class?
14
+ next if defined_model.name.start_with?("ActiveRecord::")
15
+ next if defined_model.name.start_with?("ActiveStorage::")
16
+ next if defined_model.name.start_with?("ActionText::")
17
+ next if defined_model.name.start_with?("ActionMailbox::")
18
+
19
+ table_name = defined_model.table_name
20
+ model = {
21
+ TableName: table_name,
22
+ TableComment: ::ActiveRecord::Base.connection.table_comment(table_name.to_sym) || "",
23
+ ModelName: defined_model.name,
24
+ IsModelExist: true,
25
+ Columns: []
26
+ }
27
+
28
+ foreign_keys = ::ActiveRecord::Base.connection.foreign_keys(defined_model.table_name).map { |k| k.options[:column] }
29
+ primary_key = defined_model.primary_key
30
+ defined_model.columns.each do |column|
31
+ key = ""
32
+ if column.name == primary_key
33
+ key = "PK"
34
+ elsif foreign_keys.include?(column.name)
35
+ key = "FK"
36
+ end
37
+ model[:Columns] << {
38
+ name: column.name,
39
+ type: column.type,
40
+ key: key,
41
+ comment: column.comment
42
+ }
43
+ end
44
+
45
+ result[:Models] << model
46
+
47
+ defined_model.reflect_on_all_associations(:has_many).each do |reflection|
48
+ reflection_model_name = get_reflection_model_name(reflection)
49
+
50
+ reverse_relation = result[:Relations].find { |r|
51
+ if reflection.options[:through]
52
+ r[:RightModelName] == model[:ModelName] && r[:LeftModelName] == reflection_model_name && r[:Line] == ".."
53
+ else
54
+ r[:RightModelName] == model[:ModelName] && r[:LeftModelName] == reflection_model_name && r[:Line] == "--"
55
+ end
56
+ }
57
+ if reverse_relation
58
+ reverse_relation[:Comment] = if reflection.options[:through]
59
+ "#{reverse_relation[:Comment]}, HMT:#{reflection.name}"
60
+ else
61
+ "#{reverse_relation[:Comment]}, HM:#{reflection.name}"
62
+ end
63
+ else
64
+ result[:Relations] << {
65
+ LeftModelName: model[:ModelName],
66
+ LeftValue: reflection.options[:through] ? "}o" : "||",
67
+ Line: reflection.options[:through] ? ".." : "--",
68
+ RightModelName: reflection_model_name,
69
+ RightValue: "o{",
70
+ Comment: reflection.options[:through] ? "HMT:#{reflection.name}" : "HM:#{reflection.name}"
71
+ }
72
+ end
73
+ end
74
+
75
+ defined_model.reflect_on_all_associations(:has_and_belongs_to_many).each do |reflection|
76
+ reflection_model_name = get_reflection_model_name(reflection)
77
+
78
+ reverse_relation = result[:Relations].find { |r| r[:RightModelName] == model[:ModelName] && r[:LeftModelName] == reflection_model_name }
79
+ if reverse_relation
80
+ reverse_relation[:Comment] = "HABTM"
81
+ else
82
+ result[:Relations] << {
83
+ LeftModelName: model[:ModelName],
84
+ LeftValue: "}o",
85
+ Line: "..",
86
+ RightModelName: reflection_model_name,
87
+ RightValue: "o{",
88
+ Comment: "HABTM"
89
+ }
90
+ end
91
+ end
92
+
93
+ defined_model.reflect_on_all_associations(:belongs_to).each do |reflection|
94
+ reflection_model_name = get_reflection_model_name(reflection)
95
+
96
+ reverse_relation = result[:Relations].find { |r| r[:RightModelName] == model[:ModelName] && r[:LeftModelName] == reflection_model_name }
97
+ if reverse_relation
98
+ 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])
99
+ reverse_relation[:LeftValue] = "|o"
100
+ end
101
+ reverse_relation[:Comment] = "#{reverse_relation[:Comment]}, BT:#{reflection.name}"
102
+ else
103
+ 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])
104
+ "o|"
105
+ else
106
+ "||"
107
+ end
108
+ result[:Relations] << {
109
+ LeftModelName: model[:ModelName],
110
+ LeftValue: "}o",
111
+ Line: "--",
112
+ RightModelName: reflection_model_name,
113
+ RightValue: right_value,
114
+ Comment: "BT:#{reflection.name}"
115
+ }
116
+ end
117
+ end
118
+
119
+ defined_model.reflect_on_all_associations(:has_one).each do |reflection|
120
+ reflection_model_name = get_reflection_model_name(reflection)
121
+
122
+ reverse_relation = result[:Relations].find { |r|
123
+ if reflection.options[:through]
124
+ r[:RightModelName] == model[:ModelName] && r[:LeftModelName] == reflection_model_name && r[:Line] == ".."
125
+ else
126
+ r[:RightModelName] == model[:ModelName] && r[:LeftModelName] == reflection_model_name && r[:Line] == "--"
127
+ end
128
+ }
129
+ if reverse_relation
130
+ reverse_relation[:LeftValue] = "|o"
131
+ reverse_relation[:Comment] = if reflection.options[:through]
132
+ "#{reverse_relation[:Comment]}, HOT:#{reflection.name}"
133
+ else
134
+ "#{reverse_relation[:Comment]}, HO:#{reflection.name}"
135
+ end
136
+ else
137
+ result[:Relations] << {
138
+ LeftModelName: model[:ModelName],
139
+ LeftValue: reflection.options[:through] ? "}o" : "||",
140
+ Line: reflection.options[:through] ? ".." : "--",
141
+ RightModelName: reflection_model_name,
142
+ RightValue: "o|",
143
+ Comment: reflection.options[:through] ? "HOT:#{reflection.name}" : "HO:#{reflection.name}"
144
+ }
145
+ end
146
+ end
147
+ end
148
+
149
+ result
150
+ end
151
+
152
+ def get_reflection_model_name(reflection)
153
+ if reflection.options[:class_name]
154
+ reflection.options[:class_name].to_s.classify
155
+ elsif reflection.options[:through]
156
+ if reflection.options[:source]
157
+ reflection.options[:source].to_s.classify
158
+ else
159
+ reflection.class_name
160
+ end
161
+ else
162
+ reflection.class_name
163
+ end
164
+ end
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,11 @@
1
+ module RailsRealtimeErd
2
+ class Configuration
3
+ attr_accessor :mount_path, :auto_mount, :enabled_environments
4
+
5
+ def initialize
6
+ @mount_path = "/rails/erd"
7
+ @auto_mount = true
8
+ @enabled_environments = %w[development test]
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,18 @@
1
+ require "rails/engine"
2
+
3
+ module RailsRealtimeErd
4
+ class Engine < ::Rails::Engine
5
+ isolate_namespace RailsRealtimeErd
6
+
7
+ initializer "rails_realtime_erd.auto_mount" do |app|
8
+ config = RailsRealtimeErd.configuration
9
+ next unless config.auto_mount
10
+ next unless config.enabled_environments.map(&:to_s).include?(Rails.env.to_s)
11
+
12
+ mount_path = config.mount_path
13
+ app.routes.append do
14
+ mount RailsRealtimeErd::Engine => mount_path, as: :rails_realtime_erd
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,3 @@
1
+ module RailsRealtimeErd
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,20 @@
1
+ require_relative "rails-realtime-erd/version"
2
+ require_relative "rails-realtime-erd/configuration"
3
+ require_relative "rails-realtime-erd/builder"
4
+ require_relative "rails-realtime-erd/engine"
5
+
6
+ module RailsRealtimeErd
7
+ class << self
8
+ def configuration
9
+ @configuration ||= Configuration.new
10
+ end
11
+
12
+ def configure
13
+ yield configuration
14
+ end
15
+
16
+ def reset_configuration!
17
+ @configuration = Configuration.new
18
+ end
19
+ end
20
+ end
metadata ADDED
@@ -0,0 +1,138 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rails-realtime-erd
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Jackson Pires
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-05-17 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 6.1.7.10
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 6.1.7.10
27
+ - !ruby/object:Gem::Dependency
28
+ name: sqlite3
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '='
32
+ - !ruby/object:Gem::Version
33
+ version: 1.5.0
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '='
39
+ - !ruby/object:Gem::Version
40
+ version: 1.5.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec-rails
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '='
46
+ - !ruby/object:Gem::Version
47
+ version: 6.1.5
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '='
53
+ - !ruby/object:Gem::Version
54
+ version: 6.1.5
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '='
60
+ - !ruby/object:Gem::Version
61
+ version: 13.4.2
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '='
67
+ - !ruby/object:Gem::Version
68
+ version: 13.4.2
69
+ - !ruby/object:Gem::Dependency
70
+ name: standard
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - '='
74
+ - !ruby/object:Gem::Version
75
+ version: 1.28.5
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - '='
81
+ - !ruby/object:Gem::Version
82
+ version: 1.28.5
83
+ description: Mounts /rails/erd in your Rails app. On every request the gem introspects
84
+ ActiveRecord models and renders a Mermaid ERD viewer with Stimulus-driven filters,
85
+ zoom/pan, copy & download. No pre-generated HTML file.
86
+ email:
87
+ - jackson@linkana.com
88
+ executables: []
89
+ extensions: []
90
+ extra_rdoc_files: []
91
+ files:
92
+ - MIT-LICENSE
93
+ - README.md
94
+ - Rakefile
95
+ - app/assets/stylesheets/rails_realtime_erd/erd.css
96
+ - app/controllers/rails_realtime_erd/application_controller.rb
97
+ - app/controllers/rails_realtime_erd/assets_controller.rb
98
+ - app/controllers/rails_realtime_erd/erd_controller.rb
99
+ - app/helpers/rails_realtime_erd/erd_helper.rb
100
+ - app/javascript/rails_realtime_erd/application.js
101
+ - app/javascript/rails_realtime_erd/vendor/mermaid.js
102
+ - app/javascript/rails_realtime_erd/vendor/stimulus.js
103
+ - app/views/layouts/rails_realtime_erd/application.html.erb
104
+ - app/views/rails_realtime_erd/erd/_main.html.erb
105
+ - app/views/rails_realtime_erd/erd/_sidebar.html.erb
106
+ - app/views/rails_realtime_erd/erd/show.html.erb
107
+ - config/routes.rb
108
+ - lib/rails-realtime-erd.rb
109
+ - lib/rails-realtime-erd/builder.rb
110
+ - lib/rails-realtime-erd/configuration.rb
111
+ - lib/rails-realtime-erd/engine.rb
112
+ - lib/rails-realtime-erd/version.rb
113
+ homepage: https://github.com/jacksonpires/rails-realtime-erd
114
+ licenses:
115
+ - MIT
116
+ metadata:
117
+ homepage_uri: https://github.com/jacksonpires/rails-realtime-erd
118
+ source_code_uri: https://github.com/jacksonpires/rails-realtime-erd
119
+ post_install_message:
120
+ rdoc_options: []
121
+ require_paths:
122
+ - lib
123
+ required_ruby_version: !ruby/object:Gem::Requirement
124
+ requirements:
125
+ - - ">="
126
+ - !ruby/object:Gem::Version
127
+ version: '0'
128
+ required_rubygems_version: !ruby/object:Gem::Requirement
129
+ requirements:
130
+ - - ">="
131
+ - !ruby/object:Gem::Version
132
+ version: '0'
133
+ requirements: []
134
+ rubygems_version: 3.0.3.1
135
+ signing_key:
136
+ specification_version: 4
137
+ summary: Live Mermaid ERD viewer mounted as a Rails route, powered by Hotwire.
138
+ test_files: []