Skip to content

Commit aab3ab5

Browse files
edepauwGautierDele
andauthored
Add HandlePrecognitiveRequests middleware for frontend rules validation (#180)
* add HandlePrecognitiveRequests for frontend rules validation * ✨ precognition support --------- Co-authored-by: Gautier DELEGLISE <gautier.deleglise@gmail.com>
1 parent a48d5ab commit aab3ab5

File tree

3 files changed

+193
-1
lines changed

3 files changed

+193
-1
lines changed

config/rest.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,23 @@
4343
],
4444
],
4545

46+
/*
47+
|--------------------------------------------------------------------------
48+
| Precognition Support
49+
|--------------------------------------------------------------------------
50+
|
51+
| This option enables support for Laravel Precognition, which allows
52+
| frontend applications to perform validation requests without executing
53+
| controller logic. When enabled, requests containing the "Precognition"
54+
| header will only trigger middleware and validation, skipping the actual
55+
| controller method. This is especially useful for live form validation.
56+
|
57+
*/
58+
59+
'precognition' => [
60+
'enabled' => false,
61+
],
62+
4663
/*
4764
|--------------------------------------------------------------------------
4865
| Rest Documentation

src/Rest.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace Lomkit\Rest;
44

5+
use Illuminate\Foundation\Http\Middleware\HandlePrecognitiveRequests;
56
use Lomkit\Rest\Contracts\Http\Routing\Registrar;
67
use Lomkit\Rest\Documentation\Schemas\OpenAPI;
78
use Lomkit\Rest\Http\Controllers\Controller;
@@ -30,13 +31,20 @@ public function resource(string $name, string $controller, array $options = [])
3031
$registrar = new ResourceRegistrar(app('router'));
3132
}
3233

33-
return (new PendingResourceRegistration(
34+
$pendingResourceRegistration = (new PendingResourceRegistration(
3435
$registrar,
3536
$name,
3637
$controller,
3738
$options
3839
))
3940
->middleware(EnforceExpectsJson::class);
41+
42+
return tap($pendingResourceRegistration, function ($pendingResourceRegistration) {
43+
if (config('rest.precognition.enabled', false)) {
44+
$pendingResourceRegistration
45+
->middleware(HandlePrecognitiveRequests::class);
46+
}
47+
});
4048
}
4149

4250
/**
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
<?php
2+
3+
namespace Lomkit\Rest\Tests\Feature\Controllers;
4+
5+
use Illuminate\Support\Facades\Gate;
6+
use Lomkit\Rest\Tests\Feature\TestCase;
7+
use Lomkit\Rest\Tests\Support\Database\Factories\BelongsToManyRelationFactory;
8+
use Lomkit\Rest\Tests\Support\Database\Factories\ModelFactory;
9+
use Lomkit\Rest\Tests\Support\Models\BelongsToManyRelation;
10+
use Lomkit\Rest\Tests\Support\Models\Model;
11+
use Lomkit\Rest\Tests\Support\Policies\GreenPolicy;
12+
use Lomkit\Rest\Tests\Support\Rest\Resources\ModelResource;
13+
14+
class PrecognitionOperationsTest extends TestCase
15+
{
16+
protected function getEnvironmentSetUp($app): void
17+
{
18+
$app['config']->set('rest.precognition.enabled', true);
19+
}
20+
21+
public function test_precognition_getting_a_list_of_resources_aggregating_by_unauthorized_relation(): void
22+
{
23+
ModelFactory::new()->count(2)->create();
24+
25+
Gate::policy(Model::class, GreenPolicy::class);
26+
Gate::policy(BelongsToManyRelation::class, GreenPolicy::class);
27+
28+
$response = $this->post(
29+
'/api/models/search',
30+
[
31+
'search' => [
32+
'aggregates' => [
33+
[
34+
'relation' => 'unauthorized_relation',
35+
'type' => 'min',
36+
'field' => 'id',
37+
],
38+
],
39+
],
40+
],
41+
[
42+
'Precognition' => 'true',
43+
'Accept' => 'application/json',
44+
]
45+
);
46+
47+
$response->assertStatus(422);
48+
$response->assertExactJsonStructure(['message', 'errors' => ['search.aggregates.0.relation']]);
49+
}
50+
51+
public function test_getting_a_list_of_resources_aggregating_by_specifying_field_when_not_necessary(): void
52+
{
53+
ModelFactory::new()->count(2)->create();
54+
55+
Gate::policy(Model::class, GreenPolicy::class);
56+
Gate::policy(BelongsToManyRelation::class, GreenPolicy::class);
57+
58+
$response = $this->post(
59+
'/api/models/search',
60+
[
61+
'search' => [
62+
'aggregates' => [
63+
[
64+
'relation' => 'belongsToManyRelation',
65+
'type' => 'exists',
66+
'field' => 'id',
67+
],
68+
],
69+
],
70+
],
71+
[
72+
'Accept' => 'application/json',
73+
'Precognition' => 'fields=aggregates.0.relation,aggregates.0.type',
74+
]
75+
);
76+
77+
$response->assertStatus(422);
78+
$response->assertExactJsonStructure(['message', 'errors' => ['search.aggregates.0.field']]);
79+
}
80+
81+
public function test_precognition_getting_a_list_of_resources_aggregating_by_min_number(): void
82+
{
83+
$matchingModel = ModelFactory::new()
84+
->has(
85+
BelongsToManyRelationFactory::new()
86+
->count(20)
87+
)
88+
->create()->fresh();
89+
$matchingModel2 = ModelFactory::new()
90+
->has(
91+
BelongsToManyRelationFactory::new()
92+
->count(20)
93+
)
94+
->create()->fresh();
95+
96+
Gate::policy(Model::class, GreenPolicy::class);
97+
Gate::policy(BelongsToManyRelation::class, GreenPolicy::class);
98+
99+
$response = $this->post(
100+
'/api/models/search',
101+
[
102+
'search' => [
103+
'aggregates' => [
104+
[
105+
'relation' => 'belongsToManyRelation',
106+
'type' => 'min',
107+
'field' => 'number',
108+
],
109+
],
110+
],
111+
],
112+
[
113+
'Accept' => 'application/json',
114+
'Precognition' => 'true',
115+
]
116+
);
117+
118+
$response->assertNoContent();
119+
}
120+
121+
public function test_precognition_getting_a_list_of_resources_aggregating_by_min_number_without_precognition(): void
122+
{
123+
$matchingModel = ModelFactory::new()
124+
->has(
125+
BelongsToManyRelationFactory::new()
126+
->count(20)
127+
)
128+
->create()->fresh();
129+
$matchingModel2 = ModelFactory::new()
130+
->has(
131+
BelongsToManyRelationFactory::new()
132+
->count(20)
133+
)
134+
->create()->fresh();
135+
136+
Gate::policy(Model::class, GreenPolicy::class);
137+
Gate::policy(BelongsToManyRelation::class, GreenPolicy::class);
138+
139+
$response = $this->post(
140+
'/api/models/search',
141+
[
142+
'search' => [
143+
'aggregates' => [
144+
[
145+
'relation' => 'belongsToManyRelation',
146+
'type' => 'min',
147+
'field' => 'number',
148+
],
149+
],
150+
],
151+
],
152+
[
153+
'Accept' => 'application/json',
154+
]
155+
);
156+
157+
$this->assertResourcePaginated(
158+
$response,
159+
[$matchingModel, $matchingModel2],
160+
new ModelResource(),
161+
[
162+
['belongs_to_many_relation_min_number' => $matchingModel->belongsToManyRelation()->orderBy('number', 'asc')->first()->number],
163+
['belongs_to_many_relation_min_number' => $matchingModel2->belongsToManyRelation()->orderBy('number', 'asc')->first()->number],
164+
]
165+
);
166+
}
167+
}

0 commit comments

Comments
 (0)