fix: return page as object {page, next, total, hasMore, nextUrl} for routes.js compat
routes.js reads resp.page?.hasMore and resp.page?.nextUrl — the old HTTP client returned page as an object from parsePageInfo. The CLI pivot accidentally returned page as a number, making hasMore always undefined in routes.js. Restore the object shape in both gitea and gitlab clients. Update tests to assert the object shape.
This commit is contained in:
@@ -131,24 +131,28 @@ async function teaApiCall(endpoint, { method = 'GET', body, raw, teaBin, token }
|
||||
data = bodyText;
|
||||
}
|
||||
|
||||
// Derive pagination from the Link header (H2 fix).
|
||||
// Derive pagination info object matching the pre-pivot parsePageInfo shape.
|
||||
// routes.js reads resp.page?.hasMore, resp.page?.nextUrl, etc.
|
||||
const { next } = parseLinkHeader(headers.link);
|
||||
const listIsArray = Array.isArray(data);
|
||||
const hasMore = listIsArray ? next !== null : false;
|
||||
// page is the caller's current page (extracted from the request context by the
|
||||
// caller); for the client layer we return the page number derived from the
|
||||
// Link header's "next" URL if present, otherwise 1.
|
||||
let page = 1;
|
||||
if (next) {
|
||||
const totalRaw = headers['x-total-count'];
|
||||
const total = totalRaw ? Number(totalRaw) : null;
|
||||
let currentPage = null;
|
||||
if (listIsArray && next) {
|
||||
const pageMatch = next.match(/[?&]page=(\d+)/);
|
||||
if (pageMatch) {
|
||||
// next page exists — the *current* page is next - 1 (rough heuristic;
|
||||
// callers that know the page can override).
|
||||
page = Math.max(1, Number(pageMatch[1]) - 1);
|
||||
currentPage = Math.max(1, Number(pageMatch[1]) - 1);
|
||||
}
|
||||
}
|
||||
const pageInfo = {
|
||||
page: currentPage,
|
||||
next: next || null,
|
||||
total: total !== null && Number.isFinite(total) ? total : null,
|
||||
hasMore: listIsArray ? next !== null : false,
|
||||
nextUrl: next || undefined,
|
||||
};
|
||||
|
||||
return { status, headers, data, page: listIsArray ? page : null, hasMore: listIsArray ? hasMore : undefined };
|
||||
return { status, headers, data, page: pageInfo };
|
||||
}
|
||||
|
||||
// ---- Rate-limit helpers (no-ops with CLI transport) ----
|
||||
|
||||
@@ -192,37 +192,51 @@ describe('createGiteaClient request basics', () => {
|
||||
});
|
||||
|
||||
describe('pagination', () => {
|
||||
test('returns hasMore=true when Link header has rel="next"', async () => {
|
||||
test('page is an object with hasMore=true when Link header has rel="next"', async () => {
|
||||
spawnMock = mockSpawn(
|
||||
cliOutput([{ id: 1 }], {
|
||||
headers: {
|
||||
'Link': '<https://gitea.example.com/api/v1/repos/o/r/issues?limit=1&page=2>; rel="next"',
|
||||
'X-Total-Count': '10',
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const client = createGiteaClient({ token: 't', baseUrl: 'https://gitea.example.com' });
|
||||
const result = await client.issues('o', 'r', { limit: 1, page: 1 });
|
||||
expect(result.hasMore).toBe(true);
|
||||
expect(result.page).toBe(1);
|
||||
// page is the object shape that routes.js expects
|
||||
expect(result.page).toEqual(expect.objectContaining({
|
||||
hasMore: true,
|
||||
nextUrl: 'https://gitea.example.com/api/v1/repos/o/r/issues?limit=1&page=2',
|
||||
next: 'https://gitea.example.com/api/v1/repos/o/r/issues?limit=1&page=2',
|
||||
page: 1,
|
||||
total: 10,
|
||||
}));
|
||||
expect(Array.isArray(result.data)).toBe(true);
|
||||
});
|
||||
|
||||
test('returns hasMore=false when no Link header', async () => {
|
||||
test('page is an object with hasMore=false when no Link header', async () => {
|
||||
spawnMock = mockSpawn(cliOutput([{ id: 1 }]));
|
||||
|
||||
const client = createGiteaClient({ token: 't', baseUrl: 'https://gitea.example.com' });
|
||||
const result = await client.issues('o', 'r', { limit: 50 });
|
||||
expect(result.hasMore).toBe(false);
|
||||
expect(result.page).toBe(1);
|
||||
expect(result.page).toEqual(expect.objectContaining({
|
||||
hasMore: false,
|
||||
next: null,
|
||||
total: null,
|
||||
}));
|
||||
});
|
||||
|
||||
test('page is null for non-array responses', async () => {
|
||||
test('page is an object for non-array responses too', async () => {
|
||||
spawnMock = mockSpawn(cliOutput({ id: 42 }));
|
||||
|
||||
const client = createGiteaClient({ token: 't', baseUrl: 'https://gitea.example.com' });
|
||||
const result = await client.user();
|
||||
expect(result.page).toBeNull();
|
||||
expect(result.page).toEqual(expect.objectContaining({
|
||||
hasMore: false,
|
||||
next: null,
|
||||
page: null,
|
||||
}));
|
||||
});
|
||||
|
||||
test('does NOT pass --paginate flag to tea (C1 fix)', async () => {
|
||||
|
||||
@@ -135,19 +135,28 @@ async function glabApiCall(endpoint, { method = 'GET', body, raw, glabBin, token
|
||||
data = bodyText;
|
||||
}
|
||||
|
||||
// Derive pagination from the Link header (H2 fix).
|
||||
// Derive pagination info object matching the pre-pivot parsePageInfo shape.
|
||||
// routes.js reads resp.page?.hasMore, resp.page?.nextUrl, etc.
|
||||
const { next } = parseLinkHeader(headers.link);
|
||||
const listIsArray = Array.isArray(data);
|
||||
const hasMore = listIsArray ? next !== null : false;
|
||||
let page = 1;
|
||||
if (next) {
|
||||
const totalRaw = headers['x-total'] || headers['x-total-count'];
|
||||
const total = totalRaw ? Number(totalRaw) : null;
|
||||
let currentPage = null;
|
||||
if (listIsArray && next) {
|
||||
const pageMatch = next.match(/[?&]page=(\d+)/);
|
||||
if (pageMatch) {
|
||||
page = Math.max(1, Number(pageMatch[1]) - 1);
|
||||
currentPage = Math.max(1, Number(pageMatch[1]) - 1);
|
||||
}
|
||||
}
|
||||
const pageInfo = {
|
||||
page: currentPage,
|
||||
next: next || null,
|
||||
total: total !== null && Number.isFinite(total) ? total : null,
|
||||
hasMore: listIsArray ? next !== null : false,
|
||||
nextUrl: next || undefined,
|
||||
};
|
||||
|
||||
return { status, headers, data, page: listIsArray ? page : null, hasMore: listIsArray ? hasMore : undefined };
|
||||
return { status, headers, data, page: pageInfo };
|
||||
}
|
||||
|
||||
// ---- Rate-limit helpers (no-ops with CLI transport) ----
|
||||
|
||||
@@ -187,37 +187,50 @@ describe('createGitLabClient request basics', () => {
|
||||
});
|
||||
|
||||
describe('pagination', () => {
|
||||
test('returns hasMore=true when Link header has rel="next"', async () => {
|
||||
test('page is an object with hasMore=true when Link header has rel="next"', async () => {
|
||||
spawnMock = mockSpawn(
|
||||
cliOutput([{ id: 1 }], {
|
||||
headers: {
|
||||
'Link': '<https://gitlab.com/api/v4/projects/g%2Fp/issues?page=2>; rel="next"',
|
||||
'X-Total': '10',
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const client = createGitLabClient({ token: 't', baseUrl: 'https://gitlab.com' });
|
||||
const result = await client.issues('g/p', { per_page: 1 });
|
||||
expect(result.hasMore).toBe(true);
|
||||
expect(result.page).toBe(1);
|
||||
expect(result.page).toEqual(expect.objectContaining({
|
||||
hasMore: true,
|
||||
nextUrl: 'https://gitlab.com/api/v4/projects/g%2Fp/issues?page=2',
|
||||
next: 'https://gitlab.com/api/v4/projects/g%2Fp/issues?page=2',
|
||||
page: 1,
|
||||
total: 10,
|
||||
}));
|
||||
expect(Array.isArray(result.data)).toBe(true);
|
||||
});
|
||||
|
||||
test('returns hasMore=false when no Link header', async () => {
|
||||
test('page is an object with hasMore=false when no Link header', async () => {
|
||||
spawnMock = mockSpawn(cliOutput([{ id: 1 }]));
|
||||
|
||||
const client = createGitLabClient({ token: 't', baseUrl: 'https://gitlab.com' });
|
||||
const result = await client.issues('g/p', { per_page: 50 });
|
||||
expect(result.hasMore).toBe(false);
|
||||
expect(result.page).toBe(1);
|
||||
expect(result.page).toEqual(expect.objectContaining({
|
||||
hasMore: false,
|
||||
next: null,
|
||||
total: null,
|
||||
}));
|
||||
});
|
||||
|
||||
test('page is null for non-array responses', async () => {
|
||||
test('page is an object for non-array responses too', async () => {
|
||||
spawnMock = mockSpawn(cliOutput({ id: 42 }));
|
||||
|
||||
const client = createGitLabClient({ token: 't', baseUrl: 'https://gitlab.com' });
|
||||
const result = await client.user();
|
||||
expect(result.page).toBeNull();
|
||||
expect(result.page).toEqual(expect.objectContaining({
|
||||
hasMore: false,
|
||||
next: null,
|
||||
page: null,
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user