Skip to content

Lists: guard the destructive DSL rebuild vs the non-destructive properties edit - #164

Merged
Adron merged 2 commits into
mainfrom
issue-22-destructive-guard
Sep 24, 2026
Merged

Adron merged 2 commits into
mainfrom
issue-22-destructive-guard

Conversation

@Adron

@Adron Adron commented Sep 16, 2026

Copy link
Copy Markdown
Member

Stacked PR — 3 of 3

issue-22-destructive-guard → base issue-21-typed-row-editor (PR #163) → issue-18-column-builder (PR #161) → issue-17-list-schema-service (PR #143). Merge bottom-up: #143, #161, #163, then this.

What this adds

PUT /api/lists/{id}/schema is one route with two bodies whose consequences are nothing alike, so the column editor offers two clearly different actions — never one “Save schema” that quietly picks the destructive one:

Save columns → { properties: [ … ] }
Renames, adds, single removals and reordering, applied in place: column ids kept, row data kept, the list's own title and description untouched. It states its plan first — “Save columns adds 1, renames 1, deletes link, reorders the columns — in place, with every row's data kept.”

Rebuild columns… → { schema: { … } }
A separate, explicitly-labelled action behind an in-place confirmation (no native message box; nothing blocks the dispatcher) that names exactly what it costs:

  • all N columns dropped and recreated with new ids;
  • rows are NOT deleted — values whose key no longer has a column stay in each row, unshown and unvalidated, with the affected keys named;
  • validation rules and visibility conditions survive only because the drafts re-send what they read — said plainly, including that anything added on the web since the editor opened does not;
  • the list's title is overwritten, from an editable box pre-filled with the current title;
  • the list's description is overwritten, and cleared if the box is blank.

When a draft can only go through the rebuild (any of the six richer types), the safe save refuses and points at the rebuild button by name instead of dead-ending.

Live evidence (probed today, 2026-09-16)

Driven with the bodies the real draft view-model serializes — a throwaway net10.0 console project <Compile Include>-ing the model and draft files by absolute path read the live list's data.properties[] and …/schema, built the drafts through ListColumnDraftViewModel.FromField, applied a rename + an add + a drop + a reorder, and emitted both bodies. Lists were titled ZZ claude-probe … and deleted; a closing GET /api/lists shows only the account's pre-existing New list.

safe properties destructive schema
rows kept, ids identical kept, ids identical
dropped column's values stripped from every row (409 + propertiesWithData: ["link"] without ?force=true; 200 with it) left in rowData as orphans
column ids preserved all new
list title untouched overwritten from schema.name
list description untouched overwritten — null when omitted

That asymmetry in the middle row is the one that is easy to get backwards, and it is why the two confirmations are worded differently: properties ?force=true deletes the value, a rebuild keeps it as an orphan. The rebuild confirmation deliberately never says “rows will be deleted”.

Verified in the same run: the safe save's 409 → confirm → ?force=true cycle; the title/description overwrite on rebuild; the description coming back null from a rebuild that omits it; and ListSchema.Validate() clean on the generated DSL.

Left unverified

Closes #22

🤖 Generated with Claude Code

…edit

PUT /api/lists/{id}/schema is one route with two bodies whose consequences are
nothing alike, so the column editor now offers two clearly different actions
instead of one "Save schema" that would silently pick the destructive one:

* **Save columns** → `{ properties: [ … ] }`. Renames, adds, single removals and
  reordering, applied in place: column ids kept, row data kept, and the list's
  own title and description untouched. It also prints what it is about to do
  ("Save columns adds 1, renames 1, deletes link, reorders the columns — in
  place, with every row's data kept") before it runs.
* **Rebuild columns…** → `{ schema: { … } }`. A separate, explicitly-labelled
  action behind an in-place confirmation (no native dialog, nothing blocking the
  dispatcher) that names exactly what is lost:
  - every column dropped and recreated with a new id;
  - rows are **NOT deleted** — values whose key no longer has a column stay in
    each row, unshown and unvalidated, and the affected keys are named;
  - validation rules and visibility conditions survive only because the drafts
    re-send what they read, which the copy says plainly;
  - the list's **title** is overwritten, from an editable box pre-filled with the
    current one;
  - the list's **description** is overwritten, and CLEARED if the box is blank.

Verified live today on throwaway lists (created, exercised, deleted — the account
is back to its one real list), driving the bodies the real draft view-model
serializes:

* Safe path with a rename + an added column + a reorder + dropping a data-bearing
  column: 409 `propertiesWithData: ["link"]`, then `?force=true` → 200. Both rows
  came back with identical ids, the dropped key stripped from each, and the
  list's title and description untouched.
* Destructive path on the same list: 200, title became the name in the DSL, the
  description survived when sent and came back null when omitted, and both rows
  were still there with their ids.

That last asymmetry is why the two confirmations are worded differently, and it
is easy to get backwards: removing a column through `properties ?force=true`
DELETES that value from every row, while a rebuild KEEPS it as an orphan.

Closes #22

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Adron
Adron merged commit 00b9a8a into main Sep 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Schema: guard the destructive DSL rebuild vs the non-destructive properties edit

1 participant