Skip to content

Commit 5f8603a

Browse files
feat: allow sorting by multiple fields
1 parent 3a8b636 commit 5f8603a

11 files changed

Lines changed: 110 additions & 39 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ API package and provide you with the information you need to configure and use t
2323
- [Installation and Configuration](https://pfrest.org/INSTALL_AND_CONFIG/)
2424
- [Authentication and Authorization](https://pfrest.org/AUTHENTICATION_AND_AUTHORIZATION/)
2525
- [Swagger and OpenAPI](https://pfrest.org/SWAGGER_AND_OPENAPI/)
26-
- [Queries and Filters](https://pfrest.org/QUERIES_AND_FILTERS/)
26+
- [Queries, Filters, and Sorting](https://pfrest.org/QUERIES_FILTERS_AND_SORTING/)
2727

2828
## Quickstart
2929

docs/COMMON_CONTROL_PARAMETERS.md

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,5 +84,22 @@ parameters you can use:
8484
looking for an object near the end of the list. Additionally, it is helpful for time-sorted objects, such as logs,
8585
where you may want to view the most recent entries first.
8686

87-
!!! Note
88-
This parameter is only available for `GET` requests to [plural endpoints](ENDPOINT_TYPES.md#plural-many-endpoints).
87+
## sort_by
88+
89+
- Type: String or Array
90+
- Default: _Defaults to the primary sort attribute for the endpoint, typically `null`._
91+
- Description: This parameters allows you to select the fields to use to sort the objects related to the endpoint. The
92+
behavior of this parameter varies based on the request method and endpoint type. Refer to the
93+
[Sorting](QUERIES_FILTERS_AND_SORTING.md#sorting) section for more information.
94+
95+
## sort_order
96+
97+
- Type: String
98+
- Default: `SORT_ASC`
99+
- Choices:
100+
- `SORT_ASC`
101+
- `SORT_DESC`
102+
- Description: This parameter allows you to control the order in which the objects are sorted. The default value is
103+
`SORT_ASC` which sorts the objects in ascending order. Setting this parameter to `SORT_DESC` will sort the objects in
104+
descending order. The behavior of this parameter varies based on the request method and endpoint type. Refer to the
105+
[Sorting](QUERIES_FILTERS_AND_SORTING.md#sorting) section for more information.

docs/ENDPOINT_TYPES.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,9 @@ Some examples of plural endpoints are:
6161
### GET Requests to Plural Endpoints
6262

6363
A `GET` request to a plural endpoint will allow you to retrieve a list of all existing objects at once. GET requests to plural
64-
endpoints also support [querying and filtering](QUERIES_AND_FILTERS.md) to help you narrow down the list of objects
64+
endpoints also support [querying and filtering](QUERIES_FILTERS_AND_SORTING) to help you narrow down the list of objects
6565
returned by specifying criteria for the objects you want to retrieve. This includes support for
66-
[pagination](QUERIES_AND_FILTERS.md#pagination) which will allow you to limit the amount of objects returned in a single request.
66+
[pagination](QUERIES_FILTERS_AND_SORTING#pagination) which will allow you to limit the amount of objects returned in a single request.
6767

6868
### PUT Requests to Plural Endpoints
6969

@@ -87,7 +87,7 @@ one by one, and can instead replace the entire dataset as a whole in a single re
8787

8888
### DELETE Requests to Plural Endpoints
8989

90-
A `DELETE` request to a plural endpoint will allow you to delete many objects at once using a [query](QUERIES_AND_FILTERS.md).
90+
A `DELETE` request to a plural endpoint will allow you to delete many objects at once using a [query](QUERIES_FILTERS_AND_SORTING).
9191
This is useful when you need to remove a large number of objects from the system, such as when decommissioning services,
9292
cleaning up old data, or removing objects that are no longer needed. This is primarily used as a method of deleting
9393
objects without requiring an ID.
Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Queries and Filters
1+
# Queries, Filters and Sorting
22

33
## Queries
44

@@ -114,11 +114,47 @@ Search for objects whose field value matches a given PCRE regular expression.
114114
- Name: `regex`
115115
- Example: `https://pfsense.example.com/api/v2/examples?fieldname__regex=^example`
116116

117-
## Custom Query Filters
117+
### Custom Query Filters
118118

119119
For advanced users, the REST API's framework allows for custom query filter classes to be added using PHP. Refer to
120120
[Building Custom Query Filters](./BUILDING_CUSTOM_QUERY_FILTER_CLASSES.md) for more information.
121121

122+
## Sorting
123+
124+
Sorting can be used to order the data that is returned from the API based on specific criteria, as well as sorting the
125+
objects written to the pfSense configuration. Sorting is controlled by two common control parameters:
126+
[`sort_by`](./COMMON_CONTROL_PARAMETERS.md#sort_by) and [`sort_order`](./COMMON_CONTROL_PARAMETERS.md#sort_order).
127+
128+
!!! Note
129+
- Sorting is only available for model objects that allow many instances, meaning multiple objects of its type can
130+
exist in the pfSense configuration (e.g. firewall rules, static routes, etc.).
131+
- Sorting requires additional processing time and may impact performance. Use sorting only when
132+
necessary.
133+
134+
The behavior of sorting varies based on the request method and endpoint type:
135+
136+
### GET requests to Plural (Many) Endpoints
137+
138+
For `GET` requests to [plural endpoints](./ENDPOINT_TYPES.md#plural-many-endpoints), sorting allows to you sort the
139+
objects returned in the `data` section of the API response by a specific field and a specific ordering. This does not
140+
affect the order of the objects stored in the pfSense configuration.
141+
142+
### POST and PATCH requests to Singular Endpoints
143+
144+
For `POST` and `PATCH` requests to [singular endpoints](./ENDPOINT_TYPES.md#singular-endpoints), sorting allows you to
145+
sort the relevant objects in the pfSense configuration after creating or updating an object. This is useful when you
146+
need to control the order of objects in the configuration, especially for objects where the order of objects directly
147+
affects the behavior (like ACLs). Some example use cases for sorting the configuration include:
148+
149+
- Reordering firewall rules based on a custom description.
150+
- Reordering NAT rules based on interface.
151+
152+
!!! Warning
153+
- Use caution when setting the sort order of objects which may be sensitive to order such as firewall rules. Placing
154+
the object in the wrong location may have unintended consequences such as blocking all traffic or allowing all traffic.
155+
- Some endpoints may already have default sorting attributes. Setting the `sort_by` parameter will override these
156+
defaults which may result in unexpected behavior.
157+
122158
## Pagination
123159

124160
Pagination can be used to limit the number of items returned in a single request. Pagination is controlled by two query

docs/WORKING_WITH_HATEOAS.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ easily navigate the API and discover available actions and resources related to
1515
!!! Important
1616
Enabling HATEOAS can greatly increase the size of API responses as additional links are included in the response data;
1717
which may also impact performance on large datasets. It is strongly recommended to only enable HATEOAS when needed and
18-
to use [pagination](./QUERIES_AND_FILTERS.md#pagination) to limit the amount of data returned in a single request.
18+
to use [pagination](./QUERIES_FILTERS_AND_SORTING#pagination) to limit the amount of data returned in a single request.
1919

2020
### Link types
2121

@@ -28,11 +28,11 @@ API response.
2828

2929
#### next
3030

31-
Provides a link to the next set of data when [pagination](./QUERIES_AND_FILTERS.md#pagination) is used.
31+
Provides a link to the next set of data when [pagination](./QUERIES_FILTERS_AND_SORTING#pagination) is used.
3232

3333
#### prev
3434

35-
Provides a link to the previous set of data when [pagination](./QUERIES_AND_FILTERS.md#pagination) is used.
35+
Provides a link to the previous set of data when [pagination](./QUERIES_FILTERS_AND_SORTING#pagination) is used.
3636

3737
#### self
3838

mkdocs.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ nav:
77
- Endpoint Types: ENDPOINT_TYPES.md
88
- Authentication & Authorization: AUTHENTICATION_AND_AUTHORIZATION.md
99
- Content & Accept Types: CONTENT_AND_ACCEPT_TYPES.md
10-
- Queries & Filters: QUERIES_AND_FILTERS.md
10+
- Queries & Filters: QUERIES_FILTERS_AND_SORTING.md
1111
- Working with Object IDs: WORKING_WITH_OBJECT_IDS.md
1212
- Working with HATEOAS: WORKING_WITH_HATEOAS.md
1313
- Common Control Parameters: COMMON_CONTROL_PARAMETERS.md

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

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -317,12 +317,12 @@ class Endpoint {
317317

318318
/**
319319
* @var string|null $sort_by
320-
* Sets the default value for the `sort_by` field in the request data. This value is used to control the sorting of
320+
* Sets the default value(s) for the `sort_by` field in the request data. This value is used to control the sorting of
321321
* the Model objects returned by this Endpoint. This value can be overridden by the client in the request data. Use
322322
* caution when assigning this value as it may force objects to be sorted in this order when they are written to the
323323
* configuration file.
324324
*/
325-
public ?string $sort_by = null;
325+
public string|array|null $sort_by = null;
326326

327327
/**
328328
* @var string $sort_order
@@ -794,20 +794,28 @@ class Endpoint {
794794
{
795795
# Only validate this field if the client specifically requested it in the request data
796796
if (isset($this->request_data['sort_by'])) {
797-
# Ensure value is a string
798-
if (!is_string($this->request_data['sort_by'])) {
799-
throw new ValidationError(
800-
message: 'Field `sort_by` must be of type `string`.',
801-
response_id: 'ENDPOINT_SORT_BY_FIELD_INVALID_TYPE',
802-
);
803-
}
797+
# Ensure value is an array
798+
$this->request_data['sort_by'] = is_array($this->request_data['sort_by'])
799+
? $this->request_data['sort_by']
800+
: [$this->request_data['sort_by']];
801+
802+
# Check each field in the array
803+
foreach ($this->request_data['sort_by'] as $sort_by) {
804+
# Ensure value is a string
805+
if (!is_string($sort_by)) {
806+
throw new ValidationError(
807+
message: 'Field `sort_by` must be of type `string`.',
808+
response_id: 'ENDPOINT_SORT_BY_FIELD_INVALID_TYPE',
809+
);
810+
}
804811

805-
# Ensure the field is a valid field in the Model
806-
if (!in_array($this->request_data['sort_by'], $this->model->get_fields())) {
807-
throw new ValidationError(
808-
message: 'Field `sort_by` must be a valid field in the Model.',
809-
response_id: 'ENDPOINT_SORT_BY_FIELD_NON_EXISTENT_FIELD',
810-
);
812+
# Ensure the field is a valid field in the Model
813+
if (!in_array($sort_by, $this->model->get_fields())) {
814+
throw new ValidationError(
815+
message: 'Field `sort_by` must be a valid field in the Model.',
816+
response_id: 'ENDPOINT_SORT_BY_FIELD_NON_EXISTENT_FIELD',
817+
);
818+
}
811819
}
812820

813821
# Update the sort_by property to use the client's requested value and remove it from the request data

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1856,7 +1856,7 @@ class Model {
18561856
int $limit = 0,
18571857
int $offset = 0,
18581858
bool $reverse = false,
1859-
?string $sort_by = null,
1859+
?array $sort_by = null,
18601860
int $sort_order = SORT_ASC,
18611861
...$vl_query_params,
18621862
): ModelSet {
@@ -1872,7 +1872,7 @@ class Model {
18721872
$modelset = self::read_all(parent_id: $parent_id)->query(query_params: $query_params, excluded: $excluded);
18731873

18741874
# Sort the set if a sort field was provided
1875-
$modelset = ($sort_by) ? $modelset->sort(field: $sort_by, order: $sort_order, retain_ids: true) : $modelset;
1875+
$modelset = ($sort_by) ? $modelset->sort(fields: $sort_by, order: $sort_order, retain_ids: true) : $modelset;
18761876

18771877
# Apply pagination to limit the number of objects returned and/or reverse the order if requested
18781878
$modelset->model_objects = self::paginate($modelset->model_objects, $limit, $offset);

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

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -81,23 +81,33 @@ class ModelSet {
8181

8282
/**
8383
* Sorts the Model objects in this ModelSet by a specific field and order.
84-
* @param string $field The field to sort the Model objects by.
84+
* @param string|array $fields The field(s) to sort the Model objects by.
8585
* @param int $order The order to sort the Model objects by. This must be a PHP sort order constant.
8686
* @param bool $retain_ids Retain the original Model object IDs when sorting.
8787
* @return ModelSet A new ModelSet object with the Model objects sorted by the specified field and order.
8888
*/
89-
public function sort(string $field, int $order = SORT_ASC, bool $retain_ids = false): ModelSet {
89+
public function sort(string|array $fields, int $order = SORT_ASC, bool $retain_ids = false): ModelSet {
9090
# Variables
91-
$sort_values = [];
9291
$model_objects = $this->model_objects;
92+
$fields = is_array($fields) ? $fields : [$fields];
93+
$sort_criteria = [];
9394

94-
# Loop through each model object and store the sort value
95-
foreach ($model_objects as $model_object) {
96-
$sort_values[] = $model_object->$field->value;
95+
# Loop through each Model object and add the field values to the sort criteria
96+
foreach ($this->model_objects as $model_object) {
97+
# Loop variables
98+
$sort_values = [];
99+
100+
# Loop through each field to sort by and extract it's value
101+
foreach ($fields as $field) {
102+
$sort_values[] = $field === 'id' ? $model_object->id : $model_object->$field->value;
103+
}
104+
105+
# Add the sort values to the sort criteria
106+
$sort_criteria[] = $sort_values;
97107
}
98108

99-
# Sort the sort values
100-
array_multisort($sort_values, $order, $model_objects);
109+
# Sort using array_multisort
110+
array_multisort($sort_criteria, $order, $model_objects);
101111

102112
# Re-assign the model object IDs if they should not be retained
103113
if (!$retain_ids) {

pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Fields/NestedModelField.inc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ class NestedModelField extends Field {
209209

210210
# Sort the nested Model objects by the specified field
211211
$this->modelset = $this->modelset->sort(
212-
field: $this->model->sort_by_field,
212+
fields: $this->model->sort_by_field,
213213
order: $this->model->sort_option,
214214
);
215215

0 commit comments

Comments
 (0)