forge-sql-orm 2.0.6 → 2.0.8
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 +335 -14
- package/dist/ForgeSQLORM.js +166 -39
- package/dist/ForgeSQLORM.js.map +1 -1
- package/dist/ForgeSQLORM.mjs +150 -23
- package/dist/ForgeSQLORM.mjs.map +1 -1
- package/dist/core/ForgeSQLCrudOperations.d.ts.map +1 -1
- package/dist/core/ForgeSQLORM.d.ts +39 -2
- package/dist/core/ForgeSQLORM.d.ts.map +1 -1
- package/dist/core/ForgeSQLQueryBuilder.d.ts +5 -8
- package/dist/core/ForgeSQLQueryBuilder.d.ts.map +1 -1
- package/dist/core/ForgeSQLSelectOperations.d.ts.map +1 -1
- package/dist/utils/forgeDriver.d.ts.map +1 -1
- package/dist/utils/sqlUtils.d.ts +5 -0
- package/dist/utils/sqlUtils.d.ts.map +1 -1
- package/dist/webtriggers/applyMigrationsWebTrigger.d.ts.map +1 -1
- package/dist/webtriggers/dropMigrationWebTrigger.d.ts +0 -5
- package/dist/webtriggers/dropMigrationWebTrigger.d.ts.map +1 -1
- package/dist/webtriggers/index.d.ts +2 -2
- package/dist/webtriggers/index.d.ts.map +1 -1
- package/dist-cli/cli.js.map +1 -1
- package/dist-cli/cli.mjs.map +1 -1
- package/package.json +1 -1
- package/src/core/ForgeSQLCrudOperations.ts +2 -2
- package/src/core/ForgeSQLORM.ts +102 -3
- package/src/core/ForgeSQLQueryBuilder.ts +19 -8
- package/src/core/ForgeSQLSelectOperations.ts +10 -4
- package/src/utils/forgeDriver.ts +21 -27
- package/src/utils/sqlUtils.ts +55 -5
- package/src/webtriggers/applyMigrationsWebTrigger.ts +7 -6
- package/src/webtriggers/dropMigrationWebTrigger.ts +8 -20
- package/src/webtriggers/index.ts +19 -20
package/README.md
CHANGED
|
@@ -23,14 +23,53 @@ import { drizzle } from "drizzle-orm/mysql-core";
|
|
|
23
23
|
import { forgeDriver } from "forge-sql-orm";
|
|
24
24
|
const db = drizzle(forgeDriver);
|
|
25
25
|
```
|
|
26
|
-
Best for: Simple CRUD operations without optimistic locking
|
|
26
|
+
Best for: Simple CRUD operations without optimistic locking. Note that you need to manually set `mapSelectFieldsWithAlias` for select fields to prevent field name collisions in Atlassian Forge SQL.
|
|
27
27
|
|
|
28
28
|
### 2. Full Forge-SQL-ORM Usage
|
|
29
29
|
```typescript
|
|
30
30
|
import ForgeSQL from "forge-sql-orm";
|
|
31
31
|
const forgeSQL = new ForgeSQL();
|
|
32
32
|
```
|
|
33
|
-
Best for: Advanced features like optimistic locking and automatic
|
|
33
|
+
Best for: Advanced features like optimistic locking, automatic versioning, and automatic field name collision prevention in complex queries.
|
|
34
|
+
|
|
35
|
+
## Field Name Collision Prevention in Complex Queries
|
|
36
|
+
|
|
37
|
+
When working with complex queries involving multiple tables (joins, inner joins, etc.), Atlassian Forge SQL has a specific behavior where fields with the same name from different tables get collapsed into a single field with a null value. This is not a Drizzle ORM issue but rather a characteristic of Atlassian Forge SQL's behavior.
|
|
38
|
+
|
|
39
|
+
Forge-SQL-ORM provides two ways to handle this:
|
|
40
|
+
|
|
41
|
+
### Using Forge-SQL-ORM
|
|
42
|
+
```typescript
|
|
43
|
+
import ForgeSQL from "forge-sql-orm";
|
|
44
|
+
|
|
45
|
+
const forgeSQL = new ForgeSQL();
|
|
46
|
+
|
|
47
|
+
// Automatic field name collision prevention
|
|
48
|
+
await forgeSQL
|
|
49
|
+
.select({user: users, order: orders})
|
|
50
|
+
.from(orders)
|
|
51
|
+
.innerJoin(users, eq(orders.userId, users.id));
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Using Direct Drizzle
|
|
55
|
+
```typescript
|
|
56
|
+
import { drizzle } from "drizzle-orm/mysql-core";
|
|
57
|
+
import { forgeDriver, mapSelectFieldsWithAlias } from "forge-sql-orm";
|
|
58
|
+
|
|
59
|
+
const db = drizzle(forgeDriver);
|
|
60
|
+
|
|
61
|
+
// Manual field name collision prevention
|
|
62
|
+
await db
|
|
63
|
+
.select(mapSelectFieldsWithAlias({user: users, order: orders}))
|
|
64
|
+
.from(orders)
|
|
65
|
+
.innerJoin(users, eq(orders.userId, users.id));
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Important Notes
|
|
69
|
+
- This is a specific behavior of Atlassian Forge SQL, not Drizzle ORM
|
|
70
|
+
- For complex queries involving multiple tables, it's recommended to always specify select fields and avoid using `select()` without field selection
|
|
71
|
+
- The solution automatically creates unique aliases for each field by prefixing them with the table name
|
|
72
|
+
- This ensures that fields with the same name from different tables remain distinct in the query results
|
|
34
73
|
|
|
35
74
|
## Installation
|
|
36
75
|
|
|
@@ -40,7 +79,7 @@ Forge-SQL-ORM is designed to work with @forge/sql and requires some additional s
|
|
|
40
79
|
|
|
41
80
|
```sh
|
|
42
81
|
npm install forge-sql-orm @forge/sql drizzle-orm momment -S
|
|
43
|
-
npm install mysql2
|
|
82
|
+
npm install mysql2 drizzle-kit inquirer@8.0.0 -D
|
|
44
83
|
```
|
|
45
84
|
|
|
46
85
|
This will:
|
|
@@ -293,13 +332,13 @@ const users = await db.select().from(users);
|
|
|
293
332
|
// Using forgeSQL.getDrizzleQueryBuilder()
|
|
294
333
|
const user = await forgeSQL
|
|
295
334
|
.getDrizzleQueryBuilder()
|
|
296
|
-
.select(
|
|
335
|
+
.select().from(Users)
|
|
297
336
|
.where(eq(Users.id, 1));
|
|
298
337
|
|
|
299
338
|
// OR using direct drizzle with custom driver
|
|
300
339
|
const db = drizzle(forgeDriver);
|
|
301
340
|
const user = await db
|
|
302
|
-
.select(
|
|
341
|
+
.select().from(Users)
|
|
303
342
|
.where(eq(Users.id, 1));
|
|
304
343
|
// Returns: { id: 1, name: "John Doe" }
|
|
305
344
|
|
|
@@ -309,7 +348,7 @@ const user = await forgeSQL
|
|
|
309
348
|
.executeQueryOnlyOne(
|
|
310
349
|
forgeSQL
|
|
311
350
|
.getDrizzleQueryBuilder()
|
|
312
|
-
.select(
|
|
351
|
+
.select().from(Users)
|
|
313
352
|
.where(eq(Users.id, 1))
|
|
314
353
|
);
|
|
315
354
|
// Returns: { id: 1, name: "John Doe" }
|
|
@@ -322,24 +361,306 @@ const usersAlias = alias(Users, "u");
|
|
|
322
361
|
const result = await forgeSQL
|
|
323
362
|
.getDrizzleQueryBuilder()
|
|
324
363
|
.select({
|
|
325
|
-
userId:
|
|
326
|
-
userName:
|
|
364
|
+
userId: sql<string>`${usersAlias.id} as \`userId\``,
|
|
365
|
+
userName: sql<string>`${usersAlias.name} as \`userName\``
|
|
327
366
|
}).from(usersAlias);
|
|
328
367
|
|
|
329
368
|
// OR with direct drizzle
|
|
330
369
|
const db = drizzle(forgeDriver);
|
|
331
370
|
const result = await db
|
|
332
371
|
.select({
|
|
333
|
-
userId:
|
|
334
|
-
userName:
|
|
372
|
+
userId: sql<string>`${usersAlias.id} as \`userId\``,
|
|
373
|
+
userName: sql<string>`${usersAlias.name} as \`userName\``
|
|
335
374
|
}).from(usersAlias);
|
|
336
375
|
// Returns: { userId: 1, userName: "John Doe" }
|
|
376
|
+
```
|
|
337
377
|
|
|
338
|
-
|
|
378
|
+
### Complex Queries
|
|
379
|
+
```js
|
|
380
|
+
|
|
381
|
+
// Using joins with automatic field name collision prevention
|
|
339
382
|
// With forgeSQL
|
|
340
383
|
const orderWithUser = await forgeSQL
|
|
384
|
+
.select({user: users, order: orders})
|
|
385
|
+
.from(orders)
|
|
386
|
+
.innerJoin(users, eq(orders.userId, users.id));
|
|
387
|
+
|
|
388
|
+
// OR with direct drizzle
|
|
389
|
+
const db = drizzle(forgeDriver);
|
|
390
|
+
const orderWithUser = await db
|
|
391
|
+
.select(mapSelectFieldsWithAlias({user: users, order: orders}))
|
|
392
|
+
.from(orders)
|
|
393
|
+
.innerJoin(users, eq(orders.userId, users.id));
|
|
394
|
+
// Returns: {
|
|
395
|
+
// user_id: 1,
|
|
396
|
+
// user_name: "John Doe",
|
|
397
|
+
// order_id: 1,
|
|
398
|
+
// order_product: "Product 1"
|
|
399
|
+
// }
|
|
400
|
+
|
|
401
|
+
// Using distinct select with automatic field name collision prevention
|
|
402
|
+
const uniqueOrdersWithUsers = await forgeSQL
|
|
403
|
+
.selectDistinct({user: users, order: orders})
|
|
404
|
+
.from(orders)
|
|
405
|
+
.innerJoin(users, eq(orders.userId, users.id));
|
|
406
|
+
|
|
407
|
+
// Finding duplicates
|
|
408
|
+
// With forgeSQL
|
|
409
|
+
const duplicates = await forgeSQL
|
|
341
410
|
.getDrizzleQueryBuilder()
|
|
342
411
|
.select({
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
412
|
+
name: Users.name,
|
|
413
|
+
count: sql<number>`COUNT(*) as \`count\``
|
|
414
|
+
}).from(Users)
|
|
415
|
+
.groupBy(Users.name)
|
|
416
|
+
.having(sql`COUNT(*) > 1`);
|
|
417
|
+
|
|
418
|
+
// OR with direct drizzle
|
|
419
|
+
const db = drizzle(forgeDriver);
|
|
420
|
+
const duplicates = await db
|
|
421
|
+
.select({
|
|
422
|
+
name: Users.name,
|
|
423
|
+
count: sql<number>`COUNT(*) as \`count\``
|
|
424
|
+
}).from(Users)
|
|
425
|
+
.groupBy(Users.name)
|
|
426
|
+
.having(sql`COUNT(*) > 1`);
|
|
427
|
+
// Returns: { name: "John Doe", count: 2 }
|
|
428
|
+
|
|
429
|
+
// Using executeQueryOnlyOne for unique results
|
|
430
|
+
const userStats = await forgeSQL
|
|
431
|
+
.fetch()
|
|
432
|
+
.executeQueryOnlyOne(
|
|
433
|
+
forgeSQL
|
|
434
|
+
.getDrizzleQueryBuilder()
|
|
435
|
+
.select({
|
|
436
|
+
totalUsers: sql`COUNT(*) as \`totalUsers\``,
|
|
437
|
+
uniqueNames: sql`COUNT(DISTINCT name) as \`uniqueNames\``
|
|
438
|
+
}).from(Users)
|
|
439
|
+
);
|
|
440
|
+
// Returns: { totalUsers: 100, uniqueNames: 80 }
|
|
441
|
+
// Throws error if multiple records found
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
### Raw SQL Queries
|
|
445
|
+
|
|
446
|
+
```js
|
|
447
|
+
// Using executeRawSQL for direct SQL queries
|
|
448
|
+
const users = await forgeSQL
|
|
449
|
+
.fetch()
|
|
450
|
+
.executeRawSQL<Users>("SELECT * FROM users");
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
## CRUD Operations
|
|
454
|
+
|
|
455
|
+
### Insert Operations
|
|
456
|
+
|
|
457
|
+
```js
|
|
458
|
+
// Single insert
|
|
459
|
+
const userId = await forgeSQL.crud().insert(Users, [{ id: 1, name: "Smith" }]);
|
|
460
|
+
|
|
461
|
+
// Bulk insert
|
|
462
|
+
await forgeSQL.crud().insert(Users, [
|
|
463
|
+
{ id: 2, name: "Smith" },
|
|
464
|
+
{ id: 3, name: "Vasyl" },
|
|
465
|
+
]);
|
|
466
|
+
|
|
467
|
+
// Insert with duplicate handling
|
|
468
|
+
await forgeSQL.crud().insert(
|
|
469
|
+
Users,
|
|
470
|
+
[
|
|
471
|
+
{ id: 4, name: "Smith" },
|
|
472
|
+
{ id: 4, name: "Vasyl" },
|
|
473
|
+
],
|
|
474
|
+
true
|
|
475
|
+
);
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
### Update Operations
|
|
479
|
+
|
|
480
|
+
```js
|
|
481
|
+
// Update by ID with optimistic locking
|
|
482
|
+
await forgeSQL.crud().updateById({ id: 1, name: "Smith Updated" }, Users);
|
|
483
|
+
|
|
484
|
+
// Update specific fields
|
|
485
|
+
await forgeSQL.crud().updateById(
|
|
486
|
+
{ id: 1, age: 35 },
|
|
487
|
+
Users
|
|
488
|
+
);
|
|
489
|
+
|
|
490
|
+
// Update with custom WHERE condition
|
|
491
|
+
await forgeSQL.crud().updateFields(
|
|
492
|
+
{ name: "New Name", age: 35 },
|
|
493
|
+
Users,
|
|
494
|
+
eq(Users.email, "smith@example.com")
|
|
495
|
+
);
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
### Delete Operations
|
|
499
|
+
|
|
500
|
+
```js
|
|
501
|
+
// Delete by ID
|
|
502
|
+
await forgeSQL.crud().deleteById(1, Users);
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
## Optimistic Locking
|
|
506
|
+
|
|
507
|
+
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.
|
|
508
|
+
|
|
509
|
+
### Supported Version Field Types
|
|
510
|
+
|
|
511
|
+
- `datetime` - Timestamp-based versioning
|
|
512
|
+
- `timestamp` - Timestamp-based versioning
|
|
513
|
+
- `integer` - Numeric version increment
|
|
514
|
+
- `decimal` - Numeric version increment
|
|
515
|
+
|
|
516
|
+
### Configuration
|
|
517
|
+
|
|
518
|
+
```typescript
|
|
519
|
+
const options = {
|
|
520
|
+
additionalMetadata: {
|
|
521
|
+
users: {
|
|
522
|
+
tableName: "users",
|
|
523
|
+
versionField: {
|
|
524
|
+
fieldName: "updatedAt",
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
const forgeSQL = new ForgeSQL(options);
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
### Example Usage
|
|
534
|
+
|
|
535
|
+
```typescript
|
|
536
|
+
// The version field will be automatically handled
|
|
537
|
+
await forgeSQL.crud().updateById(
|
|
538
|
+
{
|
|
539
|
+
id: 1,
|
|
540
|
+
name: "Updated Name",
|
|
541
|
+
updatedAt: new Date() // Will be automatically set if not provided
|
|
542
|
+
},
|
|
543
|
+
Users
|
|
544
|
+
);
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
## ForgeSqlOrmOptions
|
|
548
|
+
|
|
549
|
+
The `ForgeSqlOrmOptions` object allows customization of ORM behavior:
|
|
550
|
+
|
|
551
|
+
| Option | Type | Description |
|
|
552
|
+
| -------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
553
|
+
| `logRawSqlQuery` | `boolean` | Enables logging of raw SQL queries in the Atlassian Forge Developer Console. Useful for debugging and monitoring. Defaults to `false`. |
|
|
554
|
+
| `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. |
|
|
555
|
+
| `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. |
|
|
556
|
+
|
|
557
|
+
## CLI Commands
|
|
558
|
+
|
|
559
|
+
```sh
|
|
560
|
+
$ npx forge-sql-orm --help
|
|
561
|
+
|
|
562
|
+
Usage: forge-sql-orm [options] [command]
|
|
563
|
+
|
|
564
|
+
Options:
|
|
565
|
+
-V, --version Output the version number
|
|
566
|
+
-h, --help Display help for command
|
|
567
|
+
|
|
568
|
+
Commands:
|
|
569
|
+
generate:model [options] Generate Drizzle models from the database
|
|
570
|
+
migrations:create [options] Generate an initial migration for the entire database
|
|
571
|
+
migrations:update [options] Generate a migration to update the database schema
|
|
572
|
+
migrations:drop [options] Generate a migration to drop all tables
|
|
573
|
+
help [command] Display help for a specific command
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
## Web Triggers for Migrations
|
|
577
|
+
|
|
578
|
+
Forge-SQL-ORM provides two web triggers for managing database migrations in Atlassian Forge:
|
|
579
|
+
|
|
580
|
+
### 1. Apply Migrations Trigger
|
|
581
|
+
|
|
582
|
+
This trigger allows you to apply database migrations through a web endpoint. It's useful for:
|
|
583
|
+
- Manually triggering migrations
|
|
584
|
+
- Running migrations as part of your deployment process
|
|
585
|
+
- Testing migrations in different environments
|
|
586
|
+
|
|
587
|
+
```typescript
|
|
588
|
+
// Example usage in your Forge app
|
|
589
|
+
import { applySchemaMigrations } from "forge-sql-orm";
|
|
590
|
+
import migration from "./migration";
|
|
591
|
+
|
|
592
|
+
export const handlerMigration = async () => {
|
|
593
|
+
return applySchemaMigrations(migration);
|
|
594
|
+
};
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
Configure in `manifest.yml`:
|
|
598
|
+
```yaml
|
|
599
|
+
webtrigger:
|
|
600
|
+
- key: invoke-schema-migration
|
|
601
|
+
function: runSchemaMigration
|
|
602
|
+
security:
|
|
603
|
+
egress:
|
|
604
|
+
allowDataEgress: false
|
|
605
|
+
allowedResponses:
|
|
606
|
+
- statusCode: 200
|
|
607
|
+
body: '{"body": "Migrations successfully executed"}'
|
|
608
|
+
sql:
|
|
609
|
+
- key: main
|
|
610
|
+
engine: mysql
|
|
611
|
+
function:
|
|
612
|
+
- key: runSchemaMigration
|
|
613
|
+
handler: index.handlerMigration
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
### 2. Drop Migrations Trigger
|
|
617
|
+
|
|
618
|
+
⚠️ **WARNING**: This trigger will permanently delete all data in the specified tables and clear the migrations history. This operation cannot be undone!
|
|
619
|
+
|
|
620
|
+
This trigger allows you to completely reset your database schema. It's useful for:
|
|
621
|
+
- Development environments where you need to start fresh
|
|
622
|
+
- Testing scenarios requiring a clean database
|
|
623
|
+
- Resetting the database before applying new migrations
|
|
624
|
+
|
|
625
|
+
**Important**: The trigger will only drop tables that are defined in your models. Any tables that exist in the database but are not defined in your models will remain untouched.
|
|
626
|
+
|
|
627
|
+
```typescript
|
|
628
|
+
// Example usage in your Forge app
|
|
629
|
+
import { dropSchemaMigrations } from "forge-sql-orm";
|
|
630
|
+
import * as schema from "./entities/schema";
|
|
631
|
+
|
|
632
|
+
export const dropMigrations = () => {
|
|
633
|
+
return dropSchemaMigrations(Object.values(schema));
|
|
634
|
+
};
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
Configure in `manifest.yml`:
|
|
638
|
+
```yaml
|
|
639
|
+
webtrigger:
|
|
640
|
+
- key: drop-schema-migration
|
|
641
|
+
function: dropMigrations
|
|
642
|
+
sql:
|
|
643
|
+
- key: main
|
|
644
|
+
engine: mysql
|
|
645
|
+
function:
|
|
646
|
+
- key: dropMigrations
|
|
647
|
+
handler: index.dropMigrations
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
### Important Notes
|
|
651
|
+
|
|
652
|
+
**Security Considerations**:
|
|
653
|
+
- The drop migrations trigger should be restricted to development environments
|
|
654
|
+
- Consider implementing additional authentication for these endpoints
|
|
655
|
+
- Use the `security` section in `manifest.yml` to control access
|
|
656
|
+
|
|
657
|
+
**Best Practices**:
|
|
658
|
+
- Always backup your data before using the drop migrations trigger
|
|
659
|
+
- Test migrations in a development environment first
|
|
660
|
+
- Use these triggers as part of your deployment pipeline
|
|
661
|
+
- Monitor the execution logs in the Forge Developer Console
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
## License
|
|
665
|
+
This project is licensed under the **MIT License**.
|
|
666
|
+
Feel free to use it for commercial and personal projects.
|