Repository navigation
feat: add doc update --if-updated-at to refuse overwriting newer changes - #157
Conversation
…anges `doc update` overwrote the document unconditionally, so an update based on a stale copy silently discarded edits saved in the meantime, for example from the browser. `--if-updated-at <timestamp>` takes the `updatedAt` printed by `doc get --json`: when the document's current `updatedAt` differs, the update stops with `DOCUMENT_CHANGED` before writing. Otherwise the update is sent with `lastRevision`, so Outline also rejects it (409) if the document is saved between the check and the write. Edits not yet saved from an open editor aren't detected. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
scottlovegrove
left a comment
There was a problem hiding this comment.
Thanks, the approach looks right to me: the timestamp check catches changes made since the caller's get, and lastRevision catches anything that lands between the lookup and the write. One issue inline with how an empty or invalid timestamp is handled.
| const body: Record<string, unknown> = { id: resolvedId } | ||
| const current = (await resolveDocumentRef(id)) as Document | ||
| if ( | ||
| opts.ifUpdatedAt && |
There was a problem hiding this comment.
An empty value turns the guard off: --if-updated-at "" is falsy, so both the timestamp check and lastRevision are skipped and the update overwrites the document unconditionally. That's easy to hit with the skill's example, since ts ends up empty if the preceding ol doc get fails for any reason. A value that doesn't parse (e.g. null, which jq -r prints when .updatedAt is missing) does fail safe, but it reports DOCUMENT_CHANGED with "…not null", which points at the wrong problem.
Could you check opts.ifUpdatedAt !== undefined here and on the lastRevision line below, and reject a value where Date.parse returns NaN with its own error before resolving the document? A test for the empty-string case would be good too.
There was a problem hiding this comment.
Thanks. Fixed in bc119c2: a value where Date.parse returns NaN is now rejected with INVALID_TIMESTAMP before the document is looked up. I also added DOCUMENT_CHANGED and INVALID_TIMESTAMP to the ErrorCode list in src/lib/errors.ts, which I'd missed. Checked against Outline 1.11.0: both values exit 1 without touching the document, and a valid timestamp still updates.
… the guard An empty value is falsy, so `--if-updated-at ""` skipped both the timestamp check and `lastRevision` and overwrote the document unconditionally; the skill's example produces that value when the preceding `doc get` fails. A value that doesn't parse, such as the `null` that `jq -r` prints for a missing field, was refused but reported as `DOCUMENT_CHANGED`. Both are now rejected with `INVALID_TIMESTAMP` before the document is looked up, and the guard checks test for presence rather than truthiness. `DOCUMENT_CHANGED` and `INVALID_TIMESTAMP` are added to the `ErrorCode` list. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
## [2.3.0](v2.2.0...v2.3.0) (2026-10-23) ### Features * add `doc update --if-updated-at` to refuse overwriting newer changes ([#157](#157)) ([7fd2e15](7fd2e15))
## [2.3.0](v2.2.0...v2.3.0) (2026-10-23) ### Features * add `doc update --if-updated-at` to refuse overwriting newer changes ([#157](#157)) ([7fd2e15](7fd2e15))
|
🎉 This PR is included in version 2.3.0 🎉 The release is available on: Your semantic-release bot 📦🚀 |
ol doc updateoverwrites the document unconditionally, so an update built from a stale copy silently discards whatever was saved in the meantime, for example edits made in the browser. Scripts and agents that edit a document with get → edit → update had no way to detect that.--if-updated-at <timestamp>takes theupdatedAtthatol doc get --jsonprints. If the document's currentupdatedAtdiffers, the update stops withDOCUMENT_CHANGEDbefore writing. Otherwise it's sent withlastRevision, so Outline also rejects it with a 409 if the document is saved between the check and the write; Outline increments the revision on every save, including collaborative edits from the editor. The one thing it can't see is typing that an open editor hasn't saved yet. The lookupupdatealready does suppliesupdatedAtandrevision, so the guard adds no API call. The README and the skill list it, and the skill has an example of the get → edit → update flow.Tested:
npm test(247 passing, with tests that a stale timestamp exits before anydocuments.updatecall and that a matching one sendslastRevision), type-check, lint, format and the skill sync check. Against self-hosted Outline 1.11.0: a matching--if-updated-atupdates the document; a stale one exits 1 withDOCUMENT_CHANGEDand leaves it unchanged;documents.updatewith a stalelastRevisionreturns 409Document has been modified since the provided revision; and the skill's get → edit → update example works as written.