Files
fullstack-starter-SQLite/apps/server/AGENTS.md
imbytecat 5e56383f6f feat: 迁移数据库至SQLite并适配嵌入式环境
- 添加SQLite数据库文件及数据目录的忽略规则
- 将数据库连接地址更新为本地SQLite数据库文件路径
- 将数据库从 PostgreSQL 迁移至 SQLite,更新依赖、Drizzle 方言及数据类型映射,确保嵌入式环境兼容性并保持应用层代码不变。
- 将数据库方言从 PostgreSQL 更改为 SQLite。
- 将数据库依赖从 postgres 替换为 better-sqlite3,并添加 better-sqlite3 的类型定义。
- 修改数据库连接字符串验证规则,从URL格式验证改为非空字符串验证。
- 将数据库连接从 PostgreSQL 切换为 Better-SQLite3,并支持创建目录和内存数据库。
- 将 todo 表的 completed 字段从 PostgreSQL 的 boolean 类型改为 SQLite 的 boolean 模式整数类型。
- 将字段定义从 PostgreSQL 适配改为 SQLite 适配,使用文本类型 ID 和时间戳整数类型,并统一使用 UUIDv7 和当前时间作为默认值。
- 添加 better-sqlite3 及其类型定义,并引入相关依赖以支持其功能
- 添加 better-sqlite3 依赖并移除 postgres 依赖
2026-01-26 14:10:25 +08:00

509 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# AGENTS.md - AI Coding Agent Guidelines
本文档为 AI 编程助手提供此 TanStack Start 全栈项目的开发规范和指南。
## 项目概览
- **框架**: TanStack Start (React SSR 框架,文件路由)
- **运行时**: Bun
- **语言**: TypeScript (strict mode, ESNext)
- **样式**: Tailwind CSS v4
- **数据库**: SQLite + Drizzle ORM
- **状态管理**: TanStack Query
- **路由**: TanStack Router (文件路由)
- **RPC**: ORPC (类型安全 RPC契约优先)
- **构建工具**: Vite + Turbo
- **代码质量**: Biome (格式化 + Lint)
## 依赖管理
### Bun Catalog 系统
项目使用 **Bun Catalog** 统一管理依赖版本(定义在根目录 `package.json``catalog` 字段)。
**安装依赖的正确方式**
```bash
# ✅ 正确:使用 catalog: 前缀
bun add <package-name>@catalog:
# ❌ 错误:直接安装会绕过版本统一管理
bun add <package-name>@latest
```
**示例**
```bash
# 添加 systeminformation 依赖到 packages/utils
cd packages/utils
bun add systeminformation@catalog:
# 添加 react 依赖到 apps/server
cd apps/server
bun add react@catalog:
```
**为什么使用 Catalog**
- 确保 monorepo 中所有包使用相同版本
- 集中管理依赖版本,避免版本冲突
- 简化依赖升级(只需修改根 package.json
**添加新依赖的步骤**
1. 在根目录 `package.json``catalog` 字段添加依赖及版本
2. 在目标包中使用 `bun add <package>@catalog:` 安装
## 构建、Lint 和测试命令
### 开发
```bash
bun dev # 启动 Vite 开发服务器
bun db:studio # 打开 Drizzle Studio 数据库管理界面
```
### 构建
```bash
bun build # 构建 Vite 应用 (输出到 .output/)
bun compile # 编译为独立可执行文件 (使用 build.ts)
```
### 代码质量
```bash
bun typecheck # 运行 TypeScript 类型检查
bun fix # 运行 Biome 自动修复格式和 Lint 问题
biome check . # 检查但不自动修复
biome format --write . # 仅格式化代码
```
### 数据库
```bash
bun db:generate # 从 schema 生成迁移文件
bun db:migrate # 执行数据库迁移
bun db:push # 直接推送 schema 变更 (仅开发环境)
```
### 测试
**注意**: 当前未配置测试框架。添加测试时:
- 使用 Vitest 或 Bun 内置测试运行器
- 运行单个测试文件: `bun test path/to/test.ts`
- 运行特定测试: `bun test -t "测试名称模式"`
## 代码风格指南
### 格式化 (Biome)
**缩进**: 2 空格 (不使用 tab)
**换行符**: LF (Unix 风格)
**引号**: 单引号 `'string'`
**分号**: 按需 (ASI - 自动分号插入)
**箭头函数括号**: 始终使用 `(x) => x`
示例:
```typescript
const myFunc = (value: string) => {
return value.toUpperCase()
}
```
### 导入组织
Biome 自动组织导入。顺序:
1. 外部依赖
2. 内部导入 (使用 `@/*` 别名)
3. 类型导入 (仅导入类型时使用 `type` 关键字)
示例:
```typescript
import { createFileRoute } from '@tanstack/react-router'
import { oc } from '@orpc/contract'
import { z } from 'zod'
import { db } from '@/db'
import { todoTable } from '@/db/schema'
import type { ReactNode } from 'react'
```
### TypeScript
**严格模式**: 启用了额外的严格检查
- `strict: true`
- `noUncheckedIndexedAccess: true` - 数组/对象索引返回 `T | undefined`
- `noImplicitOverride: true`
- `noFallthroughCasesInSwitch: true`
**模块解析**: `bundler` 模式 + `verbatimModuleSyntax`
- 导入时始终使用 `.ts`/`.tsx` 扩展名
- 使用 `@/*` 路径别名指向 `src/*`
**类型注解**:
- 公共 API 的函数参数和返回类型必须注解
- 优先使用显式类型而非 `any`
- 对象形状用 `type`,可扩展契约用 `interface`
- 不可变 props 使用 `Readonly<T>`
### 命名规范
- **文件**: 工具函数用 kebab-case组件用 PascalCase
- `utils.ts`, `todo.tsx`, `NotFound.tsx`
- **路由**: 遵循 TanStack Router 约定
- `routes/index.tsx``/`
- `routes/__root.tsx` → 根布局
- **组件**: PascalCase 箭头函数 (Biome 规则 `useArrowFunction` 强制)
- **函数**: camelCase
- **常量**: 真常量用 UPPER_SNAKE_CASE配置对象用 camelCase
- **类型/接口**: PascalCase
### React 模式
**组件**: 使用箭头函数
```typescript
const MyComponent = ({ title }: { title: string }) => {
return <div>{title}</div>
}
```
**路由**: 使用 `createFileRoute` 定义路由
```typescript
export const Route = createFileRoute('/')({
component: Home,
})
```
**数据获取**: 使用 TanStack Query hooks
- `useSuspenseQuery` - 保证有数据
- `useQuery` - 数据可能为空
**Props**: 禁止直接修改 props (Biome 规则 `noReactPropAssignments`)
### 数据库 Schema (Drizzle)
-`src/server/db/schema/*.ts` 定义 schema
-`src/server/db/schema/index.ts` 导出
- 使用 `drizzle-orm/sqlite-core` 的 SQLite 类型
- 主键使用 `uuidv7()` (TEXT 存储)
- 时间戳使用 `integer({ mode: 'timestamp_ms' })` (Unix 毫秒时间戳)
- 始终包含 `createdAt``updatedAt` 时间戳
示例:
```typescript
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'
import { v7 as uuidv7 } from 'uuid'
export const myTable = sqliteTable('my_table', {
id: text('id').primaryKey().$defaultFn(() => uuidv7()),
name: text('name').notNull(),
createdAt: integer('created_at', { mode: 'timestamp_ms' }).notNull().$defaultFn(() => new Date()),
updatedAt: integer('updated_at', { mode: 'timestamp_ms' }).notNull().$defaultFn(() => new Date()).$onUpdateFn(() => new Date()),
})
```
### 环境变量
- 使用 `@t3-oss/env-core` 进行类型安全的环境变量验证
-`src/env.ts` 定义 schema
- 服务端变量: 无前缀
- 客户端变量: 必须有 `VITE_` 前缀
- 使用 Zod schema 验证
### 错误处理
- 异步操作使用 try-catch
- 抛出带有描述性消息的错误
- 用户界面错误优先使用 Result 类型或错误边界
- 适当记录错误 (避免记录敏感数据)
### 样式 (Tailwind CSS)
- 使用 Tailwind v4 工具类
- 通过 `@/styles.css?url` 导入样式
- 优先使用组合而非自定义 CSS
- 响应式修饰符: `sm:`, `md:`, `lg:`
- UI 文本适当使用中文
## 目录结构
```
src/
├── components/ # 可复用 React 组件
├── db/
│ ├── schema/ # Drizzle schema 定义
│ └── index.ts # 数据库实例
├── integrations/ # 第三方集成 (TanStack Query/Router)
├── lib/ # 工具函数
├── orpc/ # ORPC (RPC 层)
│ ├── contracts/ # 契约定义 (input/output schemas)
│ ├── handlers/ # 服务端过程实现
│ ├── middlewares/ # 中间件 (如 DB provider)
│ ├── contract.ts # 契约聚合
│ ├── router.ts # 路由组合
│ ├── server.ts # 服务端实例
│ └── client.ts # 同构客户端
├── routes/ # TanStack Router 文件路由
│ ├── __root.tsx # 根布局
│ ├── index.tsx # 首页
│ └── api/rpc.$.ts # ORPC HTTP 端点
├── env.ts # 环境变量验证
└── router.tsx # 路由配置
```
## 重要提示
- **禁止** 编辑 `src/routeTree.gen.ts` - 自动生成
- **禁止** 提交 `.env` 文件 - 使用 `.env.example` 作为模板
- **必须** 在提交前运行 `bun fix`
- **必须** 使用 `@/*` 路径别名而非相对导入
- **必须** 利用 React Compiler (babel-plugin-react-compiler) - 避免手动 memoization
## Git 工作流
1. 按照上述风格指南进行修改
2. 运行 `bun fix` 自动格式化和 lint
3. 运行 `bun typecheck` 确保类型安全
4. 使用 `bun dev` 本地测试变更
5. 使用清晰的描述性消息提交
## 常见模式
### 创建 ORPC 过程
**步骤 1: 定义契约** (`src/orpc/contracts/my-feature.ts`)
```typescript
import { oc } from '@orpc/contract'
import { z } from 'zod'
export const myContract = {
get: oc.input(z.object({ id: z.uuid() })).output(mySchema),
create: oc.input(createSchema).output(mySchema),
}
```
**步骤 2: 实现处理器** (`src/orpc/handlers/my-feature.ts`)
```typescript
import { os } from '@/orpc/server'
import { dbProvider } from '@/orpc/middlewares'
export const get = os.myFeature.get
.use(dbProvider)
.handler(async ({ context, input }) => {
return await context.db.query.myTable.findFirst(...)
})
```
**步骤 3: 注册到契约和路由**
```typescript
// src/orpc/contract.ts
export const contract = { myFeature: myContract }
// src/orpc/router.ts
import * as myFeature from './handlers/my-feature'
export const router = os.router({ myFeature })
```
**步骤 4: 在组件中使用**
```typescript
import { orpc } from '@/orpc'
const query = useSuspenseQuery(orpc.myFeature.get.queryOptions({ id }))
const mutation = useMutation(orpc.myFeature.create.mutationOptions())
```
---
## 已知问题与解决方案
### 构建问题
**已解决**: Vite 8.0.0-beta.10 已修复与 Nitro 插件的兼容性问题
- **当前版本**: Vite 8.0.0-beta.10 + nitro-nightly@3.0.1-20260125
- **状态**: 构建稳定,开发和生产环境均正常工作
### 依赖选择经验
**ohash vs crypto.createHash**
在实现硬件指纹功能时,曾误判 `ohash` 不适合用于硬件指纹识别。经深入研究发现:
**事实**
- `ohash` 内部使用**完整的 SHA-256** 算法256 位)
- 输出 43 字符 Base64URL 编码(等价于 64 字符 Hex
- 碰撞概率与 `crypto.createHash('sha256')` **完全相同**2^128
- 自动处理对象序列化,代码更简洁
**对比**
```typescript
// ohash - 推荐用于对象哈希
import { hash } from 'ohash'
const fingerprint = hash(systemInfo) // 一行搞定
// crypto - 需要手动序列化
import { createHash } from 'node:crypto'
const fingerprint = createHash('sha256')
.update(JSON.stringify(systemInfo))
.digest('base64url')
```
**结论**
-`ohash` 完全适合硬件指纹场景(数据来自系统 API非用户输入
- ✅ 两者安全性等价,选择取决于代码风格偏好
- ⚠️ ohash 文档警告的"序列化安全性"仅针对**用户输入**场景
**经验教训**
- 不要仅凭名称("短哈希")判断库的实现
- 深入研究文档和源码再做技术决策
- 区分"用户输入场景"和"系统数据场景"的安全要求
### 缓存库选择:@isaacs/ttlcache
**决策时间**: 2026-01-26
**背景**
硬件指纹功能最初使用手动实现的 TTL 缓存module-level 变量 + 手动过期检查)。为提高代码可维护性,迁移到专业缓存库。
**选型**
- **选择**: `@isaacs/ttlcache` v2.1.4
- **理由**:
- 专为 TTL 场景优化,无需 LRU 追踪开销
- 零依赖6M+ 周下载量
- 内置 TypeScript 类型
- 自动过期管理,无需手动定时器
- API 简洁: `new TTLCache({ ttl, max })`
**实现细节**
- 保留 `inFlight` Promise 模式用于并发请求去重TTLCache 不提供此功能)
- 使用单一缓存键 `'fingerprint'`单服务器场景opts 不影响输出)
- 默认 TTL: 10 分钟(可通过 `cacheTtlMs` 参数覆盖)
**对比手动实现**
- ✅ 更少自定义代码
- ✅ 更清晰的 TTL 语义
- ✅ 经过充分测试的库
- ⚠️ 仍需手动处理并发去重
**经验教训**
- 专业库不一定解决所有问题(如并发去重)
- 对于简单场景,手动实现 vs 库的选择主要取决于可维护性而非功能
### PostgreSQL → SQLite 迁移
**迁移时间**: 2026-01-26
**原因**: 项目需要在嵌入环境中运行,无法安装 PostgreSQL
**技术选择**:
- **驱动**: `better-sqlite3` v11.8.1 (跨平台,成熟稳定,原生模块)
- **类型定义**: `@types/better-sqlite3` v7.6.12
- **主键**: TEXT 存储 UUIDv7 (保持全局唯一性36 字符字符串)
- **时间戳**: INTEGER 存储 Unix 毫秒时间戳 (`integer({ mode: 'timestamp_ms' })`)
- **布尔值**: INTEGER 存储 0/1 (`integer({ mode: 'boolean' })`)
- **数据库文件**: `./data/app.db`
**变更内容**:
1. **依赖**: `postgres``better-sqlite3` + `@types/better-sqlite3`
2. **Drizzle 方言**: `dialect: 'postgresql'``dialect: 'sqlite'`
3. **Schema 导入**: `drizzle-orm/pg-core``drizzle-orm/sqlite-core`
4. **表定义**: `pgTable``sqliteTable`
5. **数据库连接**: `drizzle-orm/postgres-js``drizzle-orm/better-sqlite3`
6. **数据类型映射**:
- UUID: `uuid()``text()` (使用 `uuidv7()` 生成)
- 时间戳: `timestamp({ withTimezone: true })``integer({ mode: 'timestamp_ms' })`
- 布尔值: `boolean()``integer({ mode: 'boolean' })`
- 默认值: `defaultNow()``$defaultFn(() => new Date())`
7. **环境变量**: `DATABASE_URL` 从 URL 格式改为文件路径 (`./data/app.db`)
8. **目录自动创建**: 添加了数据库文件目录的自动创建逻辑
**数据库连接层变更**:
```typescript
// Before (PostgreSQL)
import { drizzle } from 'drizzle-orm/postgres-js'
export const createDB = () =>
drizzle({
connection: { url: env.DATABASE_URL, prepare: true },
schema,
})
// After (SQLite)
import { drizzle } from 'drizzle-orm/better-sqlite3'
import Database from 'better-sqlite3'
import { mkdirSync } from 'node:fs'
import { dirname } from 'node:path'
export const createDB = () => {
const dbPath = env.DATABASE_URL
if (dbPath !== ':memory:') {
mkdirSync(dirname(dbPath), { recursive: true })
}
const sqlite = new Database(dbPath)
return drizzle(sqlite, { schema })
}
```
**Schema 变更示例**:
```typescript
// Before (PostgreSQL)
import { pgTable, text, boolean, timestamp, uuid } from 'drizzle-orm/pg-core'
export const todoTable = pgTable('todo', {
id: uuid('id').primaryKey().$defaultFn(() => uuidv7()),
title: text('title').notNull(),
completed: boolean('completed').notNull().default(false),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
})
// After (SQLite)
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'
export const todoTable = sqliteTable('todo', {
id: text('id').primaryKey().$defaultFn(() => uuidv7()),
title: text('title').notNull(),
completed: integer('completed', { mode: 'boolean' }).notNull().default(false),
createdAt: integer('created_at', { mode: 'timestamp_ms' }).notNull().$defaultFn(() => new Date()),
})
```
**兼容性保证**:
- ✅ 所有业务逻辑保持不变
- ✅ ORPC 契约和类型完全兼容
- ✅ 应用层代码无需修改
- ✅ Drizzle 自动处理类型转换 (Date ↔ number, boolean ↔ 0/1)
**构建验证**:
-`bun typecheck` - 类型检查通过
-`bun fix` - 代码质量检查通过
-`bun run build` - Vite 构建成功
-`bun db:push` - 数据库初始化成功
- ✅ SQLite 文件已创建 (`./data/app.db`, 12KB)
**注意事项**:
- SQLite 是文件数据库,适合嵌入环境和开发环境
- 不支持多进程并发写入 (适合单应用实例)
- 性能足够支持中小型应用 (日请求 < 10万)
- 数据库文件和 WAL 文件 (`.db-shm`, `.db-wal`) 已添加到 `.gitignore`
- `better-sqlite3` 是原生模块,需要匹配生产环境的 OS 和架构 (构建输出会提示)
**Drizzle 时间戳模式**:
- `timestamp_ms`: Unix 毫秒时间戳 (推荐,精度更高)
- `timestamp`: Unix 秒时间戳
- Drizzle 自动转换 `Date` 对象 ↔ 数字
**命令对比**:
```bash
# 开发环境初始化 (推荐)
bun db:push
# 生产环境迁移
bun db:generate # 生成迁移文件
bun db:migrate # 执行迁移
# 数据库管理 UI
bun db:studio
```
### Git 工作流要求
**重要原则**:保持代码仓库与文档同步
当遇到技术问题、做出架构决策、或发现重要经验时:
1. **立即更新 AGENTS.md**:记录问题、原因、解决方案
2. **持续同步**:每次重大变更后更新文档
3. **版本关联**在文档中标注相关的库版本、commit hash
这确保未来的开发者(包括 AI 助手)能快速理解项目历史和技术选择。
---
**最后更新**: 2026-01-26
**项目版本**: 基于 package.json 依赖版本