servcraft 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +29 -0
- package/.github/CODEOWNERS +18 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +46 -0
- package/.github/dependabot.yml +59 -0
- package/.github/workflows/ci.yml +188 -0
- package/.github/workflows/release.yml +195 -0
- package/AUDIT.md +602 -0
- package/README.md +1070 -1
- package/dist/cli/index.cjs +2026 -2168
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +2026 -2168
- package/dist/cli/index.js.map +1 -1
- package/dist/index.cjs +595 -616
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +114 -52
- package/dist/index.d.ts +114 -52
- package/dist/index.js +595 -616
- package/dist/index.js.map +1 -1
- package/docs/CLI-001_MULTI_DB_PLAN.md +546 -0
- package/docs/DATABASE_MULTI_ORM.md +399 -0
- package/docs/PHASE1_BREAKDOWN.md +346 -0
- package/docs/PROGRESS.md +550 -0
- package/docs/modules/ANALYTICS.md +226 -0
- package/docs/modules/API-VERSIONING.md +252 -0
- package/docs/modules/AUDIT.md +192 -0
- package/docs/modules/AUTH.md +431 -0
- package/docs/modules/CACHE.md +346 -0
- package/docs/modules/EMAIL.md +254 -0
- package/docs/modules/FEATURE-FLAG.md +291 -0
- package/docs/modules/I18N.md +294 -0
- package/docs/modules/MEDIA-PROCESSING.md +281 -0
- package/docs/modules/MFA.md +266 -0
- package/docs/modules/NOTIFICATION.md +311 -0
- package/docs/modules/OAUTH.md +237 -0
- package/docs/modules/PAYMENT.md +804 -0
- package/docs/modules/QUEUE.md +540 -0
- package/docs/modules/RATE-LIMIT.md +339 -0
- package/docs/modules/SEARCH.md +288 -0
- package/docs/modules/SECURITY.md +327 -0
- package/docs/modules/SESSION.md +382 -0
- package/docs/modules/SWAGGER.md +305 -0
- package/docs/modules/UPLOAD.md +296 -0
- package/docs/modules/USER.md +505 -0
- package/docs/modules/VALIDATION.md +294 -0
- package/docs/modules/WEBHOOK.md +270 -0
- package/docs/modules/WEBSOCKET.md +691 -0
- package/package.json +53 -38
- package/prisma/schema.prisma +395 -1
- package/src/cli/commands/add-module.ts +520 -87
- package/src/cli/commands/db.ts +3 -4
- package/src/cli/commands/docs.ts +256 -6
- package/src/cli/commands/generate.ts +12 -19
- package/src/cli/commands/init.ts +384 -214
- package/src/cli/index.ts +0 -4
- package/src/cli/templates/repository.ts +6 -1
- package/src/cli/templates/routes.ts +6 -21
- package/src/cli/utils/docs-generator.ts +6 -7
- package/src/cli/utils/env-manager.ts +717 -0
- package/src/cli/utils/field-parser.ts +16 -7
- package/src/cli/utils/interactive-prompt.ts +223 -0
- package/src/cli/utils/template-manager.ts +346 -0
- package/src/config/database.config.ts +183 -0
- package/src/config/env.ts +0 -10
- package/src/config/index.ts +0 -14
- package/src/core/server.ts +1 -1
- package/src/database/adapters/mongoose.adapter.ts +132 -0
- package/src/database/adapters/prisma.adapter.ts +118 -0
- package/src/database/connection.ts +190 -0
- package/src/database/interfaces/database.interface.ts +85 -0
- package/src/database/interfaces/index.ts +7 -0
- package/src/database/interfaces/repository.interface.ts +129 -0
- package/src/database/models/mongoose/index.ts +7 -0
- package/src/database/models/mongoose/payment.schema.ts +347 -0
- package/src/database/models/mongoose/user.schema.ts +154 -0
- package/src/database/prisma.ts +1 -4
- package/src/database/redis.ts +101 -0
- package/src/database/repositories/mongoose/index.ts +7 -0
- package/src/database/repositories/mongoose/payment.repository.ts +380 -0
- package/src/database/repositories/mongoose/user.repository.ts +255 -0
- package/src/database/seed.ts +6 -1
- package/src/index.ts +9 -20
- package/src/middleware/security.ts +2 -6
- package/src/modules/analytics/analytics.routes.ts +80 -0
- package/src/modules/analytics/analytics.service.ts +364 -0
- package/src/modules/analytics/index.ts +18 -0
- package/src/modules/analytics/types.ts +180 -0
- package/src/modules/api-versioning/index.ts +15 -0
- package/src/modules/api-versioning/types.ts +86 -0
- package/src/modules/api-versioning/versioning.middleware.ts +120 -0
- package/src/modules/api-versioning/versioning.routes.ts +54 -0
- package/src/modules/api-versioning/versioning.service.ts +189 -0
- package/src/modules/audit/audit.repository.ts +206 -0
- package/src/modules/audit/audit.service.ts +27 -59
- package/src/modules/auth/auth.controller.ts +2 -2
- package/src/modules/auth/auth.middleware.ts +3 -9
- package/src/modules/auth/auth.routes.ts +10 -107
- package/src/modules/auth/auth.service.ts +126 -23
- package/src/modules/auth/index.ts +3 -4
- package/src/modules/cache/cache.service.ts +367 -0
- package/src/modules/cache/index.ts +10 -0
- package/src/modules/cache/types.ts +44 -0
- package/src/modules/email/email.service.ts +3 -10
- package/src/modules/email/templates.ts +2 -8
- package/src/modules/feature-flag/feature-flag.repository.ts +303 -0
- package/src/modules/feature-flag/feature-flag.routes.ts +247 -0
- package/src/modules/feature-flag/feature-flag.service.ts +566 -0
- package/src/modules/feature-flag/index.ts +20 -0
- package/src/modules/feature-flag/types.ts +192 -0
- package/src/modules/i18n/i18n.middleware.ts +186 -0
- package/src/modules/i18n/i18n.routes.ts +191 -0
- package/src/modules/i18n/i18n.service.ts +456 -0
- package/src/modules/i18n/index.ts +18 -0
- package/src/modules/i18n/types.ts +118 -0
- package/src/modules/media-processing/index.ts +17 -0
- package/src/modules/media-processing/media-processing.routes.ts +111 -0
- package/src/modules/media-processing/media-processing.service.ts +245 -0
- package/src/modules/media-processing/types.ts +156 -0
- package/src/modules/mfa/index.ts +20 -0
- package/src/modules/mfa/mfa.repository.ts +206 -0
- package/src/modules/mfa/mfa.routes.ts +595 -0
- package/src/modules/mfa/mfa.service.ts +572 -0
- package/src/modules/mfa/totp.ts +150 -0
- package/src/modules/mfa/types.ts +57 -0
- package/src/modules/notification/index.ts +20 -0
- package/src/modules/notification/notification.repository.ts +356 -0
- package/src/modules/notification/notification.service.ts +483 -0
- package/src/modules/notification/types.ts +119 -0
- package/src/modules/oauth/index.ts +20 -0
- package/src/modules/oauth/oauth.repository.ts +219 -0
- package/src/modules/oauth/oauth.routes.ts +446 -0
- package/src/modules/oauth/oauth.service.ts +293 -0
- package/src/modules/oauth/providers/apple.provider.ts +250 -0
- package/src/modules/oauth/providers/facebook.provider.ts +181 -0
- package/src/modules/oauth/providers/github.provider.ts +248 -0
- package/src/modules/oauth/providers/google.provider.ts +189 -0
- package/src/modules/oauth/providers/twitter.provider.ts +214 -0
- package/src/modules/oauth/types.ts +94 -0
- package/src/modules/payment/index.ts +19 -0
- package/src/modules/payment/payment.repository.ts +733 -0
- package/src/modules/payment/payment.routes.ts +390 -0
- package/src/modules/payment/payment.service.ts +354 -0
- package/src/modules/payment/providers/mobile-money.provider.ts +274 -0
- package/src/modules/payment/providers/paypal.provider.ts +190 -0
- package/src/modules/payment/providers/stripe.provider.ts +215 -0
- package/src/modules/payment/types.ts +140 -0
- package/src/modules/queue/cron.ts +438 -0
- package/src/modules/queue/index.ts +87 -0
- package/src/modules/queue/queue.routes.ts +600 -0
- package/src/modules/queue/queue.service.ts +842 -0
- package/src/modules/queue/types.ts +222 -0
- package/src/modules/queue/workers.ts +366 -0
- package/src/modules/rate-limit/index.ts +59 -0
- package/src/modules/rate-limit/rate-limit.middleware.ts +134 -0
- package/src/modules/rate-limit/rate-limit.routes.ts +269 -0
- package/src/modules/rate-limit/rate-limit.service.ts +348 -0
- package/src/modules/rate-limit/stores/memory.store.ts +165 -0
- package/src/modules/rate-limit/stores/redis.store.ts +322 -0
- package/src/modules/rate-limit/types.ts +153 -0
- package/src/modules/search/adapters/elasticsearch.adapter.ts +326 -0
- package/src/modules/search/adapters/meilisearch.adapter.ts +261 -0
- package/src/modules/search/adapters/memory.adapter.ts +278 -0
- package/src/modules/search/index.ts +21 -0
- package/src/modules/search/search.service.ts +234 -0
- package/src/modules/search/types.ts +214 -0
- package/src/modules/security/index.ts +40 -0
- package/src/modules/security/sanitize.ts +223 -0
- package/src/modules/security/security-audit.service.ts +388 -0
- package/src/modules/security/security.middleware.ts +398 -0
- package/src/modules/session/index.ts +3 -0
- package/src/modules/session/session.repository.ts +159 -0
- package/src/modules/session/session.service.ts +340 -0
- package/src/modules/session/types.ts +38 -0
- package/src/modules/swagger/index.ts +7 -1
- package/src/modules/swagger/schema-builder.ts +16 -4
- package/src/modules/swagger/swagger.service.ts +9 -10
- package/src/modules/swagger/types.ts +0 -2
- package/src/modules/upload/index.ts +14 -0
- package/src/modules/upload/types.ts +83 -0
- package/src/modules/upload/upload.repository.ts +199 -0
- package/src/modules/upload/upload.routes.ts +311 -0
- package/src/modules/upload/upload.service.ts +448 -0
- package/src/modules/user/index.ts +3 -3
- package/src/modules/user/user.controller.ts +15 -9
- package/src/modules/user/user.repository.ts +237 -113
- package/src/modules/user/user.routes.ts +39 -164
- package/src/modules/user/user.service.ts +4 -3
- package/src/modules/validation/validator.ts +12 -17
- package/src/modules/webhook/index.ts +91 -0
- package/src/modules/webhook/retry.ts +196 -0
- package/src/modules/webhook/signature.ts +135 -0
- package/src/modules/webhook/types.ts +181 -0
- package/src/modules/webhook/webhook.repository.ts +358 -0
- package/src/modules/webhook/webhook.routes.ts +442 -0
- package/src/modules/webhook/webhook.service.ts +457 -0
- package/src/modules/websocket/features.ts +504 -0
- package/src/modules/websocket/index.ts +106 -0
- package/src/modules/websocket/middlewares.ts +298 -0
- package/src/modules/websocket/types.ts +181 -0
- package/src/modules/websocket/websocket.service.ts +692 -0
- package/src/utils/errors.ts +7 -0
- package/src/utils/pagination.ts +4 -1
- package/tests/helpers/db-check.ts +79 -0
- package/tests/integration/auth-redis.test.ts +94 -0
- package/tests/integration/cache-redis.test.ts +387 -0
- package/tests/integration/mongoose-repositories.test.ts +410 -0
- package/tests/integration/payment-prisma.test.ts +637 -0
- package/tests/integration/queue-bullmq.test.ts +417 -0
- package/tests/integration/user-prisma.test.ts +441 -0
- package/tests/integration/websocket-socketio.test.ts +552 -0
- package/tests/setup.ts +11 -9
- package/vitest.config.ts +3 -8
- package/npm-cache/_cacache/content-v2/sha512/1c/d0/03440d500a0487621aad1d6402978340698976602046db8e24fa03c01ee6c022c69b0582f969042d9442ee876ac35c038e960dd427d1e622fa24b8eb7dba +0 -0
- package/npm-cache/_cacache/content-v2/sha512/42/55/28b493ca491833e5aab0e9c3108d29ab3f36c248ca88f45d4630674fce9130959e56ae308797ac2b6328fa7f09a610b9550ed09cb971d039876d293fc69d +0 -0
- package/npm-cache/_cacache/content-v2/sha512/e0/12/f360dc9315ee5f17844a0c8c233ee6bf7c30837c4a02ea0d56c61c7f7ab21c0e958e50ed2c57c59f983c762b93056778c9009b2398ffc26def0183999b13 +0 -0
- package/npm-cache/_cacache/content-v2/sha512/ed/b0/fae1161902898f4c913c67d7f6cdf6be0665aec3b389b9c4f4f0a101ca1da59badf1b59c4e0030f5223023b8d63cfe501c46a32c20c895d4fb3f11ca2232 +0 -0
- package/npm-cache/_cacache/index-v5/58/94/c2cba79e0f16b4c10e95a87e32255741149e8222cc314a476aab67c39cc0 +0 -5
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import { logger } from '../../core/logger.js';
|
|
2
|
+
import type { CacheConfig, CacheEntry, CacheOptions, CacheStats, RedisConfig } from './types.js';
|
|
3
|
+
import { Redis } from 'ioredis';
|
|
4
|
+
|
|
5
|
+
const defaultConfig: CacheConfig = {
|
|
6
|
+
provider: 'memory',
|
|
7
|
+
ttl: 3600, // 1 hour default
|
|
8
|
+
prefix: 'servcraft:',
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// In-memory cache storage
|
|
12
|
+
const memoryCache = new Map<string, CacheEntry>();
|
|
13
|
+
const stats: CacheStats = { hits: 0, misses: 0, keys: 0 };
|
|
14
|
+
|
|
15
|
+
export class CacheService {
|
|
16
|
+
private config: CacheConfig;
|
|
17
|
+
private redisClient: Redis | null = null;
|
|
18
|
+
|
|
19
|
+
constructor(config: Partial<CacheConfig> = {}) {
|
|
20
|
+
this.config = { ...defaultConfig, ...config };
|
|
21
|
+
|
|
22
|
+
if (this.config.provider === 'redis' && this.config.redis) {
|
|
23
|
+
this.initRedis(this.config.redis);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Start cleanup interval for memory cache
|
|
27
|
+
if (this.config.provider === 'memory') {
|
|
28
|
+
const checkPeriod = (this.config.memory?.checkPeriod || 60) * 1000;
|
|
29
|
+
setInterval(() => this.cleanupExpired(), checkPeriod);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
private buildKey(key: string): string {
|
|
34
|
+
return `${this.config.prefix}${key}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async get<T>(key: string): Promise<T | null> {
|
|
38
|
+
const fullKey = this.buildKey(key);
|
|
39
|
+
|
|
40
|
+
if (this.config.provider === 'redis' && this.redisClient) {
|
|
41
|
+
return this.redisGet<T>(fullKey);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return this.memoryGet<T>(fullKey);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async set<T>(key: string, value: T, options: CacheOptions = {}): Promise<void> {
|
|
48
|
+
const fullKey = this.buildKey(key);
|
|
49
|
+
const ttl = options.ttl ?? this.config.ttl;
|
|
50
|
+
|
|
51
|
+
if (this.config.provider === 'redis' && this.redisClient) {
|
|
52
|
+
await this.redisSet(fullKey, value, ttl, options.tags);
|
|
53
|
+
} else {
|
|
54
|
+
this.memorySet(fullKey, value, ttl, options.tags);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async delete(key: string): Promise<boolean> {
|
|
59
|
+
const fullKey = this.buildKey(key);
|
|
60
|
+
|
|
61
|
+
if (this.config.provider === 'redis' && this.redisClient) {
|
|
62
|
+
return this.redisDelete(fullKey);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return this.memoryDelete(fullKey);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async exists(key: string): Promise<boolean> {
|
|
69
|
+
const fullKey = this.buildKey(key);
|
|
70
|
+
|
|
71
|
+
if (this.config.provider === 'redis' && this.redisClient) {
|
|
72
|
+
return this.redisExists(fullKey);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return this.memoryExists(fullKey);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async clear(): Promise<void> {
|
|
79
|
+
if (this.config.provider === 'redis' && this.redisClient) {
|
|
80
|
+
await this.redisClear();
|
|
81
|
+
} else {
|
|
82
|
+
memoryCache.clear();
|
|
83
|
+
stats.keys = 0;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
logger.info('Cache cleared');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async invalidateByTag(tag: string): Promise<number> {
|
|
90
|
+
let count = 0;
|
|
91
|
+
|
|
92
|
+
if (this.config.provider === 'memory') {
|
|
93
|
+
for (const [key, entry] of memoryCache.entries()) {
|
|
94
|
+
if (entry.tags?.includes(tag)) {
|
|
95
|
+
memoryCache.delete(key);
|
|
96
|
+
count++;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
stats.keys = memoryCache.size;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
logger.debug({ tag, count }, 'Cache invalidated by tag');
|
|
103
|
+
return count;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async getOrSet<T>(
|
|
107
|
+
key: string,
|
|
108
|
+
factory: () => Promise<T>,
|
|
109
|
+
options: CacheOptions = {}
|
|
110
|
+
): Promise<T> {
|
|
111
|
+
const cached = await this.get<T>(key);
|
|
112
|
+
if (cached !== null) {
|
|
113
|
+
return cached;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const value = await factory();
|
|
117
|
+
await this.set(key, value, options);
|
|
118
|
+
return value;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async mget<T>(keys: string[]): Promise<(T | null)[]> {
|
|
122
|
+
const results: (T | null)[] = [];
|
|
123
|
+
for (const key of keys) {
|
|
124
|
+
results.push(await this.get<T>(key));
|
|
125
|
+
}
|
|
126
|
+
return results;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async mset<T>(entries: Array<{ key: string; value: T; ttl?: number }>): Promise<void> {
|
|
130
|
+
for (const entry of entries) {
|
|
131
|
+
await this.set(entry.key, entry.value, { ttl: entry.ttl });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async increment(key: string, delta = 1): Promise<number> {
|
|
136
|
+
const fullKey = this.buildKey(key);
|
|
137
|
+
|
|
138
|
+
if (this.config.provider === 'redis' && this.redisClient) {
|
|
139
|
+
return this.redisIncrement(fullKey, delta);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const entry = memoryCache.get(fullKey);
|
|
143
|
+
const current = typeof entry?.value === 'number' ? entry.value : 0;
|
|
144
|
+
const newValue = current + delta;
|
|
145
|
+
|
|
146
|
+
this.memorySet(
|
|
147
|
+
fullKey,
|
|
148
|
+
newValue,
|
|
149
|
+
entry?.expiresAt ? (entry.expiresAt - Date.now()) / 1000 : this.config.ttl
|
|
150
|
+
);
|
|
151
|
+
return newValue;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async decrement(key: string, delta = 1): Promise<number> {
|
|
155
|
+
return this.increment(key, -delta);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
getStats(): CacheStats {
|
|
159
|
+
return {
|
|
160
|
+
...stats,
|
|
161
|
+
keys: this.config.provider === 'memory' ? memoryCache.size : stats.keys,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Memory cache methods
|
|
166
|
+
private memoryGet<T>(key: string): T | null {
|
|
167
|
+
const entry = memoryCache.get(key);
|
|
168
|
+
|
|
169
|
+
if (!entry) {
|
|
170
|
+
stats.misses++;
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (Date.now() > entry.expiresAt) {
|
|
175
|
+
memoryCache.delete(key);
|
|
176
|
+
stats.misses++;
|
|
177
|
+
stats.keys = memoryCache.size;
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
stats.hits++;
|
|
182
|
+
return entry.value as T;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private memorySet<T>(key: string, value: T, ttl: number, tags?: string[]): void {
|
|
186
|
+
const entry: CacheEntry<T> = {
|
|
187
|
+
value,
|
|
188
|
+
expiresAt: Date.now() + ttl * 1000,
|
|
189
|
+
tags,
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
// Check max size
|
|
193
|
+
const maxSize = this.config.memory?.maxSize;
|
|
194
|
+
if (maxSize && memoryCache.size >= maxSize && !memoryCache.has(key)) {
|
|
195
|
+
// Remove oldest entry
|
|
196
|
+
const oldestKey = memoryCache.keys().next().value;
|
|
197
|
+
if (oldestKey) {
|
|
198
|
+
memoryCache.delete(oldestKey);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
memoryCache.set(key, entry);
|
|
203
|
+
stats.keys = memoryCache.size;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
private memoryDelete(key: string): boolean {
|
|
207
|
+
const deleted = memoryCache.delete(key);
|
|
208
|
+
stats.keys = memoryCache.size;
|
|
209
|
+
return deleted;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private memoryExists(key: string): boolean {
|
|
213
|
+
const entry = memoryCache.get(key);
|
|
214
|
+
if (!entry) return false;
|
|
215
|
+
if (Date.now() > entry.expiresAt) {
|
|
216
|
+
memoryCache.delete(key);
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
private cleanupExpired(): void {
|
|
223
|
+
const now = Date.now();
|
|
224
|
+
let cleaned = 0;
|
|
225
|
+
|
|
226
|
+
for (const [key, entry] of memoryCache.entries()) {
|
|
227
|
+
if (now > entry.expiresAt) {
|
|
228
|
+
memoryCache.delete(key);
|
|
229
|
+
cleaned++;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (cleaned > 0) {
|
|
234
|
+
stats.keys = memoryCache.size;
|
|
235
|
+
logger.debug({ cleaned }, 'Expired cache entries cleaned');
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Redis methods
|
|
240
|
+
private initRedis(config: RedisConfig): void {
|
|
241
|
+
try {
|
|
242
|
+
this.redisClient = new Redis({
|
|
243
|
+
host: config.host,
|
|
244
|
+
port: config.port,
|
|
245
|
+
password: config.password,
|
|
246
|
+
db: config.db || 0,
|
|
247
|
+
connectTimeout: config.connectTimeout || 10000,
|
|
248
|
+
retryStrategy: (times: number) => {
|
|
249
|
+
const maxRetries = config.maxRetries || 10;
|
|
250
|
+
if (times > maxRetries) {
|
|
251
|
+
logger.error('Redis max retries reached, giving up');
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
const delay = Math.min(times * 50, 2000);
|
|
255
|
+
return delay;
|
|
256
|
+
},
|
|
257
|
+
...(config.tls && { tls: {} }),
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
this.redisClient.on('connect', () => {
|
|
261
|
+
logger.info({ host: config.host, port: config.port }, 'Redis cache connected');
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
this.redisClient.on('error', (error: Error) => {
|
|
265
|
+
logger.error({ err: error }, 'Redis cache error');
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
this.redisClient.on('close', () => {
|
|
269
|
+
logger.warn('Redis cache connection closed');
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
this.redisClient.on('reconnecting', () => {
|
|
273
|
+
logger.info('Redis cache reconnecting');
|
|
274
|
+
});
|
|
275
|
+
} catch (error) {
|
|
276
|
+
logger.error({ err: error }, 'Failed to initialize Redis cache');
|
|
277
|
+
this.redisClient = null;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
private async redisGet<T>(key: string): Promise<T | null> {
|
|
282
|
+
if (!this.redisClient) return null;
|
|
283
|
+
try {
|
|
284
|
+
const data = await this.redisClient.get(key);
|
|
285
|
+
if (!data) {
|
|
286
|
+
stats.misses++;
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
stats.hits++;
|
|
290
|
+
return JSON.parse(data) as T;
|
|
291
|
+
} catch {
|
|
292
|
+
stats.misses++;
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
private async redisSet<T>(key: string, value: T, ttl: number, _tags?: string[]): Promise<void> {
|
|
298
|
+
if (!this.redisClient) return;
|
|
299
|
+
await this.redisClient.setex(key, ttl, JSON.stringify(value));
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
private async redisDelete(key: string): Promise<boolean> {
|
|
303
|
+
if (!this.redisClient) return false;
|
|
304
|
+
const result = await this.redisClient.del(key);
|
|
305
|
+
return result > 0;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
private async redisExists(key: string): Promise<boolean> {
|
|
309
|
+
if (!this.redisClient) return false;
|
|
310
|
+
const result = await this.redisClient.exists(key);
|
|
311
|
+
return result > 0;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
private async redisClear(): Promise<void> {
|
|
315
|
+
if (!this.redisClient) return;
|
|
316
|
+
const pattern = `${this.config.prefix}*`;
|
|
317
|
+
const keys = await this.redisClient.keys(pattern);
|
|
318
|
+
if (keys.length > 0) {
|
|
319
|
+
await this.redisClient.del(...keys);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
private async redisIncrement(key: string, delta: number): Promise<number> {
|
|
324
|
+
if (!this.redisClient) return 0;
|
|
325
|
+
return this.redisClient.incrby(key, delta);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async close(): Promise<void> {
|
|
329
|
+
if (this.redisClient) {
|
|
330
|
+
await this.redisClient.quit();
|
|
331
|
+
this.redisClient = null;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
let cacheService: CacheService | null = null;
|
|
337
|
+
|
|
338
|
+
export function getCacheService(): CacheService {
|
|
339
|
+
if (!cacheService) {
|
|
340
|
+
cacheService = new CacheService();
|
|
341
|
+
}
|
|
342
|
+
return cacheService;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export function createCacheService(config: Partial<CacheConfig>): CacheService {
|
|
346
|
+
cacheService = new CacheService(config);
|
|
347
|
+
return cacheService;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Cache decorator helper
|
|
351
|
+
export function cached<T extends (...args: unknown[]) => Promise<unknown>>(
|
|
352
|
+
keyFn: (...args: Parameters<T>) => string,
|
|
353
|
+
options: CacheOptions = {}
|
|
354
|
+
) {
|
|
355
|
+
return function (_target: unknown, _propertyKey: string, descriptor: PropertyDescriptor) {
|
|
356
|
+
const originalMethod = descriptor.value;
|
|
357
|
+
|
|
358
|
+
descriptor.value = async function (...args: Parameters<T>) {
|
|
359
|
+
const cache = getCacheService();
|
|
360
|
+
const key = keyFn(...args);
|
|
361
|
+
|
|
362
|
+
return cache.getOrSet(key, () => originalMethod.apply(this, args), options);
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
return descriptor;
|
|
366
|
+
};
|
|
367
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export type CacheProvider = 'memory' | 'redis';
|
|
2
|
+
|
|
3
|
+
export interface CacheConfig {
|
|
4
|
+
provider: CacheProvider;
|
|
5
|
+
ttl: number; // default TTL in seconds
|
|
6
|
+
prefix?: string;
|
|
7
|
+
redis?: RedisConfig;
|
|
8
|
+
memory?: MemoryCacheConfig;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface RedisConfig {
|
|
12
|
+
host: string;
|
|
13
|
+
port: number;
|
|
14
|
+
password?: string;
|
|
15
|
+
db?: number;
|
|
16
|
+
tls?: boolean;
|
|
17
|
+
keyPrefix?: string;
|
|
18
|
+
connectTimeout?: number;
|
|
19
|
+
maxRetries?: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface MemoryCacheConfig {
|
|
23
|
+
maxSize?: number; // max number of items
|
|
24
|
+
maxMemory?: number; // max memory in bytes
|
|
25
|
+
checkPeriod?: number; // cleanup check interval in seconds
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface CacheEntry<T = unknown> {
|
|
29
|
+
value: T;
|
|
30
|
+
expiresAt: number;
|
|
31
|
+
tags?: string[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface CacheOptions {
|
|
35
|
+
ttl?: number;
|
|
36
|
+
tags?: string[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface CacheStats {
|
|
40
|
+
hits: number;
|
|
41
|
+
misses: number;
|
|
42
|
+
keys: number;
|
|
43
|
+
memory?: number;
|
|
44
|
+
}
|
|
@@ -2,7 +2,7 @@ import nodemailer from 'nodemailer';
|
|
|
2
2
|
import type { Transporter } from 'nodemailer';
|
|
3
3
|
import { config } from '../../config/index.js';
|
|
4
4
|
import { logger } from '../../core/logger.js';
|
|
5
|
-
import { renderTemplate
|
|
5
|
+
import { renderTemplate } from './templates.js';
|
|
6
6
|
import type { EmailOptions, EmailResult, EmailConfig, TemplateData } from './types.js';
|
|
7
7
|
|
|
8
8
|
export class EmailService {
|
|
@@ -73,10 +73,7 @@ export class EmailService {
|
|
|
73
73
|
|
|
74
74
|
const result = await this.transporter.sendMail(mailOptions);
|
|
75
75
|
|
|
76
|
-
logger.info(
|
|
77
|
-
{ messageId: result.messageId, to: options.to },
|
|
78
|
-
'Email sent successfully'
|
|
79
|
-
);
|
|
76
|
+
logger.info({ messageId: result.messageId, to: options.to }, 'Email sent successfully');
|
|
80
77
|
|
|
81
78
|
return {
|
|
82
79
|
success: true,
|
|
@@ -93,11 +90,7 @@ export class EmailService {
|
|
|
93
90
|
}
|
|
94
91
|
}
|
|
95
92
|
|
|
96
|
-
async sendTemplate(
|
|
97
|
-
to: string,
|
|
98
|
-
template: string,
|
|
99
|
-
data: TemplateData
|
|
100
|
-
): Promise<EmailResult> {
|
|
93
|
+
async sendTemplate(to: string, template: string, data: TemplateData): Promise<EmailResult> {
|
|
101
94
|
const subjects: Record<string, string> = {
|
|
102
95
|
welcome: `Welcome to ${data.appName || 'Servcraft'}!`,
|
|
103
96
|
'verify-email': 'Verify Your Email',
|
|
@@ -163,10 +163,7 @@ for (const [name, template] of Object.entries(templates)) {
|
|
|
163
163
|
compiledTemplates[name] = Handlebars.compile(template);
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
-
export function renderTemplate(
|
|
167
|
-
templateName: string,
|
|
168
|
-
data: Record<string, unknown>
|
|
169
|
-
): string {
|
|
166
|
+
export function renderTemplate(templateName: string, data: Record<string, unknown>): string {
|
|
170
167
|
const template = compiledTemplates[templateName];
|
|
171
168
|
|
|
172
169
|
if (!template) {
|
|
@@ -183,10 +180,7 @@ export function renderTemplate(
|
|
|
183
180
|
});
|
|
184
181
|
}
|
|
185
182
|
|
|
186
|
-
export function renderCustomTemplate(
|
|
187
|
-
htmlTemplate: string,
|
|
188
|
-
data: Record<string, unknown>
|
|
189
|
-
): string {
|
|
183
|
+
export function renderCustomTemplate(htmlTemplate: string, data: Record<string, unknown>): string {
|
|
190
184
|
const template = Handlebars.compile(htmlTemplate);
|
|
191
185
|
const body = template(data);
|
|
192
186
|
|