Skip to content

Commit c16f8bf

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-19046-grid-pagination-accept-set
2 parents 6996fd9 + 07c6f82 commit c16f8bf

26 files changed

Lines changed: 1375 additions & 78 deletions
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
`@objectstack/spec/data` now exports the typed hook `ctx.api` face — `HookApi`, `HookObjectApi`, `HookQuery`, `HookCountQuery`, `HookUpdateDoc`, `HookUpdateOptions`, `HookDeleteOptions`, `HookDoc` and `HookDriverPassthroughOptions` — so a metadata app's `*.hook.ts` imports the platform's type instead of hand-declaring one (#18163). The same entry additionally re-exports `EngineTransactionInfo` and `EngineTransactionOptions`, which its public declarations reference structurally: without them a consumer that imports only `@objectstack/spec/data` and emits declarations answers `TS2883: The inferred type ... cannot be named without a reference to ...`. Type-only re-exports of the declarations `@objectstack/spec/contracts` already publishes, not second declarations.
6+
7+
```ts
8+
import type { HookApi } from '@objectstack/spec/data';
9+
10+
const api = ctx.api as HookApi | undefined;
11+
if (!api) return;
12+
const owner = await api.object('user').findOne({ where: { id: ctx.input.owner } });
13+
```
14+
15+
The platform already implemented this surface; it just never published a type an app could import, so every app re-derived the engine's option vocabulary in a copy that drifts the moment the engine moves. The reference third-party app carried ~2,358 authored tokens of one in a single file, imported by 17 hook files.
16+
17+
- **The query shape is `where`-only — there is no `filter` key, deliberately.** `RPC_QUERY_ALIAS_SLOTS` declares `filter` as the alias of `where` (and `top` as the alias of `limit`); every engine entry point folds the `where` slot, collapsing redundant identical spellings and REFUSING the slot when the two spellings carry different values. So `{ where, filter }` is silent when they happen to agree and a runtime throw when they do not. Omitting the alias keys makes it neither: `TS2353: 'filter' does not exist in type 'HookQuery'`, at the authoring site.
18+
- **Not a second dialect of `IScopedContext`.** `contracts/scoped-context.ts` stays the CHECKED IMPLEMENTATION contract ObjectQL's `ScopedContext` and `ObjectRepository` carry `implements` clauses against, with its deliberately loose `Record<string, unknown>` bags. This is the authoring half of the same seam: `HookApi` is assignable to `IScopedContext`, so `ctx.api as HookApi` stays a direct cast, and nothing about the older contract changes.
19+
- **Every option shape is DERIVED, not transcribed.** Each is an `Omit`/`Pick` over the `Engine*Options` schemas that the engine's own per-method legal-key sets are pinned against, so a key added to a schema reaches the published type in the same run it reaches the engine's accepted set. `count` is the one shape without the driver pass-through keys, because the engine forwards no bag on that method and rejects them there — engine behaviour no document states, and exactly what a hand-written copy gets wrong.
20+
- **What is deliberately absent, each for a stated reason**: `context` (the repository injects it and discards a caller's), the `cursor` / `distinct` / `upsert` tombstones, `sudo()` (the #5945 exclusion stands — `Hook.runAs: 'system'` is the declared way to run elevated), and `aggregate` / `execute` / `create` / `deleteById`.
21+
22+
Additive only: eleven new exported names from `./data` (nine new declarations plus two type-only re-exports), no removal and no signature change, so nothing an existing consumer imports moves.
23+
24+
Clause-②: yes (widening)
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
fix(spec)!: `scale` is bounded at the renderer ceiling of 100 (#18972)
6+
7+
Clause-②: no (narrowing)
8+
9+
`FieldSchema.scale` — and the inline grid column's own `scale` — were declared as
10+
any non-negative integer with no upper bound. Every renderer that turns a declared
11+
`scale` into fraction digits reaches one of two platform primitives, and both of
12+
them refuse above 100: `Number.prototype.toFixed` throws `RangeError: toFixed()
13+
digits argument must be between 0 and 100`, and `Intl.NumberFormat` throws
14+
`RangeError: maximumFractionDigits value is out of range.` So a spec-valid
15+
declaration was unrenderable by any conforming consumer, and its author got no
16+
signal at publish time — the failure arrived as a render-time crash in someone
17+
else's repository. Both live readers are objectui's: the grid's `computeRow` rounds
18+
a computed cell with `Number(v.toFixed(column.scale))`, and the number cell renderer
19+
passes a field's `scale` straight into `maximumFractionDigits`.
20+
21+
Both declarations now carry an upper bound of 100, and the refusal says **why** —
22+
it names both primitives, the `RangeError` and the legal maximum — so an author
23+
reads a platform limit they can verify rather than a cap somebody chose. The bound
24+
is the platform's own: at 100 both primitives are measured to succeed, at 101 both
25+
are measured to throw, and a unit test re-measures that boundary on every run
26+
rather than trusting the literal.
27+
28+
**BREAKING** — a declaration above 100 that parsed clean before is refused at
29+
authoring now. This is a deliberate narrowing of a published accepted set, priced
30+
as such rather than as a tidy-up. The declarations it refuses could only ever have
31+
crashed a renderer: there is no value above 100 that any conforming consumer can
32+
render, which is why the bound is the platform's limit and not a policy number.
33+
`packages/objectql` already carries the consumer-side half of the same fact and
34+
skips its formula rounding past 100, so no read is newly affected.
35+
36+
Unchanged in both directions: `scale: 100` still parses, `scale: 0` still parses,
37+
absence is still absence, and the malformed-declaration refusals from #8321
38+
(`scale: -1`, `scale: 2.5`) keep their existing codes and their existing wording.
39+
`precision` is untouched — it is a total digit count that reaches neither
40+
primitive, so the renderer-ceiling argument does not carry to it.
41+
42+
Shipped as `minor` under the repo's launch-window convention, in which
43+
`check-changeset-no-major` refuses `major` and breaking-ness is carried by this
44+
banner plus the ADR-0087 disposition rather than by the level.
45+
46+
<!-- adr-0087: not-required (no-migration-prescription) no authorable key is renamed, retired or reshaped: `scale` keeps its name, its place and its type, and what moves is the top of one existing key's accepted numeric range. So `objectstack migrate meta` has nothing to visit — there is no stored spelling to rewrite and no FROM side to map, because the values this now refuses have no correct mechanical replacement: 101 fraction digits is not a precision a renderer can honour at all, and picking the display precision an author actually meant is their judgement, not a transform the ledger can carry. A ledger row would therefore have to invent the very semantics ADR-0078 and PD #12 forbid inventing, and the channel that does reach every affected author is the parse refusal itself, which names both primitives, the `RangeError` and the legal maximum at the moment the declaration is written. Measured on this tree: 123 `scale:` declarations across `*.ts` / `*.tsx` / `*.json` / `*.mdx`, of which zero declare more than 100 — the lit control for a zero whose radius is this repository's tracked files and whose known outside is a `sys_metadata` row already stored in a running deployment, which no in-repo instrument reaches. The other four categories are closed on facts: `@objectstack/spec` publishes to npm and declares no `private` (not `unpublished`); no ADR-0087 id is minted in this diff (not `registered`) and none pre-dating the base covers it (not `already-registered`); and the surface that moves is a Zod metadata schema, not a runtime-only TS interface and not a type annotation (neither `runtime-interface-only` nor `type-surface-only`). -->
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
spec(ui): a navigation entry may omit `label` — it then inherits its target's CURRENT label at render time (#19049)
6+
7+
Clause-②: yes (widening)
8+
9+
`BaseNavItemSchema.label` is `.optional()`. An `app.navigation` entry written without a `label` now parses, and the semantic it parses into is declared on the key itself: **absent means the entry inherits, at render time, the current label of whatever it opens** — the view's label when it names a view and that view is labelled, else the object's / dashboard's label. A label the author *did* write renders verbatim and is never overwritten.
10+
11+
This executes the maintainer's cloud#2021 ruling (「2021 可以接受有些修改刷新才生效」) as letter **A** on objectui#9868: sync by render-time inheritance, no stored state. The spec moves first because the console reads its navigation contract from here — until now an unnamed entry was not *representable*, so the promise "an unnamed entry shows its target's name" had nowhere to be declared.
12+
13+
- **Accept-set widening only, on eight branches at once.** `BaseNavItemSchema` is spread (`...BaseNavItemSchema.shape`) into the `object`, `dashboard`, `page`, `url`, `report`, `action`, `component` and `group` nav-item declarations, so the one-line relaxation reaches all eight. The ninth branch, `separator`, spreads nothing and has never carried a `label`. Nothing that parsed before stops parsing: a present `label` is accepted exactly as before, and every other key on the item is untouched.
14+
- **Nothing is stored for the absent case.** There is no new member and no `inherited` flag — the parse adds no key the author did not write. That is the whole point of resolving at render: a target renamed after the entry was authored shows its new name on the next render, where a label materialised at authoring time would be a stale snapshot. Consumers must resolve an absent `label` at render, not at ingest.
15+
- **The rule this relaxes still holds.** *Every real destination must have identity and text* — identity is the target, text is inherited at render. That sentence is recorded in the key's `describe`, so it ships to the reference page and to any tool reading the JSON Schema.
16+
- **The three sibling `label` declarations in this file are unchanged and still required**: `NavigationArea.label`, `AppContextSelector.label` and `App.label`. Each names a container the author is creating rather than a target it could inherit from, so there is nothing for an absent label to resolve against. The ruling covers navigation entries only.
17+
18+
Downstream, in order: objectui#9868 relaxes its own `packages/types` validator to match, resolves the absent label in the nav renderer, and stops writing `label || pageName` for an unnamed entry; then cloud#2021 stops materialising an inherited label in `apply_blueprint`.

‎content/docs/references/api/metadata.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ const result = AppDefinitionResponseSchema.parse(data);
9292
| **isDefault** | `boolean` | optional (default: `false`) | Is default app |
9393
| **hidden** | `boolean` | optional | Hide from the App Switcher; the shell surfaces hidden apps via the avatar menu instead (navigation only — never an access gate) |
9494
| **_unpublished** | `boolean` | optional | Machine-managed publish gate (ADR-0045 §3) — true = unpublished, externally unobservable. Written by AI materialization, cleared by publish-drafts. Never authored. |
95-
| **navigation** | `({ id: string; label: string \| Record<string, string>; icon?: string; order?: number; … } \| { type: 'separator'; id?: string; order?: number } \| … +7 more)[]` | optional | Full navigation tree for the app sidebar |
95+
| **navigation** | `({ id: string; label?: string \| Record<string, string>; icon?: string; order?: number; … } \| { type: 'separator'; id?: string; order?: number } \| … +7 more)[]` | optional | Full navigation tree for the app sidebar |
9696
| **areas** | `{ id: string; label: string \| Record<string, string>; icon?: string; description?: string \| Record<string, string>; … }[]` | optional | Navigation areas for partitioning navigation by business domain |
9797
| **contextSelectors** | `{ id: string; label: string \| Record<string, string>; icon?: string; optionsSource: object; … }[]` | optional | App-level scope dropdowns whose value is injected into nav items as `{<id>}` template vars |
9898
| **homePageId** | `never` | optional | [REMOVED] `app.homePageId` was removed in @objectstack/spec 17.0.0 (ADR-0049). objectui's console did read it before v17 (`resolveLandingRoute`), so this key had a consumer — it was retired because the capability is better expressed on the navigation item itself than as an ID cross-reference that silently falls back when it dangles. An app's landing page IS its first navigation item (by `order`), and the root landing follows `isDefault` routing. Delete the key; to change where an app opens, reorder `navigation` so the intended entry is first, and set `isDefault` on the app that should own the root landing. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. |

‎content/docs/references/api/package-api.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -624,7 +624,7 @@ A navigation contribution: a package injecting nav items into an app it does not
624624
| **app** | `string` | ✅ | Target app name to contribute navigation into (e.g. "setup") |
625625
| **group** | `string` | optional | Target group nav-item id to append into (e.g. "group_integrations"); omit to append at the app top level. Naming a group the target app does not declare is not refused: the items are appended at the app top level anyway and a `nav_contribution_group_missing` diagnostic is emitted — by the runtime at `warn`, and by `os build` and `os validate` at compile time. |
626626
| **priority** | `integer` | optional (default: `200`) | Merge priority within the target group — lower applied first (matches object extender priority) |
627-
| **items** | `({ id: string; label: string \| Record<string, string>; icon?: string; order?: number; … } \| { type: 'separator'; id?: string; order?: number } \| … +7 more)[]` | ✅ | Navigation items contributed into the target app/group |
627+
| **items** | `({ id: string; label?: string \| Record<string, string>; icon?: string; order?: number; … } \| { type: 'separator'; id?: string; order?: number } \| … +7 more)[]` | ✅ | Navigation items contributed into the target app/group |
628628

629629
### Nested Shape: `PackageInstallBody[option 2].engine`
630630

0 commit comments

Comments
 (0)