mirror of
https://github.com/sune-org/store.git
synced 2026-01-14 00:27:59 +00:00
1 line
14 KiB
JSON
1 line
14 KiB
JSON
[{"id":"xo6k2dz","name":"D1","pinned":false,"avatar":"","url":"gh://sune-org/store/cloudflare-utilities/d1.sune","updatedAt":1757415040599,"settings":{"model":"openai/gpt-5-chat","temperature":"","top_p":"","top_k":"","frequency_penalty":"","repetition_penalty":"","min_p":"","top_a":"","verbosity":"","reasoning_effort":"default","system_prompt":"","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.0.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=\"`Account: ${accountId.substring(0, 8)}...`\"></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 <!-- Actions -->\n <div class=\"grid grid-cols-2 sm:grid-cols-4 gap-2 mb-4 text-xs\">\n <button @click=\"fetchTables(true)\" class=\"flex items-center justify-center gap-1.5 p-2 bg-white border rounded-md hover:bg-gray-100 transition\"><i data-lucide=\"refresh-cw\" class=\"h-3.5 w-3.5\" :class=\"{'animate-spin':isLoading.tables}\"></i>Tables</button>\n <button @click=\"createBackup(selectedDb)\" :disabled=\"isLoading.backup\" class=\"flex items-center justify-center gap-1.5 p-2 bg-white border rounded-md hover:bg-gray-100 transition disabled:opacity-50\"><i data-lucide=\"download\" class=\"h-3.5 w-3.5\"></i><span x-text=\"isLoading.backup?'Backing up...':'Backup'\"></span></button>\n <button @click=\"view='schema'\" class=\"flex items-center justify-center gap-1.5 p-2 bg-white border rounded-md hover:bg-gray-100 transition\"><i data-lucide=\"file-json-2\" class=\"h-3.5 w-3.5\"></i>Schema</button>\n <button @click=\"deleteDb(selectedDb)\" class=\"flex items-center justify-center gap-1.5 p-2 bg-white border border-red-200 text-red-600 rounded-md hover:bg-red-50 transition\"><i data-lucide=\"trash-2\" class=\"h-3.5 w-3.5\"></i>Delete</button>\n </div>\n \n <!-- Schema View -->\n <template x-if=\"view === 'schema'\">\n <div class=\"mb-4\">\n <h4 class=\"font-medium mb-2\">Schema</h4>\n <pre class=\"text-xs bg-gray-900 text-white p-3 rounded-md overflow-auto max-h-60\" x-text=\"tables.map(t => t.sql).join('\\n\\n') || 'No tables found.'\"></pre>\n </div>\n </template>\n\n <!-- Query Runner -->\n <div class=\"space-y-3\">\n <label for=\"d1-query\" class=\"font-medium\">SQL Query</label>\n <textarea id=\"d1-query\" 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 <button @click=\"runQuery()\" :disabled=\"isLoading.query\" class=\"w-full 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>\n\n <!-- Query Results -->\n <div x-show=\"results\" class=\"mt-5\" x-cloak>\n <template x-if=\"results.success\">\n <div class=\"bg-green-50 border border-green-200 p-3 rounded-lg\">\n <p class=\"font-semibold text-green-800\">Success</p>\n <p class=\"text-xs text-green-700\" x-text=\"`Duration: ${results.meta.duration.toFixed(2)}ms. ${results.results?.length || 0} rows returned.`\"></p>\n <template x-if=\"results.results?.length\">\n <div class=\"mt-3 overflow-x-auto border-t border-green-200 pt-2\">\n <table class=\"w-full text-left text-xs\">\n <thead class=\"text-gray-500\">\n <tr>\n <template x-for=\"key in Object.keys(results.results[0])\">\n <th class=\"p-1.5 font-medium border-b border-green-200\" x-text=\"key\"></th>\n </template>\n </tr>\n </thead>\n <tbody>\n <template x-for=\"row in results.results\">\n <tr class=\"border-b border-green-100 last:border-b-0\">\n <template x-for=\"val in Object.values(row)\">\n <td class=\"p-1.5 align-top\" x-text=\"val\"></td>\n </template>\n </tr>\n </template>\n </tbody>\n </table>\n </div>\n </template>\n </div>\n </template>\n <template x-if=\"!results.success\">\n <div class=\"bg-red-50 border border-red-200 p-3 rounded-lg\">\n <p class=\"font-semibold text-red-800\">Query Failed</p>\n <pre class=\"text-xs text-red-700 whitespace-pre-wrap font-mono mt-1\" x-text=\"results.errors[0].message\"></pre>\n </div>\n </template>\n </div>\n </div>\n </div>\n</div>\n\n<script>\nfunction d1Manager() {\n return {\n version: '1.0.0',\n isLoading: { initial: true, dbs: false, query: false, backup: false, tables: false },\n error: '',\n accountId: '',\n databases: [],\n selectedDb: null,\n tables: [],\n query: 'SELECT name, sql FROM sqlite_master WHERE type=\\'table\\';',\n results: null,\n view: 'list',\n cacheKey: `sune_${window.SUNE.id}_d1_cache`,\n\n // --- Core Methods ---\n init() {\n this.$watch('query', val => this.saveState());\n this.loadState();\n this.fetchAccountId();\n },\n async cfApi(endpoint, options = {}) {\n this.error = '';\n const headers = { ...options.headers, 'Authorization': `Bearer ${window.USER.apiKeyCF}`, 'Content-Type': 'application/json' };\n try {\n const res = options.raw ? \n await fetch(`https://apip.awww.workers.dev/client/v4${endpoint}`, { ...options, headers }) :\n 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) {\n this.error = e.message;\n console.error('D1 API Error:', e);\n // Reset specific loading states on error\n Object.keys(this.isLoading).forEach(k => this.isLoading[k] = false);\n }\n },\n // --- State Management ---\n saveState() {\n try {\n const state = { query: this.query, selectedDbUuid: this.selectedDb?.uuid };\n localStorage.setItem(this.cacheKey, JSON.stringify(state));\n } catch (e) { console.warn('Failed to save state', e) }\n },\n loadState() {\n try {\n const state = JSON.parse(localStorage.getItem(this.cacheKey));\n if (state) {\n this.query = state.query || this.query;\n this.initialDbUuid = state.selectedDbUuid || null;\n }\n } catch (e) { console.warn('Failed to load state', e) }\n },\n \n // --- Data Fetching & Actions ---\n async fetchAccountId() {\n if (!window.USER.apiKeyCF) {\n this.error = \"Cloudflare API Token not set in Account Settings.\";\n this.isLoading.initial = false;\n return;\n }\n const data = await this.cfApi('/accounts');\n if (data?.result?.[0]?.id) {\n this.accountId = data.result[0].id;\n await this.fetchDatabases();\n }\n this.isLoading.initial = false;\n },\n async fetchDatabases(force = false) {\n if (!this.accountId) return;\n this.isLoading.dbs = true;\n const data = await this.cfApi(`/accounts/${this.accountId}/d1/database`);\n if (data?.result) {\n this.databases = data.result.sort((a,b) => a.name.localeCompare(b.name));\n if (this.initialDbUuid && !force) {\n const cachedDb = this.databases.find(db => db.uuid === this.initialDbUuid);\n if (cachedDb) this.selectDb(cachedDb);\n this.initialDbUuid = null; // Clear after first load\n }\n }\n this.isLoading.dbs = false;\n },\n async selectDb(db) {\n this.selectedDb = db;\n this.results = null;\n this.view = 'db';\n await this.fetchTables();\n this.saveState();\n },\n async fetchTables(force = false) {\n if (!this.selectedDb) return;\n this.isLoading.tables = true;\n const res = await this.cfApi(`/accounts/${this.accountId}/d1/database/${this.selectedDb.uuid}/query`, {\n method: 'POST',\n body: JSON.stringify({ sql: `SELECT name, sql FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name NOT LIKE '_cf_%';` }),\n });\n if (res?.result?.[0]?.results) this.tables = res.result[0].results;\n this.isLoading.tables = false;\n },\n async runQuery() {\n if (!this.query.trim() || !this.selectedDb) return;\n this.isLoading.query = true;\n const data = await this.cfApi(`/accounts/${this.accountId}/d1/database/${this.selectedDb.uuid}/query`, {\n method: 'POST',\n body: JSON.stringify({ sql: this.query }),\n });\n if (data?.result?.[0]) {\n this.results = data.result[0];\n } else if (data) {\n // Handle cases where API returns success:false\n this.results = data;\n }\n this.isLoading.query = false;\n },\n async createDb() {\n const name = prompt(\"Enter new database name (e.g., 'my-app-db'):\");\n if (!name || !name.trim()) return;\n this.isLoading.dbs = true;\n await this.cfApi(`/accounts/${this.accountId}/d1/database`, {\n method: 'POST',\n body: JSON.stringify({ name: name.trim() }),\n });\n await this.fetchDatabases(true);\n },\n async deleteDb(db) {\n if (!confirm(`Are you sure you want to PERMANENTLY DELETE the database \"${db.name}\"?\\n\\nThis action cannot be undone.`)) return;\n this.isLoading.dbs = true;\n await this.cfApi(`/accounts/${this.accountId}/d1/database/${db.uuid}`, { method: 'DELETE' });\n this.goBack();\n await this.fetchDatabases(true);\n },\n async createBackup(db) {\n if (!confirm(`Create and download a backup for \"${db.name}\"?`)) return;\n this.isLoading.backup = true;\n const res = await this.cfApi(`/accounts/${this.accountId}/d1/database/${db.uuid}/backup`, { raw: true });\n if (res?.ok) {\n const blob = await res.blob();\n const url = URL.createObjectURL(blob);\n const a = document.createElement('a');\n a.href = url;\n a.download = `${db.name}-${new Date().toISOString().split('T')[0]}.sql`;\n document.body.appendChild(a);\n a.click();\n a.remove();\n URL.revokeObjectURL(url);\n }\n this.isLoading.backup = false;\n },\n goBack() {\n this.selectedDb = null;\n this.tables = [];\n this.view = 'list';\n this.results = null;\n this.saveState();\n },\n }\n}\n</script>\n","extension_html":"<sune src='https://raw.githubusercontent.com/sune-org/store/refs/heads/main/sync.sune' private />","hide_composer":false},"storage":{}}] |