Skip to content

Commit df3c233

Browse files
authored
1154 enhancement apiv2 return total amount of pages for paginated api calls (#1173)
* FEAT: Added total page size in meta data and get correct first page for pagination * FEAT: made pagination default page size and maximum page size configurable through config * Fixed bug in upgrade script
1 parent f269948 commit df3c233

File tree

4 files changed

+56
-13
lines changed

4 files changed

+56
-13
lines changed

src/inc/apiv2/common/AbstractModelAPI.class.php

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use Middlewares\Utils\HttpErrorException;
1010

1111
use DBA\AbstractModelFactory;
12+
use DBA\Aggregation;
1213
use DBA\JoinFilter;
1314
use DBA\Factory;
1415
use DBA\ContainFilter;
@@ -382,9 +383,11 @@ public static function getManyResources(object $apiClass, Request $request, Resp
382383
$aliasedfeatures = $apiClass->getAliasedFeatures();
383384
$factory = $apiClass->getFactory();
384385

385-
// TODO: Maximum and default should be configurable per server instance
386386
$defaultPageSize = 10000;
387387
$maxPageSize = 50000;
388+
// TODO: if 0.14.4 release has happened, following parameters can be retrieved from config
389+
// $defaultPageSize = SConfig::getInstance()->getVal(DConfig::DEFAULT_PAGE_SIZE);
390+
// $maxPageSize = SConfig::getInstance()->getVal(DConfig::MAX_PAGE_SIZE);
388391

389392
$pageAfter = $apiClass->getQueryParameterFamilyMember($request, 'page', 'after') ?? 0;
390393
$pageSize = $apiClass->getQueryParameterFamilyMember($request, 'page', 'size') ?? $defaultPageSize;
@@ -426,18 +429,28 @@ public static function getManyResources(object $apiClass, Request $request, Resp
426429
/* Include relation filters */
427430
$finalFs = array_merge($aFs, $relationFs);
428431

432+
$primaryKey = $apiClass->getPrimaryKey();
429433
//according to JSON API spec, first and last have to be calculated if inexpensive to compute
430434
//(https://jsonapi.org/profiles/ethanresnick/cursor-pagination/#auto-id-links))
431-
//if this query is too expensive for big tables, it should be removed
432-
$max = $factory->minMaxFilter($finalFs, $apiClass->getPrimaryKey(), "MAX");
435+
//if this query is too expensive for big tables, it can be removed
436+
$agg1 = new Aggregation($primaryKey, Aggregation::MAX);
437+
$agg2 = new Aggregation($primaryKey, Aggregation::MIN);
438+
$agg3 = new Aggregation($primaryKey, Aggregation::COUNT);
439+
$aggregation_results = $factory->multicolAggregationFilter($finalFs, [$agg1, $agg2, $agg3]);
440+
441+
$max = $aggregation_results[$agg1->getName()];
442+
$min = $aggregation_results[$agg2->getName()];
443+
$total = $aggregation_results[$agg3->getName()];
444+
445+
$totalPages = ceil($total / $pageSize);
433446

434447
//pagination filters need to be added after max has been calculated
435448
$finalFs[Factory::LIMIT] = new LimitFilter($pageSize);
436449

437-
$finalFs[Factory::FILTER][] = new QueryFilter($apiClass->getPrimaryKey(), $pageAfter, '>', $factory);
450+
$finalFs[Factory::FILTER][] = new QueryFilter($primaryKey, $pageAfter, '>', $factory);
438451
$pageBefore = $apiClass->getQueryParameterFamilyMember($request, 'page', 'before');
439452
if (isset($pageBefore)) {
440-
$finalFs[Factory::FILTER][] = new QueryFilter($apiClass->getPrimaryKey(), $pageBefore, '<', $factory);
453+
$finalFs[Factory::FILTER][] = new QueryFilter($primaryKey, $pageBefore, '<', $factory);
441454
}
442455

443456
/* Request objects */
@@ -525,12 +538,8 @@ public static function getManyResources(object $apiClass, Request $request, Resp
525538
}
526539
// Build prev link
527540
$prevId = $defaultSort == "DESC" ? $maxId : $minId;
528-
if ($prevId != 1) { //only set previous page when its not the first page
541+
if ($prevId != $min) { //only set previous page when its not the first page
529542
$prevParams = $selfParams;
530-
//This scenario might return a link to an empty array if the elements with the lowest id are deleted, but this is allowed according
531-
//to the json API spec https://jsonapi.org/profiles/ethanresnick/cursor-pagination/#auto-id-links
532-
//We could also get the lowest id the same way we got the max, but this is probably unnecessary expensive.
533-
//But pull request: https://github.com/hashtopolis/server/pull/1069 would create a cheaper way of doing this in a single query
534543
$prevParams['page']['before'] = $prevId;
535544
unset($prevParams['page']['after']);
536545
$linksPrev = $request->getUri()->getPath() . '?' . urldecode(http_build_query($prevParams));
@@ -541,7 +550,7 @@ public static function getManyResources(object $apiClass, Request $request, Resp
541550
$firstParams = $request->getQueryParams();
542551
unset($firstParams['page']['before']);
543552
$firstParams['page']['size'] = $pageSize;
544-
$firstParams['page']['after'] = 0;
553+
$firstParams['page']['after'] = $min;
545554
$linksFirst = $request->getUri()->getPath() . '?' . urldecode(http_build_query($firstParams));
546555
$links = [
547556
"self" => $linksSelf,
@@ -551,8 +560,9 @@ public static function getManyResources(object $apiClass, Request $request, Resp
551560
"prev" => $linksPrev,
552561
];
553562

563+
$metadata = ["page" => ["total_pages" => $totalPages]];
554564
// Generate JSON:API GET output
555-
$ret = self::createJsonResponse($dataResources, $links, $includedResources);
565+
$ret = self::createJsonResponse($dataResources, $links, $includedResources, $metadata);
556566

557567
$body = $response->getBody();
558568
$body->write($apiClass->ret2json($ret));

src/inc/defines/config.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ class DConfig {
6868
const HASH_MAX_LENGTH = "hashMaxLength";
6969
const MAX_HASHLIST_SIZE = "maxHashlistSize";
7070
const UAPI_SEND_TASK_IS_COMPLETE = "uApiSendTaskIsComplete";
71+
const DEFAULT_PAGE_SIZE = "defaultPageSize";
72+
const MAX_PAGE_SIZE = "maxPageSize";
7173

7274
// Section: UI
7375
const TIME_FORMAT = "timefmt";
@@ -272,6 +274,10 @@ public static function getConfigType($config) {
272274
return DConfigType::TICKBOX;
273275
case DConfig::HC_ERROR_IGNORE:
274276
return DConfigType::STRING_INPUT;
277+
case DConfig::DEFAULT_PAGE_SIZE:
278+
return DConfigType::NUMBER_INPUT;
279+
case DConfig::MAX_PAGE_SIZE:
280+
return DConfigType::NUMBER_INPUT;
275281
}
276282
return DConfigType::STRING_INPUT;
277283
}
@@ -406,6 +412,10 @@ public static function getConfigDescription($config) {
406412
return "Also send 'isComplete' for each task on the User API when listing all tasks (might affect performance)";
407413
case DConfig::HC_ERROR_IGNORE:
408414
return "Ignore error messages from crackers which contain given strings (multiple values separated by comma)";
415+
case DConfig::DEFAULT_PAGE_SIZE:
416+
return "The default page size of items that are returned in API calls.";
417+
case DConfig::MAX_PAGE_SIZE:
418+
return "The maximum page size of items that are allowed to return in an API call.";
409419
}
410420
return $config;
411421
}

src/install/hashtopolis.sql

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,10 @@ INSERT INTO `Config` (`configId`, `configSectionId`, `item`, `value`) VALUES
170170
(74, 4, 'agentUtilThreshold1', '90'),
171171
(75, 4, 'agentUtilThreshold2', '75'),
172172
(76, 3, 'uApiSendTaskIsComplete', '0'),
173-
(77, 1, 'hcErrorIgnore', 'DeviceGetFanSpeed');
173+
(77, 1, 'hcErrorIgnore', 'DeviceGetFanSpeed'),
174+
(78, 3, 'defaultPageSize', '10000'),
175+
(79, 3, 'maxPageSize', '50000');
176+
174177

175178
CREATE TABLE `ConfigSection` (
176179
`configSectionId` INT(11) NOT NULL,
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php /** @noinspection SqlNoDataSourceInspection */
2+
3+
use DBA\Config;
4+
use DBA\Factory;
5+
use DBA\QueryFilter;
6+
7+
if (!isset($PRESENT["v0.14.x_pagination"])) {
8+
$qF = new QueryFilter(Config::ITEM, DConfig::DEFAULT_PAGE_SIZE, "=");
9+
$item = Factory::getConfigFactory()->filter([Factory::FILTER => $qF], true);
10+
if (!$item) {
11+
$config = new Config(null, 3, DConfig::DEFAULT_PAGE_SIZE, '10000');
12+
Factory::getConfigFactory()->save($config);
13+
}
14+
$qF = new QueryFilter(Config::ITEM, DConfig::MAX_PAGE_SIZE, "=");
15+
$item = Factory::getConfigFactory()->filter([Factory::FILTER => $qF], true);
16+
if (!$item) {
17+
$config = new Config(null, 3, DConfig::MAX_PAGE_SIZE, '50000');
18+
Factory::getConfigFactory()->save($config);
19+
}
20+
}

0 commit comments

Comments
 (0)