tina4-nodejs 3.10.13 → 3.10.15

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,10 +1,10 @@
1
- # CLAUDE.md — AI Developer Guide for tina4-nodejs (v3.10.13)
1
+ # CLAUDE.md — AI Developer Guide for tina4-nodejs (v3.10.15)
2
2
 
3
3
  > This file helps AI assistants (Claude, Copilot, Cursor, etc.) understand and work on this codebase effectively.
4
4
 
5
5
  ## What This Project Is
6
6
 
7
- Tina4 for Node.js/TypeScript v3.10.13 — a convention-over-configuration structural paradigm. **Not a framework.** The developer writes TypeScript; Tina4 is invisible infrastructure.
7
+ Tina4 for Node.js/TypeScript v3.10.15 — a convention-over-configuration structural paradigm. **Not a framework.** The developer writes TypeScript; Tina4 is invisible infrastructure.
8
8
 
9
9
  The philosophy: zero ceremony, batteries included, file system as source of truth.
10
10
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tina4-nodejs",
3
- "version": "3.10.13",
3
+ "version": "3.10.15",
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"],
@@ -666,14 +666,45 @@ function parseFilterChain(expr: string): [string, [string, string[]][]] {
666
666
  return [variable, filters];
667
667
  }
668
668
 
669
+ function processEscapes(s: string): string {
670
+ let out = "";
671
+ for (let i = 0; i < s.length; i++) {
672
+ if (s[i] === "\\" && i + 1 < s.length) {
673
+ const nxt = s[i + 1];
674
+ switch (nxt) {
675
+ case "n": out += "\n"; i++; break;
676
+ case "t": out += "\t"; i++; break;
677
+ case "\\": out += "\\"; i++; break;
678
+ case "'": out += "'"; i++; break;
679
+ case '"': out += '"'; i++; break;
680
+ default: out += "\\"; break;
681
+ }
682
+ } else {
683
+ out += s[i];
684
+ }
685
+ }
686
+ return out;
687
+ }
688
+
669
689
  function parseArgs(raw: string): string[] {
670
690
  const args: string[] = [];
671
691
  let current = "";
672
692
  let inQuote: string | null = null;
673
693
  let wasQuoted = false;
674
694
  let depth = 0;
695
+ let escaped = false;
675
696
 
676
697
  for (const ch of raw) {
698
+ if (escaped) {
699
+ current += ch;
700
+ escaped = false;
701
+ continue;
702
+ }
703
+ if (ch === "\\" && inQuote) {
704
+ current += ch;
705
+ escaped = true;
706
+ continue;
707
+ }
677
708
  if (inQuote) {
678
709
  if (ch === inQuote) {
679
710
  inQuote = null;
@@ -692,7 +723,7 @@ function parseArgs(raw: string): string[] {
692
723
  if (ch === "(") { depth++; current += ch; continue; }
693
724
  if (ch === ")") { depth--; current += ch; continue; }
694
725
  if (ch === "," && depth === 0) {
695
- args.push(wasQuoted ? current : current.trim());
726
+ args.push(wasQuoted ? processEscapes(current) : current.trim());
696
727
  current = "";
697
728
  wasQuoted = false;
698
729
  continue;
@@ -700,7 +731,7 @@ function parseArgs(raw: string): string[] {
700
731
  current += ch;
701
732
  }
702
733
 
703
- const final = wasQuoted ? current : current.trim();
734
+ const final = wasQuoted ? processEscapes(current) : current.trim();
704
735
  if (final !== "" || wasQuoted) {
705
736
  args.push(final);
706
737
  }
@@ -936,6 +967,9 @@ const BUILTIN_FILTERS: Record<string, FilterFn> = {
936
967
  dump: (v) => JSON.stringify(v),
937
968
  formToken: (v?: unknown) => _generateFormToken(v != null ? String(v) : ""),
938
969
  form_token: (v?: unknown) => _generateFormToken(v != null ? String(v) : ""),
970
+ to_json: (v) => JSON.stringify(v).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026"),
971
+ tojson: (v) => JSON.stringify(v).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026"),
972
+ js_escape: (v) => String(v).replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r"),
939
973
  };
940
974
 
941
975
  // ── Form Token ────────────────────────────────────────────────
@@ -208,6 +208,9 @@ export class Database {
208
208
  /** Whether to automatically commit after each write operation */
209
209
  private autoCommit: boolean = process.env.TINA4_AUTOCOMMIT === "true";
210
210
 
211
+ /** Database engine type (sqlite, postgres, mysql, mssql, firebird) */
212
+ private dbType: string = "sqlite";
213
+
211
214
  /**
212
215
  * Create a Database wrapping an existing adapter.
213
216
  * For creating a Database from a URL, use the async static factories:
@@ -227,6 +230,8 @@ export class Database {
227
230
  * @param pool - Number of pooled connections (0 = single, N>0 = round-robin)
228
231
  */
229
232
  static async create(url: string, username?: string, password?: string, pool: number = 0): Promise<Database> {
233
+ const parsed = parseDatabaseUrl(url, username, password);
234
+
230
235
  if (pool > 0) {
231
236
  // Pooled mode — create all adapters eagerly
232
237
  const adapters: DatabaseAdapter[] = [];
@@ -243,13 +248,16 @@ export class Database {
243
248
  db.poolIndex = 0;
244
249
  db.adapter = null; // Don't use single-adapter path
245
250
  db.adapterFactory = () => createAdapterFromUrl(url, username, password);
251
+ db.dbType = parsed.type;
246
252
  return db;
247
253
  }
248
254
 
249
255
  // Single-connection mode — current behavior
250
256
  const adapter = await createAdapterFromUrl(url, username, password);
251
257
  setAdapter(adapter);
252
- return new Database(adapter);
258
+ const db = new Database(adapter);
259
+ db.dbType = parsed.type;
260
+ return db;
253
261
  }
254
262
 
255
263
  /**
@@ -391,6 +399,55 @@ export class Database {
391
399
  if (id === null) return 0;
392
400
  return typeof id === "bigint" ? id.toString() : id;
393
401
  }
402
+
403
+ /**
404
+ * Pre-generate the next available primary key ID using engine-aware strategies.
405
+ *
406
+ * - Firebird: auto-creates a generator if missing, then increments via GEN_ID.
407
+ * - PostgreSQL: tries nextval() on the standard sequence, falls through to MAX+1.
408
+ * - SQLite/MySQL/MSSQL: uses MAX(pk) + 1.
409
+ * - Returns 1 if the table is empty or does not exist.
410
+ */
411
+ getNextId(table: string, pkColumn = "id", generatorName?: string): number {
412
+ const adapter = this.getNextAdapter();
413
+
414
+ // Firebird — use generators
415
+ if (this.dbType === "firebird") {
416
+ const genName = generatorName ?? `GEN_${table.toUpperCase()}_ID`;
417
+
418
+ // Auto-create the generator if it does not exist
419
+ try {
420
+ adapter.execute(`CREATE GENERATOR ${genName}`);
421
+ } catch {
422
+ // Generator already exists — ignore
423
+ }
424
+
425
+ const row = adapter.fetchOne<Record<string, unknown>>(`SELECT GEN_ID(${genName}, 1) AS NEXT_ID FROM RDB$DATABASE`);
426
+ return Number(row?.NEXT_ID ?? row?.next_id ?? 1);
427
+ }
428
+
429
+ // PostgreSQL — try sequence first, fall through to MAX
430
+ if (this.dbType === "postgres") {
431
+ const seqName = `${table.toLowerCase()}_${pkColumn.toLowerCase()}_seq`;
432
+ try {
433
+ const row = adapter.fetchOne<Record<string, unknown>>(`SELECT nextval('${seqName}') AS next_id`);
434
+ if (row?.next_id != null) {
435
+ return Number(row.next_id);
436
+ }
437
+ } catch {
438
+ // No sequence — fall through to MAX
439
+ }
440
+ }
441
+
442
+ // SQLite / MySQL / MSSQL / PostgreSQL fallback — MAX + 1
443
+ try {
444
+ const row = adapter.fetchOne<Record<string, unknown>>(`SELECT MAX(${pkColumn}) + 1 AS next_id FROM ${table}`);
445
+ const nextId = row?.next_id;
446
+ return nextId != null ? Number(nextId) : 1;
447
+ } catch {
448
+ return 1;
449
+ }
450
+ }
394
451
  }
395
452
 
396
453
  /**