| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import path from 'path'; |
| import type { Database } from 'better-sqlite3'; |
| import { StorageAdapter } from './types'; |
| import { Project, VirtualFile, FileTreeNode, CustomTemplate, Deployment, EdgeFunction, ServerFunction, Secret, ScheduledFunction } from '../types'; |
| import { Skill } from '../skills/types'; |
| import type { ModelTemplate, ModelAssignment } from '@/lib/llm/models/assignment'; |
| import type { CustomConnection } from '@/lib/llm/providers/connection-record'; |
| import { |
| getCoreDatabase, |
| getDeploymentDatabase, |
| closeDeploymentDatabase, |
| deleteDeploymentDatabase, |
| deleteProjectDatabase, |
| listDeploymentIds, |
| deploymentExists, |
| closeAllConnections, |
| closeCoreDatabaseByPath, |
| } from './sqlite-connection'; |
| import { DeploymentDatabase } from './deployment-database'; |
| import { AnalyticsDatabase } from './analytics-database'; |
| import { ProjectDatabase } from './project-database'; |
| import type { AnalyticsConfig, ComplianceConfig, SeoConfig } from '../types'; |
|
|
| |
| const DEFAULT_ANALYTICS: AnalyticsConfig = { |
| enabled: false, |
| provider: 'builtin', |
| privacyMode: true, |
| }; |
|
|
| const DEFAULT_COMPLIANCE: ComplianceConfig = { |
| enabled: false, |
| bannerPosition: 'bottom', |
| bannerStyle: 'bar', |
| message: 'This site uses cookies to improve your experience.', |
| acceptButtonText: 'Accept', |
| declineButtonText: 'Decline', |
| mode: 'opt-in', |
| blockAnalytics: true, |
| }; |
|
|
| const DEFAULT_SEO: SeoConfig = {}; |
|
|
| |
| interface Migration { |
| id: string; |
| up: (db: Database) => void; |
| } |
|
|
| const MIGRATIONS: Migration[] = [ |
| { |
| id: 'initial_schema_v1', |
| up: (db) => { |
| |
| db.exec(` |
| CREATE TABLE IF NOT EXISTS _migrations ( |
| id TEXT PRIMARY KEY, |
| applied_at TEXT NOT NULL DEFAULT (datetime('now')) |
| ) |
| `); |
|
|
| |
| db.exec(` |
| CREATE TABLE IF NOT EXISTS projects ( |
| id TEXT PRIMARY KEY, |
| name TEXT NOT NULL, |
| description TEXT, |
| created_at TEXT NOT NULL, |
| updated_at TEXT NOT NULL, |
| last_saved_at TEXT, |
| last_saved_checkpoint_id TEXT, |
| settings TEXT DEFAULT '{}', |
| cost_tracking TEXT DEFAULT '{}', |
| preview_image TEXT, |
| last_synced_at TEXT, |
| server_updated_at TEXT |
| ) |
| `); |
|
|
| |
| db.exec(` |
| CREATE TABLE IF NOT EXISTS custom_templates ( |
| id TEXT PRIMARY KEY, |
| name TEXT NOT NULL, |
| description TEXT, |
| version TEXT, |
| files TEXT DEFAULT '[]', |
| directories TEXT DEFAULT '[]', |
| assets TEXT DEFAULT '[]', |
| metadata TEXT DEFAULT '{}', |
| imported_at TEXT NOT NULL |
| ) |
| `); |
|
|
| |
| db.exec(` |
| CREATE TABLE IF NOT EXISTS skills ( |
| id TEXT PRIMARY KEY, |
| name TEXT NOT NULL, |
| description TEXT NOT NULL, |
| content TEXT NOT NULL, |
| markdown TEXT NOT NULL, |
| is_built_in INTEGER DEFAULT 0, |
| created_at TEXT NOT NULL, |
| updated_at TEXT NOT NULL |
| ) |
| `); |
|
|
| |
| db.exec(` |
| CREATE INDEX IF NOT EXISTS idx_skills_is_built_in ON skills(is_built_in) |
| `); |
| } |
| }, |
| { |
| id: 'add_files_and_tree_v2', |
| up: (db) => { |
| |
| db.exec(` |
| CREATE TABLE IF NOT EXISTS files ( |
| id TEXT PRIMARY KEY, |
| project_id TEXT NOT NULL, |
| path TEXT NOT NULL, |
| name TEXT NOT NULL, |
| type TEXT NOT NULL CHECK(type IN ('html', 'css', 'js', 'json', 'text', 'template', 'image', 'video', 'binary')), |
| content TEXT, |
| mime_type TEXT, |
| size INTEGER DEFAULT 0, |
| created_at TEXT NOT NULL DEFAULT (datetime('now')), |
| updated_at TEXT NOT NULL DEFAULT (datetime('now')), |
| metadata TEXT DEFAULT '{}', |
| UNIQUE(project_id, path), |
| FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE |
| ) |
| `); |
|
|
| |
| db.exec(` |
| CREATE INDEX IF NOT EXISTS idx_files_project_id ON files(project_id) |
| `); |
|
|
| |
| db.exec(` |
| CREATE TABLE IF NOT EXISTS file_tree_nodes ( |
| id TEXT PRIMARY KEY, |
| project_id TEXT NOT NULL, |
| path TEXT NOT NULL, |
| name TEXT NOT NULL, |
| type TEXT NOT NULL CHECK(type IN ('file', 'directory')), |
| parent_path TEXT, |
| is_expanded INTEGER DEFAULT 0, |
| metadata TEXT DEFAULT '{}', |
| UNIQUE(project_id, path), |
| FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE |
| ) |
| `); |
|
|
| |
| db.exec(` |
| CREATE INDEX IF NOT EXISTS idx_tree_nodes_project_id ON file_tree_nodes(project_id) |
| `); |
| db.exec(` |
| CREATE INDEX IF NOT EXISTS idx_tree_nodes_parent ON file_tree_nodes(project_id, parent_path) |
| `); |
|
|
| |
| db.exec(` |
| CREATE TABLE IF NOT EXISTS sites ( |
| id TEXT PRIMARY KEY, |
| project_id TEXT NOT NULL, |
| name TEXT NOT NULL, |
| slug TEXT, |
| enabled INTEGER DEFAULT 1, |
| under_construction INTEGER DEFAULT 0, |
| custom_domain TEXT, |
| head_scripts TEXT DEFAULT '[]', |
| body_scripts TEXT DEFAULT '[]', |
| cdn_links TEXT DEFAULT '[]', |
| analytics TEXT DEFAULT '{}', |
| seo TEXT DEFAULT '{}', |
| compliance TEXT DEFAULT '{}', |
| settings_version INTEGER DEFAULT 1, |
| last_published_version INTEGER, |
| preview_image TEXT, |
| preview_updated_at TEXT, |
| database_enabled INTEGER DEFAULT 0, |
| created_at TEXT NOT NULL DEFAULT (datetime('now')), |
| updated_at TEXT NOT NULL DEFAULT (datetime('now')), |
| published_at TEXT, |
| FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE |
| ) |
| `); |
|
|
| |
| db.exec(` |
| CREATE INDEX IF NOT EXISTS idx_sites_project_id ON sites(project_id) |
| `); |
| } |
| }, |
| { |
| id: 'add_request_log_v3', |
| up: (db) => { |
| |
| db.exec(` |
| CREATE TABLE IF NOT EXISTS request_log ( |
| id INTEGER PRIMARY KEY AUTOINCREMENT, |
| site_id TEXT NOT NULL, |
| path TEXT NOT NULL, |
| status_code INTEGER NOT NULL, |
| ip_hash TEXT, |
| user_agent TEXT, |
| timestamp TEXT NOT NULL DEFAULT (datetime('now')) |
| ) |
| `); |
|
|
| |
| db.exec(` |
| CREATE INDEX IF NOT EXISTS idx_request_log_timestamp ON request_log(timestamp) |
| `); |
| db.exec(` |
| CREATE INDEX IF NOT EXISTS idx_request_log_site_id ON request_log(site_id) |
| `); |
| } |
| }, |
| { |
| id: 'rename_sites_to_deployments_v4', |
| up: (db) => { |
| |
| |
| |
| |
| db.exec(`ALTER TABLE sites RENAME TO deployments`); |
|
|
| |
| db.exec(`DROP INDEX IF EXISTS idx_sites_project_id`); |
| db.exec(` |
| CREATE INDEX IF NOT EXISTS idx_deployments_project_id ON deployments(project_id) |
| `); |
| } |
| }, |
| { |
| id: 'add_project_server_features_v5', |
| up: (db) => { |
| |
| db.exec(` |
| CREATE TABLE IF NOT EXISTS project_edge_functions ( |
| id TEXT PRIMARY KEY, |
| project_id TEXT NOT NULL, |
| name TEXT NOT NULL, |
| description TEXT, |
| code TEXT NOT NULL, |
| method TEXT NOT NULL DEFAULT 'GET' CHECK(method IN ('GET', 'POST', 'PUT', 'DELETE', 'ANY')), |
| enabled INTEGER DEFAULT 1, |
| timeout_ms INTEGER DEFAULT 5000, |
| created_at TEXT NOT NULL DEFAULT (datetime('now')), |
| updated_at TEXT NOT NULL DEFAULT (datetime('now')), |
| UNIQUE(project_id, name), |
| FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE |
| ) |
| `); |
| db.exec(`CREATE INDEX IF NOT EXISTS idx_project_edge_functions_project_id ON project_edge_functions(project_id)`); |
|
|
| |
| db.exec(` |
| CREATE TABLE IF NOT EXISTS project_server_functions ( |
| id TEXT PRIMARY KEY, |
| project_id TEXT NOT NULL, |
| name TEXT NOT NULL, |
| description TEXT, |
| code TEXT NOT NULL, |
| enabled INTEGER DEFAULT 1, |
| created_at TEXT NOT NULL DEFAULT (datetime('now')), |
| updated_at TEXT NOT NULL DEFAULT (datetime('now')), |
| UNIQUE(project_id, name), |
| FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE |
| ) |
| `); |
| db.exec(`CREATE INDEX IF NOT EXISTS idx_project_server_functions_project_id ON project_server_functions(project_id)`); |
|
|
| |
| db.exec(` |
| CREATE TABLE IF NOT EXISTS project_secrets ( |
| id TEXT PRIMARY KEY, |
| project_id TEXT NOT NULL, |
| name TEXT NOT NULL, |
| description TEXT, |
| has_value INTEGER DEFAULT 0, |
| value TEXT, |
| created_at TEXT NOT NULL DEFAULT (datetime('now')), |
| updated_at TEXT NOT NULL DEFAULT (datetime('now')), |
| UNIQUE(project_id, name), |
| FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE |
| ) |
| `); |
| db.exec(`CREATE INDEX IF NOT EXISTS idx_project_secrets_project_id ON project_secrets(project_id)`); |
|
|
| |
| db.exec(` |
| CREATE TABLE IF NOT EXISTS project_scheduled_functions ( |
| id TEXT PRIMARY KEY, |
| project_id TEXT NOT NULL, |
| name TEXT NOT NULL, |
| description TEXT, |
| function_id TEXT NOT NULL, |
| cron_expression TEXT NOT NULL, |
| timezone TEXT DEFAULT 'UTC', |
| config TEXT DEFAULT '{}', |
| enabled INTEGER DEFAULT 1, |
| last_run_at TEXT, |
| next_run_at TEXT, |
| last_status TEXT, |
| last_error TEXT, |
| last_duration_ms INTEGER, |
| created_at TEXT NOT NULL DEFAULT (datetime('now')), |
| updated_at TEXT NOT NULL DEFAULT (datetime('now')), |
| UNIQUE(project_id, name), |
| FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE |
| ) |
| `); |
| db.exec(`CREATE INDEX IF NOT EXISTS idx_project_scheduled_functions_project_id ON project_scheduled_functions(project_id)`); |
| } |
| }, |
| { |
| id: 'add_model_templates_v6', |
| up: (db) => { |
| |
| db.exec(` |
| CREATE TABLE IF NOT EXISTS model_templates ( |
| id TEXT PRIMARY KEY, |
| name TEXT NOT NULL, |
| description TEXT, |
| assignment TEXT NOT NULL DEFAULT '{}', |
| updated_at TEXT NOT NULL |
| ) |
| `); |
| } |
| }, |
| { |
| id: 'add_custom_template_updated_at_v7', |
| up: (db) => { |
| |
| |
| |
| |
| |
| const cols = db.prepare(`PRAGMA table_info(custom_templates)`).all() as Array<{ name: string }>; |
| if (!cols.some((c) => c.name === 'updated_at')) { |
| db.exec(`ALTER TABLE custom_templates ADD COLUMN updated_at TEXT`); |
| } |
| } |
| }, |
| { |
| id: 'add_connections_v8', |
| up: (db) => { |
| |
| |
| db.exec(` |
| CREATE TABLE IF NOT EXISTS connections ( |
| id TEXT PRIMARY KEY, |
| name TEXT NOT NULL, |
| base_url TEXT NOT NULL, |
| format TEXT NOT NULL DEFAULT 'openai', |
| api_key_required INTEGER NOT NULL DEFAULT 1, |
| updated_at TEXT NOT NULL |
| ) |
| `); |
| } |
| }, |
| { |
| id: 'add_interview_templates_v9', |
| up: (db) => { |
| |
| |
| db.exec(` |
| CREATE TABLE IF NOT EXISTS interview_templates ( |
| id TEXT PRIMARY KEY, |
| name TEXT NOT NULL, |
| description TEXT, |
| spec TEXT NOT NULL DEFAULT '{}', |
| updated_at TEXT NOT NULL |
| ) |
| `); |
| } |
| }, |
| ]; |
|
|
| |
| |
| |
| function parseJSON<T>(value: string | null | undefined, fallback: T): T { |
| if (!value) return fallback; |
| try { |
| return JSON.parse(value) as T; |
| } catch { |
| return fallback; |
| } |
| } |
|
|
| |
| |
| |
| function parseDate(value: string | null | undefined): Date { |
| if (!value) return new Date(); |
| return new Date(value); |
| } |
|
|
| |
| |
| |
| |
| function toISOString(value: Date | string | null | undefined): string | null { |
| if (!value) return null; |
| if (typeof value === 'string') return value; |
| return value.toISOString(); |
| } |
|
|
| |
| |
| |
| function toISOStringRequired(value: Date | string): string { |
| if (typeof value === 'string') return value; |
| return value.toISOString(); |
| } |
|
|
| |
| |
| |
| export class SQLiteAdapter implements StorageAdapter { |
| private db: Database | null = null; |
| private initialized = false; |
| private deploymentDatabases = new Map<string, DeploymentDatabase>(); |
| private analyticsDatabases = new Map<string, AnalyticsDatabase>(); |
| private projectDatabases = new Map<string, ProjectDatabase>(); |
| private dbPath: string | undefined; |
| private baseDir: string | undefined; |
|
|
| constructor(dbPath?: string) { |
| this.dbPath = dbPath; |
| if (dbPath) { |
| this.baseDir = path.dirname(dbPath); |
| } |
| } |
|
|
| |
| |
| |
| |
| async init(): Promise<void> { |
| if (this.initialized && this.db?.open) return; |
|
|
| this.db = getCoreDatabase(this.dbPath); |
| await this.runMigrations(); |
| this.initialized = true; |
| } |
|
|
| |
| |
| |
| async close(): Promise<void> { |
| |
| for (const [, deploymentDb] of this.deploymentDatabases) { |
| deploymentDb.close(); |
| } |
| this.deploymentDatabases.clear(); |
|
|
| |
| for (const [, analyticsDb] of this.analyticsDatabases) { |
| analyticsDb.close(); |
| } |
| this.analyticsDatabases.clear(); |
|
|
| |
| for (const [, projectDb] of this.projectDatabases) { |
| projectDb.close(); |
| } |
| this.projectDatabases.clear(); |
|
|
| |
| if (this.dbPath) { |
| closeCoreDatabaseByPath(this.dbPath); |
| } else { |
| closeAllConnections(); |
| } |
| this.db = null; |
| this.initialized = false; |
| } |
|
|
| |
| |
| |
| getProjectDatabase(projectId: string): ProjectDatabase { |
| let projectDb = this.projectDatabases.get(projectId); |
| if (!projectDb) { |
| projectDb = new ProjectDatabase(projectId, this.baseDir); |
| projectDb.init(); |
| this.projectDatabases.set(projectId, projectDb); |
| } |
| return projectDb; |
| } |
|
|
| |
| |
| |
| private getDB(): Database { |
| if (!this.db || !this.db.open) { |
| if (this.initialized) { |
| this.db = getCoreDatabase(this.dbPath); |
| } else { |
| throw new Error('SQLiteAdapter not initialized. Call init() first.'); |
| } |
| } |
| return this.db; |
| } |
|
|
| |
| |
| |
| |
| |
| getCoreDB(): Database { |
| return this.getDB(); |
| } |
|
|
| |
| |
| |
| |
| private getOrCreateDeploymentDB(deploymentId: string): DeploymentDatabase { |
| let deploymentDb = this.deploymentDatabases.get(deploymentId); |
| if (!deploymentDb) { |
| deploymentDb = new DeploymentDatabase(deploymentId); |
| deploymentDb.init(); |
| this.deploymentDatabases.set(deploymentId, deploymentDb); |
| } |
| return deploymentDb; |
| } |
|
|
| |
| |
| |
|
|
| async createEdgeFunction(fn: EdgeFunction): Promise<void> { |
| const db = this.getDB(); |
| db.prepare(` |
| INSERT INTO project_edge_functions (id, project_id, name, description, code, method, enabled, timeout_ms, created_at, updated_at) |
| VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) |
| `).run(fn.id, fn.projectId, fn.name, fn.description || null, fn.code, fn.method, fn.enabled ? 1 : 0, fn.timeoutMs, toISOStringRequired(fn.createdAt), toISOStringRequired(fn.updatedAt)); |
| } |
|
|
| async getEdgeFunction(id: string): Promise<EdgeFunction | null> { |
| const db = this.getDB(); |
| const row = db.prepare('SELECT * FROM project_edge_functions WHERE id = ?').get(id) as any; |
| return row ? this.rowToEdgeFunction(row) : null; |
| } |
|
|
| async listEdgeFunctions(projectId: string): Promise<EdgeFunction[]> { |
| const db = this.getDB(); |
| const rows = db.prepare('SELECT * FROM project_edge_functions WHERE project_id = ? ORDER BY name').all(projectId) as any[]; |
| return rows.map((row) => this.rowToEdgeFunction(row)); |
| } |
|
|
| async updateEdgeFunction(fn: EdgeFunction): Promise<void> { |
| const db = this.getDB(); |
| db.prepare(` |
| UPDATE project_edge_functions SET name = ?, description = ?, code = ?, method = ?, enabled = ?, timeout_ms = ?, updated_at = ? |
| WHERE id = ? |
| `).run(fn.name, fn.description || null, fn.code, fn.method, fn.enabled ? 1 : 0, fn.timeoutMs, toISOStringRequired(fn.updatedAt), fn.id); |
| } |
|
|
| async deleteEdgeFunction(id: string): Promise<void> { |
| const db = this.getDB(); |
| db.prepare('DELETE FROM project_edge_functions WHERE id = ?').run(id); |
| } |
|
|
| private rowToEdgeFunction(row: any): EdgeFunction { |
| return { |
| id: row.id, |
| projectId: row.project_id, |
| name: row.name, |
| description: row.description || undefined, |
| code: row.code, |
| method: row.method as EdgeFunction['method'], |
| enabled: row.enabled === 1, |
| timeoutMs: row.timeout_ms, |
| createdAt: parseDate(row.created_at), |
| updatedAt: parseDate(row.updated_at), |
| }; |
| } |
|
|
| |
| |
| |
|
|
| async createServerFunction(fn: ServerFunction): Promise<void> { |
| const db = this.getDB(); |
| db.prepare(` |
| INSERT INTO project_server_functions (id, project_id, name, description, code, enabled, created_at, updated_at) |
| VALUES (?, ?, ?, ?, ?, ?, ?, ?) |
| `).run(fn.id, fn.projectId, fn.name, fn.description || null, fn.code, fn.enabled ? 1 : 0, toISOStringRequired(fn.createdAt), toISOStringRequired(fn.updatedAt)); |
| } |
|
|
| async getServerFunction(id: string): Promise<ServerFunction | null> { |
| const db = this.getDB(); |
| const row = db.prepare('SELECT * FROM project_server_functions WHERE id = ?').get(id) as any; |
| return row ? this.rowToServerFunction(row) : null; |
| } |
|
|
| async listServerFunctions(projectId: string): Promise<ServerFunction[]> { |
| const db = this.getDB(); |
| const rows = db.prepare('SELECT * FROM project_server_functions WHERE project_id = ? ORDER BY name').all(projectId) as any[]; |
| return rows.map((row) => this.rowToServerFunction(row)); |
| } |
|
|
| async updateServerFunction(fn: ServerFunction): Promise<void> { |
| const db = this.getDB(); |
| db.prepare(` |
| UPDATE project_server_functions SET name = ?, description = ?, code = ?, enabled = ?, updated_at = ? |
| WHERE id = ? |
| `).run(fn.name, fn.description || null, fn.code, fn.enabled ? 1 : 0, toISOStringRequired(fn.updatedAt), fn.id); |
| } |
|
|
| async deleteServerFunction(id: string): Promise<void> { |
| const db = this.getDB(); |
| db.prepare('DELETE FROM project_server_functions WHERE id = ?').run(id); |
| } |
|
|
| private rowToServerFunction(row: any): ServerFunction { |
| return { |
| id: row.id, |
| projectId: row.project_id, |
| name: row.name, |
| description: row.description || undefined, |
| code: row.code, |
| enabled: row.enabled === 1, |
| createdAt: parseDate(row.created_at), |
| updatedAt: parseDate(row.updated_at), |
| }; |
| } |
|
|
| |
| |
| |
|
|
| async createSecret(secret: Secret): Promise<void> { |
| const db = this.getDB(); |
| db.prepare(` |
| INSERT INTO project_secrets (id, project_id, name, description, has_value, value, created_at, updated_at) |
| VALUES (?, ?, ?, ?, ?, ?, ?, ?) |
| `).run(secret.id, secret.projectId, secret.name, secret.description || null, secret.hasValue ? 1 : 0, secret.value || null, toISOStringRequired(secret.createdAt), toISOStringRequired(secret.updatedAt)); |
| } |
|
|
| async getSecret(id: string): Promise<Secret | null> { |
| const db = this.getDB(); |
| const row = db.prepare('SELECT * FROM project_secrets WHERE id = ?').get(id) as any; |
| return row ? this.rowToSecret(row) : null; |
| } |
|
|
| async listSecrets(projectId: string): Promise<Secret[]> { |
| const db = this.getDB(); |
| const rows = db.prepare('SELECT * FROM project_secrets WHERE project_id = ? ORDER BY name').all(projectId) as any[]; |
| return rows.map((row) => this.rowToSecret(row)); |
| } |
|
|
| async updateSecret(secret: Secret): Promise<void> { |
| const db = this.getDB(); |
| db.prepare(` |
| UPDATE project_secrets SET name = ?, description = ?, has_value = ?, value = ?, updated_at = ? |
| WHERE id = ? |
| `).run(secret.name, secret.description || null, secret.hasValue ? 1 : 0, secret.value || null, toISOStringRequired(secret.updatedAt), secret.id); |
| } |
|
|
| async deleteSecret(id: string): Promise<void> { |
| const db = this.getDB(); |
| db.prepare('DELETE FROM project_secrets WHERE id = ?').run(id); |
| } |
|
|
| private rowToSecret(row: any): Secret { |
| return { |
| id: row.id, |
| projectId: row.project_id, |
| name: row.name, |
| description: row.description || undefined, |
| hasValue: row.has_value === 1, |
| value: row.value || undefined, |
| createdAt: parseDate(row.created_at), |
| updatedAt: parseDate(row.updated_at), |
| }; |
| } |
|
|
| |
| |
| |
|
|
| async createScheduledFunction(fn: ScheduledFunction): Promise<void> { |
| const db = this.getDB(); |
| db.prepare(` |
| INSERT INTO project_scheduled_functions (id, project_id, name, description, function_id, cron_expression, timezone, config, enabled, last_run_at, next_run_at, last_status, last_error, last_duration_ms, created_at, updated_at) |
| VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) |
| `).run(fn.id, fn.projectId, fn.name, fn.description || null, fn.functionId, fn.cronExpression, fn.timezone, JSON.stringify(fn.config), fn.enabled ? 1 : 0, toISOString(fn.lastRunAt ?? null), toISOString(fn.nextRunAt ?? null), fn.lastStatus || null, fn.lastError || null, fn.lastDurationMs ?? null, toISOStringRequired(fn.createdAt), toISOStringRequired(fn.updatedAt)); |
| } |
|
|
| async getScheduledFunction(id: string): Promise<ScheduledFunction | null> { |
| const db = this.getDB(); |
| const row = db.prepare('SELECT * FROM project_scheduled_functions WHERE id = ?').get(id) as any; |
| return row ? this.rowToScheduledFunction(row) : null; |
| } |
|
|
| async listScheduledFunctions(projectId: string): Promise<ScheduledFunction[]> { |
| const db = this.getDB(); |
| const rows = db.prepare('SELECT * FROM project_scheduled_functions WHERE project_id = ? ORDER BY name').all(projectId) as any[]; |
| return rows.map((row) => this.rowToScheduledFunction(row)); |
| } |
|
|
| async updateScheduledFunction(fn: ScheduledFunction): Promise<void> { |
| const db = this.getDB(); |
| db.prepare(` |
| UPDATE project_scheduled_functions SET name = ?, description = ?, function_id = ?, cron_expression = ?, timezone = ?, config = ?, enabled = ?, last_run_at = ?, next_run_at = ?, last_status = ?, last_error = ?, last_duration_ms = ?, updated_at = ? |
| WHERE id = ? |
| `).run(fn.name, fn.description || null, fn.functionId, fn.cronExpression, fn.timezone, JSON.stringify(fn.config), fn.enabled ? 1 : 0, toISOString(fn.lastRunAt ?? null), toISOString(fn.nextRunAt ?? null), fn.lastStatus || null, fn.lastError || null, fn.lastDurationMs ?? null, toISOStringRequired(fn.updatedAt), fn.id); |
| } |
|
|
| async deleteScheduledFunction(id: string): Promise<void> { |
| const db = this.getDB(); |
| db.prepare('DELETE FROM project_scheduled_functions WHERE id = ?').run(id); |
| } |
|
|
| private rowToScheduledFunction(row: any): ScheduledFunction { |
| return { |
| id: row.id, |
| projectId: row.project_id, |
| name: row.name, |
| description: row.description || undefined, |
| functionId: row.function_id, |
| cronExpression: row.cron_expression, |
| timezone: row.timezone, |
| config: parseJSON(row.config, {}), |
| enabled: row.enabled === 1, |
| lastRunAt: row.last_run_at ? parseDate(row.last_run_at) : undefined, |
| nextRunAt: row.next_run_at ? parseDate(row.next_run_at) : undefined, |
| lastStatus: row.last_status || undefined, |
| lastError: row.last_error || undefined, |
| lastDurationMs: row.last_duration_ms ?? undefined, |
| createdAt: parseDate(row.created_at), |
| updatedAt: parseDate(row.updated_at), |
| }; |
| } |
|
|
| |
| |
| |
| |
| async enableDeploymentDatabase(deploymentId: string): Promise<void> { |
| const db = this.getDB(); |
|
|
| |
| db.prepare('UPDATE deployments SET database_enabled = 1 WHERE id = ?').run(deploymentId); |
|
|
| |
| this.getOrCreateDeploymentDB(deploymentId); |
| } |
|
|
| |
| |
| |
| async disableDeploymentDatabase(deploymentId: string): Promise<void> { |
| const db = this.getDB(); |
|
|
| |
| db.prepare('UPDATE deployments SET database_enabled = 0 WHERE id = ?').run(deploymentId); |
|
|
| |
| const deploymentDb = this.deploymentDatabases.get(deploymentId); |
| if (deploymentDb) { |
| deploymentDb.close(); |
| this.deploymentDatabases.delete(deploymentId); |
| } |
|
|
| if (deploymentExists(deploymentId)) { |
| deleteDeploymentDatabase(deploymentId); |
| } |
| } |
|
|
| |
| |
| |
| private async runMigrations(): Promise<void> { |
| const db = this.getDB(); |
|
|
| |
| db.exec(` |
| CREATE TABLE IF NOT EXISTS _migrations ( |
| id TEXT PRIMARY KEY, |
| applied_at TEXT NOT NULL DEFAULT (datetime('now')) |
| ) |
| `); |
|
|
| |
| const rows = db.prepare('SELECT id FROM _migrations').all() as Array<{ id: string }>; |
| const applied = new Set(rows.map((row) => row.id)); |
|
|
| |
| const runMigration = db.transaction((migration: Migration) => { |
| if (!applied.has(migration.id)) { |
| migration.up(db); |
| db.prepare('INSERT INTO _migrations (id) VALUES (?)').run(migration.id); |
| } |
| }); |
|
|
| for (const migration of MIGRATIONS) { |
| runMigration(migration); |
| } |
| } |
|
|
| |
| |
| |
|
|
| async createProject(project: Project): Promise<void> { |
| const db = this.getDB(); |
| const stmt = db.prepare(` |
| INSERT INTO projects ( |
| id, name, description, created_at, updated_at, |
| last_saved_at, last_saved_checkpoint_id, settings, |
| cost_tracking, preview_image, last_synced_at, server_updated_at |
| ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) |
| `); |
|
|
| stmt.run( |
| project.id, |
| project.name, |
| project.description ?? null, |
| toISOStringRequired(project.createdAt), |
| toISOStringRequired(project.updatedAt), |
| toISOString(project.lastSavedAt), |
| project.lastSavedCheckpointId ?? null, |
| JSON.stringify(project.settings ?? {}), |
| JSON.stringify(project.costTracking ?? {}), |
| project.previewImage ?? null, |
| toISOString(project.lastSyncedAt), |
| toISOString(project.serverUpdatedAt) |
| ); |
| } |
|
|
| async getProject(id: string): Promise<Project | null> { |
| const db = this.getDB(); |
| const row = db.prepare('SELECT * FROM projects WHERE id = ?').get(id) as Record<string, unknown> | undefined; |
|
|
| if (!row) return null; |
|
|
| return this.rowToProject(row); |
| } |
|
|
| async updateProject(project: Project): Promise<void> { |
| const db = this.getDB(); |
| const stmt = db.prepare(` |
| UPDATE projects SET |
| name = ?, |
| description = ?, |
| updated_at = ?, |
| last_saved_at = ?, |
| last_saved_checkpoint_id = ?, |
| settings = ?, |
| cost_tracking = ?, |
| preview_image = ?, |
| last_synced_at = ?, |
| server_updated_at = ? |
| WHERE id = ? |
| `); |
|
|
| stmt.run( |
| project.name, |
| project.description ?? null, |
| toISOStringRequired(project.updatedAt), |
| toISOString(project.lastSavedAt), |
| project.lastSavedCheckpointId ?? null, |
| JSON.stringify(project.settings ?? {}), |
| JSON.stringify(project.costTracking ?? {}), |
| project.previewImage ?? null, |
| toISOString(project.lastSyncedAt), |
| toISOString(project.serverUpdatedAt), |
| project.id |
| ); |
| } |
|
|
| async deleteProject(id: string): Promise<void> { |
| const db = this.getDB(); |
|
|
| |
| db.prepare('DELETE FROM projects WHERE id = ?').run(id); |
|
|
| |
| if (deploymentExists(id)) { |
| this.deploymentDatabases.delete(id); |
| deleteDeploymentDatabase(id); |
| } |
|
|
| |
| this.projectDatabases.delete(id); |
| deleteProjectDatabase(id, this.baseDir); |
| } |
|
|
| async listProjects(fields?: string[]): Promise<Project[]> { |
| const db = this.getDB(); |
|
|
| |
| |
| const rows = db.prepare('SELECT * FROM projects ORDER BY updated_at DESC').all() as Record<string, unknown>[]; |
|
|
| return rows.map(row => this.rowToProject(row)); |
| } |
|
|
| listProjectSummaries(): Array<{ id: string; name: string; updatedAt: string }> { |
| const db = this.getDB(); |
| return db.prepare('SELECT id, name, updated_at as updatedAt FROM projects').all() as Array<{ id: string; name: string; updatedAt: string }>; |
| } |
|
|
| listSkillSummaries(): Array<{ id: string; name: string; updatedAt: string }> { |
| const db = this.getDB(); |
| return db.prepare("SELECT id, name, updated_at as updatedAt FROM skills WHERE is_built_in = 0").all() as Array<{ id: string; name: string; updatedAt: string }>; |
| } |
|
|
| listTemplateSummaries(): Array<{ id: string; name: string; updatedAt: string }> { |
| const db = this.getDB(); |
| return db.prepare('SELECT id, name, COALESCE(updated_at, imported_at) as updatedAt FROM custom_templates').all() as Array<{ id: string; name: string; updatedAt: string }>; |
| } |
|
|
| countDeployments(): number { |
| const db = this.getDB(); |
| const row = db.prepare('SELECT COUNT(*) as c FROM deployments').get() as { c: number } | undefined; |
| return row?.c ?? 0; |
| } |
|
|
| |
| |
| |
| private rowToProject(row: Record<string, unknown>): Project { |
| return { |
| id: row.id as string, |
| name: row.name as string, |
| description: row.description as string | undefined, |
| createdAt: parseDate(row.created_at as string), |
| updatedAt: parseDate(row.updated_at as string), |
| lastSavedAt: row.last_saved_at ? parseDate(row.last_saved_at as string) : undefined, |
| lastSavedCheckpointId: row.last_saved_checkpoint_id as string | undefined, |
| settings: parseJSON(row.settings as string, {}), |
| costTracking: parseJSON(row.cost_tracking as string, undefined), |
| previewImage: row.preview_image as string | undefined, |
| lastSyncedAt: row.last_synced_at ? parseDate(row.last_synced_at as string) : undefined, |
| serverUpdatedAt: row.server_updated_at ? parseDate(row.server_updated_at as string) : undefined, |
| }; |
| } |
|
|
| |
| |
| |
|
|
| async createFile(file: VirtualFile): Promise<void> { |
| const db = this.getDB(); |
|
|
| |
| |
| let content: string; |
| if (file.content instanceof ArrayBuffer) { |
| content = Buffer.from(file.content).toString('base64'); |
| } else if (typeof file.content === 'string') { |
| content = file.content; |
| } else { |
| content = ''; |
| } |
|
|
| const stmt = db.prepare(` |
| INSERT OR REPLACE INTO files ( |
| id, project_id, path, name, type, content, |
| mime_type, size, created_at, updated_at, metadata |
| ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) |
| `); |
|
|
| stmt.run( |
| file.id, |
| file.projectId, |
| file.path, |
| file.name, |
| file.type, |
| content, |
| file.mimeType ?? null, |
| file.size ?? 0, |
| toISOStringRequired(file.createdAt), |
| toISOStringRequired(file.updatedAt), |
| JSON.stringify(file.metadata ?? {}) |
| ); |
| } |
|
|
| async getFile(projectId: string, path: string): Promise<VirtualFile | null> { |
| const db = this.getDB(); |
| const row = db.prepare( |
| 'SELECT * FROM files WHERE project_id = ? AND path = ?' |
| ).get(projectId, path) as Record<string, unknown> | undefined; |
|
|
| if (!row) return null; |
| return this.rowToFile(row, projectId); |
| } |
|
|
| async updateFile(file: VirtualFile): Promise<void> { |
| const db = this.getDB(); |
|
|
| |
| |
| let content: string; |
| if (file.content instanceof ArrayBuffer) { |
| content = Buffer.from(file.content).toString('base64'); |
| } else if (typeof file.content === 'string') { |
| content = file.content; |
| } else { |
| content = ''; |
| } |
|
|
| const stmt = db.prepare(` |
| UPDATE files SET |
| name = ?, type = ?, content = ?, mime_type = ?, |
| size = ?, updated_at = ?, metadata = ? |
| WHERE project_id = ? AND path = ? |
| `); |
|
|
| stmt.run( |
| file.name, |
| file.type, |
| content, |
| file.mimeType ?? null, |
| file.size ?? 0, |
| toISOStringRequired(file.updatedAt), |
| JSON.stringify(file.metadata ?? {}), |
| file.projectId, |
| file.path |
| ); |
| } |
|
|
| async deleteFile(projectId: string, path: string): Promise<void> { |
| const db = this.getDB(); |
| db.prepare('DELETE FROM files WHERE project_id = ? AND path = ?').run(projectId, path); |
| } |
|
|
| async listFiles(projectId: string): Promise<VirtualFile[]> { |
| const db = this.getDB(); |
| const rows = db.prepare( |
| 'SELECT * FROM files WHERE project_id = ? ORDER BY path' |
| ).all(projectId) as Record<string, unknown>[]; |
|
|
| return rows.map(row => this.rowToFile(row, projectId)); |
| } |
|
|
| async deleteProjectFiles(projectId: string): Promise<void> { |
| const db = this.getDB(); |
| db.prepare('DELETE FROM files WHERE project_id = ?').run(projectId); |
| } |
|
|
| private rowToFile(row: Record<string, unknown>, projectId: string): VirtualFile { |
| const type = row.type as VirtualFile['type']; |
| const rawContent = row.content as string; |
|
|
| |
| let content: string | ArrayBuffer = rawContent; |
| if ((type === 'image' || type === 'video') && rawContent) { |
| try { |
| |
| let base64Data = rawContent; |
| if (rawContent.startsWith('data:')) { |
| const commaIndex = rawContent.indexOf(','); |
| if (commaIndex !== -1) { |
| base64Data = rawContent.slice(commaIndex + 1); |
| } |
| } |
| const buffer = Buffer.from(base64Data, 'base64'); |
| content = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength); |
| } catch { |
| |
| content = rawContent; |
| } |
| } |
|
|
| return { |
| id: row.id as string, |
| projectId, |
| path: row.path as string, |
| name: row.name as string, |
| type, |
| content, |
| mimeType: row.mime_type as string, |
| size: row.size as number, |
| createdAt: parseDate(row.created_at as string), |
| updatedAt: parseDate(row.updated_at as string), |
| metadata: parseJSON(row.metadata as string, {}), |
| }; |
| } |
|
|
| |
| |
| |
|
|
| async createTreeNode(node: FileTreeNode): Promise<void> { |
| const db = this.getDB(); |
| const stmt = db.prepare(` |
| INSERT INTO file_tree_nodes ( |
| id, project_id, path, name, type, parent_path, is_expanded, metadata |
| ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) |
| `); |
|
|
| stmt.run( |
| node.id, |
| node.projectId, |
| node.path, |
| node.name, |
| node.type, |
| node.parentPath ?? null, |
| node.isExpanded ? 1 : 0, |
| JSON.stringify(node.metadata ?? {}) |
| ); |
| } |
|
|
| async getTreeNode(projectId: string, path: string): Promise<FileTreeNode | null> { |
| const db = this.getDB(); |
| const row = db.prepare( |
| 'SELECT * FROM file_tree_nodes WHERE project_id = ? AND path = ?' |
| ).get(projectId, path) as Record<string, unknown> | undefined; |
|
|
| if (!row) return null; |
| return this.rowToTreeNode(row, projectId); |
| } |
|
|
| async updateTreeNode(node: FileTreeNode): Promise<void> { |
| const db = this.getDB(); |
| const stmt = db.prepare(` |
| UPDATE file_tree_nodes SET |
| name = ?, type = ?, parent_path = ?, is_expanded = ?, metadata = ? |
| WHERE project_id = ? AND path = ? |
| `); |
|
|
| stmt.run( |
| node.name, |
| node.type, |
| node.parentPath ?? null, |
| node.isExpanded ? 1 : 0, |
| JSON.stringify(node.metadata ?? {}), |
| node.projectId, |
| node.path |
| ); |
| } |
|
|
| async deleteTreeNode(projectId: string, path: string): Promise<void> { |
| const db = this.getDB(); |
| db.prepare('DELETE FROM file_tree_nodes WHERE project_id = ? AND path = ?').run(projectId, path); |
| } |
|
|
| async getChildNodes(projectId: string, parentPath: string | null): Promise<FileTreeNode[]> { |
| const db = this.getDB(); |
| let rows: Record<string, unknown>[]; |
|
|
| if (parentPath === null) { |
| rows = db.prepare( |
| 'SELECT * FROM file_tree_nodes WHERE project_id = ? AND parent_path IS NULL ORDER BY type DESC, name' |
| ).all(projectId) as Record<string, unknown>[]; |
| } else { |
| rows = db.prepare( |
| 'SELECT * FROM file_tree_nodes WHERE project_id = ? AND parent_path = ? ORDER BY type DESC, name' |
| ).all(projectId, parentPath) as Record<string, unknown>[]; |
| } |
|
|
| return rows.map(row => this.rowToTreeNode(row, projectId)); |
| } |
|
|
| async getAllTreeNodes(projectId: string): Promise<FileTreeNode[]> { |
| const db = this.getDB(); |
| const rows = db.prepare( |
| 'SELECT * FROM file_tree_nodes WHERE project_id = ? ORDER BY path' |
| ).all(projectId) as Record<string, unknown>[]; |
|
|
| return rows.map(row => this.rowToTreeNode(row, projectId)); |
| } |
|
|
| private rowToTreeNode(row: Record<string, unknown>, projectId: string): FileTreeNode { |
| return { |
| id: row.id as string, |
| projectId, |
| path: row.path as string, |
| name: row.name as string, |
| type: row.type as 'file' | 'directory', |
| parentPath: row.parent_path as string | null, |
| isExpanded: Boolean(row.is_expanded), |
| metadata: parseJSON(row.metadata as string, {}), |
| }; |
| } |
|
|
| |
| |
| |
|
|
| async saveCustomTemplate(template: CustomTemplate): Promise<void> { |
| const db = this.getDB(); |
|
|
| |
| const existing = db.prepare('SELECT id FROM custom_templates WHERE id = ?').get(template.id); |
|
|
| if (existing) { |
| const stmt = db.prepare(` |
| UPDATE custom_templates SET |
| name = ?, description = ?, version = ?, |
| files = ?, directories = ?, assets = ?, |
| metadata = ?, updated_at = ? |
| WHERE id = ? |
| `); |
| stmt.run( |
| template.name, |
| template.description, |
| template.version, |
| JSON.stringify(template.files ?? []), |
| JSON.stringify(template.directories ?? []), |
| JSON.stringify(template.assets ?? []), |
| JSON.stringify(template.metadata ?? {}), |
| new Date().toISOString(), |
| template.id |
| ); |
| } else { |
| const importedAt = toISOStringRequired(template.importedAt); |
| const stmt = db.prepare(` |
| INSERT INTO custom_templates ( |
| id, name, description, version, |
| files, directories, assets, metadata, imported_at, updated_at |
| ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) |
| `); |
| stmt.run( |
| template.id, |
| template.name, |
| template.description, |
| template.version, |
| JSON.stringify(template.files ?? []), |
| JSON.stringify(template.directories ?? []), |
| JSON.stringify(template.assets ?? []), |
| JSON.stringify(template.metadata ?? {}), |
| importedAt, |
| importedAt |
| ); |
| } |
| } |
|
|
| async getCustomTemplate(id: string): Promise<CustomTemplate | null> { |
| const db = this.getDB(); |
| const row = db.prepare('SELECT * FROM custom_templates WHERE id = ?').get(id) as Record<string, unknown> | undefined; |
|
|
| if (!row) return null; |
|
|
| return this.rowToTemplate(row); |
| } |
|
|
| async getAllCustomTemplates(): Promise<CustomTemplate[]> { |
| const db = this.getDB(); |
| const rows = db.prepare('SELECT * FROM custom_templates ORDER BY imported_at DESC').all() as Record<string, unknown>[]; |
|
|
| return rows.map(row => this.rowToTemplate(row)); |
| } |
|
|
| async deleteCustomTemplate(id: string): Promise<void> { |
| const db = this.getDB(); |
| db.prepare('DELETE FROM custom_templates WHERE id = ?').run(id); |
| } |
|
|
| private rowToTemplate(row: Record<string, unknown>): CustomTemplate { |
| return { |
| id: row.id as string, |
| name: row.name as string, |
| description: row.description as string, |
| version: row.version as string, |
| files: parseJSON(row.files as string, []), |
| directories: parseJSON(row.directories as string, []), |
| assets: parseJSON(row.assets as string, []), |
| metadata: parseJSON(row.metadata as string, { license: 'personal' }), |
| importedAt: parseDate(row.imported_at as string), |
| }; |
| } |
|
|
| |
| |
| |
|
|
| async createSkill(skill: Omit<Skill, 'isBuiltIn'>): Promise<void> { |
| const db = this.getDB(); |
| const stmt = db.prepare(` |
| INSERT INTO skills ( |
| id, name, description, content, markdown, |
| is_built_in, created_at, updated_at |
| ) VALUES (?, ?, ?, ?, ?, 0, ?, ?) |
| `); |
|
|
| stmt.run( |
| skill.id, |
| skill.name, |
| skill.description, |
| skill.content, |
| skill.markdown, |
| toISOStringRequired(skill.createdAt), |
| toISOStringRequired(skill.updatedAt) |
| ); |
| } |
|
|
| async getSkill(id: string): Promise<Skill | null> { |
| const db = this.getDB(); |
| const row = db.prepare('SELECT * FROM skills WHERE id = ?').get(id) as Record<string, unknown> | undefined; |
|
|
| if (!row) return null; |
|
|
| return this.rowToSkill(row); |
| } |
|
|
| async updateSkill(skill: Omit<Skill, 'isBuiltIn'>): Promise<void> { |
| const db = this.getDB(); |
| const stmt = db.prepare(` |
| UPDATE skills SET |
| name = ?, description = ?, content = ?, |
| markdown = ?, updated_at = ? |
| WHERE id = ? AND is_built_in = 0 |
| `); |
|
|
| stmt.run( |
| skill.name, |
| skill.description, |
| skill.content, |
| skill.markdown, |
| toISOStringRequired(skill.updatedAt), |
| skill.id |
| ); |
| } |
|
|
| async deleteSkill(id: string): Promise<void> { |
| const db = this.getDB(); |
| |
| db.prepare('DELETE FROM skills WHERE id = ? AND is_built_in = 0').run(id); |
| } |
|
|
| async getAllSkills(): Promise<Skill[]> { |
| const db = this.getDB(); |
| const rows = db.prepare('SELECT * FROM skills ORDER BY name').all() as Record<string, unknown>[]; |
|
|
| return rows.map(row => this.rowToSkill(row)); |
| } |
|
|
| private rowToSkill(row: Record<string, unknown>): Skill { |
| return { |
| id: row.id as string, |
| name: row.name as string, |
| description: row.description as string, |
| content: row.content as string, |
| markdown: row.markdown as string, |
| isBuiltIn: Boolean(row.is_built_in), |
| createdAt: parseDate(row.created_at as string), |
| updatedAt: parseDate(row.updated_at as string), |
| }; |
| } |
|
|
| |
| |
| |
|
|
| async createModelTemplate(t: ModelTemplate): Promise<void> { |
| const db = this.getDB(); |
| db.prepare(` |
| INSERT INTO model_templates (id, name, description, assignment, updated_at) |
| VALUES (?, ?, ?, ?, ?) |
| `).run( |
| t.id, |
| t.name, |
| t.description ?? null, |
| JSON.stringify(t.assignment ?? {}), |
| toISOStringRequired(t.updatedAt ?? new Date()), |
| ); |
| } |
|
|
| async updateModelTemplate(t: ModelTemplate): Promise<void> { |
| const db = this.getDB(); |
| db.prepare(` |
| UPDATE model_templates SET name = ?, description = ?, assignment = ?, updated_at = ? |
| WHERE id = ? |
| `).run( |
| t.name, |
| t.description ?? null, |
| JSON.stringify(t.assignment ?? {}), |
| toISOStringRequired(t.updatedAt ?? new Date()), |
| t.id, |
| ); |
| } |
|
|
| async getModelTemplate(id: string): Promise<ModelTemplate | null> { |
| const db = this.getDB(); |
| const row = db.prepare('SELECT * FROM model_templates WHERE id = ?').get(id) as Record<string, unknown> | undefined; |
| return row ? this.rowToModelTemplate(row) : null; |
| } |
|
|
| async getAllModelTemplates(): Promise<ModelTemplate[]> { |
| const db = this.getDB(); |
| const rows = db.prepare('SELECT * FROM model_templates ORDER BY name').all() as Record<string, unknown>[]; |
| return rows.map((row) => this.rowToModelTemplate(row)); |
| } |
|
|
| async deleteModelTemplate(id: string): Promise<void> { |
| const db = this.getDB(); |
| db.prepare('DELETE FROM model_templates WHERE id = ?').run(id); |
| } |
|
|
| listModelTemplateSummaries(): Array<{ id: string; name: string; updatedAt: string }> { |
| const db = this.getDB(); |
| return db.prepare('SELECT id, name, updated_at as updatedAt FROM model_templates').all() as Array<{ id: string; name: string; updatedAt: string }>; |
| } |
|
|
| private rowToModelTemplate(row: Record<string, unknown>): ModelTemplate { |
| return { |
| id: row.id as string, |
| name: row.name as string, |
| description: (row.description as string) ?? undefined, |
| assignment: parseJSON(row.assignment as string, {} as ModelAssignment), |
| updatedAt: parseDate(row.updated_at as string), |
| builtin: false, |
| }; |
| } |
|
|
| |
| |
| |
|
|
| async createInterviewTemplate(t: import('@/lib/interview/types').InterviewTemplate): Promise<void> { |
| const db = this.getDB(); |
| db.prepare(` |
| INSERT INTO interview_templates (id, name, description, spec, updated_at) |
| VALUES (?, ?, ?, ?, ?) |
| `).run( |
| t.id, t.title, t.description ?? null, |
| JSON.stringify({ items: t.items, artifacts: t.artifacts, handoff: t.handoff ?? null }), |
| toISOStringRequired(t.updatedAt ?? new Date()), |
| ); |
| } |
|
|
| async updateInterviewTemplate(t: import('@/lib/interview/types').InterviewTemplate): Promise<void> { |
| const db = this.getDB(); |
| db.prepare(` |
| UPDATE interview_templates SET name = ?, description = ?, spec = ?, updated_at = ? |
| WHERE id = ? |
| `).run( |
| t.title, t.description ?? null, |
| JSON.stringify({ items: t.items, artifacts: t.artifacts, handoff: t.handoff ?? null }), |
| toISOStringRequired(t.updatedAt ?? new Date()), t.id, |
| ); |
| } |
|
|
| async getInterviewTemplate(id: string): Promise<import('@/lib/interview/types').InterviewTemplate | null> { |
| const db = this.getDB(); |
| const row = db.prepare('SELECT * FROM interview_templates WHERE id = ?').get(id) as Record<string, unknown> | undefined; |
| return row ? this.rowToInterviewTemplate(row) : null; |
| } |
|
|
| async getAllInterviewTemplates(): Promise<import('@/lib/interview/types').InterviewTemplate[]> { |
| const db = this.getDB(); |
| const rows = db.prepare('SELECT * FROM interview_templates ORDER BY name').all() as Record<string, unknown>[]; |
| return rows.map((r) => this.rowToInterviewTemplate(r)); |
| } |
|
|
| async deleteInterviewTemplate(id: string): Promise<void> { |
| this.getDB().prepare('DELETE FROM interview_templates WHERE id = ?').run(id); |
| } |
|
|
| listInterviewTemplateSummaries(): Array<{ id: string; name: string; updatedAt: string }> { |
| return this.getDB().prepare('SELECT id, name, updated_at as updatedAt FROM interview_templates') |
| .all() as Array<{ id: string; name: string; updatedAt: string }>; |
| } |
|
|
| private rowToInterviewTemplate(row: Record<string, unknown>): import('@/lib/interview/types').InterviewTemplate { |
| const spec = parseJSON<{ |
| items: import('@/lib/interview/types').InterviewItem[]; |
| artifacts: { path: string; description?: string }[]; |
| handoff: import('@/lib/interview/types').InterviewHandoff | null; |
| }>(row.spec as string, { items: [], artifacts: [], handoff: null }); |
| return { |
| id: row.id as string, |
| title: row.name as string, |
| description: (row.description as string) ?? '', |
| items: spec.items ?? [], |
| artifacts: spec.artifacts ?? [], |
| handoff: spec.handoff ?? undefined, |
| updatedAt: row.updated_at ? parseDate(row.updated_at as string) : undefined, |
| }; |
| } |
|
|
| |
| |
| |
|
|
| async createConnection(c: CustomConnection): Promise<void> { |
| const db = this.getDB(); |
| db.prepare(` |
| INSERT INTO connections (id, name, base_url, format, api_key_required, updated_at) |
| VALUES (?, ?, ?, ?, ?, ?) |
| `).run( |
| c.id, |
| c.name, |
| c.baseUrl, |
| c.format, |
| c.apiKeyRequired ? 1 : 0, |
| toISOStringRequired(c.updatedAt ?? new Date()), |
| ); |
| } |
|
|
| async updateConnection(c: CustomConnection): Promise<void> { |
| const db = this.getDB(); |
| db.prepare(` |
| UPDATE connections SET name = ?, base_url = ?, format = ?, api_key_required = ?, updated_at = ? |
| WHERE id = ? |
| `).run( |
| c.name, |
| c.baseUrl, |
| c.format, |
| c.apiKeyRequired ? 1 : 0, |
| toISOStringRequired(c.updatedAt ?? new Date()), |
| c.id, |
| ); |
| } |
|
|
| async getConnection(id: string): Promise<CustomConnection | null> { |
| const db = this.getDB(); |
| const row = db.prepare('SELECT * FROM connections WHERE id = ?').get(id) as Record<string, unknown> | undefined; |
| return row ? this.rowToConnection(row) : null; |
| } |
|
|
| async getAllConnections(): Promise<CustomConnection[]> { |
| const db = this.getDB(); |
| const rows = db.prepare('SELECT * FROM connections ORDER BY name').all() as Record<string, unknown>[]; |
| return rows.map((row) => this.rowToConnection(row)); |
| } |
|
|
| async deleteConnection(id: string): Promise<void> { |
| const db = this.getDB(); |
| db.prepare('DELETE FROM connections WHERE id = ?').run(id); |
| } |
|
|
| private rowToConnection(row: Record<string, unknown>): CustomConnection { |
| return { |
| id: row.id as string, |
| name: row.name as string, |
| baseUrl: row.base_url as string, |
| format: (row.format as 'openai') ?? 'openai', |
| apiKeyRequired: row.api_key_required === 1, |
| updatedAt: row.updated_at as string, |
| }; |
| } |
|
|
| |
| |
| |
|
|
| async createDeployment(deployment: Deployment): Promise<void> { |
| const db = this.getDB(); |
| const stmt = db.prepare(` |
| INSERT INTO deployments ( |
| id, project_id, name, slug, enabled, under_construction, |
| custom_domain, head_scripts, body_scripts, cdn_links, |
| analytics, seo, compliance, settings_version, |
| last_published_version, preview_image, preview_updated_at, |
| database_enabled, created_at, updated_at, published_at |
| ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) |
| `); |
|
|
| stmt.run( |
| deployment.id, |
| deployment.projectId, |
| deployment.name, |
| deployment.slug ?? null, |
| deployment.enabled ? 1 : 0, |
| deployment.underConstruction ? 1 : 0, |
| deployment.customDomain ?? null, |
| JSON.stringify(deployment.headScripts ?? []), |
| JSON.stringify(deployment.bodyScripts ?? []), |
| JSON.stringify(deployment.cdnLinks ?? []), |
| JSON.stringify(deployment.analytics ?? DEFAULT_ANALYTICS), |
| JSON.stringify(deployment.seo ?? DEFAULT_SEO), |
| JSON.stringify(deployment.compliance ?? DEFAULT_COMPLIANCE), |
| deployment.settingsVersion ?? 1, |
| deployment.lastPublishedVersion ?? null, |
| deployment.previewImage ?? null, |
| toISOString(deployment.previewUpdatedAt), |
| deployment.databaseEnabled ? 1 : 0, |
| toISOStringRequired(deployment.createdAt), |
| toISOStringRequired(deployment.updatedAt), |
| toISOString(deployment.publishedAt) |
| ); |
| } |
|
|
| async getDeployment(deploymentId: string): Promise<Deployment | null> { |
| const db = this.getDB(); |
| const row = db.prepare('SELECT * FROM deployments WHERE id = ?').get(deploymentId) as Record<string, unknown> | undefined; |
|
|
| if (!row) return null; |
| return this.rowToDeployment(row); |
| } |
|
|
| async getDeploymentBySlug(slug: string): Promise<Deployment | null> { |
| const db = this.getDB(); |
| const row = db.prepare('SELECT * FROM deployments WHERE slug = ?').get(slug) as Record<string, unknown> | undefined; |
|
|
| if (!row) return null; |
| return this.rowToDeployment(row); |
| } |
|
|
| async listDeployments(): Promise<Deployment[]> { |
| const db = this.getDB(); |
| const rows = db.prepare('SELECT * FROM deployments ORDER BY updated_at DESC').all() as Record<string, unknown>[]; |
|
|
| return rows.map(row => this.rowToDeployment(row)); |
| } |
|
|
| async listDeploymentsByProject(projectId: string): Promise<Deployment[]> { |
| const db = this.getDB(); |
| const rows = db.prepare('SELECT * FROM deployments WHERE project_id = ? ORDER BY created_at').all(projectId) as Record<string, unknown>[]; |
|
|
| return rows.map(row => this.rowToDeployment(row)); |
| } |
|
|
| async updateDeployment(deployment: Deployment): Promise<void> { |
| const db = this.getDB(); |
| const stmt = db.prepare(` |
| UPDATE deployments SET |
| project_id = ?, name = ?, slug = ?, enabled = ?, under_construction = ?, |
| custom_domain = ?, head_scripts = ?, body_scripts = ?, cdn_links = ?, |
| analytics = ?, seo = ?, compliance = ?, settings_version = ?, |
| last_published_version = ?, preview_image = ?, preview_updated_at = ?, |
| database_enabled = ?, updated_at = ?, published_at = ? |
| WHERE id = ? |
| `); |
|
|
| stmt.run( |
| deployment.projectId, |
| deployment.name, |
| deployment.slug ?? null, |
| deployment.enabled ? 1 : 0, |
| deployment.underConstruction ? 1 : 0, |
| deployment.customDomain ?? null, |
| JSON.stringify(deployment.headScripts ?? []), |
| JSON.stringify(deployment.bodyScripts ?? []), |
| JSON.stringify(deployment.cdnLinks ?? []), |
| JSON.stringify(deployment.analytics ?? DEFAULT_ANALYTICS), |
| JSON.stringify(deployment.seo ?? DEFAULT_SEO), |
| JSON.stringify(deployment.compliance ?? DEFAULT_COMPLIANCE), |
| deployment.settingsVersion ?? 1, |
| deployment.lastPublishedVersion ?? null, |
| deployment.previewImage ?? null, |
| toISOString(deployment.previewUpdatedAt), |
| deployment.databaseEnabled ? 1 : 0, |
| toISOStringRequired(deployment.updatedAt), |
| toISOString(deployment.publishedAt), |
| deployment.id |
| ); |
| } |
|
|
| async deleteDeployment(deploymentId: string): Promise<void> { |
| const db = this.getDB(); |
|
|
| |
| db.prepare('DELETE FROM deployments WHERE id = ?').run(deploymentId); |
|
|
| |
| const deploymentDb = this.deploymentDatabases.get(deploymentId); |
| if (deploymentDb) { |
| deploymentDb.close(); |
| this.deploymentDatabases.delete(deploymentId); |
| } |
|
|
| if (deploymentExists(deploymentId)) { |
| deleteDeploymentDatabase(deploymentId); |
| } |
| } |
|
|
| private rowToDeployment(row: Record<string, unknown>): Deployment { |
| return { |
| id: row.id as string, |
| projectId: row.project_id as string, |
| name: row.name as string, |
| slug: row.slug as string | undefined, |
| enabled: Boolean(row.enabled), |
| underConstruction: Boolean(row.under_construction), |
| customDomain: row.custom_domain as string | undefined, |
| headScripts: parseJSON(row.head_scripts as string, []), |
| bodyScripts: parseJSON(row.body_scripts as string, []), |
| cdnLinks: parseJSON(row.cdn_links as string, []), |
| analytics: parseJSON(row.analytics as string, DEFAULT_ANALYTICS), |
| seo: parseJSON(row.seo as string, DEFAULT_SEO), |
| compliance: parseJSON(row.compliance as string, DEFAULT_COMPLIANCE), |
| settingsVersion: row.settings_version as number ?? 1, |
| lastPublishedVersion: row.last_published_version as number | undefined, |
| previewImage: row.preview_image as string | undefined, |
| previewUpdatedAt: row.preview_updated_at ? parseDate(row.preview_updated_at as string) : undefined, |
| databaseEnabled: Boolean(row.database_enabled), |
| createdAt: parseDate(row.created_at as string), |
| updatedAt: parseDate(row.updated_at as string), |
| publishedAt: row.published_at ? parseDate(row.published_at as string) : undefined, |
| }; |
| } |
|
|
| |
| |
| |
|
|
| |
| |
| |
| |
| |
| getDeploymentDatabaseForAnalytics(deploymentId: string): DeploymentDatabase | null { |
| |
| const deployment = this.getDeploymentSync(deploymentId); |
| if (!deployment || !deployment.databaseEnabled) return null; |
|
|
| |
| return this.getOrCreateDeploymentDB(deploymentId); |
| } |
|
|
| |
| |
| |
| |
| getAnalyticsDatabaseInstance(deploymentId: string): AnalyticsDatabase | null { |
| const deployment = this.getDeploymentSync(deploymentId); |
| if (!deployment || !deployment.databaseEnabled) return null; |
|
|
| let analyticsDb = this.analyticsDatabases.get(deploymentId); |
| if (!analyticsDb) { |
| analyticsDb = new AnalyticsDatabase(deploymentId); |
| analyticsDb.init(); |
| this.analyticsDatabases.set(deploymentId, analyticsDb); |
| } |
| return analyticsDb; |
| } |
|
|
| |
| |
| |
| private getDeploymentSync(deploymentId: string): Deployment | null { |
| const db = this.getDB(); |
| const row = db.prepare('SELECT * FROM deployments WHERE id = ?').get(deploymentId) as Record<string, unknown> | undefined; |
|
|
| if (!row) return null; |
| return this.rowToDeployment(row); |
| } |
| } |
|
|