Download lib/vfs/adapters/runtime-database.ts from Leon4gr45/builder: direct link, hf CLI and curl.
- Browser
- Download file 42.5 kB
-
https://huggingface.co/spaces/Leon4gr45/builder/resolve/main/lib/vfs/adapters/runtime-database.ts
- Command line
-
hf download hf://spaces/Leon4gr45/builder/lib/vfs/adapters/runtime-database.ts
-
curl -L -o runtime-database.ts https://huggingface.co/spaces/Leon4gr45/builder/resolve/main/lib/vfs/adapters/runtime-database.ts
42.5 kB
| /** | |
| * Runtime Database Manager | |
| * | |
| * Manages the runtime portion of per-deployment SQLite databases containing: | |
| * - Deployment info/settings (single row) | |
| * - Files and file tree nodes | |
| * - Edge functions and execution logs | |
| * - Server functions | |
| * - Secrets (encrypted) | |
| * - Scheduled functions | |
| * - User-created tables (DDL execution) | |
| * | |
| * Each deployment gets its own runtime database at deployments/{deploymentId}/runtime.sqlite | |
| */ | |
| import type { Database } from 'better-sqlite3'; | |
| import { v4 as uuidv4 } from 'uuid'; | |
| import { VirtualFile, FileTreeNode, Deployment, EdgeFunction, FunctionLog, TableInfo, ServerFunction, Secret, ScheduledFunction } from '../types'; | |
| import { encryptSecret, isEncryptionConfigured } from '../../edge-functions/secrets-crypto'; | |
| import { getRuntimeDatabaseConnection, closeRuntimeDatabase } from './sqlite-connection'; | |
| /** | |
| * Helper to ensure a value is a Date and convert to ISO string | |
| */ | |
| function toISOString(value: Date | string | null | undefined): string | null { | |
| if (!value) return null; | |
| if (typeof value === 'string') return value; | |
| return value.toISOString(); | |
| } | |
| /** | |
| * Helper to ensure a value is a Date and convert to ISO string (non-null) | |
| */ | |
| function toISOStringRequired(value: Date | string): string { | |
| if (typeof value === 'string') return value; | |
| return value.toISOString(); | |
| } | |
| /** | |
| * Helper to parse JSON safely with a fallback | |
| */ | |
| function parseJSON<T>(value: string | null | undefined, fallback: T): T { | |
| if (!value) return fallback; | |
| try { | |
| return JSON.parse(value) as T; | |
| } catch { | |
| return fallback; | |
| } | |
| } | |
| /** | |
| * Helper to parse Date from ISO string | |
| */ | |
| function parseDate(value: string | null | undefined): Date { | |
| if (!value) return new Date(); | |
| return new Date(value); | |
| } | |
| /** | |
| * Per-deployment runtime database manager | |
| */ | |
| export class RuntimeDatabase { | |
| private db: Database; | |
| private deploymentId: string; | |
| private initialized = false; | |
| constructor(deploymentId: string) { | |
| this.deploymentId = deploymentId; | |
| this.db = getRuntimeDatabaseConnection(deploymentId); | |
| } | |
| /** | |
| * Initialize the database schema | |
| */ | |
| init(): void { | |
| if (this.initialized) return; | |
| // Deployment info (single row) | |
| this.db.exec(` | |
| CREATE TABLE IF NOT EXISTS site_info ( | |
| id TEXT PRIMARY KEY DEFAULT 'main', | |
| project_id TEXT NOT NULL, | |
| name TEXT NOT NULL, | |
| slug TEXT, | |
| enabled INTEGER NOT NULL DEFAULT 0, | |
| under_construction INTEGER NOT NULL DEFAULT 0, | |
| custom_domain TEXT, | |
| head_scripts TEXT NOT NULL DEFAULT '[]', | |
| body_scripts TEXT NOT NULL DEFAULT '[]', | |
| cdn_links TEXT NOT NULL DEFAULT '[]', | |
| analytics TEXT NOT NULL DEFAULT '{}', | |
| seo TEXT NOT NULL DEFAULT '{}', | |
| compliance TEXT NOT NULL DEFAULT '{}', | |
| settings_version INTEGER NOT NULL DEFAULT 1, | |
| last_published_version INTEGER, | |
| preview_image TEXT, | |
| preview_updated_at TEXT, | |
| created_at TEXT NOT NULL, | |
| updated_at TEXT NOT NULL, | |
| published_at TEXT | |
| ) | |
| `); | |
| // Files | |
| this.db.exec(` | |
| CREATE TABLE IF NOT EXISTS files ( | |
| id TEXT PRIMARY KEY, | |
| path TEXT NOT NULL UNIQUE, | |
| name TEXT NOT NULL, | |
| type TEXT NOT NULL, | |
| content TEXT, | |
| mime_type TEXT NOT NULL, | |
| size INTEGER NOT NULL, | |
| created_at TEXT NOT NULL, | |
| updated_at TEXT NOT NULL, | |
| metadata TEXT DEFAULT '{}' | |
| ) | |
| `); | |
| this.db.exec(`CREATE INDEX IF NOT EXISTS idx_files_path ON files(path)`); | |
| // File tree nodes | |
| this.db.exec(` | |
| CREATE TABLE IF NOT EXISTS file_tree_nodes ( | |
| id TEXT PRIMARY KEY, | |
| path TEXT NOT NULL UNIQUE, | |
| name TEXT NOT NULL, | |
| type TEXT NOT NULL, | |
| parent_path TEXT, | |
| is_expanded INTEGER DEFAULT 0, | |
| children TEXT DEFAULT '[]', | |
| metadata TEXT DEFAULT '{}' | |
| ) | |
| `); | |
| this.db.exec(`CREATE INDEX IF NOT EXISTS idx_tree_nodes_parent_path ON file_tree_nodes(parent_path)`); | |
| // Edge Functions | |
| this.db.exec(` | |
| CREATE TABLE IF NOT EXISTS edge_functions ( | |
| id TEXT PRIMARY KEY, | |
| name TEXT NOT NULL UNIQUE, | |
| description TEXT, | |
| code TEXT NOT NULL, | |
| method TEXT NOT NULL DEFAULT 'ANY', | |
| enabled INTEGER NOT NULL DEFAULT 1, | |
| timeout_ms INTEGER NOT NULL DEFAULT 5000, | |
| created_at TEXT NOT NULL, | |
| updated_at TEXT NOT NULL | |
| ) | |
| `); | |
| this.db.exec(`CREATE INDEX IF NOT EXISTS idx_edge_functions_name ON edge_functions(name)`); | |
| // Function Execution Logs | |
| this.db.exec(` | |
| CREATE TABLE IF NOT EXISTS function_logs ( | |
| id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| function_id TEXT NOT NULL, | |
| method TEXT NOT NULL, | |
| path TEXT NOT NULL, | |
| status_code INTEGER, | |
| duration_ms INTEGER, | |
| error TEXT, | |
| timestamp TEXT NOT NULL DEFAULT (datetime('now')), | |
| FOREIGN KEY (function_id) REFERENCES edge_functions(id) ON DELETE CASCADE | |
| ) | |
| `); | |
| this.db.exec(`CREATE INDEX IF NOT EXISTS idx_function_logs_function_id ON function_logs(function_id)`); | |
| this.db.exec(`CREATE INDEX IF NOT EXISTS idx_function_logs_timestamp ON function_logs(timestamp)`); | |
| // Server Functions (callable from edge functions) | |
| this.db.exec(` | |
| CREATE TABLE IF NOT EXISTS server_functions ( | |
| id TEXT PRIMARY KEY, | |
| name TEXT NOT NULL UNIQUE, | |
| description TEXT, | |
| code TEXT NOT NULL, | |
| enabled INTEGER NOT NULL DEFAULT 1, | |
| created_at TEXT NOT NULL, | |
| updated_at TEXT NOT NULL | |
| ) | |
| `); | |
| this.db.exec(`CREATE INDEX IF NOT EXISTS idx_server_functions_name ON server_functions(name)`); | |
| // Secrets (encrypted key-value storage) | |
| this.db.exec(` | |
| CREATE TABLE IF NOT EXISTS secrets ( | |
| id TEXT PRIMARY KEY, | |
| name TEXT NOT NULL UNIQUE, | |
| encrypted_value TEXT NOT NULL, | |
| iv TEXT NOT NULL, | |
| auth_tag TEXT NOT NULL, | |
| description TEXT, | |
| created_at TEXT NOT NULL, | |
| updated_at TEXT NOT NULL | |
| ) | |
| `); | |
| this.db.exec(`CREATE INDEX IF NOT EXISTS idx_secrets_name ON secrets(name)`); | |
| // Scheduled Functions (cron-triggered edge function execution) | |
| this.db.exec(` | |
| CREATE TABLE IF NOT EXISTS scheduled_functions ( | |
| id TEXT PRIMARY KEY, | |
| name TEXT NOT NULL UNIQUE, | |
| description TEXT, | |
| function_id TEXT NOT NULL, | |
| cron_expression TEXT NOT NULL, | |
| timezone TEXT NOT NULL DEFAULT 'UTC', | |
| config TEXT NOT NULL DEFAULT '{}', | |
| enabled INTEGER NOT NULL 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, | |
| updated_at TEXT NOT NULL, | |
| FOREIGN KEY (function_id) REFERENCES edge_functions(id) ON DELETE CASCADE | |
| ) | |
| `); | |
| this.db.exec(`CREATE INDEX IF NOT EXISTS idx_scheduled_functions_name ON scheduled_functions(name)`); | |
| this.db.exec(`CREATE INDEX IF NOT EXISTS idx_scheduled_functions_next_run ON scheduled_functions(next_run_at)`); | |
| this.initialized = true; | |
| } | |
| /** | |
| * Close the database connection | |
| */ | |
| close(): void { | |
| closeRuntimeDatabase(this.deploymentId); | |
| } | |
| // ============================================ | |
| // Deployment Info | |
| // ============================================ | |
| createDeploymentInfo(deployment: Deployment): void { | |
| const stmt = this.db.prepare(` | |
| INSERT OR REPLACE INTO site_info ( | |
| 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, | |
| created_at, updated_at, published_at | |
| ) VALUES ( | |
| 'main', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? | |
| ) | |
| `); | |
| 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 ?? {}), | |
| JSON.stringify(deployment.seo ?? {}), | |
| JSON.stringify(deployment.compliance ?? {}), | |
| deployment.settingsVersion ?? 1, | |
| deployment.lastPublishedVersion ?? null, | |
| deployment.previewImage ?? null, | |
| toISOString(deployment.previewUpdatedAt), | |
| toISOStringRequired(deployment.createdAt), | |
| toISOStringRequired(deployment.updatedAt), | |
| toISOString(deployment.publishedAt) | |
| ); | |
| } | |
| getDeploymentInfo(): Deployment | null { | |
| const row = this.db.prepare('SELECT * FROM site_info WHERE id = ?').get('main') as Record<string, unknown> | undefined; | |
| if (!row) return null; | |
| return { | |
| id: this.deploymentId, | |
| 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, { enabled: false, provider: 'builtin' as const, privacyMode: true }), | |
| seo: parseJSON(row.seo as string, {}), | |
| compliance: parseJSON(row.compliance as string, { enabled: false, bannerPosition: 'bottom' as const, bannerStyle: 'bar' as const, message: '', acceptButtonText: 'Accept', declineButtonText: 'Decline', mode: 'opt-in' as const, blockAnalytics: false }), | |
| settingsVersion: row.settings_version as number, | |
| 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, | |
| createdAt: parseDate(row.created_at as string), | |
| updatedAt: parseDate(row.updated_at as string), | |
| publishedAt: row.published_at ? parseDate(row.published_at as string) : null, | |
| }; | |
| } | |
| updateDeploymentInfo(deployment: Partial<Deployment>): void { | |
| const updates: string[] = []; | |
| const values: unknown[] = []; | |
| if (deployment.name !== undefined) { updates.push('name = ?'); values.push(deployment.name); } | |
| if (deployment.slug !== undefined) { updates.push('slug = ?'); values.push(deployment.slug); } | |
| if (deployment.enabled !== undefined) { updates.push('enabled = ?'); values.push(deployment.enabled ? 1 : 0); } | |
| if (deployment.underConstruction !== undefined) { updates.push('under_construction = ?'); values.push(deployment.underConstruction ? 1 : 0); } | |
| if (deployment.customDomain !== undefined) { updates.push('custom_domain = ?'); values.push(deployment.customDomain); } | |
| if (deployment.headScripts !== undefined) { updates.push('head_scripts = ?'); values.push(JSON.stringify(deployment.headScripts)); } | |
| if (deployment.bodyScripts !== undefined) { updates.push('body_scripts = ?'); values.push(JSON.stringify(deployment.bodyScripts)); } | |
| if (deployment.cdnLinks !== undefined) { updates.push('cdn_links = ?'); values.push(JSON.stringify(deployment.cdnLinks)); } | |
| if (deployment.analytics !== undefined) { updates.push('analytics = ?'); values.push(JSON.stringify(deployment.analytics)); } | |
| if (deployment.seo !== undefined) { updates.push('seo = ?'); values.push(JSON.stringify(deployment.seo)); } | |
| if (deployment.compliance !== undefined) { updates.push('compliance = ?'); values.push(JSON.stringify(deployment.compliance)); } | |
| if (deployment.settingsVersion !== undefined) { updates.push('settings_version = ?'); values.push(deployment.settingsVersion); } | |
| if (deployment.lastPublishedVersion !== undefined) { updates.push('last_published_version = ?'); values.push(deployment.lastPublishedVersion); } | |
| if (deployment.previewImage !== undefined) { updates.push('preview_image = ?'); values.push(deployment.previewImage); } | |
| if (deployment.previewUpdatedAt !== undefined) { updates.push('preview_updated_at = ?'); values.push(toISOString(deployment.previewUpdatedAt)); } | |
| if (deployment.updatedAt !== undefined) { updates.push('updated_at = ?'); values.push(toISOStringRequired(deployment.updatedAt)); } | |
| if (deployment.publishedAt !== undefined) { updates.push('published_at = ?'); values.push(toISOString(deployment.publishedAt)); } | |
| if (updates.length === 0) return; | |
| if (!deployment.updatedAt) { | |
| updates.push('updated_at = ?'); | |
| values.push(new Date().toISOString()); | |
| } | |
| const sql = `UPDATE site_info SET ${updates.join(', ')} WHERE id = 'main'`; | |
| this.db.prepare(sql).run(...values); | |
| } | |
| // ============================================ | |
| // Files | |
| // ============================================ | |
| createFile(file: VirtualFile): void { | |
| const stmt = this.db.prepare(` | |
| INSERT INTO files ( | |
| id, path, name, type, content, mime_type, size, | |
| created_at, updated_at, metadata | |
| ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) | |
| `); | |
| 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 = ''; | |
| } | |
| stmt.run( | |
| file.id, | |
| file.path, | |
| file.name, | |
| file.type, | |
| content, | |
| file.mimeType ?? null, | |
| file.size ?? 0, | |
| toISOStringRequired(file.createdAt), | |
| toISOStringRequired(file.updatedAt), | |
| JSON.stringify(file.metadata ?? {}) | |
| ); | |
| } | |
| getFile(path: string): VirtualFile | null { | |
| const row = this.db.prepare('SELECT * FROM files WHERE path = ?').get(path) as Record<string, unknown> | undefined; | |
| if (!row) return null; | |
| return this.rowToFile(row); | |
| } | |
| updateFile(file: VirtualFile): void { | |
| const stmt = this.db.prepare(` | |
| UPDATE files SET | |
| name = ?, type = ?, content = ?, mime_type = ?, | |
| size = ?, updated_at = ?, metadata = ? | |
| WHERE path = ? | |
| `); | |
| 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 = ''; | |
| } | |
| stmt.run( | |
| file.name, | |
| file.type, | |
| content, | |
| file.mimeType ?? null, | |
| file.size ?? 0, | |
| toISOStringRequired(file.updatedAt), | |
| JSON.stringify(file.metadata ?? {}), | |
| file.path | |
| ); | |
| } | |
| deleteFile(path: string): void { | |
| this.db.prepare('DELETE FROM files WHERE path = ?').run(path); | |
| } | |
| listFiles(): VirtualFile[] { | |
| const rows = this.db.prepare('SELECT * FROM files ORDER BY path').all() as Record<string, unknown>[]; | |
| return rows.map(row => this.rowToFile(row)); | |
| } | |
| deleteAllFiles(): void { | |
| this.db.prepare('DELETE FROM files').run(); | |
| } | |
| private rowToFile(row: Record<string, unknown>): VirtualFile { | |
| const metadata = parseJSON(row.metadata as string, {}); | |
| const type = row.type as VirtualFile['type']; | |
| let content: string | ArrayBuffer = row.content as string; | |
| if (type === 'image' || type === 'video' || type === 'binary') { | |
| try { | |
| let base64Data = content as string; | |
| if (base64Data.startsWith('data:')) { | |
| const commaIndex = base64Data.indexOf(','); | |
| if (commaIndex !== -1) { | |
| base64Data = base64Data.slice(commaIndex + 1); | |
| } | |
| } | |
| content = Buffer.from(base64Data, 'base64').buffer; | |
| } catch { | |
| // Keep as string if conversion fails | |
| } | |
| } | |
| return { | |
| id: row.id as string, | |
| projectId: this.deploymentId, | |
| 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, | |
| }; | |
| } | |
| // ============================================ | |
| // File Tree Nodes | |
| // ============================================ | |
| createTreeNode(node: FileTreeNode): void { | |
| const stmt = this.db.prepare(` | |
| INSERT INTO file_tree_nodes (id, path, name, type, parent_path, is_expanded, children, metadata) | |
| VALUES (?, ?, ?, ?, ?, ?, ?, ?) | |
| `); | |
| stmt.run( | |
| node.id, | |
| node.path, | |
| node.name, | |
| node.type, | |
| node.parentPath, | |
| node.isExpanded ? 1 : 0, | |
| JSON.stringify(node.children ?? []), | |
| JSON.stringify(node.metadata ?? {}) | |
| ); | |
| } | |
| getTreeNode(path: string): FileTreeNode | null { | |
| const row = this.db.prepare('SELECT * FROM file_tree_nodes WHERE path = ?').get(path) as Record<string, unknown> | undefined; | |
| if (!row) return null; | |
| return this.rowToTreeNode(row); | |
| } | |
| updateTreeNode(node: FileTreeNode): void { | |
| const stmt = this.db.prepare(` | |
| UPDATE file_tree_nodes SET | |
| name = ?, type = ?, parent_path = ?, is_expanded = ?, children = ?, metadata = ? | |
| WHERE path = ? | |
| `); | |
| stmt.run( | |
| node.name, | |
| node.type, | |
| node.parentPath, | |
| node.isExpanded ? 1 : 0, | |
| JSON.stringify(node.children ?? []), | |
| JSON.stringify(node.metadata ?? {}), | |
| node.path | |
| ); | |
| } | |
| deleteTreeNode(path: string): void { | |
| this.db.prepare('DELETE FROM file_tree_nodes WHERE path = ?').run(path); | |
| } | |
| getChildNodes(parentPath: string | null): FileTreeNode[] { | |
| const rows = this.db.prepare( | |
| 'SELECT * FROM file_tree_nodes WHERE parent_path IS ? ORDER BY type DESC, path' | |
| ).all(parentPath) as Record<string, unknown>[]; | |
| return rows.map(row => this.rowToTreeNode(row)); | |
| } | |
| getAllTreeNodes(): FileTreeNode[] { | |
| const rows = this.db.prepare('SELECT * FROM file_tree_nodes ORDER BY path').all() as Record<string, unknown>[]; | |
| return rows.map(row => this.rowToTreeNode(row)); | |
| } | |
| private rowToTreeNode(row: Record<string, unknown>): FileTreeNode { | |
| return { | |
| id: row.id as string, | |
| projectId: this.deploymentId, | |
| 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), | |
| children: parseJSON(row.children as string, []), | |
| metadata: parseJSON(row.metadata as string, {}), | |
| }; | |
| } | |
| // ============================================ | |
| // Edge Functions: CRUD | |
| // ============================================ | |
| createFunction(fn: Omit<EdgeFunction, 'id' | 'projectId' | 'createdAt' | 'updatedAt'>): string { | |
| const id = uuidv4(); | |
| const now = new Date().toISOString(); | |
| const stmt = this.db.prepare(` | |
| INSERT INTO edge_functions ( | |
| id, name, description, code, method, enabled, timeout_ms, created_at, updated_at | |
| ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) | |
| `); | |
| stmt.run( | |
| id, | |
| fn.name, | |
| fn.description ?? null, | |
| fn.code, | |
| fn.method, | |
| fn.enabled ? 1 : 0, | |
| fn.timeoutMs, | |
| now, | |
| now | |
| ); | |
| return id; | |
| } | |
| getFunction(id: string): EdgeFunction | null { | |
| const row = this.db.prepare('SELECT * FROM edge_functions WHERE id = ?').get(id) as Record<string, unknown> | undefined; | |
| if (!row) return null; | |
| return this.rowToFunction(row); | |
| } | |
| getFunctionByName(name: string): EdgeFunction | null { | |
| const row = this.db.prepare('SELECT * FROM edge_functions WHERE name = ?').get(name) as Record<string, unknown> | undefined; | |
| if (!row) return null; | |
| return this.rowToFunction(row); | |
| } | |
| updateFunction(id: string, updates: Partial<Omit<EdgeFunction, 'id' | 'createdAt' | 'updatedAt'>>): void { | |
| const setClauses: string[] = []; | |
| const values: unknown[] = []; | |
| if (updates.name !== undefined) { setClauses.push('name = ?'); values.push(updates.name); } | |
| if (updates.description !== undefined) { setClauses.push('description = ?'); values.push(updates.description); } | |
| if (updates.code !== undefined) { setClauses.push('code = ?'); values.push(updates.code); } | |
| if (updates.method !== undefined) { setClauses.push('method = ?'); values.push(updates.method); } | |
| if (updates.enabled !== undefined) { setClauses.push('enabled = ?'); values.push(updates.enabled ? 1 : 0); } | |
| if (updates.timeoutMs !== undefined) { setClauses.push('timeout_ms = ?'); values.push(updates.timeoutMs); } | |
| if (setClauses.length === 0) return; | |
| setClauses.push('updated_at = ?'); | |
| values.push(new Date().toISOString()); | |
| values.push(id); | |
| const sql = `UPDATE edge_functions SET ${setClauses.join(', ')} WHERE id = ?`; | |
| this.db.prepare(sql).run(...values); | |
| } | |
| deleteFunction(id: string): void { | |
| this.db.prepare('DELETE FROM edge_functions WHERE id = ?').run(id); | |
| } | |
| listFunctions(): EdgeFunction[] { | |
| const rows = this.db.prepare('SELECT * FROM edge_functions ORDER BY name').all() as Record<string, unknown>[]; | |
| return rows.map(row => this.rowToFunction(row)); | |
| } | |
| private rowToFunction(row: Record<string, unknown>): EdgeFunction { | |
| return { | |
| id: row.id as string, | |
| projectId: '', | |
| name: row.name as string, | |
| description: row.description as string | undefined, | |
| code: row.code as string, | |
| method: row.method as EdgeFunction['method'], | |
| enabled: Boolean(row.enabled), | |
| timeoutMs: row.timeout_ms as number, | |
| createdAt: parseDate(row.created_at as string), | |
| updatedAt: parseDate(row.updated_at as string), | |
| }; | |
| } | |
| // ============================================ | |
| // Edge Functions: Logging | |
| // ============================================ | |
| logFunctionExecution(functionId: string, data: { | |
| method: string; | |
| path: string; | |
| statusCode: number; | |
| durationMs: number; | |
| error?: string; | |
| }): void { | |
| const stmt = this.db.prepare(` | |
| INSERT INTO function_logs (function_id, method, path, status_code, duration_ms, error) | |
| VALUES (?, ?, ?, ?, ?, ?) | |
| `); | |
| stmt.run( | |
| functionId, | |
| data.method, | |
| data.path, | |
| data.statusCode, | |
| data.durationMs, | |
| data.error ?? null | |
| ); | |
| } | |
| getFunctionLogs(functionId: string, limit: number = 100): FunctionLog[] { | |
| const rows = this.db.prepare(` | |
| SELECT * FROM function_logs WHERE function_id = ? | |
| ORDER BY timestamp DESC LIMIT ? | |
| `).all(functionId, limit) as Record<string, unknown>[]; | |
| return rows.map(row => this.rowToFunctionLog(row)); | |
| } | |
| getRecentLogs(limit: number = 100): FunctionLog[] { | |
| const rows = this.db.prepare(` | |
| SELECT * FROM function_logs ORDER BY timestamp DESC LIMIT ? | |
| `).all(limit) as Record<string, unknown>[]; | |
| return rows.map(row => this.rowToFunctionLog(row)); | |
| } | |
| clearFunctionLogs(functionId?: string, beforeDate?: Date): void { | |
| if (functionId && beforeDate) { | |
| this.db.prepare('DELETE FROM function_logs WHERE function_id = ? AND timestamp < ?') | |
| .run(functionId, beforeDate.toISOString()); | |
| } else if (functionId) { | |
| this.db.prepare('DELETE FROM function_logs WHERE function_id = ?').run(functionId); | |
| } else if (beforeDate) { | |
| this.db.prepare('DELETE FROM function_logs WHERE timestamp < ?').run(beforeDate.toISOString()); | |
| } else { | |
| this.db.prepare('DELETE FROM function_logs').run(); | |
| } | |
| } | |
| private rowToFunctionLog(row: Record<string, unknown>): FunctionLog { | |
| return { | |
| id: row.id as number, | |
| functionId: row.function_id as string, | |
| method: row.method as string, | |
| path: row.path as string, | |
| statusCode: row.status_code as number, | |
| durationMs: row.duration_ms as number, | |
| error: row.error as string | undefined, | |
| timestamp: parseDate(row.timestamp as string), | |
| }; | |
| } | |
| // ============================================ | |
| // Server Functions: CRUD | |
| // ============================================ | |
| createServerFunction(fn: Omit<ServerFunction, 'id' | 'projectId' | 'createdAt' | 'updatedAt'>): string { | |
| const id = uuidv4(); | |
| const now = new Date().toISOString(); | |
| const stmt = this.db.prepare(` | |
| INSERT INTO server_functions ( | |
| id, name, description, code, enabled, created_at, updated_at | |
| ) VALUES (?, ?, ?, ?, ?, ?, ?) | |
| `); | |
| stmt.run( | |
| id, | |
| fn.name, | |
| fn.description ?? null, | |
| fn.code, | |
| fn.enabled ? 1 : 0, | |
| now, | |
| now | |
| ); | |
| return id; | |
| } | |
| getServerFunction(id: string): ServerFunction | null { | |
| const row = this.db.prepare('SELECT * FROM server_functions WHERE id = ?').get(id) as Record<string, unknown> | undefined; | |
| if (!row) return null; | |
| return this.rowToServerFunction(row); | |
| } | |
| getServerFunctionByName(name: string): ServerFunction | null { | |
| const row = this.db.prepare('SELECT * FROM server_functions WHERE name = ?').get(name) as Record<string, unknown> | undefined; | |
| if (!row) return null; | |
| return this.rowToServerFunction(row); | |
| } | |
| updateServerFunction(id: string, updates: Partial<Omit<ServerFunction, 'id' | 'createdAt' | 'updatedAt'>>): void { | |
| const setClauses: string[] = []; | |
| const values: unknown[] = []; | |
| if (updates.name !== undefined) { setClauses.push('name = ?'); values.push(updates.name); } | |
| if (updates.description !== undefined) { setClauses.push('description = ?'); values.push(updates.description); } | |
| if (updates.code !== undefined) { setClauses.push('code = ?'); values.push(updates.code); } | |
| if (updates.enabled !== undefined) { setClauses.push('enabled = ?'); values.push(updates.enabled ? 1 : 0); } | |
| if (setClauses.length === 0) return; | |
| setClauses.push('updated_at = ?'); | |
| values.push(new Date().toISOString()); | |
| values.push(id); | |
| const sql = `UPDATE server_functions SET ${setClauses.join(', ')} WHERE id = ?`; | |
| this.db.prepare(sql).run(...values); | |
| } | |
| deleteServerFunction(id: string): void { | |
| this.db.prepare('DELETE FROM server_functions WHERE id = ?').run(id); | |
| } | |
| listServerFunctions(): ServerFunction[] { | |
| const rows = this.db.prepare('SELECT * FROM server_functions ORDER BY name').all() as Record<string, unknown>[]; | |
| return rows.map(row => this.rowToServerFunction(row)); | |
| } | |
| private rowToServerFunction(row: Record<string, unknown>): ServerFunction { | |
| return { | |
| id: row.id as string, | |
| projectId: '', | |
| name: row.name as string, | |
| description: row.description as string | undefined, | |
| code: row.code as string, | |
| enabled: Boolean(row.enabled), | |
| createdAt: parseDate(row.created_at as string), | |
| updatedAt: parseDate(row.updated_at as string), | |
| }; | |
| } | |
| // ============================================ | |
| // Secrets Management | |
| // ============================================ | |
| createSecret(name: string, value: string, description?: string): string { | |
| if (!isEncryptionConfigured()) { | |
| throw new Error('Secrets encryption not configured. Set SECRETS_ENCRYPTION_KEY environment variable.'); | |
| } | |
| const id = uuidv4(); | |
| const now = new Date().toISOString(); | |
| const encrypted = encryptSecret(value); | |
| this.db.prepare(` | |
| INSERT INTO secrets (id, name, encrypted_value, iv, auth_tag, description, created_at, updated_at) | |
| VALUES (?, ?, ?, ?, ?, ?, ?, ?) | |
| `).run(id, name, encrypted.encryptedValue, encrypted.iv, encrypted.authTag, description || null, now, now); | |
| return id; | |
| } | |
| getSecret(id: string): Secret | null { | |
| const row = this.db.prepare('SELECT * FROM secrets WHERE id = ?').get(id) as Record<string, unknown> | undefined; | |
| if (!row) return null; | |
| return this.rowToSecret(row); | |
| } | |
| getSecretByName(name: string): Secret | null { | |
| const row = this.db.prepare('SELECT * FROM secrets WHERE name = ?').get(name) as Record<string, unknown> | undefined; | |
| if (!row) return null; | |
| return this.rowToSecret(row); | |
| } | |
| updateSecretValue(id: string, value: string): void { | |
| if (!isEncryptionConfigured()) { | |
| throw new Error('Secrets encryption not configured. Set SECRETS_ENCRYPTION_KEY environment variable.'); | |
| } | |
| const now = new Date().toISOString(); | |
| const encrypted = encryptSecret(value); | |
| this.db.prepare(` | |
| UPDATE secrets | |
| SET encrypted_value = ?, iv = ?, auth_tag = ?, updated_at = ? | |
| WHERE id = ? | |
| `).run(encrypted.encryptedValue, encrypted.iv, encrypted.authTag, now, id); | |
| } | |
| updateSecretMetadata(id: string, updates: { name?: string; description?: string }): void { | |
| const now = new Date().toISOString(); | |
| if (updates.name !== undefined) { | |
| this.db.prepare('UPDATE secrets SET name = ?, updated_at = ? WHERE id = ?') | |
| .run(updates.name, now, id); | |
| } | |
| if (updates.description !== undefined) { | |
| this.db.prepare('UPDATE secrets SET description = ?, updated_at = ? WHERE id = ?') | |
| .run(updates.description, now, id); | |
| } | |
| } | |
| deleteSecret(id: string): void { | |
| this.db.prepare('DELETE FROM secrets WHERE id = ?').run(id); | |
| } | |
| listSecrets(): Secret[] { | |
| const rows = this.db.prepare('SELECT * FROM secrets ORDER BY name').all() as Record<string, unknown>[]; | |
| return rows.map(row => this.rowToSecret(row)); | |
| } | |
| listSecretsWithValues(): Array<{ | |
| name: string; | |
| encryptedValue: string; | |
| iv: string; | |
| authTag: string; | |
| }> { | |
| const rows = this.db.prepare('SELECT name, encrypted_value, iv, auth_tag FROM secrets').all() as Record<string, unknown>[]; | |
| return rows.map(row => ({ | |
| name: row.name as string, | |
| encryptedValue: row.encrypted_value as string, | |
| iv: row.iv as string, | |
| authTag: row.auth_tag as string, | |
| })); | |
| } | |
| private rowToSecret(row: Record<string, unknown>): Secret { | |
| return { | |
| id: row.id as string, | |
| projectId: '', | |
| name: row.name as string, | |
| description: row.description as string | undefined, | |
| hasValue: row.encrypted_value !== null && row.encrypted_value !== '', | |
| createdAt: parseDate(row.created_at as string), | |
| updatedAt: parseDate(row.updated_at as string), | |
| }; | |
| } | |
| createSecretPlaceholder(name: string, description?: string): string { | |
| const id = uuidv4(); | |
| const now = new Date().toISOString(); | |
| this.db.prepare(` | |
| INSERT INTO secrets (id, name, encrypted_value, iv, auth_tag, description, created_at, updated_at) | |
| VALUES (?, ?, '', '', '', ?, ?, ?) | |
| `).run(id, name, description || null, now, now); | |
| return id; | |
| } | |
| // ============================================ | |
| // Scheduled Functions: CRUD | |
| // ============================================ | |
| createScheduledFunction(fn: Omit<ScheduledFunction, 'id' | 'projectId' | 'createdAt' | 'updatedAt'>): string { | |
| const id = uuidv4(); | |
| const now = new Date().toISOString(); | |
| const stmt = this.db.prepare(` | |
| INSERT INTO scheduled_functions ( | |
| 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) | |
| `); | |
| stmt.run( | |
| id, | |
| fn.name, | |
| fn.description ?? null, | |
| fn.functionId, | |
| fn.cronExpression, | |
| fn.timezone || 'UTC', | |
| JSON.stringify(fn.config || {}), | |
| fn.enabled ? 1 : 0, | |
| toISOString(fn.lastRunAt), | |
| toISOString(fn.nextRunAt), | |
| fn.lastStatus ?? null, | |
| fn.lastError ?? null, | |
| fn.lastDurationMs ?? null, | |
| now, | |
| now | |
| ); | |
| return id; | |
| } | |
| getScheduledFunction(id: string): ScheduledFunction | null { | |
| const row = this.db.prepare('SELECT * FROM scheduled_functions WHERE id = ?').get(id) as Record<string, unknown> | undefined; | |
| if (!row) return null; | |
| return this.rowToScheduledFunction(row); | |
| } | |
| getScheduledFunctionByName(name: string): ScheduledFunction | null { | |
| const row = this.db.prepare('SELECT * FROM scheduled_functions WHERE name = ?').get(name) as Record<string, unknown> | undefined; | |
| if (!row) return null; | |
| return this.rowToScheduledFunction(row); | |
| } | |
| updateScheduledFunction(id: string, updates: Partial<Omit<ScheduledFunction, 'id' | 'createdAt' | 'updatedAt'>>): void { | |
| const setClauses: string[] = []; | |
| const values: unknown[] = []; | |
| if (updates.name !== undefined) { setClauses.push('name = ?'); values.push(updates.name); } | |
| if (updates.description !== undefined) { setClauses.push('description = ?'); values.push(updates.description); } | |
| if (updates.functionId !== undefined) { setClauses.push('function_id = ?'); values.push(updates.functionId); } | |
| if (updates.cronExpression !== undefined) { setClauses.push('cron_expression = ?'); values.push(updates.cronExpression); } | |
| if (updates.timezone !== undefined) { setClauses.push('timezone = ?'); values.push(updates.timezone); } | |
| if (updates.config !== undefined) { setClauses.push('config = ?'); values.push(JSON.stringify(updates.config)); } | |
| if (updates.enabled !== undefined) { setClauses.push('enabled = ?'); values.push(updates.enabled ? 1 : 0); } | |
| if (updates.lastRunAt !== undefined) { setClauses.push('last_run_at = ?'); values.push(toISOString(updates.lastRunAt)); } | |
| if (updates.nextRunAt !== undefined) { setClauses.push('next_run_at = ?'); values.push(toISOString(updates.nextRunAt)); } | |
| if (updates.lastStatus !== undefined) { setClauses.push('last_status = ?'); values.push(updates.lastStatus); } | |
| if (updates.lastError !== undefined) { setClauses.push('last_error = ?'); values.push(updates.lastError); } | |
| if (updates.lastDurationMs !== undefined) { setClauses.push('last_duration_ms = ?'); values.push(updates.lastDurationMs); } | |
| if (setClauses.length === 0) return; | |
| setClauses.push('updated_at = ?'); | |
| values.push(new Date().toISOString()); | |
| values.push(id); | |
| const sql = `UPDATE scheduled_functions SET ${setClauses.join(', ')} WHERE id = ?`; | |
| this.db.prepare(sql).run(...values); | |
| } | |
| deleteScheduledFunction(id: string): void { | |
| this.db.prepare('DELETE FROM scheduled_functions WHERE id = ?').run(id); | |
| } | |
| listScheduledFunctions(): ScheduledFunction[] { | |
| const rows = this.db.prepare('SELECT * FROM scheduled_functions ORDER BY name').all() as Record<string, unknown>[]; | |
| return rows.map(row => this.rowToScheduledFunction(row)); | |
| } | |
| listDueScheduledFunctions(): ScheduledFunction[] { | |
| const rows = this.db.prepare( | |
| `SELECT * FROM scheduled_functions WHERE enabled = 1 AND next_run_at IS NOT NULL AND next_run_at <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now')` | |
| ).all() as Record<string, unknown>[]; | |
| return rows.map(row => this.rowToScheduledFunction(row)); | |
| } | |
| private rowToScheduledFunction(row: Record<string, unknown>): ScheduledFunction { | |
| return { | |
| id: row.id as string, | |
| projectId: '', | |
| name: row.name as string, | |
| description: row.description as string | undefined, | |
| functionId: row.function_id as string, | |
| cronExpression: row.cron_expression as string, | |
| timezone: row.timezone as string, | |
| config: parseJSON(row.config as string, {}), | |
| enabled: Boolean(row.enabled), | |
| lastRunAt: row.last_run_at ? parseDate(row.last_run_at as string) : undefined, | |
| nextRunAt: row.next_run_at ? parseDate(row.next_run_at as string) : undefined, | |
| lastStatus: row.last_status as ScheduledFunction['lastStatus'], | |
| lastError: row.last_error as string | undefined, | |
| lastDurationMs: row.last_duration_ms as number | undefined, | |
| createdAt: parseDate(row.created_at as string), | |
| updatedAt: parseDate(row.updated_at as string), | |
| }; | |
| } | |
| // ============================================ | |
| // DDL Execution | |
| // ============================================ | |
| executeDDL(sql: string): void { | |
| this.db.exec(sql); | |
| } | |
| // ============================================ | |
| // Schema Management & Raw SQL | |
| // ============================================ | |
| private static readonly SYSTEM_TABLES = [ | |
| 'site_info', | |
| 'files', | |
| 'file_tree_nodes', | |
| 'edge_functions', | |
| 'function_logs', | |
| 'server_functions', | |
| 'secrets', | |
| 'scheduled_functions', | |
| ]; | |
| private static readonly BLOCKED_PATTERNS = /^\s*(ATTACH|DETACH|PRAGMA|VACUUM)\b/i; | |
| executeRawSQL(sql: string, params?: unknown[]): { | |
| columns: string[]; | |
| rows: unknown[][]; | |
| rowsAffected: number; | |
| } { | |
| if (RuntimeDatabase.BLOCKED_PATTERNS.test(sql)) { | |
| throw new Error('Statement type not allowed'); | |
| } | |
| const trimmedSql = sql.trim().toLowerCase(); | |
| const isSelect = trimmedSql.startsWith('select'); | |
| if (isSelect) { | |
| const stmt = this.db.prepare(sql); | |
| const rows = params ? stmt.all(...params) : stmt.all(); | |
| if (rows.length === 0) { | |
| return { columns: [], rows: [], rowsAffected: 0 }; | |
| } | |
| const columns = Object.keys(rows[0] as Record<string, unknown>); | |
| const rowsArray = rows.map(row => columns.map(col => (row as Record<string, unknown>)[col])); | |
| return { columns, rows: rowsArray, rowsAffected: 0 }; | |
| } else { | |
| const stmt = this.db.prepare(sql); | |
| const result = params ? stmt.run(...params) : stmt.run(); | |
| return { | |
| columns: [], | |
| rows: [], | |
| rowsAffected: result.changes, | |
| }; | |
| } | |
| } | |
| getTableSchema(): TableInfo[] { | |
| const tables = this.db.prepare(` | |
| SELECT name FROM sqlite_master | |
| WHERE type = 'table' AND name NOT LIKE 'sqlite_%' | |
| ORDER BY name | |
| `).all() as Array<{ name: string }>; | |
| return tables.flatMap(table => { | |
| if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(table.name)) return []; | |
| const isSystemTable = RuntimeDatabase.SYSTEM_TABLES.includes(table.name); | |
| const columns = this.db.prepare(`PRAGMA table_info('${table.name}')`).all() as Array<{ | |
| cid: number; | |
| name: string; | |
| type: string; | |
| notnull: number; | |
| dflt_value: string | null; | |
| pk: number; | |
| }>; | |
| const countResult = this.db.prepare(`SELECT COUNT(*) as count FROM "${table.name}"`).get() as { count: number }; | |
| return { | |
| name: table.name, | |
| columns: columns.map(col => ({ | |
| name: col.name, | |
| type: col.type, | |
| nullable: !col.notnull, | |
| primaryKey: col.pk > 0, | |
| defaultValue: col.dflt_value ?? undefined, | |
| })), | |
| rowCount: countResult.count, | |
| isSystemTable, | |
| }; | |
| }); | |
| } | |
| getTableData(tableName: string, limit: number = 100, offset: number = 0): { | |
| columns: string[]; | |
| rows: unknown[][]; | |
| total: number; | |
| } { | |
| const validTables = this.db.prepare(` | |
| SELECT name FROM sqlite_master WHERE type = 'table' AND name = ? | |
| `).get(tableName); | |
| if (!validTables) { | |
| throw new Error(`Table "${tableName}" does not exist`); | |
| } | |
| const countResult = this.db.prepare(`SELECT COUNT(*) as count FROM "${tableName}"`).get() as { count: number }; | |
| const rows = this.db.prepare(`SELECT * FROM "${tableName}" LIMIT ? OFFSET ?`).all(limit, offset) as Record<string, unknown>[]; | |
| if (rows.length === 0) { | |
| return { columns: [], rows: [], total: countResult.count }; | |
| } | |
| const columns = Object.keys(rows[0]); | |
| const rowsArray = rows.map(row => columns.map(col => row[col])); | |
| return { | |
| columns, | |
| rows: rowsArray, | |
| total: countResult.count, | |
| }; | |
| } | |
| isSystemTable(tableName: string): boolean { | |
| return RuntimeDatabase.SYSTEM_TABLES.includes(tableName); | |
| } | |
| executeUserQuery(sql: string, params?: unknown[]): { | |
| columns: string[]; | |
| rows: unknown[][]; | |
| rowsAffected: number; | |
| error?: string; | |
| } { | |
| const trimmedSql = sql.trim(); | |
| const upperSql = trimmedSql.toUpperCase(); | |
| const systemTableError = this.validateNotSystemTable(upperSql); | |
| if (systemTableError) { | |
| return { | |
| columns: [], | |
| rows: [], | |
| rowsAffected: 0, | |
| error: systemTableError, | |
| }; | |
| } | |
| try { | |
| return this.executeRawSQL(sql, params); | |
| } catch (e: unknown) { | |
| const message = e instanceof Error ? e.message : String(e); | |
| return { | |
| columns: [], | |
| rows: [], | |
| rowsAffected: 0, | |
| error: message, | |
| }; | |
| } | |
| } | |
| private validateNotSystemTable(upperSql: string): string | null { | |
| const ddlMatch = upperSql.match(/^(DROP|ALTER|TRUNCATE)\s+TABLE\s+(?:IF\s+EXISTS\s+)?["'`]?(\w+)["'`]?/i); | |
| if (ddlMatch) { | |
| const tableName = ddlMatch[2].toLowerCase(); | |
| if (RuntimeDatabase.SYSTEM_TABLES.includes(tableName)) { | |
| return `Cannot modify system table: ${tableName}`; | |
| } | |
| } | |
| const insertMatch = upperSql.match(/^INSERT\s+INTO\s+["'`]?(\w+)["'`]?/i); | |
| if (insertMatch) { | |
| const tableName = insertMatch[1].toLowerCase(); | |
| if (RuntimeDatabase.SYSTEM_TABLES.includes(tableName)) { | |
| return `Cannot insert into system table: ${tableName}`; | |
| } | |
| } | |
| const updateMatch = upperSql.match(/^UPDATE\s+["'`]?(\w+)["'`]?/i); | |
| if (updateMatch) { | |
| const tableName = updateMatch[1].toLowerCase(); | |
| if (RuntimeDatabase.SYSTEM_TABLES.includes(tableName)) { | |
| return `Cannot update system table: ${tableName}`; | |
| } | |
| } | |
| const deleteMatch = upperSql.match(/^DELETE\s+FROM\s+["'`]?(\w+)["'`]?/i); | |
| if (deleteMatch) { | |
| const tableName = deleteMatch[1].toLowerCase(); | |
| if (RuntimeDatabase.SYSTEM_TABLES.includes(tableName)) { | |
| return `Cannot delete from system table: ${tableName}`; | |
| } | |
| } | |
| return null; | |
| } | |
| getSchemaForExport(): string { | |
| const tables = this.getTableSchema(); | |
| const userTables = tables.filter(t => !t.isSystemTable); | |
| if (userTables.length === 0) { | |
| return '-- No user tables defined\n-- Create tables using the SQL Editor or edge functions\n'; | |
| } | |
| let sql = '-- Database Schema\n'; | |
| sql += `-- ${userTables.length} user table(s)\n\n`; | |
| for (const table of userTables) { | |
| sql += `-- Table: ${table.name} (${table.rowCount} rows)\n`; | |
| sql += `CREATE TABLE ${table.name} (\n`; | |
| sql += table.columns.map(col => { | |
| let def = ` ${col.name} ${col.type}`; | |
| if (col.primaryKey) def += ' PRIMARY KEY'; | |
| if (!col.nullable) def += ' NOT NULL'; | |
| if (col.defaultValue !== undefined) def += ` DEFAULT ${col.defaultValue}`; | |
| return def; | |
| }).join(',\n'); | |
| sql += '\n);\n\n'; | |
| } | |
| return sql; | |
| } | |
| } | |