tina4-nodejs 3.11.12 → 3.11.14
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 +1 -2
- package/README.md +19 -13
- package/package.json +1 -1
- package/packages/core/src/router.ts +30 -12
- package/packages/orm/src/adapters/firebird.ts +9 -1
- package/packages/orm/src/adapters/postgres.ts +7 -1
package/CLAUDE.md
CHANGED
|
@@ -108,7 +108,6 @@ The HTTP foundation. Handles request/response lifecycle, route matching, middlew
|
|
|
108
108
|
- `response.ts` — Wraps `ServerResponse`, adds `.json()`, `.html()`, `.status()`, `.send()`, `.redirect()`
|
|
109
109
|
- `middleware.ts` — Chain runner, built-in CORS and request logger
|
|
110
110
|
- `static.ts` — Serves files from `public/` with MIME type detection
|
|
111
|
-
- `watcher.ts` — `fs.watch` for hot-reload in dev mode
|
|
112
111
|
- `types.ts` — All shared type definitions (`Tina4Request`, `Tina4Response`, `RouteHandler`, etc.)
|
|
113
112
|
- `events.ts` — Observer-pattern event system (`Events.on`, `emit`, `once`, `off`, `clear`)
|
|
114
113
|
- `ai.ts` — AI coding tool detection and context scaffolding (`detectAi`, `installAiContext`, `aiStatusReport`)
|
|
@@ -566,7 +565,7 @@ import { Router } from "./router.js"; // .js even though the file is .ts
|
|
|
566
565
|
2. **`tsx` for dev** — No build step needed during development. TypeScript runs directly.
|
|
567
566
|
3. **Convention-based models** — `static fields = {}` over decorators. No special TypeScript config needed.
|
|
568
567
|
4. **CDN for Swagger UI** — Keeps install under 8MB. Single HTML file loads from unpkg.com.
|
|
569
|
-
5. **
|
|
568
|
+
5. **Browser reload, not process restart** — The `tina4` Rust CLI watches `src/`, `migrations/`, `.env` and POSTs `/__dev/api/reload` to the running server. The server stays up; only the browser reloads (via WS on `/__dev_reload`, polling fallback on `GET /__dev/api/mtime`). No ESM HMR gymnastics, no server restart, no framework-side watcher.
|
|
570
569
|
6. **SQLite default** — `node:sqlite` is synchronous and fast. Full adapters for Postgres, MySQL, MSSQL/SQL Server, and Firebird.
|
|
571
570
|
7. **CLI named `tina4nodejs`** (primary) with `tina4` as alias — So `npx tina4nodejs init` or `npx tina4 init` both work.
|
|
572
571
|
8. **Event system** — Static `Events` class, synchronous dispatch, priority ordering, zero deps.
|
package/README.md
CHANGED
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
</p>
|
|
4
4
|
<h1 align="center">Tina4 Node.js</h1>
|
|
5
5
|
<h3 align="center">The Intelligent Native Application 4ramework</h3>
|
|
6
|
-
<p align="center">
|
|
6
|
+
<p align="center">55 built-in features. Zero dependencies. One import, everything works.</p>
|
|
7
7
|
<p align="center">
|
|
8
|
-
<a href="https://www.npmjs.com/package/
|
|
9
|
-
<img src="https://img.shields.io/badge/tests-
|
|
10
|
-
<img src="https://img.shields.io/badge/features-
|
|
8
|
+
<a href="https://www.npmjs.com/package/@tina4/core"><img src="https://img.shields.io/npm/v/@tina4/core?color=7b1fa2&label=npm" alt="npm"></a>
|
|
9
|
+
<img src="https://img.shields.io/badge/tests-2%2C897%20passing-brightgreen" alt="Tests">
|
|
10
|
+
<img src="https://img.shields.io/badge/features-55-blue" alt="Features">
|
|
11
11
|
<img src="https://img.shields.io/badge/dependencies-0-brightgreen" alt="Zero Deps">
|
|
12
12
|
<a href="https://tina4.com"><img src="https://img.shields.io/badge/docs-tina4.com-7b1fa2" alt="Docs"></a>
|
|
13
13
|
</p>
|
|
@@ -17,6 +17,12 @@
|
|
|
17
17
|
## Quick Start
|
|
18
18
|
|
|
19
19
|
```bash
|
|
20
|
+
# With the Tina4 CLI (recommended — enables SCSS + live reload)
|
|
21
|
+
cargo install tina4 # or grab a binary from https://github.com/tina4stack/tina4/releases
|
|
22
|
+
tina4 init nodejs ./my-app
|
|
23
|
+
cd my-app && tina4 serve
|
|
24
|
+
|
|
25
|
+
# Without the Tina4 CLI
|
|
20
26
|
npx tina4nodejs init my-app
|
|
21
27
|
cd my-app && npx tina4nodejs serve
|
|
22
28
|
```
|
|
@@ -61,7 +67,7 @@ export default class User {
|
|
|
61
67
|
| **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
68
|
| **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
69
|
|
|
64
|
-
**
|
|
70
|
+
**2,897 tests. Zero dependencies. Full parity across Python, PHP, Ruby, and Node.js.**
|
|
65
71
|
|
|
66
72
|
---
|
|
67
73
|
|
|
@@ -85,9 +91,9 @@ Benchmarked with `wrk` — 5,000 requests, 50 concurrent, median of 3 runs:
|
|
|
85
91
|
| Framework | JSON req/s | Deps | Features |
|
|
86
92
|
|-----------|-----------|------|----------|
|
|
87
93
|
| Raw `node:http` | 91,110 | 0 | 1 |
|
|
88
|
-
| **Tina4 Node.js** | **84,771** | 0 |
|
|
94
|
+
| **Tina4 Node.js** | **84,771** | 0 | 55 |
|
|
89
95
|
|
|
90
|
-
Tina4 Node.js runs at **93% of raw Node.js speed** while providing
|
|
96
|
+
Tina4 Node.js runs at **93% of raw Node.js speed** while providing 55 built-in features — zero overhead architecture.
|
|
91
97
|
|
|
92
98
|
**Across all 4 Tina4 implementations:**
|
|
93
99
|
|
|
@@ -95,21 +101,21 @@ Tina4 Node.js runs at **93% of raw Node.js speed** while providing 54 built-in f
|
|
|
95
101
|
|---|--------|-----|------|---------|
|
|
96
102
|
| **JSON req/s** | 6,508 | 29,293 | 10,243 | 84,771 |
|
|
97
103
|
| **Dependencies** | 0 | 0 | 0 | 0 |
|
|
98
|
-
| **Features** |
|
|
104
|
+
| **Features** | 55 | 55 | 55 | 55 |
|
|
99
105
|
|
|
100
106
|
---
|
|
101
107
|
|
|
102
108
|
## Cross-Framework Parity
|
|
103
109
|
|
|
104
|
-
Tina4 ships identical features across four languages — same architecture, same conventions, same
|
|
110
|
+
Tina4 ships identical features across four languages — same architecture, same conventions, same 55 features:
|
|
105
111
|
|
|
106
112
|
| | Python | PHP | Ruby | Node.js |
|
|
107
113
|
|---|--------|-----|------|---------|
|
|
108
|
-
| **Package** | `tina4-python` | `tina4stack/tina4php` | `tina4ruby` |
|
|
109
|
-
| **Tests** | 2,
|
|
110
|
-
| **Default port** |
|
|
114
|
+
| **Package** | `tina4-python` | `tina4stack/tina4php` | `tina4ruby` | `@tina4/core` |
|
|
115
|
+
| **Tests (v3.11.12)** | 2,281 | 2,073 | 2,508 | 2,897 |
|
|
116
|
+
| **Default port** | 7146 | 7145 | 7147 | 7148 |
|
|
111
117
|
|
|
112
|
-
|
|
118
|
+
**~9,700 tests** across all 4 frameworks. See [tina4.com](https://tina4.com).
|
|
113
119
|
|
|
114
120
|
---
|
|
115
121
|
|
package/package.json
CHANGED
|
@@ -384,6 +384,29 @@ export class Router {
|
|
|
384
384
|
defaultRouter.group(prefix, callback, middlewares);
|
|
385
385
|
}
|
|
386
386
|
|
|
387
|
+
/**
|
|
388
|
+
* Supported typed-parameter constraints. Mirrored verbatim in
|
|
389
|
+
* tina4-python / tina4-php / tina4-ruby for cross-framework parity.
|
|
390
|
+
*
|
|
391
|
+
* Any type name not in this table throws at route registration time —
|
|
392
|
+
* we never silently fall through to the default matcher, because a
|
|
393
|
+
* typo like `{id:inetger}` would otherwise match anything and create
|
|
394
|
+
* a security footgun (see tina4-book#125).
|
|
395
|
+
*/
|
|
396
|
+
private static readonly PARAM_TYPE_PATTERNS: Record<string, string> = {
|
|
397
|
+
string: "[^/]+", // default, any non-slash segment
|
|
398
|
+
int: "\\d+",
|
|
399
|
+
integer: "\\d+",
|
|
400
|
+
float: "[\\d.]+",
|
|
401
|
+
number: "[\\d.]+",
|
|
402
|
+
alpha: "[A-Za-z]+", // letters only
|
|
403
|
+
alnum: "[A-Za-z0-9]+", // letters + digits
|
|
404
|
+
slug: "[a-z0-9-]+", // URL slug
|
|
405
|
+
uuid: "[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}",
|
|
406
|
+
path: ".+", // greedy
|
|
407
|
+
".*": ".+",
|
|
408
|
+
};
|
|
409
|
+
|
|
387
410
|
private compilePattern(pattern: string): { regex: RegExp; paramNames: string[] } {
|
|
388
411
|
const paramNames: string[] = [];
|
|
389
412
|
|
|
@@ -407,19 +430,14 @@ export class Router {
|
|
|
407
430
|
const name = colonIdx >= 0 ? inner.slice(0, colonIdx) : inner;
|
|
408
431
|
const type = colonIdx >= 0 ? inner.slice(colonIdx + 1) : "string";
|
|
409
432
|
paramNames.push(name);
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
return "([\\d.]+)";
|
|
417
|
-
case "path":
|
|
418
|
-
case ".*":
|
|
419
|
-
return "(.+)";
|
|
420
|
-
default:
|
|
421
|
-
return "([^/]+)";
|
|
433
|
+
const table = Router.PARAM_TYPE_PATTERNS;
|
|
434
|
+
if (!Object.prototype.hasOwnProperty.call(table, type)) {
|
|
435
|
+
const valid = Object.keys(table).filter((k) => k !== ".*").sort().join(", ");
|
|
436
|
+
throw new Error(
|
|
437
|
+
`Unknown param type '${type}' in route '${pattern}'. Valid types: ${valid}.`
|
|
438
|
+
);
|
|
422
439
|
}
|
|
440
|
+
return `(${table[type]})`;
|
|
423
441
|
}
|
|
424
442
|
// Dynamic param: [id] (file-based routing internal syntax)
|
|
425
443
|
if (segment.startsWith("[") && segment.endsWith("]")) {
|
|
@@ -161,7 +161,15 @@ export class FirebirdAdapter implements DatabaseAdapter {
|
|
|
161
161
|
async queryAsync<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<T[]> {
|
|
162
162
|
this.ensureConnected();
|
|
163
163
|
const rows = await this.queryPromise(sql, params);
|
|
164
|
-
return rows as T[];
|
|
164
|
+
return (rows as T[]).map(row => this.decodeBlobs(row));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Ensure BLOB columns are readable — node-firebird may return callback-based
|
|
168
|
+
* blob readers. Convert to Buffer. Regular buffers pass through unchanged. */
|
|
169
|
+
private decodeBlobs<T>(row: T): T {
|
|
170
|
+
// node-firebird returns BLOBs as Buffer by default when using
|
|
171
|
+
// query(sql, params, callback) — already raw bytes.
|
|
172
|
+
return row;
|
|
165
173
|
}
|
|
166
174
|
|
|
167
175
|
fetch<T = Record<string, unknown>>(sql: string, params?: unknown[], limit?: number, skip?: number): T[] {
|
|
@@ -60,6 +60,12 @@ export class PostgresAdapter implements DatabaseAdapter {
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
/** Convert ? placeholders to $1, $2, ... for pg. */
|
|
63
|
+
/** Ensure bytea columns are Buffer (already the case with pg). No-op guard. */
|
|
64
|
+
private decodeBlobs<T>(row: T): T {
|
|
65
|
+
// pg npm returns bytea as Buffer — already raw bytes. No conversion needed.
|
|
66
|
+
return row;
|
|
67
|
+
}
|
|
68
|
+
|
|
63
69
|
private convertPlaceholders(sql: string): string {
|
|
64
70
|
let count = 0;
|
|
65
71
|
return sql.replace(/\?/g, () => {
|
|
@@ -114,7 +120,7 @@ export class PostgresAdapter implements DatabaseAdapter {
|
|
|
114
120
|
this.ensureConnected();
|
|
115
121
|
const convertedSql = this.convertPlaceholders(sql);
|
|
116
122
|
const result = await this.client!.query(convertedSql, params);
|
|
117
|
-
return result.rows as T[];
|
|
123
|
+
return (result.rows as T[]).map(row => this.decodeBlobs(row));
|
|
118
124
|
}
|
|
119
125
|
|
|
120
126
|
fetch<T = Record<string, unknown>>(sql: string, params?: unknown[], limit?: number, skip?: number): T[] {
|