tina4ruby 3.11.13 → 3.11.15

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 (132) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +80 -80
  3. data/LICENSE.txt +21 -21
  4. data/README.md +137 -137
  5. data/exe/tina4ruby +5 -5
  6. data/lib/tina4/ai.rb +696 -696
  7. data/lib/tina4/api.rb +189 -189
  8. data/lib/tina4/auth.rb +305 -305
  9. data/lib/tina4/auto_crud.rb +244 -244
  10. data/lib/tina4/cache.rb +154 -154
  11. data/lib/tina4/cli.rb +1449 -1449
  12. data/lib/tina4/constants.rb +46 -46
  13. data/lib/tina4/container.rb +74 -74
  14. data/lib/tina4/cors.rb +74 -74
  15. data/lib/tina4/crud.rb +692 -692
  16. data/lib/tina4/database/sqlite3_adapter.rb +165 -165
  17. data/lib/tina4/database.rb +625 -625
  18. data/lib/tina4/database_result.rb +208 -208
  19. data/lib/tina4/debug.rb +8 -8
  20. data/lib/tina4/dev.rb +14 -14
  21. data/lib/tina4/dev_admin.rb +935 -935
  22. data/lib/tina4/dev_mailbox.rb +191 -191
  23. data/lib/tina4/drivers/firebird_driver.rb +124 -110
  24. data/lib/tina4/drivers/mongodb_driver.rb +561 -561
  25. data/lib/tina4/drivers/mssql_driver.rb +112 -112
  26. data/lib/tina4/drivers/mysql_driver.rb +90 -90
  27. data/lib/tina4/drivers/odbc_driver.rb +191 -191
  28. data/lib/tina4/drivers/postgres_driver.rb +116 -106
  29. data/lib/tina4/drivers/sqlite_driver.rb +122 -122
  30. data/lib/tina4/env.rb +95 -95
  31. data/lib/tina4/error_overlay.rb +252 -252
  32. data/lib/tina4/events.rb +109 -109
  33. data/lib/tina4/field_types.rb +154 -154
  34. data/lib/tina4/frond.rb +2025 -2025
  35. data/lib/tina4/gallery/auth/meta.json +1 -1
  36. data/lib/tina4/gallery/auth/src/routes/api/gallery_auth.rb +114 -114
  37. data/lib/tina4/gallery/database/meta.json +1 -1
  38. data/lib/tina4/gallery/database/src/routes/api/gallery_db.rb +43 -43
  39. data/lib/tina4/gallery/error-overlay/meta.json +1 -1
  40. data/lib/tina4/gallery/error-overlay/src/routes/api/gallery_crash.rb +17 -17
  41. data/lib/tina4/gallery/orm/meta.json +1 -1
  42. data/lib/tina4/gallery/orm/src/routes/api/gallery_products.rb +16 -16
  43. data/lib/tina4/gallery/queue/meta.json +1 -1
  44. data/lib/tina4/gallery/queue/src/routes/api/gallery_queue.rb +325 -325
  45. data/lib/tina4/gallery/rest-api/meta.json +1 -1
  46. data/lib/tina4/gallery/rest-api/src/routes/api/gallery_hello.rb +14 -14
  47. data/lib/tina4/gallery/templates/meta.json +1 -1
  48. data/lib/tina4/gallery/templates/src/routes/gallery_page.rb +12 -12
  49. data/lib/tina4/gallery/templates/src/templates/gallery_page.twig +257 -257
  50. data/lib/tina4/graphql.rb +966 -966
  51. data/lib/tina4/health.rb +39 -39
  52. data/lib/tina4/html_element.rb +170 -170
  53. data/lib/tina4/job.rb +80 -80
  54. data/lib/tina4/localization.rb +168 -168
  55. data/lib/tina4/log.rb +203 -203
  56. data/lib/tina4/mcp.rb +696 -696
  57. data/lib/tina4/messenger.rb +587 -587
  58. data/lib/tina4/metrics.rb +793 -793
  59. data/lib/tina4/middleware.rb +445 -445
  60. data/lib/tina4/migration.rb +451 -451
  61. data/lib/tina4/orm.rb +790 -790
  62. data/lib/tina4/public/css/tina4.css +2463 -2463
  63. data/lib/tina4/public/css/tina4.min.css +1 -1
  64. data/lib/tina4/public/images/logo.svg +5 -5
  65. data/lib/tina4/public/js/frond.min.js +2 -2
  66. data/lib/tina4/public/js/tina4-dev-admin.js +565 -565
  67. data/lib/tina4/public/js/tina4-dev-admin.min.js +480 -480
  68. data/lib/tina4/public/js/tina4.min.js +92 -92
  69. data/lib/tina4/public/js/tina4js.min.js +48 -48
  70. data/lib/tina4/public/swagger/index.html +90 -90
  71. data/lib/tina4/public/swagger/oauth2-redirect.html +63 -63
  72. data/lib/tina4/query_builder.rb +380 -380
  73. data/lib/tina4/queue.rb +366 -366
  74. data/lib/tina4/queue_backends/kafka_backend.rb +80 -80
  75. data/lib/tina4/queue_backends/lite_backend.rb +298 -298
  76. data/lib/tina4/queue_backends/mongo_backend.rb +126 -126
  77. data/lib/tina4/queue_backends/rabbitmq_backend.rb +73 -73
  78. data/lib/tina4/rack_app.rb +817 -817
  79. data/lib/tina4/rate_limiter.rb +130 -130
  80. data/lib/tina4/request.rb +268 -255
  81. data/lib/tina4/response.rb +346 -346
  82. data/lib/tina4/response_cache.rb +551 -551
  83. data/lib/tina4/router.rb +406 -406
  84. data/lib/tina4/scss/tina4css/_alerts.scss +34 -34
  85. data/lib/tina4/scss/tina4css/_badges.scss +22 -22
  86. data/lib/tina4/scss/tina4css/_buttons.scss +69 -69
  87. data/lib/tina4/scss/tina4css/_cards.scss +49 -49
  88. data/lib/tina4/scss/tina4css/_forms.scss +156 -156
  89. data/lib/tina4/scss/tina4css/_grid.scss +81 -81
  90. data/lib/tina4/scss/tina4css/_modals.scss +84 -84
  91. data/lib/tina4/scss/tina4css/_nav.scss +149 -149
  92. data/lib/tina4/scss/tina4css/_reset.scss +94 -94
  93. data/lib/tina4/scss/tina4css/_tables.scss +54 -54
  94. data/lib/tina4/scss/tina4css/_typography.scss +55 -55
  95. data/lib/tina4/scss/tina4css/_utilities.scss +197 -197
  96. data/lib/tina4/scss/tina4css/_variables.scss +117 -117
  97. data/lib/tina4/scss/tina4css/base.scss +1 -1
  98. data/lib/tina4/scss/tina4css/colors.scss +48 -48
  99. data/lib/tina4/scss/tina4css/tina4.scss +17 -17
  100. data/lib/tina4/scss_compiler.rb +178 -178
  101. data/lib/tina4/seeder.rb +567 -567
  102. data/lib/tina4/service_runner.rb +303 -303
  103. data/lib/tina4/session.rb +297 -297
  104. data/lib/tina4/session_handlers/database_handler.rb +72 -72
  105. data/lib/tina4/session_handlers/file_handler.rb +67 -67
  106. data/lib/tina4/session_handlers/mongo_handler.rb +49 -49
  107. data/lib/tina4/session_handlers/redis_handler.rb +43 -43
  108. data/lib/tina4/session_handlers/valkey_handler.rb +43 -43
  109. data/lib/tina4/shutdown.rb +84 -84
  110. data/lib/tina4/sql_translation.rb +158 -158
  111. data/lib/tina4/swagger.rb +124 -124
  112. data/lib/tina4/template.rb +894 -894
  113. data/lib/tina4/templates/base.twig +26 -26
  114. data/lib/tina4/templates/errors/302.twig +14 -14
  115. data/lib/tina4/templates/errors/401.twig +9 -9
  116. data/lib/tina4/templates/errors/403.twig +29 -29
  117. data/lib/tina4/templates/errors/404.twig +29 -29
  118. data/lib/tina4/templates/errors/500.twig +38 -38
  119. data/lib/tina4/templates/errors/502.twig +9 -9
  120. data/lib/tina4/templates/errors/503.twig +12 -12
  121. data/lib/tina4/templates/errors/base.twig +37 -37
  122. data/lib/tina4/test_client.rb +159 -159
  123. data/lib/tina4/testing.rb +340 -340
  124. data/lib/tina4/validator.rb +174 -174
  125. data/lib/tina4/version.rb +1 -1
  126. data/lib/tina4/webserver.rb +312 -312
  127. data/lib/tina4/websocket.rb +343 -343
  128. data/lib/tina4/websocket_backplane.rb +190 -190
  129. data/lib/tina4/wsdl.rb +564 -564
  130. data/lib/tina4.rb +458 -458
  131. data/lib/tina4ruby.rb +4 -4
  132. metadata +3 -3
@@ -1,122 +1,122 @@
1
- # frozen_string_literal: true
2
-
3
- module Tina4
4
- module Drivers
5
- class SqliteDriver
6
- attr_reader :connection
7
-
8
- def connect(connection_string, username: nil, password: nil)
9
- require "sqlite3"
10
- db_path = self.class.resolve_path(connection_string)
11
-
12
- @connection = SQLite3::Database.new(db_path)
13
- @connection.results_as_hash = true
14
- @connection.execute("PRAGMA journal_mode=WAL")
15
- @connection.execute("PRAGMA foreign_keys=ON")
16
- end
17
-
18
- # Resolve a SQLite URL / path against the project root (cwd).
19
- #
20
- # Convention (matches tina4-python, tina4-php, tina4-nodejs):
21
- # sqlite::memory: → :memory:
22
- # sqlite:///:memory: → :memory:
23
- # sqlite:///app.db → {cwd}/app.db (relative)
24
- # sqlite:///data/app.db → {cwd}/data/app.db (relative; auto-mkdir under cwd)
25
- # sqlite:////var/data/app.db → /var/data/app.db (absolute; no auto-mkdir)
26
- # sqlite:///C:/Users/app.db → C:/Users/app.db (Windows absolute)
27
- #
28
- # Never mkdir outside cwd — that was the root cause of the
29
- # "Read-only file system: '/data'" crash on macOS.
30
- def self.resolve_path(connection_string)
31
- return ":memory:" if connection_string == "sqlite::memory:" || connection_string == "sqlite:///:memory:"
32
-
33
- # Strip the scheme + up to three slashes, preserving a potential fourth
34
- # slash (absolute) or drive letter.
35
- raw = connection_string.sub(/^sqlite:\/\/\//, "").sub(/^sqlite:\/\//, "").sub(/^sqlite:/, "")
36
- return ":memory:" if raw == ":memory:"
37
-
38
- is_windows_abs = raw.match?(/^[A-Za-z]:[\/\\]/)
39
- is_unix_abs = raw.start_with?("/")
40
-
41
- if is_windows_abs || is_unix_abs
42
- # Absolute — trust the user; don't auto-mkdir outside cwd.
43
- raw
44
- else
45
- # Relative — resolve under cwd; auto-mkdir parent dir.
46
- resolved = File.join(Dir.pwd, raw)
47
- parent = File.dirname(resolved)
48
- require "fileutils"
49
- FileUtils.mkdir_p(parent) unless File.directory?(parent)
50
- resolved
51
- end
52
- end
53
-
54
- def close
55
- @connection&.close
56
- end
57
-
58
- def execute_query(sql, params = [])
59
- results = @connection.execute(sql, params)
60
- results.map { |row| symbolize_keys(row) }
61
- end
62
-
63
- def execute(sql, params = [])
64
- @connection.execute(sql, params)
65
- end
66
-
67
- def last_insert_id
68
- @connection.last_insert_row_id
69
- end
70
-
71
- def placeholder
72
- "?"
73
- end
74
-
75
- def placeholders(count)
76
- (["?"] * count).join(", ")
77
- end
78
-
79
- def apply_limit(sql, limit, offset = 0)
80
- "#{sql} LIMIT #{limit} OFFSET #{offset}"
81
- end
82
-
83
- def begin_transaction
84
- @connection.execute("BEGIN TRANSACTION")
85
- end
86
-
87
- def commit
88
- @connection.execute("COMMIT")
89
- end
90
-
91
- def rollback
92
- @connection.execute("ROLLBACK")
93
- end
94
-
95
- def tables
96
- rows = execute_query("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'")
97
- rows.map { |r| r[:name] }
98
- end
99
-
100
- def columns(table_name)
101
- rows = execute_query("PRAGMA table_info(#{table_name})")
102
- rows.map do |r|
103
- {
104
- name: r[:name],
105
- type: r[:type],
106
- nullable: r[:notnull] == 0,
107
- default: r[:dflt_value],
108
- primary_key: r[:pk] == 1
109
- }
110
- end
111
- end
112
-
113
- private
114
-
115
- def symbolize_keys(hash)
116
- hash.each_with_object({}) do |(k, v), h|
117
- h[k.to_s.to_sym] = v if k.is_a?(String) || k.is_a?(Symbol)
118
- end
119
- end
120
- end
121
- end
122
- end
1
+ # frozen_string_literal: true
2
+
3
+ module Tina4
4
+ module Drivers
5
+ class SqliteDriver
6
+ attr_reader :connection
7
+
8
+ def connect(connection_string, username: nil, password: nil)
9
+ require "sqlite3"
10
+ db_path = self.class.resolve_path(connection_string)
11
+
12
+ @connection = SQLite3::Database.new(db_path)
13
+ @connection.results_as_hash = true
14
+ @connection.execute("PRAGMA journal_mode=WAL")
15
+ @connection.execute("PRAGMA foreign_keys=ON")
16
+ end
17
+
18
+ # Resolve a SQLite URL / path against the project root (cwd).
19
+ #
20
+ # Convention (matches tina4-python, tina4-php, tina4-nodejs):
21
+ # sqlite::memory: → :memory:
22
+ # sqlite:///:memory: → :memory:
23
+ # sqlite:///app.db → {cwd}/app.db (relative)
24
+ # sqlite:///data/app.db → {cwd}/data/app.db (relative; auto-mkdir under cwd)
25
+ # sqlite:////var/data/app.db → /var/data/app.db (absolute; no auto-mkdir)
26
+ # sqlite:///C:/Users/app.db → C:/Users/app.db (Windows absolute)
27
+ #
28
+ # Never mkdir outside cwd — that was the root cause of the
29
+ # "Read-only file system: '/data'" crash on macOS.
30
+ def self.resolve_path(connection_string)
31
+ return ":memory:" if connection_string == "sqlite::memory:" || connection_string == "sqlite:///:memory:"
32
+
33
+ # Strip the scheme + up to three slashes, preserving a potential fourth
34
+ # slash (absolute) or drive letter.
35
+ raw = connection_string.sub(/^sqlite:\/\/\//, "").sub(/^sqlite:\/\//, "").sub(/^sqlite:/, "")
36
+ return ":memory:" if raw == ":memory:"
37
+
38
+ is_windows_abs = raw.match?(/^[A-Za-z]:[\/\\]/)
39
+ is_unix_abs = raw.start_with?("/")
40
+
41
+ if is_windows_abs || is_unix_abs
42
+ # Absolute — trust the user; don't auto-mkdir outside cwd.
43
+ raw
44
+ else
45
+ # Relative — resolve under cwd; auto-mkdir parent dir.
46
+ resolved = File.join(Dir.pwd, raw)
47
+ parent = File.dirname(resolved)
48
+ require "fileutils"
49
+ FileUtils.mkdir_p(parent) unless File.directory?(parent)
50
+ resolved
51
+ end
52
+ end
53
+
54
+ def close
55
+ @connection&.close
56
+ end
57
+
58
+ def execute_query(sql, params = [])
59
+ results = @connection.execute(sql, params)
60
+ results.map { |row| symbolize_keys(row) }
61
+ end
62
+
63
+ def execute(sql, params = [])
64
+ @connection.execute(sql, params)
65
+ end
66
+
67
+ def last_insert_id
68
+ @connection.last_insert_row_id
69
+ end
70
+
71
+ def placeholder
72
+ "?"
73
+ end
74
+
75
+ def placeholders(count)
76
+ (["?"] * count).join(", ")
77
+ end
78
+
79
+ def apply_limit(sql, limit, offset = 0)
80
+ "#{sql} LIMIT #{limit} OFFSET #{offset}"
81
+ end
82
+
83
+ def begin_transaction
84
+ @connection.execute("BEGIN TRANSACTION")
85
+ end
86
+
87
+ def commit
88
+ @connection.execute("COMMIT")
89
+ end
90
+
91
+ def rollback
92
+ @connection.execute("ROLLBACK")
93
+ end
94
+
95
+ def tables
96
+ rows = execute_query("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'")
97
+ rows.map { |r| r[:name] }
98
+ end
99
+
100
+ def columns(table_name)
101
+ rows = execute_query("PRAGMA table_info(#{table_name})")
102
+ rows.map do |r|
103
+ {
104
+ name: r[:name],
105
+ type: r[:type],
106
+ nullable: r[:notnull] == 0,
107
+ default: r[:dflt_value],
108
+ primary_key: r[:pk] == 1
109
+ }
110
+ end
111
+ end
112
+
113
+ private
114
+
115
+ def symbolize_keys(hash)
116
+ hash.each_with_object({}) do |(k, v), h|
117
+ h[k.to_s.to_sym] = v if k.is_a?(String) || k.is_a?(Symbol)
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end
data/lib/tina4/env.rb CHANGED
@@ -1,95 +1,95 @@
1
- # frozen_string_literal: true
2
- require "digest"
3
-
4
- module Tina4
5
- module Env
6
- DEFAULT_ENV = {
7
- "PROJECT_NAME" => "Tina4 Ruby Project",
8
- "VERSION" => "1.0.0",
9
- "TINA4_LOCALE" => "en",
10
- "TINA4_DEBUG" => "true",
11
- "TINA4_LOG_LEVEL" => "[TINA4_LOG_ALL]",
12
- "SECRET" => "tina4-secret-change-me"
13
- }.freeze
14
-
15
- # Check if a value is truthy for env boolean checks.
16
- #
17
- # Accepts: "true", "True", "TRUE", "1", "yes", "Yes", "YES", "on", "On", "ON".
18
- # Everything else is falsy (including empty string, nil, not set).
19
- def self.is_truthy(val)
20
- %w[true 1 yes on].include?(val.to_s.strip.downcase)
21
- end
22
-
23
- class << self
24
- def load_env(root_dir = Dir.pwd)
25
- env_file = resolve_env_file(root_dir)
26
- unless File.exist?(env_file)
27
- create_default_env(env_file)
28
- end
29
- parse_env_file(env_file)
30
- end
31
-
32
- # Get an env var value, with optional default
33
- def get_env(key, default = nil)
34
- ENV[key.to_s] || default
35
- end
36
-
37
- # Check if an env var exists
38
- def has_env?(key)
39
- ENV.key?(key.to_s)
40
- end
41
-
42
- # Return all current ENV vars as a hash
43
- def all_env
44
- ENV.to_h
45
- end
46
-
47
- # Raise if any of the given keys are missing from ENV
48
- def require_env!(*keys)
49
- missing = keys.map(&:to_s).reject { |k| ENV.key?(k) }
50
- unless missing.empty?
51
- raise KeyError, "Missing required env vars: #{missing.join(', ')}"
52
- end
53
- end
54
-
55
- # Reset: clear all env vars that were loaded (restore to process defaults)
56
- def reset_env
57
- @loaded_keys&.each { |k| ENV.delete(k) }
58
- @loaded_keys = []
59
- end
60
-
61
- private
62
-
63
- def resolve_env_file(root_dir)
64
- environment = ENV["ENVIRONMENT"]
65
- if environment && !environment.empty?
66
- candidate = File.join(root_dir, ".env.#{environment}")
67
- return candidate if File.exist?(candidate)
68
- end
69
- File.join(root_dir, ".env")
70
- end
71
-
72
- def create_default_env(path)
73
- api_key = Digest::MD5.hexdigest(Time.now.to_s)
74
- content = DEFAULT_ENV.map { |k, v| "#{k}=\"#{v}\"" }.join("\n")
75
- content += "\nAPI_KEY=\"#{api_key}\"\n"
76
- File.write(path, content)
77
- end
78
-
79
- def parse_env_file(path)
80
- return unless File.exist?(path)
81
- File.readlines(path).each do |line|
82
- line = line.strip
83
- next if line.empty? || line.start_with?("#")
84
- if (match = line.match(/\A([A-Za-z_][A-Za-z0-9_]*)=["']?(.*)["']?\z/))
85
- key = match[1]
86
- value = match[2].gsub(/["']\z/, "")
87
- ENV[key] ||= value
88
- @loaded_keys ||= []
89
- @loaded_keys << key
90
- end
91
- end
92
- end
93
- end
94
- end
95
- end
1
+ # frozen_string_literal: true
2
+ require "digest"
3
+
4
+ module Tina4
5
+ module Env
6
+ DEFAULT_ENV = {
7
+ "PROJECT_NAME" => "Tina4 Ruby Project",
8
+ "VERSION" => "1.0.0",
9
+ "TINA4_LOCALE" => "en",
10
+ "TINA4_DEBUG" => "true",
11
+ "TINA4_LOG_LEVEL" => "[TINA4_LOG_ALL]",
12
+ "SECRET" => "tina4-secret-change-me"
13
+ }.freeze
14
+
15
+ # Check if a value is truthy for env boolean checks.
16
+ #
17
+ # Accepts: "true", "True", "TRUE", "1", "yes", "Yes", "YES", "on", "On", "ON".
18
+ # Everything else is falsy (including empty string, nil, not set).
19
+ def self.is_truthy(val)
20
+ %w[true 1 yes on].include?(val.to_s.strip.downcase)
21
+ end
22
+
23
+ class << self
24
+ def load_env(root_dir = Dir.pwd)
25
+ env_file = resolve_env_file(root_dir)
26
+ unless File.exist?(env_file)
27
+ create_default_env(env_file)
28
+ end
29
+ parse_env_file(env_file)
30
+ end
31
+
32
+ # Get an env var value, with optional default
33
+ def get_env(key, default = nil)
34
+ ENV[key.to_s] || default
35
+ end
36
+
37
+ # Check if an env var exists
38
+ def has_env?(key)
39
+ ENV.key?(key.to_s)
40
+ end
41
+
42
+ # Return all current ENV vars as a hash
43
+ def all_env
44
+ ENV.to_h
45
+ end
46
+
47
+ # Raise if any of the given keys are missing from ENV
48
+ def require_env!(*keys)
49
+ missing = keys.map(&:to_s).reject { |k| ENV.key?(k) }
50
+ unless missing.empty?
51
+ raise KeyError, "Missing required env vars: #{missing.join(', ')}"
52
+ end
53
+ end
54
+
55
+ # Reset: clear all env vars that were loaded (restore to process defaults)
56
+ def reset_env
57
+ @loaded_keys&.each { |k| ENV.delete(k) }
58
+ @loaded_keys = []
59
+ end
60
+
61
+ private
62
+
63
+ def resolve_env_file(root_dir)
64
+ environment = ENV["ENVIRONMENT"]
65
+ if environment && !environment.empty?
66
+ candidate = File.join(root_dir, ".env.#{environment}")
67
+ return candidate if File.exist?(candidate)
68
+ end
69
+ File.join(root_dir, ".env")
70
+ end
71
+
72
+ def create_default_env(path)
73
+ api_key = Digest::MD5.hexdigest(Time.now.to_s)
74
+ content = DEFAULT_ENV.map { |k, v| "#{k}=\"#{v}\"" }.join("\n")
75
+ content += "\nAPI_KEY=\"#{api_key}\"\n"
76
+ File.write(path, content)
77
+ end
78
+
79
+ def parse_env_file(path)
80
+ return unless File.exist?(path)
81
+ File.readlines(path).each do |line|
82
+ line = line.strip
83
+ next if line.empty? || line.start_with?("#")
84
+ if (match = line.match(/\A([A-Za-z_][A-Za-z0-9_]*)=["']?(.*)["']?\z/))
85
+ key = match[1]
86
+ value = match[2].gsub(/["']\z/, "")
87
+ ENV[key] ||= value
88
+ @loaded_keys ||= []
89
+ @loaded_keys << key
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end