Skip to content

Commit 83ca872

Browse files
Aggregate alias support (#164)
* Aggregate alias support * Fix codeRabbit * Fix StyleCI * 🐛 small fixes --------- Co-authored-by: Gautier DELEGLISE <[email protected]>
1 parent d6141f9 commit 83ca872

File tree

5 files changed

+284
-4
lines changed

5 files changed

+284
-4
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,4 +90,4 @@ Here is a quick look at what you can do using API search method:
9090

9191
- Metrics support
9292
- Refactor the response class
93-
- Alias for includes / aggregates
93+
- Alias for includes

src/Http/Response.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ protected function buildGatesForModel(Model $model, Resource $resource, array $g
5353
*
5454
* @return array The structured array representation of the model, including attributes and recursively processed relations.
5555
*/
56-
public function modelToResponse(Model $model, Resource $resource, array $requestArray, Relation $relation = null)
56+
public function modelToResponse(Model $model, Resource $resource, array $requestArray, ?Relation $relation = null)
5757
{
5858
$currentRequestArray = $relation === null ? $requestArray : collect($requestArray['includes'] ?? [])
5959
->first(function ($include) use ($relation) {
@@ -71,7 +71,7 @@ public function modelToResponse(Model $model, Resource $resource, array $request
7171
// Here we add the aggregates
7272
collect($currentRequestArray['aggregates'] ?? [])
7373
->map(function ($aggregate) {
74-
return Str::snake($aggregate['relation']).'_'.$aggregate['type'].(isset($aggregate['field']) ? '_'.$aggregate['field'] : '');
74+
return $aggregate['alias'] ?? Str::snake($aggregate['relation']).'_'.$aggregate['type'].(isset($aggregate['field']) ? '_'.$aggregate['field'] : '');
7575
})
7676
->toArray()
7777
)

src/Query/Traits/PerformSearch.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,13 @@ public function applyIncludes($includes)
237237
*/
238238
public function aggregate($aggregate)
239239
{
240-
return $this->queryBuilder->withAggregate([$aggregate['relation'] => function (Builder $query) use ($aggregate) {
240+
$relation = $aggregate['relation'];
241+
242+
if (isset($aggregate['alias'])) {
243+
$relation .= ' as '.$aggregate['alias'];
244+
}
245+
246+
return $this->queryBuilder->withAggregate([$relation => function (Builder $query) use ($aggregate) {
241247
$resource = $this->resource->relation($aggregate['relation'])?->resource();
242248

243249
$queryBuilder = $this->newQueryBuilder(['resource' => $resource, 'query' => $query]);

src/Rules/SearchRules.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,10 @@ protected function aggregatesRules(\Lomkit\Rest\Http\Resource $resource, string
341341
'required_if:'.$prefix.'.*.type,min,max,avg,sum',
342342
'prohibited_if:'.$prefix.'.*.type,count,exists',
343343
],
344+
$prefix.'.*.alias' => [
345+
'nullable',
346+
'string',
347+
],
344348
$prefix.'.*' => [
345349
AggregateField::make()
346350
->resource($resource),

tests/Feature/Controllers/SearchAggregatesOperationsTest.php

Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,52 @@ public function test_getting_a_list_of_resources_aggregating_by_min_number(): vo
192192
);
193193
}
194194

195+
public function test_getting_a_list_of_resources_aggregating_by_min_number_with_alias(): void
196+
{
197+
$matchingModel = ModelFactory::new()
198+
->has(
199+
BelongsToManyRelationFactory::new()
200+
->count(20)
201+
)
202+
->create()->fresh();
203+
$matchingModel2 = ModelFactory::new()
204+
->has(
205+
BelongsToManyRelationFactory::new()
206+
->count(20)
207+
)
208+
->create()->fresh();
209+
210+
Gate::policy(Model::class, GreenPolicy::class);
211+
Gate::policy(BelongsToManyRelation::class, GreenPolicy::class);
212+
213+
$response = $this->post(
214+
'/api/models/search',
215+
[
216+
'search' => [
217+
'aggregates' => [
218+
[
219+
'relation' => 'belongsToManyRelation',
220+
'type' => 'min',
221+
'field' => 'number',
222+
'alias' => 'min_alias',
223+
],
224+
],
225+
],
226+
],
227+
['Accept' => 'application/json']
228+
);
229+
230+
$this->assertResourcePaginated(
231+
$response,
232+
[$matchingModel, $matchingModel2],
233+
new ModelResource(),
234+
[
235+
['min_alias' => $matchingModel->belongsToManyRelation()->orderBy('number', 'asc')->first()->number],
236+
['min_alias' => $matchingModel2->belongsToManyRelation()->orderBy('number', 'asc')->first()->number],
237+
]
238+
);
239+
}
240+
195241
public function test_getting_a_list_of_resources_aggregating_by_max_number(): void
196242
{
197243
$matchingModel = ModelFactory::new()
@@ -237,6 +283,52 @@ public function test_getting_a_list_of_resources_aggregating_by_max_number(): vo
237283
);
238284
}
239285

286+
public function test_getting_a_list_of_resources_aggregating_by_max_number_with_alias(): void
287+
{
288+
$matchingModel = ModelFactory::new()
289+
->has(
290+
BelongsToManyRelationFactory::new()
291+
->count(20)
292+
)
293+
->create()->fresh();
294+
$matchingModel2 = ModelFactory::new()
295+
->has(
296+
BelongsToManyRelationFactory::new()
297+
->count(20)
298+
)
299+
->create()->fresh();
300+
301+
Gate::policy(Model::class, GreenPolicy::class);
302+
Gate::policy(BelongsToManyRelation::class, GreenPolicy::class);
303+
304+
$response = $this->post(
305+
'/api/models/search',
306+
[
307+
'search' => [
308+
'aggregates' => [
309+
[
310+
'relation' => 'belongsToManyRelation',
311+
'type' => 'max',
312+
'field' => 'number',
313+
'alias' => 'max_alias',
314+
],
315+
],
316+
],
317+
],
318+
['Accept' => 'application/json']
319+
);
320+
321+
$this->assertResourcePaginated(
322+
$response,
323+
[$matchingModel, $matchingModel2],
324+
new ModelResource(),
325+
[
326+
['max_alias' => $matchingModel->belongsToManyRelation()->orderBy('number', 'desc')->first()->number],
327+
['max_alias' => $matchingModel2->belongsToManyRelation()->orderBy('number', 'desc')->first()->number],
328+
]
329+
);
330+
}
331+
240332
public function test_getting_a_list_of_resources_aggregating_by_avg_number(): void
241333
{
242334
$matchingModel = ModelFactory::new()
@@ -282,6 +374,52 @@ public function test_getting_a_list_of_resources_aggregating_by_avg_number(): vo
282374
);
283375
}
284376

377+
public function test_getting_a_list_of_resources_aggregating_by_avg_number_with_alias(): void
378+
{
379+
$matchingModel = ModelFactory::new()
380+
->has(
381+
BelongsToManyRelationFactory::new()
382+
->count(20)
383+
)
384+
->create()->fresh();
385+
$matchingModel2 = ModelFactory::new()
386+
->has(
387+
BelongsToManyRelationFactory::new()
388+
->count(20)
389+
)
390+
->create()->fresh();
391+
392+
Gate::policy(Model::class, GreenPolicy::class);
393+
Gate::policy(BelongsToManyRelation::class, GreenPolicy::class);
394+
395+
$response = $this->post(
396+
'/api/models/search',
397+
[
398+
'search' => [
399+
'aggregates' => [
400+
[
401+
'relation' => 'belongsToManyRelation',
402+
'type' => 'avg',
403+
'field' => 'number',
404+
'alias' => 'avg_alias',
405+
],
406+
],
407+
],
408+
],
409+
['Accept' => 'application/json']
410+
);
411+
412+
$this->assertResourcePaginated(
413+
$response,
414+
[$matchingModel, $matchingModel2],
415+
new ModelResource(),
416+
[
417+
['avg_alias' => $matchingModel->belongsToManyRelation()->avg('belongs_to_many_relations.number')],
418+
['avg_alias' => $matchingModel2->belongsToManyRelation()->avg('belongs_to_many_relations.number')],
419+
]
420+
);
421+
}
422+
285423
public function test_getting_a_list_of_resources_aggregating_by_sum_number(): void
286424
{
287425
$matchingModel = ModelFactory::new()
@@ -327,6 +465,52 @@ public function test_getting_a_list_of_resources_aggregating_by_sum_number(): vo
327465
);
328466
}
329467

468+
public function test_getting_a_list_of_resources_aggregating_by_sum_number_with_alias(): void
469+
{
470+
$matchingModel = ModelFactory::new()
471+
->has(
472+
BelongsToManyRelationFactory::new()
473+
->count(20)
474+
)
475+
->create()->fresh();
476+
$matchingModel2 = ModelFactory::new()
477+
->has(
478+
BelongsToManyRelationFactory::new()
479+
->count(20)
480+
)
481+
->create()->fresh();
482+
483+
Gate::policy(Model::class, GreenPolicy::class);
484+
Gate::policy(BelongsToManyRelation::class, GreenPolicy::class);
485+
486+
$response = $this->post(
487+
'/api/models/search',
488+
[
489+
'search' => [
490+
'aggregates' => [
491+
[
492+
'relation' => 'belongsToManyRelation',
493+
'type' => 'sum',
494+
'field' => 'number',
495+
'alias' => 'sum_alias',
496+
],
497+
],
498+
],
499+
],
500+
['Accept' => 'application/json']
501+
);
502+
503+
$this->assertResourcePaginated(
504+
$response,
505+
[$matchingModel, $matchingModel2],
506+
new ModelResource(),
507+
[
508+
['sum_alias' => $matchingModel->belongsToManyRelation()->sum('belongs_to_many_relations.number')],
509+
['sum_alias' => $matchingModel2->belongsToManyRelation()->sum('belongs_to_many_relations.number')],
510+
]
511+
);
512+
}
513+
330514
public function test_getting_a_list_of_resources_aggregating_by_count_relation(): void
331515
{
332516
$matchingModel = ModelFactory::new()
@@ -371,6 +555,51 @@ public function test_getting_a_list_of_resources_aggregating_by_count_relation()
371555
);
372556
}
373557

558+
public function test_getting_a_list_of_resources_aggregating_by_count_relation_with_alias(): void
559+
{
560+
$matchingModel = ModelFactory::new()
561+
->has(
562+
BelongsToManyRelationFactory::new()
563+
->count(20)
564+
)
565+
->create()->fresh();
566+
$matchingModel2 = ModelFactory::new()
567+
->has(
568+
BelongsToManyRelationFactory::new()
569+
->count(20)
570+
)
571+
->create()->fresh();
572+
573+
Gate::policy(Model::class, GreenPolicy::class);
574+
Gate::policy(BelongsToManyRelation::class, GreenPolicy::class);
575+
576+
$response = $this->post(
577+
'/api/models/search',
578+
[
579+
'search' => [
580+
'aggregates' => [
581+
[
582+
'relation' => 'belongsToManyRelation',
583+
'type' => 'count',
584+
'alias' => 'count_alias',
585+
],
586+
],
587+
],
588+
],
589+
['Accept' => 'application/json']
590+
);
591+
592+
$this->assertResourcePaginated(
593+
$response,
594+
[$matchingModel, $matchingModel2],
595+
new ModelResource(),
596+
[
597+
['count_alias' => $matchingModel->belongsToManyRelation()->count()],
598+
['count_alias' => $matchingModel2->belongsToManyRelation()->count()],
599+
]
600+
);
601+
}
602+
374603
public function test_getting_a_list_of_resources_aggregating_by_exists_relation(): void
375604
{
376605
$matchingModel = ModelFactory::new()
@@ -411,6 +640,47 @@ public function test_getting_a_list_of_resources_aggregating_by_exists_relation(
411640
);
412641
}
413642

643+
public function test_getting_a_list_of_resources_aggregating_by_exists_relation_with_alias(): void
644+
{
645+
$matchingModel = ModelFactory::new()
646+
->create()->fresh();
647+
$matchingModel2 = ModelFactory::new()
648+
->has(
649+
BelongsToManyRelationFactory::new()
650+
->count(20)
651+
)
652+
->create()->fresh();
653+
654+
Gate::policy(Model::class, GreenPolicy::class);
655+
Gate::policy(BelongsToManyRelation::class, GreenPolicy::class);
656+
657+
$response = $this->post(
658+
'/api/models/search',
659+
[
660+
'search' => [
661+
'aggregates' => [
662+
[
663+
'relation' => 'belongsToManyRelation',
664+
'type' => 'exists',
665+
'alias' => 'exists_alias',
666+
],
667+
],
668+
],
669+
],
670+
['Accept' => 'application/json']
671+
);
672+
673+
$this->assertResourcePaginated(
674+
$response,
675+
[$matchingModel, $matchingModel2],
676+
new ModelResource(),
677+
[
678+
['exists_alias' => false],
679+
['exists_alias' => true],
680+
]
681+
);
682+
}
683+
414684
public function test_getting_a_list_of_resources_aggregating_by_min_number_with_filters(): void
415685
{
416686
$matchingModel = ModelFactory::new()

0 commit comments

Comments
 (0)