Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,16 @@ Here is a quick look at what you can do using API search method:
]
}
],
"aggregates": [
{
"relation": "stars",
"type": "max",
"field": "rate",
"filters": [
{"field": 'approved', "value": true}
]
},
],
"page": 2,
"limit": 10
}
Expand All @@ -74,12 +84,13 @@ TODO

## Contributing


TODO

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

- Custom directives (Filters / sorting)
- Actions / Metrics
- Automatic Gates (with config customisation)
- Aggregating
- Automatic documentation with extension possible
- Automatic documentation with extension possible
- Add the possibility to disable authorization
34 changes: 33 additions & 1 deletion src/Http/Requests/SearchRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
use Lomkit\Rest\Contracts\QueryBuilder;
use Lomkit\Rest\Http\Controllers\Controller;
use Lomkit\Rest\Http\Resource;
use Lomkit\Rest\Rules\AggregateField;
use Lomkit\Rest\Rules\AggregateFilterable;
use Lomkit\Rest\Rules\Includable;

class SearchRequest extends RestRequest
Expand All @@ -31,6 +33,8 @@ public function searchRules(Resource $resource, $prefix = '')
$this->sortsRules($resource, $prefix.'sorts'),
[$prefix.'selects' => ['sometimes', 'array']],
$this->selectsRules($resource, $prefix.'selects'),
[$prefix.'aggregates' => ['sometimes', 'array']],
$this->aggregatesRules($resource, $prefix.'aggregates'),
[
'limit' => ['sometimes', 'integer', Rule::in($resource->exposedLimits($this))],
'page' => ['sometimes', 'integer']
Expand All @@ -41,7 +45,7 @@ public function searchRules(Resource $resource, $prefix = '')
}

// @TODO: For now it's prohibited to have more than one nested depth, is this needed ?
protected function filtersRules(Resource $resource, string $prefix, $isMaxDepth = false) {
public function filtersRules(Resource $resource, string $prefix, $isMaxDepth = false) {
$rules = array_merge(
[
$prefix.'.*.field' => [
Expand Down Expand Up @@ -138,4 +142,32 @@ protected function includesRules(Resource $resource) {
]
];
}

protected function aggregatesRules(Resource $resource, string $prefix) {
return [
$prefix.'.*.relation' => [
'required',
Rule::in(
array_keys(
$resource->nestedRelations(app()->make(RestRequest::class))
)
)
],
$prefix.'.*.type' => [
Rule::in(['count', 'min', 'max', 'avg', 'sum', 'exists'])
],
$prefix.'.*.field' => [
'required_if:'.$prefix.'.*.type,min,max,avg,sum',
'prohibited_if:'.$prefix.'.*.type,count,exists'
],
$prefix.'.*' => [
AggregateField::make()
->resource($resource)
],
$prefix.'.*.filters' => [
AggregateFilterable::make()
->resource($resource)
]
];
}
}
14 changes: 11 additions & 3 deletions src/Http/Response.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,17 @@ public function modelToResponse(Model $model, Resource $resource, array $request
// toArray to take advantage of Laravel's logic
collect($model->attributesToArray())
->only(
isset($requestArray['selects']) ?
collect($requestArray['selects'])->pluck('field') :
$resource->exposedFields(app()->make(RestRequest::class))
array_merge(
isset($requestArray['selects']) ?
collect($requestArray['selects'])->pluck('field')->toArray() :
$resource->exposedFields(app()->make(RestRequest::class)),
// Here we add the aggregates
collect($requestArray['aggregates'] ?? [])
->map(function ($aggregate) {
return Str::snake($aggregate['relation']).'_'.$aggregate['type'].(isset($aggregate['field']) ? '_'.$aggregate['field'] : '');
})
->toArray()
)
)
->toArray(),
collect($model->getRelations())
Expand Down
21 changes: 21 additions & 0 deletions src/Query/Traits/PerformSearch.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Lomkit\Rest\Query\Traits;

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

$this->when(isset($parameters['aggregates']), function () use ($parameters) {
$this->applyAggregates($parameters['aggregates']);
});

// @TODO: is this a problem also with HasMany ??
// @TODO: this will be the problem for every relation, need to fix this
// In case of BelongsToMany we cap the limit
Expand Down Expand Up @@ -114,4 +119,20 @@ public function applyIncludes($includes) {
$this->include($include);
}
}

public function aggregate($aggregate) {
return $this->queryBuilder->withAggregate([$aggregate['relation'] => function(Builder $query) use ($aggregate) {
$resource = $this->resource->relationResource($aggregate['relation']);

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

return $queryBuilder->search(['filters' => $aggregate['filters'] ?? []]);
}], $aggregate['field'] ?? '*', $aggregate['type']);
}

public function applyAggregates($aggregates) {
foreach ($aggregates as $aggregate) {
$this->aggregate($aggregate);
}
}
}
148 changes: 148 additions & 0 deletions src/Rules/AggregateField.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
<?php

namespace Lomkit\Rest\Rules;

use Closure;
use Illuminate\Contracts\Validation\DataAwareRule;
use Illuminate\Contracts\Validation\Rule;
use Illuminate\Contracts\Validation\ValidatorAwareRule;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Validator;
use Lomkit\Rest\Concerns\Makeable;
use Lomkit\Rest\Http\Requests\RestRequest;
use Lomkit\Rest\Http\Requests\SearchRequest;
use Lomkit\Rest\Http\Resource;

class AggregateField implements Rule, DataAwareRule, ValidatorAwareRule
{

use Makeable;

/**
* The data under validation.
*
* @var array
*/
protected $data;

/**
* The error message after validation, if any.
*
* @var array
*/
protected $messages = [];

/**
* The resource related to.
*
* @var Resource
*/
protected $resource = null;

/**
* The validator performing the validation.
*
* @var \Illuminate\Validation\Validator
*/
protected $validator;

/**
*
*
* @param $resource
* @return $this
*/
public function resource($resource)
{
$this->resource = $resource;

return $this;
}

public function passes($attribute, $value)
{
$validator = Validator::make(
$this->data,
$this->buildValidationRules($attribute, $value)
);

if ($validator->fails()) {
return $this->fail($validator->messages()->all());
}

return true;
}

/**
* Build the array of underlying validation rules based on the current state.
*
* @return array
*/
protected function buildValidationRules($attribute, $value)
{
$relationResource = $this->resource->relationResource($value['relation']);

if (is_null($relationResource)) {
return [];
}

return [
$attribute.'.field' => \Illuminate\Validation\Rule::in(
$relationResource->exposedFields(app()->make(RestRequest::class))
)
];
}

/**
* Get the validation error message.
*
* @return array
*/
public function message()
{
return $this->messages;
}

/**
* Adds the given failures, and return false.
*
* @param array|string $messages
* @return bool
*/
protected function fail($messages)
{
$messages = collect(Arr::wrap($messages))->map(function ($message) {
return $this->validator->getTranslator()->get($message);
})->all();

$this->messages = array_merge($this->messages, $messages);

return false;
}

/**
* Set the current validator.
*
* @param \Illuminate\Contracts\Validation\Validator $validator
* @return $this
*/
public function setValidator($validator)
{
$this->validator = $validator;

return $this;
}

/**
* Set the current data under validation.
*
* @param array $data
* @return $this
*/
public function setData($data)
{
$this->data = $data;

return $this;
}
}
Loading