feat(utils): 添加 Docker 方式执行 SQL 语句的功能

- 新增 dockerUtils.ts 文件,实现通过 Docker 容器执行 SQL 语句
- 支持 SQL Server 和 MySQL 数据库类型
- 使用 Dockerode 库与 Docker 引擎交互
- 功能包括启动容器、执行 SQL 命令、停止并移除容器
This commit is contained in:
fly6516 2025-02-19 16:34:18 +08:00
parent 9f09737b72
commit 8888b96992
4 changed files with 1000 additions and 51 deletions

889
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -10,10 +10,18 @@
},
"dependencies": {
"@types/pg": "^8.11.10",
"bun": "^1.2.2",
"child_process": "^1.0.2",
"dockerode": "^4.0.4",
"fs": "^0.0.1-security",
"http2": "^3.3.6",
"monaco-editor": "^0.52.2",
"net": "^1.0.2",
"next": "15.1.4",
"pg": "^8.13.1",
"react": "^19.0.0",
"react-dom": "^19.0.0"
"react-dom": "^19.0.0",
"tls": "^0.0.1"
},
"devDependencies": {
"@eslint/eslintrc": "^3",

View File

@ -1,52 +1,53 @@
"use client";
import Image from "next/image";
import { useEffect, useState } from 'react';
import serverAction from '../utils/dockerUtils'; // 更新引用
export default function Home() {
const [sqlCode, setSqlCode] = useState('SELECT * FROM your_table;');
const [databaseType, setDatabaseType] = useState('sqlserver');
const [result, setResult] = useState('');
useEffect(() => {
// 初始化逻辑可以放在这里
}, []);
const handleExecute = async () => {
try {
const res = await serverAction(sqlCode, databaseType); // 更新函数调用
setResult(res);
} catch (error) {
if (error instanceof Error) {
setResult(`Error: ${error.message}`);
} else {
setResult('An unknown error occurred.');
}
}
};
return (
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
<main className="flex flex-col gap-8 row-start-2 items-center sm:items-start">
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={180}
height={38}
priority
/>
<ol className="list-inside list-decimal text-sm text-center sm:text-left font-[family-name:var(--font-geist-mono)]">
<li className="mb-2">
Get started by editing{" "}
<code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-semibold">
src/app/page.tsx
</code>
.
</li>
<li>Save and see your changes instantly.</li>
</ol>
<textarea
value={sqlCode}
onChange={(e) => setSqlCode(e.target.value)}
style={{ width: '100%', height: '500px' }}
className="border border-gray-300 p-4 rounded"
></textarea>
<div className="flex gap-4 items-center flex-col sm:flex-row">
<a
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={20}
height={20}
/>
Deploy now
</a>
<a
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:min-w-44"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Read our docs
</a>
<select value={databaseType} onChange={(e) => setDatabaseType(e.target.value)}>
<option value="sqlserver">SQL Server</option>
<option value="mysql">MySQL</option>
{/* 可以在这里添加其他数据库选项 */}
</select>
<button onClick={handleExecute} className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5">
Execute SQL
</button>
</div>
<div>
<h3>Result:</h3>
<pre>{result}</pre>
</div>
</main>
<footer className="row-start-3 flex gap-6 flex-wrap items-center justify-center">

67
src/utils/dockerUtils.ts Normal file
View File

@ -0,0 +1,67 @@
"use server";
import Docker from 'dockerode';
const docker = new Docker();
export default async function serverAction(sql: string, databaseType: string): Promise<string> {
let dockerImage: string;
let command: string;
let env: string[] = [];
switch (databaseType) {
case 'sqlserver':
dockerImage = 'mcr.microsoft.com/mssql/server:2019-latest';
command = `sqlcmd -S localhost -U SA -P YourStrong!Passw0rd -Q "${sql}"`;
env = ['ACCEPT_EULA=Y', 'SA_PASSWORD=YourStrong!Passw0rd'];
break;
case 'mysql':
dockerImage = 'mysql:latest';
command = `mysql -u root -pYourStrong!Passw0rd -e "${sql}"`;
env = ['MYSQL_ROOT_PASSWORD=YourStrong!Passw0rd'];
break;
default:
throw new Error('Unsupported database type');
}
try {
// Start Docker container
const container = await docker.run(dockerImage, [], process.stdout, {
Tty: false,
Env: env,
HostConfig: {
PortBindings: {
'3306/tcp': [{ HostPort: '3306' }], // MySQL port binding
'1433/tcp': [{ HostPort: '1433' }], // SQL Server port binding
},
},
});
// Wait for the database to start (this is a simple sleep, you might want a more robust solution)
await new Promise(resolve => setTimeout(resolve, 10000));
// Execute SQL command
const exec = await container.exec({
Cmd: ['/bin/sh', '-c', command],
AttachStdout: true,
AttachStderr: true,
});
const stream = await exec.start({});
let output = '';
stream.on('data', (chunk: { toString: () => string; }) => {
output += chunk.toString();
});
await new Promise((resolve) => stream.on('end', resolve));
// Stop and remove Docker container
await container.stop();
await container.remove();
return output;
} catch (error) {
throw error;
}
}