tina4ruby 3.13.30 → 3.13.32
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/database.rb +22 -7
- data/lib/tina4/graphql.rb +3 -1
- data/lib/tina4/request.rb +49 -13
- data/lib/tina4/response.rb +32 -5
- data/lib/tina4/response_cache.rb +35 -10
- data/lib/tina4/version.rb +1 -1
- data/lib/tina4/wsdl.rb +11 -6
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 02b84dbb5f8e4d009306495921ab7849a28fd769d81d13019b311523d970a0e5
|
|
4
|
+
data.tar.gz: e1b298c7bd03444def8787941f87a7e6fd0adcb6be23efe810990c6005d27910
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8bf774f6208956221c87bb2f546a04aab950a3cae5962dae94bbd430ee55463481886ba315a21f2680b017828d99df0c6e7a54fff331cff0e559b176da45c5a9
|
|
7
|
+
data.tar.gz: 5242a9295d6e70763ca68a377f2aa3dfd2ac8b37402b8660f439ea6bc6e0c5ddb1f278d277c2ff4ab8bf62b6d80f0ad316c8c07c099f07ecdee9040bea0ddfe5
|
data/lib/tina4/database.rb
CHANGED
|
@@ -366,11 +366,20 @@ module Tina4
|
|
|
366
366
|
# v3.13.12: default `limit` is **nil** (no truncation) — the method
|
|
367
367
|
# name says fetch_all, so it returns all matching rows. Pre-v3.13.12
|
|
368
368
|
# silently truncated to 100. Pass an explicit `limit:` to cap.
|
|
369
|
-
|
|
370
|
-
|
|
369
|
+
#
|
|
370
|
+
# Pass `no_cache: true` to bypass the query cache for this call (see #fetch).
|
|
371
|
+
def fetch_all(sql, params = [], limit: nil, offset: nil, no_cache: false)
|
|
372
|
+
fetch(sql, params, limit: limit, offset: offset, no_cache: no_cache).records
|
|
371
373
|
end
|
|
372
374
|
|
|
373
|
-
|
|
375
|
+
# Fetch rows with pagination, returning a DatabaseResult.
|
|
376
|
+
#
|
|
377
|
+
# Pass `no_cache: true` to bypass the query cache entirely for this single
|
|
378
|
+
# call — no lookup, no store — and run the query directly against the
|
|
379
|
+
# driver. Works for both the request-scoped auto-cache and the persistent
|
|
380
|
+
# DB cache. The default `false` preserves the cached behaviour. Parity with
|
|
381
|
+
# Python db.fetch(no_cache=) / PHP / Node.
|
|
382
|
+
def fetch(sql, params = [], limit: 100, offset: nil, no_cache: false)
|
|
374
383
|
offset ||= 0
|
|
375
384
|
drv = current_driver
|
|
376
385
|
|
|
@@ -388,7 +397,7 @@ module Tina4
|
|
|
388
397
|
effective_sql = drv.apply_limit(effective_sql, limit, offset)
|
|
389
398
|
end
|
|
390
399
|
|
|
391
|
-
if @cache_enabled
|
|
400
|
+
if @cache_enabled && !no_cache
|
|
392
401
|
key = cache_key(effective_sql, params)
|
|
393
402
|
cached = cache_get(key)
|
|
394
403
|
if cached
|
|
@@ -406,9 +415,15 @@ module Tina4
|
|
|
406
415
|
Tina4::DatabaseResult.new(rows, sql: effective_sql, db: self)
|
|
407
416
|
end
|
|
408
417
|
|
|
409
|
-
|
|
418
|
+
# Fetch a single row (or nil).
|
|
419
|
+
#
|
|
420
|
+
# Pass `no_cache: true` to bypass the query cache entirely for this call —
|
|
421
|
+
# no lookup, no store — running the query directly. The `no_cache` flag is
|
|
422
|
+
# propagated to the inner #fetch so the request-scoped/persistent cache is
|
|
423
|
+
# never populated either. Default `false` preserves cached behaviour.
|
|
424
|
+
def fetch_one(sql, params = [], no_cache: false)
|
|
410
425
|
sql = Tina4::Database.strip_trailing_semicolons(sql)
|
|
411
|
-
if @cache_enabled
|
|
426
|
+
if @cache_enabled && !no_cache
|
|
412
427
|
key = cache_key(sql + ":ONE", params)
|
|
413
428
|
cached = cache_get(key)
|
|
414
429
|
if cached
|
|
@@ -422,7 +437,7 @@ module Tina4
|
|
|
422
437
|
return value
|
|
423
438
|
end
|
|
424
439
|
|
|
425
|
-
result = fetch(sql, params, limit: 1)
|
|
440
|
+
result = fetch(sql, params, limit: 1, no_cache: no_cache)
|
|
426
441
|
result.first
|
|
427
442
|
end
|
|
428
443
|
|
data/lib/tina4/graphql.rb
CHANGED
|
@@ -1026,7 +1026,9 @@ module Tina4
|
|
|
1026
1026
|
|
|
1027
1027
|
graphql = self
|
|
1028
1028
|
Tina4.post path, auth: false do |request, response|
|
|
1029
|
-
|
|
1029
|
+
# handle_request expects the raw JSON text (it JSON.parses internally),
|
|
1030
|
+
# so read body_raw — request.body now returns the PARSED payload.
|
|
1031
|
+
body = request.body_raw
|
|
1030
1032
|
result = graphql.handle_request(body, context: { request: request })
|
|
1031
1033
|
response.json(result)
|
|
1032
1034
|
end
|
data/lib/tina4/request.rb
CHANGED
|
@@ -35,6 +35,26 @@ module Tina4
|
|
|
35
35
|
end
|
|
36
36
|
end
|
|
37
37
|
|
|
38
|
+
# Per-file upload hash: indifferent access plus a lazily-materialised
|
|
39
|
+
# `content` field. The raw bytes are read from the tempfile only on first
|
|
40
|
+
# access to `content` (then rewound, so :tempfile streaming still works),
|
|
41
|
+
# so :tempfile-only handlers never buffer large uploads in memory.
|
|
42
|
+
class FileUpload < IndifferentHash
|
|
43
|
+
def [](key)
|
|
44
|
+
if key.to_s == "content" && !key?("content") && (tf = super("tempfile"))
|
|
45
|
+
self["content"] = begin
|
|
46
|
+
tf.rewind if tf.respond_to?(:rewind)
|
|
47
|
+
data = tf.read
|
|
48
|
+
tf.rewind if tf.respond_to?(:rewind)
|
|
49
|
+
data
|
|
50
|
+
rescue StandardError
|
|
51
|
+
nil
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
super(key)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
38
58
|
# Hash subclass for HTTP headers — string keys are case-insensitive.
|
|
39
59
|
#
|
|
40
60
|
# HTTP header field-names are case-insensitive per RFC 7230 §3.2. With
|
|
@@ -161,16 +181,25 @@ module Tina4
|
|
|
161
181
|
@session ||= Tina4::Session.new(@env)
|
|
162
182
|
end
|
|
163
183
|
|
|
164
|
-
#
|
|
184
|
+
# Parsed body (JSON -> Hash, form-urlencoded -> Hash, multipart ->
|
|
185
|
+
# fields Hash, else the current fallback). This matches Python's
|
|
186
|
+
# `request.body`, PHP's, and Node's: `body` is the PARSED payload, not
|
|
187
|
+
# the raw bytes. For the raw string use `body_raw`.
|
|
165
188
|
def body
|
|
166
|
-
@
|
|
189
|
+
@body_parsed ||= parse_body
|
|
167
190
|
end
|
|
168
191
|
|
|
169
|
-
#
|
|
170
|
-
|
|
171
|
-
|
|
192
|
+
# Raw body string — the bytes exactly as the client sent them.
|
|
193
|
+
# (This is what `body` used to return before the cross-framework
|
|
194
|
+
# parity flip; SOAP/GraphQL and any consumer that needs the raw text
|
|
195
|
+
# reads this.)
|
|
196
|
+
def body_raw
|
|
197
|
+
@body_raw ||= read_body
|
|
172
198
|
end
|
|
173
199
|
|
|
200
|
+
# Backwards-compatible alias of `body` — both return the parsed payload.
|
|
201
|
+
alias body_parsed body
|
|
202
|
+
|
|
174
203
|
# Parsed query string as hash
|
|
175
204
|
def query
|
|
176
205
|
@query_hash ||= parse_query_to_hash(@query_string)
|
|
@@ -204,7 +233,7 @@ module Tina4
|
|
|
204
233
|
|
|
205
234
|
def json_body
|
|
206
235
|
@json_body ||= begin
|
|
207
|
-
JSON.parse(
|
|
236
|
+
JSON.parse(body_raw)
|
|
208
237
|
rescue JSON::ParserError, TypeError
|
|
209
238
|
{}
|
|
210
239
|
end
|
|
@@ -273,7 +302,7 @@ module Tina4
|
|
|
273
302
|
if @content_type.include?("application/json")
|
|
274
303
|
json_body
|
|
275
304
|
elsif @content_type.include?("application/x-www-form-urlencoded")
|
|
276
|
-
parse_query_to_hash(
|
|
305
|
+
parse_query_to_hash(body_raw)
|
|
277
306
|
elsif @content_type.include?("multipart/form-data")
|
|
278
307
|
# Extract form fields from Rack's parsed multipart data.
|
|
279
308
|
# Files are handled separately by extract_files.
|
|
@@ -324,12 +353,19 @@ module Tina4
|
|
|
324
353
|
if form_hash
|
|
325
354
|
form_hash.each do |key, value|
|
|
326
355
|
if value.is_a?(Hash) && value[:tempfile]
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
356
|
+
tempfile = value[:tempfile]
|
|
357
|
+
|
|
358
|
+
# Indifferent-access per-file hash so file["content"],
|
|
359
|
+
# file[:content], file["filename"], file[:filename] all work.
|
|
360
|
+
# `content` (raw bytes, never base64) is materialised lazily on
|
|
361
|
+
# first access (see FileUpload) — :tempfile-only handlers never
|
|
362
|
+
# buffer large uploads in memory.
|
|
363
|
+
file = FileUpload.new
|
|
364
|
+
file[:filename] = value[:filename]
|
|
365
|
+
file[:type] = value[:type]
|
|
366
|
+
file[:tempfile] = tempfile
|
|
367
|
+
file[:size] = tempfile.size
|
|
368
|
+
result[key] = file
|
|
333
369
|
end
|
|
334
370
|
end
|
|
335
371
|
end
|
data/lib/tina4/response.rb
CHANGED
|
@@ -285,9 +285,19 @@ module Tina4
|
|
|
285
285
|
self
|
|
286
286
|
end
|
|
287
287
|
|
|
288
|
-
# Stream
|
|
288
|
+
# Stream a response for Server-Sent Events (SSE) / chunked transfer.
|
|
289
289
|
#
|
|
290
|
-
#
|
|
290
|
+
# Two equivalent call styles (cross-framework parity — Python/PHP/Node
|
|
291
|
+
# pass a generator positionally; Ruby additionally supports a block):
|
|
292
|
+
#
|
|
293
|
+
# # 1. Positional generator (Enumerator, or anything responding to
|
|
294
|
+
# # #each or #call that yields string chunks):
|
|
295
|
+
# gen = Enumerator.new do |y|
|
|
296
|
+
# 10.times { |i| y << "data: message #{i}\n\n" }
|
|
297
|
+
# end
|
|
298
|
+
# response.stream(gen)
|
|
299
|
+
#
|
|
300
|
+
# # 2. Block form (unchanged):
|
|
291
301
|
# Tina4::Router.get "/events" do |request, response|
|
|
292
302
|
# response.stream do |out|
|
|
293
303
|
# 10.times do |i|
|
|
@@ -297,16 +307,18 @@ module Tina4
|
|
|
297
307
|
# end
|
|
298
308
|
# end
|
|
299
309
|
#
|
|
310
|
+
# @param generator [#each, #call, nil] Optional source of string chunks.
|
|
300
311
|
# @param content_type [String] Content type (default: text/event-stream)
|
|
301
312
|
# @yield [Enumerator::Yielder] Block receives a yielder to push chunks
|
|
302
313
|
# @return [self]
|
|
303
|
-
def stream(content_type: "text/event-stream", &block)
|
|
314
|
+
def stream(generator = nil, content_type: "text/event-stream", &block)
|
|
304
315
|
@status_code = @status_code || 200
|
|
305
316
|
@headers["content-type"] = content_type
|
|
306
317
|
@headers["cache-control"] = "no-cache"
|
|
307
318
|
@headers["connection"] = "keep-alive"
|
|
308
319
|
@headers["x-accel-buffering"] = "no"
|
|
309
320
|
@_streaming = true
|
|
321
|
+
@_stream_generator = generator
|
|
310
322
|
@_stream_block = block
|
|
311
323
|
self
|
|
312
324
|
end
|
|
@@ -330,9 +342,24 @@ module Tina4
|
|
|
330
342
|
final_headers["set-cookie"] = @cookies.join("\n") if @cookies && !@cookies.empty?
|
|
331
343
|
|
|
332
344
|
if @_streaming
|
|
333
|
-
# Streaming mode — return an Enumerator as the body
|
|
345
|
+
# Streaming mode — return an Enumerator as the Rack body. A positional
|
|
346
|
+
# generator wins over a block when both are somehow present.
|
|
347
|
+
gen = @_stream_generator
|
|
348
|
+
blk = @_stream_block
|
|
334
349
|
body = Enumerator.new do |yielder|
|
|
335
|
-
|
|
350
|
+
if gen
|
|
351
|
+
if gen.respond_to?(:each)
|
|
352
|
+
# Enumerator / array / any Enumerable of string chunks
|
|
353
|
+
gen.each { |chunk| yielder << chunk }
|
|
354
|
+
elsif gen.respond_to?(:call)
|
|
355
|
+
# Callable that receives the yielder, like the block form
|
|
356
|
+
gen.call(yielder)
|
|
357
|
+
else
|
|
358
|
+
yielder << gen.to_s
|
|
359
|
+
end
|
|
360
|
+
elsif blk
|
|
361
|
+
blk.call(yielder)
|
|
362
|
+
end
|
|
336
363
|
end
|
|
337
364
|
return [@status_code, final_headers, body]
|
|
338
365
|
end
|
data/lib/tina4/response_cache.rb
CHANGED
|
@@ -86,6 +86,7 @@ module Tina4
|
|
|
86
86
|
if hit
|
|
87
87
|
if response.respond_to?(:call)
|
|
88
88
|
new_response = response.call(hit.body, hit.status_code, hit.content_type)
|
|
89
|
+
set_cache_headers(new_response, "HIT", remaining_ttl(hit.expires_at))
|
|
89
90
|
return [request, new_response]
|
|
90
91
|
end
|
|
91
92
|
end
|
|
@@ -106,16 +107,17 @@ module Tina4
|
|
|
106
107
|
def after_cache(request, response)
|
|
107
108
|
return [request, response] unless enabled?
|
|
108
109
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
110
|
+
# Read the tags using the SAME mechanism before_cache wrote them with.
|
|
111
|
+
# before_cache keys the write on respond_to?(:[]=), so read the same way:
|
|
112
|
+
# a Tina4::Request responds to #[] (read-only param lookup) but NOT #[]=,
|
|
113
|
+
# so the tags live on instance variables, not the param hash.
|
|
114
|
+
if request.respond_to?(:[]=)
|
|
115
|
+
method = request[:_cache_method]
|
|
116
|
+
url = request[:_cache_url]
|
|
117
|
+
else
|
|
118
|
+
method = request.instance_variable_get(:@_cache_method)
|
|
119
|
+
url = request.instance_variable_get(:@_cache_url)
|
|
120
|
+
end
|
|
119
121
|
return [request, response] if method.nil? || url.nil?
|
|
120
122
|
|
|
121
123
|
status = if response.respond_to?(:status_code)
|
|
@@ -137,6 +139,9 @@ module Tina4
|
|
|
137
139
|
end
|
|
138
140
|
|
|
139
141
|
internal_store(method, url, status.to_i, content_type.to_s, body)
|
|
142
|
+
# The handler ran (cache miss) — annotate the response so clients can
|
|
143
|
+
# see this was a fresh response and how long it will be cached.
|
|
144
|
+
set_cache_headers(response, "MISS", @ttl)
|
|
140
145
|
[request, response]
|
|
141
146
|
end
|
|
142
147
|
|
|
@@ -279,6 +284,26 @@ module Tina4
|
|
|
279
284
|
"#{method}:#{url}"
|
|
280
285
|
end
|
|
281
286
|
|
|
287
|
+
# Stamp X-Cache / X-Cache-TTL on a response. `state` is "HIT" or "MISS";
|
|
288
|
+
# `ttl` is the remaining (HIT) or configured (MISS) TTL in seconds.
|
|
289
|
+
# Parity with Python/PHP/Node ResponseCache, which set the same two
|
|
290
|
+
# headers (no Cache-Control). No-op for responses that don't carry a
|
|
291
|
+
# mutable headers hash (e.g. test doubles).
|
|
292
|
+
def set_cache_headers(response, state, ttl)
|
|
293
|
+
return unless response.respond_to?(:headers) && response.headers.is_a?(Hash)
|
|
294
|
+
|
|
295
|
+
response.headers["X-Cache"] = state
|
|
296
|
+
response.headers["X-Cache-TTL"] = ttl.to_i.to_s
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# Remaining whole seconds until `expires_at` (a monotonic-ish Time.now.to_f
|
|
300
|
+
# epoch), floored at 0 so an entry on the cusp of expiry never reports a
|
|
301
|
+
# negative TTL.
|
|
302
|
+
def remaining_ttl(expires_at)
|
|
303
|
+
remaining = (expires_at || 0) - Time.now.to_f
|
|
304
|
+
remaining.positive? ? remaining : 0
|
|
305
|
+
end
|
|
306
|
+
|
|
282
307
|
# Internal: retrieve a cached response. Used by middleware hooks only.
|
|
283
308
|
def internal_lookup(method, url)
|
|
284
309
|
return nil unless enabled?
|
data/lib/tina4/version.rb
CHANGED
data/lib/tina4/wsdl.rb
CHANGED
|
@@ -116,9 +116,18 @@ module Tina4
|
|
|
116
116
|
def handle
|
|
117
117
|
return generate_wsdl if @request.nil?
|
|
118
118
|
|
|
119
|
+
# SOAP bodies are raw XML, so read the raw bytes. On a real
|
|
120
|
+
# Tina4::Request use body_raw (request.body now returns the PARSED
|
|
121
|
+
# payload); test stubs that only expose `body` fall back to it.
|
|
122
|
+
raw_body = if @request.respond_to?(:body_raw)
|
|
123
|
+
@request.body_raw
|
|
124
|
+
elsif @request.respond_to?(:body)
|
|
125
|
+
@request.body
|
|
126
|
+
end
|
|
127
|
+
|
|
119
128
|
method = if @request.respond_to?(:method)
|
|
120
129
|
@request.method.to_s.upcase
|
|
121
|
-
elsif
|
|
130
|
+
elsif raw_body && !raw_body.to_s.empty?
|
|
122
131
|
"POST"
|
|
123
132
|
else
|
|
124
133
|
"GET"
|
|
@@ -131,11 +140,7 @@ module Tina4
|
|
|
131
140
|
return generate_wsdl
|
|
132
141
|
end
|
|
133
142
|
|
|
134
|
-
body =
|
|
135
|
-
@request.body.is_a?(String) ? @request.body : @request.body.to_s
|
|
136
|
-
else
|
|
137
|
-
""
|
|
138
|
-
end
|
|
143
|
+
body = raw_body.is_a?(String) ? raw_body : raw_body.to_s
|
|
139
144
|
|
|
140
145
|
process_soap(body)
|
|
141
146
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: tina4ruby
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.13.
|
|
4
|
+
version: 3.13.32
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Tina4 Team
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-06-
|
|
11
|
+
date: 2026-06-17 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rack
|