Inbox Zero 的 Prisma 7 使用指南:统一从 @/generated/prisma 导入枚举与类型
2026/9/15 19:38:26 网站建设 项目流程

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执行查询、事务、扩展方法
枚举值,如ActionTypeSystemType@/generated/prisma/enums作为运行时的值使用(如条件判断、写入数据)
类型,如RulePrismaClientPrisma@/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;

这段代码揭示了几个关键实现事实:

  1. 驱动适配器模式:Prisma 7 采用 driver adapter 架构,通过PrismaPg(来自@prisma/adapter-pg)将连接字符串交给pg驱动。连接串优先级为PREVIEW_DATABASE_URL(预览/隔离环境)优先,回退到DATABASE_URL
  2. 扩展链($extends):实例依次叠加两个扩展——encryptedTokens用于敏感字段的透明加解密,auditPrismaQueries用于查询审计,具体见下文。
  3. 开发环境全局缓存if (env.NODE_ENV === "development") global.prisma = _prisma;借助 Node 全局对象复用实例,避免开发热重载时反复创建数据库连接;生产环境则不挂载全局变量。
  4. 类型回退:由于$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.sessionTokenEmailToken.token),因为随机 IV 加密会破坏WHERE等值查找;meetingRecording.meetingUrl之所以安全,是因为去重查询走的是normalizedMeetingUrlactiveKey字段,从不按原始链接检索。

查询审计扩展: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 设置、推荐系统与多组织关联等字段,AccountSession基于 Auth.js Prisma 适配器模型扩展而来,并加入了emailOtpactiveOrganization、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 相关的脚本包括:

脚本命令作用
postinstallprisma generate安装依赖后自动生成客户端,保证@/generated/prisma随时可用
buildprisma migrate deploy && ... next build生产构建前先应用迁移,再构建 Next.js
prisma:migrate:localdotenv -e .env.local -- prisma migrate deploy本地环境应用迁移
prisma:migrate:e2edotenv -e .env.e2e -- prisma migrate deployE2E 测试环境应用迁移
check-enumsnode scripts/check-enum-imports.js静态检查枚举导入规范

其中postinstallprisma generate意味着只要执行pnpm installapps/web/generated/prisma就会被重新生成,开发者在 Clone 仓库后无需手动生成即可运行代码。

用 check-enums 守住导入边界

为了把“从@/generated/prisma/client只导入类型、枚举一律走enums”的规范固化到 CI,仓库提供了 apps/web/scripts/check-enum-imports.js。其工作原理:

  1. 用 grep 扫描所有.ts/.tsx文件中from "@/generated/prisma/client"的导入语句;
  2. 归一化多行 import 为单行便于解析;
  3. 跳过import type { ... }纯类型导入;
  4. 在剩余导入中,用内置的PRISMA_ENUMS清单(含ActionTypeLogicalOperatorSystemTypeExecutedRuleStatusPremiumTierNewsletterStatusColdEmailStatusGroupItemTypeReferralStatusScheduledActionStatusDigestStatusFrequencyCleanActionThreadTrackerType共 14 个枚举)做负向前瞻匹配,揪出以形式从 client 导入的枚举;
  5. 发现违规即打印文件:行号、枚举名与导入语句,并以退出码 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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询