Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions app/api/rast/genome/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/**
* Server-side proxy for RAST genome data.
*
* Proxies GET /api/rast/genome from the configured modelseed-api backend.
* Runs server-side so it can access PATRIC_TOKEN and avoid CORS restrictions.
*/
import { NextRequest, NextResponse } from 'next/server';

function buildUpstreamCandidates(params: string): string[] {
const configured = process.env.NEXT_PUBLIC_API_BASE_URL?.replace(/\/+$/, '');
const candidates = new Set<string>();
if (configured) {
candidates.add(`${configured}/api/rast/genome?${params}`);
}
candidates.add(`https://staging.modelseed.org/PMS/api/rast/genome?${params}`);
candidates.add(`https://modelseed.org/PMS/api/rast/genome?${params}`);
return Array.from(candidates);
}

export async function GET(request: NextRequest): Promise<NextResponse> {
const token =
request.headers.get('authorization') ||
process.env.PATRIC_TOKEN;

if (!token) {
return NextResponse.json(
{ error: 'Authentication required' },
{ status: 401 },
);
}

const { searchParams } = new URL(request.url);
const genomeId = searchParams.get('genome_id');
const jobId = searchParams.get('job_id');

if (!genomeId) {
return NextResponse.json(
{ error: 'genome_id is required' },
{ status: 400 },
);
}

const params = new URLSearchParams({ genome_id: genomeId });
if (jobId) params.set('job_id', jobId);

const headers: HeadersInit = {
Accept: 'application/json',
Authorization: token,
};

const upstreamCandidates = buildUpstreamCandidates(params.toString());
let lastStatus = 503;
let lastDetail: string | null = null;

for (const url of upstreamCandidates) {
try {
const res = await fetch(url, { headers, cache: 'no-store' });

if (res.ok) {
const data: unknown = await res.json();
return NextResponse.json(data);
}

lastStatus = res.status;
try {
const body = await res.json() as Record<string, unknown>;
lastDetail = typeof body.detail === 'string' ? body.detail : null;
} catch {
// ignore JSON parse failure
}

console.warn(`[rast/genome proxy] ${url} → HTTP ${res.status}${lastDetail ? ': ' + lastDetail : ''}`);
} catch (err) {
console.warn(
`[rast/genome proxy] ${url} unreachable:`,
err instanceof Error ? err.message : err,
);
}
}

return NextResponse.json(
{
error: 'RAST genome data service unavailable',
detail: lastDetail ?? 'All upstream endpoints failed',
},
{ status: lastStatus },
);
}
3 changes: 2 additions & 1 deletion components/build-model/RastGenomePreviewDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export default function RastGenomePreviewDialog({ open, job, onProceed, onClose
if (!open || !job) return;

const genomeId = job.genome_id || job.id;
const jobId = job.id;
if (!genomeId) return;

let cancelled = false;
Expand All @@ -40,7 +41,7 @@ export default function RastGenomePreviewDialog({ open, job, onProceed, onClose
setGenomeData(null);
setError(null);

getRastGenomeData(genomeId)
getRastGenomeData(genomeId, jobId)
.then((data) => {
if (cancelled) return;
setGenomeData(data);
Expand Down
33 changes: 28 additions & 5 deletions lib/api/modelseed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
MODELSEED_API_URL,
MODELSEED_SUPPORT_URL,
USE_MODELSEED_API,
USE_NEW_PROXY,

Check warning on line 14 in lib/api/modelseed.ts

View workflow job for this annotation

GitHub Actions / Verify (lint, typecheck, test, build, audit)

'USE_NEW_PROXY' is defined but never used
WORKSPACE_URL,
} from './config';
import { getStoredAuthUsername, withRawTokenAuth } from './requestAuth';
Expand Down Expand Up @@ -65,13 +65,36 @@
/**
* Fetch genome annotation data from RAST.
*
* Calls MSSeedSupportServer.get_rast_genome_data over JSON-RPC.
* Returns genome metadata including taxonomy, domain, features, and contigs.
* Tries José's modelseed-api endpoint first (GET /api/rast/genome),
* then falls back to MSSS JSON-RPC (MSSeedSupportServer.getRastGenomeData).
*
* @param genomeId - RAST genome ID to fetch data for
* @param jobId - Optional RAST job ID (needed for the modelseed-api endpoint)
* @returns Promise resolving to genome data record
*/
export async function getRastGenomeData(genomeId: string): Promise<Record<string, unknown>> {
export async function getRastGenomeData(genomeId: string, jobId?: string): Promise<Record<string, unknown>> {
// Try José's modelseed-api endpoint first
if (jobId) {
try {
const token = getStoredAuthUsername();
const headers: Record<string, string> = { Accept: 'application/json' };
if (token) {
headers['Authorization'] = token;
}
const params = new URLSearchParams({ genome_id: genomeId, job_id: jobId });
const res = await fetch(`/api/rast/genome?${params}`, { headers });
if (res.ok) {
const data: unknown = await res.json();
if (data && typeof data === 'object') {
return data as Record<string, unknown>;
}
}
} catch {
// Fall through to MSSS
}
}

// Fallback: MSSS JSON-RPC
const response = await fetch(MODELSEED_SUPPORT_URL, {
method: 'POST',
headers: withRawTokenAuth(
Expand All @@ -83,9 +106,9 @@
),
body: JSON.stringify({
version: '1.1',
method: 'MSSeedSupportServer.get_rast_genome_data',
method: 'MSSeedSupportServer.getRastGenomeData',
id: 'get-rast-genome-data',
params: [{ genome_id: genomeId }],
params: [{ genome: genomeId }],
}),
});

Expand Down Expand Up @@ -850,7 +873,7 @@
}
}

async function listMediaViaWorkspaceLs(path: string): Promise<ModelseedMediaSummary[]> {

Check warning on line 876 in lib/api/modelseed.ts

View workflow job for this annotation

GitHub Actions / Verify (lint, typecheck, test, build, audit)

'listMediaViaWorkspaceLs' is defined but never used
const response = await fetch(`${WORKSPACE_URL}/ls`, {
method: 'POST',
headers: withRawTokenAuth(
Expand Down
Loading