forge-sql-orm 2.0.7 → 2.0.9

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.
@@ -6,8 +6,11 @@ import {
6
6
  SchemaSqlForgeSql,
7
7
  } from "./ForgeSQLQueryBuilder";
8
8
  import { ForgeSQLSelectOperations } from "./ForgeSQLSelectOperations";
9
- import { drizzle } from "drizzle-orm/mysql2";
9
+ import { drizzle, MySqlRemoteDatabase, MySqlRemotePreparedQueryHKT } from "drizzle-orm/mysql-proxy";
10
10
  import { forgeDriver } from "../utils/forgeDriver";
11
+ import type { SelectedFields } from "drizzle-orm/mysql-core/query-builders/select.types";
12
+ import { mapSelectFieldsWithAlias } from "../utils/sqlUtils";
13
+ import { MySqlSelectBuilder } from "drizzle-orm/mysql-core";
11
14
 
12
15
  /**
13
16
  * Implementation of ForgeSQLORM that uses Drizzle ORM for query building.
@@ -34,7 +37,7 @@ class ForgeSQLORMImpl implements ForgeSqlOperation {
34
37
  console.debug("Initializing ForgeSQLORM...");
35
38
  }
36
39
  // Initialize Drizzle instance with our custom driver
37
- this.drizzle = drizzle(forgeDriver);
40
+ this.drizzle = drizzle(forgeDriver, { logger: newOptions.logRawSqlQuery });
38
41
  this.crudOperations = new ForgeSQLCrudOperations(this, newOptions);
39
42
  this.fetchOperations = new ForgeSQLSelectOperations(newOptions);
40
43
  } catch (error) {
@@ -80,22 +83,118 @@ class ForgeSQLORMImpl implements ForgeSqlOperation {
80
83
  *
81
84
  * @returns A Drizzle query builder instance for query construction only.
82
85
  */
83
- getDrizzleQueryBuilder() {
86
+ getDrizzleQueryBuilder(): MySqlRemoteDatabase<Record<string, unknown>> {
84
87
  return this.drizzle;
85
88
  }
89
+
90
+ /**
91
+ * Creates a select query with unique field aliases to prevent field name collisions in joins.
92
+ * This is particularly useful when working with Atlassian Forge SQL, which collapses fields with the same name in joined tables.
93
+ *
94
+ * @template TSelection - The type of the selected fields
95
+ * @param {TSelection} fields - Object containing the fields to select, with table schemas as values
96
+ * @returns {MySqlSelectBuilder<TSelection, MySql2PreparedQueryHKT>} A select query builder with unique field aliases
97
+ * @throws {Error} If fields parameter is empty
98
+ * @example
99
+ * ```typescript
100
+ * await forgeSQL
101
+ * .select({user: users, order: orders})
102
+ * .from(orders)
103
+ * .innerJoin(users, eq(orders.userId, users.id));
104
+ * ```
105
+ */
106
+ select<TSelection extends SelectedFields>(
107
+ fields: TSelection,
108
+ ): MySqlSelectBuilder<TSelection, MySqlRemotePreparedQueryHKT> {
109
+ if (!fields) {
110
+ throw new Error("fields is empty");
111
+ }
112
+ return this.drizzle.select(mapSelectFieldsWithAlias(fields));
113
+ }
114
+
115
+ /**
116
+ * Creates a distinct select query with unique field aliases to prevent field name collisions in joins.
117
+ * This is particularly useful when working with Atlassian Forge SQL, which collapses fields with the same name in joined tables.
118
+ *
119
+ * @template TSelection - The type of the selected fields
120
+ * @param {TSelection} fields - Object containing the fields to select, with table schemas as values
121
+ * @returns {MySqlSelectBuilder<TSelection, MySql2PreparedQueryHKT>} A distinct select query builder with unique field aliases
122
+ * @throws {Error} If fields parameter is empty
123
+ * @example
124
+ * ```typescript
125
+ * await forgeSQL
126
+ * .selectDistinct({user: users, order: orders})
127
+ * .from(orders)
128
+ * .innerJoin(users, eq(orders.userId, users.id));
129
+ * ```
130
+ */
131
+ selectDistinct<TSelection extends SelectedFields>(
132
+ fields: TSelection,
133
+ ): MySqlSelectBuilder<TSelection, MySqlRemotePreparedQueryHKT> {
134
+ if (!fields) {
135
+ throw new Error("fields is empty");
136
+ }
137
+ return this.drizzle.selectDistinct(mapSelectFieldsWithAlias(fields));
138
+ }
86
139
  }
87
140
 
88
141
  /**
89
142
  * Public class that acts as a wrapper around the private ForgeSQLORMImpl.
90
143
  * Provides a clean interface for working with Forge SQL and Drizzle ORM.
91
144
  */
92
- class ForgeSQLORM {
145
+ class ForgeSQLORM implements ForgeSqlOperation {
93
146
  private readonly ormInstance: ForgeSqlOperation;
94
147
 
95
148
  constructor(options?: ForgeSqlOrmOptions) {
96
149
  this.ormInstance = ForgeSQLORMImpl.getInstance(options);
97
150
  }
98
151
 
152
+ /**
153
+ * Creates a select query with unique field aliases to prevent field name collisions in joins.
154
+ * This is particularly useful when working with Atlassian Forge SQL, which collapses fields with the same name in joined tables.
155
+ *
156
+ * @template TSelection - The type of the selected fields
157
+ * @param {TSelection} fields - Object containing the fields to select, with table schemas as values
158
+ * @returns {MySqlSelectBuilder<TSelection, MySql2PreparedQueryHKT>} A select query builder with unique field aliases
159
+ * @throws {Error} If fields parameter is empty
160
+ * @example
161
+ * ```typescript
162
+ * await forgeSQL
163
+ * .select({user: users, order: orders})
164
+ * .from(orders)
165
+ * .innerJoin(users, eq(orders.userId, users.id));
166
+ * ```
167
+ */
168
+ select<TSelection extends SelectedFields>(
169
+ fields: TSelection,
170
+ ): MySqlSelectBuilder<TSelection, MySqlRemotePreparedQueryHKT> {
171
+ return this.ormInstance.getDrizzleQueryBuilder().select(mapSelectFieldsWithAlias(fields));
172
+ }
173
+
174
+ /**
175
+ * Creates a distinct select query with unique field aliases to prevent field name collisions in joins.
176
+ * This is particularly useful when working with Atlassian Forge SQL, which collapses fields with the same name in joined tables.
177
+ *
178
+ * @template TSelection - The type of the selected fields
179
+ * @param {TSelection} fields - Object containing the fields to select, with table schemas as values
180
+ * @returns {MySqlSelectBuilder<TSelection, MySql2PreparedQueryHKT>} A distinct select query builder with unique field aliases
181
+ * @throws {Error} If fields parameter is empty
182
+ * @example
183
+ * ```typescript
184
+ * await forgeSQL
185
+ * .selectDistinct({user: users, order: orders})
186
+ * .from(orders)
187
+ * .innerJoin(users, eq(orders.userId, users.id));
188
+ * ```
189
+ */
190
+ selectDistinct<TSelection extends SelectedFields>(
191
+ fields: TSelection,
192
+ ): MySqlSelectBuilder<TSelection, MySqlRemotePreparedQueryHKT> {
193
+ return this.ormInstance
194
+ .getDrizzleQueryBuilder()
195
+ .selectDistinct(mapSelectFieldsWithAlias(fields));
196
+ }
197
+
99
198
  /**
100
199
  * Proxies the `crud` method from `ForgeSQLORMImpl`.
101
200
  * @returns CRUD operations.
@@ -1,11 +1,19 @@
1
1
  import { UpdateQueryResponse } from "@forge/sql";
2
2
  import { SqlParameters } from "@forge/sql/out/sql-statement";
3
- import { MySql2Database } from "drizzle-orm/mysql2/driver";
4
- import { AnyMySqlSelectQueryBuilder, AnyMySqlTable, customType } from "drizzle-orm/mysql-core";
5
- import { MySqlSelectDynamic } from "drizzle-orm/mysql-core/query-builders/select.types";
3
+ import {
4
+ AnyMySqlSelectQueryBuilder,
5
+ AnyMySqlTable,
6
+ customType,
7
+ MySqlSelectBuilder,
8
+ } from "drizzle-orm/mysql-core";
9
+ import {
10
+ MySqlSelectDynamic,
11
+ type SelectedFields,
12
+ } from "drizzle-orm/mysql-core/query-builders/select.types";
6
13
  import { InferInsertModel, SQL } from "drizzle-orm";
7
14
  import moment from "moment/moment";
8
15
  import { parseDateTime } from "../utils/sqlUtils";
16
+ import { MySqlRemoteDatabase, MySqlRemotePreparedQueryHKT } from "drizzle-orm/mysql-proxy/index";
9
17
 
10
18
  // ============= Core Types =============
11
19
 
@@ -36,7 +44,15 @@ export interface QueryBuilderForgeSql {
36
44
  * Creates a new query builder for the given entity.
37
45
  * @returns {MySql2Database<Record<string, unknown>>} The Drizzle database instance for building queries
38
46
  */
39
- getDrizzleQueryBuilder(): MySql2Database<Record<string, unknown>>;
47
+ getDrizzleQueryBuilder(): MySqlRemoteDatabase<Record<string, unknown>>;
48
+
49
+ select<TSelection extends SelectedFields>(
50
+ fields: TSelection,
51
+ ): MySqlSelectBuilder<TSelection, MySqlRemotePreparedQueryHKT>;
52
+
53
+ selectDistinct<TSelection extends SelectedFields>(
54
+ fields: TSelection,
55
+ ): MySqlSelectBuilder<TSelection, MySqlRemotePreparedQueryHKT>;
40
56
  }
41
57
 
42
58
  // ============= CRUD Operations =============
@@ -189,12 +205,6 @@ export interface ForgeSqlOrmOptions {
189
205
  * @default false
190
206
  */
191
207
  logRawSqlQuery?: boolean;
192
- /**
193
- * Enables logging of raw SQL queries in the Atlassian Forge Developer Console.
194
- * Useful for debugging and monitoring SQL operations.
195
- * @default false
196
- */
197
- logRawSqlQueryParams?: boolean;
198
208
 
199
209
  /**
200
210
  * Disables optimistic locking for all operations.
@@ -57,13 +57,12 @@ export class ForgeSQLSelectOperations implements SchemaSqlForgeSql {
57
57
  */
58
58
  async executeRawSQL<T extends object | unknown>(query: string, params?: unknown[]): Promise<T[]> {
59
59
  if (this.options.logRawSqlQuery) {
60
- console.debug("Executing raw SQL: " + query);
60
+ console.debug(
61
+ `Executing with SQL ${query}` + params ? `, with params: ${JSON.stringify(params)}` : "",
62
+ );
61
63
  }
62
64
  const sqlStatement = sql.prepare<T>(query);
63
65
  if (params) {
64
- if (this.options.logRawSqlQuery && this.options.logRawSqlQueryParams) {
65
- console.debug("Executing with SQL Params: " + JSON.stringify(params));
66
- }
67
66
  sqlStatement.bindParams(...params);
68
67
  }
69
68
  const result = await sqlStatement.execute();
@@ -81,6 +80,13 @@ export class ForgeSQLSelectOperations implements SchemaSqlForgeSql {
81
80
  if (params) {
82
81
  sqlStatement.bindParams(...params);
83
82
  }
83
+ if (this.options.logRawSqlQuery) {
84
+ console.debug(
85
+ `Executing Update with SQL ${query}` + params
86
+ ? `, with params: ${JSON.stringify(params)}`
87
+ : "",
88
+ );
89
+ }
84
90
  const updateQueryResponseResults = await sqlStatement.execute();
85
91
  return updateQueryResponseResults.rows;
86
92
  }
@@ -1,34 +1,39 @@
1
- import { sql } from "@forge/sql";
2
- import {AnyMySql2Connection} from "drizzle-orm/mysql2/driver";
1
+ import { sql, UpdateQueryResponse } from "@forge/sql";
3
2
 
4
3
  interface ForgeSQLResult {
5
- rows: Record<string, unknown>[] | Record<string, unknown>;
4
+ rows: Record<string, unknown>[] | Record<string, unknown>;
6
5
  }
7
6
 
8
- export const forgeDriver = {
9
- query: async (query: { sql: string }, params?: unknown[]) => {
10
- try {
11
- const sqlStatement = await sql.prepare<unknown>(query.sql);
12
- if (params) {
13
- await sqlStatement.bindParams(...params);
14
- }
15
- const result = await sqlStatement.execute() as ForgeSQLResult;
16
-
17
- let rows;
18
- if (Array.isArray(result.rows)) {
19
- rows = [
20
- result.rows.map(r => Object.values(r as Record<string, unknown>))
21
- ];
22
- } else {
23
- rows = [
24
- result.rows as Record<string, unknown>
25
- ];
26
- }
27
-
28
- return rows;
29
- } catch (error) {
30
- console.error("SQL Error:", JSON.stringify(error));
31
- throw error;
32
- }
7
+ export const forgeDriver = async (
8
+ query: string,
9
+ params: any[],
10
+ method: "all" | "execute",
11
+ ): Promise<{
12
+ rows: any[];
13
+ insertId?: number;
14
+ affectedRows?: number;
15
+ }> => {
16
+ try {
17
+ if (method == "execute") {
18
+ const sqlStatement = sql.prepare<UpdateQueryResponse>(query);
19
+ if (params) {
20
+ sqlStatement.bindParams(...params);
21
+ }
22
+ const updateQueryResponseResults = await sqlStatement.execute();
23
+ let result = updateQueryResponseResults.rows as any;
24
+ return { ...result, rows: [result] };
25
+ } else {
26
+ const sqlStatement = await sql.prepare<unknown>(query);
27
+ if (params) {
28
+ await sqlStatement.bindParams(...params);
29
+ }
30
+ const result = (await sqlStatement.execute()) as ForgeSQLResult;
31
+ let rows;
32
+ rows = (result.rows as any[]).map((r) => Object.values(r as Record<string, unknown>));
33
+ return { rows: rows };
33
34
  }
34
- } as unknown as AnyMySql2Connection;
35
+ } catch (error) {
36
+ console.error("SQL Error:", JSON.stringify(error));
37
+ throw error;
38
+ }
39
+ };
@@ -1,11 +1,15 @@
1
1
  import moment from "moment";
2
- import { AnyColumn } from "drizzle-orm";
2
+ import { AnyColumn, Column, isTable, sql } from "drizzle-orm";
3
3
  import { AnyMySqlTable } from "drizzle-orm/mysql-core/index";
4
4
  import { PrimaryKeyBuilder } from "drizzle-orm/mysql-core/primary-keys";
5
5
  import { AnyIndexBuilder } from "drizzle-orm/mysql-core/indexes";
6
6
  import { CheckBuilder } from "drizzle-orm/mysql-core/checks";
7
7
  import { ForeignKeyBuilder } from "drizzle-orm/mysql-core/foreign-keys";
8
8
  import { UniqueConstraintBuilder } from "drizzle-orm/mysql-core/unique-constraint";
9
+ import type { SelectedFields } from "drizzle-orm/mysql-core/query-builders/select.types";
10
+ import { MySqlTable } from "drizzle-orm/mysql-core";
11
+ import { getTableName } from "drizzle-orm/table";
12
+ import { isSQLWrapper } from "drizzle-orm/sql/sql";
9
13
 
10
14
  /**
11
15
  * Interface representing table metadata information
@@ -113,7 +117,7 @@ export function getPrimaryKeys<T extends AnyMySqlTable>(table: T): [string, AnyC
113
117
  function processForeignKeys(
114
118
  table: AnyMySqlTable,
115
119
  foreignKeysSymbol: symbol | undefined,
116
- extraSymbol: symbol | undefined
120
+ extraSymbol: symbol | undefined,
117
121
  ): ForeignKeyBuilder[] {
118
122
  const foreignKeys: ForeignKeyBuilder[] = [];
119
123
 
@@ -122,7 +126,7 @@ function processForeignKeys(
122
126
  // @ts-ignore
123
127
  const fkArray: any[] = table[foreignKeysSymbol];
124
128
  if (fkArray) {
125
- fkArray.forEach(fk => {
129
+ fkArray.forEach((fk) => {
126
130
  if (fk.reference) {
127
131
  const item = fk.reference(fk);
128
132
  foreignKeys.push(item);
@@ -148,7 +152,7 @@ function processForeignKeys(
148
152
  if (!builder?.constructor) return;
149
153
 
150
154
  const builderName = builder.constructor.name.toLowerCase();
151
- if (builderName.includes('foreignkeybuilder')) {
155
+ if (builderName.includes("foreignkeybuilder")) {
152
156
  foreignKeys.push(builder);
153
157
  }
154
158
  });
@@ -242,7 +246,7 @@ export function getTableMetadata(table: AnyMySqlTable): MetadataInfo {
242
246
  export function generateDropTableStatements(tables: AnyMySqlTable[]): string[] {
243
247
  const dropStatements: string[] = [];
244
248
 
245
- tables.forEach(table => {
249
+ tables.forEach((table) => {
246
250
  const tableMetadata = getTableMetadata(table);
247
251
  if (tableMetadata.tableName) {
248
252
  dropStatements.push(`DROP TABLE IF EXISTS \`${tableMetadata.tableName}\`;`);
@@ -254,3 +258,49 @@ export function generateDropTableStatements(tables: AnyMySqlTable[]): string[] {
254
258
 
255
259
  return dropStatements;
256
260
  }
261
+
262
+ export function mapSelectTableToAlias(table: MySqlTable): any {
263
+ const { columns, tableName } = getTableMetadata(table);
264
+ const selectionsTableFields: Record<string, unknown> = {};
265
+ Object.keys(columns).forEach((name) => {
266
+ const column = columns[name] as AnyColumn;
267
+ const fieldAlias = sql.raw(`${tableName}_${column.name}`);
268
+ selectionsTableFields[name] = sql`${column} as \`${fieldAlias}\``;
269
+ });
270
+ return selectionsTableFields;
271
+ }
272
+
273
+ function isDrizzleColumn(column: any): boolean {
274
+ return column && typeof column === "object" && "table" in column;
275
+ }
276
+
277
+ export function mapSelectAllFieldsToAlias(selections: any, name: string, fields: any): any {
278
+ if (isTable(fields)) {
279
+ selections[name] = mapSelectTableToAlias(fields as MySqlTable);
280
+ } else if (isDrizzleColumn(fields)) {
281
+ const column = fields as Column;
282
+ let aliasName = sql.raw(`${getTableName(column.table)}_${column.name}`);
283
+ selections[name] = sql`${column} as \`${aliasName}\``;
284
+ } else if (isSQLWrapper(fields)) {
285
+ selections[name] = fields;
286
+ } else {
287
+ const innerSelections: any = {};
288
+ Object.entries(fields).forEach(([iname, ifields]) => {
289
+ mapSelectAllFieldsToAlias(innerSelections, iname, ifields);
290
+ });
291
+ selections[name] = innerSelections;
292
+ }
293
+ return selections;
294
+ }
295
+ export function mapSelectFieldsWithAlias<TSelection extends SelectedFields>(
296
+ fields: TSelection,
297
+ ): TSelection {
298
+ if (!fields) {
299
+ throw new Error("fields is empty");
300
+ }
301
+ const selections: any = {};
302
+ Object.entries(fields).forEach(([name, fields]) => {
303
+ mapSelectAllFieldsToAlias(selections, name, fields);
304
+ });
305
+ return selections;
306
+ }
@@ -1,7 +1,9 @@
1
- import {migrationRunner, sql} from "@forge/sql";
2
- import {MigrationRunner} from "@forge/sql/out/migration";
1
+ import { migrationRunner, sql } from "@forge/sql";
2
+ import { MigrationRunner } from "@forge/sql/out/migration";
3
3
 
4
- export const applySchemaMigrations = async (migration: (migrationRunner:MigrationRunner )=>Promise<MigrationRunner>) => {
4
+ export const applySchemaMigrations = async (
5
+ migration: (migrationRunner: MigrationRunner) => Promise<MigrationRunner>,
6
+ ) => {
5
7
  console.log("Provisioning the database");
6
8
  await sql._provision();
7
9
  console.info("Running schema migrations");
@@ -10,8 +12,8 @@ export const applySchemaMigrations = async (migration: (migrationRunner:Migratio
10
12
  console.info("Migrations applied:", successfulMigrations);
11
13
 
12
14
  const migrationHistory = (await migrationRunner.list())
13
- .map((y) => `${y.id}, ${y.name}, ${y.migratedAt.toUTCString()}`)
14
- .join("\n");
15
+ .map((y) => `${y.id}, ${y.name}, ${y.migratedAt.toUTCString()}`)
16
+ .join("\n");
15
17
 
16
18
  console.info("Migrations history:\nid, name, migrated_at\n", migrationHistory);
17
19
 
@@ -22,4 +24,3 @@ export const applySchemaMigrations = async (migration: (migrationRunner:Migratio
22
24
  body: "Migrations successfully executed",
23
25
  };
24
26
  };
25
-
@@ -1,14 +1,7 @@
1
1
  import { sql } from "@forge/sql";
2
2
  import { AnyMySqlTable } from "drizzle-orm/mysql-core";
3
- import { generateDropTableStatements as generateStatements, getTableMetadata } from "../utils/sqlUtils";
4
- import {getHttpResponse, TriggerResponse} from "./index";
5
-
6
- export interface DropTablesResponse {
7
- message: string;
8
- droppedTables: string[];
9
- warning?: string;
10
- }
11
-
3
+ import { generateDropTableStatements as generateStatements } from "../utils/sqlUtils";
4
+ import { getHttpResponse, TriggerResponse } from "./index";
12
5
 
13
6
  /**
14
7
  * ⚠️ WARNING: This web trigger will permanently delete all data in the specified tables.
@@ -19,7 +12,7 @@ export interface DropTablesResponse {
19
12
  * @returns Trigger response with execution status and list of dropped tables
20
13
  */
21
14
  export async function dropSchemaMigrations(
22
- tables: AnyMySqlTable[]
15
+ tables: AnyMySqlTable[],
23
16
  ): Promise<TriggerResponse<string>> {
24
17
  try {
25
18
  // Generate drop statements
@@ -31,17 +24,12 @@ export async function dropSchemaMigrations(
31
24
  await sql.executeDDL(statement);
32
25
  }
33
26
 
34
- // Get list of dropped tables
35
- const droppedTables = tables
36
- .map(table => {
37
- const metadata = getTableMetadata(table);
38
- return metadata.tableName;
39
- })
40
- .filter(Boolean);
41
-
42
- return getHttpResponse<string>(200, "⚠️ All data in these tables has been permanently deleted. This operation cannot be undone.");
27
+ return getHttpResponse<string>(
28
+ 200,
29
+ "⚠️ All data in these tables has been permanently deleted. This operation cannot be undone.",
30
+ );
43
31
  } catch (error: unknown) {
44
- const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
32
+ const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
45
33
  return getHttpResponse<string>(500, errorMessage);
46
34
  }
47
35
  }
@@ -1,26 +1,25 @@
1
-
2
- export * from './dropMigrationWebTrigger'
3
- export * from './applyMigrationsWebTrigger'
1
+ export * from "./dropMigrationWebTrigger";
2
+ export * from "./applyMigrationsWebTrigger";
4
3
 
5
4
  export interface TriggerResponse<BODY> {
6
- body?: BODY;
7
- headers?: Record<string, string[]>;
8
- statusCode: number;
9
- statusText?: string;
5
+ body?: BODY;
6
+ headers?: Record<string, string[]>;
7
+ statusCode: number;
8
+ statusText?: string;
10
9
  }
11
10
 
12
11
  export const getHttpResponse = <Body>(statusCode: number, body: Body): TriggerResponse<Body> => {
13
- let statusText = "";
14
- if (statusCode === 200) {
15
- statusText = "Ok";
16
- } else {
17
- statusText = "Bad Request";
18
- }
12
+ let statusText = "";
13
+ if (statusCode === 200) {
14
+ statusText = "Ok";
15
+ } else {
16
+ statusText = "Bad Request";
17
+ }
19
18
 
20
- return {
21
- headers: { "Content-Type": ["application/json"] },
22
- statusCode,
23
- statusText,
24
- body,
25
- };
26
- };
19
+ return {
20
+ headers: { "Content-Type": ["application/json"] },
21
+ statusCode,
22
+ statusText,
23
+ body,
24
+ };
25
+ };