tina4ruby 3.13.36 → 3.13.38
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 +118 -7
- data/lib/tina4/cli.rb +106 -2
- data/lib/tina4/database.rb +356 -46
- data/lib/tina4/dev_admin.rb +54 -11
- data/lib/tina4/drivers/sqlite_driver.rb +23 -0
- data/lib/tina4/env.rb +40 -4
- data/lib/tina4/events.rb +54 -8
- data/lib/tina4/graphql.rb +68 -12
- data/lib/tina4/html_element.rb +55 -7
- data/lib/tina4/mcp.rb +10 -3
- data/lib/tina4/messenger.rb +130 -25
- data/lib/tina4/metrics.rb +238 -47
- data/lib/tina4/middleware.rb +136 -13
- data/lib/tina4/migration.rb +6 -4
- data/lib/tina4/orm.rb +13 -10
- data/lib/tina4/public/js/tina4-dev-admin.js +212 -212
- data/lib/tina4/public/js/tina4-dev-admin.min.js +212 -212
- data/lib/tina4/rack_app.rb +17 -10
- data/lib/tina4/response.rb +31 -11
- data/lib/tina4/seeder.rb +433 -84
- data/lib/tina4/session.rb +94 -17
- data/lib/tina4/version.rb +1 -1
- data/lib/tina4/websocket.rb +354 -18
- data/lib/tina4/wsdl.rb +25 -2
- data/lib/tina4.rb +11 -9
- metadata +6 -47
data/lib/tina4/middleware.rb
CHANGED
|
@@ -41,24 +41,44 @@ module Tina4
|
|
|
41
41
|
@global_middleware = []
|
|
42
42
|
end
|
|
43
43
|
|
|
44
|
-
# Run all "before" hooks: block-based handlers, then class-based before_*
|
|
44
|
+
# Run all "before" hooks: block-based handlers, then class-based before_*
|
|
45
|
+
# methods (in definition order).
|
|
45
46
|
#
|
|
46
47
|
# Signature matches Python/PHP/Node orchestrators: pass the list of
|
|
47
48
|
# middleware classes explicitly.
|
|
48
49
|
#
|
|
49
|
-
#
|
|
50
|
+
# M2 — visible-but-resilient: every before_* call is wrapped so a THROW
|
|
51
|
+
# never crashes the worker. On a throw the error is LOGGED and the
|
|
52
|
+
# response becomes a clean 500 ({"error":"Internal Server Error",
|
|
53
|
+
# "status":500}), then processing halts (handler skipped) — deterministic,
|
|
54
|
+
# never an unhandled exception. A before_* that sets status >= 400 also
|
|
55
|
+
# halts (the existing 4xx short-circuit). after_* still run on either
|
|
56
|
+
# halt path (see the dispatcher / #run_after docstring).
|
|
57
|
+
#
|
|
58
|
+
# Returns true on success, or false to halt the request (handler skipped).
|
|
50
59
|
def run_before(middleware_classes, request, response)
|
|
51
60
|
# 1. Block-based before handlers (pattern-matched)
|
|
52
61
|
before_handlers.each do |entry|
|
|
53
62
|
next unless matches_pattern?(request.path, entry[:pattern])
|
|
54
|
-
|
|
63
|
+
|
|
64
|
+
begin
|
|
65
|
+
result = entry[:handler].call(request, response)
|
|
66
|
+
rescue StandardError, ScriptError => error
|
|
67
|
+
middleware_500(response, "before handler", error)
|
|
68
|
+
return false
|
|
69
|
+
end
|
|
55
70
|
return false if result == false
|
|
56
71
|
end
|
|
57
72
|
|
|
58
|
-
# 2. Class-based middleware: call every before_* method
|
|
73
|
+
# 2. Class-based middleware: call every before_* method (definition order)
|
|
59
74
|
middleware_classes.each do |klass|
|
|
60
75
|
before_methods_for(klass).each do |method_name|
|
|
61
|
-
|
|
76
|
+
begin
|
|
77
|
+
result = klass.send(method_name, request, response)
|
|
78
|
+
rescue StandardError, ScriptError => error
|
|
79
|
+
middleware_500(response, "#{class_label(klass)}.#{method_name}", error)
|
|
80
|
+
return false
|
|
81
|
+
end
|
|
62
82
|
# Support returning [request, response] (Python convention) or false to halt
|
|
63
83
|
if result == false
|
|
64
84
|
return false
|
|
@@ -73,21 +93,42 @@ module Tina4
|
|
|
73
93
|
true
|
|
74
94
|
end
|
|
75
95
|
|
|
76
|
-
# Run all "after" hooks: block-based handlers, then class-based after_*
|
|
96
|
+
# Run all "after" hooks: block-based handlers, then class-based after_*
|
|
97
|
+
# methods (in definition order).
|
|
77
98
|
#
|
|
78
99
|
# Signature matches Python/PHP/Node orchestrators: pass the list of
|
|
79
100
|
# middleware classes explicitly.
|
|
101
|
+
#
|
|
102
|
+
# AFTER-ON-4xx RULE (M2, documented + consistent across all 4 frameworks):
|
|
103
|
+
# after_* ALWAYS run even when a before_* short-circuited with status >= 400
|
|
104
|
+
# and the handler was skipped — so they can still add headers / logging.
|
|
105
|
+
# The dispatcher calls #run_after unconditionally after the before/handler
|
|
106
|
+
# block (including on the 4xx / throw halt path).
|
|
107
|
+
#
|
|
108
|
+
# M2 — every after_* call is wrapped: a THROW is LOGGED and turns the
|
|
109
|
+
# response into a clean 500, then the REMAINING after_* still run (they
|
|
110
|
+
# may add headers/logging). Never an unhandled crash.
|
|
80
111
|
def run_after(middleware_classes, request, response)
|
|
81
112
|
# 1. Block-based after handlers (pattern-matched)
|
|
82
113
|
after_handlers.each do |entry|
|
|
83
114
|
next unless matches_pattern?(request.path, entry[:pattern])
|
|
84
|
-
|
|
115
|
+
|
|
116
|
+
begin
|
|
117
|
+
entry[:handler].call(request, response)
|
|
118
|
+
rescue StandardError, ScriptError => error
|
|
119
|
+
middleware_500(response, "after handler", error)
|
|
120
|
+
end
|
|
85
121
|
end
|
|
86
122
|
|
|
87
|
-
# 2. Class-based middleware: call every after_* method
|
|
123
|
+
# 2. Class-based middleware: call every after_* method (definition order)
|
|
88
124
|
middleware_classes.each do |klass|
|
|
89
125
|
after_methods_for(klass).each do |method_name|
|
|
90
|
-
|
|
126
|
+
begin
|
|
127
|
+
result = klass.send(method_name, request, response)
|
|
128
|
+
rescue StandardError, ScriptError => error
|
|
129
|
+
middleware_500(response, "#{class_label(klass)}.#{method_name}", error)
|
|
130
|
+
next
|
|
131
|
+
end
|
|
91
132
|
if result.is_a?(Array) && result.length == 2
|
|
92
133
|
request, response = result
|
|
93
134
|
end
|
|
@@ -95,8 +136,38 @@ module Tina4
|
|
|
95
136
|
end
|
|
96
137
|
end
|
|
97
138
|
|
|
139
|
+
# Deterministic clean 500 for a middleware that threw. Logs the cause
|
|
140
|
+
# (NEVER silent) then sets the response to the canonical error shape —
|
|
141
|
+
# byte-identical to the Python master ({"error":"Internal Server Error",
|
|
142
|
+
# "status":500} + status 500). Returns the response for chaining.
|
|
143
|
+
def middleware_500(response, label, error)
|
|
144
|
+
begin
|
|
145
|
+
Tina4::Log.error(
|
|
146
|
+
"Middleware #{label} raised #{error.class.name}: #{error.message}"
|
|
147
|
+
)
|
|
148
|
+
rescue StandardError
|
|
149
|
+
begin
|
|
150
|
+
$stderr.puts("Middleware #{label} raised #{error.class.name}: #{error.message}")
|
|
151
|
+
$stderr.flush
|
|
152
|
+
rescue StandardError
|
|
153
|
+
# never let logging break the worker
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
response.json({ error: "Internal Server Error", status: 500 }, 500)
|
|
157
|
+
end
|
|
158
|
+
|
|
98
159
|
private
|
|
99
160
|
|
|
161
|
+
# Human-readable label for a middleware (class name, or the class of an
|
|
162
|
+
# instance) used in the logged 500 message.
|
|
163
|
+
def class_label(klass)
|
|
164
|
+
if klass.is_a?(Class) || klass.is_a?(Module)
|
|
165
|
+
klass.name || klass.to_s
|
|
166
|
+
else
|
|
167
|
+
klass.class.name || klass.class.to_s
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
100
171
|
def matches_pattern?(path, pattern)
|
|
101
172
|
return true if pattern.nil?
|
|
102
173
|
case pattern
|
|
@@ -109,14 +180,66 @@ module Tina4
|
|
|
109
180
|
end
|
|
110
181
|
end
|
|
111
182
|
|
|
112
|
-
# Collect all
|
|
183
|
+
# Collect all class methods matching before_* in DEFINITION order.
|
|
113
184
|
def before_methods_for(klass)
|
|
114
|
-
klass
|
|
185
|
+
discover_methods(klass, "before_")
|
|
115
186
|
end
|
|
116
187
|
|
|
117
|
-
# Collect all
|
|
188
|
+
# Collect all class methods matching after_* in DEFINITION order.
|
|
118
189
|
def after_methods_for(klass)
|
|
119
|
-
klass
|
|
190
|
+
discover_methods(klass, "after_")
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# ----------------------------------------------------------------------
|
|
194
|
+
# MIDDLEWARE ORDERING (M1) — within a class, before_*/after_* methods run
|
|
195
|
+
# in SOURCE-DEFINITION order, NOT alphabetical. Cross-class order is the
|
|
196
|
+
# natural iteration of the registered middleware list (registration
|
|
197
|
+
# order). before_* run before the handler, after_* after.
|
|
198
|
+
#
|
|
199
|
+
# WHY source line numbers, not instance_methods(false): in Ruby/PRISM
|
|
200
|
+
# `instance_methods(false)` is NOT a reliable definition-order report —
|
|
201
|
+
# once a method NAME (symbol) has been defined on any other class first,
|
|
202
|
+
# that name can sort ahead in a later class's list. So we sort the
|
|
203
|
+
# matching methods by their `source_location` line number, which IS the
|
|
204
|
+
# true source-definition order and is immune to the symbol-table quirk.
|
|
205
|
+
# (Methods with no source_location — e.g. C-defined — sort to the front
|
|
206
|
+
# deterministically by name.) We walk the ancestry base→derived so
|
|
207
|
+
# inherited middleware methods run before a subclass's own, de-duping
|
|
208
|
+
# overrides to their first (base) position. Mirrors the Python master's
|
|
209
|
+
# Middleware._discover_methods MRO walk (which leans on __dict__ insertion
|
|
210
|
+
# order — the equivalent of source-definition order).
|
|
211
|
+
def discover_methods(klass, prefix)
|
|
212
|
+
target = klass.is_a?(Class) || klass.is_a?(Module) ? klass.singleton_class : klass.class
|
|
213
|
+
seen = {}
|
|
214
|
+
names = []
|
|
215
|
+
target.ancestors.reverse_each do |ancestor|
|
|
216
|
+
matched = begin
|
|
217
|
+
ancestor.instance_methods(false).select do |name|
|
|
218
|
+
name.to_s.start_with?(prefix) &&
|
|
219
|
+
!seen.key?(name) &&
|
|
220
|
+
klass.respond_to?(name)
|
|
221
|
+
end
|
|
222
|
+
rescue StandardError
|
|
223
|
+
[]
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
ordered = matched.sort_by.with_index do |name, idx|
|
|
227
|
+
line = begin
|
|
228
|
+
loc = ancestor.instance_method(name).source_location
|
|
229
|
+
loc ? loc[1] : -1
|
|
230
|
+
rescue StandardError
|
|
231
|
+
-1
|
|
232
|
+
end
|
|
233
|
+
# Tie-break on the symbol-table index so the result is total/stable.
|
|
234
|
+
[line, idx]
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
ordered.each do |name|
|
|
238
|
+
seen[name] = true
|
|
239
|
+
names << name
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
names
|
|
120
243
|
end
|
|
121
244
|
end
|
|
122
245
|
end
|
data/lib/tina4/migration.rb
CHANGED
|
@@ -356,10 +356,12 @@ module Tina4
|
|
|
356
356
|
Tina4::Log.info("Migration #{File.basename(file)}: #{skip_reason}")
|
|
357
357
|
next
|
|
358
358
|
end
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
359
|
+
# db.execute() now RAISES on a SQL error (it no longer returns false),
|
|
360
|
+
# so a bad statement throws straight up to run_migration's rescue, which
|
|
361
|
+
# records the migration as failed and surfaces the error. The old
|
|
362
|
+
# "if result == false: raise" check is dead — a plain execute carries
|
|
363
|
+
# the same failure semantics.
|
|
364
|
+
@db.execute(stmt)
|
|
363
365
|
end
|
|
364
366
|
end
|
|
365
367
|
|
data/lib/tina4/orm.rb
CHANGED
|
@@ -425,17 +425,20 @@ module Tina4
|
|
|
425
425
|
translator_engine = %w[postgres postgresql].include?(engine) ? "postgresql" : engine
|
|
426
426
|
sql = SQLTranslator.auto_increment_syntax(sql, translator_engine)
|
|
427
427
|
|
|
428
|
-
# Don't claim success when the DDL failed. db.execute()
|
|
429
|
-
#
|
|
430
|
-
# any DDL error)
|
|
431
|
-
#
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
428
|
+
# Don't claim success when the DDL failed. db.execute() now RAISES on a
|
|
429
|
+
# SQL error (it no longer swallows it into get_error() and returns
|
|
430
|
+
# false), so a bad type (or any DDL error) surfaces here as an
|
|
431
|
+
# exception. create_table keeps its documented bool contract: catch the
|
|
432
|
+
# raise, log the cause, and return false so callers that test the return
|
|
433
|
+
# still see a clean failure instead of a thrown error.
|
|
434
|
+
begin
|
|
435
|
+
db.execute(sql)
|
|
436
|
+
db.commit
|
|
437
|
+
true
|
|
438
|
+
rescue => e
|
|
439
|
+
Tina4::Log.error("create_table failed for #{table_name}: #{db.get_error || e.message}", { sql: sql })
|
|
440
|
+
false
|
|
437
441
|
end
|
|
438
|
-
true
|
|
439
442
|
end
|
|
440
443
|
|
|
441
444
|
def scope(name, filter_sql, params = [])
|