159 lines
5.5 KiB
TypeScript
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();
|