Files
the_order/packages/database/src/query-cache.ts

129 lines
3.7 KiB
TypeScript
Raw Normal View History

/**
* Database query caching with Redis
* Implements query result caching with automatic invalidation
*
* Note: This module uses optional dynamic import for @the-order/cache
* to avoid requiring it as a direct dependency. If cache is not available,
* queries will execute directly without caching.
*/
import { query } from './client';
import type { QueryResult, QueryResultRow } from './client';
export interface CacheOptions {
ttl?: number; // Time to live in seconds
keyPrefix?: string;
enabled?: boolean;
}
// Cache client interface (matches @the-order/cache API)
// This interface allows us to use the cache without a compile-time dependency
interface CacheClient {
get<T>(key: string): Promise<T | null>;
set(key: string, value: unknown, ttl?: number): Promise<void>;
delete(key: string): Promise<void>;
invalidate(pattern: string): Promise<number>;
}
// Cache client instance (lazy-loaded via dynamic import)
let cacheClientPromise: Promise<CacheClient | null> | null = null;
/**
* Get cache client (lazy-loaded via dynamic import)
* Returns null if cache module is not available
*/
async function getCacheClient(): Promise<CacheClient | null> {
if (cacheClientPromise === null) {
cacheClientPromise = (async () => {
try {
// Use dynamic import with a string literal that TypeScript can't resolve at compile time
// This is done by constructing the import path dynamically
const cacheModulePath = '@the-order/cache';
// eslint-disable-next-line @typescript-eslint/no-implied-eval
const importFunc = new Function('specifier', 'return import(specifier)');
const cacheModule = await importFunc(cacheModulePath);
return cacheModule.getCacheClient() as CacheClient;
} catch {
// Cache module not available - caching will be disabled
return null;
}
})();
}
return cacheClientPromise;
}
/**
* Execute a query with caching
*/
export async function cachedQuery<T extends QueryResultRow = QueryResultRow>(
sql: string,
params?: unknown[],
options: CacheOptions = {}
): Promise<QueryResult<T>> {
const { ttl = 3600, keyPrefix = 'db:query:', enabled = true } = options;
if (!enabled) {
return query<T>(sql, params);
}
const cache = await getCacheClient();
if (!cache) {
// Cache not available - execute query directly
return query<T>(sql, params);
}
const cacheKey = `${keyPrefix}${sql}:${JSON.stringify(params || [])}`;
// Try to get from cache
const cached = await cache.get<QueryResult<T>>(cacheKey);
if (cached) {
return cached;
}
// Execute query
const result = await query<T>(sql, params);
// Cache result
await cache.set(cacheKey, result, ttl);
return result;
}
/**
* Invalidate cache for a pattern
*/
export async function invalidateCache(pattern: string): Promise<number> {
const cache = await getCacheClient();
if (!cache) {
return 0;
}
return cache.invalidate(`db:query:${pattern}*`);
}
/**
* Invalidate cache for a specific query
*/
export async function invalidateQueryCache(sql: string, params?: unknown[]): Promise<void> {
const cache = await getCacheClient();
if (!cache) {
return;
}
const cacheKey = `db:query:${sql}:${JSON.stringify(params || [])}`;
await cache.delete(cacheKey);
}
/**
* Cache decorator for database functions
* Note: This is a simplified implementation. In production, you'd need to
* extract SQL and params from the function or pass them as metadata.
*/
export function cached<T extends (...args: unknown[]) => Promise<QueryResult<QueryResultRow>>>(
fn: T
): T {
return (async (...args: Parameters<T>) => {
const result = await fn(...args);
return result;
}) as T;
}