tina4-nodejs 3.10.40 → 3.10.41
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.
- package/CLAUDE.md +4 -2
- package/README.md +59 -613
- package/package.json +2 -2
- package/packages/orm/src/baseModel.ts +17 -6
package/CLAUDE.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
# CLAUDE.md — AI Developer Guide for tina4-nodejs (v3.10.
|
|
1
|
+
# CLAUDE.md — AI Developer Guide for tina4-nodejs (v3.10.41)
|
|
2
2
|
|
|
3
3
|
> This file helps AI assistants (Claude, Copilot, Cursor, etc.) understand and work on this codebase effectively.
|
|
4
4
|
|
|
5
5
|
## What This Project Is
|
|
6
6
|
|
|
7
|
-
Tina4 for Node.js/TypeScript v3.10.
|
|
7
|
+
Tina4 for Node.js/TypeScript v3.10.41 — a convention-over-configuration structural paradigm. **Not a framework.** The developer writes TypeScript; Tina4 is invisible infrastructure.
|
|
8
8
|
|
|
9
9
|
The philosophy: zero ceremony, batteries included, file system as source of truth.
|
|
10
10
|
|
|
@@ -153,6 +153,8 @@ Database layer with auto-CRUD generation, seeding, fake data, and SQL translatio
|
|
|
153
153
|
- `fakeData.ts` — ORM-aware fake data extending core (adds `forField()` with column-name heuristics)
|
|
154
154
|
- `seeder.ts` — Database seeding (`seedTable` for raw SQL, `seedOrm` for model-based)
|
|
155
155
|
- `sqlTranslation.ts` — Cross-engine SQL translator (`SQLTranslator`) and TTL query cache (`QueryCache`)
|
|
156
|
+
- `BaseModel.select<T>(sql, params?)` — Raw SQL query returning `T[]`
|
|
157
|
+
- `BaseModel.selectOne<T>(sql, params?, include?)` — Raw SQL query returning `T | null` (first match or null, with optional eager loading)
|
|
156
158
|
- QueryBuilder supports `toMongo()` for generating MongoDB query documents from the same fluent API
|
|
157
159
|
- `getNextId(table: string, pkColumn?: string, generatorName?: string): Promise<number>` — Race-safe ID generation using atomic sequence table (`tina4_sequences`). SQLite/MySQL/MSSQL use `tina4_sequences` with atomic UPDATE+SELECT. PostgreSQL auto-creates sequences if missing. Firebird uses existing generators (unchanged).
|
|
158
160
|
|
package/README.md
CHANGED
|
@@ -1,665 +1,115 @@
|
|
|
1
1
|
<p align="center">
|
|
2
2
|
<img src="https://tina4.com/logo.svg" alt="Tina4" width="200">
|
|
3
3
|
</p>
|
|
4
|
-
|
|
5
4
|
<h1 align="center">Tina4 Node.js</h1>
|
|
6
5
|
<h3 align="center">This Is Now A 4Framework</h3>
|
|
7
|
-
|
|
8
|
-
<p align="center">
|
|
9
|
-
Laravel joy. TypeScript speed. 10x less code. Zero third-party dependencies.
|
|
10
|
-
</p>
|
|
11
|
-
|
|
6
|
+
<p align="center">54 built-in features. Zero dependencies. One import, everything works.</p>
|
|
12
7
|
<p align="center">
|
|
13
8
|
<a href="https://www.npmjs.com/package/tina4-nodejs"><img src="https://img.shields.io/npm/v/tina4-nodejs?color=7b1fa2&label=npm" alt="npm"></a>
|
|
14
|
-
<img src="https://img.shields.io/badge/tests-1%
|
|
15
|
-
<img src="https://img.shields.io/badge/features-
|
|
9
|
+
<img src="https://img.shields.io/badge/tests-1%2C950%20passing-brightgreen" alt="Tests">
|
|
10
|
+
<img src="https://img.shields.io/badge/features-54-blue" alt="Features">
|
|
16
11
|
<img src="https://img.shields.io/badge/dependencies-0-brightgreen" alt="Zero Deps">
|
|
17
12
|
<a href="https://tina4.com"><img src="https://img.shields.io/badge/docs-tina4.com-7b1fa2" alt="Docs"></a>
|
|
18
13
|
</p>
|
|
19
14
|
|
|
20
|
-
<p align="center">
|
|
21
|
-
<a href="https://tina4.com">Documentation</a> •
|
|
22
|
-
<a href="#getting-started">Getting Started</a> •
|
|
23
|
-
<a href="#features">Features</a> •
|
|
24
|
-
<a href="#cli-reference">CLI Reference</a> •
|
|
25
|
-
<a href="https://tina4.com">tina4.com</a>
|
|
26
|
-
</p>
|
|
27
|
-
|
|
28
15
|
---
|
|
29
16
|
|
|
30
17
|
## Quick Start
|
|
31
18
|
|
|
32
19
|
```bash
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
# Create a project
|
|
37
|
-
tina4 init nodejs ./my-app
|
|
38
|
-
|
|
39
|
-
# Run it
|
|
40
|
-
cd my-app && tina4 serve
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
Open http://localhost:7148 — your app is running.
|
|
44
|
-
|
|
45
|
-
<details>
|
|
46
|
-
<summary><strong>Without the Tina4 CLI</strong></summary>
|
|
47
|
-
|
|
48
|
-
```bash
|
|
49
|
-
# 1. Create project
|
|
50
|
-
mkdir my-app && cd my-app
|
|
51
|
-
npm init -y
|
|
52
|
-
npm install tina4-nodejs
|
|
53
|
-
|
|
54
|
-
# 2. Create entry point
|
|
55
|
-
cat > app.ts << 'EOF'
|
|
56
|
-
import { startServer } from "tina4-nodejs";
|
|
57
|
-
startServer({ port: 7148, host: "0.0.0.0" });
|
|
58
|
-
EOF
|
|
59
|
-
|
|
60
|
-
# 3. Create .env
|
|
61
|
-
echo 'TINA4_DEBUG=true' > .env
|
|
62
|
-
echo 'TINA4_LOG_LEVEL=ALL' >> .env
|
|
63
|
-
|
|
64
|
-
# 4. Create route directory
|
|
65
|
-
mkdir -p src/routes
|
|
66
|
-
|
|
67
|
-
# 5. Run
|
|
68
|
-
npx tsx app.ts
|
|
20
|
+
npx tina4nodejs init my-app
|
|
21
|
+
cd my-app && npx tina4nodejs serve
|
|
69
22
|
```
|
|
70
23
|
|
|
71
24
|
Open http://localhost:7148
|
|
72
25
|
|
|
73
|
-
</details>
|
|
74
|
-
|
|
75
26
|
---
|
|
76
27
|
|
|
77
|
-
##
|
|
78
|
-
|
|
79
|
-
Every feature is built from scratch -- no npm install, no node_modules bloat, no third-party runtime dependencies in core.
|
|
80
|
-
|
|
81
|
-
| Category | Features |
|
|
82
|
-
|----------|----------|
|
|
83
|
-
| **HTTP** | Native `node:http` server, file-based + programmatic routing, path params (`{id}`, `[...slug]`), middleware pipeline, CORS, rate limiting, graceful shutdown |
|
|
84
|
-
| **Templates** | Frond engine (Twig-compatible), inheritance, partials, 53+ filters, macros, fragment caching, sandboxing |
|
|
85
|
-
| **ORM** | Active Record, typed fields with validation, soft delete, relationships (`hasOne`/`hasMany`/`belongsTo`), scopes, result caching, auto-CRUD |
|
|
86
|
-
| **Database** | SQLite, PostgreSQL, MySQL, MSSQL/SQL Server, Firebird -- unified adapter interface, query caching (TINA4_DB_CACHE=true for 4x speedup) |
|
|
87
|
-
| **Auth** | Zero-dep JWT (HS256/RS256), sessions (file/Redis/Valkey/MongoDB/database), password hashing, form tokens |
|
|
88
|
-
| **API** | Swagger/OpenAPI auto-generation, GraphQL with ORM auto-schema and GraphiQL IDE, WSDL/SOAP with auto WSDL |
|
|
89
|
-
| **Background** | Queue (SQLite/RabbitMQ/Kafka/MongoDB) with priority, delayed jobs, retry, batch processing |
|
|
90
|
-
| **Real-time** | Native WebSocket (RFC 6455), per-path routing, connection manager, broadcast |
|
|
91
|
-
| **Frontend** | tina4-css (~24 KB), frond.js helper, SCSS compiler, live reload, CSS hot-reload |
|
|
92
|
-
| **DX** | Dev admin dashboard, error overlay, request inspector, hot-reload, Carbonah green benchmarks |
|
|
93
|
-
| **Data** | Migrations with rollback, 26+ fake data generators, ORM and table seeders |
|
|
94
|
-
| **Other** | Service runner, localization (i18n), cache (memory/Redis/file), messenger (.env driven), HTTP constants, health check, configurable error pages |
|
|
95
|
-
|
|
96
|
-
**1,812 tests across 38 built-in features. Zero dependencies. All Carbonah benchmarks rated A+.**
|
|
97
|
-
|
|
98
|
-
For full documentation visit **[tina4.com](https://tina4.com)**.
|
|
99
|
-
|
|
100
|
-
---
|
|
101
|
-
|
|
102
|
-
## Install
|
|
103
|
-
|
|
104
|
-
```bash
|
|
105
|
-
npm install tina4-nodejs
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
Or scaffold a new project directly:
|
|
109
|
-
|
|
110
|
-
```bash
|
|
111
|
-
npx tina4nodejs init my-app
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
---
|
|
115
|
-
|
|
116
|
-
## Getting Started
|
|
117
|
-
|
|
118
|
-
### 1. Create a project
|
|
119
|
-
|
|
120
|
-
```bash
|
|
121
|
-
npx tina4nodejs init my-app
|
|
122
|
-
cd my-app
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
This creates:
|
|
126
|
-
|
|
127
|
-
```
|
|
128
|
-
my-app/
|
|
129
|
-
├── package.json # Entry point
|
|
130
|
-
├── tsconfig.json # TypeScript config
|
|
131
|
-
├── .env # Configuration
|
|
132
|
-
├── src/
|
|
133
|
-
│ ├── routes/ # API + page routes (auto-discovered)
|
|
134
|
-
│ ├── models/ # Database models (auto-CRUD)
|
|
135
|
-
│ ├── templates/ # Frond/Twig templates
|
|
136
|
-
│ ├── seeds/ # Database seeders
|
|
137
|
-
│ ├── scss/ # SCSS (auto-compiled to public/css/)
|
|
138
|
-
│ └── public/ # Static assets served at /
|
|
139
|
-
├── migrations/ # SQL migration files
|
|
140
|
-
├── data/ # SQLite database (auto-created)
|
|
141
|
-
└── test/ # Tests
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
### 2. Create a route
|
|
145
|
-
|
|
146
|
-
**File-based routing** -- the directory path becomes the URL:
|
|
147
|
-
|
|
148
|
-
```
|
|
149
|
-
src/routes/api/hello/get.ts -> GET /api/hello
|
|
150
|
-
```
|
|
28
|
+
## Code Examples
|
|
151
29
|
|
|
152
30
|
```typescript
|
|
153
31
|
// src/routes/api/hello/get.ts
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
export default async function (request: Tina4Request, response: Tina4Response) {
|
|
157
|
-
response({message: "Hello from Tina4!"}, HTTP_OK);
|
|
32
|
+
export default async function(req: Tina4Request, res: Tina4Response) {
|
|
33
|
+
res.json({ message: "Hello from Tina4!" });
|
|
158
34
|
}
|
|
159
|
-
```
|
|
160
|
-
|
|
161
|
-
**Programmatic routing** -- decorator-style in a single file:
|
|
162
|
-
|
|
163
|
-
```typescript
|
|
164
|
-
// src/routes/hello.ts
|
|
165
|
-
import { get } from "tina4-nodejs";
|
|
166
|
-
|
|
167
|
-
get("/api/hello/{name}", async (request, response) => {
|
|
168
|
-
response({message: `Hello, ${request.params.name}!`}, HTTP_OK);
|
|
169
|
-
});
|
|
170
|
-
```
|
|
171
|
-
|
|
172
|
-
Visit `http://localhost:7148/api/hello` -- routes are auto-discovered, no imports needed.
|
|
173
|
-
|
|
174
|
-
### 3. Add a database
|
|
175
|
-
|
|
176
|
-
Edit `.env`:
|
|
177
|
-
|
|
178
|
-
```bash
|
|
179
|
-
DATABASE_URL=sqlite:///data/app.db
|
|
180
|
-
```
|
|
181
|
-
|
|
182
|
-
Create and run a migration:
|
|
183
|
-
|
|
184
|
-
```bash
|
|
185
|
-
npx tina4nodejs migrate:create "create users table"
|
|
186
|
-
```
|
|
187
|
-
|
|
188
|
-
Edit the generated SQL:
|
|
189
|
-
|
|
190
|
-
```sql
|
|
191
|
-
CREATE TABLE IF NOT EXISTS users (
|
|
192
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
193
|
-
name TEXT NOT NULL,
|
|
194
|
-
email TEXT NOT NULL,
|
|
195
|
-
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
196
|
-
);
|
|
197
|
-
```
|
|
198
|
-
|
|
199
|
-
```bash
|
|
200
|
-
npx tina4nodejs migrate
|
|
201
|
-
```
|
|
202
|
-
|
|
203
|
-
### 4. Create an ORM model
|
|
204
|
-
|
|
205
|
-
Create `src/models/User.ts`:
|
|
206
|
-
|
|
207
|
-
```typescript
|
|
208
|
-
export default class User {
|
|
209
|
-
static tableName = "users";
|
|
210
|
-
|
|
211
|
-
static fields = {
|
|
212
|
-
id: { type: "integer" as const, primaryKey: true, autoIncrement: true },
|
|
213
|
-
name: { type: "string" as const, required: true, maxLength: 100 },
|
|
214
|
-
email: { type: "string" as const, required: true, pattern: "^[^@]+@[^@]+\\.[^@]+$" },
|
|
215
|
-
createdAt: { type: "datetime" as const, default: "now" },
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
|
-
```
|
|
219
|
-
|
|
220
|
-
### 5. Build a REST API
|
|
221
|
-
|
|
222
|
-
**File-based** -- create `src/routes/api/users/get.ts`:
|
|
223
|
-
|
|
224
|
-
```typescript
|
|
225
|
-
import type { Tina4Request, Tina4Response } from "tina4-nodejs";
|
|
226
|
-
import { getAdapter } from "tina4-nodejs";
|
|
227
|
-
|
|
228
|
-
export default async function (request: Tina4Request, response: Tina4Response) {
|
|
229
|
-
const db = getAdapter();
|
|
230
|
-
const users = db.query("SELECT * FROM users LIMIT 100");
|
|
231
|
-
response(users, HTTP_OK);
|
|
232
|
-
}
|
|
233
|
-
```
|
|
234
|
-
|
|
235
|
-
Create `src/routes/api/users/[id]/get.ts`:
|
|
236
|
-
|
|
237
|
-
```typescript
|
|
238
|
-
export default async function (request: Tina4Request, response: Tina4Response) {
|
|
239
|
-
const db = getAdapter();
|
|
240
|
-
const user = db.query("SELECT * FROM users WHERE id = ?", [request.params.id]);
|
|
241
|
-
if (user.length) {
|
|
242
|
-
response(user[0], HTTP_OK);
|
|
243
|
-
} else {
|
|
244
|
-
response({error: "Not found"}, HTTP_NOT_FOUND);
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
```
|
|
248
|
-
|
|
249
|
-
Create `src/routes/api/users/post.ts`:
|
|
250
|
-
|
|
251
|
-
```typescript
|
|
252
|
-
export default async function (request: Tina4Request, response: Tina4Response) {
|
|
253
|
-
const db = getAdapter();
|
|
254
|
-
db.execute("INSERT INTO users (name, email) VALUES (?, ?)",
|
|
255
|
-
[request.body.name, request.body.email]);
|
|
256
|
-
response({success: true}, HTTP_CREATED);
|
|
257
|
-
}
|
|
258
|
-
```
|
|
259
|
-
|
|
260
|
-
> **Auto-CRUD alternative**: Simply define the model in `src/models/User.ts` and Tina4 auto-generates all CRUD endpoints. File routes override auto-CRUD when both exist.
|
|
261
|
-
|
|
262
|
-
### 6. Add a template
|
|
263
|
-
|
|
264
|
-
Create `src/templates/base.twig`:
|
|
265
|
-
|
|
266
|
-
```twig
|
|
267
|
-
<!DOCTYPE html>
|
|
268
|
-
<html>
|
|
269
|
-
<head>
|
|
270
|
-
<title>{% block title %}My App{% endblock %}</title>
|
|
271
|
-
<link rel="stylesheet" href="/css/tina4.min.css">
|
|
272
|
-
{% block stylesheets %}{% endblock %}
|
|
273
|
-
</head>
|
|
274
|
-
<body>
|
|
275
|
-
{% block content %}{% endblock %}
|
|
276
|
-
<script src="/js/frond.js"></script>
|
|
277
|
-
{% block javascripts %}{% endblock %}
|
|
278
|
-
</body>
|
|
279
|
-
</html>
|
|
280
|
-
```
|
|
281
|
-
|
|
282
|
-
Create `src/templates/pages/home.twig`:
|
|
283
|
-
|
|
284
|
-
```twig
|
|
285
|
-
{% extends "base.twig" %}
|
|
286
|
-
{% block content %}
|
|
287
|
-
<div class="container mt-4">
|
|
288
|
-
<h1>{{ title }}</h1>
|
|
289
|
-
<ul>
|
|
290
|
-
{% for user in users %}
|
|
291
|
-
<li>{{ user.name }} -- {{ user.email }}</li>
|
|
292
|
-
{% endfor %}
|
|
293
|
-
</ul>
|
|
294
|
-
</div>
|
|
295
|
-
{% endblock %}
|
|
296
|
-
```
|
|
297
|
-
|
|
298
|
-
Render it from a route:
|
|
299
|
-
|
|
300
|
-
```typescript
|
|
301
|
-
// src/routes/page/home/get.ts
|
|
302
|
-
export default async function (request: Tina4Request, response: Tina4Response) {
|
|
303
|
-
const db = getAdapter();
|
|
304
|
-
const users = db.query("SELECT * FROM users LIMIT 20");
|
|
305
|
-
await response.render("pages/home.twig", {title: "Users", users});
|
|
306
|
-
}
|
|
307
|
-
```
|
|
308
|
-
|
|
309
|
-
### 7. Seed, test, deploy
|
|
310
|
-
|
|
311
|
-
```bash
|
|
312
|
-
npx tina4nodejs seed # Run seeders from src/seeds/
|
|
313
|
-
npx tina4nodejs test # Run test suite
|
|
314
|
-
npx tina4nodejs build # Build distributable
|
|
315
|
-
```
|
|
316
|
-
|
|
317
|
-
For the complete step-by-step guide, visit **[tina4.com](https://tina4.com)**.
|
|
318
|
-
|
|
319
|
-
---
|
|
320
|
-
|
|
321
|
-
## Features
|
|
322
|
-
|
|
323
|
-
### Routing
|
|
324
|
-
|
|
325
|
-
Tina4 supports both **file-based** and **programmatic** routing:
|
|
326
35
|
|
|
327
|
-
```typescript
|
|
328
|
-
// File-based: src/routes/api/items/get.ts
|
|
329
|
-
export default async function (request: Tina4Request, response: Tina4Response) {
|
|
330
|
-
response({items: []}, HTTP_OK);
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
// Programmatic: src/routes/webhooks.ts
|
|
334
|
-
import { get, post } from "tina4-nodejs";
|
|
335
|
-
|
|
336
|
-
get("/api/items", async (request, response) => {
|
|
337
|
-
response({items: []}, HTTP_OK);
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
// POST routes require auth by default; GET routes are public by default.
|
|
341
|
-
// Use JSDoc @noauth / @secured annotations to override:
|
|
342
|
-
|
|
343
|
-
/** @noauth */
|
|
344
|
-
post("/api/webhook", async (request, response) => {
|
|
345
|
-
response({ok: true}, HTTP_OK);
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
/** @secured */
|
|
349
|
-
get("/api/admin/stats", async (request, response) => {
|
|
350
|
-
response({secret: true}, HTTP_OK);
|
|
351
|
-
});
|
|
352
|
-
```
|
|
353
|
-
|
|
354
|
-
Path parameter types: `{id}` (string), `[id]` (file-based), `[...slug]` (catch-all).
|
|
355
|
-
|
|
356
|
-
### ORM
|
|
357
|
-
|
|
358
|
-
Active Record with typed fields, validation, soft delete, relationships, and auto-CRUD:
|
|
359
|
-
|
|
360
|
-
```typescript
|
|
361
36
|
// src/models/User.ts
|
|
362
37
|
export default class User {
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
role: { type: "string" as const, default: "user" },
|
|
370
|
-
age: { type: "integer" as const, min: 0, max: 150 },
|
|
371
|
-
};
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
// Auto-generates: GET/POST /api/users, GET/PUT/DELETE /api/users/{id}
|
|
375
|
-
// With filtering, sorting, pagination, and validation built in
|
|
376
|
-
```
|
|
377
|
-
|
|
378
|
-
### Database
|
|
379
|
-
|
|
380
|
-
Unified interface across multiple engines:
|
|
381
|
-
|
|
382
|
-
```typescript
|
|
383
|
-
import { getAdapter, initDatabase } from "tina4-nodejs";
|
|
384
|
-
|
|
385
|
-
const db = initDatabase("sqlite:///data/app.db");
|
|
386
|
-
|
|
387
|
-
const result = db.query("SELECT * FROM users WHERE age > ?", [18]);
|
|
388
|
-
const row = db.query("SELECT * FROM users WHERE id = ?", [1]);
|
|
389
|
-
db.execute("INSERT INTO users (name, email) VALUES (?, ?)", ["Alice", "alice@test.com"]);
|
|
390
|
-
```
|
|
391
|
-
|
|
392
|
-
### Middleware
|
|
393
|
-
|
|
394
|
-
```typescript
|
|
395
|
-
import { get } from "tina4-nodejs";
|
|
396
|
-
|
|
397
|
-
const authCheck = async (request: Tina4Request, response: Tina4Response, next: Function) => {
|
|
398
|
-
if (!request.headers.authorization) {
|
|
399
|
-
return response({error: "Unauthorized"}, HTTP_UNAUTHORIZED);
|
|
400
|
-
}
|
|
401
|
-
return next();
|
|
402
|
-
};
|
|
403
|
-
|
|
404
|
-
get("/protected", async (request, response) => {
|
|
405
|
-
response({secret: true}, HTTP_OK);
|
|
406
|
-
}, [authCheck]);
|
|
407
|
-
```
|
|
408
|
-
|
|
409
|
-
### JWT Authentication
|
|
410
|
-
|
|
411
|
-
```typescript
|
|
412
|
-
import { getToken, validToken } from "tina4-nodejs";
|
|
413
|
-
|
|
414
|
-
const token = getToken({userId: 42}, "your-secret");
|
|
415
|
-
const payload = validToken(token, "your-secret");
|
|
416
|
-
```
|
|
417
|
-
|
|
418
|
-
POST/PUT/PATCH/DELETE routes require `Authorization: Bearer <token>` by default. Use `noauth()` to make public, `secured()` to protect GET routes.
|
|
419
|
-
|
|
420
|
-
### Sessions
|
|
421
|
-
|
|
422
|
-
```typescript
|
|
423
|
-
request.session.set("userId", 42);
|
|
424
|
-
const userId = request.session.get("userId");
|
|
425
|
-
```
|
|
426
|
-
|
|
427
|
-
Backend: file (default). Set via `TINA4_SESSION_HANDLER` in `.env`.
|
|
428
|
-
|
|
429
|
-
### Queues
|
|
430
|
-
|
|
431
|
-
```typescript
|
|
432
|
-
import { Queue } from "tina4-nodejs";
|
|
433
|
-
|
|
434
|
-
const queue = new Queue({ topic: "emails" });
|
|
435
|
-
queue.push({ to: "alice@example.com" });
|
|
436
|
-
|
|
437
|
-
const job = queue.pop();
|
|
438
|
-
if (job) {
|
|
439
|
-
sendEmail(job.data);
|
|
440
|
-
job.complete();
|
|
441
|
-
}
|
|
442
|
-
```
|
|
443
|
-
|
|
444
|
-
### GraphQL
|
|
445
|
-
|
|
446
|
-
```typescript
|
|
447
|
-
import { GraphQL } from "tina4-nodejs";
|
|
448
|
-
|
|
449
|
-
const gql = new GraphQL();
|
|
450
|
-
gql.schema.fromModels();
|
|
451
|
-
gql.registerRoute("/graphql"); // GET = GraphiQL IDE, POST = queries
|
|
452
|
-
```
|
|
453
|
-
|
|
454
|
-
### WebSocket
|
|
455
|
-
|
|
456
|
-
```typescript
|
|
457
|
-
import { WebSocketServer } from "tina4-nodejs";
|
|
458
|
-
|
|
459
|
-
const wss = new WebSocketServer({ port: 8080 });
|
|
460
|
-
|
|
461
|
-
wss.on("connection", (client) => {
|
|
462
|
-
client.on("message", (msg) => {
|
|
463
|
-
wss.broadcast(`User said: ${msg}`);
|
|
464
|
-
});
|
|
465
|
-
});
|
|
466
|
-
```
|
|
467
|
-
|
|
468
|
-
### Swagger / OpenAPI
|
|
469
|
-
|
|
470
|
-
Auto-generated at `/swagger`:
|
|
471
|
-
|
|
472
|
-
```typescript
|
|
473
|
-
// src/routes/api/users/get.ts
|
|
474
|
-
export const meta = {
|
|
475
|
-
summary: "Get all users",
|
|
476
|
-
tags: ["Users"],
|
|
477
|
-
};
|
|
478
|
-
|
|
479
|
-
export default async function (request: Tina4Request, response: Tina4Response) {
|
|
480
|
-
const db = getAdapter();
|
|
481
|
-
response(db.query("SELECT * FROM users"), HTTP_OK);
|
|
38
|
+
static tableName = "users";
|
|
39
|
+
static fields = {
|
|
40
|
+
id: { type: "integer" as const, primaryKey: true, autoIncrement: true },
|
|
41
|
+
name: { type: "string" as const, required: true },
|
|
42
|
+
email: { type: "string" as const },
|
|
43
|
+
};
|
|
482
44
|
}
|
|
483
45
|
```
|
|
484
46
|
|
|
485
|
-
### Service Runner
|
|
486
|
-
|
|
487
|
-
```typescript
|
|
488
|
-
import { ServiceRunner } from "tina4-nodejs";
|
|
489
|
-
|
|
490
|
-
const runner = new ServiceRunner();
|
|
491
|
-
|
|
492
|
-
runner.register("cleanup", "0 */6 * * *", async () => {
|
|
493
|
-
// Runs every 6 hours
|
|
494
|
-
await cleanupExpiredSessions();
|
|
495
|
-
});
|
|
496
|
-
```
|
|
497
|
-
|
|
498
|
-
### Template Engine (Frond)
|
|
499
|
-
|
|
500
|
-
Twig-compatible, 53+ filters, macros, inheritance, fragment caching, sandboxing:
|
|
501
|
-
|
|
502
|
-
```twig
|
|
503
|
-
{% extends "base.twig" %}
|
|
504
|
-
{% block content %}
|
|
505
|
-
<h1>{{ title | upper }}</h1>
|
|
506
|
-
{% for item in items %}
|
|
507
|
-
<p>{{ item.name }} -- {{ item.price | number_format(2) }}</p>
|
|
508
|
-
{% endfor %}
|
|
509
|
-
|
|
510
|
-
{% cache "sidebar" 300 %}
|
|
511
|
-
{% include "partials/sidebar.twig" %}
|
|
512
|
-
{% endcache %}
|
|
513
|
-
{% endblock %}
|
|
514
|
-
```
|
|
515
|
-
|
|
516
|
-
### REST Client
|
|
517
|
-
|
|
518
|
-
```typescript
|
|
519
|
-
import { Api } from "tina4-nodejs";
|
|
520
|
-
|
|
521
|
-
const api = new Api("https://api.example.com", {authHeader: "Bearer xyz"});
|
|
522
|
-
const result = await api.sendRequest("/users/42");
|
|
523
|
-
```
|
|
524
|
-
|
|
525
|
-
### Data Seeder
|
|
526
|
-
|
|
527
|
-
```typescript
|
|
528
|
-
import { FakeData } from "tina4-nodejs";
|
|
529
|
-
|
|
530
|
-
const fake = new FakeData();
|
|
531
|
-
fake.name(); // "Alice Johnson"
|
|
532
|
-
fake.email(); // "alice.johnson@example.com"
|
|
533
|
-
|
|
534
|
-
// Seed via ORM package:
|
|
535
|
-
// import { seedOrm } from "@tina4/orm";
|
|
536
|
-
// await seedOrm(User, 50);
|
|
537
|
-
```
|
|
538
|
-
|
|
539
|
-
### Response Cache
|
|
540
|
-
|
|
541
|
-
```typescript
|
|
542
|
-
import { get, responseCache } from "tina4-nodejs";
|
|
543
|
-
|
|
544
|
-
// Apply response cache as middleware with 60-second TTL
|
|
545
|
-
get("/api/stats", async (request, response) => {
|
|
546
|
-
response(computeExpensiveStats(), HTTP_OK);
|
|
547
|
-
}, [responseCache({ ttl: 60 })]);
|
|
548
|
-
```
|
|
549
|
-
|
|
550
|
-
### SCSS, Localization
|
|
551
|
-
|
|
552
|
-
- **SCSS**: Drop `.scss` in `src/scss/` -- auto-compiled to CSS. Variables, nesting, mixins, `@import`, `@extend`.
|
|
553
|
-
- **i18n**: JSON translation files, parameter substitution.
|
|
554
|
-
|
|
555
47
|
---
|
|
556
48
|
|
|
557
|
-
##
|
|
558
|
-
|
|
559
|
-
Set `TINA4_DEBUG=true` in `.env` to enable:
|
|
49
|
+
## What's Included
|
|
560
50
|
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
51
|
+
| Category | Features |
|
|
52
|
+
|----------|----------|
|
|
53
|
+
| **Core HTTP** (7) | Router with path params (`{id:int}`, `{p:path}`), Server, Request/Response, Middleware pipeline, Static file serving, CORS |
|
|
54
|
+
| **Database** (6) | SQLite, PostgreSQL, MySQL, MSSQL, Firebird — unified adapter, connection pooling, query cache, transactions, race-safe ID generation, SQL dialect translation |
|
|
55
|
+
| **ORM** (7) | Active Record with typed fields, relationships (`has_one`/`has_many`/`belongs_to`), soft delete, QueryBuilder + MongoDB support, Auto-CRUD generator, migrations with rollback |
|
|
56
|
+
| **Auth & Security** (5) | JWT (HS256/RS256), password hashing (PBKDF2-SHA256), API key validation, rate limiting, CSRF form tokens |
|
|
57
|
+
| **Templating** (3) | Frond engine (Twig/Jinja2-compatible, pre-compiled 2.8× faster), SCSS auto-compilation, built-in CSS (~24 KB) |
|
|
58
|
+
| **API & Integration** (5) | HTTP client (zero-dep), GraphQL with ORM auto-schema + GraphiQL IDE, WSDL/SOAP with auto WSDL, WebSocket (RFC 6455) + Redis backplane, MCP server (24 dev tools) |
|
|
59
|
+
| **Background** (3) | Job queue (File/RabbitMQ/Kafka/MongoDB) with priority, delay, retry, dead letters — service runner — event system (on/emit/once/off) |
|
|
60
|
+
| **Data & Storage** (4) | Session (File/Redis/Valkey/MongoDB/DB), response cache (LRU, TTL), seeder + 50+ fake data generators, messenger (SMTP/IMAP) |
|
|
61
|
+
| **Developer Tools** (7) | Dev dashboard (11 tabs), dev toolbar, error overlay (Catppuccin Mocha), dev mailbox, hot reload + CSS hot-reload, code metrics (complexity, coupling, maintainability), AI context installer (7 tools) |
|
|
62
|
+
| **Utilities** (7) | DI container (transient + singleton), HtmlElement builder, inline testing (`@tests` decorator), i18n (6 languages), Swagger/OpenAPI auto-generation, CLI scaffolding (`generate model/route/migration/middleware`), structured logging |
|
|
63
|
+
|
|
64
|
+
**1,950 tests. Zero dependencies. Full parity across Python, PHP, Ruby, and Node.js.**
|
|
565
65
|
|
|
566
66
|
---
|
|
567
67
|
|
|
568
68
|
## CLI Reference
|
|
569
69
|
|
|
570
70
|
```bash
|
|
571
|
-
npx tina4nodejs init [dir]
|
|
572
|
-
npx tina4nodejs serve [--port
|
|
573
|
-
npx tina4nodejs
|
|
574
|
-
npx tina4nodejs
|
|
575
|
-
npx tina4nodejs
|
|
576
|
-
npx tina4nodejs
|
|
577
|
-
npx tina4nodejs generate model <name> # Generate model scaffold
|
|
578
|
-
npx tina4nodejs generate route <name> # Generate route scaffold
|
|
579
|
-
npx tina4nodejs generate migration <d> # Generate migration file
|
|
580
|
-
npx tina4nodejs generate middleware <n># Generate middleware scaffold
|
|
581
|
-
npx tina4nodejs seed # Run seeders from src/seeds/
|
|
582
|
-
npx tina4nodejs routes # List all registered routes
|
|
583
|
-
npx tina4nodejs test # Run test suite
|
|
584
|
-
npx tina4nodejs build # Build distributable package
|
|
585
|
-
npx tina4nodejs ai [--all] # Detect AI tools and install context
|
|
586
|
-
```
|
|
587
|
-
|
|
588
|
-
### Production Server Auto-Detection
|
|
589
|
-
|
|
590
|
-
`tina4 serve` automatically detects and uses the best available production server:
|
|
591
|
-
|
|
592
|
-
- **Node.js**: cluster mode with multiple workers, otherwise single http server
|
|
593
|
-
- Use `npx tina4nodejs serve --production` to auto-use cluster mode
|
|
594
|
-
|
|
595
|
-
### Scaffolding with `tina4 generate`
|
|
596
|
-
|
|
597
|
-
Quickly scaffold new components:
|
|
598
|
-
|
|
599
|
-
```bash
|
|
600
|
-
npx tina4nodejs generate model User # Creates src/models/User.ts
|
|
601
|
-
npx tina4nodejs generate route users # Creates src/routes/api/users/
|
|
602
|
-
npx tina4nodejs generate migration "add age" # Creates migration SQL file
|
|
603
|
-
npx tina4nodejs generate middleware AuthLog # Creates middleware
|
|
604
|
-
```
|
|
605
|
-
|
|
606
|
-
### ORM Relationships & Eager Loading
|
|
607
|
-
|
|
608
|
-
```typescript
|
|
609
|
-
// Relationships defined in model
|
|
610
|
-
static relationships = {
|
|
611
|
-
orders: { type: "hasMany", model: "Order", foreignKey: "userId" },
|
|
612
|
-
profile: { type: "hasOne", model: "Profile", foreignKey: "userId" },
|
|
613
|
-
customer: { type: "belongsTo", model: "Customer", foreignKey: "customerId" },
|
|
614
|
-
};
|
|
615
|
-
|
|
616
|
-
// Eager loading with include
|
|
617
|
-
const users = await db.query("SELECT * FROM users", [], { include: ["orders", "profile"] });
|
|
71
|
+
npx tina4nodejs init [dir]
|
|
72
|
+
npx tina4nodejs serve [--port PORT]
|
|
73
|
+
npx tina4nodejs migrate
|
|
74
|
+
npx tina4nodejs seed
|
|
75
|
+
npx tina4nodejs ai [--all]
|
|
76
|
+
npx tina4nodejs generate model <name>
|
|
618
77
|
```
|
|
619
78
|
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
Enable query caching for up to 4x speedup on read-heavy workloads:
|
|
79
|
+
---
|
|
623
80
|
|
|
624
|
-
|
|
625
|
-
# .env
|
|
626
|
-
TINA4_DB_CACHE=true
|
|
627
|
-
```
|
|
81
|
+
## Performance
|
|
628
82
|
|
|
629
|
-
|
|
83
|
+
Benchmarked with `wrk` — 5,000 requests, 50 concurrent, median of 3 runs:
|
|
630
84
|
|
|
631
|
-
|
|
85
|
+
| Framework | JSON req/s | Deps | Features |
|
|
86
|
+
|-----------|-----------|------|----------|
|
|
87
|
+
| Raw `node:http` | 91,110 | 0 | 1 |
|
|
88
|
+
| **Tina4 Node.js** | **84,771** | 0 | 54 |
|
|
632
89
|
|
|
633
|
-
|
|
90
|
+
Tina4 Node.js runs at **93% of raw Node.js speed** while providing 54 built-in features — zero overhead architecture.
|
|
634
91
|
|
|
635
|
-
|
|
92
|
+
**Across all 4 Tina4 implementations:**
|
|
636
93
|
|
|
637
|
-
|
|
94
|
+
| | Python | PHP | Ruby | Node.js |
|
|
95
|
+
|---|--------|-----|------|---------|
|
|
96
|
+
| **JSON req/s** | 6,508 | 29,293 | 10,243 | 84,771 |
|
|
97
|
+
| **Dependencies** | 0 | 0 | 0 | 0 |
|
|
98
|
+
| **Features** | 54 | 54 | 54 | 54 |
|
|
638
99
|
|
|
639
|
-
|
|
640
|
-
SECRET=your-jwt-secret
|
|
641
|
-
DATABASE_URL=sqlite:///data/app.db
|
|
642
|
-
DATABASE_USERNAME=admin # Separate credentials for networked databases
|
|
643
|
-
DATABASE_PASSWORD=secret
|
|
644
|
-
TINA4_DEBUG=true # Enable dev toolbar, error overlay
|
|
645
|
-
TINA4_LOG_LEVEL=ALL # ALL, DEBUG, INFO, WARNING, ERROR
|
|
646
|
-
TINA4_LOCALE=en # en, fr, af, zh, ja, es
|
|
647
|
-
TINA4_SESSION_HANDLER=SessionFileHandler
|
|
648
|
-
SWAGGER_TITLE=My API
|
|
649
|
-
```
|
|
100
|
+
---
|
|
650
101
|
|
|
651
|
-
##
|
|
102
|
+
## Cross-Framework Parity
|
|
652
103
|
|
|
653
|
-
|
|
104
|
+
Tina4 ships identical features across four languages — same architecture, same conventions, same 54 features:
|
|
654
105
|
|
|
655
|
-
|
|
|
656
|
-
|
|
657
|
-
|
|
|
658
|
-
|
|
|
659
|
-
|
|
|
660
|
-
| Grade | A+ |
|
|
106
|
+
| | Python | PHP | Ruby | Node.js |
|
|
107
|
+
|---|--------|-----|------|---------|
|
|
108
|
+
| **Package** | `tina4-python` | `tina4stack/tina4php` | `tina4ruby` | `tina4-nodejs` |
|
|
109
|
+
| **Tests** | 2,066 | 1,427 | 1,793 | 1,950 |
|
|
110
|
+
| **Default port** | 7145 | 7146 | 7147 | 7148 |
|
|
661
111
|
|
|
662
|
-
|
|
112
|
+
**7,236 tests** across all 4 frameworks. See [tina4.com](https://tina4.com).
|
|
663
113
|
|
|
664
114
|
---
|
|
665
115
|
|
|
@@ -674,14 +124,10 @@ https://opensource.org/licenses/MIT
|
|
|
674
124
|
|
|
675
125
|
---
|
|
676
126
|
|
|
677
|
-
<p align="center"><b>Tina4</b> -- The framework that keeps out of the way of your coding.</p>
|
|
678
|
-
|
|
679
|
-
---
|
|
680
|
-
|
|
681
127
|
## Our Sponsors
|
|
682
128
|
|
|
683
129
|
**Sponsored with 🩵 by Code Infinity**
|
|
684
130
|
|
|
685
131
|
[<img src="https://codeinfinity.co.za/wp-content/uploads/2025/09/c8e-logo-github.png" alt="Code Infinity" width="100">](https://codeinfinity.co.za/about-open-source-policy?utm_source=github&utm_medium=website&utm_campaign=opensource_campaign&utm_id=opensource)
|
|
686
132
|
|
|
687
|
-
*Supporting open source communities
|
|
133
|
+
*Supporting open source communities • Innovate • Code • Empower*
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tina4-nodejs",
|
|
3
|
-
"version": "3.10.
|
|
3
|
+
"version": "3.10.41",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "
|
|
5
|
+
"description": "Tina4 for Node.js/TypeScript — 54 built-in features, zero dependencies",
|
|
6
6
|
"keywords": ["tina4", "framework", "web", "api", "orm", "graphql", "websocket", "typescript"],
|
|
7
7
|
"homepage": "https://tina4.com/nodejs",
|
|
8
8
|
"repository": {
|
|
@@ -167,8 +167,6 @@ export class BaseModel {
|
|
|
167
167
|
*/
|
|
168
168
|
static findById<T extends BaseModel>(this: new (data?: Record<string, unknown>) => T, id: unknown, include?: string[]): T | null {
|
|
169
169
|
const ModelClass = this as unknown as typeof BaseModel & (new (data?: Record<string, unknown>) => T);
|
|
170
|
-
const db = ModelClass.getDb();
|
|
171
|
-
const pk = ModelClass.getPkField();
|
|
172
170
|
const pkCol = ModelClass.getPkColumn();
|
|
173
171
|
let sql = `SELECT * FROM "${ModelClass.tableName}" WHERE "${pkCol}" = ?`;
|
|
174
172
|
|
|
@@ -179,10 +177,8 @@ export class BaseModel {
|
|
|
179
177
|
sql += ` AND ${ModelClass.tableFilter}`;
|
|
180
178
|
}
|
|
181
179
|
|
|
182
|
-
const
|
|
183
|
-
if (
|
|
184
|
-
const instance = new ModelClass(rows[0] as Record<string, unknown>) as T;
|
|
185
|
-
if (include) {
|
|
180
|
+
const instance = ModelClass.selectOne<T>(sql, [id]);
|
|
181
|
+
if (instance && include) {
|
|
186
182
|
ModelClass._eagerLoad([instance], include);
|
|
187
183
|
}
|
|
188
184
|
return instance;
|
|
@@ -533,6 +529,21 @@ export class BaseModel {
|
|
|
533
529
|
return rows.map((row) => new ModelClass(row as Record<string, unknown>) as T);
|
|
534
530
|
}
|
|
535
531
|
|
|
532
|
+
static selectOne<T extends BaseModel>(
|
|
533
|
+
this: new (data?: Record<string, unknown>) => T,
|
|
534
|
+
sql: string,
|
|
535
|
+
params?: unknown[],
|
|
536
|
+
include?: string[],
|
|
537
|
+
): T | null {
|
|
538
|
+
const ModelClass = this as unknown as typeof BaseModel & (new (data?: Record<string, unknown>) => T);
|
|
539
|
+
const results = ModelClass.select<T>(sql, params);
|
|
540
|
+
const instance = results[0] ?? null;
|
|
541
|
+
if (instance && include) {
|
|
542
|
+
ModelClass._eagerLoad([instance], include);
|
|
543
|
+
}
|
|
544
|
+
return instance;
|
|
545
|
+
}
|
|
546
|
+
|
|
536
547
|
/**
|
|
537
548
|
* Permanently delete this instance, bypassing soft delete.
|
|
538
549
|
*/
|