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.
- package/.claude/settings.local.json +30 -0
- package/.github/CODEOWNERS +18 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +46 -0
- package/.github/dependabot.yml +59 -0
- package/.github/workflows/ci.yml +188 -0
- package/.github/workflows/release.yml +195 -0
- package/AUDIT.md +602 -0
- package/LICENSE +21 -0
- package/README.md +1102 -1
- package/dist/cli/index.cjs +2026 -2168
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +2026 -2168
- package/dist/cli/index.js.map +1 -1
- package/dist/index.cjs +595 -616
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +114 -52
- package/dist/index.d.ts +114 -52
- package/dist/index.js +595 -616
- package/dist/index.js.map +1 -1
- package/docs/CLI-001_MULTI_DB_PLAN.md +546 -0
- package/docs/DATABASE_MULTI_ORM.md +399 -0
- package/docs/PHASE1_BREAKDOWN.md +346 -0
- package/docs/PROGRESS.md +550 -0
- package/docs/modules/ANALYTICS.md +226 -0
- package/docs/modules/API-VERSIONING.md +252 -0
- package/docs/modules/AUDIT.md +192 -0
- package/docs/modules/AUTH.md +431 -0
- package/docs/modules/CACHE.md +346 -0
- package/docs/modules/EMAIL.md +254 -0
- package/docs/modules/FEATURE-FLAG.md +291 -0
- package/docs/modules/I18N.md +294 -0
- package/docs/modules/MEDIA-PROCESSING.md +281 -0
- package/docs/modules/MFA.md +266 -0
- package/docs/modules/NOTIFICATION.md +311 -0
- package/docs/modules/OAUTH.md +237 -0
- package/docs/modules/PAYMENT.md +804 -0
- package/docs/modules/QUEUE.md +540 -0
- package/docs/modules/RATE-LIMIT.md +339 -0
- package/docs/modules/SEARCH.md +288 -0
- package/docs/modules/SECURITY.md +327 -0
- package/docs/modules/SESSION.md +382 -0
- package/docs/modules/SWAGGER.md +305 -0
- package/docs/modules/UPLOAD.md +296 -0
- package/docs/modules/USER.md +505 -0
- package/docs/modules/VALIDATION.md +294 -0
- package/docs/modules/WEBHOOK.md +270 -0
- package/docs/modules/WEBSOCKET.md +691 -0
- package/package.json +53 -38
- package/prisma/schema.prisma +395 -1
- package/src/cli/commands/add-module.ts +520 -87
- package/src/cli/commands/db.ts +3 -4
- package/src/cli/commands/docs.ts +256 -6
- package/src/cli/commands/generate.ts +12 -19
- package/src/cli/commands/init.ts +384 -214
- package/src/cli/index.ts +0 -4
- package/src/cli/templates/repository.ts +6 -1
- package/src/cli/templates/routes.ts +6 -21
- package/src/cli/utils/docs-generator.ts +6 -7
- package/src/cli/utils/env-manager.ts +717 -0
- package/src/cli/utils/field-parser.ts +16 -7
- package/src/cli/utils/interactive-prompt.ts +223 -0
- package/src/cli/utils/template-manager.ts +346 -0
- package/src/config/database.config.ts +183 -0
- package/src/config/env.ts +0 -10
- package/src/config/index.ts +0 -14
- package/src/core/server.ts +1 -1
- package/src/database/adapters/mongoose.adapter.ts +132 -0
- package/src/database/adapters/prisma.adapter.ts +118 -0
- package/src/database/connection.ts +190 -0
- package/src/database/interfaces/database.interface.ts +85 -0
- package/src/database/interfaces/index.ts +7 -0
- package/src/database/interfaces/repository.interface.ts +129 -0
- package/src/database/models/mongoose/index.ts +7 -0
- package/src/database/models/mongoose/payment.schema.ts +347 -0
- package/src/database/models/mongoose/user.schema.ts +154 -0
- package/src/database/prisma.ts +1 -4
- package/src/database/redis.ts +101 -0
- package/src/database/repositories/mongoose/index.ts +7 -0
- package/src/database/repositories/mongoose/payment.repository.ts +380 -0
- package/src/database/repositories/mongoose/user.repository.ts +255 -0
- package/src/database/seed.ts +6 -1
- package/src/index.ts +9 -20
- package/src/middleware/security.ts +2 -6
- package/src/modules/analytics/analytics.routes.ts +80 -0
- package/src/modules/analytics/analytics.service.ts +364 -0
- package/src/modules/analytics/index.ts +18 -0
- package/src/modules/analytics/types.ts +180 -0
- package/src/modules/api-versioning/index.ts +15 -0
- package/src/modules/api-versioning/types.ts +86 -0
- package/src/modules/api-versioning/versioning.middleware.ts +120 -0
- package/src/modules/api-versioning/versioning.routes.ts +54 -0
- package/src/modules/api-versioning/versioning.service.ts +189 -0
- package/src/modules/audit/audit.repository.ts +206 -0
- package/src/modules/audit/audit.service.ts +27 -59
- package/src/modules/auth/auth.controller.ts +2 -2
- package/src/modules/auth/auth.middleware.ts +3 -9
- package/src/modules/auth/auth.routes.ts +10 -107
- package/src/modules/auth/auth.service.ts +126 -23
- package/src/modules/auth/index.ts +3 -4
- package/src/modules/cache/cache.service.ts +367 -0
- package/src/modules/cache/index.ts +10 -0
- package/src/modules/cache/types.ts +44 -0
- package/src/modules/email/email.service.ts +3 -10
- package/src/modules/email/templates.ts +2 -8
- package/src/modules/feature-flag/feature-flag.repository.ts +303 -0
- package/src/modules/feature-flag/feature-flag.routes.ts +247 -0
- package/src/modules/feature-flag/feature-flag.service.ts +566 -0
- package/src/modules/feature-flag/index.ts +20 -0
- package/src/modules/feature-flag/types.ts +192 -0
- package/src/modules/i18n/i18n.middleware.ts +186 -0
- package/src/modules/i18n/i18n.routes.ts +191 -0
- package/src/modules/i18n/i18n.service.ts +456 -0
- package/src/modules/i18n/index.ts +18 -0
- package/src/modules/i18n/types.ts +118 -0
- package/src/modules/media-processing/index.ts +17 -0
- package/src/modules/media-processing/media-processing.routes.ts +111 -0
- package/src/modules/media-processing/media-processing.service.ts +245 -0
- package/src/modules/media-processing/types.ts +156 -0
- package/src/modules/mfa/index.ts +20 -0
- package/src/modules/mfa/mfa.repository.ts +206 -0
- package/src/modules/mfa/mfa.routes.ts +595 -0
- package/src/modules/mfa/mfa.service.ts +572 -0
- package/src/modules/mfa/totp.ts +150 -0
- package/src/modules/mfa/types.ts +57 -0
- package/src/modules/notification/index.ts +20 -0
- package/src/modules/notification/notification.repository.ts +356 -0
- package/src/modules/notification/notification.service.ts +483 -0
- package/src/modules/notification/types.ts +119 -0
- package/src/modules/oauth/index.ts +20 -0
- package/src/modules/oauth/oauth.repository.ts +219 -0
- package/src/modules/oauth/oauth.routes.ts +446 -0
- package/src/modules/oauth/oauth.service.ts +293 -0
- package/src/modules/oauth/providers/apple.provider.ts +250 -0
- package/src/modules/oauth/providers/facebook.provider.ts +181 -0
- package/src/modules/oauth/providers/github.provider.ts +248 -0
- package/src/modules/oauth/providers/google.provider.ts +189 -0
- package/src/modules/oauth/providers/twitter.provider.ts +214 -0
- package/src/modules/oauth/types.ts +94 -0
- package/src/modules/payment/index.ts +19 -0
- package/src/modules/payment/payment.repository.ts +733 -0
- package/src/modules/payment/payment.routes.ts +390 -0
- package/src/modules/payment/payment.service.ts +354 -0
- package/src/modules/payment/providers/mobile-money.provider.ts +274 -0
- package/src/modules/payment/providers/paypal.provider.ts +190 -0
- package/src/modules/payment/providers/stripe.provider.ts +215 -0
- package/src/modules/payment/types.ts +140 -0
- package/src/modules/queue/cron.ts +438 -0
- package/src/modules/queue/index.ts +87 -0
- package/src/modules/queue/queue.routes.ts +600 -0
- package/src/modules/queue/queue.service.ts +842 -0
- package/src/modules/queue/types.ts +222 -0
- package/src/modules/queue/workers.ts +366 -0
- package/src/modules/rate-limit/index.ts +59 -0
- package/src/modules/rate-limit/rate-limit.middleware.ts +134 -0
- package/src/modules/rate-limit/rate-limit.routes.ts +269 -0
- package/src/modules/rate-limit/rate-limit.service.ts +348 -0
- package/src/modules/rate-limit/stores/memory.store.ts +165 -0
- package/src/modules/rate-limit/stores/redis.store.ts +322 -0
- package/src/modules/rate-limit/types.ts +153 -0
- package/src/modules/search/adapters/elasticsearch.adapter.ts +326 -0
- package/src/modules/search/adapters/meilisearch.adapter.ts +261 -0
- package/src/modules/search/adapters/memory.adapter.ts +278 -0
- package/src/modules/search/index.ts +21 -0
- package/src/modules/search/search.service.ts +234 -0
- package/src/modules/search/types.ts +214 -0
- package/src/modules/security/index.ts +40 -0
- package/src/modules/security/sanitize.ts +223 -0
- package/src/modules/security/security-audit.service.ts +388 -0
- package/src/modules/security/security.middleware.ts +398 -0
- package/src/modules/session/index.ts +3 -0
- package/src/modules/session/session.repository.ts +159 -0
- package/src/modules/session/session.service.ts +340 -0
- package/src/modules/session/types.ts +38 -0
- package/src/modules/swagger/index.ts +7 -1
- package/src/modules/swagger/schema-builder.ts +16 -4
- package/src/modules/swagger/swagger.service.ts +9 -10
- package/src/modules/swagger/types.ts +0 -2
- package/src/modules/upload/index.ts +14 -0
- package/src/modules/upload/types.ts +83 -0
- package/src/modules/upload/upload.repository.ts +199 -0
- package/src/modules/upload/upload.routes.ts +311 -0
- package/src/modules/upload/upload.service.ts +448 -0
- package/src/modules/user/index.ts +3 -3
- package/src/modules/user/user.controller.ts +15 -9
- package/src/modules/user/user.repository.ts +237 -113
- package/src/modules/user/user.routes.ts +39 -164
- package/src/modules/user/user.service.ts +4 -3
- package/src/modules/validation/validator.ts +12 -17
- package/src/modules/webhook/index.ts +91 -0
- package/src/modules/webhook/retry.ts +196 -0
- package/src/modules/webhook/signature.ts +135 -0
- package/src/modules/webhook/types.ts +181 -0
- package/src/modules/webhook/webhook.repository.ts +358 -0
- package/src/modules/webhook/webhook.routes.ts +442 -0
- package/src/modules/webhook/webhook.service.ts +457 -0
- package/src/modules/websocket/features.ts +504 -0
- package/src/modules/websocket/index.ts +106 -0
- package/src/modules/websocket/middlewares.ts +298 -0
- package/src/modules/websocket/types.ts +181 -0
- package/src/modules/websocket/websocket.service.ts +692 -0
- package/src/utils/errors.ts +7 -0
- package/src/utils/pagination.ts +4 -1
- package/tests/helpers/db-check.ts +79 -0
- package/tests/integration/auth-redis.test.ts +94 -0
- package/tests/integration/cache-redis.test.ts +387 -0
- package/tests/integration/mongoose-repositories.test.ts +410 -0
- package/tests/integration/payment-prisma.test.ts +637 -0
- package/tests/integration/queue-bullmq.test.ts +417 -0
- package/tests/integration/user-prisma.test.ts +441 -0
- package/tests/integration/websocket-socketio.test.ts +552 -0
- package/tests/setup.ts +11 -9
- package/vitest.config.ts +3 -8
- package/npm-cache/_cacache/content-v2/sha512/1c/d0/03440d500a0487621aad1d6402978340698976602046db8e24fa03c01ee6c022c69b0582f969042d9442ee876ac35c038e960dd427d1e622fa24b8eb7dba +0 -0
- package/npm-cache/_cacache/content-v2/sha512/42/55/28b493ca491833e5aab0e9c3108d29ab3f36c248ca88f45d4630674fce9130959e56ae308797ac2b6328fa7f09a610b9550ed09cb971d039876d293fc69d +0 -0
- package/npm-cache/_cacache/content-v2/sha512/e0/12/f360dc9315ee5f17844a0c8c233ee6bf7c30837c4a02ea0d56c61c7f7ab21c0e958e50ed2c57c59f983c762b93056778c9009b2398ffc26def0183999b13 +0 -0
- package/npm-cache/_cacache/content-v2/sha512/ed/b0/fae1161902898f4c913c67d7f6cdf6be0665aec3b389b9c4f4f0a101ca1da59badf1b59c4e0030f5223023b8d63cfe501c46a32c20c895d4fb3f11ca2232 +0 -0
- 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
|
+
}
|