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/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
|
-
@
|
|
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
|
|
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
|
-
@
|
|
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
|
|
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
|
-
|
|
161
|
+
query.each { |k, v| p[k] = v }
|
|
113
162
|
|
|
114
163
|
# Body params
|
|
115
|
-
|
|
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
|
|
127
|
-
|
|
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
|
-
|
|
176
|
+
result[URI.decode_www_form_component(key.to_s)] = URI.decode_www_form_component(value.to_s)
|
|
131
177
|
end
|
|
132
|
-
|
|
178
|
+
result
|
|
133
179
|
end
|
|
134
180
|
|
|
135
181
|
def extract_files
|
data/lib/tina4/response.rb
CHANGED
|
@@ -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 :
|
|
29
|
+
attr_accessor :status_code, :headers, :body, :cookies
|
|
30
30
|
|
|
31
31
|
def initialize
|
|
32
|
-
@
|
|
32
|
+
@status_code = 200
|
|
33
33
|
@headers = { "content-type" => HTML_CONTENT_TYPE }
|
|
34
34
|
@body = ""
|
|
35
|
-
@cookies = nil # Lazy
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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:
|
|
75
|
-
@
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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 <<
|
|
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 [@
|
|
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
|
-
[@
|
|
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.
|
|
190
|
+
response.status_code = result
|
|
161
191
|
response.body = ""
|
|
162
192
|
response
|
|
163
193
|
when NilClass
|
|
164
|
-
response.
|
|
194
|
+
response.status_code = 204
|
|
165
195
|
response.body = ""
|
|
166
196
|
response
|
|
167
197
|
else
|