]> BookStack Code Mirror - bookstack/blob - app/Api/ListingResponseBuilder.php
Updated attachment links to have dropdown for open type
[bookstack] / app / Api / ListingResponseBuilder.php
1 <?php
2
3 namespace BookStack\Api;
4
5 use BookStack\Model;
6 use Illuminate\Database\Eloquent\Builder;
7 use Illuminate\Database\Eloquent\Collection;
8 use Illuminate\Http\JsonResponse;
9 use Illuminate\Http\Request;
10
11 class ListingResponseBuilder
12 {
13     protected $query;
14     protected $request;
15     protected $fields;
16
17     /**
18      * @var array<callable>
19      */
20     protected $resultModifiers = [];
21
22     protected $filterOperators = [
23         'eq'   => '=',
24         'ne'   => '!=',
25         'gt'   => '>',
26         'lt'   => '<',
27         'gte'  => '>=',
28         'lte'  => '<=',
29         'like' => 'like',
30     ];
31
32     /**
33      * ListingResponseBuilder constructor.
34      * The given fields will be forced visible within the model results.
35      */
36     public function __construct(Builder $query, Request $request, array $fields)
37     {
38         $this->query = $query;
39         $this->request = $request;
40         $this->fields = $fields;
41     }
42
43     /**
44      * Get the response from this builder.
45      */
46     public function toResponse(): JsonResponse
47     {
48         $filteredQuery = $this->filterQuery($this->query);
49
50         $total = $filteredQuery->count();
51         $data = $this->fetchData($filteredQuery)->each(function ($model) {
52             foreach ($this->resultModifiers as $modifier) {
53                 $modifier($model);
54             }
55         });
56
57         return response()->json([
58             'data'  => $data,
59             'total' => $total,
60         ]);
61     }
62
63     /**
64      * Add a callback to modify each element of the results.
65      *
66      * @param (callable(Model)) $modifier
67      */
68     public function modifyResults($modifier): void
69     {
70         $this->resultModifiers[] = $modifier;
71     }
72
73     /**
74      * Fetch the data to return within the response.
75      */
76     protected function fetchData(Builder $query): Collection
77     {
78         $query = $this->countAndOffsetQuery($query);
79         $query = $this->sortQuery($query);
80
81         return $query->get($this->fields);
82     }
83
84     /**
85      * Apply any filtering operations found in the request.
86      */
87     protected function filterQuery(Builder $query): Builder
88     {
89         $query = clone $query;
90         $requestFilters = $this->request->get('filter', []);
91         if (!is_array($requestFilters)) {
92             return $query;
93         }
94
95         $queryFilters = collect($requestFilters)->map(function ($value, $key) {
96             return $this->requestFilterToQueryFilter($key, $value);
97         })->filter(function ($value) {
98             return !is_null($value);
99         })->values()->toArray();
100
101         return $query->where($queryFilters);
102     }
103
104     /**
105      * Convert a request filter query key/value pair into a [field, op, value] where condition.
106      */
107     protected function requestFilterToQueryFilter($fieldKey, $value): ?array
108     {
109         $splitKey = explode(':', $fieldKey);
110         $field = $splitKey[0];
111         $filterOperator = $splitKey[1] ?? 'eq';
112
113         if (!in_array($field, $this->fields)) {
114             return null;
115         }
116
117         if (!in_array($filterOperator, array_keys($this->filterOperators))) {
118             $filterOperator = 'eq';
119         }
120
121         $queryOperator = $this->filterOperators[$filterOperator];
122
123         return [$field, $queryOperator, $value];
124     }
125
126     /**
127      * Apply sorting operations to the query from given parameters
128      * otherwise falling back to the first given field, ascending.
129      */
130     protected function sortQuery(Builder $query): Builder
131     {
132         $query = clone $query;
133         $defaultSortName = $this->fields[0];
134         $direction = 'asc';
135
136         $sort = $this->request->get('sort', '');
137         if (strpos($sort, '-') === 0) {
138             $direction = 'desc';
139         }
140
141         $sortName = ltrim($sort, '+- ');
142         if (!in_array($sortName, $this->fields)) {
143             $sortName = $defaultSortName;
144         }
145
146         return $query->orderBy($sortName, $direction);
147     }
148
149     /**
150      * Apply count and offset for paging, based on params from the request while falling
151      * back to system defined default, taking the max limit into account.
152      */
153     protected function countAndOffsetQuery(Builder $query): Builder
154     {
155         $query = clone $query;
156         $offset = max(0, $this->request->get('offset', 0));
157         $maxCount = config('api.max_item_count');
158         $count = $this->request->get('count', config('api.default_item_count'));
159         $count = max(min($maxCount, $count), 1);
160
161         return $query->skip($offset)->take($count);
162     }
163 }