tina4ruby 3.13.74 → 3.13.76

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: ddc9319e2cc05c631d4f456805dccecaf55f2cee47906a06624ff93c7f778c97
4
- data.tar.gz: 871df06b691800d7eed0e9693b5b2ebecc131d0dec5c00084aa603142f0fb35d
3
+ metadata.gz: da6364e88c7169f37da6b3e667d31b9d736448190b6004f07405a5547fff2fb9
4
+ data.tar.gz: 5c98dd7dbf126623a6839b6b0c34b75f05ea2a123f2028602ed9857edd7452b9
5
5
  SHA512:
6
- metadata.gz: 132581be6195fb4a7d9a90ac086903c1c4466776075ee0cdcef7256647ba9e147e3d14c7a198fbba1ef3594f2c9ec06497ce560ea3540763f0033526930157d6
7
- data.tar.gz: da580bca2f664d573b0dc628fd2b90f7885bcf3cb58b72a49c74e2f1a240e343122de9e75f4e351fcccd5635d8cc2ecba725115d98c205f13816298680b98c91
6
+ metadata.gz: 972e3873fd9f9bc30171db684976ef9313321c3e7d82c10e09cdbcde43bda9a494d47f322e619f9bd2568f204b9f579589dd3b79c37092539905121176bd1c9c
7
+ data.tar.gz: 262a054d5b6a81a9b6b8f3101dedf70eff3c90117bc57a22a8e4cdbca65e3ea17a604fee65604291ad8c729fbc9b8002eddfbec1d63c24c35a3f4d58a32b861d
@@ -648,22 +648,48 @@ module Tina4
648
648
  # record_migration path it is a standalone write, mirroring the Python
649
649
  # master's two-statement delete-then-insert.
650
650
  _remove_migration_record(name)
651
+
652
+ # Build the column list from the columns that ACTUALLY exist on the table,
653
+ # so a legacy column left behind by an in-place upgrade is populated rather
654
+ # than defaulted to NULL. Ruby never created a `migration_id` column, but a
655
+ # Ruby app can be pointed at a database whose tina4_migration table was
656
+ # created by tina4-python v3 <= 3.13.54 - there `migration_id` is NOT NULL,
657
+ # and an insert that omits it fails, wedging every migration on that
658
+ # database (tina4-python#93). Mirrors the PHP + Python masters.
659
+ cols = _tracking_columns
660
+ insert_cols = %w[migration_name description batch executed_at passed]
661
+ values = [name, desc, batch, executed_at, passed]
662
+
663
+ if cols.include?("migration_id")
664
+ insert_cols << "migration_id"
665
+ values << name
666
+ end
667
+
651
668
  if firebird?
652
669
  # Firebird: generate ID from sequence
653
670
  row = @db.fetch_one(
654
671
  "SELECT GEN_ID(GEN_TINA4_MIGRATION_ID, 1) AS NEXT_ID FROM RDB\$DATABASE"
655
672
  )
656
673
  next_id = row ? (row[:NEXT_ID] || row[:next_id] || 1).to_i : 1
657
- @db.execute(
658
- "INSERT INTO #{TRACKING_TABLE} (id, migration_name, description, batch, executed_at, passed) VALUES (?, ?, ?, ?, ?, ?)",
659
- [next_id, name, desc, batch, executed_at, passed]
660
- )
661
- else
662
- @db.execute(
663
- "INSERT INTO #{TRACKING_TABLE} (migration_name, description, batch, executed_at, passed) VALUES (?, ?, ?, ?, ?)",
664
- [name, desc, batch, executed_at, passed]
665
- )
674
+ insert_cols.unshift("id")
675
+ values.unshift(next_id)
676
+ end
677
+
678
+ placeholders = Array.new(insert_cols.length, "?").join(", ")
679
+ @db.execute(
680
+ "INSERT INTO #{TRACKING_TABLE} (#{insert_cols.join(', ')}) VALUES (#{placeholders})",
681
+ values
682
+ )
683
+ end
684
+
685
+ # Lowercased column names on the tracking table; empty on any failure so a
686
+ # missing/unreadable table falls through to the canonical column list.
687
+ def _tracking_columns
688
+ (@db.columns(TRACKING_TABLE) || []).map do |c|
689
+ (c.is_a?(Hash) ? (c[:name] || c["name"]) : c).to_s.downcase
666
690
  end
691
+ rescue StandardError
692
+ []
667
693
  end
668
694
 
669
695
  def _remove_migration_record(name)
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
  require "json"
3
3
  require "securerandom"
4
+ require "time"
4
5
  require "uri"
5
6
 
6
7
  module Tina4
@@ -128,7 +129,7 @@ module Tina4
128
129
  end
129
130
 
130
131
  # Static files (only for non-API paths)
131
- static_response = try_static(path)
132
+ static_response = try_static(path, env)
132
133
  return static_response if static_response
133
134
  end
134
135
 
@@ -386,30 +387,94 @@ module Tina4
386
387
  final_response.to_rack
387
388
  end
388
389
 
389
- def try_static(path)
390
+ def try_static(path, env = nil)
390
391
  return nil if path.include?("..")
391
392
 
392
393
  @static_roots.each do |root|
393
394
  full_path = File.join(root, path)
394
395
  if File.file?(full_path)
395
- return serve_static_file(full_path)
396
+ return serve_static_file(full_path, env)
396
397
  end
397
398
 
398
399
  # Only try index.html for directory-like paths
399
400
  if path.end_with?("/") || !path.include?(".")
400
401
  index_path = File.join(full_path, "index.html")
401
402
  if File.file?(index_path)
402
- return serve_static_file(index_path)
403
+ return serve_static_file(index_path, env)
403
404
  end
404
405
  end
405
406
  end
406
407
  nil
407
408
  end
408
409
 
409
- def serve_static_file(full_path)
410
+ # Serve a static asset with cache-revalidation semantics (parity with the
411
+ # Python master). A static file may be cached by the browser but MUST be
412
+ # revalidated before use, so a redeployed asset reaches the browser on the
413
+ # next load without a manual hard refresh — and an unchanged asset costs a
414
+ # cheap 304 round-trip, not a re-download.
415
+ #
416
+ # Validators are derived from the file's mtime + size (a weak ETag) plus a
417
+ # Last-Modified date, so we never have to read/hash the bytes to build them.
418
+ # When the request already carries a matching validator (If-None-Match, or
419
+ # If-Modified-Since not older than the file) we answer a bare 304 with no
420
+ # body. Stdlib only — no new dependency.
421
+ def serve_static_file(full_path, env = nil)
410
422
  ext = File.extname(full_path).downcase
411
423
  content_type = Tina4::Response::MIME_TYPES[ext] || "application/octet-stream"
412
- [200, { "content-type" => content_type }, [File.binread(full_path)]]
424
+
425
+ stat = File.stat(full_path)
426
+ etag = %(W/"#{stat.mtime.to_i.to_s(16)}-#{stat.size.to_s(16)}")
427
+ last_modified = stat.mtime.httpdate
428
+
429
+ headers = {
430
+ "content-type" => content_type,
431
+ "cache-control" => "no-cache, must-revalidate",
432
+ "etag" => etag,
433
+ "last-modified" => last_modified
434
+ }
435
+
436
+ if env && static_not_modified?(env, etag, stat.mtime)
437
+ # 304 carries the validators (RFC 9110 §15.4.5) but NO body.
438
+ return [304, {
439
+ "cache-control" => "no-cache, must-revalidate",
440
+ "etag" => etag,
441
+ "last-modified" => last_modified
442
+ }, []]
443
+ end
444
+
445
+ [200, headers, [File.binread(full_path)]]
446
+ end
447
+
448
+ # Decide whether a conditional GET for a static asset can be answered 304.
449
+ # If-None-Match takes precedence over If-Modified-Since (RFC 9110 §13.1.3);
450
+ # ETag comparison is weak (a leading `W/` is ignored on either side).
451
+ def static_not_modified?(env, etag, mtime)
452
+ if_none_match = env["HTTP_IF_NONE_MATCH"]
453
+ if if_none_match && !if_none_match.empty?
454
+ return true if if_none_match.strip == "*"
455
+
456
+ want = weak_etag_value(etag)
457
+ return if_none_match.split(",").any? { |candidate| weak_etag_value(candidate) == want }
458
+ end
459
+
460
+ if_modified_since = env["HTTP_IF_MODIFIED_SINCE"]
461
+ if if_modified_since && !if_modified_since.empty?
462
+ begin
463
+ # HTTP dates have 1-second resolution — compare at whole seconds so a
464
+ # sub-second mtime never spuriously looks "newer" than the client copy.
465
+ return mtime.to_i <= Time.httpdate(if_modified_since).to_i
466
+ rescue ArgumentError
467
+ return false # Unparseable date — treat as no validator, serve the body.
468
+ end
469
+ end
470
+
471
+ false
472
+ end
473
+
474
+ # Normalise an ETag for weak comparison: drop a leading `W/` and surrounding
475
+ # whitespace so `W/"abc"` and `"abc"` compare equal.
476
+ def weak_etag_value(tag)
477
+ tag.to_s.strip.sub(/\AW\//, "")
413
478
  end
414
479
 
415
480
  def serve_swagger_ui
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.74"
4
+ VERSION = "3.13.76"
5
5
  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.74
4
+ version: 3.13.76
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-07-13 00:00:00.000000000 Z
11
+ date: 2026-07-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack