feat: 迁移数据库至 SQLite 并新增项目文档

- 将 Postgres 数据库替换为 SQLite
- 并同步添加 README 文档以优化项目初始化流程
This commit is contained in:
2026-01-20 16:56:11 +08:00
parent 5513979ebc
commit b967deb4b1
14 changed files with 482 additions and 78 deletions

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() {
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>

View File

@@ -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()),
})

View File

@@ -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(),

View File

@@ -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: {

View File

@@ -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({

View File

@@ -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 }) => {

View File

@@ -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()