forge-sql-orm 2.0.30 → 2.1.1
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 +1410 -81
- package/dist/ForgeSQLORM.js +1456 -60
- package/dist/ForgeSQLORM.js.map +1 -1
- package/dist/ForgeSQLORM.mjs +1440 -61
- package/dist/ForgeSQLORM.mjs.map +1 -1
- package/dist/core/ForgeSQLAnalyseOperations.d.ts +1 -1
- package/dist/core/ForgeSQLAnalyseOperations.d.ts.map +1 -1
- package/dist/core/ForgeSQLCacheOperations.d.ts +119 -0
- package/dist/core/ForgeSQLCacheOperations.d.ts.map +1 -0
- package/dist/core/ForgeSQLCrudOperations.d.ts +38 -22
- package/dist/core/ForgeSQLCrudOperations.d.ts.map +1 -1
- package/dist/core/ForgeSQLORM.d.ts +248 -13
- package/dist/core/ForgeSQLORM.d.ts.map +1 -1
- package/dist/core/ForgeSQLQueryBuilder.d.ts +394 -19
- package/dist/core/ForgeSQLQueryBuilder.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/lib/drizzle/extensions/additionalActions.d.ts +90 -0
- package/dist/lib/drizzle/extensions/additionalActions.d.ts.map +1 -0
- package/dist/utils/cacheContextUtils.d.ts +123 -0
- package/dist/utils/cacheContextUtils.d.ts.map +1 -0
- package/dist/utils/cacheUtils.d.ts +56 -0
- package/dist/utils/cacheUtils.d.ts.map +1 -0
- package/dist/utils/sqlUtils.d.ts +8 -0
- package/dist/utils/sqlUtils.d.ts.map +1 -1
- package/dist/webtriggers/clearCacheSchedulerTrigger.d.ts +46 -0
- package/dist/webtriggers/clearCacheSchedulerTrigger.d.ts.map +1 -0
- package/dist/webtriggers/index.d.ts +1 -0
- package/dist/webtriggers/index.d.ts.map +1 -1
- package/package.json +15 -12
- package/src/core/ForgeSQLAnalyseOperations.ts +1 -1
- package/src/core/ForgeSQLCacheOperations.ts +195 -0
- package/src/core/ForgeSQLCrudOperations.ts +49 -40
- package/src/core/ForgeSQLORM.ts +743 -34
- package/src/core/ForgeSQLQueryBuilder.ts +456 -20
- package/src/index.ts +1 -1
- package/src/lib/drizzle/extensions/additionalActions.ts +852 -0
- package/src/lib/drizzle/extensions/types.d.ts +99 -10
- package/src/utils/cacheContextUtils.ts +212 -0
- package/src/utils/cacheUtils.ts +403 -0
- package/src/utils/sqlUtils.ts +42 -0
- package/src/webtriggers/clearCacheSchedulerTrigger.ts +79 -0
- package/src/webtriggers/index.ts +1 -0
- package/dist/lib/drizzle/extensions/selectAliased.d.ts +0 -9
- package/dist/lib/drizzle/extensions/selectAliased.d.ts.map +0 -1
- package/src/lib/drizzle/extensions/selectAliased.ts +0 -72
package/README.md
CHANGED
|
@@ -17,8 +17,13 @@
|
|
|
17
17
|
|
|
18
18
|
## Key Features
|
|
19
19
|
- ✅ **Custom Drizzle Driver** for direct integration with @forge/sql
|
|
20
|
+
- ✅ **Local Cache System (Level 1)** for in-memory query optimization within single resolver invocation scope
|
|
21
|
+
- ✅ **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/) )
|
|
20
22
|
- ✅ **Type-Safe Query Building**: Write SQL queries with full TypeScript support
|
|
21
23
|
- ✅ **Supports complex SQL queries** with joins and filtering using Drizzle ORM
|
|
24
|
+
- ✅ **Advanced Query Methods**: `selectFrom()`, `selectDistinctFrom()`, `selectCacheableFrom()`, `selectDistinctCacheableFrom()` for all-column queries with field aliasing
|
|
25
|
+
- ✅ **Raw SQL Execution**: `execute()` and `executeCacheable()` methods for direct SQL queries with local and global caching
|
|
26
|
+
- ✅ **Common Table Expressions (CTEs)**: `with()` method for complex queries with subqueries
|
|
22
27
|
- ✅ **Schema migration support**, allowing automatic schema evolution
|
|
23
28
|
- ✅ **Automatic entity generation** from MySQL/tidb databases
|
|
24
29
|
- ✅ **Automatic migration generation** from MySQL/tidb databases
|
|
@@ -26,24 +31,129 @@
|
|
|
26
31
|
- ✅ **Schema Fetching** Development-only web trigger to retrieve current database schema and generate SQL statements for schema recreation
|
|
27
32
|
- ✅ **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
|
|
28
33
|
- ✅ **Optimistic Locking** Ensures data consistency by preventing conflicts when multiple users update the same record
|
|
29
|
-
- ✅ **Query Plan Analysis**: Detailed execution plan analysis and optimization insights
|
|
34
|
+
- ✅ **Query Plan Analysis**: Detailed execution plan analysis and optimization insights
|
|
35
|
+
|
|
36
|
+
## Table of Contents
|
|
37
|
+
|
|
38
|
+
### 🚀 Getting Started
|
|
39
|
+
- [Key Features](#key-features)
|
|
40
|
+
- [Usage Approaches](#usage-approaches)
|
|
41
|
+
- [Installation](#installation)
|
|
42
|
+
- [CLI Commands](#cli-commands) | [CLI Documentation](forge-sql-orm-cli/README.md)
|
|
43
|
+
- [Quick Start](#quick-start)
|
|
44
|
+
|
|
45
|
+
### 📖 Core Features
|
|
46
|
+
- [Field Name Collision Prevention](#field-name-collision-prevention-in-complex-queries)
|
|
47
|
+
- [Drizzle Usage with forge-sql-orm](#drizzle-usage-with-forge-sql-orm)
|
|
48
|
+
- [Direct Drizzle Usage with Custom Driver](#direct-drizzle-usage-with-custom-driver)
|
|
49
|
+
|
|
50
|
+
### 🗄️ Database Operations
|
|
51
|
+
- [Fetch Data](#fetch-data)
|
|
52
|
+
- [Modify Operations](#modify-operations)
|
|
53
|
+
- [SQL Utilities](#sql-utilities)
|
|
54
|
+
|
|
55
|
+
### ⚡ Caching System
|
|
56
|
+
- [Setting Up Caching with @forge/kvs](#setting-up-caching-with-forgekvs-optional)
|
|
57
|
+
- [Global Cache System (Level 2)](#global-cache-system-level-2)
|
|
58
|
+
- [Cache Context Operations](#cache-context-operations)
|
|
59
|
+
- [Local Cache Operations (Level 1)](#local-cache-operations-level-1)
|
|
60
|
+
- [Cache-Aware Query Operations](#cache-aware-query-operations)
|
|
61
|
+
- [Manual Cache Management](#manual-cache-management)
|
|
62
|
+
|
|
63
|
+
### 🔒 Advanced Features
|
|
64
|
+
- [Optimistic Locking](#optimistic-locking)
|
|
65
|
+
- [Query Analysis and Performance Optimization](#query-analysis-and-performance-optimization)
|
|
66
|
+
- [Date and Time Types](#date-and-time-types)
|
|
67
|
+
|
|
68
|
+
### 🛠️ Development Tools
|
|
69
|
+
- [CLI Commands](#cli-commands) | [CLI Documentation](forge-sql-orm-cli/README.md)
|
|
70
|
+
- [Web Triggers for Migrations](#web-triggers-for-migrations)
|
|
71
|
+
- [Step-by-Step Migration Workflow](#step-by-step-migration-workflow)
|
|
72
|
+
- [Drop Migrations](#drop-migrations)
|
|
73
|
+
|
|
74
|
+
### 📚 Examples
|
|
75
|
+
- [Simple Example](examples/forge-sql-orm-example-simple)
|
|
76
|
+
- [Drizzle Driver Example](examples/forge-sql-orm-example-drizzle-driver-simple)
|
|
77
|
+
- [Optimistic Locking Example](examples/forge-sql-orm-example-optimistic-locking)
|
|
78
|
+
- [Dynamic Queries Example](examples/forge-sql-orm-example-dynamic)
|
|
79
|
+
- [Query Analysis Example](examples/forge-sql-orm-example-query-analyses)
|
|
80
|
+
- [Organization Tracker Example](examples/forge-sql-orm-example-org-tracker)
|
|
81
|
+
- [Checklist Example](examples/forge-sql-orm-example-checklist)
|
|
82
|
+
|
|
83
|
+
### 📚 Reference
|
|
84
|
+
- [ForgeSqlOrmOptions](#forgesqlormoptions)
|
|
85
|
+
- [Migration Guide](#migration-guide)
|
|
86
|
+
|
|
87
|
+
## 🚀 Quick Navigation
|
|
88
|
+
|
|
89
|
+
**New to Forge-SQL-ORM?** Start here:
|
|
90
|
+
- [Quick Start](#quick-start) - Get up and running in 5 minutes
|
|
91
|
+
- [Installation](#installation) - Complete setup guide
|
|
92
|
+
- [Basic Usage Examples](#fetch-data) - Simple query examples
|
|
93
|
+
|
|
94
|
+
**Looking for specific features?**
|
|
95
|
+
- [Global Cache System (Level 2)](#global-cache-system-level-2) - Cross-invocation persistent caching
|
|
96
|
+
- [Local Cache System (Level 1)](#local-cache-operations-level-1) - In-memory invocation caching
|
|
97
|
+
- [Optimistic Locking](#optimistic-locking) - Data consistency
|
|
98
|
+
- [Migration Tools](#web-triggers-for-migrations) - Database migrations
|
|
99
|
+
- [Query Analysis](#query-analysis-and-performance-optimization) - Performance optimization
|
|
100
|
+
|
|
101
|
+
**Looking for practical examples?**
|
|
102
|
+
- [Simple Example](examples/forge-sql-orm-example-simple) - Basic ORM usage
|
|
103
|
+
- [Optimistic Locking Example](examples/forge-sql-orm-example-optimistic-locking) - Real-world conflict handling
|
|
104
|
+
- [Organization Tracker Example](examples/forge-sql-orm-example-org-tracker) - Complex relationships
|
|
105
|
+
- [Checklist Example](examples/forge-sql-orm-example-checklist) - Jira integration
|
|
30
106
|
|
|
31
107
|
## Usage Approaches
|
|
32
108
|
|
|
33
|
-
|
|
109
|
+
|
|
110
|
+
### 1. Full Forge-SQL-ORM Usage
|
|
111
|
+
```typescript
|
|
112
|
+
import ForgeSQL from "forge-sql-orm";
|
|
113
|
+
const forgeSQL = new ForgeSQL();
|
|
114
|
+
```
|
|
115
|
+
Best for: Advanced features like optimistic locking, automatic versioning, and automatic field name collision prevention in complex queries.
|
|
116
|
+
|
|
117
|
+
### 2. Direct Drizzle Usage
|
|
34
118
|
```typescript
|
|
35
119
|
import { drizzle } from "drizzle-orm/mysql-proxy";
|
|
36
120
|
import { forgeDriver } from "forge-sql-orm";
|
|
37
121
|
const db = drizzle(forgeDriver);
|
|
38
122
|
```
|
|
39
|
-
Best for: Simple
|
|
123
|
+
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.
|
|
40
124
|
|
|
41
|
-
|
|
125
|
+
|
|
126
|
+
### 3. Local Cache Optimization
|
|
42
127
|
```typescript
|
|
43
128
|
import ForgeSQL from "forge-sql-orm";
|
|
44
129
|
const forgeSQL = new ForgeSQL();
|
|
130
|
+
|
|
131
|
+
// Optimize repeated queries within a single invocation
|
|
132
|
+
await forgeSQL.executeWithLocalContext(async () => {
|
|
133
|
+
// Multiple queries here will benefit from local caching
|
|
134
|
+
const users = await forgeSQL.select({ id: users.id, name: users.name })
|
|
135
|
+
.from(users).where(eq(users.active, true));
|
|
136
|
+
|
|
137
|
+
// This query will use local cache (no database call)
|
|
138
|
+
const cachedUsers = await forgeSQL.select({ id: users.id, name: users.name })
|
|
139
|
+
.from(users).where(eq(users.active, true));
|
|
140
|
+
|
|
141
|
+
// Using new methods for better performance
|
|
142
|
+
const usersFrom = await forgeSQL.selectFrom(users)
|
|
143
|
+
.where(eq(users.active, true));
|
|
144
|
+
|
|
145
|
+
// This will use local cache (no database call)
|
|
146
|
+
const cachedUsersFrom = await forgeSQL.selectFrom(users)
|
|
147
|
+
.where(eq(users.active, true));
|
|
148
|
+
|
|
149
|
+
// Raw SQL with local caching
|
|
150
|
+
const rawUsers = await forgeSQL.execute(
|
|
151
|
+
"SELECT id, name FROM users WHERE active = ?",
|
|
152
|
+
[true]
|
|
153
|
+
);
|
|
154
|
+
});
|
|
45
155
|
```
|
|
46
|
-
Best for:
|
|
156
|
+
Best for: Performance optimization of repeated queries within resolvers or single invocation contexts.
|
|
47
157
|
|
|
48
158
|
## Field Name Collision Prevention in Complex Queries
|
|
49
159
|
|
|
@@ -91,18 +201,149 @@ Forge-SQL-ORM is designed to work with @forge/sql and requires some additional s
|
|
|
91
201
|
|
|
92
202
|
✅ Step 1: Install Dependencies
|
|
93
203
|
|
|
204
|
+
**Basic installation (without caching):**
|
|
94
205
|
```sh
|
|
95
|
-
npm install forge-sql-orm @forge/sql drizzle-orm
|
|
96
|
-
|
|
206
|
+
npm install forge-sql-orm @forge/sql drizzle-orm -S
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
**With caching support:**
|
|
210
|
+
```sh
|
|
211
|
+
npm install forge-sql-orm @forge/sql @forge/kvs drizzle-orm -S
|
|
97
212
|
```
|
|
98
213
|
|
|
99
214
|
This will:
|
|
100
215
|
- Install Forge-SQL-ORM (the ORM for @forge/sql)
|
|
101
216
|
- Install @forge/sql, the Forge database layer
|
|
217
|
+
- Install @forge/kvs, the Forge Key-Value Store for caching (optional, only needed for caching features)
|
|
102
218
|
- Install Drizzle ORM and its MySQL driver
|
|
103
219
|
- Install TypeScript types for MySQL
|
|
104
220
|
- Install forge-sql-orm-cli A command-line interface tool for managing Atlassian Forge SQL migrations and model generation with Drizzle ORM integration.
|
|
105
221
|
|
|
222
|
+
## Quick Start
|
|
223
|
+
|
|
224
|
+
### 1. Basic Setup
|
|
225
|
+
```typescript
|
|
226
|
+
import ForgeSQL from "forge-sql-orm";
|
|
227
|
+
|
|
228
|
+
// Initialize ForgeSQL
|
|
229
|
+
const forgeSQL = new ForgeSQL();
|
|
230
|
+
|
|
231
|
+
// Simple query
|
|
232
|
+
const users = await forgeSQL.select().from(users);
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### 2. With Caching (Optional)
|
|
236
|
+
```typescript
|
|
237
|
+
import ForgeSQL from "forge-sql-orm";
|
|
238
|
+
|
|
239
|
+
// Initialize with caching
|
|
240
|
+
const forgeSQL = new ForgeSQL({
|
|
241
|
+
cacheEntityName: "cache",
|
|
242
|
+
cacheTTL: 300
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// Cached query
|
|
246
|
+
const users = await forgeSQL.selectCacheable({ id: users.id, name: users.name })
|
|
247
|
+
.from(users).where(eq(users.active, true));
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### 3. Local Cache Optimization
|
|
251
|
+
```typescript
|
|
252
|
+
// Optimize repeated queries within a single invocation
|
|
253
|
+
await forgeSQL.executeWithLocalContext(async () => {
|
|
254
|
+
const users = await forgeSQL.select({ id: users.id, name: users.name })
|
|
255
|
+
.from(users).where(eq(users.active, true));
|
|
256
|
+
|
|
257
|
+
// This query will use local cache (no database call)
|
|
258
|
+
const cachedUsers = await forgeSQL.select({ id: users.id, name: users.name })
|
|
259
|
+
.from(users).where(eq(users.active, true));
|
|
260
|
+
|
|
261
|
+
// Using new methods for better performance
|
|
262
|
+
const usersFrom = await forgeSQL.selectFrom(users)
|
|
263
|
+
.where(eq(users.active, true));
|
|
264
|
+
|
|
265
|
+
// Raw SQL with local caching
|
|
266
|
+
const rawUsers = await forgeSQL.execute(
|
|
267
|
+
"SELECT id, name FROM users WHERE active = ?",
|
|
268
|
+
[true]
|
|
269
|
+
);
|
|
270
|
+
});
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### 4. Next Steps
|
|
274
|
+
- [Full Installation Guide](#installation) - Complete setup instructions
|
|
275
|
+
- [Core Features](#core-features) - Learn about key capabilities
|
|
276
|
+
- [Global Cache System (Level 2)](#global-cache-system-level-2) - Cross-invocation caching features
|
|
277
|
+
- [Local Cache System (Level 1)](#local-cache-operations-level-1) - In-memory caching features
|
|
278
|
+
- [API Reference](#reference) - Complete API documentation
|
|
279
|
+
|
|
280
|
+
## Drizzle Usage with forge-sql-orm
|
|
281
|
+
|
|
282
|
+
If you prefer to use Drizzle ORM with the additional features of Forge-SQL-ORM (like optimistic locking and caching), you can use the enhanced API:
|
|
283
|
+
|
|
284
|
+
```typescript
|
|
285
|
+
import ForgeSQL from "forge-sql-orm";
|
|
286
|
+
const forgeSQL = new ForgeSQL();
|
|
287
|
+
|
|
288
|
+
// Versioned operations with cache management (recommended)
|
|
289
|
+
await forgeSQL.modifyWithVersioningAndEvictCache().insert(Users, [userData]);
|
|
290
|
+
await forgeSQL.modifyWithVersioningAndEvictCache().updateById(updateData, Users);
|
|
291
|
+
|
|
292
|
+
// Versioned operations without cache management
|
|
293
|
+
await forgeSQL.modifyWithVersioning().insert(Users, [userData]);
|
|
294
|
+
await forgeSQL.modifyWithVersioning().updateById(updateData, Users);
|
|
295
|
+
|
|
296
|
+
// Non-versioned operations with cache management
|
|
297
|
+
await forgeSQL.insertAndEvictCache(Users).values(userData);
|
|
298
|
+
await forgeSQL.updateAndEvictCache(Users).set(updateData).where(eq(Users.id, 1));
|
|
299
|
+
|
|
300
|
+
// Basic Drizzle operations (cache context aware)
|
|
301
|
+
await forgeSQL.insert(Users).values(userData);
|
|
302
|
+
await forgeSQL.update(Users).set(updateData).where(eq(Users.id, 1));
|
|
303
|
+
|
|
304
|
+
// Direct Drizzle access
|
|
305
|
+
const db = forgeSQL.getDrizzleQueryBuilder();
|
|
306
|
+
const users = await db.select().from(users);
|
|
307
|
+
|
|
308
|
+
// Using new methods for enhanced functionality
|
|
309
|
+
const usersFrom = await forgeSQL.selectFrom(users)
|
|
310
|
+
.where(eq(users.active, true));
|
|
311
|
+
|
|
312
|
+
const usersDistinct = await forgeSQL.selectDistinctFrom(users)
|
|
313
|
+
.where(eq(users.active, true));
|
|
314
|
+
|
|
315
|
+
const usersCacheable = await forgeSQL.selectCacheableFrom(users)
|
|
316
|
+
.where(eq(users.active, true));
|
|
317
|
+
|
|
318
|
+
// Raw SQL execution
|
|
319
|
+
const rawUsers = await forgeSQL.execute(
|
|
320
|
+
"SELECT * FROM users WHERE active = ?",
|
|
321
|
+
[true]
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
// Raw SQL with caching
|
|
325
|
+
const cachedRawUsers = await forgeSQL.executeCacheable(
|
|
326
|
+
"SELECT * FROM users WHERE active = ?",
|
|
327
|
+
[true],
|
|
328
|
+
300
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
// Common Table Expressions (CTEs)
|
|
332
|
+
const userStats = await forgeSQL
|
|
333
|
+
.with(
|
|
334
|
+
forgeSQL.selectFrom(users).where(eq(users.active, true)).as('activeUsers'),
|
|
335
|
+
forgeSQL.selectFrom(orders).where(eq(orders.status, 'completed')).as('completedOrders')
|
|
336
|
+
)
|
|
337
|
+
.select({
|
|
338
|
+
totalActiveUsers: sql`COUNT(au.id)`,
|
|
339
|
+
totalCompletedOrders: sql`COUNT(co.id)`
|
|
340
|
+
})
|
|
341
|
+
.from(sql`activeUsers au`)
|
|
342
|
+
.leftJoin(sql`completedOrders co`, eq(sql`au.id`, sql`co.userId`));
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
This approach gives you direct access to all Drizzle ORM features while still using the @forge/sql backend with enhanced caching and versioning capabilities.
|
|
346
|
+
|
|
106
347
|
## Direct Drizzle Usage with Custom Driver
|
|
107
348
|
|
|
108
349
|
If you prefer to use Drizzle ORM directly without the additional features of Forge-SQL-ORM (like optimistic locking), you can use the custom driver:
|
|
@@ -116,24 +357,424 @@ const db = patchDbWithSelectAliased(drizzle(forgeDriver));
|
|
|
116
357
|
|
|
117
358
|
// Use drizzle directly
|
|
118
359
|
const users = await db.select().from(users);
|
|
360
|
+
const users = await db.selectAliased(getTableColumns(users)).from(users);
|
|
361
|
+
const users = await db.selectAliasedDistinct(getTableColumns(users)).from(users);
|
|
362
|
+
await db.insert(users)...;
|
|
363
|
+
await db.update(users)...;
|
|
364
|
+
await db.delete(users)...;
|
|
365
|
+
// Use drizzle with kvs cache
|
|
366
|
+
const users = await db.selectAliasedCacheable(getTableColumns(users)).from(users);
|
|
367
|
+
const users = await db.selectAliasedDistinctCacheable(getTableColumns(users)).from(users);
|
|
368
|
+
await db.insertAndEvictCache(users)...;
|
|
369
|
+
await db.updateAndEvictCache(users)...;
|
|
370
|
+
await db.deleteAndEvictCache(users)...;
|
|
371
|
+
|
|
372
|
+
// Use drizzle with kvs cache context
|
|
373
|
+
await forgeSQL.executeWithCacheContext(async () => {
|
|
374
|
+
await db.insertWithCacheContext(users)...;
|
|
375
|
+
await db.updateWithCacheContext(users)...;
|
|
376
|
+
await db.deleteWithCacheContext(users)...;
|
|
377
|
+
// invoke without cache
|
|
378
|
+
const users = await db.selectAliasedCacheable(getTableColumns(users)).from(users);
|
|
379
|
+
// Cache is cleared only once at the end for all affected tables
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
// Using new methods with direct drizzle
|
|
383
|
+
const usersFrom = await forgeSQL.selectFrom(users)
|
|
384
|
+
.where(eq(users.active, true));
|
|
385
|
+
|
|
386
|
+
const usersDistinct = await forgeSQL.selectDistinctFrom(users)
|
|
387
|
+
.where(eq(users.active, true));
|
|
388
|
+
|
|
389
|
+
const usersCacheable = await forgeSQL.selectCacheableFrom(users)
|
|
390
|
+
.where(eq(users.active, true));
|
|
391
|
+
|
|
392
|
+
// Raw SQL execution
|
|
393
|
+
const rawUsers = await forgeSQL.execute(
|
|
394
|
+
"SELECT * FROM users WHERE active = ?",
|
|
395
|
+
[true]
|
|
396
|
+
);
|
|
397
|
+
|
|
398
|
+
// Raw SQL with caching
|
|
399
|
+
const cachedRawUsers = await forgeSQL.executeCacheable(
|
|
400
|
+
"SELECT * FROM users WHERE active = ?",
|
|
401
|
+
[true],
|
|
402
|
+
300
|
|
403
|
+
);
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
## Setting Up Caching with @forge/kvs (Optional)
|
|
407
|
+
|
|
408
|
+
The caching system is optional and only needed if you want to use cache-related features. To enable the caching system, you need to install the required dependency and configure your manifest.
|
|
409
|
+
|
|
410
|
+
### How Caching Works
|
|
411
|
+
|
|
412
|
+
To use caching, you need to use Forge-SQL-ORM methods that support cache management:
|
|
413
|
+
|
|
414
|
+
**Methods that perform cache eviction after execution and in cache context (batch eviction):**
|
|
415
|
+
- `forgeSQL.insertAndEvictCache()`
|
|
416
|
+
- `forgeSQL.updateAndEvictCache()`
|
|
417
|
+
- `forgeSQL.deleteAndEvictCache()`
|
|
418
|
+
- `forgeSQL.modifyWithVersioningAndEvictCache()`
|
|
419
|
+
- `forgeSQL.getDrizzleQueryBuilder().insertAndEvictCache()`
|
|
420
|
+
- `forgeSQL.getDrizzleQueryBuilder().updateAndEvictCache()`
|
|
421
|
+
- `forgeSQL.getDrizzleQueryBuilder().deleteAndEvictCache()`
|
|
422
|
+
|
|
423
|
+
**Methods that participate in cache context only (batch eviction):**
|
|
424
|
+
- All methods except the default Drizzle methods:
|
|
425
|
+
- `forgeSQL.insert()`
|
|
426
|
+
- `forgeSQL.update()`
|
|
427
|
+
- `forgeSQL.delete()`
|
|
428
|
+
- `forgeSQL.modifyWithVersioning()`
|
|
429
|
+
- `forgeSQL.getDrizzleQueryBuilder().insertWithCacheContext()`
|
|
430
|
+
- `forgeSQL.getDrizzleQueryBuilder().updateWithCacheContext()`
|
|
431
|
+
- `forgeSQL.getDrizzleQueryBuilder().deleteWithCacheContext()`
|
|
432
|
+
|
|
433
|
+
**Methods do not do evict cache, better do not use with cache feature:**
|
|
434
|
+
- `forgeSQL.getDrizzleQueryBuilder().insert()`
|
|
435
|
+
- `forgeSQL.getDrizzleQueryBuilder().update()`
|
|
436
|
+
- `forgeSQL.getDrizzleQueryBuilder().delete()`
|
|
437
|
+
|
|
438
|
+
**Cacheable methods:**
|
|
439
|
+
- `forgeSQL.selectCacheable()`
|
|
440
|
+
- `forgeSQL.selectDistinctCacheable()`
|
|
441
|
+
- `forgeSQL.getDrizzleQueryBuilder().selectAliasedCacheable()`
|
|
442
|
+
- `forgeSQL.getDrizzleQueryBuilder().selectAliasedDistinctCacheable()`
|
|
443
|
+
|
|
444
|
+
**Cache context example:**
|
|
445
|
+
```typescript
|
|
446
|
+
await forgeSQL.executeWithCacheContext(async () => {
|
|
447
|
+
// These methods participate in batch cache clearing
|
|
448
|
+
await forgeSQL.insert(Users).values(userData);
|
|
449
|
+
await forgeSQL.update(Users).set(updateData).where(eq(Users.id, 1));
|
|
450
|
+
await forgeSQL.delete(Users).where(eq(Users.id, 1));
|
|
451
|
+
// Cache is cleared only once at the end for all affected tables
|
|
452
|
+
});
|
|
119
453
|
```
|
|
120
454
|
|
|
121
|
-
## Drizzle Usage with forge-sql-orm
|
|
122
455
|
|
|
123
|
-
|
|
456
|
+
The diagram below shows the lifecycle of a cacheable query in Forge-SQL-ORM:
|
|
457
|
+
|
|
458
|
+
1. Resolver calls forge-sql-orm with a SQL query and parameters.
|
|
459
|
+
2. forge-sql-orm generates a cache key = hash(sql, parameters).
|
|
460
|
+
3. It asks @forge/kvs for an existing cached result.
|
|
461
|
+
- Cache hit → result is returned immediately.
|
|
462
|
+
- Cache miss / expired → query is executed against @forge/sql.
|
|
463
|
+
4. Fresh result is stored in @forge/kvs with TTL and returned to the caller.
|
|
464
|
+
|
|
465
|
+

|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
The diagram below shows how Evict Cache works in Forge-SQL-ORM:
|
|
469
|
+
|
|
470
|
+
1. **Data modification** is executed through `@forge/sql` (e.g., `UPDATE users ...`).
|
|
471
|
+
2. After a successful update, **forge-sql-orm** queries the `cache` entity by using the **`sql` field** with `filter.contains("users")` to find affected cached queries.
|
|
472
|
+
3. The returned cache entries are deleted in **batches** (up to 25 per transaction).
|
|
473
|
+
4. Once eviction is complete, the update result is returned to the resolver.
|
|
474
|
+
5. **Note:** Expired entries are not processed here — they are cleaned up separately by the scheduled cache cleanup trigger using the `expiration` index.
|
|
475
|
+
|
|
476
|
+

|
|
124
477
|
|
|
478
|
+
The diagram below shows how Scheduled Expiration Cleanup works:
|
|
479
|
+
|
|
480
|
+
1. A periodic scheduler (Forge trigger) runs cache cleanup independently of data modifications.
|
|
481
|
+
2. forge-sql-orm queries the cache entity by the expiration index to find entries with expiration < now.
|
|
482
|
+
3. Entries are deleted in batches (up to 25 per transaction) until the page is empty; pagination is done with a cursor (e.g., 100 per page).
|
|
483
|
+
4. This keeps the cache footprint small and prevents stale data accumulation.
|
|
484
|
+
|
|
485
|
+

|
|
486
|
+
|
|
487
|
+
The diagram below shows how Cache Context works:
|
|
488
|
+
|
|
489
|
+
`executeWithCacheContext(fn)` lets you group multiple data modifications and perform **one consolidated cache eviction** at the end:
|
|
490
|
+
|
|
491
|
+
1. The context starts with an empty `affectedTables` set.
|
|
492
|
+
2. Each successful `INSERT/UPDATE/DELETE` inside the context registers its table name in `affectedTables`.
|
|
493
|
+
3. **Reads inside the same context** that target tables present in `affectedTables` will **bypass the cache** (read-through to SQL) to avoid serving stale data. These reads also **do not write** back to cache until eviction completes.
|
|
494
|
+
4. On context completion, `affectedTables` is de-duplicated and used to build **one combined KVS query** over the `sql` field with
|
|
495
|
+
`filter.or(filter.contains("<t1>"), filter.contains("<t2>"), ...)`, returning all impacted cache entries in a single scan (paged by cursor, e.g., 100/page).
|
|
496
|
+
5. Matching cache entries are deleted in **batches** (≤25 per transaction) until the page is exhausted; then the next page is fetched via the cursor.
|
|
497
|
+
6. Expiration is handled separately by the scheduled cleanup and is **not part of** the context flow.
|
|
498
|
+
|
|
499
|
+

|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
### Important Considerations
|
|
503
|
+
|
|
504
|
+
**@forge/kvs Limits:**
|
|
505
|
+
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.
|
|
506
|
+
|
|
507
|
+
**Caching Guidelines:**
|
|
508
|
+
- Don't cache everything - be selective about what to cache
|
|
509
|
+
- Don't cache simple and fast queries - sometimes direct query is faster than cache
|
|
510
|
+
- Consider data size and frequency of changes
|
|
511
|
+
- Monitor cache usage to stay within quotas
|
|
512
|
+
- Use appropriate TTL values
|
|
513
|
+
|
|
514
|
+
### Step 1: Install Dependencies
|
|
515
|
+
|
|
516
|
+
```bash
|
|
517
|
+
npm install @forge/kvs -S
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
### Step 2: Configure Manifest
|
|
521
|
+
|
|
522
|
+
Add the storage entity configuration and scheduler trigger to your `manifest.yml`:
|
|
523
|
+
|
|
524
|
+
```yaml
|
|
525
|
+
modules:
|
|
526
|
+
scheduledTrigger:
|
|
527
|
+
- key: clear-cache-trigger
|
|
528
|
+
function: clearCache
|
|
529
|
+
interval: fiveMinute
|
|
530
|
+
storage:
|
|
531
|
+
entities:
|
|
532
|
+
- name: cache
|
|
533
|
+
attributes:
|
|
534
|
+
sql:
|
|
535
|
+
type: string
|
|
536
|
+
expiration:
|
|
537
|
+
type: integer
|
|
538
|
+
data:
|
|
539
|
+
type: string
|
|
540
|
+
indexes:
|
|
541
|
+
- sql
|
|
542
|
+
- expiration
|
|
543
|
+
sql:
|
|
544
|
+
- key: main
|
|
545
|
+
engine: mysql
|
|
546
|
+
function:
|
|
547
|
+
- key: clearCache
|
|
548
|
+
handler: index.clearCache
|
|
549
|
+
```
|
|
550
|
+
```typescript
|
|
551
|
+
// Example usage in your Forge app
|
|
552
|
+
import { clearCacheSchedulerTrigger } from "forge-sql-orm";
|
|
553
|
+
|
|
554
|
+
export const clearCache = () => {
|
|
555
|
+
return clearCacheSchedulerTrigger({
|
|
556
|
+
cacheEntityName: "cache",
|
|
557
|
+
});
|
|
558
|
+
};
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
### Step 3: Configure ORM Options
|
|
563
|
+
|
|
564
|
+
Set the cache entity name in your ForgeSQL configuration:
|
|
565
|
+
|
|
566
|
+
```typescript
|
|
567
|
+
const options = {
|
|
568
|
+
cacheEntityName: "cache", // Must match the entity name in manifest.yml
|
|
569
|
+
cacheTTL: 300, // Default cache TTL in seconds (5 minutes)
|
|
570
|
+
cacheWrapTable: true, // Wrap table names with backticks in cache keys
|
|
571
|
+
// ... other options
|
|
572
|
+
};
|
|
573
|
+
|
|
574
|
+
const forgeSQL = new ForgeSQL(options);
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
**Important Notes:**
|
|
578
|
+
- The `cacheEntityName` must exactly match the `name` in your manifest storage entities
|
|
579
|
+
- The entity attributes (`sql`, `expiration`, `data`) are required for proper cache functionality
|
|
580
|
+
- Indexes on `sql` and `expiration` improve cache lookup performance
|
|
581
|
+
- Cache data is automatically cleaned up based on TTL settings
|
|
582
|
+
- No additional permissions are required beyond standard Forge app permissions
|
|
583
|
+
|
|
584
|
+
### Complete Setup Examples
|
|
585
|
+
|
|
586
|
+
**Basic setup (without caching):**
|
|
587
|
+
|
|
588
|
+
**package.json:**
|
|
589
|
+
```shell
|
|
590
|
+
npm install forge-sql-orm @forge/sql drizzle-orm -S
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
**manifest.yml:**
|
|
594
|
+
```yaml
|
|
595
|
+
modules:
|
|
596
|
+
sql:
|
|
597
|
+
- key: main
|
|
598
|
+
engine: mysql
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
**index.ts:**
|
|
125
602
|
```typescript
|
|
126
603
|
import ForgeSQL from "forge-sql-orm";
|
|
604
|
+
|
|
127
605
|
const forgeSQL = new ForgeSQL();
|
|
128
|
-
forgeSQL.crud().insert(...);
|
|
129
|
-
forgeSQL.crud().updateById(...);
|
|
130
|
-
const db = forgeSQL.getDrizzleQueryBuilder();
|
|
131
606
|
|
|
132
|
-
//
|
|
133
|
-
|
|
607
|
+
// simple insert
|
|
608
|
+
await forgeSQL.insert(Users, [userData]);
|
|
609
|
+
// Use versioned operations without caching
|
|
610
|
+
await forgeSQL.modifyWithVersioning().insert(Users, [userData]);
|
|
611
|
+
const users = await forgeSQL.select({id: Users.id});
|
|
612
|
+
|
|
613
|
+
|
|
134
614
|
```
|
|
135
615
|
|
|
136
|
-
|
|
616
|
+
**With caching support:**
|
|
617
|
+
```shell
|
|
618
|
+
npm install forge-sql-orm @forge/sql @forge/kvs drizzle-orm -S
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
**manifest.yml:**
|
|
622
|
+
```yaml
|
|
623
|
+
modules:
|
|
624
|
+
scheduledTrigger:
|
|
625
|
+
- key: clear-cache-trigger
|
|
626
|
+
function: clearCache
|
|
627
|
+
interval: fiveMinute
|
|
628
|
+
storage:
|
|
629
|
+
entities:
|
|
630
|
+
- name: cache
|
|
631
|
+
attributes:
|
|
632
|
+
sql:
|
|
633
|
+
type: string
|
|
634
|
+
expiration:
|
|
635
|
+
type: integer
|
|
636
|
+
data:
|
|
637
|
+
type: string
|
|
638
|
+
indexes:
|
|
639
|
+
- sql
|
|
640
|
+
- expiration
|
|
641
|
+
sql:
|
|
642
|
+
- key: main
|
|
643
|
+
engine: mysql
|
|
644
|
+
function:
|
|
645
|
+
- key: clearCache
|
|
646
|
+
handler: index.clearCache
|
|
647
|
+
```
|
|
648
|
+
|
|
649
|
+
**index.ts:**
|
|
650
|
+
|
|
651
|
+
```typescript
|
|
652
|
+
import ForgeSQL from "forge-sql-orm";
|
|
653
|
+
|
|
654
|
+
const forgeSQL = new ForgeSQL({
|
|
655
|
+
cacheEntityName: "cache"
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
import {clearCacheSchedulerTrigger} from "forge-sql-orm";
|
|
659
|
+
import {getTableColumns} from "drizzle-orm";
|
|
660
|
+
|
|
661
|
+
export const clearCache = () => {
|
|
662
|
+
return clearCacheSchedulerTrigger({
|
|
663
|
+
cacheEntityName: "cache",
|
|
664
|
+
});
|
|
665
|
+
};
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
// Now you can use caching features
|
|
669
|
+
const usersData = await forgeSQL.selectCacheable(getTableColumns(users)).from(users).where(eq(users.active, true))
|
|
670
|
+
|
|
671
|
+
// simple insert
|
|
672
|
+
await forgeSQL.insertAndEvictCache(users, [userData]);
|
|
673
|
+
// Use versioned operations with caching
|
|
674
|
+
await forgeSQL.modifyWithVersioningAndEvictCache().insert(users, [userData]);
|
|
675
|
+
|
|
676
|
+
// use Cache Context
|
|
677
|
+
const data = await forgeSQL.executeWithCacheContextAndReturnValue(async () => {
|
|
678
|
+
// after insert mark users to evict
|
|
679
|
+
await forgeSQL.insert(users, [userData]);
|
|
680
|
+
// after insertAndEvictCache mark orders to evict
|
|
681
|
+
await forgeSQL.insertAndEvictCache(orders, [order1, order2]);
|
|
682
|
+
// execute query and put result to local cache
|
|
683
|
+
await forgeSQL.selectCacheable({userId: users.id, userName: users.name, orderId: orders.id, orderName: orders.name})
|
|
684
|
+
.from(users)
|
|
685
|
+
.innerJoin(orders, eq(orders.userId, users.id)).where(eq(users.active, true))
|
|
686
|
+
// use local cache without @forge/kvs and @forge/sql
|
|
687
|
+
return await forgeSQL.selectCacheable({userId: users.id, userName: users.name, orderId: orders.id, orderName: orders.name})
|
|
688
|
+
.from(users)
|
|
689
|
+
.innerJoin(orders, eq(orders.userId, users.id)).where(eq(users.active, true))
|
|
690
|
+
})
|
|
691
|
+
// execute query and put result to kvs cache
|
|
692
|
+
await forgeSQL.selectCacheable({userId: users.id, userName: users.name, orderId: orders.id, orderName: orders.name})
|
|
693
|
+
.from(users)
|
|
694
|
+
.innerJoin(orders, eq(orders.userId, users.id)).where(eq(users.active, true))
|
|
695
|
+
|
|
696
|
+
// get result from @foge/kvs cache without real @forge/sql call
|
|
697
|
+
await forgeSQL.selectCacheable({userId: users.id, userName: users.name, orderId: orders.id, orderName: orders.name})
|
|
698
|
+
.from(users)
|
|
699
|
+
.innerJoin(orders, eq(orders.userId, users.id)).where(eq(users.active, true))
|
|
700
|
+
|
|
701
|
+
// use Local Cache for performance optimization
|
|
702
|
+
const optimizedData = await forgeSQL.executeWithLocalCacheContextAndReturnValue(async () => {
|
|
703
|
+
// First query - hits database and caches result
|
|
704
|
+
const users = await forgeSQL.select({id: users.id, name: users.name})
|
|
705
|
+
.from(users).where(eq(users.active, true));
|
|
706
|
+
|
|
707
|
+
// Second query - uses local cache (no database call)
|
|
708
|
+
const cachedUsers = await forgeSQL.select({id: users.id, name: users.name})
|
|
709
|
+
.from(users).where(eq(users.active, true));
|
|
710
|
+
|
|
711
|
+
// Using new methods for better performance
|
|
712
|
+
const usersFrom = await forgeSQL.selectFrom(users)
|
|
713
|
+
.where(eq(users.active, true));
|
|
714
|
+
|
|
715
|
+
// This will use local cache (no database call)
|
|
716
|
+
const cachedUsersFrom = await forgeSQL.selectFrom(users)
|
|
717
|
+
.where(eq(users.active, true));
|
|
718
|
+
|
|
719
|
+
// Raw SQL with local caching
|
|
720
|
+
const rawUsers = await forgeSQL.execute(
|
|
721
|
+
"SELECT id, name FROM users WHERE active = ?",
|
|
722
|
+
[true]
|
|
723
|
+
);
|
|
724
|
+
|
|
725
|
+
// Insert operation - evicts local cache
|
|
726
|
+
await forgeSQL.insert(users).values({name: 'New User', active: true});
|
|
727
|
+
|
|
728
|
+
// Third query - hits database again and caches new result
|
|
729
|
+
const updatedUsers = await forgeSQL.select({id: users.id, name: users.name})
|
|
730
|
+
.from(users).where(eq(users.active, true));
|
|
731
|
+
|
|
732
|
+
return { users, cachedUsers, updatedUsers, usersFrom, cachedUsersFrom, rawUsers };
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
```
|
|
736
|
+
|
|
737
|
+
## Choosing the Right Method - ForgeSQL ORM
|
|
738
|
+
|
|
739
|
+
### When to Use Each Approach
|
|
740
|
+
|
|
741
|
+
| Method | Use Case | Versioning | Cache Management |
|
|
742
|
+
|--------|----------|------------|------------------|
|
|
743
|
+
| `modifyWithVersioningAndEvictCache()` | High-concurrency scenarios with Cache support| ✅ Yes | ✅ Yes |
|
|
744
|
+
| `modifyWithVersioning()` | High-concurrency scenarios | ✅ Yes | Cache Context |
|
|
745
|
+
| `insertAndEvictCache()` | Simple inserts | ❌ No | ✅ Yes |
|
|
746
|
+
| `updateAndEvictCache()` | Simple updates | ❌ No | ✅ Yes |
|
|
747
|
+
| `deleteAndEvictCache()` | Simple deletes | ❌ No | ✅ Yes |
|
|
748
|
+
| `insert/update/delete` | Basic Drizzle operations | ❌ No | Cache Context |
|
|
749
|
+
| `selectFrom()` | All-column queries with field aliasing | ❌ No | Local Cache |
|
|
750
|
+
| `selectDistinctFrom()` | Distinct all-column queries with field aliasing | ❌ No | Local Cache |
|
|
751
|
+
| `selectCacheableFrom()` | All-column queries with field aliasing and caching | ❌ No | Local + Global Cache |
|
|
752
|
+
| `selectDistinctCacheableFrom()` | Distinct all-column queries with field aliasing and caching | ❌ No | Local + Global Cache |
|
|
753
|
+
| `execute()` | Raw SQL queries with local caching | ❌ No | Local Cache |
|
|
754
|
+
| `executeCacheable()` | Raw SQL queries with local and global caching | ❌ No | Local + Global Cache |
|
|
755
|
+
| `with()` | Common Table Expressions (CTEs) | ❌ No | Local Cache |
|
|
756
|
+
|
|
757
|
+
|
|
758
|
+
## Choosing the Right Method - Direct Drizzle
|
|
759
|
+
|
|
760
|
+
### When to Use Each Approach
|
|
761
|
+
|
|
762
|
+
| Method | Use Case | Versioning | Cache Management |
|
|
763
|
+
|--------|----------|------------|------------------|
|
|
764
|
+
| `insertWithCacheContext/insertWithCacheContext/updateWithCacheContext` | Basic Drizzle operations | ❌ No | Cache Context |
|
|
765
|
+
| `insertAndEvictCache()` | Simple inserts without conflicts | ❌ No | ✅ Yes |
|
|
766
|
+
| `updateAndEvictCache()` | Simple updates without conflicts | ❌ No | ✅ Yes |
|
|
767
|
+
| `deleteAndEvictCache()` | Simple deletes without conflicts | ❌ No | ✅ Yes |
|
|
768
|
+
| `insert/update/delete` | Basic Drizzle operations | ❌ No | ❌ No |
|
|
769
|
+
| `selectFrom()` | All-column queries with field aliasing | ❌ No | Local Cache |
|
|
770
|
+
| `selectDistinctFrom()` | Distinct all-column queries with field aliasing | ❌ No | Local Cache |
|
|
771
|
+
| `selectCacheableFrom()` | All-column queries with field aliasing and caching | ❌ No | Local + Global Cache |
|
|
772
|
+
| `selectDistinctCacheableFrom()` | Distinct all-column queries with field aliasing and caching | ❌ No | Local + Global Cache |
|
|
773
|
+
| `execute()` | Raw SQL queries with local caching | ❌ No | Local Cache |
|
|
774
|
+
| `executeCacheable()` | Raw SQL queries with local and global caching | ❌ No | Local + Global Cache |
|
|
775
|
+
| `with()` | Common Table Expressions (CTEs) | ❌ No | Local Cache |
|
|
776
|
+
where Cache context - allows you to batch cache invalidation events and bypass cache reads for affected tables.
|
|
777
|
+
|
|
137
778
|
|
|
138
779
|
## Step-by-Step Migration Workflow
|
|
139
780
|
|
|
@@ -344,6 +985,49 @@ const users = await db.select().from(users);
|
|
|
344
985
|
### Basic Fetch Operations
|
|
345
986
|
|
|
346
987
|
```js
|
|
988
|
+
// Using forgeSQL.select()
|
|
989
|
+
const user = await forgeSQL
|
|
990
|
+
.select({user: users})
|
|
991
|
+
.from(users);
|
|
992
|
+
|
|
993
|
+
// Using forgeSQL.selectDistinct()
|
|
994
|
+
const user = await forgeSQL
|
|
995
|
+
.selectDistinct({user: users})
|
|
996
|
+
.from(users);
|
|
997
|
+
|
|
998
|
+
// Using forgeSQL.selectCacheable()
|
|
999
|
+
const user = await forgeSQL
|
|
1000
|
+
.selectCacheable({user: users})
|
|
1001
|
+
.from(users);
|
|
1002
|
+
|
|
1003
|
+
// Using forgeSQL.selectFrom() - Select all columns with field aliasing
|
|
1004
|
+
const user = await forgeSQL
|
|
1005
|
+
.selectFrom(users)
|
|
1006
|
+
.where(eq(users.id, 1));
|
|
1007
|
+
|
|
1008
|
+
// Using forgeSQL.selectDistinctFrom() - Select distinct all columns with field aliasing
|
|
1009
|
+
const user = await forgeSQL
|
|
1010
|
+
.selectDistinctFrom(users)
|
|
1011
|
+
.where(eq(users.id, 1));
|
|
1012
|
+
|
|
1013
|
+
// Using forgeSQL.selectCacheableFrom() - Select all columns with field aliasing and caching
|
|
1014
|
+
const user = await forgeSQL
|
|
1015
|
+
.selectCacheableFrom(users)
|
|
1016
|
+
.where(eq(users.id, 1));
|
|
1017
|
+
|
|
1018
|
+
// Using forgeSQL.selectDistinctCacheableFrom() - Select distinct all columns with field aliasing and caching
|
|
1019
|
+
const user = await forgeSQL
|
|
1020
|
+
.selectDistinctCacheableFrom(users)
|
|
1021
|
+
.where(eq(users.id, 1));
|
|
1022
|
+
|
|
1023
|
+
// Using forgeSQL.execute() - Execute raw SQL with local caching
|
|
1024
|
+
const user = await forgeSQL
|
|
1025
|
+
.execute("SELECT * FROM users WHERE id = ?", [1]);
|
|
1026
|
+
|
|
1027
|
+
// Using forgeSQL.executeCacheable() - Execute raw SQL with local and global caching
|
|
1028
|
+
const user = await forgeSQL
|
|
1029
|
+
.executeCacheable("SELECT * FROM users WHERE id = ?", [1], 300);
|
|
1030
|
+
|
|
347
1031
|
// Using forgeSQL.getDrizzleQueryBuilder()
|
|
348
1032
|
const user = await forgeSQL
|
|
349
1033
|
.getDrizzleQueryBuilder()
|
|
@@ -399,6 +1083,31 @@ const orderWithUser = await forgeSQL
|
|
|
399
1083
|
.from(orders)
|
|
400
1084
|
.innerJoin(users, eq(orders.userId, users.id));
|
|
401
1085
|
|
|
1086
|
+
// Using new selectFrom methods with joins
|
|
1087
|
+
const orderWithUser = await forgeSQL
|
|
1088
|
+
.selectFrom(orders)
|
|
1089
|
+
.innerJoin(users, eq(orders.userId, users.id))
|
|
1090
|
+
.where(eq(orders.id, 1));
|
|
1091
|
+
|
|
1092
|
+
// Using selectCacheableFrom with joins and caching
|
|
1093
|
+
const orderWithUser = await forgeSQL
|
|
1094
|
+
.selectCacheableFrom(orders)
|
|
1095
|
+
.innerJoin(users, eq(orders.userId, users.id))
|
|
1096
|
+
.where(eq(orders.id, 1));
|
|
1097
|
+
|
|
1098
|
+
// Using with() for Common Table Expressions (CTEs)
|
|
1099
|
+
const userStats = await forgeSQL
|
|
1100
|
+
.with(
|
|
1101
|
+
forgeSQL.selectFrom(users).where(eq(users.active, true)).as('activeUsers'),
|
|
1102
|
+
forgeSQL.selectFrom(orders).where(eq(orders.status, 'completed')).as('completedOrders')
|
|
1103
|
+
)
|
|
1104
|
+
.select({
|
|
1105
|
+
totalActiveUsers: sql`COUNT(au.id)`,
|
|
1106
|
+
totalCompletedOrders: sql`COUNT(co.id)`
|
|
1107
|
+
})
|
|
1108
|
+
.from(sql`activeUsers au`)
|
|
1109
|
+
.leftJoin(sql`completedOrders co`, eq(sql`au.id`, sql`co.userId`));
|
|
1110
|
+
|
|
402
1111
|
// OR with direct drizzle
|
|
403
1112
|
const db = patchDbWithSelectAliased(drizzle(forgeDriver));
|
|
404
1113
|
const orderWithUser = await db
|
|
@@ -440,32 +1149,125 @@ const userStats = await forgeSQL
|
|
|
440
1149
|
const users = await forgeSQL
|
|
441
1150
|
.fetch()
|
|
442
1151
|
.executeRawSQL<Users>("SELECT * FROM users");
|
|
1152
|
+
|
|
1153
|
+
// Using execute() for raw SQL with local caching
|
|
1154
|
+
const users = await forgeSQL
|
|
1155
|
+
.execute("SELECT * FROM users WHERE active = ?", [true]);
|
|
1156
|
+
|
|
1157
|
+
// Using executeCacheable() for raw SQL with local and global caching
|
|
1158
|
+
const users = await forgeSQL
|
|
1159
|
+
.executeCacheable("SELECT * FROM users WHERE active = ?", [true], 300);
|
|
1160
|
+
|
|
1161
|
+
// Using execute() with complex queries
|
|
1162
|
+
const userStats = await forgeSQL
|
|
1163
|
+
.execute(`
|
|
1164
|
+
SELECT
|
|
1165
|
+
u.id,
|
|
1166
|
+
u.name,
|
|
1167
|
+
COUNT(o.id) as order_count,
|
|
1168
|
+
SUM(o.amount) as total_amount
|
|
1169
|
+
FROM users u
|
|
1170
|
+
LEFT JOIN orders o ON u.id = o.user_id
|
|
1171
|
+
WHERE u.active = ?
|
|
1172
|
+
GROUP BY u.id, u.name
|
|
1173
|
+
`, [true]);
|
|
443
1174
|
```
|
|
444
1175
|
|
|
445
|
-
##
|
|
1176
|
+
## Modify Operations
|
|
446
1177
|
|
|
447
|
-
|
|
1178
|
+
Forge-SQL-ORM provides multiple approaches for Modify operations, each with different characteristics:
|
|
448
1179
|
|
|
449
|
-
|
|
450
|
-
// Single insert
|
|
451
|
-
const userId = await forgeSQL.crud().insert(Users, [{ id: 1, name: "Smith" }]);
|
|
1180
|
+
### 1. Basic Drizzle Operations (Cache Context Aware)
|
|
452
1181
|
|
|
453
|
-
|
|
454
|
-
|
|
1182
|
+
These operations work like standard Drizzle methods but participate in cache context when used within `executeWithCacheContext()`:
|
|
1183
|
+
|
|
1184
|
+
```js
|
|
1185
|
+
// Basic insert (participates in cache context when used within executeWithCacheContext)
|
|
1186
|
+
await forgeSQL.insert(Users).values({ id: 1, name: "Smith" });
|
|
1187
|
+
|
|
1188
|
+
// Basic update (participates in cache context when used within executeWithCacheContext)
|
|
1189
|
+
await forgeSQL.update(Users)
|
|
1190
|
+
.set({ name: "Smith Updated" })
|
|
1191
|
+
.where(eq(Users.id, 1));
|
|
1192
|
+
|
|
1193
|
+
// Basic delete (participates in cache context when used within executeWithCacheContext)
|
|
1194
|
+
await forgeSQL.delete(Users)
|
|
1195
|
+
.where(eq(Users.id, 1));
|
|
1196
|
+
```
|
|
1197
|
+
|
|
1198
|
+
### 2. Non-Versioned Operations with Cache Management
|
|
1199
|
+
|
|
1200
|
+
These operations don't use optimistic locking but provide cache invalidation:
|
|
1201
|
+
|
|
1202
|
+
```js
|
|
1203
|
+
// Insert without versioning but with cache invalidation
|
|
1204
|
+
await forgeSQL.insertAndEvictCache(Users).values({ id: 1, name: "Smith" });
|
|
1205
|
+
|
|
1206
|
+
// Update without versioning but with cache invalidation
|
|
1207
|
+
await forgeSQL.updateAndEvictCache(Users)
|
|
1208
|
+
.set({ name: "Smith Updated" })
|
|
1209
|
+
.where(eq(Users.id, 1));
|
|
1210
|
+
|
|
1211
|
+
// Delete without versioning but with cache invalidation
|
|
1212
|
+
await forgeSQL.deleteAndEvictCache(Users)
|
|
1213
|
+
.where(eq(Users.id, 1));
|
|
1214
|
+
```
|
|
1215
|
+
|
|
1216
|
+
### 3. Versioned Operations with Cache Management (Recommended)
|
|
1217
|
+
|
|
1218
|
+
These operations use optimistic locking and automatic cache invalidation:
|
|
1219
|
+
|
|
1220
|
+
```js
|
|
1221
|
+
// Insert with versioning and cache management
|
|
1222
|
+
const userId = await forgeSQL.modifyWithVersioningAndEvictCache().insert(Users, [{ id: 1, name: "Smith" }]);
|
|
1223
|
+
|
|
1224
|
+
// Bulk insert with versioning
|
|
1225
|
+
await forgeSQL.modifyWithVersioningAndEvictCache().insert(Users, [
|
|
455
1226
|
{ id: 2, name: "Smith" },
|
|
456
1227
|
{ id: 3, name: "Vasyl" },
|
|
457
1228
|
]);
|
|
458
1229
|
|
|
459
|
-
//
|
|
460
|
-
|
|
461
|
-
Users,
|
|
462
|
-
[
|
|
463
|
-
{ id: 4, name: "Smith" },
|
|
464
|
-
{ id: 4, name: "Vasyl" },
|
|
465
|
-
],
|
|
466
|
-
true
|
|
467
|
-
);
|
|
1230
|
+
// Update by ID with optimistic locking and cache invalidation
|
|
1231
|
+
await forgeSQL.modifyWithVersioningAndEvictCache().updateById({ id: 1, name: "Smith Updated" }, Users);
|
|
468
1232
|
|
|
1233
|
+
// Delete by ID with versioning and cache invalidation
|
|
1234
|
+
await forgeSQL.modifyWithVersioningAndEvictCache().deleteById(1, Users);
|
|
1235
|
+
```
|
|
1236
|
+
|
|
1237
|
+
### 4. Versioned Operations without Cache Management
|
|
1238
|
+
|
|
1239
|
+
These operations use optimistic locking but don't manage cache:
|
|
1240
|
+
|
|
1241
|
+
```js
|
|
1242
|
+
// Insert with versioning only (no cache management)
|
|
1243
|
+
const userId = await forgeSQL.modifyWithVersioning().insert(Users, [{ id: 1, name: "Smith" }]);
|
|
1244
|
+
|
|
1245
|
+
// Update with versioning only
|
|
1246
|
+
await forgeSQL.modifyWithVersioning().updateById({ id: 1, name: "Smith Updated" }, Users);
|
|
1247
|
+
|
|
1248
|
+
// Delete with versioning only
|
|
1249
|
+
await forgeSQL.modifyWithVersioning().deleteById(1, Users);
|
|
1250
|
+
```
|
|
1251
|
+
|
|
1252
|
+
### 5. Legacy Modify Operations (Removed in 2.1.x)
|
|
1253
|
+
|
|
1254
|
+
⚠️ **BREAKING CHANGE**: The `crud()` and `modify()` methods have been completely removed in version 2.1.x.
|
|
1255
|
+
|
|
1256
|
+
```js
|
|
1257
|
+
// ❌ These methods no longer exist in 2.1.x
|
|
1258
|
+
// const userId = await forgeSQL.crud().insert(Users, [{ id: 1, name: "Smith" }]);
|
|
1259
|
+
// await forgeSQL.crud().updateById({ id: 1, name: "Smith Updated" }, Users);
|
|
1260
|
+
// await forgeSQL.crud().deleteById(1, Users);
|
|
1261
|
+
|
|
1262
|
+
// ✅ Use the new methods instead
|
|
1263
|
+
const userId = await forgeSQL.modifyWithVersioning().insert(Users, [{ id: 1, name: "Smith" }]);
|
|
1264
|
+
await forgeSQL.modifyWithVersioning().updateById({ id: 1, name: "Smith Updated" }, Users);
|
|
1265
|
+
await forgeSQL.modifyWithVersioning().deleteById(1, Users);
|
|
1266
|
+
```
|
|
1267
|
+
|
|
1268
|
+
### Advanced Operations
|
|
1269
|
+
|
|
1270
|
+
```js
|
|
469
1271
|
// Insert with sequence (nextVal)
|
|
470
1272
|
import { nextVal } from "forge-sql-orm";
|
|
471
1273
|
|
|
@@ -474,38 +1276,24 @@ const user = {
|
|
|
474
1276
|
name: "user test",
|
|
475
1277
|
organization_id: 1
|
|
476
1278
|
};
|
|
477
|
-
const id = await forgeSQL.
|
|
478
|
-
|
|
479
|
-
// The generated SQL will be:
|
|
480
|
-
// INSERT INTO app_user (id, name, organization_id)
|
|
481
|
-
// VALUES (NEXTVAL(user_id_seq), ?, ?) -- params: ["user test", 1]
|
|
482
|
-
```
|
|
483
|
-
|
|
484
|
-
### Update Operations
|
|
485
|
-
|
|
486
|
-
```js
|
|
487
|
-
// Update by ID with optimistic locking
|
|
488
|
-
await forgeSQL.crud().updateById({ id: 1, name: "Smith Updated" }, Users);
|
|
489
|
-
|
|
490
|
-
// Update specific fields
|
|
491
|
-
await forgeSQL.crud().updateById(
|
|
492
|
-
{ id: 1, age: 35 },
|
|
493
|
-
Users
|
|
494
|
-
);
|
|
1279
|
+
const id = await forgeSQL.modifyWithVersioning().insert(appUser, [user]);
|
|
495
1280
|
|
|
496
1281
|
// Update with custom WHERE condition
|
|
497
|
-
await forgeSQL.
|
|
1282
|
+
await forgeSQL.modifyWithVersioning().updateFields(
|
|
498
1283
|
{ name: "New Name", age: 35 },
|
|
499
1284
|
Users,
|
|
500
1285
|
eq(Users.email, "smith@example.com")
|
|
501
1286
|
);
|
|
502
|
-
```
|
|
503
|
-
|
|
504
|
-
### Delete Operations
|
|
505
1287
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
1288
|
+
// Insert with duplicate handling
|
|
1289
|
+
await forgeSQL.modifyWithVersioning().insert(
|
|
1290
|
+
Users,
|
|
1291
|
+
[
|
|
1292
|
+
{ id: 4, name: "Smith" },
|
|
1293
|
+
{ id: 4, name: "Vasyl" },
|
|
1294
|
+
],
|
|
1295
|
+
true
|
|
1296
|
+
);
|
|
509
1297
|
```
|
|
510
1298
|
|
|
511
1299
|
## SQL Utilities
|
|
@@ -543,8 +1331,332 @@ const result = await forgeSQL
|
|
|
543
1331
|
- This prevents SQL injection by ensuring only numeric values are inserted
|
|
544
1332
|
- Always use this function instead of string concatenation for LIMIT and OFFSET values
|
|
545
1333
|
|
|
1334
|
+
## Global Cache System (Level 2)
|
|
1335
|
+
|
|
1336
|
+
[↑ Back to Top](#table-of-contents)
|
|
1337
|
+
|
|
1338
|
+
Forge-SQL-ORM includes a sophisticated global caching system that provides **cross-invocation caching** - the ability to share cached data between different resolver invocations. The global cache system is built on top of [@forge/kvs Custom entity store](https://developer.atlassian.com/platform/forge/storage-reference/storage-api-custom-entities/) and provides persistent cross-invocation caching with automatic serialization/deserialization of complex data structures.
|
|
1339
|
+
|
|
1340
|
+
### Cache Levels Overview
|
|
1341
|
+
|
|
1342
|
+
Forge-SQL-ORM implements a two-level caching architecture:
|
|
1343
|
+
|
|
1344
|
+
- **Level 1 (Local Cache)**: In-memory caching within a single resolver invocation scope
|
|
1345
|
+
- **Level 2 (Global Cache)**: Cross-invocation persistent caching using KVS storage
|
|
1346
|
+
|
|
1347
|
+
This multi-level approach provides optimal performance by checking the fastest cache first, then falling back to cross-invocation persistent storage.
|
|
1348
|
+
|
|
1349
|
+
### Cache Configuration
|
|
1350
|
+
|
|
1351
|
+
The caching system uses Atlassian Forge's Custom entity store to persist cache data. Each cache entry is stored as a custom entity with automatic TTL management and efficient key-based retrieval.
|
|
1352
|
+
|
|
1353
|
+
```typescript
|
|
1354
|
+
const options = {
|
|
1355
|
+
cacheEntityName: "cache", // KVS Custom entity name for cache storage
|
|
1356
|
+
cacheTTL: 300, // Default cache TTL in seconds (5 minutes)
|
|
1357
|
+
cacheWrapTable: true, // Wrap table names with backticks in cache keys
|
|
1358
|
+
additionalMetadata: {
|
|
1359
|
+
users: {
|
|
1360
|
+
tableName: "users",
|
|
1361
|
+
versionField: {
|
|
1362
|
+
fieldName: "updatedAt",
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
};
|
|
1367
|
+
|
|
1368
|
+
const forgeSQL = new ForgeSQL(options);
|
|
1369
|
+
```
|
|
1370
|
+
|
|
1371
|
+
### How Caching Works with @forge/kvs
|
|
1372
|
+
|
|
1373
|
+
The caching system leverages Forge's Custom entity store to provide:
|
|
1374
|
+
|
|
1375
|
+
- **Persistent Storage**: Cache data survives app restarts and deployments
|
|
1376
|
+
- **Automatic TTL**: Built-in expiration handling through Forge's entity lifecycle
|
|
1377
|
+
- **Efficient Retrieval**: Fast key-based lookups using Forge's optimized storage
|
|
1378
|
+
- **Data Serialization**: Automatic handling of complex objects and query results
|
|
1379
|
+
- **Batch Operations**: Efficient bulk cache operations for better performance
|
|
1380
|
+
|
|
1381
|
+
```typescript
|
|
1382
|
+
// Cache entries are stored as custom entities in Forge's KVS
|
|
1383
|
+
// Example cache key structure:
|
|
1384
|
+
// Key: "CachedQuery_8d74bdd9d85064b72fb2ee072ca948e5"
|
|
1385
|
+
// Value: { data: [...], expiration: 1234567890, sql: "select * from 1" }
|
|
1386
|
+
```
|
|
1387
|
+
|
|
1388
|
+
|
|
1389
|
+
### Cache Context Operations
|
|
1390
|
+
|
|
1391
|
+
The cache context allows you to batch cache invalidation events and bypass cache reads for affected tables:
|
|
1392
|
+
|
|
1393
|
+
```typescript
|
|
1394
|
+
// Execute operations within a cache context
|
|
1395
|
+
await forgeSQL.executeWithCacheContext(async () => {
|
|
1396
|
+
// All cache invalidation events are collected and executed in batch
|
|
1397
|
+
await forgeSQL.modifyWithVersioningAndEvictCache().insert(Users, [userData]);
|
|
1398
|
+
await forgeSQL.modifyWithVersioningAndEvictCache().updateById(updateData, Users);
|
|
1399
|
+
// Cache is cleared only once at the end for all affected tables
|
|
1400
|
+
});
|
|
1401
|
+
|
|
1402
|
+
// Execute with return value
|
|
1403
|
+
const result = await forgeSQL.executeWithCacheContextAndReturnValue(async () => {
|
|
1404
|
+
const user = await forgeSQL.modifyWithVersioningAndEvictCache().insert(Users, [userData]);
|
|
1405
|
+
return user;
|
|
1406
|
+
});
|
|
1407
|
+
|
|
1408
|
+
// Basic operations also participate in cache context
|
|
1409
|
+
await forgeSQL.executeWithCacheContext(async () => {
|
|
1410
|
+
// These operations will participate in batch cache clearing
|
|
1411
|
+
await forgeSQL.insert(Users).values(userData);
|
|
1412
|
+
await forgeSQL.update(Users).set(updateData).where(eq(Users.id, 1));
|
|
1413
|
+
await forgeSQL.delete(Users).where(eq(Users.id, 1));
|
|
1414
|
+
// Cache is cleared only once at the end for all affected tables
|
|
1415
|
+
});
|
|
1416
|
+
```
|
|
1417
|
+
|
|
1418
|
+
### Local Cache Operations (Level 1)
|
|
1419
|
+
|
|
1420
|
+
Forge-SQL-ORM provides a local cache system (Level 1 cache) that stores query results in memory for the duration of a single resolver invocation. This is particularly useful for optimizing repeated queries within the same execution context(resolver invocation).
|
|
1421
|
+
|
|
1422
|
+
#### What is Local Cache?
|
|
1423
|
+
|
|
1424
|
+
Local cache is an in-memory caching layer that operates within a single resolver invocation scope. Unlike the global KVS cache, local cache:
|
|
1425
|
+
|
|
1426
|
+
- **Stores data in memory** using Node.js `AsyncLocalStorage`
|
|
1427
|
+
- **Automatically clears** when the invocation completes (Resolver call)
|
|
1428
|
+
- **Provides instant access** to previously executed queries in resolver invocation
|
|
1429
|
+
- **Reduces database load** for repeated operations within the same invocation
|
|
1430
|
+
- **Works alongside** the global KVS cache system
|
|
1431
|
+
|
|
1432
|
+
#### Key Features of Local Cache
|
|
1433
|
+
|
|
1434
|
+
- **In-Memory Storage**: Query results are cached in memory using Node.js `AsyncLocalStorage`
|
|
1435
|
+
- **Invocation-Scoped**: Cache is automatically cleared when the invocation completes
|
|
1436
|
+
- **Automatic Eviction**: Cache is cleared when insert/update/delete operations are performed
|
|
1437
|
+
- **No Persistence**: Data is not stored between Invocations (unlike global KVS cache)
|
|
1438
|
+
- **Performance Optimization**: Reduces database queries for repeated operations
|
|
1439
|
+
- **Simple Configuration**: Works out of the box with simple setup
|
|
1440
|
+
|
|
1441
|
+
#### Usage Examples
|
|
1442
|
+
|
|
1443
|
+
##### Basic Local Cache Usage
|
|
1444
|
+
|
|
1445
|
+
```typescript
|
|
1446
|
+
// Execute operations within a local cache context
|
|
1447
|
+
await forgeSQL.executeWithLocalContext(async () => {
|
|
1448
|
+
// First call - executes query and caches result
|
|
1449
|
+
const users = await forgeSQL.select({ id: users.id, name: users.name })
|
|
1450
|
+
.from(users).where(eq(users.active, true));
|
|
1451
|
+
|
|
1452
|
+
// Second call - gets result from local cache (no database query)
|
|
1453
|
+
const cachedUsers = await forgeSQL.select({ id: users.id, name: users.name })
|
|
1454
|
+
.from(users).where(eq(users.active, true));
|
|
1455
|
+
|
|
1456
|
+
// Using new selectFrom methods with local caching
|
|
1457
|
+
const usersFrom = await forgeSQL.selectFrom(users)
|
|
1458
|
+
.where(eq(users.active, true));
|
|
1459
|
+
|
|
1460
|
+
// This will use local cache (no database call)
|
|
1461
|
+
const cachedUsersFrom = await forgeSQL.selectFrom(users)
|
|
1462
|
+
.where(eq(users.active, true));
|
|
1463
|
+
|
|
1464
|
+
// Using execute() with local caching
|
|
1465
|
+
const rawUsers = await forgeSQL.execute(
|
|
1466
|
+
"SELECT id, name FROM users WHERE active = ?",
|
|
1467
|
+
[true]
|
|
1468
|
+
);
|
|
1469
|
+
|
|
1470
|
+
// This will use local cache (no database call)
|
|
1471
|
+
const cachedRawUsers = await forgeSQL.execute(
|
|
1472
|
+
"SELECT id, name FROM users WHERE active = ?",
|
|
1473
|
+
[true]
|
|
1474
|
+
);
|
|
1475
|
+
|
|
1476
|
+
// Insert operation - evicts local cache for users table
|
|
1477
|
+
await forgeSQL.insert(users).values({ name: 'New User', active: true });
|
|
1478
|
+
|
|
1479
|
+
// Third call - executes query again and caches new result
|
|
1480
|
+
const updatedUsers = await forgeSQL.select({ id: users.id, name: users.name })
|
|
1481
|
+
.from(users).where(eq(users.active, true));
|
|
1482
|
+
});
|
|
1483
|
+
|
|
1484
|
+
// Execute with return value
|
|
1485
|
+
const result = await forgeSQL.executeWithLocalCacheContextAndReturnValue(async () => {
|
|
1486
|
+
// First call - executes query and caches result
|
|
1487
|
+
const users = await forgeSQL.select({ id: users.id, name: users.name })
|
|
1488
|
+
.from(users).where(eq(users.active, true));
|
|
1489
|
+
|
|
1490
|
+
// Second call - gets result from local cache (no database query)
|
|
1491
|
+
const cachedUsers = await forgeSQL.select({ id: users.id, name: users.name })
|
|
1492
|
+
.from(users).where(eq(users.active, true));
|
|
1493
|
+
|
|
1494
|
+
return { users, cachedUsers };
|
|
1495
|
+
});
|
|
1496
|
+
```
|
|
1497
|
+
|
|
1498
|
+
##### Real-World Resolver Example
|
|
1499
|
+
|
|
1500
|
+
```typescript
|
|
1501
|
+
// Atlassian forge resolver with local cache optimization
|
|
1502
|
+
const userResolver = async (req) => {
|
|
1503
|
+
return await forgeSQL.executeWithLocalCacheContextAndReturnValue(async () => {
|
|
1504
|
+
// Get user details using selectFrom (all columns with field aliasing)
|
|
1505
|
+
const user = await forgeSQL.selectFrom(users)
|
|
1506
|
+
.where(eq(users.id, args.userId));
|
|
1507
|
+
|
|
1508
|
+
// Get user's orders using selectCacheableFrom (with caching)
|
|
1509
|
+
const orders = await forgeSQL.selectCacheableFrom(orders)
|
|
1510
|
+
.where(eq(orders.userId, args.userId));
|
|
1511
|
+
|
|
1512
|
+
// Get user's profile using raw SQL with execute()
|
|
1513
|
+
const profile = await forgeSQL.execute(
|
|
1514
|
+
"SELECT id, bio, avatar FROM profiles WHERE user_id = ?",
|
|
1515
|
+
[args.userId]
|
|
1516
|
+
);
|
|
1517
|
+
|
|
1518
|
+
// Get user statistics using complex raw SQL
|
|
1519
|
+
const stats = await forgeSQL.execute(`
|
|
1520
|
+
SELECT
|
|
1521
|
+
COUNT(o.id) as total_orders,
|
|
1522
|
+
SUM(o.amount) as total_spent,
|
|
1523
|
+
AVG(o.amount) as avg_order_value
|
|
1524
|
+
FROM orders o
|
|
1525
|
+
WHERE o.user_id = ? AND o.status = 'completed'
|
|
1526
|
+
`, [args.userId]);
|
|
1527
|
+
|
|
1528
|
+
// If any of these queries are repeated within the same resolver,
|
|
1529
|
+
// they will use the local cache instead of hitting the database
|
|
1530
|
+
|
|
1531
|
+
return {
|
|
1532
|
+
...user[0],
|
|
1533
|
+
orders,
|
|
1534
|
+
profile: profile[0],
|
|
1535
|
+
stats: stats[0]
|
|
1536
|
+
};
|
|
1537
|
+
});
|
|
1538
|
+
};
|
|
1539
|
+
```
|
|
1540
|
+
|
|
1541
|
+
|
|
1542
|
+
#### Local Cache (Level 1) vs Global Cache (Level 2)
|
|
1543
|
+
|
|
1544
|
+
| Feature | Local Cache (Level 1) | Global Cache (Level 2) |
|
|
1545
|
+
|---------|----------------------|------------------------|
|
|
1546
|
+
| **Storage** | In-memory (Node.js process) | Persistent (KVS Custom Entities) |
|
|
1547
|
+
| **Scope** | Single forge invocation | Cross-invocation (between calls) |
|
|
1548
|
+
| **Persistence** | No (cleared on invocation end) | Yes (survives app redeploy) |
|
|
1549
|
+
| **Performance** | Very fast (memory access) | Fast (KVS optimized storage) |
|
|
1550
|
+
| **Memory Usage** | Low (invocation-scoped) | Higher (persistent storage) |
|
|
1551
|
+
| **Use Case** | Invocation optimization | Cross-invocation data sharing |
|
|
1552
|
+
| **Configuration** | None required | Requires KVS setup |
|
|
1553
|
+
| **TTL Support** | No (invocation-scoped) | Yes (automatic expiration) |
|
|
1554
|
+
| **Cache Eviction** | Automatic on DML operations | Manual or scheduled cleanup |
|
|
1555
|
+
| **Best For** | Repeated queries in single invocation | Frequently accessed data across invocations |
|
|
1556
|
+
|
|
1557
|
+
#### Integration with Global Cache (Level 2)
|
|
1558
|
+
|
|
1559
|
+
Local cache (Level 1) works alongside the global cache (Level 2) system:
|
|
1560
|
+
|
|
1561
|
+
```typescript
|
|
1562
|
+
// Multi-level cache checking: Level 1 → Level 2 → Database
|
|
1563
|
+
await forgeSQL.executeWithLocalContext(async () => {
|
|
1564
|
+
// This will check:
|
|
1565
|
+
// 1. Local cache (Level 1 - in-memory)
|
|
1566
|
+
// 2. Global cache (Level 2 - KVS)
|
|
1567
|
+
// 3. Database query
|
|
1568
|
+
const users = await forgeSQL.selectCacheable({ id: users.id, name: users.name })
|
|
1569
|
+
.from(users).where(eq(users.active, true));
|
|
1570
|
+
|
|
1571
|
+
// Using new methods with multi-level caching
|
|
1572
|
+
const usersFrom = await forgeSQL.selectCacheableFrom(users)
|
|
1573
|
+
.where(eq(users.active, true));
|
|
1574
|
+
|
|
1575
|
+
// Raw SQL with multi-level caching
|
|
1576
|
+
const rawUsers = await forgeSQL.executeCacheable(
|
|
1577
|
+
"SELECT id, name FROM users WHERE active = ?",
|
|
1578
|
+
[true],
|
|
1579
|
+
300 // TTL in seconds
|
|
1580
|
+
);
|
|
1581
|
+
});
|
|
1582
|
+
```
|
|
1583
|
+
|
|
1584
|
+
#### Local Cache Flow Diagram
|
|
1585
|
+
|
|
1586
|
+
The diagram below shows how local cache works in Forge-SQL-ORM:
|
|
1587
|
+
|
|
1588
|
+
1. **Request Start**: Local cache context is initialized with empty cache
|
|
1589
|
+
2. **First Query**: Cache miss → Global cache miss → Database query → Save to local cache
|
|
1590
|
+
3. **Repeated Query**: Cache hit → Return cached result (no database call)
|
|
1591
|
+
4. **Data Modification**: Insert/Update/Delete → Evict local cache for affected table
|
|
1592
|
+
5. **Query After Modification**: Cache miss (was evicted) → Database query → Save to local cache
|
|
1593
|
+
6. **Request End**: Local cache context is destroyed, all data cleared
|
|
1594
|
+
|
|
1595
|
+

|
|
1596
|
+
|
|
1597
|
+
### Cache-Aware Query Operations
|
|
1598
|
+
|
|
1599
|
+
```typescript
|
|
1600
|
+
// Execute queries with caching
|
|
1601
|
+
const users = await forgeSQL.modifyWithVersioningAndEvictCache().executeQuery(
|
|
1602
|
+
forgeSQL.select().from(Users).where(eq(Users.active, true)),
|
|
1603
|
+
600 // Custom TTL in seconds
|
|
1604
|
+
);
|
|
1605
|
+
|
|
1606
|
+
// Execute single result queries with caching
|
|
1607
|
+
const user = await forgeSQL.modifyWithVersioningAndEvictCache().executeQueryOnlyOne(
|
|
1608
|
+
forgeSQL.select().from(Users).where(eq(Users.id, 1))
|
|
1609
|
+
);
|
|
1610
|
+
|
|
1611
|
+
// Execute raw SQL with caching
|
|
1612
|
+
const results = await forgeSQL.modifyWithVersioningAndEvictCache().executeRawSQL(
|
|
1613
|
+
"SELECT * FROM users WHERE active = ?",
|
|
1614
|
+
[true],
|
|
1615
|
+
300 // TTL in seconds
|
|
1616
|
+
);
|
|
1617
|
+
|
|
1618
|
+
// Using new methods for cache-aware operations
|
|
1619
|
+
const usersFrom = await forgeSQL.selectCacheableFrom(Users)
|
|
1620
|
+
.where(eq(Users.active, true));
|
|
1621
|
+
|
|
1622
|
+
const usersDistinct = await forgeSQL.selectDistinctCacheableFrom(Users)
|
|
1623
|
+
.where(eq(Users.active, true));
|
|
1624
|
+
|
|
1625
|
+
// Raw SQL with local and global caching
|
|
1626
|
+
const rawUsers = await forgeSQL.executeCacheable(
|
|
1627
|
+
"SELECT * FROM users WHERE active = ?",
|
|
1628
|
+
[true],
|
|
1629
|
+
300 // TTL in seconds
|
|
1630
|
+
);
|
|
1631
|
+
|
|
1632
|
+
// Using with() for Common Table Expressions with caching
|
|
1633
|
+
const userStats = await forgeSQL
|
|
1634
|
+
.with(
|
|
1635
|
+
forgeSQL.selectFrom(users).where(eq(users.active, true)).as('activeUsers'),
|
|
1636
|
+
forgeSQL.selectFrom(orders).where(eq(orders.status, 'completed')).as('completedOrders')
|
|
1637
|
+
)
|
|
1638
|
+
.select({
|
|
1639
|
+
totalActiveUsers: sql`COUNT(au.id)`,
|
|
1640
|
+
totalCompletedOrders: sql`COUNT(co.id)`
|
|
1641
|
+
})
|
|
1642
|
+
.from(sql`activeUsers au`)
|
|
1643
|
+
.leftJoin(sql`completedOrders co`, eq(sql`au.id`, sql`co.userId`));
|
|
1644
|
+
```
|
|
1645
|
+
|
|
1646
|
+
### Manual Cache Management
|
|
1647
|
+
|
|
1648
|
+
```typescript
|
|
1649
|
+
// Clear cache for specific tables
|
|
1650
|
+
await forgeSQL.modifyWithVersioningAndEvictCache().evictCache(["users", "orders"]);
|
|
1651
|
+
|
|
1652
|
+
// Clear cache for specific entities
|
|
1653
|
+
await forgeSQL.modifyWithVersioningAndEvictCache().evictCacheEntities([Users, Orders]);
|
|
1654
|
+
```
|
|
1655
|
+
|
|
546
1656
|
## Optimistic Locking
|
|
547
1657
|
|
|
1658
|
+
[↑ Back to Top](#table-of-contents)
|
|
1659
|
+
|
|
548
1660
|
Optimistic locking is a concurrency control mechanism that prevents data conflicts when multiple transactions attempt to update the same record concurrently. Instead of using locks, this technique relies on a version field in your entity models.
|
|
549
1661
|
|
|
550
1662
|
### Supported Version Field Types
|
|
@@ -575,7 +1687,19 @@ const forgeSQL = new ForgeSQL(options);
|
|
|
575
1687
|
|
|
576
1688
|
```typescript
|
|
577
1689
|
// The version field will be automatically handled
|
|
578
|
-
await forgeSQL.
|
|
1690
|
+
await forgeSQL.modifyWithVersioning().updateById(
|
|
1691
|
+
{
|
|
1692
|
+
id: 1,
|
|
1693
|
+
name: "Updated Name",
|
|
1694
|
+
updatedAt: new Date() // Will be automatically set if not provided
|
|
1695
|
+
},
|
|
1696
|
+
Users
|
|
1697
|
+
);
|
|
1698
|
+
```
|
|
1699
|
+
or with cache support
|
|
1700
|
+
```typescript
|
|
1701
|
+
// The version field will be automatically handled
|
|
1702
|
+
await forgeSQL.modifyWithVersioningAndEvictCache().updateById(
|
|
579
1703
|
{
|
|
580
1704
|
id: 1,
|
|
581
1705
|
name: "Updated Name",
|
|
@@ -594,10 +1718,46 @@ The `ForgeSqlOrmOptions` object allows customization of ORM behavior:
|
|
|
594
1718
|
| `logRawSqlQuery` | `boolean` | Enables logging of raw SQL queries in the Atlassian Forge Developer Console. Useful for debugging and monitoring. Defaults to `false`. |
|
|
595
1719
|
| `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. |
|
|
596
1720
|
| `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. |
|
|
1721
|
+
| `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"`. |
|
|
1722
|
+
| `cacheTTL` | `number` | Default cache TTL in seconds. Defaults to `120` (2 minutes). |
|
|
1723
|
+
| `cacheWrapTable` | `boolean` | Whether to wrap table names with backticks in cache keys. Defaults to `true`. |
|
|
1724
|
+
| `hints` | `object` | SQL hints for query optimization. Optional configuration for advanced query tuning. |
|
|
597
1725
|
|
|
598
1726
|
## CLI Commands
|
|
599
1727
|
|
|
600
|
-
|
|
1728
|
+
Forge-SQL-ORM provides a command-line interface for managing database migrations and model generation.
|
|
1729
|
+
|
|
1730
|
+
**📖 [Full CLI Documentation](forge-sql-orm-cli/README.md)** - Complete CLI reference with all commands and options.
|
|
1731
|
+
|
|
1732
|
+
### Quick CLI Reference
|
|
1733
|
+
|
|
1734
|
+
The CLI tool provides the following main commands:
|
|
1735
|
+
|
|
1736
|
+
- `generate:model` - Generate Drizzle ORM models from your database schema
|
|
1737
|
+
- `migrations:create` - Create new migration files
|
|
1738
|
+
- `migrations:update` - Update existing migrations with schema changes
|
|
1739
|
+
- `migrations:drop` - Create migration to drop tables
|
|
1740
|
+
|
|
1741
|
+
### Installation
|
|
1742
|
+
|
|
1743
|
+
```bash
|
|
1744
|
+
npm install -g forge-sql-orm-cli
|
|
1745
|
+
```
|
|
1746
|
+
|
|
1747
|
+
### Basic Usage
|
|
1748
|
+
|
|
1749
|
+
```bash
|
|
1750
|
+
# Generate models from database
|
|
1751
|
+
forge-sql-orm-cli generate:model --dbName myapp --output ./database/entities
|
|
1752
|
+
|
|
1753
|
+
# Create migration
|
|
1754
|
+
forge-sql-orm-cli migrations:create --dbName myapp --entitiesPath ./database/entities
|
|
1755
|
+
|
|
1756
|
+
# Update migration
|
|
1757
|
+
forge-sql-orm-cli migrations:update --dbName myapp --entitiesPath ./database/entities
|
|
1758
|
+
```
|
|
1759
|
+
|
|
1760
|
+
For detailed information about all available options and advanced usage, see the [Full CLI Documentation](forge-sql-orm-cli/README.md).
|
|
601
1761
|
|
|
602
1762
|
## Web Triggers for Migrations
|
|
603
1763
|
|
|
@@ -718,6 +1878,41 @@ CREATE TABLE IF NOT EXISTS orders (...);
|
|
|
718
1878
|
SET foreign_key_checks = 1;
|
|
719
1879
|
```
|
|
720
1880
|
|
|
1881
|
+
### 4. Clear Cache Scheduler Trigger
|
|
1882
|
+
|
|
1883
|
+
This trigger automatically cleans up expired cache entries based on their TTL (Time To Live). It's useful for:
|
|
1884
|
+
- Automatic cache maintenance
|
|
1885
|
+
- Preventing cache storage from growing indefinitely
|
|
1886
|
+
- Ensuring optimal cache performance
|
|
1887
|
+
- Reducing storage costs
|
|
1888
|
+
|
|
1889
|
+
```typescript
|
|
1890
|
+
// Example usage in your Forge app
|
|
1891
|
+
import { clearCacheSchedulerTrigger } from "forge-sql-orm";
|
|
1892
|
+
|
|
1893
|
+
export const clearCache = () => {
|
|
1894
|
+
return clearCacheSchedulerTrigger({
|
|
1895
|
+
cacheEntityName: "cache",
|
|
1896
|
+
});
|
|
1897
|
+
};
|
|
1898
|
+
```
|
|
1899
|
+
|
|
1900
|
+
Configure in `manifest.yml`:
|
|
1901
|
+
```yaml
|
|
1902
|
+
scheduledTrigger:
|
|
1903
|
+
- key: clear-cache-trigger
|
|
1904
|
+
function: clearCache
|
|
1905
|
+
interval: fiveMinute
|
|
1906
|
+
function:
|
|
1907
|
+
- key: clearCache
|
|
1908
|
+
handler: index.clearCache
|
|
1909
|
+
```
|
|
1910
|
+
|
|
1911
|
+
**Available Intervals**:
|
|
1912
|
+
- `fiveMinute` - Every 5 minutes
|
|
1913
|
+
- `hour` - Every hour
|
|
1914
|
+
- `day` - Every day
|
|
1915
|
+
|
|
721
1916
|
### Important Notes
|
|
722
1917
|
|
|
723
1918
|
**Security Considerations**:
|
|
@@ -733,34 +1928,20 @@ SET foreign_key_checks = 1;
|
|
|
733
1928
|
|
|
734
1929
|
## Query Analysis and Performance Optimization
|
|
735
1930
|
|
|
736
|
-
|
|
1931
|
+
[↑ Back to Top](#table-of-contents)
|
|
1932
|
+
|
|
1933
|
+
Forge-SQL-ORM provides comprehensive query analysis tools to help you optimize your database queries and identify performance bottlenecks.
|
|
737
1934
|
|
|
738
1935
|
### About Atlassian's Built-in Analysis Tools
|
|
739
1936
|
|
|
740
|
-
Atlassian
|
|
1937
|
+
Atlassian provides comprehensive query analysis tools in the development console, including:
|
|
741
1938
|
- Basic query performance metrics
|
|
742
1939
|
- Slow query tracking (queries over 500ms)
|
|
743
1940
|
- Basic execution statistics
|
|
744
1941
|
- Query history and patterns
|
|
745
1942
|
|
|
746
|
-
Our analysis tools
|
|
747
|
-
|
|
748
|
-
### Usage Guidelines
|
|
1943
|
+
Our analysis tools complement these built-in features by providing additional insights directly from TiDB's system schemas.
|
|
749
1944
|
|
|
750
|
-
1. **Development and Troubleshooting Only**
|
|
751
|
-
- These tools should not be used in production code
|
|
752
|
-
- Intended only for development and debugging
|
|
753
|
-
- Use for identifying and fixing performance issues
|
|
754
|
-
|
|
755
|
-
2. **Schema Stability**
|
|
756
|
-
- Features rely on TiDB's `information_schema` and `performance_schema`
|
|
757
|
-
- Schema structure may change in future TiDB updates
|
|
758
|
-
- No guarantee of long-term availability
|
|
759
|
-
|
|
760
|
-
3. **Current Availability (April 2025)**
|
|
761
|
-
- `information_schema` based analysis is currently functional
|
|
762
|
-
- Query plan analysis is available
|
|
763
|
-
- Performance metrics collection is working
|
|
764
1945
|
|
|
765
1946
|
### Available Analysis Tools
|
|
766
1947
|
|
|
@@ -773,10 +1954,10 @@ const analyzeForgeSql = forgeSQL.analyze();
|
|
|
773
1954
|
|
|
774
1955
|
#### Query Plan Analysis
|
|
775
1956
|
|
|
776
|
-
|
|
1957
|
+
Query plan analysis helps you understand how your queries are executed and identify optimization opportunities.
|
|
777
1958
|
|
|
778
1959
|
```typescript
|
|
779
|
-
// Example usage for
|
|
1960
|
+
// Example usage for analyzing a specific query
|
|
780
1961
|
const forgeSQL = new ForgeSQL();
|
|
781
1962
|
const analyzeForgeSql = forgeSQL.analyze();
|
|
782
1963
|
|
|
@@ -801,15 +1982,163 @@ const rawPlan = await analyzeForgeSql.explainRaw(
|
|
|
801
1982
|
"SELECT * FROM users WHERE id = ?",
|
|
802
1983
|
[1]
|
|
803
1984
|
);
|
|
1985
|
+
|
|
1986
|
+
// Analyze new methods
|
|
1987
|
+
const usersFromPlan = await analyzeForgeSql.explain(
|
|
1988
|
+
forgeSQL.selectFrom(users).where(eq(users.active, true))
|
|
1989
|
+
);
|
|
1990
|
+
|
|
1991
|
+
const usersCacheablePlan = await analyzeForgeSql.explain(
|
|
1992
|
+
forgeSQL.selectCacheableFrom(users).where(eq(users.active, true))
|
|
1993
|
+
);
|
|
1994
|
+
|
|
1995
|
+
// Analyze Common Table Expressions (CTEs)
|
|
1996
|
+
const ctePlan = await analyzeForgeSql.explain(
|
|
1997
|
+
forgeSQL
|
|
1998
|
+
.with(
|
|
1999
|
+
forgeSQL.selectFrom(users).where(eq(users.active, true)).as('activeUsers'),
|
|
2000
|
+
forgeSQL.selectFrom(orders).where(eq(orders.status, 'completed')).as('completedOrders')
|
|
2001
|
+
)
|
|
2002
|
+
.select({
|
|
2003
|
+
totalActiveUsers: sql`COUNT(au.id)`,
|
|
2004
|
+
totalCompletedOrders: sql`COUNT(co.id)`
|
|
2005
|
+
})
|
|
2006
|
+
.from(sql`activeUsers au`)
|
|
2007
|
+
.leftJoin(sql`completedOrders co`, eq(sql`au.id`, sql`co.userId`))
|
|
2008
|
+
);
|
|
804
2009
|
```
|
|
805
2010
|
|
|
806
|
-
This analysis
|
|
2011
|
+
This analysis provides insights into:
|
|
807
2012
|
- How the database executes your query
|
|
808
2013
|
- Which indexes are being used
|
|
809
2014
|
- Estimated vs actual row counts
|
|
810
2015
|
- Resource usage at each step
|
|
811
|
-
-
|
|
2016
|
+
- Performance optimization opportunities
|
|
2017
|
+
|
|
2018
|
+
|
|
2019
|
+
## Migration Guide
|
|
2020
|
+
|
|
2021
|
+
### Migrating from 2.0.x to 2.1.x
|
|
2022
|
+
|
|
2023
|
+
This section covers the breaking changes introduced in version 2.1.x and how to migrate your existing code.
|
|
2024
|
+
|
|
2025
|
+
#### 1. Method Renaming (BREAKING CHANGES)
|
|
2026
|
+
|
|
2027
|
+
**Removed Methods:**
|
|
2028
|
+
- `forgeSQL.modify()` → **REMOVED** (use `forgeSQL.modifyWithVersioning()`)
|
|
2029
|
+
- `forgeSQL.crud()` → **REMOVED** (use `forgeSQL.modifyWithVersioning()`)
|
|
2030
|
+
|
|
2031
|
+
**Migration Steps:**
|
|
2032
|
+
|
|
2033
|
+
1. **Replace `modify()` calls:**
|
|
2034
|
+
```typescript
|
|
2035
|
+
// ❌ Old (2.0.x) - NO LONGER WORKS
|
|
2036
|
+
await forgeSQL.modify().insert(Users, [userData]);
|
|
2037
|
+
await forgeSQL.modify().updateById(updateData, Users);
|
|
2038
|
+
await forgeSQL.modify().deleteById(1, Users);
|
|
2039
|
+
|
|
2040
|
+
// ✅ New (2.1.x) - REQUIRED
|
|
2041
|
+
await forgeSQL.modifyWithVersioning().insert(Users, [userData]);
|
|
2042
|
+
await forgeSQL.modifyWithVersioning().updateById(updateData, Users);
|
|
2043
|
+
await forgeSQL.modifyWithVersioning().deleteById(1, Users);
|
|
2044
|
+
```
|
|
2045
|
+
|
|
2046
|
+
2. **Replace `crud()` calls:**
|
|
2047
|
+
```typescript
|
|
2048
|
+
// ❌ Old (2.0.x) - NO LONGER WORKS
|
|
2049
|
+
await forgeSQL.crud().insert(Users, [userData]);
|
|
2050
|
+
await forgeSQL.crud().updateById(updateData, Users);
|
|
2051
|
+
await forgeSQL.crud().deleteById(1, Users);
|
|
2052
|
+
|
|
2053
|
+
// ✅ New (2.1.x) - REQUIRED
|
|
2054
|
+
await forgeSQL.modifyWithVersioning().insert(Users, [userData]);
|
|
2055
|
+
await forgeSQL.modifyWithVersioning().updateById(updateData, Users);
|
|
2056
|
+
await forgeSQL.modifyWithVersioning().deleteById(1, Users);
|
|
2057
|
+
```
|
|
2058
|
+
|
|
2059
|
+
#### 2. New API Methods
|
|
2060
|
+
|
|
2061
|
+
**New Methods Available:**
|
|
2062
|
+
- `forgeSQL.insert()` - Basic Drizzle operations
|
|
2063
|
+
- `forgeSQL.update()` - Basic Drizzle operations
|
|
2064
|
+
- `forgeSQL.delete()` - Basic Drizzle operations
|
|
2065
|
+
- `forgeSQL.insertAndEvictCache()` - Basic Drizzle operations with evict cache after execution
|
|
2066
|
+
- `forgeSQL.updateAndEvictCache()` - Basic Drizzle operations with evict cache after execution
|
|
2067
|
+
- `forgeSQL.deleteAndEvictCache()` - Basic Drizzle operations with evict cache after execution
|
|
2068
|
+
- `forgeSQL.selectFrom()` - All-column queries with field aliasing
|
|
2069
|
+
- `forgeSQL.selectDistinctFrom()` - Distinct all-column queries with field aliasing
|
|
2070
|
+
- `forgeSQL.selectCacheableFrom()` - All-column queries with field aliasing and caching
|
|
2071
|
+
- `forgeSQL.selectDistinctCacheableFrom()` - Distinct all-column queries with field aliasing and caching
|
|
2072
|
+
- `forgeSQL.execute()` - Raw SQL queries with local caching
|
|
2073
|
+
- `forgeSQL.executeCacheable()` - Raw SQL queries with local and global caching
|
|
2074
|
+
- `forgeSQL.with()` - Common Table Expressions (CTEs)
|
|
2075
|
+
|
|
2076
|
+
**Optional Migration:**
|
|
2077
|
+
You can optionally migrate to the new API methods for better performance and cache management:
|
|
2078
|
+
|
|
2079
|
+
```typescript
|
|
2080
|
+
// ❌ Old approach (still works)
|
|
2081
|
+
await forgeSQL.modifyWithVersioning().insert(Users, [userData]);
|
|
2082
|
+
|
|
2083
|
+
// ✅ New approach (recommended for new code)
|
|
2084
|
+
await forgeSQL.insert(Users).values(userData);
|
|
2085
|
+
// or for versioned operations with cache management
|
|
2086
|
+
await forgeSQL.modifyWithVersioningAndEvictCache().insert(Users, [userData]);
|
|
2087
|
+
|
|
2088
|
+
// ✅ New query methods for better performance
|
|
2089
|
+
const users = await forgeSQL.selectFrom(Users)
|
|
2090
|
+
.where(eq(Users.active, true));
|
|
2091
|
+
|
|
2092
|
+
const usersDistinct = await forgeSQL.selectDistinctFrom(Users)
|
|
2093
|
+
.where(eq(Users.active, true));
|
|
2094
|
+
|
|
2095
|
+
const usersCacheable = await forgeSQL.selectCacheableFrom(Users)
|
|
2096
|
+
.where(eq(Users.active, true));
|
|
2097
|
+
|
|
2098
|
+
// ✅ Raw SQL execution with caching
|
|
2099
|
+
const rawUsers = await forgeSQL.execute(
|
|
2100
|
+
"SELECT * FROM users WHERE active = ?",
|
|
2101
|
+
[true]
|
|
2102
|
+
);
|
|
2103
|
+
|
|
2104
|
+
const cachedRawUsers = await forgeSQL.executeCacheable(
|
|
2105
|
+
"SELECT * FROM users WHERE active = ?",
|
|
2106
|
+
[true],
|
|
2107
|
+
300
|
|
2108
|
+
);
|
|
2109
|
+
|
|
2110
|
+
// ✅ Common Table Expressions (CTEs)
|
|
2111
|
+
const userStats = await forgeSQL
|
|
2112
|
+
.with(
|
|
2113
|
+
forgeSQL.selectFrom(users).where(eq(users.active, true)).as('activeUsers'),
|
|
2114
|
+
forgeSQL.selectFrom(orders).where(eq(orders.status, 'completed')).as('completedOrders')
|
|
2115
|
+
)
|
|
2116
|
+
.select({
|
|
2117
|
+
totalActiveUsers: sql`COUNT(au.id)`,
|
|
2118
|
+
totalCompletedOrders: sql`COUNT(co.id)`
|
|
2119
|
+
})
|
|
2120
|
+
.from(sql`activeUsers au`)
|
|
2121
|
+
.leftJoin(sql`completedOrders co`, eq(sql`au.id`, sql`co.userId`));
|
|
2122
|
+
```
|
|
2123
|
+
|
|
2124
|
+
#### 3. Automatic Migration Script
|
|
2125
|
+
|
|
2126
|
+
You can use a simple find-and-replace to migrate your code:
|
|
2127
|
+
|
|
2128
|
+
```bash
|
|
2129
|
+
# Replace modify() calls
|
|
2130
|
+
find . -name "*.ts" -o -name "*.js" | xargs sed -i 's/forgeSQL\.modify()/forgeSQL.modifyWithVersioning()/g'
|
|
2131
|
+
|
|
2132
|
+
# Replace crud() calls
|
|
2133
|
+
find . -name "*.ts" -o -name "*.js" | xargs sed -i 's/forgeSQL\.crud()/forgeSQL.modifyWithVersioning()/g'
|
|
2134
|
+
```
|
|
2135
|
+
|
|
2136
|
+
#### 4. Breaking Changes
|
|
2137
|
+
|
|
2138
|
+
**Important:** The old methods (`modify()` and `crud()`) have been completely removed in version 2.1.x.
|
|
812
2139
|
|
|
2140
|
+
- ❌ **2.1.x**: Old methods are no longer available
|
|
2141
|
+
- ✅ **Migration Required**: You must update your code to use the new methods
|
|
813
2142
|
|
|
814
2143
|
## License
|
|
815
2144
|
This project is licensed under the **MIT License**.
|