tina4-nodejs 3.9.3 → 3.10.1
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 +2 -2
- package/package.json +1 -1
- package/packages/core/src/queue.ts +3 -3
- package/packages/core/src/queueBackends/kafkaBackend.ts +2 -1
- package/packages/core/src/queueBackends/rabbitmqBackend.ts +2 -1
- package/packages/core/src/server.ts +18 -6
- package/packages/core/src/session.ts +4 -4
- package/packages/core/src/sessionHandlers/databaseHandler.ts +1 -4
- package/packages/core/src/websocketBackplane.ts +17 -16
- package/packages/orm/src/adapters/firebird.ts +1 -1
- package/packages/orm/src/adapters/mssql.ts +1 -1
- package/packages/orm/src/adapters/mysql.ts +1 -1
- package/packages/orm/src/adapters/postgres.ts +4 -11
- package/packages/orm/src/baseModel.ts +33 -0
- package/packages/orm/src/index.ts +1 -1
package/CLAUDE.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
# CLAUDE.md — AI Developer Guide for tina4-nodejs (v3.
|
|
1
|
+
# CLAUDE.md — AI Developer Guide for tina4-nodejs (v3.10.1)
|
|
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.
|
|
7
|
+
Tina4 for Node.js/TypeScript v3.10.1 — a convention-over-configuration structural paradigm. **Not a framework.** The developer writes TypeScript; Tina4 is invisible infrastructure.
|
|
8
8
|
|
|
9
9
|
The philosophy: zero ceremony, batteries included, file system as source of truth.
|
|
10
10
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tina4-nodejs",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.10.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "This is not a framework. Tina4 for Node.js/TypeScript — zero deps, 38 built-in features.",
|
|
6
6
|
"keywords": ["tina4", "framework", "web", "api", "orm", "graphql", "websocket", "typescript"],
|
|
@@ -31,6 +31,9 @@
|
|
|
31
31
|
import { mkdirSync, readdirSync, readFileSync, writeFileSync, unlinkSync, existsSync } from "node:fs";
|
|
32
32
|
import { join } from "node:path";
|
|
33
33
|
import { randomUUID } from "node:crypto";
|
|
34
|
+
import { RabbitMQBackend } from "./queueBackends/rabbitmqBackend.js";
|
|
35
|
+
import { KafkaBackend } from "./queueBackends/kafkaBackend.js";
|
|
36
|
+
import { MongoBackend } from "./queueBackends/mongoBackend.js";
|
|
34
37
|
|
|
35
38
|
// ── Types ────────────────────────────────────────────────────
|
|
36
39
|
|
|
@@ -137,13 +140,10 @@ export class Queue {
|
|
|
137
140
|
|
|
138
141
|
// Initialize external backends
|
|
139
142
|
if (this.backendName === "rabbitmq") {
|
|
140
|
-
const { RabbitMQBackend } = require("./queueBackends/rabbitmqBackend.js");
|
|
141
143
|
this.externalBackend = new RabbitMQBackend();
|
|
142
144
|
} else if (this.backendName === "kafka") {
|
|
143
|
-
const { KafkaBackend } = require("./queueBackends/kafkaBackend.js");
|
|
144
145
|
this.externalBackend = new KafkaBackend();
|
|
145
146
|
} else if (this.backendName === "mongodb" || this.backendName === "mongo") {
|
|
146
|
-
const { MongoBackend } = require("./queueBackends/mongoBackend.js");
|
|
147
147
|
this.externalBackend = new MongoBackend();
|
|
148
148
|
}
|
|
149
149
|
}
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* TINA4_KAFKA_GROUP_ID (default: "tina4_consumer_group")
|
|
10
10
|
*/
|
|
11
11
|
import net from "node:net";
|
|
12
|
+
import { execFileSync } from "node:child_process";
|
|
12
13
|
import { randomUUID } from "node:crypto";
|
|
13
14
|
import type { QueueJob } from "../queue.js";
|
|
14
15
|
|
|
@@ -72,7 +73,7 @@ export class KafkaBackend implements QueueBackend {
|
|
|
72
73
|
* Execute a Kafka operation synchronously via a child process.
|
|
73
74
|
*/
|
|
74
75
|
private execSync(operation: string, topic: string, data?: string): string {
|
|
75
|
-
|
|
76
|
+
// execFileSync imported at top level
|
|
76
77
|
const broker = this.parseBroker();
|
|
77
78
|
|
|
78
79
|
const script = `
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* TINA4_RABBITMQ_VHOST (default: "/")
|
|
13
13
|
*/
|
|
14
14
|
import net from "node:net";
|
|
15
|
+
import { execFileSync } from "node:child_process";
|
|
15
16
|
import { randomUUID } from "node:crypto";
|
|
16
17
|
import type { QueueJob } from "../queue.js";
|
|
17
18
|
|
|
@@ -144,7 +145,7 @@ export class RabbitMQBackend implements QueueBackend {
|
|
|
144
145
|
* Execute an AMQP operation synchronously via a child process.
|
|
145
146
|
*/
|
|
146
147
|
private execSync(operation: string, queue: string, data?: string): string {
|
|
147
|
-
|
|
148
|
+
// execFileSync imported at top level
|
|
148
149
|
|
|
149
150
|
const script = `
|
|
150
151
|
const net = require("node:net");
|
|
@@ -3,6 +3,7 @@ import { resolve, dirname, join, relative } from "node:path";
|
|
|
3
3
|
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
4
4
|
import { isatty } from "node:tty";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { execFileSync, exec } from "node:child_process";
|
|
6
7
|
import cluster from "node:cluster";
|
|
7
8
|
import os from "node:os";
|
|
8
9
|
import type { Tina4Config, Tina4Request, Tina4Response } from "./types.js";
|
|
@@ -30,12 +31,15 @@ const BUILTIN_PUBLIC_DIR = resolve(__dirname, "..", "public");
|
|
|
30
31
|
|
|
31
32
|
const TINA4_VERSION = "3.0.0";
|
|
32
33
|
|
|
34
|
+
/** Cache Frond instances by template directory to avoid repeated instantiation. */
|
|
35
|
+
const frondCache = new Map<string, InstanceType<any>>();
|
|
36
|
+
|
|
33
37
|
/**
|
|
34
38
|
* Test-bind each port in a subprocess to find one that is available.
|
|
35
39
|
* Falls back to `start` if none of the candidates work.
|
|
36
40
|
*/
|
|
37
41
|
function findAvailablePort(start: number, maxTries = 10): number {
|
|
38
|
-
|
|
42
|
+
// execFileSync imported at top of file (ESM)
|
|
39
43
|
for (let offset = 0; offset < maxTries; offset++) {
|
|
40
44
|
const port = start + offset;
|
|
41
45
|
try {
|
|
@@ -54,7 +58,7 @@ function findAvailablePort(start: number, maxTries = 10): number {
|
|
|
54
58
|
* Open the user's default browser after a short delay so the server is ready.
|
|
55
59
|
*/
|
|
56
60
|
function openBrowser(url: string) {
|
|
57
|
-
|
|
61
|
+
// exec imported at top of file (ESM)
|
|
58
62
|
setTimeout(() => {
|
|
59
63
|
if (process.platform === "darwin") exec(`open ${url}`);
|
|
60
64
|
else if (process.platform === "win32") exec(`start "" "${url}"`);
|
|
@@ -93,18 +97,26 @@ async function renderErrorPage(
|
|
|
93
97
|
const { Frond } = await import("@tina4/frond");
|
|
94
98
|
const templateFile = `errors/${code}.twig`;
|
|
95
99
|
|
|
100
|
+
// Helper: get-or-create a cached Frond instance for a directory
|
|
101
|
+
const getCachedFrond = (dir: string) => {
|
|
102
|
+
let instance = frondCache.get(dir);
|
|
103
|
+
if (!instance) {
|
|
104
|
+
instance = new Frond(dir);
|
|
105
|
+
frondCache.set(dir, instance);
|
|
106
|
+
}
|
|
107
|
+
return instance;
|
|
108
|
+
};
|
|
109
|
+
|
|
96
110
|
// 1. Try user override in the project's templates directory
|
|
97
111
|
const userTemplatePath = join(templatesDir, templateFile);
|
|
98
112
|
if (existsSync(userTemplatePath)) {
|
|
99
|
-
|
|
100
|
-
return frond.render(templateFile, data);
|
|
113
|
+
return getCachedFrond(templatesDir).render(templateFile, data);
|
|
101
114
|
}
|
|
102
115
|
|
|
103
116
|
// 2. Try built-in framework default
|
|
104
117
|
const builtinTemplatePath = join(BUILTIN_ERROR_TEMPLATES_DIR, templateFile);
|
|
105
118
|
if (existsSync(builtinTemplatePath)) {
|
|
106
|
-
|
|
107
|
-
return frond.render(templateFile, data);
|
|
119
|
+
return getCachedFrond(BUILTIN_ERROR_TEMPLATES_DIR).render(templateFile, data);
|
|
108
120
|
}
|
|
109
121
|
|
|
110
122
|
// 3. No template found
|
|
@@ -28,6 +28,10 @@ import { randomBytes } from "node:crypto";
|
|
|
28
28
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from "node:fs";
|
|
29
29
|
import { join } from "node:path";
|
|
30
30
|
import { execFileSync } from "node:child_process";
|
|
31
|
+
import { RedisNpmSessionHandler } from "./sessionHandlers/redisHandler.js";
|
|
32
|
+
import { ValkeySessionHandler } from "./sessionHandlers/valkeyHandler.js";
|
|
33
|
+
import { MongoSessionHandler } from "./sessionHandlers/mongoHandler.js";
|
|
34
|
+
import { DatabaseSessionHandler } from "./sessionHandlers/databaseHandler.js";
|
|
31
35
|
|
|
32
36
|
// ── Types ─────────────────────────────────────────────────────────
|
|
33
37
|
|
|
@@ -303,24 +307,20 @@ export class Session {
|
|
|
303
307
|
this.handler = new RedisSessionHandler(config);
|
|
304
308
|
break;
|
|
305
309
|
case "redis-npm": {
|
|
306
|
-
const { RedisNpmSessionHandler } = require("./sessionHandlers/redisHandler.js");
|
|
307
310
|
this.handler = new RedisNpmSessionHandler(config);
|
|
308
311
|
break;
|
|
309
312
|
}
|
|
310
313
|
case "valkey": {
|
|
311
|
-
const { ValkeySessionHandler } = require("./sessionHandlers/valkeyHandler.js");
|
|
312
314
|
this.handler = new ValkeySessionHandler(config);
|
|
313
315
|
break;
|
|
314
316
|
}
|
|
315
317
|
case "mongo":
|
|
316
318
|
case "mongodb": {
|
|
317
|
-
const { MongoSessionHandler } = require("./sessionHandlers/mongoHandler.js");
|
|
318
319
|
this.handler = new MongoSessionHandler(config);
|
|
319
320
|
break;
|
|
320
321
|
}
|
|
321
322
|
case "database":
|
|
322
323
|
case "db": {
|
|
323
|
-
const { DatabaseSessionHandler } = require("./sessionHandlers/databaseHandler.js");
|
|
324
324
|
this.handler = new DatabaseSessionHandler(config);
|
|
325
325
|
break;
|
|
326
326
|
}
|
|
@@ -10,11 +10,9 @@
|
|
|
10
10
|
* The handler dynamically imports `better-sqlite3` and throws a clear
|
|
11
11
|
* error if the package is not installed.
|
|
12
12
|
*/
|
|
13
|
-
import {
|
|
13
|
+
import { DatabaseSync } from "node:sqlite";
|
|
14
14
|
import type { SessionHandler } from "../session.js";
|
|
15
15
|
|
|
16
|
-
const _require = createRequire(import.meta.url);
|
|
17
|
-
|
|
18
16
|
interface SessionData {
|
|
19
17
|
_created: number;
|
|
20
18
|
_accessed: number;
|
|
@@ -39,7 +37,6 @@ export class DatabaseSessionHandler implements SessionHandler {
|
|
|
39
37
|
constructor(config?: DatabaseSessionConfig) {
|
|
40
38
|
const dbPath = config?.dbPath ?? this.resolveDbPath();
|
|
41
39
|
|
|
42
|
-
const { DatabaseSync } = require("node:sqlite");
|
|
43
40
|
this.db = new DatabaseSync(dbPath);
|
|
44
41
|
this.db.exec("PRAGMA journal_mode = WAL");
|
|
45
42
|
}
|
|
@@ -53,25 +53,26 @@ export class RedisBackplane implements WebSocketBackplane {
|
|
|
53
53
|
constructor(url?: string) {
|
|
54
54
|
this.url = url ?? process.env.TINA4_WS_BACKPLANE_URL ?? "redis://localhost:6379";
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
56
|
+
this.ready = (async () => {
|
|
57
|
+
let redis: any;
|
|
58
|
+
try {
|
|
59
|
+
redis = await import("redis");
|
|
60
|
+
} catch {
|
|
61
|
+
throw new Error(
|
|
62
|
+
"The 'redis' package is required for RedisBackplane. " +
|
|
63
|
+
"Install it with: npm install redis"
|
|
64
|
+
);
|
|
65
|
+
}
|
|
65
66
|
|
|
66
|
-
|
|
67
|
-
|
|
67
|
+
this.publisher = redis.createClient({ url: this.url });
|
|
68
|
+
this.subscriber = this.publisher.duplicate();
|
|
68
69
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
await Promise.all([
|
|
71
|
+
this.publisher.connect(),
|
|
72
|
+
this.subscriber.connect(),
|
|
73
|
+
]);
|
|
73
74
|
console.log(`[Tina4] RedisBackplane connected to ${this.url}`);
|
|
74
|
-
});
|
|
75
|
+
})();
|
|
75
76
|
}
|
|
76
77
|
|
|
77
78
|
async publish(channel: string, message: string): Promise<void> {
|
|
@@ -6,13 +6,13 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import type { DatabaseAdapter, DatabaseResult, ColumnInfo, FieldDefinition } from "../types.js";
|
|
8
8
|
import { SQLTranslator } from "../sqlTranslation.js";
|
|
9
|
+
import { createRequire } from "node:module";
|
|
9
10
|
|
|
10
11
|
let firebird: any = null;
|
|
11
12
|
|
|
12
13
|
function requireFirebird(): any {
|
|
13
14
|
if (firebird) return firebird;
|
|
14
15
|
try {
|
|
15
|
-
const { createRequire } = require("node:module") as typeof import("node:module");
|
|
16
16
|
const req = createRequire(import.meta.url);
|
|
17
17
|
firebird = req("node-firebird");
|
|
18
18
|
return firebird;
|
|
@@ -6,13 +6,13 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import type { DatabaseAdapter, DatabaseResult, ColumnInfo, FieldDefinition } from "../types.js";
|
|
8
8
|
import { SQLTranslator } from "../sqlTranslation.js";
|
|
9
|
+
import { createRequire } from "node:module";
|
|
9
10
|
|
|
10
11
|
let tedious: any = null;
|
|
11
12
|
|
|
12
13
|
function requireTedious(): any {
|
|
13
14
|
if (tedious) return tedious;
|
|
14
15
|
try {
|
|
15
|
-
const { createRequire } = require("node:module") as typeof import("node:module");
|
|
16
16
|
const req = createRequire(import.meta.url);
|
|
17
17
|
tedious = req("tedious");
|
|
18
18
|
return tedious;
|
|
@@ -6,13 +6,13 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import type { DatabaseAdapter, DatabaseResult, ColumnInfo, FieldDefinition } from "../types.js";
|
|
8
8
|
import { SQLTranslator } from "../sqlTranslation.js";
|
|
9
|
+
import { createRequire } from "node:module";
|
|
9
10
|
|
|
10
11
|
let mysql2: any = null;
|
|
11
12
|
|
|
12
13
|
function requireMysql2(): any {
|
|
13
14
|
if (mysql2) return mysql2;
|
|
14
15
|
try {
|
|
15
|
-
const { createRequire } = require("node:module") as typeof import("node:module");
|
|
16
16
|
const req = createRequire(import.meta.url);
|
|
17
17
|
mysql2 = req("mysql2");
|
|
18
18
|
return mysql2;
|
|
@@ -7,15 +7,15 @@
|
|
|
7
7
|
import type { DatabaseAdapter, DatabaseResult, ColumnInfo, FieldDefinition } from "../types.js";
|
|
8
8
|
import { SQLTranslator } from "../sqlTranslation.js";
|
|
9
9
|
|
|
10
|
+
import { createRequire } from "node:module";
|
|
11
|
+
|
|
10
12
|
let pg: typeof import("pg") | null = null;
|
|
11
13
|
|
|
12
14
|
function requirePg(): typeof import("pg") {
|
|
13
15
|
if (pg) return pg;
|
|
14
16
|
try {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const require = createRequire(import.meta.url);
|
|
18
|
-
pg = require("pg");
|
|
17
|
+
const req = createRequire(import.meta.url);
|
|
18
|
+
pg = req("pg");
|
|
19
19
|
return pg!;
|
|
20
20
|
} catch {
|
|
21
21
|
throw new Error(
|
|
@@ -24,13 +24,6 @@ function requirePg(): typeof import("pg") {
|
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
/** Synchronous helper to get createRequire — we call this at connection time. */
|
|
28
|
-
function await_import_module() {
|
|
29
|
-
// node:module is always available
|
|
30
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
31
|
-
return require("node:module") as typeof import("node:module");
|
|
32
|
-
}
|
|
33
|
-
|
|
34
27
|
export interface PostgresConfig {
|
|
35
28
|
host?: string;
|
|
36
29
|
port?: number;
|
|
@@ -3,6 +3,20 @@ import { validate as validateFields } from "./validation.js";
|
|
|
3
3
|
import { QueryBuilder } from "./queryBuilder.js";
|
|
4
4
|
import type { DatabaseAdapter, FieldDefinition, RelationshipDefinition } from "./types.js";
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Convert a snake_case name to camelCase.
|
|
8
|
+
*/
|
|
9
|
+
export function snakeToCamel(name: string): string {
|
|
10
|
+
return name.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Convert a camelCase name to snake_case.
|
|
15
|
+
*/
|
|
16
|
+
export function camelToSnake(name: string): string {
|
|
17
|
+
return name.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
6
20
|
/**
|
|
7
21
|
* BaseModel provides instance methods for ORM models.
|
|
8
22
|
* Models extend this class and define static properties.
|
|
@@ -17,6 +31,7 @@ import type { DatabaseAdapter, FieldDefinition, RelationshipDefinition } from ".
|
|
|
17
31
|
* static hasMany = [{ model: "Post", foreignKey: "author_id" }];
|
|
18
32
|
* static _db = "secondary";
|
|
19
33
|
* static fieldMapping = { firstName: "first_name", lastName: "last_name" };
|
|
34
|
+
* static autoMap = true; // auto-generate fieldMapping from camelCase → snake_case
|
|
20
35
|
* }
|
|
21
36
|
*/
|
|
22
37
|
export class BaseModel {
|
|
@@ -29,6 +44,12 @@ export class BaseModel {
|
|
|
29
44
|
static belongsTo?: RelationshipDefinition[];
|
|
30
45
|
static _db?: string;
|
|
31
46
|
|
|
47
|
+
/**
|
|
48
|
+
* When true, auto-generates fieldMapping entries from camelCase field names
|
|
49
|
+
* to snake_case DB column names. Explicit fieldMapping entries always win.
|
|
50
|
+
*/
|
|
51
|
+
static autoMap: boolean = false;
|
|
52
|
+
|
|
32
53
|
/**
|
|
33
54
|
* Maps JS property names to database column names.
|
|
34
55
|
* Example: { firstName: "first_name" } means the JS property `firstName`
|
|
@@ -46,6 +67,18 @@ export class BaseModel {
|
|
|
46
67
|
constructor(data?: Record<string, unknown>) {
|
|
47
68
|
if (data) {
|
|
48
69
|
const ModelClass = this.constructor as typeof BaseModel;
|
|
70
|
+
// If autoMap is on, auto-generate fieldMapping from camelCase fields
|
|
71
|
+
if (ModelClass.autoMap) {
|
|
72
|
+
const fields = ModelClass.fields || {};
|
|
73
|
+
for (const key of Object.keys(fields)) {
|
|
74
|
+
if (!ModelClass.fieldMapping[key]) {
|
|
75
|
+
const snaked = camelToSnake(key);
|
|
76
|
+
if (snaked !== key) {
|
|
77
|
+
ModelClass.fieldMapping[key] = snaked;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
49
82
|
const reverseMapping = ModelClass.getReverseMapping();
|
|
50
83
|
for (const [key, value] of Object.entries(data)) {
|
|
51
84
|
// If this DB column has a mapping, use the JS property name instead
|
|
@@ -38,7 +38,7 @@ export { generateCrudRoutes } from "./autoCrud.js";
|
|
|
38
38
|
export { buildQuery, parseQueryString } from "./query.js";
|
|
39
39
|
export { validate } from "./validation.js";
|
|
40
40
|
export type { ValidationError } from "./validation.js";
|
|
41
|
-
export { BaseModel } from "./baseModel.js";
|
|
41
|
+
export { BaseModel, snakeToCamel, camelToSnake } from "./baseModel.js";
|
|
42
42
|
export { QueryBuilder } from "./queryBuilder.js";
|
|
43
43
|
export { SQLTranslator, QueryCache } from "./sqlTranslation.js";
|
|
44
44
|
export { CachedDatabaseAdapter } from "./cachedDatabase.js";
|