forge-sql-orm 1.0.31 → 1.1.31

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.
Files changed (37) hide show
  1. package/README.md +216 -695
  2. package/dist/ForgeSQLORM.js +538 -567
  3. package/dist/ForgeSQLORM.js.map +1 -1
  4. package/dist/ForgeSQLORM.mjs +536 -554
  5. package/dist/ForgeSQLORM.mjs.map +1 -1
  6. package/dist/core/ForgeSQLCrudOperations.d.ts +101 -130
  7. package/dist/core/ForgeSQLCrudOperations.d.ts.map +1 -1
  8. package/dist/core/ForgeSQLORM.d.ts +11 -10
  9. package/dist/core/ForgeSQLORM.d.ts.map +1 -1
  10. package/dist/core/ForgeSQLQueryBuilder.d.ts +271 -113
  11. package/dist/core/ForgeSQLQueryBuilder.d.ts.map +1 -1
  12. package/dist/core/ForgeSQLSelectOperations.d.ts +65 -22
  13. package/dist/core/ForgeSQLSelectOperations.d.ts.map +1 -1
  14. package/dist/core/SystemTables.d.ts +59 -0
  15. package/dist/core/SystemTables.d.ts.map +1 -0
  16. package/dist/index.d.ts +1 -2
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/utils/sqlUtils.d.ts +53 -6
  19. package/dist/utils/sqlUtils.d.ts.map +1 -1
  20. package/dist-cli/cli.js +471 -397
  21. package/dist-cli/cli.js.map +1 -1
  22. package/dist-cli/cli.mjs +471 -397
  23. package/dist-cli/cli.mjs.map +1 -1
  24. package/package.json +21 -22
  25. package/src/core/ForgeSQLCrudOperations.ts +360 -473
  26. package/src/core/ForgeSQLORM.ts +38 -79
  27. package/src/core/ForgeSQLQueryBuilder.ts +250 -133
  28. package/src/core/ForgeSQLSelectOperations.ts +182 -72
  29. package/src/core/SystemTables.ts +7 -0
  30. package/src/index.ts +1 -2
  31. package/src/utils/sqlUtils.ts +155 -23
  32. package/dist/core/ComplexQuerySchemaBuilder.d.ts +0 -38
  33. package/dist/core/ComplexQuerySchemaBuilder.d.ts.map +0 -1
  34. package/dist/knex/index.d.ts +0 -4
  35. package/dist/knex/index.d.ts.map +0 -1
  36. package/src/core/ComplexQuerySchemaBuilder.ts +0 -63
  37. package/src/knex/index.ts +0 -4
package/README.md CHANGED
@@ -2,16 +2,17 @@
2
2
 
3
3
  [![forge-sql-orm CI](https://github.com/vzakharchenko/forge-sql-orm/actions/workflows/node.js.yml/badge.svg)](https://github.com/vzakharchenko/forge-sql-orm/actions/workflows/node.js.yml)
4
4
 
5
- **Forge-SQL-ORM** is an ORM designed for working with [@forge/sql](https://developer.atlassian.com/platform/forge/storage-reference/sql-tutorial/) in **Atlassian Forge**. It is built on top of [MikroORM](https://mikro-orm.io/docs/query-builder) and provides advanced capabilities for working with relational databases inside Forge.
5
+ **Forge-SQL-ORM** is an ORM designed for working with [@forge/sql](https://developer.atlassian.com/platform/forge/storage-reference/sql-tutorial/) in **Atlassian Forge**. It is built on top of [Drizzle ORM](https://orm.drizzle.team) and provides advanced capabilities for working with relational databases inside Forge.
6
6
 
7
7
  ## Key Features
8
- - ✅ **Supports complex SQL queries** with joins and filtering.
9
- - ✅ **Batch insert support** with duplicate key handling.
10
- - ✅ **Schema migration support**, allowing automatic schema evolution.
11
- - ✅ **Automatic entity generation** from MySQL/tidb databases.
12
- - ✅ **Automatic migration generation** from MySQL/tidb databases.
8
+ - ✅ **Supports complex SQL queries** with joins and filtering using Drizzle ORM
9
+ - ✅ **Batch insert support** with duplicate key handling
10
+ - ✅ **Schema migration support**, allowing automatic schema evolution
11
+ - ✅ **Automatic entity generation** from MySQL/tidb databases
12
+ - ✅ **Automatic migration generation** from MySQL/tidb databases
13
13
  - ✅ **Drop Migrations** Generate a migration to drop all tables and clear migrations history for subsequent schema recreation
14
- - ✅ **Optimistic Locking** Ensures data consistency by preventing conflicts when multiple users update the same record.
14
+ - ✅ **Optimistic Locking** Ensures data consistency by preventing conflicts when multiple users update the same record
15
+ - ✅ **Type Safety** Full TypeScript support with proper type inference
15
16
 
16
17
  ## Installation
17
18
 
@@ -22,51 +23,22 @@ Forge-SQL-ORM is designed to work with @forge/sql and requires some additional s
22
23
  ```sh
23
24
  npm install forge-sql-orm -S
24
25
  npm install @forge/sql -S
25
- npm i @mikro-orm/entity-generator -D
26
- npm i mysql -D
26
+ npm install drizzle-orm mysql2
27
+ npm install mysql2 @types/mysql2 -D
27
28
  ```
28
29
 
29
30
  This will:
30
-
31
- Install Forge-SQL-ORM (the ORM for @forge/sql).
32
- Install @forge/sql, the Forge database layer.
33
-
34
- ✅ Step 2: Configure Post-Installation Patch
35
- By default, MikroORM and Knex include some features that are not compatible with Forge's restricted runtime.
36
- To fix this, we need to patch these libraries after installation.
37
-
38
- Run:
39
-
40
- ```sh
41
- npm pkg set scripts.postinstall="forge-sql-orm patch:mikroorm"
42
- ```
43
-
44
- ✅ Step 3: Apply the Patch
45
- After setting up the postinstall script, run:
46
-
47
- ```sh
48
- npm i
49
- ```
50
-
51
- This will:
52
-
53
- Trigger the postinstall hook, which applies the necessary patches to MikroORM and Knex.
54
- Ensure everything is correctly configured for running inside Forge.
55
-
56
- 🔧 Why is the Patch Required?
57
- Atlassian Forge has a restricted execution environment, which does not allow:
58
-
59
- - Dynamic import(id) calls, commonly used in MikroORM.
60
- - Direct file system access, which MikroORM sometimes relies on.
61
- - Unsupported database dialects, such as PostgreSQL or SQLite.
62
- - The patch removes these unsupported features to ensure full compatibility.
31
+ - Install Forge-SQL-ORM (the ORM for @forge/sql)
32
+ - Install @forge/sql, the Forge database layer
33
+ - Install Drizzle ORM and its MySQL driver
34
+ - Install TypeScript types for MySQL
63
35
 
64
36
  ## Step-by-Step Migration Workflow
65
37
 
66
- 1. **Generate initial entity models from an existing database**
38
+ 1. **Generate initial schema from an existing database**
67
39
 
68
40
  ```sh
69
- npx forge-sql-orm generate:model --dbName testDb --output ./database/entities
41
+ npx forge-sql-orm generate:model --dbName testDb --output ./database/schema
70
42
  ```
71
43
 
72
44
  _(This is done only once when setting up the project)_
@@ -74,7 +46,7 @@ Atlassian Forge has a restricted execution environment, which does not allow:
74
46
  2. **Create the first migration**
75
47
 
76
48
  ```sh
77
- npx forge-sql-orm migrations:create --dbName testDb --entitiesPath ./database/entities --output ./database/migration
49
+ npx forge-sql-orm migrations:create --dbName testDb --entitiesPath ./database/schema --output ./database/migration
78
50
  ```
79
51
 
80
52
  _(This initializes the database migration structure, also done once)_
@@ -91,387 +63,28 @@ Atlassian Forge has a restricted execution environment, which does not allow:
91
63
  5. **Update the migration**
92
64
 
93
65
  ```sh
94
- npx forge-sql-orm migrations:update --dbName testDb --entitiesPath ./database/entities --output ./database/migration
66
+ npx forge-sql-orm migrations:update --dbName testDb --entitiesPath ./database/schema --output ./database/migration
95
67
  ```
96
68
 
97
- - ⚠️ **Do NOT update entities before this step!**
98
- - If entities are updated first, the migration will be empty!
69
+ - ⚠️ **Do NOT update schema before this step!**
70
+ - If schema is updated first, the migration will be empty!
99
71
 
100
72
  6. **Deploy to Forge and verify that the migration runs without issues**
101
73
 
102
74
  - Run the updated migration on Forge.
103
75
 
104
- 7. **Update the entity models**
76
+ 7. **Update the schema**
105
77
 
106
78
  ```sh
107
- npx forge-sql-orm generate:model --dbName testDb --output ./database/entities
79
+ npx forge-sql-orm generate:model --dbName testDb --output ./database/schema
108
80
  ```
109
81
 
110
82
  8. **Repeat steps 4-7 as needed**
111
83
 
112
84
  **⚠️ WARNING:**
113
85
 
114
- - **Do NOT swap steps 7 and 5!** If you update models before generating a migration, the migration will be empty!
115
- - Always generate the **migration first**, then update the **entities**.
116
-
117
- ---
118
-
119
- # Connection to ORM
120
-
121
- ```js
122
- import ForgeSQL from "forge-sql-orm";
123
- import { Orders } from "./entities/Orders";
124
- import { Users } from "./entities/Users";
125
- import ENTITIES from "./entities";
126
-
127
- const forgeSQL = new ForgeSQL(ENTITIES);
128
- ```
129
-
130
- - Fetch Data:
131
-
132
- ```js
133
- const formattedQuery = forgeSQL
134
- .createQueryBuilder(Users)
135
- .select("*")
136
- .limit(limit)
137
- .offset(offset)
138
- .getFormattedQuery();
139
- //select `u0`.* from `users` as `u0` limit 10 offset 1
140
- return await forgeSQL.fetch().executeSchemaSQL(formattedQuery, UsersSchema);
141
- ```
142
-
143
- - Raw Fetch Data
144
-
145
- ```js
146
- const users = (await forgeSQL.fetch().executeRawSQL) < Users > "SELECT * FROM users";
147
- ```
148
-
149
- - Complex Query
150
-
151
- ```js
152
- // Define schema for join result
153
- const innerJoinSchema = forgeSQL.fetch().createComplexQuerySchema();
154
- schemaBuilder.addField(Users.meta.properties.name);
155
- schemaBuilder.addField(Orders.meta.properties.product);
156
-
157
- // Execute query
158
- const query = forgeSQL.createQueryBuilder(Orders, "order")
159
- .limit(10).offset(10)
160
- .innerJoin("user", "user")
161
- .select(["user.name", "order.product"])
162
- .getFormattedQuery();
163
- // select `user`.`name`, `order`.`product` from `orders` as `order` inner join `users` as `user` on `order`.`user_id` = `user`.`id` limit 10 offset 10
164
- const results = await forgeSQL.fetch().executeSchemaSQL(query, innerJoinSchema);
165
- console.log(results);
166
- ```
167
-
168
- ## CRUD Operations
169
-
170
- - **Insert Data**
171
-
172
- ```js
173
- // INSERT INTO users (id, name) VALUES (1, 'Smith')
174
- const userId = await forgeSQL.crud().insert(UsersSchema, [{ id: 1, name: "Smith" }]);
175
- ```
176
-
177
- - **Insert Bulk Data**
178
-
179
- ```js
180
- // INSERT INTO users (id, name) VALUES (2, 'Smith'), (3, 'Vasyl')
181
- await forgeSQL.crud().insert(UsersSchema, [
182
- { id: 2, name: "Smith" },
183
- { id: 3, name: "Vasyl" },
184
- ]);
185
- ```
186
-
187
- - **Insert Data with Duplicates**
188
-
189
- ```js
190
- // INSERT INTO users (id, name) VALUES (4, 'Smith'), (4, 'Vasyl')
191
- // ON DUPLICATE KEY UPDATE name = VALUES(name)
192
- await forgeSQL.crud().insert(
193
- UsersSchema,
194
- [
195
- { id: 4, name: "Smith" },
196
- { id: 4, name: "Vasyl" },
197
- ],
198
- true,
199
- );
200
- ```
201
-
202
- - **Update Data by Primary Key**
203
-
204
- ```js
205
- // This uses the updateById method which wraps updateFieldById (with optimistic locking if configured)
206
- await forgeSQL.crud().updateById({ id: 1, name: "Smith Updated" }, UsersSchema);
207
- ```
208
-
209
- - **Update Specific Fields by Primary Key**
210
-
211
- ```js
212
- // Updates specific fields of a record identified by its primary key.
213
- // Note: The primary key field (e.g. id) must be included in the fields array.
214
- await forgeSQL.crud().updateFieldById({ id: 1, name: "Updated Name" }, ["id", "name"], UsersSchema);
215
- ```
216
-
217
- - **Update Fields Without Primary Key and Versioning**
218
-
219
- ```js
220
- // Updates specified fields for records matching the given conditions.
221
- // In this example, the "name" and "age" fields are updated for users where the email is 'smith@example.com'.
222
- const affectedRows = await forgeSQL.crud().updateFields(
223
- { name: "New Name", age: 35, email: "smith@example.com" },
224
- ["name", "age"],
225
- UsersSchema
226
- );
227
- console.log(`Rows affected: ${affectedRows}`);
228
-
229
- // Alternatively, you can provide an explicit WHERE condition:
230
- const affectedRowsWithWhere = await forgeSQL.crud().updateFields(
231
- { name: "New Name", age: 35 },
232
- ["name", "age"],
233
- UsersSchema,
234
- { email: "smith@example.com" }
235
- );
236
- console.log(`Rows affected: ${affectedRowsWithWhere}`);
237
- ```
238
-
239
- - **Delete Data**
240
-
241
- ```js
242
- await forgeSQL.crud().deleteById(1, UsersSchema);
243
- ```
244
-
245
- ## Quick Start
246
-
247
- ### 1. Designing the Database
248
-
249
- You can start by designing a **MySQL/tidb database** using tools like [DbSchema](https://dbschema.com/) or by using an existing MySQL/tidb database.
250
-
251
- **Schema visualization:**
252
- ![](https://github.com/vzakharchenko/forge-sql-orm/blob/master/img/joinSchema.png?raw=true)
253
-
254
- #### DDL Scripts
255
-
256
- ```sql
257
- CREATE DATABASE testDb;
258
- CREATE TABLE testDb.users (
259
- id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
260
- name VARCHAR(200)
261
- ) engine=InnoDB;
262
-
263
- CREATE TABLE testDb.orders (
264
- id INT NOT NULL PRIMARY KEY,
265
- user_id INT NOT NULL ,
266
- product VARCHAR(200)
267
- ) engine=InnoDB;
268
-
269
- ALTER TABLE testDb.orders ADD CONSTRAINT fk_orders_users FOREIGN KEY ( user_id ) REFERENCES testDb.users( id ) ON DELETE NO ACTION ON UPDATE NO ACTION;
270
- ```
271
-
272
- ### 2. Generate Models
273
-
274
- Run the following command to generate entity models based on your database:
275
-
276
- ```sh
277
- npx forge-sql-orm generate:model --host localhost --port 3306 --user root --password secret --dbName testDb --output ./database/entities
278
- ```
279
-
280
- This will generate **entity schemas** in the `./database/entities` directory.
281
- Users Model and Schema:
282
-
283
- ```js
284
- import { EntitySchema } from 'forge-sql-orm';
285
-
286
- export class Users {
287
- id!: number;
288
- name?: string;
289
- }
290
-
291
- export const UsersSchema = new EntitySchema({
292
- class: Users,
293
- properties: {
294
- id: { primary: true, type: 'integer', unsigned: false },
295
- name: { type: 'string', length: 200, nullable: true },
296
- },
297
- });
298
-
299
- ```
300
-
301
- Orders Model and Schema:
302
-
303
- ```js
304
- import { EntitySchema } from 'forge-sql-orm';
305
- import { Users } from './Users';
306
-
307
- export class Orders {
308
- id!: number;
309
- user!: Users;
310
- userId!: number;
311
- product?: string;
312
- }
313
-
314
- export const OrdersSchema = new EntitySchema({
315
- class: Orders,
316
- properties: {
317
- id: { primary: true, type: 'integer', unsigned: false, autoincrement: false },
318
- user: {
319
- kind: 'm:1',
320
- entity: () => Users,
321
- fieldName: 'user_id',
322
- index: 'fk_orders_users',
323
- },
324
- userId: {
325
- type: 'integer',
326
- fieldName: 'user_id',
327
- persist: false,
328
- index: 'fk_orders_users',
329
- },
330
- product: { type: 'string', length: 200, nullable: true },
331
- },
332
- });
333
- ```
334
-
335
- index.ts
336
-
337
- ```js
338
- import { Orders } from "./Orders";
339
- import { Users } from "./Users";
340
-
341
- export default [Orders, Users];
342
- ```
343
-
344
- ### 3. Create the First Migration
345
-
346
- After generating the models, create the first migration file that represents the current database state:
347
-
348
- ```sh
349
- npx forge-sql-orm migrations:create --dbName testDb --entitiesPath ./database/entities --output ./database/migration
350
- ```
351
-
352
- Generated migration:
353
-
354
- ```js
355
- import { MigrationRunner } from "@forge/sql/out/migration";
356
-
357
- export default (migrationRunner: MigrationRunner): MigrationRunner => {
358
- return migrationRunner
359
- .enqueue("v1_MIGRATION0", "create table `users` (`id` int not null auto_increment primary key, `name` varchar(200) null)")
360
- .enqueue("v1_MIGRATION1", "create table `orders` (`id` int not null, `user_id` int not null, `product` varchar(200) null, primary key (`id`))")
361
- .enqueue("v1_MIGRATION2", "alter table `orders` add index `fk_orders_users`(`user_id`)");
362
- };
363
- ```
364
-
365
- ### 4. Deploy and Run Migrations
366
-
367
- ```js
368
- import migration from "./database/migration";
369
- import sql, { migrationRunner } from "@forge/sql";
370
-
371
- export const trigger = async () => {
372
- console.log("Provisioning the database");
373
- await sql._provision();
374
-
375
- console.log("Running schema migrations");
376
- const migrations = await migration(migrationRunner);
377
- const successfulMigrations = await migrations.run();
378
- console.log("Migrations applied:", successfulMigrations);
379
-
380
- const migrationHistory = (await migrationRunner.list())
381
- .map((y) => `${y.id}, ${y.name}, ${y.migratedAt.toUTCString()}`)
382
- .join("\n");
383
-
384
- console.log("Migrations history:\nid, name, migrated_at\n", migrationHistory);
385
-
386
- return {
387
- headers: { "Content-Type": ["application/json"] },
388
- statusCode: 200,
389
- statusText: "OK",
390
- body: "Migrations successfully executed",
391
- };
392
- };
393
- ```
394
-
395
- ### 5. Running Queries
396
-
397
- Once the migrations are applied, you can start working with the ORM:
398
-
399
- ```js
400
- import Entities from "./entities";
401
- import ForgeSQL from "forge-sql-orm";
402
- import { UsersSchema, Users } from "./entities/Users";
403
-
404
- const forgeSQL = new ForgeSQL(ENTITIES);
405
-
406
- // Insert Data
407
- const user = new Users();
408
- user.name = "John Doe";
409
- const userId = await forgeSQL.crud().insert(UsersSchema, [user]);
410
- console.log("Inserted User ID:", userId);
411
-
412
- // Fetch Users
413
- const users = await forgeSQL.fetch().executeSchemaSQL("SELECT * FROM users", UsersSchema);
414
- console.log(users);
415
- ```
416
-
417
- ### 6. Updating Database Schema
418
-
419
- If you modify the database schema, you can generate an update migration:
420
-
421
- Modify the schema in DbSchema
422
- ![](https://github.com/vzakharchenko/forge-sql-orm/blob/master/img/joinSchema2.png?raw=true)
423
- or manually run:
424
-
425
- ```sh
426
- ALTER TABLE `users` ADD email VARCHAR(255);
427
- ```
428
-
429
- Then, generate a new migration:
430
-
431
- ```sh
432
- npx forge-sql-orm migrations:update --dbName testDb --entitiesPath ./database/entities --output ./database/migration
433
- ```
434
-
435
- Generated migration:
436
-
437
- ```js
438
- import { MigrationRunner } from "@forge/sql/out/migration";
439
-
440
- export default (migrationRunner: MigrationRunner): MigrationRunner => {
441
- return migrationRunner.enqueue("v2_MIGRATION0", "alter table `users` add `email` varchar(255) null");
442
- };
443
- ```
444
-
445
- ### 7. Updating Entities
446
-
447
- After applying the migration, update your entity models:
448
-
449
- ```sh
450
- npx forge-sql-orm generate:model --dbName testDb --output ./database/entities
451
- ```
452
-
453
- Updated Users Model and Schema:
454
-
455
- ```js
456
- import { EntitySchema } from 'forge-sql-orm';
457
-
458
- export class Users {
459
- id!: number;
460
- name?: string;
461
- email?: string;
462
- }
463
-
464
- export const UsersSchema = new EntitySchema({
465
- class: Users,
466
- properties: {
467
- id: { primary: true, type: 'integer', unsigned: false },
468
- name: { type: 'string', length: 200, nullable: true },
469
- email: { type: 'string', nullable: true },
470
- },
471
- });
472
-
473
- ```
474
- ---
86
+ - **Do NOT swap steps 7 and 5!** If you update schema before generating a migration, the migration will be empty!
87
+ - Always generate the **migration first**, then update the **schema**.
475
88
 
476
89
  ## Drop Migrations
477
90
 
@@ -479,25 +92,25 @@ The Drop Migrations feature allows you to completely reset your database schema
479
92
  - Start fresh with a new schema
480
93
  - Reset all tables and their data
481
94
  - Clear migration history
482
- - Ensure your local models match the deployed database
95
+ - Ensure your local schema matches the deployed database
483
96
 
484
97
  ### Important Requirements
485
98
 
486
99
  Before using Drop Migrations, ensure that:
487
- 1. Your local entity models exactly match the current database schema deployed in Atlassian Forge SQL
100
+ 1. Your local schema exactly matches the current database schema deployed in Atlassian Forge SQL
488
101
  2. You have a backup of your data if needed
489
102
  3. You understand that this operation will delete all tables and data
490
103
 
491
104
  ### Usage
492
105
 
493
- 1. First, ensure your local models match the deployed database:
106
+ 1. First, ensure your local schema matches the deployed database:
494
107
  ```bash
495
- npx forge-sql-orm generate:model --output ./database/entities
108
+ npx forge-sql-orm generate:model --output ./database/schema
496
109
  ```
497
110
 
498
111
  2. Generate the drop migration:
499
112
  ```bash
500
- npx forge-sql-orm migrations:drop --entitiesPath ./database/entities --output ./database/migration
113
+ npx forge-sql-orm migrations:drop --entitiesPath ./database/schema --output ./database/migration
501
114
  ```
502
115
 
503
116
  3. Deploy and run the migration in your Forge app:
@@ -512,7 +125,7 @@ Before using Drop Migrations, ensure that:
512
125
 
513
126
  4. After dropping all tables, you can create a new migration to recreate the schema:
514
127
  ```bash
515
- npx forge-sql-orm migrations:create --entitiesPath ./database/entities --output ./database/migration --force
128
+ npx forge-sql-orm migrations:create --entitiesPath ./database/schema --output ./database/migration --force
516
129
  ```
517
130
  The `--force` parameter is required here because we're creating a new migration after dropping all tables.
518
131
 
@@ -524,8 +137,10 @@ import { MigrationRunner } from "@forge/sql/out/migration";
524
137
 
525
138
  export default (migrationRunner: MigrationRunner): MigrationRunner => {
526
139
  return migrationRunner
527
- .enqueue("v1_MIGRATION0", "DROP TABLE IF EXISTS `users`")
528
- .enqueue("v1_MIGRATION1", "DROP TABLE IF EXISTS `orders`")
140
+ .enqueue("v1_MIGRATION0", "ALTER TABLE `orders` DROP FOREIGN KEY `fk_orders_users`")
141
+ .enqueue("v1_MIGRATION1", "DROP INDEX `idx_orders_user_id` ON `orders`")
142
+ .enqueue("v1_MIGRATION2", "DROP TABLE IF EXISTS `orders`")
143
+ .enqueue("v1_MIGRATION3", "DROP TABLE IF EXISTS `users`")
529
144
  .enqueue("MIGRATION_V1_1234567890", "DELETE FROM __migrations");
530
145
  };
531
146
  ```
@@ -533,339 +148,245 @@ export default (migrationRunner: MigrationRunner): MigrationRunner => {
533
148
  ### ⚠️ Important Notes
534
149
 
535
150
  - This operation is **irreversible** - all data will be lost
536
- - Make sure your local models are up-to-date with the deployed database
151
+ - Make sure your local schema is up-to-date with the deployed database
537
152
  - Consider backing up your data before running drop migrations
538
153
  - The migration will clear the `__migrations` table to allow for fresh migration history
154
+ - Drop operations are performed in the correct order: first foreign keys, then indexes, then tables
539
155
 
540
156
  ---
541
157
 
542
- ## Complex Queries
543
-
544
- ### INNER JOIN Example
545
-
546
- Using **ForgeSQLORM**, you can perform complex queries with joins.
547
- For example, fetching **users and their purchased products**:
548
-
549
- ```ts
550
- import ForgeSQL, { EntitySchema } from "forge-sql-orm";
551
- import { Orders } from "./entities/Orders";
552
- import { Users } from "./entities/Users";
553
- import ENTITIES from "./entities";
554
-
555
- const forgeSQL = new ForgeSQL(ENTITIES);
556
-
557
- // Define schema for join result
558
- class InnerJoinResult {
559
- name!: string;
560
- product!: string;
561
- }
562
-
563
- export const innerJoinSchema = new EntitySchema<InnerJoinResult>({
564
- class: InnerJoinResult,
565
- properties: {
566
- name: { type: "string", fieldName: "name" },
567
- product: { type: "string", fieldName: "product" },
568
- },
569
- });
570
- innerJoinSchema.init();
571
-
572
- // Execute query
573
- const query = forgeSQL
574
- .createQueryBuilder(Orders, "order")
575
- .limit(10)
576
- .offset(0)
577
- .innerJoin("user", "user")
578
- .select(["user.name", "order.product"])
579
- .getFormattedQuery();
580
-
581
- const results = await forgeSQL.fetch().executeSchemaSQL(query, innerJoinSchema);
582
- console.log(results);
583
- ```
584
-
585
- ---
586
-
587
- ## ForgeSqlOrmOptions
588
-
589
- The `ForgeSqlOrmOptions` object allows customization of ORM behavior. Currently, it supports the following options:
590
-
591
- | Option | Type | Description |
592
- | -------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
593
- | `logRawSqlQuery` | `boolean` | Enables logging of raw SQL queries in the Atlassian Forge Developer Console. Useful for debugging and monitoring. Defaults to `false`. |
594
- | `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. |
595
-
596
- ---
597
-
598
- ### Example: Initializing `ForgeSQL` with Options
158
+ # Connection to ORM
599
159
 
600
- ```typescript
160
+ ```js
601
161
  import ForgeSQL from "forge-sql-orm";
602
- import { Orders } from "./entities/Orders";
603
- import { Users } from "./entities/Users";
604
- import ENTITIES from "./entities";
605
162
 
606
- const options = {
607
- logRawSqlQuery: true, // Enable query logging for debugging purposes
608
- };
609
-
610
- const forgeSQL = new ForgeSQL(ENTITIES, options);
163
+ const forgeSQL = new ForgeSQL();
611
164
  ```
612
165
 
613
- ## Using `getKnex()` for Advanced SQL Queries
614
-
615
- The `getKnex()` method allows direct interaction with Knex.js, enabling execution of raw SQL queries and complex query building.
166
+ ## Fetch Data
616
167
 
617
- ### Example: Finding Duplicate Records in `UsersSchema`
618
-
619
- ```typescript
620
- const fields: string[] = ["name", "email"];
621
-
622
- // Define selected fields, including a count of duplicate occurrences
623
- const selectFields: Array<string | Knex.Raw> = [
624
- ...fields,
625
- forgeSQL.getKnex().raw("COUNT(*) as count"),
626
- ];
627
-
628
- // Create a QueryBuilder with grouping and filtering for duplicates
629
- let selectQueryBuilder = forgeSQL
630
- .createQueryBuilder(UsersSchema)
631
- .select(selectFields as unknown as string[])
632
- .groupBy(fields)
633
- .having("COUNT(*) > 1");
634
-
635
- // Generate the final SQL query with ordering by count
636
- const query = selectQueryBuilder.getKnexQuery().orderByRaw("count ASC").toSQL().sql;
637
-
638
- /*
639
- SQL Query:
640
- SELECT `u0`.`name`, `u0`.`email`, COUNT(*) as count
641
- FROM `users` AS `u0`
642
- GROUP BY `u0`.`name`, `u0`.`email`
643
- HAVING COUNT(*) > 1
644
- ORDER BY count ASC;
645
- */
646
-
647
- // Execute the SQL query and retrieve results
648
- const duplicateResult = await forgeSQL
649
- .fetch()
650
- .executeSchemaSQL<DuplicateResult>(query, DuplicateSchema);
651
- ```
652
-
653
- 🔹 **What does this example do?**
654
-
655
- 1. Selects `name` and `email`, along with the count of duplicate occurrences (`COUNT(*) as count`).
656
- 2. Groups the data by `name` and `email` to identify duplicates.
657
- 3. Filters the results to include only groups with more than one record (`HAVING COUNT(*) > 1`).
658
- 4. Sorts the final results in ascending order by count (`ORDER BY count ASC`).
659
- 5. Executes the SQL query and returns the duplicate records.
660
-
661
- ---
662
-
663
- ## Optimistic Locking
664
-
665
- 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. Each time an update occurs, the current version is checked, and if it doesn't match the stored version, the update is rejected. This ensures data consistency and helps avoid accidental overwrites.
666
-
667
- ### How It Works
668
-
669
- - **Version Field:**
670
- A specific field in your entity schema is designated to track the version of the record. This field is marked with the flag `version: true`.
671
-
672
- - **Supported Types:**
673
- The version field must be of type `datetime`, `timestamp`, `integer`, or `decimal` and must be non-nullable. If the field's type does not meet these requirements, a warning message will be logged to the console during model generation.
674
-
675
- - **Automatic Configuration:**
676
- When generating models, you can specify a field (e.g., `updatedAt`) that automatically becomes the version field by using the `--versionField` flag. For example:
677
- ```sh
678
- npx forge-sql-orm generate:model --versionField updatedAt
679
- ```
680
- In this case, any model that includes a field named `updatedAt` meeting the required conditions will have it configured for optimistic locking.
681
-
682
- ### Example
683
-
684
- Here's how you can define an entity with optimistic locking using MikroORM:
168
+ ### Basic Fetch Operations
685
169
 
686
170
  ```js
687
- export class TestEntityVersion {
688
- id!: number;
689
- name?: string;
690
- version!: number;
691
- }
692
-
693
- export const TestEntityVersionSchema = new EntitySchema({
694
- class: TestEntityVersion,
695
- properties: {
696
- id: { primary: true, type: "integer", unsigned: false, autoincrement: false },
697
- name: { type: "string", nullable: true },
698
- version: { type: "integer", nullable: false, version: true },
699
- },
700
- });
701
- ```
702
-
703
- In this example, the `version` field is used to track changes. Every update will check the current version to ensure that no conflicting modifications occur.
704
-
705
- ---
171
+ // Using executeQuery for single result
172
+ const user = await forgeSQL
173
+ .fetch()
174
+ .executeQuery(
175
+ forgeSQL.getDrizzleQueryBuilder()
176
+ .select("*").from(Users)
177
+ .where(eq(Users.id, 1))
178
+ );
179
+ // Returns: { id: 1, name: "John Doe" }
706
180
 
707
- ## Usage with MikroORM Generator
181
+ // Using executeQueryOnlyOne for single result with error handling
182
+ const user = await forgeSQL
183
+ .fetch()
184
+ .executeQueryOnlyOne(
185
+ forgeSQL
186
+ .getDrizzleQueryBuilder()
187
+ .select("*").from(Users)
188
+ .where(eq(Users.id, 1))
189
+ );
190
+ // Returns: { id: 1, name: "John Doe" }
191
+ // Throws error if multiple records found
192
+ // Returns undefined if no records found
708
193
 
709
- If you prefer to use MikroORM's default entity generator, then manually import your entities:
194
+ // Using executeQuery with aliases
195
+ const usersAlias = alias(Users, "u");
196
+ const result = await forgeSQL
197
+ .fetch()
198
+ .executeQuery(
199
+ forgeSQL
200
+ .getDrizzleQueryBuilder()
201
+ .select({
202
+ userId: rawSql`${usersAlias.id} as \`userId\``,
203
+ userName: rawSql`${usersAlias.name} as \`userName\``
204
+ }).from(usersAlias)
205
+ );
206
+ // Returns: { userId: 1, userName: "John Doe" }
710
207
 
711
- ```ts
712
- import { UserEntity, TaskEntity } from "./entities";
208
+ // Using executeQuery with joins
209
+ const orderWithUser = await forgeSQL
210
+ .fetch()
211
+ .executeQuery(
212
+ forgeSQL
213
+ .getDrizzleQueryBuilder()
214
+ .select({
215
+ orderId: rawSql`${Orders.id} as \`orderId\``,
216
+ product: Orders.product,
217
+ userName: rawSql`${Users.name} as \`userName\``
218
+ }).from(Orders)
219
+ .innerJoin(Users, eq(Orders.userId, Users.id))
220
+ .where(eq(Orders.id, 1))
221
+ );
222
+ // Returns: { orderId: 1, product: "Product 1", userName: "John Doe" }
713
223
  ```
714
224
 
715
- ---
716
-
717
- ## Forge SQL ORM CLI Documentation
718
-
719
- The CLI provides commands to generate models and manage migrations for MikroORM in Forge.
720
-
721
- ---
722
-
723
- ### 📌 Available Commands
724
-
725
- ```sh
726
- $ npx forge-sql-orm --help
727
-
728
- Usage: forge-sql-orm [options] [command]
225
+ ### Complex Queries with Aggregations
729
226
 
730
- Options:
731
- -V, --version Output the version number
732
- -h, --help Display help for command
227
+ ```js
228
+ // Finding duplicates
229
+ const duplicates = await forgeSQL
230
+ .fetch()
231
+ .executeQuery(
232
+ forgeSQL
233
+ .getDrizzleQueryBuilder()
234
+ .select({
235
+ name: Users.name,
236
+ count: rawSql`COUNT(*) as \`count\``
237
+ }).from(Users)
238
+ .groupBy(Users.name)
239
+ .having(rawSql`COUNT(*) > 1`)
240
+ );
241
+ // Returns: { name: "John Doe", count: 2 }
733
242
 
734
- Commands:
735
- generate:model [options] Generate MikroORM models from the database.
736
- migrations:create [options] Generate an initial migration for the entire database.
737
- migrations:update [options] Generate a migration to update the database schema.
738
- patch:mikroorm Patch MikroORM and Knex dependencies to work properly with Forge.
739
- help [command] Display help for a specific command.
243
+ // Using executeQueryOnlyOne for unique results
244
+ const userStats = await forgeSQL
245
+ .fetch()
246
+ .executeQueryOnlyOne(
247
+ forgeSQL
248
+ .getDrizzleQueryBuilder()
249
+ .select({
250
+ totalUsers: rawSql`COUNT(*) as \`totalUsers\``,
251
+ uniqueNames: rawSql`COUNT(DISTINCT name) as \`uniqueNames\``
252
+ }).from(Users)
253
+ );
254
+ // Returns: { totalUsers: 100, uniqueNames: 80 }
255
+ // Throws error if multiple records found
740
256
  ```
741
257
 
742
- ---
743
-
744
- ### 📌 Entity Generation
258
+ ### Raw SQL Queries
745
259
 
746
- ```sh
747
- npx forge-sql-orm generate:model --host localhost --port 3306 --user root --password secret --dbName mydb --output ./src/database/entities --versionField updatedAt --saveEnv
260
+ ```js
261
+ // Using executeRawSQL for direct SQL queries
262
+ const users = await forgeSQL
263
+ .fetch()
264
+ .executeRawSQL<Users>("SELECT * FROM users");
748
265
  ```
749
266
 
750
- This command will:
751
-
752
- - Connect to `mydb` on `localhost:3306`.
753
- - Generate MikroORM entity classes.
754
- - Save them in `./src/database/entities`.
755
- - Create an `index.ts` file with all entities.
756
- - **`--versionField updatedAt`**: Specifies the field used for entity versioning.
757
- - **`--saveEnv`**: Saves configuration settings to `.env` for future use.
758
-
759
- #### 🔹 VersionField Explanation
760
-
761
- The `--versionField` option is crucial for handling entity versioning. It should be a field of type `datetime`, `integer`, or `decimal`. This field is used to track changes to entities, ensuring that updates follow proper versioning strategies.
762
-
763
- **Example:**
764
-
765
- - `updatedAt` (datetime) - Commonly used for timestamp-based versioning.
766
- - `versionNumber` (integer) - Can be used for numeric version increments.
767
-
768
- If the specified field does not meet the required criteria, warnings will be logged.
267
+ ## CRUD Operations
769
268
 
770
- ---
269
+ ### Insert Operations
771
270
 
772
- ### 📌 Database Migrations
271
+ ```js
272
+ // Single insert
273
+ const userId = await forgeSQL.crud().insert(Users, [{ id: 1, name: "Smith" }]);
773
274
 
774
- ```sh
775
- npx forge-sql-orm migrations:create --host localhost --port 3306 --user root --password secret --dbName mydb --output ./src/database/migration --entitiesPath ./src/database/entities --saveEnv
776
- ```
275
+ // Bulk insert
276
+ await forgeSQL.crud().insert(Users, [
277
+ { id: 2, name: "Smith" },
278
+ { id: 3, name: "Vasyl" },
279
+ ]);
777
280
 
778
- This command will:
281
+ // Insert with duplicate handling
282
+ await forgeSQL.crud().insert(
283
+ Users,
284
+ [
285
+ { id: 4, name: "Smith" },
286
+ { id: 4, name: "Vasyl" },
287
+ ],
288
+ true
289
+ );
290
+ ```
779
291
 
780
- - Create the initial migration based on all detected entities.
781
- - Save migration files in `./src/database/migration`.
782
- - Create `index.ts` for automatic migration execution.
783
- - **`--saveEnv`**: Saves configuration settings to `.env` for future use.
292
+ ### Update Operations
784
293
 
785
- ---
294
+ ```js
295
+ // Update by ID with optimistic locking
296
+ await forgeSQL.crud().updateById({ id: 1, name: "Smith Updated" }, Users);
786
297
 
787
- ### 📌 Update Schema Migration
298
+ // Update specific fields
299
+ await forgeSQL.crud().updateById(
300
+ { id: 1, age: 35 },
301
+ Users
302
+ );
788
303
 
789
- ```sh
790
- npx forge-sql-orm migrations:update --host localhost --port 3306 --user root --password secret --dbName mydb --output ./src/database/migration --entitiesPath ./src/database/entities --saveEnv
304
+ // Update with custom WHERE condition
305
+ await forgeSQL.crud().updateFields(
306
+ { name: "New Name", age: 35 },
307
+ Users,
308
+ eq(Users.email, "smith@example.com")
309
+ );
791
310
  ```
792
311
 
793
- This command will:
794
-
795
- - Detect schema changes (new tables, columns, indexes).
796
- - Generate only the required migrations.
797
- - Update `index.ts` to include new migrations.
798
- - **`--saveEnv`**: Saves configuration settings to `.env` for future use.
799
-
800
- ---
801
-
802
- ### 📌 Using the patch:mikroorm Command
803
-
804
- If needed, you can manually apply the patch at any time using:
312
+ ### Delete Operations
805
313
 
806
- ```sh
807
- npx forge-sql-orm patch:mikroorm
314
+ ```js
315
+ // Delete by ID
316
+ await forgeSQL.crud().deleteById(1, Users);
808
317
  ```
809
318
 
810
- This command:
319
+ ## Optimistic Locking
811
320
 
812
- - Removes unsupported database dialects (e.g., PostgreSQL, SQLite).
813
- - Fixes dynamic imports to work in Forge.
814
- - Ensures Knex and MikroORM work properly inside Forge.
321
+ 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.
815
322
 
816
- ---
323
+ ### Supported Version Field Types
817
324
 
818
- ### 📌 Configuration Methods
325
+ - `datetime` - Timestamp-based versioning
326
+ - `timestamp` - Timestamp-based versioning
327
+ - `integer` - Numeric version increment
328
+ - `decimal` - Numeric version increment
819
329
 
820
- You can define database credentials using:
330
+ ### Configuration
821
331
 
822
- 1️⃣ **Command-line arguments**:
332
+ ```typescript
333
+ const options = {
334
+ additionalMetadata: {
335
+ users: {
336
+ tableName: "users",
337
+ versionField: {
338
+ fieldName: "updatedAt",
339
+ }
340
+ }
341
+ }
342
+ };
823
343
 
824
- ```sh
825
- --host, --port, --user, --password, --dbName, --output, --versionField, --saveEnv
344
+ const forgeSQL = new ForgeSQL(options);
826
345
  ```
827
346
 
828
- 2️⃣ **Environment variables**:
347
+ ### Example Usage
829
348
 
830
- ```bash
831
- export FORGE_SQL_ORM_HOST=localhost
832
- export FORGE_SQL_ORM_PORT=3306
833
- export FORGE_SQL_ORM_USER=root
834
- export FORGE_SQL_ORM_PASSWORD=secret
835
- export FORGE_SQL_ORM_DBNAME=mydb
349
+ ```typescript
350
+ // The version field will be automatically handled
351
+ await forgeSQL.crud().updateById(
352
+ {
353
+ id: 1,
354
+ name: "Updated Name",
355
+ updatedAt: new Date() // Will be automatically set if not provided
356
+ },
357
+ Users
358
+ );
836
359
  ```
837
360
 
838
- 3️⃣ **Using a `.env` file**:
361
+ ## ForgeSqlOrmOptions
839
362
 
840
- ```sh
841
- FORGE_SQL_ORM_HOST=localhost
842
- FORGE_SQL_ORM_PORT=3306
843
- FORGE_SQL_ORM_USER=root
844
- FORGE_SQL_ORM_PASSWORD=secret
845
- FORGE_SQL_ORM_DBNAME=mydb
846
- ```
363
+ The `ForgeSqlOrmOptions` object allows customization of ORM behavior:
847
364
 
848
- 4️⃣ **Interactive prompts** (if missing parameters, the CLI will ask for input).
365
+ | Option | Type | Description |
366
+ | -------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
367
+ | `logRawSqlQuery` | `boolean` | Enables logging of raw SQL queries in the Atlassian Forge Developer Console. Useful for debugging and monitoring. Defaults to `false`. |
368
+ | `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. |
369
+ | `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. |
849
370
 
850
- ---
371
+ ## CLI Commands
851
372
 
852
- ### 📌 Manual Migration Execution
373
+ ```sh
374
+ $ npx forge-sql-orm --help
853
375
 
854
- To manually execute migrations in your application:
376
+ Usage: forge-sql-orm [options] [command]
855
377
 
856
- ```js
857
- import migrationRunner from "./src/database/migration";
858
- import { MigrationRunner } from "@forge/sql/out/migration";
378
+ Options:
379
+ -V, --version Output the version number
380
+ -h, --help Display help for command
859
381
 
860
- const runner = new MigrationRunner();
861
- await migrationRunner(runner);
862
- await runner.run(); // Apply migrations
382
+ Commands:
383
+ generate:model [options] Generate Drizzle models from the database
384
+ migrations:create [options] Generate an initial migration for the entire database
385
+ migrations:update [options] Generate a migration to update the database schema
386
+ migrations:drop [options] Generate a migration to drop all tables
387
+ help [command] Display help for a specific command
863
388
  ```
864
389
 
865
- This approach allows you to apply migrations programmatically in a Forge application.
866
-
867
- ---
868
-
869
- 📜 **License**
390
+ ## License
870
391
  This project is licensed under the **MIT License**.
871
392
  Feel free to use it for commercial and personal projects.