Module: Clickwrap::SchemaRequirements
- Defined in:
- lib/clickwrap/schema_requirements.rb
Overview
Which optional tables this installation needs, and whether it has them.
Seven of the seventeen tables this gem knows about cannot receive a row
until a matching configuration is turned on, and every one of those is off
by default. clickwrap:install therefore emits only the reachable ones, and
each capability comes with its own generator flag.
That trade has exactly one failure mode: a host turns the capability on and forgets the migration. It must not be discovered by a NoMethodError inside a capture at 3am, so it is discovered at boot, reported by the doctor, and refused at the entry points nothing else covers — always with the exact command that fixes it.
Defined Under Namespace
Classes: Feature
Constant Summary collapse
- FEATURES =
[ Feature.new( key: :persisted_presentations, tables: %w[clickwrap_presentations].freeze, flag: "--with-persisted-presentations", because: "A policy declares `persist_presentations_before_submission_for`, so Clickwrap " \ "is expected to keep the presentation it offered before the person submitted." ), Feature.new( key: :request_evidence, tables: %w[clickwrap_request_evidence].freeze, flag: "--with-request-evidence", because: "This installation records request evidence — an IP address, a browser " \ "user-agent, or provider-estimated IP geolocation." ), Feature.new( key: :integrity, tables: %w[clickwrap_chain_heads clickwrap_integrity_attestations].freeze, flag: "--with-integrity", because: "This installation configures event chaining, external anchoring, or " \ "third-party timestamping." ), Feature.new( key: :retention_ops, tables: %w[clickwrap_legal_holds clickwrap_disposition_plans].freeze, flag: "--with-retention-ops", because: "Legal holds and reviewed disposition are the operator tooling for deleting " \ "evidence on schedule and pausing that schedule." ), Feature.new( key: :external_actions, tables: %w[clickwrap_external_actions].freeze, flag: "--with-external-actions", because: "`Clickwrap.authorize_external_action!` commits a pending outbox row in the " \ "same transaction as the evidence that authorizes it." ) ].freeze
- FEATURES_BY_KEY =
FEATURES.index_by(&:key).freeze
Class Method Summary collapse
-
.available?(key) ⇒ Boolean
"Is it safe to query this capability's tables?" — false only when the answer is a definite no.
- .feature!(key) ⇒ Object
-
.installed?(feature) ⇒ Boolean
true, false, or nil when there is no database to ask.
-
.missing_for_configuration ⇒ Object
Features whose tables are absent, among the ones the CONFIGURATION says this installation uses.
-
.pending_migrations? ⇒ Boolean
migration_context.migrations_pathsis relative to the application root, and nothing guarantees the process is running from there — a rake task, a test suite, and a console all disagree. -
.require!(key) ⇒ Object
Raised at the entry point of a capability nothing in the configuration announces.
- .reset! ⇒ Object
-
.schema_is_settled? ⇒ Boolean
False when migrations are pending, when Clickwrap's own core tables are not there yet, and when the question cannot be answered at all.
Class Method Details
.available?(key) ⇒ Boolean
"Is it safe to query this capability's tables?" — false only when the answer is a definite no. An unanswerable question is not a no: code that skipped work because a connection was not up yet would skip it silently.
119 120 121 |
# File 'lib/clickwrap/schema_requirements.rb', line 119 def available?(key) installed?(feature!(key)) != false end |
.feature!(key) ⇒ Object
69 |
# File 'lib/clickwrap/schema_requirements.rb', line 69 def feature!(key) = FEATURES_BY_KEY.fetch(key) |
.installed?(feature) ⇒ Boolean
true, false, or nil when there is no database to ask. Memoized per feature and cleared with the rest of the global state, because asking the connection on every capture would be a query per event.
136 137 138 139 140 141 142 143 144 145 146 147 |
# File 'lib/clickwrap/schema_requirements.rb', line 136 def installed?(feature) cache = (@installed ||= {}) return cache[feature.key] if cache.key?(feature.key) answers = feature.tables.map { |table| table_presence(table) } # One unanswerable table makes the whole answer unknown. Folding nil # into false here would report "not installed" for every application # whose connection was not up yet, which is most of them at boot. cache[feature.key] = answers.any?(&:nil?) ? nil : answers.all? rescue StandardError cache[feature.key] = nil end |
.missing_for_configuration ⇒ Object
Features whose tables are absent, among the ones the CONFIGURATION says
this installation uses. Retention operations and external actions are
not configured — they are called — so they are checked at their entry
points instead, through require!.
Empty while migrations are pending, and that is the whole point: rails db:migrate boots the application before it runs the migration that
would satisfy this check, so a boot-time raise here would make the fix
unrunnable. An installation mid-migration is not a misconfigured one.
80 81 82 83 84 85 86 |
# File 'lib/clickwrap/schema_requirements.rb', line 80 def missing_for_configuration return [] unless schema_is_settled? %i[persisted_presentations request_evidence integrity] .map { |key| feature!(key) } .select { |feature| configured?(feature.key) && installed?(feature) == false } end |
.pending_migrations? ⇒ Boolean
migration_context.migrations_paths is relative to the application
root, and nothing guarantees the process is running from there — a rake
task, a test suite, and a console all disagree. Resolving against
Rails.root is what makes the answer the same from all three.
105 106 107 108 109 110 111 112 113 114 |
# File 'lib/clickwrap/schema_requirements.rb', line 105 def pending_migrations? context = ::ActiveRecord::Base.connection_pool.migration_context root = (::Rails.root if defined?(::Rails) && ::Rails.respond_to?(:root)) paths = Array(context.migrations_paths).map do |path| root && !Pathname.new(path).absolute? ? root.join(path).to_s : path end ::ActiveRecord::MigrationContext.new(paths).needs_migration? end |
.require!(key) ⇒ Object
Raised at the entry point of a capability nothing in the configuration
announces. installed? returning nil means the question could not be
asked (no database yet), and an unanswerable question is not a refusal.
126 127 128 129 130 131 |
# File 'lib/clickwrap/schema_requirements.rb', line 126 def require!(key) feature = feature!(key) return if installed?(feature) != false raise ConfigurationError, feature.explanation end |
.reset! ⇒ Object
149 150 151 |
# File 'lib/clickwrap/schema_requirements.rb', line 149 def reset! @installed = nil end |
.schema_is_settled? ⇒ Boolean
False when migrations are pending, when Clickwrap's own core tables are not there yet, and when the question cannot be answered at all. Each of those is an installation part-way through being set up, and the fix for a missing table is a migration — so refusing to boot before it can run would make the fix unreachable.
93 94 95 96 97 98 99 |
# File 'lib/clickwrap/schema_requirements.rb', line 93 def schema_is_settled? return false if table_presence("clickwrap_events") != true !pending_migrations? rescue StandardError false end |