ruby_event_store-cli 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/README.md +98 -0
- data/bin/res +20 -0
- data/lib/ruby_event_store/cli/commands/base.rb +17 -0
- data/lib/ruby_event_store/cli/commands/event_show.rb +39 -0
- data/lib/ruby_event_store/cli/commands/event_streams.rb +30 -0
- data/lib/ruby_event_store/cli/commands/search.rb +34 -0
- data/lib/ruby_event_store/cli/commands/stats.rb +39 -0
- data/lib/ruby_event_store/cli/commands/stream_events.rb +50 -0
- data/lib/ruby_event_store/cli/commands/stream_show.rb +38 -0
- data/lib/ruby_event_store/cli/commands/trace.rb +55 -0
- data/lib/ruby_event_store/cli/commands/watch.rb +125 -0
- data/lib/ruby_event_store/cli/commands.rb +38 -0
- data/lib/ruby_event_store/cli/event_renderer.rb +34 -0
- data/lib/ruby_event_store/cli/read_events.rb +21 -0
- data/lib/ruby_event_store/cli/version.rb +7 -0
- data/lib/ruby_event_store/cli.rb +8 -0
- metadata +88 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 3cc76bc96f8012d2bdfaa641285adc472b2b10d6478b7e357acfafb6437bc4ac
|
|
4
|
+
data.tar.gz: 72784ab8ca2775dd942881df73f876b6a909aabebf5694ab94e7a3fe89c1628c
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 9863ab71a7a826037c01055ec8e548cfbb1ab94104d592dccda9a15013ca5df7096daa46ef7cb9859a3c4d009d85ec65b141acda505570117afb91e6c4448fb4
|
|
7
|
+
data.tar.gz: b76082eb5ad9e552a22187d357298f5f7e1fbc83a7b452250333da110f9a1a5c4c2ce5d358dcc37f540b5f9692f37417b15f7349779ac6819664883a91f9e2ee
|
data/README.md
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# ruby_event_store-cli
|
|
2
|
+
|
|
3
|
+
Command-line interface for inspecting a RubyEventStore event store without needing `rails console`.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Add to your application's Gemfile:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
gem "ruby_event_store-cli"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The `res` executable will be available in your project. Run all commands from your Rails app root directory — the CLI autodetects `config/environment.rb` and loads your environment.
|
|
14
|
+
|
|
15
|
+
## Commands
|
|
16
|
+
|
|
17
|
+
### Stream
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
# List events in a stream (default: last 50)
|
|
21
|
+
bundle exec res stream events MyStream
|
|
22
|
+
bundle exec res stream events MyStream --limit 20
|
|
23
|
+
bundle exec res stream events MyStream --format json
|
|
24
|
+
bundle exec res stream events MyStream --type OrderPlaced
|
|
25
|
+
bundle exec res stream events MyStream --after 2024-01-01T00:00:00Z
|
|
26
|
+
bundle exec res stream events MyStream --before 2024-03-01T00:00:00Z
|
|
27
|
+
bundle exec res stream events MyStream --from <event_uuid>
|
|
28
|
+
|
|
29
|
+
# Follow a stream for new events (Ctrl+C to stop)
|
|
30
|
+
bundle exec res stream events MyStream --follow
|
|
31
|
+
bundle exec res stream events MyStream -f
|
|
32
|
+
|
|
33
|
+
# Show stream summary
|
|
34
|
+
bundle exec res stream show MyStream
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Event
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
# Show full event details (data, metadata, timestamps)
|
|
41
|
+
bundle exec res event show <uuid>
|
|
42
|
+
|
|
43
|
+
# List all streams an event belongs to
|
|
44
|
+
bundle exec res event streams <uuid>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Search
|
|
48
|
+
|
|
49
|
+
Search events across all streams or within a specific one:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
bundle exec res search --type OrderPlaced
|
|
53
|
+
bundle exec res search --type OrderPlaced --limit 100
|
|
54
|
+
bundle exec res search --type OrderPlaced --after 2024-01-01T00:00:00Z
|
|
55
|
+
bundle exec res search --stream Orders --type OrderPlaced
|
|
56
|
+
bundle exec res search --format json | jq '.[].data'
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Trace
|
|
60
|
+
|
|
61
|
+
Display the causal tree for a correlation ID — all events triggered by a single request, in order:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
bundle exec res trace <correlation_id>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Stats
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
# Total event count and unique event types
|
|
71
|
+
bundle exec res stats
|
|
72
|
+
|
|
73
|
+
# Stats for a specific stream
|
|
74
|
+
bundle exec res stats --stream Orders
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Watch
|
|
78
|
+
|
|
79
|
+
Live view of new events as they arrive, grouped by bounded context (namespace prefix of the class name):
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
# Watch all new events (Ctrl+C to stop)
|
|
83
|
+
bundle exec res watch
|
|
84
|
+
|
|
85
|
+
# Filter to specific namespace(s)
|
|
86
|
+
bundle exec res watch --namespace Ordering
|
|
87
|
+
bundle exec res watch --namespace Ordering,Payments
|
|
88
|
+
|
|
89
|
+
# Watch events from a point in time
|
|
90
|
+
bundle exec res watch --since 2024-01-15T10:00:00Z
|
|
91
|
+
|
|
92
|
+
# Adjust polling interval and max events shown per namespace
|
|
93
|
+
bundle exec res watch --interval 2 --limit 20
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## License
|
|
97
|
+
|
|
98
|
+
MIT
|
data/bin/res
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "dry/cli"
|
|
5
|
+
require_relative "../lib/ruby_event_store/cli"
|
|
6
|
+
require_relative "../lib/ruby_event_store/cli/commands"
|
|
7
|
+
|
|
8
|
+
env_file = File.expand_path("config/environment.rb")
|
|
9
|
+
|
|
10
|
+
abort "Could not find config/environment.rb. Run `res` from the root of your Rails application." unless File.exist?(env_file)
|
|
11
|
+
|
|
12
|
+
require env_file
|
|
13
|
+
|
|
14
|
+
abort <<~MSG unless defined?(Rails) && Rails.configuration.respond_to?(:event_store)
|
|
15
|
+
Could not find event store instance after loading config/environment.rb.
|
|
16
|
+
|
|
17
|
+
Expected Rails.configuration.event_store to be set (standard RES setup).
|
|
18
|
+
MSG
|
|
19
|
+
|
|
20
|
+
Dry::CLI.new(RubyEventStore::CLI::Commands).call
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "dry/cli"
|
|
4
|
+
require "json"
|
|
5
|
+
require_relative "base"
|
|
6
|
+
|
|
7
|
+
module RubyEventStore
|
|
8
|
+
module CLI
|
|
9
|
+
module Commands
|
|
10
|
+
class EventShow < Base
|
|
11
|
+
desc "Print full event details including data, metadata, and timestamps"
|
|
12
|
+
|
|
13
|
+
argument :event_id, required: true, desc: "Event ID (UUID)"
|
|
14
|
+
|
|
15
|
+
def call(event_id:, **)
|
|
16
|
+
event = event_store.read.event!(event_id)
|
|
17
|
+
print_event(event)
|
|
18
|
+
rescue RubyEventStore::EventNotFound
|
|
19
|
+
warn "Event not found: #{event_id}"
|
|
20
|
+
exit 1
|
|
21
|
+
rescue => e
|
|
22
|
+
warn e.message
|
|
23
|
+
exit 1
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def print_event(event)
|
|
29
|
+
puts "Event ID: #{event.event_id}"
|
|
30
|
+
puts "Type: #{event.event_type}"
|
|
31
|
+
puts "Timestamp: #{event.timestamp.iso8601(3)}"
|
|
32
|
+
puts "Valid at: #{event.valid_at.iso8601(3)}"
|
|
33
|
+
puts "Data: #{JSON.pretty_generate(event.data)}"
|
|
34
|
+
puts "Metadata: #{JSON.pretty_generate(event.metadata.to_h)}"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "dry/cli"
|
|
4
|
+
require_relative "base"
|
|
5
|
+
|
|
6
|
+
module RubyEventStore
|
|
7
|
+
module CLI
|
|
8
|
+
module Commands
|
|
9
|
+
class EventStreams < Base
|
|
10
|
+
desc "List all streams the event has been published or linked to"
|
|
11
|
+
|
|
12
|
+
argument :event_id, required: true, desc: "Event ID (UUID)"
|
|
13
|
+
|
|
14
|
+
def call(event_id:, **)
|
|
15
|
+
streams = streams_of(event_id)
|
|
16
|
+
streams.empty? ? puts("(no streams — event not found or not linked to any stream)") : streams.each { |stream| puts stream.name }
|
|
17
|
+
rescue => e
|
|
18
|
+
warn e.message
|
|
19
|
+
exit 1
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def streams_of(event_id)
|
|
25
|
+
event_store.streams_of(event_id)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "dry/cli"
|
|
4
|
+
require_relative "base"
|
|
5
|
+
require_relative "../event_renderer"
|
|
6
|
+
require_relative "../read_events"
|
|
7
|
+
|
|
8
|
+
module RubyEventStore
|
|
9
|
+
module CLI
|
|
10
|
+
module Commands
|
|
11
|
+
class Search < Base
|
|
12
|
+
include EventRenderer
|
|
13
|
+
|
|
14
|
+
desc "Search events across all streams by type, time range, or stream name"
|
|
15
|
+
|
|
16
|
+
option :type, desc: "Filter by event type (class name)"
|
|
17
|
+
option :after, desc: "Filter events newer than timestamp (ISO8601)"
|
|
18
|
+
option :before, desc: "Filter events older than timestamp (ISO8601)"
|
|
19
|
+
option :stream, desc: "Limit search to a specific stream"
|
|
20
|
+
option :limit, type: :integer, default: 50, desc: "Max number of events (default: 50)"
|
|
21
|
+
option :format, default: "table", values: %w[table json], desc: "Output format"
|
|
22
|
+
|
|
23
|
+
def call(limit:, format:, type: nil, after: nil, before: nil, stream: nil, **)
|
|
24
|
+
specification = stream ? event_store.read.stream(stream) : event_store.read
|
|
25
|
+
events = ReadEvents.of(specification, type: type, after: after, before: before, limit: limit)
|
|
26
|
+
render(events, format: format)
|
|
27
|
+
rescue => e
|
|
28
|
+
warn e.message
|
|
29
|
+
exit 1
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "dry/cli"
|
|
4
|
+
require_relative "base"
|
|
5
|
+
|
|
6
|
+
module RubyEventStore
|
|
7
|
+
module CLI
|
|
8
|
+
module Commands
|
|
9
|
+
class Stats < Base
|
|
10
|
+
desc "Show total event count and unique event types. Use --stream for per-stream stats."
|
|
11
|
+
|
|
12
|
+
option :stream, desc: "Show stats for a specific stream"
|
|
13
|
+
|
|
14
|
+
def call(stream: nil, **)
|
|
15
|
+
specification = stream ? event_store.read.stream(stream) : event_store.read
|
|
16
|
+
print_stats(specification, stream: stream)
|
|
17
|
+
rescue => e
|
|
18
|
+
warn e.message
|
|
19
|
+
exit 1
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def print_stats(specification, stream:)
|
|
25
|
+
puts "Stream: #{stream}" if stream
|
|
26
|
+
puts "Events: #{specification.count}"
|
|
27
|
+
print_event_types(specification)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def print_event_types(specification)
|
|
31
|
+
types = specification.map(&:event_type).uniq.sort
|
|
32
|
+
return if types.empty?
|
|
33
|
+
puts "\nEvent types:"
|
|
34
|
+
types.each { |t| puts " #{t}" }
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "dry/cli"
|
|
4
|
+
require_relative "base"
|
|
5
|
+
require_relative "../event_renderer"
|
|
6
|
+
require_relative "../read_events"
|
|
7
|
+
|
|
8
|
+
module RubyEventStore
|
|
9
|
+
module CLI
|
|
10
|
+
module Commands
|
|
11
|
+
class StreamEvents < Base
|
|
12
|
+
include EventRenderer
|
|
13
|
+
desc "Print events from a stream. Supports filtering by type, time, and position. Use --follow/-f to tail live."
|
|
14
|
+
|
|
15
|
+
argument :stream_name, required: true, desc: "Stream name"
|
|
16
|
+
option :limit, type: :integer, default: 50, desc: "Max number of events (default: 50)"
|
|
17
|
+
option :format, default: "table", values: %w[table json], desc: "Output format"
|
|
18
|
+
option :type, desc: "Filter by event type (class name)"
|
|
19
|
+
option :after, desc: "Filter events newer than timestamp (ISO8601)"
|
|
20
|
+
option :before, desc: "Filter events older than timestamp (ISO8601)"
|
|
21
|
+
option :from, desc: "Start reading from event ID (exclusive)"
|
|
22
|
+
option :follow, type: :boolean, default: false, aliases: ["-f"], desc: "Watch for new events (Ctrl+C to stop)"
|
|
23
|
+
|
|
24
|
+
def call(stream_name:, limit:, format:, type: nil, after: nil, before: nil, from: nil, follow: false, **)
|
|
25
|
+
specification = event_store.read.stream(stream_name)
|
|
26
|
+
events = ReadEvents.of(specification, type: type, after: after, before: before, from: from, limit: limit)
|
|
27
|
+
render(events, format: format)
|
|
28
|
+
tail(stream_name, last_id: events.last&.event_id, type: type, format: format) if follow
|
|
29
|
+
rescue Interrupt
|
|
30
|
+
exit 0
|
|
31
|
+
rescue => e
|
|
32
|
+
warn e.message
|
|
33
|
+
exit 1
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def tail(stream_name, last_id:, type:, format:)
|
|
39
|
+
loop do
|
|
40
|
+
sleep 1
|
|
41
|
+
events = ReadEvents.of(event_store.read.stream(stream_name), type: type, from: last_id)
|
|
42
|
+
next if events.empty?
|
|
43
|
+
render(events, format: format)
|
|
44
|
+
last_id = events.last.event_id
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "dry/cli"
|
|
4
|
+
require_relative "base"
|
|
5
|
+
|
|
6
|
+
module RubyEventStore
|
|
7
|
+
module CLI
|
|
8
|
+
module Commands
|
|
9
|
+
class StreamShow < Base
|
|
10
|
+
desc "Show event count, version, and first/last event for a stream"
|
|
11
|
+
|
|
12
|
+
argument :stream_name, required: true, desc: "Stream name"
|
|
13
|
+
|
|
14
|
+
def call(stream_name:, **)
|
|
15
|
+
specification = event_store.read.stream(stream_name)
|
|
16
|
+
print_stream(stream_name, specification)
|
|
17
|
+
rescue => e
|
|
18
|
+
warn e.message
|
|
19
|
+
exit 1
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def print_stream(stream_name, specification)
|
|
25
|
+
count = specification.count
|
|
26
|
+
puts "Stream: #{stream_name}"
|
|
27
|
+
puts "Events: #{count}"
|
|
28
|
+
return if count.zero?
|
|
29
|
+
first = specification.first
|
|
30
|
+
last = specification.last
|
|
31
|
+
puts "Version: #{count - 1}"
|
|
32
|
+
puts "First: #{first.timestamp.iso8601(3)} (#{first.event_type})"
|
|
33
|
+
puts "Last: #{last.timestamp.iso8601(3)} (#{last.event_type})"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "dry/cli"
|
|
4
|
+
require_relative "base"
|
|
5
|
+
|
|
6
|
+
module RubyEventStore
|
|
7
|
+
module CLI
|
|
8
|
+
module Commands
|
|
9
|
+
class Trace < Base
|
|
10
|
+
desc "Print the causation tree for all events sharing a correlation ID"
|
|
11
|
+
|
|
12
|
+
argument :correlation_id, required: true, desc: "Correlation ID (UUID)"
|
|
13
|
+
|
|
14
|
+
def call(correlation_id:, **)
|
|
15
|
+
events = events_for(correlation_id)
|
|
16
|
+
if events.empty?
|
|
17
|
+
puts "(no events found for correlation ID #{correlation_id})"
|
|
18
|
+
return
|
|
19
|
+
end
|
|
20
|
+
print_causation_tree(events)
|
|
21
|
+
rescue => e
|
|
22
|
+
warn e.message
|
|
23
|
+
exit 1
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def events_for(correlation_id)
|
|
29
|
+
event_store.read.stream("$by_correlation_id_#{correlation_id}").to_a
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def print_causation_tree(events)
|
|
33
|
+
causation = events.group_by { |e| e.metadata[:causation_id] }
|
|
34
|
+
roots = root_events(events)
|
|
35
|
+
roots.each { |e| print_tree(e, causation, "", true, roots.last == e) }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def root_events(events)
|
|
39
|
+
event_ids = events.map(&:event_id).to_set
|
|
40
|
+
events.reject { |e| event_ids.include?(e.metadata[:causation_id]) }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def print_tree(event, by_causation, prefix, root, last)
|
|
44
|
+
connector = root ? "" : (last ? "└── " : "├── ")
|
|
45
|
+
puts "#{prefix}#{connector}#{event.event_type} [#{event.event_id}]"
|
|
46
|
+
children = by_causation[event.event_id] || []
|
|
47
|
+
child_prefix = root ? prefix : prefix + (last ? " " : "│ ")
|
|
48
|
+
children.each_with_index do |child, i|
|
|
49
|
+
print_tree(child, by_causation, child_prefix, false, i == children.size - 1)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "dry/cli"
|
|
4
|
+
require_relative "base"
|
|
5
|
+
|
|
6
|
+
module RubyEventStore
|
|
7
|
+
module CLI
|
|
8
|
+
module Commands
|
|
9
|
+
class Watch < Base
|
|
10
|
+
desc "Watch new events live, grouped by bounded context"
|
|
11
|
+
|
|
12
|
+
option :namespace, desc: "Filter by namespace(s), comma-separated (e.g. Ordering,Payments)"
|
|
13
|
+
option :since, desc: "Watch events since timestamp (ISO8601, default: now)"
|
|
14
|
+
option :limit, type: :integer, default: 50, desc: "Max events shown per namespace (default: 50)"
|
|
15
|
+
option :interval, type: :integer, default: 1, desc: "Refresh interval in seconds (default: 1)"
|
|
16
|
+
|
|
17
|
+
def call(namespace: nil, since: nil, limit:, interval:, **)
|
|
18
|
+
started_at = since ? Time.parse(since) : Time.now
|
|
19
|
+
namespaces = namespace&.split(",")&.map(&:strip)
|
|
20
|
+
watch(since: started_at, namespaces: namespaces, limit: limit.to_i, interval: interval.to_i)
|
|
21
|
+
rescue Interrupt
|
|
22
|
+
show_cursor
|
|
23
|
+
exit 0
|
|
24
|
+
rescue => e
|
|
25
|
+
show_cursor
|
|
26
|
+
warn e.message
|
|
27
|
+
exit 1
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def watch(since:, namespaces:, limit:, interval:)
|
|
33
|
+
hide_cursor
|
|
34
|
+
loop do
|
|
35
|
+
events = grouped_events(since: since, namespaces: namespaces)
|
|
36
|
+
render(events, limit: limit, since: since)
|
|
37
|
+
sleep interval
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def grouped_events(since:, namespaces:)
|
|
42
|
+
events = events_since(since)
|
|
43
|
+
events = filter_by_namespaces(events, namespaces)
|
|
44
|
+
group_by_namespace(events)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def events_since(since)
|
|
48
|
+
event_store.read.newer_than(since).map do |e|
|
|
49
|
+
{ event_id: e.event_id, type: e.event_type, timestamp: e.timestamp }
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def filter_by_namespaces(events, namespaces)
|
|
54
|
+
return events unless namespaces
|
|
55
|
+
events.select { |e| namespaces.include?(namespace(e[:type])) }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def group_by_namespace(events)
|
|
59
|
+
events.group_by { |e| namespace(e[:type]) }.sort
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def render(grouped, limit:, since:)
|
|
63
|
+
lines = grouped.empty? ? [empty_message(since)] : event_lines(grouped, limit)
|
|
64
|
+
lines << footer(since)
|
|
65
|
+
clear_screen
|
|
66
|
+
puts lines.join("\n")
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def event_lines(grouped, limit)
|
|
70
|
+
grouped.flat_map do |ns, ns_events|
|
|
71
|
+
[
|
|
72
|
+
bold("#{ns} (#{ns_events.size} events)"),
|
|
73
|
+
*ns_events.last(limit).map { |e| format_event(e) },
|
|
74
|
+
""
|
|
75
|
+
]
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def format_event(e)
|
|
80
|
+
" #{pad(short_type(e[:type]), 30)} #{e[:timestamp].strftime("%H:%M:%S")} #{e[:event_id]}"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def empty_message(since)
|
|
84
|
+
dim("No events yet — waiting since #{since.strftime("%H:%M:%S")}")
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def footer(since)
|
|
88
|
+
dim("Watching since #{since.strftime("%H:%M:%S")} — Press Ctrl+C to exit")
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def namespace(type)
|
|
92
|
+
type.include?("::") ? type.split("::").first : "Other"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def short_type(type)
|
|
96
|
+
type.split("::").last
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def pad(str, width)
|
|
100
|
+
str.ljust(width)[0, width]
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def clear_screen
|
|
104
|
+
system("clear")
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def hide_cursor
|
|
108
|
+
print "\e[?25l"
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def show_cursor
|
|
112
|
+
print "\e[?25h"
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def bold(str)
|
|
116
|
+
"\e[1m#{str}\e[0m"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def dim(str)
|
|
120
|
+
"\e[2m#{str}\e[0m"
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "dry/cli"
|
|
4
|
+
require_relative "commands/base"
|
|
5
|
+
require_relative "commands/stream_events"
|
|
6
|
+
require_relative "commands/stream_show"
|
|
7
|
+
require_relative "commands/event_show"
|
|
8
|
+
require_relative "commands/event_streams"
|
|
9
|
+
require_relative "commands/trace"
|
|
10
|
+
require_relative "commands/search"
|
|
11
|
+
require_relative "commands/stats"
|
|
12
|
+
require_relative "commands/watch"
|
|
13
|
+
|
|
14
|
+
module RubyEventStore
|
|
15
|
+
module CLI
|
|
16
|
+
module Commands
|
|
17
|
+
extend Dry::CLI::Registry
|
|
18
|
+
|
|
19
|
+
register "stream", Class.new(Dry::CLI::Command) {
|
|
20
|
+
desc "Inspect a stream"
|
|
21
|
+
def call(**) = warn "Usage: res stream SUBCOMMAND\n\nSubcommands: events, show\n\nRun `res stream --help` for details."
|
|
22
|
+
}
|
|
23
|
+
register "stream events", StreamEvents
|
|
24
|
+
register "stream show", StreamShow
|
|
25
|
+
|
|
26
|
+
register "event", Class.new(Dry::CLI::Command) {
|
|
27
|
+
desc "Inspect an event"
|
|
28
|
+
def call(**) = warn "Usage: res event SUBCOMMAND\n\nSubcommands: show, streams\n\nRun `res event --help` for details."
|
|
29
|
+
}
|
|
30
|
+
register "event show", EventShow
|
|
31
|
+
register "event streams", EventStreams
|
|
32
|
+
register "trace", Trace
|
|
33
|
+
register "search", Search
|
|
34
|
+
register "stats", Stats
|
|
35
|
+
register "watch", Watch
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module RubyEventStore
|
|
6
|
+
module CLI
|
|
7
|
+
module EventRenderer
|
|
8
|
+
def render(events, format:)
|
|
9
|
+
case format
|
|
10
|
+
when "json" then render_json(events)
|
|
11
|
+
when "table" then render_table(events)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def render_json(events)
|
|
16
|
+
puts JSON.pretty_generate(events.map { |e|
|
|
17
|
+
{ event_id: e.event_id, event_type: e.event_type, data: e.data,
|
|
18
|
+
metadata: e.metadata.to_h, timestamp: e.timestamp.iso8601(3) }
|
|
19
|
+
})
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def render_table(events)
|
|
23
|
+
return puts "(no events)" if events.empty?
|
|
24
|
+
|
|
25
|
+
puts "%-36s %-40s %s" % ["EVENT ID", "TYPE", "TIMESTAMP"]
|
|
26
|
+
puts "-" * 90
|
|
27
|
+
events.each do |e|
|
|
28
|
+
puts "%-36s %-40s %s" % [e.event_id, e.event_type, e.timestamp.iso8601(3)]
|
|
29
|
+
end
|
|
30
|
+
puts "\n#{events.size} event(s)"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyEventStore
|
|
4
|
+
module CLI
|
|
5
|
+
class ReadEvents
|
|
6
|
+
def self.of(specification, type: nil, after: nil, before: nil, from: nil, limit: nil)
|
|
7
|
+
specification = specification.of_type(resolve_type(type)) if type
|
|
8
|
+
specification = specification.newer_than(Time.parse(after)) if after
|
|
9
|
+
specification = specification.older_than(Time.parse(before)) if before
|
|
10
|
+
specification = specification.from(from) if from
|
|
11
|
+
limit ? specification.limit(limit.to_i).to_a : specification.to_a
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.resolve_type(name)
|
|
15
|
+
Object.const_get(name)
|
|
16
|
+
rescue NameError
|
|
17
|
+
raise "Unknown event type: #{name}"
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: ruby_event_store-cli
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Arkency
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: dry-cli
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '1.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '1.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: ruby_event_store
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: 1.0.0
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: 1.0.0
|
|
40
|
+
email: dev@arkency.com
|
|
41
|
+
executables:
|
|
42
|
+
- res
|
|
43
|
+
extensions: []
|
|
44
|
+
extra_rdoc_files:
|
|
45
|
+
- README.md
|
|
46
|
+
files:
|
|
47
|
+
- README.md
|
|
48
|
+
- bin/res
|
|
49
|
+
- lib/ruby_event_store/cli.rb
|
|
50
|
+
- lib/ruby_event_store/cli/commands.rb
|
|
51
|
+
- lib/ruby_event_store/cli/commands/base.rb
|
|
52
|
+
- lib/ruby_event_store/cli/commands/event_show.rb
|
|
53
|
+
- lib/ruby_event_store/cli/commands/event_streams.rb
|
|
54
|
+
- lib/ruby_event_store/cli/commands/search.rb
|
|
55
|
+
- lib/ruby_event_store/cli/commands/stats.rb
|
|
56
|
+
- lib/ruby_event_store/cli/commands/stream_events.rb
|
|
57
|
+
- lib/ruby_event_store/cli/commands/stream_show.rb
|
|
58
|
+
- lib/ruby_event_store/cli/commands/trace.rb
|
|
59
|
+
- lib/ruby_event_store/cli/commands/watch.rb
|
|
60
|
+
- lib/ruby_event_store/cli/event_renderer.rb
|
|
61
|
+
- lib/ruby_event_store/cli/read_events.rb
|
|
62
|
+
- lib/ruby_event_store/cli/version.rb
|
|
63
|
+
homepage: https://railseventstore.org
|
|
64
|
+
licenses:
|
|
65
|
+
- MIT
|
|
66
|
+
metadata:
|
|
67
|
+
homepage_uri: https://railseventstore.org
|
|
68
|
+
source_code_uri: https://github.com/RailsEventStore/rails_event_store
|
|
69
|
+
bug_tracker_uri: https://github.com/RailsEventStore/rails_event_store/issues
|
|
70
|
+
rubygems_mfa_required: 'true'
|
|
71
|
+
rdoc_options: []
|
|
72
|
+
require_paths:
|
|
73
|
+
- lib
|
|
74
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
75
|
+
requirements:
|
|
76
|
+
- - ">="
|
|
77
|
+
- !ruby/object:Gem::Version
|
|
78
|
+
version: '2.7'
|
|
79
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
80
|
+
requirements:
|
|
81
|
+
- - ">="
|
|
82
|
+
- !ruby/object:Gem::Version
|
|
83
|
+
version: '0'
|
|
84
|
+
requirements: []
|
|
85
|
+
rubygems_version: 3.7.1
|
|
86
|
+
specification_version: 4
|
|
87
|
+
summary: Command-line interface for Ruby Event Store
|
|
88
|
+
test_files: []
|