Skip to content

Commit 8f1851e

Browse files
os-helpclaude
andauthored
fix(engine-core): propagate package disable to metadata listings and the data plane (#7557) (#7700)
* fix(engine-core): propagate package disable to metadata listings and the data plane (#7557) Half (A) of #7557: a package at `status: 'disabled'` had its nav and views correctly dropped while `GET /api/v1/data/<object>` still answered 200 with every row and `GET /meta/objects` still listed the object. Reproduced on a real server before the change, over two disable/enable cycles. Both skips were deliberate and gave the same reason — filtering objects "would break data queries that depend on their schema". That conflated two classes of reader, which are now separated: * RESOLUTION readers keep serving. `registry.getObject` and `registry.listItems('object')` still return a disabled package's objects; migrations, cross-package references and the runtime authoring gate's object universe all resolve through them. Disable stays reversible and still destroys no data. * API readers now stop. `getMetaItems` drops the `object`/`objects` exemption (`package` is still never filtered, or a disable could never be undone), and the data plane refuses in `assertObjectRegistered` — the one gate every findData/getData entry point funnels through. The data-plane refusal is loud rather than silent: a new `OBJECT_PACKAGE_DISABLED` / 404 naming the cause and the remedy, instead of a bare `OBJECT_NOT_FOUND` that sends a caller hunting for a typo. The 404 status matches the closest sibling switch, `OBJECT_API_DISABLED`. Registered in the ADR-0112 ledger. `SchemaRegistry.isObjectPackageDisabled` resolves names through the same `resolveObjectKey` the read path uses, so the gate and the read cannot disagree about which contributor entry a bare name addresses (#6808's lesson). Half (B), envelope only: `DELETE /packages/:id` on the dispatcher door stated `success: true` unconditionally and forwarded the protocol's own `{ success: false, deletedCount: 0 }` underneath it. Per-item failures now answer 400 `PACKAGE_DELETE_PARTIAL` with the failed items and the uninstall cleanup outcomes, matching the direct-mount REST door of the same route — including its carve-out that zero metadata rows is still a successful uninstall. The remaining half of (B), a persistence defect where `deletePackage` finds zero rows while package-bound rows exist, is reported for transfer rather than patched from the consumer side. Tests: one table-driven test walks every reader of the disable predicate and states which class each belongs to, so a new reader that forgets to consult it fails there rather than in one test per surface. Both halves reverse-verified independently. * chore(spec): regenerate reference docs for the new error code (#7557) `OBJECT_PACKAGE_DISABLED` joins the ADR-0112 ledger, so the generated `content/docs/references/` pages move with it: the ledger page gains the entry, and every page rendering the ErrorCode union bumps its "+259 more" count to "+260". Regenerated with `gen:schema && gen:docs`, never hand-edited. Release-owned `content/docs/releases/` is untouched. --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 7674859 commit 8f1851e

18 files changed

Lines changed: 881 additions & 77 deletions
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
---
2+
"@objectstack/metadata-protocol": patch
3+
"@objectstack/objectql": patch
4+
"@objectstack/runtime": patch
5+
"@objectstack/spec": patch
6+
---
7+
8+
fix(engine-core): disabling a package now stops its objects being served, and a failed uninstall stops answering 200 (#7557)
9+
10+
## Disabling a package is now an enforcement for its objects
11+
12+
A package at `status: 'disabled'` had its nav entries and views correctly
13+
dropped, while `GET /api/v1/data/<object>` still answered **200 with every
14+
row** and `GET /api/v1/meta/objects` still listed the object. The status was
15+
consulted by some readers and skipped by others, so "disabled" meant different
16+
things depending on which surface you asked.
17+
18+
Both skips were deliberate and both gave the same reason — filtering objects
19+
"would break data queries that depend on their schema". That conflated two
20+
different kinds of reader, and they are now separated explicitly:
21+
22+
- **Resolution readers keep serving.** `registry.getObject` and
23+
`registry.listItems('object')` still return a disabled package's objects.
24+
Migrations, cross-package references and the runtime authoring gate's object
25+
universe all resolve through them, and blanking them would break authoring
26+
that has nothing to do with the disabled package. Disable remains reversible
27+
and still destroys no data.
28+
- **API readers now stop.** The `/meta/*` listing drops the objects (the
29+
`object`/`objects` exemption in `getMetaItems` is gone; `package` is still
30+
never filtered, or a disabled package could never be re-enabled), and the data
31+
plane refuses.
32+
33+
**The data-plane refusal is loud, not silent.** `assertObjectRegistered` — the
34+
single gate every `findData`/`getData` entry point funnels through — now answers
35+
a new error code:
36+
37+
```
38+
404 { "error": { "code": "OBJECT_PACKAGE_DISABLED",
39+
"message": "Object 'x' belongs to a disabled package and is not
40+
being served. Re-enable the package to restore access." } }
41+
```
42+
43+
The 404 status matches the closest existing sibling, `OBJECT_API_DISABLED` for
44+
`enable.apiEnabled: false`, so "this object exists but is switched off" keeps
45+
one status across both switches. The distinct **code** is what makes it
46+
actionable: a bare `OBJECT_NOT_FOUND` sends a caller — an AI agent especially —
47+
hunting for a typo or re-creating an object that is merely switched off, while
48+
this one names the cause and therefore the fix. `OBJECT_PACKAGE_DISABLED` is
49+
registered in the ADR-0112 ledger under `@objectstack/metadata-protocol`.
50+
51+
If you have a client that treats a disabled package's objects as queryable, it
52+
now receives a 404 with the code above instead of rows. Re-enabling the package
53+
restores every surface.
54+
55+
## A failed uninstall is no longer wrapped in a 200
56+
57+
`DELETE /packages/:id` on the dispatcher door stated `success: true`
58+
unconditionally and forwarded the protocol's own `{ success: false,
59+
deletedCount: 0 }` underneath it, so the status line and the payload disagreed
60+
and any caller reading the status recorded an uninstall that had not happened.
61+
Per-item failures now answer **400 `PACKAGE_DELETE_PARTIAL`**, carrying the
62+
failed items and the uninstall cleanup outcomes (a failed permission revocation
63+
is a ghost grant, so it must survive the failure path).
64+
65+
The rule is copied deliberately from the direct-mount REST door of the same
66+
route, which already answered this way — two doors to one route answering
67+
differently is how the divergence arrived. That includes its carve-out: **zero
68+
metadata rows is still a successful uninstall**, because a runtime-registered
69+
package that never published metadata has nothing in `sys_metadata`. The
70+
failure predicate is therefore `failedCount > 0`, not `!persisted.success`.
71+
72+
An all-rows-failed uninstall now answers 400 rather than the 404 its zero
73+
`deletedCount` previously implied.
74+
75+
**Not fixed here:** the separate persistence defect where `deletePackage` finds
76+
zero rows while package-bound `sys_metadata` rows demonstrably exist, leaving
77+
them behind on an otherwise-clean uninstall. That is a `sys_metadata` query
78+
defect one layer below this handler and is reported for its own fix; see #7557.

‎content/docs/references/api/analytics.mdx‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ const result = AnalyticsEndpoint.parse(data);
4444
| Property | Type | Required | Description |
4545
| :--- | :--- | :--- | :--- |
4646
| **success** | `boolean` | ✅ | Operation success status |
47-
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +259 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false |
47+
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +260 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false |
4848
| **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata |
4949
| **data** | `{ name: string; title?: string; measures: object[]; dimensions: object[] }[]` | ✅ | Available cubes, each as the `CubeMeta` discovery projection — the cube name, its title, and the measures/dimensions a client may name in a query. A bare array: there is no `cubes` wrapper object, and no cube `sql` is published. |
5050

@@ -79,7 +79,7 @@ const result = AnalyticsEndpoint.parse(data);
7979
| Property | Type | Required | Description |
8080
| :--- | :--- | :--- | :--- |
8181
| **success** | `boolean` | ✅ | Operation success status |
82-
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +259 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false |
82+
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +260 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false |
8383
| **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata |
8484
| **data** | `{ rows: Record<string, any>[]; fields: object[]; sql?: string }` | ✅ | |
8585

@@ -93,7 +93,7 @@ const result = AnalyticsEndpoint.parse(data);
9393
| Property | Type | Required | Description |
9494
| :--- | :--- | :--- | :--- |
9595
| **success** | `boolean` | ✅ | Operation success status |
96-
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +259 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false |
96+
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +260 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false |
9797
| **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata |
9898
| **data** | `{ sql: string; params: any[] }` | ✅ | |
9999

‎content/docs/references/api/auth.mdx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ const result = AuthProvider.parse(data);
117117
| Property | Type | Required | Description |
118118
| :--- | :--- | :--- | :--- |
119119
| **success** | `boolean` | ✅ | Operation success status |
120-
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +259 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false |
120+
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +260 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false |
121121
| **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata |
122122
| **data** | `{ session: object; user: object; token?: string }` | ✅ | |
123123

@@ -153,7 +153,7 @@ const result = AuthProvider.parse(data);
153153
| Property | Type | Required | Description |
154154
| :--- | :--- | :--- | :--- |
155155
| **success** | `boolean` | ✅ | Operation success status |
156-
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +259 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false |
156+
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +260 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false |
157157
| **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata |
158158
| **data** | `{ id: string; email: string; emailVerified: boolean; name: string; … }` | ✅ | |
159159

‎content/docs/references/api/automation-api.mdx‎

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ const result = AutomationApiErrorCode.parse(data);
119119
| Property | Type | Required | Description |
120120
| :--- | :--- | :--- | :--- |
121121
| **success** | `boolean` | ✅ | Operation success status |
122-
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +259 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false |
122+
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +260 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false |
123123
| **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata |
124124
| **data** | `{ name: string; label: string; description?: string; successMessage?: string; … }` | ✅ | The created flow definition |
125125

@@ -144,7 +144,7 @@ const result = AutomationApiErrorCode.parse(data);
144144
| Property | Type | Required | Description |
145145
| :--- | :--- | :--- | :--- |
146146
| **success** | `boolean` | ✅ | Operation success status |
147-
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +259 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false |
147+
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +260 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false |
148148
| **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata |
149149
| **data** | `{ name: string; deleted: boolean }` | ✅ | |
150150

@@ -187,7 +187,7 @@ const result = AutomationApiErrorCode.parse(data);
187187
| Property | Type | Required | Description |
188188
| :--- | :--- | :--- | :--- |
189189
| **success** | `boolean` | ✅ | Operation success status |
190-
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +259 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false |
190+
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +260 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false |
191191
| **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata |
192192
| **data** | `{ name: string; label: string; description?: string; successMessage?: string; … }` | ✅ | Full flow definition |
193193

@@ -213,7 +213,7 @@ const result = AutomationApiErrorCode.parse(data);
213213
| Property | Type | Required | Description |
214214
| :--- | :--- | :--- | :--- |
215215
| **success** | `boolean` | ✅ | Operation success status |
216-
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +259 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false |
216+
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +260 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false |
217217
| **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata |
218218
| **data** | `{ id: string; flowName: string; flowVersion?: integer; status: Enum<'pending' \| 'running' \| 'paused' \| 'completed' \| 'failed' \| 'cancelled' \| … +2 more>; … }` | ✅ | Full execution log with step details |
219219

@@ -241,7 +241,7 @@ const result = AutomationApiErrorCode.parse(data);
241241
| Property | Type | Required | Description |
242242
| :--- | :--- | :--- | :--- |
243243
| **success** | `boolean` | ✅ | Operation success status |
244-
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +259 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false |
244+
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +260 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false |
245245
| **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata |
246246
| **data** | `{ flows: object[]; total?: integer; nextCursor?: string; hasMore: boolean }` | ✅ | |
247247

@@ -269,7 +269,7 @@ const result = AutomationApiErrorCode.parse(data);
269269
| Property | Type | Required | Description |
270270
| :--- | :--- | :--- | :--- |
271271
| **success** | `boolean` | ✅ | Operation success status |
272-
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +259 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false |
272+
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +260 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false |
273273
| **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata |
274274
| **data** | `{ runs: object[]; total?: integer; nextCursor?: string; hasMore: boolean }` | ✅ | |
275275

@@ -295,7 +295,7 @@ const result = AutomationApiErrorCode.parse(data);
295295
| Property | Type | Required | Description |
296296
| :--- | :--- | :--- | :--- |
297297
| **success** | `boolean` | ✅ | Operation success status |
298-
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +259 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false |
298+
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +260 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false |
299299
| **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata |
300300
| **data** | `{ name: string; enabled: boolean }` | ✅ | |
301301

@@ -325,7 +325,7 @@ const result = AutomationApiErrorCode.parse(data);
325325
| Property | Type | Required | Description |
326326
| :--- | :--- | :--- | :--- |
327327
| **success** | `boolean` | ✅ | Operation success status |
328-
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +259 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false |
328+
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +260 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false |
329329
| **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata |
330330
| **data** | `{ success: boolean; output?: any; error?: string; durationMs?: number }` | ✅ | |
331331

@@ -351,7 +351,7 @@ const result = AutomationApiErrorCode.parse(data);
351351
| Property | Type | Required | Description |
352352
| :--- | :--- | :--- | :--- |
353353
| **success** | `boolean` | ✅ | Operation success status |
354-
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +259 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false |
354+
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +260 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false |
355355
| **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata |
356356
| **data** | `{ name: string; label: string; description?: string; successMessage?: string; … }` | ✅ | The updated flow definition |
357357

0 commit comments

Comments
 (0)