diagrammer 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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +25 -0
- data/LICENSE.txt +21 -0
- data/README.md +387 -0
- data/lib/diagrammer/generator.rb +30 -0
- data/lib/diagrammer/html_renderer.rb +727 -0
- data/lib/diagrammer/model_introspector.rb +113 -0
- data/lib/diagrammer/railtie.rb +11 -0
- data/lib/diagrammer/tasks/diagrammer.rake +11 -0
- data/lib/diagrammer/version.rb +5 -0
- data/lib/diagrammer.rb +15 -0
- metadata +125 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'set'
|
|
4
|
+
|
|
5
|
+
module Diagrammer
|
|
6
|
+
class ModelIntrospector
|
|
7
|
+
ASSOCIATION_MACROS = %i[belongs_to has_one has_many has_and_belongs_to_many].freeze
|
|
8
|
+
|
|
9
|
+
def initialize(models: nil)
|
|
10
|
+
@models = models
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def call
|
|
14
|
+
eager_load_rails_application
|
|
15
|
+
|
|
16
|
+
models = selected_models.sort_by(&:name)
|
|
17
|
+
{
|
|
18
|
+
tables: unique_tables(models),
|
|
19
|
+
relationships: relationships_for(models)
|
|
20
|
+
}
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
# Several models can share one database table (STI subclasses, gem base
|
|
26
|
+
# classes such as I18n's Translation, multi-schema rpush models). Emit a
|
|
27
|
+
# single card per table; associations from every model still merge onto it
|
|
28
|
+
# via relationships_for, which is keyed by table name.
|
|
29
|
+
def unique_tables(models)
|
|
30
|
+
models.each_with_object({}) do |model, by_table|
|
|
31
|
+
by_table[model.table_name] ||= table_for(model)
|
|
32
|
+
end.values
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def eager_load_rails_application
|
|
36
|
+
return unless defined?(Rails) && Rails.respond_to?(:application)
|
|
37
|
+
|
|
38
|
+
Rails.application.eager_load!
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def selected_models
|
|
42
|
+
models = @models || active_record_models
|
|
43
|
+
models.select { |model| concrete_model?(model) }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def active_record_models
|
|
47
|
+
return [] unless defined?(ActiveRecord::Base)
|
|
48
|
+
|
|
49
|
+
ActiveRecord::Base.descendants
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def concrete_model?(model)
|
|
53
|
+
model.respond_to?(:table_name) &&
|
|
54
|
+
model.respond_to?(:columns) &&
|
|
55
|
+
!model.abstract_class? &&
|
|
56
|
+
model.table_exists?
|
|
57
|
+
rescue StandardError
|
|
58
|
+
false
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def table_for(model)
|
|
62
|
+
{
|
|
63
|
+
model_name: model.name,
|
|
64
|
+
table_name: model.table_name,
|
|
65
|
+
columns: columns_for(model)
|
|
66
|
+
}
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def columns_for(model)
|
|
70
|
+
primary_key = model.primary_key.to_s
|
|
71
|
+
|
|
72
|
+
model.columns.map do |column|
|
|
73
|
+
{
|
|
74
|
+
name: column.name,
|
|
75
|
+
type: column.type.to_s,
|
|
76
|
+
primary_key: column.name == primary_key,
|
|
77
|
+
foreign_key: column.name.end_with?('_id')
|
|
78
|
+
}
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def relationships_for(models)
|
|
83
|
+
table_names = models.to_set(&:table_name)
|
|
84
|
+
models.flat_map { |model| model_relationships(model, table_names) }.uniq
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def model_relationships(model, table_names)
|
|
88
|
+
model.reflect_on_all_associations.filter_map do |association|
|
|
89
|
+
relationship_for(model, association, table_names)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def relationship_for(model, association, table_names)
|
|
94
|
+
return unless ASSOCIATION_MACROS.include?(association.macro)
|
|
95
|
+
|
|
96
|
+
target_table = association_table_name(association)
|
|
97
|
+
return unless target_table && table_names.include?(target_table)
|
|
98
|
+
|
|
99
|
+
{
|
|
100
|
+
from: model.table_name,
|
|
101
|
+
to: target_table,
|
|
102
|
+
name: association.name.to_s,
|
|
103
|
+
macro: association.macro
|
|
104
|
+
}
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def association_table_name(association)
|
|
108
|
+
association.klass.table_name
|
|
109
|
+
rescue StandardError
|
|
110
|
+
nil
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
namespace :diagrammer do
|
|
4
|
+
desc 'Generate a standalone HTML database diagram'
|
|
5
|
+
task :generate, [:output] => :environment do |_task, args|
|
|
6
|
+
output = args[:output] || Rails.root.join('dbdiagram.html')
|
|
7
|
+
path = Diagrammer.generate(output: output)
|
|
8
|
+
|
|
9
|
+
puts "Generated #{path}"
|
|
10
|
+
end
|
|
11
|
+
end
|
data/lib/diagrammer.rb
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'diagrammer/version'
|
|
4
|
+
require_relative 'diagrammer/model_introspector'
|
|
5
|
+
require_relative 'diagrammer/html_renderer'
|
|
6
|
+
require_relative 'diagrammer/generator'
|
|
7
|
+
require_relative 'diagrammer/railtie' if defined?(Rails::Railtie)
|
|
8
|
+
|
|
9
|
+
module Diagrammer
|
|
10
|
+
class Error < StandardError; end
|
|
11
|
+
|
|
12
|
+
def self.generate(output:, models: nil)
|
|
13
|
+
Generator.new(output: output, models: models).call
|
|
14
|
+
end
|
|
15
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: diagrammer
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Alex Andreiev
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 2026-06-28 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: activerecord
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '6.1'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '6.1'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: railties
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '6.1'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '6.1'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: rake
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '13.0'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '13.0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: rspec
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '3.0'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '3.0'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: rubocop
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - ">="
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '1.75'
|
|
75
|
+
type: :development
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - ">="
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '1.75'
|
|
82
|
+
description: Diagrammer introspects ActiveRecord models and renders an interactive,
|
|
83
|
+
fully offline ER diagram as a standalone HTML file, without Graphviz.
|
|
84
|
+
email:
|
|
85
|
+
- andreyev0204@gmail.com
|
|
86
|
+
executables: []
|
|
87
|
+
extensions: []
|
|
88
|
+
extra_rdoc_files: []
|
|
89
|
+
files:
|
|
90
|
+
- CHANGELOG.md
|
|
91
|
+
- LICENSE.txt
|
|
92
|
+
- README.md
|
|
93
|
+
- lib/diagrammer.rb
|
|
94
|
+
- lib/diagrammer/generator.rb
|
|
95
|
+
- lib/diagrammer/html_renderer.rb
|
|
96
|
+
- lib/diagrammer/model_introspector.rb
|
|
97
|
+
- lib/diagrammer/railtie.rb
|
|
98
|
+
- lib/diagrammer/tasks/diagrammer.rake
|
|
99
|
+
- lib/diagrammer/version.rb
|
|
100
|
+
homepage: https://github.com/alex-andreiev/diagrammer
|
|
101
|
+
licenses:
|
|
102
|
+
- MIT
|
|
103
|
+
metadata:
|
|
104
|
+
homepage_uri: https://github.com/alex-andreiev/diagrammer
|
|
105
|
+
source_code_uri: https://github.com/alex-andreiev/diagrammer
|
|
106
|
+
changelog_uri: https://github.com/alex-andreiev/diagrammer/blob/main/CHANGELOG.md
|
|
107
|
+
rubygems_mfa_required: 'true'
|
|
108
|
+
rdoc_options: []
|
|
109
|
+
require_paths:
|
|
110
|
+
- lib
|
|
111
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
112
|
+
requirements:
|
|
113
|
+
- - ">="
|
|
114
|
+
- !ruby/object:Gem::Version
|
|
115
|
+
version: '3.1'
|
|
116
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
117
|
+
requirements:
|
|
118
|
+
- - ">="
|
|
119
|
+
- !ruby/object:Gem::Version
|
|
120
|
+
version: '0'
|
|
121
|
+
requirements: []
|
|
122
|
+
rubygems_version: 3.6.2
|
|
123
|
+
specification_version: 4
|
|
124
|
+
summary: Generate Rails database relationship diagrams as standalone HTML.
|
|
125
|
+
test_files: []
|