forge-sql-orm 2.0.0 → 2.0.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 CHANGED
@@ -5,6 +5,7 @@
5
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
+ - ✅ **Custom Drizzle Driver** for direct integration with @forge/sql
8
9
  - ✅ **Supports complex SQL queries** with joins and filtering using Drizzle ORM
9
10
  - ✅ **Batch insert support** with duplicate key handling
10
11
  - ✅ **Schema migration support**, allowing automatic schema evolution
@@ -14,6 +15,23 @@
14
15
  - ✅ **Optimistic Locking** Ensures data consistency by preventing conflicts when multiple users update the same record
15
16
  - ✅ **Type Safety** Full TypeScript support with proper type inference
16
17
 
18
+ ## Usage Approaches
19
+
20
+ ### 1. Direct Drizzle Usage
21
+ ```typescript
22
+ import { drizzle } from "drizzle-orm/mysql-core";
23
+ import { forgeDriver } from "forge-sql-orm";
24
+ const db = drizzle(forgeDriver);
25
+ ```
26
+ Best for: Simple CRUD operations without optimistic locking
27
+
28
+ ### 2. Full Forge-SQL-ORM Usage
29
+ ```typescript
30
+ import ForgeSQL from "forge-sql-orm";
31
+ const forgeSQL = new ForgeSQL();
32
+ ```
33
+ Best for: Advanced features like optimistic locking and automatic versioning
34
+
17
35
  ## Installation
18
36
 
19
37
  Forge-SQL-ORM is designed to work with @forge/sql and requires some additional setup to ensure compatibility within Atlassian Forge.
@@ -22,8 +40,7 @@ Forge-SQL-ORM is designed to work with @forge/sql and requires some additional s
22
40
 
23
41
  ```sh
24
42
  npm install forge-sql-orm -S
25
- npm install @forge/sql -S
26
- npm install drizzle-orm mysql2
43
+ npm install @forge/sql drizzle-orm -S
27
44
  npm install mysql2 @types/mysql2 -D
28
45
  ```
29
46
 
@@ -33,6 +50,38 @@ This will:
33
50
  - Install Drizzle ORM and its MySQL driver
34
51
  - Install TypeScript types for MySQL
35
52
 
53
+ ## Direct Drizzle Usage with Custom Driver
54
+
55
+ 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:
56
+
57
+ ```typescript
58
+ import { drizzle } from "drizzle-orm/mysql-core";
59
+ import { forgeDriver } from "forge-sql-orm";
60
+
61
+ // Initialize drizzle with the custom driver
62
+ const db = drizzle(forgeDriver);
63
+
64
+ // Use drizzle directly
65
+ const users = await db.select().from(users);
66
+ ```
67
+
68
+ ## Drizzle Usage with forge-sql-orm
69
+
70
+ If you prefer to use Drizzle ORM with the additional features of Forge-SQL-ORM (like optimistic locking), you can use the custom driver:
71
+
72
+ ```typescript
73
+ import ForgeSQL from "forge-sql-orm";
74
+ const forgeSQL = new ForgeSQL();
75
+ forgeSQL.crud().insert(...);
76
+ forgeSQL.crud().updateById(...);
77
+ const db = forgeSQL.getDrizzleQueryBuilder();
78
+
79
+ // Use drizzle
80
+ const users = await db.select().from(users);
81
+ ```
82
+
83
+ This approach gives you direct access to all Drizzle ORM features while still using the @forge/sql backend.
84
+
36
85
  ## Step-by-Step Migration Workflow
37
86
 
38
87
  1. **Generate initial schema from an existing database**
@@ -155,6 +204,68 @@ export default (migrationRunner: MigrationRunner): MigrationRunner => {
155
204
 
156
205
  ---
157
206
 
207
+ ## Date and Time Types
208
+
209
+ When working with date and time fields in your models, you should use the custom types provided by Forge-SQL-ORM to ensure proper handling of date/time values. This is necessary because Forge SQL has specific format requirements for date/time values:
210
+
211
+ | Date type | Required Format | Example |
212
+ |-----------|----------------|---------|
213
+ | DATE | YYYY-MM-DD | 2024-09-19 |
214
+ | TIME | HH:MM:SS[.fraction] | 06:40:34 |
215
+ | TIMESTAMP | YYYY-MM-DD HH:MM:SS[.fraction] | 2024-09-19 06:40:34.999999 |
216
+
217
+ ```typescript
218
+ // ❌ Don't use standard Drizzle date/time types
219
+ export const testEntityTimeStampVersion = mysqlTable('test_entity', {
220
+ id: int('id').primaryKey().autoincrement(),
221
+ time_stamp: timestamp('times_tamp').notNull(),
222
+ date_time: datetime('date_time').notNull(),
223
+ time: time('time').notNull(),
224
+ date: date('date').notNull(),
225
+ });
226
+
227
+ // ✅ Use Forge-SQL-ORM custom types instead
228
+ import { forgeDateTimeString, forgeDateString, mySqlTimestampString, mySqlTimeString } from 'forge-sql-orm'
229
+
230
+ export const testEntityTimeStampVersion = mysqlTable('test_entity', {
231
+ id: int('id').primaryKey().autoincrement(),
232
+ time_stamp: forgeTimestampString('times_tamp').notNull(),
233
+ date_time: forgeDateTimeString('date_time').notNull(),
234
+ time: forgeTimeString('time').notNull(),
235
+ date: forgeDateString('date').notNull(),
236
+ });
237
+ ```
238
+
239
+ ### Why Custom Types?
240
+
241
+ The custom types in Forge-SQL-ORM handle the conversion between JavaScript Date objects and Forge SQL's required string formats automatically. Without these custom types, you would need to manually format dates like this:
242
+
243
+ ```typescript
244
+ // Without custom types, you'd need to do this manually:
245
+ const date = moment().format("YYYY-MM-DD");
246
+ const time = moment().format("HH:mm:ss.SSS");
247
+ const timestamp = moment().format("YYYY-MM-DDTHH:mm:ss.SSS");
248
+ ```
249
+
250
+ Our custom types provide:
251
+ - Automatic conversion between JavaScript Date objects and Forge SQL's required string formats
252
+ - Consistent date/time handling across your application
253
+ - Type safety for date/time fields
254
+ - Proper handling of timezone conversions
255
+ - Support for all Forge SQL date/time types (datetime, timestamp, date, time)
256
+
257
+ ### Available Custom Types
258
+
259
+ - `forgeDateTimeString` - For datetime fields (YYYY-MM-DD HH:MM:SS[.fraction])
260
+ - `forgeTimestampString` - For timestamp fields (YYYY-MM-DD HH:MM:SS[.fraction])
261
+ - `forgeDateString` - For date fields (YYYY-MM-DD)
262
+ - `forgeTimeString` - For time fields (HH:MM:SS[.fraction])
263
+
264
+ Each type ensures that the data is properly formatted according to Forge SQL's requirements while providing a clean, type-safe interface for your application code.
265
+
266
+
267
+
268
+
158
269
  # Connection to ORM
159
270
 
160
271
  ```js
@@ -162,20 +273,35 @@ import ForgeSQL from "forge-sql-orm";
162
273
 
163
274
  const forgeSQL = new ForgeSQL();
164
275
  ```
276
+ or
277
+
278
+ ```typescript
279
+ import { drizzle } from "drizzle-orm/mysql-core";
280
+ import { forgeDriver } from "forge-sql-orm";
281
+
282
+ // Initialize drizzle with the custom driver
283
+ const db = drizzle(forgeDriver);
284
+
285
+ // Use drizzle directly
286
+ const users = await db.select().from(users);
287
+ ```
165
288
 
166
289
  ## Fetch Data
167
290
 
168
291
  ### Basic Fetch Operations
169
292
 
170
293
  ```js
171
- // Using executeQuery for single result
294
+ // Using forgeSQL.getDrizzleQueryBuilder()
172
295
  const user = await forgeSQL
173
- .fetch()
174
- .executeQuery(
175
- forgeSQL.getDrizzleQueryBuilder()
176
- .select("*").from(Users)
177
- .where(eq(Users.id, 1))
178
- );
296
+ .getDrizzleQueryBuilder()
297
+ .select("*").from(Users)
298
+ .where(eq(Users.id, 1));
299
+
300
+ // OR using direct drizzle with custom driver
301
+ const db = drizzle(forgeDriver);
302
+ const user = await db
303
+ .select("*").from(Users)
304
+ .where(eq(Users.id, 1));
179
305
  // Returns: { id: 1, name: "John Doe" }
180
306
 
181
307
  // Using executeQueryOnlyOne for single result with error handling
@@ -191,34 +317,47 @@ const user = await forgeSQL
191
317
  // Throws error if multiple records found
192
318
  // Returns undefined if no records found
193
319
 
194
- // Using executeQuery with aliases
320
+ // Using with aliases
321
+ // With forgeSQL
195
322
  const usersAlias = alias(Users, "u");
196
323
  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
- );
324
+ .getDrizzleQueryBuilder()
325
+ .select({
326
+ userId: rawSql`${usersAlias.id} as \`userId\``,
327
+ userName: rawSql`${usersAlias.name} as \`userName\``
328
+ }).from(usersAlias);
329
+
330
+ // OR with direct drizzle
331
+ const db = drizzle(forgeDriver);
332
+ const result = await db
333
+ .select({
334
+ userId: rawSql`${usersAlias.id} as \`userId\``,
335
+ userName: rawSql`${usersAlias.name} as \`userName\``
336
+ }).from(usersAlias);
206
337
  // Returns: { userId: 1, userName: "John Doe" }
207
338
 
208
- // Using executeQuery with joins
339
+ // Using joins
340
+ // With forgeSQL
209
341
  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
- );
342
+ .getDrizzleQueryBuilder()
343
+ .select({
344
+ orderId: rawSql`${Orders.id} as \`orderId\``,
345
+ product: Orders.product,
346
+ userName: rawSql`${Users.name} as \`userName\``
347
+ }).from(Orders)
348
+ .innerJoin(Users, eq(Orders.userId, Users.id))
349
+ .where(eq(Orders.id, 1));
350
+
351
+ // OR with direct drizzle
352
+ const db = drizzle(forgeDriver);
353
+ const orderWithUser = await db
354
+ .select({
355
+ orderId: rawSql`${Orders.id} as \`orderId\``,
356
+ product: Orders.product,
357
+ userName: rawSql`${Users.name} as \`userName\``
358
+ }).from(Orders)
359
+ .innerJoin(Users, eq(Orders.userId, Users.id))
360
+ .where(eq(Orders.id, 1));
222
361
  // Returns: { orderId: 1, product: "Product 1", userName: "John Doe" }
223
362
  ```
224
363
 
@@ -226,18 +365,25 @@ const orderWithUser = await forgeSQL
226
365
 
227
366
  ```js
228
367
  // Finding duplicates
368
+ // With forgeSQL
229
369
  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
- );
370
+ .getDrizzleQueryBuilder()
371
+ .select({
372
+ name: Users.name,
373
+ count: rawSql`COUNT(*) as \`count\``
374
+ }).from(Users)
375
+ .groupBy(Users.name)
376
+ .having(rawSql`COUNT(*) > 1`);
377
+
378
+ // OR with direct drizzle
379
+ const db = drizzle(forgeDriver);
380
+ const duplicates = await db
381
+ .select({
382
+ name: Users.name,
383
+ count: rawSql`COUNT(*) as \`count\``
384
+ }).from(Users)
385
+ .groupBy(Users.name)
386
+ .having(rawSql`COUNT(*) > 1`);
241
387
  // Returns: { name: "John Doe", count: 2 }
242
388
 
243
389
  // Using executeQueryOnlyOne for unique results
@@ -124,8 +124,8 @@ class ForgeSQLCrudOperations {
124
124
  if (this.options?.logRawSqlQuery) {
125
125
  console.debug("INSERT SQL:", query.sql);
126
126
  }
127
- const result = await this.forgeOperations.fetch().executeRawUpdateSQL(query.sql, query.params);
128
- return result.insertId;
127
+ const result = await finalQuery;
128
+ return result[0].insertId;
129
129
  }
130
130
  /**
131
131
  * Deletes a record by its primary key with optional version check.
@@ -161,8 +161,8 @@ class ForgeSQLCrudOperations {
161
161
  if (this.options?.logRawSqlQuery) {
162
162
  console.debug("DELETE SQL:", queryBuilder.toSQL().sql);
163
163
  }
164
- const result = await this.forgeOperations.fetch().executeRawUpdateSQL(queryBuilder.toSQL().sql, queryBuilder.toSQL().params);
165
- return result.affectedRows;
164
+ const result = await queryBuilder;
165
+ return result[0].affectedRows;
166
166
  }
167
167
  /**
168
168
  * Updates a record by its primary key with optimistic locking support.
@@ -211,13 +211,13 @@ class ForgeSQLCrudOperations {
211
211
  if (this.options?.logRawSqlQuery) {
212
212
  console.debug("UPDATE SQL:", queryBuilder.toSQL().sql);
213
213
  }
214
- const result = await this.forgeOperations.fetch().executeRawUpdateSQL(queryBuilder.toSQL().sql, queryBuilder.toSQL().params);
215
- if (versionMetadata && result.affectedRows === 0) {
214
+ const result = await queryBuilder;
215
+ if (versionMetadata && result[0].affectedRows === 0) {
216
216
  throw new Error(
217
217
  `Optimistic locking failed: record with primary key ${entity[primaryKeyName]} has been modified`
218
218
  );
219
219
  }
220
- return result.affectedRows;
220
+ return result[0].affectedRows;
221
221
  }
222
222
  /**
223
223
  * Updates specified fields of records based on provided conditions.
@@ -239,8 +239,8 @@ class ForgeSQLCrudOperations {
239
239
  if (this.options?.logRawSqlQuery) {
240
240
  console.debug("UPDATE SQL:", queryBuilder.toSQL().sql);
241
241
  }
242
- const result = await this.forgeOperations.fetch().executeRawUpdateSQL(queryBuilder.toSQL().sql, queryBuilder.toSQL().params);
243
- return result.affectedRows;
242
+ const result = await queryBuilder;
243
+ return result[0].affectedRows;
244
244
  }
245
245
  // Helper methods
246
246
  /**
@@ -387,105 +387,6 @@ class ForgeSQLSelectOperations {
387
387
  constructor(options) {
388
388
  this.options = options;
389
389
  }
390
- /**
391
- * Executes a Drizzle query and returns the results.
392
- * Maps the raw database results to the appropriate entity types.
393
- *
394
- * @template T - The type of the query builder
395
- * @param {T} query - The Drizzle query to execute
396
- * @returns {Promise<Awaited<T>>} The query results mapped to entity types
397
- */
398
- async executeQuery(query) {
399
- const queryType = query;
400
- const querySql = queryType.toSQL();
401
- const datas = await this.executeRawSQL(querySql.sql, querySql.params);
402
- if (!datas.length) return [];
403
- return datas.map((r) => {
404
- const rawModel = r;
405
- const newModel = {};
406
- const columns = queryType.config.fields;
407
- Object.entries(columns).forEach(([name, column]) => {
408
- const { realColumn, aliasName } = this.extractColumnInfo(column);
409
- const value = rawModel[aliasName];
410
- if (value === null || value === void 0) {
411
- newModel[name] = value;
412
- return;
413
- }
414
- newModel[name] = this.parseColumnValue(realColumn, value);
415
- });
416
- return newModel;
417
- });
418
- }
419
- /**
420
- * Extracts column information and alias name from a column definition.
421
- * @param {any} column - The column definition from Drizzle
422
- * @returns {Object} Object containing the real column and its alias name
423
- */
424
- extractColumnInfo(column) {
425
- if (column instanceof drizzleOrm.SQL) {
426
- const realColumnSql = column;
427
- const realColumn = realColumnSql.queryChunks.find(
428
- (q) => q instanceof drizzleOrm.Column
429
- );
430
- let stringChunk = this.findAliasChunk(realColumnSql);
431
- let withoutAlias = false;
432
- if (!realColumn && (!stringChunk || !stringChunk.value || !stringChunk.value?.length)) {
433
- stringChunk = realColumnSql.queryChunks.filter((q) => q instanceof drizzleOrm.StringChunk).find((q) => q.value[0]);
434
- withoutAlias = true;
435
- }
436
- const aliasName = this.resolveAliasName(stringChunk, realColumn, withoutAlias);
437
- return { realColumn, aliasName };
438
- }
439
- return { realColumn: column, aliasName: column.name };
440
- }
441
- /**
442
- * Finds the alias chunk in SQL query chunks.
443
- * @param {SQL} realColumnSql - The SQL query chunks
444
- * @returns {StringChunk | undefined} The string chunk containing the alias or undefined
445
- */
446
- findAliasChunk(realColumnSql) {
447
- return realColumnSql.queryChunks.filter((q) => q instanceof drizzleOrm.StringChunk).find(
448
- (q) => q.value.find((f) => f.toLowerCase().includes("as"))
449
- );
450
- }
451
- /**
452
- * Resolves the alias name from the string chunk or column.
453
- * @param {StringChunk | undefined} stringChunk - The string chunk containing the alias
454
- * @param {Column | undefined} realColumn - The real column definition
455
- * @param {boolean} withoutAlias - Whether the column has no alias
456
- * @returns {string} The resolved alias name
457
- */
458
- resolveAliasName(stringChunk, realColumn, withoutAlias) {
459
- if (stringChunk) {
460
- if (withoutAlias) {
461
- return stringChunk.value[0];
462
- }
463
- const asClause = stringChunk.value.find((f) => f.toLowerCase().includes("as"));
464
- return asClause ? extractAlias(asClause.trim()) : realColumn?.name || "";
465
- }
466
- return realColumn?.name || "";
467
- }
468
- /**
469
- * Parses a column value based on its SQL type.
470
- * Handles datetime, date, and time types with appropriate formatting.
471
- *
472
- * @param {Column} column - The column definition
473
- * @param {unknown} value - The raw value to parse
474
- * @returns {unknown} The parsed value
475
- */
476
- parseColumnValue(column, value) {
477
- if (!column) return value;
478
- switch (column.getSQLType()) {
479
- case "datetime":
480
- return parseDateTime(value, "YYYY-MM-DDTHH:mm:ss.SSS");
481
- case "date":
482
- return parseDateTime(value, "YYYY-MM-DD");
483
- case "time":
484
- return parseDateTime(value, "HH:mm:ss.SSS");
485
- default:
486
- return value;
487
- }
488
- }
489
390
  /**
490
391
  * Executes a Drizzle query and returns a single result.
491
392
  * Throws an error if more than one record is returned.
@@ -496,7 +397,7 @@ class ForgeSQLSelectOperations {
496
397
  * @throws {Error} If more than one record is returned
497
398
  */
498
399
  async executeQueryOnlyOne(query) {
499
- const results = await this.executeQuery(query);
400
+ const results = await query;
500
401
  const datas = results;
501
402
  if (!datas.length) {
502
403
  return void 0;
@@ -544,6 +445,33 @@ class ForgeSQLSelectOperations {
544
445
  return updateQueryResponseResults.rows;
545
446
  }
546
447
  }
448
+ const forgeDriver = {
449
+ query: async (query, params) => {
450
+ try {
451
+ const sqlStatement = await sql.sql.prepare(query.sql);
452
+ if (params) {
453
+ await sqlStatement.bindParams(...params);
454
+ }
455
+ const result = await sqlStatement.execute();
456
+ let rows;
457
+ if (Array.isArray(result.rows)) {
458
+ rows = [
459
+ result.rows.map((r) => Object.values(r))
460
+ ];
461
+ } else {
462
+ rows = [
463
+ result.rows
464
+ ];
465
+ }
466
+ return rows;
467
+ } catch (error) {
468
+ console.error("SQL Error:", JSON.stringify(error));
469
+ throw error;
470
+ }
471
+ },
472
+ transaction: async (transactionFn) => {
473
+ }
474
+ };
547
475
  class ForgeSQLORMImpl {
548
476
  static instance = null;
549
477
  drizzle;
@@ -562,7 +490,7 @@ class ForgeSQLORMImpl {
562
490
  if (newOptions.logRawSqlQuery) {
563
491
  console.debug("Initializing ForgeSQLORM...");
564
492
  }
565
- this.drizzle = mysql2.drizzle("");
493
+ this.drizzle = mysql2.drizzle(forgeDriver);
566
494
  this.crudOperations = new ForgeSQLCrudOperations(this, newOptions);
567
495
  this.fetchOperations = new ForgeSQLSelectOperations(newOptions);
568
496
  } catch (error) {
@@ -691,6 +619,7 @@ exports.ForgeSQLCrudOperations = ForgeSQLCrudOperations;
691
619
  exports.ForgeSQLSelectOperations = ForgeSQLSelectOperations;
692
620
  exports.default = ForgeSQLORM;
693
621
  exports.extractAlias = extractAlias;
622
+ exports.forgeDriver = forgeDriver;
694
623
  exports.getPrimaryKeys = getPrimaryKeys;
695
624
  exports.getTableMetadata = getTableMetadata;
696
625
  exports.mySqlDateString = mySqlDateString;