Skip to content

Commit 4ae5c92

Browse files
refactor: support queries on child models
1 parent 2e22e67 commit 4ae5c92

File tree

9 files changed

+77
-42
lines changed

9 files changed

+77
-42
lines changed

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

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -412,14 +412,6 @@ class Endpoint {
412412
);
413413
}
414414
}
415-
416-
# Do not allow `many` Endpoints that are assigned a Model with a parent Model
417-
if ($this->many and $this->model->parent_model_class) {
418-
throw new ServerError(
419-
message: 'Endpoints cannot enable `many` when the assigned Model has a parent Model',
420-
response_id: 'ENDPOINT_MANY_WHEN_MODEL_HAS_PARENT',
421-
);
422-
}
423415
}
424416

425417
/**

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -731,7 +731,7 @@ class Field {
731731
}
732732

733733
# Use $modelset if provided. Otherwise, use all existing $context model objects as the $modelset
734-
$modelset = $modelset ?: $this->context->read_all(parent_id: $this->context->parent_id);
734+
$modelset = $modelset ?: $this->context->query(parent_id: $this->context->parent_id);
735735

736736
# If this is a $many field, query for any other object has this value in the array
737737
if ($this->many) {

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

Lines changed: 43 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -864,6 +864,33 @@ class Model {
864864
return $internal_object;
865865
}
866866

867+
/**
868+
* Recursively obtains all internal objects for this Model from all parent objects in config. This method is only
869+
* applicable to Models with a `parent_model_class` assigned.
870+
* @return array An array of all internal objects for this Model from all parent objects in
871+
*/
872+
protected function get_internal_objects_from_all_parents(): array {
873+
# Variables
874+
$internal_objects = [];
875+
$parent_model_class = "\\RESTAPI\\Models\\$this->parent_model_class";
876+
$parent_model = new $parent_model_class(skip_init: true);
877+
$parent_configs = $this->get_config($parent_model->config_path, []);
878+
879+
# Check for internal objects from all parents
880+
foreach ($parent_configs as $parent_id => $parent_config) {
881+
# Obtain the list of child objects from this parent config
882+
$child_configs = $this->get_config("$parent_model->config_path/$parent_id/$this->config_path", []);
883+
884+
foreach ($child_configs as $child_id => $child_config) {
885+
$child_config["parent_id"] = $parent_id;
886+
$child_config["id"] = $child_id;
887+
$internal_objects[] = $child_config;
888+
}
889+
}
890+
891+
return $internal_objects;
892+
}
893+
867894
/**
868895
* Obtains all internal objects for this Model. When a `config_path` is specified, this method will obtain the
869896
* internal objects directly from config. When an `internal_callable` is assigned, this method will return
@@ -886,6 +913,10 @@ class Model {
886913
elseif ($mock_internal_objects) {
887914
$internal_objects = $mock_internal_objects;
888915
}
916+
# Obtain all internal objects from all parents if a parent Model class is assigned
917+
elseif ($this->parent_model_class) {
918+
$internal_objects = $this->get_internal_objects_from_all_parents();
919+
}
889920
# Obtain the internal objects from the config path if specified
890921
elseif ($this->config_path) {
891922
$internal_objects = $this->get_config($this->get_config_path(), []);
@@ -913,7 +944,7 @@ class Model {
913944
* @throws ServerError When this Model does not have a `config_path` set.
914945
* @throws NotFoundError When an object with the specified $id does not exist.
915946
*/
916-
public function from_internal() {
947+
public function from_internal(): void {
917948
# Require a `parent_id` if a `many` parent Model is assigned
918949
if ($this->parent_model_class and $this->parent_model->many and !isset($this->parent_id)) {
919950
throw new ServerError(
@@ -1319,7 +1350,7 @@ class Model {
13191350
}
13201351

13211352
# Use the $modelset if provided, otherwise obtain a ModelSet of all existing objects for this Model
1322-
$modelset = $modelset ?: $this->read_all(parent_id: $this->parent_id);
1353+
$modelset = $modelset ?: $this->query(parent_id: $this->parent_id);
13231354

13241355
# Use this variable to keep track of query parameters to use when checking uniqueness
13251356
$query_params = [];
@@ -1415,7 +1446,7 @@ class Model {
14151446

14161447
# For 'many' Models, capture all current Models in a ModelSet if one was not already given
14171448
if ($this->many and !$modelset) {
1418-
$modelset = $this->read_all(parent_id: $this->parent_id);
1449+
$modelset = $this->query(parent_id: $this->parent_id);
14191450
}
14201451

14211452
# Loop through each of this object's assigned Fields and validate them.
@@ -1887,8 +1918,6 @@ class Model {
18871918
* Fetches Model objects for all objects stored in the internal pfSense values. If `config_path` is set, this will
18881919
* load Model objects for each object stored at the config path. If `internal_callable` is set, this will create
18891920
* Model objects for each object returned by the specified callable.
1890-
* @param mixed|null $parent_id Specifies the ID of the parent Model to read all objects from. This is required for
1891-
* $many Models with a $parent_model_class. This value has no affect otherwise.
18921921
* @param int $offset The starting point in the dataset to be used with $limit. This is only applicable to $many
18931922
* enabled Models.
18941923
* @param int $limit The maximum number of Model objects to retrieve. This is only applicable to $many
@@ -1899,26 +1928,16 @@ class Model {
18991928
* not enabled.
19001929
*/
19011930
public static function read_all(
1902-
mixed $parent_id = null,
19031931
int $limit = 0,
19041932
int $offset = 0,
19051933
bool $reverse = false,
19061934
): ModelSet|Model {
19071935
# Variables
19081936
$model_name = get_called_class();
1909-
$model = new $model_name(parent_id: $parent_id);
1937+
$model = new $model_name();
19101938
$model_objects = [];
1911-
$is_parent_model_many = $model->is_parent_model_many();
19121939
$requests_pagination = ($limit or $offset);
1913-
$cache_exempt = ($requests_pagination or $reverse or isset($parent_id));
1914-
1915-
# Throw an error if this Model has a $many parent Model, but no parent Model ID was given
1916-
if ($is_parent_model_many and !isset($parent_id)) {
1917-
throw new ValidationError(
1918-
message: 'Field `parent_id` is required to read all.',
1919-
response_id: 'MODEL_PARENT_ID_REQUIRED',
1920-
);
1921-
}
1940+
$cache_exempt = ($requests_pagination or $reverse);
19221941

19231942
# Throw an error if pagination was requested on a Model without $many enabled
19241943
if (!$model->many and $requests_pagination) {
@@ -1946,8 +1965,11 @@ class Model {
19461965

19471966
# Loop through each internal object and create a Model object for it
19481967
foreach ($internal_objects as $internal_id => $internal_object) {
1949-
# Ensure numeric IDs are converted to integers
1968+
# Populate the ID and parent ID values where applicable
1969+
$internal_id = array_key_exists('id', $internal_object) ? $internal_object['id'] : $internal_id;
19501970
$internal_id = is_numeric($internal_id) ? (int) $internal_id : $internal_id;
1971+
$parent_id = array_key_exists('parent_id', $internal_object) ? $internal_object['parent_id'] : null;
1972+
$parent_id = is_numeric($parent_id) ? (int) $parent_id : $parent_id;
19511973

19521974
# Create a new Model object for this internal object and assign its ID
19531975
$model_object = new $model(id: $internal_id, parent_id: $parent_id, skip_init: true);
@@ -1978,8 +2000,6 @@ class Model {
19782000
* @param array $query_params An array of query parameters.
19792001
* @param array $excluded An array of field names to exclude from the query. This is helpful when
19802002
* query data may have extra values that you do not want to include in the query.
1981-
* @param mixed|null $parent_id Specifies the ID of the parent Model to read all objects from. This is required for
1982-
* $many Models with a $parent_model_class. This value has no affect otherwise.
19832003
* @param int $offset The starting point in the dataset to be used with $limit. This is only applicable to $many
19842004
* enabled Models.
19852005
* @param int $limit The maximum number of Model objects to retrieve. This is only applicable to $many
@@ -1993,7 +2013,6 @@ class Model {
19932013
public static function query(
19942014
array $query_params = [],
19952015
array $excluded = [],
1996-
mixed $parent_id = null,
19972016
int $limit = 0,
19982017
int $offset = 0,
19992018
bool $reverse = false,
@@ -2007,11 +2026,11 @@ class Model {
20072026

20082027
# If no query or sort parameters were provided, just run read_all() with pagination for optimal performance
20092028
if (!$query_params and $sort_by === null) {
2010-
return self::read_all(parent_id: $parent_id, limit: $limit, offset: $offset, reverse: $reverse);
2029+
return self::read_all(limit: $limit, offset: $offset, reverse: $reverse);
20112030
}
20122031

20132032
# Perform the query against all Model objects for this Model first
2014-
$modelset = self::read_all(parent_id: $parent_id)->query(query_params: $query_params, excluded: $excluded);
2033+
$modelset = self::read_all()->query(query_params: $query_params, excluded: $excluded);
20152034

20162035
# Sort the set if a sort field was provided
20172036
if ($sort_by) {
@@ -2277,7 +2296,7 @@ class Model {
22772296
# Keep track of all existing objects for this Model before anything is changed. This will be passed back
22782297
# into `apply_replace_all()` so that method can gracefully bring down these objects before replacing them
22792298
# if needed.
2280-
$initial_objects = $this->read_all(parent_id: $this->parent_id);
2299+
$initial_objects = $this->query(parent_id: $this->parent_id);
22812300

22822301
# Obtain any Models that are deemed protected to ensure they do not removed in the next step.
22832302
$new_objects = new ModelSet();
@@ -2453,7 +2472,6 @@ class Model {
24532472
$model_objects = self::query(
24542473
query_params: $query_params,
24552474
excluded: $excluded,
2456-
parent_id: $parent_id,
24572475
limit: $limit,
24582476
offset: $offset,
24592477
);

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,8 +206,9 @@ class ModelSet {
206206
continue;
207207
}
208208

209-
# Obtain the value for this field. If it is the ID, handle accordingly.
209+
# Obtain the value for this field. If it is the ID or parent ID, handle accordingly.
210210
$field_value = $field === 'id' ? $model_object->id : $model_object->$field->value;
211+
$field_value = $field === 'parent_id' ? $model_object->parent_id : $field_value;
211212

212213
# Ensure the filter matches
213214
$filter = QueryFilter::get_by_name($filter_name);
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
namespace RESTAPI\Endpoints;
4+
5+
require_once 'RESTAPI/autoloader.inc';
6+
7+
use RESTAPI\Core\Endpoint;
8+
9+
/**
10+
* Defines an Endpoint for interacting with a singular DNSResolverHostOverrideAlias Model object at
11+
* /api/v2/services/dns_resolver/host_override/alias.
12+
*/
13+
class ServicesDNSResolverHostOverrideAliasesEndpoint extends Endpoint {
14+
public function __construct() {
15+
# Set Endpoint attributes
16+
$this->url = '/api/v2/services/dns_resolver/host_override/aliases';
17+
$this->model_name = 'DNSResolverHostOverrideAlias';
18+
$this->many = true;
19+
$this->request_method_options = ['GET', 'DELETE'];
20+
21+
# Construct the parent Endpoint object
22+
parent::__construct();
23+
}
24+
}

pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/DHCPServer.inc

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -461,7 +461,7 @@ class DHCPServer extends Model {
461461
*/
462462
public function validate_staticarp(bool $staticarp): bool {
463463
# Do not allow `staticarp` to be enabled if there are any configured static mappings without IPs
464-
$static_mappings = DHCPServerStaticMapping::read_all(parent_id: $this->id);
464+
$static_mappings = DHCPServerStaticMapping::query(parent_id: $this->id);
465465
foreach ($static_mappings->model_objects as $static_mapping) {
466466
if ($staticarp and !$static_mapping->ipaddr->value) {
467467
throw new ValidationError(
@@ -484,15 +484,15 @@ class DHCPServer extends Model {
484484
*/
485485
private function get_range_overlap(string $range_from, string $range_to): ?Model {
486486
# Ensure range does not overlap with existing static mappings
487-
$static_mappings = DHCPServerStaticMapping::read_all(parent_id: $this->id);
487+
$static_mappings = DHCPServerStaticMapping::query(parent_id: $this->id);
488488
foreach ($static_mappings->model_objects as $static_mapping) {
489489
if (is_inrange_v4($static_mapping->ipaddr->value, $range_from, $range_to)) {
490490
return $static_mapping;
491491
}
492492
}
493493

494494
# Ensure range does not overlap with existing address pools `range_from` or `range_to` addresses
495-
$pools = DHCPServerAddressPool::read_all(parent_id: $this->id);
495+
$pools = DHCPServerAddressPool::query(parent_id: $this->id);
496496
foreach ($pools->model_objects as $pool) {
497497
if (is_inrange_v4($pool->range_from->value, $range_from, $range_to)) {
498498
return $pool;

pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/DHCPServerAddressPool.inc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -314,7 +314,7 @@ class DHCPServerAddressPool extends Model {
314314
}
315315

316316
# Ensure range does not overlap with existing static mappings
317-
$static_mappings = DHCPServerStaticMapping::read_all(parent_id: $this->parent_id);
317+
$static_mappings = DHCPServerStaticMapping::query(parent_id: $this->parent_id);
318318
foreach ($static_mappings->model_objects as $static_mapping) {
319319
if (is_inrange_v4($static_mapping->ipaddr->value, $range_from, $range_to)) {
320320
return $static_mapping;

pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/DHCPServerStaticMapping.inc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,7 @@ class DHCPServerStaticMapping extends Model {
234234
}
235235

236236
# Ensure IP is not reserved by any existing DHCPServerAddressPools
237-
$pools = DHCPServerAddressPool::read_all(parent_id: $this->parent_id);
237+
$pools = DHCPServerAddressPool::query(parent_id: $this->parent_id);
238238
foreach ($pools->model_objects as $pool) {
239239
if (is_inrange_v4($ipaddr, $pool->range_from->value, $pool->range_to->value)) {
240240
return $pool;

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ class APIModelsRoutingGatewayGroupPriorityTestCase extends TestCase {
5252
$gateway_prio->create();
5353

5454
# Ensure the gateway group object was created
55-
$this->assert_equals(RoutingGatewayGroupPriority::read_all(parent_id: $gateway_group->id)->count(), 2);
55+
$this->assert_equals(RoutingGatewayGroupPriority::query(parent_id: $gateway_group->id)->count(), 2);
5656

5757
# Update the gateway group priority object with new values
5858
$gateway_prio->tier->value = 3;
@@ -67,7 +67,7 @@ class APIModelsRoutingGatewayGroupPriorityTestCase extends TestCase {
6767

6868
# Delete the gateway group priority object and ensure it was deleted
6969
$gateway_prio->delete();
70-
$this->assert_equals(RoutingGatewayGroupPriority::read_all(parent_id: $gateway_group->id)->count(), 1);
70+
$this->assert_equals(RoutingGatewayGroupPriority::query(parent_id: $gateway_group->id)->count(), 1);
7171

7272
# Cleanup
7373
$gateway_group->delete();

0 commit comments

Comments
 (0)