mirror of
https://github.com/ershisan99/db-studio.git
synced 2025-12-16 12:33:05 +00:00
add raw query editor and response table
This commit is contained in:
@@ -6,7 +6,10 @@
|
|||||||
"linter": {
|
"linter": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"rules": {
|
"rules": {
|
||||||
"recommended": true
|
"recommended": true,
|
||||||
|
"suspicious": {
|
||||||
|
"noExplicitAny": "warn"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"formatter": {
|
"formatter": {
|
||||||
|
|||||||
Binary file not shown.
@@ -29,6 +29,7 @@
|
|||||||
"lucide-react": "^0.400.0",
|
"lucide-react": "^0.400.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
|
"sonner": "^1.5.0",
|
||||||
"tailwind-merge": "^2.3.0",
|
"tailwind-merge": "^2.3.0",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"zod": "^3.23.8",
|
"zod": "^3.23.8",
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui";
|
} from "@/components/ui";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn, isImageUrl, isUrl } from "@/lib/utils";
|
||||||
import { useTableColumnsQuery, useTableDataQuery } from "@/services/db";
|
import { useTableColumnsQuery, useTableDataQuery } from "@/services/db";
|
||||||
import { useSettingsStore } from "@/state";
|
import { useSettingsStore } from "@/state";
|
||||||
import {
|
import {
|
||||||
@@ -27,19 +27,6 @@ import {
|
|||||||
} from "@tanstack/react-table";
|
} from "@tanstack/react-table";
|
||||||
import { ArrowUp, Rows3 } from "lucide-react";
|
import { ArrowUp, Rows3 } from "lucide-react";
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
function isUrl(value: string) {
|
|
||||||
return z.string().url().safeParse(value).success;
|
|
||||||
}
|
|
||||||
|
|
||||||
const imageUrlRegex = new RegExp(
|
|
||||||
/(http)?s?:?(\/\/[^"']*\.(?:png|jpg|jpeg|gif|svg|webp|bmp))/i,
|
|
||||||
);
|
|
||||||
|
|
||||||
function isImageUrl(value: string) {
|
|
||||||
return value.match(imageUrlRegex);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DataTable = ({
|
export const DataTable = ({
|
||||||
tableName,
|
tableName,
|
||||||
|
|||||||
@@ -6,5 +6,8 @@ export * from "./input";
|
|||||||
export * from "./label";
|
export * from "./label";
|
||||||
export * from "./mode-toggle";
|
export * from "./mode-toggle";
|
||||||
export * from "./select";
|
export * from "./select";
|
||||||
|
export * from "./sonner";
|
||||||
|
export * from "./sql-data-table";
|
||||||
|
export * from "./sql-data-table-cell";
|
||||||
export * from "./switch";
|
export * from "./switch";
|
||||||
export * from "./table";
|
export * from "./table";
|
||||||
|
|||||||
29
frontend/src/components/ui/sonner.tsx
Normal file
29
frontend/src/components/ui/sonner.tsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { useUiStore } from "@/state";
|
||||||
|
import { Toaster as Sonner } from "sonner";
|
||||||
|
|
||||||
|
type ToasterProps = React.ComponentProps<typeof Sonner>;
|
||||||
|
|
||||||
|
const Toaster = ({ ...props }: ToasterProps) => {
|
||||||
|
const theme = useUiStore.use.theme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Sonner
|
||||||
|
theme={theme as ToasterProps["theme"]}
|
||||||
|
className="toaster group"
|
||||||
|
toastOptions={{
|
||||||
|
classNames: {
|
||||||
|
toast:
|
||||||
|
"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
|
||||||
|
description: "group-[.toast]:text-muted-foreground",
|
||||||
|
actionButton:
|
||||||
|
"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
|
||||||
|
cancelButton:
|
||||||
|
"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { Toaster };
|
||||||
55
frontend/src/components/ui/sql-data-table-cell.tsx
Normal file
55
frontend/src/components/ui/sql-data-table-cell.tsx
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { cn, isImageUrl, isUrl } from "@/lib/utils";
|
||||||
|
import { useSettingsStore } from "@/state";
|
||||||
|
import type { Row } from "@tanstack/react-table";
|
||||||
|
import { memo } from "react";
|
||||||
|
|
||||||
|
type Props<T> = {
|
||||||
|
row: Row<T>;
|
||||||
|
column_name: string;
|
||||||
|
isDate?: boolean;
|
||||||
|
isNumber?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
function RawSqlDataTableCell<T>({
|
||||||
|
isNumber,
|
||||||
|
row,
|
||||||
|
column_name,
|
||||||
|
isDate,
|
||||||
|
}: Props<T>) {
|
||||||
|
const formatDates = useSettingsStore.use.formatDates();
|
||||||
|
const showImagesPreview = useSettingsStore.use.showImagesPreview();
|
||||||
|
const value = row.getValue(column_name) as any;
|
||||||
|
let finalValue = value;
|
||||||
|
if (formatDates && isDate) {
|
||||||
|
finalValue = new Date(value as string).toLocaleString();
|
||||||
|
}
|
||||||
|
if (showImagesPreview && typeof value === "string" && isUrl(value)) {
|
||||||
|
const isImage = isImageUrl(value);
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
href={value}
|
||||||
|
target={"_blank"}
|
||||||
|
className={cn("hover:underline")}
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
<div className={"flex items-center justify-between break-all gap-4"}>
|
||||||
|
{value}
|
||||||
|
{isImage && (
|
||||||
|
<img src={value} alt={"preview"} className="size-20 object-cover" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className={cn("break-all", isNumber && "text-right")}>
|
||||||
|
{finalValue}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const SqlDataTableCell = memo(RawSqlDataTableCell);
|
||||||
|
|
||||||
|
SqlDataTableCell.displayName = "SqlDataTableCell";
|
||||||
|
|
||||||
|
export { SqlDataTableCell };
|
||||||
123
frontend/src/components/ui/sql-data-table.tsx
Normal file
123
frontend/src/components/ui/sql-data-table.tsx
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "@/components/ui/table";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { flexRender } from "@tanstack/react-table";
|
||||||
|
import type { Table as TableType } from "@tanstack/react-table";
|
||||||
|
import { ArrowUp } from "lucide-react";
|
||||||
|
import { type ComponentPropsWithoutRef, memo } from "react";
|
||||||
|
|
||||||
|
type Props<TData> = {
|
||||||
|
table: TableType<TData>;
|
||||||
|
columnsLength: number;
|
||||||
|
} & ComponentPropsWithoutRef<typeof Table>;
|
||||||
|
|
||||||
|
function RawSqlDataTable<TData>({
|
||||||
|
table,
|
||||||
|
columnsLength,
|
||||||
|
style,
|
||||||
|
...rest
|
||||||
|
}: Props<TData>) {
|
||||||
|
return (
|
||||||
|
<Table
|
||||||
|
style={{
|
||||||
|
width: table.getCenterTotalSize(),
|
||||||
|
...style,
|
||||||
|
}}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
<TableHeader>
|
||||||
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
|
<TableRow key={headerGroup.id}>
|
||||||
|
{headerGroup.headers.map((header) => {
|
||||||
|
const sorted = header.column.getIsSorted();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableHead
|
||||||
|
className={"p-0 relative"}
|
||||||
|
key={header.id}
|
||||||
|
style={{
|
||||||
|
width: header.getSize(),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={header.column.getToggleSortingHandler()}
|
||||||
|
title={
|
||||||
|
header.column.getNextSortingOrder() === "asc"
|
||||||
|
? "Sort ascending"
|
||||||
|
: header.column.getNextSortingOrder() === "desc"
|
||||||
|
? "Sort descending"
|
||||||
|
: "Clear sort"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{header.isPlaceholder
|
||||||
|
? null
|
||||||
|
: flexRender(
|
||||||
|
header.column.columnDef.header,
|
||||||
|
header.getContext(),
|
||||||
|
)}
|
||||||
|
<ArrowUp
|
||||||
|
className={cn(
|
||||||
|
"ml-2 size-4 opacity-0 transition-transform",
|
||||||
|
sorted && "opacity-100",
|
||||||
|
(sorted as string) === "desc" && "rotate-180",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
<div
|
||||||
|
{...{
|
||||||
|
onDoubleClick: () => header.column.resetSize(),
|
||||||
|
onMouseDown: header.getResizeHandler(),
|
||||||
|
onTouchStart: header.getResizeHandler(),
|
||||||
|
className: `resizer ${header.column.getIsResizing() ? "isResizing" : ""}`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</TableHead>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{table.getRowModel().rows?.length ? (
|
||||||
|
table.getRowModel().rows.map((row) => (
|
||||||
|
<TableRow
|
||||||
|
key={row.id}
|
||||||
|
data-state={row.getIsSelected() && "selected"}
|
||||||
|
>
|
||||||
|
{row.getVisibleCells().map((cell) => (
|
||||||
|
<TableCell
|
||||||
|
key={cell.id}
|
||||||
|
style={{
|
||||||
|
width: cell.column.getSize(),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={columnsLength} className="h-24 text-center">
|
||||||
|
No results.
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const SqlDataTable = memo(RawSqlDataTable);
|
||||||
|
|
||||||
|
SqlDataTable.displayName = "SqlDataTable";
|
||||||
|
|
||||||
|
export { SqlDataTable };
|
||||||
@@ -1,24 +1,44 @@
|
|||||||
import { type ClassValue, clsx } from 'clsx'
|
import { type ClassValue, clsx } from "clsx";
|
||||||
import { twMerge } from 'tailwind-merge'
|
import { twMerge } from "tailwind-merge";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs))
|
return twMerge(clsx(inputs));
|
||||||
}
|
}
|
||||||
|
|
||||||
type Valuable<T> = { [K in keyof T as T[K] extends null | undefined ? never : K]: T[K] }
|
type Valuable<T> = {
|
||||||
|
[K in keyof T as T[K] extends null | undefined ? never : K]: T[K];
|
||||||
|
};
|
||||||
|
|
||||||
export function getValuable<T extends object, V = Valuable<T>>(obj: T): V {
|
export function getValuable<T extends object, V = Valuable<T>>(obj: T): V {
|
||||||
return Object.fromEntries(
|
return Object.fromEntries(
|
||||||
Object.entries(obj).filter(
|
Object.entries(obj).filter(
|
||||||
([, v]) => !((typeof v === 'string' && !v.length) || v === null || typeof v === 'undefined')
|
([, v]) =>
|
||||||
)
|
!(
|
||||||
) as V
|
(typeof v === "string" && !v.length) ||
|
||||||
|
v === null ||
|
||||||
|
typeof v === "undefined"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
) as V;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function prettyBytes(bytes: number): string {
|
export function prettyBytes(bytes: number): string {
|
||||||
const units = ['B', 'kB', 'MB', 'GB', 'TB', 'PB']
|
const units = ["B", "kB", "MB", "GB", "TB", "PB"];
|
||||||
if (bytes === 0) return '0 B'
|
if (bytes === 0) return "0 B";
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(1024))
|
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||||
const size = bytes / Math.pow(1024, i)
|
const size = bytes / 1024 ** i;
|
||||||
return `${size.toFixed(2)} ${units[i]}`
|
return `${size.toFixed(2)} ${units[i]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isUrl(value: string) {
|
||||||
|
return z.string().url().safeParse(value).success;
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageUrlRegex = new RegExp(
|
||||||
|
/(http)?s?:?(\/\/[^"']*\.(?:png|jpg|jpeg|gif|svg|webp|bmp))/i,
|
||||||
|
);
|
||||||
|
|
||||||
|
export function isImageUrl(value: string) {
|
||||||
|
return value.match(imageUrlRegex);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import "@fontsource/inter/600.css";
|
|||||||
import "@fontsource/inter/700.css";
|
import "@fontsource/inter/700.css";
|
||||||
import "@fontsource/inter/800.css";
|
import "@fontsource/inter/800.css";
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
|
import { Toaster } from "@/components/ui";
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
||||||
// Import the generated route tree
|
// Import the generated route tree
|
||||||
@@ -35,6 +36,7 @@ if (rootElement && !rootElement.innerHTML) {
|
|||||||
initialIsOpen={false}
|
initialIsOpen={false}
|
||||||
buttonPosition={"bottom-left"}
|
buttonPosition={"bottom-left"}
|
||||||
/>
|
/>
|
||||||
|
<Toaster richColors />
|
||||||
<RouterProvider router={router} />
|
<RouterProvider router={router} />
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
</StrictMode>,
|
</StrictMode>,
|
||||||
|
|||||||
@@ -1,16 +1,77 @@
|
|||||||
|
import { Button, SqlDataTable, SqlDataTableCell } from "@/components/ui";
|
||||||
|
import { useQueryRawSqlMutation } from "@/services/db";
|
||||||
|
import { useUiStore } from "@/state";
|
||||||
import Editor from "@monaco-editor/react";
|
import Editor from "@monaco-editor/react";
|
||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import {
|
||||||
|
type ColumnDef,
|
||||||
|
getCoreRowModel,
|
||||||
|
useReactTable,
|
||||||
|
} from "@tanstack/react-table";
|
||||||
|
import { ArrowRight } from "lucide-react";
|
||||||
|
import { useMemo, useState } from "react";
|
||||||
|
|
||||||
export const Route = createFileRoute("/raw/")({
|
export const Route = createFileRoute("/raw/")({
|
||||||
component: Component,
|
component: Component,
|
||||||
});
|
});
|
||||||
|
|
||||||
function Component() {
|
function Component() {
|
||||||
|
const [query, setQuery] = useState<string | undefined>("SELECT * FROM");
|
||||||
|
const { mutate, data } = useQueryRawSqlMutation();
|
||||||
|
const appTheme = useUiStore.use.theme();
|
||||||
|
const themeToUse = appTheme === "light" ? "light" : "vs-dark";
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
if (!query) return;
|
||||||
|
|
||||||
|
mutate({ query });
|
||||||
|
};
|
||||||
|
const columns = useMemo<ColumnDef<any>[]>(() => {
|
||||||
|
if (!data) return [] as ColumnDef<any>[];
|
||||||
|
|
||||||
|
return Object.keys(data[0]).map((key) => ({
|
||||||
|
header: key,
|
||||||
|
accessorKey: key,
|
||||||
|
cell: ({ row }) => <SqlDataTableCell row={row} column_name={key} />,
|
||||||
|
})) as ColumnDef<any>[];
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
const table = useReactTable({
|
||||||
|
data: data ?? [],
|
||||||
|
columns,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Editor
|
<div className={"h-layout w-layout p-3"}>
|
||||||
height="90vh"
|
<div className={"flex flex-col gap-4 flex-1 h-full max-h-full pb-3"}>
|
||||||
defaultLanguage="sql"
|
<Editor
|
||||||
defaultValue="SELECT * FROM table"
|
theme={themeToUse}
|
||||||
/>
|
value={query}
|
||||||
|
onChange={setQuery}
|
||||||
|
className={"h-full border"}
|
||||||
|
height={"35%"}
|
||||||
|
language="sql"
|
||||||
|
options={{
|
||||||
|
tabFocusMode: true,
|
||||||
|
detectIndentation: false,
|
||||||
|
minimap: { enabled: false },
|
||||||
|
automaticLayout: true,
|
||||||
|
scrollBeyondLastLine: false,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className={"flex gap-4 items-center justify-between"}>
|
||||||
|
{data && <p>Result: {data.length} rows</p>}
|
||||||
|
<Button disabled={!query} onClick={handleSubmit} className={"gap-2"}>
|
||||||
|
Execute Query <ArrowRight className={"size-4"} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{data && (
|
||||||
|
<div className="rounded-md border min-h-0 h-full overflow-auto w-full min-w-0">
|
||||||
|
<SqlDataTable table={table} columnsLength={columns.length} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import { useQuery } from "@tanstack/react-query";
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
|
import { HTTPError } from "ky";
|
||||||
|
import { toast } from "sonner";
|
||||||
import { DB_QUERY_KEYS } from "./db.query-keys";
|
import { DB_QUERY_KEYS } from "./db.query-keys";
|
||||||
import { dbService } from "./db.service";
|
import { dbService } from "./db.service";
|
||||||
import type {
|
import type {
|
||||||
@@ -7,6 +9,7 @@ import type {
|
|||||||
GetTableForeignKeysArgs,
|
GetTableForeignKeysArgs,
|
||||||
GetTableIndexesArgs,
|
GetTableIndexesArgs,
|
||||||
GetTablesListArgs,
|
GetTablesListArgs,
|
||||||
|
QueryRawSqlArgs,
|
||||||
} from "./db.types";
|
} from "./db.types";
|
||||||
|
|
||||||
export const useDatabasesListQuery = () => {
|
export const useDatabasesListQuery = () => {
|
||||||
@@ -75,3 +78,19 @@ export const useTableForeignKeysQuery = (args: GetTableForeignKeysArgs) => {
|
|||||||
enabled: !!args.tableName && !!args.dbName,
|
enabled: !!args.tableName && !!args.dbName,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useQueryRawSqlMutation = () => {
|
||||||
|
return useMutation({
|
||||||
|
onError: async (error) => {
|
||||||
|
if (error instanceof HTTPError) {
|
||||||
|
const errorJson = await error.response.json();
|
||||||
|
console.log(errorJson);
|
||||||
|
toast.error(errorJson.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
toast.error(error.message);
|
||||||
|
},
|
||||||
|
mutationFn: ({ query }: QueryRawSqlArgs) =>
|
||||||
|
dbService.queryRawSql({ query }),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import type {
|
|||||||
GetTableIndexesArgs,
|
GetTableIndexesArgs,
|
||||||
GetTablesListArgs,
|
GetTablesListArgs,
|
||||||
GetTablesListResponse,
|
GetTablesListResponse,
|
||||||
|
QueryRawSqlArgs,
|
||||||
|
QueryRawSqlResponse,
|
||||||
TableColumns,
|
TableColumns,
|
||||||
TableForeignKeys,
|
TableForeignKeys,
|
||||||
TableIndexes,
|
TableIndexes,
|
||||||
@@ -59,6 +61,14 @@ class DbService {
|
|||||||
.get(`api/databases/${dbName}/tables/${tableName}/foreign-keys`)
|
.get(`api/databases/${dbName}/tables/${tableName}/foreign-keys`)
|
||||||
.json<TableForeignKeys>();
|
.json<TableForeignKeys>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
queryRawSql({ query }: QueryRawSqlArgs) {
|
||||||
|
return dbInstance
|
||||||
|
.post("api/raw", {
|
||||||
|
json: { query },
|
||||||
|
})
|
||||||
|
.json<QueryRawSqlResponse>();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const dbService = new DbService();
|
export const dbService = new DbService();
|
||||||
|
|||||||
@@ -69,6 +69,13 @@ export type GetTableForeignKeysArgs = {
|
|||||||
tableName: string;
|
tableName: string;
|
||||||
dbName: string;
|
dbName: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type QueryRawSqlArgs = {
|
||||||
|
query: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type QueryRawSqlResponse = Array<Record<string, any>>;
|
||||||
|
|
||||||
export type TableForeignKey = {
|
export type TableForeignKey = {
|
||||||
conname: string;
|
conname: string;
|
||||||
deferrable: boolean;
|
deferrable: boolean;
|
||||||
@@ -80,4 +87,5 @@ export type TableForeignKey = {
|
|||||||
on_delete: string;
|
on_delete: string;
|
||||||
on_update: string;
|
on_update: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TableForeignKeys = TableForeignKey[];
|
export type TableForeignKeys = TableForeignKey[];
|
||||||
|
|||||||
Reference in New Issue
Block a user