Skip to content

Put entry and entity operations in change sets - #2519

Merged
pournasserian merged 5 commits into
v2from
2515-entry-change-sets
Oct 18, 2026
Merged

pournasserian merged 5 commits into
v2from
2515-entry-change-sets

Conversation

@pournasserian

@pournasserian pournasserian commented Oct 18, 2026 •

Copy link
Copy Markdown
Member

Closes #2515

What

The next F-CNT (#2416) slice: change sets hold operations on entries and entities, and diff, publish and revert them like schema operations (CHG-01, CHG-03, CHG-05, CHG-06). Builds on atomic publish (#2482) and revert (#2511). This is the first thing that moves Entries.PublishedVersion.

  • Item kinds: an entry operation must point at an entry of a plain content type, and an entity operation at an entry of an entity type, in each case of a type the agent's scope allows for the operation (MCP-05). The wrong kind fails with the new item_kind_mismatch. A missing or soft-deleted entry fails with item_not_found, and a missing version with version_not_found. change_set.operation_added carries the base version's row as previous version, as for schema items. Other kinds (media, rules) are still item_kind_unavailable.
  • Diff (ItemDiff.ForEntry in Core): two versions of an entry, field by field, with ValueDiff.Compare. On an entity a field's value is its whole fact, so a new source shows as a change. Fields follow the definition the shown version was written against, so the order is the same on SQLite and PostgreSQL even though jsonb reorders keys. DiffAsync reads every version the entry operations name, with their definitions, in one query.
  • Publish: each entry's published version moves from its operation's base version to the proposed one with a conditional update, alongside schema items in the same unit of work. Stale bases fail with base_version_stale before anything moves, and a soft-deleted entry fails with item_not_found. Events: entry.published / entry.unpublished, or entity.*.
  • Delete means unpublish: a published delete clears the published version and keeps the entry, its draft and its history. Revert of a create unpublishes, and revert of a delete publishes the base version again, through the same publish.
  • One history per entity: EntryDirectory now names entries of entity types entity in its events (entity.created, entity.updated and so on), so an entity's own events and the change set events about it share a subject.

Why

Entries were stored and versioned (#2491) but nothing could put them live. Publishing through change sets keeps CHG-05's all-or-nothing rule for mixed sets of schema, entry and entity items (CHG-09), and revert comes for free from ChangeOperation.Inverse.

Choices worth a look:

  • Separate kinds for entries and entities let the desk and blast radius (ENT-04) tell content changes from fact changes, without a lookup per item.
  • A published delete unpublishes rather than soft-deletes, so its revert only has to move a pointer back. Soft delete stays a direct action with its retention window (Soft delete and restore entries within the retention window #2502). The entry states slice will call an unpublished entry archived.
  • Writes do not join change sets themselves yet. A write stores a draft version and an operation points at it, as content types did before SCH-05. Entry writes will follow the change set target that Add the write tools for change sets and content types #2514 introduced, together with the entry tools.
  • Changes to the F-CHG lane's file are kept in separate methods (ResolveEntryAsync, EntryDiffsAsync, PublishedEntriesAsync, PublishableEntry), so the SCH-05 work merges easily.

Tests

Unit (Core):

  • ItemDiff.ForEntry: changed fields in definition order, with a removed field the type no longer has last; formatting-only differences ignored; facts compared whole; create and delete; refusal of non-entry operations and of fields that do not match the operation's versions.

Integration, each on SQLite and PostgreSQL:

  • An agent cannot add an operation on an entry of a type outside its scope.
  • Entry and entity operations stored with their kinds and events; the wrong kind both ways, a missing entry, a soft-deleted entry and a missing version each fail and write nothing; an update's event carries its base version's row.
  • The diff of an entry update and an entity create.
  • Publishing moves both pointers with one event per item; a resolved view follows the published price, not a new draft; an entity's own and change set events share its history.
  • A stale base and a soft-deleted entry fail the publish and move nothing.
  • A published delete unpublishes and keeps the entry; its revert publishes it again and marks the delete reverted.
  • Reverting an update puts the earlier version back, and its diff shows the change backwards.
  • A failure injected at each audit append of a publish leaves no entry published.

The full suite ran locally against a postgres:17 container as well as SQLite, after merging the latest v2. No migration, no new package and no new project reference.

Checklist

  • Linked to an issue a maintainer has confirmed
  • Targets v2
  • Tests or docs updated where behaviour changed
  • No tool-generated co-author trailers or footers

@pournasserian pournasserian added this to the M1 Governance core milestone Oct 18, 2026
@pournasserian pournasserian added type:feature New capability v2 FluentCMS v2 (v2 branch) area:content labels Oct 18, 2026
@pournasserian
pournasserian merged commit b9bf93e into v2 Oct 18, 2026
2 checks passed
@pournasserian
pournasserian deleted the 2515-entry-change-sets branch October 18, 2026 23:50
pournasserian added a commit that referenced this pull request Oct 19, 2026
Closes #2521

## What

The next F-CNT (#2416) slice: entries have the CNT-01 states (draft, in
review, published, reverted and archived), and only change sets move
them. Builds on entry operations in change sets (#2519).

- **Core** (`FluentCMS.Core.Content`): `EntryState`, carried by `Entry`
and `EntrySummary`. `EntryStates.Review` lists the change set states
that hold an item in review (checks running, checks failed, awaiting
review, approved). `EntryStates.AfterPublish` says what a publish leaves
an entry as: published for a create or update, archived for a delete,
and reverted for any item of a revert.
- **In review is read, not stored**: `GetAsync` and `ListAsync` show an
entry as in review while any set in a review state holds an operation on
it. The query is one subquery on `ChangeSetOperations` and `ChangeSets`,
so two sets reviewing one entry keep it in review until neither does,
and a rejected or reopened set gives it back its earlier state with
nothing to undo.
- **The other states are stored** in `Entries.State` and written only by
publish, in the same conditional update that moves the published
version. A new entry starts as a draft. `IEntryDirectory` has no way to
set a state, and writes, soft deletes and restores leave it alone.
- **Migration** for SQLite and PostgreSQL: `State` defaults to `Draft`,
entries that already have a published version become `Published`, and a
check constraint allows only the four stored states.

## Why

CNT-01 wants states that only governance moves. Storing only what
publish decides, and reading in review from the sets themselves, means
no write path can change a state, and there is no remembered "state
before review" to get wrong when sets overlap.

Choices worth a look:

- **A published entry with a newer draft is still published.** The state
describes what is live; the pending draft shows in `LatestVersion`
against `PublishedVersion`.
- **Checks failed counts as in review**, because the set is still on its
way to a decision: it either goes back to open, which ends the review,
or gets rejected.
- **Every item of a published revert is reverted**, whether the revert
republished it or took it offline, so the desk's reverted view (DSK-02)
can find them.

## Tests

Unit (Core):
- A new entry is a draft; a revision keeps the published version and the
state; `Restore` takes the state.

Integration, each on SQLite and PostgreSQL:
- Draft while its set is open, in review through checks running, checks
failed, awaiting review and approval, draft again when reopened,
published when published; `GetAsync` and `ListAsync` agree at every
step.
- Two sets review a published entry; rejecting one keeps it in review,
rejecting both shows it as published with its pending draft.
- A published delete archives the entry, its revert makes it reverted, a
published update makes it published, and that update's revert makes it
reverted.
- A new draft, a soft delete and a restore leave a published entry
published.
- The migration turns an entry that already had a published version into
published and leaves the other a draft, and the newest migration's
`Down` works on both providers.

The full suite ran locally against a `postgres:17` container as well as
SQLite. No new package and no new project reference.

## Checklist

- [x] Linked to an issue a maintainer has confirmed
- [x] Targets `v2`
- [x] Tests or docs updated where behaviour changed
- [x] No tool-generated co-author trailers or footers
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type:feature New capability v2 FluentCMS v2 (v2 branch)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Put entry and entity operations in change sets: diff, publish and revert

1 participant