feat: 迁移数据库至 SQLite 并新增项目文档
- 将 Postgres 数据库替换为 SQLite - 并同步添加 README 文档以优化项目初始化流程
This commit is contained in:
@@ -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