Skip to content
Merged
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,7 @@ CVSS vector filters, SSVC filters, date-bounded positive `knownExploited` search

The reported total and pagination are complete only within that bounded candidate set; qualifying records beyond the first 10,000 OpenSearch candidates may not be represented. This preserves the existing bounded post-filter behavior until PIT-based pagination is implemented. A `knownExploited` filter without date boundaries uses a direct OpenSearch query and retains ordinary exact-total behavior.

`/search` orders OpenSearch results by CVE ID so repeated shallow-page requests use a deterministic sort. The response `pageSize` is the number of records actually returned, including zero for an out-of-range page. A stable sort does not provide a point-in-time snapshot, so index updates between separate page requests can still shift results until PIT-based pagination is implemented.
`/search` orders OpenSearch results by CVE ID so repeated shallow-page requests use a deterministic sort. The response `pageSize` preserves the requested `resultsPerPage` value, while `resultsReturned` reports the number of records actually returned and may be smaller for a partial or out-of-range page. A stable sort does not provide a point-in-time snapshot, so index updates between separate page requests can still shift results until PIT-based pagination is implemented.

The API does not cache, coalesce, queue, or rate-limit searches in process. Each request executes its own required OpenSearch operations. Public rate enforcement, admission control, shared caching, and performance telemetry must be supplied by the deployment infrastructure when those capabilities are enabled.

Expand Down
8 changes: 7 additions & 1 deletion api-docs/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,7 @@
"required": [
"page",
"pageSize",
"resultsReturned",
"totalPages",
"status",
"data"
Expand All @@ -465,9 +466,14 @@
"description": "One-based page number, or 0 when the search has no matches."
},
"pageSize": {
"type": "integer",
"minimum": 1,
"description": "Maximum number of CVE records requested for this page."
},
"resultsReturned": {
"type": "integer",
"minimum": 0,
"description": "Number of CVE records returned in this response."
"description": "Number of CVE records actually returned in this response."
},
"totalPages": {
"type": "integer",
Expand Down
12 changes: 10 additions & 2 deletions api-docs/openapi.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -187,8 +187,16 @@ describe('OpenAPI contract', () => {
const schemas = openapi.components.schemas;
const searchResponse = schemas.SearchResponse;

expect(searchResponse.required).toStrictEqual(['page', 'pageSize', 'totalPages', 'status', 'data']);
expect(searchResponse.properties.pageSize.description).toContain('returned in this response');
expect(searchResponse.required).toStrictEqual([
'page',
'pageSize',
'resultsReturned',
'totalPages',
'status',
'data'
]);
expect(searchResponse.properties.pageSize.description).toContain('requested for this page');
expect(searchResponse.properties.resultsReturned.description).toContain('actually returned');
expect(searchResponse.properties.data.properties.hits.$ref).toBe('#/components/schemas/SearchHits');
expect(schemas.SearchHits.properties.hits.items.$ref).toBe('#/components/schemas/SearchHit');
expect(schemas.ApiError.required).toStrictEqual(['error', 'message']);
Expand Down
5 changes: 3 additions & 2 deletions controllers/searchController.js
Original file line number Diff line number Diff line change
Expand Up @@ -218,13 +218,14 @@ async function searchHandler(req, res, next) {
//Calculate current page and total pages to append to JSON output
const totalValue = resp.data.hits.total.value;
const requestedPageSize = pagingOptions.resultsPerPage;
const pageSize = resp.data.hits.hits.length;
const resultsReturned = resp.data.hits.hits.length;
const page = totalValue === 0 ? 0 : Math.floor(pagingOptions.from / requestedPageSize) + 1;
const totalPages = totalValue === 0 ? 0 : Math.ceil(totalValue / requestedPageSize);

return res.status(200).json({
page,
pageSize,
pageSize: requestedPageSize,
resultsReturned,
totalPages,
...resp
});
Expand Down
14 changes: 10 additions & 4 deletions controllers/searchController.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,8 @@ describe('Search controller SEARCH handler', () => {
expect(res.status).toHaveBeenCalledWith(200);
expect(res.json).toHaveBeenCalledWith({
page: 3,
pageSize: 1,
pageSize: 10,
resultsReturned: 1,
totalPages: 3,
...resp
});
Expand Down Expand Up @@ -523,6 +524,7 @@ describe('Search controller SEARCH handler', () => {
expect(res.json).toHaveBeenCalledWith({
page: 1,
pageSize: 1,
resultsReturned: 1,
totalPages: 1,
status: 'ok',
data: {
Expand Down Expand Up @@ -734,6 +736,7 @@ describe('Search controller SEARCH handler', () => {
expect(res.json).toHaveBeenCalledWith({
page: 1,
pageSize: 1,
resultsReturned: 1,
totalPages: 1,
status: 'ok',
data: {
Expand Down Expand Up @@ -789,6 +792,7 @@ describe('Search controller SEARCH handler', () => {
expect(res.json).toHaveBeenCalledWith({
page: 1,
pageSize: 1,
resultsReturned: 1,
totalPages: 1,
status: 'ok',
data: {
Expand Down Expand Up @@ -1034,7 +1038,7 @@ describe('Search controller SEARCH handler', () => {
expect(mockApiSearch).not.toHaveBeenCalled();
});

test('SEARCH returns zeroed pagination metadata when OpenSearch returns zero hits', async () => {
test('SEARCH retains requested page size and reports zero returned results when OpenSearch returns zero hits', async () => {
const resp = advancedResponse(0);
mockApiSearch.mockResolvedValue(resp);

Expand All @@ -1049,7 +1053,8 @@ describe('Search controller SEARCH handler', () => {
expect(res.status).toHaveBeenCalledWith(200);
expect(res.json).toHaveBeenCalledWith({
page: 0,
pageSize: 0,
pageSize: 20,
resultsReturned: 0,
totalPages: 0,
...resp
});
Expand All @@ -1073,7 +1078,8 @@ describe('Search controller SEARCH handler', () => {
expect(res.status).toHaveBeenCalledWith(200);
expect(res.json).toHaveBeenCalledWith({
page: 2,
pageSize: 0,
pageSize: 10,
resultsReturned: 0,
totalPages: 1,
...resp
});
Expand Down
1 change: 1 addition & 0 deletions routes/search.integration.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ describe('search route integration', () => {
);
expect(response.body.page).toBe(1);
expect(response.body.pageSize).toBe(1);
expect(response.body.resultsReturned).toBe(1);
expect(response.body.totalPages).toBe(1);
});

Expand Down
9 changes: 7 additions & 2 deletions routes/swagger.js
Original file line number Diff line number Diff line change
Expand Up @@ -417,17 +417,22 @@ const doc = {
SearchResponse: {
type: 'object',
additionalProperties: false,
required: ['page', 'pageSize', 'totalPages', 'status', 'data'],
required: ['page', 'pageSize', 'resultsReturned', 'totalPages', 'status', 'data'],
properties: {
page: {
type: 'integer',
minimum: 0,
description: 'One-based page number, or 0 when the search has no matches.'
},
pageSize: {
type: 'integer',
minimum: 1,
description: 'Maximum number of CVE records requested for this page.'
},
resultsReturned: {
type: 'integer',
minimum: 0,
description: 'Number of CVE records returned in this response.'
description: 'Number of CVE records actually returned in this response.'
},
totalPages: {
type: 'integer',
Expand Down
Loading
Loading