tina4-nodejs 3.10.24 → 3.10.31

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/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  </p>
4
4
 
5
5
  <h1 align="center">Tina4 Node.js</h1>
6
- <h3 align="center">This is not a framework</h3>
6
+ <h3 align="center">This Is Now A 4Framework</h3>
7
7
 
8
8
  <p align="center">
9
9
  Laravel joy. TypeScript speed. 10x less code. Zero third-party dependencies.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tina4-nodejs",
3
- "version": "3.10.24",
3
+ "version": "3.10.31",
4
4
  "type": "module",
5
5
  "description": "This is not a framework. Tina4 for Node.js/TypeScript — zero deps, 38 built-in features.",
6
6
  "keywords": ["tina4", "framework", "web", "api", "orm", "graphql", "websocket", "typescript"],
@@ -20,7 +20,7 @@ import { isTruthy } from "./dotenv.js";
20
20
 
21
21
  const cpuCount = osCpus().length;
22
22
 
23
- const TINA4_VERSION = "3.0.0";
23
+ const TINA4_VERSION = "3.10.30";
24
24
 
25
25
  // ---------------------------------------------------------------------------
26
26
  // Types
@@ -29,7 +29,7 @@ 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.0.0";
32
+ const TINA4_VERSION = "3.10.30";
33
33
 
34
34
  /** Cache Frond instances by template directory to avoid repeated instantiation. */
35
35
  const frondCache = new Map<string, InstanceType<any>>();
@@ -419,6 +419,40 @@ function evalExpr(expr: string, context: Record<string, unknown>): unknown {
419
419
  }
420
420
  }
421
421
 
422
+ // Arithmetic operators: +, -, *, //, /, %, ** (lowest to highest precedence)
423
+ for (const op of [" + ", " - ", " * ", " // ", " / ", " % ", " ** "]) {
424
+ const pos = findOutsideQuotes(expr, op);
425
+ if (pos >= 0) {
426
+ const left = expr.slice(0, pos).trim();
427
+ const right = expr.slice(pos + op.length).trim();
428
+ const lVal = evalExpr(left, context);
429
+ const rVal = evalExpr(right, context);
430
+ try {
431
+ let lNum = lVal != null ? Number(lVal) : 0;
432
+ let rNum = rVal != null ? Number(rVal) : 0;
433
+ if (isNaN(lNum)) lNum = 0;
434
+ if (isNaN(rNum)) rNum = 0;
435
+ const opS = op.trim();
436
+ // Preserve int type when both operands are int-like (except for / which returns float)
437
+ const bothInt = Number.isInteger(lNum) && Number.isInteger(rNum) && opS !== "/";
438
+ let result: number;
439
+ switch (opS) {
440
+ case "+": result = lNum + rNum; break;
441
+ case "-": result = lNum - rNum; break;
442
+ case "*": result = lNum * rNum; break;
443
+ case "//": result = rNum !== 0 ? Math.floor(lNum / rNum) : 0; break;
444
+ case "/": result = rNum !== 0 ? lNum / rNum : 0; break;
445
+ case "%": result = rNum !== 0 ? lNum % rNum : 0; break;
446
+ case "**": result = lNum ** rNum; break;
447
+ default: result = 0;
448
+ }
449
+ return bothInt && Number.isInteger(result) ? result : result;
450
+ } catch {
451
+ return null;
452
+ }
453
+ }
454
+ }
455
+
422
456
  // Function call: name("arg1", "arg2") — supports dotted names like user.t("key")
423
457
  const fnMatch = expr.match(FN_CALL_RE);
424
458
  if (fnMatch) {
@@ -1782,7 +1816,7 @@ export class Frond {
1782
1816
  if (m) {
1783
1817
  const name = m[1];
1784
1818
  const expr = m[2].trim();
1785
- context[name] = evalExpr(expr, context);
1819
+ context[name] = this.evalVarRaw(expr, context);
1786
1820
  }
1787
1821
  }
1788
1822
 
@@ -236,37 +236,43 @@ export class BaseModel {
236
236
  const pkValue = this[pk];
237
237
  this._relCache = {}; // Clear relationship cache on save
238
238
 
239
- if (pkValue !== undefined && pkValue !== null) {
240
- // Update
241
- const updateFields = Object.entries(ModelClass.fields).filter(
242
- ([name, def]) => !def.primaryKey && this[name] !== undefined,
243
- );
244
- if (updateFields.length === 0) return;
245
-
246
- const setClause = updateFields.map(([k]) => `"${ModelClass.getDbColumn(k)}" = ?`).join(", ");
247
- const values = [...updateFields.map(([k]) => this[k]), pkValue];
248
-
249
- db.execute(`UPDATE "${ModelClass.tableName}" SET ${setClause} WHERE "${pkCol}" = ?`, values);
250
- } else {
251
- // Insert
252
- const insertFields = Object.entries(ModelClass.fields).filter(
253
- ([name, def]) => !(def.primaryKey && def.autoIncrement) && this[name] !== undefined,
254
- );
255
-
256
- const columns = insertFields.map(([k]) => `"${ModelClass.getDbColumn(k)}"`).join(", ");
257
- const placeholders = insertFields.map(() => "?").join(", ");
258
- const values = insertFields.map(([k]) => this[k]);
259
-
260
- const result = db.execute(
261
- `INSERT INTO "${ModelClass.tableName}" (${columns}) VALUES (${placeholders})`,
262
- values,
263
- ) as { lastInsertRowid?: number };
264
-
265
- if (result.lastInsertRowid) {
266
- this[pk] = result.lastInsertRowid;
239
+ db.startTransaction();
240
+ try {
241
+ if (pkValue !== undefined && pkValue !== null) {
242
+ // Update
243
+ const updateFields = Object.entries(ModelClass.fields).filter(
244
+ ([name, def]) => !def.primaryKey && this[name] !== undefined,
245
+ );
246
+ if (updateFields.length === 0) { db.commit(); return; }
247
+
248
+ const setClause = updateFields.map(([k]) => `"${ModelClass.getDbColumn(k)}" = ?`).join(", ");
249
+ const values = [...updateFields.map(([k]) => this[k]), pkValue];
250
+
251
+ db.execute(`UPDATE "${ModelClass.tableName}" SET ${setClause} WHERE "${pkCol}" = ?`, values);
252
+ } else {
253
+ // Insert
254
+ const insertFields = Object.entries(ModelClass.fields).filter(
255
+ ([name, def]) => !(def.primaryKey && def.autoIncrement) && this[name] !== undefined,
256
+ );
257
+
258
+ const columns = insertFields.map(([k]) => `"${ModelClass.getDbColumn(k)}"`).join(", ");
259
+ const placeholders = insertFields.map(() => "?").join(", ");
260
+ const values = insertFields.map(([k]) => this[k]);
261
+
262
+ const result = db.execute(
263
+ `INSERT INTO "${ModelClass.tableName}" (${columns}) VALUES (${placeholders})`,
264
+ values,
265
+ ) as { lastInsertRowid?: number };
266
+
267
+ if (result.lastInsertRowid) {
268
+ this[pk] = result.lastInsertRowid;
269
+ }
267
270
  }
271
+ db.commit();
272
+ } catch (e) {
273
+ db.rollback();
274
+ throw e;
268
275
  }
269
- db.commit();
270
276
  }
271
277
 
272
278
  /**
@@ -283,19 +289,25 @@ export class BaseModel {
283
289
  throw new Error("Cannot delete a model without a primary key value");
284
290
  }
285
291
 
286
- if (ModelClass.softDelete) {
287
- db.execute(
288
- `UPDATE "${ModelClass.tableName}" SET is_deleted = 1 WHERE "${pkCol}" = ?`,
289
- [pkValue],
290
- );
291
- this.is_deleted = 1;
292
- } else {
293
- db.execute(
294
- `DELETE FROM "${ModelClass.tableName}" WHERE "${pkCol}" = ?`,
295
- [pkValue],
296
- );
292
+ db.startTransaction();
293
+ try {
294
+ if (ModelClass.softDelete) {
295
+ db.execute(
296
+ `UPDATE "${ModelClass.tableName}" SET is_deleted = 1 WHERE "${pkCol}" = ?`,
297
+ [pkValue],
298
+ );
299
+ this.is_deleted = 1;
300
+ } else {
301
+ db.execute(
302
+ `DELETE FROM "${ModelClass.tableName}" WHERE "${pkCol}" = ?`,
303
+ [pkValue],
304
+ );
305
+ }
306
+ db.commit();
307
+ } catch (e) {
308
+ db.rollback();
309
+ throw e;
297
310
  }
298
- db.commit();
299
311
  }
300
312
 
301
313
  /**
@@ -459,9 +471,15 @@ export class BaseModel {
459
471
  }
460
472
 
461
473
  const sql = `CREATE TABLE IF NOT EXISTS "${this.tableName}" (${colDefs.join(", ")})`;
462
- db.execute(sql);
474
+ db.startTransaction();
475
+ try {
476
+ db.execute(sql);
477
+ db.commit();
478
+ } catch (e) {
479
+ db.rollback();
480
+ throw e;
481
+ }
463
482
  }
464
- db.commit();
465
483
  }
466
484
 
467
485
  /**
@@ -504,11 +522,17 @@ export class BaseModel {
504
522
  throw new Error("Cannot delete a model without a primary key value");
505
523
  }
506
524
 
507
- db.execute(
508
- `DELETE FROM "${ModelClass.tableName}" WHERE "${pkCol}" = ?`,
509
- [pkValue],
510
- );
511
- db.commit();
525
+ db.startTransaction();
526
+ try {
527
+ db.execute(
528
+ `DELETE FROM "${ModelClass.tableName}" WHERE "${pkCol}" = ?`,
529
+ [pkValue],
530
+ );
531
+ db.commit();
532
+ } catch (e) {
533
+ db.rollback();
534
+ throw e;
535
+ }
512
536
  }
513
537
 
514
538
  /**
@@ -529,11 +553,17 @@ export class BaseModel {
529
553
  throw new Error("Cannot restore a model without a primary key value");
530
554
  }
531
555
 
532
- db.execute(
533
- `UPDATE "${ModelClass.tableName}" SET is_deleted = 0 WHERE "${pkCol}" = ?`,
534
- [pkValue],
535
- );
536
- db.commit();
556
+ db.startTransaction();
557
+ try {
558
+ db.execute(
559
+ `UPDATE "${ModelClass.tableName}" SET is_deleted = 0 WHERE "${pkCol}" = ?`,
560
+ [pkValue],
561
+ );
562
+ db.commit();
563
+ } catch (e) {
564
+ db.rollback();
565
+ throw e;
566
+ }
537
567
  this.is_deleted = 0;
538
568
  }
539
569