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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1c71dd3827f3ae5125eb53a9fb77ebb9df940f02616163b93d3934b28561c902
4
- data.tar.gz: 4269e49ecb160a17cd58f17ee10dce63d483fd3f411aab05642014dd64ef5e81
3
+ metadata.gz: 02b84dbb5f8e4d009306495921ab7849a28fd769d81d13019b311523d970a0e5
4
+ data.tar.gz: e1b298c7bd03444def8787941f87a7e6fd0adcb6be23efe810990c6005d27910
5
5
  SHA512:
6
- metadata.gz: c3c4db5b3fa3359a8c27ad62d89dce04c60953be4d9e9dc6f60343f34f5168e1bc86a8df0cb6c37d40a9559cec5e950737438a18573d473960151beccd0ddacf
7
- data.tar.gz: af8d3b0c1847de387fac1bf7af34eca83bbbf287e57268f4e86d83610dd919d358f9e071e814653503bb599dbf545aa3f1547edbe13d529166904c0773b72151
6
+ metadata.gz: 8bf774f6208956221c87bb2f546a04aab950a3cae5962dae94bbd430ee55463481886ba315a21f2680b017828d99df0c6e7a54fff331cff0e559b176da45c5a9
7
+ data.tar.gz: 5242a9295d6e70763ca68a377f2aa3dfd2ac8b37402b8660f439ea6bc6e0c5ddb1f278d277c2ff4ab8bf62b6d80f0ad316c8c07c099f07ecdee9040bea0ddfe5
@@ -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
- def fetch_all(sql, params = [], limit: nil, offset: nil)
370
- fetch(sql, params, limit: limit, offset: offset).records
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
- def fetch(sql, params = [], limit: 100, offset: nil)
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
- def fetch_one(sql, params = [])
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
- body = request.body
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
- # Raw body string
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
- @body_raw ||= read_body
189
+ @body_parsed ||= parse_body
167
190
  end
168
191
 
169
- # Parsed body (JSON or form data)
170
- def body_parsed
171
- @body_parsed ||= parse_body
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(body)
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(body)
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
- result[key] = {
328
- filename: value[:filename],
329
- type: value[:type],
330
- tempfile: value[:tempfile],
331
- size: value[:tempfile].size
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
@@ -285,9 +285,19 @@ module Tina4
285
285
  self
286
286
  end
287
287
 
288
- # Stream response from a block for Server-Sent Events (SSE).
288
+ # Stream a response for Server-Sent Events (SSE) / chunked transfer.
289
289
  #
290
- # Usage:
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
- @_stream_block.call(yielder)
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
@@ -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
- method = if request.respond_to?(:[])
110
- request[:_cache_method]
111
- else
112
- request.instance_variable_get(:@_cache_method)
113
- end
114
- url = if request.respond_to?(:[])
115
- request[:_cache_url]
116
- else
117
- request.instance_variable_get(:@_cache_url)
118
- end
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Tina4
4
- VERSION = "3.13.30"
4
+ VERSION = "3.13.32"
5
5
  end
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 @request.respond_to?(:body) && @request.body && !@request.body.to_s.empty?
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 = if @request.respond_to?(: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.30
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-16 00:00:00.000000000 Z
11
+ date: 2026-06-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack