- 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
308 lines
6.3 KiB
TypeScript
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;
|
|
}
|
|
|