feat: Implement platform service providers for MinIO and MongoDB
- Added base interface and abstract class for platform service providers. - Created MinIOProvider class for S3-compatible storage with deployment, provisioning, and deprovisioning functionalities. - Implemented MongoDBProvider class for MongoDB service with similar capabilities. - Introduced error handling utilities for better error management. - Developed TokensComponent for managing registry tokens in the UI, including creation, deletion, and display of tokens.
This commit is contained in:
@@ -6,6 +6,7 @@ import * as plugins from '../plugins.ts';
|
||||
import type {
|
||||
IService,
|
||||
IRegistry,
|
||||
IRegistryToken,
|
||||
INginxConfig,
|
||||
ISslCertificate,
|
||||
IDnsRecord,
|
||||
@@ -13,11 +14,22 @@ import type {
|
||||
ILogEntry,
|
||||
IUser,
|
||||
ISetting,
|
||||
IPlatformService,
|
||||
IPlatformResource,
|
||||
IPlatformRequirements,
|
||||
TPlatformServiceType,
|
||||
IDomain,
|
||||
ICertificate,
|
||||
ICertRequirement,
|
||||
} from '../types.ts';
|
||||
|
||||
// Type alias for sqlite bind parameters
|
||||
type BindValue = string | number | bigint | boolean | null | undefined | Uint8Array;
|
||||
import { logger } from '../logging.ts';
|
||||
import { getErrorMessage } from '../utils/error.ts';
|
||||
|
||||
export class OneboxDatabase {
|
||||
private db: plugins.sqlite.DB | null = null;
|
||||
private db: InstanceType<typeof plugins.sqlite.DB> | null = null;
|
||||
private dbPath: string;
|
||||
|
||||
constructor(dbPath = './.nogit/onebox.db') {
|
||||
@@ -43,7 +55,7 @@ export class OneboxDatabase {
|
||||
// Run migrations if needed
|
||||
await this.runMigrations();
|
||||
} catch (error) {
|
||||
logger.error(`Failed to initialize database: ${error.message}`);
|
||||
logger.error(`Failed to initialize database: ${getErrorMessage(error)}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -447,28 +459,40 @@ export class OneboxDatabase {
|
||||
|
||||
// 4. Migrate existing ssl_certificates data
|
||||
// Extract unique base domains from existing certificates
|
||||
const existingCerts = this.query('SELECT * FROM ssl_certificates');
|
||||
interface OldSslCert {
|
||||
id?: number;
|
||||
domain?: string;
|
||||
cert_path?: string;
|
||||
key_path?: string;
|
||||
full_chain_path?: string;
|
||||
expiry_date?: number;
|
||||
issuer?: string;
|
||||
created_at?: number;
|
||||
updated_at?: number;
|
||||
[key: number]: unknown; // Allow array-style access as fallback
|
||||
}
|
||||
const existingCerts = this.query<OldSslCert>('SELECT * FROM ssl_certificates');
|
||||
|
||||
const now = Date.now();
|
||||
const domainMap = new Map<string, number>();
|
||||
|
||||
// Create domain entries for each unique base domain
|
||||
for (const cert of existingCerts) {
|
||||
const domain = String(cert.domain ?? cert[1]);
|
||||
const domain = String(cert.domain ?? (cert as Record<number, unknown>)[1]);
|
||||
if (!domainMap.has(domain)) {
|
||||
this.query(
|
||||
'INSERT INTO domains (domain, dns_provider, is_obsolete, default_wildcard, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)',
|
||||
[domain, null, 0, 1, now, now]
|
||||
);
|
||||
const result = this.query('SELECT last_insert_rowid() as id');
|
||||
const domainId = result[0].id ?? result[0][0];
|
||||
const result = this.query<{ id?: number; [key: number]: unknown }>('SELECT last_insert_rowid() as id');
|
||||
const domainId = result[0].id ?? (result[0] as Record<number, unknown>)[0];
|
||||
domainMap.set(domain, Number(domainId));
|
||||
}
|
||||
}
|
||||
|
||||
// Migrate certificates to new table
|
||||
for (const cert of existingCerts) {
|
||||
const domain = String(cert.domain ?? cert[1]);
|
||||
const domain = String(cert.domain ?? (cert as Record<number, unknown>)[1]);
|
||||
const domainId = domainMap.get(domain);
|
||||
|
||||
this.query(
|
||||
@@ -480,14 +504,14 @@ export class OneboxDatabase {
|
||||
domainId,
|
||||
domain,
|
||||
0, // We don't know if it's wildcard, default to false
|
||||
String(cert.cert_path ?? cert[2]),
|
||||
String(cert.key_path ?? cert[3]),
|
||||
String(cert.full_chain_path ?? cert[4]),
|
||||
Number(cert.expiry_date ?? cert[5]),
|
||||
String(cert.issuer ?? cert[6]),
|
||||
String(cert.cert_path ?? (cert as Record<number, unknown>)[2]),
|
||||
String(cert.key_path ?? (cert as Record<number, unknown>)[3]),
|
||||
String(cert.full_chain_path ?? (cert as Record<number, unknown>)[4]),
|
||||
Number(cert.expiry_date ?? (cert as Record<number, unknown>)[5]),
|
||||
String(cert.issuer ?? (cert as Record<number, unknown>)[6]),
|
||||
1, // Assume valid
|
||||
Number(cert.created_at ?? cert[7]),
|
||||
Number(cert.updated_at ?? cert[8])
|
||||
Number(cert.created_at ?? (cert as Record<number, unknown>)[7]),
|
||||
Number(cert.updated_at ?? (cert as Record<number, unknown>)[8])
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -534,9 +558,143 @@ export class OneboxDatabase {
|
||||
this.setMigrationVersion(4);
|
||||
logger.success('Migration 4 completed: Onebox Registry columns added to services table');
|
||||
}
|
||||
|
||||
// Migration 5: Registry tokens table
|
||||
const version5 = this.getMigrationVersion();
|
||||
if (version5 < 5) {
|
||||
logger.info('Running migration 5: Creating registry_tokens table...');
|
||||
|
||||
this.query(`
|
||||
CREATE TABLE registry_tokens (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
token_hash TEXT NOT NULL UNIQUE,
|
||||
token_type TEXT NOT NULL,
|
||||
scope TEXT NOT NULL,
|
||||
expires_at REAL,
|
||||
created_at REAL NOT NULL,
|
||||
last_used_at REAL,
|
||||
created_by TEXT NOT NULL
|
||||
)
|
||||
`);
|
||||
|
||||
// Create indices for performance
|
||||
this.query('CREATE INDEX IF NOT EXISTS idx_registry_tokens_type ON registry_tokens(token_type)');
|
||||
this.query('CREATE INDEX IF NOT EXISTS idx_registry_tokens_hash ON registry_tokens(token_hash)');
|
||||
|
||||
this.setMigrationVersion(5);
|
||||
logger.success('Migration 5 completed: Registry tokens table created');
|
||||
}
|
||||
|
||||
// Migration 6: Drop registry_token column from services table (replaced by registry_tokens table)
|
||||
const version6 = this.getMigrationVersion();
|
||||
if (version6 < 6) {
|
||||
logger.info('Running migration 6: Dropping registry_token column from services table...');
|
||||
|
||||
// SQLite doesn't support DROP COLUMN directly, so we need to recreate the table
|
||||
// Create new table without registry_token
|
||||
this.query(`
|
||||
CREATE TABLE services_new (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
image TEXT NOT NULL,
|
||||
registry TEXT,
|
||||
env_vars TEXT,
|
||||
port INTEGER NOT NULL,
|
||||
domain TEXT,
|
||||
container_id TEXT,
|
||||
status TEXT NOT NULL,
|
||||
created_at REAL NOT NULL,
|
||||
updated_at REAL NOT NULL,
|
||||
use_onebox_registry INTEGER DEFAULT 0,
|
||||
registry_repository TEXT,
|
||||
registry_image_tag TEXT DEFAULT 'latest',
|
||||
auto_update_on_push INTEGER DEFAULT 0,
|
||||
image_digest TEXT
|
||||
)
|
||||
`);
|
||||
|
||||
// Copy data (excluding registry_token)
|
||||
this.query(`
|
||||
INSERT INTO services_new (
|
||||
id, name, image, registry, env_vars, port, domain, container_id, status,
|
||||
created_at, updated_at, use_onebox_registry, registry_repository,
|
||||
registry_image_tag, auto_update_on_push, image_digest
|
||||
)
|
||||
SELECT
|
||||
id, name, image, registry, env_vars, port, domain, container_id, status,
|
||||
created_at, updated_at, use_onebox_registry, registry_repository,
|
||||
registry_image_tag, auto_update_on_push, image_digest
|
||||
FROM services
|
||||
`);
|
||||
|
||||
// Drop old table
|
||||
this.query('DROP TABLE services');
|
||||
|
||||
// Rename new table
|
||||
this.query('ALTER TABLE services_new RENAME TO services');
|
||||
|
||||
// Recreate indices
|
||||
this.query('CREATE INDEX IF NOT EXISTS idx_services_name ON services(name)');
|
||||
this.query('CREATE INDEX IF NOT EXISTS idx_services_status ON services(status)');
|
||||
|
||||
this.setMigrationVersion(6);
|
||||
logger.success('Migration 6 completed: registry_token column dropped from services table');
|
||||
}
|
||||
|
||||
// Migration 7: Platform services tables
|
||||
const version7 = this.getMigrationVersion();
|
||||
if (version7 < 7) {
|
||||
logger.info('Running migration 7: Creating platform services tables...');
|
||||
|
||||
// Create platform_services table
|
||||
this.query(`
|
||||
CREATE TABLE platform_services (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
type TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'stopped',
|
||||
container_id TEXT,
|
||||
config TEXT NOT NULL DEFAULT '{}',
|
||||
admin_credentials_encrypted TEXT,
|
||||
created_at REAL NOT NULL,
|
||||
updated_at REAL NOT NULL
|
||||
)
|
||||
`);
|
||||
|
||||
// Create platform_resources table
|
||||
this.query(`
|
||||
CREATE TABLE platform_resources (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
platform_service_id INTEGER NOT NULL,
|
||||
service_id INTEGER NOT NULL,
|
||||
resource_type TEXT NOT NULL,
|
||||
resource_name TEXT NOT NULL,
|
||||
credentials_encrypted TEXT NOT NULL,
|
||||
created_at REAL NOT NULL,
|
||||
FOREIGN KEY (platform_service_id) REFERENCES platform_services(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (service_id) REFERENCES services(id) ON DELETE CASCADE
|
||||
)
|
||||
`);
|
||||
|
||||
// Add platform_requirements column to services table
|
||||
this.query(`
|
||||
ALTER TABLE services ADD COLUMN platform_requirements TEXT DEFAULT '{}'
|
||||
`);
|
||||
|
||||
// Create indices
|
||||
this.query('CREATE INDEX IF NOT EXISTS idx_platform_services_type ON platform_services(type)');
|
||||
this.query('CREATE INDEX IF NOT EXISTS idx_platform_resources_service ON platform_resources(service_id)');
|
||||
this.query('CREATE INDEX IF NOT EXISTS idx_platform_resources_platform ON platform_resources(platform_service_id)');
|
||||
|
||||
this.setMigrationVersion(7);
|
||||
logger.success('Migration 7 completed: Platform services tables created');
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Migration failed: ${error.message}`);
|
||||
logger.error(`Stack: ${error.stack}`);
|
||||
logger.error(`Migration failed: ${getErrorMessage(error)}`);
|
||||
if (error instanceof Error && error.stack) {
|
||||
logger.error(`Stack: ${error.stack}`);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -548,14 +706,14 @@ export class OneboxDatabase {
|
||||
if (!this.db) throw new Error('Database not initialized');
|
||||
|
||||
try {
|
||||
const result = this.query('SELECT MAX(version) as version FROM migrations');
|
||||
const result = this.query<{ version?: number | null; [key: number]: unknown }>('SELECT MAX(version) as version FROM migrations');
|
||||
if (result.length === 0) return 0;
|
||||
|
||||
// Handle both array and object access patterns
|
||||
const versionValue = result[0].version ?? result[0][0];
|
||||
const versionValue = result[0].version ?? (result[0] as Record<number, unknown>)[0];
|
||||
return versionValue !== null && versionValue !== undefined ? Number(versionValue) : 0;
|
||||
} catch (error) {
|
||||
logger.warn(`Error getting migration version: ${error.message}, defaulting to 0`);
|
||||
logger.warn(`Error getting migration version: ${getErrorMessage(error)}, defaulting to 0`);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -587,7 +745,7 @@ export class OneboxDatabase {
|
||||
/**
|
||||
* Execute a raw query
|
||||
*/
|
||||
query<T = unknown[]>(sql: string, params: unknown[] = []): T[] {
|
||||
query<T = Record<string, unknown>>(sql: string, params: BindValue[] = []): T[] {
|
||||
if (!this.db) {
|
||||
const error = new Error('Database not initialized');
|
||||
console.error('Database access before initialization!');
|
||||
@@ -621,8 +779,8 @@ export class OneboxDatabase {
|
||||
`INSERT INTO services (
|
||||
name, image, registry, env_vars, port, domain, container_id, status,
|
||||
created_at, updated_at,
|
||||
use_onebox_registry, registry_repository, registry_token, registry_image_tag,
|
||||
auto_update_on_push, image_digest
|
||||
use_onebox_registry, registry_repository, registry_image_tag,
|
||||
auto_update_on_push, image_digest, platform_requirements
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
service.name,
|
||||
@@ -637,10 +795,10 @@ export class OneboxDatabase {
|
||||
now,
|
||||
service.useOneboxRegistry ? 1 : 0,
|
||||
service.registryRepository || null,
|
||||
service.registryToken || null,
|
||||
service.registryImageTag || 'latest',
|
||||
service.autoUpdateOnPush ? 1 : 0,
|
||||
service.imageDigest || null,
|
||||
JSON.stringify(service.platformRequirements || {}),
|
||||
]
|
||||
);
|
||||
|
||||
@@ -717,10 +875,6 @@ export class OneboxDatabase {
|
||||
fields.push('registry_repository = ?');
|
||||
values.push(updates.registryRepository);
|
||||
}
|
||||
if (updates.registryToken !== undefined) {
|
||||
fields.push('registry_token = ?');
|
||||
values.push(updates.registryToken);
|
||||
}
|
||||
if (updates.registryImageTag !== undefined) {
|
||||
fields.push('registry_image_tag = ?');
|
||||
values.push(updates.registryImageTag);
|
||||
@@ -733,6 +887,10 @@ export class OneboxDatabase {
|
||||
fields.push('image_digest = ?');
|
||||
values.push(updates.imageDigest);
|
||||
}
|
||||
if (updates.platformRequirements !== undefined) {
|
||||
fields.push('platform_requirements = ?');
|
||||
values.push(JSON.stringify(updates.platformRequirements));
|
||||
}
|
||||
|
||||
fields.push('updated_at = ?');
|
||||
values.push(Date.now());
|
||||
@@ -754,11 +912,23 @@ export class OneboxDatabase {
|
||||
try {
|
||||
envVars = JSON.parse(String(envVarsRaw));
|
||||
} catch (e) {
|
||||
logger.warn(`Failed to parse env_vars for service: ${e.message}`);
|
||||
logger.warn(`Failed to parse env_vars for service: ${getErrorMessage(e)}`);
|
||||
envVars = {};
|
||||
}
|
||||
}
|
||||
|
||||
// Handle platform_requirements JSON parsing safely
|
||||
let platformRequirements: IPlatformRequirements | undefined;
|
||||
const platformReqRaw = row.platform_requirements;
|
||||
if (platformReqRaw && platformReqRaw !== 'undefined' && platformReqRaw !== 'null' && platformReqRaw !== '{}') {
|
||||
try {
|
||||
platformRequirements = JSON.parse(String(platformReqRaw));
|
||||
} catch (e) {
|
||||
logger.warn(`Failed to parse platform_requirements for service: ${getErrorMessage(e)}`);
|
||||
platformRequirements = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: Number(row.id || row[0]),
|
||||
name: String(row.name || row[1]),
|
||||
@@ -774,10 +944,11 @@ export class OneboxDatabase {
|
||||
// Onebox Registry fields
|
||||
useOneboxRegistry: row.use_onebox_registry ? Boolean(row.use_onebox_registry) : undefined,
|
||||
registryRepository: row.registry_repository ? String(row.registry_repository) : undefined,
|
||||
registryToken: row.registry_token ? String(row.registry_token) : undefined,
|
||||
registryImageTag: row.registry_image_tag ? String(row.registry_image_tag) : undefined,
|
||||
autoUpdateOnPush: row.auto_update_on_push ? Boolean(row.auto_update_on_push) : undefined,
|
||||
imageDigest: row.image_digest ? String(row.image_digest) : undefined,
|
||||
// Platform service requirements
|
||||
platformRequirements,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1392,4 +1563,279 @@ export class OneboxDatabase {
|
||||
updatedAt: Number(row.updated_at || row[7]),
|
||||
};
|
||||
}
|
||||
|
||||
// ============ Registry Tokens ============
|
||||
|
||||
createRegistryToken(token: Omit<IRegistryToken, 'id'>): IRegistryToken {
|
||||
if (!this.db) throw new Error('Database not initialized');
|
||||
|
||||
const scopeJson = Array.isArray(token.scope) ? JSON.stringify(token.scope) : token.scope;
|
||||
|
||||
this.query(
|
||||
`INSERT INTO registry_tokens (name, token_hash, token_type, scope, expires_at, created_at, last_used_at, created_by)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
token.name,
|
||||
token.tokenHash,
|
||||
token.type,
|
||||
scopeJson,
|
||||
token.expiresAt,
|
||||
token.createdAt,
|
||||
token.lastUsedAt,
|
||||
token.createdBy,
|
||||
]
|
||||
);
|
||||
|
||||
const rows = this.query('SELECT * FROM registry_tokens WHERE id = last_insert_rowid()');
|
||||
return this.rowToRegistryToken(rows[0]);
|
||||
}
|
||||
|
||||
getRegistryTokenById(id: number): IRegistryToken | null {
|
||||
if (!this.db) throw new Error('Database not initialized');
|
||||
|
||||
const rows = this.query('SELECT * FROM registry_tokens WHERE id = ?', [id]);
|
||||
return rows.length > 0 ? this.rowToRegistryToken(rows[0]) : null;
|
||||
}
|
||||
|
||||
getRegistryTokenByHash(tokenHash: string): IRegistryToken | null {
|
||||
if (!this.db) throw new Error('Database not initialized');
|
||||
|
||||
const rows = this.query('SELECT * FROM registry_tokens WHERE token_hash = ?', [tokenHash]);
|
||||
return rows.length > 0 ? this.rowToRegistryToken(rows[0]) : null;
|
||||
}
|
||||
|
||||
getAllRegistryTokens(): IRegistryToken[] {
|
||||
if (!this.db) throw new Error('Database not initialized');
|
||||
|
||||
const rows = this.query('SELECT * FROM registry_tokens ORDER BY created_at DESC');
|
||||
return rows.map((row) => this.rowToRegistryToken(row));
|
||||
}
|
||||
|
||||
getRegistryTokensByType(type: 'global' | 'ci'): IRegistryToken[] {
|
||||
if (!this.db) throw new Error('Database not initialized');
|
||||
|
||||
const rows = this.query('SELECT * FROM registry_tokens WHERE token_type = ? ORDER BY created_at DESC', [type]);
|
||||
return rows.map((row) => this.rowToRegistryToken(row));
|
||||
}
|
||||
|
||||
updateRegistryTokenLastUsed(id: number): void {
|
||||
if (!this.db) throw new Error('Database not initialized');
|
||||
this.query('UPDATE registry_tokens SET last_used_at = ? WHERE id = ?', [Date.now(), id]);
|
||||
}
|
||||
|
||||
deleteRegistryToken(id: number): void {
|
||||
if (!this.db) throw new Error('Database not initialized');
|
||||
this.query('DELETE FROM registry_tokens WHERE id = ?', [id]);
|
||||
}
|
||||
|
||||
private rowToRegistryToken(row: any): IRegistryToken {
|
||||
// Parse scope - it's either 'all' or a JSON array
|
||||
let scope: 'all' | string[];
|
||||
const scopeRaw = row.scope || row[4];
|
||||
if (scopeRaw === 'all') {
|
||||
scope = 'all';
|
||||
} else {
|
||||
try {
|
||||
scope = JSON.parse(String(scopeRaw));
|
||||
} catch {
|
||||
scope = 'all';
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: Number(row.id || row[0]),
|
||||
name: String(row.name || row[1]),
|
||||
tokenHash: String(row.token_hash || row[2]),
|
||||
type: String(row.token_type || row[3]) as IRegistryToken['type'],
|
||||
scope,
|
||||
expiresAt: row.expires_at || row[5] ? Number(row.expires_at || row[5]) : null,
|
||||
createdAt: Number(row.created_at || row[6]),
|
||||
lastUsedAt: row.last_used_at || row[7] ? Number(row.last_used_at || row[7]) : null,
|
||||
createdBy: String(row.created_by || row[8]),
|
||||
};
|
||||
}
|
||||
|
||||
// ============ Platform Services CRUD ============
|
||||
|
||||
createPlatformService(service: Omit<IPlatformService, 'id'>): IPlatformService {
|
||||
if (!this.db) throw new Error('Database not initialized');
|
||||
|
||||
const now = Date.now();
|
||||
this.query(
|
||||
`INSERT INTO platform_services (name, type, status, container_id, config, admin_credentials_encrypted, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
service.name,
|
||||
service.type,
|
||||
service.status,
|
||||
service.containerId || null,
|
||||
JSON.stringify(service.config),
|
||||
service.adminCredentialsEncrypted || null,
|
||||
now,
|
||||
now,
|
||||
]
|
||||
);
|
||||
|
||||
return this.getPlatformServiceByName(service.name)!;
|
||||
}
|
||||
|
||||
getPlatformServiceByName(name: string): IPlatformService | null {
|
||||
if (!this.db) throw new Error('Database not initialized');
|
||||
|
||||
const rows = this.query('SELECT * FROM platform_services WHERE name = ?', [name]);
|
||||
return rows.length > 0 ? this.rowToPlatformService(rows[0]) : null;
|
||||
}
|
||||
|
||||
getPlatformServiceById(id: number): IPlatformService | null {
|
||||
if (!this.db) throw new Error('Database not initialized');
|
||||
|
||||
const rows = this.query('SELECT * FROM platform_services WHERE id = ?', [id]);
|
||||
return rows.length > 0 ? this.rowToPlatformService(rows[0]) : null;
|
||||
}
|
||||
|
||||
getPlatformServiceByType(type: TPlatformServiceType): IPlatformService | null {
|
||||
if (!this.db) throw new Error('Database not initialized');
|
||||
|
||||
const rows = this.query('SELECT * FROM platform_services WHERE type = ?', [type]);
|
||||
return rows.length > 0 ? this.rowToPlatformService(rows[0]) : null;
|
||||
}
|
||||
|
||||
getAllPlatformServices(): IPlatformService[] {
|
||||
if (!this.db) throw new Error('Database not initialized');
|
||||
|
||||
const rows = this.query('SELECT * FROM platform_services ORDER BY created_at DESC');
|
||||
return rows.map((row) => this.rowToPlatformService(row));
|
||||
}
|
||||
|
||||
updatePlatformService(id: number, updates: Partial<IPlatformService>): void {
|
||||
if (!this.db) throw new Error('Database not initialized');
|
||||
|
||||
const fields: string[] = [];
|
||||
const values: unknown[] = [];
|
||||
|
||||
if (updates.status !== undefined) {
|
||||
fields.push('status = ?');
|
||||
values.push(updates.status);
|
||||
}
|
||||
if (updates.containerId !== undefined) {
|
||||
fields.push('container_id = ?');
|
||||
values.push(updates.containerId);
|
||||
}
|
||||
if (updates.config !== undefined) {
|
||||
fields.push('config = ?');
|
||||
values.push(JSON.stringify(updates.config));
|
||||
}
|
||||
if (updates.adminCredentialsEncrypted !== undefined) {
|
||||
fields.push('admin_credentials_encrypted = ?');
|
||||
values.push(updates.adminCredentialsEncrypted);
|
||||
}
|
||||
|
||||
fields.push('updated_at = ?');
|
||||
values.push(Date.now());
|
||||
values.push(id);
|
||||
|
||||
this.query(`UPDATE platform_services SET ${fields.join(', ')} WHERE id = ?`, values);
|
||||
}
|
||||
|
||||
deletePlatformService(id: number): void {
|
||||
if (!this.db) throw new Error('Database not initialized');
|
||||
this.query('DELETE FROM platform_services WHERE id = ?', [id]);
|
||||
}
|
||||
|
||||
private rowToPlatformService(row: any): IPlatformService {
|
||||
let config = { image: '', port: 0 };
|
||||
const configRaw = row.config;
|
||||
if (configRaw) {
|
||||
try {
|
||||
config = JSON.parse(String(configRaw));
|
||||
} catch (e) {
|
||||
logger.warn(`Failed to parse platform service config: ${getErrorMessage(e)}`);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: Number(row.id),
|
||||
name: String(row.name),
|
||||
type: String(row.type) as TPlatformServiceType,
|
||||
status: String(row.status) as IPlatformService['status'],
|
||||
containerId: row.container_id ? String(row.container_id) : undefined,
|
||||
config,
|
||||
adminCredentialsEncrypted: row.admin_credentials_encrypted ? String(row.admin_credentials_encrypted) : undefined,
|
||||
createdAt: Number(row.created_at),
|
||||
updatedAt: Number(row.updated_at),
|
||||
};
|
||||
}
|
||||
|
||||
// ============ Platform Resources CRUD ============
|
||||
|
||||
createPlatformResource(resource: Omit<IPlatformResource, 'id'>): IPlatformResource {
|
||||
if (!this.db) throw new Error('Database not initialized');
|
||||
|
||||
const now = Date.now();
|
||||
this.query(
|
||||
`INSERT INTO platform_resources (platform_service_id, service_id, resource_type, resource_name, credentials_encrypted, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
resource.platformServiceId,
|
||||
resource.serviceId,
|
||||
resource.resourceType,
|
||||
resource.resourceName,
|
||||
resource.credentialsEncrypted,
|
||||
now,
|
||||
]
|
||||
);
|
||||
|
||||
const rows = this.query('SELECT * FROM platform_resources WHERE id = last_insert_rowid()');
|
||||
return this.rowToPlatformResource(rows[0]);
|
||||
}
|
||||
|
||||
getPlatformResourceById(id: number): IPlatformResource | null {
|
||||
if (!this.db) throw new Error('Database not initialized');
|
||||
|
||||
const rows = this.query('SELECT * FROM platform_resources WHERE id = ?', [id]);
|
||||
return rows.length > 0 ? this.rowToPlatformResource(rows[0]) : null;
|
||||
}
|
||||
|
||||
getPlatformResourcesByService(serviceId: number): IPlatformResource[] {
|
||||
if (!this.db) throw new Error('Database not initialized');
|
||||
|
||||
const rows = this.query('SELECT * FROM platform_resources WHERE service_id = ?', [serviceId]);
|
||||
return rows.map((row) => this.rowToPlatformResource(row));
|
||||
}
|
||||
|
||||
getPlatformResourcesByPlatformService(platformServiceId: number): IPlatformResource[] {
|
||||
if (!this.db) throw new Error('Database not initialized');
|
||||
|
||||
const rows = this.query('SELECT * FROM platform_resources WHERE platform_service_id = ?', [platformServiceId]);
|
||||
return rows.map((row) => this.rowToPlatformResource(row));
|
||||
}
|
||||
|
||||
getAllPlatformResources(): IPlatformResource[] {
|
||||
if (!this.db) throw new Error('Database not initialized');
|
||||
|
||||
const rows = this.query('SELECT * FROM platform_resources ORDER BY created_at DESC');
|
||||
return rows.map((row) => this.rowToPlatformResource(row));
|
||||
}
|
||||
|
||||
deletePlatformResource(id: number): void {
|
||||
if (!this.db) throw new Error('Database not initialized');
|
||||
this.query('DELETE FROM platform_resources WHERE id = ?', [id]);
|
||||
}
|
||||
|
||||
deletePlatformResourcesByService(serviceId: number): void {
|
||||
if (!this.db) throw new Error('Database not initialized');
|
||||
this.query('DELETE FROM platform_resources WHERE service_id = ?', [serviceId]);
|
||||
}
|
||||
|
||||
private rowToPlatformResource(row: any): IPlatformResource {
|
||||
return {
|
||||
id: Number(row.id),
|
||||
platformServiceId: Number(row.platform_service_id),
|
||||
serviceId: Number(row.service_id),
|
||||
resourceType: String(row.resource_type) as IPlatformResource['resourceType'],
|
||||
resourceName: String(row.resource_name),
|
||||
credentialsEncrypted: String(row.credentials_encrypted),
|
||||
createdAt: Number(row.created_at),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user