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.
@@ -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, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
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 paramNames = m[2].split(",").map((p) => p.trim()).filter(Boolean);
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 < paramNames.length; pi++) {
5648
- macroCtx[paramNames[pi]] = pi < args.length ? args[pi] : null;
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].split(",").map((p) => p.trim()).filter(Boolean);
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
- macroCtx[capturedParams[pi]] = pi < args.length ? args[pi] : null;
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.db.query(translated, params ?? [], (err, result) => {
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.db.execute(translated, params ?? [], (err) => {
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
- await adapterStartTransaction(adapter);
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.db.query(translated, params ?? [], (err: Error | null, result: any[]) => {
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.db.execute(translated, params ?? [], (err: Error | null) => {
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
- await adapterStartTransaction(adapter);
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