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, + }, + }, +];