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
|
@@ -3415,6 +3415,7 @@ var init_request = __esm({
|
|
|
3415
3415
|
var engine_exports = {};
|
|
3416
3416
|
__export(engine_exports, {
|
|
3417
3417
|
Frond: () => Frond,
|
|
3418
|
+
TEMPLATE_CACHE_MAX: () => TEMPLATE_CACHE_MAX,
|
|
3418
3419
|
setFormTokenSessionId: () => setFormTokenSessionId
|
|
3419
3420
|
});
|
|
3420
3421
|
import { createHash as createHash2, createHmac as createHmac2, randomBytes as randomBytes2 } from "node:crypto";
|
|
@@ -3490,6 +3491,14 @@ function renderDump(value) {
|
|
|
3490
3491
|
function liveAttr(value) {
|
|
3491
3492
|
return String(value).replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
3492
3493
|
}
|
|
3494
|
+
function capCache(cache, maxEntries) {
|
|
3495
|
+
if (cache.size < maxEntries) return;
|
|
3496
|
+
let drop = Math.floor(maxEntries / 2);
|
|
3497
|
+
for (const key of cache.keys()) {
|
|
3498
|
+
cache.delete(key);
|
|
3499
|
+
if (--drop <= 0) break;
|
|
3500
|
+
}
|
|
3501
|
+
}
|
|
3493
3502
|
function tokenize(source) {
|
|
3494
3503
|
const rawBlocks = [];
|
|
3495
3504
|
source = source.replace(RAW_BLOCK_RE, (_match, content) => {
|
|
@@ -3841,6 +3850,9 @@ function evalExpr(expr, context) {
|
|
|
3841
3850
|
}).join("");
|
|
3842
3851
|
}
|
|
3843
3852
|
}
|
|
3853
|
+
if (expr.startsWith("not ")) {
|
|
3854
|
+
return evalComparison(expr, context);
|
|
3855
|
+
}
|
|
3844
3856
|
for (const op of [" not in ", " in ", " is not ", " is ", "!=", "==", ">=", "<=", ">", "<", " and ", " or ", " not "]) {
|
|
3845
3857
|
if (findOutsideQuotes(expr, op) >= 0) {
|
|
3846
3858
|
return evalComparison(expr, context);
|
|
@@ -4396,7 +4408,7 @@ function _generateFormToken(descriptor = "") {
|
|
|
4396
4408
|
function _generateFormTokenValue(descriptor = "") {
|
|
4397
4409
|
return new SafeString(_buildFormTokenJwt(descriptor));
|
|
4398
4410
|
}
|
|
4399
|
-
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;
|
|
4411
|
+
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;
|
|
4400
4412
|
var init_engine = __esm({
|
|
4401
4413
|
"../frond/src/engine.ts"() {
|
|
4402
4414
|
"use strict";
|
|
@@ -4429,6 +4441,7 @@ var init_engine = __esm({
|
|
|
4429
4441
|
LIVE_SRC_RE = /src\s+["']([^"']+)["']/;
|
|
4430
4442
|
filterChainCache = /* @__PURE__ */ new Map();
|
|
4431
4443
|
pathParseCache = /* @__PURE__ */ new Map();
|
|
4444
|
+
TEMPLATE_CACHE_MAX = 256;
|
|
4432
4445
|
TOKEN_RE = /(\{%-?\s*[\s\S]*?\s*-?%\})|(\{\{-?\s*[\s\S]*?\s*-?\}\})|(\{#[\s\S]*?#\})/g;
|
|
4433
4446
|
RAW_BLOCK_RE = /\{%-?\s*raw\s*-?%\}([\s\S]*?)\{%-?\s*endraw\s*-?%\}/g;
|
|
4434
4447
|
VarRef = class {
|
|
@@ -4834,6 +4847,7 @@ var init_engine = __esm({
|
|
|
4834
4847
|
const source = readFileSync4(filePath, "utf-8");
|
|
4835
4848
|
const mtime = statSync4(filePath).mtimeMs;
|
|
4836
4849
|
const tokens = tokenize(source);
|
|
4850
|
+
capCache(this.compiled, TEMPLATE_CACHE_MAX);
|
|
4837
4851
|
this.compiled.set(template, { tokens, mtime, cachedAt: Date.now() });
|
|
4838
4852
|
return this.executeWithSource(source, tokens, context);
|
|
4839
4853
|
}
|
|
@@ -4848,6 +4862,7 @@ var init_engine = __esm({
|
|
|
4848
4862
|
}
|
|
4849
4863
|
}
|
|
4850
4864
|
const tokens = tokenize(source);
|
|
4865
|
+
capCache(this.compiledStrings, TEMPLATE_CACHE_MAX);
|
|
4851
4866
|
this.compiledStrings.set(key, { tokens, cachedAt: Date.now() });
|
|
4852
4867
|
return this.executeCached(tokens, context);
|
|
4853
4868
|
}
|
|
@@ -5063,6 +5078,9 @@ var init_engine = __esm({
|
|
|
5063
5078
|
} else if (tag === "macro") {
|
|
5064
5079
|
const skip = this.handleMacro(tokens, i, context);
|
|
5065
5080
|
i = skip;
|
|
5081
|
+
} else if (tag === "import") {
|
|
5082
|
+
this.handleImportAs(content, context);
|
|
5083
|
+
i++;
|
|
5066
5084
|
} else if (tag === "from") {
|
|
5067
5085
|
this.handleFromImport(content, context);
|
|
5068
5086
|
i++;
|
|
@@ -5629,7 +5647,7 @@ var init_engine = __esm({
|
|
|
5629
5647
|
return i2;
|
|
5630
5648
|
}
|
|
5631
5649
|
const macroName = m[1];
|
|
5632
|
-
const
|
|
5650
|
+
const params = _Frond.parseMacroParams(m[2]);
|
|
5633
5651
|
const bodyTokens = [];
|
|
5634
5652
|
let i = start2 + 1;
|
|
5635
5653
|
while (i < tokens.length) {
|
|
@@ -5644,13 +5662,94 @@ var init_engine = __esm({
|
|
|
5644
5662
|
const capturedContext = { ...context };
|
|
5645
5663
|
context[macroName] = (...args) => {
|
|
5646
5664
|
const macroCtx = { ...capturedContext };
|
|
5647
|
-
for (let pi = 0; pi <
|
|
5648
|
-
|
|
5665
|
+
for (let pi = 0; pi < params.length; pi++) {
|
|
5666
|
+
const [pname, pdefault] = params[pi];
|
|
5667
|
+
macroCtx[pname] = pi < args.length ? args[pi] : pdefault;
|
|
5649
5668
|
}
|
|
5650
5669
|
return new SafeString(engine.renderTokens([...bodyTokens], macroCtx));
|
|
5651
5670
|
};
|
|
5652
5671
|
return i;
|
|
5653
5672
|
}
|
|
5673
|
+
/**
|
|
5674
|
+
* Parse a macro parameter list into [name, default] pairs.
|
|
5675
|
+
*
|
|
5676
|
+
* Handles: name, name="default", name='default'. Splitting on "," alone left a
|
|
5677
|
+
* defaulted parameter literally NAMED `greeting='Hello'`, so the body's
|
|
5678
|
+
* {{ greeting }} matched nothing (rendered empty) AND the caller's positional
|
|
5679
|
+
* argument was stored under that junk key and lost. Mirrors the Python master's
|
|
5680
|
+
* _parse_macro_params. The default is null when none is declared.
|
|
5681
|
+
*/
|
|
5682
|
+
static parseMacroParams(rawParams) {
|
|
5683
|
+
return rawParams.split(",").map((p) => p.trim()).filter(Boolean).map((p) => {
|
|
5684
|
+
const eq = p.indexOf("=");
|
|
5685
|
+
if (eq === -1) return [p, null];
|
|
5686
|
+
const name = p.slice(0, eq).trim();
|
|
5687
|
+
let dflt = p.slice(eq + 1).trim();
|
|
5688
|
+
if (dflt.length >= 2 && (dflt.startsWith('"') && dflt.endsWith('"') || dflt.startsWith("'") && dflt.endsWith("'"))) {
|
|
5689
|
+
dflt = dflt.slice(1, -1);
|
|
5690
|
+
}
|
|
5691
|
+
return [name, dflt];
|
|
5692
|
+
});
|
|
5693
|
+
}
|
|
5694
|
+
/**
|
|
5695
|
+
* {% import "file" as alias %} -- load EVERY macro in a file under one namespace.
|
|
5696
|
+
*
|
|
5697
|
+
* The alias is bound as a plain object of macro functions, so {{ alias.greet(x) }}
|
|
5698
|
+
* resolves through the engine's existing dotted-call path and each macro keeps the
|
|
5699
|
+
* same argument binding, default handling and SafeString output as any other macro.
|
|
5700
|
+
* A namespace object (not a class) is deliberate: a function stored as a class
|
|
5701
|
+
* attribute binds as a method and would inject the namespace as the first argument,
|
|
5702
|
+
* which is exactly the argument-shift bug the Python master carried (fixed there
|
|
5703
|
+
* with types.SimpleNamespace). Both import forms must render identically.
|
|
5704
|
+
*/
|
|
5705
|
+
handleImportAs(content, context) {
|
|
5706
|
+
const m = content.match(/^import\s+["'](.+?)["']\s+as\s+(\w+)/);
|
|
5707
|
+
if (!m) return;
|
|
5708
|
+
const filename = m[1];
|
|
5709
|
+
const alias = m[2];
|
|
5710
|
+
const namespace = {};
|
|
5711
|
+
const source = this.load(filename);
|
|
5712
|
+
const tokens = tokenize(source);
|
|
5713
|
+
let i = 0;
|
|
5714
|
+
while (i < tokens.length) {
|
|
5715
|
+
const [ttype, raw] = tokens[i];
|
|
5716
|
+
if (ttype === "BLOCK") {
|
|
5717
|
+
const [tagContent] = stripTag(raw);
|
|
5718
|
+
if ((tagContent.split(/\s+/)[0] || "") === "macro") {
|
|
5719
|
+
const macroM = tagContent.match(/^macro\s+(\w+)\s*\(([^)]*)\)/);
|
|
5720
|
+
if (macroM) {
|
|
5721
|
+
const macroName = macroM[1];
|
|
5722
|
+
const params = _Frond.parseMacroParams(macroM[2]);
|
|
5723
|
+
const bodyTokens = [];
|
|
5724
|
+
i++;
|
|
5725
|
+
while (i < tokens.length) {
|
|
5726
|
+
if (tokens[i][0] === "BLOCK" && tokens[i][1].includes("endmacro")) {
|
|
5727
|
+
i++;
|
|
5728
|
+
break;
|
|
5729
|
+
}
|
|
5730
|
+
bodyTokens.push(tokens[i]);
|
|
5731
|
+
i++;
|
|
5732
|
+
}
|
|
5733
|
+
const capturedBody = [...bodyTokens];
|
|
5734
|
+
const capturedParams = [...params];
|
|
5735
|
+
const capturedCtx = { ...context };
|
|
5736
|
+
const engine = this;
|
|
5737
|
+
namespace[macroName] = (...args) => {
|
|
5738
|
+
const macroCtx = { ...capturedCtx };
|
|
5739
|
+
for (let pi = 0; pi < capturedParams.length; pi++) {
|
|
5740
|
+
const [pname, pdefault] = capturedParams[pi];
|
|
5741
|
+
macroCtx[pname] = pi < args.length ? args[pi] : pdefault;
|
|
5742
|
+
}
|
|
5743
|
+
return new SafeString(engine.renderTokens([...capturedBody], macroCtx));
|
|
5744
|
+
};
|
|
5745
|
+
continue;
|
|
5746
|
+
}
|
|
5747
|
+
}
|
|
5748
|
+
}
|
|
5749
|
+
i++;
|
|
5750
|
+
}
|
|
5751
|
+
context[alias] = namespace;
|
|
5752
|
+
}
|
|
5654
5753
|
handleFromImport(content, context) {
|
|
5655
5754
|
const m = content.match(/^from\s+["'](.+?)["']\s+import\s+(.+)/);
|
|
5656
5755
|
if (!m) return;
|
|
@@ -5668,7 +5767,7 @@ var init_engine = __esm({
|
|
|
5668
5767
|
const macroM = tagContent.match(/^macro\s+(\w+)\s*\(([^)]*)\)/);
|
|
5669
5768
|
if (macroM && names.includes(macroM[1])) {
|
|
5670
5769
|
const macroName = macroM[1];
|
|
5671
|
-
const paramNames = macroM[2]
|
|
5770
|
+
const paramNames = _Frond.parseMacroParams(macroM[2]);
|
|
5672
5771
|
const bodyTokens = [];
|
|
5673
5772
|
i++;
|
|
5674
5773
|
while (i < tokens.length) {
|
|
@@ -5686,7 +5785,8 @@ var init_engine = __esm({
|
|
|
5686
5785
|
context[macroName] = (...args) => {
|
|
5687
5786
|
const macroCtx = { ...capturedCtx };
|
|
5688
5787
|
for (let pi = 0; pi < capturedParams.length; pi++) {
|
|
5689
|
-
|
|
5788
|
+
const [pname, pdefault] = capturedParams[pi];
|
|
5789
|
+
macroCtx[pname] = pi < args.length ? args[pi] : pdefault;
|
|
5690
5790
|
}
|
|
5691
5791
|
return new SafeString(engine.renderTokens([...capturedBody], macroCtx));
|
|
5692
5792
|
};
|
|
@@ -8793,7 +8893,14 @@ function fullAnalysis(root = "src") {
|
|
|
8793
8893
|
total_functions: allFunctions.length,
|
|
8794
8894
|
avg_complexity: Math.round(avgCC * 100) / 100,
|
|
8795
8895
|
avg_maintainability: Math.round(avgMI * 10) / 10,
|
|
8896
|
+
// Display-only: the top-15 for the "most complex functions" report.
|
|
8897
|
+
// Do NOT source offenders / --fail-on from this — capping here silently
|
|
8898
|
+
// hides the 16th+ over-threshold function from the gate. offenders()
|
|
8899
|
+
// reads "all_functions" (below) instead.
|
|
8796
8900
|
most_complex_functions: allFunctions.slice(0, 15),
|
|
8901
|
+
// Full, uncapped, complexity-sorted list — offenders()/--fail-on use this
|
|
8902
|
+
// so no function over the complexity threshold ever escapes the gate.
|
|
8903
|
+
all_functions: allFunctions,
|
|
8797
8904
|
file_metrics: fileMetrics,
|
|
8798
8905
|
violations,
|
|
8799
8906
|
dependency_graph: importGraph,
|
|
@@ -29953,18 +30060,21 @@ var init_postgres = __esm({
|
|
|
29953
30060
|
}
|
|
29954
30061
|
async startTransactionAsync() {
|
|
29955
30062
|
await this.executeAsync("BEGIN");
|
|
30063
|
+
this._inTransaction = true;
|
|
29956
30064
|
}
|
|
29957
30065
|
commit() {
|
|
29958
30066
|
throw new Error("Use commitAsync() for PostgreSQL.");
|
|
29959
30067
|
}
|
|
29960
30068
|
async commitAsync() {
|
|
29961
30069
|
await this.executeAsync("COMMIT");
|
|
30070
|
+
this._inTransaction = false;
|
|
29962
30071
|
}
|
|
29963
30072
|
rollback() {
|
|
29964
30073
|
throw new Error("Use rollbackAsync() for PostgreSQL.");
|
|
29965
30074
|
}
|
|
29966
30075
|
async rollbackAsync() {
|
|
29967
30076
|
await this.executeAsync("ROLLBACK");
|
|
30077
|
+
this._inTransaction = false;
|
|
29968
30078
|
}
|
|
29969
30079
|
tables() {
|
|
29970
30080
|
throw new Error("Use tablesAsync() for PostgreSQL.");
|
|
@@ -30983,10 +31093,35 @@ var init_firebird = __esm({
|
|
|
30983
31093
|
translated = SQLTranslator.ilikeToLike(translated);
|
|
30984
31094
|
return translated;
|
|
30985
31095
|
}
|
|
31096
|
+
/**
|
|
31097
|
+
* The handle every statement runs on. While an explicit transaction is open
|
|
31098
|
+
* (startTransactionAsync set `this.transaction`), statements MUST run on that
|
|
31099
|
+
* transaction object so they are undone by rollbackAsync() / persisted by
|
|
31100
|
+
* commitAsync() — node-firebird's transaction exposes the same
|
|
31101
|
+
* query()/execute() as the connection. With no transaction open we run on
|
|
31102
|
+
* `this.db`, whose per-statement work auto-commits on the connection.
|
|
31103
|
+
*
|
|
31104
|
+
* This matches the Python master's contract (tina4_python/database/firebird.py):
|
|
31105
|
+
* there, ALL statements run on the single connection and start_transaction()
|
|
31106
|
+
* merely suppresses the per-statement autocommit in execute() so the batch
|
|
31107
|
+
* stays open until commit()/rollback(). node-firebird has no such suppression
|
|
31108
|
+
* hook — its `db.query/execute` always auto-commit — so the equivalent is to
|
|
31109
|
+
* route statements through the transaction object instead. Same observable
|
|
31110
|
+
* behaviour: an open transaction is atomic and rolls back cleanly.
|
|
31111
|
+
*
|
|
31112
|
+
* Previously every statement ran on `this.db` unconditionally, so the
|
|
31113
|
+
* transaction created by startTransactionAsync() never saw a single statement
|
|
31114
|
+
* — rollbackAsync() rolled back an EMPTY transaction and the already
|
|
31115
|
+
* auto-committed write survived (silent no-op). Twin of the PHP pdo_firebird
|
|
31116
|
+
* bug fixed in 3.13.86.
|
|
31117
|
+
*/
|
|
31118
|
+
statementHandle() {
|
|
31119
|
+
return this.transaction ?? this.db;
|
|
31120
|
+
}
|
|
30986
31121
|
queryPromise(sql, params) {
|
|
30987
31122
|
return new Promise((resolve21, reject) => {
|
|
30988
31123
|
const translated = this.translateSql(sql);
|
|
30989
|
-
this.
|
|
31124
|
+
this.statementHandle().query(translated, params ?? [], (err, result) => {
|
|
30990
31125
|
if (err) reject(err);
|
|
30991
31126
|
else resolve21(result ?? []);
|
|
30992
31127
|
});
|
|
@@ -30995,7 +31130,7 @@ var init_firebird = __esm({
|
|
|
30995
31130
|
executePromise(sql, params) {
|
|
30996
31131
|
return new Promise((resolve21, reject) => {
|
|
30997
31132
|
const translated = this.translateSql(sql);
|
|
30998
|
-
this.
|
|
31133
|
+
this.statementHandle().execute(translated, params ?? [], (err) => {
|
|
30999
31134
|
if (err) reject(err);
|
|
31000
31135
|
else resolve21();
|
|
31001
31136
|
});
|
|
@@ -32969,14 +33104,15 @@ var init_database = __esm({
|
|
|
32969
33104
|
async executeMany(sql, paramSets = []) {
|
|
32970
33105
|
const adapter = this.getNextAdapter();
|
|
32971
33106
|
const results = [];
|
|
32972
|
-
|
|
33107
|
+
const owns = !this.inExplicitTransaction();
|
|
33108
|
+
if (owns) await adapterStartTransaction(adapter);
|
|
32973
33109
|
try {
|
|
32974
33110
|
for (const params of paramSets) {
|
|
32975
33111
|
results.push(await adapterExecute(adapter, sql, params));
|
|
32976
33112
|
}
|
|
32977
|
-
await adapterCommit(adapter);
|
|
33113
|
+
if (owns) await adapterCommit(adapter);
|
|
32978
33114
|
} catch (e) {
|
|
32979
|
-
await adapterRollback(adapter);
|
|
33115
|
+
if (owns) await adapterRollback(adapter);
|
|
32980
33116
|
throw e;
|
|
32981
33117
|
}
|
|
32982
33118
|
return results;
|
|
@@ -236,10 +236,36 @@ export class FirebirdAdapter implements DatabaseAdapter {
|
|
|
236
236
|
return translated;
|
|
237
237
|
}
|
|
238
238
|
|
|
239
|
+
/**
|
|
240
|
+
* The handle every statement runs on. While an explicit transaction is open
|
|
241
|
+
* (startTransactionAsync set `this.transaction`), statements MUST run on that
|
|
242
|
+
* transaction object so they are undone by rollbackAsync() / persisted by
|
|
243
|
+
* commitAsync() — node-firebird's transaction exposes the same
|
|
244
|
+
* query()/execute() as the connection. With no transaction open we run on
|
|
245
|
+
* `this.db`, whose per-statement work auto-commits on the connection.
|
|
246
|
+
*
|
|
247
|
+
* This matches the Python master's contract (tina4_python/database/firebird.py):
|
|
248
|
+
* there, ALL statements run on the single connection and start_transaction()
|
|
249
|
+
* merely suppresses the per-statement autocommit in execute() so the batch
|
|
250
|
+
* stays open until commit()/rollback(). node-firebird has no such suppression
|
|
251
|
+
* hook — its `db.query/execute` always auto-commit — so the equivalent is to
|
|
252
|
+
* route statements through the transaction object instead. Same observable
|
|
253
|
+
* behaviour: an open transaction is atomic and rolls back cleanly.
|
|
254
|
+
*
|
|
255
|
+
* Previously every statement ran on `this.db` unconditionally, so the
|
|
256
|
+
* transaction created by startTransactionAsync() never saw a single statement
|
|
257
|
+
* — rollbackAsync() rolled back an EMPTY transaction and the already
|
|
258
|
+
* auto-committed write survived (silent no-op). Twin of the PHP pdo_firebird
|
|
259
|
+
* bug fixed in 3.13.86.
|
|
260
|
+
*/
|
|
261
|
+
private statementHandle(): any {
|
|
262
|
+
return this.transaction ?? this.db;
|
|
263
|
+
}
|
|
264
|
+
|
|
239
265
|
private queryPromise(sql: string, params?: unknown[]): Promise<any[]> {
|
|
240
266
|
return new Promise((resolve, reject) => {
|
|
241
267
|
const translated = this.translateSql(sql);
|
|
242
|
-
this.
|
|
268
|
+
this.statementHandle().query(translated, params ?? [], (err: Error | null, result: any[]) => {
|
|
243
269
|
if (err) reject(err);
|
|
244
270
|
else resolve(result ?? []);
|
|
245
271
|
});
|
|
@@ -249,7 +275,7 @@ export class FirebirdAdapter implements DatabaseAdapter {
|
|
|
249
275
|
private executePromise(sql: string, params?: unknown[]): Promise<void> {
|
|
250
276
|
return new Promise((resolve, reject) => {
|
|
251
277
|
const translated = this.translateSql(sql);
|
|
252
|
-
this.
|
|
278
|
+
this.statementHandle().execute(translated, params ?? [], (err: Error | null) => {
|
|
253
279
|
if (err) reject(err);
|
|
254
280
|
else resolve();
|
|
255
281
|
});
|
|
@@ -325,6 +325,12 @@ export class PostgresAdapter implements DatabaseAdapter {
|
|
|
325
325
|
|
|
326
326
|
async startTransactionAsync(): Promise<void> {
|
|
327
327
|
await this.executeAsync("BEGIN");
|
|
328
|
+
// Mark the connection as inside an explicit transaction so executeManyAsync's
|
|
329
|
+
// owns-guard (owns = !_inTransaction) joins THIS transaction instead of
|
|
330
|
+
// opening its own inner BEGIN/COMMIT (which would commit this outer
|
|
331
|
+
// transaction early and defeat a later rollback). Mirrors the Python master
|
|
332
|
+
// (tina4_python/database/postgres.py start_transaction -> _in_transaction = True).
|
|
333
|
+
this._inTransaction = true;
|
|
328
334
|
}
|
|
329
335
|
|
|
330
336
|
commit(): void {
|
|
@@ -333,6 +339,9 @@ export class PostgresAdapter implements DatabaseAdapter {
|
|
|
333
339
|
|
|
334
340
|
async commitAsync(): Promise<void> {
|
|
335
341
|
await this.executeAsync("COMMIT");
|
|
342
|
+
// Transaction closed — clear the flag so subsequent standalone batches own
|
|
343
|
+
// their own transaction again (parity with Python master commit()).
|
|
344
|
+
this._inTransaction = false;
|
|
336
345
|
}
|
|
337
346
|
|
|
338
347
|
rollback(): void {
|
|
@@ -341,6 +350,8 @@ export class PostgresAdapter implements DatabaseAdapter {
|
|
|
341
350
|
|
|
342
351
|
async rollbackAsync(): Promise<void> {
|
|
343
352
|
await this.executeAsync("ROLLBACK");
|
|
353
|
+
// Transaction closed — clear the flag (parity with Python master rollback()).
|
|
354
|
+
this._inTransaction = false;
|
|
344
355
|
}
|
|
345
356
|
|
|
346
357
|
tables(): string[] {
|
|
@@ -962,14 +962,26 @@ export class Database {
|
|
|
962
962
|
const adapter = this.getNextAdapter();
|
|
963
963
|
const results: unknown[] = [];
|
|
964
964
|
|
|
965
|
-
|
|
965
|
+
// Own the batch transaction ONLY when not already inside a caller's explicit
|
|
966
|
+
// transaction. inExplicitTransaction() is true when startTransaction() has
|
|
967
|
+
// pinned an adapter to this async context (getNextAdapter() returns that same
|
|
968
|
+
// pinned connection). If we owned a BEGIN/COMMIT here regardless, the inner
|
|
969
|
+
// COMMIT would commit the caller's OUTER transaction early and their later
|
|
970
|
+
// rollback() would undo nothing (the batch rows survive). So: standalone
|
|
971
|
+
// batch -> own BEGIN/COMMIT (atomic, all-or-nothing); nested batch -> join
|
|
972
|
+
// the caller's transaction and let their commit/rollback decide. Mirrors the
|
|
973
|
+
// sibling execute()/insert()/update()/delete() owns-guard, the PostgreSQL
|
|
974
|
+
// adapter's executeManyAsync owns-guard, and the Python master
|
|
975
|
+
// (Database.execute_many delegating to adapter.execute_many's owns_txn guard).
|
|
976
|
+
const owns = !this.inExplicitTransaction();
|
|
977
|
+
if (owns) await adapterStartTransaction(adapter);
|
|
966
978
|
try {
|
|
967
979
|
for (const params of paramSets) {
|
|
968
980
|
results.push(await adapterExecute(adapter, sql, params));
|
|
969
981
|
}
|
|
970
|
-
await adapterCommit(adapter);
|
|
982
|
+
if (owns) await adapterCommit(adapter);
|
|
971
983
|
} catch (e) {
|
|
972
|
-
await adapterRollback(adapter);
|
|
984
|
+
if (owns) await adapterRollback(adapter);
|
|
973
985
|
throw e;
|
|
974
986
|
}
|
|
975
987
|
|