better_model 1.0.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/MIT-LICENSE +20 -0
- data/README.md +312 -0
- data/Rakefile +12 -0
- data/lib/better_model/archivable.rb +272 -0
- data/lib/better_model/permissible.rb +151 -0
- data/lib/better_model/predicable.rb +481 -0
- data/lib/better_model/railtie.rb +4 -0
- data/lib/better_model/searchable.rb +524 -0
- data/lib/better_model/sortable.rb +217 -0
- data/lib/better_model/statusable.rb +154 -0
- data/lib/better_model/version.rb +3 -0
- data/lib/better_model.rb +24 -0
- data/lib/generators/better_model/archivable/archivable_generator.rb +55 -0
- data/lib/generators/better_model/archivable/templates/migration.rb.tt +26 -0
- data/lib/tasks/better_model_tasks.rake +4 -0
- metadata +82 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Sortable - Sistema di ordinamento dichiarativo per modelli Rails
|
|
4
|
+
#
|
|
5
|
+
# Questo concern permette di definire ordinamenti sui modelli utilizzando un DSL
|
|
6
|
+
# semplice e dichiarativo che genera automaticamente scope basati sul tipo di colonna.
|
|
7
|
+
#
|
|
8
|
+
# Esempio di utilizzo:
|
|
9
|
+
# class Article < ApplicationRecord
|
|
10
|
+
# include BetterModel::Sortable
|
|
11
|
+
#
|
|
12
|
+
# sort :title, :view_count, :published_at
|
|
13
|
+
# end
|
|
14
|
+
#
|
|
15
|
+
# Utilizzo:
|
|
16
|
+
# Article.sort_title_asc # ORDER BY title ASC
|
|
17
|
+
# Article.sort_title_desc_i # ORDER BY LOWER(title) DESC
|
|
18
|
+
# Article.sort_view_count_desc_nulls_last # ORDER BY view_count DESC NULLS LAST
|
|
19
|
+
# Article.sort_published_at_newest # ORDER BY published_at DESC
|
|
20
|
+
#
|
|
21
|
+
module BetterModel
|
|
22
|
+
module Sortable
|
|
23
|
+
extend ActiveSupport::Concern
|
|
24
|
+
|
|
25
|
+
included do
|
|
26
|
+
# Valida che sia incluso solo in modelli ActiveRecord
|
|
27
|
+
unless ancestors.include?(ActiveRecord::Base)
|
|
28
|
+
raise ArgumentError, "BetterModel::Sortable can only be included in ActiveRecord models"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Registry dei campi sortable definiti per questa classe
|
|
32
|
+
class_attribute :sortable_fields, default: Set.new
|
|
33
|
+
# Registry degli scope sortable generati
|
|
34
|
+
class_attribute :sortable_scopes, default: Set.new
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
class_methods do
|
|
38
|
+
# DSL per definire campi sortable
|
|
39
|
+
#
|
|
40
|
+
# Genera automaticamente scope di ordinamento basati sul tipo di colonna:
|
|
41
|
+
# - String: _asc, _desc, _asc_i, _desc_i (case-insensitive)
|
|
42
|
+
# - Numeric: _asc, _desc, _asc_nulls_last, _desc_nulls_last, etc.
|
|
43
|
+
# - Date: _asc, _desc, _newest, _oldest
|
|
44
|
+
#
|
|
45
|
+
# Esempio:
|
|
46
|
+
# sort :title, :view_count, :published_at
|
|
47
|
+
def sort(*field_names)
|
|
48
|
+
field_names.each do |field_name|
|
|
49
|
+
validate_sortable_field!(field_name)
|
|
50
|
+
register_sortable_field(field_name)
|
|
51
|
+
|
|
52
|
+
# Auto-rileva tipo e genera scope appropriati
|
|
53
|
+
column = columns_hash[field_name.to_s]
|
|
54
|
+
next unless column
|
|
55
|
+
|
|
56
|
+
case column.type
|
|
57
|
+
when :string, :text
|
|
58
|
+
define_string_sorting(field_name)
|
|
59
|
+
when :integer, :decimal, :float, :bigint
|
|
60
|
+
define_numeric_sorting(field_name)
|
|
61
|
+
when :date, :datetime, :time, :timestamp
|
|
62
|
+
define_date_sorting(field_name)
|
|
63
|
+
else
|
|
64
|
+
# Default: genera solo scope base
|
|
65
|
+
define_base_sorting(field_name)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Verifica se un campo è stato registrato come sortable
|
|
71
|
+
def sortable_field?(field_name)
|
|
72
|
+
sortable_fields.include?(field_name.to_sym)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Verifica se uno scope sortable è stato generato
|
|
76
|
+
def sortable_scope?(scope_name)
|
|
77
|
+
sortable_scopes.include?(scope_name.to_sym)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
# Valida che il campo esista nella tabella
|
|
83
|
+
def validate_sortable_field!(field_name)
|
|
84
|
+
unless column_names.include?(field_name.to_s)
|
|
85
|
+
raise ArgumentError, "Invalid field name: #{field_name}. Field does not exist in #{table_name}"
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Registra un campo nel registry sortable_fields
|
|
90
|
+
def register_sortable_field(field_name)
|
|
91
|
+
self.sortable_fields = (sortable_fields + [ field_name.to_sym ]).to_set.freeze
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Registra scope nel registry sortable_scopes
|
|
95
|
+
def register_sortable_scopes(*scope_names)
|
|
96
|
+
self.sortable_scopes = (sortable_scopes + scope_names.map(&:to_sym)).to_set.freeze
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Genera scope base: sort_field_asc e sort_field_desc
|
|
100
|
+
def define_base_sorting(field_name)
|
|
101
|
+
quoted_field = connection.quote_column_name(field_name)
|
|
102
|
+
|
|
103
|
+
scope :"sort_#{field_name}_asc", -> { order(Arel.sql("#{quoted_field} ASC")) }
|
|
104
|
+
scope :"sort_#{field_name}_desc", -> { order(Arel.sql("#{quoted_field} DESC")) }
|
|
105
|
+
|
|
106
|
+
register_sortable_scopes(
|
|
107
|
+
:"sort_#{field_name}_asc",
|
|
108
|
+
:"sort_#{field_name}_desc"
|
|
109
|
+
)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Genera scope per campi stringa (include case-insensitive)
|
|
113
|
+
def define_string_sorting(field_name)
|
|
114
|
+
quoted_field = connection.quote_column_name(field_name)
|
|
115
|
+
|
|
116
|
+
# Scope base
|
|
117
|
+
scope :"sort_#{field_name}_asc", -> { order(Arel.sql("#{quoted_field} ASC")) }
|
|
118
|
+
scope :"sort_#{field_name}_desc", -> { order(Arel.sql("#{quoted_field} DESC")) }
|
|
119
|
+
|
|
120
|
+
# Scope case-insensitive
|
|
121
|
+
scope :"sort_#{field_name}_asc_i", -> { order(Arel.sql("LOWER(#{quoted_field}) ASC")) }
|
|
122
|
+
scope :"sort_#{field_name}_desc_i", -> { order(Arel.sql("LOWER(#{quoted_field}) DESC")) }
|
|
123
|
+
|
|
124
|
+
register_sortable_scopes(
|
|
125
|
+
:"sort_#{field_name}_asc",
|
|
126
|
+
:"sort_#{field_name}_desc",
|
|
127
|
+
:"sort_#{field_name}_asc_i",
|
|
128
|
+
:"sort_#{field_name}_desc_i"
|
|
129
|
+
)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Genera scope per campi numerici (include gestione NULL)
|
|
133
|
+
def define_numeric_sorting(field_name)
|
|
134
|
+
quoted_field = connection.quote_column_name(field_name)
|
|
135
|
+
|
|
136
|
+
# Scope base
|
|
137
|
+
scope :"sort_#{field_name}_asc", -> { order(Arel.sql("#{quoted_field} ASC")) }
|
|
138
|
+
scope :"sort_#{field_name}_desc", -> { order(Arel.sql("#{quoted_field} DESC")) }
|
|
139
|
+
|
|
140
|
+
# Pre-calcola SQL per gestione NULL (necessario perché lo scope non ha accesso ai metodi privati)
|
|
141
|
+
sql_asc_nulls_last = nulls_order_sql(field_name, "ASC", "LAST")
|
|
142
|
+
sql_desc_nulls_last = nulls_order_sql(field_name, "DESC", "LAST")
|
|
143
|
+
sql_asc_nulls_first = nulls_order_sql(field_name, "ASC", "FIRST")
|
|
144
|
+
sql_desc_nulls_first = nulls_order_sql(field_name, "DESC", "FIRST")
|
|
145
|
+
|
|
146
|
+
# Scope con gestione NULL
|
|
147
|
+
scope :"sort_#{field_name}_asc_nulls_last", -> {
|
|
148
|
+
order(Arel.sql(sql_asc_nulls_last))
|
|
149
|
+
}
|
|
150
|
+
scope :"sort_#{field_name}_desc_nulls_last", -> {
|
|
151
|
+
order(Arel.sql(sql_desc_nulls_last))
|
|
152
|
+
}
|
|
153
|
+
scope :"sort_#{field_name}_asc_nulls_first", -> {
|
|
154
|
+
order(Arel.sql(sql_asc_nulls_first))
|
|
155
|
+
}
|
|
156
|
+
scope :"sort_#{field_name}_desc_nulls_first", -> {
|
|
157
|
+
order(Arel.sql(sql_desc_nulls_first))
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
register_sortable_scopes(
|
|
161
|
+
:"sort_#{field_name}_asc",
|
|
162
|
+
:"sort_#{field_name}_desc",
|
|
163
|
+
:"sort_#{field_name}_asc_nulls_last",
|
|
164
|
+
:"sort_#{field_name}_desc_nulls_last",
|
|
165
|
+
:"sort_#{field_name}_asc_nulls_first",
|
|
166
|
+
:"sort_#{field_name}_desc_nulls_first"
|
|
167
|
+
)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Genera scope per campi data/datetime (include shortcuts semantici)
|
|
171
|
+
def define_date_sorting(field_name)
|
|
172
|
+
quoted_field = connection.quote_column_name(field_name)
|
|
173
|
+
|
|
174
|
+
# Scope base
|
|
175
|
+
scope :"sort_#{field_name}_asc", -> { order(Arel.sql("#{quoted_field} ASC")) }
|
|
176
|
+
scope :"sort_#{field_name}_desc", -> { order(Arel.sql("#{quoted_field} DESC")) }
|
|
177
|
+
|
|
178
|
+
# Shortcuts semantici
|
|
179
|
+
scope :"sort_#{field_name}_newest", -> { order(Arel.sql("#{quoted_field} DESC")) }
|
|
180
|
+
scope :"sort_#{field_name}_oldest", -> { order(Arel.sql("#{quoted_field} ASC")) }
|
|
181
|
+
|
|
182
|
+
register_sortable_scopes(
|
|
183
|
+
:"sort_#{field_name}_asc",
|
|
184
|
+
:"sort_#{field_name}_desc",
|
|
185
|
+
:"sort_#{field_name}_newest",
|
|
186
|
+
:"sort_#{field_name}_oldest"
|
|
187
|
+
)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Genera SQL per gestione NULL multi-database
|
|
191
|
+
def nulls_order_sql(field_name, direction, nulls_position)
|
|
192
|
+
quoted_field = connection.quote_column_name(field_name)
|
|
193
|
+
|
|
194
|
+
# PostgreSQL e SQLite 3.30+ supportano NULLS LAST/FIRST nativamente
|
|
195
|
+
if connection.adapter_name.match?(/PostgreSQL|SQLite/)
|
|
196
|
+
"#{quoted_field} #{direction} NULLS #{nulls_position}"
|
|
197
|
+
else
|
|
198
|
+
# MySQL/MariaDB: emulazione con CASE
|
|
199
|
+
if nulls_position == "LAST"
|
|
200
|
+
"CASE WHEN #{quoted_field} IS NULL THEN 1 ELSE 0 END, #{quoted_field} #{direction}"
|
|
201
|
+
else # FIRST
|
|
202
|
+
"CASE WHEN #{quoted_field} IS NULL THEN 0 ELSE 1 END, #{quoted_field} #{direction}"
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Metodi di istanza
|
|
209
|
+
|
|
210
|
+
# Ritorna lista di attributi sortable (esclude campi sensibili)
|
|
211
|
+
def sortable_attributes
|
|
212
|
+
self.class.column_names.reject do |attr|
|
|
213
|
+
attr.start_with?("password", "encrypted_")
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Statusable - Sistema di stati dichiarativi per modelli Rails
|
|
4
|
+
#
|
|
5
|
+
# Questo concern permette di definire stati sui modelli utilizzando un DSL
|
|
6
|
+
# semplice e dichiarativo, simile al pattern Enrichable ma per gli stati.
|
|
7
|
+
#
|
|
8
|
+
# Esempio di utilizzo:
|
|
9
|
+
# class Communications::Consult < ApplicationRecord
|
|
10
|
+
# include BetterModel::Statusable
|
|
11
|
+
#
|
|
12
|
+
# is :pending, -> { status == 'initialized' }
|
|
13
|
+
# is :active_session, -> { status == 'active' && !expired? }
|
|
14
|
+
# is :expired, -> { expires_at.present? && expires_at <= Time.current }
|
|
15
|
+
# is :scheduled, -> { scheduled_at.present? }
|
|
16
|
+
# is :immediate, -> { scheduled_at.blank? }
|
|
17
|
+
# is :ready_to_start, -> { scheduled? && scheduled_at <= Time.current }
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
# Utilizzo:
|
|
21
|
+
# consult.is?(:pending) # => true/false
|
|
22
|
+
# consult.is_pending? # => true/false
|
|
23
|
+
# consult.is_active_session? # => true/false
|
|
24
|
+
# consult.is_expired? # => true/false
|
|
25
|
+
# consult.is_scheduled? # => true/false
|
|
26
|
+
#
|
|
27
|
+
module BetterModel
|
|
28
|
+
module Statusable
|
|
29
|
+
extend ActiveSupport::Concern
|
|
30
|
+
|
|
31
|
+
included do
|
|
32
|
+
# Registry degli stati definiti per questa classe
|
|
33
|
+
class_attribute :is_definitions
|
|
34
|
+
self.is_definitions = {}
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
class_methods do
|
|
38
|
+
# DSL per definire stati
|
|
39
|
+
#
|
|
40
|
+
# Parametri:
|
|
41
|
+
# - status_name: simbolo che rappresenta lo stato (es. :pending, :active)
|
|
42
|
+
# - condition_proc: lambda o proc che definisce la condizione
|
|
43
|
+
# - block: blocco alternativo alla condition_proc
|
|
44
|
+
#
|
|
45
|
+
# Esempi:
|
|
46
|
+
# is :pending, -> { status == 'initialized' }
|
|
47
|
+
# is :expired, -> { expires_at.present? && expires_at <= Time.current }
|
|
48
|
+
# is :ready do
|
|
49
|
+
# scheduled_at.present? && scheduled_at <= Time.current
|
|
50
|
+
# end
|
|
51
|
+
def is(status_name, condition_proc = nil, &block)
|
|
52
|
+
# Valida i parametri prima di convertire
|
|
53
|
+
raise ArgumentError, "Status name cannot be blank" if status_name.blank?
|
|
54
|
+
|
|
55
|
+
status_name = status_name.to_sym
|
|
56
|
+
condition = condition_proc || block
|
|
57
|
+
raise ArgumentError, "Condition proc or block is required" unless condition
|
|
58
|
+
raise ArgumentError, "Condition must respond to call" unless condition.respond_to?(:call)
|
|
59
|
+
|
|
60
|
+
# Registra lo stato nel registry
|
|
61
|
+
self.is_definitions = is_definitions.merge(status_name => condition.freeze).freeze
|
|
62
|
+
|
|
63
|
+
# Genera il metodo dinamico is_#{status_name}?
|
|
64
|
+
define_is_method(status_name)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Lista di tutti gli stati definiti per questa classe
|
|
68
|
+
def defined_statuses
|
|
69
|
+
is_definitions.keys
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Verifica se uno stato è definito
|
|
73
|
+
def status_defined?(status_name)
|
|
74
|
+
is_definitions.key?(status_name.to_sym)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
# Genera dinamicamente il metodo is_#{status_name}? per ogni stato definito
|
|
80
|
+
def define_is_method(status_name)
|
|
81
|
+
method_name = "is_#{status_name}?"
|
|
82
|
+
|
|
83
|
+
# Evita di ridefinire metodi se già esistono
|
|
84
|
+
return if method_defined?(method_name)
|
|
85
|
+
|
|
86
|
+
define_method(method_name) do
|
|
87
|
+
is?(status_name)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Metodo generico per verificare se uno stato è attivo
|
|
93
|
+
#
|
|
94
|
+
# Parametri:
|
|
95
|
+
# - status_name: simbolo dello stato da verificare
|
|
96
|
+
#
|
|
97
|
+
# Ritorna:
|
|
98
|
+
# - true se lo stato è attivo
|
|
99
|
+
# - false se lo stato non è attivo o non è definito
|
|
100
|
+
#
|
|
101
|
+
# Esempio:
|
|
102
|
+
# consult.is?(:pending)
|
|
103
|
+
def is?(status_name)
|
|
104
|
+
status_name = status_name.to_sym
|
|
105
|
+
condition = self.class.is_definitions[status_name]
|
|
106
|
+
|
|
107
|
+
# Se lo stato non è definito, ritorna false (secure by default)
|
|
108
|
+
return false unless condition
|
|
109
|
+
|
|
110
|
+
# Valuta la condizione nel contesto dell'istanza del modello
|
|
111
|
+
# Gli errori si propagano naturalmente - fail fast
|
|
112
|
+
instance_exec(&condition)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Ritorna tutti gli stati disponibili per questa istanza con i loro valori
|
|
116
|
+
#
|
|
117
|
+
# Ritorna:
|
|
118
|
+
# - Hash con chiavi simbolo (stati) e valori booleani (attivi/inattivi)
|
|
119
|
+
#
|
|
120
|
+
# Esempio:
|
|
121
|
+
# consult.statuses
|
|
122
|
+
# # => { pending: true, active: false, expired: false, scheduled: true }
|
|
123
|
+
def statuses
|
|
124
|
+
self.class.is_definitions.each_with_object({}) do |(status_name, _condition), result|
|
|
125
|
+
result[status_name] = is?(status_name)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Verifica se l'istanza ha almeno uno stato attivo
|
|
130
|
+
def has_any_status?
|
|
131
|
+
statuses.values.any?
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Verifica se l'istanza ha tutti gli stati specificati attivi
|
|
135
|
+
def has_all_statuses?(status_names)
|
|
136
|
+
Array(status_names).all? { |status_name| is?(status_name) }
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Filtra una lista di stati restituendo solo quelli attivi
|
|
140
|
+
def active_statuses(status_names)
|
|
141
|
+
Array(status_names).select { |status_name| is?(status_name) }
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Override di as_json per includere automaticamente gli stati se richiesto
|
|
145
|
+
def as_json(options = {})
|
|
146
|
+
result = super
|
|
147
|
+
|
|
148
|
+
# Include gli stati se esplicitamente richiesto, converting symbol keys to strings
|
|
149
|
+
result["statuses"] = statuses.transform_keys(&:to_s) if options[:include_statuses]
|
|
150
|
+
|
|
151
|
+
result
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
data/lib/better_model.rb
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
require "better_model/version"
|
|
2
|
+
require "better_model/railtie"
|
|
3
|
+
require "better_model/statusable"
|
|
4
|
+
require "better_model/permissible"
|
|
5
|
+
require "better_model/sortable"
|
|
6
|
+
require "better_model/predicable"
|
|
7
|
+
require "better_model/searchable"
|
|
8
|
+
require "better_model/archivable"
|
|
9
|
+
|
|
10
|
+
module BetterModel
|
|
11
|
+
extend ActiveSupport::Concern
|
|
12
|
+
|
|
13
|
+
# When BetterModel is included, automatically include all sub-concerns
|
|
14
|
+
included do
|
|
15
|
+
include BetterModel::Statusable
|
|
16
|
+
include BetterModel::Permissible
|
|
17
|
+
include BetterModel::Sortable
|
|
18
|
+
include BetterModel::Predicable
|
|
19
|
+
include BetterModel::Searchable
|
|
20
|
+
include BetterModel::Archivable
|
|
21
|
+
# Future concerns will be added here:
|
|
22
|
+
# include BetterModel::Validatable
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
require "rails/generators/migration"
|
|
5
|
+
|
|
6
|
+
module BetterModel
|
|
7
|
+
module Generators
|
|
8
|
+
class ArchivableGenerator < Rails::Generators::NamedBase
|
|
9
|
+
include Rails::Generators::Migration
|
|
10
|
+
|
|
11
|
+
source_root File.expand_path("templates", __dir__)
|
|
12
|
+
|
|
13
|
+
class_option :with_tracking, type: :boolean, default: false,
|
|
14
|
+
desc: "Add archived_by_id and archive_reason columns"
|
|
15
|
+
class_option :with_by, type: :boolean, default: false,
|
|
16
|
+
desc: "Add archived_by_id column only"
|
|
17
|
+
class_option :with_reason, type: :boolean, default: false,
|
|
18
|
+
desc: "Add archive_reason column only"
|
|
19
|
+
class_option :skip_indexes, type: :boolean, default: false,
|
|
20
|
+
desc: "Skip adding indexes"
|
|
21
|
+
|
|
22
|
+
def self.next_migration_number(dirname)
|
|
23
|
+
next_migration_number = current_migration_number(dirname) + 1
|
|
24
|
+
ActiveRecord::Migration.next_migration_number(next_migration_number)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def create_migration_file
|
|
28
|
+
migration_template "migration.rb.tt",
|
|
29
|
+
"db/migrate/add_archivable_to_#{table_name}.rb"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def table_name
|
|
35
|
+
name.tableize
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def migration_class_name
|
|
39
|
+
"AddArchivableTo#{name.camelize.pluralize}"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def with_by_column?
|
|
43
|
+
options[:with_tracking] || options[:with_by]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def with_reason_column?
|
|
47
|
+
options[:with_tracking] || options[:with_reason]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def add_indexes?
|
|
51
|
+
!options[:skip_indexes]
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
|
|
2
|
+
def change
|
|
3
|
+
change_table :<%= table_name %> do |t|
|
|
4
|
+
# Required column for archivable
|
|
5
|
+
t.datetime :archived_at
|
|
6
|
+
<% if with_by_column? -%>
|
|
7
|
+
|
|
8
|
+
# Optional: track who archived the record
|
|
9
|
+
t.integer :archived_by_id
|
|
10
|
+
<% end -%>
|
|
11
|
+
<% if with_reason_column? -%>
|
|
12
|
+
|
|
13
|
+
# Optional: track why the record was archived
|
|
14
|
+
t.string :archive_reason
|
|
15
|
+
<% end -%>
|
|
16
|
+
end
|
|
17
|
+
<% if add_indexes? -%>
|
|
18
|
+
|
|
19
|
+
# Indexes for performance
|
|
20
|
+
add_index :<%= table_name %>, :archived_at
|
|
21
|
+
<% if with_by_column? -%>
|
|
22
|
+
add_index :<%= table_name %>, :archived_by_id
|
|
23
|
+
<% end -%>
|
|
24
|
+
<% end -%>
|
|
25
|
+
end
|
|
26
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: better_model
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- alessiobussolari
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2025-10-29 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: rails
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: 8.1.0
|
|
20
|
+
- - "<"
|
|
21
|
+
- !ruby/object:Gem::Version
|
|
22
|
+
version: '9.0'
|
|
23
|
+
type: :runtime
|
|
24
|
+
prerelease: false
|
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
26
|
+
requirements:
|
|
27
|
+
- - ">="
|
|
28
|
+
- !ruby/object:Gem::Version
|
|
29
|
+
version: 8.1.0
|
|
30
|
+
- - "<"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '9.0'
|
|
33
|
+
description: BetterModel is a Rails engine gem (Rails 8.1+) that provides powerful
|
|
34
|
+
extensions for ActiveRecord models including declarative status management and more.
|
|
35
|
+
email:
|
|
36
|
+
- alessio@cosmic.tech
|
|
37
|
+
executables: []
|
|
38
|
+
extensions: []
|
|
39
|
+
extra_rdoc_files: []
|
|
40
|
+
files:
|
|
41
|
+
- MIT-LICENSE
|
|
42
|
+
- README.md
|
|
43
|
+
- Rakefile
|
|
44
|
+
- lib/better_model.rb
|
|
45
|
+
- lib/better_model/archivable.rb
|
|
46
|
+
- lib/better_model/permissible.rb
|
|
47
|
+
- lib/better_model/predicable.rb
|
|
48
|
+
- lib/better_model/railtie.rb
|
|
49
|
+
- lib/better_model/searchable.rb
|
|
50
|
+
- lib/better_model/sortable.rb
|
|
51
|
+
- lib/better_model/statusable.rb
|
|
52
|
+
- lib/better_model/version.rb
|
|
53
|
+
- lib/generators/better_model/archivable/archivable_generator.rb
|
|
54
|
+
- lib/generators/better_model/archivable/templates/migration.rb.tt
|
|
55
|
+
- lib/tasks/better_model_tasks.rake
|
|
56
|
+
homepage: https://github.com/alessiobussolari/better_model
|
|
57
|
+
licenses:
|
|
58
|
+
- MIT
|
|
59
|
+
metadata:
|
|
60
|
+
allowed_push_host: https://rubygems.org
|
|
61
|
+
source_code_uri: https://github.com/alessiobussolari/better_model
|
|
62
|
+
changelog_uri: https://github.com/alessiobussolari/better_model/blob/main/CHANGELOG.md
|
|
63
|
+
post_install_message:
|
|
64
|
+
rdoc_options: []
|
|
65
|
+
require_paths:
|
|
66
|
+
- lib
|
|
67
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
68
|
+
requirements:
|
|
69
|
+
- - ">="
|
|
70
|
+
- !ruby/object:Gem::Version
|
|
71
|
+
version: 3.0.0
|
|
72
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
73
|
+
requirements:
|
|
74
|
+
- - ">="
|
|
75
|
+
- !ruby/object:Gem::Version
|
|
76
|
+
version: '0'
|
|
77
|
+
requirements: []
|
|
78
|
+
rubygems_version: 3.5.11
|
|
79
|
+
signing_key:
|
|
80
|
+
specification_version: 4
|
|
81
|
+
summary: Rails engine gem that extends ActiveRecord model functionality
|
|
82
|
+
test_files: []
|