rodauth-tools 0.3.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/.gitignore +93 -0
- data/.gitlint +9 -0
- data/.markdownlint-cli2.jsonc +26 -0
- data/.pre-commit-config.yaml +46 -0
- data/.rubocop.yml +18 -0
- data/.rubocop_todo.yml +243 -0
- data/CHANGELOG.md +81 -0
- data/CLAUDE.md +262 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/CONTRIBUTING.md +111 -0
- data/Gemfile +35 -0
- data/Gemfile.lock +356 -0
- data/LICENSE.txt +21 -0
- data/README.md +339 -0
- data/Rakefile +8 -0
- data/lib/rodauth/features/external_identity.rb +946 -0
- data/lib/rodauth/features/hmac_secret_guard.rb +119 -0
- data/lib/rodauth/features/jwt_secret_guard.rb +120 -0
- data/lib/rodauth/features/table_guard.rb +937 -0
- data/lib/rodauth/sequel_generator.rb +531 -0
- data/lib/rodauth/table_inspector.rb +124 -0
- data/lib/rodauth/template_inspector.rb +134 -0
- data/lib/rodauth/tools/console_helpers.rb +158 -0
- data/lib/rodauth/tools/migration/sequel/account_expiration.erb +9 -0
- data/lib/rodauth/tools/migration/sequel/active_sessions.erb +10 -0
- data/lib/rodauth/tools/migration/sequel/audit_logging.erb +12 -0
- data/lib/rodauth/tools/migration/sequel/base.erb +41 -0
- data/lib/rodauth/tools/migration/sequel/disallow_password_reuse.erb +8 -0
- data/lib/rodauth/tools/migration/sequel/email_auth.erb +17 -0
- data/lib/rodauth/tools/migration/sequel/jwt_refresh.erb +18 -0
- data/lib/rodauth/tools/migration/sequel/lockout.erb +21 -0
- data/lib/rodauth/tools/migration/sequel/otp.erb +9 -0
- data/lib/rodauth/tools/migration/sequel/otp_unlock.erb +8 -0
- data/lib/rodauth/tools/migration/sequel/password_expiration.erb +7 -0
- data/lib/rodauth/tools/migration/sequel/recovery_codes.erb +8 -0
- data/lib/rodauth/tools/migration/sequel/remember.erb +16 -0
- data/lib/rodauth/tools/migration/sequel/reset_password.erb +17 -0
- data/lib/rodauth/tools/migration/sequel/single_session.erb +7 -0
- data/lib/rodauth/tools/migration/sequel/sms_codes.erb +10 -0
- data/lib/rodauth/tools/migration/sequel/verify_account.erb +9 -0
- data/lib/rodauth/tools/migration/sequel/verify_login_change.erb +17 -0
- data/lib/rodauth/tools/migration/sequel/webauthn.erb +15 -0
- data/lib/rodauth/tools/migration.rb +188 -0
- data/lib/rodauth/tools/version.rb +9 -0
- data/lib/rodauth/tools.rb +29 -0
- data/package-lock.json +500 -0
- data/package.json +11 -0
- data/rodauth-tools.gemspec +40 -0
- metadata +136 -0
|
@@ -0,0 +1,937 @@
|
|
|
1
|
+
# lib/rodauth/features/table_guard.rb
|
|
2
|
+
#
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
# Ensure dependencies are loaded (they should be via require 'rodauth/tools')
|
|
6
|
+
require_relative '../table_inspector' unless defined?(Rodauth::TableInspector)
|
|
7
|
+
require_relative '../sequel_generator' unless defined?(Rodauth::SequelGenerator)
|
|
8
|
+
|
|
9
|
+
#
|
|
10
|
+
# Enable with:
|
|
11
|
+
# enable :table_guard
|
|
12
|
+
#
|
|
13
|
+
# Configuration:
|
|
14
|
+
# table_guard_mode :warn # :warn, :error, :silent, :skip, :raise, :halt/:exit, or block
|
|
15
|
+
# table_guard_sequel_mode :log # :log, :migration, :create, :sync
|
|
16
|
+
# table_guard_skip_tables [:some_table] # Skip checking specific tables
|
|
17
|
+
#
|
|
18
|
+
# Logging:
|
|
19
|
+
# def logger; MyLogger; end # Standard Rodauth logger (recommended)
|
|
20
|
+
# table_guard_logger MyLogger # Feature-specific logger (alternative)
|
|
21
|
+
#
|
|
22
|
+
# Example modes:
|
|
23
|
+
#
|
|
24
|
+
# # Warn but continue
|
|
25
|
+
# table_guard_mode :warn
|
|
26
|
+
#
|
|
27
|
+
# # Error log but continue
|
|
28
|
+
# table_guard_mode :error
|
|
29
|
+
#
|
|
30
|
+
# # Raise exception for handling upstream
|
|
31
|
+
# table_guard_mode :raise
|
|
32
|
+
#
|
|
33
|
+
# # Halt/exit startup (not recommended for multi-tenant)
|
|
34
|
+
# table_guard_mode :halt
|
|
35
|
+
#
|
|
36
|
+
# # Custom handling with block
|
|
37
|
+
# table_guard_mode do |missing, config|
|
|
38
|
+
# TenantLogger.log_missing_tables(current_tenant, missing)
|
|
39
|
+
# end
|
|
40
|
+
#
|
|
41
|
+
# Sequel generation modes:
|
|
42
|
+
#
|
|
43
|
+
# # Log migration code to logger
|
|
44
|
+
# table_guard_sequel_mode :log
|
|
45
|
+
#
|
|
46
|
+
# # Generate migration file
|
|
47
|
+
# table_guard_sequel_mode :migration
|
|
48
|
+
#
|
|
49
|
+
# # Create tables immediately (JIT)
|
|
50
|
+
# table_guard_sequel_mode :create
|
|
51
|
+
#
|
|
52
|
+
# # Drop and recreate missing tables (dev/test only)
|
|
53
|
+
# table_guard_sequel_mode :sync
|
|
54
|
+
#
|
|
55
|
+
# # Drop and recreate ALL tables every startup (dev/test only)
|
|
56
|
+
# table_guard_sequel_mode :recreate
|
|
57
|
+
|
|
58
|
+
module Rodauth
|
|
59
|
+
Feature.define(:table_guard, :TableGuard) do
|
|
60
|
+
# Configuration methods
|
|
61
|
+
auth_value_method :table_guard_mode, nil
|
|
62
|
+
auth_value_method :table_guard_sequel_mode, nil
|
|
63
|
+
auth_value_method :table_guard_skip_tables, []
|
|
64
|
+
auth_value_method :table_guard_check_columns?, true
|
|
65
|
+
auth_value_method :table_guard_migration_path, 'db/migrate'
|
|
66
|
+
auth_value_method :table_guard_logger, nil
|
|
67
|
+
auth_value_method :table_guard_logger_name, nil # For SemanticLogger integration
|
|
68
|
+
|
|
69
|
+
# Public API methods
|
|
70
|
+
auth_methods(
|
|
71
|
+
:check_required_tables!,
|
|
72
|
+
:missing_tables,
|
|
73
|
+
:missing_columns,
|
|
74
|
+
:all_table_methods,
|
|
75
|
+
:list_all_required_tables,
|
|
76
|
+
:list_all_required_columns,
|
|
77
|
+
:table_status,
|
|
78
|
+
:column_status,
|
|
79
|
+
:register_required_column
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# Use auth_cached_method for table_configuration so it's computed
|
|
83
|
+
# lazily per-instance and cached. This ensures it works in both:
|
|
84
|
+
# - Normal web request flow (post_configure runs on throwaway instance)
|
|
85
|
+
# - Console interrogation (new instances need access to configuration)
|
|
86
|
+
auth_cached_method :table_configuration
|
|
87
|
+
|
|
88
|
+
# Use auth_cached_method for column_requirements so it persists across instances
|
|
89
|
+
# Column requirements are registered dynamically by features like external_identity
|
|
90
|
+
auth_cached_method :column_requirements
|
|
91
|
+
|
|
92
|
+
# Runs after configuration is complete
|
|
93
|
+
#
|
|
94
|
+
# Checks tables based on mode. Note: post_configure runs on a throwaway
|
|
95
|
+
# instance during initial configuration (see Rodauth.configure line 66),
|
|
96
|
+
# so we can't rely on instance variables persisting. Use auth_cached_method
|
|
97
|
+
# for any data needed by later instances.
|
|
98
|
+
def post_configure
|
|
99
|
+
super if defined?(super)
|
|
100
|
+
|
|
101
|
+
# Check tables based on mode (uses lazy-loaded table_configuration)
|
|
102
|
+
# Always check if sequel_mode is set (even in silent mode), since sequel_mode
|
|
103
|
+
# indicates we want to create/generate even if we don't want validation messages
|
|
104
|
+
check_required_tables! if should_check_tables? || table_guard_sequel_mode
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Override hook_action to check table status
|
|
108
|
+
#
|
|
109
|
+
# [Reviewer note] Do not remove this method even though it does nothing by default.
|
|
110
|
+
#
|
|
111
|
+
# @param [Symbol] hook_type :before or :after
|
|
112
|
+
# @param [Symbol] action :login, :logout, etc.
|
|
113
|
+
def hook_action(hook_type, action)
|
|
114
|
+
super # does nothing by default
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Determine if table checking should run
|
|
118
|
+
#
|
|
119
|
+
# Returns true unless mode is :skip, :silent, or nil
|
|
120
|
+
def should_check_tables?
|
|
121
|
+
# Check if table_guard_mode is defined as a block (has parameters)
|
|
122
|
+
# by checking the method's arity. If arity > 0, it's a block that
|
|
123
|
+
# expects arguments and we can't call it without args.
|
|
124
|
+
mode_method = method(:table_guard_mode)
|
|
125
|
+
|
|
126
|
+
# If method expects parameters (arity > 0), it's a custom block handler
|
|
127
|
+
return true if mode_method.arity > 0
|
|
128
|
+
|
|
129
|
+
# Safe to call the method - it either returns a symbol or is a 0-arity block
|
|
130
|
+
mode_value = table_guard_mode
|
|
131
|
+
|
|
132
|
+
# Always check if mode is a Proc (0-arity custom handler)
|
|
133
|
+
return true if mode_value.is_a?(Proc)
|
|
134
|
+
|
|
135
|
+
# Check if mode indicates checking is enabled
|
|
136
|
+
mode_value != :silent && mode_value != :skip && !mode_value.nil?
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Internal method called by auth_cached_method :table_configuration
|
|
140
|
+
#
|
|
141
|
+
# Discovers and returns table configuration. This is called lazily
|
|
142
|
+
# per-instance and the result is cached in @table_configuration.
|
|
143
|
+
#
|
|
144
|
+
# @return [Hash<Symbol, Hash>] Table configuration
|
|
145
|
+
def _table_configuration
|
|
146
|
+
config = Rodauth::TableInspector.table_information(self)
|
|
147
|
+
rodauth_debug("[table_guard] Discovered #{config.size} required tables") if ENV['RODAUTH_DEBUG']
|
|
148
|
+
config
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Internal method called by auth_cached_method :column_requirements
|
|
152
|
+
#
|
|
153
|
+
# Initializes and returns column requirements hash. This is called lazily
|
|
154
|
+
# per-instance and the result is cached in @column_requirements.
|
|
155
|
+
#
|
|
156
|
+
# Structure: { table_name => { column_name => { type:, null:, feature: } } }
|
|
157
|
+
#
|
|
158
|
+
# @return [Hash<Symbol, Hash<Symbol, Hash>>] Column requirements by table
|
|
159
|
+
def _column_requirements
|
|
160
|
+
{}
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Check required tables and handle based on mode
|
|
164
|
+
#
|
|
165
|
+
# This is the main entry point for table validation
|
|
166
|
+
def check_required_tables!
|
|
167
|
+
missing = missing_tables
|
|
168
|
+
missing_cols = missing_columns
|
|
169
|
+
|
|
170
|
+
# Special case: :recreate and :drop modes always run, even when no tables are missing
|
|
171
|
+
if missing.empty? && missing_cols.empty? && !%i[recreate drop].include?(table_guard_sequel_mode)
|
|
172
|
+
rodauth_info('')
|
|
173
|
+
rodauth_info('─' * 50)
|
|
174
|
+
rodauth_info('✅ TableGuard: All required tables and columns exist')
|
|
175
|
+
rodauth_info(" #{table_configuration.size} tables validated successfully")
|
|
176
|
+
if list_all_required_columns.any?
|
|
177
|
+
rodauth_info(" #{list_all_required_columns.size} columns validated successfully")
|
|
178
|
+
end
|
|
179
|
+
rodauth_info('─' * 50)
|
|
180
|
+
rodauth_info('')
|
|
181
|
+
return
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Handle based on validation mode (unless recreate/drop mode which handles its own validation)
|
|
185
|
+
handle_table_guard_mode(missing) unless %i[recreate drop].include?(table_guard_sequel_mode)
|
|
186
|
+
|
|
187
|
+
# Handle missing columns separately if validation mode passes
|
|
188
|
+
handle_column_guard_mode(missing_cols) if missing_cols.any? && !%i[recreate
|
|
189
|
+
drop].include?(table_guard_sequel_mode)
|
|
190
|
+
|
|
191
|
+
# Generate Sequel if configured
|
|
192
|
+
handle_sequel_generation(missing, missing_cols) if table_guard_sequel_mode
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Get list of tables that are missing
|
|
196
|
+
#
|
|
197
|
+
# @return [Array<Hash>] Array of missing table information
|
|
198
|
+
def missing_tables
|
|
199
|
+
result = []
|
|
200
|
+
|
|
201
|
+
table_configuration.each do |method, info|
|
|
202
|
+
table_name = info[:name]
|
|
203
|
+
next if table_exists?(table_name)
|
|
204
|
+
|
|
205
|
+
result << {
|
|
206
|
+
method: method,
|
|
207
|
+
table: table_name,
|
|
208
|
+
feature: info[:feature],
|
|
209
|
+
structure: info[:structure]
|
|
210
|
+
}
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
result
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Get all table method names ending in _table
|
|
217
|
+
#
|
|
218
|
+
# @return [Array<Symbol>] Table method names
|
|
219
|
+
def all_table_methods
|
|
220
|
+
methods.select { |m| m.to_s.end_with?('_table') }
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Check if a table exists in the database
|
|
224
|
+
#
|
|
225
|
+
# Temporarily suppresses Sequel's logger to avoid confusing error logs
|
|
226
|
+
# when checking non-existent tables (Sequel logs SQLite exceptions before
|
|
227
|
+
# catching them internally).
|
|
228
|
+
#
|
|
229
|
+
# @param table_name [String, Symbol] Table name
|
|
230
|
+
# @return [Boolean] True if table exists
|
|
231
|
+
def table_exists?(table_name)
|
|
232
|
+
return true if table_guard_skip_tables.include?(table_name.to_sym) ||
|
|
233
|
+
table_guard_skip_tables.include?(table_name.to_s)
|
|
234
|
+
|
|
235
|
+
# Temporarily suppress Sequel's logger to prevent confusing error logs
|
|
236
|
+
# during table existence checks. Sequel's table_exists? implementation
|
|
237
|
+
# attempts a SELECT query and logs the exception if table doesn't exist,
|
|
238
|
+
# even though it catches the error internally.
|
|
239
|
+
original_logger = db.loggers.dup
|
|
240
|
+
db.loggers.clear
|
|
241
|
+
|
|
242
|
+
db.table_exists?(table_name)
|
|
243
|
+
rescue StandardError => e
|
|
244
|
+
rodauth_warn("[table_guard] Unable to check table existence for #{table_name}: #{e.message}")
|
|
245
|
+
true # Assume exists to avoid false positives
|
|
246
|
+
ensure
|
|
247
|
+
# Restore original loggers
|
|
248
|
+
if original_logger
|
|
249
|
+
db.loggers.clear
|
|
250
|
+
original_logger.each { |logger| db.loggers << logger }
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# List all required table names (sorted)
|
|
255
|
+
#
|
|
256
|
+
# @return [Array<String>] Sorted table names
|
|
257
|
+
def list_all_required_tables
|
|
258
|
+
table_configuration.values.map { |info| info[:name] }.uniq.sort
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# Get detailed status for all tables
|
|
262
|
+
#
|
|
263
|
+
# @return [Array<Hash>] Status information for each table
|
|
264
|
+
def table_status
|
|
265
|
+
table_configuration.map do |method, info|
|
|
266
|
+
{
|
|
267
|
+
method: method,
|
|
268
|
+
table: info[:name],
|
|
269
|
+
feature: info[:feature],
|
|
270
|
+
exists: table_exists?(info[:name])
|
|
271
|
+
}
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# Register a required column for validation and generation
|
|
276
|
+
#
|
|
277
|
+
# This method allows features like external_identity to register
|
|
278
|
+
# column requirements that should be validated and optionally
|
|
279
|
+
# created via ALTER TABLE statements.
|
|
280
|
+
#
|
|
281
|
+
# @param table_name [Symbol] Table name (e.g., :accounts)
|
|
282
|
+
# @param column_def [Hash] Column definition with keys:
|
|
283
|
+
# - :name [Symbol] Column name (required)
|
|
284
|
+
# - :type [Symbol] Column type (default: :String)
|
|
285
|
+
# - :null [Boolean] Allow NULL (default: true)
|
|
286
|
+
# - :default [Object] Default value (optional)
|
|
287
|
+
# - :unique [Boolean] Unique constraint (default: false)
|
|
288
|
+
# - :size [Integer] Column size for strings (optional)
|
|
289
|
+
# - :index [Boolean, Hash] Create index (default: false)
|
|
290
|
+
# - :feature [Symbol] Feature that requires this column (default: :unknown)
|
|
291
|
+
#
|
|
292
|
+
# @example
|
|
293
|
+
# register_required_column(:accounts, {
|
|
294
|
+
# name: :stripe_customer_id,
|
|
295
|
+
# type: :String,
|
|
296
|
+
# null: true,
|
|
297
|
+
# unique: true,
|
|
298
|
+
# index: true,
|
|
299
|
+
# feature: :external_identity
|
|
300
|
+
# })
|
|
301
|
+
def register_required_column(table_name, column_def)
|
|
302
|
+
table_name = table_name.to_sym
|
|
303
|
+
column_name = column_def[:name].to_sym
|
|
304
|
+
|
|
305
|
+
# Initialize table entry if needed
|
|
306
|
+
column_requirements[table_name] ||= {}
|
|
307
|
+
|
|
308
|
+
# Store column definition with all Sequel options
|
|
309
|
+
column_requirements[table_name][column_name] = {
|
|
310
|
+
type: column_def[:type] || :String,
|
|
311
|
+
null: column_def.fetch(:null, true),
|
|
312
|
+
default: column_def[:default],
|
|
313
|
+
unique: column_def[:unique],
|
|
314
|
+
size: column_def[:size],
|
|
315
|
+
index: column_def[:index],
|
|
316
|
+
feature: column_def[:feature] || :unknown
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
rodauth_debug("[table_guard] Registered required column #{table_name}.#{column_name} (#{column_def[:feature]})")
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
# Get missing columns across all registered requirements
|
|
323
|
+
#
|
|
324
|
+
# @return [Array<Hash>] Array of missing column information
|
|
325
|
+
def missing_columns
|
|
326
|
+
result = []
|
|
327
|
+
|
|
328
|
+
column_requirements.each do |table_name, columns|
|
|
329
|
+
# Skip if table doesn't exist yet
|
|
330
|
+
next unless table_exists?(table_name)
|
|
331
|
+
|
|
332
|
+
# Get actual columns from database
|
|
333
|
+
actual_columns = db.schema(table_name).map { |col| col[0] }
|
|
334
|
+
|
|
335
|
+
# Check each required column
|
|
336
|
+
columns.each do |column_name, column_def|
|
|
337
|
+
next if actual_columns.include?(column_name)
|
|
338
|
+
|
|
339
|
+
result << {
|
|
340
|
+
table: table_name,
|
|
341
|
+
column: column_name,
|
|
342
|
+
type: column_def[:type],
|
|
343
|
+
null: column_def[:null],
|
|
344
|
+
default: column_def[:default],
|
|
345
|
+
unique: column_def[:unique],
|
|
346
|
+
size: column_def[:size],
|
|
347
|
+
index: column_def[:index],
|
|
348
|
+
feature: column_def[:feature]
|
|
349
|
+
}
|
|
350
|
+
end
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
result
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
# List all required columns
|
|
357
|
+
#
|
|
358
|
+
# @return [Array<Hash>] Array of column requirements
|
|
359
|
+
def list_all_required_columns
|
|
360
|
+
result = []
|
|
361
|
+
|
|
362
|
+
column_requirements.each do |table_name, columns|
|
|
363
|
+
columns.each do |column_name, column_def|
|
|
364
|
+
result << {
|
|
365
|
+
table: table_name,
|
|
366
|
+
column: column_name,
|
|
367
|
+
type: column_def[:type],
|
|
368
|
+
null: column_def[:null],
|
|
369
|
+
default: column_def[:default],
|
|
370
|
+
unique: column_def[:unique],
|
|
371
|
+
size: column_def[:size],
|
|
372
|
+
index: column_def[:index],
|
|
373
|
+
feature: column_def[:feature]
|
|
374
|
+
}
|
|
375
|
+
end
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
result.sort_by { |col| [col[:table].to_s, col[:column].to_s] }
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
# Get detailed status for all columns
|
|
382
|
+
#
|
|
383
|
+
# @return [Array<Hash>] Status information for each column
|
|
384
|
+
def column_status
|
|
385
|
+
result = []
|
|
386
|
+
|
|
387
|
+
column_requirements.each do |table_name, columns|
|
|
388
|
+
# Get actual columns if table exists
|
|
389
|
+
actual_columns = if table_exists?(table_name)
|
|
390
|
+
db.schema(table_name).map { |col| col[0] }
|
|
391
|
+
else
|
|
392
|
+
[]
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
columns.each do |column_name, column_def|
|
|
396
|
+
result << {
|
|
397
|
+
table: table_name,
|
|
398
|
+
column: column_name,
|
|
399
|
+
type: column_def[:type],
|
|
400
|
+
null: column_def[:null],
|
|
401
|
+
default: column_def[:default],
|
|
402
|
+
unique: column_def[:unique],
|
|
403
|
+
size: column_def[:size],
|
|
404
|
+
index: column_def[:index],
|
|
405
|
+
feature: column_def[:feature],
|
|
406
|
+
exists: actual_columns.include?(column_name),
|
|
407
|
+
table_exists: table_exists?(table_name)
|
|
408
|
+
}
|
|
409
|
+
end
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
result
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
private
|
|
416
|
+
|
|
417
|
+
# Handle column validation based on mode setting
|
|
418
|
+
#
|
|
419
|
+
# @param missing_cols [Array<Hash>] Missing column information
|
|
420
|
+
def handle_column_guard_mode(missing_cols)
|
|
421
|
+
return if missing_cols.empty?
|
|
422
|
+
|
|
423
|
+
# Check if table_guard_mode is a block by inspecting method arity
|
|
424
|
+
mode_method = method(:table_guard_mode)
|
|
425
|
+
|
|
426
|
+
# If method expects parameters, it's a custom block handler
|
|
427
|
+
if mode_method.arity > 0
|
|
428
|
+
# Call with appropriate arguments based on arity
|
|
429
|
+
result = case mode_method.arity
|
|
430
|
+
when 1 then table_guard_mode(missing_cols)
|
|
431
|
+
else table_guard_mode(missing_cols, column_requirements)
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
case result
|
|
435
|
+
when :error, :raise, true
|
|
436
|
+
raise Rodauth::ConfigurationError, build_missing_columns_message(missing_cols)
|
|
437
|
+
when String
|
|
438
|
+
raise Rodauth::ConfigurationError, result
|
|
439
|
+
# :continue, nil, false means don't raise
|
|
440
|
+
end
|
|
441
|
+
return
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
# Safe to call without arguments - get the mode value
|
|
445
|
+
mode = table_guard_mode
|
|
446
|
+
|
|
447
|
+
# If it's a 0-arity Proc, call it
|
|
448
|
+
if mode.is_a?(Proc)
|
|
449
|
+
result = mode.call
|
|
450
|
+
|
|
451
|
+
case result
|
|
452
|
+
when :error, :raise, true
|
|
453
|
+
raise Rodauth::ConfigurationError, build_missing_columns_message(missing_cols)
|
|
454
|
+
when String
|
|
455
|
+
raise Rodauth::ConfigurationError, result
|
|
456
|
+
# :continue, nil, false means don't raise
|
|
457
|
+
end
|
|
458
|
+
return
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
# Handle symbol modes
|
|
462
|
+
case mode
|
|
463
|
+
when :silent, :skip, nil
|
|
464
|
+
rodauth_debug("[table_guard] Discovered #{list_all_required_columns.size} columns, skipping validation")
|
|
465
|
+
|
|
466
|
+
when :warn
|
|
467
|
+
rodauth_warn(build_missing_columns_message(missing_cols))
|
|
468
|
+
|
|
469
|
+
when :error
|
|
470
|
+
# Print distinctive message to error log but continue execution
|
|
471
|
+
rodauth_error(build_missing_columns_error(missing_cols))
|
|
472
|
+
|
|
473
|
+
when :raise
|
|
474
|
+
# Let the error propagate up
|
|
475
|
+
rodauth_error(build_missing_columns_error(missing_cols))
|
|
476
|
+
raise Rodauth::ConfigurationError, build_missing_columns_message(missing_cols)
|
|
477
|
+
|
|
478
|
+
when :halt, :exit
|
|
479
|
+
# Exit the process early
|
|
480
|
+
rodauth_error(build_missing_columns_error(missing_cols))
|
|
481
|
+
exit(1)
|
|
482
|
+
|
|
483
|
+
else
|
|
484
|
+
raise Rodauth::ConfigurationError,
|
|
485
|
+
"Invalid table_guard_mode: #{mode.inspect}. " \
|
|
486
|
+
'Expected :silent, :skip, :warn, :error, :raise, :halt, or a Proc.'
|
|
487
|
+
end
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
# Handle table validation based on mode setting
|
|
491
|
+
#
|
|
492
|
+
# @param missing [Array<Hash>] Missing table information
|
|
493
|
+
def handle_table_guard_mode(missing)
|
|
494
|
+
# Check if table_guard_mode is a block by inspecting method arity
|
|
495
|
+
mode_method = method(:table_guard_mode)
|
|
496
|
+
|
|
497
|
+
# If method expects parameters, it's a custom block handler
|
|
498
|
+
if mode_method.arity > 0
|
|
499
|
+
# Call with appropriate arguments based on arity
|
|
500
|
+
result = case mode_method.arity
|
|
501
|
+
when 1 then table_guard_mode(missing)
|
|
502
|
+
else table_guard_mode(missing, table_configuration)
|
|
503
|
+
end
|
|
504
|
+
|
|
505
|
+
case result
|
|
506
|
+
when :error, :raise, true
|
|
507
|
+
raise Rodauth::ConfigurationError, build_missing_tables_message(missing)
|
|
508
|
+
when String
|
|
509
|
+
raise Rodauth::ConfigurationError, result
|
|
510
|
+
# :continue, nil, false means don't raise
|
|
511
|
+
end
|
|
512
|
+
return
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
# Safe to call without arguments - get the mode value
|
|
516
|
+
mode = table_guard_mode
|
|
517
|
+
|
|
518
|
+
# If it's a 0-arity Proc, call it
|
|
519
|
+
if mode.is_a?(Proc)
|
|
520
|
+
result = mode.call
|
|
521
|
+
|
|
522
|
+
case result
|
|
523
|
+
when :error, :raise, true
|
|
524
|
+
raise Rodauth::ConfigurationError, build_missing_tables_message(missing)
|
|
525
|
+
when String
|
|
526
|
+
raise Rodauth::ConfigurationError, result
|
|
527
|
+
# :continue, nil, false means don't raise
|
|
528
|
+
end
|
|
529
|
+
return
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
# Handle symbol modes
|
|
533
|
+
case mode
|
|
534
|
+
when :silent, :skip, nil
|
|
535
|
+
rodauth_debug("[table_guard] Discovered #{@table_configuration.size} tables, skipping validation")
|
|
536
|
+
|
|
537
|
+
when :warn
|
|
538
|
+
rodauth_warn(build_missing_tables_message(missing))
|
|
539
|
+
|
|
540
|
+
when :error
|
|
541
|
+
# Print distinctive message to error log but continue execution
|
|
542
|
+
rodauth_error(build_missing_tables_error(missing))
|
|
543
|
+
|
|
544
|
+
when :raise
|
|
545
|
+
# Let the error propagate up
|
|
546
|
+
rodauth_error(build_missing_tables_error(missing))
|
|
547
|
+
raise Rodauth::ConfigurationError, build_missing_tables_message(missing)
|
|
548
|
+
|
|
549
|
+
when :halt, :exit
|
|
550
|
+
# Exit the process early
|
|
551
|
+
rodauth_error(build_missing_tables_error(missing))
|
|
552
|
+
exit(1)
|
|
553
|
+
|
|
554
|
+
else
|
|
555
|
+
raise Rodauth::ConfigurationError,
|
|
556
|
+
"Invalid table_guard_mode: #{mode.inspect}. " \
|
|
557
|
+
'Expected :silent, :skip, :warn, :error, :raise, :halt, or a Proc.'
|
|
558
|
+
end
|
|
559
|
+
end
|
|
560
|
+
|
|
561
|
+
# Handle Sequel generation based on sequel mode
|
|
562
|
+
#
|
|
563
|
+
# @param missing [Array<Hash>] Missing table information
|
|
564
|
+
# @param missing_cols [Array<Hash>] Missing column information
|
|
565
|
+
def handle_sequel_generation(missing, missing_cols = [])
|
|
566
|
+
generator = Rodauth::SequelGenerator.new(missing, self, missing_cols)
|
|
567
|
+
|
|
568
|
+
case table_guard_sequel_mode
|
|
569
|
+
when :log
|
|
570
|
+
rodauth_info("[table_guard] Sequel migration code:\n\n#{generator.generate_migration}")
|
|
571
|
+
|
|
572
|
+
when :migration
|
|
573
|
+
filename = generate_migration_filename
|
|
574
|
+
FileUtils.mkdir_p(File.dirname(filename))
|
|
575
|
+
File.write(filename, generator.generate_migration)
|
|
576
|
+
rodauth_info("[table_guard] Generated migration file: #{filename}")
|
|
577
|
+
|
|
578
|
+
when :create
|
|
579
|
+
rodauth_debug("[table_guard] Creating #{missing.size} table(s)...")
|
|
580
|
+
generator.execute_creates(db)
|
|
581
|
+
rodauth_info("[table_guard] Created #{missing.size} table(s)")
|
|
582
|
+
|
|
583
|
+
# Re-validate to show success message
|
|
584
|
+
revalidate_after_creation
|
|
585
|
+
|
|
586
|
+
when :sync
|
|
587
|
+
unless %w[dev development test].any? { |env| ENV['RACK_ENV']&.start_with?(env) }
|
|
588
|
+
rodauth_error("[table_guard] :sync mode only available in dev/test environments (current: #{ENV.fetch(
|
|
589
|
+
"RACK_ENV", nil
|
|
590
|
+
)})")
|
|
591
|
+
return
|
|
592
|
+
end
|
|
593
|
+
|
|
594
|
+
# Drop and recreate only missing tables
|
|
595
|
+
rodauth_info("[table_guard] Syncing #{missing.size} table(s)...")
|
|
596
|
+
generator.execute_drops(db)
|
|
597
|
+
generator.execute_creates(db)
|
|
598
|
+
rodauth_info("[table_guard] Synced #{missing.size} table(s) (dropped and recreated)")
|
|
599
|
+
|
|
600
|
+
# Re-validate to show success message
|
|
601
|
+
revalidate_after_creation
|
|
602
|
+
|
|
603
|
+
when :recreate
|
|
604
|
+
unless %w[dev development test].any? { |env| ENV['RACK_ENV']&.start_with?(env) }
|
|
605
|
+
rodauth_error("[table_guard] :recreate mode only available in dev/test environments (current: #{ENV.fetch(
|
|
606
|
+
"RACK_ENV", nil
|
|
607
|
+
)})")
|
|
608
|
+
return
|
|
609
|
+
end
|
|
610
|
+
|
|
611
|
+
# Get all required tables from configuration
|
|
612
|
+
all_tables = table_configuration.map { |_, info| info[:name] }.uniq
|
|
613
|
+
|
|
614
|
+
# Drop all existing tables in reverse dependency order
|
|
615
|
+
rodauth_info("[table_guard] Recreating #{all_tables.size} table(s) (dropping all, creating fresh)...")
|
|
616
|
+
drop_tables(all_tables.reverse)
|
|
617
|
+
|
|
618
|
+
# Create all tables fresh (uses missing_tables which should now be all of them)
|
|
619
|
+
current_missing = missing_tables
|
|
620
|
+
current_missing_cols = missing_columns
|
|
621
|
+
if current_missing.any? || current_missing_cols.any?
|
|
622
|
+
generator_for_all = Rodauth::SequelGenerator.new(current_missing, self, current_missing_cols)
|
|
623
|
+
generator_for_all.execute_creates(db)
|
|
624
|
+
end
|
|
625
|
+
|
|
626
|
+
rodauth_info("[table_guard] Recreated #{all_tables.size} table(s)")
|
|
627
|
+
|
|
628
|
+
# Re-validate to show success message
|
|
629
|
+
revalidate_after_creation
|
|
630
|
+
|
|
631
|
+
# This is useful when you already have auto migrations that run at start
|
|
632
|
+
# time. This will drop the tables so that the migrations run every time.
|
|
633
|
+
when :drop
|
|
634
|
+
unless %w[dev development test].any? { |env| ENV['RACK_ENV']&.start_with?(env) }
|
|
635
|
+
rodauth_error("[table_guard] :drop mode only available in dev/test environments (current: #{ENV.fetch(
|
|
636
|
+
"RACK_ENV", nil
|
|
637
|
+
)})")
|
|
638
|
+
return
|
|
639
|
+
end
|
|
640
|
+
|
|
641
|
+
# Get all required tables from configuration
|
|
642
|
+
all_tables = table_configuration.map { |_, info| info[:name] }.uniq
|
|
643
|
+
|
|
644
|
+
# Drop all existing tables in reverse dependency order
|
|
645
|
+
rodauth_info("[table_guard] Dropping #{all_tables.size} table(s)...")
|
|
646
|
+
drop_tables(all_tables.reverse)
|
|
647
|
+
|
|
648
|
+
# Drop Sequel migration tracking tables so migrations re-run from scratch
|
|
649
|
+
drop_tables(%i[schema_info schema_migrations])
|
|
650
|
+
|
|
651
|
+
rodauth_info("[table_guard] Dropped #{all_tables.size} table(s) and migration tracking")
|
|
652
|
+
rodauth_info('[table_guard] Migrations will run from scratch on next execution')
|
|
653
|
+
|
|
654
|
+
else
|
|
655
|
+
rodauth_error("[table_guard] Invalid sequel mode: #{table_guard_sequel_mode.inspect}")
|
|
656
|
+
end
|
|
657
|
+
rescue StandardError => e
|
|
658
|
+
rodauth_error("[table_guard] Sequel generation failed: #{e.class} - #{e.message}")
|
|
659
|
+
rodauth_error(" Location: #{e.backtrace.first}")
|
|
660
|
+
raise if %i[raise halt exit].include?(table_guard_mode)
|
|
661
|
+
end
|
|
662
|
+
|
|
663
|
+
# Check if the database supports CASCADE on DELETE
|
|
664
|
+
#
|
|
665
|
+
# @return [Boolean] True if using a db engine that supports DELETE ... CASCADE
|
|
666
|
+
def cascade_supported?
|
|
667
|
+
%i[postgres mysql].include?(db.database_type)
|
|
668
|
+
end
|
|
669
|
+
|
|
670
|
+
# Drop tables with proper CASCADE handling for non-SQLite databases
|
|
671
|
+
#
|
|
672
|
+
# SQLite doesn't support CASCADE on DROP TABLE, so we need to detect
|
|
673
|
+
# the database type and avoid using it. For other databases, CASCADE
|
|
674
|
+
# ensures dependent objects are properly cleaned up.
|
|
675
|
+
#
|
|
676
|
+
# @param table_names [Array<String, Symbol>] Tables to drop
|
|
677
|
+
def drop_tables(table_names)
|
|
678
|
+
table_names.each do |table_name|
|
|
679
|
+
next unless db.table_exists?(table_name)
|
|
680
|
+
|
|
681
|
+
# SQLite: simple drop without CASCADE
|
|
682
|
+
# PostgreSQL, MySQL: use CASCADE for proper cleanup
|
|
683
|
+
options = {
|
|
684
|
+
cascade: cascade_supported?
|
|
685
|
+
}
|
|
686
|
+
db.drop_table(table_name, **options)
|
|
687
|
+
|
|
688
|
+
rodauth_debug("[table_guard] Dropped #{table_name} (#{options})") if ENV['RODAUTH_DEBUG']
|
|
689
|
+
end
|
|
690
|
+
end
|
|
691
|
+
|
|
692
|
+
# Re-validate tables after creation to show success message
|
|
693
|
+
#
|
|
694
|
+
# This runs the validation again after tables are created,
|
|
695
|
+
# which will display the success message instead of leaving
|
|
696
|
+
# the error/warning messages as the last output
|
|
697
|
+
def revalidate_after_creation
|
|
698
|
+
rodauth_info('') # Blank line for readability
|
|
699
|
+
|
|
700
|
+
still_missing = missing_tables
|
|
701
|
+
|
|
702
|
+
if still_missing.empty?
|
|
703
|
+
rodauth_info('=' * 70)
|
|
704
|
+
rodauth_info('✓ [table_guard] All required tables now exist')
|
|
705
|
+
rodauth_info(" #{table_configuration.size} tables validated successfully")
|
|
706
|
+
rodauth_info('=' * 70)
|
|
707
|
+
else
|
|
708
|
+
rodauth_error("[table_guard] Still missing #{still_missing.size} table(s) after creation!")
|
|
709
|
+
still_missing.each do |info|
|
|
710
|
+
rodauth_error(" - #{info[:table]} (#{info[:feature]})")
|
|
711
|
+
end
|
|
712
|
+
end
|
|
713
|
+
end
|
|
714
|
+
|
|
715
|
+
# Generate migration filename with timestamp
|
|
716
|
+
#
|
|
717
|
+
# @return [String] Full path to migration file
|
|
718
|
+
def generate_migration_filename
|
|
719
|
+
timestamp = Time.now.strftime('%Y%m%d%H%M%S')
|
|
720
|
+
filename = "#{timestamp}_create_rodauth_tables.rb"
|
|
721
|
+
File.join(table_guard_migration_path, filename)
|
|
722
|
+
end
|
|
723
|
+
|
|
724
|
+
# Build user-friendly message for missing tables
|
|
725
|
+
#
|
|
726
|
+
# @param missing [Array<Hash>] Missing table information
|
|
727
|
+
# @return [String] Formatted message
|
|
728
|
+
def build_missing_tables_message(missing)
|
|
729
|
+
lines = ['Rodauth [table_guard] Missing required database tables!']
|
|
730
|
+
lines << ''
|
|
731
|
+
|
|
732
|
+
missing.each do |info|
|
|
733
|
+
lines << " - Table: #{info[:table]} (feature: #{info[:feature]}, method: #{info[:method]})"
|
|
734
|
+
end
|
|
735
|
+
|
|
736
|
+
lines << ''
|
|
737
|
+
lines << build_migration_hints(missing)
|
|
738
|
+
|
|
739
|
+
lines.join("\n")
|
|
740
|
+
end
|
|
741
|
+
|
|
742
|
+
# Build distinctive error message for error-level logging
|
|
743
|
+
#
|
|
744
|
+
# @param missing [Array<Hash>] Missing table information
|
|
745
|
+
# @return [String] Formatted error message
|
|
746
|
+
def build_missing_tables_error(missing)
|
|
747
|
+
table_list = missing.map { |i| i[:table] }.join(', ')
|
|
748
|
+
"CRITICAL: Missing Rodauth tables - #{table_list}"
|
|
749
|
+
end
|
|
750
|
+
|
|
751
|
+
# Build helpful hints for resolving missing tables
|
|
752
|
+
#
|
|
753
|
+
# @param missing [Array<Hash>] Missing table information
|
|
754
|
+
# @return [String] Formatted hints
|
|
755
|
+
def build_migration_hints(missing)
|
|
756
|
+
hints = []
|
|
757
|
+
hints << ''
|
|
758
|
+
hints << '⚠️ DATABASE OPERATIONS WILL FAIL UNTIL TABLES ARE CREATED'
|
|
759
|
+
hints << ''
|
|
760
|
+
|
|
761
|
+
unique_tables = missing.map { |i| i[:table] }.uniq
|
|
762
|
+
|
|
763
|
+
if table_guard_sequel_mode.nil?
|
|
764
|
+
hints << 'Quick fix for development (creates tables automatically):'
|
|
765
|
+
hints << ' table_guard_sequel_mode :create'
|
|
766
|
+
hints << ''
|
|
767
|
+
hints << 'Other options:'
|
|
768
|
+
hints << ' table_guard_sequel_mode :log # Show migration code'
|
|
769
|
+
hints << ' table_guard_sequel_mode :migration # Generate migration file'
|
|
770
|
+
hints << ''
|
|
771
|
+
end
|
|
772
|
+
|
|
773
|
+
hints << 'Required tables:'
|
|
774
|
+
unique_tables.each do |table|
|
|
775
|
+
hints << " - #{table}"
|
|
776
|
+
end
|
|
777
|
+
|
|
778
|
+
hints << ''
|
|
779
|
+
hints << 'To disable checking: table_guard_mode :silent'
|
|
780
|
+
hints << "To skip specific tables: table_guard_skip_tables #{unique_tables.inspect}"
|
|
781
|
+
|
|
782
|
+
hints.join("\n")
|
|
783
|
+
end
|
|
784
|
+
|
|
785
|
+
# Build user-friendly message for missing columns
|
|
786
|
+
#
|
|
787
|
+
# @param missing_cols [Array<Hash>] Missing column information
|
|
788
|
+
# @return [String] Formatted message
|
|
789
|
+
def build_missing_columns_message(missing_cols)
|
|
790
|
+
lines = ['Rodauth [table_guard] Missing required database columns!']
|
|
791
|
+
lines << ''
|
|
792
|
+
|
|
793
|
+
# Group by table
|
|
794
|
+
by_table = missing_cols.group_by { |col| col[:table] }
|
|
795
|
+
by_table.each do |table, cols|
|
|
796
|
+
lines << " Table: #{table}"
|
|
797
|
+
cols.each do |col|
|
|
798
|
+
lines << " - Column: #{col[:column]} (type: #{col[:type]}, feature: #{col[:feature]})"
|
|
799
|
+
end
|
|
800
|
+
end
|
|
801
|
+
|
|
802
|
+
lines << ''
|
|
803
|
+
lines << build_column_migration_hints(missing_cols)
|
|
804
|
+
|
|
805
|
+
lines.join("\n")
|
|
806
|
+
end
|
|
807
|
+
|
|
808
|
+
# Build distinctive error message for columns
|
|
809
|
+
#
|
|
810
|
+
# @param missing_cols [Array<Hash>] Missing column information
|
|
811
|
+
# @return [String] Formatted error message
|
|
812
|
+
def build_missing_columns_error(missing_cols)
|
|
813
|
+
column_list = missing_cols.map { |c| "#{c[:table]}.#{c[:column]}" }.join(', ')
|
|
814
|
+
"CRITICAL: Missing Rodauth columns - #{column_list}"
|
|
815
|
+
end
|
|
816
|
+
|
|
817
|
+
# Build helpful hints for resolving missing columns
|
|
818
|
+
#
|
|
819
|
+
# @param missing_cols [Array<Hash>] Missing column information
|
|
820
|
+
# @return [String] Formatted hints
|
|
821
|
+
def build_column_migration_hints(missing_cols)
|
|
822
|
+
hints = []
|
|
823
|
+
hints << ''
|
|
824
|
+
hints << '⚠️ DATABASE OPERATIONS MAY FAIL UNTIL COLUMNS ARE ADDED'
|
|
825
|
+
hints << ''
|
|
826
|
+
|
|
827
|
+
if table_guard_sequel_mode.nil?
|
|
828
|
+
hints << 'Quick fix for development (adds columns automatically):'
|
|
829
|
+
hints << ' table_guard_sequel_mode :create'
|
|
830
|
+
hints << ''
|
|
831
|
+
hints << 'Other options:'
|
|
832
|
+
hints << ' table_guard_sequel_mode :log # Show migration code'
|
|
833
|
+
hints << ' table_guard_sequel_mode :migration # Generate migration file'
|
|
834
|
+
hints << ''
|
|
835
|
+
end
|
|
836
|
+
|
|
837
|
+
hints << 'Required columns:'
|
|
838
|
+
missing_cols.each do |col|
|
|
839
|
+
hints << " - #{col[:table]}.#{col[:column]} (#{col[:type]}, #{col[:feature]})"
|
|
840
|
+
end
|
|
841
|
+
|
|
842
|
+
hints << ''
|
|
843
|
+
hints << 'To disable checking: table_guard_mode :silent'
|
|
844
|
+
|
|
845
|
+
hints.join("\n")
|
|
846
|
+
end
|
|
847
|
+
|
|
848
|
+
# Get logger instance with fallback chain
|
|
849
|
+
#
|
|
850
|
+
# Checks in order:
|
|
851
|
+
# 1. table_guard_logger (feature-specific logger instance)
|
|
852
|
+
# 2. SemanticLogger[table_guard_logger_name] (if name provided)
|
|
853
|
+
# 3. logger (Rodauth instance method if defined by user)
|
|
854
|
+
# 4. scope.logger (Roda app logger if available)
|
|
855
|
+
# 5. nil (no logger available)
|
|
856
|
+
#
|
|
857
|
+
# For SemanticLogger integration, use table_guard_logger_name instead of
|
|
858
|
+
# table_guard_logger to ensure level configuration is preserved:
|
|
859
|
+
#
|
|
860
|
+
# table_guard_logger_name 'rodauth' # Looks up SemanticLogger['rodauth']
|
|
861
|
+
#
|
|
862
|
+
# Note: SemanticLogger[] creates a new logger instance each time it's called.
|
|
863
|
+
# If you configure logger levels via YAML or code, use table_guard_logger_name
|
|
864
|
+
# so the feature can look up the logger at runtime and get the configured instance.
|
|
865
|
+
def get_logger
|
|
866
|
+
result = table_guard_logger
|
|
867
|
+
|
|
868
|
+
# If no direct logger but name provided, look up SemanticLogger
|
|
869
|
+
result = SemanticLogger[table_guard_logger_name] if !result && table_guard_logger_name && defined?(SemanticLogger)
|
|
870
|
+
|
|
871
|
+
# Fallback chain if still no logger
|
|
872
|
+
result ||= (respond_to?(:logger) ? logger : nil) ||
|
|
873
|
+
(respond_to?(:scope) && scope.respond_to?(:logger) ? scope.logger : nil)
|
|
874
|
+
|
|
875
|
+
# Warn once if logger appears to be SemanticLogger but has no appenders
|
|
876
|
+
if result && result.class.name&.include?('SemanticLogger') &&
|
|
877
|
+
defined?(SemanticLogger) && SemanticLogger.appenders.empty?
|
|
878
|
+
warn '[table_guard] WARNING: SemanticLogger has no appenders configured. ' \
|
|
879
|
+
'Add: SemanticLogger.add_appender(io: STDOUT, level: :info)'
|
|
880
|
+
end
|
|
881
|
+
|
|
882
|
+
result
|
|
883
|
+
end
|
|
884
|
+
|
|
885
|
+
# Debug logging helper
|
|
886
|
+
def rodauth_debug(msg)
|
|
887
|
+
logger = get_logger
|
|
888
|
+
return unless logger
|
|
889
|
+
|
|
890
|
+
if logger.respond_to?(:debug)
|
|
891
|
+
logger.debug(msg)
|
|
892
|
+
elsif ENV['RODAUTH_DEBUG']
|
|
893
|
+
warn "[DEBUG] #{msg}"
|
|
894
|
+
end
|
|
895
|
+
end
|
|
896
|
+
|
|
897
|
+
# Info logging helper
|
|
898
|
+
def rodauth_info(msg)
|
|
899
|
+
logger = get_logger
|
|
900
|
+
|
|
901
|
+
if logger&.respond_to?(:info)
|
|
902
|
+
logger.info(msg)
|
|
903
|
+
elsif logger&.respond_to?(:<<)
|
|
904
|
+
# Support loggers that only have << method
|
|
905
|
+
logger << "#{msg}\n"
|
|
906
|
+
else
|
|
907
|
+
puts msg
|
|
908
|
+
end
|
|
909
|
+
end
|
|
910
|
+
|
|
911
|
+
# Warn logging helper
|
|
912
|
+
def rodauth_warn(msg)
|
|
913
|
+
logger = get_logger
|
|
914
|
+
|
|
915
|
+
if logger&.respond_to?(:warn)
|
|
916
|
+
logger.warn(msg)
|
|
917
|
+
elsif logger&.respond_to?(:<<)
|
|
918
|
+
logger << "[WARN] #{msg}\n"
|
|
919
|
+
else
|
|
920
|
+
warn msg
|
|
921
|
+
end
|
|
922
|
+
end
|
|
923
|
+
|
|
924
|
+
# Error logging helper
|
|
925
|
+
def rodauth_error(msg)
|
|
926
|
+
logger = get_logger
|
|
927
|
+
|
|
928
|
+
if logger&.respond_to?(:error)
|
|
929
|
+
logger.error(msg)
|
|
930
|
+
elsif logger&.respond_to?(:<<)
|
|
931
|
+
logger << "[ERROR] #{msg}\n"
|
|
932
|
+
else
|
|
933
|
+
warn "[ERROR] #{msg}"
|
|
934
|
+
end
|
|
935
|
+
end
|
|
936
|
+
end
|
|
937
|
+
end
|