tina4-nodejs 3.10.5 → 3.10.10

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.5)
1
+ # CLAUDE.md — AI Developer Guide for tina4-nodejs (v3.10.10)
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.5 — 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.10 — 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.5",
3
+ "version": "3.10.10",
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"],
@@ -161,6 +161,23 @@ export abstract class WSDLService {
161
161
 
162
162
  protected namespace: string = "http://tina4.com/wsdl";
163
163
 
164
+ /**
165
+ * Lifecycle hook: called before operation invocation.
166
+ * Override to validate, log, or modify the incoming request.
167
+ */
168
+ protected onRequest(_request: unknown): void {
169
+ // no-op — override in subclass
170
+ }
171
+
172
+ /**
173
+ * Lifecycle hook: called after operation returns.
174
+ * Override to transform, audit, or enrich the result.
175
+ * Must return the (possibly modified) result.
176
+ */
177
+ protected onResult(result: Record<string, unknown>): Record<string, unknown> {
178
+ return result;
179
+ }
180
+
164
181
  /** Discovered operations (populated on first use). */
165
182
  private _operations: Map<string, WSDLOperation> | null = null;
166
183
 
@@ -376,10 +393,15 @@ export abstract class WSDLService {
376
393
  }
377
394
  }
378
395
 
396
+ // Lifecycle hook: before invocation
397
+ this.onRequest(soapXml);
398
+
379
399
  // Invoke the method
380
400
  try {
381
- const result = await (method as (...args: unknown[]) => Promise<unknown>).call(this, ...params);
382
- return this.soapResponse(opName, result as Record<string, unknown>);
401
+ const rawResult = await (method as (...args: unknown[]) => Promise<unknown>).call(this, ...params);
402
+ // Lifecycle hook: after invocation — allow result transformation
403
+ const result = this.onResult(rawResult as Record<string, unknown>);
404
+ return this.soapResponse(opName, result);
383
405
  } catch (err) {
384
406
  const errMsg = err instanceof Error ? err.message : String(err);
385
407
  return this.soapFault("Server", errMsg);
@@ -341,19 +341,39 @@ function evalExpr(expr: string, context: Record<string, unknown>): unknown {
341
341
  }
342
342
  }
343
343
 
344
- // Function call: name("arg1", "arg2")
345
- const fnMatch = expr.match(/^(\w+)\s*\(([\s\S]*)?\)$/);
344
+ // Function call: name("arg1", "arg2") — supports dotted names like user.t("key")
345
+ const fnMatch = expr.match(/^([\w.]+)\s*\(([\s\S]*)?\)$/);
346
346
  if (fnMatch) {
347
347
  const fnName = fnMatch[1];
348
348
  const rawArgs = fnMatch[2] || "";
349
- const fn = context[fnName] ?? resolveVar(fnName, context);
350
- if (typeof fn === "function") {
351
- if (rawArgs.trim()) {
352
- const parts = splitArgs(rawArgs);
353
- const evalArgs = parts.map(a => evalExpr(a.trim(), context));
354
- return fn(...evalArgs);
349
+
350
+ // Dotted function name: resolve object, then call method
351
+ if (fnName.includes(".")) {
352
+ const lastDot = fnName.lastIndexOf(".");
353
+ const objPath = fnName.slice(0, lastDot);
354
+ const methodName = fnName.slice(lastDot + 1);
355
+ const obj = resolveVar(objPath, context);
356
+ if (obj && typeof obj === "object" && methodName in (obj as Record<string, unknown>)) {
357
+ const method = (obj as Record<string, unknown>)[methodName];
358
+ if (typeof method === "function") {
359
+ if (rawArgs.trim()) {
360
+ const parts = splitArgs(rawArgs);
361
+ const evalArgs = parts.map(a => evalExpr(a.trim(), context));
362
+ return method.apply(obj, evalArgs);
363
+ }
364
+ return method.call(obj);
365
+ }
366
+ }
367
+ } else {
368
+ const fn = context[fnName] ?? resolveVar(fnName, context);
369
+ if (typeof fn === "function") {
370
+ if (rawArgs.trim()) {
371
+ const parts = splitArgs(rawArgs);
372
+ const evalArgs = parts.map(a => evalExpr(a.trim(), context));
373
+ return fn(...evalArgs);
374
+ }
375
+ return fn();
355
376
  }
356
- return fn();
357
377
  }
358
378
  }
359
379
 
@@ -127,18 +127,34 @@ const MIGRATION_TABLE = "tina4_migration";
127
127
  * Ensure the migration tracking table exists with batch support.
128
128
  */
129
129
  export function ensureMigrationTable(): void {
130
- const adapter = getAdapter() as SQLiteAdapter;
130
+ const adapter = getAdapter();
131
131
  if (!adapter.tableExists(MIGRATION_TABLE)) {
132
- adapter.createTable(MIGRATION_TABLE, {
133
- id: { type: "integer", primaryKey: true, autoIncrement: true },
134
- name: { type: "string", required: true },
135
- batch: { type: "integer", required: true },
136
- applied_at: { type: "datetime", default: "now" },
137
- });
132
+ if (isFirebirdAdapter(adapter)) {
133
+ // Firebird: no AUTOINCREMENT, no TEXT type, use generator for IDs
134
+ try {
135
+ adapter.execute("CREATE GENERATOR GEN_TINA4_MIGRATION_ID");
136
+ try { adapter.execute("COMMIT"); } catch { /* ignore */ }
137
+ } catch {
138
+ // Generator may already exist
139
+ }
140
+ adapter.execute(`CREATE TABLE "${MIGRATION_TABLE}" (
141
+ id INTEGER NOT NULL PRIMARY KEY,
142
+ name VARCHAR(500) NOT NULL,
143
+ batch INTEGER NOT NULL DEFAULT 1,
144
+ applied_at VARCHAR(50) NOT NULL
145
+ )`);
146
+ } else {
147
+ (adapter as SQLiteAdapter).createTable(MIGRATION_TABLE, {
148
+ id: { type: "integer", primaryKey: true, autoIncrement: true },
149
+ name: { type: "string", required: true },
150
+ batch: { type: "integer", required: true },
151
+ applied_at: { type: "datetime", default: "now" },
152
+ });
153
+ }
138
154
  } else {
139
155
  // Ensure batch column exists on older tables that only had passed/description
140
156
  try {
141
- const cols = adapter.getTableColumns(MIGRATION_TABLE);
157
+ const cols = (adapter as SQLiteAdapter).getTableColumns(MIGRATION_TABLE);
142
158
  const colNames = new Set(cols.map((c) => c.name));
143
159
  if (!colNames.has("batch")) {
144
160
  adapter.execute(`ALTER TABLE "${MIGRATION_TABLE}" ADD COLUMN batch INTEGER NOT NULL DEFAULT 1`);
@@ -177,10 +193,22 @@ export function isMigrationApplied(name: string): boolean {
177
193
  */
178
194
  export function recordMigration(name: string, batch: number): void {
179
195
  const adapter = getAdapter();
180
- adapter.execute(
181
- `INSERT INTO "${MIGRATION_TABLE}" (name, batch) VALUES (?, ?)`,
182
- [name, batch],
183
- );
196
+ if (isFirebirdAdapter(adapter)) {
197
+ // Firebird: generate ID from sequence
198
+ const rows = adapter.query<{ NEXT_ID: number }>(
199
+ "SELECT GEN_ID(GEN_TINA4_MIGRATION_ID, 1) AS NEXT_ID FROM RDB$DATABASE",
200
+ );
201
+ const nextId = rows[0]?.NEXT_ID ?? 1;
202
+ adapter.execute(
203
+ `INSERT INTO "${MIGRATION_TABLE}" (id, name, batch) VALUES (?, ?, ?)`,
204
+ [nextId, name, batch],
205
+ );
206
+ } else {
207
+ adapter.execute(
208
+ `INSERT INTO "${MIGRATION_TABLE}" (name, batch) VALUES (?, ?)`,
209
+ [name, batch],
210
+ );
211
+ }
184
212
  }
185
213
 
186
214
  /**
@@ -438,12 +466,28 @@ export async function migrate(
438
466
 
439
467
  // Ensure tracking table with batch support
440
468
  if (!db.tableExists(MIGRATION_TABLE)) {
441
- db.execute(`CREATE TABLE IF NOT EXISTS "${MIGRATION_TABLE}" (
442
- id INTEGER PRIMARY KEY AUTOINCREMENT,
443
- name TEXT NOT NULL,
444
- batch INTEGER NOT NULL DEFAULT 1,
445
- applied_at TEXT NOT NULL
446
- )`);
469
+ if (isFirebirdAdapter(db)) {
470
+ // Firebird: no AUTOINCREMENT, no TEXT type, use generator for IDs
471
+ try {
472
+ db.execute("CREATE GENERATOR GEN_TINA4_MIGRATION_ID");
473
+ try { db.execute("COMMIT"); } catch { /* ignore */ }
474
+ } catch {
475
+ // Generator may already exist
476
+ }
477
+ db.execute(`CREATE TABLE "${MIGRATION_TABLE}" (
478
+ id INTEGER NOT NULL PRIMARY KEY,
479
+ name VARCHAR(500) NOT NULL,
480
+ batch INTEGER NOT NULL DEFAULT 1,
481
+ applied_at VARCHAR(50) NOT NULL
482
+ )`);
483
+ } else {
484
+ db.execute(`CREATE TABLE IF NOT EXISTS "${MIGRATION_TABLE}" (
485
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
486
+ name TEXT NOT NULL,
487
+ batch INTEGER NOT NULL DEFAULT 1,
488
+ applied_at TEXT NOT NULL
489
+ )`);
490
+ }
447
491
  } else {
448
492
  // Migrate old schema: if table has 'description' + 'passed' columns, migrate data
449
493
  try {
@@ -538,10 +582,22 @@ export async function migrate(
538
582
  // Record as applied with batch number
539
583
  const now = new Date().toISOString();
540
584
  try {
541
- db.execute(
542
- `INSERT INTO "${MIGRATION_TABLE}" (name, batch, applied_at) VALUES (?, ?, ?)`,
543
- [migrationId, currentBatch, now],
544
- );
585
+ if (isFirebirdAdapter(db)) {
586
+ // Firebird: generate ID from sequence
587
+ const idRows = db.query<{ NEXT_ID: number }>(
588
+ "SELECT GEN_ID(GEN_TINA4_MIGRATION_ID, 1) AS NEXT_ID FROM RDB$DATABASE",
589
+ );
590
+ const nextId = idRows[0]?.NEXT_ID ?? 1;
591
+ db.execute(
592
+ `INSERT INTO "${MIGRATION_TABLE}" (id, name, batch, applied_at) VALUES (?, ?, ?, ?)`,
593
+ [nextId, migrationId, currentBatch, now],
594
+ );
595
+ } else {
596
+ db.execute(
597
+ `INSERT INTO "${MIGRATION_TABLE}" (name, batch, applied_at) VALUES (?, ?, ?)`,
598
+ [migrationId, currentBatch, now],
599
+ );
600
+ }
545
601
  } catch {
546
602
  // Old schema fallback — try description/content/passed columns
547
603
  db.execute(