Files
the-order/packages/cache/src/redis.ts
T
defiQUG 2633de4d33 feat(eresidency): Complete eResidency service implementation
- Implement credential revocation endpoint with proper database integration
- Fix database row mapping (snake_case to camelCase) for eResidency applications
- Add missing imports (getRiskAssessmentEngine, VeriffKYCProvider, ComplyAdvantageSanctionsProvider)
- Fix environment variable type checking for Veriff and ComplyAdvantage providers
- Add required 'message' field to notification service calls
- Fix risk assessment type mismatches
- Update audit logging to use 'verified' action type (supported by schema)
- Resolve all TypeScript errors and unused variable warnings
- Add TypeScript ignore comments for placeholder implementations
- Temporarily disable security/detect-non-literal-regexp rule due to ESLint 9 compatibility
- Service now builds successfully with no linter errors

All core functionality implemented:
- Application submission and management
- KYC integration (Veriff placeholder)
- Sanctions screening (ComplyAdvantage placeholder)
- Risk assessment engine
- Credential issuance and revocation
- Reviewer console
- Status endpoints
- Auto-issuance service
2025-11-10 19:43:02 -08:00

308 lines
6.3 KiB
TypeScript

/**
* Redis caching layer for The Order
* Implements caching for database queries, cache invalidation, and cache monitoring
*/
import { createClient } from 'redis';
import type { RedisClientType } from 'redis';
import { getEnv, createLogger } from '@the-order/shared';
const logger = createLogger('cache');
export interface CacheConfig {
url?: string;
ttl?: number; // Default TTL in seconds
keyPrefix?: string;
enableCompression?: boolean;
}
export interface CacheStats {
hits: number;
misses: number;
sets: number;
deletes: number;
errors: number;
}
/**
* Redis Cache Client
*/
export class CacheClient {
private client: RedisClientType | null = null;
private config: Required<CacheConfig>;
private stats: CacheStats = {
hits: 0,
misses: 0,
sets: 0,
deletes: 0,
errors: 0,
};
constructor(config: CacheConfig = {}) {
const env = getEnv();
this.config = {
url: config.url || env.REDIS_URL || 'redis://localhost:6379',
ttl: config.ttl || 3600, // 1 hour default
keyPrefix: config.keyPrefix || 'the-order:',
enableCompression: config.enableCompression || false,
};
}
/**
* Initialize Redis client
*/
async connect(): Promise<void> {
if (this.client) {
return;
}
try {
this.client = createClient({
url: this.config.url,
}) as RedisClientType;
this.client.on('error', (err) => {
logger.error('Redis client error:', err);
this.stats.errors++;
});
this.client.on('connect', () => {
logger.info('Redis client connected');
});
this.client.on('disconnect', () => {
logger.warn('Redis client disconnected');
});
await this.client.connect();
} catch (error) {
logger.error('Failed to connect to Redis:', error);
throw error;
}
}
/**
* Disconnect Redis client
*/
async disconnect(): Promise<void> {
if (this.client) {
await this.client.quit();
this.client = null;
}
}
/**
* Get value from cache
*/
async get<T>(key: string): Promise<T | null> {
if (!this.client) {
await this.connect();
}
if (!this.client) {
this.stats.errors++;
return null;
}
try {
const fullKey = this.getFullKey(key);
const value = await this.client.get(fullKey);
if (value === null) {
this.stats.misses++;
return null;
}
this.stats.hits++;
return this.deserialize<T>(value);
} catch (error) {
logger.error(`Cache get error for key ${key}:`, error);
this.stats.errors++;
this.stats.misses++;
return null;
}
}
/**
* Set value in cache
*/
async set(key: string, value: unknown, ttl?: number): Promise<void> {
if (!this.client) {
await this.connect();
}
if (!this.client) {
this.stats.errors++;
return;
}
try {
const fullKey = this.getFullKey(key);
const serialized = this.serialize(value);
const expiresIn = ttl || this.config.ttl;
await this.client.setEx(fullKey, expiresIn, serialized);
this.stats.sets++;
} catch (error) {
logger.error(`Cache set error for key ${key}:`, error);
this.stats.errors++;
}
}
/**
* Delete value from cache
*/
async delete(key: string): Promise<void> {
if (!this.client) {
await this.connect();
}
if (!this.client) {
this.stats.errors++;
return;
}
try {
const fullKey = this.getFullKey(key);
await this.client.del(fullKey);
this.stats.deletes++;
} catch (error) {
logger.error(`Cache delete error for key ${key}:`, error);
this.stats.errors++;
}
}
/**
* Delete multiple keys by pattern
*/
async invalidate(pattern: string): Promise<number> {
if (!this.client) {
await this.connect();
}
if (!this.client) {
this.stats.errors++;
return 0;
}
try {
const fullPattern = this.getFullKey(pattern);
const keys = await this.client.keys(fullPattern);
if (keys.length === 0) {
return 0;
}
const deleted = await this.client.del(keys);
this.stats.deletes += deleted;
return deleted;
} catch (error) {
logger.error(`Cache invalidate error for pattern ${pattern}:`, error);
this.stats.errors++;
return 0;
}
}
/**
* Check if key exists
*/
async exists(key: string): Promise<boolean> {
if (!this.client) {
await this.connect();
}
if (!this.client) {
this.stats.errors++;
return false;
}
try {
const fullKey = this.getFullKey(key);
const result = await this.client.exists(fullKey);
return result === 1;
} catch (error) {
logger.error(`Cache exists error for key ${key}:`, error);
this.stats.errors++;
return false;
}
}
/**
* Get cache statistics
*/
getStats(): CacheStats {
return { ...this.stats };
}
/**
* Reset cache statistics
*/
resetStats(): void {
this.stats = {
hits: 0,
misses: 0,
sets: 0,
deletes: 0,
errors: 0,
};
}
/**
* Get full key with prefix
*/
private getFullKey(key: string): string {
return `${this.config.keyPrefix}${key}`;
}
/**
* Serialize value
*/
private serialize(value: unknown): string {
return JSON.stringify(value);
}
/**
* Deserialize value
*/
private deserialize<T>(value: string): T {
return JSON.parse(value) as T;
}
}
/**
* Get default cache client
*/
let defaultCacheClient: CacheClient | null = null;
export function getCacheClient(config?: CacheConfig): CacheClient {
if (!defaultCacheClient) {
defaultCacheClient = new CacheClient(config);
}
return defaultCacheClient;
}
/**
* Cache decorator for functions
*/
export function cached<T extends (...args: unknown[]) => Promise<unknown>>(
fn: T,
keyGenerator?: (...args: Parameters<T>) => string,
ttl?: number
): T {
const cache = getCacheClient();
return (async (...args: Parameters<T>) => {
const key = keyGenerator ? keyGenerator(...args) : `fn:${fn.name}:${JSON.stringify(args)}`;
const cachedValue = await cache.get(key);
if (cachedValue !== null) {
return cachedValue as ReturnType<T>;
}
const result = await fn(...args);
await cache.set(key, result, ttl);
return result;
}) as T;
}