sqlui 0.1.69 → 0.1.70
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.release-version +1 -1
- data/app/checks.rb +49 -0
- data/app/count_down_latch.rb +31 -0
- data/app/database_config.rb +4 -2
- data/app/deep.rb +3 -46
- data/app/github/cache.rb +65 -0
- data/app/github/caching_client.rb +36 -0
- data/app/github/client.rb +36 -0
- data/app/github/file.rb +19 -0
- data/app/github/paths.rb +21 -0
- data/app/github/tree.rb +40 -0
- data/app/github/tree_client.rb +93 -0
- data/app/saved_config.rb +16 -0
- data/app/server.rb +19 -8
- data/app/views/databases.erb +1 -1
- data/client/resources/sqlui.css +1 -1
- data/client/resources/sqlui.js +7 -0
- metadata +82 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 476cdcf4c9f35aa0b46a5d8dd3f41aa409a4ef5cf72bb0d02099ea44d4eb92e3
|
4
|
+
data.tar.gz: f52996d5793db9b6529cf7f1c8f5dd1ee171bd010f00419604e2399d430eb811
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f37ae3ed5654474330b6ce34a432cbe55c87911494819aeb46e279cf22246b48d3afeddeeb7a4f2f69bee8e39e4186d2feab86c1dc8405e7b3f149a9e09a8d87
|
7
|
+
data.tar.gz: a8eaad53be8c5740cc63fc0a5d2017a4ff3070fc90521381e3f8d2dce99d5ea399dce493544e4112579a89b0da925c606328739b1b89c137bbeeb1e865bb1af0
|
data/.release-version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.1.
|
1
|
+
0.1.70
|
data/app/checks.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Some argument check methods.
|
4
|
+
module Checks
|
5
|
+
def check_non_nil(hash)
|
6
|
+
hash.each do |key, value|
|
7
|
+
raise "invalid #{key}: #{value.nil? ? 'nil' : value}" if value.nil?
|
8
|
+
end
|
9
|
+
|
10
|
+
hash.size == 1 ? hash.values.first : hash.values
|
11
|
+
end
|
12
|
+
|
13
|
+
def check_is_a(hash)
|
14
|
+
hash.each do |key, (clazz, value)|
|
15
|
+
raise "invalid #{key} #{clazz}: #{value.nil? ? 'nil' : value} (#{value.class})" unless value.is_a?(clazz)
|
16
|
+
end
|
17
|
+
|
18
|
+
hash.size == 1 ? hash.values.first[1] : hash.values.map { |(_clazz, value)| value }
|
19
|
+
end
|
20
|
+
|
21
|
+
def check_boolean(hash)
|
22
|
+
hash.each do |key, value|
|
23
|
+
raise "invalid #{key}: #{value.nil? ? 'nil' : value}" unless value.is_a?(TrueClass) || value.is_a?(FalseClass)
|
24
|
+
end
|
25
|
+
|
26
|
+
hash.size == 1 ? hash.values.first : hash.values
|
27
|
+
end
|
28
|
+
|
29
|
+
def check_non_empty_string(hash)
|
30
|
+
hash.each do |key, value|
|
31
|
+
raise "invalid #{key}: #{value.nil? ? 'nil' : value}" unless value.is_a?(String) && !value.empty?
|
32
|
+
end
|
33
|
+
|
34
|
+
hash.size == 1 ? hash.values.first : hash.values
|
35
|
+
end
|
36
|
+
|
37
|
+
def check_positive_integer(hash)
|
38
|
+
hash.each do |key, value|
|
39
|
+
raise "invalid #{key}: #{value.nil? ? 'nil' : value}" unless value.is_a?(Integer) && value.positive?
|
40
|
+
end
|
41
|
+
|
42
|
+
hash.size == 1 ? hash.values.first : hash.values
|
43
|
+
end
|
44
|
+
|
45
|
+
def check_enumerable_of(enumerable, clazz)
|
46
|
+
check_is_a(enumerable: [Enumerable, enumerable])
|
47
|
+
enumerable.each { |value| check_is_a(value: [clazz, value]) }
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# A latch that allows you to wait for a maximum number of seconds.
|
4
|
+
class CountDownLatch
|
5
|
+
def initialize(count)
|
6
|
+
@count = count
|
7
|
+
@mutex = Mutex.new
|
8
|
+
@resource = ConditionVariable.new
|
9
|
+
end
|
10
|
+
|
11
|
+
def count_down
|
12
|
+
@mutex.synchronize do
|
13
|
+
if @count.positive?
|
14
|
+
@count -= 1
|
15
|
+
@resource.signal if @count.zero?
|
16
|
+
end
|
17
|
+
@count
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def count
|
22
|
+
@mutex.synchronize { @count }
|
23
|
+
end
|
24
|
+
|
25
|
+
def await(timeout:)
|
26
|
+
@mutex.synchronize do
|
27
|
+
@resource.wait(@mutex, timeout) if @count.positive?
|
28
|
+
raise 'timed out while waiting' unless @count.zero?
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
data/app/database_config.rb
CHANGED
@@ -5,10 +5,11 @@ require 'mysql2'
|
|
5
5
|
require 'set'
|
6
6
|
|
7
7
|
require_relative 'args'
|
8
|
+
require_relative 'saved_config'
|
8
9
|
|
9
10
|
# Config for a single database.
|
10
11
|
class DatabaseConfig
|
11
|
-
attr_reader :display_name, :description, :url_path, :joins, :
|
12
|
+
attr_reader :display_name, :description, :url_path, :joins, :saved_config, :tables, :columns, :client_params
|
12
13
|
|
13
14
|
def initialize(hash)
|
14
15
|
@display_name = Args.fetch_non_empty_string(hash, :display_name).strip
|
@@ -17,7 +18,8 @@ class DatabaseConfig
|
|
17
18
|
raise ArgumentError, 'url_path should not start with a /' if @url_path.start_with?('/')
|
18
19
|
raise ArgumentError, 'url_path should not end with a /' if @url_path.end_with?('/')
|
19
20
|
|
20
|
-
|
21
|
+
saved_config_hash = Args.fetch_optional_hash(hash, :saved_config)
|
22
|
+
@saved_config = saved_config_hash.nil? ? nil : SavedConfig.new(saved_config_hash)
|
21
23
|
|
22
24
|
# Make joins an array. It is only a map to allow for YAML extension.
|
23
25
|
@joins = (Args.fetch_optional_hash(hash, :joins) || {}).values
|
data/app/deep.rb
CHANGED
@@ -1,43 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
each { |value| value.deep_transform_keys!(&block) if value.respond_to?(:deep_transform_keys!) }
|
7
|
-
self
|
8
|
-
end
|
9
|
-
|
10
|
-
def deep_symbolize_keys!
|
11
|
-
deep_transform_keys!(&:to_sym)
|
12
|
-
end
|
13
|
-
|
14
|
-
def deep_dup(result = {})
|
15
|
-
map do |value|
|
16
|
-
value.respond_to?(:deep_dup) ? value.deep_dup : value.clone
|
17
|
-
end
|
18
|
-
result
|
19
|
-
end
|
20
|
-
end
|
3
|
+
require 'active_support/core_ext/hash/deep_merge'
|
4
|
+
require 'active_support/core_ext/hash/keys'
|
5
|
+
require 'active_support/core_ext/object/deep_dup'
|
21
6
|
|
22
7
|
# Deep extensions for Hash.
|
23
8
|
class Hash
|
24
|
-
def deep_transform_keys!(&block)
|
25
|
-
transform_keys!(&block)
|
26
|
-
each_value { |value| value.deep_transform_keys!(&block) if value.respond_to?(:deep_transform_keys!) }
|
27
|
-
self
|
28
|
-
end
|
29
|
-
|
30
|
-
def deep_symbolize_keys!
|
31
|
-
deep_transform_keys!(&:to_sym)
|
32
|
-
end
|
33
|
-
|
34
|
-
def deep_dup(result = {})
|
35
|
-
each do |key, value|
|
36
|
-
result[key] = value.respond_to?(:deep_dup) ? value.deep_dup : value.clone
|
37
|
-
end
|
38
|
-
result
|
39
|
-
end
|
40
|
-
|
41
9
|
def deep_set(*path, value:)
|
42
10
|
raise ArgumentError, 'no path specified' if path.empty?
|
43
11
|
|
@@ -63,15 +31,4 @@ class Hash
|
|
63
31
|
self.[](path[0]).deep_delete(*path[1..])
|
64
32
|
end
|
65
33
|
end
|
66
|
-
|
67
|
-
def deep_merge!(hash)
|
68
|
-
hash.each do |key, value|
|
69
|
-
if self[key].is_a?(Hash) && value.is_a?(Hash)
|
70
|
-
self[key].deep_merge!(value)
|
71
|
-
else
|
72
|
-
self[key] = value
|
73
|
-
end
|
74
|
-
end
|
75
|
-
self
|
76
|
-
end
|
77
34
|
end
|
data/app/github/cache.rb
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../checks'
|
4
|
+
|
5
|
+
module Github
|
6
|
+
# A cache for GitHub API GET requests.
|
7
|
+
class Cache
|
8
|
+
include Checks
|
9
|
+
|
10
|
+
# A cache entry.
|
11
|
+
class Entry
|
12
|
+
include Checks
|
13
|
+
|
14
|
+
attr_accessor :value, :expires_at
|
15
|
+
|
16
|
+
def initialize(value, max_age_seconds)
|
17
|
+
@value = check_non_nil(value: value)
|
18
|
+
@expires_at = Time.now + check_positive_integer(max_age_seconds: max_age_seconds)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def initialize(hash, logger: Logger.new($stdout))
|
23
|
+
@mutex = Mutex.new
|
24
|
+
@hash = check_is_a(hash: [Hash, hash])
|
25
|
+
@logger = check_non_nil(logger: logger)
|
26
|
+
end
|
27
|
+
|
28
|
+
def [](key)
|
29
|
+
check_non_empty_string(key: key)
|
30
|
+
|
31
|
+
@mutex.synchronize do
|
32
|
+
evict
|
33
|
+
if (value = @hash[key])
|
34
|
+
@logger.info "#{self.class} entry found for #{key}"
|
35
|
+
value
|
36
|
+
else
|
37
|
+
@logger.info "#{self.class} entry not found for #{key}"
|
38
|
+
nil
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def []=(key, value)
|
44
|
+
check_non_empty_string(key: key)
|
45
|
+
check_is_a(value: [Entry, value])
|
46
|
+
|
47
|
+
@mutex.synchronize do
|
48
|
+
@logger.info "#{self.class} caching entry for #{key} until #{value.expires_at}"
|
49
|
+
@hash[key] = value
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def evict
|
56
|
+
now = Time.now
|
57
|
+
@hash.delete_if do |key, value|
|
58
|
+
if value.expires_at < now
|
59
|
+
@logger.info "#{self.class} evicting #{key}"
|
60
|
+
true
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../checks'
|
4
|
+
require_relative '../deep'
|
5
|
+
require_relative 'cache'
|
6
|
+
require_relative 'client'
|
7
|
+
|
8
|
+
module Github
|
9
|
+
# Like a Github::Client but with a cache.
|
10
|
+
class CachingClient
|
11
|
+
include Checks
|
12
|
+
|
13
|
+
def initialize(client, cache)
|
14
|
+
@client = check_is_a(client: [Client, client])
|
15
|
+
@cache = check_is_a(cache: [Cache, cache])
|
16
|
+
end
|
17
|
+
|
18
|
+
def get_with_caching(url, cache_for:)
|
19
|
+
check_non_empty_string(url: url)
|
20
|
+
check_positive_integer(cache_for: cache_for)
|
21
|
+
|
22
|
+
if (cache_entry = @cache[url])
|
23
|
+
return cache_entry.value
|
24
|
+
end
|
25
|
+
|
26
|
+
response = @client.get(url)
|
27
|
+
@cache[url] = Cache::Entry.new(response, cache_for)
|
28
|
+
response.deep_dup
|
29
|
+
end
|
30
|
+
|
31
|
+
def get_without_caching(url)
|
32
|
+
check_non_empty_string(url: url)
|
33
|
+
@client.get(url)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require 'logger'
|
5
|
+
require 'rest-client'
|
6
|
+
|
7
|
+
require_relative '../checks'
|
8
|
+
|
9
|
+
module Github
|
10
|
+
# It's a GitHub client. I know there is an official client library.
|
11
|
+
class Client
|
12
|
+
include Checks
|
13
|
+
|
14
|
+
def initialize(access_token:, logger: Logger.new($stdout))
|
15
|
+
@access_token = check_non_empty_string(access_token: access_token)
|
16
|
+
@logger = check_non_nil(logger: logger)
|
17
|
+
end
|
18
|
+
|
19
|
+
def get(url)
|
20
|
+
check_non_empty_string(url: url)
|
21
|
+
|
22
|
+
@logger.info "#{self.class} GET #{url}"
|
23
|
+
response = RestClient.get(
|
24
|
+
url,
|
25
|
+
{
|
26
|
+
'Accept' => 'application/vnd.github+json',
|
27
|
+
'Authorization' => "Bearer #{@access_token}",
|
28
|
+
'X-GitHub-Api-Version' => '2022-11-28'
|
29
|
+
}
|
30
|
+
)
|
31
|
+
raise "get #{url} failed: #{response}" unless response.code == 200
|
32
|
+
|
33
|
+
JSON.parse(response)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
data/app/github/file.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../checks'
|
4
|
+
|
5
|
+
module Github
|
6
|
+
# A GitHub file.
|
7
|
+
class File
|
8
|
+
include Checks
|
9
|
+
|
10
|
+
attr_reader :display_path, :content, :github_url
|
11
|
+
|
12
|
+
def initialize(owner:, repo:, branch:, path:, content:)
|
13
|
+
check_non_empty_string(owner: owner, repo: repo, branch: branch, path: path)
|
14
|
+
@content = check_is_a(content: [String, content])
|
15
|
+
@display_path = "#{owner}/#{repo}/#{branch}/#{path}"
|
16
|
+
@github_url = "https://github.com/#{owner}/#{repo}/blob/#{branch}/#{path}"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
data/app/github/paths.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Github
|
4
|
+
# Methods for dealing with GitHub paths.
|
5
|
+
class Paths
|
6
|
+
# "<owner>/<repo>/<ref>/<some_path>"
|
7
|
+
PATH_PATTERN = %r{^(?:[^/]+/){3}.*[^/]$}
|
8
|
+
private_constant :PATH_PATTERN
|
9
|
+
|
10
|
+
# Parses a path like "<owner>/<repo>/<ref>/<some_path>" into owner, repo, ref, path.
|
11
|
+
def self.parse_file_path(path)
|
12
|
+
check_non_empty_string(path: path)
|
13
|
+
raise "invalid path: #{path}" unless PATH_PATTERN.match?(path)
|
14
|
+
|
15
|
+
owner, repo, ref, *path = path.split('/')
|
16
|
+
path = path.join('/')
|
17
|
+
|
18
|
+
[owner, repo, ref, path]
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
data/app/github/tree.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../checks'
|
4
|
+
require_relative 'file'
|
5
|
+
|
6
|
+
module Github
|
7
|
+
# A GitHub file tree.
|
8
|
+
class Tree
|
9
|
+
include Checks
|
10
|
+
include Enumerable
|
11
|
+
|
12
|
+
attr_reader :truncated
|
13
|
+
|
14
|
+
def initialize(files:, truncated: false)
|
15
|
+
@files = check_enumerable_of(files, File)
|
16
|
+
@truncated = check_boolean(truncated: truncated)
|
17
|
+
end
|
18
|
+
|
19
|
+
class << self
|
20
|
+
include Checks
|
21
|
+
|
22
|
+
def for(owner:, repo:, branch:, tree_response:)
|
23
|
+
check_non_empty_string(owner: owner, repo: repo, branch: branch)
|
24
|
+
check_is_a(tree_response: [Hash, tree_response])
|
25
|
+
|
26
|
+
truncated = check_boolean(truncated: tree_response['truncated'])
|
27
|
+
tree = check_is_a(tree: [Array, tree_response['tree']])
|
28
|
+
files = tree.map do |blob|
|
29
|
+
File.new(owner: owner, repo: repo, branch: branch, path: blob['path'], content: blob['content'])
|
30
|
+
end
|
31
|
+
|
32
|
+
Tree.new(files: files, truncated: truncated)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def each(&block)
|
37
|
+
@files.each(&block)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'base64'
|
4
|
+
require 'concurrent/executor/fixed_thread_pool'
|
5
|
+
require 'json'
|
6
|
+
require 'logger'
|
7
|
+
|
8
|
+
require_relative 'caching_client'
|
9
|
+
require_relative 'tree'
|
10
|
+
require_relative '../checks'
|
11
|
+
require_relative '../count_down_latch'
|
12
|
+
|
13
|
+
module Github
|
14
|
+
# Wraps a Github::Client to provide tree-specific features.
|
15
|
+
class TreeClient
|
16
|
+
include Checks
|
17
|
+
|
18
|
+
@thread_pool = Concurrent::FixedThreadPool.new(5)
|
19
|
+
|
20
|
+
class << self
|
21
|
+
attr_reader :thread_pool
|
22
|
+
end
|
23
|
+
|
24
|
+
DEFAULT_MAX_TREE_CACHE_AGE_SECONDS = 60 * 5 # 5 minutes
|
25
|
+
private_constant :DEFAULT_MAX_TREE_CACHE_AGE_SECONDS
|
26
|
+
|
27
|
+
DEFAULT_MAX_FILE_CACHE_AGE_SECONDS = 60 * 60 * 24 * 7 # 1 week
|
28
|
+
private_constant :DEFAULT_MAX_FILE_CACHE_AGE_SECONDS
|
29
|
+
|
30
|
+
MAX_TREE_SIZE = 50
|
31
|
+
private_constant :MAX_TREE_SIZE
|
32
|
+
|
33
|
+
def initialize(access_token:, cache:, logger: Logger.new($stdout))
|
34
|
+
check_non_empty_string(access_token: access_token)
|
35
|
+
check_is_a(cache: [Cache, cache])
|
36
|
+
|
37
|
+
@client = Github::CachingClient.new(Github::Client.new(access_token: access_token, logger: logger), cache)
|
38
|
+
@logger = check_non_nil(logger: logger)
|
39
|
+
end
|
40
|
+
|
41
|
+
def get_tree(owner:, repo:, branch:, regex:)
|
42
|
+
check_non_empty_string(owner: owner, repo: repo, branch: branch)
|
43
|
+
check_is_a(regex: [Regexp, regex])
|
44
|
+
|
45
|
+
response = @client.get_with_caching(
|
46
|
+
"https://api.github.com/repos/#{owner}/#{repo}/git/trees/#{branch}?recursive=true",
|
47
|
+
cache_for: DEFAULT_MAX_TREE_CACHE_AGE_SECONDS
|
48
|
+
)
|
49
|
+
|
50
|
+
response['tree'] = response['tree'].select { |blob| regex.match?(blob['path']) }
|
51
|
+
raise "trees with more than #{MAX_TREE_SIZE} blobs not supported" if response['tree'].size > MAX_TREE_SIZE
|
52
|
+
|
53
|
+
latch = CountDownLatch.new(response['tree'].size)
|
54
|
+
response['tree'].each do |blob|
|
55
|
+
TreeClient.thread_pool.post do
|
56
|
+
blob['content'] =
|
57
|
+
get_file_content_with_caching(owner: owner, repo: repo, path: blob['path'], ref: response['sha'])
|
58
|
+
ensure
|
59
|
+
latch.count_down
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
latch.await(timeout: 10)
|
64
|
+
raise 'failed to load saved files' unless response['tree'].all? { |blob| blob['content'] }
|
65
|
+
|
66
|
+
Tree.for(owner: owner, repo: repo, branch: branch, tree_response: response)
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
# Returns the contents of the file at the specified path. Uses the cache.
|
72
|
+
def get_file_content_with_caching(owner:, repo:, path:, ref:)
|
73
|
+
check_non_empty_string(owner: owner, repo: repo, sha: ref, path: path)
|
74
|
+
|
75
|
+
response = @client.get_with_caching(
|
76
|
+
"https://api.github.com/repos/#{owner}/#{repo}/contents/#{path}?ref=#{ref}",
|
77
|
+
cache_for: DEFAULT_MAX_FILE_CACHE_AGE_SECONDS
|
78
|
+
)
|
79
|
+
Base64.decode64(response['content'])
|
80
|
+
end
|
81
|
+
|
82
|
+
# Returns the contents of the file at the specified path. Uses the cache.
|
83
|
+
def get_file_content_without_caching(owner:, repo:, path:, ref:)
|
84
|
+
check_non_empty_string(owner: owner, repo: repo, sha: ref, path: path)
|
85
|
+
|
86
|
+
response = @client.get_without_caching(
|
87
|
+
"https://api.github.com/repos/#{owner}/#{repo}/contents/#{path}?ref=#{ref}"
|
88
|
+
)
|
89
|
+
|
90
|
+
Base64.decode64(response['content'])
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
data/app/saved_config.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'args'
|
4
|
+
|
5
|
+
# Config for saved files.
|
6
|
+
class SavedConfig
|
7
|
+
attr_reader :token, :owner, :repo, :branch, :regex
|
8
|
+
|
9
|
+
def initialize(hash)
|
10
|
+
@token = Args.fetch_non_empty_string(hash, :token).strip
|
11
|
+
@owner = Args.fetch_non_empty_string(hash, :owner).strip
|
12
|
+
@repo = Args.fetch_non_empty_string(hash, :repo).strip
|
13
|
+
@branch = Args.fetch_non_empty_string(hash, :branch).strip
|
14
|
+
@regex = Regexp.new(Args.fetch_non_empty_string(hash, :regex))
|
15
|
+
end
|
16
|
+
end
|
data/app/server.rb
CHANGED
@@ -11,6 +11,11 @@ require 'sinatra/base'
|
|
11
11
|
require 'uri'
|
12
12
|
require 'webrick'
|
13
13
|
require_relative 'database_metadata'
|
14
|
+
require_relative 'github/cache'
|
15
|
+
require_relative 'github/caching_client'
|
16
|
+
require_relative 'github/client'
|
17
|
+
require_relative 'github/tree'
|
18
|
+
require_relative 'github/tree_client'
|
14
19
|
require_relative 'mysql_types'
|
15
20
|
require_relative 'sql_parser'
|
16
21
|
require_relative 'sqlui'
|
@@ -22,7 +27,7 @@ class Server < Sinatra::Base
|
|
22
27
|
@logger ||= WEBrick::Log.new
|
23
28
|
end
|
24
29
|
|
25
|
-
def self.init_and_run(config, resources_dir)
|
30
|
+
def self.init_and_run(config, resources_dir, github_cache = Github::Cache.new({}, logger: Server.logger))
|
26
31
|
logger.info("Starting SQLUI v#{Version::SQLUI}")
|
27
32
|
logger.info("Airbrake enabled: #{config.airbrake[:server]&.[](:enabled) || false}")
|
28
33
|
|
@@ -105,6 +110,13 @@ class Server < Sinatra::Base
|
|
105
110
|
end
|
106
111
|
|
107
112
|
post "#{config.base_url_path}/#{database.url_path}/metadata" do
|
113
|
+
tree = Github::Tree.new(files: [], truncated: false)
|
114
|
+
if (saved_config = database.saved_config)
|
115
|
+
tree_client = Github::TreeClient.new(access_token: database.saved_config.token, cache: github_cache,
|
116
|
+
logger: Server.logger)
|
117
|
+
tree = tree_client.get_tree(owner: saved_config.owner, repo: saved_config.repo, branch: saved_config.branch,
|
118
|
+
regex: saved_config.regex)
|
119
|
+
end
|
108
120
|
metadata = database.with_client do |client|
|
109
121
|
{
|
110
122
|
server: "#{config.name} - #{database.display_name}",
|
@@ -113,19 +125,18 @@ class Server < Sinatra::Base
|
|
113
125
|
tables: database.tables,
|
114
126
|
columns: database.columns,
|
115
127
|
joins: database.joins,
|
116
|
-
saved:
|
117
|
-
|
118
|
-
comment_lines = contents.split("\n").take_while do |l|
|
128
|
+
saved: tree.to_h do |file|
|
129
|
+
comment_lines = file.content.split("\n").take_while do |l|
|
119
130
|
l.start_with?('--')
|
120
131
|
end
|
121
|
-
filename = File.basename(path)
|
122
132
|
description = comment_lines.map { |l| l.sub(/^-- */, '') }.join("\n")
|
123
133
|
[
|
124
|
-
|
134
|
+
file.display_path,
|
125
135
|
{
|
126
|
-
filename:
|
136
|
+
filename: file.display_path,
|
137
|
+
github_url: file.github_url,
|
127
138
|
description: description,
|
128
|
-
contents:
|
139
|
+
contents: file.content
|
129
140
|
}
|
130
141
|
]
|
131
142
|
end
|
data/app/views/databases.erb
CHANGED
data/client/resources/sqlui.css
CHANGED
data/client/resources/sqlui.js
CHANGED
@@ -24997,6 +24997,12 @@
|
|
24997
24997
|
route(event.target, event, runUrl, true);
|
24998
24998
|
});
|
24999
24999
|
|
25000
|
+
const viewOnGithubLinkElement = document.createElement('a');
|
25001
|
+
viewOnGithubLinkElement.classList.add('link', 'view-github-link');
|
25002
|
+
viewOnGithubLinkElement.innerText = 'github';
|
25003
|
+
viewOnGithubLinkElement.href = file.github_url;
|
25004
|
+
viewOnGithubLinkElement.target = '_blank';
|
25005
|
+
|
25000
25006
|
const nameElement = document.createElement('h2');
|
25001
25007
|
nameElement.innerText = file.filename;
|
25002
25008
|
nameElement.classList.add('name');
|
@@ -25005,6 +25011,7 @@
|
|
25005
25011
|
linksElement.classList.add('links');
|
25006
25012
|
linksElement.appendChild(viewLinkElement);
|
25007
25013
|
linksElement.appendChild(runLinkElement);
|
25014
|
+
linksElement.appendChild(viewOnGithubLinkElement);
|
25008
25015
|
|
25009
25016
|
const descriptionElement = document.createElement('p');
|
25010
25017
|
descriptionElement.innerText = file.description;
|
metadata
CHANGED
@@ -1,15 +1,29 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sqlui
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.70
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Nick Dower
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-04-
|
11
|
+
date: 2023-04-03 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activesupport
|
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'
|
13
27
|
- !ruby/object:Gem::Dependency
|
14
28
|
name: airbrake
|
15
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -24,6 +38,20 @@ dependencies:
|
|
24
38
|
- - "~>"
|
25
39
|
- !ruby/object:Gem::Version
|
26
40
|
version: '13.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: concurrent-ruby
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.0'
|
27
55
|
- !ruby/object:Gem::Dependency
|
28
56
|
name: mysql2
|
29
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -52,6 +80,20 @@ dependencies:
|
|
52
80
|
- - "~>"
|
53
81
|
- !ruby/object:Gem::Version
|
54
82
|
version: '4.0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rest-client
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '2.0'
|
90
|
+
type: :runtime
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '2.0'
|
55
97
|
- !ruby/object:Gem::Dependency
|
56
98
|
name: sinatra
|
57
99
|
requirement: !ruby/object:Gem::Requirement
|
@@ -80,6 +122,20 @@ dependencies:
|
|
80
122
|
- - "~>"
|
81
123
|
- !ruby/object:Gem::Version
|
82
124
|
version: '1.0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: pry
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - "~>"
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0.0'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - "~>"
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0.0'
|
83
139
|
- !ruby/object:Gem::Dependency
|
84
140
|
name: rspec-core
|
85
141
|
requirement: !ruby/object:Gem::Requirement
|
@@ -150,6 +206,20 @@ dependencies:
|
|
150
206
|
- - "~>"
|
151
207
|
- !ruby/object:Gem::Version
|
152
208
|
version: '4.0'
|
209
|
+
- !ruby/object:Gem::Dependency
|
210
|
+
name: webmock
|
211
|
+
requirement: !ruby/object:Gem::Requirement
|
212
|
+
requirements:
|
213
|
+
- - "~>"
|
214
|
+
- !ruby/object:Gem::Version
|
215
|
+
version: '3.0'
|
216
|
+
type: :development
|
217
|
+
prerelease: false
|
218
|
+
version_requirements: !ruby/object:Gem::Requirement
|
219
|
+
requirements:
|
220
|
+
- - "~>"
|
221
|
+
- !ruby/object:Gem::Version
|
222
|
+
version: '3.0'
|
153
223
|
description: A SQL UI.
|
154
224
|
email: nicholasdower@gmail.com
|
155
225
|
executables:
|
@@ -159,10 +229,20 @@ extra_rdoc_files: []
|
|
159
229
|
files:
|
160
230
|
- ".release-version"
|
161
231
|
- app/args.rb
|
232
|
+
- app/checks.rb
|
233
|
+
- app/count_down_latch.rb
|
162
234
|
- app/database_config.rb
|
163
235
|
- app/database_metadata.rb
|
164
236
|
- app/deep.rb
|
237
|
+
- app/github/cache.rb
|
238
|
+
- app/github/caching_client.rb
|
239
|
+
- app/github/client.rb
|
240
|
+
- app/github/file.rb
|
241
|
+
- app/github/paths.rb
|
242
|
+
- app/github/tree.rb
|
243
|
+
- app/github/tree_client.rb
|
165
244
|
- app/mysql_types.rb
|
245
|
+
- app/saved_config.rb
|
166
246
|
- app/server.rb
|
167
247
|
- app/sql_parser.rb
|
168
248
|
- app/sqlui.rb
|