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
@@ -22,42 +22,49 @@ export class AppError extends Error {
22
22
  export class NotFoundError extends AppError {
23
23
  constructor(resource = 'Resource') {
24
24
  super(`${resource} not found`, 404);
25
+ Object.setPrototypeOf(this, NotFoundError.prototype);
25
26
  }
26
27
  }
27
28
 
28
29
  export class UnauthorizedError extends AppError {
29
30
  constructor(message = 'Unauthorized') {
30
31
  super(message, 401);
32
+ Object.setPrototypeOf(this, UnauthorizedError.prototype);
31
33
  }
32
34
  }
33
35
 
34
36
  export class ForbiddenError extends AppError {
35
37
  constructor(message = 'Forbidden') {
36
38
  super(message, 403);
39
+ Object.setPrototypeOf(this, ForbiddenError.prototype);
37
40
  }
38
41
  }
39
42
 
40
43
  export class BadRequestError extends AppError {
41
44
  constructor(message = 'Bad request', errors?: Record<string, string[]>) {
42
45
  super(message, 400, true, errors);
46
+ Object.setPrototypeOf(this, BadRequestError.prototype);
43
47
  }
44
48
  }
45
49
 
46
50
  export class ConflictError extends AppError {
47
51
  constructor(message = 'Resource already exists') {
48
52
  super(message, 409);
53
+ Object.setPrototypeOf(this, ConflictError.prototype);
49
54
  }
50
55
  }
51
56
 
52
57
  export class ValidationError extends AppError {
53
58
  constructor(errors: Record<string, string[]>) {
54
59
  super('Validation failed', 422, true, errors);
60
+ Object.setPrototypeOf(this, ValidationError.prototype);
55
61
  }
56
62
  }
57
63
 
58
64
  export class TooManyRequestsError extends AppError {
59
65
  constructor(message = 'Too many requests') {
60
66
  super(message, 429);
67
+ Object.setPrototypeOf(this, TooManyRequestsError.prototype);
61
68
  }
62
69
  }
63
70
 
@@ -6,7 +6,10 @@ export const MAX_LIMIT = 100;
6
6
 
7
7
  export function parsePaginationParams(query: Record<string, unknown>): PaginationParams {
8
8
  const page = Math.max(1, parseInt(String(query.page || DEFAULT_PAGE), 10));
9
- const limit = Math.min(MAX_LIMIT, Math.max(1, parseInt(String(query.limit || DEFAULT_LIMIT), 10)));
9
+ const limit = Math.min(
10
+ MAX_LIMIT,
11
+ Math.max(1, parseInt(String(query.limit || DEFAULT_LIMIT), 10))
12
+ );
10
13
  const sortBy = typeof query.sortBy === 'string' ? query.sortBy : undefined;
11
14
  const sortOrder = query.sortOrder === 'desc' ? 'desc' : 'asc';
12
15
 
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Database availability check helpers for integration tests
3
+ */
4
+ import { describe } from 'vitest';
5
+
6
+ let _dbAvailable: boolean | null = null;
7
+ let _redisAvailable: boolean | null = null;
8
+
9
+ /**
10
+ * Check if PostgreSQL database is available
11
+ */
12
+ export async function isDatabaseAvailable(): Promise<boolean> {
13
+ if (_dbAvailable !== null) return _dbAvailable;
14
+
15
+ try {
16
+ // Dynamic import to avoid initialization errors
17
+ const { prisma } = await import('../../src/database/prisma.js');
18
+ await prisma.$queryRaw`SELECT 1`;
19
+ _dbAvailable = true;
20
+ } catch {
21
+ _dbAvailable = false;
22
+ }
23
+
24
+ return _dbAvailable;
25
+ }
26
+
27
+ /**
28
+ * Check if Redis is available
29
+ */
30
+ export async function isRedisAvailable(): Promise<boolean> {
31
+ if (_redisAvailable !== null) return _redisAvailable;
32
+
33
+ try {
34
+ const { getRedis } = await import('../../src/database/redis.js');
35
+ const redis = getRedis();
36
+ await redis.ping();
37
+ _redisAvailable = true;
38
+ } catch {
39
+ _redisAvailable = false;
40
+ }
41
+
42
+ return _redisAvailable;
43
+ }
44
+
45
+ /**
46
+ * Conditionally run describe block based on database availability
47
+ * If database is not available, tests are skipped
48
+ */
49
+ export function describeWithDb(
50
+ name: string,
51
+ fn: () => void
52
+ ): ReturnType<typeof describe> | ReturnType<typeof describe.skip> {
53
+ // We need to check synchronously for Vitest
54
+ // Use environment variable to indicate DB availability
55
+ const dbUrl = process.env.DATABASE_URL;
56
+ const hasValidDbUrl = dbUrl && !dbUrl.includes('test:test@localhost');
57
+
58
+ if (process.env.SKIP_DB_TESTS === 'true' || !hasValidDbUrl) {
59
+ return describe.skip(name, fn);
60
+ }
61
+
62
+ return describe(name, fn);
63
+ }
64
+
65
+ /**
66
+ * Conditionally run describe block based on Redis availability
67
+ */
68
+ export function describeWithRedis(
69
+ name: string,
70
+ fn: () => void
71
+ ): ReturnType<typeof describe> | ReturnType<typeof describe.skip> {
72
+ const redisUrl = process.env.REDIS_URL;
73
+
74
+ if (process.env.SKIP_REDIS_TESTS === 'true' || !redisUrl) {
75
+ return describe.skip(name, fn);
76
+ }
77
+
78
+ return describe(name, fn);
79
+ }
@@ -0,0 +1,94 @@
1
+ import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
2
+ import Redis from 'ioredis';
3
+
4
+ // These tests verify the Redis token blacklist functionality
5
+ // The @fastify/jwt integration is tested separately when Fastify 5.x is available
6
+
7
+ describe('Auth Service - Redis Token Blacklist Integration', () => {
8
+ let redis: Redis;
9
+ const testRedisUrl = process.env.REDIS_URL || 'redis://localhost:6379';
10
+
11
+ beforeAll(async () => {
12
+ // Setup Redis client for verification
13
+ redis = new Redis(testRedisUrl);
14
+
15
+ // Wait for Redis connection
16
+ await new Promise<void>((resolve, reject) => {
17
+ const timeout = setTimeout(() => reject(new Error('Redis connection timeout')), 5000);
18
+ redis.once('ready', () => {
19
+ clearTimeout(timeout);
20
+ resolve();
21
+ });
22
+ redis.once('error', (err) => {
23
+ clearTimeout(timeout);
24
+ reject(err);
25
+ });
26
+ });
27
+ });
28
+
29
+ afterAll(async () => {
30
+ if (redis) {
31
+ await redis.quit();
32
+ }
33
+ });
34
+
35
+ beforeEach(async () => {
36
+ // Clean up test keys before each test
37
+ const keys = await redis.keys('auth:blacklist:*');
38
+ if (keys.length > 0) {
39
+ await redis.del(...keys);
40
+ }
41
+ });
42
+
43
+ describe('Redis Connection', () => {
44
+ it('should connect to Redis', async () => {
45
+ const pong = await redis.ping();
46
+ expect(pong).toBe('PONG');
47
+ });
48
+
49
+ it('should set and get values', async () => {
50
+ await redis.set('test:key', 'test-value');
51
+ const value = await redis.get('test:key');
52
+ expect(value).toBe('test-value');
53
+ await redis.del('test:key');
54
+ });
55
+ });
56
+
57
+ describe('Token Blacklist Operations', () => {
58
+ it('should blacklist a token', async () => {
59
+ const token = 'test-token-123';
60
+ const ttl = 3600; // 1 hour
61
+
62
+ await redis.setex(`auth:blacklist:${token}`, ttl, '1');
63
+
64
+ const exists = await redis.exists(`auth:blacklist:${token}`);
65
+ expect(exists).toBe(1);
66
+ });
67
+
68
+ it('should check if token is blacklisted', async () => {
69
+ const token = 'blacklisted-token';
70
+ await redis.setex(`auth:blacklist:${token}`, 3600, '1');
71
+
72
+ const isBlacklisted = (await redis.exists(`auth:blacklist:${token}`)) === 1;
73
+ expect(isBlacklisted).toBe(true);
74
+ });
75
+
76
+ it('should return false for non-blacklisted token', async () => {
77
+ const token = 'valid-token';
78
+
79
+ const isBlacklisted = (await redis.exists(`auth:blacklist:${token}`)) === 1;
80
+ expect(isBlacklisted).toBe(false);
81
+ });
82
+
83
+ it('should expire blacklisted tokens', async () => {
84
+ const token = 'expiring-token';
85
+ await redis.setex(`auth:blacklist:${token}`, 1, '1'); // 1 second TTL
86
+
87
+ // Wait for expiration
88
+ await new Promise((resolve) => setTimeout(resolve, 1500));
89
+
90
+ const exists = await redis.exists(`auth:blacklist:${token}`);
91
+ expect(exists).toBe(0);
92
+ });
93
+ });
94
+ });
@@ -0,0 +1,387 @@
1
+ import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
2
+ import { CacheService } from '../../src/modules/cache/cache.service.js';
3
+
4
+ describe('CacheService - Redis Integration', () => {
5
+ let redisCache: CacheService;
6
+ let memoryCache: CacheService;
7
+
8
+ beforeAll(() => {
9
+ // Redis cache instance
10
+ redisCache = new CacheService({
11
+ provider: 'redis',
12
+ ttl: 60, // 1 minute for tests
13
+ prefix: 'test:',
14
+ redis: {
15
+ host: process.env.REDIS_HOST || 'localhost',
16
+ port: parseInt(process.env.REDIS_PORT || '6379'),
17
+ password: process.env.REDIS_PASSWORD,
18
+ db: 1, // Use DB 1 for tests
19
+ },
20
+ });
21
+
22
+ // Memory cache instance for comparison
23
+ memoryCache = new CacheService({
24
+ provider: 'memory',
25
+ ttl: 60,
26
+ prefix: 'test:',
27
+ });
28
+ });
29
+
30
+ afterAll(async () => {
31
+ await redisCache.close();
32
+ });
33
+
34
+ beforeEach(async () => {
35
+ // Clear cache before each test
36
+ await redisCache.clear();
37
+ await memoryCache.clear();
38
+ });
39
+
40
+ // ==========================================
41
+ // BASIC OPERATIONS
42
+ // ==========================================
43
+
44
+ describe('Basic Operations', () => {
45
+ it('should set and get a string value', async () => {
46
+ await redisCache.set('test-string', 'Hello Redis!');
47
+ const value = await redisCache.get<string>('test-string');
48
+ expect(value).toBe('Hello Redis!');
49
+ });
50
+
51
+ it('should set and get a number value', async () => {
52
+ await redisCache.set('test-number', 42);
53
+ const value = await redisCache.get<number>('test-number');
54
+ expect(value).toBe(42);
55
+ });
56
+
57
+ it('should set and get an object value', async () => {
58
+ const obj = { name: 'John', age: 30, active: true };
59
+ await redisCache.set('test-object', obj);
60
+ const value = await redisCache.get<typeof obj>('test-object');
61
+ expect(value).toEqual(obj);
62
+ });
63
+
64
+ it('should set and get an array value', async () => {
65
+ const arr = [1, 2, 3, 4, 5];
66
+ await redisCache.set('test-array', arr);
67
+ const value = await redisCache.get<number[]>('test-array');
68
+ expect(value).toEqual(arr);
69
+ });
70
+
71
+ it('should return null for non-existent key', async () => {
72
+ const value = await redisCache.get('non-existent-key');
73
+ expect(value).toBeNull();
74
+ });
75
+
76
+ it('should delete a key', async () => {
77
+ await redisCache.set('test-delete', 'value');
78
+ const deleted = await redisCache.delete('test-delete');
79
+ expect(deleted).toBe(true);
80
+
81
+ const value = await redisCache.get('test-delete');
82
+ expect(value).toBeNull();
83
+ });
84
+
85
+ it('should return false when deleting non-existent key', async () => {
86
+ const deleted = await redisCache.delete('non-existent');
87
+ expect(deleted).toBe(false);
88
+ });
89
+
90
+ it('should check if key exists', async () => {
91
+ await redisCache.set('test-exists', 'value');
92
+ const exists = await redisCache.exists('test-exists');
93
+ expect(exists).toBe(true);
94
+
95
+ const notExists = await redisCache.exists('not-exists');
96
+ expect(notExists).toBe(false);
97
+ });
98
+ });
99
+
100
+ // ==========================================
101
+ // TTL AND EXPIRATION
102
+ // ==========================================
103
+
104
+ describe('TTL and Expiration', () => {
105
+ it('should expire key after TTL', async () => {
106
+ await redisCache.set('test-ttl', 'expires soon', { ttl: 1 }); // 1 second
107
+
108
+ const valueBefore = await redisCache.get('test-ttl');
109
+ expect(valueBefore).toBe('expires soon');
110
+
111
+ // Wait for expiration
112
+ await new Promise((resolve) => setTimeout(resolve, 1500));
113
+
114
+ const valueAfter = await redisCache.get('test-ttl');
115
+ expect(valueAfter).toBeNull();
116
+ });
117
+
118
+ it('should use custom TTL per key', async () => {
119
+ await redisCache.set('short-ttl', 'short', { ttl: 1 });
120
+ await redisCache.set('long-ttl', 'long', { ttl: 60 });
121
+
122
+ await new Promise((resolve) => setTimeout(resolve, 1500));
123
+
124
+ const shortValue = await redisCache.get('short-ttl');
125
+ const longValue = await redisCache.get('long-ttl');
126
+
127
+ expect(shortValue).toBeNull();
128
+ expect(longValue).toBe('long');
129
+ });
130
+
131
+ it('should use default TTL when not specified', async () => {
132
+ await redisCache.set('default-ttl', 'uses default');
133
+ const value = await redisCache.get('default-ttl');
134
+ expect(value).toBe('uses default');
135
+ });
136
+ });
137
+
138
+ // ==========================================
139
+ // ADVANCED OPERATIONS
140
+ // ==========================================
141
+
142
+ describe('Advanced Operations', () => {
143
+ it('should implement getOrSet pattern', async () => {
144
+ let factoryCalled = 0;
145
+ const factory = async () => {
146
+ factoryCalled++;
147
+ return 'computed value';
148
+ };
149
+
150
+ // First call - should execute factory
151
+ const value1 = await redisCache.getOrSet('getOrSet-key', factory);
152
+ expect(value1).toBe('computed value');
153
+ expect(factoryCalled).toBe(1);
154
+
155
+ // Second call - should use cache
156
+ const value2 = await redisCache.getOrSet('getOrSet-key', factory);
157
+ expect(value2).toBe('computed value');
158
+ expect(factoryCalled).toBe(1); // Factory not called again
159
+ });
160
+
161
+ it('should get multiple keys (mget)', async () => {
162
+ await redisCache.set('mget-1', 'value1');
163
+ await redisCache.set('mget-2', 'value2');
164
+ await redisCache.set('mget-3', 'value3');
165
+
166
+ const values = await redisCache.mget<string>(['mget-1', 'mget-2', 'mget-3', 'mget-missing']);
167
+ expect(values).toEqual(['value1', 'value2', 'value3', null]);
168
+ });
169
+
170
+ it('should set multiple keys (mset)', async () => {
171
+ await redisCache.mset([
172
+ { key: 'mset-1', value: 'val1' },
173
+ { key: 'mset-2', value: 'val2' },
174
+ { key: 'mset-3', value: 'val3', ttl: 30 },
175
+ ]);
176
+
177
+ const value1 = await redisCache.get('mset-1');
178
+ const value2 = await redisCache.get('mset-2');
179
+ const value3 = await redisCache.get('mset-3');
180
+
181
+ expect(value1).toBe('val1');
182
+ expect(value2).toBe('val2');
183
+ expect(value3).toBe('val3');
184
+ });
185
+
186
+ it('should increment a counter', async () => {
187
+ await redisCache.set('counter', 10);
188
+
189
+ const val1 = await redisCache.increment('counter');
190
+ expect(val1).toBe(11);
191
+
192
+ const val2 = await redisCache.increment('counter', 5);
193
+ expect(val2).toBe(16);
194
+ });
195
+
196
+ it('should increment from 0 if key does not exist', async () => {
197
+ const val = await redisCache.increment('new-counter');
198
+ expect(val).toBe(1);
199
+ });
200
+
201
+ it('should decrement a counter', async () => {
202
+ await redisCache.set('decrement-counter', 20);
203
+
204
+ const val1 = await redisCache.decrement('decrement-counter');
205
+ expect(val1).toBe(19);
206
+
207
+ const val2 = await redisCache.decrement('decrement-counter', 5);
208
+ expect(val2).toBe(14);
209
+ });
210
+
211
+ it('should clear all keys with prefix', async () => {
212
+ await redisCache.set('clear-1', 'val1');
213
+ await redisCache.set('clear-2', 'val2');
214
+ await redisCache.set('clear-3', 'val3');
215
+
216
+ await redisCache.clear();
217
+
218
+ const val1 = await redisCache.get('clear-1');
219
+ const val2 = await redisCache.get('clear-2');
220
+ const val3 = await redisCache.get('clear-3');
221
+
222
+ expect(val1).toBeNull();
223
+ expect(val2).toBeNull();
224
+ expect(val3).toBeNull();
225
+ });
226
+ });
227
+
228
+ // ==========================================
229
+ // STATISTICS
230
+ // ==========================================
231
+
232
+ describe('Statistics', () => {
233
+ it('should track cache hits and misses', async () => {
234
+ const initialStats = redisCache.getStats();
235
+ const initialHits = initialStats.hits;
236
+ const initialMisses = initialStats.misses;
237
+
238
+ await redisCache.set('stats-key', 'value');
239
+
240
+ // Hit
241
+ await redisCache.get('stats-key');
242
+
243
+ // Miss
244
+ await redisCache.get('non-existent');
245
+
246
+ const finalStats = redisCache.getStats();
247
+
248
+ expect(finalStats.hits).toBeGreaterThan(initialHits);
249
+ expect(finalStats.misses).toBeGreaterThan(initialMisses);
250
+ });
251
+ });
252
+
253
+ // ==========================================
254
+ // COMPARISON WITH MEMORY CACHE
255
+ // ==========================================
256
+
257
+ describe('Comparison with Memory Cache', () => {
258
+ it('should behave identically to memory cache for basic operations', async () => {
259
+ const testData = { name: 'Test', value: 123, active: true };
260
+
261
+ // Set in both
262
+ await redisCache.set('comparison-key', testData);
263
+ await memoryCache.set('comparison-key', testData);
264
+
265
+ // Get from both
266
+ const redisValue = await redisCache.get<typeof testData>('comparison-key');
267
+ const memoryValue = await memoryCache.get<typeof testData>('comparison-key');
268
+
269
+ expect(redisValue).toEqual(memoryValue);
270
+ expect(redisValue).toEqual(testData);
271
+ });
272
+
273
+ it('should handle increment identically', async () => {
274
+ await redisCache.set('redis-counter', 5);
275
+ await memoryCache.set('memory-counter', 5);
276
+
277
+ const redisVal = await redisCache.increment('redis-counter', 3);
278
+ const memoryVal = await memoryCache.increment('memory-counter', 3);
279
+
280
+ expect(redisVal).toBe(memoryVal);
281
+ expect(redisVal).toBe(8);
282
+ });
283
+ });
284
+
285
+ // ==========================================
286
+ // ERROR HANDLING
287
+ // ==========================================
288
+
289
+ describe('Error Handling', () => {
290
+ it('should handle invalid JSON gracefully', async () => {
291
+ // This test verifies error handling
292
+ // Set a value and then try to get it
293
+ await redisCache.set('json-test', { valid: 'json' });
294
+ const value = await redisCache.get('json-test');
295
+ expect(value).toEqual({ valid: 'json' });
296
+ });
297
+
298
+ it('should handle concurrent operations', async () => {
299
+ const operations = [];
300
+ for (let i = 0; i < 10; i++) {
301
+ operations.push(redisCache.set(`concurrent-${i}`, `value-${i}`));
302
+ }
303
+
304
+ await Promise.all(operations);
305
+
306
+ const value5 = await redisCache.get('concurrent-5');
307
+ expect(value5).toBe('value-5');
308
+ });
309
+ });
310
+
311
+ // ==========================================
312
+ // PREFIX HANDLING
313
+ // ==========================================
314
+
315
+ describe('Prefix Handling', () => {
316
+ it('should apply prefix to all keys', async () => {
317
+ await redisCache.set('prefixed-key', 'value');
318
+
319
+ // The actual key in Redis should be "test:prefixed-key"
320
+ // We can't directly verify this without accessing Redis client
321
+ // But we can verify that get/delete work correctly
322
+
323
+ const value = await redisCache.get('prefixed-key');
324
+ expect(value).toBe('value');
325
+
326
+ const deleted = await redisCache.delete('prefixed-key');
327
+ expect(deleted).toBe(true);
328
+ });
329
+ });
330
+
331
+ // ==========================================
332
+ // COMPLEX DATA TYPES
333
+ // ==========================================
334
+
335
+ describe('Complex Data Types', () => {
336
+ it('should handle nested objects', async () => {
337
+ const complex = {
338
+ user: {
339
+ name: 'John',
340
+ profile: {
341
+ age: 30,
342
+ settings: {
343
+ theme: 'dark',
344
+ notifications: true,
345
+ },
346
+ },
347
+ },
348
+ metadata: {
349
+ created: new Date().toISOString(),
350
+ tags: ['admin', 'premium'],
351
+ },
352
+ };
353
+
354
+ await redisCache.set('complex-object', complex);
355
+ const value = await redisCache.get<typeof complex>('complex-object');
356
+
357
+ expect(value).toEqual(complex);
358
+ expect(value?.user.profile.settings.theme).toBe('dark');
359
+ });
360
+
361
+ it('should handle arrays of objects', async () => {
362
+ const users = [
363
+ { id: 1, name: 'Alice' },
364
+ { id: 2, name: 'Bob' },
365
+ { id: 3, name: 'Charlie' },
366
+ ];
367
+
368
+ await redisCache.set('users-array', users);
369
+ const value = await redisCache.get<typeof users>('users-array');
370
+
371
+ expect(value).toEqual(users);
372
+ expect(value?.length).toBe(3);
373
+ });
374
+ });
375
+
376
+ // ==========================================
377
+ // CONNECTION RESILIENCE
378
+ // ==========================================
379
+
380
+ describe('Connection Resilience', () => {
381
+ it('should handle operations when Redis is available', async () => {
382
+ await redisCache.set('resilience-test', 'working');
383
+ const value = await redisCache.get('resilience-test');
384
+ expect(value).toBe('working');
385
+ });
386
+ });
387
+ });