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
28 changes: 28 additions & 0 deletions .changeset/21639-view-container-name-collision.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
'@objectstack/metadata-protocol': minor
---

The runtime save door refuses a view container whose save name, or any name its expansion produces, is a name already served from elsewhere; a package-less container row named after a view item a package ships belongs to no package

Clause-②: no (narrowing)

<!-- adr-0087: not-required (no-migration-prescription) A validity narrowing at one runtime write door over existing keys: no key of `ViewSchema` or of any other metadata schema is removed, renamed or re-shaped, so there is no tombstone and nothing mechanical for `objectstack migrate meta` to rewrite. Whether a refused container was meant as a member of the container that already serves the name, as a view item of that name, or as a member under a key of its own is authoring intent no conversion entry can decide. New saves are refused with the remedy; a row stored before this change keeps its bytes, and no stored row is re-saved. The census, taken first over the set the ruling names, found no writer that saves a second container of one object, a container under a name another container expands, or a container under the name of a view item a package ships on purpose: the platform checklist's live view-authoring item saves one container per object (`qa_repair_asset_views` on `repair_asset`); the save door's own name rulings (P2, P2b) save one container; a container on another package's object expands under its own name; and Studio at the objectui pin creates view items (the metadata-admin create body, `createView`, `setViewConfig`) and re-saves a stored body under the name it carries. Package duplication can copy a container whose object is outside the copied package into a second container of that object: before this change the copy silently took the source package's view, and it is now reported as a failed item. The source registrars never reach this door, and the example apps seed no `sys_metadata` view rows; hosted tenants and the cloud AI author were not measured. The other categories are closed on facts: the package publishes (not unpublished); no ADR-0087 id covers this rule and this diff adds none (not registered / already-registered); and the change narrows what a runtime write door accepts, not a runtime interface or a type surface alone (not runtime-interface-only / type-surface-only). -->

**BREAKING** accept-set narrowing at the runtime save door, shipped as `minor` under the repo's launch-window convention for breaking changes, the grade the same door's earlier container-name refusals shipped with.

**One rule.** `saveMetaItem`, which `PUT /api/v1/meta/view/:name` and the dispatcher's metadata save both call, now refuses an aggregated view container (`list` / `form` / `listViews` / `formViews`) when its save name, or any name its expansion produces, is already served from elsewhere: by another stored container's expansion in the caller's selection (environment-wide rows plus the caller's organization's), whatever that container's object, or by a view item (a body carrying `viewKind`) a package ships. A name the container's own expansion produces is refused as its save name too. Every name is judged where the read doors place it, so every member kind, the expander's de-duplicated names and a container on another package's object (which expands under its own name) are all covered. The refusal is `VALIDATION_ERROR` / 400, in draft and in publish mode, before anything is stored or registered, and it names the other owner: the stored container, the shipping package, or the container's own expansion.

**Before and after, per shape** (with `{ name: 'crm_lead', object: 'crm_lead', list, listViews: { pipeline } }` stored where a sibling is named):

- A container bound to **another object**, saved under a sibling's expanded name (`{ object: 'crm_account', list }` as `crm_lead.pipeline`). Before: accepted; the sibling's `crm_lead.pipeline` view was no longer served on either door, and the by-name read answered the raw container. After: refused, naming the container `crm_lead`.
- An **unbound** container (`{ list }`) under the same name. Before and after: as above.
- A **second container of one object** whose bare `list` takes `crm_lead.default`, under a free name (`{ object: 'crm_lead', list }` as `lead_other_views`), or the object-named container saved after such a one. Before: accepted; whichever container was read last replaced the other's default on both doors, and nothing said why. After: refused, naming the stored container that already serves the name.
- A container saved under the name of a **view item a package ships** (`{ name: 'showcase_task.in_progress', object: 'showcase_task', list }` as `showcase_task.in_progress`), package-less, organization-scoped or in a writable package. Before: accepted; the packaged view was no longer served on the object door and the by-name read answered the raw container. Package-less, the row was also judged a container of the shipping package, so its bare `list` replaced the packaged `showcase_task.default` on both doors, wearing that package's `_packageId`. After: refused, naming the shipping package.
- An overlay of a package's own container whose new member takes the name of a view item **the package ships on its own**. Before: accepted; the member replaced that packaged view on both doors. After: refused, naming the package.
- A container under a name its own expansion produces, or under a name another stored container of the same object expands. Refused before and after, with the same envelope. The own-expansion refusal no longer tells the author to save the container under its object's name when a stored container already holds that name; it names that container to add the view to.

**What still saves.** A view item under any of these names: it is that name's sanctioned override. A container under its object's name, or under any other name of its own, whose expansion takes no name served elsewhere, and its own re-save. An overlay of a package's own container under that container's name. A container on another package's object, which expands under its own name. A container whose would-be sibling is in another organization: the caller's own selection decides, as it does for the read doors.

**Rows stored before this change.** They keep their bytes and are served as before, with one change: a package-less container row stored under the name of a view item a package ships now belongs to no package. On that package's object it expands under its own name (`showcase_task.showcase_task.in_progress` for a bare `list`), with no `_packageId` and no default, and the packaged views it used to replace are served again on both doors. The row itself still takes its own name's slot, as any stored row does. A new save of a row in a refused shape, a re-save included, is refused until its body stops colliding; `migrate meta --stored` and package duplication report such a row as failed with this refusal instead of re-saving it. Delete stays open.

**The fix.** Add the view as a member of the stored container that already serves the name (its `list`, `listViews`, `form` or `formViews`), or save a view item (`name`, `object`, `viewKind`, `config`) under that name to override it. For a name a package ships: save a view item under it to override the packaged view, or save the container under a name of its own that no package ships and no stored container expands.
Loading
Loading