Repository navigation
Commit 7532ea3
feat(spec,objectql): report a caller value a stored expression replaced, as DroppedFieldsEvent reason stored_expression (#24778)
Closes #24690
Clause-②: yes (widening)
## What
PR #24673 (`c0803b04d2`) made the engine compute a stored
(non-`formula`) field's declared `expression` on every write, so a value
the caller sends for such a field never stands. Before this PR nothing
said so. `onFieldsDropped` stayed silent, so the PATCH and `/batch`
doors answered with no `droppedFields`, and a flow's `update_record`
reported success over a row that holds the computed value.
Scope follows the maintainer's ruling 「甲」 (#6015 comment 6292005854,
applied on decision card #24854): in 18.x only the loud half ships. That
half is the new `reason` value, its emit, and its report. The
`strictReadonlyWrites` refusal is runtime behaviour that no conversion
can carry, so it waits for 19.0.
- **spec** (`packages/spec/src/data/data-engine.zod.ts`):
`DroppedFieldsEventSchema.reason` gains `stored_expression`, with its
describe. The field IS written, with the computed value in place of the
caller's, so `computed` (a `formula` with no column, whose key is
dropped) would be untrue of it. The name follows the existing arms,
which name the field's role (`primary_key`, `computed`). The `fields`
describe and the schema describe now also cover a field written with the
computed value. In `contracts/data-engine.ts`, the
`strictReadonlyWrites` TSDoc names `stored_expression` as the one arm
outside its coverage in 18.x.
- **objectql** (`engine.ts`): `replacedStoredExpressionFields` reports a
field when three things hold: the caller sent it, no other strip took
it, and the value stored differs from the caller's. It runs on insert
(single, batch, and `insertMany`'s per-row `droppedFields`), on update
by id and on predicate update, in every context, `isSystem` included. A
client that sends the computed value (the console grid's consistent
save) or no value is told nothing. A value a static `readonly` strip
already took is reported once, as `readonly`.
- **strictReadonlyWrites is unchanged.** The update path sends the
report through a listener-only channel (`notifyDrop`). On insert, the
strict check judges only the strips' own drops. Both strict checks sit
exactly where they sit on `main`, before the computation. So a strict
write whose only drop is a `stored_expression` replacement completes
with the computed value and is reported to `onFieldsDropped`. A strict
write that also carries a refusable drop is refused for that drop alone,
writes nothing, and fires no listener. `readonly-strict-errors.ts` gains
only the `REASON_PHRASE` row its exhaustive key type requires. No
refusal carries that row in 18.x.
- **service-automation** (`crud-nodes.ts`): `DROPPED_REASON_LABEL` is an
exhaustive `Record` over the reason union, so it needs the new row. For
this reason the step warning says the requested value(s) were NOT
stored, and that the write succeeded with the computed value(s). The
other reasons keep their exact text.
- **Generated**: `content/docs/references/**` regenerated with
`check:generated --fix`.
- **Changeset**: `Clause-②: yes (widening)`. `@objectstack/spec` and
`@objectstack/objectql` are `minor`, `@objectstack/service-automation`
is `patch`. Not breaking. It records that the strict refusal is deferred
to 19.0 per 「甲」.
## Landing order: the objectui pin must move first
objectui's `STRIPPED_LINE`
(`packages/app-shell/src/providers/writeWarningToast.ts`) is a `Record`
keyed by `DroppedFieldsEvent['reason']`. `app-shell` builds with `tsc`,
and `scripts/build-console.sh` installs this tree's `@objectstack/*`
into the pinned objectui. The pin on `main` today is `616df6598b`. It
does not contain objectui PR 13233 (objectstack-ai/objectui#13178),
which adds the `stored_expression` row ahead of the spec pin: merge
commit `8803ca54ac269643101f98dd0cc24ab25feb55a0`, which is 36 commits
ahead of `616df6598b` and is on objectui `main`. Landing this PR at
today's pin would turn the full-battery `Console Pin Gate` red.
The pin is not bumped here. `docs/releases-maintenance.md` says the pin
moves by hand, in its own PR, with `scripts/bump-objectui.sh`, to a
revision chosen in an objectstack issue. **This PR must not land before
that pin PR does.** The pin PR must:
1. Run `scripts/bump-objectui.sh
8803ca54ac269643101f98dd0cc24ab25feb55a0` (or a later objectui `main`
commit). It writes `.objectui-sha` and the `@objectstack/console`
changeset for `616df6598b..NEW`.
2. Regenerate the committed manifest in the same PR: `pnpm
objectui:build`, then `node scripts/gen-sdui-manifest-node.mjs`. That
rewrites `sdui.manifest.json` and `scripts/sdui-manifest.record.json`,
and `scripts/check-sdui-manifest.mjs` must be green.
3. Land before this PR. The objectui row is keyed by the spec union plus
`stored_expression`, so the pinned objectui compiles against `main`'s
spec both before and after this PR.
## Tests (on the head named in the latest report)
-
`packages/objectql/src/engine-stored-expression-dropped-fields.test.ts`
covers by-id update (the PATCH door and each by-id `/batch` op), insert
single, batch and `insertMany`, and predicate update. It has controls
for an equal value and an unsent value, and checks the single report
when the `readonly` strip took the value. Under `strictReadonlyWrites`
it checks three things. By id, on insert and on a predicate update, the
write completes and is reported. A write that also has a `readonly_when`
drop is refused for that drop alone, writes nothing and fires no
listener. An insert with a refusable drop is refused before the
computation, carrying no `stored_expression` drop.
- `packages/rest/src/rest-stored-expression-dropped-fields.test.ts` runs
through the composed stack (`RestServer`, then the protocol, then
`ObjectQL`, then a real `SqlDriver` on better-sqlite3). PATCH answers
`droppedFields` and the `X-ObjectStack-Dropped-Fields` header. `/batch`
tags the op index. Each has a consistent-value control.
- `crud-dropped-fields.test.ts` (service-automation) pins the new
warning. `data-engine.test.ts` (spec) pins the five-value enum.
- Two ablations, one-time and with no test file left behind, via
`scripts/ablation-replace.mjs`. The tool restored and verified the file
each time (blob equal to HEAD, `git diff HEAD` empty).
- The reporter's guard was forced to `return []`, and 12 of 30 objectql
tests failed (first round).
- The by-id `notifyDrop` was routed back through the strict-or-listener
`emitDrop`, and the strict by-id report test failed (1 of 17).
- Reverse type check (first round): with the `stored_expression` row
commented out of `DROPPED_REASON_LABEL`, service-automation `tsc` failed
with TS2741.
## Acceptance notes
- `Object.is` is the comparison, as in the compute passes, over caller
values normalized before the snapshot. Take a stored expression whose
computed value has a different JS type from an equal value the caller
sends, such as a date expression computing a `Date` against an ISO
string: it would be reported. This was not measured.
- The `ImportRowResult.droppedFields` (`api/export`), `/batch` and
protocol `droppedFields` describes still say "caller-supplied fields the
engine LEGALLY strips". They carry the fifth reason through the shared
enum, and their prose was left alone because it is outside this card's
file surface.
- The 19.0 half, refusing a `stored_expression` replacement under
`strictReadonlyWrites`, needs its own card, filed by the seat.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 4b8f597 commit 7532ea3
15 files changed
Lines changed: 729 additions & 84 deletions
File tree
- .changeset
- content/docs/references
- api
- data
- packages
- objectql/src
- rest/src
- services/service-automation/src/builtin
- spec/src
- contracts
- data
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
68 | 68 | | |
69 | 69 | | |
70 | 70 | | |
71 | | - | |
| 71 | + | |
72 | 72 | | |
73 | 73 | | |
74 | 74 | | |
| |||
86 | 86 | | |
87 | 87 | | |
88 | 88 | | |
89 | | - | |
| 89 | + | |
90 | 90 | | |
91 | 91 | | |
92 | 92 | | |
93 | 93 | | |
94 | | - | |
95 | | - | |
| 94 | + | |
| 95 | + | |
96 | 96 | | |
97 | 97 | | |
98 | 98 | | |
| |||
213 | 213 | | |
214 | 214 | | |
215 | 215 | | |
216 | | - | |
| 216 | + | |
217 | 217 | | |
218 | 218 | | |
219 | 219 | | |
| |||
227 | 227 | | |
228 | 228 | | |
229 | 229 | | |
230 | | - | |
231 | | - | |
| 230 | + | |
| 231 | + | |
232 | 232 | | |
233 | 233 | | |
234 | 234 | | |
| |||
276 | 276 | | |
277 | 277 | | |
278 | 278 | | |
279 | | - | |
| 279 | + | |
280 | 280 | | |
281 | 281 | | |
282 | 282 | | |
| |||
285 | 285 | | |
286 | 286 | | |
287 | 287 | | |
288 | | - | |
289 | | - | |
| 288 | + | |
| 289 | + | |
290 | 290 | | |
291 | 291 | | |
292 | 292 | | |
| |||
0 commit comments