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.
Files changed (90) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1 -1
  3. data/README.md +434 -544
  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 +389 -97
  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 +144 -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 +1497 -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 +325 -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 +562 -0
  47. data/lib/tina4/migration.rb +132 -29
  48. data/lib/tina4/orm.rb +463 -35
  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 +162 -6
  60. data/lib/tina4/queue_backends/lite_backend.rb +88 -0
  61. data/lib/tina4/rack_app.rb +331 -27
  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 +551 -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 +118 -21
  88. metadata +68 -8
  89. data/lib/tina4/public/js/tina4.js +0 -134
  90. data/lib/tina4/public/js/tina4helper.js +0 -387
data/lib/tina4/request.rb CHANGED
@@ -13,20 +13,36 @@ module Tina4
13
13
  @path = env["PATH_INFO"] || "/"
14
14
  @query_string = env["QUERY_STRING"] || ""
15
15
  @content_type = env["CONTENT_TYPE"] || ""
16
- @ip = env["REMOTE_ADDR"] || "127.0.0.1"
17
16
  @path_params = path_params
18
17
 
18
+ # Client IP with X-Forwarded-For support
19
+ @ip = extract_client_ip
20
+
19
21
  # Lazy-initialized fields (nil = not yet computed)
20
22
  @headers = nil
21
23
  @cookies = nil
22
24
  @session = nil
23
- @body = nil
25
+ @body_raw = nil
24
26
  @params = nil
25
27
  @files = nil
26
28
  @json_body = nil
29
+ @query_hash = nil
30
+ @body_parsed = nil
31
+ end
32
+
33
+ # Full URL reconstruction
34
+ def url
35
+ scheme = env["rack.url_scheme"] || "http"
36
+ host = env["HTTP_HOST"] || env["SERVER_NAME"] || "localhost"
37
+ port = env["SERVER_PORT"]
38
+ url_str = "#{scheme}://#{host}"
39
+ url_str += ":#{port}" if port && port != "80" && port != "443"
40
+ url_str += @path
41
+ url_str += "?#{@query_string}" unless @query_string.empty?
42
+ url_str
27
43
  end
28
44
 
29
- # Lazy accessors — only compute when needed
45
+ # Lazy accessors
30
46
  def headers
31
47
  @headers ||= extract_headers
32
48
  end
@@ -39,14 +55,26 @@ module Tina4
39
55
  @session ||= @env["tina4.session"] || {}
40
56
  end
41
57
 
58
+ # Raw body string
42
59
  def body
43
- @body ||= read_body
60
+ @body_raw ||= read_body
61
+ end
62
+
63
+ # Parsed body (JSON or form data)
64
+ def body_parsed
65
+ @body_parsed ||= parse_body
66
+ end
67
+
68
+ # Parsed query string as hash
69
+ def query
70
+ @query_hash ||= parse_query_to_hash(@query_string)
44
71
  end
45
72
 
46
73
  def files
47
74
  @files ||= extract_files
48
75
  end
49
76
 
77
+ # Merged params: query + body + path_params (path_params highest priority)
50
78
  def params
51
79
  @params ||= build_params
52
80
  end
@@ -74,11 +102,22 @@ module Tina4
74
102
 
75
103
  private
76
104
 
105
+ def extract_client_ip
106
+ # Check X-Forwarded-For first (proxy/load balancer)
107
+ forwarded = @env["HTTP_X_FORWARDED_FOR"]
108
+ if forwarded && !forwarded.empty?
109
+ # Take the first (original client) IP
110
+ forwarded.split(",").first.strip
111
+ else
112
+ @env["HTTP_X_REAL_IP"] || @env["REMOTE_ADDR"] || "127.0.0.1"
113
+ end
114
+ end
115
+
77
116
  def extract_headers
78
117
  h = {}
79
118
  @env.each do |key, value|
80
119
  if key.start_with?("HTTP_")
81
- h[key[5..].downcase] = value
120
+ h[key[5..-1].downcase] = value
82
121
  end
83
122
  end
84
123
  h
@@ -105,31 +144,38 @@ module Tina4
105
144
  data
106
145
  end
107
146
 
147
+ def parse_body
148
+ if @content_type.include?("application/json")
149
+ json_body
150
+ elsif @content_type.include?("application/x-www-form-urlencoded")
151
+ parse_query_to_hash(body)
152
+ else
153
+ {}
154
+ end
155
+ end
156
+
108
157
  def build_params
109
158
  p = {}
110
159
 
111
160
  # Query string params
112
- parse_query_string(@query_string, p) unless @query_string.empty?
161
+ query.each { |k, v| p[k] = v }
113
162
 
114
163
  # Body params
115
- if @content_type.include?("application/json")
116
- json_body.each { |k, v| p[k.to_s] = v }
117
- elsif @content_type.include?("application/x-www-form-urlencoded")
118
- parse_query_string(body, p)
119
- end
164
+ body_parsed.each { |k, v| p[k.to_s] = v }
120
165
 
121
166
  # Path params (highest priority)
122
167
  @path_params.each { |k, v| p[k.to_s] = v }
123
168
  p
124
169
  end
125
170
 
126
- def parse_query_string(qs, target = {})
127
- return target if qs.nil? || qs.empty?
171
+ def parse_query_to_hash(qs)
172
+ result = {}
173
+ return result if qs.nil? || qs.empty?
128
174
  qs.split("&").each do |pair|
129
175
  key, value = pair.split("=", 2)
130
- target[URI.decode_www_form_component(key.to_s)] = URI.decode_www_form_component(value.to_s)
176
+ result[URI.decode_www_form_component(key.to_s)] = URI.decode_www_form_component(value.to_s)
131
177
  end
132
- target
178
+ result
133
179
  end
134
180
 
135
181
  def extract_files
@@ -26,53 +26,63 @@ module Tina4
26
26
  TEXT_CONTENT_TYPE = "text/plain; charset=utf-8"
27
27
  XML_CONTENT_TYPE = "application/xml; charset=utf-8"
28
28
 
29
- attr_accessor :status, :headers, :body, :cookies
29
+ attr_accessor :status_code, :headers, :body, :cookies
30
30
 
31
31
  def initialize
32
- @status = 200
32
+ @status_code = 200
33
33
  @headers = { "content-type" => HTML_CONTENT_TYPE }
34
34
  @body = ""
35
- @cookies = nil # Lazy most responses have no cookies
35
+ @cookies = nil # Lazy -- most responses have no cookies
36
+ end
37
+
38
+ # Chainable status setter
39
+ def status(code = nil)
40
+ if code.nil?
41
+ @status_code
42
+ else
43
+ @status_code = code
44
+ self
45
+ end
36
46
  end
37
47
 
38
48
  def json(data, status_or_opts = nil, status: nil)
39
- @status = status || (status_or_opts.is_a?(Integer) ? status_or_opts : 200)
49
+ @status_code = status || (status_or_opts.is_a?(Integer) ? status_or_opts : 200)
40
50
  @headers["content-type"] = JSON_CONTENT_TYPE
41
51
  @body = data.is_a?(String) ? data : JSON.generate(data)
42
52
  self
43
53
  end
44
54
 
45
55
  def html(content, status_or_opts = nil, status: nil)
46
- @status = status || (status_or_opts.is_a?(Integer) ? status_or_opts : 200)
56
+ @status_code = status || (status_or_opts.is_a?(Integer) ? status_or_opts : 200)
47
57
  @headers["content-type"] = HTML_CONTENT_TYPE
48
58
  @body = content.to_s
49
59
  self
50
60
  end
51
61
 
52
62
  def text(content, status_or_opts = nil, status: nil)
53
- @status = status || (status_or_opts.is_a?(Integer) ? status_or_opts : 200)
63
+ @status_code = status || (status_or_opts.is_a?(Integer) ? status_or_opts : 200)
54
64
  @headers["content-type"] = TEXT_CONTENT_TYPE
55
65
  @body = content.to_s
56
66
  self
57
67
  end
58
68
 
59
69
  def xml(content, status: 200)
60
- @status = status
70
+ @status_code = status
61
71
  @headers["content-type"] = XML_CONTENT_TYPE
62
72
  @body = content.to_s
63
73
  self
64
74
  end
65
75
 
66
76
  def csv(content, filename: "export.csv", status: 200)
67
- @status = status
77
+ @status_code = status
68
78
  @headers["content-type"] = "text/csv"
69
79
  @headers["content-disposition"] = "attachment; filename=\"#{filename}\""
70
80
  @body = content.to_s
71
81
  self
72
82
  end
73
83
 
74
- def redirect(url, status: 302)
75
- @status = status
84
+ def redirect(url, status_or_opts = nil, status: nil)
85
+ @status_code = status || (status_or_opts.is_a?(Integer) ? status_or_opts : 302)
76
86
  @headers["location"] = url
77
87
  @body = ""
78
88
  self
@@ -80,7 +90,7 @@ module Tina4
80
90
 
81
91
  def file(path, content_type: nil, download: false)
82
92
  unless ::File.exist?(path)
83
- @status = 404
93
+ @status_code = 404
84
94
  @body = "File not found"
85
95
  return self
86
96
  end
@@ -94,22 +104,37 @@ module Tina4
94
104
  end
95
105
 
96
106
  def render(template_path, data = {}, status: 200)
97
- @status = status
107
+ @status_code = status
98
108
  @headers["content-type"] = HTML_CONTENT_TYPE
99
109
  @body = Tina4::Template.render(template_path, data)
100
110
  self
101
111
  end
102
112
 
113
+ # Chainable header setter
114
+ def header(name, value = nil)
115
+ if value.nil?
116
+ @headers[name]
117
+ else
118
+ @headers[name] = value
119
+ self
120
+ end
121
+ end
122
+
123
+ # Chainable cookie setter
124
+ def cookie(name, value, opts = {})
125
+ set_cookie(name, value, opts)
126
+ end
127
+
103
128
  def set_cookie(name, value, opts = {})
104
- cookie = "#{name}=#{URI.encode_www_form_component(value)}"
105
- cookie += "; Path=#{opts[:path] || '/'}"
106
- cookie += "; HttpOnly" if opts.fetch(:http_only, true)
107
- cookie += "; Secure" if opts[:secure]
108
- cookie += "; SameSite=#{opts[:same_site] || 'Lax'}"
109
- cookie += "; Max-Age=#{opts[:max_age]}" if opts[:max_age]
110
- cookie += "; Expires=#{opts[:expires].httpdate}" if opts[:expires]
129
+ cookie_str = "#{name}=#{URI.encode_www_form_component(value)}"
130
+ cookie_str += "; Path=#{opts[:path] || '/'}"
131
+ cookie_str += "; HttpOnly" if opts.fetch(:http_only, true)
132
+ cookie_str += "; Secure" if opts[:secure]
133
+ cookie_str += "; SameSite=#{opts[:same_site] || 'Lax'}"
134
+ cookie_str += "; Max-Age=#{opts[:max_age]}" if opts[:max_age]
135
+ cookie_str += "; Expires=#{opts[:expires].httpdate}" if opts[:expires]
111
136
  @cookies ||= []
112
- @cookies << cookie
137
+ @cookies << cookie_str
113
138
  self
114
139
  end
115
140
 
@@ -132,16 +157,21 @@ module Tina4
132
157
  self
133
158
  end
134
159
 
160
+ # Flush / finalize -- alias for to_rack for semantic clarity
161
+ def send
162
+ to_rack
163
+ end
164
+
135
165
  def to_rack
136
166
  # Fast path: no cookies (99% of API responses)
137
167
  if @cookies.nil? || @cookies.empty?
138
- return [@status, @headers, [@body.to_s]]
168
+ return [@status_code, @headers, [@body.to_s]]
139
169
  end
140
170
 
141
171
  # Cookie path
142
172
  final_headers = @headers.dup
143
173
  final_headers["set-cookie"] = @cookies.join("\n")
144
- [@status, final_headers, [@body.to_s]]
174
+ [@status_code, final_headers, [@body.to_s]]
145
175
  end
146
176
 
147
177
  def self.auto_detect(result, response)
@@ -157,11 +187,11 @@ module Tina4
157
187
  response.text(result)
158
188
  end
159
189
  when Integer
160
- response.status = result
190
+ response.status_code = result
161
191
  response.body = ""
162
192
  response
163
193
  when NilClass
164
- response.status = 204
194
+ response.status_code = 204
165
195
  response.body = ""
166
196
  response
167
197
  else