tina4ruby 3.13.97 → 3.13.99

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.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +84 -0
  3. data/lib/tina4/ai.rb +32 -3
  4. data/lib/tina4/api.rb +5 -0
  5. data/lib/tina4/auto_crud.rb +62 -4
  6. data/lib/tina4/background.rb +112 -31
  7. data/lib/tina4/cache.rb +3 -2
  8. data/lib/tina4/cli.rb +55 -67
  9. data/lib/tina4/database.rb +97 -49
  10. data/lib/tina4/database_adapter.rb +169 -15
  11. data/lib/tina4/dev_admin.rb +137 -9
  12. data/lib/tina4/dispatch_pipeline.rb +145 -4
  13. data/lib/tina4/drivers/firebird_driver.rb +59 -12
  14. data/lib/tina4/drivers/mongodb_driver.rb +98 -14
  15. data/lib/tina4/drivers/mssql_driver.rb +39 -2
  16. data/lib/tina4/drivers/mysql_driver.rb +43 -3
  17. data/lib/tina4/drivers/odbc_driver.rb +36 -2
  18. data/lib/tina4/drivers/postgres_driver.rb +5 -0
  19. data/lib/tina4/drivers/sqlite_driver.rb +11 -1
  20. data/lib/tina4/env.rb +1 -1
  21. data/lib/tina4/error_overlay.rb +43 -49
  22. data/lib/tina4/field_types.rb +33 -16
  23. data/lib/tina4/frond.rb +24 -2
  24. data/lib/tina4/gallery/auth/src/routes/api/gallery_auth.rb +1 -1
  25. data/lib/tina4/gallery/templates/src/templates/gallery_page.twig +1 -1
  26. data/lib/tina4/graphql.rb +2 -2
  27. data/lib/tina4/log.rb +652 -485
  28. data/lib/tina4/mcp.rb +9 -1
  29. data/lib/tina4/messenger.rb +25 -0
  30. data/lib/tina4/middleware.rb +189 -76
  31. data/lib/tina4/migration.rb +47 -15
  32. data/lib/tina4/orm.rb +280 -59
  33. data/lib/tina4/port_takeover.rb +202 -0
  34. data/lib/tina4/public/js/tina4-dev-admin.min.js +23 -19
  35. data/lib/tina4/rack_app.rb +201 -59
  36. data/lib/tina4/realtime.rb +6 -1
  37. data/lib/tina4/request.rb +259 -51
  38. data/lib/tina4/router.rb +20 -2
  39. data/lib/tina4/seeder.rb +68 -19
  40. data/lib/tina4/shutdown.rb +4 -0
  41. data/lib/tina4/sql_translator.rb +115 -86
  42. data/lib/tina4/swagger.rb +19 -3
  43. data/lib/tina4/template.rb +61 -6
  44. data/lib/tina4/test_client.rb +49 -3
  45. data/lib/tina4/testing.rb +16 -11
  46. data/lib/tina4/validator.rb +7 -1
  47. data/lib/tina4/version.rb +1 -1
  48. data/lib/tina4/webserver.rb +28 -40
  49. data/lib/tina4.rb +12 -1
  50. metadata +3 -19
  51. data/lib/tina4/scss/tina4css/_alerts.scss +0 -34
  52. data/lib/tina4/scss/tina4css/_badges.scss +0 -22
  53. data/lib/tina4/scss/tina4css/_buttons.scss +0 -69
  54. data/lib/tina4/scss/tina4css/_cards.scss +0 -49
  55. data/lib/tina4/scss/tina4css/_forms.scss +0 -156
  56. data/lib/tina4/scss/tina4css/_grid.scss +0 -81
  57. data/lib/tina4/scss/tina4css/_modals.scss +0 -84
  58. data/lib/tina4/scss/tina4css/_nav.scss +0 -149
  59. data/lib/tina4/scss/tina4css/_pagination.scss +0 -63
  60. data/lib/tina4/scss/tina4css/_reset.scss +0 -94
  61. data/lib/tina4/scss/tina4css/_tables.scss +0 -54
  62. data/lib/tina4/scss/tina4css/_typography.scss +0 -55
  63. data/lib/tina4/scss/tina4css/_utilities.scss +0 -208
  64. data/lib/tina4/scss/tina4css/_variables.scss +0 -117
  65. data/lib/tina4/scss/tina4css/base.scss +0 -1
  66. data/lib/tina4/scss/tina4css/colors.scss +0 -48
  67. data/lib/tina4/scss/tina4css/tina4.scss +0 -18
@@ -43,31 +43,45 @@ module Tina4
43
43
  end
44
44
  end
45
45
 
46
- def integer_field(name, primary_key: false, auto_increment: false, nullable: true, default: nil)
46
+ # Feature 19: the constraint options (required:, min:/max: for numbers,
47
+ # min_length:/max_length:/pattern: for strings) are ENFORCED on save() by
48
+ # ORM#validate -- bringing Ruby up to the shared cross-framework richness
49
+ # (Ruby used to be null-only). `length:` stays a DDL VARCHAR sizing hint and
50
+ # is deliberately never validated (parity with PHP's `length`); use
51
+ # max_length: for a value cap.
52
+ def integer_field(name, primary_key: false, auto_increment: false, nullable: true, default: nil,
53
+ required: false, min: nil, max: nil)
47
54
  register_field(name, :integer, primary_key: primary_key, auto_increment: auto_increment,
48
- nullable: nullable, default: default)
55
+ nullable: nullable, default: default, required: required, min: min, max: max)
49
56
  end
50
57
 
51
- def string_field(name, length: 255, primary_key: false, nullable: true, default: nil)
58
+ def string_field(name, length: 255, primary_key: false, nullable: true, default: nil,
59
+ required: false, min_length: nil, max_length: nil, pattern: nil)
52
60
  register_field(name, :string, length: length, primary_key: primary_key,
53
- nullable: nullable, default: default)
61
+ nullable: nullable, default: default, required: required,
62
+ min_length: min_length, max_length: max_length, pattern: pattern)
54
63
  end
55
64
 
56
- def text_field(name, nullable: true, default: nil)
57
- register_field(name, :text, nullable: nullable, default: default)
65
+ def text_field(name, nullable: true, default: nil,
66
+ required: false, min_length: nil, max_length: nil, pattern: nil)
67
+ register_field(name, :text, nullable: nullable, default: default, required: required,
68
+ min_length: min_length, max_length: max_length, pattern: pattern)
58
69
  end
59
70
 
60
- def float_field(name, nullable: true, default: nil)
61
- register_field(name, :float, nullable: nullable, default: default)
71
+ def float_field(name, nullable: true, default: nil, required: false, min: nil, max: nil)
72
+ register_field(name, :float, nullable: nullable, default: default,
73
+ required: required, min: min, max: max)
62
74
  end
63
75
 
64
- def decimal_field(name, precision: 10, scale: 2, nullable: true, default: nil)
76
+ def decimal_field(name, precision: 10, scale: 2, nullable: true, default: nil,
77
+ required: false, min: nil, max: nil)
65
78
  register_field(name, :decimal, precision: precision, scale: scale,
66
- nullable: nullable, default: default)
79
+ nullable: nullable, default: default, required: required, min: min, max: max)
67
80
  end
68
81
 
69
- def numeric_field(name, nullable: true, default: nil)
70
- register_field(name, :float, nullable: nullable, default: default)
82
+ def numeric_field(name, nullable: true, default: nil, required: false, min: nil, max: nil)
83
+ register_field(name, :float, nullable: nullable, default: default,
84
+ required: required, min: min, max: max)
71
85
  end
72
86
 
73
87
  def boolean_field(name, nullable: true, default: nil)
@@ -100,8 +114,11 @@ module Tina4
100
114
  # - Registers an integer field for the column
101
115
  # - Calls belongs_to on this class (strip _id suffix for association name)
102
116
  # - Calls has_many on the referenced class. The accessor name is the
103
- # declaring class name lowercased + "s" (e.g. Post → posts), matching
104
- # Python; override with related_name:. Works whether the referenced
117
+ # declaring class name lowercased and SMART-pluralized via
118
+ # Tina4.pluralize (e.g. Post posts, Category → categories -- the same
119
+ # inflection the hand-written has_many uses, not a naive + "s" that
120
+ # produced "categorys"); override with related_name:. Works whether the
121
+ # referenced
105
122
  # class loaded before OR after this one, including the string form
106
123
  # (references: "Author") for forward references — resolution is
107
124
  # deferred via Tina4::ORM.inherited until the target class exists.
@@ -128,7 +145,7 @@ module Tina4
128
145
 
129
146
  # Wire has_many on referenced class (if already a loaded Class)
130
147
  if references.is_a?(Class) && references.respond_to?(:has_many, true)
131
- hm_name = (related_name || "#{self.name.split("::").last.downcase}s").to_sym
148
+ hm_name = (related_name || Tina4.pluralize(self.name.split("::").last.downcase)).to_sym
132
149
  references.has_many(hm_name, class_name: self.name.split("::").last, foreign_key: name.to_s)
133
150
  end
134
151
 
@@ -136,7 +153,7 @@ module Tina4
136
153
  @@_fk_registry ||= {}
137
154
  ref_name = references.is_a?(Class) ? references.name.split("::").last : references.to_s.split("::").last
138
155
  @@_fk_registry[ref_name] ||= []
139
- hm_key = (related_name || "#{self.name.split("::").last.downcase}s").to_s
156
+ hm_key = (related_name || Tina4.pluralize(self.name.split("::").last.downcase)).to_s
140
157
  @@_fk_registry[ref_name] << {
141
158
  declaring_class: self,
142
159
  has_many_name: hm_key.to_sym,
data/lib/tina4/frond.rb CHANGED
@@ -651,11 +651,33 @@ module Tina4
651
651
  # Template loading
652
652
  # -----------------------------------------------------------------------
653
653
 
654
+ # Load a template's source, CONFINED under the templates directory.
655
+ #
656
+ # Every path-taking tag ({% include %}, {% extends %}, {% import %},
657
+ # {% from ... import %}) funnels through this one loader, so this single guard
658
+ # confines them all (TAG-DEC-01): a name that is absolute, climbs out with a
659
+ # +..+ up-level segment, or resolves through a symlink to a location OUTSIDE
660
+ # the templates root is REFUSED -- the outside file is never read. Template
661
+ # -side analogue of the static-asset confinement (feature 41 / ADR-0050).
654
662
  def load_template(name)
663
+ # Lexical belt: refuse an absolute path or a `..` up-level segment before
664
+ # touching the filesystem (defense in depth in front of the realpath check).
665
+ if name.start_with?("/", "\\") || name =~ %r{\A[A-Za-z]:} ||
666
+ name.split(%r{[\\/]}).include?("..")
667
+ raise "Template path escapes the templates directory: #{name}"
668
+ end
655
669
  path = File.join(@template_dir, name)
656
- raise "Template not found: #{path}" unless File.exist?(path)
670
+ raise "Template not found: #{path}" unless File.file?(path)
671
+
672
+ # Realpath containment: a symlink INSIDE the templates dir whose target
673
+ # resolves OUTSIDE it is refused (the lexical belt cannot see a symlink).
674
+ root = File.realpath(@template_dir)
675
+ real = File.realpath(path)
676
+ unless real == root || real.start_with?(root + File::SEPARATOR)
677
+ raise "Template path escapes the templates directory: #{name}"
678
+ end
657
679
 
658
- File.read(path, encoding: "utf-8")
680
+ File.read(real, encoding: "utf-8")
659
681
  end
660
682
 
661
683
  # -----------------------------------------------------------------------
@@ -108,7 +108,7 @@ Tina4::Router.post("/api/gallery/auth/login") do |request, response|
108
108
  end
109
109
 
110
110
  Tina4::Router.get("/api/gallery/auth/verify") do |request, response|
111
- token = request.params["token"].to_s
111
+ token = request.query["token"].to_s
112
112
  result = Tina4::Auth.validate_token(token)
113
113
  response.json({ valid: result[:valid] })
114
114
  end
@@ -176,7 +176,7 @@
176
176
  </thead>
177
177
  <tbody>
178
178
  <tr><td>1</td><td>tina4-python</td><td>7145</td><td>Python 3.12+</td><td><span class="badge bg-success">Stable</span></td></tr>
179
- <tr><td>2</td><td>tina4-php</td><td>7146</td><td>PHP 8.2+</td><td><span class="badge bg-success">Stable</span></td></tr>
179
+ <tr><td>2</td><td>tina4-php</td><td>7145</td><td>PHP 8.2+</td><td><span class="badge bg-success">Stable</span></td></tr>
180
180
  <tr><td>3</td><td>tina4-ruby</td><td>7147</td><td>Ruby 3.1+</td><td><span class="badge bg-success">Stable</span></td></tr>
181
181
  <tr><td>4</td><td>tina4-nodejs</td><td>7148</td><td>Node 20+</td><td><span class="badge bg-success">Stable</span></td></tr>
182
182
  </tbody>
data/lib/tina4/graphql.rb CHANGED
@@ -1091,9 +1091,9 @@ module Tina4
1091
1091
 
1092
1092
  # Optional: GET for GraphiQL/introspection
1093
1093
  Tina4.get path, auth: false do |request, response|
1094
- query = request.params["query"]
1094
+ query = request.query["query"]
1095
1095
  if query
1096
- variables = request.params["variables"]
1096
+ variables = request.query["variables"]
1097
1097
  variables = JSON.parse(variables) if variables.is_a?(String) && !variables.empty?
1098
1098
  result = graphql.execute(query, variables: variables || {}, context: { request: request })
1099
1099
  response.json(result)