tina4-nodejs 3.10.4 → 3.10.6
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 +100 -13
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.6)
|
|
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.6 — 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.6",
|
|
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);
|
|
@@ -219,9 +219,76 @@ function resolveVar(expr: string, context: Record<string, unknown>): unknown {
|
|
|
219
219
|
return value;
|
|
220
220
|
}
|
|
221
221
|
|
|
222
|
+
function findOutsideQuotes(expr: string, needle: string): number {
|
|
223
|
+
let inQuote: string | null = null;
|
|
224
|
+
let depth = 0;
|
|
225
|
+
let i = 0;
|
|
226
|
+
while (i <= expr.length - needle.length) {
|
|
227
|
+
const ch = expr[i];
|
|
228
|
+
if ((ch === '"' || ch === "'") && depth === 0) {
|
|
229
|
+
if (inQuote === null) {
|
|
230
|
+
inQuote = ch;
|
|
231
|
+
} else if (ch === inQuote) {
|
|
232
|
+
inQuote = null;
|
|
233
|
+
}
|
|
234
|
+
i++;
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
if (inQuote) { i++; continue; }
|
|
238
|
+
if (ch === "(") depth++;
|
|
239
|
+
else if (ch === ")") depth--;
|
|
240
|
+
if (depth === 0 && expr.slice(i, i + needle.length) === needle) {
|
|
241
|
+
return i;
|
|
242
|
+
}
|
|
243
|
+
i++;
|
|
244
|
+
}
|
|
245
|
+
return -1;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function splitOutsideQuotes(expr: string, sep: string): string[] {
|
|
249
|
+
const parts: string[] = [];
|
|
250
|
+
let currentStart = 0;
|
|
251
|
+
let inQuote: string | null = null;
|
|
252
|
+
let depth = 0;
|
|
253
|
+
let i = 0;
|
|
254
|
+
while (i <= expr.length - sep.length) {
|
|
255
|
+
const ch = expr[i];
|
|
256
|
+
if ((ch === '"' || ch === "'") && depth === 0) {
|
|
257
|
+
if (inQuote === null) {
|
|
258
|
+
inQuote = ch;
|
|
259
|
+
} else if (ch === inQuote) {
|
|
260
|
+
inQuote = null;
|
|
261
|
+
}
|
|
262
|
+
i++;
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
if (inQuote) { i++; continue; }
|
|
266
|
+
if (ch === "(") depth++;
|
|
267
|
+
else if (ch === ")") depth--;
|
|
268
|
+
if (depth === 0 && expr.slice(i, i + sep.length) === sep) {
|
|
269
|
+
parts.push(expr.slice(currentStart, i));
|
|
270
|
+
i += sep.length;
|
|
271
|
+
currentStart = i;
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
i++;
|
|
275
|
+
}
|
|
276
|
+
parts.push(expr.slice(currentStart));
|
|
277
|
+
return parts;
|
|
278
|
+
}
|
|
279
|
+
|
|
222
280
|
function evalExpr(expr: string, context: Record<string, unknown>): unknown {
|
|
223
281
|
expr = expr.trim();
|
|
224
282
|
|
|
283
|
+
// String literal early-return: if the entire expression is a single quoted
|
|
284
|
+
// string with no unescaped matching quotes inside, return its content.
|
|
285
|
+
if (expr.length >= 2) {
|
|
286
|
+
const q = expr[0];
|
|
287
|
+
if ((q === '"' || q === "'") && expr.endsWith(q) && !expr.slice(1, -1).includes(q)) {
|
|
288
|
+
return expr.slice(1, -1);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
225
292
|
// Ternary: condition ? true_val : false_val
|
|
226
293
|
// Match carefully to handle nested ternaries
|
|
227
294
|
const ternaryIdx = findTernary(expr);
|
|
@@ -245,7 +312,7 @@ function evalExpr(expr: string, context: Record<string, unknown>): unknown {
|
|
|
245
312
|
}
|
|
246
313
|
|
|
247
314
|
// Null coalescing: value ?? "default"
|
|
248
|
-
const qqIdx = expr
|
|
315
|
+
const qqIdx = findOutsideQuotes(expr, "??");
|
|
249
316
|
if (qqIdx !== -1) {
|
|
250
317
|
const left = expr.slice(0, qqIdx).trim();
|
|
251
318
|
const right = expr.slice(qqIdx + 2).trim();
|
|
@@ -257,8 +324,8 @@ function evalExpr(expr: string, context: Record<string, unknown>): unknown {
|
|
|
257
324
|
}
|
|
258
325
|
|
|
259
326
|
// String concatenation with ~
|
|
260
|
-
if (expr
|
|
261
|
-
const parts =
|
|
327
|
+
if (findOutsideQuotes(expr, "~") >= 0) {
|
|
328
|
+
const parts = splitOutsideQuotes(expr, "~");
|
|
262
329
|
if (parts.length > 1) {
|
|
263
330
|
return parts.map(p => {
|
|
264
331
|
const v = evalExpr(p.trim(), context);
|
|
@@ -269,24 +336,44 @@ function evalExpr(expr: string, context: Record<string, unknown>): unknown {
|
|
|
269
336
|
|
|
270
337
|
// Check for comparison/logical operators
|
|
271
338
|
for (const op of [" not in ", " in ", " is not ", " is ", "!=", "==", ">=", "<=", ">", "<", " and ", " or ", " not "]) {
|
|
272
|
-
if (expr
|
|
339
|
+
if (findOutsideQuotes(expr, op) >= 0) {
|
|
273
340
|
return evalComparison(expr, context);
|
|
274
341
|
}
|
|
275
342
|
}
|
|
276
343
|
|
|
277
|
-
// Function call: name("arg1", "arg2")
|
|
278
|
-
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]*)?\)$/);
|
|
279
346
|
if (fnMatch) {
|
|
280
347
|
const fnName = fnMatch[1];
|
|
281
348
|
const rawArgs = fnMatch[2] || "";
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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();
|
|
288
376
|
}
|
|
289
|
-
return fn();
|
|
290
377
|
}
|
|
291
378
|
}
|
|
292
379
|
|