|
| 1 | +--- |
| 2 | +"@objectstack/spec": minor |
| 3 | +"@objectstack/objectql": minor |
| 4 | +"@objectstack/service-automation": patch |
| 5 | +--- |
| 6 | + |
| 7 | +feat(spec,objectql): `DroppedFieldsEvent.reason` names the dispatch-ruled id strip (#6437) |
| 8 | + |
| 9 | +The write path's strip-observability seam declared a narrower vocabulary than |
| 10 | +the strips it reports on. `DroppedFieldsEvent.reason` was a closed enum over the |
| 11 | +two READ-ONLY strips (`readonly` #2948 / `readonly_when` #3042), so the |
| 12 | +primary-key strip added by #6262 / PR #6433 (multi branch) and #6435 (by-id |
| 13 | +branch) — a `data.id` the update dispatch has ALREADY RULED is not a primary |
| 14 | +key, removed from the SET payload before it can overwrite the targeted rows' |
| 15 | +identity — was invisible to `onFieldsDropped` and to `strictReadonlyWrites`. |
| 16 | +Both PRs were right to refuse the alternative: force-fitting `readonly` would |
| 17 | +make `reason` lie, which is worse than silence. This adds the value instead. |
| 18 | + |
| 19 | +**New reason: `primary_key`.** It names the FIELD's role, not the offending |
| 20 | +value's shape, so it stays true if the strip ever widens to the same-value |
| 21 | +truthy-scalar no-op the engine deliberately leaves alone today — |
| 22 | +`not_a_primary_key` would describe the value and become false that day. The |
| 23 | +house rule it follows is #5503's, applied in the other direction: a new arm is |
| 24 | +warranted exactly when no existing arm is truthful. #5503 reported the |
| 25 | +implicitly-readonly runtime-owned strip as plain `readonly` because that *was* |
| 26 | +true of it; `readonly` is not true of an `id` (a truthy scalar `id` writes |
| 27 | +fine), so this one gets its own value. |
| 28 | + |
| 29 | +**⚠️ Behaviour change, deliberate and measured: `strictReadonlyWrites` gains a |
| 30 | +new refusal.** The option's contract says it covers "every drop |
| 31 | +`onFieldsDropped` reports" — coverage DERIVED from the reported set, never an |
| 32 | +enumeration frozen at #5126, and confirmed by reading `reportDroppedFields` on |
| 33 | +`main`, whose `strictDrops.push` applies no reason-class filter. So reporting a |
| 34 | +new reason necessarily refuses it. A caller that passes |
| 35 | +`strictReadonlyWrites: true` **and** puts a ruled-non-key value in `data.id` now |
| 36 | +gets `ERR_READONLY_FIELD_REJECTED` where it previously got a success whose `id` |
| 37 | +had been silently dropped. That is the option's whole promise ("don't |
| 38 | +half-apply my payload") reaching one more strip class, and it is the outcome the |
| 39 | +flag's own doc now states. Nothing else moves: default-mode callers still get a |
| 40 | +successful write plus an event, the strip itself is unchanged, and |
| 41 | +`strictReadonlyWrites` is in-process only (`WriteObservabilityOptions`), so no |
| 42 | +REST/wire caller can reach either behaviour. |
| 43 | + |
| 44 | +**The refusal error no longer describes every rejection as read-only.** |
| 45 | +`ReadonlyFieldRejectedError` composed one sentence ("… are read-only and would |
| 46 | +have been stripped", remedied by `{ context: { isSystem: true } }`) that is |
| 47 | +false for a `primary_key` drop — `isSystem` does not exempt that strip. The |
| 48 | +message is now built from the `drops` breakdown the error already carried, so it |
| 49 | +names each reason against its own fields and offers the right remedy. The |
| 50 | +**read-only-only message is byte-identical** to #5126's / #5503's text (pinned |
| 51 | +directly), the error `code` is unchanged, and adding a reason deliberately does |
| 52 | +not add an error code: callers catch one code and read `drops`. |
| 53 | + |
| 54 | +Consumers that branch on `reason` were swept. `service-automation`'s flow-step |
| 55 | +warning map is a `Record<DroppedFieldsEvent['reason'], string>`, so tsc demanded |
| 56 | +the new wording — the loud shape, kept that way on purpose. The protocol |
| 57 | +responses that carry `droppedFields` (`api/batch.zod.ts`, `api/protocol.zod.ts` |
| 58 | +×3, plus the cross-object batch extension) all derive from |
| 59 | +`DroppedFieldsEventSchema` and widen transitively; REST's |
| 60 | +`X-ObjectStack-Dropped-Fields` header is generic over the reason and needed no |
| 61 | +change. One consumer does NOT widen safely and is filed rather than fixed here: |
| 62 | +objectui's `writeWarningToast` picks its wording with a binary ternary whose |
| 63 | +`else` arm would announce a stripped `id` as "Read-only" (objectui#3935). |
0 commit comments