Skip to content

feat: add doc update --if-updated-at to refuse overwriting newer changes - #157

Merged
scottlovegrove merged 2 commits into
Doist:mainfrom
andriykohut:feat/doc-update-if-updated-at
Oct 23, 2026
Merged

scottlovegrove merged 2 commits into
Doist:mainfrom
andriykohut:feat/doc-update-if-updated-at

Conversation

@andriykohut

Copy link
Copy Markdown
Contributor

ol doc update overwrites 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 the updatedAt that ol doc get --json prints. If the document's current updatedAt differs, the update stops with DOCUMENT_CHANGED before writing. Otherwise it's sent with lastRevision, 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 lookup update already does supplies updatedAt and revision, 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 any documents.update call and that a matching one sends lastRevision), type-check, lint, format and the skill sync check. Against self-hosted Outline 1.11.0: a matching --if-updated-at updates the document; a stale one exits 1 with DOCUMENT_CHANGED and leaves it unchanged; documents.update with a stale lastRevision returns 409 Document has been modified since the provided revision; and the skill's get → edit → update example works as written.

…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 scottlovegrove left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/commands/document.ts Outdated
const body: Record<string, unknown> = { id: resolvedId }
const current = (await resolveDocumentRef(id)) as Document
if (
opts.ifUpdatedAt &&

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
@scottlovegrove
scottlovegrove merged commit 7fd2e15 into Doist:main Oct 23, 2026
4 checks passed
doist-release-bot Bot added a commit that referenced this pull request Oct 23, 2026
## [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))
@andriykohut
andriykohut deleted the feat/doc-update-if-updated-at branch October 23, 2026 12:00
doist-release-bot Bot added a commit that referenced this pull request Oct 23, 2026
## [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))
@doist-release-bot

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 2.3.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants