Skip to content
19 changes: 19 additions & 0 deletions .changeset/28722-customizations-serve-nav-contributions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
'@objectstack/runtime': patch
'@objectstack/spec': patch
---

feat(runtime): the customizations read serves the `nav` contributions: the navigation items other packages add to a base's apps

Clause-②: no
Metadata-protocol: none

`GET /packages/:id/customizations` now lists, in `contributions`, the navigation items other installed packages add to the base's apps, each naming the package that adds them. Studio can draw those menu entries greyed with their source, as it already can for contributed fields and flows (ADR-0029 D7 as amended by ADR-0146 D3).

- **One entry per item.** Every item of a package's `navigationContributions` entry that declares an `id`, contributed to an app the base ships, is a `nav` entry: `{ kind: 'nav', app, group?, name, packageId }`. `group` is the group the contribution targets, when it targets one. A separator without an `id` is not an entry, and a contributed group's children ride inside it.
- **The same answer the menu shows.** The entries come from the registry the served app's navigation fold reads, never a join on names. A disabled package's items are not listed, as the fold drops them, and they come back when it is enabled again. The base's own items and its `<base>.custom` pair's are not contributions, and no nav item gains a key.
- **Order and counts.** The `field` and `flow` entries come first as before, then the `nav` entries by app, group (no group first), name and package. `counts.contributions` counts them all.

The spec's `nav` arm drops its `[PLANNED — not yet served]` marker, and the member's descriptions no longer say the kind is unserved. The schema accepts exactly what it accepted before.

Nothing to do for existing metadata. A client that read `contributions` and assumed only `field` and `flow` entries now also receives `nav` entries, which the declared schema already allowed.
5 changes: 2 additions & 3 deletions content/docs/api/metadata-api.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,8 @@ never overridden; it is copied under a new name. What the reads then serve is in
`POST /packages/hotcrm/ensure-customization` creates the pair when absent (Studio calls
it on the first change), and `GET /packages/hotcrm/customizations` lists its overrides,
extensions and switched-off items in one read, beside `contributions`: the fields (kind `field`)
and flows (kind `flow`) other installed packages contribute to `hotcrm`'s objects, each entry
naming its package. Its `nav` kind (a navigation item another package contributes to a
`hotcrm` app) is declared and not yet served.
and flows (kind `flow`) other installed packages contribute to `hotcrm`'s objects, and the
navigation items (kind `nav`) they contribute to `hotcrm`'s apps, each entry naming its package.

**Body**: `{ type: "object", name: "account", item: { ... } }`
**Response**: `{ success: true }`
Expand Down
16 changes: 8 additions & 8 deletions content/docs/references/api/package-lifecycle.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,12 @@ declared a VERSION rollback against the live COMMIT-rollback path;
`RollbackToPackageCommitResponseSchema` below is the true contract,
authored after that retirement per the ruling's sequencing (3A).

Two members are not transcriptions but DECLARED AHEAD OF THEIR PRODUCER
(ADR-0049), each describe opening `[PLANNED — not yet served]` until the
change that serves it drops the marker: the `nav` arm of
`GetPackageCustomizationsResponseSchema.contributions` (#27928) and
Two declarations are not transcriptions but DECLARED AHEAD OF THEIR
PRODUCER (ADR-0049), each describe opening `[PLANNED — not yet served]`
until the change that serves it drops the marker:
`PackageExportManifestSchema.withheldMaps` (#28640 ruling ③A, served by
#29342). Their own docblocks say what is not served yet.
#29342) and `PackageExportBundleSchema` (`GET /packages/export`, #28914).
Their own docblocks say what is not served yet.

<Callout type="info">
**Source:** `packages/spec/src/api/package-lifecycle.zod.ts`
Expand Down Expand Up @@ -170,7 +170,7 @@ Per-package customization read request: the base package id, no query parameters

## GetPackageCustomizationsResponse

Per-package customization read: overrides, extensions and switched-off items of one installed base, and the fields, flows and (not yet served) navigation items other packages contribute to it.
Per-package customization read: overrides, extensions and switched-off items of one installed base, and the fields, flows and navigation items other packages contribute to it.

### Properties

Expand All @@ -180,7 +180,7 @@ Per-package customization read: overrides, extensions and switched-off items of
| **overrides** | `{ type: string; name: string; member?: string; basedOn?: string; … }[]` | ✅ | Every ACTIVE item of the base's paired `<base>.custom` package that declares `overrides.package` = the base and that the effective read serves over the base's item of the same name: the rows the served-override predicate admits (a declaration from any other package, and an undeclared pair item, are not listed), minus the stored rows every read declines (a shipped flow name). Ordered by type, name and member. |
| **extensions** | `{ object: string; kind: Enum<'field' \| 'validation' \| 'index'>; name?: string; fields?: string[] }[]` | ✅ | Every field, validation rule and index the paired `<base>.custom` package declares in an object extension (`objectExtensions`) of an object the base owns, one entry per member, as the extension declares it. Ordered by object, kind and name. |
| **switchedOff** | `{ type: string; name: string }[]` | ✅ | Every activation-ledger row (`sys_metadata_activation`) that records the base as its package and `active: false`: the base's items this deployment switched off. Ordered by type and name. |
| **contributions** | `({ kind: Enum<'field' \| 'flow'>; object: string; name: string; packageId: string } \| { kind: 'nav'; app: string; group?: string; name: string; … })[]` | ✅ | Every field, flow and navigation item another installed package contributes to the base, one entry per item and contributing package: the fields of every object extension of an object the base owns, from any package but the base and its pair; every flow registered under such a package whose trigger object the base owns (a flow of a disabled package is not served, so not listed); and, declared ahead of its producer and not yet served, every navigation item such a package contributes to an app the base owns. The entry shape is per `kind`: `object` for `field` and `flow`, `app` and an optional `group` for `nav`. Ordered: the `field` and `flow` entries first, by object, kind, name and package; then the `nav` entries, by app, group (an entry with no group first), name and package. |
| **contributions** | `({ kind: Enum<'field' \| 'flow'>; object: string; name: string; packageId: string } \| { kind: 'nav'; app: string; group?: string; name: string; … })[]` | ✅ | Every field, flow and navigation item another installed package contributes to the base, one entry per item and contributing package: the fields of every object extension of an object the base owns, from any package but the base and its pair; every flow registered under such a package whose trigger object the base owns (a flow of a disabled package is not served, so not listed); and every navigation item such a package contributes to an app the base owns (a disabled package's are not listed). The entry shape is per `kind`: `object` for `field` and `flow`, `app` and an optional `group` for `nav`. Ordered: the `field` and `flow` entries first, by object, kind, name and package; then the `nav` entries, by app, group (an entry with no group first), name and package. |
| **counts** | `{ overrides: integer; extensions: integer; switchedOff: integer; contributions: integer }` | ✅ | Each list's length, derived from the lists in this same answer. |

### Nested Shape: `GetPackageCustomizationsResponse.overrides[number]`
Expand Down Expand Up @@ -222,7 +222,7 @@ A field or flow another package contributes to one of the base's objects.

### Nested Shape: `GetPackageCustomizationsResponse.contributions[number][option 2]`

[PLANNED — not yet served] Declared ahead of its producer (ADR-0049): the read lists no `nav` entry yet. A navigation item another installed package contributes to an app the base owns, one entry per item of the contribution's `items` (a contributed group's children ride inside it and are not entries of their own), read from the same contribution registry the served app's navigation fold reads (ADR-0029 D7). An item that declares no `id` (a separator may omit it) has no `name` to give and is not an entry. A disabled package's contributions are not listed, as the fold drops them.
A navigation item another installed package contributes to an app the base owns, one entry per item of the contribution's `items` (a contributed group's children ride inside it and are not entries of their own), read from the same contribution registry the served app's navigation fold reads (ADR-0029 D7). An item that declares no `id` (a separator may omit it) has no `name` to give and is not an entry. A disabled package's contributions are not listed, as the fold drops them.

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
Expand Down
Loading
Loading