Skip to content

Commit 2dc9a85

Browse files
authored
Merge pull request #9 from Lomkit/feature/aggregates
✨ aggregates
2 parents c6e209f + b22f0f1 commit 2dc9a85

File tree

7 files changed

+1057
-6
lines changed

7 files changed

+1057
-6
lines changed

README.md

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,16 @@ Here is a quick look at what you can do using API search method:
6363
]
6464
}
6565
],
66+
"aggregates": [
67+
{
68+
"relation": "stars",
69+
"type": "max",
70+
"field": "rate",
71+
"filters": [
72+
{"field": 'approved', "value": true}
73+
]
74+
},
75+
],
6676
"page": 2,
6777
"limit": 10
6878
}
@@ -74,12 +84,13 @@ TODO
7484

7585
## Contributing
7686

87+
7788
TODO
7889

7990
## Roadmap for the end of bêta (Estimated delivery October 2023)
8091

8192
- Custom directives (Filters / sorting)
8293
- Actions / Metrics
8394
- Automatic Gates (with config customisation)
84-
- Aggregating
85-
- Automatic documentation with extension possible
95+
- Automatic documentation with extension possible
96+
- Add the possibility to disable authorization

src/Http/Requests/SearchRequest.php

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
use Lomkit\Rest\Contracts\QueryBuilder;
88
use Lomkit\Rest\Http\Controllers\Controller;
99
use Lomkit\Rest\Http\Resource;
10+
use Lomkit\Rest\Rules\AggregateField;
11+
use Lomkit\Rest\Rules\AggregateFilterable;
1012
use Lomkit\Rest\Rules\Includable;
1113

1214
class SearchRequest extends RestRequest
@@ -31,6 +33,8 @@ public function searchRules(Resource $resource, $prefix = '')
3133
$this->sortsRules($resource, $prefix.'sorts'),
3234
[$prefix.'selects' => ['sometimes', 'array']],
3335
$this->selectsRules($resource, $prefix.'selects'),
36+
[$prefix.'aggregates' => ['sometimes', 'array']],
37+
$this->aggregatesRules($resource, $prefix.'aggregates'),
3438
[
3539
'limit' => ['sometimes', 'integer', Rule::in($resource->exposedLimits($this))],
3640
'page' => ['sometimes', 'integer']
@@ -41,7 +45,7 @@ public function searchRules(Resource $resource, $prefix = '')
4145
}
4246

4347
// @TODO: For now it's prohibited to have more than one nested depth, is this needed ?
44-
protected function filtersRules(Resource $resource, string $prefix, $isMaxDepth = false) {
48+
public function filtersRules(Resource $resource, string $prefix, $isMaxDepth = false) {
4549
$rules = array_merge(
4650
[
4751
$prefix.'.*.field' => [
@@ -138,4 +142,32 @@ protected function includesRules(Resource $resource) {
138142
]
139143
];
140144
}
145+
146+
protected function aggregatesRules(Resource $resource, string $prefix) {
147+
return [
148+
$prefix.'.*.relation' => [
149+
'required',
150+
Rule::in(
151+
array_keys(
152+
$resource->nestedRelations(app()->make(RestRequest::class))
153+
)
154+
)
155+
],
156+
$prefix.'.*.type' => [
157+
Rule::in(['count', 'min', 'max', 'avg', 'sum', 'exists'])
158+
],
159+
$prefix.'.*.field' => [
160+
'required_if:'.$prefix.'.*.type,min,max,avg,sum',
161+
'prohibited_if:'.$prefix.'.*.type,count,exists'
162+
],
163+
$prefix.'.*' => [
164+
AggregateField::make()
165+
->resource($resource)
166+
],
167+
$prefix.'.*.filters' => [
168+
AggregateFilterable::make()
169+
->resource($resource)
170+
]
171+
];
172+
}
141173
}

src/Http/Response.php

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,17 @@ public function modelToResponse(Model $model, Resource $resource, array $request
3333
// toArray to take advantage of Laravel's logic
3434
collect($model->attributesToArray())
3535
->only(
36-
isset($requestArray['selects']) ?
37-
collect($requestArray['selects'])->pluck('field') :
38-
$resource->exposedFields(app()->make(RestRequest::class))
36+
array_merge(
37+
isset($requestArray['selects']) ?
38+
collect($requestArray['selects'])->pluck('field')->toArray() :
39+
$resource->exposedFields(app()->make(RestRequest::class)),
40+
// Here we add the aggregates
41+
collect($requestArray['aggregates'] ?? [])
42+
->map(function ($aggregate) {
43+
return Str::snake($aggregate['relation']).'_'.$aggregate['type'].(isset($aggregate['field']) ? '_'.$aggregate['field'] : '');
44+
})
45+
->toArray()
46+
)
3947
)
4048
->toArray(),
4149
collect($model->getRelations())

src/Query/Traits/PerformSearch.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace Lomkit\Rest\Query\Traits;
44

5+
use Illuminate\Database\Eloquent\Builder;
56
use Illuminate\Database\Eloquent\Relations\Relation;
67
use Illuminate\Support\Str;
78
use Lomkit\Rest\Http\Requests\RestRequest;
@@ -41,6 +42,10 @@ public function search(array $parameters = []) {
4142
$this->applyIncludes($parameters['includes']);
4243
});
4344

45+
$this->when(isset($parameters['aggregates']), function () use ($parameters) {
46+
$this->applyAggregates($parameters['aggregates']);
47+
});
48+
4449
// @TODO: is this a problem also with HasMany ??
4550
// @TODO: this will be the problem for every relation, need to fix this
4651
// In case of BelongsToMany we cap the limit
@@ -114,4 +119,20 @@ public function applyIncludes($includes) {
114119
$this->include($include);
115120
}
116121
}
122+
123+
public function aggregate($aggregate) {
124+
return $this->queryBuilder->withAggregate([$aggregate['relation'] => function(Builder $query) use ($aggregate) {
125+
$resource = $this->resource->relationResource($aggregate['relation']);
126+
127+
$queryBuilder = $this->newQueryBuilder(['resource' => $resource, 'query' => $query]);
128+
129+
return $queryBuilder->search(['filters' => $aggregate['filters'] ?? []]);
130+
}], $aggregate['field'] ?? '*', $aggregate['type']);
131+
}
132+
133+
public function applyAggregates($aggregates) {
134+
foreach ($aggregates as $aggregate) {
135+
$this->aggregate($aggregate);
136+
}
137+
}
117138
}

src/Rules/AggregateField.php

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
<?php
2+
3+
namespace Lomkit\Rest\Rules;
4+
5+
use Closure;
6+
use Illuminate\Contracts\Validation\DataAwareRule;
7+
use Illuminate\Contracts\Validation\Rule;
8+
use Illuminate\Contracts\Validation\ValidatorAwareRule;
9+
use Illuminate\Support\Arr;
10+
use Illuminate\Support\Facades\Validator;
11+
use Lomkit\Rest\Concerns\Makeable;
12+
use Lomkit\Rest\Http\Requests\RestRequest;
13+
use Lomkit\Rest\Http\Requests\SearchRequest;
14+
use Lomkit\Rest\Http\Resource;
15+
16+
class AggregateField implements Rule, DataAwareRule, ValidatorAwareRule
17+
{
18+
19+
use Makeable;
20+
21+
/**
22+
* The data under validation.
23+
*
24+
* @var array
25+
*/
26+
protected $data;
27+
28+
/**
29+
* The error message after validation, if any.
30+
*
31+
* @var array
32+
*/
33+
protected $messages = [];
34+
35+
/**
36+
* The resource related to.
37+
*
38+
* @var Resource
39+
*/
40+
protected $resource = null;
41+
42+
/**
43+
* The validator performing the validation.
44+
*
45+
* @var \Illuminate\Validation\Validator
46+
*/
47+
protected $validator;
48+
49+
/**
50+
*
51+
*
52+
* @param $resource
53+
* @return $this
54+
*/
55+
public function resource($resource)
56+
{
57+
$this->resource = $resource;
58+
59+
return $this;
60+
}
61+
62+
public function passes($attribute, $value)
63+
{
64+
$validator = Validator::make(
65+
$this->data,
66+
$this->buildValidationRules($attribute, $value)
67+
);
68+
69+
if ($validator->fails()) {
70+
return $this->fail($validator->messages()->all());
71+
}
72+
73+
return true;
74+
}
75+
76+
/**
77+
* Build the array of underlying validation rules based on the current state.
78+
*
79+
* @return array
80+
*/
81+
protected function buildValidationRules($attribute, $value)
82+
{
83+
$relationResource = $this->resource->relationResource($value['relation']);
84+
85+
if (is_null($relationResource)) {
86+
return [];
87+
}
88+
89+
return [
90+
$attribute.'.field' => \Illuminate\Validation\Rule::in(
91+
$relationResource->exposedFields(app()->make(RestRequest::class))
92+
)
93+
];
94+
}
95+
96+
/**
97+
* Get the validation error message.
98+
*
99+
* @return array
100+
*/
101+
public function message()
102+
{
103+
return $this->messages;
104+
}
105+
106+
/**
107+
* Adds the given failures, and return false.
108+
*
109+
* @param array|string $messages
110+
* @return bool
111+
*/
112+
protected function fail($messages)
113+
{
114+
$messages = collect(Arr::wrap($messages))->map(function ($message) {
115+
return $this->validator->getTranslator()->get($message);
116+
})->all();
117+
118+
$this->messages = array_merge($this->messages, $messages);
119+
120+
return false;
121+
}
122+
123+
/**
124+
* Set the current validator.
125+
*
126+
* @param \Illuminate\Contracts\Validation\Validator $validator
127+
* @return $this
128+
*/
129+
public function setValidator($validator)
130+
{
131+
$this->validator = $validator;
132+
133+
return $this;
134+
}
135+
136+
/**
137+
* Set the current data under validation.
138+
*
139+
* @param array $data
140+
* @return $this
141+
*/
142+
public function setData($data)
143+
{
144+
$this->data = $data;
145+
146+
return $this;
147+
}
148+
}

0 commit comments

Comments
 (0)