Module: ConsoleThinkTwice

Defined in:
lib/console_think_twice.rb,
lib/console_think_twice/railtie.rb,
lib/console_think_twice/version.rb,
lib/console_think_twice/record_guard.rb,
lib/console_think_twice/configuration.rb,
lib/console_think_twice/relation_guard.rb,
lib/console_think_twice/collection_guard.rb

Overview

Confirmation prompts for destructive Active Record calls typed into a console.

>> User.first.destroy!
This will permanently destroy User #1 in production.
Cascades to: sessions, company_memberships, comments, api_keys
Confirm? (y/N)

Every guarded call takes force: true to skip the prompt. In a Rails app the railtie installs this on console boot only, so the web app, jobs and rails runner are untouched.

Defined Under Namespace

Modules: CollectionGuard, RecordGuard, RelationGuard Classes: Configuration, Railtie

Constant Summary collapse

Aborted =

Raised when a destructive call is declined, or when there is nobody to ask.

Class.new(StandardError)
CASCADING_STRATEGIES =
%i[destroy destroy_async delete_all].freeze
SUPPRESSION_KEY =
:console_think_twice_suppressed
RAILS_SOURCE =

Source files belonging to Rails itself. Matched on the path rather than resolved from Gem.loaded_specs so that a vendored or git-checkout Rails is recognised too.

%r{/active_(?:record|support|model)/}
RAILS_INTERNAL_WORK =

The Active Record internals that destroy a record as one step of a larger operation: association bookkeeping, autosave, and nested attributes.

%r{/active_record/(?:associations/|autosave_association\.rb|nested_attributes\.rb)}
GEM_SOURCE =

This gem's own frames, skipped when looking for the caller of a guarded method.

__dir__
VERSION =
"0.2.0"

Class Method Summary collapse

Class Method Details

.active?Boolean

Returns:

  • (Boolean)


74
75
76
# File 'lib/console_think_twice.rb', line 74

def active?
  !!@active && configuration.enabled?
end

.configurationObject



40
41
42
# File 'lib/console_think_twice.rb', line 40

def configuration
  @configuration ||= Configuration.new
end

.configure {|configuration| ... } ⇒ Object

Yields:



44
45
46
# File 'lib/console_think_twice.rb', line 44

def configure
  yield configuration
end

.disable!Object



66
67
68
# File 'lib/console_think_twice.rb', line 66

def disable!
  @active = false
end

.enable!Object



70
71
72
# File 'lib/console_think_twice.rb', line 70

def enable!
  @active = true
end

.guard(target, action:, force:, count: nil) ⇒ Object

Confirms the call, then runs it with nested prompts suppressed so that a dependent: cascade, or a relation destroying its records one by one, only ever asks once.

count is passed when the caller already knows how many records are involved, which saves a COUNT and is the only way to size a call like books.destroy(a, b) that names its records rather than describing them.



84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
# File 'lib/console_think_twice.rb', line 84

def guard(target, action:, force:, count: nil)
  return yield if !active? || suppressed?

  # Active Record's own work is confirmed by whatever the user typed to set it off, so it
  # runs suppressed rather than merely unguarded: `destroy!` re-enters as `destroy`, and
  # that second hop is dispatched from persistence.rb, where nothing marks it as internal.
  return suppressed { yield } if nested_in_active_record?(target)

  unless force
    count ||= affected_count(target)
    return yield if count.zero?

    confirm!(target, action, count)
  end

  suppressed { yield }
end

.install!Object



48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# File 'lib/console_think_twice.rb', line 48

def install!
  return false unless configuration.enabled?

  unless @installed
    ActiveRecord::Base.prepend(RecordGuard)
    # CollectionProxy overrides destroy_all and delete_all rather than inheriting them,
    # so guarding Relation alone would miss every has_many collection.
    ActiveRecord::Relation.prepend(RelationGuard)
    ActiveRecord::Associations::CollectionProxy.prepend(RelationGuard)
    ActiveRecord::Associations::CollectionProxy.prepend(CollectionGuard)
    @installed = true
  end

  @active = true
  announce
  true
end