Skip to content

Commit 29f6acd

Browse files
authored
Merge branch 'main' into claude/issue-7309-confirmtext-params
2 parents 6113216 + 97ace2a commit 29f6acd

22 files changed

Lines changed: 1842 additions & 135 deletions
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
---
2+
"@objectstack/objectql": patch
3+
"@objectstack/service-datasource": patch
4+
---
5+
6+
fix(objectql,service-datasource): bind federated objects to their remote tables whatever the boot order, and report the ones that could not be bound (#7737)
7+
8+
`driver.registerExternalObject(obj)` is the only thing that installs an
9+
ADR-0015 federated object's read metadata — the object -> remote-table mapping
10+
(`external.remoteName` / `remoteSchema`), the `external.columnMap` translation
11+
and the coercion maps. An external object that never gets it resolves to a
12+
table named after the OBJECT rather than the remote table it declares, so every
13+
read against it fails with `no such table`, or answers from the wrong table.
14+
15+
`ObjectQLPlugin`'s boot schema-sync calls it, but that call runs inside the
16+
engine plugin's `start()`, while the declared datasource that owns the remote
17+
database is auto-connected in `AppPlugin.start()` — a later `start()`. So on a
18+
perfectly healthy boot `getDriverForObject()` answers `undefined` for every
19+
federated object at that moment and the call is skipped; whether the object ends
20+
up bound depended on some other component re-driving it afterwards. Two cases
21+
where nothing did:
22+
23+
- an object routed to the datasource by a **`datasourceMapping` rule** (#4462)
24+
rather than an explicit `object.datasource``DatasourceConnectionService`
25+
re-drove only the explicitly-bound list;
26+
- any deployment running with **`OS_SKIP_SCHEMA_SYNC`** — that flag is about DDL
27+
managed out of band, and this binding is DDL-free, but it took both
28+
`syncRegisteredSchemas()` calls (and the only in-plugin binding site) with it.
29+
30+
**What changed**
31+
32+
- `ObjectQLPlugin` now runs a federated-binding reconciliation on
33+
`kernel:ready`, after every plugin's `start()` has completed: it re-drives the
34+
binding for every registered external object (idempotent) regardless of which
35+
plugin connected the datasource, in which slot, or whether DDL was skipped.
36+
Boot order no longer decides whether federation works.
37+
- The same pass **reports** what it could not bind, at `error`, naming the
38+
objects, their datasources, the consequence and the fix. Previously the entire
39+
diagnosis of a broken federation was one `debug` line reading
40+
`No driver available for object, skipping schema sync` — invisible at any
41+
normal log level, and emitted on healthy boots too. A boot with nothing to
42+
report stays silent.
43+
- `DatasourceConnectionService.connect()` now re-drives `mappedObjects`
44+
alongside `objects` when a datasource comes up, so a mapping-routed federated
45+
object is also bound by a **runtime** (UI-created) datasource connect, not
46+
only at boot.
47+
48+
No authoring surface changes; a deployment whose federated objects already
49+
worked behaves identically.
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
fix(spec): `NormalizedFilterSchema` judges its members instead of catching them all (#7711)
6+
7+
Each `$and` / `$or` member and the `$not` operand was
8+
`z.union([z.record(z.string(), FieldOperatorsSchema), NormalizedFilterSchema])`,
9+
and the second branch was a NON-strict `z.object({ $and, $or, $not })` with every
10+
key optional. "All three of my optional keys are absent" is true of any object
11+
whatsoever, so that branch was a catch-all: whenever the record branch **rejected**
12+
a field condition, the group branch accepted the very same value. The whole-filter
13+
face validated the logical skeleton and nothing else — no comparand shape it
14+
declared could ever make it fail.
15+
16+
Measured before the change, every one of these parsed green while
17+
`FieldOperatorsSchema` — the copy this schema is documented as validating
18+
against — refused the identical operator map:
19+
20+
```
21+
{ $and: [{ c: { $null: 'not-a-boolean' } }] }
22+
{ $and: [{ c: { $between: [1, 2, 3] } }] }
23+
{ $and: [{ hello: 'world' }] }
24+
```
25+
26+
The green was also lossy: an admitted member came back parsed to `{}`, so the
27+
accepted output no longer carried the condition it was asked about — a whole
28+
`{ $not: … }` subtree parsed as a field named `$not` and returned empty.
29+
30+
Now the group branch is `.strict()` and the field-condition branch rules out
31+
`$`-prefixed keys, so a member the operator map refuses has nowhere to land. The
32+
refusal names the offending keys and both valid member shapes:
33+
34+
```
35+
Not a valid $and member — got an object with key(s) "c". A $and member is either
36+
a FIELD CONDITION ({ "field": { "$op": value } }, whose keys are field names and
37+
whose operator map must satisfy FieldOperatorsSchema — comparand shapes
38+
included), or a nested LOGICAL GROUP carrying only $and / $or / $not and nothing
39+
else. Ruled on #7711: declared = enforced (ADR-0049).
40+
```
41+
42+
Graded a narrowing of the accepted surface, so `minor` rather than `patch` — but
43+
the blast radius is measured at zero: nothing in the repo called `.parse` /
44+
`.safeParse` on `NormalizedFilterSchema` outside this package's own tests
45+
(swept with `FilterConditionSchema`'s 20-plus call sites as the positive
46+
control), no driver or evaluator references the normalized AST at all, and the
47+
exported `NormalizedFilter` TYPE is unchanged — every shape that stops parsing
48+
was already outside it. Nothing is removed from the declared surface, which is
49+
what separates this from the ADR-0087 removal grade.
50+
51+
`FilterConditionSchema`, the AUTHORING face every driver, `read-scope-sql` and
52+
`cel-to-filter` actually consume, is untouched and still admits sugar by design,
53+
so no request path's row set can move with this.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
fix(spec): `PageHeaderProps.title` is optional — matches the platform's own synthesized header (#7702)
6+
7+
`PageHeaderProps.title` was declared **required**, but the platform's own
8+
synthesizer (objectui `buildDefaultHeader`) emits every seeded `page:header`
9+
with **no `title` at all**`{ type: 'page:header', recordChrome, …actions }`.
10+
`PageHeaderRenderer` (`containers.tsx`) reads
11+
`schema?.title ?? schema?.properties?.title` and, finding neither, falls
12+
through to the record chip's own record-derived heading: a static authored
13+
title would be wrong on every record but one. `PageHeaderProps.safeParse`
14+
therefore rejected the platform's own canonical output with `title: Invalid
15+
input` — invisible on the write path today (`PageComponent.properties` is an
16+
opaque `z.record`), but a standing contradiction that surfaces the moment any
17+
props-level validation runs against a header node (`validateComponentProps`,
18+
#5068, is exactly that consumer).
19+
20+
Maintainer ruling 2026-08-11 (accepting the spec lane's A/B recommendation,
21+
rejecting a sentinel-value option C): `title` becomes optional, and its
22+
describe states the sanctioned spelling — **title omitted ⇒ the renderer
23+
derives the heading from the record**. Authors still set it explicitly on
24+
non-record pages (dashboards, landing pages) where there is no record to
25+
derive a heading from.
26+
27+
This is a widening change: every payload that validated before (with `title`)
28+
still validates identically, and `title`, when present, still parses as
29+
`I18nLabelSchema` exactly as before. The only newly-accepted shape is a
30+
`page:header` with `title` omitted — the platform's own default. Minor, not
31+
patch, because the accepted-input surface grows.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
---
2+
"@objectstack/spec": patch
3+
"@objectstack/platform-objects": patch
4+
---
5+
6+
fix(spec,platform-objects): put `sys_api_key`'s missing batch route on the record (#7802)
7+
8+
`@objectstack/spec`'s `apiMethods` conformance scan was failing on `main` — and,
9+
because the scan lives in `spec` while the object it judges lives in
10+
`platform-objects`, failing for every PR that touched `spec` and no others.
11+
#7769 had added `update` to `sys_api_key`'s `enable.apiMethods` so the Setup
12+
UI's Revoke button had a working route, which tripped the rule "a whitelist that
13+
grants single-record writes must also grant `bulk`".
14+
15+
Resolved as the rule's second documented outcome — a registered exemption, not a
16+
widened object. `sys_api_key` now carries the monorepo's only
17+
`SINGLE_RECORD_WRITE_ONLY` entry, with the evidence behind it:
18+
19+
- **No batch surface exists to deny.** The console renders no checkbox column on
20+
any of the object's list views: multi-select is auto-enabled only when a bulk
21+
action exists, the sole implicit one is bulk-delete, and this object grants no
22+
delete affordance (`managedBy: 'better-auth'` denies by default, `userActions`
23+
opens `edit` alone, `delete` is not in `apiMethods`).
24+
- **A future multi-select revoke would not need `bulk` either.** `revoke_api_key`
25+
/ `restore_api_key` are `list_item` actions; promoting one into a view's
26+
`bulkActions` resolves it to a `custom` def that the grid executor fans out
27+
through the action runner as N single-record PATCHes — never `/batch`.
28+
29+
So `POST /api/v1/data/sys_api_key/batch` and the `*Many` routes keep answering
30+
405 for API keys, deliberately: the object's authorable surface is the single
31+
`revoked` boolean that ADR-0092 D2's identity write guard admits, and nothing
32+
asks to write it in bulk. #7769's `update` grant is untouched — the Revoke
33+
button keeps working. Adding `bulk` later requires retiring the exemption in the
34+
same commit; the conformance suite's stale-entry check refuses to let both stand.

content/docs/references/ui/component.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ const result = AIChatWindowProps.parse(data);
241241

242242
| Property | Type | Required | Description |
243243
| :--- | :--- | :--- | :--- |
244-
| **title** | `string \| Record<string, string>` | | Page title |
244+
| **title** | `string \| Record<string, string>` | optional | Page title. Omit to let the renderer derive the heading from the record (the default for record pages) — set explicitly on non-record pages (dashboard, landing) with no record to derive from. |
245245
| **subtitle** | `string \| Record<string, string>` | optional | Page subtitle |
246246
| **icon** | `never` | optional | [REMOVED] `page:header` property `icon` was removed in @objectstack/spec 17.0.0 (#6946, ADR-0087 D2) — no renderer ever read it: objectui resolves `icon` only per header action (`action.icon`), never off the header's own props bag, and the component registry never published it as an input, so an authored value was accepted and dropped. Delete the key. The header's own identity is drawn by the record chrome (`recordChrome`, on by default) and each action carries its own `icon`. Run `os migrate meta --from 16` to rewrite existing sources automatically. |
247247
| **breadcrumb** | `boolean` || Show breadcrumb |

docs/audits/2026-07-unknown-key-strictness-ledger.counts.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ regenerate.
2222
|---|---|
2323
| Triaged directories | 5 |
2424
| Object sites in them | 438 |
25-
| Still-open (strip) sites | 181 |
25+
| Still-open (strip) sites | 180 |
2626
| Files carrying at least one | 27 |
2727

2828
Remaining strip sites by class:
@@ -31,7 +31,7 @@ Remaining strip sites by class:
3131
|---|---|
3232
| authorable — the ruling's forced scope | 40 |
3333
| unresolved — needs a per-schema verdict | 34 |
34-
| wire / open — out of forced scope | 105 |
34+
| wire / open — out of forced scope | 104 |
3535
| no door — no carrier, ADR-0049 territory | 1 |
3636
| no gate — carrier live, no parse | 0 |
3737
| covered — no carrier, no parse, guarded at every consumer | 1 |
@@ -45,11 +45,11 @@ The `strict` column is the one the campaign schedules against; it counts both th
4545
| Dir | Sites | strict | passthrough | catchall | strip |
4646
|---|---|---|---|---|---|
4747
| `ui/` | 161 | 119 | 5 | 0 | 37 |
48-
| `data/` | 165 | 56 | 1 | 0 | 108 |
48+
| `data/` | 165 | 57 | 1 | 0 | 107 |
4949
| `automation/` | 65 | 42 | 0 | 0 | 23 |
5050
| `security/` | 20 | 7 | 0 | 0 | 13 |
5151
| `studio/` | 27 | 27 | 0 | 0 | 0 |
52-
| **total** | **438** | **251** | **6** | **0** | **181** |
52+
| **total** | **438** | **252** | **6** | **0** | **180** |
5353

5454
## File-level triage — site counts
5555

@@ -179,7 +179,7 @@ over it is here.
179179

180180
### `data/` — open
181181

182-
**108 strip of 165**, in 16 file(s).
182+
**107 strip of 165**, in 16 file(s).
183183

184184
| File | Strip | Sites |
185185
|---|---|---|
@@ -194,18 +194,18 @@ over it is here.
194194
| `external-lookup.zod.ts` | 12 | 12 |
195195
| `field-value.zod.ts` | 2 | 3 |
196196
| `field.zod.ts` | 2 | 10 |
197-
| `filter.zod.ts` | 11 | 11 |
197+
| `filter.zod.ts` | 10 | 11 |
198198
| `hook.zod.ts` | 5 | 7 |
199199
| `object.zod.ts` | 1 | 20 |
200200
| `query.zod.ts` | 4 | 5 |
201201
| `seed-loader.zod.ts` | 12 | 12 |
202-
| **total** | **108** | **165** |
202+
| **total** | **107** | **165** |
203203

204204
| Bucket | Sites |
205205
|---|---|
206206
| authorable — the ruling's forced scope | 8 |
207207
| unresolved — needs a per-schema verdict | 34 |
208-
| wire / open — out of forced scope | 66 |
208+
| wire / open — out of forced scope | 65 |
209209
| no door — no carrier, ADR-0049 territory | 0 |
210210
| no gate — carrier live, no parse | 0 |
211211
| covered — no carrier, no parse, guarded at every consumer | 0 |

0 commit comments

Comments
 (0)