Writing

Deleting and restoring

Delete, restore and force delete rows by key with destroy, restore and forceDestroy, in one transaction with per-row policy checks.

Three endpoints manage the lifecycle of rows: destroy deletes them (a soft delete on a SoftDeletes model), restore brings soft-deleted rows back, and forceDestroy removes them for good.

routes/api.php
use Lomkit\Rest\Facades\Rest;

Rest::resource('documents', DocumentsController::class)->withSoftDeletes();
MethodURIEndpointPolicy ability
DELETE/api/documentsdestroydelete
POST/api/documents/restorerestorerestore
DELETE/api/documents/forceforceDestroyforceDelete
  • destroy is registered by default. restore and forceDestroy are opt-in: withSoftDeletes() registers both, withSoftDeletes('restore') only one.
  • withSoftDeletes() throws InvalidResourceException when the routes are registered if the model does not use SoftDeletes.
  • except('destroy') leaves destroy out.
  • restore and forceDelete exist only for a model that uses SoftDeletes. On another model, they are not gates, and the details leave them out of gates and authorizations.

Request and response

DELETE /api/documents
{
  "resources": [1, 2]
}
200 OK
{
  "data": [
    {"id": 1, "title": "Annual report", "archived": false},
    {"id": 2, "title": "Budget", "archived": false}
  ],
  "meta": {}
}
  • The response holds the rows that the endpoint wrote, with the selectable fields of the resource, without gates or aggregates.
  • restore and forceDestroy take the same body and answer the same way. forceDestroy accepts live and soft-deleted rows.

Keys

  • resources is a list of keys, typed like the ID field of the resource: an integer for integer keys, a string, a UUID or a ULID otherwise. A list, an object or null as an entry gives a 422 under resources.0, such as The resources.0 field must be an integer.
  • resources must be a JSON list of at most rest.limits.sizes.resources keys, 1000 by default: The resources field must be a list.
  • A repeated key gives a 422 on every copy. UUID and ULID keys compare without case.
  • The keys are read through the perimeterQuery() of the resource, with the global scopes of the model, trashed rows included on restore and forceDestroy. A key that it does not return gives The selected resources.0 is invalid. On destroy, a key that is already trashed gives the same 422.
  • A key outside the perimeter also dispatches the KeysOutOfReach event. A key refused for another reason, its type or a repeat, dispatches nothing.
  • confirmation carries the token of a confirmation. It must be a string: The confirmation field must be a string.
  • The endpoints take no search. An empty array is accepted, and any other value gives The search field is prohibited., or The search field must be an array. for null. Nothing is written then.

Checks and transaction

  1. viewAny on the resource is checked before validation.
  2. Validation reads the rows of the keys, through the perimeter, with one query per 1,000 keys.
  3. Every row is checked with delete, restore or forceDelete before any row is written. A denied row answers 403, and nothing is written.
  4. destroyConfirmation(), restoreConfirmation() or forceDestroyConfirmation() can ask for a confirmation: the endpoint then answers 428, and nothing is written.
  5. The rows are written in one database transaction, on the connection of the model, in the order of resources.

Inside the transaction:

destroyrestoreforceDestroy
destroyingAll($models)restoringAll($models)forceDestroyingAll($models)
per row: destroying($model), performDelete(), destroyed($model)per row: restoring($model), performRestore(), restored($model)per row: forceDestroying($model), performForceDelete(), forceDestroyed($model)
destroyedAll($models)restoredAll($models)forceDestroyedAll($models)
  • A failure on any row, in a hook, in a perform method or in the database, rolls back the rows written before it, and the request answers with the error.
  • An observer or a listener that runs after commit runs only when the transaction commits. The ResourceDestroyed, ResourceRestored and ResourceForceDestroyed events are dispatched once, after the commit.
  • With transactions.attempts above 1, a deadlock runs the transaction again, see Deadlock retries.
  • A perform method that writes on another connection writes outside the transaction.

See Events and hooks.

Deleting children with their parent

To delete child rows in the same request as an update of their parent, use a delete entry in mutate on a relation declared deletable(), see Relations. The destroy hooks do not run there.

Errors

403 Forbidden
{
  "message": "This action is unauthorized."
}
422 Unprocessable Content
{
  "message": "The selected resources.0 is invalid.",
  "errors": {
    "resources.0": ["The selected resources.0 is invalid."]
  }
}
StatusCause
403viewAny is denied, or a row is denied by delete, restore or forceDelete. The message is the one of the first denied row in the order of resources
422A key outside the perimeter, missing, already trashed on destroy, repeated, or of the wrong type
422A search key that is not an empty array
428The resource asks for a confirmation, see Confirmation

With the DenialsRenderer registered, a 403 also lists every denied row under denials, with its path (resources.1), ability, key and message. See Policies.