eventsimple 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.DS_Store +0 -0
- data/.rspec +3 -0
- data/.rubocop.yml +18 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +164 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +320 -0
- data/Guardfile +35 -0
- data/LICENSE +22 -0
- data/README.md +510 -0
- data/Rakefile +17 -0
- data/app/controllers/eventsimple/application_controller.rb +15 -0
- data/app/controllers/eventsimple/entities_controller.rb +59 -0
- data/app/controllers/eventsimple/home_controller.rb +5 -0
- data/app/controllers/eventsimple/models_controller.rb +10 -0
- data/app/views/eventsimple/entities/show.html.erb +109 -0
- data/app/views/eventsimple/home/index.html.erb +0 -0
- data/app/views/eventsimple/models/show.html.erb +19 -0
- data/app/views/eventsimple/shared/_header.html.erb +26 -0
- data/app/views/eventsimple/shared/_sidebar.html.erb +19 -0
- data/app/views/eventsimple/shared/_style.html.erb +105 -0
- data/app/views/layouts/eventsimple/application.html.erb +76 -0
- data/catalog-info.yaml +11 -0
- data/config/routes.rb +11 -0
- data/eventsimple.gemspec +42 -0
- data/lib/dry_types.rb +5 -0
- data/lib/eventsimple/configuration.rb +36 -0
- data/lib/eventsimple/data_type.rb +48 -0
- data/lib/eventsimple/dispatcher.rb +17 -0
- data/lib/eventsimple/engine.rb +37 -0
- data/lib/eventsimple/entity.rb +54 -0
- data/lib/eventsimple/event.rb +189 -0
- data/lib/eventsimple/event_dispatcher.rb +93 -0
- data/lib/eventsimple/generators/install_generator.rb +42 -0
- data/lib/eventsimple/generators/outbox/install_generator.rb +31 -0
- data/lib/eventsimple/generators/outbox/templates/create_outbox_cursor.erb +13 -0
- data/lib/eventsimple/generators/templates/create_events.erb +21 -0
- data/lib/eventsimple/generators/templates/event.erb +8 -0
- data/lib/eventsimple/invalid_transition.rb +14 -0
- data/lib/eventsimple/message.rb +23 -0
- data/lib/eventsimple/metadata.rb +11 -0
- data/lib/eventsimple/metadata_type.rb +38 -0
- data/lib/eventsimple/outbox/consumer.rb +52 -0
- data/lib/eventsimple/outbox/models/cursor.rb +25 -0
- data/lib/eventsimple/reactor_worker.rb +18 -0
- data/lib/eventsimple/support/spec_helpers.rb +47 -0
- data/lib/eventsimple/version.rb +5 -0
- data/lib/eventsimple.rb +41 -0
- data/log/development.log +0 -0
- data/sonar-project.properties +4 -0
- metadata +304 -0
@@ -0,0 +1,109 @@
|
|
1
|
+
<div class="row align-items-start">
|
2
|
+
<!-- Pane: Entity Details Start -->
|
3
|
+
<div class="col g-4 col-sm-8">
|
4
|
+
<h2><%= @model_name%>: <%= @aggregate_id %></h2>
|
5
|
+
<hr/>
|
6
|
+
|
7
|
+
<ul class="nav nav-tabs" role="tablist">
|
8
|
+
<li class="nav-item" role="presentation">
|
9
|
+
<a href="#" class="nav-link action-entity-query <%= @tab_id == 'entity' ? 'active' : '' %>" id="nav-tab-entity" data-param="t" data-value="entity">Entity</a>
|
10
|
+
</li>
|
11
|
+
<li class="nav-item" role="presentation">
|
12
|
+
<a href="#" class="nav-link action-entity-query <%= @tab_id == 'event' ? 'active' : '' %>" id="nav-tab-event" data-param="t" data-value="event">Event</a>
|
13
|
+
</li>
|
14
|
+
</ul>
|
15
|
+
|
16
|
+
<div class="tab-content pt-4 px-3">
|
17
|
+
<!-- Tab: Entity Start -->
|
18
|
+
<div id="tab-entity" class="tab-pane <%= @tab_id == 'entity' ? 'active' : '' %>" role="tabpanel" aria-labelledby="nav-tab-entity">
|
19
|
+
<p>
|
20
|
+
Shows the changes made before and after the currently selected event was applied.
|
21
|
+
</p>
|
22
|
+
<div class="table-responsive">
|
23
|
+
<table class="table table-striped">
|
24
|
+
<thead>
|
25
|
+
<tr>
|
26
|
+
<th scope="col" width="15%"></th>
|
27
|
+
<th scope="col" width="25%">Before</th>
|
28
|
+
<th scope="col" width="25%">After</th>
|
29
|
+
</tr>
|
30
|
+
</thead>
|
31
|
+
<tbody>
|
32
|
+
<% @entity_changes.each do |property| %>
|
33
|
+
<tr class="<%= property[:is_changed] ? 'table-info' : '' %>">
|
34
|
+
<th scope="row"><%= property[:label] %></th>
|
35
|
+
<td><code class="entity-property"><%= property[:historical_value].nil? ? '-' : property[:historical_value] %></code></th>
|
36
|
+
<td><code class="entity-property"><%= property[:current_value].nil? ? '-' : property[:current_value] %></code></td>
|
37
|
+
</tr>
|
38
|
+
<% end %>
|
39
|
+
</tbody>
|
40
|
+
</table>
|
41
|
+
</div>
|
42
|
+
</div>
|
43
|
+
<!-- Tab: Entity End -->
|
44
|
+
|
45
|
+
<!-- Tab: Event Start -->
|
46
|
+
<div id="tab-event" class="tab-pane <%= @tab_id == 'event' ? 'active' : '' %>" role="tabpanel" aria-labelledby="nav-tab-event">
|
47
|
+
<p>
|
48
|
+
The properties of the <code><%= @selected_event.type %></code> event.
|
49
|
+
</p>
|
50
|
+
<table class="table">
|
51
|
+
<tbody>
|
52
|
+
<tr>
|
53
|
+
<th scope="row">Identifier</th>
|
54
|
+
<td><code class="entity-property"><%= @selected_event.id %></code></td>
|
55
|
+
</tr>
|
56
|
+
<tr>
|
57
|
+
<th scope="row">Timestamp</th>
|
58
|
+
<td><code class="entity-property"><%= @selected_event.created_at %></code></td>
|
59
|
+
</tr>
|
60
|
+
<tr>
|
61
|
+
<th scope="row" colspan="2">Data</th>
|
62
|
+
</tr>
|
63
|
+
<% if @selected_event.data.present? %>
|
64
|
+
<% @selected_event.data.attributes.each do |attr_name, attr_value| %>
|
65
|
+
<tr>
|
66
|
+
<td> <%= attr_name %></td>
|
67
|
+
<td><code class="entity-property"><%= attr_value %></code></td>
|
68
|
+
</tr>
|
69
|
+
<% end %>
|
70
|
+
<% end %>
|
71
|
+
</tr>
|
72
|
+
<tr>
|
73
|
+
<th scope="row" colspan="2">Metadata</th>
|
74
|
+
</tr>
|
75
|
+
<% @selected_event.metadata.attributes.each do |attr_name, attr_value| %>
|
76
|
+
<tr>
|
77
|
+
<td> <%= attr_name %></td>
|
78
|
+
<td><code class="entity-property">: <%= attr_value %></code></td>
|
79
|
+
</tr>
|
80
|
+
<% end %>
|
81
|
+
</tbody>
|
82
|
+
</table>
|
83
|
+
</div>
|
84
|
+
<!-- Tab: Event End -->
|
85
|
+
</div>
|
86
|
+
</div>
|
87
|
+
<!-- Pane: Entity Details End -->
|
88
|
+
|
89
|
+
<!-- Pane: Time Travel Start -->
|
90
|
+
<div class="col g-4 col-sm-4">
|
91
|
+
<h3>Time Travel</h3>
|
92
|
+
<ul class="list-group">
|
93
|
+
<% @entity_event_history.each_with_index do | event, index | %>
|
94
|
+
<% is_active_list_item = event == @selected_event ? 'list-group-item-primary' : '' %>
|
95
|
+
<% is_active_link = event == @selected_event ? 'link-dark' : 'link-dark' %>
|
96
|
+
<% event_timestamp = index.zero? ? 'Current' : "#{time_ago_in_words(event.created_at)} ago" %>
|
97
|
+
<li class="list-group-item <%= is_active_list_item %>">
|
98
|
+
<a href="#" class="text-decoration-none action-entity-query <%= is_active_link %>" data-param="e" data-value="<%= event.id %>">
|
99
|
+
<div class="d-flex w-100 justify-content-between">
|
100
|
+
<span title="<%= event.type %>" style="width: 75%; overflow: hidden; white-space: nowrap; text-overflow: ellipsis;"><%= event.type.demodulize %></span>
|
101
|
+
<small><%= event_timestamp %></small>
|
102
|
+
</div>
|
103
|
+
</a>
|
104
|
+
</li>
|
105
|
+
<% end %>
|
106
|
+
</ul>
|
107
|
+
</div>
|
108
|
+
<!-- Pane: Time Travel End -->
|
109
|
+
</div>
|
File without changes
|
@@ -0,0 +1,19 @@
|
|
1
|
+
|
2
|
+
<table class="table table-striped">
|
3
|
+
<thead>
|
4
|
+
<tr>
|
5
|
+
<th scope="col" width="15%">Time</th>
|
6
|
+
<th scope="col" width="30%">Identifier</th>
|
7
|
+
<th scope="col" width="65%">Event</th>
|
8
|
+
</tr>
|
9
|
+
</thead>
|
10
|
+
<tbody>
|
11
|
+
<% @latest_entities.each do |event| %>
|
12
|
+
<tr>
|
13
|
+
<td scope="row"><%= "#{time_ago_in_words(event.created_at)} ago" %></td>
|
14
|
+
<td><%= link_to event.aggregate_id, model_entity_path(@model_name, event.aggregate_id) %></td>
|
15
|
+
<td><%= event.type %></td>
|
16
|
+
</tr>
|
17
|
+
<% end %>
|
18
|
+
</tbody>
|
19
|
+
</table>
|
@@ -0,0 +1,26 @@
|
|
1
|
+
<!-- Header Search Start -->
|
2
|
+
<header class="container mb-4">
|
3
|
+
<div class="row">
|
4
|
+
<div class="col col-sm-3">
|
5
|
+
<select id="form-search-model-name" class="form-select" aria-label="Model select" onchange="document.location.href=this.value" required>
|
6
|
+
<option value="">Choose event source model</option>
|
7
|
+
<% event_class_names.each do |klass| %>
|
8
|
+
<% is_active = klass === @model_name ? 'selected' : '' %>
|
9
|
+
<option value="<%= model_path(klass) %>" <%= is_active %>><%= klass %></option>
|
10
|
+
<% end %>
|
11
|
+
</select>
|
12
|
+
</div>
|
13
|
+
|
14
|
+
<% if @model_name %>
|
15
|
+
<div class="col col-sm-7">
|
16
|
+
<%= form_with url: model_entity_path(@model_name, ''), id: 'model-search' do |f| %>
|
17
|
+
<%= f.search_field :event_id, class: 'form-control', placeholder: 'Canonical identifier', value: @aggregate_id, required: true, aria: { label: 'Entity canonical identifier' } %>
|
18
|
+
</div>
|
19
|
+
<div class="col col-sm-2">
|
20
|
+
<%= f.submit 'Search', class: 'btn btn-primary' %>
|
21
|
+
<% end %>
|
22
|
+
</div>
|
23
|
+
<% end %>
|
24
|
+
</div>
|
25
|
+
</header>
|
26
|
+
<!-- Header Search End -->
|
@@ -0,0 +1,19 @@
|
|
1
|
+
<!-- Sidebar Nav Start -->
|
2
|
+
<nav class="d-flex flex-column flex-shrink-0 p-3 text-white bg-dark">
|
3
|
+
<%= link_to root_path, class: 'd-flex align-items-center mb-3 mb-md-0 me-md-auto text-white text-decoration-none' do %>
|
4
|
+
<span class="fs-4">Eventsimple</span>
|
5
|
+
<% end %>
|
6
|
+
<hr>
|
7
|
+
<ul class="nav nav-pills flex-column mb-auto">
|
8
|
+
<% event_class_names.each do |klass| %>
|
9
|
+
<% is_active = klass === @model_name ? 'active text-white' : 'text-white' %>
|
10
|
+
<li class="nav-item text-white overflow-hidden" style="width: 100%;">
|
11
|
+
<%= link_to model_path(klass), class: "nav-link #{is_active}", style: "overflow: hidden; white-space: nowrap; text-overflow: ellipsis;" do %>
|
12
|
+
<i class="bi bi-table"></i>
|
13
|
+
<%= klass %>
|
14
|
+
<% end %>
|
15
|
+
</li>
|
16
|
+
<% end %>
|
17
|
+
</ul>
|
18
|
+
</nav>
|
19
|
+
<!-- Sidebar Nav End -->
|
@@ -0,0 +1,105 @@
|
|
1
|
+
<style>
|
2
|
+
// Structure
|
3
|
+
body {
|
4
|
+
min-height: 100vh;
|
5
|
+
min-height: -webkit-fill-available;
|
6
|
+
}
|
7
|
+
|
8
|
+
html {
|
9
|
+
height: -webkit-fill-available;
|
10
|
+
}
|
11
|
+
|
12
|
+
#page {
|
13
|
+
display: flex;
|
14
|
+
flex-wrap: nowrap;
|
15
|
+
height: 100vh;
|
16
|
+
height: -webkit-fill-available;
|
17
|
+
max-height: 100vh;
|
18
|
+
overflow-x: auto;
|
19
|
+
overflow-y: hidden;
|
20
|
+
}
|
21
|
+
|
22
|
+
nav {
|
23
|
+
width: 20%;
|
24
|
+
}
|
25
|
+
|
26
|
+
main {
|
27
|
+
flex-shrink: 0;
|
28
|
+
height: 100vh;
|
29
|
+
width: 80%;
|
30
|
+
overflow-y: auto;
|
31
|
+
}
|
32
|
+
|
33
|
+
// Sidebar Nav
|
34
|
+
|
35
|
+
.bi {
|
36
|
+
vertical-align: -.125em;
|
37
|
+
pointer-events: none;
|
38
|
+
fill: currentColor;
|
39
|
+
}
|
40
|
+
|
41
|
+
.dropdown-toggle { outline: 0; }
|
42
|
+
|
43
|
+
.nav-flush .nav-link {
|
44
|
+
border-radius: 0;
|
45
|
+
}
|
46
|
+
|
47
|
+
.btn-toggle {
|
48
|
+
display: inline-flex;
|
49
|
+
align-items: center;
|
50
|
+
padding: .25rem .5rem;
|
51
|
+
font-weight: 600;
|
52
|
+
color: rgba(0, 0, 0, .65);
|
53
|
+
background-color: transparent;
|
54
|
+
border: 0;
|
55
|
+
}
|
56
|
+
.btn-toggle:hover,
|
57
|
+
.btn-toggle:focus {
|
58
|
+
color: rgba(0, 0, 0, .85);
|
59
|
+
background-color: #d2f4ea;
|
60
|
+
}
|
61
|
+
|
62
|
+
.btn-toggle::before {
|
63
|
+
width: 1.25em;
|
64
|
+
line-height: 0;
|
65
|
+
content: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='rgba%280,0,0,.5%29' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M5 14l6-6-6-6'/%3e%3c/svg%3e");
|
66
|
+
transition: transform .35s ease;
|
67
|
+
transform-origin: .5em 50%;
|
68
|
+
}
|
69
|
+
|
70
|
+
.btn-toggle[aria-expanded="true"] {
|
71
|
+
color: rgba(0, 0, 0, .85);
|
72
|
+
}
|
73
|
+
.btn-toggle[aria-expanded="true"]::before {
|
74
|
+
transform: rotate(90deg);
|
75
|
+
}
|
76
|
+
|
77
|
+
.btn-toggle-nav a {
|
78
|
+
display: inline-flex;
|
79
|
+
padding: .1875rem .5rem;
|
80
|
+
margin-top: .125rem;
|
81
|
+
margin-left: 1.25rem;
|
82
|
+
text-decoration: none;
|
83
|
+
}
|
84
|
+
.btn-toggle-nav a:hover,
|
85
|
+
.btn-toggle-nav a:focus {
|
86
|
+
background-color: #d2f4ea;
|
87
|
+
}
|
88
|
+
|
89
|
+
.scrollarea {
|
90
|
+
overflow-y: auto;
|
91
|
+
}
|
92
|
+
|
93
|
+
.fw-semibold { font-weight: 600; }
|
94
|
+
.lh-tight { line-height: 1.25; }
|
95
|
+
|
96
|
+
// Styles
|
97
|
+
|
98
|
+
code {
|
99
|
+
color: deeppink;
|
100
|
+
}
|
101
|
+
|
102
|
+
code.entity-property {
|
103
|
+
color: #333;
|
104
|
+
}
|
105
|
+
</style>
|
@@ -0,0 +1,76 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<meta charset="utf-8">
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
6
|
+
<meta name="description" content="">
|
7
|
+
<title>Eventsimple</title>
|
8
|
+
<%= csrf_meta_tags %>
|
9
|
+
<%= csp_meta_tag %>
|
10
|
+
|
11
|
+
<%= stylesheet_link_tag "https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css",
|
12
|
+
media: "all",
|
13
|
+
integrity: 'sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65',
|
14
|
+
crossorigin: 'anonymous'
|
15
|
+
%>
|
16
|
+
<%= stylesheet_link_tag "https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.3/font/bootstrap-icons.css",
|
17
|
+
media: "all",
|
18
|
+
crossorigin: 'anonymous'
|
19
|
+
%>
|
20
|
+
<%= javascript_include_tag "https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.min.js",
|
21
|
+
integrity: "sha384-cuYeSxntonz0PPNlHhBs68uyIAVpIIOZZ5JqeqvYYIcEL727kskC66kF92t6Xl2V",
|
22
|
+
crossorigin: "anonymous"
|
23
|
+
%>
|
24
|
+
<%= javascript_include_tag "https://cdn.jsdelivr.net/npm/jquery@3.6.3/dist/jquery.min.js",
|
25
|
+
crossorigin: "anonymous"
|
26
|
+
%>
|
27
|
+
|
28
|
+
<%= render partial: 'eventsimple/shared/style' %>
|
29
|
+
</head>
|
30
|
+
<body>
|
31
|
+
<div id="page">
|
32
|
+
<%= render partial: 'eventsimple/shared/sidebar' %>
|
33
|
+
|
34
|
+
<!-- Main Container Start -->
|
35
|
+
<main class="pt-4 px-2">
|
36
|
+
<%= render partial: 'eventsimple/shared/header' %>
|
37
|
+
|
38
|
+
<!-- Content Start -->
|
39
|
+
<article class="container">
|
40
|
+
<% if @error_message.present? %>
|
41
|
+
<div class="alert alert-warning d-flex" role="alert">
|
42
|
+
<i class="bi bi-exclamation-triangle-fill me-2"></i>
|
43
|
+
<div>
|
44
|
+
<%= @error_message %>
|
45
|
+
</div>
|
46
|
+
</div>
|
47
|
+
<% else %>
|
48
|
+
<%= yield %>
|
49
|
+
<% end %>
|
50
|
+
</article>
|
51
|
+
<!-- Content End -->
|
52
|
+
|
53
|
+
</main>
|
54
|
+
<!-- Main Container End -->
|
55
|
+
|
56
|
+
<script>
|
57
|
+
$( document ).ready(() => {
|
58
|
+
$('.action-entity-query').click((event) => {
|
59
|
+
const link = $(event.currentTarget);
|
60
|
+
const { param, value } = link.data();
|
61
|
+
const query = new URLSearchParams(window.location.search)
|
62
|
+
query.set(param, value);
|
63
|
+
link.attr("href",window.location.pathname + '?' + query.toString());
|
64
|
+
});
|
65
|
+
|
66
|
+
$('#model-search').on('submit', (event) => {
|
67
|
+
event.preventDefault();
|
68
|
+
const form = $(event.currentTarget);
|
69
|
+
window.location = form.attr('action') + form.children('#event_id').val();
|
70
|
+
});
|
71
|
+
});
|
72
|
+
</script>
|
73
|
+
|
74
|
+
</div>
|
75
|
+
</body>
|
76
|
+
</html>
|
data/catalog-info.yaml
ADDED
data/config/routes.rb
ADDED
data/eventsimple.gemspec
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "lib/eventsimple/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = 'eventsimple'
|
7
|
+
spec.version = Eventsimple::VERSION
|
8
|
+
spec.authors = ['Zulfiqar Ali']
|
9
|
+
spec.email = ['zulfiqar@wealthsimple.com']
|
10
|
+
|
11
|
+
spec.summary = 'Event driven toolkit using Rails and Sidekiq'
|
12
|
+
spec.description = 'Event driven architecture using Rails and Sidekiq'
|
13
|
+
spec.homepage = 'https://github.com/wealthsimple/eventsimple'
|
14
|
+
spec.required_ruby_version = ">= 3.2.0"
|
15
|
+
|
16
|
+
spec.metadata['changelog_uri'] = "https://github.com/wealthsimple/eventsimple/blob/main/CHANGELOG.md"
|
17
|
+
spec.license = 'MIT'
|
18
|
+
|
19
|
+
spec.files = Dir.chdir(__dir__) do
|
20
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
21
|
+
(f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git))})
|
22
|
+
end
|
23
|
+
end
|
24
|
+
spec.require_paths = ['lib']
|
25
|
+
|
26
|
+
spec.add_runtime_dependency 'dry-struct', '~> 1.6'
|
27
|
+
spec.add_runtime_dependency 'dry-types', '~> 1.7'
|
28
|
+
spec.add_runtime_dependency 'pg', '~> 1.4'
|
29
|
+
spec.add_runtime_dependency 'rails', '~> 7.0'
|
30
|
+
spec.add_runtime_dependency 'retriable', '~> 3.1'
|
31
|
+
spec.add_runtime_dependency 'sidekiq', '~> 7.0'
|
32
|
+
|
33
|
+
spec.add_development_dependency 'bundle-audit'
|
34
|
+
spec.add_development_dependency 'fuubar'
|
35
|
+
spec.add_development_dependency 'git'
|
36
|
+
spec.add_development_dependency 'guard-rspec'
|
37
|
+
spec.add_development_dependency 'parse_a_changelog'
|
38
|
+
spec.add_development_dependency 'pry'
|
39
|
+
spec.add_development_dependency 'puma'
|
40
|
+
spec.add_development_dependency 'rspec-rails'
|
41
|
+
spec.add_development_dependency 'ws-style'
|
42
|
+
end
|
data/lib/dry_types.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Eventsimple
|
4
|
+
class Configuration
|
5
|
+
attr_reader :max_concurrency_retries, :dispatchers
|
6
|
+
attr_writer :metadata_klass
|
7
|
+
|
8
|
+
attr_accessor :ui_visible_models
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@dispatchers = []
|
12
|
+
@max_concurrency_retries = 2
|
13
|
+
@metadata_klass = 'Eventsimple::Metadata'
|
14
|
+
|
15
|
+
@ui_visible_models = [] # internal use only
|
16
|
+
end
|
17
|
+
|
18
|
+
def max_concurrency_retries=(value)
|
19
|
+
unless value.is_a?(Integer) && value.positive?
|
20
|
+
raise ArgumentError, 'max_concurrency_retries must be a positive integer'
|
21
|
+
end
|
22
|
+
|
23
|
+
@max_concurrency_retries = value
|
24
|
+
end
|
25
|
+
|
26
|
+
def dispatchers=(value)
|
27
|
+
raise ArgumentError, 'dispatchers must be an array' unless value.is_a?(Array)
|
28
|
+
|
29
|
+
@dispatchers = value
|
30
|
+
end
|
31
|
+
|
32
|
+
def metadata_klass
|
33
|
+
@klass ||= @metadata_klass.constantize # rubocop:disable Naming/MemoizedInstanceVariableName
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Eventsimple
|
4
|
+
class DataType < ActiveModel::Type::Value
|
5
|
+
def initialize(event_klass)
|
6
|
+
@event_klass = event_klass
|
7
|
+
super()
|
8
|
+
end
|
9
|
+
|
10
|
+
attr_reader :event_klass
|
11
|
+
|
12
|
+
def type
|
13
|
+
:data_type
|
14
|
+
end
|
15
|
+
|
16
|
+
def cast_value(value)
|
17
|
+
case value
|
18
|
+
when String
|
19
|
+
decoded = ActiveSupport::JSON.decode(value)
|
20
|
+
return event_klass::Message.new(decoded) if event_klass.const_defined?(:Message)
|
21
|
+
|
22
|
+
decoded
|
23
|
+
when Hash
|
24
|
+
return event_klass::Message.new(value) if event_klass.const_defined?(:Message)
|
25
|
+
|
26
|
+
value
|
27
|
+
when event_klass::Message
|
28
|
+
value
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def serialize(value)
|
33
|
+
case value
|
34
|
+
when Hash, event_klass::Message
|
35
|
+
ActiveSupport::JSON.encode(value)
|
36
|
+
else
|
37
|
+
super
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def deserialize(value)
|
42
|
+
decoded = ActiveSupport::JSON.decode(value)
|
43
|
+
return event_klass::Message.new(decoded) if event_klass.const_defined?(:Message)
|
44
|
+
|
45
|
+
decoded
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Eventsimple
|
2
|
+
class Dispatcher
|
3
|
+
def self.on(*events, sync: [], async: [])
|
4
|
+
# Register Reactors to Events.
|
5
|
+
# * Reactors registered with `sync` will be synced synchronously
|
6
|
+
# * Reactors registered with `async` will be synced asynchronously via a Sidekiq Job
|
7
|
+
#
|
8
|
+
# Example:
|
9
|
+
#
|
10
|
+
# on BaseEvent, sync: LogEvent, async: TrackEvent
|
11
|
+
# on PledgeCancelled, PaymentFailed, async: [NotifyAdmin, CreateTask]
|
12
|
+
# on [PledgeCancelled, PaymentFailed], async: [NotifyAdmin, CreateTask]
|
13
|
+
#
|
14
|
+
EventDispatcher.rules.register(events: events.flatten, sync: Array(sync), async: Array(async))
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'rails'
|
2
|
+
|
3
|
+
module Eventsimple
|
4
|
+
class Engine < ::Rails::Engine
|
5
|
+
isolate_namespace Eventsimple
|
6
|
+
|
7
|
+
config.generators do |g|
|
8
|
+
g.test_framework :rspec
|
9
|
+
g.helper false
|
10
|
+
g.view_specs false
|
11
|
+
end
|
12
|
+
|
13
|
+
config.after_initialize do
|
14
|
+
dispatchers = Eventsimple.configuration.dispatchers.map(&:constantize)
|
15
|
+
|
16
|
+
unless dispatchers.all? { |dispatcher| dispatcher.superclass == Eventsimple::Dispatcher }
|
17
|
+
raise ArgumentError, 'dispatchers must inherit from Eventsimple::Dispatcher'
|
18
|
+
end
|
19
|
+
|
20
|
+
retry_intervals = Array.new(Eventsimple.configuration.max_concurrency_retries) { 0 }
|
21
|
+
|
22
|
+
Retriable.configure do |c|
|
23
|
+
c.contexts[:reactor] = {
|
24
|
+
tries: 7,
|
25
|
+
base_interval: 1.0,
|
26
|
+
multiplier: 1.0,
|
27
|
+
rand_factor: 0.0,
|
28
|
+
on: ActiveRecord::RecordNotFound,
|
29
|
+
}
|
30
|
+
c.contexts[:optimistic_locking] = {
|
31
|
+
intervals: retry_intervals,
|
32
|
+
on: ActiveRecord::StaleObjectError,
|
33
|
+
}
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Eventsimple
|
2
|
+
module Entity
|
3
|
+
DEFAULT_IGNORE_PROPS = %w[id lock_version].freeze
|
4
|
+
|
5
|
+
def event_driven_by(event_klass, aggregate_id:)
|
6
|
+
has_many :events, class_name: event_klass.name.to_s,
|
7
|
+
foreign_key: :aggregate_id,
|
8
|
+
primary_key: aggregate_id,
|
9
|
+
dependent: :delete_all,
|
10
|
+
inverse_of: model_name.element.to_sym,
|
11
|
+
autosave: false,
|
12
|
+
validate: false
|
13
|
+
|
14
|
+
class_attribute :ignored_for_projection, default: []
|
15
|
+
|
16
|
+
# disable automatic timestamp updates
|
17
|
+
self.record_timestamps = false
|
18
|
+
|
19
|
+
Eventsimple.configuration.ui_visible_models |= [self]
|
20
|
+
|
21
|
+
include InstanceMethods
|
22
|
+
extend ClassMethods
|
23
|
+
end
|
24
|
+
|
25
|
+
module InstanceMethods
|
26
|
+
def projection_matches_events?
|
27
|
+
reprojected = self.class.find(id).reproject
|
28
|
+
|
29
|
+
attributes == reprojected.attributes
|
30
|
+
end
|
31
|
+
|
32
|
+
def reproject(at: nil)
|
33
|
+
event_history = at ? events.where('created_at <= ?', at).load : events.load
|
34
|
+
ignore_props = (DEFAULT_IGNORE_PROPS + ignored_for_projection).map(&:to_s)
|
35
|
+
assign_attributes(self.class.column_defaults.except(*ignore_props))
|
36
|
+
|
37
|
+
event_history.each do |event|
|
38
|
+
event.apply(self)
|
39
|
+
event.apply_timestamps(self)
|
40
|
+
end
|
41
|
+
|
42
|
+
self
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
module ClassMethods
|
47
|
+
def event_class
|
48
|
+
reflect_on_all_associations(:has_many).find { |association|
|
49
|
+
association.name == :events
|
50
|
+
}.klass
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|