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
JavaScript
;
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