tina4-nodejs 3.13.86 → 3.13.87
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 +3 -3
- package/package.json +1 -1
- package/packages/cli/dist/bin.js +148 -12
- package/packages/core/dist/index.js +147 -11
- package/packages/core/src/metrics.ts +11 -2
- package/packages/frond/dist/index.js +104 -5
- package/packages/frond/src/engine.ts +159 -6
- package/packages/orm/dist/index.js +147 -11
- package/packages/orm/src/adapters/firebird.ts +28 -2
- package/packages/orm/src/adapters/postgres.ts +11 -0
- package/packages/orm/src/database.ts +15 -3
package/CLAUDE.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
# CLAUDE.md - AI Developer Guide for tina4-nodejs (v3.13.
|
|
1
|
+
# CLAUDE.md - AI Developer Guide for tina4-nodejs (v3.13.87)
|
|
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.13.
|
|
7
|
+
Tina4 for Node.js/TypeScript v3.13.87 - The Intelligent Native Application 4ramework. A convention-over-configuration structural paradigm. 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
|
|
|
@@ -1244,7 +1244,7 @@ When adding new features, add a corresponding `test/<feature>.test.ts` file.
|
|
|
1244
1244
|
## v3 Features Summary
|
|
1245
1245
|
|
|
1246
1246
|
- **98 built-in features**, zero third-party dependencies
|
|
1247
|
-
- **5,
|
|
1247
|
+
- **5,879 tests** passing across 189 files (build + typecheck green)
|
|
1248
1248
|
- **Race-safe `getNextId()`** with atomic sequence table (`tina4_sequences`) for SQLite/MySQL/MSSQL; PostgreSQL auto-creates sequences
|
|
1249
1249
|
- **Frond template engine optimizations**: pre-compiled regexes, lazy loop context (copy-on-write), filter chain caching, path split caching, inline common filters (11-15% speedup)
|
|
1250
1250
|
- **Production server auto-detect**: `npx tina4nodejs serve --production` auto-uses cluster mode
|
package/package.json
CHANGED
package/packages/cli/dist/bin.js
CHANGED
|
@@ -1675,18 +1675,21 @@ var init_postgres = __esm({
|
|
|
1675
1675
|
}
|
|
1676
1676
|
async startTransactionAsync() {
|
|
1677
1677
|
await this.executeAsync("BEGIN");
|
|
1678
|
+
this._inTransaction = true;
|
|
1678
1679
|
}
|
|
1679
1680
|
commit() {
|
|
1680
1681
|
throw new Error("Use commitAsync() for PostgreSQL.");
|
|
1681
1682
|
}
|
|
1682
1683
|
async commitAsync() {
|
|
1683
1684
|
await this.executeAsync("COMMIT");
|
|
1685
|
+
this._inTransaction = false;
|
|
1684
1686
|
}
|
|
1685
1687
|
rollback() {
|
|
1686
1688
|
throw new Error("Use rollbackAsync() for PostgreSQL.");
|
|
1687
1689
|
}
|
|
1688
1690
|
async rollbackAsync() {
|
|
1689
1691
|
await this.executeAsync("ROLLBACK");
|
|
1692
|
+
this._inTransaction = false;
|
|
1690
1693
|
}
|
|
1691
1694
|
tables() {
|
|
1692
1695
|
throw new Error("Use tablesAsync() for PostgreSQL.");
|
|
@@ -2705,10 +2708,35 @@ var init_firebird = __esm({
|
|
|
2705
2708
|
translated = SQLTranslator.ilikeToLike(translated);
|
|
2706
2709
|
return translated;
|
|
2707
2710
|
}
|
|
2711
|
+
/**
|
|
2712
|
+
* The handle every statement runs on. While an explicit transaction is open
|
|
2713
|
+
* (startTransactionAsync set `this.transaction`), statements MUST run on that
|
|
2714
|
+
* transaction object so they are undone by rollbackAsync() / persisted by
|
|
2715
|
+
* commitAsync() — node-firebird's transaction exposes the same
|
|
2716
|
+
* query()/execute() as the connection. With no transaction open we run on
|
|
2717
|
+
* `this.db`, whose per-statement work auto-commits on the connection.
|
|
2718
|
+
*
|
|
2719
|
+
* This matches the Python master's contract (tina4_python/database/firebird.py):
|
|
2720
|
+
* there, ALL statements run on the single connection and start_transaction()
|
|
2721
|
+
* merely suppresses the per-statement autocommit in execute() so the batch
|
|
2722
|
+
* stays open until commit()/rollback(). node-firebird has no such suppression
|
|
2723
|
+
* hook — its `db.query/execute` always auto-commit — so the equivalent is to
|
|
2724
|
+
* route statements through the transaction object instead. Same observable
|
|
2725
|
+
* behaviour: an open transaction is atomic and rolls back cleanly.
|
|
2726
|
+
*
|
|
2727
|
+
* Previously every statement ran on `this.db` unconditionally, so the
|
|
2728
|
+
* transaction created by startTransactionAsync() never saw a single statement
|
|
2729
|
+
* — rollbackAsync() rolled back an EMPTY transaction and the already
|
|
2730
|
+
* auto-committed write survived (silent no-op). Twin of the PHP pdo_firebird
|
|
2731
|
+
* bug fixed in 3.13.86.
|
|
2732
|
+
*/
|
|
2733
|
+
statementHandle() {
|
|
2734
|
+
return this.transaction ?? this.db;
|
|
2735
|
+
}
|
|
2708
2736
|
queryPromise(sql, params) {
|
|
2709
2737
|
return new Promise((resolve31, reject) => {
|
|
2710
2738
|
const translated = this.translateSql(sql);
|
|
2711
|
-
this.
|
|
2739
|
+
this.statementHandle().query(translated, params ?? [], (err, result) => {
|
|
2712
2740
|
if (err) reject(err);
|
|
2713
2741
|
else resolve31(result ?? []);
|
|
2714
2742
|
});
|
|
@@ -2717,7 +2745,7 @@ var init_firebird = __esm({
|
|
|
2717
2745
|
executePromise(sql, params) {
|
|
2718
2746
|
return new Promise((resolve31, reject) => {
|
|
2719
2747
|
const translated = this.translateSql(sql);
|
|
2720
|
-
this.
|
|
2748
|
+
this.statementHandle().execute(translated, params ?? [], (err) => {
|
|
2721
2749
|
if (err) reject(err);
|
|
2722
2750
|
else resolve31();
|
|
2723
2751
|
});
|
|
@@ -4691,14 +4719,15 @@ var init_database = __esm({
|
|
|
4691
4719
|
async executeMany(sql, paramSets = []) {
|
|
4692
4720
|
const adapter = this.getNextAdapter();
|
|
4693
4721
|
const results = [];
|
|
4694
|
-
|
|
4722
|
+
const owns = !this.inExplicitTransaction();
|
|
4723
|
+
if (owns) await adapterStartTransaction(adapter);
|
|
4695
4724
|
try {
|
|
4696
4725
|
for (const params of paramSets) {
|
|
4697
4726
|
results.push(await adapterExecute(adapter, sql, params));
|
|
4698
4727
|
}
|
|
4699
|
-
await adapterCommit(adapter);
|
|
4728
|
+
if (owns) await adapterCommit(adapter);
|
|
4700
4729
|
} catch (e) {
|
|
4701
|
-
await adapterRollback(adapter);
|
|
4730
|
+
if (owns) await adapterRollback(adapter);
|
|
4702
4731
|
throw e;
|
|
4703
4732
|
}
|
|
4704
4733
|
return results;
|
|
@@ -12751,6 +12780,7 @@ var init_request = __esm({
|
|
|
12751
12780
|
var engine_exports = {};
|
|
12752
12781
|
__export(engine_exports, {
|
|
12753
12782
|
Frond: () => Frond,
|
|
12783
|
+
TEMPLATE_CACHE_MAX: () => TEMPLATE_CACHE_MAX,
|
|
12754
12784
|
setFormTokenSessionId: () => setFormTokenSessionId
|
|
12755
12785
|
});
|
|
12756
12786
|
import { createHash as createHash2, createHmac as createHmac3, randomBytes as randomBytes4 } from "node:crypto";
|
|
@@ -12826,6 +12856,14 @@ function renderDump(value) {
|
|
|
12826
12856
|
function liveAttr(value) {
|
|
12827
12857
|
return String(value).replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
12828
12858
|
}
|
|
12859
|
+
function capCache(cache, maxEntries) {
|
|
12860
|
+
if (cache.size < maxEntries) return;
|
|
12861
|
+
let drop = Math.floor(maxEntries / 2);
|
|
12862
|
+
for (const key of cache.keys()) {
|
|
12863
|
+
cache.delete(key);
|
|
12864
|
+
if (--drop <= 0) break;
|
|
12865
|
+
}
|
|
12866
|
+
}
|
|
12829
12867
|
function tokenize(source) {
|
|
12830
12868
|
const rawBlocks = [];
|
|
12831
12869
|
source = source.replace(RAW_BLOCK_RE, (_match, content) => {
|
|
@@ -13177,6 +13215,9 @@ function evalExpr(expr, context) {
|
|
|
13177
13215
|
}).join("");
|
|
13178
13216
|
}
|
|
13179
13217
|
}
|
|
13218
|
+
if (expr.startsWith("not ")) {
|
|
13219
|
+
return evalComparison(expr, context);
|
|
13220
|
+
}
|
|
13180
13221
|
for (const op of [" not in ", " in ", " is not ", " is ", "!=", "==", ">=", "<=", ">", "<", " and ", " or ", " not "]) {
|
|
13181
13222
|
if (findOutsideQuotes(expr, op) >= 0) {
|
|
13182
13223
|
return evalComparison(expr, context);
|
|
@@ -13732,7 +13773,7 @@ function _generateFormToken(descriptor = "") {
|
|
|
13732
13773
|
function _generateFormTokenValue(descriptor = "") {
|
|
13733
13774
|
return new SafeString(_buildFormTokenJwt(descriptor));
|
|
13734
13775
|
}
|
|
13735
|
-
var SafeString, NUMERIC_RE, METHOD_CALL_RE, FN_CALL_RE, IS_NOT_RE, IS_RE, NOT_IN_RE, IN_RE, DIVISIBLE_BY_RE, FILTER_WITH_ARGS_RE, FILTER_COMPARISON_RE, TITLE_WORD_RE, STRIP_TAGS_RE, FORMAT_RE, LEADING_WS_RE, TRAILING_WS_RE, THOUSANDS_RE, LIVE_RE, LIVE_WS_RE, LIVE_SRC_RE, filterChainCache, pathParseCache, TOKEN_RE, RAW_BLOCK_RE, VarRef, BUILTIN_FILTERS, _formTokenSessionId, Frond;
|
|
13776
|
+
var SafeString, NUMERIC_RE, METHOD_CALL_RE, FN_CALL_RE, IS_NOT_RE, IS_RE, NOT_IN_RE, IN_RE, DIVISIBLE_BY_RE, FILTER_WITH_ARGS_RE, FILTER_COMPARISON_RE, TITLE_WORD_RE, STRIP_TAGS_RE, FORMAT_RE, LEADING_WS_RE, TRAILING_WS_RE, THOUSANDS_RE, LIVE_RE, LIVE_WS_RE, LIVE_SRC_RE, filterChainCache, pathParseCache, TEMPLATE_CACHE_MAX, TOKEN_RE, RAW_BLOCK_RE, VarRef, BUILTIN_FILTERS, _formTokenSessionId, Frond;
|
|
13736
13777
|
var init_engine = __esm({
|
|
13737
13778
|
"../frond/src/engine.ts"() {
|
|
13738
13779
|
"use strict";
|
|
@@ -13765,6 +13806,7 @@ var init_engine = __esm({
|
|
|
13765
13806
|
LIVE_SRC_RE = /src\s+["']([^"']+)["']/;
|
|
13766
13807
|
filterChainCache = /* @__PURE__ */ new Map();
|
|
13767
13808
|
pathParseCache = /* @__PURE__ */ new Map();
|
|
13809
|
+
TEMPLATE_CACHE_MAX = 256;
|
|
13768
13810
|
TOKEN_RE = /(\{%-?\s*[\s\S]*?\s*-?%\})|(\{\{-?\s*[\s\S]*?\s*-?\}\})|(\{#[\s\S]*?#\})/g;
|
|
13769
13811
|
RAW_BLOCK_RE = /\{%-?\s*raw\s*-?%\}([\s\S]*?)\{%-?\s*endraw\s*-?%\}/g;
|
|
13770
13812
|
VarRef = class {
|
|
@@ -14170,6 +14212,7 @@ var init_engine = __esm({
|
|
|
14170
14212
|
const source = readFileSync6(filePath, "utf-8");
|
|
14171
14213
|
const mtime = statSync6(filePath).mtimeMs;
|
|
14172
14214
|
const tokens = tokenize(source);
|
|
14215
|
+
capCache(this.compiled, TEMPLATE_CACHE_MAX);
|
|
14173
14216
|
this.compiled.set(template, { tokens, mtime, cachedAt: Date.now() });
|
|
14174
14217
|
return this.executeWithSource(source, tokens, context);
|
|
14175
14218
|
}
|
|
@@ -14184,6 +14227,7 @@ var init_engine = __esm({
|
|
|
14184
14227
|
}
|
|
14185
14228
|
}
|
|
14186
14229
|
const tokens = tokenize(source);
|
|
14230
|
+
capCache(this.compiledStrings, TEMPLATE_CACHE_MAX);
|
|
14187
14231
|
this.compiledStrings.set(key, { tokens, cachedAt: Date.now() });
|
|
14188
14232
|
return this.executeCached(tokens, context);
|
|
14189
14233
|
}
|
|
@@ -14399,6 +14443,9 @@ var init_engine = __esm({
|
|
|
14399
14443
|
} else if (tag === "macro") {
|
|
14400
14444
|
const skip = this.handleMacro(tokens, i, context);
|
|
14401
14445
|
i = skip;
|
|
14446
|
+
} else if (tag === "import") {
|
|
14447
|
+
this.handleImportAs(content, context);
|
|
14448
|
+
i++;
|
|
14402
14449
|
} else if (tag === "from") {
|
|
14403
14450
|
this.handleFromImport(content, context);
|
|
14404
14451
|
i++;
|
|
@@ -14965,7 +15012,7 @@ var init_engine = __esm({
|
|
|
14965
15012
|
return i2;
|
|
14966
15013
|
}
|
|
14967
15014
|
const macroName = m[1];
|
|
14968
|
-
const
|
|
15015
|
+
const params = _Frond.parseMacroParams(m[2]);
|
|
14969
15016
|
const bodyTokens = [];
|
|
14970
15017
|
let i = start2 + 1;
|
|
14971
15018
|
while (i < tokens.length) {
|
|
@@ -14980,13 +15027,94 @@ var init_engine = __esm({
|
|
|
14980
15027
|
const capturedContext = { ...context };
|
|
14981
15028
|
context[macroName] = (...args) => {
|
|
14982
15029
|
const macroCtx = { ...capturedContext };
|
|
14983
|
-
for (let pi = 0; pi <
|
|
14984
|
-
|
|
15030
|
+
for (let pi = 0; pi < params.length; pi++) {
|
|
15031
|
+
const [pname, pdefault] = params[pi];
|
|
15032
|
+
macroCtx[pname] = pi < args.length ? args[pi] : pdefault;
|
|
14985
15033
|
}
|
|
14986
15034
|
return new SafeString(engine.renderTokens([...bodyTokens], macroCtx));
|
|
14987
15035
|
};
|
|
14988
15036
|
return i;
|
|
14989
15037
|
}
|
|
15038
|
+
/**
|
|
15039
|
+
* Parse a macro parameter list into [name, default] pairs.
|
|
15040
|
+
*
|
|
15041
|
+
* Handles: name, name="default", name='default'. Splitting on "," alone left a
|
|
15042
|
+
* defaulted parameter literally NAMED `greeting='Hello'`, so the body's
|
|
15043
|
+
* {{ greeting }} matched nothing (rendered empty) AND the caller's positional
|
|
15044
|
+
* argument was stored under that junk key and lost. Mirrors the Python master's
|
|
15045
|
+
* _parse_macro_params. The default is null when none is declared.
|
|
15046
|
+
*/
|
|
15047
|
+
static parseMacroParams(rawParams) {
|
|
15048
|
+
return rawParams.split(",").map((p) => p.trim()).filter(Boolean).map((p) => {
|
|
15049
|
+
const eq = p.indexOf("=");
|
|
15050
|
+
if (eq === -1) return [p, null];
|
|
15051
|
+
const name = p.slice(0, eq).trim();
|
|
15052
|
+
let dflt = p.slice(eq + 1).trim();
|
|
15053
|
+
if (dflt.length >= 2 && (dflt.startsWith('"') && dflt.endsWith('"') || dflt.startsWith("'") && dflt.endsWith("'"))) {
|
|
15054
|
+
dflt = dflt.slice(1, -1);
|
|
15055
|
+
}
|
|
15056
|
+
return [name, dflt];
|
|
15057
|
+
});
|
|
15058
|
+
}
|
|
15059
|
+
/**
|
|
15060
|
+
* {% import "file" as alias %} -- load EVERY macro in a file under one namespace.
|
|
15061
|
+
*
|
|
15062
|
+
* The alias is bound as a plain object of macro functions, so {{ alias.greet(x) }}
|
|
15063
|
+
* resolves through the engine's existing dotted-call path and each macro keeps the
|
|
15064
|
+
* same argument binding, default handling and SafeString output as any other macro.
|
|
15065
|
+
* A namespace object (not a class) is deliberate: a function stored as a class
|
|
15066
|
+
* attribute binds as a method and would inject the namespace as the first argument,
|
|
15067
|
+
* which is exactly the argument-shift bug the Python master carried (fixed there
|
|
15068
|
+
* with types.SimpleNamespace). Both import forms must render identically.
|
|
15069
|
+
*/
|
|
15070
|
+
handleImportAs(content, context) {
|
|
15071
|
+
const m = content.match(/^import\s+["'](.+?)["']\s+as\s+(\w+)/);
|
|
15072
|
+
if (!m) return;
|
|
15073
|
+
const filename = m[1];
|
|
15074
|
+
const alias = m[2];
|
|
15075
|
+
const namespace = {};
|
|
15076
|
+
const source = this.load(filename);
|
|
15077
|
+
const tokens = tokenize(source);
|
|
15078
|
+
let i = 0;
|
|
15079
|
+
while (i < tokens.length) {
|
|
15080
|
+
const [ttype, raw] = tokens[i];
|
|
15081
|
+
if (ttype === "BLOCK") {
|
|
15082
|
+
const [tagContent] = stripTag(raw);
|
|
15083
|
+
if ((tagContent.split(/\s+/)[0] || "") === "macro") {
|
|
15084
|
+
const macroM = tagContent.match(/^macro\s+(\w+)\s*\(([^)]*)\)/);
|
|
15085
|
+
if (macroM) {
|
|
15086
|
+
const macroName = macroM[1];
|
|
15087
|
+
const params = _Frond.parseMacroParams(macroM[2]);
|
|
15088
|
+
const bodyTokens = [];
|
|
15089
|
+
i++;
|
|
15090
|
+
while (i < tokens.length) {
|
|
15091
|
+
if (tokens[i][0] === "BLOCK" && tokens[i][1].includes("endmacro")) {
|
|
15092
|
+
i++;
|
|
15093
|
+
break;
|
|
15094
|
+
}
|
|
15095
|
+
bodyTokens.push(tokens[i]);
|
|
15096
|
+
i++;
|
|
15097
|
+
}
|
|
15098
|
+
const capturedBody = [...bodyTokens];
|
|
15099
|
+
const capturedParams = [...params];
|
|
15100
|
+
const capturedCtx = { ...context };
|
|
15101
|
+
const engine = this;
|
|
15102
|
+
namespace[macroName] = (...args) => {
|
|
15103
|
+
const macroCtx = { ...capturedCtx };
|
|
15104
|
+
for (let pi = 0; pi < capturedParams.length; pi++) {
|
|
15105
|
+
const [pname, pdefault] = capturedParams[pi];
|
|
15106
|
+
macroCtx[pname] = pi < args.length ? args[pi] : pdefault;
|
|
15107
|
+
}
|
|
15108
|
+
return new SafeString(engine.renderTokens([...capturedBody], macroCtx));
|
|
15109
|
+
};
|
|
15110
|
+
continue;
|
|
15111
|
+
}
|
|
15112
|
+
}
|
|
15113
|
+
}
|
|
15114
|
+
i++;
|
|
15115
|
+
}
|
|
15116
|
+
context[alias] = namespace;
|
|
15117
|
+
}
|
|
14990
15118
|
handleFromImport(content, context) {
|
|
14991
15119
|
const m = content.match(/^from\s+["'](.+?)["']\s+import\s+(.+)/);
|
|
14992
15120
|
if (!m) return;
|
|
@@ -15004,7 +15132,7 @@ var init_engine = __esm({
|
|
|
15004
15132
|
const macroM = tagContent.match(/^macro\s+(\w+)\s*\(([^)]*)\)/);
|
|
15005
15133
|
if (macroM && names.includes(macroM[1])) {
|
|
15006
15134
|
const macroName = macroM[1];
|
|
15007
|
-
const paramNames = macroM[2]
|
|
15135
|
+
const paramNames = _Frond.parseMacroParams(macroM[2]);
|
|
15008
15136
|
const bodyTokens = [];
|
|
15009
15137
|
i++;
|
|
15010
15138
|
while (i < tokens.length) {
|
|
@@ -15022,7 +15150,8 @@ var init_engine = __esm({
|
|
|
15022
15150
|
context[macroName] = (...args) => {
|
|
15023
15151
|
const macroCtx = { ...capturedCtx };
|
|
15024
15152
|
for (let pi = 0; pi < capturedParams.length; pi++) {
|
|
15025
|
-
|
|
15153
|
+
const [pname, pdefault] = capturedParams[pi];
|
|
15154
|
+
macroCtx[pname] = pi < args.length ? args[pi] : pdefault;
|
|
15026
15155
|
}
|
|
15027
15156
|
return new SafeString(engine.renderTokens([...capturedBody], macroCtx));
|
|
15028
15157
|
};
|
|
@@ -18129,7 +18258,14 @@ function fullAnalysis(root = "src") {
|
|
|
18129
18258
|
total_functions: allFunctions.length,
|
|
18130
18259
|
avg_complexity: Math.round(avgCC * 100) / 100,
|
|
18131
18260
|
avg_maintainability: Math.round(avgMI * 10) / 10,
|
|
18261
|
+
// Display-only: the top-15 for the "most complex functions" report.
|
|
18262
|
+
// Do NOT source offenders / --fail-on from this — capping here silently
|
|
18263
|
+
// hides the 16th+ over-threshold function from the gate. offenders()
|
|
18264
|
+
// reads "all_functions" (below) instead.
|
|
18132
18265
|
most_complex_functions: allFunctions.slice(0, 15),
|
|
18266
|
+
// Full, uncapped, complexity-sorted list — offenders()/--fail-on use this
|
|
18267
|
+
// so no function over the complexity threshold ever escapes the gate.
|
|
18268
|
+
all_functions: allFunctions,
|
|
18133
18269
|
file_metrics: fileMetrics,
|
|
18134
18270
|
violations,
|
|
18135
18271
|
dependency_graph: importGraph,
|
|
@@ -18145,7 +18281,7 @@ function offenders(root = "src", top = 20) {
|
|
|
18145
18281
|
return { offenders: [], summary: { error: analysis.error } };
|
|
18146
18282
|
}
|
|
18147
18283
|
const items = [];
|
|
18148
|
-
for (const fn of analysis.most_complex_functions || []) {
|
|
18284
|
+
for (const fn of analysis.all_functions || analysis.most_complex_functions || []) {
|
|
18149
18285
|
const cc = fn.complexity;
|
|
18150
18286
|
if (cc > 10) {
|
|
18151
18287
|
items.push({
|
|
@@ -1674,18 +1674,21 @@ var init_postgres = __esm({
|
|
|
1674
1674
|
}
|
|
1675
1675
|
async startTransactionAsync() {
|
|
1676
1676
|
await this.executeAsync("BEGIN");
|
|
1677
|
+
this._inTransaction = true;
|
|
1677
1678
|
}
|
|
1678
1679
|
commit() {
|
|
1679
1680
|
throw new Error("Use commitAsync() for PostgreSQL.");
|
|
1680
1681
|
}
|
|
1681
1682
|
async commitAsync() {
|
|
1682
1683
|
await this.executeAsync("COMMIT");
|
|
1684
|
+
this._inTransaction = false;
|
|
1683
1685
|
}
|
|
1684
1686
|
rollback() {
|
|
1685
1687
|
throw new Error("Use rollbackAsync() for PostgreSQL.");
|
|
1686
1688
|
}
|
|
1687
1689
|
async rollbackAsync() {
|
|
1688
1690
|
await this.executeAsync("ROLLBACK");
|
|
1691
|
+
this._inTransaction = false;
|
|
1689
1692
|
}
|
|
1690
1693
|
tables() {
|
|
1691
1694
|
throw new Error("Use tablesAsync() for PostgreSQL.");
|
|
@@ -2704,10 +2707,35 @@ var init_firebird = __esm({
|
|
|
2704
2707
|
translated = SQLTranslator.ilikeToLike(translated);
|
|
2705
2708
|
return translated;
|
|
2706
2709
|
}
|
|
2710
|
+
/**
|
|
2711
|
+
* The handle every statement runs on. While an explicit transaction is open
|
|
2712
|
+
* (startTransactionAsync set `this.transaction`), statements MUST run on that
|
|
2713
|
+
* transaction object so they are undone by rollbackAsync() / persisted by
|
|
2714
|
+
* commitAsync() — node-firebird's transaction exposes the same
|
|
2715
|
+
* query()/execute() as the connection. With no transaction open we run on
|
|
2716
|
+
* `this.db`, whose per-statement work auto-commits on the connection.
|
|
2717
|
+
*
|
|
2718
|
+
* This matches the Python master's contract (tina4_python/database/firebird.py):
|
|
2719
|
+
* there, ALL statements run on the single connection and start_transaction()
|
|
2720
|
+
* merely suppresses the per-statement autocommit in execute() so the batch
|
|
2721
|
+
* stays open until commit()/rollback(). node-firebird has no such suppression
|
|
2722
|
+
* hook — its `db.query/execute` always auto-commit — so the equivalent is to
|
|
2723
|
+
* route statements through the transaction object instead. Same observable
|
|
2724
|
+
* behaviour: an open transaction is atomic and rolls back cleanly.
|
|
2725
|
+
*
|
|
2726
|
+
* Previously every statement ran on `this.db` unconditionally, so the
|
|
2727
|
+
* transaction created by startTransactionAsync() never saw a single statement
|
|
2728
|
+
* — rollbackAsync() rolled back an EMPTY transaction and the already
|
|
2729
|
+
* auto-committed write survived (silent no-op). Twin of the PHP pdo_firebird
|
|
2730
|
+
* bug fixed in 3.13.86.
|
|
2731
|
+
*/
|
|
2732
|
+
statementHandle() {
|
|
2733
|
+
return this.transaction ?? this.db;
|
|
2734
|
+
}
|
|
2707
2735
|
queryPromise(sql, params) {
|
|
2708
2736
|
return new Promise((resolve21, reject) => {
|
|
2709
2737
|
const translated = this.translateSql(sql);
|
|
2710
|
-
this.
|
|
2738
|
+
this.statementHandle().query(translated, params ?? [], (err, result) => {
|
|
2711
2739
|
if (err) reject(err);
|
|
2712
2740
|
else resolve21(result ?? []);
|
|
2713
2741
|
});
|
|
@@ -2716,7 +2744,7 @@ var init_firebird = __esm({
|
|
|
2716
2744
|
executePromise(sql, params) {
|
|
2717
2745
|
return new Promise((resolve21, reject) => {
|
|
2718
2746
|
const translated = this.translateSql(sql);
|
|
2719
|
-
this.
|
|
2747
|
+
this.statementHandle().execute(translated, params ?? [], (err) => {
|
|
2720
2748
|
if (err) reject(err);
|
|
2721
2749
|
else resolve21();
|
|
2722
2750
|
});
|
|
@@ -4690,14 +4718,15 @@ var init_database = __esm({
|
|
|
4690
4718
|
async executeMany(sql, paramSets = []) {
|
|
4691
4719
|
const adapter = this.getNextAdapter();
|
|
4692
4720
|
const results = [];
|
|
4693
|
-
|
|
4721
|
+
const owns = !this.inExplicitTransaction();
|
|
4722
|
+
if (owns) await adapterStartTransaction(adapter);
|
|
4694
4723
|
try {
|
|
4695
4724
|
for (const params of paramSets) {
|
|
4696
4725
|
results.push(await adapterExecute(adapter, sql, params));
|
|
4697
4726
|
}
|
|
4698
|
-
await adapterCommit(adapter);
|
|
4727
|
+
if (owns) await adapterCommit(adapter);
|
|
4699
4728
|
} catch (e) {
|
|
4700
|
-
await adapterRollback(adapter);
|
|
4729
|
+
if (owns) await adapterRollback(adapter);
|
|
4701
4730
|
throw e;
|
|
4702
4731
|
}
|
|
4703
4732
|
return results;
|
|
@@ -12750,6 +12779,7 @@ var init_request = __esm({
|
|
|
12750
12779
|
var engine_exports = {};
|
|
12751
12780
|
__export(engine_exports, {
|
|
12752
12781
|
Frond: () => Frond,
|
|
12782
|
+
TEMPLATE_CACHE_MAX: () => TEMPLATE_CACHE_MAX,
|
|
12753
12783
|
setFormTokenSessionId: () => setFormTokenSessionId
|
|
12754
12784
|
});
|
|
12755
12785
|
import { createHash as createHash2, createHmac as createHmac3, randomBytes as randomBytes4 } from "node:crypto";
|
|
@@ -12825,6 +12855,14 @@ function renderDump(value) {
|
|
|
12825
12855
|
function liveAttr(value) {
|
|
12826
12856
|
return String(value).replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
12827
12857
|
}
|
|
12858
|
+
function capCache(cache, maxEntries) {
|
|
12859
|
+
if (cache.size < maxEntries) return;
|
|
12860
|
+
let drop = Math.floor(maxEntries / 2);
|
|
12861
|
+
for (const key of cache.keys()) {
|
|
12862
|
+
cache.delete(key);
|
|
12863
|
+
if (--drop <= 0) break;
|
|
12864
|
+
}
|
|
12865
|
+
}
|
|
12828
12866
|
function tokenize(source) {
|
|
12829
12867
|
const rawBlocks = [];
|
|
12830
12868
|
source = source.replace(RAW_BLOCK_RE, (_match, content) => {
|
|
@@ -13176,6 +13214,9 @@ function evalExpr(expr, context) {
|
|
|
13176
13214
|
}).join("");
|
|
13177
13215
|
}
|
|
13178
13216
|
}
|
|
13217
|
+
if (expr.startsWith("not ")) {
|
|
13218
|
+
return evalComparison(expr, context);
|
|
13219
|
+
}
|
|
13179
13220
|
for (const op of [" not in ", " in ", " is not ", " is ", "!=", "==", ">=", "<=", ">", "<", " and ", " or ", " not "]) {
|
|
13180
13221
|
if (findOutsideQuotes(expr, op) >= 0) {
|
|
13181
13222
|
return evalComparison(expr, context);
|
|
@@ -13731,7 +13772,7 @@ function _generateFormToken(descriptor = "") {
|
|
|
13731
13772
|
function _generateFormTokenValue(descriptor = "") {
|
|
13732
13773
|
return new SafeString(_buildFormTokenJwt(descriptor));
|
|
13733
13774
|
}
|
|
13734
|
-
var SafeString, NUMERIC_RE, METHOD_CALL_RE, FN_CALL_RE, IS_NOT_RE, IS_RE, NOT_IN_RE, IN_RE, DIVISIBLE_BY_RE, FILTER_WITH_ARGS_RE, FILTER_COMPARISON_RE, TITLE_WORD_RE, STRIP_TAGS_RE, FORMAT_RE, LEADING_WS_RE, TRAILING_WS_RE, THOUSANDS_RE, LIVE_RE, LIVE_WS_RE, LIVE_SRC_RE, filterChainCache, pathParseCache, TOKEN_RE, RAW_BLOCK_RE, VarRef, BUILTIN_FILTERS, _formTokenSessionId, Frond;
|
|
13775
|
+
var SafeString, NUMERIC_RE, METHOD_CALL_RE, FN_CALL_RE, IS_NOT_RE, IS_RE, NOT_IN_RE, IN_RE, DIVISIBLE_BY_RE, FILTER_WITH_ARGS_RE, FILTER_COMPARISON_RE, TITLE_WORD_RE, STRIP_TAGS_RE, FORMAT_RE, LEADING_WS_RE, TRAILING_WS_RE, THOUSANDS_RE, LIVE_RE, LIVE_WS_RE, LIVE_SRC_RE, filterChainCache, pathParseCache, TEMPLATE_CACHE_MAX, TOKEN_RE, RAW_BLOCK_RE, VarRef, BUILTIN_FILTERS, _formTokenSessionId, Frond;
|
|
13735
13776
|
var init_engine = __esm({
|
|
13736
13777
|
"../frond/src/engine.ts"() {
|
|
13737
13778
|
"use strict";
|
|
@@ -13764,6 +13805,7 @@ var init_engine = __esm({
|
|
|
13764
13805
|
LIVE_SRC_RE = /src\s+["']([^"']+)["']/;
|
|
13765
13806
|
filterChainCache = /* @__PURE__ */ new Map();
|
|
13766
13807
|
pathParseCache = /* @__PURE__ */ new Map();
|
|
13808
|
+
TEMPLATE_CACHE_MAX = 256;
|
|
13767
13809
|
TOKEN_RE = /(\{%-?\s*[\s\S]*?\s*-?%\})|(\{\{-?\s*[\s\S]*?\s*-?\}\})|(\{#[\s\S]*?#\})/g;
|
|
13768
13810
|
RAW_BLOCK_RE = /\{%-?\s*raw\s*-?%\}([\s\S]*?)\{%-?\s*endraw\s*-?%\}/g;
|
|
13769
13811
|
VarRef = class {
|
|
@@ -14169,6 +14211,7 @@ var init_engine = __esm({
|
|
|
14169
14211
|
const source = readFileSync6(filePath, "utf-8");
|
|
14170
14212
|
const mtime = statSync6(filePath).mtimeMs;
|
|
14171
14213
|
const tokens = tokenize(source);
|
|
14214
|
+
capCache(this.compiled, TEMPLATE_CACHE_MAX);
|
|
14172
14215
|
this.compiled.set(template, { tokens, mtime, cachedAt: Date.now() });
|
|
14173
14216
|
return this.executeWithSource(source, tokens, context);
|
|
14174
14217
|
}
|
|
@@ -14183,6 +14226,7 @@ var init_engine = __esm({
|
|
|
14183
14226
|
}
|
|
14184
14227
|
}
|
|
14185
14228
|
const tokens = tokenize(source);
|
|
14229
|
+
capCache(this.compiledStrings, TEMPLATE_CACHE_MAX);
|
|
14186
14230
|
this.compiledStrings.set(key, { tokens, cachedAt: Date.now() });
|
|
14187
14231
|
return this.executeCached(tokens, context);
|
|
14188
14232
|
}
|
|
@@ -14398,6 +14442,9 @@ var init_engine = __esm({
|
|
|
14398
14442
|
} else if (tag === "macro") {
|
|
14399
14443
|
const skip = this.handleMacro(tokens, i, context);
|
|
14400
14444
|
i = skip;
|
|
14445
|
+
} else if (tag === "import") {
|
|
14446
|
+
this.handleImportAs(content, context);
|
|
14447
|
+
i++;
|
|
14401
14448
|
} else if (tag === "from") {
|
|
14402
14449
|
this.handleFromImport(content, context);
|
|
14403
14450
|
i++;
|
|
@@ -14964,7 +15011,7 @@ var init_engine = __esm({
|
|
|
14964
15011
|
return i2;
|
|
14965
15012
|
}
|
|
14966
15013
|
const macroName = m[1];
|
|
14967
|
-
const
|
|
15014
|
+
const params = _Frond.parseMacroParams(m[2]);
|
|
14968
15015
|
const bodyTokens = [];
|
|
14969
15016
|
let i = start2 + 1;
|
|
14970
15017
|
while (i < tokens.length) {
|
|
@@ -14979,13 +15026,94 @@ var init_engine = __esm({
|
|
|
14979
15026
|
const capturedContext = { ...context };
|
|
14980
15027
|
context[macroName] = (...args) => {
|
|
14981
15028
|
const macroCtx = { ...capturedContext };
|
|
14982
|
-
for (let pi = 0; pi <
|
|
14983
|
-
|
|
15029
|
+
for (let pi = 0; pi < params.length; pi++) {
|
|
15030
|
+
const [pname, pdefault] = params[pi];
|
|
15031
|
+
macroCtx[pname] = pi < args.length ? args[pi] : pdefault;
|
|
14984
15032
|
}
|
|
14985
15033
|
return new SafeString(engine.renderTokens([...bodyTokens], macroCtx));
|
|
14986
15034
|
};
|
|
14987
15035
|
return i;
|
|
14988
15036
|
}
|
|
15037
|
+
/**
|
|
15038
|
+
* Parse a macro parameter list into [name, default] pairs.
|
|
15039
|
+
*
|
|
15040
|
+
* Handles: name, name="default", name='default'. Splitting on "," alone left a
|
|
15041
|
+
* defaulted parameter literally NAMED `greeting='Hello'`, so the body's
|
|
15042
|
+
* {{ greeting }} matched nothing (rendered empty) AND the caller's positional
|
|
15043
|
+
* argument was stored under that junk key and lost. Mirrors the Python master's
|
|
15044
|
+
* _parse_macro_params. The default is null when none is declared.
|
|
15045
|
+
*/
|
|
15046
|
+
static parseMacroParams(rawParams) {
|
|
15047
|
+
return rawParams.split(",").map((p) => p.trim()).filter(Boolean).map((p) => {
|
|
15048
|
+
const eq = p.indexOf("=");
|
|
15049
|
+
if (eq === -1) return [p, null];
|
|
15050
|
+
const name = p.slice(0, eq).trim();
|
|
15051
|
+
let dflt = p.slice(eq + 1).trim();
|
|
15052
|
+
if (dflt.length >= 2 && (dflt.startsWith('"') && dflt.endsWith('"') || dflt.startsWith("'") && dflt.endsWith("'"))) {
|
|
15053
|
+
dflt = dflt.slice(1, -1);
|
|
15054
|
+
}
|
|
15055
|
+
return [name, dflt];
|
|
15056
|
+
});
|
|
15057
|
+
}
|
|
15058
|
+
/**
|
|
15059
|
+
* {% import "file" as alias %} -- load EVERY macro in a file under one namespace.
|
|
15060
|
+
*
|
|
15061
|
+
* The alias is bound as a plain object of macro functions, so {{ alias.greet(x) }}
|
|
15062
|
+
* resolves through the engine's existing dotted-call path and each macro keeps the
|
|
15063
|
+
* same argument binding, default handling and SafeString output as any other macro.
|
|
15064
|
+
* A namespace object (not a class) is deliberate: a function stored as a class
|
|
15065
|
+
* attribute binds as a method and would inject the namespace as the first argument,
|
|
15066
|
+
* which is exactly the argument-shift bug the Python master carried (fixed there
|
|
15067
|
+
* with types.SimpleNamespace). Both import forms must render identically.
|
|
15068
|
+
*/
|
|
15069
|
+
handleImportAs(content, context) {
|
|
15070
|
+
const m = content.match(/^import\s+["'](.+?)["']\s+as\s+(\w+)/);
|
|
15071
|
+
if (!m) return;
|
|
15072
|
+
const filename = m[1];
|
|
15073
|
+
const alias = m[2];
|
|
15074
|
+
const namespace = {};
|
|
15075
|
+
const source = this.load(filename);
|
|
15076
|
+
const tokens = tokenize(source);
|
|
15077
|
+
let i = 0;
|
|
15078
|
+
while (i < tokens.length) {
|
|
15079
|
+
const [ttype, raw] = tokens[i];
|
|
15080
|
+
if (ttype === "BLOCK") {
|
|
15081
|
+
const [tagContent] = stripTag(raw);
|
|
15082
|
+
if ((tagContent.split(/\s+/)[0] || "") === "macro") {
|
|
15083
|
+
const macroM = tagContent.match(/^macro\s+(\w+)\s*\(([^)]*)\)/);
|
|
15084
|
+
if (macroM) {
|
|
15085
|
+
const macroName = macroM[1];
|
|
15086
|
+
const params = _Frond.parseMacroParams(macroM[2]);
|
|
15087
|
+
const bodyTokens = [];
|
|
15088
|
+
i++;
|
|
15089
|
+
while (i < tokens.length) {
|
|
15090
|
+
if (tokens[i][0] === "BLOCK" && tokens[i][1].includes("endmacro")) {
|
|
15091
|
+
i++;
|
|
15092
|
+
break;
|
|
15093
|
+
}
|
|
15094
|
+
bodyTokens.push(tokens[i]);
|
|
15095
|
+
i++;
|
|
15096
|
+
}
|
|
15097
|
+
const capturedBody = [...bodyTokens];
|
|
15098
|
+
const capturedParams = [...params];
|
|
15099
|
+
const capturedCtx = { ...context };
|
|
15100
|
+
const engine = this;
|
|
15101
|
+
namespace[macroName] = (...args) => {
|
|
15102
|
+
const macroCtx = { ...capturedCtx };
|
|
15103
|
+
for (let pi = 0; pi < capturedParams.length; pi++) {
|
|
15104
|
+
const [pname, pdefault] = capturedParams[pi];
|
|
15105
|
+
macroCtx[pname] = pi < args.length ? args[pi] : pdefault;
|
|
15106
|
+
}
|
|
15107
|
+
return new SafeString(engine.renderTokens([...capturedBody], macroCtx));
|
|
15108
|
+
};
|
|
15109
|
+
continue;
|
|
15110
|
+
}
|
|
15111
|
+
}
|
|
15112
|
+
}
|
|
15113
|
+
i++;
|
|
15114
|
+
}
|
|
15115
|
+
context[alias] = namespace;
|
|
15116
|
+
}
|
|
14989
15117
|
handleFromImport(content, context) {
|
|
14990
15118
|
const m = content.match(/^from\s+["'](.+?)["']\s+import\s+(.+)/);
|
|
14991
15119
|
if (!m) return;
|
|
@@ -15003,7 +15131,7 @@ var init_engine = __esm({
|
|
|
15003
15131
|
const macroM = tagContent.match(/^macro\s+(\w+)\s*\(([^)]*)\)/);
|
|
15004
15132
|
if (macroM && names.includes(macroM[1])) {
|
|
15005
15133
|
const macroName = macroM[1];
|
|
15006
|
-
const paramNames = macroM[2]
|
|
15134
|
+
const paramNames = _Frond.parseMacroParams(macroM[2]);
|
|
15007
15135
|
const bodyTokens = [];
|
|
15008
15136
|
i++;
|
|
15009
15137
|
while (i < tokens.length) {
|
|
@@ -15021,7 +15149,8 @@ var init_engine = __esm({
|
|
|
15021
15149
|
context[macroName] = (...args) => {
|
|
15022
15150
|
const macroCtx = { ...capturedCtx };
|
|
15023
15151
|
for (let pi = 0; pi < capturedParams.length; pi++) {
|
|
15024
|
-
|
|
15152
|
+
const [pname, pdefault] = capturedParams[pi];
|
|
15153
|
+
macroCtx[pname] = pi < args.length ? args[pi] : pdefault;
|
|
15025
15154
|
}
|
|
15026
15155
|
return new SafeString(engine.renderTokens([...capturedBody], macroCtx));
|
|
15027
15156
|
};
|
|
@@ -18128,7 +18257,14 @@ function fullAnalysis(root = "src") {
|
|
|
18128
18257
|
total_functions: allFunctions.length,
|
|
18129
18258
|
avg_complexity: Math.round(avgCC * 100) / 100,
|
|
18130
18259
|
avg_maintainability: Math.round(avgMI * 10) / 10,
|
|
18260
|
+
// Display-only: the top-15 for the "most complex functions" report.
|
|
18261
|
+
// Do NOT source offenders / --fail-on from this — capping here silently
|
|
18262
|
+
// hides the 16th+ over-threshold function from the gate. offenders()
|
|
18263
|
+
// reads "all_functions" (below) instead.
|
|
18131
18264
|
most_complex_functions: allFunctions.slice(0, 15),
|
|
18265
|
+
// Full, uncapped, complexity-sorted list — offenders()/--fail-on use this
|
|
18266
|
+
// so no function over the complexity threshold ever escapes the gate.
|
|
18267
|
+
all_functions: allFunctions,
|
|
18132
18268
|
file_metrics: fileMetrics,
|
|
18133
18269
|
violations,
|
|
18134
18270
|
dependency_graph: importGraph,
|
|
@@ -1183,7 +1183,14 @@ export function fullAnalysis(root: string = "src"): Record<string, any> {
|
|
|
1183
1183
|
total_functions: allFunctions.length,
|
|
1184
1184
|
avg_complexity: Math.round(avgCC * 100) / 100,
|
|
1185
1185
|
avg_maintainability: Math.round(avgMI * 10) / 10,
|
|
1186
|
+
// Display-only: the top-15 for the "most complex functions" report.
|
|
1187
|
+
// Do NOT source offenders / --fail-on from this — capping here silently
|
|
1188
|
+
// hides the 16th+ over-threshold function from the gate. offenders()
|
|
1189
|
+
// reads "all_functions" (below) instead.
|
|
1186
1190
|
most_complex_functions: allFunctions.slice(0, 15),
|
|
1191
|
+
// Full, uncapped, complexity-sorted list — offenders()/--fail-on use this
|
|
1192
|
+
// so no function over the complexity threshold ever escapes the gate.
|
|
1193
|
+
all_functions: allFunctions,
|
|
1187
1194
|
file_metrics: fileMetrics,
|
|
1188
1195
|
violations,
|
|
1189
1196
|
dependency_graph: importGraph,
|
|
@@ -1243,8 +1250,10 @@ export function offenders(root: string = "src", top: number = 20): OffendersResu
|
|
|
1243
1250
|
|
|
1244
1251
|
const items: Offender[] = [];
|
|
1245
1252
|
|
|
1246
|
-
// Function-level: cyclomatic complexity.
|
|
1247
|
-
|
|
1253
|
+
// Function-level: cyclomatic complexity. Use the FULL function list (not the
|
|
1254
|
+
// display-capped most_complex_functions[:15]) so a 16th+ over-threshold
|
|
1255
|
+
// function is never silently dropped from the offenders list or --fail-on.
|
|
1256
|
+
for (const fn of analysis.all_functions || analysis.most_complex_functions || []) {
|
|
1248
1257
|
const cc: number = fn.complexity;
|
|
1249
1258
|
if (cc > 10) {
|
|
1250
1259
|
items.push({
|