Skip to content

Commit 03234e2

Browse files
committed
chore(spec): changeset for the external-binding refusal; the strictness fixtures name their federated datasource
Claude-Session: https://claude.ai/code/session_013dX1BHfcVSJNSkfA6cvwJN Co-authored-by: Claude <noreply@anthropic.com>
1 parent 1ac50ac commit 03234e2

2 files changed

Lines changed: 28 additions & 3 deletions

File tree

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
fix(spec)!: an object's `external` binding on a managed datasource is refused at the parse and by `defineStack`, as ADR-0015 §4.2 declares
6+
7+
Clause-②: no (narrowing)
8+
9+
Metadata-protocol: narrowed
10+
11+
<!-- adr-0087: registered object-external-binding-managed-datasource-refused -->
12+
13+
**BREAKING** (what an author can declare narrows), shipped as `minor` under the launch-window convention for breaking changes.
14+
15+
ADR-0015 §4.2 allows `object.external` only on a datasource whose `schemaMode` is not `'managed'`, and declares the rule "enforced at metadata-load time". No load door enforced it: `defineStack`, `os validate` and the object save door (`PUT /api/v1/meta/object/NAME`) all accepted an `external` binding on a managed datasource, the primary `default` included. The runtime then disagreed with itself about such an object: its schema sync treated it as federated and never created its table, while the boot validation skipped it and the write gate exempted it because its datasource is managed.
16+
17+
- **FROM:** an object declaring `external` was accepted whatever its datasource: `datasource` omitted or `'default'`, or a datasource declared with `schemaMode: 'managed'` or with no `schemaMode`.
18+
- **TO:** the rule is judged where each half is visible.
19+
- An `external` binding whose object `datasource` is `'default'` (also the value when none is written) is refused by `ObjectSchema`'s parse at `external`: `ObjectSchema.create` throws at module load, `defineStack` and `os validate` refuse with `STACK_SCHEMA_INVALID` (422), and the object save door answers `422 INVALID_METADATA` and stores nothing. The platform's primary datasource is managed by construction.
20+
- An `external` binding on a datasource the same stack declares with `schemaMode: 'managed'`, or with no `schemaMode`, is refused by `defineStack` (and so `os validate` and `os build`) with `STACK_CROSS_REFERENCE_INVALID` (422), one finding per object.
21+
- A package `defaultDatasource` or a `datasourceMapping` rule does not make an object federated: they route its queries, while the boot validation and the write gate read the object's own `datasource`.
22+
- **Unchanged:** a binding on a datasource declared `schemaMode: 'external'` or `'validate-only'` is accepted. A binding on a datasource the stack does not declare (registered by the host or another package, or stored at runtime) gets no verdict at these doors, because they cannot see its `schemaMode`. `defineStack(config, { strict: false })` skips the stack-level judgement by contract. A stored object row in the refused shape still registers, under a `[metadata_spec_invalid]` diagnostic, and its next save is refused; `os migrate meta --stored` lists each such row under `object-external-binding-managed-datasource-refused`.
23+
- **The fix for an author:** set the object's `datasource` to the datasource that holds the remote table and declare that datasource with `schemaMode: 'external'` (or `'validate-only'`) and its `external` settings, for example `datasource: 'warehouse'` beside `defineDatasource({ name: 'warehouse', schemaMode: 'external', external: { … } })`. An object whose rows ObjectStack should own drops `external` instead.

‎packages/spec/src/data/object-strictness-batch20.test.ts‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,8 @@ describe('批 20, unknown keys refused — the doors these shapes are reachable
112112
access: { default: 'private' },
113113
lifecycle: { class: 'audit', retention: { maxAge: '7y' } },
114114
fieldGroups: [{ key: 'contact_info', label: 'Contact' }],
115+
// ADR-0015 §4.2: a binding names its non-managed datasource on the object.
116+
datasource: 'remote_crm',
115117
external: { remoteName: 'remote_contacts', writable: true },
116118
userActions: { create: false, exportCsv: true },
117119
systemFields: { tenant: false, audit: true },
@@ -169,7 +171,7 @@ describe('批 20 — closed sites reject unknown keys where they live', () => {
169171

170172
it('`external` — the ADR-0015 federated binding', () => {
171173
accept(ObjectExternalBindingSchema, { remoteName: 'remote_contacts', writable: true });
172-
expect(rejectOnObject({ external: { remoteName: 'r', notABindingKey: 1 } })).toContain('notABindingKey');
174+
expect(rejectOnObject({ datasource: 'remote_crm', external: { remoteName: 'r', notABindingKey: 1 } })).toContain('notABindingKey');
173175
});
174176

175177
it('`userActions` — the CRUD affordance overrides', () => {
@@ -402,7 +404,7 @@ describe('批 20, unknown keys refused — curation is anchored to the sibling c
402404
});
403405

404406
it('`external.allowWrites` names the DOUBLE opt-in — the datasource half and the object half', () => {
405-
const msg = rejectOnObject({ external: { remoteName: 'r', allowWrites: true } });
407+
const msg = rejectOnObject({ datasource: 'remote_crm', external: { remoteName: 'r', allowWrites: true } });
406408
expect(msg).toContain('writable');
407409
expect(msg).toContain('datasource');
408410
accept(ObjectExternalBindingSchema, { remoteName: 'r', writable: true });
@@ -510,7 +512,7 @@ describe('批 20, unknown keys refused — curation is anchored to the sibling c
510512
['userActions.export → exportCsv', { userActions: { export: true } }, 'exportCsv'],
511513
['publicSharing.audiences → allowedAudiences', { publicSharing: { enabled: true, audiences: [] } }, 'allowedAudiences'],
512514
['publicSharing.redact → redactFields', { publicSharing: { enabled: true, redact: [] } }, 'redactFields'],
513-
['external.table → remoteName', { external: { table: 't' } }, 'remoteName'],
515+
['external.table → remoteName', { datasource: 'remote_crm', external: { table: 't' } }, 'remoteName'],
514516
['fieldGroups[].title → label', { fieldGroups: [{ key: 'g', label: 'G', title: 'T' }] }, 'label'],
515517
])('%s', (_name, patch, expected) => {
516518
expect(rejectOnObject(patch)).toContain(expected);

0 commit comments

Comments
 (0)