Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b967deb4b1 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,5 +1,8 @@
|
||||
### Custom ###
|
||||
|
||||
# SQLite database
|
||||
data/
|
||||
|
||||
# TanStack
|
||||
.tanstack/
|
||||
|
||||
|
||||
28
AGENTS.md
28
AGENTS.md
@@ -8,7 +8,7 @@
|
||||
- **运行时**: Bun
|
||||
- **语言**: TypeScript (strict mode, ESNext)
|
||||
- **样式**: Tailwind CSS v4
|
||||
- **数据库**: PostgreSQL + Drizzle ORM
|
||||
- **数据库**: SQLite (Bun 内置) + Drizzle ORM
|
||||
- **状态管理**: TanStack Query
|
||||
- **路由**: TanStack Router (文件路由)
|
||||
- **RPC**: ORPC (类型安全 RPC,契约优先)
|
||||
@@ -44,9 +44,10 @@ biome format --write . # 仅格式化代码
|
||||
|
||||
### 数据库
|
||||
```bash
|
||||
bun db:init # 初始化 SQLite 数据库 (创建表)
|
||||
bun db:generate # 从 schema 生成迁移文件
|
||||
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/index.ts` 导出
|
||||
- 使用 `drizzle-orm/pg-core` 的 PostgreSQL 类型
|
||||
- 主键使用 `uuidv7()` (需要 PostgreSQL 扩展)
|
||||
- 使用 `drizzle-orm/sqlite-core` 的 SQLite 类型
|
||||
- 主键使用 `crypto.randomUUID()` 生成 UUID
|
||||
- 始终包含 `createdAt` 和 `updatedAt` 时间戳
|
||||
|
||||
示例:
|
||||
```typescript
|
||||
import { pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core'
|
||||
import { sql } from 'drizzle-orm'
|
||||
import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'
|
||||
|
||||
export const myTable = pgTable('my_table', {
|
||||
id: uuid().primaryKey().default(sql`uuidv7()`),
|
||||
name: text().notNull(),
|
||||
createdAt: timestamp({ withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp({ withTimezone: true }).notNull().defaultNow().$onUpdateFn(() => new Date()),
|
||||
export const myTable = sqliteTable('my_table', {
|
||||
id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
|
||||
name: text('name').notNull(),
|
||||
createdAt: integer('created_at', { mode: 'timestamp' })
|
||||
.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 依赖版本
|
||||
|
||||
242
README.md
Normal file
242
README.md
Normal 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 问题 |
|
||||
|
||||
### 代码规范
|
||||
|
||||
- **格式化**: 使用 Biome,2 空格缩进,单引号
|
||||
- **导入**: 使用 `@/*` 路径别名
|
||||
- **组件**: 箭头函数组件
|
||||
- **命名**: 文件 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
|
||||
29
bun.lock
29
bun.lock
@@ -18,15 +18,12 @@
|
||||
"@tauri-apps/api": "^2.9.1",
|
||||
"drizzle-orm": "^0.45.1",
|
||||
"drizzle-zod": "^0.8.3",
|
||||
"postgres": "^3.4.8",
|
||||
"react": "^19.2.3",
|
||||
"react-dom": "^19.2.3",
|
||||
"zod": "^4.3.5",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^2.3.11",
|
||||
"@effect/platform": "^0.94.1",
|
||||
"@effect/schema": "^0.75.5",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@tanstack/devtools-vite": "^0.4.1",
|
||||
"@tanstack/react-devtools": "^0.9.2",
|
||||
@@ -132,10 +129,6 @@
|
||||
|
||||
"@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/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=="],
|
||||
|
||||
"@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=="],
|
||||
|
||||
"@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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { defineConfig } from 'drizzle-kit'
|
||||
import { env } from '@/env'
|
||||
|
||||
export default defineConfig({
|
||||
out: './drizzle',
|
||||
schema: './src/db/schema/index.ts',
|
||||
dialect: 'postgresql',
|
||||
dialect: 'sqlite',
|
||||
dbCredentials: {
|
||||
url: env.DATABASE_URL,
|
||||
url: './data/app.db',
|
||||
},
|
||||
})
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"build:tauri": "tauri build",
|
||||
"build:vite": "vite build",
|
||||
"db:generate": "drizzle-kit generate",
|
||||
"db:init": "bun run scripts/init-db.ts",
|
||||
"db:migrate": "drizzle-kit migrate",
|
||||
"db:push": "drizzle-kit push",
|
||||
"db:studio": "drizzle-kit studio",
|
||||
@@ -31,15 +32,12 @@
|
||||
"@tauri-apps/api": "^2.9.1",
|
||||
"drizzle-orm": "^0.45.1",
|
||||
"drizzle-zod": "^0.8.3",
|
||||
"postgres": "^3.4.8",
|
||||
"react": "^19.2.3",
|
||||
"react-dom": "^19.2.3",
|
||||
"zod": "^4.3.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^2.3.11",
|
||||
"@effect/platform": "^0.94.1",
|
||||
"@effect/schema": "^0.75.5",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@tanstack/devtools-vite": "^0.4.1",
|
||||
"@tanstack/react-devtools": "^0.9.2",
|
||||
|
||||
22
scripts/init-db.ts
Normal file
22
scripts/init-db.ts
Normal 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()
|
||||
@@ -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() {
|
||||
return drizzle({
|
||||
connection: {
|
||||
url: env.DATABASE_URL,
|
||||
prepare: true,
|
||||
},
|
||||
schema,
|
||||
})
|
||||
import { Database } from 'bun:sqlite'
|
||||
import { existsSync, mkdirSync } from 'node:fs'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { drizzle } from 'drizzle-orm/bun-sqlite'
|
||||
import * as schema from '@/db/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>
|
||||
|
||||
@@ -1,15 +1,33 @@
|
||||
/**
|
||||
* Todo 表 Schema
|
||||
*
|
||||
* 使用 SQLite 数据类型:
|
||||
* - text: 字符串类型
|
||||
* - integer: 整数类型 (可配置为 boolean/timestamp 模式)
|
||||
*/
|
||||
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', {
|
||||
id: uuid('id').primaryKey().default(sql`uuidv7()`),
|
||||
export const todoTable = sqliteTable('todo', {
|
||||
/** 主键 UUID */
|
||||
id: text('id')
|
||||
.primaryKey()
|
||||
.$defaultFn(() => crypto.randomUUID()),
|
||||
|
||||
/** 待办事项标题 */
|
||||
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()
|
||||
.defaultNow(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true })
|
||||
.default(sql`(unixepoch())`),
|
||||
|
||||
/** 更新时间 (Unix 时间戳,自动更新) */
|
||||
updatedAt: integer('updated_at', { mode: 'timestamp' })
|
||||
.notNull()
|
||||
.defaultNow()
|
||||
.default(sql`(unixepoch())`)
|
||||
.$onUpdateFn(() => new Date()),
|
||||
})
|
||||
|
||||
@@ -2,9 +2,7 @@ import { createEnv } from '@t3-oss/env-core'
|
||||
import { z } from 'zod'
|
||||
|
||||
export const env = createEnv({
|
||||
server: {
|
||||
DATABASE_URL: z.url(),
|
||||
},
|
||||
server: {},
|
||||
clientPrefix: 'VITE_',
|
||||
client: {
|
||||
VITE_APP_TITLE: z.string().min(1).optional(),
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
/**
|
||||
* ORPC 同构客户端
|
||||
*
|
||||
* 根据运行环境自动选择最优调用方式:
|
||||
* - SSR (服务端): 直接调用 router,无 HTTP 开销
|
||||
* - CSR (客户端): 通过 /api/rpc 端点 HTTP 调用
|
||||
*
|
||||
* 同时配置了 TanStack Query 集成,mutation 成功后自动刷新相关查询。
|
||||
*/
|
||||
import { createORPCClient } from '@orpc/client'
|
||||
import { RPCLink } from '@orpc/client/fetch'
|
||||
import { createRouterClient } from '@orpc/server'
|
||||
@@ -7,6 +16,12 @@ import { getRequestHeaders } from '@tanstack/react-start/server'
|
||||
import { router } from './router'
|
||||
import type { RouterClient } from './types'
|
||||
|
||||
/**
|
||||
* 创建同构 ORPC 客户端
|
||||
*
|
||||
* 服务端: 直接调用路由处理器
|
||||
* 客户端: 通过 HTTP 调用 /api/rpc 端点
|
||||
*/
|
||||
const getORPCClient = createIsomorphicFn()
|
||||
.server(() =>
|
||||
createRouterClient(router, {
|
||||
@@ -24,7 +39,23 @@ const getORPCClient = createIsomorphicFn()
|
||||
|
||||
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, {
|
||||
// 配置 mutation 成功后自动刷新相关查询
|
||||
experimental_defaults: {
|
||||
todo: {
|
||||
create: {
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
/**
|
||||
* Todo API 契约
|
||||
*
|
||||
* 使用 ORPC 契约定义 API 的输入/输出类型。
|
||||
* drizzle-zod 自动从表 schema 生成验证规则。
|
||||
*/
|
||||
import { oc } from '@orpc/contract'
|
||||
import {
|
||||
createInsertSchema,
|
||||
@@ -7,24 +13,34 @@ import {
|
||||
import { z } from 'zod'
|
||||
import { todoTable } from '@/db/schema'
|
||||
|
||||
/** 查询返回的完整 Todo 类型 */
|
||||
const selectSchema = createSelectSchema(todoTable)
|
||||
|
||||
/** 创建 Todo 时的输入类型 (排除自动生成的字段) */
|
||||
const insertSchema = createInsertSchema(todoTable).omit({
|
||||
id: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
})
|
||||
|
||||
/** 更新 Todo 时的输入类型 (所有字段可选) */
|
||||
const updateSchema = createUpdateSchema(todoTable).omit({
|
||||
id: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
})
|
||||
|
||||
// ============================================================
|
||||
// API 契约定义
|
||||
// ============================================================
|
||||
|
||||
/** 获取所有 Todo */
|
||||
export const list = oc.input(z.void()).output(z.array(selectSchema))
|
||||
|
||||
/** 创建新 Todo */
|
||||
export const create = oc.input(insertSchema).output(selectSchema)
|
||||
|
||||
/** 更新 Todo */
|
||||
export const update = oc
|
||||
.input(
|
||||
z.object({
|
||||
@@ -34,6 +50,7 @@ export const update = oc
|
||||
)
|
||||
.output(selectSchema)
|
||||
|
||||
/** 删除 Todo */
|
||||
export const remove = oc
|
||||
.input(
|
||||
z.object({
|
||||
|
||||
@@ -1,9 +1,20 @@
|
||||
/**
|
||||
* Todo API 处理器
|
||||
*
|
||||
* 实现 Todo CRUD 操作的业务逻辑。
|
||||
* 每个处理器都使用 dbProvider 中间件获取数据库连接。
|
||||
*/
|
||||
import { ORPCError } from '@orpc/server'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { todoTable } from '@/db/schema'
|
||||
import { dbProvider } from '@/orpc/middlewares'
|
||||
import { os } from '@/orpc/server'
|
||||
|
||||
/**
|
||||
* 获取所有 Todo
|
||||
*
|
||||
* 按创建时间倒序排列 (最新的在前)
|
||||
*/
|
||||
export const list = os.todo.list
|
||||
.use(dbProvider)
|
||||
.handler(async ({ context }) => {
|
||||
@@ -13,6 +24,11 @@ export const list = os.todo.list
|
||||
return todos
|
||||
})
|
||||
|
||||
/**
|
||||
* 创建新 Todo
|
||||
*
|
||||
* @throws ORPCError NOT_FOUND - 创建失败时
|
||||
*/
|
||||
export const create = os.todo.create
|
||||
.use(dbProvider)
|
||||
.handler(async ({ context, input }) => {
|
||||
@@ -28,6 +44,11 @@ export const create = os.todo.create
|
||||
return newTodo
|
||||
})
|
||||
|
||||
/**
|
||||
* 更新 Todo
|
||||
*
|
||||
* @throws ORPCError NOT_FOUND - Todo 不存在时
|
||||
*/
|
||||
export const update = os.todo.update
|
||||
.use(dbProvider)
|
||||
.handler(async ({ context, input }) => {
|
||||
@@ -44,6 +65,9 @@ export const update = os.todo.update
|
||||
return updatedTodo
|
||||
})
|
||||
|
||||
/**
|
||||
* 删除 Todo
|
||||
*/
|
||||
export const remove = os.todo.remove
|
||||
.use(dbProvider)
|
||||
.handler(async ({ context, input }) => {
|
||||
|
||||
@@ -1,22 +1,41 @@
|
||||
/**
|
||||
* 数据库中间件
|
||||
*
|
||||
* 为 ORPC 处理器提供数据库连接。使用单例模式管理连接,
|
||||
* 避免每次请求都创建新连接。
|
||||
*/
|
||||
import { os } from '@orpc/server'
|
||||
import { createDb } from '@/db'
|
||||
import { createDb, type Db } from '@/db'
|
||||
|
||||
const IS_SERVERLESS = false // TODO: 这里需要优化
|
||||
|
||||
let globalDb: ReturnType<typeof createDb> | null = null
|
||||
|
||||
function getDb() {
|
||||
if (IS_SERVERLESS) {
|
||||
return createDb()
|
||||
}
|
||||
/** 全局数据库实例 (单例模式) */
|
||||
let globalDb: Db | null = null
|
||||
|
||||
/**
|
||||
* 获取数据库实例
|
||||
*
|
||||
* 首次调用时创建连接,后续调用返回同一实例。
|
||||
* 这种模式适合长时间运行的服务器进程。
|
||||
*/
|
||||
function getDb(): Db {
|
||||
if (!globalDb) {
|
||||
globalDb = createDb()
|
||||
}
|
||||
|
||||
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 }) => {
|
||||
const db = getDb()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user