tina4ruby 0.5.2 → 3.2.1
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 +4 -4
- data/CHANGELOG.md +1 -1
- data/README.md +434 -544
- data/exe/{tina4 → tina4ruby} +1 -0
- data/lib/tina4/ai.rb +312 -0
- data/lib/tina4/auth.rb +44 -3
- data/lib/tina4/auto_crud.rb +163 -0
- data/lib/tina4/cli.rb +389 -97
- data/lib/tina4/constants.rb +46 -0
- data/lib/tina4/cors.rb +74 -0
- data/lib/tina4/database/sqlite3_adapter.rb +139 -0
- data/lib/tina4/database.rb +144 -7
- data/lib/tina4/debug.rb +4 -79
- data/lib/tina4/dev_admin.rb +1162 -0
- data/lib/tina4/dev_mailbox.rb +191 -0
- data/lib/tina4/dev_reload.rb +9 -9
- data/lib/tina4/drivers/firebird_driver.rb +19 -3
- data/lib/tina4/drivers/mssql_driver.rb +3 -3
- data/lib/tina4/drivers/mysql_driver.rb +4 -4
- data/lib/tina4/drivers/postgres_driver.rb +9 -2
- data/lib/tina4/drivers/sqlite_driver.rb +1 -1
- data/lib/tina4/env.rb +42 -2
- data/lib/tina4/error_overlay.rb +252 -0
- data/lib/tina4/events.rb +90 -0
- data/lib/tina4/field_types.rb +4 -0
- data/lib/tina4/frond.rb +1497 -0
- data/lib/tina4/gallery/auth/meta.json +1 -0
- data/lib/tina4/gallery/auth/src/routes/api/gallery_auth.rb +114 -0
- data/lib/tina4/gallery/database/meta.json +1 -0
- data/lib/tina4/gallery/database/src/routes/api/gallery_db.rb +43 -0
- data/lib/tina4/gallery/error-overlay/meta.json +1 -0
- data/lib/tina4/gallery/error-overlay/src/routes/api/gallery_crash.rb +17 -0
- data/lib/tina4/gallery/orm/meta.json +1 -0
- data/lib/tina4/gallery/orm/src/routes/api/gallery_products.rb +16 -0
- data/lib/tina4/gallery/queue/meta.json +1 -0
- data/lib/tina4/gallery/queue/src/routes/api/gallery_queue.rb +325 -0
- data/lib/tina4/gallery/rest-api/meta.json +1 -0
- data/lib/tina4/gallery/rest-api/src/routes/api/gallery_hello.rb +14 -0
- data/lib/tina4/gallery/templates/meta.json +1 -0
- data/lib/tina4/gallery/templates/src/routes/gallery_page.rb +12 -0
- data/lib/tina4/gallery/templates/src/templates/gallery_page.twig +257 -0
- data/lib/tina4/health.rb +39 -0
- data/lib/tina4/html_element.rb +148 -0
- data/lib/tina4/localization.rb +2 -2
- data/lib/tina4/log.rb +203 -0
- data/lib/tina4/messenger.rb +562 -0
- data/lib/tina4/migration.rb +132 -29
- data/lib/tina4/orm.rb +463 -35
- data/lib/tina4/public/css/tina4.css +178 -1
- data/lib/tina4/public/css/tina4.min.css +1 -2
- data/lib/tina4/public/favicon.ico +0 -0
- data/lib/tina4/public/images/logo.svg +5 -0
- data/lib/tina4/public/images/tina4-logo-icon.webp +0 -0
- data/lib/tina4/public/js/frond.min.js +420 -0
- data/lib/tina4/public/js/tina4-dev-admin.min.js +367 -0
- data/lib/tina4/public/js/tina4.min.js +93 -0
- data/lib/tina4/public/swagger/index.html +90 -0
- data/lib/tina4/public/swagger/oauth2-redirect.html +63 -0
- data/lib/tina4/queue.rb +162 -6
- data/lib/tina4/queue_backends/lite_backend.rb +88 -0
- data/lib/tina4/rack_app.rb +331 -27
- data/lib/tina4/rate_limiter.rb +123 -0
- data/lib/tina4/request.rb +61 -15
- data/lib/tina4/response.rb +54 -24
- data/lib/tina4/response_cache.rb +551 -0
- data/lib/tina4/router.rb +90 -15
- data/lib/tina4/scss_compiler.rb +2 -2
- data/lib/tina4/seeder.rb +56 -61
- data/lib/tina4/service_runner.rb +303 -0
- data/lib/tina4/session.rb +85 -0
- data/lib/tina4/session_handlers/mongo_handler.rb +1 -1
- data/lib/tina4/session_handlers/valkey_handler.rb +43 -0
- data/lib/tina4/shutdown.rb +84 -0
- data/lib/tina4/sql_translation.rb +295 -0
- data/lib/tina4/template.rb +36 -6
- data/lib/tina4/templates/base.twig +2 -2
- data/lib/tina4/templates/errors/302.twig +14 -0
- data/lib/tina4/templates/errors/401.twig +9 -0
- data/lib/tina4/templates/errors/403.twig +22 -15
- data/lib/tina4/templates/errors/404.twig +22 -15
- data/lib/tina4/templates/errors/500.twig +31 -15
- data/lib/tina4/templates/errors/502.twig +9 -0
- data/lib/tina4/templates/errors/503.twig +12 -0
- data/lib/tina4/templates/errors/base.twig +37 -0
- data/lib/tina4/version.rb +1 -1
- data/lib/tina4/webserver.rb +28 -18
- data/lib/tina4.rb +118 -21
- metadata +68 -8
- data/lib/tina4/public/js/tina4.js +0 -134
- data/lib/tina4/public/js/tina4helper.js +0 -387
data/lib/tina4/cors.rb
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tina4
|
|
4
|
+
module CorsMiddleware
|
|
5
|
+
class << self
|
|
6
|
+
def config
|
|
7
|
+
@config ||= load_config
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def reset!
|
|
11
|
+
@config = nil
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Handle OPTIONS preflight request, returns a Rack response array
|
|
15
|
+
def preflight_response(env = {})
|
|
16
|
+
origin = resolve_origin(env)
|
|
17
|
+
[
|
|
18
|
+
204,
|
|
19
|
+
{
|
|
20
|
+
"access-control-allow-origin" => origin,
|
|
21
|
+
"access-control-allow-methods" => config[:methods],
|
|
22
|
+
"access-control-allow-headers" => config[:headers],
|
|
23
|
+
"access-control-max-age" => config[:max_age],
|
|
24
|
+
"access-control-allow-credentials" => config[:credentials]
|
|
25
|
+
},
|
|
26
|
+
[""]
|
|
27
|
+
]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Apply CORS headers to a response headers hash
|
|
31
|
+
def apply_headers(response_headers, env = {})
|
|
32
|
+
origin = resolve_origin(env)
|
|
33
|
+
response_headers["access-control-allow-origin"] = origin
|
|
34
|
+
response_headers["access-control-allow-methods"] = config[:methods]
|
|
35
|
+
response_headers["access-control-allow-headers"] = config[:headers]
|
|
36
|
+
response_headers["access-control-max-age"] = config[:max_age]
|
|
37
|
+
response_headers["access-control-allow-credentials"] = config[:credentials] if config[:credentials] == "true"
|
|
38
|
+
response_headers
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Check if a given origin is allowed
|
|
42
|
+
def origin_allowed?(origin)
|
|
43
|
+
return true if config[:origins] == "*"
|
|
44
|
+
|
|
45
|
+
allowed = config[:origins].split(",").map(&:strip)
|
|
46
|
+
allowed.include?(origin)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def load_config
|
|
52
|
+
{
|
|
53
|
+
origins: ENV["TINA4_CORS_ORIGINS"] || "*",
|
|
54
|
+
methods: ENV["TINA4_CORS_METHODS"] || "GET, POST, PUT, PATCH, DELETE, OPTIONS",
|
|
55
|
+
headers: ENV["TINA4_CORS_HEADERS"] || "Content-Type, Authorization, Accept",
|
|
56
|
+
max_age: ENV["TINA4_CORS_MAX_AGE"] || "86400",
|
|
57
|
+
credentials: ENV["TINA4_CORS_CREDENTIALS"] || "false"
|
|
58
|
+
}.freeze
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def resolve_origin(env)
|
|
62
|
+
request_origin = env["HTTP_ORIGIN"] || env["HTTP_REFERER"]
|
|
63
|
+
|
|
64
|
+
if config[:origins] == "*"
|
|
65
|
+
"*"
|
|
66
|
+
elsif request_origin && origin_allowed?(request_origin.chomp("/"))
|
|
67
|
+
request_origin.chomp("/")
|
|
68
|
+
else
|
|
69
|
+
config[:origins].split(",").first&.strip || "*"
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tina4
|
|
4
|
+
module Adapters
|
|
5
|
+
class Sqlite3Adapter
|
|
6
|
+
attr_reader :connection, :db_path
|
|
7
|
+
|
|
8
|
+
def initialize(connection_string = nil)
|
|
9
|
+
@connection = nil
|
|
10
|
+
@db_path = nil
|
|
11
|
+
@in_transaction = false
|
|
12
|
+
connect(connection_string) if connection_string
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def connect(connection_string)
|
|
16
|
+
require "sqlite3"
|
|
17
|
+
@db_path = connection_string.to_s.sub(/^sqlite3?:\/\//, "").sub(/^sqlite3?:/, "")
|
|
18
|
+
@connection = SQLite3::Database.new(@db_path)
|
|
19
|
+
@connection.results_as_hash = true
|
|
20
|
+
@connection.execute("PRAGMA journal_mode=WAL")
|
|
21
|
+
@connection.execute("PRAGMA foreign_keys=ON")
|
|
22
|
+
self
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def close
|
|
26
|
+
@connection.close if @connection
|
|
27
|
+
@connection = nil
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def connected?
|
|
31
|
+
!@connection.nil? && !@connection.closed?
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Execute a query and return rows as array of symbol-keyed hashes
|
|
35
|
+
def query(sql, params = [])
|
|
36
|
+
results = @connection.execute(sql, params)
|
|
37
|
+
results.map { |row| symbolize_keys(row) }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Paginated fetch
|
|
41
|
+
def fetch(sql, limit = nil, offset = nil)
|
|
42
|
+
effective_sql = sql
|
|
43
|
+
if limit
|
|
44
|
+
effective_sql = "#{sql} LIMIT #{limit}"
|
|
45
|
+
effective_sql += " OFFSET #{offset}" if offset && offset > 0
|
|
46
|
+
end
|
|
47
|
+
query(effective_sql)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Execute DDL or DML without returning rows
|
|
51
|
+
def exec(sql, params = [])
|
|
52
|
+
@connection.execute(sql, params)
|
|
53
|
+
{ affected_rows: @connection.changes }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Check if a table exists
|
|
57
|
+
def table_exists?(table)
|
|
58
|
+
rows = query(
|
|
59
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name = ?",
|
|
60
|
+
[table.to_s]
|
|
61
|
+
)
|
|
62
|
+
!rows.empty?
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Get column metadata for a table
|
|
66
|
+
def columns(table)
|
|
67
|
+
query("PRAGMA table_info(#{table})").map do |r|
|
|
68
|
+
{
|
|
69
|
+
name: r[:name],
|
|
70
|
+
type: r[:type],
|
|
71
|
+
nullable: r[:notnull] == 0,
|
|
72
|
+
default: r[:dflt_value],
|
|
73
|
+
primary_key: r[:pk] == 1
|
|
74
|
+
}
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# List all user tables
|
|
79
|
+
def tables
|
|
80
|
+
rows = query("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'")
|
|
81
|
+
rows.map { |r| r[:name] }
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Get last inserted row id
|
|
85
|
+
def last_insert_id
|
|
86
|
+
@connection.last_insert_row_id
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Transaction support
|
|
90
|
+
def begin_transaction
|
|
91
|
+
return if @in_transaction
|
|
92
|
+
@connection.execute("BEGIN TRANSACTION")
|
|
93
|
+
@in_transaction = true
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def commit
|
|
97
|
+
return unless @in_transaction
|
|
98
|
+
@connection.execute("COMMIT")
|
|
99
|
+
@in_transaction = false
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def rollback
|
|
103
|
+
return unless @in_transaction
|
|
104
|
+
@connection.execute("ROLLBACK")
|
|
105
|
+
@in_transaction = false
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def transaction
|
|
109
|
+
begin_transaction
|
|
110
|
+
yield self
|
|
111
|
+
commit
|
|
112
|
+
rescue => e
|
|
113
|
+
rollback
|
|
114
|
+
raise e
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Convenience: placeholder for parameterized queries
|
|
118
|
+
def placeholder
|
|
119
|
+
"?"
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def placeholders(count)
|
|
123
|
+
(["?"] * count).join(", ")
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def apply_limit(sql, limit, offset = 0)
|
|
127
|
+
"#{sql} LIMIT #{limit} OFFSET #{offset}"
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
private
|
|
131
|
+
|
|
132
|
+
def symbolize_keys(hash)
|
|
133
|
+
hash.each_with_object({}) do |(k, v), h|
|
|
134
|
+
h[k.to_s.to_sym] = v if k.is_a?(String) || k.is_a?(Symbol)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
data/lib/tina4/database.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
require "json"
|
|
3
|
+
require "uri"
|
|
4
|
+
require "digest"
|
|
3
5
|
|
|
4
6
|
module Tina4
|
|
5
7
|
class Database
|
|
@@ -11,26 +13,36 @@ module Tina4
|
|
|
11
13
|
"postgres" => "Tina4::Drivers::PostgresDriver",
|
|
12
14
|
"postgresql" => "Tina4::Drivers::PostgresDriver",
|
|
13
15
|
"mysql" => "Tina4::Drivers::MysqlDriver",
|
|
14
|
-
"mysql2" => "Tina4::Drivers::MysqlDriver",
|
|
15
16
|
"mssql" => "Tina4::Drivers::MssqlDriver",
|
|
16
17
|
"sqlserver" => "Tina4::Drivers::MssqlDriver",
|
|
17
18
|
"firebird" => "Tina4::Drivers::FirebirdDriver"
|
|
18
19
|
}.freeze
|
|
19
20
|
|
|
20
|
-
def initialize(connection_string, driver_name: nil)
|
|
21
|
-
@connection_string = connection_string
|
|
22
|
-
@
|
|
21
|
+
def initialize(connection_string = nil, username: nil, password: nil, driver_name: nil)
|
|
22
|
+
@connection_string = connection_string || ENV["DATABASE_URL"]
|
|
23
|
+
@username = username || ENV["DATABASE_USERNAME"]
|
|
24
|
+
@password = password || ENV["DATABASE_PASSWORD"]
|
|
25
|
+
@driver_name = driver_name || detect_driver(@connection_string)
|
|
23
26
|
@driver = create_driver
|
|
24
27
|
@connected = false
|
|
28
|
+
|
|
29
|
+
# Query cache — off by default, opt-in via TINA4_DB_CACHE=true
|
|
30
|
+
@cache_enabled = truthy?(ENV["TINA4_DB_CACHE"])
|
|
31
|
+
@cache_ttl = (ENV["TINA4_DB_CACHE_TTL"] || "30").to_i
|
|
32
|
+
@query_cache = {} # key => { expires_at:, value: }
|
|
33
|
+
@cache_hits = 0
|
|
34
|
+
@cache_misses = 0
|
|
35
|
+
@cache_mutex = Mutex.new
|
|
36
|
+
|
|
25
37
|
connect
|
|
26
38
|
end
|
|
27
39
|
|
|
28
40
|
def connect
|
|
29
|
-
@driver.connect(@connection_string)
|
|
41
|
+
@driver.connect(@connection_string, username: @username, password: @password)
|
|
30
42
|
@connected = true
|
|
31
|
-
Tina4::
|
|
43
|
+
Tina4::Log.info("Database connected: #{@driver_name}")
|
|
32
44
|
rescue => e
|
|
33
|
-
Tina4::
|
|
45
|
+
Tina4::Log.error("Database connection failed: #{e.message}")
|
|
34
46
|
@connected = false
|
|
35
47
|
end
|
|
36
48
|
|
|
@@ -39,21 +51,84 @@ module Tina4
|
|
|
39
51
|
@connected = false
|
|
40
52
|
end
|
|
41
53
|
|
|
54
|
+
# ── Query Cache ──────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
def cache_stats
|
|
57
|
+
@cache_mutex.synchronize do
|
|
58
|
+
{
|
|
59
|
+
enabled: @cache_enabled,
|
|
60
|
+
hits: @cache_hits,
|
|
61
|
+
misses: @cache_misses,
|
|
62
|
+
size: @query_cache.size,
|
|
63
|
+
ttl: @cache_ttl
|
|
64
|
+
}
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def cache_clear
|
|
69
|
+
@cache_mutex.synchronize do
|
|
70
|
+
@query_cache.clear
|
|
71
|
+
@cache_hits = 0
|
|
72
|
+
@cache_misses = 0
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
42
76
|
def fetch(sql, params = [], limit: nil, skip: nil)
|
|
43
77
|
effective_sql = sql
|
|
44
78
|
if limit
|
|
45
79
|
effective_sql = @driver.apply_limit(effective_sql, limit, skip || 0)
|
|
46
80
|
end
|
|
81
|
+
|
|
82
|
+
if @cache_enabled
|
|
83
|
+
key = cache_key(effective_sql, params)
|
|
84
|
+
cached = cache_get(key)
|
|
85
|
+
if cached
|
|
86
|
+
@cache_mutex.synchronize { @cache_hits += 1 }
|
|
87
|
+
return cached
|
|
88
|
+
end
|
|
89
|
+
result = @driver.execute_query(effective_sql, params)
|
|
90
|
+
result = Tina4::DatabaseResult.new(result, sql: effective_sql)
|
|
91
|
+
cache_set(key, result)
|
|
92
|
+
@cache_mutex.synchronize { @cache_misses += 1 }
|
|
93
|
+
return result
|
|
94
|
+
end
|
|
95
|
+
|
|
47
96
|
rows = @driver.execute_query(effective_sql, params)
|
|
48
97
|
Tina4::DatabaseResult.new(rows, sql: effective_sql)
|
|
49
98
|
end
|
|
50
99
|
|
|
51
100
|
def fetch_one(sql, params = [])
|
|
101
|
+
if @cache_enabled
|
|
102
|
+
key = cache_key(sql + ":ONE", params)
|
|
103
|
+
cached = cache_get(key)
|
|
104
|
+
if cached
|
|
105
|
+
@cache_mutex.synchronize { @cache_hits += 1 }
|
|
106
|
+
return cached
|
|
107
|
+
end
|
|
108
|
+
result = fetch(sql, params, limit: 1)
|
|
109
|
+
value = result.first
|
|
110
|
+
cache_set(key, value)
|
|
111
|
+
@cache_mutex.synchronize { @cache_misses += 1 }
|
|
112
|
+
return value
|
|
113
|
+
end
|
|
114
|
+
|
|
52
115
|
result = fetch(sql, params, limit: 1)
|
|
53
116
|
result.first
|
|
54
117
|
end
|
|
55
118
|
|
|
56
119
|
def insert(table, data)
|
|
120
|
+
cache_invalidate if @cache_enabled
|
|
121
|
+
|
|
122
|
+
# List of hashes — batch insert
|
|
123
|
+
if data.is_a?(Array)
|
|
124
|
+
return { success: true, affected_rows: 0 } if data.empty?
|
|
125
|
+
keys = data.first.keys.map(&:to_s)
|
|
126
|
+
placeholders = @driver.placeholders(keys.length)
|
|
127
|
+
sql = "INSERT INTO #{table} (#{keys.join(', ')}) VALUES (#{placeholders})"
|
|
128
|
+
params_list = data.map { |row| keys.map { |k| row[k.to_sym] || row[k] } }
|
|
129
|
+
return execute_many(sql, params_list)
|
|
130
|
+
end
|
|
131
|
+
|
|
57
132
|
columns = data.keys.map(&:to_s)
|
|
58
133
|
placeholders = @driver.placeholders(columns.length)
|
|
59
134
|
sql = "INSERT INTO #{table} (#{columns.join(', ')}) VALUES (#{placeholders})"
|
|
@@ -62,6 +137,8 @@ module Tina4
|
|
|
62
137
|
end
|
|
63
138
|
|
|
64
139
|
def update(table, data, filter = {})
|
|
140
|
+
cache_invalidate if @cache_enabled
|
|
141
|
+
|
|
65
142
|
set_parts = data.keys.map { |k| "#{k} = #{@driver.placeholder}" }
|
|
66
143
|
where_parts = filter.keys.map { |k| "#{k} = #{@driver.placeholder}" }
|
|
67
144
|
sql = "UPDATE #{table} SET #{set_parts.join(', ')}"
|
|
@@ -72,6 +149,23 @@ module Tina4
|
|
|
72
149
|
end
|
|
73
150
|
|
|
74
151
|
def delete(table, filter = {})
|
|
152
|
+
cache_invalidate if @cache_enabled
|
|
153
|
+
|
|
154
|
+
# List of hashes — delete each row
|
|
155
|
+
if filter.is_a?(Array)
|
|
156
|
+
filter.each { |row| delete(table, row) }
|
|
157
|
+
return { success: true }
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# String filter — raw WHERE clause
|
|
161
|
+
if filter.is_a?(String)
|
|
162
|
+
sql = "DELETE FROM #{table}"
|
|
163
|
+
sql += " WHERE #{filter}" unless filter.empty?
|
|
164
|
+
@driver.execute(sql)
|
|
165
|
+
return { success: true }
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Hash filter — build WHERE from keys
|
|
75
169
|
where_parts = filter.keys.map { |k| "#{k} = #{@driver.placeholder}" }
|
|
76
170
|
sql = "DELETE FROM #{table}"
|
|
77
171
|
sql += " WHERE #{where_parts.join(' AND ')}" unless filter.empty?
|
|
@@ -80,9 +174,19 @@ module Tina4
|
|
|
80
174
|
end
|
|
81
175
|
|
|
82
176
|
def execute(sql, params = [])
|
|
177
|
+
cache_invalidate if @cache_enabled
|
|
83
178
|
@driver.execute(sql, params)
|
|
84
179
|
end
|
|
85
180
|
|
|
181
|
+
def execute_many(sql, params_list = [])
|
|
182
|
+
total_affected = 0
|
|
183
|
+
params_list.each do |params|
|
|
184
|
+
@driver.execute(sql, params)
|
|
185
|
+
total_affected += 1
|
|
186
|
+
end
|
|
187
|
+
{ success: true, affected_rows: total_affected }
|
|
188
|
+
end
|
|
189
|
+
|
|
86
190
|
def transaction
|
|
87
191
|
@driver.begin_transaction
|
|
88
192
|
yield self
|
|
@@ -106,6 +210,39 @@ module Tina4
|
|
|
106
210
|
|
|
107
211
|
private
|
|
108
212
|
|
|
213
|
+
def truthy?(val)
|
|
214
|
+
%w[true 1 yes on].include?((val || "").to_s.strip.downcase)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def cache_key(sql, params)
|
|
218
|
+
Digest::SHA256.hexdigest(sql + params.to_s)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def cache_get(key)
|
|
222
|
+
@cache_mutex.synchronize do
|
|
223
|
+
entry = @query_cache[key]
|
|
224
|
+
return nil unless entry
|
|
225
|
+
if Process.clock_gettime(Process::CLOCK_MONOTONIC) > entry[:expires_at]
|
|
226
|
+
@query_cache.delete(key)
|
|
227
|
+
return nil
|
|
228
|
+
end
|
|
229
|
+
entry[:value]
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def cache_set(key, value)
|
|
234
|
+
@cache_mutex.synchronize do
|
|
235
|
+
@query_cache[key] = {
|
|
236
|
+
expires_at: Process.clock_gettime(Process::CLOCK_MONOTONIC) + @cache_ttl,
|
|
237
|
+
value: value
|
|
238
|
+
}
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def cache_invalidate
|
|
243
|
+
@cache_mutex.synchronize { @query_cache.clear }
|
|
244
|
+
end
|
|
245
|
+
|
|
109
246
|
def detect_driver(conn)
|
|
110
247
|
case conn.to_s.downcase
|
|
111
248
|
when /\.db$/, /\.sqlite/, /sqlite/
|
data/lib/tina4/debug.rb
CHANGED
|
@@ -1,83 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
-
require "logger"
|
|
3
|
-
require "fileutils"
|
|
4
2
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
LEVELS = {
|
|
8
|
-
"[TINA4_LOG_ALL]" => Logger::DEBUG,
|
|
9
|
-
"[TINA4_LOG_DEBUG]" => Logger::DEBUG,
|
|
10
|
-
"[TINA4_LOG_INFO]" => Logger::INFO,
|
|
11
|
-
"[TINA4_LOG_WARNING]" => Logger::WARN,
|
|
12
|
-
"[TINA4_LOG_ERROR]" => Logger::ERROR,
|
|
13
|
-
"[TINA4_LOG_NONE]" => Logger::FATAL
|
|
14
|
-
}.freeze
|
|
15
|
-
|
|
16
|
-
COLORS = {
|
|
17
|
-
reset: "\e[0m", red: "\e[31m", green: "\e[32m",
|
|
18
|
-
yellow: "\e[33m", blue: "\e[34m", magenta: "\e[35m",
|
|
19
|
-
cyan: "\e[36m", gray: "\e[90m"
|
|
20
|
-
}.freeze
|
|
21
|
-
|
|
22
|
-
class << self
|
|
23
|
-
def setup(root_dir = Dir.pwd)
|
|
24
|
-
log_dir = File.join(root_dir, "logs")
|
|
25
|
-
FileUtils.mkdir_p(log_dir)
|
|
26
|
-
log_file = File.join(log_dir, "debug.log")
|
|
27
|
-
@file_logger = Logger.new(log_file, 10, 5 * 1024 * 1024)
|
|
28
|
-
@file_logger.level = Logger::DEBUG
|
|
29
|
-
@console_logger = Logger.new($stdout)
|
|
30
|
-
@console_logger.level = resolve_level
|
|
31
|
-
@console_logger.formatter = method(:color_formatter)
|
|
32
|
-
@file_logger.formatter = method(:plain_formatter)
|
|
33
|
-
@initialized = true
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def info(message, *args)
|
|
37
|
-
log(:info, message, *args)
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def debug(message, *args)
|
|
41
|
-
log(:debug, message, *args)
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
def warning(message, *args)
|
|
45
|
-
log(:warn, message, *args)
|
|
46
|
-
end
|
|
3
|
+
# Backward compatibility: Tina4::Debug is now Tina4::Log
|
|
4
|
+
require_relative "log"
|
|
47
5
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
private
|
|
53
|
-
|
|
54
|
-
def log(level, message, *args)
|
|
55
|
-
setup unless @initialized
|
|
56
|
-
full_message = args.empty? ? message.to_s : "#{message} #{args.map(&:to_s).join(' ')}"
|
|
57
|
-
@console_logger.send(level, full_message)
|
|
58
|
-
@file_logger.send(level, full_message)
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
def resolve_level
|
|
62
|
-
env_level = ENV["TINA4_DEBUG_LEVEL"] || "[TINA4_LOG_ALL]"
|
|
63
|
-
LEVELS[env_level] || Logger::DEBUG
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
def color_formatter(severity, datetime, _progname, message)
|
|
67
|
-
color = case severity
|
|
68
|
-
when "DEBUG" then COLORS[:gray]
|
|
69
|
-
when "INFO" then COLORS[:green]
|
|
70
|
-
when "WARN" then COLORS[:yellow]
|
|
71
|
-
when "ERROR" then COLORS[:red]
|
|
72
|
-
else COLORS[:reset]
|
|
73
|
-
end
|
|
74
|
-
ts = datetime.strftime("%Y-%m-%d %H:%M:%S")
|
|
75
|
-
"#{COLORS[:gray]}[#{ts}]#{COLORS[:reset]} #{color}[#{severity}]#{COLORS[:reset]} #{message}\n"
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
def plain_formatter(severity, datetime, _progname, message)
|
|
79
|
-
"[#{datetime.strftime('%Y-%m-%d %H:%M:%S')}] [#{severity}] #{message}\n"
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
end
|
|
6
|
+
module Tina4
|
|
7
|
+
Debug = Log
|
|
83
8
|
end
|