canvas_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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +9 -0
- data/LICENSE +21 -0
- data/README.md +86 -0
- data/exe/canvas_erd +9 -0
- data/lib/canvas_erd/application_loader.rb +37 -0
- data/lib/canvas_erd/browser.rb +40 -0
- data/lib/canvas_erd/cli.rb +125 -0
- data/lib/canvas_erd/diagram.rb +130 -0
- data/lib/canvas_erd/diagram_store.rb +70 -0
- data/lib/canvas_erd/png_document.rb +80 -0
- data/lib/canvas_erd/schema_provider.rb +15 -0
- data/lib/canvas_erd/server.rb +210 -0
- data/lib/canvas_erd/version.rb +6 -0
- data/lib/canvas_erd/web/app.js +1394 -0
- data/lib/canvas_erd/web/document.js +473 -0
- data/lib/canvas_erd/web/index.html +119 -0
- data/lib/canvas_erd/web/layout.js +74 -0
- data/lib/canvas_erd/web/png.js +146 -0
- data/lib/canvas_erd/web/styles.css +439 -0
- data/lib/canvas_erd/web/vendor/elk-api.js +227 -0
- data/lib/canvas_erd/web/vendor/elk-worker.min.js +6317 -0
- data/lib/canvas_erd/web/vendor/fabric.min.js +473 -0
- data/lib/canvas_erd.rb +14 -0
- data/licenses/ELKJS-LICENSE.md +263 -0
- data/licenses/FABRIC-JS-LICENSE.txt +22 -0
- data/licenses/ICONOIR-LICENSE +21 -0
- metadata +155 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "rack/utils"
|
|
5
|
+
require "socket"
|
|
6
|
+
|
|
7
|
+
module CanvasERD
|
|
8
|
+
class WebApplication
|
|
9
|
+
WEB_ROOT = File.expand_path("web", __dir__)
|
|
10
|
+
STATIC_FILES = {
|
|
11
|
+
"/" => ["index.html", "text/html; charset=utf-8"],
|
|
12
|
+
"/assets/app.js" => ["app.js", "application/javascript; charset=utf-8"],
|
|
13
|
+
"/assets/document.js" => ["document.js", "application/javascript; charset=utf-8"],
|
|
14
|
+
"/assets/layout.js" => ["layout.js", "application/javascript; charset=utf-8"],
|
|
15
|
+
"/assets/png.js" => ["png.js", "application/javascript; charset=utf-8"],
|
|
16
|
+
"/assets/styles.css" => ["styles.css", "text/css; charset=utf-8"],
|
|
17
|
+
"/assets/fabric.min.js" => ["vendor/fabric.min.js", "application/javascript; charset=utf-8"],
|
|
18
|
+
"/assets/elk-api.js" => ["vendor/elk-api.js", "application/javascript; charset=utf-8"],
|
|
19
|
+
"/assets/elk-worker.min.js" => ["vendor/elk-worker.min.js", "application/javascript; charset=utf-8"]
|
|
20
|
+
}.freeze
|
|
21
|
+
|
|
22
|
+
attr_reader :schema
|
|
23
|
+
|
|
24
|
+
def initialize(schema:, schema_provider: nil, state: nil, diagram_store: nil)
|
|
25
|
+
@schema = schema
|
|
26
|
+
@schema_provider = schema_provider || -> { schema }
|
|
27
|
+
@diagram_store = diagram_store
|
|
28
|
+
@schema_mutex = Mutex.new
|
|
29
|
+
@document = {
|
|
30
|
+
"format" => "canvas_erd",
|
|
31
|
+
"version" => 1,
|
|
32
|
+
"schema" => schema,
|
|
33
|
+
"state" => state
|
|
34
|
+
}
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def call(environment)
|
|
38
|
+
method = environment.fetch("REQUEST_METHOD")
|
|
39
|
+
path = environment.fetch("PATH_INFO")
|
|
40
|
+
|
|
41
|
+
status, headers, body = route(method, path, environment)
|
|
42
|
+
|
|
43
|
+
body = [] if method == "HEAD"
|
|
44
|
+
[status, security_headers.merge(headers), body]
|
|
45
|
+
rescue DiagramStore::NotFound => error
|
|
46
|
+
status, headers, body = response(404, "text/plain; charset=utf-8", error.message)
|
|
47
|
+
[status, security_headers.merge(headers), method == "HEAD" ? [] : body]
|
|
48
|
+
rescue DiagramStore::Error => error
|
|
49
|
+
status, headers, body = response(400, "text/plain; charset=utf-8", error.message)
|
|
50
|
+
[status, security_headers.merge(headers), method == "HEAD" ? [] : body]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def route(method, path, environment)
|
|
56
|
+
if path == "/api/diagram"
|
|
57
|
+
return method_not_allowed("GET, HEAD, PUT") unless %w[GET HEAD PUT].include?(method)
|
|
58
|
+
|
|
59
|
+
return diagram_response(environment, write: method == "PUT")
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
return method_not_allowed("GET, HEAD") unless %w[GET HEAD].include?(method)
|
|
63
|
+
|
|
64
|
+
if path == "/api/schema"
|
|
65
|
+
schema_response(environment, refresh: method == "GET")
|
|
66
|
+
elsif path == "/api/document"
|
|
67
|
+
document_response
|
|
68
|
+
elsif path == "/api/diagrams"
|
|
69
|
+
diagrams_response
|
|
70
|
+
elsif STATIC_FILES.key?(path)
|
|
71
|
+
file_response(*STATIC_FILES.fetch(path))
|
|
72
|
+
else
|
|
73
|
+
response(404, "text/plain; charset=utf-8", "Not Found")
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def method_not_allowed(allow)
|
|
78
|
+
response(405, "text/plain; charset=utf-8", "Method Not Allowed", "allow" => allow)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def schema_response(environment, refresh:)
|
|
82
|
+
if refresh && environment.fetch("QUERY_STRING", "").split("&").include?("refresh=1")
|
|
83
|
+
@schema_mutex.synchronize { @schema = @schema_provider.call }
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
response(
|
|
87
|
+
200,
|
|
88
|
+
"application/json; charset=utf-8",
|
|
89
|
+
JSON.generate(schema),
|
|
90
|
+
"cache-control" => "no-store"
|
|
91
|
+
)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def document_response
|
|
95
|
+
document = @document["state"] ? @document : @document.merge("schema" => schema)
|
|
96
|
+
response(
|
|
97
|
+
200,
|
|
98
|
+
"application/json; charset=utf-8",
|
|
99
|
+
JSON.generate(document),
|
|
100
|
+
"cache-control" => "no-store"
|
|
101
|
+
)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def diagrams_response
|
|
105
|
+
return response(404, "text/plain; charset=utf-8", "Diagram storage is not configured") unless @diagram_store
|
|
106
|
+
|
|
107
|
+
response(
|
|
108
|
+
200,
|
|
109
|
+
"application/json; charset=utf-8",
|
|
110
|
+
JSON.generate("directory" => @diagram_store.relative_directory, "diagrams" => @diagram_store.names),
|
|
111
|
+
"cache-control" => "no-store"
|
|
112
|
+
)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def diagram_response(environment, write:)
|
|
116
|
+
return response(404, "text/plain; charset=utf-8", "Diagram storage is not configured") unless @diagram_store
|
|
117
|
+
|
|
118
|
+
name = Rack::Utils.parse_query(environment.fetch("QUERY_STRING", ""))["name"]
|
|
119
|
+
if write
|
|
120
|
+
bytes = environment.fetch("rack.input").read.b
|
|
121
|
+
@diagram_store.write(name, bytes)
|
|
122
|
+
response(200, "application/json; charset=utf-8", JSON.generate("name" => name))
|
|
123
|
+
else
|
|
124
|
+
response(
|
|
125
|
+
200,
|
|
126
|
+
"image/png",
|
|
127
|
+
@diagram_store.read(name),
|
|
128
|
+
"cache-control" => "no-store"
|
|
129
|
+
)
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def file_response(relative_path, content_type)
|
|
134
|
+
response(
|
|
135
|
+
200,
|
|
136
|
+
content_type,
|
|
137
|
+
File.binread(File.join(WEB_ROOT, relative_path)),
|
|
138
|
+
"cache-control" => "no-store"
|
|
139
|
+
)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def response(status, content_type, body, headers = {})
|
|
143
|
+
headers = { "content-type" => content_type }.merge(headers)
|
|
144
|
+
[status, headers, [body]]
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def security_headers
|
|
148
|
+
{
|
|
149
|
+
"content-security-policy" => "default-src 'self'; connect-src 'self'; img-src 'self' data: blob:; " \
|
|
150
|
+
"object-src 'none'; base-uri 'none'; frame-ancestors 'none'",
|
|
151
|
+
"x-content-type-options" => "nosniff",
|
|
152
|
+
"referrer-policy" => "no-referrer"
|
|
153
|
+
}
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
class Server
|
|
158
|
+
class Error < StandardError; end
|
|
159
|
+
|
|
160
|
+
HOST = "127.0.0.1"
|
|
161
|
+
|
|
162
|
+
attr_reader :port
|
|
163
|
+
|
|
164
|
+
def initialize(app:, port: nil, rack_server_class: nil)
|
|
165
|
+
@app = app
|
|
166
|
+
@port = port || available_port
|
|
167
|
+
@rack_server_class = rack_server_class || self.class.rack_server_class
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def start
|
|
171
|
+
@rack_server_class.start(
|
|
172
|
+
app: @app,
|
|
173
|
+
Host: HOST,
|
|
174
|
+
Port: port,
|
|
175
|
+
environment: "none"
|
|
176
|
+
)
|
|
177
|
+
rescue LoadError => error
|
|
178
|
+
raise Error, "No Rack server handler is available: #{error.message}"
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def url
|
|
182
|
+
"http://#{HOST}:#{port}/"
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def self.rack_server_class(loader: method(:load_rack_server_class))
|
|
186
|
+
loader.call("rackup", "Rackup") ||
|
|
187
|
+
loader.call("rack/server", "Rack") ||
|
|
188
|
+
raise(Error, "No Rack server is available in this Rails application")
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def self.load_rack_server_class(feature, namespace_name)
|
|
192
|
+
require feature
|
|
193
|
+
namespace = Object.const_get(namespace_name)
|
|
194
|
+
namespace.const_get(:Server, false) if namespace.const_defined?(:Server, false)
|
|
195
|
+
rescue LoadError, NameError
|
|
196
|
+
nil
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
private_class_method :load_rack_server_class
|
|
200
|
+
|
|
201
|
+
private
|
|
202
|
+
|
|
203
|
+
def available_port
|
|
204
|
+
socket = TCPServer.new(HOST, 0)
|
|
205
|
+
socket.addr.fetch(1)
|
|
206
|
+
ensure
|
|
207
|
+
socket&.close
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|