mirror of
https://github.com/sune-org/store.git
synced 2026-01-13 16:17:58 +00:00
1 line
30 KiB
JSON
1 line
30 KiB
JSON
[{"id":"xo6k2dz","name":"D1","pinned":false,"avatar":"data:image/webp;base64,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","url":"gh://sune-org/store/cloudflare-utilities/d1.sune","updatedAt":1759077903300,"settings":{"model":"g:gemini-2.5-flash","temperature":"","top_p":"","top_k":"","frequency_penalty":"","repetition_penalty":"","min_p":"","top_a":"","verbosity":"","reasoning_effort":"default","system_prompt":"You are Cloudflare D1 GPT. Create queries for the user to quickly operate on their db.","html":"<div x-data=\"d1Manager()\" x-init=\"init()\" class=\"bg-gray-50 text-gray-800 rounded-lg border border-gray-200 m-2 sm:m-4 p-4 max-w-2xl mx-auto text-sm font-sans shadow-md not-prose\">\n <!-- Header -->\n <div class=\"flex justify-between items-center pb-3 border-b border-gray-200 mb-4\">\n <div class=\"flex items-center gap-3\">\n <i data-lucide=\"database\" class=\"text-gray-600\"></i>\n <h2 class=\"font-semibold text-lg text-gray-900\">D1 Database Master</h2>\n </div>\n <span class=\"text-xs text-gray-400 font-mono\">v1.5.0</span>\n </div>\n\n <!-- Loading State -->\n <template x-if=\"isLoading.initial\">\n <div class=\"flex flex-col items-center justify-center p-8 text-gray-500\">\n <i data-lucide=\"loader-circle\" class=\"h-8 w-8 animate-spin mb-2\"></i>\n <span>Connecting to Cloudflare...</span>\n </div>\n </template>\n\n <!-- Error State -->\n <template x-if=\"error\">\n <div @click=\"error=''\" class=\"bg-red-100 border border-red-300 text-red-800 rounded-lg p-3 text-center cursor-pointer\">\n <p class=\"font-semibold\">An Error Occurred</p>\n <p x-text=\"error\" class=\"text-xs mt-1\"></p>\n </div>\n </template>\n\n <!-- Main Content -->\n <div x-show=\"!isLoading.initial && !error\" x-cloak>\n <!-- Database List View -->\n <div x-show=\"view === 'list'\">\n <div class=\"flex justify-between items-center mb-3\">\n <h3 class=\"font-medium text-gray-700\" x-text=\"accountId ? `Account: ${accountId.substring(0, 8)}...` : 'Databases'\"></h3>\n <div>\n <button @click=\"createDb()\" class=\"p-2 rounded-md hover:bg-gray-100 active:scale-95 transition\" title=\"New Database\"><i data-lucide=\"plus\" class=\"h-4 w-4\"></i></button>\n <button @click=\"fetchDatabases(true)\" class=\"p-2 rounded-md hover:bg-gray-100 active:scale-95 transition\" title=\"Refresh List\"><i data-lucide=\"refresh-cw\" class=\"h-4 w-4\" :class=\"{'animate-spin': isLoading.dbs}\"></i></button>\n </div>\n </div>\n <div class=\"space-y-2\">\n <template x-for=\"db in databases\" :key=\"db.uuid\">\n <button @click=\"selectDb(db)\" class=\"w-full text-left p-3 bg-white border border-gray-200 rounded-lg hover:bg-blue-50 hover:border-blue-300 transition flex items-center justify-between\">\n <div>\n <p class=\"font-semibold\" x-text=\"db.name\"></p>\n <p class=\"text-xs text-gray-500 font-mono\" x-text=\"db.uuid\"></p>\n </div>\n <i data-lucide=\"chevron-right\" class=\"h-5 w-5 text-gray-400\"></i>\n </button>\n </template>\n <template x-if=\"!databases.length && !isLoading.dbs\">\n <p class=\"text-center text-gray-500 py-6\">No D1 databases found.</p>\n </template>\n </div>\n </div>\n\n <!-- Single Database View -->\n <div x-show=\"view === 'db'\" x-cloak>\n <div class=\"flex items-center gap-2 mb-4\">\n <button @click=\"goBack()\" class=\"p-2 rounded-md hover:bg-gray-100 active:scale-95 transition\" title=\"Back to list\"><i data-lucide=\"arrow-left\" class=\"h-5 w-5\"></i></button>\n <div>\n <h3 class=\"font-semibold text-base leading-tight\" x-text=\"selectedDb.name\"></h3>\n <p class=\"text-xs text-gray-500 font-mono\" x-text=\"selectedDb.uuid\"></p>\n </div>\n </div>\n \n <!-- Tabs -->\n <div class=\"border-b border-gray-200 mb-4\">\n <nav class=\"-mb-px flex gap-4 text-xs font-medium\">\n <button @click=\"activeTab='query'\" :class=\"{'border-blue-500 text-blue-600': activeTab==='query', 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300': activeTab!=='query'}\" class=\"whitespace-nowrap py-2 px-1 border-b-2\">Query</button>\n <button @click=\"activeTab='manage'\" :class=\"{'border-blue-500 text-blue-600': activeTab==='manage', 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300': activeTab!=='manage'}\" class=\"whitespace-nowrap py-2 px-1 border-b-2\">Manage</button>\n <button @click=\"activeTab='schema'\" :class=\"{'border-blue-500 text-blue-600': activeTab==='schema', 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300': activeTab!=='schema'}\" class=\"whitespace-nowrap py-2 px-1 border-b-2\">Schema</button>\n <button @click=\"activeTab='actions'\" :class=\"{'border-blue-500 text-blue-600': activeTab==='actions', 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300': activeTab!=='actions'}\" class=\"whitespace-nowrap py-2 px-1 border-b-2\">Actions</button>\n </nav>\n </div>\n\n <!-- Query Tab Content -->\n <div x-show=\"activeTab === 'query'\" class=\"space-y-3\">\n <div>\n <div class=\"flex justify-between items-center mb-1\">\n <label for=\"d1-query\" class=\"font-medium text-gray-800\">SQL Query <span class=\"text-gray-400 font-normal text-xs\">(Ctrl+Enter)</span></label>\n <div class=\"flex items-center gap-1\">\n <button @click=\"pasteQuery()\" class=\"p-1.5 rounded hover:bg-gray-200 transition\" title=\"Paste\"><i data-lucide=\"clipboard-paste\" class=\"h-3.5 w-3.5\"></i></button>\n <button @click=\"clearQuery()\" class=\"p-1.5 rounded hover:bg-gray-200 transition\" title=\"Clear\"><i data-lucide=\"x\" class=\"h-3.5 w-3.5\"></i></button>\n </div>\n </div>\n <textarea id=\"d1-query\" x-ref=\"queryTextarea\" x-model=\"query\" rows=\"5\" class=\"w-full p-2 border border-gray-300 rounded-md bg-white font-mono text-[13px] focus:ring-2 focus:ring-blue-400 focus:border-blue-400\" placeholder=\"SELECT * FROM users LIMIT 5;\"></textarea>\n </div>\n <div class=\"grid grid-cols-2 sm:grid-cols-4 gap-2 text-xs\">\n <button @click=\"useSnippet('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT);')\" class=\"p-2 bg-white border rounded-md hover:bg-gray-100 transition truncate\">CREATE TABLE</button>\n <button @click=\"useSnippet('INSERT INTO users (name, email) VALUES (\\'Master\\', \\'master@sune.dev\\');')\" class=\"p-2 bg-white border rounded-md hover:bg-gray-100 transition truncate\">INSERT INTO</button>\n <button @click=\"useSnippet('SELECT * FROM users;')\" class=\"p-2 bg-white border rounded-md hover:bg-gray-100 transition truncate\">SELECT *</button>\n <button @click=\"useSnippet('DROP TABLE users;')\" class=\"p-2 bg-white border rounded-md hover:bg-gray-100 transition truncate\">DROP TABLE</button>\n </div>\n <div class=\"flex gap-2\">\n <button @click=\"runQuery()\" :disabled=\"isLoading.query\" class=\"flex-1 flex items-center justify-center gap-2 bg-black text-white font-semibold px-4 py-2.5 rounded-lg hover:bg-gray-800 active:scale-[.98] transition disabled:opacity-60\">\n <i data-lucide=\"play\" class=\"h-4 w-4\"></i>\n <span x-text=\"isLoading.query ? 'Executing...' : 'Run Query'\"></span>\n </button>\n <div class=\"relative\" x-data=\"{ open: false }\" @click.outside=\"open = false\">\n <button @click=\"open = !open\" :disabled=\"!queryHistory.length\" class=\"h-full px-3 bg-gray-200 text-gray-800 rounded-lg hover:bg-gray-300 disabled:opacity-50\" title=\"Query History\"><i data-lucide=\"history\" class=\"h-4 w-4\"></i></button>\n <div x-show=\"open\" x-transition class=\"absolute bottom-full right-0 mb-2 w-72 bg-white border rounded-lg shadow-lg max-h-60 overflow-y-auto z-10\">\n <template x-for=\"(h, i) in queryHistory\" :key=\"i\"><button @click=\"useSnippet(h); open=false\" class=\"w-full text-left text-xs p-2 hover:bg-gray-100 font-mono truncate\" x-text=\"h\"></button></template>\n </div>\n </div>\n </div>\n <div x-show=\"results\" class=\"pt-2\" x-cloak x-html=\"renderResults()\"></div>\n </div>\n\n <!-- Manage Tab -->\n <div x-show=\"activeTab === 'manage'\" class=\"space-y-3\">\n <div class=\"flex justify-between items-center\">\n <h4 class=\"font-medium\">Table Management</h4>\n <button @click=\"fetchTables(true)\" class=\"p-2 rounded-md hover:bg-gray-100 active:scale-95 transition\" title=\"Refresh Tables\"><i data-lucide=\"refresh-cw\" class=\"h-4 w-4\" :class=\"{'animate-spin':isLoading.tables}\"></i></button>\n </div>\n <div x-show=\"!tables.length && !isLoading.tables\" class=\"text-center text-gray-500 py-6\">No tables found. Create one using the Query tab.</div>\n <div class=\"space-y-2\">\n <template x-for=\"table in tables\" :key=\"table.name\">\n <div x-data=\"{ open: false }\" class=\"bg-white border border-gray-200 rounded-lg\">\n <button @click=\"open = !open; if(open) fetchTableInfo(table.name)\" class=\"w-full flex justify-between items-center p-3 text-left\">\n <span class=\"font-semibold text-gray-800\" x-text=\"table.name\"></span>\n <i data-lucide=\"chevron-down\" class=\"h-5 w-5 text-gray-400 transition-transform\" :class=\"{'rotate-180': open}\"></i>\n </button>\n <div x-show=\"open\" x-collapse.duration.300ms class=\"border-t border-gray-200\">\n <div x-show=\"isLoading.columns === table.name\" class=\"flex items-center justify-center p-4 text-gray-500\"><i data-lucide=\"loader-circle\" class=\"h-5 w-5 animate-spin\"></i></div>\n <div x-show=\"tableDetails[table.name]\">\n <div class=\"px-3 pt-3\">\n <h5 class=\"text-xs font-semibold text-gray-500 mb-2\">COLUMNS</h5>\n <div class=\"divide-y divide-gray-100 border rounded-md\">\n <template x-for=\"col in tableDetails[table.name]?.columns\" :key=\"col.cid\">\n <div class=\"p-2 flex justify-between items-center gap-2\">\n <div class=\"flex items-center gap-2 overflow-hidden text-xs\"><span class=\"font-mono font-medium text-gray-900 truncate\" x-text=\"col.name\"></span><span class=\"text-gray-500 uppercase\" x-text=\"col.type\"></span><span x-show=\"col.pk\" class=\"px-1.5 py-0.5 bg-yellow-100 text-yellow-800 rounded text-[10px] font-semibold\">PK</span></div>\n </div>\n </template>\n </div>\n </div>\n <div class=\"p-3\">\n <h5 class=\"text-xs font-semibold text-gray-500 mb-2\" x-text=\"`ROWS (showing first ${tableData[table.name]?.length || 0})`\"></h5>\n <div x-show=\"!tableData[table.name]?.length\" class=\"text-center text-gray-400 text-xs py-4 border rounded-md\">Table is empty.</div>\n <div x-show=\"tableDetails[table.name] && !tableDetails[table.name].pkColumn\" class=\"mb-2 p-2 bg-amber-50 text-amber-800 text-xs border border-amber-200 rounded-md\">Row deletion is disabled because a single primary key was not found.</div>\n <div x-show=\"tableData[table.name]?.length\" class=\"overflow-x-auto border rounded-md\">\n <table class=\"w-full text-left text-xs\">\n <thead>\n <tr class=\"bg-gray-50\">\n <template x-for=\"key in Object.keys(tableData[table.name]?.[0] || {})\"><th class=\"p-2 font-medium border-b\" x-text=\"key\"></th></template>\n <th class=\"p-2 font-medium border-b text-right\">Actions</th>\n </tr>\n </thead>\n <tbody>\n <template x-for=\"(row, i) in tableData[table.name]\" :key=\"i\">\n <tr class=\"border-b border-gray-100 last:border-b-0\">\n <template x-for=\"value in Object.values(row)\"><td class=\"p-2 align-top font-mono max-w-[200px] truncate\" :title=\"value\" x-text=\"value\"></td></template>\n <td class=\"p-1.5 align-top text-right\">\n <button @click=\"deleteRow(table.name, row[tableDetails[table.name].pkColumn])\" :disabled=\"!tableDetails[table.name].pkColumn || isLoading.rowDelete\" class=\"p-1 rounded text-gray-400 hover:bg-red-100 hover:text-red-600 disabled:opacity-30 disabled:hover:bg-transparent\" title=\"Delete Row\">\n <i data-lucide=\"trash-2\" class=\"h-3.5 w-3.5\"></i>\n </button>\n </td>\n </tr>\n </template>\n </tbody>\n </table>\n </div>\n </div>\n </div>\n </div>\n </div>\n </template>\n </div>\n </div>\n \n <!-- Schema Tab -->\n <div x-show=\"activeTab === 'schema'\" class=\"space-y-3\">\n <div class=\"flex justify-between items-center\">\n <h4 class=\"font-medium\">Database Schema</h4>\n <div class=\"flex items-center\">\n <button @click=\"cloneDb()\" :disabled=\"!tables.length || isLoading.cloning\" class=\"p-2 rounded-md hover:bg-gray-100 active:scale-95 transition disabled:opacity-50\" title=\"Clone Database\">\n <i data-lucide=\"copy-plus\" class=\"h-4 w-4\" :class=\"{'animate-spin': isLoading.cloning}\"></i>\n </button>\n <button @click=\"copySchema()\" :disabled=\"!tables.length\" class=\"p-2 rounded-md hover:bg-gray-100 active:scale-95 transition disabled:opacity-50\" :title=\"copiedSchema ? 'Copied!' : 'Copy Schema'\">\n <i data-lucide=\"check\" class=\"h-4 w-4 text-green-600\" x-show=\"copiedSchema\" x-transition></i>\n <i data-lucide=\"clipboard-copy\" class=\"h-4 w-4\" x-show=\"!copiedSchema\"></i>\n </button>\n <button @click=\"fetchTables(true)\" class=\"p-2 rounded-md hover:bg-gray-100 active:scale-95 transition\" title=\"Refresh Schema\"><i data-lucide=\"refresh-cw\" class=\"h-4 w-4\" :class=\"{'animate-spin':isLoading.tables}\"></i></button>\n </div>\n </div>\n <div class=\"bg-white p-3 border rounded-md text-gray-600\">\n The schema is the blueprint of your database, defining its tables and columns from the `CREATE TABLE` statements. This view is empty if no tables exist.\n </div>\n <pre class=\"text-xs bg-gray-900 text-white p-3 rounded-md overflow-auto max-h-80\" x-text=\"tables.length ? tables.map(t => t.sql).join(';\\n\\n') + ';' : 'No tables found.'\"></pre>\n </div>\n\n <!-- Actions Tab -->\n <div x-show=\"activeTab === 'actions'\" class=\"space-y-4\">\n <div>\n <h4 class=\"font-medium mb-2\">Backup</h4>\n <p class=\"mb-2 text-gray-600\">Create and download a `.sql` file containing the commands to recreate your database.</p>\n <button @click=\"createBackup(selectedDb)\" :disabled=\"isLoading.backup\" class=\"inline-flex items-center justify-center gap-1.5 p-2 bg-white border rounded-md hover:bg-gray-100 transition disabled:opacity-50\">\n <i data-lucide=\"download\" class=\"h-3.5 w-3.5\"></i>\n <span x-text=\"isLoading.backup?'Backing up...':'Download Backup'\"></span>\n </button>\n </div>\n <div>\n <h4 class=\"font-medium mb-2 text-red-700\">Danger Zone</h4>\n <div class=\"bg-white border border-red-200 p-3 rounded-lg\">\n <p class=\"mb-2 text-gray-600\">Permanently delete this database. This action is irreversible and will delete all data within it.</p>\n <button @click=\"deleteDb(selectedDb)\" class=\"inline-flex items-center justify-center gap-1.5 p-2 bg-white border border-red-300 text-red-600 rounded-md hover:bg-red-50 transition\">\n <i data-lucide=\"trash-2\" class=\"h-3.5 w-3.5\"></i>\n <span>Delete this Database</span>\n </button>\n </div>\n </div>\n </div>\n </div>\n </div>\n</div>\n\n<script>\nfunction d1Manager() {\n const esc=s=>String(s).replace(/[&<>'\"`]/g,c=>({\"&\":\"&\",\"<\":\"<\",\">\":\">\",\"\\\"\":\""\",\"'\":\"'\",\"`\":\"`\"}[c]));\n return {\n isLoading: { initial: true, dbs: false, query: false, backup: false, tables: false, columns: false, rowDelete: false, cloning: false },\n error: '', accountId: '', databases: [], selectedDb: null, tables: [], tableDetails: {}, tableData: {},\n query: 'SELECT name, sql FROM sqlite_master WHERE type=\\'table\\';',\n queryHistory: [], results: null, view: 'list', activeTab: 'query', copiedSchema: false,\n cacheKey: `sune_${window.SUNE.id}_d1_cache`,\n historyKey: `sune_${window.SUNE.id}_d1_history`,\n\n init() {\n this.$watch('query', val => this.saveState()); this.loadState(); this.fetchAccountId();\n this.$nextTick(() => { this.$refs.queryTextarea?.addEventListener('keydown', e => { if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') { e.preventDefault(); this.runQuery(); } }); });\n },\n async cfApi(endpoint, options = {}) {\n this.error = '';\n const headers = { ...options.headers, 'Authorization': `Bearer ${window.USER.apiKeyCloudflare}`, 'Content-Type': 'application/json' };\n try {\n const res = options.raw ? await fetch(`https://apip.awww.workers.dev/client/v4${endpoint}`, { ...options, headers }) : await fetch(`https://apip.awww.workers.dev/client/v4${endpoint}`, { ...options, headers }).then(r => r.json());\n if (res.success === false || res.ok === false) throw new Error(res.errors?.[0]?.message || `HTTP error! Status: ${res.status}`);\n return res;\n } catch (e) { this.error = e.message; console.error('D1 API Error:', e); Object.keys(this.isLoading).forEach(k => this.isLoading[k] = false); return null;}\n },\n saveState() { try { localStorage.setItem(this.cacheKey, JSON.stringify({ selectedDbUuid: this.selectedDb?.uuid })); localStorage.setItem(this.historyKey, JSON.stringify(this.queryHistory)); } catch (e) { console.warn('Failed to save state', e) } },\n loadState() { try { this.initialDbUuid = JSON.parse(localStorage.getItem(this.cacheKey))?.selectedDbUuid || null; this.queryHistory = JSON.parse(localStorage.getItem(this.historyKey)) || []; } catch (e) { console.warn('Failed to load state', e) } },\n addToHistory(q) { if (!q || q === this.queryHistory[0]) return; this.queryHistory.unshift(q); if (this.queryHistory.length > 20) this.queryHistory.pop(); this.saveState(); },\n async fetchAccountId() {\n if (!window.USER.apiKeyCloudflare) { this.error = \"Cloudflare API Token not set.\"; this.isLoading.initial = false; return; }\n const data = await this.cfApi('/accounts'); if (data?.result?.[0]?.id) { this.accountId = data.result[0].id; await this.fetchDatabases(); } this.isLoading.initial = false;\n },\n async fetchDatabases(force = false) {\n if (!this.accountId) return; this.isLoading.dbs = true;\n const data = await this.cfApi(`/accounts/${this.accountId}/d1/database`);\n if (data?.result) { this.databases = data.result.sort((a,b) => a.name.localeCompare(b.name)); if (this.initialDbUuid && !force) { const db = this.databases.find(db => db.uuid === this.initialDbUuid); if (db) this.selectDb(db); this.initialDbUuid = null; } }\n this.isLoading.dbs = false;\n },\n async selectDb(db) { this.selectedDb = db; this.results = null; this.view = 'db'; this.activeTab='query'; this.tableDetails = {}; this.tableData = {}; await this.fetchTables(); this.saveState(); },\n async fetchTables(force = false) {\n if (!this.selectedDb) return; this.isLoading.tables = true; this.tableDetails={}; this.tableData={};\n const res = await this.cfApi(`/accounts/${this.accountId}/d1/database/${this.selectedDb.uuid}/query`, { method: 'POST', body: JSON.stringify({ sql: `SELECT name, sql FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name NOT LIKE '_cf_%';` }), });\n if (res?.result?.[0]?.results) this.tables = res.result[0].results; this.isLoading.tables = false;\n },\n async fetchTableInfo(tableName, force = false) {\n if (!this.selectedDb || (this.tableDetails[tableName] && !force)) return;\n this.isLoading.columns = tableName;\n const [colRes, dataRes] = await Promise.all([\n this.cfApi(`/accounts/${this.accountId}/d1/database/${this.selectedDb.uuid}/query`, { method: 'POST', body: JSON.stringify({ sql: `PRAGMA table_info(\\`${tableName}\\`);` }) }),\n this.cfApi(`/accounts/${this.accountId}/d1/database/${this.selectedDb.uuid}/query`, { method: 'POST', body: JSON.stringify({ sql: `SELECT * FROM \\`${tableName}\\` LIMIT 25;` }) })\n ]);\n if(colRes?.result?.[0]?.results) {\n const cols=colRes.result[0].results, pkCols=cols.filter(c=>c.pk>0);\n this.tableDetails[tableName] = { columns: cols, pkColumn: pkCols.length===1?pkCols[0].name:null };\n }\n if(dataRes?.result?.[0]?.results) this.tableData[tableName] = dataRes.result[0].results;\n this.isLoading.columns = false;\n },\n async deleteRow(tableName, pkValue) {\n const pkCol = this.tableDetails[tableName]?.pkColumn; if (!pkCol) return;\n if (!confirm(`Permanently delete row from \"${tableName}\" where ${pkCol} = ${pkValue}?`)) return;\n this.isLoading.rowDelete = true;\n const q = `DELETE FROM \\`${tableName}\\` WHERE \\`${pkCol}\\` = ?;`;\n const res = await this.cfApi(`/accounts/${this.accountId}/d1/database/${this.selectedDb.uuid}/query`, { method: 'POST', body: JSON.stringify({ sql: q, params: [pkValue] }) });\n if (res?.success) { await this.fetchTableInfo(tableName, true); }\n this.isLoading.rowDelete = false;\n },\n async cloneDb() {\n if(!this.tables.length){this.error=\"Cannot clone empty db.\";return}\n const newName=prompt('Enter name for the new cloned database:');\n if(!newName?.trim())return;\n this.isLoading.cloning=true;\n try{\n const createRes=await this.cfApi(`/accounts/${this.accountId}/d1/database`,{method:'POST',body:JSON.stringify({name:newName.trim()})});\n if(!createRes?.result?.uuid)throw new Error('Failed to create db. Name might exist.');\n const newDbUuid=createRes.result.uuid;\n const schemaSql=this.tables.map(t=>t.sql).join(';\\n')+';';\n const applySchemaRes=await this.cfApi(`/accounts/${this.accountId}/d1/database/${newDbUuid}/query`,{method:'POST',body:JSON.stringify({sql:schemaSql})});\n if(!applySchemaRes?.success){\n await this.cfApi(`/accounts/${this.accountId}/d1/database/${newDbUuid}`,{method:'DELETE'});\n throw new Error('Schema apply failed. Cloned db deleted. Error: '+applySchemaRes?.errors?.[0]?.message);\n }\n alert(`'${newName.trim()}' created successfully!`);\n await this.fetchDatabases(true); this.goBack();\n }catch(e){this.error=e.message}\n finally{this.isLoading.cloning=false}\n },\n async runQuery(){if(this.isLoading.query||!this.query.trim())return;this.isLoading.query=!0;this.results=null;const res=await this.cfApi(`/accounts/${this.accountId}/d1/database/${this.selectedDb.uuid}/query`,{method:'POST',body:JSON.stringify({sql:this.query})});if(res?.result?.[0]){this.results=res.result[0]}else if(res?.errors){this.results=res}this.addToHistory(this.query);this.isLoading.query=!1;if(this.results.success&&/(CREATE|DROP|ALTER|INSERT|DELETE|UPDATE)\\s/i.test(this.query)){this.fetchTables(!0)}},\n async createDb(){const name=prompt('Enter new database name:');if(!name?.trim())return;const data=await this.cfApi(`/accounts/${this.accountId}/d1/database`,{method:'POST',body:JSON.stringify({name:name})});if(data?.result)this.fetchDatabases(!0)},\n async deleteDb(db){if(!confirm(`Delete \"${db.name}\"? This is irreversible.`))return;const data=await this.cfApi(`/accounts/${this.accountId}/d1/database/${db.uuid}`,{method:'DELETE'});if(data?.success){this.selectedDb=null;this.view='list';this.fetchDatabases(!0)}},\n async createBackup(db){this.isLoading.backup=!0;const data=await this.cfApi(`/accounts/${this.accountId}/d1/database/${db.uuid}/backup`,{method:'POST',raw:!0});if(data.ok){const blob=await data.blob(),a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download=`${db.name}_${new Date().toISOString().split('T')[0]}.sql`;document.body.appendChild(a);a.click();document.body.removeChild(a);URL.revokeObjectURL(a.href)}this.isLoading.backup=!1},\n async copySchema(){if(!this.tables.length)return;try{const schema=this.tables.map(t=>t.sql).join(';\\n\\n')+';';await navigator.clipboard.writeText(schema);this.copiedSchema=!0;setTimeout(()=>this.copiedSchema=!1,2e3)}catch(e){console.error('Copy schema failed',e)}},\n async pasteQuery() { try { this.query = await navigator.clipboard.readText(); } catch (e) { console.error('Paste failed', e); } },\n clearQuery() { this.query = ''; },\n useSnippet(text) { this.query = text; },\n goBack() { this.selectedDb = null; this.tables = []; this.tableDetails = {}; this.tableData = {}; this.view = 'list'; this.results = null; this.saveState(); },\n renderResults() {\n if (!this.results) return '';\n if (this.results.success) {\n const r=this.results.results||[],info=`<p class=\"font-semibold text-green-800\">Success</p><p class=\"text-xs text-green-700\">Duration: ${this.results.meta.duration.toFixed(2)}ms. ${r.length} rows returned.</p>`;\n if(!r.length)return `<div class=\"bg-green-50 border border-green-200 p-3 rounded-lg\">${info}</div>`;\n const h=Object.keys(r[0]).map(k=>`<th class=\"p-1.5 font-medium border-b border-green-200\">${esc(k)}</th>`).join(''),b=r.map(row=>`<tr class=\"border-b border-green-100 last:border-b-0\">${Object.values(row).map(v=>`<td class=\"p-1.5 align-top\">${esc(v)}</td>`).join('')}</tr>`).join('');\n return `<div class=\"bg-green-50 border border-green-200 p-3 rounded-lg\">${info}<div class=\"mt-3 overflow-x-auto border-t border-green-200 pt-2\"><table class=\"w-full text-left text-xs\"><thead><tr>${h}</tr></thead><tbody>${b}</tbody></table></div></div>`;\n }return `<div class=\"bg-red-50 border border-red-200 p-3 rounded-lg\"><p class=\"font-semibold text-red-800\">Query Failed</p><pre class=\"text-xs text-red-700 whitespace-pre-wrap font-mono mt-1\">${esc(this.results.errors[0].message)}</pre></div>`;\n }\n }\n}\n</script>\n","extension_html":"<sune src='https://raw.githubusercontent.com/sune-org/store/refs/heads/main/sync.sune' private></sune>","hide_composer":true,"include_thoughts":false,"json_output":false,"ignore_master_prompt":false,"json_schema":""},"storage":{}}] |