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,340 @@
1
+ /**
2
+ * Session Service
3
+ * Redis-based session management with optional Prisma persistence
4
+ */
5
+ import { randomUUID } from 'crypto';
6
+ import type { Redis } from 'ioredis';
7
+ import { getRedis } from '../../database/redis.js';
8
+ import { prisma } from '../../database/prisma.js';
9
+ import { logger } from '../../core/logger.js';
10
+ import type { Session, CreateSessionData, SessionConfig, SessionStats } from './types.js';
11
+ import { SessionRepository } from './session.repository.js';
12
+
13
+ const DEFAULT_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
14
+ const DEFAULT_PREFIX = 'session:';
15
+
16
+ export class SessionService {
17
+ private redis: Redis;
18
+ private repository: SessionRepository;
19
+ private config: Required<SessionConfig>;
20
+
21
+ constructor(config: SessionConfig = {}) {
22
+ this.redis = getRedis();
23
+ this.repository = new SessionRepository(prisma);
24
+ this.config = {
25
+ ttlMs: config.ttlMs ?? DEFAULT_TTL_MS,
26
+ prefix: config.prefix ?? DEFAULT_PREFIX,
27
+ persistToDb: config.persistToDb ?? false,
28
+ slidingExpiration: config.slidingExpiration ?? true,
29
+ };
30
+
31
+ logger.info({ config: this.config }, 'Session service initialized');
32
+ }
33
+
34
+ /**
35
+ * Create a new session
36
+ */
37
+ async create(data: CreateSessionData): Promise<Session> {
38
+ const id = randomUUID();
39
+ const now = new Date();
40
+ const ttlMs = data.expiresInMs ?? this.config.ttlMs;
41
+ const expiresAt = new Date(now.getTime() + ttlMs);
42
+
43
+ const session: Session = {
44
+ id,
45
+ userId: data.userId,
46
+ userAgent: data.userAgent,
47
+ ipAddress: data.ipAddress,
48
+ data: data.data,
49
+ expiresAt,
50
+ createdAt: now,
51
+ lastAccessedAt: now,
52
+ };
53
+
54
+ // Store in Redis
55
+ const redisKey = this.buildKey(id);
56
+ const ttlSeconds = Math.ceil(ttlMs / 1000);
57
+ await this.redis.setex(redisKey, ttlSeconds, JSON.stringify(session));
58
+
59
+ // Optionally persist to database
60
+ if (this.config.persistToDb) {
61
+ await this.repository.create({
62
+ id,
63
+ userId: data.userId,
64
+ userAgent: data.userAgent,
65
+ ipAddress: data.ipAddress,
66
+ expiresAt,
67
+ });
68
+ }
69
+
70
+ logger.debug({ sessionId: id, userId: data.userId }, 'Session created');
71
+ return session;
72
+ }
73
+
74
+ /**
75
+ * Get session by ID
76
+ */
77
+ async get(sessionId: string): Promise<Session | null> {
78
+ const redisKey = this.buildKey(sessionId);
79
+ const data = await this.redis.get(redisKey);
80
+
81
+ if (!data) {
82
+ // Try database fallback if persistence is enabled
83
+ if (this.config.persistToDb) {
84
+ const dbSession = await this.repository.findById(sessionId);
85
+ if (dbSession) {
86
+ // Restore to Redis
87
+ const ttlMs = dbSession.expiresAt.getTime() - Date.now();
88
+ if (ttlMs > 0) {
89
+ await this.redis.setex(redisKey, Math.ceil(ttlMs / 1000), JSON.stringify(dbSession));
90
+ return dbSession;
91
+ }
92
+ }
93
+ }
94
+ return null;
95
+ }
96
+
97
+ const session = JSON.parse(data) as Session;
98
+
99
+ // Check if expired
100
+ if (new Date(session.expiresAt) < new Date()) {
101
+ await this.destroy(sessionId);
102
+ return null;
103
+ }
104
+
105
+ // Sliding expiration: refresh TTL on access
106
+ if (this.config.slidingExpiration) {
107
+ await this.touch(sessionId);
108
+ }
109
+
110
+ return session;
111
+ }
112
+
113
+ /**
114
+ * Validate session and return user ID
115
+ */
116
+ async validate(sessionId: string): Promise<string | null> {
117
+ const session = await this.get(sessionId);
118
+ return session?.userId ?? null;
119
+ }
120
+
121
+ /**
122
+ * Update session data
123
+ */
124
+ async update(sessionId: string, data: Partial<Session['data']>): Promise<Session | null> {
125
+ const session = await this.get(sessionId);
126
+ if (!session) return null;
127
+
128
+ session.data = { ...session.data, ...data };
129
+ session.lastAccessedAt = new Date();
130
+
131
+ const redisKey = this.buildKey(sessionId);
132
+ const ttlMs = session.expiresAt.getTime() - Date.now();
133
+
134
+ if (ttlMs > 0) {
135
+ await this.redis.setex(redisKey, Math.ceil(ttlMs / 1000), JSON.stringify(session));
136
+ }
137
+
138
+ return session;
139
+ }
140
+
141
+ /**
142
+ * Touch session (refresh TTL)
143
+ */
144
+ async touch(sessionId: string): Promise<boolean> {
145
+ const session = await this.getWithoutTouch(sessionId);
146
+ if (!session) return false;
147
+
148
+ const now = new Date();
149
+ session.lastAccessedAt = now;
150
+ session.expiresAt = new Date(now.getTime() + this.config.ttlMs);
151
+
152
+ const redisKey = this.buildKey(sessionId);
153
+ const ttlSeconds = Math.ceil(this.config.ttlMs / 1000);
154
+ await this.redis.setex(redisKey, ttlSeconds, JSON.stringify(session));
155
+
156
+ // Update database if persistence is enabled
157
+ if (this.config.persistToDb) {
158
+ await this.repository.updateExpiration(sessionId, session.expiresAt);
159
+ }
160
+
161
+ return true;
162
+ }
163
+
164
+ /**
165
+ * Destroy session
166
+ */
167
+ async destroy(sessionId: string): Promise<boolean> {
168
+ const redisKey = this.buildKey(sessionId);
169
+ const deleted = await this.redis.del(redisKey);
170
+
171
+ // Delete from database if persistence is enabled
172
+ if (this.config.persistToDb) {
173
+ await this.repository.delete(sessionId);
174
+ }
175
+
176
+ if (deleted > 0) {
177
+ logger.debug({ sessionId }, 'Session destroyed');
178
+ }
179
+
180
+ return deleted > 0;
181
+ }
182
+
183
+ /**
184
+ * Destroy all sessions for a user
185
+ */
186
+ async destroyUserSessions(userId: string): Promise<number> {
187
+ // Get all sessions for user from database
188
+ const sessions = await this.repository.findByUserId(userId);
189
+ let count = 0;
190
+
191
+ // Delete from Redis
192
+ for (const session of sessions) {
193
+ const redisKey = this.buildKey(session.id);
194
+ const deleted = await this.redis.del(redisKey);
195
+ if (deleted > 0) count++;
196
+ }
197
+
198
+ // Delete from database
199
+ if (this.config.persistToDb) {
200
+ await this.repository.deleteByUserId(userId);
201
+ }
202
+
203
+ // Also scan Redis for any sessions not in database
204
+ const pattern = `${this.config.prefix}*`;
205
+ const keys = await this.redis.keys(pattern);
206
+ for (const key of keys) {
207
+ const data = await this.redis.get(key);
208
+ if (data) {
209
+ const session = JSON.parse(data) as Session;
210
+ if (session.userId === userId) {
211
+ await this.redis.del(key);
212
+ count++;
213
+ }
214
+ }
215
+ }
216
+
217
+ logger.info({ userId, count }, 'User sessions destroyed');
218
+ return count;
219
+ }
220
+
221
+ /**
222
+ * Get all sessions for a user
223
+ */
224
+ async getUserSessions(userId: string): Promise<Session[]> {
225
+ const sessions: Session[] = [];
226
+
227
+ // Scan Redis for user sessions
228
+ const pattern = `${this.config.prefix}*`;
229
+ const keys = await this.redis.keys(pattern);
230
+
231
+ for (const key of keys) {
232
+ const data = await this.redis.get(key);
233
+ if (data) {
234
+ const session = JSON.parse(data) as Session;
235
+ if (session.userId === userId && new Date(session.expiresAt) > new Date()) {
236
+ sessions.push(session);
237
+ }
238
+ }
239
+ }
240
+
241
+ return sessions.sort(
242
+ (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
243
+ );
244
+ }
245
+
246
+ /**
247
+ * Get session stats
248
+ */
249
+ async getStats(): Promise<SessionStats> {
250
+ const pattern = `${this.config.prefix}*`;
251
+ const keys = await this.redis.keys(pattern);
252
+ const userSessions = new Map<string, number>();
253
+ let activeSessions = 0;
254
+
255
+ for (const key of keys) {
256
+ const data = await this.redis.get(key);
257
+ if (data) {
258
+ const session = JSON.parse(data) as Session;
259
+ if (new Date(session.expiresAt) > new Date()) {
260
+ activeSessions++;
261
+ const count = userSessions.get(session.userId) || 0;
262
+ userSessions.set(session.userId, count + 1);
263
+ }
264
+ }
265
+ }
266
+
267
+ return { activeSessions, userSessions };
268
+ }
269
+
270
+ /**
271
+ * Cleanup expired sessions
272
+ */
273
+ async cleanup(): Promise<number> {
274
+ let count = 0;
275
+
276
+ // Redis handles TTL automatically, but clean database
277
+ if (this.config.persistToDb) {
278
+ count = await this.repository.deleteExpired();
279
+ if (count > 0) {
280
+ logger.info({ count }, 'Cleaned up expired sessions from database');
281
+ }
282
+ }
283
+
284
+ return count;
285
+ }
286
+
287
+ /**
288
+ * Clear all sessions (for testing)
289
+ */
290
+ async clear(): Promise<void> {
291
+ const pattern = `${this.config.prefix}*`;
292
+ const keys = await this.redis.keys(pattern);
293
+ if (keys.length > 0) {
294
+ await this.redis.del(...keys);
295
+ }
296
+
297
+ if (this.config.persistToDb) {
298
+ await this.repository.clear();
299
+ }
300
+ }
301
+
302
+ /**
303
+ * Build Redis key for session
304
+ */
305
+ private buildKey(sessionId: string): string {
306
+ return `${this.config.prefix}${sessionId}`;
307
+ }
308
+
309
+ /**
310
+ * Get session without triggering sliding expiration
311
+ */
312
+ private async getWithoutTouch(sessionId: string): Promise<Session | null> {
313
+ const redisKey = this.buildKey(sessionId);
314
+ const data = await this.redis.get(redisKey);
315
+
316
+ if (!data) return null;
317
+
318
+ const session = JSON.parse(data) as Session;
319
+
320
+ if (new Date(session.expiresAt) < new Date()) {
321
+ return null;
322
+ }
323
+
324
+ return session;
325
+ }
326
+ }
327
+
328
+ let sessionService: SessionService | null = null;
329
+
330
+ export function getSessionService(): SessionService {
331
+ if (!sessionService) {
332
+ sessionService = new SessionService();
333
+ }
334
+ return sessionService;
335
+ }
336
+
337
+ export function createSessionService(config?: SessionConfig): SessionService {
338
+ sessionService = new SessionService(config);
339
+ return sessionService;
340
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Session Types
3
+ */
4
+
5
+ export interface Session {
6
+ id: string;
7
+ userId: string;
8
+ userAgent?: string;
9
+ ipAddress?: string;
10
+ data?: Record<string, unknown>;
11
+ expiresAt: Date;
12
+ createdAt: Date;
13
+ lastAccessedAt?: Date;
14
+ }
15
+
16
+ export interface CreateSessionData {
17
+ userId: string;
18
+ userAgent?: string;
19
+ ipAddress?: string;
20
+ data?: Record<string, unknown>;
21
+ expiresInMs?: number;
22
+ }
23
+
24
+ export interface SessionConfig {
25
+ /** Session TTL in milliseconds (default: 24 hours) */
26
+ ttlMs?: number;
27
+ /** Redis key prefix (default: 'session:') */
28
+ prefix?: string;
29
+ /** Whether to persist sessions to database (default: false) */
30
+ persistToDb?: boolean;
31
+ /** Sliding expiration - refresh TTL on access (default: true) */
32
+ slidingExpiration?: boolean;
33
+ }
34
+
35
+ export interface SessionStats {
36
+ activeSessions: number;
37
+ userSessions: Map<string, number>;
38
+ }
@@ -1,3 +1,9 @@
1
1
  export { registerSwagger, commonResponses, paginationQuery, idParam } from './swagger.service.js';
2
2
  export { buildOpenApiSchema, generateRouteSchema } from './schema-builder.js';
3
- export type { SwaggerConfig, SwaggerTag, SwaggerServer, RouteSchema, EndpointDoc } from './types.js';
3
+ export type {
4
+ SwaggerConfig,
5
+ SwaggerTag,
6
+ SwaggerServer,
7
+ RouteSchema,
8
+ EndpointDoc,
9
+ } from './types.js';
@@ -24,8 +24,16 @@ export function buildOpenApiSchema(
24
24
  }
25
25
 
26
26
  if (includeTimestamps) {
27
- properties.createdAt = { type: 'string', format: 'date-time', description: 'Creation timestamp' };
28
- properties.updatedAt = { type: 'string', format: 'date-time', description: 'Last update timestamp' };
27
+ properties.createdAt = {
28
+ type: 'string',
29
+ format: 'date-time',
30
+ description: 'Creation timestamp',
31
+ };
32
+ properties.updatedAt = {
33
+ type: 'string',
34
+ format: 'date-time',
35
+ description: 'Last update timestamp',
36
+ };
29
37
  }
30
38
 
31
39
  return {
@@ -106,7 +114,10 @@ export function generateRouteSchema(
106
114
  properties: {
107
115
  data: {
108
116
  type: 'array',
109
- items: buildOpenApiSchema(fields, { includeId: true, includeTimestamps: true }),
117
+ items: buildOpenApiSchema(fields, {
118
+ includeId: true,
119
+ includeTimestamps: true,
120
+ }),
110
121
  },
111
122
  meta: {
112
123
  type: 'object',
@@ -190,7 +201,7 @@ export function generateRouteSchema(
190
201
  },
191
202
  };
192
203
 
193
- case 'update':
204
+ case 'update': {
194
205
  // Make all fields optional for update
195
206
  const optionalFields = fields.map((f) => ({ ...f, isOptional: true }));
196
207
  return {
@@ -226,6 +237,7 @@ export function generateRouteSchema(
226
237
  },
227
238
  },
228
239
  };
240
+ }
229
241
 
230
242
  case 'delete':
231
243
  return {
@@ -6,8 +6,6 @@ import { logger } from '../../core/logger.js';
6
6
  import type { SwaggerConfig } from './types.js';
7
7
 
8
8
  const defaultConfig: SwaggerConfig = {
9
- enabled: true,
10
- route: '/docs',
11
9
  title: 'Servcraft API',
12
10
  description: 'API documentation generated by Servcraft',
13
11
  version: '1.0.0',
@@ -24,11 +22,6 @@ export async function registerSwagger(
24
22
  ): Promise<void> {
25
23
  const swaggerConfig = { ...defaultConfig, ...customConfig };
26
24
 
27
- if (swaggerConfig.enabled === false) {
28
- logger.info('Swagger documentation disabled');
29
- return;
30
- }
31
-
32
25
  await app.register(swagger, {
33
26
  openapi: {
34
27
  openapi: '3.0.3',
@@ -60,7 +53,7 @@ export async function registerSwagger(
60
53
  });
61
54
 
62
55
  await app.register(swaggerUi, {
63
- routePrefix: swaggerConfig.route || '/docs',
56
+ routePrefix: '/docs',
64
57
  uiConfig: {
65
58
  docExpansion: 'list',
66
59
  deepLinking: true,
@@ -77,7 +70,7 @@ export async function registerSwagger(
77
70
  }
78
71
 
79
72
  // Helper to generate schema from Zod
80
- export function zodToJsonSchema(zodSchema: unknown): Record<string, unknown> {
73
+ export function zodToJsonSchema(_zodSchema: unknown): Record<string, unknown> {
81
74
  // This is a simplified version - for full support use zod-to-json-schema package
82
75
  return {
83
76
  type: 'object',
@@ -152,7 +145,13 @@ export const paginationQuery = {
152
145
  type: 'object',
153
146
  properties: {
154
147
  page: { type: 'integer', minimum: 1, default: 1, description: 'Page number' },
155
- limit: { type: 'integer', minimum: 1, maximum: 100, default: 20, description: 'Items per page' },
148
+ limit: {
149
+ type: 'integer',
150
+ minimum: 1,
151
+ maximum: 100,
152
+ default: 20,
153
+ description: 'Items per page',
154
+ },
156
155
  sortBy: { type: 'string', description: 'Field to sort by' },
157
156
  sortOrder: { type: 'string', enum: ['asc', 'desc'], default: 'asc', description: 'Sort order' },
158
157
  search: { type: 'string', description: 'Search query' },
@@ -1,6 +1,4 @@
1
1
  export interface SwaggerConfig {
2
- enabled?: boolean;
3
- route?: string;
4
2
  title: string;
5
3
  description: string;
6
4
  version: string;
@@ -0,0 +1,14 @@
1
+ export { UploadService, getUploadService, createUploadService } from './upload.service.js';
2
+ export { registerUploadRoutes } from './upload.routes.js';
3
+ export type {
4
+ UploadedFile,
5
+ UploadConfig,
6
+ UploadOptions,
7
+ MultipartFile,
8
+ StorageProvider,
9
+ ImageTransformOptions,
10
+ LocalStorageConfig,
11
+ S3Config,
12
+ CloudinaryConfig,
13
+ GCSConfig,
14
+ } from './types.js';
@@ -0,0 +1,83 @@
1
+ export type StorageProvider = 'local' | 's3' | 'cloudinary' | 'gcs';
2
+
3
+ export interface UploadedFile {
4
+ id: string;
5
+ originalName: string;
6
+ filename: string;
7
+ mimetype: string;
8
+ size: number;
9
+ path: string;
10
+ url: string;
11
+ provider: StorageProvider;
12
+ bucket?: string;
13
+ metadata?: Record<string, unknown>;
14
+ userId?: string;
15
+ createdAt: Date;
16
+ }
17
+
18
+ export interface UploadConfig {
19
+ provider: StorageProvider;
20
+ maxFileSize: number; // in bytes
21
+ allowedMimeTypes: string[];
22
+ local?: LocalStorageConfig;
23
+ s3?: S3Config;
24
+ cloudinary?: CloudinaryConfig;
25
+ gcs?: GCSConfig;
26
+ }
27
+
28
+ export interface LocalStorageConfig {
29
+ uploadDir: string;
30
+ publicPath: string;
31
+ }
32
+
33
+ export interface S3Config {
34
+ accessKeyId: string;
35
+ secretAccessKey: string;
36
+ region: string;
37
+ bucket: string;
38
+ endpoint?: string; // For S3-compatible services
39
+ acl?: 'private' | 'public-read' | 'public-read-write';
40
+ }
41
+
42
+ export interface CloudinaryConfig {
43
+ cloudName: string;
44
+ apiKey: string;
45
+ apiSecret: string;
46
+ folder?: string;
47
+ }
48
+
49
+ export interface GCSConfig {
50
+ projectId: string;
51
+ keyFilename?: string;
52
+ credentials?: {
53
+ client_email: string;
54
+ private_key: string;
55
+ };
56
+ bucket: string;
57
+ }
58
+
59
+ export interface ImageTransformOptions {
60
+ width?: number;
61
+ height?: number;
62
+ fit?: 'cover' | 'contain' | 'fill' | 'inside' | 'outside';
63
+ format?: 'jpeg' | 'png' | 'webp' | 'avif';
64
+ quality?: number;
65
+ blur?: number;
66
+ grayscale?: boolean;
67
+ }
68
+
69
+ export interface UploadOptions {
70
+ filename?: string;
71
+ folder?: string;
72
+ isPublic?: boolean;
73
+ metadata?: Record<string, unknown>;
74
+ transform?: ImageTransformOptions;
75
+ }
76
+
77
+ export interface MultipartFile {
78
+ filename: string;
79
+ mimetype: string;
80
+ encoding: string;
81
+ file: NodeJS.ReadableStream;
82
+ toBuffer(): Promise<Buffer>;
83
+ }