Revisions — writing against what was read
A caller reads a sales order, a person edits it for five minutes, and the save goes out. Meanwhile a colleague changed the same order. Without a precondition, the save overwrites every field it touches with values decided against a state that no longer exists.
A revision names the state a caller read. A write that sends it back is refused if the record has moved on since.
What a revision covers
Section titled “What a revision covers”A record’s revision is a hash of its input fields — the values a person or an integration writes. System fields are not part of it:
- An input field changes its value → the revision changes, whoever changed it. A determination that rewrites an input field moves it too.
- Only system fields change → the revision stays. A rollup adding a picked quantity to an open sales order, an action moving a status, a snapshot address refreshed by the system: none of these make an open form stale.
- Row order is not part of it; the rows of an input collection count by id and by their input fields. A system collection or struct counts not at all.
The revision is computed, never stored. The same function, revisionOf(schema, record) from @onerp/meta/records, runs on the server and in the browser, so a
client holding a record and its schema knows the record’s revision without asking.
A caller deciding on a system fact — reducing a quantity because nothing was picked yet — is not protected by the revision. That rule belongs on the document, as a validator or an action’s precondition.
The HTTP contract
Section titled “The HTTP contract”Every response that carries one record — read, create, update, archive, restore, delete — answers with
ETag: W/"3f1c9a0b7d2e4c65"Cache-Control: no-storeThe tag is weak: two responses with the same revision may still differ in system fields, so no cache may answer a later read with it. Lists carry no tag.
If-Match is honoured on every write to one record: PATCH, DELETE, archive,
restore, and a bound action’s POST.
- No header → no check, exactly as before.
*→ no check.- A list of tags → the write proceeds if any of them is the current revision.
- Otherwise →
412 core.stale_revision, and nothing is written.
The check runs against the record as the unit of work loaded it. A write racing in
between the check and the commit is caught by the internal row version, which answers
409 core.concurrent_modification.
In the client and the form
Section titled “In the client and the form”@onerp/client takes the revision as a trailing revision argument on update,
delete, archive, restore and operations.run, and sends it as If-Match. The form
computes it from the record it loaded and sends it with every save and every action.
A refused save keeps the edits and shows the conflict; the person reloads to see what
changed.
- Drafts carry no check: the draft routes ignore
If-Match. A draft is one person’s unfinished document. - A response replayed for a repeated idempotency key carries no tag; the replay is the stored body alone.
- The MCP tools do not take or return a revision yet.