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
@@ -56,7 +56,7 @@ export async function POST(
})
if (capa.owner_user_id) {
await createInAppNotifications(supabase, [{
await createInAppNotifications([{
userId: capa.owner_user_id,
title: body.verdict === 'verified'
? 'Your CAPA action was verified'
+1 -1
View File
@@ -67,7 +67,7 @@ export async function POST(request: NextRequest) {
p_new_value: { incident_id, description, owner_user_id, department, due_date },
})
await createInAppNotifications(supabase, [{
await createInAppNotifications([{
userId: owner_user_id,
title: `CAPA assigned to you, due ${due_date}`,
link: `/hse/capa/${capa.id}`,
+1 -3
View File
@@ -2,7 +2,6 @@ export const dynamic = 'force-dynamic'
import { timingSafeEqual } from 'crypto'
import { NextRequest, NextResponse } from 'next/server'
import { createClient } from '@/lib/supabase/server'
import { escalateOverdueCapa } from '@/lib/notifications/capa-escalation'
export async function GET(request: NextRequest) {
@@ -15,7 +14,6 @@ export async function GET(request: NextRequest) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const supabase = await createClient()
const { notified } = await escalateOverdueCapa(supabase)
const { notified } = await escalateOverdueCapa()
return NextResponse.json({ ok: true, notified })
}
+1 -3
View File
@@ -2,7 +2,6 @@ export const dynamic = 'force-dynamic'
import { timingSafeEqual } from 'crypto'
import { NextRequest, NextResponse } from 'next/server'
import { createClient } from '@/lib/supabase/server'
import { sendEffectivenessRecheckNotifications } from '@/lib/notifications/effectiveness-recheck'
export async function GET(request: NextRequest) {
@@ -15,7 +14,6 @@ export async function GET(request: NextRequest) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const supabase = await createClient()
const { notified } = await sendEffectivenessRecheckNotifications(supabase)
const { notified } = await sendEffectivenessRecheckNotifications()
return NextResponse.json({ ok: true, notified })
}
+1 -1
View File
@@ -92,7 +92,7 @@ export async function POST() {
return NextResponse.json({ error: 'Rate limit: wait 60 seconds between AI requests' }, { status: 429 })
}
const deepseekKey = await getApiKey(supabase, 'DEEPSEEK_API_KEY')
const deepseekKey = await getApiKey('DEEPSEEK_API_KEY')
const client = createDeepSeekClient(deepseekKey)
let res: Awaited<ReturnType<typeof client.chat.completions.create>>
+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({