3 namespace BookStack\Api;
5 use Illuminate\Database\Eloquent\Builder;
6 use Illuminate\Database\Eloquent\Collection;
7 use Illuminate\Database\Eloquent\Model;
8 use Illuminate\Http\JsonResponse;
9 use Illuminate\Http\Request;
11 class ListingResponseBuilder
13 protected Builder $query;
14 protected Request $request;
19 protected array $fields;
22 * @var array<callable>
24 protected array $resultModifiers = [];
27 * @var array<string, string>
29 protected array $filterOperators = [
40 * ListingResponseBuilder constructor.
41 * The given fields will be forced visible within the model results.
43 public function __construct(Builder $query, Request $request, array $fields)
45 $this->query = $query;
46 $this->request = $request;
47 $this->fields = $fields;
51 * Get the response from this builder.
53 public function toResponse(): JsonResponse
55 $filteredQuery = $this->filterQuery($this->query);
57 $total = $filteredQuery->count();
58 $data = $this->fetchData($filteredQuery)->each(function ($model) {
59 foreach ($this->resultModifiers as $modifier) {
64 return response()->json([
71 * Add a callback to modify each element of the results.
73 * @param (callable(Model): void) $modifier
75 public function modifyResults(callable $modifier): void
77 $this->resultModifiers[] = $modifier;
81 * Fetch the data to return within the response.
83 protected function fetchData(Builder $query): Collection
85 $query = $this->countAndOffsetQuery($query);
86 $query = $this->sortQuery($query);
88 return $query->get($this->fields);
92 * Apply any filtering operations found in the request.
94 protected function filterQuery(Builder $query): Builder
96 $query = clone $query;
97 $requestFilters = $this->request->get('filter', []);
98 if (!is_array($requestFilters)) {
102 $queryFilters = collect($requestFilters)->map(function ($value, $key) {
103 return $this->requestFilterToQueryFilter($key, $value);
104 })->filter(function ($value) {
105 return !is_null($value);
106 })->values()->toArray();
108 return $query->where($queryFilters);
112 * Convert a request filter query key/value pair into a [field, op, value] where condition.
114 protected function requestFilterToQueryFilter($fieldKey, $value): ?array
116 $splitKey = explode(':', $fieldKey);
117 $field = $splitKey[0];
118 $filterOperator = $splitKey[1] ?? 'eq';
120 if (!in_array($field, $this->fields)) {
124 if (!in_array($filterOperator, array_keys($this->filterOperators))) {
125 $filterOperator = 'eq';
128 $queryOperator = $this->filterOperators[$filterOperator];
130 return [$field, $queryOperator, $value];
134 * Apply sorting operations to the query from given parameters
135 * otherwise falling back to the first given field, ascending.
137 protected function sortQuery(Builder $query): Builder
139 $query = clone $query;
140 $defaultSortName = $this->fields[0];
143 $sort = $this->request->get('sort', '');
144 if (strpos($sort, '-') === 0) {
148 $sortName = ltrim($sort, '+- ');
149 if (!in_array($sortName, $this->fields)) {
150 $sortName = $defaultSortName;
153 return $query->orderBy($sortName, $direction);
157 * Apply count and offset for paging, based on params from the request while falling
158 * back to system defined default, taking the max limit into account.
160 protected function countAndOffsetQuery(Builder $query): Builder
162 $query = clone $query;
163 $offset = max(0, $this->request->get('offset', 0));
164 $maxCount = config('api.max_item_count');
165 $count = $this->request->get('count', config('api.default_item_count'));
166 $count = max(min($maxCount, $count), 1);
168 return $query->skip($offset)->take($count);