Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .changeset/30062-declared-override-mapping-removal-remedies.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
'@objectstack/metadata-protocol': minor
'@object-ui/app-shell': minor
---

fix(metadata-protocol): a mapping override is served, a pair's removal of its own override of any overridable type brings the base's item back, and an excluded type's `overrides` refusal names a remedy that works for it (#30062)

Clause-②: no — ADR-0146 D2 says an item in the customization package replaces the base's item and removing it brings the base's back, and ADR-0146's revert ("Restore package original") is deleting the overriding item from `.custom` so the base's document is served again. The 403 on removing a pair's own doc or mapping override, and the unserved mapping override, refused what that text already admits. A removal that names the base still keeps today's answer.
Metadata-protocol: semantic

- **A mapping override in `<base>.custom` is served.** A mapping item that declares `overrides: { package: '<base>', … }` in the base's customization package now replaces the base's mapping for the by-name read (no `?package`, `?package=<base>`), both lists and the layered read, as page, app, dataset, doc and book already did. The import door (`@objectstack/rest`'s `import-mapping.ts`) resolves a named mapping through that package-less by-name read, so it now gets the pair's mapping.
- **Removing a pair's own override works for every overridable type.** `DELETE` of a declared override the customization package holds (named with `?package=<base>.custom`) was admitted only for a flow; a doc or mapping override answered `403 NOT_OVERRIDABLE` on the environment kernel. The removal now admits the pair's row for every type `isOverridableMetadataType` names. The row is deleted and the base's item is served again under every address. A removal that names the base (`?package=<base>`) is unchanged: the pair's row stays and its override stays served.
- **An excluded type's refusal names a remedy that works for it.** A save of `overrides` on a type that takes no override now cites that type's own record (ADR-0146 D6 for the security catalog and code-only types, D1 for a datasource, D9 for a behavioural switch, D10 for a field, ADR-0126 §3 for one-shot types, D2 for the rest) and prescribes a door that exists for that type: a new item under a new name where the type may be created at runtime, the documented switch route for a sharing rule, Setup › Queues for a queue, Setup › Datasources for a datasource, an object extension for a field, and a new version of the base for a type defined only in package source. The error code and status are unchanged.
- **`@object-ui/app-shell`: the All metadata tile agrees with the Customize door.** On a read-only package, a type's tile reads 可定制 (Customizable) exactly where Studio offers a Customize door for it (flow and app included, whose overlay flag is off) and 只读 (Read-only) elsewhere (doc and mapping, which have no door yet). It no longer reads the overlay flag.

The label precedence of a translated override (card item 3) is not in this change.
5 changes: 4 additions & 1 deletion content/docs/api/metadata-api.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,10 @@ declaring `overrides: { package: 'hotcrm', basedOn: '3.2.0' }` (`basedOn` is the
`manifest.version` of `hotcrm` it was taken from, recorded and never judged). Only the pair declares an override: an item of any other
package that depends on `hotcrm` and bears a `hotcrm` item's name is refused at save
and publish, and its fix is a new name. A permission set, position or capability is
never overridden; it is copied under a new name. What the reads then serve is in
never overridden: a permission set or position is copied under a new name, and a
capability changes only with a new version of the package that declares it. An
`overrides` on a type that takes none is refused `403 NOT_OVERRIDABLE`, and the
sentence names that type's own route. What the reads then serve is in
[Metadata lifecycle](/docs/concepts/metadata-lifecycle#customizing-an-installed-package-the-paired-custom-override-adr-0146).
`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,
Expand Down
13 changes: 9 additions & 4 deletions content/docs/concepts/metadata-lifecycle.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -128,10 +128,15 @@ document **whole** for every read that serves the base's name
override, `effective` = the override), the list scoped to the base and the
environment-wide list all serve the override, stamped `_packageId: 'hotcrm'`. The
declaration counts only from the pair — the stored row's own `package_id` must be
`hotcrm.custom` — and today only for a `view`, `dashboard`, `report`, `email_template`,
`translation` or `flow`: the rest of `OVERRIDABLE_METADATA_TYPES` (`@objectstack/spec/kernel`)
— `page`, `app`, `dataset`, `doc`, `book`, `mapping` and `action` — waits for
its doors and is not served yet. A declaration from any other package is ignored.
`hotcrm.custom` — and for every type of `OVERRIDABLE_METADATA_TYPES` (`@objectstack/spec/kernel`)
but `action`: a `view`, `dashboard`, `report`, `email_template`, `translation`, `flow`,
`page`, `app`, `dataset`, `doc`, `book` or `mapping`. An `action` override waits for
ADR-0146 D9's conditions and is not served yet. A declaration from any other package is ignored.
The import door and the connector pull read a named `mapping` through the by-name read,
so they run the pair's mapping.
- **Removing the override brings the base's back.** `DELETE /meta/:type/:name?package=hotcrm.custom`
("Restore package original") deletes the pair's row, for every type above, and every read
then serves the base's document again. A `DELETE` naming `hotcrm` removes nothing of the pair's.
- **A new locale is not an override.** An `email_template` the pair saves at a locale the
base does not ship is the pair's own member, served under `hotcrm.custom`, and carries no
`overrides`; declaring one there is refused (`403 NOT_OVERRIDABLE` at the save, publish
Expand Down
18 changes: 9 additions & 9 deletions packages/metadata-protocol/src/protocol-diff-delete-load.ts
Original file line number Diff line number Diff line change
Expand Up @@ -401,12 +401,12 @@ export class ProtocolDiffDeleteLoadFacet {
// here, but a stored row under its name is, as repair.
const originGatedRefusal = host.originGatedRemovalRefusal(request.type, request.name);
// [#28412, ADR-0146 D9 item 1] A removal naming the shipping package's
// pair, of a type whose D9 conditions are delivered (`flow` only —
// every other open type keeps main's verdict): it removes
// the row the override is stored in, and the code package's item is
// served — and armed — again. Admitted past the sealed-removal refusal
// and told to the store as a runtime-only removal, as the declared
// save is ({@link isPairRowRemoval}).
// pair, of a type whose declared override the gate admits ([#30062]
// every open type, no longer `flow` alone): it removes the row the
// override is stored in, and the code package's item is served (a
// flow's armed) again. Admitted past the sealed-removal refusal and
// told to the store as a runtime-only removal, as the declared save
// is ({@link isPairRowRemoval}).
const pairRowRemoval = isPairRowRemoval(
request.type, host.packagedArtifactOwner({ type: request.type, name: request.name }), request.packageId,
);
Expand Down Expand Up @@ -755,10 +755,10 @@ export class ProtocolDiffDeleteLoadFacet {
// #5927 — was an inline `this.isArtifactBacked(...)` call
// with these exact arguments; now reads the method-scoped
// binding the receipts share. Same fact, one call fewer.
// [#28412] The pair's row of a type whose D9 conditions
// are delivered (`flow` only, {@link isPairRowRemoval})
// [#28412] The pair's row of a declared override
// ({@link isPairRowRemoval}; [#30062] every open type)
// is removed as the declared save wrote it: runtime-only.
// Every other type keeps the intent main gave it.
// Every other removal keeps the intent main gave it.
intent: artifactBacked && !pairRowRemoval
? 'override-artifact'
: 'runtime-only',
Expand Down
4 changes: 2 additions & 2 deletions packages/metadata-protocol/src/protocol-lock-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,8 +150,8 @@ export function declinesStoredRow(
if (typeof name !== 'string' || name === '') return false;
if (this.isDeclaredCodeDatasource(type, name)) return true;
if (!this.isUnservedSealedOverlay(type, name)) return false;
// [#29613, ADR-0146 D8 item 2] …except, for a page, app, dataset, doc or
// book, the shipping package's declared override: judged as the flow's
// [#29613, ADR-0146 D8 item 2] …except, for a page, app, dataset, doc,
// book or [#30062] mapping, the shipping package's declared override: judged as the flow's
// above, on the row's own `package_id` and body, against the package
// that ships the name. The reads then serve it from its row, as they
// serve a dashboard's. Every other sealed row of the name stays declined.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
import { describe, expect, it } from 'vitest';
import { declinesBorrowedEnvelope, servesSealedOverrideRow } from './protocol-package-bindings.js';

const ROW_SERVED = ['page', 'app', 'dataset', 'doc', 'book'] as const;
/** [#30062] `mapping` the sixth: its reads were measured and its published override was served by none. */
const ROW_SERVED = ['page', 'app', 'dataset', 'doc', 'book', 'mapping'] as const;
/** The base's code artifact the hydrator's lookup falls back to for a pair's row. */
const BASE_ARTIFACT = { _packageId: 'com.example.showcase' };

Expand All @@ -30,7 +31,7 @@ describe('[#29613] the sealed row-served override types', () => {
expect(servesSealedOverrideRow('apps')).toBe(true);
});

it.each(['mapping', 'action', 'flow', 'object', 'permission', 'view', 'dashboard', 'report', 'email_template'])(
it.each(['action', 'flow', 'object', 'permission', 'view', 'dashboard', 'report', 'email_template'])(
'%s: not a sealed row-served override type',
(type) => {
expect(servesSealedOverrideRow(type)).toBe(false);
Expand Down
30 changes: 19 additions & 11 deletions packages/metadata-protocol/src/protocol-package-bindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -307,9 +307,9 @@ function boundPackageOf(packageId: unknown): string | undefined {
* (#22057, `anotherPackageShips`).
*
* ⛔ A closed list, judged on the canonical type. [#29613] It includes the
* five {@link SEALED_ROW_SERVED_OVERRIDE_TYPES} since their reads serve a
* `.custom` pair's declared override from its row, as these three types'
* reads do. `flow` registers with no artifact envelope at all (Regime C,
* {@link SEALED_ROW_SERVED_OVERRIDE_TYPES} ([#30062] `mapping` the sixth)
* since their reads serve a `.custom` pair's declared override from its row,
* as these three types' reads do. `flow` registers with no artifact envelope at all (Regime C,
* #20913), and the boot pull arms its declared override from the bare slot.
*/
const BORROWED_ENVELOPE_DECLINED_TYPES: ReadonlySet<string> = new Set([
Expand All @@ -322,16 +322,19 @@ const BORROWED_ENVELOPE_DECLINED_TYPES: ReadonlySet<string> = new Set([
'dataset',
'doc',
'book',
// [#30062] An import mapping's declared override, served from its row as the five above.
'mapping',
]);

/**
* [#29613, ADR-0146 D8 item 2] The SEALED types (ADR-0131 D6: a registry
* entry with `allowEnvironmentOverlay: false`) whose stored row the reads
* serve when it is the shipping package's declared `.custom` override
* ({@link isServedOverrideRowOf}): `page`, `app`, `dataset`, `doc` and
* `book`, the overridable types of ADR-0146 D2 that open no environment
* overlay. Every other stored row of a name a managed package ships stays
* unserved for them ({@link isUnservedSealedOverlay}).
* ({@link isServedOverrideRowOf}): `page`, `app`, `dataset`, `doc`, `book`
* and [#30062] `mapping`, the overridable types of ADR-0146 D2 that open no
* environment overlay and wait for no D9 condition. Every other stored row of
* a name a managed package ships stays unserved for them
* ({@link isUnservedSealedOverlay}).
*
* Before, the reads declined the override's row with the rest, and the
* override was served only from the registry's bare slot, where the hydrator
Expand All @@ -340,14 +343,19 @@ const BORROWED_ENVELOPE_DECLINED_TYPES: ReadonlySet<string> = new Set([
* instead, while the lists answered the override, and the layered read's
* effective layer and the pair's per-package layer never saw it.
*
* ⛔ A closed list, judged on the canonical type. `mapping` is overridable and
* sealed too, and is not here: no card has measured its reads. `action` and
* `flow` wait for ADR-0146 D9's conditions (`flow` has its own admission,
* [#30062] `mapping` joined once its reads were measured: before, a pair's
* mapping override saved and published, and every read (the by-name read the
* import door resolves a mapping through, `@objectstack/rest`'s
* `import-mapping.ts`, included) answered the base's body, so a published
* override did nothing.
*
* ⛔ A closed list, judged on the canonical type. `action` and `flow` wait for
* ADR-0146 D9's conditions (`flow` has its own admission,
* `declinesStoredRow`'s first branch). Every type here is also in
* {@link BORROWED_ENVELOPE_DECLINED_TYPES}: the bare entry served no reader
* the row does not serve now.
*/
const SEALED_ROW_SERVED_OVERRIDE_TYPES: ReadonlySet<string> = new Set(['page', 'app', 'dataset', 'doc', 'book']);
const SEALED_ROW_SERVED_OVERRIDE_TYPES: ReadonlySet<string> = new Set(['page', 'app', 'dataset', 'doc', 'book', 'mapping']);

/** [#29613] Whether `type` is one of {@link SEALED_ROW_SERVED_OVERRIDE_TYPES}, folded to the canonical singular. */
function servesSealedOverrideRow(type: string): boolean {
Expand Down
Loading
Loading