Skip to content

Commit c2899d4

Browse files
committed
test(metadata-protocol,spec): pin the external-binding save doors and composeStacks, with docs and changeset (#25211)
- the object and datasource save doors, draft and publish, force, the publish re-judge, and the controls - composeStacks: the cross-input pair in both orders under last and preserve - #25192's harness gains the registry read the object door now makes - docs: the write example names its datasource; section 2 states the rule - api-surface and export-origins rows for the exported predicate Claude-Session: https://claude.ai/code/session_01QVYg9FaRCUgSV13upGJSZQ Co-authored-by: Claude <noreply@anthropic.com>
1 parent a0e7aa9 commit c2899d4

8 files changed

Lines changed: 459 additions & 6 deletions

File tree

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/metadata-protocol': minor
4+
---
5+
6+
fix(metadata-protocol)!: the metadata save doors and `composeStacks` refuse an object's `external` binding on a named managed datasource, from either side of the pair
7+
8+
Clause-②: yes (narrowing)
9+
10+
Metadata-protocol: narrowed
11+
12+
<!-- adr-0087: not-required (already-registered object-external-binding-managed-datasource-refused) registered by #25168 before this change, amended in this diff: its surface, replacement and reason now name composeStacks and the two metadata save doors -->
13+
14+
**BREAKING** (what the metadata API stores narrows), shipped as `minor` under the 18.x convention for breaking changes.
15+
16+
ADR-0015 §4.2 allows `object.external` only on a datasource whose `schemaMode` is not `'managed'` (an absent `schemaMode` is `'managed'` by the spec default). #25168 enforced the halves a parse and a single stack can see. A binding on a datasource stored or registered at runtime, or declared by another input of a composition, still got no verdict: the save door stored an `external` object on a managed datasource, stored a datasource re-saved as managed under objects bound to it with `external`, and `composeStacks` accepted the pair split across two inputs. The runtime then disagrees with itself about the object: its schema sync skips the table as federated while the boot validation and the write gate judge it as managed.
17+
18+
- **FROM:** `PUT /api/v1/meta/object/NAME` stored an object with `external` and a `datasource` naming a stored or registered datasource declared `schemaMode: 'managed'` or with no `schemaMode`, in draft and publish mode and under `force`. `PUT /api/v1/meta/datasource/NAME` stored a datasource as managed (or with no `schemaMode`) while stored or registered objects bound it with `external`. `composeStacks` accepted an `external` object of one input bound to a managed datasource another input declares.
19+
- **TO:**
20+
- The object save door refuses the object with `422 INVALID_METADATA`, the finding at `external`, nothing stored. The datasource it judges against is the one the door's own by-name read serves: the environment's stored row, else the definition the code layer registers.
21+
- The datasource save door refuses the datasource with `422 INVALID_METADATA`, one finding per bound object at `schemaMode`, the stored row unchanged. The objects it judges are the environment's active object rows and the registered objects no stored row overlays. An object's draft binds nothing until it is published.
22+
- Both are judged in draft and publish mode, and again when a draft is published (`publishMetaItem` and each item of a package publish), against the other half as it stands then. `force` does not skip them.
23+
- `composeStacks` refuses the cross-input pair with `STACK_CROSS_REFERENCE_INVALID` (422), in either input order and under every `manifest` strategy, `preserve` included. An input built with `strict: false` that holds the pair itself is refused here too, as with the artifact pass's other rules.
24+
- Every finding is the spec predicate's, so it reads as the parse's and `defineStack`'s do: "Object 'OBJECT' declares `external` on datasource 'NAME', whose `schemaMode` is 'managed' …".
25+
- **Unchanged:** the rule itself. A binding on a datasource declared `schemaMode: 'external'` or `'validate-only'` is accepted on every door. A datasource no layer holds gets no verdict: the absence of a definition is no evidence about its `schemaMode`. The datasource-admin service's update (`PATCH /api/v1/datasources/NAME`) does not pass through the metadata save door and is not judged by this change. Rows already stored in the refused shape still load as before; a save of either side that leaves the pair inconsistent is refused, so the next object save waits until the datasource is external or the object drops `external`.
26+
- **The fix for an author:** keep the pair consistent, in this order. To federate, save the datasource with `schemaMode: 'external'` (or `'validate-only'`) and its `external` settings first, then bind objects to it. To make a datasource managed, remove `external` from the objects that bind it first, then save the datasource.
27+
28+
This narrows the **Unchanged** bullet of the #25168 changeset, which said a binding on a datasource "registered by the host or another package, or stored at runtime" gets no verdict at the load doors: the metadata save doors now judge it, and `composeStacks` judges one another input declares.
29+
30+
`@objectstack/spec/data`: exports `externalBindingDatasourceRefusal` and its result type `ExternalBindingDatasourceRefusal`, the one predicate behind the rule, so a runtime door resolves the datasource it can see and asks it rather than copying the judgement. Its behaviour is unchanged.

‎content/docs/data-modeling/external-datasources.mdx‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,15 @@ export const Customer = ObjectSchema.create({
8989
That's it. `GET /api/v1/data/ext_customer` now returns live rows from the remote
9090
`customers` table; filters (`?region=EU`) push down to the remote query.
9191

92+
The object's **own** `datasource` must name a datasource declared
93+
`schemaMode: 'external'` or `'validate-only'` (ADR-0015 §4.2). An `external` binding on
94+
`'default'` (also the value when `datasource` is omitted) or on a `managed` datasource is
95+
refused — by `ObjectSchema.create()`, `defineStack` and `composeStacks`, and at runtime by
96+
`PUT /api/v1/meta/object/<name>` with `422 INVALID_METADATA`. The same save door refuses
97+
`PUT /api/v1/meta/datasource/<name>` that would make a datasource `managed` while objects
98+
bind it with `external`: change the datasource to `external` before binding objects, and
99+
remove `external` from the objects before making it managed.
100+
92101
<Callout type="warn">
93102
**`field.columnName` no longer exists.** It was removed from `FieldSchema` in the
94103
16.x line (#2377, ADR-0049) — the SQL driver always used the field key as the
@@ -250,7 +259,7 @@ object must opt in:
250259

251260
```typescript
252261
defineDatasource({ /* ... */ external: { allowWrites: true } }); // datasource gate
253-
ObjectSchema.create({ /* ... */ external: { remoteName: 'orders', writable: true } }); // object gate
262+
ObjectSchema.create({ /* ... */ datasource: 'warehouse', external: { remoteName: 'orders', writable: true } }); // object gate
254263
```
255264

256265
With either gate off, insert/update/delete on the federated object is rejected.

0 commit comments

Comments
 (0)