Skip to content

Commit faf8dce

Browse files
fix(runtime,service-datasource): an import over a code-defined datasource is held to its package's ADR-0028 namespace (#21906)
Fixes #21889 Clause-②: no An import over a code-defined datasource is now held to the ADR-0028 namespace of the package that declares the datasource, and the draft door answers the prefixed name. This follows triage's direction A (`5999047022`, which amends `5998041661`). There are two halves. ## What changed - **Writer (`packages/runtime/src/app-plugin.ts`).** `AppPlugin` registers each code-defined datasource through `applyProtection` (`@objectstack/spec/shared`), the stamping helper the other load paths use. The datasource is stamped with the id and version of the package body that declares it. The owner is read the way the metadata plugin's artifact door reads it (ADR-0130 D4): - `packages` absent: every datasource takes the manifest's id, the key `registerApp` installs the package under. The list read is the same one as before (D7). - `packages` present: each body's datasources take that body's id (`artifactPackageId`, in `resolveArtifactPackageOrder` order). The artifact's top-level manifest id is never stamped onto every entry (the #14599 misattribution class), and no name-to-package index is built over the flattened list. - A top-level datasource that no body declares keeps its registration under the artifact's own id, as the door's residual sweep does, and a warning names it. - **Reader (`packages/services/service-datasource/src/plugin.ts`, `getNamespace` and its docblock only).** The package record is read from the engine registry (`registry.getPackage` on the `objectql` service), the store the publish gate reads for the same check (`publishPackageDrafts`, `metadata-protocol`). It used to be asked of the `metadata` service, which holds no package records in any composition. The id comes only from the stamped `_packageId`: no guess, no fallback store, no second resolution path. The engine is resolved when it is used, like `metadata()` (the read-at-use posture). Outside `getNamespace`, `plugin.ts` changes two docblock words, so that neither docblock names package reads: the `metadata()` docblock ("every datasource read below") and the `MetadataServiceLike` docblock ("datasource definitions"). Nothing under `packages/spec`, no metadata-door change, and no new error code. ## The ruling's pins, at the real doors Measured at `8e5ff7aa` on the showcase's `showcase_external`, under `objectstack dev` (`pnpm dev -- --fresh -p 38931`) and `objectstack start` (`--compile -p 38933`, fresh database). Both gave the same readings: | door | answer | |---|---| | `POST …/external/tables/orders/import` `{"name":"probe_orders_21889"}` | `400 EXTERNAL_IMPORT_ERROR` "Object 'probe_orders_21889' is missing the package namespace prefix. Rename it to 'showcase_probe_orders_21889' (namespace = 'showcase')." `GET /meta/object/probe_orders_21889` answers `404`. | | `POST …/external/tables/orders/draft` | `200`, `name: 'showcase_orders'`, no `TODO(namespace)` in the source | | import `{"name":"showcase_probe_orders_21889"}` (control) | `201`, and `GET /data/showcase_probe_orders_21889` answers `200` with 4 rows | | import with no `name` override (the carry) | `201`, saved as `showcase_customers` | | `PATCH` / `DELETE` / `PUT /api/v1/datasources/showcase_external` (control) | `400 DATASOURCE_ADMIN_ERROR` / `400 DATASOURCE_ADMIN_ERROR` / `405 METHOD_NOT_ALLOWED`, unchanged | | `POST …/external/validate` (control) | `200`, `ok: true`, over `showcase_ext_customer` and `showcase_ext_order` | | `GET /data/showcase_ext_order` (control) | `200`, 4 rows | ## Clause-② readings - `GET /api/v1/meta/datasource`, `showcase_external`, after the change (dev and start alike): `_diagnostics, _packageId, _packageVersion, _provenance, active, autoConnect, config, driver, external, label, name, origin, schemaMode`. The values are `com.example.showcase`, `0.1.0` and `package`. Before the change the item carried none of the three (the writer ablation below, on the harness). All three are declared on `DatasourceSchema` (`...MetadataProtectionFields`). On an item with no `protection` block, `applyProtection` writes exactly those three (`shared/protection.zod.ts`), so no `_lock*` key and no further key is added. - The host `default` item is unchanged: `_diagnostics, config, driver, label, name, origin`, with no `_packageId`. - The admin list (`GET /api/v1/datasources`) is unchanged on dev and start for both items: `active, driver, label, name, origin, schemaMode, status`. - No published entry gains an export. `codeDefinedDatasourceOwners` is a private method, and `artifact-collections.ts` is not touched. ## Tests - `packages/runtime/src/app-plugin.datasource-provenance.test.ts` (new, 5 cases): the single-package bundle in both datasource spellings, the two-body ADDITIVE `packages[]` artifact and the option-B artifact (each datasource carries its own body's id and is registered once), and the residual top-level datasource (the artifact's own id, plus the warning). No in-repo example declares a datasource in a multi-package artifact, so the two-body cases are pinned here. - `packages/services/service-datasource/src/__tests__/external-namespace-reads-engine-registry.test.ts` (new, 11 cases): the namespace pins now sit on the store the reader reads. - The draft is prefixed and carries no TODO. - An unprefixed import name is refused with `code` + `status` and the shared validator's own ADR-0028 message, and nothing is saved. - A prefixed import name is saved, and an import with no override saves the derived prefixed name. - The engine is read at each use, and the start and dev orderings give the same answer. - Nothing else resolves a namespace: a `package` item in the metadata service (seeded with a different namespace) is never read, a datasource with no `_packageId` or with `sys_metadata` resolves none, a package with no namespace resolves none, and with no engine the documented fallback holds. - `external-metadata-read-at-use.test.ts`: the `package` map the metadata fake seeded (a store no composition fills) is removed. Its two namespace cases and the dev ordering's draft line moved to the file above. - `packages/qa/dogfood/test/external-import-code-datasource-namespace.dogfood.test.ts` (new, 7 cases, a real boot of the showcase): - Premise: the provenance on `GET /meta/datasource/showcase_external`. - The two ruling pins. - Controls: the prefixed import with its rows, the `default` datasource with no package, the admin service's refusals and its list (the harness mounts no admin routes, so the door's status and code are the dev/start readings above), and validate with the federated read. - The carry, as triage accepted it: - `external-import-saves-like-meta.dogfood.test.ts` moves to `showcase_dogfood_ext_cust_21788`. Its control used to import `orders` under the remote table's own name, a shape the namespace now refuses. It now imports with no `name` override and asserts the saved name `showcase_orders` across a cold boot. - `external-import-destructive-remedy.dogfood.test.ts` moves to `showcase_dogfood_ext_cust_21841` and `…_v2`. - `external-validate-sees-runtime-save.dogfood.test.ts` (landed on `main` by #21875 after this PR branched) moves its imported object to `showcase_dogfood_ext_ord_21842`. Nothing else in it changes: `SAVED` goes through `PUT /meta/object`, which runs no namespace check, and the later assertions pass on the renamed object. - A repo-wide grep for other pins of an unprefixed import, or of `TODO(namespace)`, on a datasource whose package resolves found none. The remaining draft tests drive `ExternalDatasourceService` with an injected `getNamespace`, and the REST tests mock the service. Runs at `3ec0e9f6`, the current head (it carries merges of `origin/main` at `d2ed5e04` and `374ca5cb`). Each ran through the shared verify lock, `VERDICT command-exit 0`: - `pnpm --filter @objectstack/runtime test`: 328 files passed, 4644 passed, 19 skipped. - `pnpm --filter @objectstack/service-datasource test`: 40 files, 741 passed. - `typecheck` for `runtime`, `service-datasource` and `dogfood`: exit 0. - Dogfood, 6 files (the three import files, `external-validate-sees-runtime-save`, `external-validate-start-ordering` and `showcase-external-autoconnect`): 23 passed. ## Ablations (one-time; restored by blob hash and an empty `git diff HEAD`, then rebuilt) Every leg went through `scripts/ablation-replace.mjs` (anchor hit and landed) and `scripts/ablation-dist-preflight.mjs` (the mutation reached `dist/`, and the restore removed it). - **W, writer stamp removed** (`applyProtection(…)` replaced by an unstamped copy, runtime rebuilt): - The runtime unit file went red, 5 of 5. - Dogfood went red, 3 of 7: the premise, the refusal (it answered `201` and saved `dogfood_ext_order_21889`, the defect itself), and the draft (`'orders'`). The 4 controls stayed green. - The rebuild's DTS step exited 1 on TS6133 (the now-unused `applyProtection` and `owner`). The JS was emitted, and the preflight read the stamp absent from `dist/index.js`. - The first attempt was a no-op that the tool refused (the replacement text already occurred inside the anchor). It is void, and the reading above is the second attempt. - **R, reader reverted to asking the metadata service** (service-datasource rebuilt, build exit 0): - The new service-datasource unit file went red, 8 of 11. - Dogfood went red, 2 of 7: the refusal (`201`) and the draft (`'orders'`). The premise and the controls stayed green. - **A, every body stamped with the top-level manifest's coordinates** (source-level suite, no build): the runtime unit file went red, 3 of 5 (additive, option-B and residual). The two single-package cases stayed green. ## Gates All at `3ec0e9f6`. - `node scripts/pm/dispatch-gates.mjs --ran`: 69 derived, 69 run, 0 NOT-MEASURED, 0 UNRUN. - 73 commands exit 0: those 69, the full `pnpm lint` (`eslint . --no-inline-config`), and the three PR-context checks run against this PR (`check-closing-target-claim`, `check-partof-closing-keyword`, `check-single-claim-paths`). - The dispatch's whole list (134 commands) last ran at `f7d3aac1`, and every one exits 0. Two of them (`check:dual-build-cjs-loads`, `check:published-readme-exports`) exit 0 only after a workspace build cleared their prerequisite. ## Acceptance notes - **Wider than the title, by construction.** Any datasource that carries `_packageId` now resolves its package's namespace, including one the metadata plugin's artifact door registers. That was the reader's documented intent all along. It is measured here only on `showcase_external`. - **Serial:** #21899 remains open and owns the metadata door's refusal for `origin: 'code'` datasources. Until it lands, a meta-door save can replace the stamped item and drop `_packageId`. - **#21875 landed first.** This PR carries both docblock words its reader change made false (the `metadata()` docblock and `MetadataServiceLike`), and moves #21875's runtime-save validate pin to the prefixed import name. --- _Generated by [Claude Code](https://claude.ai/code/session_01RWZbGvPFcRKvUqASZtunCU)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent f243a29 commit faf8dce

10 files changed

Lines changed: 771 additions & 57 deletions
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
'@objectstack/runtime': patch
3+
'@objectstack/service-datasource': patch
4+
---
5+
6+
An import over a code-defined datasource is held to the namespace of the package that declares it (ADR-0028), and the draft door answers the prefixed name (#21889).
7+
8+
Clause-②: no
9+
10+
- **Before.** `POST /api/v1/datasources/:name/external/tables/:remote/import` with an explicit `name` that carried no namespace prefix answered `201` and saved an unprefixed federated object, and `POST …/external/tables/:remote/draft` answered the bare remote table name with a `TODO(namespace)` note. Measured on the showcase's `showcase_external` under `objectstack dev` and `objectstack start`.
11+
- **`@objectstack/runtime`.** `AppPlugin` registers each code-defined datasource through `applyProtection` with the id and version of the package body that declares it, so the item carries `_packageId`, `_packageVersion` and `_provenance: 'package'`. On an ADR-0130 `packages[]` artifact each datasource takes its own body's id, never the artifact's top-level manifest id. A top-level datasource that no body declares keeps its registration under the artifact's own id, and a warning names it.
12+
- **`@objectstack/service-datasource`.** The federation service reads the datasource's package record from the engine registry (`registry.getPackage` on the `objectql` service), the store the runtime publish gate reads for the same check. It used to ask the `metadata` service, which holds no package records in any composition, so no datasource resolved a namespace. The package id still comes only from the stamped `_packageId`.
13+
- **What a caller sees now.** On a datasource whose package declares `manifest.namespace`, an unprefixed import `name` answers `400 EXTERNAL_IMPORT_ERROR` with ADR-0028's message, which names the prefixed name to use. An import with no `name` override saves the prefixed name the draft derives (for example `showcase_customers` instead of `customers`). `GET /api/v1/meta/datasource` lists the three provenance keys on a code-defined datasource; all three are declared on `DatasourceSchema`. The datasource admin list (`GET /api/v1/datasources`) is unchanged, and the admin door still refuses to edit or remove a code-defined datasource.
14+
- **Unchanged.** A datasource that carries no `_packageId` (the host `default` is one) and a package that declares no namespace resolve no namespace, so their imports and drafts answer as before.
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
//
3+
// [#21889, ADR-0028] An import over a CODE-DEFINED datasource is held to the
4+
// namespace of the package that declares the datasource, over the real
5+
// showcase composition.
6+
//
7+
// ## What was broken
8+
//
9+
// The showcase declares `showcase_external` in its own package, whose
10+
// `manifest.namespace` is `showcase`. `POST
11+
// /api/v1/datasources/showcase_external/external/tables/orders/import` with an
12+
// explicit `name` that carried no prefix answered `201` and persisted an
13+
// unprefixed federated object, and the draft door answered the bare remote
14+
// table name with its `TODO(namespace)` note. Two links were missing:
15+
// `AppPlugin` registered the code-defined datasource with no `_packageId`, and
16+
// the federation service then looked the package record up on the `metadata`
17+
// service, which holds none in any composition. The datasource is now stamped
18+
// with the package body that declares it, and the namespace is read from the
19+
// engine registry, the store the publish gate reads.
20+
//
21+
// ## What each case pins
22+
//
23+
// The ruling's two pins: an unprefixed import name is refused with ADR-0028's
24+
// message and saves nothing, and the draft door answers the prefixed name with
25+
// no `TODO(namespace)` note. Then the controls that show the stamp moved
26+
// nothing else: a prefixed import is saved and serves the remote rows, the
27+
// host `default` datasource carries no package, the admin service keeps
28+
// refusing edits to a code-defined datasource and lists it as before, and
29+
// validate and the existing federated objects answer as before.
30+
//
31+
// The verify harness does not mount the federation service, so this file
32+
// mounts `ExternalDatasourceServicePlugin` itself, as `objectstack dev` and
33+
// `serve` do. The working directory is a temporary one because the showcase's
34+
// external datasource and its fixture both name a cwd-relative SQLite file.
35+
36+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
37+
import showcaseStack, { onEnable } from '@objectstack/example-showcase';
38+
import { ExternalDatasourceServicePlugin } from '@objectstack/service-datasource';
39+
import { bootStack, type VerifyStack } from '@objectstack/verify';
40+
import { mkdtempSync, rmSync } from 'node:fs';
41+
import { tmpdir } from 'node:os';
42+
import { join } from 'node:path';
43+
44+
/** The showcase's code-defined external datasource (`showcase-external.datasource.ts`). */
45+
const DATASOURCE = 'showcase_external';
46+
/** The package that declares it, and that package's ADR-0028 namespace. */
47+
const PACKAGE_ID = 'com.example.showcase';
48+
const NAMESPACE = 'showcase';
49+
/** An explicit import name with no namespace prefix: the defect's shape. */
50+
const UNPREFIXED = 'dogfood_ext_order_21889';
51+
const PREFIXED = `${NAMESPACE}_${UNPREFIXED}`;
52+
53+
const ext = `/datasources/${DATASOURCE}/external`;
54+
55+
interface Envelope {
56+
success?: boolean;
57+
error?: { code?: string; message?: string };
58+
item?: Record<string, unknown>;
59+
data?: unknown;
60+
records?: Array<Record<string, unknown>>;
61+
}
62+
63+
describe('an import over a code-defined datasource is held to its package\'s namespace (showcase)', () => {
64+
let stack: VerifyStack;
65+
let token: string;
66+
let prevCwd: string;
67+
let dir: string;
68+
69+
const call = async (method: string, path: string, body?: unknown) => {
70+
const res = await stack.apiAs(token, method, path, body);
71+
const json = (await res.json().catch(() => ({}))) as Envelope;
72+
return { status: res.status, json };
73+
};
74+
75+
beforeAll(async () => {
76+
prevCwd = process.cwd();
77+
dir = mkdtempSync(join(tmpdir(), 'dogfood-21889-'));
78+
process.chdir(dir);
79+
// Provision the remote tables, exactly as `os dev` does at boot (the
80+
// harness imports only the stack's default export, so `onEnable` never
81+
// runs on its own).
82+
await onEnable({ logger: { info() {}, warn() {} } } as never);
83+
stack = await bootStack(showcaseStack, {
84+
databaseFile: join(dir, 'showcase.db'),
85+
extraPlugins: [new ExternalDatasourceServicePlugin()],
86+
});
87+
token = await stack.signIn();
88+
}, 180_000);
89+
90+
afterAll(async () => {
91+
await stack?.stop();
92+
if (prevCwd) process.chdir(prevCwd);
93+
if (dir) rmSync(dir, { recursive: true, force: true });
94+
});
95+
96+
it('premise: the code-defined datasource carries the provenance of the package that declares it', async () => {
97+
const version = (showcaseStack as unknown as { manifest?: { version?: string } }).manifest?.version;
98+
expect(version).toMatch(/^\d+\.\d+\.\d+/);
99+
100+
const read = await call('GET', `/meta/datasource/${DATASOURCE}`);
101+
expect(read.status, JSON.stringify(read.json)).toBe(200);
102+
expect(read.json.item).toMatchObject({
103+
origin: 'code',
104+
_packageId: PACKAGE_ID,
105+
_packageVersion: version,
106+
_provenance: 'package',
107+
});
108+
});
109+
110+
it('control: validate and the code-defined federated objects answer as before, ahead of any import', async () => {
111+
const validated = await call('POST', `${ext}/validate`);
112+
expect(validated.status, JSON.stringify(validated.json)).toBe(200);
113+
const report = validated.json.data as { ok?: boolean; results?: Array<{ object: string }> };
114+
expect(report.ok).toBe(true);
115+
expect(report.results?.map((r) => r.object).sort()).toEqual(['showcase_ext_customer', 'showcase_ext_order']);
116+
117+
const rows = await call('GET', '/data/showcase_ext_order');
118+
expect(rows.status, JSON.stringify(rows.json)).toBe(200);
119+
expect(rows.json.records).toHaveLength(4);
120+
});
121+
122+
it('an explicit import name without the prefix is refused with ADR-0028\'s message, and nothing is saved', async () => {
123+
const refused = await call('POST', `${ext}/tables/orders/import`, { name: UNPREFIXED });
124+
125+
expect(refused.status, JSON.stringify(refused.json)).toBe(400);
126+
expect(refused.json.error?.code).toBe('EXTERNAL_IMPORT_ERROR');
127+
expect(refused.json.error?.message).toContain('missing the package namespace prefix');
128+
expect(refused.json.error?.message).toContain(`'${PREFIXED}'`);
129+
130+
const stored = await call('GET', `/meta/object/${UNPREFIXED}`);
131+
expect(stored.status, JSON.stringify(stored.json)).toBe(404);
132+
});
133+
134+
it('the draft door answers the prefixed name, with no TODO(namespace) note', async () => {
135+
const drafted = await call('POST', `${ext}/tables/orders/draft`, {});
136+
137+
expect(drafted.status, JSON.stringify(drafted.json)).toBe(200);
138+
const draft = (drafted.json.data as { draft?: { name?: string; source?: string } } | undefined)?.draft;
139+
expect(draft?.name).toBe(`${NAMESPACE}_orders`);
140+
expect(draft?.source).not.toContain('TODO(namespace)');
141+
});
142+
143+
it('control: a prefixed import name is saved and serves the remote rows', async () => {
144+
const imported = await call('POST', `${ext}/tables/orders/import`, { name: PREFIXED });
145+
expect(imported.status, JSON.stringify(imported.json)).toBe(201);
146+
147+
const rows = await call('GET', `/data/${PREFIXED}`);
148+
expect(rows.status, JSON.stringify(rows.json)).toBe(200);
149+
expect(rows.json.records).toHaveLength(4);
150+
});
151+
152+
it('control: the host `default` datasource carries no package, so no namespace is demanded on it', async () => {
153+
const read = await call('GET', '/meta/datasource/default');
154+
expect(read.status, JSON.stringify(read.json)).toBe(200);
155+
expect(read.json.item).not.toHaveProperty('_packageId');
156+
expect(read.json.item).not.toHaveProperty('_provenance');
157+
});
158+
159+
it('control: the admin service still refuses to edit or remove a code-defined datasource, and lists no envelope key', async () => {
160+
// The harness mounts no `/api/v1/datasources` admin routes (the CLI's
161+
// `serve` does), so this reads the service those routes relay: a refusal
162+
// here is the door's `400 DATASOURCE_ADMIN_ERROR`. The refusals key on
163+
// `origin`, which the stamp leaves as it was.
164+
const admin = stack.kernel.getService<{
165+
updateDatasource(name: string, patch: Record<string, unknown>): Promise<unknown>;
166+
removeDatasource(name: string): Promise<void>;
167+
listDatasources(): Promise<Array<Record<string, unknown>>>;
168+
}>('datasource-admin');
169+
170+
await expect(admin.updateDatasource(DATASOURCE, { label: 'Renamed 21889' })).rejects.toBeInstanceOf(Error);
171+
await expect(admin.removeDatasource(DATASOURCE)).rejects.toBeInstanceOf(Error);
172+
173+
const entry = (await admin.listDatasources()).find((d) => d.name === DATASOURCE);
174+
expect(entry).toMatchObject({ origin: 'code', label: 'External Analytics (SQLite)' });
175+
expect(entry).not.toHaveProperty('_packageId');
176+
});
177+
});

‎packages/qa/dogfood/test/external-import-destructive-remedy.dogfood.test.ts‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@
3030
// mounts `ExternalDatasourceServicePlugin` itself, as `objectstack dev` and
3131
// `serve` do. The working directory is a temporary one because the showcase's
3232
// external datasource and its fixture both name a cwd-relative SQLite file.
33+
//
34+
// [#21889] Both imported names carry the showcase's ADR-0028 prefix:
35+
// `showcase_external` is declared by the showcase package (namespace
36+
// `showcase`), so an import over it is held to that namespace.
3337

3438
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
3539
import showcaseStack, { onEnable } from '@objectstack/example-showcase';
@@ -43,9 +47,9 @@ import { join } from 'node:path';
4347
const DATASOURCE = 'showcase_external';
4448
const REMOTE = 'customers';
4549
/** The object the first import creates and the re-import would shrink. */
46-
const NAME = 'dogfood_ext_cust_21841';
50+
const NAME = 'showcase_dogfood_ext_cust_21841';
4751
/** The remedy's "new `name`". */
48-
const NEW_NAME = 'dogfood_ext_cust_21841_v2';
52+
const NEW_NAME = 'showcase_dogfood_ext_cust_21841_v2';
4953
/** The column the re-import leaves out, so the stored object would lose its field. */
5054
const DROPPED = 'region';
5155
/** The re-import's options: the same table, one column fewer. */

‎packages/qa/dogfood/test/external-import-saves-like-meta.dogfood.test.ts‎

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,13 @@
3131
// `serve` do. The working directory is a temporary one because the showcase's
3232
// external datasource and its fixture both name a cwd-relative SQLite file:
3333
// this file's remote database is its own, not one a parallel file writes.
34+
//
35+
// [#21889] Both imported names carry the showcase's ADR-0028 prefix:
36+
// `showcase_external` is declared by the showcase package (namespace
37+
// `showcase`), so an import over it is held to that namespace. That retires the
38+
// shape this file's control first pinned, an object named exactly as its
39+
// remote table (`orders`), which the namespace now refuses; the control imports
40+
// with no `name` override instead, under the prefixed name the draft derives.
3441

3542
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
3643
import showcaseStack, { onEnable } from '@objectstack/example-showcase';
@@ -43,9 +50,9 @@ import { join } from 'node:path';
4350
/** The showcase's external datasource and its two remote tables (`external-fixture.ts`). */
4451
const DATASOURCE = 'showcase_external';
4552
/** Imported under a name that differs from its remote table — the defect's shape. */
46-
const RENAMED = 'dogfood_ext_cust_21788';
47-
/** Imported under the remote table's own name — the shape that hid the defect. */
48-
const SAME_NAME = 'orders';
53+
const RENAMED = 'showcase_dogfood_ext_cust_21788';
54+
/** Imported with no `name` override: the draft's name, the remote table's prefixed with the namespace. */
55+
const DERIVED = 'showcase_orders';
4956

5057
const importPath = (remote: string) => `/datasources/${DATASOURCE}/external/tables/${remote}/import`;
5158

@@ -101,11 +108,12 @@ describe('Import as Object persists like the metadata door (showcase, cold boot)
101108
);
102109
});
103110

104-
it('control: an object imported under its remote table\'s own name serves the remote rows', async () => {
105-
const imported = await call('POST', importPath('orders'), { name: SAME_NAME });
111+
it('control: an object imported with no name override is saved under the prefixed name and serves the remote rows', async () => {
112+
const imported = await call('POST', importPath('orders'), {});
106113
expect(imported.status, JSON.stringify(imported.json)).toBe(201);
114+
expect((imported.json as { data?: { object?: { name?: string } } }).data?.object?.name).toBe(DERIVED);
107115

108-
const read = await call('GET', `/data/${SAME_NAME}`);
116+
const read = await call('GET', `/data/${DERIVED}`);
109117
expect(read.status, JSON.stringify(read.json)).toBe(200);
110118
expect(recordsOf(read.json)).toHaveLength(4);
111119
});
@@ -120,8 +128,8 @@ describe('Import as Object persists like the metadata door (showcase, cold boot)
120128
expect.soft(renamed.status, JSON.stringify(renamed.json)).toBe(200);
121129
expect.soft(recordsOf(renamed.json)).toHaveLength(3);
122130

123-
const sameName = await call('GET', `/data/${SAME_NAME}`);
124-
expect.soft(sameName.status, JSON.stringify(sameName.json)).toBe(200);
125-
expect.soft(recordsOf(sameName.json)).toHaveLength(4);
131+
const derived = await call('GET', `/data/${DERIVED}`);
132+
expect.soft(derived.status, JSON.stringify(derived.json)).toBe(200);
133+
expect.soft(recordsOf(derived.json)).toHaveLength(4);
126134
}, 180_000);
127135
});

‎packages/qa/dogfood/test/external-validate-sees-runtime-save.dogfood.test.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ const SAVED = 'dogfood_ext_cust_21842';
4949
/** A column the saved object declares and the remote `customers` table does not have. */
5050
const MISSING = 'loyalty_tier';
5151
/** Imported at runtime from the remote `orders` table. */
52-
const IMPORTED = 'dogfood_ext_ord_21842';
52+
// [#21889] Prefixed: an import over `showcase_external` is held to the showcase package's namespace.
53+
const IMPORTED = 'showcase_dogfood_ext_ord_21842';
5354

5455
const validatePath = `/datasources/${DATASOURCE}/external/validate`;
5556

0 commit comments

Comments
 (0)