tina4-nodejs 3.13.45 → 3.13.47
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
CHANGED
package/package.json
CHANGED
|
@@ -122,6 +122,9 @@ function compileString(
|
|
|
122
122
|
// 5. Resolve @include
|
|
123
123
|
scss = resolveIncludes(scss, mixins);
|
|
124
124
|
|
|
125
|
+
// 5.5. Resolve #{ ... } interpolation (before $var substitution + nesting).
|
|
126
|
+
scss = resolveInterpolation(scss, variables);
|
|
127
|
+
|
|
125
128
|
// 6. Substitute variables
|
|
126
129
|
scss = substituteVariables(scss, variables);
|
|
127
130
|
|
|
@@ -182,6 +185,26 @@ function substituteVariables(scss: string, variables: Record<string, string>): s
|
|
|
182
185
|
return scss;
|
|
183
186
|
}
|
|
184
187
|
|
|
188
|
+
/**
|
|
189
|
+
* Resolve SCSS `#{ ... }` interpolation. Each `#{ expr }` is replaced by its
|
|
190
|
+
* resolved inner text: a `$variable` inside the braces resolves to its value,
|
|
191
|
+
* anything else is inlined verbatim (trimmed). This lets a value carry a
|
|
192
|
+
* variable inside a string context plain `$var` substitution can't reach —
|
|
193
|
+
* e.g. `calc(100% - #{$gap})` → `calc(100% - 20px)` — and lets a variable
|
|
194
|
+
* appear in a selector (`.icon-#{$name}` → `.icon-home`). Run BEFORE nested
|
|
195
|
+
* rule flattening so the literal braces never confuse the block matcher.
|
|
196
|
+
*/
|
|
197
|
+
function resolveInterpolation(scss: string, variables: Record<string, string>): string {
|
|
198
|
+
const sorted = Object.keys(variables).sort((a, b) => b.length - a.length);
|
|
199
|
+
return scss.replace(/#\{([^{}]*)\}/g, (_m, inner: string) => {
|
|
200
|
+
let resolved = inner.trim();
|
|
201
|
+
for (const name of sorted) {
|
|
202
|
+
resolved = resolved.replaceAll(`$${name}`, variables[name]);
|
|
203
|
+
}
|
|
204
|
+
return resolved;
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
185
208
|
// ── Mixins ───────────────────────────────────────────────────────
|
|
186
209
|
|
|
187
210
|
function extractMixins(
|
|
@@ -40,6 +40,10 @@ export interface MssqlConfig {
|
|
|
40
40
|
export class MssqlAdapter implements DatabaseAdapter {
|
|
41
41
|
private connection: any = null;
|
|
42
42
|
private _lastInsertId: number | bigint | null = null;
|
|
43
|
+
// True between startTransactionAsync() and commit/rollback. executeManyAsync
|
|
44
|
+
// uses it to decide whether IT owns the batch transaction (mirrors the Python
|
|
45
|
+
// master's owns_txn guard) so it never double-BEGINs inside an explicit one.
|
|
46
|
+
private _inTransaction = false;
|
|
43
47
|
|
|
44
48
|
constructor(private config: MssqlConfig | string) {}
|
|
45
49
|
|
|
@@ -179,10 +183,26 @@ export class MssqlAdapter implements DatabaseAdapter {
|
|
|
179
183
|
}
|
|
180
184
|
|
|
181
185
|
async executeManyAsync(sql: string, paramsList: unknown[][]): Promise<{ totalAffected: number; lastInsertId?: number | bigint }> {
|
|
186
|
+
// Run the whole batch in ONE transaction so it is atomic (all-or-nothing) —
|
|
187
|
+
// a bad row mid-batch rolls back the rows already inserted instead of
|
|
188
|
+
// leaving a partial write. Mirrors the documented "wrapped in a transaction"
|
|
189
|
+
// contract, the SQLite adapter, and the Python master's execute_many. Only
|
|
190
|
+
// own the transaction when not already inside an explicit one (owns guard),
|
|
191
|
+
// so a batch insert nested in a caller's startTransaction() just joins it.
|
|
192
|
+
const owns = !this._inTransaction;
|
|
193
|
+
if (owns) await this.startTransactionAsync();
|
|
182
194
|
let totalAffected = 0;
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
195
|
+
try {
|
|
196
|
+
for (const params of paramsList) {
|
|
197
|
+
await this.executeAsync(sql, params);
|
|
198
|
+
totalAffected++;
|
|
199
|
+
}
|
|
200
|
+
if (owns) await this.commitAsync();
|
|
201
|
+
} catch (e) {
|
|
202
|
+
if (owns) {
|
|
203
|
+
try { await this.rollbackAsync(); } catch { /* surface the original error */ }
|
|
204
|
+
}
|
|
205
|
+
throw e;
|
|
186
206
|
}
|
|
187
207
|
return { totalAffected };
|
|
188
208
|
}
|
|
@@ -275,7 +295,11 @@ export class MssqlAdapter implements DatabaseAdapter {
|
|
|
275
295
|
if (id !== null) this._lastInsertId = id;
|
|
276
296
|
return {
|
|
277
297
|
success: true,
|
|
278
|
-
|
|
298
|
+
// A single-object insert affects exactly one row. Do NOT use
|
|
299
|
+
// result.rowCount here: the statement is "INSERT ...; SELECT
|
|
300
|
+
// SCOPE_IDENTITY()", and tedious sums the row counts of BOTH statements
|
|
301
|
+
// (1 for the INSERT + 1 for the SELECT), which reported rowsAffected=2.
|
|
302
|
+
rowsAffected: 1,
|
|
279
303
|
lastInsertId: id ?? undefined,
|
|
280
304
|
};
|
|
281
305
|
} catch (e) {
|
|
@@ -331,7 +355,16 @@ export class MssqlAdapter implements DatabaseAdapter {
|
|
|
331
355
|
}
|
|
332
356
|
|
|
333
357
|
async startTransactionAsync(): Promise<void> {
|
|
334
|
-
|
|
358
|
+
// Use tedious's NATIVE transaction API, NOT a raw "BEGIN TRANSACTION" via
|
|
359
|
+
// execSql: every adapter statement runs through sp_executesql (an RPC), and
|
|
360
|
+
// SQL Server forbids changing @@TRANCOUNT inside an sp_executesql call, so a
|
|
361
|
+
// raw BEGIN raised "Transaction count ... mismatching BEGIN and COMMIT".
|
|
362
|
+
// beginTransaction manages the transaction at the TDS protocol level, so the
|
|
363
|
+
// INSERTs inside it commit/rollback atomically.
|
|
364
|
+
await new Promise<void>((resolve, reject) => {
|
|
365
|
+
this.connection.beginTransaction((err: Error | null) => (err ? reject(err) : resolve()));
|
|
366
|
+
});
|
|
367
|
+
this._inTransaction = true;
|
|
335
368
|
}
|
|
336
369
|
|
|
337
370
|
commit(): void {
|
|
@@ -339,7 +372,10 @@ export class MssqlAdapter implements DatabaseAdapter {
|
|
|
339
372
|
}
|
|
340
373
|
|
|
341
374
|
async commitAsync(): Promise<void> {
|
|
342
|
-
await
|
|
375
|
+
await new Promise<void>((resolve, reject) => {
|
|
376
|
+
this.connection.commitTransaction((err: Error | null) => (err ? reject(err) : resolve()));
|
|
377
|
+
});
|
|
378
|
+
this._inTransaction = false;
|
|
343
379
|
}
|
|
344
380
|
|
|
345
381
|
rollback(): void {
|
|
@@ -347,7 +383,10 @@ export class MssqlAdapter implements DatabaseAdapter {
|
|
|
347
383
|
}
|
|
348
384
|
|
|
349
385
|
async rollbackAsync(): Promise<void> {
|
|
350
|
-
await
|
|
386
|
+
await new Promise<void>((resolve, reject) => {
|
|
387
|
+
this.connection.rollbackTransaction((err: Error | null) => (err ? reject(err) : resolve()));
|
|
388
|
+
});
|
|
389
|
+
this._inTransaction = false;
|
|
351
390
|
}
|
|
352
391
|
|
|
353
392
|
tables(): string[] {
|
|
@@ -39,6 +39,10 @@ export interface MysqlConfig {
|
|
|
39
39
|
export class MysqlAdapter implements DatabaseAdapter {
|
|
40
40
|
private connection: any = null;
|
|
41
41
|
private _lastInsertId: number | bigint | null = null;
|
|
42
|
+
// True between startTransactionAsync() and commit/rollback. executeManyAsync
|
|
43
|
+
// uses it to decide whether IT owns the batch transaction (mirrors the Python
|
|
44
|
+
// master's owns_txn guard) so it never double-BEGINs inside an explicit one.
|
|
45
|
+
private _inTransaction = false;
|
|
42
46
|
|
|
43
47
|
constructor(private config: MysqlConfig | string) {}
|
|
44
48
|
|
|
@@ -108,12 +112,28 @@ export class MysqlAdapter implements DatabaseAdapter {
|
|
|
108
112
|
}
|
|
109
113
|
|
|
110
114
|
async executeManyAsync(sql: string, paramsList: unknown[][]): Promise<{ totalAffected: number; lastInsertId?: number | bigint }> {
|
|
115
|
+
// Run the whole batch in ONE transaction so it is atomic (all-or-nothing) —
|
|
116
|
+
// a bad row mid-batch rolls back the rows already inserted instead of
|
|
117
|
+
// leaving a partial write. Mirrors the documented "wrapped in a transaction"
|
|
118
|
+
// contract, the SQLite adapter, and the Python master's execute_many. Only
|
|
119
|
+
// own the transaction when not already inside an explicit one (owns guard),
|
|
120
|
+
// so a batch insert nested in a caller's startTransaction() just joins it.
|
|
121
|
+
const owns = !this._inTransaction;
|
|
122
|
+
if (owns) await this.startTransactionAsync();
|
|
111
123
|
let totalAffected = 0;
|
|
112
124
|
let lastId: number | bigint | undefined;
|
|
113
|
-
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
125
|
+
try {
|
|
126
|
+
for (const params of paramsList) {
|
|
127
|
+
const result = await this.executeAsync(sql, params) as any;
|
|
128
|
+
totalAffected += result?.affectedRows ?? 1;
|
|
129
|
+
if (result?.insertId) lastId = result.insertId;
|
|
130
|
+
}
|
|
131
|
+
if (owns) await this.commitAsync();
|
|
132
|
+
} catch (e) {
|
|
133
|
+
if (owns) {
|
|
134
|
+
try { await this.rollbackAsync(); } catch { /* surface the original error */ }
|
|
135
|
+
}
|
|
136
|
+
throw e;
|
|
117
137
|
}
|
|
118
138
|
return { totalAffected, lastInsertId: lastId };
|
|
119
139
|
}
|
|
@@ -248,6 +268,7 @@ export class MysqlAdapter implements DatabaseAdapter {
|
|
|
248
268
|
|
|
249
269
|
async startTransactionAsync(): Promise<void> {
|
|
250
270
|
await this.executeAsync("START TRANSACTION");
|
|
271
|
+
this._inTransaction = true;
|
|
251
272
|
}
|
|
252
273
|
|
|
253
274
|
commit(): void {
|
|
@@ -256,6 +277,7 @@ export class MysqlAdapter implements DatabaseAdapter {
|
|
|
256
277
|
|
|
257
278
|
async commitAsync(): Promise<void> {
|
|
258
279
|
await this.executeAsync("COMMIT");
|
|
280
|
+
this._inTransaction = false;
|
|
259
281
|
}
|
|
260
282
|
|
|
261
283
|
rollback(): void {
|
|
@@ -264,6 +286,7 @@ export class MysqlAdapter implements DatabaseAdapter {
|
|
|
264
286
|
|
|
265
287
|
async rollbackAsync(): Promise<void> {
|
|
266
288
|
await this.executeAsync("ROLLBACK");
|
|
289
|
+
this._inTransaction = false;
|
|
267
290
|
}
|
|
268
291
|
|
|
269
292
|
tables(): string[] {
|
|
@@ -72,6 +72,10 @@ export class PostgresAdapter implements DatabaseAdapter {
|
|
|
72
72
|
// `id uuid PRIMARY KEY DEFAULT gen_random_uuid()` shape) returns its id as a
|
|
73
73
|
// 36-char string via RETURNING, not a SERIAL integer (#256).
|
|
74
74
|
private _lastInsertId: number | bigint | string | null = null;
|
|
75
|
+
// True between startTransactionAsync() and commit/rollback. executeManyAsync
|
|
76
|
+
// uses it to decide whether IT owns the batch transaction (mirrors the Python
|
|
77
|
+
// master's owns_txn guard) so it never double-BEGINs inside an explicit one.
|
|
78
|
+
private _inTransaction = false;
|
|
75
79
|
|
|
76
80
|
constructor(private config: PostgresConfig | string) {}
|
|
77
81
|
|
|
@@ -150,14 +154,30 @@ export class PostgresAdapter implements DatabaseAdapter {
|
|
|
150
154
|
|
|
151
155
|
/** Async executeMany for real usage. */
|
|
152
156
|
async executeManyAsync(sql: string, paramsList: unknown[][]): Promise<{ totalAffected: number; lastInsertId?: number | bigint }> {
|
|
157
|
+
// Run the whole batch in ONE transaction so it is atomic (all-or-nothing) —
|
|
158
|
+
// a bad row mid-batch rolls back the rows already inserted instead of
|
|
159
|
+
// leaving a partial write. Mirrors the documented "wrapped in a transaction"
|
|
160
|
+
// contract, the SQLite adapter, and the Python master's execute_many. Only
|
|
161
|
+
// own the transaction when not already inside an explicit one (owns guard),
|
|
162
|
+
// so a batch insert nested in a caller's startTransaction() just joins it.
|
|
163
|
+
const owns = !this._inTransaction;
|
|
164
|
+
if (owns) await this.startTransactionAsync();
|
|
153
165
|
let totalAffected = 0;
|
|
154
166
|
let lastId: number | bigint | undefined;
|
|
155
|
-
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
167
|
+
try {
|
|
168
|
+
for (const params of paramsList) {
|
|
169
|
+
const result = await this.executeAsync(sql, params);
|
|
170
|
+
totalAffected++;
|
|
171
|
+
if (result && typeof result === "object" && "lastInsertId" in (result as any)) {
|
|
172
|
+
lastId = (result as any).lastInsertId;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (owns) await this.commitAsync();
|
|
176
|
+
} catch (e) {
|
|
177
|
+
if (owns) {
|
|
178
|
+
try { await this.rollbackAsync(); } catch { /* surface the original error */ }
|
|
160
179
|
}
|
|
180
|
+
throw e;
|
|
161
181
|
}
|
|
162
182
|
return { totalAffected, lastInsertId: lastId };
|
|
163
183
|
}
|
|
@@ -569,54 +569,137 @@ export function normalizeQuotes(sql: string): string {
|
|
|
569
569
|
}
|
|
570
570
|
|
|
571
571
|
/**
|
|
572
|
-
* Split SQL text into individual statements
|
|
572
|
+
* Split SQL text into individual statements with a single-pass, quote- and
|
|
573
|
+
* comment-aware scanner. The split decision is made character by character so
|
|
574
|
+
* the delimiter only ever fires in real statement position.
|
|
573
575
|
*
|
|
574
|
-
*
|
|
575
|
-
*
|
|
576
|
+
* This is the fix for issue #54: the old implementation split on `delimiter`
|
|
577
|
+
* BEFORE stripping `-- …` line comments, so a `;` inside a line comment
|
|
578
|
+
* fragmented one statement into several broken pieces. A scanner that knows
|
|
579
|
+
* where it is (code / comment / string) cannot make that mistake.
|
|
580
|
+
*
|
|
581
|
+
* Handled, in priority order, only when NOT already inside a stored-proc block:
|
|
582
|
+
* - `$$ … $$` and `// … //` stored-proc blocks are kept intact (inner `;` never
|
|
583
|
+
* splits). A `//` preceded by `:` is a URL scheme (`https://…`), not a delimiter.
|
|
584
|
+
* - `/* … */` block comments are stripped.
|
|
585
|
+
* - `-- …` line comments are stripped to end of line (the newline is kept).
|
|
586
|
+
* - `'…'` single-quoted strings and `"…"` double-quoted identifiers are copied
|
|
587
|
+
* verbatim, honouring the SQL doubled-quote escape (`''` / `""`); a `;`, `--`
|
|
588
|
+
* or `/*` inside a literal is data, not a delimiter or comment.
|
|
589
|
+
* Mirrors the tina4-python `_split_statements` / tina4-php scanner (parity).
|
|
576
590
|
*/
|
|
577
591
|
export function splitStatements(sql: string, delimiter = ";"): string[] {
|
|
578
592
|
// Normalize smart/curly quotes to straight ASCII first, so SQL pasted from
|
|
579
|
-
// an editor/doc (which converts " → “ ” and ' → ‘ ’) actually runs.
|
|
580
|
-
// Python's _split_statements applying _normalize_quotes as its first line.
|
|
593
|
+
// an editor/doc (which converts " → “ ” and ' → ‘ ’) actually runs.
|
|
581
594
|
sql = normalizeQuotes(sql);
|
|
582
595
|
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
const
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
596
|
+
const statements: string[] = [];
|
|
597
|
+
let current = "";
|
|
598
|
+
const n = sql.length;
|
|
599
|
+
const dlen = delimiter.length;
|
|
600
|
+
let i = 0;
|
|
601
|
+
let inDollarBlock = false;
|
|
602
|
+
let inSlashBlock = false;
|
|
603
|
+
|
|
604
|
+
while (i < n) {
|
|
605
|
+
const ch = sql[i];
|
|
606
|
+
|
|
607
|
+
// $$ … $$ stored-proc block (toggle).
|
|
608
|
+
if (!inSlashBlock && ch === "$" && i + 1 < n && sql[i + 1] === "$") {
|
|
609
|
+
current += "$$";
|
|
610
|
+
i += 2;
|
|
611
|
+
inDollarBlock = !inDollarBlock;
|
|
612
|
+
continue;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// // … // stored-proc block (toggle) — but NOT a `://` URL scheme.
|
|
616
|
+
if (
|
|
617
|
+
!inDollarBlock && ch === "/" && i + 1 < n && sql[i + 1] === "/" &&
|
|
618
|
+
!(i > 0 && sql[i - 1] === ":")
|
|
619
|
+
) {
|
|
620
|
+
current += "//";
|
|
621
|
+
i += 2;
|
|
622
|
+
inSlashBlock = !inSlashBlock;
|
|
623
|
+
continue;
|
|
624
|
+
}
|
|
589
625
|
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
processed = processed.replace(/(?<!:)\/\/([\s\S]*?)(?<!:)\/\//g, saveBlock);
|
|
626
|
+
// Inside a stored-proc block: consume verbatim (inner ; never splits).
|
|
627
|
+
if (inDollarBlock || inSlashBlock) {
|
|
628
|
+
current += ch;
|
|
629
|
+
i += 1;
|
|
630
|
+
continue;
|
|
631
|
+
}
|
|
597
632
|
|
|
598
|
-
|
|
599
|
-
|
|
633
|
+
// Block comment /* … */ — stripped.
|
|
634
|
+
if (ch === "/" && i + 1 < n && sql[i + 1] === "*") {
|
|
635
|
+
const end = sql.indexOf("*/", i + 2);
|
|
636
|
+
i = end !== -1 ? end + 2 : n;
|
|
637
|
+
continue;
|
|
638
|
+
}
|
|
600
639
|
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
// Remove inline comments
|
|
608
|
-
const commentPos = line.indexOf("--");
|
|
609
|
-
lines.push(commentPos >= 0 ? line.slice(0, commentPos) : line);
|
|
640
|
+
// Line comment -- … — stripped to end of line; the newline is left for the
|
|
641
|
+
// next iteration so line structure (and NEXT-line boundaries) survive.
|
|
642
|
+
if (ch === "-" && i + 1 < n && sql[i + 1] === "-") {
|
|
643
|
+
const end = sql.indexOf("\n", i + 2);
|
|
644
|
+
i = end !== -1 ? end : n;
|
|
645
|
+
continue;
|
|
610
646
|
}
|
|
611
|
-
let cleaned = lines.join("\n").trim();
|
|
612
647
|
|
|
613
|
-
//
|
|
614
|
-
|
|
615
|
-
|
|
648
|
+
// Single-quoted string literal — '' escapes a quote. Copied verbatim.
|
|
649
|
+
if (ch === "'") {
|
|
650
|
+
current += "'";
|
|
651
|
+
i += 1;
|
|
652
|
+
while (i < n) {
|
|
653
|
+
if (sql[i] === "'" && i + 1 < n && sql[i + 1] === "'") {
|
|
654
|
+
current += "''";
|
|
655
|
+
i += 2;
|
|
656
|
+
} else if (sql[i] === "'") {
|
|
657
|
+
current += "'";
|
|
658
|
+
i += 1;
|
|
659
|
+
break;
|
|
660
|
+
} else {
|
|
661
|
+
current += sql[i];
|
|
662
|
+
i += 1;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
continue;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// Double-quoted identifier — "" escapes a quote. Same verbatim handling.
|
|
669
|
+
if (ch === '"') {
|
|
670
|
+
current += '"';
|
|
671
|
+
i += 1;
|
|
672
|
+
while (i < n) {
|
|
673
|
+
if (sql[i] === '"' && i + 1 < n && sql[i + 1] === '"') {
|
|
674
|
+
current += '""';
|
|
675
|
+
i += 2;
|
|
676
|
+
} else if (sql[i] === '"') {
|
|
677
|
+
current += '"';
|
|
678
|
+
i += 1;
|
|
679
|
+
break;
|
|
680
|
+
} else {
|
|
681
|
+
current += sql[i];
|
|
682
|
+
i += 1;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
continue;
|
|
616
686
|
}
|
|
617
687
|
|
|
618
|
-
|
|
688
|
+
// Statement delimiter — only reached outside blocks/comments/strings.
|
|
689
|
+
if (dlen > 0 && sql.startsWith(delimiter, i)) {
|
|
690
|
+
const stmt = current.trim();
|
|
691
|
+
if (stmt) statements.push(stmt);
|
|
692
|
+
current = "";
|
|
693
|
+
i += dlen;
|
|
694
|
+
continue;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
current += ch;
|
|
698
|
+
i += 1;
|
|
619
699
|
}
|
|
700
|
+
|
|
701
|
+
const stmt = current.trim();
|
|
702
|
+
if (stmt) statements.push(stmt);
|
|
620
703
|
return statements;
|
|
621
704
|
}
|
|
622
705
|
|