orfeas_lyra 0.6.0 → 0.9.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 +4 -4
- data/CHANGELOG.md +851 -0
- data/README.md +196 -987
- data/app/controllers/lyra/application_controller.rb +48 -0
- data/app/controllers/lyra/dashboard_controller.rb +0 -1
- data/app/views/lyra/dashboard/projections.html.erb +0 -4
- data/app/views/lyra/dashboard/verification.html.erb +8 -2
- data/app/workflows/es_async_mode_workflow.rb +24 -60
- data/app/workflows/es_sync_mode_workflow.rb +25 -50
- data/app/workflows/hijack_mode_workflow.rb +18 -33
- data/app/workflows/lifecycle_workflow.rb +1 -1
- data/app/workflows/monitor_mode_workflow.rb +12 -22
- data/config/privacy_policies.rb +2 -1
- data/config/routes.rb +4 -2
- data/lib/lyra/access_log.rb +121 -0
- data/lib/lyra/advisory_lock.rb +45 -0
- data/lib/lyra/aggregate.rb +25 -13
- data/lib/lyra/associations/event_aware.rb +3 -4
- data/lib/lyra/associations/habtm_links.rb +255 -0
- data/lib/lyra/bypass_events.rb +208 -0
- data/lib/lyra/command.rb +3 -0
- data/lib/lyra/command_handler.rb +164 -42
- data/lib/lyra/configuration.rb +217 -8
- data/lib/lyra/consistency/read_your_writes.rb +74 -23
- data/lib/lyra/domain_events.rb +157 -0
- data/lib/lyra/dual_view.rb +27 -6
- data/lib/lyra/dual_view_sampler.rb +33 -0
- data/lib/lyra/encrypted_attributes.rb +101 -0
- data/lib/lyra/engine.rb +67 -4
- data/lib/lyra/erasure.rb +316 -0
- data/lib/lyra/event.rb +120 -2
- data/lib/lyra/event_flow.rb +16 -6
- data/lib/lyra/event_serializer.rb +45 -0
- data/lib/lyra/event_store_adapter.rb +58 -62
- data/lib/lyra/genesis.rb +189 -0
- data/lib/lyra/id_generator.rb +42 -10
- data/lib/lyra/interceptors/association_interceptor.rb +123 -6
- data/lib/lyra/interceptors/crud_interceptor.rb +400 -74
- data/lib/lyra/interceptors/lazy_reads.rb +46 -0
- data/lib/lyra/mode_sync.rb +125 -0
- data/lib/lyra/mode_transition.rb +386 -0
- data/lib/lyra/model_selection.rb +160 -0
- data/lib/lyra/optimistic_locking.rb +88 -0
- data/lib/lyra/optional_dependency.rb +24 -0
- data/lib/lyra/own_connection.rb +54 -0
- data/lib/lyra/privacy/adapters/pam.rb +151 -0
- data/lib/lyra/privacy/gdpr_compliance.rb +51 -1
- data/lib/lyra/privacy/interface.rb +262 -0
- data/lib/lyra/privacy/pii_detector.rb +8 -10
- data/lib/lyra/privacy/pii_masker.rb +30 -13
- data/lib/lyra/privacy/policy_integration.rb +39 -148
- data/lib/lyra/projection.rb +30 -7
- data/lib/lyra/projections/association_conditions.rb +81 -0
- data/lib/lyra/projections/async_checkpoint.rb +57 -0
- data/lib/lyra/projections/async_projection_job.rb +68 -15
- data/lib/lyra/projections/cached_joins.rb +192 -0
- data/lib/lyra/projections/cached_projection.rb +279 -163
- data/lib/lyra/projections/cached_relation.rb +666 -85
- data/lib/lyra/projections/deferred_relation.rb +120 -0
- data/lib/lyra/projections/event_store_reader.rb +27 -4
- data/lib/lyra/projections/lazy_projection.rb +259 -0
- data/lib/lyra/projections/model_projection.rb +32 -34
- data/lib/lyra/projections/rebuild.rb +170 -0
- data/lib/lyra/projections/reference_links.rb +309 -0
- data/lib/lyra/purpose_bound_reads.rb +197 -0
- data/lib/lyra/repair.rb +114 -0
- data/lib/lyra/retention.rb +180 -0
- data/lib/lyra/schema/event_class_registrar.rb +2 -1
- data/lib/lyra/schema/validator.rb +14 -5
- data/lib/lyra/sti_stream_migration.rb +70 -0
- data/lib/lyra/strict_data_access.rb +225 -175
- data/lib/lyra/temporal.rb +128 -0
- data/lib/lyra/verification/bypass_workflow.rb +170 -0
- data/lib/lyra/verification/crud_lifecycle_workflow.rb +32 -6
- data/lib/lyra/verification/workflow_generator.rb +73 -179
- data/lib/lyra/version.rb +1 -1
- data/lib/lyra.rb +114 -17
- data/lib/tasks/lyra_mode.rake +89 -0
- data/lib/tasks/lyra_projections.rake +78 -0
- data/lib/tasks/lyra_schema.rake +4 -0
- data/lib/tasks/lyra_workflows.rake +28 -27
- metadata +39 -11
- data/lib/lyra/event_mapper.rb +0 -106
- data/lib/tasks/dist.rake +0 -391
- data/lib/tasks/gems.rake +0 -185
- data/lib/tasks/public_release.rake +0 -351
- data/lib/tasks/stats.rake +0 -175
- data/lib/tasks/testbed.rake +0 -479
data/README.md
CHANGED
|
@@ -4,984 +4,104 @@
|
|
|
4
4
|
|
|
5
5
|
*Part of the ORFEAS (Object-Relational to Event-Sourcing Architecture) Framework*
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
PhD Candidate, University of the Aegean
|
|
11
|
-
Department of Information and Communication Systems Engineering
|
|
12
|
-
|
|
13
|
-
---
|
|
14
|
-
|
|
15
|
-
## Table of Contents
|
|
16
|
-
|
|
17
|
-
1. [Overview](#overview)
|
|
18
|
-
2. [The Problem](#the-problem)
|
|
19
|
-
3. [The Solution: ORFEAS Framework](#the-solution-orfeas-framework)
|
|
20
|
-
4. [Key Features](#key-features)
|
|
21
|
-
5. [Architecture](#architecture)
|
|
22
|
-
6. [Components](#components)
|
|
23
|
-
7. [Installation](#installation)
|
|
24
|
-
8. [Quick Start](#quick-start)
|
|
25
|
-
9. [Usage Guide](#usage-guide)
|
|
26
|
-
10. [Example Application](#example-application)
|
|
27
|
-
11. [Benefits](#benefits)
|
|
28
|
-
12. [Documentation](#documentation)
|
|
29
|
-
13. [Research Foundation](#research-foundation)
|
|
30
|
-
14. [Development Methodology](#development-methodology)
|
|
31
|
-
15. [Contributing](#contributing)
|
|
32
|
-
16. [Citation](#citation)
|
|
33
|
-
17. [Support](#support)
|
|
34
|
-
|
|
35
|
-
---
|
|
36
|
-
|
|
37
|
-
## Overview
|
|
38
|
-
|
|
39
|
-
Lyra is a Rails engine that enables **gradual, non-intrusive transformation** of traditional CRUD (Create, Read, Update, Delete) operations into Event Sourcing architectures, with built-in privacy compliance capabilities. It is the practical implementation of the **ORFEAS (Object-Relational to Event-Sourcing Architecture)** framework, developed as part of a PhD thesis addressing the fundamental challenge of bridging 40+ years of ORM dominance with modern event-driven, GDPR-compliant application architectures.
|
|
40
|
-
|
|
41
|
-
### What Makes Lyra Unique?
|
|
42
|
-
|
|
43
|
-
- **Dual-Mode Operation**: Monitor existing applications non-intrusively, then gradually transform to full event sourcing
|
|
44
|
-
- **Formal Mathematical Foundation**: Built on Petri nets (P/T nets for verification, CPNs for advanced modeling) and matrix analysis for rigorous CRUD-to-event mapping
|
|
45
|
-
- **Privacy-First Design**: Integrated Privacy Attribute Matrix (PAM) for GDPR compliance
|
|
46
|
-
- **Zero-Downtime Migration**: Maintain CRUD as a safety net while transitioning to event sourcing
|
|
47
|
-
- **Research-Backed**: Grounded in peer-reviewed research on event sourcing patterns and privacy preservation
|
|
48
|
-
|
|
49
|
-
---
|
|
50
|
-
|
|
51
|
-
## The Problem
|
|
52
|
-
|
|
53
|
-
Modern enterprise systems face a critical architectural challenge:
|
|
54
|
-
|
|
55
|
-
### The Legacy Dilemma
|
|
56
|
-
- **40+ Years of ORM Investment**: Decades of business logic encoded in Object-Relational Mapping systems
|
|
57
|
-
- **CRUD Limitations**: State-based systems lose behavioral history and temporal context
|
|
58
|
-
- **Privacy Requirements**: GDPR mandates comprehensive data lineage and processing transparency
|
|
59
|
-
- **Migration Risk**: Complete rewrites from CRUD to Event Sourcing are expensive and risky
|
|
60
|
-
|
|
61
|
-
### The Event Sourcing Promise
|
|
62
|
-
- **Complete Audit Trails**: Every state change recorded as an immutable event
|
|
63
|
-
- **Temporal Queries**: Query system state at any point in time
|
|
64
|
-
- **Behavioral Analysis**: Understand *how* and *why* state changes occurred
|
|
65
|
-
- **Microservices Alignment**: Natural fit for distributed, event-driven architectures
|
|
66
|
-
|
|
67
|
-
### The Gap
|
|
68
|
-
**There is no formal, proven method for gradually migrating CRUD systems to event sourcing while maintaining operational safety and privacy compliance.**
|
|
69
|
-
|
|
70
|
-
---
|
|
71
|
-
|
|
72
|
-
## The Solution: ORFEAS Framework
|
|
73
|
-
|
|
74
|
-
The **ORFEAS (Object-Relational to Event-Sourcing Architecture)** framework provides:
|
|
75
|
-
|
|
76
|
-
1. **Formal Mathematical Models**: Petri nets map CRUD operations to event sequences with formal verification (P/T nets for structural proofs, CPNs optional for data modeling)
|
|
77
|
-
2. **Privacy Compliance**: Privacy Attribute Matrix (PAM) ensures GDPR compliance throughout the transformation
|
|
78
|
-
3. **Gradual Migration Path**: Two operational modes support safe, incremental transition
|
|
79
|
-
4. **Dual-View Analysis**: Compare CRUD and event-sourced views to verify correctness
|
|
80
|
-
5. **Practical Tooling**: Lyra implements ORFEAS as a production-ready Rails engine
|
|
81
|
-
|
|
82
|
-
### Theoretical Foundation
|
|
83
|
-
|
|
84
|
-
ORFEAS is built on three pillars:
|
|
85
|
-
|
|
86
|
-
1. **Petri Nets**: Model CRUD-to-event transformations (P/T nets for verification proofs; CPNs with tokens, guards, and arc expressions for advanced data modeling)
|
|
87
|
-
2. **Matrix Analysis**: Complementary linear algebra approach for causation and lineage tracking
|
|
88
|
-
3. **Privacy Attribute Matrix (PAM)**: DSL for declaring field-level privacy policies and transformations
|
|
89
|
-
|
|
90
|
-
[Read the complete theoretical foundation →](docs/ORFEAS_FRAMEWORK_OVERVIEW.md)
|
|
91
|
-
|
|
92
|
-
---
|
|
93
|
-
|
|
94
|
-
## Key Features
|
|
95
|
-
|
|
96
|
-
### Operational Features
|
|
97
|
-
- ✅ **Non-intrusive Monitoring** - Works with existing Rails applications without code changes
|
|
98
|
-
- ✅ **Dual Operational Modes** - Monitor or hijack CRUD operations
|
|
99
|
-
- ✅ **Automatic Event Mapping** - CRUD operations automatically mapped to domain events
|
|
100
|
-
- ✅ **State Reconstruction** - Rebuild state from event streams
|
|
101
|
-
- ✅ **Dual-View Dashboard** - Compare CRUD state vs event-sourced state
|
|
102
|
-
|
|
103
|
-
### Technical Features
|
|
104
|
-
- ✅ **Rails Event Store Integration** - Built on proven event sourcing infrastructure
|
|
105
|
-
- ✅ **Pluggable Event Backends** - Support for custom event storage (Kafka, EventStoreDB)
|
|
106
|
-
- ✅ **Aggregate Support** - Custom domain aggregates for complex business logic
|
|
107
|
-
- ✅ **Command/Query Separation** - CQRS-ready architecture
|
|
108
|
-
- ✅ **Event Versioning** - Support for event schema evolution
|
|
109
|
-
|
|
110
|
-
### Privacy & Compliance
|
|
111
|
-
- ✅ **Privacy Attribute Matrix (PAM)** - Built-in PAM DSL for defining privacy policies
|
|
112
|
-
- ✅ **GDPR Compliance** - Purpose-based access control, consent management, retention policies
|
|
113
|
-
- ✅ **PII Detection & Transformation** - Automatic PII field identification and transformation
|
|
114
|
-
- ✅ **Audit Trails** - Complete lineage tracking for regulatory compliance
|
|
115
|
-
|
|
116
|
-
---
|
|
117
|
-
|
|
118
|
-
## Architecture
|
|
119
|
-
|
|
120
|
-
### High-Level Architecture
|
|
121
|
-
|
|
122
|
-
```
|
|
123
|
-
┌─────────────────────────────────────────────────────────────┐
|
|
124
|
-
│ Rails Application │
|
|
125
|
-
├─────────────────────────────────────────────────────────────┤
|
|
126
|
-
│ │
|
|
127
|
-
│ ActiveRecord Models (with monitor_with_lyra) │
|
|
128
|
-
│ │ │
|
|
129
|
-
│ ▼ │
|
|
130
|
-
│ ┌──────────────────┐ │
|
|
131
|
-
│ │ CRUD Interceptor │ │
|
|
132
|
-
│ └────────┬─────────┘ │
|
|
133
|
-
│ │ │
|
|
134
|
-
│ ┌────┴────┐ │
|
|
135
|
-
│ │ │ │
|
|
136
|
-
│ MONITOR HIJACK │
|
|
137
|
-
│ MODE MODE │
|
|
138
|
-
│ │ │ │
|
|
139
|
-
│ ▼ ▼ │
|
|
140
|
-
│ ┌─────┐ ┌─────────┐ │
|
|
141
|
-
│ │ Log │ │ Command │ │
|
|
142
|
-
│ │Event│ │ Handler │ │
|
|
143
|
-
│ └──┬──┘ └────┬────┘ │
|
|
144
|
-
│ │ │ │
|
|
145
|
-
│ └─────┬─────┘ │
|
|
146
|
-
│ ▼ │
|
|
147
|
-
│ ┌─────────────────┐ │
|
|
148
|
-
│ │ Event Store │ │
|
|
149
|
-
│ │ (Rails Event │ │
|
|
150
|
-
│ │ Store) │ │
|
|
151
|
-
│ └─────────────────┘ │
|
|
152
|
-
│ │ │
|
|
153
|
-
│ ▼ │
|
|
154
|
-
│ ┌─────────────────┐ │
|
|
155
|
-
│ │ Aggregates & │ │
|
|
156
|
-
│ │ Projections │ │
|
|
157
|
-
│ └─────────────────┘ │
|
|
158
|
-
│ │
|
|
159
|
-
└─────────────────────────────────────────────────────────────┘
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
### Operational Modes
|
|
163
|
-
|
|
164
|
-
#### 1. Monitor Mode (Non-intrusive)
|
|
165
|
-
- Observes CRUD operations and logs them as domain events
|
|
166
|
-
- **No changes** to application behavior
|
|
167
|
-
- Perfect for **analysis and planning** migration
|
|
168
|
-
- Zero risk to production systems
|
|
169
|
-
|
|
170
|
-
#### 2. Hijack Mode (Transformative)
|
|
171
|
-
- Intercepts CRUD operations and routes through event sourcing
|
|
172
|
-
- Replaces traditional relational backend
|
|
173
|
-
- Maintains CRUD interface for **backward compatibility**
|
|
174
|
-
- Full event sourcing benefits
|
|
175
|
-
|
|
176
|
-
[See detailed architecture documentation →](docs/ARCHITECTURE.md)
|
|
177
|
-
|
|
178
|
-
---
|
|
179
|
-
|
|
180
|
-
## Components
|
|
181
|
-
|
|
182
|
-
### Lyra Engine (Core)
|
|
183
|
-
The main Rails engine providing:
|
|
184
|
-
- CRUD interception and monitoring
|
|
185
|
-
- Event mapping and publishing
|
|
186
|
-
- Dual-view analysis
|
|
187
|
-
- Command/Query handlers
|
|
188
|
-
- State reconstruction
|
|
189
|
-
|
|
190
|
-
### PetriFlow Gem
|
|
191
|
-
Complete Petri net and Colored Petri Net library:
|
|
192
|
-
- Core Petri net components (places, transitions, arcs)
|
|
193
|
-
- Colored extensions (guards, arc expressions, token types)
|
|
194
|
-
- Matrix analysis (CRUD-event mapping, causation, lineage)
|
|
195
|
-
- Visualization (GraphViz, Mermaid, ASCII)
|
|
196
|
-
- Formal verification (reachability, boundedness, liveness)
|
|
197
|
-
- Export functionality (PNML, CPN Tools, JSON, YAML)
|
|
198
|
-
|
|
199
|
-
[Read the PetriFlow documentation →](gems/petri_flow/README.md)
|
|
200
|
-
|
|
201
|
-
### PAM DSL Gem
|
|
202
|
-
Privacy Attribute Matrix Domain-Specific Language:
|
|
203
|
-
- Field-level PII classification with sensitivity levels
|
|
204
|
-
- Purpose-based access control aligned with GDPR
|
|
205
|
-
- Retention policies with field-level granularity
|
|
206
|
-
- Consent management with expiration tracking
|
|
207
|
-
- Data transformation for different contexts (display, logging, API)
|
|
208
|
-
|
|
209
|
-
[Read the PAM DSL documentation →](gems/pam_dsl/README.md)
|
|
210
|
-
|
|
211
|
-
### Monorepo Structure
|
|
212
|
-
|
|
213
|
-
This repository is organized as a monorepo:
|
|
214
|
-
- **`/` (root)** - Lyra Rails engine
|
|
215
|
-
- **`gems/petri_flow/`** - PetriFlow Petri net library
|
|
216
|
-
- **`gems/pam_dsl/`** - PAM DSL privacy policy language
|
|
217
|
-
- **`docs/`** - Theoretical documentation
|
|
218
|
-
- **`docs-site/`** - Jekyll documentation site
|
|
219
|
-
- **`examples/`** - Example applications
|
|
220
|
-
|
|
221
|
-
[See complete monorepo structure →](docs/MONOREPO.md)
|
|
222
|
-
|
|
223
|
-
---
|
|
224
|
-
|
|
225
|
-
## Installation
|
|
226
|
-
|
|
227
|
-
### Prerequisites
|
|
228
|
-
|
|
229
|
-
- Ruby 3.4.5+ (tested up to Ruby 4.0) and Rails 8.0+
|
|
230
|
-
- PostgreSQL 14+ (recommended for Rails Event Store)
|
|
231
|
-
- Bundler
|
|
232
|
-
|
|
233
|
-
### Add to Gemfile
|
|
234
|
-
|
|
235
|
-
```ruby
|
|
236
|
-
gem 'lyra', path: 'path/to/lyra' # or from git/rubygems when published
|
|
237
|
-
gem 'pam_dsl', '~> 0.1.0' # Privacy Attribute Matrix DSL
|
|
238
|
-
gem 'petri_flow', '~> 0.1.0' # Petri net library
|
|
239
|
-
```
|
|
240
|
-
|
|
241
|
-
### Install Dependencies
|
|
242
|
-
|
|
243
|
-
```bash
|
|
244
|
-
bundle install
|
|
245
|
-
rails generate rails_event_store_active_record:migration
|
|
246
|
-
rails db:migrate
|
|
247
|
-
```
|
|
248
|
-
|
|
249
|
-
---
|
|
250
|
-
|
|
251
|
-
## Quick Start
|
|
252
|
-
|
|
253
|
-
### 1. Configure Lyra
|
|
254
|
-
|
|
255
|
-
Create `config/initializers/lyra.rb`:
|
|
256
|
-
|
|
257
|
-
```ruby
|
|
258
|
-
Lyra.configure do |config|
|
|
259
|
-
# Start in monitor mode (non-intrusive)
|
|
260
|
-
config.mode = :monitor
|
|
261
|
-
|
|
262
|
-
# Configure event store
|
|
263
|
-
config.event_backend = :rails_event_store
|
|
264
|
-
config.event_store = RailsEventStore::Client.new
|
|
265
|
-
|
|
266
|
-
# Optional: User tracking for audit trails (see User Tracking section below)
|
|
267
|
-
# config.metadata_proc = ->(record, operation) { { user_id: Current.user&.id } }
|
|
268
|
-
end
|
|
269
|
-
```
|
|
270
|
-
|
|
271
|
-
### 2. Monitor a Model
|
|
272
|
-
|
|
273
|
-
Add monitoring to your ActiveRecord models:
|
|
274
|
-
|
|
275
|
-
```ruby
|
|
276
|
-
class Order < ApplicationRecord
|
|
277
|
-
# Enable Lyra monitoring
|
|
278
|
-
monitor_with_lyra
|
|
279
|
-
|
|
280
|
-
# Your existing code continues to work normally
|
|
281
|
-
validates :total, presence: true
|
|
282
|
-
belongs_to :customer
|
|
283
|
-
end
|
|
284
|
-
```
|
|
285
|
-
|
|
286
|
-
### 3. Observe Events
|
|
287
|
-
|
|
288
|
-
All CRUD operations are now logged as events:
|
|
289
|
-
|
|
290
|
-
```ruby
|
|
291
|
-
order = Order.create!(total: 100, customer: customer)
|
|
292
|
-
# => Publishes OrderCreated event
|
|
293
|
-
|
|
294
|
-
order.update!(total: 150)
|
|
295
|
-
# => Publishes OrderUpdated event
|
|
296
|
-
|
|
297
|
-
order.destroy!
|
|
298
|
-
# => Publishes OrderDestroyed event
|
|
299
|
-
```
|
|
300
|
-
|
|
301
|
-
### 4. Analyze State
|
|
302
|
-
|
|
303
|
-
Compare CRUD vs event-sourced views:
|
|
304
|
-
|
|
305
|
-
```ruby
|
|
306
|
-
comparison = Lyra::DualView.new(Order, order.id).compare
|
|
307
|
-
|
|
308
|
-
puts comparison[:differences]
|
|
309
|
-
# => { no_differences: true }
|
|
310
|
-
|
|
311
|
-
# View complete audit trail
|
|
312
|
-
audit = Lyra::DualView.new(Order, order.id).audit_trail
|
|
313
|
-
# => [{ timestamp: ..., operation: :created, changes: {...} }, ...]
|
|
314
|
-
```
|
|
315
|
-
|
|
316
|
-
[Continue to full usage guide →](#usage-guide)
|
|
7
|
+
Lyra is a Rails engine that records the writes an ActiveRecord application
|
|
8
|
+
makes as events (RailsEventStore), and can then move the application, one
|
|
9
|
+
checked step at a time, to treating those events as the source of truth.
|
|
317
10
|
|
|
318
11
|
---
|
|
319
12
|
|
|
320
|
-
##
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
# Configure retention
|
|
365
|
-
retention do
|
|
366
|
-
for_model 'Order' do
|
|
367
|
-
keep_for 7.years
|
|
368
|
-
on_expiry :anonymize
|
|
369
|
-
end
|
|
370
|
-
end
|
|
371
|
-
end
|
|
372
|
-
|
|
373
|
-
# Apply to model
|
|
374
|
-
class Order < ApplicationRecord
|
|
375
|
-
monitor_with_lyra privacy_policy: :order_system
|
|
376
|
-
end
|
|
377
|
-
```
|
|
378
|
-
|
|
379
|
-
[See complete PAM DSL guide →](gems/pam_dsl/docs/PAM_DSL_INTEGRATION.md)
|
|
380
|
-
|
|
381
|
-
### Schema Validation (Strict Mode)
|
|
382
|
-
|
|
383
|
-
Lyra can enforce schema consistency to prevent silent breaking changes in production:
|
|
384
|
-
|
|
385
|
-
```ruby
|
|
386
|
-
# config/initializers/lyra.rb
|
|
387
|
-
Lyra.configure do |config|
|
|
388
|
-
config.mode = :monitor
|
|
389
|
-
|
|
390
|
-
# Enable strict schema validation (recommended for production)
|
|
391
|
-
config.strict_schema = Rails.env.production?
|
|
392
|
-
|
|
393
|
-
# Custom schema storage path (optional, defaults to db/lyra_schemas/)
|
|
394
|
-
config.schema_path = Rails.root.join('db/lyra_schemas')
|
|
395
|
-
|
|
396
|
-
config.monitor_model User
|
|
397
|
-
config.monitor_model Order
|
|
398
|
-
end
|
|
399
|
-
```
|
|
400
|
-
|
|
401
|
-
#### Schema Management Rake Tasks
|
|
402
|
-
|
|
403
|
-
```bash
|
|
404
|
-
# Generate initial schema from monitored models
|
|
405
|
-
rake lyra:schema:create
|
|
406
|
-
|
|
407
|
-
# Check for schema changes without updating
|
|
408
|
-
rake lyra:schema:verify
|
|
409
|
-
|
|
410
|
-
# Create new schema version (after migrations)
|
|
411
|
-
rake lyra:schema:update
|
|
412
|
-
|
|
413
|
-
# Display model → event mappings
|
|
414
|
-
rake lyra:schema:report
|
|
415
|
-
|
|
416
|
-
# Show schema version history
|
|
417
|
-
rake lyra:schema:history
|
|
418
|
-
|
|
419
|
-
# Compare two schema versions
|
|
420
|
-
rake lyra:schema:diff[1,2]
|
|
421
|
-
```
|
|
422
|
-
|
|
423
|
-
#### Schema Validation Workflow
|
|
424
|
-
|
|
425
|
-
1. **Development**: Run `rake lyra:schema:create` to generate initial schema
|
|
426
|
-
2. **After migrations**: Run `rake lyra:schema:verify` to detect changes
|
|
427
|
-
3. **If changes detected**: Run `rake lyra:schema:update` to create new version
|
|
428
|
-
4. **Production**: With `strict_schema = true`, app fails to start if schema changes
|
|
429
|
-
|
|
430
|
-
#### Schema Change Severity Levels
|
|
431
|
-
|
|
432
|
-
| Severity | Changes | Impact |
|
|
433
|
-
|----------|---------|--------|
|
|
434
|
-
| **BREAKING** | model_removed, column_removed, column_type_changed | Requires new schema version |
|
|
435
|
-
| **WARNING** | column_nullable_changed, pii_field_added | Review recommended |
|
|
436
|
-
| **INFO** | model_added, column_added | Documentation update |
|
|
437
|
-
|
|
438
|
-
### User Tracking (metadata_proc)
|
|
439
|
-
|
|
440
|
-
Lyra can capture custom metadata with each event, such as the current user, for audit trails and GDPR compliance.
|
|
441
|
-
|
|
442
|
-
#### Configuration
|
|
443
|
-
|
|
444
|
-
```ruby
|
|
445
|
-
# config/initializers/lyra.rb
|
|
446
|
-
Lyra.configure do |config|
|
|
447
|
-
config.mode = :monitor
|
|
448
|
-
config.event_backend = :rails_event_store
|
|
449
|
-
|
|
450
|
-
# Custom metadata proc - called for every event
|
|
451
|
-
# Signature: ->(record, operation) { Hash }
|
|
452
|
-
config.metadata_proc = lambda do |record, operation|
|
|
453
|
-
{
|
|
454
|
-
user_id: Current.user&.id,
|
|
455
|
-
user_email: Current.user&.email,
|
|
456
|
-
ip_address: Current.request&.remote_ip,
|
|
457
|
-
source: 'my_app'
|
|
458
|
-
}
|
|
459
|
-
end
|
|
460
|
-
end
|
|
461
|
-
```
|
|
462
|
-
|
|
463
|
-
#### Using with Rails CurrentAttributes
|
|
464
|
-
|
|
465
|
-
Rails 5.2+ provides `CurrentAttributes` for request-scoped state:
|
|
466
|
-
|
|
467
|
-
```ruby
|
|
468
|
-
# app/models/current.rb
|
|
469
|
-
class Current < ActiveSupport::CurrentAttributes
|
|
470
|
-
attribute :user, :request_id, :request
|
|
471
|
-
end
|
|
472
|
-
|
|
473
|
-
# app/controllers/application_controller.rb
|
|
474
|
-
class ApplicationController < ActionController::Base
|
|
475
|
-
before_action :set_current_attributes
|
|
476
|
-
|
|
477
|
-
private
|
|
478
|
-
|
|
479
|
-
def set_current_attributes
|
|
480
|
-
Current.user = current_user
|
|
481
|
-
Current.request_id = request.request_id
|
|
482
|
-
Current.request = request
|
|
483
|
-
end
|
|
484
|
-
end
|
|
485
|
-
```
|
|
486
|
-
|
|
487
|
-
#### Using with Devise/Warden
|
|
488
|
-
|
|
489
|
-
For Devise-based authentication:
|
|
490
|
-
|
|
491
|
-
```ruby
|
|
492
|
-
config.metadata_proc = lambda do |record, operation|
|
|
493
|
-
user = if defined?(Warden) && Thread.current[:request_env]
|
|
494
|
-
Warden::Proxy.new(Thread.current[:request_env], Warden::Manager.new(nil)).user
|
|
495
|
-
end
|
|
496
|
-
|
|
497
|
-
{ user_id: user&.id, user_email: user&.email }
|
|
498
|
-
end
|
|
499
|
-
```
|
|
500
|
-
|
|
501
|
-
#### Using with Rails Engines (Solidus, Spree, etc.)
|
|
502
|
-
|
|
503
|
-
Many Rails Engines provide their own `Current` class:
|
|
504
|
-
|
|
505
|
-
```ruby
|
|
506
|
-
config.metadata_proc = lambda do |record, operation|
|
|
507
|
-
user_id = nil
|
|
508
|
-
|
|
509
|
-
# Try standard Current
|
|
510
|
-
user_id ||= Current.user&.id if defined?(Current)
|
|
511
|
-
|
|
512
|
-
# Try Spree/Solidus Current
|
|
513
|
-
user_id ||= Spree::Current.user&.id if defined?(Spree::Current)
|
|
514
|
-
|
|
515
|
-
{ user_id: user_id }
|
|
516
|
-
end
|
|
517
|
-
```
|
|
518
|
-
|
|
519
|
-
#### Metadata in Events
|
|
520
|
-
|
|
521
|
-
The custom metadata is merged with Lyra's built-in metadata:
|
|
522
|
-
|
|
523
|
-
```ruby
|
|
524
|
-
event = Lyra.event_store.read.last
|
|
525
|
-
event.metadata
|
|
526
|
-
# => {
|
|
527
|
-
# user_id: 123,
|
|
528
|
-
# user_email: "admin@example.com",
|
|
529
|
-
# ip_address: "127.0.0.1",
|
|
530
|
-
# source: "my_app",
|
|
531
|
-
# request_id: "abc-123", # Built-in from Current
|
|
532
|
-
# correlation_id: "corr-456", # Built-in from Correlation context
|
|
533
|
-
# causation_id: "cause-789" # Built-in from Causation context
|
|
534
|
-
# }
|
|
535
|
-
```
|
|
536
|
-
|
|
537
|
-
### Advanced Configuration
|
|
538
|
-
|
|
539
|
-
Use custom event names and aggregates:
|
|
540
|
-
|
|
541
|
-
```ruby
|
|
542
|
-
class Order < ApplicationRecord
|
|
543
|
-
monitor_with_lyra(
|
|
544
|
-
event_prefix: 'Order',
|
|
545
|
-
aggregate_class: OrderAggregate
|
|
546
|
-
)
|
|
547
|
-
end
|
|
548
|
-
|
|
549
|
-
class OrderAggregate < Lyra::Aggregate
|
|
550
|
-
def total
|
|
551
|
-
get_state(:total)
|
|
552
|
-
end
|
|
553
|
-
|
|
554
|
-
private
|
|
555
|
-
|
|
556
|
-
def apply_order_created(event)
|
|
557
|
-
@id = event.model_id
|
|
558
|
-
set_state(:total, event.attributes['total'])
|
|
559
|
-
set_state(:status, 'pending')
|
|
560
|
-
end
|
|
561
|
-
|
|
562
|
-
def apply_order_updated(event)
|
|
563
|
-
event.changes.each { |k, (old, new)| set_state(k.to_sym, new) }
|
|
564
|
-
end
|
|
565
|
-
end
|
|
566
|
-
```
|
|
567
|
-
|
|
568
|
-
### Event Capture Behavior
|
|
569
|
-
|
|
570
|
-
Lyra uses ActiveRecord `after_commit` callbacks to capture events. This means **all** operations that go through ActiveRecord are captured, regardless of source:
|
|
571
|
-
|
|
572
|
-
| Source | Captured |
|
|
573
|
-
|--------|----------|
|
|
574
|
-
| Web requests | ✓ |
|
|
575
|
-
| Rails console (`rails c`) | ✓ |
|
|
576
|
-
| Background jobs (Sidekiq, etc.) | ✓ |
|
|
577
|
-
| Rake tasks | ✓ |
|
|
578
|
-
| Rails runner scripts | ✓ |
|
|
579
|
-
| Seeds (`db:seed`) | ✓ |
|
|
580
|
-
|
|
581
|
-
**Operations NOT captured** (bypass ActiveRecord callbacks):
|
|
582
|
-
|
|
583
|
-
| Operation | Captured | Alternative |
|
|
584
|
-
|-----------|----------|-------------|
|
|
585
|
-
| `update_columns` / `update_column` | ✗ | Use `update!` |
|
|
586
|
-
| `update_all` / `delete_all` | ✗ | Iterate with `find_each` |
|
|
587
|
-
| `insert_all` / `upsert_all` | ✗ | Use `create!` |
|
|
588
|
-
| Raw SQL (`connection.execute`) | ✗ | Use ActiveRecord methods |
|
|
589
|
-
| `touch` with no callbacks | ✗ | Use `update!` |
|
|
590
|
-
|
|
591
|
-
### Strict Data Access Mode
|
|
592
|
-
|
|
593
|
-
To prevent inconsistencies between CRUD state and event store, enable strict mode to raise errors on callback-bypassing operations:
|
|
594
|
-
|
|
595
|
-
```ruby
|
|
596
|
-
# config/initializers/lyra.rb
|
|
597
|
-
Lyra.configure do |config|
|
|
598
|
-
config.strict_data_access = true # Raises on update_columns, etc.
|
|
599
|
-
# Or only in specific environments:
|
|
600
|
-
config.strict_data_access = Rails.env.development? || Rails.env.test?
|
|
601
|
-
end
|
|
602
|
-
```
|
|
603
|
-
|
|
604
|
-
When enabled, monitored models will raise `Lyra::StrictDataAccessViolation` if you attempt:
|
|
605
|
-
|
|
606
|
-
| Operation | Alternative | Scope |
|
|
607
|
-
|-----------|-------------|-------|
|
|
608
|
-
| `update_columns` | `update!` | Instance |
|
|
609
|
-
| `update_column` | `update!` | Instance |
|
|
610
|
-
| `delete` | `destroy` | Instance |
|
|
611
|
-
| `update_all` | `find_each { \|r\| r.update!(...) }` | Relation |
|
|
612
|
-
| `delete_all` | `find_each(&:destroy)` | Relation |
|
|
613
|
-
| `insert_all` | `records.each { \|attrs\| create!(attrs) }` | Class |
|
|
614
|
-
| `upsert_all` | `find_or_create_by!(...).update!(...)` | Class |
|
|
615
|
-
|
|
616
|
-
```ruby
|
|
617
|
-
user.update_columns(name: "New")
|
|
618
|
-
# => raises Lyra::StrictDataAccessViolation:
|
|
619
|
-
# "update_columns bypasses callbacks and won't be captured by Lyra.
|
|
620
|
-
# Use update! instead. Disable strict_data_access mode if this is intentional."
|
|
621
|
-
|
|
622
|
-
Registration.where(status: "pending").update_all(status: "expired")
|
|
623
|
-
# => raises Lyra::StrictDataAccessViolation:
|
|
624
|
-
# "update_all bypasses callbacks. Use find_each { |r| r.update!(...) } instead."
|
|
625
|
-
```
|
|
626
|
-
|
|
627
|
-
#### Bypassing Strict Mode
|
|
628
|
-
|
|
629
|
-
For legitimate bulk operations in migrations, seeds, or admin tasks, use `Lyra.without_strict_access`:
|
|
630
|
-
|
|
631
|
-
```ruby
|
|
632
|
-
# Temporarily bypass strict mode for bulk operations
|
|
633
|
-
Lyra.without_strict_access do
|
|
634
|
-
User.where(active: false).delete_all # No error raised
|
|
635
|
-
end
|
|
636
|
-
```
|
|
637
|
-
|
|
638
|
-
**Note:** Rails association operations (like `dependent: :nullify`) are automatically allowed since they're internal framework operations.
|
|
639
|
-
|
|
640
|
-
### Dual View Analysis
|
|
641
|
-
|
|
642
|
-
Compare CRUD state with event-sourced state:
|
|
643
|
-
|
|
644
|
-
```ruby
|
|
645
|
-
# Single record comparison
|
|
646
|
-
comparison = Lyra::DualView.new(Order, order_id).compare
|
|
647
|
-
|
|
648
|
-
puts comparison[:crud_view]
|
|
649
|
-
# => { exists: true, attributes: {...} }
|
|
650
|
-
|
|
651
|
-
puts comparison[:event_sourced_view]
|
|
652
|
-
# => { exists: true, state: {...}, events_count: 5 }
|
|
653
|
-
|
|
654
|
-
puts comparison[:differences]
|
|
655
|
-
# => { no_differences: true } or differences hash
|
|
656
|
-
|
|
657
|
-
# Audit trail
|
|
658
|
-
audit = Lyra::DualView.new(Order, order_id).audit_trail
|
|
659
|
-
# => Array of all operations with timestamps and changes
|
|
660
|
-
```
|
|
661
|
-
|
|
662
|
-
### Batch Analysis
|
|
663
|
-
|
|
664
|
-
Find discrepancies across all records:
|
|
665
|
-
|
|
666
|
-
```ruby
|
|
667
|
-
discrepancies = Lyra::DualView.find_discrepancies(Order)
|
|
668
|
-
# => Returns all orders where CRUD state != Event-sourced state
|
|
669
|
-
|
|
670
|
-
analysis = Lyra::StateAnalyzer.analyze(Order, order_id)
|
|
671
|
-
puts analysis[:recommendations]
|
|
672
|
-
# => Actionable recommendations based on state comparison
|
|
673
|
-
```
|
|
674
|
-
|
|
675
|
-
### Switching to Hijack Mode
|
|
676
|
-
|
|
677
|
-
After analyzing in monitor mode, switch to full event sourcing:
|
|
678
|
-
|
|
679
|
-
```ruby
|
|
680
|
-
# In config/initializers/lyra.rb
|
|
681
|
-
Lyra.configure do |config|
|
|
682
|
-
config.enable_hijack!
|
|
683
|
-
end
|
|
684
|
-
```
|
|
685
|
-
|
|
686
|
-
Now CRUD operations are intercepted and routed through event sourcing:
|
|
687
|
-
|
|
688
|
-
```ruby
|
|
689
|
-
order = Order.create!(total: 100, customer: customer)
|
|
690
|
-
# => CreateCommand processed
|
|
691
|
-
# => OrderCreated event published
|
|
692
|
-
# => Aggregate updated
|
|
693
|
-
# => Database record created with event-sourced ID
|
|
694
|
-
```
|
|
695
|
-
|
|
696
|
-
### Dashboard
|
|
697
|
-
|
|
698
|
-
Lyra provides a web dashboard for monitoring and analyzing event-sourced data.
|
|
699
|
-
|
|
700
|
-
#### Mounting the Dashboard
|
|
701
|
-
|
|
702
|
-
Add to your `config/routes.rb`:
|
|
703
|
-
|
|
704
|
-
```ruby
|
|
705
|
-
Rails.application.routes.draw do
|
|
706
|
-
mount Lyra::Engine, at: "/lyra"
|
|
707
|
-
# ... rest of your routes
|
|
708
|
-
end
|
|
709
|
-
```
|
|
710
|
-
|
|
711
|
-
#### Available Routes
|
|
712
|
-
|
|
713
|
-
| Route | Description |
|
|
714
|
-
|-------|-------------|
|
|
715
|
-
| `/lyra/dashboard` | Main dashboard with monitored models overview |
|
|
716
|
-
| `/lyra/dashboard/model/:class` | Model-specific overview (e.g., `/lyra/dashboard/model/Order`) |
|
|
717
|
-
| `/lyra/dashboard/compare/:class/:id` | Dual View - Compare CRUD state vs Event-sourced state |
|
|
718
|
-
| `/lyra/dashboard/discrepancies/:class` | List records with state discrepancies |
|
|
719
|
-
|
|
720
|
-
**Event Flow Routes:**
|
|
721
|
-
|
|
722
|
-
| Route | Description |
|
|
723
|
-
|-------|-------------|
|
|
724
|
-
| `/lyra/flow/timeline` | Global event timeline |
|
|
725
|
-
| `/lyra/flow/event_chain/:class/:id` | Event chain for a specific record |
|
|
726
|
-
| `/lyra/flow/crud_mapping` | CRUD operation to event type mapping |
|
|
727
|
-
| `/lyra/flow/correlation/:correlation_id` | Events by correlation ID |
|
|
728
|
-
| `/lyra/flow/user_actions/:user_id` | Events by user |
|
|
729
|
-
|
|
730
|
-
**Visualization Routes:**
|
|
731
|
-
|
|
732
|
-
| Route | Description |
|
|
733
|
-
|-------|-------------|
|
|
734
|
-
| `/lyra/visualizations/event_graph` | Interactive event graph (HTML + Mermaid) |
|
|
735
|
-
| `/lyra/visualizations/event_graph.json` | Event graph data with filters |
|
|
736
|
-
| `/lyra/visualizations/entity_graph/:class/:id.json` | Entity lifecycle graph |
|
|
737
|
-
| `/lyra/visualizations/heatmap` | Activity heatmap view |
|
|
738
|
-
| `/lyra/visualizations/heatmap.json` | Heatmap data for time period |
|
|
739
|
-
| `/lyra/visualizations/event_list.json` | List of entities with event counts |
|
|
740
|
-
|
|
741
|
-
**Event Graph Features:**
|
|
742
|
-
|
|
743
|
-
The event graph provides an interactive visualization of entity lifecycles:
|
|
744
|
-
|
|
745
|
-
- **Entity Picker**: Filter by model class, select specific entities to view
|
|
746
|
-
- **Changed Fields**: Each event node displays which fields were modified (📝 prefix)
|
|
747
|
-
- **Link Types**:
|
|
748
|
-
- Solid lines → Same entity lifecycle (chronological order)
|
|
749
|
-
- Dashed lines → Correlated events (same transaction/request)
|
|
750
|
-
- **Node Details**: Includes operation type, timestamp, and changed field names
|
|
751
|
-
|
|
752
|
-
Example node display:
|
|
753
|
-
```
|
|
754
|
-
┌─────────────────────┐
|
|
755
|
-
│ UPDATED │
|
|
756
|
-
│ 14:30:45 │
|
|
757
|
-
│ 📝 status, amount │
|
|
758
|
-
└─────────────────────┘
|
|
759
|
-
```
|
|
760
|
-
|
|
761
|
-
Timestamp fields (`*_at`) are automatically excluded from the changed fields display.
|
|
762
|
-
|
|
763
|
-
**Schema Management Routes:**
|
|
764
|
-
|
|
765
|
-
| Route | Description |
|
|
766
|
-
|-------|-------------|
|
|
767
|
-
| `/lyra/dashboard/schema` | Current schema with pending changes |
|
|
768
|
-
| `/lyra/dashboard/schema/history` | Schema version history |
|
|
769
|
-
| `/lyra/dashboard/schema/:version` | View specific schema version |
|
|
770
|
-
|
|
771
|
-
**Formal Verification Routes (requires PetriFlow):**
|
|
772
|
-
|
|
773
|
-
| Route | Description |
|
|
774
|
-
|-------|-------------|
|
|
775
|
-
| `/lyra/verification` | Verification dashboard |
|
|
776
|
-
| `/lyra/verification.json` | Verification results (JSON) |
|
|
777
|
-
|
|
778
|
-
**Privacy & GDPR Routes:**
|
|
779
|
-
|
|
780
|
-
| Route | Description |
|
|
781
|
-
|-------|-------------|
|
|
782
|
-
| `/lyra/privacy/pii_detection` | Automatic PII field detection |
|
|
783
|
-
| `/lyra/privacy/gdpr_report/:type/:id` | GDPR Article 15 compliant report |
|
|
784
|
-
| `/lyra/privacy/subject/:type/:id` | View all data for a subject |
|
|
785
|
-
|
|
786
|
-
#### Securing Dashboard Access
|
|
787
|
-
|
|
788
|
-
**IMPORTANT**: The Lyra dashboard exposes sensitive data including PII fields, event history, and audit trails. Always restrict access in production.
|
|
789
|
-
|
|
790
|
-
##### Option 1: Authentication Constraint (Recommended)
|
|
791
|
-
|
|
792
|
-
```ruby
|
|
793
|
-
# config/routes.rb
|
|
794
|
-
Rails.application.routes.draw do
|
|
795
|
-
# Restrict to authenticated admin users
|
|
796
|
-
authenticate :user, ->(u) { u.admin? } do
|
|
797
|
-
mount Lyra::Engine, at: "/lyra"
|
|
798
|
-
end
|
|
799
|
-
end
|
|
800
|
-
```
|
|
801
|
-
|
|
802
|
-
##### Option 2: Basic HTTP Authentication
|
|
803
|
-
|
|
804
|
-
```ruby
|
|
805
|
-
# config/routes.rb
|
|
806
|
-
Rails.application.routes.draw do
|
|
807
|
-
mount Lyra::Engine, at: "/lyra", constraints: ->(req) {
|
|
808
|
-
Rack::Auth::Basic::Request.new(req.env).provided? &&
|
|
809
|
-
Rack::Auth::Basic::Request.new(req.env).credentials ==
|
|
810
|
-
[ENV['LYRA_USER'], ENV['LYRA_PASSWORD']]
|
|
811
|
-
}
|
|
812
|
-
end
|
|
813
|
-
```
|
|
814
|
-
|
|
815
|
-
##### Option 3: IP Whitelist
|
|
816
|
-
|
|
817
|
-
```ruby
|
|
818
|
-
# config/routes.rb
|
|
819
|
-
Rails.application.routes.draw do
|
|
820
|
-
constraints ->(req) { ['127.0.0.1', '::1'].include?(req.remote_ip) } do
|
|
821
|
-
mount Lyra::Engine, at: "/lyra"
|
|
822
|
-
end
|
|
823
|
-
end
|
|
824
|
-
```
|
|
825
|
-
|
|
826
|
-
##### Option 4: Custom Middleware
|
|
827
|
-
|
|
828
|
-
```ruby
|
|
829
|
-
# lib/lyra_auth_middleware.rb
|
|
830
|
-
class LyraAuthMiddleware
|
|
831
|
-
def initialize(app)
|
|
832
|
-
@app = app
|
|
833
|
-
end
|
|
834
|
-
|
|
835
|
-
def call(env)
|
|
836
|
-
if env['PATH_INFO'].start_with?('/lyra')
|
|
837
|
-
# Your authentication logic here
|
|
838
|
-
return [403, {}, ['Forbidden']] unless authorized?(env)
|
|
839
|
-
end
|
|
840
|
-
@app.call(env)
|
|
841
|
-
end
|
|
842
|
-
|
|
843
|
-
private
|
|
844
|
-
|
|
845
|
-
def authorized?(env)
|
|
846
|
-
# Implement your authorization logic
|
|
847
|
-
env['warden']&.user&.admin?
|
|
848
|
-
end
|
|
849
|
-
end
|
|
850
|
-
|
|
851
|
-
# config/application.rb
|
|
852
|
-
config.middleware.use LyraAuthMiddleware
|
|
853
|
-
```
|
|
854
|
-
|
|
855
|
-
##### Option 5: Disable in Production
|
|
856
|
-
|
|
857
|
-
```ruby
|
|
858
|
-
# config/routes.rb
|
|
859
|
-
Rails.application.routes.draw do
|
|
860
|
-
unless Rails.env.production?
|
|
861
|
-
mount Lyra::Engine, at: "/lyra"
|
|
862
|
-
end
|
|
863
|
-
end
|
|
864
|
-
```
|
|
865
|
-
|
|
866
|
-
#### Environment-Based Configuration
|
|
867
|
-
|
|
868
|
-
```ruby
|
|
869
|
-
# config/routes.rb
|
|
870
|
-
Rails.application.routes.draw do
|
|
871
|
-
case Rails.env
|
|
872
|
-
when 'development'
|
|
873
|
-
# Open access in development
|
|
874
|
-
mount Lyra::Engine, at: "/lyra"
|
|
875
|
-
when 'staging'
|
|
876
|
-
# Basic auth in staging
|
|
877
|
-
mount Lyra::Engine, at: "/lyra", constraints: LyraBasicAuth
|
|
878
|
-
when 'production'
|
|
879
|
-
# Full authentication in production
|
|
880
|
-
authenticate :user, ->(u) { u.admin? } do
|
|
881
|
-
mount Lyra::Engine, at: "/lyra"
|
|
882
|
-
end
|
|
883
|
-
end
|
|
884
|
-
end
|
|
885
|
-
```
|
|
886
|
-
|
|
887
|
-
### Custom Event Mappers
|
|
888
|
-
|
|
889
|
-
Create custom event mapping logic:
|
|
890
|
-
|
|
891
|
-
```ruby
|
|
892
|
-
class OrderEventMapper < Lyra::EventMapper
|
|
893
|
-
def event_data
|
|
894
|
-
super.merge(
|
|
895
|
-
business_context: {
|
|
896
|
-
total: data[:attributes]['total'],
|
|
897
|
-
items_count: data[:attributes]['items_count']
|
|
898
|
-
}
|
|
899
|
-
)
|
|
900
|
-
end
|
|
901
|
-
end
|
|
902
|
-
|
|
903
|
-
Lyra::EventMapper.register_mapper(Order, OrderEventMapper)
|
|
904
|
-
```
|
|
905
|
-
|
|
906
|
-
### State Reconstruction
|
|
907
|
-
|
|
908
|
-
Rebuild current state from events:
|
|
909
|
-
|
|
910
|
-
```ruby
|
|
911
|
-
# Using projection
|
|
912
|
-
state = Lyra::StateProjection.rebuild_state(Order, order_id)
|
|
913
|
-
# => { total: 150, status: "confirmed", ... }
|
|
914
|
-
|
|
915
|
-
# Using aggregate
|
|
916
|
-
aggregate = OrderAggregate.load(order_id)
|
|
917
|
-
aggregate.total # => 150
|
|
918
|
-
aggregate.status # => "confirmed"
|
|
919
|
-
```
|
|
920
|
-
|
|
921
|
-
### Pluggable Backends
|
|
922
|
-
|
|
923
|
-
Implement custom event storage:
|
|
13
|
+
## Modes
|
|
14
|
+
|
|
15
|
+
The mode is application-wide. Per model you choose only whether it is
|
|
16
|
+
monitored (`monitor_with_lyra` in the model, or `config.models=` in the
|
|
17
|
+
initializer). Event sourcing takes one of four projection modes, which gives
|
|
18
|
+
seven configurations:
|
|
19
|
+
|
|
20
|
+
| Configuration | `mode` | `projection_mode` | Authoritative store |
|
|
21
|
+
|---|---|---|---|
|
|
22
|
+
| Disabled | `:disabled` | – | tables |
|
|
23
|
+
| Monitor | `:monitor` (default) | – | tables |
|
|
24
|
+
| Hijack | `:hijack` | – | events |
|
|
25
|
+
| ES-Sync | `:event_sourcing` | `:sync` (default) | events |
|
|
26
|
+
| ES-Async | `:event_sourcing` | `:async` | events |
|
|
27
|
+
| ES-NoProj | `:event_sourcing` | `:disabled` | events |
|
|
28
|
+
| ES-Lazy | `:event_sourcing` | `:lazy` | events |
|
|
29
|
+
|
|
30
|
+
- **Monitor** appends the event in `after_create`/`after_update`/`after_destroy`,
|
|
31
|
+
inside the write's transaction (not `after_commit`).
|
|
32
|
+
- **Hijack and event sourcing** build the event in hooks prepended to
|
|
33
|
+
ActiveRecord's persistence, after the model's `before_*` callbacks.
|
|
34
|
+
- **Callback-bypassing writes** (`update_columns`, `update_column`, `touch`,
|
|
35
|
+
`delete`, `update_all`, `delete_all`, `insert_all`, `insert_all!`, `upsert_all`,
|
|
36
|
+
and the updates of `dependent: :nullify`) on a monitored model publish bypass
|
|
37
|
+
events; with `config.strict_data_access = true` they raise instead (except
|
|
38
|
+
`touch` and the `dependent: :nullify` updates).
|
|
39
|
+
- Each record has its own stream, named `"Model$id"` (for example `"Order$42"`).
|
|
40
|
+
- **ES-NoProj** writes no rows; it answers a query exactly from the events or
|
|
41
|
+
raises `Lyra::Projections::UnsupportedQuery`.
|
|
42
|
+
- **Switching modes after boot is gated**: `Lyra::ModeTransition.to!` (and
|
|
43
|
+
`config.enable_hijack!`, `enable_monitor!`, `enable_event_sourcing!`) checks
|
|
44
|
+
that rows and events agree before a switch that changes the authoritative
|
|
45
|
+
store, and the engine refuses to boot into such a switch without a
|
|
46
|
+
certificate from `rake lyra:mode:check`. See
|
|
47
|
+
[Switching Modes](docs/MIGRATION_GUIDE.md#switching-modes).
|
|
48
|
+
|
|
49
|
+
## Requirements and installation
|
|
50
|
+
|
|
51
|
+
- Ruby >= 4.0, Rails >= 8.0, `rails_event_store` ~> 3.0 (from `lyra.gemspec`).
|
|
52
|
+
- PostgreSQL. Lyra depends on the `pg` gem; the advisory locks behind Genesis
|
|
53
|
+
and ES-Lazy, and id reservation in Hijack mode, need PostgreSQL.
|
|
54
|
+
|
|
55
|
+
The gem names differ from the files to require:
|
|
924
56
|
|
|
925
57
|
```ruby
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
58
|
+
# Gemfile
|
|
59
|
+
gem "orfeas_lyra", path: "path/to/lyra", require: "lyra"
|
|
60
|
+
gem "orfeas_pam_dsl", path: "path/to/lyra/gems/pam_dsl", require: "pam_dsl" # optional, privacy features
|
|
61
|
+
gem "orfeas_petri_flow", path: "path/to/lyra/gems/petri_flow", require: "petri_flow" # optional, verification
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Create the event store tables with the RailsEventStore 3 generator
|
|
65
|
+
(`bin/rails generate ruby_event_store:active_record:migration`), then add an
|
|
66
|
+
initializer. The [Migration Guide](docs/MIGRATION_GUIDE.md) walks through the
|
|
67
|
+
whole path from Disabled to event sourcing.
|
|
68
|
+
|
|
69
|
+
## Features
|
|
70
|
+
|
|
71
|
+
Beyond the modes above (see the [API Reference](docs/API_REFERENCE.md) for each):
|
|
72
|
+
|
|
73
|
+
- **Genesis**: rows that predate Lyra get an `Imported` event on first use, or
|
|
74
|
+
ahead of time with `rake lyra:genesis`.
|
|
75
|
+
- **Failure policies**: Hijack and event sourcing are fail-closed (the write
|
|
76
|
+
rolls back); Monitor logs and continues, and `rake lyra:repair` brings the
|
|
77
|
+
event log back in line with the tables.
|
|
78
|
+
- **DualView**: compare a record's table row with its event-sourced state;
|
|
79
|
+
optionally sampled after commit (`config.dual_view_sample_rate`, off by default).
|
|
80
|
+
- **Point-in-time state**: `Lyra.state_at(Model, id, time)` and `Model.as_of(time)`.
|
|
81
|
+
- **Privacy** (all opt-in; most need the pam_dsl gem): purpose-bound reads,
|
|
82
|
+
an access log, privacy stamps in event metadata, erasure of one record's
|
|
83
|
+
personal data from its row and its events (`rake lyra:erase`), and retention
|
|
84
|
+
rules (`rake lyra:retention:apply`).
|
|
85
|
+
- **Formal verification** (needs petri_flow): Petri net models of each mode's
|
|
86
|
+
write path, checked with PetriFlow (`rake lyra:workflows:verify`), and a
|
|
87
|
+
check of the CRUD-to-event mapping (`Lyra.verify_mapping!`).
|
|
88
|
+
- **Dashboard**: `mount Lyra::Engine, at: "/lyra"`. Outside development and
|
|
89
|
+
test it answers 403 until `config.dashboard_authorization` (a proc run on the
|
|
90
|
+
controller, e.g. `->(controller) { controller.current_user&.admin? }`) says
|
|
91
|
+
who may use it.
|
|
941
92
|
|
|
942
93
|
---
|
|
943
94
|
|
|
944
|
-
## Example
|
|
945
|
-
|
|
946
|
-
### Aegean E-Pay Testbed (Comprehensive)
|
|
947
|
-
|
|
948
|
-
A full-featured university payment system testbed is included in `examples/aegean_epay_testbed/`. This real-world example demonstrates:
|
|
949
|
-
|
|
950
|
-
- **CRUD Operations** - Registrations, payments, refunds with automatic event generation
|
|
951
|
-
- **Dual-View Comparison** - Compare CRUD state vs event-sourced state
|
|
952
|
-
- **Privacy Policy Enforcement** - PAM DSL integration for GDPR compliance
|
|
953
|
-
- **Audit Trail Generation** - Complete history from immutable events
|
|
954
|
-
- **State Machine Workflows** - Refund request lifecycle with events
|
|
955
|
-
- **Performance Benchmarking** - Compare overhead with/without Lyra
|
|
956
|
-
|
|
957
|
-
#### Running the Testbed
|
|
958
|
-
|
|
959
|
-
```bash
|
|
960
|
-
cd examples/aegean_epay_testbed
|
|
961
|
-
bundle install
|
|
962
|
-
rails db:create db:migrate db:seed
|
|
963
|
-
|
|
964
|
-
# Run integration tests
|
|
965
|
-
ruby test_lyra_integration.rb
|
|
966
|
-
|
|
967
|
-
# Or with performance benchmarks
|
|
968
|
-
ruby test_lyra_integration.rb --performance
|
|
969
|
-
```
|
|
970
|
-
|
|
971
|
-
[Explore the Aegean E-Pay testbed →](examples/aegean_epay_testbed/README.md)
|
|
972
|
-
|
|
973
|
-
### Blog App (Getting Started)
|
|
974
|
-
|
|
975
|
-
A simpler example for learning Lyra basics is in `examples/blog_app/`:
|
|
976
|
-
|
|
977
|
-
```bash
|
|
978
|
-
cd examples/blog_app
|
|
979
|
-
bundle install
|
|
980
|
-
rails db:create db:migrate db:seed
|
|
981
|
-
rails console
|
|
982
|
-
```
|
|
95
|
+
## Example application
|
|
983
96
|
|
|
984
|
-
|
|
97
|
+
`examples/blog_app/` is a small blog (users, posts, comments) in Monitor, on
|
|
98
|
+
Rails 8.1, RailsEventStore 3 and the repository's PostgreSQL
|
|
99
|
+
(`docker compose up -d db`, then `bin/setup` and `bin/rails test` in the app);
|
|
100
|
+
see its [README](examples/blog_app/README.md). The files
|
|
101
|
+
[`examples/usage_examples.rb`](examples/usage_examples.rb),
|
|
102
|
+
[`examples/privacy_examples.rb`](examples/privacy_examples.rb) and
|
|
103
|
+
[`examples/privacy_policy_usage.rb`](examples/privacy_policy_usage.rb) show
|
|
104
|
+
individual calls.
|
|
985
105
|
|
|
986
106
|
---
|
|
987
107
|
|
|
@@ -991,21 +111,21 @@ rails console
|
|
|
991
111
|
- **Orthogonal Analysis**: Compare static vs. dynamic views of system state
|
|
992
112
|
- **Behavioral Analysis**: Understand system evolution over time
|
|
993
113
|
- **Migration Patterns**: Study CRUD-to-ES transformation strategies
|
|
994
|
-
- **Formal Verification**:
|
|
114
|
+
- **Formal Verification**: Check the CRUD-to-event mapping with Petri net models (petri_flow)
|
|
995
115
|
- **Privacy Compliance**: Research privacy-preserving event sourcing patterns
|
|
996
116
|
|
|
997
117
|
### For Development
|
|
998
|
-
- **
|
|
999
|
-
- **Audit Trail**:
|
|
1000
|
-
- **Temporal Queries**: Query state at
|
|
118
|
+
- **Gradual Migration**: Move to event sourcing one checked mode switch at a time
|
|
119
|
+
- **Audit Trail**: History of every write to a monitored model that goes through ActiveRecord
|
|
120
|
+
- **Temporal Queries**: Query state at a point in time
|
|
1001
121
|
- **Debugging**: Replay events to understand issues
|
|
1002
|
-
- **CQRS Support**:
|
|
122
|
+
- **CQRS Support**: Commands and aggregates alongside the projected tables
|
|
1003
123
|
|
|
1004
124
|
### For Operations
|
|
1005
|
-
- **
|
|
1006
|
-
- **Rollback
|
|
1007
|
-
- **
|
|
1008
|
-
- **Validation**:
|
|
125
|
+
- **Few code changes**: A model is monitored by one line or by naming it in the initializer; controllers and queries stay as they are (see [Adopting Lyra](docs/MIGRATION_GUIDE.md#adopting-lyra-in-an-existing-application) for what this costs)
|
|
126
|
+
- **Rollback**: Every mode can be switched back to the previous one
|
|
127
|
+
- **Monitoring**: Compare table and event-sourced state (DualView)
|
|
128
|
+
- **Validation**: A switch that makes the events authoritative is checked first
|
|
1009
129
|
- **Performance Analysis**: Measure overhead before committing
|
|
1010
130
|
|
|
1011
131
|
---
|
|
@@ -1013,12 +133,19 @@ rails console
|
|
|
1013
133
|
## Documentation
|
|
1014
134
|
|
|
1015
135
|
### Core Documentation
|
|
1016
|
-
- **[Getting Started
|
|
136
|
+
- **[Getting Started](docs/GETTING_STARTED.md)** - Install, monitor one model, and check its events
|
|
137
|
+
- **[Migration Guide](docs/MIGRATION_GUIDE.md)** - Installation and the step-by-step move from CRUD to event sourcing
|
|
138
|
+
- **[API Reference](docs/API_REFERENCE.md)** - Configuration options, methods, rake tasks and errors
|
|
1017
139
|
- **[Architecture Overview](docs/ARCHITECTURE.md)** - System design and components
|
|
1018
|
-
- **[
|
|
140
|
+
- **[Switching Modes](docs/MIGRATION_GUIDE.md#switching-modes)** - Who holds the mode, and how to switch safely (the deploy-time rule)
|
|
141
|
+
- **[Adopting Lyra](docs/MIGRATION_GUIDE.md#adopting-lyra-in-an-existing-application)** - What "non-intrusive" means, the costs, and a checklist for a new codebase
|
|
142
|
+
- **[Performance](docs/PERFORMANCE.md)** - What each mode costs and why (counted in SQL statements per write; throughput being re-measured)
|
|
143
|
+
- **[Troubleshooting](docs/TROUBLESHOOTING.md)** - Common problems
|
|
144
|
+
- **[Workflow Generator](docs/WORKFLOW_GENERATOR.md)** - The Petri net workflow models of Lyra's modes and their verification
|
|
145
|
+
- **[Changelog](CHANGELOG.md)**
|
|
1019
146
|
|
|
1020
147
|
### Theoretical Foundation
|
|
1021
|
-
- **[ORFEAS Framework Overview](docs/
|
|
148
|
+
- **[ORFEAS Framework Overview](docs/ARCHITECTURE.md#about-orfeas)** - The framework: naming, vision, components and migration path (the opening of ARCHITECTURE.md)
|
|
1022
149
|
- **[Petri Nets Model](gems/petri_flow/docs/THEORETICAL_MODEL_PETRI_NETS.md)** - P/T nets for verification, CPNs for data modeling
|
|
1023
150
|
- **[Matrix Analysis Model](gems/petri_flow/docs/THEORETICAL_MODEL_MATRICES.md)** - Linear algebra approach
|
|
1024
151
|
|
|
@@ -1042,10 +169,26 @@ ORFEAS and Lyra are grounded in peer-reviewed research:
|
|
|
1042
169
|
|
|
1043
170
|
### Published Papers
|
|
1044
171
|
|
|
172
|
+
**Pantelelis, M., & Kalloniatis, C. (2026).** *From ORM to OEM: Object-to-Events Mapping for the OLEP Era.*
|
|
173
|
+
IEEE Software.
|
|
174
|
+
DOI: [10.1109/MS.2026.3734188](https://doi.org/10.1109/MS.2026.3734188)
|
|
175
|
+
|
|
176
|
+
**Pantelelis, M., & Kalloniatis, C. (2026).** *PAM: A Domain-Specific Language for Specifying Privacy Requirements from Regulation to Runtime.*
|
|
177
|
+
Requirements Engineering, Springer.
|
|
178
|
+
DOI: [10.1007/s00766-026-00469-6](https://doi.org/10.1007/s00766-026-00469-6)
|
|
179
|
+
|
|
180
|
+
**Pantelelis, M., & Kalloniatis, C. (2024).** *Create, Read, Update, Delete: Implications on Security and Privacy Principles regarding GDPR.*
|
|
181
|
+
ARES 2024, 21st International Workshop on Trust, Privacy and Security in the Digital Society (TrustBus), ACM.
|
|
182
|
+
DOI: [10.1145/3664476.3670898](https://doi.org/10.1145/3664476.3670898)
|
|
183
|
+
|
|
1045
184
|
**Pantelelis, M., & Kalloniatis, C. (2022).** *Mapping CRUD to Events: Towards an object to event-sourcing framework.*
|
|
1046
185
|
26th Pan-Hellenic Conference on Informatics (PCI 2022).
|
|
1047
186
|
DOI: [10.1145/3575879.3576006](https://doi.org/10.1145/3575879.3576006)
|
|
1048
187
|
|
|
188
|
+
**Pantelelis, M., & Kalloniatis, C. (2022).** *Object Relational Mapping vs. Event-Sourcing: Systematic Review.*
|
|
189
|
+
EGOVIS 2022, Lecture Notes in Computer Science 13429, Springer, pp. 18–31.
|
|
190
|
+
DOI: [10.1007/978-3-031-12673-4_2](https://doi.org/10.1007/978-3-031-12673-4_2)
|
|
191
|
+
|
|
1049
192
|
### Research Areas
|
|
1050
193
|
|
|
1051
194
|
- **Event Sourcing Patterns**: Formal models for CRUD-to-event transformation
|
|
@@ -1097,12 +240,19 @@ cd lyra
|
|
|
1097
240
|
# Install dependencies
|
|
1098
241
|
bundle install
|
|
1099
242
|
|
|
1100
|
-
#
|
|
1101
|
-
|
|
243
|
+
# The engine's tests need PostgreSQL on localhost:5433 (user and password
|
|
244
|
+
# "postgres", database lyra_test; see test/dummy/config/database.yml).
|
|
245
|
+
# docker-compose.yml provides one:
|
|
246
|
+
bundle exec rake docker:start
|
|
247
|
+
docker exec lyra_postgres createdb -U postgres lyra_test
|
|
248
|
+
|
|
249
|
+
# Run the engine's tests, then pam_dsl's and petri_flow's
|
|
250
|
+
bundle exec rake test:all
|
|
1102
251
|
|
|
1103
|
-
# Build gems
|
|
1104
|
-
|
|
1105
|
-
cd gems/pam_dsl && rake build
|
|
252
|
+
# Build the gems
|
|
253
|
+
bundle exec rake build
|
|
254
|
+
(cd gems/pam_dsl && bundle exec rake build)
|
|
255
|
+
(cd gems/petri_flow && bundle exec rake build)
|
|
1106
256
|
```
|
|
1107
257
|
|
|
1108
258
|
---
|
|
@@ -1123,7 +273,50 @@ If you use this software in academic research, please cite:
|
|
|
1123
273
|
}
|
|
1124
274
|
```
|
|
1125
275
|
|
|
1126
|
-
### Research Paper
|
|
276
|
+
### Research Paper Citations
|
|
277
|
+
|
|
278
|
+
Lyra's object-to-events mapping, and how its modes compare in cost:
|
|
279
|
+
|
|
280
|
+
```bibtex
|
|
281
|
+
@article{pantelelis2026oem,
|
|
282
|
+
title={From {ORM} to {OEM}: Object-to-Events Mapping for the {OLEP} Era},
|
|
283
|
+
author={Pantelelis, Michail and Kalloniatis, Christos},
|
|
284
|
+
journal={IEEE Software},
|
|
285
|
+
year={2026},
|
|
286
|
+
doi={10.1109/MS.2026.3734188}
|
|
287
|
+
}
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
PAM, the privacy DSL (`gems/pam_dsl`), from regulation to runtime:
|
|
291
|
+
|
|
292
|
+
```bibtex
|
|
293
|
+
@article{pantelelis2026pam,
|
|
294
|
+
title={{PAM}: A Domain-Specific Language for Specifying Privacy Requirements from Regulation to Runtime},
|
|
295
|
+
author={Pantelelis, Michail and Kalloniatis, Christos},
|
|
296
|
+
journal={Requirements Engineering},
|
|
297
|
+
publisher={Springer},
|
|
298
|
+
year={2026},
|
|
299
|
+
doi={10.1007/s00766-026-00469-6}
|
|
300
|
+
}
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
What each CRUD operation means for GDPR's security and privacy principles, the ground of PAM and Lyra's privacy events:
|
|
304
|
+
|
|
305
|
+
```bibtex
|
|
306
|
+
@inproceedings{pantelelis2024crud,
|
|
307
|
+
title={Create, Read, Update, Delete: Implications on Security and Privacy Principles regarding {GDPR}},
|
|
308
|
+
author={Pantelelis, Michail and Kalloniatis, Christos},
|
|
309
|
+
booktitle={Proceedings of the 19th International Conference on Availability, Reliability and Security (ARES 2024), 21st International Workshop on Trust, Privacy and Security in the Digital Society (TrustBus)},
|
|
310
|
+
articleno={178},
|
|
311
|
+
pages={1--7},
|
|
312
|
+
publisher={ACM},
|
|
313
|
+
address={Vienna, Austria},
|
|
314
|
+
year={2024},
|
|
315
|
+
doi={10.1145/3664476.3670898}
|
|
316
|
+
}
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
The CRUD-to-events mapping as first proposed:
|
|
1127
320
|
|
|
1128
321
|
```bibtex
|
|
1129
322
|
@inproceedings{pantelelis2022mapping,
|
|
@@ -1135,6 +328,22 @@ If you use this software in academic research, please cite:
|
|
|
1135
328
|
}
|
|
1136
329
|
```
|
|
1137
330
|
|
|
331
|
+
The literature review behind it, ORM against Event Sourcing:
|
|
332
|
+
|
|
333
|
+
```bibtex
|
|
334
|
+
@inproceedings{pantelelis2022egovis,
|
|
335
|
+
title={Object Relational Mapping vs. Event-Sourcing: Systematic Review},
|
|
336
|
+
author={Pantelelis, Michail and Kalloniatis, Christos},
|
|
337
|
+
booktitle={Electronic Government and the Information Systems Perspective (EGOVIS 2022)},
|
|
338
|
+
series={Lecture Notes in Computer Science},
|
|
339
|
+
volume={13429},
|
|
340
|
+
pages={18--31},
|
|
341
|
+
publisher={Springer},
|
|
342
|
+
year={2022},
|
|
343
|
+
doi={10.1007/978-3-031-12673-4_2}
|
|
344
|
+
}
|
|
345
|
+
```
|
|
346
|
+
|
|
1138
347
|
### References
|
|
1139
348
|
|
|
1140
349
|
- [Rails Event Store](https://railseventstore.org/) - Event Store implementation
|