Wrap non-paginated list endpoints in paginated response shape

Wrap 4 list endpoints that returned bare arrays in the standard
paginated `{ data, meta: { has_more } }` shape so adding real
pagination later is additive rather than a breaking change.

Endpoints: GET /runs/{id}/questions, /runs/{id}/stages,
/runs/{id}/verifications, and /verifications.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-03-05 00:41:32 -05:00
parent 40b707b077
commit 14322f40c2
17 changed files with 262 additions and 52 deletions

View file

@ -5,7 +5,7 @@ import { CheckCircleIcon, ArrowPathIcon, PauseCircleIcon, XCircleIcon } from "@h
import { DocumentTextIcon, MapIcon, CommandLineIcon, ChatBubbleLeftIcon, WrenchScrewdriverIcon } from "@heroicons/react/24/outline";
import { apiJson } from "../api-client";
import { formatDurationSecs } from "../lib/format";
import type { RunStage, StageTurn as ApiStageTurn, PaginatedStageTurnList } from "@qltysh/arc-api-client";
import type { PaginatedRunStageList, StageTurn as ApiStageTurn, PaginatedStageTurnList } from "@qltysh/arc-api-client";
import type { Route } from "./+types/run-stages";
export const handle = { wide: true };
@ -20,7 +20,7 @@ interface Stage {
}
export async function loader({ request, params }: Route.LoaderArgs) {
const apiStages = await apiJson<RunStage[]>(`/runs/${params.id}/stages`, { request });
const { data: apiStages } = await apiJson<PaginatedRunStageList>(`/runs/${params.id}/stages`, { request });
const stages: Stage[] = apiStages.map((s) => ({
id: s.id,
name: s.name,

View file

@ -20,11 +20,11 @@ import type {
VerificationCategory,
} from "../data/verifications";
import { apiJson } from "../api-client";
import type { RunVerification } from "@qltysh/arc-api-client";
import type { PaginatedRunVerificationList } from "@qltysh/arc-api-client";
import type { Route } from "./+types/run-verifications";
export async function loader({ request, params }: Route.LoaderArgs) {
const apiCategories = await apiJson<RunVerification[]>(`/runs/${params.id}/verifications`, { request });
const { data: apiCategories } = await apiJson<PaginatedRunVerificationList>(`/runs/${params.id}/verifications`, { request });
const categories: VerificationCategory[] = apiCategories.map((cat) => ({
name: cat.name,
question: cat.question,

View file

@ -22,6 +22,10 @@ fn paginated_response<T: serde::Serialize>(items: Vec<T>, pagination: &Paginatio
(StatusCode::OK, Json(json!({ "data": data, "meta": { "has_more": has_more } }))).into_response()
}
fn list_response<T: serde::Serialize>(items: T) -> Response {
(StatusCode::OK, Json(json!({ "data": items, "meta": { "has_more": false } }))).into_response()
}
// ── Runs ───────────────────────────────────────────────────────────────
pub async fn list_runs(
@ -48,7 +52,7 @@ pub async fn get_run_stages(
State(_state): State<Arc<AppState>>,
Path(_id): Path<String>,
) -> Response {
(StatusCode::OK, Json(runs::stages())).into_response()
list_response(runs::stages())
}
pub async fn get_stage_turns(
@ -88,7 +92,7 @@ pub async fn get_run_verifications(
State(_state): State<Arc<AppState>>,
Path(_id): Path<String>,
) -> Response {
(StatusCode::OK, Json(runs::verifications())).into_response()
list_response(runs::verifications())
}
pub async fn get_run_configuration(
@ -150,7 +154,7 @@ pub async fn get_questions_stub(
Path(_id): Path<String>,
) -> Response {
let empty: Vec<arc_types::ApiQuestion> = vec![];
(StatusCode::OK, Json(empty)).into_response()
list_response(empty)
}
pub async fn answer_stub(
@ -459,7 +463,7 @@ pub async fn list_verifications(
_auth: AuthenticatedService,
State(_state): State<Arc<AppState>>,
) -> Response {
(StatusCode::OK, Json(verifications::categories())).into_response()
list_response(verifications::categories())
}
pub async fn get_verification_detail(

View file

@ -12,6 +12,7 @@ use tokio::sync::broadcast;
use tokio_stream::wrappers::BroadcastStream;
use tokio_stream::StreamExt;
use serde_json::json;
use tracing::{error, info};
use arc_agent::LocalSandbox;
@ -644,7 +645,7 @@ async fn get_questions(
Some(managed_run) => {
let interviewer = match &managed_run.interviewer {
Some(i) => i,
None => return (StatusCode::OK, Json(Vec::<ApiQuestion>::new())).into_response(),
None => return (StatusCode::OK, Json(json!({ "data": Vec::<ApiQuestion>::new(), "meta": { "has_more": false } }))).into_response(),
};
let pending = interviewer.pending_questions();
let questions: Vec<ApiQuestion> = pending
@ -665,7 +666,7 @@ async fn get_questions(
allow_freeform: pq.question.allow_freeform,
})
.collect();
(StatusCode::OK, Json(questions)).into_response()
(StatusCode::OK, Json(json!({ "data": questions, "meta": { "has_more": false } }))).into_response()
}
None => ApiError::not_found("Run not found.").into_response(),
}
@ -1041,7 +1042,8 @@ mod tests {
assert_eq!(response.status(), StatusCode::OK);
let body = body_json(response.into_body()).await;
assert!(body.is_array());
assert!(body["data"].is_array());
assert_eq!(body["meta"]["has_more"], false);
}
#[tokio::test]

View file

@ -496,7 +496,7 @@ mod server_lifecycle {
.unwrap();
let response = app.clone().oneshot(req).await.unwrap();
let body = body_json(response.into_body()).await;
let arr = body.as_array().unwrap();
let arr = body["data"].as_array().unwrap();
if !arr.is_empty() {
question_id = arr[0]["id"].as_str().unwrap().to_string();
break;
@ -557,7 +557,7 @@ mod server_lifecycle {
let response = app.clone().oneshot(req).await.unwrap();
let body = body_json(response.into_body()).await;
assert!(
body.as_array().unwrap().is_empty(),
body["data"].as_array().unwrap().is_empty(),
"no pending questions after completion"
);
}

View file

@ -298,9 +298,7 @@ paths:
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/ApiQuestion"
$ref: "#/components/schemas/PaginatedApiQuestionList"
"404":
description: Run not found
content:
@ -379,9 +377,7 @@ paths:
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/RunStage"
$ref: "#/components/schemas/PaginatedRunStageList"
"404":
description: Run not found
content:
@ -477,9 +473,7 @@ paths:
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/RunVerification"
$ref: "#/components/schemas/PaginatedRunVerificationList"
"404":
description: Run not found
content:
@ -673,9 +667,7 @@ paths:
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/VerificationCategory"
$ref: "#/components/schemas/PaginatedVerificationCategoryList"
/verifications/{slug}:
get:
@ -1193,6 +1185,58 @@ components:
meta:
$ref: "#/components/schemas/PaginationMeta"
PaginatedApiQuestionList:
type: object
required:
- data
- meta
properties:
data:
type: array
items:
$ref: "#/components/schemas/ApiQuestion"
meta:
$ref: "#/components/schemas/PaginationMeta"
PaginatedRunStageList:
type: object
required:
- data
- meta
properties:
data:
type: array
items:
$ref: "#/components/schemas/RunStage"
meta:
$ref: "#/components/schemas/PaginationMeta"
PaginatedRunVerificationList:
type: object
required:
- data
- meta
properties:
data:
type: array
items:
$ref: "#/components/schemas/RunVerification"
meta:
$ref: "#/components/schemas/PaginationMeta"
PaginatedVerificationCategoryList:
type: object
required:
- data
- meta
properties:
data:
type: array
items:
$ref: "#/components/schemas/VerificationCategory"
meta:
$ref: "#/components/schemas/PaginationMeta"
# ── Existing Run Schemas ────────────────────────────────────────────
RunStatus:

View file

@ -298,9 +298,7 @@ paths:
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/ApiQuestion"
$ref: "#/components/schemas/PaginatedApiQuestionList"
"404":
description: Run not found
content:
@ -379,9 +377,7 @@ paths:
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/RunStage"
$ref: "#/components/schemas/PaginatedRunStageList"
"404":
description: Run not found
content:
@ -477,9 +473,7 @@ paths:
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/RunVerification"
$ref: "#/components/schemas/PaginatedRunVerificationList"
"404":
description: Run not found
content:
@ -673,9 +667,7 @@ paths:
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/VerificationCategory"
$ref: "#/components/schemas/PaginatedVerificationCategoryList"
/verifications/{slug}:
get:
@ -1193,6 +1185,58 @@ components:
meta:
$ref: "#/components/schemas/PaginationMeta"
PaginatedApiQuestionList:
type: object
required:
- data
- meta
properties:
data:
type: array
items:
$ref: "#/components/schemas/ApiQuestion"
meta:
$ref: "#/components/schemas/PaginationMeta"
PaginatedRunStageList:
type: object
required:
- data
- meta
properties:
data:
type: array
items:
$ref: "#/components/schemas/RunStage"
meta:
$ref: "#/components/schemas/PaginationMeta"
PaginatedRunVerificationList:
type: object
required:
- data
- meta
properties:
data:
type: array
items:
$ref: "#/components/schemas/RunVerification"
meta:
$ref: "#/components/schemas/PaginationMeta"
PaginatedVerificationCategoryList:
type: object
required:
- data
- meta
properties:
data:
type: array
items:
$ref: "#/components/schemas/VerificationCategory"
meta:
$ref: "#/components/schemas/PaginationMeta"
# ── Existing Run Schemas ────────────────────────────────────────────
RunStatus:

View file

@ -40,14 +40,18 @@ models/file-diff.ts
models/health-response.ts
models/history-entry.ts
models/index.ts
models/paginated-api-question-list.ts
models/paginated-branch-list.ts
models/paginated-history-entry-list.ts
models/paginated-project-list.ts
models/paginated-retro-list.ts
models/paginated-run-list.ts
models/paginated-run-stage-list.ts
models/paginated-run-verification-list.ts
models/paginated-saved-query-list.ts
models/paginated-session-group-list.ts
models/paginated-stage-turn-list.ts
models/paginated-verification-category-list.ts
models/paginated-workflow-list.ts
models/pagination-meta.ts
models/preview-url-request.ts

View file

@ -22,10 +22,10 @@ import { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObj
// @ts-ignore
import { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';
// @ts-ignore
import type { ApiQuestion } from '../models';
// @ts-ignore
import type { ErrorResponse } from '../models';
// @ts-ignore
import type { PaginatedApiQuestionList } from '../models';
// @ts-ignore
import type { PreviewUrlRequest } from '../models';
// @ts-ignore
import type { PreviewUrlResponse } from '../models';
@ -227,7 +227,7 @@ export const HumanInTheLoopApiFp = function(configuration?: Configuration) {
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async listRunQuestions(id: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<ApiQuestion>>> {
async listRunQuestions(id: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<PaginatedApiQuestionList>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.listRunQuestions(id, options);
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
const localVarOperationServerBasePath = operationServerMap['HumanInTheLoopApi.listRunQuestions']?.[localVarOperationServerIndex]?.url;
@ -289,7 +289,7 @@ export const HumanInTheLoopApiFactory = function (configuration?: Configuration,
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
listRunQuestions(id: string, options?: RawAxiosRequestConfig): AxiosPromise<Array<ApiQuestion>> {
listRunQuestions(id: string, options?: RawAxiosRequestConfig): AxiosPromise<PaginatedApiQuestionList> {
return localVarFp.listRunQuestions(id, options).then((request) => request(axios, basePath));
},
/**

View file

@ -24,9 +24,9 @@ import { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError
// @ts-ignore
import type { ErrorResponse } from '../models';
// @ts-ignore
import type { PaginatedStageTurnList } from '../models';
import type { PaginatedRunStageList } from '../models';
// @ts-ignore
import type { RunStage } from '../models';
import type { PaginatedStageTurnList } from '../models';
/**
* RunInternalsApi - axios parameter creator
*/
@ -232,7 +232,7 @@ export const RunInternalsApiFp = function(configuration?: Configuration) {
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async listRunStages(id: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<RunStage>>> {
async listRunStages(id: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<PaginatedRunStageList>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.listRunStages(id, options);
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
const localVarOperationServerBasePath = operationServerMap['RunInternalsApi.listRunStages']?.[localVarOperationServerIndex]?.url;
@ -309,7 +309,7 @@ export const RunInternalsApiFactory = function (configuration?: Configuration, b
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
listRunStages(id: string, options?: RawAxiosRequestConfig): AxiosPromise<Array<RunStage>> {
listRunStages(id: string, options?: RawAxiosRequestConfig): AxiosPromise<PaginatedRunStageList> {
return localVarFp.listRunStages(id, options).then((request) => request(axios, basePath));
},
/**

View file

@ -24,11 +24,11 @@ import { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError
// @ts-ignore
import type { ErrorResponse } from '../models';
// @ts-ignore
import type { PaginatedRunVerificationList } from '../models';
// @ts-ignore
import type { RunFiles } from '../models';
// @ts-ignore
import type { RunUsage } from '../models';
// @ts-ignore
import type { RunVerification } from '../models';
/**
* RunOutputsApi - axios parameter creator
*/
@ -171,7 +171,7 @@ export const RunOutputsApiFp = function(configuration?: Configuration) {
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async listRunVerifications(id: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<RunVerification>>> {
async listRunVerifications(id: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<PaginatedRunVerificationList>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.listRunVerifications(id, options);
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
const localVarOperationServerBasePath = operationServerMap['RunOutputsApi.listRunVerifications']?.[localVarOperationServerIndex]?.url;
@ -217,7 +217,7 @@ export const RunOutputsApiFactory = function (configuration?: Configuration, bas
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
listRunVerifications(id: string, options?: RawAxiosRequestConfig): AxiosPromise<Array<RunVerification>> {
listRunVerifications(id: string, options?: RawAxiosRequestConfig): AxiosPromise<PaginatedRunVerificationList> {
return localVarFp.listRunVerifications(id, options).then((request) => request(axios, basePath));
},
/**

View file

@ -24,7 +24,7 @@ import { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError
// @ts-ignore
import type { ErrorResponse } from '../models';
// @ts-ignore
import type { VerificationCategory } from '../models';
import type { PaginatedVerificationCategoryList } from '../models';
// @ts-ignore
import type { VerificationDetailResponse } from '../models';
/**
@ -111,7 +111,7 @@ export const VerificationsApiFp = function(configuration?: Configuration) {
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async listVerifications(options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<VerificationCategory>>> {
async listVerifications(options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<PaginatedVerificationCategoryList>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.listVerifications(options);
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
const localVarOperationServerBasePath = operationServerMap['VerificationsApi.listVerifications']?.[localVarOperationServerIndex]?.url;
@ -145,7 +145,7 @@ export const VerificationsApiFactory = function (configuration?: Configuration,
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
listVerifications(options?: RawAxiosRequestConfig): AxiosPromise<Array<VerificationCategory>> {
listVerifications(options?: RawAxiosRequestConfig): AxiosPromise<PaginatedVerificationCategoryList> {
return localVarFp.listVerifications(options).then((request) => request(axios, basePath));
},
/**

View file

@ -21,14 +21,18 @@ export * from './file-checkpoint';
export * from './file-diff';
export * from './health-response';
export * from './history-entry';
export * from './paginated-api-question-list';
export * from './paginated-branch-list';
export * from './paginated-history-entry-list';
export * from './paginated-project-list';
export * from './paginated-retro-list';
export * from './paginated-run-list';
export * from './paginated-run-stage-list';
export * from './paginated-run-verification-list';
export * from './paginated-saved-query-list';
export * from './paginated-session-group-list';
export * from './paginated-stage-turn-list';
export * from './paginated-verification-category-list';
export * from './paginated-workflow-list';
export * from './pagination-meta';
export * from './preview-url-request';

View file

@ -0,0 +1,27 @@
/* tslint:disable */
/* eslint-disable */
/**
* Arc Run API
* HTTP API for managing Arc workflow run executions.
*
* The version of the OpenAPI document: 0.1.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
// May contain unused imports in some cases
// @ts-ignore
import type { ApiQuestion } from './api-question';
// May contain unused imports in some cases
// @ts-ignore
import type { PaginationMeta } from './pagination-meta';
export interface PaginatedApiQuestionList {
'data': Array<ApiQuestion>;
'meta': PaginationMeta;
}

View file

@ -0,0 +1,27 @@
/* tslint:disable */
/* eslint-disable */
/**
* Arc Run API
* HTTP API for managing Arc workflow run executions.
*
* The version of the OpenAPI document: 0.1.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
// May contain unused imports in some cases
// @ts-ignore
import type { PaginationMeta } from './pagination-meta';
// May contain unused imports in some cases
// @ts-ignore
import type { RunStage } from './run-stage';
export interface PaginatedRunStageList {
'data': Array<RunStage>;
'meta': PaginationMeta;
}

View file

@ -0,0 +1,27 @@
/* tslint:disable */
/* eslint-disable */
/**
* Arc Run API
* HTTP API for managing Arc workflow run executions.
*
* The version of the OpenAPI document: 0.1.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
// May contain unused imports in some cases
// @ts-ignore
import type { PaginationMeta } from './pagination-meta';
// May contain unused imports in some cases
// @ts-ignore
import type { RunVerification } from './run-verification';
export interface PaginatedRunVerificationList {
'data': Array<RunVerification>;
'meta': PaginationMeta;
}

View file

@ -0,0 +1,27 @@
/* tslint:disable */
/* eslint-disable */
/**
* Arc Run API
* HTTP API for managing Arc workflow run executions.
*
* The version of the OpenAPI document: 0.1.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
// May contain unused imports in some cases
// @ts-ignore
import type { PaginationMeta } from './pagination-meta';
// May contain unused imports in some cases
// @ts-ignore
import type { VerificationCategory } from './verification-category';
export interface PaginatedVerificationCategoryList {
'data': Array<VerificationCategory>;
'meta': PaginationMeta;
}