tina4-nodejs 3.10.47 → 3.10.50

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tina4-nodejs",
3
- "version": "3.10.47",
3
+ "version": "3.10.50",
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"],
@@ -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"),
@@ -21,8 +21,8 @@ export async function serveProject(options: ServeOptions): Promise<void> {
21
21
  process.exit(1);
22
22
  }
23
23
 
24
- const { startServer } = await import("@tina4/core");
25
- const { watchForChanges } = await import("@tina4/core/src/watcher.js");
24
+ const { startServer } = await import("../../../core/src/index.js");
25
+ const { watchForChanges } = await import("../../../core/src/watcher.js");
26
26
 
27
27
  const server = await startServer({
28
28
  port,
@@ -36,7 +36,7 @@ export async function serveProject(options: ServeOptions): Promise<void> {
36
36
  const watchDirs = [routesDir, ormDir, modelsDir, templatesDir].filter((d) => existsSync(d));
37
37
  const watcher = watchForChanges(watchDirs, async () => {
38
38
  try {
39
- const { discoverRoutes } = await import("@tina4/core");
39
+ const { discoverRoutes } = await import("../../../core/src/index.js");
40
40
  // Clear routes BEFORE re-discovery to avoid stale duplicates
41
41
  server.router.clear();
42
42
  const routes = await discoverRoutes(routesDir);
@@ -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
- const TINA4_VERSION = "3.10.30";
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("@tina4/frond");
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
@@ -419,8 +430,9 @@ export async function startServer(config?: Tina4Config): Promise<{
419
430
  }
420
431
 
421
432
  // Cluster mode for production: fork workers based on CPU count
422
- // Only when not in dev mode and running as primary process
423
- if (cluster.isPrimary && !isDevMode()) {
433
+ // Only when --production is explicitly set (via TINA4_PRODUCTION env var)
434
+ const isProduction = (process.env.TINA4_PRODUCTION ?? "").toLowerCase() === "true";
435
+ if (cluster.isPrimary && isProduction) {
424
436
  const numCPUs = os.cpus().length;
425
437
  if (numCPUs > 1) {
426
438
  const displayHost = host === "0.0.0.0" ? "localhost" : host;
@@ -493,7 +505,7 @@ ${reset}
493
505
  let frondEngine: any = null;
494
506
  setDefaultTemplatesDir(templatesDir);
495
507
  try {
496
- const { Frond } = await import("@tina4/frond");
508
+ const { Frond } = await import("../../frond/src/engine.js");
497
509
  frondEngine = new Frond(templatesDir);
498
510
  } catch {
499
511
  // Frond not available
@@ -523,7 +535,7 @@ ${reset}
523
535
  const hasModelsDir = existsSync(modelsDir);
524
536
  if (hasOrmDir || hasModelsDir) {
525
537
  try {
526
- const orm = await import("@tina4/orm");
538
+ const orm = await import("../../orm/src/index.js");
527
539
  const dbConfig = config?.database ?? {};
528
540
  await orm.initDatabase({
529
541
  type: dbConfig.type ?? "sqlite",
@@ -571,13 +583,13 @@ ${reset}
571
583
 
572
584
  // Initialize Swagger
573
585
  try {
574
- const swagger = await import("@tina4/swagger");
586
+ const swagger = await import("../../swagger/src/index.js");
575
587
  const allRoutes = router.getRoutes();
576
588
 
577
589
  // Collect model definitions for schema generation
578
590
  let modelDefs: Array<{ tableName: string; fields: Record<string, unknown> }> = [];
579
591
  try {
580
- const orm = await import("@tina4/orm");
592
+ const orm = await import("../../orm/src/index.js");
581
593
  const allModelDirs = [ormDir, modelsDir].filter((d) => existsSync(d));
582
594
  const seenTables = new Set<string>();
583
595
  for (const dir of allModelDirs) {
@@ -729,7 +741,9 @@ ${reset}
729
741
  }
730
742
 
731
743
  // Auth enforcement: secure routes require a valid Bearer token
732
- if (match.secure === true && match.noAuth !== true) {
744
+ // Dev admin routes (/__dev) are always public
745
+ const isDevAdmin = pathname.startsWith("/__dev");
746
+ if (match.secure === true && match.noAuth !== true && !isDevAdmin) {
733
747
  const authHeader = req.headers.authorization ?? "";
734
748
  const token = authHeader.startsWith("Bearer ") ? authHeader.slice(7) : "";
735
749
  const secret = process.env.SECRET || "";
@@ -874,12 +888,15 @@ ${reset}
874
888
  Dashboard: http://localhost:${port}/__dev
875
889
  Debug: ${isDebug ? "ON" : "OFF"} (Log level: ${logLevel})
876
890
  `);
877
- openBrowser(`http://${displayHost}:${port}`);
891
+ const noBrowser = isTruthy(process.env.TINA4_NO_BROWSER);
892
+ if (!noBrowser) {
893
+ openBrowser(`http://${displayHost}:${port}`);
894
+ }
878
895
  resolvePromise({
879
896
  close: () => {
880
897
  server.close();
881
898
  // Close database if ORM was initialized
882
- import("@tina4/orm").then((orm) => orm.closeDatabase()).catch(() => {});
899
+ import("../../orm/src/index.js").then((orm) => orm.closeDatabase()).catch(() => {});
883
900
  },
884
901
  router,
885
902
  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
- return JSON.parse(raw) as SessionData;
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, _ttl: number): void {
115
+ write(sessionId: string, data: SessionData, ttl: number): void {
110
116
  this.ensureDir();
111
- writeFileSync(this.filePath(sessionId), JSON.stringify(data), "utf-8");
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(maxLifetime: number): void {
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 data = JSON.parse(raw) as SessionData;
132
- if (data._accessed && (now - data._accessed) > maxLifetime) {
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 {
@@ -45,7 +45,7 @@ export function generateCrudRoutes(models: DiscoveredModel[]): RouteDefinition[]
45
45
  const items = adapter.query(sql, params);
46
46
  const [{ total }] = adapter.query<{ total: number }>(countSql, countParams);
47
47
 
48
- const limit = options.limit ?? 20;
48
+ const limit = options.limit ?? 100;
49
49
  const page = options.page ?? 1;
50
50
 
51
51
  res.json({
@@ -73,18 +73,27 @@ export class DatabaseResult implements Iterable<Record<string, unknown>> {
73
73
  return this.records;
74
74
  }
75
75
 
76
- /** Pagination envelope. */
77
- toPaginate(): {
78
- records: Record<string, unknown>[];
79
- count: number;
80
- limit: number;
81
- offset: number;
76
+ /** Pagination envelope with slicing. */
77
+ toPaginate(page = 1, perPage = 10): {
78
+ data: Record<string, unknown>[];
79
+ page: number;
80
+ per_page: number;
81
+ total: number;
82
+ total_pages: number;
83
+ has_next: boolean;
84
+ has_prev: boolean;
82
85
  } {
86
+ const totalPages = Math.max(1, Math.ceil(this.count / perPage));
87
+ const start = (page - 1) * perPage;
88
+ const end = start + perPage;
83
89
  return {
84
- records: this.records,
85
- count: this.count,
86
- limit: this.limit,
87
- offset: this.offset,
90
+ data: this.records.slice(start, end),
91
+ page,
92
+ per_page: perPage,
93
+ total: this.count,
94
+ total_pages: totalPages,
95
+ has_next: page < totalPages,
96
+ has_prev: page > 1,
88
97
  };
89
98
  }
90
99
 
@@ -58,7 +58,7 @@ export function buildQuery(
58
58
  }
59
59
 
60
60
  // Pagination
61
- const limit = options.limit ?? 20;
61
+ const limit = options.limit ?? 100;
62
62
  const page = options.page ?? 1;
63
63
  const offset = (page - 1) * limit;
64
64