refactor: 统一表命名规范,简化 DB 单例

- 去掉所有 Drizzle 表变量的 Table 后缀(userTable→user 等)
- 修复 Better Auth adapter 找不到 schema model 的问题
- DB 实例从 IIFE 闭包工厂简化为模块级导出
- db middleware 重命名为 dbMiddleware 避免与 db 实例冲突
- 添加 babel-plugin-react-compiler 依赖
This commit is contained in:
2026-03-30 21:53:30 +08:00
parent 8c3425359d
commit 430c0b0c64
10 changed files with 76 additions and 86 deletions
+1
View File
@@ -55,6 +55,7 @@
"@tanstack/react-router-devtools": "catalog:", "@tanstack/react-router-devtools": "catalog:",
"@types/bun": "catalog:", "@types/bun": "catalog:",
"@vitejs/plugin-react": "catalog:", "@vitejs/plugin-react": "catalog:",
"babel-plugin-react-compiler": "^1.0.0",
"drizzle-kit": "catalog:", "drizzle-kit": "catalog:",
"nitro": "catalog:", "nitro": "catalog:",
"tailwindcss": "catalog:", "tailwindcss": "catalog:",
@@ -1,16 +1,16 @@
import { oc } from '@orpc/contract' import { oc } from '@orpc/contract'
import { createInsertSchema, createSelectSchema, createUpdateSchema } from 'drizzle-orm/zod' import { createInsertSchema, createSelectSchema, createUpdateSchema } from 'drizzle-orm/zod'
import { z } from 'zod' import { z } from 'zod'
import { bookmarkTable, categoryTable } from '@/modules/bookmarks/schema' import * as schema from '@/modules/bookmarks/schema'
import { generatedFieldKeys } from '@/server/db/fields' import { generatedFieldKeys } from '@/server/db/fields'
const categorySelect = createSelectSchema(categoryTable) const categorySelect = createSelectSchema(schema.category)
const categoryInsert = createInsertSchema(categoryTable).omit(generatedFieldKeys).omit({ userId: true }) const categoryInsert = createInsertSchema(schema.category).omit(generatedFieldKeys).omit({ userId: true })
const categoryUpdate = createUpdateSchema(categoryTable).omit(generatedFieldKeys).omit({ userId: true }) const categoryUpdate = createUpdateSchema(schema.category).omit(generatedFieldKeys).omit({ userId: true })
const bookmarkSelect = createSelectSchema(bookmarkTable) const bookmarkSelect = createSelectSchema(schema.bookmark)
const bookmarkInsert = createInsertSchema(bookmarkTable).omit(generatedFieldKeys).omit({ userId: true }) const bookmarkInsert = createInsertSchema(schema.bookmark).omit(generatedFieldKeys).omit({ userId: true })
const bookmarkUpdate = createUpdateSchema(bookmarkTable).omit(generatedFieldKeys).omit({ userId: true }) const bookmarkUpdate = createUpdateSchema(schema.bookmark).omit(generatedFieldKeys).omit({ userId: true })
export const category = { export const category = {
list: oc.input(z.void()).output(z.array(categorySelect.extend({ bookmarks: z.array(bookmarkSelect) }))), list: oc.input(z.void()).output(z.array(categorySelect.extend({ bookmarks: z.array(bookmarkSelect) }))),
+28 -28
View File
@@ -1,15 +1,15 @@
import { ORPCError } from '@orpc/server' import { ORPCError } from '@orpc/server'
import { and, eq } from 'drizzle-orm' import { and, eq } from 'drizzle-orm'
import { bookmarkTable, categoryTable } from '@/modules/bookmarks/schema' import * as schema from '@/modules/bookmarks/schema'
import { authMiddleware, db } from '@/server/api/middlewares' import { authMiddleware, dbMiddleware } from '@/server/api/middlewares'
import { os } from '@/server/api/server' import { os } from '@/server/api/server'
export const category = { export const category = {
list: os.bookmarks.category.list list: os.bookmarks.category.list
.use(db) .use(dbMiddleware)
.use(authMiddleware) .use(authMiddleware)
.handler(async ({ context }) => { .handler(async ({ context }) => {
return await context.db.query.categoryTable.findMany({ return await context.db.query.category.findMany({
where: { userId: context.user.id }, where: { userId: context.user.id },
orderBy: { orderId: 'asc' }, orderBy: { orderId: 'asc' },
with: { with: {
@@ -21,11 +21,11 @@ export const category = {
}), }),
create: os.bookmarks.category.create create: os.bookmarks.category.create
.use(db) .use(dbMiddleware)
.use(authMiddleware) .use(authMiddleware)
.handler(async ({ context, input }) => { .handler(async ({ context, input }) => {
const [created] = await context.db const [created] = await context.db
.insert(categoryTable) .insert(schema.category)
.values({ ...input, userId: context.user.id }) .values({ ...input, userId: context.user.id })
.returning() .returning()
if (!created) throw new ORPCError('INTERNAL_SERVER_ERROR', { message: 'Failed to create category' }) if (!created) throw new ORPCError('INTERNAL_SERVER_ERROR', { message: 'Failed to create category' })
@@ -33,39 +33,39 @@ export const category = {
}), }),
update: os.bookmarks.category.update update: os.bookmarks.category.update
.use(db) .use(dbMiddleware)
.use(authMiddleware) .use(authMiddleware)
.handler(async ({ context, input }) => { .handler(async ({ context, input }) => {
const [updated] = await context.db const [updated] = await context.db
.update(categoryTable) .update(schema.category)
.set(input.data) .set(input.data)
.where(and(eq(categoryTable.id, input.id), eq(categoryTable.userId, context.user.id))) .where(and(eq(schema.category.id, input.id), eq(schema.category.userId, context.user.id)))
.returning() .returning()
if (!updated) throw new ORPCError('NOT_FOUND') if (!updated) throw new ORPCError('NOT_FOUND')
return updated return updated
}), }),
remove: os.bookmarks.category.remove remove: os.bookmarks.category.remove
.use(db) .use(dbMiddleware)
.use(authMiddleware) .use(authMiddleware)
.handler(async ({ context, input }) => { .handler(async ({ context, input }) => {
const [deleted] = await context.db const [deleted] = await context.db
.delete(categoryTable) .delete(schema.category)
.where(and(eq(categoryTable.id, input.id), eq(categoryTable.userId, context.user.id))) .where(and(eq(schema.category.id, input.id), eq(schema.category.userId, context.user.id)))
.returning({ id: categoryTable.id }) .returning({ id: schema.category.id })
if (!deleted) throw new ORPCError('NOT_FOUND') if (!deleted) throw new ORPCError('NOT_FOUND')
}), }),
reorder: os.bookmarks.category.reorder reorder: os.bookmarks.category.reorder
.use(db) .use(dbMiddleware)
.use(authMiddleware) .use(authMiddleware)
.handler(async ({ context, input }) => { .handler(async ({ context, input }) => {
await context.db.transaction(async (tx) => { await context.db.transaction(async (tx) => {
for (const item of input) { for (const item of input) {
await tx await tx
.update(categoryTable) .update(schema.category)
.set({ orderId: item.orderId }) .set({ orderId: item.orderId })
.where(and(eq(categoryTable.id, item.id), eq(categoryTable.userId, context.user.id))) .where(and(eq(schema.category.id, item.id), eq(schema.category.userId, context.user.id)))
} }
}) })
}), }),
@@ -73,11 +73,11 @@ export const category = {
export const bookmark = { export const bookmark = {
create: os.bookmarks.bookmark.create create: os.bookmarks.bookmark.create
.use(db) .use(dbMiddleware)
.use(authMiddleware) .use(authMiddleware)
.handler(async ({ context, input }) => { .handler(async ({ context, input }) => {
const [created] = await context.db const [created] = await context.db
.insert(bookmarkTable) .insert(schema.bookmark)
.values({ ...input, userId: context.user.id }) .values({ ...input, userId: context.user.id })
.returning() .returning()
if (!created) throw new ORPCError('INTERNAL_SERVER_ERROR', { message: 'Failed to create bookmark' }) if (!created) throw new ORPCError('INTERNAL_SERVER_ERROR', { message: 'Failed to create bookmark' })
@@ -85,39 +85,39 @@ export const bookmark = {
}), }),
update: os.bookmarks.bookmark.update update: os.bookmarks.bookmark.update
.use(db) .use(dbMiddleware)
.use(authMiddleware) .use(authMiddleware)
.handler(async ({ context, input }) => { .handler(async ({ context, input }) => {
const [updated] = await context.db const [updated] = await context.db
.update(bookmarkTable) .update(schema.bookmark)
.set(input.data) .set(input.data)
.where(and(eq(bookmarkTable.id, input.id), eq(bookmarkTable.userId, context.user.id))) .where(and(eq(schema.bookmark.id, input.id), eq(schema.bookmark.userId, context.user.id)))
.returning() .returning()
if (!updated) throw new ORPCError('NOT_FOUND') if (!updated) throw new ORPCError('NOT_FOUND')
return updated return updated
}), }),
remove: os.bookmarks.bookmark.remove remove: os.bookmarks.bookmark.remove
.use(db) .use(dbMiddleware)
.use(authMiddleware) .use(authMiddleware)
.handler(async ({ context, input }) => { .handler(async ({ context, input }) => {
const [deleted] = await context.db const [deleted] = await context.db
.delete(bookmarkTable) .delete(schema.bookmark)
.where(and(eq(bookmarkTable.id, input.id), eq(bookmarkTable.userId, context.user.id))) .where(and(eq(schema.bookmark.id, input.id), eq(schema.bookmark.userId, context.user.id)))
.returning({ id: bookmarkTable.id }) .returning({ id: schema.bookmark.id })
if (!deleted) throw new ORPCError('NOT_FOUND') if (!deleted) throw new ORPCError('NOT_FOUND')
}), }),
reorder: os.bookmarks.bookmark.reorder reorder: os.bookmarks.bookmark.reorder
.use(db) .use(dbMiddleware)
.use(authMiddleware) .use(authMiddleware)
.handler(async ({ context, input }) => { .handler(async ({ context, input }) => {
await context.db.transaction(async (tx) => { await context.db.transaction(async (tx) => {
for (const item of input) { for (const item of input) {
await tx await tx
.update(bookmarkTable) .update(schema.bookmark)
.set({ orderId: item.orderId }) .set({ orderId: item.orderId })
.where(and(eq(bookmarkTable.id, item.id), eq(bookmarkTable.userId, context.user.id))) .where(and(eq(schema.bookmark.id, item.id), eq(schema.bookmark.userId, context.user.id)))
} }
}) })
}), }),
+6 -6
View File
@@ -1,8 +1,8 @@
import { boolean, integer, pgTable, text, uuid } from 'drizzle-orm/pg-core' import { boolean, integer, pgTable, text, uuid } from 'drizzle-orm/pg-core'
import { userTable } from '../../server/auth/schema' import { user } from '../../server/auth/schema'
import { generatedFields } from '../../server/db/fields' import { generatedFields } from '../../server/db/fields'
export const categoryTable = pgTable('category', { export const category = pgTable('category', {
...generatedFields, ...generatedFields,
name: text('name').notNull(), name: text('name').notNull(),
isPinned: boolean('is_pinned').notNull().default(false), isPinned: boolean('is_pinned').notNull().default(false),
@@ -10,20 +10,20 @@ export const categoryTable = pgTable('category', {
orderId: integer('order_id').notNull().default(0), orderId: integer('order_id').notNull().default(0),
userId: text('user_id') userId: text('user_id')
.notNull() .notNull()
.references(() => userTable.id, { onDelete: 'cascade' }), .references(() => user.id, { onDelete: 'cascade' }),
}) })
export const bookmarkTable = pgTable('bookmark', { export const bookmark = pgTable('bookmark', {
...generatedFields, ...generatedFields,
name: text('name').notNull(), name: text('name').notNull(),
url: text('url').notNull(), url: text('url').notNull(),
icon: text('icon'), icon: text('icon'),
categoryId: uuid('category_id') categoryId: uuid('category_id')
.notNull() .notNull()
.references(() => categoryTable.id, { onDelete: 'cascade' }), .references(() => category.id, { onDelete: 'cascade' }),
isPublic: boolean('is_public').notNull().default(true), isPublic: boolean('is_public').notNull().default(true),
orderId: integer('order_id').notNull().default(0), orderId: integer('order_id').notNull().default(0),
userId: text('user_id') userId: text('user_id')
.notNull() .notNull()
.references(() => userTable.id, { onDelete: 'cascade' }), .references(() => user.id, { onDelete: 'cascade' }),
}) })
@@ -1,11 +1,11 @@
import { os } from '@/server/api/server' import { os } from '@/server/api/server'
import { getDB } from '@/server/db' import { db } from '@/server/db'
export const db = os.middleware(async ({ context, next }) => { export const dbMiddleware = os.middleware(async ({ context, next }) => {
return next({ return next({
context: { context: {
...context, ...context,
db: getDB(), db,
}, },
}) })
}) })
+2 -2
View File
@@ -3,12 +3,12 @@ import { drizzleAdapter } from 'better-auth/adapters/drizzle'
import { tanstackStartCookies } from 'better-auth/tanstack-start' import { tanstackStartCookies } from 'better-auth/tanstack-start'
import { env } from '@/env' import { env } from '@/env'
import * as authSchema from '@/server/auth/schema' import * as authSchema from '@/server/auth/schema'
import { getDB } from '@/server/db' import { db } from '@/server/db'
export const auth = betterAuth({ export const auth = betterAuth({
baseURL: env.BETTER_AUTH_URL, baseURL: env.BETTER_AUTH_URL,
secret: env.BETTER_AUTH_SECRET, secret: env.BETTER_AUTH_SECRET,
database: drizzleAdapter(getDB(), { database: drizzleAdapter(db, {
provider: 'pg', provider: 'pg',
schema: authSchema, schema: authSchema,
}), }),
+6 -6
View File
@@ -7,7 +7,7 @@ import { boolean, pgTable, text, timestamp } from 'drizzle-orm/pg-core'
* 不使用项目的 generatedFieldsUUID v7)。 * 不使用项目的 generatedFieldsUUID v7)。
*/ */
export const userTable = pgTable('user', { export const user = pgTable('user', {
id: text('id').primaryKey(), id: text('id').primaryKey(),
name: text('name').notNull(), name: text('name').notNull(),
email: text('email').notNull().unique(), email: text('email').notNull().unique(),
@@ -20,7 +20,7 @@ export const userTable = pgTable('user', {
.$onUpdateFn(() => new Date()), .$onUpdateFn(() => new Date()),
}) })
export const sessionTable = pgTable('session', { export const session = pgTable('session', {
id: text('id').primaryKey(), id: text('id').primaryKey(),
expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(), expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),
token: text('token').notNull().unique(), token: text('token').notNull().unique(),
@@ -33,16 +33,16 @@ export const sessionTable = pgTable('session', {
userAgent: text('user_agent'), userAgent: text('user_agent'),
userId: text('user_id') userId: text('user_id')
.notNull() .notNull()
.references(() => userTable.id, { onDelete: 'cascade' }), .references(() => user.id, { onDelete: 'cascade' }),
}) })
export const accountTable = pgTable('account', { export const account = pgTable('account', {
id: text('id').primaryKey(), id: text('id').primaryKey(),
accountId: text('account_id').notNull(), accountId: text('account_id').notNull(),
providerId: text('provider_id').notNull(), providerId: text('provider_id').notNull(),
userId: text('user_id') userId: text('user_id')
.notNull() .notNull()
.references(() => userTable.id, { onDelete: 'cascade' }), .references(() => user.id, { onDelete: 'cascade' }),
accessToken: text('access_token'), accessToken: text('access_token'),
refreshToken: text('refresh_token'), refreshToken: text('refresh_token'),
idToken: text('id_token'), idToken: text('id_token'),
@@ -57,7 +57,7 @@ export const accountTable = pgTable('account', {
.$onUpdateFn(() => new Date()), .$onUpdateFn(() => new Date()),
}) })
export const verificationTable = pgTable('verification', { export const verification = pgTable('verification', {
id: text('id').primaryKey(), id: text('id').primaryKey(),
identifier: text('identifier').notNull(), identifier: text('identifier').notNull(),
value: text('value').notNull(), value: text('value').notNull(),
+5 -19
View File
@@ -2,23 +2,9 @@ import { drizzle } from 'drizzle-orm/postgres-js'
import { env } from '@/env' import { env } from '@/env'
import { relations } from '@/server/db/relations' import { relations } from '@/server/db/relations'
export const createDB = () => export const db = drizzle({
drizzle({ connection: env.DATABASE_URL,
connection: env.DATABASE_URL, relations,
relations, })
})
export type DB = ReturnType<typeof createDB> export type DB = typeof db
export const getDB = (() => {
let db: DB | null = null
return (singleton = true): DB => {
if (!singleton) {
return createDB()
}
db ??= createDB()
return db
}
})()
+15 -15
View File
@@ -2,25 +2,25 @@ import { defineRelations } from 'drizzle-orm'
import * as schema from './schema' import * as schema from './schema'
export const relations = defineRelations(schema, (r) => ({ export const relations = defineRelations(schema, (r) => ({
userTable: { user: {
categories: r.many.categoryTable(), categories: r.many.category(),
bookmarks: r.many.bookmarkTable(), bookmarks: r.many.bookmark(),
}, },
categoryTable: { category: {
user: r.one.userTable({ user: r.one.user({
from: r.categoryTable.userId, from: r.category.userId,
to: r.userTable.id, to: r.user.id,
}), }),
bookmarks: r.many.bookmarkTable(), bookmarks: r.many.bookmark(),
}, },
bookmarkTable: { bookmark: {
user: r.one.userTable({ user: r.one.user({
from: r.bookmarkTable.userId, from: r.bookmark.userId,
to: r.userTable.id, to: r.user.id,
}), }),
category: r.one.categoryTable({ category: r.one.category({
from: r.bookmarkTable.categoryId, from: r.bookmark.categoryId,
to: r.categoryTable.id, to: r.category.id,
}), }),
}, },
})) }))
+3
View File
@@ -46,6 +46,7 @@
"@tanstack/react-router-devtools": "catalog:", "@tanstack/react-router-devtools": "catalog:",
"@types/bun": "catalog:", "@types/bun": "catalog:",
"@vitejs/plugin-react": "catalog:", "@vitejs/plugin-react": "catalog:",
"babel-plugin-react-compiler": "^1.0.0",
"drizzle-kit": "catalog:", "drizzle-kit": "catalog:",
"nitro": "catalog:", "nitro": "catalog:",
"tailwindcss": "catalog:", "tailwindcss": "catalog:",
@@ -537,6 +538,8 @@
"babel-dead-code-elimination": ["babel-dead-code-elimination@1.0.12", "", { "dependencies": { "@babel/core": "^7.23.7", "@babel/parser": "^7.23.6", "@babel/traverse": "^7.23.7", "@babel/types": "^7.23.6" } }, "sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig=="], "babel-dead-code-elimination": ["babel-dead-code-elimination@1.0.12", "", { "dependencies": { "@babel/core": "^7.23.7", "@babel/parser": "^7.23.6", "@babel/traverse": "^7.23.7", "@babel/types": "^7.23.6" } }, "sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig=="],
"babel-plugin-react-compiler": ["babel-plugin-react-compiler@1.0.0", "", { "dependencies": { "@babel/types": "^7.26.0" } }, "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw=="],
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.12", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-qyq26DxfY4awP2gIRXhhLWfwzwI+N5Nxk6iQi8EFizIaWIjqicQTE4sLnZZVdeKPRcVNoJOkkpfzoIYuvCKaIQ=="], "baseline-browser-mapping": ["baseline-browser-mapping@2.10.12", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-qyq26DxfY4awP2gIRXhhLWfwzwI+N5Nxk6iQi8EFizIaWIjqicQTE4sLnZZVdeKPRcVNoJOkkpfzoIYuvCKaIQ=="],