forge-sql-orm 2.1.13 → 2.1.15

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 (36) hide show
  1. package/README.md +550 -21
  2. package/dist/core/ForgeSQLORM.d.ts +45 -8
  3. package/dist/core/ForgeSQLORM.d.ts.map +1 -1
  4. package/dist/core/ForgeSQLORM.js +134 -15
  5. package/dist/core/ForgeSQLORM.js.map +1 -1
  6. package/dist/core/ForgeSQLQueryBuilder.d.ts +192 -5
  7. package/dist/core/ForgeSQLQueryBuilder.d.ts.map +1 -1
  8. package/dist/core/ForgeSQLQueryBuilder.js.map +1 -1
  9. package/dist/core/Rovo.d.ts +116 -0
  10. package/dist/core/Rovo.d.ts.map +1 -0
  11. package/dist/core/Rovo.js +647 -0
  12. package/dist/core/Rovo.js.map +1 -0
  13. package/dist/utils/forgeDriver.d.ts +3 -2
  14. package/dist/utils/forgeDriver.d.ts.map +1 -1
  15. package/dist/utils/forgeDriver.js +20 -16
  16. package/dist/utils/forgeDriver.js.map +1 -1
  17. package/dist/utils/metadataContextUtils.d.ts +27 -1
  18. package/dist/utils/metadataContextUtils.d.ts.map +1 -1
  19. package/dist/utils/metadataContextUtils.js +215 -12
  20. package/dist/utils/metadataContextUtils.js.map +1 -1
  21. package/dist/webtriggers/index.d.ts +1 -0
  22. package/dist/webtriggers/index.d.ts.map +1 -1
  23. package/dist/webtriggers/index.js +1 -0
  24. package/dist/webtriggers/index.js.map +1 -1
  25. package/dist/webtriggers/topSlowestStatementLastHourTrigger.d.ts +60 -0
  26. package/dist/webtriggers/topSlowestStatementLastHourTrigger.d.ts.map +1 -0
  27. package/dist/webtriggers/topSlowestStatementLastHourTrigger.js +55 -0
  28. package/dist/webtriggers/topSlowestStatementLastHourTrigger.js.map +1 -0
  29. package/package.json +13 -11
  30. package/src/core/ForgeSQLORM.ts +142 -14
  31. package/src/core/ForgeSQLQueryBuilder.ts +213 -4
  32. package/src/core/Rovo.ts +765 -0
  33. package/src/utils/forgeDriver.ts +34 -19
  34. package/src/utils/metadataContextUtils.ts +267 -12
  35. package/src/webtriggers/index.ts +1 -0
  36. package/src/webtriggers/topSlowestStatementLastHourTrigger.ts +69 -0
package/README.md CHANGED
@@ -10,6 +10,7 @@
10
10
  [![forge-sql-orm CI](https://github.com/vzakharchenko/forge-sql-orm/actions/workflows/node.js.yml/badge.svg)](https://github.com/vzakharchenko/forge-sql-orm/actions/workflows/node.js.yml)
11
11
  [![Coverage Status](https://coveralls.io/repos/github/vzakharchenko/forge-sql-orm/badge.svg?branch=master)](https://coveralls.io/github/vzakharchenko/forge-sql-orm?branch=master)
12
12
  [![DeepScan grade](https://deepscan.io/api/teams/26652/projects/29272/branches/940614/badge/grade.svg)](https://deepscan.io/dashboard#view=project&tid=26652&pid=29272&bid=940614)
13
+ [![Snyk Vulnerabilities](https://snyk.io/test/github/vzakharchenko/forge-sql-orm/badge.svg)](https://snyk.io/test/github/vzakharchenko/forge-sql-orm)
13
14
 
14
15
  **Forge-SQL-ORM** is an ORM designed for working with [@forge/sql](https://developer.atlassian.com/platform/forge/storage-reference/sql-tutorial/) in **Atlassian Forge**. It is built on top of [Drizzle ORM](https://orm.drizzle.team) and provides advanced capabilities for working with relational databases inside Forge.
15
16
 
@@ -22,7 +23,7 @@
22
23
  - ✅ **Type-Safe Query Building**: Write SQL queries with full TypeScript support
23
24
  - ✅ **Supports complex SQL queries** with joins and filtering using Drizzle ORM
24
25
  - ✅ **Advanced Query Methods**: `selectFrom()`, `selectDistinctFrom()`, `selectCacheableFrom()`, `selectDistinctCacheableFrom()` for all-column queries with field aliasing
25
- - ✅ **Query Execution with Metadata**: `executeWithMetadata()` method for capturing detailed execution metrics including database execution time, response size, and query analysis capabilities with performance monitoring
26
+ - ✅ **Query Execution with Metadata**: `executeWithMetadata()` method for capturing detailed execution metrics including database execution time, response size, and query analysis capabilities with performance monitoring. Supports two modes for query plan printing: TopSlowest mode (default) and SummaryTable mode
26
27
  - ✅ **Raw SQL Execution**: `execute()`, `executeCacheable()`, `executeDDL()`, and `executeDDLActions()` methods for direct SQL queries with local and global caching
27
28
  - ✅ **Common Table Expressions (CTEs)**: `with()` method for complex queries with subqueries
28
29
  - ✅ **Schema migration support**, allowing automatic schema evolution
@@ -33,6 +34,7 @@
33
34
  - ✅ **Ready-to-use Migration Triggers** Built-in web triggers for applying migrations, dropping tables (development-only), and fetching schema (development-only) with proper error handling and security controls
34
35
  - ✅ **Optimistic Locking** Ensures data consistency by preventing conflicts when multiple users update the same record
35
36
  - ✅ **Query Plan Analysis**: Detailed execution plan analysis and optimization insights
37
+ - ✅ **Rovo Integration** Secure pattern for natural-language analytics with comprehensive security validations, Row-Level Security (RLS) support, and dynamic SQL query execution
36
38
 
37
39
  ## Table of Contents
38
40
 
@@ -68,6 +70,7 @@
68
70
  ### 🔒 Advanced Features
69
71
 
70
72
  - [Optimistic Locking](#optimistic-locking)
73
+ - [Rovo Integration](#rovo-integration) - Secure pattern for natural-language analytics with dynamic SQL queries
71
74
  - [Query Analysis and Performance Optimization](#query-analysis-and-performance-optimization)
72
75
  - [Automatic Error Analysis](#automatic-error-analysis) - Automatic timeout and OOM error detection with execution plans
73
76
  - [Slow Query Monitoring](#slow-query-monitoring) - Scheduled monitoring of slow queries with execution plans
@@ -90,6 +93,7 @@
90
93
  - [Organization Tracker Example](examples/forge-sql-orm-example-org-tracker)
91
94
  - [Checklist Example](examples/forge-sql-orm-example-checklist)
92
95
  - [Cache Example](examples/forge-sql-orm-example-cache) - Advanced caching capabilities with performance monitoring
96
+ - [Rovo Integration Example](https://github.com/vzakharchenko/Forge-Secure-Notes-for-Jira) - Real-world Rovo AI agent implementation with secure natural-language analytics
93
97
 
94
98
  ### 📚 Reference
95
99
 
@@ -109,6 +113,7 @@
109
113
  - [Global Cache System (Level 2)](#global-cache-system-level-2) - Cross-invocation persistent caching
110
114
  - [Local Cache System (Level 1)](#local-cache-operations-level-1) - In-memory invocation caching
111
115
  - [Optimistic Locking](#optimistic-locking) - Data consistency
116
+ - [Rovo Integration](#rovo-integration) - Secure natural-language analytics
112
117
  - [Migration Tools](#web-triggers-for-migrations) - Database migrations
113
118
  - [Query Analysis](#query-analysis-and-performance-optimization) - Performance optimization
114
119
 
@@ -119,6 +124,7 @@
119
124
  - [Organization Tracker Example](examples/forge-sql-orm-example-org-tracker) - Complex relationships
120
125
  - [Checklist Example](examples/forge-sql-orm-example-checklist) - Jira integration
121
126
  - [Cache Example](examples/forge-sql-orm-example-cache) - Advanced caching capabilities
127
+ - [Rovo Integration Example](https://github.com/vzakharchenko/Forge-Secure-Notes-for-Jira) - Real-world Rovo AI agent with secure analytics
122
128
 
123
129
  ## Usage Approaches
124
130
 
@@ -342,6 +348,11 @@ resolver.define("fetch", async (req: Request) => {
342
348
  console.debug(`[Performance Debug fetch] High DB time: ${totalDbExecutionTime} ms`);
343
349
  }
344
350
  },
351
+ {
352
+ // Optional: Configure query plan printing behavior
353
+ mode: "TopSlowest", // Print top slowest queries (default)
354
+ topQueries: 3, // Print top 3 slowest queries
355
+ },
345
356
  );
346
357
  } catch (e) {
347
358
  const error = e?.cause?.debug?.sqlMessage ?? e?.cause;
@@ -351,12 +362,46 @@ resolver.define("fetch", async (req: Request) => {
351
362
  });
352
363
  ```
353
364
 
354
- ### 5. Next Steps
365
+ **Query Plan Printing Options:**
366
+
367
+ The `printQueriesWithPlan` function supports two modes:
368
+
369
+ 1. **TopSlowest Mode (default)**: Prints execution plans for the slowest queries from the current resolver invocation
370
+ - `mode`: Set to `'TopSlowest'` (default)
371
+ - `topQueries`: Number of top slowest queries to analyze (default: 1)
372
+
373
+ 2. **SummaryTable Mode**: Uses `CLUSTER_STATEMENTS_SUMMARY` for query analysis
374
+ - `mode`: Set to `'SummaryTable'`
375
+ - `summaryTableWindowTime`: Time window in milliseconds (default: 15000ms)
376
+ - Only works if queries are executed within the specified time window
377
+
378
+ ### 5. Rovo Integration (Secure Analytics)
379
+
380
+ ```typescript
381
+ // Secure dynamic SQL queries for natural-language analytics
382
+ const rovo = forgeSQL.rovo();
383
+ const settings = await rovo
384
+ .rovoSettingBuilder(usersTable, accountId)
385
+ .addContextParameter(":currentUserId", accountId)
386
+ .useRLS()
387
+ .addRlsColumn(usersTable.id)
388
+ .addRlsWherePart((alias) => `${alias}.${usersTable.id.name} = '${accountId}'`)
389
+ .finish()
390
+ .build();
391
+
392
+ const result = await rovo.dynamicIsolatedQuery(
393
+ "SELECT id, name FROM users WHERE status = 'active' AND userId = :currentUserId",
394
+ settings,
395
+ );
396
+ ```
397
+
398
+ ### 6. Next Steps
355
399
 
356
400
  - [Full Installation Guide](#installation) - Complete setup instructions
357
401
  - [Core Features](#core-features) - Learn about key capabilities
358
402
  - [Global Cache System (Level 2)](#global-cache-system-level-2) - Cross-invocation caching features
359
403
  - [Local Cache System (Level 1)](#local-cache-operations-level-1) - In-memory caching features
404
+ - [Rovo Integration](#rovo-integration) - Secure natural-language analytics
360
405
  - [API Reference](#reference) - Complete API documentation
361
406
 
362
407
  ## Drizzle Usage with forge-sql-orm
@@ -426,6 +471,11 @@ const usersWithMetadata = await forgeSQL.executeWithMetadata(
426
471
 
427
472
  console.log(`DB response size: ${totalResponseSize} bytes`);
428
473
  },
474
+ {
475
+ // Optional: Configure query plan printing
476
+ mode: "TopSlowest", // Print top slowest queries (default)
477
+ topQueries: 2, // Print top 2 slowest queries
478
+ },
429
479
  );
430
480
 
431
481
  // DDL operations for schema modifications
@@ -466,6 +516,22 @@ const userStats = await forgeSQL
466
516
  })
467
517
  .from(sql`activeUsers au`)
468
518
  .leftJoin(sql`completedOrders co`, eq(sql`au.id`, sql`co.userId`));
519
+
520
+ // Rovo Integration for secure dynamic SQL queries
521
+ const rovo = forgeSQL.rovo();
522
+ const settings = await rovo
523
+ .rovoSettingBuilder(usersTable, accountId)
524
+ .addContextParameter(":currentUserId", accountId)
525
+ .useRLS()
526
+ .addRlsColumn(usersTable.id)
527
+ .addRlsWherePart((alias) => `${alias}.${usersTable.id.name} = '${accountId}'`)
528
+ .finish()
529
+ .build();
530
+
531
+ const rovoResult = await rovo.dynamicIsolatedQuery(
532
+ "SELECT id, name FROM users WHERE status = 'active' AND userId = :currentUserId",
533
+ settings,
534
+ );
469
535
  ```
470
536
 
471
537
  This approach gives you direct access to all Drizzle ORM features while still using the @forge/sql backend with enhanced caching and versioning capabilities.
@@ -547,7 +613,12 @@ const usersWithMetadata = await forgeSQL.executeWithMetadata(
547
613
  }
548
614
 
549
615
  console.log(`DB response size: ${totalResponseSize} bytes`);
550
- }
616
+ },
617
+ {
618
+ // Optional: Configure query plan printing
619
+ mode: 'TopSlowest', // Print top slowest queries (default)
620
+ topQueries: 1, // Print top slowest query
621
+ },
551
622
  );
552
623
  ```
553
624
 
@@ -951,23 +1022,23 @@ const optimizedData = await forgeSQL.executeWithLocalCacheContextAndReturnValue(
951
1022
 
952
1023
  ### When to Use Each Approach
953
1024
 
954
- | Method | Use Case | Versioning | Cache Management |
955
- | ---------------------------------------------------------------------- | ----------------------------------------------------------- | ---------- | -------------------- |
956
- | `insertWithCacheContext/insertWithCacheContext/updateWithCacheContext` | Basic Drizzle operations | ❌ No | Cache Context |
957
- | `insertAndEvictCache()` | Simple inserts without conflicts | ❌ No | ✅ Yes |
958
- | `updateAndEvictCache()` | Simple updates without conflicts | ❌ No | ✅ Yes |
959
- | `deleteAndEvictCache()` | Simple deletes without conflicts | ❌ No | ✅ Yes |
960
- | `insert/update/delete` | Basic Drizzle operations | ❌ No | ❌ No |
961
- | `selectFrom()` | All-column queries with field aliasing | ❌ No | Local Cache |
962
- | `selectDistinctFrom()` | Distinct all-column queries with field aliasing | ❌ No | Local Cache |
963
- | `selectCacheableFrom()` | All-column queries with field aliasing and caching | ❌ No | Local + Global Cache |
964
- | `selectDistinctCacheableFrom()` | Distinct all-column queries with field aliasing and caching | ❌ No | Local + Global Cache |
965
- | `execute()` | Raw SQL queries with local caching | ❌ No | Local Cache |
966
- | `executeCacheable()` | Raw SQL queries with local and global caching | ❌ No | Local + Global Cache |
967
- | `executeWithMetadata()` | Raw SQL queries with execution metrics capture | ❌ No | Local Cache |
968
- | `executeDDL()` | DDL operations (CREATE, ALTER, DROP, etc.) | ❌ No | No Caching |
969
- | `executeDDLActions()` | Execute regular SQL queries in DDL operation context | ❌ No | No Caching |
970
- | `with()` | Common Table Expressions (CTEs) | ❌ No | Local Cache |
1025
+ | Method | Use Case | Versioning | Cache Management |
1026
+ | ---------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | ---------- | -------------------- |
1027
+ | `insertWithCacheContext/insertWithCacheContext/updateWithCacheContext` | Basic Drizzle operations | ❌ No | Cache Context |
1028
+ | `insertAndEvictCache()` | Simple inserts without conflicts | ❌ No | ✅ Yes |
1029
+ | `updateAndEvictCache()` | Simple updates without conflicts | ❌ No | ✅ Yes |
1030
+ | `deleteAndEvictCache()` | Simple deletes without conflicts | ❌ No | ✅ Yes |
1031
+ | `insert/update/delete` | Basic Drizzle operations | ❌ No | ❌ No |
1032
+ | `selectFrom()` | All-column queries with field aliasing | ❌ No | Local Cache |
1033
+ | `selectDistinctFrom()` | Distinct all-column queries with field aliasing | ❌ No | Local Cache |
1034
+ | `selectCacheableFrom()` | All-column queries with field aliasing and caching | ❌ No | Local + Global Cache |
1035
+ | `selectDistinctCacheableFrom()` | Distinct all-column queries with field aliasing and caching | ❌ No | Local + Global Cache |
1036
+ | `execute()` | Raw SQL queries with local caching | ❌ No | Local Cache |
1037
+ | `executeCacheable()` | Raw SQL queries with local and global caching | ❌ No | Local + Global Cache |
1038
+ | `executeWithMetadata()` | Resolver-level profiling with execution metrics and configurable query plan printing (TopSlowest or SummaryTable mode) | ❌ No | Local Cache |
1039
+ | `executeDDL()` | DDL operations (CREATE, ALTER, DROP, etc.) | ❌ No | No Caching |
1040
+ | `executeDDLActions()` | Execute regular SQL queries in DDL operation context | ❌ No | No Caching |
1041
+ | `with()` | Common Table Expressions (CTEs) | ❌ No | Local Cache |
971
1042
 
972
1043
  where Cache context - allows you to batch cache invalidation events and bypass cache reads for affected tables.
973
1044
 
@@ -1371,7 +1442,12 @@ const usersWithMetadata = await forgeSQL.executeWithMetadata(
1371
1442
  }
1372
1443
 
1373
1444
  console.log(`DB response size: ${totalResponseSize} bytes`);
1374
- }
1445
+ },
1446
+ {
1447
+ // Optional: Configure query plan printing
1448
+ mode: 'TopSlowest', // Print top slowest queries (default)
1449
+ topQueries: 1, // Print top slowest query
1450
+ },
1375
1451
  );
1376
1452
 
1377
1453
  // Using executeDDL() for DDL operations (CREATE, ALTER, DROP, etc.)
@@ -1739,6 +1815,11 @@ await forgeSQL.executeWithLocalContext(async () => {
1739
1815
 
1740
1816
  console.log(`DB response size: ${totalResponseSize} bytes`);
1741
1817
  },
1818
+ {
1819
+ // Optional: Configure query plan printing
1820
+ topQueries: 1, // Print top slowest query (default)
1821
+ mode: "TopSlowest", // Print top slowest queries (default)
1822
+ },
1742
1823
  );
1743
1824
 
1744
1825
  // Insert operation - evicts local cache for users table
@@ -1940,6 +2021,11 @@ const usersWithMetadata = await forgeSQL.executeWithMetadata(
1940
2021
 
1941
2022
  console.log(`DB response size: ${totalResponseSize} bytes`);
1942
2023
  },
2024
+ {
2025
+ // Optional: Configure query plan printing
2026
+ mode: "TopSlowest", // Print top slowest queries (default)
2027
+ topQueries: 1, // Print top slowest query
2028
+ },
1943
2029
  );
1944
2030
  ```
1945
2031
 
@@ -2011,6 +2097,223 @@ await forgeSQL.modifyWithVersioningAndEvictCache().updateById(
2011
2097
  );
2012
2098
  ```
2013
2099
 
2100
+ ## Rovo Integration
2101
+
2102
+ [↑ Back to Top](#table-of-contents)
2103
+
2104
+ Rovo is a secure pattern for natural-language analytics in Forge apps. It enables safe execution of dynamic SQL queries with comprehensive security validations, making it ideal for AI-powered analytics features where users can query data using natural language.
2105
+
2106
+ **📖 Real-World Example**: See [Forge-Secure-Notes-for-Jira](https://github.com/vzakharchenko/Forge-Secure-Notes-for-Jira) for a complete implementation of Rovo AI agent with secure natural-language analytics.
2107
+
2108
+ ### Key Features
2109
+
2110
+ - **Security-First Design**: Multiple layers of security validations to prevent SQL injection and unauthorized data access
2111
+ - **Single Table Isolation**: Queries are restricted to a single table to prevent cross-table data access
2112
+ - **Row-Level Security (RLS)**: Built-in support for data isolation based on user context
2113
+ - **Comprehensive Validation**: Blocks JOINs, subqueries, window functions, and other potentially unsafe operations
2114
+ - **Post-Execution Validation**: Verifies query results to ensure security fields are present and come from the correct table
2115
+ - **Type-Safe Configuration**: Uses Drizzle ORM table objects for type-safe column references
2116
+
2117
+ ### Security Validations
2118
+
2119
+ Rovo performs multiple security checks before and after query execution:
2120
+
2121
+ 1. **Query Type Validation**: Only SELECT queries are allowed
2122
+ 2. **Table Restriction**: Queries must target only the specified table
2123
+ 3. **JOIN Detection**: JOINs are blocked using EXPLAIN analysis
2124
+ 4. **Subquery Detection**: Scalar subqueries in SELECT columns are blocked
2125
+ 5. **Window Function Detection**: Window functions are blocked for security
2126
+ 6. **Execution Plan Validation**: Verifies that only the expected table is accessed
2127
+ 7. **RLS Field Validation**: Ensures required security fields are present in results
2128
+ 8. **Post-Execution Validation**: Verifies all fields come from the correct table
2129
+
2130
+ ### Basic Usage
2131
+
2132
+ ```typescript
2133
+ import ForgeSQL from "forge-sql-orm";
2134
+
2135
+ const forgeSQL = new ForgeSQL();
2136
+
2137
+ // Get Rovo instance
2138
+ const rovo = forgeSQL.rovo();
2139
+
2140
+ // Create settings builder using Drizzle table object
2141
+ const settings = await rovo
2142
+ .rovoSettingBuilder(usersTable, accountId)
2143
+ .addContextParameter(":currentUserId", accountId)
2144
+ .useRLS()
2145
+ .addRlsColumn(usersTable.id)
2146
+ .addRlsWherePart((alias) => `${alias}.${usersTable.id.name} = '${accountId}'`)
2147
+ .finish()
2148
+ .build();
2149
+
2150
+ // Execute dynamic SQL query
2151
+ const result = await rovo.dynamicIsolatedQuery(
2152
+ "SELECT id, name FROM users WHERE status = 'active' AND userId = :currentUserId",
2153
+ settings,
2154
+ );
2155
+
2156
+ console.log(result.rows); // Query results
2157
+ console.log(result.metadata); // Query metadata
2158
+ ```
2159
+
2160
+ ### Row-Level Security (RLS) Configuration
2161
+
2162
+ RLS allows you to filter data based on user context, ensuring users can only access their own data:
2163
+
2164
+ ```typescript
2165
+ const rovo = forgeSQL.rovo();
2166
+
2167
+ // Configure RLS with conditional activation and multiple security fields
2168
+ const settings = await rovo
2169
+ .rovoSettingBuilder(securityNotesTable, accountId)
2170
+ .addContextParameter(":currentUserId", accountId)
2171
+ .addContextParameter(":currentProjectKey", projectKey)
2172
+ .addContextParameter(":currentIssueKey", issueKey)
2173
+ .useRLS()
2174
+ .addRlsCondition(async () => {
2175
+ // Conditionally enable RLS based on user role
2176
+ const userService = getUserService();
2177
+ return !(await userService.isAdmin()); // Only apply RLS for non-admin users
2178
+ })
2179
+ .addRlsColumn(securityNotesTable.createdBy) // Required field for RLS validation
2180
+ .addRlsColumn(securityNotesTable.targetUserId) // Additional security field
2181
+ .addRlsWherePart(
2182
+ (alias) =>
2183
+ `${alias}.${securityNotesTable.createdBy.name} = '${accountId}' OR ${alias}.${securityNotesTable.targetUserId.name} = '${accountId}'`,
2184
+ ) // RLS filter with OR condition
2185
+ .finish()
2186
+ .build();
2187
+
2188
+ // The query will automatically be wrapped with RLS filtering:
2189
+ // SELECT * FROM (original_query) AS t WHERE (t.createdBy = 'accountId' OR t.targetUserId = 'accountId')
2190
+ ```
2191
+
2192
+ ### Context Parameters
2193
+
2194
+ You can use context parameters for query substitution. Parameters use the `:parameterName` format (colon prefix, not double braces):
2195
+
2196
+ ```typescript
2197
+ const rovo = forgeSQL.rovo();
2198
+
2199
+ const settings = await rovo
2200
+ .rovoSettingBuilder(usersTable, accountId)
2201
+ .addContextParameter(":currentUserId", accountId)
2202
+ .addContextParameter(":projectKey", "PROJ-123")
2203
+ .addContextParameter(":status", "active")
2204
+ .useRLS()
2205
+ .addRlsColumn(usersTable.id)
2206
+ .addRlsWherePart((alias) => `${alias}.${usersTable.userId.name} = '${accountId}'`)
2207
+ .finish()
2208
+ .build();
2209
+
2210
+ // In the SQL query, parameters are replaced:
2211
+ const result = await rovo.dynamicIsolatedQuery(
2212
+ "SELECT * FROM users WHERE projectKey = :projectKey AND status = :status AND userId = :currentUserId",
2213
+ settings,
2214
+ );
2215
+ // Becomes: SELECT * FROM users WHERE projectKey = 'PROJ-123' AND status = 'active' AND userId = 'accountId'
2216
+ ```
2217
+
2218
+ ### Using Raw Table Names
2219
+
2220
+ You can use `rovoRawSettingBuilder` with raw table name string:
2221
+
2222
+ ```typescript
2223
+ const rovo = forgeSQL.rovo();
2224
+
2225
+ // Using rovoRawSettingBuilder with raw table name
2226
+ const settings = await rovo
2227
+ .rovoRawSettingBuilder("users", accountId)
2228
+ .addContextParameter(":currentUserId", accountId)
2229
+ .useRLS()
2230
+ .addRlsColumnName("id")
2231
+ .addRlsWherePart((alias) => `${alias}.id = '${accountId}'`)
2232
+ .finish()
2233
+ .build();
2234
+
2235
+ const result = await rovo.dynamicIsolatedQuery(
2236
+ "SELECT id, name FROM users WHERE status = 'active' AND userId = :currentUserId",
2237
+ settings,
2238
+ );
2239
+ ```
2240
+
2241
+ ### Security Restrictions
2242
+
2243
+ Rovo blocks the following operations for security:
2244
+
2245
+ - **Data Modification**: Only SELECT queries are allowed
2246
+ - **JOINs**: JOIN operations are detected and blocked
2247
+ - **Subqueries**: Scalar subqueries in SELECT columns are blocked
2248
+ - **Window Functions**: Window functions (e.g., `COUNT(*) OVER(...)`) are blocked
2249
+ - **Multiple Tables**: Queries referencing multiple tables are blocked
2250
+ - **Table Aliases**: Post-execution validation ensures fields come from the correct table
2251
+
2252
+ ### Error Handling
2253
+
2254
+ Rovo provides detailed error messages when security violations are detected:
2255
+
2256
+ ```typescript
2257
+ try {
2258
+ const result = await rovo.dynamicIsolatedQuery(
2259
+ "SELECT * FROM users u JOIN orders o ON u.id = o.userId",
2260
+ settings,
2261
+ );
2262
+ } catch (error) {
2263
+ // Error: "Security violation: JOIN operations are not allowed..."
2264
+ console.error(error.message);
2265
+ }
2266
+ ```
2267
+
2268
+ ### Example: Real-World Function Implementation
2269
+
2270
+ > **💡 Full Example**: See the complete implementation in [Forge-Secure-Notes-for-Jira](https://github.com/vzakharchenko/Forge-Secure-Notes-for-Jira) repository.
2271
+
2272
+ ```typescript
2273
+ import ForgeSQL from "forge-sql-orm";
2274
+ import { Result } from "@forge/sql";
2275
+
2276
+ const FORGE_SQL_ORM = new ForgeSQL();
2277
+
2278
+ export async function runSecurityNotesQuery(
2279
+ event: {
2280
+ sql: string;
2281
+ context: {
2282
+ jira: {
2283
+ issueKey: string;
2284
+ projectKey: string;
2285
+ };
2286
+ };
2287
+ },
2288
+ context: { principal: { accountId: string } },
2289
+ ): Promise<Result<unknown>> {
2290
+ const rovoIntegration = FORGE_SQL_ORM.rovo();
2291
+ const accountId = context.principal.accountId;
2292
+
2293
+ const settings = await rovoIntegration
2294
+ .rovoSettingBuilder(securityNotesTable, accountId)
2295
+ .addContextParameter(":currentUserId", accountId)
2296
+ .addContextParameter(":currentProjectKey", event.context?.jira?.projectKey ?? "")
2297
+ .addContextParameter(":currentIssueKey", event.context?.jira?.issueKey ?? "")
2298
+ .useRLS()
2299
+ .addRlsCondition(async () => {
2300
+ // Conditionally disable RLS for admin users
2301
+ const userService = getUserService();
2302
+ return !(await userService.isAdmin());
2303
+ })
2304
+ .addRlsColumn(securityNotesTable.createdBy)
2305
+ .addRlsColumn(securityNotesTable.targetUserId)
2306
+ .addRlsWherePart(
2307
+ (alias: string) =>
2308
+ `${alias}.${securityNotesTable.createdBy.name} = '${accountId}' OR ${alias}.${securityNotesTable.targetUserId.name} = '${accountId}'`,
2309
+ )
2310
+ .finish()
2311
+ .build();
2312
+
2313
+ return await rovoIntegration.dynamicIsolatedQuery(event.sql, settings);
2314
+ }
2315
+ ```
2316
+
2014
2317
  ## ForgeSqlOrmOptions
2015
2318
 
2016
2319
  The `ForgeSqlOrmOptions` object allows customization of ORM behavior:
@@ -2363,6 +2666,227 @@ The error analysis mechanism:
2363
2666
 
2364
2667
  > **💡 Tip**: The automatic error analysis only triggers for timeout and OOM errors. Other errors are logged normally without plan analysis.
2365
2668
 
2669
+ ### Resolver-Level Performance Monitoring
2670
+
2671
+ The `executeWithMetadata()` method provides resolver-level profiling with configurable query plan printing. It aggregates metrics across all database operations within a resolver and supports two modes for query plan analysis.
2672
+
2673
+ #### Basic Usage
2674
+
2675
+ ```typescript
2676
+ const result = await forgeSQL.executeWithMetadata(
2677
+ async () => {
2678
+ const users = await forgeSQL.selectFrom(usersTable);
2679
+ const orders = await forgeSQL
2680
+ .selectFrom(ordersTable)
2681
+ .where(eq(ordersTable.userId, usersTable.id));
2682
+ return { users, orders };
2683
+ },
2684
+ async (totalDbExecutionTime, totalResponseSize, printQueriesWithPlan) => {
2685
+ const threshold = 500; // ms baseline for this resolver
2686
+
2687
+ if (totalDbExecutionTime > threshold * 1.5) {
2688
+ console.warn(`[Performance Warning] Resolver exceeded DB time: ${totalDbExecutionTime} ms`);
2689
+ await printQueriesWithPlan(); // Analyze and print query execution plans
2690
+ } else if (totalDbExecutionTime > threshold) {
2691
+ console.debug(`[Performance Debug] High DB time: ${totalDbExecutionTime} ms`);
2692
+ }
2693
+
2694
+ console.log(`DB response size: ${totalResponseSize} bytes`);
2695
+ },
2696
+ );
2697
+ ```
2698
+
2699
+ #### Query Plan Printing Options
2700
+
2701
+ The `printQueriesWithPlan` function supports two modes, configurable via the optional `options` parameter:
2702
+
2703
+ **1. TopSlowest Mode (default)**: Prints execution plans for the slowest queries from the current resolver invocation
2704
+
2705
+ ```typescript
2706
+ // Full configuration example
2707
+ const result = await forgeSQL.executeWithMetadata(
2708
+ async () => {
2709
+ const users = await forgeSQL.selectFrom(usersTable);
2710
+ return users;
2711
+ },
2712
+ async (totalDbExecutionTime, totalResponseSize, printQueriesWithPlan) => {
2713
+ if (totalDbExecutionTime > 1000) {
2714
+ await printQueriesWithPlan(); // Will print top 3 slowest queries with execution plans
2715
+ }
2716
+ },
2717
+ {
2718
+ mode: "TopSlowest", // Print top slowest queries (default)
2719
+ topQueries: 3, // Number of top slowest queries to analyze (default: 1)
2720
+ showSlowestPlans: true, // Show execution plans (default: true)
2721
+ },
2722
+ );
2723
+
2724
+ // Minimal configuration - only specify what you need
2725
+ const result2 = await forgeSQL.executeWithMetadata(
2726
+ async () => {
2727
+ const users = await forgeSQL.selectFrom(usersTable);
2728
+ return users;
2729
+ },
2730
+ async (totalDbExecutionTime, totalResponseSize, printQueriesWithPlan) => {
2731
+ if (totalDbExecutionTime > 1000) {
2732
+ await printQueriesWithPlan(); // Will print top 3 slowest queries (all other options use defaults)
2733
+ }
2734
+ },
2735
+ {
2736
+ topQueries: 3, // Only specify topQueries, mode and showSlowestPlans use defaults
2737
+ },
2738
+ );
2739
+
2740
+ // Disable execution plans - only show SQL and execution time
2741
+ const result3 = await forgeSQL.executeWithMetadata(
2742
+ async () => {
2743
+ const users = await forgeSQL.selectFrom(usersTable);
2744
+ return users;
2745
+ },
2746
+ async (totalDbExecutionTime, totalResponseSize, printQueriesWithPlan) => {
2747
+ if (totalDbExecutionTime > 1000) {
2748
+ await printQueriesWithPlan(); // Will print SQL and time only, no execution plans
2749
+ }
2750
+ },
2751
+ {
2752
+ showSlowestPlans: false, // Disable execution plan printing
2753
+ },
2754
+ );
2755
+
2756
+ // Use all defaults - pass empty object or omit options parameter
2757
+ const result4 = await forgeSQL.executeWithMetadata(
2758
+ async () => {
2759
+ const users = await forgeSQL.selectFrom(usersTable);
2760
+ return users;
2761
+ },
2762
+ async (totalDbExecutionTime, totalResponseSize, printQueriesWithPlan) => {
2763
+ if (totalDbExecutionTime > 1000) {
2764
+ await printQueriesWithPlan(); // Uses all defaults: TopSlowest mode, topQueries: 1, showSlowestPlans: true
2765
+ }
2766
+ },
2767
+ {}, // Empty object - all options use defaults
2768
+ );
2769
+ ```
2770
+
2771
+ <|tool▁calls▁begin|><|tool▁call▁begin|>
2772
+ read_file
2773
+
2774
+ **2. SummaryTable Mode**: Uses `CLUSTER_STATEMENTS_SUMMARY` for query analysis
2775
+
2776
+ ```typescript
2777
+ const result = await forgeSQL.executeWithMetadata(
2778
+ async () => {
2779
+ const users = await forgeSQL.selectFrom(usersTable);
2780
+ return users;
2781
+ },
2782
+ async (totalDbExecutionTime, totalResponseSize, printQueriesWithPlan) => {
2783
+ if (totalDbExecutionTime > 1000) {
2784
+ await printQueriesWithPlan(); // Will use CLUSTER_STATEMENTS_SUMMARY if within time window
2785
+ }
2786
+ },
2787
+ {
2788
+ mode: "SummaryTable", // Use SummaryTable mode
2789
+ summaryTableWindowTime: 10000, // Time window in milliseconds (default: 15000ms)
2790
+ },
2791
+ );
2792
+ ```
2793
+
2794
+ #### Configuration Options
2795
+
2796
+ All options are **optional**. If not specified, default values are used. You can pass only the options you need to customize.
2797
+
2798
+ | Option | Type | Default | Description |
2799
+ | ------------------------ | -------------------------------- | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
2800
+ | `mode` | `'TopSlowest' \| 'SummaryTable'` | `'TopSlowest'` | Query plan printing mode. `'TopSlowest'` prints execution plans for the slowest queries from the current resolver. `'SummaryTable'` uses `CLUSTER_STATEMENTS_SUMMARY` when within time window |
2801
+ | `summaryTableWindowTime` | `number` | `15000` | Time window in milliseconds for summary table queries. Only used when `mode` is `'SummaryTable'` |
2802
+ | `topQueries` | `number` | `1` | Number of top slowest queries to analyze when `mode` is `'TopSlowest'` |
2803
+ | `showSlowestPlans` | `boolean` | `true` | Whether to show execution plans for slowest queries in TopSlowest mode. If `false`, only SQL and execution time are printed |
2804
+ | `normalizeQuery` | `boolean` | `true` | Whether to normalize SQL queries by replacing parameter values with `?` placeholders. Set to `false` to disable normalization if it causes issues with complex queries |
2805
+
2806
+ **Examples:**
2807
+
2808
+ ```typescript
2809
+ // Use all defaults - omit options or pass empty object
2810
+ await forgeSQL.executeWithMetadata(queryFn, onMetadataFn); // or { }
2811
+
2812
+ // Customize only what you need
2813
+ await forgeSQL.executeWithMetadata(queryFn, onMetadataFn, { topQueries: 3 });
2814
+ await forgeSQL.executeWithMetadata(queryFn, onMetadataFn, { mode: "SummaryTable" });
2815
+ await forgeSQL.executeWithMetadata(queryFn, onMetadataFn, { showSlowestPlans: false });
2816
+ await forgeSQL.executeWithMetadata(queryFn, onMetadataFn, { normalizeQuery: false }); // Disable query normalization
2817
+
2818
+ // Combine multiple options
2819
+ await forgeSQL.executeWithMetadata(queryFn, onMetadataFn, {
2820
+ mode: "TopSlowest",
2821
+ topQueries: 5,
2822
+ showSlowestPlans: false,
2823
+ normalizeQuery: true, // Enable query normalization (default)
2824
+ });
2825
+ ```
2826
+
2827
+ #### How It Works
2828
+
2829
+ 1. **TopSlowest Mode** (default):
2830
+ - Collects all queries executed within the resolver
2831
+ - Sorts them by execution time (slowest first)
2832
+ - Prints execution plans for the top N queries (configurable via `topQueries`)
2833
+ - If `showSlowestPlans` is `false`, only prints SQL and execution time without plans
2834
+ - Works immediately after query execution
2835
+
2836
+ 2. **SummaryTable Mode**:
2837
+ - Attempts to use `CLUSTER_STATEMENTS_SUMMARY` for query analysis
2838
+ - Only works if queries are executed within the specified time window (`summaryTableWindowTime`)
2839
+ - If the time window expires, falls back to TopSlowest mode
2840
+ - Provides aggregated statistics from TiDB's system tables
2841
+
2842
+ #### Example: Real-World Resolver
2843
+
2844
+ ```typescript
2845
+ resolver.define("fetch", async (req: Request) => {
2846
+ try {
2847
+ return await forgeSQL.executeWithMetadata(
2848
+ async () => {
2849
+ const users = await forgeSQL.selectFrom(demoUsers);
2850
+ const orders = await forgeSQL
2851
+ .selectFrom(demoOrders)
2852
+ .where(eq(demoOrders.userId, demoUsers.id));
2853
+ return { users, orders };
2854
+ },
2855
+ async (totalDbExecutionTime, totalResponseSize, printQueriesWithPlan) => {
2856
+ const threshold = 500; // ms baseline for this resolver
2857
+
2858
+ if (totalDbExecutionTime > threshold * 1.5) {
2859
+ console.warn(
2860
+ `[Performance Warning fetch] Resolver exceeded DB time: ${totalDbExecutionTime} ms`,
2861
+ );
2862
+ await printQueriesWithPlan(); // Analyze and print query execution plans
2863
+ } else if (totalDbExecutionTime > threshold) {
2864
+ console.debug(`[Performance Debug] High DB time: ${totalDbExecutionTime} ms`);
2865
+ }
2866
+ },
2867
+ {
2868
+ mode: "TopSlowest", // Print top slowest queries (default)
2869
+ topQueries: 2, // Print top 2 slowest queries
2870
+ },
2871
+ );
2872
+ } catch (e) {
2873
+ const error = e?.cause?.debug?.sqlMessage ?? e?.cause;
2874
+ console.error(error, e);
2875
+ throw error;
2876
+ }
2877
+ });
2878
+ ```
2879
+
2880
+ #### Benefits
2881
+
2882
+ - **Resolver-Level Profiling**: Aggregates metrics across all database operations in a resolver
2883
+ - **Configurable Analysis**: Choose between TopSlowest mode or SummaryTable mode
2884
+ - **Automatic Plan Formatting**: Execution plans are formatted in a readable format
2885
+ - **Performance Thresholds**: Set custom thresholds for performance warnings
2886
+ - **Zero Configuration**: Works out of the box with sensible defaults
2887
+
2888
+ > **💡 Tip**: When multiple resolvers are running concurrently, their query data may also appear in `printQueriesWithPlan()` analysis when using SummaryTable mode, as it queries the global `CLUSTER_STATEMENTS_SUMMARY` table.
2889
+
2366
2890
  ### Slow Query Monitoring
2367
2891
 
2368
2892
  Forge-SQL-ORM provides a scheduler trigger (`slowQuerySchedulerTrigger`) that automatically monitors and analyzes slow queries on an hourly basis. This trigger queries TiDB's slow query log system table and provides detailed performance information including SQL query text, memory usage, execution time, and execution plans.
@@ -2653,6 +3177,11 @@ const usersWithMetadata = await forgeSQL.executeWithMetadata(
2653
3177
 
2654
3178
  console.log(`DB response size: ${totalResponseSize} bytes`);
2655
3179
  },
3180
+ {
3181
+ // Optional: Configure query plan printing
3182
+ mode: "TopSlowest", // Print top slowest queries (default)
3183
+ topQueries: 1, // Print top slowest query
3184
+ },
2656
3185
  );
2657
3186
 
2658
3187
  // ✅ DDL operations for schema modifications