A field you write is never read back — $select is an allowlist and a ?? fallback hides the gap
Adding a column to your WRITE path does nothing until the same column is added to every READ $select. Items come back without the property, your code sees undefined, and a snapshot ?? liveValue fallback silently computes from the live value — the exact thing the snapshot was created to prevent. Nothing throws, nothing logs; the feature just quietly does not exist.
Přidat sloupec do ZÁPISU je k ničemu, dokud ho nepřidáš i do všech čtecích $select. Položky se vrací bez té vlastnosti, kód vidí undefined a fallback snímek ?? živá hodnota tiše počítá ze živé hodnoty – přesně z té, před kterou měl snímek chránit. Nic nespadne, nic se nezaloguje; funkce prostě potichu neexistuje.
Symptom
A "snapshot" column is introduced so that historical calculations stay stable — say, the monthly allowance valid for a closed period, written by the month-close routine onto that month's record. The write works; you can see the value in the list. Yet every screen keeps recalculating history from the current contract value, as if the snapshot did not exist. No errors anywhere.
Cause
Two well-behaved features compose into silence:
$selectis an allowlist. A REST read likeGET …/items?$select=Id,Title,Period,BudgetHoursreturns objects that simply lack every unlisted property. Reading
item.AllowanceSnapshotyieldsundefined— indistinguishable from "the routine never wrote a snapshot".The fallback is a legal branch. The consuming code is deliberately tolerant:
const allowance = item.AllowanceSnapshot != null ? item.AllowanceSnapshot : contract.AllowanceHours;With the property missing, this always takes the live branch. That is correct behavior for genuinely old rows — which is why nothing ever flags the missing column.
The bug survives code review because the write path, the read path and the fallback each look right in isolation.
Rule
- The commit that adds a column to a write must add it to every read
$selectin the same change. Grep the data layer for the field name before merging — one hit (the write) means you are not done. - Prove the snapshot wins: set snapshot ≠ live value on one row and check which number the UI shows. A fallback you have never seen not taken is untested code.
- Compare with
!= null, not truthiness —0is a valid snapshot (a period with a zero allowance) and must not fall through to the live value. - If the field is provisioned asynchronously on older sites, keep the field in the extended
$selectwith a 400-fallback to a narrower select, so old sites degrade instead of failing (seeprovisioning-skips-schema-changes-to-existing-fields.md).