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
2 changes: 1 addition & 1 deletion .changeset/32072-dataset-resolve-by-package.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
---
'@objectstack/rest': patch
'@objectstack/service-analytics': patch
Expand All @@ -18,7 +18,7 @@

A publish's build probes read a dashboard widget's dataset by name, so a name several packages hold has no dataset to probe: the widget now gets one warning-tier `widget_not_probed` issue naming every holder, where it was skipped without a word.

**BREAKING** for answers that served one package's dataset for an address that does not name it, and for the failure answers around them (first-party count for each: 0 — no first-party package ships a dataset name another package also ships, no first-party package reads a dataset by bare name with a package caller, and nothing first-party sends `?package=` to the query door or reads a dataset at a package that does not hold it):
**BREAKING** for answers that served one package's dataset for an address that does not name it, and for the failure answers around them (first-party count for each, read in this repository: 0 — no first-party package ships a dataset name another package also ships, no first-party package reads a dataset by bare name with a package caller, and nothing first-party sends `?package=` to the query door or reads a dataset at a package that does not hold it; **corrected by #32929:** one first-party app outside this repository moves with the reader's package — HotCRM's CRM Overview dashboard, in its app package `app.objectstack.hotcrm`, binds the dataset `product_metrics` that its module package `app.objectstack.hotcrm.revenue` holds; the module depends on the app, not the app on the module, so once the console sends the reader's package the tile answers `409 RESOURCE_CONFLICT` (`outside-closure`) where it drew on 18.3.0, and declaring the module from the app would be a dependency cycle the kernel refuses at boot. HotCRM fixes it in its own release, by moving the dashboard into the module or the dataset into the app package; the HotCRM lane chooses (#32929). Other first-party apps outside this repository: not measured.):

1. A bare `datasetName` two packages hold, on the query door and on `query_dataset`, ran the first listed. It is now `409 RESOURCE_CONFLICT` naming both, and nothing runs. Phase 1's install door already refuses the second holder, so only a deployment that booted with the clash (and was warned at boot) holds one. Fix: `?package=ID` on the query door; on `query_dataset`, rename one of the two datasets, since the tool takes no package.
2. A bare by-name read of a dataset two packages hold — `GET /meta/dataset/:name`, and the protocol's `getMetaItem({ type: 'dataset', name })` it answers from — served the first registered. It is now `409 RESOURCE_CONFLICT` naming both. Fix: `/meta/dataset/NAMESPACE/:name` or `?package=ID`.
Expand Down
2 changes: 1 addition & 1 deletion .changeset/32077-dataset-query-package.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
---
'@object-ui/core': patch
'@object-ui/data-objectstack': patch
Expand All @@ -21,4 +21,4 @@

An item authored in the environment (no `_packageId`, or the package-less `sys_metadata` sentinel) sends no `?package=`, so its read stays caller-less exactly as before. An inline dataset (the dashboard filter bar's option query, the Studio dataset preview) names no saved item and sends none. A `409` is shown as the server's sentence, naming every candidate, and is never retried with the bare name.

**BREAKING** (the dataset query of a package item, interim under the epic's ruling A): `?package=` is the exact qualifier (ADR-0150 D5), not the dependency-closure read of D3. **FROM:** a dashboard, report or list-view chart in package P, whose dataset only a dependency of P holds, is served by the bare read. **TO:** it is refused `409 RESOURCE_CONFLICT` (`not-held-by-qualifier`), naming the holder, and the item's widget or block shows that sentence. **The interim fix:** ship the dataset in the item's own package. No first-party package references a dataset across packages. Follow-ups #32310 (a caller-package arm on the dataset query door, with closure semantics) and #32311 (the console switches to it) restore the dependency case.
**BREAKING** (the dataset query of a package item, interim under the epic's ruling A): `?package=` is the exact qualifier (ADR-0150 D5), not the dependency-closure read of D3. **FROM:** a dashboard, report or list-view chart in package P, whose dataset only a dependency of P holds, is served by the bare read. **TO:** it is refused `409 RESOURCE_CONFLICT` (`not-held-by-qualifier`), naming the holder, and the item's widget or block shows that sentence. **The interim fix:** ship the dataset in the item's own package. No first-party package references a dataset across packages (read in this repository; **corrected by #32929:** one first-party app outside this repository does — HotCRM's CRM Overview dashboard, in its app package `app.objectstack.hotcrm`, binds the dataset `product_metrics` that its module package `app.objectstack.hotcrm.revenue` holds, so once the console sends the item's package the tile is refused where it drew on 18.3.0; the module depends on the app, so declaring the module from the app is a cycle the kernel refuses at boot. Other first-party apps outside this repository: not measured). Follow-ups #32310 (a caller-package arm on the dataset query door, with closure semantics) and #32311 (the console switches to it) restore the dependency case.
2 changes: 1 addition & 1 deletion .changeset/32310-dataset-caller-package.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
---
'@objectstack/metadata-protocol': patch
'@objectstack/rest': patch
Expand Down Expand Up @@ -28,6 +28,6 @@
2. `?callerPackage=` beside `?package=`, blank, `all`, repeated, or not a string (`?callerPackage[x]=`) is `400 VALIDATION_ERROR`, and nothing runs (on the batch, the whole request).
3. A package id that is not installed gets the `?package=` answer for that id: `409` naming the holders, `404` when nothing holds the name.
4. A deployment whose composed metadata protocol has a by-name read (`getMetaItem`) but no caller resolution (`resolveCallerPackageAddress`), or whose engine registry lacks the package-scoped reads the exact qualifier judges with, answers a `datasetName` sent with the arm `503 SERVICE_UNAVAILABLE` (on the batch, that query's entry), and runs nothing, where it answered `200` with the bare read. `@objectstack/metadata-protocol`'s protocol on an ObjectQL engine has both.
- **Who moves.** Nothing first-party sent the parameter to either door. A script that sent it and relied on the bare read gets the reader's resolution.
- **Who moves.** Nothing first-party sent the parameter to either door. A script that sent it and relied on the bare read gets the reader's resolution. **Corrected by #32929:** once the console sends it (`32311-dataset-caller-package-ui`), one first-party app is refused — HotCRM's CRM Overview dashboard, in its app package `app.objectstack.hotcrm`, binds the dataset `product_metrics` that its module package `app.objectstack.hotcrm.revenue` holds; the module depends on the app, not the app on the module, so once the console sends the reader's package the tile answers `409 RESOURCE_CONFLICT` (`outside-closure`) where it drew on 18.3.0, and declaring the module from the app would be a dependency cycle the kernel refuses at boot. HotCRM fixes it in its own release, by moving the dashboard into the module or the dataset into the app package; the HotCRM lane chooses (#32929). Other first-party apps outside this repository: not measured.

**The remedy.** Drop `?callerPackage=` for the bare read, send `?package=HOLDER` to read one holder's dataset, or add the holder to the reading package's `dependencies`.
2 changes: 1 addition & 1 deletion .changeset/32311-dataset-caller-package-ui.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
---
'@object-ui/data-objectstack': patch
'@object-ui/plugin-charts': patch
Expand All @@ -18,4 +18,4 @@

The package stays part of each query's signature, so a cached answer never crosses packages. An item authored in the environment (no `_packageId`, or the `sys_metadata` and `__runtime__` sentinels) sends no package, and its read stays caller-less exactly as before. An inline dataset names no saved item and sends none. A `409` is shown as the server's sentence on the widget and the report block, and as its status (`HTTP 409`) on the date range; the option-colour read keeps the theme palette. None of them is retried with the bare name. The `packageId` option (`?package=`) stays on both adapter methods for a caller that means the holder.

**What this does to the BREAKING note of the `?package=` change** (`32077-dataset-query-package`, the interim this follows up). That note refuses a dashboard, report or list-view chart in package P whose dataset only a dependency of P holds, with `409 RESOURCE_CONFLICT` (`not-held-by-qualifier`). After this change such an item is served again: the dependency's dataset, resolved for P. Two cases stay refused, now as `outside-closure`: a dataset held only by a package P does not declare in its `dependencies` (transitively), and a dataset authored in the environment rather than in a package. **The fix for those:** add the holder to P's `dependencies`, or ship the dataset in P.
**What this does to the BREAKING note of the `?package=` change** (`32077-dataset-query-package`, the interim this follows up). That note refuses a dashboard, report or list-view chart in package P whose dataset only a dependency of P holds, with `409 RESOURCE_CONFLICT` (`not-held-by-qualifier`). After this change such an item is served again: the dependency's dataset, resolved for P. Two cases stay refused, now as `outside-closure`: a dataset held only by a package P does not declare in its `dependencies` (transitively), and a dataset authored in the environment rather than in a package. **The fix for those:** add the holder to P's `dependencies`, or ship the dataset in P. When the holder already depends on P (a module of the app that reads it), that dependency is a cycle: move the reading item into the holder instead. **First-party (corrected by #32929):** HotCRM's CRM Overview dashboard, in its app package `app.objectstack.hotcrm`, binds the dataset `product_metrics` that its module package `app.objectstack.hotcrm.revenue` holds; the module depends on the app, not the app on the module, so once the console sends the reader's package the tile answers `409 RESOURCE_CONFLICT` (`outside-closure`) where it drew on 18.3.0, and declaring the module from the app would be a dependency cycle the kernel refuses at boot. HotCRM fixes it in its own release, by moving the dashboard into the module or the dataset into the app package; the HotCRM lane chooses (#32929). Other first-party apps outside this repository: not measured.
12 changes: 12 additions & 0 deletions .changeset/32929-outside-closure-cycle-fix-sentence.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
'@objectstack/core': patch
---

Clause-②: no
Metadata-protocol: none

fix(core): a by-name read refused as held outside the reader's declared dependency closure now names the fix that works when the holder already depends on the reader

The `outside-closure` refusal (ADR-0150 D3), answered `409 RESOURCE_CONFLICT` by every package-scoped by-name door, told the reader to declare a dependency on the package that holds the item. When that package already depends on the reader, as a module depends on the app package of its suite, the dependency is a cycle the kernel refuses at boot (`Circular dependency detected`), so the only fix it named could not be followed. The sentence now keeps that fix and adds the one that can: move what reads the item into the package that holds it, or move the item into the reading package. Which item a read answers, its status, code and `details.candidates` do not change; only the message's wording does.

Measured on HotCRM's shape (#32929): an app package's dashboard bound to a dataset its own module holds, `POST /api/v1/analytics/dataset/query?callerPackage=<app>`, before: `… Declare a dependency on the package that holds it (ADR-0141).`; after, the same sentence followed by `If that package already depends on "<app>", the dependency would be a cycle: move what reads it into that package, or move the dataset into "<app>" (ADR-0150 D3).`
18 changes: 18 additions & 0 deletions packages/core/src/package-scoped-resolution.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,24 @@ describe('ADR-0150 D3 over a supplied holder set — a caller that belongs to a
});
});

describe('[#32929] the outside-closure sentence names both fixes', () => {
it('keeps the dependency fix and adds the move for a holder that already depends on the reader', () => {
const answer = resolvePackageScopedHolders({ type: 'dataset', name: 'n', callerPackageId: P }, packageScopedHolderSource([[A, 'a']]), closure([]));
expect(answer).toMatchObject({ status: 'refused', reason: 'outside-closure', candidates: [{ kind: 'package', packageId: A }] });
expect((answer as { message: string }).message).toBe(
`The dataset "n" is held outside the declared dependency closure of package "${P}": package "${A}". `
+ 'Declare a dependency on the package that holds it (ADR-0141). '
+ `If that package already depends on "${P}", the dependency would be a cycle: move what reads it into that package, `
+ `or move the dataset into "${P}" (ADR-0150 D3).`,
);
});

it('names the type in its own words', () => {
const answer = resolvePackageScopedHolders({ type: 'email_template', name: 'n', callerPackageId: P }, packageScopedHolderSource([[A, 'a']]), closure([]));
expect((answer as { message: string }).message).toContain(`or move the email template into "${P}"`);
});
});

describe('ADR-0150 D3 over a supplied holder set — a caller with no package', () => {
it("answers the environment's own item first, then the unique package holder", () => {
expect(resolve([[A, 'a'], [null, 'env']], [])).toMatchObject({ status: 'resolved', item: 'env', via: 'environment', holder: { kind: 'environment' } });
Expand Down
8 changes: 7 additions & 1 deletion packages/core/src/package-scoped-resolution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -560,8 +560,14 @@ export function packageScopedRefusalMessage(refusal: Omit<PackageScopedRefusal,
return `${subject} is held by ${refusal.candidates.length} ${counted} in this environment and the caller names ` +
`no package: ${named}. Nothing picks one of them: address it by its package.`;
case 'outside-closure':
// [#32929] Two fixes, because the first is not always open: when the
// holder already depends on the caller (a module of the app that reads
// it), declaring the holder back is a dependency cycle the kernel
// refuses at boot. The sentence is static; the answer does not move.
return `${subject} is held outside the declared dependency closure of package "${refusal.callerPackageId}": ` +
`${named}. Declare a dependency on the package that holds it (ADR-0141).`;
`${named}. Declare a dependency on the package that holds it (ADR-0141). If that package already depends on ` +
`"${refusal.callerPackageId}", the dependency would be a cycle: move what reads it into that package, or move the ` +
`${typeLabel(refusal.type)} into "${refusal.callerPackageId}" (ADR-0150 D3).`;
case 'not-held-by-qualifier':
return `${subject} is not held by ${refusal.callerPackageId === null ? 'the environment' : `package "${refusal.callerPackageId}"`}, ` +
`which the address names exactly (ADR-0150 D5): it is held by ${named}. Address it by its holder.`;
Expand Down
Loading