tina4-nodejs 3.10.48 → 3.10.55
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/package.json +1 -1
- package/packages/cli/src/bin.ts +3 -1
- package/packages/cli/src/commands/init.ts +25 -2
- package/packages/cli/src/commands/serve.ts +25 -16
- package/packages/core/src/devAdmin.ts +1 -1
- package/packages/core/src/request.ts +1 -1
- package/packages/core/src/router.ts +5 -0
- package/packages/core/src/server.ts +66 -11
- package/packages/core/src/session.ts +14 -6
- package/packages/orm/src/adapters/mongodb.ts +679 -0
- package/packages/orm/src/adapters/odbc.ts +413 -0
- package/packages/orm/src/adapters/sqlite.ts +23 -3
- package/packages/orm/src/autoCrud.ts +16 -7
- package/packages/orm/src/baseModel.ts +32 -5
- package/packages/orm/src/database.ts +73 -2
- package/packages/orm/src/databaseResult.ts +34 -5
- package/packages/orm/src/index.ts +4 -0
- package/packages/orm/src/query.ts +8 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tina4-nodejs",
|
|
3
|
-
"version": "3.10.
|
|
3
|
+
"version": "3.10.55",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Tina4 for Node.js/TypeScript — 54 built-in features, zero dependencies",
|
|
6
6
|
"keywords": ["tina4", "framework", "web", "api", "orm", "graphql", "websocket", "typescript"],
|
package/packages/cli/src/bin.ts
CHANGED
|
@@ -46,6 +46,7 @@ const HELP = `
|
|
|
46
46
|
Options:
|
|
47
47
|
--port <number> Server port (default: 7148)
|
|
48
48
|
--no-browser Don't open the browser on serve
|
|
49
|
+
--no-reload Disable file watcher / live-reload on serve
|
|
49
50
|
--all Install AI context for all tools (with ai command)
|
|
50
51
|
--force Overwrite existing AI context files (with ai command)
|
|
51
52
|
--help Show this help message
|
|
@@ -85,11 +86,12 @@ async function main(): Promise<void> {
|
|
|
85
86
|
const portIndex = args.indexOf("--port");
|
|
86
87
|
const port = portIndex !== -1 ? parseInt(args[portIndex + 1], 10) : 7148;
|
|
87
88
|
const noBrowser = args.includes("--no-browser");
|
|
89
|
+
const noReload = args.includes("--no-reload");
|
|
88
90
|
|
|
89
91
|
// Kill any existing process on the port
|
|
90
92
|
killProcessOnPort(port);
|
|
91
93
|
|
|
92
|
-
await serveProject({ port, noBrowser });
|
|
94
|
+
await serveProject({ port, noBrowser, noReload });
|
|
93
95
|
break;
|
|
94
96
|
}
|
|
95
97
|
case "migrate": {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { mkdirSync, writeFileSync, existsSync } from "node:fs";
|
|
2
|
-
import { join, resolve, basename } from "node:path";
|
|
1
|
+
import { mkdirSync, writeFileSync, existsSync, copyFileSync } from "node:fs";
|
|
2
|
+
import { join, resolve, basename, dirname } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
3
4
|
import { execSync } from "node:child_process";
|
|
4
5
|
|
|
5
6
|
export async function initProject(name: string): Promise<void> {
|
|
@@ -30,6 +31,28 @@ export async function initProject(name: string): Promise<void> {
|
|
|
30
31
|
mkdirSync(join(targetDir, dir), { recursive: true });
|
|
31
32
|
}
|
|
32
33
|
|
|
34
|
+
// Copy framework public assets into the project so they're visible
|
|
35
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
36
|
+
const __dirname = dirname(__filename);
|
|
37
|
+
const frameworkPublic = resolve(__dirname, "..", "..", "..", "core", "public");
|
|
38
|
+
const projectPublic = join(targetDir, "public");
|
|
39
|
+
const assetsToCopy = [
|
|
40
|
+
"css/tina4.css",
|
|
41
|
+
"css/tina4.min.css",
|
|
42
|
+
"js/tina4.min.js",
|
|
43
|
+
"js/frond.min.js",
|
|
44
|
+
"images/tina4-logo-icon.webp",
|
|
45
|
+
];
|
|
46
|
+
for (const asset of assetsToCopy) {
|
|
47
|
+
const src = join(frameworkPublic, ...asset.split("/"));
|
|
48
|
+
const dst = join(projectPublic, ...asset.split("/"));
|
|
49
|
+
mkdirSync(dirname(dst), { recursive: true });
|
|
50
|
+
if (existsSync(src) && !existsSync(dst)) {
|
|
51
|
+
copyFileSync(src, dst);
|
|
52
|
+
console.log(` Copied ${asset}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
33
56
|
// package.json
|
|
34
57
|
writeFileSync(
|
|
35
58
|
join(targetDir, "package.json"),
|
|
@@ -4,9 +4,14 @@ import { existsSync } from "node:fs";
|
|
|
4
4
|
export interface ServeOptions {
|
|
5
5
|
port?: number;
|
|
6
6
|
noBrowser?: boolean;
|
|
7
|
+
noReload?: boolean;
|
|
7
8
|
}
|
|
8
9
|
|
|
9
10
|
export async function serveProject(options: ServeOptions): Promise<void> {
|
|
11
|
+
if (options.noReload) {
|
|
12
|
+
process.env.TINA4_NO_RELOAD = "true";
|
|
13
|
+
}
|
|
14
|
+
|
|
10
15
|
const port = options.port ?? 7148;
|
|
11
16
|
const cwd = process.cwd();
|
|
12
17
|
|
|
@@ -21,8 +26,8 @@ export async function serveProject(options: ServeOptions): Promise<void> {
|
|
|
21
26
|
process.exit(1);
|
|
22
27
|
}
|
|
23
28
|
|
|
24
|
-
const { startServer } = await import("
|
|
25
|
-
const { watchForChanges } = await import("
|
|
29
|
+
const { startServer } = await import("../../../core/src/index.js");
|
|
30
|
+
const { watchForChanges } = await import("../../../core/src/watcher.js");
|
|
26
31
|
|
|
27
32
|
const server = await startServer({
|
|
28
33
|
port,
|
|
@@ -33,26 +38,30 @@ export async function serveProject(options: ServeOptions): Promise<void> {
|
|
|
33
38
|
});
|
|
34
39
|
|
|
35
40
|
// Watch for file changes and reload routes
|
|
41
|
+
const noReload = ["true", "1", "yes"].includes((process.env.TINA4_NO_RELOAD ?? "").toLowerCase());
|
|
36
42
|
const watchDirs = [routesDir, ormDir, modelsDir, templatesDir].filter((d) => existsSync(d));
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
let watcher: { close: () => void } | null = null;
|
|
44
|
+
if (!noReload) {
|
|
45
|
+
watcher = watchForChanges(watchDirs, async () => {
|
|
46
|
+
try {
|
|
47
|
+
const { discoverRoutes } = await import("../../../core/src/index.js");
|
|
48
|
+
// Clear routes BEFORE re-discovery to avoid stale duplicates
|
|
49
|
+
server.router.clear();
|
|
50
|
+
const routes = await discoverRoutes(routesDir);
|
|
51
|
+
for (const route of routes) {
|
|
52
|
+
server.router.addRoute(route);
|
|
53
|
+
}
|
|
54
|
+
console.log(` Reloaded ${routes.length} route(s)`);
|
|
55
|
+
} catch (err) {
|
|
56
|
+
console.error(" Error reloading routes:", err);
|
|
45
57
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
console.error(" Error reloading routes:", err);
|
|
49
|
-
}
|
|
50
|
-
});
|
|
58
|
+
});
|
|
59
|
+
}
|
|
51
60
|
|
|
52
61
|
// Graceful shutdown
|
|
53
62
|
const shutdown = () => {
|
|
54
63
|
console.log("\n Shutting down...");
|
|
55
|
-
watcher
|
|
64
|
+
watcher?.close();
|
|
56
65
|
server.close();
|
|
57
66
|
process.exit(0);
|
|
58
67
|
};
|
|
@@ -2467,7 +2467,7 @@ function renderBubbleChart(files,depGraph,scanMode){
|
|
|
2467
2467
|
bubbles.forEach(function(b,i){if(b.f.path===src)srcIdx=i;});
|
|
2468
2468
|
if(srcIdx===null)return;
|
|
2469
2469
|
(depGraph[src]||[]).forEach(function(tgt){
|
|
2470
|
-
var tgtName=tgt
|
|
2470
|
+
var tgtName=basename(tgt);
|
|
2471
2471
|
var tgtIdx=nameIdx[tgtName];
|
|
2472
2472
|
if(tgtIdx!==undefined&&srcIdx!==tgtIdx)edges.push([srcIdx,tgtIdx]);
|
|
2473
2473
|
});
|
|
@@ -106,7 +106,7 @@ function extractBoundary(contentType: string): string | null {
|
|
|
106
106
|
* Parse multipart/form-data body into fields and files.
|
|
107
107
|
* Zero-dependency implementation.
|
|
108
108
|
*/
|
|
109
|
-
function parseMultipart(
|
|
109
|
+
export function parseMultipart(
|
|
110
110
|
body: Buffer,
|
|
111
111
|
boundary: string,
|
|
112
112
|
): { fields: Record<string, string>; files: UploadedFile[] } {
|
|
@@ -413,6 +413,11 @@ export class Router {
|
|
|
413
413
|
paramNames.push(name);
|
|
414
414
|
return "([^/]+)";
|
|
415
415
|
}
|
|
416
|
+
// Wildcard: * (catch-all, param key is "*")
|
|
417
|
+
if (segment === "*") {
|
|
418
|
+
paramNames.push("*");
|
|
419
|
+
return "(.+)";
|
|
420
|
+
}
|
|
416
421
|
return segment;
|
|
417
422
|
})
|
|
418
423
|
.join("/");
|
|
@@ -29,7 +29,18 @@ const BUILTIN_ERROR_TEMPLATES_DIR = resolve(__dirname, "..", "templates");
|
|
|
29
29
|
/** Built-in public directory for framework-bundled static assets. */
|
|
30
30
|
const BUILTIN_PUBLIC_DIR = resolve(__dirname, "..", "public");
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
/** Read version from root package.json so the banner always matches the published version. */
|
|
33
|
+
function readPackageVersion(): string {
|
|
34
|
+
try {
|
|
35
|
+
const pkgPath = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "package.json");
|
|
36
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
37
|
+
return pkg.version ?? "0.0.0";
|
|
38
|
+
} catch {
|
|
39
|
+
return "0.0.0";
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const TINA4_VERSION = readPackageVersion();
|
|
33
44
|
|
|
34
45
|
/** Cache Frond instances by template directory to avoid repeated instantiation. */
|
|
35
46
|
const frondCache = new Map<string, InstanceType<any>>();
|
|
@@ -94,7 +105,7 @@ async function renderErrorPage(
|
|
|
94
105
|
templatesDir: string,
|
|
95
106
|
): Promise<string | null> {
|
|
96
107
|
try {
|
|
97
|
-
const { Frond } = await import("
|
|
108
|
+
const { Frond } = await import("../../frond/src/engine.js");
|
|
98
109
|
const templateFile = `errors/${code}.twig`;
|
|
99
110
|
|
|
100
111
|
// Helper: get-or-create a cached Frond instance for a directory
|
|
@@ -494,7 +505,7 @@ ${reset}
|
|
|
494
505
|
let frondEngine: any = null;
|
|
495
506
|
setDefaultTemplatesDir(templatesDir);
|
|
496
507
|
try {
|
|
497
|
-
const { Frond } = await import("
|
|
508
|
+
const { Frond } = await import("../../frond/src/engine.js");
|
|
498
509
|
frondEngine = new Frond(templatesDir);
|
|
499
510
|
} catch {
|
|
500
511
|
// Frond not available
|
|
@@ -524,7 +535,7 @@ ${reset}
|
|
|
524
535
|
const hasModelsDir = existsSync(modelsDir);
|
|
525
536
|
if (hasOrmDir || hasModelsDir) {
|
|
526
537
|
try {
|
|
527
|
-
const orm = await import("
|
|
538
|
+
const orm = await import("../../orm/src/index.js");
|
|
528
539
|
const dbConfig = config?.database ?? {};
|
|
529
540
|
await orm.initDatabase({
|
|
530
541
|
type: dbConfig.type ?? "sqlite",
|
|
@@ -572,13 +583,13 @@ ${reset}
|
|
|
572
583
|
|
|
573
584
|
// Initialize Swagger
|
|
574
585
|
try {
|
|
575
|
-
const swagger = await import("
|
|
586
|
+
const swagger = await import("../../swagger/src/index.js");
|
|
576
587
|
const allRoutes = router.getRoutes();
|
|
577
588
|
|
|
578
589
|
// Collect model definitions for schema generation
|
|
579
590
|
let modelDefs: Array<{ tableName: string; fields: Record<string, unknown> }> = [];
|
|
580
591
|
try {
|
|
581
|
-
const orm = await import("
|
|
592
|
+
const orm = await import("../../orm/src/index.js");
|
|
582
593
|
const allModelDirs = [ormDir, modelsDir].filter((d) => existsSync(d));
|
|
583
594
|
const seenTables = new Set<string>();
|
|
584
595
|
for (const dir of allModelDirs) {
|
|
@@ -662,7 +673,17 @@ ${reset}
|
|
|
662
673
|
const requestId = Date.now().toString(36);
|
|
663
674
|
|
|
664
675
|
// Wrap res.raw.end to inject dev toolbar and capture requests
|
|
665
|
-
|
|
676
|
+
// Skip toolbar injection on the AI port (no-reload behaviour)
|
|
677
|
+
const isAiPortRequest = !!(rawReq as any)._tina4AiPort;
|
|
678
|
+
|
|
679
|
+
// AI port: block /__dev_reload so AI tools never trigger a browser reload
|
|
680
|
+
if (isAiPortRequest && pathname === "/__dev_reload") {
|
|
681
|
+
res.raw.writeHead(404, { "Content-Type": "application/json" });
|
|
682
|
+
res.raw.end(JSON.stringify({ error: "Not available on AI port" }));
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
if (isDevMode() && !pathname.startsWith("/__dev") && !isAiPortRequest) {
|
|
666
687
|
const originalEnd = res.raw.end.bind(res.raw);
|
|
667
688
|
|
|
668
689
|
const wrappedEnd: typeof res.raw.end = function (
|
|
@@ -730,7 +751,9 @@ ${reset}
|
|
|
730
751
|
}
|
|
731
752
|
|
|
732
753
|
// Auth enforcement: secure routes require a valid Bearer token
|
|
733
|
-
|
|
754
|
+
// Dev admin routes (/__dev) are always public
|
|
755
|
+
const isDevAdmin = pathname.startsWith("/__dev");
|
|
756
|
+
if (match.secure === true && match.noAuth !== true && !isDevAdmin) {
|
|
734
757
|
const authHeader = req.headers.authorization ?? "";
|
|
735
758
|
const token = authHeader.startsWith("Bearer ") ? authHeader.slice(7) : "";
|
|
736
759
|
const secret = process.env.SECRET || "";
|
|
@@ -860,7 +883,35 @@ ${reset}
|
|
|
860
883
|
// Determine server mode label
|
|
861
884
|
const serverMode = isDebug ? "single" : (cluster.isWorker ? "cluster-worker" : "single");
|
|
862
885
|
|
|
886
|
+
// AI dual-port: start a second HTTP server on port+1 when in debug mode
|
|
887
|
+
// and TINA4_NO_AI_PORT is not set. This port serves requests without
|
|
888
|
+
// dev-reload or toolbar injection so AI coding tools get stable responses.
|
|
889
|
+
const noAiPort = isTruthy(process.env.TINA4_NO_AI_PORT ?? "");
|
|
890
|
+
let aiServer: ReturnType<typeof createServer> | null = null;
|
|
891
|
+
let aiPort = port + 1;
|
|
892
|
+
|
|
893
|
+
if (isDebug && !noAiPort) {
|
|
894
|
+
aiServer = createServer(async (req, res) => {
|
|
895
|
+
// Tag the request so dispatch knows it came from the AI port
|
|
896
|
+
(req as any)._tina4AiPort = true;
|
|
897
|
+
await dispatch(req, res);
|
|
898
|
+
});
|
|
899
|
+
|
|
900
|
+
aiServer.on("error", (err: any) => {
|
|
901
|
+
if (err.code === "EADDRINUSE") {
|
|
902
|
+
Log.warn(`AI port ${aiPort} in use — skipping`);
|
|
903
|
+
aiServer = null;
|
|
904
|
+
}
|
|
905
|
+
});
|
|
906
|
+
|
|
907
|
+
aiServer.listen(aiPort, host);
|
|
908
|
+
}
|
|
909
|
+
|
|
863
910
|
// Banner goes to stdout via console.log — NOT through the framework logger
|
|
911
|
+
const aiPortLine = (isDebug && !noAiPort)
|
|
912
|
+
? `\n AI Port: http://localhost:${aiPort} (no-reload)`
|
|
913
|
+
: "";
|
|
914
|
+
|
|
864
915
|
console.log(`${color}
|
|
865
916
|
______ _ __ __
|
|
866
917
|
/_ __/(_)___ ____ _/ // /
|
|
@@ -873,14 +924,18 @@ ${reset}
|
|
|
873
924
|
Server: http://${displayHost}:${port} (${serverMode})
|
|
874
925
|
Swagger: http://localhost:${port}/swagger
|
|
875
926
|
Dashboard: http://localhost:${port}/__dev
|
|
876
|
-
Debug: ${isDebug ? "ON" : "OFF"} (Log level: ${logLevel})
|
|
927
|
+
Debug: ${isDebug ? "ON" : "OFF"} (Log level: ${logLevel})${aiPortLine}
|
|
877
928
|
`);
|
|
878
|
-
|
|
929
|
+
const noBrowser = isTruthy(process.env.TINA4_NO_BROWSER);
|
|
930
|
+
if (!noBrowser) {
|
|
931
|
+
openBrowser(`http://${displayHost}:${port}`);
|
|
932
|
+
}
|
|
879
933
|
resolvePromise({
|
|
880
934
|
close: () => {
|
|
935
|
+
if (aiServer) aiServer.close();
|
|
881
936
|
server.close();
|
|
882
937
|
// Close database if ORM was initialized
|
|
883
|
-
import("
|
|
938
|
+
import("../../orm/src/index.js").then((orm) => orm.closeDatabase()).catch(() => {});
|
|
884
939
|
},
|
|
885
940
|
router,
|
|
886
941
|
port,
|
|
@@ -100,15 +100,23 @@ export class FileSessionHandler implements SessionHandler {
|
|
|
100
100
|
try {
|
|
101
101
|
if (!existsSync(filePath)) return null;
|
|
102
102
|
const raw = readFileSync(filePath, "utf-8");
|
|
103
|
-
|
|
103
|
+
const wrapper = JSON.parse(raw);
|
|
104
|
+
// Check expiry
|
|
105
|
+
if (wrapper._expires && wrapper._expires > 0 && Date.now() / 1000 > wrapper._expires) {
|
|
106
|
+
try { unlinkSync(filePath); } catch { /* ignore */ }
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
return (wrapper._data ?? wrapper) as SessionData;
|
|
104
110
|
} catch {
|
|
105
111
|
return null;
|
|
106
112
|
}
|
|
107
113
|
}
|
|
108
114
|
|
|
109
|
-
write(sessionId: string, data: SessionData,
|
|
115
|
+
write(sessionId: string, data: SessionData, ttl: number): void {
|
|
110
116
|
this.ensureDir();
|
|
111
|
-
|
|
117
|
+
const expires = ttl > 0 ? Math.floor(Date.now() / 1000) + ttl : 0;
|
|
118
|
+
const wrapper = { _data: data, _expires: expires };
|
|
119
|
+
writeFileSync(this.filePath(sessionId), JSON.stringify(wrapper), "utf-8");
|
|
112
120
|
}
|
|
113
121
|
|
|
114
122
|
destroy(sessionId: string): void {
|
|
@@ -118,7 +126,7 @@ export class FileSessionHandler implements SessionHandler {
|
|
|
118
126
|
} catch { /* ignore */ }
|
|
119
127
|
}
|
|
120
128
|
|
|
121
|
-
gc(
|
|
129
|
+
gc(_maxLifetime: number): void {
|
|
122
130
|
if (!existsSync(this.storagePath)) return;
|
|
123
131
|
const now = Math.floor(Date.now() / 1000);
|
|
124
132
|
try {
|
|
@@ -128,8 +136,8 @@ export class FileSessionHandler implements SessionHandler {
|
|
|
128
136
|
const fullPath = join(this.storagePath, file);
|
|
129
137
|
try {
|
|
130
138
|
const raw = readFileSync(fullPath, "utf-8");
|
|
131
|
-
const
|
|
132
|
-
if (
|
|
139
|
+
const wrapper = JSON.parse(raw);
|
|
140
|
+
if (wrapper._expires && wrapper._expires > 0 && now > wrapper._expires) {
|
|
133
141
|
unlinkSync(fullPath);
|
|
134
142
|
}
|
|
135
143
|
} catch {
|