forge-sql-orm 2.1.12 → 2.1.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +922 -549
- package/dist/core/ForgeSQLAnalyseOperations.d.ts.map +1 -1
- package/dist/core/ForgeSQLAnalyseOperations.js +257 -0
- package/dist/core/ForgeSQLAnalyseOperations.js.map +1 -0
- package/dist/core/ForgeSQLCacheOperations.js +172 -0
- package/dist/core/ForgeSQLCacheOperations.js.map +1 -0
- package/dist/core/ForgeSQLCrudOperations.js +349 -0
- package/dist/core/ForgeSQLCrudOperations.js.map +1 -0
- package/dist/core/ForgeSQLORM.d.ts +29 -1
- package/dist/core/ForgeSQLORM.d.ts.map +1 -1
- package/dist/core/ForgeSQLORM.js +1252 -0
- package/dist/core/ForgeSQLORM.js.map +1 -0
- package/dist/core/ForgeSQLQueryBuilder.d.ts +179 -1
- package/dist/core/ForgeSQLQueryBuilder.d.ts.map +1 -1
- package/dist/core/ForgeSQLQueryBuilder.js +77 -0
- package/dist/core/ForgeSQLQueryBuilder.js.map +1 -0
- package/dist/core/ForgeSQLSelectOperations.js +81 -0
- package/dist/core/ForgeSQLSelectOperations.js.map +1 -0
- package/dist/core/Rovo.d.ts +116 -0
- package/dist/core/Rovo.d.ts.map +1 -0
- package/dist/core/Rovo.js +647 -0
- package/dist/core/Rovo.js.map +1 -0
- package/dist/core/SystemTables.js +258 -0
- package/dist/core/SystemTables.js.map +1 -0
- package/dist/index.js +30 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/drizzle/extensions/additionalActions.d.ts.map +1 -1
- package/dist/lib/drizzle/extensions/additionalActions.js +527 -0
- package/dist/lib/drizzle/extensions/additionalActions.js.map +1 -0
- package/dist/utils/cacheContextUtils.d.ts.map +1 -1
- package/dist/utils/cacheContextUtils.js +198 -0
- package/dist/utils/cacheContextUtils.js.map +1 -0
- package/dist/utils/cacheUtils.d.ts.map +1 -1
- package/dist/utils/cacheUtils.js +383 -0
- package/dist/utils/cacheUtils.js.map +1 -0
- package/dist/utils/forgeDriver.d.ts.map +1 -1
- package/dist/utils/forgeDriver.js +139 -0
- package/dist/utils/forgeDriver.js.map +1 -0
- package/dist/utils/forgeDriverProxy.js +68 -0
- package/dist/utils/forgeDriverProxy.js.map +1 -0
- package/dist/utils/metadataContextUtils.d.ts.map +1 -1
- package/dist/utils/metadataContextUtils.js +26 -0
- package/dist/utils/metadataContextUtils.js.map +1 -0
- package/dist/utils/requestTypeContextUtils.js +10 -0
- package/dist/utils/requestTypeContextUtils.js.map +1 -0
- package/dist/utils/sqlHints.js +52 -0
- package/dist/utils/sqlHints.js.map +1 -0
- package/dist/utils/sqlUtils.d.ts.map +1 -1
- package/dist/utils/sqlUtils.js +590 -0
- package/dist/utils/sqlUtils.js.map +1 -0
- package/dist/webtriggers/applyMigrationsWebTrigger.js +77 -0
- package/dist/webtriggers/applyMigrationsWebTrigger.js.map +1 -0
- package/dist/webtriggers/clearCacheSchedulerTrigger.js +83 -0
- package/dist/webtriggers/clearCacheSchedulerTrigger.js.map +1 -0
- package/dist/webtriggers/dropMigrationWebTrigger.js +54 -0
- package/dist/webtriggers/dropMigrationWebTrigger.js.map +1 -0
- package/dist/webtriggers/dropTablesMigrationWebTrigger.js +54 -0
- package/dist/webtriggers/dropTablesMigrationWebTrigger.js.map +1 -0
- package/dist/webtriggers/fetchSchemaWebTrigger.js +82 -0
- package/dist/webtriggers/fetchSchemaWebTrigger.js.map +1 -0
- package/dist/webtriggers/index.js +40 -0
- package/dist/webtriggers/index.js.map +1 -0
- package/dist/webtriggers/slowQuerySchedulerTrigger.js +80 -0
- package/dist/webtriggers/slowQuerySchedulerTrigger.js.map +1 -0
- package/package.json +31 -25
- package/src/core/ForgeSQLAnalyseOperations.ts +3 -2
- package/src/core/ForgeSQLORM.ts +64 -0
- package/src/core/ForgeSQLQueryBuilder.ts +200 -1
- package/src/core/Rovo.ts +765 -0
- package/src/lib/drizzle/extensions/additionalActions.ts +11 -0
- package/src/utils/cacheContextUtils.ts +9 -6
- package/src/utils/cacheUtils.ts +6 -4
- package/src/utils/forgeDriver.ts +3 -7
- package/src/utils/metadataContextUtils.ts +1 -3
- package/src/utils/sqlUtils.ts +33 -34
- package/dist/ForgeSQLORM.js +0 -3922
- package/dist/ForgeSQLORM.js.map +0 -1
- package/dist/ForgeSQLORM.mjs +0 -3905
- package/dist/ForgeSQLORM.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -11,10 +11,10 @@
|
|
|
11
11
|
[](https://coveralls.io/github/vzakharchenko/forge-sql-orm?branch=master)
|
|
12
12
|
[](https://deepscan.io/dashboard#view=project&tid=26652&pid=29272&bid=940614)
|
|
13
13
|
|
|
14
|
-
|
|
15
14
|
**Forge-SQL-ORM** is an ORM designed for working with [@forge/sql](https://developer.atlassian.com/platform/forge/storage-reference/sql-tutorial/) in **Atlassian Forge**. It is built on top of [Drizzle ORM](https://orm.drizzle.team) and provides advanced capabilities for working with relational databases inside Forge.
|
|
16
15
|
|
|
17
16
|
## Key Features
|
|
17
|
+
|
|
18
18
|
- ✅ **Custom Drizzle Driver** for direct integration with @forge/sql
|
|
19
19
|
- ✅ **Local Cache System (Level 1)** for in-memory query optimization within single resolver invocation scope
|
|
20
20
|
- ✅ **Global Cache System (Level 2)** with cross-invocation caching, automatic cache invalidation and context-aware operations (using [@forge/kvs](https://developer.atlassian.com/platform/forge/storage-reference/storage-api-custom-entities/) )
|
|
@@ -33,10 +33,12 @@
|
|
|
33
33
|
- ✅ **Ready-to-use Migration Triggers** Built-in web triggers for applying migrations, dropping tables (development-only), and fetching schema (development-only) with proper error handling and security controls
|
|
34
34
|
- ✅ **Optimistic Locking** Ensures data consistency by preventing conflicts when multiple users update the same record
|
|
35
35
|
- ✅ **Query Plan Analysis**: Detailed execution plan analysis and optimization insights
|
|
36
|
+
- ✅ **Rovo Integration** Secure pattern for natural-language analytics with comprehensive security validations, Row-Level Security (RLS) support, and dynamic SQL query execution
|
|
36
37
|
|
|
37
38
|
## Table of Contents
|
|
38
39
|
|
|
39
40
|
### 🚀 Getting Started
|
|
41
|
+
|
|
40
42
|
- [Key Features](#key-features)
|
|
41
43
|
- [Usage Approaches](#usage-approaches)
|
|
42
44
|
- [Installation](#installation)
|
|
@@ -44,16 +46,19 @@
|
|
|
44
46
|
- [Quick Start](#quick-start)
|
|
45
47
|
|
|
46
48
|
### 📖 Core Features
|
|
49
|
+
|
|
47
50
|
- [Field Name Collision Prevention](#field-name-collision-prevention-in-complex-queries)
|
|
48
51
|
- [Drizzle Usage with forge-sql-orm](#drizzle-usage-with-forge-sql-orm)
|
|
49
52
|
- [Direct Drizzle Usage with Custom Driver](#direct-drizzle-usage-with-custom-driver)
|
|
50
53
|
|
|
51
54
|
### 🗄️ Database Operations
|
|
55
|
+
|
|
52
56
|
- [Fetch Data](#fetch-data)
|
|
53
57
|
- [Modify Operations](#modify-operations)
|
|
54
58
|
- [SQL Utilities](#sql-utilities)
|
|
55
59
|
|
|
56
60
|
### ⚡ Caching System
|
|
61
|
+
|
|
57
62
|
- [Setting Up Caching with @forge/kvs](#setting-up-caching-with-forgekvs-optional)
|
|
58
63
|
- [Global Cache System (Level 2)](#global-cache-system-level-2)
|
|
59
64
|
- [Cache Context Operations](#cache-context-operations)
|
|
@@ -62,19 +67,23 @@
|
|
|
62
67
|
- [Manual Cache Management](#manual-cache-management)
|
|
63
68
|
|
|
64
69
|
### 🔒 Advanced Features
|
|
70
|
+
|
|
65
71
|
- [Optimistic Locking](#optimistic-locking)
|
|
72
|
+
- [Rovo Integration](#rovo-integration) - Secure pattern for natural-language analytics with dynamic SQL queries
|
|
66
73
|
- [Query Analysis and Performance Optimization](#query-analysis-and-performance-optimization)
|
|
67
74
|
- [Automatic Error Analysis](#automatic-error-analysis) - Automatic timeout and OOM error detection with execution plans
|
|
68
75
|
- [Slow Query Monitoring](#slow-query-monitoring) - Scheduled monitoring of slow queries with execution plans
|
|
69
76
|
- [Date and Time Types](#date-and-time-types)
|
|
70
77
|
|
|
71
78
|
### 🛠️ Development Tools
|
|
79
|
+
|
|
72
80
|
- [CLI Commands](#cli-commands) | [CLI Documentation](forge-sql-orm-cli/README.md)
|
|
73
81
|
- [Web Triggers for Migrations](#web-triggers-for-migrations)
|
|
74
82
|
- [Step-by-Step Migration Workflow](#step-by-step-migration-workflow)
|
|
75
83
|
- [Drop Migrations](#drop-migrations)
|
|
76
84
|
|
|
77
85
|
### 📚 Examples
|
|
86
|
+
|
|
78
87
|
- [Simple Example](examples/forge-sql-orm-example-simple)
|
|
79
88
|
- [Drizzle Driver Example](examples/forge-sql-orm-example-drizzle-driver-simple)
|
|
80
89
|
- [Optimistic Locking Example](examples/forge-sql-orm-example-optimistic-locking)
|
|
@@ -83,52 +92,62 @@
|
|
|
83
92
|
- [Organization Tracker Example](examples/forge-sql-orm-example-org-tracker)
|
|
84
93
|
- [Checklist Example](examples/forge-sql-orm-example-checklist)
|
|
85
94
|
- [Cache Example](examples/forge-sql-orm-example-cache) - Advanced caching capabilities with performance monitoring
|
|
95
|
+
- [Rovo Integration Example](https://github.com/vzakharchenko/Forge-Secure-Notes-for-Jira) - Real-world Rovo AI agent implementation with secure natural-language analytics
|
|
86
96
|
|
|
87
97
|
### 📚 Reference
|
|
98
|
+
|
|
88
99
|
- [ForgeSqlOrmOptions](#forgesqlormoptions)
|
|
89
100
|
- [Migration Guide](#migration-guide)
|
|
90
101
|
|
|
91
102
|
## 🚀 Quick Navigation
|
|
92
103
|
|
|
93
104
|
**New to Forge-SQL-ORM?** Start here:
|
|
105
|
+
|
|
94
106
|
- [Quick Start](#quick-start) - Get up and running in 5 minutes
|
|
95
107
|
- [Installation](#installation) - Complete setup guide
|
|
96
108
|
- [Basic Usage Examples](#fetch-data) - Simple query examples
|
|
97
109
|
|
|
98
110
|
**Looking for specific features?**
|
|
111
|
+
|
|
99
112
|
- [Global Cache System (Level 2)](#global-cache-system-level-2) - Cross-invocation persistent caching
|
|
100
113
|
- [Local Cache System (Level 1)](#local-cache-operations-level-1) - In-memory invocation caching
|
|
101
114
|
- [Optimistic Locking](#optimistic-locking) - Data consistency
|
|
115
|
+
- [Rovo Integration](#rovo-integration) - Secure natural-language analytics
|
|
102
116
|
- [Migration Tools](#web-triggers-for-migrations) - Database migrations
|
|
103
117
|
- [Query Analysis](#query-analysis-and-performance-optimization) - Performance optimization
|
|
104
118
|
|
|
105
119
|
**Looking for practical examples?**
|
|
120
|
+
|
|
106
121
|
- [Simple Example](examples/forge-sql-orm-example-simple) - Basic ORM usage
|
|
107
122
|
- [Optimistic Locking Example](examples/forge-sql-orm-example-optimistic-locking) - Real-world conflict handling
|
|
108
123
|
- [Organization Tracker Example](examples/forge-sql-orm-example-org-tracker) - Complex relationships
|
|
109
124
|
- [Checklist Example](examples/forge-sql-orm-example-checklist) - Jira integration
|
|
110
125
|
- [Cache Example](examples/forge-sql-orm-example-cache) - Advanced caching capabilities
|
|
126
|
+
- [Rovo Integration Example](https://github.com/vzakharchenko/Forge-Secure-Notes-for-Jira) - Real-world Rovo AI agent with secure analytics
|
|
111
127
|
|
|
112
128
|
## Usage Approaches
|
|
113
129
|
|
|
114
|
-
|
|
115
130
|
### 1. Full Forge-SQL-ORM Usage
|
|
131
|
+
|
|
116
132
|
```typescript
|
|
117
133
|
import ForgeSQL from "forge-sql-orm";
|
|
118
134
|
const forgeSQL = new ForgeSQL();
|
|
119
135
|
```
|
|
136
|
+
|
|
120
137
|
Best for: Advanced features like optimistic locking, automatic versioning, and automatic field name collision prevention in complex queries.
|
|
121
138
|
|
|
122
139
|
### 2. Direct Drizzle Usage
|
|
140
|
+
|
|
123
141
|
```typescript
|
|
124
142
|
import { drizzle } from "drizzle-orm/mysql-proxy";
|
|
125
143
|
import { forgeDriver } from "forge-sql-orm";
|
|
126
144
|
const db = drizzle(forgeDriver);
|
|
127
145
|
```
|
|
128
|
-
Best for: Simple Modify operations without optimistic locking. Note that you need to manually patch drizzle `patchDbWithSelectAliased` for select fields to prevent field name collisions in Atlassian Forge SQL.
|
|
129
146
|
|
|
147
|
+
Best for: Simple Modify operations without optimistic locking. Note that you need to manually patch drizzle `patchDbWithSelectAliased` for select fields to prevent field name collisions in Atlassian Forge SQL.
|
|
130
148
|
|
|
131
149
|
### 3. Local Cache Optimization
|
|
150
|
+
|
|
132
151
|
```typescript
|
|
133
152
|
import ForgeSQL from "forge-sql-orm";
|
|
134
153
|
const forgeSQL = new ForgeSQL();
|
|
@@ -136,28 +155,28 @@ const forgeSQL = new ForgeSQL();
|
|
|
136
155
|
// Optimize repeated queries within a single invocation
|
|
137
156
|
await forgeSQL.executeWithLocalContext(async () => {
|
|
138
157
|
// Multiple queries here will benefit from local caching
|
|
139
|
-
const users = await forgeSQL
|
|
140
|
-
.
|
|
141
|
-
|
|
158
|
+
const users = await forgeSQL
|
|
159
|
+
.select({ id: users.id, name: users.name })
|
|
160
|
+
.from(users)
|
|
161
|
+
.where(eq(users.active, true));
|
|
162
|
+
|
|
142
163
|
// This query will use local cache (no database call)
|
|
143
|
-
const cachedUsers = await forgeSQL
|
|
144
|
-
.
|
|
145
|
-
|
|
146
|
-
// Using new methods for better performance
|
|
147
|
-
const usersFrom = await forgeSQL.selectFrom(users)
|
|
164
|
+
const cachedUsers = await forgeSQL
|
|
165
|
+
.select({ id: users.id, name: users.name })
|
|
166
|
+
.from(users)
|
|
148
167
|
.where(eq(users.active, true));
|
|
149
|
-
|
|
168
|
+
|
|
169
|
+
// Using new methods for better performance
|
|
170
|
+
const usersFrom = await forgeSQL.selectFrom(users).where(eq(users.active, true));
|
|
171
|
+
|
|
150
172
|
// This will use local cache (no database call)
|
|
151
|
-
const cachedUsersFrom = await forgeSQL.selectFrom(users)
|
|
152
|
-
|
|
153
|
-
|
|
173
|
+
const cachedUsersFrom = await forgeSQL.selectFrom(users).where(eq(users.active, true));
|
|
174
|
+
|
|
154
175
|
// Raw SQL with local caching
|
|
155
|
-
const rawUsers = await forgeSQL.execute(
|
|
156
|
-
"SELECT id, name FROM users WHERE active = ?",
|
|
157
|
-
[true]
|
|
158
|
-
);
|
|
176
|
+
const rawUsers = await forgeSQL.execute("SELECT id, name FROM users WHERE active = ?", [true]);
|
|
159
177
|
});
|
|
160
178
|
```
|
|
179
|
+
|
|
161
180
|
Best for: Performance optimization of repeated queries within resolvers or single invocation contexts.
|
|
162
181
|
|
|
163
182
|
## Field Name Collision Prevention in Complex Queries
|
|
@@ -167,6 +186,7 @@ When working with complex queries involving multiple tables (joins, inner joins,
|
|
|
167
186
|
Forge-SQL-ORM provides two ways to handle this:
|
|
168
187
|
|
|
169
188
|
### Using Forge-SQL-ORM
|
|
189
|
+
|
|
170
190
|
```typescript
|
|
171
191
|
import ForgeSQL from "forge-sql-orm";
|
|
172
192
|
|
|
@@ -174,12 +194,13 @@ const forgeSQL = new ForgeSQL();
|
|
|
174
194
|
|
|
175
195
|
// Automatic field name collision prevention
|
|
176
196
|
await forgeSQL
|
|
177
|
-
.select({user: users, order: orders})
|
|
197
|
+
.select({ user: users, order: orders })
|
|
178
198
|
.from(orders)
|
|
179
199
|
.innerJoin(users, eq(orders.userId, users.id));
|
|
180
200
|
```
|
|
181
201
|
|
|
182
202
|
### Using Direct Drizzle
|
|
203
|
+
|
|
183
204
|
```typescript
|
|
184
205
|
import { drizzle } from "drizzle-orm/mysql-proxy";
|
|
185
206
|
import { forgeDriver, patchDbWithSelectAliased } from "forge-sql-orm";
|
|
@@ -188,18 +209,18 @@ const db = patchDbWithSelectAliased(drizzle(forgeDriver));
|
|
|
188
209
|
|
|
189
210
|
// Manual field name collision prevention
|
|
190
211
|
await db
|
|
191
|
-
.selectAliased({user: users, order: orders})
|
|
212
|
+
.selectAliased({ user: users, order: orders })
|
|
192
213
|
.from(orders)
|
|
193
214
|
.innerJoin(users, eq(orders.userId, users.id));
|
|
194
215
|
```
|
|
195
216
|
|
|
196
217
|
### Important Notes
|
|
218
|
+
|
|
197
219
|
- This is a specific behavior of Atlassian Forge SQL, not Drizzle ORM
|
|
198
220
|
- For complex queries involving multiple tables, it's recommended to always specify select fields and avoid using `select()` without field selection
|
|
199
221
|
- The solution automatically creates unique aliases for each field by prefixing them with the table name
|
|
200
222
|
- This ensures that fields with the same name from different tables remain distinct in the query results
|
|
201
223
|
|
|
202
|
-
|
|
203
224
|
## Installation
|
|
204
225
|
|
|
205
226
|
Forge-SQL-ORM is designed to work with @forge/sql and requires some additional setup to ensure compatibility within Atlassian Forge.
|
|
@@ -207,16 +228,35 @@ Forge-SQL-ORM is designed to work with @forge/sql and requires some additional s
|
|
|
207
228
|
✅ Step 1: Install Dependencies
|
|
208
229
|
|
|
209
230
|
**Basic installation (without caching):**
|
|
231
|
+
|
|
210
232
|
```sh
|
|
211
233
|
npm install forge-sql-orm @forge/sql drizzle-orm -S
|
|
212
234
|
```
|
|
213
235
|
|
|
214
236
|
**With caching support:**
|
|
237
|
+
|
|
215
238
|
```sh
|
|
216
239
|
npm install forge-sql-orm @forge/sql @forge/kvs drizzle-orm -S
|
|
217
240
|
```
|
|
218
241
|
|
|
242
|
+
**⚠️ Important for UI-Kit projects:**
|
|
243
|
+
|
|
244
|
+
If you're installing `forge-sql-orm` in a UI-Kit project (projects using `@forge/react`), you may encounter peer dependency conflicts with `@types/react`. This is due to a conflict between `@types/react@18` (required by `@forge/react`) and `@types/react@19` (optional peer dependency from `drizzle-orm` via `bun-types`).
|
|
245
|
+
|
|
246
|
+
To resolve this, use the `--legacy-peer-deps` flag:
|
|
247
|
+
|
|
248
|
+
```sh
|
|
249
|
+
# Basic installation for UI-Kit projects
|
|
250
|
+
npm install forge-sql-orm @forge/sql drizzle-orm -S --legacy-peer-deps
|
|
251
|
+
|
|
252
|
+
# With caching support for UI-Kit projects
|
|
253
|
+
npm install forge-sql-orm @forge/sql @forge/kvs drizzle-orm -S --legacy-peer-deps
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
**Note:** The `--legacy-peer-deps` flag tells npm to ignore peer dependency conflicts. This is safe in this case because `bun-types` is an optional peer dependency and doesn't affect the functionality of `forge-sql-orm` in Forge environments.
|
|
257
|
+
|
|
219
258
|
This will:
|
|
259
|
+
|
|
220
260
|
- Install Forge-SQL-ORM (the ORM for @forge/sql)
|
|
221
261
|
- Install @forge/sql, the Forge database layer
|
|
222
262
|
- Install @forge/kvs, the Forge Key-Value Store for caching (optional, only needed for caching features)
|
|
@@ -227,6 +267,7 @@ This will:
|
|
|
227
267
|
## Quick Start
|
|
228
268
|
|
|
229
269
|
### 1. Basic Setup
|
|
270
|
+
|
|
230
271
|
```typescript
|
|
231
272
|
import ForgeSQL from "forge-sql-orm";
|
|
232
273
|
|
|
@@ -238,44 +279,49 @@ const users = await forgeSQL.select().from(users);
|
|
|
238
279
|
```
|
|
239
280
|
|
|
240
281
|
### 2. With Caching (Optional)
|
|
282
|
+
|
|
241
283
|
```typescript
|
|
242
284
|
import ForgeSQL from "forge-sql-orm";
|
|
243
285
|
|
|
244
286
|
// Initialize with caching
|
|
245
287
|
const forgeSQL = new ForgeSQL({
|
|
246
288
|
cacheEntityName: "cache",
|
|
247
|
-
cacheTTL: 300
|
|
289
|
+
cacheTTL: 300,
|
|
248
290
|
});
|
|
249
291
|
|
|
250
292
|
// Cached query
|
|
251
|
-
const users = await forgeSQL
|
|
252
|
-
.
|
|
293
|
+
const users = await forgeSQL
|
|
294
|
+
.selectCacheable({ id: users.id, name: users.name })
|
|
295
|
+
.from(users)
|
|
296
|
+
.where(eq(users.active, true));
|
|
253
297
|
```
|
|
254
298
|
|
|
255
299
|
### 3. Local Cache Optimization
|
|
300
|
+
|
|
256
301
|
```typescript
|
|
257
302
|
// Optimize repeated queries within a single invocation
|
|
258
303
|
await forgeSQL.executeWithLocalContext(async () => {
|
|
259
|
-
const users = await forgeSQL
|
|
260
|
-
.
|
|
261
|
-
|
|
304
|
+
const users = await forgeSQL
|
|
305
|
+
.select({ id: users.id, name: users.name })
|
|
306
|
+
.from(users)
|
|
307
|
+
.where(eq(users.active, true));
|
|
308
|
+
|
|
262
309
|
// This query will use local cache (no database call)
|
|
263
|
-
const cachedUsers = await forgeSQL
|
|
264
|
-
.
|
|
265
|
-
|
|
266
|
-
// Using new methods for better performance
|
|
267
|
-
const usersFrom = await forgeSQL.selectFrom(users)
|
|
310
|
+
const cachedUsers = await forgeSQL
|
|
311
|
+
.select({ id: users.id, name: users.name })
|
|
312
|
+
.from(users)
|
|
268
313
|
.where(eq(users.active, true));
|
|
269
|
-
|
|
314
|
+
|
|
315
|
+
// Using new methods for better performance
|
|
316
|
+
const usersFrom = await forgeSQL.selectFrom(users).where(eq(users.active, true));
|
|
317
|
+
|
|
270
318
|
// Raw SQL with local caching
|
|
271
|
-
const rawUsers = await forgeSQL.execute(
|
|
272
|
-
"SELECT id, name FROM users WHERE active = ?",
|
|
273
|
-
[true]
|
|
274
|
-
);
|
|
319
|
+
const rawUsers = await forgeSQL.execute("SELECT id, name FROM users WHERE active = ?", [true]);
|
|
275
320
|
});
|
|
276
321
|
```
|
|
277
322
|
|
|
278
323
|
### 4. Resolver Performance Monitoring
|
|
324
|
+
|
|
279
325
|
```typescript
|
|
280
326
|
// Resolver with performance monitoring
|
|
281
327
|
resolver.define("fetch", async (req: Request) => {
|
|
@@ -284,20 +330,23 @@ resolver.define("fetch", async (req: Request) => {
|
|
|
284
330
|
async () => {
|
|
285
331
|
// Resolver logic with multiple queries
|
|
286
332
|
const users = await forgeSQL.selectFrom(demoUsers);
|
|
287
|
-
const orders = await forgeSQL
|
|
333
|
+
const orders = await forgeSQL
|
|
334
|
+
.selectFrom(demoOrders)
|
|
288
335
|
.where(eq(demoOrders.userId, demoUsers.id));
|
|
289
336
|
return { users, orders };
|
|
290
337
|
},
|
|
291
338
|
async (totalDbExecutionTime, totalResponseSize, printQueriesWithPlan) => {
|
|
292
339
|
const threshold = 500; // ms baseline for this resolver
|
|
293
|
-
|
|
340
|
+
|
|
294
341
|
if (totalDbExecutionTime > threshold * 1.5) {
|
|
295
|
-
console.warn(
|
|
342
|
+
console.warn(
|
|
343
|
+
`[Performance Warning fetch] Resolver exceeded DB time: ${totalDbExecutionTime} ms`,
|
|
344
|
+
);
|
|
296
345
|
await printQueriesWithPlan(); // Optionally log or capture diagnostics for further analysis
|
|
297
346
|
} else if (totalDbExecutionTime > threshold) {
|
|
298
347
|
console.debug(`[Performance Debug fetch] High DB time: ${totalDbExecutionTime} ms`);
|
|
299
348
|
}
|
|
300
|
-
}
|
|
349
|
+
},
|
|
301
350
|
);
|
|
302
351
|
} catch (e) {
|
|
303
352
|
const error = e?.cause?.debug?.sqlMessage ?? e?.cause;
|
|
@@ -307,11 +356,33 @@ resolver.define("fetch", async (req: Request) => {
|
|
|
307
356
|
});
|
|
308
357
|
```
|
|
309
358
|
|
|
310
|
-
### 5.
|
|
359
|
+
### 5. Rovo Integration (Secure Analytics)
|
|
360
|
+
|
|
361
|
+
```typescript
|
|
362
|
+
// Secure dynamic SQL queries for natural-language analytics
|
|
363
|
+
const rovo = forgeSQL.rovo();
|
|
364
|
+
const settings = await rovo
|
|
365
|
+
.rovoSettingBuilder(usersTable, accountId)
|
|
366
|
+
.addContextParameter(":currentUserId", accountId)
|
|
367
|
+
.useRLS()
|
|
368
|
+
.addRlsColumn(usersTable.id)
|
|
369
|
+
.addRlsWherePart((alias) => `${alias}.${usersTable.id.name} = '${accountId}'`)
|
|
370
|
+
.finish()
|
|
371
|
+
.build();
|
|
372
|
+
|
|
373
|
+
const result = await rovo.dynamicIsolatedQuery(
|
|
374
|
+
"SELECT id, name FROM users WHERE status = 'active' AND userId = :currentUserId",
|
|
375
|
+
settings,
|
|
376
|
+
);
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
### 6. Next Steps
|
|
380
|
+
|
|
311
381
|
- [Full Installation Guide](#installation) - Complete setup instructions
|
|
312
382
|
- [Core Features](#core-features) - Learn about key capabilities
|
|
313
383
|
- [Global Cache System (Level 2)](#global-cache-system-level-2) - Cross-invocation caching features
|
|
314
384
|
- [Local Cache System (Level 1)](#local-cache-operations-level-1) - In-memory caching features
|
|
385
|
+
- [Rovo Integration](#rovo-integration) - Secure natural-language analytics
|
|
315
386
|
- [API Reference](#reference) - Complete API documentation
|
|
316
387
|
|
|
317
388
|
## Drizzle Usage with forge-sql-orm
|
|
@@ -343,48 +414,44 @@ const db = forgeSQL.getDrizzleQueryBuilder();
|
|
|
343
414
|
const users = await db.select().from(users);
|
|
344
415
|
|
|
345
416
|
// Using new methods for enhanced functionality
|
|
346
|
-
const usersFrom = await forgeSQL.selectFrom(users)
|
|
347
|
-
.where(eq(users.active, true));
|
|
417
|
+
const usersFrom = await forgeSQL.selectFrom(users).where(eq(users.active, true));
|
|
348
418
|
|
|
349
|
-
const usersDistinct = await forgeSQL.selectDistinctFrom(users)
|
|
350
|
-
.where(eq(users.active, true));
|
|
419
|
+
const usersDistinct = await forgeSQL.selectDistinctFrom(users).where(eq(users.active, true));
|
|
351
420
|
|
|
352
|
-
const usersCacheable = await forgeSQL.selectCacheableFrom(users)
|
|
353
|
-
.where(eq(users.active, true));
|
|
421
|
+
const usersCacheable = await forgeSQL.selectCacheableFrom(users).where(eq(users.active, true));
|
|
354
422
|
|
|
355
423
|
// Raw SQL execution
|
|
356
|
-
const rawUsers = await forgeSQL.execute(
|
|
357
|
-
"SELECT * FROM users WHERE active = ?",
|
|
358
|
-
[true]
|
|
359
|
-
);
|
|
424
|
+
const rawUsers = await forgeSQL.execute("SELECT * FROM users WHERE active = ?", [true]);
|
|
360
425
|
|
|
361
426
|
// Raw SQL with caching
|
|
362
427
|
// ⚠️ IMPORTANT: When using executeCacheable(), all table names must be wrapped with backticks (`)
|
|
363
428
|
const cachedRawUsers = await forgeSQL.executeCacheable(
|
|
364
|
-
"SELECT * FROM `users` WHERE active = ?",
|
|
365
|
-
[true],
|
|
366
|
-
300
|
|
429
|
+
"SELECT * FROM `users` WHERE active = ?",
|
|
430
|
+
[true],
|
|
431
|
+
300,
|
|
367
432
|
);
|
|
368
433
|
|
|
369
434
|
// Raw SQL with execution metadata and performance monitoring
|
|
370
435
|
const usersWithMetadata = await forgeSQL.executeWithMetadata(
|
|
371
436
|
async () => {
|
|
372
437
|
const users = await forgeSQL.selectFrom(usersTable);
|
|
373
|
-
const orders = await forgeSQL
|
|
438
|
+
const orders = await forgeSQL
|
|
439
|
+
.selectFrom(ordersTable)
|
|
440
|
+
.where(eq(ordersTable.userId, usersTable.id));
|
|
374
441
|
return { users, orders };
|
|
375
442
|
},
|
|
376
443
|
(totalDbExecutionTime, totalResponseSize, printQueriesWithPlan) => {
|
|
377
444
|
const threshold = 500; // ms baseline for this resolver
|
|
378
|
-
|
|
445
|
+
|
|
379
446
|
if (totalDbExecutionTime > threshold * 1.5) {
|
|
380
447
|
console.warn(`[Performance Warning] Resolver exceeded DB time: ${totalDbExecutionTime} ms`);
|
|
381
448
|
await printQueriesWithPlan(); // Analyze and print query execution plans
|
|
382
449
|
} else if (totalDbExecutionTime > threshold) {
|
|
383
450
|
console.debug(`[Performance Debug] High DB time: ${totalDbExecutionTime} ms`);
|
|
384
451
|
}
|
|
385
|
-
|
|
452
|
+
|
|
386
453
|
console.log(`DB response size: ${totalResponseSize} bytes`);
|
|
387
|
-
}
|
|
454
|
+
},
|
|
388
455
|
);
|
|
389
456
|
|
|
390
457
|
// DDL operations for schema modifications
|
|
@@ -403,28 +470,44 @@ await forgeSQL.executeDDLActions(async () => {
|
|
|
403
470
|
SELECT * FROM INFORMATION_SCHEMA.STATEMENTS_SUMMARY
|
|
404
471
|
WHERE AVG_LATENCY > 1000000
|
|
405
472
|
`);
|
|
406
|
-
|
|
473
|
+
|
|
407
474
|
// Execute complex analysis queries in DDL context
|
|
408
475
|
const performanceData = await forgeSQL.execute(`
|
|
409
476
|
SELECT * FROM INFORMATION_SCHEMA.CLUSTER_STATEMENTS_SUMMARY_HISTORY
|
|
410
477
|
WHERE SUMMARY_END_TIME > DATE_SUB(NOW(), INTERVAL 1 HOUR)
|
|
411
478
|
`);
|
|
412
|
-
|
|
479
|
+
|
|
413
480
|
return { slowQueries, performanceData };
|
|
414
481
|
});
|
|
415
482
|
|
|
416
483
|
// Common Table Expressions (CTEs)
|
|
417
484
|
const userStats = await forgeSQL
|
|
418
485
|
.with(
|
|
419
|
-
forgeSQL.selectFrom(users).where(eq(users.active, true)).as(
|
|
420
|
-
forgeSQL.selectFrom(orders).where(eq(orders.status,
|
|
486
|
+
forgeSQL.selectFrom(users).where(eq(users.active, true)).as("activeUsers"),
|
|
487
|
+
forgeSQL.selectFrom(orders).where(eq(orders.status, "completed")).as("completedOrders"),
|
|
421
488
|
)
|
|
422
489
|
.select({
|
|
423
490
|
totalActiveUsers: sql`COUNT(au.id)`,
|
|
424
|
-
totalCompletedOrders: sql`COUNT(co.id)
|
|
491
|
+
totalCompletedOrders: sql`COUNT(co.id)`,
|
|
425
492
|
})
|
|
426
493
|
.from(sql`activeUsers au`)
|
|
427
494
|
.leftJoin(sql`completedOrders co`, eq(sql`au.id`, sql`co.userId`));
|
|
495
|
+
|
|
496
|
+
// Rovo Integration for secure dynamic SQL queries
|
|
497
|
+
const rovo = forgeSQL.rovo();
|
|
498
|
+
const settings = await rovo
|
|
499
|
+
.rovoSettingBuilder(usersTable, accountId)
|
|
500
|
+
.addContextParameter(":currentUserId", accountId)
|
|
501
|
+
.useRLS()
|
|
502
|
+
.addRlsColumn(usersTable.id)
|
|
503
|
+
.addRlsWherePart((alias) => `${alias}.${usersTable.id.name} = '${accountId}'`)
|
|
504
|
+
.finish()
|
|
505
|
+
.build();
|
|
506
|
+
|
|
507
|
+
const rovoResult = await rovo.dynamicIsolatedQuery(
|
|
508
|
+
"SELECT id, name FROM users WHERE status = 'active' AND userId = :currentUserId",
|
|
509
|
+
settings,
|
|
510
|
+
);
|
|
428
511
|
```
|
|
429
512
|
|
|
430
513
|
This approach gives you direct access to all Drizzle ORM features while still using the @forge/sql backend with enhanced caching and versioning capabilities.
|
|
@@ -460,7 +543,7 @@ await forgeSQL.executeWithCacheContext(async () => {
|
|
|
460
543
|
await db.updateWithCacheContext(users)...;
|
|
461
544
|
await db.deleteWithCacheContext(users)...;
|
|
462
545
|
// invoke without cache
|
|
463
|
-
const users = await db.selectAliasedCacheable(getTableColumns(users)).from(users);
|
|
546
|
+
const users = await db.selectAliasedCacheable(getTableColumns(users)).from(users);
|
|
464
547
|
// Cache is cleared only once at the end for all affected tables
|
|
465
548
|
});
|
|
466
549
|
|
|
@@ -476,15 +559,15 @@ const usersCacheable = await forgeSQL.selectCacheableFrom(users)
|
|
|
476
559
|
|
|
477
560
|
// Raw SQL execution
|
|
478
561
|
const rawUsers = await forgeSQL.execute(
|
|
479
|
-
"SELECT * FROM users WHERE active = ?",
|
|
562
|
+
"SELECT * FROM users WHERE active = ?",
|
|
480
563
|
[true]
|
|
481
564
|
);
|
|
482
565
|
|
|
483
566
|
// Raw SQL with caching
|
|
484
567
|
// ⚠️ IMPORTANT: When using executeCacheable(), all table names must be wrapped with backticks (`)
|
|
485
568
|
const cachedRawUsers = await forgeSQL.executeCacheable(
|
|
486
|
-
"SELECT * FROM `users` WHERE active = ?",
|
|
487
|
-
[true],
|
|
569
|
+
"SELECT * FROM `users` WHERE active = ?",
|
|
570
|
+
[true],
|
|
488
571
|
300
|
|
489
572
|
);
|
|
490
573
|
|
|
@@ -497,14 +580,14 @@ const usersWithMetadata = await forgeSQL.executeWithMetadata(
|
|
|
497
580
|
},
|
|
498
581
|
(totalDbExecutionTime, totalResponseSize, printQueriesWithPlan) => {
|
|
499
582
|
const threshold = 500; // ms baseline for this resolver
|
|
500
|
-
|
|
583
|
+
|
|
501
584
|
if (totalDbExecutionTime > threshold * 1.5) {
|
|
502
585
|
console.warn(`[Performance Warning] Resolver exceeded DB time: ${totalDbExecutionTime} ms`);
|
|
503
586
|
await printQueriesWithPlan(); // Analyze and print query execution plans
|
|
504
587
|
} else if (totalDbExecutionTime > threshold) {
|
|
505
588
|
console.debug(`[Performance Debug] High DB time: ${totalDbExecutionTime} ms`);
|
|
506
589
|
}
|
|
507
|
-
|
|
590
|
+
|
|
508
591
|
console.log(`DB response size: ${totalResponseSize} bytes`);
|
|
509
592
|
}
|
|
510
593
|
);
|
|
@@ -519,6 +602,7 @@ The caching system is optional and only needed if you want to use cache-related
|
|
|
519
602
|
To use caching, you need to use Forge-SQL-ORM methods that support cache management:
|
|
520
603
|
|
|
521
604
|
**Methods that perform cache eviction after execution and in cache context (batch eviction):**
|
|
605
|
+
|
|
522
606
|
- `forgeSQL.insertAndEvictCache()`
|
|
523
607
|
- `forgeSQL.updateAndEvictCache()`
|
|
524
608
|
- `forgeSQL.deleteAndEvictCache()`
|
|
@@ -528,6 +612,7 @@ To use caching, you need to use Forge-SQL-ORM methods that support cache managem
|
|
|
528
612
|
- `forgeSQL.getDrizzleQueryBuilder().deleteAndEvictCache()`
|
|
529
613
|
|
|
530
614
|
**Methods that participate in cache context only (batch eviction):**
|
|
615
|
+
|
|
531
616
|
- All methods except the default Drizzle methods:
|
|
532
617
|
- `forgeSQL.insert()`
|
|
533
618
|
- `forgeSQL.update()`
|
|
@@ -538,17 +623,20 @@ To use caching, you need to use Forge-SQL-ORM methods that support cache managem
|
|
|
538
623
|
- `forgeSQL.getDrizzleQueryBuilder().deleteWithCacheContext()`
|
|
539
624
|
|
|
540
625
|
**Methods do not do evict cache, better do not use with cache feature:**
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
626
|
+
|
|
627
|
+
- `forgeSQL.getDrizzleQueryBuilder().insert()`
|
|
628
|
+
- `forgeSQL.getDrizzleQueryBuilder().update()`
|
|
629
|
+
- `forgeSQL.getDrizzleQueryBuilder().delete()`
|
|
544
630
|
|
|
545
631
|
**Cacheable methods:**
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
632
|
+
|
|
633
|
+
- `forgeSQL.selectCacheable()`
|
|
634
|
+
- `forgeSQL.selectDistinctCacheable()`
|
|
635
|
+
- `forgeSQL.getDrizzleQueryBuilder().selectAliasedCacheable()`
|
|
636
|
+
- `forgeSQL.getDrizzleQueryBuilder().selectAliasedDistinctCacheable()`
|
|
550
637
|
|
|
551
638
|
**Cache context example:**
|
|
639
|
+
|
|
552
640
|
```typescript
|
|
553
641
|
await forgeSQL.executeWithCacheContext(async () => {
|
|
554
642
|
// These methods participate in batch cache clearing
|
|
@@ -559,7 +647,6 @@ await forgeSQL.executeWithCacheContext(async () => {
|
|
|
559
647
|
});
|
|
560
648
|
```
|
|
561
649
|
|
|
562
|
-
|
|
563
650
|
The diagram below shows the lifecycle of a cacheable query in Forge-SQL-ORM:
|
|
564
651
|
|
|
565
652
|
1. Resolver calls forge-sql-orm with a SQL query and parameters.
|
|
@@ -571,7 +658,6 @@ The diagram below shows the lifecycle of a cacheable query in Forge-SQL-ORM:
|
|
|
571
658
|
|
|
572
659
|

|
|
573
660
|
|
|
574
|
-
|
|
575
661
|
The diagram below shows how Evict Cache works in Forge-SQL-ORM:
|
|
576
662
|
|
|
577
663
|
1. **Data modification** is executed through `@forge/sql` (e.g., `UPDATE users ...`).
|
|
@@ -605,13 +691,13 @@ The diagram below shows how Cache Context works:
|
|
|
605
691
|
|
|
606
692
|

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