mirror of
https://github.com/sqlchat/sqlchat.git
synced 2025-09-26 17:45:14 +08:00
feat: implement mysql connector
This commit is contained in:
@ -1,6 +1,7 @@
|
||||
{
|
||||
"extends": "next/core-web-vitals",
|
||||
"rules": {
|
||||
"import/no-anonymous-default-export": "off",
|
||||
"@next/next/no-img-element": "off",
|
||||
"react/jsx-no-target-blank": "off"
|
||||
}
|
||||
|
18
lib/connectors/index.ts
Normal file
18
lib/connectors/index.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { Connection } from "@/types";
|
||||
import mysql from "./mysql";
|
||||
|
||||
export interface Connector {
|
||||
testConnection: () => Promise<boolean>;
|
||||
getDatabases: () => Promise<string[]>;
|
||||
getTables: (databaseName: string) => Promise<string[]>;
|
||||
getTableStructure: (databaseName: string, tableName: string) => Promise<string>;
|
||||
}
|
||||
|
||||
export const newConnector = (connection: Connection): Connector => {
|
||||
switch (connection.engineType) {
|
||||
case "MYSQL":
|
||||
return mysql(connection);
|
||||
default:
|
||||
throw new Error("Unsupported engine type.");
|
||||
}
|
||||
};
|
71
lib/connectors/mysql/index.ts
Normal file
71
lib/connectors/mysql/index.ts
Normal file
@ -0,0 +1,71 @@
|
||||
import { Connection } from "@/types";
|
||||
import mysql, { RowDataPacket } from "mysql2/promise";
|
||||
import { Connector } from "..";
|
||||
|
||||
const convertToConnectionUrl = (connection: Connection): string => {
|
||||
// Connection URL format: mysql://USER:PASSWORD@HOST:PORT/DATABASE
|
||||
return `mysql://${connection.username}:${connection.password}@${connection.host}:${connection.port}/${connection.database ?? ""}`;
|
||||
};
|
||||
|
||||
const testConnection = async (connection: Connection): Promise<boolean> => {
|
||||
const connectionUrl = convertToConnectionUrl(connection);
|
||||
try {
|
||||
const conn = await mysql.createConnection(connectionUrl);
|
||||
conn.destroy();
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const getDatabases = async (connection: Connection): Promise<string[]> => {
|
||||
const connectionUrl = convertToConnectionUrl(connection);
|
||||
const conn = await mysql.createConnection(connectionUrl);
|
||||
const [rows] = await conn.query<RowDataPacket[]>("SELECT schema_name as db_name FROM information_schema.schemata;");
|
||||
conn.destroy();
|
||||
const databaseList = [];
|
||||
for (const row of rows) {
|
||||
if (row["db_name"]) {
|
||||
databaseList.push(row["db_name"]);
|
||||
}
|
||||
}
|
||||
return databaseList;
|
||||
};
|
||||
|
||||
const getTables = async (connection: Connection, databaseName: string): Promise<string[]> => {
|
||||
const connectionUrl = convertToConnectionUrl(connection);
|
||||
const conn = await mysql.createConnection(connectionUrl);
|
||||
const [rows] = await conn.query<RowDataPacket[]>(
|
||||
`SELECT TABLE_NAME as table_name FROM information_schema.tables WHERE TABLE_SCHEMA='${databaseName}' AND TABLE_TYPE='BASE TABLE';`
|
||||
);
|
||||
conn.destroy();
|
||||
const databaseList = [];
|
||||
for (const row of rows) {
|
||||
if (row["table_name"]) {
|
||||
databaseList.push(row["table_name"]);
|
||||
}
|
||||
}
|
||||
return databaseList;
|
||||
};
|
||||
|
||||
const getTableStructure = async (connection: Connection, databaseName: string, tableName: string): Promise<string> => {
|
||||
const connectionUrl = convertToConnectionUrl(connection);
|
||||
const conn = await mysql.createConnection(connectionUrl);
|
||||
const [rows] = await conn.query<RowDataPacket[]>(`SHOW CREATE TABLE \`${databaseName}\`.\`${tableName}\`;`);
|
||||
conn.destroy();
|
||||
if (rows.length !== 1) {
|
||||
throw new Error("Unexpected number of rows.");
|
||||
}
|
||||
return rows[0]["Create Table"] || "";
|
||||
};
|
||||
|
||||
const newConnector = (connection: Connection): Connector => {
|
||||
return {
|
||||
testConnection: () => testConnection(connection),
|
||||
getDatabases: () => getDatabases(connection),
|
||||
getTables: (databaseName: string) => getTables(connection, databaseName),
|
||||
getTableStructure: (databaseName: string, tableName: string) => getTableStructure(connection, databaseName, tableName),
|
||||
};
|
||||
};
|
||||
|
||||
export default newConnector;
|
@ -35,6 +35,7 @@
|
||||
"autoprefixer": "^10.4.13",
|
||||
"eslint": "8.20.0",
|
||||
"eslint-config-next": "12.2.3",
|
||||
"mysql2": "^3.2.0",
|
||||
"postcss": "^8.4.20",
|
||||
"tailwindcss": "^3.2.4",
|
||||
"typescript": "^4.9.4"
|
||||
|
25
pages/api/connection/db.ts
Normal file
25
pages/api/connection/db.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
import { newConnector } from "@/lib/connectors";
|
||||
import { Connection } from "@/types";
|
||||
|
||||
// POST /api/connection/db
|
||||
// req body: { connection: Connection }
|
||||
// It's mainly used to get the database list.
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
if (req.method !== "POST") {
|
||||
return res.status(405).json([]);
|
||||
}
|
||||
|
||||
const connection = req.body.connection as Connection;
|
||||
try {
|
||||
const connector = newConnector(connection);
|
||||
const databaseNameList = await connector.getDatabases();
|
||||
res.status(200).json(databaseNameList);
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
error: error,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default handler;
|
31
pages/api/connection/db_schema.ts
Normal file
31
pages/api/connection/db_schema.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
import { newConnector } from "@/lib/connectors";
|
||||
import { Connection } from "@/types";
|
||||
|
||||
// POST /api/connection/db_schema
|
||||
// req body: { connection: Connection, db: string }
|
||||
// It's mainly used to get the database list.
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
if (req.method !== "POST") {
|
||||
return res.status(405).json([]);
|
||||
}
|
||||
|
||||
const connection = req.body.connection as Connection;
|
||||
const db = req.body.db as string;
|
||||
try {
|
||||
const connector = newConnector(connection);
|
||||
const tableStructures: string[] = [];
|
||||
const tables = await connector.getTables(db);
|
||||
for (const table of tables) {
|
||||
const structure = await connector.getTableStructure(db, table);
|
||||
tableStructures.push(structure);
|
||||
}
|
||||
res.status(200).json(tableStructures);
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
error: error,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default handler;
|
23
pages/api/connection/test.ts
Normal file
23
pages/api/connection/test.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
import { newConnector } from "@/lib/connectors";
|
||||
import { Connection } from "@/types";
|
||||
|
||||
// POST /api/connection/test
|
||||
// req body: { connection: Connection }
|
||||
// It's mainly used to test the connection.
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
if (req.method !== "POST") {
|
||||
return res.status(405).json(false);
|
||||
}
|
||||
|
||||
const connection = req.body.connection as Connection;
|
||||
try {
|
||||
const connector = newConnector(connection);
|
||||
const result = await connector.testConnection();
|
||||
res.status(200).json(result);
|
||||
} catch (error) {
|
||||
res.status(400).json(false);
|
||||
}
|
||||
};
|
||||
|
||||
export default handler;
|
67
pnpm-lock.yaml
generated
67
pnpm-lock.yaml
generated
@ -18,6 +18,7 @@ specifiers:
|
||||
highlight.js: ^11.7.0
|
||||
lodash-es: ^4.17.21
|
||||
marked: ^4.2.12
|
||||
mysql2: ^3.2.0
|
||||
next: ^13.2.4
|
||||
openai: ^3.0.0
|
||||
postcss: ^8.4.20
|
||||
@ -60,6 +61,7 @@ devDependencies:
|
||||
autoprefixer: 10.4.14_postcss@8.4.21
|
||||
eslint: 8.20.0
|
||||
eslint-config-next: 12.2.3_bqegqxcnsisudkhpmmezgt6uoa
|
||||
mysql2: 3.2.0
|
||||
postcss: 8.4.21
|
||||
tailwindcss: 3.2.7_postcss@8.4.21
|
||||
typescript: 4.9.5
|
||||
@ -779,6 +781,11 @@ packages:
|
||||
engines: {node: '>=0.4.0'}
|
||||
dev: false
|
||||
|
||||
/denque/2.1.0:
|
||||
resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==}
|
||||
engines: {node: '>=0.10'}
|
||||
dev: true
|
||||
|
||||
/detective/5.2.1:
|
||||
resolution: {integrity: sha512-v9XE1zRnz1wRtgurGu0Bs8uHKFSTdteYZNbIPFVhUZ39L/S79ppMpdmVOZAnoz1jfEFodc48n6MX483Xo3t1yw==}
|
||||
engines: {node: '>=0.8.0'}
|
||||
@ -1311,6 +1318,12 @@ packages:
|
||||
resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==}
|
||||
dev: true
|
||||
|
||||
/generate-function/2.3.1:
|
||||
resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==}
|
||||
dependencies:
|
||||
is-property: 1.0.2
|
||||
dev: true
|
||||
|
||||
/get-intrinsic/1.2.0:
|
||||
resolution: {integrity: sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==}
|
||||
dependencies:
|
||||
@ -1447,6 +1460,13 @@ packages:
|
||||
engines: {node: '>=12.0.0'}
|
||||
dev: false
|
||||
|
||||
/iconv-lite/0.6.3:
|
||||
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dependencies:
|
||||
safer-buffer: 2.1.2
|
||||
dev: true
|
||||
|
||||
/ignore/5.2.4:
|
||||
resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==}
|
||||
engines: {node: '>= 4'}
|
||||
@ -1573,6 +1593,10 @@ packages:
|
||||
engines: {node: '>=0.12.0'}
|
||||
dev: true
|
||||
|
||||
/is-property/1.0.2:
|
||||
resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==}
|
||||
dev: true
|
||||
|
||||
/is-regex/1.1.4:
|
||||
resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@ -1713,6 +1737,10 @@ packages:
|
||||
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
|
||||
dev: true
|
||||
|
||||
/long/5.2.1:
|
||||
resolution: {integrity: sha512-GKSNGeNAtw8IryjjkhZxuKB3JzlcLTwjtiQCHKvqQet81I93kXslhDQruGI/QsddO83mcDToBVy7GqGS/zYf/A==}
|
||||
dev: true
|
||||
|
||||
/loose-envify/1.4.0:
|
||||
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
|
||||
hasBin: true
|
||||
@ -1726,6 +1754,11 @@ packages:
|
||||
yallist: 4.0.0
|
||||
dev: true
|
||||
|
||||
/lru-cache/7.18.3:
|
||||
resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==}
|
||||
engines: {node: '>=12'}
|
||||
dev: true
|
||||
|
||||
/marked/4.2.12:
|
||||
resolution: {integrity: sha512-yr8hSKa3Fv4D3jdZmtMMPghgVt6TWbk86WQaWhDloQjRSQhMMYCAro7jP7VDJrjjdV8pxVxMssXS8B8Y5DZ5aw==}
|
||||
engines: {node: '>= 12'}
|
||||
@ -1775,6 +1808,27 @@ packages:
|
||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||
dev: true
|
||||
|
||||
/mysql2/3.2.0:
|
||||
resolution: {integrity: sha512-0Vn6a9WSrq6fWwvPgrvIwnOCldiEcgbzapVRDAtDZ4cMTxN7pnGqCTx8EG32S/NYXl6AXkdO+9hV1tSIi/LigA==}
|
||||
engines: {node: '>= 8.0'}
|
||||
dependencies:
|
||||
denque: 2.1.0
|
||||
generate-function: 2.3.1
|
||||
iconv-lite: 0.6.3
|
||||
long: 5.2.1
|
||||
lru-cache: 7.18.3
|
||||
named-placeholders: 1.1.3
|
||||
seq-queue: 0.0.5
|
||||
sqlstring: 2.3.3
|
||||
dev: true
|
||||
|
||||
/named-placeholders/1.1.3:
|
||||
resolution: {integrity: sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
dependencies:
|
||||
lru-cache: 7.18.3
|
||||
dev: true
|
||||
|
||||
/nanoid/3.3.4:
|
||||
resolution: {integrity: sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==}
|
||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||
@ -2236,6 +2290,10 @@ packages:
|
||||
is-regex: 1.1.4
|
||||
dev: true
|
||||
|
||||
/safer-buffer/2.1.2:
|
||||
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
|
||||
dev: true
|
||||
|
||||
/scheduler/0.23.0:
|
||||
resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==}
|
||||
dependencies:
|
||||
@ -2255,6 +2313,10 @@ packages:
|
||||
lru-cache: 6.0.0
|
||||
dev: true
|
||||
|
||||
/seq-queue/0.0.5:
|
||||
resolution: {integrity: sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==}
|
||||
dev: true
|
||||
|
||||
/shebang-command/2.0.0:
|
||||
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
|
||||
engines: {node: '>=8'}
|
||||
@ -2284,6 +2346,11 @@ packages:
|
||||
resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
/sqlstring/2.3.3:
|
||||
resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
dev: true
|
||||
|
||||
/stop-iteration-iterator/1.0.0:
|
||||
resolution: {integrity: sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
@ -1,10 +1,23 @@
|
||||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
import { Connection, Database } from "@/types";
|
||||
import { Connection, Database, Engine, UNKNOWN_ID } from "@/types";
|
||||
import { generateUUID } from "@/utils";
|
||||
import axios from "axios";
|
||||
import { uniqBy } from "lodash-es";
|
||||
|
||||
export const connectionSampleData: Connection = {
|
||||
id: UNKNOWN_ID,
|
||||
title: "",
|
||||
engineType: Engine.MySQL,
|
||||
host: "127.0.0.1",
|
||||
port: "3306",
|
||||
username: "root",
|
||||
password: "",
|
||||
};
|
||||
|
||||
interface ConnectionContext {
|
||||
connection: Connection;
|
||||
database: Database;
|
||||
database?: Database;
|
||||
}
|
||||
|
||||
interface ConnectionState {
|
||||
@ -13,25 +26,64 @@ interface ConnectionState {
|
||||
currentConnectionCtx?: ConnectionContext;
|
||||
createConnection: (connection: Connection) => void;
|
||||
setCurrentConnectionCtx: (connectionCtx: ConnectionContext) => void;
|
||||
getOrFetchDatabaseList: (connection: Connection) => Promise<Database[]>;
|
||||
}
|
||||
|
||||
export const useConnectionStore = create<ConnectionState>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
connectionList: [],
|
||||
(set, get) => ({
|
||||
connectionList: [connectionSampleData],
|
||||
databaseList: [],
|
||||
createConnection: (connection: Connection) => {
|
||||
set((state) => ({
|
||||
connectionList: [...state.connectionList, connection],
|
||||
...state,
|
||||
connectionList: [
|
||||
...state.connectionList,
|
||||
{
|
||||
...connection,
|
||||
id: generateUUID(),
|
||||
},
|
||||
],
|
||||
}));
|
||||
},
|
||||
setCurrentConnectionCtx: (connectionCtx: ConnectionContext) =>
|
||||
set(() => ({
|
||||
set((state) => ({
|
||||
...state,
|
||||
currentConnectionCtx: connectionCtx,
|
||||
})),
|
||||
getOrFetchDatabaseList: async (connection: Connection) => {
|
||||
const { data } = await axios.post<string[]>("/api/connection/db", {
|
||||
connection,
|
||||
});
|
||||
const fetchedDatabaseList = data.map(
|
||||
(dbName) =>
|
||||
({
|
||||
connectionId: connection.id,
|
||||
name: dbName,
|
||||
tableList: {},
|
||||
} as Database)
|
||||
);
|
||||
const state = get();
|
||||
const databaseList = uniqBy(
|
||||
[...state.databaseList, ...fetchedDatabaseList],
|
||||
(database) => `${database.connectionId}_${database.name}`
|
||||
);
|
||||
set((state) => ({
|
||||
...state,
|
||||
databaseList,
|
||||
}));
|
||||
return databaseList.filter((database) => database.connectionId === connection.id);
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: "connection-storage",
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
export const testConnection = async (connection: Connection) => {
|
||||
const { data: result } = await axios.post<boolean>("/api/connection/test", {
|
||||
connection,
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Id } from "./";
|
||||
|
||||
enum Engine {
|
||||
export enum Engine {
|
||||
MySQL = "MYSQL",
|
||||
PostgreSQL = "POSTGRESQL",
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ export interface Database {
|
||||
tableList: Table[];
|
||||
}
|
||||
|
||||
interface Table {
|
||||
export interface Table {
|
||||
name: string;
|
||||
// structure is a string of the table structure.
|
||||
// It's mainly used for providing a chat context for the assistant.
|
||||
|
Reference in New Issue
Block a user