UNPKG

forge-sql-orm

Version:

Drizzle ORM integration for Atlassian @forge/sql. Provides a custom driver, schema migration, two levels of caching (local and global via @forge/kvs), optimistic locking, and query analysis.

577 lines 24.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.patchDbWithSelectAliased = patchDbWithSelectAliased; const __1 = require("../../.."); const cacheUtils_1 = require("../../../utils/cacheUtils"); const cacheContextUtils_1 = require("../../../utils/cacheContextUtils"); const sql_1 = require("drizzle-orm/sql/sql"); const drizzle_orm_1 = require("drizzle-orm"); /** * Error codes that should not trigger cache clearing */ const NON_CACHE_CLEARING_ERROR_CODES = ["VALIDATION_ERROR", "CONSTRAINT_ERROR"]; /** * Error codes that should trigger cache clearing */ const CACHE_CLEARING_ERROR_CODES = ["DEADLOCK", "LOCK_WAIT_TIMEOUT", "CONNECTION_ERROR"]; /** * Error message patterns that should not trigger cache clearing */ const NON_CACHE_CLEARING_PATTERNS = [/validation/i, /constraint/i]; /** * Error message patterns that should trigger cache clearing */ const CACHE_CLEARING_PATTERNS = [/timeout/i, /connection/i]; // ============================================================================ // CACHE MANAGEMENT UTILITIES // ============================================================================ /** * Determines whether cache should be cleared based on the error type. * Only clears cache for errors that might indicate data consistency issues. * * @param error - The error that occurred during query execution * @returns true if cache should be cleared, false otherwise */ function shouldClearCacheOnError(error) { // Don't clear cache for client-side errors (validation, etc.) if (error?.code && NON_CACHE_CLEARING_ERROR_CODES.includes(error.code)) { return false; } if (error?.message && NON_CACHE_CLEARING_PATTERNS.some((pattern) => pattern.test(error.message))) { return false; } // Clear cache for database-level errors that might affect data consistency if (error?.code && CACHE_CLEARING_ERROR_CODES.includes(error.code)) { return true; } if (error?.message && CACHE_CLEARING_PATTERNS.some((pattern) => pattern.test(error.message))) { return true; } // For unknown errors, be conservative and clear cache return true; } /** * Handles successful query execution with cache management. * * @param rows - Query result rows * @param onfulfilled - Success callback * @param table - The table being modified * @param options - ForgeSQL ORM options * @param isCached - Whether to clear cache immediately * @returns Promise with result */ async function handleSuccessfulExecution(rows, onfulfilled, table, options, isCached) { try { await (0, cacheContextUtils_1.evictLocalCacheQuery)(table, options); await (0, cacheContextUtils_1.saveTableIfInsideCacheContext)(table); if (isCached && !cacheContextUtils_1.cacheApplicationContext.getStore()) { await (0, cacheUtils_1.clearCache)(table, options); } const result = onfulfilled ? onfulfilled(rows) : rows; return result; } catch (error) { // Only clear cache for certain types of errors if (shouldClearCacheOnError(error)) { await (0, cacheContextUtils_1.evictLocalCacheQuery)(table, options); if (isCached) { await (0, cacheUtils_1.clearCache)(table, options).catch((e) => { // eslint-disable-next-line no-console console.warn("Ignore cache clear errors", e); }); } else { await (0, cacheContextUtils_1.saveTableIfInsideCacheContext)(table); } } throw error; } } /** * Handles function calls on the wrapped builder. * * @param value - The function to call * @param target - The target object * @param args - Function arguments * @param table - The table being modified * @param options - ForgeSQL ORM options * @param isCached - Whether to clear cache immediately * @returns Function result or wrapped builder */ function handleFunctionCall(value, target, args, table, options, isCached) { const result = value.apply(target, args); if (typeof result === "object" && result !== null && "execute" in result) { return wrapCacheEvictBuilder(result, table, options, isCached); } return result; } /** * Wraps a query builder with cache eviction functionality. * Automatically clears cache after successful execution of insert/update/delete operations. * * @param rawBuilder - The original query builder to wrap * @param table - The table being modified (used for cache eviction) * @param options - ForgeSQL ORM options including cache configuration * @param isCached - Whether to clear cache immediately * @returns Wrapped query builder with cache eviction */ const wrapCacheEvictBuilder = (rawBuilder, table, options, isCached) => { return new Proxy(rawBuilder, { get(target, prop, receiver) { if (prop === "then") { return (onfulfilled, onrejected) => target .execute() .then((rows) => handleSuccessfulExecution(rows, onfulfilled, table, options, isCached), onrejected); } const value = Reflect.get(target, prop, receiver); if (typeof value === "function") { return (...args) => handleFunctionCall(value, target, args, table, options, isCached); } return value; }, }); }; /** * Creates an insert query builder that automatically evicts cache after execution. * * @param db - The database instance * @param table - The table to insert into * @param options - ForgeSQL ORM options * @returns Insert query builder with cache eviction */ function insertAndEvictCacheBuilder(db, table, options, isCached) { const builder = db.insert(table); return wrapCacheEvictBuilder(builder, table, options, isCached); } /** * Creates an update query builder that automatically evicts cache after execution. * * @param db - The database instance * @param table - The table to update * @param options - ForgeSQL ORM options * @returns Update query builder with cache eviction */ function updateAndEvictCacheBuilder(db, table, options, isCached) { const builder = db.update(table); return wrapCacheEvictBuilder(builder, table, options, isCached); } /** * Creates a delete query builder that automatically evicts cache after execution. * * @param db - The database instance * @param table - The table to delete from * @param options - ForgeSQL ORM options * @returns Delete query builder with cache eviction */ function deleteAndEvictCacheBuilder(db, table, options, isCached) { const builder = db.delete(table); return wrapCacheEvictBuilder(builder, table, options, isCached); } /** * Handles cached query execution with proper error handling. * * @param target - The query target * @param options - ForgeSQL ORM options * @param cacheTtl - Cache TTL * @param selections - Field selections * @param aliasMap - Field alias mapping * @param onfulfilled - Success callback * @param onrejected - Error callback * @returns Promise with cached result */ async function handleCachedQuery(target, options, cacheTtl, selections, aliasMap, onfulfilled, onrejected) { try { const localCached = await (0, cacheContextUtils_1.getQueryLocalCacheQuery)(target, options); if (localCached) { return onfulfilled ? onfulfilled(localCached) : localCached; } const cacheResult = await (0, cacheUtils_1.getFromCache)(target, options); if (cacheResult) { return onfulfilled ? onfulfilled(cacheResult) : cacheResult; } const rows = await target.execute(); const transformed = (0, __1.applyFromDriverTransform)(rows, selections, aliasMap); await (0, cacheContextUtils_1.saveQueryLocalCacheQuery)(target, transformed, options); await (0, cacheUtils_1.setCacheResult)(target, options, transformed, cacheTtl).catch((cacheError) => { // Log cache error but don't fail the query // eslint-disable-next-line no-console console.warn("Cache set error:", cacheError); }); return onfulfilled ? onfulfilled(transformed) : transformed; } catch (error) { if (onrejected) { return onrejected(error); } throw error; } } /** * Handles non-cached query execution. * * @param target - The query target * @param options - ForgeSQL ORM options * @param selections - Field selections * @param aliasMap - Field alias mapping * @param onfulfilled - Success callback * @param onrejected - Error callback * @returns Promise with transformed result */ async function handleNonCachedQuery(target, options, selections, aliasMap, onfulfilled, onrejected) { try { const localCached = await (0, cacheContextUtils_1.getQueryLocalCacheQuery)(target, options); if (localCached) { return onfulfilled ? onfulfilled(localCached) : localCached; } const rows = await target.execute(); const transformed = (0, __1.applyFromDriverTransform)(rows, selections, aliasMap); await (0, cacheContextUtils_1.saveQueryLocalCacheQuery)(target, transformed, options); return onfulfilled ? onfulfilled(transformed) : transformed; } catch (error) { if (onrejected) { return onrejected(error); } throw error; } } /** * Creates a select query builder with field aliasing and optional caching support. * * @param db - The database instance * @param fields - The fields to select with aliases * @param selectFn - Function to create the base select query * @param useCache - Whether to enable caching for this query * @param options - ForgeSQL ORM options * @param cacheTtl - Optional cache TTL override * @returns Select query builder with aliasing and optional caching */ /** * Creates a catch handler for Promise-like objects */ function createCatchHandler(receiver) { return (onrejected) => receiver.then(undefined, onrejected); } /** * Creates a finally handler for Promise-like objects */ function createFinallyHandler(receiver) { return (onfinally) => { const handleFinally = (value) => Promise.resolve(value).finally(onfinally); const handleReject = (reason) => Promise.reject(reason).finally(onfinally); return receiver.then(handleFinally, handleReject); }; } /** * Creates a then handler for cached queries */ function createCachedThenHandler(target, options, cacheTtl, selections, aliasMap) { return (onfulfilled, onrejected) => { const ttl = cacheTtl ?? options.cacheTTL ?? 120; return handleCachedQuery(target, options, ttl, selections, aliasMap, onfulfilled, onrejected); }; } /** * Creates a then handler for non-cached queries */ function createNonCachedThenHandler(target, options, selections, aliasMap) { return (onfulfilled, onrejected) => { return handleNonCachedQuery(target, options, selections, aliasMap, onfulfilled, onrejected); }; } /** * Creates an execute handler that transforms results */ function createExecuteHandler(target, selections, aliasMap) { return async (...args) => { const rows = await target.execute(...args); return (0, __1.applyFromDriverTransform)(rows, selections, aliasMap); }; } /** * Creates a function call handler that wraps results */ function createFunctionCallHandler(value, target, wrapBuilder) { return (...args) => { const result = value.apply(target, args); if (typeof result === "object" && result !== null && "execute" in result) { return wrapBuilder(result); } return result; }; } /** * Creates a select query builder with field aliasing and optional caching support. * * @param db - The database instance * @param fields - The fields to select with aliases * @param selectFn - Function to create the base select query * @param useCache - Whether to enable caching for this query * @param options - ForgeSQL ORM options * @param cacheTtl - Optional cache TTL override * @returns Select query builder with aliasing and optional caching */ function createAliasedSelectBuilder(db, fields, selectFn, useCache, options, cacheTtl) { const { selections, aliasMap } = (0, __1.mapSelectFieldsWithAlias)(fields); const builder = selectFn(selections); const wrapBuilder = (rawBuilder) => { return new Proxy(rawBuilder, { get(target, prop, receiver) { if (prop === "execute") { return createExecuteHandler(target, selections, aliasMap); } if (prop === "then") { return useCache ? createCachedThenHandler(target, options, cacheTtl, selections, aliasMap) : createNonCachedThenHandler(target, options, selections, aliasMap); } if (prop === "catch") { return createCatchHandler(receiver); } if (prop === "finally") { return createFinallyHandler(receiver); } const value = Reflect.get(target, prop, receiver); if (typeof value === "function") { return createFunctionCallHandler(value, target, wrapBuilder); } return value; }, }); }; return wrapBuilder(builder); } // ============================================================================ // CONFIGURATION AND CONSTANTS // ============================================================================ /** * Default options for ForgeSQL ORM */ const DEFAULT_OPTIONS = { logRawSqlQuery: false, disableOptimisticLocking: false, cacheTTL: 120, cacheWrapTable: true, cacheEntityQueryName: "sql", cacheEntityExpirationName: "expiration", cacheEntityDataName: "data", }; // ============================================================================ // QUERY BUILDER FACTORIES // ============================================================================ /** * Creates a raw SQL query executor with caching support */ function createRawQueryExecutor(db, options, useGlobalCache = false) { return async function (query, cacheTtl) { let sql; if ((0, sql_1.isSQLWrapper)(query)) { const dialect = db.dialect; sql = dialect.sqlToQuery(query); } else { sql = { sql: query, params: [], }; } // Check local cache first const localCacheResult = await (0, cacheContextUtils_1.getQueryLocalCacheQuery)(sql, options); if (localCacheResult) { return localCacheResult; } // Check global cache if enabled if (useGlobalCache) { const cacheResult = await (0, cacheUtils_1.getFromCache)({ toSQL: () => sql }, options); if (cacheResult) { return cacheResult; } } // Execute query const results = await db.execute(query); // Save to local cache await (0, cacheContextUtils_1.saveQueryLocalCacheQuery)(sql, results, options); // Save to global cache if enabled if (useGlobalCache) { await (0, cacheUtils_1.setCacheResult)({ toSQL: () => sql }, options, results, cacheTtl ?? options.cacheTTL ?? 120); } return results; }; } // ============================================================================ // MAIN PATCH FUNCTION // ============================================================================ /** * Patches a Drizzle database instance with additional methods for aliased selects and cache management. * * This function extends the database instance with: * - selectAliased: Select with field aliasing support * - selectAliasedDistinct: Select distinct with field aliasing support * - selectAliasedCacheable: Select with field aliasing and caching * - selectAliasedDistinctCacheable: Select distinct with field aliasing and caching * - insertAndEvictCache: Insert operations that automatically evict cache * - updateAndEvictCache: Update operations that automatically evict cache * - deleteAndEvictCache: Delete operations that automatically evict cache * * @param db - The Drizzle database instance to patch * @param options - Optional ForgeSQL ORM configuration * @returns The patched database instance with additional methods */ function patchDbWithSelectAliased(db, options) { const newOptions = { ...DEFAULT_OPTIONS, ...options }; // ============================================================================ // SELECT METHODS WITH FIELD ALIASING // ============================================================================ // Select aliased without cache db.selectAliased = function (fields) { return createAliasedSelectBuilder(db, fields, (selections) => db.select(selections), false, newOptions); }; // Select aliased with cache db.selectAliasedCacheable = function (fields, cacheTtl) { return createAliasedSelectBuilder(db, fields, (selections) => db.select(selections), true, newOptions, cacheTtl); }; // Select aliased distinct without cache db.selectAliasedDistinct = function (fields) { return createAliasedSelectBuilder(db, fields, (selections) => db.selectDistinct(selections), false, newOptions); }; // Select aliased distinct with cache db.selectAliasedDistinctCacheable = function (fields, cacheTtl) { return createAliasedSelectBuilder(db, fields, (selections) => db.selectDistinct(selections), true, newOptions, cacheTtl); }; // ============================================================================ // TABLE-BASED SELECT METHODS // ============================================================================ /** * Creates a select query builder for all columns from a table with field aliasing support. * This is a convenience method that automatically selects all columns from the specified table. * * @param table - The table to select from * @returns Select query builder with all table columns and field aliasing support * @example * ```typescript * const users = await db.selectFrom(userTable).where(eq(userTable.id, 1)); * ``` */ db.selectFrom = function (table) { return db.selectAliased((0, drizzle_orm_1.getTableColumns)(table)).from(table); }; /** * Creates a select query builder for all columns from a table with field aliasing and caching support. * This is a convenience method that automatically selects all columns from the specified table with caching enabled. * * @param table - The table to select from * @param cacheTtl - Optional cache TTL override (defaults to global cache TTL) * @returns Select query builder with all table columns, field aliasing, and caching support * @example * ```typescript * const users = await db.selectFromCacheable(userTable, 300).where(eq(userTable.id, 1)); * ``` */ db.selectFromCacheable = function (table, cacheTtl) { return db .selectAliasedCacheable((0, drizzle_orm_1.getTableColumns)(table), cacheTtl) .from(table); }; /** * Creates a select distinct query builder for all columns from a table with field aliasing support. * This is a convenience method that automatically selects all distinct columns from the specified table. * * @param table - The table to select from * @returns Select distinct query builder with all table columns and field aliasing support * @example * ```typescript * const uniqueUsers = await db.selectDistinctFrom(userTable).where(eq(userTable.status, 'active')); * ``` */ db.selectDistinctFrom = function (table) { return db .selectAliasedDistinct((0, drizzle_orm_1.getTableColumns)(table)) .from(table); }; /** * Creates a select distinct query builder for all columns from a table with field aliasing and caching support. * This is a convenience method that automatically selects all distinct columns from the specified table with caching enabled. * * @param table - The table to select from * @param cacheTtl - Optional cache TTL override (defaults to global cache TTL) * @returns Select distinct query builder with all table columns, field aliasing, and caching support * @example * ```typescript * const uniqueUsers = await db.selectDistinctFromCacheable(userTable, 300).where(eq(userTable.status, 'active')); * ``` */ db.selectDistinctFromCacheable = function (table, cacheTtl) { return db .selectAliasedDistinctCacheable((0, drizzle_orm_1.getTableColumns)(table), cacheTtl) .from(table); }; // ============================================================================ // CACHE-AWARE MODIFY OPERATIONS // ============================================================================ // Insert with cache context support (participates in cache clearing when used within cache context) db.insertWithCacheContext = function (table) { return insertAndEvictCacheBuilder(db, table, newOptions, false); }; // Insert with cache eviction db.insertAndEvictCache = function (table) { return insertAndEvictCacheBuilder(db, table, newOptions, true); }; // Update with cache context support (participates in cache clearing when used within cache context) db.updateWithCacheContext = function (table) { return updateAndEvictCacheBuilder(db, table, newOptions, false); }; // Update with cache eviction db.updateAndEvictCache = function (table) { return updateAndEvictCacheBuilder(db, table, newOptions, true); }; // Delete with cache context support (participates in cache clearing when used within cache context) db.deleteWithCacheContext = function (table) { return deleteAndEvictCacheBuilder(db, table, newOptions, false); }; // Delete with cache eviction db.deleteAndEvictCache = function (table) { return deleteAndEvictCacheBuilder(db, table, newOptions, true); }; // ============================================================================ // RAW SQL QUERY EXECUTORS // ============================================================================ /** * Executes a raw SQL query with local cache support. * This method provides local caching for raw SQL queries within the current invocation context. * Results are cached locally and will be returned from cache on subsequent identical queries. * * @param query - The SQL query to execute (SQLWrapper or string) * @returns Promise with query results * @example * ```typescript * // Using SQLWrapper * const result = await db.executeQuery(sql`SELECT * FROM users WHERE id = ${userId}`); * * // Using string * const result = await db.executeQuery("SELECT * FROM users WHERE status = 'active'"); * ``` */ db.executeQuery = createRawQueryExecutor(db, newOptions, false); /** * Executes a raw SQL query with both local and global cache support. * This method provides comprehensive caching for raw SQL queries: * - Local cache: Within the current invocation context * - Global cache: Cross-invocation caching using @forge/kvs * * @param query - The SQL query to execute (SQLWrapper or string) * @param cacheTtl - Optional cache TTL override (defaults to global cache TTL) * @returns Promise with query results * @example * ```typescript * // Using SQLWrapper with custom TTL * const result = await db.executeQueryCacheable(sql`SELECT * FROM users WHERE id = ${userId}`, 300); * * // Using string with default TTL * const result = await db.executeQueryCacheable("SELECT * FROM users WHERE status = 'active'"); * ``` */ db.executeQueryCacheable = createRawQueryExecutor(db, newOptions, true); return db; } //# sourceMappingURL=additionalActions.js.map