monq 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/CODE_OF_CONDUCT.md +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +108 -0
- data/Rakefile +8 -0
- data/docs/superpowers/plans/2026-10-24-monq.md +1384 -0
- data/docs/superpowers/specs/2026-10-24-monq-design.md +401 -0
- data/exe/monq +6 -0
- data/lib/monq/app.rb +86 -0
- data/lib/monq/cli.rb +141 -0
- data/lib/monq/commands/info.rb +45 -0
- data/lib/monq/commands/models.rb +95 -0
- data/lib/monq/commands/reconnect.rb +45 -0
- data/lib/monq/commands/redefine.rb +25 -0
- data/lib/monq/commands/sandbox.rb +57 -0
- data/lib/monq/commands/show_sql.rb +92 -0
- data/lib/monq/commands.rb +25 -0
- data/lib/monq/config.rb +216 -0
- data/lib/monq/environment.rb +218 -0
- data/lib/monq/errors.rb +5 -0
- data/lib/monq/ext/array.rb +57 -0
- data/lib/monq/ext/criteria.rb +31 -0
- data/lib/monq/ext/document.rb +29 -0
- data/lib/monq/ext/kernel.rb +70 -0
- data/lib/monq/ext/object.rb +14 -0
- data/lib/monq/ext/view.rb +40 -0
- data/lib/monq/ext.rb +42 -0
- data/lib/monq/multi_io.rb +49 -0
- data/lib/monq/presenter.rb +54 -0
- data/lib/monq/repl.rb +45 -0
- data/lib/monq/ssh_proxy.rb +43 -0
- data/lib/monq/table.rb +50 -0
- data/lib/monq/version.rb +5 -0
- data/lib/monq.rb +23 -0
- data/sig/monq.rbs +4 -0
- metadata +193 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "mongoid"
|
|
4
|
+
|
|
5
|
+
module Monq
|
|
6
|
+
class Environment
|
|
7
|
+
attr_reader :name, :config, :client_name, :namespace, :client, :ssh_proxy
|
|
8
|
+
|
|
9
|
+
def initialize(name, config, first:, model_dir:)
|
|
10
|
+
@name = name
|
|
11
|
+
@config = config
|
|
12
|
+
@first = first
|
|
13
|
+
@model_dir = model_dir
|
|
14
|
+
@client_name = Config.client_name_for(name)
|
|
15
|
+
@namespace = config[:namespace]
|
|
16
|
+
@models = []
|
|
17
|
+
@sandbox_session = nil
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def connect!
|
|
21
|
+
ensure_ssh_proxy!
|
|
22
|
+
opts = Config.client_options(client_config)
|
|
23
|
+
@client = Mongo::Client.new(opts[:hosts], **opts[:options].merge(database: opts[:database]))
|
|
24
|
+
Mongoid::Clients.set(client_name, @client)
|
|
25
|
+
Mongoid::Clients.set(:default, @client) if @first
|
|
26
|
+
hello!
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def hello!
|
|
30
|
+
@client.database.command(hello: 1).documents.first
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def ping?
|
|
34
|
+
hello!
|
|
35
|
+
true
|
|
36
|
+
rescue StandardError
|
|
37
|
+
false
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def reconnect!
|
|
41
|
+
raise Error, "sandbox is active; run sandbox-quit first" if sandbox?
|
|
42
|
+
|
|
43
|
+
@ssh_proxy&.reconnect!
|
|
44
|
+
@client&.close
|
|
45
|
+
connect!
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def namespace_module
|
|
49
|
+
@namespace_module ||= Object.const_set(namespace, Module.new)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def sandbox?
|
|
53
|
+
!@sandbox_session.nil?
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def bind_session(session)
|
|
57
|
+
@sandbox_session = session
|
|
58
|
+
attach_client_session(session)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def clear_session
|
|
62
|
+
if @sandbox_session
|
|
63
|
+
@sandbox_session.abort_transaction rescue nil
|
|
64
|
+
@sandbox_session.end_session rescue nil
|
|
65
|
+
@sandbox_session = nil
|
|
66
|
+
end
|
|
67
|
+
detach_client_session
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def load_models!
|
|
71
|
+
install_namespace_api!
|
|
72
|
+
path = File.join(@model_dir, "#{Config.prefix_for(name)}.rb")
|
|
73
|
+
unless File.file?(path)
|
|
74
|
+
warn "model file not found: #{path}"
|
|
75
|
+
return
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
namespace_module.module_eval(File.read(path), path)
|
|
79
|
+
bind_clients!
|
|
80
|
+
track_embedded_classes!
|
|
81
|
+
install_abbreviations!
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def models
|
|
85
|
+
namespace_module.constants.filter_map do |const_name|
|
|
86
|
+
klass = namespace_module.const_get(const_name)
|
|
87
|
+
next unless klass.is_a?(Class) && klass < Mongoid::Document && !klass.embedded?
|
|
88
|
+
next unless klass.name.to_s.split("::").last == const_name.to_s
|
|
89
|
+
|
|
90
|
+
klass
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def redefine!
|
|
95
|
+
raise Error, "sandbox is active; run sandbox-quit first" if sandbox?
|
|
96
|
+
|
|
97
|
+
tracked_classes.each do |klass|
|
|
98
|
+
Mongoid.models.delete(klass) if Mongoid.models.respond_to?(:delete)
|
|
99
|
+
end
|
|
100
|
+
tracked_names.each { |n| namespace_module.send(:remove_const, n) if namespace_module.const_defined?(n, false) }
|
|
101
|
+
@tracked = []
|
|
102
|
+
load_models!
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
private
|
|
106
|
+
|
|
107
|
+
def attach_client_session(session)
|
|
108
|
+
return unless @client
|
|
109
|
+
|
|
110
|
+
if @client.respond_to?(:session=)
|
|
111
|
+
@client.session = session
|
|
112
|
+
else
|
|
113
|
+
@client.instance_variable_set(:@monq_session, session)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def detach_client_session
|
|
118
|
+
return unless @client
|
|
119
|
+
|
|
120
|
+
if @client.respond_to?(:session=)
|
|
121
|
+
@client.session = nil
|
|
122
|
+
elsif @client.instance_variable_defined?(:@monq_session)
|
|
123
|
+
@client.remove_instance_variable(:@monq_session)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def ensure_ssh_proxy!
|
|
128
|
+
return unless config[:ssh]
|
|
129
|
+
return if @ssh_proxy
|
|
130
|
+
|
|
131
|
+
target = Config.ssh_forward_target(config)
|
|
132
|
+
ssh = config[:ssh]
|
|
133
|
+
@ssh_proxy = SSHProxy.new(
|
|
134
|
+
host: ssh[:host],
|
|
135
|
+
user: ssh[:user],
|
|
136
|
+
password: ssh[:password],
|
|
137
|
+
port: ssh[:port] || 22,
|
|
138
|
+
forward_host: target[:host],
|
|
139
|
+
forward_port: target[:port],
|
|
140
|
+
local_port: ssh[:local_port]
|
|
141
|
+
)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def client_config
|
|
145
|
+
return config unless @ssh_proxy
|
|
146
|
+
|
|
147
|
+
config.merge(ssh: config[:ssh].merge(local_port: @ssh_proxy.local_port, local_host: "127.0.0.1"))
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def tracked_names
|
|
151
|
+
@tracked ||= []
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def tracked_classes
|
|
155
|
+
tracked_names.filter_map do |n|
|
|
156
|
+
namespace_module.const_get(n) if namespace_module.const_defined?(n, false)
|
|
157
|
+
end.select { |k| k.is_a?(Class) }
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def bind_clients!
|
|
161
|
+
namespace_module.constants.each do |const_name|
|
|
162
|
+
klass = namespace_module.const_get(const_name)
|
|
163
|
+
next unless klass.is_a?(Class) && klass < Mongoid::Document
|
|
164
|
+
next if klass.embedded?
|
|
165
|
+
|
|
166
|
+
current = klass.storage_options[:client] || klass.storage_options["client"]
|
|
167
|
+
next if current && current.to_sym != :default
|
|
168
|
+
|
|
169
|
+
klass.store_in klass.storage_options.merge(client: client_name)
|
|
170
|
+
tracked_names << const_name
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def track_embedded_classes!
|
|
175
|
+
models.each do |klass|
|
|
176
|
+
relations = klass.embedded_relations
|
|
177
|
+
relations = klass.relations if relations.nil? || (relations.respond_to?(:empty?) && relations.empty? && klass.respond_to?(:relations))
|
|
178
|
+
Array(relations).each do |entry|
|
|
179
|
+
relation = entry.is_a?(Array) ? entry.last : entry
|
|
180
|
+
class_name = relation.respond_to?(:class_name) ? relation.class_name : relation[:class_name]
|
|
181
|
+
next if class_name.nil? || class_name.to_s.empty?
|
|
182
|
+
|
|
183
|
+
embedded = namespace_module.const_get(class_name) rescue nil
|
|
184
|
+
next unless embedded.is_a?(Class) && embedded < Mongoid::Document
|
|
185
|
+
next unless embedded.name.to_s.start_with?("#{namespace}::")
|
|
186
|
+
|
|
187
|
+
short = embedded.name.split("::").last.to_sym
|
|
188
|
+
tracked_names << short unless tracked_names.include?(short)
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def install_abbreviations!
|
|
194
|
+
models.each do |klass|
|
|
195
|
+
abbr = klass.name.split("::").last.gsub(/[^A-Z]/, "")
|
|
196
|
+
abbr = "M" if abbr.empty?
|
|
197
|
+
candidate = abbr
|
|
198
|
+
n = 2
|
|
199
|
+
while namespace_module.const_defined?(candidate, false)
|
|
200
|
+
candidate = "#{abbr}#{n}"
|
|
201
|
+
n += 1
|
|
202
|
+
end
|
|
203
|
+
namespace_module.const_set(candidate, klass)
|
|
204
|
+
tracked_names << candidate.to_sym
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def install_namespace_api!
|
|
209
|
+
env = self
|
|
210
|
+
mod = namespace_module
|
|
211
|
+
mod.define_singleton_method(:client) { env.client }
|
|
212
|
+
mod.define_singleton_method(:models) { env.models }
|
|
213
|
+
mod.define_singleton_method(:collections) { env.client.database.collection_names }
|
|
214
|
+
mod.define_singleton_method(:config) { env.config }
|
|
215
|
+
mod.define_singleton_method(:q) { |doc| env.client.database.command(doc) }
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|
data/lib/monq/errors.rb
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Array
|
|
4
|
+
def t
|
|
5
|
+
puts Monq::Ext.table_for(hash_docs)
|
|
6
|
+
nil
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def v
|
|
10
|
+
Monq::Ext.vertical(hash_docs)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# visidata comes later; always print the table so vd works without the binary.
|
|
14
|
+
def vd
|
|
15
|
+
t
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def write_csv(filename)
|
|
19
|
+
require "csv"
|
|
20
|
+
docs = respond_to?(:hash_docs, true) ? hash_docs : self
|
|
21
|
+
File.open(filename, "w:UTF-8") do |file|
|
|
22
|
+
csv = CSV.new(file)
|
|
23
|
+
return if docs.empty?
|
|
24
|
+
|
|
25
|
+
keys = docs.first.keys
|
|
26
|
+
csv << keys.map(&:to_s)
|
|
27
|
+
docs.each { |row| csv << keys.map { |k| row[k].to_s } }
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def write_excel(filename, sheet_name: "Sheet1")
|
|
32
|
+
require "caxlsx"
|
|
33
|
+
docs = hash_docs
|
|
34
|
+
package = Axlsx::Package.new
|
|
35
|
+
package.workbook.add_worksheet(name: sheet_name) do |sheet|
|
|
36
|
+
next if docs.empty?
|
|
37
|
+
|
|
38
|
+
keys = docs.first.keys
|
|
39
|
+
sheet.add_row(keys.map(&:to_s))
|
|
40
|
+
docs.each { |row| sheet.add_row(keys.map { |k| row[k].to_s }) }
|
|
41
|
+
end
|
|
42
|
+
package.serialize(filename)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def hash_docs
|
|
48
|
+
return self if empty?
|
|
49
|
+
return self if first.is_a?(Hash)
|
|
50
|
+
|
|
51
|
+
if first.respond_to?(:attributes)
|
|
52
|
+
map { |item| item.attributes }
|
|
53
|
+
else
|
|
54
|
+
self
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Monq
|
|
4
|
+
module Ext
|
|
5
|
+
module Criteria
|
|
6
|
+
def t
|
|
7
|
+
Monq::Ext.display_criteria(self)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def v
|
|
11
|
+
docs, = Monq::Ext.materialize(self)
|
|
12
|
+
docs.v
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def vd
|
|
16
|
+
t
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Full result set. Display methods cap at 500; export must not.
|
|
20
|
+
def write_csv(filename)
|
|
21
|
+
to_a.write_csv(filename)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def write_excel(filename, sheet_name: "Sheet1")
|
|
25
|
+
to_a.write_excel(filename, sheet_name: sheet_name)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
Mongoid::Criteria.include(Monq::Ext::Criteria)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Monq
|
|
4
|
+
module Ext
|
|
5
|
+
module Document
|
|
6
|
+
def t
|
|
7
|
+
[attributes].t
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def v
|
|
11
|
+
[attributes].v
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def vd
|
|
15
|
+
[attributes].vd
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def write_csv(filename)
|
|
19
|
+
[self].write_csv(filename)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def write_excel(filename, sheet_name: "Sheet1")
|
|
23
|
+
[self].write_excel(filename, sheet_name: sheet_name)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
Mongoid::Document.include(Monq::Ext::Document)
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kernel
|
|
4
|
+
def models
|
|
5
|
+
Monq::App.instance.environments.flat_map { |env| env.models }
|
|
6
|
+
rescue StandardError
|
|
7
|
+
[]
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def model_names
|
|
11
|
+
models.map(&:name)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def within_namespace(pattern, &block)
|
|
15
|
+
if pattern.is_a?(Module)
|
|
16
|
+
pattern.module_eval(&block)
|
|
17
|
+
return
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
env = find_environment { |candidate| name_matches?(candidate.namespace, pattern) }
|
|
21
|
+
unless env
|
|
22
|
+
$stderr.puts "Namespace #{pattern.inspect} not found"
|
|
23
|
+
return
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
env.namespace_module.module_eval(&block)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def within_env(pattern, &block)
|
|
30
|
+
env = find_environment { |candidate| name_matches?(candidate.name, pattern) }
|
|
31
|
+
unless env
|
|
32
|
+
$stderr.puts "Environment #{pattern.inspect} not found"
|
|
33
|
+
return
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
env.namespace_module.module_eval(&block)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def env_config(regexp)
|
|
40
|
+
env = find_environment { |candidate| candidate.name.to_s =~ regexp }
|
|
41
|
+
env&.config
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def q(doc)
|
|
45
|
+
environments = Monq::App.instance.environments
|
|
46
|
+
if environments.size != 1
|
|
47
|
+
$stderr.puts "use Namespace.q"
|
|
48
|
+
return nil
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
environments.first.client.database.command(doc)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def find_environment(&block)
|
|
57
|
+
Monq::App.instance.environments.find(&block)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def name_matches?(value, pattern)
|
|
61
|
+
case pattern
|
|
62
|
+
when Symbol, String
|
|
63
|
+
value.to_s == pattern.to_s
|
|
64
|
+
when Regexp
|
|
65
|
+
value.to_s =~ pattern
|
|
66
|
+
else
|
|
67
|
+
false
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Object
|
|
4
|
+
def self.const_missing(name)
|
|
5
|
+
app = Monq::App.instance if defined?(Monq::App)
|
|
6
|
+
if app&.environments
|
|
7
|
+
app.environments.each do |env|
|
|
8
|
+
mod = env.namespace_module
|
|
9
|
+
return mod.const_get(name) if mod.const_defined?(name, false)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
super
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Monq
|
|
4
|
+
module Ext
|
|
5
|
+
module View
|
|
6
|
+
def t
|
|
7
|
+
materialized_docs.t
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def v
|
|
11
|
+
materialized_docs.v
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def vd
|
|
15
|
+
materialized_docs.vd
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Full result set. Display methods cap at 500; export must not.
|
|
19
|
+
def write_csv(filename)
|
|
20
|
+
to_a.write_csv(filename)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def write_excel(filename, sheet_name: "Sheet1")
|
|
24
|
+
to_a.write_excel(filename, sheet_name: sheet_name)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def materialized_docs
|
|
30
|
+
view = self
|
|
31
|
+
if respond_to?(:options) && respond_to?(:limit) && Monq::Ext.limit_unset?(options)
|
|
32
|
+
view = limit(Monq::Ext::CAP)
|
|
33
|
+
end
|
|
34
|
+
view.to_a
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
Mongo::Collection::View.include(Monq::Ext::View)
|
data/lib/monq/ext.rb
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Monq
|
|
4
|
+
module Ext
|
|
5
|
+
CAP = 500
|
|
6
|
+
|
|
7
|
+
def self.materialize(criteria)
|
|
8
|
+
capped = limit_unset?(criteria.options)
|
|
9
|
+
criteria = criteria.limit(CAP) if capped
|
|
10
|
+
[criteria.to_a, capped]
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.display_criteria(criteria)
|
|
14
|
+
docs, capped = materialize(criteria)
|
|
15
|
+
puts table_for(docs)
|
|
16
|
+
puts "showing #{docs.size} rows (capped at #{CAP})" if capped
|
|
17
|
+
nil
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.table_for(docs)
|
|
21
|
+
rows = Presenter.rows(docs)
|
|
22
|
+
Table.new do |table|
|
|
23
|
+
table.headers = rows.first || []
|
|
24
|
+
table.body = rows[1..] || []
|
|
25
|
+
end.to_terminal
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.vertical(docs)
|
|
29
|
+
rows = Presenter.rows(docs)
|
|
30
|
+
return [] if rows.empty?
|
|
31
|
+
|
|
32
|
+
keys = rows.first
|
|
33
|
+
[keys, nil, *rows[1..]]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.limit_unset?(options)
|
|
37
|
+
return true if options.nil?
|
|
38
|
+
|
|
39
|
+
options[:limit].nil? && options["limit"].nil?
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Monq
|
|
4
|
+
class MultiIO
|
|
5
|
+
ANSI = /\e\[(\d+)(?:;\d+)*m/.freeze
|
|
6
|
+
|
|
7
|
+
def initialize(*targets)
|
|
8
|
+
@targets = targets
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def write(*args)
|
|
12
|
+
@targets.each do |target|
|
|
13
|
+
if tty?(target)
|
|
14
|
+
target.write(*args)
|
|
15
|
+
else
|
|
16
|
+
target.write(*args.map { |str| strip_ansi(str) })
|
|
17
|
+
end
|
|
18
|
+
target.flush if target.respond_to?(:flush)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def <<(target)
|
|
23
|
+
@targets << target
|
|
24
|
+
self
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def close
|
|
28
|
+
@targets.each { |target| target.close if target.respond_to?(:close) }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def delete(io)
|
|
32
|
+
@targets.delete(io)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def include?(io)
|
|
36
|
+
@targets.include?(io)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def tty?(target)
|
|
42
|
+
target.respond_to?(:isatty) ? target.isatty : false
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def strip_ansi(value)
|
|
46
|
+
value.to_s.gsub(ANSI, "")
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "bson"
|
|
5
|
+
|
|
6
|
+
module Monq
|
|
7
|
+
module Presenter
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def scalar(value)
|
|
11
|
+
case value
|
|
12
|
+
when BSON::ObjectId
|
|
13
|
+
value.to_s
|
|
14
|
+
when Time, Date
|
|
15
|
+
value.strftime("%Y-%m-%d %H:%M:%S")
|
|
16
|
+
when BSON::Binary
|
|
17
|
+
"#<BSON::Binary type=#{value.type} size=#{value.data.bytesize}>"
|
|
18
|
+
when BSON::Decimal128
|
|
19
|
+
value.to_s
|
|
20
|
+
when Hash, Array
|
|
21
|
+
json = JSON.generate(as_json_tree(value))
|
|
22
|
+
json.length > 120 ? "#{json[0, 120]}…" : json
|
|
23
|
+
else
|
|
24
|
+
value.respond_to?(:as_json) ? value.as_json : value.to_s
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def rows(docs)
|
|
29
|
+
return [] if docs.nil? || docs.empty?
|
|
30
|
+
|
|
31
|
+
keys = docs.first.keys
|
|
32
|
+
body = docs.map { |doc| keys.map { |key| scalar(doc[key]) } }
|
|
33
|
+
[keys, *body]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def as_json_tree(value)
|
|
37
|
+
case value
|
|
38
|
+
when BSON::ObjectId, BSON::Decimal128
|
|
39
|
+
value.to_s
|
|
40
|
+
when BSON::Binary
|
|
41
|
+
"#<BSON::Binary type=#{value.type} size=#{value.data.bytesize}>"
|
|
42
|
+
when Time, Date
|
|
43
|
+
value.strftime("%Y-%m-%d %H:%M:%S")
|
|
44
|
+
when Hash
|
|
45
|
+
value.each_with_object({}) { |(key, item), hash| hash[key] = as_json_tree(item) }
|
|
46
|
+
when Array
|
|
47
|
+
value.map { |item| as_json_tree(item) }
|
|
48
|
+
else
|
|
49
|
+
value.respond_to?(:as_json) ? value.as_json : value.to_s
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
private_class_method :as_json_tree
|
|
53
|
+
end
|
|
54
|
+
end
|
data/lib/monq/repl.rb
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pry"
|
|
4
|
+
require "rainbow"
|
|
5
|
+
require_relative "commands"
|
|
6
|
+
|
|
7
|
+
module Monq
|
|
8
|
+
class Repl
|
|
9
|
+
def start
|
|
10
|
+
Commands.register_pry_commands!
|
|
11
|
+
unless App.instance.options.babel_compatable
|
|
12
|
+
Pry.config.prompt = Pry::Prompt.new("monq", "monq", prompt)
|
|
13
|
+
end
|
|
14
|
+
Pry.start
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def prompt
|
|
18
|
+
[proc do |obj, nest_level, pry_instance|
|
|
19
|
+
names = App.instance.environments.map(&:name).join("+")
|
|
20
|
+
"%s#{Rainbow('@').green}%s%s%s [%d] %s " % [
|
|
21
|
+
Rainbow("MONQ").red,
|
|
22
|
+
Rainbow(names).yellow,
|
|
23
|
+
sandbox_indicator,
|
|
24
|
+
context_prompt(obj, nest_level),
|
|
25
|
+
pry_instance.input_ring.count,
|
|
26
|
+
Rainbow("❯").green
|
|
27
|
+
]
|
|
28
|
+
end]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def context_prompt(obj, nest_level)
|
|
32
|
+
if nest_level.zero?
|
|
33
|
+
"(#{obj})"
|
|
34
|
+
else
|
|
35
|
+
"(#{obj}:#{nest_level})"
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def sandbox_indicator
|
|
40
|
+
return "" unless App.instance.environments.any?(&:sandbox?)
|
|
41
|
+
|
|
42
|
+
" [%s] " % Rainbow("sandbox").green.bright
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/ssh/gateway"
|
|
4
|
+
|
|
5
|
+
module Monq
|
|
6
|
+
class SSHProxy
|
|
7
|
+
attr_reader :local_port
|
|
8
|
+
|
|
9
|
+
def initialize(host:, user:, forward_host:, forward_port:, port: 22, password: nil, local_port: nil)
|
|
10
|
+
@host = host
|
|
11
|
+
@user = user
|
|
12
|
+
@forward_host = forward_host
|
|
13
|
+
@forward_port = forward_port
|
|
14
|
+
@opts = { port: port }
|
|
15
|
+
@opts[:password] = password if password
|
|
16
|
+
open_gateway!(local_port)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def database_host_port
|
|
20
|
+
{ host: "127.0.0.1", port: @local_port }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def active?
|
|
24
|
+
@gateway.active?
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def reconnect
|
|
28
|
+
reconnect! unless active?
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def reconnect!
|
|
32
|
+
@gateway.shutdown!
|
|
33
|
+
open_gateway!(@local_port)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def open_gateway!(local_port)
|
|
39
|
+
@gateway = Net::SSH::Gateway.new(@host, @user, @opts)
|
|
40
|
+
@local_port = @gateway.open(@forward_host, @forward_port, local_port)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|