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.
Files changed (90) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1 -1
  3. data/README.md +360 -559
  4. data/exe/{tina4 → tina4ruby} +1 -0
  5. data/lib/tina4/ai.rb +312 -0
  6. data/lib/tina4/auth.rb +44 -3
  7. data/lib/tina4/auto_crud.rb +163 -0
  8. data/lib/tina4/cli.rb +242 -77
  9. data/lib/tina4/constants.rb +46 -0
  10. data/lib/tina4/cors.rb +74 -0
  11. data/lib/tina4/database/sqlite3_adapter.rb +139 -0
  12. data/lib/tina4/database.rb +43 -7
  13. data/lib/tina4/debug.rb +4 -79
  14. data/lib/tina4/dev_admin.rb +1162 -0
  15. data/lib/tina4/dev_mailbox.rb +191 -0
  16. data/lib/tina4/dev_reload.rb +9 -9
  17. data/lib/tina4/drivers/firebird_driver.rb +19 -3
  18. data/lib/tina4/drivers/mssql_driver.rb +3 -3
  19. data/lib/tina4/drivers/mysql_driver.rb +4 -4
  20. data/lib/tina4/drivers/postgres_driver.rb +9 -2
  21. data/lib/tina4/drivers/sqlite_driver.rb +1 -1
  22. data/lib/tina4/env.rb +42 -2
  23. data/lib/tina4/error_overlay.rb +252 -0
  24. data/lib/tina4/events.rb +90 -0
  25. data/lib/tina4/field_types.rb +4 -0
  26. data/lib/tina4/frond.rb +1336 -0
  27. data/lib/tina4/gallery/auth/meta.json +1 -0
  28. data/lib/tina4/gallery/auth/src/routes/api/gallery_auth.rb +114 -0
  29. data/lib/tina4/gallery/database/meta.json +1 -0
  30. data/lib/tina4/gallery/database/src/routes/api/gallery_db.rb +43 -0
  31. data/lib/tina4/gallery/error-overlay/meta.json +1 -0
  32. data/lib/tina4/gallery/error-overlay/src/routes/api/gallery_crash.rb +17 -0
  33. data/lib/tina4/gallery/orm/meta.json +1 -0
  34. data/lib/tina4/gallery/orm/src/routes/api/gallery_products.rb +16 -0
  35. data/lib/tina4/gallery/queue/meta.json +1 -0
  36. data/lib/tina4/gallery/queue/src/routes/api/gallery_queue.rb +27 -0
  37. data/lib/tina4/gallery/rest-api/meta.json +1 -0
  38. data/lib/tina4/gallery/rest-api/src/routes/api/gallery_hello.rb +14 -0
  39. data/lib/tina4/gallery/templates/meta.json +1 -0
  40. data/lib/tina4/gallery/templates/src/routes/gallery_page.rb +12 -0
  41. data/lib/tina4/gallery/templates/src/templates/gallery_page.twig +257 -0
  42. data/lib/tina4/health.rb +39 -0
  43. data/lib/tina4/html_element.rb +148 -0
  44. data/lib/tina4/localization.rb +2 -2
  45. data/lib/tina4/log.rb +203 -0
  46. data/lib/tina4/messenger.rb +484 -0
  47. data/lib/tina4/migration.rb +132 -29
  48. data/lib/tina4/orm.rb +337 -31
  49. data/lib/tina4/public/css/tina4.css +178 -1
  50. data/lib/tina4/public/css/tina4.min.css +1 -2
  51. data/lib/tina4/public/favicon.ico +0 -0
  52. data/lib/tina4/public/images/logo.svg +5 -0
  53. data/lib/tina4/public/images/tina4-logo-icon.webp +0 -0
  54. data/lib/tina4/public/js/frond.min.js +420 -0
  55. data/lib/tina4/public/js/tina4-dev-admin.min.js +367 -0
  56. data/lib/tina4/public/js/tina4.min.js +93 -0
  57. data/lib/tina4/public/swagger/index.html +90 -0
  58. data/lib/tina4/public/swagger/oauth2-redirect.html +63 -0
  59. data/lib/tina4/queue.rb +40 -4
  60. data/lib/tina4/queue_backends/lite_backend.rb +88 -0
  61. data/lib/tina4/rack_app.rb +314 -23
  62. data/lib/tina4/rate_limiter.rb +123 -0
  63. data/lib/tina4/request.rb +61 -15
  64. data/lib/tina4/response.rb +54 -24
  65. data/lib/tina4/response_cache.rb +134 -0
  66. data/lib/tina4/router.rb +90 -15
  67. data/lib/tina4/scss_compiler.rb +2 -2
  68. data/lib/tina4/seeder.rb +56 -61
  69. data/lib/tina4/service_runner.rb +303 -0
  70. data/lib/tina4/session.rb +85 -0
  71. data/lib/tina4/session_handlers/mongo_handler.rb +1 -1
  72. data/lib/tina4/session_handlers/valkey_handler.rb +43 -0
  73. data/lib/tina4/shutdown.rb +84 -0
  74. data/lib/tina4/sql_translation.rb +295 -0
  75. data/lib/tina4/template.rb +36 -6
  76. data/lib/tina4/templates/base.twig +2 -2
  77. data/lib/tina4/templates/errors/302.twig +14 -0
  78. data/lib/tina4/templates/errors/401.twig +9 -0
  79. data/lib/tina4/templates/errors/403.twig +22 -15
  80. data/lib/tina4/templates/errors/404.twig +22 -15
  81. data/lib/tina4/templates/errors/500.twig +31 -15
  82. data/lib/tina4/templates/errors/502.twig +9 -0
  83. data/lib/tina4/templates/errors/503.twig +12 -0
  84. data/lib/tina4/templates/errors/base.twig +37 -0
  85. data/lib/tina4/version.rb +1 -1
  86. data/lib/tina4/webserver.rb +28 -18
  87. data/lib/tina4.rb +57 -21
  88. metadata +51 -19
  89. data/lib/tina4/public/js/tina4.js +0 -134
  90. 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
@@ -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
- @driver_name = driver_name || detect_driver(connection_string)
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::Debug.info("Database connected: #{@driver_name}")
33
+ Tina4::Log.info("Database connected: #{@driver_name}")
32
34
  rescue => e
33
- Tina4::Debug.error("Database connection failed: #{e.message}")
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
- module Tina4
6
- module Debug
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
- def error(message, *args)
49
- log(:error, message, *args)
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