tina4-nodejs 3.13.38 → 3.13.40
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 +54 -5
- package/README.md +6 -6
- package/package.json +1 -1
- package/packages/core/src/api.ts +64 -1
- package/packages/core/src/auth.ts +4 -1
- package/packages/core/src/devAdmin.ts +91 -21
- package/packages/core/src/index.ts +9 -4
- package/packages/core/src/logger.ts +84 -27
- package/packages/core/src/mcp.ts +105 -12
- package/packages/core/src/metrics.ts +330 -70
- package/packages/core/src/middleware.ts +1 -1
- package/packages/core/src/queueBackends/kafkaBackend.ts +97 -0
- package/packages/core/src/router.ts +54 -6
- package/packages/core/src/server.ts +120 -22
- package/packages/core/src/sessionHandlers/mongoHandler.ts +2 -0
- package/packages/core/src/types.ts +21 -2
- package/packages/core/src/websocket.ts +419 -9
- package/packages/core/src/websocketConnection.ts +6 -0
- package/packages/orm/src/baseModel.ts +167 -22
- package/packages/orm/src/docstore.ts +819 -0
- package/packages/orm/src/index.ts +14 -0
- package/packages/orm/src/migration.ts +149 -22
- package/packages/orm/src/queryBuilder.ts +14 -2
- package/packages/orm/src/types.ts +7 -0
- package/packages/orm/src/validation.ts +14 -0
- package/packages/swagger/src/generator.ts +119 -16
- package/packages/swagger/src/ui.ts +10 -2
|
@@ -67,8 +67,9 @@ function escapeRegExp(s: string): string {
|
|
|
67
67
|
/**
|
|
68
68
|
* Top-level classes DEFINED in a source file. A test that references one of
|
|
69
69
|
* these genuinely exercises this file. Classes only (distinctive PascalCase,
|
|
70
|
-
* length >
|
|
71
|
-
*
|
|
70
|
+
* length > 2 — so short-but-real names like `ORM`/`Api`/`Log`/`Env` count) —
|
|
71
|
+
* module-level function names like `get`/`run`/`init` are too generic to trust
|
|
72
|
+
* as a coverage signal.
|
|
72
73
|
*/
|
|
73
74
|
function definedClasses(source: string): Set<string> {
|
|
74
75
|
const names = new Set<string>();
|
|
@@ -76,7 +77,10 @@ function definedClasses(source: string): Set<string> {
|
|
|
76
77
|
let m: RegExpExecArray | null;
|
|
77
78
|
while ((m = re.exec(source)) !== null) {
|
|
78
79
|
const name = m[1];
|
|
79
|
-
|
|
80
|
+
// length > 2 (NOT > 3): a 3-char class like ORM/Api/Log/Env is a genuine,
|
|
81
|
+
// distinctive coverage signal — the old > 3 gate silently dropped them so a
|
|
82
|
+
// test that references `new Api()` / `Log.error()` left the file "untested".
|
|
83
|
+
if (name && !name.startsWith("_") && name.length > 2) {
|
|
80
84
|
names.add(name);
|
|
81
85
|
}
|
|
82
86
|
}
|
|
@@ -98,8 +102,9 @@ function definedClasses(source: string): Set<string> {
|
|
|
98
102
|
* 2. Import — a test that actually IMPORTS this module by its path
|
|
99
103
|
* (`import … from ".../<m>.js"`, `require(".../<m>")`).
|
|
100
104
|
* 3. Class reference — a test that references a top-level class DEFINED in
|
|
101
|
-
* this file (distinctive PascalCase, length >
|
|
102
|
-
*
|
|
105
|
+
* this file (distinctive PascalCase, length > 2 — short-but-real names
|
|
106
|
+
* like ORM/Api/Log/Env count). NO bare module-name word match and NO
|
|
107
|
+
* guessed CamelCase-from-snake_case match.
|
|
103
108
|
*
|
|
104
109
|
* Returns true only on a real signal, so the "untested" offenders surfaced by
|
|
105
110
|
* `tina4 metrics` and the dashboard "T" badge are trustworthy.
|
|
@@ -169,6 +174,11 @@ function hasMatchingTest(relPath: string): boolean {
|
|
|
169
174
|
new RegExp(`import\\b[^;\\n]*?from\\s*${spec}`),
|
|
170
175
|
// require("<spec>")
|
|
171
176
|
new RegExp(`require\\s*\\(\\s*${spec}\\s*\\)`),
|
|
177
|
+
// dynamic / inline-type import("<spec>") — covers `await import("…/m.js")`
|
|
178
|
+
// AND the TS inline type position `import("…/m.ts").SomeType`. The full
|
|
179
|
+
// package path a test uses ("../packages/core/src/<m>.ts") matches as a
|
|
180
|
+
// SUFFIX via the leading `(?:[^"']*\/)?` in <spec>.
|
|
181
|
+
new RegExp(`import\\s*\\(\\s*${spec}\\s*\\)`),
|
|
172
182
|
// side-effect import "<spec>"
|
|
173
183
|
new RegExp(`import\\s*${spec}`),
|
|
174
184
|
];
|
|
@@ -248,6 +258,214 @@ function countLines(source: string): LineCounts {
|
|
|
248
258
|
return { loc, blank, comment };
|
|
249
259
|
}
|
|
250
260
|
|
|
261
|
+
// ── Literal / comment stripping ──────────────────────────────
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Replace the CONTENTS of string literals, template literals (including
|
|
265
|
+
* interpolations), regex literals, and both comment styles with neutral
|
|
266
|
+
* placeholder characters (spaces), preserving newlines so line numbers and
|
|
267
|
+
* structure stay intact. The surrounding delimiters are kept.
|
|
268
|
+
*
|
|
269
|
+
* This is the regex-based stand-in for Python's AST: the decision-point
|
|
270
|
+
* patterns and the function-extraction patterns must only ever see real code,
|
|
271
|
+
* never text that happens to live inside a string, template, regex, line
|
|
272
|
+
* comment or block comment. Without this, a string full of boolean operators
|
|
273
|
+
* or a regex inflates complexity and yields bogus "functions".
|
|
274
|
+
*
|
|
275
|
+
* Regex-vs-division is resolved conservatively: a slash only starts a regex
|
|
276
|
+
* when the previous significant token can't end an expression (an operator,
|
|
277
|
+
* keyword, open bracket, comma, semicolon, etc.). When in doubt we treat the
|
|
278
|
+
* slash as division and DON'T strip — favouring "leave code intact" over
|
|
279
|
+
* "wrongly blank out a division", per the brief.
|
|
280
|
+
*/
|
|
281
|
+
function stripLiterals(source: string): string {
|
|
282
|
+
const out: string[] = [];
|
|
283
|
+
const n = source.length;
|
|
284
|
+
let i = 0;
|
|
285
|
+
|
|
286
|
+
// The last non-whitespace, non-comment character we EMITTED as real code —
|
|
287
|
+
// used to decide whether a `/` opens a regex or is a division operator.
|
|
288
|
+
let prevSignificant = "";
|
|
289
|
+
// The last "word" token (identifier/keyword) emitted, for keyword checks.
|
|
290
|
+
let prevWord = "";
|
|
291
|
+
|
|
292
|
+
/** Keywords after which a `/` is a regex, not division. */
|
|
293
|
+
const regexKeywords = new Set([
|
|
294
|
+
"return", "typeof", "instanceof", "in", "of", "new", "delete", "void",
|
|
295
|
+
"throw", "case", "do", "else", "yield", "await",
|
|
296
|
+
]);
|
|
297
|
+
|
|
298
|
+
/** Can the previous significant token end an expression? If so, `/` = division. */
|
|
299
|
+
function prevEndsExpression(): boolean {
|
|
300
|
+
if (prevSignificant === "") return false; // start of input → regex
|
|
301
|
+
// Identifier/number ending char → could be a value → division …
|
|
302
|
+
if (/[A-Za-z0-9_$]/.test(prevSignificant)) {
|
|
303
|
+
// …unless it's a keyword like `return`/`case` that precedes a regex.
|
|
304
|
+
return !regexKeywords.has(prevWord);
|
|
305
|
+
}
|
|
306
|
+
// Closing brackets and these chars end an expression → division.
|
|
307
|
+
if (prevSignificant === ")" || prevSignificant === "]") return true;
|
|
308
|
+
// `.` (member access) ends an expression-ish context → division ( `a./` is odd, treat as div).
|
|
309
|
+
if (prevSignificant === ".") return true;
|
|
310
|
+
// Everything else (operators, `(`, `,`, `{`, `[`, `;`, `:`, `=`, `<`, `>`, `&`,
|
|
311
|
+
// `|`, `!`, `?`, `+`, `-`, `*`, `%`, `^`, `~`) → regex context.
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
while (i < n) {
|
|
316
|
+
const ch = source[i];
|
|
317
|
+
const next = i + 1 < n ? source[i + 1] : "";
|
|
318
|
+
|
|
319
|
+
// ── Line comment ──
|
|
320
|
+
if (ch === "/" && next === "/") {
|
|
321
|
+
out.push("//");
|
|
322
|
+
i += 2;
|
|
323
|
+
while (i < n && source[i] !== "\n") {
|
|
324
|
+
out.push(" ");
|
|
325
|
+
i++;
|
|
326
|
+
}
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// ── Block comment ──
|
|
331
|
+
if (ch === "/" && next === "*") {
|
|
332
|
+
out.push("/*");
|
|
333
|
+
i += 2;
|
|
334
|
+
while (i < n && !(source[i] === "*" && source[i + 1] === "/")) {
|
|
335
|
+
out.push(source[i] === "\n" ? "\n" : " ");
|
|
336
|
+
i++;
|
|
337
|
+
}
|
|
338
|
+
if (i < n) {
|
|
339
|
+
out.push("*/");
|
|
340
|
+
i += 2;
|
|
341
|
+
}
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// ── String literals ' " ──
|
|
346
|
+
if (ch === '"' || ch === "'") {
|
|
347
|
+
const quote = ch;
|
|
348
|
+
out.push(quote);
|
|
349
|
+
i++;
|
|
350
|
+
while (i < n && source[i] !== quote) {
|
|
351
|
+
if (source[i] === "\\" && i + 1 < n) {
|
|
352
|
+
out.push(" "); // blank the escape pair, stay 2 chars wide
|
|
353
|
+
i += 2;
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
if (source[i] === "\n") {
|
|
357
|
+
out.push("\n"); // unterminated string safety
|
|
358
|
+
i++;
|
|
359
|
+
break;
|
|
360
|
+
}
|
|
361
|
+
out.push(" ");
|
|
362
|
+
i++;
|
|
363
|
+
}
|
|
364
|
+
if (i < n && source[i] === quote) {
|
|
365
|
+
out.push(quote);
|
|
366
|
+
i++;
|
|
367
|
+
}
|
|
368
|
+
prevSignificant = quote;
|
|
369
|
+
prevWord = "";
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// ── Template literals ` ` (with ${ ... } interpolation, recursively code) ──
|
|
374
|
+
if (ch === "`") {
|
|
375
|
+
out.push("`");
|
|
376
|
+
i++;
|
|
377
|
+
while (i < n && source[i] !== "`") {
|
|
378
|
+
if (source[i] === "\\" && i + 1 < n) {
|
|
379
|
+
out.push(source[i + 1] === "\n" ? " \n" : " ");
|
|
380
|
+
i += 2;
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
// Interpolation: ${ ... } — the inside IS real code, recurse on it.
|
|
384
|
+
if (source[i] === "$" && source[i + 1] === "{") {
|
|
385
|
+
out.push("${");
|
|
386
|
+
i += 2;
|
|
387
|
+
let depth = 1;
|
|
388
|
+
const exprStart = i;
|
|
389
|
+
while (i < n && depth > 0) {
|
|
390
|
+
if (source[i] === "{") depth++;
|
|
391
|
+
else if (source[i] === "}") depth--;
|
|
392
|
+
if (depth === 0) break;
|
|
393
|
+
i++;
|
|
394
|
+
}
|
|
395
|
+
// Strip literals INSIDE the interpolation too (handles nested strings/regex).
|
|
396
|
+
out.push(stripLiterals(source.slice(exprStart, i)));
|
|
397
|
+
if (i < n && source[i] === "}") {
|
|
398
|
+
out.push("}");
|
|
399
|
+
i++;
|
|
400
|
+
}
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
out.push(source[i] === "\n" ? "\n" : " ");
|
|
404
|
+
i++;
|
|
405
|
+
}
|
|
406
|
+
if (i < n && source[i] === "`") {
|
|
407
|
+
out.push("`");
|
|
408
|
+
i++;
|
|
409
|
+
}
|
|
410
|
+
prevSignificant = "`";
|
|
411
|
+
prevWord = "";
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// ── Regex literal / vs division ──
|
|
416
|
+
if (ch === "/" && !prevEndsExpression()) {
|
|
417
|
+
// Scan a regex literal: /.../flags, honouring escapes and [...] classes.
|
|
418
|
+
let j = i + 1;
|
|
419
|
+
let ok = false;
|
|
420
|
+
let inClass = false;
|
|
421
|
+
while (j < n) {
|
|
422
|
+
const c = source[j];
|
|
423
|
+
if (c === "\\") {
|
|
424
|
+
j += 2;
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
if (c === "\n") break; // regex can't span a newline → not a regex
|
|
428
|
+
if (c === "[") inClass = true;
|
|
429
|
+
else if (c === "]") inClass = false;
|
|
430
|
+
else if (c === "/" && !inClass) {
|
|
431
|
+
ok = true;
|
|
432
|
+
break;
|
|
433
|
+
}
|
|
434
|
+
j++;
|
|
435
|
+
}
|
|
436
|
+
if (ok) {
|
|
437
|
+
out.push("/");
|
|
438
|
+
for (let k = i + 1; k < j; k++) out.push(" ");
|
|
439
|
+
out.push("/");
|
|
440
|
+
i = j + 1;
|
|
441
|
+
// consume flags
|
|
442
|
+
while (i < n && /[a-z]/i.test(source[i])) {
|
|
443
|
+
out.push(source[i]);
|
|
444
|
+
i++;
|
|
445
|
+
}
|
|
446
|
+
prevSignificant = "/"; // a regex value ends an expression-ish slot
|
|
447
|
+
prevWord = "";
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
// Not a regex — fall through, emit `/` as division.
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// ── Ordinary code char ──
|
|
454
|
+
out.push(ch);
|
|
455
|
+
if (!/\s/.test(ch)) {
|
|
456
|
+
prevSignificant = ch;
|
|
457
|
+
if (/[A-Za-z0-9_$]/.test(ch)) {
|
|
458
|
+
prevWord = /[A-Za-z0-9_$]/.test(source[i - 1] ?? "") ? prevWord + ch : ch;
|
|
459
|
+
} else {
|
|
460
|
+
prevWord = "";
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
i++;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
return out.join("");
|
|
467
|
+
}
|
|
468
|
+
|
|
251
469
|
// ── Class & function counting (quick) ────────────────────────
|
|
252
470
|
|
|
253
471
|
function countClassesQuick(source: string): number {
|
|
@@ -259,21 +477,24 @@ function countClassesQuick(source: string): number {
|
|
|
259
477
|
}
|
|
260
478
|
|
|
261
479
|
function countFunctionsQuick(source: string): number {
|
|
480
|
+
// Count on cleaned source so `something(...)` inside a string/regex/comment is
|
|
481
|
+
// never mistaken for a method (the chief source of the old over-count).
|
|
482
|
+
const clean = stripLiterals(source);
|
|
262
483
|
let count = 0;
|
|
263
484
|
// function declarations: function foo(, async function foo(, export function foo(
|
|
264
|
-
const funcDecls =
|
|
485
|
+
const funcDecls = clean.match(
|
|
265
486
|
/(?:^|\n)\s*(?:export\s+)?(?:async\s+)?function\s+\w+\s*\(/g
|
|
266
487
|
);
|
|
267
488
|
if (funcDecls) count += funcDecls.length;
|
|
268
489
|
|
|
269
490
|
// Method declarations inside classes: name(, async name(, static name(, get name(, set name(
|
|
270
|
-
const methods =
|
|
491
|
+
const methods = clean.match(
|
|
271
492
|
/(?:^|\n)\s*(?:public\s+|private\s+|protected\s+)?(?:static\s+)?(?:async\s+)?(?:get\s+|set\s+)?\w+\s*\([^)]*\)\s*(?::\s*\S+)?\s*\{/g
|
|
272
493
|
);
|
|
273
494
|
if (methods) count += methods.length;
|
|
274
495
|
|
|
275
496
|
// Arrow functions assigned to const/let/var
|
|
276
|
-
const arrows =
|
|
497
|
+
const arrows = clean.match(
|
|
277
498
|
/(?:^|\n)\s*(?:export\s+)?(?:const|let|var)\s+\w+\s*=\s*(?:async\s+)?\(/g
|
|
278
499
|
);
|
|
279
500
|
if (arrows) count += arrows.length;
|
|
@@ -286,8 +507,12 @@ function countFunctionsQuick(source: string): number {
|
|
|
286
507
|
function cycloMaticComplexity(funcBody: string): number {
|
|
287
508
|
let cc = 1;
|
|
288
509
|
|
|
289
|
-
//
|
|
290
|
-
//
|
|
510
|
+
// Decision points must be counted on REAL code only — strip string/template/
|
|
511
|
+
// regex literals and comments first so `&&`/`if`/`? :` inside data don't inflate
|
|
512
|
+
// the count (matches the intent of Python's AST-based analyzer).
|
|
513
|
+
const body = stripLiterals(funcBody);
|
|
514
|
+
|
|
515
|
+
// Count decision points via regex.
|
|
291
516
|
const patterns: [RegExp, number][] = [
|
|
292
517
|
[/\bif\s*\(/g, 1],
|
|
293
518
|
[/\belse\s+if\s*\(/g, 1],
|
|
@@ -304,7 +529,7 @@ function cycloMaticComplexity(funcBody: string): number {
|
|
|
304
529
|
];
|
|
305
530
|
|
|
306
531
|
for (const [pattern, weight] of patterns) {
|
|
307
|
-
const matches =
|
|
532
|
+
const matches = body.match(pattern);
|
|
308
533
|
if (matches) cc += matches.length * weight;
|
|
309
534
|
}
|
|
310
535
|
|
|
@@ -322,94 +547,129 @@ interface FunctionInfo {
|
|
|
322
547
|
file?: string;
|
|
323
548
|
}
|
|
324
549
|
|
|
550
|
+
/**
|
|
551
|
+
* Reserved words that look like `keyword(...)` (a call/control-flow head) but are
|
|
552
|
+
* NEVER a function declaration. Guards the loose class-method pattern from
|
|
553
|
+
* extracting `if (...)`, `for (...)`, `return (...)`, `await foo()` etc. as
|
|
554
|
+
* "functions" (the bogus-name source).
|
|
555
|
+
*/
|
|
556
|
+
const NON_FUNCTION_WORDS = new Set([
|
|
557
|
+
"if", "for", "while", "switch", "catch", "return", "new", "class", "import",
|
|
558
|
+
"export", "from", "do", "else", "typeof", "instanceof", "in", "of", "void",
|
|
559
|
+
"delete", "await", "yield", "throw", "super", "this", "function", "const",
|
|
560
|
+
"let", "var", "async", "static", "public", "private", "protected", "get",
|
|
561
|
+
"set", "type", "interface", "enum", "extends", "implements", "as", "case",
|
|
562
|
+
"default", "with", "debugger",
|
|
563
|
+
]);
|
|
564
|
+
|
|
325
565
|
function extractFunctions(source: string, filePath: string, root: string = "."): FunctionInfo[] {
|
|
326
566
|
const functions: FunctionInfo[] = [];
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
//
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
567
|
+
// Detect/extract on LITERAL-STRIPPED source only — a `word(...)` or a bogus
|
|
568
|
+
// name like "name"/"if" living inside a string/template/regex/comment is now
|
|
569
|
+
// blanked out, so it can never be mistaken for a declaration. Newlines are
|
|
570
|
+
// preserved, so line numbers and the brace-matched body stay accurate.
|
|
571
|
+
const lines = stripLiterals(source).split("\n");
|
|
572
|
+
|
|
573
|
+
// Patterns — anchored at the START of the trimmed line so a mid-line call can
|
|
574
|
+
// never match. Only real declaration shapes are accepted.
|
|
575
|
+
// 1) function name(args) / async function name(args) / export …
|
|
576
|
+
const fnDecl = /^(?:export\s+)?(?:default\s+)?(?:async\s+)?function\s*\*?\s*(\w+)\s*\(([^)]*)\)/;
|
|
577
|
+
// 2) Arrow assigned to a binding: const name = (args) => / = async (args) =>
|
|
578
|
+
const arrowDecl = /^(?:export\s+)?(?:default\s+)?(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?\(([^)]*)\)\s*(?::\s*[^=]+)?\s*=>/;
|
|
579
|
+
// 3) A class member: optional modifiers then name(args) … { — only trusted
|
|
580
|
+
// when we're inside a class body, OR a modifier keyword is present.
|
|
581
|
+
const methodMod = /^(?:(public|private|protected)\s+)?(?:(static)\s+)?(?:(async)\s+)?(?:(get|set)\s+)?(\w+)\s*\(([^)]*)\)\s*(?::\s*[^{;]+)?\s*\{/;
|
|
338
582
|
|
|
339
583
|
// Track which class we're in
|
|
340
584
|
let currentClass: string | null = null;
|
|
341
585
|
|
|
342
586
|
for (let i = 0; i < lines.length; i++) {
|
|
343
|
-
const
|
|
344
|
-
const stripped = line.trim();
|
|
587
|
+
const stripped = lines[i].trim();
|
|
345
588
|
|
|
346
589
|
// Detect class entry
|
|
347
590
|
const classMatch = stripped.match(
|
|
348
|
-
|
|
591
|
+
/^(?:export\s+)?(?:default\s+)?(?:abstract\s+)?class\s+(\w+)/
|
|
349
592
|
);
|
|
350
593
|
if (classMatch) {
|
|
351
594
|
currentClass = classMatch[1];
|
|
352
595
|
}
|
|
353
596
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
597
|
+
let funcName: string | null = null;
|
|
598
|
+
let argsStr = "";
|
|
599
|
+
let isTopLevelDecl = false; // function/arrow at module/top scope (not class-qualified)
|
|
600
|
+
|
|
601
|
+
// 1) function declaration
|
|
602
|
+
let m = stripped.match(fnDecl);
|
|
603
|
+
if (m) {
|
|
604
|
+
funcName = m[1];
|
|
605
|
+
argsStr = m[2] || "";
|
|
606
|
+
isTopLevelDecl = true;
|
|
607
|
+
}
|
|
358
608
|
|
|
359
|
-
|
|
609
|
+
// 2) arrow binding
|
|
610
|
+
if (funcName === null) {
|
|
611
|
+
m = stripped.match(arrowDecl);
|
|
612
|
+
if (m) {
|
|
613
|
+
funcName = m[1];
|
|
614
|
+
argsStr = m[2] || "";
|
|
615
|
+
isTopLevelDecl = true;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// 3) class method (only with a modifier, or while inside a class)
|
|
620
|
+
if (funcName === null) {
|
|
621
|
+
m = stripped.match(methodMod);
|
|
622
|
+
if (m) {
|
|
623
|
+
const hasModifier = !!(m[1] || m[2] || m[3] || m[4]);
|
|
624
|
+
const candidate = m[5];
|
|
625
|
+
// Accept only a genuine member: needs a modifier OR an enclosing class.
|
|
626
|
+
// Never accept a reserved control-flow / declaration keyword.
|
|
360
627
|
if (
|
|
361
|
-
|
|
362
|
-
!
|
|
628
|
+
candidate &&
|
|
629
|
+
!NON_FUNCTION_WORDS.has(candidate) &&
|
|
630
|
+
(hasModifier || currentClass !== null)
|
|
363
631
|
) {
|
|
364
|
-
|
|
632
|
+
funcName = candidate;
|
|
633
|
+
argsStr = m[6] || "";
|
|
365
634
|
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
366
637
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
!stripped.startsWith("async function") &&
|
|
374
|
-
!stripped.startsWith("export async function") &&
|
|
375
|
-
!stripped.startsWith("const ") &&
|
|
376
|
-
!stripped.startsWith("let ") &&
|
|
377
|
-
!stripped.startsWith("var ") &&
|
|
378
|
-
!stripped.startsWith("export const ") &&
|
|
379
|
-
!stripped.startsWith("export let ")
|
|
638
|
+
if (funcName !== null && !NON_FUNCTION_WORDS.has(funcName)) {
|
|
639
|
+
// Constructor / class-qualified display name.
|
|
640
|
+
const displayName =
|
|
641
|
+
funcName === "constructor" && currentClass
|
|
642
|
+
? `${currentClass}.constructor`
|
|
643
|
+
: currentClass !== null && !isTopLevelDecl
|
|
380
644
|
? `${currentClass}.${funcName}`
|
|
381
645
|
: funcName;
|
|
382
646
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
});
|
|
403
|
-
|
|
404
|
-
break; // Only match first pattern per line
|
|
405
|
-
}
|
|
647
|
+
// Extract function body by brace matching (on cleaned lines).
|
|
648
|
+
const funcBody = extractFunctionBody(lines, i);
|
|
649
|
+
const funcLoc = funcBody.split("\n").length;
|
|
650
|
+
const complexity = cycloMaticComplexity(funcBody);
|
|
651
|
+
|
|
652
|
+
// Parse args
|
|
653
|
+
const args = argsStr
|
|
654
|
+
.split(",")
|
|
655
|
+
.map((a) => a.trim().split(":")[0].split("=")[0].replace("?", "").trim())
|
|
656
|
+
.filter((a) => a && a !== "this");
|
|
657
|
+
|
|
658
|
+
functions.push({
|
|
659
|
+
name: displayName,
|
|
660
|
+
line: i + 1,
|
|
661
|
+
complexity,
|
|
662
|
+
loc: funcLoc,
|
|
663
|
+
args,
|
|
664
|
+
file: relativePath(filePath, root),
|
|
665
|
+
});
|
|
406
666
|
}
|
|
407
667
|
|
|
408
668
|
// Detect class exit (simple heuristic: closing brace at column 0)
|
|
409
669
|
if (
|
|
410
670
|
currentClass &&
|
|
411
671
|
stripped === "}" &&
|
|
412
|
-
|
|
672
|
+
/^\}/.test(lines[i]) // brace at start of line
|
|
413
673
|
) {
|
|
414
674
|
currentClass = null;
|
|
415
675
|
}
|
|
@@ -345,7 +345,7 @@ export class CorsMiddleware {
|
|
|
345
345
|
const allowedHeaders = process.env.TINA4_CORS_HEADERS
|
|
346
346
|
?? "Content-Type,Authorization,X-Request-ID";
|
|
347
347
|
|
|
348
|
-
const credentials = process.env.TINA4_CORS_CREDENTIALS ?? "
|
|
348
|
+
const credentials = process.env.TINA4_CORS_CREDENTIALS ?? "false";
|
|
349
349
|
|
|
350
350
|
const maxAge = process.env.TINA4_CORS_MAX_AGE
|
|
351
351
|
? parseInt(process.env.TINA4_CORS_MAX_AGE, 10)
|
|
@@ -9,6 +9,11 @@
|
|
|
9
9
|
* TINA4_KAFKA_BROKERS (override; default: "localhost:9092")
|
|
10
10
|
* TINA4_KAFKA_GROUP_ID (default: "tina4_consumer_group")
|
|
11
11
|
*
|
|
12
|
+
* TLS/SASL (each read as TINA4_KAFKA_<NAME> first, then bare KAFKA_<NAME>):
|
|
13
|
+
* TINA4_KAFKA_SECURITY_PROTOCOL — e.g. SSL / SASL_SSL (default: PLAINTEXT)
|
|
14
|
+
* TINA4_KAFKA_SSL_CA_LOCATION — CA cert path for TLS brokers/proxies
|
|
15
|
+
* TINA4_KAFKA_SASL_MECHANISM / TINA4_KAFKA_SASL_USERNAME / TINA4_KAFKA_SASL_PASSWORD — optional SASL
|
|
16
|
+
*
|
|
12
17
|
* Precedence for brokers: specific TINA4_KAFKA_BROKERS var (if set)
|
|
13
18
|
* > value derived from TINA4_QUEUE_URL > existing default.
|
|
14
19
|
*/
|
|
@@ -24,6 +29,62 @@ export interface KafkaConfig {
|
|
|
24
29
|
groupId?: string;
|
|
25
30
|
}
|
|
26
31
|
|
|
32
|
+
/**
|
|
33
|
+
* librdkafka-style SSL/SASL client config (a TLS broker/proxy). Mirrors the
|
|
34
|
+
* keys produced by Python's `KafkaConnector._security_config`. Every key is
|
|
35
|
+
* optional — an unset env var leaves the key OUT (librdkafka defaults to the
|
|
36
|
+
* PLAINTEXT protocol with no SASL).
|
|
37
|
+
*/
|
|
38
|
+
export interface KafkaSecurityConfig {
|
|
39
|
+
"security.protocol"?: string;
|
|
40
|
+
"ssl.ca.location"?: string;
|
|
41
|
+
"sasl.mechanism"?: string;
|
|
42
|
+
"sasl.username"?: string;
|
|
43
|
+
"sasl.password"?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Resolved producer/consumer config — brokers, client id, and security keys. */
|
|
47
|
+
export interface KafkaClientConfig extends KafkaSecurityConfig {
|
|
48
|
+
"bootstrap.servers": string;
|
|
49
|
+
"client.id": string;
|
|
50
|
+
"group.id"?: string;
|
|
51
|
+
"auto.offset.reset"?: string;
|
|
52
|
+
"enable.auto.commit"?: boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Build the SSL/SASL client config from the environment (for a TLS broker or
|
|
57
|
+
* proxy in front of Kafka). Each setting is read from the Tina4-namespaced env
|
|
58
|
+
* var FIRST (`TINA4_KAFKA_SECURITY_PROTOCOL` …) and falls back to the bare
|
|
59
|
+
* librdkafka-convention name (`KAFKA_SECURITY_PROTOCOL` …) that many Kafka
|
|
60
|
+
* deployments already set. Honours security.protocol (e.g. SSL, SASL_SSL),
|
|
61
|
+
* ssl.ca.location, and optional SASL (mechanism / username / password). Unset
|
|
62
|
+
* keys are omitted so librdkafka keeps its PLAINTEXT defaults.
|
|
63
|
+
*
|
|
64
|
+
* Exported for testing/introspection — and exact parity with Python's
|
|
65
|
+
* `_security_config` (same key set, same precedence, same omit-when-unset).
|
|
66
|
+
*/
|
|
67
|
+
export function kafkaSecurityConfig(
|
|
68
|
+
env: NodeJS.ProcessEnv = process.env
|
|
69
|
+
): KafkaSecurityConfig {
|
|
70
|
+
// rdkafka key -> env suffix (read as TINA4_KAFKA_<suffix>, then KAFKA_<suffix>)
|
|
71
|
+
const mapping: [keyof KafkaSecurityConfig, string][] = [
|
|
72
|
+
["security.protocol", "SECURITY_PROTOCOL"],
|
|
73
|
+
["ssl.ca.location", "SSL_CA_LOCATION"],
|
|
74
|
+
["sasl.mechanism", "SASL_MECHANISM"],
|
|
75
|
+
["sasl.username", "SASL_USERNAME"],
|
|
76
|
+
["sasl.password", "SASL_PASSWORD"],
|
|
77
|
+
];
|
|
78
|
+
const config: KafkaSecurityConfig = {};
|
|
79
|
+
for (const [rdk, suffix] of mapping) {
|
|
80
|
+
const value = env[`TINA4_KAFKA_${suffix}`] || env[`KAFKA_${suffix}`];
|
|
81
|
+
if (value) {
|
|
82
|
+
config[rdk] = value;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return config;
|
|
86
|
+
}
|
|
87
|
+
|
|
27
88
|
export interface QueueBackend {
|
|
28
89
|
push(queue: string, payload: unknown, delay?: number): string;
|
|
29
90
|
pop(queue: string): QueueJob | null;
|
|
@@ -77,6 +138,42 @@ export class KafkaBackend implements QueueBackend {
|
|
|
77
138
|
return { brokers: this.brokers, groupId: this.groupId };
|
|
78
139
|
}
|
|
79
140
|
|
|
141
|
+
/**
|
|
142
|
+
* Resolved SSL/SASL client config from the environment (PLAINTEXT default).
|
|
143
|
+
* Mirrors Python's `KafkaConnector._security_config`.
|
|
144
|
+
*/
|
|
145
|
+
securityConfig(): KafkaSecurityConfig {
|
|
146
|
+
return kafkaSecurityConfig();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Full producer config — brokers + client id + the resolved security block.
|
|
151
|
+
* The security keys are applied to BOTH producer and consumer (matching
|
|
152
|
+
* Python's `_connect_confluent`).
|
|
153
|
+
*/
|
|
154
|
+
producerConfig(): KafkaClientConfig {
|
|
155
|
+
return {
|
|
156
|
+
"bootstrap.servers": this.brokers,
|
|
157
|
+
"client.id": "tina4-nodejs",
|
|
158
|
+
...this.securityConfig(),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Full consumer config — brokers + client id + group id + the SAME resolved
|
|
164
|
+
* security block applied to the producer.
|
|
165
|
+
*/
|
|
166
|
+
consumerConfig(): KafkaClientConfig {
|
|
167
|
+
return {
|
|
168
|
+
"bootstrap.servers": this.brokers,
|
|
169
|
+
"client.id": "tina4-nodejs",
|
|
170
|
+
"group.id": this.groupId,
|
|
171
|
+
"auto.offset.reset": "earliest",
|
|
172
|
+
"enable.auto.commit": false,
|
|
173
|
+
...this.securityConfig(),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
80
177
|
/**
|
|
81
178
|
* Parse broker string into host:port.
|
|
82
179
|
*/
|