rpki/ui/rpki-explorer/src/api/service.ts

451 lines
13 KiB
TypeScript

/**
* Typed endpoint functions for the rpki_query_service API.
* Every function validates the response envelope with zod before returning.
*/
import { z } from "zod";
import { apiFetch, apiPost, withQuery, type QueryParams } from "./client";
import {
chainEdgeRecordSchema,
envelopeSchema,
exportJobRecordSchema,
explainRecordSchema,
healthSchema,
manifestFileEntrySchema,
objectInstanceRecordSchema,
projectionRecordSchema,
publicationPointRecordSchema,
repositoryRecordSchema,
revokedCertEntrySchema,
runRecordSchema,
runSummarySchema,
searchResultSchema,
serviceInfoSchema,
statsMapSchema,
validationSummarySchema,
type ChainEdgeRecord,
type ExportJobRecord,
type ExplainRecord,
type HealthStatus,
type ListResult,
type ManifestFileEntry,
type ObjectInstanceRecord,
type ProjectionRecord,
type PublicationPointRecord,
type RepositoryRecord,
type RevokedCertEntry,
type RunRecord,
type RunSummary,
type SearchResult,
type ServiceInfo,
type ValidationSummary,
} from "./schemas";
const API = "/api/v1";
function runBase(runId: string): string {
return `${API}/runs/${encodeURIComponent(runId)}`;
}
async function getData<S extends z.ZodTypeAny>(
path: string,
schema: S,
params?: QueryParams,
): Promise<z.infer<S>> {
const raw = await apiFetch<unknown>(withQuery(path, params));
return envelopeSchema(schema).parse(raw).data;
}
async function getList<S extends z.ZodTypeAny>(
path: string,
schema: S,
params?: QueryParams,
): Promise<ListResult<z.infer<S>>> {
const raw = await apiFetch<unknown>(withQuery(path, params));
const parsed = envelopeSchema(z.array(schema)).parse(raw);
return {
items: parsed.data,
nextCursor: parsed.page?.nextCursor ?? null,
runId: parsed.meta?.runId ?? undefined,
};
}
/* ------------------------------------------------------------------ */
/* Service / health */
/* ------------------------------------------------------------------ */
export function getServiceInfo(): Promise<ServiceInfo> {
return getData(`${API}`, serviceInfoSchema);
}
export function getHealth(): Promise<HealthStatus> {
return getData(`${API}/healthz`, healthSchema);
}
/* ------------------------------------------------------------------ */
/* Runs */
/* ------------------------------------------------------------------ */
export type PageParams = {
limit?: number;
cursor?: string | null;
};
export function listRuns(params: PageParams = {}): Promise<ListResult<RunRecord>> {
return getList(`${API}/runs`, runRecordSchema, params);
}
export function getLatestRun(): Promise<RunRecord> {
return getData(`${API}/latest_run`, runRecordSchema);
}
export function getRun(runId: string): Promise<RunRecord> {
return getData(runBase(runId), runRecordSchema);
}
export function getRunArtifacts(runId: string): Promise<Record<string, string>> {
return getData(`${runBase(runId)}/artifacts`, z.record(z.string()));
}
export function getRunSummary(runId: string): Promise<RunSummary> {
return getData(`${runBase(runId)}/summary`, runSummarySchema);
}
/* ------------------------------------------------------------------ */
/* Repositories */
/* ------------------------------------------------------------------ */
export function listRepos(
runId: string,
params: PageParams = {},
): Promise<ListResult<RepositoryRecord>> {
return getList(`${runBase(runId)}/repos`, repositoryRecordSchema, params);
}
export function getRepo(runId: string, repoId: string): Promise<RepositoryRecord> {
return getData(
`${runBase(runId)}/repos/${encodeURIComponent(repoId)}`,
repositoryRecordSchema,
);
}
export interface RepoStats {
publicationPoints?: number;
objects?: number;
rejectedObjects?: number;
syncDurationMsTotal?: number;
phases?: Record<string, number>;
terminalStates?: Record<string, number>;
}
export function getRepoStats(runId: string, repoId: string): Promise<RepoStats> {
return getData(
`${runBase(runId)}/repos/${encodeURIComponent(repoId)}/stats`,
z
.object({
publicationPoints: z.number().optional(),
objects: z.number().optional(),
rejectedObjects: z.number().optional(),
syncDurationMsTotal: z.number().optional(),
phases: z.record(z.number()).optional(),
terminalStates: z.record(z.number()).optional(),
})
.passthrough(),
);
}
/* ------------------------------------------------------------------ */
/* Publication points */
/* ------------------------------------------------------------------ */
export function listPublicationPoints(
runId: string,
params: PageParams = {},
): Promise<ListResult<PublicationPointRecord>> {
return getList(`${runBase(runId)}/publication-points`, publicationPointRecordSchema, params);
}
export function listRepoPublicationPoints(
runId: string,
repoId: string,
params: PageParams = {},
): Promise<ListResult<PublicationPointRecord>> {
return getList(
`${runBase(runId)}/repos/${encodeURIComponent(repoId)}/publication-points`,
publicationPointRecordSchema,
params,
);
}
export function getPublicationPoint(
runId: string,
ppId: string,
): Promise<PublicationPointRecord> {
return getData(
`${runBase(runId)}/publication-points/${encodeURIComponent(ppId)}`,
publicationPointRecordSchema,
);
}
export interface PpStats {
objects?: number;
rejectedObjects?: number;
warnings?: number;
repoSyncSource?: string;
repoSyncPhase?: string;
repoSyncDurationMs?: number;
repoTerminalState?: string;
}
export function getPublicationPointStats(runId: string, ppId: string): Promise<PpStats> {
return getData(
`${runBase(runId)}/publication-points/${encodeURIComponent(ppId)}/stats`,
z
.object({
objects: z.number().optional(),
rejectedObjects: z.number().optional(),
warnings: z.number().optional(),
repoSyncSource: z.string().optional(),
repoSyncPhase: z.string().optional(),
repoSyncDurationMs: z.number().optional(),
repoTerminalState: z.string().optional(),
})
.passthrough(),
);
}
/* ------------------------------------------------------------------ */
/* Objects */
/* ------------------------------------------------------------------ */
export interface ObjectFilters extends PageParams {
type?: string;
result?: string;
rejected?: boolean;
reason?: string;
q?: string;
}
function filterParams(filters: ObjectFilters): QueryParams {
return {
limit: filters.limit,
cursor: filters.cursor,
type: filters.type,
result: filters.result,
rejected: filters.rejected === undefined ? undefined : String(filters.rejected),
reason: filters.reason,
q: filters.q,
};
}
export function listObjects(
runId: string,
filters: ObjectFilters = {},
): Promise<ListResult<ObjectInstanceRecord>> {
return getList(`${runBase(runId)}/objects`, objectInstanceRecordSchema, filterParams(filters));
}
export function listRepoObjects(
runId: string,
repoId: string,
filters: ObjectFilters = {},
): Promise<ListResult<ObjectInstanceRecord>> {
return getList(
`${runBase(runId)}/repos/${encodeURIComponent(repoId)}/objects`,
objectInstanceRecordSchema,
filterParams(filters),
);
}
export function listPublicationPointObjects(
runId: string,
ppId: string,
filters: ObjectFilters = {},
): Promise<ListResult<ObjectInstanceRecord>> {
return getList(
`${runBase(runId)}/publication-points/${encodeURIComponent(ppId)}/objects`,
objectInstanceRecordSchema,
filterParams(filters),
);
}
export interface IssueFilters extends PageParams {
reason?: string;
type?: string;
repoId?: string;
ppId?: string;
}
export function listIssues(
runId: string,
filters: IssueFilters = {},
): Promise<ListResult<ObjectInstanceRecord>> {
return getList(`${runBase(runId)}/issues`, objectInstanceRecordSchema, {
limit: filters.limit,
cursor: filters.cursor,
reason: filters.reason,
type: filters.type,
repoId: filters.repoId,
ppId: filters.ppId,
});
}
export function getObjectByUri(
runId: string,
uri: string,
): Promise<ObjectInstanceRecord> {
return getData(`${runBase(runId)}/objects/by-uri`, objectInstanceRecordSchema, { uri });
}
export function getObject(
runId: string,
objectInstanceId: string,
): Promise<ObjectInstanceRecord> {
return getData(
`${runBase(runId)}/objects/${encodeURIComponent(objectInstanceId)}`,
objectInstanceRecordSchema,
);
}
export function getObjectProjection(
runId: string,
objectInstanceId: string,
): Promise<ProjectionRecord | null> {
return getData(
`${runBase(runId)}/objects/${encodeURIComponent(objectInstanceId)}/parsed`,
projectionRecordSchema.nullable(),
);
}
export function getObjectValidation(
runId: string,
objectInstanceId: string,
): Promise<ValidationSummary> {
return getData(
`${runBase(runId)}/objects/${encodeURIComponent(objectInstanceId)}/validation`,
validationSummarySchema,
);
}
export function getObjectChain(
runId: string,
objectInstanceId: string,
): Promise<ChainEdgeRecord[]> {
return getData(
`${runBase(runId)}/objects/${encodeURIComponent(objectInstanceId)}/chain`,
z.array(chainEdgeRecordSchema),
);
}
export function listManifestFiles(
runId: string,
objectInstanceId: string,
params: PageParams = {},
): Promise<ListResult<ManifestFileEntry>> {
return getList(
`${runBase(runId)}/objects/${encodeURIComponent(objectInstanceId)}/parsed/manifest-files`,
manifestFileEntrySchema,
params,
);
}
export function listRevokedCertificates(
runId: string,
objectInstanceId: string,
params: PageParams = {},
): Promise<ListResult<RevokedCertEntry>> {
return getList(
`${runBase(runId)}/objects/${encodeURIComponent(objectInstanceId)}/parsed/revoked-certs`,
revokedCertEntrySchema,
params,
);
}
export function explainObjectValidation(
runId: string,
objectInstanceId: string,
options: { forceRefresh?: boolean } = {},
): Promise<ExplainRecord> {
return apiPost<unknown>(
`${runBase(runId)}/objects/${encodeURIComponent(objectInstanceId)}/validation/explain`,
{ forceRefresh: options.forceRefresh ?? false },
).then((raw) => envelopeSchema(explainRecordSchema).parse(raw).data);
}
export function rawObjectUrl(runId: string, objectInstanceId: string): string {
return `${runBase(runId)}/objects/${encodeURIComponent(objectInstanceId)}/raw`;
}
/* ------------------------------------------------------------------ */
/* Search */
/* ------------------------------------------------------------------ */
export function searchRun(
runId: string,
q: string,
limit = 10,
): Promise<SearchResult> {
return getData(`${runBase(runId)}/search`, searchResultSchema, { q, limit });
}
/* ------------------------------------------------------------------ */
/* Stats */
/* ------------------------------------------------------------------ */
export function getStatsObjectTypes(runId: string): Promise<Record<string, number>> {
return getData(`${runBase(runId)}/stats/object-types`, statsMapSchema);
}
export function getStatsValidation(runId: string): Promise<Record<string, number>> {
return getData(`${runBase(runId)}/stats/validation`, statsMapSchema);
}
export function getStatsReasons(runId: string): Promise<Record<string, number>> {
return getData(`${runBase(runId)}/stats/reasons`, statsMapSchema);
}
export function getStatsDownloads(runId: string): Promise<Record<string, unknown>> {
return getData(`${runBase(runId)}/stats/downloads`, z.record(z.unknown()));
}
/* ------------------------------------------------------------------ */
/* Exports */
/* ------------------------------------------------------------------ */
export interface ExportRequest {
scope: "repo" | "publication_point" | "object_set";
repoId?: string;
ppId?: string;
objectInstanceIds?: string[];
}
export function createExport(
runId: string,
request: ExportRequest,
): Promise<ExportJobRecord> {
const body: Record<string, unknown> = { scope: request.scope };
if (request.repoId) body.repoId = request.repoId;
if (request.ppId) body.ppId = request.ppId;
if (request.objectInstanceIds) body.objectInstanceIds = request.objectInstanceIds;
return apiPost<unknown>(`${runBase(runId)}/exports`, body).then(
(raw) => envelopeSchema(exportJobRecordSchema).parse(raw).data,
);
}
export function listExports(
runId: string,
params: PageParams = {},
): Promise<ListResult<ExportJobRecord>> {
return getList(`${runBase(runId)}/exports`, exportJobRecordSchema, params);
}
export function getExportJob(runId: string, jobId: string): Promise<ExportJobRecord> {
return getData(
`${runBase(runId)}/exports/${encodeURIComponent(jobId)}`,
exportJobRecordSchema,
);
}
export function exportDownloadUrl(runId: string, jobId: string): string {
return `${runBase(runId)}/exports/${encodeURIComponent(jobId)}/download`;
}