Versioned rows
Two users open the same product, both edit it, both save: without a check, the second save silently overwrites the first. A versioned resource lets each update entry send the version of the row that the client read. When the row has changed since, the whole mutation answers 409 and writes nothing.
use Lomkit\Rest\Fields\DateTime;
use Lomkit\Rest\Fields\ID;
use Lomkit\Rest\Fields\Integer;
use Lomkit\Rest\Fields\Text;
public function fields(): array
{
return [
ID::make('id'),
Integer::make('category_id'),
Text::make('name'),
Integer::make('price'),
DateTime::make('updated_at')->readonly(),
];
}
public function versioned(): bool
{
return true;
}
The version of a row is its updated_at, as the search returns it.
Sending a version
{
"mutate": [
{"operation": "update", "key": 1, "version": "2026-01-01T08:00:00.000000Z", "attributes": {"name": "Desk light"}}
],
"response": {
"selects": [{"field": "updated_at"}]
}
}
{
"operations": [{"operation": "update", "key": 1}],
"records": [{"updated_at": "2026-03-01T10:00:00.000000Z"}]
}
versionis optional: anupdateentry without it writes as before.- To save a row twice, read its new version from
records, as above. - Every entry is compared with the version that the row had before the first write of the request: two entries on the same row can send the same version.
Conflicts
When a row has another version, the mutation answers 409, runs no hook, dispatches no ResourceMutated and writes nothing, not even the entries that hold no conflict:
{
"mutate": [
{"operation": "update", "key": 1, "version": "2026-01-01T08:00:00.000000Z", "attributes": {"name": "Desk light"}},
{"operation": "update", "key": 2, "version": "2026-01-02T08:00:00.000000Z", "attributes": {"price": 99}},
{"operation": "create", "attributes": {"category_id": 1, "name": "Wall lamp", "price": 45}}
]
}
{
"message": "The record was changed by another request.",
"conflicts": [
{"path": "mutate.1", "key": 2, "version": "2026-03-01T10:00:00.000000Z"}
]
}
conflictslists every entry whose version differs: itspathin the payload, thekeyof the row, and theversionof the row now, ornullwhen the row is gone.- A client shows the conflict, reloads the row and lets the user save again with the new version.
Nested entries and key lists
- Entries under
relationsacceptversionwhen their resource is versioned, with paths such asmutate.0.relations.products.1. - With a list of keys, every row of the list has to hold that version. Each key whose version differs gives one conflict, under the path of the entry:
{
"message": "The record was changed by another request.",
"conflicts": [
{"path": "mutate.0.relations.products.0", "key": 2, "version": "2026-01-02T08:00:00.000000Z"}
]
}
When the check runs
- In the transaction of the mutation, after the row checks and before
mutatingAll(). A row that a policy refuses answers403first. - Before a confirmation: a conflict answers
409, and the resource asks for no confirmation. - One
SELECT ... FOR UPDATEper resource reads the rows again, so that no other request changes them before the commit. - With
transactions.attemptsabove 1, each attempt runs the check again, see Deadlock retries.
Rules
versionon a resource that is not versioned givesThe mutate.0.version field is prohibited.versionon an operation other thanupdategivesThe mutate.0.version field is prohibited when mutate.0.operation is create., or... when mutate.0.relations.products.0.operation is attach.under a relation.versionmust be a string:The mutate.0.version field must be a string.,nullincluded.destroy,restoreandforceDestroytake no version.versioned()on a model without timestamps throwsInvalidResourceException:The resource [App\Rest\Resources\ProductResource] returns true from versioned(), but its model does not use timestamps. The version of a row is its updated_at column: turn the timestamps of the model on, or return false from versioned().versioned()without theupdated_atfield throwsInvalidResourceException:The resource [App\Rest\Resources\ProductResource] returns true from versioned(), but does not declare the field [updated_at]. The version of a row is this column as the field returns it: declare it, for example DateTime::make('updated_at')->readonly().
$table->timestamps(precision: 6). With seconds, two saves in the same second give the same version, and the second one is not detected.OpenAPI
The update entries of a versioned resource document version, typed like its updated_at field. A mutate operation that writes a versioned resource, at its root or under its relations, documents the 409 response with the shared RestConflict component. See OpenAPI.