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,120 @@
1
+ import type { Request, Response, NextFunction } from 'express';
2
+ import type { VersioningService } from './versioning.service.js';
3
+ import type { VersionMiddlewareOptions } from './types.js';
4
+
5
+ /**
6
+ * Request with version info
7
+ */
8
+ export interface VersionedRequest extends Request {
9
+ apiVersion?: string;
10
+ versionInfo?: {
11
+ version: string;
12
+ source: string;
13
+ isValid: boolean;
14
+ warning?: string;
15
+ };
16
+ }
17
+
18
+ /**
19
+ * Create versioning middleware
20
+ */
21
+ export function createVersioningMiddleware(
22
+ versioningService: VersioningService,
23
+ options: VersionMiddlewareOptions = {}
24
+ ): (req: Request, res: Response, next: NextFunction) => void {
25
+ return (req: Request, res: Response, next: NextFunction): void => {
26
+ const versionedReq = req as VersionedRequest;
27
+
28
+ // Detect version
29
+ const detection = versioningService.detectVersion({
30
+ url: req.url,
31
+ headers: req.headers as Record<string, string | string[] | undefined>,
32
+ query: req.query as Record<string, string | string[] | undefined>,
33
+ });
34
+
35
+ versionedReq.apiVersion = detection.version;
36
+ versionedReq.versionInfo = detection;
37
+
38
+ // Set response headers
39
+ res.setHeader('X-API-Version', detection.version);
40
+
41
+ if (detection.warning) {
42
+ res.setHeader('X-API-Deprecation-Warning', detection.warning);
43
+ }
44
+
45
+ // Validation
46
+ if (!detection.isValid) {
47
+ res.status(400).json({
48
+ error: 'Invalid API version',
49
+ version: detection.version,
50
+ availableVersions: versioningService.getActiveVersions().map((v) => v.version),
51
+ });
52
+ return;
53
+ }
54
+
55
+ // Check version requirements
56
+ if (options.requiredVersion && detection.version !== options.requiredVersion) {
57
+ res.status(400).json({
58
+ error: `This endpoint requires API version ${options.requiredVersion}`,
59
+ currentVersion: detection.version,
60
+ });
61
+ return;
62
+ }
63
+
64
+ if (options.minVersion) {
65
+ if (versioningService.compareVersions(detection.version, options.minVersion) < 0) {
66
+ res.status(400).json({
67
+ error: `This endpoint requires at least API version ${options.minVersion}`,
68
+ currentVersion: detection.version,
69
+ });
70
+ return;
71
+ }
72
+ }
73
+
74
+ if (options.maxVersion) {
75
+ if (versioningService.compareVersions(detection.version, options.maxVersion) > 0) {
76
+ res.status(400).json({
77
+ error: `This endpoint supports up to API version ${options.maxVersion}`,
78
+ currentVersion: detection.version,
79
+ });
80
+ return;
81
+ }
82
+ }
83
+
84
+ // Check if deprecated versions are allowed
85
+ const versionInfo = versioningService.getVersion(detection.version);
86
+ if (versionInfo?.status === 'deprecated' && options.allowDeprecated === false) {
87
+ res.status(410).json({
88
+ error: `API version ${detection.version} is deprecated and no longer supported`,
89
+ sunsetAt: versionInfo.sunsetAt,
90
+ migrateToVersion: versioningService.getActiveVersions()[0]?.version,
91
+ });
92
+ return;
93
+ }
94
+
95
+ next();
96
+ };
97
+ }
98
+
99
+ /**
100
+ * Version-specific route handler
101
+ */
102
+ export function versionedRoute(
103
+ versions: Record<string, (req: Request, res: Response) => void | Promise<void>>
104
+ ): (req: Request, res: Response) => void | Promise<void> {
105
+ return (req: Request, res: Response): void | Promise<void> => {
106
+ const versionedReq = req as VersionedRequest;
107
+ const version = versionedReq.apiVersion || 'v1';
108
+
109
+ const handler = versions[version];
110
+
111
+ if (!handler) {
112
+ res.status(404).json({
113
+ error: `Endpoint not available in version ${version}`,
114
+ });
115
+ return;
116
+ }
117
+
118
+ return handler(req, res);
119
+ };
120
+ }
@@ -0,0 +1,54 @@
1
+ import { Router } from 'express';
2
+ import type { Request, Response } from 'express';
3
+ import type { VersioningService } from './versioning.service.js';
4
+
5
+ /**
6
+ * Create versioning routes
7
+ */
8
+ export function createVersioningRoutes(versioningService: VersioningService): Router {
9
+ const router = Router();
10
+
11
+ /**
12
+ * Get all API versions
13
+ * GET /versions
14
+ */
15
+ router.get('/versions', (_req: Request, res: Response) => {
16
+ const versions = versioningService.getAllVersions();
17
+ res.json({
18
+ versions,
19
+ default: versioningService.getAllVersions().find((v) => v.isDefault)?.version,
20
+ });
21
+ });
22
+
23
+ /**
24
+ * Get active versions
25
+ * GET /versions/active
26
+ */
27
+ router.get('/versions/active', (_req: Request, res: Response) => {
28
+ const versions = versioningService.getActiveVersions();
29
+ res.json({ versions, count: versions.length });
30
+ });
31
+
32
+ /**
33
+ * Get specific version info
34
+ * GET /versions/:version
35
+ */
36
+ router.get('/versions/:version', (req: Request, res: Response) => {
37
+ const versionParam = req.params.version;
38
+ if (!versionParam) {
39
+ res.status(400).json({ error: 'Version parameter required' });
40
+ return;
41
+ }
42
+
43
+ const version = versioningService.getVersion(versionParam);
44
+
45
+ if (!version) {
46
+ res.status(404).json({ error: 'Version not found' });
47
+ return;
48
+ }
49
+
50
+ res.json(version);
51
+ });
52
+
53
+ return router;
54
+ }
@@ -0,0 +1,189 @@
1
+ import { logger } from '../../core/logger.js';
2
+ import type {
3
+ VersioningConfig,
4
+ ApiVersion,
5
+ VersionDetectionResult,
6
+ VersionStrategy,
7
+ VersionMigration,
8
+ } from './types.js';
9
+
10
+ /**
11
+ * API Versioning Service
12
+ * Multiple API versions support
13
+ */
14
+ export class VersioningService {
15
+ private config: VersioningConfig;
16
+ private migrations = new Map<string, VersionMigration>();
17
+
18
+ constructor(config: VersioningConfig) {
19
+ this.config = {
20
+ headerName: 'X-API-Version',
21
+ queryParam: 'version',
22
+ strict: true,
23
+ deprecationWarnings: true,
24
+ ...config,
25
+ };
26
+
27
+ logger.info(
28
+ {
29
+ strategy: this.config.strategy,
30
+ defaultVersion: this.config.defaultVersion,
31
+ versions: this.config.versions.map((v) => v.version),
32
+ },
33
+ 'API versioning service initialized'
34
+ );
35
+ }
36
+
37
+ /**
38
+ * Detect version from request
39
+ */
40
+ detectVersion(req: {
41
+ url?: string;
42
+ headers?: Record<string, string | string[] | undefined>;
43
+ query?: Record<string, string | string[] | undefined>;
44
+ }): VersionDetectionResult {
45
+ let version: string | null = null;
46
+ let source: VersionStrategy = this.config.strategy;
47
+
48
+ // Try to detect based on strategy
49
+ switch (this.config.strategy) {
50
+ case 'url':
51
+ version = this.detectFromUrl(req.url || '');
52
+ source = 'url';
53
+ break;
54
+
55
+ case 'header':
56
+ version = this.detectFromHeader(req.headers || {});
57
+ source = 'header';
58
+ break;
59
+
60
+ case 'query':
61
+ version = this.detectFromQuery(req.query || {});
62
+ source = 'query';
63
+ break;
64
+
65
+ case 'accept-header':
66
+ version = this.detectFromAcceptHeader(req.headers || {});
67
+ source = 'accept-header';
68
+ break;
69
+ }
70
+
71
+ // Fallback to default version
72
+ if (!version) {
73
+ version = this.config.defaultVersion;
74
+ source = this.config.strategy;
75
+ }
76
+
77
+ // Validate version
78
+ const isValid = this.isVersionValid(version);
79
+ const versionInfo = this.getVersion(version);
80
+
81
+ let warning: string | undefined;
82
+ if (versionInfo?.status === 'deprecated' && this.config.deprecationWarnings) {
83
+ warning = `API version ${version} is deprecated`;
84
+ if (versionInfo.sunsetAt) {
85
+ warning += ` and will be removed on ${versionInfo.sunsetAt.toISOString().split('T')[0]}`;
86
+ }
87
+ }
88
+
89
+ return {
90
+ version,
91
+ source,
92
+ isValid,
93
+ warning,
94
+ };
95
+ }
96
+
97
+ /**
98
+ * Get version info
99
+ */
100
+ getVersion(version: string): ApiVersion | undefined {
101
+ return this.config.versions.find((v) => v.version === version);
102
+ }
103
+
104
+ /**
105
+ * Get all versions
106
+ */
107
+ getAllVersions(): ApiVersion[] {
108
+ return this.config.versions;
109
+ }
110
+
111
+ /**
112
+ * Get active versions
113
+ */
114
+ getActiveVersions(): ApiVersion[] {
115
+ return this.config.versions.filter((v) => v.status === 'active');
116
+ }
117
+
118
+ /**
119
+ * Check if version is valid
120
+ */
121
+ isVersionValid(version: string): boolean {
122
+ return this.config.versions.some((v) => v.version === version);
123
+ }
124
+
125
+ /**
126
+ * Add version migration
127
+ */
128
+ addMigration(migration: VersionMigration): void {
129
+ const key = `${migration.from}-${migration.to}`;
130
+ this.migrations.set(key, migration);
131
+ logger.info({ from: migration.from, to: migration.to }, 'Migration added');
132
+ }
133
+
134
+ /**
135
+ * Get migration
136
+ */
137
+ getMigration(from: string, to: string): VersionMigration | undefined {
138
+ return this.migrations.get(`${from}-${to}`);
139
+ }
140
+
141
+ /**
142
+ * Compare versions
143
+ */
144
+ compareVersions(v1: string, v2: string): number {
145
+ const num1 = parseInt(v1.replace(/\D/g, ''), 10);
146
+ const num2 = parseInt(v2.replace(/\D/g, ''), 10);
147
+ return num1 - num2;
148
+ }
149
+
150
+ /**
151
+ * Detect version from URL path
152
+ */
153
+ private detectFromUrl(url: string): string | null {
154
+ const match = url.match(/\/(v\d+)\//);
155
+ return match?.[1] ?? null;
156
+ }
157
+
158
+ /**
159
+ * Detect version from header
160
+ */
161
+ private detectFromHeader(headers: Record<string, string | string[] | undefined>): string | null {
162
+ const headerValue = headers[this.config.headerName?.toLowerCase() || 'x-api-version'];
163
+ return Array.isArray(headerValue) ? (headerValue[0] ?? null) : (headerValue ?? null);
164
+ }
165
+
166
+ /**
167
+ * Detect version from query parameter
168
+ */
169
+ private detectFromQuery(query: Record<string, string | string[] | undefined>): string | null {
170
+ const queryValue = query[this.config.queryParam || 'version'];
171
+ return Array.isArray(queryValue) ? (queryValue[0] ?? null) : (queryValue ?? null);
172
+ }
173
+
174
+ /**
175
+ * Detect version from Accept header
176
+ */
177
+ private detectFromAcceptHeader(
178
+ headers: Record<string, string | string[] | undefined>
179
+ ): string | null {
180
+ const accept = headers['accept'];
181
+ const acceptStr = Array.isArray(accept) ? accept[0] : accept;
182
+
183
+ if (!acceptStr) return null;
184
+
185
+ // Look for version in Accept header like: application/vnd.myapp.v2+json
186
+ const match = acceptStr.match(/\.v(\d+)\+/);
187
+ return match ? `v${match[1]}` : null;
188
+ }
189
+ }
@@ -0,0 +1,206 @@
1
+ /**
2
+ * Audit Repository
3
+ * Prisma-based persistence for audit logs
4
+ */
5
+ import type { Prisma } from '@prisma/client';
6
+ import type { PrismaClient, AuditLog as PrismaAuditLog } from '@prisma/client';
7
+ import type { AuditLogEntry, AuditLogQuery } from './types.js';
8
+ import type { PaginatedResult, PaginationParams } from '../../types/index.js';
9
+ import { createPaginatedResult, getSkip } from '../../utils/pagination.js';
10
+
11
+ export interface AuditLogRecord extends AuditLogEntry {
12
+ id: string;
13
+ createdAt: Date;
14
+ }
15
+
16
+ export class AuditRepository {
17
+ constructor(private prisma: PrismaClient) {}
18
+
19
+ /**
20
+ * Create a new audit log entry
21
+ */
22
+ async create(entry: AuditLogEntry): Promise<AuditLogRecord> {
23
+ const log = await this.prisma.auditLog.create({
24
+ data: {
25
+ userId: entry.userId,
26
+ action: entry.action,
27
+ resource: entry.resource,
28
+ resourceId: entry.resourceId,
29
+ oldValue: entry.oldValue as Prisma.InputJsonValue,
30
+ newValue: entry.newValue as Prisma.InputJsonValue,
31
+ ipAddress: entry.ipAddress,
32
+ userAgent: entry.userAgent,
33
+ metadata: entry.metadata as Prisma.InputJsonValue,
34
+ },
35
+ });
36
+
37
+ return this.mapFromPrisma(log);
38
+ }
39
+
40
+ /**
41
+ * Create multiple audit log entries
42
+ */
43
+ async createMany(entries: AuditLogEntry[]): Promise<number> {
44
+ const result = await this.prisma.auditLog.createMany({
45
+ data: entries.map((entry) => ({
46
+ userId: entry.userId,
47
+ action: entry.action,
48
+ resource: entry.resource,
49
+ resourceId: entry.resourceId,
50
+ oldValue: entry.oldValue as Prisma.InputJsonValue,
51
+ newValue: entry.newValue as Prisma.InputJsonValue,
52
+ ipAddress: entry.ipAddress,
53
+ userAgent: entry.userAgent,
54
+ metadata: entry.metadata as Prisma.InputJsonValue,
55
+ })),
56
+ });
57
+
58
+ return result.count;
59
+ }
60
+
61
+ /**
62
+ * Find audit log by ID
63
+ */
64
+ async findById(id: string): Promise<AuditLogRecord | null> {
65
+ const log = await this.prisma.auditLog.findUnique({
66
+ where: { id },
67
+ });
68
+
69
+ return log ? this.mapFromPrisma(log) : null;
70
+ }
71
+
72
+ /**
73
+ * Query audit logs with filters and pagination
74
+ */
75
+ async query(params: AuditLogQuery): Promise<PaginatedResult<AuditLogRecord>> {
76
+ const { page = 1, limit = 20 } = params;
77
+ const pagination: PaginationParams = { page, limit };
78
+
79
+ const where: Prisma.AuditLogWhereInput = {};
80
+
81
+ if (params.userId) where.userId = params.userId;
82
+ if (params.action) where.action = params.action;
83
+ if (params.resource) where.resource = params.resource;
84
+ if (params.resourceId) where.resourceId = params.resourceId;
85
+
86
+ if (params.startDate || params.endDate) {
87
+ where.createdAt = {};
88
+ if (params.startDate) where.createdAt.gte = params.startDate;
89
+ if (params.endDate) where.createdAt.lte = params.endDate;
90
+ }
91
+
92
+ const [logs, total] = await Promise.all([
93
+ this.prisma.auditLog.findMany({
94
+ where,
95
+ orderBy: { createdAt: 'desc' },
96
+ skip: getSkip(pagination),
97
+ take: limit,
98
+ }),
99
+ this.prisma.auditLog.count({ where }),
100
+ ]);
101
+
102
+ const data = logs.map((log) => this.mapFromPrisma(log));
103
+ return createPaginatedResult(data, total, pagination);
104
+ }
105
+
106
+ /**
107
+ * Find logs by user ID
108
+ */
109
+ async findByUser(userId: string, limit = 50): Promise<AuditLogRecord[]> {
110
+ const logs = await this.prisma.auditLog.findMany({
111
+ where: { userId },
112
+ orderBy: { createdAt: 'desc' },
113
+ take: limit,
114
+ });
115
+
116
+ return logs.map((log) => this.mapFromPrisma(log));
117
+ }
118
+
119
+ /**
120
+ * Find logs by resource
121
+ */
122
+ async findByResource(
123
+ resource: string,
124
+ resourceId?: string,
125
+ limit = 50
126
+ ): Promise<AuditLogRecord[]> {
127
+ const where: Prisma.AuditLogWhereInput = { resource };
128
+ if (resourceId) where.resourceId = resourceId;
129
+
130
+ const logs = await this.prisma.auditLog.findMany({
131
+ where,
132
+ orderBy: { createdAt: 'desc' },
133
+ take: limit,
134
+ });
135
+
136
+ return logs.map((log) => this.mapFromPrisma(log));
137
+ }
138
+
139
+ /**
140
+ * Find logs by action
141
+ */
142
+ async findByAction(action: string, limit = 50): Promise<AuditLogRecord[]> {
143
+ const logs = await this.prisma.auditLog.findMany({
144
+ where: { action },
145
+ orderBy: { createdAt: 'desc' },
146
+ take: limit,
147
+ });
148
+
149
+ return logs.map((log) => this.mapFromPrisma(log));
150
+ }
151
+
152
+ /**
153
+ * Count logs with optional filters
154
+ */
155
+ async count(filters?: Partial<AuditLogQuery>): Promise<number> {
156
+ const where: Prisma.AuditLogWhereInput = {};
157
+
158
+ if (filters?.userId) where.userId = filters.userId;
159
+ if (filters?.action) where.action = filters.action;
160
+ if (filters?.resource) where.resource = filters.resource;
161
+
162
+ return this.prisma.auditLog.count({ where });
163
+ }
164
+
165
+ /**
166
+ * Delete old audit logs (for data retention)
167
+ */
168
+ async deleteOlderThan(days: number): Promise<number> {
169
+ const cutoffDate = new Date();
170
+ cutoffDate.setDate(cutoffDate.getDate() - days);
171
+
172
+ const result = await this.prisma.auditLog.deleteMany({
173
+ where: {
174
+ createdAt: { lt: cutoffDate },
175
+ },
176
+ });
177
+
178
+ return result.count;
179
+ }
180
+
181
+ /**
182
+ * Clear all audit logs (for testing)
183
+ */
184
+ async clear(): Promise<void> {
185
+ await this.prisma.auditLog.deleteMany();
186
+ }
187
+
188
+ /**
189
+ * Map Prisma model to domain type
190
+ */
191
+ private mapFromPrisma(log: PrismaAuditLog): AuditLogRecord {
192
+ return {
193
+ id: log.id,
194
+ userId: log.userId ?? undefined,
195
+ action: log.action,
196
+ resource: log.resource,
197
+ resourceId: log.resourceId ?? undefined,
198
+ oldValue: log.oldValue as Record<string, unknown> | undefined,
199
+ newValue: log.newValue as Record<string, unknown> | undefined,
200
+ ipAddress: log.ipAddress ?? undefined,
201
+ userAgent: log.userAgent ?? undefined,
202
+ metadata: log.metadata as Record<string, unknown> | undefined,
203
+ createdAt: log.createdAt,
204
+ };
205
+ }
206
+ }
@@ -1,24 +1,20 @@
1
- import { randomUUID } from 'crypto';
2
1
  import { logger } from '../../core/logger.js';
3
2
  import type { AuditLogEntry, AuditLogQuery } from './types.js';
4
3
  import type { PaginatedResult } from '../../types/index.js';
5
- import { createPaginatedResult } from '../../utils/pagination.js';
6
-
7
- // In-memory storage (will be replaced by Prisma in production)
8
- const auditLogs: Map<string, AuditLogEntry & { id: string; createdAt: Date }> = new Map();
4
+ import { AuditRepository, type AuditLogRecord } from './audit.repository.js';
5
+ import { prisma } from '../../database/prisma.js';
9
6
 
10
7
  export class AuditService {
11
- async log(entry: AuditLogEntry): Promise<void> {
12
- const id = randomUUID();
13
- const auditEntry = {
14
- ...entry,
15
- id,
16
- createdAt: new Date(),
17
- };
8
+ private repository: AuditRepository;
9
+
10
+ constructor() {
11
+ this.repository = new AuditRepository(prisma);
12
+ }
18
13
 
19
- auditLogs.set(id, auditEntry);
14
+ async log(entry: AuditLogEntry): Promise<void> {
15
+ await this.repository.create(entry);
20
16
 
21
- // Also log to structured logger
17
+ // Also log to structured logger for real-time monitoring
22
18
  logger.info(
23
19
  {
24
20
  audit: true,
@@ -32,54 +28,20 @@ export class AuditService {
32
28
  );
33
29
  }
34
30
 
35
- async query(
36
- params: AuditLogQuery
37
- ): Promise<PaginatedResult<AuditLogEntry & { id: string; createdAt: Date }>> {
38
- const { page = 1, limit = 20 } = params;
39
- let logs = Array.from(auditLogs.values());
40
-
41
- // Apply filters
42
- if (params.userId) {
43
- logs = logs.filter((log) => log.userId === params.userId);
44
- }
45
- if (params.action) {
46
- logs = logs.filter((log) => log.action === params.action);
47
- }
48
- if (params.resource) {
49
- logs = logs.filter((log) => log.resource === params.resource);
50
- }
51
- if (params.resourceId) {
52
- logs = logs.filter((log) => log.resourceId === params.resourceId);
53
- }
54
- if (params.startDate) {
55
- logs = logs.filter((log) => log.createdAt >= params.startDate!);
56
- }
57
- if (params.endDate) {
58
- logs = logs.filter((log) => log.createdAt <= params.endDate!);
59
- }
60
-
61
- // Sort by date descending
62
- logs.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
63
-
64
- const total = logs.length;
65
- const skip = (page - 1) * limit;
66
- const data = logs.slice(skip, skip + limit);
67
-
68
- return createPaginatedResult(data, total, { page, limit });
31
+ async query(params: AuditLogQuery): Promise<PaginatedResult<AuditLogRecord>> {
32
+ return this.repository.query(params);
69
33
  }
70
34
 
71
- async findByUser(userId: string, limit = 50): Promise<(AuditLogEntry & { id: string; createdAt: Date })[]> {
72
- const result = await this.query({ userId, limit });
73
- return result.data;
35
+ async findByUser(userId: string, limit = 50): Promise<AuditLogRecord[]> {
36
+ return this.repository.findByUser(userId, limit);
74
37
  }
75
38
 
76
39
  async findByResource(
77
40
  resource: string,
78
41
  resourceId: string,
79
42
  limit = 50
80
- ): Promise<(AuditLogEntry & { id: string; createdAt: Date })[]> {
81
- const result = await this.query({ resource, resourceId, limit });
82
- return result.data;
43
+ ): Promise<AuditLogRecord[]> {
44
+ return this.repository.findByResource(resource, resourceId, limit);
83
45
  }
84
46
 
85
47
  // Shortcut methods for common audit events
@@ -136,10 +98,7 @@ export class AuditService {
136
98
  });
137
99
  }
138
100
 
139
- async logLogin(
140
- userId: string,
141
- meta?: { ipAddress?: string; userAgent?: string }
142
- ): Promise<void> {
101
+ async logLogin(userId: string, meta?: { ipAddress?: string; userAgent?: string }): Promise<void> {
143
102
  await this.log({
144
103
  action: 'login',
145
104
  resource: 'auth',
@@ -172,9 +131,18 @@ export class AuditService {
172
131
  });
173
132
  }
174
133
 
134
+ // Data retention: delete old logs
135
+ async cleanupOldLogs(retentionDays: number): Promise<number> {
136
+ const count = await this.repository.deleteOlderThan(retentionDays);
137
+ if (count > 0) {
138
+ logger.info({ count, retentionDays }, 'Cleaned up old audit logs');
139
+ }
140
+ return count;
141
+ }
142
+
175
143
  // Clear all logs (for testing)
176
144
  async clear(): Promise<void> {
177
- auditLogs.clear();
145
+ await this.repository.clear();
178
146
  }
179
147
  }
180
148