tina4ruby 3.11.35 → 3.12.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/lib/tina4/auth.rb +5 -5
- data/lib/tina4/background.rb +81 -0
- data/lib/tina4/constants.rb +40 -0
- data/lib/tina4/container.rb +1 -1
- data/lib/tina4/database.rb +37 -10
- data/lib/tina4/dev_admin.rb +464 -2
- data/lib/tina4/docs.rb +636 -0
- data/lib/tina4/drivers/postgres_driver.rb +38 -4
- data/lib/tina4/env.rb +74 -3
- data/lib/tina4/field_types.rb +1 -1
- data/lib/tina4/frond.rb +62 -0
- data/lib/tina4/mcp.rb +191 -1
- data/lib/tina4/messenger.rb +13 -14
- data/lib/tina4/orm.rb +85 -12
- data/lib/tina4/plan.rb +471 -0
- data/lib/tina4/project_index.rb +366 -0
- data/lib/tina4/public/js/frond.js +600 -0
- data/lib/tina4/public/js/frond.min.js +1 -1
- data/lib/tina4/public/js/tina4-dev-admin.js +1086 -238
- data/lib/tina4/public/js/tina4-dev-admin.min.js +1142 -209
- data/lib/tina4/rack_app.rb +98 -16
- data/lib/tina4/response.rb +3 -0
- data/lib/tina4/session.rb +1 -1
- data/lib/tina4/session_handlers/database_handler.rb +1 -1
- data/lib/tina4/shutdown.rb +10 -0
- data/lib/tina4/swagger.rb +3 -3
- data/lib/tina4/version.rb +1 -1
- data/lib/tina4/webserver.rb +3 -0
- data/lib/tina4.rb +15 -1
- metadata +6 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 271f281382fed50cadd4ca76a86ecdd5273f5b9f8e80ed721d147e75aa197704
|
|
4
|
+
data.tar.gz: 10b1b39f0dda79f7fe5af9b42a4903a2d2c4d93e3331b7f7b246a0e890b85ca6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ca638ffaba48b33c56a138509991bc3beed2b32cbeb63d4705cefac22b90a63563c6dc24835c71d82eed52b330e46c47953d738d28f1daf9ae5b214ce86ce8ef
|
|
7
|
+
data.tar.gz: 86a81881255a911e474e662c1b069c50bbf7fc58b1d336d6c36c08c1f5f870a46ee50681879e1248c869317dcf3a80bbccd8694821c8a39e17d19874f413c09c
|
data/lib/tina4/auth.rb
CHANGED
|
@@ -19,7 +19,7 @@ module Tina4
|
|
|
19
19
|
|
|
20
20
|
# Returns true when SECRET env var is set and no RSA keys exist in .keys/
|
|
21
21
|
def use_hmac?
|
|
22
|
-
secret = ENV["
|
|
22
|
+
secret = ENV["TINA4_SECRET"]
|
|
23
23
|
return false if secret.nil? || secret.empty?
|
|
24
24
|
|
|
25
25
|
# If RSA keys already exist on disk, prefer RS256 for backward compat
|
|
@@ -29,7 +29,7 @@ module Tina4
|
|
|
29
29
|
end
|
|
30
30
|
|
|
31
31
|
def hmac_secret
|
|
32
|
-
ENV["
|
|
32
|
+
ENV["TINA4_SECRET"]
|
|
33
33
|
end
|
|
34
34
|
|
|
35
35
|
# Base64url-encode without padding (JWT spec)
|
|
@@ -191,7 +191,7 @@ module Tina4
|
|
|
191
191
|
token = Regexp.last_match(1)
|
|
192
192
|
|
|
193
193
|
# API_KEY bypass — matches tina4_python behavior
|
|
194
|
-
api_key = ENV["TINA4_API_KEY"]
|
|
194
|
+
api_key = ENV["TINA4_API_KEY"]
|
|
195
195
|
if api_key && !api_key.empty? && token == api_key
|
|
196
196
|
return { "api_key" => true }
|
|
197
197
|
end
|
|
@@ -206,7 +206,7 @@ module Tina4
|
|
|
206
206
|
end
|
|
207
207
|
|
|
208
208
|
def validate_api_key(provided, expected: nil)
|
|
209
|
-
expected ||= ENV["TINA4_API_KEY"]
|
|
209
|
+
expected ||= ENV["TINA4_API_KEY"]
|
|
210
210
|
return false if expected.nil? || expected.empty?
|
|
211
211
|
return false if provided.nil? || provided.empty?
|
|
212
212
|
return false if provided.length != expected.length
|
|
@@ -230,7 +230,7 @@ module Tina4
|
|
|
230
230
|
token = Regexp.last_match(1)
|
|
231
231
|
|
|
232
232
|
# API_KEY bypass — matches tina4_python behavior
|
|
233
|
-
api_key = ENV["TINA4_API_KEY"]
|
|
233
|
+
api_key = ENV["TINA4_API_KEY"]
|
|
234
234
|
if api_key && !api_key.empty? && token == api_key
|
|
235
235
|
env["tina4.auth"] = { "api_key" => true }
|
|
236
236
|
return true
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tina4
|
|
4
|
+
# Periodic background task registry.
|
|
5
|
+
#
|
|
6
|
+
# Matches Python's `tina4_python.core.server.background(fn, interval)` and
|
|
7
|
+
# PHP's `$app->background($callback, $interval)` — a callback that runs
|
|
8
|
+
# periodically alongside the server lifecycle.
|
|
9
|
+
#
|
|
10
|
+
# Ruby has no asyncio event loop, so each task runs in its own thread.
|
|
11
|
+
# The GIL keeps it cooperative-enough for the periodic work this is meant
|
|
12
|
+
# for (queue draining, health checks, simulators). Errors in the callback
|
|
13
|
+
# are caught and logged so they don't kill the thread.
|
|
14
|
+
module Background
|
|
15
|
+
class << self
|
|
16
|
+
# Register a periodic callback.
|
|
17
|
+
#
|
|
18
|
+
# @param callback [#call, nil] Object responding to `call` with no args.
|
|
19
|
+
# @param interval [Float] Seconds between invocations (default 1.0).
|
|
20
|
+
# @param block [Proc] Optional block (used if callback is nil).
|
|
21
|
+
# @return [Hash] The registered task descriptor.
|
|
22
|
+
def register(callback = nil, interval: 1.0, &block)
|
|
23
|
+
cb = callback || block
|
|
24
|
+
raise ArgumentError, "background requires a callback or block" if cb.nil?
|
|
25
|
+
raise ArgumentError, "callback must respond to :call" unless cb.respond_to?(:call)
|
|
26
|
+
|
|
27
|
+
task = { callback: cb, interval: interval.to_f, thread: nil, running: false }
|
|
28
|
+
mutex.synchronize { tasks << task }
|
|
29
|
+
start_task(task)
|
|
30
|
+
task
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# All registered task descriptors. Tests use this for introspection.
|
|
34
|
+
def tasks
|
|
35
|
+
@tasks ||= []
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Stop and join every running task. Called on graceful shutdown.
|
|
39
|
+
def stop_all(timeout: 2.0)
|
|
40
|
+
snapshot = mutex.synchronize { tasks.dup }
|
|
41
|
+
snapshot.each { |task| stop_task(task, timeout: timeout) }
|
|
42
|
+
mutex.synchronize { tasks.clear }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Stop a single task. Used by tests that register, fire, then stop.
|
|
46
|
+
def stop_task(task, timeout: 2.0)
|
|
47
|
+
task[:running] = false
|
|
48
|
+
thread = task[:thread]
|
|
49
|
+
return unless thread
|
|
50
|
+
|
|
51
|
+
thread.join(timeout) || thread.kill
|
|
52
|
+
task[:thread] = nil
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def mutex
|
|
58
|
+
@mutex ||= Mutex.new
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def start_task(task)
|
|
62
|
+
task[:running] = true
|
|
63
|
+
task[:thread] = Thread.new do
|
|
64
|
+
while task[:running]
|
|
65
|
+
sleep task[:interval]
|
|
66
|
+
break unless task[:running]
|
|
67
|
+
|
|
68
|
+
begin
|
|
69
|
+
task[:callback].call
|
|
70
|
+
rescue => e
|
|
71
|
+
# Never let a callback error kill the thread — next interval still fires.
|
|
72
|
+
if defined?(Tina4::Log) && Tina4::Log.respond_to?(:error)
|
|
73
|
+
Tina4::Log.error("background task error: #{e.class}: #{e.message}")
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
data/lib/tina4/constants.rb
CHANGED
|
@@ -43,4 +43,44 @@ module Tina4
|
|
|
43
43
|
TEXT_PLAIN = "text/plain; charset=utf-8"
|
|
44
44
|
TEXT_CSV = "text/csv"
|
|
45
45
|
TEXT_XML = "text/xml"
|
|
46
|
+
|
|
47
|
+
# ── HTTP Reason Phrases (RFC 7231 / RFC 9110) ──
|
|
48
|
+
#
|
|
49
|
+
# Used to write a correct HTTP/1.1 status line wherever the framework
|
|
50
|
+
# emits one manually. Previously code paths that built the status line
|
|
51
|
+
# by hand wrote "HTTP/1.1 404 OK" regardless of code, which is
|
|
52
|
+
# malformed. ``Tina4.http_reason(status)`` always returns a non-empty
|
|
53
|
+
# phrase that matches the status family.
|
|
54
|
+
HTTP_REASON_PHRASES = {
|
|
55
|
+
100 => "Continue", 101 => "Switching Protocols",
|
|
56
|
+
200 => "OK", 201 => "Created", 202 => "Accepted", 204 => "No Content",
|
|
57
|
+
206 => "Partial Content",
|
|
58
|
+
301 => "Moved Permanently", 302 => "Found", 303 => "See Other",
|
|
59
|
+
304 => "Not Modified", 307 => "Temporary Redirect", 308 => "Permanent Redirect",
|
|
60
|
+
400 => "Bad Request", 401 => "Unauthorized", 403 => "Forbidden",
|
|
61
|
+
404 => "Not Found", 405 => "Method Not Allowed", 406 => "Not Acceptable",
|
|
62
|
+
409 => "Conflict", 410 => "Gone", 413 => "Content Too Large",
|
|
63
|
+
415 => "Unsupported Media Type", 422 => "Unprocessable Content",
|
|
64
|
+
429 => "Too Many Requests",
|
|
65
|
+
500 => "Internal Server Error", 501 => "Not Implemented",
|
|
66
|
+
502 => "Bad Gateway", 503 => "Service Unavailable", 504 => "Gateway Timeout"
|
|
67
|
+
}.freeze
|
|
68
|
+
|
|
69
|
+
# Return the canonical HTTP reason phrase for ``status``.
|
|
70
|
+
#
|
|
71
|
+
# Falls back to a sensible label when an exotic status is used. Never
|
|
72
|
+
# returns an empty string — the HTTP/1.1 status line requires a phrase.
|
|
73
|
+
# Prefers Rack::Utils::HTTP_STATUS_CODES when Rack is available so the
|
|
74
|
+
# phrase tracks Rack's mapping, otherwise uses the local table above.
|
|
75
|
+
def self.http_reason(status)
|
|
76
|
+
code = status.to_i
|
|
77
|
+
if defined?(Rack::Utils::HTTP_STATUS_CODES)
|
|
78
|
+
phrase = Rack::Utils::HTTP_STATUS_CODES[code]
|
|
79
|
+
return phrase if phrase && !phrase.empty?
|
|
80
|
+
end
|
|
81
|
+
phrase = HTTP_REASON_PHRASES[code]
|
|
82
|
+
return phrase if phrase && !phrase.empty?
|
|
83
|
+
return "OK" if code >= 200 && code < 300
|
|
84
|
+
"Error"
|
|
85
|
+
end
|
|
46
86
|
end
|
data/lib/tina4/container.rb
CHANGED
|
@@ -4,7 +4,7 @@ module Tina4
|
|
|
4
4
|
# Lightweight dependency injection container.
|
|
5
5
|
#
|
|
6
6
|
# Tina4::Container.register(:mailer) { MailService.new } # transient — new instance each get
|
|
7
|
-
# Tina4::Container.singleton(:db) { Database.new(ENV["
|
|
7
|
+
# Tina4::Container.singleton(:db) { Database.new(ENV["TINA4_DATABASE_URL"]) } # singleton — memoised
|
|
8
8
|
# Tina4::Container.register(:cache, RedisCacheInstance) # concrete instance (always same)
|
|
9
9
|
# Tina4::Container.get(:db) # => Database instance
|
|
10
10
|
#
|
data/lib/tina4/database.rb
CHANGED
|
@@ -91,24 +91,33 @@ module Tina4
|
|
|
91
91
|
|
|
92
92
|
# Construct a Database from environment variables.
|
|
93
93
|
# Returns nil if the named env var is not set.
|
|
94
|
-
def self.from_env(env_key: "
|
|
94
|
+
def self.from_env(env_key: "TINA4_DATABASE_URL", pool: 0)
|
|
95
95
|
url = ENV[env_key]
|
|
96
96
|
return nil if url.nil? || url.strip.empty?
|
|
97
97
|
|
|
98
98
|
new(url,
|
|
99
|
-
username: ENV["
|
|
100
|
-
password: ENV["
|
|
99
|
+
username: ENV["TINA4_DATABASE_USERNAME"],
|
|
100
|
+
password: ENV["TINA4_DATABASE_PASSWORD"],
|
|
101
101
|
pool: pool)
|
|
102
102
|
end
|
|
103
103
|
|
|
104
104
|
def initialize(connection_string = nil, username: nil, password: nil, driver_name: nil, pool: 0)
|
|
105
|
-
@connection_string = connection_string || ENV["
|
|
106
|
-
@username = username || ENV["
|
|
107
|
-
@password = password || ENV["
|
|
105
|
+
@connection_string = connection_string || ENV["TINA4_DATABASE_URL"]
|
|
106
|
+
@username = username || ENV["TINA4_DATABASE_USERNAME"]
|
|
107
|
+
@password = password || ENV["TINA4_DATABASE_PASSWORD"]
|
|
108
108
|
@driver_name = driver_name || detect_driver(@connection_string)
|
|
109
109
|
@pool_size = pool # 0 = single connection, N>0 = N pooled connections
|
|
110
110
|
@connected = false
|
|
111
111
|
|
|
112
|
+
# Per-instance thread-local key for the transaction adapter pin.
|
|
113
|
+
# Without this pin, every Database method call rotates to a different
|
|
114
|
+
# pooled connection. Inside a transaction this silently breaks atomicity:
|
|
115
|
+
# start_transaction begins on adapter A, executes autocommit on B/C, and
|
|
116
|
+
# commit/rollback land on D — a no-op. start_transaction sets the pin,
|
|
117
|
+
# commit/rollback clear it. While pinned, current_driver returns the same
|
|
118
|
+
# driver for every call so the whole transaction runs on one connection.
|
|
119
|
+
@tx_pin_key = :"tina4_pinned_adapter_#{object_id}"
|
|
120
|
+
|
|
112
121
|
# Query cache — off by default, opt-in via TINA4_DB_CACHE=true
|
|
113
122
|
@cache_enabled = truthy?(ENV["TINA4_DB_CACHE"])
|
|
114
123
|
@cache_ttl = (ENV["TINA4_DB_CACHE_TTL"] || "30").to_i
|
|
@@ -161,7 +170,14 @@ module Tina4
|
|
|
161
170
|
end
|
|
162
171
|
|
|
163
172
|
# Get the current driver — from pool (round-robin) or single connection.
|
|
173
|
+
#
|
|
174
|
+
# Inside a transaction, all calls must land on the SAME driver — otherwise
|
|
175
|
+
# start_transaction, execute, and commit each rotate to a different pooled
|
|
176
|
+
# connection and the transaction is meaningless. start_transaction pins
|
|
177
|
+
# the driver to the calling thread; commit/rollback release it.
|
|
164
178
|
def current_driver
|
|
179
|
+
pinned = Thread.current[@tx_pin_key]
|
|
180
|
+
return pinned if pinned
|
|
165
181
|
if @pool
|
|
166
182
|
@pool.checkout
|
|
167
183
|
else
|
|
@@ -355,27 +371,38 @@ module Tina4
|
|
|
355
371
|
|
|
356
372
|
def transaction
|
|
357
373
|
drv = current_driver
|
|
374
|
+
Thread.current[@tx_pin_key] = drv
|
|
358
375
|
drv.begin_transaction
|
|
359
376
|
yield self
|
|
360
377
|
drv.commit
|
|
361
378
|
rescue => e
|
|
362
|
-
drv.rollback
|
|
379
|
+
drv.rollback if drv
|
|
363
380
|
raise e
|
|
381
|
+
ensure
|
|
382
|
+
Thread.current[@tx_pin_key] = nil
|
|
364
383
|
end
|
|
365
384
|
|
|
366
385
|
# Begin a transaction without a block — matches PHP/Python/Node API.
|
|
386
|
+
# Pins the driver to this thread for the whole transaction so executes
|
|
387
|
+
# and the final commit/rollback all run on the same connection.
|
|
367
388
|
def start_transaction
|
|
368
|
-
current_driver
|
|
389
|
+
drv = current_driver
|
|
390
|
+
Thread.current[@tx_pin_key] = drv
|
|
391
|
+
drv.begin_transaction
|
|
369
392
|
end
|
|
370
393
|
|
|
371
|
-
# Commit the current transaction
|
|
394
|
+
# Commit the current transaction and release the driver pin.
|
|
372
395
|
def commit
|
|
373
396
|
current_driver.commit
|
|
397
|
+
ensure
|
|
398
|
+
Thread.current[@tx_pin_key] = nil
|
|
374
399
|
end
|
|
375
400
|
|
|
376
|
-
# Roll back the current transaction
|
|
401
|
+
# Roll back the current transaction and release the driver pin.
|
|
377
402
|
def rollback
|
|
378
403
|
current_driver.rollback
|
|
404
|
+
ensure
|
|
405
|
+
Thread.current[@tx_pin_key] = nil
|
|
379
406
|
end
|
|
380
407
|
|
|
381
408
|
def tables
|