Class: ColdStorage::Policy

Inherits:
Object
  • Object
show all
Defined in:
lib/cold_storage/policy.rb

Overview

The archivable options of a single model, and the relation they describe.

archivable after: 18.months                  # older than 18 months
archivable after: 30.days, on: :closed_at    # ... measured on another column
archivable deleted: true                     # soft-deleted rows only
archivable deleted: true, after: 90.days     # soft-deleted 90+ days ago
archivable after: 1.year, every: 1.month     # ... and only run monthly
archivable after: 2.years, cascade: [:items] # take the children along
archivable after: 2.years, cascade: { items: [:taxes] } # ... and theirs
archivable on_destroy: true                  # catch hard deletes

Constant Summary collapse

OPTIONS =
%i[
  after older_than on deleted deleted_column every scope
  batch_size delete_after_archive delete_method cascade
  on_destroy hard_delete
].freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(model, **options) ⇒ Policy

Returns a new instance of Policy.



24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# File 'lib/cold_storage/policy.rb', line 24

def initialize(model, **options)
  unknown = options.keys - OPTIONS
  unless unknown.empty?
    raise InvalidPolicyError,
          "unknown archivable option(s) #{unknown.map(&:inspect).join(', ')} for #{model}. " \
          "Known options: #{OPTIONS.map(&:inspect).join(', ')}"
  end

  @model_name           = model.name
  @after                = normalize_duration(options[:after] || options[:older_than])
  @deleted              = options.fetch(:deleted, false)
  @deleted_column       = (options[:deleted_column] || ColdStorage.config.deleted_column).to_sym
  @on                   = (options[:on] || default_column).to_sym
  @every                = normalize_duration(options[:every])
  @scope                = options[:scope]
  @batch_size           = options[:batch_size]
  @delete_after_archive = options[:delete_after_archive]
  @delete_method        = options[:delete_method]
  @cascade              = AssociationTree.normalize(options[:cascade])
  @on_destroy           = options.fetch(:on_destroy) { options.fetch(:hard_delete, false) }

  validate!
end

Instance Attribute Details

#afterObject (readonly)

Returns the value of attribute after.



21
22
23
# File 'lib/cold_storage/policy.rb', line 21

def after
  @after
end

#batch_sizeObject (readonly)

Returns the value of attribute batch_size.



21
22
23
# File 'lib/cold_storage/policy.rb', line 21

def batch_size
  @batch_size
end

#cascadeObject (readonly)

Returns the value of attribute cascade.



21
22
23
# File 'lib/cold_storage/policy.rb', line 21

def cascade
  @cascade
end

#delete_methodObject (readonly)

Returns the value of attribute delete_method.



21
22
23
# File 'lib/cold_storage/policy.rb', line 21

def delete_method
  @delete_method
end

#deleted_columnObject (readonly)

Returns the value of attribute deleted_column.



21
22
23
# File 'lib/cold_storage/policy.rb', line 21

def deleted_column
  @deleted_column
end

#everyObject (readonly)

Returns the value of attribute every.



21
22
23
# File 'lib/cold_storage/policy.rb', line 21

def every
  @every
end

#model_nameObject (readonly)

Returns the value of attribute model_name.



21
22
23
# File 'lib/cold_storage/policy.rb', line 21

def model_name
  @model_name
end

#onObject (readonly)

Returns the value of attribute on.



21
22
23
# File 'lib/cold_storage/policy.rb', line 21

def on
  @on
end

#scopeObject (readonly)

Returns the value of attribute scope.



21
22
23
# File 'lib/cold_storage/policy.rb', line 21

def scope
  @scope
end

Class Method Details

.cascade_reflection!(klass, name) ⇒ ActiveRecord::Reflection::AssociationReflection

Returns:

  • (ActiveRecord::Reflection::AssociationReflection)

Raises:



129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
# File 'lib/cold_storage/policy.rb', line 129

def self.cascade_reflection!(klass, name)
  reflection = klass.reflect_on_association(name)
  raise InvalidPolicyError, "#{klass} has no association #{name.inspect} to cascade to" if reflection.nil?

  if reflection.through_reflection?
    raise InvalidPolicyError, "cascade does not support :through associations (#{klass}##{name})"
  end

  unless reflection.collection? || reflection.has_one?
    raise InvalidPolicyError,
          "cascade only supports has_many / has_one associations (#{klass}##{name} is a #{reflection.macro})"
  end

  reflection
end

Instance Method Details

#batch_size_valueObject



67
68
69
# File 'lib/cold_storage/policy.rb', line 67

def batch_size_value
  batch_size || ColdStorage.config.batch_size
end

#cascade_reflection!(klass, name) ⇒ Object



124
125
126
# File 'lib/cold_storage/policy.rb', line 124

def cascade_reflection!(klass, name)
  self.class.cascade_reflection!(klass, name)
end

#cutoff(now = Time.current) ⇒ Time?

Returns rows on the on: column older than this are archivable.

Returns:

  • (Time, nil)

    rows on the on: column older than this are archivable



95
96
97
98
99
# File 'lib/cold_storage/policy.rb', line 95

def cutoff(now = Time.current)
  return nil unless after

  now - after
end

#delete_after_archive?Boolean

Returns:

  • (Boolean)


71
72
73
# File 'lib/cold_storage/policy.rb', line 71

def delete_after_archive?
  @delete_after_archive.nil? ? ColdStorage.config.delete_after_archive : @delete_after_archive
end

#delete_method_valueObject



75
76
77
# File 'lib/cold_storage/policy.rb', line 75

def delete_method_value
  delete_method || ColdStorage.config.delete_method
end

#deleted?Boolean

Returns:

  • (Boolean)


48
49
50
# File 'lib/cold_storage/policy.rb', line 48

def deleted?
  @deleted
end

#due?(last_run_at) ⇒ Boolean

Has enough time passed since the last successful run?

Returns:

  • (Boolean)


102
103
104
105
106
# File 'lib/cold_storage/policy.rb', line 102

def due?(last_run_at)
  return true if every.nil? || last_run_at.nil?

  last_run_at <= Time.current - every
end

#modelObject



63
64
65
# File 'lib/cold_storage/policy.rb', line 63

def model
  @model_name.constantize
end

#on_destroy?Boolean

Copy the row to the archive when it is really destroyed.

Returns:

  • (Boolean)


53
54
55
# File 'lib/cold_storage/policy.rb', line 53

def on_destroy?
  @on_destroy
end

#relation(klass = model) ⇒ ActiveRecord::Relation

The rows that are archivable right now.

Always starts from unscoped: a soft-delete default scope would otherwise hide exactly the rows we are asked to archive.

Returns:

  • (ActiveRecord::Relation)


85
86
87
88
89
90
91
92
# File 'lib/cold_storage/policy.rb', line 85

def relation(klass = model)
  return klass.none unless sweeps?

  relation = klass.unscoped
  relation = relation.where.not(deleted_column => nil) if deleted?
  relation = relation.where(klass.arel_table[on].lt(cutoff)) if after
  apply_scope(relation)
end

#sweeps?Boolean

Does this policy describe rows a scheduled run should sweep up? on_destroy: true on its own does not: it only reacts to deletes.

Returns:

  • (Boolean)


59
60
61
# File 'lib/cold_storage/policy.rb', line 59

def sweeps?
  !after.nil? || deleted? || !scope.nil?
end

#to_sString

Returns one-line summary used by the status report.

Returns:

  • (String)

    one-line summary used by the status report



146
147
148
149
150
151
152
153
154
155
156
# File 'lib/cold_storage/policy.rb', line 146

def to_s
  parts = []
  parts << 'deleted' if deleted?
  parts << "#{on} < #{humanize_duration(after)} ago" if after
  parts << "scope: #{scope.is_a?(Symbol) ? scope : 'custom'}" if scope
  parts << 'on destroy' if on_destroy?
  parts << "every #{humanize_duration(every)}" if every && sweeps?
  parts << "cascade: #{cascade.keys.join(', ')}" if cascade.any?
  parts << 'keeps source rows' unless delete_after_archive?
  parts.join(', ')
end

#validate_against_schema!(klass = model) ⇒ Object

Checks the options against the real table. Raises with an actionable message instead of failing later with a SQL error.



110
111
112
113
114
115
116
117
118
119
120
121
122
# File 'lib/cold_storage/policy.rb', line 110

def validate_against_schema!(klass = model)
  columns = klass.column_names
  check_column!(klass, on, columns)
  check_column!(klass, deleted_column, columns) if deleted?

  if klass.primary_key.nil? || klass.primary_key.is_a?(Array)
    raise InvalidPolicyError,
          "#{klass} must have a single-column primary key to be archivable (got #{klass.primary_key.inspect})"
  end

  validate_cascade!(klass, cascade)
  true
end