forge-sql-orm 2.1.5 → 2.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +135 -53
  2. package/dist/ForgeSQLORM.js +572 -231
  3. package/dist/ForgeSQLORM.js.map +1 -1
  4. package/dist/ForgeSQLORM.mjs +572 -231
  5. package/dist/ForgeSQLORM.mjs.map +1 -1
  6. package/dist/core/ForgeSQLORM.d.ts +91 -3
  7. package/dist/core/ForgeSQLORM.d.ts.map +1 -1
  8. package/dist/core/ForgeSQLQueryBuilder.d.ts +89 -2
  9. package/dist/core/ForgeSQLQueryBuilder.d.ts.map +1 -1
  10. package/dist/core/SystemTables.d.ts +3654 -0
  11. package/dist/core/SystemTables.d.ts.map +1 -1
  12. package/dist/lib/drizzle/extensions/additionalActions.d.ts +2 -2
  13. package/dist/lib/drizzle/extensions/additionalActions.d.ts.map +1 -1
  14. package/dist/utils/forgeDriver.d.ts +61 -14
  15. package/dist/utils/forgeDriver.d.ts.map +1 -1
  16. package/dist/utils/metadataContextUtils.d.ts +1 -1
  17. package/dist/utils/metadataContextUtils.d.ts.map +1 -1
  18. package/dist/utils/requestTypeContextUtils.d.ts +8 -0
  19. package/dist/utils/requestTypeContextUtils.d.ts.map +1 -0
  20. package/dist/webtriggers/topSlowestStatementLastHourTrigger.d.ts +90 -65
  21. package/dist/webtriggers/topSlowestStatementLastHourTrigger.d.ts.map +1 -1
  22. package/package.json +9 -9
  23. package/src/core/ForgeSQLCrudOperations.ts +3 -3
  24. package/src/core/ForgeSQLORM.ts +334 -124
  25. package/src/core/ForgeSQLQueryBuilder.ts +116 -20
  26. package/src/core/ForgeSQLSelectOperations.ts +2 -2
  27. package/src/core/SystemTables.ts +16 -0
  28. package/src/lib/drizzle/extensions/additionalActions.ts +24 -22
  29. package/src/utils/cacheContextUtils.ts +2 -2
  30. package/src/utils/cacheUtils.ts +12 -12
  31. package/src/utils/forgeDriver.ts +219 -40
  32. package/src/utils/forgeDriverProxy.ts +2 -2
  33. package/src/utils/metadataContextUtils.ts +11 -13
  34. package/src/utils/requestTypeContextUtils.ts +11 -0
  35. package/src/utils/sqlUtils.ts +1 -1
  36. package/src/webtriggers/applyMigrationsWebTrigger.ts +9 -9
  37. package/src/webtriggers/clearCacheSchedulerTrigger.ts +1 -1
  38. package/src/webtriggers/dropMigrationWebTrigger.ts +2 -2
  39. package/src/webtriggers/dropTablesMigrationWebTrigger.ts +2 -2
  40. package/src/webtriggers/fetchSchemaWebTrigger.ts +1 -1
  41. package/src/webtriggers/topSlowestStatementLastHourTrigger.ts +511 -308
@@ -1,55 +1,234 @@
1
1
  import { sql, UpdateQueryResponse } from "@forge/sql";
2
- import {saveMetaDataInContextContext} from "./metadataContextUtils";
2
+ import { saveMetaDataToContext } from "./metadataContextUtils";
3
+ import { getOperationType } from "./requestTypeContextUtils";
3
4
 
5
+ /**
6
+ * Metadata structure for Forge SQL query results.
7
+ * Contains execution timing, response size, and field information.
8
+ */
4
9
  export type ForgeSQLMetadata = {
5
- dbExecutionTime: number,
6
- responseSize: number,
7
- fields: {
8
- "catalog": string,
9
- "name": string,
10
- "schema": string,
11
- "characterSet": number,
12
- "decimals": number,
13
- "table": string,
14
- "orgTable": string,
15
- "orgName": string,
16
- "flags": number,
17
- "columnType": number,
18
- "columnLength": number
19
- }[]
10
+ dbExecutionTime: number;
11
+ responseSize: number;
12
+ fields: {
13
+ catalog: string;
14
+ name: string;
15
+ schema: string;
16
+ characterSet: number;
17
+ decimals: number;
18
+ table: string;
19
+ orgTable: string;
20
+ orgName: string;
21
+ flags: number;
22
+ columnType: number;
23
+ columnLength: number;
24
+ }[];
20
25
  };
21
26
 
27
+ /**
28
+ * Result structure for Forge SQL queries.
29
+ * Contains rows data and execution metadata.
30
+ */
22
31
  export interface ForgeSQLResult {
23
32
  rows: Record<string, unknown>[] | Record<string, unknown>;
24
- metadata: ForgeSQLMetadata
33
+ metadata: ForgeSQLMetadata;
25
34
  }
26
35
 
27
- export const forgeDriver = async (
28
- query: string,
29
- params: any[],
30
- method: "all" | "execute",
31
- ): Promise<{
32
- rows: any[];
36
+ /**
37
+ * Driver result structure for Drizzle ORM compatibility.
38
+ */
39
+ export interface ForgeDriverResult {
40
+ rows: unknown[];
33
41
  insertId?: number;
34
42
  affectedRows?: number;
35
- }> => {
36
- if (method == "execute") {
37
- const sqlStatement = sql.prepare<UpdateQueryResponse>(query);
38
- if (params) {
39
- sqlStatement.bindParams(...params);
43
+ }
44
+
45
+ /**
46
+ * Query execution method types.
47
+ */
48
+ export type QueryMethod = "all" | "execute";
49
+
50
+ /**
51
+ * Type guard to check if an object is an UpdateQueryResponse.
52
+ *
53
+ * @param obj - The object to check
54
+ * @returns True if the object is an UpdateQueryResponse
55
+ */
56
+ export function isUpdateQueryResponse(obj: unknown): obj is UpdateQueryResponse {
57
+ return (
58
+ obj !== null &&
59
+ typeof obj === "object" &&
60
+ typeof (obj as any).affectedRows === "number" &&
61
+ typeof (obj as any).insertId === "number"
62
+ );
63
+ }
64
+
65
+ /**
66
+ * Executes a promise with a timeout.
67
+ *
68
+ * @param promise - The promise to execute
69
+ * @param timeoutMs - Timeout in milliseconds (default: 10000ms)
70
+ * @returns Promise that resolves with the result or rejects on timeout
71
+ * @throws {Error} When the operation times out
72
+ */
73
+ async function withTimeout<T>(promise: Promise<T>, timeoutMs: number = 10000): Promise<T> {
74
+ let timeoutId: ReturnType<typeof setTimeout> | undefined;
75
+
76
+ const timeoutPromise = new Promise<never>((_, reject) => {
77
+ timeoutId = setTimeout(() => {
78
+ reject(
79
+ new Error(
80
+ `Atlassian @forge/sql did not return a response within ${timeoutMs}ms (${timeoutMs / 1000} seconds), so the request is blocked. Possible causes: slow query, network issues, or exceeding Forge SQL limits.`,
81
+ ),
82
+ );
83
+ }, timeoutMs);
84
+ });
85
+
86
+ try {
87
+ return await Promise.race([promise, timeoutPromise]);
88
+ } finally {
89
+ if (timeoutId) {
90
+ clearTimeout(timeoutId);
40
91
  }
41
- const updateQueryResponseResults = await sqlStatement.execute();
42
- let result = updateQueryResponseResults.rows as any;
43
- return { ...result, rows: [result] };
44
- } else {
45
- const sqlStatement = await sql.prepare<unknown>(query);
46
- if (params) {
47
- await sqlStatement.bindParams(...params);
92
+ }
93
+ }
94
+
95
+ function inlineParams(sql: string, params: unknown[]): string {
96
+ let i = 0;
97
+ return sql.replace(/\?/g, () => {
98
+ const val = params[i++];
99
+ if (val === null) return "NULL";
100
+ if (typeof val === "number") return val.toString();
101
+ return `'${String(val).replace(/'/g, "''")}'`;
102
+ });
103
+ }
104
+
105
+ /**
106
+ * Processes DDL query results and saves metadata.
107
+ *
108
+ * @param result - The DDL query result
109
+ * @returns Processed result for Drizzle ORM
110
+ */
111
+ async function processDDLResult(method: QueryMethod, result: any): Promise<ForgeDriverResult> {
112
+ if (result.metadata) {
113
+ await saveMetaDataToContext(result.metadata as ForgeSQLMetadata);
114
+ }
115
+
116
+ if (!result?.rows) {
117
+ return { rows: [] };
118
+ }
119
+
120
+ if (isUpdateQueryResponse(result.rows)) {
121
+ const oneRow = result.rows as any;
122
+ return { ...oneRow, rows: [oneRow] };
123
+ }
124
+
125
+ if (Array.isArray(result.rows)) {
126
+ if (method === "execute") {
127
+ return { rows: result.rows };
128
+ } else {
129
+ const rows = (result.rows as any[]).map((r) => Object.values(r as Record<string, unknown>));
130
+ return { rows };
48
131
  }
49
- const result = (await sqlStatement.execute()) as ForgeSQLResult;
50
- await saveMetaDataInContextContext(result.metadata)
51
- let rows;
52
- rows = (result.rows as any[]).map((r) => Object.values(r as Record<string, unknown>));
53
- return { rows: rows };
54
132
  }
133
+
134
+ return { rows: [] };
135
+ }
136
+
137
+ /**
138
+ * Processes execute method results (UPDATE, INSERT, DELETE).
139
+ *
140
+ * @param query - The SQL query
141
+ * @param params - Query parameters
142
+ * @returns Processed result for Drizzle ORM
143
+ */
144
+ async function processExecuteMethod(query: string, params: unknown[]): Promise<ForgeDriverResult> {
145
+ const sqlStatement = sql.prepare<UpdateQueryResponse>(query);
146
+
147
+ if (params) {
148
+ sqlStatement.bindParams(...params);
149
+ }
150
+
151
+ const result = await withTimeout(sqlStatement.execute());
152
+ await saveMetaDataToContext(result.metadata as ForgeSQLMetadata);
153
+ if (!result?.rows) {
154
+ return { rows: [] };
155
+ }
156
+
157
+ if (isUpdateQueryResponse(result.rows)) {
158
+ const oneRow = result.rows as any;
159
+ return { ...oneRow, rows: [oneRow] };
160
+ }
161
+
162
+ return { rows: result.rows };
163
+ }
164
+
165
+ /**
166
+ * Processes all method results (SELECT queries).
167
+ *
168
+ * @param query - The SQL query
169
+ * @param params - Query parameters
170
+ * @returns Processed result for Drizzle ORM
171
+ */
172
+ async function processAllMethod(query: string, params: unknown[]): Promise<ForgeDriverResult> {
173
+ const sqlStatement = await sql.prepare<unknown>(query);
174
+
175
+ if (params) {
176
+ await sqlStatement.bindParams(...params);
177
+ }
178
+
179
+ const result = (await withTimeout(sqlStatement.execute())) as ForgeSQLResult;
180
+ await saveMetaDataToContext(result.metadata);
181
+
182
+ if (!result?.rows) {
183
+ return { rows: [] };
184
+ }
185
+
186
+ const rows = (result.rows as any[]).map((r) => Object.values(r as Record<string, unknown>));
187
+
188
+ return { rows };
189
+ }
190
+
191
+ /**
192
+ * Main Forge SQL driver function for Drizzle ORM integration.
193
+ * Handles DDL operations, execute operations (UPDATE/INSERT/DELETE), and select operations.
194
+ *
195
+ * @param query - The SQL query to execute
196
+ * @param params - Query parameters
197
+ * @param method - Execution method ("all" for SELECT, "execute" for UPDATE/INSERT/DELETE)
198
+ * @returns Promise with query results compatible with Drizzle ORM
199
+ *
200
+ * @throws {Error} When DDL operations are called with parameters
201
+ *
202
+ * @example
203
+ * ```typescript
204
+ * // DDL operation
205
+ * await forgeDriver("CREATE TABLE users (id INT)", [], "all");
206
+ *
207
+ * // SELECT operation
208
+ * await forgeDriver("SELECT * FROM users WHERE id = ?", [1], "all");
209
+ *
210
+ * // UPDATE operation
211
+ * await forgeDriver("UPDATE users SET name = ? WHERE id = ?", ["John", 1], "execute");
212
+ * ```
213
+ */
214
+ export const forgeDriver = async (
215
+ query: string,
216
+ params: unknown[],
217
+ method: QueryMethod,
218
+ ): Promise<ForgeDriverResult> => {
219
+ const operationType = await getOperationType();
220
+
221
+ // Handle DDL operations
222
+ if (operationType === "DDL") {
223
+ const result = await withTimeout(sql.executeDDL(inlineParams(query, params)));
224
+ return await processDDLResult(method, result);
225
+ }
226
+
227
+ // Handle execute method (UPDATE, INSERT, DELETE)
228
+ if (method === "execute") {
229
+ return await processExecuteMethod(query, params ?? []);
230
+ }
231
+
232
+ // Handle all method (SELECT)
233
+ return await processAllMethod(query, params ?? []);
55
234
  };
@@ -19,7 +19,7 @@ export function createForgeDriverProxy(options?: SqlHints, logRawSqlQuery?: bool
19
19
  const modifiedQuery = injectSqlHints(query, options);
20
20
 
21
21
  if (options && logRawSqlQuery && modifiedQuery !== query) {
22
- // eslint-disable-next-line no-console
22
+ // eslint-disable-next-line no-console
23
23
  console.debug("injected Hints: " + modifiedQuery);
24
24
  }
25
25
  try {
@@ -27,7 +27,7 @@ export function createForgeDriverProxy(options?: SqlHints, logRawSqlQuery?: bool
27
27
  return await forgeDriver(modifiedQuery, params, method);
28
28
  } catch (error) {
29
29
  if (logRawSqlQuery) {
30
- // eslint-disable-next-line no-console
30
+ // eslint-disable-next-line no-console
31
31
  console.debug("SQL Error:", JSON.stringify(error));
32
32
  }
33
33
  throw error;
@@ -1,24 +1,22 @@
1
1
  import { AsyncLocalStorage } from "node:async_hooks";
2
- import {ForgeSQLMetadata} from "./forgeDriver";
2
+ import { ForgeSQLMetadata } from "./forgeDriver";
3
3
 
4
4
  export type MetadataQueryContext = {
5
- totalDbExecutionTime: number,
6
- totalResponseSize: number,
7
- lastMetadata?: ForgeSQLMetadata;
8
- }
5
+ totalDbExecutionTime: number;
6
+ totalResponseSize: number;
7
+ lastMetadata?: ForgeSQLMetadata;
8
+ };
9
9
  export const metadataQueryContext = new AsyncLocalStorage<MetadataQueryContext>();
10
10
 
11
- export async function saveMetaDataInContextContext(
12
- metadata: ForgeSQLMetadata,
13
- ): Promise<void> {
11
+ export async function saveMetaDataToContext(metadata: ForgeSQLMetadata): Promise<void> {
14
12
  const context = metadataQueryContext.getStore();
15
13
  if (context && metadata) {
16
- context.totalResponseSize += metadata.responseSize
17
- context.totalDbExecutionTime += metadata.dbExecutionTime
18
- context.lastMetadata = metadata;
14
+ context.totalResponseSize += metadata.responseSize;
15
+ context.totalDbExecutionTime += metadata.dbExecutionTime;
16
+ context.lastMetadata = metadata;
19
17
  }
20
18
  }
21
19
 
22
- export async function getLastestMetadata():Promise<MetadataQueryContext|undefined> {
23
- return metadataQueryContext.getStore();
20
+ export async function getLastestMetadata(): Promise<MetadataQueryContext | undefined> {
21
+ return metadataQueryContext.getStore();
24
22
  }
@@ -0,0 +1,11 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ export type OperationType = "DML" | "DDL";
3
+
4
+ export type OperationTypeQueryContext = {
5
+ operationType: OperationType;
6
+ };
7
+ export const operationTypeQueryContext = new AsyncLocalStorage<OperationTypeQueryContext>();
8
+
9
+ export async function getOperationType(): Promise<OperationType> {
10
+ return operationTypeQueryContext.getStore()?.operationType ?? "DML";
11
+ }
@@ -329,7 +329,7 @@ export function generateDropTableStatements(
329
329
  const dropStatements: string[] = [];
330
330
  const validOptions = options ?? { sequence: true, table: true };
331
331
  if (!validOptions.sequence && !validOptions.table) {
332
- // eslint-disable-next-line no-console
332
+ // eslint-disable-next-line no-console
333
333
  console.warn('No drop operations requested: both "table" and "sequence" options are false');
334
334
  return [];
335
335
  }
@@ -31,15 +31,15 @@ export const applySchemaMigrations = async (
31
31
  if (typeof migration !== "function") {
32
32
  throw new Error("migration is not a function");
33
33
  }
34
- // eslint-disable-next-line no-console
35
- console.log("Provisioning the database");
34
+ // eslint-disable-next-line no-console
35
+ console.debug("Provisioning the database");
36
36
  await sql._provision();
37
- // eslint-disable-next-line no-console
38
- console.info("Running schema migrations");
37
+ // eslint-disable-next-line no-console
38
+ console.debug("Running schema migrations");
39
39
  const migrations = await migration(migrationRunner);
40
40
  const successfulMigrations = await migrations.run();
41
- // eslint-disable-next-line no-console
42
- console.info("Migrations applied:", successfulMigrations);
41
+ // eslint-disable-next-line no-console
42
+ console.debug("Migrations applied:", successfulMigrations);
43
43
 
44
44
  const migrationList = await migrationRunner.list();
45
45
  let migrationHistory = "No migrations found";
@@ -53,8 +53,8 @@ export const applySchemaMigrations = async (
53
53
  .map((y) => `${y.id}, ${y.name}, ${y.migratedAt.toUTCString()}`)
54
54
  .join("\n");
55
55
  }
56
- // eslint-disable-next-line no-console
57
- console.info("Migrations history:\nid, name, migrated_at\n", migrationHistory);
56
+ // eslint-disable-next-line no-console
57
+ console.debug("Migrations history:\nid, name, migrated_at\n", migrationHistory);
58
58
 
59
59
  return {
60
60
  headers: { "Content-Type": ["application/json"] },
@@ -70,7 +70,7 @@ export const applySchemaMigrations = async (
70
70
  error?.debug?.context?.message ??
71
71
  error.message ??
72
72
  "Unknown error occurred";
73
- // eslint-disable-next-line no-console
73
+ // eslint-disable-next-line no-console
74
74
  console.error("Error during migration:", errorMessage);
75
75
  return {
76
76
  headers: { "Content-Type": ["application/json"] },
@@ -64,7 +64,7 @@ export const clearCacheSchedulerTrigger = async (options?: ForgeSqlOrmOptions) =
64
64
  }),
65
65
  };
66
66
  } catch (error) {
67
- // eslint-disable-next-line no-console
67
+ // eslint-disable-next-line no-console
68
68
  console.error("Error during cache cleanup: ", JSON.stringify(error));
69
69
  return {
70
70
  headers: { "Content-Type": ["application/json"] },
@@ -34,7 +34,7 @@ export async function dropSchemaMigrations(): Promise<TriggerResponse<string>> {
34
34
 
35
35
  // Execute each statement
36
36
  for (const statement of dropStatements) {
37
- // eslint-disable-next-line no-console
37
+ // eslint-disable-next-line no-console
38
38
  console.debug(`execute DDL: ${statement}`);
39
39
  await sql.executeDDL(statement);
40
40
  }
@@ -49,7 +49,7 @@ export async function dropSchemaMigrations(): Promise<TriggerResponse<string>> {
49
49
  error?.debug?.message ??
50
50
  error.message ??
51
51
  "Unknown error occurred";
52
- // eslint-disable-next-line no-console
52
+ // eslint-disable-next-line no-console
53
53
  console.error(errorMessage);
54
54
  return getHttpResponse<string>(500, errorMessage);
55
55
  }
@@ -34,7 +34,7 @@ export async function dropTableSchemaMigrations(): Promise<TriggerResponse<strin
34
34
 
35
35
  // Execute each statement
36
36
  for (const statement of dropStatements) {
37
- // eslint-disable-next-line no-console
37
+ // eslint-disable-next-line no-console
38
38
  console.debug(`execute DDL: ${statement}`);
39
39
  await sql.executeDDL(statement);
40
40
  }
@@ -49,7 +49,7 @@ export async function dropTableSchemaMigrations(): Promise<TriggerResponse<strin
49
49
  error?.debug?.message ??
50
50
  error.message ??
51
51
  "Unknown error occurred";
52
- // eslint-disable-next-line no-console
52
+ // eslint-disable-next-line no-console
53
53
  console.error(errorMessage);
54
54
  return getHttpResponse<string>(500, errorMessage);
55
55
  }
@@ -46,7 +46,7 @@ export async function fetchSchemaWebTrigger(): Promise<TriggerResponse<string>>
46
46
  error?.debug?.message ??
47
47
  error.message ??
48
48
  "Unknown error occurred";
49
- // eslint-disable-next-line no-console
49
+ // eslint-disable-next-line no-console
50
50
  console.error(errorMessage);
51
51
  return getHttpResponse<string>(500, errorMessage);
52
52
  }