orfeas_lyra 0.6.0 → 0.8.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 +49 -0
- data/README.md +1 -966
- data/config/privacy_policies.rb +2 -1
- data/lib/lyra/engine.rb +1 -0
- data/lib/lyra/interceptors/crud_interceptor.rb +13 -4
- data/lib/lyra/privacy/gdpr_compliance.rb +1 -1
- data/lib/lyra/privacy/policy_integration.rb +4 -8
- data/lib/lyra/projections/cached_projection.rb +1 -1
- data/lib/lyra/projections/rebuild.rb +141 -0
- data/lib/lyra/version.rb +1 -1
- data/lib/lyra.rb +1 -0
- data/lib/tasks/lyra_projections.rake +35 -0
- metadata +4 -7
- 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/config/privacy_policies.rb
CHANGED
|
@@ -110,9 +110,9 @@ PamDsl.define_policy :university_system do
|
|
|
110
110
|
purpose :communication do
|
|
111
111
|
describe "Important academic and administrative communications"
|
|
112
112
|
basis :legitimate_interests
|
|
113
|
+
lia_documented!
|
|
113
114
|
requires :email
|
|
114
115
|
optionally :phone, :name
|
|
115
|
-
meta :balancing_test_performed, true
|
|
116
116
|
end
|
|
117
117
|
|
|
118
118
|
purpose :academic_records do
|
|
@@ -140,6 +140,7 @@ PamDsl.define_policy :university_system do
|
|
|
140
140
|
purpose :analytics do
|
|
141
141
|
describe "Improving educational services and user experience"
|
|
142
142
|
basis :legitimate_interests
|
|
143
|
+
lia_documented!
|
|
143
144
|
requires :student_id
|
|
144
145
|
meta :anonymization_applied, true
|
|
145
146
|
end
|
data/lib/lyra/engine.rb
CHANGED
|
@@ -8,10 +8,12 @@ module Lyra
|
|
|
8
8
|
class_attribute :lyra_monitored, default: false
|
|
9
9
|
class_attribute :lyra_config
|
|
10
10
|
|
|
11
|
-
# Callbacks for monitoring CRUD operations (after_* = non-blocking)
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
# Callbacks for monitoring CRUD operations (after_* = non-blocking).
|
|
12
|
+
# Gated on the mode as well as the model: in :disabled mode a monitored
|
|
13
|
+
# model must behave exactly like plain ActiveRecord and emit no events.
|
|
14
|
+
after_create :lyra_intercept_create, if: :lyra_events_enabled?
|
|
15
|
+
after_update :lyra_intercept_update, if: :lyra_events_enabled?
|
|
16
|
+
after_destroy :lyra_intercept_destroy, if: :lyra_events_enabled?
|
|
15
17
|
|
|
16
18
|
# For hijack mode - run before operation, store event, then let save proceed
|
|
17
19
|
before_create :lyra_hijack_create, if: :lyra_hijack_mode?
|
|
@@ -184,6 +186,13 @@ module Lyra
|
|
|
184
186
|
self.class.lyra_monitored
|
|
185
187
|
end
|
|
186
188
|
|
|
189
|
+
# Monitoring a model is not enough to emit events: :disabled mode must be a
|
|
190
|
+
# true ORM baseline. Hijack and event-sourcing modes still pass this guard;
|
|
191
|
+
# their after_* callbacks return early via @lyra_hijacked.
|
|
192
|
+
def lyra_events_enabled?
|
|
193
|
+
lyra_monitored? && !Lyra.disabled_mode?
|
|
194
|
+
end
|
|
195
|
+
|
|
187
196
|
def lyra_hijack_mode?
|
|
188
197
|
lyra_monitored? && Lyra.hijack_mode?
|
|
189
198
|
end
|
|
@@ -30,7 +30,7 @@ module Lyra
|
|
|
30
30
|
@pam_compliance = PamDsl::GDPRCompliance.new(
|
|
31
31
|
subject_id: subject_id,
|
|
32
32
|
subject_type: subject_type,
|
|
33
|
-
|
|
33
|
+
record_reader: method(:read_subject_events),
|
|
34
34
|
attribute_extractor: method(:extract_attributes),
|
|
35
35
|
timestamp_extractor: method(:extract_timestamp),
|
|
36
36
|
operation_extractor: method(:extract_operation),
|
|
@@ -37,16 +37,12 @@ module Lyra
|
|
|
37
37
|
!@policy.nil?
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
-
# Validate data access for a purpose
|
|
41
|
-
|
|
40
|
+
# Validate data access for a (fields, purpose, subject) triple.
|
|
41
|
+
# subject: is the data subject identifier required by Policy#validate_access!
|
|
42
|
+
def validate_access!(field_names, purpose, subject:)
|
|
42
43
|
return true unless policy_loaded?
|
|
43
44
|
|
|
44
|
-
@policy.validate_access!(
|
|
45
|
-
field_names,
|
|
46
|
-
purpose,
|
|
47
|
-
consent_granted: consent_status[:granted] || false,
|
|
48
|
-
consent_granted_at: consent_status[:granted_at]
|
|
49
|
-
)
|
|
45
|
+
@policy.validate_access!(field_names, purpose, subject: subject)
|
|
50
46
|
end
|
|
51
47
|
|
|
52
48
|
# Get PII fields from attributes using policy and/or detector
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lyra
|
|
4
|
+
module Projections
|
|
5
|
+
# Rebuilds read-model tables from the event log.
|
|
6
|
+
#
|
|
7
|
+
# This is the load-bearing operation of event sourcing: because the event
|
|
8
|
+
# log is the source of truth and the model tables are a derived projection,
|
|
9
|
+
# the tables must be reconstructable from the log alone. Rebuild enumerates
|
|
10
|
+
# every stream for a model, clears the read model, and replays each event
|
|
11
|
+
# through the *same* +ModelProjection.project+ path used by live sync/async
|
|
12
|
+
# projection -- so a rebuilt table is byte-for-byte what live projection
|
|
13
|
+
# would have produced.
|
|
14
|
+
#
|
|
15
|
+
# Usage:
|
|
16
|
+
# Lyra::Projections::Rebuild.rebuild(Registration)
|
|
17
|
+
# Lyra::Projections::Rebuild.rebuild_all # all monitored models
|
|
18
|
+
# Lyra::Projections::Rebuild.rebuild(Order, truncate: false)
|
|
19
|
+
#
|
|
20
|
+
# Or via rake:
|
|
21
|
+
# rake lyra:projections:rebuild MODEL=Registration
|
|
22
|
+
# rake lyra:projections:rebuild # all monitored models
|
|
23
|
+
#
|
|
24
|
+
class Rebuild
|
|
25
|
+
class << self
|
|
26
|
+
# Rebuild a single model's read-model table from its event streams.
|
|
27
|
+
#
|
|
28
|
+
# @param model_class [Class] the ActiveRecord model to rebuild
|
|
29
|
+
# @param truncate [Boolean] clear the table before replaying (default true);
|
|
30
|
+
# set false to reconstruct only the streamed rows in place
|
|
31
|
+
# @return [Hash] statistics for the rebuild
|
|
32
|
+
def rebuild(model_class, truncate: true)
|
|
33
|
+
stats = {
|
|
34
|
+
model: model_class.name,
|
|
35
|
+
streams: 0, events: 0, records: 0, destroyed: 0
|
|
36
|
+
}
|
|
37
|
+
rebuilt_ids = []
|
|
38
|
+
|
|
39
|
+
model_class.transaction do
|
|
40
|
+
clear_read_model(model_class) if truncate
|
|
41
|
+
|
|
42
|
+
stream_ids_for(model_class).each do |id|
|
|
43
|
+
stats[:streams] += 1
|
|
44
|
+
rebuilt_ids << id
|
|
45
|
+
replayed, present = replay_stream(model_class, id)
|
|
46
|
+
stats[:events] += replayed
|
|
47
|
+
present ? stats[:records] += 1 : stats[:destroyed] += 1
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
invalidate_caches(model_class, rebuilt_ids)
|
|
52
|
+
stats
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Rebuild every monitored model (or an explicit list).
|
|
56
|
+
#
|
|
57
|
+
# @param models [Array<Class>] models to rebuild (defaults to all monitored)
|
|
58
|
+
# @param truncate [Boolean] clear each table before replaying
|
|
59
|
+
# @return [Array<Hash>] per-model statistics
|
|
60
|
+
def rebuild_all(models = Lyra.config.monitored_models, truncate: true)
|
|
61
|
+
models.map { |model_class| rebuild(model_class, truncate: truncate) }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
# Replay a single stream (one record's lifecycle) through the live
|
|
67
|
+
# projector, in append order. Returns [events_replayed, still_present?].
|
|
68
|
+
def replay_stream(model_class, id)
|
|
69
|
+
stream_name = "#{model_class.name}$#{id}"
|
|
70
|
+
events = Lyra.config.event_store.read.stream(stream_name).to_a
|
|
71
|
+
replayed = 0
|
|
72
|
+
|
|
73
|
+
events.each do |event|
|
|
74
|
+
operation = operation_for(event)
|
|
75
|
+
next unless operation
|
|
76
|
+
|
|
77
|
+
ModelProjection.project(model_class, operation, build_result_from_event(event))
|
|
78
|
+
replayed += 1
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
present = !(events.last && events.last.event_type.to_s.end_with?("Destroyed"))
|
|
82
|
+
[replayed, present]
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Map an event type to a CRUD operation. Unknown event types are skipped.
|
|
86
|
+
def operation_for(event)
|
|
87
|
+
case event.event_type.to_s
|
|
88
|
+
when /Created$/ then :create
|
|
89
|
+
when /Updated$/ then :update
|
|
90
|
+
when /Destroyed$/ then :destroy
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Reconstruct the CommandResult-like struct ModelProjection expects,
|
|
95
|
+
# identical to Lyra::Projections::AsyncProjectionJob so rebuild and live
|
|
96
|
+
# async projection share one code path.
|
|
97
|
+
def build_result_from_event(event)
|
|
98
|
+
Struct.new(:events, :attributes, :success?, keyword_init: true).new(
|
|
99
|
+
events: [event],
|
|
100
|
+
attributes: event.data[:attributes] || event.data["attributes"] || {},
|
|
101
|
+
success?: true
|
|
102
|
+
)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Enumerate the record ids that have an event stream for this model.
|
|
106
|
+
def stream_ids_for(model_class)
|
|
107
|
+
prefix = "#{model_class.name}$"
|
|
108
|
+
sql = ActiveRecord::Base.sanitize_sql_array(
|
|
109
|
+
["SELECT DISTINCT stream FROM event_store_events_in_streams WHERE stream LIKE ?", "#{prefix}%"]
|
|
110
|
+
)
|
|
111
|
+
ActiveRecord::Base.connection.select_values(sql).map { |stream| stream.sub(prefix, "") }
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Clear the read-model table, bypassing callbacks/strict-access guards
|
|
115
|
+
# (this is a legitimate bulk projection operation, like ModelProjection).
|
|
116
|
+
def clear_read_model(model_class)
|
|
117
|
+
previous = Thread.current[:lyra_bypass_strict_access]
|
|
118
|
+
Thread.current[:lyra_bypass_strict_access] = true
|
|
119
|
+
begin
|
|
120
|
+
model_class.unscoped.delete_all
|
|
121
|
+
ensure
|
|
122
|
+
Thread.current[:lyra_bypass_strict_access] = previous
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Drop any cached (ES-NoProj) reconstructions so reads reflect the rebuild.
|
|
127
|
+
#
|
|
128
|
+
# Invalidates the specific rebuilt record ids individually (store-agnostic
|
|
129
|
+
# +cache_delete+) rather than a wildcard sweep -- SolidCache, the default
|
|
130
|
+
# backend, advertises +delete_matched+ but raises NotImplementedError.
|
|
131
|
+
def invalidate_caches(model_class, ids)
|
|
132
|
+
return unless defined?(CachedProjection)
|
|
133
|
+
|
|
134
|
+
ids.each { |id| CachedProjection.invalidate(model_class, id) }
|
|
135
|
+
rescue StandardError, NotImplementedError => e
|
|
136
|
+
Rails.logger.warn("Lyra::Projections::Rebuild: cache invalidation failed - #{e.message}") if defined?(Rails)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
data/lib/lyra/version.rb
CHANGED
data/lib/lyra.rb
CHANGED
|
@@ -48,6 +48,7 @@ require "lyra/strict_data_access"
|
|
|
48
48
|
|
|
49
49
|
# Event sourcing projections
|
|
50
50
|
require "lyra/projections/model_projection"
|
|
51
|
+
require "lyra/projections/rebuild"
|
|
51
52
|
require "lyra/projections/async_projection_job"
|
|
52
53
|
require "lyra/projections/cached_projection"
|
|
53
54
|
require "lyra/projections/cached_relation"
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
namespace :lyra do
|
|
4
|
+
namespace :projections do
|
|
5
|
+
desc "Rebuild read-model tables from the event log " \
|
|
6
|
+
"(MODEL=Namespace::Model[,Other] for specific models, or all monitored models; " \
|
|
7
|
+
"TRUNCATE=false to reconstruct in place)"
|
|
8
|
+
task rebuild: :environment do
|
|
9
|
+
models =
|
|
10
|
+
if ENV["MODEL"]
|
|
11
|
+
ENV["MODEL"].split(",").map { |name| name.strip.constantize }
|
|
12
|
+
else
|
|
13
|
+
Lyra.config.monitored_models
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
if models.empty?
|
|
17
|
+
puts "No models to rebuild."
|
|
18
|
+
puts "Pass MODEL=YourModel, or configure models with Lyra.config.monitor_model(YourModel)."
|
|
19
|
+
exit 1
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
truncate = ENV.fetch("TRUNCATE", "true") != "false"
|
|
23
|
+
|
|
24
|
+
puts "Rebuilding #{models.size} model(s) from the event log (truncate=#{truncate})..."
|
|
25
|
+
models.each do |model_class|
|
|
26
|
+
stats = Lyra::Projections::Rebuild.rebuild(model_class, truncate: truncate)
|
|
27
|
+
puts format(
|
|
28
|
+
" %-30s %5d records, %4d destroyed, %6d events across %5d streams",
|
|
29
|
+
stats[:model], stats[:records], stats[:destroyed], stats[:events], stats[:streams]
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
puts "Done."
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: orfeas_lyra
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Michail Pantelelis
|
|
@@ -173,6 +173,7 @@ files:
|
|
|
173
173
|
- lib/lyra/projections/cached_relation.rb
|
|
174
174
|
- lib/lyra/projections/event_store_reader.rb
|
|
175
175
|
- lib/lyra/projections/model_projection.rb
|
|
176
|
+
- lib/lyra/projections/rebuild.rb
|
|
176
177
|
- lib/lyra/schema/diff.rb
|
|
177
178
|
- lib/lyra/schema/event_class_registrar.rb
|
|
178
179
|
- lib/lyra/schema/generator.rb
|
|
@@ -186,13 +187,9 @@ files:
|
|
|
186
187
|
- lib/lyra/visualization/activity_heatmap.rb
|
|
187
188
|
- lib/lyra/visualization/event_graph.rb
|
|
188
189
|
- lib/lyra/visualization/timeline.rb
|
|
189
|
-
- lib/tasks/
|
|
190
|
-
- lib/tasks/gems.rake
|
|
190
|
+
- lib/tasks/lyra_projections.rake
|
|
191
191
|
- lib/tasks/lyra_schema.rake
|
|
192
192
|
- lib/tasks/lyra_workflows.rake
|
|
193
|
-
- lib/tasks/public_release.rake
|
|
194
|
-
- lib/tasks/stats.rake
|
|
195
|
-
- lib/tasks/testbed.rake
|
|
196
193
|
- lib/tasks/version.rake
|
|
197
194
|
homepage: https://github.com/mpantel/lyra-engine
|
|
198
195
|
licenses:
|
|
@@ -215,7 +212,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
215
212
|
- !ruby/object:Gem::Version
|
|
216
213
|
version: '0'
|
|
217
214
|
requirements: []
|
|
218
|
-
rubygems_version: 4.0.
|
|
215
|
+
rubygems_version: 4.0.20
|
|
219
216
|
specification_version: 4
|
|
220
217
|
summary: CRUD to Event Sourcing transformation engine
|
|
221
218
|
test_files: []
|