servcraft 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (216) hide show
  1. package/.claude/settings.local.json +29 -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/README.md +1070 -1
  9. package/dist/cli/index.cjs +2026 -2168
  10. package/dist/cli/index.cjs.map +1 -1
  11. package/dist/cli/index.js +2026 -2168
  12. package/dist/cli/index.js.map +1 -1
  13. package/dist/index.cjs +595 -616
  14. package/dist/index.cjs.map +1 -1
  15. package/dist/index.d.cts +114 -52
  16. package/dist/index.d.ts +114 -52
  17. package/dist/index.js +595 -616
  18. package/dist/index.js.map +1 -1
  19. package/docs/CLI-001_MULTI_DB_PLAN.md +546 -0
  20. package/docs/DATABASE_MULTI_ORM.md +399 -0
  21. package/docs/PHASE1_BREAKDOWN.md +346 -0
  22. package/docs/PROGRESS.md +550 -0
  23. package/docs/modules/ANALYTICS.md +226 -0
  24. package/docs/modules/API-VERSIONING.md +252 -0
  25. package/docs/modules/AUDIT.md +192 -0
  26. package/docs/modules/AUTH.md +431 -0
  27. package/docs/modules/CACHE.md +346 -0
  28. package/docs/modules/EMAIL.md +254 -0
  29. package/docs/modules/FEATURE-FLAG.md +291 -0
  30. package/docs/modules/I18N.md +294 -0
  31. package/docs/modules/MEDIA-PROCESSING.md +281 -0
  32. package/docs/modules/MFA.md +266 -0
  33. package/docs/modules/NOTIFICATION.md +311 -0
  34. package/docs/modules/OAUTH.md +237 -0
  35. package/docs/modules/PAYMENT.md +804 -0
  36. package/docs/modules/QUEUE.md +540 -0
  37. package/docs/modules/RATE-LIMIT.md +339 -0
  38. package/docs/modules/SEARCH.md +288 -0
  39. package/docs/modules/SECURITY.md +327 -0
  40. package/docs/modules/SESSION.md +382 -0
  41. package/docs/modules/SWAGGER.md +305 -0
  42. package/docs/modules/UPLOAD.md +296 -0
  43. package/docs/modules/USER.md +505 -0
  44. package/docs/modules/VALIDATION.md +294 -0
  45. package/docs/modules/WEBHOOK.md +270 -0
  46. package/docs/modules/WEBSOCKET.md +691 -0
  47. package/package.json +53 -38
  48. package/prisma/schema.prisma +395 -1
  49. package/src/cli/commands/add-module.ts +520 -87
  50. package/src/cli/commands/db.ts +3 -4
  51. package/src/cli/commands/docs.ts +256 -6
  52. package/src/cli/commands/generate.ts +12 -19
  53. package/src/cli/commands/init.ts +384 -214
  54. package/src/cli/index.ts +0 -4
  55. package/src/cli/templates/repository.ts +6 -1
  56. package/src/cli/templates/routes.ts +6 -21
  57. package/src/cli/utils/docs-generator.ts +6 -7
  58. package/src/cli/utils/env-manager.ts +717 -0
  59. package/src/cli/utils/field-parser.ts +16 -7
  60. package/src/cli/utils/interactive-prompt.ts +223 -0
  61. package/src/cli/utils/template-manager.ts +346 -0
  62. package/src/config/database.config.ts +183 -0
  63. package/src/config/env.ts +0 -10
  64. package/src/config/index.ts +0 -14
  65. package/src/core/server.ts +1 -1
  66. package/src/database/adapters/mongoose.adapter.ts +132 -0
  67. package/src/database/adapters/prisma.adapter.ts +118 -0
  68. package/src/database/connection.ts +190 -0
  69. package/src/database/interfaces/database.interface.ts +85 -0
  70. package/src/database/interfaces/index.ts +7 -0
  71. package/src/database/interfaces/repository.interface.ts +129 -0
  72. package/src/database/models/mongoose/index.ts +7 -0
  73. package/src/database/models/mongoose/payment.schema.ts +347 -0
  74. package/src/database/models/mongoose/user.schema.ts +154 -0
  75. package/src/database/prisma.ts +1 -4
  76. package/src/database/redis.ts +101 -0
  77. package/src/database/repositories/mongoose/index.ts +7 -0
  78. package/src/database/repositories/mongoose/payment.repository.ts +380 -0
  79. package/src/database/repositories/mongoose/user.repository.ts +255 -0
  80. package/src/database/seed.ts +6 -1
  81. package/src/index.ts +9 -20
  82. package/src/middleware/security.ts +2 -6
  83. package/src/modules/analytics/analytics.routes.ts +80 -0
  84. package/src/modules/analytics/analytics.service.ts +364 -0
  85. package/src/modules/analytics/index.ts +18 -0
  86. package/src/modules/analytics/types.ts +180 -0
  87. package/src/modules/api-versioning/index.ts +15 -0
  88. package/src/modules/api-versioning/types.ts +86 -0
  89. package/src/modules/api-versioning/versioning.middleware.ts +120 -0
  90. package/src/modules/api-versioning/versioning.routes.ts +54 -0
  91. package/src/modules/api-versioning/versioning.service.ts +189 -0
  92. package/src/modules/audit/audit.repository.ts +206 -0
  93. package/src/modules/audit/audit.service.ts +27 -59
  94. package/src/modules/auth/auth.controller.ts +2 -2
  95. package/src/modules/auth/auth.middleware.ts +3 -9
  96. package/src/modules/auth/auth.routes.ts +10 -107
  97. package/src/modules/auth/auth.service.ts +126 -23
  98. package/src/modules/auth/index.ts +3 -4
  99. package/src/modules/cache/cache.service.ts +367 -0
  100. package/src/modules/cache/index.ts +10 -0
  101. package/src/modules/cache/types.ts +44 -0
  102. package/src/modules/email/email.service.ts +3 -10
  103. package/src/modules/email/templates.ts +2 -8
  104. package/src/modules/feature-flag/feature-flag.repository.ts +303 -0
  105. package/src/modules/feature-flag/feature-flag.routes.ts +247 -0
  106. package/src/modules/feature-flag/feature-flag.service.ts +566 -0
  107. package/src/modules/feature-flag/index.ts +20 -0
  108. package/src/modules/feature-flag/types.ts +192 -0
  109. package/src/modules/i18n/i18n.middleware.ts +186 -0
  110. package/src/modules/i18n/i18n.routes.ts +191 -0
  111. package/src/modules/i18n/i18n.service.ts +456 -0
  112. package/src/modules/i18n/index.ts +18 -0
  113. package/src/modules/i18n/types.ts +118 -0
  114. package/src/modules/media-processing/index.ts +17 -0
  115. package/src/modules/media-processing/media-processing.routes.ts +111 -0
  116. package/src/modules/media-processing/media-processing.service.ts +245 -0
  117. package/src/modules/media-processing/types.ts +156 -0
  118. package/src/modules/mfa/index.ts +20 -0
  119. package/src/modules/mfa/mfa.repository.ts +206 -0
  120. package/src/modules/mfa/mfa.routes.ts +595 -0
  121. package/src/modules/mfa/mfa.service.ts +572 -0
  122. package/src/modules/mfa/totp.ts +150 -0
  123. package/src/modules/mfa/types.ts +57 -0
  124. package/src/modules/notification/index.ts +20 -0
  125. package/src/modules/notification/notification.repository.ts +356 -0
  126. package/src/modules/notification/notification.service.ts +483 -0
  127. package/src/modules/notification/types.ts +119 -0
  128. package/src/modules/oauth/index.ts +20 -0
  129. package/src/modules/oauth/oauth.repository.ts +219 -0
  130. package/src/modules/oauth/oauth.routes.ts +446 -0
  131. package/src/modules/oauth/oauth.service.ts +293 -0
  132. package/src/modules/oauth/providers/apple.provider.ts +250 -0
  133. package/src/modules/oauth/providers/facebook.provider.ts +181 -0
  134. package/src/modules/oauth/providers/github.provider.ts +248 -0
  135. package/src/modules/oauth/providers/google.provider.ts +189 -0
  136. package/src/modules/oauth/providers/twitter.provider.ts +214 -0
  137. package/src/modules/oauth/types.ts +94 -0
  138. package/src/modules/payment/index.ts +19 -0
  139. package/src/modules/payment/payment.repository.ts +733 -0
  140. package/src/modules/payment/payment.routes.ts +390 -0
  141. package/src/modules/payment/payment.service.ts +354 -0
  142. package/src/modules/payment/providers/mobile-money.provider.ts +274 -0
  143. package/src/modules/payment/providers/paypal.provider.ts +190 -0
  144. package/src/modules/payment/providers/stripe.provider.ts +215 -0
  145. package/src/modules/payment/types.ts +140 -0
  146. package/src/modules/queue/cron.ts +438 -0
  147. package/src/modules/queue/index.ts +87 -0
  148. package/src/modules/queue/queue.routes.ts +600 -0
  149. package/src/modules/queue/queue.service.ts +842 -0
  150. package/src/modules/queue/types.ts +222 -0
  151. package/src/modules/queue/workers.ts +366 -0
  152. package/src/modules/rate-limit/index.ts +59 -0
  153. package/src/modules/rate-limit/rate-limit.middleware.ts +134 -0
  154. package/src/modules/rate-limit/rate-limit.routes.ts +269 -0
  155. package/src/modules/rate-limit/rate-limit.service.ts +348 -0
  156. package/src/modules/rate-limit/stores/memory.store.ts +165 -0
  157. package/src/modules/rate-limit/stores/redis.store.ts +322 -0
  158. package/src/modules/rate-limit/types.ts +153 -0
  159. package/src/modules/search/adapters/elasticsearch.adapter.ts +326 -0
  160. package/src/modules/search/adapters/meilisearch.adapter.ts +261 -0
  161. package/src/modules/search/adapters/memory.adapter.ts +278 -0
  162. package/src/modules/search/index.ts +21 -0
  163. package/src/modules/search/search.service.ts +234 -0
  164. package/src/modules/search/types.ts +214 -0
  165. package/src/modules/security/index.ts +40 -0
  166. package/src/modules/security/sanitize.ts +223 -0
  167. package/src/modules/security/security-audit.service.ts +388 -0
  168. package/src/modules/security/security.middleware.ts +398 -0
  169. package/src/modules/session/index.ts +3 -0
  170. package/src/modules/session/session.repository.ts +159 -0
  171. package/src/modules/session/session.service.ts +340 -0
  172. package/src/modules/session/types.ts +38 -0
  173. package/src/modules/swagger/index.ts +7 -1
  174. package/src/modules/swagger/schema-builder.ts +16 -4
  175. package/src/modules/swagger/swagger.service.ts +9 -10
  176. package/src/modules/swagger/types.ts +0 -2
  177. package/src/modules/upload/index.ts +14 -0
  178. package/src/modules/upload/types.ts +83 -0
  179. package/src/modules/upload/upload.repository.ts +199 -0
  180. package/src/modules/upload/upload.routes.ts +311 -0
  181. package/src/modules/upload/upload.service.ts +448 -0
  182. package/src/modules/user/index.ts +3 -3
  183. package/src/modules/user/user.controller.ts +15 -9
  184. package/src/modules/user/user.repository.ts +237 -113
  185. package/src/modules/user/user.routes.ts +39 -164
  186. package/src/modules/user/user.service.ts +4 -3
  187. package/src/modules/validation/validator.ts +12 -17
  188. package/src/modules/webhook/index.ts +91 -0
  189. package/src/modules/webhook/retry.ts +196 -0
  190. package/src/modules/webhook/signature.ts +135 -0
  191. package/src/modules/webhook/types.ts +181 -0
  192. package/src/modules/webhook/webhook.repository.ts +358 -0
  193. package/src/modules/webhook/webhook.routes.ts +442 -0
  194. package/src/modules/webhook/webhook.service.ts +457 -0
  195. package/src/modules/websocket/features.ts +504 -0
  196. package/src/modules/websocket/index.ts +106 -0
  197. package/src/modules/websocket/middlewares.ts +298 -0
  198. package/src/modules/websocket/types.ts +181 -0
  199. package/src/modules/websocket/websocket.service.ts +692 -0
  200. package/src/utils/errors.ts +7 -0
  201. package/src/utils/pagination.ts +4 -1
  202. package/tests/helpers/db-check.ts +79 -0
  203. package/tests/integration/auth-redis.test.ts +94 -0
  204. package/tests/integration/cache-redis.test.ts +387 -0
  205. package/tests/integration/mongoose-repositories.test.ts +410 -0
  206. package/tests/integration/payment-prisma.test.ts +637 -0
  207. package/tests/integration/queue-bullmq.test.ts +417 -0
  208. package/tests/integration/user-prisma.test.ts +441 -0
  209. package/tests/integration/websocket-socketio.test.ts +552 -0
  210. package/tests/setup.ts +11 -9
  211. package/vitest.config.ts +3 -8
  212. package/npm-cache/_cacache/content-v2/sha512/1c/d0/03440d500a0487621aad1d6402978340698976602046db8e24fa03c01ee6c022c69b0582f969042d9442ee876ac35c038e960dd427d1e622fa24b8eb7dba +0 -0
  213. package/npm-cache/_cacache/content-v2/sha512/42/55/28b493ca491833e5aab0e9c3108d29ab3f36c248ca88f45d4630674fce9130959e56ae308797ac2b6328fa7f09a610b9550ed09cb971d039876d293fc69d +0 -0
  214. package/npm-cache/_cacache/content-v2/sha512/e0/12/f360dc9315ee5f17844a0c8c233ee6bf7c30837c4a02ea0d56c61c7f7ab21c0e958e50ed2c57c59f983c762b93056778c9009b2398ffc26def0183999b13 +0 -0
  215. package/npm-cache/_cacache/content-v2/sha512/ed/b0/fae1161902898f4c913c67d7f6cdf6be0665aec3b389b9c4f4f0a101ca1da59badf1b59c4e0030f5223023b8d63cfe501c46a32c20c895d4fb3f11ca2232 +0 -0
  216. package/npm-cache/_cacache/index-v5/58/94/c2cba79e0f16b4c10e95a87e32255741149e8222cc314a476aab67c39cc0 +0 -5
@@ -0,0 +1,57 @@
1
+ export type MFAMethod = 'totp' | 'sms' | 'email' | 'backup_codes';
2
+
3
+ export interface MFAConfig {
4
+ issuer: string;
5
+ totpWindow?: number; // Time window for TOTP validation (default: 1)
6
+ backupCodesCount?: number; // Number of backup codes to generate (default: 10)
7
+ smsProvider?: 'twilio' | 'nexmo';
8
+ emailProvider?: 'smtp' | 'sendgrid' | 'resend';
9
+ }
10
+
11
+ export interface UserMFA {
12
+ userId: string;
13
+ enabled: boolean;
14
+ methods: MFAMethod[];
15
+ totpSecret?: string;
16
+ totpVerified: boolean;
17
+ backupCodes?: string[];
18
+ backupCodesUsed?: string[];
19
+ phoneNumber?: string;
20
+ phoneVerified: boolean;
21
+ email?: string;
22
+ emailVerified: boolean;
23
+ lastUsed?: Date;
24
+ createdAt: Date;
25
+ updatedAt: Date;
26
+ }
27
+
28
+ export interface TOTPSetup {
29
+ secret: string;
30
+ qrCode: string; // Data URL for QR code
31
+ manualEntry: string; // Manual entry key
32
+ uri: string; // otpauth:// URI
33
+ }
34
+
35
+ export interface MFAChallenge {
36
+ id: string;
37
+ userId: string;
38
+ method: MFAMethod;
39
+ code?: string; // For SMS/email challenges
40
+ expiresAt: Date;
41
+ attempts: number;
42
+ maxAttempts: number;
43
+ verified: boolean;
44
+ createdAt: Date;
45
+ }
46
+
47
+ export interface MFAVerifyResult {
48
+ success: boolean;
49
+ method: MFAMethod;
50
+ remainingAttempts?: number;
51
+ lockedUntil?: Date;
52
+ }
53
+
54
+ export interface BackupCodesResult {
55
+ codes: string[];
56
+ generatedAt: Date;
57
+ }
@@ -0,0 +1,20 @@
1
+ export {
2
+ NotificationService,
3
+ getNotificationService,
4
+ createNotificationService,
5
+ } from './notification.service.js';
6
+ export type {
7
+ Notification,
8
+ NotificationConfig,
9
+ NotificationChannel,
10
+ NotificationStatus,
11
+ EmailConfig,
12
+ EmailMessage,
13
+ SMSConfig,
14
+ SMSMessage,
15
+ PushConfig,
16
+ PushMessage,
17
+ WebhookConfig,
18
+ WebhookMessage,
19
+ NotificationTemplate,
20
+ } from './types.js';
@@ -0,0 +1,356 @@
1
+ /**
2
+ * Notification Repository
3
+ * Prisma-based persistence for notifications and templates
4
+ */
5
+ import { Prisma } from '@prisma/client';
6
+ import type {
7
+ Notification as PrismaNotification,
8
+ NotificationTemplate as PrismaTemplate,
9
+ NotificationChannel as PrismaChannel,
10
+ NotificationStatus as PrismaStatus,
11
+ PrismaClient,
12
+ } from '@prisma/client';
13
+ import { logger } from '../../core/logger.js';
14
+ import type { Notification, NotificationTemplate, NotificationChannel } from './types.js';
15
+
16
+ // Enum mappings (Prisma UPPERCASE ↔ Application lowercase)
17
+ const channelToPrisma: Record<NotificationChannel, PrismaChannel> = {
18
+ email: 'EMAIL',
19
+ sms: 'SMS',
20
+ push: 'PUSH',
21
+ webhook: 'WEBHOOK',
22
+ in_app: 'IN_APP',
23
+ };
24
+
25
+ const channelFromPrisma: Record<PrismaChannel, NotificationChannel> = {
26
+ EMAIL: 'email',
27
+ SMS: 'sms',
28
+ PUSH: 'push',
29
+ WEBHOOK: 'webhook',
30
+ IN_APP: 'in_app',
31
+ };
32
+
33
+ type NotificationStatus = 'pending' | 'sent' | 'failed' | 'read';
34
+
35
+ const statusToPrisma: Record<NotificationStatus, PrismaStatus> = {
36
+ pending: 'PENDING',
37
+ sent: 'SENT',
38
+ failed: 'FAILED',
39
+ read: 'READ',
40
+ };
41
+
42
+ const statusFromPrisma: Record<PrismaStatus, NotificationStatus> = {
43
+ PENDING: 'pending',
44
+ SENT: 'sent',
45
+ FAILED: 'failed',
46
+ READ: 'read',
47
+ };
48
+
49
+ export class NotificationRepository {
50
+ constructor(private prisma: PrismaClient) {}
51
+
52
+ // ==========================================
53
+ // NOTIFICATION METHODS
54
+ // ==========================================
55
+
56
+ /**
57
+ * Create a notification
58
+ */
59
+ async createNotification(data: Omit<Notification, 'id' | 'createdAt'>): Promise<Notification> {
60
+ const notification = await this.prisma.notification.create({
61
+ data: {
62
+ userId: data.userId,
63
+ channel: channelToPrisma[data.channel],
64
+ status: statusToPrisma[data.status as NotificationStatus],
65
+ title: data.title,
66
+ body: data.body,
67
+ data: data.data as Prisma.InputJsonValue,
68
+ sentAt: data.sentAt,
69
+ readAt: data.readAt,
70
+ },
71
+ });
72
+
73
+ return this.mapNotificationFromPrisma(notification);
74
+ }
75
+
76
+ /**
77
+ * Get notification by ID
78
+ */
79
+ async getNotificationById(id: string): Promise<Notification | null> {
80
+ const notification = await this.prisma.notification.findUnique({
81
+ where: { id },
82
+ });
83
+
84
+ return notification ? this.mapNotificationFromPrisma(notification) : null;
85
+ }
86
+
87
+ /**
88
+ * Update notification
89
+ */
90
+ async updateNotification(
91
+ id: string,
92
+ data: Partial<Omit<Notification, 'id' | 'createdAt'>>
93
+ ): Promise<Notification | null> {
94
+ try {
95
+ const updateData: Prisma.NotificationUpdateInput = {};
96
+
97
+ if (data.status !== undefined) {
98
+ updateData.status = statusToPrisma[data.status as NotificationStatus];
99
+ }
100
+ if (data.sentAt !== undefined) {
101
+ updateData.sentAt = data.sentAt;
102
+ }
103
+ if (data.readAt !== undefined) {
104
+ updateData.readAt = data.readAt;
105
+ }
106
+ if (data.data !== undefined) {
107
+ updateData.data = data.data as Prisma.InputJsonValue;
108
+ }
109
+
110
+ const notification = await this.prisma.notification.update({
111
+ where: { id },
112
+ data: updateData,
113
+ });
114
+
115
+ return this.mapNotificationFromPrisma(notification);
116
+ } catch (error) {
117
+ if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === 'P2025') {
118
+ return null;
119
+ }
120
+ throw error;
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Get notifications by user ID
126
+ */
127
+ async getNotificationsByUserId(
128
+ userId: string,
129
+ options?: {
130
+ channel?: NotificationChannel;
131
+ status?: NotificationStatus;
132
+ limit?: number;
133
+ offset?: number;
134
+ }
135
+ ): Promise<Notification[]> {
136
+ const where: Prisma.NotificationWhereInput = { userId };
137
+
138
+ if (options?.channel) {
139
+ where.channel = channelToPrisma[options.channel];
140
+ }
141
+ if (options?.status) {
142
+ where.status = statusToPrisma[options.status];
143
+ }
144
+
145
+ const notifications = await this.prisma.notification.findMany({
146
+ where,
147
+ orderBy: { createdAt: 'desc' },
148
+ take: options?.limit || 100,
149
+ skip: options?.offset || 0,
150
+ });
151
+
152
+ return notifications.map((n) => this.mapNotificationFromPrisma(n));
153
+ }
154
+
155
+ /**
156
+ * Get unread count for user
157
+ */
158
+ async getUnreadCount(userId: string): Promise<number> {
159
+ return this.prisma.notification.count({
160
+ where: {
161
+ userId,
162
+ channel: 'IN_APP',
163
+ readAt: null,
164
+ },
165
+ });
166
+ }
167
+
168
+ /**
169
+ * Mark notification as read
170
+ */
171
+ async markAsRead(id: string): Promise<Notification | null> {
172
+ return this.updateNotification(id, {
173
+ status: 'read',
174
+ readAt: new Date(),
175
+ });
176
+ }
177
+
178
+ /**
179
+ * Mark all notifications as read for user
180
+ */
181
+ async markAllAsRead(userId: string): Promise<number> {
182
+ const result = await this.prisma.notification.updateMany({
183
+ where: {
184
+ userId,
185
+ readAt: null,
186
+ },
187
+ data: {
188
+ status: 'READ',
189
+ readAt: new Date(),
190
+ },
191
+ });
192
+
193
+ return result.count;
194
+ }
195
+
196
+ /**
197
+ * Delete notification
198
+ */
199
+ async deleteNotification(id: string): Promise<boolean> {
200
+ try {
201
+ await this.prisma.notification.delete({ where: { id } });
202
+ return true;
203
+ } catch (error) {
204
+ if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === 'P2025') {
205
+ return false;
206
+ }
207
+ throw error;
208
+ }
209
+ }
210
+
211
+ /**
212
+ * Delete old notifications
213
+ */
214
+ async deleteOldNotifications(olderThan: Date): Promise<number> {
215
+ const result = await this.prisma.notification.deleteMany({
216
+ where: {
217
+ createdAt: { lt: olderThan },
218
+ },
219
+ });
220
+
221
+ logger.info({ count: result.count, olderThan }, 'Deleted old notifications');
222
+ return result.count;
223
+ }
224
+
225
+ // ==========================================
226
+ // TEMPLATE METHODS
227
+ // ==========================================
228
+
229
+ /**
230
+ * Create a notification template
231
+ */
232
+ async createTemplate(data: Omit<NotificationTemplate, 'id'>): Promise<NotificationTemplate> {
233
+ const template = await this.prisma.notificationTemplate.create({
234
+ data: {
235
+ name: data.name,
236
+ channel: channelToPrisma[data.channel],
237
+ subject: data.subject,
238
+ body: data.body,
239
+ variables: data.variables as Prisma.InputJsonValue,
240
+ active: true,
241
+ },
242
+ });
243
+
244
+ return this.mapTemplateFromPrisma(template);
245
+ }
246
+
247
+ /**
248
+ * Get template by ID
249
+ */
250
+ async getTemplateById(id: string): Promise<NotificationTemplate | null> {
251
+ const template = await this.prisma.notificationTemplate.findUnique({
252
+ where: { id },
253
+ });
254
+
255
+ return template ? this.mapTemplateFromPrisma(template) : null;
256
+ }
257
+
258
+ /**
259
+ * Get template by name
260
+ */
261
+ async getTemplateByName(name: string): Promise<NotificationTemplate | null> {
262
+ const template = await this.prisma.notificationTemplate.findUnique({
263
+ where: { name },
264
+ });
265
+
266
+ return template ? this.mapTemplateFromPrisma(template) : null;
267
+ }
268
+
269
+ /**
270
+ * Get all templates
271
+ */
272
+ async getAllTemplates(activeOnly = true): Promise<NotificationTemplate[]> {
273
+ const templates = await this.prisma.notificationTemplate.findMany({
274
+ where: activeOnly ? { active: true } : undefined,
275
+ orderBy: { name: 'asc' },
276
+ });
277
+
278
+ return templates.map((t) => this.mapTemplateFromPrisma(t));
279
+ }
280
+
281
+ /**
282
+ * Update template
283
+ */
284
+ async updateTemplate(
285
+ id: string,
286
+ data: Partial<Omit<NotificationTemplate, 'id'>>
287
+ ): Promise<NotificationTemplate | null> {
288
+ try {
289
+ const updateData: Prisma.NotificationTemplateUpdateInput = {};
290
+
291
+ if (data.name !== undefined) updateData.name = data.name;
292
+ if (data.channel !== undefined) updateData.channel = channelToPrisma[data.channel];
293
+ if (data.subject !== undefined) updateData.subject = data.subject;
294
+ if (data.body !== undefined) updateData.body = data.body;
295
+ if (data.variables !== undefined)
296
+ updateData.variables = data.variables as Prisma.InputJsonValue;
297
+
298
+ const template = await this.prisma.notificationTemplate.update({
299
+ where: { id },
300
+ data: updateData,
301
+ });
302
+
303
+ return this.mapTemplateFromPrisma(template);
304
+ } catch (error) {
305
+ if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === 'P2025') {
306
+ return null;
307
+ }
308
+ throw error;
309
+ }
310
+ }
311
+
312
+ /**
313
+ * Delete template
314
+ */
315
+ async deleteTemplate(id: string): Promise<boolean> {
316
+ try {
317
+ await this.prisma.notificationTemplate.delete({ where: { id } });
318
+ return true;
319
+ } catch (error) {
320
+ if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === 'P2025') {
321
+ return false;
322
+ }
323
+ throw error;
324
+ }
325
+ }
326
+
327
+ // ==========================================
328
+ // MAPPING HELPERS
329
+ // ==========================================
330
+
331
+ private mapNotificationFromPrisma(prismaNotification: PrismaNotification): Notification {
332
+ return {
333
+ id: prismaNotification.id,
334
+ userId: prismaNotification.userId,
335
+ channel: channelFromPrisma[prismaNotification.channel],
336
+ status: statusFromPrisma[prismaNotification.status],
337
+ title: prismaNotification.title,
338
+ body: prismaNotification.body,
339
+ data: prismaNotification.data as Record<string, unknown> | undefined,
340
+ sentAt: prismaNotification.sentAt || undefined,
341
+ readAt: prismaNotification.readAt || undefined,
342
+ createdAt: prismaNotification.createdAt,
343
+ };
344
+ }
345
+
346
+ private mapTemplateFromPrisma(prismaTemplate: PrismaTemplate): NotificationTemplate {
347
+ return {
348
+ id: prismaTemplate.id,
349
+ name: prismaTemplate.name,
350
+ channel: channelFromPrisma[prismaTemplate.channel],
351
+ subject: prismaTemplate.subject || undefined,
352
+ body: prismaTemplate.body,
353
+ variables: (prismaTemplate.variables as string[]) || [],
354
+ };
355
+ }
356
+ }