diff --git a/InterlinedList/Models/ListDataRow.cs b/InterlinedList/Models/ListDataRow.cs index fd5b2f7..30134b5 100644 --- a/InterlinedList/Models/ListDataRow.cs +++ b/InterlinedList/Models/ListDataRow.cs @@ -2,15 +2,88 @@ namespace InterlinedList.Models; +/// +/// One row of a list's data, from GET /api/lists/{id}/data. +/// +/// +/// +/// Field set reconciled against the live read payload 2026-09-16. Row keys are +/// exactly: id, rowData, version, createdAt, updatedAt, createdByUser, +/// lastEditedByUser. +/// +/// +/// is deliberately NOT required. The read +/// endpoint does not send it — declaring it required made +/// System.Text.Json throw +/// "missing required properties including: 'listId'", so any list +/// holding at least one row failed to load. Watch for the asymmetry that hid +/// this: POST /api/lists/{id}/data does return listId in +/// its {message, data:{…}} envelope; only the read path omits it. The +/// caller already knows which list it asked for. +/// +/// public sealed class ListDataRow { public required string Id { get; init; } - public required string ListId { get; init; } + + /// + /// Owning list. Absent on the read path — see the remarks. Populated when a + /// row comes back from a create/update response. + /// + public string? ListId { get; init; } + public required Dictionary RowData { get; init; } + + /// + /// Monotonic row version, incremented on each edit. + /// + /// + /// + /// This is NOT optimistic concurrency, despite looking like it. An + /// earlier revision of this comment claimed it was; probing disproved that + /// (2026-09-16). Sending a deliberately stale version on + /// PUT /api/lists/{id}/data/{rowId} is accepted: + /// + /// + /// row at version 2, PUT with {"data":{…},"version":1} + /// -> 200, version becomes 3, the write lands + /// + /// + /// So the server does not compare-and-swap on it — row writes are + /// last-writer-wins and a concurrent edit is silently lost. Treat this as a + /// display/audit value only. (Contrast the app-settings store, which DOES + /// do real CAS via baseVersion and returns 409 + /// version_conflict — see #41.) + /// + /// + public int Version { get; init; } + + /// Server-assigned ordinal. Null on a schema-less list. + public int? RowNumber { get; init; } + public DateTimeOffset CreatedAt { get; init; } public DateTimeOffset UpdatedAt { get; init; } - /// Read-only "key: value, key2: value2" preview — good enough since rows are freeform JSON with no schema. + /// Who added the row. Useful on a shared list — see the contributors work (#65). + public ApiUser? CreatedByUser { get; init; } + + /// Who last edited it; null when never edited since creation. + public ApiUser? LastEditedByUser { get; init; } + + /// + /// Read-only "key: value, key2: value2" preview. Adequate while rows are + /// freeform; the typed, schema-driven renderer is #21. + /// + /// + /// Note for anyone writing rows: PUT /api/lists/{id}/data/{rowId} + /// replaces rowData rather than merging it. Verified live — + /// a row holding {a,b} PUT with only {a} came back as + /// {a}, silently dropping b. So an editor must re-send every + /// key it knows about, echoing untouched values. + /// public string DisplaySummary => string.Join(", ", RowData.Select(kv => $"{kv.Key}: {kv.Value}")); + + /// Edited since creation, per . + public bool HasBeenEdited => LastEditedByUser is not null; }