Skip to content

Commit 82b6bae

Browse files
Merge pull request #523 from jaredhendrickson13/next_patch
2 parents 6328750 + 9200b1e commit 82b6bae

File tree

5 files changed

+171
-84
lines changed

5 files changed

+171
-84
lines changed

docs/QUERIES_AND_FILTERS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,3 +103,4 @@ reduce the amount of data returned in a single request.
103103
- By default, the REST API does not paginate responses. If you want to paginate the response, you must include the
104104
`limit` and `offset` query parameters in your request.
105105
- Pagination is only available for `GET` requests to [plural endpoints](./ENDPOINT_TYPES.md#plural-many-endpoints).
106+
- If combined with a query, pagination will be applied after the initial query is executed.

pfSense-pkg-RESTAPI/files/etc/inc/priv/restapi.priv.inc

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,11 @@
1313
// See the License for the specific language governing permissions and
1414
// limitations under the License.
1515

16-
require_once 'RESTAPI/Caches/PrivilegesCache.inc';
17-
18-
use RESTAPI\Caches\PrivilegesCache;
19-
2016
global $priv_list;
2117

2218
# Include the privileges that are auto-generated by \RESTAPI\Caches\PrivilegesCache
23-
$privileges_cache = new PrivilegesCache();
24-
if (is_file($privileges_cache->get_file_path())) {
25-
$priv_list += $privileges_cache->read();
19+
if (is_file('/usr/local/pkg/RESTAPI/.resources/cache/PrivilegesCache.json')) {
20+
$privs_json = file_get_contents('/usr/local/pkg/RESTAPI/.resources/cache/PrivilegesCache.json');
21+
$privileges_cache = json_decode($privs_json, associative: true);
22+
$priv_list += $privileges_cache;
2623
}

pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Model.inc

Lines changed: 115 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,8 @@ class Model {
356356
private function construct_from_internal(mixed $id = null, mixed $parent_id = null): void {
357357
# Do not try to load the object from internal if the skip_init flag is set
358358
if ($this->skip_init) {
359+
$this->id = $id;
360+
$this->parent_id = $parent_id;
359361
return;
360362
}
361363

@@ -1567,79 +1569,6 @@ class Model {
15671569
$this->apply();
15681570
}
15691571

1570-
/**
1571-
* Fetches Model objects for all objects stored in the internal pfSense values. If `config_path` is set, this will
1572-
* load Model objects for each object stored at the config path. If `internal_callable` is set, this will create
1573-
* Model objects for each object returned by the specified callable.
1574-
* @param mixed|null $parent_id Specifies the ID of the parent Model to read all objects from. This is required for
1575-
* $many Models with a $parent_model_class. This value has no affect otherwise.
1576-
* @param int $offset The starting point in the dataset to be used with $limit. This is only applicable to $many
1577-
* enabled Models.
1578-
* @param int $limit The maximum number of Model objects to retrieve. This is only applicable to $many
1579-
* enabled Models.
1580-
* @return ModelSet|Model Returns a ModelSet of Models if `many` is enabled or a single Model object if `many` is
1581-
* not enabled.
1582-
*/
1583-
public static function read_all(mixed $parent_id = null, int $limit = 0, int $offset = 0): ModelSet|Model {
1584-
# Variables
1585-
$model_name = get_called_class();
1586-
$model = new $model_name(parent_id: $parent_id);
1587-
$model_objects = [];
1588-
$is_parent_model_many = $model->is_parent_model_many();
1589-
$requests_pagination = ($limit or $offset);
1590-
$offset_counter = 0;
1591-
1592-
# Throw an error if this Model has a $many parent Model, but no parent Model ID was given
1593-
if ($is_parent_model_many and !isset($parent_id)) {
1594-
throw new ValidationError(
1595-
message: 'Field `parent_id` is required to read all.',
1596-
response_id: 'MODEL_PARENT_ID_REQUIRED',
1597-
);
1598-
}
1599-
1600-
# Throw an error if pagination was requested on a Model without $many enabled
1601-
if (!$model->many and $requests_pagination) {
1602-
throw new ValidationError(
1603-
message: "Model `$model->verbose_name` does not support pagination. Please remove the `limit` and/or. " .
1604-
'`offset`parameters and try again.',
1605-
response_id: 'MODEL_DOES_NOT_SUPPORT_PAGINATION',
1606-
);
1607-
}
1608-
1609-
# Obtain all of this Model's internally stored objects
1610-
$internal_objects = $model->get_internal_objects();
1611-
1612-
# For non `many` Models, wrap the internal object in an array so we can loop
1613-
$internal_objects = $model->many ? $internal_objects : [$internal_objects];
1614-
1615-
# Loop through each internal object and create a Model object for it
1616-
foreach ($internal_objects as $internal_id => $internal_object) {
1617-
# Do not include this object if we have not reached our offset
1618-
if ($offset_counter < $offset) {
1619-
$offset_counter++;
1620-
continue;
1621-
}
1622-
1623-
# Create a new Model object for this internal object and assign its ID
1624-
$model_object = new $model(id: $internal_id, parent_id: $parent_id);
1625-
1626-
# Populate the Model object using its current internal values and add it to the array of all Model objects
1627-
$model_object->from_internal_object($internal_object);
1628-
$model_objects[] = $model_object;
1629-
1630-
# Break the loop if $limit is set, and we've met our limit of objects to obtain
1631-
if ($limit and $limit === count($model_objects)) {
1632-
break;
1633-
}
1634-
1635-
# Increase the offset counter
1636-
$offset_counter++;
1637-
}
1638-
1639-
# Unwrap the array for non `many` Models, otherwise return all objects
1640-
return $model->many ? new ModelSet($model_objects) : $model_objects[0];
1641-
}
1642-
16431572
/**
16441573
* Sorts `many` Model entries internally before writing the changes to config. This is useful for Model's whose
16451574
* internal objects must be written in a specific order.
@@ -1739,6 +1668,82 @@ class Model {
17391668
}
17401669
}
17411670

1671+
/**
1672+
* Fetches Model objects for all objects stored in the internal pfSense values. If `config_path` is set, this will
1673+
* load Model objects for each object stored at the config path. If `internal_callable` is set, this will create
1674+
* Model objects for each object returned by the specified callable.
1675+
* @param mixed|null $parent_id Specifies the ID of the parent Model to read all objects from. This is required for
1676+
* $many Models with a $parent_model_class. This value has no affect otherwise.
1677+
* @param int $offset The starting point in the dataset to be used with $limit. This is only applicable to $many
1678+
* enabled Models.
1679+
* @param int $limit The maximum number of Model objects to retrieve. This is only applicable to $many
1680+
* enabled Models.
1681+
* @return ModelSet|Model Returns a ModelSet of Models if `many` is enabled or a single Model object if `many` is
1682+
* not enabled.
1683+
*/
1684+
public static function read_all(mixed $parent_id = null, int $limit = 0, int $offset = 0): ModelSet|Model {
1685+
# Variables
1686+
$model_name = get_called_class();
1687+
$model = new $model_name(parent_id: $parent_id);
1688+
$model_objects = [];
1689+
$is_parent_model_many = $model->is_parent_model_many();
1690+
$requests_pagination = ($limit or $offset);
1691+
$offset_counter = 0;
1692+
1693+
# Throw an error if this Model has a $many parent Model, but no parent Model ID was given
1694+
if ($is_parent_model_many and !isset($parent_id)) {
1695+
throw new ValidationError(
1696+
message: 'Field `parent_id` is required to read all.',
1697+
response_id: 'MODEL_PARENT_ID_REQUIRED',
1698+
);
1699+
}
1700+
1701+
# Throw an error if pagination was requested on a Model without $many enabled
1702+
if (!$model->many and $requests_pagination) {
1703+
throw new ValidationError(
1704+
message: "Model `$model->verbose_name` does not support pagination. Please remove the `limit` and/or. " .
1705+
'`offset`parameters and try again.',
1706+
response_id: 'MODEL_DOES_NOT_SUPPORT_PAGINATION',
1707+
);
1708+
}
1709+
1710+
# Obtain all of this Model's internally stored objects
1711+
$internal_objects = $model->get_internal_objects();
1712+
1713+
# For non `many` Models, wrap the internal object in an array so we can loop
1714+
$internal_objects = $model->many ? $internal_objects : [$internal_objects];
1715+
1716+
# Loop through each internal object and create a Model object for it
1717+
foreach ($internal_objects as $internal_id => $internal_object) {
1718+
# Do not include this object if we have not reached our offset
1719+
if ($offset_counter < $offset) {
1720+
$offset_counter++;
1721+
continue;
1722+
}
1723+
1724+
# Create a new Model object for this internal object and assign its ID
1725+
$model_object = new $model(id: $internal_id, parent_id: $parent_id, skip_init: true);
1726+
1727+
# Obtain the parent Model object if this Model has a parent Model class assigned
1728+
$model_object->get_parent_model();
1729+
1730+
# Populate the Model object using its current internal values and add it to the array of all Model objects
1731+
$model_object->from_internal_object($internal_object);
1732+
$model_objects[] = $model_object;
1733+
1734+
# Break the loop if $limit is set, and we've met our limit of objects to obtain
1735+
if ($limit and $limit === count($model_objects)) {
1736+
break;
1737+
}
1738+
1739+
# Increase the offset counter
1740+
$offset_counter++;
1741+
}
1742+
1743+
# Unwrap the array for non `many` Models, otherwise return all objects
1744+
return $model->many ? new ModelSet($model_objects) : $model_objects[0];
1745+
}
1746+
17421747
/**
17431748
* Performs a query on all Model objects for this Model. This is essentially a shorthand way of calling
17441749
* `query()`. This method is only applicable to `many` Models.
@@ -1766,10 +1771,43 @@ class Model {
17661771
# Merge the $query_params and any provided variable-length arguments into a single variable
17671772
$query_params = array_merge($query_params, $vl_query_params);
17681773

1769-
return self::read_all(parent_id: $parent_id, limit: $limit, offset: $offset)->query(
1770-
query_params: $query_params,
1771-
excluded: $excluded,
1772-
);
1774+
# If no query parameters were provided, just run read_all() with pagination for optimal performance
1775+
if (!$query_params) {
1776+
return self::read_all(parent_id: $parent_id, limit: $limit, offset: $offset);
1777+
}
1778+
1779+
# Perform the query against all Model objects for this Model first
1780+
$modelset = self::read_all(parent_id: $parent_id)->query(query_params: $query_params, excluded: $excluded);
1781+
1782+
# Apply pagination to limit the number of objects returned if requested
1783+
$modelset->model_objects = self::paginate($modelset->model_objects, $limit, $offset);
1784+
return $modelset;
1785+
}
1786+
1787+
/**
1788+
* Paginates a given array but obtaining a smaller subset of the array based on the provided limit and offset values.
1789+
* @param int $limit The maximum number of items to return in the paginated array. Use 0 to impose no limit.
1790+
* @param int $offset The starting point in the array to begin the paginated array.
1791+
* @return array The paginated array containing only the subset of items that match the limit and offset values.
1792+
*/
1793+
public static function paginate(array $array, int $limit, int $offset): array {
1794+
# Return the entire array if no limit or offset is set
1795+
if (!$limit and !$offset) {
1796+
return $array;
1797+
}
1798+
1799+
# Return an empty array if the offset is greater than the array length
1800+
if ($offset >= count($array)) {
1801+
return [];
1802+
}
1803+
1804+
# Return the entire array if the limit is set to 0
1805+
if (!$limit) {
1806+
return array_slice($array, $offset);
1807+
}
1808+
1809+
# Return the subset of the array that matches the limit and offset values
1810+
return array_slice($array, $offset, $limit);
17731811
}
17741812

17751813
/**

pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/ModelSet.inc

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,14 @@ class ModelSet {
4545
return count($this->model_objects) > 0;
4646
}
4747

48+
/**
49+
* Returns the number of Model objects in this ModelSet.
50+
* @return int The number of Model objects in this ModelSet.
51+
*/
52+
public function count(): int {
53+
return count($this->model_objects);
54+
}
55+
4856
/**
4957
* Returns the first Model object in the ModelSet. This is helpful when locating the first Model object that matched
5058
* a query.

pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APICoreModelTestCase.inc

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -764,4 +764,47 @@ class APICoreModelTestCase extends RESTAPI\Core\TestCase {
764764
# Delete the alias
765765
$alias->delete();
766766
}
767+
768+
/**
769+
* Checks that the 'paginate()' method works correctly.
770+
*/
771+
public function test_paginate(): void {
772+
# Ensure the paginate method correctly returns the paginated subset of a given array
773+
$this->assert_equals(Model::paginate([1, 2, 3, 4, 5], limit: 2, offset: 1), [2, 3]);
774+
$this->assert_equals(Model::paginate([1, 2, 3, 4, 5], limit: 2, offset: 3), [4, 5]);
775+
$this->assert_equals(Model::paginate([1, 2, 3, 4, 5], limit: 2, offset: 5), []);
776+
$this->assert_equals(Model::paginate([1, 2, 3, 4, 5], limit: 4, offset: 0), [1, 2, 3, 4]);
777+
$this->assert_equals(Model::paginate([1, 2, 3, 4, 5], limit: 4, offset: 1), [2, 3, 4, 5]);
778+
779+
# Ensure a limit of 0 imposes no limit
780+
$this->assert_equals(Model::paginate([1, 2, 3, 4, 5], 0, 0), [1, 2, 3, 4, 5]);
781+
}
782+
783+
/**
784+
* Ensures that queries are performed before pagination is applied.
785+
*/
786+
public function test_paginate_with_query(): void {
787+
# Create FirewallAlias models to test this
788+
$alias_alias_0 = new FirewallAlias(name: 'alias0', type: 'host');
789+
$alias_alias_0->create();
790+
$alias_alias_1 = new FirewallAlias(name: 'alias1', type: 'port');
791+
$alias_alias_1->create();
792+
$alias_alias_2 = new FirewallAlias(name: 'alias2', type: 'network');
793+
$alias_alias_2->create();
794+
$alias_alias_3 = new FirewallAlias(name: 'alias3', type: 'host');
795+
$alias_alias_3->create();
796+
$alias_alias_4 = new FirewallAlias(name: 'alias4', type: 'port');
797+
$alias_alias_4->create();
798+
799+
# Query for only port aliases, but limit it to 2
800+
$port_aliases = FirewallAlias::query(limit: 2, type: 'port');
801+
$this->assert_equals($port_aliases->count(), 2);
802+
$this->assert_equals($port_aliases->model_objects[0]->name->value, 'alias1');
803+
$this->assert_equals($port_aliases->model_objects[1]->name->value, 'alias4');
804+
805+
# Query again but change to offset to exclude the first port alias
806+
$port_aliases = FirewallAlias::query(limit: 2, offset: 1, type: 'port');
807+
$this->assert_equals($port_aliases->count(), 1);
808+
$this->assert_equals($port_aliases->model_objects[0]->name->value, 'alias4');
809+
}
767810
}

0 commit comments

Comments
 (0)