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.
Files changed (54) hide show
  1. package/README.md +46 -23
  2. package/dist/core/ForgeSQLAnalyseOperations.d.ts +4 -0
  3. package/dist/core/ForgeSQLAnalyseOperations.d.ts.map +1 -1
  4. package/dist/core/ForgeSQLAnalyseOperations.js +17 -21
  5. package/dist/core/ForgeSQLAnalyseOperations.js.map +1 -1
  6. package/dist/core/ForgeSQLCrudOperations.d.ts +16 -0
  7. package/dist/core/ForgeSQLCrudOperations.d.ts.map +1 -1
  8. package/dist/core/ForgeSQLCrudOperations.js +60 -28
  9. package/dist/core/ForgeSQLCrudOperations.js.map +1 -1
  10. package/dist/core/ForgeSQLQueryBuilder.d.ts +15 -28
  11. package/dist/core/ForgeSQLQueryBuilder.d.ts.map +1 -1
  12. package/dist/core/ForgeSQLQueryBuilder.js +20 -47
  13. package/dist/core/ForgeSQLQueryBuilder.js.map +1 -1
  14. package/dist/core/Rovo.d.ts +32 -0
  15. package/dist/core/Rovo.d.ts.map +1 -1
  16. package/dist/core/Rovo.js +116 -67
  17. package/dist/core/Rovo.js.map +1 -1
  18. package/dist/lib/drizzle/extensions/additionalActions.d.ts.map +1 -1
  19. package/dist/lib/drizzle/extensions/additionalActions.js +168 -118
  20. package/dist/lib/drizzle/extensions/additionalActions.js.map +1 -1
  21. package/dist/utils/cacheTableUtils.d.ts +0 -8
  22. package/dist/utils/cacheTableUtils.d.ts.map +1 -1
  23. package/dist/utils/cacheTableUtils.js +183 -126
  24. package/dist/utils/cacheTableUtils.js.map +1 -1
  25. package/dist/utils/cacheUtils.d.ts +13 -1
  26. package/dist/utils/cacheUtils.d.ts.map +1 -1
  27. package/dist/utils/cacheUtils.js +60 -47
  28. package/dist/utils/cacheUtils.js.map +1 -1
  29. package/dist/utils/forgeDriverProxy.d.ts.map +1 -1
  30. package/dist/utils/forgeDriverProxy.js +31 -20
  31. package/dist/utils/forgeDriverProxy.js.map +1 -1
  32. package/dist/utils/sqlHints.d.ts.map +1 -1
  33. package/dist/utils/sqlHints.js +19 -29
  34. package/dist/utils/sqlHints.js.map +1 -1
  35. package/dist/utils/sqlUtils.d.ts +0 -29
  36. package/dist/utils/sqlUtils.d.ts.map +1 -1
  37. package/dist/utils/sqlUtils.js +107 -78
  38. package/dist/utils/sqlUtils.js.map +1 -1
  39. package/dist/webtriggers/clearCacheSchedulerTrigger.d.ts +24 -4
  40. package/dist/webtriggers/clearCacheSchedulerTrigger.d.ts.map +1 -1
  41. package/dist/webtriggers/clearCacheSchedulerTrigger.js +24 -4
  42. package/dist/webtriggers/clearCacheSchedulerTrigger.js.map +1 -1
  43. package/package.json +9 -9
  44. package/src/core/ForgeSQLAnalyseOperations.ts +18 -21
  45. package/src/core/ForgeSQLCrudOperations.ts +83 -33
  46. package/src/core/ForgeSQLQueryBuilder.ts +59 -154
  47. package/src/core/Rovo.ts +158 -98
  48. package/src/lib/drizzle/extensions/additionalActions.ts +287 -382
  49. package/src/utils/cacheTableUtils.ts +202 -144
  50. package/src/utils/cacheUtils.ts +70 -47
  51. package/src/utils/forgeDriverProxy.ts +39 -21
  52. package/src/utils/sqlHints.ts +21 -26
  53. package/src/utils/sqlUtils.ts +151 -101
  54. 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
- context?.includes("node.table") &&
15
- value.type === "backticks_quote_string" &&
16
- typeof value.value === "string"
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
- 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
- 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
- const tableName = normalizeTableName(node.table, `node.table (type: ${node.type})`);
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 isUnionType =
269
- unionNode.type === "select" ||
270
- unionNode.type === "union" ||
271
- unionNode.type === "union_all" ||
272
- unionNode.type === "union_distinct" ||
273
- unionNode.type === "intersect" ||
274
- unionNode.type === "except" ||
275
- unionNode.type === "minus";
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 isUnionOperation =
308
- node.type === "union" ||
309
- node.type === "union_all" ||
310
- node.type === "union_distinct" ||
311
- node.type === "intersect" ||
312
- node.type === "except" ||
313
- node.type === "minus";
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
- function processRecursively(node: any, tables: Set<string>): void {
346
- const isLikelyAlias =
347
- (node.type === "column_ref" && !node.table) ||
348
- (node.name &&
349
- !node.table &&
350
- node.type !== "table" &&
351
- node.type !== "dual" &&
352
- node.name.length <= 2);
353
-
354
- if (isLikelyAlias || Array.isArray(node)) {
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.forEach((item: any) => {
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 explicitly
395
- if (node.type === "subquery" || node.type === "select") {
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
- // Extract tables from WHERE clause (may contain subqueries)
405
- if (node.where) {
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
- // Extract tables from HAVING clause (may contain subqueries)
413
- if (node.having) {
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
- // Extract tables from UPDATE statement
424
- if (node.type === "update" && node.table) {
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
- // Sort to ensure consistent order for the same input
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 regex = /`([^`]+)`/g;
498
- const matches = new Set<string>();
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
  }
@@ -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
- let transactionBuilder = kvs.transact();
78
- for (const result of batch) {
79
- transactionBuilder = transactionBuilder.delete(result.key, { entityName: cacheEntityName });
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
- if (options.logCache) {
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
- if (options.logCache) {
176
- // eslint-disable-next-line no-console
177
- console.warn(`clear expired Records: ${JSON.stringify(listResult.results.map((r) => r.key))}`);
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
- if (options.logCache) {
269
- const duration = DateTime.now().toSeconds() - startTime.toSeconds();
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
- if (options?.logCache) {
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
- if (options.logCache) {
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
- cacheResult &&
340
- (cacheResult[expirationName] as number) >= getCurrentTime() &&
341
- extractBacktickedValues(sqlQuery.sql, options) === cacheResult[entityQueryName]
342
- ) {
343
- if (options.logCache) {
344
- // eslint-disable-next-line no-console
345
- console.warn(`Get value from cache, cacheKey: ${key}`);
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
- if (options.logCache) {
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
- if (options.logCache) {
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);