import type { PaginatedResponse, Credential, Environment, ScenarioCredential, Session, Scenario, ScenarioRun, ScenarioRunDetail, ScenarioRunStep, ScenarioStep, Snippet, } from './types'; import { emitApiErrorToast } from '../lib/toast-events'; // API calls are versioned under /api/v1 by default. // Set VITE_API_URL (for example, http://localhost:13000/api/v1) to use a different origin. const BASE_URL = ((import.meta.env.VITE_API_URL as string | undefined) ?? '/api/v1').replace( /\/$/, '', ); async function request(path: string, init?: RequestInit): Promise { let res: Response; try { res = await fetch(`${BASE_URL}${path}`, { headers: { 'Content-Type': 'application/json', ...init?.headers }, ...init, }); } catch (err) { const message = (err as Error).message || 'Network request failed'; emitApiErrorToast(message); throw new Error(message); } if (!res.ok) { const text = await res.text().catch(() => res.statusText); const message = `${res.status} ${text}`; emitApiErrorToast(message); throw new Error(message); } const contentLength = res.headers.get('content-length'); if (res.status === 204 || contentLength === '0') return undefined as T; const contentType = res.headers.get('content-type')?.toLowerCase() ?? ''; const text = await res.text(); if (!text) return undefined as T; if (contentType.includes('application/json')) { return JSON.parse(text) as T; } return text as T; } // ── Credentials ─────────────────────────────────────────────────────────────── export const credentials = { list(page = 1, limit = 50): Promise> { return request(`/credentials?page=${page}&limit=${limit}`); }, get(id: string): Promise { return request(`/credentials/${id}`); }, create(name: string, data?: string): Promise { return request('/credentials', { method: 'POST', body: JSON.stringify({ name, data }), }); }, update(id: string, patch: Partial>): Promise { return request(`/credentials/${id}`, { method: 'PATCH', body: JSON.stringify(patch), }); }, remove(id: string): Promise { return request(`/credentials/${id}`, { method: 'DELETE' }); }, exportCredential(id: string): Promise { return request(`/credentials/${id}/export`); }, importCredential(payload: unknown): Promise { return request('/credentials/import', { method: 'POST', body: JSON.stringify(payload), }); }, }; // ── Snippets ────────────────────────────────────────────────────────────────── export const snippets = { list(page = 1, limit = 50): Promise> { return request(`/snippets?page=${page}&limit=${limit}`); }, get(id: string): Promise { return request(`/snippets/${id}`); }, create( payload: Pick & { description?: string }, ): Promise { return request('/snippets', { method: 'POST', body: JSON.stringify(payload), }); }, update( id: string, patch: Partial>, ): Promise { return request(`/snippets/${id}`, { method: 'PATCH', body: JSON.stringify(patch), }); }, remove(id: string): Promise { return request(`/snippets/${id}`, { method: 'DELETE' }); }, exportSnippet(id: string): Promise { return request(`/snippets/${id}/export`); }, importSnippet(payload: unknown): Promise { return request('/snippets/import', { method: 'POST', body: JSON.stringify(payload), }); }, }; // ── Environments ────────────────────────────────────────────────────────────── export const environments = { list(page = 1, limit = 50): Promise> { return request(`/environments?page=${page}&limit=${limit}`); }, get(id: string): Promise { return request(`/environments/${id}`); }, create(name: string, data: Environment['data'], description?: string): Promise { return request('/environments', { method: 'POST', body: JSON.stringify({ name, description, data }), }); }, update(id: string, patch: Partial>): Promise { return request(`/environments/${id}`, { method: 'PATCH', body: JSON.stringify(patch), }); }, remove(id: string): Promise { return request(`/environments/${id}`, { method: 'DELETE' }); }, exportEnvironment(id: string): Promise { return request(`/environments/${id}/export`); }, importEnvironment(payload: unknown): Promise { return request('/environments/import', { method: 'POST', body: JSON.stringify(payload), }); }, }; // ── Sessions ────────────────────────────────────────────────────────────────── export const sessions = { list(page = 1, limit = 50, orderBy = 'id', orderDir: 'ASC' | 'DESC' = 'DESC'): Promise> { return request(`/sessions?page=${page}&limit=${limit}&orderBy=${orderBy}&orderDir=${orderDir}`); }, get(id: string): Promise { return request(`/sessions/${id}`); }, remove(id: string): Promise { return request(`/sessions/${id}`, { method: 'DELETE' }); }, }; // ── Scenarios ───────────────────────────────────────────────────────────────── export const scenarios = { list( page = 1, limit = 50, orderBy = 'updatedAt', orderDir: 'ASC' | 'DESC' = 'DESC', ): Promise> { return request(`/scenarios?page=${page}&limit=${limit}&orderBy=${orderBy}&orderDir=${orderDir}`); }, get(id: string): Promise { return request(`/scenarios/${id}`); }, create(name: string, description?: string, environmentId?: string): Promise { return request('/scenarios', { method: 'POST', body: JSON.stringify({ name, description, environmentId }), }); }, update(id: string, patch: Partial>): Promise { return request(`/scenarios/${id}`, { method: 'PATCH', body: JSON.stringify(patch), }); }, remove(id: string): Promise { return request(`/scenarios/${id}`, { method: 'DELETE' }); }, run(id: string, environmentId: string, saveSession?: boolean): Promise { return request(`/scenarios/${id}/run`, { method: 'POST', body: JSON.stringify({ environmentId, saveSession }), }); }, getRun(scenarioId: string, runId: string): Promise { return request(`/scenarios/${scenarioId}/run/${runId}`); }, waitForRun(scenarioId: string, runId: string): Promise { return request(`/scenarios/${scenarioId}/run/${runId}/wait`, { method: 'POST' }); }, listRuns( scenarioId: string, page = 1, limit = 20, ): Promise> { return request(`/scenarios/${scenarioId}/runs?page=${page}&limit=${limit}`); }, exportScenario( id: string, options: { includeEnvironment: boolean; credentialIds: string[] } = { includeEnvironment: false, credentialIds: [], }, ): Promise { const params = new URLSearchParams(); params.set('includeEnvironment', String(options.includeEnvironment)); for (const cid of options.credentialIds) { params.append('credentialIds', cid); } return request(`/scenarios/${id}/export?${params.toString()}`); }, importScenario(payload: unknown): Promise { return request('/scenarios/import', { method: 'POST', body: JSON.stringify(payload), }); }, }; // ── Runs ───────────────────────────────────────────────────────────────────── export const runs = { listAll( page = 1, limit = 20, status?: string, orderBy = 'createdAt', orderDir: 'ASC' | 'DESC' = 'DESC', ): Promise< PaginatedResponse< ScenarioRun & { scenario: { id: string; name: string } } > > { const q = new URLSearchParams({ page: String(page), limit: String(limit), orderBy, orderDir }); if (status) q.set('status', status); return request(`/scenarios/runs?${q}`); }, list( scenarioId: string, page = 1, limit = 20, orderBy = 'createdAt', orderDir: 'ASC' | 'DESC' = 'DESC', ): Promise> { return request(`/scenarios/${scenarioId}/runs?page=${page}&limit=${limit}&orderBy=${orderBy}&orderDir=${orderDir}`); }, get(scenarioId: string, runId: string, q?: string): Promise { const qs = q?.trim() ? `?q=${encodeURIComponent(q.trim())}` : ''; return request(`/scenarios/${scenarioId}/run/${runId}${qs}`); }, }; // ── Scenario Credentials ────────────────────────────────────────────────────── export const scenarioCredentials = { list(scenarioId: string): Promise { return request(`/scenarios/${scenarioId}/credentials`); }, add(scenarioId: string, credentialId: string, alias: string): Promise { return request(`/scenarios/${scenarioId}/credentials`, { method: 'POST', body: JSON.stringify({ credentialId, alias }), }); }, remove(scenarioId: string, scCredId: string): Promise { return request(`/scenarios/${scenarioId}/credentials/${scCredId}`, { method: 'DELETE', }); }, }; // ── Scenario Steps ──────────────────────────────────────────────────────────── export interface CreateStepPayload { title?: string; execCode?: string; timeoutSeconds?: number | null; } export interface UpdateStepPayload { title?: string; order?: number; execCode?: string; timeoutSeconds?: number | null; } export const steps = { get(scenarioId: string, stepId: string): Promise { return request(`/scenarios/${scenarioId}/steps/${stepId}`); }, create(scenarioId: string, payload: CreateStepPayload): Promise { return request(`/scenarios/${scenarioId}/steps`, { method: 'POST', body: JSON.stringify(payload), }); }, update(scenarioId: string, stepId: string, payload: UpdateStepPayload): Promise { return request(`/scenarios/${scenarioId}/steps/${stepId}`, { method: 'PATCH', body: JSON.stringify(payload), }); }, remove(scenarioId: string, stepId: string): Promise { return request(`/scenarios/${scenarioId}/steps/${stepId}`, { method: 'DELETE' }); }, };