tina4-nodejs 3.13.86 → 3.13.88
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 +185 -16
- package/packages/core/dist/index.js +184 -15
- package/packages/core/src/metrics.ts +11 -2
- package/packages/frond/dist/index.js +141 -9
- package/packages/frond/src/engine.ts +230 -10
- package/packages/orm/dist/index.js +184 -15
- 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,11 +3415,32 @@ 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";
|
|
3421
3422
|
import { readFileSync as readFileSync4, existsSync as existsSync6, statSync as statSync4 } from "node:fs";
|
|
3422
3423
|
import { join as join5, resolve as resolve2 } from "node:path";
|
|
3424
|
+
function jsonText(value) {
|
|
3425
|
+
try {
|
|
3426
|
+
const text = JSON.stringify(value);
|
|
3427
|
+
return text === void 0 ? "null" : text;
|
|
3428
|
+
} catch {
|
|
3429
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
3430
|
+
const text = JSON.stringify(value, (_key, v) => {
|
|
3431
|
+
if (typeof v === "bigint") return v.toString();
|
|
3432
|
+
if (typeof v === "object" && v !== null) {
|
|
3433
|
+
if (seen.has(v)) return null;
|
|
3434
|
+
seen.add(v);
|
|
3435
|
+
}
|
|
3436
|
+
return v;
|
|
3437
|
+
});
|
|
3438
|
+
return text === void 0 ? "null" : text;
|
|
3439
|
+
}
|
|
3440
|
+
}
|
|
3441
|
+
function jsonSafe(value) {
|
|
3442
|
+
return new SafeString(jsonText(value).replace(JSON_UNSAFE_RE, (c) => JSON_UNSAFE_MAP[c]));
|
|
3443
|
+
}
|
|
3423
3444
|
function inspectValue(value, seen = /* @__PURE__ */ new WeakSet(), depth = 0) {
|
|
3424
3445
|
if (value === null) return "null";
|
|
3425
3446
|
if (value === void 0) return "undefined";
|
|
@@ -3490,6 +3511,14 @@ function renderDump(value) {
|
|
|
3490
3511
|
function liveAttr(value) {
|
|
3491
3512
|
return String(value).replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
3492
3513
|
}
|
|
3514
|
+
function capCache(cache, maxEntries) {
|
|
3515
|
+
if (cache.size < maxEntries) return;
|
|
3516
|
+
let drop = Math.floor(maxEntries / 2);
|
|
3517
|
+
for (const key of cache.keys()) {
|
|
3518
|
+
cache.delete(key);
|
|
3519
|
+
if (--drop <= 0) break;
|
|
3520
|
+
}
|
|
3521
|
+
}
|
|
3493
3522
|
function tokenize(source) {
|
|
3494
3523
|
const rawBlocks = [];
|
|
3495
3524
|
source = source.replace(RAW_BLOCK_RE, (_match, content) => {
|
|
@@ -3841,6 +3870,9 @@ function evalExpr(expr, context) {
|
|
|
3841
3870
|
}).join("");
|
|
3842
3871
|
}
|
|
3843
3872
|
}
|
|
3873
|
+
if (expr.startsWith("not ")) {
|
|
3874
|
+
return evalComparison(expr, context);
|
|
3875
|
+
}
|
|
3844
3876
|
for (const op of [" not in ", " in ", " is not ", " is ", "!=", "==", ">=", "<=", ">", "<", " and ", " or ", " not "]) {
|
|
3845
3877
|
if (findOutsideQuotes(expr, op) >= 0) {
|
|
3846
3878
|
return evalComparison(expr, context);
|
|
@@ -4396,7 +4428,7 @@ function _generateFormToken(descriptor = "") {
|
|
|
4396
4428
|
function _generateFormTokenValue(descriptor = "") {
|
|
4397
4429
|
return new SafeString(_buildFormTokenJwt(descriptor));
|
|
4398
4430
|
}
|
|
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;
|
|
4431
|
+
var SafeString, JSON_UNSAFE_RE, JSON_UNSAFE_MAP, 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
4432
|
var init_engine = __esm({
|
|
4401
4433
|
"../frond/src/engine.ts"() {
|
|
4402
4434
|
"use strict";
|
|
@@ -4408,6 +4440,15 @@ var init_engine = __esm({
|
|
|
4408
4440
|
return this.value;
|
|
4409
4441
|
}
|
|
4410
4442
|
};
|
|
4443
|
+
JSON_UNSAFE_RE = /[<>&'\u2028\u2029]/g;
|
|
4444
|
+
JSON_UNSAFE_MAP = {
|
|
4445
|
+
"<": "\\u003c",
|
|
4446
|
+
">": "\\u003e",
|
|
4447
|
+
"&": "\\u0026",
|
|
4448
|
+
"'": "\\u0027",
|
|
4449
|
+
"\u2028": "\\u2028",
|
|
4450
|
+
"\u2029": "\\u2029"
|
|
4451
|
+
};
|
|
4411
4452
|
NUMERIC_RE = /^-?\d+(\.\d+)?$/;
|
|
4412
4453
|
METHOD_CALL_RE = /^(\w+)\s*\(([\s\S]*)?\)$/;
|
|
4413
4454
|
FN_CALL_RE = /^([\w.]+)\s*\(([\s\S]*)?\)$/;
|
|
@@ -4429,6 +4470,7 @@ var init_engine = __esm({
|
|
|
4429
4470
|
LIVE_SRC_RE = /src\s+["']([^"']+)["']/;
|
|
4430
4471
|
filterChainCache = /* @__PURE__ */ new Map();
|
|
4431
4472
|
pathParseCache = /* @__PURE__ */ new Map();
|
|
4473
|
+
TEMPLATE_CACHE_MAX = 256;
|
|
4432
4474
|
TOKEN_RE = /(\{%-?\s*[\s\S]*?\s*-?%\})|(\{\{-?\s*[\s\S]*?\s*-?\}\})|(\{#[\s\S]*?#\})/g;
|
|
4433
4475
|
RAW_BLOCK_RE = /\{%-?\s*raw\s*-?%\}([\s\S]*?)\{%-?\s*endraw\s*-?%\}/g;
|
|
4434
4476
|
VarRef = class {
|
|
@@ -4497,7 +4539,7 @@ var init_engine = __esm({
|
|
|
4497
4539
|
int: (v) => v ? parseInt(String(v), 10) || 0 : 0,
|
|
4498
4540
|
float: (v) => v ? parseFloat(String(v)) || 0 : 0,
|
|
4499
4541
|
string: (v) => String(v),
|
|
4500
|
-
json_encode: (v) =>
|
|
4542
|
+
json_encode: (v) => jsonSafe(v),
|
|
4501
4543
|
json_decode: (v) => typeof v === "string" ? JSON.parse(v) : v,
|
|
4502
4544
|
keys: (v) => typeof v === "object" && v !== null && !Array.isArray(v) ? Object.keys(v) : [],
|
|
4503
4545
|
values: (v) => typeof v === "object" && v !== null && !Array.isArray(v) ? Object.values(v) : [],
|
|
@@ -4642,8 +4684,12 @@ var init_engine = __esm({
|
|
|
4642
4684
|
form_token: (v) => _generateFormToken(v != null ? String(v) : ""),
|
|
4643
4685
|
formTokenValue: (v) => _generateFormTokenValue(v != null ? String(v) : ""),
|
|
4644
4686
|
form_token_value: (v) => _generateFormTokenValue(v != null ? String(v) : ""),
|
|
4645
|
-
|
|
4646
|
-
|
|
4687
|
+
// Same serializer as json_encode -- the three names are one behaviour. The
|
|
4688
|
+
// old indent argument is gone: PHP cannot honour an arbitrary indent
|
|
4689
|
+
// (JSON_PRETTY_PRINT is fixed at four spaces), so honouring it here alone
|
|
4690
|
+
// broke byte-parity for the one filter whose whole job is a wire format.
|
|
4691
|
+
tojson: (v) => jsonSafe(v),
|
|
4692
|
+
to_json: (v) => jsonSafe(v),
|
|
4647
4693
|
js_escape: (v) => new SafeString(String(v).replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t"))
|
|
4648
4694
|
};
|
|
4649
4695
|
_formTokenSessionId = "";
|
|
@@ -4834,6 +4880,7 @@ var init_engine = __esm({
|
|
|
4834
4880
|
const source = readFileSync4(filePath, "utf-8");
|
|
4835
4881
|
const mtime = statSync4(filePath).mtimeMs;
|
|
4836
4882
|
const tokens = tokenize(source);
|
|
4883
|
+
capCache(this.compiled, TEMPLATE_CACHE_MAX);
|
|
4837
4884
|
this.compiled.set(template, { tokens, mtime, cachedAt: Date.now() });
|
|
4838
4885
|
return this.executeWithSource(source, tokens, context);
|
|
4839
4886
|
}
|
|
@@ -4848,6 +4895,7 @@ var init_engine = __esm({
|
|
|
4848
4895
|
}
|
|
4849
4896
|
}
|
|
4850
4897
|
const tokens = tokenize(source);
|
|
4898
|
+
capCache(this.compiledStrings, TEMPLATE_CACHE_MAX);
|
|
4851
4899
|
this.compiledStrings.set(key, { tokens, cachedAt: Date.now() });
|
|
4852
4900
|
return this.executeCached(tokens, context);
|
|
4853
4901
|
}
|
|
@@ -5063,6 +5111,9 @@ var init_engine = __esm({
|
|
|
5063
5111
|
} else if (tag === "macro") {
|
|
5064
5112
|
const skip = this.handleMacro(tokens, i, context);
|
|
5065
5113
|
i = skip;
|
|
5114
|
+
} else if (tag === "import") {
|
|
5115
|
+
this.handleImportAs(content, context);
|
|
5116
|
+
i++;
|
|
5066
5117
|
} else if (tag === "from") {
|
|
5067
5118
|
this.handleFromImport(content, context);
|
|
5068
5119
|
i++;
|
|
@@ -5378,7 +5429,7 @@ var init_engine = __esm({
|
|
|
5378
5429
|
value = typeof value === "object" && value !== null && !Array.isArray(value) ? Object.values(value) : [];
|
|
5379
5430
|
continue;
|
|
5380
5431
|
case "json_encode":
|
|
5381
|
-
value =
|
|
5432
|
+
value = jsonSafe(value);
|
|
5382
5433
|
continue;
|
|
5383
5434
|
case "dump":
|
|
5384
5435
|
value = renderDump(value);
|
|
@@ -5629,7 +5680,7 @@ var init_engine = __esm({
|
|
|
5629
5680
|
return i2;
|
|
5630
5681
|
}
|
|
5631
5682
|
const macroName = m[1];
|
|
5632
|
-
const
|
|
5683
|
+
const params = _Frond.parseMacroParams(m[2]);
|
|
5633
5684
|
const bodyTokens = [];
|
|
5634
5685
|
let i = start2 + 1;
|
|
5635
5686
|
while (i < tokens.length) {
|
|
@@ -5644,13 +5695,94 @@ var init_engine = __esm({
|
|
|
5644
5695
|
const capturedContext = { ...context };
|
|
5645
5696
|
context[macroName] = (...args) => {
|
|
5646
5697
|
const macroCtx = { ...capturedContext };
|
|
5647
|
-
for (let pi = 0; pi <
|
|
5648
|
-
|
|
5698
|
+
for (let pi = 0; pi < params.length; pi++) {
|
|
5699
|
+
const [pname, pdefault] = params[pi];
|
|
5700
|
+
macroCtx[pname] = pi < args.length ? args[pi] : pdefault;
|
|
5649
5701
|
}
|
|
5650
5702
|
return new SafeString(engine.renderTokens([...bodyTokens], macroCtx));
|
|
5651
5703
|
};
|
|
5652
5704
|
return i;
|
|
5653
5705
|
}
|
|
5706
|
+
/**
|
|
5707
|
+
* Parse a macro parameter list into [name, default] pairs.
|
|
5708
|
+
*
|
|
5709
|
+
* Handles: name, name="default", name='default'. Splitting on "," alone left a
|
|
5710
|
+
* defaulted parameter literally NAMED `greeting='Hello'`, so the body's
|
|
5711
|
+
* {{ greeting }} matched nothing (rendered empty) AND the caller's positional
|
|
5712
|
+
* argument was stored under that junk key and lost. Mirrors the Python master's
|
|
5713
|
+
* _parse_macro_params. The default is null when none is declared.
|
|
5714
|
+
*/
|
|
5715
|
+
static parseMacroParams(rawParams) {
|
|
5716
|
+
return rawParams.split(",").map((p) => p.trim()).filter(Boolean).map((p) => {
|
|
5717
|
+
const eq = p.indexOf("=");
|
|
5718
|
+
if (eq === -1) return [p, null];
|
|
5719
|
+
const name = p.slice(0, eq).trim();
|
|
5720
|
+
let dflt = p.slice(eq + 1).trim();
|
|
5721
|
+
if (dflt.length >= 2 && (dflt.startsWith('"') && dflt.endsWith('"') || dflt.startsWith("'") && dflt.endsWith("'"))) {
|
|
5722
|
+
dflt = dflt.slice(1, -1);
|
|
5723
|
+
}
|
|
5724
|
+
return [name, dflt];
|
|
5725
|
+
});
|
|
5726
|
+
}
|
|
5727
|
+
/**
|
|
5728
|
+
* {% import "file" as alias %} -- load EVERY macro in a file under one namespace.
|
|
5729
|
+
*
|
|
5730
|
+
* The alias is bound as a plain object of macro functions, so {{ alias.greet(x) }}
|
|
5731
|
+
* resolves through the engine's existing dotted-call path and each macro keeps the
|
|
5732
|
+
* same argument binding, default handling and SafeString output as any other macro.
|
|
5733
|
+
* A namespace object (not a class) is deliberate: a function stored as a class
|
|
5734
|
+
* attribute binds as a method and would inject the namespace as the first argument,
|
|
5735
|
+
* which is exactly the argument-shift bug the Python master carried (fixed there
|
|
5736
|
+
* with types.SimpleNamespace). Both import forms must render identically.
|
|
5737
|
+
*/
|
|
5738
|
+
handleImportAs(content, context) {
|
|
5739
|
+
const m = content.match(/^import\s+["'](.+?)["']\s+as\s+(\w+)/);
|
|
5740
|
+
if (!m) return;
|
|
5741
|
+
const filename = m[1];
|
|
5742
|
+
const alias = m[2];
|
|
5743
|
+
const namespace = {};
|
|
5744
|
+
const source = this.load(filename);
|
|
5745
|
+
const tokens = tokenize(source);
|
|
5746
|
+
let i = 0;
|
|
5747
|
+
while (i < tokens.length) {
|
|
5748
|
+
const [ttype, raw] = tokens[i];
|
|
5749
|
+
if (ttype === "BLOCK") {
|
|
5750
|
+
const [tagContent] = stripTag(raw);
|
|
5751
|
+
if ((tagContent.split(/\s+/)[0] || "") === "macro") {
|
|
5752
|
+
const macroM = tagContent.match(/^macro\s+(\w+)\s*\(([^)]*)\)/);
|
|
5753
|
+
if (macroM) {
|
|
5754
|
+
const macroName = macroM[1];
|
|
5755
|
+
const params = _Frond.parseMacroParams(macroM[2]);
|
|
5756
|
+
const bodyTokens = [];
|
|
5757
|
+
i++;
|
|
5758
|
+
while (i < tokens.length) {
|
|
5759
|
+
if (tokens[i][0] === "BLOCK" && tokens[i][1].includes("endmacro")) {
|
|
5760
|
+
i++;
|
|
5761
|
+
break;
|
|
5762
|
+
}
|
|
5763
|
+
bodyTokens.push(tokens[i]);
|
|
5764
|
+
i++;
|
|
5765
|
+
}
|
|
5766
|
+
const capturedBody = [...bodyTokens];
|
|
5767
|
+
const capturedParams = [...params];
|
|
5768
|
+
const capturedCtx = { ...context };
|
|
5769
|
+
const engine = this;
|
|
5770
|
+
namespace[macroName] = (...args) => {
|
|
5771
|
+
const macroCtx = { ...capturedCtx };
|
|
5772
|
+
for (let pi = 0; pi < capturedParams.length; pi++) {
|
|
5773
|
+
const [pname, pdefault] = capturedParams[pi];
|
|
5774
|
+
macroCtx[pname] = pi < args.length ? args[pi] : pdefault;
|
|
5775
|
+
}
|
|
5776
|
+
return new SafeString(engine.renderTokens([...capturedBody], macroCtx));
|
|
5777
|
+
};
|
|
5778
|
+
continue;
|
|
5779
|
+
}
|
|
5780
|
+
}
|
|
5781
|
+
}
|
|
5782
|
+
i++;
|
|
5783
|
+
}
|
|
5784
|
+
context[alias] = namespace;
|
|
5785
|
+
}
|
|
5654
5786
|
handleFromImport(content, context) {
|
|
5655
5787
|
const m = content.match(/^from\s+["'](.+?)["']\s+import\s+(.+)/);
|
|
5656
5788
|
if (!m) return;
|
|
@@ -5668,7 +5800,7 @@ var init_engine = __esm({
|
|
|
5668
5800
|
const macroM = tagContent.match(/^macro\s+(\w+)\s*\(([^)]*)\)/);
|
|
5669
5801
|
if (macroM && names.includes(macroM[1])) {
|
|
5670
5802
|
const macroName = macroM[1];
|
|
5671
|
-
const paramNames = macroM[2]
|
|
5803
|
+
const paramNames = _Frond.parseMacroParams(macroM[2]);
|
|
5672
5804
|
const bodyTokens = [];
|
|
5673
5805
|
i++;
|
|
5674
5806
|
while (i < tokens.length) {
|
|
@@ -5686,7 +5818,8 @@ var init_engine = __esm({
|
|
|
5686
5818
|
context[macroName] = (...args) => {
|
|
5687
5819
|
const macroCtx = { ...capturedCtx };
|
|
5688
5820
|
for (let pi = 0; pi < capturedParams.length; pi++) {
|
|
5689
|
-
|
|
5821
|
+
const [pname, pdefault] = capturedParams[pi];
|
|
5822
|
+
macroCtx[pname] = pi < args.length ? args[pi] : pdefault;
|
|
5690
5823
|
}
|
|
5691
5824
|
return new SafeString(engine.renderTokens([...capturedBody], macroCtx));
|
|
5692
5825
|
};
|
|
@@ -8793,7 +8926,14 @@ function fullAnalysis(root = "src") {
|
|
|
8793
8926
|
total_functions: allFunctions.length,
|
|
8794
8927
|
avg_complexity: Math.round(avgCC * 100) / 100,
|
|
8795
8928
|
avg_maintainability: Math.round(avgMI * 10) / 10,
|
|
8929
|
+
// Display-only: the top-15 for the "most complex functions" report.
|
|
8930
|
+
// Do NOT source offenders / --fail-on from this — capping here silently
|
|
8931
|
+
// hides the 16th+ over-threshold function from the gate. offenders()
|
|
8932
|
+
// reads "all_functions" (below) instead.
|
|
8796
8933
|
most_complex_functions: allFunctions.slice(0, 15),
|
|
8934
|
+
// Full, uncapped, complexity-sorted list — offenders()/--fail-on use this
|
|
8935
|
+
// so no function over the complexity threshold ever escapes the gate.
|
|
8936
|
+
all_functions: allFunctions,
|
|
8797
8937
|
file_metrics: fileMetrics,
|
|
8798
8938
|
violations,
|
|
8799
8939
|
dependency_graph: importGraph,
|
|
@@ -29953,18 +30093,21 @@ var init_postgres = __esm({
|
|
|
29953
30093
|
}
|
|
29954
30094
|
async startTransactionAsync() {
|
|
29955
30095
|
await this.executeAsync("BEGIN");
|
|
30096
|
+
this._inTransaction = true;
|
|
29956
30097
|
}
|
|
29957
30098
|
commit() {
|
|
29958
30099
|
throw new Error("Use commitAsync() for PostgreSQL.");
|
|
29959
30100
|
}
|
|
29960
30101
|
async commitAsync() {
|
|
29961
30102
|
await this.executeAsync("COMMIT");
|
|
30103
|
+
this._inTransaction = false;
|
|
29962
30104
|
}
|
|
29963
30105
|
rollback() {
|
|
29964
30106
|
throw new Error("Use rollbackAsync() for PostgreSQL.");
|
|
29965
30107
|
}
|
|
29966
30108
|
async rollbackAsync() {
|
|
29967
30109
|
await this.executeAsync("ROLLBACK");
|
|
30110
|
+
this._inTransaction = false;
|
|
29968
30111
|
}
|
|
29969
30112
|
tables() {
|
|
29970
30113
|
throw new Error("Use tablesAsync() for PostgreSQL.");
|
|
@@ -30983,10 +31126,35 @@ var init_firebird = __esm({
|
|
|
30983
31126
|
translated = SQLTranslator.ilikeToLike(translated);
|
|
30984
31127
|
return translated;
|
|
30985
31128
|
}
|
|
31129
|
+
/**
|
|
31130
|
+
* The handle every statement runs on. While an explicit transaction is open
|
|
31131
|
+
* (startTransactionAsync set `this.transaction`), statements MUST run on that
|
|
31132
|
+
* transaction object so they are undone by rollbackAsync() / persisted by
|
|
31133
|
+
* commitAsync() — node-firebird's transaction exposes the same
|
|
31134
|
+
* query()/execute() as the connection. With no transaction open we run on
|
|
31135
|
+
* `this.db`, whose per-statement work auto-commits on the connection.
|
|
31136
|
+
*
|
|
31137
|
+
* This matches the Python master's contract (tina4_python/database/firebird.py):
|
|
31138
|
+
* there, ALL statements run on the single connection and start_transaction()
|
|
31139
|
+
* merely suppresses the per-statement autocommit in execute() so the batch
|
|
31140
|
+
* stays open until commit()/rollback(). node-firebird has no such suppression
|
|
31141
|
+
* hook — its `db.query/execute` always auto-commit — so the equivalent is to
|
|
31142
|
+
* route statements through the transaction object instead. Same observable
|
|
31143
|
+
* behaviour: an open transaction is atomic and rolls back cleanly.
|
|
31144
|
+
*
|
|
31145
|
+
* Previously every statement ran on `this.db` unconditionally, so the
|
|
31146
|
+
* transaction created by startTransactionAsync() never saw a single statement
|
|
31147
|
+
* — rollbackAsync() rolled back an EMPTY transaction and the already
|
|
31148
|
+
* auto-committed write survived (silent no-op). Twin of the PHP pdo_firebird
|
|
31149
|
+
* bug fixed in 3.13.86.
|
|
31150
|
+
*/
|
|
31151
|
+
statementHandle() {
|
|
31152
|
+
return this.transaction ?? this.db;
|
|
31153
|
+
}
|
|
30986
31154
|
queryPromise(sql, params) {
|
|
30987
31155
|
return new Promise((resolve21, reject) => {
|
|
30988
31156
|
const translated = this.translateSql(sql);
|
|
30989
|
-
this.
|
|
31157
|
+
this.statementHandle().query(translated, params ?? [], (err, result) => {
|
|
30990
31158
|
if (err) reject(err);
|
|
30991
31159
|
else resolve21(result ?? []);
|
|
30992
31160
|
});
|
|
@@ -30995,7 +31163,7 @@ var init_firebird = __esm({
|
|
|
30995
31163
|
executePromise(sql, params) {
|
|
30996
31164
|
return new Promise((resolve21, reject) => {
|
|
30997
31165
|
const translated = this.translateSql(sql);
|
|
30998
|
-
this.
|
|
31166
|
+
this.statementHandle().execute(translated, params ?? [], (err) => {
|
|
30999
31167
|
if (err) reject(err);
|
|
31000
31168
|
else resolve21();
|
|
31001
31169
|
});
|
|
@@ -32969,14 +33137,15 @@ var init_database = __esm({
|
|
|
32969
33137
|
async executeMany(sql, paramSets = []) {
|
|
32970
33138
|
const adapter = this.getNextAdapter();
|
|
32971
33139
|
const results = [];
|
|
32972
|
-
|
|
33140
|
+
const owns = !this.inExplicitTransaction();
|
|
33141
|
+
if (owns) await adapterStartTransaction(adapter);
|
|
32973
33142
|
try {
|
|
32974
33143
|
for (const params of paramSets) {
|
|
32975
33144
|
results.push(await adapterExecute(adapter, sql, params));
|
|
32976
33145
|
}
|
|
32977
|
-
await adapterCommit(adapter);
|
|
33146
|
+
if (owns) await adapterCommit(adapter);
|
|
32978
33147
|
} catch (e) {
|
|
32979
|
-
await adapterRollback(adapter);
|
|
33148
|
+
if (owns) await adapterRollback(adapter);
|
|
32980
33149
|
throw e;
|
|
32981
33150
|
}
|
|
32982
33151
|
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
|
|