erdb 1.0.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/CHANGES.md +6 -0
- data/README.md +75 -0
- data/bin/erdb +5 -0
- data/lib/erdb/adapters/db.rb +60 -0
- data/lib/erdb/adapters/nosql.rb +1 -0
- data/lib/erdb/adapters/sql.rb +114 -0
- data/lib/erdb/cli.rb +179 -0
- data/lib/erdb/providers/azimutt.rb +134 -0
- data/lib/erdb/providers/dbdiagram.rb +121 -0
- data/lib/erdb/providers/erd_provider.rb +11 -0
- data/lib/erdb/utils.rb +49 -0
- data/lib/erdb/version.rb +3 -0
- data/lib/erdb.rb +38 -0
- metadata +114 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 25ee47da83fb1accfed1d03d3c4fa6b0639640961180bfe86985a9f272bac629
|
4
|
+
data.tar.gz: 345bc5e3caf6f76a70dbc7af2c237770c9e7938e5bd39eddfeb34d5f14cba870
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 6da4c1fc2b88e3507e6e364edc27f6bb9abe7d6a3d01002224294cb2c7c302edd594cbeea0efa5d06fdb7ab7516f1c0ac0b34db9084acf488b718b26429a8970
|
7
|
+
data.tar.gz: 72beed6a8e53f9e1bd19360f1796980797bdfb35e5df889294602df85105cb3d5fe9773abeed0fabdcac20e69ce1f74f1709f7f3c1ff5efe5fbd6519a5c12aec
|
data/CHANGES.md
ADDED
data/README.md
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
# ERDB [](https://badge.fury.io/rb/erdb) [](https://github.com/rubocop/rubocop)
|
2
|
+
|
3
|
+
ERDB is a Ruby Gem for generation Entity-Relationship Diagrams (ERD).
|
4
|
+
Currently it support `sqlite3`, `mysql2` and `postgresql` database.
|
5
|
+
ERDB is just a wrapper to automate the process of generating ERD using -
|
6
|
+
|
7
|
+
- [Azimutt(Open Source)](https://azimutt.app)
|
8
|
+
- [DBDiagram](https://dbdiagram.io)
|
9
|
+
|
10
|
+
> Currently there is no support for `one-to-one` relationship. If you want to use it you have to manually edit the generated ERD.
|
11
|
+
|
12
|
+
<!-- graphviz coming soon -->
|
13
|
+
|
14
|
+
## Demo
|
15
|
+
|
16
|
+

|
17
|
+
|
18
|
+
## Requirements
|
19
|
+
|
20
|
+
- Ruby 2.7.0 or higher
|
21
|
+
- Chrome Browser.
|
22
|
+
|
23
|
+
## Installation
|
24
|
+
|
25
|
+
Use the package manager [gem](https://rubygems.org/) to install ERDB.
|
26
|
+
|
27
|
+
```bash
|
28
|
+
gem install erdb
|
29
|
+
```
|
30
|
+
|
31
|
+
> Important note for Linux users: The clipboard requires the _xsel_ or the _xclip_ command-line program. On debian and ubuntu, _xsel_ can be installed with: `sudo apt-get install xsel`
|
32
|
+
> Visit [clipboard](https://github.com/janlelis/clipboard) for more details.
|
33
|
+
|
34
|
+
#### Adapters
|
35
|
+
|
36
|
+
For `mysql2` and `postgresql` database, you have to install the required gem.
|
37
|
+
I didn't include them in the gemspec because I don't want to install them if you don't need them.
|
38
|
+
Because it depends on native extensions, you'll need a compiler and the development headers for your Ruby and database.
|
39
|
+
|
40
|
+
```bash
|
41
|
+
gem install mysql2
|
42
|
+
gem install pg
|
43
|
+
```
|
44
|
+
|
45
|
+
## Usage
|
46
|
+
|
47
|
+
After install ERDB, you can use `erdb` command to generate ER Diagram.
|
48
|
+
|
49
|
+
```bash
|
50
|
+
erdb
|
51
|
+
```
|
52
|
+
|
53
|
+
## Why I created this gem?
|
54
|
+
|
55
|
+
I know there are many tools available for generating ERD,
|
56
|
+
but I wanted to create a tool that is easy to use and can be used with any database.
|
57
|
+
`Azimutt` also support converting schema to ERD, but it's only working well with remote databases.
|
58
|
+
If I want to generate local database using `Azimutt`, I have to setup `Azimutt` locally that is alot of step -\_-
|
59
|
+
|
60
|
+
## Roadmap
|
61
|
+
|
62
|
+
- [x] Support `sqlite3`, `mysql2` and `postgresql` database.
|
63
|
+
- [x] Generate ERD using Azimutt
|
64
|
+
- [x] Generate ERD using DBDiagram
|
65
|
+
- [ ] Support `MongoDB` database.
|
66
|
+
- [ ] Generate ERD using Graphviz
|
67
|
+
|
68
|
+
## Contributing
|
69
|
+
|
70
|
+
Pull requests are welcome. Feel free to open an issue first to discuss what you would like to change.
|
71
|
+
|
72
|
+
## License
|
73
|
+
|
74
|
+
This project is licensed under [MIT](https://choosealicense.com/licenses/mit/) license.
|
75
|
+
View [LICENSE](./LICENSE) for more details.
|
data/bin/erdb
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
module ERDB
|
2
|
+
class Db
|
3
|
+
#
|
4
|
+
# @param adapter [String] The adapter to use.
|
5
|
+
# @param database [String] The database to connect to.
|
6
|
+
# @return [void]
|
7
|
+
#
|
8
|
+
def initialize(adapter, database)
|
9
|
+
@adapter = adapter
|
10
|
+
@database = database
|
11
|
+
end
|
12
|
+
|
13
|
+
#
|
14
|
+
# Connect to a database.
|
15
|
+
#
|
16
|
+
def connect
|
17
|
+
raise "[connect] Not implemented."
|
18
|
+
end
|
19
|
+
|
20
|
+
#
|
21
|
+
# Disconnect from a database.
|
22
|
+
#
|
23
|
+
def disconnect
|
24
|
+
raise "[disconnect] Not implemented."
|
25
|
+
end
|
26
|
+
|
27
|
+
#
|
28
|
+
# Convert database tables to ERD convertable Array.
|
29
|
+
# @return [Array] The converted hash format. see example result below.
|
30
|
+
#
|
31
|
+
# @example Example result:
|
32
|
+
# ```ruby
|
33
|
+
# [
|
34
|
+
# {
|
35
|
+
# name: "table_name",
|
36
|
+
# is_join_table: false,
|
37
|
+
# columns: [{ name: "column_name", type: "column_type" }, ...],
|
38
|
+
# relations: [
|
39
|
+
# {
|
40
|
+
# from: {
|
41
|
+
# table: "table_name",
|
42
|
+
# column: "column_name"
|
43
|
+
# },
|
44
|
+
# to: {
|
45
|
+
# table: "table_name",
|
46
|
+
# column: "column_name"
|
47
|
+
# }
|
48
|
+
# }
|
49
|
+
# ...
|
50
|
+
# ]
|
51
|
+
# }
|
52
|
+
# ...
|
53
|
+
# ]
|
54
|
+
# ```
|
55
|
+
#
|
56
|
+
def to_erdb
|
57
|
+
raise "[to_erdb] Not implemented."
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
# Planning to use this file to create a NoSQL database for the project
|
@@ -0,0 +1,114 @@
|
|
1
|
+
require "active_record"
|
2
|
+
require_relative "db"
|
3
|
+
|
4
|
+
module ERDB
|
5
|
+
class SQL < Db
|
6
|
+
#
|
7
|
+
# Initialize a new SQL instance.
|
8
|
+
#
|
9
|
+
def initialize(adapter, database)
|
10
|
+
super
|
11
|
+
|
12
|
+
@db = ActiveRecord::Base
|
13
|
+
@connection = nil
|
14
|
+
end
|
15
|
+
|
16
|
+
#
|
17
|
+
# Connect to a database.
|
18
|
+
#
|
19
|
+
def connect
|
20
|
+
puts "\n"
|
21
|
+
puts "Connecting to #{@adapter} database..."
|
22
|
+
|
23
|
+
case @adapter.to_sym
|
24
|
+
when :sqlite3
|
25
|
+
connect_sqlite3
|
26
|
+
when :postgresql, :mysql2
|
27
|
+
connect_with_connection_string
|
28
|
+
else
|
29
|
+
raise "Adapter not supported."
|
30
|
+
end
|
31
|
+
|
32
|
+
@connection = @db.connection
|
33
|
+
end
|
34
|
+
|
35
|
+
#
|
36
|
+
# Convert database tables to ERD convertable Array.
|
37
|
+
#
|
38
|
+
def to_erdb
|
39
|
+
puts "\nAnalyzing database..."
|
40
|
+
@connection.tables.map do |table|
|
41
|
+
columns = @connection.columns(table).map { |column| { name: column.name, type: column.type || "unknown" } }
|
42
|
+
relations = @connection.foreign_keys(table).map do |fk|
|
43
|
+
{
|
44
|
+
from: {
|
45
|
+
table: table,
|
46
|
+
column: fk.options[:column]
|
47
|
+
},
|
48
|
+
to: {
|
49
|
+
table: fk[:to_table],
|
50
|
+
column: fk.options[:primary_key]
|
51
|
+
}
|
52
|
+
}
|
53
|
+
end
|
54
|
+
|
55
|
+
hash = { name: table, columns: columns, relations: relations }
|
56
|
+
hash[:is_join_table] = join_table?(hash)
|
57
|
+
hash
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
#
|
62
|
+
# Disconnect from a database.
|
63
|
+
#
|
64
|
+
def disconnect
|
65
|
+
@db.remove_connection
|
66
|
+
@connection = nil
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
#
|
72
|
+
# Check current table is a join table or not.
|
73
|
+
#
|
74
|
+
# @param table [Hash] The table to check.
|
75
|
+
# @return [Boolean] True if the table is a join table, false otherwise.
|
76
|
+
#
|
77
|
+
def join_table?(table)
|
78
|
+
relations = table[:relations]
|
79
|
+
|
80
|
+
# remove data like id, created_at, updated_at
|
81
|
+
columns = table[:columns].map { |c| c[:name] }.reject do |column|
|
82
|
+
%w[id created_at updated_at].include?(column)
|
83
|
+
end
|
84
|
+
|
85
|
+
if relations.size == columns.size && relations.size >= 2
|
86
|
+
columns.include?(relations[0][:from][:column]) && columns.include?(relations[1][:from][:column])
|
87
|
+
else
|
88
|
+
false
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
#
|
93
|
+
# Connect to a SQLite3 database.
|
94
|
+
# @param database [String] The database to connect to.
|
95
|
+
# @raise [RuntimeError] If the database does not exist.
|
96
|
+
#
|
97
|
+
def connect_sqlite3
|
98
|
+
raise "Database does not exist." unless File.exist?(@database)
|
99
|
+
|
100
|
+
@db.establish_connection(
|
101
|
+
adapter: :sqlite3,
|
102
|
+
database: @database
|
103
|
+
)
|
104
|
+
end
|
105
|
+
|
106
|
+
#
|
107
|
+
# Connect to a PostgreSQL or MySQL database.
|
108
|
+
# @param database [String] The database to connect to.
|
109
|
+
#
|
110
|
+
def connect_with_connection_string
|
111
|
+
@db.establish_connection(@database)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
data/lib/erdb/cli.rb
ADDED
@@ -0,0 +1,179 @@
|
|
1
|
+
require "json"
|
2
|
+
|
3
|
+
module ERDB
|
4
|
+
class Cli
|
5
|
+
class << self
|
6
|
+
#
|
7
|
+
# Start the CLI.
|
8
|
+
# @param [Array] args
|
9
|
+
# @return [void]
|
10
|
+
#
|
11
|
+
def start(_args)
|
12
|
+
ARGV.clear
|
13
|
+
|
14
|
+
welcome
|
15
|
+
|
16
|
+
db = select_database
|
17
|
+
|
18
|
+
erd_builder = select_diagram_builder
|
19
|
+
|
20
|
+
ERDB.show_join_table = ask_yes_no("\nDo you want to display join tables? (Y/n)", true)
|
21
|
+
|
22
|
+
db.connect
|
23
|
+
|
24
|
+
erd_builder.create(db.to_erdb)
|
25
|
+
rescue RuntimeError => e
|
26
|
+
puts "Error: #{e.message}"
|
27
|
+
exit 1
|
28
|
+
rescue Interrupt
|
29
|
+
puts "\n\nThank you for using ERDB!"
|
30
|
+
exit 0
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
#
|
36
|
+
# Ask user which database to use.
|
37
|
+
# @return [Db]
|
38
|
+
#
|
39
|
+
def select_database
|
40
|
+
data = {
|
41
|
+
sqlite3: { name: "SQLite", gem: "sqlite3" },
|
42
|
+
postgresql: { name: "PostgreSQL(Gem 'pg' must be installed)", gem: "pg" },
|
43
|
+
mysql2: { name: "MySQL(Gem 'mysql2' must be installed)", gem: "mysql2" }
|
44
|
+
}
|
45
|
+
|
46
|
+
puts "Select a database adapter:"
|
47
|
+
|
48
|
+
data.each_with_index do |v, i|
|
49
|
+
puts "#{i + 1}. #{v[1][:name]}"
|
50
|
+
end
|
51
|
+
|
52
|
+
response = ask_number(1, data.size)
|
53
|
+
|
54
|
+
adapter = data.keys[response.to_i - 1].to_sym
|
55
|
+
|
56
|
+
# check if the gem is installed
|
57
|
+
# I don't want to include the gem in the gemspec file
|
58
|
+
# cuz it's dependencies are too big and depend on the native library
|
59
|
+
# I only include sqlite3 gem cuz it's small and doesn't have any dependencies
|
60
|
+
gem = data[adapter][:gem]
|
61
|
+
|
62
|
+
begin
|
63
|
+
require gem
|
64
|
+
rescue LoadError
|
65
|
+
puts "\nError: '#{gem}' gem is not installed."
|
66
|
+
puts "Please install the gem '#{gem}' first."
|
67
|
+
puts "Run 'gem install #{gem}' to install the gem."
|
68
|
+
exit 1
|
69
|
+
end
|
70
|
+
|
71
|
+
database = if adapter == :sqlite3
|
72
|
+
ask_file "\nEnter the path to the database file:"
|
73
|
+
else
|
74
|
+
ask "\nEnter the database connection string:"
|
75
|
+
end
|
76
|
+
|
77
|
+
return SQL.new(adapter, database) if %i[sqlite3 mysql2 postgresql].include?(adapter)
|
78
|
+
|
79
|
+
raise "Invalid database adapter"
|
80
|
+
end
|
81
|
+
|
82
|
+
#
|
83
|
+
# Select a diagram builder.
|
84
|
+
# @return [ERDProvider]
|
85
|
+
#
|
86
|
+
def select_diagram_builder
|
87
|
+
data = [Azimutt, DBDiagram]
|
88
|
+
|
89
|
+
puts "\nSelect a diagram builder:"
|
90
|
+
|
91
|
+
data.each_with_index do |v, i|
|
92
|
+
puts "#{i + 1}. #{v.name.split('::').last}"
|
93
|
+
end
|
94
|
+
|
95
|
+
response = ask_number(1, data.size)
|
96
|
+
|
97
|
+
data[response.to_i - 1]
|
98
|
+
end
|
99
|
+
|
100
|
+
#
|
101
|
+
# Display welcome message.
|
102
|
+
#
|
103
|
+
def welcome
|
104
|
+
puts <<~WELCOME
|
105
|
+
.----------------. .----------------. .----------------. .----------------.
|
106
|
+
| .--------------. || .--------------. || .--------------. || .--------------. |
|
107
|
+
| | _________ | || | _______ | || | ________ | || | ______ | |
|
108
|
+
| | |_ ___ | | || | |_ __ \\ | || | |_ ___ `. | || | |_ _ \\ | |
|
109
|
+
| | | |_ \\_| | || | | |__) | | || | | | `. \\ | || | | |_) | | |
|
110
|
+
| | | _| _ | || | | __ / | || | | | | | | || | | __'. | |
|
111
|
+
| | _| |___/ | | || | _| | \\ \\_ | || | _| |___.' / | || | _| |__) | | |
|
112
|
+
| | |_________| | || | |____| |___| | || | |________.' | || | |_______/ | |
|
113
|
+
| | | || | | || | | || | | |
|
114
|
+
| '--------------' || '--------------' || '--------------' || '--------------' |
|
115
|
+
'----------------' '----------------' '----------------' '----------------'
|
116
|
+
|
117
|
+
ERDB is an automate tool to generate Entity-Relationship Diagrams from a database.
|
118
|
+
It use Azimutt and DBDiagram to generate the diagrams.
|
119
|
+
|
120
|
+
WELCOME
|
121
|
+
end
|
122
|
+
|
123
|
+
#
|
124
|
+
# Ask a question to the user.
|
125
|
+
# @param [String] question
|
126
|
+
# @return [String]
|
127
|
+
#
|
128
|
+
def ask(question = nil)
|
129
|
+
puts question if question
|
130
|
+
print "> "
|
131
|
+
gets.chomp
|
132
|
+
end
|
133
|
+
|
134
|
+
#
|
135
|
+
# Ask a number input to the user.
|
136
|
+
# @param [Integer] min
|
137
|
+
# @param [Integer] max
|
138
|
+
# @return [Integer]
|
139
|
+
#
|
140
|
+
def ask_number(min, max)
|
141
|
+
response = nil
|
142
|
+
until response.to_i.between?(min, max)
|
143
|
+
response = ask
|
144
|
+
unless response.match(/^\d+$/)
|
145
|
+
puts "Please enter a number"
|
146
|
+
response = nil
|
147
|
+
end
|
148
|
+
|
149
|
+
unless response.to_i.between?(min, max)
|
150
|
+
puts "Please enter a number between #{min} and #{max}"
|
151
|
+
response = nil
|
152
|
+
end
|
153
|
+
end
|
154
|
+
response
|
155
|
+
end
|
156
|
+
|
157
|
+
def ask_file(question)
|
158
|
+
loop do
|
159
|
+
file = ask question
|
160
|
+
return file if File.exist?(file)
|
161
|
+
|
162
|
+
puts "Invalid file path"
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
#
|
167
|
+
# Ask a yes/no question to the user.
|
168
|
+
# @param [String] question
|
169
|
+
# @param [Boolean] default
|
170
|
+
# @return [Boolean]
|
171
|
+
#
|
172
|
+
def ask_yes_no(question, default = true)
|
173
|
+
result = ask question
|
174
|
+
|
175
|
+
result.empty? ? default : result.downcase == "y"
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
require "clipboard"
|
2
|
+
require "watir"
|
3
|
+
|
4
|
+
module ERDB
|
5
|
+
class Azimutt < ERDProvider
|
6
|
+
class << self
|
7
|
+
#
|
8
|
+
# Create a new ER Diagram using https://azimutt.app/.
|
9
|
+
# @param [Hash] tables
|
10
|
+
#
|
11
|
+
def create(tables)
|
12
|
+
converted_data = to_aml(tables)
|
13
|
+
|
14
|
+
Utils.display_output(converted_data, "Azimutt")
|
15
|
+
|
16
|
+
start_automation(converted_data)
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
#
|
22
|
+
# Start the automation process to generate the ER Diagram.
|
23
|
+
# @param [String] data
|
24
|
+
# @return [void]
|
25
|
+
#
|
26
|
+
def start_automation(data)
|
27
|
+
browser = Watir::Browser.new(ERDB.default_browser)
|
28
|
+
|
29
|
+
browser.goto "https://azimutt.app/new"
|
30
|
+
|
31
|
+
browser.span(text: "From scratch (db design)").click
|
32
|
+
|
33
|
+
browser.button(id: "create-project-btn").click
|
34
|
+
|
35
|
+
textarea = browser.textarea(id: "source-editor")
|
36
|
+
textarea.click
|
37
|
+
|
38
|
+
old_clipboard = Clipboard.paste
|
39
|
+
Clipboard.copy(data)
|
40
|
+
|
41
|
+
# set! sometimes doesn't work cuz azimutt automatically reset incorrectly formatted data
|
42
|
+
# and set is also slow as hell better to use send_keys
|
43
|
+
# sometime naive is better than smart lol
|
44
|
+
#
|
45
|
+
# textarea.set(data)
|
46
|
+
|
47
|
+
control = Utils.is_mac? ? :command : :control
|
48
|
+
browser.send_keys control, "v"
|
49
|
+
|
50
|
+
Clipboard.copy(old_clipboard)
|
51
|
+
|
52
|
+
# rubocop:disable Lint/MissingCopEnableDirective
|
53
|
+
# rubocop:disable Layout/LineLength
|
54
|
+
btn = browser.button(class: "-ml-px rounded-r-md relative inline-flex items-center p-2 border border-gray-300 text-sm font-medium focus:z-10 focus:outline-none focus:ring-1 focus:ring-primary-500 focus:border-primary-500 bg-white text-gray-700 hover:bg-gray-50")
|
55
|
+
|
56
|
+
btn.click if btn.exists?
|
57
|
+
|
58
|
+
puts "Enter 'q' to exit."
|
59
|
+
|
60
|
+
loop do
|
61
|
+
v = gets.chomp
|
62
|
+
break if v == "q"
|
63
|
+
end
|
64
|
+
|
65
|
+
browser.close
|
66
|
+
end
|
67
|
+
|
68
|
+
#
|
69
|
+
# Convert the data to AML(Azimutt Markup Language) format.
|
70
|
+
#
|
71
|
+
# @param [Hash] tables
|
72
|
+
# @return [String]
|
73
|
+
#
|
74
|
+
def to_aml(tables)
|
75
|
+
str = ""
|
76
|
+
tables.each_with_index do |table, i|
|
77
|
+
if table[:is_join_table] && !ERDB.show_join_table?
|
78
|
+
str += to_many_to_many_str(table)
|
79
|
+
next
|
80
|
+
end
|
81
|
+
|
82
|
+
str += "\n\n" if i.positive?
|
83
|
+
str += "#{table[:name]}\n"
|
84
|
+
str += table[:columns].map { |c| to_column(c[:name], c[:type]) }.join("\n")
|
85
|
+
|
86
|
+
# relations
|
87
|
+
r = table[:relations]
|
88
|
+
next if r.nil? || r.empty?
|
89
|
+
|
90
|
+
r.each do |relation|
|
91
|
+
str += "\n"
|
92
|
+
f = relation[:from]
|
93
|
+
t = relation[:to]
|
94
|
+
|
95
|
+
str += "fk #{f[:table]}.#{f[:column]} -> #{t[:table]}.#{t[:column]}"
|
96
|
+
end
|
97
|
+
end
|
98
|
+
str
|
99
|
+
end
|
100
|
+
|
101
|
+
#
|
102
|
+
# Convert a column to a string.
|
103
|
+
# @param [String] name
|
104
|
+
# @param [String] type
|
105
|
+
#
|
106
|
+
def to_column(name, type)
|
107
|
+
" #{name} #{type}"
|
108
|
+
end
|
109
|
+
|
110
|
+
#
|
111
|
+
# Convert a many-to-many table to a AML formatted string.
|
112
|
+
# @param [Hash] table
|
113
|
+
# @return [String]
|
114
|
+
#
|
115
|
+
def to_many_to_many_str(table)
|
116
|
+
str = "\n"
|
117
|
+
relations = Utils.to_many_to_many(table[:relations])
|
118
|
+
|
119
|
+
# Azimutt doesn't support many-to-many relations
|
120
|
+
# so we have to convert it to two one-to-many relations
|
121
|
+
# Really weird but does the job :/
|
122
|
+
relations.each do |relation|
|
123
|
+
relations.each do |other|
|
124
|
+
next if relation == other
|
125
|
+
|
126
|
+
str += "\nfk: #{relation} -> #{other}\n"
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
str
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
require "clipboard"
|
2
|
+
require "watir"
|
3
|
+
|
4
|
+
module ERDB
|
5
|
+
class DBDiagram < ERDProvider
|
6
|
+
class << self
|
7
|
+
#
|
8
|
+
# Create a new ER Diagram using https://dbdiagram.io
|
9
|
+
# @param [Hash] tables
|
10
|
+
#
|
11
|
+
def create(tables)
|
12
|
+
converted_data = to_dbdiagram_format(tables)
|
13
|
+
|
14
|
+
Utils.display_output(converted_data, "DBDiagram")
|
15
|
+
|
16
|
+
start_automation(converted_data)
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
#
|
22
|
+
# Start the automation process to generate the ER Diagram.
|
23
|
+
# @param [String] data
|
24
|
+
# @return [void]
|
25
|
+
#
|
26
|
+
def start_automation(data)
|
27
|
+
browser = Watir::Browser.new(ERDB.default_browser)
|
28
|
+
|
29
|
+
browser.goto "https://dbdiagram.io/d"
|
30
|
+
|
31
|
+
editor = browser.div(class: "view-lines monaco-mouse-cursor-text")
|
32
|
+
editor.click
|
33
|
+
|
34
|
+
control = Utils.is_mac? ? :command : :control
|
35
|
+
|
36
|
+
browser.send_keys control, "a"
|
37
|
+
browser.send_keys :delete
|
38
|
+
|
39
|
+
old_clipboard = Clipboard.paste
|
40
|
+
# Yep, I know this is ugly.
|
41
|
+
# But DBDiagram don't use input element for editor. -_-
|
42
|
+
Clipboard.copy(data)
|
43
|
+
|
44
|
+
browser.send_keys control, "v"
|
45
|
+
|
46
|
+
Clipboard.copy(old_clipboard)
|
47
|
+
|
48
|
+
puts "Enter 'q' to exit."
|
49
|
+
|
50
|
+
loop do
|
51
|
+
v = gets.chomp
|
52
|
+
break if v == "q"
|
53
|
+
end
|
54
|
+
|
55
|
+
browser.close
|
56
|
+
end
|
57
|
+
|
58
|
+
#
|
59
|
+
# Convert the data DBDiagram string format.
|
60
|
+
#
|
61
|
+
# @param [Hash] tables
|
62
|
+
# @return [String]
|
63
|
+
#
|
64
|
+
def to_dbdiagram_format(tables)
|
65
|
+
str = ""
|
66
|
+
tables.each_with_index do |table, i|
|
67
|
+
if table[:is_join_table] && !ERDB.show_join_table?
|
68
|
+
str += to_many_to_many_str(table)
|
69
|
+
next
|
70
|
+
end
|
71
|
+
|
72
|
+
str += "\n" if i.positive?
|
73
|
+
str += "Table #{table[:name]} {\n"
|
74
|
+
str += table[:columns].map { |c| to_column(c[:name], c[:type]) }.join("\n")
|
75
|
+
str += "\n}\n"
|
76
|
+
|
77
|
+
r = table[:relations]
|
78
|
+
next if r.nil? || r.empty?
|
79
|
+
|
80
|
+
r.each do |relation|
|
81
|
+
str += "\n"
|
82
|
+
f = relation[:from]
|
83
|
+
t = relation[:to]
|
84
|
+
|
85
|
+
str += "Ref: #{f[:table]}.#{f[:column]} > #{t[:table]}.#{t[:column]}"
|
86
|
+
end
|
87
|
+
|
88
|
+
str += "\n"
|
89
|
+
end
|
90
|
+
str
|
91
|
+
end
|
92
|
+
|
93
|
+
#
|
94
|
+
# Convert a column to a string.
|
95
|
+
# @param [String] name
|
96
|
+
# @param [String] type
|
97
|
+
#
|
98
|
+
def to_column(name, type)
|
99
|
+
" #{name} #{type}"
|
100
|
+
end
|
101
|
+
|
102
|
+
#
|
103
|
+
# Convert a many-to-many table to a dbdiagram formatted string.
|
104
|
+
# @param [Hash] table
|
105
|
+
# @return [String]
|
106
|
+
#
|
107
|
+
def to_many_to_many_str(table)
|
108
|
+
str = ""
|
109
|
+
relations = Utils.to_many_to_many(table[:relations])
|
110
|
+
|
111
|
+
relations.each_with_index do |relation, i|
|
112
|
+
next if i.zero?
|
113
|
+
|
114
|
+
str += "\nRef: #{relations.first} <> #{relation}\n"
|
115
|
+
end
|
116
|
+
|
117
|
+
str
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
data/lib/erdb/utils.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
module ERDB
|
2
|
+
class Utils
|
3
|
+
class << self
|
4
|
+
#
|
5
|
+
# Display the ER Diagram required data.
|
6
|
+
# @param [String] data
|
7
|
+
# @param [String] provider
|
8
|
+
# @return [void]
|
9
|
+
#
|
10
|
+
def display_output(data, provider)
|
11
|
+
puts "### Copy following output to #{provider} if unexpected error happen ###"
|
12
|
+
puts "### Start of output ###\n\n"
|
13
|
+
puts data
|
14
|
+
puts "\n### End of output ###"
|
15
|
+
end
|
16
|
+
|
17
|
+
#
|
18
|
+
# Check if the current OS is macOS.
|
19
|
+
# @return [Boolean]
|
20
|
+
def is_mac?
|
21
|
+
RbConfig::CONFIG["host_os"] =~ /darwin|mac os/
|
22
|
+
end
|
23
|
+
|
24
|
+
#
|
25
|
+
# Convert the relations to ER Diagram format.
|
26
|
+
# @param [Array] relations
|
27
|
+
# @return [Array]
|
28
|
+
# @example
|
29
|
+
# [
|
30
|
+
# {
|
31
|
+
# from: {
|
32
|
+
# table: "users",
|
33
|
+
# column: "id"
|
34
|
+
# },
|
35
|
+
# to: {
|
36
|
+
# table: "posts",
|
37
|
+
# column: "user_id"
|
38
|
+
# }
|
39
|
+
# }
|
40
|
+
# ]
|
41
|
+
# # => ["users.id", "posts.user_id"]
|
42
|
+
def to_many_to_many(relations)
|
43
|
+
relations.map do |relation|
|
44
|
+
"#{relation[:to][:table]}.#{relation[:to][:column]}"
|
45
|
+
end.uniq
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
data/lib/erdb/version.rb
ADDED
data/lib/erdb.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
require_relative "erdb/cli"
|
2
|
+
require_relative "erdb/version"
|
3
|
+
require_relative "erdb/utils"
|
4
|
+
require_relative "erdb/providers/erd_provider"
|
5
|
+
|
6
|
+
module ERDB
|
7
|
+
autoload :SQL, File.expand_path("erdb/adapters/sql", __dir__)
|
8
|
+
autoload :Azimutt, File.expand_path("erdb/providers/azimutt", __dir__)
|
9
|
+
autoload :DBDiagram, File.expand_path("erdb/providers/dbdiagram", __dir__)
|
10
|
+
|
11
|
+
class << self
|
12
|
+
attr_writer :default_timeout, :default_browser, :show_join_table
|
13
|
+
|
14
|
+
#
|
15
|
+
# Default wait time for wait methods.
|
16
|
+
# @return [Integer] Default wait time in seconds.
|
17
|
+
#
|
18
|
+
def default_timeout
|
19
|
+
@default_timeout ||= 30
|
20
|
+
end
|
21
|
+
|
22
|
+
#
|
23
|
+
# Default browser to use for automation.
|
24
|
+
# @return [Symbol]
|
25
|
+
#
|
26
|
+
def default_browser
|
27
|
+
@default_browser ||= :chrome
|
28
|
+
end
|
29
|
+
|
30
|
+
#
|
31
|
+
# Show join table in the diagram.
|
32
|
+
# @return [Boolean]
|
33
|
+
#
|
34
|
+
def show_join_table?
|
35
|
+
@show_join_table.nil? ? true : @show_join_table
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
metadata
ADDED
@@ -0,0 +1,114 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: erdb
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Wai Yan Phyo
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2023-05-01 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activerecord
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '7.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '7.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: clipboard
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.3'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.3'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: sqlite3
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.6'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.6'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: watir
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '7.2'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '7.2'
|
69
|
+
description: ERDB is a Ruby Gem for generation Entity-Relationship Diagrams (ERD).
|
70
|
+
email: oyhpnayiaw@gmail.com
|
71
|
+
executables:
|
72
|
+
- erdb
|
73
|
+
extensions: []
|
74
|
+
extra_rdoc_files: []
|
75
|
+
files:
|
76
|
+
- CHANGES.md
|
77
|
+
- README.md
|
78
|
+
- bin/erdb
|
79
|
+
- lib/erdb.rb
|
80
|
+
- lib/erdb/adapters/db.rb
|
81
|
+
- lib/erdb/adapters/nosql.rb
|
82
|
+
- lib/erdb/adapters/sql.rb
|
83
|
+
- lib/erdb/cli.rb
|
84
|
+
- lib/erdb/providers/azimutt.rb
|
85
|
+
- lib/erdb/providers/dbdiagram.rb
|
86
|
+
- lib/erdb/providers/erd_provider.rb
|
87
|
+
- lib/erdb/utils.rb
|
88
|
+
- lib/erdb/version.rb
|
89
|
+
homepage: https://github.com/oyhpnayiaw/erdb
|
90
|
+
licenses:
|
91
|
+
- MIT
|
92
|
+
metadata:
|
93
|
+
source_code_uri: https://github.com/oyhpnayiaw/erdb
|
94
|
+
rubygems_mfa_required: 'true'
|
95
|
+
post_install_message:
|
96
|
+
rdoc_options: []
|
97
|
+
require_paths:
|
98
|
+
- lib
|
99
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: 2.7.0
|
104
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
105
|
+
requirements:
|
106
|
+
- - ">="
|
107
|
+
- !ruby/object:Gem::Version
|
108
|
+
version: '0'
|
109
|
+
requirements: []
|
110
|
+
rubygems_version: 3.4.10
|
111
|
+
signing_key:
|
112
|
+
specification_version: 4
|
113
|
+
summary: ERDB is a Ruby Gem for generation Entity-Relationship Diagrams (ERD).
|
114
|
+
test_files: []
|