Inbox Zero 的 Prisma 7 使用指南:统一从 @/generated/prisma 导入枚举与类型
【免费下载链接】inbox-zeroThe world's best AI personal assistant for email. Open source app to help you reach inbox zero fast.项目地址: https://gitcode.com/GitHub_Trending/in/inbox-zero
Inbox Zero 是一款开源的 AI 邮件助手,其 Web 应用(apps/web)基于 PostgreSQL 与 Prisma 7 构建数据访问层。本篇指南以仓库内的 .claude/skills/prisma/SKILL.md 为骨架,结合 apps/web/utils/prisma.ts、apps/web/prisma/schema.prisma、apps/web/scripts/check-enum-imports.js 等源码,完整讲解该项目的 Prisma 导入规范、客户端初始化方式、代码生成配置与迁移流程。读完本文,你将掌握在 Inbox Zero 代码库中正确、安全地使用 Prisma 的全部要点,并能理解这些约定背后的工程原因。
技术栈与文档约定
Inbox Zero 的数据层采用PostgreSQL + Prisma 7,这一点在技能文档开头即被明确声明。与之配套的关键事实还包括:
- Prisma 运行时依赖锁定为
prisma@7.10.0与@prisma/client@7.10.0,并引入@prisma/adapter-pg@7.10.0作为 PostgreSQL 驱动适配器(见 apps/web/package.json); - 数据库 Schema 的权威位置是 apps/web/prisma/schema.prisma,该文件共 2686 行,覆盖用户、账户、邮件、规则、订阅、组织等全部业务模型;
- 所有 Prisma 相关代码的导入入口被严格限定,这是本仓库最核心的工程约定,下文详细展开。
三大导入来源:实例、枚举与类型
技能文档将 Prisma 的导入划分为三类,每一类都有唯一指定的来源模块,任何业务代码、测试代码都必须遵守:
// Prisma client 实例(单例) import prisma from "@/utils/prisma"; // 枚举(NOT from @prisma/client) import { ActionType, SystemType } from "@/generated/prisma/enums"; // 类型(NOT from @prisma/client) import type { Rule, PrismaClient } from "@/generated/prisma/client"; import { Prisma } from "@/generated/prisma/client";对应的三类模块职责如下:
| 导入内容 | 来源模块 | 用途 |
|---|---|---|
客户端实例prisma(默认导出) | @/utils/prisma | 执行查询、事务、扩展方法 |
枚举值,如ActionType、SystemType | @/generated/prisma/enums | 作为运行时的值使用(如条件判断、写入数据) |
类型,如Rule、PrismaClient、Prisma | @/generated/prisma/client | 仅用于类型标注与类型推导 |
值得注意:枚举也可以从@/generated/prisma/client导入,但只有类型才能这样做。文档明确要求枚举一律走enums入口,原因在下一节说明。
为什么严禁从 @prisma/client 导入
技能文档用加粗强调的方式给出了铁律:
Never import from
@prisma/client— always use@/generated/prisma/enumsand@/generated/prisma/client.
这条规则的根源在于Next.js 打包(bundling)问题。仓库中的校验脚本 apps/web/scripts/check-enum-imports.js 在文件头部注释里给出了完整解释:
Importing Prisma enums from @/generated/prisma/client causes Next.js bundling errors in production when used in client components. This happens because Prisma Client depends on Node.js modules that can't be bundled for the browser.
即:@prisma/client(以及与其等价的@/generated/prisma/client运行时导出)依赖 Node.js 原生模块,无法被浏览器端打包器处理;若在客户端组件中以值的形式导入枚举,生产构建就会失败。而@/generated/prisma/enums是 Prisma 自动生成的纯 TypeScript 枚举,天然适合客户端使用。
从代码结构看,这种拆分的设计思路是:
- 纯类型导入(
import type ...)在编译后会被完全擦除,不会残留任何运行时依赖,因此从@/generated/prisma/client导入类型是安全的; - 枚举值导入会留下真实的运行时代码,一旦进入客户端 bundle 就会触发 Node.js 模块解析错误,所以必须从独立的
enums文件导入; @/utils/prisma是服务端专属的客户端实例封装,默认仅被 Server Components、Server Actions 与 API 路由使用,天然不会进入客户端 bundle。
路径别名@/*在 apps/web/tsconfig.json 中被映射为./*(即apps/web目录),因此@/generated/prisma/...实际指向apps/web/generated/prisma/...,@/utils/prisma指向apps/web/utils/prisma.ts。
客户端实例的创建与扩展(源码解析)
技能文档指明的@/utils/prisma模块在 apps/web/utils/prisma.ts 中实现,完整还原如下:
import { PrismaPg } from "@prisma/adapter-pg"; import { env } from "@/env"; import { PrismaClient } from "@/generated/prisma/client"; import { encryptedTokens } from "@/utils/prisma-extensions"; import { auditPrismaQueries } from "@/utils/audit/prisma-extension"; declare global { var prisma: PrismaClient | undefined; } // Create the Prisma client with extensions, but cast it back to PrismaClient for type compatibility const _prisma = global.prisma || (new PrismaClient({ adapter: new PrismaPg({ connectionString: env.PREVIEW_DATABASE_URL ?? env.DATABASE_URL, }), }) .$extends(encryptedTokens) .$extends(auditPrismaQueries) as unknown as PrismaClient); if (env.NODE_ENV === "development") global.prisma = _prisma; export default _prisma;这段代码揭示了几个关键实现事实:
- 驱动适配器模式:Prisma 7 采用 driver adapter 架构,通过
PrismaPg(来自@prisma/adapter-pg)将连接字符串交给pg驱动。连接串优先级为PREVIEW_DATABASE_URL(预览/隔离环境)优先,回退到DATABASE_URL。 - 扩展链($extends):实例依次叠加两个扩展——
encryptedTokens用于敏感字段的透明加解密,auditPrismaQueries用于查询审计,具体见下文。 - 开发环境全局缓存:
if (env.NODE_ENV === "development") global.prisma = _prisma;借助 Node 全局对象复用实例,避免开发热重载时反复创建数据库连接;生产环境则不挂载全局变量。 - 类型回退:由于
$extends返回的是扩展后的类型,代码通过as unknown as PrismaClient将其收敛为统一的PrismaClient类型,保证全项目调用方看到的 API 一致。
透明加密扩展:encryptedTokens
apps/web/utils/prisma-extensions.ts 中定义了encryptedTokens扩展。它以一张字段清单(ENCRYPTED_FIELDS)驱动,覆盖 OAuth Token、API Key 等敏感数据:
const ENCRYPTED_FIELDS = { account: ["access_token", "refresh_token"], calendarConnection: ["accessToken", "refreshToken"], driveConnection: ["accessToken", "refreshToken"], messagingChannel: ["accessToken", "refreshToken"], mcpConnection: ["accessToken", "refreshToken", "apiKey"], mcpIntegration: ["oauthClientSecret"], meetingRecording: ["meetingUrl"], user: ["aiApiKey", "webhookSecret"], } as const satisfies Record<string, readonly string[]>;其实现机制值得注意:
- 写入时加密:通过
query拦截create/update/updateMany/upsert四个操作,在调用底层查询前用encryptToken()对字段值做随机 IV 加密;对update系列还额外兼容了{ set: ... }包装结构; - 读取时解密:通过
result计算属性在读出记录时调用decryptToken()还原明文; - 使用边界明确:注释特别警告——不要给会被按值查询的字段开启加密(如
Session.sessionToken、EmailToken.token),因为随机 IV 加密会破坏WHERE等值查找;meetingRecording.meetingUrl之所以安全,是因为去重查询走的是normalizedMeetingUrl和activeKey字段,从不按原始链接检索。
查询审计扩展:auditPrismaQueries
客户端叠加的第二个扩展来自@/utils/audit/prisma-extension,用于对 Prisma 查询执行审计,与仓库中的审计体系(apps/web/utils/audit/目录)配套,为邮件自动化操作(规则执行、归档、标记等)提供可追溯的查询记录。
Schema 与代码生成配置
apps/web/prisma/schema.prisma 顶部的配置决定了生成代码的形态:
datasource db { provider = "postgresql" } generator client { provider = "prisma-client" output = "../generated/prisma" generatedFileExtension = "ts" importFileExtension = "ts" }解读如下:
provider = "postgresql":数据源锁定 PostgreSQL;provider = "prisma-client":使用 Prisma 7 的prisma-client 生成器(区别于旧版prisma-client-js),生成的代码是带类型定义的客户端源码而非打包产物;output = "../generated/prisma":生成目录为apps/web/generated/prisma(相对apps/web/prisma上跳一级),这正是@/generated/prisma别名指向的位置;generatedFileExtension = "ts"与importFileExtension = "ts":生成物及其内部导入均使用.ts扩展名,与@/generated/prisma/enums、@/generated/prisma/client这两个模块入口一一对应。
Schema 文件中还可看到 Prisma 对项目业务模型的完整覆盖,例如User模型承载了登录、调查问卷、AI 设置、推荐系统与多组织关联等字段,Account、Session基于 Auth.js Prisma 适配器模型扩展而来,并加入了emailOtp、activeOrganization、OAuth Token 记录等 Inbox Zero 特有字段。
迁移与工程化脚本
prisma.config.ts:迁移专用配置
apps/web/prisma.config.ts 是 Prisma 7 独立的配置文件(defineConfig来自prisma/config):
const migrationUrl = process.env.PREVIEW_DATABASE_URL_UNPOOLED || process.env.PREVIEW_DATABASE_URL || process.env.DIRECT_URL || process.env.DATABASE_URL_UNPOOLED || process.env.DATABASE_URL; export default defineConfig({ schema: "./prisma/schema.prisma", datasource: { url: migrationUrl, }, migrations: { path: "./prisma/migrations", }, });它把 Schema 路径固定为./prisma/schema.prisma,迁移目录固定为./prisma/migrations(仓库中已有 271 个 SQL 迁移文件),并为迁移工具链提供独立于运行时连接串的 URL 解析优先级:先是PREVIEW_DATABASE_URL_UNPOOLED,再依次回退到预览地址、DIRECT_URL、非池化地址与默认地址。运行时实例与迁移工具使用不同的连接配置,正是为了在连接池(PgBouncer 等)环境下保证 DDL 迁移可用。
package.json 中的 Prisma 脚本
apps/web/package.json 中与 Prisma 相关的脚本包括:
| 脚本 | 命令 | 作用 |
|---|---|---|
postinstall | prisma generate | 安装依赖后自动生成客户端,保证@/generated/prisma随时可用 |
build | prisma migrate deploy && ... next build | 生产构建前先应用迁移,再构建 Next.js |
prisma:migrate:local | dotenv -e .env.local -- prisma migrate deploy | 本地环境应用迁移 |
prisma:migrate:e2e | dotenv -e .env.e2e -- prisma migrate deploy | E2E 测试环境应用迁移 |
check-enums | node scripts/check-enum-imports.js | 静态检查枚举导入规范 |
其中postinstall的prisma generate意味着只要执行pnpm install,apps/web/generated/prisma就会被重新生成,开发者在 Clone 仓库后无需手动生成即可运行代码。
用 check-enums 守住导入边界
为了把“从@/generated/prisma/client只导入类型、枚举一律走enums”的规范固化到 CI,仓库提供了 apps/web/scripts/check-enum-imports.js。其工作原理:
- 用 grep 扫描所有
.ts/.tsx文件中from "@/generated/prisma/client"的导入语句; - 归一化多行 import 为单行便于解析;
- 跳过
import type { ... }纯类型导入; - 在剩余导入中,用内置的
PRISMA_ENUMS清单(含ActionType、LogicalOperator、SystemType、ExecutedRuleStatus、PremiumTier、NewsletterStatus、ColdEmailStatus、GroupItemType、ReferralStatus、ScheduledActionStatus、DigestStatus、Frequency、CleanAction、ThreadTrackerType共 14 个枚举)做负向前瞻匹配,揪出以值形式从 client 导入的枚举; - 发现违规即打印
文件:行号、枚举名与导入语句,并以退出码 1 中断流程。
脚本头部注释中还给出了判断矩阵:从 client 导入枚举值(含与类型混导)会被拦截,从enums导入枚举、以及从 client 做纯类型导入则被放行。这意味着即使开发者误写了 import,CI 也会在合并前给出明确修复提示(import { ActionType } from "@/generated/prisma/enums")。
实践要点速查
- 写查询:
import prisma from "@/utils/prisma",直接使用该单例执行 CRUD、事务($transaction)与扩展方法; - 用枚举:
import { ActionType, SystemType } from "@/generated/prisma/enums",可以安全地在客户端组件与服务端代码中作为值使用; - 标注类型:
import type { Rule, PrismaClient } from "@/generated/prisma/client"或import { Prisma } from "@/generated/prisma/client"(仅用于类型上下文,如Prisma.UserWhereInput); - 改模型:编辑 apps/web/prisma/schema.prisma,然后运行
prisma migrate dev(本地开发)或pnpm prisma:migrate:local(应用既有迁移),迁移文件统一落在 apps/web/prisma/migrations; - 加敏感字段:若新增字段需要落库加密,在 apps/web/utils/prisma-extensions.ts 的
ENCRYPTED_FIELDS中登记,并遵守“不可按值查询”的限制; - 回归检查:提交前运行
pnpm check-enums(在apps/web下),或在 CI 中保留该步骤,防止枚举导入回退到@/generated/prisma/client。
以上约定共同保证了 Inbox Zero 在 Prisma 7 时代既能享受类型安全的数据库访问,又不会让服务端依赖泄漏进浏览器 bundle——这也是本项目将“导入规范”作为 Prisma 技能文档核心内容的根本原因。若需进一步深入,可继续阅读 apps/web/utils/audit/prisma-extension.ts 了解查询审计扩展,或浏览 apps/web/prisma/migrations 了解项目真实的 Schema 演进历史。
【免费下载链接】inbox-zeroThe world's best AI personal assistant for email. Open source app to help you reach inbox zero fast.项目地址: https://gitcode.com/GitHub_Trending/in/inbox-zero
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考