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.
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: []