feat(db): phase 4 group 1 — lib/ settings + notifications to Drizzle

Convert lib/settings.ts, lib/notifications/{in-app,email,capa-escalation,
effectiveness-recheck}.ts from Supabase PostgREST to Drizzle asAdmin queries.
Drop supabase arg from all call sites in app/api/ and cron routes. Rewrite
notification unit tests to mock @/lib/db/with-user instead of SupabaseClient.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-07-23 16:37:51 +08:00
co-authored by Claude Sonnet 4.6
parent d18d29168a
commit 25f923f530
19 changed files with 296 additions and 300 deletions
+1 -1
View File
@@ -28,7 +28,7 @@ export async function POST(
if ((recentCount ?? 0) > 0)
return NextResponse.json({ error: 'Rate limited — please wait 60 seconds' }, { status: 429 })
const deepseekKey = await getApiKey(supabase, 'DEEPSEEK_API_KEY')
const deepseekKey = await getApiKey('DEEPSEEK_API_KEY')
const client = createDeepSeekClient(deepseekKey)
const { data: incident } = await supabase
@@ -28,7 +28,7 @@ export async function POST(
if ((recentCount ?? 0) > 0)
return NextResponse.json({ error: 'Rate limited — please wait 60 seconds' }, { status: 429 })
const deepseekKey = await getApiKey(supabase, 'DEEPSEEK_API_KEY')
const deepseekKey = await getApiKey('DEEPSEEK_API_KEY')
const client = createDeepSeekClient(deepseekKey)
const { data: incident } = await supabase
+1 -1
View File
@@ -51,7 +51,7 @@ export async function POST(
})
if (incident.reported_by) {
await createInAppNotifications(supabase, [{
await createInAppNotifications([{
userId: incident.reported_by,
title: `Your incident report ${incident.reference_no ?? ''} has been closed`,
link: '/reporter',
+1 -1
View File
@@ -18,7 +18,7 @@ export async function GET(
const supabase = await createClient()
const googleAiKey = await getApiKey(supabase, 'GOOGLE_AI_API_KEY')
const googleAiKey = await getApiKey('GOOGLE_AI_API_KEY')
const { data: incident } = await supabase
.from('incidents')
+1 -1
View File
@@ -22,7 +22,7 @@ export async function POST(request: NextRequest) {
if ((recentCount ?? 0) > 0)
return NextResponse.json({ error: 'Rate limited — please wait 60 seconds' }, { status: 429 })
const deepseekKey = await getApiKey(supabase, 'DEEPSEEK_API_KEY')
const deepseekKey = await getApiKey('DEEPSEEK_API_KEY')
const client = createDeepSeekClient(deepseekKey)
let body: { description?: string; incident_type?: string }
+3 -4
View File
@@ -165,8 +165,8 @@ async function handlePost(request: Request) {
// WhatsApp alert — fire-and-forget alongside email
;(async () => {
try {
const phoneNumberId = await getApiKey(supabase, 'META_WHATSAPP_PHONE_NUMBER_ID')
const accessToken = await getApiKey(supabase, 'META_WHATSAPP_ACCESS_TOKEN')
const phoneNumberId = await getApiKey('META_WHATSAPP_PHONE_NUMBER_ID')
const accessToken = await getApiKey('META_WHATSAPP_ACCESS_TOKEN')
const { data: siteData } = await supabase
.from('sites').select('name').eq('id', zone.site_id).single()
const siteName = (siteData as { name: string } | null)?.name ?? 'Unknown'
@@ -177,7 +177,6 @@ async function handlePost(request: Request) {
.eq('site_id', zone.site_id)
await createInAppNotifications(
supabase,
(recipients ?? []).map((r: { id: string }) => ({
userId: r.id,
title: `New ${input.incident_type.replace(/_/g, ' ')} incident ${incident.reference_no ?? ''} at ${siteName}`,
@@ -205,7 +204,7 @@ async function handlePost(request: Request) {
// Embed description asynchronously for future similarity search
const supabaseForEmbed = supabase
import('@/lib/settings').then(({ getApiKey }) =>
getApiKey(supabaseForEmbed, 'GOOGLE_AI_API_KEY').then(googleAiKey =>
getApiKey('GOOGLE_AI_API_KEY').then(googleAiKey =>
import('@/lib/claude/embed').then(({ embedText }) =>
embedText(input.description.trim(), googleAiKey).then(embedding =>
supabase.from('incidents').update({