Skip to content

Commit 713d405

Browse files
xuyushun441-sysos-zhuangclaude
authored
feat(datasource): expose driver catalog with config JSON Schema (#2083)
Add GET /api/v1/datasources/drivers — a curated catalog of connection drivers (memory / sqlite / postgres / mysql / mongo), each with a JSON-Schema `configSchema` describing its connection options. This is the DriverDefinitionSchema.configSchema contract ('Used by the UI to generate the connection form'), which shipped empty for every driver. The Studio datasource editor can now fetch this to render a typed connection form instead of a raw-JSON editor. Static metadata, served without a datasource-admin service dependency (always available); a future runtime driver registry can supersede the catalog without changing the route contract. Test: GET /drivers returns the catalog with sqlite's filename config schema. Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 656789e commit 713d405

3 files changed

Lines changed: 133 additions & 0 deletions

File tree

packages/services/service-datasource/src/__tests__/admin-routes.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,18 @@ describe('registerDatasourceAdminRoutes (real HonoHttpServer)', () => {
4242
expect(listDatasources).toHaveBeenCalledOnce();
4343
});
4444

45+
it('GET /api/v1/datasources/drivers returns the driver catalog with configSchema', async () => {
46+
const app = mount({}); // no service dependency — static catalog
47+
const res = await app.fetch(json('/api/v1/datasources/drivers'));
48+
expect(res.status).toBe(200);
49+
const body = (await res.json()) as { drivers: Array<{ id: string; label: string; configSchema: any }> };
50+
expect(Array.isArray(body.drivers)).toBe(true);
51+
const sqlite = body.drivers.find((d) => d.id === 'sqlite');
52+
expect(sqlite).toBeTruthy();
53+
expect(sqlite!.label).toBe('SQLite');
54+
expect(sqlite!.configSchema?.properties?.filename?.type).toBe('string');
55+
});
56+
4557
it('POST /api/v1/datasources/test splits the inline secret out of the draft', async () => {
4658
const testConnection = vi.fn().mockResolvedValue({ ok: true });
4759
const app = mount({ testConnection });

packages/services/service-datasource/src/admin-routes.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import type { PluginContext } from '@objectstack/core';
44
import type { IHttpServer } from '@objectstack/spec/contracts';
5+
import { DRIVER_CATALOG } from './driver-catalog.js';
56

67
/**
78
* Datasource lifecycle REST routes (ADR-0015 Addendum §3.5).
@@ -63,6 +64,13 @@ export function registerDatasourceAdminRoutes(
6364
res.json({ datasources });
6465
});
6566

67+
// Catalog of connection drivers + their JSON-Schema config (drives the
68+
// Studio connection form). Static metadata — no service dependency, so it
69+
// is always available even before any datasource-admin service is wired.
70+
server.get(`${root}/drivers`, async (_req: any, res: any) => {
71+
res.json({ drivers: DRIVER_CATALOG });
72+
});
73+
6674
// Probe a connection without persisting anything. Registered before the
6775
// `:name` routes so the literal `test` segment is never captured as a name.
6876
server.post(`${root}/test`, async (req: any, res: any) => {
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* Built-in datasource driver catalog.
5+
*
6+
* Each entry carries a JSON-Schema `configSchema` describing the driver's
7+
* connection options, so the Studio UI can render a typed connection form
8+
* instead of a raw-JSON editor (the `DriverDefinitionSchema.configSchema`
9+
* contract — "Used by the UI to generate the connection form").
10+
*
11+
* Served by `GET /api/v1/datasources/drivers`. This is the curated set of
12+
* connection drivers the connection form offers; a future runtime driver
13+
* registry can supersede this list without changing the route contract.
14+
*/
15+
16+
export interface DriverCatalogEntry {
17+
/** Unique driver identifier used as `datasource.driver`. */
18+
id: string;
19+
/** Display label. */
20+
label: string;
21+
/** Optional one-line description. */
22+
description?: string;
23+
/** Optional Lucide icon name. */
24+
icon?: string;
25+
/** JSON Schema (draft-2020-12) for the driver's `config` object. */
26+
configSchema: Record<string, unknown>;
27+
}
28+
29+
const SSL_PROP = {
30+
ssl: { type: 'boolean', title: 'Use SSL/TLS', default: false },
31+
} as const;
32+
33+
export const DRIVER_CATALOG: DriverCatalogEntry[] = [
34+
{
35+
id: 'memory',
36+
label: 'In-Memory',
37+
description: 'Ephemeral in-memory driver for dev, tests, and prototyping. No connection settings.',
38+
icon: 'memory-stick',
39+
configSchema: { type: 'object', properties: {}, additionalProperties: false },
40+
},
41+
{
42+
id: 'sqlite',
43+
label: 'SQLite',
44+
description: 'File-backed (or in-memory) SQL database. Great for local dev and small deployments.',
45+
icon: 'database',
46+
configSchema: {
47+
type: 'object',
48+
properties: {
49+
filename: {
50+
type: 'string',
51+
title: 'Filename',
52+
description: 'Database file path, or ":memory:" for an ephemeral in-memory database.',
53+
default: ':memory:',
54+
},
55+
},
56+
required: ['filename'],
57+
additionalProperties: false,
58+
},
59+
},
60+
{
61+
id: 'postgres',
62+
label: 'PostgreSQL',
63+
description: 'PostgreSQL connection. Supply host/port/database or a connection URL.',
64+
icon: 'database',
65+
configSchema: {
66+
type: 'object',
67+
properties: {
68+
url: { type: 'string', title: 'Connection URL', description: 'postgres://user:pass@host:5432/db (overrides the fields below when set).' },
69+
host: { type: 'string', title: 'Host', default: 'localhost' },
70+
port: { type: 'number', title: 'Port', default: 5432 },
71+
database: { type: 'string', title: 'Database' },
72+
username: { type: 'string', title: 'User' },
73+
password: { type: 'string', title: 'Password', format: 'password' },
74+
schema: { type: 'string', title: 'Schema', default: 'public' },
75+
...SSL_PROP,
76+
},
77+
additionalProperties: true,
78+
},
79+
},
80+
{
81+
id: 'mysql',
82+
label: 'MySQL / MariaDB',
83+
description: 'MySQL or MariaDB connection.',
84+
icon: 'database',
85+
configSchema: {
86+
type: 'object',
87+
properties: {
88+
host: { type: 'string', title: 'Host', default: 'localhost' },
89+
port: { type: 'number', title: 'Port', default: 3306 },
90+
database: { type: 'string', title: 'Database' },
91+
username: { type: 'string', title: 'User' },
92+
password: { type: 'string', title: 'Password', format: 'password' },
93+
...SSL_PROP,
94+
},
95+
additionalProperties: true,
96+
},
97+
},
98+
{
99+
id: 'mongo',
100+
label: 'MongoDB',
101+
description: 'MongoDB connection via a connection URI.',
102+
icon: 'database',
103+
configSchema: {
104+
type: 'object',
105+
properties: {
106+
url: { type: 'string', title: 'Connection URI', description: 'mongodb://host:27017' },
107+
database: { type: 'string', title: 'Database' },
108+
},
109+
required: ['url'],
110+
additionalProperties: true,
111+
},
112+
},
113+
];

0 commit comments

Comments
 (0)