mirror of
https://github.com/ershisan99/db-studio.git
synced 2025-12-16 12:33:05 +00:00
wip: mysql driver
add tests for pg driver and mysql driver (in progress)
This commit is contained in:
BIN
api/bun.lockb
BIN
api/bun.lockb
Binary file not shown.
32
api/docker-compose.yml
Normal file
32
api/docker-compose.yml
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
version: '3.3'
|
||||||
|
|
||||||
|
services:
|
||||||
|
adminer:
|
||||||
|
image: adminer
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
postgres:
|
||||||
|
container_name: db-postgres-test
|
||||||
|
image: postgres:latest
|
||||||
|
environment:
|
||||||
|
POSTGRES_PASSWORD: mysecretpassword
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
volumes:
|
||||||
|
- pgdata:/var/lib/postgresql/data
|
||||||
|
|
||||||
|
db:
|
||||||
|
container_name: db-mysql-test
|
||||||
|
image: mysql:latest
|
||||||
|
environment:
|
||||||
|
- MYSQL_PASSWORD=mysecretpassword
|
||||||
|
- MYSQL_ROOT_PASSWORD=mysecretpassword
|
||||||
|
ports:
|
||||||
|
- "3306:3306"
|
||||||
|
volumes:
|
||||||
|
- mysqldata:/var/lib/mysql
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
pgdata:
|
||||||
|
mysqldata:
|
||||||
@@ -7,9 +7,15 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@elysiajs/cors": "^1.0.2",
|
"@elysiajs/cors": "^1.0.2",
|
||||||
|
"@elysiajs/eden": "^1.0.14",
|
||||||
"@elysiajs/jwt": "^1.0.2",
|
"@elysiajs/jwt": "^1.0.2",
|
||||||
"@it-incubator/prettier-config": "^0.1.2",
|
"@it-incubator/prettier-config": "^0.1.2",
|
||||||
|
"@types/cookie": "^0.6.0",
|
||||||
|
"@types/jsonwebtoken": "^9.0.6",
|
||||||
|
"cookie": "^0.6.0",
|
||||||
"elysia": "latest",
|
"elysia": "latest",
|
||||||
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
"mysql2": "^3.10.2",
|
||||||
"postgres": "^3.4.4"
|
"postgres": "^3.4.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
32
api/scripts/setup-test-db.sh
Normal file
32
api/scripts/setup-test-db.sh
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Start Docker containers
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# Wait for PostgreSQL to be ready
|
||||||
|
until docker exec db-postgres-test pg_isready -U postgres; do
|
||||||
|
>&2 echo "Postgres is unavailable - sleeping"
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
# Wait for MySQL to be ready
|
||||||
|
until docker exec db-mysql-test mysqladmin ping -h "localhost" --silent; do
|
||||||
|
>&2 echo "MySQL is unavailable - sleeping"
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
# Set up PostgreSQL test data
|
||||||
|
docker exec -i db-postgres-test psql -U postgres <<EOF
|
||||||
|
CREATE TABLE IF NOT EXISTS test_table (id SERIAL PRIMARY KEY, name VARCHAR(50));
|
||||||
|
INSERT INTO test_table (name) VALUES ('John Doe');
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Set up MySQL test data
|
||||||
|
docker exec -i db-mysql-test mysql -uroot -pmysecretpassword<<EOF
|
||||||
|
CREATE DATABASE IF NOT EXISTS test_db;
|
||||||
|
USE test_db;
|
||||||
|
CREATE TABLE IF NOT EXISTS test_table (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50));
|
||||||
|
INSERT INTO test_table (name) VALUES ('Jane Doe');
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "Test databases are ready"
|
||||||
16
api/scripts/teardown-test-db.sh
Normal file
16
api/scripts/teardown-test-db.sh
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Clean up PostgreSQL test data
|
||||||
|
docker exec -i db-postgres-test psql -U postgres <<EOF
|
||||||
|
DROP TABLE IF EXISTS test_table;
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Clean up MySQL test data
|
||||||
|
docker exec -i db-mysql-test mysql -uroot -pmysecretpassword --database=test_db<<EOF
|
||||||
|
DROP TABLE IF EXISTS test_table;
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Stop and remove Docker containers and volumes
|
||||||
|
docker-compose down -v
|
||||||
|
|
||||||
|
echo "Test databases are cleaned up"
|
||||||
376
api/src/drivers/mysql.ts
Normal file
376
api/src/drivers/mysql.ts
Normal file
@@ -0,0 +1,376 @@
|
|||||||
|
import mysql, { type ResultSetHeader } from "mysql2/promise";
|
||||||
|
import type {
|
||||||
|
Credentials,
|
||||||
|
Driver,
|
||||||
|
WithSort,
|
||||||
|
WithSortPagination,
|
||||||
|
} from "./driver.interface";
|
||||||
|
|
||||||
|
const isResultSetHeader = (data: unknown): data is ResultSetHeader => {
|
||||||
|
if (!data || typeof data !== "object") return false;
|
||||||
|
|
||||||
|
const keys = [
|
||||||
|
"fieldCount",
|
||||||
|
"affectedRows",
|
||||||
|
"insertId",
|
||||||
|
"info",
|
||||||
|
"serverStatus",
|
||||||
|
"warningStatus",
|
||||||
|
"changedRows",
|
||||||
|
];
|
||||||
|
|
||||||
|
return keys.every((key) => key in data);
|
||||||
|
};
|
||||||
|
|
||||||
|
export class MySQLDriver implements Driver {
|
||||||
|
parseCredentials({
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
host,
|
||||||
|
type,
|
||||||
|
port,
|
||||||
|
database,
|
||||||
|
}: {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
host: string;
|
||||||
|
type: string;
|
||||||
|
port: string;
|
||||||
|
database: string;
|
||||||
|
ssl: string;
|
||||||
|
}) {
|
||||||
|
return {
|
||||||
|
user: username,
|
||||||
|
password,
|
||||||
|
host,
|
||||||
|
type,
|
||||||
|
port: Number.parseInt(port, 10),
|
||||||
|
database,
|
||||||
|
ssl: {
|
||||||
|
rejectUnauthorized: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async queryRunner(credentials: Credentials) {
|
||||||
|
let connection: mysql.Connection;
|
||||||
|
try {
|
||||||
|
if ("connectionString" in credentials) {
|
||||||
|
connection = await mysql.createConnection(credentials.connectionString);
|
||||||
|
} else {
|
||||||
|
connection = await mysql.createConnection(
|
||||||
|
this.parseCredentials(credentials),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
throw new Error(`Invalid connection string, ${JSON.stringify(error)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return connection;
|
||||||
|
}
|
||||||
|
private getActions(matchString: string) {
|
||||||
|
const onActions = "RESTRICT|NO ACTION|CASCADE|SET NULL|SET DEFAULT";
|
||||||
|
const onDeleteRegex = new RegExp(`ON DELETE (${onActions})`);
|
||||||
|
const onUpdateRegex = new RegExp(`ON UPDATE (${onActions})`);
|
||||||
|
|
||||||
|
const onDeleteMatch = matchString.match(onDeleteRegex);
|
||||||
|
const onUpdateMatch = matchString.match(onUpdateRegex);
|
||||||
|
|
||||||
|
const onDeleteAction = onDeleteMatch ? onDeleteMatch[1] : "NO ACTION";
|
||||||
|
const onUpdateAction = onUpdateMatch ? onUpdateMatch[1] : "NO ACTION";
|
||||||
|
|
||||||
|
return {
|
||||||
|
onDelete: onDeleteAction,
|
||||||
|
onUpdate: onUpdateAction,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAllDatabases(credentials: Credentials) {
|
||||||
|
console.log("Get all databases");
|
||||||
|
const connection = await this.queryRunner(credentials);
|
||||||
|
|
||||||
|
const databases: Array<string> = [];
|
||||||
|
const [databases_raw] = await connection.query("SHOW DATABASES;"); // Get all databases
|
||||||
|
|
||||||
|
for (const db of Array.from(databases_raw)) {
|
||||||
|
databases.push(db.Database);
|
||||||
|
}
|
||||||
|
connection.destroy();
|
||||||
|
return databases;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAllTables(
|
||||||
|
credentials: Credentials,
|
||||||
|
{ sortDesc, sortField, dbName }: WithSort<{ dbName: string }>,
|
||||||
|
) {
|
||||||
|
const connection = await this.queryRunner(credentials);
|
||||||
|
|
||||||
|
const tablesQuery = `
|
||||||
|
SELECT
|
||||||
|
TABLE_NAME as table_name,
|
||||||
|
TABLE_SCHEMA as schema_name,
|
||||||
|
TABLE_ROWS as row_count,
|
||||||
|
(DATA_LENGTH + INDEX_LENGTH) as total_size,
|
||||||
|
DATA_LENGTH as table_size,
|
||||||
|
INDEX_LENGTH as index_size,
|
||||||
|
TABLE_COMMENT as comments
|
||||||
|
FROM
|
||||||
|
information_schema.tables
|
||||||
|
WHERE
|
||||||
|
table_schema = ?;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const [tables] = await connection.execute(tablesQuery, [dbName]);
|
||||||
|
|
||||||
|
const primaryKeysQuery = `
|
||||||
|
SELECT
|
||||||
|
TABLE_NAME,
|
||||||
|
COLUMN_NAME
|
||||||
|
FROM
|
||||||
|
information_schema.KEY_COLUMN_USAGE
|
||||||
|
WHERE
|
||||||
|
TABLE_SCHEMA = ? AND
|
||||||
|
CONSTRAINT_NAME = 'PRIMARY';
|
||||||
|
`;
|
||||||
|
|
||||||
|
const [primaryKeys] = await connection.execute(primaryKeysQuery, [dbName]);
|
||||||
|
|
||||||
|
const indexesQuery = `
|
||||||
|
SELECT
|
||||||
|
TABLE_NAME
|
||||||
|
FROM
|
||||||
|
information_schema.STATISTICS
|
||||||
|
WHERE
|
||||||
|
TABLE_SCHEMA = ?;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const [indexes] = await connection.execute(indexesQuery, [dbName]);
|
||||||
|
|
||||||
|
const formattedTables = tables.map((table) => {
|
||||||
|
const primaryKey = primaryKeys
|
||||||
|
.filter((pk) => pk.TABLE_NAME === table.table_name)
|
||||||
|
.map((pk) => pk.COLUMN_NAME)
|
||||||
|
.join(", ");
|
||||||
|
|
||||||
|
const tableIndexes = indexes
|
||||||
|
.filter((idx) => idx.TABLE_NAME === table.table_name)
|
||||||
|
.map((idx) => idx.TABLE_NAME)
|
||||||
|
.join(", ");
|
||||||
|
|
||||||
|
return {
|
||||||
|
comments: table.comments,
|
||||||
|
index_size: table.index_size,
|
||||||
|
indexes: tableIndexes,
|
||||||
|
owner: null, // No information on table owner in `information_schema`
|
||||||
|
primary_key: primaryKey,
|
||||||
|
row_count: table.row_count,
|
||||||
|
table_name: table.table_name,
|
||||||
|
table_size: table.table_size,
|
||||||
|
total_size: table.total_size,
|
||||||
|
schema_name: table.schema_name,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
connection.destroy();
|
||||||
|
return formattedTables;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTableData(
|
||||||
|
credentials: Credentials,
|
||||||
|
{
|
||||||
|
tableName,
|
||||||
|
dbName,
|
||||||
|
perPage,
|
||||||
|
page,
|
||||||
|
sortDesc,
|
||||||
|
sortField,
|
||||||
|
}: WithSortPagination<{ tableName: string; dbName: string }>,
|
||||||
|
) {
|
||||||
|
const connection = await this.queryRunner(credentials);
|
||||||
|
|
||||||
|
const offset = perPage * page;
|
||||||
|
|
||||||
|
// Get the count of rows
|
||||||
|
const [rows] = await connection.execute(
|
||||||
|
`SELECT COUNT(*) as count FROM ${dbName}.${tableName}`,
|
||||||
|
);
|
||||||
|
if ("fieldCount" in rows) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const count = rows[0].count;
|
||||||
|
// Construct the query for table data with optional sorting
|
||||||
|
let query = `SELECT * FROM ${dbName}.${tableName}`;
|
||||||
|
const params = [];
|
||||||
|
|
||||||
|
if (sortField) {
|
||||||
|
query += ` ORDER BY ${sortField} ${sortDesc ? "DESC" : "ASC"}`;
|
||||||
|
}
|
||||||
|
console.log(perPage, offset, page);
|
||||||
|
query += ` LIMIT ${perPage} OFFSET ${offset}`;
|
||||||
|
params.push(perPage.toString());
|
||||||
|
params.push(offset.toString());
|
||||||
|
// Execute the query with parameters
|
||||||
|
const [data] = await connection.execute(query);
|
||||||
|
await connection.end();
|
||||||
|
|
||||||
|
return {
|
||||||
|
count: count,
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTableColumns(
|
||||||
|
credentials: Credentials,
|
||||||
|
{ tableName, dbName }: { dbName: string; tableName: string },
|
||||||
|
) {
|
||||||
|
const connection = await this.queryRunner(credentials);
|
||||||
|
|
||||||
|
const [rows] = await connection.execute(
|
||||||
|
`
|
||||||
|
SELECT
|
||||||
|
COLUMN_NAME as column_name,
|
||||||
|
DATA_TYPE as data_type,
|
||||||
|
COLUMN_TYPE as udt_name,
|
||||||
|
COLUMN_COMMENT as column_comment
|
||||||
|
FROM
|
||||||
|
information_schema.COLUMNS
|
||||||
|
WHERE
|
||||||
|
TABLE_SCHEMA = ?
|
||||||
|
AND TABLE_NAME = ?
|
||||||
|
`,
|
||||||
|
[dbName, tableName],
|
||||||
|
);
|
||||||
|
|
||||||
|
await connection.end();
|
||||||
|
|
||||||
|
return (rows as any[]).map((row) => ({
|
||||||
|
column_name: row.column_name,
|
||||||
|
data_type: row.data_type,
|
||||||
|
udt_name: row.udt_name,
|
||||||
|
column_comment: row.column_comment,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTableIndexes(
|
||||||
|
credentials: Credentials,
|
||||||
|
{ dbName, tableName }: { dbName: string; tableName: string },
|
||||||
|
) {
|
||||||
|
const connection = await this.queryRunner(credentials);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [rows] = await connection.execute(
|
||||||
|
`
|
||||||
|
SELECT
|
||||||
|
index_name AS relname,
|
||||||
|
index_name AS \`key\`,
|
||||||
|
CASE
|
||||||
|
WHEN non_unique = 0 AND index_name = 'PRIMARY' THEN 'PRIMARY'
|
||||||
|
WHEN non_unique = 0 THEN 'UNIQUE'
|
||||||
|
ELSE 'INDEX'
|
||||||
|
END AS type,
|
||||||
|
GROUP_CONCAT(column_name ORDER BY seq_in_index) AS columns
|
||||||
|
FROM
|
||||||
|
information_schema.statistics
|
||||||
|
WHERE
|
||||||
|
table_schema = ?
|
||||||
|
AND table_name = ?
|
||||||
|
GROUP BY
|
||||||
|
index_name, non_unique
|
||||||
|
`,
|
||||||
|
[dbName, tableName],
|
||||||
|
);
|
||||||
|
|
||||||
|
await connection.end();
|
||||||
|
|
||||||
|
return (rows as any[]).map((row) => ({
|
||||||
|
relname: row.relname,
|
||||||
|
key: row.key,
|
||||||
|
type: row.type,
|
||||||
|
columns: row.columns.split(","),
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching indexes:", error);
|
||||||
|
await connection.end();
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTableForeignKeys(
|
||||||
|
credentials: Credentials,
|
||||||
|
{ dbName, tableName }: { dbName: string; tableName: string },
|
||||||
|
) {
|
||||||
|
const sql = await this.queryRunner(credentials);
|
||||||
|
|
||||||
|
const result = await sql`
|
||||||
|
SELECT
|
||||||
|
conname,
|
||||||
|
condeferrable::int AS deferrable,
|
||||||
|
pg_get_constraintdef(oid) AS definition
|
||||||
|
FROM
|
||||||
|
pg_constraint
|
||||||
|
WHERE
|
||||||
|
conrelid = (
|
||||||
|
SELECT pc.oid
|
||||||
|
FROM pg_class AS pc
|
||||||
|
INNER JOIN pg_namespace AS pn ON (pn.oid = pc.relnamespace)
|
||||||
|
WHERE pc.relname = ${tableName}
|
||||||
|
AND pn.nspname = ${dbName}
|
||||||
|
)
|
||||||
|
AND contype = 'f'::char
|
||||||
|
ORDER BY conkey, conname
|
||||||
|
`;
|
||||||
|
|
||||||
|
void sql.end();
|
||||||
|
|
||||||
|
return result.map((row) => {
|
||||||
|
const match = row.definition.match(
|
||||||
|
/FOREIGN KEY\s*\((.+)\)\s*REFERENCES (.+)\((.+)\)(.*)$/iy,
|
||||||
|
);
|
||||||
|
if (match) {
|
||||||
|
const sourceColumns = match[1]
|
||||||
|
.split(",")
|
||||||
|
.map((col) => col.replaceAll('"', "").trim());
|
||||||
|
const targetTableMatch = match[2].match(
|
||||||
|
/^(("([^"]|"")+"|[^"]+)\.)?"?("([^"]|"")+"|[^"]+)$/,
|
||||||
|
);
|
||||||
|
const targetTable = targetTableMatch
|
||||||
|
? targetTableMatch[0].trim()
|
||||||
|
: null;
|
||||||
|
const targetColumns = match[3]
|
||||||
|
.split(",")
|
||||||
|
.map((col) => col.replaceAll('"', "").trim());
|
||||||
|
const { onDelete, onUpdate } = this.getActions(match[4]);
|
||||||
|
return {
|
||||||
|
conname: row.conname,
|
||||||
|
deferrable: Boolean(row.deferrable),
|
||||||
|
definition: row.definition,
|
||||||
|
source: sourceColumns,
|
||||||
|
ns: targetTableMatch
|
||||||
|
? targetTableMatch[0].replaceAll('"', "").trim()
|
||||||
|
: null,
|
||||||
|
table: targetTable.replaceAll('"', ""),
|
||||||
|
target: targetColumns,
|
||||||
|
on_delete: onDelete ?? "NO ACTION",
|
||||||
|
on_update: onUpdate ?? "NO ACTION",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async executeQuery(credentials: Credentials, query: string) {
|
||||||
|
const sql = await this.queryRunner(credentials);
|
||||||
|
|
||||||
|
const result = await sql.unsafe(query);
|
||||||
|
|
||||||
|
void sql.end();
|
||||||
|
|
||||||
|
return {
|
||||||
|
count: result.length,
|
||||||
|
data: result,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const mySQLDriver = new MySQLDriver();
|
||||||
@@ -180,7 +180,7 @@ export class PostgresDriver implements Driver {
|
|||||||
void sql.end();
|
void sql.end();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
count: count.count,
|
count: Number.parseInt(count.count, 10),
|
||||||
data,
|
data,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import cors from "@elysiajs/cors";
|
import cors from "@elysiajs/cors";
|
||||||
import { jwt } from "@elysiajs/jwt";
|
import { jwt } from "@elysiajs/jwt";
|
||||||
import { Elysia, t } from "elysia";
|
import { Elysia, t } from "elysia";
|
||||||
|
import type { Driver } from "./drivers/driver.interface";
|
||||||
|
import { mySQLDriver } from "./drivers/mysql";
|
||||||
import { postgresDriver } from "./drivers/postgres";
|
import { postgresDriver } from "./drivers/postgres";
|
||||||
|
|
||||||
const credentialsSchema = t.Union([
|
const credentialsSchema = t.Union([
|
||||||
@@ -15,8 +17,21 @@ const credentialsSchema = t.Union([
|
|||||||
}),
|
}),
|
||||||
t.Object({
|
t.Object({
|
||||||
connectionString: t.String(),
|
connectionString: t.String(),
|
||||||
|
type: t.String(),
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const getDriver = (type: string): Driver => {
|
||||||
|
switch (type) {
|
||||||
|
case "mysql":
|
||||||
|
return mySQLDriver;
|
||||||
|
case "postgres":
|
||||||
|
return postgresDriver;
|
||||||
|
default:
|
||||||
|
throw new Error("Invalid type");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const app = new Elysia({ prefix: "/api" })
|
const app = new Elysia({ prefix: "/api" })
|
||||||
.use(
|
.use(
|
||||||
jwt({
|
jwt({
|
||||||
@@ -29,7 +44,9 @@ const app = new Elysia({ prefix: "/api" })
|
|||||||
.post(
|
.post(
|
||||||
"/auth/login",
|
"/auth/login",
|
||||||
async ({ body, jwt, cookie: { auth } }) => {
|
async ({ body, jwt, cookie: { auth } }) => {
|
||||||
const databases = await postgresDriver.getAllDatabases(body);
|
const driver = getDriver(body.type);
|
||||||
|
|
||||||
|
const databases = await driver.getAllDatabases(body);
|
||||||
|
|
||||||
auth.set({
|
auth.set({
|
||||||
value: await jwt.sign(body),
|
value: await jwt.sign(body),
|
||||||
@@ -42,13 +59,15 @@ const app = new Elysia({ prefix: "/api" })
|
|||||||
)
|
)
|
||||||
.get("/databases", async ({ jwt, set, cookie: { auth } }) => {
|
.get("/databases", async ({ jwt, set, cookie: { auth } }) => {
|
||||||
const credentials = await jwt.verify(auth.value);
|
const credentials = await jwt.verify(auth.value);
|
||||||
|
console.log(auth.value);
|
||||||
if (!credentials) {
|
if (!credentials) {
|
||||||
set.status = 401;
|
set.status = 401;
|
||||||
return "Unauthorized";
|
return "Unauthorized";
|
||||||
}
|
}
|
||||||
|
const driver = getDriver(credentials.type);
|
||||||
|
|
||||||
|
const databases = await driver.getAllDatabases(credentials);
|
||||||
|
|
||||||
const databases = await postgresDriver.getAllDatabases(credentials);
|
|
||||||
return new Response(JSON.stringify(databases, null, 2)).json();
|
return new Response(JSON.stringify(databases, null, 2)).json();
|
||||||
})
|
})
|
||||||
.get(
|
.get(
|
||||||
@@ -62,8 +81,8 @@ const app = new Elysia({ prefix: "/api" })
|
|||||||
set.status = 401;
|
set.status = 401;
|
||||||
return "Unauthorized";
|
return "Unauthorized";
|
||||||
}
|
}
|
||||||
|
const driver = getDriver(credentials.type);
|
||||||
const tables = await postgresDriver.getAllTables(credentials, {
|
const tables = await driver.getAllTables(credentials, {
|
||||||
dbName,
|
dbName,
|
||||||
sortField,
|
sortField,
|
||||||
sortDesc: sortDesc === "true",
|
sortDesc: sortDesc === "true",
|
||||||
@@ -73,7 +92,7 @@ const app = new Elysia({ prefix: "/api" })
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
.get(
|
.get(
|
||||||
"databases/:dbName/tables/:tableName/data",
|
"/databases/:dbName/tables/:tableName/data",
|
||||||
async ({ query, params, jwt, set, cookie: { auth } }) => {
|
async ({ query, params, jwt, set, cookie: { auth } }) => {
|
||||||
const { tableName, dbName } = params;
|
const { tableName, dbName } = params;
|
||||||
const { perPage = "50", page = "0", sortField, sortDesc } = query;
|
const { perPage = "50", page = "0", sortField, sortDesc } = query;
|
||||||
@@ -83,8 +102,9 @@ const app = new Elysia({ prefix: "/api" })
|
|||||||
set.status = 401;
|
set.status = 401;
|
||||||
return "Unauthorized";
|
return "Unauthorized";
|
||||||
}
|
}
|
||||||
|
const driver = getDriver(credentials.type);
|
||||||
|
|
||||||
return postgresDriver.getTableData(credentials, {
|
return driver.getTableData(credentials, {
|
||||||
tableName,
|
tableName,
|
||||||
dbName,
|
dbName,
|
||||||
perPage: Number.parseInt(perPage, 10),
|
perPage: Number.parseInt(perPage, 10),
|
||||||
@@ -95,7 +115,7 @@ const app = new Elysia({ prefix: "/api" })
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
.get(
|
.get(
|
||||||
"databases/:dbName/tables/:tableName/columns",
|
"/databases/:dbName/tables/:tableName/columns",
|
||||||
async ({ params, jwt, set, cookie: { auth } }) => {
|
async ({ params, jwt, set, cookie: { auth } }) => {
|
||||||
const { tableName, dbName } = params;
|
const { tableName, dbName } = params;
|
||||||
const credentials = await jwt.verify(auth.value);
|
const credentials = await jwt.verify(auth.value);
|
||||||
@@ -105,7 +125,9 @@ const app = new Elysia({ prefix: "/api" })
|
|||||||
return "Unauthorized";
|
return "Unauthorized";
|
||||||
}
|
}
|
||||||
|
|
||||||
const columns = await postgresDriver.getTableColumns(credentials, {
|
const driver = getDriver(credentials.type);
|
||||||
|
|
||||||
|
const columns = await driver.getTableColumns(credentials, {
|
||||||
dbName,
|
dbName,
|
||||||
tableName,
|
tableName,
|
||||||
});
|
});
|
||||||
@@ -113,7 +135,7 @@ const app = new Elysia({ prefix: "/api" })
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
.get(
|
.get(
|
||||||
"databases/:dbName/tables/:tableName/indexes",
|
"/databases/:dbName/tables/:tableName/indexes",
|
||||||
async ({ params, jwt, set, cookie: { auth } }) => {
|
async ({ params, jwt, set, cookie: { auth } }) => {
|
||||||
const { tableName, dbName } = params;
|
const { tableName, dbName } = params;
|
||||||
const credentials = await jwt.verify(auth.value);
|
const credentials = await jwt.verify(auth.value);
|
||||||
@@ -123,7 +145,9 @@ const app = new Elysia({ prefix: "/api" })
|
|||||||
return "Unauthorized";
|
return "Unauthorized";
|
||||||
}
|
}
|
||||||
|
|
||||||
const indexes = await postgresDriver.getTableIndexes(credentials, {
|
const driver = getDriver(credentials.type);
|
||||||
|
|
||||||
|
const indexes = await driver.getTableIndexes(credentials, {
|
||||||
dbName,
|
dbName,
|
||||||
tableName,
|
tableName,
|
||||||
});
|
});
|
||||||
@@ -131,7 +155,7 @@ const app = new Elysia({ prefix: "/api" })
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
.get(
|
.get(
|
||||||
"databases/:dbName/tables/:tableName/foreign-keys",
|
"/databases/:dbName/tables/:tableName/foreign-keys",
|
||||||
async ({ params, jwt, set, cookie: { auth } }) => {
|
async ({ params, jwt, set, cookie: { auth } }) => {
|
||||||
const { tableName, dbName } = params;
|
const { tableName, dbName } = params;
|
||||||
const credentials = await jwt.verify(auth.value);
|
const credentials = await jwt.verify(auth.value);
|
||||||
@@ -141,19 +165,18 @@ const app = new Elysia({ prefix: "/api" })
|
|||||||
return "Unauthorized";
|
return "Unauthorized";
|
||||||
}
|
}
|
||||||
|
|
||||||
const foreignKeys = await postgresDriver.getTableForeignKeys(
|
const driver = getDriver(credentials.type);
|
||||||
credentials,
|
|
||||||
{
|
const foreignKeys = await driver.getTableForeignKeys(credentials, {
|
||||||
dbName,
|
dbName,
|
||||||
tableName,
|
tableName,
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
return new Response(JSON.stringify(foreignKeys, null, 2)).json();
|
return new Response(JSON.stringify(foreignKeys, null, 2)).json();
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.post(
|
.post(
|
||||||
"raw",
|
"/raw",
|
||||||
async ({ body, jwt, set, cookie: { auth } }) => {
|
async ({ body, jwt, set, cookie: { auth } }) => {
|
||||||
const credentials = await jwt.verify(auth.value);
|
const credentials = await jwt.verify(auth.value);
|
||||||
|
|
||||||
@@ -161,9 +184,10 @@ const app = new Elysia({ prefix: "/api" })
|
|||||||
set.status = 401;
|
set.status = 401;
|
||||||
return "Unauthorized";
|
return "Unauthorized";
|
||||||
}
|
}
|
||||||
|
const driver = getDriver(credentials.type);
|
||||||
|
|
||||||
const { query } = body;
|
const { query } = body;
|
||||||
return await postgresDriver.executeQuery(credentials, query);
|
return await driver.executeQuery(credentials, query);
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
body: t.Object({
|
body: t.Object({
|
||||||
@@ -182,3 +206,5 @@ const app = new Elysia({ prefix: "/api" })
|
|||||||
console.log(
|
console.log(
|
||||||
`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`,
|
`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export type AppType = typeof app;
|
||||||
|
|||||||
487
api/src/test/index.test.ts
Normal file
487
api/src/test/index.test.ts
Normal file
@@ -0,0 +1,487 @@
|
|||||||
|
import { describe, expect, it } from "bun:test";
|
||||||
|
import { edenFetch } from "@elysiajs/eden";
|
||||||
|
import cookie from "cookie";
|
||||||
|
import jwt from "jsonwebtoken";
|
||||||
|
import type { AppType } from "../index";
|
||||||
|
|
||||||
|
const fetch = edenFetch<AppType>("http://localhost:3000");
|
||||||
|
|
||||||
|
const pgCookie = cookie.serialize(
|
||||||
|
"auth",
|
||||||
|
jwt.sign(
|
||||||
|
// {
|
||||||
|
// type: "postgres",
|
||||||
|
// username: "postgres",
|
||||||
|
// password: "mysecretpassword",
|
||||||
|
// host: "localhost",
|
||||||
|
// port: "5432",
|
||||||
|
// database: "postgres",
|
||||||
|
// ssl: "prefer",
|
||||||
|
// },
|
||||||
|
{
|
||||||
|
type: "postgres",
|
||||||
|
connectionString:
|
||||||
|
"postgresql://flashcards_owner:pBYW18waUHtV@ep-gentle-heart-a225yqws.eu-central-1.aws.neon.tech/flashcards?sslmode=require&schema=flashcards",
|
||||||
|
},
|
||||||
|
"Fischl von Luftschloss Narfidort",
|
||||||
|
{ noTimestamp: true },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const mysqlCookie = cookie.serialize(
|
||||||
|
"auth",
|
||||||
|
jwt.sign(
|
||||||
|
{
|
||||||
|
type: "mysql",
|
||||||
|
username: "root",
|
||||||
|
password: "mysecretpassword",
|
||||||
|
host: "localhost",
|
||||||
|
port: "3306",
|
||||||
|
database: "mysql",
|
||||||
|
ssl: "prefer",
|
||||||
|
},
|
||||||
|
"Fischl von Luftschloss Narfidort",
|
||||||
|
{ noTimestamp: true },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
describe("/auth/login", () => {
|
||||||
|
it("should log in correctly with PostgreSQL", async () => {
|
||||||
|
const res = await fetch("/api/auth/login", {
|
||||||
|
method: "POST",
|
||||||
|
body: {
|
||||||
|
type: "postgres",
|
||||||
|
username: "postgres",
|
||||||
|
password: "mysecretpassword",
|
||||||
|
host: "localhost",
|
||||||
|
port: "5432",
|
||||||
|
database: "postgres",
|
||||||
|
ssl: "prefer",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.status).toEqual(200);
|
||||||
|
expect(res.data?.databases).toEqual([
|
||||||
|
"pg_toast",
|
||||||
|
"pg_catalog",
|
||||||
|
"public",
|
||||||
|
"information_schema",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should log in correctly with MySQL", async () => {
|
||||||
|
const res = await fetch("/api/auth/login", {
|
||||||
|
method: "POST",
|
||||||
|
body: {
|
||||||
|
type: "mysql",
|
||||||
|
username: "root",
|
||||||
|
password: "mysecretpassword",
|
||||||
|
host: "localhost",
|
||||||
|
port: "3306",
|
||||||
|
database: "mysql",
|
||||||
|
ssl: "prefer",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(res.status).toEqual(200);
|
||||||
|
expect(res.data?.databases).toEqual([
|
||||||
|
"information_schema",
|
||||||
|
"mysql",
|
||||||
|
"performance_schema",
|
||||||
|
"sys",
|
||||||
|
"test_db",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("/databases", () => {
|
||||||
|
it("should return correct data from PostgreSQL", async () => {
|
||||||
|
const res = await fetch("/api/databases", {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
cookie: pgCookie,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(res.status).toEqual(200);
|
||||||
|
expect(res.data).toEqual([
|
||||||
|
"pg_toast",
|
||||||
|
"pg_catalog",
|
||||||
|
"public",
|
||||||
|
"information_schema",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return correct data from MySQL", async () => {
|
||||||
|
const res = await fetch("/api/databases", {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
cookie: mysqlCookie,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(res.status).toEqual(200);
|
||||||
|
expect(res.data).toEqual([
|
||||||
|
"information_schema",
|
||||||
|
"mysql",
|
||||||
|
"performance_schema",
|
||||||
|
"sys",
|
||||||
|
"test_db",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("/databases/:dbName/tables", () => {
|
||||||
|
it("should return correct data from PostgreSQL", async () => {
|
||||||
|
const res = await fetch("/api/databases/:dbName/tables", {
|
||||||
|
params: {
|
||||||
|
dbName: "public",
|
||||||
|
},
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
cookie: pgCookie,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(res.status).toEqual(200);
|
||||||
|
for (const table of res.data) {
|
||||||
|
expect(table).toContainAllKeys([
|
||||||
|
"comments",
|
||||||
|
"index_size",
|
||||||
|
"indexes",
|
||||||
|
"owner",
|
||||||
|
"primary_key",
|
||||||
|
"row_count",
|
||||||
|
"schema_name",
|
||||||
|
"table_name",
|
||||||
|
"table_size",
|
||||||
|
"total_size",
|
||||||
|
]);
|
||||||
|
expect(table.comments).toBeString();
|
||||||
|
expect(table.index_size).toBeNumber();
|
||||||
|
expect(table.indexes).toBeString();
|
||||||
|
expect(
|
||||||
|
typeof table.owner === "string" || table.owner === null,
|
||||||
|
).toBeTrue();
|
||||||
|
expect(table.primary_key).toBeString();
|
||||||
|
expect(table.row_count).toBeNumber();
|
||||||
|
expect(table.schema_name).toBeString();
|
||||||
|
expect(table.table_name).toBeString();
|
||||||
|
expect(table.table_size).toBeNumber();
|
||||||
|
expect(table.total_size).toBeNumber();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return correct data from MySQL", async () => {
|
||||||
|
const res = await fetch("/api/databases/:dbName/tables", {
|
||||||
|
params: {
|
||||||
|
dbName: "test_db",
|
||||||
|
},
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
cookie: mysqlCookie,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(res.status).toEqual(200);
|
||||||
|
for (const table of res.data) {
|
||||||
|
expect(table).toContainAllKeys([
|
||||||
|
"comments",
|
||||||
|
"index_size",
|
||||||
|
"indexes",
|
||||||
|
"owner",
|
||||||
|
"primary_key",
|
||||||
|
"row_count",
|
||||||
|
"schema_name",
|
||||||
|
"table_name",
|
||||||
|
"table_size",
|
||||||
|
"total_size",
|
||||||
|
]);
|
||||||
|
expect(table.comments).toBeString();
|
||||||
|
expect(table.index_size).toBeNumber();
|
||||||
|
expect(table.indexes).toBeString();
|
||||||
|
expect(
|
||||||
|
typeof table.owner === "string" || table.owner === null,
|
||||||
|
).toBeTrue();
|
||||||
|
expect(table.primary_key).toBeString();
|
||||||
|
expect(table.row_count).toBeNumber();
|
||||||
|
expect(table.schema_name).toBeString();
|
||||||
|
expect(table.table_name).toBeString();
|
||||||
|
expect(table.table_size).toBeNumber();
|
||||||
|
expect(table.total_size).toBeNumber();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("databases/:dbName/tables/:tableName/data", () => {
|
||||||
|
it("should return correct data from PostgreSQL", async () => {
|
||||||
|
const res = await fetch("/api/databases/:dbName/tables/:tableName/data", {
|
||||||
|
params: {
|
||||||
|
dbName: "public",
|
||||||
|
tableName: "test_table",
|
||||||
|
},
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
cookie: pgCookie,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(res.status).toEqual(200);
|
||||||
|
|
||||||
|
expect(res.data).not.toEqual("Unauthorized");
|
||||||
|
if (res.data === "Unauthorized") return;
|
||||||
|
|
||||||
|
expect(res.data).toBeDefined();
|
||||||
|
if (!res.data) return;
|
||||||
|
|
||||||
|
expect(res.data?.data.length).toBeGreaterThan(0);
|
||||||
|
expect(res.data?.count).toEqual(res.data?.data.length);
|
||||||
|
|
||||||
|
for (const row of res.data.data) {
|
||||||
|
expect(row).toBeObject();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return correct data from MySQL", async () => {
|
||||||
|
const res = await fetch("/api/databases/:dbName/tables/:tableName/data", {
|
||||||
|
params: {
|
||||||
|
dbName: "test_db",
|
||||||
|
tableName: "test_table",
|
||||||
|
},
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
cookie: mysqlCookie,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(res.status).toEqual(200);
|
||||||
|
|
||||||
|
expect(res.data).not.toEqual("Unauthorized");
|
||||||
|
if (res.data === "Unauthorized") return;
|
||||||
|
|
||||||
|
expect(res.data).toBeDefined();
|
||||||
|
if (!res.data) return;
|
||||||
|
|
||||||
|
expect(res.data?.data.length).toBeGreaterThan(0);
|
||||||
|
expect(res.data?.count).toEqual(res.data?.data.length);
|
||||||
|
|
||||||
|
for (const row of res.data.data) {
|
||||||
|
expect(row).toBeObject();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("databases/:dbName/tables/:tableName/indexes", () => {
|
||||||
|
it("should return correct data from PostgreSQL", async () => {
|
||||||
|
const res = await fetch(
|
||||||
|
"/api/databases/:dbName/tables/:tableName/indexes",
|
||||||
|
{
|
||||||
|
params: {
|
||||||
|
dbName: "public",
|
||||||
|
tableName: "test_table",
|
||||||
|
},
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
cookie: pgCookie,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
expect(res.status).toEqual(200);
|
||||||
|
|
||||||
|
expect(res.data).not.toEqual("Unauthorized");
|
||||||
|
if (res.data === "Unauthorized") return;
|
||||||
|
|
||||||
|
expect(res.data).toBeDefined();
|
||||||
|
if (!res.data) return;
|
||||||
|
|
||||||
|
expect(res.data).toBeArray();
|
||||||
|
|
||||||
|
expect(res.data[0].relname).toBeString();
|
||||||
|
expect(res.data[0].key).toBeString();
|
||||||
|
expect(res.data[0].type).toBeString();
|
||||||
|
expect(res.data[0].columns).toBeArray();
|
||||||
|
expect(res.data[0].columns[0]).toBeString();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return correct data from MySQL", async () => {
|
||||||
|
const res = await fetch(
|
||||||
|
"/api/databases/:dbName/tables/:tableName/indexes",
|
||||||
|
{
|
||||||
|
params: {
|
||||||
|
dbName: "test_db",
|
||||||
|
tableName: "test_table",
|
||||||
|
},
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
cookie: mysqlCookie,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
expect(res.status).toEqual(200);
|
||||||
|
|
||||||
|
expect(res.data).not.toEqual("Unauthorized");
|
||||||
|
if (res.data === "Unauthorized") return;
|
||||||
|
|
||||||
|
expect(res.data).toBeDefined();
|
||||||
|
if (!res.data) return;
|
||||||
|
|
||||||
|
expect(res.data).toBeArray();
|
||||||
|
|
||||||
|
expect(res.data[0].relname).toBeString();
|
||||||
|
expect(res.data[0].key).toBeString();
|
||||||
|
expect(res.data[0].type).toBeString();
|
||||||
|
expect(res.data[0].columns).toBeArray();
|
||||||
|
expect(res.data[0].columns[0]).toBeString();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("databases/:dbName/tables/:tableName/columns", () => {
|
||||||
|
it("should return correct data from PostgreSQL", async () => {
|
||||||
|
const res = await fetch(
|
||||||
|
"/api/databases/:dbName/tables/:tableName/columns",
|
||||||
|
{
|
||||||
|
params: {
|
||||||
|
dbName: "public",
|
||||||
|
tableName: "test_table",
|
||||||
|
},
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
cookie: pgCookie,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
expect(res.status).toEqual(200);
|
||||||
|
|
||||||
|
expect(res.data).not.toEqual("Unauthorized");
|
||||||
|
if (res.data === "Unauthorized") return;
|
||||||
|
|
||||||
|
expect(res.data).toBeDefined();
|
||||||
|
if (!res.data) return;
|
||||||
|
|
||||||
|
expect(res.data).toBeArray();
|
||||||
|
|
||||||
|
for (const row of res.data) {
|
||||||
|
expect(row).toContainAllKeys([
|
||||||
|
"column_name",
|
||||||
|
"data_type",
|
||||||
|
"udt_name",
|
||||||
|
"column_comment",
|
||||||
|
]);
|
||||||
|
expect(row.column_name).toBeString();
|
||||||
|
expect(row.data_type).toBeString();
|
||||||
|
expect(row.udt_name).toBeString();
|
||||||
|
expect(
|
||||||
|
row.column_comment === null || typeof row.column_comment === "string",
|
||||||
|
).toBeTrue();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return correct data from MySQL", async () => {
|
||||||
|
const res = await fetch(
|
||||||
|
"/api/databases/:dbName/tables/:tableName/columns",
|
||||||
|
{
|
||||||
|
params: {
|
||||||
|
dbName: "test_db",
|
||||||
|
tableName: "test_table",
|
||||||
|
},
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
cookie: mysqlCookie,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(res.data).toBeDefined();
|
||||||
|
if (!res.data) return;
|
||||||
|
|
||||||
|
expect(res.data).toBeArray();
|
||||||
|
|
||||||
|
for (const row of res.data) {
|
||||||
|
expect(row).toContainAllKeys([
|
||||||
|
"column_name",
|
||||||
|
"data_type",
|
||||||
|
"udt_name",
|
||||||
|
"column_comment",
|
||||||
|
]);
|
||||||
|
expect(row.column_name).toBeString();
|
||||||
|
expect(row.data_type).toBeString();
|
||||||
|
expect(row.udt_name).toBeString();
|
||||||
|
expect(
|
||||||
|
row.column_comment === null || typeof row.column_comment === "string",
|
||||||
|
).toBeTrue();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("databases/:dbName/tables/:tableName/foreign-keys", () => {
|
||||||
|
// it("should return correct data from PostgreSQL", async () => {
|
||||||
|
// const res = await fetch(
|
||||||
|
// "/api/databases/:dbName/tables/:tableName/foreign-keys",
|
||||||
|
// {
|
||||||
|
// params: {
|
||||||
|
// dbName: "public",
|
||||||
|
// tableName: "test_table",
|
||||||
|
// },
|
||||||
|
// method: "GET",
|
||||||
|
// headers: {
|
||||||
|
// cookie: pgCookie,
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// );
|
||||||
|
// expect(res.status).toEqual(200);
|
||||||
|
//
|
||||||
|
// expect(res.data).not.toEqual("Unauthorized");
|
||||||
|
// if (res.data === "Unauthorized") return;
|
||||||
|
//
|
||||||
|
// console.log(res.data);
|
||||||
|
//
|
||||||
|
// expect(res.data).toBeDefined();
|
||||||
|
// if (!res.data) return;
|
||||||
|
//
|
||||||
|
// expect(res.data).toBeArray();
|
||||||
|
//
|
||||||
|
// for (const row of res.data) {
|
||||||
|
// expect(row).toContainAllKeys([
|
||||||
|
// "column_name",
|
||||||
|
// "data_type",
|
||||||
|
// "udt_name",
|
||||||
|
// "column_comment",
|
||||||
|
// ]);
|
||||||
|
// expect(row.column_name).toBeString();
|
||||||
|
// expect(row.data_type).toBeString();
|
||||||
|
// expect(row.udt_name).toBeString();
|
||||||
|
// expect(
|
||||||
|
// row.column_comment === null || typeof row.column_comment === "string",
|
||||||
|
// ).toBeTrue();
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// it("should return correct data from MySQL", async () => {
|
||||||
|
// const res = await fetch(
|
||||||
|
// "/api/databases/:dbName/tables/:tableName/columns",
|
||||||
|
// {
|
||||||
|
// params: {
|
||||||
|
// dbName: "test_db",
|
||||||
|
// tableName: "test_table",
|
||||||
|
// },
|
||||||
|
// method: "GET",
|
||||||
|
// headers: {
|
||||||
|
// cookie: mysqlCookie,
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// );
|
||||||
|
// console.log(res.data);
|
||||||
|
//
|
||||||
|
// expect(res.data).toBeDefined();
|
||||||
|
// if (!res.data) return;
|
||||||
|
//
|
||||||
|
// expect(res.data).toBeArray();
|
||||||
|
//
|
||||||
|
// for (const row of res.data) {
|
||||||
|
// expect(row).toContainAllKeys([
|
||||||
|
// "column_name",
|
||||||
|
// "data_type",
|
||||||
|
// "udt_name",
|
||||||
|
// "column_comment",
|
||||||
|
// ]);
|
||||||
|
// expect(row.column_name).toBeString();
|
||||||
|
// expect(row.data_type).toBeString();
|
||||||
|
// expect(row.udt_name).toBeString();
|
||||||
|
// expect(
|
||||||
|
// row.column_comment === null || typeof row.column_comment === "string",
|
||||||
|
// ).toBeTrue();
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
});
|
||||||
@@ -26,6 +26,23 @@ export const Route = createFileRoute("/auth/login")({
|
|||||||
component: LoginForm,
|
component: LoginForm,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function DatabaseTypeSelector() {
|
||||||
|
return (
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="dbType">Database type</Label>
|
||||||
|
<Select defaultValue={"postgres"} name={"type"}>
|
||||||
|
<SelectTrigger className="w-full" id={"dbType"}>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="postgres">Postgres</SelectItem>
|
||||||
|
<SelectItem value="mysql">MySQL</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function LoginForm() {
|
function LoginForm() {
|
||||||
const [connectionMethod, setConnectionMethod] =
|
const [connectionMethod, setConnectionMethod] =
|
||||||
useState<string>("connectionString");
|
useState<string>("connectionString");
|
||||||
@@ -36,12 +53,17 @@ function LoginForm() {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const formData = new FormData(e.currentTarget);
|
const formData = new FormData(e.currentTarget);
|
||||||
const connectionString = formData.get("connectionString");
|
const connectionString = formData.get("connectionString");
|
||||||
|
const type = formData.get("type");
|
||||||
if (connectionMethod === "connectionString") {
|
if (connectionMethod === "connectionString") {
|
||||||
if (connectionString != null && typeof connectionString === "string") {
|
if (
|
||||||
|
connectionString != null &&
|
||||||
|
typeof connectionString === "string" &&
|
||||||
|
type != null &&
|
||||||
|
typeof type === "string"
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
await mutateAsync({ connectionString });
|
await mutateAsync({ connectionString, type });
|
||||||
addSession({ connectionString });
|
addSession({ connectionString, type });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
toast.error("Invalid connection string");
|
toast.error("Invalid connection string");
|
||||||
@@ -56,7 +78,6 @@ function LoginForm() {
|
|||||||
const username = formData.get("username");
|
const username = formData.get("username");
|
||||||
const password = formData.get("password");
|
const password = formData.get("password");
|
||||||
const host = formData.get("host");
|
const host = formData.get("host");
|
||||||
const type = formData.get("type");
|
|
||||||
const port = formData.get("port");
|
const port = formData.get("port");
|
||||||
const database = formData.get("database");
|
const database = formData.get("database");
|
||||||
const ssl = formData.get("ssl");
|
const ssl = formData.get("ssl");
|
||||||
@@ -138,17 +159,7 @@ function LoginForm() {
|
|||||||
</ToggleGroup>
|
</ToggleGroup>
|
||||||
{connectionMethod === "fields" ? (
|
{connectionMethod === "fields" ? (
|
||||||
<>
|
<>
|
||||||
<div className="grid gap-2">
|
<DatabaseTypeSelector />
|
||||||
<Label htmlFor="dbType">Database type</Label>
|
|
||||||
<Select defaultValue={"postgres"} name={"type"}>
|
|
||||||
<SelectTrigger className="w-full" id={"dbType"}>
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="postgres">Postgres</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label htmlFor="host">Host</Label>
|
<Label htmlFor="host">Host</Label>
|
||||||
<Input
|
<Input
|
||||||
@@ -212,6 +223,7 @@ function LoginForm() {
|
|||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
|
<DatabaseTypeSelector />
|
||||||
<Label htmlFor="connectionString">Connection string</Label>
|
<Label htmlFor="connectionString">Connection string</Label>
|
||||||
<Input
|
<Input
|
||||||
name="connectionString"
|
name="connectionString"
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export type LoginArgs =
|
|||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
connectionString: string;
|
connectionString: string;
|
||||||
|
type: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type LoginResponse = {
|
export type LoginResponse = {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ type SessionFields = {
|
|||||||
type SessionConnectionString = {
|
type SessionConnectionString = {
|
||||||
id: number;
|
id: number;
|
||||||
connectionString: string;
|
connectionString: string;
|
||||||
|
type: string;
|
||||||
};
|
};
|
||||||
type Session = SessionFields | SessionConnectionString;
|
type Session = SessionFields | SessionConnectionString;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user