tina4-nodejs 3.1.2 → 3.4.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 +30 -2
- 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/src/auth.ts +44 -10
- package/packages/core/src/devAdmin.ts +14 -16
- package/packages/core/src/errorOverlay.ts +17 -15
- package/packages/core/src/index.ts +9 -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/routeDiscovery.ts +2 -1
- package/packages/core/src/router.ts +90 -51
- package/packages/core/src/server.ts +62 -4
- package/packages/core/src/session.ts +17 -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
|
@@ -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,18 +19,42 @@ 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 {
|
|
@@ -38,7 +64,7 @@ export class Router {
|
|
|
38
64
|
/**
|
|
39
65
|
* Add a raw route definition (used internally and by file-based routing).
|
|
40
66
|
*/
|
|
41
|
-
addRoute(definition: RouteDefinition):
|
|
67
|
+
addRoute(definition: RouteDefinition): RouteRef {
|
|
42
68
|
const method = definition.method.toUpperCase();
|
|
43
69
|
const { regex, paramNames } = this.compilePattern(definition.pattern);
|
|
44
70
|
|
|
@@ -54,7 +80,7 @@ export class Router {
|
|
|
54
80
|
routes.splice(existingIndex, 1);
|
|
55
81
|
}
|
|
56
82
|
|
|
57
|
-
|
|
83
|
+
const compiled: CompiledRoute = {
|
|
58
84
|
pattern: definition.pattern,
|
|
59
85
|
regex,
|
|
60
86
|
paramNames,
|
|
@@ -62,52 +88,58 @@ export class Router {
|
|
|
62
88
|
meta: definition.meta,
|
|
63
89
|
filePath: definition.filePath,
|
|
64
90
|
middlewares: definition.middlewares,
|
|
91
|
+
secure: definition.secure,
|
|
92
|
+
cached: definition.cached,
|
|
65
93
|
template: definition.template,
|
|
66
|
-
}
|
|
94
|
+
};
|
|
95
|
+
routes.push(compiled);
|
|
96
|
+
return new RouteRef(compiled);
|
|
67
97
|
}
|
|
68
98
|
|
|
69
99
|
/**
|
|
70
100
|
* Register a GET route programmatically.
|
|
71
101
|
*/
|
|
72
|
-
get(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta):
|
|
73
|
-
this.addRoute({ method: "GET", pattern: path, handler, middlewares, meta });
|
|
102
|
+
get(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta): RouteRef {
|
|
103
|
+
return this.addRoute({ method: "GET", pattern: path, handler, middlewares, meta });
|
|
74
104
|
}
|
|
75
105
|
|
|
76
106
|
/**
|
|
77
107
|
* Register a POST route programmatically.
|
|
78
108
|
*/
|
|
79
|
-
post(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta):
|
|
80
|
-
this.addRoute({ method: "POST", pattern: path, handler, middlewares, meta });
|
|
109
|
+
post(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta): RouteRef {
|
|
110
|
+
return this.addRoute({ method: "POST", pattern: path, handler, middlewares, meta });
|
|
81
111
|
}
|
|
82
112
|
|
|
83
113
|
/**
|
|
84
114
|
* Register a PUT route programmatically.
|
|
85
115
|
*/
|
|
86
|
-
put(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta):
|
|
87
|
-
this.addRoute({ method: "PUT", pattern: path, handler, middlewares, meta });
|
|
116
|
+
put(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta): RouteRef {
|
|
117
|
+
return this.addRoute({ method: "PUT", pattern: path, handler, middlewares, meta });
|
|
88
118
|
}
|
|
89
119
|
|
|
90
120
|
/**
|
|
91
121
|
* Register a PATCH route programmatically.
|
|
92
122
|
*/
|
|
93
|
-
patch(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta):
|
|
94
|
-
this.addRoute({ method: "PATCH", pattern: path, handler, middlewares, meta });
|
|
123
|
+
patch(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta): RouteRef {
|
|
124
|
+
return this.addRoute({ method: "PATCH", pattern: path, handler, middlewares, meta });
|
|
95
125
|
}
|
|
96
126
|
|
|
97
127
|
/**
|
|
98
128
|
* Register a DELETE route programmatically.
|
|
99
129
|
*/
|
|
100
|
-
delete(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta):
|
|
101
|
-
this.addRoute({ method: "DELETE", pattern: path, handler, middlewares, meta });
|
|
130
|
+
delete(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta): RouteRef {
|
|
131
|
+
return this.addRoute({ method: "DELETE", pattern: path, handler, middlewares, meta });
|
|
102
132
|
}
|
|
103
133
|
|
|
104
134
|
/**
|
|
105
135
|
* Register a route that matches ANY HTTP method.
|
|
106
136
|
*/
|
|
107
|
-
any(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta):
|
|
137
|
+
any(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta): RouteRef {
|
|
138
|
+
let lastRef!: RouteRef;
|
|
108
139
|
for (const method of ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"]) {
|
|
109
|
-
this.addRoute({ method, pattern: path, handler, middlewares, meta });
|
|
140
|
+
lastRef = this.addRoute({ method, pattern: path, handler, middlewares, meta });
|
|
110
141
|
}
|
|
142
|
+
return lastRef;
|
|
111
143
|
}
|
|
112
144
|
|
|
113
145
|
/**
|
|
@@ -142,6 +174,8 @@ export class Router {
|
|
|
142
174
|
meta: route.meta,
|
|
143
175
|
middlewares: route.middlewares,
|
|
144
176
|
template: route.template,
|
|
177
|
+
secure: route.secure,
|
|
178
|
+
cached: route.cached,
|
|
145
179
|
};
|
|
146
180
|
}
|
|
147
181
|
}
|
|
@@ -164,6 +198,8 @@ export class Router {
|
|
|
164
198
|
filePath: route.filePath,
|
|
165
199
|
middlewares: route.middlewares,
|
|
166
200
|
template: route.template,
|
|
201
|
+
secure: route.secure,
|
|
202
|
+
cached: route.cached,
|
|
167
203
|
});
|
|
168
204
|
}
|
|
169
205
|
}
|
|
@@ -183,6 +219,7 @@ export class Router {
|
|
|
183
219
|
handler: route.filePath ?? (route.handler.name || "(anonymous)"),
|
|
184
220
|
middlewareCount: route.middlewares?.length ?? 0,
|
|
185
221
|
cached: route.cached ?? false,
|
|
222
|
+
secure: route.secure ?? false,
|
|
186
223
|
});
|
|
187
224
|
}
|
|
188
225
|
}
|
|
@@ -228,43 +265,43 @@ export class Router {
|
|
|
228
265
|
/**
|
|
229
266
|
* Register a GET route on the default global router.
|
|
230
267
|
*/
|
|
231
|
-
static get(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta):
|
|
232
|
-
defaultRouter.get(path, handler, middlewares, meta);
|
|
268
|
+
static get(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta): RouteRef {
|
|
269
|
+
return defaultRouter.get(path, handler, middlewares, meta);
|
|
233
270
|
}
|
|
234
271
|
|
|
235
272
|
/**
|
|
236
273
|
* Register a POST route on the default global router.
|
|
237
274
|
*/
|
|
238
|
-
static post(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta):
|
|
239
|
-
defaultRouter.post(path, handler, middlewares, meta);
|
|
275
|
+
static post(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta): RouteRef {
|
|
276
|
+
return defaultRouter.post(path, handler, middlewares, meta);
|
|
240
277
|
}
|
|
241
278
|
|
|
242
279
|
/**
|
|
243
280
|
* Register a PUT route on the default global router.
|
|
244
281
|
*/
|
|
245
|
-
static put(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta):
|
|
246
|
-
defaultRouter.put(path, handler, middlewares, meta);
|
|
282
|
+
static put(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta): RouteRef {
|
|
283
|
+
return defaultRouter.put(path, handler, middlewares, meta);
|
|
247
284
|
}
|
|
248
285
|
|
|
249
286
|
/**
|
|
250
287
|
* Register a PATCH route on the default global router.
|
|
251
288
|
*/
|
|
252
|
-
static patch(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta):
|
|
253
|
-
defaultRouter.patch(path, handler, middlewares, meta);
|
|
289
|
+
static patch(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta): RouteRef {
|
|
290
|
+
return defaultRouter.patch(path, handler, middlewares, meta);
|
|
254
291
|
}
|
|
255
292
|
|
|
256
293
|
/**
|
|
257
294
|
* Register a DELETE route on the default global router.
|
|
258
295
|
*/
|
|
259
|
-
static delete(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta):
|
|
260
|
-
defaultRouter.delete(path, handler, middlewares, meta);
|
|
296
|
+
static delete(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta): RouteRef {
|
|
297
|
+
return defaultRouter.delete(path, handler, middlewares, meta);
|
|
261
298
|
}
|
|
262
299
|
|
|
263
300
|
/**
|
|
264
301
|
* Register a route that matches ANY HTTP method on the default global router.
|
|
265
302
|
*/
|
|
266
|
-
static any(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta):
|
|
267
|
-
defaultRouter.any(path, handler, middlewares, meta);
|
|
303
|
+
static any(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta): RouteRef {
|
|
304
|
+
return defaultRouter.any(path, handler, middlewares, meta);
|
|
268
305
|
}
|
|
269
306
|
|
|
270
307
|
/**
|
|
@@ -343,8 +380,8 @@ export class RouteGroup {
|
|
|
343
380
|
return merged.length > 0 ? merged : undefined;
|
|
344
381
|
}
|
|
345
382
|
|
|
346
|
-
get(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta):
|
|
347
|
-
this.router.addRoute({
|
|
383
|
+
get(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta): RouteRef {
|
|
384
|
+
return this.router.addRoute({
|
|
348
385
|
method: "GET",
|
|
349
386
|
pattern: this.prefix + path,
|
|
350
387
|
handler,
|
|
@@ -353,8 +390,8 @@ export class RouteGroup {
|
|
|
353
390
|
});
|
|
354
391
|
}
|
|
355
392
|
|
|
356
|
-
post(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta):
|
|
357
|
-
this.router.addRoute({
|
|
393
|
+
post(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta): RouteRef {
|
|
394
|
+
return this.router.addRoute({
|
|
358
395
|
method: "POST",
|
|
359
396
|
pattern: this.prefix + path,
|
|
360
397
|
handler,
|
|
@@ -363,8 +400,8 @@ export class RouteGroup {
|
|
|
363
400
|
});
|
|
364
401
|
}
|
|
365
402
|
|
|
366
|
-
put(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta):
|
|
367
|
-
this.router.addRoute({
|
|
403
|
+
put(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta): RouteRef {
|
|
404
|
+
return this.router.addRoute({
|
|
368
405
|
method: "PUT",
|
|
369
406
|
pattern: this.prefix + path,
|
|
370
407
|
handler,
|
|
@@ -373,8 +410,8 @@ export class RouteGroup {
|
|
|
373
410
|
});
|
|
374
411
|
}
|
|
375
412
|
|
|
376
|
-
patch(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta):
|
|
377
|
-
this.router.addRoute({
|
|
413
|
+
patch(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta): RouteRef {
|
|
414
|
+
return this.router.addRoute({
|
|
378
415
|
method: "PATCH",
|
|
379
416
|
pattern: this.prefix + path,
|
|
380
417
|
handler,
|
|
@@ -383,8 +420,8 @@ export class RouteGroup {
|
|
|
383
420
|
});
|
|
384
421
|
}
|
|
385
422
|
|
|
386
|
-
delete(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta):
|
|
387
|
-
this.router.addRoute({
|
|
423
|
+
delete(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta): RouteRef {
|
|
424
|
+
return this.router.addRoute({
|
|
388
425
|
method: "DELETE",
|
|
389
426
|
pattern: this.prefix + path,
|
|
390
427
|
handler,
|
|
@@ -393,9 +430,10 @@ export class RouteGroup {
|
|
|
393
430
|
});
|
|
394
431
|
}
|
|
395
432
|
|
|
396
|
-
any(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta):
|
|
433
|
+
any(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta): RouteRef {
|
|
434
|
+
let lastRef!: RouteRef;
|
|
397
435
|
for (const method of ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"]) {
|
|
398
|
-
this.router.addRoute({
|
|
436
|
+
lastRef = this.router.addRoute({
|
|
399
437
|
method,
|
|
400
438
|
pattern: this.prefix + path,
|
|
401
439
|
handler,
|
|
@@ -403,6 +441,7 @@ export class RouteGroup {
|
|
|
403
441
|
meta,
|
|
404
442
|
});
|
|
405
443
|
}
|
|
444
|
+
return lastRef;
|
|
406
445
|
}
|
|
407
446
|
|
|
408
447
|
/**
|
|
@@ -458,29 +497,29 @@ export const defaultRouter = new Router();
|
|
|
458
497
|
* res.json({ id: req.params.id }, 201);
|
|
459
498
|
* });
|
|
460
499
|
*/
|
|
461
|
-
export function get(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta):
|
|
462
|
-
defaultRouter.get(path, handler, middlewares, meta);
|
|
500
|
+
export function get(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta): RouteRef {
|
|
501
|
+
return defaultRouter.get(path, handler, middlewares, meta);
|
|
463
502
|
}
|
|
464
503
|
|
|
465
|
-
export function post(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta):
|
|
466
|
-
defaultRouter.post(path, handler, middlewares, meta);
|
|
504
|
+
export function post(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta): RouteRef {
|
|
505
|
+
return defaultRouter.post(path, handler, middlewares, meta);
|
|
467
506
|
}
|
|
468
507
|
|
|
469
|
-
export function put(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta):
|
|
470
|
-
defaultRouter.put(path, handler, middlewares, meta);
|
|
508
|
+
export function put(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta): RouteRef {
|
|
509
|
+
return defaultRouter.put(path, handler, middlewares, meta);
|
|
471
510
|
}
|
|
472
511
|
|
|
473
|
-
export function patch(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta):
|
|
474
|
-
defaultRouter.patch(path, handler, middlewares, meta);
|
|
512
|
+
export function patch(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta): RouteRef {
|
|
513
|
+
return defaultRouter.patch(path, handler, middlewares, meta);
|
|
475
514
|
}
|
|
476
515
|
|
|
477
516
|
// 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);
|
|
517
|
+
export function del(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta): RouteRef {
|
|
518
|
+
return defaultRouter.delete(path, handler, middlewares, meta);
|
|
480
519
|
}
|
|
481
520
|
|
|
482
|
-
export function any(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta):
|
|
483
|
-
defaultRouter.any(path, handler, middlewares, meta);
|
|
521
|
+
export function any(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta): RouteRef {
|
|
522
|
+
return defaultRouter.any(path, handler, middlewares, meta);
|
|
484
523
|
}
|
|
485
524
|
|
|
486
525
|
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
|
|
@@ -553,7 +596,21 @@ ${reset}
|
|
|
553
596
|
if (!proceed || res.raw.writableEnded) return;
|
|
554
597
|
}
|
|
555
598
|
|
|
556
|
-
|
|
599
|
+
// Support (), (response), (request), or (request, response) handler signatures
|
|
600
|
+
// When 1 param: if named request/req, pass request; otherwise pass response
|
|
601
|
+
let result: unknown;
|
|
602
|
+
if (match.handler.length === 0) {
|
|
603
|
+
result = await (match.handler as any)();
|
|
604
|
+
} else if (match.handler.length === 1) {
|
|
605
|
+
const fnStr = match.handler.toString();
|
|
606
|
+
const paramMatch = fnStr.match(/^(?:async\s+)?(?:function\s*)?\(?\s*(\w+)/);
|
|
607
|
+
const paramName = paramMatch?.[1]?.toLowerCase() ?? "";
|
|
608
|
+
result = (paramName === "request" || paramName === "req")
|
|
609
|
+
? await match.handler(req as any)
|
|
610
|
+
: await match.handler(res as any);
|
|
611
|
+
} else {
|
|
612
|
+
result = await match.handler(req, res);
|
|
613
|
+
}
|
|
557
614
|
|
|
558
615
|
// If the route exports a template and the handler returned a plain object,
|
|
559
616
|
// render it through the template engine instead of sending as JSON.
|
|
@@ -655,6 +712,7 @@ ${reset}
|
|
|
655
712
|
Dashboard: http://localhost:${port}/__dev
|
|
656
713
|
Debug: ${isDebug ? "ON" : "OFF"} (Log level: ${logLevel})
|
|
657
714
|
`);
|
|
715
|
+
openBrowser(`http://${displayHost}:${port}`);
|
|
658
716
|
resolvePromise({
|
|
659
717
|
close: () => {
|
|
660
718
|
server.close();
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Tina4 Session — Pluggable session backends, zero core dependencies.
|
|
3
3
|
*
|
|
4
4
|
* File-based sessions by default. Redis backend available via raw TCP (no ioredis needed).
|
|
5
|
+
* Database (SQLite) backend available via better-sqlite3.
|
|
5
6
|
*
|
|
6
7
|
* import { Session, RedisSessionHandler } from "@tina4/core";
|
|
7
8
|
*
|
|
@@ -14,6 +15,10 @@
|
|
|
14
15
|
* redisPort: 6379,
|
|
15
16
|
* });
|
|
16
17
|
*
|
|
18
|
+
* // Database backend (SQLite via better-sqlite3)
|
|
19
|
+
* const session = new Session("database");
|
|
20
|
+
* // or: new Session("db");
|
|
21
|
+
*
|
|
17
22
|
* const id = session.start();
|
|
18
23
|
* session.set("user", { name: "Alice" });
|
|
19
24
|
* session.get("user"); // { name: "Alice" }
|
|
@@ -27,7 +32,7 @@ import { execFileSync } from "node:child_process";
|
|
|
27
32
|
// ── Types ─────────────────────────────────────────────────────────
|
|
28
33
|
|
|
29
34
|
export interface SessionConfig {
|
|
30
|
-
/** Session backend type: "file" or "
|
|
35
|
+
/** Session backend type: "file", "redis", "valkey", "mongo", "database" (or "db") */
|
|
31
36
|
backend?: string;
|
|
32
37
|
/** File storage path (default: "data/sessions") */
|
|
33
38
|
path?: string;
|
|
@@ -297,6 +302,11 @@ export class Session {
|
|
|
297
302
|
case "redis":
|
|
298
303
|
this.handler = new RedisSessionHandler(config);
|
|
299
304
|
break;
|
|
305
|
+
case "redis-npm": {
|
|
306
|
+
const { RedisNpmSessionHandler } = require("./sessionHandlers/redisHandler.js");
|
|
307
|
+
this.handler = new RedisNpmSessionHandler(config);
|
|
308
|
+
break;
|
|
309
|
+
}
|
|
300
310
|
case "valkey": {
|
|
301
311
|
const { ValkeySessionHandler } = require("./sessionHandlers/valkeyHandler.js");
|
|
302
312
|
this.handler = new ValkeySessionHandler(config);
|
|
@@ -308,6 +318,12 @@ export class Session {
|
|
|
308
318
|
this.handler = new MongoSessionHandler(config);
|
|
309
319
|
break;
|
|
310
320
|
}
|
|
321
|
+
case "database":
|
|
322
|
+
case "db": {
|
|
323
|
+
const { DatabaseSessionHandler } = require("./sessionHandlers/databaseHandler.js");
|
|
324
|
+
this.handler = new DatabaseSessionHandler(config);
|
|
325
|
+
break;
|
|
326
|
+
}
|
|
311
327
|
case "file":
|
|
312
328
|
default:
|
|
313
329
|
this.handler = new FileSessionHandler(config?.path);
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tina4 Database Session Handler — SQLite via better-sqlite3, zero extra dependencies.
|
|
3
|
+
*
|
|
4
|
+
* Uses the same `better-sqlite3` library the ORM already depends on.
|
|
5
|
+
* Stores sessions in a `tina4_session` table with JSON data and expiry.
|
|
6
|
+
*
|
|
7
|
+
* Configure via environment variables:
|
|
8
|
+
* DATABASE_URL (default: "sqlite:///data/tina4_sessions.db")
|
|
9
|
+
*
|
|
10
|
+
* The handler dynamically imports `better-sqlite3` and throws a clear
|
|
11
|
+
* error if the package is not installed.
|
|
12
|
+
*/
|
|
13
|
+
import { createRequire } from "node:module";
|
|
14
|
+
import type { SessionHandler } from "../session.js";
|
|
15
|
+
|
|
16
|
+
const _require = createRequire(import.meta.url);
|
|
17
|
+
|
|
18
|
+
interface SessionData {
|
|
19
|
+
_created: number;
|
|
20
|
+
_accessed: number;
|
|
21
|
+
[key: string]: unknown;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface DatabaseSessionConfig {
|
|
25
|
+
/** SQLite database file path (default: extracted from DATABASE_URL or "data/tina4_sessions.db") */
|
|
26
|
+
dbPath?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Database session handler using better-sqlite3 (synchronous SQLite).
|
|
31
|
+
*
|
|
32
|
+
* Stores session data as JSON in a `tina4_session` table.
|
|
33
|
+
* Expiry is checked on read; expired rows are cleaned up lazily.
|
|
34
|
+
*/
|
|
35
|
+
export class DatabaseSessionHandler implements SessionHandler {
|
|
36
|
+
private db: any;
|
|
37
|
+
private initialized = false;
|
|
38
|
+
|
|
39
|
+
constructor(config?: DatabaseSessionConfig) {
|
|
40
|
+
const dbPath = config?.dbPath ?? this.resolveDbPath();
|
|
41
|
+
|
|
42
|
+
let Database: any;
|
|
43
|
+
try {
|
|
44
|
+
Database = _require("better-sqlite3");
|
|
45
|
+
} catch {
|
|
46
|
+
throw new Error(
|
|
47
|
+
"DatabaseSessionHandler requires 'better-sqlite3'. " +
|
|
48
|
+
"Install it with: npm install better-sqlite3"
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
this.db = new Database(dbPath);
|
|
53
|
+
this.db.pragma("journal_mode = WAL");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Resolve the database file path from DATABASE_URL or use the default.
|
|
58
|
+
*/
|
|
59
|
+
private resolveDbPath(): string {
|
|
60
|
+
const url = process.env.DATABASE_URL;
|
|
61
|
+
if (url && url.startsWith("sqlite://")) {
|
|
62
|
+
// sqlite:///path/to/db or sqlite://./relative/path
|
|
63
|
+
return url.replace(/^sqlite:\/\//, "");
|
|
64
|
+
}
|
|
65
|
+
return "data/tina4_sessions.db";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Ensure the session table exists (called once on first use).
|
|
70
|
+
*/
|
|
71
|
+
private ensureTable(): void {
|
|
72
|
+
if (this.initialized) return;
|
|
73
|
+
this.db.exec(`
|
|
74
|
+
CREATE TABLE IF NOT EXISTS tina4_session (
|
|
75
|
+
session_id TEXT PRIMARY KEY,
|
|
76
|
+
data TEXT NOT NULL,
|
|
77
|
+
expires_at REAL NOT NULL
|
|
78
|
+
)
|
|
79
|
+
`);
|
|
80
|
+
this.initialized = true;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
read(sessionId: string): SessionData | null {
|
|
84
|
+
this.ensureTable();
|
|
85
|
+
|
|
86
|
+
const row = this.db
|
|
87
|
+
.prepare("SELECT data, expires_at FROM tina4_session WHERE session_id = ?")
|
|
88
|
+
.get(sessionId) as { data: string; expires_at: number } | undefined;
|
|
89
|
+
|
|
90
|
+
if (!row) return null;
|
|
91
|
+
|
|
92
|
+
// Check expiry
|
|
93
|
+
const now = Date.now() / 1000;
|
|
94
|
+
if (row.expires_at < now) {
|
|
95
|
+
// Expired — clean up and return null
|
|
96
|
+
this.destroy(sessionId);
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
return JSON.parse(row.data) as SessionData;
|
|
102
|
+
} catch {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
write(sessionId: string, data: SessionData, ttl: number): void {
|
|
108
|
+
this.ensureTable();
|
|
109
|
+
|
|
110
|
+
const json = JSON.stringify(data);
|
|
111
|
+
const expiresAt = (Date.now() / 1000) + (ttl > 0 ? ttl : 3600);
|
|
112
|
+
|
|
113
|
+
const existing = this.db
|
|
114
|
+
.prepare("SELECT 1 FROM tina4_session WHERE session_id = ?")
|
|
115
|
+
.get(sessionId);
|
|
116
|
+
|
|
117
|
+
if (existing) {
|
|
118
|
+
this.db
|
|
119
|
+
.prepare("UPDATE tina4_session SET data = ?, expires_at = ? WHERE session_id = ?")
|
|
120
|
+
.run(json, expiresAt, sessionId);
|
|
121
|
+
} else {
|
|
122
|
+
this.db
|
|
123
|
+
.prepare("INSERT INTO tina4_session (session_id, data, expires_at) VALUES (?, ?, ?)")
|
|
124
|
+
.run(sessionId, json, expiresAt);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
destroy(sessionId: string): void {
|
|
129
|
+
this.ensureTable();
|
|
130
|
+
this.db
|
|
131
|
+
.prepare("DELETE FROM tina4_session WHERE session_id = ?")
|
|
132
|
+
.run(sessionId);
|
|
133
|
+
}
|
|
134
|
+
}
|