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 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 versioning
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 @types/mysql2 drizzle-kit -D
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("*").from(Users)
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("*").from(Users)
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("*").from(Users)
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: rawSql`${usersAlias.id} as \`userId\``,
326
- userName: rawSql`${usersAlias.name} as \`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: rawSql`${usersAlias.id} as \`userId\``,
334
- userName: rawSql`${usersAlias.name} as \`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
- // Using joins
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
- orderId: rawSql`${Orders.id} as \`orderId\``,
344
- product: Orders.product,
345
- userName: rawSql`${Users.name} as \`
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.