tina4-nodejs 3.10.42 → 3.10.44

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.
@@ -68,7 +68,7 @@ export type { ResponseCacheConfig } from "./cache.js";
68
68
  export { Api } from "./api.js";
69
69
  export type { ApiResult } from "./api.js";
70
70
  export { Events } from "./events.js";
71
- export { DevAdmin, MessageLog, RequestInspector, ErrorTracker, DevMailboxStore, DevQueue, WsTracker } from "./devAdmin.js";
71
+ export { DevAdmin, MessageLog, RequestInspector, ErrorTracker, DevMailboxStore, DevQueue, WsTracker, renderDashboard } from "./devAdmin.js";
72
72
  export { Messenger } from "./messenger.js";
73
73
  export type { SendResult, EmailMessage } from "./messenger.js";
74
74
  export { DevMailbox, createMessenger } from "./devMailbox.js";
@@ -54,6 +54,18 @@ function relativePath(filePath: string): string {
54
54
  return path.relative(".", filePath);
55
55
  }
56
56
 
57
+ // ── Test file detection ─────────────────────────────────────
58
+
59
+ function hasMatchingTest(relPath: string): boolean {
60
+ const name = relPath.split('/').pop()?.replace('.ts', '').replace('.js', '') || '';
61
+ const patterns = [
62
+ `test/${name}.test.ts`,
63
+ `${relPath.replace('.ts', '.test.ts').replace('.js', '.test.js')}`,
64
+ `tests/${name}.test.ts`,
65
+ ];
66
+ return patterns.some(p => fs.existsSync(p));
67
+ }
68
+
57
69
  // ── Line counting ────────────────────────────────────────────
58
70
 
59
71
  interface LineCounts {
@@ -517,9 +529,27 @@ function detectViolations(
517
529
  return violations;
518
530
  }
519
531
 
532
+ // ── Root Resolution ──────────────────────────────────────────
533
+
534
+ /**
535
+ * Pick the right directory to scan.
536
+ *
537
+ * If the root dir has .ts files, scan the user's project code.
538
+ * Otherwise, scan the framework itself — so the bubble chart is never empty.
539
+ */
540
+ function resolveRoot(root: string = "src"): string {
541
+ const rootPath = path.resolve(root);
542
+ if (fs.existsSync(rootPath) && walkFiles(rootPath, [".ts", ".js"]).length > 0) {
543
+ return root;
544
+ }
545
+ // Fallback: scan the framework package itself
546
+ return path.resolve(path.dirname(new URL(import.meta.url).pathname));
547
+ }
548
+
520
549
  // ── Quick Metrics ────────────────────────────────────────────
521
550
 
522
551
  export function quickMetrics(root: string = "src"): Record<string, any> {
552
+ root = resolveRoot(root);
523
553
  const rootPath = path.resolve(root);
524
554
  if (!fs.existsSync(rootPath)) {
525
555
  return { error: `Directory not found: ${root}` };
@@ -644,6 +674,7 @@ function filesHash(root: string = "src"): string {
644
674
  }
645
675
 
646
676
  export function fullAnalysis(root: string = "src"): Record<string, any> {
677
+ root = resolveRoot(root);
647
678
  const currentHash = filesHash(root);
648
679
  const now = Date.now() / 1000;
649
680
 
@@ -730,6 +761,8 @@ export function fullAnalysis(root: string = "src"): Record<string, any> {
730
761
  coupling_afferent: ca,
731
762
  coupling_efferent: ce,
732
763
  instability: Math.round(instability * 1000) / 1000,
764
+ has_tests: hasMatchingTest(relPath),
765
+ dep_count: ce,
733
766
  });
734
767
  }
735
768
 
@@ -746,6 +779,10 @@ export function fullAnalysis(root: string = "src"): Record<string, any> {
746
779
  const totalMI = fileMetrics.reduce((sum, f) => sum + f.maintainability, 0);
747
780
  const avgMI = fileMetrics.length > 0 ? totalMI / fileMetrics.length : 0;
748
781
 
782
+ // Detect if we're scanning framework or project
783
+ const frameworkDir = path.resolve(path.dirname(new URL(import.meta.url).pathname));
784
+ const scanningFramework = rootPath === frameworkDir || rootPath.startsWith(frameworkDir + path.sep);
785
+
749
786
  const result: Record<string, any> = {
750
787
  files_analyzed: fileMetrics.length,
751
788
  total_functions: allFunctions.length,
@@ -755,6 +792,8 @@ export function fullAnalysis(root: string = "src"): Record<string, any> {
755
792
  file_metrics: fileMetrics,
756
793
  violations,
757
794
  dependency_graph: importGraph,
795
+ scan_mode: scanningFramework ? "framework" : "project",
796
+ scan_root: rootPath,
758
797
  };
759
798
 
760
799
  _fullCache = { hash: currentHash, data: result, time: now };
@@ -302,6 +302,8 @@ h1{font-size:3rem;font-weight:700;margin-bottom:0.25rem;letter-spacing:-1px}
302
302
  .gbtn-deploy{background:#3b82f6;color:#fff}
303
303
  .gbtn-deploy:hover{background:#2563eb}
304
304
  .gbtn-deployed{background:transparent;border:1px solid #22c55e;color:#22c55e;cursor:default;font-size:0.7rem}
305
+ @keyframes wiggle{0%{transform:rotate(0deg)}15%{transform:rotate(14deg)}30%{transform:rotate(-10deg)}45%{transform:rotate(8deg)}60%{transform:rotate(-4deg)}75%{transform:rotate(2deg)}100%{transform:rotate(0deg)}}
306
+ .star-wiggle{display:inline-block;transform-origin:center}
305
307
  </style>
306
308
  </head>
307
309
  <body>
@@ -315,7 +317,7 @@ h1{font-size:3rem;font-weight:700;margin-bottom:0.25rem;letter-spacing:-1px}
315
317
  <a href="/__dev" class="btn">Dev Admin</a>
316
318
  <a href="#gallery" class="btn">Gallery</a>
317
319
  <a href="https://github.com/tina4stack/tina4-nodejs" class="btn" target="_blank">GitHub</a>
318
- <a href="https://github.com/tina4stack/tina4-nodejs/stargazers" class="btn" target="_blank">&#11088; Star</a>
320
+ <a href="https://github.com/tina4stack/tina4-nodejs/stargazers" class="btn" target="_blank"><span class="star-wiggle">&#9734;</span> Star</a>
319
321
  </div>
320
322
  <div class="status">
321
323
  <span><span class="dot"></span>Server running</span>
@@ -364,6 +366,20 @@ function deployGallery(name) {
364
366
  })
365
367
  .catch(function(e) { alert('Deploy error: ' + e.message); });
366
368
  }
369
+ (function(){
370
+ var star=document.querySelector('.star-wiggle');
371
+ if(!star)return;
372
+ function doWiggle(){
373
+ star.style.animation='wiggle 1.2s ease-in-out';
374
+ star.addEventListener('animationend',function onEnd(){
375
+ star.removeEventListener('animationend',onEnd);
376
+ star.style.animation='none';
377
+ var delay=3000+Math.random()*15000;
378
+ setTimeout(doWiggle,delay);
379
+ });
380
+ }
381
+ setTimeout(doWiggle,3000);
382
+ })();
367
383
  </script>
368
384
  </body>
369
385
  </html>`;
@@ -731,7 +747,7 @@ ${reset}
731
747
  let result: unknown;
732
748
  const routeParams = req.params || {};
733
749
  const fnStr = match.handler.toString();
734
- const argMatch = fnStr.match(/^(?:async\s+)?(?:function\s*\w*)?\s*\(([^)]*)\)/);
750
+ const argMatch = fnStr.match(/^(?:async\s*)?(?:function\s*\w*)?\s*\(([^)]*)\)/);
735
751
  const argNames = argMatch?.[1]?.split(",").map((s: string) => s.trim().replace(/[:=].*/,"")) ?? [];
736
752
  const filteredArgs = argNames.filter((n: string) => n.length > 0);
737
753
 
@@ -57,9 +57,13 @@ export class SQLiteAdapter implements DatabaseAdapter {
57
57
  fetch<T = Record<string, unknown>>(sql: string, params?: unknown[], limit?: number, skip?: number): T[] {
58
58
  let effectiveSql = sql;
59
59
  if (limit !== undefined) {
60
- effectiveSql += ` LIMIT ${limit}`;
61
- if (skip !== undefined && skip > 0) {
62
- effectiveSql += ` OFFSET ${skip}`;
60
+ // Skip appending LIMIT when the SQL already contains one (dedup)
61
+ const sqlBeforeComment = sql.toUpperCase().split("--")[0];
62
+ if (!sqlBeforeComment.includes("LIMIT")) {
63
+ effectiveSql += ` LIMIT ${limit}`;
64
+ if (skip !== undefined && skip > 0) {
65
+ effectiveSql += ` OFFSET ${skip}`;
66
+ }
63
67
  }
64
68
  }
65
69
  return this.query<T>(effectiveSql, params);
@@ -17,6 +17,15 @@ export function camelToSnake(name: string): string {
17
17
  return name.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);
18
18
  }
19
19
 
20
+ /**
21
+ * Check whether ORM_PLURAL_TABLE_NAMES is enabled in .env.
22
+ * When true, hasMany relationship keys get an "s" suffix (e.g. "posts" instead of "post").
23
+ */
24
+ function _pluralRelKeys(): boolean {
25
+ const v = process.env.ORM_PLURAL_TABLE_NAMES ?? "";
26
+ return /^(true|1|yes)$/i.test(v);
27
+ }
28
+
20
29
  /**
21
30
  * BaseModel provides instance methods for ORM models.
22
31
  * Models extend this class and define static properties.
@@ -395,7 +404,8 @@ export class BaseModel {
395
404
  }
396
405
  if (ModelClass.hasMany) {
397
406
  for (const rel of ModelClass.hasMany) {
398
- const relKey = rel.model.toLowerCase() + "s";
407
+ const base = rel.model.toLowerCase();
408
+ const relKey = _pluralRelKeys() ? base + "s" : base;
399
409
  if (this[relKey] !== undefined) {
400
410
  result[relKey] = this[relKey];
401
411
  }
@@ -818,8 +828,9 @@ export class BaseModel {
818
828
  // Check hasMany
819
829
  if (ModelClass.hasMany) {
820
830
  const rel = ModelClass.hasMany.find((r) => {
821
- const key = r.model.toLowerCase() + "s";
822
- return key === relName || r.model.toLowerCase() === relName || r.model === relName;
831
+ const base = r.model.toLowerCase();
832
+ const key = _pluralRelKeys() ? base + "s" : base;
833
+ return key === relName || base === relName || r.model === relName;
823
834
  });
824
835
  if (rel) {
825
836
  const relatedClass = BaseModel._modelRegistry[rel.model];
@@ -877,8 +888,9 @@ export class BaseModel {
877
888
  }
878
889
  if (!relDef && ModelClass.hasMany) {
879
890
  relDef = ModelClass.hasMany.find((r) => {
880
- const key = r.model.toLowerCase() + "s";
881
- return key === relName || r.model.toLowerCase() === relName || r.model === relName;
891
+ const base = r.model.toLowerCase();
892
+ const key = _pluralRelKeys() ? base + "s" : base;
893
+ return key === relName || base === relName || r.model === relName;
882
894
  });
883
895
  if (relDef) relType = "hasMany";
884
896
  }