|
| 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 | +}); |
0 commit comments