Skip to content

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

Closed

Description

@Adron

Problem

PUT /api/lists/{id}/schema has two body shapes with very different consequences:

  • a schema DSL object → destructive: wipes and recreates all columns (and, per the docs, name becomes the list's new title);
  • a properties array → non-destructive: rename a label, add/remove one column, row data preserved.

Shipping a single "Save schema" button that always sends the DSL form would silently destroy users' data.

Acceptance criteria

  • Label edits, single add/remove, and reordering route through the properties shape.
  • The DSL rebuild path is a separate, explicitly-labelled action.
  • The rebuild requires in-place confirmation naming what is lost (no blocking native message box — see the dispatcher rule in CLAUDE.md).
  • The rebuild path notes that schema.name overwrites the list title.
  • Verified on a throwaway test list that the properties path preserves rows.

Activity

  1. added
    parityWeb/API feature-parity work
    P1Table stakes
    on Sep 15, 2026
  2. Adron commented on Sep 16, 2026

    @Adron
    MemberAuthor

    Live-verified findings from the schema service (#17 / PR #143) — three change the design here

    ⚠️ 1. The non-destructive properties form accepts only SIX types, not twelve

    400 Unknown propertyType 'textarea'. Allowed: text, number, boolean, date, url, email
    

    propertyType is also mandatory on every item (omitting it gives the same error for 'undefined'/'null').

    Consequence: a list using textarea, datetime, tel, select, multiselect or priority cannot be edited non-destructively at all — every change to it has to go through the destructive DSL rebuild. That is a real, permanent constraint on #22's guard, not an edge case: the moment a user picks one of the six richer types in #18's builder, they lose safe editing on that list forever.

    Exposed as ListFieldType.PropertiesEditable and ListSchema.SupportsPropertiesEdit so the UI can tell the user before they choose.

    2. "Destructive" means columns, not rows — so #22's confirmation copy must not say "rows will be deleted"

    Confirmed live. The DSL rebuild:

    • drops and recreates every column with a new id, losing any validation / options / visibility the new DSL omits;
    • leaves rows intact, but values whose keys no longer exist are orphaned in rowData — still stored, not shown, not validated.

    And it renames the list: schema.name overwrote the title ('ZZ Throwaway Schema Probe' → 'ZZ Renamed By DSL Rebuild'), and schema.description overwrites the list description too — clearing it when omitted. #22's confirmation needs to say all three things.

    3. Deleting a data-bearing column requires ?force=true

    409 {…, "propertiesWithData": ["link"]}
    

    ?force=true then strips that key from every row. The shared EnsureSuccessAsync discards the propertiesWithData array, so a dedicated ListSchemaException carries Issues / PropertiesWithData / RequiresForce. #18 should name the affected columns in its confirmation rather than showing a bare 409.

    4. The server never validates conditional visibility — #20 must

    An unknown operator, a missing watched key, and a forward reference all return 200 and then silently never match. So there is no server-side safety net: ListSchema.Validate() does these checks client-side, and #20's editor should surface them rather than letting a rule quietly do nothing.

    5. properties PUT definitively preserves row data — #22's guard is sound

    A rename + column-add on a list holding two rows returned 200 and both rows came back with identical ids, identical version (still 1), and identical rowData. Column ids were preserved too, and the validationRules / helpText / visibilityCondition that the properties shape cannot even express survived untouched.

    Note the envelope differs between the two directions: GET returns {"data":{…}}, the properties PUT returns {"properties":[…]} ordered by displayOrder — no data wrapper.

    6. Schema-less lists are unaffected

    Created without a schema they answer {"data":{"name":"<list title>","description":…,"fields":[]}} and accept rows with arbitrary, even nested, keys. Unknown keys are accepted on a schema'd list too — nothing is stripped. fields: [] cannot be PUT back (DSL must have at least one field), so ListSchema.Validate() catches that locally.

    Also relevant to #21

    Row-write validation returns 422 with details:[{field,message}], which the current plumbing flattens — so #21 can't currently attach a message to the offending field without unpacking that itself.

    Separately, #144/PR #146 fixed a crash on the row-read path (ListDataRow.ListId was required but absent) and modelled version, which is the hook for optimistic concurrency on row edits.

  3. Adron commented on Sep 16, 2026

    @Adron
    MemberAuthor

    Implemented in #164 — work continues in the PR from here. "Save columns" now routes renames, adds, single removals and reordering through the non-destructive properties shape and states its plan before running; the DSL rebuild is a separate "Rebuild columns…" action behind an in-place confirmation (no native dialog) that names all of it: new column ids, rows KEPT with orphaned values for dropped keys, the title overwritten from an editable box, and the description overwritten — cleared if that box is blank.

    Verified live on throwaway lists with the bodies the real draft view-model serializes: the safe path answered 409 propertiesWithData ["link"], then 200 with ?force=true, leaving both rows with identical ids and the list's title/description untouched; the rebuild returned 200, renamed the list, kept both rows, and returned a null description when the DSL omitted it.

    Worth recording because it is easy to get backwards: removing a column via properties ?force=true DELETES that value from every row, while a rebuild KEEPS it as an orphan — which is why the two confirmations are worded differently and the rebuild copy never says rows are deleted.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1Table stakesarea:listsArea: listsparityWeb/API feature-parity work

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions