Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand Down
8 changes: 8 additions & 0 deletions packages/services/service-datasource/src/admin-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -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) => {
Expand Down
113 changes: 113 additions & 0 deletions packages/services/service-datasource/src/driver-catalog.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown>;
}

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