forge-sql-orm 2.0.8 → 2.0.10
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 +55 -8
- package/dist/ForgeSQLORM.js +66 -21
- package/dist/ForgeSQLORM.js.map +1 -1
- package/dist/ForgeSQLORM.mjs +66 -21
- package/dist/ForgeSQLORM.mjs.map +1 -1
- package/dist/core/ForgeSQLORM.d.ts +4 -4
- package/dist/core/ForgeSQLORM.d.ts.map +1 -1
- package/dist/core/ForgeSQLQueryBuilder.d.ts +36 -5
- package/dist/core/ForgeSQLQueryBuilder.d.ts.map +1 -1
- package/dist/core/SystemTables.d.ts +8 -6
- package/dist/core/SystemTables.d.ts.map +1 -1
- package/dist/utils/forgeDriver.d.ts +5 -2
- package/dist/utils/forgeDriver.d.ts.map +1 -1
- package/dist/webtriggers/applyMigrationsWebTrigger.d.ts +23 -0
- package/dist/webtriggers/applyMigrationsWebTrigger.d.ts.map +1 -1
- package/dist/webtriggers/dropMigrationWebTrigger.d.ts +20 -4
- package/dist/webtriggers/dropMigrationWebTrigger.d.ts.map +1 -1
- package/dist/webtriggers/fetchSchemaWebTrigger.d.ts +28 -0
- package/dist/webtriggers/fetchSchemaWebTrigger.d.ts.map +1 -0
- package/dist/webtriggers/index.d.ts +1 -0
- package/dist/webtriggers/index.d.ts.map +1 -1
- package/dist-cli/cli.js +5 -0
- package/dist-cli/cli.js.map +1 -1
- package/dist-cli/cli.mjs +8 -3
- package/dist-cli/cli.mjs.map +1 -1
- package/package.json +1 -1
- package/src/core/ForgeSQLORM.ts +10 -10
- package/src/core/ForgeSQLQueryBuilder.ts +36 -5
- package/src/core/SystemTables.ts +6 -3
- package/src/utils/forgeDriver.ts +28 -17
- package/src/webtriggers/applyMigrationsWebTrigger.ts +23 -0
- package/src/webtriggers/dropMigrationWebTrigger.ts +20 -4
- package/src/webtriggers/fetchSchemaWebTrigger.ts +96 -0
- package/src/webtriggers/index.ts +1 -0
|
@@ -1,6 +1,5 @@
|
|
|
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
3
|
import {
|
|
5
4
|
AnyMySqlSelectQueryBuilder,
|
|
6
5
|
AnyMySqlTable,
|
|
@@ -14,7 +13,7 @@ import {
|
|
|
14
13
|
import { InferInsertModel, SQL } from "drizzle-orm";
|
|
15
14
|
import moment from "moment/moment";
|
|
16
15
|
import { parseDateTime } from "../utils/sqlUtils";
|
|
17
|
-
import {
|
|
16
|
+
import { MySqlRemoteDatabase, MySqlRemotePreparedQueryHKT } from "drizzle-orm/mysql-proxy/index";
|
|
18
17
|
|
|
19
18
|
// ============= Core Types =============
|
|
20
19
|
|
|
@@ -45,15 +44,47 @@ export interface QueryBuilderForgeSql {
|
|
|
45
44
|
* Creates a new query builder for the given entity.
|
|
46
45
|
* @returns {MySql2Database<Record<string, unknown>>} The Drizzle database instance for building queries
|
|
47
46
|
*/
|
|
48
|
-
getDrizzleQueryBuilder():
|
|
47
|
+
getDrizzleQueryBuilder(): MySqlRemoteDatabase<Record<string, unknown>>;
|
|
49
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Creates a select query with unique field aliases to prevent field name collisions in joins.
|
|
51
|
+
* This is particularly useful when working with Atlassian Forge SQL, which collapses fields with the same name in joined tables.
|
|
52
|
+
*
|
|
53
|
+
* @template TSelection - The type of the selected fields
|
|
54
|
+
* @param {TSelection} fields - Object containing the fields to select, with table schemas as values
|
|
55
|
+
* @returns {MySqlSelectBuilder<TSelection, MySqlRemotePreparedQueryHKT>} A select query builder with unique field aliases
|
|
56
|
+
* @throws {Error} If fields parameter is empty
|
|
57
|
+
* @example
|
|
58
|
+
* ```typescript
|
|
59
|
+
* await forgeSQL
|
|
60
|
+
* .select({user: users, order: orders})
|
|
61
|
+
* .from(orders)
|
|
62
|
+
* .innerJoin(users, eq(orders.userId, users.id));
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
50
65
|
select<TSelection extends SelectedFields>(
|
|
51
66
|
fields: TSelection,
|
|
52
|
-
): MySqlSelectBuilder<TSelection,
|
|
67
|
+
): MySqlSelectBuilder<TSelection, MySqlRemotePreparedQueryHKT>;
|
|
53
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Creates a distinct select query with unique field aliases to prevent field name collisions in joins.
|
|
71
|
+
* This is particularly useful when working with Atlassian Forge SQL, which collapses fields with the same name in joined tables.
|
|
72
|
+
*
|
|
73
|
+
* @template TSelection - The type of the selected fields
|
|
74
|
+
* @param {TSelection} fields - Object containing the fields to select, with table schemas as values
|
|
75
|
+
* @returns {MySqlSelectBuilder<TSelection, MySqlRemotePreparedQueryHKT>} A distinct select query builder with unique field aliases
|
|
76
|
+
* @throws {Error} If fields parameter is empty
|
|
77
|
+
* @example
|
|
78
|
+
* ```typescript
|
|
79
|
+
* await forgeSQL
|
|
80
|
+
* .selectDistinct({user: users, order: orders})
|
|
81
|
+
* .from(orders)
|
|
82
|
+
* .innerJoin(users, eq(orders.userId, users.id));
|
|
83
|
+
* ```
|
|
84
|
+
*/
|
|
54
85
|
selectDistinct<TSelection extends SelectedFields>(
|
|
55
86
|
fields: TSelection,
|
|
56
|
-
): MySqlSelectBuilder<TSelection,
|
|
87
|
+
): MySqlSelectBuilder<TSelection, MySqlRemotePreparedQueryHKT>;
|
|
57
88
|
}
|
|
58
89
|
|
|
59
90
|
// ============= CRUD Operations =============
|
package/src/core/SystemTables.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { bigint, mysqlTable, timestamp, varchar } from "drizzle-orm/mysql-core";
|
|
2
|
+
import { Table } from "drizzle-orm";
|
|
2
3
|
|
|
3
4
|
export const migrations = mysqlTable("__migrations", {
|
|
4
|
-
id:
|
|
5
|
+
id: bigint("id", { mode: "number" }).primaryKey().autoincrement(),
|
|
5
6
|
name: varchar("name", { length: 255 }).notNull(),
|
|
6
|
-
|
|
7
|
+
migratedAt: timestamp("migratedAt").defaultNow().notNull(),
|
|
7
8
|
});
|
|
9
|
+
|
|
10
|
+
export const forgeSystemTables: Table[] = [migrations];
|
package/src/utils/forgeDriver.ts
CHANGED
|
@@ -1,28 +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
4
|
rows: Record<string, unknown>[] | Record<string, unknown>;
|
|
6
5
|
}
|
|
7
6
|
|
|
8
|
-
export const forgeDriver =
|
|
9
|
-
query:
|
|
10
|
-
|
|
11
|
-
|
|
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);
|
|
12
27
|
if (params) {
|
|
13
28
|
await sqlStatement.bindParams(...params);
|
|
14
29
|
}
|
|
15
30
|
const result = (await sqlStatement.execute()) as ForgeSQLResult;
|
|
16
31
|
let rows;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
} else {
|
|
20
|
-
rows = [result.rows as Record<string, unknown>];
|
|
21
|
-
}
|
|
22
|
-
return rows;
|
|
23
|
-
} catch (error) {
|
|
24
|
-
console.error("SQL Error:", JSON.stringify(error));
|
|
25
|
-
throw error;
|
|
32
|
+
rows = (result.rows as any[]).map((r) => Object.values(r as Record<string, unknown>));
|
|
33
|
+
return { rows: rows };
|
|
26
34
|
}
|
|
27
|
-
}
|
|
28
|
-
|
|
35
|
+
} catch (error) {
|
|
36
|
+
console.error("SQL Error:", JSON.stringify(error));
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
@@ -1,6 +1,29 @@
|
|
|
1
1
|
import { migrationRunner, sql } from "@forge/sql";
|
|
2
2
|
import { MigrationRunner } from "@forge/sql/out/migration";
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Web trigger for applying database schema migrations in Atlassian Forge SQL.
|
|
6
|
+
* This function handles the complete migration process including:
|
|
7
|
+
* - Database provisioning
|
|
8
|
+
* - Migration execution
|
|
9
|
+
* - Migration history tracking
|
|
10
|
+
*
|
|
11
|
+
* @param migration - A function that takes a MigrationRunner instance and returns a Promise of MigrationRunner
|
|
12
|
+
* This function should define the sequence of migrations to be applied
|
|
13
|
+
* @returns {Promise<{
|
|
14
|
+
* headers: { "Content-Type": ["application/json"] },
|
|
15
|
+
* statusCode: number,
|
|
16
|
+
* statusText: string,
|
|
17
|
+
* body: string
|
|
18
|
+
* }>} A response object containing:
|
|
19
|
+
* - headers: Content-Type header set to application/json
|
|
20
|
+
* - statusCode: 200 on success
|
|
21
|
+
* - statusText: "OK" on success
|
|
22
|
+
* - body: Success message or error details
|
|
23
|
+
*
|
|
24
|
+
* @throws {Error} If database provisioning fails
|
|
25
|
+
* @throws {Error} If migration execution fails
|
|
26
|
+
*/
|
|
4
27
|
export const applySchemaMigrations = async (
|
|
5
28
|
migration: (migrationRunner: MigrationRunner) => Promise<MigrationRunner>,
|
|
6
29
|
) => {
|
|
@@ -4,12 +4,28 @@ import { generateDropTableStatements as generateStatements } from "../utils/sqlU
|
|
|
4
4
|
import { getHttpResponse, TriggerResponse } from "./index";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* ⚠️
|
|
8
|
-
*
|
|
7
|
+
* ⚠️ DEVELOPMENT ONLY WEB TRIGGER ⚠️
|
|
8
|
+
*
|
|
9
|
+
* This web trigger is designed for development environments only and will permanently delete all data in the specified tables.
|
|
10
|
+
* It generates and executes SQL statements to drop tables and their associated constraints.
|
|
11
|
+
*
|
|
12
|
+
* @warning This trigger should NEVER be used in production environments because:
|
|
13
|
+
* - It permanently deletes all data in the specified tables
|
|
14
|
+
* - The operation cannot be undone
|
|
15
|
+
* - It may affect application functionality
|
|
16
|
+
* - It could lead to data loss and system instability
|
|
9
17
|
*
|
|
10
|
-
* Generates SQL statements to drop tables and executes them
|
|
11
18
|
* @param tables - Array of table schemas to drop
|
|
12
|
-
* @returns
|
|
19
|
+
* @returns {Promise<TriggerResponse<string>>} A response containing:
|
|
20
|
+
* - On success: 200 status with warning message about permanent deletion
|
|
21
|
+
* - On failure: 500 status with error message
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```typescript
|
|
25
|
+
* // Example usage in development only
|
|
26
|
+
* await dropSchemaMigrations([users, orders]);
|
|
27
|
+
* // ⚠️ Warning: This will permanently delete all data in users and orders tables
|
|
28
|
+
* ```
|
|
13
29
|
*/
|
|
14
30
|
export async function dropSchemaMigrations(
|
|
15
31
|
tables: AnyMySqlTable[],
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { sql } from "@forge/sql";
|
|
2
|
+
import { getHttpResponse, TriggerResponse } from "./index";
|
|
3
|
+
import { forgeSystemTables } from "../core/SystemTables";
|
|
4
|
+
import { getTableName } from "drizzle-orm/table";
|
|
5
|
+
|
|
6
|
+
interface CreateTableRow {
|
|
7
|
+
Table: string;
|
|
8
|
+
"Create Table": string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* ⚠️ DEVELOPMENT ONLY WEB TRIGGER ⚠️
|
|
13
|
+
*
|
|
14
|
+
* This web trigger retrieves the current database schema from Atlassian Forge SQL.
|
|
15
|
+
* It generates SQL statements that can be used to recreate the database structure.
|
|
16
|
+
*
|
|
17
|
+
* @warning This trigger should ONLY be used in development environments. It:
|
|
18
|
+
* - Exposes your database structure
|
|
19
|
+
* - Disables foreign key checks temporarily
|
|
20
|
+
* - Generates SQL that could potentially be used maliciously
|
|
21
|
+
* - May expose sensitive table names and structures
|
|
22
|
+
*
|
|
23
|
+
* @returns {Promise<TriggerResponse<string>>} A response containing SQL statements to recreate the database schema
|
|
24
|
+
* - On success: Returns 200 status with SQL statements
|
|
25
|
+
* - On failure: Returns 500 status with error message
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```typescript
|
|
29
|
+
* // The response will contain SQL statements like:
|
|
30
|
+
* // SET foreign_key_checks = 0;
|
|
31
|
+
* // CREATE TABLE IF NOT EXISTS users (...);
|
|
32
|
+
* // CREATE TABLE IF NOT EXISTS orders (...);
|
|
33
|
+
* // SET foreign_key_checks = 1;
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export async function fetchSchemaWebTrigger(): Promise<TriggerResponse<string>> {
|
|
37
|
+
try {
|
|
38
|
+
const tables = await getTables();
|
|
39
|
+
const createTableStatements = await generateCreateTableStatements(tables);
|
|
40
|
+
const sqlStatements = wrapWithForeignKeyChecks(createTableStatements);
|
|
41
|
+
|
|
42
|
+
return getHttpResponse<string>(200, sqlStatements.join(";\n"));
|
|
43
|
+
} catch (error: unknown) {
|
|
44
|
+
console.error(JSON.stringify(error));
|
|
45
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
46
|
+
return getHttpResponse<string>(500, errorMessage);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Retrieves all tables from the database
|
|
52
|
+
*/
|
|
53
|
+
async function getTables(): Promise<string[]> {
|
|
54
|
+
const tables = await sql.executeDDL<string>("SHOW TABLES");
|
|
55
|
+
return tables.rows.flatMap((tableInfo) => Object.values(tableInfo));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Generates CREATE TABLE statements for each table
|
|
60
|
+
*/
|
|
61
|
+
async function generateCreateTableStatements(tables: string[]): Promise<string[]> {
|
|
62
|
+
const statements: string[] = [];
|
|
63
|
+
|
|
64
|
+
for (const table of tables) {
|
|
65
|
+
const createTableResult = await sql.executeDDL<CreateTableRow>(`SHOW CREATE TABLE ${table}`);
|
|
66
|
+
|
|
67
|
+
const createTableStatements = createTableResult.rows
|
|
68
|
+
.filter((row) => !isSystemTable(row.Table))
|
|
69
|
+
.map((row) => formatCreateTableStatement(row["Create Table"]));
|
|
70
|
+
|
|
71
|
+
statements.push(...createTableStatements);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return statements;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Checks if the table is a system table
|
|
79
|
+
*/
|
|
80
|
+
function isSystemTable(tableName: string): boolean {
|
|
81
|
+
return forgeSystemTables.some((st) => getTableName(st) === tableName);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Formats the CREATE TABLE statement
|
|
86
|
+
*/
|
|
87
|
+
function formatCreateTableStatement(statement: string): string {
|
|
88
|
+
return statement.replace(/"/g, "").replace("CREATE TABLE", "CREATE TABLE IF NOT EXISTS");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Wraps the SQL statements with foreign key check controls
|
|
93
|
+
*/
|
|
94
|
+
function wrapWithForeignKeyChecks(statements: string[]): string[] {
|
|
95
|
+
return ["SET foreign_key_checks = 0", ...statements, "SET foreign_key_checks = 1"];
|
|
96
|
+
}
|