129 lines
3.7 KiB
TypeScript
129 lines
3.7 KiB
TypeScript
|
|
/**
|
||
|
|
* 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;
|
||
|
|
}
|