tina4-nodejs 3.13.94 → 3.13.96

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.
Files changed (123) hide show
  1. package/CLAUDE.md +158 -30
  2. package/README.md +1 -1
  3. package/package.json +3 -1
  4. package/packages/cli/dist/bin.js +30911 -28444
  5. package/packages/cli/src/commands/metrics.ts +17 -11
  6. package/packages/cli/src/commands/serve.ts +10 -9
  7. package/packages/core/dist/index.js +30810 -28261
  8. package/packages/core/public/css/tina4.min.css +1 -1
  9. package/packages/core/src/ai.ts +7 -1
  10. package/packages/core/src/auth.ts +191 -39
  11. package/packages/core/src/background.ts +19 -19
  12. package/packages/core/src/cache.ts +492 -49
  13. package/packages/core/src/devAdmin.ts +79 -32
  14. package/packages/core/src/dispatchPipeline.ts +285 -0
  15. package/packages/core/src/dotenv.ts +185 -40
  16. package/packages/core/src/index.ts +6 -7
  17. package/packages/core/src/logger.ts +257 -36
  18. package/packages/core/src/mcp.ts +1 -1
  19. package/packages/core/src/messenger.ts +294 -106
  20. package/packages/core/src/metrics.ts +199 -961
  21. package/packages/core/src/middleware.ts +390 -123
  22. package/packages/core/src/queue.ts +188 -32
  23. package/packages/core/src/queueBackends/kafkaBackend.ts +1 -1
  24. package/packages/core/src/queueBackends/liteBackend.ts +13 -0
  25. package/packages/core/src/queueBackends/mongoBackend.ts +101 -9
  26. package/packages/core/src/queueBackends/rabbitmqBackend.ts +22 -4
  27. package/packages/core/src/rateLimiter.ts +10 -5
  28. package/packages/core/src/request.ts +34 -16
  29. package/packages/core/src/response.ts +46 -1
  30. package/packages/core/src/router.ts +29 -4
  31. package/packages/core/src/server.ts +886 -421
  32. package/packages/core/src/session.ts +244 -27
  33. package/packages/core/src/sessionHandlers/databaseHandler.ts +338 -48
  34. package/packages/core/src/sessionHandlers/memcachedHandler.ts +181 -0
  35. package/packages/core/src/sessionHandlers/mongoClient.ts +293 -208
  36. package/packages/core/src/sessionHandlers/mongoHandler.ts +88 -8
  37. package/packages/core/src/sessionHandlers/respClient.ts +16 -147
  38. package/packages/core/src/sessionHandlers/sqlClient.ts +290 -0
  39. package/packages/core/src/sessionHandlers/syncBridge.ts +190 -0
  40. package/packages/core/src/sessionHandlers/syncSocket.ts +236 -0
  41. package/packages/core/src/testClient.ts +18 -5
  42. package/packages/core/src/trustedProxy.ts +249 -0
  43. package/packages/core/src/types.ts +29 -5
  44. package/packages/core/src/websocket.ts +66 -0
  45. package/packages/orm/dist/index.js +22717 -20168
  46. package/packages/orm/src/adapters/firebird.ts +183 -56
  47. package/packages/orm/src/adapters/mongodb.ts +25 -4
  48. package/packages/orm/src/adapters/mssql.ts +114 -29
  49. package/packages/orm/src/adapters/mysql.ts +103 -40
  50. package/packages/orm/src/adapters/odbc.ts +44 -21
  51. package/packages/orm/src/adapters/postgres.ts +118 -26
  52. package/packages/orm/src/adapters/sqlDialect.ts +120 -0
  53. package/packages/orm/src/adapters/sqlite.ts +60 -24
  54. package/packages/orm/src/autoCrud.ts +12 -10
  55. package/packages/orm/src/baseModel.ts +135 -40
  56. package/packages/orm/src/cachedDatabase.ts +43 -19
  57. package/packages/orm/src/connectTimeout.ts +265 -0
  58. package/packages/orm/src/database.ts +241 -197
  59. package/packages/orm/src/databaseResult.ts +51 -28
  60. package/packages/orm/src/databaseUrl.ts +484 -0
  61. package/packages/orm/src/docstore.ts +386 -145
  62. package/packages/orm/src/index.ts +13 -6
  63. package/packages/orm/src/migration.ts +44 -11
  64. package/packages/orm/src/model.ts +4 -0
  65. package/packages/orm/src/queryBuilder.ts +47 -6
  66. package/packages/orm/src/sqlTranslator.ts +310 -4
  67. package/packages/orm/src/types.ts +21 -77
  68. package/packages/swagger/dist/index.js +78 -20
  69. package/packages/swagger/src/generator.ts +172 -29
  70. package/types/core/src/ai.d.ts +1 -1
  71. package/types/core/src/auth.d.ts +28 -5
  72. package/types/core/src/background.d.ts +3 -3
  73. package/types/core/src/cache.d.ts +15 -12
  74. package/types/core/src/dispatchPipeline.d.ts +117 -0
  75. package/types/core/src/dotenv.d.ts +38 -16
  76. package/types/core/src/index.d.ts +6 -9
  77. package/types/core/src/logger.d.ts +93 -16
  78. package/types/core/src/messenger.d.ts +47 -6
  79. package/types/core/src/metrics.d.ts +25 -61
  80. package/types/core/src/middleware.d.ts +134 -11
  81. package/types/core/src/queue.d.ts +54 -5
  82. package/types/core/src/queueBackends/kafkaBackend.d.ts +1 -1
  83. package/types/core/src/queueBackends/liteBackend.d.ts +9 -0
  84. package/types/core/src/queueBackends/mongoBackend.d.ts +24 -2
  85. package/types/core/src/queueBackends/rabbitmqBackend.d.ts +3 -3
  86. package/types/core/src/router.d.ts +14 -3
  87. package/types/core/src/server.d.ts +15 -4
  88. package/types/core/src/session.d.ts +87 -2
  89. package/types/core/src/sessionHandlers/databaseHandler.d.ts +60 -5
  90. package/types/core/src/sessionHandlers/memcachedHandler.d.ts +60 -0
  91. package/types/core/src/sessionHandlers/mongoClient.d.ts +16 -5
  92. package/types/core/src/sessionHandlers/mongoHandler.d.ts +51 -3
  93. package/types/core/src/sessionHandlers/respClient.d.ts +2 -2
  94. package/types/core/src/sessionHandlers/sqlClient.d.ts +39 -0
  95. package/types/core/src/sessionHandlers/syncBridge.d.ts +91 -0
  96. package/types/core/src/sessionHandlers/syncSocket.d.ts +49 -0
  97. package/types/core/src/trustedProxy.d.ts +44 -0
  98. package/types/core/src/types.d.ts +28 -5
  99. package/types/core/src/websocket.d.ts +26 -0
  100. package/types/orm/src/adapters/firebird.d.ts +55 -10
  101. package/types/orm/src/adapters/mongodb.d.ts +2 -2
  102. package/types/orm/src/adapters/mssql.d.ts +18 -11
  103. package/types/orm/src/adapters/mysql.d.ts +11 -10
  104. package/types/orm/src/adapters/odbc.d.ts +9 -12
  105. package/types/orm/src/adapters/postgres.d.ts +11 -10
  106. package/types/orm/src/adapters/sqlDialect.d.ts +71 -0
  107. package/types/orm/src/adapters/sqlite.d.ts +15 -3
  108. package/types/orm/src/baseModel.d.ts +45 -9
  109. package/types/orm/src/cachedDatabase.d.ts +18 -5
  110. package/types/orm/src/connectTimeout.d.ts +100 -0
  111. package/types/orm/src/database.d.ts +78 -28
  112. package/types/orm/src/databaseResult.d.ts +29 -15
  113. package/types/orm/src/databaseUrl.d.ts +125 -0
  114. package/types/orm/src/docstore.d.ts +102 -43
  115. package/types/orm/src/index.d.ts +6 -4
  116. package/types/orm/src/migration.d.ts +4 -3
  117. package/types/orm/src/queryBuilder.d.ts +23 -3
  118. package/types/orm/src/sqlTranslator.d.ts +126 -2
  119. package/types/orm/src/types.d.ts +21 -38
  120. package/packages/core/src/scss.ts +0 -623
  121. package/packages/core/src/sessionHandlers/redisHandler.ts +0 -219
  122. package/types/core/src/scss.d.ts +0 -19
  123. package/types/core/src/sessionHandlers/redisHandler.d.ts +0 -60
package/CLAUDE.md CHANGED
@@ -1,10 +1,10 @@
1
- # CLAUDE.md - AI Developer Guide for tina4-nodejs (v3.13.94)
1
+ # CLAUDE.md - AI Developer Guide for tina4-nodejs (v3.13.95)
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.13.94 - The Intelligent Native Application 4ramework. A convention-over-configuration structural paradigm. The developer writes TypeScript; Tina4 is invisible infrastructure.
7
+ Tina4 for Node.js/TypeScript v3.13.95 - The Intelligent Native Application 4ramework. A convention-over-configuration structural paradigm. 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
 
@@ -34,7 +34,6 @@ tina4-nodejs/
34
34
  messenger.ts # Messaging system
35
35
  queue.ts # Queue system
36
36
  rateLimiter.ts # Rate limiting middleware
37
- scss.ts # SCSS compilation
38
37
  service.ts # Service layer helpers
39
38
  session.ts # Session management
40
39
  testing.ts # Inline testing framework (attach tests to functions)
@@ -55,9 +54,9 @@ tina4-nodejs/
55
54
  swagger/ # OpenAPI spec generator, Swagger UI
56
55
  frond/ # Zero-dependency Twig-compatible template engine
57
56
  test/
58
- run-all.ts # Test runner — executes all 43 test files
57
+ run-all.ts # Test runner — executes every test file (262 in the last lab run)
59
58
  integration.ts # Full integration test
60
- *.test.ts # 42 individual test files covering all subsystems
59
+ *.test.ts # 267 individual test files covering all subsystems
61
60
  plan/
62
61
  FEATURES.md # Feature tracking and roadmap
63
62
  ```
@@ -72,13 +71,13 @@ This is an **npm workspaces monorepo**. All packages are in `packages/*`.
72
71
  - **Database:** SQLite via `node:sqlite` (default), with adapters for Postgres, MySQL, MSSQL/SQL Server, and Firebird
73
72
  - **Templates:** Frond — built-in zero-dependency Twig-compatible engine (`@tina4/frond`)
74
73
  - **Dev tooling:** `tsx` for runtime TS execution, `esbuild` for builds
75
- - **Testing:** 43 test files via `tsx test/run-all.ts`
74
+ - **Testing:** 267 `*.test.ts` files via `tsx test/run-all.ts`
76
75
 
77
76
  ## Key Commands
78
77
 
79
78
  ```bash
80
79
  npm install # Install all workspace dependencies
81
- npm test # Run all 43 test files via test/run-all.ts
80
+ npm test # Run every test file via test/run-all.ts
82
81
  npm run build # Build all packages to dist/
83
82
  npm run clean # Remove all dist/ directories
84
83
  ```
@@ -128,11 +127,11 @@ The HTTP foundation. Handles request/response lifecycle, route matching, middlew
128
127
  - `queue.ts` — Queue system with pluggable backends
129
128
  - `graphql.ts` — GraphQL engine. **Hardening:** selection-set nesting is bounded by `TINA4_GRAPHQL_MAX_DEPTH` (default `50`; `<= 0` disables; exposed as the public `gql.maxDepth` field + `graphqlMaxDepth()` helper). Depth increments on every recursive entry — sub-selections, fragment spreads, AND inline fragments — so an over-deep query or a circular fragment fails with `Query exceeds maximum depth of N` instead of overflowing the stack (top-level starts at depth 1). A resolver exception is logged via `Log.error` and the detail is surfaced to the client **only** under `TINA4_DEBUG` (`isDebugMode()`); otherwise it returns a generic `Internal server error` (path preserved) so internal state never leaks.
130
129
  - `i18n.ts` — Internationalization / localization
131
- - `logger.ts` — Structured logging. Five first-class severity levels: `debug`(0) < `info`(1) < `warning`(2) < `error`(3) < `critical`(4). `critical` is the HIGHEST level, NOT a relabelled `error`, and renders magenta. `Log.critical()` ALWAYS emits like every other level — subject only to `TINA4_LOG_LEVEL` (which it always clears) and teed to the log file whenever a file is being written. There is NO enable toggle: the old `TINA4_LOG_CRITICAL` opt-in was retired in v3.13.39 (the env var is no longer read), so a critical log is never a silent no-op. `Log.isEnabled("critical")` is ordinary threshold logic (`4 >= configured min`). **Dev/prod-aware default file output (v3.13.39, Python master 4c6d881):** stdout is ALWAYS on. When `TINA4_LOG_OUTPUT` is unset (default), the log FILE (`logs/tina4.log`) is written ONLY in development (`TINA4_DEBUG` truthy); in production / containers (`TINA4_DEBUG` falsy) the logger is stdout-only — no file to bloat the writable layer / disk (12-factor: logs on stdout for the platform to capture). Explicit `TINA4_LOG_OUTPUT=file`/`both`, OR an explicit `TINA4_LOG_FILE` path, always forces a file (explicit wins). `readEnv()` resolves all of this into a single `fileEnabled` flag that gates the file writer. Full parity with Python master.
130
+ - `logger.ts` — Structured logging. Five first-class severity levels: `debug`(0) < `info`(1) < `warning`(2) < `error`(3) < `critical`(4). `critical` is the HIGHEST level, NOT a relabelled `error`, and renders magenta. `Log.critical()` ALWAYS emits like every other level — subject only to `TINA4_LOG_LEVEL` (which it always clears) and teed to the log file whenever a file is being written. There is NO enable toggle: the old `TINA4_LOG_CRITICAL` opt-in was retired in v3.13.39 (the env var is no longer read), so a critical log is never a silent no-op. `Log.isEnabled("critical")` is ordinary threshold logic (`4 >= configured min`). **Dev/prod-aware default file output (v3.13.39, Python master 4c6d881):** stdout is ALWAYS on. When `TINA4_LOG_OUTPUT` is unset (default), the log FILE (`logs/tina4.log`) is written ONLY in development (`TINA4_DEBUG` truthy); in production / containers (`TINA4_DEBUG` falsy) the logger is stdout-only — no file to bloat the writable layer / disk (12-factor: logs on stdout for the platform to capture). Explicit `TINA4_LOG_OUTPUT=file`/`both`, OR an explicit `TINA4_LOG_FILE` path, always forces a file (explicit wins). `readEnv()` resolves all of this into a single `fileEnabled` flag that gates the file writer. Full parity with Python master. **Format is TEXT by default (settled logger contract, 2026-08-01):** `TINA4_LOG_FORMAT=json` is the ONLY thing that selects JSON, and it applies to BOTH sinks (stdout and file). The implicit "production means JSON" switch (an unset `TINA4_DEBUG`) is DELETED — it made the same `.env` produce four different formats across the four frameworks. `TINA4_DEBUG` now decides COLOUR only: ANSI on a dev terminal, clean bytes on a production pipe. An object/array passed as the MESSAGE is still JSON-encoded INLINE inside the text line (never `[object Object]`). **`TINA4_LOG_STRICT`:** truthy makes a log-write failure THROW instead of being swallowed (default off — logging must never crash an app that did not ask for it). **Every `TINA4_LOG_*` var is read LAZILY on each call**, so a script, worker, CLI tool or test that logs without booting a server still gets the operator's configuration; `Log.configure()` remains an explicit override.
132
131
  - `rateLimiter.ts` — Rate limiting middleware
133
132
  - `dotenv.ts` — `.env` file loading
134
133
  - `health.ts` — Health check endpoint
135
- - `scss.ts` SCSS compilation
134
+ - SCSS compilation is owned by the `tina4` Rust CLI (grass), not the framework
136
135
  - `messenger.ts` — Messaging system
137
136
  - `service.ts` — Service layer helpers
138
137
  - `wsdl.ts` — WSDL / SOAP support. **Hardening:** a SOAP message containing a `<!DOCTYPE>` is rejected with a `Client` fault ("DOCTYPE declarations are not allowed in SOAP messages") BEFORE the body is parsed and the operation never runs — SOAP 1.1 forbids DTDs and this closes the XML entity-expansion (billion-laughs) / external-entity (XXE) surface (defence in depth — the hand-rolled parser is already immune). `convertValue` for an `int`/`float`/`integer`/`double`/`number` param throws on a non-numeric value (matching Python's `int()`/`float()` raise) so it becomes a `Server` fault instead of a silent `NaN`. An operation that throws is logged via `Log.error`; the real cause reaches the client **only** under `TINA4_DEBUG` (`isDebugMode()`), else a generic `Internal server error`.
@@ -157,7 +156,7 @@ Database layer with auto-CRUD generation, seeding, fake data, and SQL translatio
157
156
  - `seeder.ts` — Database seeding (`seedTable` raw SQL, `seedOrm` model-based, `seedModels` FK-ordered batch). All return a `SeedSummary { seeded, failed, errors }`; per-row failures are logged + counted + skipped (`strict` re-raises). Options: `{ overrides, clear, seed, strict }`.
158
157
  - `sqlTranslator.ts` — Cross-engine SQL translator (`SQLTranslator`) and TTL query cache (`QueryCache`)
159
158
  - **Instance methods:** `save(): this|false` (fluent, false on failure), `delete()`, `forceDelete()`, `restore()`, `load(sql, params?, include?): boolean`, `validate(): string[]`, `toDict(include?)`, `toAssoc(include?)`, `toObject()`, `toArray(): unknown[]`, `toList()`, `toJson(include?)`, `hasOne(class, fk)`, `hasMany(class, fk, limit?, offset?)`, `belongsTo(class, fk)`
160
- - **Static methods:** `find(id, include?)`, `findById(id, include?)`, `findOrFail(id)`, `create(data)`, `all(where?, params?, include?)`, `select(sql, params?)`, `selectOne(sql, params?, include?)`, `where(conditions, params?, limit?, offset?, include?)`, `count(conditions?, params?)`, `withTrashed(conditions?, params?, limit?, offset?)`, `scope(name, filterSql, params?)` (registers reusable method), `createTable()`, `query()`, `_processForeignKeys()`, `_applyFkRegistry()`
159
+ - **Static methods:** `find(id, include?)`, `findById(id, include?)`, `findOrFail(id)`, `create(data)`, `all(limit=100, offset=0, include?, orderBy?)`, `select(sql, params?, limit=100, offset=0)`, `selectOne(sql, params?, include?)`, `where(conditions, params?, limit=100, offset=0, include?, orderBy?)`, `count(conditions?, params?)`, `withTrashed(conditions?, params?, limit=100, offset=0)`, `scope(name, filterSql, params?)` (registers reusable method), `createTable()`, `query()`, `_processForeignKeys()`, `_applyFkRegistry()`
161
160
  - **Foreign key auto-wire:** Declare a field with `type: "foreignKey"` and `references: "ModelName"` to auto-wire both `belongsTo` on the declaring model and `hasMany` on the referenced model. Optional `relatedName` overrides the has-many key. Models must be registered via `BaseModel.registerModel(name, class)` for name-based resolution. Example: `user_id: { type: "foreignKey", references: "User" }` → `post.belongsTo(User, "user_id")` and `user.hasMany(Post, "user_id")` both resolve without extra wiring.
162
161
  - QueryBuilder supports `toMongo()` for generating MongoDB query documents from the same fluent API
163
162
  - `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).
@@ -225,7 +224,27 @@ session.getSessionId(): string | null
225
224
  session.gc(): void
226
225
  ```
227
226
 
228
- Backends: file, redis, redis-npm, valkey, mongodb, database.
227
+ Backends: file, redis, valkey, mongodb, database, memcached.
228
+
229
+ **Breaking (3.13.95): the mongodb backend's default database is now `tina4`.** With
230
+ `TINA4_SESSION_MONGO_DB` unset it was `tina4_sessions` in Node and `tina4` in
231
+ tina4-python, tina4-php and tina4-ruby, so the same `.env` put Node's sessions in a
232
+ different database from the other three — identical configuration, different
233
+ observable outcome. What an operator sees on upgrade: current sessions are not
234
+ found, so users log in once more. Set `TINA4_SESSION_MONGO_DB=tina4_sessions` to
235
+ keep the old database. There is deliberately no fallback read: a session store is
236
+ ephemeral (everything in it carries a TTL, default 3600s), so the impact self-heals
237
+ within one session lifetime and a fallback would double a read path forever.
238
+
239
+ **`redis-npm` was removed on 2026-07-31.** It was a Node-only backend name that drove
240
+ Redis through the optional `redis` npm package. Python and Ruby also prefer that
241
+ driver when installed, but they choose it inside their single `redis` handler; only
242
+ Node exposed it as a selectable backend, and only Node's copy still ran
243
+ `execFileSync` per command instead of the persistent worker connection every other
244
+ handler moved to. Use `redis` — same backend, same `TINA4_SESSION_REDIS_*`
245
+ settings, faster transport. Setting `TINA4_SESSION_BACKEND=redis-npm` now **throws**
246
+ rather than falling through to the `file` default, because a silent demotion to disk
247
+ would log every user out on deploy and look like an outage.
229
248
 
230
249
  **Backend-failure policy (all 4 frameworks): log-loud + degrade.** A backend (Redis/Valkey/Mongo/DB) that becomes unreachable mid-request is logged via `Log.error` and degraded rather than crashing the app or losing data silently. The external handlers now **throw** a transport error on an unreachable server (previously they swallowed it to an empty string — silent data loss); the `Session` boundary catches it: a read failure yields an empty session (the request still serves), and `save()` returns `false` (best-effort, dirty flag retained for a later retry). A genuine key/doc miss still returns empty **without** logging — empty is not a failure. Set `TINA4_SESSION_STRICT=true` to re-throw instead. Call `regenerate()` right after a successful login or privilege change to defeat session fixation.
231
250
 
@@ -240,20 +259,23 @@ db.cacheStats(): { enabled, size, ttl } // synchronous
240
259
 
241
260
  ### DocStore — pymongo-style document store (zero-config SQLite fallback)
242
261
 
243
- `getCollection(name)` (from `@tina4/orm`) returns a Mongo-style collection. When a Mongo URI is configured it is a real Mongo collection (resolved lazily, returns a Promise); otherwise it is a `SqliteCollection` backed by a local SQLite file (`node:sqlite`, JSON1) and is synchronous. The call sites are identical either way — only the backend differs — so you develop against a zero-dependency local store and switch to MongoDB in production by setting one env var. Because `node:sqlite` is synchronous, `getCollection` is sync in the serverless path and returns a Promise only on the real-Mongo path.
262
+ `getCollection(name)` (from `@tina4/orm`) returns a Mongo-style collection. When a Mongo URI is configured it is a real Mongo collection; otherwise it is a `SqliteCollection` backed by a local SQLite file (`node:sqlite`, JSON1). The call sites are identical either way — only the backend differs — so you develop against a zero-dependency local store and switch to MongoDB in production by setting one env var.
263
+
264
+ **The API is ASYNC on both providers (ADR-0025).** `getCollection` and every collection method return a Promise; `find()` is sync and returns a cursor whose `toArray()` is async, exactly matching the MongoDB driver. `node:sqlite` is synchronous underneath, but the SHAPE never changes with the provider — before 3.13.95 the fallback was fully sync, so identical source changed TYPE when `TINA4_MONGO_URI` was set, and because a Promise is always truthy, `if (doc)` succeeded for a document that did not exist.
244
265
 
245
266
  ```typescript
246
267
  import { getCollection, isServerless, ObjectId } from "@tina4/orm";
247
268
 
248
- const orders = getCollection("orders") as any; // SqliteCollection in serverless mode
249
- const res = orders.insertOne({ customer_id: 1, total: 9.99, status: "new" });
250
- orders.findOne({ _id: res.insertedId });
251
- orders.updateOne({ _id: res.insertedId }, { $set: { status: "shipped" } });
252
- for (const doc of orders.find({ total: { $gt: 5 } }).sort("total", -1).limit(10)) {
269
+ const orders = (await getCollection("orders")) as any;
270
+ const res = await orders.insertOne({ customer_id: 1, total: 9.99, status: "new" });
271
+ await orders.findOne({ _id: res.insertedId });
272
+ await orders.updateOne({ _id: res.insertedId }, { $set: { status: "shipped" } });
273
+ // for await a real FindCursor has Symbol.asyncIterator only, never Symbol.iterator
274
+ for await (const doc of orders.find({ total: { $gt: 5 } }).sort("total", -1).limit(10)) {
253
275
  // ...
254
276
  }
255
- orders.countDocuments({ status: "shipped" });
256
- isServerless(); // true when running on the SQLite fallback
277
+ await orders.countDocuments({ status: "shipped" });
278
+ isServerless(); // true when running on the SQLite fallback (sync — reads config only)
257
279
  ```
258
280
 
259
281
  Filter operators: equality, `$in`, `$nin`, `$gt`, `$gte`, `$lt`, `$lte`, `$ne`, `$exists`, `$regex`, implicit AND, `$or`, `$and`, and dotted nested keys (`addr.city`). Updates: `$set`, `$unset`, `$inc`, replace, upsert. Cursors: `sort`, `limit`, `skip`, projection. Values round-trip (Date to/from ISO-8601, `ObjectId` to/from 24-hex) and stay queryable via `json_extract`. Non-goals: aggregation pipelines, `$elemMatch`, geo queries.
@@ -262,6 +284,8 @@ Selection and configuration:
262
284
  - `TINA4_MONGO_URI` — app-wide Mongo URI. Falls back to `TINA4_SESSION_MONGO_URI`, then the legacy `TINA4_SESSION_MONGO_URL`. When one is set, `getCollection` returns a real Mongo collection.
263
285
  - `TINA4_DOC_STORE_PATH` — SQLite file for the fallback store (default `data/tina4_docstore.db`).
264
286
 
287
+ **One client per (uri, database), and a way to close it.** `getCollection` caches the connected Mongo client rather than building a new one per call — before 3.13.95 it constructed a `new MongoClient` on EVERY call and never closed it, so 20 calls left 40 server connections open and the count grew without bound (invisible locally, because the SQLite fallback opens no connections at all). `await closeDocStore()` closes every Mongo client and the SQLite store; a pooled client keeps the event loop alive, so a script or test that touched the real provider needs it to exit.
288
+
265
289
  ### Request extras
266
290
 
267
291
  ```typescript
@@ -627,6 +651,9 @@ import { initDatabase, bindDatabase, createAdapterFromUrl, Database, DatabaseRes
627
651
  const db = await initDatabase({ url: "sqlite:///app.db" });
628
652
  // Connection pooling: pass `pool: 4` for round-robin connections.
629
653
 
654
+ // db.fetch() caps at 100 rows when no limit is passed (one number across all
655
+ // four frameworks). db.fetchAll() deliberately does NOT inherit the cap -- its
656
+ // name is the request for every row -- so it routes around fetch()'s default.
630
657
  // EVERY db method that touches the database is ASYNC on the Database wrapper --
631
658
  // it returns a Promise, so `await` it. (The node:sqlite ADAPTER underneath is
632
659
  // synchronous, but the wrapper is async so the query cache and the pg/mysql/
@@ -634,7 +661,7 @@ const db = await initDatabase({ url: "sqlite:///app.db" });
634
661
  // and close() are synchronous.
635
662
 
636
663
  // Reads — async, await them
637
- await db.fetch(sql, params?, limit?, offset?): Promise<DatabaseResult> // .records, .count, .limit, .offset
664
+ await db.fetch(sql, params?, limit?, offset?): Promise<DatabaseResult> // limit defaults to DEFAULT_ROW_CAP (100) // .records, .count, .limit, .offset
638
665
  await db.fetchOne<T>(sql, params?): Promise<T | null>
639
666
 
640
667
  // Writes — execute() RAISES on a SQL error (bad SQL, constraint violation,
@@ -773,8 +800,8 @@ User.find(id, include?);
773
800
  User.findById(id, include?);
774
801
  User.findOrFail(id); // throws if missing
775
802
  User.create(data); // construct + save
776
- User.all(where?, params?, include?);
777
- User.select(sql, params?);
803
+ User.all(limit?, offset?, include?, orderBy?); // limit defaults to 100; NO filter -- use where()
804
+ User.select(sql, params?, limit?, offset?); // limit defaults to 100
778
805
  User.selectOne(sql, params?, include?);
779
806
  User.where(conditions, params?, limit?, offset?, include?, orderBy?);
780
807
  User.count(conditions?, params?);
@@ -812,7 +839,7 @@ const orders = QueryBuilder.fromTable("orders o")
812
839
  .where("o.status = ?", ["pending"])
813
840
  .orderBy("o.created_at DESC")
814
841
  .limit(20)
815
- .get(); // → row[]
842
+ .get(); // → DatabaseResult (.records, .count, .limit, .offset)
816
843
 
817
844
  // LEFT JOIN
818
845
  QueryBuilder.fromTable("products p")
@@ -965,6 +992,10 @@ queue.purge("completed");
965
992
  queue.retryFailed();
966
993
  queue.deadLetters();
967
994
  queue.produce("notifications", payload, 0, 0);
995
+ // Release the backend connection. Idempotent; discard the queue afterwards.
996
+ // Node caveat: neither reachable backend holds a connection between calls today
997
+ // (ADR-0022 child-process design), so this releases nothing YET - it is the contract.
998
+ queue.close();
968
999
 
969
1000
  // Job methods
970
1001
  job?.complete();
@@ -984,6 +1015,37 @@ for await (const job of queue.consume("emails")) {
984
1015
  // pollInterval=0 for single-pass drain (tests).
985
1016
  ```
986
1017
 
1018
+ ## Graceful shutdown (`packages/core/src/server.ts`)
1019
+
1020
+ `startServer()` owns signal handling. It is the ONLY place that registers
1021
+ `process.on("SIGTERM"/"SIGINT")` — `background.ts` and the CLI's `serve.ts`
1022
+ deliberately register none.
1023
+
1024
+ On SIGTERM or SIGINT it: stops background tasks, sends RFC 6455 close code
1025
+ **1001 ("going away")** to every live WebSocket, closes the listeners so new
1026
+ connections get a clean refusal, waits for in-flight requests to finish (up to
1027
+ `TINA4_SHUTDOWN_TIMEOUT`), closes the database, and exits **0**.
1028
+
1029
+ | Env var | Default | Purpose |
1030
+ | --- | --- | --- |
1031
+ | `TINA4_SHUTDOWN_TIMEOUT` | `30` | Seconds to wait for in-flight requests before force-closing them. Matches Kubernetes' default `terminationGracePeriodSeconds` and Gunicorn's `graceful_timeout`, and is the same env var and default as tina4-python / tina4-php / tina4-ruby. A non-numeric or negative value warns and falls back to 30. |
1032
+ | `TINA4_DEFAULT_WEBSERVER` | unset | **Accepted and ignored in Node.** `TRUE` pins the built-in server. Node has only one server (`node:http`), so there is nothing to switch and this is a genuine no-op. It exists here so the env surface is identical across all four frameworks: in tina4-python it forces the built-in asyncio server instead of uvicorn/hypercorn/granian, and in tina4-ruby it forces WEBrick instead of Puma. Setting it must never be an error. |
1033
+
1034
+ **SIGHUP is deliberately NOT trapped** — the default disposition terminates the
1035
+ process. The Rust CLI owns file watching and production logs go to stdout, so
1036
+ neither Puma's log-reopen nor gunicorn's config-reload use for SIGHUP applies.
1037
+ `test/gracefulShutdown.test.ts` pins this so it is not restored by accident.
1038
+
1039
+ **Never register a signal handler that does not exit.** Adding any listener for
1040
+ SIGTERM REPLACES Node's default disposition, so a handler that only cleans up
1041
+ does not "add" to the default, it CANCELS it — the process then ignores SIGTERM
1042
+ and runs until SIGKILL. `background.ts` shipped exactly that bug: a server with
1043
+ one registered `background()` task hung forever on SIGTERM, burning the whole
1044
+ Kubernetes grace period on every rolling deploy.
1045
+
1046
+ Set `terminationGracePeriodSeconds` ABOVE `TINA4_SHUTDOWN_TIMEOUT` in your pod
1047
+ spec so the drain finishes before SIGKILL.
1048
+
987
1049
  ## Module: Background Tasks (`packages/core/src/background.ts`)
988
1050
 
989
1051
  Periodic callbacks that run alongside the HTTP server. Use this instead of bare `setInterval` so timers integrate with the server lifecycle and clear on graceful shutdown.
@@ -1001,11 +1063,11 @@ background(async () => {
1001
1063
  }, 30);
1002
1064
 
1003
1065
  task.stop(); // stop just this one
1004
- stopAllBackgroundTasks(); // stop everything (also runs on SIGTERM/SIGINT)
1066
+ stopAllBackgroundTasks(); // stop everything (the server's shutdown calls this on SIGTERM/SIGINT)
1005
1067
  backgroundTaskCount(); // test helper
1006
1068
  ```
1007
1069
 
1008
- **Never use bare `setInterval` for periodic work in a Tina4 app.** `background()` catches errors, integrates with shutdown signals, calls `timer.unref()` so it doesn't block process exit, and matches Python's `background()` API exactly.
1070
+ **Never use bare `setInterval` for periodic work in a Tina4 app.** `background()` catches errors, is cleared by the server's graceful shutdown (which owns the signal handlers), calls `timer.unref()` so it doesn't block process exit, and matches Python's `background()` API exactly.
1009
1071
 
1010
1072
  ## Module: DI Container (`packages/core/src/container.ts`)
1011
1073
 
@@ -1161,8 +1223,15 @@ Set `TINA4_DATABASE_URL` in your `.env` file using `driver://host:port/database`
1161
1223
 
1162
1224
  ```bash
1163
1225
  # SQLite (default if nothing configured)
1164
- TINA4_DATABASE_URL=sqlite:///path/to/db.sqlite
1226
+ # Slash count decides relative vs absolute (the SQLAlchemy convention,
1227
+ # identical in all four frameworks). THREE slashes is RELATIVE to the working
1228
+ # directory; an absolute path needs FOUR.
1229
+ TINA4_DATABASE_URL=sqlite:///app.db # relative: ./app.db
1230
+ TINA4_DATABASE_URL=sqlite:////var/data/app.db # absolute: /var/data/app.db
1231
+ TINA4_DATABASE_URL=sqlite:/var/data/app.db # absolute (one slash) too
1165
1232
  TINA4_DATABASE_URL=sqlite://./data/tina4.db
1233
+ # FOOTGUN: "sqlite://" + an absolute path yields THREE slashes, so the file is
1234
+ # created UNDER the working directory and a stray ./var/data/ tree appears.
1166
1235
 
1167
1236
  # PostgreSQL
1168
1237
  TINA4_DATABASE_URL=postgres://localhost:5432/mydb
@@ -1194,6 +1263,64 @@ TINA4_DATABASE_PASSWORD=mypass
1194
1263
 
1195
1264
  Credential priority: `config.user` > `config.username` > `TINA4_DATABASE_USERNAME` env var.
1196
1265
 
1266
+ ### Connect timeout
1267
+
1268
+ `TINA4_DATABASE_CONNECT_TIMEOUT` bounds every database connect attempt. Same name,
1269
+ unit, default and semantics in all four frameworks.
1270
+
1271
+ | | |
1272
+ |---|---|
1273
+ | unit | **seconds** |
1274
+ | default | `10` |
1275
+ | `<= 0` | disables the bound (unbounded — each driver keeps exactly its old behaviour) |
1276
+ | garbage | warns and uses `10` |
1277
+ | on expiry | throws, naming the host, the port, the elapsed seconds, and this variable |
1278
+
1279
+ Without it a driver that never calls back hangs the app on connect with no log, no
1280
+ error and no signal — measured on the Firebird adapter at 16 minutes, 0.0% CPU. It
1281
+ is applied in two layers: the driver's own knob where it has one (so one variable
1282
+ really governs, rather than tedious's 15s or Mongo's 30s quietly winning), and an
1283
+ outer bound around the whole attempt (so it exists at all for `node-firebird`,
1284
+ which has no knob, and so a knob that covers only part of the handshake cannot
1285
+ leave a gap).
1286
+
1287
+ **The driver's timer is meant to WIN, and Tina4 translates what it raises.** The
1288
+ knob is set to the bound EXACTLY (rounded up to whole milliseconds, floored at 1 —
1289
+ three of the four knobs read `0` as *wait forever*), Tina4's own clock starts
1290
+ before the driver arms its timer, and a driver failure that took at least that long
1291
+ is re-thrown in the framework's words with the driver's error preserved as `cause`:
1292
+
1293
+ ```
1294
+ Database connect to 127.0.0.1:34467 timed out after 2.0s
1295
+ (TINA4_DATABASE_CONNECT_TIMEOUT=2 seconds; set it to 0 to wait indefinitely).
1296
+ Driver reported: timeout expired
1297
+ ```
1298
+
1299
+ That is real output, captured against a server that accepts and never replies.
1300
+ Each driver contributes its own words — pg `timeout expired`, mysql2 `connect
1301
+ ETIMEDOUT`, tedious `Failed to connect to 127.0.0.1:34467 in 2000ms`, Mongo
1302
+ `Server selection timed out after 2000 ms` — and the last two quote the budget
1303
+ they were GIVEN, which is how you can see that `2` means 2000ms and not 3000ms.
1304
+ Firebird and ODBC have no knob, so the outer bound fires instead and there is no
1305
+ `Driver reported:` clause to add.
1306
+
1307
+ `N` means `N`. Whether a failure was the bound expiring is decided by **elapsed
1308
+ time**, never by matching the driver's error text — the four clients word it four
1309
+ different ways and a marker table would drift and miss. A 50ms tolerance absorbs
1310
+ libuv's timer granularity without inflating `N`. The outer bound remains as the
1311
+ backstop for the phases a knob does not cover and the adapters that have none;
1312
+ being armed after the driver's, it only fires when the driver's did not.
1313
+
1314
+ | Adapter | Can connect block? | Bounded now | How |
1315
+ |---------|--------------------|-------------|-----|
1316
+ | Firebird | yes — **the measured 16-minute hang** | ✅ | outer bound only (`node-firebird` has no timeout option) |
1317
+ | PostgreSQL | yes — pg's `connectionTimeoutMillis` defaults to `0`, no timeout | ✅ | `connectionTimeoutMillis` + outer bound |
1318
+ | MySQL | bounded at mysql2's own 10s default | ✅ | `connectTimeout` + outer bound |
1319
+ | MSSQL | bounded at tedious's own 15s default | ✅ | `connectTimeout` + outer bound |
1320
+ | MongoDB | bounded at the driver's own 30s default | ✅ | `serverSelectionTimeoutMS` + `connectTimeoutMS` + outer bound |
1321
+ | ODBC | yes — a raw driver string carries no timeout | ⚠️ caller only | outer bound; the blocked native thread cannot be cancelled from JS |
1322
+ | SQLite | no network; opens in a **synchronous** constructor | ❌ N/A | a sync call cannot be interrupted by a timer on the same thread, and there is no host/port to name |
1323
+
1197
1324
  ### Programmatic configuration
1198
1325
  ```typescript
1199
1326
  import { initDatabase } from "@tina4/orm";
@@ -1219,9 +1346,10 @@ Run tests with:
1219
1346
  npm test
1220
1347
  ```
1221
1348
 
1222
- This executes `test/run-all.ts` which runs all 43 test files:
1349
+ This executes `test/run-all.ts`, which ran **262 files** in the last lab verification:
1223
1350
  - `test/integration.ts` — Full integration test (creates a temp project, starts a real server, runs assertions)
1224
- - `test/*.test.ts` — 42 individual test files covering all subsystems (ORM, routing, middleware, database drivers, sessions, queues, WebSocket, GraphQL, i18n, etc.)
1351
+ - `test/*.test.ts` — 267 files on disk covering all subsystems (ORM, routing, middleware, database drivers, sessions, queues, WebSocket, GraphQL, i18n, etc.). 259 are spawned by the runner under `tsx`, plus `integration.ts`; the 2 i18n suites are vitest and the runner drives them itself (so no invoker can miss them); the 6 `metrics*` files need the Rust CLI's native engine and are opted in with `TINA4_SKIP_METRICS=0`.
1352
+ - `test/_*.ts` are helpers, not suites, so the runner never collects them: `_serviceGate.ts` (the require-services gate), `_testSummary.ts`, and `_driverlessTree.ts` (builds a tree where a bare specifier genuinely cannot resolve).
1225
1353
 
1226
1354
  **Always run tests after making changes.** All tests must pass.
1227
1355
 
@@ -1257,7 +1385,7 @@ When adding new features, add a corresponding `test/<feature>.test.ts` file.
1257
1385
  ## v3 Features Summary
1258
1386
 
1259
1387
  - **98 built-in features**, zero third-party dependencies
1260
- - **6,230 tests** passing, 0 failed, across 198 files (build + typecheck green) - measured 2026-07-29 on Ubuntu 24.04.4 LTS x86_64, Node 24.18.0, live services, TINA4_REQUIRE_SERVICES=1; Firebird excluded by design
1388
+ - **7,537 tests** passing, 0 failed, **0 skipped** across 262 files (typecheck exit 0) - measured 2026-08-06 on the lab host (Ubuntu 24.04.4 LTS x86_64, Node v24.18.0) against live services with TINA4_REQUIRE_SERVICES=1. **Firebird is NOT excluded** - `test/firebird*.test.ts` runs against a REAL Firebird 5. The last six skips are closed, each by giving it the environment its condition needs rather than by relaxing what it asserts: the four missing-driver cases run in a child process that genuinely cannot resolve the driver (the source is copied out of the repo to a tree with no `node_modules` above it and run with plain `node` - `test/_driverlessTree.ts`, no resolver shim); the strict-mode write failure drops the effective uid so a 0400 file really denies root; and the RabbitMQ default-config round-trip clears the per-framework isolation overrides for that one case so "default" means the default. Every one was proven able to FAIL by mutating the code it guards.
1261
1389
  - **Race-safe `getNextId()`** with atomic sequence table (`tina4_sequences`) for SQLite/MySQL/MSSQL; PostgreSQL auto-creates sequences
1262
1390
  - **Frond template engine optimizations**: pre-compiled regexes, lazy loop context (copy-on-write), filter chain caching, path split caching, inline common filters (11-15% speedup)
1263
1391
  - **Production server auto-detect**: `npx tina4nodejs serve --production` auto-uses cluster mode
@@ -1267,7 +1395,7 @@ When adding new features, add a corresponding `test/<feature>.test.ts` file.
1267
1395
  - **Sessions**: file backend (default). `TINA4_SESSION_SAMESITE` env var (default: Lax)
1268
1396
  - **Queue**: file/RabbitMQ/Kafka/MongoDB backends, configured via env vars. **Reservation/visibility timeout** (file + MongoDB): a popped job is reserved for `TINA4_QUEUE_VISIBILITY_TIMEOUT` seconds (default 300; `visibilityTimeout` Queue option; `<= 0` disables) — if the consumer dies before `complete()`/`fail()`, the next `pop()` reclaims it (incrementing `attempts`, dead-lettering past `maxRetries`), so a crashed/evicted consumer never strands a job. RabbitMQ/Kafka delegate redelivery to the broker.
1269
1397
  - **Cache**: memory/Redis/file backends
1270
- - **Messenger**: .env driven SMTP/IMAP
1398
+ - **Messenger**: .env driven SMTP/IMAP. **Cross-framework contract:** `inbox(folder = "INBOX", limit = 20, offset = 0)` takes the folder FIRST (same order in all four frameworks — Node was the outlier at `(limit, offset, folder)` before 3.13.95); `uid` is a **string** everywhere; `read(uid, folder?)` returns `ImapFullMessage | null` and a non-existent UID reads as **`null`** — falsy, so `if (!message)` is the portable missing-message check, matching Python's `{}`, PHP's `null` and Ruby's `nil`. A missing UID is NOT an error and never throws. IMAP reads DO fail loud on a connection/auth/protocol failure: `inbox`/`read`/`unread`/`search`/`folders` all raise `MessengerConnectionError` rather than swallowing it into an empty result. Real SMTP + IMAP round-trips are covered against a live GreenMail in `test/messengerGreenMail.test.ts` (ports 3025/3143; `TINA4_TEST_SMTP_*` / `TINA4_TEST_IMAP_*` to relocate)
1271
1399
  - **ORM relationships**: `hasMany`, `hasOne`, `belongsTo` with eager loading (`include`)
1272
1400
  - **Frond pre-compilation**: 2.8x template render improvement
1273
1401
  - **QueryBuilder** with NoSQL/MongoDB support (`toMongo()`)
package/README.md CHANGED
@@ -59,7 +59,7 @@ export default class User {
59
59
  | **Core HTTP** (7) | Router with path params (`{id:int}`, `{p:path}`), Server, Request/Response, Middleware pipeline, Static file serving, CORS |
60
60
  | **Database** (6) | SQLite, PostgreSQL, MySQL, MSSQL, Firebird: unified adapter, connection pooling, query cache, transactions, race-safe ID generation, SQL dialect translation |
61
61
  | **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 |
62
- | **Auth & Security** (5) | JWT (HS256/RS256), password hashing (PBKDF2-SHA256), API key validation, rate limiting, CSRF form tokens |
62
+ | **Auth & Security** (5) | JWT (HS256/HS384/HS512 standard, RS256 opt-in, all from builtin `node:crypto`), password hashing (PBKDF2-SHA256), API key validation, rate limiting, CSRF form tokens |
63
63
  | **Templating** (3) | Frond engine (Twig/Jinja2-compatible, pre-compiled 2.8× faster), SCSS auto-compilation, built-in CSS (~24 KB) |
64
64
  | **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) |
65
65
  | **Background** (3) | Job queue (File/RabbitMQ/Kafka/MongoDB) with priority, delay, retry, dead letters; service runner; event system (on/emit/once/off) |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tina4-nodejs",
3
- "version": "3.13.94",
3
+ "version": "3.13.96",
4
4
  "type": "module",
5
5
  "description": "Tina4 for Node.js/TypeScript - 54 built-in features, zero dependencies",
6
6
  "keywords": [
@@ -94,10 +94,12 @@
94
94
  "redis": "^4.7.0"
95
95
  },
96
96
  "devDependencies": {
97
+ "@apidevtools/swagger-parser": "^12.1.0",
97
98
  "@types/node": "^22.10.0",
98
99
  "@types/pg": "^8.20.0",
99
100
  "esbuild": "^0.24.0",
100
101
  "mongodb": "^6.0.0",
102
+ "node-firebird": "^2.14.3",
101
103
  "tsx": "^4.19.0",
102
104
  "typescript": "^5.7.0",
103
105
  "vitest": "^4.1.9"