Skip to content

Commit 119369c

Browse files
committed
feat(db): add mysql adapter
1 parent 625fb36 commit 119369c

28 files changed

Lines changed: 8966 additions & 11429 deletions

apps/desktop/main/ipc/handlers/metadata.handler.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ export function registerMetadataHandlers() {
1818
}
1919
})
2020

21+
ipcMain.handle(IPC_CHANNELS.DB_GET_ADAPTER_TYPE, async () => {
22+
if (!manager.isConnected()) throw new Error('DB not connected')
23+
24+
return manager.getAdapterType()
25+
})
26+
2127
ipcMain.handle(IPC_CHANNELS.DB_LOAD_DATABASES, async () => {
2228
if (!manager.isConnected()) throw new Error('DB not connected')
2329

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import type { DbAdapterPort } from '@datary/core'
2+
import { type DBConnectionConfig, MysqlAdapter, PostgresAdapter } from '@datary/db'
3+
4+
type AdapterConstructor = new (config: DBConnectionConfig) => DbAdapterPort
5+
6+
const adapters: Record<string, AdapterConstructor> = {
7+
postgresql: PostgresAdapter,
8+
mysql: MysqlAdapter
9+
}
10+
11+
export function getAdapter(config: DBConnectionConfig): DbAdapterPort {
12+
const AdapterClass = adapters[config.connectionType]
13+
if (!AdapterClass) {
14+
throw new Error(`Unsupported database type: ${config.connectionType}`)
15+
}
16+
return new AdapterClass(config)
17+
}

apps/desktop/main/services/db/connection.manager.ts

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,49 @@
1-
import { PostgresAdapter, PostgresMetadataRepository } from '@datary/db'
2-
import type { DBConnectionConfig } from '@datary/db'
1+
import type { DbAdapterPort } from '@datary/core'
2+
import {
3+
type DBConnectionConfig,
4+
MysqlAdapter,
5+
MysqlMetadataRepository,
6+
PostgresAdapter,
7+
PostgresMetadataRepository
8+
} from '@datary/db'
39

410
import { logger } from '../../utils/logger'
511

612
export class ConnectionManager {
7-
private adapter: PostgresAdapter | null = null
13+
private adapter: DbAdapterPort | null = null
14+
private adapterType: string | null = null
815

9-
private metadataRepo: PostgresMetadataRepository | null = null
16+
private metadataRepo: any = null
1017

1118
public async connect(config: DBConnectionConfig) {
12-
this.adapter = new PostgresAdapter(config)
13-
14-
await this.adapter.connect()
19+
switch (config.connectionType) {
20+
case 'postgresql':
21+
this.adapter = new PostgresAdapter(config)
22+
await this.adapter.connect()
23+
// @ts-ignore
24+
this.metadataRepo = new PostgresMetadataRepository(this.adapter['client']!)
25+
this.adapterType = 'postgres'
26+
break
27+
28+
case 'mysql':
29+
this.adapter = new MysqlAdapter(config)
30+
await this.adapter.connect()
31+
// @ts-ignore
32+
this.metadataRepo = new MysqlMetadataRepository(this.adapter['client']!)
33+
this.adapterType = 'mysql'
34+
break
35+
36+
default:
37+
throw new Error(`Unsupported DB type: ${config.connectionType}`)
38+
}
39+
40+
logger.info(`DB connected (${config.connectionType})`)
41+
}
1542

16-
this.metadataRepo = new PostgresMetadataRepository(this.adapter['client']!)
43+
public getAdapterType() {
44+
if (!this.adapterType) throw new Error('DB not connected')
1745

18-
logger.info('DB connected')
46+
return this.adapterType
1947
}
2048

2149
public isConnected() {
@@ -27,6 +55,7 @@ export class ConnectionManager {
2755

2856
this.adapter = null
2957
this.metadataRepo = null
58+
this.adapterType = null
3059

3160
logger.info('DB disconnected')
3261
}

apps/desktop/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"dist": "electron-builder"
2424
},
2525
"dependencies": {
26+
"@datary/core": "workspace:*",
2627
"@datary/db": "workspace:*",
2728
"@datary/ipc": "workspace:*",
2829
"electron-store": "^11.0.2"

apps/desktop/preload/expose/db.api.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { ipcRenderer } from 'electron'
55
export const dbApi = {
66
connect: (config: DBConnectionConfig) => ipcRenderer.invoke(IPC_CHANNELS.DB_CONNECT, config),
77

8+
getAdapterType: () => ipcRenderer.invoke(IPC_CHANNELS.DB_GET_ADAPTER_TYPE),
9+
810
loadDatabases: () => ipcRenderer.invoke(IPC_CHANNELS.DB_LOAD_DATABASES),
911

1012
loadSchemas: (database: string) => ipcRenderer.invoke(IPC_CHANNELS.DB_LOAD_SCHEMAS, database),

apps/desktop/renderer/src/app/providers/datary-provider.tsx

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { Loader2 } from 'lucide-react'
22
import { type ReactNode, useEffect, useState } from 'react'
33

4+
import { Button } from '@/shared/ui/button'
5+
46
interface DataryProviderProps {
57
children: ReactNode
68
}
@@ -43,12 +45,7 @@ export function DataryProvider({ children }: DataryProviderProps) {
4345
<p className="text-muted-foreground max-w-xs text-center">
4446
The application failed to initialize correctly. Please try reloading the app.
4547
</p>
46-
<button
47-
className="bg-primary hover:bg-primary/90 rounded px-4 py-2 text-white"
48-
onClick={handleReload}
49-
>
50-
Reload
51-
</button>
48+
<Button onClick={handleReload}>Reload</Button>
5249
</div>
5350
)
5451
}

apps/desktop/renderer/src/main.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,14 @@ declare global {
1818
version: string
1919
db: {
2020
connect: Function
21+
getAdapterType: Function
2122
loadDatabases(): Promise<DatabaseMetadataContract[]>
2223
loadSchemas(database: string): Promise<any[]>
23-
loadTables(database: string, schema: string): Promise<TableMetadataContract[]>
24-
loadViews(database: string, schema: string): Promise<TableMetadataContract[]>
24+
loadTables(
25+
database: string,
26+
schema: string | null
27+
): Promise<TableMetadataContract[]>
28+
loadViews(database: string, schema: string | null): Promise<TableMetadataContract[]>
2529
loadColumns(
2630
database: string,
2731
schema: string,

apps/desktop/renderer/src/widgets/schema-tree/components/schema-tree.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export function SchemaTree({ database, selectedTable, onSelectTable }: SchemaTre
1616
if (error) return <div className="text-destructive px-4 py-2 text-sm">{error}</div>
1717

1818
if (!tree.length)
19-
return <div className="text-muted-foreground px-4 py-2 text-sm">No schemas found</div>
19+
return <div className="text-muted-foreground px-4 py-2 text-sm">No data found</div>
2020

2121
return (
2222
<div className="custom-scrollbar max-h-full space-y-0.5 overflow-auto py-2">

apps/desktop/renderer/src/widgets/schema-tree/hooks/useSchemaTree.ts

Lines changed: 63 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,51 @@ export function useSchemaTree(database?: string) {
1212
if (!database) return
1313

1414
let cancelled = false
15+
1516
setLoading(true)
1617
setError(null)
1718

1819
const loadTree = async () => {
1920
try {
21+
const adapter = await window.datary.db.getAdapterType()
22+
23+
if (adapter === 'mysql') {
24+
const [tables, views] = await Promise.all([
25+
window.datary.db.loadTables(database, null).catch(() => []),
26+
window.datary.db.loadViews(database, null).catch(() => [])
27+
])
28+
29+
const folders: TreeNode[] = []
30+
31+
if (tables.length) {
32+
folders.push({
33+
type: 'folder',
34+
name: 'Tables',
35+
children: tables.map(t => ({
36+
type: 'table',
37+
name: t.name
38+
}))
39+
})
40+
}
41+
42+
if (views.length) {
43+
folders.push({
44+
type: 'folder',
45+
name: 'Views',
46+
children: views.map(v => ({
47+
type: 'view',
48+
name: v.name
49+
}))
50+
})
51+
}
52+
53+
if (!cancelled) setTree(folders)
54+
55+
return
56+
}
57+
2058
const schemas = await window.datary.db.loadSchemas(database)
59+
2160
if (!schemas.length) {
2261
toast.error(`No schemas found for database ${database}`)
2362
return
@@ -26,47 +65,52 @@ export function useSchemaTree(database?: string) {
2665
const treeData: TreeNode[] = []
2766

2867
for (const schema of schemas) {
29-
const schemaName = typeof schema === 'string' ? schema : schema.name
68+
const schemaName = schema.name
3069

31-
const tables = await window.datary.db
32-
.loadTables(database, schemaName)
33-
.catch(() => [])
34-
const views = await window.datary.db
35-
.loadViews(database, schemaName)
36-
.catch(() => [])
70+
const [tables, views] = await Promise.all([
71+
window.datary.db.loadTables(database, schemaName).catch(() => []),
72+
window.datary.db.loadViews(database, schemaName).catch(() => [])
73+
])
3774

38-
const folders: TreeNode[] = []
75+
const children: TreeNode[] = []
3976

4077
if (tables.length) {
41-
folders.push({
42-
name: 'Tables',
78+
children.push({
4379
type: 'folder',
44-
children: tables.map(t => ({ name: t.name, type: 'table' }))
80+
name: 'Tables',
81+
children: tables.map(t => ({
82+
type: 'table',
83+
name: t.name
84+
}))
4585
})
4686
}
4787

4888
if (views.length) {
49-
folders.push({
50-
name: 'Views',
89+
children.push({
5190
type: 'folder',
52-
children: views.map(v => ({ name: v.name, type: 'view' }))
91+
name: 'Views',
92+
children: views.map(v => ({
93+
type: 'view',
94+
name: v.name
95+
}))
5396
})
5497
}
5598

56-
if (folders.length) {
99+
if (children.length) {
57100
treeData.push({
58-
name: schemaName,
59101
type: 'schema',
60-
children: folders
102+
name: schemaName,
103+
children
61104
})
62105
}
63106
}
64107

65108
if (!cancelled) setTree(treeData)
66109
} catch (err: any) {
67110
if (!cancelled) {
68-
setError(err?.message || 'Failed to load database tree')
69-
toast.error(err?.message || 'Failed to load database tree')
111+
const message = err?.message || 'Failed to load database tree'
112+
setError(message)
113+
toast.error(message)
70114
}
71115
} finally {
72116
if (!cancelled) setLoading(false)

apps/desktop/renderer/src/widgets/sidebar/components/ConnectionItem.tsx

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { useState } from 'react'
44
import { getConnectionIcon } from '../lib/get-connection-icon'
55

66
import type { DatabaseConnection } from '@/entities/connection/model/connection.types'
7-
import { cn } from '@/shared/lib/utils'
87
import {
98
AlertDialog,
109
AlertDialogAction,
@@ -26,21 +25,13 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@/shared/ui/tooltip'
2625

2726
interface Props {
2827
connection: DatabaseConnection
29-
active?: boolean
3028
collapsed?: boolean
3129
onSelect: () => void
3230
onConnect?: () => void
3331
onDelete?: () => void
3432
}
3533

36-
export function ConnectionItem({
37-
connection,
38-
active,
39-
collapsed,
40-
onSelect,
41-
onConnect,
42-
onDelete
43-
}: Props) {
34+
export function ConnectionItem({ connection, collapsed, onSelect, onConnect, onDelete }: Props) {
4435
const [contextMenuOpen, setContextMenuOpen] = useState(false)
4536
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false)
4637

@@ -62,10 +53,7 @@ export function ConnectionItem({
6253
<TooltipTrigger asChild>
6354
<button
6455
onClick={onSelect}
65-
className={cn(
66-
'flex w-full items-center justify-center rounded-md p-2',
67-
active ? 'bg-sidebar-accent' : 'hover:bg-sidebar-accent/50'
68-
)}
56+
className="hover:bg-sidebar-accent/50 flex w-full items-center justify-center rounded-md p-2"
6957
>
7058
{getConnectionIcon(connection.connectionType)}
7159
</button>
@@ -82,10 +70,7 @@ export function ConnectionItem({
8270
<button
8371
onClick={onSelect}
8472
onContextMenu={handleContextMenu}
85-
className={cn(
86-
'group relative flex w-full gap-3 rounded-md px-3 py-2.5 text-left',
87-
active ? 'bg-sidebar-accent' : 'hover:bg-sidebar-accent/50'
88-
)}
73+
className="group hover:bg-sidebar-accent/50 relative flex w-full gap-3 rounded-md px-3 py-2.5 text-left"
8974
>
9075
<div className="mt-0.5 shrink-0">
9176
{getConnectionIcon(connection.connectionType)}

0 commit comments

Comments
 (0)