tina4-nodejs 3.10.38 → 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 CHANGED
@@ -1,10 +1,10 @@
1
- # CLAUDE.md — AI Developer Guide for tina4-nodejs (v3.10.24)
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.24 — a convention-over-configuration structural paradigm. **Not a framework.** The developer writes TypeScript; Tina4 is invisible infrastructure.
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%2C812%20passing-brightgreen" alt="Tests">
15
- <img src="https://img.shields.io/badge/features-38-blue" alt="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> &bull;
22
- <a href="#getting-started">Getting Started</a> &bull;
23
- <a href="#features">Features</a> &bull;
24
- <a href="#cli-reference">CLI Reference</a> &bull;
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
- # 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 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
- ## What's Included
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
- import type { Tina4Request, Tina4Response } from "tina4-nodejs";
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
- static tableName = "users";
364
-
365
- static fields = {
366
- id: { type: "integer" as const, primaryKey: true, autoIncrement: true },
367
- name: { type: "string" as const, required: true, maxLength: 100 },
368
- email: { type: "string" as const, required: true, pattern: "^[^@]+@[^@]+$" },
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
- ## Dev Mode
558
-
559
- Set `TINA4_DEBUG=true` in `.env` to enable:
49
+ ## What's Included
560
50
 
561
- - **Live reload** -- browser auto-refreshes on code changes
562
- - **CSS hot-reload** -- SCSS changes apply without page refresh
563
- - **Error overlay** -- rich error display in the browser
564
- - **Dev admin** with routes, queue, requests, errors, system tabs
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] # Scaffold a new project
572
- npx tina4nodejs serve [--port 7148] # Start dev server (default: 7148)
573
- npx tina4nodejs serve --production # Auto-use cluster mode (multi-core)
574
- npx tina4nodejs migrate # Run pending migrations
575
- npx tina4nodejs migrate:create <desc> # Create a migration file
576
- npx tina4nodejs migrate:rollback # Rollback last batch
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
- ### DB Query Caching
621
-
622
- Enable query caching for up to 4x speedup on read-heavy workloads:
79
+ ---
623
80
 
624
- ```bash
625
- # .env
626
- TINA4_DB_CACHE=true
627
- ```
81
+ ## Performance
628
82
 
629
- ### Frond Pre-Compilation
83
+ Benchmarked with `wrk` — 5,000 requests, 50 concurrent, median of 3 runs:
630
84
 
631
- Templates are pre-compiled for 2.8x faster rendering.
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
- ### Gallery
90
+ Tina4 Node.js runs at **93% of raw Node.js speed** while providing 54 built-in features — zero overhead architecture.
634
91
 
635
- 7 interactive examples with **Try It** deploy.
92
+ **Across all 4 Tina4 implementations:**
636
93
 
637
- ## Environment
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
- ```bash
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
- ## Carbonah Green Benchmarks
102
+ ## Cross-Framework Parity
652
103
 
653
- All benchmarks rated **A+** (South Africa grid, 1000 iterations each):
104
+ Tina4 ships identical features across four languages — same architecture, same conventions, same 54 features:
654
105
 
655
- | Metric | Value |
656
- |--------|-------|
657
- | Startup time | 38ms |
658
- | Memory usage | 104.2MB |
659
- | SCI score | 0.00552 gCO2eq |
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
- Run locally: `npx tina4nodejs benchmark`
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 <span style="color: #1DC7DE;">•</span> Innovate <span style="color: #1DC7DE;">•</span> Code <span style="color: #1DC7DE;">•</span> Empower*
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.38",
3
+ "version": "3.10.41",
4
4
  "type": "module",
5
- "description": "This is not a framework. Tina4 for Node.js/TypeScript — zero deps, 38 built-in features.",
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": {
@@ -7,6 +7,7 @@ import { migrateRollback } from "./commands/migrateRollback.js";
7
7
  import { listRoutes } from "./commands/routes.js";
8
8
  import { runTests } from "./commands/test.js";
9
9
  import { generate } from "./commands/generate.js";
10
+ import { runSeeds } from "./commands/seed.js";
10
11
 
11
12
  const args = process.argv.slice(2);
12
13
  const command = args[0];
@@ -24,6 +25,7 @@ const HELP = `
24
25
  tina4nodejs routes List all registered routes
25
26
  tina4nodejs test [file] Run project tests
26
27
  tina4nodejs generate <what> <name> Generate scaffolding (model, route, migration, middleware)
28
+ tina4nodejs seed [file] Run database seed files from src/seeds/
27
29
  tina4nodejs ai Install AI coding assistant context files
28
30
  tina4nodejs help Show this help message
29
31
 
@@ -78,6 +80,10 @@ async function main(): Promise<void> {
78
80
  await generate(what, genName);
79
81
  break;
80
82
  }
83
+ case "seed": {
84
+ await runSeeds(args[1]);
85
+ break;
86
+ }
81
87
  case "ai": {
82
88
  const { showMenu, installSelected, installAll } = await import("../../core/src/ai.js");
83
89
  const root = args[1] || ".";
@@ -0,0 +1,72 @@
1
+ /**
2
+ * CLI command: seed — Run database seed files.
3
+ *
4
+ * Scans src/seeds/ for .ts files and executes them with tsx in alphabetical order.
5
+ */
6
+ import { existsSync, readdirSync } from "node:fs";
7
+ import { resolve, join } from "node:path";
8
+ import { execSync } from "node:child_process";
9
+
10
+ export async function runSeeds(seedPath?: string): Promise<void> {
11
+ const cwd = process.cwd();
12
+ const seedDir = resolve(cwd, "src/seeds");
13
+
14
+ // If a specific seed file is provided, run it directly
15
+ if (seedPath) {
16
+ const file = resolve(seedPath);
17
+ if (!existsSync(file)) {
18
+ console.error(` Error: Seed file not found: ${seedPath}`);
19
+ process.exit(1);
20
+ }
21
+ console.log(` Running seed: ${seedPath}\n`);
22
+ try {
23
+ execSync(`npx tsx "${file}"`, { cwd, stdio: "inherit" });
24
+ } catch {
25
+ process.exit(1);
26
+ }
27
+ return;
28
+ }
29
+
30
+ // Auto-discover seed files in src/seeds/
31
+ if (!existsSync(seedDir)) {
32
+ console.log(" No seeds directory found.");
33
+ console.log(" Create seed files in src/seeds/ (e.g. src/seeds/001-users.ts)");
34
+ return;
35
+ }
36
+
37
+ let seedFiles: string[];
38
+ try {
39
+ seedFiles = readdirSync(seedDir)
40
+ .filter((f) => f.endsWith(".ts"))
41
+ .sort()
42
+ .map((f) => join(seedDir, f));
43
+ } catch {
44
+ console.log(" Could not read src/seeds/ directory.");
45
+ return;
46
+ }
47
+
48
+ if (seedFiles.length === 0) {
49
+ console.log(" No seed files found in src/seeds/");
50
+ return;
51
+ }
52
+
53
+ console.log(` Found ${seedFiles.length} seed file(s)\n`);
54
+
55
+ let failed = false;
56
+ for (const file of seedFiles) {
57
+ const relative = file.replace(cwd + "/", "");
58
+ console.log(` Seeding: ${relative}`);
59
+ try {
60
+ execSync(`npx tsx "${file}"`, { cwd, stdio: "inherit" });
61
+ } catch {
62
+ failed = true;
63
+ }
64
+ }
65
+
66
+ if (failed) {
67
+ console.error("\n Some seeds failed.");
68
+ process.exit(1);
69
+ }
70
+
71
+ console.log("\n All seeds completed.");
72
+ }
@@ -2332,6 +2332,8 @@ function tina4VersionModal(){
2332
2332
  if(l>c){isNewer=true;break;}
2333
2333
  if(l<c)break;
2334
2334
  }
2335
+ var isAhead=false;
2336
+ if(!isNewer){for(var i=0;i<Math.max(cParts.length,lParts.length);i++){var c2=cParts[i]||0,l2=lParts[i]||0;if(c2>l2){isAhead=true;break;}if(c2<l2)break;}}
2335
2337
  if(isNewer){
2336
2338
  var breaking=(lParts[0]!==cParts[0]||lParts[1]!==cParts[1]);
2337
2339
  el.innerHTML='Latest: <strong style="color:#f9e2af;">v'+latest+'</strong>';
@@ -2340,6 +2342,9 @@ function tina4VersionModal(){
2340
2342
  }else{
2341
2343
  el.innerHTML+='<div style="color:#f9e2af;margin-top:6px;">Patch update available. Run: <code style="background:#313244;padding:2px 6px;border-radius:3px;">npm install tina4-nodejs@latest</code></div>';
2342
2344
  }
2345
+ }else if(isAhead){
2346
+ el.innerHTML='You are running <strong style="color:#cba6f7;">v'+current+'</strong> (ahead of npm <strong>v'+latest+'</strong> &mdash; not yet published).';
2347
+ el.style.color='#cba6f7';
2343
2348
  }else{
2344
2349
  el.innerHTML='Latest: <strong style="color:#a6e3a1;">v'+latest+'</strong> &mdash; You are up to date!';
2345
2350
  el.style.color='#a6e3a1';
@@ -247,6 +247,11 @@ export class Router {
247
247
  return all;
248
248
  }
249
249
 
250
+ /** Alias for getRoutes(). */
251
+ allRoutes(): RouteDefinition[] {
252
+ return this.getRoutes();
253
+ }
254
+
250
255
  /**
251
256
  * List all routes in a debug-friendly format for CLI output.
252
257
  */
@@ -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,15 +177,38 @@ export class BaseModel {
179
177
  sql += ` AND ${ModelClass.tableFilter}`;
180
178
  }
181
179
 
182
- const rows = db.query(sql, [id]);
183
- if (rows.length === 0) return null;
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;
189
185
  }
190
186
 
187
+ /**
188
+ * Create a new instance from data, save it, and return the saved instance.
189
+ *
190
+ * Usage:
191
+ * const user = User.create({ name: "Alice", email: "alice@example.com" });
192
+ */
193
+ static create<T extends BaseModel>(
194
+ this: new (data?: Record<string, unknown>) => T,
195
+ data: Record<string, unknown>,
196
+ ): T {
197
+ const instance = new this(data) as T;
198
+ instance.save();
199
+ return instance;
200
+ }
201
+
202
+ /** Alias for findById(). */
203
+ static find<T extends BaseModel>(this: new (data?: Record<string, unknown>) => T, id: unknown, include?: string[]): T | null {
204
+ return (this as unknown as typeof BaseModel).findById.call(this, id, include) as T | null;
205
+ }
206
+
207
+ /** Alias for findById(). */
208
+ static load<T extends BaseModel>(this: new (data?: Record<string, unknown>) => T, id: unknown, include?: string[]): T | null {
209
+ return (this as unknown as typeof BaseModel).findById.call(this, id, include) as T | null;
210
+ }
211
+
191
212
  /**
192
213
  * Find all records, optionally with a where clause.
193
214
  * @param where Optional WHERE clause.
@@ -508,6 +529,21 @@ export class BaseModel {
508
529
  return rows.map((row) => new ModelClass(row as Record<string, unknown>) as T);
509
530
  }
510
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
+
511
547
  /**
512
548
  * Permanently delete this instance, bypassing soft delete.
513
549
  */
@@ -393,6 +393,44 @@ export class Database {
393
393
  return this.getNextAdapter().tables();
394
394
  }
395
395
 
396
+ /**
397
+ * Get column metadata for a table.
398
+ * Uses the adapter's columns() method which handles engine-specific introspection
399
+ * (PRAGMA table_info for SQLite, information_schema.columns for others).
400
+ *
401
+ * @param tableName - Name of the table to inspect.
402
+ * @returns Array of column info objects: { name, type, nullable, default, primaryKey }.
403
+ */
404
+ getColumns(tableName: string): { name: string; type: string; nullable?: boolean; default?: unknown; primaryKey?: boolean }[] {
405
+ return this.getNextAdapter().columns(tableName);
406
+ }
407
+
408
+ /**
409
+ * Execute a SQL statement with multiple parameter sets (batch insert/update).
410
+ * Wraps all executions in a single transaction for atomicity and performance.
411
+ *
412
+ * @param sql - The SQL statement with parameter placeholders.
413
+ * @param paramSets - Array of parameter arrays, one per execution.
414
+ * @returns Array of results from each execution.
415
+ */
416
+ executeMany(sql: string, paramSets: unknown[][]): unknown[] {
417
+ const adapter = this.getNextAdapter();
418
+ const results: unknown[] = [];
419
+
420
+ adapter.startTransaction();
421
+ try {
422
+ for (const params of paramSets) {
423
+ results.push(adapter.execute(sql, params));
424
+ }
425
+ adapter.commit();
426
+ } catch (e) {
427
+ adapter.rollback();
428
+ throw e;
429
+ }
430
+
431
+ return results;
432
+ }
433
+
396
434
  /** Get the last auto-increment id. */
397
435
  getLastId(): string | number {
398
436
  const id = this.getNextAdapter().lastInsertId();