1 Commits
ux ... main

Author SHA1 Message Date
b967deb4b1 feat: 迁移数据库至 SQLite 并新增项目文档
- 将 Postgres 数据库替换为 SQLite
- 并同步添加 README 文档以优化项目初始化流程
2026-01-20 16:56:11 +08:00
14 changed files with 482 additions and 78 deletions

3
.gitignore vendored
View File

@@ -1,5 +1,8 @@
### Custom ### ### Custom ###
# SQLite database
data/
# TanStack # TanStack
.tanstack/ .tanstack/

View File

@@ -8,7 +8,7 @@
- **运行时**: Bun - **运行时**: Bun
- **语言**: TypeScript (strict mode, ESNext) - **语言**: TypeScript (strict mode, ESNext)
- **样式**: Tailwind CSS v4 - **样式**: Tailwind CSS v4
- **数据库**: PostgreSQL + Drizzle ORM - **数据库**: SQLite (Bun 内置) + Drizzle ORM
- **状态管理**: TanStack Query - **状态管理**: TanStack Query
- **路由**: TanStack Router (文件路由) - **路由**: TanStack Router (文件路由)
- **RPC**: ORPC (类型安全 RPC契约优先) - **RPC**: ORPC (类型安全 RPC契约优先)
@@ -44,9 +44,10 @@ biome format --write . # 仅格式化代码
### 数据库 ### 数据库
```bash ```bash
bun db:init # 初始化 SQLite 数据库 (创建表)
bun db:generate # 从 schema 生成迁移文件 bun db:generate # 从 schema 生成迁移文件
bun db:migrate # 执行数据库迁移 bun db:migrate # 执行数据库迁移
bun db:push # 直接推送 schema 变更 (仅开发环境) bun db:studio # 打开 Drizzle Studio 数据库管理界面
``` ```
### 测试 ### 测试
@@ -145,20 +146,25 @@ export const Route = createFileRoute('/')({
-`src/db/schema/*.ts` 定义 schema -`src/db/schema/*.ts` 定义 schema
-`src/db/schema/index.ts` 导出 -`src/db/schema/index.ts` 导出
- 使用 `drizzle-orm/pg-core`PostgreSQL 类型 - 使用 `drizzle-orm/sqlite-core` 的 SQLite 类型
- 主键使用 `uuidv7()` (需要 PostgreSQL 扩展) - 主键使用 `crypto.randomUUID()` 生成 UUID
- 始终包含 `createdAt``updatedAt` 时间戳 - 始终包含 `createdAt``updatedAt` 时间戳
示例: 示例:
```typescript ```typescript
import { pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core'
import { sql } from 'drizzle-orm' import { sql } from 'drizzle-orm'
import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'
export const myTable = pgTable('my_table', { export const myTable = sqliteTable('my_table', {
id: uuid().primaryKey().default(sql`uuidv7()`), id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
name: text().notNull(), name: text('name').notNull(),
createdAt: timestamp({ withTimezone: true }).notNull().defaultNow(), createdAt: integer('created_at', { mode: 'timestamp' })
updatedAt: timestamp({ withTimezone: true }).notNull().defaultNow().$onUpdateFn(() => new Date()), .notNull()
.default(sql`(unixepoch())`),
updatedAt: integer('updated_at', { mode: 'timestamp' })
.notNull()
.default(sql`(unixepoch())`)
.$onUpdateFn(() => new Date()),
}) })
``` ```
@@ -273,5 +279,5 @@ const mutation = useMutation(orpc.myFeature.create.mutationOptions())
--- ---
**最后更新**: 2026-01-18 **最后更新**: 2026-01-20
**项目版本**: 基于 package.json 依赖版本 **项目版本**: 基于 package.json 依赖版本

242
README.md Normal file
View File

@@ -0,0 +1,242 @@
# Fullstack Starter (SQLite)
一个基于 **TanStack Start + Bun + Tauri + SQLite** 的全栈桌面应用脚手架。
包含一个完整的 **Todo List** 示例,展示了从前端到后端的完整数据流。
## 技术栈
| 层级 | 技术 |
|------|------|
| 前端框架 | React 19 + TanStack Router (文件路由) |
| 状态管理 | TanStack Query |
| 样式 | Tailwind CSS v4 |
| RPC 通信 | ORPC (类型安全,契约优先) |
| 数据库 | SQLite (Bun 内置) + Drizzle ORM |
| 桌面壳 | Tauri v2 |
| 运行时 | Bun |
| 构建 | Vite + Turbo |
## 快速开始
### 前置要求
- [Bun](https://bun.sh/) >= 1.0
- [Rust](https://www.rust-lang.org/) (仅 Tauri 桌面应用需要)
### 安装与运行
```bash
# 1. 克隆项目
git clone <your-repo-url>
cd fullstack-starter-SQLite
# 2. 安装依赖
bun install
# 3. 初始化数据库
bun run db:init
# 4. 启动开发服务器
bun run dev:vite # 仅 Web (http://localhost:3000)
bun run dev # Tauri 桌面应用 + Web
```
### 构建
```bash
bun run build:vite # 构建 Web 版本
bun run build # 构建 Tauri 桌面安装包
```
## 项目结构
```
├── src/
│ ├── components/ # 可复用组件
│ │ ├── Error.tsx # 错误边界组件
│ │ └── NotFound.tsx # 404 页面组件
│ │
│ ├── db/ # 数据库层
│ │ ├── index.ts # 数据库连接
│ │ └── schema/ # Drizzle 表定义
│ │ ├── index.ts # Schema 导出入口
│ │ └── todo.ts # Todo 表定义
│ │
│ ├── orpc/ # RPC 层 (后端 API)
│ │ ├── contracts/ # 契约定义 (输入/输出 Schema)
│ │ │ └── todo.ts # Todo API 契约
│ │ ├── handlers/ # 业务逻辑实现
│ │ │ └── todo.ts # Todo CRUD 处理器
│ │ ├── middlewares/ # 中间件
│ │ │ └── db.ts # 数据库注入中间件
│ │ ├── contract.ts # 契约聚合
│ │ ├── router.ts # 路由聚合
│ │ ├── client.ts # 同构客户端 (SSR/CSR)
│ │ ├── server.ts # 服务端实例
│ │ └── index.ts # 导出入口
│ │
│ ├── routes/ # 页面路由 (文件路由)
│ │ ├── __root.tsx # 根布局
│ │ ├── index.tsx # 首页 (Todo List)
│ │ └── api/
│ │ └── rpc.$.ts # RPC HTTP 端点
│ │
│ ├── integrations/ # 第三方库集成
│ ├── lib/ # 工具函数
│ ├── env.ts # 环境变量验证
│ ├── router.tsx # 路由配置
│ └── styles.css # 全局样式
├── scripts/
│ └── init-db.ts # 数据库初始化脚本
├── src-tauri/ # Tauri 桌面应用配置
├── data/ # SQLite 数据库文件 (gitignore)
└── drizzle.config.ts # Drizzle 配置
```
## 开发指南
### 添加新功能的步骤
以添加一个 "Note" 功能为例:
#### 1. 定义数据库 Schema
```typescript
// src/db/schema/note.ts
import { sql } from 'drizzle-orm'
import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'
export const noteTable = sqliteTable('note', {
id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
content: text('content').notNull(),
createdAt: integer('created_at', { mode: 'timestamp' })
.notNull()
.default(sql`(unixepoch())`),
})
```
```typescript
// src/db/schema/index.ts
export * from './todo.ts'
export * from './note.ts' // 添加导出
```
#### 2. 定义 API 契约
```typescript
// src/orpc/contracts/note.ts
import { oc } from '@orpc/contract'
import { z } from 'zod'
export const note = {
list: oc.output(z.array(noteSchema)),
create: oc.input(z.object({ content: z.string() })).output(noteSchema),
}
```
#### 3. 实现业务逻辑
```typescript
// src/orpc/handlers/note.ts
import { os } from '@/orpc/server'
import { dbProvider } from '@/orpc/middlewares/db'
import { noteTable } from '@/db/schema'
export const list = os.note.list
.use(dbProvider)
.handler(async ({ context }) => {
return context.db.select().from(noteTable)
})
export const create = os.note.create
.use(dbProvider)
.handler(async ({ context, input }) => {
const [note] = await context.db.insert(noteTable).values(input).returning()
return note
})
```
#### 4. 注册到路由
```typescript
// src/orpc/contract.ts
import { note } from './contracts/note'
export const contract = { todo, note }
// src/orpc/router.ts
import * as note from './handlers/note'
export const router = os.router({ todo, note })
```
#### 5. 在页面中使用
```typescript
// src/routes/notes.tsx
import { orpc } from '@/orpc'
import { useSuspenseQuery, useMutation } from '@tanstack/react-query'
const NotesPage = () => {
const { data: notes } = useSuspenseQuery(orpc.note.list.queryOptions())
const createNote = useMutation(orpc.note.create.mutationOptions())
// ...
}
```
### 常用命令
| 命令 | 说明 |
|------|------|
| `bun run dev` | 启动 Tauri + Vite 开发服务器 |
| `bun run dev:vite` | 仅启动 Vite 开发服务器 |
| `bun run build` | 完整构建 (Tauri 桌面应用) |
| `bun run build:vite` | 仅构建 Web 版本 |
| `bun run db:init` | 初始化/重置数据库 |
| `bun run db:studio` | 打开 Drizzle Studio |
| `bun run typecheck` | TypeScript 类型检查 |
| `bun run fix` | 自动修复格式和 Lint 问题 |
### 代码规范
- **格式化**: 使用 Biome2 空格缩进,单引号
- **导入**: 使用 `@/*` 路径别名
- **组件**: 箭头函数组件
- **命名**: 文件 kebab-case组件 PascalCase
## 核心概念
### ORPC 数据流
```
前端组件
↓ useSuspenseQuery / useMutation
ORPC 客户端 (src/orpc/client.ts)
↓ 自动选择 SSR 直调 / CSR HTTP
契约验证 (src/orpc/contracts/)
↓ 输入/输出类型安全
处理器 (src/orpc/handlers/)
↓ 业务逻辑
中间件 (src/orpc/middlewares/)
↓ 注入数据库连接
Drizzle ORM
↓ 类型安全查询
SQLite 数据库
```
### 同构渲染
- **SSR**: 服务端直接调用 router无 HTTP 开销
- **CSR**: 客户端通过 `/api/rpc` 端点调用
### 数据库
- SQLite 文件存储在 `./data/app.db`
- 使用 WAL 模式提高并发性能
- 单例模式管理连接
## 许可证
MIT

View File

@@ -18,15 +18,12 @@
"@tauri-apps/api": "^2.9.1", "@tauri-apps/api": "^2.9.1",
"drizzle-orm": "^0.45.1", "drizzle-orm": "^0.45.1",
"drizzle-zod": "^0.8.3", "drizzle-zod": "^0.8.3",
"postgres": "^3.4.8",
"react": "^19.2.3", "react": "^19.2.3",
"react-dom": "^19.2.3", "react-dom": "^19.2.3",
"zod": "^4.3.5", "zod": "^4.3.5",
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "^2.3.11", "@biomejs/biome": "^2.3.11",
"@effect/platform": "^0.94.1",
"@effect/schema": "^0.75.5",
"@tailwindcss/vite": "^4.1.18", "@tailwindcss/vite": "^4.1.18",
"@tanstack/devtools-vite": "^0.4.1", "@tanstack/devtools-vite": "^0.4.1",
"@tanstack/react-devtools": "^0.9.2", "@tanstack/react-devtools": "^0.9.2",
@@ -132,10 +129,6 @@
"@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="], "@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="],
"@effect/platform": ["@effect/platform@0.94.1", "", { "dependencies": { "find-my-way-ts": "^0.1.6", "msgpackr": "^1.11.4", "multipasta": "^0.2.7" }, "peerDependencies": { "effect": "^3.19.14" } }, "sha512-SlL8OMTogHmMNnFLnPAHHo3ua1yrB1LNQOVQMiZsqYu9g3216xjr0gn5WoDgCxUyOdZcseegMjWJ7dhm/2vnfg=="],
"@effect/schema": ["@effect/schema@0.75.5", "", { "dependencies": { "fast-check": "^3.21.0" }, "peerDependencies": { "effect": "^3.9.2" } }, "sha512-TQInulTVCuF+9EIbJpyLP6dvxbQJMphrnRqgexm/Ze39rSjfhJuufF7XvU3SxTgg3HnL7B/kpORTJbHhlE6thw=="],
"@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="], "@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="],
"@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="], "@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="],
@@ -208,18 +201,6 @@
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
"@msgpackr-extract/msgpackr-extract-darwin-arm64": ["@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw=="],
"@msgpackr-extract/msgpackr-extract-darwin-x64": ["@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw=="],
"@msgpackr-extract/msgpackr-extract-linux-arm": ["@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3", "", { "os": "linux", "cpu": "arm" }, "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw=="],
"@msgpackr-extract/msgpackr-extract-linux-arm64": ["@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg=="],
"@msgpackr-extract/msgpackr-extract-linux-x64": ["@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3", "", { "os": "linux", "cpu": "x64" }, "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg=="],
"@msgpackr-extract/msgpackr-extract-win32-x64": ["@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3", "", { "os": "win32", "cpu": "x64" }, "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ=="],
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" } }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="], "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" } }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="],
"@oozcitak/dom": ["@oozcitak/dom@2.0.2", "", { "dependencies": { "@oozcitak/infra": "^2.0.2", "@oozcitak/url": "^3.0.0", "@oozcitak/util": "^10.0.0" } }, "sha512-GjpKhkSYC3Mj4+lfwEyI1dqnsKTgwGy48ytZEhm4A/xnH/8z9M3ZVXKr/YGQi3uCLs1AEBS+x5T2JPiueEDW8w=="], "@oozcitak/dom": ["@oozcitak/dom@2.0.2", "", { "dependencies": { "@oozcitak/infra": "^2.0.2", "@oozcitak/url": "^3.0.0", "@oozcitak/util": "^10.0.0" } }, "sha512-GjpKhkSYC3Mj4+lfwEyI1dqnsKTgwGy48ytZEhm4A/xnH/8z9M3ZVXKr/YGQi3uCLs1AEBS+x5T2JPiueEDW8w=="],
@@ -704,8 +685,6 @@
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
"find-my-way-ts": ["find-my-way-ts@0.1.6", "", {}, "sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA=="],
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
@@ -812,20 +791,12 @@
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"msgpackr": ["msgpackr@1.11.8", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.2" } }, "sha512-bC4UGzHhVvgDNS7kn9tV8fAucIYUBuGojcaLiz7v+P63Lmtm0Xeji8B/8tYKddALXxJLpwIeBmUN3u64C4YkRA=="],
"msgpackr-extract": ["msgpackr-extract@3.0.3", "", { "dependencies": { "node-gyp-build-optional-packages": "5.2.2" }, "optionalDependencies": { "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" }, "bin": { "download-msgpackr-prebuilds": "bin/download-prebuilds.js" } }, "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA=="],
"multipasta": ["multipasta@0.2.7", "", {}, "sha512-KPA58d68KgGil15oDqXjkUBEBYc00XvbPj5/X+dyzeo/lWm9Nc25pQRlf1D+gv4OpK7NM0J1odrbu9JNNGvynA=="],
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"nf3": ["nf3@0.3.4", "", {}, "sha512-GnEgxkyJBjxbI+PxWICbQ2CaoAKeH8g7NaN8EidW+YvImlY/9HUJaGJ+1+ycEqBiZpZtIMyd/ppCXkkUw4iMrA=="], "nf3": ["nf3@0.3.4", "", {}, "sha512-GnEgxkyJBjxbI+PxWICbQ2CaoAKeH8g7NaN8EidW+YvImlY/9HUJaGJ+1+ycEqBiZpZtIMyd/ppCXkkUw4iMrA=="],
"nitro": ["nitro-nightly@3.0.1-20260115-135431-98fc91c5", "", { "dependencies": { "consola": "^3.4.2", "crossws": "^0.4.1", "db0": "^0.3.4", "h3": "^2.0.1-rc.8", "jiti": "^2.6.1", "nf3": "^0.3.4", "ofetch": "^2.0.0-alpha.3", "ohash": "^2.0.11", "oxc-minify": "^0.108.0", "oxc-transform": "^0.108.0", "srvx": "^0.10.0", "undici": "^7.18.2", "unenv": "^2.0.0-rc.24", "unstorage": "^2.0.0-alpha.5" }, "peerDependencies": { "rolldown": ">=1.0.0-beta.0", "rollup": "^4", "vite": "^7 || ^8 || >=8.0.0-0", "xml2js": "^0.6.2" }, "optionalPeers": ["rolldown", "rollup", "vite", "xml2js"], "bin": { "nitro": "dist/cli/index.mjs" } }, "sha512-dLGCF/NjNz0dfso6NP4Eck6HSVXCT2Mu3CIpsj6dDxfnQycXVvrRLXY5/mK2qnNjfVwr3PsbDLTrqAKNWIKWMw=="], "nitro": ["nitro-nightly@3.0.1-20260115-135431-98fc91c5", "", { "dependencies": { "consola": "^3.4.2", "crossws": "^0.4.1", "db0": "^0.3.4", "h3": "^2.0.1-rc.8", "jiti": "^2.6.1", "nf3": "^0.3.4", "ofetch": "^2.0.0-alpha.3", "ohash": "^2.0.11", "oxc-minify": "^0.108.0", "oxc-transform": "^0.108.0", "srvx": "^0.10.0", "undici": "^7.18.2", "unenv": "^2.0.0-rc.24", "unstorage": "^2.0.0-alpha.5" }, "peerDependencies": { "rolldown": ">=1.0.0-beta.0", "rollup": "^4", "vite": "^7 || ^8 || >=8.0.0-0", "xml2js": "^0.6.2" }, "optionalPeers": ["rolldown", "rollup", "vite", "xml2js"], "bin": { "nitro": "dist/cli/index.mjs" } }, "sha512-dLGCF/NjNz0dfso6NP4Eck6HSVXCT2Mu3CIpsj6dDxfnQycXVvrRLXY5/mK2qnNjfVwr3PsbDLTrqAKNWIKWMw=="],
"node-gyp-build-optional-packages": ["node-gyp-build-optional-packages@5.2.2", "", { "dependencies": { "detect-libc": "^2.0.1" }, "bin": { "node-gyp-build-optional-packages": "bin.js", "node-gyp-build-optional-packages-optional": "optional.js", "node-gyp-build-optional-packages-test": "build-test.js" } }, "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw=="],
"node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], "node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="],
"normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],

View File

@@ -1,11 +1,10 @@
import { defineConfig } from 'drizzle-kit' import { defineConfig } from 'drizzle-kit'
import { env } from '@/env'
export default defineConfig({ export default defineConfig({
out: './drizzle', out: './drizzle',
schema: './src/db/schema/index.ts', schema: './src/db/schema/index.ts',
dialect: 'postgresql', dialect: 'sqlite',
dbCredentials: { dbCredentials: {
url: env.DATABASE_URL, url: './data/app.db',
}, },
}) })

View File

@@ -8,6 +8,7 @@
"build:tauri": "tauri build", "build:tauri": "tauri build",
"build:vite": "vite build", "build:vite": "vite build",
"db:generate": "drizzle-kit generate", "db:generate": "drizzle-kit generate",
"db:init": "bun run scripts/init-db.ts",
"db:migrate": "drizzle-kit migrate", "db:migrate": "drizzle-kit migrate",
"db:push": "drizzle-kit push", "db:push": "drizzle-kit push",
"db:studio": "drizzle-kit studio", "db:studio": "drizzle-kit studio",
@@ -31,15 +32,12 @@
"@tauri-apps/api": "^2.9.1", "@tauri-apps/api": "^2.9.1",
"drizzle-orm": "^0.45.1", "drizzle-orm": "^0.45.1",
"drizzle-zod": "^0.8.3", "drizzle-zod": "^0.8.3",
"postgres": "^3.4.8",
"react": "^19.2.3", "react": "^19.2.3",
"react-dom": "^19.2.3", "react-dom": "^19.2.3",
"zod": "^4.3.5" "zod": "^4.3.5"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "^2.3.11", "@biomejs/biome": "^2.3.11",
"@effect/platform": "^0.94.1",
"@effect/schema": "^0.75.5",
"@tailwindcss/vite": "^4.1.18", "@tailwindcss/vite": "^4.1.18",
"@tanstack/devtools-vite": "^0.4.1", "@tanstack/devtools-vite": "^0.4.1",
"@tanstack/react-devtools": "^0.9.2", "@tanstack/react-devtools": "^0.9.2",

22
scripts/init-db.ts Normal file
View File

@@ -0,0 +1,22 @@
import { Database } from 'bun:sqlite'
import { mkdir } from 'node:fs/promises'
// Ensure data directory exists
await mkdir('./data', { recursive: true })
const db = new Database('./data/app.db', { create: true })
db.exec('PRAGMA journal_mode = WAL;')
// Create todo table
db.exec(`
CREATE TABLE IF NOT EXISTS todo (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
completed INTEGER NOT NULL DEFAULT 0,
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
updated_at INTEGER NOT NULL DEFAULT (unixepoch())
);
`)
console.log('Database initialized successfully!')
db.close()

View File

@@ -1,13 +1,69 @@
import { drizzle } from 'drizzle-orm/postgres-js' /**
import * as schema from '@/db/schema' * 数据库连接模块
import { env } from '@/env' *
* 使用 Bun 内置的 SQLite 驱动,无需额外安装原生模块。
* 数据库文件存储在可执行文件同级的 data/app.db
*/
export function createDb() { import { Database } from 'bun:sqlite'
return drizzle({ import { existsSync, mkdirSync } from 'node:fs'
connection: { import { dirname, join } from 'node:path'
url: env.DATABASE_URL, import { drizzle } from 'drizzle-orm/bun-sqlite'
prepare: true, import * as schema from '@/db/schema'
},
schema, /**
}) * 获取数据库路径
* - 在打包后的 sidecar 中,使用可执行文件所在目录
* - 在开发模式下,使用项目根目录
*/
function getDbPath(): string {
const execPath = process.execPath
const isBundled = !execPath.includes('node') && !execPath.includes('bun')
const baseDir = isBundled ? dirname(execPath) : process.cwd()
const dataDir = join(baseDir, 'data')
// 确保 data 目录存在
if (!existsSync(dataDir)) {
mkdirSync(dataDir, { recursive: true })
} }
return join(dataDir, 'app.db')
}
/** 数据库文件路径 */
const DB_PATH = getDbPath()
/**
* 初始化数据库表结构
*/
function initTables(sqlite: Database) {
sqlite.exec(`
CREATE TABLE IF NOT EXISTS todo (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
completed INTEGER NOT NULL DEFAULT 0,
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
updated_at INTEGER NOT NULL DEFAULT (unixepoch())
);
`)
}
/**
* 创建数据库连接
*
* 启用 WAL (Write-Ahead Logging) 模式以提高并发读写性能。
* 如果数据库文件不存在,会自动创建并初始化表结构。
*/
export function createDb() {
const sqlite = new Database(DB_PATH, { create: true })
sqlite.exec('PRAGMA journal_mode = WAL;')
// 自动初始化表结构
initTables(sqlite)
return drizzle(sqlite, { schema })
}
/** 数据库实例类型 */
export type Db = ReturnType<typeof createDb>

View File

@@ -1,15 +1,33 @@
/**
* Todo 表 Schema
*
* 使用 SQLite 数据类型:
* - text: 字符串类型
* - integer: 整数类型 (可配置为 boolean/timestamp 模式)
*/
import { sql } from 'drizzle-orm' import { sql } from 'drizzle-orm'
import { boolean, pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core' import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'
export const todoTable = pgTable('todo', { export const todoTable = sqliteTable('todo', {
id: uuid('id').primaryKey().default(sql`uuidv7()`), /** 主键 UUID */
id: text('id')
.primaryKey()
.$defaultFn(() => crypto.randomUUID()),
/** 待办事项标题 */
title: text('title').notNull(), title: text('title').notNull(),
completed: boolean('completed').notNull().default(false),
createdAt: timestamp('created_at', { withTimezone: true }) /** 是否已完成 (SQLite 用 0/1 表示布尔值) */
completed: integer('completed', { mode: 'boolean' }).notNull().default(false),
/** 创建时间 (Unix 时间戳) */
createdAt: integer('created_at', { mode: 'timestamp' })
.notNull() .notNull()
.defaultNow(), .default(sql`(unixepoch())`),
updatedAt: timestamp('updated_at', { withTimezone: true })
/** 更新时间 (Unix 时间戳,自动更新) */
updatedAt: integer('updated_at', { mode: 'timestamp' })
.notNull() .notNull()
.defaultNow() .default(sql`(unixepoch())`)
.$onUpdateFn(() => new Date()), .$onUpdateFn(() => new Date()),
}) })

View File

@@ -2,9 +2,7 @@ import { createEnv } from '@t3-oss/env-core'
import { z } from 'zod' import { z } from 'zod'
export const env = createEnv({ export const env = createEnv({
server: { server: {},
DATABASE_URL: z.url(),
},
clientPrefix: 'VITE_', clientPrefix: 'VITE_',
client: { client: {
VITE_APP_TITLE: z.string().min(1).optional(), VITE_APP_TITLE: z.string().min(1).optional(),

View File

@@ -1,3 +1,12 @@
/**
* ORPC 同构客户端
*
* 根据运行环境自动选择最优调用方式:
* - SSR (服务端): 直接调用 router无 HTTP 开销
* - CSR (客户端): 通过 /api/rpc 端点 HTTP 调用
*
* 同时配置了 TanStack Query 集成mutation 成功后自动刷新相关查询。
*/
import { createORPCClient } from '@orpc/client' import { createORPCClient } from '@orpc/client'
import { RPCLink } from '@orpc/client/fetch' import { RPCLink } from '@orpc/client/fetch'
import { createRouterClient } from '@orpc/server' import { createRouterClient } from '@orpc/server'
@@ -7,6 +16,12 @@ import { getRequestHeaders } from '@tanstack/react-start/server'
import { router } from './router' import { router } from './router'
import type { RouterClient } from './types' import type { RouterClient } from './types'
/**
* 创建同构 ORPC 客户端
*
* 服务端: 直接调用路由处理器
* 客户端: 通过 HTTP 调用 /api/rpc 端点
*/
const getORPCClient = createIsomorphicFn() const getORPCClient = createIsomorphicFn()
.server(() => .server(() =>
createRouterClient(router, { createRouterClient(router, {
@@ -24,7 +39,23 @@ const getORPCClient = createIsomorphicFn()
const client: RouterClient = getORPCClient() const client: RouterClient = getORPCClient()
/**
* ORPC + TanStack Query 工具
*
* 使用方式:
* ```tsx
* // 查询
* const { data } = useSuspenseQuery(orpc.todo.list.queryOptions())
*
* // 变更
* const mutation = useMutation(orpc.todo.create.mutationOptions())
* mutation.mutate({ title: '新任务' })
* ```
*
* 配置了自动缓存失效: 创建/更新/删除操作后自动刷新列表
*/
export const orpc = createTanstackQueryUtils(client, { export const orpc = createTanstackQueryUtils(client, {
// 配置 mutation 成功后自动刷新相关查询
experimental_defaults: { experimental_defaults: {
todo: { todo: {
create: { create: {

View File

@@ -1,3 +1,9 @@
/**
* Todo API 契约
*
* 使用 ORPC 契约定义 API 的输入/输出类型。
* drizzle-zod 自动从表 schema 生成验证规则。
*/
import { oc } from '@orpc/contract' import { oc } from '@orpc/contract'
import { import {
createInsertSchema, createInsertSchema,
@@ -7,24 +13,34 @@ import {
import { z } from 'zod' import { z } from 'zod'
import { todoTable } from '@/db/schema' import { todoTable } from '@/db/schema'
/** 查询返回的完整 Todo 类型 */
const selectSchema = createSelectSchema(todoTable) const selectSchema = createSelectSchema(todoTable)
/** 创建 Todo 时的输入类型 (排除自动生成的字段) */
const insertSchema = createInsertSchema(todoTable).omit({ const insertSchema = createInsertSchema(todoTable).omit({
id: true, id: true,
createdAt: true, createdAt: true,
updatedAt: true, updatedAt: true,
}) })
/** 更新 Todo 时的输入类型 (所有字段可选) */
const updateSchema = createUpdateSchema(todoTable).omit({ const updateSchema = createUpdateSchema(todoTable).omit({
id: true, id: true,
createdAt: true, createdAt: true,
updatedAt: true, updatedAt: true,
}) })
// ============================================================
// API 契约定义
// ============================================================
/** 获取所有 Todo */
export const list = oc.input(z.void()).output(z.array(selectSchema)) export const list = oc.input(z.void()).output(z.array(selectSchema))
/** 创建新 Todo */
export const create = oc.input(insertSchema).output(selectSchema) export const create = oc.input(insertSchema).output(selectSchema)
/** 更新 Todo */
export const update = oc export const update = oc
.input( .input(
z.object({ z.object({
@@ -34,6 +50,7 @@ export const update = oc
) )
.output(selectSchema) .output(selectSchema)
/** 删除 Todo */
export const remove = oc export const remove = oc
.input( .input(
z.object({ z.object({

View File

@@ -1,9 +1,20 @@
/**
* Todo API 处理器
*
* 实现 Todo CRUD 操作的业务逻辑。
* 每个处理器都使用 dbProvider 中间件获取数据库连接。
*/
import { ORPCError } from '@orpc/server' import { ORPCError } from '@orpc/server'
import { eq } from 'drizzle-orm' import { eq } from 'drizzle-orm'
import { todoTable } from '@/db/schema' import { todoTable } from '@/db/schema'
import { dbProvider } from '@/orpc/middlewares' import { dbProvider } from '@/orpc/middlewares'
import { os } from '@/orpc/server' import { os } from '@/orpc/server'
/**
* 获取所有 Todo
*
* 按创建时间倒序排列 (最新的在前)
*/
export const list = os.todo.list export const list = os.todo.list
.use(dbProvider) .use(dbProvider)
.handler(async ({ context }) => { .handler(async ({ context }) => {
@@ -13,6 +24,11 @@ export const list = os.todo.list
return todos return todos
}) })
/**
* 创建新 Todo
*
* @throws ORPCError NOT_FOUND - 创建失败时
*/
export const create = os.todo.create export const create = os.todo.create
.use(dbProvider) .use(dbProvider)
.handler(async ({ context, input }) => { .handler(async ({ context, input }) => {
@@ -28,6 +44,11 @@ export const create = os.todo.create
return newTodo return newTodo
}) })
/**
* 更新 Todo
*
* @throws ORPCError NOT_FOUND - Todo 不存在时
*/
export const update = os.todo.update export const update = os.todo.update
.use(dbProvider) .use(dbProvider)
.handler(async ({ context, input }) => { .handler(async ({ context, input }) => {
@@ -44,6 +65,9 @@ export const update = os.todo.update
return updatedTodo return updatedTodo
}) })
/**
* 删除 Todo
*/
export const remove = os.todo.remove export const remove = os.todo.remove
.use(dbProvider) .use(dbProvider)
.handler(async ({ context, input }) => { .handler(async ({ context, input }) => {

View File

@@ -1,22 +1,41 @@
/**
* 数据库中间件
*
* 为 ORPC 处理器提供数据库连接。使用单例模式管理连接,
* 避免每次请求都创建新连接。
*/
import { os } from '@orpc/server' import { os } from '@orpc/server'
import { createDb } from '@/db' import { createDb, type Db } from '@/db'
const IS_SERVERLESS = false // TODO: 这里需要优化 /** 全局数据库实例 (单例模式) */
let globalDb: Db | null = null
let globalDb: ReturnType<typeof createDb> | null = null
function getDb() {
if (IS_SERVERLESS) {
return createDb()
}
/**
* 获取数据库实例
*
* 首次调用时创建连接,后续调用返回同一实例。
* 这种模式适合长时间运行的服务器进程。
*/
function getDb(): Db {
if (!globalDb) { if (!globalDb) {
globalDb = createDb() globalDb = createDb()
} }
return globalDb return globalDb
} }
/**
* 数据库提供者中间件
*
* 使用方式:
* ```ts
* export const list = os.todo.list
* .use(dbProvider)
* .handler(async ({ context }) => {
* // context.db 可用
* return context.db.query.todoTable.findMany()
* })
* ```
*/
export const dbProvider = os.middleware(async ({ context, next }) => { export const dbProvider = os.middleware(async ({ context, next }) => {
const db = getDb() const db = getDb()