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
54 changes: 54 additions & 0 deletions .changeset/unregister-items-by-package.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
---
"@objectstack/objectql": patch
---

fix(objectql): uninstalling a package now removes the non-object metadata it shipped (#7221)

"Unregister all metadata from a package" reached only `objectContributors`, so
every non-object item a package shipped — its `page`, `view`, `flow`, `app`,
`api` … — stayed registered and fully resolvable after the package was gone.
Not a stale-cache nuisance: an uninstall that leaves the package's UI and API
metadata installed.

A package writes into two stores. `SchemaRegistry.unregisterObjectsByPackage`
walks the contributor list; everything else lives in the generic `metadata` map
under the composite `${packageId}:${name}` key `registerItem` builds, and no
verb removed those. Measured on the real registry: after
`uninstallPackage('crm')` the package record was gone while
`getItem('page', 'home')` kept serving the uninstalled package's page and
`metadata.get('flow')` still held `crm:onboard`, for the life of the process.
The same call through `MetadataFacade.unregisterPackage` additionally left the
generic-map half of the package's objects behind as a genuine orphan.

`SchemaRegistry.unregisterItemsByPackage(packageId)` is the missing verb, and it
sits on the registry rather than privately on the facade because **both** callers
were measured to have the gap — `uninstallPackage` is registry-direct and shares
it exactly. A private copy in the facade would have been a second expression of
the same package-ownership rule, and would have left every registry-direct
uninstall still half-done. Membership is the exact inverse of the construction in
`registerItem`, so a discriminated type's whole i18n bundle leaves with the
package that shipped it, and a scoped package id (`@acme/crm`) is handled by the
same relation.

**Tenant overlays are deliberately kept.** A bare-key entry is the ADR-0005
runtime/DB overlay slot — a tenant's own customization, carrying no package
provenance and with no separate contributor list holding a durable copy. An
uninstall that deleted it would take tenant-authored data along with the package
it merely overlaid, so the sweep is scoped to composite keys only. The
consequence — an overlay that now layers over nothing — is made **loud** rather
than silently deleted or silently kept, the same house pattern as ADR-0029
D9.5's orphan-overlay violation: the verb warns naming every orphan it left and
returns them as `orphanedOverlays` for a caller that wants to act. What nothing
yet does with that report is filed separately as #7951.

This is deliberately **not** the object-side D9.7 rule ("an overlay layer leaves
with the base it layers over"), which is safe only because an object overlay
layer is a runtime projection of a `sys_metadata` row the removal does not touch.

Ordering: in both callers the item sweep runs after the object verb, because that
one can refuse (ADR-0029 extenders) — a refused uninstall removes nothing at all.

Unaffected: another package's same-named items (including a package id that is a
string prefix of another), runtime-authored items with no package, and the
persisted `sys_metadata` rows — a distinct mechanism this change does not reach
into.
25 changes: 24 additions & 1 deletion packages/objectql/src/metadata-facade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,10 +258,33 @@ export class MetadataFacade {
}

/**
* Unregister all metadata from a package
* Unregister all metadata from a package.
*
* [#7221] Both stores, for the same reason {@link register} and
* {@link unregister} reach both: `unregisterObjectsByPackage` walks
* `objectContributors` alone, so this verb — whose `IMetadataService`
* contract reads "Unregister all metadata items from a specific package" —
* used to leave every non-object item the package shipped (`page`, `view`,
* `flow`, `app`, `api` …) fully resolvable through this class's own `get`,
* `list`, `listNames` and `exists`, plus the generic-map half of its
* objects, which {@link registerObjectBothPlaces} writes. A half-uninstall,
* silently.
*
* `SchemaRegistry.unregisterItemsByPackage` is the registry-side verb rather
* than a scan private to this class, because `SchemaRegistry.uninstallPackage`
* was measured to have the identical gap — a second copy of the
* package-ownership rule here would be the #6808 drift, and would have left
* the registry-direct caller half-done. It deliberately keeps bare-key
* ADR-0005 runtime/DB overlays and warns about the ones it orphans; see its
* header for why that is loudness rather than a silent delete.
*
* Ordering mirrors {@link unregister}: the object verb runs first because it
* is the half that can refuse (ADR-0029 extenders), so a refusal removes
* nothing at all rather than taking the generic half with it.
*/
async unregisterPackage(packageName: string): Promise<void> {
this.registry.unregisterObjectsByPackage(packageName);
this.registry.unregisterItemsByPackage(packageName);
}

/**
Expand Down
Loading
Loading