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
This commit is contained in:
5
packages/cache/src/index.d.ts
vendored
Normal file
5
packages/cache/src/index.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* Cache package for The Order
|
||||
*/
|
||||
export * from './redis';
|
||||
//# sourceMappingURL=index.d.ts.map
|
||||
1
packages/cache/src/index.d.ts.map
vendored
Normal file
1
packages/cache/src/index.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,cAAc,SAAS,CAAC"}
|
||||
5
packages/cache/src/index.js
vendored
Normal file
5
packages/cache/src/index.js
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* Cache package for The Order
|
||||
*/
|
||||
export * from './redis';
|
||||
//# sourceMappingURL=index.js.map
|
||||
1
packages/cache/src/index.js.map
vendored
Normal file
1
packages/cache/src/index.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.js","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,cAAc,SAAS,CAAC"}
|
||||
6
packages/cache/src/index.ts
vendored
Normal file
6
packages/cache/src/index.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Cache package for The Order
|
||||
*/
|
||||
|
||||
export * from './redis';
|
||||
|
||||
80
packages/cache/src/redis.d.ts
vendored
Normal file
80
packages/cache/src/redis.d.ts
vendored
Normal file
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* Redis caching layer for The Order
|
||||
* Implements caching for database queries, cache invalidation, and cache monitoring
|
||||
*/
|
||||
export interface CacheConfig {
|
||||
url?: string;
|
||||
ttl?: number;
|
||||
keyPrefix?: string;
|
||||
enableCompression?: boolean;
|
||||
}
|
||||
export interface CacheStats {
|
||||
hits: number;
|
||||
misses: number;
|
||||
sets: number;
|
||||
deletes: number;
|
||||
errors: number;
|
||||
}
|
||||
/**
|
||||
* Redis Cache Client
|
||||
*/
|
||||
export declare class CacheClient {
|
||||
private client;
|
||||
private config;
|
||||
private stats;
|
||||
constructor(config?: CacheConfig);
|
||||
/**
|
||||
* Initialize Redis client
|
||||
*/
|
||||
connect(): Promise<void>;
|
||||
/**
|
||||
* Disconnect Redis client
|
||||
*/
|
||||
disconnect(): Promise<void>;
|
||||
/**
|
||||
* Get value from cache
|
||||
*/
|
||||
get<T>(key: string): Promise<T | null>;
|
||||
/**
|
||||
* Set value in cache
|
||||
*/
|
||||
set(key: string, value: unknown, ttl?: number): Promise<void>;
|
||||
/**
|
||||
* Delete value from cache
|
||||
*/
|
||||
delete(key: string): Promise<void>;
|
||||
/**
|
||||
* Delete multiple keys by pattern
|
||||
*/
|
||||
invalidate(pattern: string): Promise<number>;
|
||||
/**
|
||||
* Check if key exists
|
||||
*/
|
||||
exists(key: string): Promise<boolean>;
|
||||
/**
|
||||
* Get cache statistics
|
||||
*/
|
||||
getStats(): CacheStats;
|
||||
/**
|
||||
* Reset cache statistics
|
||||
*/
|
||||
resetStats(): void;
|
||||
/**
|
||||
* Get full key with prefix
|
||||
*/
|
||||
private getFullKey;
|
||||
/**
|
||||
* Serialize value
|
||||
*/
|
||||
private serialize;
|
||||
/**
|
||||
* Deserialize value
|
||||
*/
|
||||
private deserialize;
|
||||
}
|
||||
export declare function getCacheClient(config?: CacheConfig): CacheClient;
|
||||
/**
|
||||
* Cache decorator for functions
|
||||
*/
|
||||
export declare function cached<T extends (...args: unknown[]) => Promise<unknown>>(fn: T, keyGenerator?: (...args: Parameters<T>) => string, ttl?: number): T;
|
||||
//# sourceMappingURL=redis.d.ts.map
|
||||
1
packages/cache/src/redis.d.ts.map
vendored
Normal file
1
packages/cache/src/redis.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"redis.d.ts","sourceRoot":"","sources":["redis.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAQH,MAAM,WAAW,WAAW;IAC1B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;GAEG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,MAAM,CAAgC;IAC9C,OAAO,CAAC,MAAM,CAAwB;IACtC,OAAO,CAAC,KAAK,CAMX;gBAEU,MAAM,GAAE,WAAgB;IAUpC;;OAEG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IA8B9B;;OAEG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAOjC;;OAEG;IACG,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;IAwB5C;;OAEG;IACG,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAuBnE;;OAEG;IACG,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAoBxC;;OAEG;IACG,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IA4BlD;;OAEG;IACG,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAqB3C;;OAEG;IACH,QAAQ,IAAI,UAAU;IAItB;;OAEG;IACH,UAAU,IAAI,IAAI;IAUlB;;OAEG;IACH,OAAO,CAAC,UAAU;IAIlB;;OAEG;IACH,OAAO,CAAC,SAAS;IAIjB;;OAEG;IACH,OAAO,CAAC,WAAW;CAGpB;AAOD,wBAAgB,cAAc,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,WAAW,CAKhE;AAED;;GAEG;AACH,wBAAgB,MAAM,CAAC,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,OAAO,CAAC,EACvE,EAAE,EAAE,CAAC,EACL,YAAY,CAAC,EAAE,CAAC,GAAG,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC,KAAK,MAAM,EACjD,GAAG,CAAC,EAAE,MAAM,GACX,CAAC,CAeH"}
|
||||
247
packages/cache/src/redis.js
vendored
Normal file
247
packages/cache/src/redis.js
vendored
Normal file
@@ -0,0 +1,247 @@
|
||||
/**
|
||||
* Redis caching layer for The Order
|
||||
* Implements caching for database queries, cache invalidation, and cache monitoring
|
||||
*/
|
||||
import { createClient } from 'redis';
|
||||
import { getEnv, createLogger } from '@the-order/shared';
|
||||
const logger = createLogger('cache');
|
||||
/**
|
||||
* Redis Cache Client
|
||||
*/
|
||||
export class CacheClient {
|
||||
client = null;
|
||||
config;
|
||||
stats = {
|
||||
hits: 0,
|
||||
misses: 0,
|
||||
sets: 0,
|
||||
deletes: 0,
|
||||
errors: 0,
|
||||
};
|
||||
constructor(config = {}) {
|
||||
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() {
|
||||
if (this.client) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this.client = createClient({
|
||||
url: this.config.url,
|
||||
});
|
||||
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() {
|
||||
if (this.client) {
|
||||
await this.client.quit();
|
||||
this.client = null;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Get value from cache
|
||||
*/
|
||||
async get(key) {
|
||||
if (!this.client) {
|
||||
await this.connect();
|
||||
}
|
||||
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(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, value, ttl) {
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
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() {
|
||||
return { ...this.stats };
|
||||
}
|
||||
/**
|
||||
* Reset cache statistics
|
||||
*/
|
||||
resetStats() {
|
||||
this.stats = {
|
||||
hits: 0,
|
||||
misses: 0,
|
||||
sets: 0,
|
||||
deletes: 0,
|
||||
errors: 0,
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Get full key with prefix
|
||||
*/
|
||||
getFullKey(key) {
|
||||
return `${this.config.keyPrefix}${key}`;
|
||||
}
|
||||
/**
|
||||
* Serialize value
|
||||
*/
|
||||
serialize(value) {
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
/**
|
||||
* Deserialize value
|
||||
*/
|
||||
deserialize(value) {
|
||||
return JSON.parse(value);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Get default cache client
|
||||
*/
|
||||
let defaultCacheClient = null;
|
||||
export function getCacheClient(config) {
|
||||
if (!defaultCacheClient) {
|
||||
defaultCacheClient = new CacheClient(config);
|
||||
}
|
||||
return defaultCacheClient;
|
||||
}
|
||||
/**
|
||||
* Cache decorator for functions
|
||||
*/
|
||||
export function cached(fn, keyGenerator, ttl) {
|
||||
const cache = getCacheClient();
|
||||
return (async (...args) => {
|
||||
const key = keyGenerator ? keyGenerator(...args) : `fn:${fn.name}:${JSON.stringify(args)}`;
|
||||
const cachedValue = await cache.get(key);
|
||||
if (cachedValue !== null) {
|
||||
return cachedValue;
|
||||
}
|
||||
const result = await fn(...args);
|
||||
await cache.set(key, result, ttl);
|
||||
return result;
|
||||
});
|
||||
}
|
||||
//# sourceMappingURL=redis.js.map
|
||||
1
packages/cache/src/redis.js.map
vendored
Normal file
1
packages/cache/src/redis.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
167
packages/cache/src/redis.test.ts
vendored
Normal file
167
packages/cache/src/redis.test.ts
vendored
Normal file
@@ -0,0 +1,167 @@
|
||||
/**
|
||||
* Tests for Redis cache client
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { CacheClient } from './redis';
|
||||
|
||||
// Mock redis client
|
||||
vi.mock('redis', () => {
|
||||
const mockClient = {
|
||||
get: vi.fn(),
|
||||
setEx: vi.fn(),
|
||||
del: vi.fn(),
|
||||
exists: vi.fn(),
|
||||
keys: vi.fn(),
|
||||
on: vi.fn(),
|
||||
connect: vi.fn().mockResolvedValue(undefined),
|
||||
quit: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
|
||||
return {
|
||||
createClient: vi.fn(() => mockClient),
|
||||
};
|
||||
});
|
||||
|
||||
describe('CacheClient', () => {
|
||||
let cacheClient: CacheClient;
|
||||
let mockRedisClient: any;
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks();
|
||||
const redis = await import('redis');
|
||||
mockRedisClient = (redis.createClient as any)();
|
||||
cacheClient = new CacheClient({ url: 'redis://localhost:6379' });
|
||||
await cacheClient.connect();
|
||||
});
|
||||
|
||||
describe('get', () => {
|
||||
it('should get value from cache', async () => {
|
||||
mockRedisClient.get.mockResolvedValue('{"key": "value"}');
|
||||
|
||||
const value = await cacheClient.get<{ key: string }>('test-key');
|
||||
|
||||
expect(value).toEqual({ key: 'value' });
|
||||
expect(mockRedisClient.get).toHaveBeenCalledWith('the-order:test-key');
|
||||
});
|
||||
|
||||
it('should return null if key not found', async () => {
|
||||
mockRedisClient.get.mockResolvedValue(null);
|
||||
|
||||
const value = await cacheClient.get('nonexistent-key');
|
||||
|
||||
expect(value).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle errors gracefully', async () => {
|
||||
mockRedisClient.get.mockRejectedValue(new Error('Redis error'));
|
||||
|
||||
const value = await cacheClient.get('error-key');
|
||||
|
||||
expect(value).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('set', () => {
|
||||
it('should set value in cache', async () => {
|
||||
mockRedisClient.setEx.mockResolvedValue('OK');
|
||||
|
||||
await cacheClient.set('test-key', { key: 'value' }, 3600);
|
||||
|
||||
expect(mockRedisClient.setEx).toHaveBeenCalledWith(
|
||||
'the-order:test-key',
|
||||
3600,
|
||||
'{"key":"value"}'
|
||||
);
|
||||
});
|
||||
|
||||
it('should use default TTL if not provided', async () => {
|
||||
mockRedisClient.setEx.mockResolvedValue('OK');
|
||||
const client = new CacheClient({ url: 'redis://localhost:6379', ttl: 7200 });
|
||||
await client.connect();
|
||||
|
||||
await client.set('test-key', 'value');
|
||||
|
||||
expect(mockRedisClient.setEx).toHaveBeenCalledWith(
|
||||
'the-order:test-key',
|
||||
7200,
|
||||
'"value"'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete', () => {
|
||||
it('should delete key from cache', async () => {
|
||||
mockRedisClient.del.mockResolvedValue(1);
|
||||
|
||||
await cacheClient.delete('test-key');
|
||||
|
||||
expect(mockRedisClient.del).toHaveBeenCalledWith('the-order:test-key');
|
||||
});
|
||||
});
|
||||
|
||||
describe('invalidate', () => {
|
||||
it('should invalidate keys by pattern', async () => {
|
||||
mockRedisClient.keys.mockResolvedValue(['the-order:test-key1', 'the-order:test-key2']);
|
||||
mockRedisClient.del.mockResolvedValue(2);
|
||||
|
||||
const deleted = await cacheClient.invalidate('test-key*');
|
||||
|
||||
expect(deleted).toBe(2);
|
||||
expect(mockRedisClient.keys).toHaveBeenCalledWith('the-order:test-key*');
|
||||
});
|
||||
|
||||
it('should return 0 if no keys found', async () => {
|
||||
mockRedisClient.keys.mockResolvedValue([]);
|
||||
|
||||
const deleted = await cacheClient.invalidate('nonexistent*');
|
||||
|
||||
expect(deleted).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('exists', () => {
|
||||
it('should check if key exists', async () => {
|
||||
mockRedisClient.exists.mockResolvedValue(1);
|
||||
|
||||
const exists = await cacheClient.exists('test-key');
|
||||
|
||||
expect(exists).toBe(true);
|
||||
expect(mockRedisClient.exists).toHaveBeenCalledWith('the-order:test-key');
|
||||
});
|
||||
|
||||
it('should return false if key does not exist', async () => {
|
||||
mockRedisClient.exists.mockResolvedValue(0);
|
||||
|
||||
const exists = await cacheClient.exists('nonexistent-key');
|
||||
|
||||
expect(exists).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getStats', () => {
|
||||
it('should return cache statistics', () => {
|
||||
const stats = cacheClient.getStats();
|
||||
|
||||
expect(stats).toHaveProperty('hits');
|
||||
expect(stats).toHaveProperty('misses');
|
||||
expect(stats).toHaveProperty('sets');
|
||||
expect(stats).toHaveProperty('deletes');
|
||||
expect(stats).toHaveProperty('errors');
|
||||
});
|
||||
});
|
||||
|
||||
describe('resetStats', () => {
|
||||
it('should reset cache statistics', async () => {
|
||||
mockRedisClient.get.mockResolvedValue('{"key": "value"}');
|
||||
await cacheClient.get('test-key');
|
||||
|
||||
cacheClient.resetStats();
|
||||
const stats = cacheClient.getStats();
|
||||
|
||||
expect(stats.hits).toBe(0);
|
||||
expect(stats.misses).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
307
packages/cache/src/redis.ts
vendored
Normal file
307
packages/cache/src/redis.ts
vendored
Normal file
@@ -0,0 +1,307 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user