Skip to content

Commit 3df8f0b

Browse files
MajorTalclaude
andcommitted
fix(deploy): expose operation pagination cursor
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent c7bafa8 commit 3df8f0b

5 files changed

Lines changed: 122 additions & 3 deletions

File tree

sdk/src/namespaces/deploy.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3555,6 +3555,22 @@ describe("Deploy.list", () => {
35553555
assert.equal(w.requests[0].path, "/deploy/v2/operations?limit=5");
35563556
});
35573557

3558+
it("forwards cursor as a query string", async () => {
3559+
const w = makeWiring();
3560+
w.setHandler(() => ({ operations: [], cursor: null }));
3561+
const deploy = new Deploy(w.client);
3562+
await deploy.list({ project: "prj_test", cursor: "op_cursor" });
3563+
assert.equal(w.requests[0].path, "/deploy/v2/operations?cursor=op_cursor");
3564+
});
3565+
3566+
it("forwards limit and cursor as query strings", async () => {
3567+
const w = makeWiring();
3568+
w.setHandler(() => ({ operations: [], cursor: null }));
3569+
const deploy = new Deploy(w.client);
3570+
await deploy.list({ project: "prj_test", limit: 5, cursor: "op_cursor" });
3571+
assert.equal(w.requests[0].path, "/deploy/v2/operations?limit=5&cursor=op_cursor");
3572+
});
3573+
35583574
it("accepts a bare projectId string and issues the same request as the options form", async () => {
35593575
const projectLookups: string[] = [];
35603576
const wiringFor = (): FakeWiring => {

sdk/src/namespaces/deploy.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import type {
4646
DeployEvent,
4747
DeployEventsResponse,
4848
DeployDiff,
49+
DeployListOptions,
4950
DeployListResponse,
5051
DeployOperation,
5152
DeployResult,
@@ -274,14 +275,15 @@ export class Deploy {
274275
* List recent deploy operations for a project. The endpoint requires
275276
* `apikey` auth, so a project id is required — accepted as a bare string
276277
* (matches `r.functions.list(projectId)` and friends) or as `{ project,
277-
* limit? }`. `limit` is forwarded to the gateway as a query string when
278-
* set; the gateway picks a default otherwise.
278+
* limit?, cursor? }`. `limit` and `cursor` are forwarded to the gateway
279+
* as query strings when set; the gateway picks a default page otherwise.
279280
*/
280281
async list(
281-
opts: string | { project: string; limit?: number },
282+
opts: string | DeployListOptions,
282283
): Promise<DeployListResponse> {
283284
const project = typeof opts === "string" ? opts : opts?.project;
284285
const limit = typeof opts === "string" ? undefined : opts?.limit;
286+
const cursor = typeof opts === "string" ? undefined : opts?.cursor;
285287
if (!project) {
286288
throw new LocalError(
287289
"r.deploy.list requires a project id (as a string or { project: 'prj_...' })",
@@ -291,6 +293,7 @@ export class Deploy {
291293
const headers = await apikeyHeaders(this.client, project);
292294
const qs = new URLSearchParams();
293295
if (limit !== undefined) qs.set("limit", String(limit));
296+
if (cursor !== undefined) qs.set("cursor", cursor);
294297
const path =
295298
qs.toString().length > 0
296299
? `/deploy/v2/operations?${qs.toString()}`

sdk/src/namespaces/deploy.types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1630,6 +1630,12 @@ export interface OperationSnapshot {
16301630
/** Response from `GET /deploy/v2/operations`. The gateway may return a
16311631
* pagination cursor when there are more operations than the requested
16321632
* page size; clients pass it back as `?cursor=` to fetch the next page. */
1633+
export interface DeployListOptions {
1634+
project: string;
1635+
limit?: number;
1636+
cursor?: string;
1637+
}
1638+
16331639
export interface DeployListResponse {
16341640
operations: OperationSnapshot[];
16351641
cursor?: string | null;

src/tools/deploy-list.test.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { describe, it, beforeEach, mock } from "node:test";
2+
import assert from "node:assert/strict";
3+
import { z } from "zod";
4+
5+
let calls: unknown[] = [];
6+
let nextListImpl: (opts: unknown) => Promise<unknown> = async () => ({
7+
operations: [],
8+
cursor: null,
9+
});
10+
11+
mock.module("../allowance-auth.js", {
12+
namedExports: {
13+
requireAllowanceAuth: () => ({ headers: { "SIGN-IN-WITH-X": "dGVzdA==" } }),
14+
},
15+
});
16+
17+
mock.module("../sdk.js", {
18+
namedExports: {
19+
getSdk: () => ({
20+
deploy: {
21+
list: async (opts: unknown) => {
22+
calls.push(opts);
23+
return nextListImpl(opts);
24+
},
25+
},
26+
}),
27+
_resetSdk: () => {},
28+
},
29+
});
30+
31+
mock.module("../errors.js", {
32+
namedExports: {
33+
mapSdkError: () => ({
34+
content: [{ type: "text", text: "mapped SDK error" }],
35+
isError: true,
36+
}),
37+
},
38+
});
39+
40+
const { deployListSchema, handleDeployList } = await import("./deploy-list.js");
41+
42+
beforeEach(() => {
43+
calls = [];
44+
nextListImpl = async () => ({
45+
operations: [],
46+
cursor: null,
47+
});
48+
});
49+
50+
describe("deploy_list", () => {
51+
it("accepts a pagination cursor in the schema", () => {
52+
const parsed = z.object(deployListSchema).parse({
53+
project_id: "prj_test",
54+
limit: 5,
55+
cursor: "op_cursor",
56+
});
57+
58+
assert.equal(parsed.cursor, "op_cursor");
59+
});
60+
61+
it("forwards cursor to SDK deploy.list and still renders the next cursor", async () => {
62+
nextListImpl = async () => ({
63+
operations: [
64+
{
65+
operation_id: "op_1",
66+
status: "ready",
67+
release_id: "rel_1",
68+
updated_at: "2026-05-15T00:00:00Z",
69+
},
70+
],
71+
cursor: "op_next",
72+
});
73+
74+
const result = await handleDeployList({
75+
project_id: "prj_test",
76+
limit: 5,
77+
cursor: "op_cursor",
78+
});
79+
80+
assert.equal(result.isError, undefined);
81+
assert.deepEqual(calls, [
82+
{ project: "prj_test", limit: 5, cursor: "op_cursor" },
83+
]);
84+
assert.match(result.content[0]!.text, /Next cursor: `op_next`/);
85+
});
86+
});

src/tools/deploy-list.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,18 @@ export const deployListSchema = {
2323
.describe(
2424
"Maximum number of operations to return. Forwarded to the gateway as `?limit=`; the gateway picks a default when omitted.",
2525
),
26+
cursor: z
27+
.string()
28+
.optional()
29+
.describe(
30+
"Pagination cursor returned by a previous deploy_list response. Forwarded to the gateway as `?cursor=`.",
31+
),
2632
};
2733

2834
export async function handleDeployList(args: {
2935
project_id: string;
3036
limit?: number;
37+
cursor?: string;
3138
}): Promise<{ content: Array<{ type: "text"; text: string }>; isError?: boolean }> {
3239
const auth = requireAllowanceAuth("/deploy/v2/operations");
3340
if ("error" in auth) return auth.error;
@@ -36,6 +43,7 @@ export async function handleDeployList(args: {
3643
const result = await getSdk().deploy.list({
3744
project: args.project_id,
3845
limit: args.limit,
46+
cursor: args.cursor,
3947
});
4048

4149
const lines: string[] = [

0 commit comments

Comments
 (0)