forge-sql-orm 2.0.6 → 2.0.7
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 +281 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -342,4 +342,284 @@ const orderWithUser = await forgeSQL
|
|
|
342
342
|
.select({
|
|
343
343
|
orderId: rawSql`${Orders.id} as \`orderId\``,
|
|
344
344
|
product: Orders.product,
|
|
345
|
-
userName: rawSql`${Users.name} as \`
|
|
345
|
+
userName: rawSql`${Users.name} as \`userName\``
|
|
346
|
+
}).from(Orders)
|
|
347
|
+
.innerJoin(Users, eq(Orders.userId, Users.id))
|
|
348
|
+
.where(eq(Orders.id, 1));
|
|
349
|
+
|
|
350
|
+
// OR with direct drizzle
|
|
351
|
+
const db = drizzle(forgeDriver);
|
|
352
|
+
const orderWithUser = await db
|
|
353
|
+
.select({
|
|
354
|
+
orderId: rawSql`${Orders.id} as \`orderId\``,
|
|
355
|
+
product: Orders.product,
|
|
356
|
+
userName: rawSql`${Users.name} as \`userName\``
|
|
357
|
+
}).from(Orders)
|
|
358
|
+
.innerJoin(Users, eq(Orders.userId, Users.id))
|
|
359
|
+
.where(eq(Orders.id, 1));
|
|
360
|
+
// Returns: { orderId: 1, product: "Product 1", userName: "John Doe" }
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
### Complex Queries with Aggregations
|
|
364
|
+
|
|
365
|
+
```js
|
|
366
|
+
// Finding duplicates
|
|
367
|
+
// With forgeSQL
|
|
368
|
+
const duplicates = await forgeSQL
|
|
369
|
+
.getDrizzleQueryBuilder()
|
|
370
|
+
.select({
|
|
371
|
+
name: Users.name,
|
|
372
|
+
count: rawSql`COUNT(*) as \`count\``
|
|
373
|
+
}).from(Users)
|
|
374
|
+
.groupBy(Users.name)
|
|
375
|
+
.having(rawSql`COUNT(*) > 1`);
|
|
376
|
+
|
|
377
|
+
// OR with direct drizzle
|
|
378
|
+
const db = drizzle(forgeDriver);
|
|
379
|
+
const duplicates = await db
|
|
380
|
+
.select({
|
|
381
|
+
name: Users.name,
|
|
382
|
+
count: rawSql`COUNT(*) as \`count\``
|
|
383
|
+
}).from(Users)
|
|
384
|
+
.groupBy(Users.name)
|
|
385
|
+
.having(rawSql`COUNT(*) > 1`);
|
|
386
|
+
// Returns: { name: "John Doe", count: 2 }
|
|
387
|
+
|
|
388
|
+
// Using executeQueryOnlyOne for unique results
|
|
389
|
+
const userStats = await forgeSQL
|
|
390
|
+
.fetch()
|
|
391
|
+
.executeQueryOnlyOne(
|
|
392
|
+
forgeSQL
|
|
393
|
+
.getDrizzleQueryBuilder()
|
|
394
|
+
.select({
|
|
395
|
+
totalUsers: rawSql`COUNT(*) as \`totalUsers\``,
|
|
396
|
+
uniqueNames: rawSql`COUNT(DISTINCT name) as \`uniqueNames\``
|
|
397
|
+
}).from(Users)
|
|
398
|
+
);
|
|
399
|
+
// Returns: { totalUsers: 100, uniqueNames: 80 }
|
|
400
|
+
// Throws error if multiple records found
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
### Raw SQL Queries
|
|
404
|
+
|
|
405
|
+
```js
|
|
406
|
+
// Using executeRawSQL for direct SQL queries
|
|
407
|
+
const users = await forgeSQL
|
|
408
|
+
.fetch()
|
|
409
|
+
.executeRawSQL<Users>("SELECT * FROM users");
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
## CRUD Operations
|
|
413
|
+
|
|
414
|
+
### Insert Operations
|
|
415
|
+
|
|
416
|
+
```js
|
|
417
|
+
// Single insert
|
|
418
|
+
const userId = await forgeSQL.crud().insert(Users, [{ id: 1, name: "Smith" }]);
|
|
419
|
+
|
|
420
|
+
// Bulk insert
|
|
421
|
+
await forgeSQL.crud().insert(Users, [
|
|
422
|
+
{ id: 2, name: "Smith" },
|
|
423
|
+
{ id: 3, name: "Vasyl" },
|
|
424
|
+
]);
|
|
425
|
+
|
|
426
|
+
// Insert with duplicate handling
|
|
427
|
+
await forgeSQL.crud().insert(
|
|
428
|
+
Users,
|
|
429
|
+
[
|
|
430
|
+
{ id: 4, name: "Smith" },
|
|
431
|
+
{ id: 4, name: "Vasyl" },
|
|
432
|
+
],
|
|
433
|
+
true
|
|
434
|
+
);
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
### Update Operations
|
|
438
|
+
|
|
439
|
+
```js
|
|
440
|
+
// Update by ID with optimistic locking
|
|
441
|
+
await forgeSQL.crud().updateById({ id: 1, name: "Smith Updated" }, Users);
|
|
442
|
+
|
|
443
|
+
// Update specific fields
|
|
444
|
+
await forgeSQL.crud().updateById(
|
|
445
|
+
{ id: 1, age: 35 },
|
|
446
|
+
Users
|
|
447
|
+
);
|
|
448
|
+
|
|
449
|
+
// Update with custom WHERE condition
|
|
450
|
+
await forgeSQL.crud().updateFields(
|
|
451
|
+
{ name: "New Name", age: 35 },
|
|
452
|
+
Users,
|
|
453
|
+
eq(Users.email, "smith@example.com")
|
|
454
|
+
);
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
### Delete Operations
|
|
458
|
+
|
|
459
|
+
```js
|
|
460
|
+
// Delete by ID
|
|
461
|
+
await forgeSQL.crud().deleteById(1, Users);
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
## Optimistic Locking
|
|
465
|
+
|
|
466
|
+
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.
|
|
467
|
+
|
|
468
|
+
### Supported Version Field Types
|
|
469
|
+
|
|
470
|
+
- `datetime` - Timestamp-based versioning
|
|
471
|
+
- `timestamp` - Timestamp-based versioning
|
|
472
|
+
- `integer` - Numeric version increment
|
|
473
|
+
- `decimal` - Numeric version increment
|
|
474
|
+
|
|
475
|
+
### Configuration
|
|
476
|
+
|
|
477
|
+
```typescript
|
|
478
|
+
const options = {
|
|
479
|
+
additionalMetadata: {
|
|
480
|
+
users: {
|
|
481
|
+
tableName: "users",
|
|
482
|
+
versionField: {
|
|
483
|
+
fieldName: "updatedAt",
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
const forgeSQL = new ForgeSQL(options);
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
### Example Usage
|
|
493
|
+
|
|
494
|
+
```typescript
|
|
495
|
+
// The version field will be automatically handled
|
|
496
|
+
await forgeSQL.crud().updateById(
|
|
497
|
+
{
|
|
498
|
+
id: 1,
|
|
499
|
+
name: "Updated Name",
|
|
500
|
+
updatedAt: new Date() // Will be automatically set if not provided
|
|
501
|
+
},
|
|
502
|
+
Users
|
|
503
|
+
);
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
## ForgeSqlOrmOptions
|
|
507
|
+
|
|
508
|
+
The `ForgeSqlOrmOptions` object allows customization of ORM behavior:
|
|
509
|
+
|
|
510
|
+
| Option | Type | Description |
|
|
511
|
+
| -------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
512
|
+
| `logRawSqlQuery` | `boolean` | Enables logging of raw SQL queries in the Atlassian Forge Developer Console. Useful for debugging and monitoring. Defaults to `false`. |
|
|
513
|
+
| `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. |
|
|
514
|
+
| `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. |
|
|
515
|
+
|
|
516
|
+
## CLI Commands
|
|
517
|
+
|
|
518
|
+
```sh
|
|
519
|
+
$ npx forge-sql-orm --help
|
|
520
|
+
|
|
521
|
+
Usage: forge-sql-orm [options] [command]
|
|
522
|
+
|
|
523
|
+
Options:
|
|
524
|
+
-V, --version Output the version number
|
|
525
|
+
-h, --help Display help for command
|
|
526
|
+
|
|
527
|
+
Commands:
|
|
528
|
+
generate:model [options] Generate Drizzle models from the database
|
|
529
|
+
migrations:create [options] Generate an initial migration for the entire database
|
|
530
|
+
migrations:update [options] Generate a migration to update the database schema
|
|
531
|
+
migrations:drop [options] Generate a migration to drop all tables
|
|
532
|
+
help [command] Display help for a specific command
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
## Web Triggers for Migrations
|
|
536
|
+
|
|
537
|
+
Forge-SQL-ORM provides two web triggers for managing database migrations in Atlassian Forge:
|
|
538
|
+
|
|
539
|
+
### 1. Apply Migrations Trigger
|
|
540
|
+
|
|
541
|
+
This trigger allows you to apply database migrations through a web endpoint. It's useful for:
|
|
542
|
+
- Manually triggering migrations
|
|
543
|
+
- Running migrations as part of your deployment process
|
|
544
|
+
- Testing migrations in different environments
|
|
545
|
+
|
|
546
|
+
```typescript
|
|
547
|
+
// Example usage in your Forge app
|
|
548
|
+
import { applySchemaMigrations } from "forge-sql-orm";
|
|
549
|
+
import migration from "./migration";
|
|
550
|
+
|
|
551
|
+
export const handlerMigration = async () => {
|
|
552
|
+
return applySchemaMigrations(migration);
|
|
553
|
+
};
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
Configure in `manifest.yml`:
|
|
557
|
+
```yaml
|
|
558
|
+
webtrigger:
|
|
559
|
+
- key: invoke-schema-migration
|
|
560
|
+
function: runSchemaMigration
|
|
561
|
+
security:
|
|
562
|
+
egress:
|
|
563
|
+
allowDataEgress: false
|
|
564
|
+
allowedResponses:
|
|
565
|
+
- statusCode: 200
|
|
566
|
+
body: '{"body": "Migrations successfully executed"}'
|
|
567
|
+
sql:
|
|
568
|
+
- key: main
|
|
569
|
+
engine: mysql
|
|
570
|
+
function:
|
|
571
|
+
- key: runSchemaMigration
|
|
572
|
+
handler: index.handlerMigration
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
### 2. Drop Migrations Trigger
|
|
576
|
+
|
|
577
|
+
⚠️ **WARNING**: This trigger will permanently delete all data in the specified tables and clear the migrations history. This operation cannot be undone!
|
|
578
|
+
|
|
579
|
+
This trigger allows you to completely reset your database schema. It's useful for:
|
|
580
|
+
- Development environments where you need to start fresh
|
|
581
|
+
- Testing scenarios requiring a clean database
|
|
582
|
+
- Resetting the database before applying new migrations
|
|
583
|
+
|
|
584
|
+
**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.
|
|
585
|
+
|
|
586
|
+
```typescript
|
|
587
|
+
// Example usage in your Forge app
|
|
588
|
+
import { dropSchemaMigrations } from "forge-sql-orm";
|
|
589
|
+
import * as schema from "./entities/schema";
|
|
590
|
+
|
|
591
|
+
export const dropMigrations = () => {
|
|
592
|
+
return dropSchemaMigrations(Object.values(schema));
|
|
593
|
+
};
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
Configure in `manifest.yml`:
|
|
597
|
+
```yaml
|
|
598
|
+
webtrigger:
|
|
599
|
+
- key: drop-schema-migration
|
|
600
|
+
function: dropMigrations
|
|
601
|
+
sql:
|
|
602
|
+
- key: main
|
|
603
|
+
engine: mysql
|
|
604
|
+
function:
|
|
605
|
+
- key: dropMigrations
|
|
606
|
+
handler: index.dropMigrations
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
### Important Notes
|
|
610
|
+
|
|
611
|
+
**Security Considerations**:
|
|
612
|
+
- The drop migrations trigger should be restricted to development environments
|
|
613
|
+
- Consider implementing additional authentication for these endpoints
|
|
614
|
+
- Use the `security` section in `manifest.yml` to control access
|
|
615
|
+
|
|
616
|
+
**Best Practices**:
|
|
617
|
+
- Always backup your data before using the drop migrations trigger
|
|
618
|
+
- Test migrations in a development environment first
|
|
619
|
+
- Use these triggers as part of your deployment pipeline
|
|
620
|
+
- Monitor the execution logs in the Forge Developer Console
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
## License
|
|
624
|
+
This project is licensed under the **MIT License**.
|
|
625
|
+
Feel free to use it for commercial and personal projects.
|