tina4-nodejs 3.10.98 → 3.11.1

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 CHANGED
@@ -1,4 +1,4 @@
1
- # CLAUDE.md — AI Developer Guide for tina4-nodejs (v3.10.97)
1
+ # CLAUDE.md — AI Developer Guide for tina4-nodejs (v3.11.0)
2
2
 
3
3
  > This file helps AI assistants (Claude, Copilot, Cursor, etc.) understand and work on this codebase effectively.
4
4
 
package/package.json CHANGED
@@ -1,6 +1,10 @@
1
1
  {
2
2
  "name": "tina4-nodejs",
3
- "version": "3.10.98",
3
+
4
+
5
+
6
+ "version": "3.11.1",
7
+
4
8
  "type": "module",
5
9
  "description": "Tina4 for Node.js/TypeScript — 54 built-in features, zero dependencies",
6
10
  "keywords": ["tina4", "framework", "web", "api", "orm", "graphql", "websocket", "typescript"],
@@ -48,7 +48,7 @@ export class ScssCompiler {
48
48
  }
49
49
 
50
50
  /** Compile all .scss files in a directory into a single CSS output file. */
51
- compileScss(scssDir: string = "src/scss", output: string = "public/css/default.css", minify: boolean = false): string {
51
+ compileScss(scssDir: string = "src/scss", output: string = "src/public/css/default.css", minify: boolean = false): string {
52
52
  const absDir = resolve(scssDir);
53
53
  if (!existsSync(absDir)) return "";
54
54
 
@@ -80,11 +80,19 @@ export class ScssCompiler {
80
80
  css = css.trim();
81
81
  }
82
82
 
83
- // Write output
83
+ // Write output only if content changed (avoids triggering DevReload loops)
84
84
  const absOutput = resolve(output);
85
85
  const outDir = dirname(absOutput);
86
86
  if (!existsSync(outDir)) mkdirSync(outDir, { recursive: true });
87
- writeFileSync(absOutput, css, "utf-8");
87
+ let existing: string | null = null;
88
+ try {
89
+ existing = existsSync(absOutput) ? readFileSync(absOutput, "utf-8") : null;
90
+ } catch {
91
+ existing = null;
92
+ }
93
+ if (existing !== css) {
94
+ writeFileSync(absOutput, css, "utf-8");
95
+ }
88
96
 
89
97
  return css;
90
98
  }
@@ -1085,7 +1085,20 @@ const BUILTIN_FILTERS: Record<string, FilterFn> = {
1085
1085
  last: (v) => Array.isArray(v) ? v[v.length - 1] ?? null : null,
1086
1086
  join: (v, sep) => Array.isArray(v) ? v.map(String).join(sep !== undefined ? String(sep) : ", ") : String(v),
1087
1087
  split: (v, sep) => String(v).split(sep !== undefined ? String(sep) : " "),
1088
- replace: (v, from, to) => from !== undefined && to !== undefined ? String(v).split(String(from)).join(String(to)) : String(v),
1088
+ replace: (v: unknown, from?: unknown, to?: unknown) => {
1089
+ const s = String(v);
1090
+ if (from !== undefined && typeof from === 'object' && from !== null && !Array.isArray(from)) {
1091
+ let result = s;
1092
+ for (const [old, newVal] of Object.entries(from as Record<string, unknown>)) {
1093
+ result = result.split(old).join(String(newVal));
1094
+ }
1095
+ return result;
1096
+ }
1097
+ if (from !== undefined && to !== undefined) {
1098
+ return s.split(String(from)).join(String(to));
1099
+ }
1100
+ return s;
1101
+ },
1089
1102
  default: (v, fallback) => (v !== null && v !== undefined && v !== "") ? v : (fallback !== undefined ? fallback : ""),
1090
1103
  raw: (v) => v,
1091
1104
  safe: (v) => v,
@@ -67,7 +67,7 @@ export class BaseModel {
67
67
  * When true, auto-generates fieldMapping entries from camelCase field names
68
68
  * to snake_case DB column names. Explicit fieldMapping entries always win.
69
69
  */
70
- static autoMap: boolean = false;
70
+ static autoMap: boolean = true;
71
71
 
72
72
  /**
73
73
  * Maps JS property names to database column names.
@@ -563,13 +563,15 @@ export class BaseModel {
563
563
  /**
564
564
  * Convert to plain object (dictionary).
565
565
  * @param include Optional array of relationship names to include (supports dot notation for nesting).
566
+ * @param case_ Key casing: 'camel' (default, keys as-is) or 'snake' (convert via fieldMapping).
566
567
  */
567
- toDict(include?: string[]): Record<string, unknown> {
568
+ toDict(include?: string[], case_: "camel" | "snake" = "camel"): Record<string, unknown> {
568
569
  const ModelClass = this.constructor as typeof BaseModel;
569
570
  const result: Record<string, unknown> = {};
570
571
  for (const key of Object.keys(ModelClass.fields)) {
571
572
  if (this[key] !== undefined) {
572
- result[key] = this[key];
573
+ const outKey = case_ === "snake" ? (ModelClass.fieldMapping[key] ?? key) : key;
574
+ result[outKey] = this[key];
573
575
  }
574
576
  }
575
577
  // Include soft delete field
@@ -604,11 +606,11 @@ export class BaseModel {
604
606
  result[relName] = null;
605
607
  } else if (Array.isArray(data)) {
606
608
  result[relName] = (data as BaseModel[]).map((r) =>
607
- r.toDict(nested.length > 0 ? nested : undefined),
609
+ r.toDict(nested.length > 0 ? nested : undefined, case_),
608
610
  );
609
611
  } else if (typeof (data as BaseModel).toDict === "function") {
610
612
  result[relName] = (data as BaseModel).toDict(
611
- nested.length > 0 ? nested : undefined,
613
+ nested.length > 0 ? nested : undefined, case_,
612
614
  );
613
615
  }
614
616
  }
@@ -639,15 +641,15 @@ export class BaseModel {
639
641
  /**
640
642
  * Convert to an associative object (alias for toDict).
641
643
  */
642
- toAssoc(include?: string[]): Record<string, unknown> {
643
- return this.toDict(include);
644
+ toAssoc(include?: string[], case_: "camel" | "snake" = "camel"): Record<string, unknown> {
645
+ return this.toDict(include, case_);
644
646
  }
645
647
 
646
648
  /**
647
649
  * Convert to a plain object (alias for toDict).
648
650
  */
649
- toObject(): Record<string, unknown> {
650
- return this.toDict();
651
+ toObject(case_: "camel" | "snake" = "camel"): Record<string, unknown> {
652
+ return this.toDict(undefined, case_);
651
653
  }
652
654
 
653
655
  /**
@@ -668,8 +670,8 @@ export class BaseModel {
668
670
  * Convert to JSON string.
669
671
  * @param include Optional relationship names to include.
670
672
  */
671
- toJson(include?: string[]): string {
672
- return JSON.stringify(this.toDict(include));
673
+ toJson(include?: string[], case_: "camel" | "snake" = "camel"): string {
674
+ return JSON.stringify(this.toDict(include, case_));
673
675
  }
674
676
 
675
677
  /**