From 0c960f096cb61c95423eadec06348e150e733f4f Mon Sep 17 00:00:00 2001 From: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Date: Sun, 21 Jun 2026 09:55:43 +0800 Subject: [PATCH] feat(datasource): expose driver catalog with config JSON Schema MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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: Claude Opus 4.8 --- .../src/__tests__/admin-routes.test.ts | 12 ++ .../service-datasource/src/admin-routes.ts | 8 ++ .../service-datasource/src/driver-catalog.ts | 113 ++++++++++++++++++ 3 files changed, 133 insertions(+) create mode 100644 packages/services/service-datasource/src/driver-catalog.ts diff --git a/packages/services/service-datasource/src/__tests__/admin-routes.test.ts b/packages/services/service-datasource/src/__tests__/admin-routes.test.ts index fafa8c3a6f..bef2045582 100644 --- a/packages/services/service-datasource/src/__tests__/admin-routes.test.ts +++ b/packages/services/service-datasource/src/__tests__/admin-routes.test.ts @@ -42,6 +42,18 @@ describe('registerDatasourceAdminRoutes (real HonoHttpServer)', () => { expect(listDatasources).toHaveBeenCalledOnce(); }); + it('GET /api/v1/datasources/drivers returns the driver catalog with configSchema', async () => { + const app = mount({}); // no service dependency — static catalog + const res = await app.fetch(json('/api/v1/datasources/drivers')); + expect(res.status).toBe(200); + const body = (await res.json()) as { drivers: Array<{ id: string; label: string; configSchema: any }> }; + expect(Array.isArray(body.drivers)).toBe(true); + const sqlite = body.drivers.find((d) => d.id === 'sqlite'); + expect(sqlite).toBeTruthy(); + expect(sqlite!.label).toBe('SQLite'); + expect(sqlite!.configSchema?.properties?.filename?.type).toBe('string'); + }); + it('POST /api/v1/datasources/test splits the inline secret out of the draft', async () => { const testConnection = vi.fn().mockResolvedValue({ ok: true }); const app = mount({ testConnection }); diff --git a/packages/services/service-datasource/src/admin-routes.ts b/packages/services/service-datasource/src/admin-routes.ts index 8bac1551ec..f39225d75b 100644 --- a/packages/services/service-datasource/src/admin-routes.ts +++ b/packages/services/service-datasource/src/admin-routes.ts @@ -2,6 +2,7 @@ import type { PluginContext } from '@objectstack/core'; import type { IHttpServer } from '@objectstack/spec/contracts'; +import { DRIVER_CATALOG } from './driver-catalog.js'; /** * Datasource lifecycle REST routes (ADR-0015 Addendum §3.5). @@ -63,6 +64,13 @@ export function registerDatasourceAdminRoutes( res.json({ datasources }); }); + // Catalog of connection drivers + their JSON-Schema config (drives the + // Studio connection form). Static metadata — no service dependency, so it + // is always available even before any datasource-admin service is wired. + server.get(`${root}/drivers`, async (_req: any, res: any) => { + res.json({ drivers: DRIVER_CATALOG }); + }); + // Probe a connection without persisting anything. Registered before the // `:name` routes so the literal `test` segment is never captured as a name. server.post(`${root}/test`, async (req: any, res: any) => { diff --git a/packages/services/service-datasource/src/driver-catalog.ts b/packages/services/service-datasource/src/driver-catalog.ts new file mode 100644 index 0000000000..0d275717fc --- /dev/null +++ b/packages/services/service-datasource/src/driver-catalog.ts @@ -0,0 +1,113 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * Built-in datasource driver catalog. + * + * Each entry carries a JSON-Schema `configSchema` describing the driver's + * connection options, so the Studio UI can render a typed connection form + * instead of a raw-JSON editor (the `DriverDefinitionSchema.configSchema` + * contract — "Used by the UI to generate the connection form"). + * + * Served by `GET /api/v1/datasources/drivers`. This is the curated set of + * connection drivers the connection form offers; a future runtime driver + * registry can supersede this list without changing the route contract. + */ + +export interface DriverCatalogEntry { + /** Unique driver identifier used as `datasource.driver`. */ + id: string; + /** Display label. */ + label: string; + /** Optional one-line description. */ + description?: string; + /** Optional Lucide icon name. */ + icon?: string; + /** JSON Schema (draft-2020-12) for the driver's `config` object. */ + configSchema: Record; +} + +const SSL_PROP = { + ssl: { type: 'boolean', title: 'Use SSL/TLS', default: false }, +} as const; + +export const DRIVER_CATALOG: DriverCatalogEntry[] = [ + { + id: 'memory', + label: 'In-Memory', + description: 'Ephemeral in-memory driver for dev, tests, and prototyping. No connection settings.', + icon: 'memory-stick', + configSchema: { type: 'object', properties: {}, additionalProperties: false }, + }, + { + id: 'sqlite', + label: 'SQLite', + description: 'File-backed (or in-memory) SQL database. Great for local dev and small deployments.', + icon: 'database', + configSchema: { + type: 'object', + properties: { + filename: { + type: 'string', + title: 'Filename', + description: 'Database file path, or ":memory:" for an ephemeral in-memory database.', + default: ':memory:', + }, + }, + required: ['filename'], + additionalProperties: false, + }, + }, + { + id: 'postgres', + label: 'PostgreSQL', + description: 'PostgreSQL connection. Supply host/port/database or a connection URL.', + icon: 'database', + configSchema: { + type: 'object', + properties: { + url: { type: 'string', title: 'Connection URL', description: 'postgres://user:pass@host:5432/db (overrides the fields below when set).' }, + host: { type: 'string', title: 'Host', default: 'localhost' }, + port: { type: 'number', title: 'Port', default: 5432 }, + database: { type: 'string', title: 'Database' }, + username: { type: 'string', title: 'User' }, + password: { type: 'string', title: 'Password', format: 'password' }, + schema: { type: 'string', title: 'Schema', default: 'public' }, + ...SSL_PROP, + }, + additionalProperties: true, + }, + }, + { + id: 'mysql', + label: 'MySQL / MariaDB', + description: 'MySQL or MariaDB connection.', + icon: 'database', + configSchema: { + type: 'object', + properties: { + host: { type: 'string', title: 'Host', default: 'localhost' }, + port: { type: 'number', title: 'Port', default: 3306 }, + database: { type: 'string', title: 'Database' }, + username: { type: 'string', title: 'User' }, + password: { type: 'string', title: 'Password', format: 'password' }, + ...SSL_PROP, + }, + additionalProperties: true, + }, + }, + { + id: 'mongo', + label: 'MongoDB', + description: 'MongoDB connection via a connection URI.', + icon: 'database', + configSchema: { + type: 'object', + properties: { + url: { type: 'string', title: 'Connection URI', description: 'mongodb://host:27017' }, + database: { type: 'string', title: 'Database' }, + }, + required: ['url'], + additionalProperties: true, + }, + }, +];