Files
dashboard/backend/src/services/notification.service.ts
2026-03-12 16:42:21 +01:00

159 lines
5.5 KiB
TypeScript

// =============================================================================
// Notification Service
// =============================================================================
import pool from '../config/database';
import logger from '../utils/logger';
import { Notification, CreateNotificationData } from '../models/notification.model';
function rowToNotification(row: any): Notification {
return {
id: row.id,
user_id: row.user_id,
typ: row.typ,
titel: row.titel,
nachricht: row.nachricht,
schwere: row.schwere,
gelesen: row.gelesen,
gelesen_am: row.gelesen_am ? new Date(row.gelesen_am) : null,
link: row.link ?? null,
quell_id: row.quell_id ?? null,
quell_typ: row.quell_typ ?? null,
erstellt_am: new Date(row.erstellt_am),
};
}
class NotificationService {
/** Returns all notifications for a user (newest first, max 100). */
async getByUser(userId: string): Promise<Notification[]> {
try {
const result = await pool.query(
`SELECT * FROM notifications WHERE user_id = $1 ORDER BY erstellt_am DESC LIMIT 100`,
[userId]
);
return result.rows.map(rowToNotification);
} catch (error) {
logger.error('NotificationService.getByUser failed', { error, userId });
throw new Error('Notifications konnten nicht geladen werden');
}
}
/** Returns the count of unread notifications for a user. */
async getUnreadCount(userId: string): Promise<number> {
try {
const result = await pool.query(
`SELECT COUNT(*) AS cnt FROM notifications WHERE user_id = $1 AND gelesen = FALSE`,
[userId]
);
return parseInt(result.rows[0].cnt, 10);
} catch (error) {
logger.error('NotificationService.getUnreadCount failed', { error, userId });
throw new Error('Ungelesene Notifications konnten nicht gezählt werden');
}
}
/** Marks a single notification as read. Returns false if not found or not owned by user. */
async markAsRead(id: string, userId: string): Promise<boolean> {
try {
const result = await pool.query(
`UPDATE notifications
SET gelesen = TRUE, gelesen_am = NOW()
WHERE id = $1 AND user_id = $2 AND gelesen = FALSE
RETURNING id`,
[id, userId]
);
return (result.rowCount ?? 0) > 0;
} catch (error) {
logger.error('NotificationService.markAsRead failed', { error, id, userId });
throw new Error('Notification konnte nicht als gelesen markiert werden');
}
}
/** Marks all notifications as read for a user. */
async markAllRead(userId: string): Promise<void> {
try {
await pool.query(
`UPDATE notifications SET gelesen = TRUE, gelesen_am = NOW()
WHERE user_id = $1 AND gelesen = FALSE`,
[userId]
);
} catch (error) {
logger.error('NotificationService.markAllRead failed', { error, userId });
throw new Error('Notifications konnten nicht als gelesen markiert werden');
}
}
/**
* Creates a notification. If quell_typ + quell_id are provided the insert is
* silently ignored when an identical unread notification already exists
* (dedup via unique index).
*/
async createNotification(data: CreateNotificationData): Promise<void> {
try {
await pool.query(
`INSERT INTO notifications
(user_id, typ, titel, nachricht, schwere, link, quell_id, quell_typ)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
ON CONFLICT (user_id, quell_typ, quell_id) WHERE NOT gelesen AND quell_typ IS NOT NULL AND quell_id IS NOT NULL
DO NOTHING`,
[
data.user_id,
data.typ,
data.titel,
data.nachricht,
data.schwere ?? 'info',
data.link ?? null,
data.quell_id ?? null,
data.quell_typ ?? null,
]
);
} catch (error) {
logger.error('NotificationService.createNotification failed', { error });
// Non-fatal — don't propagate to callers
}
}
/** Marks all unread notifications of a given quell_typ as read for a user. */
async dismissByType(userId: string, quellTyp: string): Promise<void> {
try {
await pool.query(
`UPDATE notifications SET gelesen = TRUE, gelesen_am = NOW()
WHERE user_id = $1 AND quell_typ = $2 AND gelesen = FALSE`,
[userId, quellTyp]
);
} catch (error) {
logger.error('NotificationService.dismissByType failed', { error, userId, quellTyp });
throw new Error('Notifications konnten nicht als gelesen markiert werden');
}
}
/** Deletes all read notifications for a user. */
async deleteAllRead(userId: string): Promise<void> {
try {
await pool.query(
`DELETE FROM notifications WHERE user_id = $1 AND gelesen = TRUE`,
[userId]
);
} catch (error) {
logger.error('NotificationService.deleteAllRead failed', { error, userId });
throw new Error('Gelesene Notifications konnten nicht gelöscht werden');
}
}
/** Deletes read notifications older than 90 days for all users. */
async deleteOldRead(): Promise<void> {
try {
const result = await pool.query(
`DELETE FROM notifications WHERE gelesen = TRUE AND gelesen_am < NOW() - INTERVAL '90 days'`
);
if ((result.rowCount ?? 0) > 0) {
logger.info(`NotificationService.deleteOldRead: removed ${result.rowCount} old notifications`);
}
} catch (error) {
logger.error('NotificationService.deleteOldRead failed', { error });
}
}
}
export default new NotificationService();