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.
- package/CLAUDE.md +158 -30
- package/README.md +1 -1
- package/package.json +3 -1
- package/packages/cli/dist/bin.js +30911 -28444
- package/packages/cli/src/commands/metrics.ts +17 -11
- package/packages/cli/src/commands/serve.ts +10 -9
- package/packages/core/dist/index.js +30810 -28261
- package/packages/core/public/css/tina4.min.css +1 -1
- package/packages/core/src/ai.ts +7 -1
- package/packages/core/src/auth.ts +191 -39
- package/packages/core/src/background.ts +19 -19
- package/packages/core/src/cache.ts +492 -49
- package/packages/core/src/devAdmin.ts +79 -32
- package/packages/core/src/dispatchPipeline.ts +285 -0
- package/packages/core/src/dotenv.ts +185 -40
- package/packages/core/src/index.ts +6 -7
- package/packages/core/src/logger.ts +257 -36
- package/packages/core/src/mcp.ts +1 -1
- package/packages/core/src/messenger.ts +294 -106
- package/packages/core/src/metrics.ts +199 -961
- package/packages/core/src/middleware.ts +390 -123
- package/packages/core/src/queue.ts +188 -32
- package/packages/core/src/queueBackends/kafkaBackend.ts +1 -1
- package/packages/core/src/queueBackends/liteBackend.ts +13 -0
- package/packages/core/src/queueBackends/mongoBackend.ts +101 -9
- package/packages/core/src/queueBackends/rabbitmqBackend.ts +22 -4
- package/packages/core/src/rateLimiter.ts +10 -5
- package/packages/core/src/request.ts +34 -16
- package/packages/core/src/response.ts +46 -1
- package/packages/core/src/router.ts +29 -4
- package/packages/core/src/server.ts +886 -421
- package/packages/core/src/session.ts +244 -27
- package/packages/core/src/sessionHandlers/databaseHandler.ts +338 -48
- package/packages/core/src/sessionHandlers/memcachedHandler.ts +181 -0
- package/packages/core/src/sessionHandlers/mongoClient.ts +293 -208
- package/packages/core/src/sessionHandlers/mongoHandler.ts +88 -8
- package/packages/core/src/sessionHandlers/respClient.ts +16 -147
- package/packages/core/src/sessionHandlers/sqlClient.ts +290 -0
- package/packages/core/src/sessionHandlers/syncBridge.ts +190 -0
- package/packages/core/src/sessionHandlers/syncSocket.ts +236 -0
- package/packages/core/src/testClient.ts +18 -5
- package/packages/core/src/trustedProxy.ts +249 -0
- package/packages/core/src/types.ts +29 -5
- package/packages/core/src/websocket.ts +66 -0
- package/packages/orm/dist/index.js +22717 -20168
- package/packages/orm/src/adapters/firebird.ts +183 -56
- package/packages/orm/src/adapters/mongodb.ts +25 -4
- package/packages/orm/src/adapters/mssql.ts +114 -29
- package/packages/orm/src/adapters/mysql.ts +103 -40
- package/packages/orm/src/adapters/odbc.ts +44 -21
- package/packages/orm/src/adapters/postgres.ts +118 -26
- package/packages/orm/src/adapters/sqlDialect.ts +120 -0
- package/packages/orm/src/adapters/sqlite.ts +60 -24
- package/packages/orm/src/autoCrud.ts +12 -10
- package/packages/orm/src/baseModel.ts +135 -40
- package/packages/orm/src/cachedDatabase.ts +43 -19
- package/packages/orm/src/connectTimeout.ts +265 -0
- package/packages/orm/src/database.ts +241 -197
- package/packages/orm/src/databaseResult.ts +51 -28
- package/packages/orm/src/databaseUrl.ts +484 -0
- package/packages/orm/src/docstore.ts +386 -145
- package/packages/orm/src/index.ts +13 -6
- package/packages/orm/src/migration.ts +44 -11
- package/packages/orm/src/model.ts +4 -0
- package/packages/orm/src/queryBuilder.ts +47 -6
- package/packages/orm/src/sqlTranslator.ts +310 -4
- package/packages/orm/src/types.ts +21 -77
- package/packages/swagger/dist/index.js +78 -20
- package/packages/swagger/src/generator.ts +172 -29
- package/types/core/src/ai.d.ts +1 -1
- package/types/core/src/auth.d.ts +28 -5
- package/types/core/src/background.d.ts +3 -3
- package/types/core/src/cache.d.ts +15 -12
- package/types/core/src/dispatchPipeline.d.ts +117 -0
- package/types/core/src/dotenv.d.ts +38 -16
- package/types/core/src/index.d.ts +6 -9
- package/types/core/src/logger.d.ts +93 -16
- package/types/core/src/messenger.d.ts +47 -6
- package/types/core/src/metrics.d.ts +25 -61
- package/types/core/src/middleware.d.ts +134 -11
- package/types/core/src/queue.d.ts +54 -5
- package/types/core/src/queueBackends/kafkaBackend.d.ts +1 -1
- package/types/core/src/queueBackends/liteBackend.d.ts +9 -0
- package/types/core/src/queueBackends/mongoBackend.d.ts +24 -2
- package/types/core/src/queueBackends/rabbitmqBackend.d.ts +3 -3
- package/types/core/src/router.d.ts +14 -3
- package/types/core/src/server.d.ts +15 -4
- package/types/core/src/session.d.ts +87 -2
- package/types/core/src/sessionHandlers/databaseHandler.d.ts +60 -5
- package/types/core/src/sessionHandlers/memcachedHandler.d.ts +60 -0
- package/types/core/src/sessionHandlers/mongoClient.d.ts +16 -5
- package/types/core/src/sessionHandlers/mongoHandler.d.ts +51 -3
- package/types/core/src/sessionHandlers/respClient.d.ts +2 -2
- package/types/core/src/sessionHandlers/sqlClient.d.ts +39 -0
- package/types/core/src/sessionHandlers/syncBridge.d.ts +91 -0
- package/types/core/src/sessionHandlers/syncSocket.d.ts +49 -0
- package/types/core/src/trustedProxy.d.ts +44 -0
- package/types/core/src/types.d.ts +28 -5
- package/types/core/src/websocket.d.ts +26 -0
- package/types/orm/src/adapters/firebird.d.ts +55 -10
- package/types/orm/src/adapters/mongodb.d.ts +2 -2
- package/types/orm/src/adapters/mssql.d.ts +18 -11
- package/types/orm/src/adapters/mysql.d.ts +11 -10
- package/types/orm/src/adapters/odbc.d.ts +9 -12
- package/types/orm/src/adapters/postgres.d.ts +11 -10
- package/types/orm/src/adapters/sqlDialect.d.ts +71 -0
- package/types/orm/src/adapters/sqlite.d.ts +15 -3
- package/types/orm/src/baseModel.d.ts +45 -9
- package/types/orm/src/cachedDatabase.d.ts +18 -5
- package/types/orm/src/connectTimeout.d.ts +100 -0
- package/types/orm/src/database.d.ts +78 -28
- package/types/orm/src/databaseResult.d.ts +29 -15
- package/types/orm/src/databaseUrl.d.ts +125 -0
- package/types/orm/src/docstore.d.ts +102 -43
- package/types/orm/src/index.d.ts +6 -4
- package/types/orm/src/migration.d.ts +4 -3
- package/types/orm/src/queryBuilder.d.ts +23 -3
- package/types/orm/src/sqlTranslator.d.ts +126 -2
- package/types/orm/src/types.d.ts +21 -38
- package/packages/core/src/scss.ts +0 -623
- package/packages/core/src/sessionHandlers/redisHandler.ts +0 -219
- package/types/core/src/scss.d.ts +0 -19
- 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.
|
|
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.
|
|
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
|
|
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 #
|
|
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:**
|
|
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
|
|
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
|
-
- `
|
|
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(
|
|
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,
|
|
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
|
|
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;
|
|
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
|
|
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(
|
|
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(); // →
|
|
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 (
|
|
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,
|
|
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
|
-
|
|
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
|
|
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` —
|
|
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
|
-
- **
|
|
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.
|
|
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"
|