feat: 迁移数据库至 SQLite 并新增项目文档
- 将 Postgres 数据库替换为 SQLite - 并同步添加 README 文档以优化项目初始化流程
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,5 +1,8 @@
|
|||||||
### Custom ###
|
### Custom ###
|
||||||
|
|
||||||
|
# SQLite database
|
||||||
|
data/
|
||||||
|
|
||||||
# TanStack
|
# TanStack
|
||||||
.tanstack/
|
.tanstack/
|
||||||
|
|
||||||
|
|||||||
28
AGENTS.md
28
AGENTS.md
@@ -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
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",
|
"@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=="],
|
||||||
|
|||||||
@@ -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',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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
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() {
|
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>
|
||||||
|
|||||||
@@ -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()),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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 }) => {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user