Skip to content

Commit d5f6d29

Browse files
xuyushun441-sysos-zhuangclaude
authored
fix(runtime): surface code-defined datasources on the host-config boot path (ADR-0015 §18) (#2157)
A datasource declared in `defineStack({ datasources: [...] })` was absent from `GET /api/v1/datasources` and `GET /api/v1/meta/datasource` under `os dev`/`serve` for any config whose `plugins` are already instantiated (`isHostConfig` true), even though the boot banner counted it and its federated objects were queryable. Root cause is write-side, not read-side: on the host-config path no MetadataPlugin loads, so the `metadata` service is the kernel's in-memory fallback, which implemented register/list/get but not `registerInMemory`. AppPlugin gates code-defined-datasource (and stack-RBAC) registration on `typeof metadata.registerInMemory === 'function'`, so the registration was silently skipped and both REST surfaces read empty. The read paths (metadata.list, datasource-admin listDatasources, protocol.getMetaItems which merges metadata.list) were already correct. Add `registerInMemory` (synchronous, no persistence — identical to register for an in-memory store) to both in-memory metadata fallbacks: @objectstack/core's createMemoryMetadata and @objectstack/plugin-dev's dev stub. Also restores stack-declared RBAC listing on this path. Adds a runtime regression test that boots the host-config shape and asserts all three backing surfaces include the code datasource. Follow-up to #2111. Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 92db3e5 commit d5f6d29

6 files changed

Lines changed: 132 additions & 0 deletions

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"@objectstack/core": patch
3+
---
4+
5+
fix(runtime): surface code-defined datasources at `GET /api/v1/datasources` and `GET /api/v1/meta/datasource` on the standalone / host-config boot path (ADR-0015 §18, follow-up to #2111).
6+
7+
A datasource declared in `defineStack({ datasources: [...] })` (e.g. the showcase's `showcase_external`) is stamped `origin: 'code'` and registered by `AppPlugin` via `metadata.registerInMemory('datasource', …)` — gated on `typeof metadata.registerInMemory === 'function'`. On the standalone / host-config path (`os dev`/`serve` for a config whose `plugins` are already instantiated — `isHostConfig` true — so no `MetadataPlugin` loads) the `metadata` service is an in-memory fallback that implemented `register`/`list`/`get` but **not** `registerInMemory`. The guard was therefore false, AppPlugin silently skipped the registration, and the datasource was absent from both REST surfaces (and Setup → Integrations → Datasources) even though the boot banner counted it and its federated objects were queryable.
8+
9+
Both in-memory `metadata` fallbacks (`@objectstack/core`'s `createMemoryMetadata` and `@objectstack/plugin-dev`'s dev stub) now implement `registerInMemory` (synchronous, no persistence — identical to `register` for these in-memory stores, matching `MetadataManager`'s signature). The read paths (`metadata.list`, datasource-admin `listDatasources`, and `protocol.getMetaItems` which merges `metadata.list`) were already correct; this restores the write-side registration they depend on. It also makes stack-declared security metadata (`roles`/`permissions`/`sharingRules`/`policies`, registered through the same guard) listable on this path.

packages/core/src/fallbacks/memory-metadata.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,19 @@ export function createMemoryMetadata() {
2525
async register(type: string, name: string, data: any): Promise<void> {
2626
getTypeMap(type).set(name, data);
2727
},
28+
// Mirror MetadataManager.registerInMemory (synchronous, no persistence).
29+
// AppPlugin gates code-defined-datasource / stack-RBAC registration on
30+
// `typeof metadata.registerInMemory === 'function'` (it must register
31+
// GitOps-managed artefacts *listably* but never persist them). Without this
32+
// method the guard was false on the host-config / standalone boot path —
33+
// where this fallback (not MetadataPlugin) provides the `metadata` service —
34+
// so `defineStack({ datasources })` entries silently never reached the
35+
// registry and were absent from GET /api/v1/datasources and
36+
// GET /api/v1/meta/datasource (ADR-0015 §18). This store is already
37+
// in-memory only, so registerInMemory and register share an implementation.
38+
registerInMemory(type: string, name: string, data: any): void {
39+
getTypeMap(type).set(name, data);
40+
},
2841
async get(type: string, name: string): Promise<any> {
2942
return getTypeMap(type).get(name);
3043
},

packages/plugins/plugin-dev/src/dev-plugin.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,18 @@ function createMetadataStub() {
210210
store.get(type)!.set(nameOrDef, data);
211211
}
212212
},
213+
// Mirror MetadataManager.registerInMemory — AppPlugin gates code-defined
214+
// datasource / stack-RBAC registration on its presence (see
215+
// packages/core/src/fallbacks/memory-metadata.ts).
216+
registerInMemory(type: string, nameOrDef: string | Record<string, any>, data?: unknown) {
217+
if (!store.has(type)) store.set(type, new Map());
218+
if (typeof nameOrDef === 'object' && nameOrDef !== null) {
219+
const key = nameOrDef.name ?? nameOrDef.id ?? 'unknown';
220+
store.get(type)!.set(key, nameOrDef);
221+
} else {
222+
store.get(type)!.set(nameOrDef, data);
223+
}
224+
},
213225
get(type: string, name: string) { return store.get(type)?.get(name); },
214226
list(type: string) { return [...(store.get(type)?.values() ?? [])]; },
215227
unregister(type: string, name: string) { store.get(type)?.delete(name); },

packages/runtime/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
"@objectstack/driver-mongodb": "workspace:*"
4343
},
4444
"devDependencies": {
45+
"@objectstack/service-datasource": "workspace:*",
4546
"typescript": "^6.0.3",
4647
"vitest": "^4.1.9"
4748
},
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
//
3+
// Reproduction + regression (ADR-0015 §18, follow-up from #2111): a code-defined
4+
// external datasource declared in `defineStack({ datasources: [...] })` — stamped
5+
// `origin: 'code'` at compile time — must be VISIBLE through the runtime metadata
6+
// surfaces on the standalone / config-load (`os dev`/`serve`) path:
7+
//
8+
// • datasource-admin `listDatasources()` -> backs GET /api/v1/datasources
9+
// • `protocol.getMetaItems({ type: 'datasource' })` -> backs GET /api/v1/meta/datasource
10+
// • the `metadata` service's `list('datasource')` -> the source both of the above read
11+
//
12+
// AppPlugin registers code datasources via `metadata.registerInMemory('datasource', ...)`
13+
// at start(). This boots the HOST-CONFIG shape (instantiated plugins, NO
14+
// MetadataPlugin) so the kernel auto-injects its in-memory `metadata` fallback —
15+
// the exact shape `examples/app-showcase` boots under `os dev` (its config carries
16+
// instantiated connector plugins, so `isHostConfig` is true and the lightweight
17+
// assembler runs instead of createStandaloneStack/MetadataPlugin).
18+
//
19+
// Before the fix, the fallback (packages/core/src/fallbacks/memory-metadata.ts)
20+
// lacked `registerInMemory`, so AppPlugin's
21+
// `typeof metadata?.registerInMemory === 'function'` guard was false and the
22+
// datasource registration was skipped entirely — both surfaces returned [].
23+
24+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
25+
import { Runtime } from './runtime.js';
26+
import { DriverPlugin } from './driver-plugin.js';
27+
import { AppPlugin } from './app-plugin.js';
28+
29+
// A minimal compiled artifact carrying ONE code-defined external datasource
30+
// (origin stamped by `defineDatasource` at compile time) plus a single object.
31+
const ARTIFACT = {
32+
manifest: { id: 'com.test.ds-visibility', name: 'DS Visibility', version: '1.0.0' },
33+
objects: [{ name: 'note', label: 'Note', fields: { title: { type: 'text' } } }],
34+
datasources: [
35+
{
36+
name: 'showcase_external',
37+
label: 'External Analytics (SQLite)',
38+
driver: 'sqlite',
39+
schemaMode: 'external',
40+
origin: 'code',
41+
config: { filename: ':memory:' },
42+
external: { allowWrites: false },
43+
active: true,
44+
},
45+
],
46+
};
47+
48+
const BOOT_TIMEOUT = 60_000;
49+
50+
describe('code-defined datasource visibility (ADR-0015 §18)', () => {
51+
let kernel: ReturnType<Runtime['getKernel']>;
52+
53+
beforeAll(async () => {
54+
const { ObjectQLPlugin } = await import('@objectstack/objectql');
55+
const { InMemoryDriver } = await import('@objectstack/driver-memory');
56+
const { DatasourceAdminServicePlugin } = await import('@objectstack/service-datasource');
57+
58+
// Host-config shape: NO MetadataPlugin — the kernel auto-injects its
59+
// in-memory `metadata` fallback (CORE_FALLBACK_FACTORIES.metadata).
60+
const runtime = new Runtime({ cluster: false });
61+
kernel = runtime.getKernel();
62+
await kernel.use(new DriverPlugin(new InMemoryDriver()));
63+
await kernel.use(new ObjectQLPlugin());
64+
await kernel.use(new AppPlugin(ARTIFACT));
65+
await kernel.use(new DatasourceAdminServicePlugin({}));
66+
await kernel.bootstrap();
67+
}, BOOT_TIMEOUT);
68+
69+
afterAll(async () => {
70+
try { await (kernel as any)?.stop?.(); } catch { /* noop */ }
71+
});
72+
73+
it('metadata.list("datasource") surfaces the code datasource (shared source)', async () => {
74+
const metadata = kernel.getService<{ list(t: string): Promise<any[]> }>('metadata');
75+
const list = await metadata.list('datasource');
76+
expect(list.map((d) => d?.name)).toContain('showcase_external');
77+
expect(list.find((d) => d?.name === 'showcase_external')?.origin).toBe('code');
78+
});
79+
80+
it('GET /api/v1/datasources backing: datasource-admin.listDatasources() includes the code datasource', async () => {
81+
const admin = kernel.getService<{ listDatasources(): Promise<any[]> }>('datasource-admin');
82+
const list = await admin.listDatasources();
83+
const ds = list.find((d) => d?.name === 'showcase_external');
84+
expect(ds).toBeDefined();
85+
expect(ds?.origin).toBe('code');
86+
});
87+
88+
it('GET /api/v1/meta/datasource backing: protocol.getMetaItems({type:"datasource"}) includes the code datasource', async () => {
89+
const protocol = kernel.getService<{ getMetaItems(r: { type: string }): Promise<any> }>('protocol');
90+
const res = await protocol.getMetaItems({ type: 'datasource' });
91+
const items: any[] = Array.isArray(res) ? res : (res?.items ?? []);
92+
expect(items.map((d) => d?.name)).toContain('showcase_external');
93+
});
94+
});

pnpm-lock.yaml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)