• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

vzakharchenko / forge-sql-orm / 17374695647

01 Sep 2025 10:18AM UTC coverage: 86.819% (+0.1%) from 86.696%
17374695647

push

github

web-flow
Merge pull request #565 from vzakharchenko/remove-moment

Remove moment

267 of 329 branches covered (81.16%)

Branch coverage included in aggregate %.

18 of 23 new or added lines in 3 files covered. (78.26%)

1103 of 1249 relevant lines covered (88.31%)

11.14 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

72.88
/src/core/ForgeSQLQueryBuilder.ts
1
import { UpdateQueryResponse } from "@forge/sql";
2
import { SqlParameters } from "@forge/sql/out/sql-statement";
3
import {
1✔
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, Query, SQL } from "drizzle-orm";
14
import { DateTime } from "luxon";
1✔
15
import { parseDateTime } from "../utils/sqlUtils";
1✔
16
import { MySqlRemoteDatabase, MySqlRemotePreparedQueryHKT } from "drizzle-orm/mysql-proxy/index";
17
import { SqlHints } from "../utils/sqlHints";
18
import {
19
  ClusterStatementRowCamelCase,
20
  ExplainAnalyzeRow,
21
  SlowQueryNormalized,
22
} from "./SystemTables";
23

24
/**
25
 * Core interface for ForgeSQL operations.
26
 * Provides access to CRUD operations, schema-level SQL operations, and query analysis capabilities.
27
 *
28
 * @interface ForgeSqlOperation
29
 * @extends {QueryBuilderForgeSql}
30
 */
31
export interface ForgeSqlOperation extends QueryBuilderForgeSql {
32
  /**
33
   * Provides CRUD (Create, Update, Delete) operations.
34
   * @deprecated Use modify() instead for better type safety and consistency
35
   * @returns {CRUDForgeSQL} Interface for performing CRUD operations
36
   */
37
  crud(): CRUDForgeSQL;
38

39
  /**
40
   * Provides modify (Create, Update, Delete) operations with optimistic locking support.
41
   * @returns {CRUDForgeSQL} Interface for performing CRUD operations
42
   */
43
  modify(): CRUDForgeSQL;
44

45
  /**
46
   * Provides schema-level SQL fetch operations with type safety.
47
   * @returns {SchemaSqlForgeSql} Interface for executing schema-bound SQL queries
48
   */
49
  fetch(): SchemaSqlForgeSql;
50

51
  /**
52
   * Provides query analysis capabilities including EXPLAIN ANALYZE and slow query analysis.
53
   * @returns {SchemaAnalyzeForgeSql} Interface for analyzing query performance
54
   */
55
  analyze(): SchemaAnalyzeForgeSql;
56
}
57

58
/**
59
 * Interface for Query Builder operations.
60
 * Provides access to the underlying Drizzle ORM query builder with enhanced functionality.
61
 *
62
 * @interface QueryBuilderForgeSql
63
 */
64
export interface QueryBuilderForgeSql {
65
  /**
66
   * Creates a new query builder for the given entity.
67
   * @returns {MySqlRemoteDatabase<Record<string, unknown>>} The Drizzle database instance for building queries
68
   */
69
  getDrizzleQueryBuilder(): MySqlRemoteDatabase<Record<string, unknown>>;
70

71
  /**
72
   * Creates a select query with unique field aliases to prevent field name collisions in joins.
73
   * This is particularly useful when working with Atlassian Forge SQL, which collapses fields with the same name in joined tables.
74
   *
75
   * @template TSelection - The type of the selected fields
76
   * @param {TSelection} fields - Object containing the fields to select, with table schemas as values
77
   * @returns {MySqlSelectBuilder<TSelection, MySqlRemotePreparedQueryHKT>} A select query builder with unique field aliases
78
   * @throws {Error} If fields parameter is empty
79
   * @example
80
   * ```typescript
81
   * await forgeSQL
82
   *   .select({user: users, order: orders})
83
   *   .from(orders)
84
   *   .innerJoin(users, eq(orders.userId, users.id));
85
   * ```
86
   */
87
  select<TSelection extends SelectedFields>(
88
    fields: TSelection,
89
  ): MySqlSelectBuilder<TSelection, MySqlRemotePreparedQueryHKT>;
90

91
  /**
92
   * Creates a distinct select query with unique field aliases to prevent field name collisions in joins.
93
   * This is particularly useful when working with Atlassian Forge SQL, which collapses fields with the same name in joined tables.
94
   *
95
   * @template TSelection - The type of the selected fields
96
   * @param {TSelection} fields - Object containing the fields to select, with table schemas as values
97
   * @returns {MySqlSelectBuilder<TSelection, MySqlRemotePreparedQueryHKT>} A distinct select query builder with unique field aliases
98
   * @throws {Error} If fields parameter is empty
99
   * @example
100
   * ```typescript
101
   * await forgeSQL
102
   *   .selectDistinct({user: users, order: orders})
103
   *   .from(orders)
104
   *   .innerJoin(users, eq(orders.userId, users.id));
105
   * ```
106
   */
107
  selectDistinct<TSelection extends SelectedFields>(
108
    fields: TSelection,
109
  ): MySqlSelectBuilder<TSelection, MySqlRemotePreparedQueryHKT>;
110
}
111

112
/**
113
 * Interface for Modify (Create, Update, Delete) operations.
114
 * Provides methods for basic database operations with support for optimistic locking.
115
 *
116
 * @interface CRUDForgeSQL
117
 */
118
export interface CRUDForgeSQL {
119
  /**
120
   * Inserts multiple records into the database.
121
   * @template T - The type of the table schema
122
   * @param {T} schema - The entity schema
123
   * @param {InferInsertModel<T>[]} models - The list of entities to insert
124
   * @param {boolean} [updateIfExists] - Whether to update the row if it already exists (default: false)
125
   * @returns {Promise<number>} The number of inserted rows
126
   * @throws {Error} If the insert operation fails
127
   */
128
  insert<T extends AnyMySqlTable>(
129
    schema: T,
130
    models: InferInsertModel<T>[],
131
    updateIfExists?: boolean,
132
  ): Promise<number>;
133

134
  /**
135
   * Deletes a record by its ID.
136
   * @template T - The type of the table schema
137
   * @param {unknown} id - The ID of the record to delete
138
   * @param {T} schema - The entity schema
139
   * @returns {Promise<number>} The number of rows affected
140
   * @throws {Error} If the delete operation fails
141
   */
142
  deleteById<T extends AnyMySqlTable>(id: unknown, schema: T): Promise<number>;
143

144
  /**
145
   * Updates a record by its ID with optimistic locking support.
146
   * If a version field is defined in the schema, versioning is applied:
147
   * - the current record version is retrieved
148
   * - checked for concurrent modifications
149
   * - and then incremented
150
   *
151
   * @template T - The type of the table schema
152
   * @param {Partial<InferInsertModel<T>>} entity - The entity with updated values
153
   * @param {T} schema - The entity schema
154
   * @returns {Promise<number>} The number of rows affected
155
   * @throws {Error} If the primary key is not included in the update fields
156
   * @throws {Error} If optimistic locking check fails
157
   */
158
  updateById<T extends AnyMySqlTable>(
159
    entity: Partial<InferInsertModel<T>>,
160
    schema: T,
161
  ): Promise<number>;
162

163
  /**
164
   * Updates specified fields of records based on provided conditions.
165
   * If the "where" parameter is not provided, the WHERE clause is built from the entity fields
166
   * that are not included in the list of fields to update.
167
   *
168
   * @template T - The type of the table schema
169
   * @param {Partial<InferInsertModel<T>>} updateData - The object containing values to update
170
   * @param {T} schema - The entity schema
171
   * @param {SQL<unknown>} [where] - Optional filtering conditions for the WHERE clause
172
   * @returns {Promise<number>} The number of affected rows
173
   * @throws {Error} If no filtering criteria are provided
174
   * @throws {Error} If the update operation fails
175
   */
176
  updateFields<T extends AnyMySqlTable>(
177
    updateData: Partial<InferInsertModel<T>>,
178
    schema: T,
179
    where?: SQL<unknown>,
180
  ): Promise<number>;
181
}
182

183
/**
184
 * Interface for schema analysis operations.
185
 * Provides methods for analyzing query performance and execution plans.
186
 *
187
 * @interface SchemaAnalyzeForgeSql
188
 */
189
export interface SchemaAnalyzeForgeSql {
190
  /**
191
   * Executes EXPLAIN on a Drizzle query.
192
   * @param {{ toSQL: () => Query }} query - The Drizzle query to analyze
193
   * @returns {Promise<ExplainAnalyzeRow[]>} The execution plan analysis results
194
   */
195
  explain(query: { toSQL: () => Query }): Promise<ExplainAnalyzeRow[]>;
196

197
  /**
198
   * Executes EXPLAIN on a raw SQL query.
199
   * @param {string} query - The SQL query to analyze
200
   * @param {unknown[]} bindParams - The query parameters
201
   * @returns {Promise<ExplainAnalyzeRow[]>} The execution plan analysis results
202
   */
203
  explainRaw(query: string, bindParams: unknown[]): Promise<ExplainAnalyzeRow[]>;
204

205
  /**
206
   * Executes EXPLAIN ANALYZE on a Drizzle query.
207
   * @param {{ toSQL: () => Query }} query - The Drizzle query to analyze
208
   * @returns {Promise<ExplainAnalyzeRow[]>} The execution plan analysis results
209
   */
210
  explainAnalyze(query: { toSQL: () => Query }): Promise<ExplainAnalyzeRow[]>;
211

212
  /**
213
   * Executes EXPLAIN ANALYZE on a raw SQL query.
214
   * @param {string} query - The SQL query to analyze
215
   * @param {unknown[]} bindParams - The query parameters
216
   * @returns {Promise<ExplainAnalyzeRow[]>} The execution plan analysis results
217
   */
218
  explainAnalyzeRaw(query: string, bindParams: unknown[]): Promise<ExplainAnalyzeRow[]>;
219

220
  /**
221
   * Analyzes slow queries from the database.
222
   * @returns {Promise<SlowQueryNormalized[]>} The normalized slow query data
223
   */
224
  analyzeSlowQueries(): Promise<SlowQueryNormalized[]>;
225

226
  /**
227
   * Analyzes query history for specific tables using Drizzle table objects.
228
   * @param {AnyMySqlTable[]} tables - The Drizzle table objects to analyze
229
   * @param {Date} [fromDate] - The start date for the analysis
230
   * @param {Date} [toDate] - The end date for the analysis
231
   * @returns {Promise<ClusterStatementRowCamelCase[]>} The analyzed query history
232
   */
233
  analyzeQueriesHistory(
234
    tables: AnyMySqlTable[],
235
    fromDate?: Date,
236
    toDate?: Date,
237
  ): Promise<ClusterStatementRowCamelCase[]>;
238

239
  /**
240
   * Analyzes query history for specific tables using raw table names.
241
   * @param {string[]} tables - The table names to analyze
242
   * @param {Date} [fromDate] - The start date for the analysis
243
   * @param {Date} [toDate] - The end date for the analysis
244
   * @returns {Promise<ClusterStatementRowCamelCase[]>} The analyzed query history
245
   */
246
  analyzeQueriesHistoryRaw(
247
    tables: string[],
248
    fromDate?: Date,
249
    toDate?: Date,
250
  ): Promise<ClusterStatementRowCamelCase[]>;
251
}
252

253
/**
254
 * Interface for schema-level SQL operations.
255
 * Provides methods for executing SQL queries with schema binding and type safety.
256
 *
257
 * @interface SchemaSqlForgeSql
258
 */
259
export interface SchemaSqlForgeSql {
260
  /**
261
   * Executes a Drizzle query and returns a single result.
262
   * @template T - The type of the query builder
263
   * @param {T} query - The Drizzle query to execute
264
   * @returns {Promise<Awaited<T> extends Array<any> ? Awaited<T>[number] | undefined : Awaited<T> | undefined>} A single result object or undefined
265
   * @throws {Error} If more than one record is returned
266
   * @throws {Error} If the query execution fails
267
   */
268
  executeQueryOnlyOne<T extends MySqlSelectDynamic<AnyMySqlSelectQueryBuilder>>(
269
    query: T,
270
  ): Promise<
271
    Awaited<T> extends Array<any> ? Awaited<T>[number] | undefined : Awaited<T> | undefined
272
  >;
273

274
  /**
275
   * Executes a raw SQL query and returns the results.
276
   * @template T - The type of the result objects
277
   * @param {string} query - The raw SQL query
278
   * @param {SqlParameters[]} [params] - Optional SQL parameters
279
   * @returns {Promise<T[]>} A list of results as objects
280
   * @throws {Error} If the query execution fails
281
   */
282
  executeRawSQL<T extends object | unknown>(query: string, params?: SqlParameters[]): Promise<T[]>;
283

284
  /**
285
   * Executes a raw SQL update query.
286
   * @param {string} query - The raw SQL update query
287
   * @param {SqlParameters[]} [params] - Optional SQL parameters
288
   * @returns {Promise<UpdateQueryResponse>} The update response containing affected rows
289
   * @throws {Error} If the update operation fails
290
   */
291
  executeRawUpdateSQL(query: string, params?: unknown[]): Promise<UpdateQueryResponse>;
292
}
293

294
/**
295
 * Interface for version field metadata.
296
 * Defines the configuration for optimistic locking version fields.
297
 *
298
 * @interface VersionFieldMetadata
299
 */
300
export interface VersionFieldMetadata {
301
  /** Name of the version field */
302
  fieldName: string;
303
}
304

305
/**
306
 * Interface for table metadata.
307
 * Defines the configuration for a specific table.
308
 *
309
 * @interface TableMetadata
310
 */
311
export interface TableMetadata {
312
  /** Name of the table */
313
  tableName: string;
314
  /** Version field configuration for optimistic locking */
315
  versionField: VersionFieldMetadata;
316
}
317

318
/**
319
 * Type for additional metadata configuration.
320
 * Maps table names to their metadata configuration.
321
 *
322
 * @type {AdditionalMetadata}
323
 */
324
export type AdditionalMetadata = Record<string, TableMetadata>;
325

326
/**
327
 * Interface for ForgeSQL ORM options
328
 *
329
 * @interface ForgeSqlOrmOptions
330
 */
331
export interface ForgeSqlOrmOptions {
332
  /** Whether to log raw SQL queries */
333
  logRawSqlQuery?: boolean;
334
  /** Whether to disable optimistic locking */
335
  disableOptimisticLocking?: boolean;
336
  /** SQL hints to be applied to queries */
337
  hints?: SqlHints;
338

339
  /**
340
   * Additional metadata for table configuration.
341
   * Allows specifying table-specific settings and behaviors.
342
   * @example
343
   * ```typescript
344
   * {
345
   *   users: {
346
   *     tableName: "users",
347
   *     versionField: {
348
   *       fieldName: "updatedAt",
349
   *       type: "datetime",
350
   *       nullable: false
351
   *     }
352
   *   }
353
   * }
354
   * ```
355
   */
356
  additionalMetadata?: AdditionalMetadata;
357
}
358

359
/**
360
 * Custom type for MySQL datetime fields.
361
 * Handles conversion between JavaScript Date objects and MySQL datetime strings.
362
 *
363
 * @type {CustomType}
364
 */
365
export const forgeDateTimeString = customType<{
1✔
366
  data: Date;
367
  driver: string;
368
  config: { format?: string };
369
}>({
1✔
370
  dataType() {
1✔
371
    return "datetime";
3✔
372
  },
3✔
373
  toDriver(value: Date) {
1✔
374
    return DateTime.fromJSDate(value).toFormat("yyyy-LL-dd'T'HH:mm:ss.SSS");
4✔
375
  },
4✔
376
  fromDriver(value: unknown) {
1✔
377
    const format = "yyyy-LL-dd'T'HH:mm:ss.SSS";
7✔
378
    return parseDateTime(value as string, format);
7✔
379
  },
7✔
380
});
1✔
381

382
/**
383
 * Custom type for MySQL timestamp fields.
384
 * Handles conversion between JavaScript Date objects and MySQL timestamp strings.
385
 *
386
 * @type {CustomType}
387
 */
388
export const forgeTimestampString = customType<{
1✔
389
  data: Date;
390
  driver: string;
391
  config: { format?: string };
392
}>({
1✔
393
  dataType() {
1✔
394
    return "timestamp";
1✔
395
  },
1✔
396
  toDriver(value: Date) {
1✔
397
    return DateTime.fromJSDate(value).toFormat("yyyy-LL-dd'T'HH:mm:ss.SSS");
2✔
398
  },
2✔
399
  fromDriver(value: unknown) {
1✔
NEW
400
    const format = "yyyy-LL-dd'T'HH:mm:ss.SSS";
×
401
    return parseDateTime(value as string, format);
×
402
  },
×
403
});
1✔
404

405
/**
406
 * Custom type for MySQL date fields.
407
 * Handles conversion between JavaScript Date objects and MySQL date strings.
408
 *
409
 * @type {CustomType}
410
 */
411
export const forgeDateString = customType<{
1✔
412
  data: Date;
413
  driver: string;
414
  config: { format?: string };
415
}>({
1✔
416
  dataType() {
1✔
417
    return "date";
×
418
  },
×
419
  toDriver(value: Date) {
1✔
NEW
420
    return DateTime.fromJSDate(value).toFormat("yyyy-LL-dd");
×
421
  },
×
422
  fromDriver(value: unknown) {
1✔
NEW
423
    const format = "yyyy-LL-dd";
×
424
    return parseDateTime(value as string, format);
×
425
  },
×
426
});
1✔
427

428
/**
429
 * Custom type for MySQL time fields.
430
 * Handles conversion between JavaScript Date objects and MySQL time strings.
431
 *
432
 * @type {CustomType}
433
 */
434
export const forgeTimeString = customType<{
1✔
435
  data: Date;
436
  driver: string;
437
  config: { format?: string };
438
}>({
1✔
439
  dataType() {
1✔
440
    return "time";
×
441
  },
×
442
  toDriver(value: Date) {
1✔
NEW
443
    return DateTime.fromJSDate(value).toFormat("HH:mm:ss.SSS");
×
444
  },
×
445
  fromDriver(value: unknown) {
1✔
446
    return parseDateTime(value as string, "HH:mm:ss.SSS");
×
447
  },
×
448
});
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc