Perimeter
The perimeter of a resource
perimeterQuery() sets the rows of a resource that exist for the client of the request. Rows outside it are never returned, never written and never deleted: for the client, they do not exist.
use Illuminate\Contracts\Database\Eloquent\Builder;
public function perimeterQuery(Builder $query): Builder
{
return $query->where('team_id', $this->request()->user()->team_id);
}
- Apply the constraints to
$queryitself and return it. The package ignores the returned builder, but an override that returns nothing throws aTypeError. - Read the request with
$this->request(), and the user with$this->request()->user(). - The default perimeter adds nothing: every row of the model, after its global scopes, exists.
- Put it on your base resource,
app/Rest/Resources/Resource.php, when its constraints apply to every resource.
Resources without a perimeter
php artisan rest:check lists the resources that the routes reach, through their relations too, and fails when one of them does not override perimeterQuery(): every row of such a resource is visible to the users who pass viewAny. Run it in CI. Mark a resource whose rows are meant for all these users, such as countries, with an attribute:
use Lomkit\Rest\Attributes\WithoutPerimeter;
#[WithoutPerimeter]
class CountryResource extends Resource
{
public static string $model = Country::class;
}
rows ... 1 resource with a perimeter, 1 marked #[WithoutPerimeter]
App\Rest\Resources\CountryResource ... #[WithoutPerimeter]
See Artisan.
403. A row that must not exist for the client belongs in the perimeter, which answers as if the row did not exist. See Policies.Where it applies
The perimeter of each resource applies wherever a request reaches its rows, at every level:
| Request | The perimeter applies to |
|---|---|
search | the rows of the page, each include level, each relation of a dotted include, each relation of a filter path, each relation group, the subquery of a relation sort, and each level of an aggregate |
mutate | the keys of every entry, at the root and under relations, each through the perimeter of its own resource, and the records of the response block |
destroy, restore, forceDestroy | the keys of resources |
| actions | the rows of the Search target and the keys of the Resources target |
| metrics | the query that the metric computes on |
| gates | the update, delete, restore and forceDelete answers of a row |
Rest::search() | the same rows as the search endpoint, for the given user |
Searches
A search reads the rows of the page through the perimeter. Each include, each filter on a relation and each aggregate applies the perimeter of the related resource, each in its own group:
{"search": {"filters": [{"field": "author.tags.name", "value": "featured"}]}}
This filter matches a post only through an author and a tag that the perimeters of their resources keep. An include renders null, or leaves a row out, when the perimeter of its resource leaves the related row out.
Writes
The keys of a mutation are checked during validation, with one query per resource class, through the perimeter of the resource of each row. A key outside the perimeter gives a 422, with the message of a key that does not exist:
{
"message": "The selected mutate.0.key is invalid.",
"errors": {
"mutate.0.key": ["The selected mutate.0.key is invalid."]
}
}
- The rows that this query loads are the rows that the mutation writes: the perimeter runs once per request, before any write.
- A created row is not checked against the perimeter: it is written, and its record in the
responseblock isnullwhen the perimeter leaves it out. syncandtogglecompare their keys with the attached rows that the perimeter of the related resource returns. An attached row outside it stays attached.
Keys out of reach
When a request names keys that the perimeter of their resource does not return, the package dispatches the Lomkit\Rest\Events\KeysOutOfReach event before the 422, once per resource and per request, so that a listener can record it. It covers the key of mutate entries and of relation entries, the resources of an action, destroy, restore and forceDestroy. The answer does not change. See Events and hooks.
Deletions
destroy, restore and forceDestroy read their keys through the perimeter, with the trashed rows on restore and forceDestroy. A key outside it gives The selected resources.0 is invalid. See Deleting.
One perimeter for every endpoint
A resource has one perimeter. To let one endpoint reach rows that the others leave out, branch on the endpoint of the request:
use Illuminate\Contracts\Database\Eloquent\Builder;
use Lomkit\Rest\Http\Routing\Endpoint;
public function perimeterQuery(Builder $query): Builder
{
if ($this->request()->endpoint() === Endpoint::Mutate) {
return $query;
}
return $query->where('published', true);
}
A row that the perimeter leaves out on one endpoint only gives a 422 on that endpoint only. A search still answers that endpoint's gates from the policy: put a condition that a gate must follow in the policy.
Conditions stay grouped
The conditions of the perimeter form one group, joined to the rest of the query with and. An orWhere() in it cannot widen the filters of the client or the keys of a write:
public function perimeterQuery(Builder $query): Builder
{
return $query
->where('user_id', $this->request()->user()->id)
->orWhere('public', true);
}
This perimeter runs as (user_id = ? or public = ?) and ..., whatever the request adds after it. The conditions of scopes and instructions are grouped the same way.
Text searches on a search engine
A text search that runs on a Laravel Scout search engine takes its rows from the index: the rows of its page are loaded by key, without perimeterQuery(). Its resource declares the perimeter of the index with perimeter():
use Lomkit\Rest\Fields\Integer;
use Lomkit\Rest\Text\Engine\EngineQuery;
use Lomkit\Rest\Text\ScoutEngine;
use Lomkit\Rest\Text\TextSearch;
public function text(): ?TextSearch
{
return ScoutEngine::make()->perimeter(
fn (EngineQuery $query) => $query->where('team_id', '=', $this->request()->user()->team_id),
[Integer::make('team_id')],
);
}
Declare there every condition of perimeterQuery() that keeps rows from some users. Includes and aggregates of such a search still apply the perimeter of their own resource. See Search engines.
Global scopes and tenants
The global scopes of the model apply under the perimeter. A perimeter can take in rows that a global scope leaves out, such as with withTrashed(), and the write endpoints then reach them.
With several tenants, the perimeter is the place to scope every resource to the current tenant, on your base resource. A global scope of the model works too, and also applies to the rows of a search engine page.
Validation
Field rules, resource rules, rules that depend on the entry, labels and messages, unknown keys, and how the package validates each payload in one pass.
Limits
Page sizes, include limits, capped totals, and the limits of rest.limits on lists, depths, totals, values, response rows, action jobs and metric groups.