servcraft 0.1.0 → 0.1.3

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.
Files changed (217) hide show
  1. package/.claude/settings.local.json +30 -0
  2. package/.github/CODEOWNERS +18 -0
  3. package/.github/PULL_REQUEST_TEMPLATE.md +46 -0
  4. package/.github/dependabot.yml +59 -0
  5. package/.github/workflows/ci.yml +188 -0
  6. package/.github/workflows/release.yml +195 -0
  7. package/AUDIT.md +602 -0
  8. package/LICENSE +21 -0
  9. package/README.md +1102 -1
  10. package/dist/cli/index.cjs +2026 -2168
  11. package/dist/cli/index.cjs.map +1 -1
  12. package/dist/cli/index.js +2026 -2168
  13. package/dist/cli/index.js.map +1 -1
  14. package/dist/index.cjs +595 -616
  15. package/dist/index.cjs.map +1 -1
  16. package/dist/index.d.cts +114 -52
  17. package/dist/index.d.ts +114 -52
  18. package/dist/index.js +595 -616
  19. package/dist/index.js.map +1 -1
  20. package/docs/CLI-001_MULTI_DB_PLAN.md +546 -0
  21. package/docs/DATABASE_MULTI_ORM.md +399 -0
  22. package/docs/PHASE1_BREAKDOWN.md +346 -0
  23. package/docs/PROGRESS.md +550 -0
  24. package/docs/modules/ANALYTICS.md +226 -0
  25. package/docs/modules/API-VERSIONING.md +252 -0
  26. package/docs/modules/AUDIT.md +192 -0
  27. package/docs/modules/AUTH.md +431 -0
  28. package/docs/modules/CACHE.md +346 -0
  29. package/docs/modules/EMAIL.md +254 -0
  30. package/docs/modules/FEATURE-FLAG.md +291 -0
  31. package/docs/modules/I18N.md +294 -0
  32. package/docs/modules/MEDIA-PROCESSING.md +281 -0
  33. package/docs/modules/MFA.md +266 -0
  34. package/docs/modules/NOTIFICATION.md +311 -0
  35. package/docs/modules/OAUTH.md +237 -0
  36. package/docs/modules/PAYMENT.md +804 -0
  37. package/docs/modules/QUEUE.md +540 -0
  38. package/docs/modules/RATE-LIMIT.md +339 -0
  39. package/docs/modules/SEARCH.md +288 -0
  40. package/docs/modules/SECURITY.md +327 -0
  41. package/docs/modules/SESSION.md +382 -0
  42. package/docs/modules/SWAGGER.md +305 -0
  43. package/docs/modules/UPLOAD.md +296 -0
  44. package/docs/modules/USER.md +505 -0
  45. package/docs/modules/VALIDATION.md +294 -0
  46. package/docs/modules/WEBHOOK.md +270 -0
  47. package/docs/modules/WEBSOCKET.md +691 -0
  48. package/package.json +53 -38
  49. package/prisma/schema.prisma +395 -1
  50. package/src/cli/commands/add-module.ts +520 -87
  51. package/src/cli/commands/db.ts +3 -4
  52. package/src/cli/commands/docs.ts +256 -6
  53. package/src/cli/commands/generate.ts +12 -19
  54. package/src/cli/commands/init.ts +384 -214
  55. package/src/cli/index.ts +0 -4
  56. package/src/cli/templates/repository.ts +6 -1
  57. package/src/cli/templates/routes.ts +6 -21
  58. package/src/cli/utils/docs-generator.ts +6 -7
  59. package/src/cli/utils/env-manager.ts +717 -0
  60. package/src/cli/utils/field-parser.ts +16 -7
  61. package/src/cli/utils/interactive-prompt.ts +223 -0
  62. package/src/cli/utils/template-manager.ts +346 -0
  63. package/src/config/database.config.ts +183 -0
  64. package/src/config/env.ts +0 -10
  65. package/src/config/index.ts +0 -14
  66. package/src/core/server.ts +1 -1
  67. package/src/database/adapters/mongoose.adapter.ts +132 -0
  68. package/src/database/adapters/prisma.adapter.ts +118 -0
  69. package/src/database/connection.ts +190 -0
  70. package/src/database/interfaces/database.interface.ts +85 -0
  71. package/src/database/interfaces/index.ts +7 -0
  72. package/src/database/interfaces/repository.interface.ts +129 -0
  73. package/src/database/models/mongoose/index.ts +7 -0
  74. package/src/database/models/mongoose/payment.schema.ts +347 -0
  75. package/src/database/models/mongoose/user.schema.ts +154 -0
  76. package/src/database/prisma.ts +1 -4
  77. package/src/database/redis.ts +101 -0
  78. package/src/database/repositories/mongoose/index.ts +7 -0
  79. package/src/database/repositories/mongoose/payment.repository.ts +380 -0
  80. package/src/database/repositories/mongoose/user.repository.ts +255 -0
  81. package/src/database/seed.ts +6 -1
  82. package/src/index.ts +9 -20
  83. package/src/middleware/security.ts +2 -6
  84. package/src/modules/analytics/analytics.routes.ts +80 -0
  85. package/src/modules/analytics/analytics.service.ts +364 -0
  86. package/src/modules/analytics/index.ts +18 -0
  87. package/src/modules/analytics/types.ts +180 -0
  88. package/src/modules/api-versioning/index.ts +15 -0
  89. package/src/modules/api-versioning/types.ts +86 -0
  90. package/src/modules/api-versioning/versioning.middleware.ts +120 -0
  91. package/src/modules/api-versioning/versioning.routes.ts +54 -0
  92. package/src/modules/api-versioning/versioning.service.ts +189 -0
  93. package/src/modules/audit/audit.repository.ts +206 -0
  94. package/src/modules/audit/audit.service.ts +27 -59
  95. package/src/modules/auth/auth.controller.ts +2 -2
  96. package/src/modules/auth/auth.middleware.ts +3 -9
  97. package/src/modules/auth/auth.routes.ts +10 -107
  98. package/src/modules/auth/auth.service.ts +126 -23
  99. package/src/modules/auth/index.ts +3 -4
  100. package/src/modules/cache/cache.service.ts +367 -0
  101. package/src/modules/cache/index.ts +10 -0
  102. package/src/modules/cache/types.ts +44 -0
  103. package/src/modules/email/email.service.ts +3 -10
  104. package/src/modules/email/templates.ts +2 -8
  105. package/src/modules/feature-flag/feature-flag.repository.ts +303 -0
  106. package/src/modules/feature-flag/feature-flag.routes.ts +247 -0
  107. package/src/modules/feature-flag/feature-flag.service.ts +566 -0
  108. package/src/modules/feature-flag/index.ts +20 -0
  109. package/src/modules/feature-flag/types.ts +192 -0
  110. package/src/modules/i18n/i18n.middleware.ts +186 -0
  111. package/src/modules/i18n/i18n.routes.ts +191 -0
  112. package/src/modules/i18n/i18n.service.ts +456 -0
  113. package/src/modules/i18n/index.ts +18 -0
  114. package/src/modules/i18n/types.ts +118 -0
  115. package/src/modules/media-processing/index.ts +17 -0
  116. package/src/modules/media-processing/media-processing.routes.ts +111 -0
  117. package/src/modules/media-processing/media-processing.service.ts +245 -0
  118. package/src/modules/media-processing/types.ts +156 -0
  119. package/src/modules/mfa/index.ts +20 -0
  120. package/src/modules/mfa/mfa.repository.ts +206 -0
  121. package/src/modules/mfa/mfa.routes.ts +595 -0
  122. package/src/modules/mfa/mfa.service.ts +572 -0
  123. package/src/modules/mfa/totp.ts +150 -0
  124. package/src/modules/mfa/types.ts +57 -0
  125. package/src/modules/notification/index.ts +20 -0
  126. package/src/modules/notification/notification.repository.ts +356 -0
  127. package/src/modules/notification/notification.service.ts +483 -0
  128. package/src/modules/notification/types.ts +119 -0
  129. package/src/modules/oauth/index.ts +20 -0
  130. package/src/modules/oauth/oauth.repository.ts +219 -0
  131. package/src/modules/oauth/oauth.routes.ts +446 -0
  132. package/src/modules/oauth/oauth.service.ts +293 -0
  133. package/src/modules/oauth/providers/apple.provider.ts +250 -0
  134. package/src/modules/oauth/providers/facebook.provider.ts +181 -0
  135. package/src/modules/oauth/providers/github.provider.ts +248 -0
  136. package/src/modules/oauth/providers/google.provider.ts +189 -0
  137. package/src/modules/oauth/providers/twitter.provider.ts +214 -0
  138. package/src/modules/oauth/types.ts +94 -0
  139. package/src/modules/payment/index.ts +19 -0
  140. package/src/modules/payment/payment.repository.ts +733 -0
  141. package/src/modules/payment/payment.routes.ts +390 -0
  142. package/src/modules/payment/payment.service.ts +354 -0
  143. package/src/modules/payment/providers/mobile-money.provider.ts +274 -0
  144. package/src/modules/payment/providers/paypal.provider.ts +190 -0
  145. package/src/modules/payment/providers/stripe.provider.ts +215 -0
  146. package/src/modules/payment/types.ts +140 -0
  147. package/src/modules/queue/cron.ts +438 -0
  148. package/src/modules/queue/index.ts +87 -0
  149. package/src/modules/queue/queue.routes.ts +600 -0
  150. package/src/modules/queue/queue.service.ts +842 -0
  151. package/src/modules/queue/types.ts +222 -0
  152. package/src/modules/queue/workers.ts +366 -0
  153. package/src/modules/rate-limit/index.ts +59 -0
  154. package/src/modules/rate-limit/rate-limit.middleware.ts +134 -0
  155. package/src/modules/rate-limit/rate-limit.routes.ts +269 -0
  156. package/src/modules/rate-limit/rate-limit.service.ts +348 -0
  157. package/src/modules/rate-limit/stores/memory.store.ts +165 -0
  158. package/src/modules/rate-limit/stores/redis.store.ts +322 -0
  159. package/src/modules/rate-limit/types.ts +153 -0
  160. package/src/modules/search/adapters/elasticsearch.adapter.ts +326 -0
  161. package/src/modules/search/adapters/meilisearch.adapter.ts +261 -0
  162. package/src/modules/search/adapters/memory.adapter.ts +278 -0
  163. package/src/modules/search/index.ts +21 -0
  164. package/src/modules/search/search.service.ts +234 -0
  165. package/src/modules/search/types.ts +214 -0
  166. package/src/modules/security/index.ts +40 -0
  167. package/src/modules/security/sanitize.ts +223 -0
  168. package/src/modules/security/security-audit.service.ts +388 -0
  169. package/src/modules/security/security.middleware.ts +398 -0
  170. package/src/modules/session/index.ts +3 -0
  171. package/src/modules/session/session.repository.ts +159 -0
  172. package/src/modules/session/session.service.ts +340 -0
  173. package/src/modules/session/types.ts +38 -0
  174. package/src/modules/swagger/index.ts +7 -1
  175. package/src/modules/swagger/schema-builder.ts +16 -4
  176. package/src/modules/swagger/swagger.service.ts +9 -10
  177. package/src/modules/swagger/types.ts +0 -2
  178. package/src/modules/upload/index.ts +14 -0
  179. package/src/modules/upload/types.ts +83 -0
  180. package/src/modules/upload/upload.repository.ts +199 -0
  181. package/src/modules/upload/upload.routes.ts +311 -0
  182. package/src/modules/upload/upload.service.ts +448 -0
  183. package/src/modules/user/index.ts +3 -3
  184. package/src/modules/user/user.controller.ts +15 -9
  185. package/src/modules/user/user.repository.ts +237 -113
  186. package/src/modules/user/user.routes.ts +39 -164
  187. package/src/modules/user/user.service.ts +4 -3
  188. package/src/modules/validation/validator.ts +12 -17
  189. package/src/modules/webhook/index.ts +91 -0
  190. package/src/modules/webhook/retry.ts +196 -0
  191. package/src/modules/webhook/signature.ts +135 -0
  192. package/src/modules/webhook/types.ts +181 -0
  193. package/src/modules/webhook/webhook.repository.ts +358 -0
  194. package/src/modules/webhook/webhook.routes.ts +442 -0
  195. package/src/modules/webhook/webhook.service.ts +457 -0
  196. package/src/modules/websocket/features.ts +504 -0
  197. package/src/modules/websocket/index.ts +106 -0
  198. package/src/modules/websocket/middlewares.ts +298 -0
  199. package/src/modules/websocket/types.ts +181 -0
  200. package/src/modules/websocket/websocket.service.ts +692 -0
  201. package/src/utils/errors.ts +7 -0
  202. package/src/utils/pagination.ts +4 -1
  203. package/tests/helpers/db-check.ts +79 -0
  204. package/tests/integration/auth-redis.test.ts +94 -0
  205. package/tests/integration/cache-redis.test.ts +387 -0
  206. package/tests/integration/mongoose-repositories.test.ts +410 -0
  207. package/tests/integration/payment-prisma.test.ts +637 -0
  208. package/tests/integration/queue-bullmq.test.ts +417 -0
  209. package/tests/integration/user-prisma.test.ts +441 -0
  210. package/tests/integration/websocket-socketio.test.ts +552 -0
  211. package/tests/setup.ts +11 -9
  212. package/vitest.config.ts +3 -8
  213. package/npm-cache/_cacache/content-v2/sha512/1c/d0/03440d500a0487621aad1d6402978340698976602046db8e24fa03c01ee6c022c69b0582f969042d9442ee876ac35c038e960dd427d1e622fa24b8eb7dba +0 -0
  214. package/npm-cache/_cacache/content-v2/sha512/42/55/28b493ca491833e5aab0e9c3108d29ab3f36c248ca88f45d4630674fce9130959e56ae308797ac2b6328fa7f09a610b9550ed09cb971d039876d293fc69d +0 -0
  215. package/npm-cache/_cacache/content-v2/sha512/e0/12/f360dc9315ee5f17844a0c8c233ee6bf7c30837c4a02ea0d56c61c7f7ab21c0e958e50ed2c57c59f983c762b93056778c9009b2398ffc26def0183999b13 +0 -0
  216. package/npm-cache/_cacache/content-v2/sha512/ed/b0/fae1161902898f4c913c67d7f6cdf6be0665aec3b389b9c4f4f0a101ca1da59badf1b59c4e0030f5223023b8d63cfe501c46a32c20c895d4fb3f11ca2232 +0 -0
  217. 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,10 @@
1
+ export { CacheService, getCacheService, createCacheService, cached } from './cache.service.js';
2
+ export type {
3
+ CacheConfig,
4
+ CacheEntry,
5
+ CacheOptions,
6
+ CacheStats,
7
+ CacheProvider,
8
+ RedisConfig,
9
+ MemoryCacheConfig,
10
+ } from './types.js';
@@ -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, renderCustomTemplate } from './templates.js';
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