tina4ruby 3.13.98 → 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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +84 -0
- data/lib/tina4/ai.rb +32 -3
- data/lib/tina4/api.rb +5 -0
- data/lib/tina4/auto_crud.rb +62 -4
- data/lib/tina4/background.rb +112 -31
- data/lib/tina4/cache.rb +3 -2
- data/lib/tina4/cli.rb +55 -67
- data/lib/tina4/database.rb +97 -49
- data/lib/tina4/database_adapter.rb +169 -15
- data/lib/tina4/dev_admin.rb +137 -9
- data/lib/tina4/dispatch_pipeline.rb +145 -4
- data/lib/tina4/drivers/firebird_driver.rb +59 -12
- data/lib/tina4/drivers/mongodb_driver.rb +98 -14
- data/lib/tina4/drivers/mssql_driver.rb +39 -2
- data/lib/tina4/drivers/mysql_driver.rb +43 -3
- data/lib/tina4/drivers/odbc_driver.rb +36 -2
- data/lib/tina4/drivers/postgres_driver.rb +5 -0
- data/lib/tina4/drivers/sqlite_driver.rb +11 -1
- data/lib/tina4/env.rb +1 -1
- data/lib/tina4/error_overlay.rb +43 -49
- data/lib/tina4/field_types.rb +33 -16
- data/lib/tina4/frond.rb +24 -2
- data/lib/tina4/gallery/auth/src/routes/api/gallery_auth.rb +1 -1
- data/lib/tina4/gallery/templates/src/templates/gallery_page.twig +1 -1
- data/lib/tina4/graphql.rb +2 -2
- data/lib/tina4/log.rb +652 -485
- data/lib/tina4/mcp.rb +9 -1
- data/lib/tina4/messenger.rb +25 -0
- data/lib/tina4/middleware.rb +189 -76
- data/lib/tina4/migration.rb +47 -15
- data/lib/tina4/orm.rb +280 -59
- data/lib/tina4/port_takeover.rb +202 -0
- data/lib/tina4/public/js/tina4-dev-admin.min.js +23 -19
- data/lib/tina4/rack_app.rb +201 -59
- data/lib/tina4/realtime.rb +6 -1
- data/lib/tina4/request.rb +259 -51
- data/lib/tina4/router.rb +20 -2
- data/lib/tina4/seeder.rb +68 -19
- data/lib/tina4/shutdown.rb +4 -0
- data/lib/tina4/sql_translator.rb +115 -86
- data/lib/tina4/swagger.rb +19 -3
- data/lib/tina4/template.rb +61 -6
- data/lib/tina4/test_client.rb +49 -3
- data/lib/tina4/testing.rb +16 -11
- data/lib/tina4/validator.rb +7 -1
- data/lib/tina4/version.rb +1 -1
- data/lib/tina4/webserver.rb +28 -40
- data/lib/tina4.rb +12 -1
- metadata +3 -2
data/lib/tina4/field_types.rb
CHANGED
|
@@ -43,31 +43,45 @@ module Tina4
|
|
|
43
43
|
end
|
|
44
44
|
end
|
|
45
45
|
|
|
46
|
-
|
|
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
|
-
|
|
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
|
|
104
|
-
#
|
|
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 ||
|
|
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 ||
|
|
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.
|
|
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(
|
|
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.
|
|
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>
|
|
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.
|
|
1094
|
+
query = request.query["query"]
|
|
1095
1095
|
if query
|
|
1096
|
-
variables = request.
|
|
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)
|