158 lines
4.0 KiB
TypeScript
158 lines
4.0 KiB
TypeScript
import { query } from '../db/client.js'
|
|
|
|
export type AdminNotificationRow = {
|
|
id: number
|
|
notification_type: string
|
|
priority: string
|
|
entity_type: string
|
|
entity_id: number
|
|
dedupe_key: string
|
|
title: string
|
|
body: string
|
|
action_path: string
|
|
payload_json: string | Record<string, unknown>
|
|
visible: boolean
|
|
sound_enabled: boolean
|
|
reminder_count: number
|
|
last_reminded_at: string | null
|
|
status: string
|
|
resolved_at: string | null
|
|
resolution_reason: string
|
|
created_at: string
|
|
updated_at: string
|
|
}
|
|
|
|
export async function createAdminNotification(input: {
|
|
notificationType: string
|
|
priority: string
|
|
entityType: string
|
|
entityId: number
|
|
dedupeKey: string
|
|
title: string
|
|
body: string
|
|
actionPath: string
|
|
payloadJson?: string
|
|
visible?: boolean
|
|
soundEnabled?: boolean
|
|
now: string
|
|
}): Promise<AdminNotificationRow | null> {
|
|
const result = await query<AdminNotificationRow>(
|
|
`
|
|
INSERT INTO admin_notifications (
|
|
notification_type, priority, entity_type, entity_id, dedupe_key,
|
|
title, body, action_path, payload_json, visible, sound_enabled, created_at, updated_at
|
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::jsonb, $10, $11, $12, $12)
|
|
ON CONFLICT (dedupe_key) DO NOTHING
|
|
RETURNING *
|
|
`,
|
|
[
|
|
input.notificationType,
|
|
input.priority,
|
|
input.entityType,
|
|
input.entityId,
|
|
input.dedupeKey,
|
|
input.title,
|
|
input.body,
|
|
input.actionPath,
|
|
input.payloadJson || '{}',
|
|
input.visible !== false,
|
|
input.soundEnabled !== false,
|
|
input.now,
|
|
],
|
|
)
|
|
return result.rows[0] || null
|
|
}
|
|
|
|
export async function getPendingAdminNotificationByDedupeKey(
|
|
dedupeKey: string,
|
|
): Promise<AdminNotificationRow | null> {
|
|
const result = await query<AdminNotificationRow>(
|
|
`SELECT * FROM admin_notifications WHERE dedupe_key = $1 AND status = 'pending' LIMIT 1`,
|
|
[dedupeKey],
|
|
)
|
|
return result.rows[0] || null
|
|
}
|
|
|
|
export async function bumpAdminNotificationReminder(input: {
|
|
dedupeKey: string
|
|
notificationType: string
|
|
title: string
|
|
visible: boolean
|
|
soundEnabled: boolean
|
|
now: string
|
|
}): Promise<AdminNotificationRow | null> {
|
|
const result = await query<AdminNotificationRow>(
|
|
`
|
|
UPDATE admin_notifications
|
|
SET
|
|
notification_type = $1,
|
|
title = $2,
|
|
priority = 'high',
|
|
visible = visible OR $3,
|
|
sound_enabled = $4,
|
|
reminder_count = reminder_count + 1,
|
|
last_reminded_at = $5,
|
|
updated_at = $5
|
|
WHERE dedupe_key = $6
|
|
AND status = 'pending'
|
|
AND reminder_count < 3
|
|
AND (
|
|
last_reminded_at IS NULL
|
|
OR last_reminded_at <= $5::timestamptz - INTERVAL '30 minutes'
|
|
)
|
|
RETURNING *
|
|
`,
|
|
[
|
|
input.notificationType,
|
|
input.title,
|
|
input.visible,
|
|
input.soundEnabled,
|
|
input.now,
|
|
input.dedupeKey,
|
|
],
|
|
)
|
|
return result.rows[0] || null
|
|
}
|
|
|
|
export async function resolveAdminNotificationsByEntity(input: {
|
|
entityType: string
|
|
entityId: number
|
|
reason: string
|
|
now: string
|
|
}): Promise<void> {
|
|
await query(
|
|
`
|
|
UPDATE admin_notifications
|
|
SET status = 'resolved', resolved_at = $1, resolution_reason = $2, updated_at = $1
|
|
WHERE entity_type = $3 AND entity_id = $4 AND status = 'pending'
|
|
`,
|
|
[input.now, input.reason, input.entityType, input.entityId],
|
|
)
|
|
}
|
|
|
|
export async function listPendingAdminNotifications(
|
|
limit: number,
|
|
): Promise<AdminNotificationRow[]> {
|
|
const result = await query<AdminNotificationRow>(
|
|
`
|
|
SELECT *
|
|
FROM admin_notifications
|
|
WHERE status = 'pending' AND visible = TRUE
|
|
ORDER BY
|
|
CASE priority WHEN 'urgent' THEN 3 WHEN 'high' THEN 2 ELSE 1 END DESC,
|
|
updated_at DESC,
|
|
id DESC
|
|
LIMIT $1
|
|
`,
|
|
[limit],
|
|
)
|
|
return result.rows
|
|
}
|
|
|
|
export async function countPendingAdminNotifications(): Promise<number> {
|
|
const result = await query<{ total: number }>(
|
|
`SELECT COUNT(*)::int AS total FROM admin_notifications WHERE status = 'pending' AND visible = TRUE`,
|
|
)
|
|
return Number(result.rows[0]?.total || 0)
|
|
}
|