2021-06-26 17:23:15 +02:00
|
|
|
<?php
|
|
|
|
|
|
|
|
namespace BookStack\Api;
|
2019-12-28 15:58:07 +01:00
|
|
|
|
|
|
|
use Illuminate\Database\Eloquent\Builder;
|
|
|
|
use Illuminate\Database\Eloquent\Collection;
|
2022-10-24 13:12:48 +02:00
|
|
|
use Illuminate\Database\Eloquent\Model;
|
2022-02-03 13:33:26 +01:00
|
|
|
use Illuminate\Http\JsonResponse;
|
2020-01-01 17:33:47 +01:00
|
|
|
use Illuminate\Http\Request;
|
2019-12-28 15:58:07 +01:00
|
|
|
|
|
|
|
class ListingResponseBuilder
|
|
|
|
{
|
2022-10-24 13:12:48 +02:00
|
|
|
protected Builder $query;
|
|
|
|
protected Request $request;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var string[]
|
|
|
|
*/
|
|
|
|
protected array $fields;
|
2022-02-03 13:33:26 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @var array<callable>
|
|
|
|
*/
|
2022-10-24 13:12:48 +02:00
|
|
|
protected array $resultModifiers = [];
|
2019-12-28 15:58:07 +01:00
|
|
|
|
2022-10-24 13:12:48 +02:00
|
|
|
/**
|
|
|
|
* @var array<string, string>
|
|
|
|
*/
|
|
|
|
protected array $filterOperators = [
|
2020-01-01 17:33:47 +01:00
|
|
|
'eq' => '=',
|
|
|
|
'ne' => '!=',
|
|
|
|
'gt' => '>',
|
|
|
|
'lt' => '<',
|
|
|
|
'gte' => '>=',
|
|
|
|
'lte' => '<=',
|
2021-06-26 17:23:15 +02:00
|
|
|
'like' => 'like',
|
2020-01-01 17:33:47 +01:00
|
|
|
];
|
|
|
|
|
2019-12-28 15:58:07 +01:00
|
|
|
/**
|
|
|
|
* ListingResponseBuilder constructor.
|
2022-02-03 13:33:26 +01:00
|
|
|
* The given fields will be forced visible within the model results.
|
2019-12-28 15:58:07 +01:00
|
|
|
*/
|
2022-02-03 13:33:26 +01:00
|
|
|
public function __construct(Builder $query, Request $request, array $fields)
|
2019-12-28 15:58:07 +01:00
|
|
|
{
|
|
|
|
$this->query = $query;
|
2020-01-01 17:33:47 +01:00
|
|
|
$this->request = $request;
|
2019-12-28 15:58:07 +01:00
|
|
|
$this->fields = $fields;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the response from this builder.
|
|
|
|
*/
|
2022-02-03 13:33:26 +01:00
|
|
|
public function toResponse(): JsonResponse
|
2019-12-28 15:58:07 +01:00
|
|
|
{
|
2020-04-25 23:15:59 +02:00
|
|
|
$filteredQuery = $this->filterQuery($this->query);
|
2020-04-25 22:37:52 +02:00
|
|
|
|
2020-04-25 23:15:59 +02:00
|
|
|
$total = $filteredQuery->count();
|
2022-02-08 16:29:58 +01:00
|
|
|
$data = $this->fetchData($filteredQuery)->each(function ($model) {
|
2022-02-03 13:33:26 +01:00
|
|
|
foreach ($this->resultModifiers as $modifier) {
|
|
|
|
$modifier($model);
|
|
|
|
}
|
|
|
|
});
|
2019-12-28 15:58:07 +01:00
|
|
|
|
|
|
|
return response()->json([
|
2021-06-26 17:23:15 +02:00
|
|
|
'data' => $data,
|
2019-12-28 15:58:07 +01:00
|
|
|
'total' => $total,
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2022-02-08 16:29:58 +01:00
|
|
|
* Add a callback to modify each element of the results.
|
|
|
|
*
|
2022-10-24 13:12:48 +02:00
|
|
|
* @param (callable(Model): void) $modifier
|
2022-02-03 13:33:26 +01:00
|
|
|
*/
|
2022-10-24 13:12:48 +02:00
|
|
|
public function modifyResults(callable $modifier): void
|
2022-02-03 13:33:26 +01:00
|
|
|
{
|
|
|
|
$this->resultModifiers[] = $modifier;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Fetch the data to return within the response.
|
2019-12-28 15:58:07 +01:00
|
|
|
*/
|
2020-04-25 23:15:59 +02:00
|
|
|
protected function fetchData(Builder $query): Collection
|
2019-12-28 15:58:07 +01:00
|
|
|
{
|
2020-04-25 23:15:59 +02:00
|
|
|
$query = $this->countAndOffsetQuery($query);
|
|
|
|
$query = $this->sortQuery($query);
|
2021-06-26 17:23:15 +02:00
|
|
|
|
2020-04-25 23:15:59 +02:00
|
|
|
return $query->get($this->fields);
|
2019-12-28 15:58:07 +01:00
|
|
|
}
|
|
|
|
|
2020-01-01 17:33:47 +01:00
|
|
|
/**
|
|
|
|
* Apply any filtering operations found in the request.
|
|
|
|
*/
|
2020-04-25 23:15:59 +02:00
|
|
|
protected function filterQuery(Builder $query): Builder
|
2020-01-01 17:33:47 +01:00
|
|
|
{
|
2020-04-25 23:15:59 +02:00
|
|
|
$query = clone $query;
|
2020-01-01 17:33:47 +01:00
|
|
|
$requestFilters = $this->request->get('filter', []);
|
|
|
|
if (!is_array($requestFilters)) {
|
2020-04-25 23:15:59 +02:00
|
|
|
return $query;
|
2020-01-01 17:33:47 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
$queryFilters = collect($requestFilters)->map(function ($value, $key) {
|
|
|
|
return $this->requestFilterToQueryFilter($key, $value);
|
|
|
|
})->filter(function ($value) {
|
|
|
|
return !is_null($value);
|
|
|
|
})->values()->toArray();
|
|
|
|
|
2020-04-25 23:15:59 +02:00
|
|
|
return $query->where($queryFilters);
|
2020-01-01 17:33:47 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Convert a request filter query key/value pair into a [field, op, value] where condition.
|
|
|
|
*/
|
|
|
|
protected function requestFilterToQueryFilter($fieldKey, $value): ?array
|
|
|
|
{
|
|
|
|
$splitKey = explode(':', $fieldKey);
|
|
|
|
$field = $splitKey[0];
|
|
|
|
$filterOperator = $splitKey[1] ?? 'eq';
|
|
|
|
|
|
|
|
if (!in_array($field, $this->fields)) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!in_array($filterOperator, array_keys($this->filterOperators))) {
|
|
|
|
$filterOperator = 'eq';
|
|
|
|
}
|
|
|
|
|
|
|
|
$queryOperator = $this->filterOperators[$filterOperator];
|
2021-06-26 17:23:15 +02:00
|
|
|
|
2020-01-01 17:33:47 +01:00
|
|
|
return [$field, $queryOperator, $value];
|
|
|
|
}
|
|
|
|
|
2019-12-28 15:58:07 +01:00
|
|
|
/**
|
|
|
|
* Apply sorting operations to the query from given parameters
|
|
|
|
* otherwise falling back to the first given field, ascending.
|
|
|
|
*/
|
2020-04-25 23:15:59 +02:00
|
|
|
protected function sortQuery(Builder $query): Builder
|
2019-12-28 15:58:07 +01:00
|
|
|
{
|
2020-04-25 23:15:59 +02:00
|
|
|
$query = clone $query;
|
2019-12-28 15:58:07 +01:00
|
|
|
$defaultSortName = $this->fields[0];
|
|
|
|
$direction = 'asc';
|
|
|
|
|
2020-01-01 17:33:47 +01:00
|
|
|
$sort = $this->request->get('sort', '');
|
2019-12-28 15:58:07 +01:00
|
|
|
if (strpos($sort, '-') === 0) {
|
|
|
|
$direction = 'desc';
|
|
|
|
}
|
|
|
|
|
|
|
|
$sortName = ltrim($sort, '+- ');
|
|
|
|
if (!in_array($sortName, $this->fields)) {
|
|
|
|
$sortName = $defaultSortName;
|
|
|
|
}
|
|
|
|
|
2020-04-25 23:15:59 +02:00
|
|
|
return $query->orderBy($sortName, $direction);
|
2019-12-28 15:58:07 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Apply count and offset for paging, based on params from the request while falling
|
|
|
|
* back to system defined default, taking the max limit into account.
|
|
|
|
*/
|
2020-04-25 23:15:59 +02:00
|
|
|
protected function countAndOffsetQuery(Builder $query): Builder
|
2019-12-28 15:58:07 +01:00
|
|
|
{
|
2020-04-25 23:15:59 +02:00
|
|
|
$query = clone $query;
|
2020-01-01 17:33:47 +01:00
|
|
|
$offset = max(0, $this->request->get('offset', 0));
|
2019-12-28 15:58:07 +01:00
|
|
|
$maxCount = config('api.max_item_count');
|
2020-01-01 17:33:47 +01:00
|
|
|
$count = $this->request->get('count', config('api.default_item_count'));
|
2019-12-28 15:58:07 +01:00
|
|
|
$count = max(min($maxCount, $count), 1);
|
|
|
|
|
2020-04-25 23:15:59 +02:00
|
|
|
return $query->skip($offset)->take($count);
|
2019-12-28 15:58:07 +01:00
|
|
|
}
|
|
|
|
}
|