Files
smartdb/ts/ts_smartdb/server/SmartdbServer.ts
T

374 lines
10 KiB
TypeScript

import { RustDbBridge } from '../rust-db-bridge.js';
import { StorageMigrator } from '../../ts_migration/index.js';
import type {
IOpLogEntry,
IOpLogResult,
IOpLogStats,
IRevertResult,
ICollectionInfo,
IDocumentsResult,
ISmartDbMetrics,
} from '../rust-db-bridge.js';
import type {
ISmartDbHealth,
ISmartDbDatabaseTenantInput,
ISmartDbDeleteDatabaseTenantInput,
ISmartDbRotateDatabaseTenantPasswordInput,
ISmartDbDatabaseTenantDescriptor,
ISmartDbDeleteDatabaseTenantResult,
ISmartDbDatabaseExport,
ISmartDbImportDatabaseInput,
ISmartDbImportDatabaseResult,
} from '../service-types.js';
/**
* Server configuration options
*/
export interface ISmartdbServerOptions {
/** Port to listen on (default: 27017) - ignored if socketPath is set */
port?: number;
/** Host to bind to (default: 127.0.0.1) - ignored if socketPath is set */
host?: string;
/** Unix socket path - if set, server listens on socket instead of TCP */
socketPath?: string;
/** Storage type: 'memory' or 'file' (default: 'memory') */
storage?: 'memory' | 'file';
/** Path for file storage (required if storage is 'file') */
storagePath?: string;
/** Enable persistence for memory storage */
persistPath?: string;
/** Persistence interval in ms (default: 60000) */
persistIntervalMs?: number;
/** Authentication configuration. Disabled by default. */
auth?: ISmartdbAuthOptions;
/** TLS transport configuration for TCP listeners. Disabled by default. */
tls?: ISmartdbTlsOptions;
}
export interface ISmartdbAuthOptions {
enabled?: boolean;
users?: ISmartdbAuthUser[];
usersPath?: string;
scramIterations?: number;
}
export interface ISmartdbAuthUser {
username: string;
password: string;
database?: string;
roles?: string[];
}
export interface ISmartdbTlsOptions {
enabled?: boolean;
certPath?: string;
keyPath?: string;
caPath?: string;
requireClientCert?: boolean;
}
/**
* SmartdbServer - Wire protocol compatible database server backed by Rust
*
* This server implements the wire protocol to allow official drivers to
* connect and perform operations. The core engine runs as a Rust sidecar
* binary managed via @push.rocks/smartrust IPC.
*
* @example
* ```typescript
* import { SmartdbServer } from '@push.rocks/smartdb';
* import { MongoClient } from 'mongodb';
*
* const server = new SmartdbServer({ port: 27017 });
* await server.start();
*
* const client = new MongoClient(server.getConnectionUri());
* await client.connect();
* ```
*/
export class SmartdbServer {
private options: ISmartdbServerOptions;
private bridge: RustDbBridge;
private isRunning = false;
private resolvedConnectionUri = '';
constructor(options: ISmartdbServerOptions = {}) {
this.options = {
port: options.port ?? 27017,
host: options.host ?? '127.0.0.1',
socketPath: options.socketPath,
storage: options.storage ?? 'memory',
storagePath: options.storagePath ?? './data',
persistPath: options.persistPath,
persistIntervalMs: options.persistIntervalMs ?? 60000,
auth: options.auth,
tls: options.tls,
};
this.bridge = new RustDbBridge();
}
/**
* Start the server
*/
async start(): Promise<void> {
if (this.isRunning) {
throw new Error('Server is already running');
}
// Run storage migration for file-based storage before starting Rust engine
if (this.options.storage === 'file' && this.options.storagePath) {
const migrator = new StorageMigrator(this.options.storagePath);
await migrator.run();
}
const spawned = await this.bridge.spawn();
if (!spawned) {
throw new Error(
'smartdb Rust binary not found. Set SMARTDB_RUST_BINARY env var, ' +
'install the platform package, or build locally with `tsrust`.'
);
}
// Forward unexpected exit
this.bridge.on('exit', (code: number | null, signal: string | null) => {
if (this.isRunning) {
console.error(`smartdb Rust process exited unexpectedly (code=${code}, signal=${signal})`);
}
});
// Send config, get back connectionUri
const result = await this.bridge.startDb({
port: this.options.port,
host: this.options.host,
socketPath: this.options.socketPath,
storage: this.options.storage ?? 'memory',
storagePath: this.options.storagePath,
persistPath: this.options.persistPath,
persistIntervalMs: this.options.persistIntervalMs,
auth: this.options.auth,
tls: this.options.tls,
});
this.resolvedConnectionUri = result.connectionUri;
this.isRunning = true;
}
/**
* Stop the server
*/
async stop(): Promise<void> {
if (!this.isRunning) {
return;
}
try {
await this.bridge.stopDb();
} catch {
// Bridge may already be dead
}
this.bridge.kill();
this.isRunning = false;
}
/**
* Get the connection URI for this server
*/
getConnectionUri(): string {
if (this.resolvedConnectionUri) {
return this.resolvedConnectionUri;
}
// Fallback: compute from options
if (this.options.socketPath) {
const encodedPath = encodeURIComponent(this.options.socketPath);
return `mongodb://${encodedPath}`;
}
const baseUri = `mongodb://${this.options.host ?? '127.0.0.1'}:${this.options.port ?? 27017}`;
return this.options.tls?.enabled ? `${baseUri}/?tls=true` : baseUri;
}
/**
* Get the socket path (if using Unix socket mode)
*/
get socketPath(): string | undefined {
return this.options.socketPath;
}
/**
* Check if the server is running
*/
get running(): boolean {
return this.isRunning;
}
/**
* Get the port the server is listening on
*/
get port(): number {
return this.options.port ?? 27017;
}
/**
* Get the host the server is bound to
*/
get host(): string {
return this.options.host ?? '127.0.0.1';
}
/**
* Create an isolated database/user pair for an application tenant.
*/
async createDatabaseTenant(
params: ISmartDbDatabaseTenantInput,
): Promise<ISmartDbDatabaseTenantDescriptor> {
const descriptor = await this.bridge.createDatabaseTenant(params);
return this.withTenantMongoUri(descriptor, params.password);
}
/**
* Delete a tenant database and its tenant user(s).
*/
async deleteDatabaseTenant(
params: ISmartDbDeleteDatabaseTenantInput,
): Promise<ISmartDbDeleteDatabaseTenantResult> {
return this.bridge.deleteDatabaseTenant(params);
}
/**
* Rotate a tenant user's password without restarting the server.
*/
async rotateDatabaseTenantPassword(
params: ISmartDbRotateDatabaseTenantPasswordInput,
): Promise<ISmartDbDatabaseTenantDescriptor> {
const descriptor = await this.bridge.rotateDatabaseTenantPassword(params);
return this.withTenantMongoUri(descriptor, params.password);
}
/**
* List known database tenants.
*/
async listDatabaseTenants(): Promise<ISmartDbDatabaseTenantDescriptor[]> {
return this.bridge.listDatabaseTenants();
}
/**
* Get a tenant descriptor without exposing a password.
*/
async getDatabaseTenantDescriptor(params: {
databaseName: string;
username: string;
}): Promise<ISmartDbDatabaseTenantDescriptor> {
return this.bridge.getDatabaseTenantDescriptor(params);
}
/**
* Export one database as an Extended JSON snapshot.
*/
async exportDatabase(params: { databaseName: string }): Promise<ISmartDbDatabaseExport> {
return this.bridge.exportDatabase(params);
}
/**
* Replace one database with a previously exported snapshot.
*/
async importDatabase(params: ISmartDbImportDatabaseInput): Promise<ISmartDbImportDatabaseResult> {
return this.bridge.importDatabase(params);
}
/**
* Get readiness/health details for long-running service use.
*/
async getHealth(): Promise<ISmartDbHealth> {
if (!this.isRunning) {
return {
running: false,
storage: this.options.storage,
storagePath: this.options.storage === 'file' ? this.options.storagePath : this.options.persistPath,
authEnabled: Boolean(this.options.auth?.enabled),
authUsers: this.options.auth?.users?.length ?? 0,
usersPathConfigured: Boolean(this.options.auth?.usersPath),
databaseCount: 0,
collectionCount: 0,
};
}
return this.bridge.getHealth();
}
// --- OpLog / Debug API ---
/**
* Get oplog entries, optionally filtered.
*/
async getOpLog(params: {
sinceSeq?: number;
limit?: number;
db?: string;
collection?: string;
} = {}): Promise<IOpLogResult> {
return this.bridge.getOpLog(params);
}
/**
* Get aggregate oplog statistics.
*/
async getOpLogStats(): Promise<IOpLogStats> {
return this.bridge.getOpLogStats();
}
/**
* Revert database state to a specific oplog sequence number.
* Use dryRun=true to preview which entries would be reverted.
*/
async revertToSeq(seq: number, dryRun = false): Promise<IRevertResult> {
return this.bridge.revertToSeq(seq, dryRun);
}
/**
* List all collections across all databases, with document counts.
*/
async getCollections(db?: string): Promise<ICollectionInfo[]> {
return this.bridge.getCollections(db);
}
/**
* Get documents from a collection with pagination.
*/
async getDocuments(
db: string,
collection: string,
limit = 50,
skip = 0,
): Promise<IDocumentsResult> {
return this.bridge.getDocuments(db, collection, limit, skip);
}
/**
* Get server metrics including database/collection counts and oplog info.
*/
async getMetrics(): Promise<ISmartDbMetrics> {
return this.bridge.getMetrics();
}
private withTenantMongoUri(
descriptor: ISmartDbDatabaseTenantDescriptor,
password: string,
): ISmartDbDatabaseTenantDescriptor {
return {
...descriptor,
mongodbUri: this.buildTenantMongoUri(descriptor.databaseName, descriptor.username, password),
};
}
private buildTenantMongoUri(databaseName: string, username: string, password: string): string {
const host = this.options.socketPath
? encodeURIComponent(this.options.socketPath)
: `${this.options.host ?? '127.0.0.1'}:${this.options.port ?? 27017}`;
const auth = `${encodeURIComponent(username)}:${encodeURIComponent(password)}@`;
const query = new URLSearchParams({ authSource: databaseName });
if (this.options.tls?.enabled) {
query.set('tls', 'true');
}
return `mongodb://${auth}${host}/${encodeURIComponent(databaseName)}?${query.toString()}`;
}
}