Summary
On a collection whose supports does not include revisions, content update writes data straight into the content table columns — correct and immediately live. A subsequent content publish then promotes the stored liveRevisionId, which may be arbitrarily old, and syncs every field of it back over the fresh columns.
The update is destroyed, and publish reports {"success": true} with exit code 0.
The damage scales with the age of liveRevisionId. In our case the live revision was six weeks old.
Affected: verified in 0.1.0 (our deployment) and 0.29.0 (latest) — the logic is unchanged between them.
Reproduction
Verified end-to-end against a live deployment on 0.1.0:
- A collection with
supports = NULL (or any value without revisions), on an item that has a live_revision_id older than its current column data.
content update <collection> <slug> --rev <rev> --file payload.json — change one field, e.g. title.
→ Works. Columns updated, marker present in content get, in the single-item API and in ?status=published. version bumps, no revision row is created, draft_revision_id stays NULL.
content publish <collection> <slug>
→ {"success": true}, ✔ Published, exit 0.
- Re-read any channel: the change is gone.
live_revision_id unchanged, still zero revision rows for the entry.
Measured state around step 3 (title had marker before, clean after; live_revision_id never moved):
|
before publish |
after publish |
column title |
… W842TEST |
… (six-week-old value) |
live_revision_id |
01KT90EE87RDH41K5096A1MC2E |
unchanged |
| revision rows |
1 (six weeks old) |
1 |
Root cause
Update correctly skips the revision machinery when the collection lacks revisions:
// 0.29.0 src/emdash-runtime.ts:2743-2811 (0.1.0: 1457-1521)
let usesDraftRevisions = false;
if (processedData) {
try {
const collectionInfo = await this.schemaRegistry.getCollectionWithFields(collection);
if (collectionInfo?.supports?.includes("revisions")) {
usesDraftRevisions = true;
// ... create draft revision, set draft_revision_id
}
} catch { /* Don't fail the update if revision creation fails */ }
}
const result = await handleContentUpdate(this.db, collection, resolvedId, {
data: usesDraftRevisions ? undefined : processedData, // -> columns, correctly
});
Publish then has no draft to promote and falls back to the live revision:
// 0.29.0 src/database/repositories/content.ts:1283
let revisionToPublish = existing.draftRevisionId || existing.liveRevisionId;
// ...
// :1299
await this.syncDataColumns(type, id, revision.data); // writes EVERY field back
For a revisionless collection draftRevisionId is always NULL, so publish always restores whatever snapshot liveRevisionId happens to point at — overwriting every update made since.
Suggested fix
When the collection does not support revisions, publish should change status only and leave the data columns alone — the columns are the source of truth for that collection. Concretely: skip the syncDataColumns call (and the _slug sync) unless the promoted revision came from draftRevisionId, or gate the whole promotion on supports.includes("revisions").
An alternative that also closes it: refresh liveRevisionId from the current columns on each update for revisionless collections — though that reintroduces the revision writes the design deliberately avoids there.
Notes
- The silent
catch {} in the update path is a separate hazard: if the revision block throws after usesDraftRevisions = true, the edit is written nowhere while the call still reports success. We did not hit this, but the shape is the same — a write that vanishes with a green exit code.
- We're aware
supports = NULL may be unintended configuration on our side. The report stands regardless: the code tolerates it explicitly (?.includes), and the resulting behaviour is silent data loss rather than an error.
Summary
On a collection whose
supportsdoes not includerevisions,content updatewrites data straight into the content table columns — correct and immediately live. A subsequentcontent publishthen promotes the storedliveRevisionId, which may be arbitrarily old, and syncs every field of it back over the fresh columns.The update is destroyed, and
publishreports{"success": true}with exit code 0.The damage scales with the age of
liveRevisionId. In our case the live revision was six weeks old.Affected: verified in 0.1.0 (our deployment) and 0.29.0 (latest) — the logic is unchanged between them.
Reproduction
Verified end-to-end against a live deployment on 0.1.0:
supports = NULL(or any value withoutrevisions), on an item that has alive_revision_idolder than its current column data.content update <collection> <slug> --rev <rev> --file payload.json— change one field, e.g.title.→ Works. Columns updated, marker present in
content get, in the single-item API and in?status=published.versionbumps, no revision row is created,draft_revision_idstays NULL.content publish <collection> <slug>→
{"success": true},✔ Published, exit 0.live_revision_idunchanged, still zero revision rows for the entry.Measured state around step 3 (title had marker before, clean after;
live_revision_idnever moved):title… W842TEST…(six-week-old value)live_revision_id01KT90EE87RDH41K5096A1MC2ERoot cause
Update correctly skips the revision machinery when the collection lacks
revisions:Publish then has no draft to promote and falls back to the live revision:
For a revisionless collection
draftRevisionIdis always NULL, sopublishalways restores whatever snapshotliveRevisionIdhappens to point at — overwriting every update made since.Suggested fix
When the collection does not support revisions,
publishshould change status only and leave the data columns alone — the columns are the source of truth for that collection. Concretely: skip thesyncDataColumnscall (and the_slugsync) unless the promoted revision came fromdraftRevisionId, or gate the whole promotion onsupports.includes("revisions").An alternative that also closes it: refresh
liveRevisionIdfrom the current columns on each update for revisionless collections — though that reintroduces the revision writes the design deliberately avoids there.Notes
catch {}in the update path is a separate hazard: if the revision block throws afterusesDraftRevisions = true, the edit is written nowhere while the call still reports success. We did not hit this, but the shape is the same — a write that vanishes with a green exit code.supports = NULLmay be unintended configuration on our side. The report stands regardless: the code tolerates it explicitly (?.includes), and the resulting behaviour is silent data loss rather than an error.