tina4-nodejs 3.8.3 → 3.8.5

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
@@ -9,6 +9,14 @@
9
9
  Laravel joy. TypeScript speed. 10x less code. Zero third-party dependencies.
10
10
  </p>
11
11
 
12
+ <p align="center">
13
+ <a href="https://www.npmjs.com/package/tina4-nodejs"><img src="https://img.shields.io/npm/v/tina4-nodejs?color=7b1fa2&label=npm" alt="npm"></a>
14
+ <img src="https://img.shields.io/badge/tests-1%2C812%20passing-brightgreen" alt="Tests">
15
+ <img src="https://img.shields.io/badge/features-38-blue" alt="Features">
16
+ <img src="https://img.shields.io/badge/dependencies-0-brightgreen" alt="Zero Deps">
17
+ <a href="https://tina4.com"><img src="https://img.shields.io/badge/docs-tina4.com-7b1fa2" alt="Docs"></a>
18
+ </p>
19
+
12
20
  <p align="center">
13
21
  <a href="https://tina4.com">Documentation</a> &bull;
14
22
  <a href="#getting-started">Getting Started</a> &bull;
@@ -17,14 +25,6 @@
17
25
  <a href="https://tina4.com">tina4.com</a>
18
26
  </p>
19
27
 
20
- <p align="center">
21
- <img src="https://img.shields.io/badge/tests-1669%20passing-brightgreen" alt="Tests">
22
- <img src="https://img.shields.io/badge/carbonah-A%2B%20rated-00cc44" alt="Carbonah A+">
23
- <img src="https://img.shields.io/badge/zero--dep-core-blue" alt="Zero Dependencies">
24
- <img src="https://img.shields.io/badge/node-20%2B-blue" alt="Node 20+">
25
- <img src="https://img.shields.io/badge/license-MIT-lightgrey" alt="MIT License">
26
- </p>
27
-
28
28
  ---
29
29
 
30
30
  ## Quick Start
@@ -84,8 +84,8 @@ Every feature is built from scratch -- no npm install, no node_modules bloat, no
84
84
  | **Templates** | Frond engine (Twig-compatible), inheritance, partials, 53+ filters, macros, fragment caching, sandboxing |
85
85
  | **ORM** | Active Record, typed fields with validation, soft delete, relationships (`hasOne`/`hasMany`/`belongsTo`), scopes, result caching, auto-CRUD |
86
86
  | **Database** | SQLite, PostgreSQL, MySQL, MSSQL/SQL Server, Firebird -- unified adapter interface, query caching (TINA4_DB_CACHE=true for 4x speedup) |
87
- | **Auth** | Zero-dep JWT (HS256 + RS256), sessions (file backend), PBKDF2 password hashing, form tokens |
88
- | **API** | Swagger/OpenAPI auto-generation, GraphQL with schema builder and GraphiQL IDE |
87
+ | **Auth** | Zero-dep JWT (HS256/RS256), sessions (file/Redis/Valkey/MongoDB/database), password hashing, form tokens |
88
+ | **API** | Swagger/OpenAPI auto-generation, GraphQL with ORM auto-schema and GraphiQL IDE, WSDL/SOAP with auto WSDL |
89
89
  | **Background** | Queue (SQLite/RabbitMQ/Kafka/MongoDB) with priority, delayed jobs, retry, batch processing |
90
90
  | **Real-time** | Native WebSocket (RFC 6455), per-path routing, connection manager, broadcast |
91
91
  | **Frontend** | tina4-css (~24 KB), frond.js helper, SCSS compiler, live reload, CSS hot-reload |
@@ -93,7 +93,7 @@ Every feature is built from scratch -- no npm install, no node_modules bloat, no
93
93
  | **Data** | Migrations with rollback, 26+ fake data generators, ORM and table seeders |
94
94
  | **Other** | Service runner, localization (i18n), cache (memory/Redis/file), messenger (.env driven), HTTP constants, health check, configurable error pages |
95
95
 
96
- **1,669 tests across 38 built-in features. Zero dependencies. All Carbonah benchmarks rated A+.**
96
+ **1,812 tests across 38 built-in features. Zero dependencies. All Carbonah benchmarks rated A+.**
97
97
 
98
98
  For full documentation visit **[tina4.com](https://tina4.com)**.
99
99
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tina4-nodejs",
3
- "version": "3.8.3",
3
+ "version": "3.8.5",
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"],
@@ -49,9 +49,7 @@
49
49
  "engines": {
50
50
  "node": ">=20.0.0"
51
51
  },
52
- "dependencies": {
53
- "better-sqlite3": "^11.0.0"
54
- },
52
+ "dependencies": {},
55
53
  "devDependencies": {
56
54
  "typescript": "^5.7.0",
57
55
  "tsx": "^4.19.0",
@@ -170,7 +170,7 @@ test/ — Test files
170
170
  | Routing | router | \`import { get, post, put, del } from "@tina4/core"\` |
171
171
  | ORM | orm | \`import { BaseModel } from "@tina4/orm"\` |
172
172
  | Database | database | \`import { initDatabase } from "@tina4/orm"\` |
173
- | Templates | twig | \`import { renderTemplate } from "@tina4/twig"\` |
173
+ | Templates | twig | \`import { renderTemplate } from "@tina4/frond"\` |
174
174
  | JWT Auth | auth | \`import { createToken, validateToken } from "@tina4/core"\` |
175
175
  | REST API Client | api | \`import { Api } from "@tina4/core"\` |
176
176
  | GraphQL | graphql | \`import { GraphQL } from "@tina4/core"\` |
@@ -161,36 +161,19 @@ export function createResponse(res: ServerResponse): Tina4Response {
161
161
  return response;
162
162
  };
163
163
 
164
- response.render = async function (templateName: string, data?: Record<string, unknown>): Promise<Tina4Response> {
165
- try {
166
- const twig = await import("@tina4/twig");
167
- const html = await twig.renderTemplate(templateName, data);
168
- response.html(html);
169
- } catch (err) {
170
- res.statusCode = 500;
171
- response.json({
172
- error: "Template rendering failed",
173
- statusCode: 500,
174
- message: String(err),
175
- });
176
- }
164
+ // Default render/template stubs overwritten by server.ts when Frond is available
165
+ response.render = async function (templateName: string, _data?: Record<string, unknown>): Promise<Tina4Response> {
166
+ res.statusCode = 500;
167
+ response.json({
168
+ error: "Template engine not available",
169
+ statusCode: 500,
170
+ message: "Frond template engine is not initialized. Ensure @tina4/frond is installed.",
171
+ });
177
172
  return response;
178
173
  };
179
174
 
180
175
  response.template = async function (name: string, data?: Record<string, unknown>): Promise<Tina4Response> {
181
- try {
182
- const twig = await import("@tina4/twig");
183
- const html = await twig.renderTemplate(name, data);
184
- response.html(html);
185
- } catch (err) {
186
- res.statusCode = 500;
187
- response.json({
188
- error: "Template rendering failed",
189
- statusCode: 500,
190
- message: String(err),
191
- });
192
- }
193
- return response;
176
+ return response.render(name, data);
194
177
  };
195
178
 
196
179
  return response;
@@ -446,14 +446,13 @@ ${reset}
446
446
  const healthRoute = createHealthRoute(TINA4_VERSION);
447
447
  router.addRoute(healthRoute);
448
448
 
449
- // Initialize Twig if available
450
- let twigAvailable = false;
449
+ // Initialize Frond template engine
450
+ let frondEngine: any = null;
451
451
  try {
452
- const twig = await import("@tina4/twig");
453
- twig.setTemplatesDir(templatesDir);
454
- twigAvailable = true;
452
+ const { Frond } = await import("@tina4/frond");
453
+ frondEngine = new Frond(templatesDir);
455
454
  } catch {
456
- // Twig not installed, res.render() won't be available
455
+ // Frond not available
457
456
  }
458
457
 
459
458
  // Built-in middleware
@@ -569,12 +568,12 @@ ${reset}
569
568
  const req = createRequest(rawReq);
570
569
  const res = createResponse(rawRes);
571
570
 
572
- // Add res.render() if Twig is available
573
- if (twigAvailable) {
574
- try {
575
- const twig = await import("@tina4/twig");
576
- twig.addRenderMethod(res);
577
- } catch { /* ignore */ }
571
+ // Add res.render() if Frond is available
572
+ if (frondEngine) {
573
+ res.render = (template: string, data?: Record<string, unknown>, statusCode?: number) => {
574
+ const html = frondEngine.render(template, data);
575
+ return res.html(html, statusCode ?? 200);
576
+ };
578
577
  }
579
578
 
580
579
  try {
@@ -39,18 +39,9 @@ export class DatabaseSessionHandler implements SessionHandler {
39
39
  constructor(config?: DatabaseSessionConfig) {
40
40
  const dbPath = config?.dbPath ?? this.resolveDbPath();
41
41
 
42
- let Database: any;
43
- try {
44
- Database = _require("better-sqlite3");
45
- } catch {
46
- throw new Error(
47
- "DatabaseSessionHandler requires 'better-sqlite3'. " +
48
- "Install it with: npm install better-sqlite3"
49
- );
50
- }
51
-
52
- this.db = new Database(dbPath);
53
- this.db.pragma("journal_mode = WAL");
42
+ const { DatabaseSync } = require("node:sqlite");
43
+ this.db = new DatabaseSync(dbPath);
44
+ this.db.exec("PRAGMA journal_mode = WAL");
54
45
  }
55
46
 
56
47
  /**
@@ -848,6 +848,8 @@ export class Frond {
848
848
  /** Token pre-compilation cache for string templates */
849
849
  private compiledStrings = new Map<string, Token[]>();
850
850
 
851
+ getTemplateDir(): string { return this.templateDir; }
852
+
851
853
  constructor(templateDir: string = "src/templates") {
852
854
  this.templateDir = resolve(templateDir);
853
855
  this.filters = { ...BUILTIN_FILTERS };
@@ -1,19 +1,19 @@
1
- import Database from "better-sqlite3";
1
+ import { DatabaseSync } from "node:sqlite";
2
2
  import { mkdirSync } from "node:fs";
3
3
  import { dirname } from "node:path";
4
4
  import type { DatabaseAdapter, DatabaseResult, ColumnInfo, FieldDefinition } from "../types.js";
5
5
 
6
6
  export class SQLiteAdapter implements DatabaseAdapter {
7
- private db: Database.Database;
7
+ private db: DatabaseSync;
8
8
  private _lastInsertId: number | bigint | null = null;
9
9
 
10
10
  constructor(dbPath: string) {
11
- // Create directory if needed
12
- mkdirSync(dirname(dbPath), { recursive: true });
13
-
14
- this.db = new Database(dbPath);
15
- this.db.pragma("journal_mode = WAL");
16
- this.db.pragma("foreign_keys = ON");
11
+ if (dbPath !== ":memory:") {
12
+ mkdirSync(dirname(dbPath), { recursive: true });
13
+ }
14
+ this.db = new DatabaseSync(dbPath);
15
+ this.db.exec("PRAGMA journal_mode = WAL");
16
+ this.db.exec("PRAGMA foreign_keys = ON");
17
17
  }
18
18
 
19
19
  execute(sql: string, params?: unknown[]): unknown {
@@ -30,8 +30,9 @@ export class SQLiteAdapter implements DatabaseAdapter {
30
30
  let totalAffected = 0;
31
31
  let lastId: number | bigint | undefined;
32
32
 
33
- const runMany = this.db.transaction((rows: unknown[][]) => {
34
- for (const params of rows) {
33
+ this.db.exec("BEGIN TRANSACTION");
34
+ try {
35
+ for (const params of paramsList) {
35
36
  const result = stmt.run(...params);
36
37
  totalAffected += result.changes;
37
38
  if (result.lastInsertRowid) {
@@ -39,9 +40,12 @@ export class SQLiteAdapter implements DatabaseAdapter {
39
40
  this._lastInsertId = result.lastInsertRowid;
40
41
  }
41
42
  }
42
- });
43
+ this.db.exec("COMMIT");
44
+ } catch (e) {
45
+ this.db.exec("ROLLBACK");
46
+ throw e;
47
+ }
43
48
 
44
- runMany(paramsList);
45
49
  return { totalAffected, lastInsertId: lastId };
46
50
  }
47
51
 
@@ -68,7 +72,6 @@ export class SQLiteAdapter implements DatabaseAdapter {
68
72
  }
69
73
 
70
74
  insert(table: string, data: Record<string, unknown> | Record<string, unknown>[]): DatabaseResult {
71
- // Handle list of rows — batch insert
72
75
  if (Array.isArray(data)) {
73
76
  if (data.length === 0) return { success: true, rowsAffected: 0 };
74
77
  const keys = Object.keys(data[0]);
@@ -87,17 +90,9 @@ export class SQLiteAdapter implements DatabaseAdapter {
87
90
  try {
88
91
  const result = this.db.prepare(sql).run(...values);
89
92
  this._lastInsertId = result.lastInsertRowid;
90
- return {
91
- success: true,
92
- rowsAffected: result.changes,
93
- lastInsertId: result.lastInsertRowid,
94
- };
93
+ return { success: true, rowsAffected: result.changes, lastInsertId: result.lastInsertRowid };
95
94
  } catch (e) {
96
- return {
97
- success: false,
98
- rowsAffected: 0,
99
- error: (e as Error).message,
100
- };
95
+ return { success: false, rowsAffected: 0, error: (e as Error).message };
101
96
  }
102
97
  }
103
98
 
@@ -116,7 +111,6 @@ export class SQLiteAdapter implements DatabaseAdapter {
116
111
  }
117
112
 
118
113
  delete(table: string, filter: Record<string, unknown> | string | Record<string, unknown>[]): DatabaseResult {
119
- // Array of objects — delete each row
120
114
  if (Array.isArray(filter)) {
121
115
  let totalAffected = 0;
122
116
  for (const row of filter) {
@@ -126,7 +120,6 @@ export class SQLiteAdapter implements DatabaseAdapter {
126
120
  return { success: true, rowsAffected: totalAffected };
127
121
  }
128
122
 
129
- // String filter — raw WHERE clause
130
123
  if (typeof filter === "string") {
131
124
  const sql = filter ? `DELETE FROM "${table}" WHERE ${filter}` : `DELETE FROM "${table}"`;
132
125
  try {
@@ -137,7 +130,6 @@ export class SQLiteAdapter implements DatabaseAdapter {
137
130
  }
138
131
  }
139
132
 
140
- // Object filter — build WHERE from keys
141
133
  const whereClauses = Object.keys(filter).map((k) => `"${k}" = ?`).join(" AND ");
142
134
  const sql = `DELETE FROM "${table}" WHERE ${whereClauses}`;
143
135
  const values = Object.values(filter);
@@ -150,17 +142,9 @@ export class SQLiteAdapter implements DatabaseAdapter {
150
142
  }
151
143
  }
152
144
 
153
- startTransaction(): void {
154
- this.db.exec("BEGIN TRANSACTION");
155
- }
156
-
157
- commit(): void {
158
- this.db.exec("COMMIT");
159
- }
160
-
161
- rollback(): void {
162
- this.db.exec("ROLLBACK");
163
- }
145
+ startTransaction(): void { this.db.exec("BEGIN TRANSACTION"); }
146
+ commit(): void { this.db.exec("COMMIT"); }
147
+ rollback(): void { this.db.exec("ROLLBACK"); }
164
148
 
165
149
  tables(): string[] {
166
150
  const rows = this.query<{ name: string }>(
@@ -171,95 +155,57 @@ export class SQLiteAdapter implements DatabaseAdapter {
171
155
 
172
156
  columns(table: string): ColumnInfo[] {
173
157
  const rows = this.db.prepare(`PRAGMA table_info("${table}")`).all() as Array<{
174
- name: string;
175
- type: string;
176
- notnull: number;
177
- dflt_value: unknown;
178
- pk: number;
158
+ name: string; type: string; notnull: number; dflt_value: unknown; pk: number;
179
159
  }>;
180
160
  return rows.map((r) => ({
181
- name: r.name,
182
- type: r.type,
183
- nullable: r.notnull === 0,
184
- default: r.dflt_value,
185
- primaryKey: r.pk === 1,
161
+ name: r.name, type: r.type, nullable: r.notnull === 0, default: r.dflt_value, primaryKey: r.pk === 1,
186
162
  }));
187
163
  }
188
164
 
189
- lastInsertId(): number | bigint | null {
190
- return this._lastInsertId;
191
- }
192
-
193
- close(): void {
194
- this.db.close();
195
- }
165
+ lastInsertId(): number | bigint | null { return this._lastInsertId; }
166
+ close(): void { this.db.close(); }
196
167
 
197
168
  tableExists(name: string): boolean {
198
- const result = this.db
199
- .prepare("SELECT name FROM sqlite_master WHERE type='table' AND name=?")
200
- .get(name);
169
+ const result = this.db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name=?").get(name);
201
170
  return !!result;
202
171
  }
203
172
 
204
173
  createTable(name: string, columns: Record<string, FieldDefinition>): void {
205
174
  const colDefs: string[] = [];
206
-
207
175
  for (const [colName, def] of Object.entries(columns)) {
208
176
  const sqlType = fieldTypeToSQLite(def.type);
209
177
  const parts = [`"${colName}" ${sqlType}`];
210
-
211
178
  if (def.primaryKey) parts.push("PRIMARY KEY");
212
179
  if (def.autoIncrement) parts.push("AUTOINCREMENT");
213
180
  if (def.required && !def.primaryKey) parts.push("NOT NULL");
214
- if (def.default !== undefined && def.default !== "now") {
215
- parts.push(`DEFAULT ${sqlDefault(def.default)}`);
216
- }
217
- if (def.default === "now") {
218
- parts.push("DEFAULT CURRENT_TIMESTAMP");
219
- }
220
-
181
+ if (def.default !== undefined && def.default !== "now") parts.push(`DEFAULT ${sqlDefault(def.default)}`);
182
+ if (def.default === "now") parts.push("DEFAULT CURRENT_TIMESTAMP");
221
183
  colDefs.push(parts.join(" "));
222
184
  }
223
-
224
- const sql = `CREATE TABLE IF NOT EXISTS "${name}" (${colDefs.join(", ")})`;
225
- this.db.exec(sql);
185
+ this.db.exec(`CREATE TABLE IF NOT EXISTS "${name}" (${colDefs.join(", ")})`);
226
186
  }
227
187
 
228
188
  getTableColumns(name: string): Array<{ name: string; type: string }> {
229
- return this.db.prepare(`PRAGMA table_info("${name}")`).all() as Array<{
230
- name: string;
231
- type: string;
232
- }>;
189
+ return this.db.prepare(`PRAGMA table_info("${name}")`).all() as Array<{ name: string; type: string }>;
233
190
  }
234
191
 
235
192
  addColumn(table: string, colName: string, def: FieldDefinition): void {
236
193
  const sqlType = fieldTypeToSQLite(def.type);
237
194
  let sql = `ALTER TABLE "${table}" ADD COLUMN "${colName}" ${sqlType}`;
238
- if (def.default !== undefined && def.default !== "now") {
239
- sql += ` DEFAULT ${sqlDefault(def.default)}`;
240
- } else if (def.default === "now") {
241
- sql += " DEFAULT CURRENT_TIMESTAMP";
242
- }
195
+ if (def.default !== undefined && def.default !== "now") sql += ` DEFAULT ${sqlDefault(def.default)}`;
196
+ else if (def.default === "now") sql += " DEFAULT CURRENT_TIMESTAMP";
243
197
  this.db.exec(sql);
244
198
  }
245
199
  }
246
200
 
247
201
  function fieldTypeToSQLite(type: string): string {
248
202
  switch (type) {
249
- case "integer":
250
- return "INTEGER";
251
- case "number":
252
- case "numeric":
253
- return "REAL";
254
- case "boolean":
255
- return "INTEGER";
256
- case "datetime":
257
- return "TEXT";
258
- case "text":
259
- return "TEXT";
260
- case "string":
261
- default:
262
- return "TEXT";
203
+ case "integer": return "INTEGER";
204
+ case "number": case "numeric": return "REAL";
205
+ case "boolean": return "INTEGER";
206
+ case "datetime": return "TEXT";
207
+ case "text": return "TEXT";
208
+ case "string": default: return "TEXT";
263
209
  }
264
210
  }
265
211