forge-sql-orm 2.0.11 → 2.0.12

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.
@@ -0,0 +1,351 @@
1
+ import { UpdateQueryResponse } from "@forge/sql";
2
+ import { SqlParameters } from "@forge/sql/out/sql-statement";
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";
13
+ import { InferInsertModel, SQL } from "drizzle-orm";
14
+ import moment from "moment/moment";
15
+ import { parseDateTime } from "../utils/sqlUtils";
16
+ import { MySqlRemoteDatabase, MySqlRemotePreparedQueryHKT } from "drizzle-orm/mysql-proxy/index";
17
+
18
+ // ============= Core Types =============
19
+
20
+ /**
21
+ * Interface representing the main ForgeSQL operations.
22
+ * Provides access to CRUD operations and schema-level SQL operations.
23
+ */
24
+ export interface ForgeSqlOperation extends QueryBuilderForgeSql {
25
+ /**
26
+ * Provides CRUD (Create, Read, Update, Delete) operations.
27
+ * @returns {CRUDForgeSQL} Interface for performing CRUD operations
28
+ */
29
+ crud(): CRUDForgeSQL;
30
+
31
+ /**
32
+ * Provides schema-level SQL fetch operations.
33
+ * @returns {SchemaSqlForgeSql} Interface for executing schema-bound SQL queries
34
+ */
35
+ fetch(): SchemaSqlForgeSql;
36
+ }
37
+
38
+ /**
39
+ * Interface for Query Builder operations.
40
+ * Provides access to the underlying Drizzle ORM query builder.
41
+ */
42
+ export interface QueryBuilderForgeSql {
43
+ /**
44
+ * Creates a new query builder for the given entity.
45
+ * @returns {MySql2Database<Record<string, unknown>>} The Drizzle database instance for building queries
46
+ */
47
+ getDrizzleQueryBuilder(): MySqlRemoteDatabase<Record<string, unknown>>;
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
+ */
65
+ select<TSelection extends SelectedFields>(
66
+ fields: TSelection,
67
+ ): MySqlSelectBuilder<TSelection, MySqlRemotePreparedQueryHKT>;
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
+ */
85
+ selectDistinct<TSelection extends SelectedFields>(
86
+ fields: TSelection,
87
+ ): MySqlSelectBuilder<TSelection, MySqlRemotePreparedQueryHKT>;
88
+ }
89
+
90
+ // ============= CRUD Operations =============
91
+
92
+ /**
93
+ * Interface for CRUD (Create, Read, Update, Delete) operations.
94
+ * Provides methods for basic database operations with support for optimistic locking.
95
+ */
96
+ export interface CRUDForgeSQL {
97
+ /**
98
+ * Inserts multiple records into the database.
99
+ * @template T - The type of the table schema
100
+ * @param {T} schema - The entity schema
101
+ * @param {Partial<InferInsertModel<T>>[]} models - The list of entities to insert
102
+ * @param {boolean} [updateIfExists] - Whether to update the row if it already exists (default: false)
103
+ * @returns {Promise<number>} The number of inserted rows
104
+ * @throws {Error} If the insert operation fails
105
+ */
106
+ insert<T extends AnyMySqlTable>(
107
+ schema: T,
108
+ models: Partial<InferInsertModel<T>>[],
109
+ updateIfExists?: boolean,
110
+ ): Promise<number>;
111
+
112
+ /**
113
+ * Deletes a record by its ID.
114
+ * @template T - The type of the table schema
115
+ * @param {unknown} id - The ID of the record to delete
116
+ * @param {T} schema - The entity schema
117
+ * @returns {Promise<number>} The number of rows affected
118
+ * @throws {Error} If the delete operation fails
119
+ */
120
+ deleteById<T extends AnyMySqlTable>(id: unknown, schema: T): Promise<number>;
121
+
122
+ /**
123
+ * Updates a record by its ID with optimistic locking support.
124
+ * If a version field is defined in the schema, versioning is applied:
125
+ * - the current record version is retrieved
126
+ * - checked for concurrent modifications
127
+ * - and then incremented
128
+ *
129
+ * @template T - The type of the table schema
130
+ * @param {Partial<InferInsertModel<T>>} entity - The entity with updated values
131
+ * @param {T} schema - The entity schema
132
+ * @returns {Promise<number>} The number of rows affected
133
+ * @throws {Error} If the primary key is not included in the update fields
134
+ * @throws {Error} If optimistic locking check fails
135
+ */
136
+ updateById<T extends AnyMySqlTable>(
137
+ entity: Partial<InferInsertModel<T>>,
138
+ schema: T,
139
+ ): Promise<number>;
140
+
141
+ /**
142
+ * Updates specified fields of records based on provided conditions.
143
+ * If the "where" parameter is not provided, the WHERE clause is built from the entity fields
144
+ * that are not included in the list of fields to update.
145
+ *
146
+ * @template T - The type of the table schema
147
+ * @param {Partial<InferInsertModel<T>>} updateData - The object containing values to update
148
+ * @param {T} schema - The entity schema
149
+ * @param {SQL<unknown>} [where] - Optional filtering conditions for the WHERE clause
150
+ * @returns {Promise<number>} The number of affected rows
151
+ * @throws {Error} If no filtering criteria are provided
152
+ * @throws {Error} If the update operation fails
153
+ */
154
+ updateFields<T extends AnyMySqlTable>(
155
+ updateData: Partial<InferInsertModel<T>>,
156
+ schema: T,
157
+ where?: SQL<unknown>,
158
+ ): Promise<number>;
159
+ }
160
+
161
+ // ============= Schema SQL Operations =============
162
+
163
+ /**
164
+ * Interface for schema-level SQL operations.
165
+ * Provides methods for executing SQL queries with schema binding and type safety.
166
+ */
167
+ export interface SchemaSqlForgeSql {
168
+ /**
169
+ * Executes a Drizzle query and returns a single result.
170
+ * @template T - The type of the query builder
171
+ * @param {T} query - The Drizzle query to execute
172
+ * @returns {Promise<Awaited<T> extends Array<any> ? Awaited<T>[number] | undefined : Awaited<T> | undefined>} A single result object or undefined
173
+ * @throws {Error} If more than one record is returned
174
+ * @throws {Error} If the query execution fails
175
+ */
176
+ executeQueryOnlyOne<T extends MySqlSelectDynamic<AnyMySqlSelectQueryBuilder>>(
177
+ query: T,
178
+ ): Promise<
179
+ Awaited<T> extends Array<any> ? Awaited<T>[number] | undefined : Awaited<T> | undefined
180
+ >;
181
+
182
+ /**
183
+ * Executes a raw SQL query and returns the results.
184
+ * @template T - The type of the result objects
185
+ * @param {string} query - The raw SQL query
186
+ * @param {SqlParameters[]} [params] - Optional SQL parameters
187
+ * @returns {Promise<T[]>} A list of results as objects
188
+ * @throws {Error} If the query execution fails
189
+ */
190
+ executeRawSQL<T extends object | unknown>(query: string, params?: SqlParameters[]): Promise<T[]>;
191
+
192
+ /**
193
+ * Executes a raw SQL update query.
194
+ * @param {string} query - The raw SQL update query
195
+ * @param {SqlParameters[]} [params] - Optional SQL parameters
196
+ * @returns {Promise<UpdateQueryResponse>} The update response containing affected rows
197
+ * @throws {Error} If the update operation fails
198
+ */
199
+ executeRawUpdateSQL(query: string, params?: unknown[]): Promise<UpdateQueryResponse>;
200
+ }
201
+
202
+ // ============= Configuration Types =============
203
+
204
+ /**
205
+ * Interface for version field metadata.
206
+ * Defines the configuration for optimistic locking version fields.
207
+ */
208
+ export interface VersionFieldMetadata {
209
+ /** Name of the version field */
210
+ fieldName: string;
211
+ }
212
+
213
+ /**
214
+ * Interface for table metadata.
215
+ * Defines the configuration for a specific table.
216
+ */
217
+ export interface TableMetadata {
218
+ /** Name of the table */
219
+ tableName: string;
220
+ /** Version field configuration for optimistic locking */
221
+ versionField: VersionFieldMetadata;
222
+ }
223
+
224
+ /**
225
+ * Type for additional metadata configuration.
226
+ * Maps table names to their metadata configuration.
227
+ */
228
+ export type AdditionalMetadata = Record<string, TableMetadata>;
229
+
230
+ /**
231
+ * Options for configuring ForgeSQL ORM behavior.
232
+ */
233
+ export interface ForgeSqlOrmOptions {
234
+ /**
235
+ * Enables logging of raw SQL queries in the Atlassian Forge Developer Console.
236
+ * Useful for debugging and monitoring SQL operations.
237
+ * @default false
238
+ */
239
+ logRawSqlQuery?: boolean;
240
+
241
+ /**
242
+ * Disables optimistic locking for all operations.
243
+ * When enabled, version checks are skipped during updates.
244
+ * @default false
245
+ */
246
+ disableOptimisticLocking?: boolean;
247
+
248
+ /**
249
+ * Additional metadata for table configuration.
250
+ * Allows specifying table-specific settings and behaviors.
251
+ * @example
252
+ * ```typescript
253
+ * {
254
+ * users: {
255
+ * tableName: "users",
256
+ * versionField: {
257
+ * fieldName: "updatedAt",
258
+ * type: "datetime",
259
+ * nullable: false
260
+ * }
261
+ * }
262
+ * }
263
+ * ```
264
+ */
265
+ additionalMetadata?: AdditionalMetadata;
266
+ }
267
+
268
+ // ============= Custom Types =============
269
+
270
+ /**
271
+ * Custom type for MySQL datetime fields.
272
+ * Handles conversion between JavaScript Date objects and MySQL datetime strings.
273
+ */
274
+ export const forgeDateTimeString = customType<{
275
+ data: Date;
276
+ driver: string;
277
+ config: { format?: string };
278
+ }>({
279
+ dataType() {
280
+ return "datetime";
281
+ },
282
+ toDriver(value: Date) {
283
+ return moment(value as Date).format("YYYY-MM-DDTHH:mm:ss.SSS");
284
+ },
285
+ fromDriver(value: unknown) {
286
+ const format = "YYYY-MM-DDTHH:mm:ss.SSS";
287
+ return parseDateTime(value as string, format);
288
+ },
289
+ });
290
+
291
+ /**
292
+ * Custom type for MySQL timestamp fields.
293
+ * Handles conversion between JavaScript Date objects and MySQL timestamp strings.
294
+ */
295
+ export const forgeTimestampString = customType<{
296
+ data: Date;
297
+ driver: string;
298
+ config: { format?: string };
299
+ }>({
300
+ dataType() {
301
+ return "timestamp";
302
+ },
303
+ toDriver(value: Date) {
304
+ return moment(value as Date).format("YYYY-MM-DDTHH:mm:ss.SSS");
305
+ },
306
+ fromDriver(value: unknown) {
307
+ const format = "YYYY-MM-DDTHH:mm:ss.SSS";
308
+ return parseDateTime(value as string, format);
309
+ },
310
+ });
311
+
312
+ /**
313
+ * Custom type for MySQL date fields.
314
+ * Handles conversion between JavaScript Date objects and MySQL date strings.
315
+ */
316
+ export const forgeDateString = customType<{
317
+ data: Date;
318
+ driver: string;
319
+ config: { format?: string };
320
+ }>({
321
+ dataType() {
322
+ return "date";
323
+ },
324
+ toDriver(value: Date) {
325
+ return moment(value as Date).format("YYYY-MM-DD");
326
+ },
327
+ fromDriver(value: unknown) {
328
+ const format = "YYYY-MM-DD";
329
+ return parseDateTime(value as string, format);
330
+ },
331
+ });
332
+
333
+ /**
334
+ * Custom type for MySQL time fields.
335
+ * Handles conversion between JavaScript Date objects and MySQL time strings.
336
+ */
337
+ export const forgeTimeString = customType<{
338
+ data: Date;
339
+ driver: string;
340
+ config: { format?: string };
341
+ }>({
342
+ dataType() {
343
+ return "time";
344
+ },
345
+ toDriver(value: Date) {
346
+ return moment(value as Date).format("HH:mm:ss.SSS");
347
+ },
348
+ fromDriver(value: unknown) {
349
+ return parseDateTime(value as string, "HH:mm:ss.SSS");
350
+ },
351
+ });
@@ -0,0 +1,93 @@
1
+ import { sql, UpdateQueryResponse } from "@forge/sql";
2
+ import { ForgeSqlOrmOptions, SchemaSqlForgeSql } from "./ForgeSQLQueryBuilder";
3
+ import {
4
+ AnyMySqlSelectQueryBuilder,
5
+ MySqlSelectDynamic,
6
+ } from "drizzle-orm/mysql-core/query-builders/select.types";
7
+
8
+ /**
9
+ * Class implementing SQL select operations for ForgeSQL ORM.
10
+ * Provides methods for executing queries and mapping results to entity types.
11
+ */
12
+ export class ForgeSQLSelectOperations implements SchemaSqlForgeSql {
13
+ private readonly options: ForgeSqlOrmOptions;
14
+
15
+ /**
16
+ * Creates a new instance of ForgeSQLSelectOperations.
17
+ * @param {ForgeSqlOrmOptions} options - Configuration options for the ORM
18
+ */
19
+ constructor(options: ForgeSqlOrmOptions) {
20
+ this.options = options;
21
+ }
22
+
23
+ /**
24
+ * Executes a Drizzle query and returns a single result.
25
+ * Throws an error if more than one record is returned.
26
+ *
27
+ * @template T - The type of the query builder
28
+ * @param {T} query - The Drizzle query to execute
29
+ * @returns {Promise<Awaited<T> extends Array<any> ? Awaited<T>[number] | undefined : Awaited<T> | undefined>} A single result object or undefined
30
+ * @throws {Error} If more than one record is returned
31
+ */
32
+ async executeQueryOnlyOne<T extends MySqlSelectDynamic<AnyMySqlSelectQueryBuilder>>(
33
+ query: T,
34
+ ): Promise<
35
+ Awaited<T> extends Array<any> ? Awaited<T>[number] | undefined : Awaited<T> | undefined
36
+ > {
37
+ const results: Awaited<T> = await query;
38
+ const datas = results as unknown[];
39
+ if (!datas.length) {
40
+ return undefined;
41
+ }
42
+ if (datas.length > 1) {
43
+ throw new Error(`Expected 1 record but returned ${datas.length}`);
44
+ }
45
+
46
+ return datas[0] as Awaited<T> extends Array<any> ? Awaited<T>[number] : Awaited<T>;
47
+ }
48
+
49
+ /**
50
+ * Executes a raw SQL query and returns the results.
51
+ * Logs the query if logging is enabled.
52
+ *
53
+ * @template T - The type of the result objects
54
+ * @param {string} query - The raw SQL query to execute
55
+ * @param {SqlParameters[]} [params] - Optional SQL parameters
56
+ * @returns {Promise<T[]>} A list of results as objects
57
+ */
58
+ async executeRawSQL<T extends object | unknown>(query: string, params?: unknown[]): Promise<T[]> {
59
+ if (this.options.logRawSqlQuery) {
60
+ console.debug(
61
+ `Executing with SQL ${query}` + params ? `, with params: ${JSON.stringify(params)}` : "",
62
+ );
63
+ }
64
+ const sqlStatement = sql.prepare<T>(query);
65
+ if (params) {
66
+ sqlStatement.bindParams(...params);
67
+ }
68
+ const result = await sqlStatement.execute();
69
+ return result.rows as T[];
70
+ }
71
+
72
+ /**
73
+ * Executes a raw SQL update query.
74
+ * @param {string} query - The raw SQL update query
75
+ * @param {SqlParameters[]} [params] - Optional SQL parameters
76
+ * @returns {Promise<UpdateQueryResponse>} The update response containing affected rows
77
+ */
78
+ async executeRawUpdateSQL(query: string, params?: unknown[]): Promise<UpdateQueryResponse> {
79
+ const sqlStatement = sql.prepare<UpdateQueryResponse>(query);
80
+ if (params) {
81
+ sqlStatement.bindParams(...params);
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
+ }
90
+ const updateQueryResponseResults = await sqlStatement.execute();
91
+ return updateQueryResponseResults.rows;
92
+ }
93
+ }
@@ -0,0 +1,10 @@
1
+ import { bigint, mysqlTable, timestamp, varchar } from "drizzle-orm/mysql-core";
2
+ import { Table } from "drizzle-orm";
3
+
4
+ export const migrations = mysqlTable("__migrations", {
5
+ id: bigint("id", { mode: "number" }).primaryKey().autoincrement(),
6
+ name: varchar("name", { length: 255 }).notNull(),
7
+ migratedAt: timestamp("migratedAt").defaultNow().notNull(),
8
+ });
9
+
10
+ export const forgeSystemTables: Table[] = [migrations];
package/src/index.ts ADDED
@@ -0,0 +1,11 @@
1
+ import ForgeSQLORM from "./core/ForgeSQLORM";
2
+
3
+ export * from "./core/ForgeSQLQueryBuilder";
4
+ export * from "./core/ForgeSQLCrudOperations";
5
+ export * from "./core/ForgeSQLSelectOperations";
6
+ export * from "./utils/sqlUtils";
7
+ export * from "./utils/forgeDriver";
8
+ export * from "./webtriggers";
9
+ export * from "./lib/drizzle/extensions/selectAliased";
10
+
11
+ export default ForgeSQLORM;
@@ -0,0 +1,74 @@
1
+ import {MySqlRemoteDatabase} from "drizzle-orm/mysql-proxy";
2
+ import type {SelectedFields} from "drizzle-orm/mysql-core/query-builders/select.types";
3
+ import {applyFromDriverTransform, mapSelectFieldsWithAlias} from "../../..";
4
+ import {MySqlSelectBuilder} from "drizzle-orm/mysql-core";
5
+ import {MySqlRemotePreparedQueryHKT} from "drizzle-orm/mysql-proxy";
6
+
7
+ function createAliasedSelectBuilder<TSelection extends SelectedFields>(
8
+ db: MySqlRemoteDatabase<any>,
9
+ fields: TSelection,
10
+ selectFn: (selections: any) => MySqlSelectBuilder<TSelection, MySqlRemotePreparedQueryHKT>
11
+ ): MySqlSelectBuilder<TSelection, MySqlRemotePreparedQueryHKT> {
12
+ const { selections, aliasMap } = mapSelectFieldsWithAlias(fields);
13
+ const builder = selectFn(selections);
14
+
15
+ const wrapBuilder = (rawBuilder: any): any => {
16
+ return new Proxy(rawBuilder, {
17
+ get(target, prop, receiver) {
18
+ if (prop === 'execute') {
19
+ return async (...args: any[]) => {
20
+ const rows = await target.execute(...args);
21
+ return applyFromDriverTransform(rows, selections, aliasMap);
22
+ };
23
+ }
24
+
25
+ if (prop === 'then') {
26
+ return (onfulfilled: any, onrejected: any) =>
27
+ target.execute().then(
28
+ (rows: unknown[]) => {
29
+ const transformed = applyFromDriverTransform(rows, selections, aliasMap);
30
+ return onfulfilled?.(transformed);
31
+ },
32
+ onrejected,
33
+ );
34
+ }
35
+
36
+ const value = Reflect.get(target, prop, receiver);
37
+
38
+ if (typeof value === 'function') {
39
+ return (...args: any[]) => {
40
+ const result = value.apply(target, args);
41
+
42
+ if (typeof result === 'object' && result !== null && 'execute' in result) {
43
+ return wrapBuilder(result);
44
+ }
45
+
46
+ return result;
47
+ };
48
+ }
49
+
50
+ return value;
51
+ },
52
+ });
53
+ };
54
+
55
+ return wrapBuilder(builder);
56
+ }
57
+
58
+ export function patchDbWithSelectAliased(db: MySqlRemoteDatabase<any>): MySqlRemoteDatabase<any> & {
59
+ selectAliased:<TSelection extends SelectedFields>(
60
+ fields: TSelection,
61
+ )=> MySqlSelectBuilder<TSelection, MySqlRemotePreparedQueryHKT>,
62
+ selectAliasedDistinct:<TSelection extends SelectedFields>(
63
+ fields: TSelection,
64
+ )=> MySqlSelectBuilder<TSelection, MySqlRemotePreparedQueryHKT> } {
65
+ db.selectAliased = function <TSelection extends SelectedFields>(fields: TSelection) {
66
+ return createAliasedSelectBuilder(db, fields, (selections) => db.select(selections));
67
+ };
68
+
69
+ db.selectAliasedDistinct = function <TSelection extends SelectedFields>(fields: TSelection) {
70
+ return createAliasedSelectBuilder(db, fields, (selections) => db.selectDistinct(selections));
71
+ };
72
+
73
+ return db;
74
+ }
@@ -0,0 +1,14 @@
1
+ import { SelectedFields } from 'drizzle-orm';
2
+ import {MySqlSelectBuilder} from "drizzle-orm/mysql-core";
3
+ import {MySqlRemotePreparedQueryHKT} from "drizzle-orm/mysql-proxy";
4
+
5
+ declare module 'drizzle-orm/mysql-proxy' {
6
+ interface MySqlRemoteDatabase<> {
7
+ selectAliased<TSelection extends SelectedFields>(
8
+ fields: TSelection,
9
+ ): MySqlSelectBuilder<TSelection, MySqlRemotePreparedQueryHKT>;
10
+ selectAliasedDistinct<TSelection extends SelectedFields>(
11
+ fields: TSelection,
12
+ ): MySqlSelectBuilder<TSelection, MySqlRemotePreparedQueryHKT>;
13
+ }
14
+ }
@@ -0,0 +1,39 @@
1
+ import { sql, UpdateQueryResponse } from "@forge/sql";
2
+
3
+ interface ForgeSQLResult {
4
+ rows: Record<string, unknown>[] | Record<string, unknown>;
5
+ }
6
+
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 };
34
+ }
35
+ } catch (error) {
36
+ console.error("SQL Error:", JSON.stringify(error));
37
+ throw error;
38
+ }
39
+ };