UNPKG

forge-sql-orm

Version:

Drizzle ORM integration for Forge-SQL in Atlassian Forge applications.

266 lines (246 loc) 9.88 kB
import { ForgeSQLCrudOperations } from "./ForgeSQLCrudOperations"; import { CRUDForgeSQL, ForgeSqlOperation, ForgeSqlOrmOptions, SchemaAnalyzeForgeSql, SchemaSqlForgeSql, } from "./ForgeSQLQueryBuilder"; import { ForgeSQLSelectOperations } from "./ForgeSQLSelectOperations"; import { drizzle, MySqlRemoteDatabase, MySqlRemotePreparedQueryHKT } from "drizzle-orm/mysql-proxy"; import { createForgeDriverProxy } from "../utils/forgeDriverProxy"; import type { SelectedFields } from "drizzle-orm/mysql-core/query-builders/select.types"; import { MySqlSelectBuilder } from "drizzle-orm/mysql-core"; import { patchDbWithSelectAliased } from "../lib/drizzle/extensions/selectAliased"; import { ForgeSQLAnalyseOperation } from "./ForgeSQLAnalyseOperations"; /** * Implementation of ForgeSQLORM that uses Drizzle ORM for query building. * This class provides a bridge between Forge SQL and Drizzle ORM, allowing * to use Drizzle's query builder while executing queries through Forge SQL. */ class ForgeSQLORMImpl implements ForgeSqlOperation { private static instance: ForgeSQLORMImpl | null = null; private readonly drizzle: MySqlRemoteDatabase<any> & { selectAliased: <TSelection extends SelectedFields>( fields: TSelection, ) => MySqlSelectBuilder<TSelection, MySqlRemotePreparedQueryHKT>; selectAliasedDistinct: <TSelection extends SelectedFields>( fields: TSelection, ) => MySqlSelectBuilder<TSelection, MySqlRemotePreparedQueryHKT>; }; private readonly crudOperations: CRUDForgeSQL; private readonly fetchOperations: SchemaSqlForgeSql; private readonly analyzeOperations: SchemaAnalyzeForgeSql; /** * Private constructor to enforce singleton behavior. * @param options - Options for configuring ForgeSQL ORM behavior. */ private constructor(options?: ForgeSqlOrmOptions) { try { const newOptions: ForgeSqlOrmOptions = options ?? { logRawSqlQuery: false, disableOptimisticLocking: false, }; if (newOptions.logRawSqlQuery) { console.debug("Initializing ForgeSQLORM..."); } // Initialize Drizzle instance with our custom driver const proxiedDriver = createForgeDriverProxy(newOptions.hints, newOptions.logRawSqlQuery); this.drizzle = patchDbWithSelectAliased( drizzle(proxiedDriver, { logger: newOptions.logRawSqlQuery }), ); this.crudOperations = new ForgeSQLCrudOperations(this, newOptions); this.fetchOperations = new ForgeSQLSelectOperations(newOptions); this.analyzeOperations = new ForgeSQLAnalyseOperation(this); } catch (error) { console.error("ForgeSQLORM initialization failed:", error); throw error; } } /** * Create the modify operations instance. * @returns modify operations. */ modify(): CRUDForgeSQL { return this.crudOperations; } /** * Returns the singleton instance of ForgeSQLORMImpl. * @param options - Options for configuring ForgeSQL ORM behavior. * @returns The singleton instance of ForgeSQLORMImpl. */ static getInstance(options?: ForgeSqlOrmOptions): ForgeSqlOperation { ForgeSQLORMImpl.instance ??= new ForgeSQLORMImpl(options); return ForgeSQLORMImpl.instance; } /** * Retrieves the CRUD operations instance. * @returns CRUD operations. */ crud(): CRUDForgeSQL { return this.modify(); } /** * Retrieves the fetch operations instance. * @returns Fetch operations. */ fetch(): SchemaSqlForgeSql { return this.fetchOperations; } analyze(): SchemaAnalyzeForgeSql { return this.analyzeOperations; } /** * Returns a Drizzle query builder instance. * * ⚠️ IMPORTANT: This method should be used ONLY for query building purposes. * The returned instance should NOT be used for direct database connections or query execution. * All database operations should be performed through Forge SQL's executeRawSQL or executeRawUpdateSQL methods. * * @returns A Drizzle query builder instance for query construction only. */ getDrizzleQueryBuilder(): MySqlRemoteDatabase<Record<string, unknown>> { return this.drizzle; } /** * Creates a select query with unique field aliases to prevent field name collisions in joins. * This is particularly useful when working with Atlassian Forge SQL, which collapses fields with the same name in joined tables. * * @template TSelection - The type of the selected fields * @param {TSelection} fields - Object containing the fields to select, with table schemas as values * @returns {MySqlSelectBuilder<TSelection, MySql2PreparedQueryHKT>} A select query builder with unique field aliases * @throws {Error} If fields parameter is empty * @example * ```typescript * await forgeSQL * .select({user: users, order: orders}) * .from(orders) * .innerJoin(users, eq(orders.userId, users.id)); * ``` */ select<TSelection extends SelectedFields>( fields: TSelection, ): MySqlSelectBuilder<TSelection, MySqlRemotePreparedQueryHKT> { if (!fields) { throw new Error("fields is empty"); } return this.drizzle.selectAliased(fields); } /** * Creates a distinct select query with unique field aliases to prevent field name collisions in joins. * This is particularly useful when working with Atlassian Forge SQL, which collapses fields with the same name in joined tables. * * @template TSelection - The type of the selected fields * @param {TSelection} fields - Object containing the fields to select, with table schemas as values * @returns {MySqlSelectBuilder<TSelection, MySql2PreparedQueryHKT>} A distinct select query builder with unique field aliases * @throws {Error} If fields parameter is empty * @example * ```typescript * await forgeSQL * .selectDistinct({user: users, order: orders}) * .from(orders) * .innerJoin(users, eq(orders.userId, users.id)); * ``` */ selectDistinct<TSelection extends SelectedFields>( fields: TSelection, ): MySqlSelectBuilder<TSelection, MySqlRemotePreparedQueryHKT> { if (!fields) { throw new Error("fields is empty"); } return this.drizzle.selectAliasedDistinct(fields); } } /** * Public class that acts as a wrapper around the private ForgeSQLORMImpl. * Provides a clean interface for working with Forge SQL and Drizzle ORM. */ class ForgeSQLORM implements ForgeSqlOperation { private readonly ormInstance: ForgeSqlOperation; constructor(options?: ForgeSqlOrmOptions) { this.ormInstance = ForgeSQLORMImpl.getInstance(options); } /** * Creates a select query with unique field aliases to prevent field name collisions in joins. * This is particularly useful when working with Atlassian Forge SQL, which collapses fields with the same name in joined tables. * * @template TSelection - The type of the selected fields * @param {TSelection} fields - Object containing the fields to select, with table schemas as values * @returns {MySqlSelectBuilder<TSelection, MySql2PreparedQueryHKT>} A select query builder with unique field aliases * @throws {Error} If fields parameter is empty * @example * ```typescript * await forgeSQL * .select({user: users, order: orders}) * .from(orders) * .innerJoin(users, eq(orders.userId, users.id)); * ``` */ select<TSelection extends SelectedFields>( fields: TSelection, ): MySqlSelectBuilder<TSelection, MySqlRemotePreparedQueryHKT> { return this.ormInstance.select(fields); } /** * Creates a distinct select query with unique field aliases to prevent field name collisions in joins. * This is particularly useful when working with Atlassian Forge SQL, which collapses fields with the same name in joined tables. * * @template TSelection - The type of the selected fields * @param {TSelection} fields - Object containing the fields to select, with table schemas as values * @returns {MySqlSelectBuilder<TSelection, MySqlRemotePreparedQueryHKT>} A distinct select query builder with unique field aliases * @throws {Error} If fields parameter is empty * @example * ```typescript * await forgeSQL * .selectDistinct({user: users, order: orders}) * .from(orders) * .innerJoin(users, eq(orders.userId, users.id)); * ``` */ selectDistinct<TSelection extends SelectedFields>( fields: TSelection, ): MySqlSelectBuilder<TSelection, MySqlRemotePreparedQueryHKT> { return this.ormInstance.selectDistinct(fields); } /** * Proxies the `crud` method from `ForgeSQLORMImpl`. * @returns CRUD operations. */ crud(): CRUDForgeSQL { return this.ormInstance.modify(); } /** * Proxies the `modify` method from `ForgeSQLORMImpl`. * @returns Modify operations. */ modify(): CRUDForgeSQL { return this.ormInstance.modify(); } /** * Proxies the `fetch` method from `ForgeSQLORMImpl`. * @returns Fetch operations. */ fetch(): SchemaSqlForgeSql { return this.ormInstance.fetch(); } /** * Provides query analysis capabilities including EXPLAIN ANALYZE and slow query analysis. * @returns {SchemaAnalyzeForgeSql} Interface for analyzing query performance */ analyze(): SchemaAnalyzeForgeSql { return this.ormInstance.analyze(); } /** * Returns a Drizzle query builder instance. * * ⚠️ IMPORTANT: This method should be used ONLY for query building purposes. * The returned instance should NOT be used for direct database connections or query execution. * All database operations should be performed through Forge SQL's executeRawSQL or executeRawUpdateSQL methods. * * @returns A Drizzle query builder instance for query construction only. */ getDrizzleQueryBuilder() { return this.ormInstance.getDrizzleQueryBuilder(); } } export default ForgeSQLORM;