Skip to content

Commit 8fafcdd

Browse files
Claudehotlong
andauthored
feat: add flexible driver configuration for organization databases
- Add DriverConfig discriminated union supporting 5 driver types - Support Turso, Memory, SQL, SQLite, and Custom drivers - Update TenantDatabase schema to use driverConfig instead of hardcoded Turso fields - Update TenantProvisioningService with driver-specific provisioning methods - Add DriverFactory for runtime driver instance management - Update TenantContextService with getDriverForOrganization() method - Update sys_tenant_database object schema to store driver_config - Enable flexible deployment scenarios (dev/test/prod/enterprise) Agent-Logs-Url: https://github.com/objectstack-ai/framework/sessions/ad2ea41d-bf29-42ea-af00-fcc7d6deea5f Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent db088d2 commit 8fafcdd

6 files changed

Lines changed: 714 additions & 115 deletions

File tree

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import type { IDataDriver } from '@objectstack/spec';
4+
import type { DriverConfig } from '@objectstack/spec/cloud';
5+
6+
/**
7+
* Driver Factory Configuration
8+
*/
9+
export interface DriverFactoryConfig {
10+
/**
11+
* Available driver instances keyed by driver name
12+
* Example: { 'turso': tursoDriver, 'memory': memoryDriver }
13+
*/
14+
drivers?: Map<string, IDataDriver>;
15+
16+
/**
17+
* Driver class constructors for dynamic instantiation
18+
* Example: { 'turso': TursoDriver, 'memory': InMemoryDriver }
19+
*/
20+
driverConstructors?: Map<string, new (config: any) => IDataDriver>;
21+
}
22+
23+
/**
24+
* Driver Factory
25+
*
26+
* Manages driver instances for multi-tenant deployments.
27+
* Each organization's database uses a specific driver configuration,
28+
* and the factory creates/caches driver instances on demand.
29+
*
30+
* Features:
31+
* - Instance caching to avoid creating duplicate drivers
32+
* - Support for pre-configured drivers
33+
* - Dynamic driver instantiation
34+
* - Type-safe driver configuration
35+
*/
36+
export class DriverFactory {
37+
private driverCache = new Map<string, IDataDriver>();
38+
private config: DriverFactoryConfig;
39+
40+
constructor(config: DriverFactoryConfig = {}) {
41+
this.config = config;
42+
43+
// Pre-populate cache with provided drivers
44+
if (config.drivers) {
45+
for (const [key, driver] of config.drivers) {
46+
this.driverCache.set(key, driver);
47+
}
48+
}
49+
}
50+
51+
/**
52+
* Create or retrieve a driver instance based on configuration
53+
*
54+
* @param driverConfig - Driver configuration from TenantDatabase
55+
* @returns Driver instance ready to use
56+
*/
57+
async create(driverConfig: DriverConfig): Promise<IDataDriver> {
58+
const cacheKey = this.getCacheKey(driverConfig);
59+
60+
// Check cache first
61+
if (this.driverCache.has(cacheKey)) {
62+
return this.driverCache.get(cacheKey)!;
63+
}
64+
65+
// Create new driver instance
66+
const driver = await this.instantiateDriver(driverConfig);
67+
68+
// Cache for future use
69+
this.driverCache.set(cacheKey, driver);
70+
71+
return driver;
72+
}
73+
74+
/**
75+
* Instantiate a new driver based on configuration
76+
*/
77+
private async instantiateDriver(driverConfig: DriverConfig): Promise<IDataDriver> {
78+
switch (driverConfig.driver) {
79+
case 'turso':
80+
return this.createTursoDriver(driverConfig);
81+
82+
case 'memory':
83+
return this.createMemoryDriver(driverConfig);
84+
85+
case 'sql':
86+
return this.createSQLDriver(driverConfig);
87+
88+
case 'sqlite':
89+
return this.createSQLiteDriver(driverConfig);
90+
91+
case 'custom':
92+
return this.createCustomDriver(driverConfig);
93+
94+
default:
95+
throw new Error(`Unsupported driver type: ${(driverConfig as any).driver}`);
96+
}
97+
}
98+
99+
/**
100+
* Create Turso driver instance
101+
*/
102+
private async createTursoDriver(config: DriverConfig): Promise<IDataDriver> {
103+
if (config.driver !== 'turso') {
104+
throw new Error('Invalid driver config for Turso');
105+
}
106+
107+
// Check if constructor is available
108+
const TursoDriverConstructor = this.config.driverConstructors?.get('turso');
109+
if (!TursoDriverConstructor) {
110+
throw new Error(
111+
'Turso driver constructor not registered. Register it via DriverFactory config.',
112+
);
113+
}
114+
115+
return new TursoDriverConstructor({
116+
url: config.databaseUrl,
117+
authToken: config.authToken,
118+
syncUrl: config.syncUrl,
119+
});
120+
}
121+
122+
/**
123+
* Create Memory driver instance
124+
*/
125+
private async createMemoryDriver(config: DriverConfig): Promise<IDataDriver> {
126+
if (config.driver !== 'memory') {
127+
throw new Error('Invalid driver config for Memory');
128+
}
129+
130+
const MemoryDriverConstructor = this.config.driverConstructors?.get('memory');
131+
if (!MemoryDriverConstructor) {
132+
throw new Error(
133+
'Memory driver constructor not registered. Register it via DriverFactory config.',
134+
);
135+
}
136+
137+
return new MemoryDriverConstructor({
138+
persistent: config.persistent,
139+
dataFile: config.dataFile,
140+
});
141+
}
142+
143+
/**
144+
* Create SQL driver instance
145+
*/
146+
private async createSQLDriver(config: DriverConfig): Promise<IDataDriver> {
147+
if (config.driver !== 'sql') {
148+
throw new Error('Invalid driver config for SQL');
149+
}
150+
151+
const SQLDriverConstructor = this.config.driverConstructors?.get('sql');
152+
if (!SQLDriverConstructor) {
153+
throw new Error(
154+
'SQL driver constructor not registered. Register it via DriverFactory config.',
155+
);
156+
}
157+
158+
return new SQLDriverConstructor({
159+
dialect: config.dialect,
160+
host: config.host,
161+
port: config.port,
162+
database: config.database,
163+
username: config.username,
164+
password: config.password,
165+
ssl: config.ssl,
166+
pool: config.pool,
167+
});
168+
}
169+
170+
/**
171+
* Create SQLite driver instance
172+
*/
173+
private async createSQLiteDriver(config: DriverConfig): Promise<IDataDriver> {
174+
if (config.driver !== 'sqlite') {
175+
throw new Error('Invalid driver config for SQLite');
176+
}
177+
178+
const SQLiteDriverConstructor = this.config.driverConstructors?.get('sqlite');
179+
if (!SQLiteDriverConstructor) {
180+
throw new Error(
181+
'SQLite driver constructor not registered. Register it via DriverFactory config.',
182+
);
183+
}
184+
185+
return new SQLiteDriverConstructor({
186+
filename: config.filename,
187+
readonly: config.readonly,
188+
});
189+
}
190+
191+
/**
192+
* Create Custom driver instance
193+
*/
194+
private async createCustomDriver(config: DriverConfig): Promise<IDataDriver> {
195+
if (config.driver !== 'custom') {
196+
throw new Error('Invalid driver config for Custom');
197+
}
198+
199+
const CustomDriverConstructor = this.config.driverConstructors?.get(config.driverName);
200+
if (!CustomDriverConstructor) {
201+
throw new Error(
202+
`Custom driver '${config.driverName}' constructor not registered. Register it via DriverFactory config.`,
203+
);
204+
}
205+
206+
return new CustomDriverConstructor(config.config);
207+
}
208+
209+
/**
210+
* Generate cache key from driver configuration
211+
*/
212+
private getCacheKey(driverConfig: DriverConfig): string {
213+
// Create a unique key based on driver type and critical connection params
214+
switch (driverConfig.driver) {
215+
case 'turso':
216+
return `turso:${driverConfig.databaseUrl}`;
217+
218+
case 'memory':
219+
return `memory:${driverConfig.dataFile || 'ephemeral'}`;
220+
221+
case 'sql':
222+
return `sql:${driverConfig.dialect}:${driverConfig.host}:${driverConfig.port}:${driverConfig.database}`;
223+
224+
case 'sqlite':
225+
return `sqlite:${driverConfig.filename}`;
226+
227+
case 'custom':
228+
return `custom:${driverConfig.driverName}:${JSON.stringify(driverConfig.config)}`;
229+
230+
default:
231+
return `unknown:${JSON.stringify(driverConfig)}`;
232+
}
233+
}
234+
235+
/**
236+
* Clear driver cache (useful for testing)
237+
*/
238+
clearCache(): void {
239+
this.driverCache.clear();
240+
}
241+
242+
/**
243+
* Remove specific driver from cache
244+
*/
245+
invalidateDriver(cacheKey: string): void {
246+
this.driverCache.delete(cacheKey);
247+
}
248+
249+
/**
250+
* Get number of cached drivers
251+
*/
252+
getCacheSize(): number {
253+
return this.driverCache.size;
254+
}
255+
}

packages/services/service-tenant/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ export * from './tenant-plugin.js';
55
export * from './tenant-provisioning.js';
66
export * from './turso-platform-client.js';
77
export * from './tenant-schema-initializer.js';
8+
export * from './driver-factory.js';
89
export * from './objects/index.js';

packages/services/service-tenant/src/objects/sys-tenant-database.object.ts

Lines changed: 8 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import { ObjectSchema, Field } from '@objectstack/spec/data';
55
/**
66
* sys_tenant_database — Global Tenant Registry Object
77
*
8-
* Stores tenant database information in the global control plane.
9-
* Each tenant has its own isolated Turso database with UUID-based naming.
8+
* Stores tenant database configuration in the global control plane.
9+
* Each tenant can use different database drivers (Turso, Memory, SQL, SQLite, Custom).
1010
*
1111
* @namespace sys
1212
*/
@@ -17,9 +17,9 @@ export const SysTenantDatabase = ObjectSchema.create({
1717
pluralLabel: 'Tenant Databases',
1818
icon: 'database',
1919
isSystem: true,
20-
description: 'Tenant database registry for multi-tenant architecture',
21-
titleFormat: '{database_name}',
22-
compactLayout: ['database_name', 'organization_id', 'status', 'plan'],
20+
description: 'Tenant database registry with flexible driver configuration',
21+
titleFormat: '{id}',
22+
compactLayout: ['id', 'organization_id', 'status', 'plan'],
2323

2424
fields: {
2525
id: Field.text({
@@ -47,24 +47,10 @@ export const SysTenantDatabase = ObjectSchema.create({
4747
description: 'Foreign key to sys_organization',
4848
}),
4949

50-
database_name: Field.text({
51-
label: 'Database Name',
50+
driver_config: Field.textarea({
51+
label: 'Driver Configuration',
5252
required: true,
53-
maxLength: 255,
54-
description: 'UUID-based database name (immutable)',
55-
}),
56-
57-
database_url: Field.url({
58-
label: 'Database URL',
59-
required: true,
60-
description: 'Full database connection URL (e.g., libsql://{uuid}.turso.io)',
61-
}),
62-
63-
auth_token: Field.text({
64-
label: 'Auth Token',
65-
required: true,
66-
maxLength: 2000,
67-
description: 'Encrypted database-specific auth token',
53+
description: 'JSON-serialized driver configuration (type: turso|memory|sql|sqlite|custom)',
6854
}),
6955

7056
status: Field.picklist({
@@ -80,13 +66,6 @@ export const SysTenantDatabase = ObjectSchema.create({
8066
defaultValue: 'provisioning',
8167
}),
8268

83-
region: Field.text({
84-
label: 'Region',
85-
required: true,
86-
maxLength: 100,
87-
description: 'Deployment region (e.g., us-east-1, eu-west-1)',
88-
}),
89-
9069
plan: Field.picklist({
9170
label: 'Plan',
9271
required: true,
@@ -121,7 +100,6 @@ export const SysTenantDatabase = ObjectSchema.create({
121100
},
122101

123102
indexes: [
124-
{ fields: ['database_name'], unique: true },
125103
{ fields: ['organization_id'] },
126104
{ fields: ['status'] },
127105
{ fields: ['plan'] },

0 commit comments

Comments
 (0)