tina4ruby 0.5.2 → 3.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 +4 -4
- data/CHANGELOG.md +1 -1
- data/README.md +360 -559
- 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 +242 -77
- 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 +43 -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 +1336 -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 +27 -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 +484 -0
- data/lib/tina4/migration.rb +132 -29
- data/lib/tina4/orm.rb +337 -31
- 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 +40 -4
- data/lib/tina4/queue_backends/lite_backend.rb +88 -0
- data/lib/tina4/rack_app.rb +314 -23
- 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 +134 -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 +57 -21
- metadata +51 -19
- data/lib/tina4/public/js/tina4.js +0 -134
- data/lib/tina4/public/js/tina4helper.js +0 -387
|
@@ -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,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
require "json"
|
|
3
|
+
require "uri"
|
|
3
4
|
|
|
4
5
|
module Tina4
|
|
5
6
|
class Database
|
|
@@ -11,26 +12,27 @@ module Tina4
|
|
|
11
12
|
"postgres" => "Tina4::Drivers::PostgresDriver",
|
|
12
13
|
"postgresql" => "Tina4::Drivers::PostgresDriver",
|
|
13
14
|
"mysql" => "Tina4::Drivers::MysqlDriver",
|
|
14
|
-
"mysql2" => "Tina4::Drivers::MysqlDriver",
|
|
15
15
|
"mssql" => "Tina4::Drivers::MssqlDriver",
|
|
16
16
|
"sqlserver" => "Tina4::Drivers::MssqlDriver",
|
|
17
17
|
"firebird" => "Tina4::Drivers::FirebirdDriver"
|
|
18
18
|
}.freeze
|
|
19
19
|
|
|
20
|
-
def initialize(connection_string, driver_name: nil)
|
|
21
|
-
@connection_string = connection_string
|
|
22
|
-
@
|
|
20
|
+
def initialize(connection_string = nil, username: nil, password: nil, driver_name: nil)
|
|
21
|
+
@connection_string = connection_string || ENV["DATABASE_URL"]
|
|
22
|
+
@username = username || ENV["DATABASE_USERNAME"]
|
|
23
|
+
@password = password || ENV["DATABASE_PASSWORD"]
|
|
24
|
+
@driver_name = driver_name || detect_driver(@connection_string)
|
|
23
25
|
@driver = create_driver
|
|
24
26
|
@connected = false
|
|
25
27
|
connect
|
|
26
28
|
end
|
|
27
29
|
|
|
28
30
|
def connect
|
|
29
|
-
@driver.connect(@connection_string)
|
|
31
|
+
@driver.connect(@connection_string, username: @username, password: @password)
|
|
30
32
|
@connected = true
|
|
31
|
-
Tina4::
|
|
33
|
+
Tina4::Log.info("Database connected: #{@driver_name}")
|
|
32
34
|
rescue => e
|
|
33
|
-
Tina4::
|
|
35
|
+
Tina4::Log.error("Database connection failed: #{e.message}")
|
|
34
36
|
@connected = false
|
|
35
37
|
end
|
|
36
38
|
|
|
@@ -54,6 +56,16 @@ module Tina4
|
|
|
54
56
|
end
|
|
55
57
|
|
|
56
58
|
def insert(table, data)
|
|
59
|
+
# List of hashes — batch insert
|
|
60
|
+
if data.is_a?(Array)
|
|
61
|
+
return { success: true, affected_rows: 0 } if data.empty?
|
|
62
|
+
keys = data.first.keys.map(&:to_s)
|
|
63
|
+
placeholders = @driver.placeholders(keys.length)
|
|
64
|
+
sql = "INSERT INTO #{table} (#{keys.join(', ')}) VALUES (#{placeholders})"
|
|
65
|
+
params_list = data.map { |row| keys.map { |k| row[k.to_sym] || row[k] } }
|
|
66
|
+
return execute_many(sql, params_list)
|
|
67
|
+
end
|
|
68
|
+
|
|
57
69
|
columns = data.keys.map(&:to_s)
|
|
58
70
|
placeholders = @driver.placeholders(columns.length)
|
|
59
71
|
sql = "INSERT INTO #{table} (#{columns.join(', ')}) VALUES (#{placeholders})"
|
|
@@ -72,6 +84,21 @@ module Tina4
|
|
|
72
84
|
end
|
|
73
85
|
|
|
74
86
|
def delete(table, filter = {})
|
|
87
|
+
# List of hashes — delete each row
|
|
88
|
+
if filter.is_a?(Array)
|
|
89
|
+
filter.each { |row| delete(table, row) }
|
|
90
|
+
return { success: true }
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# String filter — raw WHERE clause
|
|
94
|
+
if filter.is_a?(String)
|
|
95
|
+
sql = "DELETE FROM #{table}"
|
|
96
|
+
sql += " WHERE #{filter}" unless filter.empty?
|
|
97
|
+
@driver.execute(sql)
|
|
98
|
+
return { success: true }
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Hash filter — build WHERE from keys
|
|
75
102
|
where_parts = filter.keys.map { |k| "#{k} = #{@driver.placeholder}" }
|
|
76
103
|
sql = "DELETE FROM #{table}"
|
|
77
104
|
sql += " WHERE #{where_parts.join(' AND ')}" unless filter.empty?
|
|
@@ -83,6 +110,15 @@ module Tina4
|
|
|
83
110
|
@driver.execute(sql, params)
|
|
84
111
|
end
|
|
85
112
|
|
|
113
|
+
def execute_many(sql, params_list = [])
|
|
114
|
+
total_affected = 0
|
|
115
|
+
params_list.each do |params|
|
|
116
|
+
@driver.execute(sql, params)
|
|
117
|
+
total_affected += 1
|
|
118
|
+
end
|
|
119
|
+
{ success: true, affected_rows: total_affected }
|
|
120
|
+
end
|
|
121
|
+
|
|
86
122
|
def transaction
|
|
87
123
|
@driver.begin_transaction
|
|
88
124
|
yield self
|
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
|