lethe 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +41 -0
- data/LICENSE +21 -0
- data/README.md +326 -0
- data/lib/generators/lethe/install/install_generator.rb +24 -0
- data/lib/generators/lethe/install/templates/create_lethe_erasures.rb.tt +14 -0
- data/lib/generators/lethe/install/templates/lethe.rb.tt +7 -0
- data/lib/lethe/checker.rb +205 -0
- data/lib/lethe/configuration.rb +75 -0
- data/lib/lethe/definition.rb +118 -0
- data/lib/lethe/discovery.rb +117 -0
- data/lib/lethe/enums.rb +46 -0
- data/lib/lethe/eraser.rb +126 -0
- data/lib/lethe/exporter.rb +163 -0
- data/lib/lethe/formatters.rb +178 -0
- data/lib/lethe/ledger.rb +29 -0
- data/lib/lethe/leftovers.rb +98 -0
- data/lib/lethe/locales/en.yml +77 -0
- data/lib/lethe/locales/ja.yml +77 -0
- data/lib/lethe/locator.rb +59 -0
- data/lib/lethe/plan.rb +110 -0
- data/lib/lethe/policy.rb +31 -0
- data/lib/lethe/railtie.rb +10 -0
- data/lib/lethe/reference.rb +43 -0
- data/lib/lethe/report.rb +36 -0
- data/lib/lethe/result.rb +22 -0
- data/lib/lethe/rows.rb +37 -0
- data/lib/lethe/rules.rb +136 -0
- data/lib/lethe/runner.rb +126 -0
- data/lib/lethe/sweeper.rb +50 -0
- data/lib/lethe/target.rb +42 -0
- data/lib/lethe/tasks/lethe.rake +26 -0
- data/lib/lethe/version.rb +5 -0
- data/lib/lethe.rb +193 -0
- metadata +119 -0
data/lib/lethe.rb
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_record"
|
|
4
|
+
require "i18n"
|
|
5
|
+
require_relative "lethe/version"
|
|
6
|
+
require_relative "lethe/configuration"
|
|
7
|
+
require_relative "lethe/target"
|
|
8
|
+
require_relative "lethe/policy"
|
|
9
|
+
require_relative "lethe/definition"
|
|
10
|
+
require_relative "lethe/reference"
|
|
11
|
+
require_relative "lethe/discovery"
|
|
12
|
+
require_relative "lethe/result"
|
|
13
|
+
require_relative "lethe/report"
|
|
14
|
+
require_relative "lethe/rules"
|
|
15
|
+
require_relative "lethe/checker"
|
|
16
|
+
require_relative "lethe/formatters"
|
|
17
|
+
require_relative "lethe/exporter"
|
|
18
|
+
require_relative "lethe/runner"
|
|
19
|
+
require_relative "lethe/rows"
|
|
20
|
+
require_relative "lethe/enums"
|
|
21
|
+
require_relative "lethe/locator"
|
|
22
|
+
require_relative "lethe/plan"
|
|
23
|
+
require_relative "lethe/ledger"
|
|
24
|
+
require_relative "lethe/eraser"
|
|
25
|
+
require_relative "lethe/leftovers"
|
|
26
|
+
require_relative "lethe/sweeper"
|
|
27
|
+
|
|
28
|
+
# Declares what happens to the data that points at a model, such as User,
|
|
29
|
+
# when that record goes away, and finds the tables nobody declared.
|
|
30
|
+
module Lethe
|
|
31
|
+
class Error < StandardError; end
|
|
32
|
+
|
|
33
|
+
# The definitions file could not be read, or names something that is not a model.
|
|
34
|
+
class DefinitionError < Error; end
|
|
35
|
+
|
|
36
|
+
# Raised by Lethe.check! when the check finds problems.
|
|
37
|
+
class Unclean < Error
|
|
38
|
+
attr_reader :report
|
|
39
|
+
|
|
40
|
+
def initialize(report)
|
|
41
|
+
@report = report
|
|
42
|
+
super(Formatters::Text.new(report).render)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
I18n.load_path += Dir[File.join(__dir__, "lethe", "locales", "*.yml")]
|
|
47
|
+
|
|
48
|
+
PREPARE_MUTEX = Mutex.new
|
|
49
|
+
|
|
50
|
+
class << self
|
|
51
|
+
def configuration
|
|
52
|
+
@configuration ||= Configuration.new
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def configure
|
|
56
|
+
yield configuration
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# The definitions declared so far, keyed by subject class name.
|
|
60
|
+
def definitions
|
|
61
|
+
@definitions ||= {}
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Declares the policies for one subject. Calling it again for the same
|
|
65
|
+
# subject adds to what was declared before.
|
|
66
|
+
def define(subject, &block)
|
|
67
|
+
raise ArgumentError, "Lethe.define needs a block" unless block
|
|
68
|
+
|
|
69
|
+
name = subject.is_a?(Module) ? subject.name : subject.to_s
|
|
70
|
+
raise ArgumentError, "Lethe.define needs a class or a class name" if name.nil? || name.empty?
|
|
71
|
+
|
|
72
|
+
definition = definitions[name] ||= Definition.new(name)
|
|
73
|
+
Definition::Builder.new(definition).instance_exec(&block)
|
|
74
|
+
definition
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Forgets every definition, then loads the definitions file.
|
|
78
|
+
def load_definitions(path)
|
|
79
|
+
raise DefinitionError, I18n.t("lethe.errors.no_definitions_file", path: path.to_s) unless File.file?(path)
|
|
80
|
+
|
|
81
|
+
reset_definitions!
|
|
82
|
+
load File.expand_path(path)
|
|
83
|
+
definitions
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def reset_definitions!
|
|
87
|
+
@definitions = {}
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def reset!
|
|
91
|
+
reset_definitions!
|
|
92
|
+
@configuration = nil
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Checks every definition against the schema and returns a Report.
|
|
96
|
+
# models defaults to every loaded Active Record model with a table.
|
|
97
|
+
def check(definitions: nil, models: nil, connection: nil, strict: false)
|
|
98
|
+
prepare_application if definitions.nil?
|
|
99
|
+
definitions ||= self.definitions.values
|
|
100
|
+
run = lambda do |conn|
|
|
101
|
+
definitions.flat_map do |definition|
|
|
102
|
+
Checker.new(definition, models: models, connection: conn, configuration: configuration).call
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
results = connection ? run.call(connection) : ActiveRecord::Base.with_connection(&run)
|
|
106
|
+
Report.new(results, strict: strict)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Like check, but raises Lethe::Unclean when there are problems. For tests.
|
|
110
|
+
def check!(**options)
|
|
111
|
+
report = check(**options)
|
|
112
|
+
raise Unclean, report unless report.clean?
|
|
113
|
+
|
|
114
|
+
report
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# What erasing record would do, step by step, without doing it.
|
|
118
|
+
# Raises Lethe::Unclean when the record's definition does not pass the check.
|
|
119
|
+
def plan(record, models: nil)
|
|
120
|
+
prepare_application
|
|
121
|
+
definition, report = checked(record.class, models)
|
|
122
|
+
ActiveRecord::Base.with_connection do |connection|
|
|
123
|
+
Plan.new(record, definition, report.results, models: models || application_models, connection: connection)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Erases record as its definition declares, in one transaction, and
|
|
128
|
+
# returns what it did. Refuses with Lethe::Unclean unless the check passes.
|
|
129
|
+
def erase!(record, models: nil)
|
|
130
|
+
plan = plan(record, models: models)
|
|
131
|
+
ledger = ActiveRecord::Base.with_connection { |connection| Ledger.available?(connection) }
|
|
132
|
+
if !ledger && plan.definition.policies.any?(&:retention)
|
|
133
|
+
raise Error, I18n.t("lethe.errors.ledger_required", table: Ledger::TABLE)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
Eraser.new(plan, configuration: configuration, ledger: ledger).call
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# After erase!, what still points at the subject in a way its definition
|
|
140
|
+
# does not allow. Empty when the erasure left nothing behind. For tests.
|
|
141
|
+
def leftovers(record_or_class, id = nil, models: nil)
|
|
142
|
+
prepare_application
|
|
143
|
+
subject = record_or_class.is_a?(Class) ? record_or_class : record_or_class.class
|
|
144
|
+
id ||= record_or_class.id
|
|
145
|
+
definition, report = checked(subject, models)
|
|
146
|
+
Leftovers.new(subject, id, definition, report.results, models: models || application_models).call
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Deletes kept rows whose `for:` has passed since their subject was erased.
|
|
150
|
+
def sweep!(models: nil, now: Time.current)
|
|
151
|
+
prepare_application
|
|
152
|
+
ActiveRecord::Base.with_connection do |connection|
|
|
153
|
+
raise Error, I18n.t("lethe.errors.ledger_required", table: Ledger::TABLE) unless Ledger.available?(connection)
|
|
154
|
+
end
|
|
155
|
+
swept = Sweeper.new(definitions.values, models: models || application_models, now: now).call
|
|
156
|
+
ActiveSupport::Notifications.instrument("sweep.lethe", counts: swept)
|
|
157
|
+
swept
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
private
|
|
161
|
+
|
|
162
|
+
# Inside a Rails application: loads config/lethe.rb the first time it is
|
|
163
|
+
# needed, unless definitions were loaded or declared already, and loads
|
|
164
|
+
# every model once, so that development and test see the same
|
|
165
|
+
# associations as production and CI.
|
|
166
|
+
def prepare_application
|
|
167
|
+
return unless defined?(Rails.application) && Rails.application
|
|
168
|
+
|
|
169
|
+
PREPARE_MUTEX.synchronize do
|
|
170
|
+
if definitions.empty?
|
|
171
|
+
path = Rails.root.join(configuration.definition_path)
|
|
172
|
+
load_definitions(path) if File.file?(path)
|
|
173
|
+
end
|
|
174
|
+
unless @eager_loaded || Rails.application.config.eager_load
|
|
175
|
+
Rails.application.eager_load!
|
|
176
|
+
@eager_loaded = true
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def checked(subject, models)
|
|
182
|
+
definition = definitions[subject.name] || definitions[subject.base_class.name]
|
|
183
|
+
raise Error, I18n.t("lethe.errors.no_definition", subject: subject.name) unless definition
|
|
184
|
+
|
|
185
|
+
report = check(definitions: [definition], models: models)
|
|
186
|
+
raise Unclean, report unless report.clean?
|
|
187
|
+
|
|
188
|
+
[definition, report]
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
require_relative "lethe/railtie" if defined?(Rails::Railtie)
|
metadata
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: lethe
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- KAIBYAKU K.K
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: activerecord
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '7.2'
|
|
19
|
+
- - "<"
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: '9'
|
|
22
|
+
type: :runtime
|
|
23
|
+
prerelease: false
|
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
25
|
+
requirements:
|
|
26
|
+
- - ">="
|
|
27
|
+
- !ruby/object:Gem::Version
|
|
28
|
+
version: '7.2'
|
|
29
|
+
- - "<"
|
|
30
|
+
- !ruby/object:Gem::Version
|
|
31
|
+
version: '9'
|
|
32
|
+
- !ruby/object:Gem::Dependency
|
|
33
|
+
name: railties
|
|
34
|
+
requirement: !ruby/object:Gem::Requirement
|
|
35
|
+
requirements:
|
|
36
|
+
- - ">="
|
|
37
|
+
- !ruby/object:Gem::Version
|
|
38
|
+
version: '7.2'
|
|
39
|
+
- - "<"
|
|
40
|
+
- !ruby/object:Gem::Version
|
|
41
|
+
version: '9'
|
|
42
|
+
type: :runtime
|
|
43
|
+
prerelease: false
|
|
44
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
45
|
+
requirements:
|
|
46
|
+
- - ">="
|
|
47
|
+
- !ruby/object:Gem::Version
|
|
48
|
+
version: '7.2'
|
|
49
|
+
- - "<"
|
|
50
|
+
- !ruby/object:Gem::Version
|
|
51
|
+
version: '9'
|
|
52
|
+
description: |
|
|
53
|
+
lethe finds every table and column that points at a model such as User,
|
|
54
|
+
from foreign keys, associations and column names, and checks that each one
|
|
55
|
+
has a declared policy for when that user leaves: destroy, scrub, keep or
|
|
56
|
+
ignore. It reads the schema only and never touches your data.
|
|
57
|
+
executables: []
|
|
58
|
+
extensions: []
|
|
59
|
+
extra_rdoc_files: []
|
|
60
|
+
files:
|
|
61
|
+
- CHANGELOG.md
|
|
62
|
+
- LICENSE
|
|
63
|
+
- README.md
|
|
64
|
+
- lib/generators/lethe/install/install_generator.rb
|
|
65
|
+
- lib/generators/lethe/install/templates/create_lethe_erasures.rb.tt
|
|
66
|
+
- lib/generators/lethe/install/templates/lethe.rb.tt
|
|
67
|
+
- lib/lethe.rb
|
|
68
|
+
- lib/lethe/checker.rb
|
|
69
|
+
- lib/lethe/configuration.rb
|
|
70
|
+
- lib/lethe/definition.rb
|
|
71
|
+
- lib/lethe/discovery.rb
|
|
72
|
+
- lib/lethe/enums.rb
|
|
73
|
+
- lib/lethe/eraser.rb
|
|
74
|
+
- lib/lethe/exporter.rb
|
|
75
|
+
- lib/lethe/formatters.rb
|
|
76
|
+
- lib/lethe/ledger.rb
|
|
77
|
+
- lib/lethe/leftovers.rb
|
|
78
|
+
- lib/lethe/locales/en.yml
|
|
79
|
+
- lib/lethe/locales/ja.yml
|
|
80
|
+
- lib/lethe/locator.rb
|
|
81
|
+
- lib/lethe/plan.rb
|
|
82
|
+
- lib/lethe/policy.rb
|
|
83
|
+
- lib/lethe/railtie.rb
|
|
84
|
+
- lib/lethe/reference.rb
|
|
85
|
+
- lib/lethe/report.rb
|
|
86
|
+
- lib/lethe/result.rb
|
|
87
|
+
- lib/lethe/rows.rb
|
|
88
|
+
- lib/lethe/rules.rb
|
|
89
|
+
- lib/lethe/runner.rb
|
|
90
|
+
- lib/lethe/sweeper.rb
|
|
91
|
+
- lib/lethe/target.rb
|
|
92
|
+
- lib/lethe/tasks/lethe.rake
|
|
93
|
+
- lib/lethe/version.rb
|
|
94
|
+
homepage: https://github.com/kaibyaku-jp/lethe
|
|
95
|
+
licenses:
|
|
96
|
+
- MIT
|
|
97
|
+
metadata:
|
|
98
|
+
source_code_uri: https://github.com/kaibyaku-jp/lethe
|
|
99
|
+
changelog_uri: https://github.com/kaibyaku-jp/lethe/blob/main/CHANGELOG.md
|
|
100
|
+
rubygems_mfa_required: 'true'
|
|
101
|
+
rdoc_options: []
|
|
102
|
+
require_paths:
|
|
103
|
+
- lib
|
|
104
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
105
|
+
requirements:
|
|
106
|
+
- - ">="
|
|
107
|
+
- !ruby/object:Gem::Version
|
|
108
|
+
version: '3.4'
|
|
109
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
110
|
+
requirements:
|
|
111
|
+
- - ">="
|
|
112
|
+
- !ruby/object:Gem::Version
|
|
113
|
+
version: '0'
|
|
114
|
+
requirements: []
|
|
115
|
+
rubygems_version: 3.6.9
|
|
116
|
+
specification_version: 4
|
|
117
|
+
summary: Declare what happens to each table when a user leaves, and catch the ones
|
|
118
|
+
you forgot
|
|
119
|
+
test_files: []
|