forge-sql-orm 2.1.12 → 2.1.14

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 (79) hide show
  1. package/README.md +922 -549
  2. package/dist/core/ForgeSQLAnalyseOperations.d.ts.map +1 -1
  3. package/dist/core/ForgeSQLAnalyseOperations.js +257 -0
  4. package/dist/core/ForgeSQLAnalyseOperations.js.map +1 -0
  5. package/dist/core/ForgeSQLCacheOperations.js +172 -0
  6. package/dist/core/ForgeSQLCacheOperations.js.map +1 -0
  7. package/dist/core/ForgeSQLCrudOperations.js +349 -0
  8. package/dist/core/ForgeSQLCrudOperations.js.map +1 -0
  9. package/dist/core/ForgeSQLORM.d.ts +29 -1
  10. package/dist/core/ForgeSQLORM.d.ts.map +1 -1
  11. package/dist/core/ForgeSQLORM.js +1252 -0
  12. package/dist/core/ForgeSQLORM.js.map +1 -0
  13. package/dist/core/ForgeSQLQueryBuilder.d.ts +179 -1
  14. package/dist/core/ForgeSQLQueryBuilder.d.ts.map +1 -1
  15. package/dist/core/ForgeSQLQueryBuilder.js +77 -0
  16. package/dist/core/ForgeSQLQueryBuilder.js.map +1 -0
  17. package/dist/core/ForgeSQLSelectOperations.js +81 -0
  18. package/dist/core/ForgeSQLSelectOperations.js.map +1 -0
  19. package/dist/core/Rovo.d.ts +116 -0
  20. package/dist/core/Rovo.d.ts.map +1 -0
  21. package/dist/core/Rovo.js +647 -0
  22. package/dist/core/Rovo.js.map +1 -0
  23. package/dist/core/SystemTables.js +258 -0
  24. package/dist/core/SystemTables.js.map +1 -0
  25. package/dist/index.js +30 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/lib/drizzle/extensions/additionalActions.d.ts.map +1 -1
  28. package/dist/lib/drizzle/extensions/additionalActions.js +527 -0
  29. package/dist/lib/drizzle/extensions/additionalActions.js.map +1 -0
  30. package/dist/utils/cacheContextUtils.d.ts.map +1 -1
  31. package/dist/utils/cacheContextUtils.js +198 -0
  32. package/dist/utils/cacheContextUtils.js.map +1 -0
  33. package/dist/utils/cacheUtils.d.ts.map +1 -1
  34. package/dist/utils/cacheUtils.js +383 -0
  35. package/dist/utils/cacheUtils.js.map +1 -0
  36. package/dist/utils/forgeDriver.d.ts.map +1 -1
  37. package/dist/utils/forgeDriver.js +139 -0
  38. package/dist/utils/forgeDriver.js.map +1 -0
  39. package/dist/utils/forgeDriverProxy.js +68 -0
  40. package/dist/utils/forgeDriverProxy.js.map +1 -0
  41. package/dist/utils/metadataContextUtils.d.ts.map +1 -1
  42. package/dist/utils/metadataContextUtils.js +26 -0
  43. package/dist/utils/metadataContextUtils.js.map +1 -0
  44. package/dist/utils/requestTypeContextUtils.js +10 -0
  45. package/dist/utils/requestTypeContextUtils.js.map +1 -0
  46. package/dist/utils/sqlHints.js +52 -0
  47. package/dist/utils/sqlHints.js.map +1 -0
  48. package/dist/utils/sqlUtils.d.ts.map +1 -1
  49. package/dist/utils/sqlUtils.js +590 -0
  50. package/dist/utils/sqlUtils.js.map +1 -0
  51. package/dist/webtriggers/applyMigrationsWebTrigger.js +77 -0
  52. package/dist/webtriggers/applyMigrationsWebTrigger.js.map +1 -0
  53. package/dist/webtriggers/clearCacheSchedulerTrigger.js +83 -0
  54. package/dist/webtriggers/clearCacheSchedulerTrigger.js.map +1 -0
  55. package/dist/webtriggers/dropMigrationWebTrigger.js +54 -0
  56. package/dist/webtriggers/dropMigrationWebTrigger.js.map +1 -0
  57. package/dist/webtriggers/dropTablesMigrationWebTrigger.js +54 -0
  58. package/dist/webtriggers/dropTablesMigrationWebTrigger.js.map +1 -0
  59. package/dist/webtriggers/fetchSchemaWebTrigger.js +82 -0
  60. package/dist/webtriggers/fetchSchemaWebTrigger.js.map +1 -0
  61. package/dist/webtriggers/index.js +40 -0
  62. package/dist/webtriggers/index.js.map +1 -0
  63. package/dist/webtriggers/slowQuerySchedulerTrigger.js +80 -0
  64. package/dist/webtriggers/slowQuerySchedulerTrigger.js.map +1 -0
  65. package/package.json +31 -25
  66. package/src/core/ForgeSQLAnalyseOperations.ts +3 -2
  67. package/src/core/ForgeSQLORM.ts +64 -0
  68. package/src/core/ForgeSQLQueryBuilder.ts +200 -1
  69. package/src/core/Rovo.ts +765 -0
  70. package/src/lib/drizzle/extensions/additionalActions.ts +11 -0
  71. package/src/utils/cacheContextUtils.ts +9 -6
  72. package/src/utils/cacheUtils.ts +6 -4
  73. package/src/utils/forgeDriver.ts +3 -7
  74. package/src/utils/metadataContextUtils.ts +1 -3
  75. package/src/utils/sqlUtils.ts +33 -34
  76. package/dist/ForgeSQLORM.js +0 -3922
  77. package/dist/ForgeSQLORM.js.map +0 -1
  78. package/dist/ForgeSQLORM.mjs +0 -3905
  79. package/dist/ForgeSQLORM.mjs.map +0 -1
package/README.md CHANGED
@@ -11,10 +11,10 @@
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
13
 
14
-
15
14
  **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.
16
15
 
17
16
  ## Key Features
17
+
18
18
  - ✅ **Custom Drizzle Driver** for direct integration with @forge/sql
19
19
  - ✅ **Local Cache System (Level 1)** for in-memory query optimization within single resolver invocation scope
20
20
  - ✅ **Global Cache System (Level 2)** with cross-invocation caching, automatic cache invalidation and context-aware operations (using [@forge/kvs](https://developer.atlassian.com/platform/forge/storage-reference/storage-api-custom-entities/) )
@@ -33,10 +33,12 @@
33
33
  - ✅ **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
34
  - ✅ **Optimistic Locking** Ensures data consistency by preventing conflicts when multiple users update the same record
35
35
  - ✅ **Query Plan Analysis**: Detailed execution plan analysis and optimization insights
36
+ - ✅ **Rovo Integration** Secure pattern for natural-language analytics with comprehensive security validations, Row-Level Security (RLS) support, and dynamic SQL query execution
36
37
 
37
38
  ## Table of Contents
38
39
 
39
40
  ### 🚀 Getting Started
41
+
40
42
  - [Key Features](#key-features)
41
43
  - [Usage Approaches](#usage-approaches)
42
44
  - [Installation](#installation)
@@ -44,16 +46,19 @@
44
46
  - [Quick Start](#quick-start)
45
47
 
46
48
  ### 📖 Core Features
49
+
47
50
  - [Field Name Collision Prevention](#field-name-collision-prevention-in-complex-queries)
48
51
  - [Drizzle Usage with forge-sql-orm](#drizzle-usage-with-forge-sql-orm)
49
52
  - [Direct Drizzle Usage with Custom Driver](#direct-drizzle-usage-with-custom-driver)
50
53
 
51
54
  ### 🗄️ Database Operations
55
+
52
56
  - [Fetch Data](#fetch-data)
53
57
  - [Modify Operations](#modify-operations)
54
58
  - [SQL Utilities](#sql-utilities)
55
59
 
56
60
  ### ⚡ Caching System
61
+
57
62
  - [Setting Up Caching with @forge/kvs](#setting-up-caching-with-forgekvs-optional)
58
63
  - [Global Cache System (Level 2)](#global-cache-system-level-2)
59
64
  - [Cache Context Operations](#cache-context-operations)
@@ -62,19 +67,23 @@
62
67
  - [Manual Cache Management](#manual-cache-management)
63
68
 
64
69
  ### 🔒 Advanced Features
70
+
65
71
  - [Optimistic Locking](#optimistic-locking)
72
+ - [Rovo Integration](#rovo-integration) - Secure pattern for natural-language analytics with dynamic SQL queries
66
73
  - [Query Analysis and Performance Optimization](#query-analysis-and-performance-optimization)
67
74
  - [Automatic Error Analysis](#automatic-error-analysis) - Automatic timeout and OOM error detection with execution plans
68
75
  - [Slow Query Monitoring](#slow-query-monitoring) - Scheduled monitoring of slow queries with execution plans
69
76
  - [Date and Time Types](#date-and-time-types)
70
77
 
71
78
  ### 🛠️ Development Tools
79
+
72
80
  - [CLI Commands](#cli-commands) | [CLI Documentation](forge-sql-orm-cli/README.md)
73
81
  - [Web Triggers for Migrations](#web-triggers-for-migrations)
74
82
  - [Step-by-Step Migration Workflow](#step-by-step-migration-workflow)
75
83
  - [Drop Migrations](#drop-migrations)
76
84
 
77
85
  ### 📚 Examples
86
+
78
87
  - [Simple Example](examples/forge-sql-orm-example-simple)
79
88
  - [Drizzle Driver Example](examples/forge-sql-orm-example-drizzle-driver-simple)
80
89
  - [Optimistic Locking Example](examples/forge-sql-orm-example-optimistic-locking)
@@ -83,52 +92,62 @@
83
92
  - [Organization Tracker Example](examples/forge-sql-orm-example-org-tracker)
84
93
  - [Checklist Example](examples/forge-sql-orm-example-checklist)
85
94
  - [Cache Example](examples/forge-sql-orm-example-cache) - Advanced caching capabilities with performance monitoring
95
+ - [Rovo Integration Example](https://github.com/vzakharchenko/Forge-Secure-Notes-for-Jira) - Real-world Rovo AI agent implementation with secure natural-language analytics
86
96
 
87
97
  ### 📚 Reference
98
+
88
99
  - [ForgeSqlOrmOptions](#forgesqlormoptions)
89
100
  - [Migration Guide](#migration-guide)
90
101
 
91
102
  ## 🚀 Quick Navigation
92
103
 
93
104
  **New to Forge-SQL-ORM?** Start here:
105
+
94
106
  - [Quick Start](#quick-start) - Get up and running in 5 minutes
95
107
  - [Installation](#installation) - Complete setup guide
96
108
  - [Basic Usage Examples](#fetch-data) - Simple query examples
97
109
 
98
110
  **Looking for specific features?**
111
+
99
112
  - [Global Cache System (Level 2)](#global-cache-system-level-2) - Cross-invocation persistent caching
100
113
  - [Local Cache System (Level 1)](#local-cache-operations-level-1) - In-memory invocation caching
101
114
  - [Optimistic Locking](#optimistic-locking) - Data consistency
115
+ - [Rovo Integration](#rovo-integration) - Secure natural-language analytics
102
116
  - [Migration Tools](#web-triggers-for-migrations) - Database migrations
103
117
  - [Query Analysis](#query-analysis-and-performance-optimization) - Performance optimization
104
118
 
105
119
  **Looking for practical examples?**
120
+
106
121
  - [Simple Example](examples/forge-sql-orm-example-simple) - Basic ORM usage
107
122
  - [Optimistic Locking Example](examples/forge-sql-orm-example-optimistic-locking) - Real-world conflict handling
108
123
  - [Organization Tracker Example](examples/forge-sql-orm-example-org-tracker) - Complex relationships
109
124
  - [Checklist Example](examples/forge-sql-orm-example-checklist) - Jira integration
110
125
  - [Cache Example](examples/forge-sql-orm-example-cache) - Advanced caching capabilities
126
+ - [Rovo Integration Example](https://github.com/vzakharchenko/Forge-Secure-Notes-for-Jira) - Real-world Rovo AI agent with secure analytics
111
127
 
112
128
  ## Usage Approaches
113
129
 
114
-
115
130
  ### 1. Full Forge-SQL-ORM Usage
131
+
116
132
  ```typescript
117
133
  import ForgeSQL from "forge-sql-orm";
118
134
  const forgeSQL = new ForgeSQL();
119
135
  ```
136
+
120
137
  Best for: Advanced features like optimistic locking, automatic versioning, and automatic field name collision prevention in complex queries.
121
138
 
122
139
  ### 2. Direct Drizzle Usage
140
+
123
141
  ```typescript
124
142
  import { drizzle } from "drizzle-orm/mysql-proxy";
125
143
  import { forgeDriver } from "forge-sql-orm";
126
144
  const db = drizzle(forgeDriver);
127
145
  ```
128
- Best for: Simple Modify operations without optimistic locking. Note that you need to manually patch drizzle `patchDbWithSelectAliased` for select fields to prevent field name collisions in Atlassian Forge SQL.
129
146
 
147
+ Best for: Simple Modify operations without optimistic locking. Note that you need to manually patch drizzle `patchDbWithSelectAliased` for select fields to prevent field name collisions in Atlassian Forge SQL.
130
148
 
131
149
  ### 3. Local Cache Optimization
150
+
132
151
  ```typescript
133
152
  import ForgeSQL from "forge-sql-orm";
134
153
  const forgeSQL = new ForgeSQL();
@@ -136,28 +155,28 @@ const forgeSQL = new ForgeSQL();
136
155
  // Optimize repeated queries within a single invocation
137
156
  await forgeSQL.executeWithLocalContext(async () => {
138
157
  // Multiple queries here will benefit from local caching
139
- const users = await forgeSQL.select({ id: users.id, name: users.name })
140
- .from(users).where(eq(users.active, true));
141
-
158
+ const users = await forgeSQL
159
+ .select({ id: users.id, name: users.name })
160
+ .from(users)
161
+ .where(eq(users.active, true));
162
+
142
163
  // This query will use local cache (no database call)
143
- const cachedUsers = await forgeSQL.select({ id: users.id, name: users.name })
144
- .from(users).where(eq(users.active, true));
145
-
146
- // Using new methods for better performance
147
- const usersFrom = await forgeSQL.selectFrom(users)
164
+ const cachedUsers = await forgeSQL
165
+ .select({ id: users.id, name: users.name })
166
+ .from(users)
148
167
  .where(eq(users.active, true));
149
-
168
+
169
+ // Using new methods for better performance
170
+ const usersFrom = await forgeSQL.selectFrom(users).where(eq(users.active, true));
171
+
150
172
  // This will use local cache (no database call)
151
- const cachedUsersFrom = await forgeSQL.selectFrom(users)
152
- .where(eq(users.active, true));
153
-
173
+ const cachedUsersFrom = await forgeSQL.selectFrom(users).where(eq(users.active, true));
174
+
154
175
  // Raw SQL with local caching
155
- const rawUsers = await forgeSQL.execute(
156
- "SELECT id, name FROM users WHERE active = ?",
157
- [true]
158
- );
176
+ const rawUsers = await forgeSQL.execute("SELECT id, name FROM users WHERE active = ?", [true]);
159
177
  });
160
178
  ```
179
+
161
180
  Best for: Performance optimization of repeated queries within resolvers or single invocation contexts.
162
181
 
163
182
  ## Field Name Collision Prevention in Complex Queries
@@ -167,6 +186,7 @@ When working with complex queries involving multiple tables (joins, inner joins,
167
186
  Forge-SQL-ORM provides two ways to handle this:
168
187
 
169
188
  ### Using Forge-SQL-ORM
189
+
170
190
  ```typescript
171
191
  import ForgeSQL from "forge-sql-orm";
172
192
 
@@ -174,12 +194,13 @@ const forgeSQL = new ForgeSQL();
174
194
 
175
195
  // Automatic field name collision prevention
176
196
  await forgeSQL
177
- .select({user: users, order: orders})
197
+ .select({ user: users, order: orders })
178
198
  .from(orders)
179
199
  .innerJoin(users, eq(orders.userId, users.id));
180
200
  ```
181
201
 
182
202
  ### Using Direct Drizzle
203
+
183
204
  ```typescript
184
205
  import { drizzle } from "drizzle-orm/mysql-proxy";
185
206
  import { forgeDriver, patchDbWithSelectAliased } from "forge-sql-orm";
@@ -188,18 +209,18 @@ const db = patchDbWithSelectAliased(drizzle(forgeDriver));
188
209
 
189
210
  // Manual field name collision prevention
190
211
  await db
191
- .selectAliased({user: users, order: orders})
212
+ .selectAliased({ user: users, order: orders })
192
213
  .from(orders)
193
214
  .innerJoin(users, eq(orders.userId, users.id));
194
215
  ```
195
216
 
196
217
  ### Important Notes
218
+
197
219
  - This is a specific behavior of Atlassian Forge SQL, not Drizzle ORM
198
220
  - For complex queries involving multiple tables, it's recommended to always specify select fields and avoid using `select()` without field selection
199
221
  - The solution automatically creates unique aliases for each field by prefixing them with the table name
200
222
  - This ensures that fields with the same name from different tables remain distinct in the query results
201
223
 
202
-
203
224
  ## Installation
204
225
 
205
226
  Forge-SQL-ORM is designed to work with @forge/sql and requires some additional setup to ensure compatibility within Atlassian Forge.
@@ -207,16 +228,35 @@ Forge-SQL-ORM is designed to work with @forge/sql and requires some additional s
207
228
  ✅ Step 1: Install Dependencies
208
229
 
209
230
  **Basic installation (without caching):**
231
+
210
232
  ```sh
211
233
  npm install forge-sql-orm @forge/sql drizzle-orm -S
212
234
  ```
213
235
 
214
236
  **With caching support:**
237
+
215
238
  ```sh
216
239
  npm install forge-sql-orm @forge/sql @forge/kvs drizzle-orm -S
217
240
  ```
218
241
 
242
+ **⚠️ Important for UI-Kit projects:**
243
+
244
+ If you're installing `forge-sql-orm` in a UI-Kit project (projects using `@forge/react`), you may encounter peer dependency conflicts with `@types/react`. This is due to a conflict between `@types/react@18` (required by `@forge/react`) and `@types/react@19` (optional peer dependency from `drizzle-orm` via `bun-types`).
245
+
246
+ To resolve this, use the `--legacy-peer-deps` flag:
247
+
248
+ ```sh
249
+ # Basic installation for UI-Kit projects
250
+ npm install forge-sql-orm @forge/sql drizzle-orm -S --legacy-peer-deps
251
+
252
+ # With caching support for UI-Kit projects
253
+ npm install forge-sql-orm @forge/sql @forge/kvs drizzle-orm -S --legacy-peer-deps
254
+ ```
255
+
256
+ **Note:** The `--legacy-peer-deps` flag tells npm to ignore peer dependency conflicts. This is safe in this case because `bun-types` is an optional peer dependency and doesn't affect the functionality of `forge-sql-orm` in Forge environments.
257
+
219
258
  This will:
259
+
220
260
  - Install Forge-SQL-ORM (the ORM for @forge/sql)
221
261
  - Install @forge/sql, the Forge database layer
222
262
  - Install @forge/kvs, the Forge Key-Value Store for caching (optional, only needed for caching features)
@@ -227,6 +267,7 @@ This will:
227
267
  ## Quick Start
228
268
 
229
269
  ### 1. Basic Setup
270
+
230
271
  ```typescript
231
272
  import ForgeSQL from "forge-sql-orm";
232
273
 
@@ -238,44 +279,49 @@ const users = await forgeSQL.select().from(users);
238
279
  ```
239
280
 
240
281
  ### 2. With Caching (Optional)
282
+
241
283
  ```typescript
242
284
  import ForgeSQL from "forge-sql-orm";
243
285
 
244
286
  // Initialize with caching
245
287
  const forgeSQL = new ForgeSQL({
246
288
  cacheEntityName: "cache",
247
- cacheTTL: 300
289
+ cacheTTL: 300,
248
290
  });
249
291
 
250
292
  // Cached query
251
- const users = await forgeSQL.selectCacheable({ id: users.id, name: users.name })
252
- .from(users).where(eq(users.active, true));
293
+ const users = await forgeSQL
294
+ .selectCacheable({ id: users.id, name: users.name })
295
+ .from(users)
296
+ .where(eq(users.active, true));
253
297
  ```
254
298
 
255
299
  ### 3. Local Cache Optimization
300
+
256
301
  ```typescript
257
302
  // Optimize repeated queries within a single invocation
258
303
  await forgeSQL.executeWithLocalContext(async () => {
259
- const users = await forgeSQL.select({ id: users.id, name: users.name })
260
- .from(users).where(eq(users.active, true));
261
-
304
+ const users = await forgeSQL
305
+ .select({ id: users.id, name: users.name })
306
+ .from(users)
307
+ .where(eq(users.active, true));
308
+
262
309
  // This query will use local cache (no database call)
263
- const cachedUsers = await forgeSQL.select({ id: users.id, name: users.name })
264
- .from(users).where(eq(users.active, true));
265
-
266
- // Using new methods for better performance
267
- const usersFrom = await forgeSQL.selectFrom(users)
310
+ const cachedUsers = await forgeSQL
311
+ .select({ id: users.id, name: users.name })
312
+ .from(users)
268
313
  .where(eq(users.active, true));
269
-
314
+
315
+ // Using new methods for better performance
316
+ const usersFrom = await forgeSQL.selectFrom(users).where(eq(users.active, true));
317
+
270
318
  // Raw SQL with local caching
271
- const rawUsers = await forgeSQL.execute(
272
- "SELECT id, name FROM users WHERE active = ?",
273
- [true]
274
- );
319
+ const rawUsers = await forgeSQL.execute("SELECT id, name FROM users WHERE active = ?", [true]);
275
320
  });
276
321
  ```
277
322
 
278
323
  ### 4. Resolver Performance Monitoring
324
+
279
325
  ```typescript
280
326
  // Resolver with performance monitoring
281
327
  resolver.define("fetch", async (req: Request) => {
@@ -284,20 +330,23 @@ resolver.define("fetch", async (req: Request) => {
284
330
  async () => {
285
331
  // Resolver logic with multiple queries
286
332
  const users = await forgeSQL.selectFrom(demoUsers);
287
- const orders = await forgeSQL.selectFrom(demoOrders)
333
+ const orders = await forgeSQL
334
+ .selectFrom(demoOrders)
288
335
  .where(eq(demoOrders.userId, demoUsers.id));
289
336
  return { users, orders };
290
337
  },
291
338
  async (totalDbExecutionTime, totalResponseSize, printQueriesWithPlan) => {
292
339
  const threshold = 500; // ms baseline for this resolver
293
-
340
+
294
341
  if (totalDbExecutionTime > threshold * 1.5) {
295
- console.warn(`[Performance Warning fetch] Resolver exceeded DB time: ${totalDbExecutionTime} ms`);
342
+ console.warn(
343
+ `[Performance Warning fetch] Resolver exceeded DB time: ${totalDbExecutionTime} ms`,
344
+ );
296
345
  await printQueriesWithPlan(); // Optionally log or capture diagnostics for further analysis
297
346
  } else if (totalDbExecutionTime > threshold) {
298
347
  console.debug(`[Performance Debug fetch] High DB time: ${totalDbExecutionTime} ms`);
299
348
  }
300
- }
349
+ },
301
350
  );
302
351
  } catch (e) {
303
352
  const error = e?.cause?.debug?.sqlMessage ?? e?.cause;
@@ -307,11 +356,33 @@ resolver.define("fetch", async (req: Request) => {
307
356
  });
308
357
  ```
309
358
 
310
- ### 5. Next Steps
359
+ ### 5. Rovo Integration (Secure Analytics)
360
+
361
+ ```typescript
362
+ // Secure dynamic SQL queries for natural-language analytics
363
+ const rovo = forgeSQL.rovo();
364
+ const settings = await rovo
365
+ .rovoSettingBuilder(usersTable, accountId)
366
+ .addContextParameter(":currentUserId", accountId)
367
+ .useRLS()
368
+ .addRlsColumn(usersTable.id)
369
+ .addRlsWherePart((alias) => `${alias}.${usersTable.id.name} = '${accountId}'`)
370
+ .finish()
371
+ .build();
372
+
373
+ const result = await rovo.dynamicIsolatedQuery(
374
+ "SELECT id, name FROM users WHERE status = 'active' AND userId = :currentUserId",
375
+ settings,
376
+ );
377
+ ```
378
+
379
+ ### 6. Next Steps
380
+
311
381
  - [Full Installation Guide](#installation) - Complete setup instructions
312
382
  - [Core Features](#core-features) - Learn about key capabilities
313
383
  - [Global Cache System (Level 2)](#global-cache-system-level-2) - Cross-invocation caching features
314
384
  - [Local Cache System (Level 1)](#local-cache-operations-level-1) - In-memory caching features
385
+ - [Rovo Integration](#rovo-integration) - Secure natural-language analytics
315
386
  - [API Reference](#reference) - Complete API documentation
316
387
 
317
388
  ## Drizzle Usage with forge-sql-orm
@@ -343,48 +414,44 @@ const db = forgeSQL.getDrizzleQueryBuilder();
343
414
  const users = await db.select().from(users);
344
415
 
345
416
  // Using new methods for enhanced functionality
346
- const usersFrom = await forgeSQL.selectFrom(users)
347
- .where(eq(users.active, true));
417
+ const usersFrom = await forgeSQL.selectFrom(users).where(eq(users.active, true));
348
418
 
349
- const usersDistinct = await forgeSQL.selectDistinctFrom(users)
350
- .where(eq(users.active, true));
419
+ const usersDistinct = await forgeSQL.selectDistinctFrom(users).where(eq(users.active, true));
351
420
 
352
- const usersCacheable = await forgeSQL.selectCacheableFrom(users)
353
- .where(eq(users.active, true));
421
+ const usersCacheable = await forgeSQL.selectCacheableFrom(users).where(eq(users.active, true));
354
422
 
355
423
  // Raw SQL execution
356
- const rawUsers = await forgeSQL.execute(
357
- "SELECT * FROM users WHERE active = ?",
358
- [true]
359
- );
424
+ const rawUsers = await forgeSQL.execute("SELECT * FROM users WHERE active = ?", [true]);
360
425
 
361
426
  // Raw SQL with caching
362
427
  // ⚠️ IMPORTANT: When using executeCacheable(), all table names must be wrapped with backticks (`)
363
428
  const cachedRawUsers = await forgeSQL.executeCacheable(
364
- "SELECT * FROM `users` WHERE active = ?",
365
- [true],
366
- 300
429
+ "SELECT * FROM `users` WHERE active = ?",
430
+ [true],
431
+ 300,
367
432
  );
368
433
 
369
434
  // Raw SQL with execution metadata and performance monitoring
370
435
  const usersWithMetadata = await forgeSQL.executeWithMetadata(
371
436
  async () => {
372
437
  const users = await forgeSQL.selectFrom(usersTable);
373
- const orders = await forgeSQL.selectFrom(ordersTable).where(eq(ordersTable.userId, usersTable.id));
438
+ const orders = await forgeSQL
439
+ .selectFrom(ordersTable)
440
+ .where(eq(ordersTable.userId, usersTable.id));
374
441
  return { users, orders };
375
442
  },
376
443
  (totalDbExecutionTime, totalResponseSize, printQueriesWithPlan) => {
377
444
  const threshold = 500; // ms baseline for this resolver
378
-
445
+
379
446
  if (totalDbExecutionTime > threshold * 1.5) {
380
447
  console.warn(`[Performance Warning] Resolver exceeded DB time: ${totalDbExecutionTime} ms`);
381
448
  await printQueriesWithPlan(); // Analyze and print query execution plans
382
449
  } else if (totalDbExecutionTime > threshold) {
383
450
  console.debug(`[Performance Debug] High DB time: ${totalDbExecutionTime} ms`);
384
451
  }
385
-
452
+
386
453
  console.log(`DB response size: ${totalResponseSize} bytes`);
387
- }
454
+ },
388
455
  );
389
456
 
390
457
  // DDL operations for schema modifications
@@ -403,28 +470,44 @@ await forgeSQL.executeDDLActions(async () => {
403
470
  SELECT * FROM INFORMATION_SCHEMA.STATEMENTS_SUMMARY
404
471
  WHERE AVG_LATENCY > 1000000
405
472
  `);
406
-
473
+
407
474
  // Execute complex analysis queries in DDL context
408
475
  const performanceData = await forgeSQL.execute(`
409
476
  SELECT * FROM INFORMATION_SCHEMA.CLUSTER_STATEMENTS_SUMMARY_HISTORY
410
477
  WHERE SUMMARY_END_TIME > DATE_SUB(NOW(), INTERVAL 1 HOUR)
411
478
  `);
412
-
479
+
413
480
  return { slowQueries, performanceData };
414
481
  });
415
482
 
416
483
  // Common Table Expressions (CTEs)
417
484
  const userStats = await forgeSQL
418
485
  .with(
419
- forgeSQL.selectFrom(users).where(eq(users.active, true)).as('activeUsers'),
420
- forgeSQL.selectFrom(orders).where(eq(orders.status, 'completed')).as('completedOrders')
486
+ forgeSQL.selectFrom(users).where(eq(users.active, true)).as("activeUsers"),
487
+ forgeSQL.selectFrom(orders).where(eq(orders.status, "completed")).as("completedOrders"),
421
488
  )
422
489
  .select({
423
490
  totalActiveUsers: sql`COUNT(au.id)`,
424
- totalCompletedOrders: sql`COUNT(co.id)`
491
+ totalCompletedOrders: sql`COUNT(co.id)`,
425
492
  })
426
493
  .from(sql`activeUsers au`)
427
494
  .leftJoin(sql`completedOrders co`, eq(sql`au.id`, sql`co.userId`));
495
+
496
+ // Rovo Integration for secure dynamic SQL queries
497
+ const rovo = forgeSQL.rovo();
498
+ const settings = await rovo
499
+ .rovoSettingBuilder(usersTable, accountId)
500
+ .addContextParameter(":currentUserId", accountId)
501
+ .useRLS()
502
+ .addRlsColumn(usersTable.id)
503
+ .addRlsWherePart((alias) => `${alias}.${usersTable.id.name} = '${accountId}'`)
504
+ .finish()
505
+ .build();
506
+
507
+ const rovoResult = await rovo.dynamicIsolatedQuery(
508
+ "SELECT id, name FROM users WHERE status = 'active' AND userId = :currentUserId",
509
+ settings,
510
+ );
428
511
  ```
429
512
 
430
513
  This approach gives you direct access to all Drizzle ORM features while still using the @forge/sql backend with enhanced caching and versioning capabilities.
@@ -460,7 +543,7 @@ await forgeSQL.executeWithCacheContext(async () => {
460
543
  await db.updateWithCacheContext(users)...;
461
544
  await db.deleteWithCacheContext(users)...;
462
545
  // invoke without cache
463
- const users = await db.selectAliasedCacheable(getTableColumns(users)).from(users);
546
+ const users = await db.selectAliasedCacheable(getTableColumns(users)).from(users);
464
547
  // Cache is cleared only once at the end for all affected tables
465
548
  });
466
549
 
@@ -476,15 +559,15 @@ const usersCacheable = await forgeSQL.selectCacheableFrom(users)
476
559
 
477
560
  // Raw SQL execution
478
561
  const rawUsers = await forgeSQL.execute(
479
- "SELECT * FROM users WHERE active = ?",
562
+ "SELECT * FROM users WHERE active = ?",
480
563
  [true]
481
564
  );
482
565
 
483
566
  // Raw SQL with caching
484
567
  // ⚠️ IMPORTANT: When using executeCacheable(), all table names must be wrapped with backticks (`)
485
568
  const cachedRawUsers = await forgeSQL.executeCacheable(
486
- "SELECT * FROM `users` WHERE active = ?",
487
- [true],
569
+ "SELECT * FROM `users` WHERE active = ?",
570
+ [true],
488
571
  300
489
572
  );
490
573
 
@@ -497,14 +580,14 @@ const usersWithMetadata = await forgeSQL.executeWithMetadata(
497
580
  },
498
581
  (totalDbExecutionTime, totalResponseSize, printQueriesWithPlan) => {
499
582
  const threshold = 500; // ms baseline for this resolver
500
-
583
+
501
584
  if (totalDbExecutionTime > threshold * 1.5) {
502
585
  console.warn(`[Performance Warning] Resolver exceeded DB time: ${totalDbExecutionTime} ms`);
503
586
  await printQueriesWithPlan(); // Analyze and print query execution plans
504
587
  } else if (totalDbExecutionTime > threshold) {
505
588
  console.debug(`[Performance Debug] High DB time: ${totalDbExecutionTime} ms`);
506
589
  }
507
-
590
+
508
591
  console.log(`DB response size: ${totalResponseSize} bytes`);
509
592
  }
510
593
  );
@@ -519,6 +602,7 @@ The caching system is optional and only needed if you want to use cache-related
519
602
  To use caching, you need to use Forge-SQL-ORM methods that support cache management:
520
603
 
521
604
  **Methods that perform cache eviction after execution and in cache context (batch eviction):**
605
+
522
606
  - `forgeSQL.insertAndEvictCache()`
523
607
  - `forgeSQL.updateAndEvictCache()`
524
608
  - `forgeSQL.deleteAndEvictCache()`
@@ -528,6 +612,7 @@ To use caching, you need to use Forge-SQL-ORM methods that support cache managem
528
612
  - `forgeSQL.getDrizzleQueryBuilder().deleteAndEvictCache()`
529
613
 
530
614
  **Methods that participate in cache context only (batch eviction):**
615
+
531
616
  - All methods except the default Drizzle methods:
532
617
  - `forgeSQL.insert()`
533
618
  - `forgeSQL.update()`
@@ -538,17 +623,20 @@ To use caching, you need to use Forge-SQL-ORM methods that support cache managem
538
623
  - `forgeSQL.getDrizzleQueryBuilder().deleteWithCacheContext()`
539
624
 
540
625
  **Methods do not do evict cache, better do not use with cache feature:**
541
- - `forgeSQL.getDrizzleQueryBuilder().insert()`
542
- - `forgeSQL.getDrizzleQueryBuilder().update()`
543
- - `forgeSQL.getDrizzleQueryBuilder().delete()`
626
+
627
+ - `forgeSQL.getDrizzleQueryBuilder().insert()`
628
+ - `forgeSQL.getDrizzleQueryBuilder().update()`
629
+ - `forgeSQL.getDrizzleQueryBuilder().delete()`
544
630
 
545
631
  **Cacheable methods:**
546
- - `forgeSQL.selectCacheable()`
547
- - `forgeSQL.selectDistinctCacheable()`
548
- - `forgeSQL.getDrizzleQueryBuilder().selectAliasedCacheable()`
549
- - `forgeSQL.getDrizzleQueryBuilder().selectAliasedDistinctCacheable()`
632
+
633
+ - `forgeSQL.selectCacheable()`
634
+ - `forgeSQL.selectDistinctCacheable()`
635
+ - `forgeSQL.getDrizzleQueryBuilder().selectAliasedCacheable()`
636
+ - `forgeSQL.getDrizzleQueryBuilder().selectAliasedDistinctCacheable()`
550
637
 
551
638
  **Cache context example:**
639
+
552
640
  ```typescript
553
641
  await forgeSQL.executeWithCacheContext(async () => {
554
642
  // These methods participate in batch cache clearing
@@ -559,7 +647,6 @@ await forgeSQL.executeWithCacheContext(async () => {
559
647
  });
560
648
  ```
561
649
 
562
-
563
650
  The diagram below shows the lifecycle of a cacheable query in Forge-SQL-ORM:
564
651
 
565
652
  1. Resolver calls forge-sql-orm with a SQL query and parameters.
@@ -571,7 +658,6 @@ The diagram below shows the lifecycle of a cacheable query in Forge-SQL-ORM:
571
658
 
572
659
  ![img.png](img/umlCache1.png)
573
660
 
574
-
575
661
  The diagram below shows how Evict Cache works in Forge-SQL-ORM:
576
662
 
577
663
  1. **Data modification** is executed through `@forge/sql` (e.g., `UPDATE users ...`).
@@ -605,13 +691,13 @@ The diagram below shows how Cache Context works:
605
691
 
606
692
  ![img.png](img/umlCacheEvictCacheContext1.png)
607
693
 
608
-
609
694
  ### Important Considerations
610
695
 
611
696
  **@forge/kvs Limits:**
612
697
  Please review the [official @forge/kvs quotas and limits](https://developer.atlassian.com/platform/forge/platform-quotas-and-limits/#kvs-and-custom-entity-store-quotas) before implementing caching.
613
698
 
614
699
  **Caching Guidelines:**
700
+
615
701
  - Don't cache everything - be selective about what to cache
616
702
  - Don't cache simple and fast queries - sometimes direct query is faster than cache
617
703
  - Consider data size and frequency of changes
@@ -619,7 +705,8 @@ Please review the [official @forge/kvs quotas and limits](https://developer.atla
619
705
  - Use appropriate TTL values
620
706
 
621
707
  **⚠️ Important Cache Limitations:**
622
- - **Table names starting with `a_`**: Tables whose names start with `a_` (case-insensitive) are automatically ignored in cache operations. KVS Cache will not work with such tables, and they will be excluded from cache invalidation and cache key generation. This is by design to support special system tables or temporary tables.
708
+
709
+ - **Table names starting with `a_`**: Tables whose names start with `a_` (case-insensitive) are automatically ignored in cache operations. KVS Cache will not work with such tables, and they will be excluded from cache invalidation and cache key generation.
623
710
 
624
711
  ### Step 1: Install Dependencies
625
712
 
@@ -657,18 +744,18 @@ modules:
657
744
  - key: clearCache
658
745
  handler: index.clearCache
659
746
  ```
747
+
660
748
  ```typescript
661
749
  // Example usage in your Forge app
662
750
  import { clearCacheSchedulerTrigger } from "forge-sql-orm";
663
751
 
664
752
  export const clearCache = () => {
665
- return clearCacheSchedulerTrigger({
753
+ return clearCacheSchedulerTrigger({
666
754
  cacheEntityName: "cache",
667
755
  });
668
756
  };
669
757
  ```
670
758
 
671
-
672
759
  ### Step 3: Configure ORM Options
673
760
 
674
761
  Set the cache entity name in your ForgeSQL configuration:
@@ -685,6 +772,7 @@ const forgeSQL = new ForgeSQL(options);
685
772
  ```
686
773
 
687
774
  **Important Notes:**
775
+
688
776
  - The `cacheEntityName` must exactly match the `name` in your manifest storage entities
689
777
  - The entity attributes (`sql`, `expiration`, `data`) are required for proper cache functionality
690
778
  - Indexes on `sql` and `expiration` improve cache lookup performance
@@ -696,11 +784,14 @@ const forgeSQL = new ForgeSQL(options);
696
784
  **Basic setup (without caching):**
697
785
 
698
786
  **package.json:**
787
+
699
788
  ```shell
700
789
  npm install forge-sql-orm @forge/sql drizzle-orm -S
790
+ # For UI-Kit projects, use: npm install forge-sql-orm @forge/sql drizzle-orm -S --legacy-peer-deps
701
791
  ```
702
792
 
703
793
  **manifest.yml:**
794
+
704
795
  ```yaml
705
796
  modules:
706
797
  sql:
@@ -709,6 +800,7 @@ modules:
709
800
  ```
710
801
 
711
802
  **index.ts:**
803
+
712
804
  ```typescript
713
805
  import ForgeSQL from "forge-sql-orm";
714
806
 
@@ -718,17 +810,18 @@ const forgeSQL = new ForgeSQL();
718
810
  await forgeSQL.insert(Users, [userData]);
719
811
  // Use versioned operations without caching
720
812
  await forgeSQL.modifyWithVersioning().insert(Users, [userData]);
721
- const users = await forgeSQL.select({id: Users.id});
722
-
723
-
813
+ const users = await forgeSQL.select({ id: Users.id });
724
814
  ```
725
815
 
726
816
  **With caching support:**
817
+
727
818
  ```shell
728
819
  npm install forge-sql-orm @forge/sql @forge/kvs drizzle-orm -S
820
+ # For UI-Kit projects, use: npm install forge-sql-orm @forge/sql @forge/kvs drizzle-orm -S --legacy-peer-deps
729
821
  ```
730
822
 
731
823
  **manifest.yml:**
824
+
732
825
  ```yaml
733
826
  modules:
734
827
  scheduledTrigger:
@@ -762,21 +855,23 @@ modules:
762
855
  import ForgeSQL from "forge-sql-orm";
763
856
 
764
857
  const forgeSQL = new ForgeSQL({
765
- cacheEntityName: "cache"
858
+ cacheEntityName: "cache",
766
859
  });
767
860
 
768
- import {clearCacheSchedulerTrigger} from "forge-sql-orm";
769
- import {getTableColumns} from "drizzle-orm";
861
+ import { clearCacheSchedulerTrigger } from "forge-sql-orm";
862
+ import { getTableColumns } from "drizzle-orm";
770
863
 
771
864
  export const clearCache = () => {
772
- return clearCacheSchedulerTrigger({
773
- cacheEntityName: "cache",
774
- });
865
+ return clearCacheSchedulerTrigger({
866
+ cacheEntityName: "cache",
867
+ });
775
868
  };
776
869
 
777
-
778
870
  // Now you can use caching features
779
- const usersData = await forgeSQL.selectCacheable(getTableColumns(users)).from(users).where(eq(users.active, true))
871
+ const usersData = await forgeSQL
872
+ .selectCacheable(getTableColumns(users))
873
+ .from(users)
874
+ .where(eq(users.active, true));
780
875
 
781
876
  // simple insert
782
877
  await forgeSQL.insertAndEvictCache(users, [userData]);
@@ -785,159 +880,194 @@ await forgeSQL.modifyWithVersioningAndEvictCache().insert(users, [userData]);
785
880
 
786
881
  // use Cache Context
787
882
  const data = await forgeSQL.executeWithCacheContextAndReturnValue(async () => {
788
- // after insert mark users to evict
789
- await forgeSQL.insert(users, [userData]);
790
- // after insertAndEvictCache mark orders to evict
791
- await forgeSQL.insertAndEvictCache(orders, [order1, order2]);
792
- // execute query and put result to local cache
793
- await forgeSQL.selectCacheable({userId: users.id, userName: users.name, orderId: orders.id, orderName: orders.name})
794
- .from(users)
795
- .innerJoin(orders, eq(orders.userId, users.id)).where(eq(users.active, true))
796
- // use local cache without @forge/kvs and @forge/sql
797
- return await forgeSQL.selectCacheable({userId: users.id, userName: users.name, orderId: orders.id, orderName: orders.name})
798
- .from(users)
799
- .innerJoin(orders, eq(orders.userId, users.id)).where(eq(users.active, true))
800
- })
883
+ // after insert mark users to evict
884
+ await forgeSQL.insert(users, [userData]);
885
+ // after insertAndEvictCache mark orders to evict
886
+ await forgeSQL.insertAndEvictCache(orders, [order1, order2]);
887
+ // execute query and put result to local cache
888
+ await forgeSQL
889
+ .selectCacheable({
890
+ userId: users.id,
891
+ userName: users.name,
892
+ orderId: orders.id,
893
+ orderName: orders.name,
894
+ })
895
+ .from(users)
896
+ .innerJoin(orders, eq(orders.userId, users.id))
897
+ .where(eq(users.active, true));
898
+ // use local cache without @forge/kvs and @forge/sql
899
+ return await forgeSQL
900
+ .selectCacheable({
901
+ userId: users.id,
902
+ userName: users.name,
903
+ orderId: orders.id,
904
+ orderName: orders.name,
905
+ })
906
+ .from(users)
907
+ .innerJoin(orders, eq(orders.userId, users.id))
908
+ .where(eq(users.active, true));
909
+ });
801
910
  // execute query and put result to kvs cache
802
- await forgeSQL.selectCacheable({userId: users.id, userName: users.name, orderId: orders.id, orderName: orders.name})
803
- .from(users)
804
- .innerJoin(orders, eq(orders.userId, users.id)).where(eq(users.active, true))
911
+ await forgeSQL
912
+ .selectCacheable({
913
+ userId: users.id,
914
+ userName: users.name,
915
+ orderId: orders.id,
916
+ orderName: orders.name,
917
+ })
918
+ .from(users)
919
+ .innerJoin(orders, eq(orders.userId, users.id))
920
+ .where(eq(users.active, true));
805
921
 
806
922
  // get result from @foge/kvs cache without real @forge/sql call
807
- await forgeSQL.selectCacheable({userId: users.id, userName: users.name, orderId: orders.id, orderName: orders.name})
808
- .from(users)
809
- .innerJoin(orders, eq(orders.userId, users.id)).where(eq(users.active, true))
923
+ await forgeSQL
924
+ .selectCacheable({
925
+ userId: users.id,
926
+ userName: users.name,
927
+ orderId: orders.id,
928
+ orderName: orders.name,
929
+ })
930
+ .from(users)
931
+ .innerJoin(orders, eq(orders.userId, users.id))
932
+ .where(eq(users.active, true));
810
933
 
811
934
  // use Local Cache for performance optimization
812
935
  const optimizedData = await forgeSQL.executeWithLocalCacheContextAndReturnValue(async () => {
813
- // First query - hits database and caches result
814
- const users = await forgeSQL.select({id: users.id, name: users.name})
815
- .from(users).where(eq(users.active, true));
816
-
817
- // Second query - uses local cache (no database call)
818
- const cachedUsers = await forgeSQL.select({id: users.id, name: users.name})
819
- .from(users).where(eq(users.active, true));
820
-
821
- // Using new methods for better performance
822
- const usersFrom = await forgeSQL.selectFrom(users)
823
- .where(eq(users.active, true));
824
-
825
- // This will use local cache (no database call)
826
- const cachedUsersFrom = await forgeSQL.selectFrom(users)
827
- .where(eq(users.active, true));
828
-
829
- // Raw SQL with local caching
830
- const rawUsers = await forgeSQL.execute(
831
- "SELECT id, name FROM users WHERE active = ?",
832
- [true]
833
- );
834
-
835
- // Insert operation - evicts local cache
836
- await forgeSQL.insert(users).values({name: 'New User', active: true});
837
-
838
- // Third query - hits database again and caches new result
839
- const updatedUsers = await forgeSQL.select({id: users.id, name: users.name})
840
- .from(users).where(eq(users.active, true));
841
-
842
- return { users, cachedUsers, updatedUsers, usersFrom, cachedUsersFrom, rawUsers };
843
- });
936
+ // First query - hits database and caches result
937
+ const users = await forgeSQL
938
+ .select({ id: users.id, name: users.name })
939
+ .from(users)
940
+ .where(eq(users.active, true));
941
+
942
+ // Second query - uses local cache (no database call)
943
+ const cachedUsers = await forgeSQL
944
+ .select({ id: users.id, name: users.name })
945
+ .from(users)
946
+ .where(eq(users.active, true));
947
+
948
+ // Using new methods for better performance
949
+ const usersFrom = await forgeSQL.selectFrom(users).where(eq(users.active, true));
950
+
951
+ // This will use local cache (no database call)
952
+ const cachedUsersFrom = await forgeSQL.selectFrom(users).where(eq(users.active, true));
844
953
 
954
+ // Raw SQL with local caching
955
+ const rawUsers = await forgeSQL.execute("SELECT id, name FROM users WHERE active = ?", [true]);
956
+
957
+ // Insert operation - evicts local cache
958
+ await forgeSQL.insert(users).values({ name: "New User", active: true });
959
+
960
+ // Third query - hits database again and caches new result
961
+ const updatedUsers = await forgeSQL
962
+ .select({ id: users.id, name: users.name })
963
+ .from(users)
964
+ .where(eq(users.active, true));
965
+
966
+ return { users, cachedUsers, updatedUsers, usersFrom, cachedUsersFrom, rawUsers };
967
+ });
845
968
  ```
846
969
 
847
970
  ## Choosing the Right Method - ForgeSQL ORM
848
971
 
849
972
  ### When to Use Each Approach
850
973
 
851
- | Method | Use Case | Versioning | Cache Management |
852
- |--------|----------|------------|------------------|
853
- | `modifyWithVersioningAndEvictCache()` | High-concurrency scenarios with Cache support| ✅ Yes | ✅ Yes |
854
- | `modifyWithVersioning()` | High-concurrency scenarios | ✅ Yes | Cache Context |
855
- | `insertAndEvictCache()` | Simple inserts | ❌ No | ✅ Yes |
856
- | `updateAndEvictCache()` | Simple updates | ❌ No | ✅ Yes |
857
- | `deleteAndEvictCache()` | Simple deletes | ❌ No | ✅ Yes |
858
- | `insert/update/delete` | Basic Drizzle operations | ❌ No | Cache Context |
859
- | `selectFrom()` | All-column queries with field aliasing | ❌ No | Local Cache |
860
- | `selectDistinctFrom()` | Distinct all-column queries with field aliasing | ❌ No | Local Cache |
861
- | `selectCacheableFrom()` | All-column queries with field aliasing and caching | ❌ No | Local + Global Cache |
862
- | `selectDistinctCacheableFrom()` | Distinct all-column queries with field aliasing and caching | ❌ No | Local + Global Cache |
863
- | `execute()` | Raw SQL queries with local caching | ❌ No | Local Cache |
864
- | `executeCacheable()` | Raw SQL queries with local and global caching | ❌ No | Local + Global Cache |
865
- | `executeDDL()` | DDL operations (CREATE, ALTER, DROP, etc.) | ❌ No | No Caching |
866
- | `executeDDLActions()` | Execute regular SQL queries in DDL operation context | ❌ No | No Caching |
867
- | `with()` | Common Table Expressions (CTEs) | ❌ No | Local Cache |
868
-
974
+ | Method | Use Case | Versioning | Cache Management |
975
+ | ------------------------------------- | ----------------------------------------------------------- | ---------- | -------------------- |
976
+ | `modifyWithVersioningAndEvictCache()` | High-concurrency scenarios with Cache support | ✅ Yes | ✅ Yes |
977
+ | `modifyWithVersioning()` | High-concurrency scenarios | ✅ Yes | Cache Context |
978
+ | `insertAndEvictCache()` | Simple inserts | ❌ No | ✅ Yes |
979
+ | `updateAndEvictCache()` | Simple updates | ❌ No | ✅ Yes |
980
+ | `deleteAndEvictCache()` | Simple deletes | ❌ No | ✅ Yes |
981
+ | `insert/update/delete` | Basic Drizzle operations | ❌ No | Cache Context |
982
+ | `selectFrom()` | All-column queries with field aliasing | ❌ No | Local Cache |
983
+ | `selectDistinctFrom()` | Distinct all-column queries with field aliasing | ❌ No | Local Cache |
984
+ | `selectCacheableFrom()` | All-column queries with field aliasing and caching | ❌ No | Local + Global Cache |
985
+ | `selectDistinctCacheableFrom()` | Distinct all-column queries with field aliasing and caching | ❌ No | Local + Global Cache |
986
+ | `execute()` | Raw SQL queries with local caching | ❌ No | Local Cache |
987
+ | `executeCacheable()` | Raw SQL queries with local and global caching | ❌ No | Local + Global Cache |
988
+ | `executeDDL()` | DDL operations (CREATE, ALTER, DROP, etc.) | ❌ No | No Caching |
989
+ | `executeDDLActions()` | Execute regular SQL queries in DDL operation context | ❌ No | No Caching |
990
+ | `with()` | Common Table Expressions (CTEs) | ❌ No | Local Cache |
869
991
 
870
992
  ## Choosing the Right Method - Direct Drizzle
871
993
 
872
994
  ### When to Use Each Approach
873
995
 
874
- | Method | Use Case | Versioning | Cache Management |
875
- |--------|----------|------------|------------------|
876
- | `insertWithCacheContext/insertWithCacheContext/updateWithCacheContext` | Basic Drizzle operations | ❌ No | Cache Context |
877
- | `insertAndEvictCache()` | Simple inserts without conflicts | ❌ No | ✅ Yes |
878
- | `updateAndEvictCache()` | Simple updates without conflicts | ❌ No | ✅ Yes |
879
- | `deleteAndEvictCache()` | Simple deletes without conflicts | ❌ No | ✅ Yes |
880
- | `insert/update/delete` | Basic Drizzle operations | ❌ No | ❌ No |
881
- | `selectFrom()` | All-column queries with field aliasing | ❌ No | Local Cache |
882
- | `selectDistinctFrom()` | Distinct all-column queries with field aliasing | ❌ No | Local Cache |
883
- | `selectCacheableFrom()` | All-column queries with field aliasing and caching | ❌ No | Local + Global Cache |
884
- | `selectDistinctCacheableFrom()` | Distinct all-column queries with field aliasing and caching | ❌ No | Local + Global Cache |
885
- | `execute()` | Raw SQL queries with local caching | ❌ No | Local Cache |
886
- | `executeCacheable()` | Raw SQL queries with local and global caching | ❌ No | Local + Global Cache |
887
- | `executeWithMetadata()` | Raw SQL queries with execution metrics capture | ❌ No | Local Cache |
888
- | `executeDDL()` | DDL operations (CREATE, ALTER, DROP, etc.) | ❌ No | No Caching |
889
- | `executeDDLActions()` | Execute regular SQL queries in DDL operation context | ❌ No | No Caching |
890
- | `with()` | Common Table Expressions (CTEs) | ❌ No | Local Cache |
891
- where Cache context - allows you to batch cache invalidation events and bypass cache reads for affected tables.
996
+ | Method | Use Case | Versioning | Cache Management |
997
+ | ---------------------------------------------------------------------- | ----------------------------------------------------------- | ---------- | -------------------- |
998
+ | `insertWithCacheContext/insertWithCacheContext/updateWithCacheContext` | Basic Drizzle operations | ❌ No | Cache Context |
999
+ | `insertAndEvictCache()` | Simple inserts without conflicts | ❌ No | ✅ Yes |
1000
+ | `updateAndEvictCache()` | Simple updates without conflicts | ❌ No | ✅ Yes |
1001
+ | `deleteAndEvictCache()` | Simple deletes without conflicts | ❌ No | ✅ Yes |
1002
+ | `insert/update/delete` | Basic Drizzle operations | ❌ No | ❌ No |
1003
+ | `selectFrom()` | All-column queries with field aliasing | ❌ No | Local Cache |
1004
+ | `selectDistinctFrom()` | Distinct all-column queries with field aliasing | ❌ No | Local Cache |
1005
+ | `selectCacheableFrom()` | All-column queries with field aliasing and caching | ❌ No | Local + Global Cache |
1006
+ | `selectDistinctCacheableFrom()` | Distinct all-column queries with field aliasing and caching | ❌ No | Local + Global Cache |
1007
+ | `execute()` | Raw SQL queries with local caching | ❌ No | Local Cache |
1008
+ | `executeCacheable()` | Raw SQL queries with local and global caching | ❌ No | Local + Global Cache |
1009
+ | `executeWithMetadata()` | Raw SQL queries with execution metrics capture | ❌ No | Local Cache |
1010
+ | `executeDDL()` | DDL operations (CREATE, ALTER, DROP, etc.) | ❌ No | No Caching |
1011
+ | `executeDDLActions()` | Execute regular SQL queries in DDL operation context | ❌ No | No Caching |
1012
+ | `with()` | Common Table Expressions (CTEs) | ❌ No | Local Cache |
892
1013
 
1014
+ where Cache context - allows you to batch cache invalidation events and bypass cache reads for affected tables.
893
1015
 
894
1016
  ## Step-by-Step Migration Workflow
895
1017
 
896
- 1. **Generate initial schema from an existing database**
1018
+ 1. **Install CLI and setup scripts**
897
1019
 
898
- ```sh
899
- npx forge-sql-orm-cli generate:model --dbName testDb --output ./database/schema
1020
+ ```bash
1021
+ npm install forge-sql-orm-cli -D
1022
+ npm pkg set scripts.models:create="forge-sql-orm-cli generate:model --output src/entities --saveEnv"
1023
+ npm pkg set scripts.migration:create="forge-sql-orm-cli migrations:create --force --output src/migration --entitiesPath src/entities"
1024
+ npm pkg set scripts.migration:update="forge-sql-orm-cli migrations:update --entitiesPath src/entities --output src/migration"
900
1025
  ```
901
1026
 
902
1027
  _(This is done only once when setting up the project)_
903
1028
 
904
- 2. **Create the first migration**
1029
+ 2. **Generate initial schema from an existing database**
905
1030
 
906
1031
  ```sh
907
- npx forge-sql-orm-cli migrations:create --dbName testDb --entitiesPath ./database/schema --output ./database/migration
1032
+ npm run models:create
908
1033
  ```
909
1034
 
910
- _(This initializes the database migration structure, also done once)_
1035
+ _(This will prompt for database credentials on first run and save them to `.env` file)_
911
1036
 
912
- 3. **Deploy to Forge and verify that migrations work**
1037
+ 3. **Create the first migration**
913
1038
 
1039
+ ```sh
1040
+ npm run migration:create
1041
+ ```
1042
+
1043
+ _(This initializes the database migration structure, also done once)_
1044
+
1045
+ 4. **Deploy to Forge and verify that migrations work**
914
1046
  - Deploy your **Forge app** with migrations.
915
1047
  - Run migrations using a **Forge web trigger** or **Forge scheduler**.
916
1048
 
917
- 4. **Modify the database (e.g., add a new column, index, etc.)**
918
-
1049
+ 5. **Modify the database (e.g., add a new column, index, etc.)**
919
1050
  - Use **DbSchema** or manually alter the database schema.
920
1051
 
921
- 5. **Update the migration**
1052
+ 6. **Update the migration**
922
1053
 
923
1054
  ```sh
924
- npx forge-sql-orm-cli migrations:update --dbName testDb --entitiesPath ./database/schema --output ./database/migration
1055
+ npm run migration:update
925
1056
  ```
926
1057
 
927
1058
  - ⚠️ **Do NOT update schema before this step!**
928
1059
  - If schema is updated first, the migration will be empty!
929
1060
 
930
- 6. **Deploy to Forge and verify that the migration runs without issues**
931
-
1061
+ 7. **Deploy to Forge and verify that the migration runs without issues**
932
1062
  - Run the updated migration on Forge.
933
1063
 
934
- 7. **Update the schema**
1064
+ 8. **Update the schema**
935
1065
 
936
1066
  ```sh
937
- npx forge-sql-orm-cli generate:model --dbName testDb --output ./database/schema
1067
+ npm run models:create
938
1068
  ```
939
1069
 
940
- 8. **Repeat steps 4-7 as needed**
1070
+ 9. **Repeat steps 5-8 as needed**
941
1071
 
942
1072
  **⚠️ WARNING:**
943
1073
 
@@ -947,6 +1077,7 @@ where Cache context - allows you to batch cache invalidation events and bypass c
947
1077
  ## Drop Migrations
948
1078
 
949
1079
  The Drop Migrations feature allows you to completely reset your database schema in Atlassian Forge SQL. This is useful when you need to:
1080
+
950
1081
  - Start fresh with a new schema
951
1082
  - Reset all tables and their data
952
1083
  - Clear migration history
@@ -955,6 +1086,7 @@ The Drop Migrations feature allows you to completely reset your database schema
955
1086
  ### Important Requirements
956
1087
 
957
1088
  Before using Drop Migrations, ensure that:
1089
+
958
1090
  1. Your local schema exactly matches the current database schema deployed in Atlassian Forge SQL
959
1091
  2. You have a backup of your data if needed
960
1092
  3. You understand that this operation will delete all tables and data
@@ -962,16 +1094,21 @@ Before using Drop Migrations, ensure that:
962
1094
  ### Usage
963
1095
 
964
1096
  1. First, ensure your local schema matches the deployed database:
1097
+
965
1098
  ```bash
966
- npx forge-sql-orm-cli generate:model --output ./database/schema
1099
+ npm run models:create
967
1100
  ```
968
1101
 
969
1102
  2. Generate the drop migration:
1103
+
970
1104
  ```bash
971
- npx forge-sql-orm-cli migrations:drop --entitiesPath ./database/schema --output ./database/migration
1105
+ npm run migration:drop
972
1106
  ```
973
1107
 
1108
+ _(Add this script to your package.json: `npm pkg set scripts.migration:drop="forge-sql-orm-cli migrations:drop --entitiesPath src/entities --output src/migration"`)_
1109
+
974
1110
  3. Deploy and run the migration in your Forge app:
1111
+
975
1112
  ```js
976
1113
  import migrationRunner from "./database/migration";
977
1114
  import { MigrationRunner } from "@forge/sql/out/migration";
@@ -983,13 +1120,14 @@ Before using Drop Migrations, ensure that:
983
1120
 
984
1121
  4. After dropping all tables, you can create a new migration to recreate the schema:
985
1122
  ```bash
986
- npx forge-sql-orm-cli migrations:create --entitiesPath ./database/schema --output ./database/migration --force
1123
+ npm run migration:create
987
1124
  ```
988
- The `--force` parameter is required here because we're creating a new migration after dropping all tables.
1125
+ The `--force` parameter is already included in the script to allow creating migrations after dropping all tables.
989
1126
 
990
1127
  ### Example Migration Output
991
1128
 
992
1129
  The generated drop migration will look like this:
1130
+
993
1131
  ```js
994
1132
  import { MigrationRunner } from "@forge/sql/out/migration";
995
1133
 
@@ -1017,31 +1155,36 @@ export default (migrationRunner: MigrationRunner): MigrationRunner => {
1017
1155
 
1018
1156
  When working with date and time fields in your models, you should use the custom types provided by Forge-SQL-ORM to ensure proper handling of date/time values. This is necessary because Forge SQL has specific format requirements for date/time values:
1019
1157
 
1020
- | Date type | Required Format | Example |
1021
- |-----------|----------------|---------|
1022
- | DATE | YYYY-MM-DD | 2024-09-19 |
1023
- | TIME | HH:MM:SS[.fraction] | 06:40:34 |
1158
+ | Date type | Required Format | Example |
1159
+ | --------- | ------------------------------ | -------------------------- |
1160
+ | DATE | YYYY-MM-DD | 2024-09-19 |
1161
+ | TIME | HH:MM:SS[.fraction] | 06:40:34 |
1024
1162
  | TIMESTAMP | YYYY-MM-DD HH:MM:SS[.fraction] | 2024-09-19 06:40:34.999999 |
1025
1163
 
1026
1164
  ```typescript
1027
1165
  // ❌ Don't use standard Drizzle date/time types
1028
- export const testEntityTimeStampVersion = mysqlTable('test_entity', {
1029
- id: int('id').primaryKey().autoincrement(),
1030
- time_stamp: timestamp('times_tamp').notNull(),
1031
- date_time: datetime('date_time').notNull(),
1032
- time: time('time').notNull(),
1033
- date: date('date').notNull(),
1166
+ export const testEntityTimeStampVersion = mysqlTable("test_entity", {
1167
+ id: int("id").primaryKey().autoincrement(),
1168
+ time_stamp: timestamp("times_tamp").notNull(),
1169
+ date_time: datetime("date_time").notNull(),
1170
+ time: time("time").notNull(),
1171
+ date: date("date").notNull(),
1034
1172
  });
1035
1173
 
1036
1174
  // ✅ Use Forge-SQL-ORM custom types instead
1037
- import { forgeDateTimeString, forgeDateString, forgeTimestampString, forgeTimeString } from 'forge-sql-orm'
1038
-
1039
- export const testEntityTimeStampVersion = mysqlTable('test_entity', {
1040
- id: int('id').primaryKey().autoincrement(),
1041
- time_stamp: forgeTimestampString('times_tamp').notNull(),
1042
- date_time: forgeDateTimeString('date_time').notNull(),
1043
- time: forgeTimeString('time').notNull(),
1044
- date: forgeDateString('date').notNull(),
1175
+ import {
1176
+ forgeDateTimeString,
1177
+ forgeDateString,
1178
+ forgeTimestampString,
1179
+ forgeTimeString,
1180
+ } from "forge-sql-orm";
1181
+
1182
+ export const testEntityTimeStampVersion = mysqlTable("test_entity", {
1183
+ id: int("id").primaryKey().autoincrement(),
1184
+ time_stamp: forgeTimestampString("times_tamp").notNull(),
1185
+ date_time: forgeDateTimeString("date_time").notNull(),
1186
+ time: forgeTimeString("time").notNull(),
1187
+ date: forgeDateString("date").notNull(),
1045
1188
  });
1046
1189
  ```
1047
1190
 
@@ -1057,6 +1200,7 @@ const timestamp = moment().format("YYYY-MM-DDTHH:mm:ss.SSS");
1057
1200
  ```
1058
1201
 
1059
1202
  Our custom types provide:
1203
+
1060
1204
  - Automatic conversion between JavaScript Date objects and Forge SQL's required string formats
1061
1205
  - Consistent date/time handling across your application
1062
1206
  - Type safety for date/time fields
@@ -1072,9 +1216,6 @@ Our custom types provide:
1072
1216
 
1073
1217
  Each type ensures that the data is properly formatted according to Forge SQL's requirements while providing a clean, type-safe interface for your application code.
1074
1218
 
1075
-
1076
-
1077
-
1078
1219
  # Connection to ORM
1079
1220
 
1080
1221
  ```js
@@ -1082,7 +1223,8 @@ import ForgeSQL from "forge-sql-orm";
1082
1223
 
1083
1224
  const forgeSQL = new ForgeSQL();
1084
1225
  ```
1085
- or
1226
+
1227
+ or
1086
1228
 
1087
1229
  ```typescript
1088
1230
  import { drizzle } from "drizzle-orm/mysql-proxy";
@@ -1101,71 +1243,47 @@ const users = await db.select().from(users);
1101
1243
 
1102
1244
  ```js
1103
1245
  // Using forgeSQL.select()
1104
- const user = await forgeSQL
1105
- .select({user: users})
1106
- .from(users);
1246
+ const user = await forgeSQL.select({ user: users }).from(users);
1107
1247
 
1108
1248
  // Using forgeSQL.selectDistinct()
1109
- const user = await forgeSQL
1110
- .selectDistinct({user: users})
1111
- .from(users);
1249
+ const user = await forgeSQL.selectDistinct({ user: users }).from(users);
1112
1250
 
1113
1251
  // Using forgeSQL.selectCacheable()
1114
- const user = await forgeSQL
1115
- .selectCacheable({user: users})
1116
- .from(users);
1252
+ const user = await forgeSQL.selectCacheable({ user: users }).from(users);
1117
1253
 
1118
1254
  // Using forgeSQL.selectFrom() - Select all columns with field aliasing
1119
- const user = await forgeSQL
1120
- .selectFrom(users)
1121
- .where(eq(users.id, 1));
1255
+ const user = await forgeSQL.selectFrom(users).where(eq(users.id, 1));
1122
1256
 
1123
1257
  // Using forgeSQL.selectDistinctFrom() - Select distinct all columns with field aliasing
1124
- const user = await forgeSQL
1125
- .selectDistinctFrom(users)
1126
- .where(eq(users.id, 1));
1258
+ const user = await forgeSQL.selectDistinctFrom(users).where(eq(users.id, 1));
1127
1259
 
1128
1260
  // Using forgeSQL.selectCacheableFrom() - Select all columns with field aliasing and caching
1129
- const user = await forgeSQL
1130
- .selectCacheableFrom(users)
1131
- .where(eq(users.id, 1));
1261
+ const user = await forgeSQL.selectCacheableFrom(users).where(eq(users.id, 1));
1132
1262
 
1133
1263
  // Using forgeSQL.selectDistinctCacheableFrom() - Select distinct all columns with field aliasing and caching
1134
- const user = await forgeSQL
1135
- .selectDistinctCacheableFrom(users)
1136
- .where(eq(users.id, 1));
1264
+ const user = await forgeSQL.selectDistinctCacheableFrom(users).where(eq(users.id, 1));
1137
1265
 
1138
1266
  // Using forgeSQL.execute() - Execute raw SQL with local caching
1139
- const user = await forgeSQL
1140
- .execute("SELECT * FROM users WHERE id = ?", [1]);
1267
+ const user = await forgeSQL.execute("SELECT * FROM users WHERE id = ?", [1]);
1141
1268
 
1142
1269
  // Using forgeSQL.executeCacheable() - Execute raw SQL with local and global caching
1143
1270
  // ⚠️ IMPORTANT: When using executeCacheable(), all table names in SQL queries must be wrapped with backticks (`)
1144
1271
  // Example: SELECT * FROM `users` WHERE id = ? (NOT: SELECT * FROM users WHERE id = ?)
1145
- const user = await forgeSQL
1146
- .executeCacheable("SELECT * FROM `users` WHERE id = ?", [1], 300);
1272
+ const user = await forgeSQL.executeCacheable("SELECT * FROM `users` WHERE id = ?", [1], 300);
1147
1273
 
1148
1274
  // Using forgeSQL.getDrizzleQueryBuilder()
1149
- const user = await forgeSQL
1150
- .getDrizzleQueryBuilder()
1151
- .select().from(Users)
1152
- .where(eq(Users.id, 1));
1275
+ const user = await forgeSQL.getDrizzleQueryBuilder().select().from(Users).where(eq(Users.id, 1));
1153
1276
 
1154
1277
  // OR using direct drizzle with custom driver
1155
1278
  const db = drizzle(forgeDriver);
1156
- const user = await db
1157
- .select().from(Users)
1158
- .where(eq(Users.id, 1));
1279
+ const user = await db.select().from(Users).where(eq(Users.id, 1));
1159
1280
  // Returns: { id: 1, name: "John Doe" }
1160
1281
 
1161
1282
  // Using executeQueryOnlyOne for single result with error handling
1162
1283
  const user = await forgeSQL
1163
1284
  .fetch()
1164
1285
  .executeQueryOnlyOne(
1165
- forgeSQL
1166
- .getDrizzleQueryBuilder()
1167
- .select().from(Users)
1168
- .where(eq(Users.id, 1))
1286
+ forgeSQL.getDrizzleQueryBuilder().select().from(Users).where(eq(Users.id, 1)),
1169
1287
  );
1170
1288
  // Returns: { id: 1, name: "John Doe" }
1171
1289
  // Throws error if multiple records found
@@ -1177,26 +1295,29 @@ const usersAlias = alias(Users, "u");
1177
1295
  const result = await forgeSQL
1178
1296
  .getDrizzleQueryBuilder()
1179
1297
  .select({
1180
- userId: sql<string>`${usersAlias.id} as \`userId\``,
1181
- userName: sql<string>`${usersAlias.name} as \`userName\``
1182
- }).from(usersAlias);
1298
+ userId: sql < string > `${usersAlias.id} as \`userId\``,
1299
+ userName: sql < string > `${usersAlias.name} as \`userName\``,
1300
+ })
1301
+ .from(usersAlias);
1183
1302
 
1184
1303
  // OR with direct drizzle
1185
1304
  const db = drizzle(forgeDriver);
1186
1305
  const result = await db
1187
1306
  .select({
1188
- userId: sql<string>`${usersAlias.id} as \`userId\``,
1189
- userName: sql<string>`${usersAlias.name} as \`userName\``
1190
- }).from(usersAlias);
1307
+ userId: sql < string > `${usersAlias.id} as \`userId\``,
1308
+ userName: sql < string > `${usersAlias.name} as \`userName\``,
1309
+ })
1310
+ .from(usersAlias);
1191
1311
  // Returns: { userId: 1, userName: "John Doe" }
1192
1312
  ```
1193
1313
 
1194
1314
  ### Complex Queries
1315
+
1195
1316
  ```js
1196
1317
  // Using joins with automatic field name collision prevention
1197
1318
  // With forgeSQL
1198
1319
  const orderWithUser = await forgeSQL
1199
- .select({user: users, order: orders})
1320
+ .select({ user: users, order: orders })
1200
1321
  .from(orders)
1201
1322
  .innerJoin(users, eq(orders.userId, users.id));
1202
1323
 
@@ -1215,12 +1336,12 @@ const orderWithUser = await forgeSQL
1215
1336
  // Using with() for Common Table Expressions (CTEs)
1216
1337
  const userStats = await forgeSQL
1217
1338
  .with(
1218
- forgeSQL.selectFrom(users).where(eq(users.active, true)).as('activeUsers'),
1219
- forgeSQL.selectFrom(orders).where(eq(orders.status, 'completed')).as('completedOrders')
1339
+ forgeSQL.selectFrom(users).where(eq(users.active, true)).as("activeUsers"),
1340
+ forgeSQL.selectFrom(orders).where(eq(orders.status, "completed")).as("completedOrders"),
1220
1341
  )
1221
1342
  .select({
1222
1343
  totalActiveUsers: sql`COUNT(au.id)`,
1223
- totalCompletedOrders: sql`COUNT(co.id)`
1344
+ totalCompletedOrders: sql`COUNT(co.id)`,
1224
1345
  })
1225
1346
  .from(sql`activeUsers au`)
1226
1347
  .leftJoin(sql`completedOrders co`, eq(sql`au.id`, sql`co.userId`));
@@ -1228,33 +1349,30 @@ const userStats = await forgeSQL
1228
1349
  // OR with direct drizzle
1229
1350
  const db = patchDbWithSelectAliased(drizzle(forgeDriver));
1230
1351
  const orderWithUser = await db
1231
- .selectAliased({user: users, order: orders})
1352
+ .selectAliased({ user: users, order: orders })
1232
1353
  .from(orders)
1233
1354
  .innerJoin(users, eq(orders.userId, users.id));
1234
- // Returns: {
1235
- // user_id: 1,
1355
+ // Returns: {
1356
+ // user_id: 1,
1236
1357
  // user_name: "John Doe",
1237
1358
  // order_id: 1,
1238
1359
  // order_product: "Product 1"
1239
1360
  // }
1240
1361
 
1241
1362
  // Using distinct with aliases
1242
- const uniqueUsers = await db
1243
- .selectAliasedDistinct({user: users})
1244
- .from(users);
1363
+ const uniqueUsers = await db.selectAliasedDistinct({ user: users }).from(users);
1245
1364
  // Returns unique users with aliased fields
1246
1365
 
1247
1366
  // Using executeQueryOnlyOne for unique results
1248
- const userStats = await forgeSQL
1249
- .fetch()
1250
- .executeQueryOnlyOne(
1251
- forgeSQL
1252
- .getDrizzleQueryBuilder()
1253
- .select({
1254
- totalUsers: sql`COUNT(*) as \`totalUsers\``,
1255
- uniqueNames: sql`COUNT(DISTINCT name) as \`uniqueNames\``
1256
- }).from(Users)
1257
- );
1367
+ const userStats = await forgeSQL.fetch().executeQueryOnlyOne(
1368
+ forgeSQL
1369
+ .getDrizzleQueryBuilder()
1370
+ .select({
1371
+ totalUsers: sql`COUNT(*) as \`totalUsers\``,
1372
+ uniqueNames: sql`COUNT(DISTINCT name) as \`uniqueNames\``,
1373
+ })
1374
+ .from(Users),
1375
+ );
1258
1376
  // Returns: { totalUsers: 100, uniqueNames: 80 }
1259
1377
  // Throws error if multiple records found
1260
1378
  ```
@@ -1286,14 +1404,14 @@ const usersWithMetadata = await forgeSQL.executeWithMetadata(
1286
1404
  },
1287
1405
  (totalDbExecutionTime, totalResponseSize, printQueriesWithPlan) => {
1288
1406
  const threshold = 500; // ms baseline for this resolver
1289
-
1407
+
1290
1408
  if (totalDbExecutionTime > threshold * 1.5) {
1291
1409
  console.warn(`[Performance Warning] Resolver exceeded DB time: ${totalDbExecutionTime} ms`);
1292
1410
  await printQueriesWithPlan(); // Analyze and print query execution plans
1293
1411
  } else if (totalDbExecutionTime > threshold) {
1294
1412
  console.debug(`[Performance Debug] High DB time: ${totalDbExecutionTime} ms`);
1295
1413
  }
1296
-
1414
+
1297
1415
  console.log(`DB response size: ${totalResponseSize} bytes`);
1298
1416
  }
1299
1417
  );
@@ -1308,7 +1426,7 @@ await forgeSQL.executeDDL(`
1308
1426
  `);
1309
1427
 
1310
1428
  await forgeSQL.executeDDL(sql`
1311
- ALTER TABLE users
1429
+ ALTER TABLE users
1312
1430
  ADD COLUMN created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
1313
1431
  `);
1314
1432
 
@@ -1319,23 +1437,23 @@ await forgeSQL.executeDDL("DROP TABLE IF EXISTS old_users");
1319
1437
  await forgeSQL.executeDDLActions(async () => {
1320
1438
  // Execute regular SQL queries in DDL context for performance monitoring
1321
1439
  const slowQueries = await forgeSQL.execute(`
1322
- SELECT * FROM INFORMATION_SCHEMA.STATEMENTS_SUMMARY
1440
+ SELECT * FROM INFORMATION_SCHEMA.STATEMENTS_SUMMARY
1323
1441
  WHERE AVG_LATENCY > 1000000
1324
1442
  `);
1325
-
1443
+
1326
1444
  // Execute complex analysis queries in DDL context
1327
1445
  const performanceData = await forgeSQL.execute(`
1328
1446
  SELECT * FROM INFORMATION_SCHEMA.CLUSTER_STATEMENTS_SUMMARY_HISTORY
1329
1447
  WHERE SUMMARY_END_TIME > DATE_SUB(NOW(), INTERVAL 1 HOUR)
1330
1448
  `);
1331
-
1449
+
1332
1450
  return { slowQueries, performanceData };
1333
1451
  });
1334
1452
 
1335
1453
  // Using execute() with complex queries
1336
1454
  const userStats = await forgeSQL
1337
1455
  .execute(`
1338
- SELECT
1456
+ SELECT
1339
1457
  u.id,
1340
1458
  u.name,
1341
1459
  COUNT(o.id) as order_count,
@@ -1360,13 +1478,10 @@ These operations work like standard Drizzle methods but participate in cache con
1360
1478
  await forgeSQL.insert(Users).values({ id: 1, name: "Smith" });
1361
1479
 
1362
1480
  // Basic update (participates in cache context when used within executeWithCacheContext)
1363
- await forgeSQL.update(Users)
1364
- .set({ name: "Smith Updated" })
1365
- .where(eq(Users.id, 1));
1481
+ await forgeSQL.update(Users).set({ name: "Smith Updated" }).where(eq(Users.id, 1));
1366
1482
 
1367
1483
  // Basic delete (participates in cache context when used within executeWithCacheContext)
1368
- await forgeSQL.delete(Users)
1369
- .where(eq(Users.id, 1));
1484
+ await forgeSQL.delete(Users).where(eq(Users.id, 1));
1370
1485
  ```
1371
1486
 
1372
1487
  ### 2. Non-Versioned Operations with Cache Management
@@ -1378,13 +1493,10 @@ These operations don't use optimistic locking but provide cache invalidation:
1378
1493
  await forgeSQL.insertAndEvictCache(Users).values({ id: 1, name: "Smith" });
1379
1494
 
1380
1495
  // Update without versioning but with cache invalidation
1381
- await forgeSQL.updateAndEvictCache(Users)
1382
- .set({ name: "Smith Updated" })
1383
- .where(eq(Users.id, 1));
1496
+ await forgeSQL.updateAndEvictCache(Users).set({ name: "Smith Updated" }).where(eq(Users.id, 1));
1384
1497
 
1385
1498
  // Delete without versioning but with cache invalidation
1386
- await forgeSQL.deleteAndEvictCache(Users)
1387
- .where(eq(Users.id, 1));
1499
+ await forgeSQL.deleteAndEvictCache(Users).where(eq(Users.id, 1));
1388
1500
  ```
1389
1501
 
1390
1502
  ### 3. Versioned Operations with Cache Management (Recommended)
@@ -1393,16 +1505,20 @@ These operations use optimistic locking and automatic cache invalidation:
1393
1505
 
1394
1506
  ```js
1395
1507
  // Insert with versioning and cache management
1396
- const userId = await forgeSQL.modifyWithVersioningAndEvictCache().insert(Users, [{ id: 1, name: "Smith" }]);
1508
+ const userId = await forgeSQL
1509
+ .modifyWithVersioningAndEvictCache()
1510
+ .insert(Users, [{ id: 1, name: "Smith" }]);
1397
1511
 
1398
1512
  // Bulk insert with versioning
1399
1513
  await forgeSQL.modifyWithVersioningAndEvictCache().insert(Users, [
1400
- { id: 2, name: "Smith" },
1401
- { id: 3, name: "Vasyl" },
1402
- ]);
1514
+ { id: 2, name: "Smith" },
1515
+ { id: 3, name: "Vasyl" },
1516
+ ]);
1403
1517
 
1404
1518
  // Update by ID with optimistic locking and cache invalidation
1405
- await forgeSQL.modifyWithVersioningAndEvictCache().updateById({ id: 1, name: "Smith Updated" }, Users);
1519
+ await forgeSQL
1520
+ .modifyWithVersioningAndEvictCache()
1521
+ .updateById({ id: 1, name: "Smith Updated" }, Users);
1406
1522
 
1407
1523
  // Delete by ID with versioning and cache invalidation
1408
1524
  await forgeSQL.modifyWithVersioningAndEvictCache().deleteById(1, Users);
@@ -1446,18 +1562,16 @@ await forgeSQL.modifyWithVersioning().deleteById(1, Users);
1446
1562
  import { nextVal } from "forge-sql-orm";
1447
1563
 
1448
1564
  const user = {
1449
- id: nextVal('user_id_seq'),
1565
+ id: nextVal("user_id_seq"),
1450
1566
  name: "user test",
1451
- organization_id: 1
1567
+ organization_id: 1,
1452
1568
  };
1453
1569
  const id = await forgeSQL.modifyWithVersioning().insert(appUser, [user]);
1454
1570
 
1455
1571
  // Update with custom WHERE condition
1456
- await forgeSQL.modifyWithVersioning().updateFields(
1457
- { name: "New Name", age: 35 },
1458
- Users,
1459
- eq(Users.email, "smith@example.com")
1460
- );
1572
+ await forgeSQL
1573
+ .modifyWithVersioning()
1574
+ .updateFields({ name: "New Name", age: 35 }, Users, eq(Users.email, "smith@example.com"));
1461
1575
 
1462
1576
  // Insert with duplicate handling
1463
1577
  await forgeSQL.modifyWithVersioning().insert(
@@ -1466,7 +1580,7 @@ await forgeSQL.modifyWithVersioning().insert(
1466
1580
  { id: 4, name: "Smith" },
1467
1581
  { id: 4, name: "Vasyl" },
1468
1582
  ],
1469
- true
1583
+ true,
1470
1584
  );
1471
1585
  ```
1472
1586
 
@@ -1488,19 +1602,21 @@ const result = await forgeSQL
1488
1602
  .offset(formatLimitOffset(350000));
1489
1603
 
1490
1604
  // The generated SQL will be:
1491
- // SELECT * FROM order_item
1492
- // ORDER BY created_at ASC
1493
- // LIMIT 10
1605
+ // SELECT * FROM order_item
1606
+ // ORDER BY created_at ASC
1607
+ // LIMIT 10
1494
1608
  // OFFSET 350000
1495
1609
  ```
1496
1610
 
1497
1611
  **Important Notes:**
1612
+
1498
1613
  - The function performs type checking to prevent SQL injection
1499
1614
  - It throws an error if the input is not a valid number
1500
1615
  - Use this function instead of direct parameter binding for LIMIT and OFFSET clauses
1501
1616
  - The function is specifically designed to work with Atlassian Forge SQL's limitations
1502
1617
 
1503
1618
  **Security Considerations:**
1619
+
1504
1620
  - The function includes validation to ensure the input is a valid number
1505
1621
  - This prevents SQL injection by ensuring only numeric values are inserted
1506
1622
  - Always use this function instead of string concatenation for LIMIT and OFFSET values
@@ -1534,9 +1650,9 @@ const options = {
1534
1650
  tableName: "users",
1535
1651
  versionField: {
1536
1652
  fieldName: "updatedAt",
1537
- }
1538
- }
1539
- }
1653
+ },
1654
+ },
1655
+ },
1540
1656
  };
1541
1657
 
1542
1658
  const forgeSQL = new ForgeSQL(options);
@@ -1559,7 +1675,6 @@ The caching system leverages Forge's Custom entity store to provide:
1559
1675
  // Value: { data: [...], expiration: 1234567890, sql: "select * from 1" }
1560
1676
  ```
1561
1677
 
1562
-
1563
1678
  ### Cache Context Operations
1564
1679
 
1565
1680
  The cache context allows you to batch cache invalidation events and bypass cache reads for affected tables:
@@ -1620,72 +1735,78 @@ Local cache is an in-memory caching layer that operates within a single resolver
1620
1735
  // Execute operations within a local cache context
1621
1736
  await forgeSQL.executeWithLocalContext(async () => {
1622
1737
  // First call - executes query and caches result
1623
- const users = await forgeSQL.select({ id: users.id, name: users.name })
1624
- .from(users).where(eq(users.active, true));
1625
-
1738
+ const users = await forgeSQL
1739
+ .select({ id: users.id, name: users.name })
1740
+ .from(users)
1741
+ .where(eq(users.active, true));
1742
+
1626
1743
  // Second call - gets result from local cache (no database query)
1627
- const cachedUsers = await forgeSQL.select({ id: users.id, name: users.name })
1628
- .from(users).where(eq(users.active, true));
1629
-
1630
- // Using new selectFrom methods with local caching
1631
- const usersFrom = await forgeSQL.selectFrom(users)
1744
+ const cachedUsers = await forgeSQL
1745
+ .select({ id: users.id, name: users.name })
1746
+ .from(users)
1632
1747
  .where(eq(users.active, true));
1633
-
1748
+
1749
+ // Using new selectFrom methods with local caching
1750
+ const usersFrom = await forgeSQL.selectFrom(users).where(eq(users.active, true));
1751
+
1634
1752
  // This will use local cache (no database call)
1635
- const cachedUsersFrom = await forgeSQL.selectFrom(users)
1636
- .where(eq(users.active, true));
1637
-
1753
+ const cachedUsersFrom = await forgeSQL.selectFrom(users).where(eq(users.active, true));
1754
+
1638
1755
  // Using execute() with local caching
1639
- const rawUsers = await forgeSQL.execute(
1640
- "SELECT id, name FROM users WHERE active = ?",
1641
- [true]
1642
- );
1643
-
1756
+ const rawUsers = await forgeSQL.execute("SELECT id, name FROM users WHERE active = ?", [true]);
1757
+
1644
1758
  // This will use local cache (no database call)
1645
- const cachedRawUsers = await forgeSQL.execute(
1646
- "SELECT id, name FROM users WHERE active = ?",
1647
- [true]
1648
- );
1649
-
1759
+ const cachedRawUsers = await forgeSQL.execute("SELECT id, name FROM users WHERE active = ?", [
1760
+ true,
1761
+ ]);
1762
+
1650
1763
  // Raw SQL with execution metadata and performance monitoring
1651
1764
  const usersWithMetadata = await forgeSQL.executeWithMetadata(
1652
1765
  async () => {
1653
1766
  const users = await forgeSQL.selectFrom(usersTable);
1654
- const orders = await forgeSQL.selectFrom(ordersTable).where(eq(ordersTable.userId, usersTable.id));
1767
+ const orders = await forgeSQL
1768
+ .selectFrom(ordersTable)
1769
+ .where(eq(ordersTable.userId, usersTable.id));
1655
1770
  return { users, orders };
1656
1771
  },
1657
1772
  (totalDbExecutionTime, totalResponseSize, printQueriesWithPlan) => {
1658
1773
  const threshold = 500; // ms baseline for this resolver
1659
-
1774
+
1660
1775
  if (totalDbExecutionTime > threshold * 1.5) {
1661
1776
  console.warn(`[Performance Warning] Resolver exceeded DB time: ${totalDbExecutionTime} ms`);
1662
1777
  await printQueriesWithPlan(); // Analyze and print query execution plans
1663
1778
  } else if (totalDbExecutionTime > threshold) {
1664
1779
  console.debug(`[Performance Debug] High DB time: ${totalDbExecutionTime} ms`);
1665
1780
  }
1666
-
1781
+
1667
1782
  console.log(`DB response size: ${totalResponseSize} bytes`);
1668
- }
1783
+ },
1669
1784
  );
1670
-
1785
+
1671
1786
  // Insert operation - evicts local cache for users table
1672
- await forgeSQL.insert(users).values({ name: 'New User', active: true });
1673
-
1787
+ await forgeSQL.insert(users).values({ name: "New User", active: true });
1788
+
1674
1789
  // Third call - executes query again and caches new result
1675
- const updatedUsers = await forgeSQL.select({ id: users.id, name: users.name })
1676
- .from(users).where(eq(users.active, true));
1790
+ const updatedUsers = await forgeSQL
1791
+ .select({ id: users.id, name: users.name })
1792
+ .from(users)
1793
+ .where(eq(users.active, true));
1677
1794
  });
1678
1795
 
1679
1796
  // Execute with return value
1680
1797
  const result = await forgeSQL.executeWithLocalCacheContextAndReturnValue(async () => {
1681
1798
  // First call - executes query and caches result
1682
- const users = await forgeSQL.select({ id: users.id, name: users.name })
1683
- .from(users).where(eq(users.active, true));
1684
-
1799
+ const users = await forgeSQL
1800
+ .select({ id: users.id, name: users.name })
1801
+ .from(users)
1802
+ .where(eq(users.active, true));
1803
+
1685
1804
  // Second call - gets result from local cache (no database query)
1686
- const cachedUsers = await forgeSQL.select({ id: users.id, name: users.name })
1687
- .from(users).where(eq(users.active, true));
1688
-
1805
+ const cachedUsers = await forgeSQL
1806
+ .select({ id: users.id, name: users.name })
1807
+ .from(users)
1808
+ .where(eq(users.active, true));
1809
+
1689
1810
  return { users, cachedUsers };
1690
1811
  });
1691
1812
  ```
@@ -1697,57 +1818,57 @@ const result = await forgeSQL.executeWithLocalCacheContextAndReturnValue(async (
1697
1818
  const userResolver = async (req) => {
1698
1819
  return await forgeSQL.executeWithLocalCacheContextAndReturnValue(async () => {
1699
1820
  // Get user details using selectFrom (all columns with field aliasing)
1700
- const user = await forgeSQL.selectFrom(users)
1701
- .where(eq(users.id, args.userId));
1702
-
1821
+ const user = await forgeSQL.selectFrom(users).where(eq(users.id, args.userId));
1822
+
1703
1823
  // Get user's orders using selectCacheableFrom (with caching)
1704
- const orders = await forgeSQL.selectCacheableFrom(orders)
1705
- .where(eq(orders.userId, args.userId));
1706
-
1824
+ const orders = await forgeSQL.selectCacheableFrom(orders).where(eq(orders.userId, args.userId));
1825
+
1707
1826
  // Get user's profile using raw SQL with execute()
1708
1827
  const profile = await forgeSQL.execute(
1709
- "SELECT id, bio, avatar FROM profiles WHERE user_id = ?",
1710
- [args.userId]
1828
+ "SELECT id, bio, avatar FROM profiles WHERE user_id = ?",
1829
+ [args.userId],
1711
1830
  );
1712
-
1831
+
1713
1832
  // Get user statistics using complex raw SQL
1714
- const stats = await forgeSQL.execute(`
1833
+ const stats = await forgeSQL.execute(
1834
+ `
1715
1835
  SELECT
1716
1836
  COUNT(o.id) as total_orders,
1717
1837
  SUM(o.amount) as total_spent,
1718
1838
  AVG(o.amount) as avg_order_value
1719
1839
  FROM orders o
1720
1840
  WHERE o.user_id = ? AND o.status = 'completed'
1721
- `, [args.userId]);
1722
-
1841
+ `,
1842
+ [args.userId],
1843
+ );
1844
+
1723
1845
  // If any of these queries are repeated within the same resolver,
1724
1846
  // they will use the local cache instead of hitting the database
1725
-
1847
+
1726
1848
  return {
1727
1849
  ...user[0],
1728
1850
  orders,
1729
1851
  profile: profile[0],
1730
- stats: stats[0]
1852
+ stats: stats[0],
1731
1853
  };
1732
1854
  });
1733
1855
  };
1734
1856
  ```
1735
1857
 
1736
-
1737
1858
  #### Local Cache (Level 1) vs Global Cache (Level 2)
1738
1859
 
1739
- | Feature | Local Cache (Level 1) | Global Cache (Level 2) |
1740
- |---------|----------------------|------------------------|
1741
- | **Storage** | In-memory (Node.js process) | Persistent (KVS Custom Entities) |
1742
- | **Scope** | Single forge invocation | Cross-invocation (between calls) |
1743
- | **Persistence** | No (cleared on invocation end) | Yes (survives app redeploy) |
1744
- | **Performance** | Very fast (memory access) | Fast (KVS optimized storage) |
1745
- | **Memory Usage** | Low (invocation-scoped) | Higher (persistent storage) |
1746
- | **Use Case** | Invocation optimization | Cross-invocation data sharing |
1747
- | **Configuration** | None required | Requires KVS setup |
1748
- | **TTL Support** | No (invocation-scoped) | Yes (automatic expiration) |
1749
- | **Cache Eviction** | Automatic on DML operations | Manual or scheduled cleanup |
1750
- | **Best For** | Repeated queries in single invocation | Frequently accessed data across invocations |
1860
+ | Feature | Local Cache (Level 1) | Global Cache (Level 2) |
1861
+ | ------------------ | ------------------------------------- | ------------------------------------------- |
1862
+ | **Storage** | In-memory (Node.js process) | Persistent (KVS Custom Entities) |
1863
+ | **Scope** | Single forge invocation | Cross-invocation (between calls) |
1864
+ | **Persistence** | No (cleared on invocation end) | Yes (survives app redeploy) |
1865
+ | **Performance** | Very fast (memory access) | Fast (KVS optimized storage) |
1866
+ | **Memory Usage** | Low (invocation-scoped) | Higher (persistent storage) |
1867
+ | **Use Case** | Invocation optimization | Cross-invocation data sharing |
1868
+ | **Configuration** | None required | Requires KVS setup |
1869
+ | **TTL Support** | No (invocation-scoped) | Yes (automatic expiration) |
1870
+ | **Cache Eviction** | Automatic on DML operations | Manual or scheduled cleanup |
1871
+ | **Best For** | Repeated queries in single invocation | Frequently accessed data across invocations |
1751
1872
 
1752
1873
  #### Integration with Global Cache (Level 2)
1753
1874
 
@@ -1760,19 +1881,20 @@ await forgeSQL.executeWithLocalContext(async () => {
1760
1881
  // 1. Local cache (Level 1 - in-memory)
1761
1882
  // 2. Global cache (Level 2 - KVS)
1762
1883
  // 3. Database query
1763
- const users = await forgeSQL.selectCacheable({ id: users.id, name: users.name })
1764
- .from(users).where(eq(users.active, true));
1765
-
1766
- // Using new methods with multi-level caching
1767
- const usersFrom = await forgeSQL.selectCacheableFrom(users)
1884
+ const users = await forgeSQL
1885
+ .selectCacheable({ id: users.id, name: users.name })
1886
+ .from(users)
1768
1887
  .where(eq(users.active, true));
1769
-
1888
+
1889
+ // Using new methods with multi-level caching
1890
+ const usersFrom = await forgeSQL.selectCacheableFrom(users).where(eq(users.active, true));
1891
+
1770
1892
  // Raw SQL with multi-level caching
1771
1893
  // ⚠️ IMPORTANT: When using executeCacheable(), all table names must be wrapped with backticks (`)
1772
1894
  const rawUsers = await forgeSQL.executeCacheable(
1773
- "SELECT id, name FROM `users` WHERE active = ?",
1774
- [true],
1775
- 300 // TTL in seconds
1895
+ "SELECT id, name FROM `users` WHERE active = ?",
1896
+ [true],
1897
+ 300, // TTL in seconds
1776
1898
  );
1777
1899
  });
1778
1900
  ```
@@ -1796,26 +1918,26 @@ The diagram below shows how local cache works in Forge-SQL-ORM:
1796
1918
  // Execute queries with caching
1797
1919
  const users = await forgeSQL.modifyWithVersioningAndEvictCache().executeQuery(
1798
1920
  forgeSQL.select().from(Users).where(eq(Users.active, true)),
1799
- 600 // Custom TTL in seconds
1921
+ 600, // Custom TTL in seconds
1800
1922
  );
1801
1923
 
1802
1924
  // Execute single result queries with caching
1803
- const user = await forgeSQL.modifyWithVersioningAndEvictCache().executeQueryOnlyOne(
1804
- forgeSQL.select().from(Users).where(eq(Users.id, 1))
1805
- );
1925
+ const user = await forgeSQL
1926
+ .modifyWithVersioningAndEvictCache()
1927
+ .executeQueryOnlyOne(forgeSQL.select().from(Users).where(eq(Users.id, 1)));
1806
1928
 
1807
1929
  // Execute raw SQL with caching
1808
1930
  const results = await forgeSQL.modifyWithVersioningAndEvictCache().executeRawSQL(
1809
1931
  "SELECT * FROM users WHERE active = ?",
1810
1932
  [true],
1811
- 300 // TTL in seconds
1933
+ 300, // TTL in seconds
1812
1934
  );
1813
1935
 
1814
1936
  // Using new methods for cache-aware operations
1815
- const usersFrom = await forgeSQL.selectCacheableFrom(Users)
1816
- .where(eq(Users.active, true));
1937
+ const usersFrom = await forgeSQL.selectCacheableFrom(Users).where(eq(Users.active, true));
1817
1938
 
1818
- const usersDistinct = await forgeSQL.selectDistinctCacheableFrom(Users)
1939
+ const usersDistinct = await forgeSQL
1940
+ .selectDistinctCacheableFrom(Users)
1819
1941
  .where(eq(Users.active, true));
1820
1942
 
1821
1943
  // Raw SQL with local and global caching
@@ -1823,18 +1945,18 @@ const usersDistinct = await forgeSQL.selectDistinctCacheableFrom(Users)
1823
1945
  const rawUsers = await forgeSQL.executeCacheable(
1824
1946
  "SELECT * FROM `users` WHERE active = ?",
1825
1947
  [true],
1826
- 300 // TTL in seconds
1948
+ 300, // TTL in seconds
1827
1949
  );
1828
1950
 
1829
1951
  // Using with() for Common Table Expressions with caching
1830
1952
  const userStats = await forgeSQL
1831
1953
  .with(
1832
- forgeSQL.selectFrom(users).where(eq(users.active, true)).as('activeUsers'),
1833
- forgeSQL.selectFrom(orders).where(eq(orders.status, 'completed')).as('completedOrders')
1954
+ forgeSQL.selectFrom(users).where(eq(users.active, true)).as("activeUsers"),
1955
+ forgeSQL.selectFrom(orders).where(eq(orders.status, "completed")).as("completedOrders"),
1834
1956
  )
1835
1957
  .select({
1836
1958
  totalActiveUsers: sql`COUNT(au.id)`,
1837
- totalCompletedOrders: sql`COUNT(co.id)`
1959
+ totalCompletedOrders: sql`COUNT(co.id)`,
1838
1960
  })
1839
1961
  .from(sql`activeUsers au`)
1840
1962
  .leftJoin(sql`completedOrders co`, eq(sql`au.id`, sql`co.userId`));
@@ -1843,21 +1965,23 @@ const userStats = await forgeSQL
1843
1965
  const usersWithMetadata = await forgeSQL.executeWithMetadata(
1844
1966
  async () => {
1845
1967
  const users = await forgeSQL.selectFrom(usersTable);
1846
- const orders = await forgeSQL.selectFrom(ordersTable).where(eq(ordersTable.userId, usersTable.id));
1968
+ const orders = await forgeSQL
1969
+ .selectFrom(ordersTable)
1970
+ .where(eq(ordersTable.userId, usersTable.id));
1847
1971
  return { users, orders };
1848
1972
  },
1849
1973
  (totalDbExecutionTime, totalResponseSize, printQueriesWithPlan) => {
1850
1974
  const threshold = 500; // ms baseline for this resolver
1851
-
1975
+
1852
1976
  if (totalDbExecutionTime > threshold * 1.5) {
1853
1977
  console.warn(`[Performance Warning] Resolver exceeded DB time: ${totalDbExecutionTime} ms`);
1854
1978
  await printQueriesWithPlan(); // Analyze and print query execution plans
1855
1979
  } else if (totalDbExecutionTime > threshold) {
1856
1980
  console.debug(`[Performance Debug] High DB time: ${totalDbExecutionTime} ms`);
1857
1981
  }
1858
-
1982
+
1859
1983
  console.log(`DB response size: ${totalResponseSize} bytes`);
1860
- }
1984
+ },
1861
1985
  );
1862
1986
  ```
1863
1987
 
@@ -1893,9 +2017,9 @@ const options = {
1893
2017
  tableName: "users",
1894
2018
  versionField: {
1895
2019
  fieldName: "updatedAt",
1896
- }
1897
- }
1898
- }
2020
+ },
2021
+ },
2022
+ },
1899
2023
  };
1900
2024
 
1901
2025
  const forgeSQL = new ForgeSQL(options);
@@ -1906,41 +2030,260 @@ const forgeSQL = new ForgeSQL(options);
1906
2030
  ```typescript
1907
2031
  // The version field will be automatically handled
1908
2032
  await forgeSQL.modifyWithVersioning().updateById(
1909
- {
1910
- id: 1,
2033
+ {
2034
+ id: 1,
1911
2035
  name: "Updated Name",
1912
- updatedAt: new Date() // Will be automatically set if not provided
1913
- },
1914
- Users
2036
+ updatedAt: new Date(), // Will be automatically set if not provided
2037
+ },
2038
+ Users,
1915
2039
  );
1916
2040
  ```
2041
+
1917
2042
  or with cache support
2043
+
1918
2044
  ```typescript
1919
2045
  // The version field will be automatically handled
1920
2046
  await forgeSQL.modifyWithVersioningAndEvictCache().updateById(
1921
- {
1922
- id: 1,
2047
+ {
2048
+ id: 1,
1923
2049
  name: "Updated Name",
1924
- updatedAt: new Date() // Will be automatically set if not provided
1925
- },
1926
- Users
2050
+ updatedAt: new Date(), // Will be automatically set if not provided
2051
+ },
2052
+ Users,
2053
+ );
2054
+ ```
2055
+
2056
+ ## Rovo Integration
2057
+
2058
+ [↑ Back to Top](#table-of-contents)
2059
+
2060
+ 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.
2061
+
2062
+ **📖 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.
2063
+
2064
+ ### Key Features
2065
+
2066
+ - **Security-First Design**: Multiple layers of security validations to prevent SQL injection and unauthorized data access
2067
+ - **Single Table Isolation**: Queries are restricted to a single table to prevent cross-table data access
2068
+ - **Row-Level Security (RLS)**: Built-in support for data isolation based on user context
2069
+ - **Comprehensive Validation**: Blocks JOINs, subqueries, window functions, and other potentially unsafe operations
2070
+ - **Post-Execution Validation**: Verifies query results to ensure security fields are present and come from the correct table
2071
+ - **Type-Safe Configuration**: Uses Drizzle ORM table objects for type-safe column references
2072
+
2073
+ ### Security Validations
2074
+
2075
+ Rovo performs multiple security checks before and after query execution:
2076
+
2077
+ 1. **Query Type Validation**: Only SELECT queries are allowed
2078
+ 2. **Table Restriction**: Queries must target only the specified table
2079
+ 3. **JOIN Detection**: JOINs are blocked using EXPLAIN analysis
2080
+ 4. **Subquery Detection**: Scalar subqueries in SELECT columns are blocked
2081
+ 5. **Window Function Detection**: Window functions are blocked for security
2082
+ 6. **Execution Plan Validation**: Verifies that only the expected table is accessed
2083
+ 7. **RLS Field Validation**: Ensures required security fields are present in results
2084
+ 8. **Post-Execution Validation**: Verifies all fields come from the correct table
2085
+
2086
+ ### Basic Usage
2087
+
2088
+ ```typescript
2089
+ import ForgeSQL from "forge-sql-orm";
2090
+
2091
+ const forgeSQL = new ForgeSQL();
2092
+
2093
+ // Get Rovo instance
2094
+ const rovo = forgeSQL.rovo();
2095
+
2096
+ // Create settings builder using Drizzle table object
2097
+ const settings = await rovo
2098
+ .rovoSettingBuilder(usersTable, accountId)
2099
+ .addContextParameter(":currentUserId", accountId)
2100
+ .useRLS()
2101
+ .addRlsColumn(usersTable.id)
2102
+ .addRlsWherePart((alias) => `${alias}.${usersTable.id.name} = '${accountId}'`)
2103
+ .finish()
2104
+ .build();
2105
+
2106
+ // Execute dynamic SQL query
2107
+ const result = await rovo.dynamicIsolatedQuery(
2108
+ "SELECT id, name FROM users WHERE status = 'active' AND userId = :currentUserId",
2109
+ settings,
1927
2110
  );
2111
+
2112
+ console.log(result.rows); // Query results
2113
+ console.log(result.metadata); // Query metadata
2114
+ ```
2115
+
2116
+ ### Row-Level Security (RLS) Configuration
2117
+
2118
+ RLS allows you to filter data based on user context, ensuring users can only access their own data:
2119
+
2120
+ ```typescript
2121
+ const rovo = forgeSQL.rovo();
2122
+
2123
+ // Configure RLS with conditional activation and multiple security fields
2124
+ const settings = await rovo
2125
+ .rovoSettingBuilder(securityNotesTable, accountId)
2126
+ .addContextParameter(":currentUserId", accountId)
2127
+ .addContextParameter(":currentProjectKey", projectKey)
2128
+ .addContextParameter(":currentIssueKey", issueKey)
2129
+ .useRLS()
2130
+ .addRlsCondition(async () => {
2131
+ // Conditionally enable RLS based on user role
2132
+ const userService = getUserService();
2133
+ return !(await userService.isAdmin()); // Only apply RLS for non-admin users
2134
+ })
2135
+ .addRlsColumn(securityNotesTable.createdBy) // Required field for RLS validation
2136
+ .addRlsColumn(securityNotesTable.targetUserId) // Additional security field
2137
+ .addRlsWherePart(
2138
+ (alias) =>
2139
+ `${alias}.${securityNotesTable.createdBy.name} = '${accountId}' OR ${alias}.${securityNotesTable.targetUserId.name} = '${accountId}'`,
2140
+ ) // RLS filter with OR condition
2141
+ .finish()
2142
+ .build();
2143
+
2144
+ // The query will automatically be wrapped with RLS filtering:
2145
+ // SELECT * FROM (original_query) AS t WHERE (t.createdBy = 'accountId' OR t.targetUserId = 'accountId')
2146
+ ```
2147
+
2148
+ ### Context Parameters
2149
+
2150
+ You can use context parameters for query substitution. Parameters use the `:parameterName` format (colon prefix, not double braces):
2151
+
2152
+ ```typescript
2153
+ const rovo = forgeSQL.rovo();
2154
+
2155
+ const settings = await rovo
2156
+ .rovoSettingBuilder(usersTable, accountId)
2157
+ .addContextParameter(":currentUserId", accountId)
2158
+ .addContextParameter(":projectKey", "PROJ-123")
2159
+ .addContextParameter(":status", "active")
2160
+ .useRLS()
2161
+ .addRlsColumn(usersTable.id)
2162
+ .addRlsWherePart((alias) => `${alias}.${usersTable.userId.name} = '${accountId}'`)
2163
+ .finish()
2164
+ .build();
2165
+
2166
+ // In the SQL query, parameters are replaced:
2167
+ const result = await rovo.dynamicIsolatedQuery(
2168
+ "SELECT * FROM users WHERE projectKey = :projectKey AND status = :status AND userId = :currentUserId",
2169
+ settings,
2170
+ );
2171
+ // Becomes: SELECT * FROM users WHERE projectKey = 'PROJ-123' AND status = 'active' AND userId = 'accountId'
2172
+ ```
2173
+
2174
+ ### Using Raw Table Names
2175
+
2176
+ You can use `rovoRawSettingBuilder` with raw table name string:
2177
+
2178
+ ```typescript
2179
+ const rovo = forgeSQL.rovo();
2180
+
2181
+ // Using rovoRawSettingBuilder with raw table name
2182
+ const settings = await rovo
2183
+ .rovoRawSettingBuilder("users", accountId)
2184
+ .addContextParameter(":currentUserId", accountId)
2185
+ .useRLS()
2186
+ .addRlsColumnName("id")
2187
+ .addRlsWherePart((alias) => `${alias}.id = '${accountId}'`)
2188
+ .finish()
2189
+ .build();
2190
+
2191
+ const result = await rovo.dynamicIsolatedQuery(
2192
+ "SELECT id, name FROM users WHERE status = 'active' AND userId = :currentUserId",
2193
+ settings,
2194
+ );
2195
+ ```
2196
+
2197
+ ### Security Restrictions
2198
+
2199
+ Rovo blocks the following operations for security:
2200
+
2201
+ - **Data Modification**: Only SELECT queries are allowed
2202
+ - **JOINs**: JOIN operations are detected and blocked
2203
+ - **Subqueries**: Scalar subqueries in SELECT columns are blocked
2204
+ - **Window Functions**: Window functions (e.g., `COUNT(*) OVER(...)`) are blocked
2205
+ - **Multiple Tables**: Queries referencing multiple tables are blocked
2206
+ - **Table Aliases**: Post-execution validation ensures fields come from the correct table
2207
+
2208
+ ### Error Handling
2209
+
2210
+ Rovo provides detailed error messages when security violations are detected:
2211
+
2212
+ ```typescript
2213
+ try {
2214
+ const result = await rovo.dynamicIsolatedQuery(
2215
+ "SELECT * FROM users u JOIN orders o ON u.id = o.userId",
2216
+ settings,
2217
+ );
2218
+ } catch (error) {
2219
+ // Error: "Security violation: JOIN operations are not allowed..."
2220
+ console.error(error.message);
2221
+ }
2222
+ ```
2223
+
2224
+ ### Example: Real-World Function Implementation
2225
+
2226
+ > **💡 Full Example**: See the complete implementation in [Forge-Secure-Notes-for-Jira](https://github.com/vzakharchenko/Forge-Secure-Notes-for-Jira) repository.
2227
+
2228
+ ```typescript
2229
+ import ForgeSQL from "forge-sql-orm";
2230
+ import { Result } from "@forge/sql";
2231
+
2232
+ const FORGE_SQL_ORM = new ForgeSQL();
2233
+
2234
+ export async function runSecurityNotesQuery(
2235
+ event: {
2236
+ sql: string;
2237
+ context: {
2238
+ jira: {
2239
+ issueKey: string;
2240
+ projectKey: string;
2241
+ };
2242
+ };
2243
+ },
2244
+ context: { principal: { accountId: string } },
2245
+ ): Promise<Result<unknown>> {
2246
+ const rovoIntegration = FORGE_SQL_ORM.rovo();
2247
+ const accountId = context.principal.accountId;
2248
+
2249
+ const settings = await rovoIntegration
2250
+ .rovoSettingBuilder(securityNotesTable, accountId)
2251
+ .addContextParameter(":currentUserId", accountId)
2252
+ .addContextParameter(":currentProjectKey", event.context?.jira?.projectKey ?? "")
2253
+ .addContextParameter(":currentIssueKey", event.context?.jira?.issueKey ?? "")
2254
+ .useRLS()
2255
+ .addRlsCondition(async () => {
2256
+ // Conditionally disable RLS for admin users
2257
+ const userService = getUserService();
2258
+ return !(await userService.isAdmin());
2259
+ })
2260
+ .addRlsColumn(securityNotesTable.createdBy)
2261
+ .addRlsColumn(securityNotesTable.targetUserId)
2262
+ .addRlsWherePart(
2263
+ (alias: string) =>
2264
+ `${alias}.${securityNotesTable.createdBy.name} = '${accountId}' OR ${alias}.${securityNotesTable.targetUserId.name} = '${accountId}'`,
2265
+ )
2266
+ .finish()
2267
+ .build();
2268
+
2269
+ return await rovoIntegration.dynamicIsolatedQuery(event.sql, settings);
2270
+ }
1928
2271
  ```
1929
2272
 
1930
2273
  ## ForgeSqlOrmOptions
1931
2274
 
1932
2275
  The `ForgeSqlOrmOptions` object allows customization of ORM behavior:
1933
2276
 
1934
- | Option | Type | Description |
1935
- | -------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1936
- | `logRawSqlQuery` | `boolean` | Enables logging of raw SQL queries in the Atlassian Forge Developer Console. Useful for debugging and monitoring. Defaults to `false`. |
1937
- | `logCache` | `boolean` | Enables logging of cache operations (hits, misses, evictions) in the Atlassian Forge Developer Console. Useful for debugging caching issues. Defaults to `false`. |
2277
+ | Option | Type | Description |
2278
+ | -------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
2279
+ | `logRawSqlQuery` | `boolean` | Enables logging of raw SQL queries in the Atlassian Forge Developer Console. Useful for debugging and monitoring. Defaults to `false`. |
2280
+ | `logCache` | `boolean` | Enables logging of cache operations (hits, misses, evictions) in the Atlassian Forge Developer Console. Useful for debugging caching issues. Defaults to `false`. |
1938
2281
  | `disableOptimisticLocking` | `boolean` | Disables optimistic locking. When set to `true`, no additional condition (e.g., a version check) is added during record updates, which can improve performance. However, this may lead to conflicts when multiple transactions attempt to update the same record concurrently. |
1939
- | `additionalMetadata` | `object` | Allows adding custom metadata to all entities. This is useful for tracking common fields across all tables (e.g., `createdAt`, `updatedAt`, `createdBy`, etc.). The metadata will be automatically added to all generated entities. |
1940
- | `cacheEntityName` | `string` | KVS Custom entity name for cache storage. Must match the `name` in your `manifest.yml` storage entities configuration. Required for caching functionality. Defaults to `"cache"`. |
1941
- | `cacheTTL` | `number` | Default cache TTL in seconds. Defaults to `120` (2 minutes). |
1942
- | `cacheWrapTable` | `boolean` | Whether to wrap table names with backticks in cache keys. Defaults to `true`. |
1943
- | `hints` | `object` | SQL hints for query optimization. Optional configuration for advanced query tuning. |
2282
+ | `additionalMetadata` | `object` | Allows adding custom metadata to all entities. This is useful for tracking common fields across all tables (e.g., `createdAt`, `updatedAt`, `createdBy`, etc.). The metadata will be automatically added to all generated entities. |
2283
+ | `cacheEntityName` | `string` | KVS Custom entity name for cache storage. Must match the `name` in your `manifest.yml` storage entities configuration. Required for caching functionality. Defaults to `"cache"`. |
2284
+ | `cacheTTL` | `number` | Default cache TTL in seconds. Defaults to `120` (2 minutes). |
2285
+ | `cacheWrapTable` | `boolean` | Whether to wrap table names with backticks in cache keys. Defaults to `true`. |
2286
+ | `hints` | `object` | SQL hints for query optimization. Optional configuration for advanced query tuning. |
1944
2287
 
1945
2288
  ## CLI Commands
1946
2289
 
@@ -1959,23 +2302,39 @@ The CLI tool provides the following main commands:
1959
2302
 
1960
2303
  ### Installation
1961
2304
 
2305
+ The CLI tool must be installed as a local dependency and used via npm scripts in your `package.json`:
2306
+
1962
2307
  ```bash
1963
- npm install -g forge-sql-orm-cli
2308
+ npm install forge-sql-orm-cli -D
2309
+ ```
2310
+
2311
+ ### Setup npm Scripts
2312
+
2313
+ Add the following scripts to your `package.json`:
2314
+
2315
+ ```bash
2316
+ npm pkg set scripts.models:create="forge-sql-orm-cli generate:model --output src/entities --saveEnv"
2317
+ npm pkg set scripts.migration:create="forge-sql-orm-cli migrations:create --force --output src/migration --entitiesPath src/entities"
2318
+ npm pkg set scripts.migration:update="forge-sql-orm-cli migrations:update --entitiesPath src/entities --output src/migration"
1964
2319
  ```
1965
2320
 
1966
2321
  ### Basic Usage
1967
2322
 
2323
+ After setting up the scripts, use them via npm:
2324
+
1968
2325
  ```bash
1969
2326
  # Generate models from database
1970
- forge-sql-orm-cli generate:model --dbName myapp --output ./database/entities
2327
+ npm run models:create
1971
2328
 
1972
2329
  # Create migration
1973
- forge-sql-orm-cli migrations:create --dbName myapp --entitiesPath ./database/entities
2330
+ npm run migration:create
1974
2331
 
1975
2332
  # Update migration
1976
- forge-sql-orm-cli migrations:update --dbName myapp --entitiesPath ./database/entities
2333
+ npm run migration:update
1977
2334
  ```
1978
2335
 
2336
+ **Note:** The CLI tool is designed to work as a local dependency through npm scripts. Configuration is saved to `.env` file using the `--saveEnv` flag, so you only need to provide database credentials once.
2337
+
1979
2338
  For detailed information about all available options and advanced usage, see the [Full CLI Documentation](forge-sql-orm-cli/README.md).
1980
2339
 
1981
2340
  ## Web Triggers for Migrations
@@ -1985,7 +2344,8 @@ Forge-SQL-ORM provides web triggers for managing database migrations in Atlassia
1985
2344
  ### 1. Apply Migrations Trigger
1986
2345
 
1987
2346
  This trigger allows you to apply database migrations through a web endpoint. It's useful for:
1988
- - Manually triggering migrations
2347
+
2348
+ - Manually triggering migrations
1989
2349
  - Running migrations as part of your deployment process
1990
2350
  - Testing migrations in different environments
1991
2351
 
@@ -2000,22 +2360,23 @@ export const handlerMigration = async () => {
2000
2360
  ```
2001
2361
 
2002
2362
  Configure in `manifest.yml`:
2363
+
2003
2364
  ```yaml
2004
- webtrigger:
2005
- - key: invoke-schema-migration
2006
- function: runSchemaMigration
2007
- security:
2008
- egress:
2009
- allowDataEgress: false
2010
- allowedResponses:
2011
- - statusCode: 200
2012
- body: '{"body": "Migrations successfully executed"}'
2013
- sql:
2014
- - key: main
2015
- engine: mysql
2016
- function:
2017
- - key: runSchemaMigration
2018
- handler: index.handlerMigration
2365
+ webtrigger:
2366
+ - key: invoke-schema-migration
2367
+ function: runSchemaMigration
2368
+ security:
2369
+ egress:
2370
+ allowDataEgress: false
2371
+ allowedResponses:
2372
+ - statusCode: 200
2373
+ body: '{"body": "Migrations successfully executed"}'
2374
+ sql:
2375
+ - key: main
2376
+ engine: mysql
2377
+ function:
2378
+ - key: runSchemaMigration
2379
+ handler: index.handlerMigration
2019
2380
  ```
2020
2381
 
2021
2382
  ### 2. Drop Migrations Trigger
@@ -2023,11 +2384,12 @@ Configure in `manifest.yml`:
2023
2384
  ⚠️ **WARNING**: This trigger will permanently delete all data in the specified tables and clear the migrations history. This operation cannot be undone!
2024
2385
 
2025
2386
  This trigger allows you to completely reset your database schema. It's useful for:
2387
+
2026
2388
  - Development environments where you need to start fresh
2027
2389
  - Testing scenarios requiring a clean database
2028
2390
  - Resetting the database before applying new migrations
2029
2391
 
2030
- **Important**: The trigger will drop all tables including migration.
2392
+ **Important**: The trigger will drop all tables including migration.
2031
2393
 
2032
2394
  ```typescript
2033
2395
  // Example usage in your Forge app
@@ -2039,16 +2401,17 @@ export const dropMigrations = () => {
2039
2401
  ```
2040
2402
 
2041
2403
  Configure in `manifest.yml`:
2404
+
2042
2405
  ```yaml
2043
- webtrigger:
2044
- - key: drop-schema-migration
2045
- function: dropMigrations
2046
- sql:
2047
- - key: main
2048
- engine: mysql
2049
- function:
2050
- - key: dropMigrations
2051
- handler: index.dropMigrations
2406
+ webtrigger:
2407
+ - key: drop-schema-migration
2408
+ function: dropMigrations
2409
+ sql:
2410
+ - key: main
2411
+ engine: mysql
2412
+ function:
2413
+ - key: dropMigrations
2414
+ handler: index.dropMigrations
2052
2415
  ```
2053
2416
 
2054
2417
  ### 3. Fetch Schema Trigger
@@ -2056,12 +2419,14 @@ Configure in `manifest.yml`:
2056
2419
  ⚠️ **DEVELOPMENT ONLY**: This trigger is designed for development environments only and should not be used in production.
2057
2420
 
2058
2421
  This trigger retrieves the current database schema from Atlassian Forge SQL and generates SQL statements that can be used to recreate the database structure. It's useful for:
2422
+
2059
2423
  - Development environment setup
2060
2424
  - Schema documentation
2061
2425
  - Database structure verification
2062
2426
  - Creating backup scripts
2063
2427
 
2064
2428
  **Security Considerations**:
2429
+
2065
2430
  - This trigger exposes your database structure
2066
2431
  - It temporarily disables foreign key checks
2067
2432
  - It may expose sensitive table names and structures
@@ -2077,19 +2442,21 @@ export const fetchSchema = async () => {
2077
2442
  ```
2078
2443
 
2079
2444
  Configure in `manifest.yml`:
2445
+
2080
2446
  ```yaml
2081
- webtrigger:
2082
- - key: fetch-schema
2083
- function: fetchSchema
2084
- sql:
2085
- - key: main
2086
- engine: mysql
2087
- function:
2088
- - key: fetchSchema
2089
- handler: index.fetchSchema
2447
+ webtrigger:
2448
+ - key: fetch-schema
2449
+ function: fetchSchema
2450
+ sql:
2451
+ - key: main
2452
+ engine: mysql
2453
+ function:
2454
+ - key: fetchSchema
2455
+ handler: index.fetchSchema
2090
2456
  ```
2091
2457
 
2092
2458
  The response will contain SQL statements like:
2459
+
2093
2460
  ```sql
2094
2461
  SET foreign_key_checks = 0;
2095
2462
  CREATE TABLE IF NOT EXISTS users (...);
@@ -2100,6 +2467,7 @@ SET foreign_key_checks = 1;
2100
2467
  ### 4. Clear Cache Scheduler Trigger
2101
2468
 
2102
2469
  This trigger automatically cleans up expired cache entries based on their TTL (Time To Live). It's useful for:
2470
+
2103
2471
  - Automatic cache maintenance
2104
2472
  - Preventing cache storage from growing indefinitely
2105
2473
  - Ensuring optimal cache performance
@@ -2110,24 +2478,26 @@ This trigger automatically cleans up expired cache entries based on their TTL (T
2110
2478
  import { clearCacheSchedulerTrigger } from "forge-sql-orm";
2111
2479
 
2112
2480
  export const clearCache = () => {
2113
- return clearCacheSchedulerTrigger({
2481
+ return clearCacheSchedulerTrigger({
2114
2482
  cacheEntityName: "cache",
2115
2483
  });
2116
2484
  };
2117
2485
  ```
2118
2486
 
2119
2487
  Configure in `manifest.yml`:
2488
+
2120
2489
  ```yaml
2121
- scheduledTrigger:
2122
- - key: clear-cache-trigger
2123
- function: clearCache
2124
- interval: fiveMinute
2125
- function:
2126
- - key: clearCache
2127
- handler: index.clearCache
2490
+ scheduledTrigger:
2491
+ - key: clear-cache-trigger
2492
+ function: clearCache
2493
+ interval: fiveMinute
2494
+ function:
2495
+ - key: clearCache
2496
+ handler: index.clearCache
2128
2497
  ```
2129
2498
 
2130
2499
  **Available Intervals**:
2500
+
2131
2501
  - `fiveMinute` - Every 5 minutes
2132
2502
  - `hour` - Every hour
2133
2503
  - `day` - Every day
@@ -2148,14 +2518,15 @@ export const slowQueryTrigger = () =>
2148
2518
  ```
2149
2519
 
2150
2520
  Configure in `manifest.yml`:
2521
+
2151
2522
  ```yaml
2152
- scheduledTrigger:
2153
- - key: slow-query-trigger
2154
- function: slowQueryTrigger
2155
- interval: hour
2156
- function:
2157
- - key: slowQueryTrigger
2158
- handler: index.slowQueryTrigger
2523
+ scheduledTrigger:
2524
+ - key: slow-query-trigger
2525
+ function: slowQueryTrigger
2526
+ interval: hour
2527
+ function:
2528
+ - key: slowQueryTrigger
2529
+ handler: index.slowQueryTrigger
2159
2530
  ```
2160
2531
 
2161
2532
  > **💡 Note**: For complete documentation, examples, and configuration options, see the [Slow Query Monitoring](#slow-query-monitoring) section.
@@ -2163,15 +2534,17 @@ Configure in `manifest.yml`:
2163
2534
  ### Important Notes
2164
2535
 
2165
2536
  **Security Considerations**:
2166
- - The drop migrations trigger should be restricted to development environments
2167
- - The fetch schema trigger should only be used in development
2168
- - Consider implementing additional authentication for these endpoints
2537
+
2538
+ - The drop migrations trigger should be restricted to development environments
2539
+ - The fetch schema trigger should only be used in development
2540
+ - Consider implementing additional authentication for these endpoints
2169
2541
 
2170
2542
  **Best Practices**:
2171
- - Always backup your data before using the drop migrations trigger
2172
- - Test migrations in a development environment first
2173
- - Use these triggers as part of your deployment pipeline
2174
- - Monitor the execution logs in the Forge Developer Console
2543
+
2544
+ - Always backup your data before using the drop migrations trigger
2545
+ - Test migrations in a development environment first
2546
+ - Use these triggers as part of your deployment pipeline
2547
+ - Monitor the execution logs in the Forge Developer Console
2175
2548
 
2176
2549
  ## Query Analysis and Performance Optimization
2177
2550
 
@@ -2182,6 +2555,7 @@ Forge-SQL-ORM provides comprehensive query analysis tools to help you optimize y
2182
2555
  ### About Atlassian's Built-in Analysis Tools
2183
2556
 
2184
2557
  Atlassian provides comprehensive query analysis tools in the development console, including:
2558
+
2185
2559
  - Basic query performance metrics
2186
2560
  - Slow query tracking (queries over 500ms)
2187
2561
  - Basic execution statistics
@@ -2281,8 +2655,8 @@ modules:
2281
2655
  scheduledTrigger:
2282
2656
  - key: slow-query-trigger
2283
2657
  function: slowQueryTrigger
2284
- interval: hour # Run every hour
2285
-
2658
+ interval: hour # Run every hour
2659
+
2286
2660
  function:
2287
2661
  - key: slowQueryTrigger
2288
2662
  handler: index.slowQueryTrigger
@@ -2290,10 +2664,10 @@ modules:
2290
2664
 
2291
2665
  #### Configuration Options
2292
2666
 
2293
- | Option | Type | Default | Description |
2294
- |--------|------|---------|-------------|
2295
- | `hours` | `number` | `1` | Number of hours to look back for slow queries |
2296
- | `timeout` | `number` | `3000` | Timeout in milliseconds for the diagnostic query execution |
2667
+ | Option | Type | Default | Description |
2668
+ | --------- | -------- | ------- | ---------------------------------------------------------- |
2669
+ | `hours` | `number` | `1` | Number of hours to look back for slow queries |
2670
+ | `timeout` | `number` | `3000` | Timeout in milliseconds for the diagnostic query execution |
2297
2671
 
2298
2672
  #### Example Console Output
2299
2673
 
@@ -2374,59 +2748,57 @@ const analyzeForgeSql = forgeSQL.analyze();
2374
2748
 
2375
2749
  // Analyze a Drizzle query
2376
2750
  const plan = await analyzeForgeSql.explain(
2377
- forgeSQL.select({
2378
- table1: testEntityJoin1,
2379
- table2: { name: testEntityJoin2.name, email: testEntityJoin2.email },
2380
- count: rawSql<number>`COUNT(*)`,
2381
- table3: {
2382
- table12: testEntityJoin1.name,
2383
- table22: testEntityJoin2.email,
2384
- table32: testEntity.id
2385
- },
2386
- })
2387
- .from(testEntityJoin1)
2388
- .innerJoin(testEntityJoin2, eq(testEntityJoin1.id, testEntityJoin2.id))
2751
+ forgeSQL
2752
+ .select({
2753
+ table1: testEntityJoin1,
2754
+ table2: { name: testEntityJoin2.name, email: testEntityJoin2.email },
2755
+ count: rawSql<number>`COUNT(*)`,
2756
+ table3: {
2757
+ table12: testEntityJoin1.name,
2758
+ table22: testEntityJoin2.email,
2759
+ table32: testEntity.id,
2760
+ },
2761
+ })
2762
+ .from(testEntityJoin1)
2763
+ .innerJoin(testEntityJoin2, eq(testEntityJoin1.id, testEntityJoin2.id)),
2389
2764
  );
2390
2765
 
2391
2766
  // Analyze a raw SQL query
2392
- const rawPlan = await analyzeForgeSql.explainRaw(
2393
- "SELECT * FROM users WHERE id = ?",
2394
- [1]
2395
- );
2767
+ const rawPlan = await analyzeForgeSql.explainRaw("SELECT * FROM users WHERE id = ?", [1]);
2396
2768
 
2397
2769
  // Analyze new methods
2398
2770
  const usersFromPlan = await analyzeForgeSql.explain(
2399
- forgeSQL.selectFrom(users).where(eq(users.active, true))
2771
+ forgeSQL.selectFrom(users).where(eq(users.active, true)),
2400
2772
  );
2401
2773
 
2402
2774
  const usersCacheablePlan = await analyzeForgeSql.explain(
2403
- forgeSQL.selectCacheableFrom(users).where(eq(users.active, true))
2775
+ forgeSQL.selectCacheableFrom(users).where(eq(users.active, true)),
2404
2776
  );
2405
2777
 
2406
2778
  // Analyze Common Table Expressions (CTEs)
2407
2779
  const ctePlan = await analyzeForgeSql.explain(
2408
2780
  forgeSQL
2409
2781
  .with(
2410
- forgeSQL.selectFrom(users).where(eq(users.active, true)).as('activeUsers'),
2411
- forgeSQL.selectFrom(orders).where(eq(orders.status, 'completed')).as('completedOrders')
2782
+ forgeSQL.selectFrom(users).where(eq(users.active, true)).as("activeUsers"),
2783
+ forgeSQL.selectFrom(orders).where(eq(orders.status, "completed")).as("completedOrders"),
2412
2784
  )
2413
2785
  .select({
2414
2786
  totalActiveUsers: sql`COUNT(au.id)`,
2415
- totalCompletedOrders: sql`COUNT(co.id)`
2787
+ totalCompletedOrders: sql`COUNT(co.id)`,
2416
2788
  })
2417
2789
  .from(sql`activeUsers au`)
2418
- .leftJoin(sql`completedOrders co`, eq(sql`au.id`, sql`co.userId`))
2790
+ .leftJoin(sql`completedOrders co`, eq(sql`au.id`, sql`co.userId`)),
2419
2791
  );
2420
2792
  ```
2421
2793
 
2422
2794
  This analysis provides insights into:
2795
+
2423
2796
  - How the database executes your query
2424
2797
  - Which indexes are being used
2425
2798
  - Estimated vs actual row counts
2426
2799
  - Resource usage at each step
2427
2800
  - Performance optimization opportunities
2428
2801
 
2429
-
2430
2802
  ## Migration Guide
2431
2803
 
2432
2804
  ### Migrating from 2.0.x to 2.1.x
@@ -2436,18 +2808,20 @@ This section covers the breaking changes introduced in version 2.1.x and how to
2436
2808
  #### 1. Method Renaming (BREAKING CHANGES)
2437
2809
 
2438
2810
  **Removed Methods:**
2811
+
2439
2812
  - `forgeSQL.modify()` → **REMOVED** (use `forgeSQL.modifyWithVersioning()`)
2440
2813
  - `forgeSQL.crud()` → **REMOVED** (use `forgeSQL.modifyWithVersioning()`)
2441
2814
 
2442
2815
  **Migration Steps:**
2443
2816
 
2444
2817
  1. **Replace `modify()` calls:**
2818
+
2445
2819
  ```typescript
2446
2820
  // ❌ Old (2.0.x) - NO LONGER WORKS
2447
2821
  await forgeSQL.modify().insert(Users, [userData]);
2448
2822
  await forgeSQL.modify().updateById(updateData, Users);
2449
2823
  await forgeSQL.modify().deleteById(1, Users);
2450
-
2824
+
2451
2825
  // ✅ New (2.1.x) - REQUIRED
2452
2826
  await forgeSQL.modifyWithVersioning().insert(Users, [userData]);
2453
2827
  await forgeSQL.modifyWithVersioning().updateById(updateData, Users);
@@ -2455,12 +2829,13 @@ This section covers the breaking changes introduced in version 2.1.x and how to
2455
2829
  ```
2456
2830
 
2457
2831
  2. **Replace `crud()` calls:**
2832
+
2458
2833
  ```typescript
2459
2834
  // ❌ Old (2.0.x) - NO LONGER WORKS
2460
2835
  await forgeSQL.crud().insert(Users, [userData]);
2461
2836
  await forgeSQL.crud().updateById(updateData, Users);
2462
2837
  await forgeSQL.crud().deleteById(1, Users);
2463
-
2838
+
2464
2839
  // ✅ New (2.1.x) - REQUIRED
2465
2840
  await forgeSQL.modifyWithVersioning().insert(Users, [userData]);
2466
2841
  await forgeSQL.modifyWithVersioning().updateById(updateData, Users);
@@ -2470,8 +2845,9 @@ This section covers the breaking changes introduced in version 2.1.x and how to
2470
2845
  #### 2. New API Methods
2471
2846
 
2472
2847
  **New Methods Available:**
2848
+
2473
2849
  - `forgeSQL.insert()` - Basic Drizzle operations
2474
- - `forgeSQL.update()` - Basic Drizzle operations
2850
+ - `forgeSQL.update()` - Basic Drizzle operations
2475
2851
  - `forgeSQL.delete()` - Basic Drizzle operations
2476
2852
  - `forgeSQL.insertAndEvictCache()` - Basic Drizzle operations with evict cache after execution
2477
2853
  - `forgeSQL.updateAndEvictCache()` - Basic Drizzle operations with evict cache after execution
@@ -2499,47 +2875,43 @@ await forgeSQL.insert(Users).values(userData);
2499
2875
  await forgeSQL.modifyWithVersioningAndEvictCache().insert(Users, [userData]);
2500
2876
 
2501
2877
  // ✅ New query methods for better performance
2502
- const users = await forgeSQL.selectFrom(Users)
2503
- .where(eq(Users.active, true));
2878
+ const users = await forgeSQL.selectFrom(Users).where(eq(Users.active, true));
2504
2879
 
2505
- const usersDistinct = await forgeSQL.selectDistinctFrom(Users)
2506
- .where(eq(Users.active, true));
2880
+ const usersDistinct = await forgeSQL.selectDistinctFrom(Users).where(eq(Users.active, true));
2507
2881
 
2508
- const usersCacheable = await forgeSQL.selectCacheableFrom(Users)
2509
- .where(eq(Users.active, true));
2882
+ const usersCacheable = await forgeSQL.selectCacheableFrom(Users).where(eq(Users.active, true));
2510
2883
 
2511
2884
  // ✅ Raw SQL execution with caching
2512
- const rawUsers = await forgeSQL.execute(
2513
- "SELECT * FROM users WHERE active = ?",
2514
- [true]
2515
- );
2885
+ const rawUsers = await forgeSQL.execute("SELECT * FROM users WHERE active = ?", [true]);
2516
2886
 
2517
2887
  // ⚠️ IMPORTANT: When using executeCacheable(), all table names must be wrapped with backticks (`)
2518
2888
  const cachedRawUsers = await forgeSQL.executeCacheable(
2519
- "SELECT * FROM `users` WHERE active = ?",
2520
- [true],
2521
- 300
2889
+ "SELECT * FROM `users` WHERE active = ?",
2890
+ [true],
2891
+ 300,
2522
2892
  );
2523
2893
 
2524
2894
  // ✅ Raw SQL execution with metadata capture and performance monitoring
2525
2895
  const usersWithMetadata = await forgeSQL.executeWithMetadata(
2526
2896
  async () => {
2527
2897
  const users = await forgeSQL.selectFrom(usersTable);
2528
- const orders = await forgeSQL.selectFrom(ordersTable).where(eq(ordersTable.userId, usersTable.id));
2898
+ const orders = await forgeSQL
2899
+ .selectFrom(ordersTable)
2900
+ .where(eq(ordersTable.userId, usersTable.id));
2529
2901
  return { users, orders };
2530
2902
  },
2531
2903
  (totalDbExecutionTime, totalResponseSize, printQueriesWithPlan) => {
2532
2904
  const threshold = 500; // ms baseline for this resolver
2533
-
2905
+
2534
2906
  if (totalDbExecutionTime > threshold * 1.5) {
2535
2907
  console.warn(`[Performance Warning] Resolver exceeded DB time: ${totalDbExecutionTime} ms`);
2536
2908
  await printQueriesWithPlan(); // Analyze and print query execution plans
2537
2909
  } else if (totalDbExecutionTime > threshold) {
2538
2910
  console.debug(`[Performance Debug] High DB time: ${totalDbExecutionTime} ms`);
2539
2911
  }
2540
-
2912
+
2541
2913
  console.log(`DB response size: ${totalResponseSize} bytes`);
2542
- }
2914
+ },
2543
2915
  );
2544
2916
 
2545
2917
  // ✅ DDL operations for schema modifications
@@ -2563,25 +2935,25 @@ await forgeSQL.executeDDLActions(async () => {
2563
2935
  SELECT * FROM INFORMATION_SCHEMA.STATEMENTS_SUMMARY
2564
2936
  WHERE AVG_LATENCY > 1000000
2565
2937
  `);
2566
-
2938
+
2567
2939
  // Execute complex analysis queries in DDL context
2568
2940
  const performanceData = await forgeSQL.execute(`
2569
2941
  SELECT * FROM INFORMATION_SCHEMA.CLUSTER_STATEMENTS_SUMMARY_HISTORY
2570
2942
  WHERE SUMMARY_END_TIME > DATE_SUB(NOW(), INTERVAL 1 HOUR)
2571
2943
  `);
2572
-
2944
+
2573
2945
  return { slowQueries, performanceData };
2574
2946
  });
2575
2947
 
2576
2948
  // ✅ Common Table Expressions (CTEs)
2577
2949
  const userStats = await forgeSQL
2578
2950
  .with(
2579
- forgeSQL.selectFrom(users).where(eq(users.active, true)).as('activeUsers'),
2580
- forgeSQL.selectFrom(orders).where(eq(orders.status, 'completed')).as('completedOrders')
2951
+ forgeSQL.selectFrom(users).where(eq(users.active, true)).as("activeUsers"),
2952
+ forgeSQL.selectFrom(orders).where(eq(orders.status, "completed")).as("completedOrders"),
2581
2953
  )
2582
2954
  .select({
2583
2955
  totalActiveUsers: sql`COUNT(au.id)`,
2584
- totalCompletedOrders: sql`COUNT(co.id)`
2956
+ totalCompletedOrders: sql`COUNT(co.id)`,
2585
2957
  })
2586
2958
  .from(sql`activeUsers au`)
2587
2959
  .leftJoin(sql`completedOrders co`, eq(sql`au.id`, sql`co.userId`));
@@ -2595,7 +2967,7 @@ You can use a simple find-and-replace to migrate your code:
2595
2967
  # Replace modify() calls
2596
2968
  find . -name "*.ts" -o -name "*.js" | xargs sed -i 's/forgeSQL\.modify()/forgeSQL.modifyWithVersioning()/g'
2597
2969
 
2598
- # Replace crud() calls
2970
+ # Replace crud() calls
2599
2971
  find . -name "*.ts" -o -name "*.js" | xargs sed -i 's/forgeSQL\.crud()/forgeSQL.modifyWithVersioning()/g'
2600
2972
  ```
2601
2973
 
@@ -2607,5 +2979,6 @@ find . -name "*.ts" -o -name "*.js" | xargs sed -i 's/forgeSQL\.crud()/forgeSQL.
2607
2979
  - ✅ **Migration Required**: You must update your code to use the new methods
2608
2980
 
2609
2981
  ## License
2982
+
2610
2983
  This project is licensed under the **MIT License**.
2611
2984
  Feel free to use it for commercial and personal projects.