├── .npmrc ├── .gitattributes ├── .husky ├── post-merge └── pre-commit ├── images ├── tables.png ├── bind-d1.png ├── add-record.png ├── run-query.png ├── set-env-var.png ├── table-browser.png ├── semantic-query.png └── semantic-query-demo.gif ├── project.inlang ├── project_id └── settings.json ├── static └── favicon.png ├── .changeset ├── tricky-garlics-pump.md ├── gorgeous-pots-leave.md ├── popular-pets-smell.md ├── three-shoes-hunt.md ├── config.json └── README.md ├── pnpm-workspace.yaml ├── .github ├── renovate.json └── workflows │ ├── test.yml │ └── release.yml ├── src ├── index.test.ts ├── lib │ ├── log.ts │ ├── plugin │ │ ├── type.ts │ │ ├── RunQuery.svelte │ │ ├── SemanticQuery.svelte │ │ ├── AddRecord.svelte │ │ ├── CSV.svelte │ │ └── TableBrowser.svelte │ ├── i18n.ts │ ├── csv.ts │ ├── server │ │ ├── db │ │ │ └── dbms.ts │ │ └── ai │ │ │ └── index.ts │ ├── sqlite.ts │ ├── sql.ts │ ├── storage.ts │ ├── sqlite2sql.ts │ └── sql.test.ts ├── routes │ ├── db │ │ ├── +page.server.ts │ │ └── [database] │ │ │ ├── +layout.server.ts │ │ │ ├── [table] │ │ │ ├── SidePanel.svelte │ │ │ └── +page.svelte │ │ │ ├── +layout.svelte │ │ │ └── +page.svelte │ ├── +layout.server.ts │ ├── api │ │ ├── db │ │ │ ├── [database] │ │ │ │ ├── dump │ │ │ │ │ ├── +server.ts │ │ │ │ │ └── [filename] │ │ │ │ │ │ └── +server.ts │ │ │ │ ├── exec │ │ │ │ │ └── +server.ts │ │ │ │ ├── all │ │ │ │ │ └── +server.ts │ │ │ │ ├── [table] │ │ │ │ │ ├── +server.ts │ │ │ │ │ └── data │ │ │ │ │ │ └── +server.ts │ │ │ │ ├── assistant │ │ │ │ │ └── +server.ts │ │ │ │ └── +server.ts │ │ │ └── +server.ts │ │ └── +server.ts │ ├── +layout.ts │ ├── +layout.svelte │ └── +page.svelte ├── app.css ├── app.html ├── app.d.ts └── hooks.server.ts ├── tests └── test.ts ├── .gitignore ├── .devcontainer ├── docker-compose.yml ├── Dockerfile └── devcontainer.json ├── playwright.config.ts ├── .vscode ├── extensions.json └── settings.json ├── vite.config.ts ├── .prettierrc.yml ├── svelte.config.js ├── tsconfig.json ├── LICENSE ├── eslint.config.js ├── locales ├── zh-CN.json ├── zh-TW.json ├── ja.json ├── en.json ├── pt.json ├── es-ES.json └── es-MX.json ├── README.md └── package.json /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.husky/post-merge: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | pnpm i 5 | -------------------------------------------------------------------------------- /images/tables.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JacobLinCool/d1-manager/HEAD/images/tables.png -------------------------------------------------------------------------------- /project.inlang/project_id: -------------------------------------------------------------------------------- 1 | 1ee0eaed580e51c9eaf903e05c8923cc7f4c14c62c10d0df91db814d86a09d26 -------------------------------------------------------------------------------- /images/bind-d1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JacobLinCool/d1-manager/HEAD/images/bind-d1.png -------------------------------------------------------------------------------- /static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JacobLinCool/d1-manager/HEAD/static/favicon.png -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | pnpm lint-staged 5 | -------------------------------------------------------------------------------- /images/add-record.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JacobLinCool/d1-manager/HEAD/images/add-record.png -------------------------------------------------------------------------------- /images/run-query.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JacobLinCool/d1-manager/HEAD/images/run-query.png -------------------------------------------------------------------------------- /images/set-env-var.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JacobLinCool/d1-manager/HEAD/images/set-env-var.png -------------------------------------------------------------------------------- /images/table-browser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JacobLinCool/d1-manager/HEAD/images/table-browser.png -------------------------------------------------------------------------------- /.changeset/tricky-garlics-pump.md: -------------------------------------------------------------------------------- 1 | --- 2 | "d1-manager": patch 3 | --- 4 | 5 | Change D1 shim fallback logic 6 | -------------------------------------------------------------------------------- /images/semantic-query.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JacobLinCool/d1-manager/HEAD/images/semantic-query.png -------------------------------------------------------------------------------- /.changeset/gorgeous-pots-leave.md: -------------------------------------------------------------------------------- 1 | --- 2 | "d1-manager": minor 3 | --- 4 | 5 | Add CSV Support (Export / Import) 6 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | onlyBuiltDependencies: 2 | - es5-ext 3 | - esbuild 4 | - sharp 5 | - workerd 6 | -------------------------------------------------------------------------------- /.changeset/popular-pets-smell.md: -------------------------------------------------------------------------------- 1 | --- 2 | "d1-manager": minor 3 | --- 4 | 5 | Support Cloudflare AI as query builder 6 | -------------------------------------------------------------------------------- /images/semantic-query-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JacobLinCool/d1-manager/HEAD/images/semantic-query-demo.gif -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["config:base"] 4 | } 5 | -------------------------------------------------------------------------------- /src/index.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | 3 | describe("sum test", () => { 4 | it("adds 1 + 2 to equal 3", () => { 5 | expect(1 + 2).toBe(3); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /src/lib/log.ts: -------------------------------------------------------------------------------- 1 | import debug from "debug"; 2 | 3 | export const log = debug("d1-manager"); 4 | export const extend = (name: string) => log.extend(name); 5 | debug.enable("d1-manager*"); 6 | -------------------------------------------------------------------------------- /src/routes/db/+page.server.ts: -------------------------------------------------------------------------------- 1 | import { redirect } from "@sveltejs/kit"; 2 | import type { PageServerLoad } from "./$types"; 3 | 4 | export const load: PageServerLoad = async () => { 5 | throw redirect(301, "/"); 6 | }; 7 | -------------------------------------------------------------------------------- /tests/test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test"; 2 | 3 | test("index page has expected h1", async ({ page }) => { 4 | await page.goto("/"); 5 | await expect(page.getByRole("heading")).toBeVisible(); 6 | }); 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | vite.config.js.timestamp-* 10 | vite.config.ts.timestamp-* 11 | wrangler.toml 12 | 13 | .wrangler/ 14 | 15 | project.inlang/cache 16 | -------------------------------------------------------------------------------- /.devcontainer/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | 3 | services: 4 | workspace: 5 | build: 6 | context: . 7 | dockerfile: Dockerfile 8 | volumes: 9 | - ..:/workspace 10 | command: sleep infinity 11 | -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | import type { PlaywrightTestConfig } from "@playwright/test"; 2 | 3 | const config: PlaywrightTestConfig = { 4 | webServer: { 5 | command: "npm run build && npm run preview", 6 | port: 4173, 7 | }, 8 | testDir: "tests", 9 | }; 10 | 11 | export default config; 12 | -------------------------------------------------------------------------------- /.changeset/three-shoes-hunt.md: -------------------------------------------------------------------------------- 1 | --- 2 | "d1-manager": major 3 | --- 4 | 5 | First Release for Web UI and API 6 | 7 | - [x] Multiple D1 Databases 8 | - [x] List all tables in a database 9 | - [x] Show table schema 10 | - [x] Run SQL queries 11 | - [x] I18n support (English, Chinese) 12 | - [x] API support 13 | -------------------------------------------------------------------------------- /src/app.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | @plugin "@tailwindcss/typography"; 3 | @plugin "daisyui"; 4 | 5 | * { 6 | @apply relative font-sans; 7 | } 8 | 9 | *::-webkit-scrollbar { 10 | @apply hidden; 11 | } 12 | 13 | html, 14 | body { 15 | @apply absolute m-0 h-full w-full overflow-hidden p-0 transition-colors; 16 | } 17 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "esbenp.prettier-vscode", 5 | "GitHub.copilot", 6 | "GitHub.copilot-labs", 7 | "svelte.svelte-vscode", 8 | "bradlc.vscode-tailwindcss", 9 | "antfu.iconify", 10 | "lokalise.i18n-ally", 11 | "inlang.vs-code-extension" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /src/routes/+layout.server.ts: -------------------------------------------------------------------------------- 1 | import { available } from "$lib/server/ai"; 2 | import type { LayoutServerLoad } from "./$types"; 3 | 4 | export const load: LayoutServerLoad = async ({ fetch }) => { 5 | const dbms = await fetch("/api/db").then((r) => r.json()); 6 | const assistant = available(); 7 | return { dbms, assistant }; 8 | }; 9 | -------------------------------------------------------------------------------- /src/lib/plugin/type.ts: -------------------------------------------------------------------------------- 1 | export type PluginData = { 2 | db: { 3 | name: string; 4 | columns: [ 5 | { 6 | cid: number; 7 | name: string; 8 | type: "INTEGER" | "TEXT" | "REAL" | "BLOB"; 9 | notnull: number; 10 | dflt_value: string | null; 11 | pk: number; 12 | }, 13 | ]; 14 | count: number; 15 | }[]; 16 | }; 17 | -------------------------------------------------------------------------------- /src/routes/api/db/[database]/dump/+server.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This route dumps a database into a file. 3 | */ 4 | import { redirect } from "@sveltejs/kit"; 5 | import type { RequestHandler } from "./$types"; 6 | 7 | export const GET: RequestHandler = async ({ params }) => { 8 | return redirect(301, `/api/db/${params.database}/dump/db-${params.database}.sqlite3`); 9 | }; 10 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.3.0/schema.json", 3 | "changelog": ["@changesets/changelog-github", { "repo": "jacoblincool/d1-manager" }], 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "public", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [] 11 | } 12 | -------------------------------------------------------------------------------- /src/routes/+layout.ts: -------------------------------------------------------------------------------- 1 | import { browser } from "$app/environment"; 2 | import "$lib/i18n"; 3 | import { locale, waitLocale } from "svelte-i18n"; 4 | import type { LayoutLoad } from "./$types"; 5 | 6 | export const load: LayoutLoad = async ({ data }) => { 7 | if (browser) { 8 | locale.set(window.navigator.language); 9 | } 10 | await waitLocale(); 11 | 12 | return data; 13 | }; 14 | -------------------------------------------------------------------------------- /src/routes/api/+server.ts: -------------------------------------------------------------------------------- 1 | import { version } from "$app/environment"; 2 | import { available } from "$lib/server/ai"; 3 | import { json } from "@sveltejs/kit"; 4 | import type { RequestHandler } from "./$types"; 5 | 6 | export const GET: RequestHandler = async ({ locals }) => { 7 | return json({ 8 | db: Object.keys(locals.db), 9 | assistant: available(), 10 | version, 11 | }); 12 | }; 13 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { sveltekit } from "@sveltejs/kit/vite"; 2 | import tailwindcss from "@tailwindcss/vite"; 3 | import { defineConfig } from "vitest/config"; 4 | 5 | export default defineConfig({ 6 | plugins: [tailwindcss(), sveltekit()], 7 | test: { 8 | include: ["src/**/*.{test,spec}.{js,ts}"], 9 | }, 10 | server: { 11 | fs: { 12 | allow: ["./locales"], 13 | }, 14 | }, 15 | }); 16 | -------------------------------------------------------------------------------- /src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %sveltekit.head% 8 | 9 | 10 |
%sveltekit.body%
11 | 12 | 13 | -------------------------------------------------------------------------------- /.prettierrc.yml: -------------------------------------------------------------------------------- 1 | --- 2 | printWidth: 100 3 | tabWidth: 4 4 | useTabs: true 5 | trailingComma: all 6 | semi: true 7 | singleQuote: false 8 | 9 | overrides: 10 | - files: "*.md" 11 | options: 12 | useTabs: false 13 | - files: "*.svelte" 14 | options: 15 | parser: svelte 16 | 17 | plugins: 18 | - prettier-plugin-svelte 19 | - prettier-plugin-organize-imports 20 | - prettier-plugin-tailwindcss 21 | -------------------------------------------------------------------------------- /src/app.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare global { 4 | namespace App { 5 | // interface Error {} 6 | interface Locals { 7 | db: Record; 8 | } 9 | // interface PageData {} 10 | interface Platform { 11 | env: { 12 | SHOW_INTERNAL_TABLES?: string; 13 | OPENAI_API_KEY?: string; 14 | AI?: unknown; 15 | } & Record; 16 | } 17 | } 18 | } 19 | 20 | export {}; 21 | -------------------------------------------------------------------------------- /src/routes/api/db/+server.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This route reports the connected databases. 3 | */ 4 | import { dev } from "$app/environment"; 5 | import { json } from "@sveltejs/kit"; 6 | import type { RequestHandler } from "./$types"; 7 | 8 | export const GET: RequestHandler = async ({ locals, url, fetch }) => { 9 | if (dev) { 10 | const remote = new URL("https://d1-manager.pages.dev" + url.pathname + url.search); 11 | return fetch(remote); 12 | } 13 | 14 | return json(Object.keys(locals.db)); 15 | }; 16 | -------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | import adapter from "@sveltejs/adapter-cloudflare"; 2 | import { vitePreprocess } from "@sveltejs/vite-plugin-svelte"; 3 | 4 | /** @type {import('@sveltejs/kit').Config} */ 5 | const config = { 6 | // Consult https://kit.svelte.dev/docs/integrations#preprocessors 7 | // for more information about preprocessors 8 | preprocess: vitePreprocess(), 9 | 10 | kit: { 11 | adapter: adapter(), 12 | alias: { 13 | "$i18n/*": "./locales/*", 14 | }, 15 | }, 16 | }; 17 | 18 | export default config; 19 | -------------------------------------------------------------------------------- /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /src/lib/i18n.ts: -------------------------------------------------------------------------------- 1 | import { browser } from "$app/environment"; 2 | import { init, register } from "svelte-i18n"; 3 | 4 | const fallback = "en"; 5 | 6 | register("en", () => import("$i18n/en.json")); 7 | register("zh-TW", () => import("$i18n/zh-TW.json")); 8 | register("zh-CN", () => import("$i18n/zh-CN.json")); 9 | register("es-ES", () => import("$i18n/es-ES.json")); 10 | register("es-MX", () => import("$i18n/es-MX.json")); 11 | register("ja", () => import("$i18n/ja.json")); 12 | 13 | init({ 14 | fallbackLocale: fallback, 15 | initialLocale: browser ? window.navigator.language : fallback, 16 | }); 17 | -------------------------------------------------------------------------------- /src/lib/csv.ts: -------------------------------------------------------------------------------- 1 | export async function export_csv(results: Record[], name = "table") { 2 | if (!results) { 3 | return; 4 | } 5 | 6 | const module = await import("csv-stringify/browser/esm/sync"); 7 | const { stringify } = module; 8 | 9 | const csv = stringify(results, { 10 | header: true, 11 | columns: Object.keys(results[0]), 12 | }); 13 | 14 | const a = document.createElement("a"); 15 | a.href = URL.createObjectURL(new Blob([csv], { type: "text/csv" })); 16 | a.setAttribute("download", `${name}.csv`); 17 | a.click(); 18 | URL.revokeObjectURL(a.href); 19 | a.remove(); 20 | } 21 | -------------------------------------------------------------------------------- /src/routes/db/[database]/+layout.server.ts: -------------------------------------------------------------------------------- 1 | import type { LayoutServerLoad } from "./$types"; 2 | 3 | export const load: LayoutServerLoad = async ({ fetch, params }) => { 4 | const db = await fetch(`/api/db/${params.database}`).then((r) => 5 | r.json< 6 | { 7 | name: string; 8 | columns: [ 9 | { 10 | cid: number; 11 | name: string; 12 | type: "INTEGER" | "TEXT" | "REAL" | "BLOB"; 13 | notnull: number; 14 | dflt_value: null | string; 15 | pk: number; 16 | }, 17 | ]; 18 | count: number; 19 | }[] 20 | >(), 21 | ); 22 | return { db }; 23 | }; 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true 12 | } 13 | // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias 14 | // 15 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes 16 | // from the referenced tsconfig.json - TypeScript does not merge them in 17 | } 18 | -------------------------------------------------------------------------------- /src/lib/server/db/dbms.ts: -------------------------------------------------------------------------------- 1 | import { extend } from "$lib/log"; 2 | import type { D1Database } from "@cloudflare/workers-types"; 3 | 4 | const log = extend("dbms"); 5 | 6 | export function DBMS( 7 | env: Record, 8 | ): Record { 9 | const keys = Object.keys(env).filter((k) => k.startsWith("DB")); 10 | log("Database Bindings:", keys.join(", ")); 11 | 12 | const results: Record = {}; 13 | for (const k of keys) { 14 | const e = env[k]; 15 | if (typeof e === "string") { 16 | continue; 17 | } 18 | if (!("prepare" in e)) { 19 | continue; 20 | } 21 | results[k.replace(/^DB_?/, "") || "default"] = e; 22 | } 23 | return results; 24 | } 25 | -------------------------------------------------------------------------------- /project.inlang/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://inlang.com/schema/project-settings", 3 | "sourceLanguageTag": "en", 4 | "languageTags": ["en", "es-ES", "es-MX", "ja", "zh-CN", "zh-TW"], 5 | "modules": [ 6 | "https://cdn.jsdelivr.net/npm/@inlang/plugin-json@4/dist/index.js", 7 | "https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-empty-pattern@1/dist/index.js", 8 | "https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-identical-pattern@1/dist/index.js", 9 | "https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-without-source@1/dist/index.js", 10 | "https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-missing-translation@1/dist/index.js" 11 | ], 12 | "plugin.inlang.json": { 13 | "pathPattern": "./locales/{languageTag}.json" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/routes/api/db/[database]/dump/[filename]/+server.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This route dumps a database into a file. 3 | */ 4 | import { dev } from "$app/environment"; 5 | import { error } from "@sveltejs/kit"; 6 | import type { RequestHandler } from "./$types"; 7 | 8 | export const GET: RequestHandler = async ({ params, locals, url, fetch }) => { 9 | if (dev) { 10 | const remote = new URL("https://d1-manager.pages.dev" + url.pathname + url.search); 11 | return fetch(remote); 12 | } 13 | 14 | const db = locals.db[params.database]; 15 | if (!db) { 16 | throw error(404, "Database not found"); 17 | } 18 | 19 | const data = await db.dump(); 20 | return new Response(data, { 21 | headers: { 22 | "Content-Type": "application/octet-stream", 23 | }, 24 | }); 25 | }; 26 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | test: 13 | name: Test 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Checkout Repository 18 | uses: actions/checkout@v4 19 | 20 | - name: Setup PNPM 21 | uses: pnpm/action-setup@v4 22 | with: 23 | run_install: true 24 | 25 | # - name: Install Browser 26 | # run: npx playwright install chromium --with-deps 27 | 28 | - name: Build All 29 | run: pnpm build 30 | 31 | # - name: Run Tests 32 | # run: pnpm test 33 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[svelte]": { 3 | "editor.formatOnSave": true 4 | }, 5 | "[typescript]": { 6 | "editor.formatOnSave": true 7 | }, 8 | "[css]": { 9 | "editor.formatOnSave": true 10 | }, 11 | "[json]": { 12 | "editor.formatOnSave": true 13 | }, 14 | "i18n-ally.localesPaths": ["locales"], 15 | "i18n-ally.keystyle": "nested", 16 | "i18n-ally.sortKeys": true, 17 | "i18n-ally.keepFulfilled": true, 18 | "i18n-ally.extract.parsers.html": { 19 | "attributes": ["text", "title", "alt", "placeholder", "label", "aria-label"], 20 | "ignoredTags": ["script", "style"], 21 | "inlineText": true 22 | }, 23 | "i18n-ally.extract.autoDetect": true, 24 | "i18n-ally.refactor.templates": [ 25 | { 26 | "source": "html-attribute", 27 | "templates": ["{$t('{key}'{args})}"], 28 | "include": ["src/**/*.{svelte,ts}", "index.html"] 29 | } 30 | ], 31 | "i18n-ally.sourceLanguage": "en" 32 | } 33 | -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/devcontainers/base:ubuntu 2 | 3 | ENV PNPM_HOME="/usr/local/share/pnpm" 4 | ENV PATH="$PNPM_HOME:$PATH" 5 | 6 | RUN export DEBIAN_FRONTEND=noninteractive && \ 7 | rm -f /bin/sh && \ 8 | ln -s /bin/bash /bin/sh && \ 9 | apt update && \ 10 | apt upgrade -y && \ 11 | apt install -y software-properties-common && \ 12 | add-apt-repository -y ppa:git-core/ppa && \ 13 | apt update && \ 14 | apt install -y git bash-completion htop jq lsof less curl zip unzip tree python3 build-essential && \ 15 | apt autoremove -y && \ 16 | rm -f /usr/bin/python && \ 17 | ln -s /usr/bin/python3 /usr/bin/python 18 | 19 | RUN curl -fsSL https://get.pnpm.io/install.sh | sh - && \ 20 | pnpm env use -g lts && \ 21 | sudo rm -rf /tmp/* && \ 22 | chmod -R 777 /usr/local/share/pnpm 23 | 24 | RUN pnpm i -g pnpm taze tsx prettier @changesets/cli serve 25 | -------------------------------------------------------------------------------- /src/hooks.server.ts: -------------------------------------------------------------------------------- 1 | import { extend } from "$lib/log"; 2 | import { DBMS } from "$lib/server/db/dbms"; 3 | import type { Handle, HandleServerError } from "@sveltejs/kit"; 4 | import { locale, waitLocale } from "svelte-i18n"; 5 | 6 | export const handle: Handle = async ({ event, resolve }) => { 7 | const lang = event.request.headers.get("accept-language")?.split(",")[0] || "en"; 8 | locale.set(lang); 9 | await waitLocale(lang); 10 | 11 | event.locals.db = DBMS(event.platform?.env || {}); 12 | 13 | const result = await resolve(event); 14 | return result; 15 | }; 16 | 17 | const elog = extend("server-error"); 18 | elog.enabled = true; 19 | 20 | export const handleError: HandleServerError = async ({ error }) => { 21 | elog(error); 22 | 23 | if (error instanceof Error && error.message.startsWith("D1_")) { 24 | return { 25 | code: 400, 26 | message: error.message, 27 | }; 28 | } 29 | 30 | return { 31 | code: 500, 32 | message: "Internal Server Error", 33 | }; 34 | }; 35 | -------------------------------------------------------------------------------- /src/routes/api/db/[database]/exec/+server.ts: -------------------------------------------------------------------------------- 1 | import { dev } from "$app/environment"; 2 | import { error, json } from "@sveltejs/kit"; 3 | import type { RequestHandler } from "./$types"; 4 | 5 | export const POST: RequestHandler = async ({ request, params, locals, url, fetch }) => { 6 | if (dev) { 7 | const remote = new URL("https://d1-manager.pages.dev" + url.pathname + url.search); 8 | const res = await fetch(remote, { method: "POST", body: await request.text() }); 9 | return json(await res.json()); 10 | } 11 | 12 | const db = locals.db[params.database]; 13 | if (!db) { 14 | throw error(404, "Database not found"); 15 | } 16 | 17 | let data: { query?: string }; 18 | try { 19 | data = await request.json(); 20 | } catch (err) { 21 | if (err instanceof Error) { 22 | throw error(400, err.message); 23 | } 24 | throw err; 25 | } 26 | 27 | const { query } = data; 28 | if (!query) { 29 | throw error(400, "Missing query"); 30 | } 31 | 32 | const result = await db.exec(query); 33 | return json(result); 34 | }; 35 | -------------------------------------------------------------------------------- /src/lib/sqlite.ts: -------------------------------------------------------------------------------- 1 | export type Type = "TEXT" | "INTEGER" | "REAL" | "BLOB" | "NUMERIC"; 2 | 3 | export function affinity(name?: string): Type { 4 | name = name?.toUpperCase(); 5 | 6 | if (name?.includes("INT")) { 7 | return "INTEGER"; 8 | } 9 | 10 | if (name?.includes("CHAR") || name?.includes("CLOB") || name?.includes("TEXT")) { 11 | return "TEXT"; 12 | } 13 | 14 | if (typeof name === "undefined" || name.includes("BLOB")) { 15 | return "BLOB"; 16 | } 17 | 18 | if (name.includes("REAL") || name.includes("FLOA") || name.includes("DOUB")) { 19 | return "REAL"; 20 | } 21 | 22 | return "NUMERIC"; 23 | } 24 | 25 | export function cast(value: unknown, type: T): unknown { 26 | switch (type) { 27 | case "TEXT": 28 | return String(value); 29 | case "INTEGER": 30 | return Number.isNaN(Number(value)) ? null : parseInt(value as string); 31 | case "REAL": 32 | return Number.isNaN(Number(value)) ? null : parseFloat(value as string); 33 | case "BLOB": 34 | return value; 35 | case "NUMERIC": 36 | return value; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 JacobLinCool 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/routes/api/db/[database]/all/+server.ts: -------------------------------------------------------------------------------- 1 | import { dev } from "$app/environment"; 2 | import { error, json } from "@sveltejs/kit"; 3 | import type { RequestHandler } from "./$types"; 4 | 5 | export const POST: RequestHandler = async ({ request, params, locals, url, fetch }) => { 6 | if (dev) { 7 | const remote = new URL("https://d1-manager.pages.dev" + url.pathname + url.search); 8 | const res = await fetch(remote, { 9 | method: "POST", 10 | body: await request.text(), 11 | headers: request.headers, 12 | }); 13 | return json(await res.json()); 14 | } 15 | 16 | const db = locals.db[params.database]; 17 | if (!db) { 18 | throw error(404, "Database not found"); 19 | } 20 | 21 | let data: { query?: string; params?: string[] }; 22 | try { 23 | data = await request.json(); 24 | } catch (err) { 25 | if (err instanceof Error) { 26 | throw error(400, err.message); 27 | } 28 | throw err; 29 | } 30 | 31 | const { query, params: args } = data; 32 | if (!query) { 33 | throw error(400, "Missing query"); 34 | } 35 | 36 | const statement = db.prepare(query).bind(...(args ?? [])); 37 | const result = await statement.all(); 38 | return json(result); 39 | }; 40 | -------------------------------------------------------------------------------- /src/routes/api/db/[database]/[table]/+server.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This route reports the metadata of a table. 3 | * It also allows deleting a table. 4 | */ 5 | import { dev } from "$app/environment"; 6 | import { error, json } from "@sveltejs/kit"; 7 | import type { RequestHandler } from "./$types"; 8 | 9 | export const GET: RequestHandler = async ({ params, locals, url, fetch }) => { 10 | if (dev) { 11 | const remote = new URL("https://d1-manager.pages.dev" + url.pathname + url.search); 12 | const res = await fetch(remote); 13 | return json(await res.json()); 14 | } 15 | 16 | const db = locals.db[params.database]; 17 | if (!db) { 18 | throw error(404, "Database not found"); 19 | } 20 | 21 | const { results } = await db.prepare(`SELECT COUNT(*) AS count FROM \`${params.table}\``).all<{ 22 | count: number; 23 | }>(); 24 | 25 | if (!results?.[0]) { 26 | throw error(404, "No data found"); 27 | } 28 | 29 | return json(results[0]); 30 | }; 31 | 32 | export const DELETE: RequestHandler = async ({ params, locals }) => { 33 | const db = locals.db[params.database]; 34 | if (!db) { 35 | throw error(404, "Database not found"); 36 | } 37 | 38 | const result = await db.prepare(`DROP TABLE \`${params.table}\``).run(); 39 | return json(result); 40 | }; 41 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | concurrency: ${{ github.workflow }}-${{ github.ref }} 9 | 10 | jobs: 11 | release: 12 | name: Release 13 | runs-on: ubuntu-latest 14 | timeout-minutes: 30 15 | permissions: 16 | contents: write 17 | issues: write 18 | pull-requests: write 19 | steps: 20 | - name: Checkout Repo 21 | uses: actions/checkout@v4 22 | 23 | - name: Setup PNPM 24 | uses: pnpm/action-setup@v4 25 | with: 26 | run_install: true 27 | 28 | - name: Build 29 | run: pnpm build 30 | 31 | - name: Create Release Pull Request or Publish to NPM 32 | id: changesets 33 | uses: changesets/action@v1 34 | with: 35 | publish: pnpm changeset publish 36 | version: pnpm changeset version 37 | title: Release Packages 38 | commit: bump versions 39 | env: 40 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 41 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 42 | -------------------------------------------------------------------------------- /src/lib/sql.ts: -------------------------------------------------------------------------------- 1 | import { Parser } from "node-sql-parser"; 2 | 3 | // Whitelist of clauses considered safe for read-only operations. 4 | export const READONLY_CLAUSES = ["SELECT", "PRAGMA", "EXPLAIN"]; 5 | 6 | // Clauses that are safe to modify but not dangerous. (not destructive) 7 | export const SAFE_MODIFY_CLAUSES = ["CREATE", "INSERT", "REPLACE", "UPSERT", "MERGE"]; 8 | 9 | export const SAFE_CLAUSES = [...READONLY_CLAUSES, ...SAFE_MODIFY_CLAUSES]; 10 | 11 | const parser = new Parser(); 12 | 13 | function get_statement_types(sql: string): string[] { 14 | try { 15 | const ast = parser.astify(sql); 16 | const stmts = Array.isArray(ast) ? ast : [ast]; 17 | return stmts.map((stmt) => stmt.type.toUpperCase()); 18 | } catch { 19 | const first = sql.trim().split(/\s+/)[0].toUpperCase(); 20 | return [first]; 21 | } 22 | } 23 | 24 | export function is_dangerous(sql: string): boolean { 25 | const types = get_statement_types(sql); 26 | return types.some((type) => !SAFE_CLAUSES.includes(type)); 27 | } 28 | 29 | export function is_modify(sql: string): boolean { 30 | return !is_readonly(sql); 31 | } 32 | 33 | export function is_readonly(sql: string): boolean { 34 | const types = get_statement_types(sql); 35 | return types.every((type) => READONLY_CLAUSES.includes(type)); 36 | } 37 | -------------------------------------------------------------------------------- /src/lib/storage.ts: -------------------------------------------------------------------------------- 1 | import { browser } from "$app/environment"; 2 | import type { Writable } from "svelte/store"; 3 | import { writable } from "svelte/store"; 4 | 5 | type Val = string | null | undefined; 6 | 7 | const cache = new Map>(); 8 | 9 | export function get( 10 | key: string, 11 | { default_value = undefined as Val, ttl = 0 } = {}, 12 | ): Writable { 13 | const cached = cache.get(key); 14 | if (cached) { 15 | console.log("[storage]", "cached", key, cached); 16 | return cached; 17 | } 18 | 19 | function update(value: Val) { 20 | if (browser) { 21 | localStorage.setItem(`storage:${key}`, JSON.stringify([Date.now(), value])); 22 | console.log("[storage]", "updated", key, value); 23 | } 24 | } 25 | 26 | if (browser) { 27 | const value = localStorage.getItem(`storage:${key}`); 28 | if (value) { 29 | const data: [number, Val] = JSON.parse(value); 30 | if (Date.now() - data[0] < ttl) { 31 | const store = writable(data[1]); 32 | console.log("[storage]", "restored", key, store); 33 | store.subscribe(update); 34 | cache.set(key, store); 35 | return store; 36 | } 37 | } 38 | } 39 | 40 | const store = writable(default_value); 41 | console.log("[storage]", "created", key, store); 42 | store.subscribe(update); 43 | cache.set(key, store); 44 | return store; 45 | } 46 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "workspace", 3 | "dockerComposeFile": "docker-compose.yml", 4 | "service": "workspace", 5 | "workspaceFolder": "/workspace", 6 | "customizations": { 7 | "vscode": { 8 | "settings": { 9 | "[svelte]": { 10 | "editor.formatOnSave": true 11 | }, 12 | "[typescript]": { 13 | "editor.formatOnSave": true 14 | }, 15 | "[css]": { 16 | "editor.formatOnSave": true 17 | }, 18 | "[json]": { 19 | "editor.formatOnSave": true 20 | }, 21 | "i18n-ally.localesPaths": ["locales"], 22 | "i18n-ally.keystyle": "nested", 23 | "i18n-ally.sortKeys": true, 24 | "i18n-ally.keepFulfilled": true, 25 | "i18n-ally.extract.parsers.html": { 26 | "attributes": ["text", "title", "alt", "placeholder", "label", "aria-label"], 27 | "ignoredTags": ["script", "style"], 28 | "inlineText": true 29 | }, 30 | "i18n-ally.extract.autoDetect": true, 31 | "i18n-ally.refactor.templates": [ 32 | { 33 | "source": "html-attribute", 34 | "templates": ["{$t('{key}'{args})}"], 35 | "include": ["src/**/*.{svelte,ts}", "index.html"] 36 | } 37 | ] 38 | }, 39 | "extensions": [ 40 | "dbaeumer.vscode-eslint", 41 | "esbenp.prettier-vscode", 42 | "GitHub.copilot", 43 | "GitHub.copilot-labs", 44 | "svelte.svelte-vscode", 45 | "bradlc.vscode-tailwindcss", 46 | "antfu.iconify", 47 | "lokalise.i18n-ally" 48 | ] 49 | } 50 | }, 51 | "onCreateCommand": "pnpm config set -g store-dir /workspace/node_modules/.pnpm-store", 52 | "postStartCommand": "pnpm i" 53 | } 54 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from "@eslint/js"; 2 | import svelte from "eslint-plugin-svelte"; 3 | import globals from "globals"; 4 | import ts from "typescript-eslint"; 5 | import svelteConfig from "./svelte.config.js"; 6 | 7 | export default ts.config( 8 | js.configs.recommended, 9 | ...ts.configs.recommended, 10 | ...svelte.configs.recommended, 11 | { 12 | languageOptions: { 13 | globals: { 14 | ...globals.browser, 15 | ...globals.node, 16 | }, 17 | }, 18 | }, 19 | { 20 | files: ["**/*.svelte", "**/*.svelte.ts", "**/*.svelte.js"], 21 | // See more details at: https://typescript-eslint.io/packages/parser/ 22 | languageOptions: { 23 | parserOptions: { 24 | projectService: true, 25 | extraFileExtensions: [".svelte"], // Add support for additional file extensions, such as .svelte 26 | parser: ts.parser, 27 | // Specify a parser for each language, if needed: 28 | // parser: { 29 | // ts: ts.parser, 30 | // js: espree, // Use espree for .js files (add: import espree from 'espree') 31 | // typescript: ts.parser 32 | // }, 33 | 34 | // We recommend importing and specifying svelte.config.js. 35 | // By doing so, some rules in eslint-plugin-svelte will automatically read the configuration and adjust their behavior accordingly. 36 | // While certain Svelte settings may be statically loaded from svelte.config.js even if you don’t specify it, 37 | // explicitly specifying it ensures better compatibility and functionality. 38 | svelteConfig, 39 | }, 40 | }, 41 | }, 42 | { 43 | ignores: ["node_modules", "build", ".svelte-kit", "package", "pnpm-lock.yaml"], 44 | }, 45 | { 46 | rules: { 47 | // Override or add rule settings here, such as: 48 | // 'svelte/rule-name': 'error' 49 | }, 50 | }, 51 | ); 52 | -------------------------------------------------------------------------------- /src/routes/db/[database]/[table]/SidePanel.svelte: -------------------------------------------------------------------------------- 1 | 36 | 37 | {#if show} 38 |
42 |
45 |
46 | {#each data.db as table, i} 47 |
48 | 49 | {table.name} 50 | 51 | 52 |
53 | {#each table.columns as column} 54 |

55 | {column.name} {column.type} 56 |

57 | {/each} 58 |
59 |
60 | 61 | {#if i !== data.db.length - 1} 62 |
63 | {/if} 64 | {/each} 65 |
66 |
67 |
68 | {/if} 69 | -------------------------------------------------------------------------------- /src/lib/server/ai/index.ts: -------------------------------------------------------------------------------- 1 | import { env } from "$env/dynamic/private"; 2 | import type { BaseChatMessage, BaseChatMessageRole, BaseChatParam } from "@ai-d/aid"; 3 | import { Aid } from "@ai-d/aid"; 4 | import debug from "debug"; 5 | 6 | debug.enable("aid*"); 7 | 8 | const log = debug("assistant"); 9 | log.enabled = true; 10 | 11 | const OPENAI_MODEL = env.OPENAI_MODEL || "gpt-4.1-mini"; 12 | const CFAI_MODEL = env.CFAI_MODEL || "@cf/mistral/mistral-7b-instruct-v0.1"; 13 | 14 | /** 15 | * Checks the availability of AI models. 16 | */ 17 | export function available(): string | null { 18 | if (env.OPENAI_API_KEY) { 19 | return `OpenAI (${OPENAI_MODEL})`; 20 | } 21 | 22 | if (env.AI) { 23 | return `Cloudflare (${CFAI_MODEL})`; 24 | } 25 | 26 | return null; 27 | } 28 | 29 | export type AiBackend = 30 | | Aid< 31 | string, 32 | BaseChatParam, 33 | BaseChatMessage[], 34 | BaseChatMessage[] 35 | > 36 | | undefined; 37 | 38 | export async function select_backend(): Promise { 39 | if (env.OPENAI_API_KEY) { 40 | log("using OpenAI backend"); 41 | const { OpenAI } = await import("openai"); 42 | return Aid.from( 43 | new OpenAI({ 44 | baseURL: env.OPENAI_API_URL, 45 | apiKey: env.OPENAI_API_KEY, 46 | }), 47 | { model: OPENAI_MODEL }, 48 | ); 49 | } 50 | 51 | if (env.AI) { 52 | log("using Cloudflare backend"); 53 | return Aid.chat(async (messages) => { 54 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 55 | const ai = env.AI as any; 56 | const { response } = await ai.run(CFAI_MODEL, { 57 | messages, 58 | }); 59 | 60 | const res: string = response; 61 | const match = res.match(/{\s*"sql":\s*".*?"\s*}/); 62 | if (match) { 63 | return match[0]; 64 | } 65 | 66 | return res; 67 | }); 68 | } 69 | 70 | log("no backend"); 71 | return undefined; 72 | } 73 | -------------------------------------------------------------------------------- /src/routes/api/db/[database]/assistant/+server.ts: -------------------------------------------------------------------------------- 1 | import { select_backend } from "$lib/server/ai"; 2 | import { json } from "@sveltejs/kit"; 3 | import { z } from "zod"; 4 | import type { RequestHandler } from "./$types"; 5 | 6 | export const POST: RequestHandler = async ({ params, request, fetch, platform }) => { 7 | const aid = await select_backend(); 8 | if (!aid) { 9 | return json({ 10 | error: "no backend", 11 | }); 12 | } 13 | 14 | const tables_p = fetch("/api/db/" + params.database).then((r) => 15 | r.json< 16 | { 17 | name: string; 18 | columns: { name: string; type: string }[]; 19 | count: number; 20 | }[] 21 | >(), 22 | ); 23 | 24 | const data = await request.json<{ 25 | q: string; 26 | t?: string; 27 | }>(); 28 | 29 | const tables = await tables_p; 30 | 31 | // if t is provided, swap the table t with the first table 32 | if (data.t) { 33 | const i = tables.findIndex(({ name }) => name === data.t); 34 | if (i > 0) { 35 | [tables[0], tables[i]] = [tables[i], tables[0]]; 36 | } 37 | } 38 | 39 | const question = data.q || "show first 10 records in the table"; 40 | 41 | const system = `SQLite tables, with their properties: 42 | 43 | ${tables 44 | .map( 45 | ({ name, columns }) => 46 | `${name} (${columns.map(({ name, type }) => `${name}: ${type}`).join(", ")})`, 47 | ) 48 | .join("\n")} 49 | 50 | write a raw SQL, without comment`; 51 | 52 | const get_sql = aid.task( 53 | system, 54 | z.object({ 55 | sql: z.string().describe("SQL query"), 56 | }), 57 | { 58 | examples: [ 59 | [ 60 | { text: "show first 10 records in the table" }, 61 | { sql: `SELECT * FROM \`${tables[0].name}\` LIMIT 10` }, 62 | ], 63 | [ 64 | { text: "show columns in the table" }, 65 | { 66 | sql: `SELECT name, type FROM pragma_table_info('${tables[0].name}')`, 67 | }, 68 | ], 69 | ], 70 | }, 71 | ); 72 | 73 | const { result } = await get_sql(question); 74 | 75 | return json({ 76 | sql: result.sql, 77 | }); 78 | }; 79 | -------------------------------------------------------------------------------- /src/routes/+layout.svelte: -------------------------------------------------------------------------------- 1 | 47 | 48 |
49 | 70 |
71 | 72 |
73 |
74 | -------------------------------------------------------------------------------- /src/routes/db/[database]/+layout.svelte: -------------------------------------------------------------------------------- 1 | 45 | 46 |
47 | 75 |
76 | {#key $page.params.table} 77 | 78 | {/key} 79 |
80 |
81 | -------------------------------------------------------------------------------- /src/routes/+page.svelte: -------------------------------------------------------------------------------- 1 | 50 | 51 | 52 | {$t("d1-manager.name")} 53 | 54 | 55 | 56 |
57 |
58 |

{$t("select-database-from-above")}

59 |

60 | {$t("see-github")} 63 |

64 | 65 |

66 | {$t("d1-manager.description")} 67 |

68 | 69 |
70 | 71 |
72 | 75 | 80 |
81 | 82 |
83 | 86 | 91 |
92 |
93 |
94 | -------------------------------------------------------------------------------- /locales/zh-CN.json: -------------------------------------------------------------------------------- 1 | { 2 | "col-default": "预设值", 3 | "col-name": "栏位", 4 | "col-type": "型别", 5 | "d1-manager": { 6 | "description": "D1 Manager 是 Cloudflare D1 的 Web UI 和 API,Cloudflare D1 是一个无服务器 SQL 数据库。\n它提供了一个用户友好的界面来管理数据库、表格和记录,以及一个用于以编程方式执行操作的 API。 \nD1 Manager 简化了数据库管理,使用户能够专注于他们的数据。", 7 | "name": "D1 Manager", 8 | "short": "用于管理 Cloudflare D1 中数据的 Web UI。" 9 | }, 10 | "d1-manager-manage-db": "使用 D1 Manager 管理 {db} 中的资料", 11 | "download": "下载", 12 | "execute-sql-query-in-database": "在数据库 {db} 中执行 SQL 查询", 13 | "import": "汇入", 14 | "lang": { 15 | "en": "英语", 16 | "es-ES": "西班牙语", 17 | "es-MX": "墨西哥西班牙语", 18 | "ja": "日语", 19 | "zh-CN": "简体中文", 20 | "zh-TW": "繁体中文" 21 | }, 22 | "language": "语言", 23 | "n-ms": "花费 {n} 毫秒", 24 | "n-table-in-db": "{db} 中的 {n} 个表", 25 | "plugin": { 26 | "add-record": { 27 | "add": "添加记录", 28 | "column": "栏位", 29 | "error": "错误", 30 | "integer-is-required": "整数是必需的", 31 | "n-ms": "{n} 毫秒", 32 | "name": "添加记录", 33 | "real-is-required": "需要实数", 34 | "success": "成功", 35 | "value": "值" 36 | }, 37 | "csv": { 38 | "export": "导出", 39 | "export-csv": "导出 CSV", 40 | "import": "导入", 41 | "import-csv": "导入 CSV", 42 | "invalid-column-name-key": "无效的列名:{key}", 43 | "n-ms-m-changes": "花费 {n} 毫秒,{m} 笔资料更动", 44 | "name": "CSV 导入和导出", 45 | "no-result": "没有结果", 46 | "select-a-csv-file": "选择 CSV 文件" 47 | }, 48 | "run-query": { 49 | "export": "导出", 50 | "n-ms-m-changes": "{n} 毫秒,读取 {rr} 行,写入 {rw} 行", 51 | "name": "运行 SQL 查询", 52 | "no-result": "没有结果", 53 | "no-results": "没有结果", 54 | "run": "运行", 55 | "unknown-error": "未知错误" 56 | }, 57 | "semantic-query": { 58 | "autorun-on-read-only-queries": "只读查询将自动运行。", 59 | "export": "导出", 60 | "n-ms-m-changes": "{n} 毫秒,读取 {rr} 行,写入 {rw} 行", 61 | "name": "语义查询", 62 | "no-result": "没有结果", 63 | "no-results": "没有结果", 64 | "requires-openai_api_key": "此插件需要设置 OPENAI_API_KEY 环境变量。", 65 | "run": "运行", 66 | "suggest": "编写语句", 67 | "unknown-error": "未知错误" 68 | }, 69 | "table-browser": { 70 | "click-to-sort-by": "点击按 {col} 排序", 71 | "invalid-rowid": "行号无效", 72 | "name": "表格浏览器", 73 | "next": "下一页", 74 | "no-record": "无记录", 75 | "no-result": "没有结果", 76 | "no-results": "无匹配结果", 77 | "prev": "上一页", 78 | "showing": "显示 {from} 至 {to} 笔资料", 79 | "table-is-locked": "表格已锁定,解锁以对其进行编辑", 80 | "table-is-locked-click-to-unlock": "已锁定,点击启用编辑", 81 | "table-is-unlocked-click-to-lock": "已解锁,点击关闭编辑", 82 | "unknown-error": "未知错误" 83 | } 84 | }, 85 | "rows": "行数", 86 | "see-github": "在 GitHub 上查看使用说明", 87 | "select-database": "选择数据库", 88 | "select-database-from-above": "从右上角选择一个数据库", 89 | "show-first-10-records": "显示 {table} 中的前 10 条记录", 90 | "suggestion-will-appear-here": "生成的 SQL 会出现在这里", 91 | "theme": "主题" 92 | } 93 | -------------------------------------------------------------------------------- /locales/zh-TW.json: -------------------------------------------------------------------------------- 1 | { 2 | "col-default": "預設值", 3 | "col-name": "欄位", 4 | "col-type": "型別", 5 | "d1-manager": { 6 | "description": "D1 Manager 是一個為 Cloudflare D1(無伺服器 SQL 資料庫)設計的網頁界面和 API。它提供了一個易於使用的界面,使用者可以輕鬆地管理數據庫、表格和資料。API 提供了靈活性和與現有工作流程的整合。D1 Manager 讓使用者專注於他們的數據。", 7 | "name": "D1 Manager", 8 | "short": "輕鬆的管理 Cloudflare D1 裡面的資料" 9 | }, 10 | "d1-manager-manage-db": "使用 D1 Manager 管理 {db} 中的資料", 11 | "download": "下載", 12 | "execute-sql-query-in-database": "在資料庫 {db} 中執行 SQL 查詢", 13 | "import": "匯入", 14 | "lang": { 15 | "en": "英語", 16 | "es-ES": "西班牙語", 17 | "es-MX": "墨西哥西班牙語", 18 | "ja": "日語", 19 | "zh-CN": "簡體中文", 20 | "zh-TW": "正體中文" 21 | }, 22 | "language": "語言", 23 | "n-ms": "花費 {n} 毫秒", 24 | "n-table-in-db": "{db} 中有 {n} 張表", 25 | "plugin": { 26 | "add-record": { 27 | "add": "新增資料", 28 | "column": "欄位", 29 | "error": "發生錯誤", 30 | "integer-is-required": "必須是整數", 31 | "n-ms": "執行花費 {n} 毫秒", 32 | "name": "新增資料", 33 | "real-is-required": "必須是實數", 34 | "success": "新增成功", 35 | "value": "值" 36 | }, 37 | "csv": { 38 | "export": "匯出", 39 | "export-csv": "匯出 CSV", 40 | "import": "匯入", 41 | "import-csv": "匯入 CSV", 42 | "invalid-column-name-key": "無效的欄位:{key}", 43 | "n-ms-m-changes": "花費 {n} 毫秒,{m} 筆資料更動", 44 | "name": "CSV 匯入與匯出", 45 | "no-result": "沒有結果", 46 | "select-a-csv-file": "選擇 CSV 文件" 47 | }, 48 | "run-query": { 49 | "export": "匯出", 50 | "n-ms-m-changes": "{n} 毫秒,讀取 {rr} 列,寫入 {rw} 列", 51 | "name": "執行 SQL 查詢", 52 | "no-result": "回應中沒有資料", 53 | "no-results": "沒有符合的結果", 54 | "run": "執行", 55 | "unknown-error": "未知的錯誤" 56 | }, 57 | "semantic-query": { 58 | "autorun-on-read-only-queries": "只讀查詢將自動執行。", 59 | "export": "匯出", 60 | "n-ms-m-changes": "{n} 毫秒,讀取 {rr} 列,寫入 {rw} 列", 61 | "name": "語意查詢", 62 | "no-result": "回應中沒有資料", 63 | "no-results": "沒有符合的結果", 64 | "requires-openai_api_key": "提醒:此工具需要設置 OPENAI_API_KEY 環境變數。", 65 | "run": "執行", 66 | "suggest": "編寫語句", 67 | "unknown-error": "未知的錯誤" 68 | }, 69 | "table-browser": { 70 | "click-to-sort-by": "點擊以 {col} 排序", 71 | "invalid-rowid": "無效的定位 ID", 72 | "name": "表格瀏覽器", 73 | "next": "下一頁", 74 | "no-record": "無資料", 75 | "no-result": "無資料", 76 | "no-results": "沒有符合的結果", 77 | "prev": "上一頁", 78 | "showing": "顯示 {from} 至 {to} 筆資料", 79 | "table-is-locked": "表格已鎖定,解鎖以對其進行編輯", 80 | "table-is-locked-click-to-unlock": "已鎖定,點擊啟用編輯", 81 | "table-is-unlocked-click-to-lock": "已解鎖,點擊關閉編輯", 82 | "unknown-error": "未知錯誤" 83 | } 84 | }, 85 | "rows": "資料筆數", 86 | "see-github": "在 GitHub 上查看使用說明", 87 | "select-database": "選擇資料庫", 88 | "select-database-from-above": "請從右上角選擇一個資料庫", 89 | "show-first-10-records": "顯示 {table} 裡前十筆資料", 90 | "suggestion-will-appear-here": "產生的 SQL 語句將顯示在此", 91 | "theme": "主題" 92 | } 93 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # D1 Manager 2 | 3 | D1 Manager is a web UI and API for Cloudflare D1, a serverless SQL database. It provides a user-friendly interface for managing databases, tables, and records, as well as an API for performing operations programmatically. D1 Manager simplifies database management, enabling users to focus on their data. 4 | 5 | [![semantic-query-demo](./images/semantic-query-demo.gif)](https://storage.jacoblin.cool/semantic-query-demo.mp4) 6 | 7 | ## Features 8 | 9 | - [x] Multiple D1 Databases 10 | - [x] List all tables in a database 11 | - [x] Show table schema 12 | - [x] Run SQL queries 13 | - [x] Run Semantic Queries (OpenAI API or Cloudflare AI Worker) 14 | - [x] Edit table data through UI 15 | - [x] I18n support (English, Chinese, Spanish, Japanese) [add more](./locales/) ([Online Editor](https://fink.inlang.com/github.com/JacobLinCool/d1-manager)) 16 | - [x] API support (see [routes/api](./src/routes/api/) for details) 17 | 18 | ## Setup 19 | 20 | 1. Fork this repo 21 | 2. Setup a **Cloudflare Pages** with the forked repo 22 | - Select the **SveltKit** framework preset. 23 | - Build command: `npm run build` 24 | - Build output directory: `.svelte-kit/cloudflare` 25 | 3. Use **Cloudflare Access** to protect the your site 26 | - The default access rules only restrict access to preview pages, so make sure to add other urls you want protected. 27 | 4. **Bind databases** to `DB_*` environment variables 28 | 29 | ![bind-d1](./images/bind-d1.png) 30 | 31 | > Note: You can bind multiple databases to the manager. In theis example, `DB` will be `default` in the UI, and `DB_test` will be `test`. 32 | 33 | ### Environment Variables 34 | 35 | Some plugins (e.g. Semantic Query) require additional environment variables to be set. 36 | 37 | ![set-env-var](./images/set-env-var.png) 38 | 39 | Also, there are some configuration options that can be set through environment variables. 40 | 41 | - `SHOW_INTERNAL_TABLES`: Show internal tables (`splite_*` and `d1_*`) in the UI. 42 | 43 | #### Semantic Query 44 | 45 | You can use OpenAI API or Cloudflare AI Worker to run Semantic Query. 46 | 47 | OpenAI API: 48 | 49 | - `OPENAI_API_KEY`: OpenAI API key for Semantic Query. 50 | - `OPENAI_API_URL`: You may use this with Cloudflare AI Gateway to proxy requests to OpenAI API. 51 | - `OPENAI_MODEL`: OpenAI API model for Semantic Query. Default to `gpt-4.1-mini`. 52 | 53 | Cloudflare AI Worker: 54 | 55 | - `AI`: Bind a Cloudflare AI Worker to this variable. 56 | - `CFAI_MODEL`: Cloudflare AI Worker model for Semantic Query. Default to `@cf/mistral/mistral-7b-instruct-v0.1`. 57 | 58 | ## Screenshots 59 | 60 | ![tables](./images/tables.png) 61 | 62 | ![run-query](./images/run-query.png) 63 | 64 | ![table-browser](./images/table-browser.png) 65 | 66 | ![add-record](./images/add-record.png) 67 | 68 | ![semantic-query](./images/semantic-query.png) 69 | 70 | > Semantic Query uses OpenAI GPT-4.1 Mini to translate natural language queries into SQL. 71 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "d1-manager", 3 | "description": "D1 Manager is a web UI and API for Cloudflare D1, a serverless SQL database. It provides a user-friendly interface for managing databases, tables, and records, as well as an API for performing operations programmatically. D1 Manager simplifies database management, enabling users to focus on their data.", 4 | "version": "0.0.0", 5 | "type": "module", 6 | "files": [ 7 | ".svelte-kit/cloudflare" 8 | ], 9 | "scripts": { 10 | "prepare": "husky", 11 | "dev": "vite dev", 12 | "build": "vite build", 13 | "preview": "vite preview", 14 | "test": "playwright test", 15 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 16 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", 17 | "test:unit": "vitest", 18 | "lint": "prettier --ignore-path .gitignore --check . && eslint .", 19 | "format": "prettier --ignore-path .gitignore --write .", 20 | "upload": "pnpm build && wrangler pages deploy --project-name d1-manager .svelte-kit/cloudflare", 21 | "tail": "wrangler pages deployment tail --project-name d1-manager" 22 | }, 23 | "devDependencies": { 24 | "@ai-d/aid": "^0.1.5", 25 | "@changesets/changelog-github": "^0.5.1", 26 | "@changesets/cli": "^2.29.2", 27 | "@cloudflare/workers-types": "^4.20250430.0", 28 | "@eslint/js": "^9.25.1", 29 | "@iconify/svelte": "^5.0.0", 30 | "@playwright/test": "^1.52.0", 31 | "@sveltejs/adapter-cloudflare": "^7.0.2", 32 | "@sveltejs/kit": "^2.20.8", 33 | "@sveltejs/vite-plugin-svelte": "^5.0.3", 34 | "@tailwindcss/typography": "^0.5.16", 35 | "@tailwindcss/vite": "^4.1.5", 36 | "@types/debug": "^4.1.12", 37 | "@types/sql.js": "^1.4.9", 38 | "csv-parse": "^5.6.0", 39 | "csv-stringify": "^6.5.2", 40 | "daisyui": "^5.0.34", 41 | "debug": "^4.4.0", 42 | "eslint": "^9.25.1", 43 | "eslint-config-prettier": "^10.1.2", 44 | "eslint-plugin-svelte": "^3.5.1", 45 | "globals": "^16.0.0", 46 | "husky": "^9.1.7", 47 | "lint-staged": "^15.5.1", 48 | "node-sql-parser": "^5.3.8", 49 | "openai": "^4.96.2", 50 | "prettier": "^3.5.3", 51 | "prettier-plugin-organize-imports": "^4.1.0", 52 | "prettier-plugin-svelte": "^3.3.3", 53 | "prettier-plugin-tailwindcss": "^0.6.11", 54 | "sql.js": "^1.13.0", 55 | "svelte": "^5.28.2", 56 | "svelte-check": "^4.1.6", 57 | "svelte-i18n": "^4.0.1", 58 | "tailwindcss": "^4.1.5", 59 | "theme-change": "2.5.0", 60 | "tslib": "^2.8.1", 61 | "tsup": "^8.4.0", 62 | "typescript": "^5.8.3", 63 | "typescript-eslint": "^8.31.1", 64 | "vite": "^6.3.4", 65 | "vitest": "^3.1.2", 66 | "wrangler": "^4.13.2", 67 | "zod": "^3.24.3" 68 | }, 69 | "lint-staged": { 70 | "*.{ts,js,json,yaml,yml,svelte,html,css}": [ 71 | "prettier --write" 72 | ] 73 | }, 74 | "repository": { 75 | "type": "git", 76 | "url": "https://github.com/JacobLinCool/d1-manager.git" 77 | }, 78 | "homepage": "https://github.com/JacobLinCool/d1-manager", 79 | "bugs": { 80 | "url": "https://github.com/JacobLinCool/d1-manager/issues" 81 | }, 82 | "packageManager": "pnpm@10.10.0" 83 | } 84 | -------------------------------------------------------------------------------- /locales/ja.json: -------------------------------------------------------------------------------- 1 | { 2 | "col-default": "デフォルト値", 3 | "col-name": "列名", 4 | "col-type": "型", 5 | "d1-manager": { 6 | "description": "D1 Managerは、Cloudflare D1(サーバーレスSQLデータベース)用のウェブインターフェースとAPIです。簡単にデータベース、テーブル、データを管理できるユーザーフレンドリーなインターフェースを提供します。APIは柔軟性と既存のワークフローとの統合を提供します。D1 Managerは、ユーザーがデータに集中できるようにします。", 7 | "name": "D1 Manager", 8 | "short": "Cloudflare D1 内のデータを簡単に管理" 9 | }, 10 | "d1-manager-manage-db": "D1 Manager を使用して {db} 内のデータを管理", 11 | "download": "ダウンロード", 12 | "execute-sql-query-in-database": "データベース {db} で SQL クエリを実行します", 13 | "import": "インポート", 14 | "lang": { 15 | "en": "英語", 16 | "es-ES": "スペイン語", 17 | "es-MX": "メキシコスペイン語", 18 | "ja": "日本語", 19 | "zh-CN": "簡体字中国語", 20 | "zh-TW": "繁体字中国語" 21 | }, 22 | "language": "言語", 23 | "n-ms": "{n} ミリ秒かかりました", 24 | "n-table-in-db": "{db} には {n} つのテーブルがあります", 25 | "plugin": { 26 | "add-record": { 27 | "add": "データを追加", 28 | "column": "列", 29 | "error": "エラーが発生しました", 30 | "integer-is-required": "整数が必要です", 31 | "n-ms": "実行には {n} ミリ秒かかりました", 32 | "name": "データ追加", 33 | "real-is-required": "実数が必要です", 34 | "success": "追加成功", 35 | "value": "値" 36 | }, 37 | "csv": { 38 | "export": "エクスポート", 39 | "export-csv": "CSV をエクスポート", 40 | "import": "インポート", 41 | "import-csv": "CSVをインポート", 42 | "invalid-column-name-key": "無効な列:{key}", 43 | "n-ms-m-changes": "{n} ミリ秒で、{m} 件のデータ変更", 44 | "name": "CSV のインポートとエクスポート", 45 | "no-result": "結果がありません", 46 | "select-a-csv-file": "CSV ファイルを選択" 47 | }, 48 | "run-query": { 49 | "export": "エクスポート", 50 | "n-ms-m-changes": "{n} ミリ秒、{rr} 行読み取り、{rw} 行書き込み", 51 | "name": "SQL クエリの実行", 52 | "no-result": "結果にデータがありません", 53 | "no-results": "一致する結果はありません", 54 | "run": "実行", 55 | "unknown-error": "不明なエラー" 56 | }, 57 | "semantic-query": { 58 | "autorun-on-read-only-queries": "読み取り専用のクエリは自動的に実行されます。", 59 | "export": "エクスポート", 60 | "n-ms-m-changes": "{n} ミリ秒、{rr} 行読み取り、{rw} 行書き込み", 61 | "name": "セマンティッククエリ", 62 | "no-result": "結果にデータがありません", 63 | "no-results": "一致する結果はありません", 64 | "requires-openai_api_key": "注意:このツールは OPENAI_API_KEY 環境変数の設定が必要です。", 65 | "run": "実行", 66 | "suggest": "文を作成", 67 | "unknown-error": "不明なエラー" 68 | }, 69 | "table-browser": { 70 | "click-to-sort-by": "{col} でソートするにはクリック", 71 | "invalid-rowid": "無効な行 ID", 72 | "name": "テーブルブラウザ", 73 | "next": "次のページ", 74 | "no-record": "データがありません", 75 | "no-result": "データがありません", 76 | "no-results": "一致する結果はありません", 77 | "prev": "前のページ", 78 | "showing": "{from} から {to} までのデータを表示", 79 | "table-is-locked": "テーブルはロックされています。編集するにはロックを解除してください", 80 | "table-is-locked-click-to-unlock": "ロックされています。編集を有効にするにはクリックしてください", 81 | "table-is-unlocked-click-to-lock": "ロックが解除されています。編集を無効にするにはクリックしてください", 82 | "unknown-error": "不明なエラー" 83 | } 84 | }, 85 | "rows": "データの数", 86 | "see-github": "GitHub で使用説明を見る", 87 | "select-database": "データベースを選択", 88 | "select-database-from-above": "右上からデータベースを選択してください", 89 | "show-first-10-records": "{table} の最初の 10 レコードを表示", 90 | "suggestion-will-appear-here": "生成された SQL 文がここに表示されます", 91 | "theme": "テーマ" 92 | } 93 | -------------------------------------------------------------------------------- /src/routes/db/[database]/[table]/+page.svelte: -------------------------------------------------------------------------------- 1 | 37 | 38 | 39 | {$page.params.table} @ {$page.params.database} | {$t("d1-manager.name")} 40 | 44 | 45 | 46 |
47 |
48 |
49 |
50 |

{meta.name}

51 |
52 | 53 |
54 |
55 | 56 |
57 |
58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | {#each meta.columns as column} 68 | 69 | 70 | 71 | 72 | 73 | {/each} 74 | 75 |
{$t("col-name")}{$t("col-type")}{$t("col-default")}
{column.name}{column.type}{column.dflt_value}
76 |
77 |
78 | 79 |
80 | 81 | 90 | 91 | {#if PluginComponent} 92 | 98 | {/if} 99 |
100 |
101 |
102 | 103 | 104 | -------------------------------------------------------------------------------- /src/routes/api/db/[database]/+server.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This route reports the tables in a database. 3 | * It also allows creating new tables. 4 | */ 5 | import { dev } from "$app/environment"; 6 | import { extend } from "$lib/log"; 7 | import { error, json } from "@sveltejs/kit"; 8 | import type { RequestHandler } from "./$types"; 9 | 10 | const log = extend("api/db/+server"); 11 | 12 | export const GET: RequestHandler = async ({ params, locals, url, fetch, platform }) => { 13 | if (dev) { 14 | const remote = new URL("https://d1-manager.pages.dev" + url.pathname + url.search); 15 | return fetch(remote); 16 | } 17 | 18 | const db = locals.db[params.database]; 19 | if (!db) { 20 | throw error(404, "Database not found"); 21 | } 22 | 23 | const tables = await db.prepare("PRAGMA table_list").all<{ 24 | schema: string; 25 | name: string; 26 | type: string; 27 | ncol: number; 28 | wr: number; 29 | strict: number; 30 | }>(); 31 | 32 | if (!tables.results) { 33 | throw error(404, "No tables found"); 34 | } 35 | const results = tables.results.filter(({ name }) => { 36 | // Avoid all Cloudflare internal table names. 37 | if (name.startsWith("_cf_")) { 38 | return false; 39 | } 40 | if (name.startsWith("sqlite_") || name.startsWith("d1_")) { 41 | return !!platform?.env.SHOW_INTERNAL_TABLES; 42 | } 43 | return true; 44 | }); 45 | 46 | const _columns = db.batch( 47 | results.map(({ name }) => { 48 | return db.prepare(`PRAGMA table_info(\`${name}\`)`); 49 | }), 50 | ); 51 | 52 | const _count = db.batch<{ c: number }>( 53 | results.map(({ name }) => { 54 | return db.prepare(`SELECT COUNT(*) AS c FROM \`${name}\``); 55 | }), 56 | ); 57 | 58 | const columns = (await _columns).map( 59 | ({ results }) => results as { name: string; type: string }[], 60 | ); 61 | const count = (await _count).map(({ results }) => results?.[0].c); 62 | 63 | const response = results 64 | .map(({ name }, i) => ({ 65 | name, 66 | columns: columns[i], 67 | count: count[i], 68 | })) 69 | .sort(({ name: a }, { name: b }) => { 70 | if (a.startsWith("sqlite_") && !b.startsWith("sqlite_")) { 71 | return 1; 72 | } else if (!a.startsWith("sqlite_") && b.startsWith("sqlite_")) { 73 | return -1; 74 | } 75 | 76 | if (a.startsWith("d1_") && !b.startsWith("d1_")) { 77 | return 1; 78 | } else if (!a.startsWith("d1_") && b.startsWith("d1_")) { 79 | return -1; 80 | } 81 | 82 | return a.replace(/^(d1|sqlite)_/, "").localeCompare(b.replace(/^(d1|sqlite)_/, "")); 83 | }); 84 | 85 | log(response); 86 | return json(response); 87 | }; 88 | 89 | export const POST: RequestHandler = async ({ request, params, locals }) => { 90 | const db = locals.db[params.database]; 91 | if (!db) { 92 | throw error(404, "Database not found"); 93 | } 94 | 95 | const { name, columns } = await request.json<{ 96 | name: string; 97 | columns: Record; 98 | }>(); 99 | if (!name) { 100 | throw error(400, "Missing name"); 101 | } 102 | 103 | const result = await db 104 | .prepare( 105 | `CREATE TABLE ${name} (${Object.entries(columns) 106 | .map(([name, type]) => `${name} ${type}`) 107 | .join(", ")}) STRICT`, 108 | ) 109 | .run(); 110 | return json(result); 111 | }; 112 | -------------------------------------------------------------------------------- /locales/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "col-default": "Default", 3 | "col-name": "Name", 4 | "col-type": "Type", 5 | "d1-manager": { 6 | "description": "D1 Manager is a web UI and API for Cloudflare D1, a serverless SQL database. It provides a user-friendly interface for managing databases, tables, and records, as well as an API for performing operations programmatically. D1 Manager simplifies database management, enabling users to focus on their data.", 7 | "name": "D1 Manager", 8 | "short": "A Web UI to manage data in Cloudflare D1." 9 | }, 10 | "d1-manager-manage-db": "Use D1 Manager to mange data in {db}", 11 | "download": "Download", 12 | "execute-sql-query-in-database": "Execute SQL query in database {db}", 13 | "import": "Import", 14 | "lang": { 15 | "en": "English", 16 | "es-ES": "Spanish from Spain", 17 | "es-MX": "Mexican Spanish", 18 | "ja": "Japanese", 19 | "zh-CN": "Simplified Chinese", 20 | "zh-TW": "Traditional Chinese" 21 | }, 22 | "language": "Language", 23 | "n-ms": "{n} ms", 24 | "n-table-in-db": "{n} tables in {db}", 25 | "plugin": { 26 | "add-record": { 27 | "add": "Add Record", 28 | "column": "Column", 29 | "error": "Error", 30 | "integer-is-required": "INTEGER is required", 31 | "n-ms": "{n} ms", 32 | "name": "Add Record", 33 | "real-is-required": "REAL number is required", 34 | "success": "Success", 35 | "value": "Value" 36 | }, 37 | "csv": { 38 | "export": "Export", 39 | "export-csv": "Export CSV", 40 | "import": "Import", 41 | "import-csv": "Import CSV", 42 | "invalid-column-name-key": "Invalid column name: {key}", 43 | "n-ms-m-changes": "{n} ms, {m} changes", 44 | "name": "CSV Importer and Exporter", 45 | "no-result": "No Result", 46 | "select-a-csv-file": "Select a CSV file" 47 | }, 48 | "run-query": { 49 | "export": "Export", 50 | "n-ms-m-changes": "{n} ms, {rr} rows read, {rw} rows written", 51 | "name": "Run SQL Query", 52 | "no-result": "No result", 53 | "no-results": "No Results", 54 | "run": "Run", 55 | "unknown-error": "Unknown error" 56 | }, 57 | "semantic-query": { 58 | "autorun-on-read-only-queries": "Readonly query will be run automatically.", 59 | "export": "Export", 60 | "n-ms-m-changes": "{n} ms, {rr} rows read, {rw} rows written", 61 | "name": "Semantic Query", 62 | "no-result": "No result", 63 | "no-results": "No Results", 64 | "requires-openai_api_key": "This plugin requires the OPENAI_API_KEY environment variable to be set.", 65 | "run": "Run", 66 | "suggest": "Get SQL", 67 | "unknown-error": "Unknown error" 68 | }, 69 | "table-browser": { 70 | "click-to-sort-by": "Click to sort by {col}", 71 | "invalid-rowid": "Invalid rowid", 72 | "name": "Table Browser", 73 | "next": "Next", 74 | "no-record": "No Record", 75 | "no-result": "No Result", 76 | "no-results": "No Matched Result", 77 | "prev": "Previous", 78 | "showing": "{from} ~ {to}", 79 | "table-is-locked": "Table is locked, unlock to edit it", 80 | "table-is-locked-click-to-unlock": "Table is locked, click to unlock", 81 | "table-is-unlocked-click-to-lock": "Table is unlocked, click to lock", 82 | "unknown-error": "Unknown Error" 83 | } 84 | }, 85 | "rows": "Rows", 86 | "see-github": "Checkout the usage and guide on GitHub", 87 | "select-database": "Select Database", 88 | "select-database-from-above": "Select a database from the top-right corner", 89 | "show-first-10-records": "show first 10 records in {table}", 90 | "suggestion-will-appear-here": "Generated SQL will appear here", 91 | "theme": "Theme" 92 | } 93 | -------------------------------------------------------------------------------- /src/lib/plugin/RunQuery.svelte: -------------------------------------------------------------------------------- 1 | 59 | 60 |
61 | 67 |
68 | 69 | 72 | 73 | {#if result} 74 |
75 | 76 | {#if result?.results?.length} 77 |
78 | 79 | 80 | 81 | {#each Object.keys(result.results[0]) as key} 82 | 83 | {/each} 84 | 85 | 86 | 87 | {#each result.results as row} 88 | 89 | {#each Object.values(row) as value} 90 | 93 | {/each} 94 | 95 | {/each} 96 | 97 |
{key}
{value}
98 |
99 | {:else} 100 |

101 | {$t("plugin.run-query.no-results")} 102 |

103 | {/if} 104 | 105 |
106 |

107 | {$t("plugin.run-query.n-ms-m-changes", { 108 | values: { 109 | n: result.meta.duration.toFixed(2), 110 | rr: result.meta.rows_read ?? "x", 111 | rw: result.meta.rows_written ?? result.meta.changes, 112 | }, 113 | })} 114 |

115 | {#if result?.results?.length} 116 | 122 | {/if} 123 |
124 | {/if} 125 | 126 | {#if error} 127 |
128 | 129 |
130 |
{error}
131 |
132 | {/if} 133 | 134 |
135 | -------------------------------------------------------------------------------- /locales/pt.json: -------------------------------------------------------------------------------- 1 | { 2 | "col-default": "Padrão", 3 | "col-name": "Nome", 4 | "col-type": "Tipo", 5 | "d1-manager": { 6 | "description": "O D1 Manager é uma interface web e API para o Cloudflare D1, uma base de dados SQL sem servidor. Oferece uma interface amigável para gerir bases de dados, tabelas e registos, bem como uma API para realizar operações de forma programática. O D1 Manager simplifica a gestão de bases de dados, permitindo aos utilizadores concentrarem-se nos seus dados.", 7 | "name": "D1 Manager", 8 | "short": "Uma interface web para gerir dados no Cloudflare D1." 9 | }, 10 | "d1-manager-manage-db": "Use o D1 Manager para gerir dados em {db}", 11 | "download": "Descarregar", 12 | "execute-sql-query-in-database": "Executar consulta SQL na base de dados {db}", 13 | "import": "Importar", 14 | "lang": { 15 | "en": "Inglês", 16 | "es-ES": "Espanhol de Espanha", 17 | "es-MX": "Espanhol do México", 18 | "ja": "Japonês", 19 | "zh-CN": "Chinês Simplificado", 20 | "zh-TW": "Chinês Tradicional" 21 | }, 22 | "language": "Idioma", 23 | "n-ms": "{n} ms", 24 | "n-table-in-db": "{n} tabelas em {db}", 25 | "plugin": { 26 | "add-record": { 27 | "add": "Adicionar Registo", 28 | "column": "Coluna", 29 | "error": "Erro", 30 | "integer-is-required": "É necessário um número INTEIRO", 31 | "n-ms": "{n} ms", 32 | "name": "Adicionar Registo", 33 | "real-is-required": "É necessário um número REAL", 34 | "success": "Sucesso", 35 | "value": "Valor" 36 | }, 37 | "csv": { 38 | "export": "Exportar", 39 | "export-csv": "Exportar CSV", 40 | "import": "Importar", 41 | "import-csv": "Importar CSV", 42 | "invalid-column-name-key": "Nome da coluna inválido: {key}", 43 | "n-ms-m-changes": "{n} ms, {m} alterações", 44 | "name": "Importador e Exportador de CSV", 45 | "no-result": "Sem Resultado", 46 | "select-a-csv-file": "Selecionar um ficheiro CSV" 47 | }, 48 | "run-query": { 49 | "export": "Exportar", 50 | "n-ms-m-changes": "{n} ms, {rr} linhas lidas, {rw} linhas escritas", 51 | "name": "Executar Consulta SQL", 52 | "no-result": "Sem Resultado", 53 | "no-results": "Sem Resultados", 54 | "run": "Executar", 55 | "unknown-error": "Erro Desconhecido" 56 | }, 57 | "semantic-query": { 58 | "autorun-on-read-only-queries": "Consulta somente de leitura será executada automaticamente.", 59 | "export": "Exportar", 60 | "n-ms-m-changes": "{n} ms, {rr} linhas lidas, {rw} linhas escritas", 61 | "name": "Consulta Semântica", 62 | "no-result": "Sem Resultado", 63 | "no-results": "Sem Resultados", 64 | "requires-openai_api_key": "Este plugin requer que a variável de ambiente OPENAI_API_KEY esteja definida.", 65 | "run": "Executar", 66 | "suggest": "Obter SQL", 67 | "unknown-error": "Erro Desconhecido" 68 | }, 69 | "table-browser": { 70 | "click-to-sort-by": "Clique para ordenar por {col}", 71 | "invalid-rowid": "rowid inválido", 72 | "name": "Navegador de Tabelas", 73 | "next": "Seguinte", 74 | "no-record": "Sem Registo", 75 | "no-result": "Sem Resultado", 76 | "no-results": "Sem Resultados Correspondentes", 77 | "prev": "Anterior", 78 | "showing": "{from} ~ {to}", 79 | "table-is-locked": "A tabela está bloqueada, desbloqueie para editar", 80 | "table-is-locked-click-to-unlock": "A tabela está bloqueada, clique para desbloquear", 81 | "table-is-unlocked-click-to-lock": "A tabela está desbloqueada, clique para bloquear", 82 | "unknown-error": "Erro Desconhecido" 83 | } 84 | }, 85 | "rows": "Linhas", 86 | "see-github": "Consulte o uso e guia no GitHub", 87 | "select-database": "Selecionar Base de Dados", 88 | "select-database-from-above": "Selecione uma base de dados no canto superior direito", 89 | "show-first-10-records": "mostrar os primeiros 10 registos em {table}", 90 | "suggestion-will-appear-here": "O SQL gerado aparecerá aqui", 91 | "theme": "Tema" 92 | } 93 | -------------------------------------------------------------------------------- /locales/es-ES.json: -------------------------------------------------------------------------------- 1 | { 2 | "col-default": "Por Defecto", 3 | "col-name": "Nombre", 4 | "col-type": "Tipo", 5 | "d1-manager": { 6 | "description": "D1 Manager es una interfaz web y API para Cloudflare D1, una base de datos SQL sin servidor. Proporciona una interfaz intuitiva para gestionar bases de datos, tablas y registros, así como una API para realizar operaciones de manera programática. D1 Manager simplifica la gestión de bases de datos, permitiendo a los usuarios centrarse en sus datos.", 7 | "name": "D1 Manager", 8 | "short": "Una interfaz web para gestionar datos en Cloudflare D1." 9 | }, 10 | "d1-manager-manage-db": "Usa D1 Manager para gestionar datos en {db}", 11 | "download": "Descargar", 12 | "execute-sql-query-in-database": "Ejecutar consulta SQL en la base de datos {db}", 13 | "import": "Importar", 14 | "lang": { 15 | "en": "Inglés", 16 | "es-ES": "Español de españa", 17 | "es-MX": "Español mexicano", 18 | "ja": "Japonés", 19 | "zh-CN": "Chino simplificado", 20 | "zh-TW": "Chino tradicional" 21 | }, 22 | "language": "Idioma", 23 | "n-ms": "{n} ms", 24 | "n-table-in-db": "{n} tablas en {db}", 25 | "plugin": { 26 | "add-record": { 27 | "add": "Añadir Registro", 28 | "column": "Columna", 29 | "error": "Error", 30 | "integer-is-required": "Se requiere ENTERO", 31 | "n-ms": "{n} ms", 32 | "name": "Añadir Registro", 33 | "real-is-required": "Se requiere número REAL", 34 | "success": "Éxito", 35 | "value": "Valor" 36 | }, 37 | "csv": { 38 | "export": "Exportar", 39 | "export-csv": "Exportar CSV", 40 | "import": "Importar", 41 | "import-csv": "Importar CSV", 42 | "invalid-column-name-key": "Nombre de columna no válido: {key}", 43 | "n-ms-m-changes": "{n} ms, {m} cambios", 44 | "name": "Importador y Exportador de CSV", 45 | "no-result": "Sin Resultado", 46 | "select-a-csv-file": "Seleccionar un archivo CSV" 47 | }, 48 | "run-query": { 49 | "export": "Exportar", 50 | "n-ms-m-changes": "{n} ms, {rr} filas leídas, {rw} filas escritas", 51 | "name": "Ejecutar Consulta SQL", 52 | "no-result": "Sin resultado", 53 | "no-results": "Sin resultados", 54 | "run": "Ejecutar", 55 | "unknown-error": "Error desconocido" 56 | }, 57 | "semantic-query": { 58 | "autorun-on-read-only-queries": "La consulta de solo lectura se ejecutará automáticamente.", 59 | "export": "Exportar", 60 | "n-ms-m-changes": "{n} ms, {rr} filas leídas, {rw} filas escritas", 61 | "name": "Consulta Semántica", 62 | "no-result": "Sin resultado", 63 | "no-results": "Sin resultados", 64 | "requires-openai_api_key": "Este plugin requiere que se establezca la variable de entorno OPENAI_API_KEY.", 65 | "run": "Ejecutar", 66 | "suggest": "Obtener SQL", 67 | "unknown-error": "Error desconocido" 68 | }, 69 | "table-browser": { 70 | "click-to-sort-by": "Haz clic para ordenar por {col}", 71 | "invalid-rowid": "Rowid no válido", 72 | "name": "Explorador de tablas", 73 | "next": "Siguiente", 74 | "no-record": "Sin registro", 75 | "no-result": "Sin resultado", 76 | "no-results": "Sin resultados coincidentes", 77 | "prev": "Anterior", 78 | "showing": "{from} ~ {to}", 79 | "table-is-locked": "La tabla está bloqueada, desbloquéela para editarla", 80 | "table-is-locked-click-to-unlock": "La tabla está bloqueada, haz clic para desbloquearla", 81 | "table-is-unlocked-click-to-lock": "La tabla está desbloqueada, haz clic para bloquearla", 82 | "unknown-error": "Error desconocido" 83 | } 84 | }, 85 | "rows": "Filas", 86 | "see-github": "Consulta el uso y guía en GitHub", 87 | "select-database": "Seleccionar base de datos", 88 | "select-database-from-above": "Selecciona una base de datos de la esquina superior derecha", 89 | "show-first-10-records": "mostrar los primeros 10 registros en {table}", 90 | "suggestion-will-appear-here": "El SQL generado aparecerá aquí", 91 | "theme": "Tema" 92 | } 93 | -------------------------------------------------------------------------------- /locales/es-MX.json: -------------------------------------------------------------------------------- 1 | { 2 | "col-default": "Predeterminado", 3 | "col-name": "Nombre", 4 | "col-type": "Tipo", 5 | "d1-manager": { 6 | "description": "D1 Manager es una interfaz web y API para Cloudflare D1, una base de datos SQL sin servidor. Proporciona una interfaz amigable para gestionar bases de datos, tablas y registros, así como una API para realizar operaciones de manera programática. D1 Manager simplifica la gestión de bases de datos, permitiendo a los usuarios concentrarse en sus datos.", 7 | "name": "D1 Manager", 8 | "short": "Una interfaz web para gestionar datos en Cloudflare D1." 9 | }, 10 | "d1-manager-manage-db": "Usa D1 Manager para gestionar datos en {db}", 11 | "download": "Descargar", 12 | "execute-sql-query-in-database": "Ejecutar consulta SQL en la base de datos {db}", 13 | "import": "Importar", 14 | "lang": { 15 | "en": "Inglés", 16 | "es-ES": "Español de españa", 17 | "es-MX": "Español mexicano", 18 | "ja": "Japonés", 19 | "zh-CN": "Chino simplificado", 20 | "zh-TW": "Chino tradicional" 21 | }, 22 | "language": "Idioma", 23 | "n-ms": "{n} ms", 24 | "n-table-in-db": "{n} tablas en {db}", 25 | "plugin": { 26 | "add-record": { 27 | "add": "Agregar Registro", 28 | "column": "Columna", 29 | "error": "Error", 30 | "integer-is-required": "Se requiere ENTERO", 31 | "n-ms": "{n} ms", 32 | "name": "Agregar Registro", 33 | "real-is-required": "Se requiere número REAL", 34 | "success": "Éxito", 35 | "value": "Valor" 36 | }, 37 | "csv": { 38 | "export": "Exportar", 39 | "export-csv": "Exportar CSV", 40 | "import": "Importar", 41 | "import-csv": "Importar CSV", 42 | "invalid-column-name-key": "Nombre de columna inválido: {key}", 43 | "n-ms-m-changes": "{n} ms, {m} cambios", 44 | "name": "Importador y Exportador de CSV", 45 | "no-result": "Sin resultado", 46 | "select-a-csv-file": "Selecciona un archivo CSV" 47 | }, 48 | "run-query": { 49 | "export": "Exportar", 50 | "n-ms-m-changes": "{n} ms, {rr} filas leídas, {rw} filas escritas", 51 | "name": "Ejecutar consulta SQL", 52 | "no-result": "Sin resultado", 53 | "no-results": "Sin resultados", 54 | "run": "Ejecutar", 55 | "unknown-error": "Error desconocido" 56 | }, 57 | "semantic-query": { 58 | "autorun-on-read-only-queries": "La consulta de solo lectura se ejecutará automáticamente.", 59 | "export": "Exportar", 60 | "n-ms-m-changes": "{n} ms, {rr} filas leídas, {rw} filas escritas", 61 | "name": "Consulta semántica", 62 | "no-result": "Sin resultado", 63 | "no-results": "Sin resultados", 64 | "requires-openai_api_key": "Este plugin requiere que se configure la variable de entorno OPENAI_API_KEY.", 65 | "run": "Ejecutar", 66 | "suggest": "Obtener SQL", 67 | "unknown-error": "Error desconocido" 68 | }, 69 | "table-browser": { 70 | "click-to-sort-by": "Haz clic para ordenar por {col}", 71 | "invalid-rowid": "Rowid inválido", 72 | "name": "Navegador de tablas", 73 | "next": "Siguiente", 74 | "no-record": "Sin registro", 75 | "no-result": "Sin resultado", 76 | "no-results": "Sin resultados coincidentes", 77 | "prev": "Anterior", 78 | "showing": "Mostrando {from} ~ {to}", 79 | "table-is-locked": "La tabla está bloqueada, desbloquearla para editar", 80 | "table-is-locked-click-to-unlock": "La tabla está bloqueada, haz clic para desbloquear", 81 | "table-is-unlocked-click-to-lock": "La tabla está desbloqueada, haz clic para bloquear", 82 | "unknown-error": "Error desconocido" 83 | } 84 | }, 85 | "rows": "Filas", 86 | "see-github": "Consulta el uso y guía en GitHub", 87 | "select-database": "Seleccionar Base de Datos", 88 | "select-database-from-above": "Selecciona una base de datos de la esquina superior derecha", 89 | "show-first-10-records": "mostrar los primeros 10 registros en {table}", 90 | "suggestion-will-appear-here": "El SQL generado aparecerá aquí", 91 | "theme": "Tema" 92 | } 93 | -------------------------------------------------------------------------------- /src/lib/sqlite2sql.ts: -------------------------------------------------------------------------------- 1 | export async function sqlite2sql(sqlite3: ArrayBuffer): Promise { 2 | const module = await import("sql.js"); 3 | const init = module.default; 4 | console.log(init); 5 | const SQL = await init({ locateFile: (file) => `https://sql.js.org/dist/${file}` }); 6 | 7 | const db = new SQL.Database(new Uint8Array(sqlite3)); 8 | 9 | let sql = ""; 10 | 11 | // list all tables 12 | const tables_res = db.exec("SELECT name, sql FROM sqlite_master WHERE type='table';")[0]; 13 | console.log({ tables_res }); 14 | const tables: [string, string][] = []; 15 | if (tables_res) { 16 | const unresolved = new Set(); 17 | const t = tables_res.values.filter((table) => { 18 | const name = table[0] as string; 19 | return ( 20 | !name.startsWith("sqlite_") && !name.startsWith("d1_") && !name.startsWith("_cf_") 21 | ); 22 | }); 23 | for (const table of t) { 24 | unresolved.add(table[0] as string); 25 | } 26 | 27 | // table order 28 | let resolving = true; 29 | while (resolving) { 30 | resolving = false; 31 | for (const table of t) { 32 | const name = table[0] as string; 33 | const sql = table[1] as string; 34 | // if sql incults any of the unresolved tables, skip it 35 | if ( 36 | Array.from(unresolved) 37 | .filter((x) => name !== x) 38 | .some((name) => sql.includes(name)) 39 | ) { 40 | continue; 41 | } 42 | // otherwise, add it to the resolved list 43 | tables.push(table as [string, string]); 44 | unresolved.delete(table[0] as string); 45 | t.splice(t.indexOf(table), 1); 46 | resolving = true; 47 | } 48 | } 49 | for (const table of unresolved) { 50 | console.log(`Unresolved table ${table}`); 51 | tables.push([table, (t.find((t) => t[0] === table)?.[1] || "") as string]); 52 | } 53 | 54 | for (const stmt of tables) { 55 | sql += stmt[1] + "\n"; 56 | } 57 | } 58 | 59 | // list all indexes 60 | const indexes = db.exec("SELECT name, sql FROM sqlite_master WHERE type='index';")[0]; 61 | console.log({ indexes }); 62 | if (indexes) { 63 | for (const index of indexes.values) { 64 | if (index[1]) { 65 | sql += index[1] + "\n"; 66 | } 67 | } 68 | } 69 | 70 | // list all triggers 71 | const triggers = db.exec("SELECT name, sql FROM sqlite_master WHERE type='trigger';")[0]; 72 | console.log({ triggers }); 73 | if (triggers) { 74 | for (const trigger of triggers.values) { 75 | if (trigger[1]) { 76 | sql += trigger[1] + "\n"; 77 | } 78 | } 79 | } 80 | 81 | // list all views 82 | const views = db.exec("SELECT name, sql FROM sqlite_master WHERE type='view';")[0]; 83 | console.log({ views }); 84 | if (views) { 85 | for (const view of views.values) { 86 | if (view[1]) { 87 | sql += view[1] + "\n"; 88 | } 89 | } 90 | } 91 | 92 | // list all virtual tables 93 | const vtables = db.exec("SELECT name, sql FROM sqlite_master WHERE type='vtable';")[0]; 94 | console.log({ vtables }); 95 | if (vtables) { 96 | for (const vtable of vtables.values) { 97 | if (vtable[1]) { 98 | sql += vtable[1] + "\n"; 99 | } 100 | } 101 | } 102 | 103 | // add all data 104 | if (tables) { 105 | for (const table of tables) { 106 | const name = table[0]; 107 | const rows = db.exec(`SELECT * FROM \`${name}\`;`)[0]; 108 | if (!rows) { 109 | console.log(`No rows found for table ${name}`); 110 | continue; 111 | } 112 | const cols = rows.columns; 113 | const vals = rows.values; 114 | console.log({ name, cols, vals }); 115 | for (const val of vals) { 116 | sql += `INSERT INTO \`${name}\` (${cols.map((c) => `\`${c}\``).join(", ")}) VALUES (${val 117 | .map((v) => 118 | typeof v === "string" 119 | ? `'${v.replace(/'/g, "''").replace(/\n/g, "\\n")}'` 120 | : typeof v === "number" 121 | ? v 122 | : v instanceof Uint8Array 123 | ? `X'${v.reduce((s, v) => s + v.toString(16).padStart(2, "0"), "")}'` 124 | : "NULL", 125 | ) 126 | .join(", ")})\n`; 127 | } 128 | } 129 | } 130 | 131 | return sql; 132 | } 133 | -------------------------------------------------------------------------------- /src/routes/api/db/[database]/[table]/data/+server.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This route reports the data in a table. 3 | * It also allows inserting, updating, and deleting data. 4 | */ 5 | import { dev } from "$app/environment"; 6 | import { error, json } from "@sveltejs/kit"; 7 | import type { RequestHandler } from "./$types"; 8 | 9 | export const GET: RequestHandler = async ({ url, params, locals, fetch }) => { 10 | if (dev) { 11 | const remote = new URL("https://d1-manager.pages.dev" + url.pathname + url.search); 12 | const res = await fetch(remote); 13 | return json(await res.json()); 14 | } 15 | 16 | const db = locals.db[params.database]; 17 | if (!db) { 18 | throw error(404, "Database not found"); 19 | } 20 | 21 | const offset = Number(url.searchParams.get("offset")) || 0; 22 | const limit = Number(url.searchParams.get("limit")) || 100; 23 | const order = url.searchParams.get("order") || ""; 24 | const dir = url.searchParams.get("dir") || "ASC"; 25 | const select = url.searchParams.get("select") || "*"; 26 | const where = url.searchParams.get("where") || ""; 27 | 28 | const { results } = await db 29 | .prepare( 30 | `SELECT ${select} FROM \`${params.table}\`${where ? ` WHERE ${where}` : ""}${ 31 | order ? ` ORDER BY ${order} ${dir}` : "" 32 | } LIMIT ${limit} OFFSET ${offset}`, 33 | ) 34 | .all(); 35 | 36 | if (!results) { 37 | throw error(404, "No data found"); 38 | } 39 | 40 | return json(results); 41 | }; 42 | 43 | export const POST: RequestHandler = async ({ request, params, locals }) => { 44 | const db = locals.db[params.database]; 45 | if (!db) { 46 | throw error(404, "Database not found"); 47 | } 48 | 49 | let data: Record; 50 | try { 51 | data = await request.json(); 52 | } catch (err) { 53 | if (err instanceof Error) { 54 | throw error(400, err.message); 55 | } 56 | throw err; 57 | } 58 | 59 | const statement = db 60 | .prepare( 61 | `INSERT INTO \`${params.table}\` (${Object.keys(data) 62 | .map((key) => `\`${key}\``) 63 | .join(", ")}) VALUES (${Object.keys(data) 64 | .map(() => "?") 65 | .join(", ")})`, 66 | ) 67 | .bind(...Object.values(data)); 68 | const result = await statement.run(); 69 | return json(result); 70 | }; 71 | 72 | export const PUT: RequestHandler = async ({ url, request, params, locals }) => { 73 | if (dev) { 74 | const remote = new URL("https://d1-manager.pages.dev" + url.pathname + url.search); 75 | const res = await fetch(remote, { method: "PUT", body: await request.text() }); 76 | return json(await res.json()); 77 | } 78 | 79 | const db = locals.db[params.database]; 80 | if (!db) { 81 | throw error(404, "Database not found"); 82 | } 83 | 84 | const where = Object.fromEntries(url.searchParams.entries()); 85 | 86 | let data: Record; 87 | try { 88 | data = await request.json(); 89 | } catch (err) { 90 | if (err instanceof Error) { 91 | throw error(400, err.message); 92 | } 93 | throw err; 94 | } 95 | 96 | const statement = db 97 | .prepare( 98 | `UPDATE \`${params.table}\` SET ${Object.keys(data) 99 | .map((key) => `\`${key}\` = ?`) 100 | .join(", ")} WHERE ${where_sql(where)}`, 101 | ) 102 | .bind(...Object.values(data), ...Object.values(where)); 103 | const result = await statement.run(); 104 | return json(result); 105 | }; 106 | 107 | export const DELETE: RequestHandler = async ({ url, params, locals }) => { 108 | if (dev) { 109 | const remote = new URL("https://d1-manager.pages.dev" + url.pathname + url.search); 110 | const res = await fetch(remote, { method: "DELETE" }); 111 | return json(await res.json()); 112 | } 113 | 114 | const db = locals.db[params.database]; 115 | if (!db) { 116 | throw error(404, "Database not found"); 117 | } 118 | 119 | const where = Object.fromEntries(url.searchParams.entries()); 120 | 121 | const statement = db 122 | .prepare(`DELETE FROM \`${params.table}\` WHERE ${where_sql(where)}`) 123 | .bind(...Object.values(where)); 124 | const result = await statement.run(); 125 | return json(result); 126 | }; 127 | 128 | function where_sql(where: Record): string { 129 | return Object.keys(where) 130 | .map((key) => `${key} = ?`) 131 | .join(" AND "); 132 | } 133 | -------------------------------------------------------------------------------- /src/lib/sql.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { is_dangerous, is_modify, is_readonly } from "./sql"; 3 | 4 | describe("SQL statement type checks", () => { 5 | // Safe read-only statements 6 | it("SELECT is neither dangerous nor modify", () => { 7 | expect(is_dangerous("SELECT * FROM users")).toBe(false); 8 | expect(is_modify("SELECT * FROM users")).toBe(false); 9 | expect(is_readonly("SELECT * FROM users")).toBe(true); 10 | }); 11 | 12 | // Insert statements 13 | it("INSERT is modify but not dangerous", () => { 14 | expect(is_dangerous("INSERT INTO users (id) VALUES (1)")).toBe(false); 15 | expect(is_modify("INSERT INTO users (id) VALUES (1)")).toBe(true); 16 | expect(is_readonly("INSERT INTO users (id) VALUES (1)")).toBe(false); 17 | }); 18 | 19 | // Update statements 20 | it("UPDATE is both dangerous and modify", () => { 21 | expect(is_dangerous("UPDATE users SET name = 'a' WHERE id = 1")).toBe(true); 22 | expect(is_modify("UPDATE users SET name = 'a' WHERE id = 1")).toBe(true); 23 | expect(is_readonly("UPDATE users SET name = 'a' WHERE id = 1")).toBe(false); 24 | }); 25 | 26 | // Delete statements 27 | it("DELETE is dangerous and modify", () => { 28 | expect(is_dangerous("DELETE FROM users WHERE id = 2")).toBe(true); 29 | expect(is_modify("DELETE FROM users WHERE id = 2")).toBe(true); 30 | expect(is_readonly("DELETE FROM users WHERE id = 2")).toBe(false); 31 | }); 32 | 33 | // Drop table 34 | it("DROP TABLE is dangerous and modify", () => { 35 | expect(is_dangerous("DROP TABLE users")).toBe(true); 36 | expect(is_modify("DROP TABLE users")).toBe(true); 37 | }); 38 | 39 | // Truncate and alter 40 | it("TRUNCATE and ALTER are dangerous and modify", () => { 41 | expect(is_dangerous("TRUNCATE TABLE users")).toBe(true); 42 | expect(is_modify("TRUNCATE TABLE users")).toBe(true); 43 | expect(is_dangerous("ALTER TABLE users ADD COLUMN age INT")).toBe(true); 44 | expect(is_modify("ALTER TABLE users ADD COLUMN age INT")).toBe(true); 45 | }); 46 | 47 | // Grant and Revoke 48 | it("GRANT and REVOKE are dangerous operations", () => { 49 | expect(is_dangerous("GRANT SELECT ON users TO role_user")).toBe(true); 50 | expect(is_dangerous("REVOKE UPDATE ON users FROM role_user")).toBe(true); 51 | }); 52 | 53 | // Create table 54 | it("CREATE TABLE is neither dangerous nor modify", () => { 55 | expect(is_dangerous("CREATE TABLE users (id INT)")).toBe(false); 56 | expect(is_modify("CREATE TABLE users (id INT)")).toBe(true); 57 | }); 58 | 59 | // Multiple statements 60 | it("Mixed statements detect highest risk", () => { 61 | const sql = `SELECT * FROM users; DELETE FROM users WHERE id=3;`; 62 | expect(is_dangerous(sql)).toBe(true); 63 | expect(is_modify(sql)).toBe(true); 64 | }); 65 | 66 | // Case insensitivity 67 | it("Lowercase statements are handled correctly", () => { 68 | expect(is_dangerous("delete from users")).toBe(true); 69 | expect(is_modify("insert into users values(1)")).toBe(true); 70 | }); 71 | 72 | // Malformed SQL 73 | it("Malformed SQL does not throw and returns false", () => { 74 | expect(is_dangerous("FOOBAR")).toBe(true); 75 | expect(is_readonly("FOOBAR")).toBe(false); 76 | }); 77 | 78 | // CTE with read-only inner SELECT 79 | it("WITH CTE SELECT is read-only", () => { 80 | const sql = "WITH cte AS (SELECT * FROM users) SELECT id FROM cte"; 81 | expect(is_dangerous(sql)).toBe(false); 82 | expect(is_modify(sql)).toBe(false); 83 | }); 84 | 85 | // CTE with UPDATE outer statement 86 | it("WITH CTE and UPDATE is dangerous and modify", () => { 87 | const sql = 88 | "WITH cte AS (SELECT * FROM users) UPDATE users SET name='a' WHERE id IN (SELECT id FROM cte)"; 89 | expect(is_dangerous(sql)).toBe(true); 90 | expect(is_modify(sql)).toBe(true); 91 | }); 92 | 93 | // Mixed CTE and DELETE statements 94 | it("mixed CTE and DELETE detect highest risk", () => { 95 | const sql = 96 | "WITH cte AS (SELECT * FROM users) SELECT * FROM cte; DELETE FROM users WHERE id=1;"; 97 | expect(is_dangerous(sql)).toBe(true); 98 | expect(is_modify(sql)).toBe(true); 99 | }); 100 | 101 | it("WITH delete_cte AS (DELETE FROM users WHERE id=1) DELETE FROM users WHERE id=2", () => { 102 | const sql = 103 | "WITH delete_cte AS (DELETE FROM users WHERE id=1) DELETE FROM users WHERE id=2"; 104 | expect(is_dangerous(sql)).toBe(true); 105 | expect(is_modify(sql)).toBe(true); 106 | }); 107 | }); 108 | -------------------------------------------------------------------------------- /src/routes/db/[database]/+page.svelte: -------------------------------------------------------------------------------- 1 | 63 | 64 | 65 | {$page.params.database} | {$t("d1-manager.name")} 66 | 70 | 71 | 72 |
73 |
74 |
75 |
76 | 86 | {#if query} 87 | 95 | {/if} 96 |
97 | 98 | {#if error} 99 |
{error}
100 | {:else if duration} 101 |
102 | {$t("n-ms", { values: { n: duration.toFixed(2) } })} 103 |
104 | {/if} 105 |
106 |
107 | 108 | {#each data.db as table} 109 | 110 |
113 |
114 |

{table.name}

115 | 116 |
117 |
118 |
{$t("rows")}
119 |
{table.count}
120 |
121 |
122 | 123 |
124 | 125 |
126 |
127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | {#each table.columns as column} 137 | 138 | 139 | 140 | 141 | 142 | {/each} 143 | 144 |
{$t("col-name")}{$t("col-type")}{$t("col-default")}
{column.name}{column.type}{column.dflt_value}
145 |
146 |
147 |
148 |
149 |
150 | {/each} 151 |
152 | -------------------------------------------------------------------------------- /src/lib/plugin/SemanticQuery.svelte: -------------------------------------------------------------------------------- 1 | 104 | 105 |

106 | {$t("plugin.semantic-query.requires-openai_api_key")} 107 | {$t("plugin.semantic-query.autorun-on-read-only-queries")} 108 |

109 | 110 |
111 |
112 | 119 | 120 | 127 |
128 |
129 | 130 |
131 |
132 | 139 | 140 | 148 |
149 |
150 | 151 | {#if result} 152 |
153 | 154 | {#if result.results?.length} 155 |
156 | 157 | 158 | 159 | {#each Object.keys(result.results[0]) as key} 160 | 161 | {/each} 162 | 163 | 164 | 165 | {#each result.results as row} 166 | 167 | {#each Object.values(row) as value} 168 | 171 | {/each} 172 | 173 | {/each} 174 | 175 |
{key}
{value}
176 |
177 | {:else} 178 |

179 | {$t("plugin.semantic-query.no-results")} 180 |

181 | {/if} 182 | 183 |
184 |

185 | {$t("plugin.semantic-query.n-ms-m-changes", { 186 | values: { 187 | n: result.meta.duration.toFixed(2), 188 | rr: result.meta.rows_read ?? "x", 189 | rw: result.meta.rows_written ?? result.meta.changes, 190 | }, 191 | })} 192 |

193 | {#if result.results?.length} 194 | 200 | {/if} 201 |
202 | {/if} 203 | 204 | {#if error} 205 |
206 | 207 |
208 |
{error}
209 |
210 | {/if} 211 | 212 |
213 | -------------------------------------------------------------------------------- /src/lib/plugin/AddRecord.svelte: -------------------------------------------------------------------------------- 1 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | {#each cols as col, i} 165 | 166 | 167 | 197 | 198 | {/each} 199 | 200 |
{$t("plugin.add-record.column")}{$t("plugin.add-record.value")}
{col.name} 168 | {#if input_type(col.type) !== "file"} 169 |
170 | { 175 | const err = type_check(col.type, e.currentTarget.value); 176 | record[col.name] = { 177 | type: col.type, 178 | val: e.currentTarget.value, 179 | err, 180 | nullable: !!col.notnull, 181 | }; 182 | }} 183 | placeholder={col.dflt_value || ""} 184 | /> 185 | {#if record[col.name].err} 186 | 191 | {/if} 192 |
193 | {:else} 194 | File upload not supported yet 195 | {/if} 196 |
201 | 202 | 205 | 206 | {#if result} 207 |
208 |

{$t("plugin.add-record.success")}

209 |

210 | {$t("plugin.add-record.n-ms", { 211 | values: { 212 | n: result.meta.duration.toFixed(2), 213 | }, 214 | })} 215 |

216 |
217 | {:else if error} 218 |
219 |

{$t("plugin.add-record.error")}

220 |

{error.error.cause || error.error.message}

221 |
222 | {/if} 223 | -------------------------------------------------------------------------------- /src/lib/plugin/CSV.svelte: -------------------------------------------------------------------------------- 1 | 179 | 180 |
181 |

{$t("plugin.csv.import-csv")}

182 | 183 |
184 | 185 |
186 | 189 | 198 | 202 |
203 | 204 | {#if keys && casted?.length} 205 |
206 | 207 | 208 | 209 | {#each keys as key} 210 | 211 | {/each} 212 | 213 | 214 | 215 | {#each casted as row} 216 | 217 | {#each row as value} 218 | 219 | {/each} 220 | 221 | {/each} 222 | 223 |
{key}
{value}
224 |
225 | 226 | 229 | {/if} 230 | {#if result} 231 |

232 | {$t("plugin.csv.n-ms-m-changes", { 233 | values: { 234 | n: result.meta.duration.toFixed(2), 235 | m: result.meta.changes, 236 | }, 237 | })} 238 |

239 | {/if} 240 |
241 | 242 |
243 |

{$t("plugin.csv.export-csv")}

244 | 245 |
246 | 247 | 250 |
251 | -------------------------------------------------------------------------------- /src/lib/plugin/TableBrowser.svelte: -------------------------------------------------------------------------------- 1 | 195 | 196 |
197 | 208 |
209 | 210 | {#if result} 211 | {#if result.length} 212 |
213 | 214 | 215 | 216 | {#each cols as col} 217 | 229 | {/each} 230 | 231 | 232 | 233 | 234 | {#each result as row} 235 | 236 | {#each Object.keys(row) as key} 237 | {#if key !== "_"} 238 | 262 | {/if} 263 | {/each} 264 | 277 | 278 | {/each} 279 | 280 |
change_sort(col)} 220 | title={$t("plugin.table-browser.click-to-sort-by", { 221 | values: { col }, 222 | })} 223 | > 224 | {col} 225 | {#if order === col} 226 | {dir} 227 | {/if} 228 |
239 | {#if typeof row[key] === "number"} 240 | edit(row._, key)} 245 | disabled={locked || running} 246 | title={locked 247 | ? $t("plugin.table-browser.table-is-locked") 248 | : undefined} 249 | /> 250 | {:else} 251 | edit(row._, key)} 255 | disabled={locked || running} 256 | title={locked 257 | ? $t("plugin.table-browser.table-is-locked") 258 | : undefined} 259 | /> 260 | {/if} 261 | 265 |
268 | 275 |
276 |
281 |
282 | {:else} 283 |

284 | {$t("plugin.table-browser.no-results")} 285 |

286 | {/if} 287 | 288 |
289 | {#if offset > 0} 290 | 300 | {/if} 301 | 302 |

303 | {$t("plugin.table-browser.showing", { 304 | values: { 305 | from: result.length ? offset + 1 : offset, 306 | to: offset + result.length, 307 | }, 308 | })} 309 |

310 | 311 | {#if result.length === limit} 312 | 322 | {/if} 323 |
324 | {/if} 325 | 326 | {#if error} 327 |
328 |
{error.error.cause || error.error.message}
329 |
330 | {/if} 331 | --------------------------------------------------------------------------------