Skip to content

Commit 492b416

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-5222-field-ref-pushdown
2 parents 152c690 + 97b6658 commit 492b416

35 files changed

Lines changed: 2830 additions & 113 deletions

.changeset/console-6314e87f2d49.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
---
2+
"@objectstack/console": minor
3+
---
4+
5+
Console (objectui) refreshed to `6314e87f2d49`. Frontend changes in this range:
6+
7+
Derived from the changesets objectui declared over the range — 27 releasing of 30 changesets added across 43 non-merge commits; omitted: 3 release-nothing changesets, 14 commits carrying no changeset (they ship no package code).
8+
9+
- **minor****BREAKING** — Retire the `global_nav` Studio designer surfaces, and track the `@objectstack` family at `17.0.0-rc.6` (objectstack#7100 / objectstack#6888). (objectui `38ab5054f`)
10+
- **minor** — One fullscreen long-text editor, hoisted to the package both render paths may import (objectui `cb1340058`)
11+
- **minor** — i18n: retire the orphaned `report.editor.*` namespace — 105 of its 106 keys, in all ten locale packs (~1050 translated strings) (objectui `fa511094a`)
12+
- **minor**`/accept-invitation/:invitationId` is one route, one component, one namespace — the console now renders the invitation page that actually shows you the invitation (objectui `0e67b53ff`)
13+
- **patch** — Inline-editing an `address` on the record detail page now edits it as real sub-fields, instead of collapsing it to one text box reading `[Object]` and saving a string over the str… (objectui `6314e87f2`)
14+
- **patch** — An image field's declared `maxSize` is enforced before the upload starts, not after it finishes (objectui `433ff9fd3`)
15+
- **patch** — A `Field.address` value now reads as a formatted postal address on the record detail page, instead of stringified JSON. (objectui `e2e6360c2`)
16+
- **patch** — Renaming a freshly-created view now persists — `updateView` reads and writes the same row, instead of reading the published overlay and losing the edit into a rejected partial wri… (objectui `b42558a4c`)
17+
- **patch** — Using a list's filter panel no longer overwrites the view's source-declared `filter` for everyone (objectui `f8595a054`)
18+
- **patch** — A list emptied by the view's own filter says "no records match", instead of inviting you to create your first record (objectui `f8595a054`)
19+
- **patch** — An illegal gantt dependency link now says why it was refused, instead of doing nothing (objectui `e1ade8f03`)
20+
- **patch** — The gantt's conflict dialog shows the number of affected tasks again, not a literal `{2}` (objectui `828549a9a`)
21+
- **patch** — An action rendered in the overflow menu, as an icon or inside a group now reaches the runner carrying the same authored keys as the same action rendered inline — `action:menu`, `a… (objectui `d6e5124a3`)
22+
- **patch** — A rejected Kanban drag rolls the card back on both data ownerships, not just when the board owns its own records (objectui `2c8ad7cdb`)
23+
- **patch** — ObjectGrid's bulk-bar **Clear** now unticks the row checkboxes, instead of only removing the toolbar (objectui `51ab34e34`)
24+
- **patch** — Conditional required (`requiredWhen`) now decides at SUBMIT time too — the star and the validator can no longer disagree (objectui `b1e42d09b`)
25+
- **patch** — fix(app-shell): the top-bar bell polls the inbox on every console surface, not only inside an app (#4110) (objectui `7b0783232`)
26+
- **patch** — An `autoTrigger` action that spills past `action:bar`'s `maxVisible` now still runs — `action:menu` consumes the flag instead of dropping it. (objectui `debad2796`)
27+
- **patch** — The first-run setup wizard no longer drops a brand-new owner outside the console (objectui `1f34b3825`)
28+
- **patch** — The AI build conversation no longer blanks itself the moment the preview opens (objectui `e16fd9597`)
29+
- **patch**`features.passkeys` and `features.magicLink` are documented as reserved, so enabling them no longer implies a login-page entry point that does not exist (objectui `564252cd8`)
30+
- **patch**`/setup` is a real address again — the console gets a stable deep link into platform administration instead of bouncing you back to home (objectui `b3f665b49`)
31+
- **patch**`?runAction=create_environment` is no longer consumed when the environments toolbar has no create action to run it on. (objectui `bf2fd3d1f`)
32+
- **patch** — The build-history panel tells an operator a 503 means "the commit store could not be reached — retry", instead of `commits HTTP 503` (objectui `f7c6430ec`)
33+
- **patch** — Stop the report config panel being titled "Title", and the view-settings colour section "Color" (objectui `ff84b0523`)
34+
- **patch** — Fail when a `t()` call site's arguments are not the holes its `en` value has, and delete the three that were inert (objectui `5f40de7d4`)
35+
- **patch**`DatasetWidget`'s option-color / dimension-label probe now rides the host's authenticated fetch (`SchemaRendererContext.apiFetch`) instead of the bare global `fetch`. (objectui `ee7a68d2d`)
36+
37+
⚠️ 1 of these carries a breaking change: 1 by the author's own breaking annotation in the changeset body — objectui declares no `major` inside a launch window (`scripts/check-changeset-no-major.mjs`). Each is marked **BREAKING** in the list above — read them before compiling the release record.
38+
39+
**In this console build, declared nowhere** — objectui merged 14 commits in this range with no `.changeset/*.md`. The code is inside the pin above and ships here, but nothing upstream declared them, so they appear in no objectui CHANGELOG and in no entry above. Listed by subject rather than counted, because a count cannot tell a dependency bump from a form-behaviour change (objectstack#6174); the upstream gate that would prevent this is objectui#3387.
40+
41+
- _(no changeset)_ fix(editor,markdown): complete the vite alias tables so the per-package test task resolves (#4218) (objectui `a9a67ec5b`)
42+
- _(no changeset)_ ci(turbo): derive the `lint` and `build` inputs guards from each package's real program (#4184, #4185) (#4200) (objectui `a5b1b8917`)
43+
- _(no changeset)_ ci(turbo): derive the test inputs guard from each package's Vitest config program (#4178) (#4188) (objectui `2b9428338`)
44+
- _(no changeset)_ ci(turbo): derive the type-check inputs guard from each package's tsc program (#3514) (#4176) (objectui `eb5f8cea0`)
45+
- _(no changeset)_ docs(ci): pin the 'can never be required, structurally' bullet to the YAML it quotes (#4170) (#4175) (objectui `c27c8981f`)
46+
- _(no changeset)_ ci(shadcn): close the three declared alarm-channel gaps (#3586) (#4174) (objectui `e63853173`)
47+
- _(no changeset)_ docs(links): scan the app READMEs and the rest of the repo root (#4148) (#4173) (objectui `da8109300`)
48+
- _(no changeset)_ chore(site): ignore the AGENTS.md/CLAUDE.md that `next dev` mints, and turn the minting off (#4172) (objectui `0ead48368`)
49+
- _(no changeset)_ docs(ci): stop the Merge Queue section keeping its own copy of the subscriber list (#4154) (#4171) (objectui `6eb40b8d7`)
50+
- _(no changeset)_ docs: repair QUICK_REFERENCE's dead commands and layout claims, and pin them (#4149) (#4159) (objectui `521a37bd0`)
51+
- _(no changeset)_ docs(ci): drop the fourth hand-copy of the object-ui ratchet list, and gate the page (#3782) (#4153) (objectui `492223d9a`)
52+
- _(no changeset)_ docs: repair and pin QUICK_REFERENCE's Current Release block, drop the console README's hand-written versions (#4143) (#4150) (objectui `43b2e4565`)
53+
- _(no changeset)_ docs: drop the hardcoded package versions from the utilities pages and correct the data-objectstack README install line (#4125, #4130) (#4144) (objectui `d86d372ad`)
54+
- _(no changeset)_ docs(console-starter): correct the 'Without a backend' root-route paragraph (#4102) (#4142) (objectui `148ade326`)
55+
56+
<!-- adr-0087: not-required (already-registered action-global-nav-location-removed) The one breaking entry in this range is objectui#4169, which drops the Studio designer surfaces for the `global_nav` action location. The spec-side retirement of that enum value landed in this repo with objectstack#7100 / #6888 and is already on the ledger as the conversion `action-global-nav-location-removed` (packages/spec/src/conversions/registry.ts) plus its protocol-17 semantic entry; this pin bump ships the console catching up to that decision and changes no spec surface of its own, so it registers nothing new. -->
57+
58+
objectui range: `92c0b1f403f7...6314e87f2d49`
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
---
2+
"@objectstack/runtime": patch
3+
---
4+
5+
fix(runtime): stop the dispatcher answering a permission denial's internal authorization payload (#7450)
6+
7+
`plugin-security`'s object gate attaches
8+
`{ operation, object, positions, permissionSets }` to every
9+
`PermissionDeniedError`. The two HTTP transports disagreed about what to do with
10+
it. `@objectstack/rest`'s `mapDataError` never reads `error.details` — its 403
11+
body is `{ error, code, object? }`, and the `object` on it is the one the ROUTE
12+
named. The dispatcher's `dispatch()` catch spread the whole payload
13+
(`{ code: 'PERMISSION_DENIED', ...(e.details ?? {}) }`), and `buildApiError`
14+
puts everything that is not the `code` on the wire as `error.details`.
15+
16+
Per the maintainer's 2026-08-11 ruling the two transports now agree on REST's
17+
shape: **message + code + the route-derived object**. `positions` and
18+
`permissionSets` are server-side diagnostics and are no longer serialized; the
19+
full withheld payload is written to a server log line instead, so a false
20+
denial is still diagnosable.
21+
22+
**The object is derived from the request path, not from `error.details`.** This
23+
is the part an "allowlist `operation` + `object`" reading gets wrong.
24+
`ObjectQL.cascadeDeleteRelations` re-enters `delete()` for every child of the
25+
row being deleted, so a child's own trip through the security middleware throws
26+
with `details.object` set to the CHILD. Forwarding that field would have reached
27+
the ruled field set and still answered the API name of an object the caller
28+
never addressed. The dispatcher now reads no field of `error.details` at all:
29+
`object` comes from `cleanPath`, exactly as REST takes `req.params.object`, and
30+
a denial on a route whose path names no object carries no `object` — REST's
31+
`...(object ? { object } : {})` behaviour.
32+
33+
**Also fixed, and required for the above to have any effect on the wire.** The
34+
domain-registry branch of `dispatch()` returned its handler's promise without
35+
awaiting it. In an async function a bare `return <promise>` settles outside the
36+
enclosing `try`, so a domain handler's rejection never reached that method's
37+
`catch` — and every domain that can raise an object-gate denial (`/data` among
38+
them) resolves through that branch, which made the `PERMISSION_DENIED` branch
39+
unreachable in practice. Denials escaped to the Hono catch-all instead, which
40+
answered `{ error: { message, code: 403 } }`: a numeric `code`, the shape
41+
`error-envelope.ts` exists to prevent, with no `PERMISSION_DENIED` string for a
42+
client to branch on. The branch now awaits, so a `/data` denial answers the
43+
ruled envelope. Non-denial errors are unaffected — the catch rethrows them and
44+
they reach the adapter exactly as before.
45+
46+
**Wire-visible.** A consumer reading `error.details.positions`,
47+
`error.details.permissionSets` or `error.details.operation` off a dispatcher 403
48+
no longer receives them, and `error.details.object` is now the object the
49+
request addressed rather than whichever object the gate refused. A `/data`
50+
denial's `error.code` is now the string `PERMISSION_DENIED` rather than the
51+
number `403`.
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
---
2+
"@objectstack/driver-mongodb": patch
3+
---
4+
5+
fix(driver-mongodb): refuse malformed `$between`, undeclared node-level `$`-keys and `{ field: {} }` (#5346, #5376)
6+
7+
`driver-mongodb` was the last backend still ANSWERING three filter shapes every
8+
other backend refuses. All three failed the same way — the query ran, reported
9+
nothing, and returned a row set nobody asked for. Measured through
10+
`translateFilter` (a pure function whose output *is* the document MongoDB
11+
receives):
12+
13+
```
14+
{ score: { $between: 5 } } => {"score":{}}
15+
{ $where: 'return true' } => {"$where":"return true"}
16+
{ stage: {} } => {"stage":{}}
17+
```
18+
19+
All three now refuse with `INVALID_FILTER` / 400 (ADR-0112), naming the position
20+
(`filter.$or[1].score.$between`), through the same `unsupportedFilterError`
21+
constructor this package's other filter refusals already used — no new envelope.
22+
23+
- **Malformed `$between`** — the emitter arm wrote both bounds inside
24+
`if (Array.isArray(value) && value.length === 2)` and had no `else`, so a
25+
malformed comparand dropped the whole range and normalised the field to `{}`.
26+
The twin, down to the missing `else`, of the arm #5328 fixed on
27+
`driver-memory`. The leading sentence is `driver-sql`'s verbatim — one
28+
condition, one wording (#5240).
29+
30+
- **An undeclared `$`-key in a NODE position** — the severe one. The translator's
31+
switch knows three combinators (`$and` / `$or` / `$not`); every other key took
32+
the FIELD path, and a key carrying no `$`-prefixed sub-keys fell to implicit
33+
equality and was written into the outgoing document verbatim, where **MongoDB
34+
executed it**. `$where` is server-side JavaScript; `$nor` is a real combinator
35+
the Filter Protocol never declared. The emitter's field-level `default:` arm
36+
has named exactly these spellings as its P0 reason for refusing them one level
37+
down for two releases — that gate was only ever installed at the field
38+
position. On the other backends the same input compiled to a column name and
39+
returned zero rows (#5348 / cloud#1077, since refused) or was already refused
40+
(#5324); only here was it evaluated.
41+
42+
- **`{ field: {} }`** — a field constrained by zero operators, ruled REFUSE on
43+
#5240 and gated on `driver-sql` / `driver-sqlite-wasm` / `driver-memory` /
44+
`formula` by #5327. This driver translated it to `{ field: {} }`, which MongoDB
45+
reads as "the field is deep-equal to the empty document" — not the FALSE the
46+
ruling declined to take, but a DIFFERENT filter that merely looks like FALSE
47+
until a document actually stores `{}` there.
48+
49+
Each gate sits on the validating walk (`classifyFilterKey`), beside the existing
50+
`$null` (#5347) and `$icontains` (#6520) gates, rather than in the emitter — the
51+
emitter is skipped wholesale when a boolean identity settles the enclosing node,
52+
so a gate there would fire or not depending on a shape's SIBLINGS. Measured
53+
before the fix: `{ $or: [ {}, { $where: 'x' } ] }`,
54+
`{ $or: [ {}, { score: { $between: 5 } } ] }` and `{ $or: [ { a: {} }, {} ] }`
55+
all translated to `{}` — match-all. The `$between` emitter arm additionally
56+
keeps a local check as defense for its own invariant, the dual-gate pattern the
57+
`$null` arm documents; both sites call one constructor with one path spelling.
58+
59+
Every filter that translated before still translates byte-identically: this adds
60+
refusals in front of the verdict, it does not reclassify any surviving shape.
61+
Authored filters using these shapes were already not doing what they appeared to
62+
do, and now say so instead of answering silently.
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
---
2+
"@objectstack/driver-mongodb": patch
3+
"@objectstack/spec": patch
4+
---
5+
6+
fix(driver-mongodb): take a structured `GroupByNode`, and answer `count` /
7+
`count_distinct` the way every other backend does (#6850, part of #6814)
8+
9+
`driver-mongodb` is now enrolled in the shared `AGGREGATION_CASES` standard
10+
(`@objectstack/spec/data`), and clearing that cell fixed three divergences — all
11+
three of the kind that ANSWER rather than fail, which is why none of them ever
12+
surfaced as an error.
13+
14+
**1. A structured `groupBy` node had no lowering at all (#6850).**
15+
`GroupByNodeSchema` declares a union: a bare field name, or
16+
`{ field, dateGranularity?, alias? }`. The pipeline builder annotated `groupBy`
17+
as `string[]` and did `groupId[field] = '$' + field`, so a structured node — an
18+
object in that loop — stringified: the `$group._id` key became the literal
19+
`"[object Object]"` and its value the field path `"$[object Object]"`, which
20+
matches nothing. The aggregation did not refuse and did not throw. It returned
21+
rows grouped by a nonexistent path, under a column named `[object Object]`.
22+
`MongoDBDriver.aggregate` passed the value through an `any` cast, which is why
23+
the declared union never met that annotation at `tsc`.
24+
25+
Both sides now spell the declared type, so the next drift between them is a
26+
compile error. The `$group._id` keys on `alias ?? field` and its value is the
27+
FIELD path — the projected column is renamed, the grouping does not move, which
28+
is the rule #6401 converged the three SQL faces onto and the one
29+
`in-memory-aggregation.ts` has always applied. The bare-string spelling emits
30+
exactly what it emitted before.
31+
32+
**2. `count_distinct` counted NULL as a distinct value (#6814).** The lowering
33+
collects a `$addToSet` and sizes it; `$addToSet` keeps an explicit `null`, so a
34+
nullable column answered one HIGHER than `COUNT(DISTINCT col)` — 3 where the
35+
standard says 2. The sizing now excludes null, which is what
36+
`COUNT(DISTINCT col)` computes on SQLite, PostgreSQL and MySQL alike and what
37+
`objectql`'s in-memory fallback already computed.
38+
39+
**3. `count(col)` counted ROWS, not values.** Measured while writing the suite
40+
and named by neither issue: the `count` arm ignored `field` entirely and emitted
41+
`{ $sum: 1 }` for both spellings, so `count(stage)` came back 6 — the answer
42+
`count(*)` already has — where the standard says 4. `count(col)` now counts
43+
non-null values, and a missing field is counted as null, the SQL reading.
44+
45+
**A `dateGranularity` node is now REFUSED rather than silently ignored**, with
46+
`NOT_IMPLEMENTED` / 501 in the ADR-0112 envelope — the same refusal, first
47+
sentence for first sentence, that `driver-sql` and `driver-turso`'s remote
48+
transport give for a granularity they cannot bucket (#6212). This driver
49+
publishes no `supports.queryDateGranularity`, so the engine buckets every
50+
granularity in memory and never pushes a bucketed node down; the refusal fires
51+
only for a caller that reached the builder directly, which previously got a
52+
`"[object Object]"` grouping instead. A native `$dateTrunc` lowering is
53+
buildable and is not ruled out — it needs the engine's bucket LABELS, a
54+
published capability record and `date-bucket-parity.test.ts`, so it is its own
55+
change. A `groupBy` entry that is neither half of the union is refused with
56+
`INVALID_QUERY` / 400.
57+
58+
The suite that holds all of this is server-free (`mongodb-aggregation-
59+
translation.test.ts`): this package's real-mongod suites are opt-in since #5517,
60+
so it drives the EMITTED pipeline through a strict in-process evaluator that
61+
refuses every shape it does not model. It holds the lowering to the shared
62+
table; it does not answer "does MongoDB agree?", which is a real-mongod half's
63+
question and is recorded as still open on #6814.
64+
65+
`driver-memory`'s half of #6814 is untouched — it remains under the #5499
66+
investment freeze, and its `AGGREGATION_CASES` DEBT row stands.

0 commit comments

Comments
 (0)