acta 0.2.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.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/.tool-versions +1 -0
  3. data/CHANGELOG.md +210 -0
  4. data/LICENSE +21 -0
  5. data/PLAN.md +158 -0
  6. data/README.md +559 -0
  7. data/Rakefile +12 -0
  8. data/app/controllers/acta/web/application_controller.rb +10 -0
  9. data/app/controllers/acta/web/events_controller.rb +37 -0
  10. data/app/helpers/acta/web/application_helper.rb +106 -0
  11. data/app/views/acta/web/events/index.html.erb +312 -0
  12. data/app/views/acta/web/events/show.html.erb +72 -0
  13. data/app/views/layouts/acta/web/application.html.erb +594 -0
  14. data/config/routes.rb +4 -0
  15. data/lib/acta/actor.rb +34 -0
  16. data/lib/acta/adapters/base.rb +59 -0
  17. data/lib/acta/adapters/postgres.rb +73 -0
  18. data/lib/acta/adapters/sqlite.rb +58 -0
  19. data/lib/acta/adapters.rb +19 -0
  20. data/lib/acta/array_type.rb +30 -0
  21. data/lib/acta/command.rb +48 -0
  22. data/lib/acta/current.rb +10 -0
  23. data/lib/acta/errors.rb +102 -0
  24. data/lib/acta/event.rb +80 -0
  25. data/lib/acta/events_query.rb +73 -0
  26. data/lib/acta/handler.rb +9 -0
  27. data/lib/acta/model.rb +58 -0
  28. data/lib/acta/model_type.rb +32 -0
  29. data/lib/acta/projection.rb +64 -0
  30. data/lib/acta/projection_managed.rb +108 -0
  31. data/lib/acta/railtie.rb +65 -0
  32. data/lib/acta/reactor.rb +15 -0
  33. data/lib/acta/reactor_job.rb +19 -0
  34. data/lib/acta/record.rb +10 -0
  35. data/lib/acta/schema.rb +12 -0
  36. data/lib/acta/serializable.rb +48 -0
  37. data/lib/acta/testing/dsl.rb +90 -0
  38. data/lib/acta/testing/matchers.rb +77 -0
  39. data/lib/acta/testing.rb +50 -0
  40. data/lib/acta/types/encrypted_string.rb +63 -0
  41. data/lib/acta/version.rb +5 -0
  42. data/lib/acta/web/engine.rb +13 -0
  43. data/lib/acta/web/events_query.rb +81 -0
  44. data/lib/acta/web.rb +45 -0
  45. data/lib/acta.rb +296 -0
  46. data/lib/generators/acta/install/install_generator.rb +23 -0
  47. data/lib/generators/acta/install/templates/create_acta_events.rb.tt +9 -0
  48. data/sig/acta.rbs +4 -0
  49. metadata +152 -0
@@ -0,0 +1,594 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <title>Acta · Event Log</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
10
+ <style>
11
+ /* ── Reset ── */
12
+ .acta-root, .acta-root *, .acta-root *::before, .acta-root *::after {
13
+ box-sizing: border-box; margin: 0; padding: 0;
14
+ }
15
+ .acta-root a { color: inherit; }
16
+ .acta-root button { font-family: inherit; cursor: pointer; }
17
+
18
+ /* ── Palette ── */
19
+ .acta-root {
20
+ --bg: #0b0c0f;
21
+ --panel: #101217;
22
+ --fg: #e7e8ec;
23
+ --dim: #6c7280;
24
+ --border: #1d2027;
25
+ --hover: #15181f;
26
+ --accent: oklch(0.78 0.14 220);
27
+ --green: #22c55e;
28
+ }
29
+
30
+ /* ── Base ── */
31
+ .acta-root {
32
+ background: var(--bg);
33
+ color: var(--fg);
34
+ font-family: 'JetBrains Mono', ui-monospace, 'Cascadia Code', Menlo, Consolas, monospace;
35
+ height: 100vh;
36
+ display: flex;
37
+ flex-direction: column;
38
+ overflow: hidden;
39
+ font-size: 12px;
40
+ line-height: 1.4;
41
+ }
42
+ .acta-sans { font-family: Inter, system-ui, sans-serif; }
43
+ .acta-dim { color: var(--dim); }
44
+ .acta-fg { color: var(--fg); }
45
+ .acta-accent { color: var(--accent); }
46
+
47
+ /* ── Scrollbars ── */
48
+ .acta-root ::-webkit-scrollbar { width: 8px; height: 8px; }
49
+ .acta-root ::-webkit-scrollbar-track { background: transparent; }
50
+ .acta-root ::-webkit-scrollbar-thumb { background: rgba(127,127,127,0.2); border-radius: 4px; }
51
+ .acta-root ::-webkit-scrollbar-thumb:hover { background: rgba(127,127,127,0.4); }
52
+
53
+ /* ── Header ── */
54
+ .acta-header {
55
+ border-bottom: 1px solid var(--border);
56
+ padding: 10px 18px;
57
+ display: flex;
58
+ align-items: center;
59
+ gap: 16px;
60
+ flex-shrink: 0;
61
+ }
62
+ .acta-logo {
63
+ font-family: Inter, system-ui, sans-serif;
64
+ font-size: 12px;
65
+ font-weight: 600;
66
+ display: flex;
67
+ align-items: center;
68
+ gap: 8px;
69
+ white-space: nowrap;
70
+ text-decoration: none;
71
+ }
72
+ .acta-logo-dot {
73
+ width: 11px; height: 11px;
74
+ border-radius: 2px;
75
+ background: var(--accent);
76
+ flex-shrink: 0;
77
+ }
78
+
79
+ /* ── Filter bar ── */
80
+ .acta-filter-bar {
81
+ display: flex;
82
+ align-items: center;
83
+ gap: 14px;
84
+ margin-left: 24px;
85
+ flex: 1;
86
+ }
87
+ .acta-filter-group {
88
+ display: flex;
89
+ align-items: center;
90
+ gap: 8px;
91
+ position: relative;
92
+ }
93
+ .acta-filter-group .acta-label {
94
+ font-family: Inter, system-ui, sans-serif;
95
+ font-size: 10px;
96
+ text-transform: uppercase;
97
+ letter-spacing: 0.6px;
98
+ color: var(--dim);
99
+ white-space: nowrap;
100
+ }
101
+ .acta-filter-input {
102
+ padding: 6px 0;
103
+ font-family: inherit;
104
+ font-size: 12px;
105
+ background: transparent;
106
+ color: var(--fg);
107
+ border: none;
108
+ border-bottom: 1px solid var(--border);
109
+ outline: none;
110
+ transition: border-color 0.1s;
111
+ }
112
+ .acta-filter-input::placeholder { color: var(--dim); }
113
+ .acta-filter-input:focus { border-bottom-color: var(--accent); }
114
+ .acta-filter-input-q { flex: 1 1 320px; max-width: 480px; }
115
+ .acta-filter-input-sk { flex: 0 1 220px; min-width: 140px; }
116
+ .acta-filter-input-sk.has-value { border-bottom-color: var(--accent); }
117
+ .acta-clear-btn {
118
+ background: transparent;
119
+ border: none;
120
+ color: var(--dim);
121
+ font-size: 11px;
122
+ padding: 0;
123
+ line-height: 1;
124
+ text-decoration: none;
125
+ flex-shrink: 0;
126
+ }
127
+ .acta-clear-btn:hover { color: var(--fg); }
128
+ .acta-filter-submit {
129
+ padding: 4px 10px;
130
+ font-size: 10px;
131
+ font-family: Inter, system-ui, sans-serif;
132
+ background: transparent;
133
+ color: var(--dim);
134
+ border: 1px solid var(--border);
135
+ border-radius: 3px;
136
+ }
137
+ .acta-filter-submit:hover { color: var(--fg); background: var(--hover); }
138
+
139
+ /* ── Status ── */
140
+ .acta-status {
141
+ margin-left: auto;
142
+ display: flex;
143
+ align-items: center;
144
+ gap: 14px;
145
+ font-family: Inter, system-ui, sans-serif;
146
+ font-size: 10px;
147
+ color: var(--dim);
148
+ white-space: nowrap;
149
+ }
150
+ .acta-status-dot { color: var(--green); }
151
+
152
+ /* ── Body layout ── */
153
+ .acta-body {
154
+ display: flex;
155
+ flex: 1;
156
+ min-height: 0;
157
+ overflow: hidden;
158
+ }
159
+
160
+ /* ── Left rail ── */
161
+ .acta-rail {
162
+ width: 220px;
163
+ border-right: 1px solid var(--border);
164
+ padding: 14px 0;
165
+ overflow-y: auto;
166
+ flex-shrink: 0;
167
+ }
168
+ .acta-facet { margin-bottom: 14px; }
169
+ .acta-facet summary {
170
+ padding: 4px 14px;
171
+ font-family: Inter, system-ui, sans-serif;
172
+ font-size: 9px;
173
+ color: var(--dim);
174
+ text-transform: uppercase;
175
+ letter-spacing: 0.8px;
176
+ cursor: pointer;
177
+ list-style: none;
178
+ display: flex;
179
+ justify-content: space-between;
180
+ align-items: center;
181
+ user-select: none;
182
+ }
183
+ .acta-facet summary::-webkit-details-marker { display: none; }
184
+ .acta-facet summary::after { content: '−'; font-size: 11px; }
185
+ .acta-facet:not([open]) summary::after { content: '+'; }
186
+ .acta-facet-item {
187
+ display: flex;
188
+ align-items: center;
189
+ gap: 6px;
190
+ padding: 3px 14px;
191
+ color: var(--dim);
192
+ text-decoration: none;
193
+ border-left: 2px solid transparent;
194
+ font-size: 10.5px;
195
+ white-space: nowrap;
196
+ overflow: hidden;
197
+ }
198
+ .acta-facet-item:hover { background: var(--hover); color: var(--fg); }
199
+ .acta-facet-item.active {
200
+ background: var(--hover);
201
+ color: var(--fg);
202
+ border-left-color: var(--accent);
203
+ }
204
+ .acta-facet-dot {
205
+ width: 6px; height: 6px;
206
+ border-radius: 50%;
207
+ flex-shrink: 0;
208
+ }
209
+ .acta-facet-name {
210
+ flex: 1;
211
+ overflow: hidden;
212
+ text-overflow: ellipsis;
213
+ color: var(--fg);
214
+ }
215
+ .acta-facet-count {
216
+ color: var(--dim);
217
+ font-variant-numeric: tabular-nums;
218
+ font-size: 10px;
219
+ flex-shrink: 0;
220
+ }
221
+
222
+ /* ── Main pane ── */
223
+ .acta-main {
224
+ flex: 1;
225
+ display: flex;
226
+ flex-direction: column;
227
+ min-width: 0;
228
+ }
229
+
230
+ /* ── Active filter chips ── */
231
+ .acta-chips {
232
+ padding: 6px 16px;
233
+ border-bottom: 1px solid var(--border);
234
+ display: flex;
235
+ align-items: center;
236
+ gap: 6px;
237
+ flex-wrap: wrap;
238
+ font-family: Inter, system-ui, sans-serif;
239
+ font-size: 10px;
240
+ min-height: 32px;
241
+ flex-shrink: 0;
242
+ }
243
+ .acta-chip {
244
+ display: inline-flex;
245
+ align-items: center;
246
+ gap: 4px;
247
+ padding: 2px 4px 2px 8px;
248
+ background: var(--hover);
249
+ color: var(--dim);
250
+ border: 1px solid var(--border);
251
+ border-radius: 3px;
252
+ font-size: 10px;
253
+ }
254
+ .acta-chip-val {
255
+ font-family: 'JetBrains Mono', ui-monospace, monospace;
256
+ color: var(--fg);
257
+ }
258
+ .acta-chip-x {
259
+ background: transparent;
260
+ border: none;
261
+ color: var(--dim);
262
+ font-size: 11px;
263
+ padding: 0 4px;
264
+ text-decoration: none;
265
+ line-height: 1;
266
+ }
267
+ .acta-chip-x:hover { color: var(--fg); }
268
+ .acta-chips-count { margin-left: auto; color: var(--dim); }
269
+
270
+ /* ── Log rows ── */
271
+ .acta-log {
272
+ flex: 1;
273
+ overflow-y: auto;
274
+ overflow-x: hidden;
275
+ min-height: 0;
276
+ }
277
+ .acta-row {
278
+ display: grid;
279
+ grid-template-columns: 90px 14px 220px 1fr 140px;
280
+ gap: 10px;
281
+ padding: 5px 16px;
282
+ border-left: 2px solid transparent;
283
+ cursor: pointer;
284
+ white-space: nowrap;
285
+ align-items: center;
286
+ font-size: 11px;
287
+ }
288
+ .acta-row:hover { background: var(--hover); }
289
+ .acta-row.selected {
290
+ background: var(--hover);
291
+ border-left-color: var(--accent);
292
+ }
293
+ .acta-row-time {
294
+ color: var(--dim);
295
+ font-variant-numeric: tabular-nums;
296
+ font-size: 10.5px;
297
+ overflow: hidden;
298
+ text-overflow: ellipsis;
299
+ }
300
+ .acta-row-dot {
301
+ display: flex;
302
+ align-items: center;
303
+ }
304
+ .acta-dot {
305
+ width: 6px; height: 6px;
306
+ border-radius: 50%;
307
+ display: inline-block;
308
+ flex-shrink: 0;
309
+ }
310
+ .acta-row-type {
311
+ color: var(--fg);
312
+ overflow: hidden;
313
+ text-overflow: ellipsis;
314
+ }
315
+ .acta-row-stream {
316
+ color: var(--dim);
317
+ overflow: hidden;
318
+ text-overflow: ellipsis;
319
+ }
320
+ .acta-stream-key {
321
+ color: var(--fg);
322
+ text-decoration: none;
323
+ }
324
+ .acta-stream-key:hover {
325
+ text-decoration: underline;
326
+ text-decoration-color: var(--accent);
327
+ text-underline-offset: 2px;
328
+ }
329
+ .acta-payload-preview {
330
+ opacity: 0.6;
331
+ margin-left: 8px;
332
+ }
333
+ .acta-row-actor {
334
+ color: var(--dim);
335
+ text-align: right;
336
+ overflow: hidden;
337
+ text-overflow: ellipsis;
338
+ }
339
+
340
+ /* ── Pagination ── */
341
+ .acta-pagination {
342
+ border-top: 1px solid var(--border);
343
+ padding: 8px 16px;
344
+ display: flex;
345
+ align-items: center;
346
+ gap: 8px;
347
+ font-family: Inter, system-ui, sans-serif;
348
+ font-size: 10px;
349
+ color: var(--dim);
350
+ flex-shrink: 0;
351
+ }
352
+ .acta-pagination-btns { margin-left: auto; display: flex; gap: 4px; }
353
+ .acta-page-btn {
354
+ padding: 3px 9px;
355
+ font-size: 10px;
356
+ font-family: inherit;
357
+ background: transparent;
358
+ color: var(--fg);
359
+ border: 1px solid var(--border);
360
+ border-radius: 3px;
361
+ text-decoration: none;
362
+ display: inline-block;
363
+ line-height: 1.4;
364
+ }
365
+ .acta-page-btn:hover { background: var(--hover); }
366
+ .acta-page-btn.disabled {
367
+ opacity: 0.4;
368
+ pointer-events: none;
369
+ }
370
+
371
+ /* ── Detail panel ── */
372
+ .acta-detail {
373
+ width: 440px;
374
+ border-left: 1px solid var(--border);
375
+ background: var(--panel);
376
+ display: flex;
377
+ flex-direction: column;
378
+ flex-shrink: 0;
379
+ overflow: hidden;
380
+ }
381
+ .acta-detail-header {
382
+ padding: 10px 16px;
383
+ border-bottom: 1px solid var(--border);
384
+ display: flex;
385
+ align-items: center;
386
+ gap: 8px;
387
+ flex-shrink: 0;
388
+ }
389
+ .acta-detail-type { font-size: 12px; color: var(--fg); }
390
+ .acta-detail-version {
391
+ font-family: Inter, system-ui, sans-serif;
392
+ font-size: 10px;
393
+ color: var(--dim);
394
+ }
395
+ .acta-detail-close {
396
+ margin-left: auto;
397
+ background: transparent;
398
+ border: none;
399
+ color: var(--dim);
400
+ font-size: 16px;
401
+ line-height: 1;
402
+ padding: 0 2px;
403
+ text-decoration: none;
404
+ }
405
+ .acta-detail-close:hover { color: var(--fg); }
406
+ .acta-detail-body {
407
+ overflow-y: auto;
408
+ flex: 1;
409
+ padding: 14px 16px;
410
+ }
411
+ .acta-kv {
412
+ display: grid;
413
+ grid-template-columns: 110px 1fr;
414
+ gap: 8px;
415
+ padding: 4px 0;
416
+ border-bottom: 1px dashed var(--border);
417
+ }
418
+ .acta-kv-key {
419
+ color: var(--dim);
420
+ font-size: 10.5px;
421
+ }
422
+ .acta-kv-val {
423
+ color: var(--fg);
424
+ word-break: break-all;
425
+ font-size: 11px;
426
+ }
427
+ .acta-kv-val a {
428
+ color: var(--accent);
429
+ text-decoration: none;
430
+ }
431
+ .acta-kv-val a:hover { text-decoration: underline; }
432
+ .acta-section-label {
433
+ margin-top: 18px;
434
+ margin-bottom: 6px;
435
+ font-family: Inter, system-ui, sans-serif;
436
+ font-size: 9px;
437
+ color: var(--dim);
438
+ text-transform: uppercase;
439
+ letter-spacing: 0.8px;
440
+ }
441
+ .acta-json-block {
442
+ margin: 0;
443
+ padding: 12px;
444
+ background: #070809;
445
+ border: 1px solid var(--border);
446
+ border-radius: 4px;
447
+ font-size: 11px;
448
+ color: var(--fg);
449
+ overflow: auto;
450
+ line-height: 1.55;
451
+ white-space: pre;
452
+ font-family: inherit;
453
+ }
454
+ /* JSON syntax colours */
455
+ .jk { color: #9ec1ff; } /* key */
456
+ .js { color: #a5d6a7; } /* string value */
457
+ .jn { color: #ffcc80; } /* number */
458
+ .jb { color: #ce93d8; } /* bool */
459
+ .jz { color: #888; } /* null */
460
+
461
+ /* ── Empty states ── */
462
+ .acta-empty-filters {
463
+ padding: 60px;
464
+ text-align: center;
465
+ font-family: Inter, system-ui, sans-serif;
466
+ font-size: 12px;
467
+ color: var(--dim);
468
+ }
469
+ .acta-empty-filters strong { display: block; color: var(--fg); margin-bottom: 6px; font-weight: 500; }
470
+
471
+ .acta-empty-install {
472
+ padding: 60px 36px 80px;
473
+ max-width: 720px;
474
+ margin: 0 auto;
475
+ }
476
+ .acta-empty-badge {
477
+ display: inline-flex;
478
+ align-items: center;
479
+ gap: 8px;
480
+ padding: 3px 8px;
481
+ border: 1px solid var(--border);
482
+ border-radius: 3px;
483
+ font-family: Inter, system-ui, sans-serif;
484
+ font-size: 10px;
485
+ color: var(--dim);
486
+ text-transform: uppercase;
487
+ letter-spacing: 0.8px;
488
+ margin-bottom: 18px;
489
+ }
490
+ .acta-empty-title {
491
+ font-family: Inter, system-ui, sans-serif;
492
+ font-size: 22px;
493
+ color: var(--fg);
494
+ font-weight: 500;
495
+ letter-spacing: -0.3px;
496
+ margin-bottom: 8px;
497
+ }
498
+ .acta-empty-desc {
499
+ font-family: Inter, system-ui, sans-serif;
500
+ font-size: 13px;
501
+ color: var(--dim);
502
+ line-height: 1.6;
503
+ margin-bottom: 28px;
504
+ max-width: 560px;
505
+ }
506
+ .acta-empty-desc code { font-family: inherit; color: var(--fg); }
507
+ .acta-empty-steps { display: flex; flex-direction: column; gap: 18px; }
508
+ .acta-step-label {
509
+ font-family: Inter, system-ui, sans-serif;
510
+ font-size: 11px;
511
+ color: var(--dim);
512
+ margin-bottom: 6px;
513
+ display: flex;
514
+ align-items: center;
515
+ }
516
+ .acta-step-num {
517
+ display: inline-flex;
518
+ width: 18px; height: 18px;
519
+ align-items: center;
520
+ justify-content: center;
521
+ font-size: 10px;
522
+ color: var(--dim);
523
+ border: 1px solid var(--border);
524
+ border-radius: 9px;
525
+ margin-right: 8px;
526
+ flex-shrink: 0;
527
+ }
528
+ .acta-code-block {
529
+ margin: 0;
530
+ padding: 12px 14px;
531
+ background: var(--panel);
532
+ border: 1px solid var(--border);
533
+ border-radius: 4px;
534
+ font-size: 11.5px;
535
+ color: var(--fg);
536
+ overflow: auto;
537
+ line-height: 1.55;
538
+ white-space: pre;
539
+ font-family: inherit;
540
+ }
541
+ .acta-empty-footer {
542
+ margin-top: 36px;
543
+ padding-top: 18px;
544
+ border-top: 1px solid var(--border);
545
+ display: flex;
546
+ gap: 18px;
547
+ align-items: center;
548
+ font-family: Inter, system-ui, sans-serif;
549
+ font-size: 11px;
550
+ }
551
+ .acta-empty-link { color: var(--accent); text-decoration: none; }
552
+ .acta-empty-link:hover { text-decoration: underline; }
553
+ .acta-empty-footer-status { margin-left: auto; color: var(--dim); }
554
+ </style>
555
+ </head>
556
+ <body style="margin:0;padding:0;background:#0b0c0f;">
557
+ <div class="acta-root">
558
+ <%= yield %>
559
+ </div>
560
+ <script>
561
+ // Row click → navigate to detail panel URL (ignores clicks on links/buttons inside row)
562
+ document.querySelectorAll('[data-href]').forEach(function(el) {
563
+ el.addEventListener('click', function(e) {
564
+ if (!e.target.closest('a, button')) {
565
+ window.location.href = this.dataset.href;
566
+ }
567
+ });
568
+ });
569
+
570
+ // Auto-submit filter form on input change (300ms debounce)
571
+ var debounceTimer;
572
+ document.querySelectorAll('.acta-filter-form input[type=search], .acta-filter-form input[type=text]').forEach(function(input) {
573
+ input.addEventListener('input', function() {
574
+ clearTimeout(debounceTimer);
575
+ var form = this.form;
576
+ debounceTimer = setTimeout(function() { form.submit(); }, 300);
577
+ });
578
+ });
579
+
580
+ // Syntax-highlight all .acta-json-block elements
581
+ document.querySelectorAll('.acta-json-block').forEach(function(el) {
582
+ var text = el.textContent;
583
+ var html = text
584
+ .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
585
+ .replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*")(\s*:)/g, '<span class="jk">$1</span>$3')
586
+ .replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*")/g, '<span class="js">$1</span>')
587
+ .replace(/\b(-?\d+\.?\d*([eE][+-]?\d+)?)\b/g, '<span class="jn">$1</span>')
588
+ .replace(/\b(true|false)\b/g, '<span class="jb">$1</span>')
589
+ .replace(/\bnull\b/g, '<span class="jz">null</span>');
590
+ el.innerHTML = html;
591
+ });
592
+ </script>
593
+ </body>
594
+ </html>
data/config/routes.rb ADDED
@@ -0,0 +1,4 @@
1
+ Acta::Web::Engine.routes.draw do
2
+ resources :events, only: %i[index show]
3
+ root to: "events#index"
4
+ end
data/lib/acta/actor.rb ADDED
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Acta
4
+ class Actor
5
+ attr_reader :type, :id, :source, :metadata
6
+
7
+ def initialize(type:, id: nil, source: nil, metadata: {})
8
+ raise ArgumentError, "Acta::Actor type must be a non-empty string" if type.nil? || type.to_s.empty?
9
+
10
+ @type = type.to_s
11
+ @id = id
12
+ @source = source
13
+ @metadata = metadata
14
+ end
15
+
16
+ def to_h
17
+ { type:, id:, source:, metadata: }
18
+ end
19
+
20
+ def ==(other)
21
+ other.is_a?(self.class) && to_h == other.to_h
22
+ end
23
+ alias_method :eql?, :==
24
+
25
+ def hash
26
+ to_h.hash
27
+ end
28
+
29
+ def self.from_h(hash)
30
+ hash = hash.transform_keys(&:to_sym)
31
+ new(type: hash[:type], id: hash[:id], source: hash[:source], metadata: hash[:metadata] || {})
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Acta
4
+ module Adapters
5
+ class Base
6
+ def install_schema(connection, table_name: :events)
7
+ uuid_type = uuid_column_type
8
+ json_type = json_column_type
9
+
10
+ connection.create_table(table_name) do |t|
11
+ t.send(uuid_type, :uuid, null: false)
12
+ t.string :event_type, null: false
13
+ t.integer :event_version, null: false, default: 1
14
+
15
+ t.string :stream_type
16
+ t.string :stream_key
17
+ t.integer :stream_sequence
18
+
19
+ t.send(json_type, :payload, null: false)
20
+
21
+ t.string :actor_type
22
+ t.string :actor_id
23
+ t.string :source
24
+ t.send(json_type, :metadata)
25
+
26
+ t.datetime :occurred_at, null: false
27
+ t.datetime :recorded_at, null: false
28
+ end
29
+
30
+ connection.add_index table_name, :uuid, unique: true
31
+ connection.add_index table_name,
32
+ [ :stream_type, :stream_key, :stream_sequence ],
33
+ unique: true,
34
+ where: "stream_type IS NOT NULL",
35
+ name: "index_events_on_stream_identity"
36
+ connection.add_index table_name, :event_type
37
+ connection.add_index table_name, [ :actor_type, :actor_id ]
38
+ connection.add_index table_name, :source
39
+ connection.add_index table_name, :occurred_at
40
+ end
41
+
42
+ def uuid_column_type
43
+ :string
44
+ end
45
+
46
+ def json_column_type
47
+ :json
48
+ end
49
+
50
+ def insert_event(_attributes)
51
+ raise NotImplementedError, "#{self.class}#insert_event"
52
+ end
53
+
54
+ def fetch_records
55
+ raise NotImplementedError, "#{self.class}#fetch_records"
56
+ end
57
+ end
58
+ end
59
+ end