tina4-nodejs 3.2.1 → 3.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +1 -1
- package/README.md +1 -1
- package/package.json +1 -1
- package/packages/cli/src/bin.ts +13 -1
- package/packages/cli/src/commands/migrate.ts +19 -5
- package/packages/cli/src/commands/migrateCreate.ts +29 -28
- package/packages/cli/src/commands/migrateRollback.ts +59 -0
- package/packages/cli/src/commands/migrateStatus.ts +62 -0
- package/packages/core/public/js/tina4-dev-admin.min.js +1 -1
- package/packages/core/public/js/tina4js.min.js +47 -0
- package/packages/core/src/auth.ts +44 -10
- package/packages/core/src/devAdmin.ts +14 -16
- package/packages/core/src/index.ts +10 -3
- package/packages/core/src/middleware.ts +232 -2
- package/packages/core/src/queue.ts +127 -25
- package/packages/core/src/queueBackends/mongoBackend.ts +223 -0
- package/packages/core/src/request.ts +3 -3
- package/packages/core/src/router.ts +115 -51
- package/packages/core/src/server.ts +47 -3
- package/packages/core/src/session.ts +29 -1
- package/packages/core/src/sessionHandlers/databaseHandler.ts +134 -0
- package/packages/core/src/sessionHandlers/redisHandler.ts +230 -0
- package/packages/core/src/types.ts +12 -6
- package/packages/core/src/websocket.ts +11 -2
- package/packages/core/src/websocketConnection.ts +4 -2
- package/packages/frond/src/engine.ts +66 -1
- package/packages/orm/src/autoCrud.ts +17 -12
- package/packages/orm/src/baseModel.ts +99 -21
- package/packages/orm/src/database.ts +197 -69
- package/packages/orm/src/databaseResult.ts +207 -0
- package/packages/orm/src/index.ts +6 -3
- package/packages/orm/src/migration.ts +296 -71
- package/packages/orm/src/model.ts +1 -0
- package/packages/orm/src/types.ts +1 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tina4 MongoDB Queue Backend — uses `mongodb` npm package via dynamic import.
|
|
3
|
+
*
|
|
4
|
+
* Implements the same interface as the file-based queue but uses MongoDB
|
|
5
|
+
* for message storage and delivery. Atomic pop via findOneAndUpdate.
|
|
6
|
+
*
|
|
7
|
+
* Configure via environment variables:
|
|
8
|
+
* TINA4_MONGO_HOST (default: "localhost")
|
|
9
|
+
* TINA4_MONGO_PORT (default: 27017)
|
|
10
|
+
* TINA4_MONGO_URI (overrides host/port/username/password)
|
|
11
|
+
* TINA4_MONGO_USERNAME (optional)
|
|
12
|
+
* TINA4_MONGO_PASSWORD (optional)
|
|
13
|
+
* TINA4_MONGO_DB (default: "tina4")
|
|
14
|
+
* TINA4_MONGO_COLLECTION (default: "tina4_queue")
|
|
15
|
+
*/
|
|
16
|
+
import { randomUUID } from "node:crypto";
|
|
17
|
+
import { execFileSync } from "node:child_process";
|
|
18
|
+
import type { QueueJob } from "../queue.js";
|
|
19
|
+
|
|
20
|
+
// ── Types ────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
export interface MongoConfig {
|
|
23
|
+
host?: string;
|
|
24
|
+
port?: number;
|
|
25
|
+
uri?: string;
|
|
26
|
+
username?: string;
|
|
27
|
+
password?: string;
|
|
28
|
+
database?: string;
|
|
29
|
+
collection?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface QueueBackend {
|
|
33
|
+
push(queue: string, payload: unknown, delay?: number): string;
|
|
34
|
+
pop(queue: string): QueueJob | null;
|
|
35
|
+
size(queue: string): number;
|
|
36
|
+
clear(queue: string): void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ── MongoDB Backend ──────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* MongoDB queue backend using the `mongodb` npm package.
|
|
43
|
+
*
|
|
44
|
+
* Uses synchronous-style communication by spawning a child process
|
|
45
|
+
* for each operation, similar to the RabbitMQ and Redis patterns.
|
|
46
|
+
* This keeps the interface synchronous as required by the Queue class.
|
|
47
|
+
*/
|
|
48
|
+
export class MongoBackend implements QueueBackend {
|
|
49
|
+
private host: string;
|
|
50
|
+
private port: number;
|
|
51
|
+
private uri: string;
|
|
52
|
+
private username: string;
|
|
53
|
+
private password: string;
|
|
54
|
+
private database: string;
|
|
55
|
+
private collection: string;
|
|
56
|
+
|
|
57
|
+
constructor(config?: MongoConfig) {
|
|
58
|
+
this.host = config?.host ?? process.env.TINA4_MONGO_HOST ?? "localhost";
|
|
59
|
+
this.port = config?.port
|
|
60
|
+
?? (process.env.TINA4_MONGO_PORT ? parseInt(process.env.TINA4_MONGO_PORT, 10) : 27017);
|
|
61
|
+
this.username = config?.username ?? process.env.TINA4_MONGO_USERNAME ?? "";
|
|
62
|
+
this.password = config?.password ?? process.env.TINA4_MONGO_PASSWORD ?? "";
|
|
63
|
+
this.database = config?.database ?? process.env.TINA4_MONGO_DB ?? "tina4";
|
|
64
|
+
this.collection = config?.collection ?? process.env.TINA4_MONGO_COLLECTION ?? "tina4_queue";
|
|
65
|
+
|
|
66
|
+
// URI overrides individual host/port/auth settings
|
|
67
|
+
if (config?.uri ?? process.env.TINA4_MONGO_URI) {
|
|
68
|
+
this.uri = config?.uri ?? process.env.TINA4_MONGO_URI!;
|
|
69
|
+
} else {
|
|
70
|
+
const auth = this.username
|
|
71
|
+
? `${encodeURIComponent(this.username)}:${encodeURIComponent(this.password)}@`
|
|
72
|
+
: "";
|
|
73
|
+
this.uri = `mongodb://${auth}${this.host}:${this.port}`;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Execute a MongoDB operation synchronously via a child process.
|
|
79
|
+
*/
|
|
80
|
+
private execSync(operation: string, queue: string, data?: string): string {
|
|
81
|
+
const script = `
|
|
82
|
+
async function main() {
|
|
83
|
+
let mongodb;
|
|
84
|
+
try {
|
|
85
|
+
mongodb = await import("mongodb");
|
|
86
|
+
} catch {
|
|
87
|
+
process.stderr.write("mongodb package not installed — run: npm install mongodb");
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const { MongoClient } = mongodb;
|
|
92
|
+
const uri = ${JSON.stringify(this.uri)};
|
|
93
|
+
const dbName = ${JSON.stringify(this.database)};
|
|
94
|
+
const collName = ${JSON.stringify(this.collection)};
|
|
95
|
+
const operation = ${JSON.stringify(operation)};
|
|
96
|
+
const queueName = ${JSON.stringify(queue)};
|
|
97
|
+
const data = ${JSON.stringify(data ?? "")};
|
|
98
|
+
|
|
99
|
+
const client = new MongoClient(uri, {
|
|
100
|
+
connectTimeoutMS: 5000,
|
|
101
|
+
serverSelectionTimeoutMS: 5000,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
await client.connect();
|
|
106
|
+
const db = client.db(dbName);
|
|
107
|
+
const col = db.collection(collName);
|
|
108
|
+
|
|
109
|
+
// Ensure indexes on first use
|
|
110
|
+
await col.createIndex({ queue: 1, status: 1, delayUntil: 1 });
|
|
111
|
+
await col.createIndex({ queue: 1, createdAt: 1 });
|
|
112
|
+
|
|
113
|
+
if (operation === "push") {
|
|
114
|
+
const job = JSON.parse(data);
|
|
115
|
+
await col.insertOne({ ...job, queue: queueName });
|
|
116
|
+
process.stdout.write("__PUSHED__");
|
|
117
|
+
}
|
|
118
|
+
else if (operation === "pop") {
|
|
119
|
+
const now = new Date().toISOString();
|
|
120
|
+
const result = await col.findOneAndUpdate(
|
|
121
|
+
{
|
|
122
|
+
queue: queueName,
|
|
123
|
+
status: "pending",
|
|
124
|
+
$or: [
|
|
125
|
+
{ delayUntil: null },
|
|
126
|
+
{ delayUntil: { $lte: now } },
|
|
127
|
+
],
|
|
128
|
+
},
|
|
129
|
+
{ $set: { status: "reserved" } },
|
|
130
|
+
{ sort: { createdAt: 1 }, returnDocument: "before" },
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
if (result && result.value) {
|
|
134
|
+
// findOneAndUpdate returns { value: doc } in older drivers
|
|
135
|
+
const doc = result.value;
|
|
136
|
+
delete doc._id;
|
|
137
|
+
delete doc.queue;
|
|
138
|
+
process.stdout.write(JSON.stringify(doc));
|
|
139
|
+
} else if (result && result._id) {
|
|
140
|
+
// Some driver versions return the doc directly
|
|
141
|
+
const doc = { ...result };
|
|
142
|
+
delete doc._id;
|
|
143
|
+
delete doc.queue;
|
|
144
|
+
process.stdout.write(JSON.stringify(doc));
|
|
145
|
+
} else {
|
|
146
|
+
process.stdout.write("__EMPTY__");
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
else if (operation === "size") {
|
|
150
|
+
const count = await col.countDocuments({
|
|
151
|
+
queue: queueName,
|
|
152
|
+
status: "pending",
|
|
153
|
+
});
|
|
154
|
+
process.stdout.write(String(count));
|
|
155
|
+
}
|
|
156
|
+
else if (operation === "clear") {
|
|
157
|
+
await col.deleteMany({ queue: queueName });
|
|
158
|
+
process.stdout.write("__CLEARED__");
|
|
159
|
+
}
|
|
160
|
+
} catch (err) {
|
|
161
|
+
process.stderr.write(err.message || String(err));
|
|
162
|
+
process.exit(1);
|
|
163
|
+
} finally {
|
|
164
|
+
await client.close();
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
main();
|
|
169
|
+
`;
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
const result = execFileSync(process.execPath, ["-e", script], {
|
|
173
|
+
encoding: "utf-8",
|
|
174
|
+
timeout: 15000,
|
|
175
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
176
|
+
});
|
|
177
|
+
return result;
|
|
178
|
+
} catch {
|
|
179
|
+
return "";
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
push(queue: string, payload: unknown, delay?: number): string {
|
|
184
|
+
const id = randomUUID();
|
|
185
|
+
const now = new Date().toISOString();
|
|
186
|
+
|
|
187
|
+
const job: QueueJob = {
|
|
188
|
+
id,
|
|
189
|
+
payload,
|
|
190
|
+
status: "pending",
|
|
191
|
+
createdAt: now,
|
|
192
|
+
attempts: 0,
|
|
193
|
+
delayUntil: delay ? new Date(Date.now() + delay * 1000).toISOString() : null,
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const result = this.execSync("push", queue, JSON.stringify(job));
|
|
197
|
+
if (!result.includes("__PUSHED__")) {
|
|
198
|
+
throw new Error("MongoDB push failed");
|
|
199
|
+
}
|
|
200
|
+
return id;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
pop(queue: string): QueueJob | null {
|
|
204
|
+
const result = this.execSync("pop", queue);
|
|
205
|
+
if (!result || result === "__EMPTY__") return null;
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
return JSON.parse(result) as QueueJob;
|
|
209
|
+
} catch {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
size(queue: string): number {
|
|
215
|
+
const result = this.execSync("size", queue);
|
|
216
|
+
const num = parseInt(result, 10);
|
|
217
|
+
return isNaN(num) ? 0 : num;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
clear(queue: string): void {
|
|
221
|
+
this.execSync("clear", queue);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
@@ -129,12 +129,12 @@ function parseMultipart(
|
|
|
129
129
|
const partContentType = parsePartContentType(headersStr);
|
|
130
130
|
|
|
131
131
|
if (disposition.filename) {
|
|
132
|
-
// File upload
|
|
132
|
+
// File upload — standardised format: filename, type, content (raw bytes), size
|
|
133
133
|
files.push({
|
|
134
134
|
fieldName: disposition.name,
|
|
135
135
|
filename: disposition.filename,
|
|
136
|
-
|
|
137
|
-
|
|
136
|
+
type: partContentType ?? "application/octet-stream",
|
|
137
|
+
content: Buffer.from(content),
|
|
138
138
|
size: content.length,
|
|
139
139
|
});
|
|
140
140
|
} else if (disposition.name) {
|
|
@@ -7,6 +7,8 @@ interface MatchResult {
|
|
|
7
7
|
meta?: RouteMeta;
|
|
8
8
|
middlewares?: Middleware[];
|
|
9
9
|
template?: string;
|
|
10
|
+
secure?: boolean;
|
|
11
|
+
cached?: boolean;
|
|
10
12
|
}
|
|
11
13
|
|
|
12
14
|
interface CompiledRoute {
|
|
@@ -17,28 +19,77 @@ interface CompiledRoute {
|
|
|
17
19
|
meta?: RouteMeta;
|
|
18
20
|
filePath?: string;
|
|
19
21
|
middlewares?: Middleware[];
|
|
22
|
+
secure?: boolean;
|
|
20
23
|
cached?: boolean;
|
|
21
24
|
cacheStore?: Map<string, { data: unknown; expires: number }>;
|
|
22
25
|
cacheTtl?: number;
|
|
23
26
|
template?: string;
|
|
24
27
|
}
|
|
25
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Thin reference to a registered route, enabling chained modifiers.
|
|
31
|
+
*
|
|
32
|
+
* Usage:
|
|
33
|
+
* router.get("/api/data", handler).secure().cache();
|
|
34
|
+
*/
|
|
35
|
+
export class RouteRef {
|
|
36
|
+
constructor(private route: CompiledRoute) {}
|
|
37
|
+
|
|
38
|
+
/** Mark this route as requiring bearer-token authentication. */
|
|
39
|
+
secure(): this {
|
|
40
|
+
this.route.secure = true;
|
|
41
|
+
return this;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Mark this route's response as cacheable. */
|
|
45
|
+
cache(): this {
|
|
46
|
+
this.route.cached = true;
|
|
47
|
+
return this;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
26
51
|
export interface RouteInfo {
|
|
27
52
|
method: string;
|
|
28
53
|
path: string;
|
|
29
54
|
handler: string;
|
|
30
55
|
middlewareCount: number;
|
|
31
56
|
cached: boolean;
|
|
57
|
+
secure: boolean;
|
|
32
58
|
}
|
|
33
59
|
|
|
34
60
|
export class Router {
|
|
35
61
|
private routes: Map<string, CompiledRoute[]> = new Map();
|
|
36
62
|
private wsRoutes: WebSocketRouteDefinition[] = [];
|
|
37
63
|
|
|
64
|
+
/** Class-based middleware registered via `use()` / `Router.use()`. */
|
|
65
|
+
private static _classMiddlewares: any[] = [];
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Register a class-based middleware (beforeX / afterX convention).
|
|
69
|
+
* Classes are stored globally and executed by MiddlewareRunner.
|
|
70
|
+
*/
|
|
71
|
+
static use(middlewareClass: any): void {
|
|
72
|
+
Router._classMiddlewares.push(middlewareClass);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Get all registered class-based middleware classes.
|
|
77
|
+
*/
|
|
78
|
+
static getClassMiddlewares(): any[] {
|
|
79
|
+
return Router._classMiddlewares;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Clear all registered class-based middleware (useful for testing).
|
|
84
|
+
*/
|
|
85
|
+
static clearClassMiddlewares(): void {
|
|
86
|
+
Router._classMiddlewares = [];
|
|
87
|
+
}
|
|
88
|
+
|
|
38
89
|
/**
|
|
39
90
|
* Add a raw route definition (used internally and by file-based routing).
|
|
40
91
|
*/
|
|
41
|
-
addRoute(definition: RouteDefinition):
|
|
92
|
+
addRoute(definition: RouteDefinition): RouteRef {
|
|
42
93
|
const method = definition.method.toUpperCase();
|
|
43
94
|
const { regex, paramNames } = this.compilePattern(definition.pattern);
|
|
44
95
|
|
|
@@ -54,7 +105,7 @@ export class Router {
|
|
|
54
105
|
routes.splice(existingIndex, 1);
|
|
55
106
|
}
|
|
56
107
|
|
|
57
|
-
|
|
108
|
+
const compiled: CompiledRoute = {
|
|
58
109
|
pattern: definition.pattern,
|
|
59
110
|
regex,
|
|
60
111
|
paramNames,
|
|
@@ -62,52 +113,58 @@ export class Router {
|
|
|
62
113
|
meta: definition.meta,
|
|
63
114
|
filePath: definition.filePath,
|
|
64
115
|
middlewares: definition.middlewares,
|
|
116
|
+
secure: definition.secure,
|
|
117
|
+
cached: definition.cached,
|
|
65
118
|
template: definition.template,
|
|
66
|
-
}
|
|
119
|
+
};
|
|
120
|
+
routes.push(compiled);
|
|
121
|
+
return new RouteRef(compiled);
|
|
67
122
|
}
|
|
68
123
|
|
|
69
124
|
/**
|
|
70
125
|
* Register a GET route programmatically.
|
|
71
126
|
*/
|
|
72
|
-
get(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta):
|
|
73
|
-
this.addRoute({ method: "GET", pattern: path, handler, middlewares, meta });
|
|
127
|
+
get(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta): RouteRef {
|
|
128
|
+
return this.addRoute({ method: "GET", pattern: path, handler, middlewares, meta });
|
|
74
129
|
}
|
|
75
130
|
|
|
76
131
|
/**
|
|
77
132
|
* Register a POST route programmatically.
|
|
78
133
|
*/
|
|
79
|
-
post(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta):
|
|
80
|
-
this.addRoute({ method: "POST", pattern: path, handler, middlewares, meta });
|
|
134
|
+
post(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta): RouteRef {
|
|
135
|
+
return this.addRoute({ method: "POST", pattern: path, handler, middlewares, meta });
|
|
81
136
|
}
|
|
82
137
|
|
|
83
138
|
/**
|
|
84
139
|
* Register a PUT route programmatically.
|
|
85
140
|
*/
|
|
86
|
-
put(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta):
|
|
87
|
-
this.addRoute({ method: "PUT", pattern: path, handler, middlewares, meta });
|
|
141
|
+
put(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta): RouteRef {
|
|
142
|
+
return this.addRoute({ method: "PUT", pattern: path, handler, middlewares, meta });
|
|
88
143
|
}
|
|
89
144
|
|
|
90
145
|
/**
|
|
91
146
|
* Register a PATCH route programmatically.
|
|
92
147
|
*/
|
|
93
|
-
patch(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta):
|
|
94
|
-
this.addRoute({ method: "PATCH", pattern: path, handler, middlewares, meta });
|
|
148
|
+
patch(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta): RouteRef {
|
|
149
|
+
return this.addRoute({ method: "PATCH", pattern: path, handler, middlewares, meta });
|
|
95
150
|
}
|
|
96
151
|
|
|
97
152
|
/**
|
|
98
153
|
* Register a DELETE route programmatically.
|
|
99
154
|
*/
|
|
100
|
-
delete(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta):
|
|
101
|
-
this.addRoute({ method: "DELETE", pattern: path, handler, middlewares, meta });
|
|
155
|
+
delete(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta): RouteRef {
|
|
156
|
+
return this.addRoute({ method: "DELETE", pattern: path, handler, middlewares, meta });
|
|
102
157
|
}
|
|
103
158
|
|
|
104
159
|
/**
|
|
105
160
|
* Register a route that matches ANY HTTP method.
|
|
106
161
|
*/
|
|
107
|
-
any(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta):
|
|
162
|
+
any(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta): RouteRef {
|
|
163
|
+
let lastRef!: RouteRef;
|
|
108
164
|
for (const method of ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"]) {
|
|
109
|
-
this.addRoute({ method, pattern: path, handler, middlewares, meta });
|
|
165
|
+
lastRef = this.addRoute({ method, pattern: path, handler, middlewares, meta });
|
|
110
166
|
}
|
|
167
|
+
return lastRef;
|
|
111
168
|
}
|
|
112
169
|
|
|
113
170
|
/**
|
|
@@ -142,6 +199,8 @@ export class Router {
|
|
|
142
199
|
meta: route.meta,
|
|
143
200
|
middlewares: route.middlewares,
|
|
144
201
|
template: route.template,
|
|
202
|
+
secure: route.secure,
|
|
203
|
+
cached: route.cached,
|
|
145
204
|
};
|
|
146
205
|
}
|
|
147
206
|
}
|
|
@@ -164,6 +223,8 @@ export class Router {
|
|
|
164
223
|
filePath: route.filePath,
|
|
165
224
|
middlewares: route.middlewares,
|
|
166
225
|
template: route.template,
|
|
226
|
+
secure: route.secure,
|
|
227
|
+
cached: route.cached,
|
|
167
228
|
});
|
|
168
229
|
}
|
|
169
230
|
}
|
|
@@ -183,6 +244,7 @@ export class Router {
|
|
|
183
244
|
handler: route.filePath ?? (route.handler.name || "(anonymous)"),
|
|
184
245
|
middlewareCount: route.middlewares?.length ?? 0,
|
|
185
246
|
cached: route.cached ?? false,
|
|
247
|
+
secure: route.secure ?? false,
|
|
186
248
|
});
|
|
187
249
|
}
|
|
188
250
|
}
|
|
@@ -228,43 +290,43 @@ export class Router {
|
|
|
228
290
|
/**
|
|
229
291
|
* Register a GET route on the default global router.
|
|
230
292
|
*/
|
|
231
|
-
static get(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta):
|
|
232
|
-
defaultRouter.get(path, handler, middlewares, meta);
|
|
293
|
+
static get(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta): RouteRef {
|
|
294
|
+
return defaultRouter.get(path, handler, middlewares, meta);
|
|
233
295
|
}
|
|
234
296
|
|
|
235
297
|
/**
|
|
236
298
|
* Register a POST route on the default global router.
|
|
237
299
|
*/
|
|
238
|
-
static post(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta):
|
|
239
|
-
defaultRouter.post(path, handler, middlewares, meta);
|
|
300
|
+
static post(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta): RouteRef {
|
|
301
|
+
return defaultRouter.post(path, handler, middlewares, meta);
|
|
240
302
|
}
|
|
241
303
|
|
|
242
304
|
/**
|
|
243
305
|
* Register a PUT route on the default global router.
|
|
244
306
|
*/
|
|
245
|
-
static put(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta):
|
|
246
|
-
defaultRouter.put(path, handler, middlewares, meta);
|
|
307
|
+
static put(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta): RouteRef {
|
|
308
|
+
return defaultRouter.put(path, handler, middlewares, meta);
|
|
247
309
|
}
|
|
248
310
|
|
|
249
311
|
/**
|
|
250
312
|
* Register a PATCH route on the default global router.
|
|
251
313
|
*/
|
|
252
|
-
static patch(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta):
|
|
253
|
-
defaultRouter.patch(path, handler, middlewares, meta);
|
|
314
|
+
static patch(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta): RouteRef {
|
|
315
|
+
return defaultRouter.patch(path, handler, middlewares, meta);
|
|
254
316
|
}
|
|
255
317
|
|
|
256
318
|
/**
|
|
257
319
|
* Register a DELETE route on the default global router.
|
|
258
320
|
*/
|
|
259
|
-
static delete(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta):
|
|
260
|
-
defaultRouter.delete(path, handler, middlewares, meta);
|
|
321
|
+
static delete(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta): RouteRef {
|
|
322
|
+
return defaultRouter.delete(path, handler, middlewares, meta);
|
|
261
323
|
}
|
|
262
324
|
|
|
263
325
|
/**
|
|
264
326
|
* Register a route that matches ANY HTTP method on the default global router.
|
|
265
327
|
*/
|
|
266
|
-
static any(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta):
|
|
267
|
-
defaultRouter.any(path, handler, middlewares, meta);
|
|
328
|
+
static any(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta): RouteRef {
|
|
329
|
+
return defaultRouter.any(path, handler, middlewares, meta);
|
|
268
330
|
}
|
|
269
331
|
|
|
270
332
|
/**
|
|
@@ -343,8 +405,8 @@ export class RouteGroup {
|
|
|
343
405
|
return merged.length > 0 ? merged : undefined;
|
|
344
406
|
}
|
|
345
407
|
|
|
346
|
-
get(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta):
|
|
347
|
-
this.router.addRoute({
|
|
408
|
+
get(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta): RouteRef {
|
|
409
|
+
return this.router.addRoute({
|
|
348
410
|
method: "GET",
|
|
349
411
|
pattern: this.prefix + path,
|
|
350
412
|
handler,
|
|
@@ -353,8 +415,8 @@ export class RouteGroup {
|
|
|
353
415
|
});
|
|
354
416
|
}
|
|
355
417
|
|
|
356
|
-
post(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta):
|
|
357
|
-
this.router.addRoute({
|
|
418
|
+
post(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta): RouteRef {
|
|
419
|
+
return this.router.addRoute({
|
|
358
420
|
method: "POST",
|
|
359
421
|
pattern: this.prefix + path,
|
|
360
422
|
handler,
|
|
@@ -363,8 +425,8 @@ export class RouteGroup {
|
|
|
363
425
|
});
|
|
364
426
|
}
|
|
365
427
|
|
|
366
|
-
put(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta):
|
|
367
|
-
this.router.addRoute({
|
|
428
|
+
put(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta): RouteRef {
|
|
429
|
+
return this.router.addRoute({
|
|
368
430
|
method: "PUT",
|
|
369
431
|
pattern: this.prefix + path,
|
|
370
432
|
handler,
|
|
@@ -373,8 +435,8 @@ export class RouteGroup {
|
|
|
373
435
|
});
|
|
374
436
|
}
|
|
375
437
|
|
|
376
|
-
patch(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta):
|
|
377
|
-
this.router.addRoute({
|
|
438
|
+
patch(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta): RouteRef {
|
|
439
|
+
return this.router.addRoute({
|
|
378
440
|
method: "PATCH",
|
|
379
441
|
pattern: this.prefix + path,
|
|
380
442
|
handler,
|
|
@@ -383,8 +445,8 @@ export class RouteGroup {
|
|
|
383
445
|
});
|
|
384
446
|
}
|
|
385
447
|
|
|
386
|
-
delete(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta):
|
|
387
|
-
this.router.addRoute({
|
|
448
|
+
delete(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta): RouteRef {
|
|
449
|
+
return this.router.addRoute({
|
|
388
450
|
method: "DELETE",
|
|
389
451
|
pattern: this.prefix + path,
|
|
390
452
|
handler,
|
|
@@ -393,9 +455,10 @@ export class RouteGroup {
|
|
|
393
455
|
});
|
|
394
456
|
}
|
|
395
457
|
|
|
396
|
-
any(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta):
|
|
458
|
+
any(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta): RouteRef {
|
|
459
|
+
let lastRef!: RouteRef;
|
|
397
460
|
for (const method of ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"]) {
|
|
398
|
-
this.router.addRoute({
|
|
461
|
+
lastRef = this.router.addRoute({
|
|
399
462
|
method,
|
|
400
463
|
pattern: this.prefix + path,
|
|
401
464
|
handler,
|
|
@@ -403,6 +466,7 @@ export class RouteGroup {
|
|
|
403
466
|
meta,
|
|
404
467
|
});
|
|
405
468
|
}
|
|
469
|
+
return lastRef;
|
|
406
470
|
}
|
|
407
471
|
|
|
408
472
|
/**
|
|
@@ -458,29 +522,29 @@ export const defaultRouter = new Router();
|
|
|
458
522
|
* res.json({ id: req.params.id }, 201);
|
|
459
523
|
* });
|
|
460
524
|
*/
|
|
461
|
-
export function get(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta):
|
|
462
|
-
defaultRouter.get(path, handler, middlewares, meta);
|
|
525
|
+
export function get(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta): RouteRef {
|
|
526
|
+
return defaultRouter.get(path, handler, middlewares, meta);
|
|
463
527
|
}
|
|
464
528
|
|
|
465
|
-
export function post(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta):
|
|
466
|
-
defaultRouter.post(path, handler, middlewares, meta);
|
|
529
|
+
export function post(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta): RouteRef {
|
|
530
|
+
return defaultRouter.post(path, handler, middlewares, meta);
|
|
467
531
|
}
|
|
468
532
|
|
|
469
|
-
export function put(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta):
|
|
470
|
-
defaultRouter.put(path, handler, middlewares, meta);
|
|
533
|
+
export function put(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta): RouteRef {
|
|
534
|
+
return defaultRouter.put(path, handler, middlewares, meta);
|
|
471
535
|
}
|
|
472
536
|
|
|
473
|
-
export function patch(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta):
|
|
474
|
-
defaultRouter.patch(path, handler, middlewares, meta);
|
|
537
|
+
export function patch(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta): RouteRef {
|
|
538
|
+
return defaultRouter.patch(path, handler, middlewares, meta);
|
|
475
539
|
}
|
|
476
540
|
|
|
477
541
|
// Named "del" to avoid conflict with the "delete" keyword; also exported as "delete" alias below.
|
|
478
|
-
export function del(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta):
|
|
479
|
-
defaultRouter.delete(path, handler, middlewares, meta);
|
|
542
|
+
export function del(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta): RouteRef {
|
|
543
|
+
return defaultRouter.delete(path, handler, middlewares, meta);
|
|
480
544
|
}
|
|
481
545
|
|
|
482
|
-
export function any(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta):
|
|
483
|
-
defaultRouter.any(path, handler, middlewares, meta);
|
|
546
|
+
export function any(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta): RouteRef {
|
|
547
|
+
return defaultRouter.any(path, handler, middlewares, meta);
|
|
484
548
|
}
|
|
485
549
|
|
|
486
550
|
export function websocket(path: string, handler: WebSocketRouteHandler): void {
|
|
@@ -29,6 +29,38 @@ const BUILTIN_PUBLIC_DIR = resolve(__dirname, "..", "public");
|
|
|
29
29
|
|
|
30
30
|
const TINA4_VERSION = "3.0.0";
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Test-bind each port in a subprocess to find one that is available.
|
|
34
|
+
* Falls back to `start` if none of the candidates work.
|
|
35
|
+
*/
|
|
36
|
+
function findAvailablePort(start: number, maxTries = 10): number {
|
|
37
|
+
const { execFileSync } = require("node:child_process");
|
|
38
|
+
for (let offset = 0; offset < maxTries; offset++) {
|
|
39
|
+
const port = start + offset;
|
|
40
|
+
try {
|
|
41
|
+
execFileSync(process.execPath, ["-e", `
|
|
42
|
+
const s = require("net").createServer();
|
|
43
|
+
s.listen(${port}, "127.0.0.1", () => { s.close(); process.exit(0); });
|
|
44
|
+
s.on("error", () => process.exit(1));
|
|
45
|
+
`], { timeout: 1000 });
|
|
46
|
+
return port;
|
|
47
|
+
} catch { continue; }
|
|
48
|
+
}
|
|
49
|
+
return start;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Open the user's default browser after a short delay so the server is ready.
|
|
54
|
+
*/
|
|
55
|
+
function openBrowser(url: string) {
|
|
56
|
+
const { exec } = require("node:child_process");
|
|
57
|
+
setTimeout(() => {
|
|
58
|
+
if (process.platform === "darwin") exec(`open ${url}`);
|
|
59
|
+
else if (process.platform === "win32") exec(`start "" "${url}"`);
|
|
60
|
+
else exec(`xdg-open ${url}`);
|
|
61
|
+
}, 2000);
|
|
62
|
+
}
|
|
63
|
+
|
|
32
64
|
/**
|
|
33
65
|
* Resolve port and host with priority: explicit config > ENV var > default.
|
|
34
66
|
* Exported for testability.
|
|
@@ -266,8 +298,10 @@ function deployGallery(name) {
|
|
|
266
298
|
if (d.error) {
|
|
267
299
|
alert('Deploy failed: ' + d.error);
|
|
268
300
|
} else {
|
|
269
|
-
|
|
270
|
-
|
|
301
|
+
// Brief delay to allow newly deployed routes to register before reloading
|
|
302
|
+
setTimeout(function() {
|
|
303
|
+
window.location.reload();
|
|
304
|
+
}, 500);
|
|
271
305
|
}
|
|
272
306
|
})
|
|
273
307
|
.catch(function(e) { alert('Deploy error: ' + e.message); });
|
|
@@ -285,7 +319,16 @@ export async function startServer(config?: Tina4Config): Promise<{
|
|
|
285
319
|
// Load .env early so TINA4_DEBUG is available for cluster decision
|
|
286
320
|
loadEnv();
|
|
287
321
|
|
|
288
|
-
const
|
|
322
|
+
const resolved = resolvePortAndHost(config);
|
|
323
|
+
const host = resolved.host;
|
|
324
|
+
let port = resolved.port;
|
|
325
|
+
|
|
326
|
+
// Auto-increment port if the requested one is already in use
|
|
327
|
+
const availablePort = findAvailablePort(port);
|
|
328
|
+
if (availablePort !== port) {
|
|
329
|
+
console.log(` Port ${port} in use, using ${availablePort} instead`);
|
|
330
|
+
port = availablePort;
|
|
331
|
+
}
|
|
289
332
|
|
|
290
333
|
// Cluster mode for production: fork workers based on CPU count
|
|
291
334
|
// Only when not in dev mode and running as primary process
|
|
@@ -669,6 +712,7 @@ ${reset}
|
|
|
669
712
|
Dashboard: http://localhost:${port}/__dev
|
|
670
713
|
Debug: ${isDebug ? "ON" : "OFF"} (Log level: ${logLevel})
|
|
671
714
|
`);
|
|
715
|
+
openBrowser(`http://${displayHost}:${port}`);
|
|
672
716
|
resolvePromise({
|
|
673
717
|
close: () => {
|
|
674
718
|
server.close();
|