Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions packages/agent-bff/src/openapi/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,12 @@ export const SortClauseSchema = SortClauseInput.openapi('SortClause', {
export const PageSchema = PageInput.openapi('Page', {
description:
'The agent paginates by page number, so `offset` must be a whole multiple of `limit`. ' +
'Any other offset is rejected with 400 invalid_request.',
'Any other offset is rejected with 400 invalid_request. `limit` and `offset` are both ' +
'required once `page` is sent, but the object itself is optional on every list — and ' +
'omitting it is not a request for the whole collection. The BFF then forwards no ' +
'pagination, so the agent applies its own default: the first page (offset 0), a limit ' +
'of 15 records on the Node agent. Anything past that page is silently missing. Send ' +
'`page` and walk it to read a collection in full.',
});

export const TimezoneSchema = TimezoneInput.openapi('Timezone', {
Expand Down Expand Up @@ -161,7 +166,12 @@ export const ListResponseSchema = z
.openapi('ListResponse', {
description:
'Records are flat, each carrying a `__forest` envelope. The list never carries a total: ' +
'call the count endpoint for that, which is why `countStatus` is always `not_requested`.',
'call the count endpoint for that, which is why `countStatus` is always `not_requested`. ' +
'It is always one page, not guaranteed to be the whole collection: a request that omitted ' +
"`page` still gets a page, the agent's default (up to 15 records from the start on the " +
'Node agent), so a response of exactly that default length is probably truncated, and ' +
'nothing in the body says so. Send `page` and walk it. Count gives the total unless the ' +
'collection disables it.',
});

export const CountResponseSchema = z
Expand Down
19 changes: 19 additions & 0 deletions packages/agent-bff/test/openapi/openapi-document.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,25 @@ describe('the documented search inputs', () => {
});
});

describe('the documented pagination inputs', () => {
it('should publish the default page applied when page is absent', () => {
expect(schemas.Page.description).toContain('the object itself is optional');
expect(schemas.Page.description).toContain('the first page (offset 0)');
expect(schemas.Page.description).toContain('a limit of 15 records on the Node agent');
expect(schemas.Page.description).toContain('silently missing');
});

it('should require both limit and offset once page is sent', () => {
expect(schemas.Page.required).toEqual(['limit', 'offset']);
Comment thread
Tonours marked this conversation as resolved.
});

it('should warn on the response that a page-less list is one page, not the collection', () => {
expect(schemas.ListResponse.description).toContain('not guaranteed to be the whole collection');
expect(schemas.ListResponse.description).toContain('up to 15 records from the start');
expect(schemas.ListResponse.description).toContain('probably truncated');
});
});

describe('serializeOpenApi', () => {
it('should produce indented JSON that parses back to the same document', () => {
const serialized = serializeOpenApi(document);
Expand Down
10 changes: 10 additions & 0 deletions packages/agent-bff/test/openapi/openapi-unfolded.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,16 @@ describe('the unfolded document', () => {
expect(request.properties.sort.items?.$ref).toBe('#/components/schemas/SortClause_My_Coll');
});

it('should leave page optional here too, pointing at the shared Page component', () => {
const request = requestSchema('My%20Coll/list') as unknown as {
required?: string[];
properties: Record<string, { $ref?: string }>;
};

expect(request.required ?? []).not.toContain('page');
expect(request.properties.page.$ref).toBe('#/components/schemas/Page');
});

it('should make every leaf and the branch mutually exclusive, which the runtime enforces', () => {
const branch = branchOf('Filter_My_Coll') as unknown as { not: { required: string[] } };

Expand Down
Loading