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 +2 -2
- package/package.json +1 -1
- package/packages/core/src/wsdl.ts +24 -2
- package/packages/frond/src/engine.ts +29 -9
- package/packages/orm/src/migration.ts +78 -22
package/CLAUDE.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
# CLAUDE.md — AI Developer Guide for tina4-nodejs (v3.10.
|
|
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.
|
|
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.
|
|
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
|
|
382
|
-
|
|
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
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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()
|
|
130
|
+
const adapter = getAdapter();
|
|
131
131
|
if (!adapter.tableExists(MIGRATION_TABLE)) {
|
|
132
|
-
adapter
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
|
181
|
-
|
|
182
|
-
|
|
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
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
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
|
|
542
|
-
|
|
543
|
-
|
|
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(
|