forge-sql-orm 2.1.23 → 2.1.25
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/README.md +46 -23
- package/dist/core/ForgeSQLAnalyseOperations.d.ts +4 -0
- package/dist/core/ForgeSQLAnalyseOperations.d.ts.map +1 -1
- package/dist/core/ForgeSQLAnalyseOperations.js +17 -21
- package/dist/core/ForgeSQLAnalyseOperations.js.map +1 -1
- package/dist/core/ForgeSQLCrudOperations.d.ts +16 -0
- package/dist/core/ForgeSQLCrudOperations.d.ts.map +1 -1
- package/dist/core/ForgeSQLCrudOperations.js +60 -28
- package/dist/core/ForgeSQLCrudOperations.js.map +1 -1
- package/dist/core/ForgeSQLQueryBuilder.d.ts +15 -28
- package/dist/core/ForgeSQLQueryBuilder.d.ts.map +1 -1
- package/dist/core/ForgeSQLQueryBuilder.js +20 -47
- package/dist/core/ForgeSQLQueryBuilder.js.map +1 -1
- package/dist/core/Rovo.d.ts +32 -0
- package/dist/core/Rovo.d.ts.map +1 -1
- package/dist/core/Rovo.js +116 -67
- package/dist/core/Rovo.js.map +1 -1
- package/dist/lib/drizzle/extensions/additionalActions.d.ts.map +1 -1
- package/dist/lib/drizzle/extensions/additionalActions.js +168 -118
- package/dist/lib/drizzle/extensions/additionalActions.js.map +1 -1
- package/dist/utils/cacheTableUtils.d.ts +0 -8
- package/dist/utils/cacheTableUtils.d.ts.map +1 -1
- package/dist/utils/cacheTableUtils.js +183 -126
- package/dist/utils/cacheTableUtils.js.map +1 -1
- package/dist/utils/cacheUtils.d.ts +13 -1
- package/dist/utils/cacheUtils.d.ts.map +1 -1
- package/dist/utils/cacheUtils.js +60 -47
- package/dist/utils/cacheUtils.js.map +1 -1
- package/dist/utils/forgeDriverProxy.d.ts.map +1 -1
- package/dist/utils/forgeDriverProxy.js +31 -20
- package/dist/utils/forgeDriverProxy.js.map +1 -1
- package/dist/utils/sqlHints.d.ts.map +1 -1
- package/dist/utils/sqlHints.js +19 -29
- package/dist/utils/sqlHints.js.map +1 -1
- package/dist/utils/sqlUtils.d.ts +0 -29
- package/dist/utils/sqlUtils.d.ts.map +1 -1
- package/dist/utils/sqlUtils.js +107 -78
- package/dist/utils/sqlUtils.js.map +1 -1
- package/dist/webtriggers/clearCacheSchedulerTrigger.d.ts +24 -4
- package/dist/webtriggers/clearCacheSchedulerTrigger.d.ts.map +1 -1
- package/dist/webtriggers/clearCacheSchedulerTrigger.js +24 -4
- package/dist/webtriggers/clearCacheSchedulerTrigger.js.map +1 -1
- package/package.json +9 -9
- package/src/core/ForgeSQLAnalyseOperations.ts +18 -21
- package/src/core/ForgeSQLCrudOperations.ts +83 -33
- package/src/core/ForgeSQLQueryBuilder.ts +59 -154
- package/src/core/Rovo.ts +158 -98
- package/src/lib/drizzle/extensions/additionalActions.ts +287 -382
- package/src/utils/cacheTableUtils.ts +202 -144
- package/src/utils/cacheUtils.ts +70 -47
- package/src/utils/forgeDriverProxy.ts +39 -21
- package/src/utils/sqlHints.ts +21 -26
- package/src/utils/sqlUtils.ts +151 -101
- package/src/webtriggers/clearCacheSchedulerTrigger.ts +24 -4
|
@@ -1,6 +1,17 @@
|
|
|
1
|
+
// qlty-ignore: +qlty:file-complexity
|
|
1
2
|
import { Parser } from "node-sql-parser";
|
|
2
3
|
import { ForgeSqlOrmOptions } from "../core/ForgeSQLQueryBuilder";
|
|
3
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Extracts table name from backticks_quote_string type.
|
|
7
|
+
*/
|
|
8
|
+
function extractTableNameFromBackticks(value: any): string | null {
|
|
9
|
+
if (value.type === "backticks_quote_string" && typeof value.value === "string") {
|
|
10
|
+
return value.value === "dual" ? null : value.value.toLowerCase();
|
|
11
|
+
}
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
|
|
4
15
|
/**
|
|
5
16
|
* Extracts table name from object value.
|
|
6
17
|
*/
|
|
@@ -9,22 +20,25 @@ function extractTableNameFromObject(value: any, context?: string): string | null
|
|
|
9
20
|
if (Array.isArray(value)) {
|
|
10
21
|
return null;
|
|
11
22
|
}
|
|
23
|
+
|
|
12
24
|
// Handle backticks_quote_string type only for node.table context
|
|
13
|
-
if (
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
return value.value === "dual" ? null : value.value.toLowerCase();
|
|
25
|
+
if (context?.includes("node.table")) {
|
|
26
|
+
const fromBackticks = extractTableNameFromBackticks(value);
|
|
27
|
+
if (fromBackticks !== null) {
|
|
28
|
+
return fromBackticks;
|
|
29
|
+
}
|
|
19
30
|
}
|
|
31
|
+
|
|
20
32
|
// Try value.name first (most common)
|
|
21
33
|
if (typeof value.name === "string") {
|
|
22
34
|
return value.name === "dual" ? null : value.name.toLowerCase();
|
|
23
35
|
}
|
|
36
|
+
|
|
24
37
|
// Try value.table if it's a nested structure
|
|
25
38
|
if (value.table) {
|
|
26
39
|
return normalizeTableName(value.table, context);
|
|
27
40
|
}
|
|
41
|
+
|
|
28
42
|
// Log when we encounter an object that we can't extract table name from
|
|
29
43
|
// eslint-disable-next-line no-console
|
|
30
44
|
console.warn(
|
|
@@ -93,6 +107,22 @@ function isLikelyAlias(node: any): boolean {
|
|
|
93
107
|
return isColumnRefAlias(node) || isExplicitAlias(node) || isShortNameAlias(node);
|
|
94
108
|
}
|
|
95
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Extracts table name from table/dual node.
|
|
112
|
+
*/
|
|
113
|
+
function extractTableNameFromTableNode(node: any): string | null {
|
|
114
|
+
const fromTable = node.table
|
|
115
|
+
? normalizeTableName(node.table, `node.type=${node.type}, node.table`)
|
|
116
|
+
: null;
|
|
117
|
+
if (fromTable) {
|
|
118
|
+
return fromTable;
|
|
119
|
+
}
|
|
120
|
+
const fromName = node.name
|
|
121
|
+
? normalizeTableName(node.name, `node.type=${node.type}, node.name`)
|
|
122
|
+
: null;
|
|
123
|
+
return fromName;
|
|
124
|
+
}
|
|
125
|
+
|
|
96
126
|
/**
|
|
97
127
|
* Extracts table name from table node.
|
|
98
128
|
*
|
|
@@ -100,38 +130,18 @@ function isLikelyAlias(node: any): boolean {
|
|
|
100
130
|
* @returns Table name in lowercase or null if not applicable
|
|
101
131
|
*/
|
|
102
132
|
function extractTableName(node: any): string | null {
|
|
103
|
-
if (!node) {
|
|
104
|
-
return null;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Early return for likely aliases
|
|
108
|
-
if (isLikelyAlias(node)) {
|
|
133
|
+
if (!node || isLikelyAlias(node)) {
|
|
109
134
|
return null;
|
|
110
135
|
}
|
|
111
136
|
|
|
112
137
|
// Handle table node directly
|
|
113
138
|
if (node.type === "table" || node.type === "dual") {
|
|
114
|
-
|
|
115
|
-
? normalizeTableName(node.table, `node.type=${node.type}, node.table`)
|
|
116
|
-
: null;
|
|
117
|
-
if (fromTable) {
|
|
118
|
-
return fromTable;
|
|
119
|
-
}
|
|
120
|
-
const fromName = node.name
|
|
121
|
-
? normalizeTableName(node.name, `node.type=${node.type}, node.name`)
|
|
122
|
-
: null;
|
|
123
|
-
if (fromName) {
|
|
124
|
-
return fromName;
|
|
125
|
-
}
|
|
126
|
-
return null;
|
|
139
|
+
return extractTableNameFromTableNode(node);
|
|
127
140
|
}
|
|
128
141
|
|
|
129
142
|
// Handle table reference in different formats
|
|
130
143
|
if (node.table) {
|
|
131
|
-
|
|
132
|
-
if (tableName) {
|
|
133
|
-
return tableName;
|
|
134
|
-
}
|
|
144
|
+
return normalizeTableName(node.table, `node.table (type: ${node.type})`);
|
|
135
145
|
}
|
|
136
146
|
|
|
137
147
|
return null;
|
|
@@ -205,6 +215,30 @@ function processFromAndJoin(node: any, tables: Set<string>): void {
|
|
|
205
215
|
}
|
|
206
216
|
}
|
|
207
217
|
|
|
218
|
+
/**
|
|
219
|
+
* Processes a single column for table extraction.
|
|
220
|
+
*/
|
|
221
|
+
function processSingleColumn(col: any, tables: Set<string>): void {
|
|
222
|
+
if (!col) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// If the column itself is a subquery
|
|
227
|
+
if (col.type === "subquery" || col.type === "select") {
|
|
228
|
+
extractTablesFromNode(col, tables);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Process expression (may contain subqueries)
|
|
232
|
+
if (col.expr) {
|
|
233
|
+
extractTablesFromNode(col.expr, tables);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Process AST (alternative structure for subqueries)
|
|
237
|
+
if (col.ast) {
|
|
238
|
+
extractTablesFromNode(col.ast, tables);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
208
242
|
/**
|
|
209
243
|
* Processes SELECT columns that may contain subqueries.
|
|
210
244
|
*/
|
|
@@ -215,24 +249,7 @@ function processSelectColumns(node: any, tables: Set<string>): void {
|
|
|
215
249
|
}
|
|
216
250
|
|
|
217
251
|
if (Array.isArray(columns)) {
|
|
218
|
-
columns.forEach((col: any) =>
|
|
219
|
-
if (!col) return;
|
|
220
|
-
|
|
221
|
-
// If the column itself is a subquery
|
|
222
|
-
if (col.type === "subquery" || col.type === "select") {
|
|
223
|
-
extractTablesFromNode(col, tables);
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// Process expression (may contain subqueries)
|
|
227
|
-
if (col.expr) {
|
|
228
|
-
extractTablesFromNode(col.expr, tables);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
// Process AST (alternative structure for subqueries)
|
|
232
|
-
if (col.ast) {
|
|
233
|
-
extractTablesFromNode(col.ast, tables);
|
|
234
|
-
}
|
|
235
|
-
});
|
|
252
|
+
columns.forEach((col: any) => processSingleColumn(col, tables));
|
|
236
253
|
} else if (typeof columns === "object") {
|
|
237
254
|
extractTablesFromNode(columns, tables);
|
|
238
255
|
}
|
|
@@ -265,14 +282,16 @@ function processUnionNode(unionNode: any, tables: Set<string>): void {
|
|
|
265
282
|
return;
|
|
266
283
|
}
|
|
267
284
|
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
285
|
+
const unionTypes = [
|
|
286
|
+
"select",
|
|
287
|
+
"union",
|
|
288
|
+
"union_all",
|
|
289
|
+
"union_distinct",
|
|
290
|
+
"intersect",
|
|
291
|
+
"except",
|
|
292
|
+
"minus",
|
|
293
|
+
];
|
|
294
|
+
const isUnionType = unionTypes.includes(unionNode.type);
|
|
276
295
|
|
|
277
296
|
if (isUnionType) {
|
|
278
297
|
extractTablesFromNode(unionNode, tables);
|
|
@@ -304,13 +323,15 @@ function processUnion(node: any, tables: Set<string>): void {
|
|
|
304
323
|
* Processes UNION/INTERSECT/EXCEPT operation nodes.
|
|
305
324
|
*/
|
|
306
325
|
function processUnionOperation(node: any, tables: Set<string>): void {
|
|
307
|
-
const
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
326
|
+
const unionOperationTypes = [
|
|
327
|
+
"union",
|
|
328
|
+
"union_all",
|
|
329
|
+
"union_distinct",
|
|
330
|
+
"intersect",
|
|
331
|
+
"except",
|
|
332
|
+
"minus",
|
|
333
|
+
];
|
|
334
|
+
const isUnionOperation = unionOperationTypes.includes(node.type);
|
|
314
335
|
|
|
315
336
|
if (!isUnionOperation) {
|
|
316
337
|
return;
|
|
@@ -342,27 +363,25 @@ function processNext(node: any, tables: Set<string>): void {
|
|
|
342
363
|
/**
|
|
343
364
|
* Recursively processes all object properties for any remaining nested structures.
|
|
344
365
|
*/
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
return;
|
|
356
|
-
}
|
|
366
|
+
/**
|
|
367
|
+
* Processes array values recursively.
|
|
368
|
+
*/
|
|
369
|
+
function processArrayValues(values: any[], tables: Set<string>): void {
|
|
370
|
+
values.forEach((item: any) => {
|
|
371
|
+
if (item && typeof item === "object") {
|
|
372
|
+
extractTablesFromNode(item, tables);
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
}
|
|
357
376
|
|
|
377
|
+
/**
|
|
378
|
+
* Processes object values recursively.
|
|
379
|
+
*/
|
|
380
|
+
function processObjectValues(node: any, tables: Set<string>): void {
|
|
358
381
|
Object.values(node).forEach((value) => {
|
|
359
382
|
if (value && typeof value === "object") {
|
|
360
383
|
if (Array.isArray(value)) {
|
|
361
|
-
value
|
|
362
|
-
if (item && typeof item === "object") {
|
|
363
|
-
extractTablesFromNode(item, tables);
|
|
364
|
-
}
|
|
365
|
-
});
|
|
384
|
+
processArrayValues(value, tables);
|
|
366
385
|
} else {
|
|
367
386
|
extractTablesFromNode(value, tables);
|
|
368
387
|
}
|
|
@@ -370,6 +389,72 @@ function processRecursively(node: any, tables: Set<string>): void {
|
|
|
370
389
|
});
|
|
371
390
|
}
|
|
372
391
|
|
|
392
|
+
function processRecursively(node: any, tables: Set<string>): void {
|
|
393
|
+
const isColumnRefAlias = node.type === "column_ref" && !node.table;
|
|
394
|
+
const hasName = Boolean(node.name);
|
|
395
|
+
const hasNoTable = !node.table;
|
|
396
|
+
const isNotTableType = node.type !== "table" && node.type !== "dual";
|
|
397
|
+
const isShortName = hasName && node.name.length <= 2;
|
|
398
|
+
const isShortNameAlias = hasName && hasNoTable && isNotTableType && isShortName;
|
|
399
|
+
const isLikelyAlias = isColumnRefAlias || isShortNameAlias;
|
|
400
|
+
|
|
401
|
+
if (isLikelyAlias || Array.isArray(node)) {
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
processObjectValues(node, tables);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Processes DML statement types (UPDATE, INSERT, DELETE).
|
|
410
|
+
*/
|
|
411
|
+
function processDmlStatements(node: any, tables: Set<string>): void {
|
|
412
|
+
if (node.type === "update" && node.table) {
|
|
413
|
+
extractTablesFromNode(node.table, tables);
|
|
414
|
+
} else if (node.type === "insert" && node.table) {
|
|
415
|
+
extractTablesFromNode(node.table, tables);
|
|
416
|
+
} else if (node.type === "delete" && node.from) {
|
|
417
|
+
extractTablesFromNode(node.from, tables);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Processes SELECT-specific clauses (WHERE, HAVING, ORDER BY, GROUP BY).
|
|
423
|
+
*/
|
|
424
|
+
function processSelectClauses(node: any, tables: Set<string>): void {
|
|
425
|
+
if (node.where) {
|
|
426
|
+
extractTablesFromNode(node.where, tables);
|
|
427
|
+
}
|
|
428
|
+
if (node.having) {
|
|
429
|
+
extractTablesFromNode(node.having, tables);
|
|
430
|
+
}
|
|
431
|
+
processOrderByOrGroupBy(node.orderby || node.order_by, tables);
|
|
432
|
+
processOrderByOrGroupBy(node.groupby || node.group_by, tables);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Processes subquery and SELECT statement types.
|
|
437
|
+
*/
|
|
438
|
+
function processSubqueryAndSelect(node: any, tables: Set<string>): void {
|
|
439
|
+
if (node.type === "subquery" || node.type === "select") {
|
|
440
|
+
if (node.ast) {
|
|
441
|
+
extractTablesFromNode(node.ast, tables);
|
|
442
|
+
}
|
|
443
|
+
if (node.from) {
|
|
444
|
+
extractTablesFromNode(node.from, tables);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Processes set operations (UNION, INTERSECT, EXCEPT).
|
|
451
|
+
*/
|
|
452
|
+
function processSetOperations(node: any, tables: Set<string>): void {
|
|
453
|
+
processUnion(node, tables);
|
|
454
|
+
processUnionOperation(node, tables);
|
|
455
|
+
processNext(node, tables);
|
|
456
|
+
}
|
|
457
|
+
|
|
373
458
|
/**
|
|
374
459
|
* Recursively extracts table names from SQL AST node.
|
|
375
460
|
* Handles regular tables, CTEs, subqueries, and complex query structures.
|
|
@@ -391,58 +476,20 @@ function extractTablesFromNode(node: any, tables: Set<string>): void {
|
|
|
391
476
|
// Extract tables from FROM and JOIN clauses
|
|
392
477
|
processFromAndJoin(node, tables);
|
|
393
478
|
|
|
394
|
-
// Handle subqueries
|
|
395
|
-
|
|
396
|
-
if (node.ast) {
|
|
397
|
-
extractTablesFromNode(node.ast, tables);
|
|
398
|
-
}
|
|
399
|
-
if (node.from) {
|
|
400
|
-
extractTablesFromNode(node.from, tables);
|
|
401
|
-
}
|
|
402
|
-
}
|
|
479
|
+
// Handle subqueries and SELECT statements
|
|
480
|
+
processSubqueryAndSelect(node, tables);
|
|
403
481
|
|
|
404
|
-
//
|
|
405
|
-
|
|
406
|
-
extractTablesFromNode(node.where, tables);
|
|
407
|
-
}
|
|
482
|
+
// Process SELECT-specific clauses
|
|
483
|
+
processSelectClauses(node, tables);
|
|
408
484
|
|
|
409
485
|
// Extract tables from SELECT columns (may contain subqueries)
|
|
410
486
|
processSelectColumns(node, tables);
|
|
411
487
|
|
|
412
|
-
//
|
|
413
|
-
|
|
414
|
-
extractTablesFromNode(node.having, tables);
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
// Extract tables from ORDER BY clause (may contain subqueries)
|
|
418
|
-
processOrderByOrGroupBy(node.orderby || node.order_by, tables);
|
|
419
|
-
|
|
420
|
-
// Extract tables from GROUP BY clause (may contain subqueries)
|
|
421
|
-
processOrderByOrGroupBy(node.groupby || node.group_by, tables);
|
|
488
|
+
// Process DML statements (UPDATE, INSERT, DELETE)
|
|
489
|
+
processDmlStatements(node, tables);
|
|
422
490
|
|
|
423
|
-
//
|
|
424
|
-
|
|
425
|
-
extractTablesFromNode(node.table, tables);
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
// Extract tables from INSERT statement
|
|
429
|
-
if (node.type === "insert" && node.table) {
|
|
430
|
-
extractTablesFromNode(node.table, tables);
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
// Extract tables from DELETE statement
|
|
434
|
-
if (node.type === "delete" && node.from) {
|
|
435
|
-
extractTablesFromNode(node.from, tables);
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
// Extract tables from UNION operations
|
|
439
|
-
processUnion(node, tables);
|
|
440
|
-
|
|
441
|
-
// Handle node types for UNION/INTERSECT/EXCEPT operations
|
|
442
|
-
processUnionOperation(node, tables);
|
|
443
|
-
|
|
444
|
-
// Handle _next property (alternative UNION structure)
|
|
445
|
-
processNext(node, tables);
|
|
491
|
+
// Process set operations (UNION, INTERSECT, EXCEPT)
|
|
492
|
+
processSetOperations(node, tables);
|
|
446
493
|
|
|
447
494
|
// Recursively process all object properties
|
|
448
495
|
processRecursively(node, tables);
|
|
@@ -456,6 +503,33 @@ function extractTablesFromNode(node: any, tables: Set<string>): void {
|
|
|
456
503
|
* @param options - ForgeSQL ORM options for logging
|
|
457
504
|
* @returns Comma-separated string of unique table names in backticks
|
|
458
505
|
*/
|
|
506
|
+
/**
|
|
507
|
+
* Formats table names as backticked comma-separated string.
|
|
508
|
+
*/
|
|
509
|
+
function formatBacktickedValues(tables: Set<string>): string {
|
|
510
|
+
return Array.from(tables)
|
|
511
|
+
.sort((a, b) => a.localeCompare(b, undefined, { sensitivity: "base", numeric: true }))
|
|
512
|
+
.map((table) => `\`${table}\``)
|
|
513
|
+
.join(",");
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Extracts table names using regex fallback.
|
|
518
|
+
*/
|
|
519
|
+
function extractTablesWithRegex(sql: string): Set<string> {
|
|
520
|
+
const regex = /`([^`]+)`/g;
|
|
521
|
+
const matches = new Set<string>();
|
|
522
|
+
let match;
|
|
523
|
+
|
|
524
|
+
while ((match = regex.exec(sql.toLowerCase())) !== null) {
|
|
525
|
+
if (!match[1].startsWith("a_")) {
|
|
526
|
+
matches.add(match[1]);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
return matches;
|
|
531
|
+
}
|
|
532
|
+
|
|
459
533
|
export function extractBacktickedValues(sql: string, options: ForgeSqlOrmOptions): string {
|
|
460
534
|
// Try to use node-sql-parser first
|
|
461
535
|
try {
|
|
@@ -471,11 +545,7 @@ export function extractBacktickedValues(sql: string, options: ForgeSqlOrmOptions
|
|
|
471
545
|
});
|
|
472
546
|
|
|
473
547
|
if (tables.size > 0) {
|
|
474
|
-
|
|
475
|
-
const backtickedValues = Array.from(tables)
|
|
476
|
-
.sort((a, b) => a.localeCompare(b, undefined, { sensitivity: "base", numeric: true }))
|
|
477
|
-
.map((table) => `\`${table}\``)
|
|
478
|
-
.join(",");
|
|
548
|
+
const backtickedValues = formatBacktickedValues(tables);
|
|
479
549
|
if (options.logCache) {
|
|
480
550
|
// eslint-disable-next-line no-console
|
|
481
551
|
console.warn(`Extracted backticked values: ${backtickedValues}`);
|
|
@@ -494,18 +564,6 @@ export function extractBacktickedValues(sql: string, options: ForgeSqlOrmOptions
|
|
|
494
564
|
}
|
|
495
565
|
|
|
496
566
|
// Fallback to regex-based extraction (original logic)
|
|
497
|
-
const
|
|
498
|
-
|
|
499
|
-
let match;
|
|
500
|
-
|
|
501
|
-
while ((match = regex.exec(sql.toLowerCase())) !== null) {
|
|
502
|
-
if (!match[1].startsWith("a_")) {
|
|
503
|
-
matches.add(`\`${match[1]}\``);
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
// Sort to ensure consistent order for the same input
|
|
508
|
-
return Array.from(matches)
|
|
509
|
-
.sort((a, b) => a.localeCompare(b, undefined, { sensitivity: "base", numeric: true }))
|
|
510
|
-
.join(",");
|
|
567
|
+
const matches = extractTablesWithRegex(sql);
|
|
568
|
+
return formatBacktickedValues(matches);
|
|
511
569
|
}
|
package/src/utils/cacheUtils.ts
CHANGED
|
@@ -48,6 +48,30 @@ function nowPlusSeconds(secondsToAdd: number): number {
|
|
|
48
48
|
return Math.floor(dt.toSeconds());
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Logs a message to console.debug when options.logCache is enabled.
|
|
53
|
+
*
|
|
54
|
+
* @param message - Message to log
|
|
55
|
+
* @param options - ForgeSQL ORM options (optional)
|
|
56
|
+
*/
|
|
57
|
+
function debugLog(message: string, options?: ForgeSqlOrmOptions): void {
|
|
58
|
+
if (options?.logCache) {
|
|
59
|
+
// eslint-disable-next-line no-console
|
|
60
|
+
console.debug(message);
|
|
61
|
+
}
|
|
62
|
+
} /**
|
|
63
|
+
* Logs a message to console.debug when options.logCache is enabled.
|
|
64
|
+
*
|
|
65
|
+
* @param message - Message to log
|
|
66
|
+
* @param options - ForgeSQL ORM options (optional)
|
|
67
|
+
*/
|
|
68
|
+
function warnLog(message: string, options?: ForgeSqlOrmOptions): void {
|
|
69
|
+
if (options?.logCache) {
|
|
70
|
+
// eslint-disable-next-line no-console
|
|
71
|
+
console.warn(message);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
51
75
|
/**
|
|
52
76
|
* Generates a hash key for a query based on its SQL and parameters.
|
|
53
77
|
*
|
|
@@ -66,19 +90,20 @@ export function hashKey(query: Query): string {
|
|
|
66
90
|
*
|
|
67
91
|
* @param results - Array of cache entries to delete
|
|
68
92
|
* @param cacheEntityName - Name of the cache entity
|
|
93
|
+
* @param options - Forge SQL ORM properties
|
|
69
94
|
* @returns Promise that resolves when all deletions are complete
|
|
70
95
|
*/
|
|
71
96
|
async function deleteCacheEntriesInBatches(
|
|
72
97
|
results: Array<{ key: string }>,
|
|
73
98
|
cacheEntityName: string,
|
|
99
|
+
options?: ForgeSqlOrmOptions,
|
|
74
100
|
): Promise<void> {
|
|
75
101
|
for (let i = 0; i < results.length; i += CACHE_CONSTANTS.BATCH_SIZE) {
|
|
76
102
|
const batch = results.slice(i, i + CACHE_CONSTANTS.BATCH_SIZE);
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
await transactionBuilder.execute();
|
|
103
|
+
const batchResult = await kvs.batchDelete(
|
|
104
|
+
batch.map((result) => ({ key: result.key, entityName: cacheEntityName })),
|
|
105
|
+
);
|
|
106
|
+
batchResult.failedKeys.forEach((failedKey) => warnLog(JSON.stringify(failedKey), options));
|
|
82
107
|
}
|
|
83
108
|
}
|
|
84
109
|
|
|
@@ -124,12 +149,9 @@ async function clearCursorCache(
|
|
|
124
149
|
|
|
125
150
|
const listResult = await entityQueryBuilder.limit(100).getMany();
|
|
126
151
|
|
|
127
|
-
|
|
128
|
-
// eslint-disable-next-line no-console
|
|
129
|
-
console.warn(`clear cache Records: ${JSON.stringify(listResult.results.map((r) => r.key))}`);
|
|
130
|
-
}
|
|
152
|
+
debugLog(`clear cache Records: ${JSON.stringify(listResult.results.map((r) => r.key))}`, options);
|
|
131
153
|
|
|
132
|
-
await deleteCacheEntriesInBatches(listResult.results, cacheEntityName);
|
|
154
|
+
await deleteCacheEntriesInBatches(listResult.results, cacheEntityName, options);
|
|
133
155
|
|
|
134
156
|
if (listResult.nextCursor) {
|
|
135
157
|
return (
|
|
@@ -172,10 +194,10 @@ async function clearExpirationCursorCache(
|
|
|
172
194
|
|
|
173
195
|
const listResult = await entityQueryBuilder.limit(100).getMany();
|
|
174
196
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
197
|
+
debugLog(
|
|
198
|
+
`clear expired Records: ${JSON.stringify(listResult.results.map((r) => r.key))}`,
|
|
199
|
+
options,
|
|
200
|
+
);
|
|
179
201
|
|
|
180
202
|
await deleteCacheEntriesInBatches(listResult.results, cacheEntityName);
|
|
181
203
|
|
|
@@ -265,14 +287,12 @@ export async function clearTablesCache(
|
|
|
265
287
|
"clearing cache",
|
|
266
288
|
);
|
|
267
289
|
} finally {
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
// eslint-disable-next-line no-console
|
|
271
|
-
console.info(`Cleared ${totalRecords} cache records in ${duration} seconds`);
|
|
272
|
-
}
|
|
290
|
+
const duration = DateTime.now().toSeconds() - startTime.toSeconds();
|
|
291
|
+
debugLog(`Cleared ${totalRecords} cache records in ${duration} seconds`, options);
|
|
273
292
|
}
|
|
274
293
|
}
|
|
275
294
|
/**
|
|
295
|
+
* since https://developer.atlassian.com/platform/forge/changelog/#CHANGE-3038
|
|
276
296
|
* Clears expired cache entries with retry logic and performance logging.
|
|
277
297
|
*
|
|
278
298
|
* @param options - ForgeSQL ORM options
|
|
@@ -293,16 +313,17 @@ export async function clearExpiredCache(options: ForgeSqlOrmOptions): Promise<vo
|
|
|
293
313
|
);
|
|
294
314
|
} finally {
|
|
295
315
|
const duration = DateTime.now().toSeconds() - startTime.toSeconds();
|
|
296
|
-
|
|
297
|
-
// eslint-disable-next-line no-console
|
|
298
|
-
console.debug(`Cleared ${totalRecords} expired cache records in ${duration} seconds`);
|
|
299
|
-
}
|
|
316
|
+
debugLog(`Cleared ${totalRecords} expired cache records in ${duration} seconds`, options);
|
|
300
317
|
}
|
|
301
318
|
}
|
|
302
319
|
|
|
303
320
|
/**
|
|
304
321
|
* Retrieves data from cache if it exists and is not expired.
|
|
305
322
|
*
|
|
323
|
+
* Note: Due to Forge KVS asynchronous deletion (up to 48 hours), expired entries may still
|
|
324
|
+
* be returned. This function checks the expiration timestamp to filter out expired entries.
|
|
325
|
+
* If cache growth impacts performance, use the Clear Cache Scheduler Trigger.
|
|
326
|
+
*
|
|
306
327
|
* @param query - Query object with toSQL method
|
|
307
328
|
* @param options - ForgeSQL ORM options
|
|
308
329
|
* @returns Cached data if found and valid, undefined otherwise
|
|
@@ -325,27 +346,27 @@ export async function getFromCache<T>(
|
|
|
325
346
|
|
|
326
347
|
// Skip cache if table is in cache context (will be cleared)
|
|
327
348
|
if (await isTableContainsTableInCacheContext(sqlQuery.sql, options)) {
|
|
328
|
-
|
|
329
|
-
// eslint-disable-next-line no-console
|
|
330
|
-
console.warn(`Context contains value to clear. Skip getting from cache`);
|
|
331
|
-
}
|
|
349
|
+
debugLog("Context contains value to clear. Skip getting from cache", options);
|
|
332
350
|
return undefined;
|
|
333
351
|
}
|
|
334
352
|
|
|
335
353
|
try {
|
|
336
354
|
const cacheResult = await kvs.entity<CacheEntity>(options.cacheEntityName).get(key);
|
|
337
355
|
|
|
338
|
-
if (
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
356
|
+
if (cacheResult) {
|
|
357
|
+
if (
|
|
358
|
+
(cacheResult[expirationName] as number) >= getCurrentTime() &&
|
|
359
|
+
extractBacktickedValues(sqlQuery.sql, options) === cacheResult[entityQueryName]
|
|
360
|
+
) {
|
|
361
|
+
debugLog(`Get value from cache, cacheKey: ${key}`, options);
|
|
362
|
+
const results = cacheResult[dataName];
|
|
363
|
+
return JSON.parse(results as string);
|
|
364
|
+
} else {
|
|
365
|
+
debugLog(
|
|
366
|
+
`Expired cache entry still exists (will be automatically removed within 48 hours per Forge KVS TTL documentation), cacheKey: ${key}`,
|
|
367
|
+
options,
|
|
368
|
+
);
|
|
346
369
|
}
|
|
347
|
-
const results = cacheResult[dataName];
|
|
348
|
-
return JSON.parse(results as string);
|
|
349
370
|
}
|
|
350
371
|
} catch (error: any) {
|
|
351
372
|
// eslint-disable-next-line no-console
|
|
@@ -358,11 +379,18 @@ export async function getFromCache<T>(
|
|
|
358
379
|
/**
|
|
359
380
|
* Stores query results in cache with specified TTL.
|
|
360
381
|
*
|
|
382
|
+
* Uses Forge KVS TTL feature to set expiration. Note that expired data deletion is asynchronous:
|
|
383
|
+
* expired data is not removed immediately upon expiry. Deletion may take up to 48 hours.
|
|
384
|
+
* During this window, read operations may still return expired results. If your app requires
|
|
385
|
+
* strict expiry semantics, consider using the Clear Cache Scheduler Trigger to proactively
|
|
386
|
+
* clean up expired entries, especially if cache growth impacts INSERT/UPDATE performance.
|
|
387
|
+
*
|
|
361
388
|
* @param query - Query object with toSQL method
|
|
362
389
|
* @param options - ForgeSQL ORM options
|
|
363
390
|
* @param results - Data to cache
|
|
364
|
-
* @param cacheTtl - Time to live in seconds
|
|
391
|
+
* @param cacheTtl - Time to live in seconds (maximum TTL is 1 year from write time)
|
|
365
392
|
* @returns Promise that resolves when data is stored in cache
|
|
393
|
+
* @see https://developer.atlassian.com/platform/forge/runtime-reference/storage-api-basic-api/#ttl
|
|
366
394
|
*/
|
|
367
395
|
export async function setCacheResult(
|
|
368
396
|
query: { toSQL: () => Query },
|
|
@@ -385,10 +413,7 @@ export async function setCacheResult(
|
|
|
385
413
|
|
|
386
414
|
// Skip cache if table is in cache context (will be cleared)
|
|
387
415
|
if (await isTableContainsTableInCacheContext(sqlQuery.sql, options)) {
|
|
388
|
-
|
|
389
|
-
// eslint-disable-next-line no-console
|
|
390
|
-
console.warn(`Context contains value to clear. Skip setting from cache`);
|
|
391
|
-
}
|
|
416
|
+
debugLog("Context contains value to clear. Skip setting from cache", options);
|
|
392
417
|
return;
|
|
393
418
|
}
|
|
394
419
|
|
|
@@ -400,17 +425,15 @@ export async function setCacheResult(
|
|
|
400
425
|
key,
|
|
401
426
|
{
|
|
402
427
|
[entityQueryName]: extractBacktickedValues(sqlQuery.sql, options),
|
|
403
|
-
[expirationName]: nowPlusSeconds(cacheTtl),
|
|
428
|
+
[expirationName]: nowPlusSeconds(cacheTtl + 2),
|
|
404
429
|
[dataName]: JSON.stringify(results),
|
|
405
430
|
},
|
|
406
431
|
{ entityName: options.cacheEntityName },
|
|
432
|
+
{ ttl: { value: cacheTtl, unit: "SECONDS" } },
|
|
407
433
|
)
|
|
408
434
|
.execute();
|
|
409
435
|
|
|
410
|
-
|
|
411
|
-
// eslint-disable-next-line no-console
|
|
412
|
-
console.warn(`Store value to cache, cacheKey: ${key}`);
|
|
413
|
-
}
|
|
436
|
+
debugLog(`Store value to cache, cacheKey: ${key}`, options);
|
|
414
437
|
} catch (error: any) {
|
|
415
438
|
// eslint-disable-next-line no-console
|
|
416
439
|
console.error(`Error setting cache: ${error.message}`, error);
|