forge-sql-orm 2.1.12 → 2.1.13

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 (66) hide show
  1. package/README.md +662 -548
  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.js +1191 -0
  10. package/dist/core/ForgeSQLORM.js.map +1 -0
  11. package/dist/core/ForgeSQLQueryBuilder.js +77 -0
  12. package/dist/core/ForgeSQLQueryBuilder.js.map +1 -0
  13. package/dist/core/ForgeSQLSelectOperations.js +81 -0
  14. package/dist/core/ForgeSQLSelectOperations.js.map +1 -0
  15. package/dist/core/SystemTables.js +258 -0
  16. package/dist/core/SystemTables.js.map +1 -0
  17. package/dist/index.js +30 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/lib/drizzle/extensions/additionalActions.d.ts.map +1 -1
  20. package/dist/lib/drizzle/extensions/additionalActions.js +527 -0
  21. package/dist/lib/drizzle/extensions/additionalActions.js.map +1 -0
  22. package/dist/utils/cacheContextUtils.d.ts.map +1 -1
  23. package/dist/utils/cacheContextUtils.js +198 -0
  24. package/dist/utils/cacheContextUtils.js.map +1 -0
  25. package/dist/utils/cacheUtils.d.ts.map +1 -1
  26. package/dist/utils/cacheUtils.js +383 -0
  27. package/dist/utils/cacheUtils.js.map +1 -0
  28. package/dist/utils/forgeDriver.d.ts.map +1 -1
  29. package/dist/utils/forgeDriver.js +139 -0
  30. package/dist/utils/forgeDriver.js.map +1 -0
  31. package/dist/utils/forgeDriverProxy.js +68 -0
  32. package/dist/utils/forgeDriverProxy.js.map +1 -0
  33. package/dist/utils/metadataContextUtils.js +28 -0
  34. package/dist/utils/metadataContextUtils.js.map +1 -0
  35. package/dist/utils/requestTypeContextUtils.js +10 -0
  36. package/dist/utils/requestTypeContextUtils.js.map +1 -0
  37. package/dist/utils/sqlHints.js +52 -0
  38. package/dist/utils/sqlHints.js.map +1 -0
  39. package/dist/utils/sqlUtils.d.ts.map +1 -1
  40. package/dist/utils/sqlUtils.js +590 -0
  41. package/dist/utils/sqlUtils.js.map +1 -0
  42. package/dist/webtriggers/applyMigrationsWebTrigger.js +77 -0
  43. package/dist/webtriggers/applyMigrationsWebTrigger.js.map +1 -0
  44. package/dist/webtriggers/clearCacheSchedulerTrigger.js +83 -0
  45. package/dist/webtriggers/clearCacheSchedulerTrigger.js.map +1 -0
  46. package/dist/webtriggers/dropMigrationWebTrigger.js +54 -0
  47. package/dist/webtriggers/dropMigrationWebTrigger.js.map +1 -0
  48. package/dist/webtriggers/dropTablesMigrationWebTrigger.js +54 -0
  49. package/dist/webtriggers/dropTablesMigrationWebTrigger.js.map +1 -0
  50. package/dist/webtriggers/fetchSchemaWebTrigger.js +82 -0
  51. package/dist/webtriggers/fetchSchemaWebTrigger.js.map +1 -0
  52. package/dist/webtriggers/index.js +40 -0
  53. package/dist/webtriggers/index.js.map +1 -0
  54. package/dist/webtriggers/slowQuerySchedulerTrigger.js +80 -0
  55. package/dist/webtriggers/slowQuerySchedulerTrigger.js.map +1 -0
  56. package/package.json +28 -23
  57. package/src/core/ForgeSQLAnalyseOperations.ts +3 -2
  58. package/src/lib/drizzle/extensions/additionalActions.ts +11 -0
  59. package/src/utils/cacheContextUtils.ts +9 -6
  60. package/src/utils/cacheUtils.ts +6 -4
  61. package/src/utils/forgeDriver.ts +3 -7
  62. package/src/utils/sqlUtils.ts +33 -34
  63. package/dist/ForgeSQLORM.js +0 -3922
  64. package/dist/ForgeSQLORM.js.map +0 -1
  65. package/dist/ForgeSQLORM.mjs +0 -3905
  66. 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/) )
@@ -37,6 +37,7 @@
37
37
  ## Table of Contents
38
38
 
39
39
  ### 🚀 Getting Started
40
+
40
41
  - [Key Features](#key-features)
41
42
  - [Usage Approaches](#usage-approaches)
42
43
  - [Installation](#installation)
@@ -44,16 +45,19 @@
44
45
  - [Quick Start](#quick-start)
45
46
 
46
47
  ### 📖 Core Features
48
+
47
49
  - [Field Name Collision Prevention](#field-name-collision-prevention-in-complex-queries)
48
50
  - [Drizzle Usage with forge-sql-orm](#drizzle-usage-with-forge-sql-orm)
49
51
  - [Direct Drizzle Usage with Custom Driver](#direct-drizzle-usage-with-custom-driver)
50
52
 
51
53
  ### 🗄️ Database Operations
54
+
52
55
  - [Fetch Data](#fetch-data)
53
56
  - [Modify Operations](#modify-operations)
54
57
  - [SQL Utilities](#sql-utilities)
55
58
 
56
59
  ### ⚡ Caching System
60
+
57
61
  - [Setting Up Caching with @forge/kvs](#setting-up-caching-with-forgekvs-optional)
58
62
  - [Global Cache System (Level 2)](#global-cache-system-level-2)
59
63
  - [Cache Context Operations](#cache-context-operations)
@@ -62,6 +66,7 @@
62
66
  - [Manual Cache Management](#manual-cache-management)
63
67
 
64
68
  ### 🔒 Advanced Features
69
+
65
70
  - [Optimistic Locking](#optimistic-locking)
66
71
  - [Query Analysis and Performance Optimization](#query-analysis-and-performance-optimization)
67
72
  - [Automatic Error Analysis](#automatic-error-analysis) - Automatic timeout and OOM error detection with execution plans
@@ -69,12 +74,14 @@
69
74
  - [Date and Time Types](#date-and-time-types)
70
75
 
71
76
  ### 🛠️ Development Tools
77
+
72
78
  - [CLI Commands](#cli-commands) | [CLI Documentation](forge-sql-orm-cli/README.md)
73
79
  - [Web Triggers for Migrations](#web-triggers-for-migrations)
74
80
  - [Step-by-Step Migration Workflow](#step-by-step-migration-workflow)
75
81
  - [Drop Migrations](#drop-migrations)
76
82
 
77
83
  ### 📚 Examples
84
+
78
85
  - [Simple Example](examples/forge-sql-orm-example-simple)
79
86
  - [Drizzle Driver Example](examples/forge-sql-orm-example-drizzle-driver-simple)
80
87
  - [Optimistic Locking Example](examples/forge-sql-orm-example-optimistic-locking)
@@ -85,17 +92,20 @@
85
92
  - [Cache Example](examples/forge-sql-orm-example-cache) - Advanced caching capabilities with performance monitoring
86
93
 
87
94
  ### 📚 Reference
95
+
88
96
  - [ForgeSqlOrmOptions](#forgesqlormoptions)
89
97
  - [Migration Guide](#migration-guide)
90
98
 
91
99
  ## 🚀 Quick Navigation
92
100
 
93
101
  **New to Forge-SQL-ORM?** Start here:
102
+
94
103
  - [Quick Start](#quick-start) - Get up and running in 5 minutes
95
104
  - [Installation](#installation) - Complete setup guide
96
105
  - [Basic Usage Examples](#fetch-data) - Simple query examples
97
106
 
98
107
  **Looking for specific features?**
108
+
99
109
  - [Global Cache System (Level 2)](#global-cache-system-level-2) - Cross-invocation persistent caching
100
110
  - [Local Cache System (Level 1)](#local-cache-operations-level-1) - In-memory invocation caching
101
111
  - [Optimistic Locking](#optimistic-locking) - Data consistency
@@ -103,6 +113,7 @@
103
113
  - [Query Analysis](#query-analysis-and-performance-optimization) - Performance optimization
104
114
 
105
115
  **Looking for practical examples?**
116
+
106
117
  - [Simple Example](examples/forge-sql-orm-example-simple) - Basic ORM usage
107
118
  - [Optimistic Locking Example](examples/forge-sql-orm-example-optimistic-locking) - Real-world conflict handling
108
119
  - [Organization Tracker Example](examples/forge-sql-orm-example-org-tracker) - Complex relationships
@@ -111,24 +122,27 @@
111
122
 
112
123
  ## Usage Approaches
113
124
 
114
-
115
125
  ### 1. Full Forge-SQL-ORM Usage
126
+
116
127
  ```typescript
117
128
  import ForgeSQL from "forge-sql-orm";
118
129
  const forgeSQL = new ForgeSQL();
119
130
  ```
131
+
120
132
  Best for: Advanced features like optimistic locking, automatic versioning, and automatic field name collision prevention in complex queries.
121
133
 
122
134
  ### 2. Direct Drizzle Usage
135
+
123
136
  ```typescript
124
137
  import { drizzle } from "drizzle-orm/mysql-proxy";
125
138
  import { forgeDriver } from "forge-sql-orm";
126
139
  const db = drizzle(forgeDriver);
127
140
  ```
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
141
 
142
+ 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
143
 
131
144
  ### 3. Local Cache Optimization
145
+
132
146
  ```typescript
133
147
  import ForgeSQL from "forge-sql-orm";
134
148
  const forgeSQL = new ForgeSQL();
@@ -136,28 +150,28 @@ const forgeSQL = new ForgeSQL();
136
150
  // Optimize repeated queries within a single invocation
137
151
  await forgeSQL.executeWithLocalContext(async () => {
138
152
  // 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
-
153
+ const users = await forgeSQL
154
+ .select({ id: users.id, name: users.name })
155
+ .from(users)
156
+ .where(eq(users.active, true));
157
+
142
158
  // 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)
159
+ const cachedUsers = await forgeSQL
160
+ .select({ id: users.id, name: users.name })
161
+ .from(users)
148
162
  .where(eq(users.active, true));
149
-
163
+
164
+ // Using new methods for better performance
165
+ const usersFrom = await forgeSQL.selectFrom(users).where(eq(users.active, true));
166
+
150
167
  // This will use local cache (no database call)
151
- const cachedUsersFrom = await forgeSQL.selectFrom(users)
152
- .where(eq(users.active, true));
153
-
168
+ const cachedUsersFrom = await forgeSQL.selectFrom(users).where(eq(users.active, true));
169
+
154
170
  // Raw SQL with local caching
155
- const rawUsers = await forgeSQL.execute(
156
- "SELECT id, name FROM users WHERE active = ?",
157
- [true]
158
- );
171
+ const rawUsers = await forgeSQL.execute("SELECT id, name FROM users WHERE active = ?", [true]);
159
172
  });
160
173
  ```
174
+
161
175
  Best for: Performance optimization of repeated queries within resolvers or single invocation contexts.
162
176
 
163
177
  ## Field Name Collision Prevention in Complex Queries
@@ -167,6 +181,7 @@ When working with complex queries involving multiple tables (joins, inner joins,
167
181
  Forge-SQL-ORM provides two ways to handle this:
168
182
 
169
183
  ### Using Forge-SQL-ORM
184
+
170
185
  ```typescript
171
186
  import ForgeSQL from "forge-sql-orm";
172
187
 
@@ -174,12 +189,13 @@ const forgeSQL = new ForgeSQL();
174
189
 
175
190
  // Automatic field name collision prevention
176
191
  await forgeSQL
177
- .select({user: users, order: orders})
192
+ .select({ user: users, order: orders })
178
193
  .from(orders)
179
194
  .innerJoin(users, eq(orders.userId, users.id));
180
195
  ```
181
196
 
182
197
  ### Using Direct Drizzle
198
+
183
199
  ```typescript
184
200
  import { drizzle } from "drizzle-orm/mysql-proxy";
185
201
  import { forgeDriver, patchDbWithSelectAliased } from "forge-sql-orm";
@@ -188,18 +204,18 @@ const db = patchDbWithSelectAliased(drizzle(forgeDriver));
188
204
 
189
205
  // Manual field name collision prevention
190
206
  await db
191
- .selectAliased({user: users, order: orders})
207
+ .selectAliased({ user: users, order: orders })
192
208
  .from(orders)
193
209
  .innerJoin(users, eq(orders.userId, users.id));
194
210
  ```
195
211
 
196
212
  ### Important Notes
213
+
197
214
  - This is a specific behavior of Atlassian Forge SQL, not Drizzle ORM
198
215
  - For complex queries involving multiple tables, it's recommended to always specify select fields and avoid using `select()` without field selection
199
216
  - The solution automatically creates unique aliases for each field by prefixing them with the table name
200
217
  - This ensures that fields with the same name from different tables remain distinct in the query results
201
218
 
202
-
203
219
  ## Installation
204
220
 
205
221
  Forge-SQL-ORM is designed to work with @forge/sql and requires some additional setup to ensure compatibility within Atlassian Forge.
@@ -207,16 +223,35 @@ Forge-SQL-ORM is designed to work with @forge/sql and requires some additional s
207
223
  ✅ Step 1: Install Dependencies
208
224
 
209
225
  **Basic installation (without caching):**
226
+
210
227
  ```sh
211
228
  npm install forge-sql-orm @forge/sql drizzle-orm -S
212
229
  ```
213
230
 
214
231
  **With caching support:**
232
+
215
233
  ```sh
216
234
  npm install forge-sql-orm @forge/sql @forge/kvs drizzle-orm -S
217
235
  ```
218
236
 
237
+ **⚠️ Important for UI-Kit projects:**
238
+
239
+ 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`).
240
+
241
+ To resolve this, use the `--legacy-peer-deps` flag:
242
+
243
+ ```sh
244
+ # Basic installation for UI-Kit projects
245
+ npm install forge-sql-orm @forge/sql drizzle-orm -S --legacy-peer-deps
246
+
247
+ # With caching support for UI-Kit projects
248
+ npm install forge-sql-orm @forge/sql @forge/kvs drizzle-orm -S --legacy-peer-deps
249
+ ```
250
+
251
+ **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.
252
+
219
253
  This will:
254
+
220
255
  - Install Forge-SQL-ORM (the ORM for @forge/sql)
221
256
  - Install @forge/sql, the Forge database layer
222
257
  - Install @forge/kvs, the Forge Key-Value Store for caching (optional, only needed for caching features)
@@ -227,6 +262,7 @@ This will:
227
262
  ## Quick Start
228
263
 
229
264
  ### 1. Basic Setup
265
+
230
266
  ```typescript
231
267
  import ForgeSQL from "forge-sql-orm";
232
268
 
@@ -238,44 +274,49 @@ const users = await forgeSQL.select().from(users);
238
274
  ```
239
275
 
240
276
  ### 2. With Caching (Optional)
277
+
241
278
  ```typescript
242
279
  import ForgeSQL from "forge-sql-orm";
243
280
 
244
281
  // Initialize with caching
245
282
  const forgeSQL = new ForgeSQL({
246
283
  cacheEntityName: "cache",
247
- cacheTTL: 300
284
+ cacheTTL: 300,
248
285
  });
249
286
 
250
287
  // Cached query
251
- const users = await forgeSQL.selectCacheable({ id: users.id, name: users.name })
252
- .from(users).where(eq(users.active, true));
288
+ const users = await forgeSQL
289
+ .selectCacheable({ id: users.id, name: users.name })
290
+ .from(users)
291
+ .where(eq(users.active, true));
253
292
  ```
254
293
 
255
294
  ### 3. Local Cache Optimization
295
+
256
296
  ```typescript
257
297
  // Optimize repeated queries within a single invocation
258
298
  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
-
299
+ const users = await forgeSQL
300
+ .select({ id: users.id, name: users.name })
301
+ .from(users)
302
+ .where(eq(users.active, true));
303
+
262
304
  // 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)
305
+ const cachedUsers = await forgeSQL
306
+ .select({ id: users.id, name: users.name })
307
+ .from(users)
268
308
  .where(eq(users.active, true));
269
-
309
+
310
+ // Using new methods for better performance
311
+ const usersFrom = await forgeSQL.selectFrom(users).where(eq(users.active, true));
312
+
270
313
  // Raw SQL with local caching
271
- const rawUsers = await forgeSQL.execute(
272
- "SELECT id, name FROM users WHERE active = ?",
273
- [true]
274
- );
314
+ const rawUsers = await forgeSQL.execute("SELECT id, name FROM users WHERE active = ?", [true]);
275
315
  });
276
316
  ```
277
317
 
278
318
  ### 4. Resolver Performance Monitoring
319
+
279
320
  ```typescript
280
321
  // Resolver with performance monitoring
281
322
  resolver.define("fetch", async (req: Request) => {
@@ -284,20 +325,23 @@ resolver.define("fetch", async (req: Request) => {
284
325
  async () => {
285
326
  // Resolver logic with multiple queries
286
327
  const users = await forgeSQL.selectFrom(demoUsers);
287
- const orders = await forgeSQL.selectFrom(demoOrders)
328
+ const orders = await forgeSQL
329
+ .selectFrom(demoOrders)
288
330
  .where(eq(demoOrders.userId, demoUsers.id));
289
331
  return { users, orders };
290
332
  },
291
333
  async (totalDbExecutionTime, totalResponseSize, printQueriesWithPlan) => {
292
334
  const threshold = 500; // ms baseline for this resolver
293
-
335
+
294
336
  if (totalDbExecutionTime > threshold * 1.5) {
295
- console.warn(`[Performance Warning fetch] Resolver exceeded DB time: ${totalDbExecutionTime} ms`);
337
+ console.warn(
338
+ `[Performance Warning fetch] Resolver exceeded DB time: ${totalDbExecutionTime} ms`,
339
+ );
296
340
  await printQueriesWithPlan(); // Optionally log or capture diagnostics for further analysis
297
341
  } else if (totalDbExecutionTime > threshold) {
298
342
  console.debug(`[Performance Debug fetch] High DB time: ${totalDbExecutionTime} ms`);
299
343
  }
300
- }
344
+ },
301
345
  );
302
346
  } catch (e) {
303
347
  const error = e?.cause?.debug?.sqlMessage ?? e?.cause;
@@ -308,6 +352,7 @@ resolver.define("fetch", async (req: Request) => {
308
352
  ```
309
353
 
310
354
  ### 5. Next Steps
355
+
311
356
  - [Full Installation Guide](#installation) - Complete setup instructions
312
357
  - [Core Features](#core-features) - Learn about key capabilities
313
358
  - [Global Cache System (Level 2)](#global-cache-system-level-2) - Cross-invocation caching features
@@ -343,48 +388,44 @@ const db = forgeSQL.getDrizzleQueryBuilder();
343
388
  const users = await db.select().from(users);
344
389
 
345
390
  // Using new methods for enhanced functionality
346
- const usersFrom = await forgeSQL.selectFrom(users)
347
- .where(eq(users.active, true));
391
+ const usersFrom = await forgeSQL.selectFrom(users).where(eq(users.active, true));
348
392
 
349
- const usersDistinct = await forgeSQL.selectDistinctFrom(users)
350
- .where(eq(users.active, true));
393
+ const usersDistinct = await forgeSQL.selectDistinctFrom(users).where(eq(users.active, true));
351
394
 
352
- const usersCacheable = await forgeSQL.selectCacheableFrom(users)
353
- .where(eq(users.active, true));
395
+ const usersCacheable = await forgeSQL.selectCacheableFrom(users).where(eq(users.active, true));
354
396
 
355
397
  // Raw SQL execution
356
- const rawUsers = await forgeSQL.execute(
357
- "SELECT * FROM users WHERE active = ?",
358
- [true]
359
- );
398
+ const rawUsers = await forgeSQL.execute("SELECT * FROM users WHERE active = ?", [true]);
360
399
 
361
400
  // Raw SQL with caching
362
401
  // ⚠️ IMPORTANT: When using executeCacheable(), all table names must be wrapped with backticks (`)
363
402
  const cachedRawUsers = await forgeSQL.executeCacheable(
364
- "SELECT * FROM `users` WHERE active = ?",
365
- [true],
366
- 300
403
+ "SELECT * FROM `users` WHERE active = ?",
404
+ [true],
405
+ 300,
367
406
  );
368
407
 
369
408
  // Raw SQL with execution metadata and performance monitoring
370
409
  const usersWithMetadata = await forgeSQL.executeWithMetadata(
371
410
  async () => {
372
411
  const users = await forgeSQL.selectFrom(usersTable);
373
- const orders = await forgeSQL.selectFrom(ordersTable).where(eq(ordersTable.userId, usersTable.id));
412
+ const orders = await forgeSQL
413
+ .selectFrom(ordersTable)
414
+ .where(eq(ordersTable.userId, usersTable.id));
374
415
  return { users, orders };
375
416
  },
376
417
  (totalDbExecutionTime, totalResponseSize, printQueriesWithPlan) => {
377
418
  const threshold = 500; // ms baseline for this resolver
378
-
419
+
379
420
  if (totalDbExecutionTime > threshold * 1.5) {
380
421
  console.warn(`[Performance Warning] Resolver exceeded DB time: ${totalDbExecutionTime} ms`);
381
422
  await printQueriesWithPlan(); // Analyze and print query execution plans
382
423
  } else if (totalDbExecutionTime > threshold) {
383
424
  console.debug(`[Performance Debug] High DB time: ${totalDbExecutionTime} ms`);
384
425
  }
385
-
426
+
386
427
  console.log(`DB response size: ${totalResponseSize} bytes`);
387
- }
428
+ },
388
429
  );
389
430
 
390
431
  // DDL operations for schema modifications
@@ -403,25 +444,25 @@ await forgeSQL.executeDDLActions(async () => {
403
444
  SELECT * FROM INFORMATION_SCHEMA.STATEMENTS_SUMMARY
404
445
  WHERE AVG_LATENCY > 1000000
405
446
  `);
406
-
447
+
407
448
  // Execute complex analysis queries in DDL context
408
449
  const performanceData = await forgeSQL.execute(`
409
450
  SELECT * FROM INFORMATION_SCHEMA.CLUSTER_STATEMENTS_SUMMARY_HISTORY
410
451
  WHERE SUMMARY_END_TIME > DATE_SUB(NOW(), INTERVAL 1 HOUR)
411
452
  `);
412
-
453
+
413
454
  return { slowQueries, performanceData };
414
455
  });
415
456
 
416
457
  // Common Table Expressions (CTEs)
417
458
  const userStats = await forgeSQL
418
459
  .with(
419
- forgeSQL.selectFrom(users).where(eq(users.active, true)).as('activeUsers'),
420
- forgeSQL.selectFrom(orders).where(eq(orders.status, 'completed')).as('completedOrders')
460
+ forgeSQL.selectFrom(users).where(eq(users.active, true)).as("activeUsers"),
461
+ forgeSQL.selectFrom(orders).where(eq(orders.status, "completed")).as("completedOrders"),
421
462
  )
422
463
  .select({
423
464
  totalActiveUsers: sql`COUNT(au.id)`,
424
- totalCompletedOrders: sql`COUNT(co.id)`
465
+ totalCompletedOrders: sql`COUNT(co.id)`,
425
466
  })
426
467
  .from(sql`activeUsers au`)
427
468
  .leftJoin(sql`completedOrders co`, eq(sql`au.id`, sql`co.userId`));
@@ -460,7 +501,7 @@ await forgeSQL.executeWithCacheContext(async () => {
460
501
  await db.updateWithCacheContext(users)...;
461
502
  await db.deleteWithCacheContext(users)...;
462
503
  // invoke without cache
463
- const users = await db.selectAliasedCacheable(getTableColumns(users)).from(users);
504
+ const users = await db.selectAliasedCacheable(getTableColumns(users)).from(users);
464
505
  // Cache is cleared only once at the end for all affected tables
465
506
  });
466
507
 
@@ -476,15 +517,15 @@ const usersCacheable = await forgeSQL.selectCacheableFrom(users)
476
517
 
477
518
  // Raw SQL execution
478
519
  const rawUsers = await forgeSQL.execute(
479
- "SELECT * FROM users WHERE active = ?",
520
+ "SELECT * FROM users WHERE active = ?",
480
521
  [true]
481
522
  );
482
523
 
483
524
  // Raw SQL with caching
484
525
  // ⚠️ IMPORTANT: When using executeCacheable(), all table names must be wrapped with backticks (`)
485
526
  const cachedRawUsers = await forgeSQL.executeCacheable(
486
- "SELECT * FROM `users` WHERE active = ?",
487
- [true],
527
+ "SELECT * FROM `users` WHERE active = ?",
528
+ [true],
488
529
  300
489
530
  );
490
531
 
@@ -497,14 +538,14 @@ const usersWithMetadata = await forgeSQL.executeWithMetadata(
497
538
  },
498
539
  (totalDbExecutionTime, totalResponseSize, printQueriesWithPlan) => {
499
540
  const threshold = 500; // ms baseline for this resolver
500
-
541
+
501
542
  if (totalDbExecutionTime > threshold * 1.5) {
502
543
  console.warn(`[Performance Warning] Resolver exceeded DB time: ${totalDbExecutionTime} ms`);
503
544
  await printQueriesWithPlan(); // Analyze and print query execution plans
504
545
  } else if (totalDbExecutionTime > threshold) {
505
546
  console.debug(`[Performance Debug] High DB time: ${totalDbExecutionTime} ms`);
506
547
  }
507
-
548
+
508
549
  console.log(`DB response size: ${totalResponseSize} bytes`);
509
550
  }
510
551
  );
@@ -519,6 +560,7 @@ The caching system is optional and only needed if you want to use cache-related
519
560
  To use caching, you need to use Forge-SQL-ORM methods that support cache management:
520
561
 
521
562
  **Methods that perform cache eviction after execution and in cache context (batch eviction):**
563
+
522
564
  - `forgeSQL.insertAndEvictCache()`
523
565
  - `forgeSQL.updateAndEvictCache()`
524
566
  - `forgeSQL.deleteAndEvictCache()`
@@ -528,6 +570,7 @@ To use caching, you need to use Forge-SQL-ORM methods that support cache managem
528
570
  - `forgeSQL.getDrizzleQueryBuilder().deleteAndEvictCache()`
529
571
 
530
572
  **Methods that participate in cache context only (batch eviction):**
573
+
531
574
  - All methods except the default Drizzle methods:
532
575
  - `forgeSQL.insert()`
533
576
  - `forgeSQL.update()`
@@ -538,17 +581,20 @@ To use caching, you need to use Forge-SQL-ORM methods that support cache managem
538
581
  - `forgeSQL.getDrizzleQueryBuilder().deleteWithCacheContext()`
539
582
 
540
583
  **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()`
584
+
585
+ - `forgeSQL.getDrizzleQueryBuilder().insert()`
586
+ - `forgeSQL.getDrizzleQueryBuilder().update()`
587
+ - `forgeSQL.getDrizzleQueryBuilder().delete()`
544
588
 
545
589
  **Cacheable methods:**
546
- - `forgeSQL.selectCacheable()`
547
- - `forgeSQL.selectDistinctCacheable()`
548
- - `forgeSQL.getDrizzleQueryBuilder().selectAliasedCacheable()`
549
- - `forgeSQL.getDrizzleQueryBuilder().selectAliasedDistinctCacheable()`
590
+
591
+ - `forgeSQL.selectCacheable()`
592
+ - `forgeSQL.selectDistinctCacheable()`
593
+ - `forgeSQL.getDrizzleQueryBuilder().selectAliasedCacheable()`
594
+ - `forgeSQL.getDrizzleQueryBuilder().selectAliasedDistinctCacheable()`
550
595
 
551
596
  **Cache context example:**
597
+
552
598
  ```typescript
553
599
  await forgeSQL.executeWithCacheContext(async () => {
554
600
  // These methods participate in batch cache clearing
@@ -559,7 +605,6 @@ await forgeSQL.executeWithCacheContext(async () => {
559
605
  });
560
606
  ```
561
607
 
562
-
563
608
  The diagram below shows the lifecycle of a cacheable query in Forge-SQL-ORM:
564
609
 
565
610
  1. Resolver calls forge-sql-orm with a SQL query and parameters.
@@ -571,7 +616,6 @@ The diagram below shows the lifecycle of a cacheable query in Forge-SQL-ORM:
571
616
 
572
617
  ![img.png](img/umlCache1.png)
573
618
 
574
-
575
619
  The diagram below shows how Evict Cache works in Forge-SQL-ORM:
576
620
 
577
621
  1. **Data modification** is executed through `@forge/sql` (e.g., `UPDATE users ...`).
@@ -605,13 +649,13 @@ The diagram below shows how Cache Context works:
605
649
 
606
650
  ![img.png](img/umlCacheEvictCacheContext1.png)
607
651
 
608
-
609
652
  ### Important Considerations
610
653
 
611
654
  **@forge/kvs Limits:**
612
655
  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
656
 
614
657
  **Caching Guidelines:**
658
+
615
659
  - Don't cache everything - be selective about what to cache
616
660
  - Don't cache simple and fast queries - sometimes direct query is faster than cache
617
661
  - Consider data size and frequency of changes
@@ -619,7 +663,8 @@ Please review the [official @forge/kvs quotas and limits](https://developer.atla
619
663
  - Use appropriate TTL values
620
664
 
621
665
  **⚠️ 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.
666
+
667
+ - **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
668
 
624
669
  ### Step 1: Install Dependencies
625
670
 
@@ -657,18 +702,18 @@ modules:
657
702
  - key: clearCache
658
703
  handler: index.clearCache
659
704
  ```
705
+
660
706
  ```typescript
661
707
  // Example usage in your Forge app
662
708
  import { clearCacheSchedulerTrigger } from "forge-sql-orm";
663
709
 
664
710
  export const clearCache = () => {
665
- return clearCacheSchedulerTrigger({
711
+ return clearCacheSchedulerTrigger({
666
712
  cacheEntityName: "cache",
667
713
  });
668
714
  };
669
715
  ```
670
716
 
671
-
672
717
  ### Step 3: Configure ORM Options
673
718
 
674
719
  Set the cache entity name in your ForgeSQL configuration:
@@ -685,6 +730,7 @@ const forgeSQL = new ForgeSQL(options);
685
730
  ```
686
731
 
687
732
  **Important Notes:**
733
+
688
734
  - The `cacheEntityName` must exactly match the `name` in your manifest storage entities
689
735
  - The entity attributes (`sql`, `expiration`, `data`) are required for proper cache functionality
690
736
  - Indexes on `sql` and `expiration` improve cache lookup performance
@@ -696,11 +742,14 @@ const forgeSQL = new ForgeSQL(options);
696
742
  **Basic setup (without caching):**
697
743
 
698
744
  **package.json:**
745
+
699
746
  ```shell
700
747
  npm install forge-sql-orm @forge/sql drizzle-orm -S
748
+ # For UI-Kit projects, use: npm install forge-sql-orm @forge/sql drizzle-orm -S --legacy-peer-deps
701
749
  ```
702
750
 
703
751
  **manifest.yml:**
752
+
704
753
  ```yaml
705
754
  modules:
706
755
  sql:
@@ -709,6 +758,7 @@ modules:
709
758
  ```
710
759
 
711
760
  **index.ts:**
761
+
712
762
  ```typescript
713
763
  import ForgeSQL from "forge-sql-orm";
714
764
 
@@ -718,17 +768,18 @@ const forgeSQL = new ForgeSQL();
718
768
  await forgeSQL.insert(Users, [userData]);
719
769
  // Use versioned operations without caching
720
770
  await forgeSQL.modifyWithVersioning().insert(Users, [userData]);
721
- const users = await forgeSQL.select({id: Users.id});
722
-
723
-
771
+ const users = await forgeSQL.select({ id: Users.id });
724
772
  ```
725
773
 
726
774
  **With caching support:**
775
+
727
776
  ```shell
728
777
  npm install forge-sql-orm @forge/sql @forge/kvs drizzle-orm -S
778
+ # For UI-Kit projects, use: npm install forge-sql-orm @forge/sql @forge/kvs drizzle-orm -S --legacy-peer-deps
729
779
  ```
730
780
 
731
781
  **manifest.yml:**
782
+
732
783
  ```yaml
733
784
  modules:
734
785
  scheduledTrigger:
@@ -762,21 +813,23 @@ modules:
762
813
  import ForgeSQL from "forge-sql-orm";
763
814
 
764
815
  const forgeSQL = new ForgeSQL({
765
- cacheEntityName: "cache"
816
+ cacheEntityName: "cache",
766
817
  });
767
818
 
768
- import {clearCacheSchedulerTrigger} from "forge-sql-orm";
769
- import {getTableColumns} from "drizzle-orm";
819
+ import { clearCacheSchedulerTrigger } from "forge-sql-orm";
820
+ import { getTableColumns } from "drizzle-orm";
770
821
 
771
822
  export const clearCache = () => {
772
- return clearCacheSchedulerTrigger({
773
- cacheEntityName: "cache",
774
- });
823
+ return clearCacheSchedulerTrigger({
824
+ cacheEntityName: "cache",
825
+ });
775
826
  };
776
827
 
777
-
778
828
  // Now you can use caching features
779
- const usersData = await forgeSQL.selectCacheable(getTableColumns(users)).from(users).where(eq(users.active, true))
829
+ const usersData = await forgeSQL
830
+ .selectCacheable(getTableColumns(users))
831
+ .from(users)
832
+ .where(eq(users.active, true));
780
833
 
781
834
  // simple insert
782
835
  await forgeSQL.insertAndEvictCache(users, [userData]);
@@ -785,159 +838,194 @@ await forgeSQL.modifyWithVersioningAndEvictCache().insert(users, [userData]);
785
838
 
786
839
  // use Cache Context
787
840
  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
- })
841
+ // after insert mark users to evict
842
+ await forgeSQL.insert(users, [userData]);
843
+ // after insertAndEvictCache mark orders to evict
844
+ await forgeSQL.insertAndEvictCache(orders, [order1, order2]);
845
+ // execute query and put result to local cache
846
+ await forgeSQL
847
+ .selectCacheable({
848
+ userId: users.id,
849
+ userName: users.name,
850
+ orderId: orders.id,
851
+ orderName: orders.name,
852
+ })
853
+ .from(users)
854
+ .innerJoin(orders, eq(orders.userId, users.id))
855
+ .where(eq(users.active, true));
856
+ // use local cache without @forge/kvs and @forge/sql
857
+ return await forgeSQL
858
+ .selectCacheable({
859
+ userId: users.id,
860
+ userName: users.name,
861
+ orderId: orders.id,
862
+ orderName: orders.name,
863
+ })
864
+ .from(users)
865
+ .innerJoin(orders, eq(orders.userId, users.id))
866
+ .where(eq(users.active, true));
867
+ });
801
868
  // 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))
869
+ await forgeSQL
870
+ .selectCacheable({
871
+ userId: users.id,
872
+ userName: users.name,
873
+ orderId: orders.id,
874
+ orderName: orders.name,
875
+ })
876
+ .from(users)
877
+ .innerJoin(orders, eq(orders.userId, users.id))
878
+ .where(eq(users.active, true));
805
879
 
806
880
  // 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))
881
+ await forgeSQL
882
+ .selectCacheable({
883
+ userId: users.id,
884
+ userName: users.name,
885
+ orderId: orders.id,
886
+ orderName: orders.name,
887
+ })
888
+ .from(users)
889
+ .innerJoin(orders, eq(orders.userId, users.id))
890
+ .where(eq(users.active, true));
810
891
 
811
892
  // use Local Cache for performance optimization
812
893
  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
- });
894
+ // First query - hits database and caches result
895
+ const users = await forgeSQL
896
+ .select({ id: users.id, name: users.name })
897
+ .from(users)
898
+ .where(eq(users.active, true));
844
899
 
900
+ // Second query - uses local cache (no database call)
901
+ const cachedUsers = await forgeSQL
902
+ .select({ id: users.id, name: users.name })
903
+ .from(users)
904
+ .where(eq(users.active, true));
905
+
906
+ // Using new methods for better performance
907
+ const usersFrom = await forgeSQL.selectFrom(users).where(eq(users.active, true));
908
+
909
+ // This will use local cache (no database call)
910
+ const cachedUsersFrom = await forgeSQL.selectFrom(users).where(eq(users.active, true));
911
+
912
+ // Raw SQL with local caching
913
+ const rawUsers = await forgeSQL.execute("SELECT id, name FROM users WHERE active = ?", [true]);
914
+
915
+ // Insert operation - evicts local cache
916
+ await forgeSQL.insert(users).values({ name: "New User", active: true });
917
+
918
+ // Third query - hits database again and caches new result
919
+ const updatedUsers = await forgeSQL
920
+ .select({ id: users.id, name: users.name })
921
+ .from(users)
922
+ .where(eq(users.active, true));
923
+
924
+ return { users, cachedUsers, updatedUsers, usersFrom, cachedUsersFrom, rawUsers };
925
+ });
845
926
  ```
846
927
 
847
928
  ## Choosing the Right Method - ForgeSQL ORM
848
929
 
849
930
  ### When to Use Each Approach
850
931
 
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
-
932
+ | Method | Use Case | Versioning | Cache Management |
933
+ | ------------------------------------- | ----------------------------------------------------------- | ---------- | -------------------- |
934
+ | `modifyWithVersioningAndEvictCache()` | High-concurrency scenarios with Cache support | ✅ Yes | ✅ Yes |
935
+ | `modifyWithVersioning()` | High-concurrency scenarios | ✅ Yes | Cache Context |
936
+ | `insertAndEvictCache()` | Simple inserts | ❌ No | ✅ Yes |
937
+ | `updateAndEvictCache()` | Simple updates | ❌ No | ✅ Yes |
938
+ | `deleteAndEvictCache()` | Simple deletes | ❌ No | ✅ Yes |
939
+ | `insert/update/delete` | Basic Drizzle operations | ❌ No | Cache Context |
940
+ | `selectFrom()` | All-column queries with field aliasing | ❌ No | Local Cache |
941
+ | `selectDistinctFrom()` | Distinct all-column queries with field aliasing | ❌ No | Local Cache |
942
+ | `selectCacheableFrom()` | All-column queries with field aliasing and caching | ❌ No | Local + Global Cache |
943
+ | `selectDistinctCacheableFrom()` | Distinct all-column queries with field aliasing and caching | ❌ No | Local + Global Cache |
944
+ | `execute()` | Raw SQL queries with local caching | ❌ No | Local Cache |
945
+ | `executeCacheable()` | Raw SQL queries with local and global caching | ❌ No | Local + Global Cache |
946
+ | `executeDDL()` | DDL operations (CREATE, ALTER, DROP, etc.) | ❌ No | No Caching |
947
+ | `executeDDLActions()` | Execute regular SQL queries in DDL operation context | ❌ No | No Caching |
948
+ | `with()` | Common Table Expressions (CTEs) | ❌ No | Local Cache |
869
949
 
870
950
  ## Choosing the Right Method - Direct Drizzle
871
951
 
872
952
  ### When to Use Each Approach
873
953
 
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.
954
+ | Method | Use Case | Versioning | Cache Management |
955
+ | ---------------------------------------------------------------------- | ----------------------------------------------------------- | ---------- | -------------------- |
956
+ | `insertWithCacheContext/insertWithCacheContext/updateWithCacheContext` | Basic Drizzle operations | ❌ No | Cache Context |
957
+ | `insertAndEvictCache()` | Simple inserts without conflicts | ❌ No | ✅ Yes |
958
+ | `updateAndEvictCache()` | Simple updates without conflicts | ❌ No | ✅ Yes |
959
+ | `deleteAndEvictCache()` | Simple deletes without conflicts | ❌ No | ✅ Yes |
960
+ | `insert/update/delete` | Basic Drizzle operations | ❌ No | ❌ No |
961
+ | `selectFrom()` | All-column queries with field aliasing | ❌ No | Local Cache |
962
+ | `selectDistinctFrom()` | Distinct all-column queries with field aliasing | ❌ No | Local Cache |
963
+ | `selectCacheableFrom()` | All-column queries with field aliasing and caching | ❌ No | Local + Global Cache |
964
+ | `selectDistinctCacheableFrom()` | Distinct all-column queries with field aliasing and caching | ❌ No | Local + Global Cache |
965
+ | `execute()` | Raw SQL queries with local caching | ❌ No | Local Cache |
966
+ | `executeCacheable()` | Raw SQL queries with local and global caching | ❌ No | Local + Global Cache |
967
+ | `executeWithMetadata()` | Raw SQL queries with execution metrics capture | ❌ No | Local Cache |
968
+ | `executeDDL()` | DDL operations (CREATE, ALTER, DROP, etc.) | ❌ No | No Caching |
969
+ | `executeDDLActions()` | Execute regular SQL queries in DDL operation context | ❌ No | No Caching |
970
+ | `with()` | Common Table Expressions (CTEs) | ❌ No | Local Cache |
892
971
 
972
+ where Cache context - allows you to batch cache invalidation events and bypass cache reads for affected tables.
893
973
 
894
974
  ## Step-by-Step Migration Workflow
895
975
 
896
- 1. **Generate initial schema from an existing database**
976
+ 1. **Install CLI and setup scripts**
897
977
 
898
- ```sh
899
- npx forge-sql-orm-cli generate:model --dbName testDb --output ./database/schema
978
+ ```bash
979
+ npm install forge-sql-orm-cli -D
980
+ npm pkg set scripts.models:create="forge-sql-orm-cli generate:model --output src/entities --saveEnv"
981
+ npm pkg set scripts.migration:create="forge-sql-orm-cli migrations:create --force --output src/migration --entitiesPath src/entities"
982
+ npm pkg set scripts.migration:update="forge-sql-orm-cli migrations:update --entitiesPath src/entities --output src/migration"
900
983
  ```
901
984
 
902
985
  _(This is done only once when setting up the project)_
903
986
 
904
- 2. **Create the first migration**
987
+ 2. **Generate initial schema from an existing database**
905
988
 
906
989
  ```sh
907
- npx forge-sql-orm-cli migrations:create --dbName testDb --entitiesPath ./database/schema --output ./database/migration
990
+ npm run models:create
908
991
  ```
909
992
 
910
- _(This initializes the database migration structure, also done once)_
993
+ _(This will prompt for database credentials on first run and save them to `.env` file)_
994
+
995
+ 3. **Create the first migration**
996
+
997
+ ```sh
998
+ npm run migration:create
999
+ ```
911
1000
 
912
- 3. **Deploy to Forge and verify that migrations work**
1001
+ _(This initializes the database migration structure, also done once)_
913
1002
 
1003
+ 4. **Deploy to Forge and verify that migrations work**
914
1004
  - Deploy your **Forge app** with migrations.
915
1005
  - Run migrations using a **Forge web trigger** or **Forge scheduler**.
916
1006
 
917
- 4. **Modify the database (e.g., add a new column, index, etc.)**
918
-
1007
+ 5. **Modify the database (e.g., add a new column, index, etc.)**
919
1008
  - Use **DbSchema** or manually alter the database schema.
920
1009
 
921
- 5. **Update the migration**
1010
+ 6. **Update the migration**
922
1011
 
923
1012
  ```sh
924
- npx forge-sql-orm-cli migrations:update --dbName testDb --entitiesPath ./database/schema --output ./database/migration
1013
+ npm run migration:update
925
1014
  ```
926
1015
 
927
1016
  - ⚠️ **Do NOT update schema before this step!**
928
1017
  - If schema is updated first, the migration will be empty!
929
1018
 
930
- 6. **Deploy to Forge and verify that the migration runs without issues**
931
-
1019
+ 7. **Deploy to Forge and verify that the migration runs without issues**
932
1020
  - Run the updated migration on Forge.
933
1021
 
934
- 7. **Update the schema**
1022
+ 8. **Update the schema**
935
1023
 
936
1024
  ```sh
937
- npx forge-sql-orm-cli generate:model --dbName testDb --output ./database/schema
1025
+ npm run models:create
938
1026
  ```
939
1027
 
940
- 8. **Repeat steps 4-7 as needed**
1028
+ 9. **Repeat steps 5-8 as needed**
941
1029
 
942
1030
  **⚠️ WARNING:**
943
1031
 
@@ -947,6 +1035,7 @@ where Cache context - allows you to batch cache invalidation events and bypass c
947
1035
  ## Drop Migrations
948
1036
 
949
1037
  The Drop Migrations feature allows you to completely reset your database schema in Atlassian Forge SQL. This is useful when you need to:
1038
+
950
1039
  - Start fresh with a new schema
951
1040
  - Reset all tables and their data
952
1041
  - Clear migration history
@@ -955,6 +1044,7 @@ The Drop Migrations feature allows you to completely reset your database schema
955
1044
  ### Important Requirements
956
1045
 
957
1046
  Before using Drop Migrations, ensure that:
1047
+
958
1048
  1. Your local schema exactly matches the current database schema deployed in Atlassian Forge SQL
959
1049
  2. You have a backup of your data if needed
960
1050
  3. You understand that this operation will delete all tables and data
@@ -962,16 +1052,21 @@ Before using Drop Migrations, ensure that:
962
1052
  ### Usage
963
1053
 
964
1054
  1. First, ensure your local schema matches the deployed database:
1055
+
965
1056
  ```bash
966
- npx forge-sql-orm-cli generate:model --output ./database/schema
1057
+ npm run models:create
967
1058
  ```
968
1059
 
969
1060
  2. Generate the drop migration:
1061
+
970
1062
  ```bash
971
- npx forge-sql-orm-cli migrations:drop --entitiesPath ./database/schema --output ./database/migration
1063
+ npm run migration:drop
972
1064
  ```
973
1065
 
1066
+ _(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"`)_
1067
+
974
1068
  3. Deploy and run the migration in your Forge app:
1069
+
975
1070
  ```js
976
1071
  import migrationRunner from "./database/migration";
977
1072
  import { MigrationRunner } from "@forge/sql/out/migration";
@@ -983,13 +1078,14 @@ Before using Drop Migrations, ensure that:
983
1078
 
984
1079
  4. After dropping all tables, you can create a new migration to recreate the schema:
985
1080
  ```bash
986
- npx forge-sql-orm-cli migrations:create --entitiesPath ./database/schema --output ./database/migration --force
1081
+ npm run migration:create
987
1082
  ```
988
- The `--force` parameter is required here because we're creating a new migration after dropping all tables.
1083
+ The `--force` parameter is already included in the script to allow creating migrations after dropping all tables.
989
1084
 
990
1085
  ### Example Migration Output
991
1086
 
992
1087
  The generated drop migration will look like this:
1088
+
993
1089
  ```js
994
1090
  import { MigrationRunner } from "@forge/sql/out/migration";
995
1091
 
@@ -1017,31 +1113,36 @@ export default (migrationRunner: MigrationRunner): MigrationRunner => {
1017
1113
 
1018
1114
  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
1115
 
1020
- | Date type | Required Format | Example |
1021
- |-----------|----------------|---------|
1022
- | DATE | YYYY-MM-DD | 2024-09-19 |
1023
- | TIME | HH:MM:SS[.fraction] | 06:40:34 |
1116
+ | Date type | Required Format | Example |
1117
+ | --------- | ------------------------------ | -------------------------- |
1118
+ | DATE | YYYY-MM-DD | 2024-09-19 |
1119
+ | TIME | HH:MM:SS[.fraction] | 06:40:34 |
1024
1120
  | TIMESTAMP | YYYY-MM-DD HH:MM:SS[.fraction] | 2024-09-19 06:40:34.999999 |
1025
1121
 
1026
1122
  ```typescript
1027
1123
  // ❌ 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(),
1124
+ export const testEntityTimeStampVersion = mysqlTable("test_entity", {
1125
+ id: int("id").primaryKey().autoincrement(),
1126
+ time_stamp: timestamp("times_tamp").notNull(),
1127
+ date_time: datetime("date_time").notNull(),
1128
+ time: time("time").notNull(),
1129
+ date: date("date").notNull(),
1034
1130
  });
1035
1131
 
1036
1132
  // ✅ 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(),
1133
+ import {
1134
+ forgeDateTimeString,
1135
+ forgeDateString,
1136
+ forgeTimestampString,
1137
+ forgeTimeString,
1138
+ } from "forge-sql-orm";
1139
+
1140
+ export const testEntityTimeStampVersion = mysqlTable("test_entity", {
1141
+ id: int("id").primaryKey().autoincrement(),
1142
+ time_stamp: forgeTimestampString("times_tamp").notNull(),
1143
+ date_time: forgeDateTimeString("date_time").notNull(),
1144
+ time: forgeTimeString("time").notNull(),
1145
+ date: forgeDateString("date").notNull(),
1045
1146
  });
1046
1147
  ```
1047
1148
 
@@ -1057,6 +1158,7 @@ const timestamp = moment().format("YYYY-MM-DDTHH:mm:ss.SSS");
1057
1158
  ```
1058
1159
 
1059
1160
  Our custom types provide:
1161
+
1060
1162
  - Automatic conversion between JavaScript Date objects and Forge SQL's required string formats
1061
1163
  - Consistent date/time handling across your application
1062
1164
  - Type safety for date/time fields
@@ -1072,9 +1174,6 @@ Our custom types provide:
1072
1174
 
1073
1175
  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
1176
 
1075
-
1076
-
1077
-
1078
1177
  # Connection to ORM
1079
1178
 
1080
1179
  ```js
@@ -1082,7 +1181,8 @@ import ForgeSQL from "forge-sql-orm";
1082
1181
 
1083
1182
  const forgeSQL = new ForgeSQL();
1084
1183
  ```
1085
- or
1184
+
1185
+ or
1086
1186
 
1087
1187
  ```typescript
1088
1188
  import { drizzle } from "drizzle-orm/mysql-proxy";
@@ -1101,71 +1201,47 @@ const users = await db.select().from(users);
1101
1201
 
1102
1202
  ```js
1103
1203
  // Using forgeSQL.select()
1104
- const user = await forgeSQL
1105
- .select({user: users})
1106
- .from(users);
1204
+ const user = await forgeSQL.select({ user: users }).from(users);
1107
1205
 
1108
1206
  // Using forgeSQL.selectDistinct()
1109
- const user = await forgeSQL
1110
- .selectDistinct({user: users})
1111
- .from(users);
1207
+ const user = await forgeSQL.selectDistinct({ user: users }).from(users);
1112
1208
 
1113
1209
  // Using forgeSQL.selectCacheable()
1114
- const user = await forgeSQL
1115
- .selectCacheable({user: users})
1116
- .from(users);
1210
+ const user = await forgeSQL.selectCacheable({ user: users }).from(users);
1117
1211
 
1118
1212
  // Using forgeSQL.selectFrom() - Select all columns with field aliasing
1119
- const user = await forgeSQL
1120
- .selectFrom(users)
1121
- .where(eq(users.id, 1));
1213
+ const user = await forgeSQL.selectFrom(users).where(eq(users.id, 1));
1122
1214
 
1123
1215
  // Using forgeSQL.selectDistinctFrom() - Select distinct all columns with field aliasing
1124
- const user = await forgeSQL
1125
- .selectDistinctFrom(users)
1126
- .where(eq(users.id, 1));
1216
+ const user = await forgeSQL.selectDistinctFrom(users).where(eq(users.id, 1));
1127
1217
 
1128
1218
  // 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));
1219
+ const user = await forgeSQL.selectCacheableFrom(users).where(eq(users.id, 1));
1132
1220
 
1133
1221
  // 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));
1222
+ const user = await forgeSQL.selectDistinctCacheableFrom(users).where(eq(users.id, 1));
1137
1223
 
1138
1224
  // Using forgeSQL.execute() - Execute raw SQL with local caching
1139
- const user = await forgeSQL
1140
- .execute("SELECT * FROM users WHERE id = ?", [1]);
1225
+ const user = await forgeSQL.execute("SELECT * FROM users WHERE id = ?", [1]);
1141
1226
 
1142
1227
  // Using forgeSQL.executeCacheable() - Execute raw SQL with local and global caching
1143
1228
  // ⚠️ IMPORTANT: When using executeCacheable(), all table names in SQL queries must be wrapped with backticks (`)
1144
1229
  // 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);
1230
+ const user = await forgeSQL.executeCacheable("SELECT * FROM `users` WHERE id = ?", [1], 300);
1147
1231
 
1148
1232
  // Using forgeSQL.getDrizzleQueryBuilder()
1149
- const user = await forgeSQL
1150
- .getDrizzleQueryBuilder()
1151
- .select().from(Users)
1152
- .where(eq(Users.id, 1));
1233
+ const user = await forgeSQL.getDrizzleQueryBuilder().select().from(Users).where(eq(Users.id, 1));
1153
1234
 
1154
1235
  // OR using direct drizzle with custom driver
1155
1236
  const db = drizzle(forgeDriver);
1156
- const user = await db
1157
- .select().from(Users)
1158
- .where(eq(Users.id, 1));
1237
+ const user = await db.select().from(Users).where(eq(Users.id, 1));
1159
1238
  // Returns: { id: 1, name: "John Doe" }
1160
1239
 
1161
1240
  // Using executeQueryOnlyOne for single result with error handling
1162
1241
  const user = await forgeSQL
1163
1242
  .fetch()
1164
1243
  .executeQueryOnlyOne(
1165
- forgeSQL
1166
- .getDrizzleQueryBuilder()
1167
- .select().from(Users)
1168
- .where(eq(Users.id, 1))
1244
+ forgeSQL.getDrizzleQueryBuilder().select().from(Users).where(eq(Users.id, 1)),
1169
1245
  );
1170
1246
  // Returns: { id: 1, name: "John Doe" }
1171
1247
  // Throws error if multiple records found
@@ -1177,26 +1253,29 @@ const usersAlias = alias(Users, "u");
1177
1253
  const result = await forgeSQL
1178
1254
  .getDrizzleQueryBuilder()
1179
1255
  .select({
1180
- userId: sql<string>`${usersAlias.id} as \`userId\``,
1181
- userName: sql<string>`${usersAlias.name} as \`userName\``
1182
- }).from(usersAlias);
1256
+ userId: sql < string > `${usersAlias.id} as \`userId\``,
1257
+ userName: sql < string > `${usersAlias.name} as \`userName\``,
1258
+ })
1259
+ .from(usersAlias);
1183
1260
 
1184
1261
  // OR with direct drizzle
1185
1262
  const db = drizzle(forgeDriver);
1186
1263
  const result = await db
1187
1264
  .select({
1188
- userId: sql<string>`${usersAlias.id} as \`userId\``,
1189
- userName: sql<string>`${usersAlias.name} as \`userName\``
1190
- }).from(usersAlias);
1265
+ userId: sql < string > `${usersAlias.id} as \`userId\``,
1266
+ userName: sql < string > `${usersAlias.name} as \`userName\``,
1267
+ })
1268
+ .from(usersAlias);
1191
1269
  // Returns: { userId: 1, userName: "John Doe" }
1192
1270
  ```
1193
1271
 
1194
1272
  ### Complex Queries
1273
+
1195
1274
  ```js
1196
1275
  // Using joins with automatic field name collision prevention
1197
1276
  // With forgeSQL
1198
1277
  const orderWithUser = await forgeSQL
1199
- .select({user: users, order: orders})
1278
+ .select({ user: users, order: orders })
1200
1279
  .from(orders)
1201
1280
  .innerJoin(users, eq(orders.userId, users.id));
1202
1281
 
@@ -1215,12 +1294,12 @@ const orderWithUser = await forgeSQL
1215
1294
  // Using with() for Common Table Expressions (CTEs)
1216
1295
  const userStats = await forgeSQL
1217
1296
  .with(
1218
- forgeSQL.selectFrom(users).where(eq(users.active, true)).as('activeUsers'),
1219
- forgeSQL.selectFrom(orders).where(eq(orders.status, 'completed')).as('completedOrders')
1297
+ forgeSQL.selectFrom(users).where(eq(users.active, true)).as("activeUsers"),
1298
+ forgeSQL.selectFrom(orders).where(eq(orders.status, "completed")).as("completedOrders"),
1220
1299
  )
1221
1300
  .select({
1222
1301
  totalActiveUsers: sql`COUNT(au.id)`,
1223
- totalCompletedOrders: sql`COUNT(co.id)`
1302
+ totalCompletedOrders: sql`COUNT(co.id)`,
1224
1303
  })
1225
1304
  .from(sql`activeUsers au`)
1226
1305
  .leftJoin(sql`completedOrders co`, eq(sql`au.id`, sql`co.userId`));
@@ -1228,33 +1307,30 @@ const userStats = await forgeSQL
1228
1307
  // OR with direct drizzle
1229
1308
  const db = patchDbWithSelectAliased(drizzle(forgeDriver));
1230
1309
  const orderWithUser = await db
1231
- .selectAliased({user: users, order: orders})
1310
+ .selectAliased({ user: users, order: orders })
1232
1311
  .from(orders)
1233
1312
  .innerJoin(users, eq(orders.userId, users.id));
1234
- // Returns: {
1235
- // user_id: 1,
1313
+ // Returns: {
1314
+ // user_id: 1,
1236
1315
  // user_name: "John Doe",
1237
1316
  // order_id: 1,
1238
1317
  // order_product: "Product 1"
1239
1318
  // }
1240
1319
 
1241
1320
  // Using distinct with aliases
1242
- const uniqueUsers = await db
1243
- .selectAliasedDistinct({user: users})
1244
- .from(users);
1321
+ const uniqueUsers = await db.selectAliasedDistinct({ user: users }).from(users);
1245
1322
  // Returns unique users with aliased fields
1246
1323
 
1247
1324
  // 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
- );
1325
+ const userStats = await forgeSQL.fetch().executeQueryOnlyOne(
1326
+ forgeSQL
1327
+ .getDrizzleQueryBuilder()
1328
+ .select({
1329
+ totalUsers: sql`COUNT(*) as \`totalUsers\``,
1330
+ uniqueNames: sql`COUNT(DISTINCT name) as \`uniqueNames\``,
1331
+ })
1332
+ .from(Users),
1333
+ );
1258
1334
  // Returns: { totalUsers: 100, uniqueNames: 80 }
1259
1335
  // Throws error if multiple records found
1260
1336
  ```
@@ -1286,14 +1362,14 @@ const usersWithMetadata = await forgeSQL.executeWithMetadata(
1286
1362
  },
1287
1363
  (totalDbExecutionTime, totalResponseSize, printQueriesWithPlan) => {
1288
1364
  const threshold = 500; // ms baseline for this resolver
1289
-
1365
+
1290
1366
  if (totalDbExecutionTime > threshold * 1.5) {
1291
1367
  console.warn(`[Performance Warning] Resolver exceeded DB time: ${totalDbExecutionTime} ms`);
1292
1368
  await printQueriesWithPlan(); // Analyze and print query execution plans
1293
1369
  } else if (totalDbExecutionTime > threshold) {
1294
1370
  console.debug(`[Performance Debug] High DB time: ${totalDbExecutionTime} ms`);
1295
1371
  }
1296
-
1372
+
1297
1373
  console.log(`DB response size: ${totalResponseSize} bytes`);
1298
1374
  }
1299
1375
  );
@@ -1308,7 +1384,7 @@ await forgeSQL.executeDDL(`
1308
1384
  `);
1309
1385
 
1310
1386
  await forgeSQL.executeDDL(sql`
1311
- ALTER TABLE users
1387
+ ALTER TABLE users
1312
1388
  ADD COLUMN created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
1313
1389
  `);
1314
1390
 
@@ -1319,23 +1395,23 @@ await forgeSQL.executeDDL("DROP TABLE IF EXISTS old_users");
1319
1395
  await forgeSQL.executeDDLActions(async () => {
1320
1396
  // Execute regular SQL queries in DDL context for performance monitoring
1321
1397
  const slowQueries = await forgeSQL.execute(`
1322
- SELECT * FROM INFORMATION_SCHEMA.STATEMENTS_SUMMARY
1398
+ SELECT * FROM INFORMATION_SCHEMA.STATEMENTS_SUMMARY
1323
1399
  WHERE AVG_LATENCY > 1000000
1324
1400
  `);
1325
-
1401
+
1326
1402
  // Execute complex analysis queries in DDL context
1327
1403
  const performanceData = await forgeSQL.execute(`
1328
1404
  SELECT * FROM INFORMATION_SCHEMA.CLUSTER_STATEMENTS_SUMMARY_HISTORY
1329
1405
  WHERE SUMMARY_END_TIME > DATE_SUB(NOW(), INTERVAL 1 HOUR)
1330
1406
  `);
1331
-
1407
+
1332
1408
  return { slowQueries, performanceData };
1333
1409
  });
1334
1410
 
1335
1411
  // Using execute() with complex queries
1336
1412
  const userStats = await forgeSQL
1337
1413
  .execute(`
1338
- SELECT
1414
+ SELECT
1339
1415
  u.id,
1340
1416
  u.name,
1341
1417
  COUNT(o.id) as order_count,
@@ -1360,13 +1436,10 @@ These operations work like standard Drizzle methods but participate in cache con
1360
1436
  await forgeSQL.insert(Users).values({ id: 1, name: "Smith" });
1361
1437
 
1362
1438
  // 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));
1439
+ await forgeSQL.update(Users).set({ name: "Smith Updated" }).where(eq(Users.id, 1));
1366
1440
 
1367
1441
  // Basic delete (participates in cache context when used within executeWithCacheContext)
1368
- await forgeSQL.delete(Users)
1369
- .where(eq(Users.id, 1));
1442
+ await forgeSQL.delete(Users).where(eq(Users.id, 1));
1370
1443
  ```
1371
1444
 
1372
1445
  ### 2. Non-Versioned Operations with Cache Management
@@ -1378,13 +1451,10 @@ These operations don't use optimistic locking but provide cache invalidation:
1378
1451
  await forgeSQL.insertAndEvictCache(Users).values({ id: 1, name: "Smith" });
1379
1452
 
1380
1453
  // Update without versioning but with cache invalidation
1381
- await forgeSQL.updateAndEvictCache(Users)
1382
- .set({ name: "Smith Updated" })
1383
- .where(eq(Users.id, 1));
1454
+ await forgeSQL.updateAndEvictCache(Users).set({ name: "Smith Updated" }).where(eq(Users.id, 1));
1384
1455
 
1385
1456
  // Delete without versioning but with cache invalidation
1386
- await forgeSQL.deleteAndEvictCache(Users)
1387
- .where(eq(Users.id, 1));
1457
+ await forgeSQL.deleteAndEvictCache(Users).where(eq(Users.id, 1));
1388
1458
  ```
1389
1459
 
1390
1460
  ### 3. Versioned Operations with Cache Management (Recommended)
@@ -1393,16 +1463,20 @@ These operations use optimistic locking and automatic cache invalidation:
1393
1463
 
1394
1464
  ```js
1395
1465
  // Insert with versioning and cache management
1396
- const userId = await forgeSQL.modifyWithVersioningAndEvictCache().insert(Users, [{ id: 1, name: "Smith" }]);
1466
+ const userId = await forgeSQL
1467
+ .modifyWithVersioningAndEvictCache()
1468
+ .insert(Users, [{ id: 1, name: "Smith" }]);
1397
1469
 
1398
1470
  // Bulk insert with versioning
1399
1471
  await forgeSQL.modifyWithVersioningAndEvictCache().insert(Users, [
1400
- { id: 2, name: "Smith" },
1401
- { id: 3, name: "Vasyl" },
1402
- ]);
1472
+ { id: 2, name: "Smith" },
1473
+ { id: 3, name: "Vasyl" },
1474
+ ]);
1403
1475
 
1404
1476
  // Update by ID with optimistic locking and cache invalidation
1405
- await forgeSQL.modifyWithVersioningAndEvictCache().updateById({ id: 1, name: "Smith Updated" }, Users);
1477
+ await forgeSQL
1478
+ .modifyWithVersioningAndEvictCache()
1479
+ .updateById({ id: 1, name: "Smith Updated" }, Users);
1406
1480
 
1407
1481
  // Delete by ID with versioning and cache invalidation
1408
1482
  await forgeSQL.modifyWithVersioningAndEvictCache().deleteById(1, Users);
@@ -1446,18 +1520,16 @@ await forgeSQL.modifyWithVersioning().deleteById(1, Users);
1446
1520
  import { nextVal } from "forge-sql-orm";
1447
1521
 
1448
1522
  const user = {
1449
- id: nextVal('user_id_seq'),
1523
+ id: nextVal("user_id_seq"),
1450
1524
  name: "user test",
1451
- organization_id: 1
1525
+ organization_id: 1,
1452
1526
  };
1453
1527
  const id = await forgeSQL.modifyWithVersioning().insert(appUser, [user]);
1454
1528
 
1455
1529
  // 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
- );
1530
+ await forgeSQL
1531
+ .modifyWithVersioning()
1532
+ .updateFields({ name: "New Name", age: 35 }, Users, eq(Users.email, "smith@example.com"));
1461
1533
 
1462
1534
  // Insert with duplicate handling
1463
1535
  await forgeSQL.modifyWithVersioning().insert(
@@ -1466,7 +1538,7 @@ await forgeSQL.modifyWithVersioning().insert(
1466
1538
  { id: 4, name: "Smith" },
1467
1539
  { id: 4, name: "Vasyl" },
1468
1540
  ],
1469
- true
1541
+ true,
1470
1542
  );
1471
1543
  ```
1472
1544
 
@@ -1488,19 +1560,21 @@ const result = await forgeSQL
1488
1560
  .offset(formatLimitOffset(350000));
1489
1561
 
1490
1562
  // The generated SQL will be:
1491
- // SELECT * FROM order_item
1492
- // ORDER BY created_at ASC
1493
- // LIMIT 10
1563
+ // SELECT * FROM order_item
1564
+ // ORDER BY created_at ASC
1565
+ // LIMIT 10
1494
1566
  // OFFSET 350000
1495
1567
  ```
1496
1568
 
1497
1569
  **Important Notes:**
1570
+
1498
1571
  - The function performs type checking to prevent SQL injection
1499
1572
  - It throws an error if the input is not a valid number
1500
1573
  - Use this function instead of direct parameter binding for LIMIT and OFFSET clauses
1501
1574
  - The function is specifically designed to work with Atlassian Forge SQL's limitations
1502
1575
 
1503
1576
  **Security Considerations:**
1577
+
1504
1578
  - The function includes validation to ensure the input is a valid number
1505
1579
  - This prevents SQL injection by ensuring only numeric values are inserted
1506
1580
  - Always use this function instead of string concatenation for LIMIT and OFFSET values
@@ -1534,9 +1608,9 @@ const options = {
1534
1608
  tableName: "users",
1535
1609
  versionField: {
1536
1610
  fieldName: "updatedAt",
1537
- }
1538
- }
1539
- }
1611
+ },
1612
+ },
1613
+ },
1540
1614
  };
1541
1615
 
1542
1616
  const forgeSQL = new ForgeSQL(options);
@@ -1559,7 +1633,6 @@ The caching system leverages Forge's Custom entity store to provide:
1559
1633
  // Value: { data: [...], expiration: 1234567890, sql: "select * from 1" }
1560
1634
  ```
1561
1635
 
1562
-
1563
1636
  ### Cache Context Operations
1564
1637
 
1565
1638
  The cache context allows you to batch cache invalidation events and bypass cache reads for affected tables:
@@ -1620,72 +1693,78 @@ Local cache is an in-memory caching layer that operates within a single resolver
1620
1693
  // Execute operations within a local cache context
1621
1694
  await forgeSQL.executeWithLocalContext(async () => {
1622
1695
  // 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
-
1696
+ const users = await forgeSQL
1697
+ .select({ id: users.id, name: users.name })
1698
+ .from(users)
1699
+ .where(eq(users.active, true));
1700
+
1626
1701
  // 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)
1702
+ const cachedUsers = await forgeSQL
1703
+ .select({ id: users.id, name: users.name })
1704
+ .from(users)
1632
1705
  .where(eq(users.active, true));
1633
-
1706
+
1707
+ // Using new selectFrom methods with local caching
1708
+ const usersFrom = await forgeSQL.selectFrom(users).where(eq(users.active, true));
1709
+
1634
1710
  // This will use local cache (no database call)
1635
- const cachedUsersFrom = await forgeSQL.selectFrom(users)
1636
- .where(eq(users.active, true));
1637
-
1711
+ const cachedUsersFrom = await forgeSQL.selectFrom(users).where(eq(users.active, true));
1712
+
1638
1713
  // Using execute() with local caching
1639
- const rawUsers = await forgeSQL.execute(
1640
- "SELECT id, name FROM users WHERE active = ?",
1641
- [true]
1642
- );
1643
-
1714
+ const rawUsers = await forgeSQL.execute("SELECT id, name FROM users WHERE active = ?", [true]);
1715
+
1644
1716
  // 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
-
1717
+ const cachedRawUsers = await forgeSQL.execute("SELECT id, name FROM users WHERE active = ?", [
1718
+ true,
1719
+ ]);
1720
+
1650
1721
  // Raw SQL with execution metadata and performance monitoring
1651
1722
  const usersWithMetadata = await forgeSQL.executeWithMetadata(
1652
1723
  async () => {
1653
1724
  const users = await forgeSQL.selectFrom(usersTable);
1654
- const orders = await forgeSQL.selectFrom(ordersTable).where(eq(ordersTable.userId, usersTable.id));
1725
+ const orders = await forgeSQL
1726
+ .selectFrom(ordersTable)
1727
+ .where(eq(ordersTable.userId, usersTable.id));
1655
1728
  return { users, orders };
1656
1729
  },
1657
1730
  (totalDbExecutionTime, totalResponseSize, printQueriesWithPlan) => {
1658
1731
  const threshold = 500; // ms baseline for this resolver
1659
-
1732
+
1660
1733
  if (totalDbExecutionTime > threshold * 1.5) {
1661
1734
  console.warn(`[Performance Warning] Resolver exceeded DB time: ${totalDbExecutionTime} ms`);
1662
1735
  await printQueriesWithPlan(); // Analyze and print query execution plans
1663
1736
  } else if (totalDbExecutionTime > threshold) {
1664
1737
  console.debug(`[Performance Debug] High DB time: ${totalDbExecutionTime} ms`);
1665
1738
  }
1666
-
1739
+
1667
1740
  console.log(`DB response size: ${totalResponseSize} bytes`);
1668
- }
1741
+ },
1669
1742
  );
1670
-
1743
+
1671
1744
  // Insert operation - evicts local cache for users table
1672
- await forgeSQL.insert(users).values({ name: 'New User', active: true });
1673
-
1745
+ await forgeSQL.insert(users).values({ name: "New User", active: true });
1746
+
1674
1747
  // 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));
1748
+ const updatedUsers = await forgeSQL
1749
+ .select({ id: users.id, name: users.name })
1750
+ .from(users)
1751
+ .where(eq(users.active, true));
1677
1752
  });
1678
1753
 
1679
1754
  // Execute with return value
1680
1755
  const result = await forgeSQL.executeWithLocalCacheContextAndReturnValue(async () => {
1681
1756
  // 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
-
1757
+ const users = await forgeSQL
1758
+ .select({ id: users.id, name: users.name })
1759
+ .from(users)
1760
+ .where(eq(users.active, true));
1761
+
1685
1762
  // 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
-
1763
+ const cachedUsers = await forgeSQL
1764
+ .select({ id: users.id, name: users.name })
1765
+ .from(users)
1766
+ .where(eq(users.active, true));
1767
+
1689
1768
  return { users, cachedUsers };
1690
1769
  });
1691
1770
  ```
@@ -1697,57 +1776,57 @@ const result = await forgeSQL.executeWithLocalCacheContextAndReturnValue(async (
1697
1776
  const userResolver = async (req) => {
1698
1777
  return await forgeSQL.executeWithLocalCacheContextAndReturnValue(async () => {
1699
1778
  // 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
-
1779
+ const user = await forgeSQL.selectFrom(users).where(eq(users.id, args.userId));
1780
+
1703
1781
  // Get user's orders using selectCacheableFrom (with caching)
1704
- const orders = await forgeSQL.selectCacheableFrom(orders)
1705
- .where(eq(orders.userId, args.userId));
1706
-
1782
+ const orders = await forgeSQL.selectCacheableFrom(orders).where(eq(orders.userId, args.userId));
1783
+
1707
1784
  // Get user's profile using raw SQL with execute()
1708
1785
  const profile = await forgeSQL.execute(
1709
- "SELECT id, bio, avatar FROM profiles WHERE user_id = ?",
1710
- [args.userId]
1786
+ "SELECT id, bio, avatar FROM profiles WHERE user_id = ?",
1787
+ [args.userId],
1711
1788
  );
1712
-
1789
+
1713
1790
  // Get user statistics using complex raw SQL
1714
- const stats = await forgeSQL.execute(`
1791
+ const stats = await forgeSQL.execute(
1792
+ `
1715
1793
  SELECT
1716
1794
  COUNT(o.id) as total_orders,
1717
1795
  SUM(o.amount) as total_spent,
1718
1796
  AVG(o.amount) as avg_order_value
1719
1797
  FROM orders o
1720
1798
  WHERE o.user_id = ? AND o.status = 'completed'
1721
- `, [args.userId]);
1722
-
1799
+ `,
1800
+ [args.userId],
1801
+ );
1802
+
1723
1803
  // If any of these queries are repeated within the same resolver,
1724
1804
  // they will use the local cache instead of hitting the database
1725
-
1805
+
1726
1806
  return {
1727
1807
  ...user[0],
1728
1808
  orders,
1729
1809
  profile: profile[0],
1730
- stats: stats[0]
1810
+ stats: stats[0],
1731
1811
  };
1732
1812
  });
1733
1813
  };
1734
1814
  ```
1735
1815
 
1736
-
1737
1816
  #### Local Cache (Level 1) vs Global Cache (Level 2)
1738
1817
 
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 |
1818
+ | Feature | Local Cache (Level 1) | Global Cache (Level 2) |
1819
+ | ------------------ | ------------------------------------- | ------------------------------------------- |
1820
+ | **Storage** | In-memory (Node.js process) | Persistent (KVS Custom Entities) |
1821
+ | **Scope** | Single forge invocation | Cross-invocation (between calls) |
1822
+ | **Persistence** | No (cleared on invocation end) | Yes (survives app redeploy) |
1823
+ | **Performance** | Very fast (memory access) | Fast (KVS optimized storage) |
1824
+ | **Memory Usage** | Low (invocation-scoped) | Higher (persistent storage) |
1825
+ | **Use Case** | Invocation optimization | Cross-invocation data sharing |
1826
+ | **Configuration** | None required | Requires KVS setup |
1827
+ | **TTL Support** | No (invocation-scoped) | Yes (automatic expiration) |
1828
+ | **Cache Eviction** | Automatic on DML operations | Manual or scheduled cleanup |
1829
+ | **Best For** | Repeated queries in single invocation | Frequently accessed data across invocations |
1751
1830
 
1752
1831
  #### Integration with Global Cache (Level 2)
1753
1832
 
@@ -1760,19 +1839,20 @@ await forgeSQL.executeWithLocalContext(async () => {
1760
1839
  // 1. Local cache (Level 1 - in-memory)
1761
1840
  // 2. Global cache (Level 2 - KVS)
1762
1841
  // 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)
1842
+ const users = await forgeSQL
1843
+ .selectCacheable({ id: users.id, name: users.name })
1844
+ .from(users)
1768
1845
  .where(eq(users.active, true));
1769
-
1846
+
1847
+ // Using new methods with multi-level caching
1848
+ const usersFrom = await forgeSQL.selectCacheableFrom(users).where(eq(users.active, true));
1849
+
1770
1850
  // Raw SQL with multi-level caching
1771
1851
  // ⚠️ IMPORTANT: When using executeCacheable(), all table names must be wrapped with backticks (`)
1772
1852
  const rawUsers = await forgeSQL.executeCacheable(
1773
- "SELECT id, name FROM `users` WHERE active = ?",
1774
- [true],
1775
- 300 // TTL in seconds
1853
+ "SELECT id, name FROM `users` WHERE active = ?",
1854
+ [true],
1855
+ 300, // TTL in seconds
1776
1856
  );
1777
1857
  });
1778
1858
  ```
@@ -1796,26 +1876,26 @@ The diagram below shows how local cache works in Forge-SQL-ORM:
1796
1876
  // Execute queries with caching
1797
1877
  const users = await forgeSQL.modifyWithVersioningAndEvictCache().executeQuery(
1798
1878
  forgeSQL.select().from(Users).where(eq(Users.active, true)),
1799
- 600 // Custom TTL in seconds
1879
+ 600, // Custom TTL in seconds
1800
1880
  );
1801
1881
 
1802
1882
  // Execute single result queries with caching
1803
- const user = await forgeSQL.modifyWithVersioningAndEvictCache().executeQueryOnlyOne(
1804
- forgeSQL.select().from(Users).where(eq(Users.id, 1))
1805
- );
1883
+ const user = await forgeSQL
1884
+ .modifyWithVersioningAndEvictCache()
1885
+ .executeQueryOnlyOne(forgeSQL.select().from(Users).where(eq(Users.id, 1)));
1806
1886
 
1807
1887
  // Execute raw SQL with caching
1808
1888
  const results = await forgeSQL.modifyWithVersioningAndEvictCache().executeRawSQL(
1809
1889
  "SELECT * FROM users WHERE active = ?",
1810
1890
  [true],
1811
- 300 // TTL in seconds
1891
+ 300, // TTL in seconds
1812
1892
  );
1813
1893
 
1814
1894
  // Using new methods for cache-aware operations
1815
- const usersFrom = await forgeSQL.selectCacheableFrom(Users)
1816
- .where(eq(Users.active, true));
1895
+ const usersFrom = await forgeSQL.selectCacheableFrom(Users).where(eq(Users.active, true));
1817
1896
 
1818
- const usersDistinct = await forgeSQL.selectDistinctCacheableFrom(Users)
1897
+ const usersDistinct = await forgeSQL
1898
+ .selectDistinctCacheableFrom(Users)
1819
1899
  .where(eq(Users.active, true));
1820
1900
 
1821
1901
  // Raw SQL with local and global caching
@@ -1823,18 +1903,18 @@ const usersDistinct = await forgeSQL.selectDistinctCacheableFrom(Users)
1823
1903
  const rawUsers = await forgeSQL.executeCacheable(
1824
1904
  "SELECT * FROM `users` WHERE active = ?",
1825
1905
  [true],
1826
- 300 // TTL in seconds
1906
+ 300, // TTL in seconds
1827
1907
  );
1828
1908
 
1829
1909
  // Using with() for Common Table Expressions with caching
1830
1910
  const userStats = await forgeSQL
1831
1911
  .with(
1832
- forgeSQL.selectFrom(users).where(eq(users.active, true)).as('activeUsers'),
1833
- forgeSQL.selectFrom(orders).where(eq(orders.status, 'completed')).as('completedOrders')
1912
+ forgeSQL.selectFrom(users).where(eq(users.active, true)).as("activeUsers"),
1913
+ forgeSQL.selectFrom(orders).where(eq(orders.status, "completed")).as("completedOrders"),
1834
1914
  )
1835
1915
  .select({
1836
1916
  totalActiveUsers: sql`COUNT(au.id)`,
1837
- totalCompletedOrders: sql`COUNT(co.id)`
1917
+ totalCompletedOrders: sql`COUNT(co.id)`,
1838
1918
  })
1839
1919
  .from(sql`activeUsers au`)
1840
1920
  .leftJoin(sql`completedOrders co`, eq(sql`au.id`, sql`co.userId`));
@@ -1843,21 +1923,23 @@ const userStats = await forgeSQL
1843
1923
  const usersWithMetadata = await forgeSQL.executeWithMetadata(
1844
1924
  async () => {
1845
1925
  const users = await forgeSQL.selectFrom(usersTable);
1846
- const orders = await forgeSQL.selectFrom(ordersTable).where(eq(ordersTable.userId, usersTable.id));
1926
+ const orders = await forgeSQL
1927
+ .selectFrom(ordersTable)
1928
+ .where(eq(ordersTable.userId, usersTable.id));
1847
1929
  return { users, orders };
1848
1930
  },
1849
1931
  (totalDbExecutionTime, totalResponseSize, printQueriesWithPlan) => {
1850
1932
  const threshold = 500; // ms baseline for this resolver
1851
-
1933
+
1852
1934
  if (totalDbExecutionTime > threshold * 1.5) {
1853
1935
  console.warn(`[Performance Warning] Resolver exceeded DB time: ${totalDbExecutionTime} ms`);
1854
1936
  await printQueriesWithPlan(); // Analyze and print query execution plans
1855
1937
  } else if (totalDbExecutionTime > threshold) {
1856
1938
  console.debug(`[Performance Debug] High DB time: ${totalDbExecutionTime} ms`);
1857
1939
  }
1858
-
1940
+
1859
1941
  console.log(`DB response size: ${totalResponseSize} bytes`);
1860
- }
1942
+ },
1861
1943
  );
1862
1944
  ```
1863
1945
 
@@ -1893,9 +1975,9 @@ const options = {
1893
1975
  tableName: "users",
1894
1976
  versionField: {
1895
1977
  fieldName: "updatedAt",
1896
- }
1897
- }
1898
- }
1978
+ },
1979
+ },
1980
+ },
1899
1981
  };
1900
1982
 
1901
1983
  const forgeSQL = new ForgeSQL(options);
@@ -1906,24 +1988,26 @@ const forgeSQL = new ForgeSQL(options);
1906
1988
  ```typescript
1907
1989
  // The version field will be automatically handled
1908
1990
  await forgeSQL.modifyWithVersioning().updateById(
1909
- {
1910
- id: 1,
1991
+ {
1992
+ id: 1,
1911
1993
  name: "Updated Name",
1912
- updatedAt: new Date() // Will be automatically set if not provided
1913
- },
1914
- Users
1994
+ updatedAt: new Date(), // Will be automatically set if not provided
1995
+ },
1996
+ Users,
1915
1997
  );
1916
1998
  ```
1999
+
1917
2000
  or with cache support
2001
+
1918
2002
  ```typescript
1919
2003
  // The version field will be automatically handled
1920
2004
  await forgeSQL.modifyWithVersioningAndEvictCache().updateById(
1921
- {
1922
- id: 1,
2005
+ {
2006
+ id: 1,
1923
2007
  name: "Updated Name",
1924
- updatedAt: new Date() // Will be automatically set if not provided
1925
- },
1926
- Users
2008
+ updatedAt: new Date(), // Will be automatically set if not provided
2009
+ },
2010
+ Users,
1927
2011
  );
1928
2012
  ```
1929
2013
 
@@ -1931,16 +2015,16 @@ await forgeSQL.modifyWithVersioningAndEvictCache().updateById(
1931
2015
 
1932
2016
  The `ForgeSqlOrmOptions` object allows customization of ORM behavior:
1933
2017
 
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`. |
2018
+ | Option | Type | Description |
2019
+ | -------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
2020
+ | `logRawSqlQuery` | `boolean` | Enables logging of raw SQL queries in the Atlassian Forge Developer Console. Useful for debugging and monitoring. Defaults to `false`. |
2021
+ | `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
2022
  | `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. |
2023
+ | `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. |
2024
+ | `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"`. |
2025
+ | `cacheTTL` | `number` | Default cache TTL in seconds. Defaults to `120` (2 minutes). |
2026
+ | `cacheWrapTable` | `boolean` | Whether to wrap table names with backticks in cache keys. Defaults to `true`. |
2027
+ | `hints` | `object` | SQL hints for query optimization. Optional configuration for advanced query tuning. |
1944
2028
 
1945
2029
  ## CLI Commands
1946
2030
 
@@ -1959,23 +2043,39 @@ The CLI tool provides the following main commands:
1959
2043
 
1960
2044
  ### Installation
1961
2045
 
2046
+ The CLI tool must be installed as a local dependency and used via npm scripts in your `package.json`:
2047
+
1962
2048
  ```bash
1963
- npm install -g forge-sql-orm-cli
2049
+ npm install forge-sql-orm-cli -D
2050
+ ```
2051
+
2052
+ ### Setup npm Scripts
2053
+
2054
+ Add the following scripts to your `package.json`:
2055
+
2056
+ ```bash
2057
+ npm pkg set scripts.models:create="forge-sql-orm-cli generate:model --output src/entities --saveEnv"
2058
+ npm pkg set scripts.migration:create="forge-sql-orm-cli migrations:create --force --output src/migration --entitiesPath src/entities"
2059
+ npm pkg set scripts.migration:update="forge-sql-orm-cli migrations:update --entitiesPath src/entities --output src/migration"
1964
2060
  ```
1965
2061
 
1966
2062
  ### Basic Usage
1967
2063
 
2064
+ After setting up the scripts, use them via npm:
2065
+
1968
2066
  ```bash
1969
2067
  # Generate models from database
1970
- forge-sql-orm-cli generate:model --dbName myapp --output ./database/entities
2068
+ npm run models:create
1971
2069
 
1972
2070
  # Create migration
1973
- forge-sql-orm-cli migrations:create --dbName myapp --entitiesPath ./database/entities
2071
+ npm run migration:create
1974
2072
 
1975
2073
  # Update migration
1976
- forge-sql-orm-cli migrations:update --dbName myapp --entitiesPath ./database/entities
2074
+ npm run migration:update
1977
2075
  ```
1978
2076
 
2077
+ **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.
2078
+
1979
2079
  For detailed information about all available options and advanced usage, see the [Full CLI Documentation](forge-sql-orm-cli/README.md).
1980
2080
 
1981
2081
  ## Web Triggers for Migrations
@@ -1985,7 +2085,8 @@ Forge-SQL-ORM provides web triggers for managing database migrations in Atlassia
1985
2085
  ### 1. Apply Migrations Trigger
1986
2086
 
1987
2087
  This trigger allows you to apply database migrations through a web endpoint. It's useful for:
1988
- - Manually triggering migrations
2088
+
2089
+ - Manually triggering migrations
1989
2090
  - Running migrations as part of your deployment process
1990
2091
  - Testing migrations in different environments
1991
2092
 
@@ -2000,22 +2101,23 @@ export const handlerMigration = async () => {
2000
2101
  ```
2001
2102
 
2002
2103
  Configure in `manifest.yml`:
2104
+
2003
2105
  ```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
2106
+ webtrigger:
2107
+ - key: invoke-schema-migration
2108
+ function: runSchemaMigration
2109
+ security:
2110
+ egress:
2111
+ allowDataEgress: false
2112
+ allowedResponses:
2113
+ - statusCode: 200
2114
+ body: '{"body": "Migrations successfully executed"}'
2115
+ sql:
2116
+ - key: main
2117
+ engine: mysql
2118
+ function:
2119
+ - key: runSchemaMigration
2120
+ handler: index.handlerMigration
2019
2121
  ```
2020
2122
 
2021
2123
  ### 2. Drop Migrations Trigger
@@ -2023,11 +2125,12 @@ Configure in `manifest.yml`:
2023
2125
  ⚠️ **WARNING**: This trigger will permanently delete all data in the specified tables and clear the migrations history. This operation cannot be undone!
2024
2126
 
2025
2127
  This trigger allows you to completely reset your database schema. It's useful for:
2128
+
2026
2129
  - Development environments where you need to start fresh
2027
2130
  - Testing scenarios requiring a clean database
2028
2131
  - Resetting the database before applying new migrations
2029
2132
 
2030
- **Important**: The trigger will drop all tables including migration.
2133
+ **Important**: The trigger will drop all tables including migration.
2031
2134
 
2032
2135
  ```typescript
2033
2136
  // Example usage in your Forge app
@@ -2039,16 +2142,17 @@ export const dropMigrations = () => {
2039
2142
  ```
2040
2143
 
2041
2144
  Configure in `manifest.yml`:
2145
+
2042
2146
  ```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
2147
+ webtrigger:
2148
+ - key: drop-schema-migration
2149
+ function: dropMigrations
2150
+ sql:
2151
+ - key: main
2152
+ engine: mysql
2153
+ function:
2154
+ - key: dropMigrations
2155
+ handler: index.dropMigrations
2052
2156
  ```
2053
2157
 
2054
2158
  ### 3. Fetch Schema Trigger
@@ -2056,12 +2160,14 @@ Configure in `manifest.yml`:
2056
2160
  ⚠️ **DEVELOPMENT ONLY**: This trigger is designed for development environments only and should not be used in production.
2057
2161
 
2058
2162
  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:
2163
+
2059
2164
  - Development environment setup
2060
2165
  - Schema documentation
2061
2166
  - Database structure verification
2062
2167
  - Creating backup scripts
2063
2168
 
2064
2169
  **Security Considerations**:
2170
+
2065
2171
  - This trigger exposes your database structure
2066
2172
  - It temporarily disables foreign key checks
2067
2173
  - It may expose sensitive table names and structures
@@ -2077,19 +2183,21 @@ export const fetchSchema = async () => {
2077
2183
  ```
2078
2184
 
2079
2185
  Configure in `manifest.yml`:
2186
+
2080
2187
  ```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
2188
+ webtrigger:
2189
+ - key: fetch-schema
2190
+ function: fetchSchema
2191
+ sql:
2192
+ - key: main
2193
+ engine: mysql
2194
+ function:
2195
+ - key: fetchSchema
2196
+ handler: index.fetchSchema
2090
2197
  ```
2091
2198
 
2092
2199
  The response will contain SQL statements like:
2200
+
2093
2201
  ```sql
2094
2202
  SET foreign_key_checks = 0;
2095
2203
  CREATE TABLE IF NOT EXISTS users (...);
@@ -2100,6 +2208,7 @@ SET foreign_key_checks = 1;
2100
2208
  ### 4. Clear Cache Scheduler Trigger
2101
2209
 
2102
2210
  This trigger automatically cleans up expired cache entries based on their TTL (Time To Live). It's useful for:
2211
+
2103
2212
  - Automatic cache maintenance
2104
2213
  - Preventing cache storage from growing indefinitely
2105
2214
  - Ensuring optimal cache performance
@@ -2110,24 +2219,26 @@ This trigger automatically cleans up expired cache entries based on their TTL (T
2110
2219
  import { clearCacheSchedulerTrigger } from "forge-sql-orm";
2111
2220
 
2112
2221
  export const clearCache = () => {
2113
- return clearCacheSchedulerTrigger({
2222
+ return clearCacheSchedulerTrigger({
2114
2223
  cacheEntityName: "cache",
2115
2224
  });
2116
2225
  };
2117
2226
  ```
2118
2227
 
2119
2228
  Configure in `manifest.yml`:
2229
+
2120
2230
  ```yaml
2121
- scheduledTrigger:
2122
- - key: clear-cache-trigger
2123
- function: clearCache
2124
- interval: fiveMinute
2125
- function:
2126
- - key: clearCache
2127
- handler: index.clearCache
2231
+ scheduledTrigger:
2232
+ - key: clear-cache-trigger
2233
+ function: clearCache
2234
+ interval: fiveMinute
2235
+ function:
2236
+ - key: clearCache
2237
+ handler: index.clearCache
2128
2238
  ```
2129
2239
 
2130
2240
  **Available Intervals**:
2241
+
2131
2242
  - `fiveMinute` - Every 5 minutes
2132
2243
  - `hour` - Every hour
2133
2244
  - `day` - Every day
@@ -2148,14 +2259,15 @@ export const slowQueryTrigger = () =>
2148
2259
  ```
2149
2260
 
2150
2261
  Configure in `manifest.yml`:
2262
+
2151
2263
  ```yaml
2152
- scheduledTrigger:
2153
- - key: slow-query-trigger
2154
- function: slowQueryTrigger
2155
- interval: hour
2156
- function:
2157
- - key: slowQueryTrigger
2158
- handler: index.slowQueryTrigger
2264
+ scheduledTrigger:
2265
+ - key: slow-query-trigger
2266
+ function: slowQueryTrigger
2267
+ interval: hour
2268
+ function:
2269
+ - key: slowQueryTrigger
2270
+ handler: index.slowQueryTrigger
2159
2271
  ```
2160
2272
 
2161
2273
  > **💡 Note**: For complete documentation, examples, and configuration options, see the [Slow Query Monitoring](#slow-query-monitoring) section.
@@ -2163,15 +2275,17 @@ Configure in `manifest.yml`:
2163
2275
  ### Important Notes
2164
2276
 
2165
2277
  **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
2278
+
2279
+ - The drop migrations trigger should be restricted to development environments
2280
+ - The fetch schema trigger should only be used in development
2281
+ - Consider implementing additional authentication for these endpoints
2169
2282
 
2170
2283
  **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
2284
+
2285
+ - Always backup your data before using the drop migrations trigger
2286
+ - Test migrations in a development environment first
2287
+ - Use these triggers as part of your deployment pipeline
2288
+ - Monitor the execution logs in the Forge Developer Console
2175
2289
 
2176
2290
  ## Query Analysis and Performance Optimization
2177
2291
 
@@ -2182,6 +2296,7 @@ Forge-SQL-ORM provides comprehensive query analysis tools to help you optimize y
2182
2296
  ### About Atlassian's Built-in Analysis Tools
2183
2297
 
2184
2298
  Atlassian provides comprehensive query analysis tools in the development console, including:
2299
+
2185
2300
  - Basic query performance metrics
2186
2301
  - Slow query tracking (queries over 500ms)
2187
2302
  - Basic execution statistics
@@ -2281,8 +2396,8 @@ modules:
2281
2396
  scheduledTrigger:
2282
2397
  - key: slow-query-trigger
2283
2398
  function: slowQueryTrigger
2284
- interval: hour # Run every hour
2285
-
2399
+ interval: hour # Run every hour
2400
+
2286
2401
  function:
2287
2402
  - key: slowQueryTrigger
2288
2403
  handler: index.slowQueryTrigger
@@ -2290,10 +2405,10 @@ modules:
2290
2405
 
2291
2406
  #### Configuration Options
2292
2407
 
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 |
2408
+ | Option | Type | Default | Description |
2409
+ | --------- | -------- | ------- | ---------------------------------------------------------- |
2410
+ | `hours` | `number` | `1` | Number of hours to look back for slow queries |
2411
+ | `timeout` | `number` | `3000` | Timeout in milliseconds for the diagnostic query execution |
2297
2412
 
2298
2413
  #### Example Console Output
2299
2414
 
@@ -2374,59 +2489,57 @@ const analyzeForgeSql = forgeSQL.analyze();
2374
2489
 
2375
2490
  // Analyze a Drizzle query
2376
2491
  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))
2492
+ forgeSQL
2493
+ .select({
2494
+ table1: testEntityJoin1,
2495
+ table2: { name: testEntityJoin2.name, email: testEntityJoin2.email },
2496
+ count: rawSql<number>`COUNT(*)`,
2497
+ table3: {
2498
+ table12: testEntityJoin1.name,
2499
+ table22: testEntityJoin2.email,
2500
+ table32: testEntity.id,
2501
+ },
2502
+ })
2503
+ .from(testEntityJoin1)
2504
+ .innerJoin(testEntityJoin2, eq(testEntityJoin1.id, testEntityJoin2.id)),
2389
2505
  );
2390
2506
 
2391
2507
  // Analyze a raw SQL query
2392
- const rawPlan = await analyzeForgeSql.explainRaw(
2393
- "SELECT * FROM users WHERE id = ?",
2394
- [1]
2395
- );
2508
+ const rawPlan = await analyzeForgeSql.explainRaw("SELECT * FROM users WHERE id = ?", [1]);
2396
2509
 
2397
2510
  // Analyze new methods
2398
2511
  const usersFromPlan = await analyzeForgeSql.explain(
2399
- forgeSQL.selectFrom(users).where(eq(users.active, true))
2512
+ forgeSQL.selectFrom(users).where(eq(users.active, true)),
2400
2513
  );
2401
2514
 
2402
2515
  const usersCacheablePlan = await analyzeForgeSql.explain(
2403
- forgeSQL.selectCacheableFrom(users).where(eq(users.active, true))
2516
+ forgeSQL.selectCacheableFrom(users).where(eq(users.active, true)),
2404
2517
  );
2405
2518
 
2406
2519
  // Analyze Common Table Expressions (CTEs)
2407
2520
  const ctePlan = await analyzeForgeSql.explain(
2408
2521
  forgeSQL
2409
2522
  .with(
2410
- forgeSQL.selectFrom(users).where(eq(users.active, true)).as('activeUsers'),
2411
- forgeSQL.selectFrom(orders).where(eq(orders.status, 'completed')).as('completedOrders')
2523
+ forgeSQL.selectFrom(users).where(eq(users.active, true)).as("activeUsers"),
2524
+ forgeSQL.selectFrom(orders).where(eq(orders.status, "completed")).as("completedOrders"),
2412
2525
  )
2413
2526
  .select({
2414
2527
  totalActiveUsers: sql`COUNT(au.id)`,
2415
- totalCompletedOrders: sql`COUNT(co.id)`
2528
+ totalCompletedOrders: sql`COUNT(co.id)`,
2416
2529
  })
2417
2530
  .from(sql`activeUsers au`)
2418
- .leftJoin(sql`completedOrders co`, eq(sql`au.id`, sql`co.userId`))
2531
+ .leftJoin(sql`completedOrders co`, eq(sql`au.id`, sql`co.userId`)),
2419
2532
  );
2420
2533
  ```
2421
2534
 
2422
2535
  This analysis provides insights into:
2536
+
2423
2537
  - How the database executes your query
2424
2538
  - Which indexes are being used
2425
2539
  - Estimated vs actual row counts
2426
2540
  - Resource usage at each step
2427
2541
  - Performance optimization opportunities
2428
2542
 
2429
-
2430
2543
  ## Migration Guide
2431
2544
 
2432
2545
  ### Migrating from 2.0.x to 2.1.x
@@ -2436,18 +2549,20 @@ This section covers the breaking changes introduced in version 2.1.x and how to
2436
2549
  #### 1. Method Renaming (BREAKING CHANGES)
2437
2550
 
2438
2551
  **Removed Methods:**
2552
+
2439
2553
  - `forgeSQL.modify()` → **REMOVED** (use `forgeSQL.modifyWithVersioning()`)
2440
2554
  - `forgeSQL.crud()` → **REMOVED** (use `forgeSQL.modifyWithVersioning()`)
2441
2555
 
2442
2556
  **Migration Steps:**
2443
2557
 
2444
2558
  1. **Replace `modify()` calls:**
2559
+
2445
2560
  ```typescript
2446
2561
  // ❌ Old (2.0.x) - NO LONGER WORKS
2447
2562
  await forgeSQL.modify().insert(Users, [userData]);
2448
2563
  await forgeSQL.modify().updateById(updateData, Users);
2449
2564
  await forgeSQL.modify().deleteById(1, Users);
2450
-
2565
+
2451
2566
  // ✅ New (2.1.x) - REQUIRED
2452
2567
  await forgeSQL.modifyWithVersioning().insert(Users, [userData]);
2453
2568
  await forgeSQL.modifyWithVersioning().updateById(updateData, Users);
@@ -2455,12 +2570,13 @@ This section covers the breaking changes introduced in version 2.1.x and how to
2455
2570
  ```
2456
2571
 
2457
2572
  2. **Replace `crud()` calls:**
2573
+
2458
2574
  ```typescript
2459
2575
  // ❌ Old (2.0.x) - NO LONGER WORKS
2460
2576
  await forgeSQL.crud().insert(Users, [userData]);
2461
2577
  await forgeSQL.crud().updateById(updateData, Users);
2462
2578
  await forgeSQL.crud().deleteById(1, Users);
2463
-
2579
+
2464
2580
  // ✅ New (2.1.x) - REQUIRED
2465
2581
  await forgeSQL.modifyWithVersioning().insert(Users, [userData]);
2466
2582
  await forgeSQL.modifyWithVersioning().updateById(updateData, Users);
@@ -2470,8 +2586,9 @@ This section covers the breaking changes introduced in version 2.1.x and how to
2470
2586
  #### 2. New API Methods
2471
2587
 
2472
2588
  **New Methods Available:**
2589
+
2473
2590
  - `forgeSQL.insert()` - Basic Drizzle operations
2474
- - `forgeSQL.update()` - Basic Drizzle operations
2591
+ - `forgeSQL.update()` - Basic Drizzle operations
2475
2592
  - `forgeSQL.delete()` - Basic Drizzle operations
2476
2593
  - `forgeSQL.insertAndEvictCache()` - Basic Drizzle operations with evict cache after execution
2477
2594
  - `forgeSQL.updateAndEvictCache()` - Basic Drizzle operations with evict cache after execution
@@ -2499,47 +2616,43 @@ await forgeSQL.insert(Users).values(userData);
2499
2616
  await forgeSQL.modifyWithVersioningAndEvictCache().insert(Users, [userData]);
2500
2617
 
2501
2618
  // ✅ New query methods for better performance
2502
- const users = await forgeSQL.selectFrom(Users)
2503
- .where(eq(Users.active, true));
2619
+ const users = await forgeSQL.selectFrom(Users).where(eq(Users.active, true));
2504
2620
 
2505
- const usersDistinct = await forgeSQL.selectDistinctFrom(Users)
2506
- .where(eq(Users.active, true));
2621
+ const usersDistinct = await forgeSQL.selectDistinctFrom(Users).where(eq(Users.active, true));
2507
2622
 
2508
- const usersCacheable = await forgeSQL.selectCacheableFrom(Users)
2509
- .where(eq(Users.active, true));
2623
+ const usersCacheable = await forgeSQL.selectCacheableFrom(Users).where(eq(Users.active, true));
2510
2624
 
2511
2625
  // ✅ Raw SQL execution with caching
2512
- const rawUsers = await forgeSQL.execute(
2513
- "SELECT * FROM users WHERE active = ?",
2514
- [true]
2515
- );
2626
+ const rawUsers = await forgeSQL.execute("SELECT * FROM users WHERE active = ?", [true]);
2516
2627
 
2517
2628
  // ⚠️ IMPORTANT: When using executeCacheable(), all table names must be wrapped with backticks (`)
2518
2629
  const cachedRawUsers = await forgeSQL.executeCacheable(
2519
- "SELECT * FROM `users` WHERE active = ?",
2520
- [true],
2521
- 300
2630
+ "SELECT * FROM `users` WHERE active = ?",
2631
+ [true],
2632
+ 300,
2522
2633
  );
2523
2634
 
2524
2635
  // ✅ Raw SQL execution with metadata capture and performance monitoring
2525
2636
  const usersWithMetadata = await forgeSQL.executeWithMetadata(
2526
2637
  async () => {
2527
2638
  const users = await forgeSQL.selectFrom(usersTable);
2528
- const orders = await forgeSQL.selectFrom(ordersTable).where(eq(ordersTable.userId, usersTable.id));
2639
+ const orders = await forgeSQL
2640
+ .selectFrom(ordersTable)
2641
+ .where(eq(ordersTable.userId, usersTable.id));
2529
2642
  return { users, orders };
2530
2643
  },
2531
2644
  (totalDbExecutionTime, totalResponseSize, printQueriesWithPlan) => {
2532
2645
  const threshold = 500; // ms baseline for this resolver
2533
-
2646
+
2534
2647
  if (totalDbExecutionTime > threshold * 1.5) {
2535
2648
  console.warn(`[Performance Warning] Resolver exceeded DB time: ${totalDbExecutionTime} ms`);
2536
2649
  await printQueriesWithPlan(); // Analyze and print query execution plans
2537
2650
  } else if (totalDbExecutionTime > threshold) {
2538
2651
  console.debug(`[Performance Debug] High DB time: ${totalDbExecutionTime} ms`);
2539
2652
  }
2540
-
2653
+
2541
2654
  console.log(`DB response size: ${totalResponseSize} bytes`);
2542
- }
2655
+ },
2543
2656
  );
2544
2657
 
2545
2658
  // ✅ DDL operations for schema modifications
@@ -2563,25 +2676,25 @@ await forgeSQL.executeDDLActions(async () => {
2563
2676
  SELECT * FROM INFORMATION_SCHEMA.STATEMENTS_SUMMARY
2564
2677
  WHERE AVG_LATENCY > 1000000
2565
2678
  `);
2566
-
2679
+
2567
2680
  // Execute complex analysis queries in DDL context
2568
2681
  const performanceData = await forgeSQL.execute(`
2569
2682
  SELECT * FROM INFORMATION_SCHEMA.CLUSTER_STATEMENTS_SUMMARY_HISTORY
2570
2683
  WHERE SUMMARY_END_TIME > DATE_SUB(NOW(), INTERVAL 1 HOUR)
2571
2684
  `);
2572
-
2685
+
2573
2686
  return { slowQueries, performanceData };
2574
2687
  });
2575
2688
 
2576
2689
  // ✅ Common Table Expressions (CTEs)
2577
2690
  const userStats = await forgeSQL
2578
2691
  .with(
2579
- forgeSQL.selectFrom(users).where(eq(users.active, true)).as('activeUsers'),
2580
- forgeSQL.selectFrom(orders).where(eq(orders.status, 'completed')).as('completedOrders')
2692
+ forgeSQL.selectFrom(users).where(eq(users.active, true)).as("activeUsers"),
2693
+ forgeSQL.selectFrom(orders).where(eq(orders.status, "completed")).as("completedOrders"),
2581
2694
  )
2582
2695
  .select({
2583
2696
  totalActiveUsers: sql`COUNT(au.id)`,
2584
- totalCompletedOrders: sql`COUNT(co.id)`
2697
+ totalCompletedOrders: sql`COUNT(co.id)`,
2585
2698
  })
2586
2699
  .from(sql`activeUsers au`)
2587
2700
  .leftJoin(sql`completedOrders co`, eq(sql`au.id`, sql`co.userId`));
@@ -2595,7 +2708,7 @@ You can use a simple find-and-replace to migrate your code:
2595
2708
  # Replace modify() calls
2596
2709
  find . -name "*.ts" -o -name "*.js" | xargs sed -i 's/forgeSQL\.modify()/forgeSQL.modifyWithVersioning()/g'
2597
2710
 
2598
- # Replace crud() calls
2711
+ # Replace crud() calls
2599
2712
  find . -name "*.ts" -o -name "*.js" | xargs sed -i 's/forgeSQL\.crud()/forgeSQL.modifyWithVersioning()/g'
2600
2713
  ```
2601
2714
 
@@ -2607,5 +2720,6 @@ find . -name "*.ts" -o -name "*.js" | xargs sed -i 's/forgeSQL\.crud()/forgeSQL.
2607
2720
  - ✅ **Migration Required**: You must update your code to use the new methods
2608
2721
 
2609
2722
  ## License
2723
+
2610
2724
  This project is licensed under the **MIT License**.
2611
2725
  Feel free to use it for commercial and personal projects.