fix: tighten diagnosis route typing

Replace unsafe `as any` casts with proper type guards using `isDiseaseSlug()`
and explicit type casting to `DiagnosisStatus` and `DiseaseCatalogItem`.
Replace non-null assertions with explicit null checks that return
`serviceUnavailable()` errors when database operations fail.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Asep Haryana Saputra
2026-05-22 16:15:45 +00:00
co-authored by Claude Opus 4.7
parent 1a36ae5cbc
commit 2e692a4b8c
+14 -8
View File
@@ -1,5 +1,6 @@
import { Elysia } from 'elysia'; import { Elysia } from 'elysia';
import type { DiagnosisPrediction, DiagnosisRecord, ExpertReviewRecord } from '@zeavis/shared'; import type { DiagnosisPrediction, DiagnosisRecord, ExpertReviewRecord, DiseaseCatalogItem } from '@zeavis/shared';
import { DiagnosisStatus, DiseaseSlug, isDiseaseSlug } from '@zeavis/shared';
import { and, desc, eq } from 'drizzle-orm'; import { and, desc, eq } from 'drizzle-orm';
import { createDbClient } from '../db/client'; import { createDbClient } from '../db/client';
import { diagnoses, diagnosisPredictions, diseaseCatalog, expertReviews, users } from '../db/schema'; import { diagnoses, diagnosisPredictions, diseaseCatalog, expertReviews, users } from '../db/schema';
@@ -7,7 +8,6 @@ import { badGateway, badRequest, notFound, serviceUnavailable, unauthorized } fr
import { getCurrentUser } from '../lib/auth'; import { getCurrentUser } from '../lib/auth';
import { classifyImage } from '../lib/image-model'; import { classifyImage } from '../lib/image-model';
import { uploadImageToStorage } from '../lib/uploader-client'; import { uploadImageToStorage } from '../lib/uploader-client';
import { toDisease } from '../lib/disease-mappers';
import { env } from '../config/env'; import { env } from '../config/env';
function toReview(row: any): ExpertReviewRecord | null { function toReview(row: any): ExpertReviewRecord | null {
@@ -93,18 +93,22 @@ async function loadDiagnosisRecord(id: string, userId: string | null, expertAcce
const predictions: DiagnosisPrediction[] = predictionRows.map((prediction) => ({ const predictions: DiagnosisPrediction[] = predictionRows.map((prediction) => ({
id: prediction.id, id: prediction.id,
diseaseSlug: prediction.diseaseSlug as any, diseaseSlug: prediction.diseaseSlug && isDiseaseSlug(prediction.diseaseSlug) ? prediction.diseaseSlug : null,
modelLabel: prediction.modelLabel, modelLabel: prediction.modelLabel,
confidence: prediction.confidence, confidence: prediction.confidence,
rank: prediction.rank, rank: prediction.rank,
})); }));
const predictedDiseaseSlug = row.predictedDiseaseSlug && isDiseaseSlug(row.predictedDiseaseSlug) ? row.predictedDiseaseSlug : null;
const status = row.status as DiagnosisStatus;
const disease = row.disease && row.disease.slug ? (row.disease as DiseaseCatalogItem) : null;
return { return {
id: row.id, id: row.id,
userId: row.userId, userId: row.userId,
predictedDiseaseSlug: row.predictedDiseaseSlug as any, predictedDiseaseSlug,
confidence: row.confidence, confidence: row.confidence,
status: row.status as any, status,
failureReason: row.failureReason, failureReason: row.failureReason,
imageUrl: row.imageUrl, imageUrl: row.imageUrl,
uploaderPublicId: row.uploaderPublicId, uploaderPublicId: row.uploaderPublicId,
@@ -113,7 +117,7 @@ async function loadDiagnosisRecord(id: string, userId: string | null, expertAcce
imageSizeBytes: row.imageSizeBytes, imageSizeBytes: row.imageSizeBytes,
createdAt: row.createdAt.toISOString(), createdAt: row.createdAt.toISOString(),
updatedAt: row.updatedAt.toISOString(), updatedAt: row.updatedAt.toISOString(),
disease: row.disease?.slug ? toDisease(row.disease as any) : null, disease,
predictions, predictions,
latestReview: toReview(row), latestReview: toReview(row),
}; };
@@ -183,7 +187,8 @@ export const diagnosisRoutes = new Elysia({ prefix: '/api/v1' })
); );
const record = await loadDiagnosisRecord(inserted[0].id, user.id, false); const record = await loadDiagnosisRecord(inserted[0].id, user.id, false);
return record!; if (!record) return serviceUnavailable('Database unavailable');
return record;
} catch (error) { } catch (error) {
const inserted = await db const inserted = await db
.insert(diagnoses) .insert(diagnoses)
@@ -200,7 +205,8 @@ export const diagnosisRoutes = new Elysia({ prefix: '/api/v1' })
.returning(); .returning();
const record = await loadDiagnosisRecord(inserted[0].id, user.id, false); const record = await loadDiagnosisRecord(inserted[0].id, user.id, false);
return record!; if (!record) return serviceUnavailable('Database unavailable');
return record;
} }
}) })
.get('/diagnoses', async ({ request }) => { .get('/diagnoses', async ({ request }) => {