tina4ruby 0.5.2 → 3.2.1
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 +1 -1
- data/README.md +434 -544
- data/exe/{tina4 → tina4ruby} +1 -0
- data/lib/tina4/ai.rb +312 -0
- data/lib/tina4/auth.rb +44 -3
- data/lib/tina4/auto_crud.rb +163 -0
- data/lib/tina4/cli.rb +389 -97
- data/lib/tina4/constants.rb +46 -0
- data/lib/tina4/cors.rb +74 -0
- data/lib/tina4/database/sqlite3_adapter.rb +139 -0
- data/lib/tina4/database.rb +144 -7
- data/lib/tina4/debug.rb +4 -79
- data/lib/tina4/dev_admin.rb +1162 -0
- data/lib/tina4/dev_mailbox.rb +191 -0
- data/lib/tina4/dev_reload.rb +9 -9
- data/lib/tina4/drivers/firebird_driver.rb +19 -3
- data/lib/tina4/drivers/mssql_driver.rb +3 -3
- data/lib/tina4/drivers/mysql_driver.rb +4 -4
- data/lib/tina4/drivers/postgres_driver.rb +9 -2
- data/lib/tina4/drivers/sqlite_driver.rb +1 -1
- data/lib/tina4/env.rb +42 -2
- data/lib/tina4/error_overlay.rb +252 -0
- data/lib/tina4/events.rb +90 -0
- data/lib/tina4/field_types.rb +4 -0
- data/lib/tina4/frond.rb +1497 -0
- data/lib/tina4/gallery/auth/meta.json +1 -0
- data/lib/tina4/gallery/auth/src/routes/api/gallery_auth.rb +114 -0
- data/lib/tina4/gallery/database/meta.json +1 -0
- data/lib/tina4/gallery/database/src/routes/api/gallery_db.rb +43 -0
- data/lib/tina4/gallery/error-overlay/meta.json +1 -0
- data/lib/tina4/gallery/error-overlay/src/routes/api/gallery_crash.rb +17 -0
- data/lib/tina4/gallery/orm/meta.json +1 -0
- data/lib/tina4/gallery/orm/src/routes/api/gallery_products.rb +16 -0
- data/lib/tina4/gallery/queue/meta.json +1 -0
- data/lib/tina4/gallery/queue/src/routes/api/gallery_queue.rb +325 -0
- data/lib/tina4/gallery/rest-api/meta.json +1 -0
- data/lib/tina4/gallery/rest-api/src/routes/api/gallery_hello.rb +14 -0
- data/lib/tina4/gallery/templates/meta.json +1 -0
- data/lib/tina4/gallery/templates/src/routes/gallery_page.rb +12 -0
- data/lib/tina4/gallery/templates/src/templates/gallery_page.twig +257 -0
- data/lib/tina4/health.rb +39 -0
- data/lib/tina4/html_element.rb +148 -0
- data/lib/tina4/localization.rb +2 -2
- data/lib/tina4/log.rb +203 -0
- data/lib/tina4/messenger.rb +562 -0
- data/lib/tina4/migration.rb +132 -29
- data/lib/tina4/orm.rb +463 -35
- data/lib/tina4/public/css/tina4.css +178 -1
- data/lib/tina4/public/css/tina4.min.css +1 -2
- data/lib/tina4/public/favicon.ico +0 -0
- data/lib/tina4/public/images/logo.svg +5 -0
- data/lib/tina4/public/images/tina4-logo-icon.webp +0 -0
- data/lib/tina4/public/js/frond.min.js +420 -0
- data/lib/tina4/public/js/tina4-dev-admin.min.js +367 -0
- data/lib/tina4/public/js/tina4.min.js +93 -0
- data/lib/tina4/public/swagger/index.html +90 -0
- data/lib/tina4/public/swagger/oauth2-redirect.html +63 -0
- data/lib/tina4/queue.rb +162 -6
- data/lib/tina4/queue_backends/lite_backend.rb +88 -0
- data/lib/tina4/rack_app.rb +331 -27
- data/lib/tina4/rate_limiter.rb +123 -0
- data/lib/tina4/request.rb +61 -15
- data/lib/tina4/response.rb +54 -24
- data/lib/tina4/response_cache.rb +551 -0
- data/lib/tina4/router.rb +90 -15
- data/lib/tina4/scss_compiler.rb +2 -2
- data/lib/tina4/seeder.rb +56 -61
- data/lib/tina4/service_runner.rb +303 -0
- data/lib/tina4/session.rb +85 -0
- data/lib/tina4/session_handlers/mongo_handler.rb +1 -1
- data/lib/tina4/session_handlers/valkey_handler.rb +43 -0
- data/lib/tina4/shutdown.rb +84 -0
- data/lib/tina4/sql_translation.rb +295 -0
- data/lib/tina4/template.rb +36 -6
- data/lib/tina4/templates/base.twig +2 -2
- data/lib/tina4/templates/errors/302.twig +14 -0
- data/lib/tina4/templates/errors/401.twig +9 -0
- data/lib/tina4/templates/errors/403.twig +22 -15
- data/lib/tina4/templates/errors/404.twig +22 -15
- data/lib/tina4/templates/errors/500.twig +31 -15
- data/lib/tina4/templates/errors/502.twig +9 -0
- data/lib/tina4/templates/errors/503.twig +12 -0
- data/lib/tina4/templates/errors/base.twig +37 -0
- data/lib/tina4/version.rb +1 -1
- data/lib/tina4/webserver.rb +28 -18
- data/lib/tina4.rb +118 -21
- metadata +68 -8
- data/lib/tina4/public/js/tina4.js +0 -134
- data/lib/tina4/public/js/tina4helper.js +0 -387
data/README.md
CHANGED
|
@@ -1,761 +1,651 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://tina4.com/logo.svg" alt="Tina4" width="200">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<h1 align="center">Tina4 Ruby</h1>
|
|
6
|
+
<h3 align="center">This is not a framework</h3>
|
|
7
|
+
|
|
8
|
+
<p align="center">
|
|
9
|
+
Laravel joy. Ruby speed. 10x less code. Zero third-party dependencies.
|
|
10
|
+
</p>
|
|
11
|
+
|
|
12
|
+
<p align="center">
|
|
13
|
+
<a href="https://tina4.com">Documentation</a> •
|
|
14
|
+
<a href="#getting-started">Getting Started</a> •
|
|
15
|
+
<a href="#features">Features</a> •
|
|
16
|
+
<a href="#cli-reference">CLI Reference</a> •
|
|
17
|
+
<a href="https://tina4.com">tina4.com</a>
|
|
18
|
+
</p>
|
|
19
|
+
|
|
20
|
+
<p align="center">
|
|
21
|
+
<img src="https://img.shields.io/badge/tests-1577%20passing-brightgreen" alt="Tests">
|
|
22
|
+
<img src="https://img.shields.io/badge/carbonah-A%2B%20rated-00cc44" alt="Carbonah A+">
|
|
23
|
+
<img src="https://img.shields.io/badge/zero--dep-core-blue" alt="Zero Dependencies">
|
|
24
|
+
<img src="https://img.shields.io/badge/ruby-3.1%2B-blue" alt="Ruby 3.1+">
|
|
25
|
+
<img src="https://img.shields.io/badge/license-MIT-lightgrey" alt="MIT License">
|
|
26
|
+
</p>
|
|
2
27
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
A lightweight, zero-configuration, Windows-friendly Ruby web framework. If you know [tina4_python](https://tina4.com) or tina4_php, you'll feel right at home.
|
|
28
|
+
---
|
|
6
29
|
|
|
7
30
|
## Quick Start
|
|
8
31
|
|
|
9
32
|
```bash
|
|
10
|
-
|
|
11
|
-
tina4
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
tina4
|
|
33
|
+
# Install the Tina4 CLI
|
|
34
|
+
cargo install tina4 # or download binary from https://github.com/tina4stack/tina4/releases
|
|
35
|
+
|
|
36
|
+
# Create a project
|
|
37
|
+
tina4 init ruby ./my-app
|
|
38
|
+
|
|
39
|
+
# Run it
|
|
40
|
+
cd my-app && tina4 serve
|
|
15
41
|
```
|
|
16
42
|
|
|
17
|
-
|
|
43
|
+
Open http://localhost:7147 — your app is running.
|
|
18
44
|
|
|
19
|
-
|
|
45
|
+
<details>
|
|
46
|
+
<summary><strong>Without the Tina4 CLI</strong></summary>
|
|
20
47
|
|
|
21
|
-
|
|
48
|
+
```bash
|
|
49
|
+
# 1. Create project
|
|
50
|
+
mkdir my-app && cd my-app
|
|
51
|
+
echo 'source "https://rubygems.org"' > Gemfile
|
|
52
|
+
echo 'gem "tina4-ruby", "~> 3.0"' >> Gemfile
|
|
53
|
+
bundle install
|
|
22
54
|
|
|
23
|
-
|
|
55
|
+
# 2. Create entry point
|
|
56
|
+
cat > app.rb << 'EOF'
|
|
24
57
|
require "tina4"
|
|
58
|
+
Tina4.initialize!(__dir__)
|
|
59
|
+
app = Tina4::RackApp.new
|
|
60
|
+
Tina4::WebServer.new(app, host: "0.0.0.0", port: 7147).start
|
|
61
|
+
EOF
|
|
25
62
|
|
|
26
|
-
#
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
end
|
|
63
|
+
# 3. Create .env
|
|
64
|
+
echo 'TINA4_DEBUG=true' > .env
|
|
65
|
+
echo 'TINA4_LOG_LEVEL=ALL' >> .env
|
|
30
66
|
|
|
31
|
-
#
|
|
32
|
-
|
|
33
|
-
data = request.json_body
|
|
34
|
-
response.json({ created: true, name: data["name"] }, 201)
|
|
35
|
-
end
|
|
67
|
+
# 4. Create route directory
|
|
68
|
+
mkdir -p src/routes
|
|
36
69
|
|
|
37
|
-
#
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
response.json({ user_id: user_id })
|
|
41
|
-
end
|
|
70
|
+
# 5. Run
|
|
71
|
+
bundle exec ruby app.rb
|
|
72
|
+
```
|
|
42
73
|
|
|
43
|
-
|
|
44
|
-
response.json({ path: request.params["path"] })
|
|
45
|
-
end
|
|
74
|
+
Open http://localhost:7147
|
|
46
75
|
|
|
47
|
-
|
|
48
|
-
Tina4.put "/api/users/{id:int}" do |request, response|
|
|
49
|
-
response.json({ updated: true })
|
|
50
|
-
end
|
|
76
|
+
</details>
|
|
51
77
|
|
|
52
|
-
|
|
53
|
-
response.json({ deleted: true })
|
|
54
|
-
end
|
|
78
|
+
---
|
|
55
79
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
80
|
+
## What's Included
|
|
81
|
+
|
|
82
|
+
Every feature is built from scratch -- no gem install, no node_modules, no third-party runtime dependencies in core.
|
|
83
|
+
|
|
84
|
+
| Category | Features |
|
|
85
|
+
|----------|----------|
|
|
86
|
+
| **HTTP** | Rack 3 server, block routing, path params (`{id:int}`, `{p:path}`), middleware pipeline, CORS, rate limiting, graceful shutdown |
|
|
87
|
+
| **Templates** | Frond engine (Twig-compatible), inheritance, partials, 35+ filters, macros, fragment caching, sandboxing |
|
|
88
|
+
| **ORM** | Active Record, typed fields with validation, soft delete, relationships (`has_one`/`has_many`/`belongs_to`), scopes, result caching, multi-database |
|
|
89
|
+
| **Database** | SQLite, PostgreSQL, MySQL, MSSQL, Firebird -- unified adapter interface, query caching (TINA4_DB_CACHE=true for 4x speedup) |
|
|
90
|
+
| **Auth** | Zero-dep JWT (RS256), sessions (file, Redis, MongoDB), password hashing, form tokens |
|
|
91
|
+
| **API** | Swagger/OpenAPI auto-generation, GraphQL with ORM auto-schema and GraphiQL IDE |
|
|
92
|
+
| **Background** | DB-backed queue with priority, delayed jobs, retry, batch processing, multi-queue |
|
|
93
|
+
| **Real-time** | Native WebSocket (RFC 6455), per-path routing, connection manager |
|
|
94
|
+
| **Frontend** | tina4-css (~24 KB), frond.js helper, SCSS compiler, live reload, CSS hot-reload |
|
|
95
|
+
| **DX** | Dev admin dashboard, error overlay, request inspector, AI tool integration, Carbonah green benchmarks |
|
|
96
|
+
| **Data** | Migrations with rollback, 50+ fake data generators, ORM and table seeders |
|
|
97
|
+
| **Other** | REST client, localization (6 languages), cache (memory/Redis/file), event system, inline testing, messenger (.env driven), configurable error pages |
|
|
98
|
+
|
|
99
|
+
**1,577 tests across 38 built-in features. Zero dependencies. All Carbonah benchmarks rated A+.**
|
|
100
|
+
|
|
101
|
+
For full documentation visit **[tina4.com](https://tina4.com)**.
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Install
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
gem install tina4ruby
|
|
60
109
|
```
|
|
61
110
|
|
|
62
|
-
###
|
|
111
|
+
### Optional database drivers
|
|
63
112
|
|
|
64
|
-
|
|
113
|
+
Install only what you need:
|
|
65
114
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
115
|
+
```bash
|
|
116
|
+
gem install pg # PostgreSQL
|
|
117
|
+
gem install mysql2 # MySQL / MariaDB (driver name: mysql)
|
|
118
|
+
gem install tiny_tds # Microsoft SQL Server
|
|
119
|
+
gem install fb # Firebird
|
|
120
|
+
```
|
|
70
121
|
|
|
71
|
-
|
|
72
|
-
# POST is secured by default — requires Bearer token
|
|
73
|
-
Tina4.post "/api/users" do |request, response|
|
|
74
|
-
response.json({ created: true })
|
|
75
|
-
end
|
|
122
|
+
---
|
|
76
123
|
|
|
77
|
-
|
|
78
|
-
Tina4.post "/api/webhook", auth: false do |request, response|
|
|
79
|
-
response.json({ received: true })
|
|
80
|
-
end
|
|
124
|
+
## Getting Started
|
|
81
125
|
|
|
82
|
-
|
|
83
|
-
custom_auth = lambda do |env|
|
|
84
|
-
env["HTTP_X_API_KEY"] == "my-secret"
|
|
85
|
-
end
|
|
126
|
+
### 1. Create a project
|
|
86
127
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
128
|
+
```bash
|
|
129
|
+
tina4ruby init my-app
|
|
130
|
+
cd my-app
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
This creates:
|
|
134
|
+
|
|
135
|
+
```
|
|
136
|
+
my-app/
|
|
137
|
+
├── app.rb # Entry point
|
|
138
|
+
├── .env # Configuration
|
|
139
|
+
├── Gemfile
|
|
140
|
+
├── src/
|
|
141
|
+
│ ├── routes/ # API + page routes (auto-discovered)
|
|
142
|
+
│ ├── orm/ # Database models
|
|
143
|
+
│ ├── app/ # Service classes and shared helpers
|
|
144
|
+
│ ├── templates/ # Frond/Twig templates
|
|
145
|
+
│ ├── seeds/ # Database seeders
|
|
146
|
+
│ ├── scss/ # SCSS (auto-compiled to public/css/)
|
|
147
|
+
│ └── public/ # Static assets served at /
|
|
148
|
+
├── migrations/ # SQL migration files
|
|
149
|
+
└── tests/ # RSpec tests
|
|
90
150
|
```
|
|
91
151
|
|
|
92
|
-
###
|
|
152
|
+
### 2. Create a route
|
|
93
153
|
|
|
94
|
-
|
|
154
|
+
Create `src/routes/hello.rb`:
|
|
95
155
|
|
|
96
156
|
```ruby
|
|
97
|
-
Tina4.
|
|
98
|
-
response.json({
|
|
157
|
+
Tina4.get "/api/hello" do |request, response|
|
|
158
|
+
response.json({ message: "Hello from Tina4!" })
|
|
99
159
|
end
|
|
100
160
|
|
|
101
|
-
Tina4.
|
|
102
|
-
response.json({
|
|
161
|
+
Tina4.get "/api/hello/{name}" do |request, response|
|
|
162
|
+
response.json({ message: "Hello, #{request.params["name"]}!" })
|
|
103
163
|
end
|
|
104
164
|
```
|
|
105
165
|
|
|
106
|
-
|
|
166
|
+
Visit `http://localhost:7147/api/hello` -- routes are auto-discovered, no requires needed.
|
|
107
167
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
168
|
+
### 3. Add a database
|
|
169
|
+
|
|
170
|
+
Edit `.env`:
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
DATABASE_URL=sqlite://data/app.db
|
|
174
|
+
DATABASE_USERNAME=
|
|
175
|
+
DATABASE_PASSWORD=
|
|
113
176
|
```
|
|
114
177
|
|
|
115
|
-
|
|
178
|
+
Create and run a migration:
|
|
116
179
|
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
request.method # "POST"
|
|
120
|
-
request.path # "/example"
|
|
121
|
-
request.params # merged path + query params
|
|
122
|
-
request.headers # HTTP headers hash
|
|
123
|
-
request.cookies # parsed cookies
|
|
124
|
-
request.body # raw body string
|
|
125
|
-
request.json_body # parsed JSON body (hash)
|
|
126
|
-
request.bearer_token # extracted Bearer token
|
|
127
|
-
request.ip # client IP address
|
|
128
|
-
request.files # uploaded files
|
|
129
|
-
request.session # lazy-loaded session
|
|
130
|
-
end
|
|
180
|
+
```bash
|
|
181
|
+
tina4ruby migrate --create "create users table"
|
|
131
182
|
```
|
|
132
183
|
|
|
133
|
-
|
|
184
|
+
Edit the generated SQL:
|
|
134
185
|
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
186
|
+
```sql
|
|
187
|
+
CREATE TABLE IF NOT EXISTS users (
|
|
188
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
189
|
+
name TEXT NOT NULL,
|
|
190
|
+
email TEXT NOT NULL,
|
|
191
|
+
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
192
|
+
);
|
|
193
|
+
```
|
|
139
194
|
|
|
140
|
-
|
|
141
|
-
|
|
195
|
+
```bash
|
|
196
|
+
tina4ruby migrate
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### 4. Create an ORM model
|
|
142
200
|
|
|
143
|
-
|
|
144
|
-
response.render("pages/home.twig", { title: "Welcome" })
|
|
201
|
+
Create `src/orm/user.rb`:
|
|
145
202
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
203
|
+
```ruby
|
|
204
|
+
class User < Tina4::ORM
|
|
205
|
+
integer_field :id, primary_key: true, auto_increment: true
|
|
206
|
+
string_field :name, nullable: false, length: 100
|
|
207
|
+
string_field :email, length: 255
|
|
208
|
+
datetime_field :created_at
|
|
209
|
+
end
|
|
210
|
+
```
|
|
149
211
|
|
|
150
|
-
|
|
151
|
-
response.text("OK")
|
|
212
|
+
### 5. Build a REST API
|
|
152
213
|
|
|
153
|
-
|
|
154
|
-
response.file("path/to/document.pdf")
|
|
214
|
+
Create `src/routes/users.rb`:
|
|
155
215
|
|
|
156
|
-
|
|
157
|
-
|
|
216
|
+
```ruby
|
|
217
|
+
Tina4.get "/api/users" do |request, response|
|
|
218
|
+
response.json(User.all(limit: 100).map(&:to_hash))
|
|
219
|
+
end
|
|
158
220
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
221
|
+
Tina4.get "/api/users/{id}" do |request, response|
|
|
222
|
+
user = User.find(request.params["id"])
|
|
223
|
+
if user
|
|
224
|
+
response.json(user.to_hash)
|
|
225
|
+
else
|
|
226
|
+
response.json({ error: "Not found" }, 404)
|
|
227
|
+
end
|
|
228
|
+
end
|
|
162
229
|
|
|
163
|
-
|
|
164
|
-
|
|
230
|
+
Tina4.post "/api/users", auth: false do |request, response|
|
|
231
|
+
user = User.create(request.json_body)
|
|
232
|
+
response.json(user.to_hash, 201)
|
|
233
|
+
end
|
|
165
234
|
```
|
|
166
235
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
Tina4 uses a Twig-compatible template engine. Templates go in `templates/` or `src/templates/`.
|
|
236
|
+
### 6. Add a template
|
|
170
237
|
|
|
171
|
-
|
|
238
|
+
Create `src/templates/base.twig`:
|
|
172
239
|
|
|
173
240
|
```twig
|
|
174
241
|
<!DOCTYPE html>
|
|
175
242
|
<html>
|
|
176
243
|
<head>
|
|
177
|
-
|
|
244
|
+
<title>{% block title %}My App{% endblock %}</title>
|
|
245
|
+
<link rel="stylesheet" href="/css/tina4.min.css">
|
|
246
|
+
{% block stylesheets %}{% endblock %}
|
|
178
247
|
</head>
|
|
179
248
|
<body>
|
|
180
|
-
|
|
249
|
+
{% block content %}{% endblock %}
|
|
250
|
+
<script src="/js/frond.js"></script>
|
|
251
|
+
{% block javascripts %}{% endblock %}
|
|
181
252
|
</body>
|
|
182
253
|
</html>
|
|
183
254
|
```
|
|
184
255
|
|
|
185
|
-
|
|
256
|
+
Create `src/templates/pages/home.twig`:
|
|
186
257
|
|
|
187
258
|
```twig
|
|
188
259
|
{% extends "base.twig" %}
|
|
189
|
-
|
|
190
|
-
{% block title %}Home{% endblock %}
|
|
191
|
-
|
|
192
260
|
{% block content %}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
{% if items %}
|
|
261
|
+
<div class="container mt-4">
|
|
262
|
+
<h1>{{ title }}</h1>
|
|
196
263
|
<ul>
|
|
197
|
-
{% for
|
|
198
|
-
|
|
264
|
+
{% for user in users %}
|
|
265
|
+
<li>{{ user.name }} -- {{ user.email }}</li>
|
|
199
266
|
{% endfor %}
|
|
200
267
|
</ul>
|
|
201
|
-
|
|
202
|
-
<p>No items found.</p>
|
|
203
|
-
{% endif %}
|
|
268
|
+
</div>
|
|
204
269
|
{% endblock %}
|
|
205
270
|
```
|
|
206
271
|
|
|
207
|
-
|
|
272
|
+
Render it from a route:
|
|
208
273
|
|
|
209
274
|
```ruby
|
|
210
|
-
Tina4.get "/
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
items: ["apple", "banana", "cherry"]
|
|
214
|
-
})
|
|
275
|
+
Tina4.get "/" do |request, response|
|
|
276
|
+
users = User.all(limit: 20).map(&:to_hash)
|
|
277
|
+
response.render("pages/home.twig", { title: "Users", users: users })
|
|
215
278
|
end
|
|
216
279
|
```
|
|
217
280
|
|
|
218
|
-
###
|
|
219
|
-
|
|
220
|
-
```twig
|
|
221
|
-
{{ name | upper }} {# ALICE #}
|
|
222
|
-
{{ name | lower }} {# alice #}
|
|
223
|
-
{{ name | capitalize }} {# Alice #}
|
|
224
|
-
{{ "hello world" | title }} {# Hello World #}
|
|
225
|
-
{{ " hi " | trim }} {# hi #}
|
|
226
|
-
{{ items | length }} {# 3 #}
|
|
227
|
-
{{ items | join(", ") }} {# a, b, c #}
|
|
228
|
-
{{ missing | default("N/A") }} {# N/A #}
|
|
229
|
-
{{ html | escape }} {# <b>hi</b> #}
|
|
230
|
-
{{ text | nl2br }} {# line<br>break #}
|
|
231
|
-
{{ 3.14159 | round(2) }} {# 3.14 #}
|
|
232
|
-
{{ data | json_encode }} {# {"key":"value"} #}
|
|
233
|
-
```
|
|
234
|
-
|
|
235
|
-
### Includes
|
|
236
|
-
|
|
237
|
-
```twig
|
|
238
|
-
{% include "partials/header.twig" %}
|
|
239
|
-
```
|
|
240
|
-
|
|
241
|
-
### Variables and Math
|
|
242
|
-
|
|
243
|
-
```twig
|
|
244
|
-
{% set greeting = "Hello" %}
|
|
245
|
-
{{ greeting ~ " " ~ name }} {# string concatenation #}
|
|
246
|
-
{{ price * quantity }} {# math #}
|
|
247
|
-
```
|
|
248
|
-
|
|
249
|
-
### Comments
|
|
281
|
+
### 7. Seed, test, deploy
|
|
250
282
|
|
|
251
|
-
```
|
|
252
|
-
|
|
283
|
+
```bash
|
|
284
|
+
tina4ruby seed # Run seeders from src/seeds/
|
|
285
|
+
tina4ruby test # Run test suite
|
|
286
|
+
tina4ruby build # Build distributable
|
|
253
287
|
```
|
|
254
288
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
Multi-database support with a unified API:
|
|
258
|
-
|
|
259
|
-
```ruby
|
|
260
|
-
# SQLite (default, zero-config)
|
|
261
|
-
db = Tina4::Database.new("sqlite://app.db")
|
|
262
|
-
|
|
263
|
-
# PostgreSQL
|
|
264
|
-
db = Tina4::Database.new("postgresql://localhost:5432/mydb")
|
|
289
|
+
For the complete step-by-step guide, visit **[tina4.com](https://tina4.com)**.
|
|
265
290
|
|
|
266
|
-
|
|
267
|
-
db = Tina4::Database.new("mysql://localhost:3306/mydb")
|
|
291
|
+
---
|
|
268
292
|
|
|
269
|
-
|
|
270
|
-
db = Tina4::Database.new("mssql://localhost:1433/mydb")
|
|
271
|
-
```
|
|
293
|
+
## Features
|
|
272
294
|
|
|
273
|
-
###
|
|
295
|
+
### Routing
|
|
274
296
|
|
|
275
297
|
```ruby
|
|
276
|
-
#
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
# Fetch one row
|
|
281
|
-
user = db.fetch_one("SELECT * FROM users WHERE id = ?", [1])
|
|
282
|
-
|
|
283
|
-
# Pagination
|
|
284
|
-
result = db.fetch("SELECT * FROM users", [], limit: 10, skip: 20)
|
|
285
|
-
|
|
286
|
-
# Insert
|
|
287
|
-
db.insert("users", { name: "Alice", email: "alice@example.com" })
|
|
288
|
-
|
|
289
|
-
# Update
|
|
290
|
-
db.update("users", { name: "Alice Updated" }, { id: 1 })
|
|
291
|
-
|
|
292
|
-
# Delete
|
|
293
|
-
db.delete("users", { id: 1 })
|
|
294
|
-
|
|
295
|
-
# Raw SQL
|
|
296
|
-
db.execute("CREATE INDEX idx_email ON users(email)")
|
|
298
|
+
Tina4.get "/api/items" do |request, response| # Public by default
|
|
299
|
+
response.json({ items: [] })
|
|
300
|
+
end
|
|
297
301
|
|
|
298
|
-
#
|
|
299
|
-
|
|
300
|
-
tx.insert("accounts", { name: "Savings", balance: 1000 })
|
|
301
|
-
tx.update("accounts", { balance: 500 }, { id: 1 })
|
|
302
|
+
Tina4.post "/api/webhook", auth: false do |request, response| # Make a write route public
|
|
303
|
+
response.json({ ok: true })
|
|
302
304
|
end
|
|
303
305
|
|
|
304
|
-
#
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
db.columns("users") # [{name: "id", type: "INTEGER", ...}, ...]
|
|
306
|
+
Tina4.secure_get "/api/admin/stats" do |request, response| # Protect a GET route
|
|
307
|
+
response.json({ secret: true })
|
|
308
|
+
end
|
|
308
309
|
```
|
|
309
310
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
```ruby
|
|
313
|
-
result = db.fetch("SELECT * FROM users")
|
|
314
|
-
result.count # number of rows
|
|
315
|
-
result.empty? # true/false
|
|
316
|
-
result.first # first row hash
|
|
317
|
-
result.to_array # array of hashes
|
|
318
|
-
result.to_json # JSON string
|
|
319
|
-
result.to_csv # CSV text
|
|
320
|
-
result.to_paginate # { records_total:, record_count:, data: }
|
|
321
|
-
```
|
|
311
|
+
Path parameter types: `{id}` (string), `{id:int}`, `{price:float}`, `{path:path}` (greedy).
|
|
322
312
|
|
|
323
|
-
|
|
313
|
+
### ORM
|
|
324
314
|
|
|
325
|
-
|
|
315
|
+
Active Record with typed fields, validation, soft delete, relationships, scopes, and multi-database support.
|
|
326
316
|
|
|
327
317
|
```ruby
|
|
328
318
|
class User < Tina4::ORM
|
|
329
319
|
integer_field :id, primary_key: true, auto_increment: true
|
|
330
|
-
string_field :name, nullable: false
|
|
320
|
+
string_field :name, nullable: false, length: 100
|
|
331
321
|
string_field :email, length: 255
|
|
322
|
+
string_field :role, default: "user"
|
|
332
323
|
integer_field :age, default: 0
|
|
333
|
-
datetime_field :created_at
|
|
334
324
|
end
|
|
335
325
|
|
|
336
|
-
#
|
|
337
|
-
Tina4.database = Tina4::Database.new("sqlite://app.db")
|
|
338
|
-
```
|
|
339
|
-
|
|
340
|
-
### CRUD Operations
|
|
341
|
-
|
|
342
|
-
```ruby
|
|
343
|
-
# Create
|
|
326
|
+
# CRUD
|
|
344
327
|
user = User.new(name: "Alice", email: "alice@example.com")
|
|
345
328
|
user.save
|
|
346
|
-
|
|
347
|
-
# Or create in one step
|
|
348
|
-
user = User.create(name: "Bob", email: "bob@example.com")
|
|
349
|
-
|
|
350
|
-
# Read
|
|
351
|
-
user = User.find(1) # by primary key
|
|
352
|
-
users = User.where("age > ?", [18]) # with conditions
|
|
353
|
-
all_users = User.all # all records
|
|
354
|
-
all_users = User.all(limit: 10, order_by: "name")
|
|
355
|
-
|
|
356
|
-
# Update
|
|
357
329
|
user = User.find(1)
|
|
358
|
-
user.name = "Alice Updated"
|
|
359
|
-
user.save
|
|
360
|
-
|
|
361
|
-
# Delete
|
|
362
330
|
user.delete
|
|
363
331
|
|
|
364
|
-
#
|
|
365
|
-
|
|
366
|
-
user.
|
|
367
|
-
user.load
|
|
332
|
+
# Relationships
|
|
333
|
+
orders = user.has_many("Order", "user_id")
|
|
334
|
+
profile = user.has_one("Profile", "user_id")
|
|
368
335
|
|
|
369
|
-
#
|
|
370
|
-
user.
|
|
371
|
-
|
|
372
|
-
|
|
336
|
+
# Soft delete, scopes, caching
|
|
337
|
+
user.soft_delete
|
|
338
|
+
active_admins = User.scope("active").scope("admin").select
|
|
339
|
+
users = User.cached("SELECT * FROM users", ttl: 300)
|
|
373
340
|
|
|
374
|
-
|
|
341
|
+
# Multi-database
|
|
342
|
+
Tina4.database = Tina4::Database.new("sqlite://app.db") # Default
|
|
343
|
+
Tina4.database("audit", Tina4::Database.new("sqlite://audit.db")) # Named
|
|
375
344
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
text_field :bio
|
|
380
|
-
float_field :score
|
|
381
|
-
decimal_field :price, precision: 10, scale: 2
|
|
382
|
-
boolean_field :active
|
|
383
|
-
date_field :birthday
|
|
384
|
-
datetime_field :created_at
|
|
385
|
-
timestamp_field :updated_at
|
|
386
|
-
blob_field :avatar
|
|
387
|
-
json_field :metadata
|
|
345
|
+
class AuditLog < Tina4::ORM
|
|
346
|
+
self.db_name = "audit" # Uses named connection
|
|
347
|
+
end
|
|
388
348
|
```
|
|
389
349
|
|
|
390
|
-
|
|
350
|
+
### Database
|
|
391
351
|
|
|
392
|
-
|
|
393
|
-
# Create a migration
|
|
394
|
-
tina4 migrate --create "create users table"
|
|
352
|
+
Unified interface across 5 engines:
|
|
395
353
|
|
|
396
|
-
|
|
397
|
-
|
|
354
|
+
```ruby
|
|
355
|
+
db = Tina4::Database.new("sqlite://data/app.db")
|
|
356
|
+
db = Tina4::Database.new("postgres://localhost:5432/mydb", username: "user", password: "pass")
|
|
357
|
+
db = Tina4::Database.new("mysql://localhost:3306/mydb", username: "user", password: "pass")
|
|
358
|
+
db = Tina4::Database.new("mssql://localhost:1433/mydb", username: "sa", password: "pass")
|
|
359
|
+
db = Tina4::Database.new("firebird://localhost:3050/path/to/db", username: "SYSDBA", password: "masterkey")
|
|
398
360
|
|
|
399
|
-
|
|
400
|
-
|
|
361
|
+
result = db.fetch("SELECT * FROM users WHERE age > ?", [18], limit: 20, skip: 0)
|
|
362
|
+
row = db.fetch_one("SELECT * FROM users WHERE id = ?", [1])
|
|
363
|
+
db.insert("users", { name: "Alice", email: "alice@test.com" })
|
|
364
|
+
db.commit
|
|
401
365
|
```
|
|
402
366
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
```sql
|
|
406
|
-
-- migrations/20260313120000_create_users_table.sql
|
|
407
|
-
CREATE TABLE users (
|
|
408
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
409
|
-
name TEXT NOT NULL,
|
|
410
|
-
email TEXT UNIQUE,
|
|
411
|
-
age INTEGER DEFAULT 0,
|
|
412
|
-
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
413
|
-
);
|
|
414
|
-
```
|
|
415
|
-
|
|
416
|
-
## Authentication
|
|
417
|
-
|
|
418
|
-
JWT RS256 tokens with auto-generated RSA keys:
|
|
367
|
+
### Middleware
|
|
419
368
|
|
|
420
369
|
```ruby
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
result = Tina4::Auth.validate_token(token)
|
|
426
|
-
if result[:valid]
|
|
427
|
-
payload = result[:payload]
|
|
428
|
-
puts payload["user_id"] # 42
|
|
370
|
+
Tina4.before("/protected") do |request, response|
|
|
371
|
+
unless request.headers["authorization"]
|
|
372
|
+
return request, response.json({ error: "Unauthorized" }, 401)
|
|
373
|
+
end
|
|
429
374
|
end
|
|
430
375
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
Tina4::Auth.verify_password("wrong", hash) # false
|
|
376
|
+
Tina4.get "/protected" do |request, response|
|
|
377
|
+
response.json({ secret: true })
|
|
378
|
+
end
|
|
435
379
|
```
|
|
436
380
|
|
|
437
|
-
###
|
|
381
|
+
### JWT Authentication
|
|
438
382
|
|
|
439
383
|
```ruby
|
|
440
|
-
|
|
441
|
-
Tina4.
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
end
|
|
384
|
+
token = Tina4::Auth.create_token({ user_id: 42 })
|
|
385
|
+
result = Tina4::Auth.validate_token(token)
|
|
386
|
+
payload = Tina4::Auth.get_payload(token)
|
|
387
|
+
```
|
|
445
388
|
|
|
446
|
-
|
|
447
|
-
custom_auth = lambda do |env|
|
|
448
|
-
api_key = env["HTTP_X_API_KEY"]
|
|
449
|
-
api_key == "my-secret-key"
|
|
450
|
-
end
|
|
389
|
+
POST/PUT/PATCH/DELETE routes require `Authorization: Bearer <token>` by default. Use `auth: false` to make public, `secure_get` to protect GET routes.
|
|
451
390
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
391
|
+
### Sessions
|
|
392
|
+
|
|
393
|
+
```ruby
|
|
394
|
+
request.session["user_id"] = 42
|
|
395
|
+
user_id = request.session["user_id"]
|
|
455
396
|
```
|
|
456
397
|
|
|
457
|
-
|
|
398
|
+
Backends: file (default), Redis, MongoDB. Set via `TINA4_SESSION_HANDLER` in `.env`.
|
|
458
399
|
|
|
459
|
-
|
|
460
|
-
Tina4.post "/login" do |request, response|
|
|
461
|
-
request.session["user_id"] = 42
|
|
462
|
-
request.session["role"] = "admin"
|
|
463
|
-
request.session.save
|
|
464
|
-
response.json({ logged_in: true })
|
|
465
|
-
end
|
|
400
|
+
### Queues
|
|
466
401
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
end
|
|
402
|
+
```ruby
|
|
403
|
+
producer = Tina4::Producer.new(Tina4::Queue.new(topic: "emails"))
|
|
404
|
+
producer.produce({ to: "alice@example.com" })
|
|
471
405
|
|
|
472
|
-
Tina4.
|
|
473
|
-
|
|
474
|
-
response.json({ logged_out: true })
|
|
475
|
-
end
|
|
406
|
+
consumer = Tina4::Consumer.new(Tina4::Queue.new(topic: "emails"))
|
|
407
|
+
consumer.each { |msg| send_email(msg.data) }
|
|
476
408
|
```
|
|
477
409
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
## Middleware
|
|
410
|
+
### GraphQL
|
|
481
411
|
|
|
482
412
|
```ruby
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
413
|
+
gql = Tina4::GraphQL.new
|
|
414
|
+
gql.schema.from_orm(User)
|
|
415
|
+
gql.register_route("/graphql") # GET = GraphiQL IDE, POST = queries
|
|
416
|
+
```
|
|
487
417
|
|
|
488
|
-
|
|
489
|
-
Tina4.after do |request, response|
|
|
490
|
-
puts "Response: #{response.status}"
|
|
491
|
-
end
|
|
418
|
+
### WebSocket
|
|
492
419
|
|
|
493
|
-
|
|
494
|
-
Tina4.
|
|
495
|
-
# Only runs for paths starting with /api
|
|
496
|
-
end
|
|
420
|
+
```ruby
|
|
421
|
+
ws = Tina4::WebSocketManager.new
|
|
497
422
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
return false unless request.session["role"] == "admin" # halts request
|
|
423
|
+
ws.route "/ws/chat" do |connection, message|
|
|
424
|
+
ws.broadcast("/ws/chat", "User said: #{message}")
|
|
501
425
|
end
|
|
502
426
|
```
|
|
503
427
|
|
|
504
|
-
|
|
428
|
+
### Swagger / OpenAPI
|
|
505
429
|
|
|
506
|
-
Auto-generated
|
|
430
|
+
Auto-generated at `/swagger`:
|
|
507
431
|
|
|
508
432
|
```ruby
|
|
509
433
|
Tina4.get "/api/users", swagger_meta: {
|
|
510
|
-
summary: "
|
|
511
|
-
tags: ["
|
|
512
|
-
description: "Returns a paginated list of users"
|
|
434
|
+
summary: "Get all users",
|
|
435
|
+
tags: ["users"]
|
|
513
436
|
} do |request, response|
|
|
514
|
-
response.json(
|
|
437
|
+
response.json(User.all.map(&:to_hash))
|
|
515
438
|
end
|
|
516
439
|
```
|
|
517
440
|
|
|
518
|
-
|
|
441
|
+
### Event System
|
|
519
442
|
|
|
520
|
-
|
|
443
|
+
```ruby
|
|
444
|
+
Tina4.on("user.created", priority: 10) do |user|
|
|
445
|
+
send_notification("New user: #{user[:name]}")
|
|
446
|
+
end
|
|
521
447
|
|
|
522
|
-
|
|
448
|
+
Tina4.emit("user.created", { name: "Alice" })
|
|
449
|
+
```
|
|
523
450
|
|
|
524
|
-
###
|
|
451
|
+
### Template Engine (Frond)
|
|
525
452
|
|
|
526
|
-
|
|
527
|
-
|
|
453
|
+
Twig-compatible, 35+ filters, macros, inheritance, fragment caching, sandboxing:
|
|
454
|
+
|
|
455
|
+
```twig
|
|
456
|
+
{% extends "base.twig" %}
|
|
457
|
+
{% block content %}
|
|
458
|
+
<h1>{{ title | upper }}</h1>
|
|
459
|
+
{% for item in items %}
|
|
460
|
+
<p>{{ item.name }} -- {{ item.price | number_format(2) }}</p>
|
|
461
|
+
{% endfor %}
|
|
462
|
+
|
|
463
|
+
{% cache "sidebar" 300 %}
|
|
464
|
+
{% include "partials/sidebar.twig" %}
|
|
465
|
+
{% endcache %}
|
|
466
|
+
{% endblock %}
|
|
467
|
+
```
|
|
528
468
|
|
|
529
|
-
|
|
530
|
-
schema.add_query("hello", type: "String") { |_root, _args, _ctx| "Hello World!" }
|
|
469
|
+
### CRUD Scaffolding
|
|
531
470
|
|
|
532
|
-
|
|
533
|
-
|
|
471
|
+
```ruby
|
|
472
|
+
Tina4.get "/admin/users" do |request, response|
|
|
473
|
+
response.json(Tina4::CRUD.to_crud(request, {
|
|
474
|
+
sql: "SELECT id, name, email FROM users",
|
|
475
|
+
title: "User Management",
|
|
476
|
+
primary_key: "id"
|
|
477
|
+
}))
|
|
534
478
|
end
|
|
479
|
+
```
|
|
535
480
|
|
|
536
|
-
|
|
537
|
-
schema.add_mutation("createUser", type: "User",
|
|
538
|
-
args: { "name" => { type: "String!" }, "email" => { type: "String!" } }
|
|
539
|
-
) do |_root, args, _ctx|
|
|
540
|
-
User.create(name: args["name"], email: args["email"]).to_hash
|
|
541
|
-
end
|
|
481
|
+
### REST Client
|
|
542
482
|
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
483
|
+
```ruby
|
|
484
|
+
api = Tina4::API.new("https://api.example.com", headers: {
|
|
485
|
+
"Authorization" => "Bearer xyz"
|
|
486
|
+
})
|
|
487
|
+
result = api.get("/users/42")
|
|
546
488
|
```
|
|
547
489
|
|
|
548
|
-
###
|
|
549
|
-
|
|
550
|
-
Generate full CRUD queries and mutations from your ORM models with one line:
|
|
490
|
+
### Data Seeder
|
|
551
491
|
|
|
552
492
|
```ruby
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
493
|
+
fake = Tina4::FakeData.new
|
|
494
|
+
fake.name # "Alice Johnson"
|
|
495
|
+
fake.email # "alice.johnson@example.com"
|
|
556
496
|
|
|
557
|
-
|
|
558
|
-
gql.register_route("/graphql")
|
|
497
|
+
Tina4.seed_orm(User, count: 50)
|
|
559
498
|
```
|
|
560
499
|
|
|
561
|
-
|
|
562
|
-
- **Queries:** `user(id)` (single), `users(limit, offset)` (list with pagination)
|
|
563
|
-
- **Mutations:** `createUser(input)`, `updateUser(id, input)`, `deleteUser(id)`
|
|
564
|
-
|
|
565
|
-
### Query Examples
|
|
500
|
+
### Email / Messenger
|
|
566
501
|
|
|
567
|
-
```
|
|
568
|
-
|
|
569
|
-
|
|
502
|
+
```ruby
|
|
503
|
+
mail = Tina4::Messenger.new
|
|
504
|
+
mail.send(to: "user@test.com", subject: "Welcome", body: "<h1>Hi!</h1>", html: true)
|
|
505
|
+
```
|
|
570
506
|
|
|
571
|
-
|
|
572
|
-
{ user(id: 42) { id name email } }
|
|
507
|
+
### In-Memory Cache
|
|
573
508
|
|
|
574
|
-
|
|
575
|
-
|
|
509
|
+
```ruby
|
|
510
|
+
cache = Tina4::Cache.new
|
|
511
|
+
cache.set("key", "value", ttl: 300)
|
|
512
|
+
cache.tag("users").flush
|
|
513
|
+
```
|
|
576
514
|
|
|
577
|
-
|
|
578
|
-
{ admin: user(id: 1) { name } guest: user(id: 2) { name } }
|
|
515
|
+
### SCSS, Localization, Inline Testing
|
|
579
516
|
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
}
|
|
517
|
+
- **SCSS**: Drop `.scss` in `src/scss/` -- auto-compiled to CSS. Variables, nesting, mixins, `@import`, `@extend`.
|
|
518
|
+
- **i18n**: JSON translation files, 6 languages (en, fr, af, zh, ja, es), placeholder interpolation.
|
|
519
|
+
- **Inline tests**: `test_method :add, assert_equal: [[5, 3], 8]` on any method.
|
|
584
520
|
|
|
585
|
-
|
|
586
|
-
fragment UserFields on User { id name email }
|
|
587
|
-
{ user(id: 1) { ...UserFields } }
|
|
521
|
+
---
|
|
588
522
|
|
|
589
|
-
|
|
590
|
-
mutation {
|
|
591
|
-
createUser(name: "Alice", email: "alice@example.com") { id name }
|
|
592
|
-
}
|
|
593
|
-
```
|
|
523
|
+
## Dev Mode
|
|
594
524
|
|
|
595
|
-
|
|
525
|
+
Set `TINA4_DEBUG=true` in `.env` to enable:
|
|
596
526
|
|
|
597
|
-
|
|
598
|
-
|
|
527
|
+
- **Live reload** -- browser auto-refreshes on code changes
|
|
528
|
+
- **CSS hot-reload** -- SCSS changes apply without page refresh
|
|
529
|
+
- **Error overlay** -- rich error display in the browser
|
|
530
|
+
- **Dev admin** at `/__dev/` with tabs: Routes, Queue, Mailbox, Messages, Database, Requests, Errors, WebSocket, System, Tools, Tina4
|
|
599
531
|
|
|
600
|
-
|
|
601
|
-
result = gql.execute('{ hello }')
|
|
602
|
-
puts result["data"]["hello"] # "Hello World!"
|
|
532
|
+
---
|
|
603
533
|
|
|
604
|
-
|
|
605
|
-
result = gql.execute(
|
|
606
|
-
'query($id: ID!) { user(id: $id) { name } }',
|
|
607
|
-
variables: { "id" => 42 }
|
|
608
|
-
)
|
|
534
|
+
## CLI Reference
|
|
609
535
|
|
|
610
|
-
|
|
611
|
-
|
|
536
|
+
```bash
|
|
537
|
+
tina4ruby init [dir] # Scaffold a new project
|
|
538
|
+
tina4ruby serve [port] # Start dev server (default: 7147)
|
|
539
|
+
tina4ruby serve --production # Auto-install and use Puma production server
|
|
540
|
+
tina4ruby migrate # Run pending migrations
|
|
541
|
+
tina4ruby migrate --create <desc># Create a migration file
|
|
542
|
+
tina4ruby migrate --rollback # Rollback last batch
|
|
543
|
+
tina4ruby generate model <name> # Generate ORM model scaffold
|
|
544
|
+
tina4ruby generate route <name> # Generate route scaffold
|
|
545
|
+
tina4ruby generate migration <d> # Generate migration file
|
|
546
|
+
tina4ruby generate middleware <n># Generate middleware scaffold
|
|
547
|
+
tina4ruby seed # Run seeders from src/seeds/
|
|
548
|
+
tina4ruby routes # List all registered routes
|
|
549
|
+
tina4ruby test # Run test suite
|
|
550
|
+
tina4ruby build # Build distributable gem
|
|
551
|
+
tina4ruby ai [--all] # Detect AI tools and install context
|
|
612
552
|
```
|
|
613
553
|
|
|
614
|
-
|
|
554
|
+
### Production Server Auto-Detection
|
|
615
555
|
|
|
616
|
-
|
|
556
|
+
`tina4 serve` automatically detects and uses the best available production server:
|
|
617
557
|
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
"Authorization" => "Bearer sk-abc123"
|
|
621
|
-
})
|
|
622
|
-
|
|
623
|
-
# GET
|
|
624
|
-
response = api.get("/users", params: { page: 1 })
|
|
625
|
-
puts response.json # parsed response body
|
|
558
|
+
- **Ruby**: Puma (if installed), otherwise WEBrick -- Puma gives 2.8x improvement
|
|
559
|
+
- Use `tina4ruby serve --production` to auto-install Puma
|
|
626
560
|
|
|
627
|
-
|
|
628
|
-
response = api.post("/users", body: { name: "Alice" })
|
|
629
|
-
puts response.success? # true for 2xx status
|
|
630
|
-
puts response.status # 201
|
|
561
|
+
### Scaffolding with `tina4 generate`
|
|
631
562
|
|
|
632
|
-
|
|
633
|
-
api.put("/users/1", body: { name: "Updated" })
|
|
634
|
-
api.patch("/users/1", body: { name: "Patched" })
|
|
635
|
-
api.delete("/users/1")
|
|
563
|
+
Quickly scaffold new components:
|
|
636
564
|
|
|
637
|
-
|
|
638
|
-
|
|
565
|
+
```bash
|
|
566
|
+
tina4ruby generate model User # Creates src/orm/user.rb with field stubs
|
|
567
|
+
tina4ruby generate route users # Creates src/routes/users.rb with CRUD stubs
|
|
568
|
+
tina4ruby generate migration "add age" # Creates migration SQL file
|
|
569
|
+
tina4ruby generate middleware AuthLog # Creates middleware class
|
|
639
570
|
```
|
|
640
571
|
|
|
641
|
-
|
|
572
|
+
### ORM Relationships & Eager Loading
|
|
642
573
|
|
|
643
|
-
|
|
574
|
+
```ruby
|
|
575
|
+
# Relationships
|
|
576
|
+
orders = user.has_many("Order", "user_id")
|
|
577
|
+
profile = user.has_one("Profile", "user_id")
|
|
578
|
+
customer = order.belongs_to("Customer", "customer_id")
|
|
644
579
|
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
VERSION=1.0.0
|
|
648
|
-
SECRET=my-jwt-secret
|
|
649
|
-
API_KEY=your-api-key-here
|
|
650
|
-
DATABASE_URL=sqlite://app.db
|
|
651
|
-
TINA4_DEBUG_LEVEL=[TINA4_LOG_DEBUG]
|
|
652
|
-
ENVIRONMENT=development
|
|
580
|
+
# Eager loading with include:
|
|
581
|
+
users = User.all(include: ["orders", "profile"])
|
|
653
582
|
```
|
|
654
583
|
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
Supports environment-specific files: `.env.development`, `.env.production`, `.env.test`.
|
|
584
|
+
### DB Query Caching
|
|
658
585
|
|
|
659
|
-
|
|
586
|
+
Enable query caching for up to 4x speedup on read-heavy workloads:
|
|
660
587
|
|
|
661
588
|
```bash
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
tina4 start -p 3000 # Custom port
|
|
665
|
-
tina4 start -d # Dev mode with auto-reload
|
|
666
|
-
tina4 migrate # Run pending migrations
|
|
667
|
-
tina4 migrate --create "desc" # Create a migration
|
|
668
|
-
tina4 migrate --rollback 1 # Rollback migrations
|
|
669
|
-
tina4 test # Run inline tests
|
|
670
|
-
tina4 routes # List all registered routes
|
|
671
|
-
tina4 console # Interactive Ruby console
|
|
672
|
-
tina4 version # Show version
|
|
589
|
+
# .env
|
|
590
|
+
TINA4_DB_CACHE=true
|
|
673
591
|
```
|
|
674
592
|
|
|
675
|
-
|
|
593
|
+
### Frond Pre-Compilation
|
|
676
594
|
|
|
677
|
-
|
|
678
|
-
myapp/
|
|
679
|
-
├── app.rb # Entry point
|
|
680
|
-
├── .env # Environment config
|
|
681
|
-
├── Gemfile
|
|
682
|
-
├── migrations/ # SQL migrations
|
|
683
|
-
├── routes/ # Auto-discovered route files
|
|
684
|
-
├── templates/ # Twig/ERB templates
|
|
685
|
-
├── public/ # Static files (CSS, JS, images)
|
|
686
|
-
│ ├── css/
|
|
687
|
-
│ ├── js/
|
|
688
|
-
│ └── images/
|
|
689
|
-
├── src/ # Application code
|
|
690
|
-
└── logs/ # Log files
|
|
691
|
-
```
|
|
595
|
+
Templates are pre-compiled for 2.8x faster rendering.
|
|
692
596
|
|
|
693
|
-
|
|
597
|
+
### Gallery
|
|
694
598
|
|
|
695
|
-
|
|
696
|
-
# routes/users.rb
|
|
697
|
-
Tina4.get "/api/users" do |request, response|
|
|
698
|
-
response.json(User.all.map(&:to_hash))
|
|
699
|
-
end
|
|
700
|
-
```
|
|
701
|
-
|
|
702
|
-
## Auto-Discovery
|
|
599
|
+
7 interactive examples with **Try It** deploy.
|
|
703
600
|
|
|
704
|
-
|
|
705
|
-
- Route files from `routes/`, `src/routes/`, `src/api/`, `api/`
|
|
706
|
-
- `app.rb` and `index.rb` from the project root
|
|
601
|
+
## Environment
|
|
707
602
|
|
|
708
|
-
|
|
603
|
+
```bash
|
|
604
|
+
SECRET=your-jwt-secret
|
|
605
|
+
DATABASE_URL=sqlite://data/app.db
|
|
606
|
+
DATABASE_USERNAME=
|
|
607
|
+
DATABASE_PASSWORD=
|
|
608
|
+
TINA4_DEBUG=true # Enable dev toolbar, error overlay
|
|
609
|
+
TINA4_LOG_LEVEL=ALL # ALL, DEBUG, INFO, WARNING, ERROR
|
|
610
|
+
TINA4_LANGUAGE=en # en, fr, af, zh, ja, es
|
|
611
|
+
TINA4_SESSION_HANDLER=SessionFileHandler
|
|
612
|
+
SWAGGER_TITLE=My API
|
|
613
|
+
```
|
|
709
614
|
|
|
710
|
-
|
|
711
|
-
# app.rb
|
|
712
|
-
require "tina4"
|
|
615
|
+
## Carbonah Green Benchmarks
|
|
713
616
|
|
|
714
|
-
|
|
715
|
-
Tina4.database = Tina4::Database.new("sqlite://app.db")
|
|
617
|
+
All 9 benchmarks rated **A+** (South Africa grid, 1000 iterations each):
|
|
716
618
|
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
619
|
+
| Benchmark | SCI (gCO2eq) | Grade |
|
|
620
|
+
|-----------|-------------|-------|
|
|
621
|
+
| JSON Hello World | 0.000897 | A+ |
|
|
622
|
+
| Single DB Query | 0.000561 | A+ |
|
|
623
|
+
| Multiple DB Queries | 0.001402 | A+ |
|
|
624
|
+
| Template Rendering | 0.003351 | A+ |
|
|
625
|
+
| Large JSON Payload | 0.001019 | A+ |
|
|
626
|
+
| Plaintext Response | 0.000391 | A+ |
|
|
627
|
+
| CRUD Cycle | 0.000473 | A+ |
|
|
628
|
+
| Paginated Query | 0.001027 | A+ |
|
|
629
|
+
| Framework Startup | 0.00267 | A+ |
|
|
723
630
|
|
|
724
|
-
|
|
725
|
-
Tina4.get "/" do |request, response|
|
|
726
|
-
response.render("home.twig", { todos: Todo.all.map(&:to_hash) })
|
|
727
|
-
end
|
|
631
|
+
Startup: 37ms | Memory: 34.9MB | SCI: 0.00267
|
|
728
632
|
|
|
729
|
-
|
|
730
|
-
response.json(Todo.all.map(&:to_hash))
|
|
731
|
-
end
|
|
633
|
+
Run locally: `ruby benchmarks/run_carbonah.rb`
|
|
732
634
|
|
|
733
|
-
|
|
734
|
-
todo = Todo.create(title: request.json_body["title"])
|
|
735
|
-
response.json(todo.to_hash, 201)
|
|
736
|
-
end
|
|
635
|
+
---
|
|
737
636
|
|
|
738
|
-
|
|
739
|
-
todo = Todo.find(request.params["id"])
|
|
740
|
-
todo.done = request.json_body["done"]
|
|
741
|
-
todo.save
|
|
742
|
-
response.json(todo.to_hash)
|
|
743
|
-
end
|
|
637
|
+
## Documentation
|
|
744
638
|
|
|
745
|
-
|
|
746
|
-
Todo.find(request.params["id"]).delete
|
|
747
|
-
response.json({ deleted: true })
|
|
748
|
-
end
|
|
749
|
-
```
|
|
639
|
+
Full guides, API reference, and examples at **[tina4.com](https://tina4.com)**.
|
|
750
640
|
|
|
751
|
-
##
|
|
641
|
+
## License
|
|
752
642
|
|
|
753
|
-
-
|
|
754
|
-
|
|
643
|
+
MIT (c) 2007-2026 Tina4 Stack
|
|
644
|
+
https://opensource.org/licenses/MIT
|
|
755
645
|
|
|
756
|
-
|
|
646
|
+
---
|
|
757
647
|
|
|
758
|
-
|
|
648
|
+
<p align="center"><b>Tina4</b> -- The framework that keeps out of the way of your coding.</p>
|
|
759
649
|
|
|
760
650
|
---
|
|
761
651
|
|