Skip to content

Commit

Permalink
feat(desktop): implement SQLite database integration with Drizzle ORM
Browse files Browse the repository at this point in the history
  • Loading branch information
Red-Asuka authored and ysfscream committed Dec 2, 2024
1 parent 08d6221 commit 980419c
Show file tree
Hide file tree
Showing 14 changed files with 890 additions and 16 deletions.
2 changes: 2 additions & 0 deletions apps/desktop/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
*.db
drizzle
7 changes: 7 additions & 0 deletions apps/desktop/drizzle.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { defineConfig } from 'drizzle-kit'

export default defineConfig({
dialect: 'sqlite',
schema: './src/database/schemas/*',
out: './drizzle',
})
9 changes: 8 additions & 1 deletion apps/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"scripts": {
"dev": "electron-vite dev",
"prebuild": "pnpm run typecheck && electron-vite build",
"rebuild": "electron-rebuild -f -w better-sqlite3",
"postinstall": "electron-builder install-app-deps",
"build": "pnpm run prebuild && electron-builder --win --mac --linux --config",
"build:win": "pnpm run prebuild && electron-builder --win --config",
Expand All @@ -20,13 +21,16 @@
"preview": "electron-vite preview",
"typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false",
"typecheck:web": "vue-tsc --noEmit -p tsconfig.web.json --composite false",
"typecheck": "pnpm run typecheck:node && npm run typecheck:web"
"typecheck": "pnpm run typecheck:node && pnpm run typecheck:web",
"db:generate": "pnpm drizzle-kit generate"
},
"dependencies": {
"@electron-toolkit/preload": "^3.0.1",
"@electron-toolkit/utils": "^3.0.0",
"@element-plus/icons-vue": "^2.3.1",
"@mqttx/ui": "workspace:*",
"better-sqlite3": "^11.6.0",
"drizzle-orm": "^0.36.4",
"electron-updater": "^6.3.9",
"element-plus": "^2.8.7",
"pinia": "^2.2.6",
Expand All @@ -35,14 +39,17 @@
},
"devDependencies": {
"@electron-toolkit/tsconfig": "^1.0.1",
"@electron/rebuild": "^3.7.1",
"@mqttx/core": "workspace:*",
"@mqttx/tailwind-config": "workspace:*",
"@mqttx/tsconfig": "workspace:*",
"@types/better-sqlite3": "^7.6.12",
"@types/mqttx": "workspace:*",
"@vitejs/plugin-vue": "^5.1.5",
"@vitest/coverage-istanbul": "^2.1.4",
"@vue/test-utils": "^2.4.6",
"autoprefixer": "^10.4.20",
"drizzle-kit": "^0.28.1",
"electron": "^33.2.0",
"electron-builder": "^26.0.0-alpha.6",
"electron-vite": "^2.3.0",
Expand Down
43 changes: 43 additions & 0 deletions apps/desktop/src/database/db.main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import fs from 'node:fs'
import path from 'node:path'
import Database from 'better-sqlite3'
import { drizzle } from 'drizzle-orm/better-sqlite3'
import { migrate } from 'drizzle-orm/better-sqlite3/migrator'
import { app } from 'electron'
import { posts } from './schemas/posts'

const dbPath = import.meta.env.DEV ? 'sqlite.db' : path.join(app.getPath('userData'), 'data.db')

fs.mkdirSync(path.dirname(dbPath), { recursive: true })

const sqlite = new Database(
dbPath,
)

export const db = drizzle(sqlite, { schema: { posts } })

function toDrizzleResult(row: Record<string, any>)
function toDrizzleResult(rows: Record<string, any> | Array<Record<string, any>>) {
if (!rows) {
return []
}
if (Array.isArray(rows)) {
return rows.map((row) => {
return Object.keys(row).map(key => row[key])
})
} else {
return Object.keys(rows).map(key => rows[key])
}
}

export async function execute(_e, sqlstr, params, method) {
const result = sqlite.prepare(sqlstr)
const ret = result[method](...params)
return toDrizzleResult(ret)
}

export async function runMigrate() {
migrate(db, {
migrationsFolder: path.join(__dirname, '../../drizzle'),
})
}
14 changes: 14 additions & 0 deletions apps/desktop/src/database/db.renderer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { drizzle } from 'drizzle-orm/sqlite-proxy'
import { posts } from './schemas/posts'

export const database = drizzle(async (...args) => {
try {
const result = await window.api.execute(...args)
return { rows: result }
} catch (e: any) {
console.error('Error from sqlite proxy server: ', e.response.data)
return { rows: [] }
}
}, {
schema: { posts },
})
9 changes: 9 additions & 0 deletions apps/desktop/src/database/schemas/posts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { int, sqliteTable, text } from 'drizzle-orm/sqlite-core'

export const posts = sqliteTable('posts', {
id: int('id').primaryKey({ autoIncrement: true }),
title: text('title').notNull().default(''),
})

export type SelectPosts = typeof posts.$inferSelect
export type InsertPosts = typeof posts.$inferInsert
11 changes: 8 additions & 3 deletions apps/desktop/src/main/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { join } from 'node:path'
import { electronApp, is, optimizer } from '@electron-toolkit/utils'
import { app, BrowserWindow, shell } from 'electron'
import { app, BrowserWindow, ipcMain, shell } from 'electron'
import icon from '../../resources/icon.png?asset'
import { execute, runMigrate } from '../database/db.main'

// const IsMacOS = process.platform === 'darwin'

Expand All @@ -14,7 +15,7 @@ function createWindow(): void {
autoHideMenuBar: true,
...(process.platform === 'linux' ? { icon } : {}),
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
preload: join(__dirname, '../preload/index.mjs'),
sandbox: false,
},
// TODO: https://github.com/electron/electron/issues/43125
Expand Down Expand Up @@ -42,7 +43,7 @@ function createWindow(): void {
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.whenReady().then(() => {
app.whenReady().then(async () => {
// Set app user model id for windows
electronApp.setAppUserModelId('com.electron')

Expand All @@ -53,6 +54,10 @@ app.whenReady().then(() => {
optimizer.watchWindowShortcuts(window)
})

ipcMain.handle('db:execute', execute)

await runMigrate()

createWindow()

app.on('activate', () => {
Expand Down
4 changes: 3 additions & 1 deletion apps/desktop/src/preload/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import type { ElectronAPI } from '@electron-toolkit/preload'
declare global {
interface Window {
electron: ElectronAPI
api: unknown
api: {
execute: (...args: any[]) => Promise<any>
}
}
}
6 changes: 4 additions & 2 deletions apps/desktop/src/preload/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { electronAPI } from '@electron-toolkit/preload'
import { contextBridge } from 'electron'
import { contextBridge, ipcRenderer } from 'electron'

// Custom APIs for renderer
const api = {}
const api = {
execute: (...args) => ipcRenderer.invoke('db:execute', ...args),
}

// Use `contextBridge` APIs to expose Electron APIs to
// renderer only if context isolation is enabled, otherwise
Expand Down
1 change: 0 additions & 1 deletion apps/desktop/src/renderer/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ declare module 'vue' {
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
ElContainer: typeof import('element-plus/es')['ElContainer']
ElMain: typeof import('element-plus/es')['ElMain']
Help: typeof import('./../../../../packages/ui/src/components/Help.vue')['default']
HelpView: typeof import('./../../../../packages/ui/src/components/HelpView.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
Expand Down
31 changes: 31 additions & 0 deletions apps/desktop/src/renderer/src/pages/settings.vue
Original file line number Diff line number Diff line change
@@ -1,4 +1,35 @@
<script setup lang="ts">
import type { SelectPosts } from '../../../database/schemas/posts'
import { database } from '../../../database/db.renderer'
import { posts } from '../../../database/schemas/posts'
const postList = ref<SelectPosts[]>([])
async function insertMockData() {
await database.delete(posts).execute()
await database.insert(posts).values([
{
title: 'Hello World',
},
{
title: 'Hello World 2',
},
])
}
insertMockData().then(() => {
console.log('Mock data inserted')
database.query.posts.findMany().then((result) => {
console.log(result)
postList.value = result
})
})
</script>

<template>
<!-- <SettingsView /> -->
TODO: Implement desktop database settings
<pre>
{{ postList }}
</pre>
</template>
7 changes: 6 additions & 1 deletion apps/desktop/tsconfig.node.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,10 @@
"composite": true,
"types": ["electron-vite/node"]
},
"include": ["electron.vite.config.*", "src/main/*", "src/preload/*"]
"include": [
"electron.vite.config.*",
"src/main/**/*",
"src/preload/**/*",
"src/database/db.main.ts"
]
}
1 change: 1 addition & 0 deletions apps/desktop/tsconfig.web.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"src/renderer/src/**/*",
"src/renderer/src/**/*.vue",
"src/preload/*.d.ts",
"src/database/**/*",
"./typed-router.d.ts",
"src/renderer/auto-imports.d.ts",
"src/renderer/components.d.ts"
Expand Down
Loading

0 comments on commit 980419c

Please sign in to comment.